DEV Community

Rocky
Rocky

Posted on

Why %0a and %0A Gave Different Burp Responses (And Why That's a Bypass, Not a Bug)

There's a question sitting unanswered on Stack Overflow right now, tagged burp and ctf: someone testing in Burp noticed that sending %0a in a request gets one response, and sending %0A, same character, just uppercase hex, gets a different one. No answers, days later. It's a good question, because the confusion is completely reasonable and the resolution is a technique worth actually knowing.

Here's the part that makes it confusing: it shouldn't matter. RFC 3986, the URI spec, is explicit that percent-encoded hex digits are case-insensitive. %0a and %0A both decode to the exact same byte, 0x0A, a line feed. Any spec-conformant decoder is required to treat them identically. So if you're seeing two different responses to what decodes to the same input, the decoder isn't where the difference is coming from. Something else in the chain is looking at the encoded string itself, before decoding, and treating %0a and %0A as two different strings because that's exactly what they are as raw text, even though they mean the same thing once decoded.

That "something else" is almost always a filter, a WAF rule, or a hand-rolled input check written as a regex or a string match against the raw request. And a huge number of those checks are written case-sensitively without anyone intending it, because it's the default: a rule built to block %0a in a header value, written as if "%0a" in raw_input, silently lets %0A straight through. The person who wrote the check was almost certainly thinking about the byte, a newline, and testing with lowercase because that's what most encoding tools output by default. They never tested the uppercase variant because nothing in the ticket said to.

This is worth caring about specifically because %0a/%0A is CRLF, and CRLF landing somewhere it shouldn't is never a cosmetic bug. A carriage-return-line-feed sequence that a filter is trying to strip or block is usually there to prevent CRLF injection, HTTP header injection, response splitting, or in the right context, request smuggling, injecting a fake header, a fake response, or a boundary the receiving system trusts because it thinks the request ended. If the block is case-sensitive and the parser downstream isn't, uppercasing the encoding is the entire bypass. No exotic technique, no chaining, just changing two characters from a to A.

The habit this should build, and it generalizes well past CRLF: the first payload you send to a filter is a test, not a conclusion. If %0a gets blocked, don't move on, try %0A. Try double encoding, %250a decodes to %0a after one pass and might slip past a filter that only decodes once. Try mixed case in multi-byte sequences. Try the raw byte if the transport allows it. A filter that blocks the exact string a scanner tries by default and nothing else isn't a solved problem, it's a filter that was tested against one payload.

This exact gap, where an automated scan or a naive filter treats "blocked the obvious payload" as "the input is safe," is the entire reason manual testing with Burp exists as a discipline instead of just running a scanner and reading the report. Codelivly's Web Application Hacking Book for Beginners walks through exactly this kind of manual bypass methodology against the OWASP Top 10 with Burp Suite, testing past the first blocked payload instead of stopping at it. The free Broken Access Control lab and Command Injection lab on codelivly.com are good reps for the same "the filter caught the obvious one, now what" instinct before you go further with it.

Top comments (0)