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 poorly implemented.
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.
Poorly implemented CAPTCHA
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: hxxps://[redacted]/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 memorised 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 authorised user via SMS.
Since the OTP is only 6-digit, the attacker can easily guess the password by brute force. To verify if any backend rate-limiting controls were 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 jeopardise 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 response page. A screenshot showing the rendered response with personal data can be found below.
Information that would be have been revealed:
FLOOR AND UNIT NUMBER*
BLOCK NUMBER AND STREET NAME
HOME/OFFICE PHONE NUMBER*
*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-digit 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.
11 Aug 2017, 0040hrs — Notification sent to appropriate authorities and system owner.
11 Aug 2017, 0923hrs — Received replies from authorities and system owner. Feedback was forwarded to the appropriate IT team.
14 Aug 2017, 18:24 hrs — Received an update from the IT team that the IT team will fix the OTP vulnerability before the CAPTCHA vulnerability.
15 Aug 2017 — Received update from the IT team that the OTP vulnerability will be fixed on 16 Aug 2017 and the CAPTCHA vulnerability will be fixed on 23 Aug 2017.
16 Aug 2017 — OTP vulnerability fixed.
25 Aug 2017 — Delays in CAPTCHA fix, postponed to 04 Sep 2017.
04 Sep 2017 — CAPTCHA vulnerability fixed.
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:
Custom implementations of security are usually not good. I felt that it would have been more secure if the 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.
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.