[Union-CTF]misc

shr1mp 发布于 21 天前 65 次阅读


Welcome

略(扫二维码关注公众号领取)

总裁四比特,这能玩?

题目名字叫做4比特,特地上网搜了下有没有相关的知识点

知识点:4-bit(半字节/nibble)操作

4-bit(半字节/nibble):
    1个字节(byte)= 8位(bits)= 2个半字节(nibbles)
    高4位(高半字节):bit7-bit4
    低4位(低半字节):bit3-bit0
    十六进制表示:每个半字节正好对应1位十六进制数字(0-F)
#python里的处理方法:(适合新手体质的解读)
#-----------------------------------------------------------
#首先对于|的用法:
#整数:将两个数字的二进制位进行比较,只要有一个为 1,结果位就为 1。
#字典:将两个字典合并成一个新字典。如果键重复,则后一个字典的值会覆盖前一个。
"""
例子:
dict1 = {'a': 1, 'b': 2}
dict2 = {'b': 99, 'c': 3}  # 键 'b' 重复了
merged_dict = dict1 | dict2
print(merged_dict)  # 输出: {'a': 1, 'b': 99, 'c': 3}
"""
#-----------------------------------------------------------------
#第一步:
#这里读取储存二进制数据,'rb'即读取二进制的模式
with open('文件', 'rb') as q:
    data = q.read()
#第二步:
#先创建字典K,将每高位节的四个二进制(半字节/nibble)保存成字典中的一个项目直到遍历整个data
K = [b >> 4 for b in data]              ###    python的”推导式“格式:[核心操作 for 循环]
                                        ###    b是一个字节(8个二进制),将这个字节二进制右移4位,即只存高位节
#第三步:
#将两个高位节拼在一起并赋值a                
a = bytes((K[i] << 4) | K[i+1]            ###一个典型的位运算操作,即把两个高位节拼在一起形成一个字节
                 for i in range(0, len(K)-1, 2))          
###tips1:空四个缩进大格是python的特殊表达方式,意味着内容的延续,这样方便观察代码
###tips2:每次操作后产生的字节会储存在bytes()等到所有循环结束,bytes()会根据接收到的数据一次性产生二进制值

首先给了JPG文件,我先尝试了foremost,发现文件末尾有两个89 50 4E 47 0D 0A 1A 0A结尾的PNG,先单独取出来,发现这两个图片是一样的,仅内容和大小,而且也没有盲水印。

后面手动分析了下,发现这个文件的结构是这样的:

JPG
中间有段莫名其妙的字符
PNG
PNG

我尝试将PNG和中间的字符单独提出来,发现两个文件内存大小是一模一样的:

又结合题目又4-bit,说明这是4-bit(半字节/nibble)操作。再分析发现:

中间的数据和png的16进制文件:

低 4 位完全相同

高 4 位不同
mid
png

关键:将 mid数据的**高 4 位**提取出来,每两个 nibble 组合成一个完整字节,得到一个 ZIP 文件!

先将mid的高四位的16进制数据粗略看下,每两个 nibble 组合成一个完整字节,发现组合起来是是50 4B 03 04,这是zip的文件头

import io
import zipfile
# 读取文件,我的文件名字就叫mid,然后最后的zip名字叫1.zip
with open('mid', 'rb') as q:
    data1 = q.read()
# 提取 mid的高 4 位
mid_high_nibbles = [b >> 4 for b in data1]
# 每两个 nibble 组合成一个字节
zip_data = bytes((mid_high_nibbles[i] << 4) | mid_high_nibbles[i+1] 
                 for i in range(0, len(mid_high_nibbles)-1, 2))
with open('1.zip', 'wb') as f:  
    f.write(zip_data)

分析这个完整的png:

后面还藏了一个zip文件,这个里面带flag.txt

提出来,解压得到:

UniCTF{Y0u_4r3_4_6r347_h4ck3r_!}

工厂应急流量分析

Silent Resolver

看看我藏了什么

拿到题目只给了一个 traffic.pcapng。我第一反应就是先在包里搜一搜关键字(`flag` / `ctf` / `{}` 之类),因为很多 Misc 就藏在明文里。

这份包不大(几十 KB),但直接扫 payload 并没有出现 `flag` / `CTF{` 这种直给的字符串,不过能看到一些 DNS 请求里带了 `unictf` 相关域名。这些是可疑的流量

