OpenRasp分析

OpenRasp分析

[TOC]

写在前面

​ 花了点时间学习了下openrasp的核心代码,这里做下简单的分析

​ 相关项目地址:

https://github.com/baidu-security/openrasp-v8

https://github.com/baidu/openrasp

​ 这里我以目前官网最新版的1.3.7来做下分析,这里为了方便简单用springboot写个简单的控制器来进行调试分析即可,当然这里不会去看后端云控部分的代码,笔者只是想理清OpenRasp的逻辑

​ 另外说点p话,顺便在这个过程当中被迫了解了点c++语法真是太妙了

一些日志说明

OpenRasp的日志会通过文件的方式记录在对应文件夹下面,里面日志具体内容就不多解释了点开一眼就看得懂,了解下面几个关于日志目录介绍完全足够了

文件名文件内容
plugin/plugin-DATE.log检测插件的日志,e.g 插件异常、插件调试输出
rasp/rasp-DATE.lograsp agent 调试日志
alarm/alarm-DATE.log攻击报警日志,JSON 格式,一行一个
policy_alarm/policy_alarm-DATE.log安全基线检查报警日志,JSON 格式,一行一个

正文

初始化

首先既然是一个基于maven的项目,很多关键信息都肯定有定义的,类似premain-class以及Agent-class分别是启动时加载和启动后加载rasp,这里我们就以premain为例子,另一个差不多类似

首先是执行init初始化

初始化第一步JarFileHelper.addJarToBootstrap(inst);,可以看到这里其实就是把当前jar包也就是rasp.jar加载至Bootstrap类加载器,这里你可能想问为什么是最顶层的这个

为什么要将rasp.jar加载至Bootstrap类加载器

通过JVM的api,把其路径追加到了启动类加载器的classpath中,这样,启动类加载器,收到类加载委派任务时,就能通过该classpath加载到rasp.jar的所有类了,根据双亲委派,意味着任何一个类加载器中的任何一个类,都能通过显式或者隐式加载,加载到rasp.jar中的类,反而网上说的啥无法hook到通过启动类加载器加载的类纯纯扯淡

配置初始化

接下来的readVersion()方法,其实就是读取一些rasp自身的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void readVersion() throws IOException {
Class clazz = Agent.class;
String className = clazz.getSimpleName() + ".class";
String classPath = clazz.getResource(className).toString();
String manifestPath = classPath.substring(0, classPath.lastIndexOf("!") + 1) + "/META-INF/MANIFEST.MF";
Manifest manifest = new Manifest((new URL(manifestPath)).openStream());
Attributes attr = manifest.getMainAttributes();
projectVersion = attr.getValue("Project-Version");
buildTime = attr.getValue("Build-Time");
gitCommit = attr.getValue("Git-Commit");
projectVersion = projectVersion == null ? "UNKNOWN" : projectVersion;
buildTime = buildTime == null ? "UNKNOWN" : buildTime;
gitCommit = gitCommit == null ? "UNKNOWN" : gitCommit;
}

没啥好看的,看看MANIFEST.MF就好

接下来执行ModuleLoader.load(mode, action, inst);

ModuleLoader类初始化

首先ModueLoader有个静态块,来看看代码做了两件事,一个是获取rasp.jar的绝对路径,另一个是获取拓展类加载器赋值给moduleClassLoader,至于为什么需要获取拓展类加载器,这里引入三梦师傅的话,很好理解没啥难度

1
其实,很多时候,比如tomcat,它在运行中,大部分类都是由实现的应用类加载器进行加载的,那么,假如Engine是通过某个应用类加载器进行加载的,而我们的hook代码,在tomcat中应用类加载器加载的某个类,插入了某段代码,这段代码直接(com.xxx.A.a();)调用了Engine的某个类的方法,那么,按照双亲委派机制,以及隐式加载的规范,将会抛出ClassNoFoundError的错误

