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

  1. OpenRASP使用了 on load 的方式进行代理。启动时首先会进入 com.baidu.openrasp.Agentpremain 函数进行初始化操作(init()),该函数会在 main 函数之前预先执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//com.baidu.openrasp.Agent
public static void premain(String agentArg, Instrumentation inst) {
init(START_MODE_NORMAL, START_ACTION_INSTALL, inst);
}
public static synchronized void init(String mode, String action, Instrumentation inst) {
try {
JarFileHelper.addJarToBootstrap(inst);
//读取MANIFEST.MF相关信息
readVersion();
ModuleLoader.load(mode, action, inst);
} catch (Throwable e) {
System.err.println("[OpenRASP] Failed to initialize, will continue without security protection.");
e.printStackTrace();
}
}
  1. 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
2
3
4
5
//com.baidu.openrasp.JarFileHelper
public static void addJarToBootstrap(Instrumentation inst) throws IOException {
String localJarPath = getLocalJarPath();
inst.appendToBootstrapClassLoaderSearch(new JarFile(localJarPath));
}
  1. 加载和初始化引擎模块(rasp-engine.jar)
    ModuleLoader类的静态方法将moduleClassLoader设置为ExtClassLoader,在ModuleLoader构造函数中实例化ModuleContainer,并调用其start()方法。
    在ModuleLoader构造函数中还会执行setStartupOptionForJboss();这里按照源码的解释应该是判断当前进程是否为jboss7 版本,并设置相关属性和预加载包。
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
//com.baidu.openrasp.ModuleLoader
static {
// juli
try {
Class clazz = Class.forName("java.nio.file.FileSystems");
clazz.getMethod("getDefault", new Class[0]).invoke(null);
} catch (Throwable t) {
// ignore
}
Class clazz = ModuleLoader.class;
// path值示例: file:/opt/apache-tomcat-xxx/rasp/rasp.jar!/com/fuxi/javaagent/Agent.class
String path = clazz.getResource("/" + clazz.getName().replace(".", "/") + ".class").getPath();
if (path.startsWith("file:")) {
path = path.substring(5);
}
if (path.contains("!")) {
path = path.substring(0, path.indexOf("!"));
}
try {
baseDirectory = URLDecoder.decode(new File(path).getParent(), "UTF-8");
} catch (UnsupportedEncodingException e) {
baseDirectory = new File(path).getParent();
}
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
while (systemClassLoader.getParent() != null
&& !systemClassLoader.getClass().getName().equals("sun.misc.Launcher$ExtClassLoader")) {
systemClassLoader = systemClassLoader.getParent();
}
moduleClassLoader = systemClassLoader;
}

/**
* 构造所有模块
*
* @param mode 启动模式
* @param inst {@link java.lang.instrument.Instrumentation}
*/
private ModuleLoader(String mode, Instrumentation inst) throws Throwable {
if (Module.START_MODE_NORMAL == mode) {
setStartupOptionForJboss();
}
engineContainer = new ModuleContainer(ENGINE_JAR);
engineContainer.start(mode, inst);
}

/**
* 加载所有 RASP 模块
*
* @param mode 启动模式
* @param inst {@link java.lang.instrument.Instrumentation}
*/
public static synchronized void load(String mode, String action, Instrumentation inst) throws Throwable {
if (Module.START_ACTION_INSTALL.equals(action)) {
if (instance == null) {
try {
instxance = new ModuleLoader(mode, inst);
} catch (Throwable t) {
instance = null;
throw t;
}
} else {
System.out.println("[OpenRASP] The OpenRASP has bean initialized and cannot be initialized again");
}
} else if (Module.START_ACTION_UNINSTALL.equals(action)) {
release(mode);
} else {
throw new IllegalStateException("[OpenRASP] Can not support the action: " + action);
}
}

在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
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
private Module module;
private String moduleName;
//jarName = rasp-engine.jar
public ModuleContainer(String jarName) throws Throwable {
try {
File originFile = new File(baseDirectory + File.separator + jarName);
JarFile jarFile = new JarFile(originFile);
Attributes attributes = jarFile.getManifest().getMainAttributes();
jarFile.close();
this.moduleName = attributes.getValue("Rasp-Module-Name");
String moduleEnterClassName = attributes.getValue("Rasp-Module-Class");
if (moduleName != null && moduleEnterClassName != null
&& !moduleName.equals("") && !moduleEnterClassName.equals("")) {
Class moduleClass;
if (ClassLoader.getSystemClassLoader() instanceof URLClassLoader) {
Method method = Class.forName("java.net.URLClassLoader").getDeclaredMethod("addURL", URL.class);
method.setAccessible(true);
method.invoke(moduleClassLoader, originFile.toURI().toURL());
method.invoke(ClassLoader.getSystemClassLoader(), originFile.toURI().toURL());
moduleClass = moduleClassLoader.loadClass(moduleEnterClassName);
module = (Module) moduleClass.newInstance();
} else if (ModuleLoader.isCustomClassloader()) {
moduleClassLoader = ClassLoader.getSystemClassLoader();
Method method = moduleClassLoader.getClass().getDeclaredMethod("appendToClassPathForInstrumentation", String.class);
method.setAccessible(true);
try {
method.invoke(moduleClassLoader, originFile.getCanonicalPath());
} catch (Exception e) {
method.invoke(moduleClassLoader, originFile.getAbsolutePath());
}
moduleClass = moduleClassLoader.loadClass(moduleEnterClassName);
module = (Module) moduleClass.newInstance();
} else {
throw new Exception("[OpenRASP] Failed to initialize module jar: " + jarName);
}
}
} catch (Throwable t) {
System.err.println("[OpenRASP] Failed to initialize module jar: " + jarName);
throw t;
}
}

