0%

spotbugs源码学习&添加安全规则

因为帮我内推阿里的师傅告诉我以后可能要做源码审计的工作,先学习一下spotbugs和find-sec-bugs的扫描规则实现,并且尝试添加一个规则。

添加扫描规则——添加一个Detector

添加扫描规则主要是继承Detector,本文介绍以下几种主要的Detector:

OpcodeStackDetector

检查每一个Java虚拟机操作码(继承其中的sawOpcode(int seen)seen即操作吗int表示),其中可以获取调用的方法名——getNameConstantOperand(),获取调用者——getClassConstantOperand(),获取函数调用的参数——stack.getStackItem(0)。可以做类似于正则匹配的简单扫描工具。

例如扫描registry.addMapping.addMapping("/**").allowedOrigins("*")

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class CorsRegistryCORSDetector extends OpcodeStackDetector {
//...
@Override
public void sawOpcode(int seen) {
//检测调用方法名
if (seen == Const.INVOKEVIRTUAL && getNameConstantOperand().equals("allowedOrigins")) {
// 检测调用对象
if ("org/springframework/web/servlet/config/annotation/CorsRegistration".equals(getClassConstantOperand())) {
OpcodeStack.Item item = stack.getStackItem(0);
// 因为allowedOrigins参数时Strings... 所以不能直接提取而需要自己通过字节码提取
if(item.isArray()) {
String[] strings=getStringArray(item);
String pattern="*";
for (String s: strings) {
if (s.equals(pattern)) {
bugReporter.reportBug(new BugInstance(this, "PERMISSIVE_CORS", HIGH_PRIORITY)
.addClassAndMethod(this).addSourceLine(this));
break;
}
}
}
}
}
}
//...
}

以上是我为find-sec-bugs提交的一个真实的Detector,其中有一个坑就是allowedOrigins()方法的参数是变长参数(实际上是一个数组),如果参数是String或是定长参数的话,直接用stack.getStackItem(0)就可以拿到参数了,现在的话就需要自己写getStringArray(item)方法,具体解决代码见find-sec-bugs#472

BasicInjectionDetector

该Detector以每次调用(invoke)为单位进行代码审计,通过污点传播技术,判断调用敏感函数时判断参数是否为用户可控(可以参考com.h3xstream.findsecbugs.file.PathTraversalDetector.java)

也可继承后重写getInjectionPoint()和getPriorityFromTaintFrame()方法,直接判断是否存在调用以及判断风险等级,这时退化成OpcodeStackDetector,例如,我们要检测CORS漏洞:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class CorsRegistryCORSDetector extends BasicInjectionDetector {

private static final String PERMISSIVE_CORS = "PERMISSIVE_CORS";
private static final String CORS_REGISTRY_CLASS = "org.springframework.web.servlet.config.annotation.CorsRegistration";
// 需要获取函数原型
private static final InvokeMatcherBuilder CORS_REGISTRY_ALLOWED_ORIGINS_METHOD = invokeInstruction()
.atClass(CORS_REGISTRY_CLASS).atMethod("allowedOrigins")
.withArgs("([Ljava/lang/String;)Lorg/springframework/web/servlet/config/annotation/CorsRegistration;");


public CorsRegistryCORSDetector(BugReporter bugReporter) {
super(bugReporter);
}

/**
* 每次调用时都会用该函数判断是否存在漏洞
* invoke:表示一次调用
*/
@Override
protected InjectionPoint getInjectionPoint(InvokeInstruction invoke, ConstantPoolGen cpg,
InstructionHandle handle) {
assert invoke != null && cpg != null;
// 可以通过一下方法获取InvokeMatcherBuilder的class、method、Signature
// System.out.println(invoke.getClassName(cpg));
// System.out.println(invoke.getMethodName(cpg));
// System.out.println(invoke.getSignature(cpg));
if (CORS_REGISTRY_ALLOWED_ORIGINS_METHOD.matches(invoke, cpg)) {
return new InjectionPoint(new int[] { 0 }, PERMISSIVE_CORS);
}
return InjectionPoint.NONE;
}

/**
* 返回危险等级
*/
@Override
protected int getPriorityFromTaintFrame(TaintFrame fact, int offset) throws DataflowAnalysisException {
// Get the value of the Access-Control-Allow-Origin parameter (Second argument from setHeader(2nd,1rst))
Taint originsTaint= fact.getStackValue(0);

if (originsTaint.getConstantOrPotentialValue().contains("*")) { //Ignore unknown/dynamic header name
return Priorities.HIGH_PRIORITY;
} else {
return Priorities.IGNORE_PRIORITY;
}
}
}

注意这里getPriorityFromTaintFrame()方法写的是有问题的,还是因为变长参数问题,导致originsTaint.getConstantOrPotentialValue()只能得到数组长度却不能拿到内容。

这里第二个难点就是获取函数原型,可以考虑是用javap:

1
javap -cp C:\Users\x5651\.m2\repository\org\springframework\spring-webmvc\5.1.6.RELEASE\spring-webmvc-5.1.6.RELEASE.jar -s org.springframework.web.servlet.config.annotation.CorsRegistration

增加一个简单的污点传播规则

一个简单的污点传播只需要定义sink点和priority就行了,以命令注入的规则为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class CommandInjectionDetector extends BasicInjectionDetector {

public CommandInjectionDetector(BugReporter bugReporter) {
super(bugReporter);
loadConfiguredSinks("command.txt", "COMMAND_INJECTION");
loadConfiguredSinks("command-scala.txt", "SCALA_COMMAND_INJECTION");
}

@Override
protected int getPriority(Taint taint) {
if (!taint.isSafe() && taint.hasTag(Taint.Tag.COMMAND_INJECTION_SAFE)) {
return Priorities.IGNORE_PRIORITY;
} else {
return super.getPriority(taint);
}
}
}