WordPress < 3.6.1 PHP 对象注入漏洞
0x00 背景
当我读到一篇关于Joomla的“PHP对象注射”的漏洞blog后,我挖深了一点就发现Stefan Esser大神在2010年黑帽大会的文章:
这篇文章提到了PHP中的unserialize函数当操作用户生成的数据的时候会产生安全漏洞。
所以呢,基本来说,unserialize函数拿到表现为序列化的数据,然后就解序列化它(unserialize嘛,当然就干这个啊~)为php的值。这个值它可以是resource之外的任何类型,可为(integer, double, string, array, boolean, object, NULL),当这个函数操作一个用户生成的字符串的时候,在低版本的PHP中可能会产生内存泄露的漏洞,当然这也不是这篇blog要关注的问题。如果你对这个问题感兴趣,你可以再去看看我上面说的大神的文章。
另外一种攻击方式发生在当攻击者的输入被unserialize函数操作的时候,这种就是我说到的“PHP对象注入”。在这种方式中,对象类型的被unserialize的话,允许攻击者设置他选择对象的属性。当这些对象中的方法被调用的时候,会出现一些效果(例如:删除一些文件),当攻击者可以去选择对象里的一些属性的时候,他就能够删除一个他所提交的文件。
让我们举个例子吧,想象以下的代码中的class是用户自己生成的内容被unserialize时载入的:
(ps:老外贴出的代码语法有问题,改了一下测试成功……)
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <?phpclassFoo {    private$bar;     public$file;    publicfunction__construct($fileName) {        $this->bar = 'foobar';        $this->file = $fileName;    }    // 一些其他的代码……    publicfunction__toString() {        returnfile_get_contents($this->file);    }} ?> | 
如果受害的缺陷代码同时还存在以下的代码:
| 1 | echounserialize($_GET['in']); | 
这攻击者就可以读取任意文件,攻击者可以如下去构造它的对象。
| 1 2 3 4 5 6 7 8 | <?phpclassFoo {    public$file;}$foo= newFoo();$foo->file = '/etc/passwd';echoserialize($foo); ?> | 
上面这段代码的结果是:O:3:"Foo":1:{s:4:"file";s:11:"/etc/passwd";} ,攻击者现在要做的事情就事通过提交get请求到存在漏洞的页面触发他的攻击代码。这个页面会吐出/etc/passwd的内容来。能读到这些文件的内容怎么看都不是一个好事情,你就想象一下,万一缺陷代码中的函数不是file_get_contents而是eval呢?
我相信上面这部分已经能让人明白允许用户输入进入unserialize这个函数危害有多大了。就连PHP手册里也说了不要把用户生成的内容交给unserialize函数。
警告:
不要把不可信的用户输入交给unserialize,使用该函数解序列化内容能导致访问且自动载入对象,恶意用户可以利用这一点,从安全的角度,如果你想让用户可以标准的传递数据,可以使用json (json_encode json_decode)。
好,让我们继续说这些问题怎么影响到Wordpress。
0x01 wordpress的安全问题
Stefan Esser's的黑帽演讲中,他提到Wordpress是一款使用了serialize和unserialize函数的知名应用。在他的幻灯片中,unserialize用来接收来自Wordpress站点上的数据。所以攻击者可以在受害站点上发起一次中间人攻击。他可以修改来自Wordpress站点的返回数据,把他的代码加进去。有趣的是就在我编写这篇文章的时候,Wordpress最新的版本也包含这个问题(距离那演讲似乎过去三年了),想象一下,如果有黑客可以劫持WordPress.org的DNS会发生什么事情吧。
然而,这也不是Wordpress使用这个unserialize的唯一地方,它还用于用于在数据库中数据。举例来说,用户的metadata就被序列化后存储在数据库中,metadata的取回方式在wp-includes/meta.php的272行的get_metadata(),我在这里引用一下该函数的部分代码(292-297行)
| 1 2 3 4 5 6 | if( isset($meta_cache[$meta_key]) ) {    if( $single)        returnmaybe_unserialize( $meta_cache[$meta_key][0] );    else        returnarray_map('maybe_unserialize', $meta_cache[$meta_key]); } | 
基本上,这个函数所干的事情就是取回数据库里的metadata(它来自每篇文章或用户输入),数据在数据库中的wp_postmeta和wp_usermeta表中,有些数据是被序列化的而有些没有被序列化,所以maybe_unserialize()函数替代了unserialize()在这里操作,这个函数在wp-includes/functions.php的230到234行之间被定义。
| 1 2 3 4 5 | functionmaybe_unserialize( $original) {    if( is_serialized( $original) ) //序列化的数据才会走到这里面        return@unserialize( $original);    return$original; } | 
所以,这个函数干的事情是检查给予它的值是不是一个序列化的数据,如果是的话,就解序列化。这里用来判断是否是序列化所使用的函数是is_serialized(),它的定义在同文件的247到276行之间。
| 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 | functionis_serialized( $data) {    // 如果连字符串都不是,那就不是序列化的数据了    if( ! is_string( $data) )        returnfalse;    $data= trim( $data);     if( 'N;'== $data)        returntrue;    $length= strlen( $data);    if( $length< 4 )        returnfalse;    if( ':'!== $data[1] )        returnfalse;    $lastc= $data[$length-1];    if( ';'!== $lastc&& '}'!== $lastc)        returnfalse;    $token= $data[0];    switch( $token) {        case's':            if( '"'!== $data[$length-2] )                returnfalse;        case'a':        case'O':            return(bool) preg_match( "/^{$token}:[0-9]+:/s", $data);        case'b':        case'i':        case'd':            return(bool) preg_match( "/^{$token}:[0-9.E-]+;\$/", $data);    }    returnfalse;} | 
WordPress检查一个值是否是序列化的字符串为什么那么重要的原因马上要变得清晰了。首先,我们看一下一个攻击者如何把他的内容最终加入到metadata表中的。每个用户的姓名,雅虎IM都存储在wp_usermeta表里。所以我们把自己的恶意代码加在那我们就可以搞掂掉WordPress,对不对?你可以试试在你该写名字的地方写个i:1试试,如果这个没有被解序列化那这里只会返回一个我们输入的i:1。
麻痹的,看来要发几个大招才可以搞掂WordPress啊。让我们挖得再深一点,看看为什么这个东西就没有给解序列化。在 wp-includes/meta.php 中,这个update_metadata() 函数定义在101-164行,这里有这个函数的部分代码。
| 1 2 3 4 5 6 7 8 9 10 | // …    $meta_value= wp_unslash($meta_value);    $meta_value= sanitize_meta( $meta_key, $meta_value, $meta_type);// …    $meta_value= maybe_serialize( $meta_value);    $data= compact( 'meta_value');// …    $wpdb->update( $table, $data, $where);// … | 
这里maybe_serialize函数可能能解释为什么我们刚才的操作没成功,我们再跟进去看看这个函数,它定义在wp-includes/functions.php的314-324行。
| 1 2 3 4 5 6 7 8 9 10 | functionmaybe_serialize( $data) {    if( is_array( $data) || is_object( $data) )        returnserialize( $data);    // 二次序列化是为了向下支持    // 详见 http://core.trac.wordpress.org/ticket/12930    if( is_serialized( $data) )        returnserialize( $data);    return$data; } | 
所以当我们传入一个序列化的值的话,它就会再序列化一下,这就是现在发生的情况,你看,数据库里的东西不是i:1;而是s:4:"i:1;";,当解序列化的时候它就显示为一个字符串,那现在该怎么办呢?
你懂的,这帖子的内容也存在数据库里,上面这就说明了为什么我们失败了。如果我们现在想往数据库插一个序列化后的东西,我们就需要在我们插入数据的时候让is_serialized()这个函数返回一个false,而当我们再从数据里取它的时候,它就应该返回个true了。
你懂的,Mysql数据库,表和字段都有他们自己的charset和collation(字符集和定序)。WordPress呢,默认的字符集是UTF-8。从这个名字就看的出来,这个字符集它不支持全部的Unicode字符,你要是对这个感兴趣,你可以看看Mathias Bynens的这篇文章:http://mathiasbynens.be/notes/mysql-utf8mb4,这文章教了我UTF-8的表储存不了Unicode编码区间是U+010000到U+10FFFF的字符。所以当我们在这个情况下尝试保存这些字符呢?显而易见,包括这个字符和这个字符之后的内容都会被忽略掉。所以在我们尝试插入foo{0xf09d8c86}bar的时候,Mysql会忽略{0xf09d8c86}bar而保存为foo。
这个迷题的最后一部分就是我们需要插入一个用以一会儿解序列化的内容,为了测试这个,你可以插入1:i{0xf09d8c86}为你的名字。正如所见到的,结果是1,意味着你的输入被解序列化了,如果你还不相信我,你试着输入一个空数组的序列化并且以该字符结尾:a:0:{}{0xf09d8c86}。这个结果是Array。
让我们继续maybe_serialized('i:1;{0xf09d8c86}')插入了数据库。WordPress不认为这是一个已序列化的数据,因为它不是;或者}结尾的。它会返回i:1;{0xf09d8c86},当插入数据库的时候,它的值是i:1,当它从数据库取回的时候,它有了;最为最后一个字符,所以它可以解序列化成功。碉堡了。漏洞。
0x02 WordPress 利用
现在我们展示了WordPress存在PHP对象注入漏洞。让我们尝试利用它。所以为了利用该漏洞(通过注入对象的方法),我们需要找到一个符合以下条件的class:
1,内有“有用”的方法可被调用。
2,存在该对象的类已经被包含了。
当一个对象被解序列化的时候,__wakeup函数会被调用,这被称作PHP的魔术方法,这也是我们确定会被调用的方法,实际上这些函数会更多写些,我写了一个以下的class来获取被调用的class到/tmp/fumc.log。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | <?phpclassFoo {    publicstaticfunctionlogFuncCall($funcName) {        $fh= fopen('/tmp/func.log', 'a');        fwrite($fh, $funcName."\n");        fclose($fh);    }    publicfunction__construct() { Foo::logFuncCall('__construct('.json_encode(func_get_args()).')');}    publicfunction__destruct() { Foo::logFuncCall('__destruct()');}    publicfunction__get($name) { Foo::logFuncCall("__get($name)"); return"Foo";}    publicfunction__set($name, $value) { Foo::logFuncCall("__set($name, value)");}     publicfunction__isset($name) { Foo::logFuncCall("__isset($name)"); returntrue;}     publicfunction__unset($name) { Foo::logFuncCall("__unset($name)");}     publicfunction__sleep() { Foo::logFuncCall("__sleep()"); returnarray();}     publicfunction__wakeup() { Foo::logFuncCall("__wakeup()");}     publicfunction__toString() { Foo::logFuncCall("__toString()"); return"Foo";}     publicfunction__invoke($a) { Foo::logFuncCall("__invoke(". json_encode(func_get_args()).")");}    publicfunction__call($a, $b) { Foo::logFuncCall("__call(". json_encode(func_get_args()).")");}    publicstaticfunction__callStatic($a, $b) { Foo::logFuncCall("__callStatic(". json_encode(func_get_args()).")");}    publicstaticfunction__set_state($a) { Foo::logFuncCall("__set_state(". json_encode(func_get_args()).")"); returnnull;}    publicfunction__clone() { Foo::logFuncCall("__clone()");} } ?> | 
为了列出这些被调用的函数,首先要确认这个函数在解序列化发生的时候是被引入被包含过的(php中的include require等)。你可以把require_once('foo.php')加到functions.php的顶端。接下来,把名字改为O:3:"Foo":0:{}{0xf09d8c86}来尝试利用这个PHP对象注入漏洞,当刷新页面后,你回看到你的名字变成了Foo,这也就是意味着这是上面那class中__toString()函数的返回,然后让我们看看都有哪些函数被调用了。
$ sort -u /tmp/func.log
__destruct()
__toString()
__wakeup()
给出了我们三个函数:__wakeup(), __destruct() 和 __toString()
很不幸的是我不能再WordPress中找到一个载入了并且解序列化时能被利用造成影响的类。所以不是一个WordPress的安全问题,而是一个可能利用的地方。
所以是不是WordPress是有安全隐患的,但是无法被利用?不一定,如果你熟悉WordPress,你可能会觉察到可能有一堆插件存在漏洞。这些插件有他们自己的类并且可能暴露出可被利用的安全漏洞。我想到这个后,已经发现了一款著名的插件存在漏洞并且可以导致远程任意代码执行。
由于道德考虑,这个时候我不会发布PoC的,有太多存在安全漏洞的WordPress了。
0x03 修复WordPress
这个修复方式是修改is_serialized函数,我简单的说说:
| 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 | functionis_serialized( $data, $strict= true ) {     // 如果不是字符串就不会是序列化后的数据     if( ! is_string( $data) )         returnfalse;     if( ':'!== $data[1] )         returnfalse;    if( $strict) {        $lastc= $data[ $length- 1 ];        if( ';'!== $lastc&& '}'!== $lastc)            returnfalse;    } else{         //确认存在;或}但不是在第一个字符        if( strpos( $data, ';') < 3 && strpos( $data, '}') < 4 )            returnfalse;    }     $token= $data[0];     switch( $token) {         case's':            if( $strict) {                if( '"'!== $data[ $length- 2 ] )                    returnfalse;            } elseif( false === strpos( $data, '"') ) {                 returnfalse;            }         case'a':         case'O':             return(bool) preg_match( "/^{$token}:[0-9]+:/s", $data);         case'b':         case'i':         case'd':            $end= $strict? '$': '';            return(bool) preg_match( "/^{$token}:[0-9.E-]+;$end/", $data);     }     returnfalse;  } | 
这主要的区别是当$strict参数设置为false的时候,会有一些强制操作导致一个字符串被标记为已序列化。举例说明,最后一个字符不需要必须是;或者{(译者注:作者此处应该笔误了,应该是;或者}),修复了我所提交的漏洞。现在大家有没有相似的内容可以拿出来做个讨论的?
WordPress依旧使用着不安全的unserialize()而非安全的json_decode。它的安全性全在判断规则或者Mysql的规则实现上。我在上面揭露的漏洞实际上是使用Mysql的规则去掉我跟在特殊符号后的所有字符。
有一个很简洁的修复方案,修改一下数据库编码不被截断就好:
ALTER TABLE wp_commentmeta CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE wp_postmeta CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE wp_usermeta CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;WordPress < 3.6.1 PHP 对象注入漏洞的更多相关文章
- Codeigniter 利用加密Key(密钥)的对象注入漏洞
		http://drops.wooyun.org/papers/1449 原文链接:http://www.mehmetince.net/codeigniter-object-injection-vuln ... 
- WordPress WP-Realty插件‘listing_id’参数SQL注入漏洞
		漏洞名称: WordPress WP-Realty插件‘listing_id’参数SQL注入漏洞 CNNVD编号: CNNVD-201310-499 发布时间: 2013-10-23 更新时间: 20 ... 
- WordPress Quick Paypal Payments插件HTML注入漏洞
		漏洞名称: WordPress Quick Paypal Payments插件HTML注入漏洞 CNNVD编号: CNNVD-201310-491 发布时间: 2013-10-22 更新时间: 201 ... 
- joomla \libraries\joomla\session\session.php 反序列化截断畸形字符串导致对象注入漏洞
		catalog . 漏洞描述 . PHP SESSION持久化 . PHP 序列化/反序列化内核实现 . 漏洞代码分析 . POC构造技巧 . 防御方案 . Code Pathc方案 1. 漏洞描述 ... 
- joomla对象注入漏洞分析
		0x00 漏洞简单介绍 jooomla 1.5 到 3.4.5 的全部版本号中存在反序列化对象造成对象注入的漏洞,漏洞利用无须登录,直接在前台就可以运行随意PHP代码. Joomla 安全团队紧急公布 ... 
- Android WebView的Js对象注入漏洞解决方案
		http://blog.csdn.net/leehong2005/article/details/11808557/ webview调用以下文件,就可以打印sdcard 文件名 <!DOCTYP ... 
- PHP对象注入 PHP Object Injection
		From:PHP Object Injection Last revision (mm/dd/yy): 01/7/2015 译者:李秋豪 Wednesday, 24. May 2017 05:48PM ... 
- PHP反序列化中过滤函数使用不当导致的对象注入
		1.漏洞产生的原因 #### 正常的反序列化语句是这样的 $a='a:2:{s:8:"username";s:7:"dimpl3s";s:8:"pa ... 
- WordPress Cart66 Lite插件HTML注入漏洞
		漏洞名称: WordPress Cart66 Lite插件HTML注入漏洞 CNNVD编号: CNNVD-201310-525 发布时间: 2013-10-23 更新时间: 2013-10-23 危害 ... 
随机推荐
- git clone命令
			从远程clone一个仓库 ...知识浅薄 git clone都发现貌似用的不顺 因为我有几个git账号 但是我也不知道就是git账号是怎么保存在终端上的 所以当我需要用一个新的github账号来clo ... 
- CentOS 6.7下配置 yum 安装 Nginx
			CentOS 6.7下配置 yum 安装 Nginx. 转载:http://www.linuxidc.com/Linux/2016-07/133283.htm 第一步,在/etc/yum.repos. ... 
- react native 生成APK
			参考地址:React native Android 命令 打包apk 首先:尝试使用模拟器测试 这里是因为需要确认目前在电脑上的模拟器是可以正常运行的,并且,开发React native的应用程序,肯 ... 
- CDH-hive支持insert、update、delete操作
			开发要求hive支持update操作,搞张临时表测试下,报错如下: 配置hive-site.xml CDH进入hive配置页,选择高级,找到hive-site.xml的HIve客户端高级配置段代码段, ... 
- 如何使用weinre来进行远程调试phonegap应用
			使用phonegap开发的应用在真机上和PC上的显示效果以及浏览器渲染方式还是有些区别的.在PC端很好调试,各种浏览器都自带了调试工具,使用起来很方便,但是在一旦安装到了手机上,这个时候要进行调试就需 ... 
- VB查询数据库之写入数据库——机房收费系统总结(三)
			在机房收费系统中,新注册的用户,更改的密码,上机下级记录,上机收费记录等等都要写入数据库,这样,后面的查询才能生效.像数据库中写入数据,首先,找到你要写入数据的数据库中的表,在表中建立新的行,让后再把 ... 
- 2017 ACM-ICPC ECFINAL过山车体验
			这次采用domjudge判题,算是比较好玩的啦.外榜地址:http://board.acmicpc.cn/ 然后我们很可惜地止步于192名QAQ,没看出C是个傻逼题,没读懂B..我得背锅,亏我还打了那 ... 
- AGC 012 C - Tautonym Puzzle
			题面在这里! 神仙构造啊qwqwq. 窝一开始只想到一个字符串长度是 O(log(N)^2) 的做法:可以发现一段相同的长度为n的字符串的贡献是 2^(n-1)-1 ,可以把它看成类二进制,枚举用了多 ... 
- BZOJ 3399 [Usaco2009 Mar]Sand Castle城堡(贪心)
			[题目链接] http://www.lydsy.com/JudgeOnline/problem.php?id=3399 [题目大意] 将一个集合调整成另一个集合中的数,把一个数+1需要消耗x,-1需要 ... 
- hibernate双向ManyToMany映射
			工作需要一个双向多对多映射,照着李刚的书做了映射,碰到了一些问题,现就问题及解决方案进行总结归纳. 1.首先奉上最初代码 Person5.java @Entity @Table(name = &quo ... 
