君は春の中にいる、かけがえのない春の中にいる.

你驻足于春色中,于那独一无二的春色之中.

PHP类继承与全局变量组合造成的漏洞分析

某CMS改动后又给自己挖坑了

0x00 起因

组里的师弟在学习代码审计时遇到了某CMS的一个XSS漏洞,问题在于老版本是没有这个漏洞的,而且看起来也调用了相关的过滤函数,但是XSS还是触发了。

经过讨论分析,发现了其中的奥妙。

0x01 PHP类继承

面向对象的特性就不在多说明了,直接看以下示例代码。

我们首先写一个基础类文件 base.class.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class base{
public $name;
public function __construct(){
echo 'base construct<br>';
$this->filter($this->name);

}

protected function filter(){
$this->name=$_GET['name'];
echo 'SQL filter<br>';
}
}
?>

base类显式定义一个构造器,调用自己的过滤函数,假设该过滤函数为SQL注入过滤,由于和我们讨论的问题无关,这里就不在具体写filter函数的代码。

接下来是测试函数 test.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
<?php
require_once('base.class.php');

class web1 extends base{
public function __construct(){
parent::__construct();
echo 'web1 construct<br>';
}

protected function filter(){
parent::filter();
$this->name=$_GET['name'];
$this->name = htmlspecialchars($this->name);
echo 'XSS filter<br>';
}
}

class web2 extends web1{
public function __construct(){
parent::__construct();
require_once('web3.class.php');
$web3 = new web3;
echo 'web2 construct<br>';
}

public function show(){
echo 'show<br>';
echo $this->name;
}

}

$web2 = new web2;
$web2->show();
?>

解释一下这段代码,web1类继承自base类,并且重写了父类中的过滤函数,此时的过滤函数具有防XSS功能,作为演示,这里就简单使用htmlspecialchars函数进行过滤。

接着,定义web2类继承web1类,拥有方法show来echo出自己的$name。特别之处在于此时的web2在构造生成时还会调用web3类。

web3类定义在 web3.class.php 中,代码如下:

1
2
3
4
5
6
7
8
9
10
<?php

class web3 extends base{
public function __construct(){
parent::__construct();
echo 'web3 construct<br>';
}
}

?>

web3类直接继承自base类,因此,当我们调用test.php生成web2时,其实有如下多继承关系。

1
2
web2->web1->base
|->web3->base

而这其中,只有web1类中写了过滤XSS的方法,调用test.php,有如下输出效果。

1
http://localhost/test.php?name=<script>alert(1)</script>

显然,经过web1的过滤,xss并没有触发,但是,如果引入全局变量机制,我们再来看一下效果。

0x02 全局变量

修改后的各文件代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//base.class.php

<?php
class base{
public function __construct(){
global $name;
echo 'base construct<br>';
$this->filter($name);

}

protected function filter(){
global $name;
$name=$_GET['name'];
echo 'SQL filter<br>';
}
}
?>
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
//test.php
<?php
require_once('base.class.php');

class web1 extends base{
public function __construct(){
parent::__construct();
global $name;
echo 'web1 construct<br>';
}

protected function filter(){
parent::filter();
global $name;
$name=$_GET['name'];
$name = htmlspecialchars($name);
echo 'XSS filter<br>';
}
}

class web2 extends web1{
public function __construct(){
parent::__construct();
global $name;
require_once('web3.class.php');
$web3 = new web3;
echo 'web2 construct<br>';
}

public function show(){
echo 'show<br>';
global $name;
echo $name;
}

}

$web2 = new web2;
$web2->show();
?>

1
2
3
4
5
6
7
8
9
10
11
12
//web3.class.php
<?php

class web3 extends base{
public function __construct(){
parent::__construct();
global $name;
echo 'web3 construct<br>';
}
}

?>

其他代码都未做改动,只是将之前的类变量用全局变量进行替代,此时的执行效果如下所示:

1
http://localhost/test.php?name=<script>alert(1)</script>

过滤方法被绕过后成功执行了XSS。

0x03 对比分析

还是回到调用关系这里

1
2
web2->web1->base
|->web3->base

我们可以知道程序员写这段代码时,一定是想着通过了web1改写基类方法灵活的在不同类之间切换过滤方式。

当web2被实例化时,首先是web1实例化,web1实例化会造成base实例化,接着发现父类的过滤函数被重写,因此加载了web1的新方法,web1实例化结束后,发现web2中又实例化了一次web3,web3的实例化造成base和父方法实例化,由于全局变量的原因,重新被实例化后的base在获取变量后覆盖了原有的变量,而此时的父方法无法过滤XSS,最终使得web2输出了未过滤的参数造成了XSS漏洞。

PS:小的修正方法,此时的web3如果继承自web1,就可以保证XSS过滤函数的调用。