Codeql 目录穿越实战
字数 1453 2025-08-22 12:23:30
CodeQL目录穿越漏洞检测实战教学文档
1. 前言
CodeQL是一种强大的语义代码分析引擎,可以自动化检测代码中的安全漏洞。本教程将详细介绍如何使用CodeQL检测目录穿越(Directory Traversal)漏洞,以ofcms 1.1.3版本为例进行实战演示。
2. 环境搭建
2.1 下载和部署ofcms
- 下载ofcms 1.1.3版本源码
- 配置Tomcat容器部署应用
- 可能遇到文件权限问题,需设置文件安全权限
- 右键文件 → 属性 → 安全 → 设置所有权限
2.2 数据库配置
- 创建名为
ofcms的数据库 - 导入SQL文件初始化数据库结构
- 修改数据库配置文件:
- 文件路径:
ofcms-V1.1.3/ofcms-admin/src/main/resources/dev/conf/db-config.properties - 修改为正确的数据库用户名和密码
- 重命名为
db.properties
- 文件路径:
- 重启Web容器
2.3 验证安装
访问应用,确认安装成功。后台管理界面位于特定路径。
3. 目录穿越漏洞复现
- 漏洞位置:模板文件操作功能
- 复现步骤:
- 点击保存模板文件
- 拦截请求包
- 修改
filename参数,添加../路径遍历序列 - 观察结果,确认文件被写入非预期目录
4. 漏洞代码分析
4.1 漏洞代码位置
在save方法中,存在以下可控参数:
- 文件内容(content)
- 文件名(filename)
4.2 漏洞产生原因
未对用户提供的文件名参数进行充分验证和过滤,导致可以包含路径遍历序列(../),从而将文件写入任意目录。
5. CodeQL实践
5.1 初始尝试
直接使用官方提供的CodeQL查询可能无法检测到此漏洞,因为:
- 默认的source点定义不包含特定框架(JFinal)的参数入口
- 需要自定义source点以匹配实际应用架构
5.2 Sink点分析
- 关键sink点是文件写入操作:
FileUtils.writeString(new File(path), content); - 在CodeQL中可表示为:
class FileWriteSink extends MethodAccess { FileWriteSink() { this.getMethod().hasName("writeString") and this.getMethod().getDeclaringType().hasQualifiedName("org.apache.commons.io", "FileUtils") } }
5.3 Source点分析
-
在JFinal框架中,参数通常通过getter方法获取
-
需要识别框架特定的参数入口点:
- JFinal Controller的getter方法
- Servlet API参数
- 自定义路由参数
-
自定义source点示例:
class JFinalSource extends RemoteFlowSource { JFinalSource() { exists(Method m | m.getDeclaringType().getASupertype*().hasQualifiedName("com.jfinal.core", "Controller") and (m.hasName("getPara") or m.hasName("getModel") or m.hasName("getBean")) ) } }
5.4 完整查询示例
import java
import semmle.code.java.dataflow.FlowSources
import semmle.code.java.dataflow.TaintTracking
class FileWriteSink extends MethodAccess {
FileWriteSink() {
this.getMethod().hasName("writeString") and
this.getMethod().getDeclaringType().hasQualifiedName("org.apache.commons.io", "FileUtils")
}
}
class JFinalSource extends RemoteFlowSource {
JFinalSource() {
exists(Method m |
m.getDeclaringType().getASupertype*().hasQualifiedName("com.jfinal.core", "Controller") and
(m.hasName("getPara") or m.hasName("getModel") or m.hasName("getBean"))
)
}
}
class DirectoryTraversalConfiguration extends TaintTracking::Configuration {
DirectoryTraversalConfiguration() { this = "DirectoryTraversalConfiguration" }
override predicate isSource(DataFlow::Node source) {
source instanceof RemoteFlowSource
}
override predicate isSink(DataFlow::Node sink) {
exists(FileWriteSink fws |
sink.asExpr() = fws.getArgument(0) or
sink.asExpr() = fws.getArgument(1)
)
}
}
from DirectoryTraversalConfiguration cfg, DataFlow::PathNode source, DataFlow::PathNode sink
where cfg.hasFlowPath(source, sink)
select sink, source, sink, "Potential directory traversal vulnerability"
6. 查询思路分析
6.1 Sink点扩展
除了文件写入操作,还可以考虑:
- 文件读取操作
- 文件删除操作
- 文件重命名操作
6.2 Source点扩展
- 框架特定source点:
- JFinal Controller
- Spring MVC Controller
- Struts Action
- Servlet API source点:
- HttpServletRequest参数
- 请求头
- Cookie值
- 自定义路由source点
6.3 完整框架支持
对于完整的企业级应用,需要构建更全面的source点模型:
- 识别应用使用的框架
- 为每个框架定义特定的参数入口点
- 考虑框架间的交互和参数传递
7. 优化建议
-
添加路径净化检查:
predicate isSanitizer(DataFlow::Node node) { exists(MethodAccess ma | ma.getMethod().hasName("normalize") and ma.getMethod().getDeclaringType().hasQualifiedName("org.apache.commons.io", "FilenameUtils") and node.asExpr() = ma ) } -
添加对特定危险字符的检查:
override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) { exists(StringLiteral sl | sl.getStringValue().regexpMatch(".*\\.\\./.*") and pred.asExpr() = sl and succ.asExpr().getEnclosingCallable() = pred.asExpr().getEnclosingCallable() ) }
8. 总结
通过本教程,我们学习了:
- 如何搭建和复现目录穿越漏洞
- CodeQL检测目录穿越的基本原理
- 如何自定义source和sink点以适应特定框架
- 如何构建完整的污点传播分析
关键要点:
- 理解应用的框架结构对定义source点至关重要
- 需要全面考虑所有可能的文件操作sink点
- 路径净化检查是减少误报的重要手段
- 特定框架需要特定的查询策略