0x01 介绍

https://github.com/baidu/openrasp

Introduction

Unlike perimeter control solutions like WAF, OpenRASP directly integrates its protection engine into the application server by instrumentation. It can monitor various events including database queries, file operations and network requests etc.

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:

  1. Only successful attacks can trigger alarms, resulting in lower false positive and higher detection rate;
  2. Detailed stack trace is logged, which makes the forensic analysis easier;
  3. Insusceptible to malformed protocol.

我的理解

在我阅读了OpenRASP的源码后,再次解读官方对其的介绍,我认为OpenRASP就是一个不同于WAF,它是通过JavaAgent,然后利用Instrumentation在class加载时,通过javassist的方式hook目标class的method,在其method插桩,以进行一系列的安全基准测试或运行时的安全检查,它相对于WAF来说,具有非常大的优势,但这是相对的,它的优点的存在恰恰也造成了一定的缺点。

优点:

  1. WAF依靠特征检测攻击,但会造成一定的误报率,而OpenRASP不一样,必须是成功的攻击才会触发报警
  2. OpenRASP插桩到代码层面,可以记录详细的栈堆跟踪信息

缺点:

  1. 因为侵入到代码层面,导致必然会造成一定的性能损耗,并且一个不合格的rasp更容易影响到业务代码

重点阅读部分

从github clone下来项目之后,我们可以看到具体的目录大致构成是这样的:

LICENSE           build-cloud.sh    build-php7.sh     docker            plugins           rasp-vue          travis
README.md         build-java.sh     cloud             openrasp-v8       rasp-2019-12-12   readme-zh_CN.md
agent             build-php5.sh     contributors.md   package-lock.json rasp-install      siem

而我主要关心的是:

  1. agent/java/boot(OpenRASP JavaAgent源码)
  2. agent/java/engine(OpenRASP主要唯一的module)
  3. rasp-install/java(OpenRASP安装源码)
  4. plugins(js插件,OpenRASP检查攻击的主要源码,因为js的热部署性而采用)

0x02 OpenRASP的安装原理

java源码实现,位置rasp-install/java

宏观上的审视

查看源码工程,其具有两个package

-install
++linux
++windows
-uninstall
++linux
++windows

其实就是对OpenRASP的安装和卸载做的封装,interfece Installer和interface Uninstaller分别是它们的抽象定义

public interface Installer {
    void install() throws RaspError, IOException;
}
public interface Uninstaller {
    void uninstall() throws RaspError, IOException;
}

包中都是对Installer、Uninstaller基于不同操作系统、web服务器的实现,并通过了工厂模式,根据参数、环境变量、目录信息特征等,选择对应的实现

public abstract class InstallerFactory
public abstract class UninstallerFactory

OpenRASP的安装程序主要入口位于:

package com.baidu.rasp

com.baidu.rasp.App#main

微观细节的跟踪

应用入口:

public static void main(String[] args) {
    try {
        operateServer(args);
    } catch (Exception e) {
        if (e instanceof RaspError || e instanceof UnrecognizedOptionException) {
            System.out.println(e.getMessage());
        } else {
            e.printStackTrace();
        }
        showNotice();
        System.exit(1);
    }
}

主要代码:

public static void operateServer(String[] args) throws RaspError, ParseException, IOException {
    showBanner();
    argsParser(args);
    checkArgs();
    if ("install".equals(install)) {
        File serverRoot = new File(baseDir);
        InstallerFactory factory = newInstallerFactory();
        Installer installer = factory.getInstaller(serverRoot, noDetect);
        if (installer != null) {
            installer.install();
        } else {
            throw new RaspError(E10007);
        }
    } else if ("uninstall".equals(install)) {
        File serverRoot = new File(baseDir);
        UninstallerFactory factory = newUninstallerFactory();
        Uninstaller uninstaller = factory.getUninstaller(serverRoot);
        if (uninstaller != null) {
            uninstaller.uninstall();
        } else {
            throw new RaspError(E10007);
        }
    }
}

代码跟进:

  1. showBanner():通过该方法输出了OpenRASP安装程序的一些banner信息
  2. argsParser(args):该方法主要是对程序启动参数的解析和校验,它通过commons-cli的功能,对启动参数进行一系列的解析和校验
