7. 过滤器:?{...} 筛选审计结果
经过前面的学习,您已经基本掌握了 SyntaxFlow 的执行原理和基本规则的编写方法。接下来,我们将介绍一个强大的功能——分析值的筛选过滤。通过使用 ?{...} 结构,您可以过滤掉不需要的审计值或排除那些没有漏洞的值,从而提升审计的精准度和效率。
简介
在静态代码分析和安全审计过程中,分析值的筛选过滤(Analysis Value Filtering)是一个至关重要的技术。它允许审计员通过条件表达式精确控制哪些数据流继续参与进一步的审计分析,从而聚焦于真正潜在的安全风险。SyntaxFlow 提供了 ?{...} 结构,使得这种筛选变得简单而强大。
通过 分析值筛选过滤,您可以:
- 排除不相关的结果,减少噪音,提升审计效率。
- 聚焦潜在的安全漏洞,确保审计过程更加精准。
- 自定义过滤条件,适应不同的审计需求和场景。
分析值筛选过滤语法定义
在 SyntaxFlow 中,?{...} 结构用于定义过滤条件。通过条件表达式,您可以指定复杂的逻辑来筛选需要关注的审计值。
运算符概览
以下是 SyntaxFlow 中可用的过滤运算符及其说明:
| 运算符类型 | 描述 | 示例 |
|---|---|---|
| 嵌套语句 | 确定方法成员或属性等嵌套语句的执行是否存在 | $vars?{.setFeature} as $new |
逻辑非 ! | 排除特定操作,用于否定条件 | $vars?{!((.setFeature) || (.setXIncludeAware))} as $new |
逻辑与 && | 同时满足多个条件 | $vars?{(.setFeature) && (.setXIncludeAware)} as $new |
逻辑或 || | 满足任一条件 | $vars?{(.setFeature) || (.setXIncludeAware)} as $new |
| Opcode : | 检查特定类型的操作,常用于操作码过滤 | $vars?{opcode: 'call', 'phi'} as $new |
| Have : | 检查是否包含指定字符串(无通配符) | $vars?{have: 'abc'} as $new |
| HaveAny : | 检查是否包含任一指定字符串(无通配符) | $vars?{any: 'abc', 'def'} as $new |
| VersionIn : | 检查依赖版本是否在某个版本区间,可以使用 || 表示区间并集 | $vars?{version_in:(1.2.3,2.3.4]} as $vulnVersion |
| 比较运算 | 对当前值或中间表达式结果进行比较(==/!=/>=/<= 等) | $vars?{==2} as $new |
| 组合条件 | 结合多种条件进行复杂的逻辑过滤 | $vars?{(.setFeature) && !(.setXIncludeAware)} as $new |
语法结构
filterItem
: filterItemFirst #First
...
| '->' # NextFilter
| '#>' # DefFilter
| '-->' # DeepNextFilter
| '-{' (recursiveConfig)? '}->' # DeepNextConfigFilter
| '#->' # TopDefFilter
| '#{' (recursiveConfig)? '}->' # TopDefConfigFilter
| '?{' conditionExpression '}' # OptionalFilter
...
;
conditionExpression
: '(' conditionExpression ')' # ParenCondition
| filterExpr op=('>'|'<'|'='|'=='|'>='|'<='|'!=' ) (numberLiteral|identifier|boolLiteral) # BinaryCompare
| filterExpr # FilterCondition // 嵌套语句/字段/NativeCall 等
| Opcode ':' opcodes (',' opcodes)* ','? # OpcodeTypeCondition
| Have ':' stringLiteralWithoutStarGroup # StringContainHaveCondition
| HaveAny ':' stringLiteralWithoutStarGroup # StringContainAnyCondition
| VersionIn ':' versionInExpression # VersionInCondition
| negativeCondition conditionExpression # NotCondition
| op=('>'|'<'|'='|'=='|'>='|'<='|'!=' ) (numberLiteral|identifier|boolLiteral) # CompareShorthand // 例如 ?{==2}
| op=('=~'|'!~') (stringLiteral|regexpLiteral) # RegexpMatchShorthand
| conditionExpression '&&' conditionExpression # FilterExpressionAnd
| conditionExpression '||' conditionExpression # FilterExpressionOr
;
?{conditionExpression}:用于定义过滤条件,包含在{}内的条件表达式。conditionExpression:支持多种条件逻辑,包括嵌套、操作码检查、字符串包含等。
关键语义:条件是“按值求值”的
在 ?{...} 中,每一个输入值都会在自己的上下文里求值:条件表达式会在该值的作用域内执行,最终再决定“这个输入 值是否保留”。
这带来两个很实用的效果:
- 不会串源:多源输入时,某个值中间展开出来的结果,不会影响另一个值是否命中。
- 自动分组:即使你在条件里做
*展开、或使用<len>/<slice>这类聚合/取位操作,计算也会按“每个输入值”分别进行,而不是把所有结果拍平后做全局计算。
示例:多源值不会互相污染
测试代码:
x1 = { b: { c: 1 } }
x2 = { b: { e: 2 } }
SyntaxFlow 规则:
x*?{.b.c} as $target
解释:
.b.c会在每个x*命中的对象各自的上下文内尝试访问。x1命中(存在.b.c),x2不命中(只有.b.e),因此$target只会保留x1。
示例:展开 + 聚合依然按源值分组
测试代码:
x1 = { b: 1, c: 2 }
x2 = { b: 1 }
SyntaxFlow 规则:
x*?{.*<len>==2} as $target
解释:
.*可能会把成员“展开”成多个中间值。<len>取的是“当前输入对象展开后的成员数量”,再与2比较。- 因此它会保留成员数为
2的x1,过滤掉x2。
使用实例与解释
通过具体的代码案例,结合 SyntaxFlow 的语法规则,可以更直观地理解 分析值筛选过滤 的应用。
案例一:审计 DocumentBuilderFactory 的配置以防止 XXE 漏洞
背景:XML 外部实体(XXE)攻击是一种常见的安全漏洞,通过未正确配置的 XML 解析器,攻击者可以读取或修改服务器上的敏感数据。DocumentBuilderFactory 类在解析 XML 时若未正确设置安全属性,可能会导致 XXE 漏洞。
审计代码示例
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
public class TestXXE {
public static void main(String[] args) throws Exception {
String xmlStr = "<!DOCTYPE foo [ <!ENTITY xxe SYSTEM \"file:///etc/passwd\" >]><foo>&xxe;</foo>";
// 这行代码将被下面的规则捕获
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
InputStream stream = new ByteArrayInputStream(xmlStr.getBytes("UTF-8"));
org.w3c.dom.Document doc = builder.parse(stream);
doc.getDocumentElement().normalize();
}
}
SyntaxFlow 规则案例
// 审计因为未设置 setFeature 等安全策略造成的 XXE 漏洞
desc(
"Title": "审计因为未设置 setFeature 等安全策略造成的XXE漏洞",
"Fix": "修复方案:需要用户设置 setFeature / setXIncludeAware / setExpandEntityReferences 等安全配置"
)
// 捕获创建 DocumentBuilderFactory 对象时的实例
$factories = DocumentBuilderFactory.newInstance();
// 过滤出未设置安全属性的 DocumentBuilderFactory 实例
$factories?{!((.setFeature) || (.setXIncludeAware) || (.setExpandEntityReferences))} as $entry;
// 追踪 $entry 中所有 Builder 对象的 parse 方法调用
$entry.*Builder().parse(* #-> as $source;
// 检查 parse 方法的调用源是否安全
check $source then "XXE Attack" else "XXE Safe";
执行效果
将上述规则保存为 xxe.sf,并使用以下命令进行审计:
yak ssa -t . --program xxe && yak sf --program xxe xxe.sf
输出示例:
[INFO] 2024-06-26 14:58:24 [ssacli:272] syntax flow query result:
rule md5 hash: f3dde2cbbb200606c3361adb0f276c0e
rule preview: desc( "Title": "审计因为未设置 setF...en "XXE Attack" else "XXE Safe";
description: {Title: "审计因为未设置 setFeature 等安全策略造成的XXE漏洞", Fix: "修复方案:需要用户设置 setFeature / setXIncludeAware / setExpandEntityReferences 等安全配置", $source: "XXE Attack"}
Result Vars:
factories:
t1325817: Undefined-DocumentBuilderFactory.newInstance(valid)()
XXE.java:17:65 - 17:78
entry:
t1325817: Undefined-DocumentBuilderFactory.newInstance(valid)()
XXE.java:17:65 - 17:78
source:
t1325822: Undefined-ByteArrayInputStream
XXE.java:18:33 - 18:79
...
解释
$factories = DocumentBuilderFactory.newInstance();:捕获DocumentBuilderFactory实例的创建,并存储在$factories中。$unsafeFactories = $factories?{!((.setFeature) || (.setXIncludeAware) || (.setExpandEntityReferences))} as $entry;:- 使用
?{...}结构过滤出未调用.setFeature、.setXIncludeAware或.setExpandEntityReferences方法的DocumentBuilderFactory实例。 - 将过滤后的结果存储在
$entry中。
- 使用
$entry.*Builder().parse(* #-> as $source;:追踪$entry中所有Builder对象调用parse方法,并将其参数存储在$source中。check $source then "XXE Attack" else "XXE Safe";:检查parse方法的调用源,若源头未被安全配置保护,则标记为 "XXE Attack",否则标记为安全。
通过这种方式,SyntaxFlow 能够精准地定位未正确配置 DocumentBuilderFactory 实例的 parse 方法调用,帮助开发和安全团队及时发现和修复潜在的 XXE 漏洞。
高级特性:根据参数筛选方法
我们经常会遇到这么一个需求:找到某个参数符合某种条件的方法调用。
例如:
在Java内置规则检测XXE漏洞的时候,有这么一条规则:"检测第一个参数为http://xml.org/sax/features/external-general-entities,第二个参数为false的XMLReader.setFeature方法"。
这种情况下,我们如何使用?{...}结构实现上述需求呢?
这种规则编写相较于其它过滤规则编写比较困难,原因是SyntaxFlow是比较"线性的",我们编写的时候一般是线性从左到右写规则。
但参数的获取是通过括号语法(...),而对括号里面参数进行条件过滤希望的又是影响到外面方法调用的结果。
这里的思维就是进去括号里面->获取参数->筛选参数->退出括号->通过参数筛选过滤方法调用,是非线性的。如下:
method( * ?{...} ) as $result
很明显可以发现,无论?{...} 的条件如何,$result的结果都不会变。
那么我们如何使用SyntaxFlow实现上述需求呢?
有一种方式是借助nativeCall来实现。
我们以上述检测XXE的需求为例子,使用NativeCall可以写成下面的形式:
XMLReader.setFeature()?{
<getActualParams><slice(index=1)>?{=="http://xml.org/sax/features/external-general-entities"}
&&
<getActualParams><slice(index=2)>?{==false}
} as $result
解释:
XMLReader.setFeature():表示我们要查找的目标方法。?{...}:用于筛选符合条件的参数。<getActualParams>:获取方法调用的实际参数的nativeCall。<slice(index=1)>:获取第一个参数。
可以发现,虽然借用nativeCall可以满足需求了,但是存在一定的弊端。
nativeCall的使用会增加规则的复杂度,可能导致规则的可读性下降。- 使用nativeCall无法继续使用逗号语法来切分参数,需要使用
<slice>来获取参数,这可能会导致规则的可维护性降低。
为了实现上述需求,我们引入新语法:?(...)
新语法:?(...)
?(...) 语法允许我们在方法调用中直接对参数进行筛选,简化了规则的编写。
如下:method的?()内调用了?{...}筛选语法来过滤参数,如果参数被过滤,那么其对应的方法调用也会被过滤掉。因此最后的$result是受到过滤条件影响的。
method?(*?{...}) as $result
我们还是以刚才XXE的规则为例,现在可以写成以下形式:
XMLReader.setFeature?(,*?{=="http://xml.org/sax/features/external-general-entities"},*?{==false}) as $result
解释:
XMLReader.setFeature?:表示我们要查找的目标方法。?(...):括号内是对“调用参数列表”的筛选逻辑;只要参数不满足,外层这个调用就会被过滤掉。- 第一个
,:用于跳过接收者(XMLReader实例)的位置,从而对第一个显式参数开始对齐。 *?{=="http://xml.org/sax/features/external-general-entities"}:约束第一个显式参数等于目标字符串。*?{==false}:约束第二个显式参数等于false。
Call-wide 与 Per-arg:两种常见语义
?(...) 内部的表达式,既可以表达“整次调用满足条件”(Call-wide),也可以表达“同一个参数位同时满足多个条件”(Per-arg)。
// Call-wide:参数集合里“存在一个 param”,并且“存在一个字符串 'a'”
a?(opcode:param && =="a") as $callWide
// Per-arg:必须存在“同一个参数位”同时满足 opcode:param 且等于 'a'
a?(*?{opcode:param && =="a"}) as $perArg
当你只想表达“参数列表中存在某类参数”时,a?(opcode:function) 与 a?(*?{opcode:function}) 等价。
常见用法:按参数数量筛选调用
// 参数数量等于 2
a?(*<len>==2) as $twoArgs
// 等价写法:先取 len 再用 ?{==2} 过滤
a?(*<len>?{==2}) as $twoArgs2
实战中的注意事项
在实际应用中,使用 分析值的筛选过滤 时需要注意以下几点:
1. 上下文敏感
理解当前代码的上下文非常重要,尤其是在处理复杂逻辑或大型代码库时。使用 -{}-> 运算符来设置适当的追踪参数,可以帮助维持追踪的可管理性。
2. 性能考虑
在大型项目中,使用 --> 或 #-> 可能会导致性能开销。建议在可能的情况下,限制追踪的深度或明确追踪的起始点和终点,以提高审计效率。
3. 追踪的精确性
使用运算符时,需要确保理解每个符号的具体含义,以便精确地捕捉到所需的数据流和定义点。避免误匹配和漏匹配。