Java OS 命令注入學習筆記
-6 mins
ofollow,noindex" target="_blank">Java Injection OS Command Injection
0x01 簡介
- Java 執行系統命令的方法
- 易導致命令注入的危險寫法以及如何避免
0x02 注意點
首先要注意的是,通過 Java 來執行系統命令時,並不是通過 shell 來執行 (Linux下),因此如果需要用到如 pipeline (|
)、;
、&&
、||
等 shell 特性時,需要建立 shell 來執行命令,如:
/bin/sh -c "ls -lh; pwd"
具體可參考 https://alvinalexander.com/java/java-exec-system-command-pipeline-pipe
0x03 執行方式
ProcessBuilder
java.lang.ProcessBuilder
中start()
方法可以執行系統命令,命令和引數可以通過構造方法的 String List 或 String 陣列來傳入
-
ProcessBuilder(List<String> command)
-
ProcessBuilder(String... command)
如執行ls -lh /home/www
的例子
String[] cmdList = new String[]{"ls", "-lh", "/home/www"}; ProcessBuilder builder = new ProcessBuilder(cmdList); builder.redirectErrorStream(true); Process process = builder.start();
因為 Java 中執行命令不是通過 shell,若沒有手動建立 shell 來執行命令,命令非完全可控時,正常的情況下是無法使用;
、&&
等來實現命令注入的,例如
命令的某個引數可控
// String dir = "xx"; String[] cmdList = new String[]{"ls", "-lh", dir}; ProcessBuilder builder = new ProcessBuilder(cmdList); builder.redirectErrorStream(true); Process process = builder.start(); printOutput(process.getInputStream());
dir
引數使用者可控,如想通過傳入/home/www;id
, 來執行 id 命令,是無法成功的,程式的輸出為
ls: /home/www;id: No such file or directory
再看一個例子
// String cmd = "xx"; ProcessBuilder builder = new ProcessBuilder(cmd); builder.redirectErrorStream(true); Process process = builder.start(); printOutput(process.getInputStream());
cmd
引數使用者可控,那是否就可以執行任意命令了呢?
答案是可執行沒有引數的命令,如ls
、pwd
,如執行curl example.com
則會失敗,會提示如下錯誤
java.io.IOException: Cannot run program "curl example.com": error=2, No such file or directory
原因為這裡cmd
的值表示的是執行命令的檔案路徑,因此無法使用引數
前面說到是在正常情況下,但一些特殊情況下,如果執行的命令的某個引數存在解析問題,即存在引數注入,也會導致命令執行,如CVE-2018-3785" rel="nofollow,noindex" target="_blank">CVE-2018-3785 、360.cn/warning/detail?id=9ba8d91f9f69c50cae5050196f39bb0c" rel="nofollow,noindex" target="_blank">CVE–2017–1000117
前面所說的是在非 shell 環境下執行命令的情況,那如果手動建立了 shell 來執行命令,則很有可能會存在命令注入,例如:
// String dir = "xxxx"; String[] cmdList = new String[]{"sh", "-c", "ls -lh " + dir}; ProcessBuilder builder = new ProcessBuilder(cmdList); builder.redirectErrorStream(true); Process process = builder.start(); printOutput(process.getInputStream());
dir
引數使用者可控,如果傳入如&& pwd
,則可以成功執行pwd
命令
再來看一種情況
String[] cmdList = new String[]{"sh", "-c", "echo test", dir}; ProcessBuilder builder = new ProcessBuilder(cmdList); builder.redirectErrorStream(true); Process process = builder.start(); printOutput(process.getInputStream());
這種情況下,dir 傳入pwd
或;pwd
都無法執行,因為只有echo test
會作為-c
選項的引數值
因此,在大多數情況下,要想通過ProcessBuilder
來執行任意命令,需要程式碼中建立 shell 來執行命令,並且引數可控或存在拼接
Runtime
java.lang.Runtime
中exec()
函式同樣可以執行系統命令,命令引數支援 String 和 String 陣列兩種方式,同時支援設定環境變數、子程序工作目錄 (working directory) 引數,具體方法包括:
exec(String command) exec(String[] cmdarray) exec(String command, String[] envp) exec(String command, String[] envp, File dir) exec(String[] cmdarray, String[] envp, File dir)
這裡來看一下exec(String command)
函式,根據原始碼可知,其內部會呼叫exec(String command, String[] envp, File dir)
,方法程式碼如下
public Process exec(String command, String[] envp, File dir) throws IOException { if (command.length() == 0) throw new IllegalArgumentException("Empty command"); StringTokenizer st = new StringTokenizer(command); String[] cmdarray = new String[st.countTokens()]; for (int i = 0; st.hasMoreTokens(); i++) cmdarray[i] = st.nextToken(); return exec(cmdarray, envp, dir); }
可以看到,傳入的字串命令會先經過StringTokenizer
進行處理,即使用分隔符,包括空格,\t\n\r\f
對字串進行分隔後,再呼叫exec(String[] cmdarray, String[] envp, File dir)
,程式碼如下
public Process exec(String[] cmdarray, String[] envp, File dir) throws IOException { return new ProcessBuilder(cmdarray) .environment(envp) .directory(dir) .start(); }
即最後是通過ProcessBuilder
來執行的,那麼如果直接呼叫引數為 String 陣列的exec()
函式,則和ProcessBuilder
存在同樣的問題
而直接傳入 String 時,會先經過StringTokenizer
的分隔處理,然後在使用ProcessBuilder
,因此這裡需要弄清StringTokenizer
是如何分割字串命令的
先來看一下Runtime 執行系統命令的程式碼示例:
// String cmd = "xx"; Process process = Runtime.getRuntime().exec(cmd); process.waitFor(); printOutput(process.getInputStream()); printOutput(process.getErrorStream());
cmd
輸入和對應StringTokenizer
分隔後的值
-
ls -lh; id
=>["ls", "-lh;", "id"]
無法執行,輸出ls: illegal option – ; usage: ls [-ABCFGHLOPRSTUWabcdefghiklmnopqrstuwx1][file …][file …]
-
ls -lh;id
=>["ls", "-lh;id"]
無法執行,輸出ls: illegal option – ; usage: ls [-ABCFGHLOPRSTUWabcdefghiklmnopqrstuwx1][file …][file …]
-
sh -c 'ls -lh;id'
=>["sh", "-c", "'ls", "-lh;id'"]
兩邊有單引號,無法執行,輸出-lh;id’: -c: line 0: unexpected EOF while looking for matching `’’ -lh;id’: -c: line 1: syntax error: unexpected end of file
-
sh -c "ls;id"
=>["sh", "-c", "\"ls;id\"]
注意兩邊的雙引號,無法執行,輸出sh: ls;id: command not found
-
sh -c ls;id
=>["sh", "-c", "ls;id"]
,id
命令可成功執行
因此,簡單總結一下:
-
如果引數完全可控,則可以執行任意命令
-
若沒有手動建立 shell 執行命令,沒有存在引數注入,則無法實現命令注入
-
手動建立 shell 執行命令,可執行
-c
的引數值的命令,但值內不能有空格、\t\n\r\f
分隔符,否則會被分割// 相當於執行 sh -c curl,example.com 引數會被忽略 String cmd = "sh -c curl example.com"; // \t 也是分割符之一 String cmd = "sh -c curl\texample.com"; // 使用 ${IFS} (對應內部欄位分隔符) 來代替空格,成功執行 String cmd = "sh -c curl${IFS}example.com";
0x04 修復方案
-
應儘量避免使用
Runtime
和ProcessBuilder
來執行系統命令,可搜尋系統是否提供 API 來完成同樣的功能,如執行刪除檔案rm /home/www/log.txt
的命令,可以使用File.delete()
等函式來代替 -
無法避免執行命令時,應當儘可能避免建立 shell 來執行系統命令,優先使用
Runtime
和ProcessBuilder
的 字串陣列String[] cmdarray
的 方法,可一定程度上降低命令注入的產生 -
最後,可考慮使用白名單的方式,限制可執行的命令和允許的引數值,或限制使用者輸入的所允許字元,如只允許字母陣列、下劃線
private static final Pattern FILTER_PATTERN = Pattern.compile("[0-9A-Za-z_]+"); if (!FILTER_PATTERN.matcher(input).matches()) { // Handle error }
0x05 參考
- https://www.owasp.org/index.php/Command_injection_in_Java
-
https://docs.oracle.com/javase/7/docs/api/java/lang/Runtime.html
- IDS07-J. Sanitize untrusted data passed to the Runtime.exec() method