install:指定该操作为安装
uninstall:指定该操作为卸载
appid:OpenRASP连接到RASP Cloud的认证appid
appsecret:OpenRASP连接到RASP Cloud的认证appsecret
heartbeat:OpenRASP连接到RASP Cloud的心跳检测时间间隔
raspid:OpenRASP的id
backendurl:RASP Cloud地址
keepconf:
h:帮助指令
pid:若使用OpenRASP的web程序非容器化,而是类似SpringBoot独立jar运行的时候,需要通过pid指定其Java server通过attach模式去使用
nodetect:指定为类似SpringBoot独立jar运行的安装
prepend:是否使用JavaAgent的模式比web容器更早启动
  1. checkArgs():对程序启动参数格式、范围等校验
appId、appSecret、raspId、url、heartbeatInterval
  1. 根据参数判断是安装还是卸载,若是安装,则通过安装工厂newInstallerFactory获取安装实例进行执行安装,若是卸载,则通过卸载工厂newUninstallerFactory获取卸载实例进行执行卸载,安装、卸载工厂的不同操作系统实现,是根据系统变量os.name进行判断
private static InstallerFactory newInstallerFactory() {
    if (System.getProperty("os.name").startsWith("Windows")) {
        return new WindowsInstallerFactory();
    } else {
        return new LinuxInstallerFactory();
    }
}

private static UninstallerFactory newUninstallerFactory() {
    if (System.getProperty("os.name").startsWith("Windows")) {
        return new WindowsUninstallerFactory();
    } else {
        return new LinuxUninstallerFactory();
    }
}

获取安装实例:

public Installer getInstaller(File serverRoot, boolean noDetect) throws RaspError {
    if (!serverRoot.exists()) {
        throw new RaspError(E10002 + serverRoot.getPath());
    }

    if (noDetect) {
        return new GenericInstaller(GENERIC, serverRoot.getAbsolutePath());
    }
    String serverName = detectServerName(serverRoot.getAbsolutePath());
    if (serverName == null) {
        App.listServerSupport(serverRoot.getPath());
    }
    System.out.println("Detected JDK version: " + System.getProperty("java.version"));
    System.out.println("Detected application server type: " + serverName);
    return getInstaller(serverName, serverRoot.getAbsolutePath());
}

可以看到,对于使用了启动参数nodetect的安装,选择的是GenericInstaller通用安装实例,否则会通过detectServerName(String serverRoot)方法进行web服务器的特征检测

public static String detectServerName(String serverRoot) throws RaspError {
    if (new File(serverRoot, "bin/catalina.sh").exists()
            || new File(serverRoot, "bin/catalina.bat").exists()
            || new File(serverRoot, "conf/catalina.properties").exists()
            || new File(serverRoot, "conf/catalina.policy").exists()) {
        return TOMCAT;
    }
    if (new File(serverRoot, "bin/probe.sh").exists()
            || new File(serverRoot, "bin/probe.bat").exists()
            || new File(serverRoot, "bin/twiddle.sh").exists()
            || new File(serverRoot, "bin/twiddle.bat").exists()) {
        return JBOSS;
    }
    if (new File(serverRoot, "bin/httpd.sh").exists()
            || new File(serverRoot, "bin/resin.sh").exists()) {
        return RESIN;
    }
    if (new File(serverRoot, "bin/startWebLogic.sh").exists()
            || new File(serverRoot, "bin/startWebLogic.bat").exists()) {
        return WEBLOGIC;
    }
    if (new File(serverRoot, "bin/standalone.sh").exists()
            || new File(serverRoot, "bin/standalone.bat").exists()) {
        try {
            return isWildfly(serverRoot) ? WILDFLY : JBOSSEAP;
        } catch (Exception e) {
            return null;
        }
    }
    return null;
}

特征检测的方式,无一不是通过检测特定目录是否存在shell脚本实现

执行安装:

安装核心方法:install()

  1. GenericInstaller通用安装:

先是根据当前jar的目录获取到其子目录rasp,若不存在则新建

String jarPath = getLocalJarPath();
File srcDir = new File(new File(jarPath).getParent() + File.separator + "rasp");
if (!(srcDir.exists() && srcDir.isDirectory())) {
    srcDir.mkdirs();
}

接着通过设定的安装目录,检测openrasp.yml是否存在,用以判断是否第一次安装,然后拷贝rasp文件夹至目标安装目录

File installDir = new File(getInstallPath(serverRoot));