只看的带unictf的流量

frame contains "exfil"
0000.<很长一串>.a1b2c3d4.exfil.unictf.local
0001.<很长一串>.a1b2c3d4.exfil.unictf.local
...
ffff.1818be0b.a1b2c3d4.exfil.unictf.local

几个明显的信号:

- `exfil` 这种前缀基本就是“我在外带数据”;
- 第一段是 `0000 / 0001 / ...`,像分片序号;
- 中间那坨字符只包含 `a-z` 和 `2-7`(夹杂少量数字),尝试复制一组进cyberchef,提示是Base32;
- 最后一个 `ffff.1818be0b` 看着像结束标记 + 校验值(十六进制 8 位)。

把这些很长的字符全部拿到一个文件里

Sign in

拿到附件是个很小的 zip(200B)

解压出来的文件很小,没有啥用,看看压缩包的hex编码,发现在结尾标识后还有一串ascll代码

U2VjcmV0S2V5

查找资料发现这是zip comment(压缩包注释)

用7z打开验证下,发现确实是注释

Base64解码:

SecretKey

接下来关于压缩包里面的Serpent.dat 是什么

**Serpent 分组密码**(128-bit block)的 ECB 两个 block。

密码学知识点:

Serpent算法加密一个128位数据块的过程,可以简化为32轮重复操作,每轮做三件事:

    混入密钥:把数据跟这一轮的密钥做异或
    S盒替换:把数据分成32个小段,每段4位,用固定的替换表(S盒)换掉
    线性变换:对数据做移位、异或等操作,让数据充分混合

最后再加一轮密钥,输出密文。
更形象的比喻:就像把数据块(128位)放进一个32层的加工流水线,每层都加一次密钥、换一次零件(S盒)、搅匀一次(线性变换),最后出来就是加密后的密文。

- 注释里给的只是 `SecretKey`(9 字节),但实际加密通常会喂给 cipher 一个固定长度 key(比如 16 字节)。
- 这题的正确做法是:`SecretKey` **右侧用 `\x00` 补到 16 字节** 作为输入 key。
解出来的明文末尾还有几个 `\x00`,直接 `rstrip(b"\x00")` 就行。

解出来

UniCTF{Serpentine_Secrets}

ai生成的解码:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import base64
import struct
import zipfile
# ---------------- Serpent primitive ----------------
SBOX = [
    [3, 8, 15, 1, 10, 6, 5, 11, 14, 13, 4, 2, 7, 0, 9, 12],
    [15, 12, 2, 7, 9, 0, 5, 10, 1, 11, 14, 8, 6, 13, 3, 4],
    [8, 6, 7, 9, 3, 12, 10, 15, 13, 1, 14, 4, 0, 11, 5, 2],
    [0, 15, 11, 8, 12, 9, 6, 3, 13, 1, 2, 4, 10, 7, 5, 14],
    [1, 15, 8, 3, 12, 0, 11, 6, 2, 5, 4, 10, 9, 14, 7, 13],
    [15, 5, 2, 11, 4, 10, 9, 12, 0, 3, 14, 8, 13, 6, 7, 1],
    [7, 2, 12, 5, 8, 4, 6, 11, 14, 9, 1, 15, 13, 3, 10, 0],
    [1, 13, 15, 0, 14, 8, 2, 11, 7, 4, 12, 10, 9, 3, 5, 6],
]
INV_SBOX = []
for sb in SBOX:
    inv = [0] * 16
    for i, v in enumerate(sb):
        inv[v] = i
    INV_SBOX.append(inv)
MASK32 = 0xFFFFFFFF
PHI = 0x9E3779B9
def rotl32(x: int, r: int) -> int:
    x &= MASK32
    return ((x << r) | (x >> (32 - r))) & MASK32
def rotr32(x: int, r: int) -> int:
    x &= MASK32
    return ((x >> r) | (x << (32 - r))) & MASK32
