一段时间没写公众号了,最近写了个 burpsuite 数据收集的插件,于是想出一篇从头编写一个 burpsuite 插件的教程。 这个插件的目的收集 burpsuite 请求中的数据,如请求中的子域名、文件名、目录名、参数名等,保存到数据库,然后根据出现的次数进行排序,出现次数多的排在前面,从而强化我们的字典。
插件效果演示 先来看看插件的效果图:
![53920a2fb0d39880d01324fe1d11d283.png](http://img.e-com-net.com/image/info8/947530821be74b8f987bea6e42d34ea5.jpg)
该插件会在 burp 上面新建一个标签页,用来保存一些配置,如数据库 ip 地址、端口、账号密码等。 还可以从数据库导出数据到文件作为字典,或者从文件中导入数据到数据库中,用于和别人分享及备份数据库。 导出的文件效果如下:
![0a557279f2b8f681e450273fc7f79c26.png](http://img.e-com-net.com/image/info8/413fd2a5062a4d48835eb69e50390e1c.jpg)
导出的字典文件是 txt 文件,主要是作为字典来使用,其中的内容是根据出现的次数来排序的, 如 /test/ 目录在 a.baidu.com 出现了一次,然后在 b.baidu.com 出现了一次,那么它的 count 值就是 2 。 目录在不同网站出现的次数越多,那么排名就会越前,证明该目录是最常现出的,因此应该把它放在前面提高命中率。 同理,对于请求中的参数名、文件名、子域名等数据,也是通过出现的次数来排序。 上图中,导出的还有 csv 文件,该文件含有出现的次数,可以通过该文件通过插件导入其它数据库,以实现备份或分享的功能。
插件开发 现在开始插件的开发,开发插件需要些前置的知识。 编写 burpsuite 插件需要对 Java 或 Python 语言有一定的基础,在这里我使用的是 Java,因为 Java 编写的插件在 burpsuite 加载得更快,性能更好。 在这里我使用的开发环境是 IDEA ,因为 IDEA 的智能补全功能就像知道我想打什么代码一样,十分强大。 在 IDEA 新建一个 gradle 项目,点击 Create New Project
![70c8aa3cb49d2df2673f21075c8b3504.png](http://img.e-com-net.com/image/info8/95c565d667f647b69ce5a82229c1f89d.jpg)
选择 Gradle 项目, Gradle 是一个构建工具,可以方便加载所需的代码仓库。 点击下一步。
![5155fa694039d64073bf3dce7741b0de.png](http://img.e-com-net.com/image/info8/f253945888614dacb6da8403b9797323.jpg)
给项目起个名称
![b5c0d6a1b6545ab83c5a48ebc96c0914.png](http://img.e-com-net.com/image/info8/0c7da76b415c4422a25a7cbe828867ab.jpg)
新建完后,在 build.gradle 文件中添加以下依赖,也就是加载 burpsuite 插件API ,如果提示 auto import, 可以点击,从而自动从远程仓库加载 burpsuite API 。
compile('net.portswigger.burp.extender:burp-extender-api:1.7.13')
同时在 plugins 里面添加 shadow 插件,该插件可以方便把项目打包成 jar 包。
id 'com.github.johnrengelman.shadow' version '5.2.0'
![d7fdb171ec9a10c9d8cbb9383d260c8c.png](http://img.e-com-net.com/image/info8/2372aafe848a414da6f3e23461432225.jpg)
接着在 /src/main/java 目录处创建一个名为 burp 的包名,在 java 目录处右键 -> New -> Package
![973aae0712f9612cd0a62f2115dfb4f3.png](http://img.e-com-net.com/image/info8/492365bcd389425ba7ce92818a5435c8.jpg)
接着在该包上右键,新建一个名为 BurpExtender 的类。
![dea0cb3c63e3fbbc1c8a646741a9e67d.png](http://img.e-com-net.com/image/info8/4b37718c917d4958888a0f58c126b43c.jpg)
这个包名和类名是固定的,burpsuite 加载插件时就是通过 burp.BurpExtender 来查找的,如果不这样起名,会报 ClassNotFoundException 。 BurpExtender 类需要实现 IBurpExtender 接口,burp 在加载插件时,会调用该接口,并传递 IBurpExtenderCallbacks 接口仅我们使用。 点击错误提示,实现接口中的方法
![6f3d0c00a4d278e8d529df797a8af54b.png](http://img.e-com-net.com/image/info8/098f0c481fb34a0a87d541628adbc334.jpg)
然后我们添加下面的代码为插件设置名称,并打印一 success 字符串
callbacks.setExtensionName("data-collect2");callbacks.printOutput("load success");
![743925ec023d2430f6845b7a76632397.png](http://img.e-com-net.com/image/info8/35d6a84977d74ab282b447e1bdb135df.jpg)
接着可以编译该项目成 jar 包,然后让 burp 加载看看效果。 点击右侧的 gradle 菜单,展开菜单,双击 shadowjar ,gradle 会自动编译项目成 jar 包,jar 包位于 build 目录中的 libs 目录中。
![d299f9f2ee8066b333f89452e0a54c2e.png](http://img.e-com-net.com/image/info8/d68cfe641ac1472299ef996edef57a4e.jpg)
接着在 burp 的扩展选项卡添加该 jar 包,点击 add
![ce1b972455b89aeba104877956432d6d.png](http://img.e-com-net.com/image/info8/96de23bcfafd4b6da58a5d06b3edc1b7.jpg)
选择 Java 扩展类型,选择我们的 jar 包,点击下一步
![0a9d0977d65160eee04af00e3e0feadc.png](http://img.e-com-net.com/image/info8/ec4fdf3ac4234b129c9a73011bd21957.jpg)
可以看到加载插件后成功打印了 success 字符串。
添加标签页 接着我们要为插件创建一个标签页,先在IDEA中创建一个 Form,用于设计UI
![d22289f63f0689b925fe98edf73f4ff7.png](http://img.e-com-net.com/image/info8/ddb48da73d1640c585b4329d7d2ab418.jpg)
创建的界面如下,左边的窗口中创建了两个文件,一个是 DataCollectGUI.java 文件,该文件与 form 文件绑定,一个是 DataCollectGUI.form 文件,可以在此文件上拖动控件来设计 UI 界面,当界面更新时,会自动生成代码插入 DataCollectGUI.java 文件中。
![94e9d5645ac671a444292cb745d8edd7.png](http://img.e-com-net.com/image/info8/9334b3612a744d328e25e0d895439b35.jpg)
接着从右边拖一个 JLabel(标签)、一个 JTextField(输入框)和 JButton(按钮) 到设计面板中,效果如下:
![34c316bdfab7fe21c90eab183f2e08fa.png](http://img.e-com-net.com/image/info8/0906ba5b342a4e17bb57d9e9b7c6eabe.jpg)
然后给根面板设置个变量名,用于后面生成代码:
![48f3aed6217e9db44ae7d81501302cc9.png](http://img.e-com-net.com/image/info8/57cd6a89f2bf43969a2bae666456b749.jpg)
为了让 IDEA 打包 GUI 界面的类,需要在 build.gradle 添加以下依赖
compile('com.intellij:forms_rt:7.0.3')
![3664a416ade9a6a73b5e0fdc66083a73.png](http://img.e-com-net.com/image/info8/8b5829ec7a76407bbb549c30a5336a8e.jpg)
接着在设置中设置根据 Form 界面自动生成 Java 源码:
![9050903175d655ff0067fba502a28723.png](http://img.e-com-net.com/image/info8/0bb8c6e3af7b4c1983978ef8f715258a.jpg)
然后在 Gradle 的编译选项中设置编译器是 IDEA 自带的编译器,这样才能自动更新 form 文件中的控件到代码中:
![704dbccb797657a97e2d42cf1f8727c0.png](http://img.e-com-net.com/image/info8/d618343c53294acfb43800e847b668e6.jpg)
设置好后,点击构建图标,就会自动生成和 form 文件相关的代码,可以看到在 $$$setupUI$$$() 方法中自动生成了我们拖到界面中的3个控件。 注意,$$$getRootComponent$$$() 方法需要给界面中的JPanel 控件设置变量名才可以生成,参考上面的步骤。
![1725cd1e3e76bfd03b4cbe3c87994337.png](http://img.e-com-net.com/image/info8/c6dc8ff6b09b411a8718765d5f90d918.jpg)
接着需要回到 BurpExtender 类中,要为插件添加一个标签页,需要实现 ITab 接口
![69f8a28f3036900baab574a35a568e18.png](http://img.e-com-net.com/image/info8/2dcedee6534649609f0efe4590dadf3b.jpg)
实现 ITab 接口后,会有两个方法需要实现,其中 getTabCaption() 方法返回标签页的名称, getUiComponent() 方法返回我们创建的 UI 面板。 注意,需要在14行下调用 callbacks.addSuiteTab(this) 来注册接口。 接着双击 gradle 中的 shadowjar 按钮重新打包 jar 包,然后在 burp 重新加载插件,就可以看到效果图了:
获取标签页设置内容 接下来我们需要获取标签页中的配置内容,可以通过添加事件监听器来实现。 回到 IDEA 的 form 文件中,在按钮上右键,点击 Create Listener,选择 ActionListener
![f44dcde1e77f7f4ab3cf7cdf58892af8.png](http://img.e-com-net.com/image/info8/a198dab5e0d74c33b1eb0184a2d74929.jpg)
创建好后就可以编写点击按键时的代码逻辑了
![01787d0e2cd8867f52f28cadf3b89885.png](http://img.e-com-net.com/image/info8/34c892e48feb4db69fe38738211990d9.jpg)
在这里简单地把输入框中的内容打印在插件日志中,要把内容打印到插件日志中,我们需要获取 IBurpExtenderCallbacks 对象,可以修改构造函数,在初始化时传入:
![94661134de3f4a6821d6e8d746f1bb0d.png](http://img.e-com-net.com/image/info8/42fe18760cd340a99551ffd52bf59e33.jpg)
还需要修改 BurpExtender 中的代码,传入 callbacks 对象
![56590882aed32cd753074ff0397b2f1a.png](http://img.e-com-net.com/image/info8/c90174a3513a43c4803ae1ec1b0c77a5.jpg)
接着在监听器中实现获取标题内容并打印到日志的代码,代码中29行通过 getText()方法获取输入框架的内容,然后在30行处通过 callbacks.printOutput()方法打印内容到日志中。
![6ed38cea79243ea9db94f2acaaeb92d8.png](http://img.e-com-net.com/image/info8/9b2514d21ea74317be47c9191d062a39.jpg)
接着双击 gradle 中的 shadowjar 按钮重新打包 jar 包,然后在 burp 重新加载插件,在插件输入框中输入 hello world, 点击按钮,就会在插件日志中打印输入框中的内容了。
![7a55f37e18cd8a8d1da0dc8305816180.png](http://img.e-com-net.com/image/info8/a2e240773ec04cf5a7a17012897a5a3e.jpg)
到此,我们可以获取插件输入框中的内容,这样我们就可以开始开发数据收集的插件了。
开发核心功能 由于插件代码比较多,这里就不一一介绍了,先导入开发完成的代码,可以到我的 github 上下载: https://github.com/QdghJ/burp_data_collector 使用 IDEA 导入项目
![f21543382d90e60f476a0e378c28121e.png](http://img.e-com-net.com/image/info8/abb7defa65d74801835cff68c130946b.jpg)
选择 gradle 项目
![54affeda1b472adc3d5bd52e73c0e141.png](http://img.e-com-net.com/image/info8/06d11aa328fc40cfbf52b2597a2789be.jpg)
导入后,我们来看看开发数据收集插件需要用到哪些库,首先需要 mysql 的 jdbc 驱动,然后是导出 csv 文件时用到的库:
![e4f1d923b2bca420790825fce43b583e.png](http://img.e-com-net.com/image/info8/b6c36dd64a1e42e2b3d49fd12e2916c9.jpg)
接着来看看项目的文件结构,在 dao 包中的主要是数据库的操作类,封装了数据库操作的代码,每个表一个类来插入和查询数据。 而 gui 包中的就是界面类。
核心要点 这个插件的核心要点如下:
- 解析功能,解析请求中的各种数据,然后插入到数据库中。
- 数据库设计,关键体现在查询和插入数据的性能上,在被我的开发朋友调教后,重新设计SQL语句,从导入一次数据需要十几分钟,优化到了几秒内完成。
- 内存去重,在内存中保存已经插入的数据,如果重复了,就不插入数据库,可以极大地减少数据库的操作。
- 定时收集数据,每10分钟收集一次数据。
- 退出时收集数据,当 burp 退出时会进行最后一次数据收集,不用人工收集。
解析请求 首先是解析功能,该功能在 BurpExtender 类的 saveData() 方法中,该方法会在点击 export data to database 按钮后或定时收集时调用,用于解析请求历史中的数据,并保存到数据库中。 在 329 行处调用 callbacks.getProxyHistory() 方法获取请求历史中的所有请求, 接着在 332 行处使用 for 循环遍历所有的请求, 在333行处解析请求内容,并在 334行处获取请求的 host,也就是主机名,如 www.baidu.com。 在 335 行处获取请求的路径,如 /aaa/bbb/ccc.php ,这里就收集到了请求中的路径。 然后在339行处插入到内存中,等到最后一次性插入数据库。 接着在 334 行到 359 行之间收集分离的目录,如 /aaa/bbb/ccc/ 目录,会收集 /aaa/bbb/ccc/, /aaa/bbb/ , /aaa/ 三个目录。
![6b082df62bfd451896ddb4957833b983.png](http://img.e-com-net.com/image/info8/b247ea2148534fc6a0726390f2b6b02b.jpg)
接着在 361 行处从 path 中分离出目录,如 /aaa/bbb/ccc/, 那么会收集 /aaa/, /bbb/, /ccc/ 。 接着在 370 行处收集文件名, 如 /js/jquery.js, 那么会收集 jquery.js 。 然后在 376 行处分离主机名,也就是分离域名,只获取子域名,如域名是 aa.bb.cc.dd.ee ,那么会收集先收集分离后的子域名,如 aa, bb, cc, 然后收集 aa.bb.cc, bb.cc, cc 几个子域名。
![811abaf01b8ec578e215375971ae6a8a.png](http://img.e-com-net.com/image/info8/5d9851428ef04f2e9e599d7c2c1a78fa.jpg)
最后在 400 行处获取请求中的所有参数,402 行处的 2 代表 cookie 参数名,暂时不收集 cookie, 然后收集符合条件的参数名。
数据库设计 数据库设计主要包括表结构的设计,导出语句的的设计,插入语句的设计。 数据库的设计对性能的影响很大,开始时我写的SQL语句收集一次数据需要20分钟,经过做开发的朋友改进后,只需1秒,所以写数据库代码就是写SQL语句。 首先是表的设计,本次要收集的数据上面已经介绍过,主要的表如下:
![8bf0ad86e1d7cb5da0c7560ad304eb8a.png](http://img.e-com-net.com/image/info8/87ca2a5ff91047b496cdaa50ef778574.jpg)
表的类型有两种,一种是 host_xxx_map 表,如 host_dir_map 表用于收集 host 信息及其对应的内容,表的字段有 host 和 dir ,上图可以看出 0.gravatar.com 这个 host 对应的目录有3个。 另一种表如,dir 表,用于导入 csv 文件中的数据,表的字段是 dir , count,分别是目录名和出现的数目,主要用于导入别人导出的 dir_import.csv, 当导出数据时,会合并这种表中的数据,再进行导出。 创建表的代码在 dao/DatabaseUtil 类中
![b737ec46e2ce2c8d6f634ee647592024.png](http://img.e-com-net.com/image/info8/8358146bad71422fbbb2ace1566a1810.jpg)
接下来说下插入数据时的语句,插入数据时应该怎么处理呢,我一开始的时候设计的是这样的: 假设要插入一条数据, host 值是 www.baidu.com, dir 值是 /js/ 下面是伪代码
数据库是否存在该记录 // 此处一条查询语句
如果存在不插入
如果不存在,插入该记录 //此处一条插入语句
假设最坏的情况,我们访问的网站是从来没访问过的,第一次收集数据的时候所有数据都不存在,当该网站有1000条请求时,这里就会进行2000次数据库处理操作。 这样必然是分钟级别的插入,在这样设计时,我需要20分钟才完成一次数据收集。 当我的开发朋友看到这种操作时,内心是崩溃的,于是给我优化了下。 首先是数据库是否存在记录的问题,可以使用 INSERT IGNORE 来解决,该语句在插入数据时,如果存在,则不插入。 接着可以使用一条语句插入所有数据,语句如下:
INSERT IGNORE INTO host_dir_map(host, dir) VALUES
("www.baidu.com", "dir1"),
("www.baidu.com", "dir2"),
...
("www.baidu.com", "dir1000")
这样,2000条语句的操作就变成了一条语句完成,从20分钟的插入时间变成了1秒。 接着是导出数据的语句,如果要统计一个目录在所有 host 中出现的次数,需要使用到 group by 语句,语句如下:
SELECT hdm.dir, COUNT(*) AS dirCount FROM host_dir_map hdm GROUP BY hdm.dir ORDER BY `dirCount` DESC
通过使用 dir 字段来分组,然后通过 count(*) 来统计出现的次数,查询结果如下:
![50d9889d98e1f90f19fff54e4ee39fb3.png](http://img.e-com-net.com/image/info8/4021bfbc9bb94d15b549012d688fd7eb.jpg)
具体的导出代码如下:
内存去重 插件是可以定时获取请求内容中的数据并插入数据库的,但每次定时操作时,不知道数据是否插入过数据库了,如果每次都插入全部数据到数据库,还是会影响性能。 因此有必要在每插入一个数据的时候,在内存中记录下该数据已经被插入,在第二次定时收集的时候,遇到已经插入过数据库的数据,就可以跳过此数据。 在内存中检查比在数据库中快很多,这样在导出一次全量的数据后,第二次定时收集的时候会快很多,因为只需要收集新出现的请求。 具体怎样实现呢? 我们需要使用 HashMap 和 HashSet 数据结构相结合。 HashMap 是一个键值对数据结构,也就是一个哈希桶,可以把一些键值对保存到该结构中进行快速查找。 HashSet 是一个集合,里面的数据是不重复的元素,也就是说 ,里面只会出现 aa,bb,cc ,而不会出现 aa,aa,bb,cc。 我们可以组合两个数据结构来实现保存 host 对应的 dir 。 具体结构如下图:
![f9b513b716232717a0445c38bab31b0c.png](http://img.e-com-net.com/image/info8/133311cc83684bba9957de93c0af7e37.jpg)
上图中,www.baidu.com 是一个键,对应的值是一个 hashset 结构,hashset里面装有这个 host 所有的目录。 由于有多个表的内容要保存,只要加多一层 HashMap 就可以实现保存所有表了。
![c190d7810b033a629fa572abb31beba4.png](http://img.e-com-net.com/image/info8/9486f16f2b5c48b2ba22e28f06ef766b.jpg)
具体实现代码如下:
private boolean addToMemory(String host, String value, String flag) { boolean result = true; HashMap> hostHashMap = memoryHostValueMap.get(host);if (hostHashMap == null) { hostHashMap = new HashMap<>(); HashSet hostHashSet = new HashSet<>(); hostHashSet.add(value); hostHashMap.put(flag, hostHashSet); memoryHostValueMap.put(host, hostHashMap); result = false; } else { HashSet hostHashSet = hostHashMap.get(flag);if (hostHashSet == null) { hostHashSet = new HashSet<>(); hostHashSet.add(value); hostHashMap.put(flag, hostHashSet); result = false; } else {if (!hostHashSet.contains(value)) { hostHashSet.add(value); result = false; } } }return result; }
flag 值代表保存的是哪种类型的数据,如 dir、path。 以上代码的逻辑是判断内存中是否存在该数据,不存在则插入并返回 false。 在解析数据的时候,可以根据 addToMemory() 函数的返回值来决定是否把数据添加到插入队列中。 具体流程如下: 解析数据时,先通过 checkDir 函数检查是否需要插入数据库
![e13f50dd23099840125db65b9dd0dfc1.png](http://img.e-com-net.com/image/info8/9cbf046ff86146fbaf603ef7107b1254.jpg)
checkDir() 函数调用 addToMemory() 函数判断内存中是否存在该数据了
![443a45ff0fb1c99a74e499ee2ce6b14c.png](http://img.e-com-net.com/image/info8/5b01d60bd91b40a7a001573288e99855.jpg)
回到上面的367行处,接着调用 addToInsertMap() 函数把数据保存到插入队列中,addToInsertMap() 函数和 addToMemory() 相似,区别是只保存新增的数据:
![624795acd87145ef942660460a18d341.png](http://img.e-com-net.com/image/info8/9b9c78bd98a248fab234bf7bf7e64a83.jpg)
在 saveData() 函数的最后会取出内存中保存的新增数据,一次性插入数据库中:
小细节 这个插件比较方便的是会定时保存数据以及在退出 burp 时保存数据。 定时保存数据使用 ScheduledExecutorService 对象定时执行任务:
service = Executors.newSingleThreadScheduledExecutor();service.scheduleWithFixedDelay(new Runnable() {@Overridepublic void run() { BurpExtender.this.saveData(); callbacks.printOutput("Scheduled export execution completed"); }}, 0, 10, TimeUnit.MINUTES);
退出 burp 时保存数据需要实现 IExtensionStateListener, 并在 extensionUnloaded() 方法中保存数据
![c8a99cc8a99302a7db41a775b68750b5.png](http://img.e-com-net.com/image/info8/3fcf540fddea4a75bca687414b5a657f.jpg)
插件的开发到这里就完成了,具体的实现可以下载源码来查看,喜欢我的文章的可以关注下我的公众号,以后可能会继续发些工具开发的文章。 https://github.com/QdghJ/burp_data_collector 这个插件的开发要感谢我的基友,让我的数据库性能得到了极大的提升。
往期文章python协程学习——写个并发获取网站标题的工具一些相见恨晚的BurpSuite插件推荐