Step by Step. Automating multistep attacks in Burp Suite

When you attack a web app, you sometimes have to perform a certain sequence of actions multiple times (e.g. brute-force a password or the second authentication factor, repeatedly use the same resource, etc.). There are plenty of tools designed for this purpose. Which one to choose if you need, for instance, to make five requests over HTTP a thousand times in a row, while maintaining the same session? My choice is Burp Suite, and in this article, I will explain why.

Scripting languages are perfectly suited for automated multistep attacks, but in many situations, it’s not reasonable to spend an extra hour writing and debugging code when a ready-made solution requiring minimum configuring is available. In addition, to be able to send and process requests at a high speed and implement parallel execution, you need to know the correct stacks that neither slow down the parallel execution nor perform unnecessary actions that complicate the execution.

Burp Suite was developed for lazy hackers unwilling to use programming languages for implementation of such tasks. The tool provides several ways to automate your actions:

  • macros;
  • third-party Stepper extension; and 
  • Turbo Intruder extension developed by the creators of Burp Suite.

Let’s discuss these approaches (and their advantages and limitations) in more detail.

To test the above methods, I will use a very typical problem: brute-forcing a four-digit one-time password. Today, such passwords are used virtually everywhere. By the way, you can gainconsiderable reward on Bug Bounty for exploiting such vulnerabilities.

The test task is available on the PortSwigger Web Security Academy educational portal; it’s perfectly suited for my purposes because you have to perform hundreds of repetitive multistep actions.

Task description

The PortSwigger Web Security Academy formulates the task as follows:

This lab’s two-factor authentication is vulnerable to brute-forcing. You have already obtained a valid username and password, but do not have access to the user’s 2FA verification code. To solve the lab, brute-force the 2FA code and access Carlos’s account page.

Victim’s credentials: carlos:montoya

The point is that you can’t just brute-force the One-Time Password (OTP) within the existing session because after two incorrect attempts, the app stops considering the session valid. To solve the task, you have to perform preauthentication using the provided credentials and then try to predict the OTP code.

Task details

The authentication page looks as follows.

Authentication page
Authentication page

When you enter credentials, the app sends the following request to the server:

POST /login HTTP/1.1
Cookie: session=rcnBF1vzBD00ZSjcoswRzttRrEPIQNj2
Content-Type: application/x-www-form-urlencoded
Content-Length: 70

If the credentials are correct, the next page appears, and you must enter the OTP code on it.

OTP entry page
OTP entry page

When you enter a random OTP, the app sends the following request:

POST /login2 HTTP/1.1
Cookie: session=2gt4P1gFqzyxZJIonAlFv9czYetD5pm0
Content-Type: application/x-www-form-urlencoded
Content-Length: 51

To solve the problem, all you have to do is guess the OTP code. In fact, the chance of guessing is not that low: 1 to 10,000. Taking that the number of attempts is not limited (even though this requires additional actions), the result is 100% guaranteed.

Before you start solving this task, note that:

  1. The app uses the session identifier that you receive when you enter the site. It changes after the first authentication phase (when you enter the correct credentials).
  2. After the authentication, you only have two attempts to enter the OTP code. After two unsuccessful attempts, your session becomes invalid, and you have to restart from the beginning.
  3. The app uses CSRF tokens that change with every request. You have to intercept and substitute them for each of your POST requests.

Overall, you have to automate the following operations: getting a session, entering primary credentials, intercepting CSRF tokens, and attempting to guess the OTP code. Let’s begin!


Solution 1: Macros

In Burp Suite, macros make it possible to automate predefined workflows. You can use macros within session processing rules to accomplish various tasks. Their usage is pretty simple, especially in this particular example.

  1. With Burp Suite running, log in as Carlos (the credentials are provided in the task description) and collect the HTTP authentication packets up to the 2FA verification. You will need them to configure the macro.
  2. Since the session is continuously changing and becomes invalid if you enter the wrong code twice, you need to ensure its persistence somehow. For that purpose, use Burp’s session handling features.
  3. In Burp, go to “Project options” → “Sessions”. In the “Session Handling Rules” panel, click “Add”. The “Session handling rule editor” dialog opens. Here you will add session persistence and resumption rules.
  4. In the dialog window, go to the “Scope” tab. Under “URL Scope”, select the option “Include all URLs” to avoid fine tuning. The session will be maintained for any URLs.
  5. Go back to the “Details” tab to create a macro. Under “Rule Actions”, click “Add” → “Run a macro”. The macro you are about to create will be run with each of your requests.
  6. In the macro settings window, select the requests that must be automated (initial login, getting the session ID, and getting the CSRF token required to send the first form). Under “Select macro”, click “Add” to open the “Macro Recorder”. Here you select the login packet for the /login page (GET request), the packet for sending credentials in a POST request to the/login page, and the login packet for the /login2 page where Burp will intercept the CSRF token required to enter the OTP code.
  7. Under the same tab, click the “Test macro” button. You will see that Burp executes the three requests in a row, intercepts the Cookie data provided to it, and receives the CSRF token required to submit the form. Now you have the automate the OTP input, and the job is done.
  8. Click “OK” to close the dialog boxes. Now Burp will execute this macro with each request to get a new session and then will substitute the session value and the CSRF token value in the outgoing request to update them.
  9. Send the request with the OTP (a POST request to the /login2 page) to Intruder for automation.
  10. Under the “Intruder” tab, leave only the mfa-code field for the payload (mfa-code = §1234§) and go to the Payloads tab. Select the Numbers value for the Payload Type and specify the numbers you want to generate: From: 0, To: 9999, Step: 1, Min integer digits: 4.
  11. Go to the “Options” tab and set “Number of threads” to 1 (this must be done because Burp cannot simultaneously maintain session identifiers for two or more threads, only for one).
  12. Finally, run “Intruder” and wait.

