Extending Burp Suite for fun and profit – The Montoya way – Part 8

  1. Setting up the environment + Hello World
  2. Inspecting and tampering HTTP requests and responses
  3. Inspecting and tampering WebSocket messages
  4. Creating new tabs for processing HTTP requests and responses
  5. Adding new functionalities to the context menu (accessible by right-clicking)
  6. Adding new checks to Burp Suite Active and Passive Scanner
  7. Using the Collaborator in Burp Suite plugins
  8. -> BChecks – A quick way to extend Burp Suite Active and Passive Scanner
  9. … and much more!

 

Hi there!

In the past two articles, we have seen how to extend Burp’s Active and Passive scanner and how to use the Collaborator in our plugins. Today, we will look at a new way to extend the scanner implemented in Burp Suite since last year, namely BChecks. This new feature allows you to add checks to the Active and Passive Scanner without developing dedicated extensions. Checks are specified in a language similar to YAML and saved in .bcheck files.

BChecks are continuously made more versatile by PortSwigger and new features periodically added. They are very quick to develop and are well-suited for certain types of checks, but as we will see, they have some limitations that make it important to also know how to develop custom checks in extensions, like those we discussed in the previous two articles. Using the previous two articles as an example, actually none of the checks implemented for the active scanner could be defined using BChecks. In the case of Part 6, this is because BChecks currently do not allow you to obtain the response time, and in the case of Part 7, it is not possible to perform operations on byte arrays, which were necessary for constructing the payloads used in our extension.

So, let’s start with some useful references:

BChecks can be created, enabled/disabled, imported/exported, deleted, etc. in the Extensions tab of Burp Suite, in the Bchecks tab:

Burp has integrated also a full development environment that can be used to develop, test, and try the BChecks. By clicking on “New” we can start developing our BCheck from scratch or using one of the supplied examples as template. A new pop-up windows open with the development environment, that can be used to develop the BCheck, check if its syntax is correct, and test it on sample requests/responses. It also offers dedicated Logger, Issue activity, and Event log tabs:

As usual, let’s start with a test case. We will develop some simple SQL Injection checks, so we need a target vulnerable to SQL Injection. Let’s code a simple Flask application for this purpose:

import flask
from flask import request
import sqlite3

app = flask.Flask(__name__)

@app.route('/', methods=['GET'])
def handle_request():

    name = request.args.get('name')

    if name:

        dbfile = 'test.db'
        con = sqlite3.connect(dbfile)

        cur = con.cursor()

        cur.execute("SELECT * FROM items WHERE name='" + name + "';")
        items = cur.fetchall();
        
        con.close()

        return items;

    else:

        return "Missing parameters";
    

app.run(host="127.0.0.1", port=5000, debug=True)

And let’s create a simple sqlite3 database in the same folder:

$ sqlite3 test.db
SQLite version 3.46.0 2024-05-23 13:25:27
Enter ".help" for usage hints.
sqlite> create table items(id int, name text, value int);
sqlite> insert into items values(1,'spoon',5);
sqlite> insert into items values(2,'fork',5);
sqlite> insert into items values(3,'table',500);
sqlite> insert into items values(4,'tv',200);
sqlite> select * from items;
1|spoon|5
2|fork|5
3|table|500
4|tv|200
sqlite> .quit

Now we can start the Flask server and try our endpoint:

As we can clearly see in the code, the endpoint is vulnerable to SQL injection and we can easily validate the assumption by adding a single quote in the name parameter:

Before beginning to create our first BCheck, let’s have a look to the documentation (BCheck definition reference). As of today, the documentation page has the following sections:

  • Metadata: contains information about the BCheck itself. The metadata section is mandatory and must be placed at the very start of the file.
  • Control flow: describes three elements, one of which is the main element of the BCheck. Each BCheck must have one single “given … then” block that contains the main logic of the check. Depending on what we want to check we may have response/request/host/path/insertion point instead of the dots (e.g., given response, given any insertion point, given host, etc.). Before the “given … then” block we may optionally have a “define” block with some variables and/or a “run for each” block that we can use to repeat the check once for each value in a defined array.
  • Conditionals: in the BCheck we can use if/then/else blocks. This section describes its syntax and the conditions we can use in the if block.
  • Actions: this section describes the action we can run on the Burp Scanner. It contains methods to execute requests and report issues.
  • Reserved variables: this section enumerate the properties of some objects available in the BChecks (at the moment, request, response, user_agent, insertion_point_base_value). Obviously, the name of these properties should not be used as variable names.
  • Functions: this section describes many utility functions available in the BCheck, including regex, strings, encodings, and hashing functions. One important function is generate_collaborator_address, that allows to generate a Collaborator address in the BCheck.
  • Misc: some miscellaneous information on strings, regex, escaping, special chars, etc.
  • BCheck structure: this section describes some  rules we need to follow in order to create valid BChecks.

