Java openrasp學習記錄(一)
前言
最近一直在做學校實驗室安排的專案,太慘了,沒多少時間學習新知識,不過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檢測,但是這種類的確不好找?
https://landgrey.me/blog/15/
關於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
參考
http://blog.nsfocus.net/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://c0d3p1ut0s.github.io/Java-RASP%E6%B5%85%E6%9E%90-%E4%BB%A5%E7%99%BE%E5%BA%A6OpenRASP%E4%B8%BA%E4%BE%8B/ 講openrasp
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的應用