https://github.com/baidu/openrasp
When an attack happens, WAF matches the malicious request with its signatures and blocks it. OpenRASP takes a different approach by hooking sensitive functions and examines/blocks the inputs fed into them. As a result, this examination is context-aware and in-place. It brings in the following benefits:
从github clone下来项目之后,我们可以看到具体的目录大致构成是这样的:
1 |
LICENSE build-cloud.sh build-php7.sh docker plugins rasp-vue travis |
而我主要关心的是:
1 |
-install |
其实就是对OpenRASP的安装和卸载做的封装,interfece Installer和interface Uninstaller分别是它们的抽象定义
1 |
public interface Installer { |
包中都是对Installer、Uninstaller基于不同操作系统、web服务器的实现,并通过了工厂模式,根据参数、环境变量、目录信息特征等,选择对应的实现
1 |
public abstract class InstallerFactory |
OpenRASP的安装程序主要入口位于:
1 |
package com.baidu.rasp |
1 |
public static void main(String[] args) { |
主要代码:
1 |
public static void operateServer(String[] args) throws RaspError, ParseException, IOException { |
代码跟进:
1 |
install:指定该操作为安装 |
1 |
appId、appSecret、raspId、url、heartbeatInterval |
1 |
private static InstallerFactory newInstallerFactory() { |
获取安装实例:
1 |
public Installer getInstaller(File serverRoot, boolean noDetect) throws RaspError { |
可以看到,对于使用了启动参数nodetect的安装,选择的是GenericInstaller通用安装实例,否则会通过detectServerName(String serverRoot)方法进行web服务器的特征检测
1 |
public static String detectServerName(String serverRoot) throws RaspError { |
特征检测的方式,无一不是通过检测特定目录是否存在shell脚本实现
执行安装:
安装核心方法:install()
先是根据当前jar的目录获取到其子目录rasp,若不存在则新建
1 |
String jarPath = getLocalJarPath(); |
接着通过设定的安装目录,检测openrasp.yml是否存在,用以判断是否第一次安装,然后拷贝rasp文件夹至目标安装目录
1 |
File installDir = new File(getInstallPath(serverRoot)); |
删除官方js插件
1 |
//安装rasp开启云控,删除官方插件 |
若不是第一次安装,则会把目标安装目录下原有配置文件修改名称为openrasp.yml.bak,然后拷贝当前jar目录下的openrasp.yml到目标安装目录的conf子目录
1 |
// 生成配置文件 |
1 |
private boolean generateConfig(String dir, boolean firstInstall) { |
其中通过setCloudConf()方法,把云控所需的程序启动参数,写到配置文件openrasp.yml中
1 |
appid:OpenRASP连接到RASP Cloud的认证appid |
写入的格式(yml):
1 |
cloud: |
相对于通用安装的主要流程,它们并没有什么区别,区别仅仅在于配置文件写完后,TomcatInstaller会tomcat的安装目录下的bin/catalina.sh脚本进行修改
1 |
位于:com.baidu.rasp.install.BaseStandardInstaller#generateStartScript |
在修改脚本时,会找到对应的位置写入或删除原有OpenRASP内容,写入新的脚本,然后根据程序启动参数prepend选择插入不同的rasp启动方式:
比web容器bootstrap更先启动:
1 |
private static String PREPEND_JAVA_AGENT_CONFIG = "\tJAVA_OPTS=\"${JAVA_OPTS} -javaagent:${CATALINA_HOME}/rasp/rasp.jar\"\n"; |
较web容器bootstrap更后启动:
1 |
private static String JAVA_AGENT_CONFIG = "\tJAVA_OPTS=\"-javaagent:${CATALINA_HOME}/rasp/rasp.jar ${JAVA_OPTS}\"\n"; |
1 |
/** |
入口代码:
1 |
/** |
具有两种方式的启动,一种是JVMTI调用premain方式,一种是attach机制加载agent的方式。
1 |
String START_MODE_ATTACH = "attach"; |
核心:
1 |
/** |
1 |
/** |
可以看到,这里的实现是创建容器并启动,容器的实现是rasp-engine.jar,如果细看ModuleContainer的源码,可以发现,在其构造方法中,读取了rasp-engine.jar中MANIFEST.MF文件的Rasp-Module-Name、Rasp-Module-Class信息,此信息用于指定rasp-engine.jar中module容器的实现类,然后agent中的module加载器根据此信息加载module容器并调用start方法启动
入口代码(com.baidu.openrasp.EngineBoot):
1 |
@Override |
可以看到,一共就做了以下这些工作:
1 |
private boolean loadConfig() throws Exception { |
LogConfig.ConfigFileAppender():初始化log4j
CloudUtils.checkCloudControlEnter():检查云控配置信息
LogConfig.syslogManager():读取配置信息,初始化syslog服务连接
为V8配置java的logger以及栈堆信息Getter(用于在js中获取当前栈堆信息)
1 |
public synchronized static boolean Initialize() { |
UpdatePlugin():读取plugins目录下的js文件,过滤掉大于10MB的js文件,然后全部读入,最后加载到V8引擎中
1 |
public synchronized static boolean UpdatePlugin() { |
这里有一个commonLRUCache,主要是用于在hook点去执行js check的时候,进行一个并发幂等(应该是这样。。。)。
InitFileWatcher():初始化一个js plugin监视器,在js文件有所变动的时候,重新去加载所有插件,实现热更新的特性
1 |
public synchronized static void InitFileWatcher() throws Exception { |
1 |
// js插件检测 |
1 |
/** |
可以看到,addAnnotationHook()读取了com.baidu.openrasp.hook包中所有被@HookAnnotation注解的class,然后缓存到集合hooks中,以提供在后续类加载通过com.baidu.openrasp.transformer.CustomClassTransformer#transform的时候,对其进行匹配,判断是否需要hook
1 |
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, |
看细节,可以发现,先根据isClassMatched(String className)方法判断是否对加载的class进行hook,接着调用的是hook类的transformClass(CtClass ctClass)->hookMethod(CtClass ctClass)方法进行了字节码的修改(hook),然后返回修改后的字节码并加载,最终实现了对class进行插桩
例子(com.baidu.openrasp.hook.ssrf.HttpClientHook):
HttpClient中发起请求前,都会先创建HttpRequestBase这个类的实例,然后才能发起请求,该实例中包含着URI信息,而对于SSRF的攻击检测,就是在请求发起前,对URI进行检测,检测是否是SSRF,因此需要hook到HttpRequestBase类
1 |
public boolean isClassMatched(String className) { |
既然要检测SSRF,那么就选择在setURI时,就对其URI进行检测,hookMethod方法其实就是通过javassist生成了一段调用com.baidu.openrasp.hook.ssrf.HttpClientHook#checkHttpUri方法的代码,并插入到HttpRequestBase.setURI方法中,以实现检测SSRF
1 |
protected void hookMethod(CtClass ctClass) throws IOException, CannotCompileException, NotFoundException { |
checkHttpUri方法通过取出相关信息,host、port、url等,然后通过一系列方法,对检测ssrf的js插件进行调用以检测攻击,当然,过程中会加入一些机制,对其可用性的增强
1 |
public static void checkHttpUri(URI uri) { |
流程汇总:
1 |
1.com.baidu.openrasp.hook.ssrf.HttpClientHook#checkHttpUri |
总的来说,大概整个OpenRASP的核心就是如此了,还有一些关于cloud的云控实现,这里的篇幅暂且不对其就行研究