前言:
最近一直在做學校實驗室安排的項目,太慘了,沒多少時間學習新知識,不過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的應用