def sbox_bitslice(sb, w0: int, w1: int, w2: int, w3: int):
    """Apply a 4-bit S-box in bitslice form on 4x32-bit words."""
    y0 = y1 = y2 = y3 = 0
    for b in range(32):
        nib = ((w0 >> b) & 1) | (((w1 >> b) & 1) << 1) | (((w2 >> b) & 1) << 2) | (((w3 >> b) & 1) << 3)
        out = sb[nib]
        if out & 1:
            y0 |= 1 << b
        if out & 2:
            y1 |= 1 << b
        if out & 4:
            y2 |= 1 << b
        if out & 8:
            y3 |= 1 << b
    return y0 & MASK32, y1 & MASK32, y2 & MASK32, y3 & MASK32
def lt(w0: int, w1: int, w2: int, w3: int):
    w0 = rotl32(w0, 13)
    w2 = rotl32(w2, 3)
    w1 = (w1 ^ w0 ^ w2) & MASK32
    w3 = (w3 ^ w2 ^ ((w0 << 3) & MASK32)) & MASK32
    w1 = rotl32(w1, 1)
    w3 = rotl32(w3, 7)
    w0 = (w0 ^ w1 ^ w3) & MASK32
    w2 = (w2 ^ w3 ^ ((w1 << 7) & MASK32)) & MASK32
    w0 = rotl32(w0, 5)
    w2 = rotl32(w2, 22)
    return w0, w1, w2, w3
def inv_lt(w0: int, w1: int, w2: int, w3: int):
    w2 = rotr32(w2, 22)
    w0 = rotr32(w0, 5)
    w2 = (w2 ^ w3 ^ ((w1 << 7) & MASK32)) & MASK32
    w0 = (w0 ^ w1 ^ w3) & MASK32
    w3 = rotr32(w3, 7)
    w1 = rotr32(w1, 1)
    w3 = (w3 ^ w2 ^ ((w0 << 3) & MASK32)) & MASK32
    w1 = (w1 ^ w0 ^ w2) & MASK32
    w2 = rotr32(w2, 3)
    w0 = rotr32(w0, 13)
    return w0, w1, w2, w3
def key_schedule(user_key: bytes):
    """Serpent key schedule (like common reference impl):
    - accepts 16/24/32 bytes key, then pads with a single 1-bit (implemented as byte 0x01) and zeros to 32 bytes.
    """
    if len(user_key) > 32:
        raise ValueError("key too long")
    if len(user_key) < 32:
        user_key = user_key + b"\x01" + b"\x00" * (31 - len(user_key))
    w = list(struct.unpack("<8I", user_key))
    for i in range(8, 140):
        t = w[i - 8] ^ w[i - 5] ^ w[i - 3] ^ w[i - 1] ^ PHI ^ (i - 8)
        w.append(rotl32(t, 11))
    keys = []
    for i in range(33):
        a, b, c, d = w[4 * i + 8], w[4 * i + 9], w[4 * i + 10], w[4 * i + 11]
        # key schedule uses S_(3-i)
        a, b, c, d = sbox_bitslice(SBOX[(3 - i) % 8], a, b, c, d)
        keys.append((a, b, c, d))
    return keys
def decrypt_block(block16: bytes, round_keys):
    w0, w1, w2, w3 = struct.unpack("<4I", block16)
    # last round: xor K32, invS7, xor K31
    k32 = round_keys[32]
    w0 ^= k32[0]
    w1 ^= k32[1]
    w2 ^= k32[2]
    w3 ^= k32[3]
    w0, w1, w2, w3 = sbox_bitslice(INV_SBOX[7], w0, w1, w2, w3)
    k31 = round_keys[31]
    w0 ^= k31[0]
    w1 ^= k31[1]
    w2 ^= k31[2]
    w3 ^= k31[3]
    for r in range(30, -1, -1):
        w0, w1, w2, w3 = inv_lt(w0, w1, w2, w3)
        w0, w1, w2, w3 = sbox_bitslice(INV_SBOX[r % 8], w0, w1, w2, w3)
        kr = round_keys[r]
        w0 ^= kr[0]
        w1 ^= kr[1]
        w2 ^= kr[2]
        w3 ^= kr[3]
    return struct.pack("<4I", w0 & MASK32, w1 & MASK32, w2 & MASK32, w3 & MASK32)