The last point highlights the basic structure of a BCheck, describing mandatory fields, valid positions, and variable scopes. Let’s have look at it and then we can start creating our own BCheck:

First, we will develop a simple passive check, aimed at detecting SQL error in responses (because those errors are an evidence of a potential SQL Injection).

First, we will define the metadata:

metadata:
    language: v2-beta
    name: "SQL error"
    description: "Passive detection for SQL errors"
    author: "Federico Dotta"
    tags: "SQL injection", "passive", "SQL"

All metadata fields are self explanatory, except for language that according to the documentation at the moment must have the value v2-beta.

Then, our rule must have exactly one block given … then block, that should have the following syntax:

We want to check for error messages in HTTP responses, so we are interested in the response. Furthermore our check is passive, so we are not interested in the insertion points, because we don’t have any. So:

given response then

Now, let’s think about what we want to achieve. We want to search for specific keywords in the response body, that usually are present in error messages from the database management systems (e.g., mysql, mariadb, sqlite, mssql, db2, pgsql, sql, etc.). If one of these strings is present, we want to create an issue.

More practically, we need an if condition (Conditionals section of the documentation), a way to access to the response body (sections “Reserved variables“), maybe some utility functions (section”Functions“), and a way to create the issue (section “Actions“). By taking all the building blocks from the documentation, the result is the following:

if {to_lower(latest.response.body)} matches "mysql|mariadb|sqlite|mssql|db2|pgsql|sql" then

    report issue:

        name: "SQL exception - Potential SQL Injection"
        severity: info
        confidence: tentative
        detail: "The response body potentially includes a SQL exception. Check for SQL injections."
        remediation: "Remove verbose errors and apply parameterized query to all SQL queries."               

end if

We take the response body of the latest response processed by the rule (we can choose latest for the latest response or base for the base response, but in this case it should be the same because it is a passive check), we use a function to convert it to lower case (all functions should be put within curly brackets {}) and we check if the result matches with one of our defined keywords, using a regex (described in the Conditionals section of the documentation). If it matches, we create an issue using the report issue action.

To summarize, this is our final simple passive BCheck:

metadata:
    language: v2-beta
    name: "SQL error"
    description: "Passive detection for SQL errors"
    author: "Federico Dotta"
    tags: "SQL injection", "passive", "SQL"

given response then

    if {to_lower(latest.response.body)} matches "mysql|mariadb|sqlite|mssql|db2|pgsql|sql" then

        report issue:
    
            name: "SQL exception - Potential SQL Injection"
            severity: info
            confidence: tentative
            detail: "The response body potentially includes a SQL exception. Check for SQL injections."
            remediation: "Remove verbose errors and apply parameterized query to all SQL queries."               

    end if

To try our rule, we can send a vulnerable request directly to the BCheck editor, as follows:

The request will popup directly in our editor, ready to test the rule:

Now, we can click on “Validate” to check the rule syntax and then on “Run test” to try the rule on the vulnerable request we sent to the BCheck editor. If all worked correctly, an issue should be reported in the “Issue activity” tab:

Our first BCheck works correctly! We can now try to create an active check. Instead of simply analyzing the response, we can try to actively add a single quote to all parameters and see if we found one of our SQL keywords in the response. Let’s have a look at the BCheck (mostly similar to the previous one):

metadata:
    language: v2-beta
    name: "SQL Injection"
    description: "Active detection of SQL Injection"
    author: "Federico Dotta"
    tags: "SQL injection", "SQL"

given any insertion point then
        
    send payload:            
        appending: "'"

        if {to_lower(latest.response.body)} matches "mysql|mariadb|sqlite|mssql|db2|pgsql|sql" then

            report issue:

                name: "SQL Injection"
                severity: high
                confidence: tentative
                detail: "The parameter seems to be vulnerable to SQL Injection"
                remediation: "Apply parameterized query to all SQL queries."               

        end if

