Fun with CAPTCHA - Pt I
After spending the last half a decade reviewing web applications, I’ve come across multiple homebrewed CAPTCHA implementations. None of them have stood up to any kind of rigorous testing and vulnerabilities tended to start appearing with only a moderate amount of poking. Because of this, I decided to go after a widespread solution to see how the best implementations stood up to analysis.
When enabling this CAPTCHA solution within your application, the vendor gives you the option to leverage either the image or audio portion to verify the user. I decided to dig into the audio portion. First thing I wanted to do was pull down a large number of samples and do a comparison to attempt to gauge how many audio samples they were actually using. This proved to take way more time than I had anticipated. The few pieces of code I wrote for this portion can be found in the following repository:
Attempt 1 - Replay Attacks
First thing I did was just capture the AJAX request for the actual mp3 audio CAPTCHA within Burp. Replayed this a handful of times and, as you would expect, quickly tripped the automation protection. The AJAX request for the mp3 contained several URL params which looked to be either encoded or encrypted (money on the latter). A quick glance at the JS used to generate this URL proved fruitless as it was heavily obfuscated. Admittedly, I am not a JS fan and so decided to save that rabbit hole for a last resort.
Attempt 2 - Replay Attacks v2
The next series of attempts all relied on replaying a single mp3 request while modifying other parts of the request. First up was to randomize the User-Agent string. Second was randomizing the order of all HTTP request headers sent. Neither of these techniques made much of a difference so I moved on.
Attempt 3 - Simulating Human use
After a bit of playing around, I had a suspicion that the CAPTCHA provider was tracking mouse movements as another anti-automation layer. With that in mind, I wrote up a quick mouse movement bot via Java’s Robot class. I implemented this within the MouseMove Java class. This robot would reload and interact with the browser page pointed at my Flask app with the CAPTCHA service enabled. The browser was proxied over Burp which had a custom extension. The extension watched for any server responses with the Content-Type header set to “audio/mp3.” If so, the request data was saved to disk with a UUID as the filename. This extension also handled the User-Agent string randomization. I had the browser running inside a Windows VM with the Burp CA installed in order to bypass HSTS restrictions.
This actually did not work at first as the robot jumped the mouse around to random locations on the screen. The CAPTCHA provider was smart enough to determine this wasn’t human-like movement and I quickly tripped the anti-automation protection. I fixed this by writing up a quick auxilary function which would linearly move the mouse across the screen from its current position to a random one. This definitely increased the number of CAPTCHAs I could harvest before tripping the anti-automation protection, but it still wasn’t enough to begin harvesting a large number of them.
Attempt 4 - Proxying via AWS
After exhausting my options for randomizing requests from a single host, I decided it was time to start randomizing my origin IP. Being the frugal person I am, I looked around for some free proxy lists. Turns out I wasn’t the only one hitting the service over these proxies (surprise, surprise) as many had already, or were very close to, tripping their rate limiting functionality. I iterated over and saved those that could connect to the service via the proxy_tester.py script. Next up, I decided to iteratively connect over different endpoints of my personal VPN provider (for which they have many). This would have worked fine, but the VPN provider didn’t seem too happy with me doing that and I ended up tripping their rate limiting functionality by constantly reconnecting.
For my last attempt, I turned to AWS EC2 instances. I created an AMI from an Ubuntu instance which had Java and Burp installed. The instances deployed with a user script to start Burp with a config file which instructed Burp to listen on public interfaces. I spun up around 7 of these in a public VPC. The Burp extension was then charged with rotating between EC2 instances by modifying the user settings for an upstream proxy. After a CAPTCHA sample was harvested, the extension rebooted the EC2 box, which assigned it a new public IP, and the upstream proxy was updated. The full implementation can be found within the BurpExtender class.
This ended up being what I needed to start mass harvesting audio CAPTCHA files. I would eventually still trip the anti-automation functionality, but I could pull down enough samples before this happened, so it didn’t matter. Cloning the AMI and rebuilding the EC2 instances within another region was enough to bypass this. All in all, this took about a months worth of agonizingly slow testing before I got to this point. After I had the final scenario set up, I managed to pull down over 6,000 samples. It’s great demonstration of no matter how much effort you put into anti-automation functionality, there’s no complete solution.
Stay tuned for Part II which will delve into actually playing with the harvested samples themselves.