命令执行漏洞

命令执行漏洞是指应用有时需要调用一些执行系统命令的函数,如果系统命令代码未对用户可控参数做过滤,则当用户能控制这些函数中的参数时,就可以将恶意系统命令拼接到正常命令中,从而造成命令执行攻击。下面以 Java 语言源代码为例,分析命令注入产生的原因以及修复方法。

命令执行攻击主要存在以下几个危害:继承 Web 服务程序的权限去执行系统命令或读/写文件,反弹 shell,控制整个网站甚至控制服务器,进一步实现内网渗透。

在 PHP 开发语言中有 system()、exec()、shell_exec()、eval()、passthru()等函数可以执行系统命令。在 Java 开发语言中可以执行系统命令的函数有 Runtime.getRuntime.exec 和ProcessBuilder.start,其中,Runtime.getRuntime.exec 是在 Java1.5 之前提供的,Java1.5 之后则提供了 ProcessBuilder 类来构建进程。

0x00:ProcessBuilder命令执行漏洞

1)ProcessBuilder命令执行方法

Java.lang.ProcessBuilder 类用于创建操作系统进程,每个 ProcessBuilder 实例管理一个进程属性集。 start() 方法利用这些属性创建一个新的 Process 实例,可以利用ProcessBuilder 执行命令。

ProcessBuilder 执行命令的方式如下:

ProcessBuilder pb = new ProcessBuilder("myCommand", "myArg"); 
Process process = pb.start();

例如,使用 ProcessBuilder 执行“ls -al”这个命令:(这里要注意是Win还是Linux)

public class exec { 
public static void main(String[] args) throws IOException { 
 //执行系统命令
 ProcessBuilder p = new ProcessBuilder("ls","-al"); 
 Process pb = p.start(); 
 //获取执行完成命令后的结果并输出 
 String line; 
 BufferedReader reader = new BufferedReader(new InputStreamReader(pb.getInputStream(), 
"GBK")); 
 while ((line = reader.readLine()) != null) { 
 System.out.println(line); 
 }
  reader.close(); 
 } 
} 

2)ProcessBuilder命令执行漏洞利用

Java 命令执行漏洞的前提是执行命令的参数可控,参数没有经过相关过滤。通过前面的例子我们知道了 ProcessBuilder 进行命令执行的方法及过程,下面我们通过典型的 Java 代码讲解命令执行漏洞,示例程序包首先通过“request.getParameter("ip")”获取 ip 参 数传入的数据,然后利用 ProcessBuilder 进行 ping 命令的执行,最后将相关结果返回。典型漏洞代码如下:

<%
  String ip=request.getParameter("ip"); try {
  ProcessBuilder p = new ProcessBuilder("ping","-t","3",ip); Process pb = p.start();
  
  String line;
  BufferedReader reader = new BufferedReader(new InputStreamReader(pb.getInputStream(),
  “GBK”));
  while ((line = reader.readLine()) != null) { 
  out.println(line);
}
  reader.close();
} catch (Exception e) {
  out.println(e); 
    }
  %>
 

因为通过“request.getParameter("ip")”获取Ip参数传入的数据没有经过过滤就传入ProcessBuilder进行执行,故攻击者通过命令连接符就有可能拼接执行额外的命令。例如 使用命令连接符“;”进行多条命令拼接以便执行 id 命令,输入“ip=127.0.0.1;id”。

但是页面出现报错,如图所示。这是因为 Java 的 Runtime.getRuntime.exec 和 ProcessBuilder.start 执行系统命令时,实际上并没有获得 UNIX 或 Linux shell 并在其中运 行命令。因此,要使用 UNIX/Linux 管道之类的功能,必须先调用一个 shell 程序。例如想要通过 Java 执行“ls;id”这个命令,必须先调用 shell 程序/bin/sh,才能在 shell 程序中 执行“ls;id”这个命令:“/bin/sh -c "ls;id"”。

 

 

当使用shell程序执行命令时,如果传入的参数没有经过过滤,就可能会产生命令执行攻击。

