Compare commits
239 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
980999a2f0 | ||
|
|
1236b9579e | ||
|
|
d561b6815d | ||
|
|
97d20bb7f5 | ||
|
|
74fa78db5e | ||
|
|
230cad7f91 | ||
|
|
1957599b07 | ||
|
|
5f26db2a9e | ||
|
|
692e26044c | ||
|
|
b597a9e6d9 | ||
|
|
fca804cb7c | ||
|
|
3cbf0591c6 | ||
|
|
186430bb35 | ||
|
|
56c7973261 | ||
|
|
d71965ce10 | ||
|
|
1ffe94e78d | ||
|
|
3859f81b2a | ||
|
|
20ae5bc811 | ||
|
|
b7734ca710 | ||
|
|
d75991043e | ||
|
|
95e1cb4dc1 | ||
|
|
baa7270f46 | ||
|
|
0b1d502f79 | ||
|
|
8c7ac8f47d | ||
|
|
ec4a10753f | ||
|
|
ed698b9861 | ||
|
|
c81094eb30 | ||
|
|
3608c3dca8 | ||
|
|
124e4c14fd | ||
|
|
6adf30f25c | ||
|
|
4fbd241ebe | ||
|
|
20afa30822 | ||
|
|
1a5ed2a6a3 | ||
|
|
1bf2b461ba | ||
|
|
79e2e58d48 | ||
|
|
bf0b7f0016 | ||
|
|
69c2b59c8c | ||
|
|
79655def48 | ||
|
|
116aec0848 | ||
|
|
c5de042b4b | ||
|
|
5bc592c6f9 | ||
|
|
be2df6472b | ||
|
|
4a53f20649 | ||
|
|
704e760912 | ||
|
|
3ccfee5a02 | ||
|
|
819ef820f8 | ||
|
|
0c795af101 | ||
|
|
5977e82ca6 | ||
|
|
452f297f55 | ||
|
|
a06ef8e25e | ||
|
|
7e53e250af | ||
|
|
b686b5e75e | ||
|
|
e83a6a1478 | ||
|
|
daacb2e146 | ||
|
|
1f1ca99f10 | ||
|
|
fa35b0a625 | ||
|
|
8ef98d20a9 | ||
|
|
e556abb6f7 | ||
|
|
471aab5ea1 | ||
|
|
76b475bd91 | ||
|
|
6014089594 | ||
|
|
910658f2e0 | ||
|
|
8692b0a494 | ||
|
|
5419d4a679 | ||
|
|
ae8cb2fd25 | ||
|
|
5b6bdbe5b6 | ||
|
|
ddb08e9a6e | ||
|
|
6a2f289d57 | ||
|
|
84746a7089 | ||
|
|
68f0bce619 | ||
|
|
4f0401347c | ||
|
|
a7e0a2a6ce | ||
|
|
b7c5a8363d | ||
|
|
d7b4419d51 | ||
|
|
5f54d1f461 | ||
|
|
e4b7f86a0c | ||
|
|
cc30f41bfa | ||
|
|
386c562311 | ||
|
|
a867039284 | ||
|
|
3a8d9eae11 | ||
|
|
e5f55b6c4c | ||
|
|
54973d9f4f | ||
|
|
fb347a8dc6 | ||
|
|
04b6652b03 | ||
|
|
6d4abae898 | ||
|
|
97172fab45 | ||
|
|
ba3b206acf | ||
|
|
99ed2cb2fd | ||
|
|
8a47f61caa | ||
|
|
ad323ba7a5 | ||
|
|
332b119064 | ||
|
|
ead03d42b9 | ||
|
|
4da3d3f42d | ||
|
|
3363ca25ed | ||
|
|
496d0d2174 | ||
|
|
f387834c4d | ||
|
|
ca773f368b | ||
|
|
a6cd01300b | ||
|
|
ba079ab1d8 | ||
|
|
a96dab6615 | ||
|
|
ad1a14b27e | ||
|
|
3a536a52de | ||
|
|
ea87c53958 | ||
|
|
e08b930fb5 | ||
|
|
49647d68d0 | ||
|
|
1c63841140 | ||
|
|
105c506039 | ||
|
|
f1941bccd7 | ||
|
|
d38e70523a | ||
|
|
1f7651c114 | ||
|
|
fc9a253d2b | ||
|
|
4cbcc1bcc4 | ||
|
|
765807de6e | ||
|
|
548315e163 | ||
|
|
d3ab207825 | ||
|
|
44260dd4ff | ||
|
|
cf3ac4978f | ||
|
|
9c8dad8ac0 | ||
|
|
5cd216e45d | ||
|
|
87c5f713fa | ||
|
|
a0946bb723 | ||
|
|
bcb5177b54 | ||
|
|
0225c00f69 | ||
|
|
eafae602b8 | ||
|
|
e56d8eb5d5 | ||
|
|
681cce0644 | ||
|
|
d43809e25f | ||
|
|
567dea6c60 | ||
|
|
8c388510c5 | ||
|
|
e22596819b | ||
|
|
d2cd7a0d03 | ||
|
|
67afe1f650 | ||
|
|
0602346249 | ||
|
|
953b966961 | ||
|
|
4c23d62576 | ||
|
|
6e9b8c8f37 | ||
|
|
ed58d891d5 | ||
|
|
33f5cab037 | ||
|
|
8b79c71df9 | ||
|
|
9ea0e4be9c | ||
|
|
41f197bcb2 | ||
|
|
31e419aed2 | ||
|
|
cf90a9366a | ||
|
|
6546446e4f | ||
|
|
6c4073c8ee | ||
|
|
1e1d51921d | ||
|
|
9135b8cbd2 | ||
|
|
cc7956d8dc | ||
|
|
405efdd5da | ||
|
|
0bb425f00b | ||
|
|
0bdff6fe28 | ||
|
|
6bd153d16a | ||
|
|
b12f9355fa | ||
|
|
fa9dcfc3d2 | ||
|
|
2e23388925 | ||
|
|
06fd54c9ce | ||
|
|
0707a773c8 | ||
|
|
d0f49f8e6c | ||
|
|
5404c90c00 | ||
|
|
e68619d1c2 | ||
|
|
dd08ffaaa2 | ||
|
|
d9aeda4cc3 | ||
|
|
e1c05ba10d | ||
|
|
6a17064b3a | ||
|
|
e698bb1caa | ||
|
|
a69503ca3d | ||
|
|
d590d4a70e | ||
|
|
548339fa58 | ||
|
|
df4496d4fd | ||
|
|
48e355ac54 | ||
|
|
b784aa1425 | ||
|
|
440b3b1504 | ||
|
|
a8f1798c7b | ||
|
|
225ee471ec | ||
|
|
5097124867 | ||
|
|
7e0e3054be | ||
|
|
17a84fc19e | ||
|
|
1573d563eb | ||
|
|
515f7b33f0 | ||
|
|
1dc510d576 | ||
|
|
f401214524 | ||
|
|
7ebba02200 | ||
|
|
93f5c73aac | ||
|
|
60b261d6ef | ||
|
|
c84ebf3a9d | ||
|
|
15f84028bb | ||
|
|
1238e536d1 | ||
|
|
5d23a68c0e | ||
|
|
d7f04526b4 | ||
|
|
acff96ed7b | ||
|
|
350c093162 | ||
|
|
0d3d4f88e9 | ||
|
|
37ca315aba | ||
|
|
241247a4a0 | ||
|
|
08bfb69fce | ||
|
|
a6d5f3a204 | ||
|
|
c4d8743fe3 | ||
|
|
903077c830 | ||
|
|
daddf15af2 | ||
|
|
e747011ec0 | ||
|
|
f7b2e99eb2 | ||
|
|
59cd0a88b9 | ||
|
|
b16cbf5b60 | ||
|
|
4ef766dd82 | ||
|
|
5d9f590977 | ||
|
|
5c326d3ca6 | ||
|
|
62edae0ab4 | ||
|
|
562378873c | ||
|
|
5f62e9653f | ||
|
|
3fc1869a7b | ||
|
|
43fbc46b65 | ||
|
|
00a4a835b2 | ||
|
|
f872dadf46 | ||
|
|
15bbb9f1a0 | ||
|
|
89f3f6cf09 | ||
|
|
cf9f434ff8 | ||
|
|
37a907d6df | ||
|
|
83e5da2f7e | ||
|
|
e43a96b8ad | ||
|
|
a7112ad297 | ||
|
|
efc7a2d7e3 | ||
|
|
0cfebb8464 | ||
|
|
2b4e56ea8c | ||
|
|
4bdb576448 | ||
|
|
e7a7f823df | ||
|
|
a0d33f8c58 | ||
|
|
72511d9f14 | ||
|
|
2336a49023 | ||
|
|
d89bec65e3 | ||
|
|
2239ca1e90 | ||
|
|
27fbf465b7 | ||
|
|
2879a1494c | ||
|
|
d9e0bf4714 | ||
|
|
170ed412b3 | ||
|
|
91009337c1 | ||
|
|
a0127d75a9 | ||
|
|
df0f4bd41b | ||
|
|
d0a61ba9ac | ||
|
|
78628b03d1 |
30
.github/ISSUE_TEMPLATE/问题反馈.md
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
name: 问题反馈
|
||||
about: 尽可能详细的描述问题并反馈
|
||||
title: "[BUG] 问题标题"
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## 使用环境
|
||||
|
||||
```
|
||||
HaE 版本:
|
||||
有无自定义规则:
|
||||
BurpSuite 版本:
|
||||
操作系统版本:
|
||||
是否阅读README:
|
||||
是否知晓注意事项:
|
||||
是否查阅历史ISSUE:
|
||||
```
|
||||
|
||||
## 问题详情
|
||||
|
||||
问题描述:
|
||||
|
||||
出现的场景:
|
||||
|
||||
## 解决建议
|
||||
|
||||
无。
|
||||
137
.gitignore
vendored
Normal file
@@ -0,0 +1,137 @@
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# AWS User-specific
|
||||
.idea/**/aws.xml
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
# .idea/artifacts
|
||||
# .idea/compiler.xml
|
||||
# .idea/jarRepositories.xml
|
||||
# .idea/modules.xml
|
||||
# .idea/*.iml
|
||||
# .idea/modules
|
||||
# *.iml
|
||||
# *.ipr
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# SonarLint plugin
|
||||
.idea/sonarlint/
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
### JetBrains+all Patch ###
|
||||
# Ignore everything but code style settings and run configurations
|
||||
# that are supposed to be shared within teams.
|
||||
|
||||
.idea/*
|
||||
|
||||
!.idea/codeStyles
|
||||
!.idea/runConfigurations
|
||||
|
||||
### macOS ###
|
||||
# General
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
### Gradle ###
|
||||
.gradle
|
||||
**/build/
|
||||
!src/**/build/
|
||||
|
||||
# Ignore Gradle GUI config
|
||||
gradle-app.setting
|
||||
|
||||
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
|
||||
!gradle-wrapper.jar
|
||||
|
||||
# Avoid ignore Gradle wrappper properties
|
||||
!gradle-wrapper.properties
|
||||
|
||||
# Cache of project
|
||||
.gradletasknamecache
|
||||
|
||||
# Eclipse Gradle plugin generated files
|
||||
# Eclipse Core
|
||||
.project
|
||||
# JDT-specific (Eclipse Java Development Tools)
|
||||
.classpath
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/macos,gradle,jetbrains+all
|
||||
201
LICENSE
Normal file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
161
README.md
@@ -1,99 +1,128 @@
|
||||
# HaE - Highlighter and Extractor
|
||||
<div align="center">
|
||||
<img src="images/logo.png" style="width: 20%" />
|
||||
<h4><a href="https://github.com/gh0stkey/HaE">Empower ethical hacker for efficient operations.</a></h4>
|
||||
<h5>First Author: <a href="https://github.com/gh0stkey">EvilChen</a><br>Second Author: <a href="https://github.com/0chencc">0chencc</a>(Mystery Security Team)<br>Third Author: <a href="https://github.com/vaycore">vaycore</a>(Independent Security Researcher)</h5>
|
||||
</div>
|
||||
|
||||
Note: My english is not very good, Thanks!
|
||||
README Version: \[[English](README.md) | [简体中文](README_CN.md)\]
|
||||
|
||||
Read Chinese simplified version ([README_zh](README_zh.md)).
|
||||
## Project Introduction
|
||||
|
||||
## Introduction
|
||||
**HaE** is a framework-style project in the field of **cybersecurity (data security)**, adopting a **Lego brick-style** modular design philosophy to achieve fine-grained tagging and extraction of HTTP messages (including WebSocket).
|
||||
|
||||
**HaE** is used to highlight HTTP requests and extract information from HTTP `response messages` or `request messages`.
|
||||
By utilizing **multi-engine** customized regular expressions, HaE can accurately match and process HTTP requests and response messages (including WebSocket), effectively tagging and extracting information from successfully matched content. This enhances the **efficiency of vulnerability and data analysis** in the field of cybersecurity (data security).
|
||||
|
||||

