基于某电力系统传输加解密操作BP插件编写

1.WEB JS逆向出密钥和算法以及签名

  • 通过前端JS逆向发现请求响应都使用的是SM4加密算法。且跟踪代码后发现密钥在前端进行了硬编码
  • 而签名逻辑为url+body参数+‘-’+url参数+请求方法,最后替换掉斜线

image-20250206102033876

image-20250208112547657

2.复现完整加解密和签名逻辑

image-20250206102148889

  • 首先对加密数据进行BASE64解码为二进制数据,然后进行SM4ECB解密。加密就反过来即可。

image-20250206102207485

  • 签名逻辑复现

image-20250208112740004

3.编写插件

3.1.新建IDEA Maven项目

  1. 我这里采用的是Maven项目,如果想用Groovy构建的也可以,Groovy构建的更快,官方也是用的Groovy,不过我用的不够熟悉,有时候构建容易出问题,所以我这采用Maven构建。需要注意的是JDK版本一定要是JAVA17版本以上的,因为我们这使用的是新版本的montoya-api,而不是老版本的API,所以必须使用高版本JAVA。

image-20250206153917250

  1. pom文件配置
    • 需要导入的是bp插件的API依赖两个,
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.gaomu</groupId>
<artifactId>SQCG-BP-Plugin</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- 导入BP插件API -->
<dependency>
<groupId>net.portswigger.burp.extensions</groupId>
<artifactId>montoya-api</artifactId>
<version>LATEST</version>
</dependency>
<dependency>
<groupId>net.portswigger.burp.extender</groupId>
<artifactId>burp-extender-api</artifactId>
<version>1.7.13</version>
</dependency>
<!-- 需要使用到的一些工具,加解密和解析JSON数据 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.23</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>1.77</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.17.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.2</version>
</dependency>

</dependencies>
<!-- 选择构建方式,需要将所有的依赖都进行打包,否则插件无法运行 -->
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.3.0</version> <!-- 使用适合你项目的版本 -->
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<!-- 省略了 <archive> 部分,因为不需要指定主类 -->
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<!-- 配置构打包方式 -->
<packaging>jar</packaging>

</project>

  1. 下载BP新版插件示例 https://github.com/PortSwigger/burp-extensions-montoya-api-examples
    • 下载burp-extension-input-editer和MyHttpHandler这两个代码示例
  2. 将示例中的JAVA代码均导入复制到我们的项目中。(以下几个文件)

image-20250206155801755

  • 然后将MyExtensionProvidedHttpRequestEditor和MyHttpRequestEditorProvider复制后重新更名为MyExtensionProvidedHttpResponseEditor和MyHttpResponseEditorProvider。
  • 看名字就应该清楚,我们不仅需要对请求参数进行加解密还需要对响应也进行加解密,因此需要再拷贝一份。

3.2.编写加密解密函数

  1. 首先编写SM4解密函数,我是从我之前的模板整改的,为了编写代码更少。之后我也会将模板链接贴在下面,这是我写了很多套加解密逻辑最后形成的自己的模板,都是精华。
  2. 首先是对密钥key的处理,SM4密钥是我们从前端逆向出来的一个硬编码UTF8的数据。我为了方便直接使用CyberChef工具将这个密钥转换为hex字符串格式,大家也可以直接在代码里转换。无论密钥本身是什么格式的字符串,BASE64还是UTF8的数据转换为SM4的16进制密钥都是32个字符长度,因为SM4的密钥长度为128bit,所以如果不是这个长度那说明你的密钥是有问题的。

image-20250206162114129

  1. 因为我们这获取到的密文是BASE64编码的所以需要对密文进行BASE64解码后再解密。
  2. 还有大家需要注意自己的填充模式,我这边使用的是无填充模式,通常SM4都会有填充的也就是PKCS5Padding。
  3. 为了方便,我将密钥KEY进行了硬编码,也就是根据代码逻辑中key我就不需要再传值了,之后调用函数的时候我直接传入空字符串就行了,如果需要传入不同的密钥再配置参数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static String decryptECB(String cipherTxt, String key) {
//默认16进制密钥密钥处理 ------
String plainTxt = "";
key = Objects.equals(key, "") ? KEY : key;
byte[] keyBytes = HexUtil.decodeHex(key);
try {
//Padding.NoPadding 无填充模式
SymmetricCrypto sm4 = new SM4(Mode.ECB, Padding.NoPadding, keyBytes);
//byte[] cipher = HexUtil.decodeHex(cipherTxt);//16进制密文转换
byte[] cipher = Base64.decode(cipherTxt);//BASE64密文转换

plainTxt = sm4.decryptStr(cipher, CharsetUtil.CHARSET_UTF_8);

} catch (Exception e) {
//e.printStackTrace();
throw new RuntimeException("SM4 decryption failed");
}
return plainTxt.replaceAll("\\u0000", ""); //由于无填充,因此需要对解密出来的空数据部分进行替换
}
  1. 加密逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static String encryptECB(String plainTxt, String key) {
String cipherTxt;
key = Objects.equals(key, "") ? KEY : key;
//默认16进制密钥
//密钥处理 ------
byte[] keyBytes = HexUtil.decodeHex(key);
try {
SymmetricCrypto sm4 = new SM4(Mode.ECB, Padding.PKCS5Padding, keyBytes);
//cipherTxt = sm4.encryptHex(plainTxt);
cipherTxt = sm4.encryptBase64(plainTxt);//加密为BASE64格式
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("SM4 encryption failed");
}

return cipherTxt;
}

