Java 代码审计实战速查(二):接口清单

上一篇把依赖和配置过完了,产出是一张"重点跟进清单"。这一篇进代码。

进代码第一件事不是看业务,是把接口全找出来。

原因很直接:审计本质是逐条检查"外部输入能不能变成危险操作"。如果连外部输入有多少个入口都不清楚,就只能靠运气翻代码。一套若依二开系统的业务接口在 200 到 400 个之间,翻是翻不完的,只能按清单走。

接口梳理的目标是拿到一张表,每条记录四个字段:

HTTP 方法 | 路径 | 处理方法 | 权限注解

第四个字段决定了这条接口是"要登录才能打"还是"匿名就能打"。做越权审计的时候,这一列就是全部。


0x01 有源码能编译:搜注解

最直接的办法,也是最多人用的办法。但有一个坑。

只搜 @RequestMapping 会漏一半

@RestController
@RequestMapping("/system/user")
public class SysUserController extends BaseController {

    @PreAuthorize("@ss.hasPermi('system:user:list')")
    @GetMapping("/list")
    public TableDataInfo list(SysUser user) {
        startPage();
        List<SysUser> list = userService.selectUserList(user);
        return getDataTable(list);
    }

    @PreAuthorize("@ss.hasPermi('system:user:export')")
    @PostMapping("/export")
    public void export(HttpServletResponse response, SysUser user) {
        List<SysUser> list = userService.selectUserList(user);
        ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class);
        util.exportExcel(response, list, "用户数据");
    }

    @PreAuthorize("@ss.hasPermi('system:user:remove')")
    @DeleteMapping("/{userIds}")
    public AjaxResult remove(@PathVariable Long[] userIds) {
        return toAjax(userService.deleteUserByIds(userIds));
    }
}

这里有三个问题。

第一,路径是拼出来的。 类上是 /system/user,方法上是 /list,实际路由是 /system/user/list。只搜方法级注解的话,你拿到的是 /list,然后要在脑子里把前缀补回去——两百个接口靠脑子补,一定会错。搜的时候必须把类级 @RequestMapping 一起抓出来。

第二,注解有五个,不是一个。

@RequestMapping、@GetMapping、@PostMapping、@PutMapping、@DeleteMapping、@PatchMapping,再加上 JAX-RS 的 @Path/@GET、WebFlux 的 @RequestMapping。少搜一个就是漏。

第三,@PreAuthorize 决定了这条接口谁能访问。

上面三条接口的注解值分别是 system:user:list、system:user:export、system:user:remove。没有 @PreAuthorize 的接口,只要登录就能调用,不管对方是什么角色。 这是越权审计里最直接的一类问题:翻一遍接口清单,看看有没有本该限管理员的功能忘了加权限注解。

RuoYi-Vue 里还有一个 @Anonymous:

@Anonymous
@GetMapping("/captchaImage")
public AjaxResult getCode(HttpServletResponse response) throws IOException { ... }

这个注解是显式声明"不需要登录"的标记。搜出来,一条条看。 这是免认证接口的完整清单,也是越权审计的第一优先级。

搜的时候用组合条件

grep -rnE "@(Get|Post|Put|Delete|Patch|Request)Mapping" --include=*.java . \
  | grep -v "^\s*//" > routes_raw.txt

# 单独把类级前缀捞出来
grep -rnE "^@RequestMapping" --include=*.java .

搜完记得排掉被注释掉的行。老项目里注释掉的接口不少,注释掉的代码不会注册路由,但会污染你的清单。


0x02 断点法:一次拿全量路由

搜注解是文本层面的猜,框架自己有准确的注册表。用框架的。

Spring MVC 启动时,RequestMappingHandlerMapping 会扫描所有带 @Controller / @RestController 的 bean,把每个方法的路由信息注册进自己的 MappingRegistry。这个注册表就是权威答案,包含所有注解形式声明的路由。

拿它有两种方式。

方式一:Actuator

management:
  endpoints:
    web:
      exposure:
        include: "*"

启动后访问 /actuator/mappings,返回的 JSON 里 contexts.*.mappings.dispatcherServlets.dispatcherServlet 下面就是全量路由。

这个办法不用调试,但有两个前提:Actuator 得引了,端点得暴露。实际项目里 Actuator 常常没引,所以更通用的是方式二。

方式二:在调试器里读