|
||||
> With the adoption of front-end and back-end separation development models in modern web applications, the amount of captured HTTP request traffic during routine vulnerability discovery has correspondingly increased. Fully assessing a web application often requires spending considerable time on irrelevant messages. **The emergence of HaE aims to address such situations**, by using HaE, you can **effectively reduce** testing time, focusing more effort on **valuable and meaningful** messages, thus **improving the efficiency of vulnerability discovery**.
|
||||
|
||||
The plugin can custom regular expression to match HTTP response messages. You can decide for yourself whether the corresponding request that meets the custom regular expression match needs to be highlighted and information extracted.
|
||||
GitHub project address: https://github.com/gh0stkey/HaE
|
||||
|
||||
**Note**: The use of HaE requires a basic regular expression foundation for testers. Since the Java regular expression library is not as elegant or convenient as Python, when using regular expressions, HaE requires users to use `()` to extract what they need The expression content contains; for example, if you want to match a response message of a Shiro application, the normal matching rule is `rememberMe=delete`, if you want to extract this content, you need to become `(rememberMe=delete)`.
|
||||
GitCode project address: https://gitcode.com/gh0stkey/HaE
|
||||
|
||||
## Instructions
|
||||
**Awards and Recognitions**:
|
||||
|
||||
Load: `Extender - Extensions - Add - Select File - Next`
|
||||
1. [Selected for the 2022 KCon Arsenal](https://mp.weixin.qq.com/s/JohMsl1WD29LHCHuLf8mVQ)
|
||||
2. [Recognized as a GitCode G-Star Project](https://gitcode.com/gh0stkey/HaE)
|
||||
|
||||
The configuration file is initialized when HaE is loaded for the first time. The default configuration file has a built-in regular expression: `Email`. The initialized configuration file will be placed in the same directory as the BurpSuite Jar package.
|
||||
**Notes and Precautions**:
|
||||
|
||||

|
||||
1. Starting with HaE version 3.0, development is done using the `Montoya API`. To use the new version of HaE, you need to upgrade your BurpSuite version (>=2023.12.1).
|
||||
2. Custom HaE rules must enclose the expressions to be extracted within parentheses `()`. For example, if you want to match a response message from a **Shiro application**, the normal matching rule would be `rememberMe=delete`, but in HaE's rule format, it needs to be written as `(rememberMe=delete)`.
|
||||
|
||||
In addition to the initial configuration file, there is `init.hae`, which is used to store the configuration file path; `HaE` supports custom configuration file paths, and you can select a custom configuration file by clicking the `Select File` button.
|
||||
## Usage
|
||||
|
||||

|
||||
**Plugin Installation**: `Extender - Extensions - Add - Select File - Next`
|
||||
|
||||
HaE supports three actions:
|
||||
When you load `HaE` for the first time, it will load the offline rule database from the Jar package. If you need to update the rules, click `Reinit` to reinitialize. The address of the built-in rule database can be found on GitHub:
|
||||
`https://github.com/gh0stkey/HaE/blob/master/src/main/resources/rules/Rules.yml`
|
||||
|
||||
1. Reload: It can be used when you do not use the HaE UI interface to modify the rules in the configuration file, but directly modify the rules based on the configuration file;
|
||||
2. New: Add a new rule will automatically add a row of table data, click or double-click to modify the data to automatically save;
|
||||
3. Delete: When you click to select a rule, press this button to delete the rule.
|
||||
The configuration file (`Config.yml`) and rule file (`Rules.yml`) are stored in a fixed directory:
|
||||
|
||||
**Note**: `HaE's operations` are based on the form UI, and all operations will be automatically saved.
|
||||
1. For Linux/Mac users: `~/.config/HaE/`
|
||||
2. For Windows users: `%USERPROFILE%/.config/HaE/`
|
||||
|
||||
## Plugin Advantages
|
||||
Alternatively, you can also place the configuration files in the `/.config/HaE/` directory under the same folder as the `HaE Jar package`, **for easier offline portability**.
|
||||
|
||||
1. Multi-option custom adaptation requirements;
|
||||
2. Multi-color classification (colors of BurpSuite): `red, orange, yellow, green, cyan, blue, pink, magenta, gray`;
|
||||
3. Color upgrade algorithm: **Two regulars expression, the colors are both orange, if the request are matched these, it will be upgraded to red.**
|
||||
4. The configuration file format uses JSON format, the format is
|
||||
```
|
||||
{name: {"loaded": isLoaded,"regex": regexText, "scope": request/response/any, "action": extract/highlight/any, "color": colorText}}
|
||||
```
|
||||
5. Built-in simple cache to reduce the stuttering phenomenon in the `multi-regular, big data scenario`.
|
||||
### Rule Definitions
|
||||
|
||||
## Actual Use
|
||||
Currently, HaE rules consist of 8 fields, with detailed meanings as follows:
|
||||
|
||||
Use RGPerson to generate test data and put it in the root directory file of the website:
|
||||
| Field | Meaning |
|
||||
| --------- | ------------------------------------------------------------ |
|
||||
| Name | Rule name, primarily used to briefly summarize the purpose of the current rule. |
|
||||
| F-Regex | Rule regex, mainly used for entering regular expressions. In HaE, any content that needs to be extracted and matched should be enclosed within `(` and `)`. |
|
||||
| S-Regex | Rule regex, with the same usage as F-Regex. S-Regex is a secondary regex, which can be used for further matching and extraction from the data results matched by F-Regex. Can be left empty if not needed. |
|
||||
| Format | Formatted output; in NFA engine regular expressions, we can use `{0}`, `{1}`, `{2}`... to format and output captured groups. By default, using `{0}` is sufficient. |
|
||||
| Scope | Rule scope, indicating which part of the HTTP message the current rule applies to. Supports request/response lines, headers, bodies, and complete messages. |
|
||||
| Engine | Regex engine, indicating which engine the current rule's regular expression uses. **DFA engine**: scans each character in the text string only once, fast speed, fewer features; **NFA engine**: repeatedly marks and unmarks characters, slower but richer features (e.g., grouping, replacement, splitting). |
|
||||
| Color | Match color, indicating the highlight color to mark when the current rule matches the corresponding HTTP message. HaE has a color upgrade algorithm that automatically upgrades the marking color when the same color appears. |
|
||||
| Sensitive | Case sensitivity, indicating whether the current rule is case-sensitive. If sensitive (`True`), it strictly matches the case; if insensitive (`False`), it does not consider case differences. |
|
||||
|
||||

|
||||
## Key Features and Advantages
|
||||
|
||||
Visit the address, you can see the highlighted request in the `Proxy-HTTP History`, and you can see the response tab contains the `MarkINFO` tag, which extracts the matched information.
|
||||
1. **Functionality**: By highlighting, annotating, and extracting information from HTTP messages, it helps users obtain meaningful insights, **focusing on high-value messages**.
|
||||
2. **Interface**: With a clear and visually intuitive design, and **simple interface interactions**, users can more easily understand and configure the project, **avoiding the complexity of a `multitude of buttons`**.
|
||||
3. **Query**: Highlights, annotations, and extracted information from HTTP messages are **centralized in a single data panel**, allowing for one-click queries and extraction of information, thereby improving testing and analysis efficiency.
|
||||
4. **Algorithm**: Built-in color upgrade algorithm automatically upgrades the marking color by one level when the same color appears, **preventing the scenario where `the dragon slayer becomes the dragon`**.
|
||||
5. **Management**: **Integrated with BurpSuite's project data management**, HaE data is stored along with BurpSuite project data when saving projects.
|
||||
6. **Practical Application**: The official rule library and rule field functionalities are **summarized and output based on real-world scenarios**, **thereby enhancing the effectiveness and accuracy of data discovery**.
|
||||
|
||||

|
||||
| Name | Display |
|
||||
| ------------------------ | ---------------------------------------------------- |
|
||||
| Rules | <img src="images/rules.png" style="width: 80%" /> |
|
||||
| Config | <img src="images/config.png" style="width: 80%" /> |
|
||||
| Databoard | <img src="images/databoard.png" style="width: 80%" /> |
|
||||
| MarkInfo | <img src="images/markinfo.png" style="width: 80%" /> |
|
||||
|
||||
## Appreciation List
|
||||
|
||||
## Regular Expression Optimization
|
||||
We appreciate everyone's support for the project. The following list is sorted based on the time of appreciation and is not in any particular order. If there are any omissions, please contact the project author for additions.
|
||||
|
||||
Some regular expression are not ideal in actual combat application scenarios.
|
||||
| ID | Amount |
|
||||
| -------- | -------- |
|
||||
| 毁三观大人 | 200.00 CNY |
|
||||
| ttt | 50.00 CNY |
|
||||
| C_soon5 | 66.66 CNY |
|
||||
| 1wtbb | 25.00 CNY |
|
||||
| Deep | 66.66 CNY |
|
||||
| NaTsUk0 | 50.00 CNY |
|
||||
| Kite | 48.00 CNY |
|
||||
| 红色键盘 | 99.99 CNY |
|
||||
| 曾哥 | 188.88 CNY |
|
||||
| 祝祝 | 488.00 CNY |
|
||||
| NOP Team | 200.00 CNY |
|
||||
| vaycore | 188.88 CNY |
|
||||
| xccc | 168.00 CNY |
|
||||
| 柯林斯-民间新秀 | 3288.8 CNY |
|
||||
| Cuber | 100.00 CNY |
|
||||
| 时光难逆 | 50.00 CNY |
|
||||
| Celvin | 150.88 CNY |
|
||||
| 呱呱 | 18.80 CNY |
|
||||
| 红炉点雪 | 50.00 CNY |
|
||||
| 王傑 | 100.00 CNY |
|
||||
| 联系不到我请拨打我手机号码 | 200.00 CNY |
|
||||
| Shu2e | 59.90 CNY |
|
||||
| 亦 | 50.00 CNY |
|
||||
| 是果实菌啊 | 38.88 CNY |
|
||||
| caytez | 77.77 CNY |
|
||||
| Sn0w33 | 18.88 CNY |
|
||||
| Edwater | 18.88 CNY |
|
||||
| 云中鹤 | 18.88 CNY |
|
||||
| Twit | 18.88 CNY |
|
||||
| cshu | 18.88 CNY |
|
||||
| Fzz2 | 50.00 CNY |
|
||||
|
||||
There will be some false positives when regular expression matching mobile phone numbers (pure numbers), the mobile phone number processing can be solved:
|
||||
## Support the Project
|
||||
|
||||
Original regular expression:
|
||||
If you find HaE useful, you can show your appreciation by donating to the author, giving them the motivation to continue updating and improving it!
|
||||
|
||||
```
|
||||
1[3-9]\d{9}
|
||||
```
|
||||
<div align=center>
|
||||
<img src="images/reward.jpeg" style="width: 30%" />
|
||||
</div>
|
||||
|
||||
False positive scenario: `12315188888888123`, it will match `15188888888`, but this paragraph is not a mobile phone number, so the modification rule is:
|
||||
## 404StarLink 2.0 - Galaxy
|
||||
|
||||
```
|
||||
[^0-9]+(1[3-9]\d{9})[^0-9]+
|
||||
```
|
||||

|
||||
|
||||
The mobile phone number required to be matched cannot be a number from 0-9.
|
||||
|
||||
|
||||
### Include Regular Expression List
|
||||
|
||||
Chinese ID-Number(From: https://github.com/gh0stkey/HaE/issues/3):
|
||||
|
||||
```
|
||||
[^0-9]([1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx])|([1-9]\d{5}\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{2}[0-9Xx])[^0-9]
|
||||
```
|
||||
|
||||
Email Address:
|
||||
|
||||
```
|
||||
([\w-]+(?:\.[\w-]+)*@(?:[\w](?:[\w-]*[\w])?\.)+[\w](?:[\w-]*[\w])?)
|
||||
```
|
||||
|
||||
Elasticsearch Unauthorized Access(From: https://github.com/gh0stkey/HaE/issues/7):
|
||||
|
||||
```
|
||||
("cluster_uuid"\s*:\s*"[A-Za-z0-9_-]{22}")
|
||||
```
|
||||
`HaE` is part of the 404Team's [Starlink Plan 2.0](https://github.com/knownsec/404StarLink2.0-Galaxy). If you have any questions about `HaE` or want to connect with other users, you can refer to the group joining methods provided by the Starlink Plan.
|
||||
|
||||
- [https://github.com/knownsec/404StarLink2.0-Galaxy#community](https://github.com/knownsec/404StarLink2.0-Galaxy#community)
|
||||
|
||||
128
README_CN.md
Normal file
@@ -0,0 +1,128 @@
|
||||
<div align="center">
|
||||
<img src="images/logo.png" style="width: 20%" />
|
||||
<h4><a href="https://github.com/gh0stkey/HaE">赋能白帽,高效作战!</a></h4>
|
||||
<h5>第一作者: <a href="https://github.com/gh0stkey">EvilChen</a><br>第二作者: <a href="https://github.com/0chencc">0chencc</a>(米斯特安全团队)<br>第三作者: <a href="https://github.com/vaycore">vaycore</a>(独立安全研究员)</h5>
|
||||
</div>
|
||||
|
||||
README 版本: \[[English](README.md) | [简体中文](README_CN.md)\]
|
||||
|
||||
## 项目介绍
|
||||
|
||||
**HaE**是一款**网络安全(数据安全)领域**下的框架式项目,采用了**乐高积木式**模块化设计理念,实现对HTTP消息(包含WebSocket)精细化的标记和提取。
|
||||
|
||||
通过运用**多引擎**的自定义正则表达式,HaE能够准确匹配并处理HTTP请求与响应报文(包含WebSocket),对匹配成功的内容进行有效的标记和信息抽取,从而提升网络安全(数据安全)领域下的**漏洞和数据分析效率**。
|
||||
|
||||
> 随着现代化Web应用采用前后端分离的开发模式,日常漏洞挖掘的过程中,捕获的HTTP请求流量也相应增加。若想全面评估一个Web应用,会花费大量时间在无用的报文上。**HaE的出现旨在解决这类情况**,借助HaE,您能够**有效减少**测试时间,将更多精力集中在**有价值且有意义**的报文上,从而**提高漏洞挖掘效率**。
|
||||
|
||||
GitHub项目地址:https://github.com/gh0stkey/HaE
|
||||
|
||||
GitCode项目地址:https://gitcode.com/gh0stkey/HaE
|
||||
|
||||
**所获荣誉**:
|
||||
|
||||
1. [入选2022年KCon兵器谱](https://mp.weixin.qq.com/s/JohMsl1WD29LHCHuLf8mVQ)
|
||||
2. [入选GitCode G-Star项目](https://gitcode.com/gh0stkey/HaE)
|
||||
|
||||
**注意事项**:
|
||||
|
||||
1. HaE 3.0版本开始采用`Montoya API`进行开发,使用新版HaE需要升级你的BurpSuite版本(>=2023.12.1)。
|
||||
2. 自定义HaE规则必须用左右括号`()`将所需提取的表达式内容包含,例如你要匹配一个**Shiro应用**的响应报文,正常匹配规则为`rememberMe=delete`,在HaE的规则中就需要变成`(rememberMe=delete)`。
|
||||
|
||||
## 使用方法
|
||||
|
||||
插件装载: `Extender - Extensions - Add - Select File - Next`
|
||||
|
||||
初次装载`HaE`会从Jar包中加载离线的规则库,如果更新可以点击`Reinit`进行重新初始化。内置规则库地址可以在Github上找到:`https://github.com/gh0stkey/HaE/blob/master/src/main/resources/rules/Rules.yml`。
|
||||
|
||||
配置文件(`Config.yml`)和规则文件(`Rules.yml`)会放在固定目录下:
|
||||
|
||||
1. Linux/Mac用户的配置文件目录:`~/.config/HaE/`
|
||||
2. Windows用户的配置文件目录:`%USERPROFILE%/.config/HaE/`
|
||||
|
||||
除此之外,您也可以选择将配置文件存放在`HaE Jar包`的同级目录下的`/.config/HaE/`中,**以便于离线携带**。
|
||||
|
||||
### 规则释义
|
||||
|
||||
HaE目前的规则一共有8个字段,详细的含义如下所示:
|
||||
|
||||
| 字段 | 含义 |
|
||||
|-----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| Name | 规则名称,主要用于简短概括当前规则的作用。 |
|
||||
| F-Regex | 规则正则,主要用于填写正则表达式。在HaE中所需提取匹配的内容需要用`(`、`)`将正则表达式进行包裹。|
|
||||
| S-Regex | 规则正则,作用及使用同F-Regex。S-Regex为二次正则,可以用于对F-Regex匹配的数据结果进行二次的匹配提取,如不需要的情况下可以留空。|
|
||||
| Format | 格式化输出,在NFA引擎的正则表达式中,我们可以通过`{0}`、`{1}`、`{2}`…的方式进行取分组格式化输出。默认情况下使用`{0}`即可。 |
|
||||
| Scope | 规则作用域,主要用于表示当前规则作用于HTTP报文的哪个部分。支持请求、响应的行、头、体,以及完整的报文。 |
|
||||
| Engine | 正则引擎,主要用于表示当前规则的正则表达式所使用的引擎。**DFA引擎**:对于文本串里的每一个字符只需扫描一次,速度快、特性少;**NFA引擎**:要翻来覆去标注字符、取消标注字符,速度慢,但是特性(如:分组、替换、分割)丰富。 |
|
||||
| Color | 规则匹配颜色,主要用于表示当前规则匹配到对应HTTP报文时所需标记的高亮颜色。在HaE中具备颜色升级算法,当出现相同颜色时会自动向上升级一个颜色进行标记。 |
|
||||
| Sensitive | 规则敏感性,主要用于表示当前规则对于大小写字母是否敏感,敏感(`True`)则严格按照大小写要求匹配,不敏感(`False`)则反之。 |
|
||||
|
||||
## 优势特点
|
||||
|
||||
1. **功能**:通过对HTTP报文的颜色高亮、注释和提取,帮助使用者获取有意义的信息,**聚焦高价值报文**。
|
||||
2. **界面**:清晰可视的界面设计,以及**简洁的界面交互**,帮助使用者更轻松的了解和配置项目,**避免`多按钮`式的复杂体验**。
|
||||
3. **查询**:将HTTP报文的高亮、注释和提取到的相关信息**集中在一个数据面板**,可以一键查询、提取信息,从而提高测试和梳理效率。
|
||||
4. **算法**:内置高亮颜色的升级算法,当出现相同颜色时**会自动向上升级一个颜色**进行标记,**避免`屠龙者终成恶龙`场景**。
|
||||
5. **管理**:**融入BurpSuite的项目数据管理**,当使用BurpSuite进行项目存储时HaE数据也会一并存储。
|
||||
6. **实战**:官方规则库和规则字段作用功能,都是**基于实战化场景总结输出**的,**以此提高数据的有效性、精准性发现**。
|
||||
|
||||
| 界面名称 | 界面展示 |
|
||||
| ------------------------ | ---------------------------------------------------- |
|
||||
| Rules(规则管理) | <img src="images/rules.png" style="width: 80%" /> |
|
||||
| Config(配置管理) | <img src="images/config.png" style="width: 80%" /> |
|
||||
| Databoard(数据集合) | <img src="images/databoard.png" style="width: 80%" /> |
|
||||
| MarkInfo(数据展示) | <img src="images/markinfo.png" style="width: 80%" /> |
|
||||
|
||||
## 赞赏榜单
|
||||
|
||||
感谢各位对项目的赞赏,以下名单基于赞赏时间进行排序,不分先后,如有遗留可联系项目作者进行补充。
|
||||
|
||||
| ID | Amount |
|
||||
| -------- | -------- |
|
||||
| 毁三观大人 | 200.00 元 |
|
||||
| ttt | 50.00 元 |
|
||||
| C_soon5 | 66.66 元 |
|
||||
| 1wtbb | 25.00 元 |
|
||||
| Deep | 66.66 元 |
|
||||
| NaTsUk0 | 50.00 元 |
|
||||
| Kite | 48.00 元 |
|
||||
| 红色键盘 | 99.99 元 |
|
||||
| 曾哥 | 188.88 元 |
|
||||
| 祝祝 | 488.00 元 |
|
||||
| NOP Team | 200.00 元 |
|
||||
| vaycore | 188.88 元 |
|
||||
| xccc | 168.00 元 |
|
||||
| 柯林斯-民间新秀 | 3288.8 元 |
|
||||
| Cuber | 100.00 元 |
|
||||
| 时光难逆 | 50.00 元 |
|
||||
| Celvin | 150.88 元 |
|
||||
| 呱呱 | 18.80 元 |
|
||||
| 红炉点雪 | 50.00 元 |
|
||||
| 王傑 | 100.00 元 |
|
||||
| 联系不到我请拨打我手机号码 | 200.00 元 |
|
||||
| Shu2e | 59.90 元 |
|
||||
| 亦 | 50.00 元 |
|
||||
| 是果实菌啊 | 38.88 元 |
|
||||
| caytez | 77.77 元 |
|
||||
| Sn0w33 | 18.88 元 |
|
||||
| Edwater | 18.88 元 |
|
||||
| 云中鹤 | 18.88 元 |
|
||||
| Twit | 18.88 元 |
|
||||
| cshu | 18.88 元 |
|
||||
| Fzz2 | 50.00 元 |
|
||||
|
||||
|
||||
## 支持项目
|
||||
|
||||
如果你觉得HaE好用,可以打赏一下作者,给作者持续更新下去的动力!
|
||||
|
||||
<div align=center>
|
||||
<img src="images/reward.jpeg" style="width: 30%" />
|
||||
</div>
|
||||
|
||||
## 404StarLink 2.0 - Galaxy
|
||||
|
||||

|
||||
|
||||
`HaE` 是 404Team [星链计划2.0](https://github.com/knownsec/404StarLink2.0-Galaxy) 中的一环,如果对 `HaE` 有任何疑问又或是想要找小伙伴交流,可以参考星链计划的加群方式。
|
||||
|
||||
- [https://github.com/knownsec/404StarLink2.0-Galaxy#community](https://github.com/knownsec/404StarLink2.0-Galaxy#community)
|
||||
108
README_zh.md
@@ -1,108 +0,0 @@
|
||||
# HaE - Highlighter and Extractor
|
||||
|
||||
## 介绍
|
||||
|
||||
**HaE**是基于 `BurpSuite` 插件 `JavaAPI` 开发的请求高亮标记与信息提取的辅助型插件。
|
||||
|
||||

|
||||
|
||||
该插件可以通过自定义正则的方式匹配**响应报文或请求报文**,可以自行决定符合该自定义正则匹配的相应请求是否需要高亮标记、信息提取。
|
||||
|
||||
**注**: `HaE`的使用,对测试人员来说需要基本的正则表达式基础,由于`Java`正则表达式的库并没有`Python`的优雅或方便,在使用正则的,HaE要求使用者必须使用`()`将所需提取的表达式内容包含;例如你要匹配一个**Shiro应用**的响应报文,正常匹配规则为`rememberMe=delete`,如果你要提取这段内容的话就需要变成`(rememberMe=delete)`。
|
||||
|
||||
## 使用方法
|
||||
|
||||
插件装载: `Extender - Extensions - Add - Select File - Next`
|
||||
|
||||
初次装载`HaE`会初始化配置文件,默认配置文件内置一个正则: `Email`,初始化的配置文件会放在与`BurpSuite Jar`包同级目录下。
|
||||
|
||||

|
||||
|
||||
除了初始化的配置文件外,还有`init.hae`,该文件用于存储配置文件路径;`HaE`支持自定义配置文件路径,你可以通过点击`Select File`按钮进行选择自定义配置文件。
|
||||
|
||||

|
||||
|
||||
HaE支持三个动作:
|
||||
|
||||
1. 重载规则(Reload): 当你不使用HaE UI界面去修改配置文件内的规则时,而是直接基于配置文件进行修改规则时可使用;
|
||||
2. 新建规则(New): 新建规则会自动添加一行表格数据,单击或双击进行修改数据即可自动保存;
|
||||
3. 删除规则(Delete): 单击选中某条规则时,按下该按钮即可删除规则。
|
||||
|
||||
**注**: HaE的操作都是基于表单UI的方式,操作即会自动保存。
|
||||
|
||||
## 插件优点
|
||||
|
||||
1. 多选项自定义控制适配需求;
|
||||
2. 多颜色高亮分类,将BurpSuite的所有高亮颜色集成: `red, orange, yellow, green, cyan, blue, pink, magenta, gray`;
|
||||
3. 颜色升级算法: 利用下标的方式进行优先级排序,当满足2个同颜色条件则以优先级顺序上升颜色。(例如: **两个正则,颜色为橘黄色,该请求两个正则都匹配到了,那么将升级为红色**)
|
||||
4. 简单的配置文件格式选用JSON格式,格式为
|
||||
```
|
||||
{name: {"loaded": isLoaded,"regex": regexText, "scope": request/response/any, "action": extract/highlight/any, "color": colorText}}
|
||||
```
|
||||
5. 内置简单缓存,在“多正则、大数据”的场景下减少卡顿现象。
|
||||
|
||||
## 实际使用
|
||||
|
||||
使用 RGPerson 生成测试数据,放入网站根目录文件中:
|
||||
|
||||

|
||||
|
||||
访问该地址,在`Proxy - HTTP History`中可以看见高亮请求,响应标签页中含有`MarkINFO`标签,其中将匹配到的信息提取了出来。
|
||||
|
||||

|
||||
|
||||
|
||||
## 正则优化
|
||||
|
||||
有些正则在实战应用场景中并不理想
|
||||
|
||||
在正则匹配手机号、身份证号码的时候(纯数字类)会存在一些误报(这里匹配身份证号码无法进行校验,误报率很高),但手机号处理这一块可以解决:
|
||||
|
||||
原正则:
|
||||
|
||||
```
|
||||
1[3-9]\d{9}
|
||||
```
|
||||
|
||||
误报场景: `12315188888888123`,这时候会匹配到`15188888888`,而实际上这一段并不是手机号,所以修改正则为:
|
||||
|
||||
```
|
||||
[^0-9]+(1[3-9]\d{9})[^0-9]+
|
||||
```
|
||||
|
||||
也就是要求匹配的手机号前后不能为0-9的数字。
|
||||
|
||||
## 实战用法
|
||||
|
||||
1. CMS指纹识别,Discuz正则: `(Powered by Discuz!)`
|
||||
2. OSS对象存储信息泄露,正则: `([A|a]ccess[K|k]ey[I|i]d|[A|a]ccess[K|k]ey[S|s]ecret)`
|
||||
3. 内网地址信息提取,正则: `(?:10\.\d{1,3}\.\d{1,3}\.\d{1,3})|(?:172\.(?:(?:1[6-9])|(?:2\d)|(?:3[01]))\.\d{1,3}\.\d{1,3})|(?:192\.168\.\d{1,3}\.\d{1,3})`
|
||||
4. 实战插件关联搭配,漏洞挖掘案例: https://mp.weixin.qq.com/s/5vNn7dMRZBtv0ojPBAHV7Q
|
||||
|
||||
...还有诸多使用方法等待大家去发掘。
|
||||
|
||||
## 文末
|
||||
|
||||
随笔: 正义感是一个不可丢失的东西。
|
||||
|
||||
Github项目地址(BUG、需求、正则欢迎提交): https://github.com/gh0stkey/HaE
|
||||
|
||||
### 收录正则列表
|
||||
|
||||
身份证号码(来自: https://github.com/gh0stkey/HaE/issues/3):
|
||||
|
||||
```
|
||||
[^0-9]([1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx])|([1-9]\d{5}\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{2}[0-9Xx])[^0-9]
|
||||
```
|
||||
|
||||
邮箱地址:
|
||||
|
||||
```
|
||||
([\w-]+(?:\.[\w-]+)*@(?:[\w](?:[\w-]*[\w])?\.)+[\w](?:[\w-]*[\w])?)
|
||||
```
|
||||
|
||||
Elasticsearch未授权访问匹配(来自: https://github.com/gh0stkey/HaE/issues/7):
|
||||
|
||||
```
|
||||
("cluster_uuid"\s*:\s*"[A-Za-z0-9_-]{22}")
|
||||
```
|
||||
36
build.gradle
@@ -1,25 +1,37 @@
|
||||
apply plugin: 'java'
|
||||
plugins {
|
||||
id 'java'
|
||||
}
|
||||
|
||||
sourceCompatibility = 17
|
||||
targetCompatibility = 17
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile 'net.portswigger.burp.extender:burp-extender-api:1.7.13'
|
||||
compile 'net.sourceforge.jregex:jregex:1.2_01'
|
||||
compile 'org.json:json:20200518'
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
java {
|
||||
srcDir '.'
|
||||
srcDir './src/main/java'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task fatJar(type: Jar) {
|
||||
baseName = project.name + '-all'
|
||||
from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
|
||||
with jar
|
||||
dependencies {
|
||||
implementation 'net.portswigger.burp.extensions:montoya-api:2023.12.1'
|
||||
implementation 'org.yaml:snakeyaml:2.0'
|
||||
implementation 'dk.brics.automaton:automaton:1.11-8'
|
||||
implementation 'com.github.ben-manes.caffeine:caffeine:3.1.8'
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
jar {
|
||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||
|
||||
from {
|
||||
configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
|
||||
}
|
||||
}
|
||||
@@ -1,426 +0,0 @@
|
||||
package burp;
|
||||
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Color;
|
||||
import java.awt.Component;
|
||||
import java.util.*;
|
||||
|
||||
import org.json.*;
|
||||
|
||||
import burp.action.DoAction;
|
||||
import burp.action.ExtractContent;
|
||||
import burp.action.MatchHTTP;
|
||||
import burp.color.GetColorKey;
|
||||
import burp.color.UpgradeColor;
|
||||
import burp.file.FileExists;
|
||||
import burp.file.ReadFile;
|
||||
import burp.file.RemoveContent;
|
||||
import burp.file.WriteFile;
|
||||
import burp.ui.FillTable;
|
||||
|
||||
import javax.swing.JFrame;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.DefaultCellEditor;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.JTable;
|
||||
import javax.swing.JTextField;
|
||||
import javax.swing.table.DefaultTableModel;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JButton;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.io.File;
|
||||
import java.awt.event.ActionEvent;
|
||||
import javax.swing.JCheckBox;
|
||||
import javax.swing.JComboBox;
|
||||
import javax.swing.JFileChooser;
|
||||
import javax.swing.border.EtchedBorder;
|
||||
import javax.swing.border.TitledBorder;
|
||||
import javax.swing.event.TableModelEvent;
|
||||
import javax.swing.event.TableModelListener;
|
||||
import javax.swing.SwingConstants;
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.JLabel;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
|
||||
public class BurpExtender implements IBurpExtender, IHttpListener, IMessageEditorTabFactory, ITab {
|
||||
|
||||
private JFrame frame;
|
||||
private JPanel panel;
|
||||
private JTable table;
|
||||
private JTextField textField;
|
||||
private IBurpExtenderCallbacks callbacks;
|
||||
private static IExtensionHelpers helpers;
|
||||
private static String configFilePath = "config.json";
|
||||
private static String initFilePath = "init.hae";
|
||||
private static IMessageEditorTab HaETab;
|
||||
private static PrintWriter stdout;
|
||||
|
||||
ReadFile rf = new ReadFile();
|
||||
WriteFile wfc = new WriteFile();
|
||||
FileExists fe = new FileExists();
|
||||
RemoveContent rc = new RemoveContent();
|
||||
GetColorKey gck = new GetColorKey();
|
||||
UpgradeColor uc = new UpgradeColor();
|
||||
ExtractContent ec = new ExtractContent();
|
||||
MatchHTTP mh = new MatchHTTP();
|
||||
FillTable ft = new FillTable();
|
||||
DoAction da = new DoAction();
|
||||
|
||||
@Override
|
||||
public void registerExtenderCallbacks(final IBurpExtenderCallbacks callbacks)
|
||||
{
|
||||
this.callbacks = callbacks;
|
||||
this.helpers = callbacks.getHelpers();
|
||||
// 设置插件名字和版本
|
||||
String version = "1.5";
|
||||
|
||||
callbacks.setExtensionName(String.format("HaE (%s) - Highlighter and Extractor", version));
|
||||
|
||||
// 定义输出
|
||||
stdout = new PrintWriter(callbacks.getStdout(), true);
|
||||
stdout.println("@Author: EvilChen");
|
||||
stdout.println("@Blog: cn.gh0st.cn");
|
||||
|
||||
// UI
|
||||
SwingUtilities.invokeLater(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// 判断"config.json"文件是否具备内容,如若不具备则进行初始化
|
||||
if (configFilePath.equals("config.json")) {
|
||||
if (rf.readFileContent(configFilePath).equals("")) {
|
||||
wfc.writeFileContent(configFilePath, Config.initConfigContent);
|
||||
wfc.writeFileContent(initFilePath, configFilePath);
|
||||
}
|
||||
}
|
||||
// 判断配置文件是否存在
|
||||
if (fe.fileExists(configFilePath)) {
|
||||
configFilePath = rf.readFileContent(initFilePath);
|
||||
} else {
|
||||
JOptionPane.showMessageDialog(null, "Config File Not Found!", "Error", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
|
||||
initialize();
|
||||
ft.fillTable(configFilePath, table);
|
||||
|
||||
}
|
||||
});
|
||||
callbacks.registerHttpListener(BurpExtender.this);
|
||||
callbacks.registerMessageEditorTabFactory(BurpExtender.this);
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
frame = new JFrame();
|
||||
frame.setBounds(100, 100, 526, 403);
|
||||
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||
|
||||
panel = new JPanel();
|
||||
frame.getContentPane().add(panel, BorderLayout.CENTER);
|
||||
panel.setLayout(new BorderLayout(0, 0));
|
||||
|
||||
JPanel panel_3 = new JPanel();
|
||||
panel.add(panel_3, BorderLayout.NORTH);
|
||||
|
||||
JLabel lblNewLabel_1 = new JLabel("Config File:");
|
||||
panel_3.add(lblNewLabel_1);
|
||||
|
||||
textField = new JTextField();
|
||||
textField.setEditable(false);
|
||||
panel_3.add(textField);
|
||||
textField.setColumns(20);
|
||||
|
||||
textField.setText(configFilePath);
|
||||
|
||||
JButton btnNewButton = new JButton("Select File ...");
|
||||
btnNewButton.addActionListener(new ActionListener() {
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
JFileChooser jfc = new JFileChooser();
|
||||
jfc.setFileSelectionMode(JFileChooser.FILES_ONLY);
|
||||
jfc.showDialog(new JLabel(), "Choose");
|
||||
File file = jfc.getSelectedFile();
|
||||
textField.setText(file.getAbsolutePath());
|
||||
configFilePath = textField.getText();
|
||||
wfc.writeFileContent(initFilePath, configFilePath);
|
||||
ft.fillTable(configFilePath, table);
|
||||
}
|
||||
});
|
||||
panel_3.add(btnNewButton);
|
||||
|
||||
JPanel panel_2 = new JPanel();
|
||||
panel.add(panel_2, BorderLayout.CENTER);
|
||||
panel_2.setLayout(new BorderLayout(0, 0));
|
||||
|
||||
JPanel panel_1 = new JPanel();
|
||||
panel_2.add(panel_1, BorderLayout.NORTH);
|
||||
panel_1.setBorder(new TitledBorder(new EtchedBorder(EtchedBorder.LOWERED, null, null), "Actions", TitledBorder.LEADING, TitledBorder.TOP, null, new Color(0, 0, 0)));
|
||||
|
||||
JButton btnReloadRule = new JButton("Reload");
|
||||
btnReloadRule.addActionListener(new ActionListener() {
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
ft.fillTable(configFilePath, table);
|
||||
}
|
||||
});
|
||||
panel_1.add(btnReloadRule);
|
||||
|
||||
JButton btnNewRule = new JButton("New");
|
||||
btnNewRule.addActionListener(new ActionListener() {
|
||||
public void actionPerformed(ActionEvent arg0) {
|
||||
DefaultTableModel dtm = (DefaultTableModel) table.getModel();
|
||||
Vector rules = new Vector();
|
||||
rules.add(true);
|
||||
rules.add("New Rule");
|
||||
rules.add("New Regex");
|
||||
rules.add("red");
|
||||
rules.add("response");
|
||||
rules.add("any");
|
||||
dtm.addRow(rules);
|
||||
}
|
||||
});
|
||||
panel_1.add(btnNewRule);
|
||||
|
||||
JButton btnDeleteRule = new JButton("Delete");
|
||||
btnDeleteRule.addActionListener(new ActionListener() {
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
int selectRows = table.getSelectedRows().length;
|
||||
DefaultTableModel dtm = (DefaultTableModel) table.getModel();
|
||||
if (selectRows == 1) {
|
||||
int selectedRowIndex = table.getSelectedRow();
|
||||
// 在配置文件中删除数据
|
||||
String cellValue = (String) dtm.getValueAt(selectedRowIndex, 1);
|
||||
// System.out.println(cellValue);
|
||||
rc.removeFileContent(cellValue, configFilePath);
|
||||
// 在表格中删除数据
|
||||
dtm.removeRow(selectedRowIndex);
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
panel_1.add(btnDeleteRule);
|
||||
|
||||
JScrollPane scrollPane = new JScrollPane();
|
||||
panel_2.add(scrollPane, BorderLayout.CENTER);
|
||||
|
||||
table = new JTable();
|
||||
table.setModel(new DefaultTableModel(
|
||||
new Object[][] {
|
||||
},
|
||||
new String[] {
|
||||
"Loaded", "Name", "Regex", "Color", "Scope", "Action"
|
||||
}
|
||||
));
|
||||
scrollPane.setViewportView(table);
|
||||
|
||||
table.getColumnModel().getColumn(2).setPreferredWidth(172);
|
||||
table.getColumnModel().getColumn(3).setCellEditor(new DefaultCellEditor(new JComboBox(Config.colorArray)));
|
||||
table.getColumnModel().getColumn(0).setCellEditor(new DefaultCellEditor(new JCheckBox()));
|
||||
table.getColumnModel().getColumn(4).setCellEditor(new DefaultCellEditor(new JComboBox(Config.scopeArray)));
|
||||
table.getColumnModel().getColumn(5).setCellEditor(new DefaultCellEditor(new JComboBox(Config.actionArray)));
|
||||
|
||||
JLabel lblNewLabel = new JLabel("@EvilChen Love YuChen.");
|
||||
lblNewLabel.setHorizontalAlignment(SwingConstants.CENTER);
|
||||
panel.add(lblNewLabel, BorderLayout.SOUTH);
|
||||
|
||||
table.getModel().addTableModelListener(
|
||||
new TableModelListener() {
|
||||
@Override
|
||||
public void tableChanged(TableModelEvent e) {
|
||||
if (e.getType() == TableModelEvent.INSERT || e.getType() == TableModelEvent.UPDATE) {
|
||||
DefaultTableModel dtm = (DefaultTableModel) table.getModel();
|
||||
int rows = dtm.getRowCount();
|
||||
JSONObject jsonObj = new JSONObject();
|
||||
|
||||
for (int i = 0; i < rows; i++) {
|
||||
JSONObject jsonObj1 = new JSONObject();
|
||||
jsonObj1.put("loaded", (boolean) dtm.getValueAt(i, 0));
|
||||
jsonObj1.put("regex", (String) dtm.getValueAt(i, 2));
|
||||
jsonObj1.put("color", (String) dtm.getValueAt(i, 3));
|
||||
jsonObj1.put("scope", (String) dtm.getValueAt(i, 4));
|
||||
jsonObj1.put("action", (String) dtm.getValueAt(i, 5));
|
||||
// 添加数据
|
||||
jsonObj.put((String) dtm.getValueAt(i, 1), jsonObj1);
|
||||
}
|
||||
|
||||
wfc.writeFileContent(configFilePath, jsonObj.toString());
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
callbacks.customizeUiComponent(panel);
|
||||
callbacks.customizeUiComponent(panel_1);
|
||||
callbacks.customizeUiComponent(panel_2);
|
||||
callbacks.customizeUiComponent(panel_3);
|
||||
callbacks.customizeUiComponent(scrollPane);
|
||||
callbacks.addSuiteTab(BurpExtender.this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IMessageEditorTab createNewInstance(IMessageEditorController controller, boolean editable) {
|
||||
HaETab = new MarkInfoTab(controller, editable);
|
||||
return HaETab;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTabCaption() {
|
||||
return "HaE";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getUiComponent() {
|
||||
return panel;
|
||||
}
|
||||
|
||||
/*
|
||||
* 使用processHttpMessage用来做Highlighter
|
||||
*/
|
||||
@Override
|
||||
public void processHttpMessage(int toolFlag, boolean messageIsRequest, IHttpRequestResponse messageInfo) {
|
||||
// 判断是否是响应,且该代码作用域为:REPEATER、INTRUDER、PROXY(分别对应toolFlag 64、32、4)
|
||||
if (toolFlag == 64 || toolFlag == 32 || toolFlag == 4) {
|
||||
JSONObject jsonObj = new JSONObject();
|
||||
byte[] content = messageInfo.getRequest();
|
||||
// 流量清洗
|
||||
String urlString = helpers.analyzeRequest(messageInfo.getHttpService(), content).getUrl().toString();
|
||||
urlString = urlString.indexOf("?") > 0 ? urlString.substring(0, urlString.indexOf("?")) : urlString;
|
||||
// 正则判断
|
||||
if (mh.matchSuffix(urlString)) {
|
||||
return;
|
||||
}
|
||||
if (messageIsRequest) {
|
||||
try {
|
||||
String c = new String(content, "UTF-8").intern();
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
jsonObj = ec.matchRegex(content, "request", "highlight", configFilePath);
|
||||
} else {
|
||||
content = messageInfo.getResponse();
|
||||
// 流量清洗
|
||||
List<String> mimeList = helpers.analyzeResponse(content).getHeaders();
|
||||
// 正则判断
|
||||
if (mh.matchMIME(mimeList)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String c = new String(content, "UTF-8").intern();
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
jsonObj = ec.matchRegex(content, "response", "highlight", configFilePath);
|
||||
}
|
||||
|
||||
List<String> colorList = da.highlightList(jsonObj);
|
||||
if (colorList.size() != 0) {
|
||||
String color = uc.getEndColor(gck.getColorKeys(colorList, Config.colorArray), Config.colorArray);;
|
||||
messageInfo.setHighlight(color);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class MarkInfoTab implements IMessageEditorTab {
|
||||
private ITextEditor markInfoText;
|
||||
private byte[] currentMessage;
|
||||
private final IMessageEditorController controller;
|
||||
private byte[] extractRequestContent;
|
||||
private byte[] extractResponseContent;
|
||||
|
||||
public MarkInfoTab(IMessageEditorController controller, boolean editable) {
|
||||
this.controller = controller;
|
||||
markInfoText = callbacks.createTextEditor();
|
||||
markInfoText.setEditable(editable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTabCaption() {
|
||||
return "MarkInfo";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getUiComponent() {
|
||||
return markInfoText.getComponent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled(byte[] content, boolean isRequest) {
|
||||
try {
|
||||
// 流量清洗
|
||||
String urlString = helpers.analyzeRequest(controller.getHttpService(), controller.getRequest()).getUrl().toString();
|
||||
urlString = urlString.indexOf("?") > 0 ? urlString.substring(0, urlString.indexOf("?")) : urlString;
|
||||
// 正则判断
|
||||
if (mh.matchSuffix(urlString)) {
|
||||
return false;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if (isRequest) {
|
||||
JSONObject jsonObj = ec.matchRegex(content, "request", "extract", configFilePath);
|
||||
if (jsonObj.length() != 0) {
|
||||
String result = da.extractString(jsonObj);
|
||||
extractRequestContent = result.getBytes();
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
// 流量清洗
|
||||
List<String> mimeList = helpers.analyzeResponse(controller.getResponse()).getHeaders();
|
||||
// 正则判断
|
||||
if (mh.matchMIME(mimeList)) {
|
||||
return false;
|
||||
}
|
||||
JSONObject jsonObj = ec.matchRegex(content, "response", "extract", configFilePath);
|
||||
if (jsonObj.length() != 0) {
|
||||
String result = da.extractString(jsonObj);
|
||||
extractResponseContent = result.getBytes();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getMessage() {
|
||||
return currentMessage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isModified() {
|
||||
return markInfoText.isTextModified();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getSelectedData() {
|
||||
return markInfoText.getSelectedText();
|
||||
}
|
||||
|
||||
/*
|
||||
* 使用setMessage用来做Extractor
|
||||
*/
|
||||
@Override
|
||||
public void setMessage(byte[] content, boolean isRequest) {
|
||||
try {
|
||||
String c = new String(content, "UTF-8").intern();
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
if (content.length > 0) {
|
||||
if (isRequest) {
|
||||
markInfoText.setText(extractRequestContent);
|
||||
} else {
|
||||
markInfoText.setText(extractResponseContent);
|
||||
}
|
||||
}
|
||||
currentMessage = content;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static void main(String[] args) {
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package burp;
|
||||
|
||||
public class Config {
|
||||
public static String initConfigContent = "{\"Email\":{\"loaded\":true,\"scope\":\"response\",\"regex\":\"([\\\\w-]+(?:\\\\.[\\\\w-]+)*@(?:[\\\\w](?:[\\\\w-]*[\\\\w])?\\\\.)+[\\\\w](?:[\\\\w-]*[\\\\w])?)\",\"action\":\"any\",\"color\":\"yellow\"}}";
|
||||
public static String[] colorArray = new String[] {"red", "orange", "yellow", "green", "cyan", "blue", "pink", "magenta", "gray"};
|
||||
public static String[] scopeArray = new String[] {"any", "response", "request"};
|
||||
public static String[] actionArray = new String[] {"any", "extract", "highight"};
|
||||
public static String excludeSuffix = "7z|aif|aifc|aiff|au|bmp|cmx|cod|css|doc|docx|gif|gz|ico|ief|jfif|jpe|jpeg|jpg|m3u|mid|mp2|mp3|mpa|mpe|mpeg|mpg|mpp|mpv2|otf|pbm|pdf|pgm|png|pnm|ppm|ra|ram|rar|ras|rgb|rmi|snd|svg|tar|tif|tiff|ttf|wav|woff|woff2|xbm|xpm|xwd|zip";
|
||||
public static String[] excludeMIME = new String[] {"application/msword", "application/vnd.ms-project", "application/x-gzip", "application/x-tar", "application/zip", "audio/basic", "audio/mid", "audio/mpeg", "audio/x-aiff", "audio/x-mpegurl", "audio/x-pn-realaudio", "audio/x-wav", "image/bmp", "image/cis-cod", "image/gif", "image/ief", "image/jpeg", "image/png", "image/pipeg", "image/svg+xml", "image/tiff", "image/x-cmu-raster", "image/x-cmx", "image/x-icon", "image/x-portable-anymap", "image/x-portable-bitmap", "image/x-portable-graymap", "image/x-portable-pixmap", "image/x-rgb", "image/x-xbitmap", "image/x-xpixmap", "image/x-xwindowdump", "text/css", "video/mpeg", "video/mpeg", "application/font-woff"};
|
||||
public static String outputTplString = "[%s]\n%s\n\n";
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package burp.action;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
import burp.Config;
|
||||
|
||||
public class DoAction {
|
||||
public String extractString(JSONObject jsonObj) {
|
||||
String result = "";
|
||||
Iterator<String> k = jsonObj.keys();
|
||||
while (k.hasNext()) {
|
||||
String name = k.next();
|
||||
JSONObject jsonObj1 = new JSONObject(jsonObj.get(name).toString());
|
||||
String tmpStr = String.format(Config.outputTplString, name, jsonObj1.getString("data")).intern();
|
||||
result += tmpStr;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public List<String> highlightList(JSONObject jsonObj) {
|
||||
List<String> colorList = new ArrayList<String>();
|
||||
Iterator<String> k = jsonObj.keys();
|
||||
while (k.hasNext()) {
|
||||
String name = k.next();
|
||||
JSONObject jsonObj2 = new JSONObject(jsonObj.get(name).toString());
|
||||
colorList.add(jsonObj2.getString("color"));
|
||||
}
|
||||
return colorList;
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
package burp.action;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
import burp.file.ReadFile;
|
||||
import jregex.Matcher;
|
||||
import jregex.Pattern;
|
||||
|
||||
public class ExtractContent {
|
||||
ReadFile rf = new ReadFile();
|
||||
public JSONObject matchRegex(byte[] content, String scopeString, String actionString, String configFilePath) {
|
||||
JSONObject tabContent = new JSONObject();
|
||||
// 正则匹配提取内容
|
||||
try {
|
||||
String jsonStr = rf.readFileContent(configFilePath);
|
||||
JSONObject jsonObj = new JSONObject(jsonStr);
|
||||
Iterator<String> k = jsonObj.keys();
|
||||
// 遍历json数组
|
||||
while (k.hasNext()) {
|
||||
String contentString = new String(content, "UTF-8").intern();
|
||||
String name = k.next();
|
||||
JSONObject jsonObj1 = new JSONObject(jsonObj.get(name).toString());
|
||||
JSONObject jsonData = new JSONObject();
|
||||
String regex = jsonObj1.getString("regex");
|
||||
boolean isLoaded = jsonObj1.getBoolean("loaded");
|
||||
String scope = jsonObj1.getString("scope");
|
||||
String action = jsonObj1.getString("action");
|
||||
String color = jsonObj1.getString("color");
|
||||
List<String> result = new ArrayList<String>();
|
||||
|
||||
if(isLoaded && (scope.equals(scopeString) || scope.equals("any")) && (action.equals(actionString) || action.equals("any"))) {
|
||||
Pattern pattern = new Pattern(regex);
|
||||
Matcher matcher = pattern.matcher(contentString);
|
||||
while (matcher.find()) {
|
||||
// 添加匹配数据至list
|
||||
// 强制用户使用()包裹正则
|
||||
result.add(matcher.group(1));
|
||||
}
|
||||
|
||||
// 去除重复内容
|
||||
HashSet tmpList = new HashSet(result);
|
||||
result.clear();
|
||||
result.addAll(tmpList);
|
||||
|
||||
if (!result.isEmpty()) {
|
||||
jsonData.put("color", color);
|
||||
jsonData.put("data", String.join("\n", result));
|
||||
jsonData.put("loaded", isLoaded);
|
||||
// 初始化格式
|
||||
tabContent.put(name, jsonData);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
} catch (Exception e) {}
|
||||
|
||||
return tabContent;
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package burp.action;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import burp.Config;
|
||||
import jregex.Matcher;
|
||||
import jregex.Pattern;
|
||||
import jregex.REFlags;
|
||||
|
||||
public class MatchHTTP {
|
||||
// 匹配后缀
|
||||
public boolean matchSuffix(String str) {
|
||||
Pattern pattern = new Pattern(String.format("[\\w]+[\\.](%s)", Config.excludeSuffix), REFlags.IGNORE_CASE);
|
||||
Matcher matcher = pattern.matcher(str);
|
||||
if(matcher.find()){
|
||||
return true;
|
||||
}else{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 匹配MIME
|
||||
public boolean matchMIME(List<String> mimeList) {
|
||||
for (String headerString : mimeList) {
|
||||
if (headerString.toLowerCase().startsWith("content-type")) {
|
||||
for (String mime : Arrays.asList(Config.excludeMIME)) {
|
||||
if (headerString.contains(mime)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package burp.color;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class GetColorKey {
|
||||
/*
|
||||
* 颜色下标获取
|
||||
*/
|
||||
public List<Integer> getColorKeys(List<String> keys, String[] colorArray){
|
||||
List<Integer> result = new ArrayList<Integer>();
|
||||
int size = colorArray.length;
|
||||
// 根据颜色获取下标
|
||||
for (int x = 0; x < keys.size(); x++) {
|
||||
for (int v = 0; v < size; v++) {
|
||||
if (colorArray[v].equals(keys.get(x))) {
|
||||
result.add(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package burp.color;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
public class UpgradeColor {
|
||||
|
||||
private String endColor = "";
|
||||
/*
|
||||
* 颜色升级递归算法
|
||||
*/
|
||||
private String colorUpgrade(List<Integer> colorList, String[] colorArray) {
|
||||
int colorSize = colorList.size();
|
||||
colorList.sort(Comparator.comparingInt(Integer::intValue));
|
||||
int i = 0;
|
||||
List<Integer> stack = new ArrayList<Integer>();
|
||||
while (i < colorSize) {
|
||||
if (stack.isEmpty()) {
|
||||
stack.add(colorList.get(i));
|
||||
i++;
|
||||
} else {
|
||||
if (colorList.get(i) != stack.stream().reduce((first, second) -> second).orElse(99999999)) {
|
||||
stack.add(colorList.get(i));
|
||||
i++;
|
||||
} else {
|
||||
stack.set(stack.size() - 1, stack.get(stack.size() - 1) - 1);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
// 利用HashSet删除重复元素
|
||||
HashSet tmpList = new HashSet(stack);
|
||||
if (stack.size() == tmpList.size()) {
|
||||
stack.sort(Comparator.comparingInt(Integer::intValue));
|
||||
if(stack.get(0).equals(-1)) {
|
||||
this.endColor = colorArray[0];
|
||||
} else {
|
||||
this.endColor = colorArray[stack.get(0)];
|
||||
}
|
||||
} else {
|
||||
this.colorUpgrade(stack, colorArray);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public String getEndColor(List<Integer> colorList, String[] colorArray) {
|
||||
colorUpgrade(colorList, colorArray);
|
||||
return endColor;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package burp.file;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class FileExists {
|
||||
|
||||
/*
|
||||
* 判断文件是否存在
|
||||
*/
|
||||
public Boolean fileExists(String fileName) {
|
||||
File file = new File(fileName);
|
||||
if(file.exists()){
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package burp.file;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
|
||||
public class ReadFile {
|
||||
/*
|
||||
* 获取文件内容
|
||||
*/
|
||||
public String readFileContent(String fileName) {
|
||||
File file = new File(fileName);
|
||||
BufferedReader reader = null;
|
||||
StringBuffer sbf = new StringBuffer();
|
||||
try {
|
||||
reader = new BufferedReader(new FileReader(file));
|
||||
String tempStr;
|
||||
while ((tempStr = reader.readLine()) != null) {
|
||||
sbf.append(tempStr);
|
||||
}
|
||||
reader.close();
|
||||
return sbf.toString();
|
||||
} catch (IOException e) {
|
||||
} finally {
|
||||
if (reader != null) {
|
||||
try {
|
||||
reader.close();
|
||||
} catch (IOException err) {
|
||||
err.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
return sbf.toString();
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package burp.file;
|
||||
|
||||
import javax.swing.JOptionPane;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class RemoveContent {
|
||||
WriteFile w = new WriteFile();
|
||||
ReadFile r = new ReadFile();
|
||||
/*
|
||||
* 删除某文件内容
|
||||
*/
|
||||
public void removeFileContent(String key, String configFilePath) {
|
||||
String jsonStr = r.readFileContent(configFilePath);
|
||||
JSONObject jsonObj = new JSONObject(jsonStr);
|
||||
jsonObj.remove(key);
|
||||
|
||||
if (w.writeFileContent(configFilePath, jsonObj.toString())) {
|
||||
JOptionPane.showMessageDialog(null, "Delete Successfully!", "Info", JOptionPane.INFORMATION_MESSAGE);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package burp.file;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
|
||||
public class WriteFile {
|
||||
/*
|
||||
* 写入文件内容
|
||||
*/
|
||||
public boolean writeFileContent(String fileName, String fileContent) {
|
||||
try {
|
||||
BufferedWriter out = new BufferedWriter(new FileWriter(fileName));
|
||||
out.write(fileContent);
|
||||
out.close();
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package burp.ui;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.Vector;
|
||||
|
||||
import javax.swing.JTable;
|
||||
import javax.swing.table.DefaultTableModel;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
import burp.file.ReadFile;
|
||||
|
||||
public class FillTable {
|
||||
ReadFile rf = new ReadFile();
|
||||
/*
|
||||
* 初始化表格内容
|
||||
*/
|
||||
public void fillTable(String configFilePath, JTable table) {
|
||||
DefaultTableModel dtm=(DefaultTableModel) table.getModel();
|
||||
dtm.setRowCount(0);
|
||||
String jsonStr = rf.readFileContent(configFilePath);
|
||||
JSONObject jsonObj = new JSONObject(jsonStr);
|
||||
Iterator<String> k = jsonObj.keys();
|
||||
// 遍历json数组
|
||||
while (k.hasNext()) {
|
||||
String name = k.next();
|
||||
JSONObject jsonObj1 = new JSONObject(jsonObj.get(name).toString());
|
||||
boolean loaded = jsonObj1.getBoolean("loaded");
|
||||
String regex = jsonObj1.getString("regex");
|
||||
String color = jsonObj1.getString("color");
|
||||
String scope = jsonObj1.getString("scope");
|
||||
String action = jsonObj1.getString("action");
|
||||
// 填充数据
|
||||
Vector rules = new Vector();
|
||||
rules.add(loaded);
|
||||
rules.add(name);
|
||||
rules.add(regex);
|
||||
rules.add(color);
|
||||
rules.add(scope);
|
||||
rules.add(action);
|
||||
dtm.addRow(rules);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 698 KiB |
|
Before Width: | Height: | Size: 223 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 223 KiB |
BIN
images/config.png
Normal file
|
After Width: | Height: | Size: 158 KiB |
BIN
images/databoard.png
Normal file
|
After Width: | Height: | Size: 175 KiB |
BIN
images/logo.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
images/markinfo.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
images/reward.jpeg
Normal file
|
After Width: | Height: | Size: 132 KiB |
BIN
images/rules.png
Normal file
|
After Width: | Height: | Size: 128 KiB |
@@ -1 +1,2 @@
|
||||
rootProject.name = 'HaE'
|
||||
rootProject.name = 'HaE'
|
||||
|
||||
|
||||
72
src/main/java/hae/Config.java
Normal file
@@ -0,0 +1,72 @@
|
||||
package hae;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class Config {
|
||||
public static String suffix = "3g2|3gp|7z|aac|abw|aif|aifc|aiff|apk|arc|au|avi|azw|bat|bin|bmp|bz|bz2|cmd|cmx|cod|com|csh|css|csv|dll|doc|docx|ear|eot|epub|exe|flac|flv|gif|gz|ico|ics|ief|jar|jfif|jpe|jpeg|jpg|less|m3u|mid|midi|mjs|mkv|mov|mp2|mp3|mp4|mpa|mpe|mpeg|mpg|mpkg|mpp|mpv2|odp|ods|odt|oga|ogg|ogv|ogx|otf|pbm|pdf|pgm|png|pnm|ppm|ppt|pptx|ra|ram|rar|ras|rgb|rmi|rtf|scss|sh|snd|svg|swf|tar|tif|tiff|ttf|vsd|war|wav|weba|webm|webp|wmv|woff|woff2|xbm|xls|xlsx|xpm|xul|xwd|zip";
|
||||
|
||||
public static String host = "gh0st.cn";
|
||||
|
||||
public static String status = "404";
|
||||
|
||||
public static String header = "Last-Modified|Date|Connection|ETag";
|
||||
|
||||
public static String size = "0";
|
||||
|
||||
public static String boundary = "\n\t\n";
|
||||
|
||||
public static String[] scope = new String[]{
|
||||
"any",
|
||||
"any header",
|
||||
"any body",
|
||||
"response",
|
||||
"response line",
|
||||
"response header",
|
||||
"response body",
|
||||
"request",
|
||||
"request line",
|
||||
"request header",
|
||||
"request body"
|
||||
};
|
||||
|
||||
public static String scopeOptions = "Suite|Target|Proxy|Scanner|Intruder|Repeater|Logger|Sequencer|Decoder|Comparer|Extensions|Organizer|Recorded login replayer";
|
||||
|
||||
public static String modeStatus = "true";
|
||||
|
||||
public static String[] ruleFields = {
|
||||
"Loaded", "Name", "F-Regex", "S-Regex", "Format", "Color", "Scope", "Engine", "Sensitive"
|
||||
};
|
||||
|
||||
public static Object[][] ruleTemplate = new Object[][]{
|
||||
{
|
||||
false, "New Name", "(First Regex)", "(Second Regex)", "{0}", "gray", "any", "nfa", false
|
||||
}
|
||||
};
|
||||
|
||||
public static String[] engine = new String[]{
|
||||
"nfa",
|
||||
"dfa"
|
||||
};
|
||||
|
||||
public static String[] color = new String[]{
|
||||
"red",
|
||||
"orange",
|
||||
"yellow",
|
||||
"green",
|
||||
"cyan",
|
||||
"blue",
|
||||
"pink",
|
||||
"magenta",
|
||||
"gray",
|
||||
"none"
|
||||
};
|
||||
|
||||
public static Boolean proVersionStatus = true;
|
||||
|
||||
public static Map<String, Object[][]> globalRules = new HashMap<>();
|
||||
|
||||
public static ConcurrentHashMap<String, Map<String, List<String>>> globalDataMap = new ConcurrentHashMap<>();
|
||||
}
|
||||
71
src/main/java/hae/HaE.java
Normal file
@@ -0,0 +1,71 @@
|
||||
package hae;
|
||||
|
||||
import burp.api.montoya.BurpExtension;
|
||||
import burp.api.montoya.MontoyaApi;
|
||||
import burp.api.montoya.core.BurpSuiteEdition;
|
||||
import burp.api.montoya.logging.Logging;
|
||||
import hae.cache.DataCache;
|
||||
import hae.component.Main;
|
||||
import hae.component.board.message.MessageTableModel;
|
||||
import hae.instances.editor.RequestEditor;
|
||||
import hae.instances.editor.ResponseEditor;
|
||||
import hae.instances.editor.WebSocketEditor;
|
||||
import hae.instances.websocket.WebSocketMessageHandler;
|
||||
import hae.utils.ConfigLoader;
|
||||
import hae.utils.DataManager;
|
||||
|
||||
public class HaE implements BurpExtension {
|
||||
@Override
|
||||
public void initialize(MontoyaApi api) {
|
||||
// 设置扩展名称
|
||||
api.extension().setName("HaE - Highlighter and Extractor");
|
||||
String version = "4.3.1";
|
||||
|
||||
// 加载扩展后输出的项目信息
|
||||
Logging logging = api.logging();
|
||||
logging.logToOutput("[ HACK THE WORLD - TO DO IT ]");
|
||||
logging.logToOutput("[#] Author: EvilChen && 0chencc && vaycore");
|
||||
logging.logToOutput("[#] Github: https://github.com/gh0stkey/HaE");
|
||||
logging.logToOutput("[#] Version: " + version);
|
||||
|
||||
// 配置文件加载
|
||||
ConfigLoader configLoader = new ConfigLoader(api);
|
||||
|
||||
MessageTableModel messageTableModel = new MessageTableModel(api, configLoader);
|
||||
|
||||
// 设置BurpSuite专业版状态
|
||||
Config.proVersionStatus = getBurpSuiteProStatus(api);
|
||||
|
||||
// 注册Tab页(用于查询数据)
|
||||
api.userInterface().registerSuiteTab("HaE", new Main(api, configLoader, messageTableModel));
|
||||
|
||||
// 注册WebSocket处理器
|
||||
api.proxy().registerWebSocketCreationHandler(proxyWebSocketCreation -> proxyWebSocketCreation.proxyWebSocket().registerProxyMessageHandler(new WebSocketMessageHandler(api, configLoader)));
|
||||
|
||||
// 注册消息编辑框(用于展示数据)
|
||||
api.userInterface().registerHttpRequestEditorProvider(new RequestEditor(api, configLoader));
|
||||
api.userInterface().registerHttpResponseEditorProvider(new ResponseEditor(api, configLoader));
|
||||
api.userInterface().registerWebSocketMessageEditorProvider(new WebSocketEditor(api, configLoader));
|
||||
|
||||
// 从BurpSuite里加载数据
|
||||
DataManager dataManager = new DataManager(api);
|
||||
dataManager.loadData(messageTableModel);
|
||||
|
||||
api.extension().registerUnloadingHandler(() -> {
|
||||
// 卸载清空数据
|
||||
Config.globalDataMap.clear();
|
||||
DataCache.clear();
|
||||
});
|
||||
}
|
||||
|
||||
private Boolean getBurpSuiteProStatus(MontoyaApi api) {
|
||||
boolean burpSuiteProStatus = false;
|
||||
|
||||
try {
|
||||
burpSuiteProStatus = api.burpSuite().version().edition() == BurpSuiteEdition.PROFESSIONAL;
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
return burpSuiteProStatus;
|
||||
}
|
||||
}
|
||||
30
src/main/java/hae/cache/DataCache.java
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
package hae.cache;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class DataCache {
|
||||
private static final int MAX_SIZE = 100000;
|
||||
private static final int EXPIRE_DURATION = 4;
|
||||
|
||||
private static final Cache<String, Map<String, Map<String, Object>>> cache =
|
||||
Caffeine.newBuilder()
|
||||
.maximumSize(MAX_SIZE)
|
||||
.expireAfterWrite(EXPIRE_DURATION, TimeUnit.HOURS)
|
||||
.build();
|
||||
|
||||
public static void put(String key, Map<String, Map<String, Object>> value) {
|
||||
cache.put(key, value);
|
||||
}
|
||||
|
||||
public static Map<String, Map<String, Object>> get(String key) {
|
||||
return cache.getIfPresent(key);
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
cache.invalidateAll();
|
||||
}
|
||||
}
|
||||
456
src/main/java/hae/component/Config.java
Normal file
@@ -0,0 +1,456 @@
|
||||
package hae.component;
|
||||
|
||||
import burp.api.montoya.MontoyaApi;
|
||||
import burp.api.montoya.core.Registration;
|
||||
import hae.component.board.message.MessageTableModel;
|
||||
import hae.component.rule.Rules;
|
||||
import hae.instances.http.HttpMessageActiveHandler;
|
||||
import hae.instances.http.HttpMessagePassiveHandler;
|
||||
import hae.utils.ConfigLoader;
|
||||
import hae.utils.UIEnhancer;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
import javax.swing.border.TitledBorder;
|
||||
import javax.swing.event.DocumentEvent;
|
||||
import javax.swing.event.DocumentListener;
|
||||
import javax.swing.event.TableModelListener;
|
||||
import javax.swing.table.DefaultTableModel;
|
||||
import java.awt.*;
|
||||
import java.awt.datatransfer.Clipboard;
|
||||
import java.awt.datatransfer.DataFlavor;
|
||||
import java.awt.event.*;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
|
||||
public class Config extends JPanel {
|
||||
private final MontoyaApi api;
|
||||
private final ConfigLoader configLoader;
|
||||
private final MessageTableModel messageTableModel;
|
||||
private final Rules rules;
|
||||
|
||||
private Registration activeHandler;
|
||||
private Registration passiveHandler;
|
||||
|
||||
private boolean isLoadingData = false;
|
||||
|
||||
public Config(MontoyaApi api, ConfigLoader configLoader, MessageTableModel messageTableModel, Rules rules) {
|
||||
this.api = api;
|
||||
this.configLoader = configLoader;
|
||||
this.messageTableModel = messageTableModel;
|
||||
this.rules = rules;
|
||||
|
||||
this.activeHandler = api.http().registerHttpHandler(new HttpMessageActiveHandler(api, configLoader, messageTableModel));
|
||||
this.passiveHandler = api.scanner().registerScanCheck(new HttpMessagePassiveHandler(api, configLoader, messageTableModel));
|
||||
|
||||
initComponents();
|
||||
}
|
||||
|
||||
private void initComponents() {
|
||||
setLayout(new BorderLayout());
|
||||
|
||||
GridBagConstraints constraints = new GridBagConstraints();
|
||||
constraints.weightx = 1.0;
|
||||
constraints.fill = GridBagConstraints.HORIZONTAL;
|
||||
|
||||
JPanel ruleInfoPanel = new JPanel(new GridBagLayout());
|
||||
ruleInfoPanel.setBorder(new EmptyBorder(10, 15, 5, 15));
|
||||
|
||||
JLabel ruleLabel = new JLabel("Path:");
|
||||
JTextField pathTextField = new JTextField();
|
||||
pathTextField.setEditable(false);
|
||||
pathTextField.setText(configLoader.getRulesFilePath());
|
||||
JButton reloadButton = new JButton("Reload");
|
||||
JButton reinitButton = new JButton("Reinit");
|
||||
ruleInfoPanel.add(ruleLabel);
|
||||
ruleInfoPanel.add(pathTextField, constraints);
|
||||
ruleInfoPanel.add(Box.createHorizontalStrut(5));
|
||||
ruleInfoPanel.add(reinitButton);
|
||||
ruleInfoPanel.add(Box.createHorizontalStrut(5));
|
||||
ruleInfoPanel.add(reloadButton);
|
||||
|
||||
reloadButton.addActionListener(this::reloadActionPerformed);
|
||||
reinitButton.addActionListener(this::reinitActionPerformed);
|
||||
|
||||
constraints.gridx = 1;
|
||||
JTabbedPane configTabbedPanel = new JTabbedPane();
|
||||
|
||||
String[] settingMode = new String[]{"Exclude suffix", "Block host", "Exclude status", "Dynamic Header"};
|
||||
JPanel settingPanel = createConfigTablePanel(settingMode);
|
||||
|
||||
JPanel northPanel = new JPanel(new BorderLayout());
|
||||
|
||||
JPanel modePanel = getModePanel();
|
||||
JScrollPane modeScrollPane = new JScrollPane(modePanel);
|
||||
modeScrollPane.setBorder(new TitledBorder("Mode"));
|
||||
|
||||
JTextField limitPanel = getLimitPanel();
|
||||
JScrollPane limitScrollPane = new JScrollPane(limitPanel);
|
||||
limitScrollPane.setBorder(new TitledBorder("Limit Size (MB)"));
|
||||
|
||||
JSplitPane northTopPanel = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, modeScrollPane, limitScrollPane);
|
||||
northTopPanel.addComponentListener(new ComponentAdapter() {
|
||||
@Override
|
||||
public void componentResized(ComponentEvent e) {
|
||||
northTopPanel.setDividerLocation(0.5);
|
||||
}
|
||||
});
|
||||
|
||||
JPanel scopePanel = getScopePanel();
|
||||
JScrollPane scopeScrollPane = new JScrollPane(scopePanel);
|
||||
scopeScrollPane.setBorder(new TitledBorder("Scope"));
|
||||
|
||||
northPanel.add(scopeScrollPane, BorderLayout.SOUTH);
|
||||
northPanel.add(northTopPanel, BorderLayout.NORTH);
|
||||
settingPanel.add(northPanel, BorderLayout.NORTH);
|
||||
|
||||
configTabbedPanel.add("Setting", settingPanel);
|
||||
add(ruleInfoPanel, BorderLayout.NORTH);
|
||||
add(configTabbedPanel, BorderLayout.CENTER);
|
||||
}
|
||||
|
||||
private JPanel getScopePanel() {
|
||||
JPanel scopePanel = new JPanel();
|
||||
scopePanel.setLayout(new BoxLayout(scopePanel, BoxLayout.X_AXIS));
|
||||
scopePanel.setBorder(new EmptyBorder(3, 0, 6, 0));
|
||||
|
||||
String[] scopeInit = hae.Config.scopeOptions.split("\\|");
|
||||
String[] scopeMode = configLoader.getScope().split("\\|");
|
||||
for (String scope : scopeInit) {
|
||||
JCheckBox checkBox = new JCheckBox(scope);
|
||||
scopePanel.add(checkBox);
|
||||
checkBox.addActionListener(e -> updateScope(checkBox));
|
||||
for (String mode : scopeMode) {
|
||||
if (scope.equals(mode)) {
|
||||
checkBox.setSelected(true);
|
||||
}
|
||||
}
|
||||
updateScope(checkBox);
|
||||
}
|
||||
|
||||
return scopePanel;
|
||||
}
|
||||
|
||||
private JPanel getModePanel() {
|
||||
JPanel modePanel = new JPanel();
|
||||
modePanel.setLayout(new BoxLayout(modePanel, BoxLayout.X_AXIS));
|
||||
|
||||
JCheckBox checkBox = new JCheckBox("Enable active http message handler");
|
||||
checkBox.setEnabled(hae.Config.proVersionStatus);
|
||||
modePanel.add(checkBox);
|
||||
checkBox.addActionListener(e -> updateModeStatus(checkBox));
|
||||
checkBox.setSelected(configLoader.getMode());
|
||||
updateModeStatus(checkBox);
|
||||
|
||||
return modePanel;
|
||||
}
|
||||
|
||||
private JTextField getLimitPanel() {
|
||||
JTextField limitSizeTextField = new JTextField();
|
||||
limitSizeTextField.getDocument().addDocumentListener(new DocumentListener() {
|
||||
@Override
|
||||
public void insertUpdate(DocumentEvent e) {
|
||||
onTextChange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeUpdate(DocumentEvent e) {
|
||||
onTextChange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changedUpdate(DocumentEvent e) {
|
||||
onTextChange();
|
||||
}
|
||||
|
||||
private void onTextChange() {
|
||||
String limitSizeText = limitSizeTextField.getText();
|
||||
configLoader.setLimitSize(limitSizeText);
|
||||
}
|
||||
});
|
||||
|
||||
limitSizeTextField.setText(configLoader.getLimitSize());
|
||||
|
||||
return limitSizeTextField;
|
||||
}
|
||||
|
||||
private TableModelListener craeteSettingTableModelListener(JComboBox<String> setTypeComboBox, DefaultTableModel model) {
|
||||
return e -> {
|
||||
// 如果是程序正在加载数据,不处理事件
|
||||
if (isLoadingData) {
|
||||
return;
|
||||
}
|
||||
|
||||
String selected = (String) setTypeComboBox.getSelectedItem();
|
||||
String values = getFirstColumnDataAsString(model);
|
||||
|
||||
if (selected != null) {
|
||||
if (selected.equals("Exclude suffix")) {
|
||||
if (!values.equals(configLoader.getExcludeSuffix())) {
|
||||
configLoader.setExcludeSuffix(values);
|
||||
}
|
||||
}
|
||||
|
||||
if (selected.equals("Block host")) {
|
||||
if (!values.equals(configLoader.getBlockHost())) {
|
||||
configLoader.setBlockHost(values);
|
||||
}
|
||||
}
|
||||
|
||||
if (selected.equals("Exclude status")) {
|
||||
if (!values.equals(configLoader.getExcludeStatus())) {
|
||||
configLoader.setExcludeStatus(values);
|
||||
}
|
||||
}
|
||||
|
||||
if (selected.equals("Dynamic Header")) {
|
||||
if (!values.equals(configLoader.getExcludeStatus())) {
|
||||
configLoader.setDynamicHeader(values);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private ActionListener createSettingActionListener(JComboBox<String> setTypeComboBox, DefaultTableModel model) {
|
||||
return e -> {
|
||||
String selected = (String) setTypeComboBox.getSelectedItem();
|
||||
|
||||
// 设置标志,表示正在加载数据
|
||||
isLoadingData = true;
|
||||
model.setRowCount(0);
|
||||
|
||||
if (selected != null) {
|
||||
if (selected.equals("Exclude suffix")) {
|
||||
addDataToTable(configLoader.getExcludeSuffix().replaceAll("\\|", "\r\n"), model);
|
||||
}
|
||||
|
||||
if (selected.equals("Block host")) {
|
||||
addDataToTable(configLoader.getBlockHost().replaceAll("\\|", "\r\n"), model);
|
||||
}
|
||||
|
||||
if (selected.equals("Exclude status")) {
|
||||
addDataToTable(configLoader.getExcludeStatus().replaceAll("\\|", "\r\n"), model);
|
||||
}
|
||||
|
||||
if (selected.equals("Dynamic Header")) {
|
||||
addDataToTable(configLoader.getDynamicHeader().replaceAll("\\|", "\r\n"), model);
|
||||
}
|
||||
}
|
||||
|
||||
// 重置标志
|
||||
isLoadingData = false;
|
||||
};
|
||||
}
|
||||
|
||||
private JPanel createConfigTablePanel(String[] mode) {
|
||||
GridBagConstraints constraints = new GridBagConstraints();
|
||||
constraints.weightx = 1.0;
|
||||
constraints.fill = GridBagConstraints.HORIZONTAL;
|
||||
|
||||
JPanel settingPanel = new JPanel(new BorderLayout());
|
||||
DefaultTableModel model = new DefaultTableModel();
|
||||
|
||||
JTable table = new JTable(model);
|
||||
model.addColumn("Value");
|
||||
JScrollPane scrollPane = new JScrollPane(table);
|
||||
|
||||
JPanel buttonPanel = new JPanel();
|
||||
buttonPanel.setBorder(new EmptyBorder(0, 3, 0, 0));
|
||||
GridBagLayout layout = new GridBagLayout();
|
||||
layout.rowHeights = new int[]{0, 0, 0, 0, 0, 0, 0};
|
||||
layout.rowWeights = new double[]{0.0, 0.0, 0.0, 0.0, 0.0, 0.0, Double.MIN_VALUE};
|
||||
buttonPanel.setLayout(layout);
|
||||
|
||||
JPanel inputPanel = new JPanel(new BorderLayout());
|
||||
JPanel inputPanelB = new JPanel(new BorderLayout());
|
||||
inputPanelB.setBorder(new EmptyBorder(0, 0, 3, 0));
|
||||
|
||||
JButton addButton = new JButton("Add");
|
||||
JButton removeButton = new JButton("Remove");
|
||||
JButton pasteButton = new JButton("Paste");
|
||||
JButton clearButton = new JButton("Clear");
|
||||
|
||||
JComboBox<String> setTypeComboBox = new JComboBox<>();
|
||||
setTypeComboBox.setModel(new DefaultComboBoxModel<>(mode));
|
||||
|
||||
model.addTableModelListener(craeteSettingTableModelListener(setTypeComboBox, model));
|
||||
|
||||
setTypeComboBox.addActionListener(createSettingActionListener(setTypeComboBox, model));
|
||||
|
||||
setTypeComboBox.setSelectedItem(mode[0]);
|
||||
|
||||
constraints.insets = new Insets(0, 0, 3, 0);
|
||||
constraints.gridy = 0;
|
||||
buttonPanel.add(setTypeComboBox, constraints);
|
||||
constraints.gridy = 1;
|
||||
buttonPanel.add(addButton, constraints);
|
||||
constraints.gridy = 2;
|
||||
buttonPanel.add(removeButton, constraints);
|
||||
constraints.gridy = 3;
|
||||
buttonPanel.add(pasteButton, constraints);
|
||||
constraints.gridy = 4;
|
||||
buttonPanel.add(clearButton, constraints);
|
||||
|
||||
JTextField addTextField = new JTextField();
|
||||
String defaultText = "Enter a new item";
|
||||
UIEnhancer.setTextFieldPlaceholder(addTextField, defaultText);
|
||||
|
||||
inputPanelB.add(addTextField, BorderLayout.CENTER);
|
||||
inputPanel.add(scrollPane, BorderLayout.CENTER);
|
||||
inputPanel.add(inputPanelB, BorderLayout.NORTH);
|
||||
|
||||
settingPanel.add(buttonPanel, BorderLayout.EAST);
|
||||
settingPanel.add(inputPanel, BorderLayout.CENTER);
|
||||
|
||||
|
||||
addButton.addActionListener(e -> addActionPerformed(e, model, addTextField));
|
||||
|
||||
addTextField.addKeyListener(new KeyAdapter() {
|
||||
@Override
|
||||
public void keyPressed(KeyEvent e) {
|
||||
if (e.getKeyCode() == KeyEvent.VK_ENTER) {
|
||||
addActionPerformed(null, model, addTextField);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
pasteButton.addActionListener(e -> {
|
||||
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
|
||||
try {
|
||||
String data = (String) clipboard.getData(DataFlavor.stringFlavor);
|
||||
if (data != null && !data.isEmpty()) {
|
||||
addDataToTable(data, model);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
});
|
||||
|
||||
removeButton.addActionListener(e -> {
|
||||
int selectedRow = table.getSelectedRow();
|
||||
if (selectedRow != -1) {
|
||||
model.removeRow(selectedRow);
|
||||
}
|
||||
});
|
||||
|
||||
clearButton.addActionListener(e -> model.setRowCount(0));
|
||||
|
||||
JPanel settingMainPanel = new JPanel(new BorderLayout());
|
||||
settingMainPanel.setBorder(new EmptyBorder(5, 15, 10, 15));
|
||||
JScrollPane settingScroller = new JScrollPane(settingPanel);
|
||||
settingScroller.setBorder(new TitledBorder("Setting"));
|
||||
settingMainPanel.add(settingScroller, BorderLayout.CENTER);
|
||||
|
||||
return settingMainPanel;
|
||||
}
|
||||
|
||||
|
||||
private String getFirstColumnDataAsString(DefaultTableModel model) {
|
||||
StringBuilder firstColumnData = new StringBuilder();
|
||||
int numRows = model.getRowCount();
|
||||
|
||||
for (int row = 0; row < numRows; row++) {
|
||||
firstColumnData.append(model.getValueAt(row, 0));
|
||||
if (row < numRows - 1) {
|
||||
firstColumnData.append("|");
|
||||
}
|
||||
}
|
||||
|
||||
return firstColumnData.toString();
|
||||
}
|
||||
|
||||
private void addDataToTable(String data, DefaultTableModel model) {
|
||||
if (!data.isBlank()) {
|
||||
String[] rows = data.split("\\r?\\n");
|
||||
for (String row : rows) {
|
||||
model.addRow(new String[]{row});
|
||||
}
|
||||
deduplicateTableData(model);
|
||||
}
|
||||
}
|
||||
|
||||
private void deduplicateTableData(DefaultTableModel model) {
|
||||
// 使用 Map 存储每一行的数据,用于去重
|
||||
Set<List<Object>> rowData = new LinkedHashSet<>();
|
||||
|
||||
int columnCount = model.getColumnCount();
|
||||
|
||||
// 将每一行数据作为一个列表,添加到 Set 中
|
||||
for (int i = 0; i < model.getRowCount(); i++) {
|
||||
List<Object> row = new ArrayList<>();
|
||||
for (int j = 0; j < columnCount; j++) {
|
||||
row.add(model.getValueAt(i, j));
|
||||
}
|
||||
rowData.add(row);
|
||||
}
|
||||
|
||||
// 清除原始数据
|
||||
model.setRowCount(0);
|
||||
|
||||
// 将去重后的数据添加回去
|
||||
for (List<Object> uniqueRow : rowData) {
|
||||
model.addRow(uniqueRow.toArray());
|
||||
}
|
||||
}
|
||||
|
||||
public void updateModeStatus(JCheckBox checkBox) {
|
||||
boolean selected = checkBox.isSelected();
|
||||
configLoader.setMode(selected ? "true" : "false");
|
||||
|
||||
if (checkBox.isSelected()) {
|
||||
if (hae.Config.proVersionStatus && passiveHandler.isRegistered()) {
|
||||
passiveHandler.deregister();
|
||||
}
|
||||
|
||||
if (!activeHandler.isRegistered()) {
|
||||
activeHandler = api.http().registerHttpHandler(new HttpMessageActiveHandler(api, configLoader, messageTableModel));
|
||||
}
|
||||
} else {
|
||||
if (hae.Config.proVersionStatus && !passiveHandler.isRegistered()) {
|
||||
passiveHandler = api.scanner().registerScanCheck(new HttpMessagePassiveHandler(api, configLoader, messageTableModel));
|
||||
}
|
||||
|
||||
if (activeHandler.isRegistered()) {
|
||||
activeHandler.deregister();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void updateScope(JCheckBox checkBox) {
|
||||
String boxText = checkBox.getText();
|
||||
boolean selected = checkBox.isSelected();
|
||||
|
||||
Set<String> HaEScope = new HashSet<>(Arrays.asList(configLoader.getScope().split("\\|")));
|
||||
|
||||
if (selected) {
|
||||
HaEScope.add(boxText);
|
||||
} else {
|
||||
HaEScope.remove(boxText);
|
||||
}
|
||||
|
||||
configLoader.setScope(String.join("|", HaEScope));
|
||||
}
|
||||
|
||||
private void addActionPerformed(ActionEvent e, DefaultTableModel model, JTextField addTextField) {
|
||||
String addTextFieldText = addTextField.getText();
|
||||
if (addTextField.getForeground().equals(Color.BLACK)) {
|
||||
addDataToTable(addTextFieldText, model);
|
||||
addTextField.setText("");
|
||||
addTextField.requestFocusInWindow();
|
||||
}
|
||||
}
|
||||
|
||||
private void reloadActionPerformed(ActionEvent e) {
|
||||
rules.reloadRuleGroup();
|
||||
}
|
||||
|
||||
private void reinitActionPerformed(ActionEvent e) {
|
||||
int retCode = JOptionPane.showConfirmDialog(this, "Do you want to reinitialize rules? This action will overwrite your existing rules.", "Info", JOptionPane.YES_NO_OPTION);
|
||||
if (retCode == JOptionPane.YES_OPTION) {
|
||||
boolean ret = configLoader.initRules();
|
||||
if (ret) {
|
||||
rules.reloadRuleGroup();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
77
src/main/java/hae/component/Main.java
Normal file
@@ -0,0 +1,77 @@
|
||||
package hae.component;
|
||||
|
||||
import burp.api.montoya.MontoyaApi;
|
||||
import hae.component.board.Databoard;
|
||||
import hae.component.board.message.MessageTableModel;
|
||||
import hae.component.rule.Rules;
|
||||
import hae.utils.ConfigLoader;
|
||||
import hae.utils.UIEnhancer;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.beans.PropertyChangeEvent;
|
||||
import java.beans.PropertyChangeListener;
|
||||
import java.net.URL;
|
||||
|
||||
public class Main extends JPanel {
|
||||
private final MontoyaApi api;
|
||||
private final ConfigLoader configLoader;
|
||||
private final MessageTableModel messageTableModel;
|
||||
|
||||
public Main(MontoyaApi api, ConfigLoader configLoader, MessageTableModel messageTableModel) {
|
||||
this.api = api;
|
||||
this.configLoader = configLoader;
|
||||
this.messageTableModel = messageTableModel;
|
||||
|
||||
initComponents();
|
||||
}
|
||||
|
||||
private void initComponents() {
|
||||
setLayout(new GridBagLayout());
|
||||
((GridBagLayout) getLayout()).columnWidths = new int[]{0, 0};
|
||||
((GridBagLayout) getLayout()).rowHeights = new int[]{0, 0};
|
||||
((GridBagLayout) getLayout()).columnWeights = new double[]{1.0, 1.0E-4};
|
||||
((GridBagLayout) getLayout()).rowWeights = new double[]{1.0, 1.0E-4};
|
||||
|
||||
JTabbedPane mainTabbedPane = new JTabbedPane();
|
||||
|
||||
// 新增Logo
|
||||
JTabbedPane HaETabbedPane = new JTabbedPane();
|
||||
boolean isDarkBg = UIEnhancer.isDarkColor(HaETabbedPane.getBackground());
|
||||
HaETabbedPane.addTab("", getImageIcon(isDarkBg), mainTabbedPane);
|
||||
// 中文Slogan:赋能白帽,高效作战
|
||||
HaETabbedPane.addTab(" Highlighter and Extractor - Empower ethical hacker for efficient operations. ", null);
|
||||
HaETabbedPane.setEnabledAt(1, false);
|
||||
HaETabbedPane.addPropertyChangeListener("background", new PropertyChangeListener() {
|
||||
@Override
|
||||
public void propertyChange(PropertyChangeEvent e) {
|
||||
boolean isDarkBg = UIEnhancer.isDarkColor(HaETabbedPane.getBackground());
|
||||
HaETabbedPane.setIconAt(0, getImageIcon(isDarkBg));
|
||||
}
|
||||
});
|
||||
|
||||
add(HaETabbedPane, new GridBagConstraints(0, 0, 1, 1, 0.0, 0.0,
|
||||
GridBagConstraints.CENTER, GridBagConstraints.BOTH,
|
||||
new Insets(0, 0, 0, 0), 0, 0));
|
||||
|
||||
// 依次添加Rules、Config、Databoard
|
||||
Rules rules = new Rules(api, configLoader);
|
||||
mainTabbedPane.addTab("Rules", rules);
|
||||
mainTabbedPane.addTab("Databoard", new Databoard(api, configLoader, messageTableModel));
|
||||
mainTabbedPane.addTab("Config", new Config(api, configLoader, messageTableModel, rules));
|
||||
}
|
||||
|
||||
private ImageIcon getImageIcon(boolean isDark) {
|
||||
ClassLoader classLoader = getClass().getClassLoader();
|
||||
URL imageURL;
|
||||
if (isDark) {
|
||||
imageURL = classLoader.getResource("logo/logo.png");
|
||||
} else {
|
||||
imageURL = classLoader.getResource("logo/logo_black.png");
|
||||
}
|
||||
ImageIcon originalIcon = new ImageIcon(imageURL);
|
||||
Image originalImage = originalIcon.getImage();
|
||||
Image scaledImage = originalImage.getScaledInstance(30, 20, Image.SCALE_FAST);
|
||||
return new ImageIcon(scaledImage);
|
||||
}
|
||||
}
|
||||
467
src/main/java/hae/component/board/Databoard.java
Normal file
@@ -0,0 +1,467 @@
|
||||
package hae.component.board;
|
||||
|
||||
import burp.api.montoya.MontoyaApi;
|
||||
import hae.Config;
|
||||
import hae.cache.DataCache;
|
||||
import hae.component.board.message.MessageTableModel;
|
||||
import hae.component.board.message.MessageTableModel.MessageTable;
|
||||
import hae.component.board.table.Datatable;
|
||||
import hae.utils.ConfigLoader;
|
||||
import hae.utils.UIEnhancer;
|
||||
import hae.utils.string.StringProcessor;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.event.DocumentEvent;
|
||||
import javax.swing.event.DocumentListener;
|
||||
import javax.swing.table.TableColumnModel;
|
||||
import javax.swing.table.TableModel;
|
||||
import javax.swing.table.TableRowSorter;
|
||||
import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
import java.text.Collator;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class Databoard extends JPanel {
|
||||
private static Boolean isMatchHost = false;
|
||||
private final MontoyaApi api;
|
||||
private final ConfigLoader configLoader;
|
||||
private final MessageTableModel messageTableModel;
|
||||
private final DefaultComboBoxModel comboBoxModel = new DefaultComboBoxModel();
|
||||
private final JComboBox hostComboBox = new JComboBox(comboBoxModel);
|
||||
private JTextField hostTextField;
|
||||
private JTabbedPane dataTabbedPane;
|
||||
private JSplitPane splitPane;
|
||||
private MessageTable messageTable;
|
||||
private JProgressBar progressBar;
|
||||
private SwingWorker<Map<String, List<String>>, Integer> handleComboBoxWorker;
|
||||
private SwingWorker<Void, Void> applyHostFilterWorker;
|
||||
|
||||
public Databoard(MontoyaApi api, ConfigLoader configLoader, MessageTableModel messageTableModel) {
|
||||
this.api = api;
|
||||
this.configLoader = configLoader;
|
||||
this.messageTableModel = messageTableModel;
|
||||
|
||||
initComponents();
|
||||
}
|
||||
|
||||
private void initComponents() {
|
||||
setLayout(new GridBagLayout());
|
||||
((GridBagLayout) getLayout()).columnWidths = new int[]{25, 0, 0, 0, 20, 0};
|
||||
((GridBagLayout) getLayout()).rowHeights = new int[]{0, 65, 20, 0, 0};
|
||||
((GridBagLayout) getLayout()).columnWeights = new double[]{0.0, 0.0, 1.0, 0.0, 0.0, 1.0E-4};
|
||||
((GridBagLayout) getLayout()).rowWeights = new double[]{0.0, 1.0, 0.0, 0.0, 1.0E-4};
|
||||
JLabel hostLabel = new JLabel("Host:");
|
||||
|
||||
JButton clearDataButton = new JButton("Clear data");
|
||||
JButton clearCacheButton = new JButton("Clear cache");
|
||||
JButton actionButton = new JButton("Action");
|
||||
JPanel menuPanel = new JPanel(new GridLayout(2, 1, 0, 5));
|
||||
menuPanel.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
|
||||
JPopupMenu menu = new JPopupMenu();
|
||||
menuPanel.add(clearDataButton);
|
||||
menuPanel.add(clearCacheButton);
|
||||
menu.add(menuPanel);
|
||||
|
||||
hostTextField = new JTextField();
|
||||
String defaultText = "Please enter the host";
|
||||
UIEnhancer.setTextFieldPlaceholder(hostTextField, defaultText);
|
||||
splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
|
||||
|
||||
dataTabbedPane = new JTabbedPane(JTabbedPane.TOP);
|
||||
dataTabbedPane.setPreferredSize(new Dimension(500, 0));
|
||||
dataTabbedPane.setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT);
|
||||
|
||||
actionButton.addActionListener(e -> {
|
||||
int x = 0;
|
||||
int y = actionButton.getHeight();
|
||||
menu.show(actionButton, x, y);
|
||||
});
|
||||
|
||||
clearDataButton.addActionListener(this::clearDataActionPerformed);
|
||||
clearCacheButton.addActionListener(this::clearCacheActionPerformed);
|
||||
|
||||
progressBar = new JProgressBar();
|
||||
splitPane.addComponentListener(new ComponentAdapter() {
|
||||
@Override
|
||||
public void componentResized(ComponentEvent e) {
|
||||
resizePanel();
|
||||
}
|
||||
});
|
||||
|
||||
splitPane.setVisible(false);
|
||||
progressBar.setVisible(false);
|
||||
|
||||
add(hostLabel, new GridBagConstraints(1, 0, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH,
|
||||
new Insets(8, 0, 5, 5), 0, 0));
|
||||
add(hostTextField, new GridBagConstraints(2, 0, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH,
|
||||
new Insets(8, 0, 5, 5), 0, 0));
|
||||
add(actionButton, new GridBagConstraints(3, 0, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH,
|
||||
new Insets(8, 0, 5, 5), 0, 0));
|
||||
|
||||
add(splitPane, new GridBagConstraints(1, 1, 3, 1, 0.0, 1.0,
|
||||
GridBagConstraints.CENTER, GridBagConstraints.BOTH,
|
||||
new Insets(0, 5, 0, 5), 0, 0));
|
||||
add(progressBar, new GridBagConstraints(1, 2, 3, 1, 1.0, 0.0,
|
||||
GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL,
|
||||
new Insets(0, 5, 0, 5), 0, 0));
|
||||
hostComboBox.setMaximumRowCount(5);
|
||||
add(hostComboBox, new GridBagConstraints(2, 0, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH,
|
||||
new Insets(8, 0, 5, 5), 0, 0));
|
||||
|
||||
setAutoMatch();
|
||||
}
|
||||
|
||||
private void resizePanel() {
|
||||
splitPane.setDividerLocation(0.4);
|
||||
TableColumnModel columnModel = messageTable.getColumnModel();
|
||||
int totalWidth = (int) (getWidth() * 0.6);
|
||||
columnModel.getColumn(0).setPreferredWidth((int) (totalWidth * 0.1));
|
||||
columnModel.getColumn(1).setPreferredWidth((int) (totalWidth * 0.3));
|
||||
columnModel.getColumn(2).setPreferredWidth((int) (totalWidth * 0.3));
|
||||
columnModel.getColumn(3).setPreferredWidth((int) (totalWidth * 0.1));
|
||||
columnModel.getColumn(4).setPreferredWidth((int) (totalWidth * 0.1));
|
||||
columnModel.getColumn(5).setPreferredWidth((int) (totalWidth * 0.1));
|
||||
}
|
||||
|
||||
private void setProgressBar(boolean status, String message, int progress) {
|
||||
progressBar.setIndeterminate(status && progress <= 0);
|
||||
progressBar.setString(message);
|
||||
progressBar.setStringPainted(true);
|
||||
progressBar.setMaximum(100);
|
||||
|
||||
if (progress > 0) {
|
||||
progressBar.setValue(progress);
|
||||
} else if (!status) {
|
||||
progressBar.setValue(progressBar.getMaximum());
|
||||
}
|
||||
}
|
||||
|
||||
private void setAutoMatch() {
|
||||
hostComboBox.setSelectedItem(null);
|
||||
hostComboBox.addActionListener(this::handleComboBoxAction);
|
||||
|
||||
hostTextField.addKeyListener(new KeyAdapter() {
|
||||
@Override
|
||||
public void keyPressed(KeyEvent e) {
|
||||
handleKeyEvents(e);
|
||||
}
|
||||
});
|
||||
|
||||
hostTextField.getDocument().addDocumentListener(new DocumentListener() {
|
||||
@Override
|
||||
public void insertUpdate(DocumentEvent e) {
|
||||
filterComboBoxList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeUpdate(DocumentEvent e) {
|
||||
filterComboBoxList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changedUpdate(DocumentEvent e) {
|
||||
filterComboBoxList();
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
private void handleComboBoxAction(ActionEvent e) {
|
||||
if (!isMatchHost && hostComboBox.getSelectedItem() != null) {
|
||||
String selectedHost = hostComboBox.getSelectedItem().toString();
|
||||
|
||||
if (getHostByList().contains(selectedHost)) {
|
||||
hostTextField.setText(selectedHost);
|
||||
hostComboBox.setPopupVisible(false);
|
||||
|
||||
if (handleComboBoxWorker != null && !handleComboBoxWorker.isDone()) {
|
||||
progressBar.setVisible(false);
|
||||
handleComboBoxWorker.cancel(true);
|
||||
}
|
||||
|
||||
handleComboBoxWorker = new DataLoadingWorker(selectedHost);
|
||||
|
||||
handleComboBoxWorker.execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleKeyEvents(KeyEvent e) {
|
||||
isMatchHost = true;
|
||||
int keyCode = e.getKeyCode();
|
||||
|
||||
if (keyCode == KeyEvent.VK_SPACE && hostComboBox.isPopupVisible()) {
|
||||
e.setKeyCode(KeyEvent.VK_ENTER);
|
||||
}
|
||||
|
||||
if (Arrays.asList(KeyEvent.VK_DOWN, KeyEvent.VK_UP).contains(keyCode)) {
|
||||
hostComboBox.dispatchEvent(e);
|
||||
}
|
||||
|
||||
if (keyCode == KeyEvent.VK_ENTER) {
|
||||
isMatchHost = false;
|
||||
handleComboBoxAction(null);
|
||||
}
|
||||
|
||||
if (keyCode == KeyEvent.VK_ESCAPE) {
|
||||
hostComboBox.setPopupVisible(false);
|
||||
}
|
||||
|
||||
isMatchHost = false;
|
||||
}
|
||||
|
||||
private Map<String, List<String>> getSelectedMapByHost(String selectedHost, DataLoadingWorker worker) {
|
||||
ConcurrentHashMap<String, Map<String, List<String>>> dataMap = Config.globalDataMap;
|
||||
Map<String, List<String>> selectedDataMap;
|
||||
|
||||
if (selectedHost.contains("*")) {
|
||||
selectedDataMap = new HashMap<>();
|
||||
List<String> matchingKeys = new ArrayList<>();
|
||||
|
||||
// 第一步:找出所有匹配的键(预处理)
|
||||
for (String key : dataMap.keySet()) {
|
||||
if ((StringProcessor.matchesHostPattern(key, selectedHost) || selectedHost.equals("*")) && !key.contains("*")) {
|
||||
matchingKeys.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
// 第二步:分批处理数据
|
||||
int totalKeys = matchingKeys.size();
|
||||
for (int i = 0; i < totalKeys; i++) {
|
||||
String key = matchingKeys.get(i);
|
||||
Map<String, List<String>> ruleMap = dataMap.get(key);
|
||||
|
||||
if (ruleMap != null) {
|
||||
for (String ruleKey : ruleMap.keySet()) {
|
||||
List<String> dataList = ruleMap.get(ruleKey);
|
||||
if (selectedDataMap.containsKey(ruleKey)) {
|
||||
List<String> mergedList = new ArrayList<>(selectedDataMap.get(ruleKey));
|
||||
mergedList.addAll(dataList);
|
||||
// 使用HashSet去重
|
||||
HashSet<String> uniqueSet = new HashSet<>(mergedList);
|
||||
selectedDataMap.put(ruleKey, new ArrayList<>(uniqueSet));
|
||||
} else {
|
||||
selectedDataMap.put(ruleKey, new ArrayList<>(dataList));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 报告进度
|
||||
if (worker != null && i % 5 == 0) {
|
||||
int progress = (int) ((i + 1) * 90.0 / totalKeys);
|
||||
worker.publishProgress(progress);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
selectedDataMap = dataMap.get(selectedHost);
|
||||
// 对于非通配符匹配,直接返回结果
|
||||
if (worker != null) {
|
||||
worker.publishProgress(90);
|
||||
}
|
||||
}
|
||||
|
||||
return selectedDataMap != null ? selectedDataMap : new HashMap<>();
|
||||
}
|
||||
|
||||
private void filterComboBoxList() {
|
||||
isMatchHost = true;
|
||||
comboBoxModel.removeAllElements();
|
||||
String input = hostTextField.getText().toLowerCase();
|
||||
|
||||
if (!input.isEmpty()) {
|
||||
for (String host : getHostByList()) {
|
||||
String lowerCaseHost = host.toLowerCase();
|
||||
if (lowerCaseHost.contains(input)) {
|
||||
if (lowerCaseHost.equals(input)) {
|
||||
comboBoxModel.insertElementAt(lowerCaseHost, 0);
|
||||
comboBoxModel.setSelectedItem(lowerCaseHost);
|
||||
} else {
|
||||
comboBoxModel.addElement(host);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hostComboBox.setPopupVisible(comboBoxModel.getSize() > 0);
|
||||
isMatchHost = false;
|
||||
}
|
||||
|
||||
private void applyHostFilter(String filterText) {
|
||||
TableRowSorter<TableModel> sorter = (TableRowSorter<TableModel>) messageTable.getRowSorter();
|
||||
String cleanedText = StringProcessor.replaceFirstOccurrence(filterText, "*.", "");
|
||||
|
||||
if (applyHostFilterWorker != null && !applyHostFilterWorker.isDone()) {
|
||||
applyHostFilterWorker.cancel(true);
|
||||
}
|
||||
|
||||
applyHostFilterWorker = new SwingWorker<>() {
|
||||
@Override
|
||||
protected Void doInBackground() {
|
||||
RowFilter<Object, Object> rowFilter = new RowFilter<>() {
|
||||
public boolean include(Entry<?, ?> entry) {
|
||||
if (cleanedText.equals("*")) {
|
||||
return true;
|
||||
} else {
|
||||
String host = StringProcessor.getHostByUrl((String) entry.getValue(1));
|
||||
return StringProcessor.matchesHostPattern(host, filterText);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
sorter.setRowFilter(rowFilter);
|
||||
messageTableModel.applyHostFilter(filterText);
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
applyHostFilterWorker.execute();
|
||||
}
|
||||
|
||||
private List<String> getHostByList() {
|
||||
List<String> result = new ArrayList<>();
|
||||
if (!Config.globalDataMap.isEmpty()) {
|
||||
result = new ArrayList<>(Config.globalDataMap.keySet());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void clearCacheActionPerformed(ActionEvent e) {
|
||||
int retCode = JOptionPane.showConfirmDialog(this, "Do you want to clear cache?", "Info",
|
||||
JOptionPane.YES_NO_OPTION);
|
||||
if (retCode == JOptionPane.YES_OPTION) {
|
||||
DataCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void clearDataActionPerformed(ActionEvent e) {
|
||||
int retCode = JOptionPane.showConfirmDialog(this, "Do you want to clear data?", "Info",
|
||||
JOptionPane.YES_NO_OPTION);
|
||||
String host = hostTextField.getText();
|
||||
if (retCode == JOptionPane.YES_OPTION && !host.isEmpty()) {
|
||||
dataTabbedPane.removeAll();
|
||||
splitPane.setVisible(false);
|
||||
progressBar.setVisible(false);
|
||||
|
||||
Config.globalDataMap.keySet().parallelStream().forEach(key -> {
|
||||
if (StringProcessor.matchesHostPattern(key, host) || host.equals("*")) {
|
||||
Config.globalDataMap.remove(key);
|
||||
}
|
||||
});
|
||||
|
||||
// 删除无用的数据
|
||||
Set<String> wildcardKeys = Config.globalDataMap.keySet().stream()
|
||||
.filter(key -> key.startsWith("*."))
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
Set<String> existingSuffixes = Config.globalDataMap.keySet().stream()
|
||||
.filter(key -> !key.startsWith("*."))
|
||||
.map(key -> {
|
||||
int dotIndex = key.indexOf(".");
|
||||
return dotIndex != -1 ? key.substring(dotIndex) : "";
|
||||
})
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
Set<String> keysToRemove = wildcardKeys.stream()
|
||||
.filter(key -> !existingSuffixes.contains(key.substring(1)))
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
keysToRemove.forEach(Config.globalDataMap::remove);
|
||||
|
||||
if (Config.globalDataMap.size() == 1 && Config.globalDataMap.keySet().stream().anyMatch(key -> key.equals("*"))) {
|
||||
Config.globalDataMap.remove("*");
|
||||
}
|
||||
|
||||
messageTableModel.deleteByHost(host);
|
||||
|
||||
hostTextField.setText("");
|
||||
}
|
||||
}
|
||||
|
||||
// 定义为内部类
|
||||
private class DataLoadingWorker extends SwingWorker<Map<String, List<String>>, Integer> {
|
||||
private final String selectedHost;
|
||||
|
||||
public DataLoadingWorker(String selectedHost) {
|
||||
this.selectedHost = selectedHost;
|
||||
progressBar.setVisible(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, List<String>> doInBackground() throws Exception {
|
||||
return getSelectedMapByHost(selectedHost, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void process(List<Integer> chunks) {
|
||||
if (!chunks.isEmpty()) {
|
||||
int progress = chunks.get(chunks.size() - 1);
|
||||
setProgressBar(true, "Loading... " + progress + "%", progress);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void done() {
|
||||
if (!isCancelled()) {
|
||||
try {
|
||||
Map<String, List<String>> selectedDataMap = get();
|
||||
if (selectedDataMap != null && !selectedDataMap.isEmpty()) {
|
||||
dataTabbedPane.removeAll();
|
||||
|
||||
for (Map.Entry<String, List<String>> entry : selectedDataMap.entrySet()) {
|
||||
String tabTitle = String.format("%s (%s)", entry.getKey(), entry.getValue().size());
|
||||
Datatable datatablePanel = new Datatable(api, configLoader, entry.getKey(), entry.getValue());
|
||||
datatablePanel.setTableListener(messageTableModel);
|
||||
insertTabSorted(dataTabbedPane, tabTitle, datatablePanel);
|
||||
}
|
||||
|
||||
JSplitPane messageSplitPane = messageTableModel.getSplitPane();
|
||||
splitPane.setLeftComponent(dataTabbedPane);
|
||||
splitPane.setRightComponent(messageSplitPane);
|
||||
messageTable = messageTableModel.getMessageTable();
|
||||
resizePanel();
|
||||
|
||||
splitPane.setVisible(true);
|
||||
|
||||
applyHostFilter(selectedHost);
|
||||
setProgressBar(false, "OK", 100);
|
||||
} else {
|
||||
setProgressBar(false, "Error", 0);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
api.logging().logToOutput("DataLoadingWorker: " + e.getMessage());
|
||||
setProgressBar(false, "Error", 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void insertTabSorted(JTabbedPane tabbedPane, String title, Component component) {
|
||||
int insertIndex = 0;
|
||||
int tabCount = tabbedPane.getTabCount();
|
||||
|
||||
// 使用 Collator 实现更友好的语言排序(支持中文、特殊字符等)
|
||||
Collator collator = Collator.getInstance(Locale.getDefault());
|
||||
collator.setStrength(Collator.PRIMARY); // 忽略大小写和重音
|
||||
|
||||
for (int i = 0; i < tabCount; i++) {
|
||||
String existingTitle = tabbedPane.getTitleAt(i);
|
||||
if (collator.compare(existingTitle, title) > 0) {
|
||||
insertIndex = i;
|
||||
break;
|
||||
}
|
||||
insertIndex = i + 1;
|
||||
}
|
||||
|
||||
tabbedPane.insertTab(title, null, component, null, insertIndex);
|
||||
}
|
||||
|
||||
// 提供一个公共方法来发布进度
|
||||
public void publishProgress(int progress) {
|
||||
publish(progress);
|
||||
}
|
||||
}
|
||||
}
|
||||
52
src/main/java/hae/component/board/message/MessageEntry.java
Normal file
@@ -0,0 +1,52 @@
|
||||
package hae.component.board.message;
|
||||
|
||||
import burp.api.montoya.http.message.HttpRequestResponse;
|
||||
|
||||
public class MessageEntry {
|
||||
|
||||
private final String comment;
|
||||
private final HttpRequestResponse requestResponse;
|
||||
private final String url;
|
||||
private final String length;
|
||||
private final String status;
|
||||
private final String color;
|
||||
private final String method;
|
||||
|
||||
MessageEntry(HttpRequestResponse requestResponse, String method, String url, String comment, String length, String color, String status) {
|
||||
this.requestResponse = requestResponse;
|
||||
this.method = method;
|
||||
this.url = url;
|
||||
this.comment = comment;
|
||||
this.length = length;
|
||||
this.color = color;
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getColor() {
|
||||
return this.color;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return this.url;
|
||||
}
|
||||
|
||||
public String getLength() {
|
||||
return this.length;
|
||||
}
|
||||
|
||||
public String getComment() {
|
||||
return this.comment;
|
||||
}
|
||||
|
||||
public String getMethod() {
|
||||
return this.method;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return this.status;
|
||||
}
|
||||
|
||||
public HttpRequestResponse getRequestResponse() {
|
||||
return this.requestResponse;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package hae.component.board.message;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.table.DefaultTableCellRenderer;
|
||||
import java.awt.*;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Map;
|
||||
|
||||
public class MessageRenderer extends DefaultTableCellRenderer {
|
||||
|
||||
private final LinkedList<MessageEntry> log;
|
||||
private final Map<String, Color> colorMap = new HashMap<>();
|
||||
private final JTable table; // 保存对表格的引用
|
||||
|
||||
public MessageRenderer(LinkedList<MessageEntry> log, JTable table) {
|
||||
this.log = log;
|
||||
// 与BurpSuite的颜色保持一致
|
||||
this.colorMap.put("red", new Color(0xFF, 0x64, 0x64));
|
||||
this.colorMap.put("orange", new Color(0xFF, 0xC8, 0x64));
|
||||
this.colorMap.put("yellow", new Color(0xFF, 0xFF, 0x64));
|
||||
this.colorMap.put("green", new Color(0x64, 0xFF, 0x64));
|
||||
this.colorMap.put("cyan", new Color(0x64, 0xFF, 0xFF));
|
||||
this.colorMap.put("blue", new Color(0x64, 0x64, 0xFF));
|
||||
this.colorMap.put("pink", new Color(0xFF, 0xC8, 0xC8));
|
||||
this.colorMap.put("magenta", new Color(0xFF, 0x64, 0xFF));
|
||||
this.colorMap.put("gray", new Color(0xB4, 0xB4, 0xB4));
|
||||
this.colorMap.put("none", new Color(0, 0, 0, 0));
|
||||
this.table = table;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
|
||||
boolean hasFocus, int row, int column) {
|
||||
Component component = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
|
||||
|
||||
// 添加边界检查以防止IndexOutOfBoundsException
|
||||
int modelRow = table.convertRowIndexToModel(row);
|
||||
if (modelRow < 0 || modelRow >= log.size()) {
|
||||
// 如果索引无效,返回默认渲染组件(使用默认背景色)
|
||||
component.setBackground(Color.WHITE);
|
||||
component.setForeground(Color.BLACK);
|
||||
return component;
|
||||
}
|
||||
|
||||
MessageEntry messageEntry = log.get(modelRow);
|
||||
|
||||
// 设置颜色
|
||||
String colorByLog = messageEntry.getColor();
|
||||
Color color = colorMap.get(colorByLog);
|
||||
|
||||
// 如果颜色映射中没有找到对应颜色,使用默认白色
|
||||
if (color == null) {
|
||||
color = Color.WHITE;
|
||||
}
|
||||
|
||||
if (isSelected) {
|
||||
component.setBackground(UIManager.getColor("Table.selectionBackground"));
|
||||
} else {
|
||||
component.setBackground(color);
|
||||
}
|
||||
|
||||
component.setForeground(Color.BLACK);
|
||||
|
||||
return component;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void firePropertyChange(String propertyName, Object oldValue, Object newValue) {
|
||||
super.firePropertyChange(propertyName, oldValue, newValue);
|
||||
// 监听表格排序的属性变化
|
||||
if ("tableCellRenderer".equals(propertyName)) {
|
||||
// 更新每一行数据的颜色
|
||||
for (int i = 0; i < table.getRowCount(); i++) {
|
||||
table.repaint(table.getCellRect(i, 0, true));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
531
src/main/java/hae/component/board/message/MessageTableModel.java
Normal file
@@ -0,0 +1,531 @@
|
||||
package hae.component.board.message;
|
||||
|
||||
import burp.api.montoya.MontoyaApi;
|
||||
import burp.api.montoya.http.message.HttpHeader;
|
||||
import burp.api.montoya.http.message.HttpRequestResponse;
|
||||
import burp.api.montoya.http.message.requests.HttpRequest;
|
||||
import burp.api.montoya.http.message.responses.HttpResponse;
|
||||
import burp.api.montoya.persistence.PersistedObject;
|
||||
import burp.api.montoya.ui.UserInterface;
|
||||
import burp.api.montoya.ui.editor.HttpRequestEditor;
|
||||
import burp.api.montoya.ui.editor.HttpResponseEditor;
|
||||
import hae.Config;
|
||||
import hae.utils.ConfigLoader;
|
||||
import hae.utils.DataManager;
|
||||
import hae.utils.string.StringProcessor;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.table.AbstractTableModel;
|
||||
import javax.swing.table.DefaultTableModel;
|
||||
import javax.swing.table.TableModel;
|
||||
import javax.swing.table.TableRowSorter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.MessageFormat;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static burp.api.montoya.ui.editor.EditorOptions.READ_ONLY;
|
||||
|
||||
public class MessageTableModel extends AbstractTableModel {
|
||||
private final MontoyaApi api;
|
||||
private final ConfigLoader configLoader;
|
||||
private final MessageTable messageTable;
|
||||
private final JSplitPane splitPane;
|
||||
private final LinkedList<MessageEntry> log = new LinkedList<>();
|
||||
private final LinkedList<MessageEntry> filteredLog;
|
||||
private SwingWorker<Void, Void> currentWorker;
|
||||
|
||||
public MessageTableModel(MontoyaApi api, ConfigLoader configLoader) {
|
||||
this.filteredLog = new LinkedList<>();
|
||||
this.api = api;
|
||||
this.configLoader = configLoader;
|
||||
|
||||
JTabbedPane messageTab = new JTabbedPane();
|
||||
UserInterface userInterface = api.userInterface();
|
||||
HttpRequestEditor requestViewer = userInterface.createHttpRequestEditor(READ_ONLY);
|
||||
HttpResponseEditor responseViewer = userInterface.createHttpResponseEditor(READ_ONLY);
|
||||
messageTab.addTab("Request", requestViewer.uiComponent());
|
||||
messageTab.addTab("Response", responseViewer.uiComponent());
|
||||
|
||||
// 请求条目表格
|
||||
messageTable = new MessageTable(MessageTableModel.this, requestViewer, responseViewer);
|
||||
messageTable.setDefaultRenderer(Object.class, new MessageRenderer(filteredLog, messageTable));
|
||||
messageTable.setAutoCreateRowSorter(true);
|
||||
|
||||
TableRowSorter<DefaultTableModel> sorter = getDefaultTableModelTableRowSorter();
|
||||
messageTable.setRowSorter(sorter);
|
||||
messageTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
|
||||
|
||||
splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
|
||||
// 请求/响应文本框
|
||||
JScrollPane scrollPane = new JScrollPane(messageTable);
|
||||
scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
|
||||
scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
|
||||
splitPane.setLeftComponent(scrollPane);
|
||||
splitPane.setRightComponent(messageTab);
|
||||
}
|
||||
|
||||
private TableRowSorter<DefaultTableModel> getDefaultTableModelTableRowSorter() {
|
||||
TableRowSorter<DefaultTableModel> sorter = (TableRowSorter<DefaultTableModel>) messageTable.getRowSorter();
|
||||
|
||||
// Length字段根据大小进行排序
|
||||
sorter.setComparator(4, (Comparator<String>) (s1, s2) -> {
|
||||
Integer age1 = Integer.parseInt(s1);
|
||||
Integer age2 = Integer.parseInt(s2);
|
||||
return age1.compareTo(age2);
|
||||
});
|
||||
|
||||
// Color字段根据颜色顺序进行排序
|
||||
sorter.setComparator(5, new Comparator<String>() {
|
||||
@Override
|
||||
public int compare(String s1, String s2) {
|
||||
int index1 = getIndex(s1);
|
||||
int index2 = getIndex(s2);
|
||||
return Integer.compare(index1, index2);
|
||||
}
|
||||
|
||||
private int getIndex(String color) {
|
||||
for (int i = 0; i < Config.color.length; i++) {
|
||||
if (Config.color[i].equals(color)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
});
|
||||
return sorter;
|
||||
}
|
||||
|
||||
public synchronized void add(HttpRequestResponse messageInfo, String url, String method, String status, String length, String comment, String color, boolean flag) {
|
||||
synchronized (log) {
|
||||
if (messageInfo == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (comment == null || comment.trim().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (color == null || color.trim().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean isDuplicate = false;
|
||||
try {
|
||||
if (!log.isEmpty() && flag) {
|
||||
String host = StringProcessor.getHostByUrl(url);
|
||||
|
||||
for (MessageEntry entry : log) {
|
||||
if (host.equals(StringProcessor.getHostByUrl(entry.getUrl()))) {
|
||||
if (isRequestDuplicate(
|
||||
messageInfo, entry.getRequestResponse(),
|
||||
url, entry.getUrl(),
|
||||
comment, entry.getComment(),
|
||||
color, entry.getColor()
|
||||
)) {
|
||||
isDuplicate = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
if (!isDuplicate) {
|
||||
if (flag) {
|
||||
persistData(messageInfo, comment, color);
|
||||
}
|
||||
log.add(new MessageEntry(messageInfo, method, url, comment, length, color, status));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isRequestDuplicate(
|
||||
HttpRequestResponse newReq, HttpRequestResponse existingReq,
|
||||
String newUrl, String existingUrl,
|
||||
String newComment, String existingComment,
|
||||
String newColor, String existingColor) {
|
||||
try {
|
||||
// 基础属性匹配
|
||||
String normalizedNewUrl = normalizeUrl(newUrl);
|
||||
String normalizedExistingUrl = normalizeUrl(existingUrl);
|
||||
boolean basicMatch = normalizedNewUrl.equals(normalizedExistingUrl);
|
||||
|
||||
// 请求响应内容匹配
|
||||
byte[] newReqBytes = newReq.request().toByteArray().getBytes();
|
||||
byte[] newResBytes = newReq.response().toByteArray().getBytes();
|
||||
byte[] existingReqBytes = existingReq.request().toByteArray().getBytes();
|
||||
byte[] existingResBytes = existingReq.response().toByteArray().getBytes();
|
||||
boolean contentMatch = Arrays.equals(newReqBytes, existingReqBytes) &&
|
||||
Arrays.equals(newResBytes, existingResBytes);
|
||||
|
||||
// 注释和颜色匹配
|
||||
boolean metadataMatch = areCommentsEqual(newComment, existingComment) &&
|
||||
newColor.equals(existingColor);
|
||||
|
||||
return (basicMatch || contentMatch) && metadataMatch;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeUrl(String url) {
|
||||
if (url == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
String normalized = url.trim().toLowerCase();
|
||||
while (normalized.endsWith("/")) {
|
||||
normalized = normalized.substring(0, normalized.length() - 1);
|
||||
}
|
||||
|
||||
return normalized.replaceAll("//", "/");
|
||||
}
|
||||
|
||||
private boolean areCommentsEqual(String comment1, String comment2) {
|
||||
if (comment1 == null || comment2 == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 将注释按规则拆分并排序
|
||||
Set<String> rules1 = new TreeSet<>(Arrays.asList(comment1.split(", ")));
|
||||
Set<String> rules2 = new TreeSet<>(Arrays.asList(comment2.split(", ")));
|
||||
|
||||
return rules1.equals(rules2);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void persistData(HttpRequestResponse messageInfo, String comment, String color) {
|
||||
try {
|
||||
DataManager dataManager = new DataManager(api);
|
||||
PersistedObject persistedObject = PersistedObject.persistedObject();
|
||||
persistedObject.setHttpRequestResponse("messageInfo", messageInfo);
|
||||
persistedObject.setString("comment", comment);
|
||||
persistedObject.setString("color", color);
|
||||
String uuidIndex = StringProcessor.getRandomUUID();
|
||||
dataManager.putData("message", uuidIndex, persistedObject);
|
||||
} catch (Exception e) {
|
||||
api.logging().logToError("Data persistence error: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void deleteByHost(String filterText) {
|
||||
filteredLog.clear();
|
||||
List<Integer> rowsToRemove = new ArrayList<>();
|
||||
|
||||
if (currentWorker != null && !currentWorker.isDone()) {
|
||||
currentWorker.cancel(true);
|
||||
}
|
||||
|
||||
currentWorker = new SwingWorker<>() {
|
||||
@Override
|
||||
protected Void doInBackground() {
|
||||
for (int i = 0; i < log.size(); i++) {
|
||||
MessageEntry entry = log.get(i);
|
||||
String host = StringProcessor.getHostByUrl(entry.getUrl());
|
||||
if (!host.isEmpty()) {
|
||||
if (StringProcessor.matchesHostPattern(host, filterText) || filterText.equals("*")) {
|
||||
rowsToRemove.add(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = rowsToRemove.size() - 1; i >= 0; i--) {
|
||||
int row = rowsToRemove.get(i);
|
||||
log.remove(row);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
currentWorker.execute();
|
||||
}
|
||||
|
||||
public void applyHostFilter(String filterText) {
|
||||
// 预分配合适的容量,避免频繁扩容
|
||||
final List<MessageEntry> newFilteredLog = new ArrayList<>(log.size() / 2);
|
||||
|
||||
// 预处理过滤条件,优化性能
|
||||
final boolean isWildcardFilter = "*".equals(filterText) || filterText.contains("*");
|
||||
final String normalizedFilter = filterText.toLowerCase().trim();
|
||||
|
||||
// 创建log的安全副本
|
||||
final List<MessageEntry> logSnapshot;
|
||||
synchronized (log) {
|
||||
logSnapshot = new ArrayList<>(log);
|
||||
}
|
||||
|
||||
// 使用并行流高效过滤,但保持有序
|
||||
logSnapshot.parallelStream()
|
||||
.filter(entry -> {
|
||||
// 快速通配符检查
|
||||
if (isWildcardFilter && "*".equals(filterText)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
String host = StringProcessor.getHostByUrl(entry.getUrl());
|
||||
if (host.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 优化后的匹配逻辑
|
||||
return StringProcessor.matchesHostPattern(host, filterText) ||
|
||||
(isWildcardFilter && host.toLowerCase().contains(normalizedFilter.replace("*", "")));
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.forEachOrdered(newFilteredLog::add);
|
||||
|
||||
// 一次性更新UI,避免频繁刷新
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
synchronized (filteredLog) {
|
||||
filteredLog.clear();
|
||||
filteredLog.addAll(newFilteredLog);
|
||||
}
|
||||
fireTableDataChanged();
|
||||
});
|
||||
}
|
||||
|
||||
public void applyMessageFilter(String tableName, String filterText) {
|
||||
List<MessageEntry> newFilteredLog = new ArrayList<>();
|
||||
|
||||
// 创建log的安全副本以避免ConcurrentModificationException
|
||||
List<MessageEntry> logSnapshot;
|
||||
synchronized (log) {
|
||||
logSnapshot = new ArrayList<>(log);
|
||||
}
|
||||
|
||||
for (MessageEntry entry : logSnapshot) {
|
||||
// 标志变量,表示是否满足过滤条件
|
||||
AtomicBoolean isMatched = new AtomicBoolean(false);
|
||||
|
||||
try {
|
||||
HttpRequestResponse requestResponse = entry.getRequestResponse();
|
||||
HttpRequest httpRequest = requestResponse.request();
|
||||
HttpResponse httpResponse = requestResponse.response();
|
||||
|
||||
String requestString = new String(httpRequest.toByteArray().getBytes(), StandardCharsets.UTF_8);
|
||||
String requestBody = new String(httpRequest.body().getBytes(), StandardCharsets.UTF_8);
|
||||
String requestHeaders = httpRequest.headers().stream()
|
||||
.map(HttpHeader::toString)
|
||||
.collect(Collectors.joining("\r\n"));
|
||||
|
||||
String responseString = new String(httpResponse.toByteArray().getBytes(), StandardCharsets.UTF_8);
|
||||
String responseBody = new String(httpResponse.body().getBytes(), StandardCharsets.UTF_8);
|
||||
String responseHeaders = httpResponse.headers().stream()
|
||||
.map(HttpHeader::toString)
|
||||
.collect(Collectors.joining("\r\n"));
|
||||
|
||||
Config.globalRules.keySet().forEach(i -> {
|
||||
for (Object[] objects : Config.globalRules.get(i)) {
|
||||
String name = objects[1].toString();
|
||||
String format = objects[4].toString();
|
||||
String scope = objects[6].toString();
|
||||
|
||||
// 从注释中查看是否包含当前规则名,包含的再进行查询,有效减少无意义的检索时间
|
||||
if (entry.getComment().contains(name)) {
|
||||
if (name.equals(tableName)) {
|
||||
// 标志变量,表示当前规则是否匹配
|
||||
boolean isMatch = false;
|
||||
|
||||
switch (scope) {
|
||||
case "any":
|
||||
isMatch = matchingString(format, filterText, requestString) || matchingString(format, filterText, responseString);
|
||||
break;
|
||||
case "request":
|
||||
isMatch = matchingString(format, filterText, requestString);
|
||||
break;
|
||||
case "response":
|
||||
isMatch = matchingString(format, filterText, responseString);
|
||||
break;
|
||||
case "any header":
|
||||
isMatch = matchingString(format, filterText, requestHeaders) || matchingString(format, filterText, responseHeaders);
|
||||
break;
|
||||
case "request header":
|
||||
isMatch = matchingString(format, filterText, requestHeaders);
|
||||
break;
|
||||
case "response header":
|
||||
isMatch = matchingString(format, filterText, responseHeaders);
|
||||
break;
|
||||
case "any body":
|
||||
isMatch = matchingString(format, filterText, requestBody) || matchingString(format, filterText, responseBody);
|
||||
break;
|
||||
case "request body":
|
||||
isMatch = matchingString(format, filterText, requestBody);
|
||||
break;
|
||||
case "response body":
|
||||
isMatch = matchingString(format, filterText, responseBody);
|
||||
break;
|
||||
case "request line":
|
||||
String requestLine = requestString.split("\\r?\\n", 2)[0];
|
||||
isMatch = matchingString(format, filterText, requestLine);
|
||||
break;
|
||||
case "response line":
|
||||
String responseLine = responseString.split("\\r?\\n", 2)[0];
|
||||
isMatch = matchingString(format, filterText, responseLine);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
isMatched.set(isMatch);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 由于每个用户规则不同,如果进行项目文件共享则需要考虑全部匹配一下
|
||||
if (!isMatched.get()) {
|
||||
isMatched.set(matchingString("{0}", filterText, requestString) || matchingString("{0}", filterText, responseString));
|
||||
}
|
||||
|
||||
if (isMatched.get()) {
|
||||
newFilteredLog.add(entry);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// 在EDT线程中更新UI
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
synchronized (filteredLog) {
|
||||
filteredLog.clear();
|
||||
filteredLog.addAll(newFilteredLog);
|
||||
}
|
||||
fireTableDataChanged();
|
||||
messageTable.lastSelectedIndex = -1;
|
||||
});
|
||||
}
|
||||
|
||||
private boolean matchingString(String format, String filterText, String target) {
|
||||
boolean isMatch = true;
|
||||
|
||||
try {
|
||||
MessageFormat mf = new MessageFormat(format);
|
||||
Object[] parsedObjects = mf.parse(filterText);
|
||||
|
||||
for (Object parsedObject : parsedObjects) {
|
||||
if (!target.contains(parsedObject.toString())) {
|
||||
isMatch = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
isMatch = false;
|
||||
}
|
||||
|
||||
return isMatch;
|
||||
}
|
||||
|
||||
public JSplitPane getSplitPane() {
|
||||
return splitPane;
|
||||
}
|
||||
|
||||
public MessageTable getMessageTable() {
|
||||
return messageTable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRowCount() {
|
||||
synchronized (filteredLog) {
|
||||
return filteredLog.size();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColumnCount() {
|
||||
return 6;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getValueAt(int rowIndex, int columnIndex) {
|
||||
synchronized (filteredLog) {
|
||||
if (rowIndex < 0 || rowIndex >= filteredLog.size()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
try {
|
||||
MessageEntry messageEntry = filteredLog.get(rowIndex);
|
||||
if (messageEntry == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return switch (columnIndex) {
|
||||
case 0 -> messageEntry.getMethod();
|
||||
case 1 -> messageEntry.getUrl();
|
||||
case 2 -> messageEntry.getComment();
|
||||
case 3 -> messageEntry.getStatus();
|
||||
case 4 -> messageEntry.getLength();
|
||||
case 5 -> messageEntry.getColor();
|
||||
default -> "";
|
||||
};
|
||||
} catch (Exception e) {
|
||||
api.logging().logToError("getValueAt: " + e.getMessage());
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getColumnName(int columnIndex) {
|
||||
return switch (columnIndex) {
|
||||
case 0 -> "Method";
|
||||
case 1 -> "URL";
|
||||
case 2 -> "Comment";
|
||||
case 3 -> "Status";
|
||||
case 4 -> "Length";
|
||||
case 5 -> "Color";
|
||||
default -> "";
|
||||
};
|
||||
}
|
||||
|
||||
public class MessageTable extends JTable {
|
||||
private final ExecutorService executorService;
|
||||
private final HttpRequestEditor requestEditor;
|
||||
private final HttpResponseEditor responseEditor;
|
||||
private int lastSelectedIndex = -1;
|
||||
|
||||
public MessageTable(TableModel messageTableModel, HttpRequestEditor requestEditor, HttpResponseEditor responseEditor) {
|
||||
super(messageTableModel);
|
||||
this.requestEditor = requestEditor;
|
||||
this.responseEditor = responseEditor;
|
||||
this.executorService = Executors.newSingleThreadExecutor();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changeSelection(int row, int col, boolean toggle, boolean extend) {
|
||||
super.changeSelection(row, col, toggle, extend);
|
||||
int selectedIndex = convertRowIndexToModel(row);
|
||||
if (lastSelectedIndex != selectedIndex) {
|
||||
lastSelectedIndex = selectedIndex;
|
||||
executorService.execute(this::getSelectedMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private void getSelectedMessage() {
|
||||
MessageEntry messageEntry = filteredLog.get(lastSelectedIndex);
|
||||
|
||||
HttpRequestResponse httpRequestResponse = messageEntry.getRequestResponse();
|
||||
|
||||
requestEditor.setRequest(HttpRequest.httpRequest(messageEntry.getRequestResponse().httpService(), httpRequestResponse.request().toByteArray()));
|
||||
int responseSizeWithMb = httpRequestResponse.response().toString().length() / 1024 / 1024;
|
||||
if ((responseSizeWithMb < Integer.parseInt(configLoader.getLimitSize())) || configLoader.getLimitSize().equals("0")) {
|
||||
responseEditor.setResponse(httpRequestResponse.response());
|
||||
} else {
|
||||
responseEditor.setResponse(HttpResponse.httpResponse("Exceeds length limit."));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
277
src/main/java/hae/component/board/table/Datatable.java
Normal file
@@ -0,0 +1,277 @@
|
||||
package hae.component.board.table;
|
||||
|
||||
import burp.api.montoya.MontoyaApi;
|
||||
import hae.component.board.message.MessageTableModel;
|
||||
import hae.utils.ConfigLoader;
|
||||
import hae.utils.UIEnhancer;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.event.DocumentEvent;
|
||||
import javax.swing.event.DocumentListener;
|
||||
import javax.swing.table.DefaultTableModel;
|
||||
import javax.swing.table.TableColumn;
|
||||
import javax.swing.table.TableRowSorter;
|
||||
import java.awt.*;
|
||||
import java.awt.datatransfer.Clipboard;
|
||||
import java.awt.datatransfer.StringSelection;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class Datatable extends JPanel {
|
||||
private final MontoyaApi api;
|
||||
private final ConfigLoader configLoader;
|
||||
private final JTable dataTable;
|
||||
private final DefaultTableModel dataTableModel;
|
||||
private final JTextField searchField;
|
||||
private final JTextField secondSearchField;
|
||||
private final TableRowSorter<DefaultTableModel> sorter;
|
||||
private final JCheckBox searchMode = new JCheckBox("Reverse search");
|
||||
private final JCheckBox regexMode = new JCheckBox("Regex mode");
|
||||
private final String tabName;
|
||||
private final JPanel footerPanel;
|
||||
private SwingWorker<Void, Void> doubleClickWorker;
|
||||
|
||||
public Datatable(MontoyaApi api, ConfigLoader configLoader, String tabName, List<String> dataList) {
|
||||
this.api = api;
|
||||
this.configLoader = configLoader;
|
||||
this.tabName = tabName;
|
||||
|
||||
String[] columnNames = {"#", "Information"};
|
||||
this.dataTableModel = new DefaultTableModel(columnNames, 0);
|
||||
|
||||
this.dataTable = new JTable(dataTableModel);
|
||||
this.sorter = new TableRowSorter<>(dataTableModel);
|
||||
this.searchField = new JTextField(10);
|
||||
this.secondSearchField = new JTextField(10);
|
||||
this.footerPanel = new JPanel(new BorderLayout(0, 5));
|
||||
|
||||
initComponents(dataList);
|
||||
}
|
||||
|
||||
private void initComponents(List<String> dataList) {
|
||||
dataTable.setRowSorter(sorter);
|
||||
|
||||
// 设置ID排序
|
||||
sorter.setComparator(0, (Comparator<Integer>) Integer::compareTo);
|
||||
|
||||
for (String item : dataList) {
|
||||
if (!item.isEmpty()) {
|
||||
addRowToTable(new Object[]{item});
|
||||
}
|
||||
}
|
||||
|
||||
UIEnhancer.setTextFieldPlaceholder(searchField, "Search");
|
||||
searchField.getDocument().addDocumentListener(new DocumentListener() {
|
||||
@Override
|
||||
public void insertUpdate(DocumentEvent e) {
|
||||
performSearch();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeUpdate(DocumentEvent e) {
|
||||
performSearch();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changedUpdate(DocumentEvent e) {
|
||||
performSearch();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
UIEnhancer.setTextFieldPlaceholder(secondSearchField, "Second search");
|
||||
secondSearchField.getDocument().addDocumentListener(new DocumentListener() {
|
||||
@Override
|
||||
public void insertUpdate(DocumentEvent e) {
|
||||
performSearch();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeUpdate(DocumentEvent e) {
|
||||
performSearch();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changedUpdate(DocumentEvent e) {
|
||||
performSearch();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// 设置布局
|
||||
JScrollPane scrollPane = new JScrollPane(dataTable);
|
||||
scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
|
||||
|
||||
TableColumn idColumn = dataTable.getColumnModel().getColumn(0);
|
||||
idColumn.setPreferredWidth(50);
|
||||
idColumn.setMaxWidth(100);
|
||||
|
||||
setLayout(new BorderLayout(0, 5));
|
||||
|
||||
JPanel optionsPanel = new JPanel();
|
||||
optionsPanel.setLayout(new BoxLayout(optionsPanel, BoxLayout.X_AXIS));
|
||||
|
||||
// Settings按钮
|
||||
JPanel settingMenuPanel = new JPanel(new GridLayout(2, 1));
|
||||
settingMenuPanel.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
|
||||
JPopupMenu settingMenu = new JPopupMenu();
|
||||
settingMenuPanel.add(searchMode);
|
||||
settingMenuPanel.add(regexMode);
|
||||
regexMode.setSelected(true);
|
||||
searchMode.addItemListener(e -> performSearch());
|
||||
settingMenu.add(settingMenuPanel);
|
||||
|
||||
JButton settingsButton = new JButton("Settings");
|
||||
setMenuShow(settingMenu, settingsButton);
|
||||
|
||||
optionsPanel.add(settingsButton);
|
||||
optionsPanel.add(Box.createHorizontalStrut(5));
|
||||
optionsPanel.add(searchField);
|
||||
optionsPanel.add(Box.createHorizontalStrut(5));
|
||||
optionsPanel.add(secondSearchField);
|
||||
|
||||
footerPanel.setBorder(BorderFactory.createEmptyBorder(2, 3, 5, 3));
|
||||
footerPanel.add(optionsPanel, BorderLayout.CENTER);
|
||||
|
||||
add(scrollPane, BorderLayout.CENTER);
|
||||
add(footerPanel, BorderLayout.SOUTH);
|
||||
}
|
||||
|
||||
private void setMenuShow(JPopupMenu menu, JButton button) {
|
||||
button.addActionListener(e -> {
|
||||
Point buttonLocation = button.getLocationOnScreen();
|
||||
Dimension menuSize = menu.getPreferredSize();
|
||||
int x = buttonLocation.x + (button.getWidth() - menuSize.width) / 2;
|
||||
int y = buttonLocation.y - menuSize.height;
|
||||
menu.show(button, x - buttonLocation.x, y - buttonLocation.y);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private void addRowToTable(Object[] data) {
|
||||
int rowCount = dataTableModel.getRowCount();
|
||||
int id = rowCount > 0 ? (Integer) dataTableModel.getValueAt(rowCount - 1, 0) + 1 : 1;
|
||||
Object[] rowData = new Object[data.length + 1];
|
||||
rowData[0] = id;
|
||||
System.arraycopy(data, 0, rowData, 1, data.length);
|
||||
dataTableModel.addRow(rowData);
|
||||
}
|
||||
|
||||
private void performSearch() {
|
||||
List<RowFilter<Object, Object>> filters = new ArrayList<>();
|
||||
|
||||
if (UIEnhancer.hasUserInput(searchField)) {
|
||||
filters.add(getObjectObjectRowFilter(searchField, true));
|
||||
}
|
||||
|
||||
if (UIEnhancer.hasUserInput(secondSearchField)) {
|
||||
filters.add(getObjectObjectRowFilter(secondSearchField, false));
|
||||
}
|
||||
|
||||
sorter.setRowFilter(filters.isEmpty() ? null : RowFilter.andFilter(filters));
|
||||
}
|
||||
|
||||
private RowFilter<Object, Object> getObjectObjectRowFilter(JTextField searchField, boolean firstFlag) {
|
||||
return new RowFilter<>() {
|
||||
public boolean include(Entry<?, ?> entry) {
|
||||
String searchFieldTextText = searchField.getText();
|
||||
searchFieldTextText = searchFieldTextText.toLowerCase();
|
||||
String entryValue = ((String) entry.getValue(1)).toLowerCase();
|
||||
boolean filterReturn = searchFieldTextText.isEmpty();
|
||||
boolean firstFlagReturn = searchMode.isSelected() && firstFlag;
|
||||
if (regexMode.isSelected()) {
|
||||
Pattern pattern = null;
|
||||
try {
|
||||
pattern = Pattern.compile(searchFieldTextText, Pattern.CASE_INSENSITIVE);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
if (pattern != null) {
|
||||
filterReturn = filterReturn || pattern.matcher(entryValue).find() != firstFlagReturn;
|
||||
}
|
||||
} else {
|
||||
filterReturn = filterReturn || entryValue.contains(searchFieldTextText) != firstFlagReturn;
|
||||
}
|
||||
|
||||
return filterReturn;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void handleDoubleClick(int selectedRow, MessageTableModel messagePanel) {
|
||||
if (doubleClickWorker != null && !doubleClickWorker.isDone()) {
|
||||
doubleClickWorker.cancel(true);
|
||||
}
|
||||
|
||||
doubleClickWorker = new SwingWorker<>() {
|
||||
@Override
|
||||
protected Void doInBackground() {
|
||||
String rowData = dataTable.getValueAt(selectedRow, 1).toString();
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
if (!isCancelled()) {
|
||||
messagePanel.applyMessageFilter(tabName, rowData);
|
||||
}
|
||||
});
|
||||
return null;
|
||||
}
|
||||
};
|
||||
doubleClickWorker.execute();
|
||||
}
|
||||
|
||||
public void setTableListener(MessageTableModel messagePanel) {
|
||||
// 表格复制功能
|
||||
dataTable.setTransferHandler(new TransferHandler() {
|
||||
@Override
|
||||
public void exportToClipboard(JComponent comp, Clipboard clip, int action) throws IllegalStateException {
|
||||
if (comp instanceof JTable) {
|
||||
StringSelection stringSelection = new StringSelection(getSelectedDataAtTable((JTable) comp).replace("\0", "").replaceAll("[\\p{Cntrl}&&[^\r\n\t]]", ""));
|
||||
clip.setContents(stringSelection, null);
|
||||
} else {
|
||||
super.exportToClipboard(comp, clip, action);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
dataTable.setDefaultEditor(Object.class, null);
|
||||
|
||||
// 表格内容双击事件
|
||||
dataTable.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
if (e.getClickCount() == 2) {
|
||||
int selectedRow = dataTable.getSelectedRow();
|
||||
if (selectedRow != -1) {
|
||||
handleDoubleClick(selectedRow, messagePanel);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public String getSelectedDataAtTable(JTable table) {
|
||||
int[] selectRows = table.getSelectedRows();
|
||||
StringBuilder selectData = new StringBuilder();
|
||||
|
||||
for (int row : selectRows) {
|
||||
selectData.append(table.getValueAt(row, 1).toString()).append("\r\n");
|
||||
}
|
||||
|
||||
if (!selectData.isEmpty()) {
|
||||
selectData.delete(selectData.length() - 2, selectData.length());
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
|
||||
return selectData.toString();
|
||||
}
|
||||
|
||||
|
||||
public JTable getDataTable() {
|
||||
return this.dataTable;
|
||||
}
|
||||
}
|
||||
|
||||
79
src/main/java/hae/component/rule/Display.java
Normal file
@@ -0,0 +1,79 @@
|
||||
package hae.component.rule;
|
||||
|
||||
import hae.Config;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
|
||||
public class Display extends JPanel {
|
||||
public JTextField firstRegexTextField;
|
||||
public JTextField secondRegexTextField;
|
||||
public JTextField formatTextField;
|
||||
public JTextField ruleNameTextField;
|
||||
public JComboBox<String> scopeComboBox;
|
||||
public JComboBox<String> engineComboBox;
|
||||
public JComboBox<String> colorComboBox;
|
||||
public JComboBox<Boolean> sensitiveComboBox;
|
||||
|
||||
public Display() {
|
||||
initComponents();
|
||||
}
|
||||
|
||||
private void initComponents() {
|
||||
setLayout(new GridBagLayout());
|
||||
GridBagConstraints c = new GridBagConstraints();
|
||||
c.fill = GridBagConstraints.BOTH;
|
||||
|
||||
addLabel("Name:", 0, c);
|
||||
ruleNameTextField = addTextField(0, c);
|
||||
|
||||
addLabel("F-Regex:", 1, c);
|
||||
firstRegexTextField = addTextField(1, c);
|
||||
|
||||
addLabel("S-Regex:", 2, c);
|
||||
secondRegexTextField = addTextField(2, c);
|
||||
|
||||
addLabel("Format:", 3, c);
|
||||
formatTextField = addTextField(3, c);
|
||||
|
||||
addLabel("Scope:", 4, c);
|
||||
scopeComboBox = addComboBox(Config.scope, 4, c);
|
||||
|
||||
addLabel("Engine:", 5, c);
|
||||
engineComboBox = addComboBox(Config.engine, 5, c);
|
||||
engineComboBox.addActionListener(e -> {
|
||||
boolean isNfa = "nfa".equals(engineComboBox.getSelectedItem().toString());
|
||||
formatTextField.setEnabled(isNfa);
|
||||
formatTextField.setText(isNfa ? formatTextField.getText() : "{0}");
|
||||
});
|
||||
|
||||
addLabel("Color:", 6, c);
|
||||
colorComboBox = addComboBox(Config.color, 6, c);
|
||||
|
||||
addLabel("Sensitive:", 7, c);
|
||||
sensitiveComboBox = addComboBox(new Boolean[]{true, false}, 7, c);
|
||||
}
|
||||
|
||||
private void addLabel(String text, int y, GridBagConstraints c) {
|
||||
JLabel label = new JLabel(text);
|
||||
c.gridx = 0;
|
||||
c.gridy = y;
|
||||
add(label, c);
|
||||
}
|
||||
|
||||
private JTextField addTextField(int y, GridBagConstraints c) {
|
||||
JTextField textField = new JTextField(35);
|
||||
c.gridx = 1;
|
||||
c.gridy = y;
|
||||
add(textField, c);
|
||||
return textField;
|
||||
}
|
||||
|
||||
private <T> JComboBox<T> addComboBox(T[] items, int y, GridBagConstraints c) {
|
||||
JComboBox<T> comboBox = new JComboBox<>(items);
|
||||
c.gridx = 1;
|
||||
c.gridy = y;
|
||||
add(comboBox, c);
|
||||
return comboBox;
|
||||
}
|
||||
}
|
||||
414
src/main/java/hae/component/rule/Rule.java
Normal file
@@ -0,0 +1,414 @@
|
||||
package hae.component.rule;
|
||||
|
||||
import burp.api.montoya.MontoyaApi;
|
||||
import hae.Config;
|
||||
import hae.utils.ConfigLoader;
|
||||
import hae.utils.rule.RuleProcessor;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.table.DefaultTableModel;
|
||||
import javax.swing.table.JTableHeader;
|
||||
import javax.swing.table.TableCellRenderer;
|
||||
import javax.swing.table.TableRowSorter;
|
||||
import java.awt.*;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.util.Vector;
|
||||
|
||||
import static javax.swing.JOptionPane.YES_OPTION;
|
||||
|
||||
public class Rule extends JPanel {
|
||||
private final MontoyaApi api;
|
||||
private final ConfigLoader configLoader;
|
||||
private final RuleProcessor ruleProcessor;
|
||||
private final JTabbedPane tabbedPane;
|
||||
private JCheckBox headerCheckBox;
|
||||
|
||||
public Rule(MontoyaApi api, ConfigLoader configLoader, Object[][] data, JTabbedPane tabbedPane) {
|
||||
this.api = api;
|
||||
this.configLoader = configLoader;
|
||||
this.ruleProcessor = new RuleProcessor(api, configLoader);
|
||||
this.tabbedPane = tabbedPane;
|
||||
|
||||
initComponents(data);
|
||||
}
|
||||
|
||||
private void initComponents(Object[][] data) {
|
||||
setLayout(new GridBagLayout());
|
||||
((GridBagLayout) getLayout()).columnWidths = new int[]{0, 0, 0};
|
||||
((GridBagLayout) getLayout()).rowHeights = new int[]{0, 0, 0, 0, 0};
|
||||
((GridBagLayout) getLayout()).columnWeights = new double[]{0.0, 1.0, 1.0E-4};
|
||||
((GridBagLayout) getLayout()).rowWeights = new double[]{0.0, 0.0, 0.0, 1.0, 1.0E-4};
|
||||
|
||||
JButton copyButton = new JButton("Copy");
|
||||
JButton addButton = new JButton("Add");
|
||||
JButton editButton = new JButton("Edit");
|
||||
JButton removeButton = new JButton("Remove");
|
||||
|
||||
JTable ruleTable = new JTable();
|
||||
JScrollPane scrollPane = new JScrollPane();
|
||||
|
||||
ruleTable.setVerifyInputWhenFocusTarget(false);
|
||||
ruleTable.setUpdateSelectionOnSort(false);
|
||||
ruleTable.setSurrendersFocusOnKeystroke(true);
|
||||
scrollPane.setViewportView(ruleTable);
|
||||
|
||||
// 按钮监听事件
|
||||
copyButton.addActionListener(e -> ruleCopyActionPerformed(e, ruleTable, tabbedPane));
|
||||
addButton.addActionListener(e -> ruleAddActionPerformed(e, ruleTable, tabbedPane));
|
||||
editButton.addActionListener(e -> ruleEditActionPerformed(e, ruleTable, tabbedPane));
|
||||
removeButton.addActionListener(e -> ruleRemoveActionPerformed(e, ruleTable, tabbedPane));
|
||||
|
||||
// 表格
|
||||
DefaultTableModel model = new DefaultTableModel() {
|
||||
@Override
|
||||
public Class<?> getColumnClass(int column) {
|
||||
return (column == 0) ? Boolean.class : String.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCellEditable(int row, int column) {
|
||||
return column == 0;
|
||||
}
|
||||
};
|
||||
|
||||
ruleTable.setModel(model);
|
||||
ruleTable.setRowSorter(new TableRowSorter<>(model));
|
||||
|
||||
model.setDataVector(data, Config.ruleFields);
|
||||
model.addTableModelListener(e -> {
|
||||
if (e.getColumn() == 0 && ruleTable.getSelectedRow() != -1) {
|
||||
int select = ruleTable.convertRowIndexToModel(ruleTable.getSelectedRow());
|
||||
ruleProcessor.changeRule(model.getDataVector().get(select), select, tabbedPane.getTitleAt(tabbedPane.getSelectedIndex()));
|
||||
|
||||
// 更新表头复选框状态并强制重新渲染
|
||||
updateHeaderCheckBoxState(model);
|
||||
ruleTable.getTableHeader().repaint();
|
||||
}
|
||||
});
|
||||
|
||||
// 设置表头复选框
|
||||
setupHeaderCheckBox(ruleTable);
|
||||
|
||||
// 设置Loaded列的宽度(第一列)
|
||||
setupColumnWidths(ruleTable);
|
||||
|
||||
GridBagConstraints constraints = new GridBagConstraints();
|
||||
constraints.weightx = 1.0;
|
||||
constraints.fill = GridBagConstraints.HORIZONTAL;
|
||||
|
||||
JPanel buttonPanel = new JPanel();
|
||||
GridBagLayout layout = new GridBagLayout();
|
||||
layout.rowHeights = new int[]{0, 0, 0, 0, 0, 0, 0};
|
||||
layout.rowWeights = new double[]{0.0, 0.0, 0.0, 0.0, 0.0, 0.0, Double.MIN_VALUE};
|
||||
buttonPanel.setLayout(layout);
|
||||
|
||||
constraints.insets = new Insets(0, 0, 3, 0);
|
||||
constraints.gridy = 0;
|
||||
buttonPanel.add(copyButton, constraints);
|
||||
constraints.gridy = 1;
|
||||
buttonPanel.add(addButton, constraints);
|
||||
constraints.gridy = 2;
|
||||
buttonPanel.add(editButton, constraints);
|
||||
constraints.gridy = 3;
|
||||
buttonPanel.add(removeButton, constraints);
|
||||
|
||||
add(buttonPanel, new GridBagConstraints(0, 0, 1, 1, 0.0, 0.0,
|
||||
GridBagConstraints.CENTER, GridBagConstraints.BOTH,
|
||||
new Insets(15, 5, 3, 2), 0, 0));
|
||||
add(scrollPane, new GridBagConstraints(1, 0, 1, 4, 0.0, 0.0,
|
||||
GridBagConstraints.CENTER, GridBagConstraints.BOTH,
|
||||
new Insets(15, 5, 5, 5), 0, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置列宽度
|
||||
*/
|
||||
private void setupColumnWidths(JTable ruleTable) {
|
||||
// 设置Loaded列(第一列)的宽度
|
||||
ruleTable.getColumnModel().getColumn(0).setPreferredWidth(50);
|
||||
ruleTable.getColumnModel().getColumn(0).setMaxWidth(50);
|
||||
ruleTable.getColumnModel().getColumn(0).setMinWidth(50);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置表头复选框
|
||||
*/
|
||||
private void setupHeaderCheckBox(JTable ruleTable) {
|
||||
// 创建表头复选框
|
||||
headerCheckBox = new JCheckBox();
|
||||
headerCheckBox.setHorizontalAlignment(SwingConstants.CENTER);
|
||||
|
||||
// 设置表头渲染器
|
||||
ruleTable.getTableHeader().setDefaultRenderer(new HeaderCheckBoxRenderer(ruleTable.getTableHeader().getDefaultRenderer()));
|
||||
|
||||
// 添加表头鼠标点击事件
|
||||
ruleTable.getTableHeader().addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
if (e.getClickCount() == 1) {
|
||||
JTableHeader header = (JTableHeader) e.getSource();
|
||||
JTable table = header.getTable();
|
||||
int columnIndex = header.columnAtPoint(e.getPoint());
|
||||
|
||||
if (columnIndex == 0) { // 点击的是Loaded列表头
|
||||
toggleAllRules(table);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义表头渲染器,在Loaded列显示复选框
|
||||
*/
|
||||
private class HeaderCheckBoxRenderer implements TableCellRenderer {
|
||||
private final TableCellRenderer originalRenderer;
|
||||
|
||||
public HeaderCheckBoxRenderer(TableCellRenderer originalRenderer) {
|
||||
this.originalRenderer = originalRenderer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
|
||||
if (column == 0) { // Loaded列
|
||||
// 获取原始表头组件作为背景
|
||||
Component originalComponent = originalRenderer.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
|
||||
|
||||
// 创建一个面板来包含复选框,保持原始样式
|
||||
JPanel panel = new JPanel(new BorderLayout());
|
||||
panel.setOpaque(true);
|
||||
|
||||
// 复制原始组件的样式
|
||||
if (originalComponent instanceof JComponent origComp) {
|
||||
panel.setBackground(origComp.getBackground());
|
||||
panel.setBorder(origComp.getBorder());
|
||||
}
|
||||
|
||||
// 更新复选框状态并添加到面板中心
|
||||
updateHeaderCheckBoxState((DefaultTableModel) table.getModel());
|
||||
headerCheckBox.setOpaque(false); // 让复选框透明,显示背景
|
||||
panel.add(headerCheckBox, BorderLayout.CENTER);
|
||||
|
||||
return panel;
|
||||
} else {
|
||||
return originalRenderer.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换所有规则的开启/关闭状态
|
||||
*/
|
||||
private void toggleAllRules(JTable ruleTable) {
|
||||
DefaultTableModel model = (DefaultTableModel) ruleTable.getModel();
|
||||
int rowCount = model.getRowCount();
|
||||
|
||||
if (rowCount == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 判断当前状态:如果所有规则都开启,则关闭所有;否则开启所有
|
||||
boolean allEnabled = true;
|
||||
for (int i = 0; i < rowCount; i++) {
|
||||
if (!(Boolean) model.getValueAt(i, 0)) {
|
||||
allEnabled = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
boolean newState = !allEnabled;
|
||||
|
||||
// 更新所有行的状态
|
||||
for (int i = 0; i < rowCount; i++) {
|
||||
model.setValueAt(newState, i, 0);
|
||||
// 通知规则处理器更新规则状态
|
||||
ruleProcessor.changeRule(model.getDataVector().get(i), i, getCurrentTabTitle());
|
||||
}
|
||||
|
||||
// 更新表头复选框状态
|
||||
updateHeaderCheckBoxState(model);
|
||||
|
||||
// 刷新表格和表头
|
||||
ruleTable.repaint();
|
||||
ruleTable.getTableHeader().repaint();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新表头复选框的状态
|
||||
*/
|
||||
private void updateHeaderCheckBoxState(DefaultTableModel model) {
|
||||
int rowCount = model.getRowCount();
|
||||
if (rowCount == 0) {
|
||||
headerCheckBox.setSelected(false);
|
||||
headerCheckBox.getModel().setArmed(false);
|
||||
headerCheckBox.getModel().setPressed(false);
|
||||
return;
|
||||
}
|
||||
|
||||
int enabledCount = 0;
|
||||
for (int i = 0; i < rowCount; i++) {
|
||||
if ((Boolean) model.getValueAt(i, 0)) {
|
||||
enabledCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (enabledCount == 0) {
|
||||
// 全部未选中
|
||||
headerCheckBox.setSelected(false);
|
||||
headerCheckBox.getModel().setArmed(false);
|
||||
headerCheckBox.getModel().setPressed(false);
|
||||
} else if (enabledCount == rowCount) {
|
||||
// 全部选中
|
||||
headerCheckBox.setSelected(true);
|
||||
headerCheckBox.getModel().setArmed(false);
|
||||
headerCheckBox.getModel().setPressed(false);
|
||||
} else {
|
||||
// 部分选中 - 显示为按下但未选中的状态
|
||||
headerCheckBox.setSelected(false);
|
||||
headerCheckBox.getModel().setArmed(true);
|
||||
headerCheckBox.getModel().setPressed(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 填充Display对象的字段值
|
||||
*/
|
||||
private void populateDisplayFromTable(Display ruleDisplay, JTable ruleTable, int selectedRow) {
|
||||
ruleDisplay.ruleNameTextField.setText(ruleTable.getValueAt(selectedRow, 1).toString());
|
||||
ruleDisplay.firstRegexTextField.setText(ruleTable.getValueAt(selectedRow, 2).toString());
|
||||
ruleDisplay.secondRegexTextField.setText(ruleTable.getValueAt(selectedRow, 3).toString());
|
||||
ruleDisplay.formatTextField.setText(ruleTable.getValueAt(selectedRow, 4).toString());
|
||||
ruleDisplay.colorComboBox.setSelectedItem(ruleTable.getValueAt(selectedRow, 5).toString());
|
||||
ruleDisplay.scopeComboBox.setSelectedItem(ruleTable.getValueAt(selectedRow, 6).toString());
|
||||
ruleDisplay.engineComboBox.setSelectedItem(ruleTable.getValueAt(selectedRow, 7).toString());
|
||||
ruleDisplay.sensitiveComboBox.setSelectedItem(ruleTable.getValueAt(selectedRow, 8));
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Display对象创建规则数据Vector
|
||||
*/
|
||||
private Vector<Object> createRuleDataFromDisplay(Display ruleDisplay) {
|
||||
Vector<Object> ruleData = new Vector<>();
|
||||
ruleData.add(false);
|
||||
ruleData.add(ruleDisplay.ruleNameTextField.getText());
|
||||
ruleData.add(ruleDisplay.firstRegexTextField.getText());
|
||||
ruleData.add(ruleDisplay.secondRegexTextField.getText());
|
||||
ruleData.add(ruleDisplay.formatTextField.getText());
|
||||
ruleData.add(ruleDisplay.colorComboBox.getSelectedItem().toString());
|
||||
ruleData.add(ruleDisplay.scopeComboBox.getSelectedItem().toString());
|
||||
ruleData.add(ruleDisplay.engineComboBox.getSelectedItem().toString());
|
||||
ruleData.add(ruleDisplay.sensitiveComboBox.getSelectedItem());
|
||||
return ruleData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示规则编辑对话框
|
||||
*/
|
||||
private boolean showRuleDialog(Display ruleDisplay, String title) {
|
||||
ruleDisplay.formatTextField.setEnabled(ruleDisplay.engineComboBox.getSelectedItem().toString().equals("nfa"));
|
||||
int showState = JOptionPane.showConfirmDialog(this, ruleDisplay, title, JOptionPane.YES_NO_OPTION);
|
||||
return showState == YES_OPTION;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有选中的行
|
||||
*/
|
||||
private boolean hasSelectedRow(JTable ruleTable) {
|
||||
return ruleTable.getSelectedRowCount() >= 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前选中的Tab标题
|
||||
*/
|
||||
private String getCurrentTabTitle() {
|
||||
return tabbedPane.getTitleAt(tabbedPane.getSelectedIndex());
|
||||
}
|
||||
|
||||
private void ruleCopyActionPerformed(ActionEvent e, JTable ruleTable, JTabbedPane tabbedPane) {
|
||||
if (!hasSelectedRow(ruleTable)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Display ruleDisplay = new Display();
|
||||
int selectedRow = ruleTable.getSelectedRow();
|
||||
|
||||
populateDisplayFromTable(ruleDisplay, ruleTable, selectedRow);
|
||||
// 为复制的规则名称添加前缀
|
||||
ruleDisplay.ruleNameTextField.setText(String.format("Copy of %s", ruleDisplay.ruleNameTextField.getText()));
|
||||
|
||||
if (showRuleDialog(ruleDisplay, "Copy Rule")) {
|
||||
Vector<Object> ruleData = createRuleDataFromDisplay(ruleDisplay);
|
||||
DefaultTableModel model = (DefaultTableModel) ruleTable.getModel();
|
||||
model.insertRow(model.getRowCount(), ruleData);
|
||||
ruleProcessor.addRule(ruleData, getCurrentTabTitle());
|
||||
|
||||
// 复制规则后更新表头复选框状态
|
||||
updateHeaderCheckBoxState(model);
|
||||
ruleTable.getTableHeader().repaint();
|
||||
}
|
||||
}
|
||||
|
||||
private void ruleAddActionPerformed(ActionEvent e, JTable ruleTable, JTabbedPane tabbedPane) {
|
||||
Display ruleDisplay = new Display();
|
||||
ruleDisplay.formatTextField.setText("{0}");
|
||||
|
||||
if (showRuleDialog(ruleDisplay, "Add Rule")) {
|
||||
Vector<Object> ruleData = createRuleDataFromDisplay(ruleDisplay);
|
||||
DefaultTableModel model = (DefaultTableModel) ruleTable.getModel();
|
||||
model.insertRow(model.getRowCount(), ruleData);
|
||||
ruleProcessor.addRule(ruleData, getCurrentTabTitle());
|
||||
|
||||
// 添加规则后更新表头复选框状态
|
||||
updateHeaderCheckBoxState(model);
|
||||
ruleTable.getTableHeader().repaint();
|
||||
}
|
||||
}
|
||||
|
||||
private void ruleEditActionPerformed(ActionEvent e, JTable ruleTable, JTabbedPane tabbedPane) {
|
||||
if (!hasSelectedRow(ruleTable)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Display ruleDisplay = new Display();
|
||||
int selectedRow = ruleTable.getSelectedRow();
|
||||
|
||||
populateDisplayFromTable(ruleDisplay, ruleTable, selectedRow);
|
||||
|
||||
if (showRuleDialog(ruleDisplay, "Edit Rule")) {
|
||||
DefaultTableModel model = (DefaultTableModel) ruleTable.getModel();
|
||||
int modelIndex = ruleTable.convertRowIndexToModel(selectedRow);
|
||||
|
||||
// 更新表格数据
|
||||
Vector<Object> ruleData = createRuleDataFromDisplay(ruleDisplay);
|
||||
for (int i = 1; i < ruleData.size(); i++) {
|
||||
model.setValueAt(ruleData.get(i), modelIndex, i);
|
||||
}
|
||||
|
||||
ruleProcessor.changeRule(model.getDataVector().get(modelIndex), modelIndex, getCurrentTabTitle());
|
||||
|
||||
// 编辑规则后更新表头复选框状态(如果编辑影响了启用状态)
|
||||
updateHeaderCheckBoxState(model);
|
||||
ruleTable.getTableHeader().repaint();
|
||||
}
|
||||
}
|
||||
|
||||
private void ruleRemoveActionPerformed(ActionEvent e, JTable ruleTable, JTabbedPane tabbedPane) {
|
||||
if (!hasSelectedRow(ruleTable)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (JOptionPane.showConfirmDialog(this, "Are you sure you want to remove this rule?", "Info", JOptionPane.YES_NO_OPTION) == 0) {
|
||||
DefaultTableModel model = (DefaultTableModel) ruleTable.getModel();
|
||||
int select = ruleTable.convertRowIndexToModel(ruleTable.getSelectedRow());
|
||||
|
||||
model.removeRow(select);
|
||||
ruleProcessor.removeRule(select, getCurrentTabTitle());
|
||||
|
||||
// 删除规则后更新表头复选框状态
|
||||
updateHeaderCheckBoxState(model);
|
||||
ruleTable.getTableHeader().repaint();
|
||||
}
|
||||
}
|
||||
}
|
||||
158
src/main/java/hae/component/rule/Rules.java
Normal file
@@ -0,0 +1,158 @@
|
||||
package hae.component.rule;
|
||||
|
||||
import burp.api.montoya.MontoyaApi;
|
||||
import hae.Config;
|
||||
import hae.utils.ConfigLoader;
|
||||
import hae.utils.rule.RuleProcessor;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
|
||||
public class Rules extends JTabbedPane {
|
||||
private final MontoyaApi api;
|
||||
private final RuleProcessor ruleProcessor;
|
||||
private final JTextField ruleGroupNameTextField;
|
||||
private ConfigLoader configLoader;
|
||||
private Component tabComponent;
|
||||
private int selectedIndex;
|
||||
private final Action cancelActionPerformed = new AbstractAction() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
if (selectedIndex >= 0) {
|
||||
setTabComponentAt(selectedIndex, tabComponent);
|
||||
|
||||
ruleGroupNameTextField.setVisible(false);
|
||||
ruleGroupNameTextField.setPreferredSize(null);
|
||||
selectedIndex = -1;
|
||||
tabComponent = null;
|
||||
|
||||
requestFocusInWindow();
|
||||
}
|
||||
}
|
||||
};
|
||||
private final Action renameTitleActionPerformed = new AbstractAction() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
String title = ruleGroupNameTextField.getText();
|
||||
if (!title.isEmpty() && selectedIndex >= 0) {
|
||||
String oldName = getTitleAt(selectedIndex);
|
||||
setTitleAt(selectedIndex, title);
|
||||
|
||||
if (!oldName.equals(title)) {
|
||||
ruleProcessor.renameRuleGroup(oldName, title);
|
||||
}
|
||||
}
|
||||
cancelActionPerformed.actionPerformed(null);
|
||||
}
|
||||
};
|
||||
|
||||
public Rules(MontoyaApi api, ConfigLoader configLoader) {
|
||||
this.api = api;
|
||||
this.configLoader = configLoader;
|
||||
this.ruleProcessor = new RuleProcessor(api, configLoader);
|
||||
this.ruleGroupNameTextField = new JTextField();
|
||||
|
||||
initComponents();
|
||||
}
|
||||
|
||||
private void initComponents() {
|
||||
reloadRuleGroup();
|
||||
|
||||
JMenuItem deleteMenuItem = new JMenuItem("Delete");
|
||||
JPopupMenu popupMenu = new JPopupMenu();
|
||||
popupMenu.add(deleteMenuItem);
|
||||
|
||||
deleteMenuItem.addActionListener(this::deleteRuleGroupActionPerformed);
|
||||
|
||||
ruleGroupNameTextField.setBorder(BorderFactory.createEmptyBorder());
|
||||
ruleGroupNameTextField.addFocusListener(new FocusAdapter() {
|
||||
@Override
|
||||
public void focusLost(FocusEvent e) {
|
||||
renameTitleActionPerformed.actionPerformed(null);
|
||||
}
|
||||
});
|
||||
|
||||
addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mousePressed(MouseEvent e) {
|
||||
int index = indexAtLocation(e.getX(), e.getY());
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (e.getButton()) {
|
||||
case MouseEvent.BUTTON1:
|
||||
if (e.getClickCount() == 2) {
|
||||
selectedIndex = index;
|
||||
tabComponent = getTabComponentAt(selectedIndex);
|
||||
String ruleGroupName = getTitleAt(selectedIndex);
|
||||
|
||||
if (!"...".equals(ruleGroupName)) {
|
||||
setTabComponentAt(selectedIndex, ruleGroupNameTextField);
|
||||
ruleGroupNameTextField.setVisible(true);
|
||||
ruleGroupNameTextField.setText(ruleGroupName);
|
||||
ruleGroupNameTextField.selectAll();
|
||||
ruleGroupNameTextField.requestFocusInWindow();
|
||||
ruleGroupNameTextField.setMinimumSize(ruleGroupNameTextField.getPreferredSize());
|
||||
}
|
||||
} else if (e.getClickCount() == 1) {
|
||||
String title = getTitleAt(index);
|
||||
if ("...".equals(title)) {
|
||||
// 阻止默认的选中行为
|
||||
e.consume();
|
||||
// 直接创建新标签
|
||||
String newTitle = ruleProcessor.newRule();
|
||||
Rule newRule = new Rule(api, configLoader, Config.ruleTemplate, Rules.this);
|
||||
insertTab(newTitle, null, newRule, null, getTabCount() - 1);
|
||||
setSelectedIndex(getTabCount() - 2);
|
||||
} else {
|
||||
renameTitleActionPerformed.actionPerformed(null);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case MouseEvent.BUTTON3:
|
||||
if (!"...".equals(getTitleAt(index))) {
|
||||
popupMenu.show(e.getComponent(), e.getX(), e.getY());
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
InputMap im = ruleGroupNameTextField.getInputMap(JComponent.WHEN_FOCUSED);
|
||||
ActionMap am = ruleGroupNameTextField.getActionMap();
|
||||
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "cancel");
|
||||
am.put("cancel", cancelActionPerformed);
|
||||
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "rename");
|
||||
am.put("rename", renameTitleActionPerformed);
|
||||
}
|
||||
|
||||
public void reloadRuleGroup() {
|
||||
removeAll();
|
||||
|
||||
this.configLoader = new ConfigLoader(api);
|
||||
Config.globalRules.keySet().forEach(i -> addTab(i, new Rule(api, configLoader, hae.Config.globalRules.get(i), this)));
|
||||
addTab("...", null);
|
||||
}
|
||||
|
||||
private void deleteRuleGroupActionPerformed(ActionEvent e) {
|
||||
if (getTabCount() > 2) {
|
||||
int retCode = JOptionPane.showConfirmDialog(this, "Do you want to delete this rule group?", "Info",
|
||||
JOptionPane.YES_NO_OPTION);
|
||||
if (retCode == JOptionPane.YES_OPTION) {
|
||||
String title = getTitleAt(getSelectedIndex());
|
||||
ruleProcessor.deleteRuleGroup(title);
|
||||
remove(getSelectedIndex());
|
||||
setSelectedIndex(getSelectedIndex() - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
142
src/main/java/hae/instances/editor/RequestEditor.java
Normal file
@@ -0,0 +1,142 @@
|
||||
package hae.instances.editor;
|
||||
|
||||
import burp.api.montoya.MontoyaApi;
|
||||
import burp.api.montoya.core.ByteArray;
|
||||
import burp.api.montoya.core.Range;
|
||||
import burp.api.montoya.http.message.HttpRequestResponse;
|
||||
import burp.api.montoya.http.message.requests.HttpRequest;
|
||||
import burp.api.montoya.ui.Selection;
|
||||
import burp.api.montoya.ui.editor.extension.EditorCreationContext;
|
||||
import burp.api.montoya.ui.editor.extension.ExtensionProvidedHttpRequestEditor;
|
||||
import burp.api.montoya.ui.editor.extension.HttpRequestEditorProvider;
|
||||
import hae.Config;
|
||||
import hae.component.board.table.Datatable;
|
||||
import hae.instances.http.utils.MessageProcessor;
|
||||
import hae.utils.ConfigLoader;
|
||||
import hae.utils.http.HttpUtils;
|
||||
import hae.utils.string.StringProcessor;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class RequestEditor implements HttpRequestEditorProvider {
|
||||
private final MontoyaApi api;
|
||||
private final ConfigLoader configLoader;
|
||||
|
||||
public RequestEditor(MontoyaApi api, ConfigLoader configLoader) {
|
||||
this.api = api;
|
||||
this.configLoader = configLoader;
|
||||
}
|
||||
|
||||
public static boolean isListHasData(List<Map<String, String>> dataList) {
|
||||
if (dataList != null && !dataList.isEmpty()) {
|
||||
Map<String, String> dataMap = dataList.get(0);
|
||||
return dataMap != null && !dataMap.isEmpty();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void generateTabbedPaneFromResultMap(MontoyaApi api, ConfigLoader configLoader, JTabbedPane tabbedPane, List<Map<String, String>> result) {
|
||||
tabbedPane.removeAll();
|
||||
if (result != null && !result.isEmpty()) {
|
||||
Map<String, String> dataMap = result.get(0);
|
||||
if (dataMap != null && !dataMap.isEmpty()) {
|
||||
dataMap.keySet().forEach(i -> {
|
||||
String[] extractData = dataMap.get(i).split(Config.boundary);
|
||||
Datatable dataPanel = new Datatable(api, configLoader, i, Arrays.asList(extractData));
|
||||
tabbedPane.addTab(i, dataPanel);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExtensionProvidedHttpRequestEditor provideHttpRequestEditor(EditorCreationContext editorCreationContext) {
|
||||
return new Editor(api, configLoader, editorCreationContext);
|
||||
}
|
||||
|
||||
private static class Editor implements ExtensionProvidedHttpRequestEditor {
|
||||
private final MontoyaApi api;
|
||||
private final ConfigLoader configLoader;
|
||||
private final HttpUtils httpUtils;
|
||||
private final EditorCreationContext creationContext;
|
||||
private final MessageProcessor messageProcessor;
|
||||
private final JTabbedPane jTabbedPane = new JTabbedPane();
|
||||
private HttpRequestResponse requestResponse;
|
||||
private List<Map<String, String>> dataList;
|
||||
|
||||
public Editor(MontoyaApi api, ConfigLoader configLoader, EditorCreationContext creationContext) {
|
||||
this.api = api;
|
||||
this.configLoader = configLoader;
|
||||
this.httpUtils = new HttpUtils(api, configLoader);
|
||||
this.creationContext = creationContext;
|
||||
this.messageProcessor = new MessageProcessor(api, configLoader);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpRequest getRequest() {
|
||||
return requestResponse.request();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRequestResponse(HttpRequestResponse requestResponse) {
|
||||
this.requestResponse = requestResponse;
|
||||
generateTabbedPaneFromResultMap(api, configLoader, jTabbedPane, this.dataList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized boolean isEnabledFor(HttpRequestResponse requestResponse) {
|
||||
HttpRequest request = requestResponse.request();
|
||||
if (request != null) {
|
||||
try {
|
||||
String host = StringProcessor.getHostByUrl(request.url());
|
||||
if (!host.isEmpty()) {
|
||||
String toolType = creationContext.toolSource().toolType().toolName();
|
||||
boolean matches = httpUtils.verifyHttpRequestResponse(requestResponse, toolType);
|
||||
|
||||
if (!matches) {
|
||||
this.dataList = messageProcessor.processRequest("", request, false);
|
||||
return isListHasData(this.dataList);
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String caption() {
|
||||
return "MarkInfo";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component uiComponent() {
|
||||
return jTabbedPane;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Selection selectedData() {
|
||||
return new Selection() {
|
||||
@Override
|
||||
public ByteArray contents() {
|
||||
Datatable dataTable = (Datatable) jTabbedPane.getSelectedComponent();
|
||||
return ByteArray.byteArray(dataTable.getSelectedDataAtTable(dataTable.getDataTable()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Range offsets() {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isModified() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
126
src/main/java/hae/instances/editor/ResponseEditor.java
Normal file
@@ -0,0 +1,126 @@
|
||||
package hae.instances.editor;
|
||||
|
||||
import burp.api.montoya.MontoyaApi;
|
||||
import burp.api.montoya.core.ByteArray;
|
||||
import burp.api.montoya.core.Range;
|
||||
import burp.api.montoya.http.message.HttpRequestResponse;
|
||||
import burp.api.montoya.http.message.requests.HttpRequest;
|
||||
import burp.api.montoya.http.message.responses.HttpResponse;
|
||||
import burp.api.montoya.ui.Selection;
|
||||
import burp.api.montoya.ui.editor.extension.EditorCreationContext;
|
||||
import burp.api.montoya.ui.editor.extension.ExtensionProvidedHttpResponseEditor;
|
||||
import burp.api.montoya.ui.editor.extension.HttpResponseEditorProvider;
|
||||
import hae.component.board.table.Datatable;
|
||||
import hae.instances.http.utils.MessageProcessor;
|
||||
import hae.utils.ConfigLoader;
|
||||
import hae.utils.http.HttpUtils;
|
||||
import hae.utils.string.StringProcessor;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class ResponseEditor implements HttpResponseEditorProvider {
|
||||
private final MontoyaApi api;
|
||||
private final ConfigLoader configLoader;
|
||||
|
||||
public ResponseEditor(MontoyaApi api, ConfigLoader configLoader) {
|
||||
this.api = api;
|
||||
this.configLoader = configLoader;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExtensionProvidedHttpResponseEditor provideHttpResponseEditor(EditorCreationContext editorCreationContext) {
|
||||
return new Editor(api, configLoader, editorCreationContext);
|
||||
}
|
||||
|
||||
private static class Editor implements ExtensionProvidedHttpResponseEditor {
|
||||
private final MontoyaApi api;
|
||||
private final ConfigLoader configLoader;
|
||||
private final HttpUtils httpUtils;
|
||||
private final EditorCreationContext creationContext;
|
||||
private final MessageProcessor messageProcessor;
|
||||
private final JTabbedPane jTabbedPane = new JTabbedPane();
|
||||
private HttpRequestResponse requestResponse;
|
||||
private List<Map<String, String>> dataList;
|
||||
|
||||
public Editor(MontoyaApi api, ConfigLoader configLoader, EditorCreationContext creationContext) {
|
||||
this.api = api;
|
||||
this.configLoader = configLoader;
|
||||
this.httpUtils = new HttpUtils(api, configLoader);
|
||||
this.creationContext = creationContext;
|
||||
this.messageProcessor = new MessageProcessor(api, configLoader);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpResponse getResponse() {
|
||||
return requestResponse.response();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRequestResponse(HttpRequestResponse requestResponse) {
|
||||
this.requestResponse = requestResponse;
|
||||
RequestEditor.generateTabbedPaneFromResultMap(api, configLoader, jTabbedPane, this.dataList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized boolean isEnabledFor(HttpRequestResponse requestResponse) {
|
||||
HttpResponse response = requestResponse.response();
|
||||
|
||||
if (response != null) {
|
||||
HttpRequest request = requestResponse.request();
|
||||
boolean matches = false;
|
||||
|
||||
if (request != null) {
|
||||
try {
|
||||
String host = StringProcessor.getHostByUrl(request.url());
|
||||
if (!host.isEmpty()) {
|
||||
String toolType = creationContext.toolSource().toolType().toolName();
|
||||
matches = httpUtils.verifyHttpRequestResponse(requestResponse, toolType);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!matches) {
|
||||
this.dataList = messageProcessor.processResponse("", response, false);
|
||||
return RequestEditor.isListHasData(this.dataList);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String caption() {
|
||||
return "MarkInfo";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component uiComponent() {
|
||||
return jTabbedPane;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Selection selectedData() {
|
||||
return new Selection() {
|
||||
@Override
|
||||
public ByteArray contents() {
|
||||
Datatable dataTable = (Datatable) jTabbedPane.getSelectedComponent();
|
||||
return ByteArray.byteArray(dataTable.getSelectedDataAtTable(dataTable.getDataTable()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Range offsets() {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isModified() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
102
src/main/java/hae/instances/editor/WebSocketEditor.java
Normal file
@@ -0,0 +1,102 @@
|
||||
package hae.instances.editor;
|
||||
|
||||
import burp.api.montoya.MontoyaApi;
|
||||
import burp.api.montoya.core.ByteArray;
|
||||
import burp.api.montoya.core.Range;
|
||||
import burp.api.montoya.ui.Selection;
|
||||
import burp.api.montoya.ui.contextmenu.WebSocketMessage;
|
||||
import burp.api.montoya.ui.editor.extension.EditorCreationContext;
|
||||
import burp.api.montoya.ui.editor.extension.ExtensionProvidedWebSocketMessageEditor;
|
||||
import burp.api.montoya.ui.editor.extension.WebSocketMessageEditorProvider;
|
||||
import hae.component.board.table.Datatable;
|
||||
import hae.instances.http.utils.MessageProcessor;
|
||||
import hae.utils.ConfigLoader;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class WebSocketEditor implements WebSocketMessageEditorProvider {
|
||||
private final MontoyaApi api;
|
||||
private final ConfigLoader configLoader;
|
||||
|
||||
public WebSocketEditor(MontoyaApi api, ConfigLoader configLoader) {
|
||||
this.api = api;
|
||||
this.configLoader = configLoader;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExtensionProvidedWebSocketMessageEditor provideMessageEditor(EditorCreationContext editorCreationContext) {
|
||||
return new Editor(api, configLoader, editorCreationContext);
|
||||
}
|
||||
|
||||
private static class Editor implements ExtensionProvidedWebSocketMessageEditor {
|
||||
private final MontoyaApi api;
|
||||
private final ConfigLoader configLoader;
|
||||
private final EditorCreationContext creationContext;
|
||||
private final MessageProcessor messageProcessor;
|
||||
private final JTabbedPane jTabbedPane = new JTabbedPane();
|
||||
private ByteArray message;
|
||||
private List<Map<String, String>> dataList;
|
||||
|
||||
public Editor(MontoyaApi api, ConfigLoader configLoader, EditorCreationContext creationContext) {
|
||||
this.api = api;
|
||||
this.configLoader = configLoader;
|
||||
this.creationContext = creationContext;
|
||||
this.messageProcessor = new MessageProcessor(api, configLoader);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteArray getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMessage(WebSocketMessage webSocketMessage) {
|
||||
this.message = webSocketMessage.payload();
|
||||
RequestEditor.generateTabbedPaneFromResultMap(api, configLoader, jTabbedPane, this.dataList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabledFor(WebSocketMessage webSocketMessage) {
|
||||
String websocketMessage = webSocketMessage.payload().toString();
|
||||
if (!websocketMessage.isEmpty()) {
|
||||
this.dataList = messageProcessor.processMessage("", websocketMessage, false);
|
||||
return RequestEditor.isListHasData(this.dataList);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String caption() {
|
||||
return "MarkInfo";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component uiComponent() {
|
||||
return jTabbedPane;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Selection selectedData() {
|
||||
return new Selection() {
|
||||
@Override
|
||||
public ByteArray contents() {
|
||||
Datatable dataTable = (Datatable) jTabbedPane.getSelectedComponent();
|
||||
return ByteArray.byteArray(dataTable.getSelectedDataAtTable(dataTable.getDataTable()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Range offsets() {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isModified() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
106
src/main/java/hae/instances/http/HttpMessageActiveHandler.java
Normal file
@@ -0,0 +1,106 @@
|
||||
package hae.instances.http;
|
||||
|
||||
import burp.api.montoya.MontoyaApi;
|
||||
import burp.api.montoya.core.Annotations;
|
||||
import burp.api.montoya.core.HighlightColor;
|
||||
import burp.api.montoya.http.handler.*;
|
||||
import burp.api.montoya.http.message.HttpRequestResponse;
|
||||
import burp.api.montoya.http.message.requests.HttpRequest;
|
||||
import hae.component.board.message.MessageTableModel;
|
||||
import hae.instances.http.utils.MessageProcessor;
|
||||
import hae.utils.ConfigLoader;
|
||||
import hae.utils.http.HttpUtils;
|
||||
import hae.utils.string.StringProcessor;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class HttpMessageActiveHandler implements HttpHandler {
|
||||
private final MontoyaApi api;
|
||||
private final ConfigLoader configLoader;
|
||||
private final HttpUtils httpUtils;
|
||||
private final MessageTableModel messageTableModel;
|
||||
private final MessageProcessor messageProcessor;
|
||||
|
||||
// Montoya API对HTTP消息的处理分为了请求和响应,因此此处设置高亮和标记需要使用全局变量的方式,以此兼顾请求和响应
|
||||
// 同时采用 ThreadLocal 来保证多线程并发的情况下全局变量的安全性
|
||||
private final ThreadLocal<String> host = ThreadLocal.withInitial(() -> "");
|
||||
private final ThreadLocal<List<String>> colorList = ThreadLocal.withInitial(ArrayList::new);
|
||||
private final ThreadLocal<List<String>> commentList = ThreadLocal.withInitial(ArrayList::new);
|
||||
|
||||
public HttpMessageActiveHandler(MontoyaApi api, ConfigLoader configLoader, MessageTableModel messageTableModel) {
|
||||
this.api = api;
|
||||
this.configLoader = configLoader;
|
||||
this.httpUtils = new HttpUtils(api, configLoader);
|
||||
this.messageTableModel = messageTableModel;
|
||||
this.messageProcessor = new MessageProcessor(api, configLoader);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestToBeSentAction handleHttpRequestToBeSent(HttpRequestToBeSent httpRequestToBeSent) {
|
||||
colorList.get().clear();
|
||||
commentList.get().clear();
|
||||
|
||||
Annotations annotations = httpRequestToBeSent.annotations();
|
||||
|
||||
try {
|
||||
host.set(StringProcessor.getHostByUrl(httpRequestToBeSent.url()));
|
||||
} catch (Exception e) {
|
||||
api.logging().logToError("handleHttpRequestToBeSent: " + e.getMessage());
|
||||
}
|
||||
|
||||
return RequestToBeSentAction.continueWith(httpRequestToBeSent, annotations);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseReceivedAction handleHttpResponseReceived(HttpResponseReceived httpResponseReceived) {
|
||||
Annotations annotations = httpResponseReceived.annotations();
|
||||
HttpRequest request = httpResponseReceived.initiatingRequest();
|
||||
HttpRequestResponse requestResponse = HttpRequestResponse.httpRequestResponse(request, httpResponseReceived);
|
||||
String toolType = httpResponseReceived.toolSource().toolType().toolName();
|
||||
|
||||
boolean matches = httpUtils.verifyHttpRequestResponse(requestResponse, toolType);
|
||||
|
||||
if (!matches) {
|
||||
try {
|
||||
setColorAndCommentList(messageProcessor.processRequest(host.get(), request, true));
|
||||
setColorAndCommentList(messageProcessor.processResponse(host.get(), httpResponseReceived, true));
|
||||
|
||||
if (!colorList.get().isEmpty() && !commentList.get().isEmpty()) {
|
||||
HttpRequestResponse httpRequestResponse = HttpRequestResponse.httpRequestResponse(request, httpResponseReceived);
|
||||
|
||||
String color = messageProcessor.retrieveFinalColor(messageProcessor.retrieveColorIndices(colorList.get()));
|
||||
annotations.setHighlightColor(HighlightColor.highlightColor(color));
|
||||
String comment = StringProcessor.mergeComment(String.join(", ", commentList.get()));
|
||||
annotations.setNotes(comment);
|
||||
|
||||
String method = request.method();
|
||||
String url = request.url();
|
||||
String status = String.valueOf(httpResponseReceived.statusCode());
|
||||
String length = String.valueOf(httpResponseReceived.toByteArray().length());
|
||||
|
||||
new SwingWorker<Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground() {
|
||||
messageTableModel.add(httpRequestResponse, url, method, status, length, comment, color, true);
|
||||
return null;
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
api.logging().logToError("handleHttpResponseReceived: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return ResponseReceivedAction.continueWith(httpResponseReceived, annotations);
|
||||
}
|
||||
|
||||
private void setColorAndCommentList(List<Map<String, String>> result) {
|
||||
if (result != null && !result.isEmpty()) {
|
||||
colorList.get().add(result.get(0).get("color"));
|
||||
commentList.get().add(result.get(1).get("comment"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package hae.instances.http;
|
||||
|
||||
import burp.api.montoya.MontoyaApi;
|
||||
import burp.api.montoya.http.message.HttpRequestResponse;
|
||||
import burp.api.montoya.http.message.requests.HttpRequest;
|
||||
import burp.api.montoya.http.message.responses.HttpResponse;
|
||||
import burp.api.montoya.scanner.AuditResult;
|
||||
import burp.api.montoya.scanner.ConsolidationAction;
|
||||
import burp.api.montoya.scanner.ScanCheck;
|
||||
import burp.api.montoya.scanner.audit.insertionpoint.AuditInsertionPoint;
|
||||
import burp.api.montoya.scanner.audit.issues.AuditIssue;
|
||||
import hae.component.board.message.MessageTableModel;
|
||||
import hae.instances.http.utils.MessageProcessor;
|
||||
import hae.utils.ConfigLoader;
|
||||
import hae.utils.http.HttpUtils;
|
||||
import hae.utils.string.StringProcessor;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static burp.api.montoya.scanner.AuditResult.auditResult;
|
||||
import static burp.api.montoya.scanner.ConsolidationAction.KEEP_BOTH;
|
||||
import static burp.api.montoya.scanner.ConsolidationAction.KEEP_EXISTING;
|
||||
import static java.util.Collections.emptyList;
|
||||
|
||||
public class HttpMessagePassiveHandler implements ScanCheck {
|
||||
private final MontoyaApi api;
|
||||
private final ConfigLoader configLoader;
|
||||
private final HttpUtils httpUtils;
|
||||
private final MessageTableModel messageTableModel;
|
||||
private final MessageProcessor messageProcessor;
|
||||
|
||||
public HttpMessagePassiveHandler(MontoyaApi api, ConfigLoader configLoader, MessageTableModel messageTableModel) {
|
||||
this.api = api;
|
||||
this.configLoader = configLoader;
|
||||
this.httpUtils = new HttpUtils(api, configLoader);
|
||||
this.messageTableModel = messageTableModel;
|
||||
this.messageProcessor = new MessageProcessor(api, configLoader);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuditResult activeAudit(HttpRequestResponse httpRequestResponse, AuditInsertionPoint auditInsertionPoint) {
|
||||
return auditResult(emptyList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuditResult passiveAudit(HttpRequestResponse httpRequestResponse) {
|
||||
List<String> colorList = new ArrayList<>();
|
||||
List<String> commentList = new ArrayList<>();
|
||||
|
||||
HttpRequest request = httpRequestResponse.request();
|
||||
HttpResponse response = httpRequestResponse.response();
|
||||
|
||||
boolean matches = httpUtils.verifyHttpRequestResponse(httpRequestResponse, "Proxy");
|
||||
|
||||
if (!matches) {
|
||||
try {
|
||||
String host = StringProcessor.getHostByUrl(request.url());
|
||||
setColorAndCommentList(messageProcessor.processRequest(host, request, true), colorList, commentList);
|
||||
setColorAndCommentList(messageProcessor.processResponse(host, response, true), colorList, commentList);
|
||||
|
||||
String url = request.url();
|
||||
String method = request.method();
|
||||
String status = String.valueOf(response.statusCode());
|
||||
String color = messageProcessor.retrieveFinalColor(messageProcessor.retrieveColorIndices(colorList));
|
||||
String comment = StringProcessor.mergeComment(String.join(", ", commentList));
|
||||
String length = String.valueOf(response.toByteArray().length());
|
||||
|
||||
new SwingWorker<Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground() {
|
||||
messageTableModel.add(httpRequestResponse, url, method, status, length, comment, color, true);
|
||||
return null;
|
||||
}
|
||||
}.execute();
|
||||
} catch (Exception e) {
|
||||
api.logging().logToError("passiveAudit: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return auditResult(emptyList());
|
||||
}
|
||||
|
||||
private void setColorAndCommentList(List<Map<String, String>> result, List<String> colorList, List<String> commentList) {
|
||||
if (result != null && !result.isEmpty()) {
|
||||
colorList.add(result.get(0).get("color"));
|
||||
commentList.add(result.get(1).get("comment"));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConsolidationAction consolidateIssues(AuditIssue newIssue, AuditIssue existingIssue) {
|
||||
return existingIssue.name().equals(newIssue.name()) ? KEEP_EXISTING : KEEP_BOTH;
|
||||
}
|
||||
}
|
||||
173
src/main/java/hae/instances/http/utils/MessageProcessor.java
Normal file
@@ -0,0 +1,173 @@
|
||||
package hae.instances.http.utils;
|
||||
|
||||
import burp.api.montoya.MontoyaApi;
|
||||
import burp.api.montoya.http.message.HttpHeader;
|
||||
import burp.api.montoya.http.message.requests.HttpRequest;
|
||||
import burp.api.montoya.http.message.responses.HttpResponse;
|
||||
import hae.Config;
|
||||
import hae.utils.ConfigLoader;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class MessageProcessor {
|
||||
private final MontoyaApi api;
|
||||
private final RegularMatcher regularMatcher;
|
||||
|
||||
public MessageProcessor(MontoyaApi api, ConfigLoader configLoader) {
|
||||
this.api = api;
|
||||
this.regularMatcher = new RegularMatcher(api, configLoader);
|
||||
}
|
||||
|
||||
public List<Map<String, String>> processMessage(String host, String message, boolean flag) {
|
||||
Map<String, Map<String, Object>> obj = null;
|
||||
|
||||
try {
|
||||
obj = regularMatcher.performRegexMatching(host, "any", message, message, message);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
return getDataList(obj, flag);
|
||||
}
|
||||
|
||||
public List<Map<String, String>> processResponse(String host, HttpResponse httpResponse, boolean flag) {
|
||||
Map<String, Map<String, Object>> obj = null;
|
||||
|
||||
try {
|
||||
String response = new String(httpResponse.toByteArray().getBytes(), StandardCharsets.UTF_8);
|
||||
String body = new String(httpResponse.body().getBytes(), StandardCharsets.UTF_8);
|
||||
String header = httpResponse.headers().stream()
|
||||
.map(HttpHeader::toString)
|
||||
.collect(Collectors.joining("\r\n"));
|
||||
|
||||
obj = regularMatcher.performRegexMatching(host, "response", response, header, body);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
return getDataList(obj, flag);
|
||||
}
|
||||
|
||||
public List<Map<String, String>> processRequest(String host, HttpRequest httpRequest, boolean flag) {
|
||||
Map<String, Map<String, Object>> obj = null;
|
||||
|
||||
try {
|
||||
String request = new String(httpRequest.toByteArray().getBytes(), StandardCharsets.UTF_8);
|
||||
String body = new String(httpRequest.body().getBytes(), StandardCharsets.UTF_8);
|
||||
String header = httpRequest.headers().stream()
|
||||
.map(HttpHeader::toString)
|
||||
.collect(Collectors.joining("\r\n"));
|
||||
|
||||
obj = regularMatcher.performRegexMatching(host, "request", request, header, body);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
return getDataList(obj, flag);
|
||||
}
|
||||
|
||||
private List<Map<String, String>> getDataList(Map<String, Map<String, Object>> obj, boolean actionFlag) {
|
||||
List<Map<String, String>> highlightList = new ArrayList<>();
|
||||
List<Map<String, String>> extractList = new ArrayList<>();
|
||||
|
||||
if (obj != null && !obj.isEmpty()) {
|
||||
if (actionFlag) {
|
||||
List<List<String>> resultList = extractColorsAndComments(obj);
|
||||
List<String> colorList = resultList.get(0);
|
||||
List<String> commentList = resultList.get(1);
|
||||
if (!colorList.isEmpty() && !commentList.isEmpty()) {
|
||||
String color = retrieveFinalColor(retrieveColorIndices(colorList));
|
||||
Map<String, String> colorMap = new HashMap<>() {{
|
||||
put("color", color);
|
||||
}};
|
||||
Map<String, String> commentMap = new HashMap<>() {{
|
||||
put("comment", String.join(", ", commentList));
|
||||
}};
|
||||
highlightList.add(colorMap);
|
||||
highlightList.add(commentMap);
|
||||
}
|
||||
} else {
|
||||
extractList.add(extractDataFromMap(obj));
|
||||
}
|
||||
}
|
||||
|
||||
return actionFlag ? highlightList : extractList;
|
||||
}
|
||||
|
||||
private Map<String, String> extractDataFromMap(Map<String, Map<String, Object>> inputData) {
|
||||
Map<String, String> extractedData = new HashMap<>();
|
||||
inputData.keySet().forEach(key -> {
|
||||
Map<String, Object> tempMap = inputData.get(key);
|
||||
String data = tempMap.get("data").toString();
|
||||
extractedData.put(key, data);
|
||||
});
|
||||
|
||||
return extractedData;
|
||||
}
|
||||
|
||||
private List<List<String>> extractColorsAndComments(Map<String, Map<String, Object>> inputData) {
|
||||
List<String> colorList = new ArrayList<>();
|
||||
List<String> commentList = new ArrayList<>();
|
||||
inputData.keySet().forEach(key -> {
|
||||
Map<String, Object> tempMap = inputData.get(key);
|
||||
String color = tempMap.get("color").toString();
|
||||
colorList.add(color);
|
||||
commentList.add(key);
|
||||
});
|
||||
List<List<String>> result = new ArrayList<>();
|
||||
result.add(colorList);
|
||||
result.add(commentList);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public List<Integer> retrieveColorIndices(List<String> colors) {
|
||||
List<Integer> indices = new ArrayList<>();
|
||||
String[] colorArray = Config.color;
|
||||
int size = colorArray.length;
|
||||
|
||||
for (String color : colors) {
|
||||
for (int i = 0; i < size; i++) {
|
||||
if (colorArray[i].equals(color)) {
|
||||
indices.add(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return indices;
|
||||
}
|
||||
|
||||
private String upgradeColors(List<Integer> colorList) {
|
||||
if (colorList == null || colorList.isEmpty()) {
|
||||
return Config.color[0];
|
||||
}
|
||||
|
||||
// 创建副本避免修改原始数据
|
||||
List<Integer> indices = new ArrayList<>(colorList);
|
||||
indices.sort(Comparator.comparingInt(Integer::intValue));
|
||||
|
||||
// 处理颜色升级
|
||||
for (int i = 1; i < indices.size(); i++) {
|
||||
if (indices.get(i).equals(indices.get(i - 1))) {
|
||||
// 如果发现重复的颜色索引,将当前索引降级
|
||||
indices.set(i - 1, indices.get(i - 1) - 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取最终的颜色索引
|
||||
int finalIndex = indices.stream()
|
||||
.min(Integer::compareTo)
|
||||
.orElse(0);
|
||||
|
||||
// 处理负数索引情况
|
||||
if (finalIndex < 0) {
|
||||
return Config.color[0];
|
||||
}
|
||||
|
||||
return Config.color[finalIndex];
|
||||
}
|
||||
|
||||
public String retrieveFinalColor(List<Integer> colorList) {
|
||||
return upgradeColors(colorList);
|
||||
}
|
||||
|
||||
}
|
||||
343
src/main/java/hae/instances/http/utils/RegularMatcher.java
Normal file
@@ -0,0 +1,343 @@
|
||||
package hae.instances.http.utils;
|
||||
|
||||
import burp.api.montoya.MontoyaApi;
|
||||
import burp.api.montoya.persistence.PersistedList;
|
||||
import burp.api.montoya.persistence.PersistedObject;
|
||||
import dk.brics.automaton.Automaton;
|
||||
import dk.brics.automaton.AutomatonMatcher;
|
||||
import dk.brics.automaton.RegExp;
|
||||
import dk.brics.automaton.RunAutomaton;
|
||||
import hae.Config;
|
||||
import hae.cache.DataCache;
|
||||
import hae.utils.ConfigLoader;
|
||||
import hae.utils.DataManager;
|
||||
import hae.utils.string.HashCalculator;
|
||||
import hae.utils.string.StringProcessor;
|
||||
|
||||
import java.text.MessageFormat;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class RegularMatcher {
|
||||
private static final Map<String, Pattern> nfaPatternCache = new ConcurrentHashMap<>();
|
||||
private static final Map<String, RunAutomaton> dfaAutomatonCache = new ConcurrentHashMap<>();
|
||||
private static final Pattern formatIndexPattern = Pattern.compile("\\{(\\d+)}");
|
||||
private final MontoyaApi api;
|
||||
private final ConfigLoader configLoader;
|
||||
|
||||
public RegularMatcher(MontoyaApi api, ConfigLoader configLoader) {
|
||||
this.api = api;
|
||||
this.configLoader = configLoader;
|
||||
}
|
||||
|
||||
public synchronized static void updateGlobalMatchCache(MontoyaApi api, String host, String name, List<String> dataList, boolean flag) {
|
||||
// 添加到全局变量中,便于Databoard检索
|
||||
if (!Objects.equals(host, "") && host != null) {
|
||||
Config.globalDataMap.compute(host, (existingHost, existingMap) -> {
|
||||
Map<String, List<String>> gRuleMap = Optional.ofNullable(existingMap).orElse(new ConcurrentHashMap<>());
|
||||
|
||||
gRuleMap.merge(name, new ArrayList<>(dataList), (existingList, newList) -> {
|
||||
Set<String> combinedSet = new LinkedHashSet<>(existingList);
|
||||
combinedSet.addAll(newList);
|
||||
return new ArrayList<>(combinedSet);
|
||||
});
|
||||
|
||||
if (flag) {
|
||||
// 数据存储在BurpSuite空间内
|
||||
try {
|
||||
DataManager dataManager = new DataManager(api);
|
||||
PersistedObject persistedObject = PersistedObject.persistedObject();
|
||||
gRuleMap.forEach((kName, vList) -> {
|
||||
PersistedList<String> persistedList = PersistedList.persistedStringList();
|
||||
persistedList.addAll(vList);
|
||||
persistedObject.setStringList(kName, persistedList);
|
||||
});
|
||||
dataManager.putData("data", host, persistedObject);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
return gRuleMap;
|
||||
});
|
||||
|
||||
String[] splitHost = host.split("\\.");
|
||||
String onlyHost = host.split(":")[0];
|
||||
|
||||
String anyHost = (splitHost.length > 2 && !StringProcessor.matchHostIsIp(onlyHost)) ? StringProcessor.replaceFirstOccurrence(onlyHost, splitHost[0], "*") : "";
|
||||
|
||||
if (!Config.globalDataMap.containsKey(anyHost) && !anyHost.isEmpty()) {
|
||||
// 添加通配符Host,实际数据从查询哪里将所有数据提取
|
||||
Config.globalDataMap.put(anyHost, new HashMap<>());
|
||||
}
|
||||
|
||||
if (!Config.globalDataMap.containsKey("*")) {
|
||||
// 添加通配符全匹配,同上
|
||||
Config.globalDataMap.put("*", new HashMap<>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Map<String, Map<String, Object>> performRegexMatching(String host, String type, String message, String header, String body) {
|
||||
// 删除动态响应头再进行存储
|
||||
String originalMessage = message;
|
||||
String dynamicHeader = configLoader.getDynamicHeader();
|
||||
|
||||
if (!dynamicHeader.isBlank()) {
|
||||
String modifiedHeader = header.replaceAll(String.format("(%s):.*?\r\n", configLoader.getDynamicHeader()), "");
|
||||
message = message.replace(header, modifiedHeader);
|
||||
}
|
||||
|
||||
String messageIndex = HashCalculator.calculateHash(message.getBytes());
|
||||
|
||||
// 从数据缓存中读取
|
||||
Map<String, Map<String, Object>> dataCacheMap = DataCache.get(messageIndex);
|
||||
|
||||
// 存在则返回
|
||||
if (dataCacheMap != null) {
|
||||
return dataCacheMap;
|
||||
}
|
||||
|
||||
// 最终返回的结果
|
||||
String firstLine = originalMessage.split("\\r?\\n", 2)[0];
|
||||
Map<String, Map<String, Object>> finalMap = applyMatchingRules(host, type, originalMessage, firstLine, header, body);
|
||||
|
||||
// 数据缓存写入,有可能是空值,当作匹配过的索引不再匹配
|
||||
DataCache.put(messageIndex, finalMap);
|
||||
|
||||
return finalMap;
|
||||
}
|
||||
|
||||
private Map<String, Map<String, Object>> applyMatchingRules(String host, String type, String message, String firstLine, String header, String body) {
|
||||
Map<String, Map<String, Object>> finalMap = new HashMap<>();
|
||||
|
||||
Config.globalRules.keySet().parallelStream().forEach(i -> {
|
||||
for (Object[] objects : Config.globalRules.get(i)) {
|
||||
String matchContent = "";
|
||||
// 遍历获取规则
|
||||
List<String> result;
|
||||
Map<String, Object> tmpMap = new HashMap<>();
|
||||
|
||||
boolean loaded = (Boolean) objects[0];
|
||||
String name = objects[1].toString();
|
||||
String f_regex = objects[2].toString();
|
||||
String s_regex = objects[3].toString();
|
||||
String format = objects[4].toString();
|
||||
String color = objects[5].toString();
|
||||
String scope = objects[6].toString();
|
||||
String engine = objects[7].toString();
|
||||
boolean sensitive = (Boolean) objects[8];
|
||||
|
||||
// 判断规则是否开启与作用域
|
||||
if (loaded && (scope.contains(type) || scope.contains("any") || type.equals("any"))) {
|
||||
// 在此处检查内容是否缓存,缓存则返回为空
|
||||
switch (scope) {
|
||||
case "any":
|
||||
case "request":
|
||||
case "response":
|
||||
matchContent = message;
|
||||
break;
|
||||
case "any header":
|
||||
case "request header":
|
||||
case "response header":
|
||||
matchContent = header;
|
||||
break;
|
||||
case "any body":
|
||||
case "request body":
|
||||
case "response body":
|
||||
matchContent = body;
|
||||
break;
|
||||
case "request line":
|
||||
case "response line":
|
||||
matchContent = firstLine;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// 匹配内容为空则跳出
|
||||
if (matchContent.isBlank()) {
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
result = new ArrayList<>(executeRegexEngine(f_regex, s_regex, matchContent, format, engine, sensitive));
|
||||
} catch (Exception e) {
|
||||
api.logging().logToError(String.format("[x] Error Info:\nName: %s\nRegex: %s", name, f_regex));
|
||||
api.logging().logToError(e.getMessage());
|
||||
continue;
|
||||
}
|
||||
|
||||
// 去除重复内容
|
||||
HashSet tmpList = new HashSet(result);
|
||||
result.clear();
|
||||
result.addAll(tmpList);
|
||||
|
||||
if (!result.isEmpty()) {
|
||||
tmpMap.put("color", color);
|
||||
String dataStr = String.join(Config.boundary, result);
|
||||
tmpMap.put("data", dataStr);
|
||||
|
||||
String nameAndSize = String.format("%s (%s)", name, result.size());
|
||||
finalMap.put(nameAndSize, tmpMap);
|
||||
|
||||
updateGlobalMatchCache(api, host, name, result, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return finalMap;
|
||||
}
|
||||
|
||||
private List<String> executeRegexEngine(String f_regex, String s_regex, String content, String format, String engine, boolean sensitive) {
|
||||
List<String> retList = new ArrayList<>();
|
||||
if ("nfa".equals(engine)) {
|
||||
Matcher matcher = createPatternMatcher(f_regex, content, sensitive);
|
||||
retList.addAll(extractRegexMatchResults(s_regex, format, sensitive, matcher));
|
||||
} else {
|
||||
// DFA不支持格式化输出,因此不关注format
|
||||
String newContent = content;
|
||||
String newFirstRegex = f_regex;
|
||||
if (!sensitive) {
|
||||
newContent = content.toLowerCase();
|
||||
newFirstRegex = f_regex.toLowerCase();
|
||||
}
|
||||
AutomatonMatcher autoMatcher = createAutomatonMatcher(newFirstRegex, newContent);
|
||||
retList.addAll(extractRegexMatchResults(s_regex, autoMatcher, content));
|
||||
}
|
||||
return retList;
|
||||
}
|
||||
|
||||
private List<String> extractRegexMatchResults(String s_regex, String format, boolean sensitive, Matcher matcher) {
|
||||
List<String> matches = new ArrayList<>();
|
||||
if (s_regex.isEmpty()) {
|
||||
matches.addAll(formatMatchResults(matcher, format));
|
||||
} else {
|
||||
while (matcher.find()) {
|
||||
String matchContent = matcher.group(1);
|
||||
if (!matchContent.isEmpty()) {
|
||||
Matcher secondMatcher = createPatternMatcher(s_regex, matchContent, sensitive);
|
||||
matches.addAll(formatMatchResults(secondMatcher, format));
|
||||
}
|
||||
}
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
private List<String> extractRegexMatchResults(String s_regex, AutomatonMatcher autoMatcher, String content) {
|
||||
List<String> matches = new ArrayList<>();
|
||||
if (s_regex.isEmpty()) {
|
||||
matches.addAll(formatMatchResults(autoMatcher, content));
|
||||
} else {
|
||||
while (autoMatcher.find()) {
|
||||
String s = autoMatcher.group();
|
||||
if (!s.isEmpty()) {
|
||||
autoMatcher = createAutomatonMatcher(s_regex, extractMatchedContent(content, s));
|
||||
matches.addAll(formatMatchResults(autoMatcher, content));
|
||||
}
|
||||
}
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
private List<String> formatMatchResults(Matcher matcher, String format) {
|
||||
List<String> stringList = new ArrayList<>();
|
||||
|
||||
// 当format为{0}时,直接返回第一个捕获组,避免格式化开销
|
||||
if ("{0}".equals(format)) {
|
||||
while (matcher.find()) {
|
||||
if (matcher.groupCount() > 0 && !matcher.group(1).isEmpty()) {
|
||||
stringList.add(matcher.group(1));
|
||||
}
|
||||
}
|
||||
return stringList;
|
||||
}
|
||||
|
||||
// 需要复杂格式化的情况
|
||||
List<Integer> indexList = parseIndexesFromString(format);
|
||||
while (matcher.find()) {
|
||||
if (!matcher.group(1).isEmpty()) {
|
||||
Object[] params = indexList.stream().map(i -> {
|
||||
if (!matcher.group(i + 1).isEmpty()) {
|
||||
return matcher.group(i + 1);
|
||||
}
|
||||
return "";
|
||||
}).toArray();
|
||||
|
||||
stringList.add(MessageFormat.format(normalizeFormatIndexes(format), params));
|
||||
}
|
||||
}
|
||||
|
||||
return stringList;
|
||||
}
|
||||
|
||||
private List<String> formatMatchResults(AutomatonMatcher matcher, String content) {
|
||||
List<String> stringList = new ArrayList<>();
|
||||
|
||||
while (matcher.find()) {
|
||||
String s = matcher.group(0);
|
||||
if (!s.isEmpty()) {
|
||||
stringList.add(extractMatchedContent(content, s));
|
||||
}
|
||||
}
|
||||
|
||||
return stringList;
|
||||
}
|
||||
|
||||
private Matcher createPatternMatcher(String regex, String content, boolean sensitive) {
|
||||
Pattern pattern = nfaPatternCache.computeIfAbsent(regex, k -> {
|
||||
int flags = sensitive ? 0 : Pattern.CASE_INSENSITIVE;
|
||||
return Pattern.compile(regex, flags);
|
||||
});
|
||||
|
||||
return pattern.matcher(content);
|
||||
}
|
||||
|
||||
private AutomatonMatcher createAutomatonMatcher(String regex, String content) {
|
||||
RunAutomaton runAuto = dfaAutomatonCache.computeIfAbsent(regex, k -> {
|
||||
RegExp regexp = new RegExp(regex);
|
||||
Automaton auto = regexp.toAutomaton();
|
||||
return new RunAutomaton(auto, true);
|
||||
});
|
||||
|
||||
return runAuto.newMatcher(content);
|
||||
}
|
||||
|
||||
private LinkedList<Integer> parseIndexesFromString(String input) {
|
||||
LinkedList<Integer> indexes = new LinkedList<>();
|
||||
Matcher matcher = formatIndexPattern.matcher(input);
|
||||
|
||||
while (matcher.find()) {
|
||||
String index = matcher.group(1);
|
||||
if (!index.isEmpty()) {
|
||||
indexes.add(Integer.valueOf(index));
|
||||
}
|
||||
}
|
||||
|
||||
return indexes;
|
||||
}
|
||||
|
||||
private String extractMatchedContent(String content, String s) {
|
||||
byte[] contentByte = api.utilities().byteUtils().convertFromString(content);
|
||||
byte[] sByte = api.utilities().byteUtils().convertFromString(s);
|
||||
int startIndex = api.utilities().byteUtils().indexOf(contentByte, sByte, false, 1, contentByte.length);
|
||||
int endIndex = startIndex + s.length();
|
||||
|
||||
return content.substring(startIndex, endIndex);
|
||||
}
|
||||
|
||||
private String normalizeFormatIndexes(String format) {
|
||||
Matcher matcher = formatIndexPattern.matcher(format);
|
||||
int count = 0;
|
||||
while (matcher.find()) {
|
||||
String newStr = String.format("{%s}", count);
|
||||
String matchStr = matcher.group(0);
|
||||
format = format.replace(matchStr, newStr);
|
||||
count++;
|
||||
}
|
||||
|
||||
return format;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package hae.instances.websocket;
|
||||
|
||||
import burp.api.montoya.MontoyaApi;
|
||||
import burp.api.montoya.core.HighlightColor;
|
||||
import burp.api.montoya.proxy.websocket.*;
|
||||
import hae.instances.http.utils.MessageProcessor;
|
||||
import hae.utils.ConfigLoader;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class WebSocketMessageHandler implements ProxyMessageHandler {
|
||||
private final MontoyaApi api;
|
||||
private final MessageProcessor messageProcessor;
|
||||
|
||||
public WebSocketMessageHandler(MontoyaApi api, ConfigLoader configLoader) {
|
||||
this.api = api;
|
||||
this.messageProcessor = new MessageProcessor(api, configLoader);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextMessageReceivedAction handleTextMessageReceived(InterceptedTextMessage interceptedTextMessage) {
|
||||
String message = interceptedTextMessage.payload();
|
||||
List<Map<String, String>> result = messageProcessor.processMessage("", message, true);
|
||||
|
||||
if (result != null && !result.isEmpty()) {
|
||||
interceptedTextMessage.annotations().setHighlightColor(HighlightColor.highlightColor(result.get(0).get("color")));
|
||||
interceptedTextMessage.annotations().setNotes(result.get(1).get("comment"));
|
||||
}
|
||||
|
||||
return TextMessageReceivedAction.continueWith(interceptedTextMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextMessageToBeSentAction handleTextMessageToBeSent(InterceptedTextMessage interceptedTextMessage) {
|
||||
return TextMessageToBeSentAction.continueWith(interceptedTextMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BinaryMessageReceivedAction handleBinaryMessageReceived(InterceptedBinaryMessage interceptedBinaryMessage) {
|
||||
return BinaryMessageReceivedAction.continueWith(interceptedBinaryMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BinaryMessageToBeSentAction handleBinaryMessageToBeSent(InterceptedBinaryMessage interceptedBinaryMessage) {
|
||||
return BinaryMessageToBeSentAction.continueWith(interceptedBinaryMessage);
|
||||
}
|
||||
}
|
||||
277
src/main/java/hae/utils/ConfigLoader.java
Normal file
@@ -0,0 +1,277 @@
|
||||
package hae.utils;
|
||||
|
||||
import burp.api.montoya.MontoyaApi;
|
||||
import hae.Config;
|
||||
import org.yaml.snakeyaml.DumperOptions;
|
||||
import org.yaml.snakeyaml.LoaderOptions;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
import org.yaml.snakeyaml.constructor.SafeConstructor;
|
||||
import org.yaml.snakeyaml.representer.Representer;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
|
||||
public class ConfigLoader {
|
||||
private final MontoyaApi api;
|
||||
private final Yaml yaml;
|
||||
private final String configFilePath;
|
||||
private final String rulesFilePath;
|
||||
|
||||
public ConfigLoader(MontoyaApi api) {
|
||||
this.api = api;
|
||||
this.yaml = createSecureYaml();
|
||||
|
||||
String configPath = determineConfigPath();
|
||||
this.configFilePath = String.format("%s/%s", configPath, "Config.yml");
|
||||
this.rulesFilePath = String.format("%s/%s", configPath, "Rules.yml");
|
||||
|
||||
// 构造函数,初始化配置
|
||||
File HaEConfigPathFile = new File(configPath);
|
||||
if (!(HaEConfigPathFile.exists() && HaEConfigPathFile.isDirectory())) {
|
||||
HaEConfigPathFile.mkdirs();
|
||||
}
|
||||
|
||||
File configFilePath = new File(this.configFilePath);
|
||||
if (!(configFilePath.exists() && configFilePath.isFile())) {
|
||||
initConfig();
|
||||
}
|
||||
|
||||
File rulesFilePath = new File(this.rulesFilePath);
|
||||
if (!(rulesFilePath.exists() && rulesFilePath.isFile())) {
|
||||
initRules();
|
||||
}
|
||||
|
||||
Config.globalRules = getRules();
|
||||
}
|
||||
|
||||
private static boolean isValidConfigPath(String configPath) {
|
||||
File configPathFile = new File(configPath);
|
||||
return configPathFile.exists() && configPathFile.isDirectory();
|
||||
}
|
||||
|
||||
private Yaml createSecureYaml() {
|
||||
// 配置 LoaderOptions 进行安全限制
|
||||
LoaderOptions loaderOptions = new LoaderOptions();
|
||||
// 禁用注释处理
|
||||
loaderOptions.setProcessComments(false);
|
||||
// 禁止递归键
|
||||
loaderOptions.setAllowRecursiveKeys(false);
|
||||
|
||||
// 配置 DumperOptions 控制输出格式
|
||||
DumperOptions dop = new DumperOptions();
|
||||
dop.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
|
||||
|
||||
// 创建 Representer
|
||||
Representer representer = new Representer(dop);
|
||||
|
||||
// 使用 SafeConstructor创建安全的 YAML 实例
|
||||
return new Yaml(new SafeConstructor(loaderOptions), representer, dop);
|
||||
}
|
||||
|
||||
private String determineConfigPath() {
|
||||
// 优先级1:用户根目录
|
||||
String userConfigPath = String.format("%s/.config/HaE", System.getProperty("user.home"));
|
||||
if (isValidConfigPath(userConfigPath)) {
|
||||
return userConfigPath;
|
||||
}
|
||||
|
||||
// 优先级2:Jar包所在目录
|
||||
String jarPath = api.extension().filename();
|
||||
String jarDirectory = new File(jarPath).getParent();
|
||||
String jarConfigPath = String.format("%s/.config/HaE", jarDirectory);
|
||||
if (isValidConfigPath(jarConfigPath)) {
|
||||
return jarConfigPath;
|
||||
}
|
||||
|
||||
return userConfigPath;
|
||||
}
|
||||
|
||||
public void initConfig() {
|
||||
Map<String, Object> r = new LinkedHashMap<>();
|
||||
r.put("ExcludeSuffix", getExcludeSuffix());
|
||||
r.put("BlockHost", getBlockHost());
|
||||
r.put("ExcludeStatus", getExcludeStatus());
|
||||
r.put("LimitSize", getLimitSize());
|
||||
r.put("HaEScope", getScope());
|
||||
r.put("DynamicHeader", getDynamicHeader());
|
||||
|
||||
try {
|
||||
Writer ws = new OutputStreamWriter(Files.newOutputStream(Paths.get(configFilePath)), StandardCharsets.UTF_8);
|
||||
yaml.dump(r, ws);
|
||||
ws.close();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
public String getRulesFilePath() {
|
||||
return rulesFilePath;
|
||||
}
|
||||
|
||||
// 获取规则配置
|
||||
public Map<String, Object[][]> getRules() {
|
||||
Map<String, Object[][]> rules = new HashMap<>();
|
||||
|
||||
try {
|
||||
InputStream inputStream = Files.newInputStream(Paths.get(getRulesFilePath()));
|
||||
Map<String, Object> rulesMap = yaml.load(inputStream);
|
||||
|
||||
Object rulesObj = rulesMap.get("rules");
|
||||
if (rulesObj instanceof List) {
|
||||
List<Map<String, Object>> groupData = (List<Map<String, Object>>) rulesObj;
|
||||
for (Map<String, Object> groupFields : groupData) {
|
||||
ArrayList<Object[]> data = new ArrayList<>();
|
||||
|
||||
Object ruleObj = groupFields.get("rule");
|
||||
if (ruleObj instanceof List) {
|
||||
List<Map<String, Object>> ruleData = (List<Map<String, Object>>) ruleObj;
|
||||
for (Map<String, Object> ruleFields : ruleData) {
|
||||
Object[] valuesArray = new Object[Config.ruleFields.length];
|
||||
for (int i = 0; i < Config.ruleFields.length; i++) {
|
||||
valuesArray[i] = ruleFields.get(Config.ruleFields[i].toLowerCase().replace("-", "_"));
|
||||
}
|
||||
data.add(valuesArray);
|
||||
}
|
||||
}
|
||||
|
||||
Object[][] dataArray = data.toArray(new Object[data.size()][]);
|
||||
rules.put(groupFields.get("group").toString(), dataArray);
|
||||
}
|
||||
}
|
||||
|
||||
return rules;
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
return rules;
|
||||
}
|
||||
|
||||
public String getBlockHost() {
|
||||
return getValueFromConfig("BlockHost", Config.host);
|
||||
}
|
||||
|
||||
public void setBlockHost(String blockHost) {
|
||||
setValueToConfig("BlockHost", blockHost);
|
||||
}
|
||||
|
||||
public String getExcludeSuffix() {
|
||||
return getValueFromConfig("ExcludeSuffix", Config.suffix);
|
||||
}
|
||||
|
||||
public void setExcludeSuffix(String excludeSuffix) {
|
||||
setValueToConfig("ExcludeSuffix", excludeSuffix);
|
||||
}
|
||||
|
||||
public String getExcludeStatus() {
|
||||
return getValueFromConfig("ExcludeStatus", Config.status);
|
||||
}
|
||||
|
||||
public void setExcludeStatus(String status) {
|
||||
setValueToConfig("ExcludeStatus", status);
|
||||
}
|
||||
|
||||
public String getDynamicHeader() {
|
||||
return getValueFromConfig("DynamicHeader", Config.header);
|
||||
}
|
||||
|
||||
public void setDynamicHeader(String header) {
|
||||
setValueToConfig("DynamicHeader", header);
|
||||
}
|
||||
|
||||
public String getLimitSize() {
|
||||
return getValueFromConfig("LimitSize", Config.size);
|
||||
}
|
||||
|
||||
public void setLimitSize(String size) {
|
||||
setValueToConfig("LimitSize", size);
|
||||
}
|
||||
|
||||
public String getScope() {
|
||||
return getValueFromConfig("HaEScope", Config.scopeOptions);
|
||||
}
|
||||
|
||||
public void setScope(String scope) {
|
||||
setValueToConfig("HaEScope", scope);
|
||||
}
|
||||
|
||||
public boolean getMode() {
|
||||
return getValueFromConfig("HaEModeStatus", Config.modeStatus).equals("true");
|
||||
}
|
||||
|
||||
public void setMode(String mode) {
|
||||
setValueToConfig("HaEModeStatus", mode);
|
||||
}
|
||||
|
||||
private String getValueFromConfig(String name, String defaultValue) {
|
||||
File yamlSetting = new File(configFilePath);
|
||||
if (!yamlSetting.exists() || !yamlSetting.isFile()) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
try (InputStream inorder = Files.newInputStream(Paths.get(configFilePath))) {
|
||||
Map<String, Object> r = new Yaml().load(inorder);
|
||||
|
||||
if (r.containsKey(name)) {
|
||||
return r.get(name).toString();
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
private void setValueToConfig(String name, String value) {
|
||||
Map<String, Object> currentConfig = loadCurrentConfig();
|
||||
currentConfig.put(name, value);
|
||||
|
||||
try (Writer ws = new OutputStreamWriter(Files.newOutputStream(Paths.get(configFilePath)), StandardCharsets.UTF_8)) {
|
||||
yaml.dump(currentConfig, ws);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> loadCurrentConfig() {
|
||||
Path path = Paths.get(configFilePath);
|
||||
if (!Files.exists(path)) {
|
||||
return new LinkedHashMap<>(); // 返回空的Map,表示没有当前配置
|
||||
}
|
||||
|
||||
try (InputStream in = Files.newInputStream(path)) {
|
||||
return yaml.load(in);
|
||||
} catch (Exception e) {
|
||||
return new LinkedHashMap<>(); // 读取失败时也返回空的Map
|
||||
}
|
||||
}
|
||||
|
||||
public boolean initRules() {
|
||||
boolean ret = copyRulesToFile(this.rulesFilePath);
|
||||
if (!ret) {
|
||||
api.extension().unload();
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
private boolean copyRulesToFile(String targetFilePath) {
|
||||
InputStream inputStream = getClass().getClassLoader().getResourceAsStream("rules/Rules.yml");
|
||||
File targetFile = new File(targetFilePath);
|
||||
|
||||
try (inputStream; OutputStream outputStream = new FileOutputStream(targetFile)) {
|
||||
if (inputStream != null) {
|
||||
byte[] buffer = new byte[1024];
|
||||
int length;
|
||||
|
||||
while ((length = inputStream.read(buffer)) > 0) {
|
||||
outputStream.write(buffer, 0, length);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
137
src/main/java/hae/utils/DataManager.java
Normal file
@@ -0,0 +1,137 @@
|
||||
package hae.utils;
|
||||
|
||||
import burp.api.montoya.MontoyaApi;
|
||||
import burp.api.montoya.http.message.HttpRequestResponse;
|
||||
import burp.api.montoya.http.message.requests.HttpRequest;
|
||||
import burp.api.montoya.http.message.responses.HttpResponse;
|
||||
import burp.api.montoya.persistence.PersistedList;
|
||||
import burp.api.montoya.persistence.PersistedObject;
|
||||
import burp.api.montoya.persistence.Persistence;
|
||||
import hae.component.board.message.MessageTableModel;
|
||||
import hae.instances.http.utils.RegularMatcher;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public class DataManager {
|
||||
private final MontoyaApi api;
|
||||
private final Persistence persistence;
|
||||
|
||||
public DataManager(MontoyaApi api) {
|
||||
this.api = api;
|
||||
this.persistence = api.persistence();
|
||||
}
|
||||
|
||||
public synchronized void putData(String dataType, String dataName, PersistedObject persistedObject) {
|
||||
if (persistence.extensionData().getChildObject(dataName) != null) {
|
||||
persistence.extensionData().deleteChildObject(dataName);
|
||||
}
|
||||
persistence.extensionData().setChildObject(dataName, persistedObject);
|
||||
|
||||
saveIndex(dataType, dataName);
|
||||
}
|
||||
|
||||
public synchronized void loadData(MessageTableModel messageTableModel) {
|
||||
// 1. 获取索引
|
||||
PersistedList<String> dataIndex = persistence.extensionData().getStringList("data"); // 数据索引
|
||||
PersistedList<String> messageIndex = persistence.extensionData().getStringList("message"); // 消息索引
|
||||
|
||||
// 2. 从索引获取数据
|
||||
loadHaEData(dataIndex);
|
||||
loadMessageData(messageIndex, messageTableModel);
|
||||
}
|
||||
|
||||
private void saveIndex(String indexName, String indexValue) {
|
||||
PersistedList<String> indexList = persistence.extensionData().getStringList(indexName);
|
||||
|
||||
if (indexList != null && !indexList.isEmpty()) {
|
||||
persistence.extensionData().deleteStringList(indexName);
|
||||
} else if (indexList == null) {
|
||||
indexList = PersistedList.persistedStringList();
|
||||
}
|
||||
|
||||
if (!indexList.contains(indexValue)) {
|
||||
indexList.add(indexValue);
|
||||
}
|
||||
|
||||
persistence.extensionData().setStringList(indexName, indexList);
|
||||
}
|
||||
|
||||
private void loadHaEData(PersistedList<String> dataIndex) {
|
||||
if (dataIndex != null && !dataIndex.isEmpty()) {
|
||||
dataIndex.forEach(index -> {
|
||||
PersistedObject dataObj = persistence.extensionData().getChildObject(index);
|
||||
try {
|
||||
dataObj.stringListKeys().forEach(dataKey -> RegularMatcher.updateGlobalMatchCache(api, index, dataKey, dataObj.getStringList(dataKey).stream().toList(), false));
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void loadMessageData(PersistedList<String> messageIndex, MessageTableModel messageTableModel) {
|
||||
if (messageIndex == null || messageIndex.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 直接转换为List,简化处理
|
||||
List<String> indexList = messageIndex.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(Object::toString)
|
||||
.toList();
|
||||
|
||||
if (indexList.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final int batchSize = 2000;
|
||||
final int threadCount = Math.max(8, Runtime.getRuntime().availableProcessors() * 2);
|
||||
ExecutorService executorService = Executors.newWorkStealingPool(threadCount);
|
||||
|
||||
try {
|
||||
// 分批处理
|
||||
for (int i = 0; i < indexList.size(); i += batchSize) {
|
||||
int endIndex = Math.min(i + batchSize, indexList.size());
|
||||
List<String> batch = indexList.subList(i, endIndex);
|
||||
|
||||
processBatch(batch, messageTableModel);
|
||||
}
|
||||
} finally {
|
||||
executorService.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
private void processBatch(List<String> batch, MessageTableModel messageTableModel) {
|
||||
batch.forEach(index -> {
|
||||
try {
|
||||
PersistedObject dataObj = persistence.extensionData().getChildObject(index);
|
||||
if (dataObj != null) {
|
||||
HttpRequestResponse messageInfo = dataObj.getHttpRequestResponse("messageInfo");
|
||||
if (messageInfo != null) {
|
||||
addMessageToModel(messageInfo, dataObj, messageTableModel);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
api.logging().logToError("processBatch: " + e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void addMessageToModel(HttpRequestResponse messageInfo, PersistedObject dataObj, MessageTableModel messageTableModel) {
|
||||
HttpRequest request = messageInfo.request();
|
||||
HttpResponse response = messageInfo.response();
|
||||
|
||||
messageTableModel.add(
|
||||
messageInfo,
|
||||
request.url(),
|
||||
request.method(),
|
||||
String.valueOf(response.statusCode()),
|
||||
String.valueOf(response.toByteArray().length()),
|
||||
dataObj.getString("comment"),
|
||||
dataObj.getString("color"),
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
81
src/main/java/hae/utils/UIEnhancer.java
Normal file
@@ -0,0 +1,81 @@
|
||||
package hae.utils;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.FocusEvent;
|
||||
import java.awt.event.FocusListener;
|
||||
|
||||
public class UIEnhancer {
|
||||
public static void setTextFieldPlaceholder(JTextField textField, String placeholderText) {
|
||||
// 存储占位符文本
|
||||
textField.putClientProperty("placeholderText", placeholderText);
|
||||
textField.putClientProperty("isPlaceholder", true);
|
||||
|
||||
updatePlaceholderText(textField);
|
||||
|
||||
textField.addPropertyChangeListener("background", evt -> {
|
||||
updateForeground(textField);
|
||||
});
|
||||
|
||||
textField.addFocusListener(new FocusListener() {
|
||||
@Override
|
||||
public void focusGained(FocusEvent e) {
|
||||
if (Boolean.TRUE.equals(textField.getClientProperty("isPlaceholder"))) {
|
||||
textField.putClientProperty("isPlaceholder", false);
|
||||
updateForeground(textField);
|
||||
|
||||
textField.setText("");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void focusLost(FocusEvent e) {
|
||||
if (textField.getText().isEmpty()) {
|
||||
updatePlaceholderText(textField);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
textField.addPropertyChangeListener("text", evt -> {
|
||||
if (Boolean.TRUE.equals(textField.getClientProperty("isPlaceholder"))) {
|
||||
if (!textField.getText().isEmpty()) {
|
||||
textField.putClientProperty("isPlaceholder", false);
|
||||
updateForeground(textField);
|
||||
}
|
||||
} else {
|
||||
if (textField.getText().isEmpty()) {
|
||||
updatePlaceholderText(textField);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void updatePlaceholderText(JTextField textField) {
|
||||
String placeholderText = (String) textField.getClientProperty("placeholderText");
|
||||
textField.putClientProperty("isPlaceholder", true);
|
||||
textField.setText(placeholderText);
|
||||
textField.setForeground(Color.GRAY);
|
||||
}
|
||||
|
||||
private static void updateForeground(JTextField textField) {
|
||||
Color bg = textField.getBackground();
|
||||
Color fg = isDarkColor(bg) ? Color.WHITE : Color.BLACK;
|
||||
|
||||
if (!Boolean.TRUE.equals(textField.getClientProperty("isPlaceholder"))) {
|
||||
textField.setForeground(fg);
|
||||
textField.putClientProperty("isPlaceholder", false);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isDarkColor(Color color) {
|
||||
double brightness = 0.299 * color.getRed()
|
||||
+ 0.587 * color.getGreen()
|
||||
+ 0.114 * color.getBlue();
|
||||
return brightness < 128;
|
||||
}
|
||||
|
||||
public static boolean hasUserInput(JTextField field) {
|
||||
Object prop = field.getClientProperty("isPlaceholder");
|
||||
return prop instanceof Boolean && !((Boolean) prop);
|
||||
}
|
||||
}
|
||||
71
src/main/java/hae/utils/http/HttpUtils.java
Normal file
@@ -0,0 +1,71 @@
|
||||
package hae.utils.http;
|
||||
|
||||
import burp.api.montoya.MontoyaApi;
|
||||
import burp.api.montoya.http.message.HttpRequestResponse;
|
||||
import burp.api.montoya.http.message.requests.HttpRequest;
|
||||
import burp.api.montoya.http.message.responses.HttpResponse;
|
||||
import hae.utils.ConfigLoader;
|
||||
import hae.utils.string.StringProcessor;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class HttpUtils {
|
||||
private final MontoyaApi api;
|
||||
private final ConfigLoader configLoader;
|
||||
|
||||
public HttpUtils(MontoyaApi api, ConfigLoader configLoader) {
|
||||
this.api = api;
|
||||
this.configLoader = configLoader;
|
||||
}
|
||||
|
||||
public boolean verifyHttpRequestResponse(HttpRequestResponse requestResponse, String toolType) {
|
||||
HttpRequest request = requestResponse.request();
|
||||
HttpResponse response = requestResponse.response();
|
||||
boolean retStatus = false;
|
||||
try {
|
||||
String host = StringProcessor.getHostByUrl(request.url());
|
||||
|
||||
boolean isBlockHost = false;
|
||||
String blockHost = configLoader.getBlockHost();
|
||||
if (!blockHost.isBlank()) {
|
||||
String[] hostList = configLoader.getBlockHost().split("\\|");
|
||||
isBlockHost = isBlockHost(hostList, host);
|
||||
}
|
||||
|
||||
boolean isExcludeSuffix = false;
|
||||
String suffix = configLoader.getExcludeSuffix();
|
||||
if (!suffix.isBlank()) {
|
||||
List<String> suffixList = Arrays.asList(configLoader.getExcludeSuffix().split("\\|"));
|
||||
isExcludeSuffix = suffixList.contains(request.fileExtension().toLowerCase());
|
||||
}
|
||||
|
||||
boolean isToolScope = !configLoader.getScope().contains(toolType);
|
||||
|
||||
boolean isExcludeStatus = false;
|
||||
String status = configLoader.getExcludeStatus();
|
||||
if (!status.isBlank()) {
|
||||
List<String> statusList = Arrays.asList(configLoader.getExcludeStatus().split("\\|"));
|
||||
isExcludeStatus = statusList.contains(String.valueOf(response.statusCode()));
|
||||
}
|
||||
|
||||
retStatus = isExcludeSuffix || isBlockHost || isToolScope || isExcludeStatus;
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
return retStatus;
|
||||
}
|
||||
|
||||
private boolean isBlockHost(String[] hostList, String host) {
|
||||
boolean isBlockHost = false;
|
||||
for (String hostName : hostList) {
|
||||
String cleanedHost = StringProcessor.replaceFirstOccurrence(hostName, "*.", "");
|
||||
if (hostName.contains("*.") && StringProcessor.matchFromEnd(host, cleanedHost)) {
|
||||
isBlockHost = true;
|
||||
} else if (host.equals(hostName) || hostName.equals("*")) {
|
||||
isBlockHost = true;
|
||||
}
|
||||
}
|
||||
return isBlockHost;
|
||||
}
|
||||
}
|
||||
113
src/main/java/hae/utils/rule/RuleProcessor.java
Normal file
@@ -0,0 +1,113 @@
|
||||
package hae.utils.rule;
|
||||
|
||||
import burp.api.montoya.MontoyaApi;
|
||||
import hae.Config;
|
||||
import hae.cache.DataCache;
|
||||
import hae.utils.ConfigLoader;
|
||||
import hae.utils.rule.model.Group;
|
||||
import hae.utils.rule.model.Info;
|
||||
import org.yaml.snakeyaml.DumperOptions;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
import org.yaml.snakeyaml.representer.Representer;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.Writer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class RuleProcessor {
|
||||
private final MontoyaApi api;
|
||||
private final ConfigLoader configLoader;
|
||||
|
||||
public RuleProcessor(MontoyaApi api, ConfigLoader configLoader) {
|
||||
this.api = api;
|
||||
this.configLoader = configLoader;
|
||||
}
|
||||
|
||||
public void rulesFormatAndSave() {
|
||||
DataCache.clear();
|
||||
|
||||
DumperOptions dop = new DumperOptions();
|
||||
dop.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
|
||||
Representer representer = new Representer(dop);
|
||||
Yaml yaml = new Yaml(representer, dop);
|
||||
|
||||
List<Group> ruleGroupList = new ArrayList<>();
|
||||
|
||||
Config.globalRules.forEach((k, v) -> {
|
||||
List<Info> ruleList = Arrays.stream(v)
|
||||
.map(objects -> new Info(
|
||||
(boolean) objects[0],
|
||||
(String) objects[1],
|
||||
(String) objects[2],
|
||||
(String) objects[3],
|
||||
(String) objects[4],
|
||||
(String) objects[5],
|
||||
(String) objects[6],
|
||||
(String) objects[7],
|
||||
(boolean) objects[8]))
|
||||
.collect(Collectors.toList());
|
||||
ruleGroupList.add(new Group(k, ruleList));
|
||||
});
|
||||
|
||||
List<Map<String, Object>> outputGroupsMap = ruleGroupList.stream()
|
||||
.map(Group::getFields)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Map<String, Object> outputMap = new LinkedHashMap<>();
|
||||
outputMap.put("rules", outputGroupsMap);
|
||||
|
||||
File f = new File(configLoader.getRulesFilePath());
|
||||
try (Writer ws = new OutputStreamWriter(Files.newOutputStream(f.toPath()), StandardCharsets.UTF_8)) {
|
||||
yaml.dump(outputMap, ws);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
public void changeRule(Vector data, int select, String type) {
|
||||
Config.globalRules.get(type)[select] = data.toArray();
|
||||
this.rulesFormatAndSave();
|
||||
}
|
||||
|
||||
public void addRule(Vector data, String type) {
|
||||
ArrayList<Object[]> x = new ArrayList<>(Arrays.asList(Config.globalRules.get(type)));
|
||||
x.add(data.toArray());
|
||||
Config.globalRules.put(type, x.toArray(new Object[x.size()][]));
|
||||
this.rulesFormatAndSave();
|
||||
}
|
||||
|
||||
public void removeRule(int select, String type) {
|
||||
ArrayList<Object[]> x = new ArrayList<>(Arrays.asList(Config.globalRules.get(type)));
|
||||
x.remove(select);
|
||||
Config.globalRules.put(type, x.toArray(new Object[x.size()][]));
|
||||
this.rulesFormatAndSave();
|
||||
}
|
||||
|
||||
public void renameRuleGroup(String oldName, String newName) {
|
||||
Config.globalRules.put(newName, Config.globalRules.remove(oldName));
|
||||
this.rulesFormatAndSave();
|
||||
}
|
||||
|
||||
public void deleteRuleGroup(String Rules) {
|
||||
Config.globalRules.remove(Rules);
|
||||
this.rulesFormatAndSave();
|
||||
}
|
||||
|
||||
public String newRule() {
|
||||
int i = 0;
|
||||
String name = "New ";
|
||||
|
||||
while (Config.globalRules.containsKey(name + i)) {
|
||||
i++;
|
||||
}
|
||||
|
||||
Config.globalRules.put(name + i, Config.ruleTemplate);
|
||||
this.rulesFormatAndSave();
|
||||
return name + i;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
29
src/main/java/hae/utils/rule/model/Group.java
Normal file
@@ -0,0 +1,29 @@
|
||||
package hae.utils.rule.model;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class Group {
|
||||
private Map<String, Object> fields;
|
||||
|
||||
public Group(String groupName, List<Info> rules) {
|
||||
List<Map<String, Object>> ruleList = new ArrayList<>();
|
||||
for (Info rule : rules) {
|
||||
ruleList.add(rule.getFields());
|
||||
}
|
||||
|
||||
fields = new LinkedHashMap<>();
|
||||
fields.put("group", groupName);
|
||||
fields.put("rule", ruleList);
|
||||
}
|
||||
|
||||
public Map<String, Object> getFields() {
|
||||
return fields;
|
||||
}
|
||||
|
||||
public void loadFields(Map<String, Object> fields) {
|
||||
this.fields = fields;
|
||||
}
|
||||
}
|
||||
29
src/main/java/hae/utils/rule/model/Info.java
Normal file
@@ -0,0 +1,29 @@
|
||||
package hae.utils.rule.model;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class Info {
|
||||
private Map<String, Object> fields;
|
||||
|
||||
public Info(boolean loaded, String name, String f_regex, String s_regex, String format, String color, String scope, String engine, boolean sensitive) {
|
||||
fields = new LinkedHashMap<>();
|
||||
fields.put("name", name);
|
||||
fields.put("loaded", loaded);
|
||||
fields.put("f_regex", f_regex);
|
||||
fields.put("s_regex", s_regex);
|
||||
fields.put("format", format);
|
||||
fields.put("color", color);
|
||||
fields.put("scope", scope);
|
||||
fields.put("engine", engine);
|
||||
fields.put("sensitive", sensitive);
|
||||
}
|
||||
|
||||
public Map<String, Object> getFields() {
|
||||
return fields;
|
||||
}
|
||||
|
||||
public void loadFields(Map<String, Object> fields) {
|
||||
this.fields = fields;
|
||||
}
|
||||
}
|
||||
28
src/main/java/hae/utils/string/HashCalculator.java
Normal file
@@ -0,0 +1,28 @@
|
||||
package hae.utils.string;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
|
||||
public class HashCalculator {
|
||||
public static String calculateHash(byte[] bytes) {
|
||||
MessageDigest digest;
|
||||
try {
|
||||
digest = MessageDigest.getInstance("MD5");
|
||||
byte[] hashBytes = digest.digest(bytes);
|
||||
return bytesToHex(hashBytes);
|
||||
} catch (Exception ignored) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private static String bytesToHex(byte[] bytes) {
|
||||
StringBuilder hexString = new StringBuilder();
|
||||
for (byte b : bytes) {
|
||||
String hex = Integer.toHexString(0xff & b);
|
||||
if (hex.length() == 1) {
|
||||
hexString.append('0');
|
||||
}
|
||||
hexString.append(hex);
|
||||
}
|
||||
return hexString.toString();
|
||||
}
|
||||
}
|
||||
124
src/main/java/hae/utils/string/StringProcessor.java
Normal file
@@ -0,0 +1,124 @@
|
||||
package hae.utils.string;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
public class StringProcessor {
|
||||
public static String replaceFirstOccurrence(String original, String find, String replace) {
|
||||
int index = original.indexOf(find);
|
||||
if (index != -1) {
|
||||
return original.substring(0, index) + replace + original.substring(index + find.length());
|
||||
}
|
||||
return original;
|
||||
}
|
||||
|
||||
public static boolean matchFromEnd(String input, String pattern) {
|
||||
int inputLength = input.length();
|
||||
int patternLength = pattern.length();
|
||||
|
||||
int inputIndex = inputLength - 1;
|
||||
int patternIndex = patternLength - 1;
|
||||
|
||||
while (inputIndex >= 0 && patternIndex >= 0) {
|
||||
if (input.charAt(inputIndex) != pattern.charAt(patternIndex)) {
|
||||
return false;
|
||||
}
|
||||
inputIndex--;
|
||||
patternIndex--;
|
||||
}
|
||||
|
||||
// 如果patternIndex为-1,表示pattern字符串已经完全匹配
|
||||
return patternIndex == -1;
|
||||
}
|
||||
|
||||
public static String extractHostname(String hostWithPort) {
|
||||
if (hostWithPort == null || hostWithPort.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
int colonIndex = hostWithPort.indexOf(":");
|
||||
if (colonIndex != -1) {
|
||||
return hostWithPort.substring(0, colonIndex);
|
||||
} else {
|
||||
return hostWithPort;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean matchesHostPattern(String host, String selectedHost) {
|
||||
String hostname = StringProcessor.extractHostname(host);
|
||||
String hostPattern = selectedHost.replace("*.", "");
|
||||
boolean matchesDirectly = selectedHost.equals("*") || host.equals(selectedHost);
|
||||
boolean matchesPattern = !host.contains("*") &&
|
||||
(hostPattern.equals(selectedHost) ?
|
||||
StringProcessor.matchFromEnd(host, hostPattern) :
|
||||
StringProcessor.matchFromEnd(hostname, hostPattern));
|
||||
return matchesDirectly || matchesPattern;
|
||||
}
|
||||
|
||||
public static String getRandomUUID() {
|
||||
UUID uuid = UUID.randomUUID();
|
||||
return uuid.toString();
|
||||
}
|
||||
|
||||
public static String mergeComment(String comment) {
|
||||
if (!comment.contains(",")) {
|
||||
return comment;
|
||||
}
|
||||
|
||||
Map<String, Integer> itemCounts = getStringIntegerMap(comment);
|
||||
|
||||
StringBuilder mergedItems = new StringBuilder();
|
||||
|
||||
for (Map.Entry<String, Integer> entry : itemCounts.entrySet()) {
|
||||
String itemName = entry.getKey();
|
||||
int count = entry.getValue();
|
||||
if (count != 0) {
|
||||
mergedItems.append(itemName).append(" (").append(count).append("), ");
|
||||
}
|
||||
}
|
||||
|
||||
return mergedItems.substring(0, mergedItems.length() - 2);
|
||||
}
|
||||
|
||||
public static String getHostByUrl(String url) {
|
||||
String host = "";
|
||||
|
||||
try {
|
||||
URL u = new URL(url);
|
||||
int port = u.getPort();
|
||||
if (port == -1) {
|
||||
host = u.getHost();
|
||||
} else {
|
||||
host = String.format("%s:%s", u.getHost(), port);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
return host;
|
||||
}
|
||||
|
||||
public static boolean matchHostIsIp(String host) {
|
||||
return host.matches("\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b");
|
||||
}
|
||||
|
||||
private static Map<String, Integer> getStringIntegerMap(String comment) {
|
||||
Map<String, Integer> itemCounts = new HashMap<>();
|
||||
String[] items = comment.split(", ");
|
||||
|
||||
for (String item : items) {
|
||||
if (item.contains("(") && item.contains(")")) {
|
||||
int openParenIndex = item.lastIndexOf("(");
|
||||
int closeParenIndex = item.lastIndexOf(")");
|
||||
String itemName = item.substring(0, openParenIndex).trim();
|
||||
int count = Integer.parseInt(item.substring(openParenIndex + 1, closeParenIndex).trim());
|
||||
itemCounts.put(itemName, itemCounts.getOrDefault(itemName, 0) + count);
|
||||
} else {
|
||||
itemCounts.put(item, 0);
|
||||
}
|
||||
}
|
||||
|
||||
return itemCounts;
|
||||
}
|
||||
}
|
||||
|
||||
BIN
src/main/resources/logo/logo.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
src/main/resources/logo/logo_black.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
361
src/main/resources/rules/Rules.yml
Normal file
@@ -0,0 +1,361 @@
|
||||
rules:
|
||||
- group: Fingerprint
|
||||
rule:
|
||||
- name: Shiro
|
||||
loaded: true
|
||||
f_regex: (=deleteMe|rememberMe=)
|
||||
s_regex: ''
|
||||
format: '{0}'
|
||||
color: green
|
||||
scope: any header
|
||||
engine: dfa
|
||||
sensitive: true
|
||||
- name: JSON Web Token
|
||||
loaded: true
|
||||
f_regex: (eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9._-]{10,}|eyJ[A-Za-z0-9_\/+-]{10,}\.[A-Za-z0-9._\/+-]{10,})
|
||||
s_regex: ''
|
||||
format: '{0}'
|
||||
color: green
|
||||
scope: any
|
||||
engine: nfa
|
||||
sensitive: true
|
||||
- name: Swagger UI
|
||||
loaded: true
|
||||
f_regex: ((swagger-ui.html)|(\"swagger\":)|(Swagger UI)|(swaggerUi)|(swaggerVersion))
|
||||
s_regex: ''
|
||||
format: '{0}'
|
||||
color: red
|
||||
scope: response body
|
||||
engine: dfa
|
||||
sensitive: false
|
||||
- name: Ueditor
|
||||
loaded: true
|
||||
f_regex: (ueditor\.(config|all)\.js)
|
||||
s_regex: ''
|
||||
format: '{0}'
|
||||
color: green
|
||||
scope: response body
|
||||
engine: dfa
|
||||
sensitive: false
|
||||
- name: Druid
|
||||
loaded: true
|
||||
f_regex: (Druid Stat Index)
|
||||
s_regex: ''
|
||||
format: '{0}'
|
||||
color: orange
|
||||
scope: response body
|
||||
engine: dfa
|
||||
sensitive: false
|
||||
- name: PDF.js Viewer
|
||||
loaded: true
|
||||
f_regex: (pdf.worker)
|
||||
s_regex: ''
|
||||
format: '{0}'
|
||||
color: green
|
||||
scope: response body
|
||||
engine: dfa
|
||||
sensitive: false
|
||||
- name: Vite DevMode
|
||||
loaded: true
|
||||
f_regex: (/\@vite/client)
|
||||
s_regex: ''
|
||||
format: '{0}'
|
||||
color: red
|
||||
scope: response body
|
||||
engine: dfa
|
||||
sensitive: true
|
||||
- group: Maybe Vulnerability
|
||||
rule:
|
||||
- name: Java Deserialization
|
||||
loaded: true
|
||||
f_regex: (javax\.faces\.ViewState)
|
||||
s_regex: ''
|
||||
format: '{0}'
|
||||
color: yellow
|
||||
scope: response body
|
||||
engine: dfa
|
||||
sensitive: false
|
||||
- name: Debug Logic Parameters
|
||||
loaded: true
|
||||
f_regex: ((access=)|(adm=)|(admin=)|(alter=)|(cfg=)|(clone=)|(config=)|(create=)|(dbg=)|(debug=)|(delete=)|(disable=)|(edit=)|(enable=)|(exec=)|(execute=)|(grant=)|(load=)|(make=)|(modify=)|(rename=)|(reset=)|(root=)|(shell=)|(test=)|(toggl=))
|
||||
s_regex: ''
|
||||
format: '{0}'
|
||||
color: cyan
|
||||
scope: request
|
||||
engine: dfa
|
||||
sensitive: false
|
||||
- name: URL As A Value
|
||||
loaded: true
|
||||
f_regex: (=(https?)(://|%3a%2f%2f))
|
||||
s_regex: ''
|
||||
format: '{0}'
|
||||
color: cyan
|
||||
scope: any
|
||||
engine: nfa
|
||||
sensitive: false
|
||||
- name: Upload Form
|
||||
loaded: true
|
||||
f_regex: (type\=\"file\")
|
||||
s_regex: ''
|
||||
format: '{0}'
|
||||
color: yellow
|
||||
scope: response body
|
||||
engine: dfa
|
||||
sensitive: false
|
||||
- name: DoS Paramters
|
||||
loaded: true
|
||||
f_regex: ((size=)|(page=)|(num=)|(limit=)|(start=)|(end=)|(count=))
|
||||
s_regex: ''
|
||||
format: '{0}'
|
||||
color: cyan
|
||||
scope: request
|
||||
engine: dfa
|
||||
sensitive: false
|
||||
- name: Passwd File
|
||||
loaded: true
|
||||
f_regex: (/root:/bin/bash)
|
||||
s_regex: ''
|
||||
format: '{0}'
|
||||
color: red
|
||||
scope: response body
|
||||
engine: dfa
|
||||
sensitive: true
|
||||
- name: Win.ini File
|
||||
loaded: true
|
||||
f_regex: (for 16-bit app)
|
||||
s_regex: ''
|
||||
format: '{0}'
|
||||
color: red
|
||||
scope: response body
|
||||
engine: dfa
|
||||
sensitive: true
|
||||
- group: Basic Information
|
||||
rule:
|
||||
- name: Email
|
||||
loaded: true
|
||||
f_regex: (\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,5}\b)
|
||||
s_regex: ^((?!.*\.(jpg|jpeg|png|gif|bmp|webp|svg|tiff|ico?)$).*@.*\..*)$
|
||||
format: '{0}'
|
||||
color: yellow
|
||||
scope: response
|
||||
engine: nfa
|
||||
sensitive: false
|
||||
- name: Chinese IDCard
|
||||
loaded: true
|
||||
f_regex: '[^0-9]((\d{8}(0\d|10|11|12)([0-2]\d|30|31)\d{3}$)|(\d{6}(18|19|20)\d{2}(0[1-9]|10|11|12)([0-2]\d|30|31)\d{3}(\d|X|x)))[^0-9]'
|
||||
s_regex: ''
|
||||
format: '{0}'
|
||||
color: orange
|
||||
scope: response body
|
||||
engine: nfa
|
||||
sensitive: true
|
||||
- name: Chinese Mobile Number
|
||||
loaded: true
|
||||
f_regex: '[^\w]((?:(?:\+|0{0,2})86)?1(?:(?:3[\d])|(?:4[5-79])|(?:5[0-35-9])|(?:6[5-7])|(?:7[0-8])|(?:8[\d])|(?:9[189]))\d{8})[^\w]'
|
||||
s_regex: ''
|
||||
format: '{0}'
|
||||
color: orange
|
||||
scope: response body
|
||||
engine: nfa
|
||||
sensitive: false
|
||||
- name: Internal IP Address
|
||||
loaded: true
|
||||
f_regex: '[^0-9]((127\.0\.0\.1)|(10\.\d{1,3}\.\d{1,3}\.\d{1,3})|(172\.((1[6-9])|(2\d)|(3[01]))\.\d{1,3}\.\d{1,3})|(192\.168\.\d{1,3}\.\d{1,3}))'
|
||||
s_regex: ''
|
||||
format: '{0}'
|
||||
color: cyan
|
||||
scope: response
|
||||
engine: nfa
|
||||
sensitive: true
|
||||
- name: MAC Address
|
||||
loaded: true
|
||||
f_regex: (^([a-fA-F0-9]{2}(:[a-fA-F0-9]{2}){5})|[^a-zA-Z0-9]([a-fA-F0-9]{2}(:[a-fA-F0-9]{2}){5}))
|
||||
s_regex: ''
|
||||
format: '{0}'
|
||||
color: green
|
||||
scope: response
|
||||
engine: nfa
|
||||
sensitive: true
|
||||
- group: Sensitive Information
|
||||
rule:
|
||||
- name: Cloud Key
|
||||
loaded: true
|
||||
f_regex: (((access)(|-|_)(key)(|-|_)(id|secret))|(LTAI[a-z0-9]{12,20}))
|
||||
s_regex: ''
|
||||
format: '{0}'
|
||||
color: yellow
|
||||
scope: any
|
||||
engine: nfa
|
||||
sensitive: false
|
||||
- name: Windows File/Dir Path
|
||||
loaded: true
|
||||
f_regex: '[^\w]([a-zA-Z]:\\\\?(?:[^<>:/\\|?*]+\\\\?)*)([^<>:/\\|?*]+(?:\.[^<>:/\\|?*]+)?)'
|
||||
s_regex: ''
|
||||
format: '{0}'
|
||||
color: green
|
||||
scope: response
|
||||
engine: nfa
|
||||
sensitive: true
|
||||
- name: Password Field
|
||||
loaded: true
|
||||
f_regex: (((|\\)(|'|")(|[\.\w]{1,32})([p](ass|wd|asswd|assword))(|[\.\w]{1,32})(|\\)(|'|")(
|
||||
|)(:|[=]{1,3}|![=]{1,2}|[\)]{0,1}\.val\()( |)(|\\)('|")([^'"]+?)(|\\)('|")(|,|\)))|((|\\)('|")([^'"]+?)(|\\)('|")(|\\)(|'|")(
|
||||
|)(:|[=]{1,3}|![=]{1,2})( |)(|[\.\w]{1,32})([p](ass|wd|asswd|assword))(|[\.\w]{1,32})(|\\)(|'|")))
|
||||
s_regex: ''
|
||||
format: '{0}'
|
||||
color: yellow
|
||||
scope: response body
|
||||
engine: nfa
|
||||
sensitive: false
|
||||
- name: Username Field
|
||||
loaded: true
|
||||
f_regex: (((|\\)(|'|")(|[\.\w]{1,32})(([u](ser|name|sername))|(account)|((((create|update)((d|r)|(by|on|at)))|(creator))))(|[\.\w]{1,32})(|\\)(|'|")(
|
||||
|)(:|[=]{1,3}|![=]{1,2}|[\)]{0,1}\.val\()( |)(|\\)('|")([^'"]+?)(|\\)('|")(|,|\)))|((|\\)('|")([^'"]+?)(|\\)('|")(|\\)(|'|")(
|
||||
|)(:|[=]{1,3}|![=]{1,2})( |)(|[\.\w]{1,32})(([u](ser|name|sername))|(account)|((((create|update)((d|r)|(by|on|at)))|(creator))))(|[\.\w]{1,32})(|\\)(|'|")))
|
||||
s_regex: ''
|
||||
format: '{0}'
|
||||
color: green
|
||||
scope: response body
|
||||
engine: nfa
|
||||
sensitive: false
|
||||
- name: WeCom Key
|
||||
loaded: true
|
||||
f_regex: ((corp)(id|secret))
|
||||
s_regex: ''
|
||||
format: '{0}'
|
||||
color: green
|
||||
scope: response body
|
||||
engine: dfa
|
||||
sensitive: false
|
||||
- name: JDBC Connection
|
||||
loaded: true
|
||||
f_regex: (jdbc:[a-z:]+://[a-z0-9\.\-_:;=/@?,&]+)
|
||||
s_regex: ''
|
||||
format: '{0}'
|
||||
color: yellow
|
||||
scope: any
|
||||
engine: nfa
|
||||
sensitive: false
|
||||
- name: Authorization Header
|
||||
loaded: true
|
||||
f_regex: ((basic [a-z0-9=:_\+\/-]{5,100})|(bearer [a-z0-9_.=:_\+\/-]{5,100}))
|
||||
s_regex: ''
|
||||
format: '{0}'
|
||||
color: yellow
|
||||
scope: response body
|
||||
engine: nfa
|
||||
sensitive: false
|
||||
- name: Sensitive Field
|
||||
loaded: true
|
||||
f_regex: (((|\\)(|'|")(|[\.\w]{1,32})(key|secret|token|config|auth|access|admin|ticket)(|[\.\w]{1,32})(|\\)(|'|")(
|
||||
|)(:|[=]{1,3}|![=]{1,2}|[\)]{0,1}\.val\()( |)(|\\)('|")([^'"]+?)(|\\)('|")(|,|\)))|((|\\)('|")([^'"]+?)(|\\)('|")(|\\)(|'|")(
|
||||
|)(:|[=]{1,3}|![=]{1,2})( |)(|[\.\w]{1,32})(key|secret|token|config|auth|access|admin|ticket)(|[\.\w]{1,32})(|\\)(|'|")))
|
||||
s_regex: ''
|
||||
format: '{0}'
|
||||
color: yellow
|
||||
scope: response
|
||||
engine: nfa
|
||||
sensitive: false
|
||||
- name: Mobile Number Field
|
||||
loaded: true
|
||||
f_regex: (((|\\)(|'|")(|[\.\w]{1,32})(mobile|phone|sjh|shoujihao|concat)(|[\.\w]{1,32})(|\\)(|'|")(
|
||||
|)(:|[=]{1,3}|![=]{1,2}|[\)]{0,1}\.val\()( |)(|\\)('|")([^'"]+?)(|\\)('|")(|,|\)))|((|\\)('|")([^'"]+?)(|\\)('|")(|\\)(|'|")(
|
||||
|)(:|[=]{1,3}|![=]{1,2})( |)(|[\.\w]{1,32})(mobile|phone|sjh|shoujihao|concat)(|[\.\w]{1,32})(|\\)(|'|")))
|
||||
s_regex: ''
|
||||
format: '{0}'
|
||||
color: green
|
||||
scope: response body
|
||||
engine: nfa
|
||||
sensitive: false
|
||||
- name: Userinfo In Link
|
||||
loaded: true
|
||||
f_regex: (?:"|'|\`)(((?:[a-zA-Z]{1,10}://|//)[^"'/]{1,}\.[a-zA-Z]{2,}[^"']{0,})|((?:/|\.\./|\./)[^"'><,;|*()(%%$^/\\\[\]][^"'><,;|()]{1,})|([a-zA-Z0-9_\-/]{1,}/[a-zA-Z0-9_\-/]{1,}\.(?:[a-zA-Z]{1,4}|action)(?:[\?|#][^"|']{0,}|))|([a-zA-Z0-9_\-/]{1,}/[a-zA-Z0-9_\-/]{3,}(?:[\?|#][^"|']{0,}|))|([a-zA-Z0-9_\-]{1,}\.(?:\w)(?:[\?|#][^"|']{0,}|)))(?:"|'|\`)
|
||||
s_regex: ((([p](ass|wd|asswd|assword))|(([u](ser|name|sername))|(account)|((((create|update)((d|r)|(by|on|at)))|(creator)))))=[\.\w]{1,32})
|
||||
format: '{0}'
|
||||
color: green
|
||||
scope: response body
|
||||
engine: nfa
|
||||
sensitive: false
|
||||
- group: Other
|
||||
rule:
|
||||
- name: Linkfinder
|
||||
loaded: true
|
||||
f_regex: (?:"|'|\`)(((?:[a-zA-Z]{1,10}://|//)[^"'/]{1,}\.[a-zA-Z]{2,}[^"']{0,})|((?:/|\.\./|\./)[^"'><,;|*()(%%$^/\\\[\]][^"'><,;|()]{1,})|([a-zA-Z0-9_\-/]{1,}/[a-zA-Z0-9_\-/]{1,}\.(?:[a-zA-Z]{1,4}|action)(?:[\?|#][^"|']{0,}|))|([a-zA-Z0-9_\-/]{1,}/[a-zA-Z0-9_\-/]{3,}(?:[\?|#][^"|']{0,}|))|([a-zA-Z0-9_\-]{1,}\.(?:\w)(?:[\?|#][^"|']{0,}|)))(?:"|'|\`)
|
||||
s_regex: ''
|
||||
format: '{0}'
|
||||
color: gray
|
||||
scope: response body
|
||||
engine: nfa
|
||||
sensitive: true
|
||||
- name: Source Map
|
||||
loaded: true
|
||||
f_regex: (\.js\.map)
|
||||
s_regex: ''
|
||||
format: '{0}'
|
||||
color: pink
|
||||
scope: response body
|
||||
engine: dfa
|
||||
sensitive: false
|
||||
- name: Create Script
|
||||
loaded: true
|
||||
f_regex: (\{[^{}]*\}\s*\[[^\s]*\]\s*\+\s*"[^\s]*\.js")
|
||||
s_regex: '"?([\w].*?)"?:"(.*?)"'
|
||||
format: '{0}.{1}'
|
||||
color: green
|
||||
scope: response body
|
||||
engine: nfa
|
||||
sensitive: false
|
||||
- name: URL Schemes
|
||||
loaded: true
|
||||
f_regex: (\b(?![\w]{0,10}?https?://)(([A-Za-z0-9-\.]{1,20})://([-\w+&@#/%?=~_|!:,.;]*[-\w+&@#/%=~_|])?))
|
||||
s_regex: ''
|
||||
format: '{0}'
|
||||
color: yellow
|
||||
scope: response body
|
||||
engine: nfa
|
||||
sensitive: false
|
||||
- name: Router Push
|
||||
loaded: true
|
||||
f_regex: (\$router\.push)
|
||||
s_regex: ''
|
||||
format: '{0}'
|
||||
color: magenta
|
||||
scope: response body
|
||||
engine: dfa
|
||||
sensitive: false
|
||||
- name: All URL
|
||||
loaded: true
|
||||
f_regex: (https?://[-A-Za-z0-9+&@#/%?=~_|!:,.;\u4E00-\u9FFF]+[-A-Za-z0-9+&@#/%=~_|])
|
||||
s_regex: ''
|
||||
format: '{0}'
|
||||
color: gray
|
||||
scope: response body
|
||||
engine: nfa
|
||||
sensitive: true
|
||||
- name: Request URI
|
||||
loaded: false
|
||||
f_regex: ' ((?!.*\.js(\?.*)?$)(.*?[^.js$])) '
|
||||
s_regex: ''
|
||||
format: '{0}'
|
||||
color: gray
|
||||
scope: request line
|
||||
engine: nfa
|
||||
sensitive: false
|
||||
- name: 302 Location
|
||||
loaded: true
|
||||
f_regex: 'Location: (.*?)\r\n'
|
||||
s_regex: ''
|
||||
format: '{0}'
|
||||
color: gray
|
||||
scope: response header
|
||||
engine: nfa
|
||||
sensitive: false
|
||||
- name: OSKeys
|
||||
loaded: false
|
||||
f_regex: <Key>(.*?)</Key>
|
||||
s_regex: ''
|
||||
format: '{0}'
|
||||
color: gray
|
||||
scope: response body
|
||||
engine: nfa
|
||||
sensitive: true
|
||||