news 2026/10/20 20:59:26

CISCN2019 Dropbox复现:PHP反序列化POP链与任意文件读取

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CISCN2019 Dropbox复现:PHP反序列化POP链与任意文件读取

BUUCTF上复现的这道CISCN2019华北赛区Day1 Web1,题名叫Dropbox,属于典型的"看着像个网盘应用,实际考的是PHP反序列化"的题目。这题从任意文件读取开始,到源码审计,最后用POP链直接读flag,整个过程没有上传马、没有SQL注入,非常干净。如果你是刚把反序列化魔术方法搞明白、想找一道综合题练手,或者刷BUUCTF卡在这了,这篇可以当一条完整路线图来用。

我会按自己实操的顺序来写:先看功能、再找漏洞突破口、然后审计源码定位反序列化入口、构造Payload、最后整理复现过程中容易踩的坑。所有关键请求和源码片段都尽量给出,方便你直接照着复现。

1. 热身:先给Dropbox做个体检

1.1 这题到底在考什么

从题目名字和CISCN出题风格来看,Dropbox模拟的是一个云盘类Web应用,主要功能就是注册登录、上传文件、下载文件、预览文件。这类题目表面上功能越简单,背后藏的考点往往越阴间。

实际考点是两段式组合:

  • 任意文件下载漏洞,用来把网站源码拖出来。
  • PHP反序列化漏洞,已经存在或隐藏在某个入口中,从源码中审计出来之后构造一个利用链,直接读取服务端任意文件。

这里"直接读取任意文件"是关键——不需要RCE、不需要写马,一条反序列化链就能把flag打出来。原因也很现实:这个应用的功能里本身就有读文件、输出文件内容的操作(比如下载和预览),反序列化只是把这段操作从正常功能中"借"出来,挂到一个我们能控制的参数上。

1.2 先把环境启动,走一遍正常用户的全部操作

去BUUCTF把题目环境跑起来之后,我建议先别急着抓包,用浏览器老老实实走一遍正常流程,把整个应用的功能范围摸清楚。这道题我一开始入手时因为太心急,直接对着上传点一顿测试,浪费了不少时间。

正常流程是:

  1. 访问首页,看到一个注册/登录界面,先注册一个用户。
  2. 登录后进入网盘主界面,有上传文件的表单。
  3. 传一张正常的png/jpg图片,界面上出现文件列表。
  4. 点"下载"按钮,浏览器会保存文件,抓包看到类似GET /download.php?filename=xxx或者POST /down.php的请求。
  5. 点"预览"按钮,页面会直接把图片内容渲染出来,说明服务端有读取图片内容并输出的功能。

重点关注第4步和第5步。下载请求里的filename参数、以及预览功能背后对应的服务端文件读取逻辑,就是后面突破口所在。

整个过程里我习惯开着Burp Suite,把代理挂上,每个请求都过一遍,同时把响应里的Set-Cookie、跳转地址、静态资源路径记下来。比如如果你发现页面里有/?sign=、/api/user.php之类的路径,对你后面阅读源码时确定文件结构很有帮助。

2. 突破口是那个不起眼的下载功能

2.1 试出任意文件读取

等正常下载流程走完,把下载请求发到Burp的Repeater里,仔细看参数。一般情况下请求类似这样:

GET /download.php?filename=1.png HTTP/1.1 Host: target Cookie: PHPSESSID=xxx

看到filename参数,第一反应就应该是:这个参数会不会直接拼到文件路径里?试试目录穿越。

GET /download.php?filename=../class.php HTTP/1.1

如果响应里直接出现了PHP源码,那这道题的大门就开了。为什么能成?原因很简单,后端逻辑多半长这样:

$filename = $_GET['filename']; $path = 'uploads/' . $filename; $content = file_get_contents($path); header('Content-Type: application/octet-stream'); echo $content;

没有过滤、没有严格判断文件扩展名、没有强制限制在upload目录内,../就被系统当成正常路径解析了。这是典型的任意文件下载漏洞,虽然危害看起来没有RCE大,但在CTF里是源码泄露的黄金通道。