It took Intruder some ten minutes to guess the code (in my case, it was 0643), which is unacceptably long for less than a thousand attempts! Not even a tenth of all possible attempts!.. Why can’t this procedure be expedited? Because Session Handling cannot support a session for two threads at the same time.

The video below demonstrates how this solution works.

Summarizing the usage of macros.


  • you can conveniently maintain a session by continuously intercepting the session ID (even if it changes); and 
  • you intercept not only session values, but all values required to execute a new request (variables, CSRF tokens, etc.).


  • brute-forcing is executed in only one thread;
  • it’s impossible to deliver ‘cross attacks’ that use sessions of two users at the same time because only one session is supported; and 
  • complicated configuring; you may get confused in numerous menus.

Solution 2: Stepper extension

Stepper is a free extension available in Burp Suite Extender. The utility helps to automate a sequence of actions. It can be downloaded from GitHub.

The developers describe Stepper as follows:

Stepper is designed to be a natural evolution of Burp Suite’s Repeater tool, providing the ability to create sequences of steps and define regular expressions to extract values from responses which can then be used in subsequent steps.

Let’s install it and solve the test task in an alternative way.


Important! If you do this after the previous experiment, disable the session handling rules created earlier and delete the macros!

The Stepper module allows to select a number of requests and declare in each of them the variables received by the request from the previous step. Then it substitutes them, as well as the variables retrieved from the response body using regular expressions, and passes them to the next request. As you can see, the logic is simple and straightforward.

  1. Under the “Proxy” tab, select three requests required to get a session, perform the primary authentication, and extract the CSRF token: GET /login, POST /login, and GET /login2. Select these request, right-click on them, and press the button “Add 3 items to Stepper” → “New Sequence” under the “Extensions” tab. The program will ask you to give a name to this sequence. Mine is called evil.

    Important: make sure that the packets are transferred in the correct order! The first packets must be located above the last ones when they are sorted under the Proxy tab (sort by packet numbers: from lowest to highest).

  2. Switch to the Stepper module that appears in the tabs together with other modules.

  3. Here you can see your sequence and three packets, numbered from 1 to 3. You can resend each of the packets by clicking the “Execute Step” button to get an example of the response body and test each of the steps.

  4. Execute the first step by clicking the “Execute Step” button. Create the first variable (to store and transmit the session ID) by clicking the “Add Variable” button in the lower right corner of the module. Name the variable session and add a search condition to it in the “Condition” field: session=([\d\w]+). Now you have the first session variable; it will be forwarded to other requests and used repeatedly.

  5. Next, add the second variable for the CSRF token; it will be forwarded to the next request to send credentials. Press the “Add Variable” button, name the variable csrf, and add a condition for its presence in the response body in the “Condition” field: name="csrf" value="([\w\d]+)".

    This is what I got after performing the above steps.

    Initial Stepper setup
    Initial Stepper setup
  6. Now you can proceed to the next request to send credentials and use the session and csrf variables in it. To do this, switch to the next step (Step 2) and substitute the existing session and CSRF token values in the following format: $VAR:session$ and $VAR:csrf$. You should get something like this:

    POST /login HTTP/1.1
    Cookie: session=<span class="katex"><span class="katex-mathml"><math xmlns=""><semantics><mrow><mi>Vmi><mi>Ami><mi>Rmi><mo>:mo><mi>smi><mi>emi><mi>smi><mi>smi><mi>imi><mi>omi><mi>nmi>mrow><annotation encoding="application/x-tex">VAR:sessionannotation>semantics>math>VARspan><span class="mrel">:span><span class="strut" style="height:0.65952em;vertical-align:0em;">session
    Content-Type: application/x-www-form-urlencoded
    Content-Length: 70
  7. Execute this second step by clicking the “Execute Step” button, and the program will try to redirect you to the /login2 page and give you a new session ID that you again have to retrieve using regular expressions and pass to the next step (Step 3). Therefore, create the same session variable as in point 4 and proceed to Step 3.

  8. At Step 3, don’t forget to change the session value again to the $VAR:session$ variable and execute the request (because you need the CSRF token for the last step). After executing the request, add again the parsing of the CSRF token in the form of the csrf variable (as you did in step 5).

  9. Now you can try to execute the entire sequence and check its workability. Click the “Execute Sequence” button at the very bottom of the module window. As you (hopefully!) can see, the sequence has been executed correctly, and at the last step, you get a response prompting you to enter the OTP code. Now you have to run this sequence 10 thousand times. To do so, transfer the /login2 POST request from the Proxy tab to Intruder.

  10. In the Intruder panel, you have to remove the substitute characters § in the session and CSRF token fields, leaving the substitution only in the mfa-code field: mfa-code=§1337§.

  11. To ensure that the actions of your sequence in the Stepper module are executed for each request from Intruder, add the following text to the request headers: X-Stepper-Execute-Before: [Name of your sequence].

  12. It’s also necessary to insert the names of your variables ($VAR:session$ and $VAR:csrf$) into the Intruder packet. Prior to doing this, change their names into $VAR:[Name of your sequence]:session$ and $VAR:[Name of your sequence]:csrf$. My request packet in Intruder looks as follows:

    POST /login2 HTTP/1.1
    Cookie: session=<span class="katex"><span class="katex-mathml"><math xmlns=""><semantics><mrow><mi>Vmi><mi>Ami><mi>Rmi><mo>:mo><mi>emi><mi>vmi><mi>imi><mi>lmi><mo>:mo><mi>smi><mi>emi><mi>smi><mi>smi><mi>imi><mi>omi><mi>nmi>mrow><annotation encoding="application/x-tex">VAR:evil:sessionannotation>semantics>math>VARspan><span class="mrel">:span><span class="strut" style="height:0.69444em;vertical-align:0em;">evilspan><span class="mrel">:span><span class="strut" style="height:0.65952em;vertical-align:0em;">session
    Content-Type: application/x-www-form-urlencoded
    X-Stepper-Execute-Before: evil
    Content-Length: 51

    In this example, the name of my sequence is evil.

    Now, the sequence of previously created requests will be executed prior to each request from Intruder, and these requests will pass the received session and CSRF token values to the packet.

  13. The last step: setting up the payload under the “Payloads” tab in the same way as you did in the previous section. Select the “Numbers” value in “Payload Type” and set the numbers you want to generate: From: 0, To: 9999, Step: 1, Min integer digits: 4.

  14. Now launch the attack! You can track the sent packets in the Logger tab or you can use the Logger++ module.

