命令注入漏洞介绍(上篇) – 作者:nobodyshome

漏洞简介

本篇文章以Linux环境为背景,暂不讨论Windows环境的情况。命令注入漏洞介绍分为上下两篇,上篇主要从代码层面分析为什么会出现命令注入漏洞,下篇主要介绍怎么测试命令注入漏洞,以及怎么利用和怎么避免漏洞的产生

命令注入(操作系统命令注入)是一种注入类型漏洞,是指用户输入的数据(命令)被程序拼接并传递给执行操作系统命令的函数执行。编程语言如C/C++,Java, PHP, Python, Go, Rust等,都支持执行系统命令,所以都有可能存在命令注入漏洞。注入的命令以应用程序的当前权限被执行,如果应用程序是使用root权限执行,那么注入的命令也是以root执行。

我们平时讲的RCE漏洞,R是指Remote,C可以指代Code也可以指代Command,E是指Execution,所以远程命令执行(Remote Command Execution)也是RCE的一种。

命令注入不同于代码注入,代码注入是注入代码并执行代码,常见于解释型语言如java,python,php等,命令注入是注入操作系统命令并执行命令。

漏洞成因

开发人员在编写代码时,有时候需要执行系统命令,或者是执行某个二进制程序/脚本,来实现预期的目标。比如配置一个ip,使能某个功能等。

我们可以把代码分为三类

I) 不需要执行系统命令,代码不会调用命令执行相关函数

II) 需要执行系统命令,执行的命令是固定的,和用户输入无关

III) 需要执行系统命令,执行的命令和用户输入有关

很明显,I和II类是不会造成命令注入的,只有第III类可能存在命令注入,无论做测试还是代码审计,都把重点放在第III类的情况下。

下面我们来看看各种语言里,都有哪些函数支持执行系统命令,我选了几个主流的语言进行分析,分别是C/C++,Java, PHP, Python, Go以及Rust。

C/C++

C/C++ 在Linux环境,有三种常用的方式执行系统命令,分别是system,popen和exec家族函数。[1]

下面分别是这三个函数的man页面

SYNOPSIS
       #include <stdlib.h>

       int system(const char *command);
SYNOPSIS
       #include <stdio.h>

       FILE *popen(const char *command, const char *type);

       int pclose(FILE *stream);
SYNOPSIS
       #include <sys/types.h>
       #include <unistd.h>

       pid_t fork(void);

SYNOPSIS
       #include <unistd.h>

       extern char **environ;

       int execl(const char *pathname, const char *arg, ...
                       /* (char  *) NULL */);
       int execlp(const char *file, const char *arg, ...
                       /* (char  *) NULL */);
       int execle(const char *pathname, const char *arg, ...
                       /*, (char *) NULL, char *const envp[] */);
       int execv(const char *pathname, char *const argv[]);
       int execve(const char *pathname, char *const argv[],
                       char *const envp[]);       
       int execvp(const char *file, char *const argv[]);
       int execvpe(const char *file, char *const argv[],
                       char *const envp[]);

system和popen都很简单,针对exec家族函数,这个家族函数还蛮多的,很不好记住,我在stackoverflow查到一段针对exec家族函数的解释[2]

L vs V: whether you want to pass the parameters to the exec'ed program as

L: individual parameters in the call (variable argument list): execl(), execle(), execlp(), and execlpe()
V: as an array of char* execv(), execve(), execvp(), and execvpe()
The array format is useful when the number of parameters that are to be sent to the exec'ed process are variable -- as in not known in advance, so you can't put in a fixed number of parameters in a function call.

E: The versions with an 'e' at the end let you additionally pass an array of char* that are a set of strings added to the spawned processes environment before the exec'ed program launches. Yet another way of passing parameters, really.

P: The versions with 'p' in there use the environment variable PATH to search for the executable file named to execute. The versions without the 'p' require an absolute or relative file path to be prepended to the filename of the executable if it is not in the current working directory.

简单来讲,exec家族可以分为两个阵营,execl和execv,后缀l代表参数是挨个传送,后缀v代表参数是通过字符串数组传送。