File configFile = new File(installDir.getCanonicalPath() + File.separator + "conf" + File.separator + "openrasp.yml");
if (!configFile.exists()) {
    firstInstall = true;
}
if (!srcDir.getCanonicalPath().equals(installDir.getCanonicalPath())) {
    // 拷贝rasp文件夹
    System.out.println("Duplicating \"rasp\" directory\n- " + installDir.getCanonicalPath());
    FileUtils.copyDirectory(srcDir, installDir);
}

删除官方js插件

//安装rasp开启云控,删除官方插件
if (App.url != null && App.appId != null && App.appSecret != null) {
    File plugin = new File(installDir.getCanonicalPath() + File.separator + "plugins" + File.separator + "official.js");
    if (plugin.exists()) {
        plugin.delete();
    }
}

若不是第一次安装,则会把目标安装目录下原有配置文件修改名称为openrasp.yml.bak,然后拷贝当前jar目录下的openrasp.yml到目标安装目录的conf子目录

// 生成配置文件
if (!generateConfig(installDir.getPath(), firstInstall)) {
    System.exit(1);
}
private boolean generateConfig(String dir, boolean firstInstall) {
    try {
        String sep = File.separator;
        File target = new File(dir + sep + "conf" + sep + "openrasp.yml");

        System.out.println("Generating \"openrasp.yml\"\n- " + target.getAbsolutePath());
        if (target.exists() && App.keepConfig) {
            System.out.println("- Already exists and reserved openrasp.yml, continuing ..");
            return true;
        }
        if (target.exists() && !firstInstall) {
            File reserve = new File(dir + sep + "conf" + sep + "openrasp.yml.bak");
            if (!reserve.exists()) {
                reserve.createNewFile();
            }
            FileOutputStream outputStream = new FileOutputStream(reserve);
            FileInputStream inputStream = new FileInputStream(target);
            IOUtils.copy(inputStream, outputStream);
            outputStream.close();
            inputStream.close();
            System.out.println("- Backed up openrasp.yml to openrasp.yml.bak");
        } else {
            System.out.println("- Create " + target.getAbsolutePath());
            target.getParentFile().mkdir();
            target.createNewFile();
        }
        FileOutputStream outputStream = new FileOutputStream(target);
        InputStream is = this.getClass().getResourceAsStream("/openrasp.yml");
        IOUtils.copy(is, outputStream);
        is.close();
        outputStream.close();

        // 配置云控
        setCloudConf();
        // 配置其它选项
        Map<String, Object> ymlData = new HashMap<String, Object>();
        if (App.raspId != null) {
            ymlData.put("rasp.id", App.raspId);
        }
        if (!ymlData.isEmpty()) {
            setRaspConf(ymlData, "# <rasp id>");
        }
    } catch (IOException e) {
        e.printStackTrace();
        return false;
    }
    return true;

}

其中通过setCloudConf()方法,把云控所需的程序启动参数,写到配置文件openrasp.yml中

appid:OpenRASP连接到RASP Cloud的认证appid
appsecret:OpenRASP连接到RASP Cloud的认证appsecret
backendurl:RASP Cloud地址
heartbeat:OpenRASP连接到RASP Cloud的心跳检测时间间隔

写入的格式(yml):

cloud:
    enable: true
    backend_url: backendurl
    app_id: appid
    app_secret: appsecret
    heartbeat_interval: heartbeat
  1. TomcatInstaller

相对于通用安装的主要流程,它们并没有什么区别,区别仅仅在于配置文件写完后,TomcatInstaller会tomcat的安装目录下的bin/catalina.sh脚本进行修改

位于:com.baidu.rasp.install.BaseStandardInstaller#generateStartScript

com.baidu.rasp.install.linux.TomcatInstaller#getScript -> foundScriptPath(String installDir):查找bin/catalina.sh脚本路径

com.baidu.rasp.install.BaseStandardInstaller#modifyStartScript(String content):入参content即为脚本内容

在修改脚本时,会找到对应的位置写入或删除原有OpenRASP内容,写入新的脚本,然后根据程序启动参数prepend选择插入不同的rasp启动方式:

比web容器bootstrap更先启动:

private static String PREPEND_JAVA_AGENT_CONFIG = "\tJAVA_OPTS=\"${JAVA_OPTS} -javaagent:${CATALINA_HOME}/rasp/rasp.jar\"\n";

较web容器bootstrap更后启动:

private static String JAVA_AGENT_CONFIG = "\tJAVA_OPTS=\"-javaagent:${CATALINA_HOME}/rasp/rasp.jar ${JAVA_OPTS}\"\n";

总结下来:

/**
 * 操作判断:
 * 1.根据第一个参数判断是安装还是卸载
 *
 * 安装类型判断:
 * 1.判断操作系统
 * 2.判断服务器类型
 * 3.创建相应的安装器
 *
 * 普通安装:
 * 1.读启动参数
 * 2.把rasp目录copy到target目录
 * 3.若openrasp.yml存在则备份成openrasp.yml.bak,然后新写入openrasp.yml文件
 * 4.写入部分启动参数到openrasp.yml文件,启动云控
 * 
 * 非普通安装:
 * 1. 修改启动shell脚本,插入agent启动信息
 *
 */

0x03 OpenRASP的启动工作

java源码实现,位置:agent/java/boot

入口代码:

/**
 * 启动时加载的agent入口方法
 *
 * 1. javaagent在JVMTI特定状态时调用premain
 * 2. 把自身jar包放到java根目录,让启动类加载器能加载到(后续在这个javaagent会对启动类加载的class进行插桩,插桩代码点会调用rasp代码,因为启动类加载器加载的类是没办法去调用得到启动类加载器加载不到的类)
 * 3. 在当前jar包所在目录找到rasp-engine.jar
 * 4. 使用rasp-engine.jar初始化模块容器
 * 5. 找到manifest文件制定的Rasp-Module-Class实例化成module,然后调用该module.start()
 *
 * @param agentArg 启动参数
 * @param inst     {@link Instrumentation}
 */
public static void premain(String agentArg, Instrumentation inst) {
    init(START_MODE_NORMAL, START_ACTION_INSTALL, inst);
}

/**
 * attach 机制加载 agent
 *
 * @param agentArg 启动参数
 * @param inst     {@link Instrumentation}
 */
public static void agentmain(String agentArg, Instrumentation inst) {
    init(Module.START_MODE_ATTACH, agentArg, inst);
}

具有两种方式的启动,一种是JVMTI调用premain方式,一种是attach机制加载agent的方式。

String START_MODE_ATTACH = "attach";
String START_MODE_NORMAL = "normal";

核心:

/**
 * attack 机制加载 agent
 *
 * @param mode 启动模式
 * @param inst {@link Instrumentation}
 */
public static synchronized void init(String mode, String action, Instrumentation inst) {
    try {
        JarFileHelper.addJarToBootstrap(inst);
        readVersion();
        ModuleLoader.load(mode, action, inst);
    } catch (Throwable e) {
        System.err.println("[OpenRASP] Failed to initialize, will continue without security protection.");
        e.printStackTrace();
    }
}
  1. JarFileHelper.addJarToBootstrap(inst):添加当前执行的jar文件至jdk的跟路径下,启动类加载器能优先加载,后续在这个javaagent会对启动类加载的class进行插桩,插桩代码点会调用rasp代码,因为启动类加载器加载的类是没办法去调用得到启动类加载器加载不到的类,因为每个类加载器都有自己的类加载目录
  2. readVersion():读取MANIFEST.MF相关信息
  3. ModuleLoader.load(mode, action, inst):加载rasp-engine.jar中的module实现(目前为止,仅有这一个module实现)