应用跑起来,在任意一个 Controller 方法上打断点,触发一次请求让断点命中,然后在 IDE 的 Evaluate Expression 里执行:

org.springframework.web.context.support.WebApplicationContextUtils
    .getWebApplicationContext(request.getServletContext())
    .getBean("requestMappingHandlerMapping",
             org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping.class)
    .getHandlerMethods()
    .keySet()

request 是断点所在方法的参数,作用域里有。IDEA 的 Evaluate Expression 支持全限定名,不用额外 import。

返回值打印出来长这样:

[{GET /system/user/list}, {POST /system/user/export}, {DELETE /system/user/{userIds}},
 {GET /common/download/resource}, {POST /monitor/job/run}, ...]

HttpMethod、路径、路径变量全都对,不需要自己拼接。

如果项目启动不起来,或者只有编译好的包,可以在启动阶段下断点:AbstractHandlerMethodMapping.initHandlerMethods()。这个方法在容器启动时逐条注册路由,命中断点后同样 evaluate getHandlerMethods(),只是这时候拿到的可能不是最终结果,需要放行到注册结束。

还有一种情况:只要路径不要方法名

用 RuoYi 这类项目,接口数量多,可以用 IDEA Ultimate 的 Endpoints 工具窗口(View → Tool Windows → Endpoints)。它直接列出 Spring MVC 的映射,支持按路径搜索和跳转源码,比在调试器里翻方便。Community 版没有这个窗口。


0x03 只有 jar 或 war,没有源码

企业交付里很常见:给你一个 war,或者一个跑着的服务。

先反编译,再搜

# 解压
unzip -o app.war -d app_extracted

# CFR 批量反编译,直接吃 jar/war
java -jar cfr.jar app.war --outputdir src_decompiled

CFR、Procyon、JD-Core 都能用。IDEA 直接把 jar 拖进去也会自动反编译,而且能直接搜注解,适合小规模。

反编译之后,按 0x01 的办法搜注解就行。注解信息在 class 文件里是保留的(@Retention(RUNTIME)),反编译出来不会丢。

不想反编译:从常量池里捞

注解的字符串值编译后存在 class 文件的常量池里,是明文的。可以用 strings 直接捞:

find app_extracted/WEB-INF/classes -name '*.class' -exec strings {} \; \
  | grep -E '^/[A-Za-z0-9_/{}._-]{2,60}$' \
  | sort -u

在 Windows 上把 strings 换成 grep -a 也一样。

这个办法几秒钟能出结果,适合先快速判断"这套系统大概有哪些接口",再决定要不要花时间完整反编译。缺点是会有噪音——常量池里所有以 / 开头的字符串都会被捞出来,包括日志格式串和文件路径。当作线索用,不要当权威清单用。


0x04 XML 里的路由

有一类路由,注解和反编译都覆盖不到,因为它们声明在 XML 里。

Servlet 3.0 注解形式:

@WebServlet("/legacy/export")
public class LegacyExportServlet extends HttpServlet { ... }

老 Spring 的 URL 映射:

<bean id="urlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
    <property name="mappings">
        <props>
            <prop key="/old/export.do">exportController</prop>
            <prop key="/old/import.do">importController</prop>
        </props>
    </property>
</bean>

Struts2:

<package name="admin" namespace="/admin" extends="struts-default">
    <action name="user_*" class="com.xxx.action.UserAction" method="{1}">
        <result name="success">/WEB-INF/jsp/user.jsp</result>
    </action>
</package>

最后这个是重点。name="user_*" 加 method="{1}" 是一个通配映射:/admin/user_login.action 会调到 UserAction.login(),/admin/user_delete.action 会调到 UserAction.delete()。一行 XML 声明对应多个 URL,靠数 XML 行数是数不出接口数量的,必须读通配规则。

MCMS 这类 war 部署的老项目,要同时检查三处:

grep -n "servlet-class\|filter-class\|url-pattern" web.xml
grep -rn "SimpleUrlHandlerMapping\|<prop key=" --include=*.xml .
grep -rn "<action\|<package" --include=struts*.xml .

这类接口的特点是通常不走框架的过滤器和拦截器链。它们可能不受 Shiro / Spring Security 保护,可能是当年为了临时需求加的,而且路径在 Java 代码里搜不到。梳理接口清单时,这一批要单独列出来,逐个确认鉴权状态。


0x05 一次翻车:我只搜了 @RequestMapping