<%
    String ip=request.getParameter("ip");
    try{
    String exec="ping -t 3 "+ip;
    ProcessBuilder p = new ProcessBuilder("bash", "-c", exec); 
    Process pb = p.start();
    
    String line;
    BufferedReader reader = new BufferedReader(new InputStreamReader(pb.getInputStream(),
    "GBK"))
    while((line = reader.readLine()!=null){
    out.println(line);
    }
    reader.close();
    )catch(Exception e){
        out.println(e);
    }
%>

输入“ip=127.0.0.1;id”,通过“;”进行命令拼接后发现:程序执行了 ping 和 id 两个 命令,命令执行攻击成功

 

 

0x01:Runtime exec命令执行漏洞

java.lang.Runtime 公共类中的 exec()方法同样也可以执行系统命令,exec()方法的使用 方式有以下 6 种:

//在单独的进程中执行指定的字符串命令
public Process exec(String command) 
//在单独的进程中执行指定的命令和参数
public Process exec(String[] cmdarray) 
//在具有指定环境的单独进程中执行指定的命令和参数
public Process exec(String[] cmdarray, String[] envp) 
//在具有指定环境和工作目录的单独进程中执行指定的命令和参数 
public Process exec(String[] cmdarray, String[] envp, File dir) 
//在具有指定环境的单独进程中执行指定的字符串命令
public Process exec(String command, String[] envp) 
//在具有指定环境和工作目录的单独进程中执行指定的字符串命令 
public Process exec(String command, String[] envp, File dir)

1)Runtime exec执行字符串参数和数组参数

  exec()方法在执行命令时,当传入的参数为字符串参数和数组参数时会有不同的返回结果:

首先利用exec()方法执行字符串命令,例如:使用 exec()执行 ping 这个命令,是使用 以下代码执行“Process proc = Runtime.getRuntime().exec("ping 127.0.0.1");”,如图所示,会返回执行完成“ping 127.0.0.1”后的结果,这个返回结果是正常的。(这里记得构造回显

当使用 exec()执行“ping 127.0.0.1;ls”时,在执行多个命令或者命令中存在“>”或 “|”等特殊字符的情况下,就会发生错误并且不会回显,进程会直接结束。执行以下代码:

Process proc = Runtime.getRuntime().exec("ping 127.0.0.1;ls");

当将传入的命令改为数组参数传入后,发现 ping 和 id 两个命令可以正常执行:

        String[] command = {"/bin/sh","-c","ping -t 3 127.0.0.1;id"};
//        Process proc = Runtime.getRuntime().exec("ping 127.0.0.1;ls");
        Process proc = Runtime.getRuntime().exec(command);

但是为什么传入字符串与传入数组会有不同的执行结果呢?下面我们跟入 exec 函数进行分析:

跟入 exec 函数后,发现其调用的是“exec(command,null,null)”方法跟入“exec(command, null, null)”方法,发现其调用的是“exec(String command, String[] envp, File dir)”这个数组参数的方法。综上所述,“exec(String command)”这个字 符串参数实际调用的是“exec(String command, String[] envp, File dir)”这一数组参数的方 法,但是为什么传入字符串跟传入数组会有不同的执行结果?我们从下面的代码中看到, command 通过 StringTokenizer 进行处理,然后再调用“exec(String[] cmdarray, String[] envp, File dir)”。那么关键点就在于 StringTokenizer 如何对 command 进行处理

 

 

我们跟入 StringTokenizer 进行分析(见图 2-85),发现\t\n\r\f 会对传入的字符串进行 分割,分割完成后会返回一个 cmdarray 数组,该处理导致了在调用 exec()方法执行命令 时,传入字符串参数和传入数组参数时的返回结果不同。

2)Runtime exec 动态调试分析执行字符串参数和数组参数

下面我们通过动态调试的方法分析当 exec()方法在传入字符串参数和数组参数时结果有何不同:

//exec 执行字符串参数
String command="/bin/sh -c \"ping -t 3 127.0.0.1;id \"";
// exec 执行数组参数
String[] command = { "/bin/sh", "-c", "ping -t 3 127.0.0.1;id" };

当 exec()执行字符串参数时,经过动态调试分析,发现经过 StringTokenizer 这个类进 行拆分之后变成:

{"/bin/sh","-c",""ping","-t","3","127.0.0.1;id","""}

这样就改变了原有的执行命令的语义,导致命令不能正常执行;

但是当 exec()执行数组参数时,经过动态调试分析,发现并没有经过 StringTokenizer 这个 类进行拆分,而是直接调用了 ProcessBuilder 执行“{ "/bin/sh", "-c", "ping -t 3 127.0.0.1;id" }”命令,这样便正常执行了数组参数的命令,同样如上图所示。