This time, my code was 0261. Note that, unlike the previous variant, you are not limited to one thread. Accordingly, the above solution involves as many as five threads. To boost the performance further, you can uncheck the “Set Connection: close” box in the payload options and remove this header from the packets in Stepper and Intruder.

Time to draw some conclusions.


  • since the Stepper module supports sessions by passing the session and token values from one request to another one, you can use multithreaded execution of requests, and your variables won’t conflict in threads;
  • cross attacks are now available: you can run several sequences in parallel; and 
  • an intuitive configuration interface for passing states from one request to another one and an easily added X-Stepper-Execute-Before: header that launches Stepper for any module.


  • too bad, Stepper does not allow as many threads as you may want. Up to three threads can be run simultaneously, but larger numbers only slow down the execution due to the specificity of the module code; and 
  • you have to set up variables for each request manually, which can be very boring.

The best way is to use this extension with the Repeater module (as the developers suggest in the welcome message).

Solution 3: Turbo Intruder extension

Turbo Intruder is one of the most powerful tools in Burp Suite, and every self-respecting pentester should master it. The utility can be downloaded from GitHub.

Turbo Intruder is specially designed to send large numbers of HTTP requests and analyze the results. Its purpose is to complement Burp Intruder by handling attacks of exceptional speed or duration. The main features of this module are:

  • Speed: Turbo Intruder uses an HTTP stack hand-coded from scratch with speed in mind. As a result, on many targets it can seriously outpace even popular asynchronous Go scripts (in fact, it’s possible to select a stack, and most of them are familiar to you);
  • Scalability: Turbo Intruder can achieve flat memory usage, enabling reliable multi-day attacks. It can also be run in headless environments using the command line;
  • Flexibility: Python is used to configure attacks. This enables handling of complex requirements such as signed requests and multi-step attack sequences. Also, the custom HTTP stack makes it possible to handle malformed requests that break other libraries; and 
  • Convenience: Boring results can be automatically filtered out by an advanced diffing algorithm adapted from Backslash Powered Scanner. This means you can launch an attack and get useful results in just two clicks.

