当前位置:WooYun >> 漏洞信息

漏洞概要 关注数(24) 关注此漏洞

缺陷编号:wooyun-2014-089474

漏洞标题:协同OA任意上传0day

相关厂商:seeyon.com

漏洞作者:

提交时间:2014-12-31 12:00

修复时间:2015-04-02 10:23

公开时间:2015-04-02 10:23

漏洞类型:文件上传导致任意代码执行

危害等级:高

自评Rank:15

漏洞状态:漏洞已经通知厂商但是厂商忽略漏洞

漏洞来源: http://www.wooyun.org,如有疑问或需要帮助请联系 [email protected]

Tags标签:

4人收藏 收藏
分享漏洞:


漏洞详情

披露状态:

2014-12-31: 细节已通知厂商并且等待厂商处理中
2015-01-05: 厂商主动忽略漏洞,细节向第三方安全合作伙伴开放
2015-03-01: 细节向核心白帽子及相关领域专家公开
2015-03-11: 细节向普通白帽子公开
2015-03-21: 细节向实习白帽子公开
2015-04-02: 细节向公众公开

简要描述:

就是过滤不严呗

详细说明:

协同OA文件上传时都调用一个Action,那就是fileUpload.do。
架构使用了Spring,所以,在服务上这个http://***.com/fileUpload.do文件是不存在的,要找到他对应的类,然后反编译才可以看到源码。
通过web.xml,找到了urlMapping.xml,

1.jpg


这个XML中可以找到一系类的URL映射文件,比如fileUpload.do映射到了

2.png


现在再找到fileUploadController就可以找到与之相对应的类了。文件上传下载一般属于公用组件,所以找到了common-controller.xml。在这个里面找到类。

3.png


终于找到类了,下载下来class然后反编译吧,看代码。
com.seeyon.v3x.common.fileupload.FileUploadController 反编译后的源码如下:

public ModelAndView processUpload(HttpServletRequest request, HttpServletResponse response)
throws Exception
{
ModelAndView modelAndView = new ModelAndView("common/fileUpload/upload");
String extensions = request.getParameter("extensions"); //扩展名
String applicationCategory = request.getParameter("applicationCategory");
String destDirectory = request.getParameter("destDirectory");
String destFilename = request.getParameter("destFilename");
String typeStr = request.getParameter("type");
String maxSizeStr = request.getParameter("maxSize");
Constants.ATTACHMENT_TYPE type = null;
if (StringUtils.isNotBlank(typeStr)) {
type = (Constants.ATTACHMENT_TYPE)EnumUtil.getEnumByOrdinal(Constants.ATTACHMENT_TYPE.class, new Integer(typeStr).intValue());
}
else {
type = Constants.ATTACHMENT_TYPE.FILE;
}
ApplicationCategoryEnum category = null;
if (StringUtils.isNotBlank(applicationCategory)) {
category = ApplicationCategoryEnum.valueOf(new Integer(applicationCategory).intValue());
}
else {
category = ApplicationCategoryEnum.global;
log.warn("上传文件:v3x:fileUpload没有设定applicationCategory属性,将设置为‘全局’。");
}
Long maxSize = null;
if (StringUtils.isNotBlank(maxSizeStr)) { //最大值不为空
maxSize = new Long(maxSizeStr);
}
Map v3xFiles = new HashMap();
try {
File destFile = null;
if (StringUtils.isNotBlank(destFilename)) { //如果文件名不为空
destFile = new File(FilenameUtils.separatorsToSystem(destFilename));
v3xFiles = this.fileManager.uploadFiles(request, extensions, destFile, maxSize);
}
else if (StringUtils.isNotBlank(destDirectory)) { //目标文件夹不为空
v3xFiles = this.fileManager.uploadFiles(request, extensions, destDirectory, maxSize);
}
else {
v3xFiles = this.fileManager.uploadFiles(request, extensions, maxSize);
}
if (v3xFiles != null) {
List keys = new ArrayList((Collection)v3xFiles.keySet());
Collections.sort(keys);
List atts = new ArrayList();
for (String key : keys) {
atts.add(new Attachment((V3XFile)v3xFiles.get(key), category, type));
}
modelAndView.addObject("atts", atts);
}
}
catch (NoSuchPartitionException e)
{
modelAndView.addObject("e", e);
}
catch (BusinessException e) {
modelAndView.addObject("e", e);
}
catch (Exception e) {
modelAndView.addObject("e", new BusinessException("fileupload.exception.unknown", new Object[] { e.getMessage() }));
}
return modelAndView;
}


