On 11 August 2017, I participated in an online lucky draw. Instead of winning some prizes, I stumbled upon two vulnerabilities. It turned out that their CAPTCHA was as poorly implemented; similar to what was reported on the M1 Wireless@SG site and their one-time password (OTP) authentication was not as strong as it should be.
The OTP vulnerability stems from the absence of throttling controls which allows an attacker to programmatically try every possible 6 digit OTP combination. This vulnerability potentially allows unauthorized access to participants’ registered profile, which contains sensitive personal information.
Both vulnerabilities had since been reported and fixed. Since the custodians of the system have requested to withhold their company’s name, I shall just refer to them as “the company”. This article serves as an advisory on potential pitfalls to avoid in the spirit of secure application development.
Special shout-out to Edgis members who have contributed and advised on this effort. Thank you for guiding me through my first responsible disclosure.
I shall get into the technical details in this section.
Poorly implemented CAPTCHA
From the images above, the value within the highlighted <p> tag, corresponds with the CAPTCHA image that is reflected on the browser. In fact, the CAPTCHA image consists of plaintext overlapping a background with static noise (below) at this url: https://xxxx/media/luckydraw/images/captcha_bg.png
In the image below, I modified the CAPTCHA field to exaggerate the effects for clarity.
One could easily bypass the CAPTCHA challenge by directly reading the codified value with a framework such as Qt. This allows an attacker to programmatically execute web form submissions to the company’s lucky draw site.
Thankfully, the CAPTCHA was not just a random value generated on the client’s side. The value was obtained using an ajax call to their backend at /UserPortal/newcaptcha.
Poorly implemented OTP
For this lucky draw site, the sign-in process does not require passwords. Instead, a 6-digit OTP will be sent via SMS to the user’s registered phone number or email address. Each user is uniquely identified by their NRIC. This could be a convenience feature to avoid the use of memorized passwords for a lucky draw that lasts for less than a month.
Because of the way it is implemented, a registered user account (with personal data) is only protected by a 6-digit PIN. As there are only 10^6 or a million possible combinations, an attacker can reasonably guess the OTP within the 10-minute timeout window using a script or tool.
Simulating an attack on the OTP authentication
Since this system only contains participants who registered for the lucky draw, the first step would be to generate an exhaustive list of valid NRIC numbers. A valid NRIC is simply a string of numbers that tallies with last check digit of the NRIC number. You can find the algorithm to generate valid NRIC numbers here: https://sawliew.com/nric-generator.
The next step would be to check which valid NRIC numbers are registered with the company’s lucky draw. This can be done by running a list of valid NRIC numbers against their sign in page. If the NRIC is NOT registered with the system, the following error message will pop-up.
If you have entered a valid NRIC, the portal will proceed to ask for a 6-digit OTP which will be sent to the authorized user via SMS.
Since the OTP is only 6 digits, the attacker can easily guess the password by brute force. To verify if any backend rate limiting controls was in put in place to prevent a successful brute force attack, I decided to simulate an attack on my own account.
Instead of sending a million requests (which may jeopardize the live system), I used Burp proxy to go through a wordlist of 20 possibilities. The last entry in the wordlist being the OTP password that was sent to my phone.
Using Burp, I set the request to point to /userPortal/getUserDetails:
Using Intruder in Burp, I can see that the userOpt parameter is where the OTP password should be. With that, the attacker can build a simple wordlist generated by just enumerating a million numbers from 000000 to 999999.
After running through the complete wordlist, we sort the results by their lengths. This is because the successful response will differ largely in length.
In this case, the larger length is attributed to the personal data found in the respond page. A screenshot showing the rendered response with personal data can be found below.
Information that would be have been revealed:
*These are listed as optional for the users to enter, it will still be displayed if the user entered them.
The vulnerability is due to the fact that the OTP is only 6 digits long with just 10^6 combinations and that the system allows unlimited attempts to sign in without timeout or detection of brute force attack. One easy way to resolve this issue without impacting the ease of use would be to rate limit the number of sign in attempts with the OTP. Any brute force attacks will be significantly hindered with rate limiting.
Overall, the vulnerabilities were fixed in a short period of time and I am glad that the system owners responded well. But these issues could have been easily avoided. On the technical side, there were a couple of important lessons from this encounter:
1. Custom implementations of security are usually not good
I felt that it would have been more secure if web developers used established CAPTCHA libraries that were written and reviewed by the community at large. Just like how software developers should not code their own encryption algorithm; perhaps web developers’ first option should not be to write their own CAPTCHA.
2. OTP is good, if you implement it correctly
The company implemented OTP, but they failed to detect or prevent brute force attacks. It would have been resolved if the company throttled the OTP submissions to prevent brute force attacks. This can be easily done at the firewall or web application.