A Practical cURL Methodology for Security Testing — Part 1: Observation

This guide documents cURL techniques for authorized security testing — from reading verbose output and diagnosing connection failures, to auditing headers, interpreting status codes, and extracting intelligence from error messages using DVWA as a real target.

A Practical cURL Methodology for Security Testing — Part 1: Observation cover

This guide documents cURL techniques for authorized security testing and penetration testing. I developed this methodology while practicing on intentionally vulnerable applications (DVWA) and testing environments. This is for security professionals, bug bounty hunters, and students learning offensive security for defensive purposes.

Important You must have DVWA set up to follow along with the practical examples.

Module 0: Setup

We will start by understanding the tool we have on hand. To do so, let's see what our curl is capable of. We will use --version.

BASH
➜  ~ curl --version
curl 8.11.1 (x86_64-redhat-linux-gnu) libcurl/8.11.1 OpenSSL/3.2.6 zlib/1.3.1.zlib-ng brotli/1.2.0 libidn2/2.3.8 libpsl/0.21.5 libssh/0.11.3/openssl/zlib nghttp2/1.64.0 OpenLDAP/2.6.10
Release-Date: 2024-12-11
Protocols: dict file ftp ftps gopher gophers http https imap imaps ipfs ipns ldap ldaps mqtt pop3 pop3s rtsp scp sftp smb smbs smtp smtps telnet tftp ws wss
Features: alt-svc AsynchDNS brotli GSS-API HSTS HTTP2 HTTPS-proxy IDN IPv6 Kerberos Largefile libz NTLM PSL SPNEGO SSL threadsafe TLS-SRP UnixSockets

As you can probably guess I'm on Fedora Linux. But that aside — what does this information give us?

  1. Version. Mine is curl 8.11.1, released December 11th 2024. This matters because:

    1. Behavior changes between versions
    2. Flags appear/disappear
    3. TLS defaults change

    When something behaves differently on another system, this is your first check.

  2. Protocols. My curl supports: dict file ftp ftps gopher gophers http https imap imaps ipfs ipns ldap ldaps mqtt pop3 pop3s rtsp scp sftp smb smbs smtp smtps telnet tftp ws wss. This defines the attack surface curl can reach from this machine.

  3. Features. This tells us whether a certain check will even work. For example, if we don't have the IPv6 feature then an IPv6 test is dead on arrival. We are mapping attack surface and capabilities.

Important curl is a protocol speaker. It connects to (IP, port) and speaks whatever the URL scheme says. The connection and request lifecycle is DNS → TCP → Protocol → Application. When troubleshooting, pay attention to this sequence.
Diagram of the curl connection and request lifecycle: DNS, TCP, Protocol, Application
curl connection and request lifecycle — DNS → TCP → Protocol → Application

Module 1: Foundations

Here we will try to fetch pages, read their headers, and if something goes wrong we will troubleshoot it.

📝
Note — stdout and stderr These will appear a few times in this material. If you don't know what they are:
  1. stdout refers to the default output stream in a computer program — the channel through which a program displays its output to the user or another program.
  2. stderr (Standard Error) is a stream generated by programs when they encounter an error or exceptional condition. It's basically how programs communicate error messages to users, admins, and other programs.

Module 2: Verbose Diagnostics

