Burpsuite Java插件开发 - API篇

网上也有一些BP插件API相关的文章,本文主要就写BP插件API的一些细节,但不会覆盖到所有的API。下方是官方的API文档和示例代码链接。入门BP插件编写,主要就是看官方的几个示例代码,去编译运行,去用用,去查API文档,就差不多了。

旧API:

新API:

BP插件API都出新的了,不过这里还是说旧API,目前网上使用旧API的插件还是比较多的。

文中的代码主要在 Burpsuite Pro v2023.4.5 版本下测试运行。

void IBurpExtenderCallbacks.setExtensionName(java.lang.String name)

设置插件名。最好在插件入口一开始就设置。 因为插件名跟插件配置绑定,所以最好不要随意更改插件名。 插件名也被用于插件自己添加的TAB页的标题和右键 - Extensions 菜单显示。

void IBurpExtenderCallbacks.customizeUiComponent(java.awt.Component component)

将指定的UI组件调整为Burp的UI风格。

某些组件调用了这个方法会导致问题 :

  • 鼠标不能拖动JTable表格的列头以更换顺序,即使表格调用了table.getTableHeader().setReorderingAllowed(true) 也不行
  • JComboBox下拉框不能编辑,即使设置了Editabletrue

解决方式:

  • 先调用customizeUiComponent方法,再修改组件(比如将JComboBox组件的Editabletrue)。
  • 不要调用customizeUiComponent方法,实际测试调不调用这个方法对界面外观没有什么影响。(所以这个方法是做了什么操作?没去细究)

void IBurpExtenderCallbacks.addScanIssue(IScanIssue issue)

Issue添加到BurpsuiteDashboard

需自己编写一个实现IScanIssue接口的类。无论该类的getIssueType()方法返回什么值,由IScannerListener监听拿到的issue对象的issueType固定是 0x08000000,表示是由插件添加的issueIssueType列表见:https://portswigger.net/kb/issues 或 打开 bupsuite - Target - Issue definitions查看。

byte[] IExtensionHelpers.updateParameter(byte[] request, IParameter parameter)

更新HTTP请求中参数的值。如果更新了请求body,还会添加或更新Content-Length请求头。

该方法不会自动对参数值编码。且仅支持IParameter.PARAM_URL, IParameter.PARAM_BODYIParameter.PARAM_COOKIE类型的IParameter对象,传递其他类型的IParameter对象会抛异常。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package burp;

public class BurpExtender implements IBurpExtender
{
@Override
public void registerExtenderCallbacks(IBurpExtenderCallbacks callbacks)
{
callbacks.setExtensionName("Test Extension");

IExtensionHelpers helpers = callbacks.getHelpers();
IParameter a = helpers.buildParameter("a", "1&b=2", IParameter.PARAM_BODY);
byte[] request = "POST / HTTP/1.1\r\nHost: example.com\r\n\r\nc=3".getBytes();
byte[] request2 = helpers.updateParameter(request, a);
callbacks.printOutput(new String(request2));

}

}
/*
输出:

POST / HTTP/1.1
Host: example.com
Content-Length: 11

c=3&a=1&b=2
*/

void IExtensionStateListener.extensionUnloaded()

该回调方法在卸载(unload)插件时被调用,关闭Burp时不会被调用。

通常在这个回调方法里关闭插件里启动的后台线程和连接、文件资源等,不然线程还会继续运行,容易造成问题。

IRequestInfo

