☰
用Python扩展Burp代理:自定义安全扫描器实战
2026/10/19 6:06:30 网站建设 项目流程

《Python黑帽子》这本书记得是前几年完整读了一遍的,前面的笔记依次写到了环境准备、网络基础、数据包嗅探和Web攻击,这次是第五篇:扩展Burp代理。说句实话,这本书前面章节在教你怎么用Python实现各种网络工具,但到了Burp扩展这一章,才真正感觉打开了新世界——Burp Suite本来就是个拦截代理,再挂上Python脚本,它就从"手动看包改包"变成了"按你的规则自动找问题"的自动化平台。这一章适合已经用Burp做过基本Web测试、想摆脱重复劳动的人,也适合想搞清楚安全测试工具插件原理的人。

我先把结论放这儿:如果只是想给Burp装现成插件,这一章可看可不看;但如果想给自己定制规则、让代理流量按特定逻辑被检查、被改写,这一章的思路基本就是所有Python类Burp扩展的地基。

1. 这一章到底在做什么:从代理到扫描器

1.1 先说我为什么单独把这一章拎出来做笔记

《Python黑帽子》前面几章的主线其实很一致:用Socket、Scapy这些东西从零写网络工具。等写到Web攻击时,作者突然把手里最重的武器拿出来了——Burp Suite。Burp本身是Java写的图形化代理工具,功能已经很完整了,为什么还要"扩展"?因为你在实际测试里总会遇到一些特别琐碎、特别重复的检查,比如每个响应都要看有没有X-Frame-Options头、每个Cookie都要确认有没有HttpOnly,手动翻几百个包做这种事,体力消耗极大且容易漏。

扩展Burp代理的本质,就是让Python脚本挂进Burp的插件体系,在流量经过代理时执行你的自定义检查。这一章教的是最核心的路子:用Jython加载Python代码,让Python对象直接实现Burp的Java接口,然后注册成扫描器、监听器等角色。我读完以后的直观感受是:Burp像个壳子,扩展才让这壳子长出你自己的手脚。

1.2 Burp扩展给了我们什么能力

这一章把Burp Extender的能力拆成了几类,我按自己的理解顺序列一下:

  • 注册扫描检查(IScannerCheck):Burp做被动扫描、主动扫描时,会把每个请求/响应丢给你写的方法过一遍,适合做安全检查、漏洞匹配。
  • 监听代理流量(IHttpListener):每一条HTTP请求或响应经过代理时都会触发你的代码,适合改头、改参数、加标记。
  • 调用Burp的工具函数(IExtensionHelpers):解析请求、解析响应、拼接报文,这些零活儿不用自己造轮子。
  • 输出与交互(IBurpExtenderCallbacks):往Burp的Output面板打日志、向Scanner面板提交自己发现的问题,甚至控制UI。

这四类能力拼起来以后,你的扩展就不再是一个"只读的过滤器",而是一个有判断、有输出、能改报文的完整自动化节点。作者在书里选的切入点是第一类,即写一个自定义扫描器,这也是我认为最适合入门的一个方向:它对流量影响最小,逻辑也最直观。

1.3 为什么是Jython而不是纯Python

第一次接触这一章时,我也很自然地问:为什么不用系统里的Python 3直接写?原因在于Burp的扩展API是Java接口,想让Python代码被Java虚拟机调用,最直接的办法是让Python跑在JVM里,也就是Jython。Jython至今还是Python 2.7语法,所以这一章所有例子都是Python 2的写法,这算是读这本书最需要适应的一个点。

作者选Jython而不是用Java写扩展,理由很简单:Python写起来快,字符串处理和逻辑表达都更灵活,而且Jython里可以直接importJava类、调用Java方法,写出来的代码比纯Java扩展短一大截。放到今天的语境里,如果你想用Python 3写Burp扩展,也不是完全没路,但要走额外的IPC通道或者用其他桥接方案,复杂度高很多。所以这一章的Jython路线,依然是当前最省事、最贴近官方支持的方式。读的时候别纠结"Python 2都淘汰了",把它当成一种运行在JVM里的方言就好。

2. 核心接口拆解:Burp把哪些把手留给了我们

2.1 要从哪个类开始:IBurpExtender

Burp对外暴露了一堆接口,但每个Python扩展都必须有一个入口类,实现IBurpExtender。Burp加载扩展时,会先在你的代码里找有没有实现这个接口的类,然后调用它的registerExtenderCallbacks方法。这个方法会把一个IBurpExtenderCallbacks对象传给你,这个对象就是你和Burp之间的"总线"。

