华夏erp审计

14 阅读13分钟

[TOC]

分析方法

  1. 拿到一个项目,加入我们要找是否存在$ 拼接导致的sql注入,先搜关键字:

    右下角的打开窗口可以显示全部

image-20260113230443907.png

  1. 看不懂的可以问AI,排除掉低风险的日志文件,我们可以锁定到 jshERP-assembly.xml 文件里的${}用法。 image-20260113230537688.png

    这些是MyBatis 的 Mapper 映射文件,是 Java 项目中用来定义数据库操作 SQL 的配置文件。

22fb35e5-4dfa-4a22-88d1-bdc2bdecaf95.png

点击里面的一个案例,看不懂就问AI,找到相关的参数传入,ctrl + 右键。

image-20260113231055726.png

找到相关参数后,可以右键找参数调用,从而找到了传参函数:

image-20260113232332262.png

再右键转到函数用例

image-20260113232359709.png

最后发现参数都是定义好的字符串,无法控制,所以不存在漏洞:

image-20260113232629771.png

补充:在 IntelliJ IDEA 中,Alt + Ctrl + H 是一个非常实用的快捷键,它的功能是:打开当前方法 / 类的调用层次结构(Call Hierarchy)。

image-20260114214556866.png

上面的方法被下面的方法调用。

华夏ERP审计案例补充

认证绕过

image-20260115104954455.png

该文件是全局认证过滤文件。

image-20260115105103568.png 这里没有过滤../,用contains函数来判断白名单。(contains仅判断是否包含目标字符串)

所以当后端端口暴露时,就可以绕过前端nginx的../过滤。利用白名单和../搭配实现认证绕过

image-20260115105630051.png

点击左上角笔的图标修改发送端口。

image-20260115105812710.png

成功绕过认证,访问到内部文件。

越权文件执行

在插件管理的类中,

所有接口的权限校验仅依赖 userInfo.getLoginName().equals(BusinessConstants.DEFAULT_MANAGER)(判断是否是 “默认管理员账号”)

image-20260115110709792.png

构造恶意的插件并上传便可以实现命令执行。

文件上传

在 SystemConfigController 类中找到了文件上传接口,接下来开始审计:

image-20260115171603405.png

在这个函数中,用户可以直接控制的参数有:biz,name,file。

fileUploadType参数决定是否本地上传。

本地上传函数过滤是:systemConfigService.uploadLocal()

直接 ctrl+右键 跳转到过滤函数定义:

image-20260115171952759.png

从 return 开始往前推,推到 orgName 这个用户直接可控参数。

下面一行 orgName = FileUtils.getFileName(orgName); 则是用来过滤和处理 orgName 参数,

在下面就是对文件名唯一性的逻辑处理,所以直接跳转过滤函数:

image-20260115172331037.png

显而易见,该过滤函数没有过滤../字符,也没有限制文件名后缀,所以导致了用户可以上传任意格式的文件到任意目录。

验证码绕过

在前端填验证码抓包,发现如果验证码填错了就不会有包发到后端,从而发现验证码绕过漏洞。

开始复现,在前端填一个正确的验证码,用burp抓包:

image-20260115175826999.png

发送到重放器,发现密码经过md5加,而且重放不经过验证码验证,开始爆破。

image-20260115175925480.png

发送到 intruder 模块,配置如下:

image-20260115180355356.png

爆破成功!

image-20260115180521789.png

任意密码重置

在用户管理这个类中,重置密码接口完全没有做有效的用户身份 / 权限验证:

image-20260115183747558.png

直接抓包重放,修改id

image-20260115184554296.png

信息泄露

image-20260115193724804.png

没有进行任何过滤,严重信息泄露。

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.doServletPath = "/user/resetPwd.do"
request.getPathInfo()映射规则外的 “可变路径部分”(null 则表示无)请求/user/resetPwd.do/PathInfo = "null"

使用上面的方式接受URI后,上面的绕过均不可用,接受过去的时候发送特殊字符一律被剔除了。

参考

漏洞案例(OpenMetadata权限绕过漏洞CVE-2024-28255)

github.com/open-metada…

url中分号的作用

blog.csdn.net/laowang2915…

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工具下载地址:

owasp.org/www-project…

推荐下载Maven版。详情参考官方文档

jeremylong.github.io/DependencyC…

  1. 在要扫描模块的pom.xml文件中引入插件,插入位置要在 内,并同步项目。

    <plugin>
    	<groupId>org.owasp</groupId>
    	<artifactId>dependency-check-maven</artifactId>
    	<executions>
    		<execution>
    			<goals>
    				<goal>check</goal>
    			</goals>
    		</execution>
    	</executions>
    </plugin>
    

image-20260117091017698.png

使用说明

点击 IDEA 右边的 Maven,在插件(Plugin)中找到 Dependency Check。

