数据泄露——江苏省第四届数据安全技术应用职业技能竞赛初赛"

admin 2026-10-20 05:40:46 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

cover_image

数据泄露——江苏省第四届数据安全技术应用职业技能竞赛初赛

原创

一只岸上的鱼
一只岸上的鱼

一只岸上的鱼

2026年7月13日 21:07
江苏

在小说阅读器读本章

去阅读

数据泄露——江苏省第四届数据安全技术应用职业技能竞赛初赛

缘起

其实这次赛事组织的真不咋地,一会儿没插座,一会儿附件无法下载,还有题目是奇奇怪怪的,比如这题,附件使用usb拷贝的,有三问,但是实际试卷却只有俩问……

总的来说,这是一道经典的题目,就是sql注入日志分析,这题还算简单————因为题目常规,脚本是备好的,

可惜,还是没得分,第一问做出来了,但是题目没有,第二问也做出来了,但是一直答案不对……,第三问就没做了

题干

先说,题目附件我放在cnb了:https://cnb.cool/netvvorm/items/datasecurity26

第一问:

但是附件下载后,第一问实际是:

分析

这种sql注入是经典题目,有时候还会套一层流量分析:就是先要从流量包里面导出这些日志,notepad打开是这样的:

有个小技巧,使用notepad++自带的工具进行url decode:

这样比较容易看得清楚:

布尔盲注

什么叫盲注:就是select的信息内容不会实际返回

什么叫布尔:即使返回结果根据对错有无,返回只有2中情况,所以借以判断注入结果

一般数据库、表、字段命名,大部遵循ascii码,而ascii码的值的范围是0-127,所以有一种简单的办法就是将想要查询的字段的每个字符与所有的ascii码进行比较,如果返回的结果与预期不符,则说明该字符不存在,否则存在,这样就可以逐个字符的查询了,当然实用的方法是二分法,而不是真的顺序查询。

以本题的第一个字符为例:

实际执行的sql是这样的:

sql

-- 布尔条件:第1个字符的ASCII码是否大于97?
-- 注入字段
ORD(MID((SELECT IFNULL(CAST(username AS NCHAR),0x20) FROM user_data.`user` ORDER BY id LIMIT 0,1),1,1))>63 AND 'CWkt'='CWkt

-- 后台执行的sql(例)
SELECT * FROM user WHERE id = 1 AND ORD(MID((SELECT username FROM user LIMIT 0,1),1,1))>63

然后来分析日志:

  1. 先确认true和false

返回的结果都是http code=200,但是长短只有2个,一个是24,一个47,看一眼(这一眼是指根据二分法查找的值的变化来判断的),24=true,47=false,

小知识:判断一般有这几种情况

信号类型 示例
响应体长度不同 True→24字节, False→47字节
HTTP状态码不同 True→200, False→302
响应时间差异 True→<100ms, False→>500ms(时间盲注)
页面特征文本 True含\"Welcome\", False含\"Error\"
  1. 二分法轮询
code

第1轮:> 63 ?  →  True → 值在 (63, 127]
第2轮:> 95 ?  →  True → 值在 (95, 127]
第3轮:> 111 ? →  False → 值在 (95, 111]
第4轮:> 103 ? →  False → 值在 (95, 103]
第5轮:> 99 ?  →  False → 值在 (95, 99]
第6轮:> 97 ?  →  False → 值在 (95, 97]
第7轮:> 96 ?  →  True  → 值 = 97  ✓

asscii码的值是97,所以第一个字符是a

后面就简单的了,最终结果:

+----------+------------+
| 字段     | 值         |
+----------+------------+
| username | admin      |
| password | Adm1n@2026 |
+----------+------------+

脚本:

这个脚本是AI给的,因为他比我的脚本好看,就提供这个了:

python