我在书里印象很深的一个观点是:所有准备工作都挤在registerExtenderCallbacks里完成。比如拿callbacks.getHelpers()得到辅助工具对象,用callbacks.setExtensionName()给扩展起名字,用callbacks.registerScannerCheck(self)把当前对象注册成扫描器,甚至从callbacks.getStdout()拿一个输出流用于打印日志。这些操作都发生在注册阶段,之后Burp才会把流量方法往你这里送。

我自己的习惯是,在这个方法里把需要持久使用的对象都存成实例属性,比如self._callbacks、self._helpers、self._stdout。这样后面doPassiveScan这些方法执行时,不用再到处找引用,代码会干净很多。

2.2 扫描器入口:IScannerCheck

IScannerCheck是书中重点用的接口,它有三个方法需要实现:

  • doPassiveScan(baseRequestResponse):被动扫描入口。Burp会把代理流量里的每个请求/响应对传进来,我们不需要主动发请求,只需要检查内容,然后返回问题列表或者None。
  • doActiveScan(baseRequestResponse, insertionPoint):主动扫描入口。Burp会向你提供可插入payload的位置,你需要自己构造测试请求并发送,这会产生额外流量,所以使用条件更严格。
  • consolidateDuplicateIssues(existingIssue, newIssue):问题去重。当Burp发现新问题和已有问题相似时,会调用这个方法让你决定它们是不是同一个,返回0表示相同,返回-1表示不同。

实现IScannerCheck后,调用callbacks.registerScannerCheck(self)把它登记上去,Burp就会在对应时机自动调用。这一章的示例就是从doPassiveScan开始的,我也建议所有新手从这里起步,因为被动扫描不修改请求、不主动发包,安全系数高,容易观察效果。

2.3 请求与响应的包装:IHttpRequestResponse

在Burp的扩展API里,你几乎不会直接拿到"请求报文"和"响应报文"两个孤零零的字节数组,Burp会用一个IHttpRequestResponse对象把两者打包在一起,再加上主机、端口、协议、URL这些上下文信息。这个设计很顺手,因为你在处理一个请求时,通常需要同时知道"它是发给谁的"和"对方回了什么"。

在代码里最常用的几个方法是:

  • getRequest():返回原始请求报文,类型是Java的byte[]。
  • getResponse():返回原始响应报文,也是byte[]。
  • getHttpService():返回服务信息,包括主机名、端口、协议。
  • getUrl():经过辅助解析后可以拿到的URL对象。

实际写扩展时,我们很少直接解析这些字节数组,而是交给IExtensionHelpers来做。比如self._helpers.analyzeResponse(response)会返回一个IResponseInfo对象,里面已经有解析好的状态码、响应头列表;self._helpers.analyzeRequest(baseRequestResponse)会返回IRequestInfo,里面有URL、请求头列表等。这些辅助API省去了大量手工切割字符串的体力活。

2.4 结果的出口:IScanIssue

如果扩展判断某个响应有问题,就要生成一个IScanIssue对象返回给Burp。这个接口同样要在Python类里实现,它有十来个getter方法,包括getUrl()、getIssueName()、getSeverity()、getConfidence()、getIssueDetail()、getHttpMessages()等。Burp拿到这个对象后,会把它的信息显示在Scanner的Issues面板里。

写这个类时我踩过一个认知上的坑:IScanIssue不只是一个问题描述字符串,它必须真正实现接口里的每一个方法,哪怕你暂时不需要某些字段,也得返回None或合理默认值。Jython虽然在动态实现接口这件事上很宽容,但Burp在读取时如果拿到未实现的方法,行为会很奇怪。所以我会专门定义一个SecurityIssue类,把所有getter全部写完整,这样扩展运行起来才稳。

3. 动手实现:一个安全响应头检查扩展

3.1 环境准备:加载Jython扩展

写代码之前,先把运行环境配好。第一步是下载jython-standalone的jar包,选2.7.x的最新版就行,它把Jython标准库和Java支持都打包在一个文件里,Burp可以直接加载。第二步是打开Burp,进入Extender标签页,点击Extensions,再点Add。在弹出的窗口里,Extension Type选Python,然后在Extension Details里选中刚下载的jar包。最后在下面选一个.py文件作为扩展脚本,加载成功后会提示Extension loaded。

