
First, let’s discuss the general multistep SQLi detection methodology, then examine built-in sqlmap functions that can be used to identify multistep SQLi, and finally write a wrapper to search for such injections.
warning
This article is intended for security specialists operating under a contract; all information provided in it is for educational purposes only. Neither the author nor the Editorial Board can be held liable for any damages caused by improper usage of this publication. Distribution of malware, disruption of systems, and violation of secrecy of correspondence are prosecuted by law.
How to search for multistep SQLi
Testing of multistep injections involves four main stages:
- Preparation. At the preparation stage, data or objects are created: the injection will be performed either into them or using them. Example: creating a draft news article on a news portal (“Create draft” button) or creating a new letter (“Write letter” button);
- Injection. At this stage, payload is directly added to the transmitted data. Example: saving a news article after editing;
- Confirmation. At this stage, the creation of the object is confirmed. Example: confirming the publication of a news article; and
- Result. It’s assumed that there is a separate page where you can see the result of the injection. Example: web page containing the published news article.

At each stage, several requests can be sent, but their nature doesn’t change:
- Requests prior to injection;
- Request containing injection;
- Requests after injection; and
- Checking the result.
Business logic of a specific app might include not all of these stages. For instance, the preparation stage can be absent: you create a news article without sending a separate request since a news article is created as soon as you start editing it.
Built-in sqlmap functions that can be used to exploit multistep SQLi
Built-in sqlmap functions make it possible to interact not only with the request containing payload, but also with requests sent before it and after it.
The main such functions are:
- tamper (handle payload);
- eval (change the main request); and
- second-req/second-url (execute a request after the main one).
Sqlmap tamper
Purpose: modify payload prior to sending it by executing Python code.
Parameter:
--tamper /path/to/tamper
Features:
- Ready-made tamper scripts are available; normally, they are located in
/
;usr/ share/ sqlmap/ tamper/ - To view existing tamper scripts, use the command
sqlmap
; and--list-tampers - You can create your own tamper scripts in Python.
Can be used to:
- bypass WAF (see examples on GitHub); and
- execute queries prior to sending the main sqlmap payload.
How to create a custom tamper script
Create a file with the .py extension (e.g. test.
). Insert the following template into it:
#!/usr/bin/env python3from lib.core.enums import PRIORITY__priority__ = PRIORITY.NORMALdef dependencies(): passdef tamper(payload, **kwargs): """ This accepts the payload that SQLMap will send to the target, then returns the formatted payload """ if payload: # Do stuff to the payload here. Probably best to # set a new variable and return that once you # manipulate it however. pass return payload
Let’s go through this structure:
-
__priority__
— variable that determines the order of multiple sqlmap tamper scripts. In total, it can take seven predefined values :-
PRIORITY.
;LOWEST -
PRIORITY.
;LOWER -
PRIORITY.
;LOW -
PRIORITY.
;NORMAL -
PRIORITY.
;HIGH -
PRIORITY.
;HIGHER -
PRIORITY.
.HIGHEST
-
-
dependencies(
— potentially, this function is intended for additional dependencies, but currently it’s used in tamper scripts only to output information in sqlmap:) def dependencies():singleTimeWarnMessage("This is a warning from your tamper script!") -
tamper(
— function that handles payload passed in thepayload, **kwargs) payload
parameter. Other request parameters can also be accessed using**kwargs
. You can even change them:def tamper(payload, **kwargs):headers = kwargs.get("headers", {})headers["X-FORWARDED-FOR"] = "127.0.0.1"return payload
The last step is to create an empty __init__.
file in the directory.
Now you can specify this file in the parameter as shown above:
--tamper /path/to/test.py
For more information, see the following materials:
Sqlmap eval
Purpose: modify a request prior to sending it by executing Python code.
Parameter:
--eval="#first line; #second line; #third line"
Features:
- Doesn’t have access to payload that will be injected. If the variable is replaced, payload would be lost;
- Modifies the request every time the main payload is sent (
--second-req
isn’t modified); and - Any part of the request can be modified, including URI.
Can be used to:
- bypass WAF (see an example on GitHub); and
- execute queries prior to sending the main sqlmap payload.
Example with eval
To use eval
, Python code is passed to the --eval
parameter as a single-line command:
sqlmap … --eval="import time; title=time.time()"
Sqlmap parses the passed data, which makes it possible to access them directly. In the image below, you can see a change in a variable inside the query body: the title
value has been replaced with the current time.