3)Runtime exec命令执行漏洞利用

当利用 exec()进行命令执行时,如果参数没有经过过滤就可能通过命令拼接符进行命令拼接执行多条命令。典型示例代码如下:(jsp相关代码)

<%
String ip=request.getParameter("ip"); try {
String[] command = { "/bin/sh", "-c", "ping -t 3 " +ip}; Process proc = Runtime.getRuntime().exec(command);
String line;
BufferedReader reader = new BufferedReader(new InputStreamReader(proc.getInputStream(),
while ((line = reader.readLine()) != null) { out.println(line);
}
reader.close();
} catch (Exception e) {
out.println(e); }
%>

通过 ip 参数传入 command 数组变量中,然后执行 ping 命令,当输入“ip=127.0.0.1” 时,返回“ping 127.0.0.1”后的数据信息

 

 

因为 ip 参数没有经过过滤就直接拼接到了 command 变量中,这样就造成了命令执行漏洞,输入“ip=127.0.0.1;id”,这样就可以执行 ping 127.0.0.1 和 id 两条命令:

通过上面的典型案例讲解了 Runtime exec 方法在参数可控的情况下命令执行漏洞的利用方式,下面我们讲解 Runtime exec 方法在命令本身可控的情况下如何进行命令执行漏洞的利用:

"GBK"));
<%
          String cmd=request.getParameter("cmd"); 
          try {
                Process proc = Runtime.getRuntime().exec(cmd);
                String line;
                BufferedReader reader = new BufferedReader(new InputStreamReader(proc.getInputStream(),
                while ((line = reader.readLine()) != null) { 
                out.println(line);
}
                reader.close();
      } catch (Exception e) {
            out.println(e);
             }
%>

通过 cmd 参数输入相关命令,通过“Runtime.getRuntime().exec”执行命令,此处的 cmd 参数可控而且没有经过任何过滤就传入 exec()方法中执行,因此造成了命令执行漏 洞。我们先看一下程序输入正常的 cmd 参数时是如何执行系统命令的,输入“cmd=ls”, 返回了执行 ls 命令后的数据信息。

 

 

当输入“cmd=ls;cat /etc/passwd”后,返回“java.io.IOException: Cannot run program"ls;id": error=2, No such file or directory”这个错误信息,如图2-91所示。因为前文讲到过Java通过“Runtime.getRuntime().exec”执行命令并不是启动一个新的shell,所以就会有报错信息,需要重新启动一个shell才能正常执行此命令

启动一个新的shell执行多个命令,输入“cmd=sh -c ls;id”,发现命令执行成功,返回了ls和id命令的信息

输入“cmd=sh -c ls;cat /etc/passwd”命令不能正常执行的原因是,如果 exec 方法执行的参数是字符串参数,参数中的空格会经过 StringTokenizer 处理,处理完成后会改变原有的语义导致命令无法正常执行。要想执行此命令要绕过 StringTokenizer 才可以,只要找到可以代替空格的字符即可,如{IFS}、IFS9 等。输入“cmd=sh -c ls;cat{IFS}/etc/passwd”后会有以下报错:

Invalid character found in the request target. 
The valid characters are defined in RFC 7230 and RFC 3986

也就是说,我们的请求中包含无效的字符。查看 RFC 规范知,url 中只允许包含英文字母(a~z 和 A~Z)、数字(0~9)、“-_.~”这 4 个特殊字符,以及保留字符(! * ’ ( ) ; :@ & = + $ , / ? # [ ])共 84 个字符,刚才的请求中出现了{}大括号,导致了报错,如图所示:

 

对{}进行 url 编码,输入“cmd=sh%20-c%20ls;cat$%7BIFS%7D/etc/passwd”,发现可以正常执行 ls 和 cat /etc/passwd 两个命令:

 


命令执行漏洞修复:

开发人员应将现有 API 用于其语言。例如:不要使用 Runtime.exec()发出“mail”命令,而要使用位于 javax.mail 的可用 Java API。如果不存在这样的可用 API,则开发人员应清除所有输入以查找恶意字符。

posted @ 2023-04-10 11:26  Arrest  阅读(606)  评论(0编辑  收藏  举报