The only portion of the BCheck different from the passive one we wrote earlier is:

given any insertion point then 

    send payload: 
        appending: "'"

This rule is called each time the active scanner is executed. Our BCheck will be executed on any insertion point, but if we want we can also run it only on a specific type of insertion point (query, header, body, or cookie). send payload is one of the actions that we can use to send a request (the other one is send request that allows to send a request without interacting with the current insertion point). With send payload we can put our payload in the current insertion point, either replacing the original value or appending to the original value (obviously this can be used only after a given insertion point instruction). We selected appending because we want to append a single quote to the original value of the insertion point.

After that, as in the passive BCheck, if we find one of our keywords in the response, we report the issue.

In this BCheck, it is important to use latest.response.body instead of base.response.body, because we executed a new request in our check and the response should be different from the base response.

We can try our rule as before, by sending the vulnerable request to the BCheck (without the single quote this time, because it will be added by our BCheck):

Great! Now, let’s think on how we can improve our BCheck… We can decrease the false positive rate for example by send also a request with two single quotes. If the request with one single quote has one of our SQL keyword in the response and the request with two single quotes doesn’t, probably we have a SQL injection. If both requests have one of our keywords in the response, it can be a false positive. Personally, as a penetration tester, I prefer a higher number of false positives over risking to miss any vulnerability, but there are other contexts where it is preferable to minimize false positives as much as possible. This is the code:

metadata:
    language: v2-beta
    name: "SQL Injection (less FP)"
    description: "Active detection of SQL Injection with less false positives"
    author: "Federico Dotta"
    tags: "SQL injection", "SQL"


given any insertion point then
        
    send payload:            
        appending: "'"

        if {to_lower(latest.response.body)} matches "mysql|mariadb|sqlite|mssql|db2|pssql|sql" then

            send payload:            
            appending: "''"

            if not({to_lower(latest.response.body)} matches "mysql|mariadb|sqlite|mssql|db2|pssql|sql") then

                report issue:
    
                    name: "SQL Injection (less FP)"
                    severity: high
                    confidence: firm
                    detail: "The parameter seems to be vulnerable to SQL Injection"
                    remediation: "Apply parameterized query to all SQL queries."               

            end if

        end if

As we can see, we don’t have anything new in this rule. We send our first request by appending a single quote to the current insertion point and we search for one of our SQL keywords as before. If we find one of the keywords, we execute a second request appending two single quotes. If this last request does not have one of our SQL keywords in the response body, we report the issue. To do so, we use the utility function not in the second if, which simply inverts the boolean outcome.

With a similar rule structure, we can write a simple rule that checks for Blind SQL Injection:

metadata:
    language: v2-beta
    name: "Blind SQL Injection"
    description: "Active detection of blind SQL Injection"
    author: "Federico Dotta"
    tags: "SQL injection", "blind", "SQL"

define:
    blind_payload_true = "' AND '534'='534"
    blind_payload_false = "' AND '534'='535"

given any insertion point then
        
    send payload called positive:            
        appending: {blind_payload_true}

    if {base.response.body} is {positive.response.body} then

        send payload called negative:            
            appending: {blind_payload_false}

        if not({positive.response.body} is {negative.response.body}) then

            report issue:

                name: "SQL Injection"
                severity: high
                confidence: firm
                detail: "The parameter seems to be vulnerable to SQL Injection (blind boolean based)"
                remediation: "Apply parameterized query to all SQL queries."               

        end if

    end if

Here, we have a couple of new elements.

First, we added a define block that contains two variables, one including a SQL injection payload containing a boolean condition that returns always true and one that returns always false. We can use these variables in our rule by surrounding the variable name with curly brackets {}.

Second, we gave a name to the requests we sent, using the construct send payload called <name>. This way we have an easy way to take the response of a particular request, more versatile than using only base and latest.

To summarize, our rule appends the blind SQL injection payload with the true boolean condition to the insertion point. If the response body is equal to the response body of the base request (that is the request sent to the scanner without modifications), our rule will send another request with the blind false SQL injection payload appended to the insertion point. If this new response body is different from the one with the true SQL injection payload (and consequently also from the base request one), probably we have a blind SQL injection (and we report the issue). This check works only if we have a SQL injection on a string, but if we want we can make a version of the check that works also on SQL injection on numbers, by using payloads without the single quotes (“ AND 534=534” and “ AND 534=535“).