再简单看看代码,待会儿说说这个moduleClassLoader的作用,在很后面这里先了解了解

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
static {
Class clazz;
try {
clazz = Class.forName("java.nio.file.FileSystems");
clazz.getMethod("getDefault").invoke((Object)null);
} catch (Throwable var4) {
}

clazz = ModuleLoader.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 var3) {
baseDirectory = (new File(path)).getParent();
}

ClassLoader systemClassLoader;
for(systemClassLoader = ClassLoader.getSystemClassLoader(); systemClassLoader.getParent() != null && !systemClassLoader.getClass().getName().equals("sun.misc.Launcher$ExtClassLoader"); systemClassLoader = systemClassLoader.getParent()) {
}

moduleClassLoader = systemClassLoader;
}

接下来进入构造函数,首先实例化赋值engineContainer = new ModuleContainer("rasp-engine.jar");

引擎启动

JS初始化

com.baidu.openrasp.EngineBoot#start中首先通过Loader.load();引入动态链接库,具体引入的是干嘛的之后就知道了,之后我们暂时先忽略配置相关的东西进入主要的

首先是JS的初始化

在这个过程,首先是设置日志输出相关

紧接着是设置StackGetter,这其实是一个回掉函数的触发

这一点可以从v8的文档得以验证,后面还会提到这里只是简单提提

紧接着是下面两行

1
2
UpdatePlugin();
InitFileWatcher();

一个UpdatePlugin();,首先遍历plugins目录下的js文件,并添加到scripts变量当中

紧接着执行UpdatePlugin(List<String[]> scripts),首先是CreateSnapshot从名字可以看出是创建快照,我们还是来具体看看干了些啥

简单对文件做了注释,因为流程确实没啥好说的

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
/*
* Class: com_baidu_openrasp_v8_V8
* Method: CreateSnapshot
* Signature: (Ljava/lang/String;[Ljava/lang/Object;Ljava/lang/String;)Z
*/
ALIGN_FUNCTION JNIEXPORT jboolean JNICALL Java_com_baidu_openrasp_v8_V8_CreateSnapshot(JNIEnv* env,
jclass cls,
jstring jconfig,
jobjectArray jplugins,
jstring jversion) {
//global.checkPoints
auto config = Jstring2String(env, jconfig);
//RASP版本信息
auto version = Jstring2String(env, jversion);
std::vector<PluginFile> plugin_list;
const size_t plugin_len = env->GetArrayLength(jplugins);
//遍历plugin,并将插件文件名与插件内容保存到plugin_list里面
for (int i = 0; i < plugin_len; i++) {
jobjectArray plugin = (jobjectArray)env->GetObjectArrayElement(jplugins, i);
if (plugin == nullptr) {
continue;
}
jstring jname = (jstring)env->GetObjectArrayElement(plugin, 0);
jstring jsource = (jstring)env->GetObjectArrayElement(plugin, 1);
if (jname == nullptr || jsource == nullptr) {
continue;
}
auto name = Jstring2String(env, jname);
auto source = Jstring2String(env, jsource);
plugin_list.emplace_back(name, source);
}
auto duration = std::chrono::system_clock::now().time_since_epoch();
auto millis = std::chrono::duration_cast<std::chrono::milliseconds>(duration).count();
//好了注释到上面这一坨就结束了
Snapshot* blob = new Snapshot(config, plugin_list, version, millis, env);
if (!blob->IsOk()) {
delete blob;
return false;
}
std::lock_guard<std::mutex> lock(snapshot_mtx);
delete snapshot;
snapshot = blob;
return true;
}

