昨天和前天参加了2025年Solar应急响应9月月赛,整体来说也算是挺好玩的。
特洛伊挖矿木马事件排查
任务一
- 任务名称: 提交挖矿文件的绝对路径
- 任务分数: 100.00
- 任务类型: 静态Flag
- 要求: 提交挖矿文件的绝对路径,最终以
flag{/xxx/xxx}格式提交 - 排查过程:
ps aux看到flag{/tmp/kworkerd}
任务二
- 任务名称: 提交挖矿文件的外联IP与端口
- 任务分数: 100.00
- 任务类型: 静态Flag
- 要求: 提交挖矿文件的外联的IP与端口,最终以
flag{ip:port}格式提交 - 排查过程:
netstat -anoflag{104.21.6.99:10235}
任务三
- 任务名称: 守护进程脚本的绝对路径
- 任务分数: 100.00
- 任务类型: 静态Flag
- 要求: 停止挖矿进程并尝试删除挖矿程序,根据异常判断,提交守护进程脚本的绝对路径,最终以
flag{/xxx/xxx/xxx/xxx}提交 - 排查过程:
尝试rm删除,但发现还在,也没有报错:
Kill掉进程,进程会被重新拉起,排查计划任务:flag{/usr/bin/.0guardian}
任务四
- 任务名称: 根据出现的异常及守护进程脚本,继续排查,以人为本,使用环境内浏览器访问:
http://chat.internal-dev.net:8081获取可疑网址 - 要求: 最终以
flag{http://www.example.com}格式提交 - 结果:
flag{http://www.superlog-pro.com}
任务五
- 任务名称: 分析病毒文件,提交其感染的所有程序
- 要求: 最终以
flag{md5(/usr/bin/whoai,/usr/bin/ls,/usr/bin/top)}进行提交,顺序需以病毒文件中为准 - 排查过程:
在靶机访问http://www.superlog-pro.com,下载,用文本打开:/bin/ls,/bin/ps,/bin/cat,/bin/rm,/bin/ss,/usr/bin/stat,/usr/bin/top,/usr/bin/wget,/usr/bin/curl,/usr/bin/vi,/usr/bin/sudoflag{dac48e98a53b81b0218e2156e364f7ba}
任务六
- 任务名称: 修复系统并恢复文件完整性
- 要求: 已知所有程序被感染,当前系统属于断网状态,所以作者贴心的在
/deb_final目录下存放了对应程序的deb包,请尝试恢复所有程序,恢复完毕后在/var/flag/1文件获取flag - 结果:
flag{e510c5fca680b1b4bd5c9d8d6b3f4bdc}
任务七
- 任务名称: 最终清理
- 要求: 删除挖矿程序、删除计划任务及守护进程及清除相关进程,等待片刻在
/var/flag/2获取flag - 排查过程:
在任务六修复后,就可以正常删除了,如果不修复是无法删除的,kill掉进程,删除守护进程/usr/bin/.0guardian,删除计划任务,删除/tmp下的木马:flag{081ce3688c6cd6e2946125081381087c}
Misc
流量分析题
在流量里找到多次 POST /shell.php、POST /shell1.php 的请求,参数形如 pass=eval(base64_decode(strrev(urldecode('...'))))。把 urldecode → 反转 strrev → base64 解出 WebShell 的 PHP 核心。
两个壳的配置分别是:
/shell.php:$pass='password',$key='5f4dcc3b5aa765d6'/shell1.php:$pass='1qaz2wsx3edc',$key='0eff44c362b13fa2'
壳的返回体被 md5($pass.$key) 的前 16/后 16 个 hex 包裹,中间是 base64( XOR(gzip(明文), key) )。
以 /shell1.php 为例,先算 md5('1qaz2wsx3edc0eff44c362b13fa2') = 2abd2b6d922769ccbb57d9b8561ce2a5,据此从响应里截出中间那段 base64。
解码:base64 → 与 ASCII key(0eff44c362b13fa2)按 ($i+1)&15 取位逐字节 XOR → gunzip,得到明文,其中一段就是 flag{ccebdb78-4b5c-4252-b20a-0039913c5c94}
解题脚本
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
一键从 pcapng 中还原 CTF flag(适用于含 HTTP WebShell 通信的场景)
依赖: pyshark (并且系统需安装 tshark)
pip install pyshark
Linux/Win 请确保 tshark 可用(可在 Wireshark 官网上获取)
用法:
python extract_flag_from_pcap.py /path/to/flag.pcapng
"""
import sys
import re
import base64
import binascii
import gzip
from urllib.parse import unquote_plus
from hashlib import md5
try:
import pyshark
except Exception as e:
print("缺少 pyshark。请先安装: pip install pyshark,并保证系统安装了 tshark。")
raise
def try_urldecode_rev_b64(s):
"""尝试还原:urldecode -> strrev -> base64decode"""
try:
u = unquote_plus(s)
rev = u[::-1]
# 有的壳把单引号/双引号包起来,去掉外层引号
rev = rev.strip('\'"')
# base64 decode
b = base64.b64decode(rev)
return b
except Exception:
return None
def extract_key_pass_from_php(php_bytes):
"""尝试从 php 代码中提取 $key 和 $pass 等常见写法,返回 (pass, key) 或 (None,None)"""
try:
s = php_bytes.decode('utf-8', errors='ignore')
except:
s = str(php_bytes)
# 常见: $pass='password'; $key='0123456789abcdef';
m_pass = re.search(r"\$pass\s*=\s*['\"]([^'\"]{4,64})['\"]", s)
m_key = re.search(r"\$key\s*=\s*['\"]([^'\"]{4,128})['\"]", s)
passv = m_pass.group(1) if m_pass else None
keyv = m_key.group(1) if m_key else None
return passv, keyv
def try_dewrap_and_decode(resp_body_bytes, passv=None, keyv=None):
"""
根据我们分析的包装格式尝试解包:
形态假设: <md5_left(32hex)><base64_middle><md5_right(32hex)>
md5_left/md5_right 大概率是 md5(pass+key) 的前/后 16 hex 或相等的两段。
解包策略:
- 正则找出 32hex + base64 + 32hex
- 若已知 pass/key:校验 md5(pass+key)
- base64 解码 -> XOR with key bytes (多种尝试) -> gunzip -> search flag
返回解出的明文(若成功)或 None
"""
try:
s = resp_body_bytes.decode('utf-8', errors='ignore').strip()
except:
s = ''
# 找 32hex + base64 + 32hex (base64 中可能含 =)
m = re.search(r"^([0-9a-fA-F]{32})([A-Za-z0-9+/=\n\r]+?)([0-9a-fA-F]{32})$", s, re.S)
if not m:
# 也尝试中间没有 32hex 的变体:有时是 md5左右各16hex(同样长度)
m2 = re.search(r"([0-9a-fA-F]{16})([A-Za-z0-9+/=\n\r]+?)([0-9a-fA-F]{16})", s, re.S)
if m2:
left, mid, right = m2.group(1), m2.group(2), m2.group(3)
# pad to 32 by duplicating? but we'll proceed
else:
return None
else:
left, mid, right = m.group(1), m.group(2), m.group(3)
mid = mid.replace('\r', '').replace('\n', '')
# 如果已知 pass/key,优先校验
if passv and keyv:
try:
mm = md5((passv + keyv).encode()).hexdigest()
# 检查是否匹配左/右其中一部分
if not (left.lower().startswith(mm[:len(left)]) or right.lower().endswith(mm[-len(right):]) \
or left.lower()==mm or right.lower()==mm):
# md5 不匹配也不一定失败,可能使用前/后16等。继续尝试,但打印提示
pass
except Exception:
pass
# base64 decode 中段
try:
mid_bytes = base64.b64decode(mid)
except Exception:
return None
# key 可能是 hex string(长度偶数)也可能是 ascii
candidate_key_bytes = []
if keyv:
# 尝试 hex -> bytes
try:
if re.fullmatch(r"[0-9a-fA-F]+", keyv) and len(keyv) % 2 == 0:
candidate_key_bytes.append(bytes.fromhex(keyv))
except Exception:
pass
# 尝试直接 ascii
candidate_key_bytes.append(keyv.encode('utf-8', errors='ignore'))
# 如果没有 key,从 mid_bytes 里推断一个可能的短 key (比如 16 字节)
if not candidate_key_bytes:
# 试几个常见长度 8,16
for L in (16, 8, 12, 32):
if len(mid_bytes) >= L:
candidate_key_bytes.append(mid_bytes[:L])
# 对每种 candidate key 做多种 XOR 尝试(包括描述里的 (i+1)&15 索引策略)
for kb in candidate_key_bytes:
for mode in ('simple_repeat', 'offset_and_mask'):
try:
if mode == 'simple_repeat':
# 直接循环 xor
plain = bytes([b ^ kb[i % len(kb)] for i, b in enumerate(mid_bytes)])
else:
# 按描述尝试:索引为 (i+1) & (len(kb)-1) 或 &15
mask = len(kb)-1 if (len(kb) & (len(kb)-1)) == 0 else 15
plain = bytes([b ^ kb[(i+1) & mask] for i, b in enumerate(mid_bytes)])
except Exception:
continue
# 尝试 gunzip 解压(很多壳会 gzip)
try:
dec = gzip.decompress(plain)
try:
dec_text = dec.decode('utf-8', errors='ignore')
except:
dec_text = str(dec)
# 查找 flag 模式
fm = re.search(r"flag\{[0-9a-fA-F\-]+\}", dec_text, re.I)
if fm:
return dec_text
# 如果没有 flag,但是可读文本,也返回(供人工检查)
if len(dec_text) > 10 and any(c.isalpha() for c in dec_text):
# 仍然返回,调用者会打印出来
return dec_text
except Exception:
# 不是 gzip,尝试直接看 plain 是否有 flag
try:
txt = plain.decode('utf-8', errors='ignore')
except:
txt = None
if txt:
fm = re.search(r"flag\{[0-9a-fA-F\-]+\}", txt, re.I)
if fm:
return txt
return None
def main(pcap_path):
print("[*] 使用 pyshark 解析 pcapng(需要 tshark),可能稍慢...")
cap = pyshark.FileCapture(pcap_path, display_filter='http')
# 按 tcp.stream 分组:收集 POST payloads 和 HTTP responses(按 stream id)
streams = {} # stream_id -> {'posts': [rawstr], 'resps': [rawbytes], 'phps': [bytes]}
pkt_count = 0
for pkt in cap:
pkt_count += 1
try:
# stream index (tcp.stream) 用来绑定请求-响应
tcp_stream = None
if 'TCP' in pkt:
if hasattr(pkt.tcp, 'stream'):
tcp_stream = int(pkt.tcp.stream)
# 仅处理 http 层
if 'HTTP' not in pkt:
continue
# POST 请求体
if hasattr(pkt.http, 'request_method') and pkt.http.request_method == 'POST':
body = None
# pyshark 常见字段: http.file_data 或 http.request_body
if hasattr(pkt.http, 'file_data'):
body = bytes(pkt.http.file_data.binary_value)
elif hasattr(pkt.http, 'request_body'):
# text 字段
try:
body = pkt.http.request_body.binary_value
except:
try:
body = pkt.http.request_body.get_default_value().encode()
except:
body = str(pkt.http.request_body).encode()
else:
# 可能部分分片,忽略
pass
if body:
rec = streams.setdefault(tcp_stream, {'posts': [], 'resps': [], 'phps': []})
rec['posts'].append(body)
# 试着在 body 中寻找被 urlencoded 后并反转 base64 的 php 段(根据你以前的描述)
try:
s = body.decode('utf-8', errors='ignore')
# 找形如 urldecode(...strrev(...base64...))
candidate = None
# 直接取 body 全部尝试 urldecode->rev->b64
cand = try_urldecode_rev_b64(s)
if cand:
rec['phps'].append(cand)
except Exception:
pass
# RESPONSE body
if hasattr(pkt.http, 'response_code'):
body = None
if hasattr(pkt.http, 'file_data'):
body = bytes(pkt.http.file_data.binary_value)
elif hasattr(pkt.http, 'response_for_uri'):
# 备用
pass
if body:
rec = streams.setdefault(tcp_stream, {'posts': [], 'resps': [], 'phps': []})
rec['resps'].append(body)
except Exception as e:
# 忽略单包解析异常,但记录
# print("包解析错误:", e)
pass
cap.close()
print(f"[*] 解析完成,共读取包约: {pkt_count}。发现 TCP streams: {len(streams)}")
# 尝试对每个 stream 做还原
found_flags = []
for sid, rec in streams.items():
print(f"--- stream {sid} posts:{len(rec['posts'])} resps:{len(rec['resps'])} phps_candidates:{len(rec['phps'])}")
# 从 php candidates 中尝试提取 pass/key
candidates = []
for php in rec['phps']:
passv, keyv = extract_key_pass_from_php(php)
if passv or keyv:
candidates.append((passv, keyv, php))
# 若没直接解到 php,可尝试解析每个 post body as urlencoded key=value 扫描其中的值再试解
if not candidates and rec['posts']:
for body in rec['posts']:
try:
s = body.decode('utf-8', errors='ignore')
except:
s = ''
# 尝试取第一个 urlencoded 字段的值并尝试还原
for kv in re.split(r'&', s):
if '=' in kv:
k, v = kv.split('=', 1)
b = try_urldecode_rev_b64(v)
if b:
passv, keyv = extract_key_pass_from_php(b)
if passv or keyv:
candidates.append((passv, keyv, b))
# 最后,如果没有候选但有 posts/resps,也尝试通用 key 尝试(不会从 php 获取 key)
if not candidates:
candidates.append((None, None, None))
# 对每个 response 以及每组 candidate 尝试还原
for passv, keyv, phpbytes in candidates:
print(f" 尝试候选 pass={passv} key={keyv} (php len {len(phpbytes) if phpbytes else 0})")
for resp in rec['resps']:
got = try_dewrap_and_decode(resp, passv, keyv)
if got:
print(f"[+] 在 stream {sid} 成功解出明文 / flag 片段:\n{got}\n")
fm = re.search(r"flag\{[0-9a-fA-F\-]+\}", got, re.I)
if fm:
found_flags.append(fm.group(0))
else:
# 若明文中没有 flag,但内容有价值,打印并继续
pass
if found_flags:
print("[*] 找到以下 flag:")
for f in set(found_flags):
print(f" {f}")
else:
print("[!] 没有自动定位到 flag。")
if __name__ == "__main__":
if len(sys.argv) != 2:
print("用法: python extract_flag_from_pcap.py /path/to/flag.pcapng")
sys.exit(1)
pcap = sys.argv[1]
main(pcap)Reverse
解题过程
直接IDA分析:
结构:
- 先写入 "ZX_LOCK"(7 字节)。
- 写入 1 字节:原始长度是否为奇数(
(len & 1))。若为 1,则在内存里补 1 个0x00使长度变偶数。 - 调
int 1Ah取时钟滴答(CX:DX),把 DX 存到word_1022A,随后也写入到文件(2 字节 little-endian)。 - 然后把缓冲区按 16 位小端逐字处理,并写入。
核心加密循环(loc_100A3):
mov ax, [bx+si] ; 取 16 位字
add di, 0FADEh ; di = di + 0xFADE(每个字都会累加)
...
; 一堆与/非 运算,化简就是: ax = ax XOR di
mov [bx+si], ax这段看似复杂,其实等价于 "ax ^= di"。验证该布尔表达式恒等于 XOR。因此解密流程就是反向过程(仍然是 XOR):
- 从文件取 seed =
*(uint16_le)(紧跟在那个 1 字节奇偶标记之后)。 - 依次遍历密文每个 16 位字:
seed = (seed + 0xFADE) & 0xFFFF; word ^= seed。 - 如果奇偶标记是 1,最后丢弃 1 个字节(因为原文被补了一个 0 使之变偶)。
得到 flag: flag{D0s_L0ck3r_WitH_n4Nd_ExpRs!|solarsec_202509}
解题脚本
from pathlib import Path
data = Path("FLAG.ENC").read_bytes()
assert data.startswith(b"ZX_LOCK")
odd = data[7] # 0/1:原文是否奇数长度
seed = int.from_bytes(data[8:10], "little")
enc = data[10:]
assert len(enc) % 2 == 0
out = bytearray()
di = seed
for i in range(0, len(enc), 2):
di = (di + 0xFADE) & 0xFFFF
w = int.from_bytes(enc[i:i+2], "little")
w ^= di
out += w.to_bytes(2, "little")
if odd & 1:
out = out[:-1]
print(out.decode("utf-8", errors="ignore"))
京公网安备11010802044340号