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 污点分析三要素

  1. Source(源点):用户可控输入
  2. Sink(汇点):危险函数调用
  3. 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 关键点回顾

  1. SSRF漏洞可以通过多种Java HTTP客户端库实现
  2. CodeQL分析需要正确定义Source、Sink和污点传播规则
  3. 特殊方法调用(如String.valueOf)需要额外处理
  4. 清理点(Sanitizer)规则影响漏洞检测结果

5.2 最佳实践

  1. 针对项目使用的HTTP客户端库定制Sink点
  2. 仔细检查污点传播路径,特别是方法调用链
  3. 合理配置清理点规则,避免误报
  4. 定期更新CodeQL规则库以支持新的API和框架

5.3 扩展思考

  1. 可以扩展支持更多HTTP客户端库(如Unirest、Retrofit等)
  2. 考虑添加对DNS查询、SMTP连接等其他网络操作的检测
  3. 结合数据流分析和控制流分析提高检测精度
CodeQL自动化寻找SSRF漏洞教学文档 1. 环境搭建 1.1 项目准备 下载示例项目: https://github.com/l4yn3/micro_service_seclab 将项目导入IDEA开发环境 运行项目进行测试 2. SSRF漏洞代码分析 2.1 五种SSRF实现方式 2.1.1 HttpURLConnection方式 使用Java标准库中的 HttpURLConnection 直接使用用户提供的URL创建连接 发送GET请求并返回响应 2.1.2 Apache HttpClient方式 使用Apache HttpClient的fluent API 用户提供的URL用于发起GET请求 2.1.3 OkHttp方式 使用OkHttp现代HTTP客户端 构建请求并执行 2.1.4 DefaultHttpClient方式 使用Apache HttpClient中的DefaultHttpClient类 创建HttpGet对象并执行 2.1.5 URL.openStream方式 使用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 远程用户输入的数据流源 实现类包括SpringServletInputParameterSource等 3.2.2 SpringServletInputParameterSource 匹配Spring请求映射方法的参数 通过 isTaintedInput() 判断是否为污点输入 3.2.3 排除的Source 排除通过HttpURLConnection.getInputStream方法发起的远程请求 这些通常被认为是安全的HTTPS请求 3.3 Sink点分析 3.3.1 RequestForgerySink 抽象类,表示请求伪造的汇点 3.3.2 DefaultRequestForgerySink 实现类,标记为"request-forgery"类型 匹配各种HTTP客户端库的请求方法 3.4 污点传播规则 3.4.1 isAdditionalTaintStep 定义额外的污点传播步骤 3.4.2 DefaultRequestForgeryAdditionalTaintStep 处理URI和URL构造时的污点传播 示例: 3.4.3 TypePropertiesRequestForgeryAdditionalTaintStep 处理Properties.setProperty方法的污点传播 示例: 3.5 Sanitizer(清理点) 3.5.1 HostnameSantizer 匹配包含特定前缀的字符串(如?或#) 这些前缀限制了主机名的控制 3.5.2 RelativeUrlSanitizer 检查URL是否为相对路径 示例: 4. CodeQL实践与改进 4.1 初始查询结果问题 原始查询只能检测到3个SSRF漏洞点(实际有5个) 问题出在污点传播过程中,特别是String.valueOf方法的处理 4.2 解决方案:扩展污点传播规则 4.2.1 定义StringValue类 匹配java.lang.String类的valueOf方法 4.2.2 修改DefaultRequestForgeryAdditionalTaintStep 添加对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连接等其他网络操作的检测 结合数据流分析和控制流分析提高检测精度