接下来是一个非常有意思的函数Snapshot,它的作用是创建一个构造好的js运行环境的快照,它继承了StartupData类,下面是我简单做的一些笔记

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
70
71
72
73
74
Snapshot::Snapshot(const std::string& config,
const std::vector<PluginFile>& plugin_list,
const std::string& version,
uint64_t timestamp,
void* custom_data)
: v8::StartupData({nullptr, 0}), timestamp(timestamp) {
IsolateData data;
data.custom_data = custom_data;
v8::SnapshotCreator creator(external_references);
//获取一个隔离的环境
Isolate* isolate = reinterpret_cast<Isolate*>(creator.GetIsolate());
//void * 则不同,任何类型的指针都可以直接赋值给它,无需进行强制类型转换
//上面这个custom_data从传递来看,传递过来的其实是JNIENV的指向
isolate->SetData(&data);
{
v8::Isolate::Scope isolate_scope(isolate);
v8::HandleScope handle_scope(isolate);
v8::Local<v8::Context> context = v8::Context::New(isolate);
v8::Context::Scope context_scope(context);
v8::TryCatch try_catch(isolate);
v8::Local<v8::Object> global = context->Global();
//上下文当中设置version/global/window等信息
global->Set(context, NewV8Key(isolate, "version"), NewV8String(isolate, version)).IsJust();
global->Set(context, NewV8Key(isolate, "global"), global).IsJust();
global->Set(context, NewV8Key(isolate, "window"), global).IsJust();
v8::Local<v8::Object> v8_stdout = v8::Object::New(isolate);
//下面都是绑定函数,比如将write绑定到函数external_references[0]的指向(这变量是啥后面会说到),其他类似,另外还有绑定标准输出与标准错误
v8_stdout
->Set(
context, NewV8Key(isolate, "write"),
v8::Function::New(context, reinterpret_cast<v8::FunctionCallback>(external_references[0])).ToLocalChecked())
.IsJust();
global->Set(context, NewV8Key(isolate, "stdout"), v8_stdout).IsJust();
global->Set(context, NewV8Key(isolate, "stderr"), v8_stdout).IsJust();
global
->Set(
context, NewV8Key(isolate, "flex_tokenize"),
v8::Function::New(context, reinterpret_cast<v8::FunctionCallback>(external_references[1])).ToLocalChecked())
.IsJust();
global
->Set(
context, NewV8Key(isolate, "request"),
v8::Function::New(context, reinterpret_cast<v8::FunctionCallback>(external_references[2])).ToLocalChecked())
.IsJust();
global
->Set(
context, NewV8Key(isolate, "request_async"),
v8::Function::New(context, reinterpret_cast<v8::FunctionCallback>(external_references[3])).ToLocalChecked())
.IsJust();
//暂时不知道干嘛的,也没有这个js文件
if (isolate->ExecScript({reinterpret_cast<const char*>(gen_builtins), gen_builtins_len}, "builtins.js").IsEmpty()) {
Exception e(isolate, try_catch);
Platform::logger(e);
// no need to continue
return;
}
//初始化配置
if (isolate->ExecScript(config, "config.js").IsEmpty()) {
Exception e(isolate, try_catch);
Platform::logger(e);
}
//执行我们的插件js脚本做参数初始化以及各种检测函数的注册
for (auto& plugin_src : plugin_list) {
if (isolate->ExecScript("(function(){\n" + plugin_src.source + "\n})()", plugin_src.filename, -1).IsEmpty()) {
Exception e(isolate, try_catch);
Platform::logger(e);
}
}
creator.SetDefaultContext(context);
}
v8::StartupData snapshot = creator.CreateBlob(v8::SnapshotCreator::FunctionCodeHandling::kClear);
this->data = snapshot.data;
this->raw_size = snapshot.raw_size;
}

另外上面提到的external_references里面的回掉函数在native-function.cc当中有定义,这里直接放过来很好理解就不做解释了,稍微占点篇幅了

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
70
#include "bundle.h"
#include "flex/flex.h"
#include "request.h"