# ---------------- challenge logic ----------------
def main(zip_path: str):
    with zipfile.ZipFile(zip_path, "r") as z:
        # zip comment -> base64 -> b"SecretKey"
        raw = z.comment.strip()
        key_str = base64.b64decode(raw)
        data = z.read("Serpent.dat")
    # 关键点:题目这里是把 key 补到 16 字节再喂给 cipher
    user_key = key_str.ljust(16, b"\x00")
    rk = key_schedule(user_key)
    # ECB (2 blocks)
    pt = decrypt_block(data[:16], rk) + decrypt_block(data[16:32], rk)
    pt = pt.rstrip(b"\x00")
    print(pt.decode())
if __name__ == "__main__":
    import sys
    zip_path = sys.argv[1] if len(sys.argv) > 1 else "attachment (1).zip"
    main(zip_path)

最终输入nc端口,拿到最终的flag

截取的线索

7文件里面是:

RinDSA|W6dlkbXsob

下面的PNG是

这个不像是摩斯密码,有长短也有中

**白(255)=1,黑(0)=0** 转成 bit 流,然后每 8 个 bit 组一个 byte。

def decode_scanline_png(png_path: str) -> str:
    img = Image.open(png_path).convert("L")
    w, h = img.size
    assert h == 1, f"expected 1px height, got {img.size}"
    pixels = list(img.getdata())
    # 白(255)=1,黑(0)=0
    bits = [1 if p > 127 else 0 for p in pixels]
    assert len(bits) % 8 == 0, "bit length should be multiple of 8"
    out = []
    for i in range(0, len(bits), 8):
        b = 0
        for j in range(8):
            b = (b << 1) | bits[i + j]
        out.append(b)
    return bytes(out).decode("ascii")

得出来:

_Great_to01}

而第一个文件我思考了很久

因为png给了个flag后半部分,所以这边尽量往前半部分靠

解出来

UniCTF{P1ckle_the_Great_to01}

BlueBreath(哥斯拉webshell)

巨兽并不会凭空消失,它只是换了一种频率在呼吸

挑战这个题

先初步看一遍,大量对 `172.30.96.1:8000` 的请求都在 404(像是目录扫描),但其中有少数返回 200

http.response.code == 200

找到这些异常的http回复OK的流

流5774:

其中 /uploads/shell.php更离谱:请求是 POST,Content-Type: application/octet-stream,body 不是表单而是一坨二进制,看着就像 WebShell 管理工具的通信数据。

另外还能看到一次 OST /index.php`的 multipart 上传(文件名 `hint.zip`),里面有个hint.png。强行提取出来利用png文件头进行明文攻击

bkcrack.exe -C 1.zip -c hint.png -p png_header

得到zip加密密钥:

24cc690a 59f6451b e4fbbab0

再用密钥继续开出png:

./bkcrack.exe -C 1.zip -c hint.png -k 24cc690a 59f6451b e4fbbab0 -d hint.png

再观察这个图片,基本的注释,文件杂糅的隐写都没有,尝试LSB隐写

(注意到这里,图片人眼看到的只有黑色白色,而这些像素点又有别的颜色,而127是255的二进制储存第一位由1改为0.说明是LSB隐写的通道7:)

用stegsolve看,在通道7的时候左上角会出现黑点点

将Ahiz_2026进行md5处理,得到key的MD5前16位

2dc3ef5ff0c67015

对/uploads/shell.php` 的连接做 Follow TCP Stream,会发现:

第一次连接里有一个超大的 POST(几万字节),服务端回包里下发 `Set-Cookie: PHPSESSID=...
 后续几次连接都带着相同的 `PHPSESSID`,POST 的 body 较短
第一次把 payload 注入到 session 里,之后都靠 cookie 维持状态。
麻烦在于:body 是加密/混淆后的,需要把它还原出来

得到了key,再用xor进行解密

第一个post前面几行:

`$parameters=array();
$_SES=array();
function run($pms)
{ 
...`

说XOR 之后的明文还被 gzip 压缩过,再 gzip.decompress() 才能看到真正内容。

解出来的请求就很直白了,例如:

- methodName ... test 
- methodName ... getBasicsInfo ; 回系统信息(当前目录、uname、php 版本等)
- methodName ... execCommand + `cmdLine ...` ; 直接执行系统命令

其中还有一条:

sh -c "cd "/var/www/html/uploads/";cat flag.txt" 

对应响应解 gzip 后直接得到 flag

UniCTF{w1reSha3k_easy_or_hard}
这个人很菜,但是在学
最后更新于 2026-02-06