3.3.编写CustomRequestEditorTab文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class CustomRequestEditorTab implements BurpExtension
{
@Override
public void initialize(MontoyaApi api)
{
//配置插件的名字
api.extension().setName("SQCG Tool");
//注册我们的请求编辑器处理器和响应编辑器处理器
api.userInterface().registerHttpRequestEditorProvider(new MyHttpRequestEditorProvider(api));
api.userInterface().registerHttpResponseEditorProvider(new MyHttpResponseEditorProvider(api));
//注册请求参数处理器
api.http().registerHttpHandler(new MyHttpHandler(api));

//日志配置
Logging logging = api.logging();
logging.logToOutput("SQCG Tool loaded successful.");
}
}

  • 下图分别为插件名和日志输出。日志输出方式有很多中,具体大家可以看官方示例,我这只演示这一种。

image-20250206160900889

3.4.请求处理器编写

  • MyHttpRequestEditorProvider 不用做任何变动,保持和官方示例相同即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MyHttpRequestEditorProvider implements HttpRequestEditorProvider
{
private final MontoyaApi api;

MyHttpRequestEditorProvider(MontoyaApi api)
{
this.api = api;
}

@Override
public ExtensionProvidedHttpRequestEditor provideHttpRequestEditor(EditorCreationContext creationContext)
{
return new MyExtensionProvidedHttpRequestEditor(api,creationContext);
}
}

  • MyExtensionProvidedHttpRequestEditor 编写
    • MyExtensionProvidedHttpRequestEditor 初始化构造器
    • getRequest 用于处理我们修改后的请求
    • setRequestResponse 用于处理原始请求
    • isEnabledFor 用于判断处理是否开启该处理器
    • caption 返回该编辑器窗口的标签名字
    • selectedData 用于判断请求是否有数据
    • isModified 用于判断请求是否被修改过
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
class MyExtensionProvidedHttpRequestEditor implements ExtensionProvidedHttpRequestEditor
{

//用来标记解密后标签页名字
private static final String TAG = "Plain";

private static final String STARTUP_SM4_KEY = "SM4_KEYS";

private final RawEditor requestEditor;
private final Logging logging;
private HttpRequestResponse requestResponse;
private final MontoyaApi api;

//用于组件通信的数据存储
private final PersistedObject myExtensionData;
//请求体字符串格式
private String bodyStr;


//初始化构造器
MyExtensionProvidedHttpRequestEditor(MontoyaApi api, EditorCreationContext creationContext)
{
this.api = api;
//日志配置
logging = api.logging();
//用于组间通信的参数,官方示例没用,本次项目也没用到。但是不能忽略,因此我把它留在下了。
myExtensionData = api.persistence().extensionData();

//配置编辑器窗口的格式,只读和自动换行
if (creationContext.editorMode() == EditorMode.READ_ONLY)
{
requestEditor = api.userInterface().createRawEditor(EditorOptions.READ_ONLY, EditorOptions.WRAP_LINES);
}
else {
requestEditor = api.userInterface().createRawEditor(EditorOptions.WRAP_LINES);
}
}

//用于处理我们修改后的请求
@Override
public HttpRequest getRequest()
{
HttpRequest request;
//获取到请求体数据
String modifiedJson = new String(requestEditor.getContents().getBytes(), StandardCharsets.UTF_8);

if (requestEditor.isModified())
{
//将修改之后的请求加密后 数据更新到requestResponse,中发送请求,我们所有在界面中的更改都会被更新
request = requestResponse.request().withBody(encryptECB(modifiedJson, ""));
//request = requestResponse.request().withUpdatedParameters(HttpParameter.parameter(parsedHttpParameter.name(), modifiedJson, parsedHttpParameter.type()));
}
else
{
//若未做更改 直接发送原数据
request = requestResponse.request();
}

return request;
}

//用于处理原始请求
@Override
public void setRequestResponse(HttpRequestResponse requestResponse)
{
this.requestResponse = requestResponse;

String urlDecoded = bodyStr;
ByteArray output;

try
{
//因为之前已经编写好了解密函数,现在我们直接解密就好了
urlDecoded = decryptECB(urlDecoded, "");

//output = ByteArray.byteArray(getPlain(urlDecoded, this.Sm4KeyCookieList[0]).getBytes(StandardCharsets.UTF_8));
//将解密的数据以UTF8的转换为二进制数组
output = byteArray(urlDecoded.getBytes(StandardCharsets.UTF_8));
}
catch (Exception e)
{
output = byteArray(urlDecoded);
}
//将数据录入到编辑器ui窗口中
this.requestEditor.setContents(output);
}

//用于判断处理是否开启该处理器
@Override
public boolean isEnabledFor(HttpRequestResponse requestResponse)
{
//判断在什么条件下会启用该窗口处理器,即请求url中包含该参数的请求并且请求不为OPTIONS方法
if (requestResponse.request().url().contains("10.208.126.93:18083") && !requestResponse.request().method().contains("OPTIONS")){
//将二进制数组的请求体转换为字符串格式并保存
bodyStr = requestResponse.request().body().toString();
//body为空时也不启用处理
if(bodyStr.equals("")) return false;
return true;
}
return false;
}

@Override
public String caption()
{
//返回插件的标签名
return TAG;
}

//添加一个ui窗口
@Override
public Component uiComponent()
{
return requestEditor.uiComponent();
}

//用于判断请求是否有数据
@Override
public Selection selectedData()
{
return requestEditor.selection().isPresent() ? requestEditor.selection().get() : null;
}

//用于判断请求是否被修改过
@Override
public boolean isModified()
{
return requestEditor.isModified();
}

}

