php反序列化小记


函数方法

is_a方法

自动触发__ autoload 方法,触发php类得自动加载机制

用在条件语句中就是,判断是不是某个类的实例,

在 PHP 5 中使用 instanceof 运算符

count方法

  • php7.2以下可传参类型为int string array object
  • php7.2以上产参类型为array object

__wakeup方法

绕过和版本差异

  • 在特定的php版本可被绕过(CVE-2016-7124)
  • 在php7以下异常类LogicException会对反序列化中的参数进行清空

方法调用

demo

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
<?php
class A{
private $a;

public function __construct()
{
$this->a = "a123";
}
public function __call($name, $arguments)
{
return call_user_func_array(array($this->a,$name),$arguments);
}
public function test(){
return "this class is A!";
}
}

class B{
private $b;
public function __construct()
{
$this->b = $this;
}
public function __destruct()
{
$this->close();// TODO: Implement __destruct() method.
}
public function aaa(){}
public function test(){
return "this class is B!";
}
public function close(){
$this->b->aaa();
$c = $this->b->test();
echo $c;
}
}

class C{
private $c;
public function __construct()
{
$this->c = $this;
}
public function test(){
return "this class is C!";
}
}

$a = $_POST['a'];
unserialize($a);

当直接调用C类中不存在的方法时,这是致命错误,程序GG

1605841586889

当通过__call方法去调用时,waring,程序继续执行

1605841674100

常量和静态变量

const和define

const是在类里面声明的,可以在类中序列化,通过self::形式会被调用

define定义的常量也可以被序列化,但是不能在类中声明,也就是利用链里面的如果有这种常量座位关键点,那么基本就断了

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
<?php
class A{
private $a;
const A = "11111111";
protected static $b = "no no no !";

public function __construct()
{
$this->a = "a123";
}
public function __destruct()
{
echo $this->a."<br>";
echo self::$b."<br>";
echo self::A;
}
}

$a = $_POST['a'];
unserialize($a);

....
//序列化
class A{
private $a;

const A = "a";
protected static $b;
public function __construct()
{
$this->a = "a: yes";
self::$b = "yes yes yes!";
}
}


$b = new A();
echo str_replace('+', '%20', urlencode(serialize($b)));

输出:

1
2
3
a: yes
no no no !
11111111

结论:

  • 可操作对象只有动态变量
  • 序列化保存的是对象的状态,静态变量数以类的状态,因此序列化并不保存静态变量。
  • 这里的不能序列化的意思,是序列化信息中不包含这个静态成员域

资源类型

碰到过几次,这是个不能被序列化的类型,代表的是一种状态,强行序列化会得到int类型

测试代码

1
2
$fp = @fopen("D:\phpstudy\\1.txt",'rb');
echo serialize($fp);

1605842922243

那么,牵扯到资源的类型的怎么做

  • 通过其他方法,初始化一个资源

思考:在用户层面如果有调用,在调用之后的序列化是否可用

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
class D{
private $fp;
private $d;
public function __construct()
{
$this->fp = @fopen("D:\phpstudy\\1.txt",'rb');
$this->d = new B();
}

public function __wakeup()
{
if(is_resource($this->fp)){
$a = $this->d->test();
echo $a;
}else{
$a = $this->test();
echo $a;
}
}
public function test(){
return "this class is D!";
}
}

$d = new D();
$a = $_POST['a'];
unserialize($a);

结论:不可用,这是两个不同的实例,互不干涉

1605844211574

解决了另外的一个问题,如果程序的反序列化利用链牵扯到了,程序运行所必须加载的类,能不能对这个类重新赋值的问题

  • 互不干涉

php的反射调用亦是入此,虽然加载了类最后会触发析构方法,但是因为是不通的实例,只不过是多触发一次析构方法罢了

方法回调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//调用静态方法
call_user_func_array(["A","test1"],["aaa"]);
//调用动态方法
call_user_func_array([new A(),"test1"],["aaa"]);
...
//静态
call_user_func(["A","test1"],"aaa");
call_user_func("A::test1","aaa");
//动态
call_user_func([new A(),"test1"],"aaa");
...
//动态
$a = [new A(),"test"];
$a();
//静态
$b = "A::test1";
$b();

