简介:burpFakeIP 是一款面向 Web 安全测试与渗透测试人员的 Burp Suite 扩展插件,核心用途是在请求过程中模拟不同源 IP,帮助测试者隐藏真实身份、模拟多用户来源,或验证目标站点基于 IP 的访问控制与地理限制策略。压缩包共 12 个文件,大小约 1.09MB,包含 1 个 Python 脚本、1 份 Markdown 说明、1 份 txt 文档和 9 张 png 截图;脚本承载插件主要逻辑,文档交代配置与使用方式,截图则直观呈现界面和测试效果。目前已有 413 人学习。对于从事授权渗透测试、隐私保护或多源访问模拟的安全从业者,这份资料可作为理解 Burp Suite 插件机制、参考 IP 伪装实现思路的实践素材,也便于结合截图和说明快速熟悉插件的功能边界,使用时需遵守法律法规并取得测试授权。
1. 一次靶场演练的意外:burpFakeIP 到底值不值得装进 Burp 插件栏
某次内部靶场演练,后台页面标着“仅内网可访问”,我在 Repeater 里手加了一行X-Forwarded-For: 127.0.0.1,管理员菜单直接就出来了。漏洞本身不高级,问题出在目标把“客户端随手能改的请求头”当成了“网络层的事实”。这类场景在 burp suite 插件生态里有个很直接的解法——burpFakeIP,github 上能拿到的这份源码包,干的事就一句话:把伪造 IP 的请求头按规则批量塞进每一个经过 Burp 的请求,省掉你一条条手改的功夫。
接下来讲清楚四件事:什么场景下伪造头才有意义、怎么在授权靶场里跑通第一次复现、限流/白名单/多级代理这三类判定链分别怎么打、以及五个让我白忙半天的坑。新手能照着把环境和验证跑起来,熟手可以直接跳到第 4 章的判定链拆解和第 5 章的排查表。
2. 先分清三种假 IP:应用层伪造才是 burpFakeIP 的战场
2.1 网络层、应用层、日志层,三条完全不同的路
刚接触 IP 伪造的人容易把三件事混成一锅:改 TCP 源地址、改 HTTP 头、改日志里记的 IP。这三条路的难度差着数量级,搞混了就会在错误的方向上耗掉一整天。
网络层伪造指的是让数据包的源 IP 不是自己的。这条路在真实环境里基本走不通:你得有中间链路的路由控制权,即便包发出去了,回程的 SYN-ACK 也回到那个伪造的地址上,三次握手根本建立不起来。除非你能在链路上做中间人,否则这条路的实战价值接近零。判断依据很简单——只要目标还要求你完成完整 TCP 握手,你就没法在网络层骗它。
应用层伪造才是 burpFakeIP 的主战场。HTTP 是无状态协议,代理链路为了让后端知道“最初的客户端是谁”,约定俗成用一堆自定义头来传递原始 IP:X-Forwarded-For、X-Real-IP、Client-IP、True-Client-IP、X-Client-IP、CF-Connecting-IP、X-Originating-IP、X-Remote-IP、X-Remote-Addr,以及 RFC 7239 定义的标准化写法Forwarded: for=1.2.3.4。这些头本质上就是普通请求头——只要中间没有代理把它们剥掉,客户端想写什么就写什么。
日志层伪造是副产品。很多应用把取到的 IP 写进审计日志、风控流水、登录记录,一旦上游采信了伪造的头,日志里留下的就是假地址。这一层本身不是攻击面,但它决定了事后能不能追溯,也决定了防守方复盘时看到的是真相还是幻觉。
注意:判断一个目标会不会采信请求头,唯一可靠的办法是实测响应差异,不是猜框架。同一套代码里,登录接口可能读真实源地址,后台鉴权却读
X-Forwarded-For,这种情况一点都不罕见。
2.2 三步判定法:先建基线,再单点注入,最后看差异
我一般不会一上来就把九个头部全塞进去。全塞的结果是响应变了你也不知道是哪个头起的作用,等于自己给自己制造黑匣子。稳妥的做法是三步:
第一步,建基线。用最干净的原始请求打三次,记录状态码、响应体长度、Location跳转目标、Set-Cookie有没有变化、以及服务端返回的时间戳。这一步的目的是拿到一个“什么都没改时长什么样”的参照物,后面所有差异都相对它来比。
第二步,单点注入。一次只加一个头,值统一用127.0.0.1。优先试X-Forwarded-For,其次X-Real-IP,再次Client-IP,最后才是那几个冷门的。多数框架的中间件只认前一到两个,按热度排序能省掉一半时间。
第三步,比对差异。关注的不是“页面有没有变”,而是这几个信号:状态码从 403 变 200、响应体长度突变、出现新的 Cookie、跳转目标从登录页变成首页、以及响应时间明显变短(说明绕过了某个鉴权中间件)。任何一个信号出现,就说明这个头进了判定链。
下面这段是把九个头挨个打一遍的最小脚本,只适合打自己的靶场,跑之前把TARGET换成你环境里的地址:
#!/bin/bash # 逐个替换伪造 IP 头,观察响应差异,仅用于自建靶场 TARGET="http://127.0.0.1:8080/admin" HEADERS=( "X-Forwarded-For: 127.0.0.1" "X-Real-IP: 127.0.0.1" "Client-IP: 127.0.0.1" "X-Client-IP: 127.0.0.1" "X-Originating-IP: 127.0.0.1" "X-Remote-IP: 127.0.0.1" "X-Remote-Addr: 127.0.0.1" "True-Client-IP: 127.0.0.1" "Forwarded: for=127.0.0.1;proto=http" ) # 先取基线:状态码 + 响应体字节数 BASE=$(curl -s -o /dev/null -w "%{http_code} %{size_download}" "$TARGET") echo "baseline -> $BASE" for h in "${HEADERS[@]}"; do # -H 只加一个头,其余保持默认,保证变量唯一 R=$(curl -s -o /dev/null -w "%{http_code} %{size_download}" -H "$h" "$TARGET") # 与基线不同的才打印,避免刷屏 [ "$R" != "$BASE" ] && echo "[DIFF] $h -> $R" done脚本里最关键的不是循环,是-o /dev/null -w这组参数:把响应体丢掉只留状态码和字节数,输出量从几十 KB 压到一行,几十个请求扫下来肉眼就能看完。[ "$R" != "$BASE" ]做的是差异过滤,只有和基线不一样的组合才会打出来。如果你发现所有头都没差异,别急着怀疑插件——先按第 2.3 节的办法确认目标到底有没有采信头部。
2.3 插件在 Burp 里的定位:它是“手动挡的放大器”,不是扫描器
搞清楚 burpFakeIP 是什么,比会用更重要。它不是漏扫,不会告诉你哪里有洞;它也不是自动化的绕过工具,不会自己判断该塞哪个头。它做的事只有一件:在你手工测试的过程中,把你本来要一条条手敲的请求头,按预设规则自动加到每一个流经 Burp 的请求上。
这个定位决定了它的正确用法。你应该是先在 Repeater 里手工确认了“某个头能影响判定”,然后再开插件,把这一条经验批量应用到几十上百个请求上——比如在 Intruder 里跑目录爆破时,每个请求都带上伪造头,避免因为“IP 变了”导致限流计数被重置,或者反过来,让每个请求看起来都来自不同 IP。顺序反了就会翻车:先开插件再手工测,你根本分不清响应变化是插件带来的还是你改的包带来的。
判断一个目标会不会采信头部,除了 2.2 的实测法,还有两个间接信号值得先看一眼。一是响应头里有没有Via、X-Cache、X-Served-By这类代理痕迹——有,说明链路上存在会处理转发头的中间件,采信概率明显更高。二是错误页面。有些框架在异常时会打印完整的请求头列表,翻一眼就知道后端有没有读取X-Forwarded-For,这比盲试九个头部省事得多。
3. 跑通第一次伪造:从 jar 加载到自建靶场验证的四步
3.1 加载方式:直接挂载 jar 还是本地构建源码
拿到burpFakeIP-master.zip之后先解压看一眼目录结构,这一步决定了你走哪条路。如果根目录里直接躺着.jar文件,最短路径就是用 Burp 的 Extender 挂载;如果只有src/和构建脚本,那就得自己编。
直接挂载的流程是固定的:打开 Burp,切到Extensions → Installed,点Add,Extension type 选Java,然后在 Extension file 里选中那个 jar。加载成功的标志是 Output 面板里出现一行 “extension loaded”,同时列表里多出一条带勾选的记录。加载失败最常见的三种报错:extension failed to load(多半是 jar 不完整或者下载被截断)、NoClassDefFoundError(依赖没打进去,得用带依赖的 fat jar)、UnsupportedClassVersionError(编译用的 JDK 比 Burp 自带的运行时新)。
编译源码的话,先看根目录有没有pom.xml或者build.gradle。有的话按项目自带的构建方式走一遍,注意一件事:编译时用的 JDK 版本不要高于 Burp 自身运行的 Java 版本,因为 Burp 用的是它自带的运行时,不是系统里那个。版本对不上就是上面那个UnsupportedClassVersionError,报错信息里会直接写 “class file version 61.0”,61 对应 Java 17,55 对应 Java 11,对一下就知道该降级还是升级。
提示:加载完插件别急着开规则。先在 Extender 的 Output 里确认没有红色堆栈,再去做第 3.3 节的靶场验证。带病运行只会把问题拖到后面,排查成本翻倍。
3.2 规则配置:注入哪几个头、用什么值、对哪些 host 生效
这类插件的配置项大同小异,核心就三组:注入哪些头、头部的值怎么生成、作用范围多大。下面的表是我一般会配的初始组合,可以直接抄,后面再按目标微调。
| 配置项 | 作用 | 建议初始值 | 调优依据 |
|---|---|---|---|
| 启用开关 | 全局控制是否注入 | 先关,测完再开 | 避免干扰手工基线 |
| 头部列表 | 决定注入哪些头 | X-Forwarded-For+X-Real-IP | 覆盖八成框架 |
| IP 取值方式 | 固定值 / 随机 / 网段 | 固定127.0.0.1 | 先验证有效性 |
| 网段范围 | 随机模式下取值区间 | 10.0.0.0/8 | 匹配目标内网假设 |
| 作用域名 | 只对哪些 host 生效 | 单个靶场域名 | 防止污染其他流量 |
| 是否覆盖原值 | 已有同名头时替换还是追加 | 追加 | 保留链路真实性 |
“是否覆盖原值”这一项最容易被忽略,但它直接决定你在多级代理场景下能不能复现成功。追加和替换的差别,在 XFF 链的解析上就是两个完全不同的结果,第 4.3 节会展开。
配置完之后建议先在一个无害的接口上做一次冒烟:随便发个请求,去 Burp 的Proxy → HTTP history里看那条请求的 Raw 面板,确认头真的被加进去了。看不到就说明作用域名写错了,或者开关没生效,别往下走。
3.3 一个三十行的自建靶场,用来验证头到底生效没有
没有靶场就测伪造,等于蒙眼开车。下面这个 Flask 服务模拟的是最常见的错误写法——直接把X-Forwarded-For当成客户端 IP 来做白名单判断。它故意写错,就是为了让你能观察到“头一改,结果就变”的完整因果。
# vuln_lab.py —— 故意写错的靶场,仅用于本地验证 from flask import Flask, request app = Flask(__name__) # 后台允许访问的“内网”网段,模拟配置错误的白名单 ALLOWED_PREFIX = "10." def get_client_ip(): """错误示范:无条件信任 X-Forwarded-For 的第一个值""" xff = request.headers.get("X-Forwarded-For", "") if xff: # 只取第一个逗号前的部分,这正是伪造能生效的根源 return xff.split(",")[0].strip() return request.remote_addr or "0.0.0.0" @app.route("/admin") def admin(): ip = get_client_ip() if ip.startswith(ALLOWED_PREFIX): return f"ADMIN OK, your ip is {ip}", 200 return f"FORBIDDEN, your ip is {ip}", 403 if __name__ == "__main__": # 关掉 debug,避免堆栈干扰响应体长度比对 app.run(host="127.0.0.1", port=8080, debug=False)代码里有三个点值得盯住。get_client_ip()是漏洞的根:它没有任何“这个头是否来自可信代理”的判断,谁写就信谁。xff.split(",")[0]取第一个值,意味着你可以直接把10.1.2.3写进去,前面什么都不用拼。request.remote_addr作为兜底,只有在完全没有 XFF 头的时候才用得上——也就是说,正常从本机访问会返回127.0.0.1,触发 403。
跑起来之后,用 curl 打两次就够验证了:不带头发一次,应该看到FORBIDDEN, your ip is 127.0.0.1;带上-H "X-Forwarded-For: 10.1.2.3"再打一次,应该返回ADMIN OK, your ip is 10.1.2.3。两次结果的差异就是全部证据。
3.4 第一次验证:从 Repeater 到 Intruder 的观察点
靶场跑通之后,把 Burp 的代理挂上,用浏览器或者 curl 走127.0.0.1:8080。抓到/admin这个请求后右键Send to Repeater,先在插件关闭的状态下手工加一次头,确认在 Burp 里能得到和前面 curl 一致的结果。这一步是在校准工具,不是在测目标。
然后开插件,回到 Repeater 点 Send,看响应。这里有个细节:Repeater 里手动加的头和插件自动加的头会叠加,如果插件配置成“追加”模式,你会看到两个X-Forwarded-For行,或者一行里两个逗号分隔的值。第一次见到这个现象很容易以为插件坏了,其实是正常行为,具体怎么处理见第 5.2 节。
再往后是把验证规模放大。把请求丢进 Intruder,攻击类型选Sniper,Payload 位置选在请求行或者某个参数上,同时在插件里把 IP 取值方式设成“随机网段”。这时候观察的重点变了:不再是“能不能过”,而是“过了之后还能不能保持”。很多目标在第一次伪造时会放行,但会在响应里种一个带 IP 指纹的 Cookie,后续请求只要 IP 和 Cookie 对不上就降权。这种细节只有把请求量拉起来才看得见。
到这一步,工具算是真正跑起来了。接下来才是正题——三类不同的判定链,打法完全不一样。
4. 三类判定链复现:限流、白名单、多级代理各怎么打
4.1 场景一:基于 IP 的限流与验证码触发
限流是最容易验证、也最容易出成果的场景。逻辑很直白:服务端用一个计数器记录“某个 IP 在时间窗口内请求了多少次”,超了就返回 429 或者弹验证码。问题在于,这个“某个 IP”如果取自请求头,计数器就等于形同虚设——你每发一个请求换一个 IP,对方的计数永远停在 1。
靶场里复现这段很简单,给 3.3 的服务加一个按 IP 计数的装饰器:
# 追加到 vuln_lab.py,模拟基于头部 IP 的限流 from collections import defaultdict import time HITS = defaultdict(list) # ip -> [时间戳列表] WINDOW, LIMIT = 60, 5 # 60 秒内最多 5 次 def rate_limited(ip): now = time.time() # 只保留窗口内的记录,过期的直接丢弃 HITS[ip] = [t for t in HITS[ip] if now - t < WINDOW] if len(HITS[ip]) >= LIMIT: return True HITS[ip].append(now) return FalseHITS以 IP 为 key,WINDOW和LIMIT决定阈值。关键在调用处——如果传进去的ip来自get_client_ip(),那这个限流器就是死的。用同样的思路验证:不带头发六次,第六次会被拦;带上不断变化的X-Forwarded-For每发一次换一个值,六次全部通过。
这个场景在 Burp 里的标准玩法是 Intruder 的Pitchfork模式:一个 payload 位放业务参数(比如用户名列表),另一个 payload 位放 IP 列表,两列一一对应,每个请求配一个不同的伪造 IP。这样跑出来的结果最能说明问题——如果目标真的按 IP 限流,你会看到请求成功率从个位数一路拉到接近 100%。
需要留个心眼的是限流粒度。有些实现不只按 IP 计数,还叠加了账号、设备指纹、UA。这时候单纯换 IP 只能把成功率从 0 拉到 30% 左右,剩下 70% 卡在别的维度上。看到这种“部分生效”的现象别急着下结论说插件没用,先把 UA 和 Cookie 也一并轮换,再看曲线。
4.2 场景二:后台 IP 白名单的判定链
白名单场景比限流复杂,因为判定可能发生在三个完全不同的层次,而只有其中一层对头部伪造敏感。搞不清这一点,你会把大量时间浪费在注定无效的地方。
第一层是反向代理层的 ACL。Nginx 的allow/deny、Apache 的Require ip,看的都是 TCP 连接的真实源地址,也就是remote_addr。这一层完全不吃伪造头,因为它在解析 HTTP 之前就做完了判断。看到配置里写allow 10.0.0.0/8; deny all;,直接放弃这条路,换目标。
第二层是应用框架层的白名单。这是最常见也最有价值的战场。典型写法是在过滤器或者中间件里取 IP 然后比对网段。下面这段 Java 是错误写法和正确写法的对照:
// 错误写法:直接从请求头取值,任何人写什么就是什么 String ip = request.getHeader("X-Forwarded-For"); if (ip != null && ip.startsWith("10.")) { chain.doFilter(request, response); // 伪造头直接放行 return; } // 正确写法:只在“最近一跳是可信代理”时才采信头部 String remote = request.getRemoteAddr(); String ip; if (TRUSTED_PROXIES.contains(remote)) { // 从右往左取,跳过可信代理,第一个不可信的才是真实客户端 ip = parseFromRight(request.getHeader("X-Forwarded-For"), TRUSTED_PROXIES); } else { ip = remote; // 直连时忽略一切头部 }request.getRemoteAddr()拿到的是 TCP 层的地址,客户端改不了;TRUSTED_PROXIES.contains(remote)是整段逻辑的关口,只有确认“这一跳是我自己的代理”才往下走;parseFromRight是从右往左扫描,遇到第一个不在可信列表里的地址才停。三段缺一段,白名单就是纸糊的。
第三层是业务代码层的二次校验。有些系统在登录时校验一次 IP,后续每个请求再用 Session 里存的 IP 比对。这一层往往还叠加了 Session 绑定,单纯换 IP 反而会触发“异地登录”风控,把人踢下线。打这种目标要先确认绑定关系,别一上来就开插件狂发。
4.3 场景三:多级代理下真实 IP 的提取顺序
XFF 的拼接规则是这套东西里最容易翻车的地方,没有之一。约定是:每经过一层代理,代理把自己看到的上一跳地址追加到末尾。所以一个正常经过两层代理的请求,头会长这样:
X-Forwarded-For: 客户端真实IP, 第一层代理IP, 第二层代理IP注意最左边那个是客户端自己声明的——也就是说,你在最左边写什么都行,后面的代理只会往后追加,不会回头校验前面那段。这就是伪造能奏效的机制根源,也是很多解析代码写错的地方。
解析正确姿势是从右往左数:跳过所有你认识的可信代理,遇到的第一个不认识的地址,才是“最接近真实的客户端”。下面这段 Python 是这个逻辑的最小实现:
# parse_xff.py —— 从右往左剥离可信代理,取第一个不可信地址 TRUSTED = {"10.0.0.1", "10.0.0.2"} # 自家代理的地址列表 def real_ip(xff_header, remote_addr): if not xff_header: return remote_addr # 没有头部就只能信 TCP 源地址 # 拆成列表并去掉空白,顺序保持原样(左 -> 右) hops = [h.strip() for h in xff_header.split(",") if h.strip()] # 反转后从最近一跳开始往前找 for hop in reversed(hops): if hop not in TRUSTED: return hop # 第一个非可信地址即为客户端 return hops[0] if hops else remote_addr # 全可信时退化为最左值TRUSTED是白名单集合,必须由运维侧维护,不能从请求里读。reversed(hops)决定了扫描方向——如果写成正序遍历,你拿到的就是攻击者随便写的那个值。最后那行兜底看起来无害,实际是个坑:全链路都可信的情况下退化成最左值,等于又信了客户端声明,正确做法应该是返回remote_addr。
这个场景在 Burp 里的复现手法是 Intruder 的Cluster bomb,专门用来打 XFF 的拼接位置。一个 payload 位放“前置伪造值”(比如随机的内网地址),另一个位放“后缀可信代理地址”,组合起来就能测出目标是取最左、取最右,还是取某个固定位置。测出规律之后,插件的“是否覆盖原值”配置就该跟着调整——追加模式适合打取最左的实现,替换模式适合打取唯一值的实现。
4.4 复现记录模板:把“疑似生效”变成能复现的证据
伪造 IP 这类测试最大的问题是不可复现。今天过了,明天同样操作就 403,你自己都说不清是环境变了还是运气好。我的做法是每次验证都填一张固定格式的记录表,字段不多,但能逼着我把变量固定下来。
| 记录字段 | 内容示例 | 为什么要记 |
|---|---|---|
| 请求时间 | 精确到分钟 | 排除时间窗口类限流干扰 |
| 目标接口 | /admin、/api/login | 不同接口判定链不同 |
| 注入头部 | X-Forwarded-For | 单变量原则 |
| 头部值 | 10.1.2.3 | 网段匹配是关键 |
| 基线响应 | 403 / 62 字节 | 差异比对参照物 |
| 伪造后响应 | 200 / 1284 字节 | 唯一有效证据 |
| 是否带 Cookie | 带 / 不带 | 排除 Session 绑定影响 |
| 插件状态 | 开 / 关 | 区分手工与自动结果 |
这张表的实际价值在于排查。当某次结果和记录对不上时,逐行比对就能定位到是哪个变量变了——八成是 Cookie 没清、或者插件状态和上次不一样。血泪经验:我曾经因为浏览器缓存了一个旧 Session,连续三次复现结果不一致,最后发现问题根本不在 IP 上。
5. 排查清单:五个让我白忙半天的坑
5.1 坑一:头明明加上了,响应一个字节都没变
现象:在 HTTP history 里确认请求头确实被注入了,但响应状态码、长度、跳转目标全部和基线一致,反复换 IP 值也没用。
原因:最可能是目标的判定链根本不看头部。前面说过,Nginx 的allow/deny、云厂商的负载均衡 ACL、防火墙的源地址策略,全都在 HTTP 解析之前就完成判断,你在应用层怎么改都影响不到它们。第二种可能是目标虽然读了头部,但只读某一个特定的头,而你注入的正好不在列表里。
解决:先按 2.2 的三步法把九个头部挨个单点打一遍,确认有没有任何一个能引起差异。全都无差异,就去看响应头里有没有Server: nginx这类标识,结合 4.2 的三层模型判断是代理层还是应用层在拦。如果确认是代理层 ACL,直接换目标,这条路没有捷径。
5.2 坑二:Repeater 里好使,Proxy 里像是没开
现象:手工在 Repeater 加头能过,但浏览器正常访问走 Proxy 时,插件像是完全没生效,请求里干干净净。
原因:八成是作用域名配错了。这类插件通常有一个“只对匹配的 host 生效”的配置,如果填的是完整 URL 或者带了端口号,匹配就会失败。另一种情况是请求走了 HTTPS,而 Burp 没有正确解密——插件只能改它看得见的明文请求,看不见的自然改不了。
解决:先去Target → Scope里确认目标在范围内,再检查插件的作用域名是不是只填了主机名。HTTPS 的问题看 5.3。最后一种可能是插件挂在Proxy之外的处理链上,只对Repeater生效——翻一下插件的说明,确认它注册的是哪个扩展点。
5.3 坑三:HTTPS 站点里插件像是失灵的
现象:HTTP 站点一切正常,换成 HTTPS 之后请求头完全没变化,Burp 的 HTTP history 里能看到明文,但发出去的包是原始的。
原因:Burp 处理 HTTPS 靠的是自签证书,浏览器或客户端没有信任这个证书时,连接会直接失败或者被旁路,插件自然没有机会介入。还有一种情况是目标启用了证书固定(Certificate Pinning),客户端只认特定证书,Burp 的中间人证书会被拒绝。
解决:先确认 Burp 的 CA 证书已经装进系统的信任库,并且是在“受信任的根证书颁发机构”里,不是普通证书存储。移动端还要额外确认代理设置生效。遇到证书固定的客户端,这条路基本走不通,换用其他测试手段,别在这上面耗时间。
5.4 坑四:IP 伪造成功了,风控还是把我拦下
现象:响应确实从 403 变成了 200,但连续发十几个请求之后,账号被锁、验证码弹出、或者直接返回“操作过于频繁”。
原因:现代风控很少只看 IP 一个维度。设备指纹、UA、Cookie、请求间隔、行为序列、账号历史,随便拎一个出来都能独立触发拦截。IP 只是最外层的一道,过了这道门里面还有好几道。
解决:把变量拆开单独测。先固定 IP 只换 UA,看会不会触发;再固定 UA 只换 IP,看会不会触发。找出真正起作用的那个维度,再决定插件该怎么配。如果确认是多维联合判定,单靠伪造 IP 收益有限,得配合请求间隔随机化一起做。
5.5 坑五:装上插件之后 Burp 开始卡,内存一路涨
现象:导入 jar 之后 Burp 明显变慢,Proxy history 滚动卡顿,任务管理器里 Java 进程内存持续上涨不回落。
原因:插件在每一个请求上做字符串处理,如果规则里配了随机 IP 生成,加上 history 默认不限制条数,几千条请求累积下来内存就吃满了。另一种可能是插件本身有 bug,在某个特定请求格式上死循环。
解决:先把Proxy → Options → History里的记录上限调小,比如从默认值降到 1000 条。再检查插件配置里有没有“随机生成”这类每请求都要计算的选项,测试阶段可以改成固定值。如果卡顿依旧,先把插件关掉对比一次,确认问题确实来自它。这类问题没什么优雅解法,就是控制数据量。
6. 收在验证上:用三行配置查清 IP 信任链断在哪一环
工具用到最后,真正有价值的不是“怎么伪造”,而是“怎么判断一个系统会不会被骗”。我把这套判断压缩成三个动作,每次接手一个带 IP 判定的目标,先做这三件事,比直接开插件省一半时间。
第一件事,看响应头里的代理痕迹。有Via、X-Cache、X-Served-By说明链路上存在会处理转发头的中间件,采信概率高;一个都没有的裸服务,大概率直接读remote_addr。第二件事,看错误页面。故意触发一个 500,很多框架会把完整请求头打印出来,那一屏就是最好的情报。第三件事,在靶场里跑一遍 4.2 的三层模型,确认目标的判定发生在代理层还是应用层——如果是代理层 ACL,立刻放弃,别浪费两小时。
判断依据落实到代码上,其实就是一段可以抄走的检查逻辑:
# trust_check.py —— 判断一条请求的 IP 判定是否可被头部影响 def judge(request): remote = request.remote_addr # TCP 源地址,客户端改不了 xff = request.headers.get("X-Forwarded-For") if xff and remote in TRUSTED_PROXIES: # 只有在可信代理由后才采信 return parse_from_right(xff) # 从右往左剥离 return remote # 其余情况一律信 TCP 源这三行里的每一行都对应一个曾经的翻车现场。第一行,我见过太多人直接用request.headers.get("X-Forwarded-For")兜底,等于把判定权交给了客户端;第二行,TRUSTED_PROXIES是硬编码的运维事实,不是从请求里推导出来的,一旦允许从请求里读,整个信任模型就塌了;第三行是从右往左,方向反了就等于白写。
回过头看,我在伪造 IP 这件事上最大的收获不是学会了加几个头,而是养成一个习惯:每遇到一个“根据来源做判断”的逻辑,先问一句这个来源是谁给的。从那以后我每次做这类测试,都会强制走一遍“基线 — 单点 — 差异”的三步,哪怕目标看起来再简单也不跳过,因为跳过的那次往往就是白忙一下午的那次。希望这份 burpFakeIP 的复现笔记,能帮你在自己的靶场里把这条链路完整跑通一遍。
本文还有配套的精品资源,点击获取