Skip to content

Commit

Permalink
Merge pull request #1 from PortSwigger/http_two_downgrade
Browse files Browse the repository at this point in the history
HTTP/2 downgrade feature
  • Loading branch information
d0ge authored Aug 27, 2024
2 parents 5b99667 + 2c3914c commit 7d0fff2
Show file tree
Hide file tree
Showing 19 changed files with 270 additions and 21 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,8 @@ bin/
### VS Code ###
.vscode/

### IntelliJ IDEA
.idea/

### Mac OS ###
.DS_Store
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,9 @@
## [0.0.3] - 2024-07-30

### Fixed
- Fixed the issue with action listener on Swig Utils
- Fixed the issue with action listener on Swig Utils

## [0.0.4] - 2024-08-27

### Added
- HTTP/2 to HTTP/1.1 downgrade
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Burp Suite extension that mutates ciphers to bypass TLS-fingerprint based bot de
- **Firefox Mode**: Install the following list of cipher suites: 4865, 4867, 4866, 49195, 49199, 52393, 52392, 49196, 49200, 49162, 49161, 49171, 49172, 156, 157, 47, 53 and add the Firefox User-Agent header.
- **Chrome Mode**: Use cipher suites 4865, 4866, 4867, 49195, 49199, 49196, 49200, 52393, 52392, 49171, 49172, 156, 157, 47, 53 and add the Chrome User-Agent header.
- **Safari Mode**: Include cipher suites 4865, 4866, 4867, 49196, 49195, 52393, 49200, 49199, 52392, 49162, 49161, 49172, 49171, 157, 156, 53, 47, 49160, 49170, 10 and add the Safari User-Agent header.
- **HTTP2 Downgrade**: By default, Burp uses HTTP/2 to communicate with all servers that advertise support for it during the TLS handshake. When that feature is selected, the Burp Suite will use HTTP/1 even if the server supports HTTP/2. It allows to bypass aggressive HTTP/2 fingerprinting.
- **Brute Force Mode**: Tries different combinations of TLS protocol versions and cipher suites. For a full list, visit: [PortSwigger/bypass-bot-detection](https://github.com/PortSwigger/bypass-bot-detection/blob/d677ad52a3cad97aa51b39b66976e35490cef76d/src/main/java/net/portswigger/burp/extensions/Constants.java#L88).

## Warning
Expand Down
4 changes: 2 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ plugins {
}

group = 'net.portswigger.burp.extensions'
version = '0.0.3'
version = '0.0.4'
description = 'bypass-bot-detection'

repositories {
Expand All @@ -12,7 +12,7 @@ repositories {
}

dependencies {
compileOnly 'net.portswigger.burp.extensions:montoya-api:2023.12.1'
implementation 'net.portswigger.burp.extensions:montoya-api:2024.7'
implementation 'com.google.code.gson:gson:2.11.0'

testImplementation platform('org.junit:junit-bom:5.10.0')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ public void initialize(MontoyaApi montoyaApi) {
try {
new Utilities(montoyaApi);
BlockingQueue<Runnable> tasks = new LinkedBlockingQueue<>();
ThreadPoolExecutor taskEngine = new ThreadPoolExecutor(1, 1, 1, TimeUnit.MINUTES, tasks);
int processors = Runtime.getRuntime().availableProcessors();
ThreadPoolExecutor taskEngine = new ThreadPoolExecutor(processors, processors*2, 1, TimeUnit.MINUTES, tasks);
Utilities.saveTLSSettings();
montoyaApi.userInterface().registerContextMenuItemsProvider(new TLSContextMenuItemsProvider(taskEngine));
montoyaApi.logging().logToOutput(Utilities.getResourceString("greetings"));
Expand Down
55 changes: 50 additions & 5 deletions src/main/java/net/portswigger/burp/extensions/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,63 @@

public class Constants {

public static int THREAD_POOL_SIZE = 1;

public static int MAX_ATTEMPTS = 3;
public static String BURP_TLS_NEGOTIATION = "use_custom";
public static String MATCH_AND_REPLACE_RULE_TYPE = "request_header";
public static String MATCH_AND_REPLACE_REGEXP = "^User-Agent.*$";
public static String FIREFOX_UA = "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:128.0) Gecko/20100101 Firefox/128.0";

public static String[] MATCH_AND_REPLACE_REGEXP_HTTP2 = new String[] {
"^Sec-Ch-Ua:.*$",
"^Sec-Ch-Ua-Mobile:.*$",
"^Sec-Ch-Ua-Platform:.*$",
"^Sec-Fetch-Site:.*$",
"^Sec-Fetch-Mode:.*$",
"^Sec-Fetch-User:.*$",
"^Sec-Fetch-Dest:.*$",
// "^Sec-CH-UA-Arch:.*$",
// "^Sec-CH-UA-Bitness:.*$",
// "^Sec-CH-UA-Model:.*$",
// "^Sec-CH-UA-Platform-Version:.*$",
// "^Sec-CH-UA-Form-Factors:.*$",
// "^Sec-CH-UA-Full-Version-List:.*$",
// "^Sec-CH-UA-WoW64:.*$",
// "^Priority:.*$"
};
public static String FROZEN_UA = "User-Agent: Mozilla/5.0 (%s) %s";

public static Map<String,String> FIREFOX_PLATFORMS = Map.of(
"Windows",
"Windows NT 10.0; Win64; x64; rv:129.0",
"Mac",
"Macintosh; Intel Mac OS X 14.6; rv:129.0",
"Linux",
"X11; Linux x86_64; rv:129.0");
public static Map<String,String> CHROME_PLATFORMS = Map.of(
"Windows",
"Windows NT 10.0; Win64; x64",
"Mac",
"Macintosh; Intel Mac OS X 10_15_7",
"Linux",
"X11; Linux x86_64");
public static Map<String,String> SAFARI_PLATFORMS = Map.of(
"Windows",
"Macintosh; Intel Mac OS X 10_15_7",
"Mac",
"Macintosh; Intel Mac OS X 10_15_7",
"Linux",
"Macintosh; Intel Mac OS X 10_15_7");
// Platforms
public static Map<String,Map<String,String>> BROWSERS_PLATFORMS = Map.of(
"Firefox", FIREFOX_PLATFORMS,
"Chrome", CHROME_PLATFORMS,
"Safari", SAFARI_PLATFORMS
);
// Browsers
public static Map<String,String> BROWSERS_USER_AGENTS = Map.of(
"Firefox", "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:128.0) Gecko/20100101 Firefox/128.0",
"Chrome", "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36",
"Safari", "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15"
"Firefox", "User-Agent: Mozilla/5.0 (%s) Gecko/20100101 Firefox/129.0",
"Chrome", "User-Agent: Mozilla/5.0 (%s) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36",
"Safari", "User-Agent: Mozilla/5.0 (%s) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15"
);
public static Map<String,String[]> BROWSERS_PROTOCOLS = Map.of(
"Firefox", new String[]{"TLSv1.2", "TLSv1.3"},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.stream.Collectors;

public class TLSContextMenuItemsProvider implements ContextMenuItemsProvider {
private ThreadPoolExecutor taskEngine;
Expand Down Expand Up @@ -51,6 +50,10 @@ public List<Component> provideMenuItems(ContextMenuEvent contextMenuEvent) {
menuItemList.add(item);
}
);
String menuLabel = Utilities.enabledHTTPDowngrade() ? "Enable " : "Disable ";
JMenuItem downgradeMenu = new JMenuItem(menuLabel + Utilities.getResourceString("menu_downgrade"));
downgradeMenu.addActionListener(e -> downgradeHttp());
menuItemList.add(downgradeMenu);

JMenuItem item = new JMenuItem(Utilities.getResourceString("menu_brute_force"));
item.addActionListener(new TriggerCipherGuesser(taskEngine, requestResponses));
Expand All @@ -63,7 +66,9 @@ public List<Component> provideMenuItems(ContextMenuEvent contextMenuEvent) {
return null;
}


public void downgradeHttp(){
Utilities.updateHTTPSettings();
}
public void addTLSCiphers(Browsers browser){
Utilities.updateTLSSettingsSync(Constants.BROWSERS_PROTOCOLS.get(browser.name), Constants.BROWSERS_CIPHERS.get(browser.name));
Utilities.updateProxySettingsSync(MatchAndReplace.create(browser));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,20 @@

import burp.api.montoya.core.Annotations;
import burp.api.montoya.http.message.HttpRequestResponse;
import burp.api.montoya.http.message.responses.HttpResponse;
import net.portswigger.burp.extensions.beens.Browsers;
import net.portswigger.burp.extensions.beens.MatchAndReplace;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.stream.Collectors;

import static net.portswigger.burp.extensions.Constants.MAX_ATTEMPTS;

public class TriggerCipherGuesser implements ActionListener, Runnable {
private ThreadPoolExecutor taskEngine;
Expand Down Expand Up @@ -48,12 +53,20 @@ public void run() {
while (it.hasNext()) {
HttpRequestResponse requestResponse = it.next();
String negotiation = Utilities.negotiation(protocol,ciphers);
HttpRequestResponse prob = Utilities.attemptRequest(requestResponse, negotiation);
if ( prob != null && Utilities.compareResponses(requestResponse, prob)) {
List<HttpRequestResponse> probs = new ArrayList<>();
for(int i = 0; i < MAX_ATTEMPTS; i++) {
HttpRequestResponse prob = Utilities.attemptRequest(requestResponse, negotiation);
probs.add(prob);
}
if ( !probs.isEmpty() && Utilities.compareResponses(requestResponse, probs)) {
String comment = String.format(
"|*| URL %s response was changed. Status code %s. TLS settings: %s",
requestResponse.request().url(),
prob.response().statusCode(),
probs.stream()
.map(HttpRequestResponse::response)
.map(HttpResponse::statusCode)
.map(String::valueOf)
.reduce("",(partial,element) -> element + "," + partial),
negotiation );
Utilities.log(comment);
Utilities.addComment(requestResponse,negotiation);
Expand Down
58 changes: 52 additions & 6 deletions src/main/java/net/portswigger/burp/extensions/Utilities.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@

import burp.api.montoya.MontoyaApi;
import burp.api.montoya.core.Annotations;
import burp.api.montoya.core.ByteArray;
import burp.api.montoya.core.HighlightColor;
import burp.api.montoya.http.HttpMode;
import burp.api.montoya.http.HttpService;
import burp.api.montoya.http.message.HttpRequestResponse;
import burp.api.montoya.http.message.requests.HttpRequest;
import burp.api.montoya.http.message.responses.HttpResponse;
import burp.api.montoya.http.message.responses.analysis.Attribute;
import burp.api.montoya.proxy.ProxyHistoryFilter;
Expand All @@ -19,6 +23,7 @@
import java.net.InetAddress;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Optional;
import java.util.ResourceBundle;
Expand Down Expand Up @@ -70,6 +75,25 @@ static void updateTLSSettingsSync(String[] protocols, String[] ciphers) {
String serializedTLSSettings = gson.toJson(currentTLSSettings);
montoyaApi.burpSuite().importProjectOptionsFromJson(serializedTLSSettings);
}
static boolean enabledHTTPDowngrade() {
String project_settings = montoyaApi.burpSuite().exportProjectOptionsAsJson("project_options");
TLSSettings currentTLSSettings = gson.fromJson(project_settings, TLSSettings.class);
return currentTLSSettings.enabledHTTPDowngrade();
}
static void updateHTTPSettings() {
List<MatchAndReplace> rules = MatchAndReplace.createDowngradeRules();
String proxy = montoyaApi.burpSuite().exportProjectOptionsAsJson("proxy.match_replace_rules");
ProxySettings currentProxySettings = gson.fromJson(proxy, ProxySettings.class);
ProxySettings changedProxySettings = currentProxySettings.toggleHTTPDowngradeMatchAndReplace(rules);
String serializedProxySettings = gson.toJson(changedProxySettings);
montoyaApi.burpSuite().importProjectOptionsFromJson(serializedProxySettings);

String project_settings = montoyaApi.burpSuite().exportProjectOptionsAsJson("project_options");
TLSSettings currentTLSSettings = gson.fromJson(project_settings, TLSSettings.class);
currentTLSSettings.toggleHTTPSettings();
String serializedTLSSettings = gson.toJson(currentTLSSettings);
montoyaApi.burpSuite().importProjectOptionsFromJson(serializedTLSSettings);
}

static void importProject(String serializedSettings) {
try {
Expand Down Expand Up @@ -145,21 +169,43 @@ static HttpRequestResponse attemptRequest(HttpRequestResponse requestResponse, S
}
}

static boolean compareResponses(HttpRequestResponse baseRequest, HttpRequestResponse comparableResponse) {
if (baseRequest.response() == null || comparableResponse.response() == null) return false;
double P = 0.1;
static HttpRequestResponse unpredictable(List<HttpRequestResponse> comparableResponses) {
HttpRequestResponse etalon = null;
double P = 0.2;
int base = -1;
for(HttpRequestResponse requestResponse: comparableResponses) {
if (requestResponse.hasResponse() && requestResponse.response() != null) {
Optional<Integer> words = requestResponse.response().attributes(WORD_COUNT).stream().map(Attribute::value).findFirst();
if (words.isPresent()) {
if (base == -1) {
base = words.get();
etalon = requestResponse;
} else {
int diff = Math.abs(base - words.get());
if (diff > Math.abs(base) * P) etalon = requestResponse;
}
}
}
}
return etalon;
}

static boolean compareResponses(HttpRequestResponse baseRequest, List<HttpRequestResponse> comparableResponses) {
HttpRequestResponse requestResponse = unpredictable(comparableResponses);
if (baseRequest.response() == null || requestResponse == null) return false;
double P = 0.2;
int b = 0;
int c = 0;
List<Attribute> baseAttributes = baseRequest.response().attributes(WORD_COUNT);
List<Attribute> comparableAttributes = comparableResponse.response().attributes(WORD_COUNT);
List<Attribute> comparableAttributes = requestResponse.response().attributes(WORD_COUNT);
Optional<Integer> baseWordCount = baseAttributes.stream().map(Attribute::value).findFirst();
Optional<Integer> comparableWordCount = comparableAttributes.stream().map(Attribute::value).findFirst();
if (baseWordCount.isPresent() && comparableWordCount.isPresent()) {
b = baseWordCount.get();
c = comparableWordCount.get();
} else {
b = baseRequest.response().headers().size();
c = comparableResponse.response().headers().size();
c = requestResponse.response().headers().size();
}
int diff = Math.abs(b - c);
return diff > Math.abs(b) * P || diff > Math.abs(c) * P;
Expand Down Expand Up @@ -194,7 +240,7 @@ public boolean matches(ProxyHttpRequestResponse requestResponse) {
});
items.forEach(item -> {
item.annotations().setNotes(comments);
item.annotations().setHighlightColor(HighlightColor.GREEN);
item.annotations().setHighlightColor(HighlightColor.RED);
});
}
static String getComment(HttpRequestResponse baseRequest) {
Expand Down
19 changes: 19 additions & 0 deletions src/main/java/net/portswigger/burp/extensions/beens/HTTP.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package net.portswigger.burp.extensions.beens;

import com.google.gson.annotations.Expose;

public class HTTP {
private @Expose HTTP2 http2;

public HTTP(HTTP2 http2) {
this.http2 = http2;
}

public HTTP2 getHttp2() {
return http2;
}

public void setHttp2(HTTP2 http2) {
this.http2 = http2;
}
}
19 changes: 19 additions & 0 deletions src/main/java/net/portswigger/burp/extensions/beens/HTTP2.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package net.portswigger.burp.extensions.beens;

import com.google.gson.annotations.Expose;

public class HTTP2 {
private @Expose boolean enable_http2;

public HTTP2(boolean enable_http2) {
this.enable_http2 = enable_http2;
}

public boolean getEnableHTTP2() {
return enable_http2;
}

public void setEnableHTTP2(boolean enable_http2) {
this.enable_http2 = enable_http2;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
import com.google.gson.annotations.Expose;
import net.portswigger.burp.extensions.Constants;

import java.util.*;

import static net.portswigger.burp.extensions.Constants.*;

public class MatchAndReplace {
private @Expose String comment;
private @Expose boolean enabled;
Expand All @@ -20,14 +24,33 @@ public MatchAndReplace(String comment, boolean enabled, boolean is_simple_match,
this.string_replace = string_replace;
}

public static List<MatchAndReplace> createDowngradeRules(){
List<MatchAndReplace> rules = new ArrayList<>();
for(String header : MATCH_AND_REPLACE_REGEXP_HTTP2) {
rules.add(new MatchAndReplace(
String.format("HTTP2 Header %s downgrade rule", header),
true,
false,
Constants.MATCH_AND_REPLACE_RULE_TYPE,
header,
""
));
}
return rules;
}

public static MatchAndReplace create(Browsers browser){
String platform = System.getProperty("os.name","Windows");
OS optionalOS = Arrays.stream(OS.values()).filter(os -> platform.contains(os.name)).findFirst().get();
String format = BROWSERS_USER_AGENTS.get(browser.name);
String value = BROWSERS_PLATFORMS.get(browser.name).get(optionalOS.name);
return new MatchAndReplace(
String.format("Emulate %s User-Agent", browser.name),
true,
false,
Constants.MATCH_AND_REPLACE_RULE_TYPE,
Constants.MATCH_AND_REPLACE_REGEXP,
Constants.BROWSERS_USER_AGENTS.get(browser.name)
String.format(format,value)
);
}
public boolean filterByComment(MatchAndReplace filter) {
Expand Down
Loading

0 comments on commit 7d0fff2

Please sign in to comment.