Skip to main content

Command Palette

Search for a command to run...

The BApp That Almost Worked: Building curl2repeater 🔧

Updated
•5 min read•View as Markdown
The BApp That Almost Worked: Building curl2repeater 🔧
K

Application Security Engineer

There's always that one extension everyone has installed and nobody's actually checked. For BurpSuite, mine was Paste cURL to Repeater. I'd had it installed for months before I bothered actually looking at what it was sending under the hood (in hindsight, probably should've checked on day one, but who does that ¯\_(ツ)_/¯ ).

The curl command that broke it

I was doing attack surface analysis work on a payout notification endpoint. Pulled the request off the network tab as curl the normal way, right click, copy as cURL. Bearer token in the header, nine -F fields, one of them a multi-line email body, two attachments=@path uploads. Nothing exotic.

Pasted it into Repeater. Looked fine on the surface. Then, mostly out of paranoia, I compared the raw bytes against what curl itself would've actually sent, and it wasn't the same request at all. The boundary in Content-Type didn't match the one in the body. One of the two attachments fields had silently overwritten the other, because somewhere in the parser, form fields were sitting in a map instead of a list. The multi-line body got cut off at the first newline like the rest of it never existed.

And none of that throws an error. Repeater just sends it. Best case the server rejects it with something unhelpful, worst case it accepts the mangled version and you burn an hour convinced the endpoint is the broken thing.

If the tool rebuilding your request gets it wrong, you're testing a request you never actually sent. Quietly, in the background, no warning, nothing. That's the part that got me.

So why did the parser miss it?

Honestly it's less a bug and more that the parser was never written with multipart in mind at all. Duplicate field names, ;filename= and ;type= overrides, boundary generation, none of that is optional if you actually want a curl-to-HTTP converter that works for forms. And the failure is invisible by design, because HTTP doesn't care whether your multipart body is internally consistent. It just wants a boundary string somewhere in the header.

A malformed body sitting behind a perfect syntactically fine Content-Type goes straight through.

At that point I just decided to write my own instead of forking the repo and updating the code. I needed it working that afternoon, not whenever someone got around to a PR (we've all been there with open source issues that sit for six months).

Building it so I could actually test it

The one decision I stuck to, and I'm glad I did, was keeping the parsing logic completely separate from Burp. CurlLexer, CurlCommand, FormField, CurlParser, MultipartBodyBuilder, HttpRequestBuilder — all of it lives in a core package with zero Montoya imports. The Burp panel itself is barely more than a button that calls into that core and dumps the result into a Repeater tab.

That's the only reason I could actually test the failure mode without touching Burp at all. Took the exact curl command that broke the original BApp and fed it straight into the parser as a unit test, then asserted on the raw bytes it spat out:

input: 9 -F fields (incl. multi-line body), 2 attachments=@path, Bearer header
expect: field order preserved, both "attachments" parts present,
        Content-Type boundary matches body, Content-Length correct

Field order and duplicate names survive because form fields stay in a list the whole way through, never a dict (this one felt obvious in hindsight, but clearly wasn't obvious enough for whoever wrote the original).

The boundary gets generated the same way curl does it, and Content-Type gets rebuilt from that boundary regardless of whatever the user typed in.

For the two @path attachments, the parser actually checks if the file exists on disk. If it does, real bytes go in. If it doesn't, and this comes up more than you'd think, especially when you're pasting someone else's curl command from a different machine; it drops in a labeled placeholder and a warning instead of quietly sending an empty part and letting you believe the upload went through.

Anything the parser doesn't recognize just gets logged and skipped. One unknown flag shouldn't stop you from testing the other fifteen things the command is doing.

Before I actually trusted it

I didn't plug the parser into the Burp panel first. Ran it standalone against the same payout notification curl command and compared the output byte by byte against what I'd built by hand.

Field order matched. Both attachments parts showed up under the same name, neither one clobbering the other. Boundary matched between header and body. Content-Length was right. The two D:\... paths from the original capture obviously don't exist on my machine, so it correctly flagged both as missing instead of pretending the uploads happened.

Only after all of that did I actually loaded the jar and check that Repeater showed the same thing.

The part that stuck with me

Everyone treats "paste curl into Burp" as a solved problem, right up until the boundary's wrong and nothing tells you. I'd apparently been sending malformed requests for months without noticing, because Burp has zero reason to flag a Content-Type header that looks perfectly valid on its face. Kind of humbling, honestly, given how much I trust that right-click menu.

These days I check the raw bytes on anything that's touched a form parser before I trust a single response that comes back. Cheap habit, saved me more than once already.

curl2repeater's on GitHub if you want the parser or the test harness: KaustubhRai/curl2repeater. Might write up the parser internals in more depth at some point, might not — depends how bored I get. 🚀