It’s been a week since I launched the SafeCurl “Capture the Bitcoins” contest, which has been a fun, but humbling event.
Whilst I work as Security Engineer, and submitted my first bug bounty entry two years ago, I come from a development background. I’ve been writing PHP coming up to nine years now, though nothing much in production for the past year and a half.
I wanted to take a break from searching for bugs, so decided to write some PHP (the language I surprisingly love). SafeCurl seemed like a great starting point - a useful package, not too large, and still involving web app security.
Once written, I launched the bounty. Primarilly to give it a thorough test, and partly because I wanted to see what it would be like receiving bug reports rathering than submitting them.
In my head, I’d assumed that it would take ages for someone to bypass my code (if it happened at all). In reality, it took 2 hours. The reason being that I had rushed the project, excited to get it released as soon as possible. Further investigation should have been done at the start, which would have stopped such a silly bypass being possible.
Initially, there was going to be one 0.25B⃦ bounty. However, if the prize was won before most people had seen the site, there’s less incentive to keep looking. So I re-filled the wallet, and assumed this time no one would find a bypass.
I paid out another 0.1B⃦ to two people suggesting a DNS rebinding attack may be possible. Whilst this was just a theory, I created a hot-fix to pin DNS in cURL.
Then, three more 0.25B⃦ bounties caused by inconsistencies in PHP’s URL parsing and the
curl_exec function. After the first two were paid out, I declared the bounty over. However, the third was so similar to the previous two, it was only fair to pay out (from my personal wallet).
I’ve rewarded an additional 0.95B⃦ than I’d planned, and I don’t have infinite Bitcoins, but it was worth the money.
As I mentioned above, this was a stupid mistake. In the code, I’d blacklisted certain private ranges (
0.0.0.0 could also be used to refer to localhost.
The solution was pretty simple - blacklist any reserved ranges.
Found by @zoczus.
I was made aware that my code wasn’t safe from a DNS rebinding attack. This would involve rapidly switching the A record for the domain name from a valid IP (which passes any checks), to an internal IP. Whilst this is theoretical, I’ve played around with it but couldn’t get it to exploit, it was 1am and didn’t want to risk it whilst I was asleep.
Two separate people raised it at the same time. Whilst I could have just paid the first, I thought it’d be fair to pay both since they came up with it independently (the Facebook attitude).
For this, the IP returned from
gethostbynamel is pinned by replacing the hostname in the URL with the IP, then passing the original hostname in the HTTP “Host” header.
URL parsing issue #1
This was an interesting one. Whilst the
btc.txt file couldn’t be accessed, it did bypass all other checks of SafeCurl so was worthy of the bounty.
http://user:[email protected][email protected] to
google.com to be returned (PHP sees
[email protected]? as the password). However, when the full URL is given to
curl_exec, it sees
safecurl.fin1te.net as the host, and
@google.com/ as the query string. Pretty cool trick.
A quick solution for this was to disable the use of credentials in the URL. This worked, until the next bypass was found.
Found by @shDaniell.
URL parsing issue #2
Similar to the previous, passing
http://validurl.com#user:[email protected] causes
parse_url to see
validurl.com as the host, and
user:[email protected] as the fragment. Like before,
curl_exec handles this differently and uses
This was patched by using
rawurlencode on the username, password and fragment to prevent the URL getting parsed differently.
Found by Marcus T.
URL parsing issue #3
The path and query string are now URL encoded too, with certain characters (
& = ; [ ]) left intact, else the receiving may not parse it properly.
Found by @iDeniSix.
Lesson #1 - Don’t Rush
The first issue, along with typos, were caused by me rushing the project. These could have been prevented by taking it a bit slower, and by doing a proper design and investigation phase before starting development.
Lesson #2 - Bug Bounties are a Great Idea
Had I launched my code straight into production, without having ~1,000,000 attempts to bypass it, would have meant that the issues above would not have been fixed, thus causing vulnerable code to be deployed.
There is a price to pay, namely the Bitcoins I paid out, but this is nothing compared to the cost of someone using it for malicious purposes.
Lesson #3 - Have Unit Tests, Get Code Reviews
This is something I’ve learnt from development in “real-life”. Unfortunately I didn’t apply this to my own project (partly because it was just me working on it, partly because of Lesson #1). Unit tests do seem a bit of a chore to write sometimes, but they can catch a lot of bugs being re-introduced in the codebase. Plus having someone look over your code from a different perspective is invaluable.
Lesson #4 - You’re Not as Good as You Think
This may sound like a horrible lesson, but it’s not. Having something “secure” you wrote be ripped to shreds is a really awesome thing. It makes you realise that there may be gaps in your knowledge, and you now know where they are, and how to fix them. I’m really excited to launch another for this exact reason.
SafeCurl version 2 will be released shortly. This will include real unit tests covering the code, and test cases for each of the bypasses (and any other techniques I can find). Plus, experimental IPv6 support will be added.
Another bounty will be launched at some point. Whether it’s a SafeCurl bounty, or another concept, I’ve not decided.
I will also be looking to port SafeCurl to other languages such as Java, Python, Ruby, etc. This will be more of a challenge, since my strongest skills lie with PHP. If anyone wants to help out drop me a message.
A great part of the event was looking inside the Apache access logs to see some of the attempts people were making. I’ve included statistics, if you’re curious.
Total attempts 1,140,803
Average attempts per person 651
Average attempts per person (Excluding top 10) 20