/**
 * 加载所有 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 {
                instance = 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);
    }
}
/**
 * 构造所有模块
 *
 * @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);
}

public static final String ENGINE_JAR = "rasp-engine.jar";

可以看到,这里的实现是创建容器并启动,容器的实现是rasp-engine.jar,如果细看ModuleContainer的源码,可以发现,在其构造方法中,读取了rasp-engine.jar中MANIFEST.MF文件的Rasp-Module-Name、Rasp-Module-Class信息,此信息用于指定rasp-engine.jar中module容器的实现类,然后agent中的module加载器根据此信息加载module容器并调用start方法启动


0x04 OpenRASP engine的启动

java源码实现,位置:agent/java/engine

入口代码(com.baidu.openrasp.EngineBoot):

@Override
    public void start(String mode, Instrumentation inst) throws Exception {
        System.out.println("\n\n" +
                "   ____                   ____  ___   _____ ____ \n" +
                "  / __ \\____  ___  ____  / __ \\/   | / ___// __ \\\n" +
                " / / / / __ \\/ _ \\/ __ \\/ /_/ / /| | \\__ \\/ /_/ /\n" +
                "/ /_/ / /_/ /  __/ / / / _, _/ ___ |___/ / ____/ \n" +
                "\\____/ .___/\\___/_/ /_/_/ |_/_/  |_/____/_/      \n" +
                "    /_/                                          \n\n");
        try {
            V8.Load();
        } catch (Exception e) {
            System.out.println("[OpenRASP] Failed to load V8 library, please refer to https://rasp.baidu.com/doc/install/software.html#faq-v8-load for possible solutions.");
            e.printStackTrace();
            return;
        }
        if (!loadConfig()) {
            return;
        }
        //缓存rasp的build信息
        Agent.readVersion();
        BuildRASPModel.initRaspInfo(Agent.projectVersion, Agent.buildTime, Agent.gitCommit);
        // 初始化插件系统
        if (!JS.Initialize()) {
            return;
        }
        CheckerManager.init();
        initTransformer(inst);
        String message = "[OpenRASP] Engine Initialized [" + Agent.projectVersion + " (build: GitCommit="
                + Agent.gitCommit + " date=" + Agent.buildTime + ")]";
        System.out.println(message);
        Logger.getLogger(EngineBoot.class.getName()).info(message);
    }

可以看到,一共就做了以下这些工作:

  1. 输出banner信息
  2. V8引擎的加载,用于解释执行JavaScript
  3. loadConfig():初始化配置
private boolean loadConfig() throws Exception {
    LogConfig.ConfigFileAppender();
    //单机模式下动态添加获取删除syslog
    if (!CloudUtils.checkCloudControlEnter()) {
        LogConfig.syslogManager();
    } else {
        System.out.println("[OpenRASP] RASP ID: " + CloudCacheModel.getInstance().getRaspId());
    }
    return true;
}

LogConfig.ConfigFileAppender():初始化log4j
CloudUtils.checkCloudControlEnter():检查云控配置信息
LogConfig.syslogManager():读取配置信息,初始化syslog服务连接

  1. JS.Initialize():初始化插件系统

为V8配置java的logger以及栈堆信息Getter(用于在js中获取当前栈堆信息)

public synchronized static boolean Initialize() {
    try {
        V8.Load();
        if (!V8.Initialize()) {
            throw new Exception("[OpenRASP] Failed to initialize V8 worker threads");
        }
        V8.SetLogger(new com.baidu.openrasp.v8.Logger() {
            @Override
            public void log(String msg) {
                PLUGIN_LOGGER.info(msg);
            }
        });
        V8.SetStackGetter(new com.baidu.openrasp.v8.StackGetter() {
            @Override
            public byte[] get() {
                try {
                    ByteArrayOutputStream stack = new ByteArrayOutputStream();
                    JsonStream.serialize(StackTrace.getParamStackTraceArray(), stack);
                    stack.write(0);
                    return stack.getByteArray();
                } catch (Exception e) {
                    return null;
                }
            }
        });
        Context.setKeys();
        if (!CloudUtils.checkCloudControlEnter()) {
            UpdatePlugin();
            InitFileWatcher();
        }
        return true;
    } catch (Exception e) {
        e.printStackTrace();
        LOGGER.error(e);
        return false;
    }
}

UpdatePlugin():读取plugins目录下的js文件,过滤掉大于10MB的js文件,然后全部读入,最后加载到V8引擎中

public synchronized static boolean UpdatePlugin() {
    boolean oldValue = HookHandler.enableHook.getAndSet(false);
    List<String[]> scripts = new ArrayList<String[]>();
    File pluginDir = new File(Config.getConfig().getScriptDirectory());
    LOGGER.debug("checker directory: " + pluginDir.getAbsolutePath());
    if (!pluginDir.isDirectory()) {
        pluginDir.mkdir();
    }
    FileFilter filter = FileFilterUtils.and(FileFilterUtils.sizeFileFilter(10 * 1024 * 1024, false),
            FileFilterUtils.suffixFileFilter(".js"));
    //过滤掉大于10MB的js文件
    File[] pluginFiles = pluginDir.listFiles(filter);
    if (pluginFiles != null) {
        for (File file : pluginFiles) {
            try {
                String name = file.getName();
                String source = FileUtils.readFileToString(file, "UTF-8");
                scripts.add(new String[]{name, source});
            } catch (Exception e) {
                LogTool.error(ErrorType.PLUGIN_ERROR, e.getMessage(), e);
            }
        }
    }
    HookHandler.enableHook.set(oldValue);
    return UpdatePlugin(scripts);
}
public synchronized static boolean UpdatePlugin(List<String[]> scripts) {
    boolean rst = V8.CreateSnapshot("{}", scripts.toArray(), BuildRASPModel.getRaspVersion());
    if (rst) {
        try {
            String jsonString = V8.ExecuteScript("JSON.stringify(RASP.algorithmConfig || {})", "get-algorithm-config.js");
            Config.getConfig().setConfig("algorithm.config", jsonString, true);
        } catch (Exception e) {
            LogTool.error(ErrorType.PLUGIN_ERROR, e.getMessage(), e);
        }
        Config.commonLRUCache.clear();
    }
    return rst;
}

这里有一个commonLRUCache,主要是用于在hook点去执行js check的时候,进行一个并发幂等(应该是这样。。。)。

InitFileWatcher():初始化一个js plugin监视器,在js文件有所变动的时候,重新去加载所有插件,实现热更新的特性

public synchronized static void InitFileWatcher() throws Exception {
    boolean oldValue = HookHandler.enableHook.getAndSet(false);
    if (watchId != null) {
        FileScanMonitor.removeMonitor(watchId);
        watchId = null;
    }
    watchId = FileScanMonitor.addMonitor(Config.getConfig().getScriptDirectory(), new FileScanListener() {
        @Override
        public void onFileCreate(File file) {
            if (file.getName().endsWith(".js")) {
                UpdatePlugin();
            }
        }

        @Override
        public void onFileChange(File file) {
            if (file.getName().endsWith(".js")) {
                UpdatePlugin();
            }
        }

        @Override
        public void onFileDelete(File file) {
            if (file.getName().endsWith(".js")) {
                UpdatePlugin();
            }
        }
    });
    HookHandler.enableHook.set(oldValue);
}
  1. CheckerManager.init():初始化所有的checker,从枚举类com.baidu.openrasp.plugin.checker.CheckParameter.Type中读取所有的checker,包含三种类型的checker,一是js插件检测,意味着这个checker会调用js plugin进行攻击检测,二是java本地检测,意味着是调用本地java代码进行攻击检测,三是安全基线检测,是用于检测一些高风险类的安全性基线检测,检测其配置是否有安全隐患。
// js插件检测
SQL("sql", new V8Checker(), 1),
COMMAND("command", new V8Checker(), 1 << 1),
DIRECTORY("directory", new V8Checker(), 1 << 2),
REQUEST("request", new V8Checker(), 1 << 3),
DUBBOREQUEST("dubboRequest", new V8Checker(), 1 << 4),
READFILE("readFile", new V8Checker(), 1 << 5),
WRITEFILE("writeFile", new V8Checker(), 1 << 6),
FILEUPLOAD("fileUpload", new V8Checker(), 1 << 7),
RENAME("rename", new V8Checker(), 1 << 8),
XXE("xxe", new V8Checker(), 1 << 9),
OGNL("ognl", new V8Checker(), 1 << 10),
DESERIALIZATION("deserialization", new V8Checker(), 1 << 11),
WEBDAV("webdav", new V8Checker(), 1 << 12),
INCLUDE("include", new V8Checker(), 1 << 13),
SSRF("ssrf", new V8Checker(), 1 << 14),
SQL_EXCEPTION("sql_exception", new V8Checker(), 1 << 15),
REQUESTEND("requestEnd", new V8Checker(), 1 << 17),
LOADLIBRARY("loadLibrary", new V8Checker(), 1 << 18),

// java本地检测
XSS_USERINPUT("xss_userinput", new XssChecker(), 1 << 16),
SQL_SLOW_QUERY("sqlSlowQuery", new SqlResultChecker(false), 0),

// 安全基线检测
POLICY_SQL_CONNECTION("sqlConnection", new SqlConnectionChecker(false), 0),
POLICY_SERVER_TOMCAT("tomcatServer", new TomcatSecurityChecker(false), 0),
POLICY_SERVER_JBOSS("jbossServer", new JBossSecurityChecker(false), 0),
POLICY_SERVER_JBOSSEAP("jbossEAPServer", new JBossEAPSecurityChecker(false), 0),
POLICY_SERVER_JETTY("jettyServer", new JettySecurityChecker(false), 0),
POLICY_SERVER_RESIN("resinServer", new ResinSecurityChecker(false), 0),
POLICY_SERVER_WEBSPHERE("websphereServer", new WebsphereSecurityChecker(false), 0),
POLICY_SERVER_WEBLOGIC("weblogicServer", new WeblogicSecurityChecker(false), 0),
POLICY_SERVER_WILDFLY("wildflyServer", new WildflySecurityChecker(false), 0),
POLICY_SERVER_TONGWEB("tongwebServer", new TongwebSecurityChecker(false), 0);
  1. initTransformer(inst):核心代码,通过加载class,在加载前使用javassist对其进行hook插桩,以实现rasp的攻击检测功能
/**
 * 初始化类字节码的转换器
 *
 * @param inst 用于管理字节码转换器
 */
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中,以提供在后续类加载通过com.baidu.openrasp.transformer.CustomClassTransformer#transform的时候,对其进行匹配,判断是否需要hook