To use Turbo Intruder, you must be familiar with Python basics. First of all, you have to install Turbo Intruder from the Extender module.

Now you can start solving the test problem.

  1. Select the very first package in the sequence under the Proxy tab (GET /login package) and right-click on it. Then select Extensions → Send to turbo intruder.
  2. The Turbo Intruder panel opens, and a request appears with examples of scripts that can be selected for usage and modification. In this particular case, you have no choice but to write a script that will solve the problem. Below is my own code with remarks explaining the script logic (please don’t judge it too harshly):
import re
import time
# Regular expressions used to retrieve session identifiers and CSRF tokens
re_csrf = 'name="csrf" value="([\w\d]+)"'
re_session = 'session=([\d\w]+)'
iterable = 0
def queueRequests(target, wordlists):
global engine
# Set one request per one connection to avoid violations of the execution logic; the number of connections depends on the app's capacity.
# All these values have to be calibrated for different servers. The task server doesn't endure high loads well; so, five parallel connections will be enough
engine = RequestEngine(endpoint='',concurrentConnections=5,requestsPerConnection=1)
# Send initial requests that will trigger subsequent requests.
# Set a 1-second delay to that the threads aren't executed synchronically but alternate.
for x in xrange(1,6):
print '1. GET /login Request'
def handleResponse(req, interesting):
global engine
global iterable
if 'Location: /my-account' in req.response:
# If you get this header in the response, when you have won!
print 'You Win!'
return None
if 'Incorrect security code' in req.response:
# If the response says that the entered code is incorrect, it means that one attempt was used, and you launch a new iteration of requests.
print '1. GET /login Request'
return None
if 'Please enter your 4-digit security code' in req.response:
# If the response prompts you to enter OTP, send a request with an attempt to enter OTP.
match_csrf =, req.response)
match_session =, req.getRequest())
req = '''POST /login2 HTTP/1.1\r\nHost:\r\nCookie: session=%s\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 51\r\n\r\ncsrf=%s&mfa-code=%s'''
print '4. POST /login2 Request'
engine.queue(req, [,,str(iterable).zfill(4)])
iterable += 1
print 'Iterable: ' + str(iterable)
return None
if 'Location: /login2' in req.response:
# If the response says that you have been redirected to the /login2 page, it means that you have previously entered the correct credentials; now you receive a new session ID and go to the page where you will retrieve CSRF required for a request with OTP.
match_session =, req.response)
req = '''GET /login2 HTTP/1.1\r\nHost:\r\nCookie: session=%s\r\n\r\n'''
print '3. GET /login2 Request'
return None
if '<form class=login-form method=POST action=/login>' in req.response:
# If the first request was executed successfully, you find yourself on the page prompting to enter the login and password. Enter the requested login and password.
match_session =, req.response)
match_csrf =, req.response)
req = '''POST /login HTTP/1.1\r\nHost:\r\nCookie: session=%s\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 70\r\n\r\ncsrf=%s&username=carlos&password=montoya'''
print '2. POST /login Request'
engine.queue(req, [,])
return None

In Turbo Intruder, there is no convenient way to maintain the session between requests; so, you have to do this manually by creating new requests based on session identifiers retrieved from previous requests.

The script logic is as follows. I run five initial requests that are executed on five parallel connections. Then, the response to each request is processed. The response handler sets a condition that it has received the expected response and then executes the next logical request. For instance, after receiving a response prompting to enter the password, a request to enter the login and password is executed, and so on.

Using this script, I made 400 attempts (~1500 requests) in 30 seconds and solved the task some 20 times faster in comparison with the above examples. Furthermore, I could spend a little more time calibrating the concurrentConnections, requestsPerConnection and pipeline parameters and solve it even faster.

Summarizing for this example.


  • Turbo Intruder makes the most of the app;
  • since the code is written in Python, you can add logic of any complexity to the tool; and 
  • the executed requests can be conveniently filtered in the results table; in addition, you can set your own fields for requests for sorting and filtering purposes.


  • you have to write code, which is not much different from writing scripts from scratch (although you can use premade abstractions for multithreaded and parallel execution of queries);
  • there is no documentation for the tool, except for a number of examples (that are supposed to be sufficient for you); and 
  • the fastest request engines for Intruder don’t log requests and responses to the Logger or Logger++ modules; as a result, there is no convenient way to monitor the network, and you have to use the debugging methods built into Turbo Intruder and its abstractions.


Personally, I prefer Turbo Intruder, although Stepper and built-in macros may be more newbie-friendly. On the other hand, macros and Stepper may be unsuitable for some real-life tasks due to their sluggishness.

I must note that in each example, I left several ways to improve the performance or increase the number of attempts almost twice by slightly increasing the number of requests. So, feel free to modify and enhance the above code. Good luck!

Leave a Reply

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>