public void start(String mode, Instrumentation inst) throws Throwable {
module.start(mode, inst);
}

engine_MANIFEST

OpenRASP的执行流程

这部分的源码在openrasp/agent/java/engine
这部分我主要是参考三梦师傅的分析流程进行对比分析。
代码入口就是上文获取的com.baidu.openrasp.EngineBoot#start(),代码虽然有改动,但流程没变:

  1. 输出banner信息
  2. Loader.load():V8引擎的加载,用于解释执行JavaScript
  3. loadConfig():初始化配置
    • LogConfig.ConfigFileAppender():初始化log4j
    • CloudUtils.checkCloudControlEnter():检查云控配置信息
    • LogConfig.syslogManager():读取配置信息,初始化syslog服务连接
  4. Agent.readVersion();BuildRASPModel.initRaspInfo():缓存rasp的build信息
  5. JS.Initialize():初始化插件系统
    • 为V8配置java的logger以及栈堆信息Getter(用于在js中获取当前栈堆信息)
    • UpdatePlugin():读取plugins目录下的js文件,过滤掉大于10MB的js文件,然后全部读入,最后加载到V8引擎中
    • 这里有一个commonLRUCache,主要是用于在hook点去执行js check的时候,进行一个并发幂等
    • InitFileWatcher():初始化一个js plugin监视器((JnotifyWatcher实现文件监控),在js文件有所变动的时候,重新去加载所有插件,实现热更新的特性
  6. 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);
  7. initTransformer(inst):核心代码,通过加载class,在加载前使用javassist对其进行hook插桩,以实现rasp的攻击检测功能,这里会重点分析。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    private 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);
    }
    }
    }
    addAnnotationHook()读取了com.baidu.openrasp.hook包中所有被@HookAnnotation注解的class,然后缓存到集合hooks中,如下图所示
    1

这个hooks用来提供在后续类加载通过com.baidu.openrasp.transformer.CustomClassTransformer#transform的时候,对其进行匹配,判断是否需要hook

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
48
//com.baidu.openrasp.transformer.CustomClassTransformer
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain domain, byte[] classfileBuffer) throws IllegalClassFormatException {
if (loader != null) {
DependencyFinder.addJarPath(domain);
}
if (loader != null && jspClassLoaderNames.contains(loader.getClass().getName())) {
jspClassLoaderCache.put(className.replace("/", "."), new SoftReference<ClassLoader>(loader));
}
for (final AbstractClassHook hook : hooks) {
if (hook.isClassMatched(className)) {
CtClass ctClass = null;
try {
ClassPool classPool = new ClassPool();
addLoader(classPool, loader);
ctClass = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));
if (loader == null) {
hook.setLoadedByBootstrapLoader(true);
}
classfileBuffer = hook.transformClass(ctClass);
if (classfileBuffer != null) {
checkNecessaryHookType(hook.getType());
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (ctClass != null) {
ctClass.detach();
}
}
}
}
serverDetector.detectServer(className, loader, domain);
return classfileBuffer;
}

