[TOC]
分析方法
-
拿到一个项目,加入我们要找是否存在$ 拼接导致的sql注入,先搜关键字:
右下角的打开窗口可以显示全部
-
看不懂的可以问AI,排除掉低风险的日志文件,我们可以锁定到 jshERP-assembly.xml 文件里的${}用法。
这些是MyBatis 的 Mapper 映射文件,是 Java 项目中用来定义数据库操作 SQL 的配置文件。
点击里面的一个案例,看不懂就问AI,找到相关的参数传入,ctrl + 右键。
找到相关参数后,可以右键找参数调用,从而找到了传参函数:
再右键转到函数用例
最后发现参数都是定义好的字符串,无法控制,所以不存在漏洞:
补充:在 IntelliJ IDEA 中,Alt + Ctrl + H 是一个非常实用的快捷键,它的功能是:打开当前方法 / 类的调用层次结构(Call Hierarchy)。
上面的方法被下面的方法调用。
华夏ERP审计案例补充
认证绕过
该文件是全局认证过滤文件。
这里没有过滤../,用contains函数来判断白名单。(contains仅判断是否包含目标字符串)
所以当后端端口暴露时,就可以绕过前端nginx的../过滤。利用白名单和../搭配实现认证绕过
点击左上角笔的图标修改发送端口。
成功绕过认证,访问到内部文件。
越权文件执行
在插件管理的类中,
所有接口的权限校验仅依赖 userInfo.getLoginName().equals(BusinessConstants.DEFAULT_MANAGER)(判断是否是 “默认管理员账号”)
构造恶意的插件并上传便可以实现命令执行。
文件上传
在 SystemConfigController 类中找到了文件上传接口,接下来开始审计:
在这个函数中,用户可以直接控制的参数有:biz,name,file。
fileUploadType参数决定是否本地上传。
本地上传函数过滤是:systemConfigService.uploadLocal()
直接 ctrl+右键 跳转到过滤函数定义:
从 return 开始往前推,推到 orgName 这个用户直接可控参数。
下面一行 orgName = FileUtils.getFileName(orgName); 则是用来过滤和处理 orgName 参数,
在下面就是对文件名唯一性的逻辑处理,所以直接跳转过滤函数:
显而易见,该过滤函数没有过滤../字符,也没有限制文件名后缀,所以导致了用户可以上传任意格式的文件到任意目录。
验证码绕过
在前端填验证码抓包,发现如果验证码填错了就不会有包发到后端,从而发现验证码绕过漏洞。
开始复现,在前端填一个正确的验证码,用burp抓包:
发送到重放器,发现密码经过md5加,而且重放不经过验证码验证,开始爆破。
发送到 intruder 模块,配置如下:
爆破成功!
任意密码重置
在用户管理这个类中,重置密码接口完全没有做有效的用户身份 / 权限验证:
直接抓包重放,修改id
信息泄露
没有进行任何过滤,严重信息泄露。
Filter权限绕过
在一些需要挖掘一些无条件RCE中,大部分类似于一些系统大部分地方都做了权限控制的,而这时候想要利用权限绕过就显得格外重要。在此来学习一波权限绕过的思路。
漏洞代码
常见的实现方式,在不调用Spring Security、 Shiro等权限控制组件的情况下,会使用Filter获取请求路径,进行校验。编写一个servlet。(Servlet 是处理具体业务请求的核心组件)
package com.nice0e3;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/helloServlet") //类功能实现
public class helloServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponseresponse) throws ServletException, IOException {
response.getWriter().write("hello!!!"); //返回 hello!!!
}
protected void doGet(HttpServletRequest request, HttpServletResponseresponse) throws ServletException, IOException {
this.doPost(request, response);
}
}
定义一个Filter
package com.nice0e3.filter;
import com.nice0e3.User;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
@WebFilter("/*") //所有访问都要经过该接口检查
public class demoFilter implements Filter {
public void destroy() {
}
public void doFilter(ServletRequest req, ServletResponse resp, FilterChainchain) throws ServletException, IOException {
HttpServletRequest request = (HttpServletRequest) req;
String uri = request.getRequestURI();
StringBuffer requestURL = request.getRequestURL();
System.out.println(requestURL);
if(uri.startsWith("/system/login")) { //登陆接口设置⽩白名单,即登录页面
// startsWith 判断以什么开头
System.out.println("login_page");
resp.getWriter(). write("login_page"); // 返回字符串"login_page"
chain.doFilter(request, resp); // 放行
}
else if(uri.endsWith(".do")||uri.endsWith(".action")) {
//检测当前⽤户是否登陆
User user =(User) request.getSession().getAttribute("user");
if(user == null) {
resp.getWriter(). write("unauthorized access"); //未授权访问
System.out.println("unauthorized access");
resp.getWriter(). write("go to login_page");//跳转登录
System.out.println("go to login_page");
return;
}
}
chain.doFilter(request, resp);
}
public void init(FilterConfig config) throws ServletException {
}
}
在Java中通常会使用 request.getRequestURL() 和 request.getRequestURI() 这两个方法获取请求路径,然后对请求路径做校验。当获取URI以 /system/login 开头,则直接放行, URI以结尾,为 .do和 .action 的请求去做校验,并判断session中有没有user的值,没有的话即返回 unauthorized access ,如果不为 .do 和 .action 的请求或session中存在user即放行。
绕过姿势
../ 绕过方式
payload: /system/login/../../login/main.do
由于浏览器会自动转换../,因此一定要在burpsuite的repeater模块修改。
URL截断绕过
将 ../ 进行过滤
package com.nice0e3.filter;
import com.nice0e3.User;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
@WebFilter("/*")
public class demoFilter implements Filter {
public void destroy() {
}
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain
chain) throws ServletException, IOException {
HttpServletRequest request = (HttpServletRequest) req;
String uri = request.getRequestURI();
if(uri.contains("./")){
resp.getWriter().write("error");
return;
} // 将 ./ 进行过滤
StringBuffer requestURL = request.getRequestURL();
System.out.println(requestURL);
if(uri.startsWith("/system/login")) { //登陆接口设置⽩白名单,即登录页面
System.out.println("login_page");
resp.getWriter(). write("login_page");
chain.doFilter(request, resp);
}
else if(uri.endsWith(".do")||uri.endsWith(".action")) {
//检测当前⽤户是否登陆
User user =(User) request.getSession().getAttribute("user");
if(user == null) {
resp.getWriter(). write("unauthorized access"); //未授权访问
System.out.println("unauthorized access");
resp.getWriter(). write("go to login_page");//跳转登录
System.out.println("go to login_page");
return;
}
}
chain.doFilter(request,resp);
}
public void init(FilterConfig config) throws ServletException {
}
}
上述代码添加了一个 uri.contains("./") 做过滤,作用是检测到uri只要包含 ./ 字符,直接报错。
这种情况可使用 ;进行绕过, payload: /login/main.do;123
补充:这段代码完全没有对 url 解码,所以也可以用 url 编码绕过,后面有讲。
绕过分析
URL中有一个保留字符,即分号 ; ,主要为参数进行分割使用,有时候是请求中传递的参数太多了,所以使用分号 ; 将参数对(key=value)连接起来作为一个请求参数进⾏传递。因此可以使用分号加入垃圾数据,添加 ; 分号后不会对原地址有任何影响。
如上述代码中会对以 .do 和 .action 结尾的uri进行权限检查,当使用上面payload后,代码中匹配不到,则会走到最下面的 chain.doFilter(request,resp);
多/ 绕过
创建一个后台接口,只允许admin用户登录访问
package com.nice0e3.Servlet;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/system/UserInfoSearch.do")
public class UserInfoServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.getWriter().write("admin_login!!!");
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doPost(request, response);
}
}
在Filter里面添加如下权限控制代码,对 /system/UserInfoSearch.do 做了校验,假如当前用户不为admin,则显示 Unauthorized
String uri = request.getRequestURI();
if(uri.equals("/system/UserInfoSearch.do")){
User user =(User) request.getSession().getAttribute("user");
String role = user.getRole();
if(role.equals("admin")) {
//当前⽤用户为admin, 允许访问该接⼝
chain.doFilter(request, resp);
}
else {
resp.getWriter().write("Unauthorized");
return;
}
}
使用之前的payload,会被拦截,使用 payload: //system/UserInfoSearch.do;123 可以成功绕过
绕过分析
上述验证代码只是对比了URI是否为 /system/UserInfoSearch.do ,而多加一个 / 并不影响正常解析,而又能让该规则匹配不到。因此绕过成功。
URL编码绕过
上面同样可以用 url 编码绕过匹配:payload: /system/%55%73%65%72%49%6e%66%6f%53%65%61%72%63%68%2e%64%6f (注:是全字符编码才能绕过)
绕过分析
当Filter处理完相关的流程后,中间件会对请求的URL进行一次URL解码操作,然后请求解码后的Servlet,而在 request.getRequestURL( )和 request.getRequestURI() 中并不会自动进行解码,所以这时候直接接收过来进行规则匹配,则识别不出来。这时候导致了绕过。
解码代码长这样:
decodedUri = URLDecoder.decode(decodedUri, StandardCharsets.UTF_8.name());
Spring MVC中追加/ 绕过
Spring MVC 是Java Web 开发的 “标准化框架” —— 它是 Spring 生态中专门处理 HTTP 请求的模块,核心作用是:
接管浏览器 / 客户端的 HTTP 请求,按 “请求路径→控制器→业务逻辑→返回响应” 的流程,标准化地处理 Web 请求。
在Spring MVC中假设以如下方法配置:(XML 配置文件)
<servlet-mapping>
<servlet-name>SpringMVC</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
绕过分析
特定情况下Spring匹配web路径的时候会容错后面的 /可通过添加/绕过,如 /admin/main.do/
修复方式
使用该代码接受URL(自定义 Filter 类)
String uri1 = request.getServletPath() + (request.getPathInfo() != null ? request.getPathInfo() : "");
| 方法 | 含义 | 举例(假设 Servlet 映射为/user/resetPwd.do) |
|---|---|---|
request.getServletPath() | 匹配 Servlet/Controller 映射规则的 “固定路径部分” | /user/resetPwd.do → ServletPath = "/user/resetPwd.do" |
request.getPathInfo() | 映射规则外的 “可变路径部分”(null 则表示无) | 请求/user/resetPwd.do/ → PathInfo = "null" |
使用上面的方式接受URI后,上面的绕过均不可用,接受过去的时候发送特殊字符一律被剔除了。
参考
漏洞案例(OpenMetadata权限绕过漏洞CVE-2024-28255)
url中分号的作用
Dependency Check 依赖扫描工具
对于代码依赖包的安全问题,业界通常使用Dependency-Check来检查代码中是否存在任何已知的,公开披露的安全漏洞。
Dependency-Check是OWASP(Open Web Application Security Project)的一个实用开源程序,用于识别项目依赖项并检查是否存在任何已知的,公开披露的漏洞。目前,已支持Java、 .NET、 Ruby、PHP、 Node.js、 Python等语言编写的程序,并为C/C++构建系统(autoconf和cmake)提供了有限的支持。
下载安装
Dependency-Check工具下载地址:
推荐下载Maven版。详情参考官方文档
jeremylong.github.io/DependencyC…
-
在要扫描模块的pom.xml文件中引入插件,插入位置要在 内,并同步项目。
<plugin> <groupId>org.owasp</groupId> <artifactId>dependency-check-maven</artifactId> <executions> <execution> <goals> <goal>check</goal> </goals> </execution> </executions> </plugin>
使用说明
点击 IDEA 右边的 Maven,在插件(Plugin)中找到 Dependency Check。
功能按名字分类:
Aggregate 分析当前项目及其子项目并生成报告
check 检查依赖
help 帮助信息
purge 清除缓存
update 更新
双击就会运行,相关的结果报告将会输出在该模块的target目录下
报告分析
报告中一些重要字段的含义:
- Dependency - 被扫描的第三依赖库名字
- CPE - 所有被识别出来的CPE.
- GAV - Maven 组, Artifact, 版本 (GAV).
- Highest Severity - 所有关联的cve的最高漏洞等级
- CVE Count - 关联的cve个数
- CPE Confidence - dependency-check正确识别cpe的程度
- Evidence Count - 识别CPE的数据个数
一般只需关注 Highest Severity 列中的CRITICAL和HIGH级别漏洞
补充
补充1:OWASP dependency-check-maven 12.2.0 插件需要 java11 来运行,可以在项目结构处切换JDK版本:
补充2:初始化工具时需要启动代理来下载RetireJS,否则会报错。
[ERROR] Failed to initialize the RetireJS repo
org.owasp.dependencycheck.data.update.exception.UpdateException: Failed to initialize the RetireJS repoat
开了代理还是报这个错的话,试一下清除缓存。
CVSS
NVD评级依赖CVSS(CommonVulnerability Scoring System),即“通用漏洞评分系统”,是一个“行业公开标准,其被设计用来评测漏洞的严重程度,并帮助确定所需反应的紧急度和重要度
CodeQL
当谈及代码分析和漏洞检测工具时, CodeQL无疑是一款备受推崇的解决方案。作为一种革命性的语义代码查询语言, CodeQL在软件安全领域展现出了卓越的实力。
解析引擎
解析引擎是闭源的,只有二进制执行文件。将源代码转换为CodeQL脚本能识别的抽象语法树。主要是项目地址
SDK
SDK是开源的,是CodeQL 标准库。仓库包含了必须的一些标准库(内置库)和一大量的查询规则。
安装
codeql的解析引擎和SDK既可以分开安装,也可以打包安装。分开安装时,需要确保SDK版本和引擎版本兼容,如果单独更新了SDK,可能会出现不兼容的问题。因此建议使用打包安装。
直接安装.zip
path环境变量:
将下载的文件添加进环境变量,注意路径中不能出现中文等字符。
验证: (cmd)
codeql resolve packs
vscode 插件
官方提供了VSCode编写CodeQL的插件
Ctrl+Shift+X(扩展) => 输入CodeQL => install
使用
创建数据库
备注:需要已经安装Maven,因为华夏erp项目是基于Maven构建的, CodeQL在创建数据库时,会自动探测并使用对应的编译方式。如果maven可执行文件不在path中,可以手动安装并添加环境变量。
idea安装的时候,已经自带maven,可以直接将如下路径添加到path中。
D:\app\IntelliJ IDEA 2025.1.3\plugins\maven\lib\maven3\bin
在华夏erp目录下面执行如下命令,会自动编译并且为该项目创建一个QL数据库。
数据库是codeql引擎将java源码进行解析后构建的,用于后续codeql规则分析。注意路径中不能含有中文等特殊字符
codeql database create test-qldb -l java -j 0
test-qldb是数据库名,-l 指定语言 -j 多线程
注意:要使用 java8 版本运行,而且要在本机终端运行命令,IDEA的终端会报错。
执行查询
使用官方默认的java漏洞查询规则进行分析并将结果保存到当前目录
codeql database analyze test-qldb java-security-extended.qls --format=sarif-latest --output=results.sarif -j 0 -M 5000
如果出现如下错误,原因是执行查询的时候,内存需求大, java虚拟机内存不够。
可以通过 -M 参数指定内存大小。
(次选,一般不用)也可以用VS执行查询:
选择数据库:
新建一个文件夹作为工作区,找到 bangle捆绑包的 java规则,复制粘贴到工作区文件中:
D:\app\codeql\qlpacks\codeql\java-queries\1.10.4
右键选择codeql进行查询
分析结果
在 VScode 里安装插件 sarif viewer。
使用vscode打开 results.sarif 所在目录。
第一个列表是位置分类,第二个列表是漏洞类型分类
CodeQLpy 自动化代码审计工具
CodeQLpy是一款基于CodeQL实现的自动化代码审计工具,目前仅支持java语言,后期会增加对其他语言的支持。
支持对多种不同类型的java代码进行代码审计,包括jsp文件、 SpringMVC的war包、 SpringBoot的jar包、 maven源代码。
开源地址:
工具安装
-
首先安装CodeQL,注意一定要使用新版本,老版本中有不支持的语法
-
python环境依赖,本项目依赖python3.7及以上版本
依赖见requirements.txt
pip3 install -r requirements.txt
在vscode打开工具压缩包,进到 requirements.txt,点击新建终端
复制粘贴,运行。
3. java环境依赖,本项目运行需要安装下面的java组件: JDK8、 JDK11、 maven。
- 修改config/config.ini文件,需要修改的配置项是 qlpath (随便写一个)和 jdk8 和 jdk11,其他项目可保持默认。 注意jdk的路径中有空格的话需要用双引号包裹。
工具使用
生成初始化
将华夏erp编译后的jar包(一般在targe目录下),放到工具目录下。
复制相对路径,执行下面命令:
python main.py -t 111\jshERP.jar -c
t参数表示目标源码的路径,支持的源码类型是文件夹, jar包和war包。注意如果是文件夹类型的源码,
-t指定的路径必须是网站跟目录,不然会因为源码中相对路径错误导致编译异常。如果是jar包或者war包,必须指定文件名。
-c表示源码是属于编译后的源码,即class文件。如果不指定,则表示源码为编译前源码,即java文件。
复制生成的命令,粘贴运行就会生成数据库。
codeql database create out/database/jshERP --language=java --command="D:\app\CodeQLpy-master\out\decode/run.cmd" --overwrite
最后会弹地址。
代码审计
由于codeqlpy依赖的是老版本的codql,因此这个功能模块已经无法使用。我们可以直接使用codeql命令生成数据库
codeql database analyze out\database\jshERP java-security-experimental.qls --format=sarif-latest --output=results.sarif -j 0 -M 5000
打开results文件审计