简介
目前最新版本为 V2.1.0.3 版本的 iface.server-1.0.jar 和 iface.common-1.0.jar 这两个核心jar包已经是加密过的了(其实从v2.0.1.2就加密了,本文也是在v2.0.1.2就已经完成分析),想要代码审计就必须获取解密后的jar包或者解密后的class文件,众所周知,Java代码最终都是运行在jvm里,且在jvm里是明文的,最简单获取代码就是直接dump jvm 里与 iface 相关的class文件即可。对于没有加密版本之前的相关漏洞汇总:e脸通智慧园区管理平台漏洞汇总
正文
默认使用反编译工具如IDEA、JD-GUI 等显示 // INTERNAL ERROR // 表明,jar包不是常规class文件打包

使用 hexdump 查看加密的jar包 class 文件如下

➜ iface.common-1.0 file AppConst.class
AppConst.class: RAR archive data
➜ iface.common-1.0 hexdump -C AppConst.class
00000000 52 61 72 21 1a 07 00 cf 90 73 00 00 0d 00 00 00 |Rar!.....s......|
00000010 aa bb cc dd ee ff c3 d3 4e 3f be d5 7e bc 5b ac |........N?..~.[.|
00000020 4c c6 b7 99 f2 70 f0 2b 0c d3 38 f8 d2 a7 3f e5 |L....p.+..8...?.|
00000030 42 1b 74 2d 4a 5c 02 60 f5 78 62 92 ee fa 00 00 |B.t-J\.`.xb.....|
00000040 02 dc 00 00 02 e0 6c 07 51 e2 09 cc 3d df 60 46 |......l.Q...=.`F|
00000050 35 81 c9 2c d1 32 15 f8 68 f0 2e ac 93 65 5c c2 |5..,.2..h....e\.|
00000060 14 72 e0 90 cd 0c fe 95 d4 49 6a eb 3d 38 24 76 |.r.......Ij.=8$v|
00000070 34 82 93 6d 51 2f 95 03 52 e0 68 9b 13 00 57 9e |4..mQ/..R.h...W.|
00000080 24 aa 04 d9 e0 f3 3d 4a ba 57 59 d9 e5 37 7f dc |$.....=J.WY..7..|
00000090 85 45 0e ef c5 7f 9d 69 f0 b0 4b 73 fe 9c aa ae |.E.....i..Ks....|
000000a0 96 49 19 4f 5c 82 3b 63 a2 12 97 99 68 08 22 f9 |.I.O\.;c....h.".|
000000b0 03 ab a2 e2 1e 79 25 c0 5b 3f df 92 d0 20 63 db |.....y%.[?... c.|
000000c0 f2 09 85 e4 25 16 eb 5a 98 f8 49 d1 aa 66 e2 33 |....%..Z..I..f.3|
file 命令识别到的 AppConst.class 表示是rar压缩文件,且通过hexdump 得到的 6152 2172 即 rar! 字节码文件幻数 52 61 72 21 1a 07 00 (RAR 版本 1.5 到 4.x 的字节码文件幻数(RAR 4 及之前版本)),RAR 版本 5.x 的字节码文件幻数是 52 61 72 21 1A 07 01 00 比旧版本多了一个字节(第7字节变成了0x01)。
但是此文件实际不是rar文件!不能解压的,我比较懒,不喜欢去分析如何加载如何解密后在jvm运行的,直接dump!一步到位!
dump class
dump jvm的calss文件,对于老鸟来说很简单,有很多工具如
- jcmd:JDK 自带的命令行工具,功能丰富
- jmap:可以dump堆快照,间接分析 class 信息(通过工具(如 Eclipse MAT)分析堆快照,找到 class 对象并导出。)
- BTrace:动态追踪工具,可以在运行时hook类加载器,dump加载的class字节码,自定义脚本,拦截类加载过程,写出对应的字节码文件
- Bytecode Outline(IDE 插件):直接查看并导出 class 文件,但需要加载 class 文件或者 jar,不是动态 dump
- 使用JVMTI agent(自定义native agent):编写 JVMTI (Java Virtual Machine Tool Interface) agent,hook类加载事件,保存字节码,比较复杂但灵活,适合深入调试
- VisualVM + Profiler 插件:VisualVM 自带一些内存和类分析插件,有些插件支持保存加载的class字节码
- Byteman:用于动态插桩,也可以dump类字节码
- Java Instrumentation API:用 Java agent 方式,在类加载时拦截并保存字节码
- 使用
arthas等 attach 工具
我这里直接使用 Java agent 直接 hook !
dump 指定 className 为 com/hanvon/iface 的jvm calss,代码如下
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
public class DumpAgent {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("[*] DumpAgent started...");
inst.addTransformer(new ClassFileTransformer() {
@Override
public byte[] transform(ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) {
if (className != null && classfileBuffer != null) {
try {
// 避免 dump JVM 内部或代理类(如 sun/, java/, javax/, etc)
if (className.startsWith("com/hanvon/iface")) {
String filePath = "C:\\\\temp\\\\dump_classes\\\\" + className + ".class";
File f = new File(filePath);
f.getParentFile().mkdirs();
try (FileOutputStream fos = new FileOutputStream(f)) {
fos.write(classfileBuffer);
System.out.println("[+] Dumped: " + className);
}
}
} catch (Throwable t) {
System.err.println("[-] Error dumping class: " + className);
t.printStackTrace();
}
}
return classfileBuffer;
}
});
}
}
Manifest-Version: 1.0
Premain-Class: DumpAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
使用如下命令编译成jar文件
因为
EFaceGo的Java运行版本是1.8版本所以你的java -version 以及 javac -version 命令的结果需要是1.8版本,不然加载会报错
javac -source 1.8 -target 1.8 DumpAgent.java && jar cfm dumpagent.jar META-INF/MANIFEST.MF *.class
编译后目录结构如下
├── DumpAgent$1.class
├── DumpAgent.class
├── DumpAgent.java
├── META-INF
│ └── MANIFEST.MF
└── dumpagent.jar
2 directories, 5 files
然后将 dumpagent.jar 复制到EFaceGo的安装目录 C:\EFaceGo\Tomcat8\webapps\manage\WEB-INF\lib ,根据你的安装目录修改路径。
设置jvm参数
打开 C:\EFaceGo\Tomcat8\bin\tomcat8w.exe ,找到 【Java】 选项卡
在 【Java Options】 部分添加如下内容
-javaagent:C:\EFaceGo\Tomcat8\webapps\manage\WEB-INF\lib\dumpagent.jar
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005

如果dump出的class还是加密的,尝试切换位置,放置在最后

然后点击 【应用】
然后在服务里重启 Apache Tomcat 8.5 Tomcat8 服务

或者你在命令行里使用 net stop tomcat8 && net start tomcat8 命令进行重启。
注意观察 C:\EFaceGo\Tomcat8\logs 下面的 tomcat8-stdout.2025-xx-xx.log 日志,出现如下内容
2025-xx-xx xx:xx:xx Apache Commons Daemon procrun stdout initialized.
[*] DumpAgent started...
Listening for transport dt_socket at address: 5005
[+] Dumped: com/hanvon/iface/web/listener/WebAppEventListener
[+] Dumped: com/hanvon/iface/web/filter/HttpServletRequestReplacedFilter
[+] Dumped: com/hanvon/iface/utils/TheApp
[+] Dumped: com/hanvon/iface/utils/RecordProcessor
[+] Dumped: com/hanvon/iface/utils/Utils
[+] Dumped: com/hanvon/iface/types/HistoryTableInfo
[+] Dumped: com/hanvon/iface/web/controller/utils/operationLog/OperationLog
[+] Dumped: com/hanvon/iface/utils/Encrypter
[+] Dumped: com/hanvon/iface/utils/SM4Utils
[+] Dumped: com/hanvon/iface/utils/StrUtil
就代表 agent hook成功,进入 C:\TEMP\dump_classes 目录查看dump 的class文件即可

压缩后将其解压到 EFaceGo 的 manage class目录下即可开始审计啦!
注:采用这种方式可能dump不全!建议加载agent后等两分钟等全部加载了,刷新页面后再去保存calss文件。
后面有空了再讲讲如何解密这种jar文件。
加上 -verbose:class 启动参数,在日志文件 tomcat8-stdout.2025-xx-xx.log 看哪些类是由哪个路径加载
[Opened C:\EFaceGo\Java\jre1.8.0_181\lib\rt.jar]
[Loaded java.lang.Object from C:\EFaceGo\Java\jre1.8.0_181\lib\rt.jar]
[Loaded java.io.Serializable from C:\EFaceGo\Java\jre1.8.0_181\lib\rt.jar]
[Loaded java.lang.Comparable from C:\EFaceGo\Java\jre1.8.0_181\lib\rt.jar]
[Loaded java.lang.CharSequence from C:\EFaceGo\Java\jre1.8.0_181\lib\rt.jar]
[Loaded java.lang.String from C:\EFaceGo\Java\jre1.8.0_181\lib\rt.jar]
[Loaded java.lang.reflect.AnnotatedElement from C:\EFaceGo\Java\jre1.8.0_181\lib\rt.jar]
[Loaded java.lang.reflect.GenericDeclaration from C:\EFaceGo\Java\jre1.8.0_181\lib\rt.jar]
[Loaded java.lang.reflect.Type from C:\EFaceGo\Java\jre1.8.0_181\lib\rt.jar]
[Loaded java.lang.Class from C:\EFaceGo\Java\jre1.8.0_181\lib\rt.jar]
[Loaded java.lang.Cloneable from C:\EFaceGo\Java\jre1.8.0_181\lib\rt.jar]
[Loaded java.lang.ClassLoader from C:\EFaceGo\Java\jre1.8.0_181\lib\rt.jar]
[Loaded java.lang.System from C:\EFaceGo\Java\jre1.8.0_181\lib\rt.jar]
[Loaded java.lang.Throwable from C:\EFaceGo\Java\jre1.8.0_181\lib\rt.jar]
[Loaded java.lang.Error from C:\EFaceGo\Java\jre1.8.0_181\lib\rt.jar]
[Loaded java.lang.ThreadDeath from C:\EFaceGo\Java\jre1.8.0_181\lib\rt.jar]
......
上述 dump 下面的class 文件还是加密的(加载顺序有关,调整后正常dumpclass文件,或者是因为在web页面尝试了导入授权,激活了解密class,之前只是刷新,没有触发加载这些类即没有解密,或者多等待一段时间后,写入解密后的class,反正会覆写),放在最后,特别是放在 -agentpath 后面

还在jvm参数里发现了
-agentpath:C:\EFaceGo\Tomcat8\bin\libhwcheck.dll
是一个 native agent,可能被用来控制或加密/解密 JVM 类的加载过程。
二者区别如下
| 项目 | -javaagent | -agentpath |
|---|---|---|
| 类型 | Java 语言写的 agent(.jar) | Native C/C++ 写的 JVMTI agent(.dll / .so) |
| 接口 | 使用 java.lang.instrument.Instrumentation | 使用 JVMTI(Java Virtual Machine Tool Interface) |
| 常见文件类型 | .jar | .dll, .so, .dylib |
| 启动调用 | premain(String args, Instrumentation inst)(或 agentmain) | Agent_OnLoad(JavaVM vm, char options, void *reserved) |
| 功能能力 | 插桩类、方法字节码修改 | JVM 更底层能力:类加载 hook、线程监控、加解密控制、反调试等 |
| 加载时机 | 启动时、attach 后 | 通常只能启动时加载(attach 支持但更复杂) |

但是 libhwcheck.dll 大概率是硬件加密狗检测或硬件绑定校验的Agent,并非class加解密

通过进入后台,各个功能点点 dump 的class更完整(不点也没事,大部分主要class都还是能dump下来的

编写解密工具
通过翻找观察tomcat目录文件时间戳变化,发现tomcat的lib目录下的一个jar包 tomcat-coyote.jar 时间明显和其他不一样:

其他jar包都是 2023年11月10号的,唯独它是2024年3月6号的,多半有猫腻!
直接下载一个官方版本的 tomcat-coyote.jar 和其进行对比
tomcat版本信息在启动时会打印在控制台,也会写入 logs/catalina.日期.log 里

直接去 https://mvnrepository.com/artifact/org.apache.tomcat/tomcat-coyote/8.5.96 下载一个 tomcat-coyote-8.5.96.jar 文件,对比如下

两个jar包就一处不一样 org/apache/tomcat/util/bcel/classfile/ClassParser.class
这个 ClassParser.class 文件是 Tomcat 中用于解析 .class 字节码文件结构的类,属于 BCEL(Byte Code Engineering Library)的一部分,主要用于读取和分析 Java 字节码。
然后将这两个jar里的这个 ClassParser.class 直接用解压软件提取出来,尝试查看这个不同的class

可以看到 EFACEGO 里面自带的这个 tomcat-coyote.jar 文件幻数明显不是常见Java calss 文件的幻数 ca fe ba be
那我们修改下前面的agent 来 dump 这个class,dump后的class用 jd-gui 查看下有啥猫腻

魔改版本增加的 decode 方法,顾名思义 那就是解码!
看下魔改的主要变动内容如下
private InputStream decode(InputStream is) throws Exception {
int size = is.available();
is.mark(size + 1);
if (size < ENCRYPT_HEAD.length + ENCRYPT_TAIL.length + 24 + 16 + 8)
return is;
byte[] head = new byte[ENCRYPT_HEAD.length];
if (is.read(head) != ENCRYPT_HEAD.length) {
is.reset();
return is;
}
for (int i = 0; i < ENCRYPT_HEAD.length; i++) {
if (head[i] != ENCRYPT_HEAD[i]) {
is.reset();
return is;
}
}
byte[] key = new byte[24];
if (is.read(key) != 24)
throw new IOException("Tomcat ClassParser failed parse file[0]");
byte[] iv = new byte[16];
if (is.read(iv) != 16)
throw new IOException("Tomcat ClassParser failed parse file[1]");
byte[] buffer = new byte[8];
if (is.read(buffer) != 8)
throw new IOException("Tomcat ClassParser failed parse file[2]");
int sourceLen = buffer[0] << 24 & 0xFF000000 | buffer[1] << 16 & 0xFF0000 | buffer[2] << 8 & 0xFF00 | buffer[3] & 0xFF;
int dataLen = buffer[4] << 24 & 0xFF000000 | buffer[5] << 16 & 0xFF0000 | buffer[6] << 8 & 0xFF00 | buffer[7] & 0xFF;
if (sourceLen <= 0 || dataLen <= 0 || size != ENCRYPT_HEAD.length + ENCRYPT_TAIL.length + 24 + 16 + 8 + dataLen)
throw new IOException("Tomcat ClassParser failed parse file[3]");
byte[] data = new byte[dataLen];
if (is.read(data) != dataLen)
throw new IOException("Tomcat ClassParser failed parse file[4]");
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
IvParameterSpec ivSpec = new IvParameterSpec(iv);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(2, keySpec, ivSpec);
byte[] decrypted = cipher.doFinal(data);
if (decrypted == null || decrypted.length != sourceLen)
throw new IOException("Tomcat ClassParser failed parse file[5]");
byte code = (byte)(sourceLen & 0xFF);
for (int j = 0; j < sourceLen; j++)
decrypted[j] = (byte)(decrypted[j] ^ code);
return new ByteArrayInputStream(decrypted);
}
private static final byte[] ENCRYPT_HEAD = new byte[] {
82, 97, 114, 33, 26, 7, 0, -49, -112, 115,
0, 0, 13, 0, 0, 0, -86, -69, -52, -35,
-18, -1 };
private static final byte[] ENCRYPT_TAIL = new byte[] { -105, -60, 61, 123, 0, 64, 7, 0 };
这不就是我们心心念念的解密逻辑吗!其主要处理逻辑如下
- 如果不满足加密数据结构长度,否则直接放回。
- 读取头部标记,与
ENCRYPT_HEAD比较,若不匹配,也说明不是加密格式,返回原始流。 - 读取密钥、IV 和长度信息,若读取失败,抛出
IOException,如果数据长度不对,也抛出异常。 - 读取密文数据
- 执行 AES 解密(AES 使用 CBC 模式,带填充,key 和 iv 是前面读取的,
2表示Cipher.DECRYPT_MODE) - 验证长度并异或还原(使用
(sourceLen & 0xFF)得到一个字节做异或) - 返回解密结果
加密格式结构如下
| 顺序 | 字节数 | 说明 |
|---|---|---|
| 1 | 22 | ENCRYPT_HEAD 魔术头 |
| 2 | 24 | AES 密钥 |
| 3 | 16 | AES IV 向量 |
| 4 | 8 | 解密后长度 + 加密数据长度 |
| 5 | N | 加密数据 |
| 6 | 8 | ENCRYPT_TAIL 魔术尾(未验证!此段代码未读取尾部,只通过 size 检查存在) |
知道了加密方式后,就可以写一个解密工具直接将整个jar包进行解密,下载方式在最后

成功解密


参考
https://github.com/accessmodifier364/Dumper- 为了方便动态dump自定义class类,魔改了 accessmodifier364/Dumper 项目
- 解密工具下载