//com.baidu.openrasp.hook.AbstractClassHook
public byte[] transformClass(CtClass ctClass) {
try {
hookMethod(ctClass);
return ctClass.toBytecode();
} catch (Throwable e) {
if (Config.getConfig().isDebugEnabled()) {
LOGGER.info("transform class " + ctClass.getName() + " failed", e);
}
}
return null;
}

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包下的CloseableHttpClientAutoRetryHttpClientDecompressingHttpClientAbstractHttpClient这四个方法。
所以当当前className等于其中之一时,进入if语句进行hook。
进入if语句,通过transformClass(CtClass ctClass)方法调用HttpClientHook#hookMethod,这个方法也在上面的commit中进行同步的修改

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
protected void hookMethod(CtClass ctClass) throws IOException, CannotCompileException, NotFoundException {
CtClass[] interfaces = ctClass.getInterfaces();
if (interfaces != null) {
for (CtClass inter : interfaces) {
// 兼容 http client 4.0 版本的 AbstractHttpClient
if (inter.getName().equals("org.apache.http.client.HttpClient")) {
LinkedList<CtBehavior> methods =
getMethod(ctClass, "execute", null, null);
String afterSrc = getInvokeStaticSrc(HttpClientHook.class, "exitCheck",
"$1,$_", Object.class, Object.class);
for (CtBehavior method : methods) {
if (method.getSignature().startsWith("(Lorg/apache/http/client/methods/HttpUriRequest")) {
String src = getInvokeStaticSrc(HttpClientHook.class,
"checkHttpUri", "$1", Object.class);
insertBefore(method, src);
insertAfter(method, afterSrc, true);
} else if (method.getSignature().startsWith("(Lorg/apache/http/HttpHost")) {
String src = getInvokeStaticSrc(HttpClientHook.class,
"checkHttpHost", "$1", Object.class);
insertBefore(method, src);
insertAfter(method, afterSrc, true);
}
}
break;
}
}
}
}

public static void checkHttpUri(Object uriValue) {
if (!isChecking.get() && uriValue != null) {
isChecking.set(true);
URI uri = (URI) Reflection.invokeMethod(uriValue, "getURI", new Class[]{});
checkHttpUrl(getSsrfParamFromURI(uri));
}
}

public static void checkHttpHost(Object host) {
if (!isChecking.get() && host != null) {
isChecking.set(true);
checkHttpUrl(getSsrfParamFromHostValue(host));
}
}

public static void exitCheck(Object uriValue, Object response) {
try {
if (isChecking.get() && response != null) {
URI redirectUri = HttpClientRedirectHook.uriCache.get();
if (redirectUri != null) {
HashMap<String, Object> params = getSsrfParam(uriValue);
if (params != null) {
HashMap<String, Object> redirectParams = getSsrfParamFromURI(redirectUri);
if (redirectParams != null) {
AbstractRedirectHook.checkHttpClientRedirect(params, redirectParams, response);
}
}
}
}
} finally {
isChecking.set(false);
HttpClientRedirectHook.uriCache.set(null);
}
}

可以清晰的看到对于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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
1. com.baidu.openrasp.hook.ssrf.AbstractSSRFHook#checkHttpUrl

2. com.baidu.openrasp.HookHandler#doCheck

3. com.baidu.openrasp.HookHandler#doCheckWithoutRequest
在这里,做了一些云控注册成功判断和白名单的处理。
这里有两个注意点:
1)当服务器的cpu使用率超过90%,禁用全部hook点。(这里能否DDOS,让CPU高负载,使用率超过90%,禁用全部hook点?)(2)当云控注册成功之前,不进入任何hook点

4. com.baidu.openrasp.HookHandler#doRealCheckWithoutRequest
在这里,做了一些参数的封装,以及失败日志、耗时日志等输出,并且在检测到攻击时(下一层返回),抛出异常

5. com.baidu.openrasp.plugin.checker.CheckerManager#check

6. com.baidu.openrasp.plugin.checker.AbstractChecker#check
在这里,对js或者其他类型的安全检测之后的结果,进行事件处理并返回结果

7. com.baidu.openrasp.plugin.checker.v8.V8AttackChecker#checkParam

8.com.baidu.openrasp.plugin.js.JS#Check
在这里,做了一些commonLRUCache的并发幂等处理

9.com.baidu.openrasp.v8.V8#Check(java.lang.String, byte[], int, com.baidu.openrasp.v8.Context, boolean, int)
因为在UpdatePlugin()中就读取plugins目录下的js文件并加载到V8引擎中,在InitFileWatcher()中初始化一个js plugin监视器,在js文件有所变动的时候,重新去加载所有插件,实现热更新的特性。所以这里就相当于要使用v8引擎调用之前写好的js进行check。

总结一下,OpenRASP工作流程为:(配置文件那些暂不描述)

  1. 准备阶段:
    • 将漏洞进行分类,然后加上@HookAnnotation注解,并重写isClassMatched()方法,然后将所有带有@HookAnnotation注解的类添加到hooks集合中。这样在执行到相应的底层类时能够根据hook.isClassMatched()的返回值判断是否需要hook。
    • 初始化v8引擎,并将js加载到其中,同时初始化一个js plugin监视器以实现热更新。
  2. 检测阶段:
    • 对于当前类属于准备阶段中设置的hooks集合中时,进行hook操作。具体的hook形式在hook.hookMethod()方法中实现,比如HttpClientHook#hookMethod()对包含不同参数的org.apache.http.client.HttpClient#execute方法添加不同的代码。
      • 添加不同的check代码后最终都会执行诸如doCheck、checkParam等方法,其中在v8引擎中调用js,实现对不同攻击的检测。

后续

后续会考虑相关绕过方式,比如到官网查找不常用的、危险的底层函数,绕过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/