命令执行
1、通过一些关键字可以定位到
一、java命令执行函数
runtime
processBuilder
ScriptEngineManager
yaml
groovy
二、SPEL表达式 (使用SimpleEvaluationContext修复)
SpelExpressionParser (解析SpEL表达式的类)
StandardEvaluationContext(SpEL的EvaluationContext实现默认)
(一)、java命令执行函数
1、runtime/exec
在 Java 代码审计中,Runtime.exec()
或 ProcessBuilder
的使用可能存在命令执行漏洞。这种漏洞通常发生在开发者接受用户输入并将其直接传递给这些执行外部命令的函数时,而未经过充分的验证和过滤。攻击者可以通过精心构造的输入来执行恶意命令,从而导致安全问题。
漏洞代码1
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class VulnerableCode {
public void executeCommand(String userInput) {
try {
String command = "echo " + userInput;
Process process = Runtime.getRuntime().exec(command);
//使用Java的Runtime类执行用户输入的命令,并返回一个Process对象。
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
//创建一个BufferedReader对象reader,用于读取从Process对象返回的输入流。
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
process.waitFor();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
VulnerableCode vulnerableCode = new VulnerableCode();
String userInput = "Hello, World!"; // 用户输入,未经验证
vulnerableCode.executeCommand(userInput);
}
}
漏洞代码2
package com.example.demo;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
@WebServlet("/rce1")//这是一个Servlet的注解
public class rce1Servlet extends HttpServlet {
//定义一个名为rce1Servlet的类,它继承了HttpServlet类
@Override//覆盖
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("utf-8");
resp.setCharacterEncoding("utf-8");
resp.setContentType("text/html; charset=utf-8");
String cmd = req.getParameter("cmd");
StringBuffer sb = new StringBuffer();
//创建一个StringBuffer对象,用于存储命令的输出结果。
BufferedReader br = new BufferedReader(new InputStreamReader(Runtime.getRuntime().exec(cmd).getInputStream()));
String line;
while ((line=br.readLine())!=null){
sb.append(line).append("</br>");
}
// 将读取到的每一行内容添加到StringBuffer中,并在每一行后面添加一个换行符。
br.close();
resp.getWriter().write(sb.toString());
}
}
漏洞触发
http://localhost:8080/rce1?cmd=calc
追踪三个函数的关系,直接跟进exec函数,一直“ctrl+点击exec”函数,最终可以跟到,是调用的ProcessBuilder类的start函数
继续跟进start函数,看到调用的是ProcessImpl.start
2、processBuilder
对于 ProcessBuilder
,攻击者可能会通过在命令或参数中插入特殊字符来尝试执行恶意命令。
下面是一个展示漏洞的简单示例:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class VulnerableProcessBuilder {
public void executeCommand(String userInput) {
try {
ProcessBuilder processBuilder = new ProcessBuilder("echo", userInput);
//创建一个ProcessBuilder对象,
Process process = processBuilder.start();
//使用ProcessBuilder启动一个新的进程
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
//// 创建一个BufferedReader对象来读取进程的输出
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
process.waitFor();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
VulnerableProcessBuilder vulnerableProcessBuilder = new VulnerableProcessBuilder();
String userInput = "Hello, World!"; // 用户输入,未经验证
vulnerableProcessBuilder.executeCommand(userInput);
}
}
漏洞案例2
package com.example.demo;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
@WebServlet("/rce2")
public class rce2Servlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("utf-8");
resp.setCharacterEncoding("utf-8");
resp.setContentType("text/html; charset=utf-8");
String cmd = req.getParameter("cmd");
String[] arrcmd={"cmd.exe","/c",cmd};
//创建一个字符串数组"arrcmd",其中包含三个元素
StringBuffer sb = new StringBuffer();
//创建一个StringBuffer对象,用于构建响应的HTML文本
ProcessBuilder processBuilder= new ProcessBuilder(arrcmd);
Process process = processBuilder.start();
BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line=br.readLine())!=null){
sb.append(line).append("</br>");
}
br.close();
resp.getWriter().write(sb.toString());
}
}
3、ProcessImpl
ProcessImpl类通常是为ProcessBuilder.start()创建新进程服务的,不能直接去调用。
看到ProcessImpl类构造器私有,所以不能直接对其进行实例化,为了演示可以用反射进行调用。
在获取到一个静态方法后,必须用setAccessible修改它的作用域,否则不能调用。
public class Demo { public static void main(String[] args){ try { String[] cmds = {"calc"}; Class clazz = Class.forName("java.lang.ProcessImpl"); //使用Java的反射API获取名为"java.lang.ProcessImpl"的类的Class对象。这个类是Java的内置类,用于处理进程。 Method method = clazz.getDeclaredMethod("start", new String[]{}.getClass(), Map.class,String.class, ProcessBuilder.Redirect[].class, boolean.class); //在ProcessImpl类中获取一个名为"start"的方法,这个方法需要六个参数:一个String数组、一个Map对象、一个String对象、一个ProcessBuilder.Redirect数组、一个boolean对象。这些参数的类型都是通过类或接口名指定的。 method.setAccessible(true);//设置方法为可访问,即使它是私有的。 method.invoke(null,cmds,null,".",null,true); //调用上面获取的方法,传入的参数分别是null、上面定义的命令数组、null、当前目录".", null和true。这行代码试图启动一个名为"calc"的命令,并在当前目录执行。 } catch (Exception e) { System.out.println(e.toString()); } } }
知识扩展
调用Runtime#exec()
来执行calc
和ipconfig
命令:Runtime.getRuntime().exec(command)
import java.lang.reflect.Method;
public class ExecExample {
public static void main(String[] args) {
try {
// 获取 Runtime 类的 Class 对象
Class<?> clazz = Class.forName("java.lang.Runtime");
// 获取 Runtime.getRuntime() 方法
Method getRuntimeMethod = clazz.getMethod("getRuntime");
// 调用 Runtime.getRuntime() 方法,获取 Runtime 实例
Object runtime = getRuntimeMethod.invoke(null);
//getRuntimeMethod是通过引用获取Runtime类的getRuntime方法。invoke(null)是调用该方法,并传递null作为调用者对象,因为getRuntime方法是静态方法,不需要实例即可调用。
// 获取 exec 方法
Method execMethod = clazz.getMethod("exec", String.class);
//String.class是用于指定exec方法的参数类型,即要执行的系统命令
// 执行 calc 命令
execMethod.invoke(runtime, "calc");
// 执行 ipconfig 命令
execMethod.invoke(runtime, "ipconfig");
} catch (Exception e) {
System.out.println(e.toString());
}
}
}
漏洞修复
利用SecurityUtil.cmdfilter对传入的参数进行过滤,严格限制用户只能输入a-zA-Z0-9_-.字符
public class SecurityUtil {
public static String cmdfilter(String input) {
// 使用正则表达式过滤,只允许a-zA-Z0-9-_字符和.
return input.replaceAll("[^a-zA-Z0-9_\\-.]", "");
}
}
@RestController
@RequestMapping("/rce")
public class RceController {
@GetMapping("/runtime/exec")
public String CommandExec(String cmd) {
// 使用cmdfilter对用户输入进行过滤
String filteredCmd = SecurityUtil.cmdfilter(cmd);
Runtime run = Runtime.getRuntime();
StringBuilder sb = new StringBuilder();
try {
Process p = run.exec(filteredCmd);
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();
}
}
(二)、SpEL表达式
Spring Expression Language(SpEL)是在Spring框架中引入的一种表达式语言,用于在运行时执行查询和操作对象图。SpEL支持在运行时查询和操作对象图,它可以用于各种用途,包括查询对象属性、调用对象方法、计算表达式等。
SpEL的用法有三种形式
一种是在注解@Value中;
一种是XML配置;
一种是在代码块中使用Expression。
各种Spring CVE漏洞都是基于Expression形式的SpEL表达式注入
以下是SpEL的一些基本特性和用法:
- 基本语法
SpEL表达式使用${}
作为定界符,可以嵌套使用。例如:
// 字符串拼接
String expression = "Hello, #{'World' + '!'}. Today is #{T(java.time.LocalDate).now()}";
- 访问属性
通过.
操作符,可以访问对象的属性:
// 访问对象属性
String expression = "person.name";
- 调用方法
通过()
操作符,可以调用对象的方法:
// 调用对象方法
String expression = "person.sayHello()";
- 字面值
支持字符串、数字、布尔值等字面值:
// 字符串字面值
String expression = "'Hello, World!'";
// 数字字面值
String expression = "42";
// 布尔字面值
String expression = "true";
- 集合操作
SpEL支持对集合的操作,例如访问列表元素、映射的键值对等:
// 访问列表元素
String expression = "myList[0]";
// 访问映射的键值对
String expression = "myMap['key']";
- 运算符
SpEL支持常见的运算符,如算术运算符、关系运算符、逻辑运算符等:
// 算术运算
String expression = "5 + 2";
// 关系运算
String expression = "age > 18";
// 逻辑运算
String expression = "isMember and hasRole('ROLE_ADMIN')";
- 类型
可以使用T()
关键字获取Java类的静态方法或字段:
// 获取当前日期
String expression = "T(java.time.LocalDate).now()";
- 条件表达式
SpEL支持三元条件表达式:
// 三元条件表达式
String expression = "isMember ? 'Member' : 'Not a Member'";
- 正则表达式
SpEL支持对字符串进行正则表达式匹配:
// 正则表达式匹配
String expression = "'hello123' matches '[a-z]+\\d+'";
- 安全导航操作符
通过?.
操作符,可以避免在访问可能为null
的对象时抛出空指针异常:
// 安全导航操作符
String expression = "person?.address?.city";
漏洞代码
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
public class VulnerableSpelExample {
public static void main(String[] args) {
// 恶意的SPEL表达式,攻击者可以传入的数据
String userInput = "${T(java.lang.Runtime).getRuntime().exec('calc.exe')}";
// 创建一个SPEL表达式解析器
ExpressionParser parser = new SpelExpressionParser();
// 解析用户输入的SPEL表达式
Expression expression = parser.parseExpression(userInput);
// 评估表达式并执行
Object result = expression.getValue();
System.out.println("Result: " + result);
}
}
漏洞修复
SimpleEvaluationContext 旨在仅支持 SpEL 语言语法的一个子集。它不包括 Java 类型引用,构造函数和 bean 引用;所以最直接的修复方式是使用 SimpleEvaluationContext 替换StandardEvaluationContext
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.SimpleEvaluationContext;
public class SecureSpelExample {
public static void main(String[] args) {
// 恶意的SPEL表达式,攻击者可以传入的数据
String userInput = "${T(java.lang.Runtime).getRuntime().exec('calc.exe')}";
// 使用 SimpleEvaluationContext 创建 ExpressionParser
ExpressionParser parser = new SpelExpressionParser();
SimpleEvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
// 使用 ExpressionParser 解析用户输入,并在受限上下文中评估它
try {
Expression expression = parser.parseExpression(userInput);
Object result = expression.getValue(context);
System.out.println("Result: " + result);
} catch (Exception e) {
System.out.println("Error: " + e.getMessage());
}
}
}
其他
、、正常无回显
T(java.lang.Runtime).getRuntime().exec("calc")
new java.lang.ProcessBuilder("calc").start()
、、正常有回显
new java.util.Scanner(new java.lang.ProcessBuilder("cmd", "/c", "ipconfig /all").start().getInputStream(), "GBK").useDelimiter("asfsfsdfsf").next()
、、模板的无回显,就将上面的payload放到“ #{} ”
#{T(java.lang.Runtime).getRuntime().exec("calc")}
#{new java.lang.ProcessBuilder("calc").start()}
、、模板有回显,就将上面的payload放到“ #{} ”
#{new java.util.Scanner(new java.lang.ProcessBuilder("cmd", "/c", "ipconfig /all").start().getInputStream(), "GBK").useDelimiter("asfsfsdfsf").next()}
暂无评论内容