3.5.响应编辑处理器

  • MyHttpResponseEditorProvider
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MyHttpResponseEditorProvider implements HttpResponseEditorProvider
{
private final MontoyaApi api;

MyHttpResponseEditorProvider(MontoyaApi api)
{
this.api = api;

}


@Override
public ExtensionProvidedHttpResponseEditor provideHttpResponseEditor(EditorCreationContext creationContext) {
return new MyExtensionProvidedHttpResponseEditor(api, creationContext);
}
}

  • MyExtensionProvidedHttpResponseEditor, 和请求处理器类似
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
class MyExtensionProvidedHttpResponseEditor implements ExtensionProvidedHttpResponseEditor
{
private final RawEditor responseEditor;
private HttpRequestResponse requestResponse;
private final MontoyaApi api;
private PersistedObject myExtensionData;
private final Logging logging;
private static final String STARTUP_SM4_KEY = "SM4_KEYS";

private static final String TAG = "Plain";
private String bodyStr;

MyExtensionProvidedHttpResponseEditor(MontoyaApi api, EditorCreationContext creationContext)
{
this.api = api;
logging = api.logging();
myExtensionData = api.persistence().extensionData();

if (creationContext.editorMode() == EditorMode.READ_ONLY)
{
responseEditor = api.userInterface().createRawEditor(EditorOptions.READ_ONLY, EditorOptions.WRAP_LINES);
}
else {
responseEditor = api.userInterface().createRawEditor(EditorOptions.WRAP_LINES);
}
}


@Override
public HttpResponse getResponse() {

return requestResponse.response();
}

@Override
public void setRequestResponse(HttpRequestResponse requestResponse)
{
this.requestResponse = requestResponse;

ByteArray output = null;
String responseBodyStr = bodyStr;

if (!responseBodyStr.isEmpty()){

try {
String data = decryptECB(responseBodyStr, "");
output = byteArray(data.getBytes(StandardCharsets.UTF_8));

logging.logToOutput("decrypt error:");
} catch (Exception e){
logging.logToError("SM4 decryption failed");
output = byteArray("have not key decrypt".getBytes(StandardCharsets.UTF_8));
}
//output = ByteArray.byteArray(getPlain(urlDecoded, this.Sm4KeyCookieList[0]).getBytes(StandardCharsets.UTF_8));

}else {
logging.logToError("keys is NULL? OR NOT HAVE ENCRYPTDATA: ");
output = byteArray(responseBodyStr.getBytes(StandardCharsets.UTF_8));
}


this.responseEditor.setContents(output);
}

@Override
public boolean isEnabledFor(HttpRequestResponse requestResponse)
{
if (requestResponse.request().url().contains("10.208.126.93:18083") && !requestResponse.request().method().contains("OPTIONS")){
ByteArray responseBody = requestResponse.response().body();
bodyStr = new String(responseBody.getBytes(), StandardCharsets.UTF_8);
if(bodyStr.equals("")) return false;
return true;
}
return false;
}

@Override
public String caption()
{
return TAG;
}

@Override
public Component uiComponent()
{
return responseEditor.uiComponent();
}

@Override
public Selection selectedData()
{
return responseEditor.selection().isPresent() ? responseEditor.selection().get() : null;
}

@Override
public boolean isModified()
{
return responseEditor.isModified();
}


}