我最初犯的错是把Jython当成"运行时"装了,但没有在Burp的Extension Type里选成Python。Burp的扩展机制是这样:你选Java,它就把jar当纯Java扩展跑;选Python,Burp才会启动内置的Jython解释器去执行你的.py文件。这个细节很多人一上来就忽略,结果装了扩展不生效还找不到原因。

准备一个最简单的BurpExtender类来验证环境是个好主意,哪怕它什么都不做,只要注册成功并在Output面板打印一行文本,就说明Jython链路通了大半。

3.2 完整代码:安全响应头检查器

环境通了以后,就可以把书中的思路落成能跑的代码。下面这个例子是我按书里的框架改写的安全检查扩展,它会在被动扫描时检查每个响应,报告缺失的安全响应头,以及没有HttpOnly标志的Cookie。全部逻辑都在一个.py文件里。

from burp import IBurpExtender from burp import IScannerCheck from burp import IScanIssue from java.io import PrintWriter from java.util import ArrayList class SecurityIssue(IScanIssue): def __init__(self, httpRequestResponse, url, httpService, issueName, detail): self._message = httpRequestResponse self._url = url self._httpService = httpService self._issueName = issueName self._detail = detail def getUrl(self): return self._url def getIssueName(self): return self._issueName def getIssueType(self): return 0x80000000 def getSeverity(self): return "Medium" def getConfidence(self): return "Certain" def getIssueBackground(self): return None def getRemediationBackground(self): return None def getIssueDetail(self): return self._detail def getRemediationDetail(self): return None def getHttpMessages(self): return [self._message] def getHttpService(self): return self._httpService class BurpExtender(IBurpExtender, IScannerCheck): def registerExtenderCallbacks(self, callbacks): self._callbacks = callbacks self._helpers = callbacks.getHelpers() self._stdout = PrintWriter(callbacks.getStdout(), True) callbacks.setExtensionName("BHP Security Header Checker") callbacks.registerScannerCheck(self) self._stdout.println("[+] Security Header Checker is active") return def doActiveScan(self, baseRequestResponse, insertionPoint): return None def doPassiveScan(self, baseRequestResponse): response = baseRequestResponse.getResponse() if response is None: return None info = self._helpers.analyzeResponse(response) headers = info.getHeaders() issues = ArrayList() checks = { "X-Frame-Options": "Missing X-Frame-Options header. The page may be embedded in an iframe.", "X-XSS-Protection": "Missing X-XSS-Protection header. Older browsers may not block reflected XSS.", "Strict-Transport-Security": "Missing HSTS header. The site is not enforcing HTTPS by default." } for headerName in checks: found = False for h in headers: if h.lower().startswith(headerName.lower() + ":"): found = True break if not found: reqInfo = self._helpers.analyzeRequest(baseRequestResponse) issue = SecurityIssue( baseRequestResponse, reqInfo.getUrl(), baseRequestResponse.getHttpService(), "Missing Security Header: " + headerName, checks[headerName] ) issues.add(issue) for h in headers: if h.lower().startswith("set-cookie:"): if "httponly" not in h.lower(): reqInfo = self._helpers.analyzeRequest(baseRequestResponse) issue = SecurityIssue( baseRequestResponse, reqInfo.getUrl(), baseRequestResponse.getHttpService(), "Cookie without HttpOnly", "The Set-Cookie header does not contain the HttpOnly flag." ) issues.add(issue) if issues.size() > 0: return issues return None def consolidateDuplicateIssues(self, existingIssue, newIssue): return 0

不要急着复制粘贴,先把文件里的几处逻辑看清楚,然后把它加载进Burp测试一下。你可以找一个本地搭建的测试环境,或者你在做安全测试时自己的授权目标站点,访问任意页面,再到Scanner的Issues面板看有没有出现我们定义的问题。

3.3 逐段说代码:注册回调与被动扫描

BurpExtender类同时实现了IBurpExtender和IScannerCheck两个接口。IBurpExtender是入口,IScannerCheck是扫描行为,用多重继承把它们合到一起,这是书里很典型的一种写法,也是Jython最方便的地方——在Java里你要写两个类,这里一个类搞定。

registerExtenderCallbacks方法里,我先把callbacks、helpers和stdout都保存下来。PrintWriter(callbacks.getStdout(), True)这个用法是书里推荐的,第二个参数True表示自动刷新缓冲区,这样println的内容能立刻显示在Burp的Extender Output面板,而不是攒到退出才一起输出。调试时这个差异非常重要,不然你可能以为扩展没干活,其实它早就打了日志,只是缓冲没刷出来。

