codeql 之 SSRF 漏洞自动化 寻找
字数 2040 2025-08-22 12:23:19
CodeQL自动化寻找SSRF漏洞教学文档
1. 环境搭建
1.1 项目准备
- 下载示例项目:
https://github.com/l4yn3/micro_service_seclab - 将项目导入IDEA开发环境
- 运行项目进行测试
2. SSRF漏洞代码分析
2.1 五种SSRF实现方式
2.1.1 HttpURLConnection方式
@RequestMapping(value = "/one")
public String One(@RequestParam(value = "url") String imageUrl) {
try {
URL url = new URL(imageUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
return connection.getResponseMessage();
} catch (IOException var3) {
System.out.println(var3);
return "Hello";
}
}
- 使用Java标准库中的
HttpURLConnection - 直接使用用户提供的URL创建连接
- 发送GET请求并返回响应
2.1.2 Apache HttpClient方式
@RequestMapping(value = "/two")
public String Two(@RequestParam(value = "url") String imageUrl) {
try {
URL url = new URL(imageUrl);
HttpResponse response = Request.Get(String.valueOf(url)).execute().returnResponse();
return response.toString();
} catch (IOException var1) {
System.out.println(var1);
return "Hello";
}
}
- 使用Apache HttpClient的fluent API
- 用户提供的URL用于发起GET请求
2.1.3 OkHttp方式
@RequestMapping(value = "/three")
public String Three(@RequestParam(value = "url") String imageUrl) {
try {
URL url = new URL(imageUrl);
OkHttpClient client = new OkHttpClient();
com.squareup.okhttp.Request request = new com.squareup.okhttp.Request.Builder()
.get().url(url).build();
Call call = client.newCall(request);
Response response = call.execute();
return response.toString();
} catch (IOException var1) {
System.out.println(var1);
return "Hello";
}
}
- 使用OkHttp现代HTTP客户端
- 构建请求并执行
2.1.4 DefaultHttpClient方式
@RequestMapping(value = "/four")
public String Four(@RequestParam(value = "url") String imageUrl) {
try {
DefaultHttpClient client = new DefaultHttpClient();
HttpGet get = new HttpGet(imageUrl);
HttpResponse response = client.execute(get);
return response.toString();
} catch (IOException var1) {
System.out.println(var1);
return "Hello";
}
}
- 使用Apache HttpClient中的DefaultHttpClient类
- 创建HttpGet对象并执行
2.1.5 URL.openStream方式
@RequestMapping(value = "five")
public String Five(@RequestParam(value = "url") String imageUrl) {
try {
URL url = new URL(imageUrl);
InputStream inputStream = url.openStream();
return String.valueOf(inputStream.read());
} catch (IOException var1) {
System.out.println(var1);
return "Hello";
}
}
- 使用URL.openStream()方法
- 直接打开URL并读取数据流
3. CodeQL分析原理
3.1 核心概念
3.1.1 RequestForgeryConfig
- 描述请求伪造风险的污点跟踪配置
- SSRF属于request-forgery风险类型
3.1.2 污点分析三要素
- Source(源点):用户可控输入
- Sink(汇点):危险函数调用
- Taint propagation(污点传播):数据从源点到汇点的流动路径
3.2 Source点分析
3.2.1 RemoteFlowSource
abstract class RemoteFlowSource extends SourceNode {
abstract string getSourceType();
override string getThreatModel() { result = "remote" }
}
- 远程用户输入的数据流源
- 实现类包括SpringServletInputParameterSource等
3.2.2 SpringServletInputParameterSource
private class SpringServletInputParameterSource extends RemoteFlowSource {
SpringServletInputParameterSource() {
this.asParameter() = any(SpringRequestMappingParameter srmp | srmp.isTaintedInput())
}
override string getSourceType() { result = "Spring servlet input parameter" }
}
- 匹配Spring请求映射方法的参数
- 通过
isTaintedInput()判断是否为污点输入
3.2.3 排除的Source
not source.asExpr().(MethodCall).getCallee() instanceof UrlConnectionGetInputStreamMethod
- 排除通过HttpURLConnection.getInputStream方法发起的远程请求
- 这些通常被认为是安全的HTTPS请求
3.3 Sink点分析
3.3.1 RequestForgerySink
abstract class RequestForgerySink extends DataFlow::Node { }
- 抽象类,表示请求伪造的汇点
3.3.2 DefaultRequestForgerySink
private class DefaultRequestForgerySink extends RequestForgerySink {
DefaultRequestForgerySink() { sinkNode(this, "request-forgery") }
}
- 实现类,标记为"request-forgery"类型
- 匹配各种HTTP客户端库的请求方法
3.4 污点传播规则
3.4.1 isAdditionalTaintStep
override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) {
any(RequestForgeryAdditionalTaintStep r).propagatesTaint(pred, succ)
}
- 定义额外的污点传播步骤
3.4.2 DefaultRequestForgeryAdditionalTaintStep
private class DefaultRequestForgeryAdditionalTaintStep extends RequestForgeryAdditionalTaintStep {
override predicate propagatesTaint(DataFlow::Node pred, DataFlow::Node succ) {
// propagate to a URI when its host is assigned to
exists(UriCreation c | c.getHostArg() = pred.asExpr() | succ.asExpr() = c)
or
// propagate to a URL when its host is assigned to
exists(UrlConstructorCall c | c.getHostArg() = pred.asExpr() | succ.asExpr() = c)
}
}
- 处理URI和URL构造时的污点传播
- 示例:
String userInput = request.getParameter("url"); URI uri = URI.create(userInput); // 污点从userInput传播到uri URL url = new URL(userInput); // 污点从userInput传播到url
3.4.3 TypePropertiesRequestForgeryAdditionalTaintStep
private class TypePropertiesRequestForgeryAdditionalTaintStep extends RequestForgeryAdditionalTaintStep {
override predicate propagatesTaint(DataFlow::Node pred, DataFlow::Node succ) {
exists(MethodCall ma |
ma.getMethod() instanceof PropertiesSetPropertyMethod
and ma.getArgument(0).(CompileTimeConstantExpr).getStringValue() = "jdbcUrl"
and pred.asExpr() = ma.getArgument(1)
and succ.asExpr() = ma.getQualifier()
)
}
}
- 处理Properties.setProperty方法的污点传播
- 示例:
Properties props = new Properties(); String userInput = request.getParameter("jdbcUrl"); props.setProperty("jdbcUrl", userInput); // 污点从userInput传播到props
3.5 Sanitizer(清理点)
3.5.1 HostnameSantizer
private class HostnameSantizer extends RequestForgerySanitizer {
HostnameSantizer() {
this.asExpr() = any(HostnameSanitizingPrefix hsp).getAnAppendedExpression()
}
}
- 匹配包含特定前缀的字符串(如?或#)
- 这些前缀限制了主机名的控制
3.5.2 RelativeUrlSanitizer
private class RelativeUrlSanitizer extends RequestForgerySanitizer {
RelativeUrlSanitizer() {
this = DataFlow::BarrierGuard<isRelativeUrlSanitizer/3>::getABarrierNode()
}
}
- 检查URL是否为相对路径
- 示例:
if (uri.isAbsolute()) { // 绝对URL被认为是安全的 return true; }
4. CodeQL实践与改进
4.1 初始查询结果问题
- 原始查询只能检测到3个SSRF漏洞点(实际有5个)
- 问题出在污点传播过程中,特别是String.valueOf方法的处理
4.2 解决方案:扩展污点传播规则
4.2.1 定义StringValue类
class TypeStringLib extends RefType {
TypeStringLib() { this.hasQualifiedName("java.lang", "String") }
}
class StringValue extends MethodAccess {
StringValue(){
this.getCallee().getDeclaringType() instanceof TypeStringLib
and this.getCallee().hasName("valueOf")
}
}
- 匹配java.lang.String类的valueOf方法
4.2.2 修改DefaultRequestForgeryAdditionalTaintStep
private class DefaultRequestForgeryAdditionalTaintStep extends RequestForgeryAdditionalTaintStep {
override predicate propagatesTaint(DataFlow::Node pred, DataFlow::Node succ) {
exists(UriCreation c | c.getHostArg() = pred.asExpr() | succ.asExpr() = c)
or
exists(UrlConstructorCall c | c.getHostArg() = pred.asExpr() | succ.asExpr() = c)
or
exists(StringValue c | c.getArgument(0) = pred.asExpr() | succ.asExpr() = c)
}
}
- 添加对String.valueOf方法的污点传播支持
- 确保类似
Request.Get(String.valueOf(url))的调用也能被正确追踪
4.3 改进后查询结果
- 成功检测到全部5个SSRF漏洞点
- 包括使用String.valueOf方法包装URL参数的案例
5. 总结
5.1 关键点回顾
- SSRF漏洞可以通过多种Java HTTP客户端库实现
- CodeQL分析需要正确定义Source、Sink和污点传播规则
- 特殊方法调用(如String.valueOf)需要额外处理
- 清理点(Sanitizer)规则影响漏洞检测结果
5.2 最佳实践
- 针对项目使用的HTTP客户端库定制Sink点
- 仔细检查污点传播路径,特别是方法调用链
- 合理配置清理点规则,避免误报
- 定期更新CodeQL规则库以支持新的API和框架
5.3 扩展思考
- 可以扩展支持更多HTTP客户端库(如Unirest、Retrofit等)
- 考虑添加对DNS查询、SMTP连接等其他网络操作的检测
- 结合数据流分析和控制流分析提高检测精度