Introduction

I was reading Zachary Cutlip’s work (who reverse engineered Netgear minidlna binary):

And wondered if his work could still be useful today.

It was therefore necessary to establish a list of router models and versions vulnerable to the SQL injection in minidlna binary.

Let’s make a list of vulnerable firmware

The first question is how to identify whether the binary is vulnerable or not, and to this question we can find two answers.

The first solution is to perform a grep on the binary and check for the presence or absence of the following string:

  • INSERT OR REPLACE into BOOKMARKS VALUES ((select DETAIL_ID from OBJECTS where OBJECT_ID = '%q'), %q)

But that assumes that we’re in possession of the binary. Based on my experience, I know that Netgear firmware are packaged in different ways depending on the model and version. Sometimes, thanks to binwalk, we can identify a SquashFS, sometimes a UBIFS, and sometimes, they use a custom SquashFS, which we decompress using the sasquatch binary (thanks devttys0).

I’m lazy and didn’t wanted to go to the trouble of creating a tool that would handle all types of firmware and end up having to deal with a lot of edge cases. So I automatically went for solution 2.

I’ve already expressed this in several blog posts, everybody believes in reverse engineering, but for me, the solution for SOHO router auditing are the GPLs. I repeat, G-P-L (GNU General Public License). It has allowed me to identify 0days and greatly simplifies the process of creating POCs for 1days. Solution 2 is simple, download the GPLs (usually a simple archive), unpack the archive and use grep.

For some downloaded archives I’ve noticed a Russian doll system (an archive within an archive) but this isn’t the majority of cases (and is therefore an edge case) that we won’t be dealing with (so my solution isn’t perfect).

How to manage the different types of archive possible?

Once again, the solution is simple:

  • 7z

And to the question of where to find GPLs, look at the link below:

wget https://www.downloads.netgear.com/files/GDC/2649_GPLv1.html

The file containing the links can also be downloaded here in case Netgear changes the way its site works.

After cleaning the file containing the links, we obtain the following list of GPLs to download. All you have to do is run the following bash command (from folder /home/<USER>/Downloads/), and you will know which models and versions are potentially vulnerable.

cat link.lst|parallel -j 3 ./run.sh

File: run_0.sh

URL=$1
RINT=$RANDOM
WORKDIR=/home/<USER>/Downloads/Tests/$RINT
mkdir -p $WORKDIR
if [ $? -ne 0 ]
then
    RINT=$RANDOM
    WORKDIR=/home/<USER>/Downloads/Tests/$RINT
    mkdir -p $WORKDIR