Sqlmap second-req/second-url
Purpose: send a request after the main request and payload.
Parameter:
--second-req /path/to/request
--second-url http://127.0.0.1/API
Features:
- Second-req (but not second-url) may conflict with
eval
, which causes sqlmap to crash at the initialization stage (WAF check); and - Cannot be changed since
tamper
is called when payload (which is absent there) is processed; whileeval
is called in response to the main request or conflicts.
A request from second-req/second-url is made every time payload is executed. If payload isn’t executed, no second-req/second-url requests will be sent. The screenshot below shows that sqlmap doesn’t make a second-req request when test queries are sent (to check availability of the website).

Brief conclusions
Several mechanisms enable you to make additional requests: tamper, eval, and second request. Their brief comparison is provided in the table below.
Function | Call order | Modification object | Represents |
---|---|---|---|
Tamper | 1 | Payload SQLmap (+ partially request – headers) |
Python code |
Eval | 2 | Main SQLmap request | Python code |
Second request | 3 | – | One simple request |
Problems arising when multistep SQLi are exploited using sqlmap
The existing customizable mechanisms (i.e. tamper and eval) can only help when you make queries before the main payload and affect the query containing the main payload. In other words, they can be used at stages 1 (preparation) and 2 (injection).
Accordingly, sqlmap cannot be used to modify queries after the main query. The query from the second request cannot be changed; furthermore, it’s impossible to send more than one query, which might be required at stage 3 (confirmation) and stage 4 (result).
Overall, sqlmap can be used to exploit multistep injections, but such injections should be performed in only two or three (as maximum) stages.

In the simplest scenario (only the preparation stage is required), sqlmap can be effectively used to search for compound SQLi and exploit them.