调用IExtensionHelpers对象的IRequestInfo analyzeRequest(byte[] request)方法解析请求数据,返回IRequestInfo对象,该对象包含一些请求信息,如请求头,参数列表等。IRequestInfo对象的几个方法的细节:

  • java.util.List<IParameter> IRequestInfo.getParameters():返回请求包里的参数列表。burp支持解析的参数类型有URL query参数、Cookie、body里的urlencoded格式、json格式、xml格式或multipart格式的参数。对于multipart格式请求包的解析结果需要说明下,以下示例代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    package burp;

    public class BurpExtender implements IBurpExtender
    {
    @Override
    public void registerExtenderCallbacks(IBurpExtenderCallbacks callbacks)
    {
    callbacks.setExtensionName("Test Extension");

    IExtensionHelpers helpers = callbacks.getHelpers();
    byte[] request = ("POST / HTTP/1.1\r\n" +
    "Host: example.org\r\n" +
    "Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryxUcPkAgfQy8GyoF5\r\n" +
    "Content-Length: 259\r\n" +
    "\r\n" +
    "------WebKitFormBoundaryxUcPkAgfQy8GyoF5\r\n" +
    "Content-Disposition: form-data; name=\"note\";\r\n" +
    "\r\n" +
    "test\r\n" +
    "------WebKitFormBoundaryxUcPkAgfQy8GyoF5\r\n" +
    "Content-Disposition: form-data; name=\"file\"; filename=\"test.txt\"\r\n" +
    "\r\n" +
    "content\r\n" +
    "------WebKitFormBoundaryxUcPkAgfQy8GyoF5--\r\n").getBytes();
    IRequestInfo requestInfo = helpers.analyzeRequest(request);
    for(IParameter parameter: requestInfo.getParameters()) {
    callbacks.printOutput(String.valueOf(parameter.getType()));
    callbacks.printOutput(parameter.getName());
    callbacks.printOutput(parameter.getValue());
    callbacks.printOutput("---------------------");
    }

    }

    }

    输出:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    1  // 即 IParameter.PARAM_BODY
    note
    test
    ---------------------
    5 // 即 IParameter.PARAM_MULTIPART_ATTR
    filename
    test.txt
    ---------------------
    1 // 即 IParameter.PARAM_BODY
    file
    content
    ---------------------

    IParameter.PARAM_MULTIPART_ATTR类型的参数,指multipart属性,即这里的filename="test.txt"部分,而body里的其他参数归于IParameter.PARAM_BODY这类。body里的urlencoded格式参数也是IParameter.PARAM_BODY

    另外对于xml这种<empty/>空元素不解析,但解析<empty></empty>这种,类型为IParameter.PARAM_XML

  • java.util.List<java.lang.String> IRequestInfo.getHeaders():获取请求头列表,包含请求体第一行(如:GET / HTTP/1.1

  • byte IRequestInfo.getContentType(): 返回body的Content Type。其不是根据Content-Type请求头判断,而是根据body内容来判断:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    package burp;

    public class BurpExtender implements IBurpExtender
    {
    @Override
    public void registerExtenderCallbacks(IBurpExtenderCallbacks callbacks)
    {
    callbacks.setExtensionName("Test Extension");

    IExtensionHelpers helpers = callbacks.getHelpers();
    byte[] request = "POST / HTTP/1.1\r\nHost: test.com\r\nContent-Type: application/json\r\n\r\n<user>admin</user>".getBytes();
    IRequestInfo requestInfo = helpers.analyzeRequest(request);
    callbacks.printOutput("ct: " + requestInfo.getContentType());
    }

    }
    /*
    输出:
    3 // 即 IRequestInfo.CONTENT_TYPE_XML
    */

IResponseInfo

调用IExtensionHelpers对象的IResponseInfo analyzeResponse(byte[] response)方法解析请求数据,返回IResponseInfo对象,该对象包含一些响应信息,如状态码,响应头,MIME类型等。

IResponseInfo对象获取MIME类型有两个方法:

  • java.lang.String IResponseInfo.getStatedMimeType():根据Content-Type响应头返回MIME类型
  • java.lang.String IResponseInfo.getInferredMimeType():根据body内容判断MIME类型。 (在Burp的Proxy面板的MIME列就是根据body内容判断的)

返回值可能是HTMLGIFSVGimagePNGJPEGCSSJSONXMLscriptvideo 或空字符串等。

IParameter

上面提到通过解析请求数据,获取请求参数列表,即IParameter对象列表。通过IParameter对象的getValue()方法可以获取到参数值,需要注意的是,获取到的值为未解码过的。示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package burp;

public class BurpExtender implements IBurpExtender
{
@Override
public void registerExtenderCallbacks(IBurpExtenderCallbacks callbacks)
{
callbacks.setExtensionName("Test Extension");

IExtensionHelpers helpers = callbacks.getHelpers();
byte[] request = "POST /redirect?url=http:%2f%2f HTTP/1.1\r\nHost: test.com\r\nContent-Type: application/json\r\n\r\n{\"username\":\"admin\",\"remember\":1}".getBytes();
IRequestInfo requestInfo = helpers.analyzeRequest(request);
for(IParameter parameter: requestInfo.getParameters()) {
callbacks.printOutput(String.valueOf(parameter.getType()));
callbacks.printOutput(parameter.getName());
callbacks.printOutput(parameter.getValue());
callbacks.printOutput("---------------------");
}
}

}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
0
url
http:%2f%2f
---------------------
6
username
admin
---------------------
6
remember
1
---------------------

上方的query参数/?url=http:%2f%2f,拿到的值是http:%2f%2f,burp没有对其进行解码,在数据包里什么样就是什么样。

对于JSON参数 {"usernmae": "admin", "rememberme": 1}"admin"1参数值,getValue()返回值分别是admin1,并不能从值去判断该JSON参数是字符串还是数字,可以使用request[parameter.getValueStart()-1] == '"' 来判断。

IScannerCheck

通过 IBurpExtenderCallbacks对象的registerScannerCheck(IScannerCheck check) 方法注册一个IScannerCheck对象来向burp添加自己的扫描检测。

IScannerCheck接口的几个方法:

  • int consolidateDuplicateIssues(IScanIssue existingIssue, IScanIssue newIssue):当同一个URL路径报了多个issue,该方法就会被调用。若返回-1则只报告existingIssue,若返回0则报告这两个issue,若返回1则报告newIssue。通常返回0
  • java.util.List<IScanIssue> doActiveScan(IHttpRequestResponse baseRequestResponse, IScannerInsertionPoint insertionPoint):主动扫描时被调用,需要时可在此方法里发包。 返回值为issue列表,其会被加到Burp的Dashboard的Issue Activity面板里。(也可以直接返回空列表,然后通过调用 IBurpExtenderCallbacks.addScanIssue(IScanIssue issue) 方法添加issue
  • java.util.List<IScanIssue> doPassiveScan(IHttpRequestResponse baseRequestResponse):被动检测时被调用,返回值为issue列表,其会被加到Burp的Dashboard的Issue Activity面板里。

调用burp主动扫描的一种方式是在请求项上右键 - Do Active Scan。对于被动检测,burp默认会在DashboardTask面板中建立一个Audit checks - passive的任务,该任务就是对Proxy流量进行被动检测,或者自己在请求项上右键 - Do passive scan

Github上很多burp被动扫描插件会在IScannerCheckdoPassiveScan这个回调方法上发包检测漏洞,但官方建议是在这个回调方法里不发包,仅做数据包检测(如匹配敏感信息等),所以被动扫描器跟burpdoPassiveScan被动检测的”被动“不是一个意思。

假如一个FastJson被动扫描插件在doPassiveScan这个回调方法里做检测,且该插件有一个是否开启FastJson扫描的配置,就会出现这样的bug:

  1. 用户将浏览器流量代理到burp,且FastJson被动扫描插件为关闭状态。
  2. 用户访问 https://example.com/api,burp会调用doPassiveScan回调方法,但FastJson被动扫描插件为关闭状态,插件直接不检测。
  3. 用户开启FastJson被动扫描插件,再访问https://example.com/api,burp不会再调用doPassiveScan回调方法了,导致即使开启了FastJson被动扫描插件也不会对https://example.com/api扫描。

原因在于,burp对于拥有相同的URL路径和参数键名的不同请求只会调用一次doPassiveScan。这个行为可以在 Dashboard - Task - "Audit check - passive" - Depulication配置里进行关闭。

所以如果想要实现一个burp被动扫描插件,应该监听Proxy流量,然后自行判断该请求是否扫描过,避免重复扫描。

makeHttpRequest

调用IBurpExtenderCallbacksmakeHttpRequest方法来发送http请求,有几个问题:

  • 该方法为同步发送请求,没有提供异步发送的方式。

  • 该方法不跟踪跳转。(测试发现,跟burp的Settings - Network - HTTP - Allowed redirect type这个配置无关)

  • 有个默认的超时时间(120秒),可在burp的Settings - Network - Connections - Timeouts里配置。大部分用户不会去配置,导致用burp api发大量请求时会比较慢。 makeHttpRequest方法没有设置超时时间的参数。

  • 发生连接不上、超时等错误时也不会抛异常。只能通过返回值判断下是否发生错误:

    • IHttpRequestResponse makeHttpRequest(IHttpService httpService, byte[] request):返回值IHttpRequestResponse对象的getResponse()方法返回 null或者长度为0的byte数组。

    • byte[] makeHttpRequest(java.lang.String host, int port, boolean useHttps, byte[] request) :返回值为 null或者长度为0的byte数组。

      具体的错误信息只在 Burp 的 Dashboard - Event Log里可以看到。

如果不使用burp提供的API,而是用第三方库(如okhttp3)发送请求,需要注意:

  • 发送的请求不会被记录到Logger面板里。
  • burp里的配置不会影响到第三方库,如代理设置。

urlEncode 和 urlDecode

IExtensionHelpers对象提供的URL编码和URL解码函数:

  • byte[] urlDecode(byte[] data)
  • java.lang.String urlDecode(java.lang.String data)
  • byte[] urlEncode(byte[] data):不对'<>/\"等编码,对?#@:等编码,将空格转为+号而不是%20
  • java.lang.String urlEncode(java.lang.String data):同上

这几个函数并不处理编码方式。下方示例会打印乱码与错误结果:

1
2
3
4
5
6
7
8
9
10
11
12
package burp;

public class BurpExtender implements IBurpExtender
{
@Override
public void registerExtenderCallbacks(IBurpExtenderCallbacks callbacks)
{
IExtensionHelpers helpers = callbacks.getHelpers();
callbacks.printOutput(helpers.urlDecode("%e4%b8%ad%e6%96%87")); // 打印乱码。
callbacks.printOutput(helpers.urlEncode("测测")); // 打印错误结果 "KK"
}
}

如果要URL解码支持中文,需调用URL解码为byte数组,再指定编码方式将byte数组转为字符串。示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package burp;

import java.nio.charset.StandardCharsets;

public class BurpExtender implements IBurpExtender
{
@Override
public void registerExtenderCallbacks(IBurpExtenderCallbacks callbacks)
{
IExtensionHelpers helpers = callbacks.getHelpers();

byte[] utf8Bytes = helpers.urlDecode("%e4%b8%ad%e6%96%87".getBytes());
String utf8String = new String(utf8Bytes, StandardCharsets.UTF_8);
callbacks.printOutput(utf8String); // 输出 "中文"
}
}

如果要对中文进行URL编码:

1
2
3
4
5
6
7
8
9
10
11
12
13
package burp;

import java.nio.charset.StandardCharsets;

public class BurpExtender implements IBurpExtender
{
@Override
public void registerExtenderCallbacks(IBurpExtenderCallbacks callbacks)
{
IExtensionHelpers helpers = callbacks.getHelpers();
callbacks.printOutput(new String(helpers.urlEncode("中+文".getBytes(StandardCharsets.UTF_8))));
}
}

上方示例并没有输出期望的结果 "%e4%b8%ad%2b%e6%96%87",而是输出 "中%2b文"。这是因为burp的urlEncode方法不对不可见字节(如空字节、0xe4)进行url编码。所以想要期望的效果,需要自己实现URL编码方法了。

bytesToString 和 stringToBytes

IExtensionHelpers对象提供的在字节数组与字符串之间转换的方法:

  • java.lang.String bytesToString(byte[] data)

  • byte[] stringToBytes(java.lang.String data)

    这两个方法也是不考虑编码方式,直接将字符串中的各个char字符和byte之间转换。示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package burp;

public class BurpExtender implements IBurpExtender
{
@Override
public void registerExtenderCallbacks(IBurpExtenderCallbacks callbacks)
{
IExtensionHelpers helpers = callbacks.getHelpers();

callbacks.printOutput(helpers.bytesToString(new byte[]{(byte) 0xe6, (byte) 0x90, (byte) 0x9c})); // 输出 乱码
callbacks.printOutput(hexEncode(helpers.bytesToString(new byte[]{(byte) 0xe6, (byte) 0x90, (byte) 0x9c}))); // 输出 e6909c

callbacks.printOutput(hexEncode("中")); // 输出 4e2d ("中" 的Unicode码为U+4e2d,因为Java里的字符串按Unicode编码方式存储)
callbacks.printOutput(hexEncode(helpers.stringToBytes("中文"))); // 输出 2d87 ( 这个结果怎么来的: "中"的Unicode码为 U+4e2d, "文"的Unicode码为 U+6587,各取一个字节,拼接为 2d87)
}

public static String hexEncode(String data) {
StringBuilder hexContent = new StringBuilder();
for (char b : data.toCharArray()) {
hexContent.append(String.format("%02x", (int) b));
}

return hexContent.toString();
}

public static String hexEncode(byte[] data) {
StringBuilder hexContent = new StringBuilder();
for (byte b : data) {
hexContent.append(String.format("%02x", b));
}

return hexContent.toString();
}
}

如果考虑中文字符的编码,就不要用这两个方法,使用:

1
2
3
4
5
// byte数组转字符串
new String(new byte[]{(byte) 0xe4, (byte) 0xb8, (byte) 0xad}, StandardCharsets.UTF_8);

// 字符串转byte数组
"中文".getBytes(StandardCharsets.UTF_8);