而execl和execv各自又可以分为两个阵营,execl可以分为execle和execlp,execv可以分为execve,execvp以及execvpe。

后缀p代表是否支持PATH环境变量搜索程序,后缀e代表是否输入环境变量,当然pe和可以一起用的,典型的例子就是execvpe函数。

下面,我写了一个带有命令注入的C语言的例子,用于说明一般场景下命令注入漏洞是什么样的。

C演示代码cmdi.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    if (argc < 2) {
        printf("usage: %s param1 [param2 ...[param n]]\n", argv[0]);
        return -1;
    }

    char cmdbuf[128] = {0};
    snprintf(cmdbuf, sizeof(cmdbuf), "ls %s", argv[1]);

    puts("call system function:");
    system(cmdbuf);

    puts("\ncall popen function:");
    FILE *fp = popen(cmdbuf, "r");
    char readbuf[2048] = {0};
    fread(readbuf, 1, 2048, fp);
    puts(readbuf);
    pclose(fp);

    int pid = fork();
    if (pid > 0) {
        sleep(1);
    } else if (pid == 0) {
        puts("call execve function:");
        execl("/bin/sh", "sh", "-c", cmdbuf, NULL);
    }
    return 0;    
}

验证C命令注入

首先编译cmdi.c,然后命令行执行./cmdi ‘./; id’, 注意一点,参数是需要加单引号,或者双引号的,否则注入的命令 ;id 会被当前的shell解析,而不是传给cmd当作参数。

root@kali:~/Desktop/cmdi# gcc cmdi.c -o cmdi
root@kali:~/Desktop/cmdi# ./cmdi './; id'
call system function:
cmdi  cmdi.c  cmdi.class  cmdi.java  cmdi.py
uid=0(root) gid=0(root) groups=0(root)

call popen function:
cmdi
cmdi.c
cmdi.class
cmdi.java
cmdi.py
uid=0(root) gid=0(root) groups=0(root)

call execve function:
cmdi  cmdi.c  cmdi.class  cmdi.java  cmdi.py
uid=0(root) gid=0(root) groups=0(root)

需要注意的是,exec家族函数,一般情况下是不太会有命令注入漏洞的,除非执行的程序本身存在漏洞,但是有个特殊情况是执行的程序是解释器,比如sh,python,perl等,是可以通过参数注入达到命令注入的效果。

上述例子中,最后一段代码就是一个典型的例子,如果代码是这么写的话,还是会存在命令注入的。

execl("/bin/sh", "sh", "-c", cmdbuf, NULL);
或者
execve("/bin/sh", argv, NULL);

反之,如果是下面这么调用,除非ls程序本身存在漏洞,否则通过参数注入命令,是不会被解析执行的。

execve("/bin/ls", argv, NULL);

Java

Java执行系统命令有两个方式,ProcessBuilder和Runtime exec。

这两种执行命令方式基本使用情况如下

ProcessBuilder builder = new ProcessBuilder(cmdList);
builder.redirectErrorStream(true);
Process process = builder.start();
Runtime.getRuntime().exec(cmdList);

下面来看看存在命令注入的Java演示代码。

Java演示代码cmdi.java

import java.io.*;