这里有个小经验:有些题目下载路径是拼接了upload/前缀的,所以../会先抵消掉前缀。如果一次穿越不成功,就多试几层../../../,或者干脆直接试绝对路径,比如/etc/passwd、/var/www/html/index.php。代码里写的是怎么拼接的,只有试出来才知道。

2.2 逐个把源码文件拖下来

一旦确认可以任意文件下载,下一步不是急着找flag,而是把整个应用的核心源码读一遍。这一步决定了后面反序列化入口能不能找到、Payload能不能构造出来。

我把常见要读的文件列一下,并标注优先级:

文件路径优先级作用
index.php高主页面逻辑,可能有反序列化入口或文件包含
class.php高类定义文件,反序列化题的核心就藏在这
download.php / down.php高确认任意文件读取代码逻辑
upload.php高了解上传校验规则,看要不要配合图片马
config.php中数据库配置、路径常量、flag位置线索
db.sqlite / database.db中SQLite数据库,flag经常被放在里面
login.php / register.php低确认是否还有SQL注入等其他入口

实际操作中我通常直接把请求改成这样,一个个试:

GET /download.php?filename=../download.php GET /download.php?filename=../index.php GET /download.php?filename=../class.php GET /download.php?filename=../config.php GET /download.php?filename=../db.sqlite GET /download.php?filename=../upload.php

读.php文件时,由于响应头是application/octet-stream,浏览器会直接当成附件下载,为了方便阅读,我习惯把Burp响应体复制到本地,或者直接在Repeater里按Raw看源码。.sqlite数据库文件也可以用这种方式拉下来,拿到本地用数据库工具打开,里面有时藏着flag或者提示信息。

有一个细节:如果下载路径是uploads/前缀加我们的参数,那../class.php实际读的是web根目录下的class.php;如果文件在uploads/里面,需要读../uploads/class.php。不同题目环境部署方式不一样,看到404或空白就换路径试试,不丢人。

3. 源码审计:找到一条能用的POP链

3.1 File类:既能上传下载,也能读文件

把class.php拖下来,仔细阅读。这道题的类设计一般是围绕文件操作展开的,核心会有File、User、Upload这几种命名。其中File类几乎就是为反序列化量身定做的。

类比一下:File类就像是一个带门禁的文件柜,正常使用时,有人负责把文件放进去(upload),有人负责把文件取出来展示(preview/download)。但如果这个文件柜的"取文件"动作停在一个魔法方法上,我们就可以通过反序列化来远程遥控它。

典型代码结构(整理后):

class File { public $filename; private $type; private $content; public function open() { $this->content = file_get_contents($this->filename); } public function close() { if ($this->content) { echo $this->content; } } }

注意这里close()方法会把文件内容直接echo出来。放到正常业务里,可能是关闭文件句柄前做个清理输出。但放到反序列化链里,这就是一个"任意文件读取+外带输出"的绝佳gadget。

3.2 User类:析构函数就是触发机关

再看另一个类,通常叫User或Member,类里有个__destruct魔术方法:

class User { public $db; public function __destruct() { $this->db->close(); } }

__destruct()是什么?理解成PHP垃圾回收机制里的"临终遗言"。当一个对象不再被引用、或者脚本执行结束时,会自动调用这个方法。这个方法里做的最危险的事,就是它会调用一个我们可控的对象的方法。

在这个例子里,$this->db完全可控,那么如果我把$db指向一个File对象,再给这个File对象的$filename赋值成目标文件路径,那么当User对象销毁时,它会自动执行$this->db->close(),也就是执行File::close(),于是目标文件内容就被echo出来了。

这条链可以简化成:

User::__destruct() --> $this->db->close() --> File::close() --> echo file_get_contents($this->filename)

整个POP链就这么短,没有任何复杂方法调用。它反映了一个很常见的出题思维:__destruct找存在危险方法调用的类,fileread这种输出型方法就是最经典的利用目标。

3.3 反序列化入口定位思路

源码读到这里,还不够。我们知道了可以利用的类,但总得有一个地方能让用户输入的数据被unserialize()执行。这个入口如果直接搜unserialize,还是能定位到的。

常见入口有这么几种:

  • 把序列化字符串写入Cookie,读取时unserialize(base64_decode($_COOKIE['user']))
  • POST参数直接传,比如$_POST['data'] = unserialize($data)
  • 数据库里取出数据后再反序列化(这种情况需要配合SQL注入,本道题一般不用)