Writing custom wrapper
In my opinion, the following testing scheme involving custom Python scripts is optimal for multistep SQLi:
- There is a chain of requests (stage 1, stage 2, …); after each request, the status, time, and length of the response are checked, as well as the presence of out-of-bounds (OOB) requests to an external server;
- These requests are performed in the function that takes payload. Then this payload is substituted into the requests (stage 1, stage 2, …); and
- The
main
function runs a function with a chain of five workers in parallel and passes to it the payload from the specified wordlist and the session cookie.
Code example
First, let’s import the required libraries, including concurrent.
(to execute parallel requests).
import concurrent.futuresimport requestsimport jsonimport argparsefrom urllib3.exceptions import InsecureRequestWarningfrom urllib3 import disable_warningsdisable_warnings(InsecureRequestWarning) #for ingnore tls errors
The args_parser(
function will parse the passed arguments:
def args_parser(): parser = argparse.ArgumentParser(add_help=True, description=''' Makes transactions ''', formatter_class=RawTextHelpFormatter) parser.add_argument("-w",'--wordlist', dest='wordlist',type=str,help="Path to wordlist with payloads") parser.add_argument("-s",'--session-cookie', dest='session',type=str,help="Current session cookie") return parser.parse_args()
Below is an example of the main
function implementing multithreading for five workers. The script takes command line arguments and loads the path to the wordlist and session cookies to perform authenticated requests. After receiving all the data using the concurrent.
abstraction, it creates a pool of workers that will execute the specified function. Threads are started using executor.
, and the function that has to be executed and its parameters are passed to it.
def main(): args=args_parser() wordlist = args.wordlist session=args.session f = open(wordlist, "r") with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: for line in f: executor.submit(send_req, line, session) f.close()
Below is an example of a function passed to executor
. It sends requests containing payload from the wordlist. In this example, the function consists of three parts: Create vars, STAGE 1, and STAGE 2. In Create vars, common values are set for all subsequent requests (e.g. the cookie
value and the User-Agent
header). The number of stages isn’t limited; they represent requests intended for different stages of the business logic. A new request corresponds to a new STAGE. In this particular example, two STAGES are presented for demonstration purposes.
def send_req(payload: str, session_cookie: str): ### Create vars cookies = { 'Session': session_cookie, } headers = { 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0', # Some headers } ### STAGE 1 json_data = { # Some data } response = requests.post( 'https://victim.ru/transactions', cookies=cookies, headers=headers, json=json_data, ) dictData = json.loads(response.content) # Work with data ... try: # Check your business logic transactionId=int(dictData['transactionId']) except: # handle_error here handle_error() check_interests(response,1,payload,210) ### STAGE 2 json_data = { # Some data 2 } response = requests.put( 'https://victim.ru/transactions/'+str(transactionId), cookies=cookies, headers=headers, json=json_data, ) if (response.status_code)!=200: handle_error() else: pass check_interests(response,2,payload,2010)
It’s also important to track anomalies (i.e. ‘interesting’ results) that occur when payloads from the wordlist are transmitted. Below is an example of a function that handles such ‘interesting’ results: it measures the server response time (and triggers if it exceeds 10 seconds) and checks the response length relative to the ‘normal’ length.
def check_interests(response, stage, payload,normal_len): if response.elapsed.total_seconds() > 10: print("Intresting time result in ", stage,": ", payload,"Diff: ",diff) diff = len(response.text)-normal_len if diff > 2: print("Intresting len result in ", stage,": ", payload,"Diff: ",diff)
In addition, it’s important to specify the server for OOB requests in transmitted payloads and check it for responses on a regular basis.
Conclusions
In almost all cases, the identification and exploitation of multistep SQLi requires some coding. If you use sqlmap, it’s required for tampering (sqlmap subsequently exploits the injection by its own). But currently, sqlmap cannot help in exploitation of multistep injections that require a confirmation request to be sent after the main request containing payload.
More sophisticated injections can be detected using custom scripts (that track status, length, and response time) and out-of-bound payloads. But in such cases, you have to exploit the detected injection manually.
How to protect against such attacks? The only effective way is to validate the data at all operational stages of the app, not just at the entry point.
Good luck in your searches for nontrivial injections!

2022.01.11 — Pentest in your own way. How to create a new testing methodology using OSCP and Hack The Box machines
Each aspiring pentester or information security enthusiast wants to advance at some point from reading exciting write-ups to practical tasks. How to do this in the best way…
Full article →
2023.02.21 — Herpaderping and Ghosting. Two new ways to hide processes from antiviruses
The primary objective of virus writers (as well as pentesters and Red Team members) is to hide their payloads from antiviruses and avoid their detection. Various…
Full article →
2022.06.01 — Routing nightmare. How to pentest OSPF and EIGRP dynamic routing protocols
The magic and charm of dynamic routing protocols can be deceptive: admins trust them implicitly and often forget to properly configure security systems embedded in these protocols. In this…
Full article →
2022.02.15 — EVE-NG: Building a cyberpolygon for hacking experiments
Virtualization tools are required in many situations: testing of security utilities, personnel training in attack scenarios or network infrastructure protection, etc. Some admins reinvent the wheel by…
Full article →
2023.02.13 — Ethernet Abyss. Network pentesting at the data link layer
When you attack a network at the data link layer, you can 'leapfrog' over all protection mechanisms set at higher levels. This article will walk…
Full article →
2023.06.08 — Cold boot attack. Dumping RAM with a USB flash drive
Even if you take efforts to protect the safety of your data, don't attach sheets with passwords to the monitor, encrypt your hard drive, and always lock your…
Full article →
2023.07.07 — VERY bad flash drive. BadUSB attack in detail
BadUSB attacks are efficient and deadly. This article explains how to deliver such an attack, describes in detail the preparation of a malicious flash drive required for it,…
Full article →
2022.02.15 — First contact: How hackers steal money from bank cards
Network fraudsters and carders continuously invent new ways to steal money from cardholders and card accounts. This article discusses techniques used by criminals to bypass security…
Full article →
2022.02.09 — F#ck da Antivirus! How to bypass antiviruses during pentest
Antiviruses are extremely useful tools - but not in situations when you need to remain unnoticed on an attacked network. Today, I will explain how…
Full article →
2022.06.01 — Quarrel on the heap. Heap exploitation on a vulnerable SOAP server in Linux
This paper discusses a challenging CTF-like task. Your goal is to get remote code execution on a SOAP server. All exploitation primitives are involved with…
Full article →