最近在学习RASP相关内容,正好OpenRASP开源,大概分析了一下流程。参考了网上师傅们的分析加上自己的理解就有了这篇文章。如有错误或不足,还望指正。
RASP,全称“Runtime application self-protection”,运行时应用程序自我保护技术。它通过JavaAgent将代码注入到某些方法中,与应用程序融为一体,使应用程序具备自我防护能力,当应用程序遭受到实际攻击伤害时,能实时检测和阻断安全攻击,而不需要进行人工干预。
WAF拦截原始http数据包,然后使用规则对数据包进行匹配扫描,如果不满足预设的所有规则,则视为安全,可通过WAF。这样就会造成规则缺失,放行危险行为;同时WAF只是对程序外层进行防护,当数据进入程序后,无法获取后续的行为。
WAF依靠特征检测攻击,但会造成一定的误报率,而RASP审查的是最终要执行的代码。
RASP插桩到代码层面,可以记录详细的栈堆跟踪信息
正是因为RASP需要侵入到代码层面,导致必然会造成一定的性能损耗,并且一个不合格的rasp更容易影响到业务代码
DAST:动态应用程序安全测试(Dynamic Application Security Testing)技术在测试或运行阶段分析应用程序的动态运行状态。它模拟黑客行为对应用程序进行动态攻击,分析应用程序的反应,从而确定该Web应用是否易受攻击。
SAST:静态应用程序安全测试(Static Application Security Testing)技术通常在编码阶段分析应用程序的源代码或二进制文件的语法、结构、过程、接口等来发现程序代码存在的安全漏洞。
IAST:交互式应用程序安全测试(Interactive Application Security Testing)是2012年Gartner公司提出的一种新的应用程序安全测试方案,通过代理、VPN或者在服务端部署Agent程序,收集、监控Web应用程序运行时函数执行、数据传输,并与扫描器端进行实时交互,高效、准确的识别安全缺陷及漏洞,同时可准确确定漏洞所在的代码文件、行数、函数及参数。IAST相当于是DAST和SAST结合的一种互相关联运行时安全检测技术。
正如Thoughtworks官网所描述的那样:
IAST:交互式应用程序安全测试。监控应用程序在运行时的安全漏洞——测试时间。
RASP:运行时应用程序自我保护。监控应用程序,以检测其运行时的攻击——生产时间。
IAST 和 RASP 是应用程序运行时寻找问题的安全工具。对于 IAST,作为测试过程的一部分,扫描漏洞。,RAST 检测生产环境中的攻击。
OpenRASP是百度在2017年针对RASP概念推出的一款开源免费的自适应安全产品,官网。
这个项目使用v8引擎载入js并实现热加载,通过编写js规则对攻击进行检测,这一点还是比较吸引人的。
另一个引入JS的原因是OpenRASP会支持PHP、DotNet、NodeJS、Python、Ruby等多种开发语言,为了避免在不同平台上重新实现检测逻辑,所以引入了插件系统,选择JS作为插件开发语言。
这里我主要分析了对于Java版本的系统架构与具体的执行流程。
java 版本使用 javaagent 机制来实现。在服务器启动时,可动态的修改Java字节码,对敏感操作的函数进行hook,比如:
当服务器发生攻击,就会触发这些Hook点,此时RASP agent就可以获取到函数的参数,比如要读取的文件名、要执行的命令等等。
Java 版本的系统架构可以参考官方手册
这部分的源码在openrasp/agent/java/boot
on load
的方式进行代理。启动时首先会进入 com.baidu.openrasp.Agent
的 premain
函数进行初始化操作(init()
),该函数会在 main 函数之前预先执行。//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(); } }
init()
方法中首先利用inst.appendToBootstrapClassLoaderSearch(new JarFile(localJarPath))
将自身添加到 BootstrapClassLoader
的ClassPath下。BootstrapClassLoader
加载的,则无法从该类调用非 BootstrapClassLoader
加载的类中的代码。要解决这个问题,就应该想办法把这种类通过BootstrapClassLoader进行加载。OpenRASP中通过调用appendToBootstrapClassLoaderSearch
方法,可以把一个jar包放到Bootstrap ClassLoader的搜索路径。这样的话,当Bootstrap ClassLoader检查自身加载过的类,发现没有找到目标类时,会在指定的jar文件中搜索。官方文档也做了如下解释:java.io.File
这样由 BootstrapClassLoader
加载的类的时候,无法从该类调用非 BootstrapClassLoader
加载的类中的接口,所以 agent.jar
会先将自己添加到 BootstrapClassLoader
的ClassPath下,这样 hook 由 BootstrapClassLoader
加载的类的时候就能够成功调用到 agent.jar
中的检测入口//com.baidu.openrasp.JarFileHelper public static void addJarToBootstrap(Instrumentation inst) throws IOException { String localJarPath = getLocalJarPath(); inst.appendToBootstrapClassLoaderSearch(new JarFile(localJarPath)); }
setStartupOptionForJboss();
这里按照源码的解释应该是判断当前进程是否为jboss7 版本,并设置相关属性和预加载包。//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()
,启动引擎。
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); }
这部分的源码在openrasp/agent/java/engine
这部分我主要是参考三梦师傅的分析流程进行对比分析。
代码入口就是上文获取的com.baidu.openrasp.EngineBoot#start()
,代码虽然有改动,但流程没变:
doCheck()
方法中的type参数就是这里的Type。// 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);
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中,如下图所示
这个hooks用来提供在后续类加载通过com.baidu.openrasp.transformer.CustomClassTransformer#transform的时候,对其进行匹配,判断是否需要hook
//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
包下的CloseableHttpClient
、AutoRetryHttpClient
、DecompressingHttpClient
和AbstractHttpClient
这四个方法。
所以当当前className等于其中之一时,进入if语句进行hook。
进入if语句,通过transformClass(CtClass ctClass)
方法调用HttpClientHook#hookMethod
,这个方法也在上面的commit中进行同步的修改
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. 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工作流程为:(配置文件那些暂不描述)
@HookAnnotation
注解,并重写isClassMatched()
方法,然后将所有带有@HookAnnotation
注解的类添加到hooks集合中。这样在执行到相应的底层类时能够根据hook.isClassMatched()
的返回值判断是否需要hook。HttpClientHook#hookMethod()
对包含不同参数的org.apache.http.client.HttpClient#execute
方法添加不同的代码。后续会考虑相关绕过方式,比如到官网查找不常用的、危险的底层函数,绕过OpenRASP的hook;绕过OpenRASP设置的规则;之前也有师傅通过开启新线程,在新线程中进行命令执行的方式绕过OpenRASP,或是通过全局的Hook开关关闭rasp。
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/