某邮件管理系统代码审计

无限制爆破管理员密码

我这里本地搭建的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 = "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的,没啥作用就不写了。


原文始发于微信公众号(台下言书):某邮件管理系统代码审计

版权声明:admin 发表于 2023年10月9日 上午11:08。
转载请注明:某邮件管理系统代码审计 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...