某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过滤函数的调用。