记一次实战环境下waf严格过滤的绕过实现sql注入思路

感觉是个很有趣的环境记录一下(实战可用性感觉还挺高的我这个思路)

本次环境fuzz后总结的waf的过滤规则速览:

比较符号类会 ban: =, <, >

常见查询关键字会 ban: SELECT, UNION , LIKE ,REGEX 等等关键字也都ban

函数调用形态会 ban: 只要在 payload 里出现“完整的 函数名(…)”就会 403(例如 aaaa(), abcdefg(1) 这种完整括号闭合的)

“空括号模式”也会 ban: xxxx() 这种只要 () 完整出现且前面有字符,就会直接 403(这里跟上面有点重复这里稍微让ai总结了一下waf规则,所以有点重复)

在常规注入位置下,这类 WAF 几乎封死了所有常见 SQL 注入手段:
无法构造条件判断、无法引入查询语句、也无法直接调用函数。

image-20251218130502548

分享一下思路

虽然看到最后

这个payload可能会很简单(bushi

但几乎所有方向都试过了才得到的

image-20251221132806346

这是正常返回包的情况

注入点在trainingSite参数这里

后续懒得打码

所以后续请求包这部分直接给payload

然后给payload对应的响应包截图

我们给他加个aaaa可以看到报错如下

trainingSite=1aaaaaaa

image-20251217225606343

根据报错我们能拿到完整的执行语句

SELECT COUNT(1) FROM (SELECT id,equipment_name,qr_code,equiment_type,upload_img,cover_img,file_video_id,training_site,suitable_people,avoid_people,instructions,create_by,create_time,update_time FROM t_equipment WHERE (FIND_IN_SET (1aaaaaaa, training_site ))) TOTAL

简单分析一下1aaaaaaa是我们的注入点

现在可以开始打sql注入了

首先肯定想到的是用)))payload–

这种形式直接注释掉后续内容直接执行payload不拼接很方便

但是这里行不通因为上面我fuzz后总结的waf的规律可以看出这种形式的payload无法执行任意函数,没有函数也就意味着很难有效的sql注入

如图

image-20251221132822672

image-20251221132837847

image-20251221132849945

仔细对比这三张图,其实差异很明确:

  • aaaa() 这种完整函数调用形态会被直接拦截(403)。
  • 1,training_site)%20AND%20FIND_IN_SET(1 不闭合括号时不会触发拦截,能正常执行。
  • 但一旦写成 1,training_site)%20AND%20FIND_IN_SET(1),括号闭合后又会被 ban。

这基本说明:WAF 并不是单纯拦截某个函数名,而是拦截“函数名(…)”这种完整调用结构。因此常见的时间盲注、报错注入等手段都会受影响——因为无论哪种注入方式,几乎都绕不开函数调用;同时 select/union 等关键字也被 ban,= < > 也不可用,整体限制非常苛刻。

在这种前提下,传统的 ) payload -- 思路就基本走不通了:一旦用注释截断后续语句,payload 里又无法写出函数调用,注入很难继续推进。于是我只能转向“闭合结构”本身,先构造出:

1
1,training_site)%20AND%20FIND_IN_SET(1

但问题仍然存在:payload 里依旧不能出现 substring/concat/sleep 等函数调用,看似会被彻底卡死

不过这里有一个关键点:我们已经拿到了原始 SQL,而且注入点本身就在多层括号和函数参数内部:

1
... WHERE (FIND_IN_SET(1aaaaaaa, training_site)) ...

也就是说:原 SQL 天然提供了括号环境和“函数形态”的外壳。在上面的 payload 中,FIND_IN_SET 已经是一个现成的函数名,我们理论上可以把它替换成其他函数名(如 sleepsubstring 等),并让“最终拼接后的 SQL”形成函数执行。

但继续测试后发现,这个环境几乎被“写死”成只能使用一个函数名
如果引入多个函数(例如 SUBSTRING + CAST),要么会因为出现完整调用触发 WAF,要么因为括号无法补全导致语法错误。例如下面这种思路虽然直观:

1
trainingSite=1,training_site)%20OR%20SUBSTRING(@@version,cast(@@version%20as%20signed

而如果为了绕过拦截不写闭合括号,又会变成括号不匹配直接报错。(右括号少一个)

进一步还有一个细节:注入位置后面原本就会拼接 , training_site 这个参数。比如你尝试写 IF(...) 时,最终会被拼成类似:

1
IF(1=1,5,5,training_site)

这会破坏我们原本的参数结构,导致逻辑难以控制。

因此这里的函数选择必须满足几个条件:

  1. 只能用一个函数名(否则触发 403 或语法错误)
  2. 至少需要两个参数(这样才“容得下”后面被拼接进来的 training_site
  3. 最后一个被拼接进来的参数对整体影响尽量小
  4. 最好能直接回显信息(否则盲注会非常难写)

基于这些条件,我把方向转为报错注入,并逐个测试常见报错函数后,发现 UPDATEXML 正好满足要求,于是得到最终 payload:

1
1,training_site)%20OR%20UPDATEXML(1,current_user

利用原 SQL 的括号/参数环境完成拼接后即可成功触发报错回显,从而实现注入。

image-20251221132910617

这里@@version之类的变量也能打

但是不能爆出库名之类的因为database()函数都被ban了….任何函数都不能用

只能爆破一些变量或者current_user之类的

可能在护网里无法扩大危害了

但是在众测,企业src或者edu资产

我感觉能到这一步你直接交上去已经够了

算一个sql注入了(

image-20251220184313226

小总结:

这次废话有点多

稍微总结一下

这里的waf本来是基本所有路都给拦截断了

但是因为特殊情况

我们这个注入点是在一个被多个()包括的情况下

于是我们可以通过利用现成的环境去实现一个函数的调用

并且不被waf拦截

因为我们这个不是一个完整的函数也不是一个完整的函数调用单纯从payload上看

但是拼接进去就是了以此绕过waf

其实这种类似的思路用一些特别的环境和位置或者处理去绕过waf的手段还是很多大牛都在用的

可能网上已经有师傅早就挖穿类似的洞了

但是我是新手所以比较喜欢记录QAQ感觉很有意思想写成blog

我还有几个很有意思的payload也都是sql注入的

我会在2025年年末总结里给出如果有感兴趣的师傅也可以看看那个文章喵~

也是成功再拿一个高危.

如果师傅们有更好的思路可以和我分享分享