doPassiveScan的第一步是取响应报文。注意getResponse()返回的是字节数组,可能为None,所以必须先判空。对于HTTPS流量,Burp在代理解密后也会把明文响应传给我们,所以这个扩展同时覆盖HTTP和HTTPS是被动扫描的好处。分析响应用的是self._helpers.analyzeResponse(response),得到info.getHeaders()是一个按顺序排列的头部列表,里面既有状态行也有每个响应头。

3.4 逐段说代码:生成IScanIssue结果

接下来就是检查逻辑:我用一个字典定义了三类缺失头。为什么不用简单字符串列表?因为我想在处理时同时拿到"头名称"和"详细描述",字典在这里比两个独立列表更紧凑。判断时把头部转成小写进行比较,避免大小写带来的误判,这是一个比书里示例更稳的细节:HTTP头部字段本来就不区分大小写,有的服务器返回x-frame-options,有的返回X-FRAME-OPTIONS,一律转小写再判断才可靠。

生成问题对象时,我调用了self._helpers.analyzeRequest(baseRequestResponse)来拿URL。这里有个值得说的坑:不要直接去baseRequestResponse.getUrl(),老版本接口没有这个方法,即使新版有,用辅助方法去解析请求拿URL,在不同版本间兼容性更好。SecurityIssue的getIssueType我填了0x80000000,这是Burp预留的扩展自定义问题类型起始值,用它不会和Burp内置的问题类型ID冲突。

4. 扩展能力:从“只看”到“能改”

4.1 再挂一个IHttpListener修改流量

doPassiveScan只是在看,而且它有个特点:它不能修改原始请求或响应。如果想要改流量,比如给经过代理的每个请求追加一个自定义请求头,你需要实现IHttpListener接口,并注册成HTTP监听器。这是书中没有重点展开、但和"扩展代理"主题关系最密切的能力。

我在实际项目里做过一个很小的演示:给所有从Burp发出去的请求追加一个X-BHP-Test: 1头,方便后端确认请求确实经过了代理。核心代码结构如下:

from burp import IBurpExtender from burp import IHttpListener class BurpExtender(IBurpExtender, IHttpListener): def registerExtenderCallbacks(self, callbacks): self._callbacks = callbacks self._helpers = callbacks.getHelpers() callbacks.setExtensionName("BHP Request Modifier") callbacks.registerHttpListener(self) return def processHttpMessage(self, toolFlag, messageIsRequest, messageInfo): if not messageIsRequest: return request = messageInfo.getRequest() analyzedRequest = self._helpers.analyzeRequest(request) headers = analyzedRequest.getHeaders() headers.add("X-BHP-Test: 1") body = request[analyzedRequest.getBodyOffset():] newRequest = self._helpers.buildHttpMessage(headers, body) messageInfo.setRequest(newRequest)

processHttpMessage是每次HTTP消息经过时会调用的入口,messageIsRequest为True表示当前是请求,为False表示是响应。我只改请求,不改响应。这里的技巧是先用analyzeRequest(request)拿到请求头和请求体的分界位置getBodyOffset(),然后切片取出请求体,再把头和体通过buildHttpMessage重新拼成一个新请求,最后用setRequest覆盖原报文。整个过程相当于在代理链路里拆开信件、塞入一张纸条、重新封好。

4.2 和书中的其他章节怎么配合

这一章不是孤立的。书前面教的网络工具,比如自定义TCP代理、数据包嗅探,解决的是"流量到了Burp之前"的问题;这一章扩展Burp,解决的是"流量进了Burp之后"的自动化问题。我在读的时候刻意把它们串起来看:前面写脚本把某个端口的流量导到Burp监听端口,Burp再把经过的HTTP流量交给Python扩展做检查,这样一个"采集、中转、分析、报告"的完整链路就搭起来了。

后续如果你想做更有意思的事,比如对参数做模糊测试、对特定接口做漏洞规则匹配、在重放请求后自动对比响应差异,都可以在IScannerCheck的基础上扩展。你可以把它理解成插件化的“规则引擎”,规则由你自己编写,这是那些现成插件永远替代不了的地方。

5. 实测与调试:Jython环境下的坑

5.1 先在本地搭一个测试站点

扩展写好后,不要一上来就用真实站点测试,我自己的做法是在本地起一个极简HTTP服务。用系统Python一条命令就能搞定:

python3 -m http.server 8000

然后让Burp代理访问http://127.0.0.1:8000,也可以随手放一个带Cookie的响应头做验证。用这种方式测试,你能完全控制环境,而且绝对不涉及授权问题。等扩展在本地稳定运转了,再去授权测试场景里实际使用。

