Abstract
该漏洞是一个 PHP 对象注入漏洞,可导致远程代码执行后果
影响范围:
- 3.0.0-3.4.6
下载源码:传送门
解压至www目录下,然后在浏览器中访问开始安装程序
【注】
- Joomla 3.4之前(包含3.4)不支持PHP7.0,会爆500的
- 最后一步安装时选择不安装示范数据
- 要求PHP5.3.10以上,但是需要小于5.6.13 ,本地5.6.27复现失败,调整为5.5.38成功
漏洞复现
利用的EXP:传送门
利用成功后就创建好了一个后门
漏洞分析
Session逃逸
进入index页面,尝试登录操作,任意输入账号和密码,抓包
提交登录时会将类似CSRF-Token的东西附加在POST数据包中,返回303状态,跳转到/joomla/index.php/component/users/?view=login
让用户重新登录
Navicat连接数据库,查看表_session
数据,复制出来
很明显的序列化数据,Joomla首先将用户输入的账号与密码进行序列化写入数据库,然后再取出来反序列化后进行比对。
我们来看写入函数Joomla/libraries/joomla/session/storage/database.php:write()
以及读取函数Joomla/libraries/joomla/session/storage/database.php:read()
protect类型的变量序列化之后存在空字节,而mysql无法保存Null字节的数据,所以写入的时候将chr(0) . '*' . chr(0)
的数据转为\0\0\0
,然后读取的时候给转回来再反序列化操作。
来看下面这个例子
很明显存入数据库的数据比原始数据要多3个字节,这就是造成这个洞的原因。
假设我们传入的用户名为\0\0\0admin
,序列化之后写入数据库的值为
1 | s:8:"username";s:11:"\0\0\0admin";s:8:"password";s:4:"pass"; |
调用read()之后,会将\0\0\0
转为N*N
,变成了
1 | s:8:"username";s:11:"N*Nadmin";s:8:"password";s:4:"pass"; |
因为长度变短了3个字节就会“吃掉”后面3个字节的数据,这时候N*Nadmin";s
表示username的值。利用这个思路,足够多的\0\0\0
就可以将Password字段的数据逃逸出来,反序列化任意类了
1 | s:8:s:"username";s:54:"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";s:8:"password";s:8:"qwertest" |
read()之后就缩减为
1 | s:8:s:"username";s:54:"NNNNNNNNNNNNNNNNNNNNNNNNNNN";s:8:"password";s:8:"qwertest" |
username的值为NNNNNNNNNNNNNNNNNNNNNNNNNNN";s:8:"password";s:8:"qwert
,后面补上"
和;
就成功逃逸出来了
1 | NNNNNNNNNNNNNNNNNNNNNNNNNNN";s:8:"password";s:8:"qwert";s:2:"HS":O:15:"ObjectInjection" |
下面就是构造POP链了
POP链
早在2015年的时候,Joomla爆出的反序列化漏洞中就有一条POP链
libraries/joomla/database/driver/mysqli.php:JDatabaseDriverMysql
跟进这个disconnect()
使得$this->connection
为true就会调用call_user_func_array($h, array( &$this));
,而且第一个参数可控
call_user_func_array 与 call_user_func 这两个函数基本上是类似的,只是在调用上传递参数时存在一些差异
- 函数call_user_func_array 传递的第二个参数必须是数组;
- 函数call_user_func 传递的第二个参数可能是数组,如果是多个参数的话,还是需要以列表的形式列出。
官方手册给了这个例子——调用类内部的方法
1 |
|
可以作为跳板执行任意类方法了
libraries/simplepie/simplepie.php:init
1 | .... |
这个call_user_func($this->cache_name_function, $this->feed_url)
两个参数都是可控的,于是只要满足$this->cache
为True,$this->raw_data
为True,$parsed_feed_url['scheme']
不为空就能够RCE了。
后者能够利用|| $a='http//';
绕过scheme的解析
话不多说,赶紧构造Payload
1 |
|
拿到账号和密码
1 | \0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0 |
参数虽然传进去了,但是执行的时候却报错
返回报错信息
1 | Fatal error: call_user_func_array(): The script tried to execute a method or access a property of an incomplete object. Please ensure that the class definition "SimplePie" of the object you are trying to operate on was loaded _before_ unserialize() gets called or provide a __autoload() function to load the class definition in E:\phpstudy\PHPTutorial\WWW\Joomla\libraries\joomla\database\driver\mysqli.php on line 206 |
SimplePie这个类是不会自动加载的,所以报错了。
来看libraries/legacy/simplepie/factory.php
刚开始就导入了SimplePie类,而且这JSimplepieFactory类属于autoload的,能够自动加载。
修改Payload
1 |
|
最终的账号与密码
1 | \0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0 |
成功执行命令
我们来看下EXP实现原理
验证阶段,借助print_r
打印出一个随机字符,如果这个字符在响应中就能说明存在漏洞
利用阶段,使用file_put_contents
写入一句话
这里补充一句
php5.6.13版本以前是第一个变量解析错误注销第一个变量,然后解析第二个变量,但是5.6.13以后如果第一个变量错误,直接销毁整个session
Reference
https://www.freebuf.com/vuls/216130.html
https://www.leavesongs.com/PENETRATION/joomla-unserialize-code-execute-vulnerability.html
https://github.com/momika233/Joomla-3.4.6-RCE/blob/master/Joomla-3.4.6-RCE.py