3.6.伪造签名参数

  • SM3签名
1
2
3
4
5
public static String enSign(String data){
SM3 sm3 = SmUtil.sm3();
byte[] hash = sm3.digest(data.getBytes());
return HexUtil.encodeHexStr(hash);
}
  • MyHttpHandler
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
class MyHttpHandler implements HttpHandler {
private static final String STARTUP_SM4_KEY = "SM4_KEYS";
private final Logging logging;
//private final PersistedObject myExtensionData;

public MyHttpHandler(MontoyaApi api) {
this.logging = api.logging();
// myExtensionData = api.persistence().extensionData();
// myExtensionData.setString(STARTUP_SM4_KEY, "[]");
}


//响应参数配置,状态配置
@Override
public RequestToBeSentAction handleHttpRequestToBeSent(HttpRequestToBeSent requestToBeSent) {
//对窗口状态进行配置,获取annotations,可进行请求颜色标记,添加笔记等
Annotations annotations = requestToBeSent.annotations();
//获取整改请求参数
HttpRequest modifiedRequest = requestToBeSent;

// If the request is a post, ADD SM4KEY
//判断是否是EXTENSIONS和REPEATER组件中的请求,只有这两个组件中的请求才进行参数处理
if (requestToBeSent.toolSource().toolType().equals(EXTENSIONS) || requestToBeSent.toolSource().toolType().equals(REPEATER)) {
//判断是否是我们测试的网站
if (requestToBeSent.url().contains("http://10.208.126.93:18083/sqcgapi")) {
//判断是否为POST请求
if (isPost(requestToBeSent)) {
//根据逆向出的签名逻辑进行签名伪造
String bodyStr = requestToBeSent.bodyToString();
String url = requestToBeSent.url().replaceAll("http://10.208.126.93:18083/sqcgapi","");
String payloads = url + bodyStr + "-POST";
payloads = payloads.replaceAll("/", "");
String sign = enSign(payloads);
modifiedRequest = modifiedRequest.withUpdatedHeader("sign", sign);
logging.logToOutput("payloads: " + payloads);
logging.logToOutput("sign: " + sign);
}

}
}



//Return the modified request to burp with updated annotations.
return continueWith(modifiedRequest, annotations);
}

//响应参数获取,状态配置
@Override
public ResponseReceivedAction handleHttpResponseReceived(HttpResponseReceived responseReceived) {
Annotations annotations = responseReceived.annotations();
//Highlight all responses where the request had a Content-Length header.
if (responseHasContentLengthHeader(responseReceived)) {
//annotations = annotations.withHighlightColor(HighlightColor.BLUE);
}

return continueWith(responseReceived, annotations);
}

private static boolean isPost(HttpRequestToBeSent httpRequestToBeSent) {
return httpRequestToBeSent.method().equalsIgnoreCase("POST");
}

private static boolean responseHasContentLengthHeader(HttpResponseReceived httpResponseReceived) {
return httpResponseReceived.initiatingRequest().headers().stream().anyMatch(header -> header.name().equalsIgnoreCase("Content-Length"));
}
}

3.7.打包-导入

  • 清除-编译-打包

image-20250207102715544

  • 注意如果显示找不到主类,可进行如下配置

image-20250207103032711

  • 默认 点击确定即可

image-20250207103114662

  • 打包成功后会有两个jar包

image-20250207103148958

  • 导入插件

image-20250207103403088

  • 一定要选择包含依赖版本的jar包

image-20250207103445581

  • 导入成功后 会显示自己配置的日志输出。

image-20250207103528662

image-20250207103648936

4.总结

  • 其实只要有固定的模板,代码编写还是比较简单的。

  • 主要难在JS逆向部分,应用加密的不确定性,特别是同一权限集成系统之类的,代码量很大的话,我们很难定位到我们想要的位置,就无法逆向成功。

  • 加解密BP插件模板下载地址,(个人模板),里面的加密编写的 都是国密的,如果要用其他密码加密算法,自己导入自己写就可以了,代码逻辑都是一样的。

  • MyHttpHandler 是很重要的,这个类可以用来对请求参数进行特定化修改,例如需要伪造签名,或者更新时间戳之类的,都需要用到这个类。还有就是可以给特定特征的请求添加笔记记录和颜色高亮显示等。