最近一直在作學校實驗室安排的項目,太慘了,沒多少時間學習新知識,不過rasp仍是要擠擠時間學的,先從小例子的分析開始,瞭解rasp的基本設計思路,後面詳細閱讀openrasp的源碼進行學習!歡迎在學習相關知識的師傅找我交流!如本文有所錯誤請指出~html
https://github.com/anbai-inc/javaweb-expression 一個hook ognl、spel、MVEL表達式注入的例子java
用的是asm5進行字節碼修改mysql
採用premain進行插樁,重寫transform方法git
expClassList是要hook的類,這裏定義在MethodHookDescgithub
這裏判斷hook點經過類名,具體其中的方法名,以及方法的描述符web
其中expClassList中定義了具體要hook的類,就mvel、ognl、spel三種spring
匹配到以上三種類後即重寫visitMethod方法,匹配具體要hook的方法名和方法描述符,若是匹配到了,則重寫MethodVisitor的visitCode方法,進行字節碼修改,這裏由於是表達式注入,所以這裏涉及到string類型的表達式,所以獲取傳到hook函數處的表達式字符串壓入操做數棧,並經過調用expression方法彈出該值進行檢測,這裏要涉及到操做數棧和局部變量表,所以要清楚本來的方法幀中局部變量表下標索引幾表明的是輸入的表達式:sql
ognl:shell
ognl對應的是parseExpression這個方法,其中expressoin參數是具體解析的表達式express
其對應的字節碼指令以下所示,Aload0即對應的即爲表達式,經過invokeSpecial調用
也能夠經過jclasslib來查看
spel:
這裏的hook點時init方法,這裏的expression即爲表達式
其init方法中aload1對應賦值時的棧頂元素,因此其爲表達式,所以下標對應的是1
mvel:
這個用的局部變量表的下標也是1,然而實際上取表達式值時用的爲下標爲0的this來取
根據局部變量表中的表達式的值傳入expression方法進行處理
其中expression將打印出當前的函數調用棧,該例子只是一個插樁+hook方法字節碼修改的例子,並無最終的判斷入侵的檢測規則
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點:
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); }
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對類加載機制的攔截
1.代碼審計
能夠對一些漏洞,好比反序列化,ognl、spel等的關鍵函數處進行hook並記錄,而後能夠輸出成相似日誌的格式,結合其調用棧以及其入口參數提供給白盒代碼審計工具進行自動化審計
2.0day捕獲
對一些危險函數進行hook,並在執行時及時告警,好比Runtime.exec,Processs,可是我的感受這樣效率可能有點低,不如交給ids進行捕獲效率更高
3.DevOps
由於進行hook時,asm中提供了大量有用的方法從而可以得到hook點處詳細的信息:調用棧、代碼行號、接口、父類等
1.首先rasp攔截是侵入程序代碼內部的,那麼它其實是和具體的語言強相關的,所以不一樣語言之間並不通用,需針對不一樣語言的特性進行開發
2.rasp是對關鍵函數進行hook,那麼意味着不管攻擊路徑從哪條路走,最終都將聚集於某一個點,所以高效率的攔截要求設計rasp的hook規則時,開發者自己即必須對各類漏洞的利用方式以及一些關鍵函數點熟悉,所以存在遺漏的可能。
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/
http://www.javashuo.com/article/p-mcosbzok-ha.html 有例子
https://www.anquanke.com/post/id/195016#h2-3 rasp繞過
https://www.freebuf.com/articles/web/217421.html openrasp梳理