第一次做接口梳理的时候,我 grep 了 @RequestMapping,得到 180 条,觉得差不多了,就直接进业务。

后来为了确认另一个问题,在 requestMappingHandlerMapping 上 evaluate 了一次 getHandlerMethods(),拿到 247 条。

差的 67 条,绝大部分是 @PostMapping 和 @DeleteMapping。

更难受的是漏掉的那批里有什么:

@PostMapping("/run")
public AjaxResult run(@PathVariable Long jobId) { ... }        // /monitor/job/run

@PostMapping("/createTable")
public AjaxResult createTable(String sql) { ... }              // /tool/gen/createTable

一条是手动触发定时任务,一条是拿前端传的 SQL 直接建表。这两条恰恰是最该先看的一批接口——能执行代码、能执行 SQL 的接口。 而它们全在我漏掉的那 67 条里。

原因很简单:/run 是动作型的,开发写的时候顺手用 @PostMapping;createTable 也是。而我当时只搜了字面量 @RequestMapping。

两个教训:

搜注解要搜全五类,或者干脆别用文本搜,直接用框架的注册表。 前者容易漏,后者不会。

用断点法拿到的清单里会混进框架自带的端点。 error、/actuator/*、SpringBoot 的 /favicon.ico 都会在里面。过滤掉这批,剩下的才是业务接口。这次 247 条里,业务接口实际是 229 条。


0x06 从接口清单到重点清单

229 条业务接口,还是要排优先级。我的分法不是按漏洞类型,是按这个接口能干什么。

按风险从高到低:

能执行代码的。 定时任务、代码生成、脚本引擎、表达式解析、动态数据源管理、Groovy/JS 引擎相关。这类接口一旦参数可控就是 RCE,审计成本低、收益高,永远第一批看。

能操作文件的。 上传、下载、导入、导出、解压、模板在线编辑。这条线上挂着上传、任意文件读取、路径穿越、XXE、模板注入。

能拼 SQL 的。 列表查询、搜索、统计、报表。重点看有没有排序参数,因为 ORDER BY 后面没法用预编译。

能发外部请求的。 回调、代理、图片抓取、远程调用、Webhook 测试。SSRF 的落点。

认证相关本身。 登录、注册、找回密码、验证码、token 刷新。逻辑漏洞和认证绕过都在这。

权限管理。 用户、角色、菜单、部门、参数、公告。越权的主要目标。

接口名说明功能,参数名说明风险

只看接口名还不够。同一个 /list,参数里有没有 orderByColumn,风险差一个数量级。

所以第二步是把每个接口的参数名过一遍:

参数名 / 关键字指向的问题
orderByColumn isAsc orderBy sortName sortORDER BY 拼接,SQL 注入
url uri target callback redirect picUrlSSRF、开放重定向
fileName filePath path name download路径穿越、任意文件读写
xml data content template tplXXE、模板注入
className beanName methodName invokeTarget反射调用,RCE
sql tableName columns ddl直接拼接 SQL
type action cmd op status逻辑分支绕过
id ids userId deptId水平越权

这张表配合接口清单用:先按功能把接口分六档,再在每个接口的参数里搜这几个关键字。命中的直接进重点清单。


0x07 清单

[ ] 类级 @RequestMapping 前缀全部摘出,与方法级路径拼接
[ ] 五类 Mapping 注解 + @Path 全部搜到,排除注释行
[ ] 搜索 @Anonymous / permitAll / 匿名放行标记,列出免认证接口
[ ] 逐条记录 @PreAuthorize 值,标出无权限注解的接口
[ ] 用 RequestMappingHandlerMapping.getHandlerMethods() 或 /actuator/mappings 交叉验证
[ ] 框架自带端点(error、actuator、favicon)从清单中剔除
[ ] web.xml / SimpleUrlHandlerMapping / struts.xml 中的路由单独列出
[ ] Struts2 通配映射展开成实际 URL
[ ] 上述 XML 路由逐条确认鉴权状态
[ ] 按功能把接口分六档,标出"能执行代码"和"能操作文件"两类
[ ] 按参数名关键字扫一遍,命中的进重点清单

产出:一张接口表,每条带 HTTP 方法、路径、处理方法、权限注解;外加一张按风险排序的重点清单。


下一篇讲数据流:从入口参数到危险函数怎么追,以及怎么判断一个过滤器到底能不能绕。


暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