H

Java 本地命令执行漏洞

HackApt-37 Team已验证会员

黑客倉庫站長

贡献: 83%

Java 本地命令执行漏洞​

背景​

JDK提供了用于执行本地系统命令的本机功能,并且攻击者可以通过此漏洞在目标服务器中执行任意系统命令。可用于在Java中执行系统命令的方法包括API:
java.lang.runtime
java.lang.processbuilder
java.lang.unixprocess/processImpl。

Runtime 命令执行​

exec(String command)​

在Java中,Java.lang.runtime类的EXEC方法通常用于执行本地系统命令。
以以下程序执行命令为例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
包com.geekby;
导入java.io.bufferedreader;
导入java.io.ioexception;
导入java.io.inputstream;
导入java.io.io.inputStreamReader;
公共类Main {
公共静态void main(string [] args)抛出ioexception {
字符串cmd='';
进程p=runtime.getRuntime()。exec('ping 127.0.0.1' + cmd);
InputStream FIS=P.GetInputStream();
BufferedReader br=new BufferedReader(new InputStreamReader(FIS));
字符串线=null;
while(((line=br.readline())!=null){
system.out.println(line);
}
}
}
以上程序可以成功执行ping命令。假设攻击者现在可以控制CMD参数,并通过命令剪接执行其他命令。以cmd='; pwd'为例,可以发现无法执行命令,甚至ping命令也无法回荡结果。
202202131520032.png-water_print

为了探索命令执行失败的原因,首先跟踪程序调用堆栈:
1
2
3
4
5
6
7
8
create:-1,processimpl(java.lang)
init:386,processImpl(java.lang)
Start:137,ProcessImpl(java.lang)
Start:1029,ProcessBuilder(Java.lang)
exec:620,运行时(java.lang)
exec:450,运行时(java.lang)
exec:347,运行时(java.lang)
Main:8,Main(com.geekby)
通过跟踪呼叫链,可以发现EXEC方法最终呼叫到Overloaded函数EXEC(String命令,String [] Envp,File Dir):
202202131512055.png-water_print

命令输入并将函数作为字符串退出后,它首先通过StringTokenizer处理,并根据\ t \ n \ r \ f对传递的命令进行划分:
202202131527906.png-water_print

处理后,最终对过程构建器进行了实例化以处理传入的CMDARRAY。也可以在这里发现Runtime.getRuntime.exec()的基础层实际上是ProcessBuilder。
202202131528937.png-water_print

继续跟进ProcessBuilder类中的启动方法,其中CMDARRY第一个参数CMDARRY [0]被用作要执行的命令,并将后续的CMDARRY [1:]作为命令执行参数转换为字节阵列ArgBlock。
202202131534214.png-water_print

目前,prog是要执行的命令ping。 ArgBlock是所有参数127.0.0.1; PWD传递给Ping。在StringTokenizer过程之后,字符串,命令执行的语义将更改。分号不能用作命令分离器,然后实现命令注射。

exec(String cmdarray[])​

在Java运行时软件包中存在EXEC函数的重载函数,其参数类型是字符串数组。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
包com.geekby;
导入java.io.bufferedreader;
导入java.io.ioexception;
导入java.io.inputstream;
导入java.io.io.inputStreamReader;
公共类Main {
公共静态void main(string [] args)抛出ioexception {
字符串cmd='; pwd';
Process P=runtime.getRuntime()。exec(new String [] {'/bin/sh','-c',cmd});
InputStream FIS=P.GetInputStream();
BufferedReader br=new BufferedReader(new InputStreamReader(FIS));
字符串线=null;
while(((line=br.readline())!=null){
system.out.println(line);
}
}
}
跟进EXEC函数的基础代码,因为数组是直接传递的,因此字符串未由StringTokenizer处理。
202202131613486.png-water_print

最后跟进UnixProcess方法
202202131617663.png-water_print

目前,prog是要执行的命令/bin /sh,argblock是将所有参数传递给ping -c \ x00'ping 127.0.0.1; pwd'
因此,当参数可控时,不能以命令分割的形式执行命令注入。根据特定情况,可以进行基本64编码。

load()​

在Java Runtime软件包中,还有另一种形式的加载外部库来执行命令。通过加载动态链接库,例如Linux下的SO文件和Windows下的DLL文件。
1
msfvenom -p windows/x64/exec - platform win -a x64 cmd=calc.exe exitfunc=thread -f dll calc.dll
测试代码:
1
2
3
4
5
6
公共类RCE {
公共静态void main(string [] args){
Runtime RT=Runtime.getRuntime();
rt.load('d: \\ calc.dll');
}
}

ProcessBuilder​

使用ProcessBuilder类创建一个过程,创建一个ProcessBuilder实例,指定过程名称和必需的参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
包com.geekby;
导入java.io.ioexception;
公共类Main {
公共静态void main(string [] args)抛出ioexception {
字符串cmd='; pwd';
ProcessBuilder PB=new ProcessBuilder('Ping','127.0.0.1',CMD);
过程过程=pb.start();
inputStream fis=process.getInputStream();
BufferedReader br=new BufferedReader(new InputStreamReader(FIS));
字符串线=null;
while(((line=br.readline())!=null){
system.out.println(line);
}
}
}
致电堆栈:
1
2
3
4
5
create:-1,processimpl(java.lang)
init:386,processImpl(java.lang)
Start:137,ProcessImpl(java.lang)
Start:1029,ProcessBuilder(Java.lang)
Main:8,Main(com.geekby)
通过分析呼叫堆栈,我们可以发现在基础层的processBuilder呼叫的逻辑类似于runtime.getruntime.exec的逻辑,因此我不会在此处详细介绍。

ProcessImpl​

由于ProcessImpl的构造函数是私有属性,因此需要在反思中调用其静态方法开始。
202202131653031.png-water_print

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
包com.geekby;
导入java.io.ioexception;
导入java.lang.reflect.invocationTargetException;
导入java.lang.reflect.Method;
导入java.util.map;
公共类Main {
公共静态void main(string [] args)抛出IOException,classNotFoundException,InvocationTargetException,illegalaccessexception,nosuchmethodexception {
class clazz=class.forname('java.lang.processimpl');
方法start=clazz.getDeclaredMethod('start',string []。class,map.class,string.class,processBuilder.redirect []。class,boolean.class);
start.setAccessible(true);
start.invoke(null,(对象)new String [] {'open','-a','calculator'},null,null,null,null,false);
}
}
致电堆栈:
1
2
3
4
5
6
7
8
create:-1,processimpl(java.lang)
init:386,processImpl(java.lang)
Start:137,ProcessImpl(java.lang)
Indoke0:-1,Nativemethodaccessorimpl(Sun.反射)
Invoke:62,Nativemethodaccessorimpl(Sun.反射)
Invoke:43,委托人莫托克斯托里姆普(Sun.反射)
Invoke:498,方法(java.lang.reflect)
Main:14,Main(com.geekby)

防御​

本地命令执行是一个非常高风险的漏洞,应始终谨慎使用。如果在业务中使用本地系统命令,则应禁止接收到传入参数。在许多情况下,攻击者将利用某些漏洞(例如Struts2,避免等)来攻击我们的业务系统,并最终使用Java Local命令执行来实现控制Web服务器的目的。在这种情况下,用户执行的系统命令不再受到控制。除了配置SecurityManager规则以限制命令的执行外,我们还可以使用RASP来防御本地命令执行,这使其更加方便和可靠。

RASP 防御 Java 本地命令执行​

在基础Java中执行系统命令的API是Java.lang.unixprocess/ProcessImpl#forkandexec方法。 forkandexec是一种本机方法。如果要挂钩,则需要在代理机制中使用CAN-SET-native-native-method-prefix为forkandexec设置一个别名,例如:__rasp__forkandexec,然后重写__rasp__rasp__forkandexec方法逻辑以实现原始的forkandexec方法。
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
/**
*挂钩Windows系统ProcessImpl类构造函数
*/
@raspmethodhook(
className='java.lang.processimpl',methodName=structureor_init,
MethodArgsdesc='。*',methoddescregexp=true

公共静态类ProcessImplhook扩展了RaspMethodAdvice {
@Override
公共rasphookresult? onMethodenter(){
尝试{
字符串[]命令=null;
//JDK9+的API参数不同!
if(getarg(getarg(0)instance of String []){
命令=getarg(0);
} else if(getarg(getarg(getarg(0)instance of byte []){
命令=new String [] {new String(byte [])getarg(0))};
}
//检测执行命令的合法性
返回localCommandHookhookHookhandler.ProcessCommand(命令,getThisObject(),this);
} catch(异常E){
rasplogger.log(Agent_name +'ProcessImpl异常:' + E,E);
}
返回新的rasphookresult?(返回);
}
}
 
后退
顶部