#!/usr/bin/env python3
\"\"\"
SQL Boolean Blind Injection Log Analyzer
========================================
从Web访问日志中还原SQL布尔盲注(二分搜索模式)窃取的数据库内容。

适用场景:
  - CTF取证题:给定access.log,还原被盲注窃取的数据
  - 应急响应:分析攻击者通过盲注读取了哪些敏感字段

支持的Payload格式(sqlmap常见风格):
  ORD(MID((SELECT IFNULL(CAST(field AS NCHAR),0x20)
         FROM database.`table` ORDER BY col LIMIT row,1),pos,1))>threshold

用法:
  python sqli_blind_log_analyzer.py -f access.log [--true-size 24] [--false-size 47] [--auto-detect]

作者:CTF Log Forensics Tool
\"\"\"

import re
import sys
import argparse
import urllib.parse
from collections import defaultdict

# ──────────────────────── 正则模式 ────────────────────────

# 匹配sqlmap风格的布尔盲注payload
PAYLOAD_PATTERN = re.compile(
    r\"ORD\\(MID\\(\\(SELECT IFNULL\\(CAST\\((\\w+) AS NCHAR\\),0x20\\)\"
    r\" FROM (\\w+)\\.`(\\w+)` ORDER BY \\w+ LIMIT (\\d+),1\\)\"
    r\",(\\d+),1\\)\\)>(\\d+)\"
)

# 更宽松的匹配:不要求ORD/MID/IFNULL的具体格式,只要有关键结构
LOOSE_PATTERN = re.compile(
    r\"(?:ORD|ASCII)\\(.*?MID\\(.*?(?:SELECT|select).*?\"
    r\"(\\w+).*?LIMIT\\s+(\\d+),1.*?,(\\d+),1\\).*?>(\\d+)\",
    re.IGNORECASE | re.DOTALL
)

# 日志行解析
LOG_PATTERN = re.compile(
    r'(\\S+).*?\"(\\w+)\\s+(\\S+)\\s+HTTP[^\"]*\"\\s+(\\d+)\\s+(\\d+)'
)

# ──────────────────────── 核心逻辑 ────────────────────────

