curl
curl sends one request and shows you exactly what came back: the body, the headers, the status code, the redirects, and the timing. Reach for it the moment the browser hides too much, whether you are reproducing a finding as a clean repeatable request, replaying an API call, or answering “is that endpoint actually returning 200?”
Mental model: curl [options] URL. Everything else is one flag away.
New to HTTP? Core concepts
- URL: scheme, host, optional port, path, query, for example
http://TARGET_IP:8080/api/users?id=1 - Method (verb): GET reads, POST sends, plus PUT, DELETE, HEAD, OPTIONS via
-X - Request headers (
-H) are what you send; response headers (-i/-I) are what the server returns - Status code:
2xxsuccess,3xxredirect,4xxyour error,5xxserver error - Body: the payload returned, or the data you send with
-d - Cookies and sessions:
-bsends,-csaves to a jar - TLS: HTTPS; curl verifies certificates by default and fails on a bad one
When to reach for curl (and when not)
Reach for it to reproduce a finding as an exact repeatable request, replay an API call, check a status or header after a change, verify a security header (HSTS, CSP) is really sent, or isolate whether a problem is the server, a redirect, TLS, or DNS.
Reach for something else when you need to render JavaScript (use a browser or headless browser), fuzz paths or parameters at scale (ffuf, gobuster), intercept and replay with a UI (Burp, mitmproxy), or just download and mirror files (wget).
Install, update, verify#
sudo apt install -y curl # Debian / Ubuntu / Kali
sudo dnf install -y curl # Fedora / RHEL
sudo apt install --only-upgrade -y curl # update
curl --version # version, TLS backend, protocols (HTTP2/HTTP3)
command -v curl # confirm it is on PATH
curl ships by default on Windows 10+ and macOS. On Windows, PowerShell’s curl is an alias for Invoke-WebRequest, so call curl.exe to get the real tool.
Flags you’ll actually use#
Skim these first; the cheat sheet below is these flags combined.
| Flag | Does | Flag | Does |
|---|---|---|---|
-s | Silent (no progress bar) | -H | Add a request header |
-S | Show errors even with -s | -d | Send a POST body (form-encoded) |
-i | Include response headers | --data-binary | Send a body’s exact bytes (@file reads a file) |
-I | HEAD, headers only | -F | Multipart form / file upload |
-L | Follow redirects | -o / -O | Save to a name / keep remote name |
-X | Set the method | -C - | Resume a partial download |
-u | Basic auth user:pass | -w | Write-out vars (status, timing, sizes) |
-b / -c | Send / save cookies | -k | Skip TLS verification (labs) |
-A / -e | Set User-Agent / Referer | -x | Route through a proxy |
--resolve | Pin a hostname to an IP | -v | Verbose sent/received trace |
Cheat sheet#
Every block starts simple and adds one flag at a time, so you can stop at the line that does the job.
# Build up a request: stop at the line you need
curl URL # 1. the body
curl -i URL # 2. + response headers
curl -sS URL # 3. + quiet, but keep errors
curl -sSiL URL # 4. + follow redirects, show each hop
# Status and timing (scriptable)
curl -s -o /dev/null -w '%{http_code}\n' URL # just the status code
curl -s -o /dev/null \
-w 'dns:%{time_namelookup} ttfb:%{time_starttransfer} total:%{time_total}\n' URL
# Send data
curl -d 'id=1&user=admin' URL # form POST (implies POST)
curl -H 'Content-Type: application/json' -d '{"id":1}' URL # JSON POST
curl --data-binary @payload.json URL # send a file's exact bytes
curl -F '[email protected]' URL # multipart upload
curl -X PUT -d '{"role":"admin"}' URL # any method
# Headers, auth, identity
curl -H 'Authorization: Bearer TOKEN' URL # bearer token
curl -u USER:PASS URL # basic auth
curl -A 'Mozilla/5.0' URL # set User-Agent
curl -e 'https://ref.example/' URL # set Referer
curl -H 'Host: vhost.local' http://TARGET_IP/ # vhost by IP + Host header
curl --resolve vhost.local:443:TARGET_IP https://vhost.local/ # pin DNS, real SNI
# Cookies and sessions
curl -c jar.txt -d 'user=U&pass=P' URL # save cookies from login
curl -b jar.txt URL # replay the saved session
# Files and ranges
curl -O URL # download, keep remote name
curl -o out.bin URL # download to a chosen name
curl -C - -O URL # resume a partial download
curl -r 0-1023 URL # first 1024 bytes only (range request)
# TLS, proxy, protocol
curl -k https://TARGET_IP/ # skip cert check (labs only)
curl --cacert ca.pem https://host/ # trust a specific CA
curl -x http://127.0.0.1:8080 URL -k # route through Burp/mitmproxy
curl --http2 -I https://host/ # force HTTP/2
curl --compressed URL # request gzip, decode transparently
# Debug and resilience
curl -v URL # full sent (>) and received (<) trace
curl --retry 3 --retry-delay 2 URL # retry transient failures
curl -s URL | jq . # pretty-print / filter JSON
Command breakdowns#
Each block below explains one situation, the exact command, and what to expect back.
Check whether a page is up (status code only)#
curl -s -o /dev/null -w '%{http_code}\n' http://TARGET_IP/
Prints a bare 200, 302, 401, or 404. -o /dev/null throws away the body, -w prints only the code. Ideal inside loops and scripts.
See headers and body together#
curl -i http://TARGET_IP/login
Headers, a blank line, then the body. The fastest single-shot triage view. Use -I for a HEAD request when you want headers only.
Follow redirects to the final page#
curl -sSiL http://TARGET_IP/
-L follows each 3xx; -i shows every hop’s headers, so you see the whole redirect chain instead of just the first Location.
POST a login form#
curl -i -X POST http://TARGET_IP/login -d 'username=admin&password=hunter2'
-d form-encodes the body and implies POST. Watch the response for a Set-Cookie or a 302 to a dashboard.
POST JSON to an API#
curl -i -X POST http://TARGET_IP:8080/api/users \
-H 'Content-Type: application/json' \
-d '{"name":"svc","role":"user"}'
A 201 usually means created. Forget the Content-Type and most APIs reject the body. For a large or exact payload use --data-binary @file.json.
Upload a file (multipart)#
curl -F '[email protected]' -F 'note=lab' http://TARGET_IP/upload
-F builds a multipart/form-data body, one -F per field; @ reads a file. This is how you test file-upload endpoints.
Send a bearer token or basic auth#
curl -s http://TARGET_IP/api/me -H 'Authorization: Bearer TOKEN'
curl -s -u USER:PASS http://TARGET_IP/protected/
A 401 back means the credential is missing or wrong; read the WWW-Authenticate header for the scheme the server wants.
Save a session and reuse it#
curl -s -c jar.txt -d 'username=U&password=P' http://TARGET_IP/login
curl -s -b jar.txt http://TARGET_IP/dashboard
-c writes the cookie jar on login, -b sends it back. This is how you stay authenticated across separate curl calls.
Test a virtual host without DNS#
curl -s http://TARGET_IP/ -H 'Host: internal.local'
curl -s --resolve internal.local:443:TARGET_IP https://internal.local/
The Host header hits a name-based vhost by IP. For HTTPS use --resolve instead, so the TLS SNI and certificate match the real hostname.
Resume a large download#
curl -C - -O http://TARGET_IP/files/big.iso
-C - tells curl to figure out the offset and continue where a dropped transfer stopped, instead of starting over.
Ignore a self-signed certificate#
curl -k -I https://TARGET_IP/
-k skips TLS verification. Labs only; never normalize it against real targets. To do it properly, trust the CA with --cacert ca.pem.
Find where the time goes on a slow endpoint#
curl -s -o /dev/null \
-w 'dns:%{time_namelookup} connect:%{time_connect} ttfb:%{time_starttransfer} total:%{time_total}\n' \
http://TARGET_IP/
Splits DNS versus connect versus server think-time, so you blame the right layer instead of guessing.
Route through Burp or mitmproxy#
curl -s -x http://127.0.0.1:8080 http://TARGET_IP/ -k
-x sends the request through your proxy, so it lands in the history for inspection and replay.
Debug exactly what is sent and received#
curl -v http://TARGET_IP/api/health
> lines are what you sent, < lines are the response, plus the TLS handshake. The first place to look when a request behaves unexpectedly.
Reading the response#
| You see | Meaning | Do next |
|---|---|---|
200 / 201 / 204 | Success | Read the body, it is the real content |
301 / 302 / 307 | Redirect | Check Location, or add -L to follow |
401 | Needs auth | Add -u or an Authorization header |
403 | Forbidden | You reached it but cannot have it |
404 | Missing | Wrong path, enumerate with ffuf/gobuster |
429 | Rate limited | Slow down, add delays |
5xx | Server broke | Often interesting; confirm it is not just an overloaded box |
Key headers: Location (redirect target), Set-Cookie (session), WWW-Authenticate (auth challenge), Content-Type (how to parse the body).
Troubleshooting#
| Problem | Cause | Fix |
|---|---|---|
command not found | Not installed / stale PATH | Reinstall; open a fresh shell |
Connection refused | Nothing on that port | Confirm the port (scan with Nmap first) |
| Hangs then times out | Host down or filtered | Check IP, route, firewall/scope |
Could not resolve host | DNS failure | Use the IP, or set Host / --resolve |
SSL certificate problem | Self-signed / bad cert | -k in labs, or --cacert the CA |
| Empty body on a 3xx | Redirect not followed | Add -L (and -i to see the hop) |
API rejects your -d body | Missing Content-Type | Add -H 'Content-Type: application/json' |
PowerShell curl acts weird | Aliased to Invoke-WebRequest | Call curl.exe |
Gotchas#
- Quote URLs with
&,?, or spaces, or the shell eats part of the address. -dstrips newlines and treats@fileas “read from file”. Use--data-binaryfor exact payloads.-ois your filename,-Ois the remote filename. Easy to overwrite the wrong file.- The status code and headers are the answer as often as the body is, so do not read the body alone.
--resolvebeats aHostheader on HTTPS, because it fixes SNI and cert validation too.
Pairs with#
nmap finds the open web ports first. ffuf and gobuster discover paths and parameters, then curl confirms each hit cleanly. jq slices JSON. Burp and mitmproxy (via -x) give full interception. For plain downloads and mirroring reach for wget; for exploratory API work with colorized output httpie is friendlier, but curl stays the most universal and scriptable.