標題:Java 本地命令執行漏洞

taibeihacker

Moderator

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
package com.geekby;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
public class Main {
public static void main(String[] args) throws IOException {
String cmd='';
Process p=Runtime.getRuntime().exec('ping 127.0.0.1' + cmd);
InputStream fis=p.getInputStream();
BufferedReader br=new BufferedReader(new InputStreamReader(fis));
String line=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, Runtime (java.lang)
exec:450, Runtime (java.lang)
exec:347, Runtime (java.lang)
main:8, Main (com.geekby)
通過跟進調用鏈可以發現,exec 方法最終調用到了一個重載函數exec(String command, String[] envp, File dir) 中:
202202131512055.png-water_print

命令以字符串出入到該函數後,首先通過StringTokenizer 對其進行了處理,根據\t\n\r\f 把傳入的command 分割:
202202131527906.png-water_print

經過處理之後,最後實例化了ProcessBuilder 來處理傳入的cmdarray。在此處也可以發現,Runtime.getRuntime.exec() 的底層實際上也是ProcessBuilder。
202202131528937.png-water_print

繼續跟進ProcessBuilder 類中的start 方法,在該方法中將cmdarry 第一個參數cmdarry[0] 當作要執行的命令,把後面的cmdarry[1:] 作為命令執行的參數轉換成byte 數組argBlock。
202202131534214.png-water_print

此時prog 是要執行的命令ping, argBlock 都是傳給ping 的參數127.0.0.1;pwd,經過StringTokenizer 對字符串的處理,改變了命令執行的語義,無法將分號作為命令分隔符,進而實現命令注入。

exec(String cmdarray[])​

Java Runtime 包中存在exec 函數的重載函數,其參數類型為字符串數組。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.geekby;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
public class Main {
public static void main(String[] args) throws IOException {
String cmd=';pwd';
Process p=Runtime.getRuntime().exec(new String[]{'/bin/sh', '-c', cmd});
InputStream fis=p.getInputStream();
BufferedReader br=new BufferedReader(new InputStreamReader(fis));
String line=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'
因此,在參數可控的情況下,不能採用命令分割的形式進行命令注入。根據具體情況,可以採取base64 編碼的形式。

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
public class RCE {
public static 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
package com.geekby;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
String cmd=';pwd';
ProcessBuilder pb=new ProcessBuilder('ping', '127.0.0.1', cmd);
Process process=pb.start();
InputStream fis=process.getInputStream();
BufferedReader br=new BufferedReader(new InputStreamReader(fis));
String line=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 的構造函數是private 屬性的,因此,需要用反射的方式調用其靜態方法start。
202202131653031.png-water_print

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.geekby;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;
public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException, InvocationTargetException, IllegalAccessException, NoSuchMethodException {
Class clazz=Class.forName('java.lang.ProcessImpl');
Method start=clazz.getDeclaredMethod('start', String[].class, Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class);
start.setAccessible(true);
start.invoke(null, (Object) new String[]{'open', '-a', 'Calculator'}, 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)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
main:14, main (com.geekby)

防御​

本地命令執行是一種非常高風險的漏洞,在任何時候都應當非常謹慎的使用,在業務中如果使用到了本地系統命令那麼應當禁止接收用戶傳入參數。在很多時候攻擊者會利用某些漏洞(如:Struts2、反序列化等)來攻擊我們的業務系統,最終利用Java 本地命令執行達到控制Web 服務器的目的。這種情況下用戶執行的系統命令對我們來說就不再受控制了,我們除了可以配置SecurityManager規則限制命令執行以外,還可以使用RASP 來防禦本地命令執行就顯得更加的便捷可靠。

RASP 防御 Java 本地命令执行​

在Java 底層執行系統命令的API 是java.lang.UNIXProcess/ProcessImpl#forkAndExec 方法,forkAndExec 是一個native 方法,如果想要Hook 該方法需要使用Agent 機制中的Can-Set-Native-Method-Prefix,為forkAndExec 設置一個別名,如:__RASP__forkAndExec,然後重寫__RASP__forkAndExec 方法邏輯,即可實現對原forkAndExec 方法Hook。
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
/**
* Hook Windows系統ProcessImpl 類構造方法
*/
@RASPMethodHook(
className='java.lang.ProcessImpl', methodName=CONSTRUCTOR_INIT,
methodArgsDesc='.*', methodDescRegexp=true
)
public static class ProcessImplHook extends RASPMethodAdvice {
@Override
public RASPHookResult? onMethodEnter() {
try {
String[] commands=null;
//JDK9+的API參數不一樣!
if (getArg(0) instanceof String[]) {
commands=getArg(0);
} else if (getArg(0) instanceof byte[]) {
commands=new String[]{new String((byte[]) getArg(0))};
}
//檢測執行的命令合法性
return LocalCommandHookHandler.processCommand(commands, getThisObject(), this);
} catch (Exception e) {
RASPLogger.log(AGENT_NAME + '處理ProcessImpl異常:' + e, e);
}
return new RASPHookResult?(RETURN);
}
}
 
返回
上方