namespace openrasp_v8 {

void log_callback(const v8::FunctionCallbackInfo<v8::Value>& info) {
Isolate* isolate = reinterpret_cast<Isolate*>(info.GetIsolate());
for (int i = 0; i < info.Length(); i++) {
v8::String::Utf8Value message(isolate, info[i]);
Platform::logger({*message, static_cast<size_t>(message.length())});
}
}

void flex_callback(const v8::FunctionCallbackInfo<v8::Value>& info) {
Isolate* isolate = reinterpret_cast<Isolate*>(info.GetIsolate());
auto context = isolate->GetCurrentContext();
if (info.Length() < 2 || !info[0]->IsString() || !info[1]->IsString()) {
return;
}
v8::String::Utf8Value str(isolate, info[0]);
v8::String::Utf8Value lexer_mode(isolate, info[1]);

char* input = *str;
int input_len = str.length();

flex_token_result token_result = flex_lexing(input, input_len, *lexer_mode);

size_t len = std::min(uint32_t(input_len), token_result.result_len);
auto arr = v8::Array::New(isolate, len);
for (int i = 0; i < len; i++) {
arr->Set(context, i, v8::Integer::New(isolate, token_result.result[i])).IsJust();
}
free(token_result.result);
info.GetReturnValue().Set(arr);
}

void request_callback(const v8::FunctionCallbackInfo<v8::Value>& info) {
auto isolate = info.GetIsolate();
v8::TryCatch try_catch(isolate);
auto context = isolate->GetCurrentContext();
v8::Local<v8::Promise::Resolver> resolver;
if (!v8::Promise::Resolver::New(context).ToLocal(&resolver)) {
try_catch.ReThrow();
return;
}
info.GetReturnValue().Set(resolver->GetPromise());
HTTPRequest req(isolate, info[0]);
HTTPResponse res = req.GetResponse();
auto object = res.ToObject(isolate);
if (res.error) {
resolver->Reject(context, object).IsJust();
} else {
resolver->Resolve(context, object).IsJust();
}
}

void request_async_callback(const v8::FunctionCallbackInfo<v8::Value>& info) {
auto isolate = info.GetIsolate();
AsyncRequest::GetInstance().Submit(std::make_shared<HTTPRequest>(isolate, info[0]));
}

intptr_t* Snapshot::external_references = new intptr_t[5]{
reinterpret_cast<intptr_t>(log_callback),
reinterpret_cast<intptr_t>(flex_callback),
reinterpret_cast<intptr_t>(request_callback),
reinterpret_cast<intptr_t>(request_async_callback),
0,
};
} // namespace openrasp_v8

理解了这一段以后接下来再次回到Java端

这里获得RASP.algorithmConfig并保存到ConfigItem.ALGORITHM_CONFIG

到这里插件更新部分就结束了

之后调用了InitFileWatcher,它的作用是创建以目录为单位的文件监听,如果文件进行增删改,就执行插件更新

Checker的初始化

接下来就是Checker的初始化

这里会遍历

遍历Type这个枚举类型将检测类型以及对应的检测函数添加到checkers这个EnumMap当中

CustomClassTransformer

继续回来接下来调用this.initTransformer(inst);,这里实例化CustomClassTransformer这个 Class 文件的转换器,

可以看到将自身作为类转换器进行添加

1
2
3
4
5
public CustomClassTransformer(Instrumentation inst) {
this.inst = inst;
inst.addTransformer(this, true);
this.addAnnotationHook();
}

并调用retransform,这里逻辑很简单就不多说,看不懂的可以自行学习JavaAgent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void retransform() {
new LinkedList();
Class[] loadedClasses = this.inst.getAllLoadedClasses();
Class[] arr$ = loadedClasses;
int len$ = loadedClasses.length;

for(int i$ = 0; i$ < len$; ++i$) {
Class clazz = arr$[i$];
if (this.isClassMatched(clazz.getName().replace(".", "/")) && this.inst.isModifiableClass(clazz) && !clazz.getName().startsWith("java.lang.invoke.LambdaForm")) {
try {
this.inst.retransformClasses(new Class[]{clazz});
} catch (Throwable var8) {
LogTool.error(ErrorType.HOOK_ERROR, "failed to retransform class " + clazz.getName() + ": " + var8.getMessage(), var8);
}
}
}

}

