OpenRASP浅析
本文首发于先知 https://xz.aliyun.com/t/11803
最近在学习RASP相关内容,正好OpenRASP开源,大概分析了一下流程。参考了网上师傅们的分析加上自己的理解就有了这篇文章。如有错误或不足,还望指正。
RASP技术
RASP,全称“Runtime application self-protection”,运行时应用程序自我保护技术。它通过JavaAgent将代码注入到某些方法中,与应用程序融为一体,使应用程序具备自我防护能力,当应用程序遭受到实际攻击伤害时,能实时检测和阻断安全攻击,而不需要进行人工干预。
RASP与WAF的比较
WAF拦截原始http数据包,然后使用规则对数据包进行匹配扫描,如果不满足预设的所有规则,则视为安全,可通过WAF。这样就会造成规则缺失,放行危险行为;同时WAF只是对程序外层进行防护,当数据进入程序后,无法获取后续的行为。
WAF依靠特征检测攻击,但会造成一定的误报率,而RASP审查的是最终要执行的代码。
RASP插桩到代码层面,可以记录详细的栈堆跟踪信息
正是因为RASP需要侵入到代码层面,导致必然会造成一定的性能损耗,并且一个不合格的rasp更容易影响到业务代码
DAST、SAST和IAST
DAST:动态应用程序安全测试(Dynamic Application Security Testing)技术在测试或运行阶段分析应用程序的动态运行状态。它模拟黑客行为对应用程序进行动态攻击,分析应用程序的反应,从而确定该Web应用是否易受攻击。
SAST:静态应用程序安全测试(Static Application Security Testing)技术通常在编码阶段分析应用程序的源代码或二进制文件的语法、结构、过程、接口等来发现程序代码存在的安全漏洞。
IAST:交互式应用程序安全测试(Interactive Application Security Testing)是2012年Gartner公司提出的一种新的应用程序安全测试方案,通过代理、VPN或者在服务端部署Agent程序,收集、监控Web应用程序运行时函数执行、数据传输,并与扫描器端进行实时交互,高效、准确的识别安全缺陷及漏洞,同时可准确确定漏洞所在的代码文件、行数、函数及参数。IAST相当于是DAST和SAST结合的一种互相关联运行时安全检测技术。
IAST与RASP
正如Thoughtworks官网所描述的那样:
IAST:交互式应用程序安全测试。监控应用程序在运行时的安全漏洞——测试时间。
RASP:运行时应用程序自我保护。监控应用程序,以检测其运行时的攻击——生产时间。
IAST 和 RASP 是应用程序运行时寻找问题的安全工具。对于 IAST,作为测试过程的一部分,扫描漏洞。RASP 检测生产环境中的攻击。
OpenRASP
OpenRASP是百度在2017年针对RASP概念推出的一款开源免费的自适应安全产品,官网。
这个项目使用v8引擎载入js并实现热加载,通过编写js规则对攻击进行检测,这一点还是比较吸引人的。
另一个引入JS的原因是OpenRASP会支持PHP、DotNet、NodeJS、Python、Ruby等多种开发语言,为了避免在不同平台上重新实现检测逻辑,所以引入了插件系统,选择JS作为插件开发语言。
这里我主要分析了对于Java版本的系统架构与具体的执行流程。
java 版本使用 javaagent 机制来实现。在服务器启动时,可动态的修改Java字节码,对敏感操作的函数进行hook,比如:
- 数据库操作
- 文件读取、写入操作
- 命令执行
- …
当服务器发生攻击,就会触发这些Hook点,此时RASP agent就可以获取到函数的参数,比如要读取的文件名、要执行的命令等等。
系统架构 - Java 版本
Java 版本的系统架构可以参考官方手册
启动流程
这部分的源码在openrasp/agent/java/boot
- OpenRASP使用了
on load
的方式进行代理。启动时首先会进入com.baidu.openrasp.Agent
的premain
函数进行初始化操作(init()
),该函数会在 main 函数之前预先执行。
1 | //com.baidu.openrasp.Agent |
- 在
init()
方法中首先利用inst.appendToBootstrapClassLoaderSearch(new JarFile(localJarPath))
将自身添加到BootstrapClassLoader
的ClassPath下。
这是因为双亲委派机制的存在,类加载器在加载类时无法往下委派加载。当被hook的类需要调用漏洞检测方法的代码时,如果该hook类为BootstrapClassLoader
加载的,则无法从该类调用非BootstrapClassLoader
加载的类中的代码。要解决这个问题,就应该想办法把这种类通过BootstrapClassLoader进行加载。OpenRASP中通过调用appendToBootstrapClassLoaderSearch
方法,可以把一个jar包放到Bootstrap ClassLoader的搜索路径。这样的话,当Bootstrap ClassLoader检查自身加载过的类,发现没有找到目标类时,会在指定的jar文件中搜索。官方文档也做了如下解释:
当去 hook 像java.io.File
这样由BootstrapClassLoader
加载的类的时候,无法从该类调用非BootstrapClassLoader
加载的类中的接口,所以agent.jar
会先将自己添加到BootstrapClassLoader
的ClassPath下,这样 hook 由BootstrapClassLoader
加载的类的时候就能够成功调用到agent.jar
中的检测入口
1 | //com.baidu.openrasp.JarFileHelper |
- 加载和初始化引擎模块(rasp-engine.jar)
ModuleLoader类的静态方法将moduleClassLoader设置为ExtClassLoader,在ModuleLoader构造函数中实例化ModuleContainer,并调用其start()方法。
在ModuleLoader构造函数中还会执行setStartupOptionForJboss();
这里按照源码的解释应该是判断当前进程是否为jboss7 版本,并设置相关属性和预加载包。
1 | //com.baidu.openrasp.ModuleLoader |
在ModuleContainer构造方法中,获取了rasp-engine.jar中MANIFEST.MF文件的Rasp-Module-Name、Rasp-Module-Class字段。然后使用ExtClassLoader加载Rasp-Module-Class,即com.baidu.openrasp.EngineBoot
,并将其实例化,赋值给module变量,然后ModuleLoader构造函数会调用new ModuleContainer(ENGINE_JAR).start()
,继而调用module.start(mode, inst);
即com.baidu.openrasp.EngineBoot#start()
,启动引擎。
1 | private Module module; |
OpenRASP的执行流程
这部分的源码在openrasp/agent/java/engine
这部分我主要是参考三梦师傅的分析流程进行对比分析。
代码入口就是上文获取的com.baidu.openrasp.EngineBoot#start()
,代码虽然有改动,但流程没变:
- 输出banner信息
- Loader.load():V8引擎的加载,用于解释执行JavaScript
- loadConfig():初始化配置
- LogConfig.ConfigFileAppender():初始化log4j
- CloudUtils.checkCloudControlEnter():检查云控配置信息
- LogConfig.syslogManager():读取配置信息,初始化syslog服务连接
- Agent.readVersion();BuildRASPModel.initRaspInfo():缓存rasp的build信息
- JS.Initialize():初始化插件系统
- 为V8配置java的logger以及栈堆信息Getter(用于在js中获取当前栈堆信息)
- UpdatePlugin():读取plugins目录下的js文件,过滤掉大于10MB的js文件,然后全部读入,最后加载到V8引擎中
- 这里有一个commonLRUCache,主要是用于在hook点去执行js check的时候,进行一个并发幂等
- InitFileWatcher():初始化一个js plugin监视器((JnotifyWatcher实现文件监控),在js文件有所变动的时候,重新去加载所有插件,实现热更新的特性。
- CheckerManager.init():初始化所有的checker,从枚举类com.baidu.openrasp.plugin.checker.CheckParameter.Type中读取所有的checker,包含三种类型的checker,一是js插件检测,意味着这个checker会调用js plugin进行攻击检测,二是java本地检测,意味着是调用本地java代码进行攻击检测,三是安全基线检测,是用于检测一些高风险类的安全性基线检测,检测其配置是否有安全隐患。
下面要讲到的doCheck()
方法中的type参数就是这里的Type。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47// js插件检测
SQL("sql", new V8AttackChecker(), 1),
COMMAND("command", new V8AttackChecker(), 1 << 1),
DIRECTORY("directory", new V8AttackChecker(), 1 << 2),
REQUEST("request", new V8AttackChecker(), 1 << 3),
READFILE("readFile", new V8AttackChecker(), 1 << 5),
WRITEFILE("writeFile", new V8AttackChecker(), 1 << 6),
FILEUPLOAD("fileUpload", new V8AttackChecker(), 1 << 7),
RENAME("rename", new V8AttackChecker(), 1 << 8),
XXE("xxe", new V8AttackChecker(), 1 << 9),
OGNL("ognl", new V8AttackChecker(), 1 << 10),
DESERIALIZATION("deserialization", new V8AttackChecker(),1 << 11),
WEBDAV("webdav", new V8AttackChecker(), 1 << 12),
INCLUDE("include", new V8AttackChecker(), 1 << 13),
SSRF("ssrf", new V8AttackChecker(), 1 << 14),
SQL_EXCEPTION("sql_exception", new V8AttackChecker(), 1 <<15),
REQUESTEND("requestEnd", new V8AttackChecker(), 1 << 17),
LOADLIBRARY("loadLibrary", new V8AttackChecker(), 1 << 20),
//js插件检测-删除
DUBBOREQUEST("dubboRequest", new V8Checker(), 1 << 4),
//js插件检测-新增
DELETEFILE("deleteFile", new V8AttackChecker(), 1 << 18),
MONGO("mongodb", new V8AttackChecker(), 1 << 19),
SSRF_REDIRECT("ssrfRedirect", new V8AttackChecker(), 1 <<21),
RESPONSE("response", new V8AttackChecker(false), 1 << 23),
LINK("link", new V8AttackChecker(), 1 << 24),
JNDI("jndi", new V8AttackChecker(), 1 << 25),
DNS("dns", new V8AttackChecker(), 1 << 26),
// java本地检测
XSS_USERINPUT("xss_userinput", new XssChecker(), 1 << 16),
SQL_SLOW_QUERY("sqlSlowQuery", new SqlResultChecke(false), 0),
// 安全基线检测
POLICY_SQL_CONNECTION("sqlConnection", newSqlConnectionChecker(false), 0),
POLICY_SERVER_TOMCAT("tomcatServer", newTomcatSecurityChecker(false), 0),
POLICY_SERVER_JBOSS("jbossServer", new JBossSecurityChecke(false), 0),
POLICY_SERVER_JBOSSEAP("jbossEAPServer", newJBossEAPSecurityChecker(false), 0),
POLICY_SERVER_JETTY("jettyServer", new JettySecurityChecke(false), 0),
POLICY_SERVER_RESIN("resinServer", new ResinSecurityChecke(false), 0),
POLICY_SERVER_WEBSPHERE("websphereServer", newWebsphereSecurityChecker(false), 0),
POLICY_SERVER_WEBLOGIC("weblogicServer", newWeblogicSecurityChecker(false), 0),
POLICY_SERVER_WILDFLY("wildflyServer", newWildflySecurityChecker(false), 0),
POLICY_SERVER_TONGWEB("tongwebServer", newTongwebSecurityChecker(false), 0),
// 安全基线检测-新增
POLICY_LOG("log", new LogChecker(false), 1 << 22),
POLICY_MONGO_CONNECTION("mongoConnection", newMongoConnectionChecker(false), 0),
POLICY_SERVER_BES("bes", new BESSecurityChecker(false), 0); - initTransformer(inst):核心代码,通过加载class,在加载前使用javassist对其进行hook插桩,以实现rasp的攻击检测功能,这里会重点分析。addAnnotationHook()读取了com.baidu.openrasp.hook包中所有被@HookAnnotation注解的class,然后缓存到集合hooks中,如下图所示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24private void initTransformer(Instrumentation inst) throws UnmodifiableClassException {
transformer = new CustomClassTransformer(inst);
transformer.retransform();
}
public CustomClassTransformer(Instrumentation inst) {
this.inst = inst;
inst.addTransformer(this, true);
addAnnotationHook();
}
private void addAnnotationHook() {
Set<Class> classesSet = AnnotationScanner.getClassWithAnnotation(SCAN_ANNOTATION_PACKAGE, HookAnnotation.class);
for (Class clazz : classesSet) {
try {
Object object = clazz.newInstance();
if (object instanceof AbstractClassHook) {
addHook((AbstractClassHook) object, clazz.getName());
}
} catch (Exception e) {
LogTool.error(ErrorType.HOOK_ERROR, "add hook failed: " + e.getMessage(), e);
}
}
}
这个hooks用来提供在后续类加载通过com.baidu.openrasp.transformer.CustomClassTransformer#transform的时候,对其进行匹配,判断是否需要hook
1 | //com.baidu.openrasp.transformer.CustomClassTransformer |
transform方法先根据hook.isClassMatched(String className)方法判断当前类名是否在hooks集合中,进而判断是否对加载的class进行hook。
接着调用的是hook类的transformClass(CtClass ctClass)
->hookMethod(CtClass ctClass)
方法进行了字节码的修改(hook),然后返回修改后的字节码并加载,最终实现了对class进行插桩。
这里依旧拿com.baidu.openrasp.hook.ssrf.HttpClientHook
类举例说明transform()方法的流程
这里hook的类在commit-d437中由org/apache/http/client/methods/HttpRequestBase
修改为org/apache/http/impl/client
包下的CloseableHttpClient
、AutoRetryHttpClient
、DecompressingHttpClient
和AbstractHttpClient
这四个方法。
所以当当前className等于其中之一时,进入if语句进行hook。
进入if语句,通过transformClass(CtClass ctClass)
方法调用HttpClientHook#hookMethod
,这个方法也在上面的commit中进行同步的修改
1 | protected void hookMethod(CtClass ctClass) throws IOException, CannotCompileException, NotFoundException { |
可以清晰的看到对于org.apache.http.client.HttpClient
这个接口中参数包含org.apache.http.client.methods.HttpUriRequest
类型的exitCheck()
方法,会在其方法体前插入checkHttpUri()
代码,并在最后插入exitCheck()
代码进行收尾工作;
对于这个接口中参数包含org.apache.http.HttpHost
类型的exitCheck()
方法,会在其方法体前插入checkHttpHost()
代码,并在最后插入exitCheck()
代码进行收尾工作。
这两个hook方法都会调用checkHttpUrl()
方法,对检测ssrf的js插件进行调用以检测攻击。
调用流程没变,这里直接搬一下三梦师傅的流程汇总(注释有部分增减):
1 | 1. com.baidu.openrasp.hook.ssrf.AbstractSSRFHook#checkHttpUrl |
总结一下,OpenRASP工作流程为:(配置文件那些暂不描述)
- 准备阶段:
- 将漏洞进行分类,然后加上
@HookAnnotation
注解,并重写isClassMatched()
方法,然后将所有带有@HookAnnotation
注解的类添加到hooks集合中。这样在执行到相应的底层类时能够根据hook.isClassMatched()
的返回值判断是否需要hook。 - 初始化v8引擎,并将js加载到其中,同时初始化一个js plugin监视器以实现热更新。
- 将漏洞进行分类,然后加上
- 检测阶段:
- 对于当前类属于准备阶段中设置的hooks集合中时,进行hook操作。具体的hook形式在hook.hookMethod()方法中实现,比如
HttpClientHook#hookMethod()
对包含不同参数的org.apache.http.client.HttpClient#execute
方法添加不同的代码。- 添加不同的check代码后最终都会执行诸如doCheck、checkParam等方法,其中在v8引擎中调用js,实现对不同攻击的检测。
- 对于当前类属于准备阶段中设置的hooks集合中时,进行hook操作。具体的hook形式在hook.hookMethod()方法中实现,比如
后续
后续会考虑相关绕过方式,比如到官网查找不常用的、危险的底层函数,绕过OpenRASP的hook;绕过OpenRASP设置的规则;之前也有师傅通过开启新线程,在新线程中进行命令执行的方式绕过OpenRASP,或是通过全局的Hook开关关闭rasp。
Reference
https://www.thoughtworks.com/zh-cn/insights/decoder/i/iast-rasp
https://rasp.baidu.com/doc/hacking/architect/main.html
https://turn1tup.github.io/2020/09/06/OpenRASP%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/
https://threedr3am.github.io/2019/12/31/OpenRASP%E6%A0%B8%E5%BF%83%E6%BA%90%E7%A0%81%E6%B5%85%E6%9E%90/