image-20260117092158801.png

功能按名字分类:

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级别漏洞

image-20260117102157284.png

补充

补充1:OWASP dependency-check-maven 12.2.0 插件需要 java11 来运行,可以在项目结构处切换JDK版本:

image-20260117092944455.png

补充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),即“通用漏洞评分系统”,是一个“行业公开标准,其被设计用来评测漏洞的严重程度,并帮助确定所需反应的紧急度和重要度

image-20260117093825238.png

CodeQL

当谈及代码分析和漏洞检测工具时, CodeQL无疑是一款备受推崇的解决方案。作为一种革命性的语义代码查询语言, CodeQL在软件安全领域展现出了卓越的实力。

解析引擎

解析引擎是闭源的,只有二进制执行文件。将源代码转换为CodeQL脚本能识别的抽象语法树。主要是项目地址

github.com/github/code…

SDK

SDK是开源的,是CodeQL 标准库。仓库包含了必须的一些标准库(内置库)和一大量的查询规则。

github.com/github/code…

安装

codeql的解析引擎和SDK既可以分开安装,也可以打包安装。分开安装时,需要确保SDK版本和引擎版本兼容,如果单独更新了SDK,可能会出现不兼容的问题。因此建议使用打包安装。

github.com/github/code…

直接安装.zip

image-20260117094843589.png

image-20260117094931163.png

path环境变量:

将下载的文件添加进环境变量,注意路径中不能出现中文等字符。

image-20260117100305853.png

验证: (cmd)

codeql resolve packs

image-20260117100435115.png

vscode 插件

官方提供了VSCode编写CodeQL的插件

Ctrl+Shift+X(扩展) => 输入CodeQL => install

image-20260117102003341.png

image-20260117102341515.png

使用

创建数据库

备注:需要已经安装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 多线程 

image-20260117104539980.png

注意:要使用 java8 版本运行,而且要在本机终端运行命令,IDEA的终端会报错。

image-20260117104701136.png

执行查询

使用官方默认的java漏洞查询规则进行分析并将结果保存到当前目录

codeql database analyze test-qldb java-security-extended.qls --format=sarif-latest --output=results.sarif -j 0 -M 5000

如果出现如下错误,原因是执行查询的时候,内存需求大, java虚拟机内存不够。

image-20260117104827067.png

可以通过 -M 参数指定内存大小。

image-20260117110505157.png

(次选,一般不用)也可以用VS执行查询:

选择数据库:

image-20260117105041105.png

image-20260117105435496.png

新建一个文件夹作为工作区,找到 bangle捆绑包的 java规则,复制粘贴到工作区文件中:

D:\app\codeql\qlpacks\codeql\java-queries\1.10.4

image-20260117110015041.png

右键选择codeql进行查询

image-20260117110054827.png

分析结果

在 VScode 里安装插件 sarif viewer。

image-20260117110605750.png

使用vscode打开 results.sarif 所在目录。

image-20260117110807528.png

第一个列表是位置分类,第二个列表是漏洞类型分类

CodeQLpy 自动化代码审计工具

CodeQLpy是一款基于CodeQL实现的自动化代码审计工具,目前仅支持java语言,后期会增加对其他语言的支持。

支持对多种不同类型的java代码进行代码审计,包括jsp文件、 SpringMVC的war包、 SpringBoot的jar包、 maven源代码。

开源地址:

github.com/webraybtl/C…

工具安装

  1. 首先安装CodeQL,注意一定要使用新版本,老版本中有不支持的语法

  2. python环境依赖,本项目依赖python3.7及以上版本

    依赖见requirements.txt

    pip3 install -r requirements.txt
    

​ 在vscode打开工具压缩包,进到 requirements.txt,点击新建终端

image-20260117113612578.png

复制粘贴,运行。

image-20260117113724749.png 3. java环境依赖,本项目运行需要安装下面的java组件: JDK8、 JDK11、 maven。

  1. 修改config/config.ini文件,需要修改的配置项是 qlpath (随便写一个)和 jdk8 和 jdk11,其他项目可保持默认。 注意jdk的路径中有空格的话需要用双引号包裹。

image-20260117114245330.png

工具使用

生成初始化

将华夏erp编译后的jar包(一般在targe目录下),放到工具目录下。

image-20260117114708115转存失败,建议直接上传图片文件

复制相对路径,执行下面命令:

python main.py -t 111\jshERP.jar -c

image-20260117114708115.png

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

image-20260117114950695.png

最后会弹地址。

代码审计

由于codeqlpy依赖的是老版本的codql,因此这个功能模块已经无法使用。我们可以直接使用codeql命令生成数据库

codeql database analyze out\database\jshERP java-security-experimental.qls --format=sarif-latest --output=results.sarif -j 0 -M 5000

image-20260117115226233.png

打开results文件审计

image-20260117120727753.png