因此之后当类加载的时候,会进入我们自己的 Transformer 中,执行 transform函数进行拦截

Hook

因此接下来我们着重看com.baidu.openrasp.transformer.CustomClassTransformer#transform方法,它会遍历hooks,如果条件符合(isClassMatched返回true)则会在制定的类方法当中进行hook

而这些类来源于哪里呢?就是open.baidu.openrasp.hook文件夹下的类

这里呢我们就随便挑一个来进行解读,那就来一个com.baidu.openrasp.hook.system.ProcessBuilderHook命令执行的类的吧,可以看到isClassMatched的规则

1
2
3
4
5
6
7
8
9
public boolean isClassMatched(String className) {
if (ModuleLoader.isModularityJdk()) {
return "java/lang/ProcessImpl".equals(className);
} else if (!OSUtil.isLinux() && !OSUtil.isMacOS()) {
return OSUtil.isWindows() ? "java/lang/ProcessImpl".equals(className) : false;
} else {
return "java/lang/UNIXProcess".equals(className);
}
}

看看调用到底是如何调用的,我们回到com.baidu.openrasp.transformer.CustomClassTransformer#transform,可以看到最终返回的字节码是受hook.transformClass处理的,在这里还有个小细节是如果loadernull,则会调用setLoadedByBootstrapLoader设置其中属性为true,我们也知道什么情况下获取不到类加载器也就是由BootStrap启动器类加载器加载的一些类如FileRuntime等等,在设置为true以后在后面hook的时候生成代码有部分区别,之后会提到

我们可以看到com.baidu.openrasp.hook.AbstractClassHook#transformClass,它会调用具体实现类的hookMethod方法

这里也就是对应com.baidu.openrasp.hook.system.ProcessBuilderHook#hookMethod,可以看到这里的处理也是很全面的挺好

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protected void hookMethod(CtClass ctClass) throws IOException, CannotCompileException, NotFoundException {
String src;
if (ctClass.getName().contains("ProcessImpl")) {
if (OSUtil.isWindows()) {
src = this.getInvokeStaticSrc(ProcessBuilderHook.class, "checkCommand", "$1,$2", new Class[]{String[].class, String.class});
this.insertBefore(ctClass, "<init>", (String)null, src);
} else if (ModuleLoader.isModularityJdk()) {
src = this.getInvokeStaticSrc(ProcessBuilderHook.class, "checkCommand", "$1,$2,$4", new Class[]{byte[].class, byte[].class, byte[].class});
this.insertBefore(ctClass, "<init>", (String)null, src);
}
} else if (ctClass.getName().contains("UNIXProcess")) {
src = this.getInvokeStaticSrc(ProcessBuilderHook.class, "checkCommand", "$1,$2,$4", new Class[]{byte[].class, byte[].class, byte[].class});
this.insertBefore(ctClass, "<init>", (String)null, src);
}

}

在具体要hook的类方法前面加上checkCommand这个函数

回答上面遗留的ModuleClassloader的问题

在这里通过getInvokeStaticSrc这个方法生成具体插入的类,在这个方法当中可以看到,对于被BootStrap加载的类,它会通过com.baidu.openrasp.ModuleLoader.moduleClassLoader.loadClass去调用检查命令的checkCommand函数,这样就避免了由于双亲委派机制导致的ClassNotFoundException

