前言
最近一直在做學校實驗室安排的項目,太慘了,沒多少時間學習新知識,不過rasp還是要擠擠時間學的,先從小例子的分析開始,了解rasp的基本設計思路,后面詳細閱讀openrasp的源碼進行學習!歡迎在學習相關知識的師傅找我交流!如本文有所錯誤請指出~
例子1
https://github.com/anbai-inc/javaweb-expression 一個hook ognl、spel、MVEL表達式注入的例子
用的是asm5進行字節碼修改
采用premain進行插樁,重寫transform方法
expClassList是要hook的類,這里定義在MethodHookDesc
這里判斷hook點通過類名,具體其中的方法名,以及方法的描述符
其中expClassList中定義了具體要hook的類,就mvel、ognl、spel三種
匹配到以上三種類后即重寫visitMethod方法,匹配具體要hook的方法名和方法描述符,如果匹配到了,則重寫MethodVisitor的visitCode方法,進行字節碼修改,這里因為是表達式注入,因此這里涉及到string類型的表達式,因此獲取傳到hook函數處的表達式字符串壓入操作數棧,並通過調用expression方法彈出該值進行檢測,這里要涉及到操作數棧和局部變量表,因此要清楚原本的方法幀中局部變量表下標索引幾代表的是輸入的表達式:
ognl:
ognl對應的是parseExpression這個方法,其中expressoin參數是具體解析的表達式
其對應的字節碼指令如下所示,Aload0即對應的即為表達式,通過invokeSpecial調用
也可以通過jclasslib來查看
spel:
這里的hook點時init方法,這里的expression即為表達式
其init方法中aload1對應賦值時的棧頂元素,所以其為表達式,因此下標對應的是1
mvel:
這個用的局部變量表的下標也是1,然而實際上取表達式值時用的為下標為0的this來取
根據局部變量表中的表達式的值傳入expression方法進行處理
其中expression將打印出當前的函數調用棧,該例子只是一個插樁+hook方法字節碼修改的例子,並沒有最終的判斷入侵的檢測規則
例子2
https://toutiao.io/posts/4kt0al/preview 中給了一個例子,也是用asm進行字節碼的修改
整體設計分析:
premain方式進行插樁,調用init方法,進一步調用Config.initConfig方法進行初始化配置
此時用到resources/main.config文件,讀取其內容,從其格式來看其為json文件,以不同的模塊名來區分不同的hook類別
{ "module": [ { "moduleName": "java/lang/ProcessBuilder", "loadClass": "xbear.javaopenrasp.visitors.rce.ProcessBuilderVisitor", "mode": "block", "whiteList":["javac"], "blackList": [ "calc", "etc", "var", "opt", "apache", "bin", "passwd", "login", "cshrc", "profile", "ifconfig", "tcpdump", "chmod", "cron", "sudo", "su", "rm", "wget", "sz", "kill", "apt-get", "find", "/applications/calculator.app/contents/macos/calculator" ] }, { "moduleName": "java/io/ObjectInputStream", "loadClass": "xbear.javaopenrasp.visitors.rce.DeserializationVisitor", "mode": "black", "whiteList":[], "blackList": [ "org.apache.commons.collections.functors.InvokerTransformer", "org.apache.commons.collections.functors.InstantiateTransformer", "org.apache.commons.collections4.functors.InvokerTransformer", "org.apache.commons.collections4.functors.InstantiateTransformer", "org.codehaus.groovy.runtime.ConvertedClosure", "org.codehaus.groovy.runtime.MethodClosure", "org.springframework.beans.factory.ObjectFactory" ] }, { "moduleName": "ognl/Ognl", "loadClass": "xbear.javaopenrasp.visitors.rce.OgnlVisitor", "mode": "black", "whiteList":[], "blackList": [ "ognl.OgnlContext", "ognl.TypeConverter", "ognl.MemberAccess", "_memberAccess", "ognl.ClassResolver", "java.lang.Runtime", "java.lang.Class", "java.lang.ClassLoader", "java.lang.System", "java.lang.ProcessBuilder", "java.lang.Object", "java.lang.Shutdown", "java.io.File", "javax.script.ScriptEngineManager", "com.opensymphony.xwork2.ActionContext", ] }, { "moduleName": "com/mysql/jdbc/StatementImpl", "loadClass": "xbear.javaopenrasp.visitors.sql.MySQLVisitor", "mode": "check", "whiteList":[], "blackList":[] }, { "moduleName": "com/microsoft/jdbc/base/BaseStatement", "loadClass": "xbear.javaopenrasp.visitors.sql.SQLServerVisitor", "mode": "check", "whiteList":[], "blackList":[] } ] }
接着取到module中的值放入ConcurrentHashmap中,對於每一個moduleName都對應一個ConcurrentHashmap,那么后面運行過程中根據moudlename就能獲取到每種hook點的信息
對於jvm將要加載的類,如果module中包含該類名,則使用asm來進行字節碼修改,這里創建ClassVisitor通過Reflections.createVisitorIns方法,因為通常在這里將需要設計具體如何對class進行檢查,那么對於不同的需要進行hook的類處理邏輯不同,因此這里是一個分支點,例子1也是相同的。
根據當前的類名得到其相對應的loadclass的類名然后利用反射進行實例化
這里定義了rce和sql兩個大類
具體對應的hook的類名和具體的loadclass類名映射關系為:
java/lang/ProcessBuilder -> xbear.javaopenrasp.visitors.rce.ProcessBuilderVisitor //命令執行 java/io/ObjectInputStream -> xbear.javaopenrasp.visitors.rce.DeserializationVisitor //反序列化 ognl/Ognl -> xbear.javaopenrasp.visitors.rce.OgnlVisitor //ognl表達式注入 com/mysql/jdbc/StatementImpl -> xbear.javaopenrasp.visitors.sql.MySQLVisitor //sql注入 com/microsoft/jdbc/base/BaseStatement -> xbear.javaopenrasp.visitors.sql.SQLServerVisitor //sql注入
從大體上整個插樁過程分析結束,初始化的主要工作還是對各種hook點如何進行初始配置,方便后面hook進行中的具體細化操作。
hook點處理分析:
命令執行hook點:
java中命令執行一般常用的有兩種,Runtime.exec和Processbuilder.start,但是Runtime.exec實際上也是利用的Processbuilder,而Processbuilder最終利用的是ProcessImpl來執行命令,那么實際上這里選擇hook點,選擇Processbuilder的start即可,因為只要執行命令,都將走到該類的start方法,在這里就能拿到具體要執行的命令。
具體的邏輯如下,這里重寫了onMethodEnter方法,asm5中的,即進入start內部之前執行
@Override protected void onMethodEnter() { mv.visitTypeInsn(NEW, "xbear/javaopenrasp/filters/rce/PrcessBuilderFilter"); //new一個命令執行過濾的對象壓入棧 mv.visitInsn(DUP); //再次壓入該對象 mv.visitMethodInsn(INVOKESPECIAL, "xbear/javaopenrasp/filters/rce/PrcessBuilderFilter", "<init>", "()V", false); //彈出對象進行初始化,此時棧中大小為2-1=1 mv.visitVarInsn(ASTORE, 1); //彈出存儲該對象到局部變量表1處,此時棧的大小為1-1=0 mv.visitVarInsn(ALOAD, 1); //加載局部變量表1處的對象壓入棧,此時棧的大小為0+1=1 mv.visitVarInsn(ALOAD, 0); //加載this壓入棧,此時棧大小為1+1=2 mv.visitFieldInsn(GETFIELD, "java/lang/ProcessBuilder", "command", "Ljava/util/List;"); //取this.command的值壓入棧,棧大小為2 mv.visitMethodInsn(INVOKEVIRTUAL, "xbear/javaopenrasp/filters/rce/PrcessBuilderFilter", "filter", //調用filer方法,彈出的值的數量為filter的方法參數大小1+1=2,棧頂的this.command的值作為參數,並將filter
方法的處理結果壓入棧中,filter返回一個Boolean值,此時棧中大小為1 "(Ljava/lang/Object;)Z", false); Label l92 = new Label(); //new一個label用來跳轉 mv.visitJumpInsn(IFNE, l92); //此時彈出filter處理的結果和0進行比較,如果不等與0,則跳到192lable,說明執行的當前的命令可以執行,則正常執行start方法,否則執行下一條指令,棧大小為0 mv.visitTypeInsn(NEW, "java/io/IOException"); //new 一個io異常對象 mv.visitInsn(DUP); //再次壓入該對象,棧大小2 mv.visitLdcInsn("invalid character in command because of security"); //壓入該字符串,棧大小3 mv.visitMethodInsn(INVOKESPECIAL, "java/io/IOException", "<init>", "(Ljava/lang/String;)V", false); //彈出1+1=2個值,初始化該異常對象,棧頂元素作為io異常的初始化參數,此時棧大小為1 mv.visitInsn(ATHROW); //拋出該異常 mv.visitLabel(l92); }
先看start方法部分如下:
這里如果直接用asm字節碼指令來寫就要結合源碼和bytecode字節碼指令來寫,可以看到0處放入的即為this,最終command.toArray的結果放到局部變量表1處,上面寫指令碼的時候也ASTORE_1了一次,這里並不一定直到1處是否有值,但是指令碼這里直接ASTORE1,因此我們不需要擔心1處是否有值
這樣就完成了hook點的構造,取command的值調用filter進行過濾,命令執行的filter如下所示:
public boolean filter(Object forCheck) { String moduleName = "java/lang/ProcessBuilder"; List<String> commandList = (List<String>) forCheck; String command = StringUtils.join(commandList, " ").trim().toLowerCase(); Console.log("即將執行命令:" + command); String mode = (String) Config.moduleMap.get(moduleName).get("mode"); //取對應的命令執行邏輯,mode為block,即阻斷 switch (mode) { case "block": Console.log("> 阻止執行命令:" + command); return false; //如果直接為block,那么所有命令都執行不了,也可以更改模式,用黑白名單過濾 case "white": if (Config.isWhite(moduleName, command)) { Console.log("> 允許執行命令:" + command); return true; } Console.log("> 阻止執行命令:" + command); return false; case "black": if (Config.isBlack(moduleName, command)) { Console.log("> 阻止執行命令:" + command); return false; } Console.log("> 允許執行命令:" + command); return true; case "log": default: Console.log("> 允許執行命令:" + command); Console.log("> 輸出打印調用棧\r\n" + StackTrace.getStackTrace()); return true; } }
asm感覺還是挺麻煩的,語句越復雜要用到的指令越多,稍微不熟練就會出錯
反序列化hook點:
在java.io.ObjectInputStream處進行hook,這里定義了一些反序列化的黑名單
@Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions); if ("resolveClass".equals(name) && "(Ljava/io/ObjectStreamClass;)Ljava/lang/Class;".equals(desc)) { mv = new DeserializationVisitorAdapter(mv, access, name, desc); } return mv; }
為什么選擇resolveClass作為hook的方法?只要記住我們的目的是拿到將要反序列化的類名,那么實際上的反序列化過程中resolveClass的代碼如下:
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { String name = desc.getName(); try { return Class.forName(name, false, latestUserDefinedLoader()); } catch (ClassNotFoundException ex) { Class<?> cl = primClasses.get(name); if (cl != null) { return cl; } else { throw ex; } } }
入口參數是ObjectStreamClass,那么在序列化過程中生成的序列化數據的過程中調用該類的lookup方法將生成類的描述信息,其中就包括的類名和SUID,那么調用該類的getName實際上就能拿到反序列化類的名字,所以只需拿到類描述符即可,從resolveClass的邏輯中將以類名通過反射進行類的加載獲取反序列化類的class對象,以CommonsCollections2為例,涉及到PriorityQueue和InvokerTrasnformer和TransformingComparator,那么肯定要涉及到這兩個類的反序列化
比如如下圖所示就能拿到反序列化的類名,然后再與黑名單進行匹配即可
對應的hook邏輯如下:
@Override protected void onMethodEnter() { mv.visitTypeInsn(NEW, "xbear/javaopenrasp/filters/rce/DeserializationFilter"); //new一個反序列化過濾對象壓入棧,棧大小1 mv.visitInsn(DUP); //再次壓入該對象,棧大小為2 mv.visitMethodInsn(INVOKESPECIAL, "xbear/javaopenrasp/filters/rce/DeserializationFilter", "<init>", "()V", false); //彈出一個對象進行實例化,棧大小為1 mv.visitVarInsn(ASTORE, 2); //存儲該對象到局部變量表,棧大小為0 mv.visitVarInsn(ALOAD, 2); //取出該對象到棧,棧大小為1 mv.visitVarInsn(ALOAD, 1); //這里要涉及到取局部變量表的值, 所以又得去看該方法的字節碼指令,取到的即為desc,壓入操作數棧,棧大小為1+1=2 mv.visitMethodInsn(INVOKEVIRTUAL, "xbear/javaopenrasp/filters/rce/DeserializationFilterr", "filter", "(Ljava/lang/Object;)Z", false); //調用反序列化過濾方法,彈出1+1=2個值,棧頂的desc作為參數 Label l92 = new Label(); //new一個label mv.visitJumpInsn(IFNE, l92); //過濾的返回值和0比 mv.visitTypeInsn(NEW, "java/io/IOException"); //如果等於0,則new一個異常對象 mv.visitInsn(DUP); //再次壓入 mv.visitLdcInsn("invalid class in deserialization because of security"); //錯誤信息壓棧 mv.visitMethodInsn(INVOKESPECIAL, "java/io/IOException", "<init>", "(Ljava/lang/String;)V", false); //實例化異常 mv.visitInsn(ATHROW); //拋出異常 mv.visitLabel(l92); //不等於0,則說明反序列化的類不在黑名單中,進行正常反序列化過程 }
從下圖可以看到aload1,然后調用棧頂元素的getname方法,並把結果壓入棧中,所以desc類描述符是在該方法的局部變量表1處存着,並且2處不管之前放什么元素,這里將被類名進行覆蓋
在對應的過濾方法中再通過類描述符調用getName拿到類名,然后通過對應的mode為black,因此
接着只要拿到預先配置好的黑名單來進行過濾即可
ognl的hook點:
hook的是ognl.Ognl的parseExpression這個方法,和第一個例子選擇的hook點是相同的,因為該方法就能拿到要執行的表達式
那么對於對應的class文件直接看該方法的局部變量表就能看到表達式再局部變量表的0處,因此只要將該值傳入過濾函數即可
對應的hook處的邏輯:
protected void onMethodEnter() { Label l30 = new Label(); //new一個label mv.visitLabel(l30); //訪問該label(貌似沒有意義) mv.visitVarInsn(ALOAD, 0); //加載局部表量表0處的表達式值到棧 mv.visitMethodInsn(INVOKESTATIC, "xbear/javaopenrasp/filters/rce/OgnlFilter", "staticFilter", "(Ljava/lang/Object;)Z", false);//調用過濾函數,傳入表達式的值,因為是static方法,所以只需要提供入口參數即可 Label l31 = new Label(); //new一個label mv.visitJumpInsn(IFNE, l31); //如果過濾表達式不為0,則表達式正常執行 Label l32 = new Label(); //new label,貌似沒有 mv.visitLabel(l32); mv.visitTypeInsn(NEW, "ognl/OgnlException"); //new一個異常對象 mv.visitInsn(DUP); //再次壓棧 mv.visitLdcInsn("invalid class in ognl expression because of security"); //異常信息壓棧 mv.visitMethodInsn(INVOKESPECIAL, "ognl/OgnlException", "<init>", "(Ljava/lang/String;)V", false); //傳入異常信息進行異常對象初始化 mv.visitInsn(ATHROW); //拋出異常 mv.visitLabel(l31); }
RASP繞過
1.https://www.anquanke.com/post/id/195016
第一種是根據線程中rce,繞過了rasp對context url的判斷,沒有url則直接返回正常
第二種直接關掉了rasp的開關
兩種措施都必須有代碼執行的權限,也就是說必須有shell的前提下
2.de1ctf中的一道繞rasp的思路,思路雖然在園長的javaseccode中提到過,defineclass來繞過rasp檢測,但是這種類的確不好找?
關於springboot為何能繞過rasp,首先defineclass,然后addclass說明已經添加到jvm中,然后class.forname再反射拿到該類時會進行類的鏈接從而執行static靜態區的代碼,不需要再重新loadclass
此時classforname時native方法直接加載加載該類,因此繞過了rasp對類加載機制的攔截
rasp的用途
1.代碼審計
可以對一些漏洞,比如反序列化,ognl、spel等的關鍵函數處進行hook並記錄,然后可以輸出成類似日志的格式,結合其調用棧以及其入口參數提供給白盒代碼審計工具進行自動化審計
2.0day捕獲
對一些危險函數進行hook,並在執行時及時告警,比如Runtime.exec,Processs,但是個人感覺這樣效率可能有點低,不如交給ids進行捕獲效率更高
3.DevOps
因為進行hook時,asm中提供了大量有用的方法從而能夠獲得hook點處詳細的信息:調用棧、代碼行號、接口、父類等
rasp的缺陷
1.首先rasp攔截是侵入程序代碼內部的,那么它實際上是和具體的語言強相關的,因此不同語言之間並不通用,需針對不同語言的特性進行開發
2.rasp是對關鍵函數進行hook,那么意味着無論攻擊路徑從哪條路走,最終都將匯集於某一個點,因此高效率的攔截要求設計rasp的hook規則時,開發者本身即必須對各種漏洞的利用方式以及一些關鍵函數點熟悉,因此存在遺漏的可能。
甲方如何應用rasp
1.直接根據開源的openrasp來進行二次開發,針對企業具體應用進行適配
問題:推廣周期長,運維難度大,以及要保證現有的業務在布置rasp后仍舊能夠正常運行,有一定的風險
2.在現有的APM程序上(cat,wiseapm)進行修改,彌補推廣的周期,在穩定性也有一定的保證,只需要將rasp的一些想法加入到APM程序中,https://www.freebuf.com/articles/es/235441.html這篇文章中介紹到平安銀行是利用cat搜集的一些信息進行輸出進行審計,比如apm本身就自帶一些監控sql語句執行的功能
結合掃描器
如果能夠得到具體的hook日志,則可以
1.流量設置標志位,對所有測試流量加某種標志位,如果hook的某個點有標志位進入,則認為該處可能存在漏洞(存在拼接且有入口)(例如sql注入,程序內部也可能有很多sql執行,這樣能篩選出外部輸入)
2.黑名單檢測,檢測hook點處函數入參是否在黑名單內,比如反序列化gadget的關鍵sink的黑名單或者sql注入的一些payload的黑名單(規則可以參考waf),sql注入還可以判斷單引號的個數
3.判斷request url中的參數和hook點處的參數是否相同,相同則為存在安全漏洞,hook點處的value是否包含一些敏感字符,比如sql注入的反斜杠 空格等關鍵payload
rasp和應用引入的第三方jar包之間版本問題
本地測試:
premain方式插樁,rasp引入fastjsonj1.2.48,應用引入fastjson1.2.47,使用1.2.47通殺payload進行嘗試:
a.插樁之前:
b.插樁之后:
直接報autotype not support,說明此時理論上應用在使用fastjson時自動使用了rasp的1.2.48版本的fastjson
列出的jvm進程id來看,agent並非以獨立進程運行,而是和應用統一jvm進程,用的同一個jvm虛擬機,那么類加載器都用的同一套,因此實際上類加載時,肯定不存在沖突問題,相同的類只會加載一次,此時主要就是看不同版本是加載哪里的,如下圖所示:
此時可以看到應用在使用fastjson時將加載rasp指定目錄下的fastjson,難道並不是智能地選擇高版本????此時控制變量,改變rasp引入的fastjson為1.2.47,應用使用1.2.48,效果如下:
彈了。。。
因此結論為:
如果rasp中引入的第三方jar包的版本和應用引入的第三方jar包版本不同,此時應用在使用jar包時將優先加載rasp引入的對應版本的jar包。
參考
http://blog.nsfocus./rasp-tech/ 已看
https://www.freebuf.com/articles/web/197823.html 已看
https://www.03sec.com/3239.shtml 例子
https://toutiao.io/posts/4kt0al/preview 例子
https://paper.seebug.org/1041/
https://www.cnblogs.com/2014asm/p/10834818.html 有例子
https://www.anquanke.com/post/id/195016#h2-3 rasp繞過
https://www.freebuf.com/articles/web/217421.html openrasp梳理
https://blog.csdn.net/sacredbook/article/details/105342185
https://www.freebuf.com/articles/web/216185.html rasp的應用