在本地测试时,我建议你故意构造几种边界情况:无响应头的普通页面、有部分安全头的页面、带Cookie但缺HttpOnly的页面。这样才能确认扩展的命中逻辑正确,而不是只靠一套碰巧能跑的数据侥幸过关。

5.2 常见问题速查表

我在做这类扩展开发时,遇到过几个高频问题,整理成了一张速查表,基本覆盖了初学者会碰到的所有坎:

问题现象常见原因处理办法
Burp加载时提示找不到burp模块没有在Extension Type中选择Python,或jar没有正确加载确认加载的是jython-standalone,扩展类型为Python
代码用了f-string语法报错Jython只支持Python 2.7语法改用format()或字符串拼接,不要用f-string
Output面板没有任何输出PrintWriter没开自动刷新,或日志写到了错误流PrintWriter(callbacks.getStdout(), True)并写英文日志
doPassiveScan报类型错误返回了Python list而不是Java List使用java.util.ArrayList保存问题列表
getUrl()方法不存在Burp API新旧版本差异改用self._helpers.analyzeRequest(...).getUrl()
修改请求后发出去的包头重复每次事件都向同一个请求追加同一个头先判断目标头是否已存在,存在则跳过

其中最后一条是我后来才补上的经验:如果你的监听器逻辑不够幂等,扩展一旦被重复触发,可能把同一个头追加好几遍。写修改型扩展时,第一件事就是检查目标头是否已经存在,这是连书里都没说清楚的细节。

5.3 调试技巧

Jython扩展的调试比较原始,没有常见IDE里那种断点点击体验。我得出的实用技巧是把self._stdout.println("[DEBUG] ...")当成主力工具,在所有可能出问题的节点都打印一行。打印内容要尽量具体,包括传入的URL、头部列表、准备返回的问题数量等。别图省事只打印一个"Enter"。

另一个技巧是分阶段验证:先让扩展什么都不做,只打印每个被调用的方法名,确认入口没问题;再逐步加上过滤和判断逻辑;最后才生成问题对象。每一步结果都完全可见,排查速度会比一次性写完再整体调试快很多。

6. 从读书到实战:几个使用建议

6.1 安全工作边界问题

聊这个话题时必须把边界说清楚:扩展Burp代理这类技术,本质是安全评估自动化,它服务于合法合规的渗透测试。所有实验都应该限定在自己搭建的测试环境、已经获得授权的内部系统,或者符合法律法规要求的安全研究项目中。

这一章里最容易越界的点是doActiveScan。被动扫描只是检查已有流量,主动扫描会向目标发送大量含payload的请求,这会直接改变目标系统的访问负载,也可能触发安全设备警报。不要在一个你没权限、或者授权范围不清晰的目标上跑这类功能。我在自己的实践里,除非书面授权明确说明可以执行主动扫描,否则一律只用被动检查。

6.2 性能与误报控制

开发扩展时,除了考虑"能不能查出来",还要考虑"扫描是否拖垮了代理"。被动扫描是每个请求都触发,如果你的代码里有耗时的网络请求、复杂的正则回溯、或者大对象复制,整个Burp代理都会被拖慢。一个实用的优化思路:把只依赖响应头的检查放在最前面,凡是需要做额外关联分析的逻辑往后放,让绝大多数请求快速结束。

误报控制也需要多花点心思。上面代码里的头部检查看起来简单,但真实网站会有各种情况,比如带X-Frame-Options的多个值、Set-Cookie后跟多个属性、大小写混合、头部重复出现等。我会在实现检查时尽量多记录响应头的原始形态,后续根据误报反馈再收紧规则。

6.3 还能扩展什么

这一章的骨架打通之后,能往什么方向延伸,思路一下就打开了。举几个我见过的实际扩展方向:定期检查代理流量中是否出现特定敏感路径、对比同一接口在不同参数下的响应差异、把所有请求参数提取出来做字典集、对接内部消息平台把扫描结果推送出去。它们的入口点各不相同,但底层都是这套Jython加Burp接口的组合。

最后说一个我个人很受益的小习惯:写Burp扩展时不要让所有逻辑挤在一个类里。这一章的例子短,怎么放都行,但扩展一旦复杂,Jython的调试会让人头大。我会把检查规则拆到单独的逻辑模块里,主入口类只负责注册和转发,这样扩展出错时定位会快很多。这个习惯,是我读完这一章以后在实际项目里摸索出来的,分享给你。

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

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

立即咨询