由于重载思想差不多就随便挑一个看看

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
public static void checkCommand(byte[] command, byte[] args, byte[] envBlock) {
if ((Boolean)HookHandler.enableCmdHook.get()) {
LinkedList<String> commands = new LinkedList();
//执行的命令
if (command != null && command.length > 0) {
commands.add(new String(command, 0, command.length - 1));
}

//执行的命令的参数
int index;
if (args != null && args.length > 0) {
int position = 0;

for(index = 0; index < args.length; ++index) {
if (args[index] == 0) {
commands.add(new String(Arrays.copyOfRange(args, position, index)));
position = index + 1;
}
}
}
//来自envp参数,通常为空,通常是自己设置的环境变量
LinkedList<String> envList = new LinkedList();
if (envBlock != null) {
index = -1;

for(int i = 0; i < envBlock.length; ++i) {
if (envBlock[i] == 0) {
String envItem = new String(envBlock, index + 1, i - index - 1);
if (envItem.length() > 0) {
envList.add(envItem);
}

index = i;
}
}
}

checkCommand((List)commands, (List)envList);
}

}

之后在讲命令和环境变量放到commandsenvList当中并执行checkCommand((List)commands, (List)envList);,这里会把执行的命令、环境变量、以及当前调用栈存放到params这个变量当中

之后带着这些参数执行HookHandler.doCheckWithoutRequest,这里省略一些废话

之后在com.baidu.openrasp.HookHandler#doRealCheckWithoutRequest

会选择合适的checker去检查我们执行的东西

1
2
3
public static boolean check(Type type, CheckParameter parameter) {
return ((Checker)checkers.get(type)).check(parameter);
}

继续省略一堆废话,最终会调用到V8.check

我们来看看对应的c源码,这里忽略前面部分,后面这里有个比较骚的v8的函数SetLazyDataProperty

函数对应的Getter是GetStack,可以看到这个函数里面比较核心的操作就是通过JNIENV去调用Java的com.baidu.openrasp.v8.V8#GetStack函数很骚

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void GetStack(v8::Local<v8::Name> name, const v8::PropertyCallbackInfo<v8::Value>& info) {
auto isolate = reinterpret_cast<openrasp_v8::Isolate*>(info.GetIsolate());
auto env = GetJNIEnv(isolate);
jbyteArray jbuf = reinterpret_cast<jbyteArray>(env->CallStaticObjectMethod(v8_class.cls, v8_class.GetStack));
if (jbuf == nullptr) {
return info.GetReturnValue().Set(v8::Array::New(isolate));
}
auto maybe_string = v8::String::NewExternalOneByte(isolate, new ExternalOneByteStringResource(env, jbuf));
if (maybe_string.IsEmpty()) {
return info.GetReturnValue().Set(v8::Array::New(isolate));
}
auto maybe_value = v8::JSON::Parse(isolate->GetCurrentContext(), maybe_string.ToLocalChecked());
if (maybe_value.IsEmpty()) {
return info.GetReturnValue().Set(v8::Array::New(isolate));
}
auto value = maybe_value.ToLocalChecked();
info.GetReturnValue().Set(value);
}

继续往下看check函数,由于我们这里分析的是command,所以if部分暂时不用看,之后调用isolate->Check去执行检测(不截图了,简单来说就是找到对应的注册的检测函数去调用)

如何绕过

绕过的方式其实真的有很多,这里简单谈几个

基于正则的绕过

首先对于规则的检测既然是基于正则表达式,那么很显然如果在规则不够完善的情况之下,那也是可以造成一部分的绕过,比如我们可以看到在官方的插件当中,我们就拿这第一个查看文件的命令来说只是任意匹配1-5位,虽然不能通过多个空格之类的绕过

1
2
3
4
5
command_common: {
name: '算法3 - 识别常用渗透命令(探针)',
action: 'log',
pattern: 'cat.{1,5}/etc/passwd|nc.{1,30}-e.{1,100}/bin/(?:ba)?sh|bash\\s-.{0,4}i.{1,20}/dev/tcp/|subprocess.call\\(.{0,6}/bin/(?:ba)?sh|fsockopen\\(.{1,50}/bin/(?:ba)?sh|perl.{1,80}socket.{1,120}open.{1,80}exec\\(.{1,5}/bin/(?:ba)?sh'
},