fi
cd $WORKDIR
rm -rf $WORKDIR/*

# Download archive.
wget -q $URL
# Try to decompress archive.
7z x ./* 1>/dev/null 2>/dev/null
if [ $? -eq 0 ]
then
    # Looking for vulnerability.
    grep -R --text "INSERT OR REPLACE into BOOKMARKS VALUES ((select DETAIL_ID from OBJECTS where OBJECT_ID = '%q'), %q)" ./* 1>/dev/null 2>/dev/null
    if [ $? -eq 0 ]
    then
        # Vulnerability detected.
        echo "[FIRMWARE][+]: '$URL', might be vulnerable" >> /home/<USER>/Downloads/run.log
    fi
fi

# Removing everything.
rm -rf $WORKDIR

After letting the script run (this may take some time), we obtain the following result:

File: run_0.log

[FIRMWARE][+]: 'https://www.downloads.netgear.com/files/GPL/D3600_v1.0.0.47_GPL_20150422.tar', might be vulnerable
[FIRMWARE][+]: 'https://www.downloads.netgear.com/files/GPL/D6000_GPL_V1.0.0.64.tar.gz', might be vulnerable
[FIRMWARE][+]: 'https://www.downloads.netgear.com/files/GPL/DGND3800B_V3.0.0.10_src_full.zip', might be vulnerable

...

[FIRMWARE][+]: 'https://www.downloads.netgear.com/files/GPL/WNDR4500v2_V1.0.0.42_1.0.25_source.and.toolchain.tar.zip', might be vulnerable

The results can be presented as follows:

Models
D3600
D6000
DGND3800B
R3450UD
R6200
R6300
R7900P
R8000P
RAX30
RAXE300
VEGN2610
VEVG2660
WNDR3700v3
WNDR4000
WNDR4500
WNDR4500v2

Now that we have a list of potential targets, let’s take a look at how we can manually determine whether each target is vulnerable or not.

Let’s check if a specific firmware is vulnerable

For example, let’s take the router R6200 using firmware 1.0.1.56_1.0.43.

First, download the firmware using wget:

$ wget https://www.downloads.netgear.com/files/GDC/R6200/R6200-V1.0.1.56_1.0.43.zip

Use binwalk to see what the firmware contains:

$ binwalk R6200-V1.0.1.56_1.0.43.chk 

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
58            0x3A            TRX firmware header, little endian, image size: 9453568 bytes, CRC32: 0x2ED5266A, flags: 0x0, version: 1, header size: 28 bytes, loader offset: 0x1C, linux kernel offset: 0x1468C4, rootfs offset: 0x0
86            0x56            LZMA compressed data, properties: 0x5D, dictionary size: 65536 bytes, uncompressed size: 3936389 bytes
1337598       0x1468FE        Squashfs filesystem, little endian, non-standard signature, version 3.0, size: 8113171 bytes, 973 inodes, blocksize: 65536 bytes, created: 2015-06-09 03:20:14

Extract the Squashfs filesystem using dd:

$ dd if=R6200-V1.0.1.56_1.0.43.chk of=R6200-V1.0.1.56_1.0.43.squashfs skip=1337598 bs=1
8116028+0 records in
8116028+0 records out
8116028 bytes transferred in 15.831382 secs (512654 bytes/sec)

Decompress the filesystem using sasquatch:

$ sasquatch R6200-V1.0.1.56_1.0.43.squashfs 
SquashFS version [3.0] / inode count [973] suggests a SquashFS image of the same endianess
Non-standard SquashFS Magic: shsq
Parallel unsquashfs: Using 1 processor
Trying to decompress using default gzip decompressor...
Trying to decompress with lzma...
Detected lzma compression
Trying to decompress with lzma-adaptive...
Trying to decompress with lzma-alt...
Detected lzma-alt compression
919 inodes (1259 blocks) to write

[=================================================================================================================/] 1259/1259 100%

created 801 files
created 54 directories
created 118 symlinks
created 0 devices
created 0 fifos

Use grep to look for the vulnerability:

$ grep -Ri --text "INSERT OR REPLACE into BOOKMARKS VALUES ((select DETAIL_ID from OBJECTS where OBJECT_ID = '%q'), %q)" ./squashfs-root/usr/sbin/minidlna.exe
scpd xmlns="urn:schemas-upnp-org:service-1-0"root xmlns="urn:schemas-upnp-org:device-1-0"Internal Server Errormp3mp4m4ax-ms-wmawmax-flacx-wavL16pcm3gpp3gpapplication/oggdatavimpgx-ms-wmvwmvmkvx-mkvx-flvvnd.dlna.mpeg-ttsquicktimemovx-tivo-mpegTiVo@childCountdc:creatordc:datedc:descriptiondlna@refIDupnp:albumupnp:albumArtURI@dlna:profileIDupnp:artistupnp:actorupnp:genreupnp:originalTrackNumberupnp:searchClassupnp:storageUsedresres@bitrate@bitrateres@duration@durationres@nrAudioChannels@nrAudioChannelsres@resolution@resolutionres@sampleFrequency@sampleFrequencyres@size@sizesec:CaptionInfoExsec:dcmInfores@pv:subtitleFileTyperes@pv:subtitleFileUriav:mediaClassorder by  , upnp:classo.CLASSdc:titled.TITLEd.DATEd.DISC, d.TRACKd.ALBUM, TITLE ASCunhandled sort CriteriadummyArgumentInvalid Args<DeviceIDInvalid ActionPosSecondINSERT OR REPLACE into BOOKMARKS VALUES ((select DETAIL_ID from OBJECTS where OBJECT_ID = '%q'), %q)varNameConnectionStatusurn:schemas-upnp-org:control-1-0Invalid Var&gt;

We’ve gone from a router with potentially a vulnerable firmware to a router with theoretically a vulnerable firmware. We could also have just looked at the minidlna source code in the GPLs. Here’s the call stack to reach the vulnerable code:

  • main() minidlna.c
    • Process_upnphttp() upnphttp.c
      • ProcessHttpQuery_upnphttp() upnphttp.c
        • ProcessHTTPPOST_upnphttp() upnphttp.c
          • ExecuteSoapAction() upnpsoap.c
            • SamsungSetBookmark() upnpsoap.c

File: upnpsoap.c


...

static void
SamsungSetBookmark(struct upnphttp * h, const char * action)
{
    static const char resp[] =
        "<u:X_SetBookmarkResponse"
        " xmlns:u=\"urn:schemas-upnp-org:service:ContentDirectory:1\">"
        "</u:X_SetBookmarkResponse>";

    struct NameValueParserData data;
    char *ObjectID, *PosSecond;
    int ret;

    ParseNameValue(h->req_buf + h->req_contentoff, h->req_contentlen, &data);
    ObjectID = GetValueFromNameValueList(&data, "ObjectID");
    PosSecond = GetValueFromNameValueList(&data, "PosSecond");
    if( ObjectID && PosSecond )
    {
        ret = sql_exec(db, "INSERT OR REPLACE into BOOKMARKS"
                           " VALUES "
                           "((select DETAIL_ID from OBJECTS where OBJECT_ID = '%q'), %q)", ObjectID, PosSecond);
        if( ret != SQLITE_OK )
            DPRINTF(E_WARN, L_METADATA, "Error setting bookmark %s on ObjectID='%s'\n", PosSecond, ObjectID);
        BuildSendAndCloseSoapResp(h, resp, sizeof(resp)-1);
    }
    else
        SoapError(h, 402, "Invalid Args");

    ClearNameValueList(&data);	
}

...

static const struct 
{
    const char * methodName; 
    void (*methodImpl)(struct upnphttp *, const char *);
}
soapMethods[] =
{
    { "QueryStateVariable", QueryStateVariable},
    { "Browse", BrowseContentDirectory},
    { "Search", SearchContentDirectory},
    { "GetSearchCapabilities", GetSearchCapabilities},
    { "GetSortCapabilities", GetSortCapabilities},
    { "GetSystemUpdateID", GetSystemUpdateID},
    { "GetProtocolInfo", GetProtocolInfo},
    { "GetCurrentConnectionIDs", GetCurrentConnectionIDs},
    { "GetCurrentConnectionInfo", GetCurrentConnectionInfo},
    { "IsAuthorized", IsAuthorizedValidated},
    { "IsValidated", IsAuthorizedValidated},
    { "X_GetFeatureList", SamsungGetFeatureList},
    { "X_SetBookmark", SamsungSetBookmark},
    { 0, 0 }
};

...

That’s when I realized that in the first step I should have grep string:

  • ((select DETAIL_ID from OBJECTS where OBJECT_ID = '%q'), %q)

And not:

  • INSERT OR REPLACE into BOOKMARKS VALUES ((select DETAIL_ID from OBJECTS where OBJECT_ID = '%q'), %q)

Because there are line breaks in the source code. So, we’ve probably missed some targets.

After applying the patch and restarting the script (run_1.sh),the result is no longer 60 version potentialy vulnerable but more than 450.

Here is the new list of potentially vulnerable routers:

Models Models
AC1450 AC2100
AC2100v1 AC2400
AC2400v1 AC2600
AC2600v1 D3600
D6000 D6400
D7000 D7000v1
D7800 D8500
DC112A DGND3800B
EAX11v2 EAX12
EAX15v2 EX3700
EX3800 EX6000
EX6120 EX6130
EX6150 EX6200
EX6200v2 EX6250v2
EX6400v3 EX6410v2
EX6470 EX7000
EXS6190 MR70
MR90 MS70
MS90 R3450UD
R6200 R6200v2
R6220 R6250
R6260 R6260v1
R6300 R6300v2
R6330 R6330v1
R6350 R6350v1
R6400 R6400v2
R6700 R6700v2
R6700v3 R6800
R6800v1 R6850
R6850v1 R6900
R6900P R6900v2
R7000 R7000P
R7000PLUS R7100LG
R7200v1 R7300
R7350v1 R7400v1
R7450 R7450v1
R7900 R7900P
R8000 R8000P
R8300 R8500
R8900 R9000
RAX30 RAX5
RAXE300 RAXE450
RAXE500 RS400
VEGN2610 VEVG2660
WAC104 WAC124
WNDR3700v3 WNDR3700v5
WNDR4000 WNDR4500
WNDR4500v2 XR300
XR500  

Let’s take a look at how the binary is compiled

Let’s use find:

$ file ./squashfs-root/usr/sbin/minidlna.exe 
./squashfs-root/usr/sbin/minidlna.exe: ELF 32-bit LSB executable, MIPS, MIPS32 version 1 (SYSV), dynamically linked, interpreter /lib/ld-uClibc.so.0, stripped

The binary is compiled for mipsel (some routers may be using ARM), so we’ll need a dedicated toolchain. Select the toolchain you are interested in at:

Since the binary is compiled dynamically, let’s look at the libraries it uses:

$ readelf -d ./squashfs-root/usr/sbin/minidlna.exe

Dynamic section at offset 0x140 contains 41 entries:
  Tag        Type                         Name/Value
 0x00000001 (NEEDED)                     Shared library: [libpthread.so.0]
 0x00000001 (NEEDED)                     Shared library: [libexif.so.12]
 0x00000001 (NEEDED)                     Shared library: [libjpeg.so.7]
 0x00000001 (NEEDED)                     Shared library: [libsqlite3.so.0]
 0x00000001 (NEEDED)                     Shared library: [libavformat.so.52]
 0x00000001 (NEEDED)                     Shared library: [libid3tag.so.0]
 0x00000001 (NEEDED)                     Shared library: [libFLAC.so.8]
 0x00000001 (NEEDED)                     Shared library: [libvorbis.so.0]
 0x00000001 (NEEDED)                     Shared library: [libavcodec.so.52]
 0x00000001 (NEEDED)                     Shared library: [libavutil.so.49]
 0x00000001 (NEEDED)                     Shared library: [libogg.so.0]
 0x00000001 (NEEDED)                     Shared library: [libz.so.1]
 0x00000001 (NEEDED)                     Shared library: [libm.so.0]
 0x00000001 (NEEDED)                     Shared library: [libdl.so.0]
 0x00000001 (NEEDED)                     Shared library: [libgcc_s.so.1]
 0x00000001 (NEEDED)                     Shared library: [libc.so.0]
 0x0000000f (RPATH)                      Library rpath: [/home/builder/project/R6200/V1.0.1.56/2013_04_03_R6200_Beamforming/ap/gpl/minidlna/lib/]
 0x0000000c (INIT)                       0x404704
 0x0000000d (FINI)                       0x439620
 0x00000004 (HASH)                       0x4002b0
 0x00000005 (STRTAB)                     0x4029c0
 0x00000006 (SYMTAB)                     0x400dd0
 0x0000000a (STRSZ)                      6468 (bytes)
 0x0000000b (SYMENT)                     16 (bytes)
 0x70000016 (MIPS_RLD_MAP)               0x487e60
 0x00000015 (DEBUG)                      0x0
 0x00000003 (PLTGOT)                     0x487e70
 0x00000011 (REL)                        0x0
 0x00000012 (RELSZ)                      0 (bytes)
 0x00000013 (RELENT)                     8 (bytes)
 0x70000001 (MIPS_RLD_VERSION)           1
 0x70000005 (MIPS_FLAGS)                 NOTPOT
 0x70000006 (MIPS_BASE_ADDRESS)          0x400000
 0x7000000a (MIPS_LOCAL_GOTNO)           12
 0x70000011 (MIPS_SYMTABNO)              447
 0x70000012 (MIPS_UNREFEXTNO)            38
 0x70000013 (MIPS_GOTSYM)                0xb
 0x6ffffffe (VERNEED)                    0x404684
 0x6fffffff (VERNEEDNUM)                 4
 0x6ffffff0 (VERSYM)                     0x404304
 0x00000000 (NULL)                       0x0

We can see that the binary uses the library libsqlite3.so.0 located at /lib/libsqlite3.so.0 (on the filesystem).

Convert the SQL Injection to an Arbitrary File Write

SQL Stacked Queries

In some cases, SQLite allows you to use stacked queries. We can check that by reading the code of minidlna.

File: sql.c


...

int
sql_exec(sqlite3 *db, const char *fmt, ...)
{
    int ret;
    char *errMsg = NULL;
    char *sql;
    va_list ap;
    //DPRINTF(E_DEBUG, L_DB_SQL, "SQL: %s\n", sql);

    va_start(ap, fmt);

    sql = sqlite3_vmprintf(fmt, ap);
    ret = sqlite3_exec(db, sql, 0, 0, &errMsg);
    if( ret != SQLITE_OK )
    {
        DPRINTF(E_ERROR, L_DB_SQL, "SQL ERROR %d [%s]\n%s\n", ret, errMsg, sql);
        if (errMsg)
            sqlite3_free(errMsg);
    }
    sqlite3_free(sql);

    return ret;
}

...

The sqlite3_exec() interface runs zero or more UTF-8 encoded, semicolon-separate SQL statements passed into its 2nd argument, in the context of the database connection passed in as its 1st argument. - https://www.sqlite.org/c3ref/exec.html

Since minidlna interacts with a SQLite database and since it supports stacked queries, we can transform the SQL injection found by Zachary into an Arbitrary File Write.

Here is a generic example of stacked queries adapted from Swissky’s git repository:

ATTACH DATABASE '/tmp/p.php' AS d;CREATE TABLE d.t (p text);INSERT INTO d.t (p) VALUES ("<?php system($_POST[0]); ?>");--

The ATTACH DATABASE command associates the database file filename with the current database connection under the logical database name database_name . If the database file filename does not exist, it will be created. - https://www.oreilly.com/library/view/using-sqlite/9781449394592/re79.html

However, we must keep in mind that by using this payload we will be able to control the file in which we are writing and part of its contents.

Why only part of the contents?

I invite you to read the following documentation:

But to put it simply, the file created will look like this!

Database file: /tmp/p.php
    ---------------------------
   |                           |
   |           DATA            | <- Ignored by the PHP engine
   |                           |
   |---------------------------|
   |                           |
   |<?php system($_POST[0]); ?>|
   |                           |
   |---------------------------|
   |                           |
   |           DATA            | <- Ignored by the PHP engine
   |                           |
    ---------------------------

As the PHP documentation so clearly states:

When PHP parses a file, it looks for opening and closing tags, which are <?php and ?> which tell PHP to start and stop interpreting the code between them. Parsing in this manner allows PHP to be embedded in all sorts of different documents, as everything outside of a pair of opening and closing tags is ignored by the PHP parser. - https://www.php.net/manual/en/language.basic-syntax.phptags.php

Write what where?

We are now able to write webshell, but the question is where to place it on the target filestytem so that we can execute it without authentication.

Tenable have identified a directory that may be suitable for us (by plugging in a USB key) thanks to CVE-2023-27851.

NETGEAR contains a file sharing mechanism that unintentionally allows users with upload permissions to execute arbitrary code on the device.

The lighttpd configuration for the device is configured to execute any PHP code in the “/webs” directory by default when browsed to. Since all file shares are served from “/webs/shares”, it is possible for user-provided code and scripts to be executed. For example, if an attacker places code, such as a PHP webshell, into a share and then visits it (for example: http://<device>/shares/USB_Storage/share/webshell.php), they are able to execute arbitrary code on the device. - https://www.tenable.com/security/research/tra-2023-9

Happily for us, we don’t need to plug in the USB stick as we have an Arbitrary File Write. We now have the complete chain that allows us to get Remote Code Execution via the LAN without authentication on vulnerable routers. Unfortunately for me, I’m not the only person to have identified this chain. So, I’d like to congratulate Insu Yun, Seunghyun Kim and Gyeongwon Kim from Hacking Lab for identifying this chain before me.


Bonus for the brave ones

Because this research has given me the opportunity to look at a lot of different HTTP server used by Netgear, here are some other bugs I’ve identified along the way, if they’re of any use to you.

D6000 Command Injection in boa (audited version: v1.0.0.80)

Because I was looking for memory corruptions in the .asp handler, I discovered that binary boa (/userfs/bin/boa) is vulnerable to command injection via a dangerous call to function popen(). The variable cmd is partially controllable by the attacker.

File: /userfs/bin/boa
Function: ExecuteHnapAction()

#include "boa.h"

int ExecuteHnapAction(request *req)
{
    char soapaction[1024] = {0}, buffer[CLIENT_STREAM_SIZE];
    char *temp_p = NULL;
    char cmd[32]={0};
    char filename[128]={0};
     
    FILE *pp = NULL, *fp = NULL;;
            
            
    bzero(buffer, CLIENT_STREAM_SIZE);
    //sscanf(line,"%*[^:]:%*[^:]://%*[^/]/%*[^/]/%[^\"]",soapaction);
    strcpy(soapaction, req->hnap_action);
    int len,read_len;
    char xml_body[4096];

    if (req->content_length){
        /* Read the XML content into buffer for futher parsing */
        len = atoi(req->content_length);
        lseek(req->post_data_fd, 0,SEEK_SET);
        read_len = read(req->post_data_fd,xml_body,len);
        //tcdbg_printf("read_len %d\n", read_len);
    }

    sprintf(filename,"/tmp/xml_body_%s.xml", soapaction);
    if (soapaction[0] != '\0')
    {

        ...

        fp = fopen(filename, "w");
        if (fp != NULL)
        {
            //fprintf(fp, xml_body); 
            fwrite(xml_body, sizeof(char), read_len, fp); 
        }
        fclose(fp);
       
        sprintf(cmd,"/userfs/bin/hnap_handler Action %s", soapaction);
        //sprintf(cmd,"/userfs/bin/hnap_handler Action %s -d", soapaction);
        if (strstr(cmd, "-d") != NULL)
            tcdbg_printf("debug %s %d cmd %s\n", __FUNCTION__, __LINE__, cmd);
        if((pp = popen(cmd, "r")) != NULL)
        {
            req->response_status = R_REQUEST_OK;
            if (req->simple)
                return 0;
            req_write(req, "HTTP/1.0 200 OK\r\n");
            print_http_headers(req);
            //print_no_cache(req);
            if (!req->is_cgi) {
               //print_content_length(req);
                print_last_modified(req);
                req_write(req, "Content-Type: text/xml; charset=\"utf-8\"\r\n");
                //print_content_type(req);
                req_write(req, "\r\n");
            }
            while(fgets(buffer,sizeof(buffer),pp)){
                req_write(req, buffer);
                if (strstr(cmd, "-d") != NULL)
                   tcdbg_printf("debug %s %d response %s",__FUNCTION__, __LINE__, buffer);
            }                
            req_flush(req);
            req->buffer_end = 0;
            req->status = DEAD;
            SQUASH_KA(req);
            pclose(pp);
        }

...

  • main() (boa.c)
    • select_loop() (boa.c)
      • process_requests() (request.c)
        • write_body() (read.c)
          • ExecuteHnapAction() (hnap.c)

R6200 Multiple Stack Based Buffer Overflow in httpd (audited version: v1.0.1.56_1.0.43)

The httpd binary contains multiple Stack Based Buffer Overflow. As you can see below as an example, the function fwCgiServiceEditPPPoE2() calls websGetVar() which parses the query parameters and retrieves the value associated with a parameter (applies URL decode if required), the parameter value is written to a buffer.

File: /usr/sbin/httpd
Function: fwCgiServiceEditPPPoE2()

undefined4 fwCgiServiceEditPPPoE2(char *param_1,int param_2,undefined4 param_3,char *param_4)

{
  bool bVar1;
  int iVar2;
  undefined4 *puVar3;
  int iVar4;
  char *pcVar5;
  char *pcVar6;
  undefined4 uVar7;
  uint uVar8;
  code *pcVar9;
  int iVar10;
  int *piVar11;
  undefined4 *__dest;
  uint unaff_s4;
  int *piVar12;
  code *pcVar13;
  in_addr local_510;
  uint local_50c [3];
  uint local_500;
  code *local_4fc;
  char acStack_4e4 [48];
  char acStack_4b4 [48];
  char acStack_484 [64];

  ...

  code *local_34;
  char *local_30;
  char *local_2c;
  
  websGetVar(param_1,"apply",local_144);
  if (local_144[0] == '\0') goto LAB_00455a38;
  piVar12 = DAT_005c86b0;
  if (DAT_005a9e20 < 1) {
    if (DAT_005a9e20 != 0) goto LAB_00455a38;
  }
  else {
    if (DAT_005c86b0 == (int *)0x0) goto LAB_00455a38;
    for (iVar4 = 1; iVar4 != DAT_005a9e20; iVar4 = iVar4 + 1) {
      if ((int *)piVar12[8] == (int *)0x0) goto LAB_00455a38;
      piVar12 = (int *)piVar12[8];
    }
  }
  if ((piVar12 == (int *)0x0) || (*piVar12 == 0)) goto LAB_00455a38;
  local_2c = acStack_244;
  websGetVar(param_1,"service_type",acStack_244);
  local_30 = acStack_4e4;
  websGetVar(param_1,"userdefined",local_30);

...

Interesting line:


...

  local_30 = acStack_4e4;
  websGetVar(param_1,(astruct *)0x4f398c,local_30);

...

local_30 point to acStack_4e4 and is a char array of length 48. As you can see, the function websGetVar() can copy up to 256 characters (255 char plus \0 as last char, \0 is shorthand for \000 which is an octal character escape) into the buffer. So, there’s an overflow. And as the buffer is located on the stack, it’s a Stack Based Buffer overflow.

Each time the function websGetVar() is called, an overflow may occur if the size of the destination buffer param_3 is less than 256 bytes.

File: /usr/sbin/httpd
Function: websGetVar()

undefined4 websGetVar(char *param_1,char *param_2,char *param_3)

{
  size_t __n;
  int iVar1;
  long lVar2;
  char cVar3;
  char *pcVar4;
  char *pcVar5;
  int iVar6;
  char local_28;
  char local_27;
  undefined local_26;
  char *pcStack_24;
  
  *param_3 = '\0';
  cVar3 = *param_1;
  iVar6 = 0;
  pcVar5 = param_1 + 1;
  do {
    pcVar4 = pcVar5;
    if (cVar3 == '\0') {
      return 0xffffffff;
    }
    __n = strlen(param_2);
    iVar1 = strncmp(param_1,param_2,__n);
    if ((iVar1 == 0) && (param_1[__n] == '=')) {
      if ((pcVar4[-2] == '&') || (iVar6 == 0)) {
        pcVar4 = pcVar4 + __n;
        cVar3 = *pcVar4;
        iVar6 = 0;
        if ((cVar3 != '&') && (cVar3 != '\0')) {
          iVar1 = 0;
          pcVar5 = param_3;
          do {
            if (cVar3 == '%') {
              local_28 = pcVar4[1];
              local_27 = pcVar4[2];
              local_26 = 0;
              lVar2 = strtol(&local_28,&pcStack_24,0x10);
              pcVar4 = pcVar4 + 3;
              *pcVar5 = (char)lVar2;
            }
            else if (cVar3 == '+') {
              pcVar4 = pcVar4 + 1;
              *pcVar5 = ' ';
            }
            else {
              *pcVar5 = cVar3;
              pcVar4 = pcVar4 + 1;
            }
            cVar3 = *pcVar4;
            pcVar5 = pcVar5 + 1;
            iVar1 = iVar1 + 1;
            if ((cVar3 == '&') || (iVar6 = 0xff, cVar3 == '\0')) {
              param_3[iVar1] = '\0';
              return 0;
            }
          } while (iVar1 != 0xff);
        }
        param_3[iVar6] = '\0';
        return 0;
      }
    }
    else {
      iVar6 = iVar6 + 1;
    }
    cVar3 = *pcVar4;
    pcVar5 = pcVar4 + 1;
    param_1 = pcVar4;
  } while( true );
}

Thanks to the parameter names (service_type and userdefined), we can easily find out which HTTP query is used to trigger the function fwCgiServiceEditPPPoE2() by looking at which .html file contains the values (using grep).

alt-text

Then all we have to do is find the right form within the file in question, which in our case is /www/BKS_service_edit.htm.

R6200 Multiple Command Injection in httpd (audited version: v1.0.1.56_1.0.43)

The feature that change the router password can have side effects. The function passwordCgiMain() is used to define the value http_passwd by calling function acosNvramConfig_set(). By controlling the parameter sysNewPasswd of the HTTP request, we control the value of http_passwd.

File: /usr/sbin/httpd
Function: passwordCgiMain()

undefined4 passwordCgiMain(undefined4 param_1,int param_2)

{
  bool bVar1;
  char *__s2;
  int iVar2;
  FILE *__stream;
  undefined4 uVar3;
  undefined4 uVar4;
  char local_738 [256];
  char local_638 [256];
  undefined auStack_538 [256];
  undefined auStack_438 [256];
  undefined auStack_338 [256];
  undefined auStack_238 [256];
  char local_138 [256];
  undefined *local_38;
  undefined *local_34;
  undefined *local_30;
  
  websGetVar(param_1,"sysOldPasswd",local_738);
  websGetVar(param_1,"sysNewPasswd",local_638);
  __s2 = (char *)acosNvramConfig_get("http_passwd");
  local_38 = auStack_538;
  websGetVar(param_1,"question1",local_38);
  local_30 = auStack_438;
  websGetVar(param_1,"question2",local_30);
  local_34 = auStack_338;
  websGetVar(param_1,"answer1",local_34);
  websGetVar(param_1,"answer2",auStack_238);
  websGetVar(param_1,"checkPassRec",local_138);
  if ((local_738[0] == '\0') && (local_638[0] == '\0')) {
    bVar1 = false;
    goto joined_r0x004475f0;
  }
  iVar2 = strcmp(local_738,__s2);
  if (iVar2 != 0) {
    uVar3 = stringOut("failure_head");
    uVar4 = stringOut("old_paswd_wrong");
    respCgiSendResp(uVar3,uVar4,"PWD_password.htm",param_2);
    return 0;
  }
  if (local_638[0] == '\0') {
    acosNvramConfig_set("http_passwd","");
    acosNvramConfig_save();
    iVar2 = strcmp(local_638,__s2);
    if (iVar2 != 0) goto LAB_004477a0;
LAB_004475ac:
    bVar1 = false;
  }
  else {
    acosNvramConfig_set("http_passwd",local_638);
    acosNvramConfig_save();
    iVar2 = strcmp(local_638,__s2);
    if (iVar2 == 0) goto LAB_004475ac;
LAB_004477a0:
    bVar1 = true;
    usbLoadSettings();
  }
  __stream = fopen("/tmp/opendns_auth.tbl","w");
  if (__stream != (FILE *)0x0) {
    fclose(__stream);
  }
joined_r0x004475f0:
  if (local_138[0] == '1') {
    acosNvramConfig_set("password_question1",local_38);
    acosNvramConfig_set("password_question2",local_30);
    acosNvramConfig_set("password_answer1",local_34);
    acosNvramConfig_set("password_answer2",auStack_238);
    acosNvramConfig_set("enable_password_recovery",local_138);
    acosNvramConfig_save();
  }
  else {
    acosNvramConfig_set("enable_password_recovery","0");
  }
  sendPage2Client("PWD_password.htm",param_2);
  if (bVar1) {
    close(param_2);
    usbRestartServices(1);
  }
  return 0;
}

alt-text

As an example, the function afp_write_shadow_passwd() uses function acosNvramConfig_get() to retrieve the value http_passwd. It then creates a string using this value via a call to function sprintf(). And then use the system() function (to execute a shell command) with this string as the only parameter.

File: /usr/sbin/httpd
Function: afp_write_shadow_passwd()

void afp_write_shadow_passwd(void)

{
  int iVar1;
  undefined4 uVar2;
  FILE *pFVar3;
  char *pcVar4;
  char local_218;
  undefined auStack_217 [127];
  undefined local_198;
  undefined auStack_197 [127];
  char acStack_118 [256];
  
  local_218 = '\0';
  memset(auStack_217,0,0x7f);
  local_198 = 0;
  memset(auStack_197,0,0x7f);
  iVar1 = acosNvramConfig_match("afpd_enable","0");
  if (iVar1 == 0) {
    system("rm /tmp/chgpasswd");
    uVar2 = acosNvramConfig_get("http_passwd");
    sprintf(acStack_118,"passwd admin pw %s > /tmp/chgpasswd",uVar2);
    system(acStack_118);
    pFVar3 = fopen("/tmp/chgpasswd","r");
    if (pFVar3 == (FILE *)0x0) {
      puts("/tmp/chgpasswd: no files!");
    }
    else {
      pcVar4 = fgets(&local_218,0x80,pFVar3);
      if (pcVar4 != (char *)0x0) {
        sscanf(&local_218,"%s",&local_198);
      }
      fclose(pFVar3);
      pFVar3 = fopen("/tmp/config/shadow","w+");
      if (pFVar3 != (FILE *)0x0) {
        fprintf(pFVar3,"admin:%s:10957:0:99999:7:::\n",&local_198);
        fprintf(pFVar3,"guest::10957:0:99999:7:::",&local_198);
        fclose(pFVar3);
        return;
      }
      puts("/tmp/config/shadow: no files!");
    }
  }
  return;
}

This bug pattern is also present in the function samba_writeEtcPasswd():

File: /usr/sbin/httpd
Function: samba_writeEtcPasswd()

void samba_writeEtcPasswd(void)

{
  FILE *__stream;
  undefined4 uVar1;
  size_t __size;
  char acStack_1328 [256];
  char local_1228;
  undefined auStack_1227 [511];
  char local_1028;
  undefined auStack_1027 [4095];
  
  local_1028 = '\0';
  memset(auStack_1027,0,0xfff);
  local_1228 = '\0';
  memset(auStack_1227,0,0x1ff);
  local_1028 = '\0';
  local_1228 = '\0';
  __stream = fopen("/tmp/samba/private/passwd","w+");
  if (__stream == (FILE *)0x0) {
    puts("/tmp/samba/private/passwd: no files!");
  }
  else {
    fread(&local_1028,1,0x1000,__stream);
    sprintf(&local_1228,"%s:*:0:0:%s:/:/bin/sh\r\n","nobody","nobody");
    strcat(&local_1028,&local_1228);
    uVar1 = acosNvramConfig_get("http_passwd");
    sprintf(&local_1228,"%s:%s:0:0:%s:/:/bin/sh\r\n","admin",uVar1,"admin");
    strcat(&local_1028,&local_1228);
    sprintf(&local_1228,"%s:%s:0:0:%s:/:/bin/sh\r\n","guest","guest","guest");
    strcat(&local_1028,&local_1228);
    uVar1 = acosNvramConfig_get("http_passwd");
    sprintf(acStack_1328,"/usr/local/samba/smb_pass %s %s","admin",uVar1);
    system(acStack_1328);
    sprintf(acStack_1328,"/usr/local/samba/smb_pass %s %s","guest","guest");
    system(acStack_1328);
    __size = strlen(&local_1028);
    fwrite(&local_1028,__size,1,__stream);
    fclose(__stream);
  }
  return;
}

Here are the two possible call stacks to trigger the bug.

  • USBAdvCgiMain()
    • usbRestartServices(2)
      • samba_writeEtcPasswd()
      • afp_write_shadow_passwd()
  • USBUmountCgi()
    • usbRestartServices(2)
      • samba_writeEtcPasswd()
      • afp_write_shadow_passwd()

With this bug, we may obtain persistence on the router which is useful for surviving reboot as our payload will be stored in NVRAM.