In this section we will learn to read verbose output and troubleshoot each layer of the connection and request lifecycle.

  1. DNS failure.

    BASH
    ➜  Training-Camp curl -v http://nonexistent.example/ # Our operation stops immediately
    * Could not resolve host: nonexistent.example
    * shutting down connection
    curl: (6) Could not resolve host: nonexistent.example
  2. TCP timeout. For this we will use an unreachable host:

    BASH
    ➜  Training-Camp curl -v http://10.255.255.1/ # No DNS resolve needed — scanning an IP
    *   Trying 10.255.255.1:80... # This took a couple minutes
    * connect to 10.255.255.1 port 80 from 192.168.15.9 port 52694 failed: Connection timed out
    * Failed to connect to 10.255.255.1 port 80 after 134377 ms: Could not connect to server
    * closing connection #0
    curl: (28) Failed to connect to 10.255.255.1 port 80 after 134377 ms: Could not connect to server

    Another example — DNS resolves fine but the port is closed:

    BASH
    ➜  Training-Camp curl -v https://httpbin.org:8000/
    * Host httpbin.org:8000 was resolved. # DNS resolution succeeded
    * IPv6: (none)
    * IPv4: 98.85.201.92, 44.209.11.94, 52.72.212.236, 52.55.226.129, 54.158.135.159, 54.205.230.94
    *   Trying 98.85.201.92:8000...
    * connect to 98.85.201.92 port 8000 from 192.168.15.9 port 39202 failed: Connection timed out
    *   Trying 44.209.11.94:8000... # Will continue until every IP is checked (6 IPv4s in our case)
  3. Successful connection. Now let's see what a full successful connection looks like and how to read it:

    BASH
    ➜  ~ curl -v https://httpbin.org                                      # Initial command
    * Host httpbin.org:443 was resolved.                                  # DNS resolution succeeded
    * IPv6: (none)
    * IPv4: 52.72.212.236, 54.158.135.159, 98.85.201.92, 52.55.226.129, 54.205.230.94, 44.209.11.94
    *   Trying 52.72.212.236:443...
    * ALPN: curl offers h2,http/1.1                                       # ALPN negotiation begins after TCP succeeds, during TLS handshake
    * TLSv1.3 (OUT), TLS handshake, Client hello (1):                    # TLS handshake started
    *  CAfile: /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem
    *  CApath: none
    * TLSv1.2 (IN), TLS handshake, Server hello (2):
    * TLSv1.2 (IN), TLS handshake, Certificate (11):                     # Mixed TLS version lines can appear due to handshake internals
    * TLSv1.2 (IN), TLS handshake, Server key exchange (12):
    * TLSv1.2 (IN), TLS handshake, Server finished (14):
    * TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
    * TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
    * TLSv1.2 (OUT), TLS handshake, Finished (20):                       # TLS handshake finished
    * SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256 / secp256r1 / rsaEncryption
    * ALPN: server accepted h2                                            # ALPN connection accepted
    * Server certificate:                                                 # curl shows host certificate info
    *  subject: CN=httpbin.org
    *  start date: Jul 20 00:00:00 2025 GMT
    *  expire date: Aug 17 23:59:59 2026 GMT
    *  subjectAltName: host "httpbin.org" matched certs "httpbin.org"
    *  issuer: C=US; O=Amazon; CN=Amazon RSA 2048 M03
    *  SSL certificate verify ok.
    *   Certificate level 0: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
    *   Certificate level 1: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
    *   Certificate level 2: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
    * Connected to httpbin.org (52.72.212.236) port 443                  # Connection confirmed
    * using HTTP/2
    * [HTTP/2] [1] OPENED stream for https://httpbin.org/                # HTTP/2 stream data
    * [HTTP/2] [1] [:method: GET]
    * [HTTP/2] [1] [:scheme: https]
    * [HTTP/2] [1] [:authority: httpbin.org]
    * [HTTP/2] [1] [:path: /]
    * [HTTP/2] [1] [user-agent: curl/8.11.1]
    * [HTTP/2] [1] [accept: */*]
    > GET / HTTP/2                                                        # Our request
    > Host: httpbin.org
    > User-Agent: curl/8.11.1
    > Accept: */*
    >
    * Request completely sent off
    < HTTP/2 200                                                          # Response headers from host
    < date: Sun, 04 Jan 2026 09:18:14 GMT
    < content-type: text/html; charset=utf-8
    < content-length: 9593
    < server: gunicorn/19.9.0
    < access-control-allow-origin: *
    < access-control-allow-credentials: true
    <
    <!DOCTYPE html>                                                       # Host HTML body
    <html lang="en">
    
    <head>
    ...

Module 3: Response Analysis

In this section I will show you how to perform response analysis on specific examples and targets.

DVWA Login

Browsers maintain session state automatically. curl doesn't. To be able to log into DVWA you have to create a cookie that curl will use to authenticate you.

Diagram showing DVWA session flow: fetch login page, extract CSRF token, authenticate, use cookie
DVWA auth flow with curl — fetch, extract CSRF token, authenticate, then operate with the cookie
  1. First we fetch the login page and save it. Reading the HTML reveals that besides username and password, DVWA also has a CSRF token required for auth.

    BASH
    curl -c cookies.txt -s http://localhost:8081/login.php -o login.html
    grep user_token login.html    # find the token
    
    <input type='hidden' name='user_token' value='8b9d5057c79412598b062e9aa28c93ea' />
  2. Now authenticate by inserting the data into our cookie:

    BASH
    curl -c cookies.txt -b cookies.txt \
      -d "username=admin&password=password&Login=Login&user_token=8b9d5057c79412598b062e9aa28c93ea" \
      http://localhost:8081/login.php -o /dev/null
  3. Now we can work with curl properly using our authenticated session:

    BASH
    curl -b cookies.txt http://localhost:8081/index.php

Section 1: HTTP Status Codes as Behavioral Signals

Status codes tell you how the server processed your request.

200 OK — Success (or Silent Failure)

BASH
curl -I https://httpbin.org/status/200

What it means: Request processed successfully — OR server failed open (dangerous).

Security implications: 200 on an authentication bypass attempt = potential vulnerability. 200 with empty/unexpected body = logic flaw. 200 when expecting 403/401 = broken access control.

Test pattern:

BASH
# Try accessing admin endpoint
curl -I https://target.com/admin
# If 200 instead of 403 → broken authz

# Try invalid input
curl -I "https://target.com/api?id=../../etc/passwd"
# If 200 instead of 400 → input validation missing

302/301 — Redirects

BASH
curl -I https://httpbin.org/redirect/1

What it means: Server is sending you somewhere else. Often used for authentication flows. Can hide logic flaws.

Security implications: Redirect to login = you hit a protected resource. Redirect with Set-Cookie = session state changed. Location header content = potential open redirect.

Test pattern:

BASH
# Check where auth failure redirects
➜  ~ curl -I http://localhost:8081    # this is DVWA
HTTP/1.1 302 Found
Date: Wed, 07 Jan 2026 05:43:42 GMT
Server: Apache/2.4.25 (Debian)
Set-Cookie: PHPSESSID=cdcups8atl3ncoj2rqus7hprq5; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Set-Cookie: PHPSESSID=cdcups8atl3ncoj2rqus7hprq5; path=/
Set-Cookie: security=low
Location: login.php    # hardcoded — no redirect manipulation possible
Content-Type: text/html; charset=UTF-8

If the redirect is not hardcoded, you can try redirect manipulation:

BASH
curl -I "https://target.com/login?redirect=https://evil.com"
📝
Note The redirect parameter doesn't have to be called redirect — it can also be next, url, return, dest, continue. Check all variants to be thorough.

Full redirect chain example using -L (follow redirects) with -I:

BASH
➜  Training-Camp curl -IL "https://httpbin.org/redirect-to?url=https://google.com"
HTTP/2 302                               # Server tells us it's about to redirect
date: Wed, 07 Jan 2026 08:10:16 GMT
content-type: text/html; charset=utf-8
content-length: 0
location: https://google.com
server: gunicorn/19.9.0
access-control-allow-origin: *
access-control-allow-credentials: true

HTTP/2 301                               # Redirected permanently
location: https://www.google.com/
content-type: text/html; charset=UTF-8
content-security-policy-report-only: object-src 'none';base-uri 'self';script-src 'nonce-4pKn9YTGRrE0cJGq7I5smg' 'strict-dynamic' 'report-sample' 'unsafe-eval' 'unsafe-inline' https: http:;report-uri https://csp.withgoogle.com/csp/gws/other-hp
date: Wed, 07 Jan 2026 08:10:17 GMT
expires: Fri, 06 Feb 2026 08:10:17 GMT
cache-control: public, max-age=2592000
server: gws
x-xss-protection: 0
x-frame-options: SAMEORIGIN

HTTP/2 200                               # We are at google.com
content-type: text/html; charset=ISO-8859-1
content-security-policy-report-only: object-src 'none';base-uri 'self';script-src 'nonce-ZdfTfpwWPExEIzghCDuBXg' 'strict-dynamic' 'report-sample' 'unsafe-eval' 'unsafe-inline' https: http:;report-uri https://csp.withgoogle.com/csp/gws/other-hp
date: Wed, 07 Jan 2026 08:10:17 GMT
server: gws
x-xss-protection: 0
x-frame-options: SAMEORIGIN
cache-control: private
set-cookie: AEC=AaJma5ukZwNRv-5N7SWv3ZJkvrZP10c4fZIiIx9Pi3FN778Q1MRyvQ1yQw; expires=Mon, 06-Jul-2026 08:10:17 GMT; path=/; domain=.google.com; Secure; HttpOnly; SameSite=lax

401 vs 403 — Authentication vs Authorization

BASH
curl -I "https://httpbin.org/status/401"
HTTP/2 401
date: Wed, 07 Jan 2026 07:53:26 GMT
content-length: 0
server: gunicorn/19.9.0
www-authenticate: Basic realm="Fake Realm"
access-control-allow-origin: *
access-control-allow-credentials: true

curl -I "https://httpbin.org/status/403"
HTTP/2 403
date: Wed, 07 Jan 2026 07:52:55 GMT
content-type: text/html; charset=utf-8
content-length: 0
server: gunicorn/19.9.0
access-control-allow-origin: *
access-control-allow-credentials: true

What they mean: 401 = "I don't know who you are" (authentication failure). 403 = "I know who you are, but you can't do this" (authorization failure).

Security implications: 401 → add authentication, might work. 403 → you're authenticated but lack permission (IDOR testing target). 403 on one method, try another (GET → POST → PUT).

Test pattern:

BASH
# No auth
curl -I https://target.com/api/users/123
# Response: 401

# With auth
curl -I -H "Authorization: Bearer TOKEN" https://target.com/api/users/123
# Response: 403 → authorization check exists, test for IDOR

# Try different user ID
curl -I -H "Authorization: Bearer TOKEN" https://target.com/api/users/456
# Response: 200 → IDOR vulnerability found

400 — Bad Request

BASH
curl -I "https://httpbin.org/status/400"

What it means: Server rejected your input early. Input validation triggered.

Security implications: 400 = validation exists (good). But what validation? Try variations. Does it validate everything or just some fields?

Test pattern:

BASH
# Malformed JSON
curl -X POST -d "not-json" https://target.com/api

# Missing required field
curl -X POST -d '{"username":"test"}' https://target.com/api

# Type mismatch
curl -X POST -d '{"age":"not-a-number"}' https://target.com/api

500 — Internal Server Error

BASH
curl -I https://httpbin.org/status/500
HTTP/2 500
date: Wed, 07 Jan 2026 08:23:01 GMT
content-type: text/html; charset=utf-8
content-length: 0
server: gunicorn/19.9.0
access-control-allow-origin: *
access-control-allow-credentials: true

What it means: You broke something. Backend exception occurred. This is valuable.

Security implications: Input reached backend logic. Potential injection point. The error might leak information.

Test pattern:

BASH
# SQL injection attempt
curl "https://target.com/search?q=test'"
# Response: 500 → query reached database

# Path traversal
curl "https://target.com/file?path=../../../../etc/passwd"
# Response: 500 → file operation attempted

# Check response body for stack traces
curl -v "https://target.com/search?q=test'" 2>&1 | grep -i "error\|exception\|stack"

Section 2: Security Headers Audit

Missing security headers = attack surface exposed.

Testing DVWA:

BASH
➜  ~ curl -I http://localhost:8081
Server: Apache/2.4.25 (Debian)
X-Powered-By: PHP/7.0.33
Set-Cookie: PHPSESSID=abc; path=/
Set-Cookie: security=low

Security issues found:

Compare with httpbin (better configured):

BASH
➜  ~ curl -I https://httpbin.org
server: gunicorn/19.9.0
access-control-allow-origin: *
access-control-allow-credentials: true

Still shows server software, but no version number leaking detailed CVE info.

Security headers audit checklist showing missing vs present protections
Security headers audit — what's present, what's missing, and what each gap enables

What to look for:

Section 3: Response Timing Patterns

Timing reveals backend behavior.

Basic timing measurement:

BASH
# Create timing template
cat > curl-timing.txt << EOF
time_total: %{time_total}s\n
EOF

# Test endpoint
curl -w "@curl-timing.txt" -o /dev/null -s https://httpbin.org/delay/2
# Output: time_total: 2.1s

What timing tells you:

Fast response (< 100ms): Input rejected early. Validation before backend logic. Good for security.

Slow response (> 2s): Backend processing occurred. Database query executed. Potential for timing-based attacks.

Testing DVWA:

BASH
# Normal SQLi request
time curl -b cookies.txt \
  "http://localhost:8081/vulnerabilities/sqli/?id=1&Submit=Submit"

# With SLEEP injection (tests blind SQLi)
time curl -b cookies.txt \
  "http://localhost:8081/vulnerabilities/sqli_blind/?id=1' AND SLEEP(5)--&Submit=Submit"
# If this takes 5+ seconds longer → SQLi confirmed

Pattern: If you can control response time, you can extract data blindly.

📝
Note Timing-based exploitation will be covered in Part 2.

Section 4: Error Message Analysis

Errors leak implementation details.

Testing SQL errors in DVWA:

BASH
➜  ~ curl -b cookies.txt \
  "http://localhost:8081/vulnerabilities/sqli/?id=1'&Submit=Submit"

# Response body contains:
# "You have an error in your SQL syntax; check the manual that
# corresponds to your MySQL server version for the right syntax
# to use near ''1''' at line 1"

What this reveals: Database type is MySQL. Query structure leaked. Input reaches the SQL query directly. SQL injection confirmed.

Testing file inclusion errors:

BASH
➜  ~ curl -b cookies.txt \
  "http://localhost:8081/vulnerabilities/fi/?page=/etc/passwd"

# Response might contain:
# "Warning: include(/etc/passwd): failed to open stream"

What this reveals: Absolute paths in error messages. File system structure exposed. File inclusion vulnerability confirmed.

What to grep for:

BASH
curl http://target.com/test | grep -i "error\|warning\|exception\|fatal\|mysql\|postgresql\|syntax"
Important Verbose errors are intelligence goldmines. Production systems should hide error details.

Conclusion

What we covered in this part:

You can now observe and analyze server behavior with curl. Part 2 will teach you to actively exploit what you observe.

Warning Only test against systems you own or have explicit written permission to test.
Back to Articles