前言

最近一直在做学校实验室安排的项目,太惨了,没多少时间学习新知识,不过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的应用

Java openrasp学习记录(一)的更多相关文章

  1. Java openrasp学习记录(二)

    Author:tr1ple 主要分析以下四个部分: 1.openrasp agent 这里主要进行插桩的定义,其pom.xml中定义了能够当类重新load时重定义以及重新转换 这里定义了两种插桩方式对 ...

  2. Java设计模式学习记录-模板方法模式

    前言 模板方法模式,定义一个操作中算法的骨架,而将一些步骤延迟到子类中.使得子类可以不改变一个算法的结构即可重新定义该算法的某些特定步骤. 模板方法模式 概念介绍 模板方法模式,其实是很好理解的,具体 ...

  3. Java设计模式学习记录-状态模式

    前言 状态模式是一种行为模式,用于解决系统中复杂的对象状态转换以及各个状态下的封装等问题.状态模式是将一个对象的状态从该对象中分离出来,封装到专门的状态类中,使得对象的状态可以灵活多变.这样在客户端使 ...

  4. Java设计模式学习记录-观察者模式

    前言 观察者模式也是对象行为模式的一种,又叫做发表-订阅(Publish/Subscribe)模式.模型-视图(Model/View)模式. 咱们目前用的最多的就是各种MQ(Message Queue ...

  5. Java设计模式学习记录-备忘录模式

    前言 这次要介绍的是备忘录模式,也是行为模式的一种 .现在人们的智能手机上都会有备忘录这样一个功能,大家也都会用,就是为了记住某件事情,防止以后自己忘记了.那么备忘录模式又是什么样子的呢?是不是和手机 ...

  6. Java设计模式学习记录-迭代器模式

    前言 这次要介绍的是迭代器模式,也是一种行为模式.我现在觉得写博客有点应付了,前阵子一天一篇,感觉这样其实有点没理解透彻就写下来了,而且写完后自己也没有多看几遍,上次在面试的时候被问到java中的I/ ...

  7. Java设计模式学习记录-解释器模式

    前言 这次介绍另一个行为模式,解释器模式,都说解释器模式用的少,其实只是我们在日常的开发中用的少,但是一些开源框架中还是能见到它的影子,例如:spring的spEL表达式在解析时就用到了解释器模式,以 ...

  8. Java设计模式学习记录-命令模式

    前言 这次要介绍的是命令模式,这也是一种行为型模式.最近反正没有面试机会我就写博客呗,该投的简历都投了.然后就继续看书,其实看书也会给自己带来成就感,原来以前不明白的东西,书上已经给彻底的介绍清楚了, ...

  9. Java设计模式学习记录-享元模式

    前言 享元模式也是一种结构型模式,这篇是介绍结构型模式的最后一篇了(因为代理模式很早之前就已经写过了).享元模式采用一个共享来避免大量拥有相同内容对象的开销.这种开销最常见.最直观的就是内存损耗. 享 ...

随机推荐

  1. Matplotlib 误差线的绘制和子图的创建方式

    一.绘制误差线 使用errorbar方法可以绘制误差线. x = np.linspace(0,10,50) dy=0.8 y = np.cos(x) + dy*np.random.randn(50) ...

  2. css3--弹性布局

    来源:https://www.cnblogs.com/xuyuntao/articles/6391728.html

  3. 存储-raid

  4. VIM 批量缩进4个空格

    vim  /etc/vimrc  或  vim  ~/.vimrc set smartindent set shiftwidth= 按v选中多行,回车 然后shifit + >

  5. java关于throw Exception的一个小秘密

    目录 简介 throw小诀窍 总结 java关于throw Exception的一个小秘密 简介 之前的文章我们讲到,在stream中处理异常,需要将checked exception转换为unche ...

  6. Android Studio快捷键动态演示

    Android Studio出来很久了,大部分已经转过来了,相对于Eclipse又是毋庸置疑,更好的使用快捷键必定达到事半功倍的效果. 友情提示:某些电脑按F1-F12键需要先按住FN,比如我的Mac ...

  7. Tomcat 8 Host-Manager配置访问的方法,全网唯一正确配置

    2019独角兽企业重金招聘Python工程师标准>>> 环境: 操作系统:         Linux version 2.6.32-696.10.1.el6.x86_64 (moc ...

  8. 图论--网络流--最大流 洛谷P4722(hlpp)

    题目描述 给定 nn 个点,mm 条有向边,给定每条边的容量,求从点 ss 到点 tt 的最大流. 输入格式 第一行包含四个正整数nn.mm.ss.tt,用空格分隔,分别表示点的个数.有向边的个数.源 ...

  9. Java——多线程之方法详解

    Java多线程系列文章是Java多线程的详解介绍,对多线程还不熟悉的同学可以先去看一下我的这篇博客Java基础系列3:多线程超详细总结,这篇博客从宏观层面介绍了多线程的整体概况,接下来的几篇文章是对多 ...

  10. Java——理解面向对象

    1.程序设计的三种基本结构 顺序结构 顺序结构表示程序中的各操作是按照它们在源代码中的排列顺序依次执行的 选择结构 选择结构表示程序的处理需要根据某个特定的条件选择其中的一个分支执行.选择结构有单选择 ...