CTFshow WEB12:利用X-Forwarded-For伪造本地访问绕过登录
CTFshow的WEB入门系列我是一路刷过来的。前面几题基本都是“送分题”右键看源码、view-source抓注释、打开robots.txt、用Burp看响应头……做到WEB12的时候我卡了差不多一个小时。登录框摆在那里密码怎么试都不对后来才发现问题根本不在密码上——这是一道“本地管理员”的访问控制题考点是请求头伪造。这篇文章就围绕WEB12这道题把完整WP、背后的原理、以及我在调试过程中踩过的坑全部写出来。不论你是刚接触CTF、准备刷CTFshow的新手还是已经在做渗透测试、想补一下Web基础的人这篇内容都可以直接参考。题目本身的难度不大但涉及的知识点非常典型理解了它后面再遇到“只允许本机访问”“IP白名单”“内网访问”这类场景你就能一眼看穿出题人的意图。1. 拿到题目先别急着登录先做信息收集1.1 题目长什么样登录页和它的“潜台词”打开WEB12的靶场地址映入眼帘的就是一个非常朴素的登录页面一个用户名输入框、一个密码输入框、一个登录按钮。没有验证码没有注册入口也没有“忘记密码”的链接。这种页面在CTF题里很常见但信息量往往比看上去大得多。我一开始的想法很简单先试几个常见弱口令比如admin/admin、admin/123456、admin/password。结果无一例外全部登录失败页面也没有给出“用户名或密码错误”的明确提示而是很含糊地显示“非本地访问”或者“Access Denied”。如果你是在本地练习可能显示的是醒目的红色警告。反正意思就是服务端知道你的请求不是从“本机”发出来的。这个提示非常关键。它意味着出题人已经拿到了一个“身份判断”的依据。很多新手到这里就卡住了因为不管怎么换密码提示都一样根本不知道往哪个方向用力。我的建议是凡是遇到登录框先别急着爆破密码先做信息收集——看源码、看响应头、看有没有隐藏文件。很多时候flag不是藏在密码后面而是藏在“某个可修改的请求参数”后面。1.2 源码、robots.txt与响应头里的线索CTFshow的WEB入门系列出题人习惯把提示放在极容易找到但又容易被忽略的地方。WEB12的页面虽然简单但按F12打开开发者工具审查元素之后我在页面的HTML注释里看到了一行!-- only localhost can access --翻译过来就是“只允许本机访问”。这个线索一出来整道题的思路就清晰了一大半这不是一道爆破题而是一道“身份伪造”题。你需要让你的请求在服务器看来来自本机127.0.0.1 / localhost。接着我顺手看了下响应头没有发现其他特别的字段。不过我也建议你在实际做题时把curl -I 题目地址输出仔细看一遍确认没有遗漏。响应头里经常藏flag或者提示比如flag: xxx、X-Powered-By: nginx或者Server: Werkzeug这些信息能帮你判断后端语言和中间件类型对接下来的操作有很大帮助。另外CTFshow平台有一部分题目会在/robots.txt里放提示WEB12我测试下来没有用到但你做题时养成顺手访问的习惯绝对没坏处。信息收集做得越充分后面的路就越顺。我把信息收集的检查清单放在这里查源码注释HTML、JS查响应头Server、Set-Cookie、藏flag的头部访问/robots.txt访问常见备份文件/.git/、/.bak、/www.zip看Cookie是否有可疑字段2. 核心考点服务器是怎么判断“本地访问”的2.1 一个容易被忽略的常识REMOTE_ADDR 与代理头大多数Web应用判断客户端来源最直接的方式是看TCP连接里的IP也就是服务器语言里的REMOTE_ADDRPHP中为$_SERVER[REMOTE_ADDR]。这个值来自socket层正常情况下客户端很难直接伪造因为你发的包最终还是要从你的真实IP到达服务器。但很多网站并非只有一台服务器前面可能还有CDN、Nginx反向代理、负载均衡器。当请求经过这些中间层时后端拿到的REMOTE_ADDR其实是上一级代理的IP而不是真实用户的IP。为了把真实客户端IP传给后端代理服务器会在请求头里加X-Forwarded-For简称XFF格式类似X-Forwarded-For: 203.0.113.5如果有多级代理则是一串IP比如203.0.113.5, 10.0.0.1, 198.51.100.7。问题就出在这里许多开发者图省事直接用$_SERVER[HTTP_X_FORWARDED_FOR]或者request.headers.get(X-Forwarded-For)来判断客户端IP而没有去核实这个头是不是代理服务器加的。客户端完全可以自己手动加一个X-Forwarded-For: 127.0.0.1塞进请求里后端如果没有做校验就会把这个值当成真实来源。生活里有个很形象的类比你去驿站取包裹工作人员看的是快递单上写的“寄件地址”而不是核实你实际从哪个城市来。如果你在快递单上手写一个“发件地北京”驿站就默认你来自北京。X-Forwarded-For就是这张可以随手改写的快递单。2.2 为什么 XFF 会被信任开发者的偷懒和误区你可能觉得“这么明显的伪造漏洞开发者怎么会犯”但现实中这个情况非常普遍。尤其在开发阶段开发者在本机测试通过Nginx转发后端拿到的REMOTE_ADDR是127.0.0.1导致登录、日志统计等功能出现偏差。为了快速解决“无法获取用户真实IP”的问题开发者在代码里直接读取X-Forwarded-For并通过Nginx配置让代理服务器主动设置这个头。这样功能是通了安全性却埋下了雷。CTF题目里考这个点本质上就是在考“你是否清楚代理头可以被客户端控制”。不只X-Forwarded-For还有X-Real-IP、Client-IP、X-Forwarded-Host、X-Original-URL等头都可能被开发者错误信任。WEB12的题目设计就是模拟了这种错误信任后端判断“是否来自本机”时查的不是REMOTE_ADDR而是HTTP_X_FORWARDED_FOR。所以只要我们在请求里加上X-Forwarded-For: 127.0.0.1后端就会乖乖放行。2.3 常见IP来源头对照表为了便于你理解不同请求头的用途我把CTF题和真实渗透中经常出现的基础知识整理成了一张表请求头名称作用是否建议在CTF中尝试X-Forwarded-For记录真实客户端IP标准代理头是最常用X-Real-IPNginx常用于传递真实IP是Client-IP部分Web框架或旧代码使用的头是特别是题目有提示时X-Forwarded-Host原始请求的Host是涉及域名判断时X-Original-URL原始URL常被错误用于URL鉴权视题目而定Referer来源页面地址涉及防盗链或伪造来源时看到没有这些头在HTTP协议里并没有被禁止客户端修改普通浏览器可以通过Burp Suite、ModHeader这类工具任意添加。理解了这一点整个第二章节的“为什么”就通了服务器用可伪造的头做判断就是这道题的突破点。3. 完整WP从弱口令到改包绕过3.1 工具准备Burp Suite 代理配置改请求头首选工具是Burp Suite。我用的是Community版功能完全够用。这里说下配置流程下载安装Burp Suite Community。打开后默认会启动Proxy监听监听地址是127.0.0.1:8080。浏览器需要配置代理指向这个地址。Firefox可以用插件FoxyProxyChrome可以用SwitchyOmega或者直接在系统代理里设置。如果你要做HTTPS题目的抓包还需要从Burp导出CA证书并导入浏览器信任列表。WEB12是HTTP还是HTTPS题目我没太在意但稳妥起见证书这一步建议提前准备好后面做HTTPS题也能用。配置完成后用浏览器访问题目地址再回到Burp的Proxy - HTTP history面板就能看到每一条请求记录。注意观察登录时的POST请求体这是后面改包的基础。Burp的关键快捷键也顺手记一下CtrlR把当前请求发送到Repeater方便手动修改重放。CtrlShiftB快速定位Burp的拦截开关。在Repeater中修改完后点Send就能看到响应结果。3.2 第一阶段弱口令探测与失败我先用Burp抓包把请求发送到Repeater后开始挨个试密码。请求体大概是这样的POST /login.php HTTP/1.1 Host: 题目服务器地址 Content-Type: application/x-www-form-urlencoded Cookie: PHPSESSIDxxx usernameadminpassword123456我连续测试了admin/123456、admin/admin888、admin/flag等组合响应每次都是那句“非本地访问”或者直接空响应。这个阶段其实没浪费太多时间因为我意识到题目已经通过注释给出了“only localhost”的明确提示那么重点就应该放在“如何让服务器认为我是localhost”而不是穷举密码。不过这里有个经验值得分享即使题目已经指向IP伪造也别忘了在改包之后重新尝试弱口令。因为很多题目的逻辑是“先验证IP再验证账号密码”顺序是取决于后端代码的。有些题的flag甚至不需要密码只要IP对了就直接返回flag。我之后会把这两层逻辑都验证一遍。3.3 第二阶段抓包分析请求结构在Burp的HTTP history里我找到了登录的POST请求。除了常规的username和password字段外没有隐藏参数。请求头也都是浏览器自动带的没有看到什么自定义字段。这里要注意抓包之后一定要看原始报文而不是只看浏览器开发者工具里的“网络”标签。因为浏览器开发者工具展示的是浏览器发出的请求Burp展示的是你实际发到服务器的内容理论上两者一致但如果浏览器有插件、代理或缓存干扰只有Burp才能还原真实传输数据。更重要的是你要在Burp里改包并观察服务器响应这是浏览器开发者工具做不到的。我把请求发送到Repeater后尝试了一组基线测试直接改POST数据为usernameadminpasswordtest观察返回。随后又试了一个对请求头中XFF的测试不加任何头响应是拒绝加了X-Forwarded-For: 127.0.0.1响应变成了“密码错误”或者直接跳转。这个变化基本可以确定服务器对IP来源的判断是靠XFF完成的。3.4 第三阶段添加 X-Forwarded-For 成功绕过在Repeater中我在HTTP请求头的空白位置加了这行X-Forwarded-For: 127.0.0.1然后重新发送。响应不再是“非本地访问”而是显示“密码错误”或者直接返回了一个包含flag的页面。如果只是显示“密码错误”说明IP校验已经过了接下来正常跑账号密码即可。我用admin admin再试一次直接登录成功flag出现在后台页面或响应体里。还有一种情况是页面提示“请使用管理员身份登录”此时可以试试把username字段改成admin或者检查Cookie里有没有roleuser之类的字段改成roleadmin再发一次。CTFshow这类入门题大概率不会卡在这层但你在实战中要养成多尝试的习惯。成功之后我的请求大概长这样POST /login.php HTTP/1.1 Host: 题目服务器地址 X-Forwarded-For: 127.0.0.1 Content-Type: application/x-www-form-urlencoded Cookie: PHPSESSIDxxx usernameadminpasswordadmin响应里能看到flag{...}的字符串复制下来提交到CTFshow平台即可得分。3.5 快速验证用浏览器插件改包如果你还不想装Burp或者卡在代理配置上也有一个更轻量级的办法使用浏览器扩展ModHeaderChrome/Edge都有。安装后在扩展里添加一个自定义请求头字段名值X-Forwarded-For127.0.0.1保存后直接刷新题目页面再次提交登录服务器就会认为请求来自本地。这个方法的原理和Burp完全一样只是把改包提前到了浏览器层。它的缺点是只能改“会被浏览器发送的请求头”如果题目需要你临时删除某些头或修改CookieModHeader就不够灵活了还是Burp全面。我的建议是日常做题尽量用Burp因为CTF题目越往后越复杂你需要精确控制请求的每一个字节。ModHeader更适合应急验证比如验证“加个XFF会不会绕过”这种假设。3.6 拿到flag之后的复盘题目做完我习惯性做了三步复盘第一步确认攻击链页面本身逻辑薄弱处是“用可伪造的请求头做IP来源判断”我的利用方式是添加XFF。第二步如果去掉XFF能否通过答案是题目返回“非本地访问”说明存在明确的校验逻辑。第三步思考如何修复这个漏洞后端不能直接信任XFF应该从REMOTE_ADDR出发或者只信任已知代理IP设置的XFF例如在Nginx层用set_real_ip_from做白名单对源IP做严格校验。这种复盘方法比单纯刷题重要得多。CTF的本质不是“拿到flag就完事”而是通过一个场景理解一个真实漏洞。我见过太多人刷了几百道题问到原理却说不出所以然最后在真实环境里仍然不知道怎么利用和防护那就等于白刷了。4. 实战中容易踩的坑和排查思路4.1 改了XFF还是提示非本地访问这个问题我印象很深因为我在其他题目里碰到过不止一次。常见原因大概有四种第一种你改的位置不对。有些新手在浏览器开发者工具里改“请求标头”发现根本改不了因为浏览器的CORS和预检策略不允许前端脚本随便添加受保护的请求头。你用Burp改包时也要确认修改的确实是HTTP请求头部分而不是把XFF写进了请求体。第二种服务器读取的是其他头。比如后端用的是X-Real-IP或Client-IP或者它按照“从右往左取第一个非127.0.0.1的IP”来判断。如果XFF不行就依次把上表里的头都加上试试比如X-Forwarded-For: 127.0.0.1和X-Real-IP: 127.0.0.1同时加。第三种IP格式不对。本地IP不只是127.0.0.1IPv6的本地地址是::1。有些后端判断的是“是否是本机”但代码写得不严谨只判断字符串是否等于127.0.0.1这种情况用127.0.0.1就行如果题目明确是IPv6环境你可以把::1也试一下。第四种题目的校验根本不在请求头上。例如它可能校验你当前会话Cookie里某个值的哈希或者校验你的User-Agent里有没有特定字符串。那你就需要重新做信息收集别死磕XFF。遇到“非本地访问”提示我的排查顺序永远是先审查源码和响应头再看题目有没有其他接口最后才是盲试请求头。4.2 登录成功后flag在哪看有时候改包成功页面也有响应但你可能会发现自己看到的不是flag而是一个没有明显文字的白页或者一个“登录成功”的提示。这种情况flag可能在响应头里Burp Repeater的Response Headers面板跳转后的新页面里Cookie里被编码的字段里base64或者URL编码需要解码我一般会先看响应头再看响应体最后才看Cookie。用Burp的Repeater发请求时响应信息是完整的比浏览器里看到的裸页面更可靠。如果响应体过长可以用Burp里的搜索功能找“flag”关键字比肉眼扫快得多。4.3 从WEB12延伸出去的考点WEB12只是请求头伪造的入门示例它后面还有不少变种。我简单列几个方向方便你继续深挖利用X-Original-URL绕过前端路由限制直接请求后台管理页面。利用X-Forwarded-Host欺骗服务器生成错误的绝对链接实现密码重置或缓存投毒。利用Referer字段伪造来源绕过图片防盗链或来源校验。在真实的SSRF服务端请求伪造漏洞里攻击者会利用服务器发起的请求携带内网IP来访问169.254.169.254等云元数据地址从而获取云服务器临时凭据。这类场景和WEB12的“本地访问”本质上是一个逻辑方向都是让服务端误以为请求来自可信任区域。如果你对后面这些感兴趣可以去刷CTFshow的进阶Web题也可以先自己做个小实验在本机的Nginx里配置一个只允许127.0.0.1访问的页面然后用Burp加XFF试试能不能绕过。自己搭一个环境亲手验证一遍比看十篇WP都管用。4.4 常见问题速查表我把做题过程中容易遇到的问题整理成了表格你可以直接收藏起来当索引现象可能原因排查方向加了XFF还是被拒绝后端信任的是其他头尝试X-Real-IP、Client-IP等浏览器控制台改不了请求头浏览器保护限制使用Burp或ModHeader登录成功但没有flagflag在响应头/Cookie中查看Burp完整响应提示“用户名或密码错误”IP校验已通过密码错误尝试弱口令或爆破字典响应里出现乱码内容编码问题检查响应头Content-Encoding尝试解码界面提示403权限校验在IP校验之后检查Cookie/Role字段尝试修改身份标识5. 写在最后刷CTF题的正确姿势WEB12这道题很快就做完了但我想多说几句。CTF刷题尤其是基础题最大的价值是帮你建立“出题人思维”。看到登录框你要想到的不只是“密码是什么”而是“服务器的判断依据是什么”。一个请求从浏览器发出到服务器接收中间有太多可以被操纵的部分URL、请求头、请求体、Cookie、Session、编码、时间、顺序。每一处都可能藏着考点。我自己踩过不少坑。刚开始刷题时我习惯拿到题目就先开字典爆破结果把时间都花在了无意义的暴力尝试上。后来学到的最重要的一件事是先停一下把你能看到的所有内容都检查一遍再决定下一步动作。WEB12的注释提示、响应文字的差异、不同请求头对响应的影响这些都是线索。做题不是猜谜而是观察和推理。如果你是在校学生或者刚入门安全我建议你把CTFshow的WEB入门1到20题认认真真刷完每一题做完后都写一个简短的WP。题目本身不难但写WP的过程会强迫你整理思路把“感觉能过”变成“我知道为什么能过”。等后面遇到真实漏洞时你会发现当年这些看似简单的题其实就是真实攻击链路的缩小版。最后再分享一个小技巧遇到登录类题目先把账号设为admin密码先随便填然后分别测试“正常发包”和“加了一个伪造头发包”两种响应。如果响应不一样说明身份判断逻辑里一定有可利用的点。这个观察法我后来在好几个靶场题目里都用上了属于性价比极高的试探手段。WEB12能拿下靠的正是这个思路。