文章总结: 本文以WebGoat靶场为例,演示使用CodeQL进行代码审计的全流程。通过定位Spring控制器路由、追踪用户输入参数、分析认证逻辑调用链,最终发现会话ID生成依赖静态long字段自增+时间戳拼接的模式,导致攻击者可通过枚举预测有效SessionID。文章提供完整的QL查询语句实现漏洞检测。 综合评分: 85 文章分类: 代码审计,漏洞分析,WEB安全,安全工具,技术标准
【代码审计】CodeQL示例一
原创
十月的进阶之路 十月的进阶之路
十月的进阶之路
2026年4月19日 22:12 重庆
在小说阅读器读本章
去阅读
千行代码稳如狗,一洞破防全白搭。
目录
- 定位入口
- 确定输入参数
- 确认敏感调用点之谁在做认证
- 从调用点到函数实现
- 在实现里找真正的漏洞模式
1、定位入口
本文依旧基于WebGoat靶场,由于我们之前已经手动测试过了,很多信息我们熟知于心,已经知道该站点提交数据到接口/HijackSession/login进行处理,因此我们先通过ql定位该接口在代码中的位置,我们先理清如下条件:
- 它是一个
Spring控制器里的路由方法。 - 它的注解里有
path属性。 - 其中路由
path的值必须是/HijackSession/login,并将这个方法本身和它所在的控制器类找出来。
那么我们可编写出如下的ql检索语句。
import java
import semmle.code.java.frameworks.spring.SpringController
from
SpringControllerMethod m,Annotation a,string p
where
m.getADeclaredAnnotation() = a and
a.getAStringArrayValue("path") = p and
p = "/HijackSession/login"
select
m,m.getDeclaringType()
上述符号解释如下:
// 库导入
import java; // 标准Java分析库
import semmle.code.java.frameworks.spring.SpringController; // Spring专用模型库
// 变量类型说明
// SpringControllerMethod: Spring控制器路由方法
// Annotation: 注解实例
// string: 路径字符串
// 核心谓词作用
m.getADeclaredAnnotation() // 获取方法m上直接声明的注解
a.getAStringArrayValue("path") // 获取注解a中"path"属性的字符串数组值
m.getDeclaringType() // 获取方法m所属的类/接口
运行结果如下。
并且通过这一步我们精确定位到了代码位置。
2、确定输入参数
我们接着编写如下的代码:
import java
import semmle.code.java.frameworks.spring.SpringController
from
SpringRequestMappingMethod m,SpringRequestMappingParameter p
where
m.getName() = "login" and
m.getARequestParameter() = p and
p.isTaintedInput()
select
m,p
上述符号含义:
// ================== 字符含义 ==================
// SpringRequestMappingMethod m: Spring路由方法
// SpringRequestMappingParameter p: Spring路由参数
// ================== 核心谓词与检索条件 ==================
m.getName() // 获取方法名,限定为 "login"
m.getARequestParameter() // 获取方法m的请求参数,关联到p
p.isTaintedInput() // 判断参数p是否为用户可控的污点输入
我们定位到如下的内容,当然这里面包含着不同的其他关卡的login以及来自于外部用户可控的输入,可以通过继续添加条件来进一步精确内容,譬如通过唯一值的注解路由确定。
当然我们希望检索的内容也赫然在列,其中username,``password,``cookieValue均为外部输入,也就是所谓的污点输入。不过也不是所有内容都值得去追,谁进入了认证分支,谁就值得继续追进。
3、确认敏感调用点之谁在做认证
接下来查的是调用点,不是实现点,先锁定login方法内部所有调用,再把目标缩到目标方法,先给出检索的条件:
- 方法名
login - 调用发生在
login方法内部 - login方法所属的类名
HijackSessionAssignment已经通过第二步已经得到。
结合条件给出如下的参考代码。
import java
from
Method m,MethodCall c
where
m.getName() = "login" and
c.getEnclosingCallable() = m and
m.getDeclaringType().getName() = "HijackSessionAssignment"
select
c,c.getArgument(0)
其中详细的解释如下。
// ================== 核心谓词作用 ==================
m.getName() // 获取方法名称
c.getEnclosingCallable() // 获取包含该调用的外层方法
m.getDeclaringType().getName()// 获取方法所属类的类名
c.getArgument(0) // 获取方法调用的第1个参数
上述检索语句运行结果如下。
同时通过vscode的跳转功能也能直接定位到代码点,如下图所示。
当然又可以根据上述检索结果进一步完善检索语句获得更加精准的结果,譬如这里我们只想检索authenticate函数,因为本关卡的漏洞点就在这个函数中,因此我们可以给出如下的检索语句。
import java
from
Method m,MethodCall c
where
m.getName() = "login" and
c.getEnclosingCallable() = m and
c.getMethod().getName() = "authenticate"
select
c,c.getArgument(0)
这样我们就能直接定位到我们在意的authenticate函数位置。
但是匹配了两处,其实我们知道只需要匹配else分支中的调用,此时需要利用codeql的抽象语法树(AST)api,判断MethodCall是否位于IfStmt(If语句)的else分支内,先分析条件:
- 定位类中名为
login的方法。 - 筛选
login方法内对authenticate的调用。 - 仅保留位于
if-else语句else分支中的调用。 - 输出符合条件的调用语句及其第一个参数。
参考如下代码。
import java
from
Method m,MethodCall c,IfStmt ifs,Stmt s
where
m.getName() = "login" and
c.getEnclosingCallable() = m and
c.getMethod().getName() = "authenticate" and
c.getAnEnclosingStmt() = s and
ifs.getElse() = s
select
c,c.getArgument(0)
运行结果如下,通过AST我们精准的定位到了目标函数。
4、从调用点到函数实现
先把入口方法里的authenticat调用抓出来,再拿到它指向的目标callable,最后审这个callable的内部逻辑,MethodCall.getMethod()可以直接得到目标方法。
import java
from
Method m,MethodCall c,IfStmt ifs,Stmt s
where
m.getName() = "login" and
c.getEnclosingCallable() = m and
c.getMethod().getName() = "authenticate" and
c.getAnEnclosingStmt() = s and
ifs.getElse() = s
select
c.getMethod(),c.getMethod().getDeclaringType(),c.getArgument(0)
通过上述代码即可定位到具体的实现,与前一小节没什么区别。
5、在实现里找真正的漏洞模式
这里实现的关键逻辑分为两段,如果authentication.getId()不为空,并且生成的userid在sessions队列里,那么就直接设置setAuthenticated(true)。如果authentication.getId()为空,就分配一个新id,但这个id的生成方式包含Random.nextLong()、递增计数和Instant.now().toEpochMilli(),按照如下条件查询。
- 查找代码库中名为
authenticate的方法 - 筛选同时包含
contains成员检查和setAuthenticated直接设权的方法 - 输出符合条件的方法及预设的安全审计说明字符串
编写的参考代码如下。
import java
from
Method auth
where
auth.getName() = "authenticate" and
exists(
MethodCall c|
c.getEnclosingCallable() = auth and
c.getMethod().getName() = "contains"
) and
exists(
MethodCall c|
c.getEnclosingCallable() = auth and
c.getMethod().getName() = "setAuthenticated"
)
select
auth,"authentication depends on membership check + direct auth flag"
部分谓词(Predicate)说明如下
// ================== 核心谓词作用 ==================
auth.getName() // 获取方法名称
exists(...) // 判断括号内的条件是否至少存在一个匹配
c.getEnclosingCallable() // 获取包含调用c的外层方法
c.getMethod().getName() // 获取被调用方法的名称
通过上述规则,我们能直接定位到集合命中就认证的内部认证策略上。
当然这个漏洞的核心逻辑是++id使得每次调用GENERATE_SESSION_ID生成的值都比上一次大1,即便拼接了时间戳,攻击者只需知道任意一个有效session ID就能枚举出其他值,先给出检测逻辑:
- 查找代码库中的静态
long类型字段。 - 筛选对该字段进行前置自增的操作。
- 筛选该自增操作直接参与加法的场景。
- 输出符合条件的加法表达式及
session ID可预测的安全风险提示。
编写查询规则如下。
import java
from AddExpr add, PreIncExpr inc, Field f
where
f.isStatic() and
f.getType().hasName("long") and
inc.getOperand().(FieldAccess).getField() = f and
(
add.getLeftOperand() = inc or
add.getRightOperand() = inc
)
select add, "静态 long 字段 '" + f.getName() + "' 自增后拼接,session ID 可预测"
部分谓词含义如下。
// ================== 核心谓词作用 ==================
f.isStatic() // 判断字段是否为静态
f.getType().hasName("long") // 获取字段类型并判断是否为long
inc.getOperand().(FieldAccess).getField() // 获取前置自增操作的字段
add.getLeftOperand() / add.getRightOperand() // 获取加法的左右操作数
f.getName()
通过运行该规则可以直接检测到本关卡的核心漏洞点。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:十月的进阶之路 十月的进阶之路 十月的进阶之路《【代码审计】CodeQL示例一》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论