php中throw异常的问题研究

前置

在几年前,2016吧,php中存在一个漏洞叫做CVE-2016-7124

这是关于php中反序列化的,我们都知道在php进行反序列化的时候,首先会调用__wakeup方法,然后才会去调用类得析构方法

很多php中反序列化的修补措施为:抛出一个异常

1
2
3
4
public function __wakeup()
{
throw new \LogicException('FnStream should never be unserialized');
}

这个漏洞是干什么的呢?

  • 关于__wakeup魔术方法的绕过问题
  • 当 序列化字符串中表示对象属性个数的值大于 真实的属性个数时会跳过__wakeup的执行
  • 影响版本
    • php5.6.25-7.0.9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static inline long object_common1(UNSERIALIZE_PARAMETER, zend_class_entry *ce)
{
...
if (ce->serialize == NULL) {
object_init_ex(*rval, ce); <=== create object
...
static inline int object_common2(UNSERIALIZE_PARAMETER, long elements)
{
...
if (!process_nested_data(UNSERIALIZE_PASSTHRU, Z_OBJPROP_PP(rval), elements, 1)) { <=== create object properties
return 0;
}

if (Z_OBJCE_PP(rval) != PHP_IC_ENTRY &&
zend_hash_exists(&Z_OBJCE_PP(rval)->function_table, "__wakeup", sizeof("__wakeup"))) {
INIT_PZVAL(&fname);
ZVAL_STRINGL(&fname, "__wakeup", sizeof("__wakeup") - 1, 0);
BG(serialize_lock)++;
call_user_function_ex(CG(function_table), rval, &fname, &retval_ptr, 0, 0, 1, NULL TSRMLS_CC); <=== call to __wakeup()
BG(serialize_lock)--;
}
1
If the process_nested_data() return 0, the __wakeup() will not be invoked, but the object and its properties has been created, then the unexpected object will be destroyed (or may not). This may cause some security issues.

如上所示,就是当process_nested_data的返回值是0的时候,会跳过对__wakeup的调用直接返回0,但是由于这个时候类已经被初始化了,所以会继续进行析构

demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
class test{
public $test = "test";
public function __wakeup(){
echo "this is __wakeup";
echo "\n";
}
public function __destruct(){
echo "this is __destruct";
echo "\n";
}
}

$str_1 = 'O:4:"test":1:{s:4:"test";s:4:"test";}';
$str_2 = 'O:4:"test":2:{s:4:"test";s:4:"test";}';
echo PHP_VERSION;
echo "\n";
unserialize($str_1);
echo file_get_contents("test.php");
?>

第一种结果

1615377179121

第二种结果

发现并没有调用__wakeup方法

1615377239110

对比底层代码

  • php7.0.9php7.0.10

1615377346551

可以看到无论个数是否一致,都要先判断是否存在__wakeup方法

导致无法被绕过

新的发现

在上面的对比图片中,还发现删除了其他的东西

1
2
3
if (EG(exception)) {
return 0;
}

大概意思是,当执行完__wakeup方法后,抓取到异常,则return 0

这必须思考一个问题就是:

  • php中所谓的throw一个异常之后,php代码则中断执行
  • 在这里,可能会存在一个问题

demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
class test{
public $test = "aaaa";
public function __wakeup(){
throw new \LogicException('Never be unserialized');
}
public function __destruct(){
echo "this is __destruct";
echo "\n";
}
}

$str_1 = 'O:4:"test":1:{s:4:"test";s:4:"test";}';
echo PHP_VERSION;
echo "\n";
$obj = unserialize($str_1);
echo $obj->test;
echo "\n";
echo file_get_contents("test.php");
?>

结果

1615377869414

可以看到,c代码接受到异常之后return 0,那看来这个返回值,返回之后就是去调用析构方法了

析构方法被成功执行,但是析构完成之后马上抛出了异常,导致下面的输出语句并没有生效

意思就是很简单

  • 在存在cve-2016-7124的版本,无需特意去修改个数的问题,反序列化操作可以成功执行

更进一步

这漏洞很明显没有办法使用了

不看代码,盲测throw是否还存在其他的问题

在除了5.6.25-7.0.9版本之外的版本中

存在__wakeup跑出异常的类,无法再作为反序列化pop链的入口点

但是经过测试,是可以作为中间节点的,throw异常仍然存在逻辑性问题

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
<?php
highlight_file("demo.php");
class A {
protected $a;
public function __construct($str)
{
$this->a = $str;
}
public function __call($name,$args)
{
echo($this->a);
echo "<br>";
}
public function __wakeup()
{
throw new Exception("error");
}
}

class B{
public $b;
public function __destruct()
{
$this->b->close();
}
}
$a = new A("phpinfo");
$b = new B();
$b->b = $a;
$c = serialize($b);
echo PHP_VERSION;
echo "<br>";
unserialize($c);


测试结果

虽然最后仍然会抛出致命错误,但是仍然可以成功反序列化,且变量未被置空

1615378591505

end

还是有不少用途的