我们的cat函数支持同时读多个文件cat /abc/def /etc/passwd,这样也是可以轻轻松松得以进行绕过

通过修改某些属性

通常如果存在反序列化漏洞,我们通常可以通过TemplatesImpl去加载任意字节码,在这里如果对于在RASP执行检测过程当中如果存在某些关键配置我们可以操控,那么就可以导致绕过,而OpenRasp里面就有,比如在执行检测前中间的调用流程有个com.baidu.openrasp.HookHandler#doCheckWithoutRequest,这里面提到了如果服务器的cpu使用率超过90%禁用全部hook点

又或者满足当云控注册成功之前,不进入任何hook点,反正这些我们不都是可以通过反射去设置的么,这里我就随便来一个,就以第一个为例子吧,我们可以通过反射获取这个已经实例化的实例,在这个基础上修改disableHooks这个属性即可

代码示例如下

1
2
3
4
5
6
7
8
9
try {
Class<?> clz = Thread.currentThread().getContextClassLoader().loadClass("com.baidu.openrasp.config.Config");
java.lang.reflect.Method getConfig = clz.getDeclaredMethod("getConfig");
java.lang.reflect.Field disableHooks = clz.getDeclaredField("disableHooks");
disableHooks.setAccessible(true);
Object ins = getConfig.invoke(null);

disableHooks.set(ins,true);
} catch (Exception e) {}

为了得到直观的效果我把插件当中的log改为block来演示下

1
2
3
4
5
// 命令注入 - 常见命令
command_common: {
name: '算法3 - 识别常用渗透命令(探针)',
action: 'block',
pattern: 'cat.{1,5}/etc/passwd|nc.{1,30}-e.{1,100}/bin/(?:ba)?sh|bash\\s-.{0,4}i.{1,20}/dev/tcp/|subprocess.call\\(.{0,6}/bin/(?:ba)?sh|fsockopen\\(.{1,50}/bin/(?:ba)?sh|perl.{1,80}socket.{1,120}open.{1,80}exec\\(.{1,5}/bin/(?:ba)?sh|\\{echo,.{10,400}{base64,-d}'}

并简单写了个控制器模拟反序列化过程(一个字懒)

1
2
3
4
5
6
7
8
9
10
11
12
@RequestMapping("/off")
public void off(){
try {
Class<?> clz = Thread.currentThread().getContextClassLoader().loadClass("com.baidu.openrasp.config.Config");
java.lang.reflect.Method getConfig = clz.getDeclaredMethod("getConfig");
java.lang.reflect.Field disableHooks = clz.getDeclaredField("disableHooks");
disableHooks.setAccessible(true);
Object ins = getConfig.invoke(null);

disableHooks.set(ins,true);
} catch (Exception e) {}
}

首先执行命令返回可爱小恐龙

当我访问off路由成功关闭rasp的hook功能

当然你可能会说还有其他的关闭的hook点,比如刚刚上面提到的doCheckWithoutRequest实际上最终是通过doRealCheckWithoutRequest去进行下一步操作,但毕竟也是类似的意思就不多考虑这些更改属性的了点到为止,毕竟只要破坏中间任一环节即可

覆盖插件

我们知道OpenRASP通过InitFileWatcher,一旦其中的js文件被创建改变删除都会触发插件的

并且我们可以看到插件配置当中对于文件上传js默认是关闭逻辑检测的开关

因此我们如果存在任意文件上传并且可以跨目录再并且知道插件路径的情况下,虽然不是很通用但好歹也是一个手段

至于有没有其他方式这里暂时我就不探究了,顺便吐槽学校的实训太累了,心理上的累

参考文章

官方文档

C++中构造函数的两种写法

JNIENV介绍

以OpenRASP为基础-展开来港港RASP的类加载