代码审计之正则表达式的应用
本文最后更新于 35 天前,其中的信息可能已经有所发展或是发生改变。

这次,分享分享正则表达式,真实工作环境中做代码审计,合理的使用正则表达式,可以大幅度的提高工作效率。

背景

现在很多的 Java 项目的鉴权,都是引入鉴权组件,然后以注解的方式去做的。

比如:@JianQuanAPI(name="dashboard", auth=Auth.NONE, cluster="intranet")

那做接口的代码审计时,在保证鉴权组件内逻辑无误的情况下,最需要关心的就是注解的配置与身份信息的提取了。

而我现在想做的,就是从项目的成千上万个接口注解中,提取出“未鉴权”与“低鉴权”的接口,然后进一步审计这些接口是否符合“不需要鉴权”或者“低鉴权”的要求且内部逻辑是否存在其他逻辑类风险。

使用 IDEA 自带的正则匹配搜索,难点就在于,注解中的配置可能会存在顺序不一致、缺省属性等,我需要尽可能的不漏匹配。

分析

目前的鉴权注解(示例数据):@JianQuanAPI,具体在实例中看:

其中包含几个属性,意义分别是:

  • name: 接口 url;
  • auth: 鉴权类型。 NONE – 不鉴权; WEAK – 弱鉴权; AUTH – 强鉴权;
  • cluster: 集群类型。 intrannet – 内网; internet – 互联网

目标:

我需要匹配 name 以 bashboard 开头,且 type 为 NONE 但 cluster 不为 intrannet 的(内网情况下允许不鉴权)

开始行动:

先复习一下正则表达式基础,然后就开始实例尝试一下

代审之正则表达式基础

  • 字面字符。
  • 元字符:. (匹配任意单个字符)、* (匹配前一个元素0次或多次)、+ (1次或多次)、? (0次或1次)
  • 字符类:[abc](匹配 a/b/c 中的任意一个)、[^abc](匹配非 a/b/c 中的任意一个)
  • 分组:(...)(用于组合模式或者捕获匹配结果)
  • 转义:\(匹配元字符本身)
  • 空白匹配:\s(匹配空白字符。空格、制表符、换行符等)
  • 量词:{n}(n 次)、{n,}(至少 n 次)、{n,m}(n 到 m 次)

代审之正则表达式进阶

贪婪匹配(默认行为)

量词(包含元字符)会尽可能的匹配多的字符。

比如:

  • 正则:a.*b
  • 文本:axbzb
  • 结果:会匹配 axbzb,而不是 axb

非贪婪匹配

在量词后面加 ?,会尽可能的匹配少的字符,也是 ? 本身的意义(0 个或者 1 个满足条件就匹配成功)。

比如:

  • 正则:a.*b
  • 文本:axbzb
  • 结果:会匹配 axb,遇到第一个 b 就停止匹配

\b单词边界

\b 匹配“单词边界”(单词开始或结束的位置),而不是一个实际字符。

比如:

  • 正则:\bname\b
  • 文本:name = ...
  • 结果: 匹配成功,因为表达式中 name 是一个单词,如果文本是 username = xxx 就会匹配失败,因为在这个表达式中 username 才是一个单词,而 name 只是部分字母。

\bname\b

\bname

\sname\s

三者的区别是什么?

\bname 是匹配以 name 开头的单词,\b 是单词边界的意思

\sname\s 也是匹配一个 name 单词,但是同样会匹配上两端空格

\bname\b 会匹配所有符合 \sname\s 的场景,并且不会包含空格,且还会匹配到譬如 ,"_ 等符号隔开的场景。

断言(零宽预查)

普通正则表达式是从左到右依次匹配的,匹配满足之后就继续向右了。而断言,是“零宽”的,它只检查条件是否满足,而不消耗字符(不移动匹配的位置)。经常用于多条件匹配。共有 4 种(前面 2 种比较常见):