public class cmdi {
    public static void main(String[] args) {
        if (args.length < 1) {
            System.out.println("usage: java cmdi param");
            return;
        }

        String[] cmdList = new String[]{"sh", "-c", "ls -al " + args[0]};
        ProcessBuilder builder = new ProcessBuilder(cmdList);
        builder.redirectErrorStream(true);
        Process process;
        try {
            process = builder.start();
        } catch (IOException e){
            return;
        }

        BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
        String line;
        try {
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (Exception e) {
            return;
        }

        try {
            process = Runtime.getRuntime().exec(cmdList);
        } catch (Exception e) {
            return;
        }
        reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
        try {
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (Exception e) {
            return;
        }
    }
}

通过javac把java编译为字节码class文件,通过java运行class文件,在参数中构造命令注入,发现注入的命令被执行了。

和C语言的例子类似,传递给java程序的参数在shell命令行中也需要单双引号,这里用了双引号,C语言的例子中用了单引号,都是可行的。

如果不加双引号,在shell中执行java cmdi ./; id, 会被shell以为是先执行java cmdi ./命令, 接下来再执行id命令,这样就失去了验证命令注入的本意。

验证Java命令注入

root@kali:~/Desktop/cmdi# javac cmdi.java
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
root@kali:~/Desktop/cmdi# 
root@kali:~/Desktop/cmdi# java cmdi "./; id"
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
total 44
drwxr-xr-x 2 root root  4096 Apr 23 23:15 .
drwxr-xr-x 5 root root  4096 Apr 11 00:51 ..
-rwxr-xr-x 1 root root 17040 Apr 23 23:02 cmdi
-rw-r--r-- 1 root root   754 Apr 23 23:02 cmdi.c
-rw-r--r-- 1 root root  1921 Apr 23 23:38 cmdi.class
-rw-r--r-- 1 root root  1070 Apr 23 23:38 cmdi.java
-rw-r--r-- 1 root root   302 Apr 11 05:14 cmdi.py
uid=0(root) gid=0(root) groups=0(root)
total 44
drwxr-xr-x 2 root root  4096 Apr 23 23:15 .
drwxr-xr-x 5 root root  4096 Apr 11 00:51 ..
-rwxr-xr-x 1 root root 17040 Apr 23 23:02 cmdi
-rw-r--r-- 1 root root   754 Apr 23 23:02 cmdi.c
-rw-r--r-- 1 root root  1921 Apr 23 23:38 cmdi.class
-rw-r--r-- 1 root root  1070 Apr 23 23:38 cmdi.java
-rw-r--r-- 1 root root   302 Apr 11 05:14 cmdi.py
uid=0(root) gid=0(root) groups=0(root)

看上面有漏洞的例子,ProcessBuilder的参数中String[] cmdList = new String[]{“sh”, “-c”, “ls -al ” + args[0]};

只有注入的字符串是拼接在一起,并作为第三个参数才能注入成功,如果是第四个或其他参数,即使有用户输入,也不会被当作命令执行。

更多关于Java的命令注入可查看参考材料[6]

PHP

PHP执行系统命令的函数有system, exec, passthru, proc_open, shell_exec, popen, pcntl_exec, 反引号 “。
这些函数的定义如下[7],[8]

string system ( string $command [, int &$return_var ] )
string exec ( string $command [, array &$output [, int &$return_var ]] )
void passthru (string command, int &return_var)
resource proc_open ( string $cmd , array $descriptorspec , array &$pipes [, string $cwd [, array $env [, array $other_options ]]] )
string shell_exec (string command)
resource popen ( string $command , string $mode )
void pcntl_exec ( string $path [, array $args [, array $envs ]] )

PHP supports one execution operator: backticks (``).PHP will attempt to execute the contents of the backticks as a shell command; the output will be returned.

PHP演示代码cmdi.php

<?php
	$line = fread(STDIN, 1024);
	$cmd = "ls -a ".$line;

	
	echo "Call system \n";
	system($cmd);

	echo "\n\nCall exec\n";
	exec($cmd, $ret);
	print_r($ret);

	echo "\n\nCall passthru\n";
	passthru($cmd);

	echo "\n\nCall proc_open\n";
	$descriptorspec = array(
		0 => array("pipe", "r"),  // stdin is a pipe that the child will read from
		1 => array("pipe", "w"),  // stdout is a pipe that the child will write to
		2 => array("file", "/tmp/error-output.txt", "a") // stderr is a file to write to
	);
	$process = proc_open($cmd, $descriptorspec, $pipes);
	if (is_resource($process)) {
		echo stream_get_contents($pipes[1]);
	}

	echo "\n\nCall shell_exec\n";
	$ret = shell_exec($cmd);
	echo $ret;

	echo "\n\nCall popen\n";
	$ret = popen($cmd, "r");
	$msg = fread($ret, 2048);
	echo $msg;
	$msg = fread($ret, 2048);
	echo $msg;

	echo "\n\nCall ` `\n";
	$ret = `$cmd`;
	echo $ret;

	echo "\n\nCall pcntl_exec\n";
	$args = array("-c", $cmd);
	pcntl_exec("/bin/sh", $args);

验证PHP命令注入

root@kali:~/Desktop/cmdi# php cmdi.php 
./;id
Call system 
.
..
cmdi_C
cmdi_C.c
cmdi.class
cmdi.java
cmdi.php
uid=0(root) gid=0(root) groups=0(root)


Call exec
Array
(
    [0] => .
    [1] => ..
    [2] => cmdi_C
    [3] => cmdi_C.c
    [4] => cmdi.class
    [5] => cmdi.java
    [6] => cmdi.php
    [7] => uid=0(root) gid=0(root) groups=0(root)
)


Call passthru
.
..
cmdi_C
cmdi_C.c
cmdi.class
cmdi.java
cmdi.php
uid=0(root) gid=0(root) groups=0(root)


Call proc_open
.
..
cmdi_C
cmdi_C.c
cmdi.class
cmdi.java
cmdi.php
uid=0(root) gid=0(root) groups=0(root)


Call shell_exec
.
..
cmdi_C
cmdi_C.c
cmdi.class
cmdi.java
cmdi.php
uid=0(root) gid=0(root) groups=0(root)


Call popen
.
..
cmdi_C
cmdi_C.c
cmdi.class
cmdi.java
cmdi.php
uid=0(root) gid=0(root) groups=0(root)


Call ` `
.
..
cmdi_C
cmdi_C.c
cmdi.class
cmdi.java
cmdi.php
uid=0(root) gid=0(root) groups=0(root)


Call pcntl_exec
.  ..  cmdi_C  cmdi_C.c  cmdi.class  cmdi.java	cmdi.php
uid=0(root) gid=0(root) groups=0(root)

有一点要注意,在调用popen的时候,因为有命令注入,所有要多次调用fread把管道的数据读出来,不然会报错,这导致不会那么巧合刚好带漏洞的代码也多次读取。所以个人感觉popen出现命令注入的可能性比其他函数稍微小一些。

Python

python语言支持执行系统命令的模块有好多个,比较常用的有os,commands,subprocess,另外还有很多如pty,shlex,sh,plumbum,pexpect,fabric,envoy等等,太多支持命令执行的模块会导致一个潜在的问题,那就是做静态代码分析,是否能监控到使用了不再上述范围的模块,该模块又能够执行系统命令。

能够执行系统命令的模块链接信息如下[5]

os: https://docs.python.org/3.5/library/os.html
commands: https://docs.python.org/2/library/commands.html
subprocess: https://docs.python.org/3.5/library/subprocess.html
shlex: https://docs.python.org/3/library/shlex.html
sh: https://amoffat.github.io/sh/
plumbum: https://plumbum.readthedocs.io/en/latest/
pexpect: https://pexpect.readthedocs.io/en/stable/
fabric: http://www.fabfile.org/
envoy: https://github.com/kennethreitz/envoy

常见的能够执行系统命令的函数如下[4]

os.system()
os.popen()
os.posix_spawn*()
os.spawn*()
subprocess.run()
subprocess.Popen()
subprocess.call()
subprocess.check_call()
subprocess.check_output()
subprocess.getstatusoutput()
subprocess.getoutput()
commands.getoutput()
commands.getstatusoutput()
pty.spawn()
...

注意,commands模块在python3中已经被弃用。

能够执行系统命令的函数定义[9]

os.system(command)
os.popen(cmd, mode='r', buffering=-1)

subprocess.run(args, *, stdin=None, input=None, stdout=None, stderr=None, capture_output=False, shell=False, cwd=None, timeout=None, check=False, encoding=None, errors=None, text=None, env=None, universal_newlines=None, **other_popen_kwargs)

class subprocess.Popen(args, bufsize=-1, executable=None, stdin=None, stdout=None, stderr=None, preexec_fn=None, close_fds=True, shell=False, cwd=None, env=None, universal_newlines=None, startupinfo=None, creationflags=0, restore_signals=True, start_new_session=False, pass_fds=(), *, group=None, extra_groups=None, user=None, umask=-1, encoding=None, errors=None, text=None)

subprocess.call(args, *, stdin=None, stdout=None, stderr=None, shell=False, cwd=None, timeout=None, **other_popen_kwargs)

subprocess.check_call(args, *, stdin=None, stdout=None, stderr=None, shell=False, cwd=None, timeout=None, **other_popen_kwargs)

subprocess.check_output(args, *, stdin=None, stderr=None, shell=False, cwd=None, encoding=None, errors=None, universal_newlines=None, timeout=None, text=None, **other_popen_kwargs)

subprocess.getstatusoutput(cmd)

subprocess.getoutput(cmd)

pty.spawn(argv[, master_read[, stdin_read]])

...

Python演示代码cmdi.py

import os
import subprocess
import pty
import sys

if len(sys.argv) < 2:
    print("usage: python3 cmdi.py param1 [...]")
    exit(1)

cmd = "ls -a " + sys.argv[1]

print("Call os.system:")
os.system(cmd)

print("\nCall os.popen:")
ret = os.popen(cmd)
print(ret.read())

print("\nCall subprocess.Popen:")
p = subprocess.Popen(["/bin/sh", "-c", cmd], stdout=subprocess.PIPE)
output, _ = p.communicate()
print(output.decode())

print("\nCall subprocess.call:")
subprocess.call([cmd], shell=True)

print("\nCall pty.spawn:")
pty.spawn(["/bin/sh", "-c", cmd])

验证Python命令注入

root@kali:~/Desktop/cmdi# python3 cmdi.py "./;id"
Call os.system:
.  ..  a.out  cmdi_C  cmdi_C.c	cmdi.class  cmdi.java  cmdi.php  cmdi.py
uid=0(root) gid=0(root) groups=0(root)

Call os.popen:
.
..
a.out
cmdi_C
cmdi_C.c
cmdi.class
cmdi.java
cmdi.php
cmdi.py
uid=0(root) gid=0(root) groups=0(root)


Call subprocess.Popen:
.
..
a.out
cmdi_C
cmdi_C.c
cmdi.class
cmdi.java
cmdi.php
cmdi.py
uid=0(root) gid=0(root) groups=0(root)


Call subprocess.call:
.  ..  a.out  cmdi_C  cmdi_C.c	cmdi.class  cmdi.java  cmdi.php  cmdi.py
uid=0(root) gid=0(root) groups=0(root)

Call pty.spawn:
.  ..  a.out  cmdi_C  cmdi_C.c	cmdi.class  cmdi.java  cmdi.php  cmdi.py
uid=0(root) gid=0(root) groups=0(root)

Python要特别关注的问题还是上面提到的,有太多模块支持命令执行。该如何做到全面监控,防止意外命令注入漏洞存在,是个需要认真思考的问题。

Python命令注入的补充说明

执行命令的函数,无论是system系还是exec系,如果是调用解释器,那么要注意,比如sh -c ls -al ./这样的形式,解释器只会执行ls命令,而不会带后面的参数。见下面code1,exe1。

除非命令是用引号包含如sh -c “ls -al ./”, 或者sh -c ‘ls -al ./’,才会执行完整的命令。这是sh -c命令的特点。见下面code2,exe2。

另外,有些函数调用形式上有点类似system系函数,但实际是内部是对exec系函数的封装。

code1

import subprocess
import sys

if len(sys.argv) < 2:
    exit(1)

cmd = sys.argv[1]
p = subprocess.Popen(["sh -c ls -al " + cmd], stdout=subprocess.PIPE, shell=True)
output, _ = p.communicate()
print(output.decode())

exe1

root@kali:~/Desktop/cmdi# python3 cmdi.py '`whoami`'
cmdi
cmdi.c
cmdi.class
cmdi.java
cmdi.py

code2

import subprocess
import sys

if len(sys.argv) < 2:
    exit(1)

cmd = sys.argv[1]
p = subprocess.Popen(["sh -c \"ls -al " + cmd + "\""], stdout=subprocess.PIPE, shell=True)
output, _ = p.communicate()
print(output.decode())

exe2

root@kali:~/Desktop/cmdi# python3 cmdi.py '`whoami`'
ls: cannot access 'root': No such file or directory

Python的shell=True

在Python中,有些命令执行函数有一个参数是shell,这个参数可以配置True和False。比如subprocess.Popen, subprocess.call等函数。

这里shell参数False和True的区别在于,shell=True参数表示Popen会在shell中去执行list中的第一个元素,当shell=False时,subprocess.call只接受数组变量作为命令,并将list的第一个元素作为可执行程序,剩下的全部作为该程序的参数。

上面的两个例子code1,code2中,当shell=False是无法被执行的,当shell=False时,subprocess.Popen会把列表的第一个元素当成可执行程序,也就是把”sh -c ls -al …”这一串字符串当作一个程序,系统当然是找不到这个程序的,所以无法找执行成功。

而当shell=True时,Popen会把[“sh -c ls -al ” + cmd]这个list的第一个元素放到shell中执行,所以最终得到的结果把执行sh进程,参数分别是 -c ls -al cmd,回到上文提到的,这里执行的结果是仅执行ls,后面的参数无效。如果要让后面的参数有效,应该在-c之后,用单引号或者双引号把参数引起来。

在shell=True时,subprocess.Popen就是system系函数,如下代码所示:

import subprocess
import sys

if len(sys.argv) < 2:
    exit(1)

cmd = sys.argv[1]
p = subprocess.Popen(["ls -al " + cmd], stdout=subprocess.PIPE, shell=True)
output, _ = p.communicate()
print(output.decode())

执行结果

root@kali:~/Desktop/cmdi# python3 cmdi.py "; id"
total 44
drwxr-xr-x 2 root root  4096 Apr 24 00:41 .
drwxr-xr-x 5 root root  4096 Apr 11 00:51 ..
-rwxr-xr-x 1 root root 17040 Apr 23 23:02 cmdi
-rw-r--r-- 1 root root   754 Apr 23 23:02 cmdi.c
-rw-r--r-- 1 root root  1921 Apr 23 23:38 cmdi.class
-rw-r--r-- 1 root root  1253 Apr 23 23:38 cmdi.java
-rw-r--r-- 1 root root   300 Apr 24 00:41 cmdi.py
uid=0(root) gid=0(root) groups=0(root)

在shell=False时,比如subprocess.Popen([“ls”, “-al”, cmd], shell=False),结果会执行ls -al …(cmd)…, 但是这种情况,除非ls有漏洞,否则不存在注入可能性。此时subprocess.Popen就是exec系函数。如下代码所示:

import subprocess
import sys

if len(sys.argv) < 2:
    exit(1)

cmd = sys.argv[1]
p = subprocess.Popen(["ls", "-al", cmd], stdout=subprocess.PIPE, shell=False)
output, _ = p.communicate()
print(output.decode())

执行结果

root@kali:~/Desktop/cmdi# python3 cmdi.py "; id"
ls: cannot access '; id': No such file or directory

又回到我重复提到的情况,如果exec函数执行解释器,还是可能存在命令注入的。如下代码所示:

root@kali:~/Desktop/cmdi# cat cmdi.py
import subprocess
import sys

if len(sys.argv) < 2:
    exit(1)

cmd = sys.argv[1]
p = subprocess.Popen(["sh", "-c", "ls -al " + cmd], stdout=subprocess.PIPE, shell=False)
output, _ = p.communicate()
print(output.decode())

执行结果

root@kali:~/Desktop/cmdi# python3 cmdi.py "; id"
total 44
drwxr-xr-x 2 root root  4096 Apr 24 00:47 .
drwxr-xr-x 5 root root  4096 Apr 11 00:51 ..
-rwxr-xr-x 1 root root 17040 Apr 23 23:02 cmdi
-rw-r--r-- 1 root root   754 Apr 23 23:02 cmdi.c
-rw-r--r-- 1 root root  1921 Apr 23 23:38 cmdi.class
-rw-r--r-- 1 root root  1253 Apr 23 23:38 cmdi.java
-rw-r--r-- 1 root root   223 Apr 24 00:47 cmdi.py
uid=0(root) gid=0(root) groups=0(root)

Go

Go语言目前我查到的大多都只说exec.Command这种方式执行命令。Go还有一种方式执行命令syscall.Exec,下面例子将介绍这两种方式。[10]

Go演示代码cmdi_GO.go

package main

import (
	"fmt"
	"bytes"
	"os"
	"os/exec"
	"syscall"
)

func main() {
	if len(os.Args) < 2 {
		fmt.Printf("usage: cmdi_GO param1")
		os.Exit(1)
	}
	command := exec.Command("sh", "-c", "ls -al " + os.Args[1])
	out := bytes.Buffer{}
	command.Stdout = &out

	command.Run()
	fmt.Println(out.String())

	args := []string{"sh", "-c", "ls -a " + os.Args[1]}
	syscall.Exec("/bin/sh", args, os.Environ())
}

验证Go命令注入

root@kali:~/Desktop/cmdi# go build -o cmdi_GO cmdi_GO.go
root@kali:~/Desktop/cmdi# ./cmdi_GO "./;id"
total 2368
drwxr-xr-x 2 root root    4096 Apr  9 03:00 .
drwxr-xr-x 6 root root    4096 Apr  7 23:14 ..
-rw-r--r-- 1 root root       0 Apr  8 22:16 a.out
-rwxr-xr-x 1 root root   17048 Apr  8 02:01 cmdi_C
-rw-r--r-- 1 root root     673 Apr  8 00:41 cmdi_C.c
-rwxrwxrwx 1 root root    1961 Apr  8 07:53 cmdi.class
-rwxr-xr-x 1 root root 2369716 Apr  9 03:00 cmdi_GO
-rw-r--r-- 1 root root     463 Apr  9 03:00 cmdi_GO.go
-rwxrwxrwx 1 root root    1134 Apr  8 07:53 cmdi.java
-rw-r--r-- 1 root root     980 Apr  8 09:39 cmdi.php
-rwxrwxrwx 1 root root     584 Apr  8 23:09 cmdi.py
uid=0(root) gid=0(root) groups=0(root)

.  ..  a.out  cmdi_C  cmdi_C.c	cmdi.class  cmdi_GO  cmdi_GO.go  cmdi.java  cmdi.php  cmdi.py
uid=0(root) gid=0(root) groups=0(root)

RUST

RUST算是比较小众的语言了,笔者之所以把RUST加进来是考虑到RUST的发展趋势以及内存安全的特性,未来可能成为底层主流语言。

RUST语言,查到的信息是std::process模块Command类支持执行系统命令,下面是针对这个方法的演示。[11],[12]

Rust演示代码cmdi_RS.rs

use std::process::Command;
use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();
    let mut cmd = "ls -a ".to_string();
    cmd += &args[1];
    let process = Command::new("sh")
            .arg("-c")
            .arg(&cmd)
            .output()
            .expect("failed to execute process");
    let s = match std::str::from_utf8(&process.stdout) {
        Ok(v) => v,
        Err(e) => panic!("Invalid UTF-8 sequence: {}", e),
    };
    println!("{}", s);
}

验证Rust命令注入

root@kali:~/Desktop/cmdi# rustc cmdi_RS.rs -o cmdi_RS
root@kali:~/Desktop/cmdi# ./cmdi_RS './; id'
.
..
cmdi
cmdi.c
cmdi.class
cmdi.java
cmdi.py
cmdi_RS
cmdi_RS.rs
uid=0(root) gid=0(root) groups=0(root)

漏洞代码总结

我们可以把命令执行的函数分为两个系列,分别是system系,和exec系。而每个系列又可以分为执行解释器和执行非解释器。解释器(如bash, python, perl等)后可以执行命令。

这样就有四种可能性

通过system系函数执行解释器

通过system系函数执行非解释器

通过exec系函数执行解释器

通过exec系函数执行非解释器

system系函数

system系函数比如C语言的system函数,Python语言的os.system函数等,这些函数接收一个完整的命令字符串。在Python中shell=True的情况和直接调用os.system函数是类似的。这种情况下,无论是否执行解释器,如果没有做适当的过滤,命令拼接并执行过程,都有可能存在命令注入。

exec系函数

通过exec系函数执行解释器 –> 存在命令注入可能性较大

通过exec系函数执行非解释器 –> 存在命令注入可能性较小

来源:freebuf.com 2021-06-06 17:47:46 by: nobodyshome

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论