PolarCTF网络安全2025春季个人挑战赛
1.签到题
关注公众号发送flagflag
题目提示是双写,,呃呃谁知道在这里双写,出题人在群里被群嘲了
2.find
附件是xlsx文件,打开注意到有的格子加粗了,有的没有,官方wp说直接联想到二维码。。额可能老手的经验吧,然后ctrl+h替换加粗的格子为黑色,不过我不是这么做的
众所周知,xlsx和doc都是压缩包,改成zip,解压出sheet1.xml
审计代码,写脚本将sheetdata里提到的s=2的单元格填充成黑色
不知道为什么代码只能把格子涂成白色。。。改了一下,在 s=”2″ 的单元格填入数字 111
import xml.etree.ElementTree as ET
from openpyxl import load_workbook
# 读取 XML 文件
try:
with open('cg1.txt', 'r', encoding='utf-8') as f:
xml_string = f.read()
except FileNotFoundError:
print("错误:找不到 sheet.xml 文件,请确保文件存在。")
exit()
# 解析 XML
root = ET.fromstring(xml_string)
# 定义命名空间
ns = {'ns': 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'}
# 查找 <sheetData> 中的所有 <c> 元素并提取 r 和 s 属性
sheet_data = root.find('.//ns:sheetData', namespaces=ns)
if sheet_data is not None:
cells = sheet_data.findall('.//ns:c', namespaces=ns)
cell_data = [(cell.get('r'), cell.get('s')) for cell in cells if cell.get('r') and cell.get('s')]
else:
print("错误:XML 中未找到 sheetData 元素。")
exit()
# 打开 Excel 文件
try:
wb = load_workbook('flag.xlsx')
except FileNotFoundError:
print("错误:找不到 flag.xlsx 文件,请确保文件存在。")
exit()
ws = wb.active # 使用活动工作表
# 根据 s 属性填充单元格
for ref, s_value in cell_data:
if s_value == "2":
ws[ref].value = 111 # 在 s="2" 的单元格填入数字 111
# s="1" 的单元格保持原样,不做操作
# 保存修改后的文件
wb.save('flag_modified.xlsx')
print("已完成修改,输出文件保存为 flag_modified.xlsx")
然后打开flag_modified.xlsx,要么用ctrl+h,要么用条件格式,总之换成黑色,调整宽高就得到二维码了,扫码得flag
参考:GKCTF 2021]excel 骚操作_excel 隐写 ctf-CSDN博客
——————————————————————————————————————————————
GHCTF新生赛
mybrave
参考:ZIP已知明文攻击深入利用 – FreeBuf网络安全行业门户
注意到压缩包使用的储存方法是Store,加密算法是ZipCrypto,加密的文件是png图片。
拿到秘钥后97d30dcc 173b15a8 6e0e7455解密,得到一张图片,010查看文件尾:
从54到结尾是base64,解码得到flag
知识点:
明文攻击并不需要得知其中完整的一个文件,知道至少 12 个字节的已知明文的话就可能可以使用明文攻击(其中8字节需要连续,需要知道已知字节的偏移)。
使用这类明文攻击要求压缩包的压缩方式是
ZipCrypto:Store或ZipCrypto:Deflate。
PNG图片的前 16 个字节基本是固定的。
——————————————————————————————————————————————
2025“红明谷”杯
1.异常行为溯源
附件是pcap,是访问日志,要找攻击者的攻击ip,题目说攻击者先低密度尝试然后再攻击
看到很多base64
考虑将数据包负载数据导出来解base64,得到JSON格式的数据保存到txt,发现还有一层base64
import base64
from scapy.all import rdpcap
def extract_packet_data(pcap_file):
# 读取 PCAP 文件
packets = rdpcap(pcap_file)
data_list = []
# 遍历每个数据包
for packet in packets:
# 检查数据包是否包含数据负载
if packet.haslayer('Raw'):
# 提取数据负载
data = packet['Raw'].load
data_list.append(data)
return data_list
def base64_decode_and_save(data_list, output_file):
with open(output_file, 'wb') as f:
for data in data_list:
try:
# 尝试进行 Base64 解码
decoded_data = base64.b64decode(data)
# 将解码后的数据写入文件
f.write(decoded_data)
f.write(b'\r\n')
except base64.binascii.Error:
print(f"数据 {data} 不是有效的 Base64 编码,跳过。")
# 替换为你的 PCAP 文件路径
pcap_file = 'network_traffic.pcap'
# 替换为你想保存的输出文件路径
output_file = 'decoded1.txt'
# 提取数据包数据
packet_data = extract_packet_data(pcap_file)
# 进行 Base64 解码并保存到文件
base64_decode_and_save(packet_data, output_file)
再写个脚本解base64
import base64
import json
def process_file(input_file_path, output_file_path):
with open(input_file_path, 'r', encoding='utf-8',errors='ignore') as input_file, open(output_file_path, 'w', encoding='utf-8') as output_file:
for line in input_file:
line = line.strip() # 去除行首尾的空白字符(如换行符)
try:
# 解析 JSON 数据
data = json.loads(line)
# 获取 msg 字段的值
msg = data.get('msg')
if msg:
# 对 msg 进行 Base64 解码
decoded_msg = base64.b64decode(msg).decode('utf-8')
# 将处理后的数据重新组装为 JSON 格式并写入输出文件
data['msg'] = decoded_msg
output_file.write(json.dumps(data) + '\n')
else:
output_file.write(line + '\n') # 如果没有 msg 字段,原样写入
except json.JSONDecodeError:
output_file.write(line + '\n') # 如果不是有效的 JSON 格式,原样写入
except base64.binascii.Error:
output_file.write(line + '\n') # 如果 msg 不是有效的 Base64 编码,原样写入
# 替换为你的输入文件路径
input_file_path = 'decoded1.txt'
# 替换为你的输出文件路径
output_file_path = 'decoded2.txt'
process_file(input_file_path, output_file_path)
看到ip了,再统计一下出现次数
import json
from collections import defaultdict
def count_ips(input_file_path, output_file_path):
ip_count = defaultdict(int)
with open(input_file_path, 'r', encoding='utf-8') as input_file:
for line in input_file:
line = line.strip()
try:
# 解析 JSON 数据
data = json.loads(line)
# 获取 msg 字段的值
msg = data.get('msg')
if msg:
# 提取 IP 地址(假设 IP 是 msg 字段中第一个出现的格式为 x.x.x.x 的部分)
# 这里简单地按空格分割,取第一个元素作为 IP
ip = msg.split()[0]
ip_count[ip] += 1
except json.JSONDecodeError:
continue # 跳过无效的 JSON 格式行
# 筛选出出现次数大于等于 4 次的 IP,并按出现次数从大到小排序
filtered_ip_count = {ip: count for ip, count in ip_count.items() if count >= 4}
sorted_ip_count = sorted(filtered_ip_count.items(), key=lambda x: x[1], reverse=True)
# 将统计结果写入输出文件
with open(output_file_path, 'w', encoding='utf-8') as output_file:
for ip, count in sorted_ip_count:
output_file.write(f"{ip}: {count}\n")
print("统计完成,结果已保存到", output_file_path)
# 替换为你的输入文件路径
input_file_path = 'decoded2.txt'
# 替换为你的输出文件路径
output_file_path = 'ip_count.txt'
count_ips(input_file_path, output_file_path)
———————————————————————————————————
2.数据校验
写脚本校验数据
import csv
import hashlib
import base64
import re
from ecdsa import VerifyingKey, BadSignatureError
import os
# 计算字符串的 32 位小写 MD5 值
def md5_hex(s):
return hashlib.md5(s.encode()).hexdigest()
# 加载 ECDSA 公钥
def load_public_key(serial_number):
pem_file = os.path.join("ecdsa-key", f"{serial_number}.pem")
with open(pem_file, "rb") as f:
pem_data = f.read()
return VerifyingKey.from_pem(pem_data)
# 定义密码格式的正则表达式:只允许大写字母、小写字母和数字
password_pattern = re.compile(r'^[A-Za-z0-9]+$')
# 定义 IP 地址格式的正则表达式
ip_pattern = re.compile(r'^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$')
# 校验 IP 地址格式
def is_valid_ip(ip):
match = ip_pattern.match(ip)
if not match:
return False
for part in match.groups():
num = int(part)
if not (0 <= num <= 255):
return False
return True
# 用于存储不合规的序列号
invalid_serials = []
# 读取并处理 CSV 文件
with open('data.csv', 'r') as f:
reader = csv.DictReader(f)
for row in reader:
sn = row['Serial_Number']
username = row['UserName']
username_check = row['UserName_Check']
password = row['Password']
password_check = row['Password_Check']
ip = row['IP']
signature = row['Signature']
# 校验 1:UserName 是否以 "User-" 开头
username_valid = username.startswith('User-')
# 校验 2:UserName 的 MD5 是否匹配
username_md5_valid = md5_hex(username) == username_check
# 校验 3:Password 是否只包含允许的字符
password_valid = bool(password_pattern.match(password))
# 校验 4:Password 的 MD5 是否匹配
password_md5_valid = md5_hex(password) == password_check
# 校验 5:IP 地址格式是否正确
ip_valid = is_valid_ip(ip)
# 校验 6:验证 ECDSA 签名
signature_valid = False
try:
vk = load_public_key(sn) # 加载对应公钥
sig = base64.b64decode(signature) # 解码签名
vk.verify(sig, username.encode(), hashfunc=hashlib.sha1) # 使用 SHA-1 哈希验证签名
signature_valid = True
except (base64.binascii.Error, BadSignatureError, Exception):
signature_valid = False
# 如果任一校验失败,记录该序列号
if not (username_valid and username_md5_valid and password_valid and password_md5_valid and ip_valid and signature_valid):
invalid_serials.append(sn)
# 将序列号转换为整数并按从小到大排序
invalid_serials = [int(sn) for sn in invalid_serials]
sorted_invalid = sorted(invalid_serials) # 默认升序排序
# 用下划线连接排序后的序列号
joined = '_'.join(map(str, sorted_invalid))
# 计算连接字符串的 MD5 值
flag_md5 = md5_hex(joined)
# 打印用 _ 连接的不合规序列号
print("Invalid Serial Numbers (joined):", joined)
# 生成并输出 flag
flag = f'flag{{{flag_md5}}}'
print(flag)
3个ecdsa不对,2个ip不对