前言
特别鸣谢:chan师傅
看了chan师傅的文章之后,发现居然还有这种提取固件的好方法,正好手头上有个同款的GIGA WiFi,于是照着chan师傅的步骤把固件提了出来。
不过鉴于本人太菜了,没法绕过固件中的过滤因此没有搞到很有用的漏洞,不过倒是因为这个固件中的过滤,让我萌生了发篇文章跟师傅分享一下的想法。
固件型号:GIGA WiFi KM08-708H
固件基本信息
首先在获取固件之后对固件的文件系统先提取一下。
接着查看程序信息。
可以看到这个固件中的程序是32位mips小端的。
漏洞分析
在对goahead中的危险函数进行查看时,发现在地址0x4258c4处有个函数,这个函数会在对gofrom/mcr_connDevicePing发出POST请求是被调用。从Ghidra的反编译中可以看到,在该函数的第47行有一个leaf_doSystemVar函数,通过这个函数可以做出命令执行的操作,并且从传参上看,完整的命令是通过拼接获取的,这看上去就很有操作空间,于是,尝试去追一下local_180这个变量。
在函数第45行,有个iVar2 = compare_text(uVar1,&local_180,0x20)
的操作,从compare_text这个函数的函数名来看,貌似跟字符串的比较有关系,并且这个函数的返回值还关系到能否进入判断中去执行命令,为此我们需要到compare_text中一探究竟。
进入到反编译中的compare_text中我们可以看到并没有什么大篇幅的操作,不过出于我们的目的是找到local_180的数据流向和函数返回值,还是能一下子定位到函数的第10、11行。这两行的内容没什么好说的,就是又调用了一个函数,对三个参数进行操作,并且有个返回值给到uVar2,然后让uVar2返回。所以我们还得探究一下44e1e0这个函数。
undefined4 FUN_0044e1e0(undefined4 param_1,char *param_2,size_t param_3)
{
int iVar1;
char *pcVar2;
int iVar3;
undefined4 uVar4;
char local_2028;
char local_2027 [8191];
memset(&local_2028,0,0x2000);
iVar1 = uri_decode(param_1,&local_2028,0x2000);
if (iVar1 < 1) {
LAB_0044e420:
pcVar2 = strstr(&local_2028,"reboot");
uVar4 = 1;
if (pcVar2 == (char *)0x0) {
pcVar2 = strstr(&local_2028,"telnetd");
if (pcVar2 == (char *)0x0) {
pcVar2 = strstr(&local_2028,"massp");
if (pcVar2 == (char *)0x0) {
pcVar2 = strstr(&local_2028,"echo");
if (pcVar2 == (char *)0x0) {
pcVar2 = strstr(&local_2028,"telnet");
if (pcVar2 == (char *)0x0) {
pcVar2 = strstr(&local_2028,"ftp");
if (pcVar2 == (char *)0x0) {
pcVar2 = strstr(&local_2028,"wget");
if (pcVar2 == (char *)0x0) {
pcVar2 = strstr(&local_2028,"\\\\");
if (pcVar2 == (char *)0x0) {
pcVar2 = strstr(&local_2028,"./");
if (pcVar2 == (char *)0x0) {
pcVar2 = strstr(&local_2028,"../");
if (pcVar2 == (char *)0x0) {
pcVar2 = strstr(&local_2028,".\\");
if (pcVar2 == (char *)0x0) {
pcVar2 = strstr(&local_2028,".\\\\");
if (pcVar2 == (char *)0x0) {
pcVar2 = strstr(&local_2028,"--");
if (pcVar2 == (char *)0x0) {
uVar4 = 0;
snprintf(param_2,param_3,"%s",param_1);
}
}
}
}
}
}
}
}
}
}
}
}
}
}
else {
uVar4 = 1;
if ((((local_2028 != '\"') && (local_2028 != '\'')) && (local_2028 != '`')) &&
(((local_2028 != '|' && (local_2028 != ';')) &&
((local_2028 != '$' && ((local_2028 != '(' && (local_2028 != ')')))))))) {
iVar3 = 0;
while ((((((local_2028 != '\\' && (local_2028 != '&')) && (local_2028 != '^')) &&
((local_2028 != '!' && (local_2028 != '~')))) && (local_2028 != '@')) &&
(((local_2028 != '*' && (local_2028 != '#')) &&
(((local_2028 != '[' &&
(((local_2028 != ']' && (local_2028 != '{')) && (local_2028 != '}')))) &&
((local_2028 != ':' && (local_2028 != '='))))))))) {
if (iVar1 <= iVar3 + 1) goto LAB_0044e420;
local_2028 = local_2027[iVar3];
if ((((local_2028 == '\"') || (local_2028 == '\'')) ||
((local_2028 == '`' ||
(((local_2028 == '|' || (local_2028 == ';')) || (local_2028 == '$')))))) ||
((local_2028 == '(' || (iVar3 = iVar3 + 1, local_2028 == ')')))) break;
}
uVar4 = 1;
}
}
return uVar4;
从44e1e0这个函数的反编译中可以看到,函数中首先对param_1进行了url解码,并将解码后的内容放到了&loacl_2028中。
然后通过对iVar1的值进行判断来选择进行过滤的路径。
如果iVar1的值小于1就会对字符串中的内容进行查看,检查是否存在reboot、telnetd、massp、echo、telnet、ftp、wget等可以利用的命令。
再者如果iVar1的值大于等于1时,则对字符串中是否出现了特殊符号做出了过滤。
通过分析发现这个过滤把"
(双引号)'
(单引号)、`
(反引号)|
(管道符)、 ;
(分号)$
(美元符)(
(左括号))
(右括号)\
(反斜杠)&
(和符)^
(异或符)!
(感叹号)~
(波浪符)@
(艾特符)*
(星号)#
(井号)、[
(左方括号)]
(右方括号){
(左花括号)}
(右花括号):
(冒号)=
(等号)都过滤了,并且在会在过滤完特殊符号之后跳回命令过滤,可以说是把常用的、不常用的、用得到的和用不到的都给滤掉了,能做到这种地步,写这一段函数的人是狠啊。
看到44e1e0这个函数这么详细的过滤,那这个命令执行只能说已将凉透了,没办法去使用,所以也没有再往上看的必要了。不过既然看到这发现了有这么严密的过滤,那正好可以拿出一个较为漏风的过滤比较一下。
如图中所示,在这个目标函数中也有过滤的操作,不过相比于上面所遇到的过滤手段是稍显不足的。
v6 = (const char *)get_cgi("SystemCmd");
if ( !v6 )
v6 = "";
if ( !strchr(v6, '&')
&& !strchr(v6, ';')
&& !strchr(v6, '%')
&& !strchr(v6, '|')
&& !strchr(v6, '\n')
&& !strchr(v6, '\r') )
首先,在这个目标函数中,虽然是对&
、;
、%
、|
、\n
、\r
进行了过滤,但是并不能够有效地解决被绕过的问题。
一般情况下使用&
、;
、%
、|
就可以很容易完成命令拼接的目标,所以这几个符号作为最常见的命令执行拼接符号被开发者很敏锐的察觉并且进行了过滤。
但是其实这种过滤是没有用的,从上面的命令执行看到,对这个漏洞点的利用其实是用到了`
(反引号) 的,当然如果使用$
也是可以完成命令执行拼接的工作的。
反引号和美元符号再linux中是可以优先执行被其包起来的命令的,所以这两位的功能更是强大,也正式因为如此,这个漏风的过滤是可以绕过的。
所以这个漏洞的具体实现的手段就是通过使用`
(反引号)或$
将目标命令包起来使原本执行的ping被拼接成
ping `cat ../tmp/syslog.log>exp.asp`
让其先执行作为外部命令的参数,接着外部的命令才会执行,但是让目标命令能够执行就已经是我们的目的了,它原本指定的是什么指令有没有正常执行都跟我们没有关系了。
其次,上面这个目标函数要是对传入的指令字符串进行了字符过滤也是可以的,就比如对字符串中是否存在其他指令进行查看,如果存在的话就进行报错,就像44e1e0函数中做的一样。
//过滤"reboot"防止被系统重启,造成服务中断。
pcVar2 = strstr(&local_2028, "reboot");
uVar4 = 1; // 默认设置 uVar4 为 1
// 如果未找到 "reboot"
if (pcVar2 == (char *)0x0) {
//过滤"telnetd"防止被启动 Telnet 服务器,导致未授权访问。
pcVar2 = strstr(&local_2028, "telnetd");
if (pcVar2 == (char *)0x0) {
//过滤"massp"
pcVar2 = strstr(&local_2028, "massp");
if (pcVar2 == (char *)0x0) {
//过滤"echo"防止泄露敏感信息。
pcVar2 = strstr(&local_2028, "echo");
if (pcVar2 == (char *)0x0) {
//过滤"telnet"从而禁止远程登录。
pcVar2 = strstr(&local_2028, "telnet");
if (pcVar2 == (char *)0x0) {
//过滤"ftp"防止不安全的文件传输。
pcVar2 = strstr(&local_2028, "ftp");
if (pcVar2 == (char *)0x0) {
//过滤"wget"预防下载恶意文件,执行不安全操作。
pcVar2 = strstr(&local_2028, "wget");
if (pcVar2 == (char *)0x0) {
//过滤"\\"防止被用于路径遍历攻击。
pcVar2 = strstr(&local_2028, "\\\\");
if (pcVar2 == (char *)0x0) {
//过滤"./"防止被执行当前目录下的文件。
pcVar2 = strstr(&local_2028, "./");
if (pcVar2 == (char *)0x0) {
//过滤"../"禁止路径遍历。
pcVar2 = strstr(&local_2028, "../");
if (pcVar2 == (char *)0x0) {
//过滤".\\"同样用于防止执行当前目录下的文件。
pcVar2 = strstr(&local_2028, ".\\");
if (pcVar2 == (char *)0x0) {
//过滤".\\"防止再次可执行当前目录下的程序。
pcVar2 = strstr(&local_2028, ".\\\\");
if (pcVar2 == (char *)0x0) {
//过滤"--"防止被用于可能的命令注入攻击。
pcVar2 = strstr(&local_2028, "--");
在44e1e0函数中我们可以清楚地看到它过滤了reboot、telnetd、massp、echo、telnet、ftp、wget,就连目录都不让更换。所以就算没有过滤`
(反引号)和$
,如果对接收到的指令字符串有一定的过滤手段的话,也是可以一定程度避免被任意命令执行的。
总结
通过对goahead中过滤方法的分析以及对比,其实我们能发现,如果要对命令注入进行防御的话,进行严密的过滤手段当然是不二之选。使用过滤将程序运行时不该出现的各种命令字符或特殊符号,隔绝其出现在命令执行出的可能,从而实现对命令注入的防御。