正向先行断言(?=...

检查当前位置之后是否匹配 pattern,但不消耗字符,如果匹配则继续,否则匹配失败。

比如:

  • 正则:password(?=\d)
  • 文本:password123
  • 结果:匹配成功。因为这个表达式意义是,匹配 password 且仅当其后面紧跟数字(\d)。所以 password匹配失败(后面没有数字),passwords 匹配失败(后面也没有数字,只有一个字母 s),password123 匹配成功(后面是数字)
负向先行断言(?!...

检查当前位置之后是否不匹配 pattern。如果不匹配则继续,否则匹配失败。

比如:

  • 正则:password(?!\d)
  • 文本:password123
  • 结果:匹配失败。因为这个表达式意义是,匹配 password 且仅当其后面不是数字。所以 passwords 可以匹配成功(后面是字母 s),password也可以匹配成功(后面什么都没有),但 password1 匹配失败(后面是数字)
正向后行断言(**?<=...**

检查当前位置之前是否匹配 pattern。但是这里:pattern 必须是固定长度写法。

比如:

  • 正则:(?<=pass)word
  • 文本:password
  • 结果: 匹配成功,因为 word 前面匹配 pass,如果是 p@ssword 就会匹配失败。
负向后行断言(?<!...

检查当前位置之前是否不匹配 pattern。

比如:

  • 正则:(?<!wang)xiaoming
  • 文本:wangxiaomingzhangxiaoming
  • 结果:wangxiaomign 匹配失败、zhangxiaomign 匹配成功,如果这是个 name 列,相当于是匹配所有不姓 wangxiaoming

实例

我的项目中,鉴权就是使用注解完成的,注解中有很多属性。

public @interface JianQuan {

    /**
     * 负责人
     */
    String author() default "";

    /**
     * API的名称
     */
    String name() default "";

    /**
     * API的版本
     */
    String version() default {};

    /**
     * 集群(内网时可以不需要鉴权)
     */
    Cluster cluster() default Cluster.common;

    /**
     * 登录和鉴权
     */
    Type auth() default Type.NONE;

    /**
     * API的描述
     */
    String desc() default "";

现在,我想筛选出:

  • name 属性以 bashboard. 开头(我今天要审计的功能模块);
  • auth 的值为 Auth.NONE(没有配置鉴权,也就是未授权);
  • 且没有 cluster 配置,或者 cluster 的值不为 Cluster.intranet (非内网集群)

的代码行。

非内网集群的未授权接口,大概率会存在未授权访问或者越权漏洞。

如下这是一个示例,我来依照这个示例完善正则表达式,但是要注意,注解内的属性顺序可能不固定。

@JianQuan(name = "bashboard.list", version = "1.0", author = "xxx", desc = "看板列表", auth = Auth.NONE, cluster = Cluster.intranet)

第一步:匹配 @JianQuanAPI(xxx) 注解所在的行

正则表达式:@JianQuanAPI\(.*\)

匹配 @JianQuanAPI 注解,内部可以是任意字符

问题:默认是贪婪匹配,如果出现多个 ) ,可能会匹配到最外面一个,不过理论上来说,只要代码是正确的,注解行时不会出现这种情况的,但既然学的是正则,那还是严谨一点

优化:@JianQuanAPI\(\s*[^)]*\)

优化成了 [^)]*, 意为多个不为 ) 的字符。\s* 是考虑任意个空位。

第二步:添加属性条件,先加 name

思路:

  • 属性顺序不确定,name 可能在 auth 前面,也可能在后面,所以,用正向肯定预查,不依赖顺序。
  • \b 来匹配 name 单词
  • name 的值包裹在双引号 " 内,所以 name 的值要匹配非 "(和上面一样,理论上来说正确的代码不会出现这种问题,但学习处于严谨考虑还是加上限制)

正则表达式:@JianQuanAPI\(\s* 这里加入对属性的匹配条件 [^)]*\)

name属性匹配条件:\bname\b\s*=\s*"dashboard[^"]+"

\b 来匹配 name 单词, \s* 匹配 = 两端,允许出现 0 个或者多个空格,[^"]+ 用来匹配一个或者多个非 " 的字符

继续优化一下:因为要忽略属性的顺序,也就是说 name 属性前面可能还会有别的属性,所以,name 的匹配条件前面,再加一个 [^)]*? ,意为非贪婪匹配任意不是 ) 的字符,只要能找到 name匹配条件就行。

优化后的 **name** 属性匹配条件:?=[^)]*?\bname\b\s*=\s*"dashboard[^"]+"

接下来,构建预查询语句(?=),然后将 name 属性匹配条件用小括号括起来(组合模式),得到

正则表达式:@JianQuanAPI\(\s*(?=[^)]*?\bname\b\s*=\s*"dashboard[^"]+")[^)]*\)

第三步:添加第二个属性条件,auth

和 name 一样

正则表达式结构:@ThorApi\(\s* (?=name条件)(?=auth条件) [^)]\)

auth属性匹配条件:\bauth\b\s*=\s*Auth\.NONE\b

name 一样,由于顺序不强制要求,所以前面也允许 [^)]*? 非贪婪匹配,并构建预查询语句

得到正则表达式:

@JianQuanAPI\(\s*(?=[^)]*?\bname\b\s*=\s*"dashboard[^"]+")(?=[^)]*?\bauth\b\s*=\s*Auth\.NONE\b)[^)]*\)

第四步:排除第三个属性条件,cluster

思考:

首先,如果 cluster 不存在,也就是没有这个属性,那应该是要匹配的,所以不需要刻意写这个条件,我们只需要处理「不匹配 cluster = Cluster.intranet」 的场景即可。

不匹配,那就是用负向预查询(?!),而至于规则,与 nameauth 就类似了,不需要贪婪匹配,使用非贪婪匹配,只要发现 cluster = Cluster.intranet,直接就可以停止了

正则表达式结构:@JianQuanAPI\(\s* (?=name条件)(?=auth条件)(?!cluster条件) [^)]\)

cluster 条件:[^)]*?\bcluster\b\s*=\s*Cluster\.intranet\b

基本上同上,唯一不同的是这次构建的是负向预查询 ?!

得到正则表达式:

@JianQuanAPI\(\s*(?=[^)]*?\bname\b\s*=\s*"dashboard[^"]+")(?=[^)]*?\bauth\b\s*=\s*Auth\.NONE\b)(?![^)]*?\bcluster\b\s*=\s*Cluster\.intranet\b)[^)]*\)

这样,就得到了完整的正则表达式,测试一下效果

第五步:补充 —— 我遗漏了一个场景

经过实测之后,我发现,我遗漏了一种情况,就是当没有 auth 属性的时候,由于它的默认值就是 Auth.NONE,所以,我还需要捕获这种情况。

这里先补充一个语法:

  • ?:: 非捕获分组。

最开始「基础」中有提到 (xxx) 的作用是分组,默认就是捕获分组,用来捕获匹配结果。而 ?: 就是写在分组括号内的最前面,来表示这个分组不需要捕获,就像这样(?:xxx)

理论上来说,分组捕获还是不捕获,并不会影响正则的匹配结果,它只会影响捕获结果。(就像我们代审时,我只需要匹配出我需要的代码即可,并不需要处理捕获结果)。而捕获,通常是应用在开发语言中的,因为开发语言中会对正则表达式匹配并捕获到的结果做进一步处理。

所以很多开发语言中,也会对正则表达式升级「命名分组」,防止后续对正则表达式进行调整后,所捕获的分组出现乱序。

思路:

  1. 先把之前的正则表达式拿过来,找到关于 auth 的匹配条件,留出占位符
  2. 编写关于 auth 的匹配条件
    1. 匹配 auth = Auth.NONE或者不存在 auth 属性,也就是说,出现 0 次或者 1 次,但可以不写,我们只需要禁止它以外的出现,剩下的就是它或者不存在的场景;
    2. 不匹配 auth = Auth.NONE 以外的任何场景,也就是说,如果出现了 auth,且值不为 NONE,就匹配失败
  3. 将其作为分组,填充回原来留的占位符中,得到完整正则表达式

原始正则:

@JianQuanAPI\(\s*(?=[^)]*?\bname\b\s*=\s*"dashboard[^"]+")(?=[^)]*?\bauth\b\s*=\s*Auth\.NONE\b)(?![^)]*?\bcluster\b\s*=\s*Cluster\.intranet\b)[^)]*\)

留出空位:

@JianQuanAPI\(\s*(?=[^)]*?\bname\b\s*=\s*"dashboard[^"]+") auth 相关的规则 (?![^)]*?\bcluster\b\s*=\s*Cluster\.intranet\b)[^)]*\)

auth 相关的规则:

匹配 auth 不存在或者存在但不为 NONE,也就是禁止匹配 auth = Auth.XXX 其中 XXX 不为 NONE 这种场景。

(?![^)]*?\bauth\b\s*=\s*Auth\.(?!NONE\b)\w+\b)

构造完整的正则表达式:

@JianQuanAPI\(\s*(?=[^)]*?\bname\b\s*=\s*"dashboard[^"]+")(?![^)]*?\bauth\b\s*=\s*Auth\.(?!NONE\b)\w+\b)(?![^)]*?\bcluster\b\s*=\s*Cluster\.intranet\b)[^)]*\)

正则表达式这个东西,要精确的话太复杂了,可能还有很多没有考虑到的特殊场景需要优化,但是目前作为人工代码审计来说,可以将大量的几千个接口过滤到符合条件的十来个接口,已经足够了,即使有遗漏的特殊场景,人工也能看出来。

当然如果想作为一个扫描工具使用的话,那这个正则表达式可能还需要多喂一些样本数据来继续优化一下。

学海无涯,回头是岸。 --- hola
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