关键代码如下:

try {
File destFile = null;
if (StringUtils.isNotBlank(destFilename)) { //如果文件名不为空
destFile = new File(FilenameUtils.separatorsToSystem(destFilename));
v3xFiles = this.fileManager.uploadFiles(request, extensions, destFile, maxSize);
}
else if (StringUtils.isNotBlank(destDirectory)) { //目标文件夹不为空
v3xFiles = this.fileManager.uploadFiles(request, extensions, destDirectory, maxSize);
}
else {
v3xFiles = this.fileManager.uploadFiles(request, extensions, maxSize);
}


如果文件名不为空,就生成文件,然后调用this.fileManager.uploadFiles(request, extensions, destFile, maxSize);
这里请注意,在方法一开始的时候使用了
String destFilename = request.getParameter("destFilename"); 接收文件名,
request.getParameter是从http协议GET/POST等方法中接收,而非在httpbody中。比如说以下请求:
Content-Disposition: form-data; name="file1"; filename="1.jpg"
Content-Type: image/jpeg
这个1.jpg是无效的,不管用,这就可以说,我们可以随意的给上传的文件传递文件名,比如说JSP。
那么问题来了,程序是怎么过来的?上传文件后,放在什么地方?
destFile = new File(FilenameUtils.separatorsToSystem(destFilename));
这句话,就代表着生成一个文件,在Java中可以使用相对路径比表示。比如
File file = new File("./webapps/seeyon/x.jsp");
这样也是合法的,那么最终会生成到哪里呢?一般会根据Web容器的Context上下文路径来生成,
比如当前上下文路径为:
d:/xxx/xxx/tomcat/ 那么,最终路径就是d:/xxx/xxx/tomcat/./webapps/seeyon/x.jsp,由此可以得知,如果存在上传漏洞,那么不用知道绝对路径,就可以到指定目录了。不用担心上传文件后找不到路径了。
但继续还得看下去,看最后是如何过滤的。但是这个方法没过滤,而是调用了this.fileManager类的uploadFiles方法,并传入了file,request,扩展名。
this.fileManager是类外一个类,在去找这个类。
import com.seeyon.v3x.common.filemanager.manager.FileManager;
找这个类可真是曲折,在classes目录了根本就没有这个类,程序员将部分代码分离了,放在了lib里面的v3x-common.jar中,继续反编译,找到类。

4.png


最终的调用方法如下:

private Map<String, V3XFile> uploadFiles(HttpServletRequest request, String allowExtensions, Map<String, File> destFiles, String destDirectory, Long maxSize)
throws BusinessException
{
User user = CurrentUser.get();
if (user == null) {
return null;
}
if (!(request instanceof MultipartHttpServletRequest)) {
throw new IllegalArgumentException(
"Argument request must be an instantce of MultipartHttpServletRequest. [" +
request.getClass() + "]");
}
Date createDate = new Date();
if (destFiles == null)
{
if (StringUtils.isNotBlank(destDirectory)) {
destDirectory = FilenameUtils.separatorsToSystem(destDirectory);
}
else {
destDirectory = getFolder(createDate, true);
}
}
MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest)request;
String maxUploadSizeExceeded = multipartRequest.getParameter("MaxUploadSizeExceeded");
if (maxUploadSizeExceeded != null) {
throw new BusinessException("fileupload.exception.MaxSize", new Object[] { maxUploadSizeExceeded });
}
String ex = multipartRequest.getParameter("unknownException");
if (ex != null) {
throw new BusinessException("fileupload.exception.unknown", new Object[] { ex });
}
Map v3xFiles = new HashMap();
Iterator fileNames = multipartRequest.getFileNames();
if (fileNames == null) {
return null;
}
String isEncrypt = request.getParameter("isEncrypt");
while (fileNames.hasNext()) {
Object name = fileNames.next();
if ((name == null) || ("".equals(name)))
{
continue;
}
String fieldName = String.valueOf(name);
MultipartFile fileItem = multipartRequest.getFile(
String.valueOf(name));
if (fileItem == null)
{
continue;
}
if ((maxSize != null) && (fileItem.getSize() > maxSize.longValue())) {
throw new BusinessException("fileupload.exception.MaxSize", new Object[] { Strings.formatFileSize(maxSize.longValue(), false) });
}
String filename = fileItem.getOriginalFilename();
String suffix = FilenameUtils.getExtension(filename).toLowerCase();
if ((!StringUtils.isEmpty(allowExtensions)) &&
(!StringUtils.isEmpty(suffix))) {
allowExtensions = allowExtensions.replace(',', '|');
if (!Pattern.matches(allowExtensions.toLowerCase(), suffix)) {
throw new BusinessException(
"fileupload.exception.UnallowedExtension", new Object[] {
allowExtensions });
}
}
long fileId = UUIDLong.longUUID();
File destFile = null;
try
{
if ((destFiles != null) && (destFiles.get(fieldName) != null)) {
destFile = (File)destFiles.get(fieldName);
destFile.getParentFile().mkdirs();
}
else {
destFile = new File(destDirectory + File.separator +
String.valueOf(fileId));
}
ConfigItem configItem = this.systemConfig.getConfigItem("v3x_system_switch", "attach_encrypt", Long.valueOf(1L));
if ((configItem != null) && (!"no".equals(configItem.getConfigValue())) && ("true".equals(isEncrypt))) {
String encryptVersion = "";
if ("middle".equals(configItem.getConfigValue()))
encryptVersion = "V02";
else
encryptVersion = "V01";
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(destFile));
CoderFactory.getInstance().upload(fileItem.getInputStream(), bos, encryptVersion);
} else {
fileItem.transferTo(destFile);
}
} catch (Exception e) {
throw new BusinessException(e.getMessage());
}
V3XFile file = new V3XFile(Long.valueOf(fileId));
file.setCreateDate(createDate);
file.setFilename(filename);
file.setSize(Long.valueOf(fileItem.getSize()));
file.setMimeType(fileItem.getContentType());
file.setCreateMember(Long.valueOf(user.getId()));
file.setAccountId(Long.valueOf(user.getAccountId()));
v3xFiles.put(fieldName, file);
}
return v3xFiles;
}


最终过滤的关键代码:

如果allowExtensions不为空,并且suffix,就按照比如allowExtensions中的逗号替换为|, 
allowExtensions 是我们传入的extensions扩展名,这个是可控的,默认会传入jpg,png,png,那么最后为变为jpg|png|png。
接下来,将suffix与jpg|png|png对比,如果匹配成功,就可以上传,否则失败,suffix是我们传入fileName的扩展名。两个都可控。如果传入文件名hello.jsp,扩展名jpg,png,png,jsp,那么上传就ok了,看截图。


漏洞证明:

6.png


7.png


还有另外一种方式,就是传如绝对路径,依然可以搞定,代码中有体现,就不过多说了。。

修复方案:

换个过滤机制。

版权声明:转载请注明来源 @乌云


漏洞回应

厂商回应:

危害等级:无影响厂商忽略

忽略时间:2015-04-02 10:23

厂商回复:

最新状态:

2015-01-23:客户的版本是V3.12的。从V3.50开始已经彻底修复了,已经告知相应客户做什么谢谢这个大侠的提醒。