我复现时是直接在源码里搜索unserialize关键字,找到类似下面这种片段:

if ($user = unserialize(base64_decode($_COOKIE['user']))) { // ... }

这种入口非常经典。很多CTFer平时调payload时习惯性地去测?data=O:...,结果发现没反应,就是因为入口根本不在GET参数,而在Cookie里。所以读源码的时候一定要把所有文件扫一遍,特别注意unserialize、serialize、$_COOKIE、$_POST['xxx']这几个关键字的组合。

3.4 为什么不需要RCE也能拿flag

读到这里可能有人会问:既然可以任意文件下载,那直接读/flag不就行了?还用费劲构造反序列化吗?

答案是否定的。任意文件下载的路径前提是我们能猜中文件路径,而flag经常不在web根目录、文件名也不是固定的,直接读大概率404或者被目录限制截断。而反序列化链里的File::close()执行的是file_get_contents(),它读文件的能力和下载功能是同一层级的,但还是那个问题:需要路径。不过,有了源码审计,我们可以从config.php、index.php里得知flag的存放路径,比如flag在根目录或者数据库的某个表里。

另外,如果flag在数据库文件db.sqlite里,我们同样可以用这条POP链把数据库文件内容读出来,拖到本地后用sqlite工具打开,翻表就能看到flag。这比猜路径要可靠得多。所以这道题的本质,就是把源码中已有的"任意文件读取"能力,通过反序列化链重新触发一遍,只不过路径由我们精确控制。

4. 实操:构造Payload的一整套动作

4.1 序列化格式避坑指南

PHP反序列化的Payload是字符串,我们手写时需要遵循PHP序列化格式。常见元素:

  • O:4:"User":1:{...}表示一个对象,类名长度4,类名User,有1个属性。
  • s:2:"db";O:4:"File":1:{...}表示一个字符串属性,长度为2。
  • i:1表示整数。
  • b:1表示布尔值。

初学很容易栽在private属性上。举个例子,File类里的$filename是public,序列化时直接写属性名就行:

s:8:"filename";s:9:"/flag.txt";

但如果某个属性是private,PHP序列化时属性名会变成\0类名\0属性名,长度也会变。比如\0File\0filename实际长度是13。手写payload时要用URL编码%00表示空字节。这里产生的问题非常经典:从网上复制writeup里的payload,粘贴到Burp或请求里时,%00被某些工具自动解码成空字节,或者被URL编码了一次,导致反序列化失败。

4.2 一条能用的完整Payload

根据我们审计到的类和入口类型,需要确定反序列化参数的提交方式。如果入口是Cookie,且源码里做了base64_decode,那么最终提交的Cookie值是Base64编码后的序列化字符串;如果入口是普通POST参数,那就直接提交序列化字符串,并做URL编码。

以Cookie入口为例,序列化Payload通常长这样:

O:4:"User":1:{s:2:"db";O:4:"File":1:{s:8:"filename";s:9:"/flag.txt";}}

把它Base64编码后作为Cookie的值提交。我自己复现时,为了不走弯路,先在本地用PHP脚本验证了一遍序列化字符串生成是否正确,再提交到靶机。

<?php class File { public $filename; } class User { public $db; } $f = new File(); $f->filename = '/flag.txt'; $u = new User(); $u->db = $f; echo serialize($u); // O:4:"User":1:{s:2:"db";O:4:"File":1:{s:8:"filename";s:9:"/flag.txt";}} ?>

把输出原样或Base64编码后提交,如果目标入口正确、flag路径正确,响应页面里就应该直接出现flag。

4.3 提交和验证:flag直接出现在响应里

我带一带提交操作细节。如果你拿到的入口是Cookie:

  1. 在Burp Repeater里修改请求,把Cookie值替换成上述序列化字符串的Base64编码。
  2. 发送请求,观察响应。
  3. 响应体中出现flag则结束;如果出现空白或报错,就检查三点:路径是否存在、URL编码是否正确、Cookie是否还带了其他必须字段。

如果你拿到的入口是POST参数:

POST /index.php HTTP/1.1 Content-Type: application/x-www-form-urlencoded data=O%3A4%3A%22User%22%3A1%3A%7Bs%3A2%3A%22db%22%3BO%3A4%3A%22File%22%3A1%3A%7Bs%3A8%3A%22filename%22%3Bs%3A9%3A%22%2Fflag.txt%22%3B%7D%7D

记得/要编码成%2F,或者直接用Burp的自动URL编码功能。

提交后能看到flag的响应,整个链路就通了。

这里补充一下flag路径问题。题目环境不同,flag可能在:

  • /flag.txt
  • /flag
  • /var/www/html/flag.txt
  • db.sqlite中某张表里,比如flag表或users表

我见过一个版本flag就在db.sqlite的某张表里。你用POP链先读config.php确定数据库路径,再读.sqlite文件回本地,用sqlite3命令行工具打开:

sqlite3 db.sqlite .tables SELECT * FROM flag;

这招能解决"flag不在根目录"的尴尬。

5. 复现过程中我踩过的坑

5.1 上传点死活传不上马,白折腾一场

第一次做这道题时,我习惯性地先把上传功能研究了一遍,生成图片马、改文件头、加GIF89a,各种折腾,最后发现完全没必要。这道题的利用链全程不经过上传点,flag是直接通过反序列化读出来的。上传校验再严格、文件类型检查再复杂,都影响不到最终利用。

所以做这类Web题要先明确一点:**上传只是应用的功能之一,不是每个功能都必须成为突破口。**优先从源码审计出发找危险点,比盲目测功能高效得多。

5.2 下载路径穿越时老是被路径前缀干扰

初次测filename=../class.php返回404,不要急着放弃。多数情况下下载代码是'uploads/' . $filename,一次../正好抵消uploads/,再往后还需要读上级目录。

我习惯把可能的相对路径都列出来:

测试路径实际解析路径
filename=../class.phpuploads/../class.php
filename=../../class.phpuploads/../../class.php
filename=....//class.php绕过某些过滤后的效果
filename=/var/www/html/class.php绝对路径

实际动手时一个个试,直到出现<?php开头的内容,然后再往下阅读其他文件。

5.3 反序列化Payload提交后页面空白

空白响应一般意味着反序列化过程中抛了异常,或者file_get_contents读取失败、echo没触发。排查顺序是:

  1. 检查属性名是否和源码里的类属性一致。如果源码里$filename是private,你的payload还用public写法,反序列化出来对象就没有正确赋值。
  2. 检查Payload里类名长度和真实类名长度是否一致,比如User长度是4,写成O:5:"User"就直接报错。
  3. 检查入口参数名和提交方式,Cookie入口需要Base64,POST入口需要URL编码,混用会导致解析失败。
  4. 检查close()方法里是否有判断条件,不是每个版本的File::close都会无条件echo文件内容。如果源码里显示if ($this->content)才输出,你可能还需要先调用open()之类的方法把content填上。真有这种版本时,POP链会变成User::__destruct -> db->close -> File::close,而close内部可能先取$this->content,所以需要额外构造。

5.4 不知道flag放哪,怎么快速定位

我总结了一套快速定位flag路径的顺序,能省不少时间:

  1. 读index.php、config.php,看有没有定义过FLAG常量或路径。
  2. 尝试读/flag.txt、/flag、/var/www/html/flag.txt。
  3. 读db.sqlite并用本地工具打开,搜索.tables和字段名里的flag。
  4. 如果题面提示了"flag in database"之类的信息,优先读数据库文件。

还有一个思路:利用任意文件下载读取/proc/self/environ或者web目录下的Dockerfile,可以通过运行信息判断镜像部署路径和flag位置。不过这道题用不上这么绕的方式,一般到第3步就出结果了。

5.5 PHP版本差异导致的序列化格式问题

BUUCTF不同环境的PHP版本会有差异。在PHP 7.x下,序列化public属性没有问题。如果在某些工具里生成了带\0的序列化字符串,提交前一定要保证空字节被正确传递。我建议所有Payload都在本地用同一个PHP环境先serialize()一次,拿到官方格式再提交。手写Payload虽然炫技,但在比赛和复现场景中没必要,稳定性优先。

最后一剑:这道题给我留下的东西

个人感受,这道题放在CISCN2019华北赛区Day1,其实难度不算高,但它把"任意文件下载->源码审计->反序列化->任意文件读取"这条链完整走了一遍,特别适合用来建立反序列化题型的整体解题框架。

我在实际复现时,最大的收获不是学会了构造一个简单的User->File链,而是明白了一个判断依据:**反序列化题不要先急着找RCE。**能用原业务里已有的文件读取能力读flag,就不需要费劲去拼system和RCE。很多情况下出题人故意留一条"温和"的利用链,就是为了降低难度、让你体会POP链的构建过程。

以后再做这类题,我的固定流程就是:先通过任意文件读取或者报错信息把源码摸清,找到危险方法(file_get_contents、eval、system、call_user_func),再看哪个魔法方法(__destruct、__wakeup、__toString)能触发它,最后顺着入口把Payload递进去。这条思路在大量题目里都能复用。

最后再分享一个小技巧:复现完这道题后,把那串payload和源码保存在本地,以后遇到类似的网盘、文件管理类题目,拿出来对比一下类结构和入口,经常能直接套用。CTF的反序列化题型虽然千变万化,但底层的类设计和利用模式是有迹可循的。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 20:58:31

LMDeploy实战:开源大模型量化、推理与OpenAI兼容服务部署

这段时间一直在折腾开源大模型的落地部署&#xff0c;试了不少推理框架&#xff0c;最后在业务环境里稳定跑下来的是 LMDeploy。如果你也在做类似的事情——想把 Qwen、Llama 这类开源模型接到自己的系统里&#xff0c;或者想在项目里做一个带 API 的聊天服务&#xff0c;那么这…

作者头像 李华
网站建设 2026/10/20 20:58:23

技能管理:如何让技能组合产生复利,而不是只做技能收集者

1. 先分清"技能"和"能力"——很多人努力错了方向我最早接触"skills"这个词&#xff0c;是在一份招聘要求里。当时觉得"技能"不就是会点什么吗&#xff1f;会写代码、会做设计、会讲外语&#xff0c;这就是技能。后来带过几个新人&…

作者头像 李华
网站建设 2026/10/20 20:58:17

快递查询小程序变现:CPS、流量主与同城业务联动方案

简介&#xff1a;2024快递平台系统独立版小程序源码&#xff0c;为面向快递代发代寄场景的完整同城寄件方案&#xff0c;特别适合拥有私域流量、想做快递CPS推广或广告流量主变现的运营者与开发者。整套源码为纯前端形态&#xff0c;已内置腾讯流量主广告组件、会员制度、分销返…

作者头像 李华
网站建设 2026/10/20 20:57:29

物体放置怎么做?双路图补全实现场景智能布局

物体放置听着像“找个位置放东西”&#xff0c;但在计算机视觉里&#xff0c;这是一个足够写一篇顶会论文的难题。ECCV 2022 那篇基于双路图补全的物体放置工作&#xff0c;我前前后后读了好几遍&#xff0c;又在自己项目里复现过一版&#xff0c;这篇文章就把它掰开揉碎聊聊&a…

作者头像 李华
网站建设 2026/10/20 20:57:09

告别裸指针:C++智能指针unique/shared/weak全解析

在我们自己的项目里&#xff0c;裸指针new出来的对象迟早会变成一块烫手山芋。早年间写C&#xff0c;最头疼的不是语法难&#xff0c;而是到了一天结束要一个个检查析构顺序&#xff1a;这个接口返回的指针到底归谁释放&#xff1f;那个缓存里的对象是不是已经被别处delete了&a…

作者头像 李华
网站建设 2026/10/20 20:55:57

VTI介质地震波数值模拟:MATLAB实现与各向异性正演避坑指南

简介&#xff1a;这份资源面向地震勘探与地球物理方向的学习者和研究者&#xff0c;提供一套基于Matlab的VTI介质地震波场数值模拟程序&#xff0c;用于分析各向异性介质中地震波的传播规律。VTI介质在垂直与平行层理方向上纵波速度存在差异&#xff0c;是实际地壳中常见的各向…

作者头像 李华