def parse_log(filepath, true_size=None, false_size=None, auto_detect=True):
    \"\"\"
    解析日志文件,提取所有盲注请求并还原数据。

    参数:
        filepath:    日志文件路径
        true_size:   响应True时的body长度(已知时传入)
        false_size:  响应False时的body长度(已知时传入)
        auto_detect: 是否自动检测True/False对应的响应长度

    返回:
        dict: {(field, db, table, row): {position: char}}
    \"\"\"
    with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
        lines = f.readlines()

    print(f\"[*] 日志行数: {len(lines)}\")

    # 第一遍:收集所有请求信息
    requests = []
    size_counter = defaultdict(int)

    for line in lines:
        m = LOG_PATTERN.search(line)
        if not m:
            continue

        ip, method, url, status_code, body_size = m.groups()
        if method != 'GET' or int(status_code) != 200:
            continue

        body_size = int(body_size)
        decoded_url = urllib.parse.unquote(url)

        # 尝试匹配payload
        pm = PAYLOAD_PATTERN.search(decoded_url)
        if not pm:
            continue

        field = pm.group(1)       # 字段名
        db = pm.group(2)          # 数据库名
        table = pm.group(3)       # 表名
        row = int(pm.group(4))    # 行偏移
        pos = int(pm.group(5))    # 字符位置
        threshold = int(pm.group(6))  # 比较阈值

        requests.append((field, db, table, row, pos, threshold, body_size))
        size_counter[body_size] += 1

    print(f\"[*] 有效盲注请求数: {len(requests)}\")
    print(f\"[*] 响应长度分布: {dict(sorted(size_counter.items(), key=lambda x: -x[1]))}\")

    # ── 自动检测 True/False 对应的响应长度 ──
    if true_size is None or false_size is None:
        if auto_detect and len(size_counter) == 2:
            sizes = sorted(size_counter.keys())
            # 启发式:较小的size通常对应True(页面返回简化内容)
            # 但这不总是成立,需要验证
            print(f\"[!] 响应只有两种长度: {sizes[0]} 和 {sizes[1]}\")
            print(f\"[!] 需要确定哪个是True,哪个是False\")

            # 尝试两种方向,用逻辑一致性验证
            for ts, fs in [(sizes[0], sizes[1]), (sizes[1], sizes[0])]:
                if verify_consistency(requests, ts, fs):
                    true_size, false_size = ts, fs
                    print(f\"[+] 验证通过: True={true_size}, False={false_size}\")
                    break
            else:
                # 两种方向都无法完美验证,默认取较小值为True
                true_size, false_size = sizes[0], sizes[1]
                print(f\"[!] 无法自动验证,默认 True={true_size}, False={false_size}\")
                print(f\"[!] 如结果不正确,请用 --true-size 和 --false-size 手动指定\")
        else:
            print(\"[!] 无法自动检测,请用 --true-size 和 --false-size 指定\")
            return {}

    # ── 按字符位置分组,执行二分搜索还原 ──
    char_tests = defaultdict(list)
    for field, db, table, row, pos, threshold, body_size in requests:
        is_true = (body_size == true_size)
        char_tests[(field, db, table, row, pos)].append((threshold, is_true))

    reconstructed = defaultdict(dict)
    for key, tests in char_tests.items():
        field, db, table, row, pos = key
        ascii_val = resolve_binary_search(tests)
        if ascii_val > 0:
            reconstructed[(field, db, table, row)][pos] = chr(ascii_val)

    return reconstructed

def verify_consistency(requests, true_size, false_size, sample_limit=50):
    \"\"\"
    验证True/False方向是否逻辑一致。

    原理:二分搜索的True/False序列应当形成合理的区间——
    所有True的阈值应当 < 所有False的阈值。
    \"\"\"
    consistent = 0
    total = 0

    # 按字符分组
    groups = defaultdict(list)
    for field, db, table, row, pos, threshold, body_size in requests[:sample_limit * 7]:
        is_true = (body_size == true_size)
        groups[(field, db, table, row, pos)].append((threshold, is_true))

    for key, tests in groups.items():
        if len(tests) < 3:
            continue
        max_true = max((t for t, v in tests if v), default=-1)
        min_false = min((t for t, v in tests if not v), default=256)

        total += 1
        if max_true < min_false:  # 合理区间
            consistent += 1

    if total == 0:
        return False

    ratio = consistent / total
    return ratio > 0.8  # 80%以上一致则通过

def resolve_binary_search(tests):
    \"\"\"
    从一组 (threshold, is_true) 对中还原字符的ASCII值。

    算法:
      max_true  = 所有True阈值中的最大值
      min_false = 所有False阈值中的最小值
      若 max_true + 1 == min_false → 值 = max_true + 1(精确命中)
      若 max_true == -1(全部False)→ 值 = 0(超出字符串长度)
      若 min_false == 256(全部True)→ 值 = max_true + 1
    \"\"\"
    max_true = max((t for t, v in tests if v), default=-1)
    min_false = min((t for t, v in tests if not v), default=256)

    if max_true + 1 == min_false:
        return max_true + 1
    elif max_true == -1:
        return 0  # 字符串结束
    elif min_false == 256:
        return max_true + 1
    else:
        # 存在噪声/不一致,取最佳估计
        return max_true + 1

# ──────────────────────── 输出格式化 ────────────────────────

def print_results(reconstructed, verbose=False):
    \"\"\"打印还原结果\"\"\"
    print(\"\
\" + \"=\" * 65)
    print(\"  SQL Boolean Blind Injection - Data Recovery Results\")
    print(\"=\" * 65)

    # 按表和行分组输出
    tables = defaultdict(list)
    for key in reconstructed:
        field, db, table, row = key
        tables[(db, table)].append((row, field, key))

    for (db, table), entries in sorted(tables.items()):
        print(f\"\
  Database: {db} | Table: {table}\")
        print(f\"  {'-' * 50}\")

        rows = defaultdict(dict)
        for row, field, key in entries:
            chars = reconstructed[key]
            value = ''.join(chars[p] for p in sorted(chars.keys()))
            rows[row][field] = value

        for row in sorted(rows.keys()):
            fields_str = ' | '.join(f\"{f}={v}\" for f, v in sorted(rows[row].items()))
            print(f\"  Row {row}: {fields_str}\")

    print(\"\
\" + \"=\" * 65)

def print_detailed_trace(reconstructed, char_tests_cache, target_field=None, target_table=None, target_row=0):
    \"\"\"打印详细的二分搜索轨迹\"\"\"
    print(f\"\
--- Detailed Binary Search Trace ---\")

    for key in sorted(char_tests_cache.keys()):
        field, db, table, row, pos = key
        if target_field and field != target_field:
            continue
        if target_table and table != target_table:
            continue
        if row != target_row:
            continue

        tests = sorted(char_tests_cache[key], key=lambda x: x[0])
        steps = ' '.join(f'>{t}={\"T\" if v else \"F\"}' for t, v in tests)
        ascii_val = resolve_binary_search(tests)
        ch = chr(ascii_val) if ascii_val > 0 else '(end)'
        print(f\"  {field} Pos{pos}: {steps}  => ASCII {ascii_val} = \\\"{ch}\\\"\")

# ──────────────────────── 主入口 ────────────────────────

def main():
    parser = argparse.ArgumentParser(
        description='SQL Boolean Blind Injection Log Analyzer',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=\"\"\"
示例:
  # 自动检测模式
  python sqli_blind_log_analyzer.py -f access.log

  # 手动指定True/False响应长度
  python sqli_blind_log_analyzer.py -f access.log --true-size 24 --false-size 47

  # 仅还原特定表
  python sqli_blind_log_analyzer.py -f access.log --table user --field username,password
        \"\"\"
    )
    parser.add_argument('-f', '--file', required=True, help='日志文件路径')
    parser.add_argument('--true-size', type=int, default=None, help='True条件对应的响应体长度')
    parser.add_argument('--false-size', type=int, default=None, help='False条件对应的响应体长度')
    parser.add_argument('--no-auto-detect', action='store_true', help='禁用自动检测')
    parser.add_argument('--table', type=str, default=None, help='只还原指定表')
    parser.add_argument('--field', type=str, default=None, help='只还原指定字段(逗号分隔)')
    parser.add_argument('-v', '--verbose', action='store_true', help='显示详细二分搜索轨迹')

    args = parser.parse_args()

    reconstructed = parse_log(
        args.file,
        true_size=args.true_size,
        false_size=args.false_size,
        auto_detect=not args.no_auto_detect
    )

    # 过滤
    if args.table or args.field:
        fields_set = set(args.field.split(',')) if args.field else None
        to_remove = []
        for key in reconstructed:
            field, db, table, row = key
            if args.table and table != args.table:
                to_remove.append(key)
            if fields_set and field not in fields_set:
                to_remove.append(key)
        for key in to_remove:
            del reconstructed[key]

    print_results(reconstructed, verbose=args.verbose)

if __name__ == '__main__':
    main()

使用方法

bash

# 基础用法:自动检测True/False并还原所有字段
python sqli_blind_log_analyzer.py -f access.log

# 手动指定响应长度(当自动检测不确定时)
python sqli_blind_log_analyzer.py -f access.log --true-size 24 --false-size 47

# 只还原user表的username和password字段
python sqli_blind_log_analyzer.py -f access.log --table user --field username,password

试试效果

第二问:

实际上这才是试题的第一问,可惜做出来答案不对,这里记录一下做题的过程,也有回来用ai溯源的新的代码:

其实只要第一个做出来了,这个就很简单了,这里就不放代码,免得有水文的嫌疑

不过很可惜,这问也没得分,因为md5计算不对,不过因为数据太多,感觉哪儿有点小问题导致的,就不折腾了。

第三问:

这题没做,不过可惜了,这题真应该做的,因为回来复盘,这个答案个位数,计算md5应该不会错/(ㄒoㄒ)/~~

卢恩算法:

这个也就名字唬人,实际上就是模10,数据安全最基数的算法之一(还有一个是计算身份证校验码的)

这是ai优化后的,感觉还没有我写的简单:

python

def luhn_check_optimized(card_number):
    \"\"\"
    优化版卢恩算法校验,使用反向遍历和更高效的计算方式
    \"\"\"
    # 清洗输入
    cleaned = ''.join(filter(str.isdigit, str(card_number)))
    if not cleaned or len(cleaned) < 12:  # 银行卡号通常至少12位
        return False

    total = 0
    # 反转字符串,从右向左处理
    reversed_num = cleaned[::-1]

    for i, digit in enumerate(reversed_num):
        n = int(digit)
        # 偶数位(反向后的第2、4、6...位,即原卡号从右数偶数位)
        if i % 2 == 1:
            n *= 2
            # 如果大于9,各位数字相加
            if n > 9:
                n = n // 10 + n % 10
        total += n

    return total % 10 == 0

小结

这次比赛,感觉运气也是背到家,好几个感觉做的是正确的,但是最后计算md5提交答案,都错了,郁闷。


数据泄露——江苏省第四届数据安全技术应用职业技能竞赛初赛" 网络安全文章

数据泄露——江苏省第四届数据安全技术应用职业技能竞赛初赛"

本文分析江苏省第四届数据安全技术应用职业技能竞赛初赛中的数据泄露题目,聚焦SQL布尔盲注日志分析。通过解析HTTP响应长度差异(24字节为True,47字节为False),利用二分法逐字符还原数据库内容,最终成功提取用户名admin和密码Adm1n@2026。文章提供了详细的日志分析步骤和自动化脚本,适用于CTF取证和应急响应场景。
第四批UAP解密背后:美国真正公开的,不是UFO" 网络安全文章

第四批UAP解密背后:美国真正公开的,不是UFO"

本文剖析美国第四批UAP解密材料,指出其核心并非外星文明,而是展示美国构建未知目标情报体系的流程,包括多源数据融合与风险管理。作者将其与网络威胁研判类比,强调数据链完整是高可信评估的基础。建议安全人员重点关注此类情报融合机制与态势感知体系建设。
DIDCTF招聘专区正式上线|连接电子数据取证人才与企业" 网络安全文章

DIDCTF招聘专区正式上线|连接电子数据取证人才与企业"

DIDCTF平台正式上线招聘专区,旨在连接电子数据取证、网络安全等垂直领域的人才与招聘方。专区支持职位浏览、按条件筛选、信息发布与统一管理,覆盖电子数据取证、应急响应、逆向分析等方向。该功能整合了学习、实战与就业场景,为技术人员和团队提供精准对接渠道,建议相关从业者或招聘方使用该平台发布或寻找机会。
银狐也想来世界杯分一杯羹?新家族开始为“菠菜”平台增设广告位!" 网络安全文章

银狐也想来世界杯分一杯羹?新家族开始为“菠菜”平台增设广告位!"

火绒安全实验室发现银狐黑产组织使用新样本,通过伪装成压缩包、视频剪辑等应用进行投毒。恶意代码以插件形式加载,灵活多变,最终投放世界杯赌球广告弹窗。样本通过多层解密与注入技术实现持久化控制,窃取用户信息并劫持网关。火绒安全产品已实现拦截与查杀,并提供手动查杀方法。
评论:0   参与:  0