触发任意类任意方法

  • call_user_func\(\$this->[a-z_]{1,},
  • cal_user_func_array\(array\(\$this->[a-z_]{1,},\$this->[a-z_]{1,}\),
  • \$[a-z_]{1,}\(
  • \(\$this->[a-z_]{1,}\)\(

节点的寻找

  • 当我们苦苦寻找一个可以调用任意类任意方法时,一个调用固定方法的的点可能会成为突破口,当然也是call_user_func方法调用固定的方法
  • 同样,N多方法不可控的情况,一个全新方法的加如,可能就会造成一个漏洞

两次失败的利用链

败在is_resource的利用链

PHPMailer第三方库

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
113
114
115
116
//PHPMailer的destruct入口
public function __destruct()
{
//Close any open SMTP connection nicely
$this->smtpClose();
}
...

//进入smtp实例
public function smtpClose()
{
if (is_a($this->smtp, 'SMTP')) {
if ($this->smtp->connected()) {
$this->smtp->quit();
$this->smtp->close();
}
}
}
...

//断点,调用资源判断
public function connected()
{
if (is_resource($this->smtp_conn)) {
$sock_status = stream_get_meta_data($this->smtp_conn);
if ($sock_status['eof']) {
// The socket is valid but we are not connected
$this->edebug(
'SMTP NOTICE: EOF caught while checking if connected',
self::DEBUG_CLIENT
);
$this->close();
return false;
}
return true; // everything looks good
}
return false;
}
//会发现其实存在edebug的调用
//close方法中也存在调用,quit同样存在,所以什么结果无所谓,就是不是个资源
//进入edebug
protected function edebug($str, $level = 0)
{
if ($level > $this->do_debug) {
return;
}
//Avoid clash with built-in function names
if (!in_array($this->Debugoutput, array('error_log', 'html', 'echo')) and is_callable($this->Debugoutput)) {
call_user_func($this->Debugoutput, $str, $level);
return;
}
switch ($this->Debugoutput) {...}
}
...
//发现了可以调用任意类的任意方法,但是参数均不可控
//寻找一个不需要参数的终点,或者让参数变得可控
//找到一个之前很容易就会不看的点 此时参数完全可控但是类和方法变得不可控了
public function preSend()
{
try {
$this->error_count = 0; // Reset errors
$this->mailHeader = '';

// Dequeue recipient and Reply-To addresses with IDN
foreach (array_merge($this->RecipientsQueue, $this->ReplyToQueue) as $params) {
$params[1] = $this->punyencodeAddress($params[1]);
call_user_func_array(array($this, 'addAnAddress'), $params);
}
...
}
}

//传说中的柳暗花明,有一次调用edebug
protected function addAnAddress($kind, $address, $name = '')
{
if (!in_array($kind, array('to', 'cc', 'bcc', 'Reply-To'))) {
$error_message = $this->lang('Invalid recipient kind: ') . $kind;
$this->setError($error_message);
$this->edebug($error_message);
...
}
...
}
//lang方法返回的值完全可控,再拼接一个可控参数,此时参数完全可控
protected function lang($key)
{
if (count($this->language) < 1) {
$this->setLanguage('en'); // set the default language
}

if (array_key_exists($key, $this->language)) {
if ($key == 'smtp_connect_failed') {
...
}
return $this->language[$key];
} else {
//Return the key as a fallback
return $key;
}
}
//刚才的时smtp的edebug,进入PHPMailer的edebug
protected function edebug($str)
{
if ($this->SMTPDebug <= 0) {
return;
}
//Avoid clash with built-in function names
if (!in_array($this->Debugoutput, array('error_log', 'html', 'echo')) and is_callable($this->Debugoutput)) {
call_user_func($this->Debugoutput, $str, $this->SMTPDebug);
return;
}
...
}

//至此调用函数,参数完全可控
//败笔就是资源类型的判断,权当学习了

phpexcle第三方库

没有合适中间节点,不列举了

主要是两点

  • 静态变量无法序列化
  • call_user_func调用不存在方法,不是致命错误

存在close不存在exec

1
2
3
4
5
6
7
8
public function __destruct()
{
if (!is_null($this->DBHandle)) {
$this->DBHandle->exec('DROP TABLE kvp_'.$this->TableName);
$this->DBHandle->close();
}
$this->DBHandle = null;
}

任意文件删除

1
2
3
4
5
6
7
public function __destruct()
{
// Unlink temporary files
if ($this->tempFileName != '') {
@unlink($this->tempFileName);
}
}