Finally, we will code one BCheck that makes use of the Collaborator, in order to try also this feature offered by the BCheck engine.

Let’s modify a little the code of our Flask backend, in order to introduce a SSRF issue:

import flask
from flask import request
import sqlite3
import socket
import requests

app = flask.Flask(__name__)

@app.route('/', methods=['GET'])
def handle_request():

    name = request.args.get('name')
    url = request.args.get('url')

    if name:

        dbfile = 'test.db'
        con = sqlite3.connect(dbfile)

        cur = con.cursor()

        cur.execute("SELECT * FROM items WHERE name='" + name + "';")
        items = cur.fetchall();
        
        con.close()

        return items;

    else:

        if url:

            r = requests.get(url = url)
            return r.text

        else:

            return "Missing parameters";
    

app.run(host="127.0.0.1", port=5000, debug=True)

Now, if we send a request without the name parameter and with a url parameter, the application will send a request to the supplied URL. We can manually verify this using a payload generated from the Collaborator tab of Burp Suite.

Well, it works. Now, let’s write a BCheck to detect HTTP interactions:

metadata:
    language: v2-beta
    name: "SSRF"
    description: "Active detection of SSRF with Collaborator"
    author: "Federico Dotta"
    tags: "SSRF", "Collaborator", "External interaction"


given any insertion point then
        
    send payload:            
        replacing: `http://{generate_collaborator_address()}`

        if http interactions then

            report issue:

                name: "SSRF (HTTP interaction)"
                severity: high
                confidence: firm
                detail: "The parameter is vulnerable to SSRF. An HTTP interaction has been received."
                remediation: "Avoid contacting arbitrary URL supplied by user"               

        end if

The rule structure is similar to all the others we wrote, with a couple of new elements.

First, we can generate collaborator payloads with the generate_collaborator_address function and we can check if we get HTTP interactions using the condition if http interactions (described in the Conditionals section of the documentation). Tip: in order to concatenate the result of a function (in this example generate_collaborator_address) to a string you have to use the backtick ` (e.g., `http://{generate_collaborator_address()}`).

Second, we used replacing instead of appending after the send payload instruction, in order to replace the original value of the insertion point with our URL.

We can send our vulnerable request to the BCheck and test the rule as before:

We can improve this rule, by requesting also a DNS interaction in addition to the HTTP interaction, in order to detect SSRF when for example there is some mechanisms like egress filtering that blocks outgoing HTTP requests (but in this situation the endpoint is still vulnerable to SSRF, exploitable for example to reach services exposed in the private network or on localhost):

metadata:
    language: v2-beta
    name: "SSRF (improved)"
    description: "Active detection of SSRF with Collaborator"
    author: "Federico Dotta"
    tags: "SSRF", "Collaborator", "External interaction"


given any insertion point then
        
    send payload:            
        replacing: `http://{generate_collaborator_address()}`

        if http interactions then

            report issue:

                name: "SSRF"
                severity: high
                confidence: firm
                detail: "The parameter is vulnerable to SSRF. An HTTP interaction has been received."
                remediation: "Avoid contacting arbitrary URL supplied by user"               

        else if dns interactions then

            report issue:

                name: "SSRF"
                severity: medium
                confidence: firm
                detail: "The parameter may be vulnerable to SSRF. Only DNS interaction has been received, maybe for egress filtering."
                remediation: "Avoid contacting arbitrary URL supplied by user"

        end if

Well, I think we saw enough examples for this article.

One last useful tip when developing these rules: when we test a rule like ours that extends the Active Scanner using the integrated IDE offered by Burp Suite, the rule is executed on all insertion points that match the rules (any, query, header, body or cookie, depending on what we chose in the rule). To speed up testing we can use Burp Suite scanner instead of the the “Run test” feature of the BChecks IDE, by creating an ad-hoc scan configuration that runs only BChecks and then use the “Scan defined insertion point” feature of the Intruder in order to scan only a single parameter of the request (refer to Part 6 for more details on this approach):

The same scan configuration can be used during a penetration test to run a scan with only BChecks, if for example we want to run our rules on all endpoints of our target without having to wait for a full Burp Suite scan to finish.

And for BChecks that’s all. Take a look at the BCheck repository on GitHub. It contains several checks useful for identifying various product issues that are not present in Burp’s active checks, which usually do not include payloads for known CVEs.

As always, the complete code of the backend and of the plugins can be downloaded from my GitHub repository.

Cheers!