public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                        ProtectionDomain domain, byte[] classfileBuffer) throws IllegalClassFormatException {
    if (loader != null && jspClassLoaderNames.contains(loader.getClass().getName())) {
        jspClassLoaderCache.put(className.replace("/", "."), new WeakReference<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;
}

看细节,可以发现,先根据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类

public boolean isClassMatched(String className) {
    return "org/apache/http/client/methods/HttpRequestBase".equals(className);
}

既然要检测SSRF,那么就选择在setURI时,就对其URI进行检测,hookMethod方法其实就是通过javassist生成了一段调用com.baidu.openrasp.hook.ssrf.HttpClientHook#checkHttpUri方法的代码,并插入到HttpRequestBase.setURI方法中,以实现检测SSRF

protected void hookMethod(CtClass ctClass) throws IOException, CannotCompileException, NotFoundException {
    String src = getInvokeStaticSrc(HttpClientHook.class, "checkHttpUri",
            "$1", URI.class);
    insertBefore(ctClass, "setURI", "(Ljava/net/URI;)V", src);
}

checkHttpUri方法通过取出相关信息,host、port、url等,然后通过一系列方法,对检测ssrf的js插件进行调用以检测攻击,当然,过程中会加入一些机制,对其可用性的增强

public static void checkHttpUri(URI uri) {
    String url = null;
    String hostName = null;
    String port = "";
    try {
        if (uri != null) {
            url = uri.toString();
            hostName = uri.toURL().getHost();
            int temp = uri.toURL().getPort();
            if (temp > 0) {
                port = temp + "";
            }
        }
    } catch (Throwable t) {
        LogTool.traceHookWarn("parse url " + url + " failed: " + t.getMessage(), t);
    }
    if (hostName != null) {
        checkHttpUrl(url, hostName, port, "httpclient");
    }
}

->com.baidu.openrasp.hook.ssrf.AbstractSSRFHook#checkHttpUrl

protected static void checkHttpUrl(String url, String hostName, String port, String function) {
    HashMap<String, Object> params = new HashMap<String, Object>();
    params.put("url", url);
    params.put("hostname", hostName);
    params.put("function", function);
    params.put("port", port);
    LinkedList<String> ip = new LinkedList<String>();
    try {
        InetAddress[] addresses = InetAddress.getAllByName(hostName);
        for (InetAddress address : addresses) {
            if (address != null && address instanceof Inet4Address) {
                ip.add(address.getHostAddress());
            }
        }
    } catch (Throwable t) {
        // ignore
    }
    Collections.sort(ip);
    params.put("ip", ip);
    HookHandler.doCheck(CheckParameter.Type.SSRF, params);
}

流程汇总:

1.com.baidu.openrasp.hook.ssrf.HttpClientHook#checkHttpUri

2.com.baidu.openrasp.hook.ssrf.AbstractSSRFHook#checkHttpUrl

3.com.baidu.openrasp.HookHandler#doCheck

4.com.baidu.openrasp.HookHandler#doCheckWithoutRequest
在这里,做了一些云控注册成功判断和白名单的处理

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

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

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

8.com.baidu.openrasp.plugin.checker.v8.V8Checker#checkParam

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

10.com.baidu.openrasp.v8.V8#Check(java.lang.String, byte[], int, com.baidu.openrasp.v8.Context, boolean, int)

总的来说,大概整个OpenRASP的核心就是如此了,还有一些关于cloud的云控实现,这里的篇幅暂且不对其就行研究

点击收藏 | 2 关注 | 3
登录 后跟帖