对一次java代码审计的学习

0x01.前言

前段时间审计的项目~也是第一次审计java,跟表哥们分享一下。

一个电子对账系统,大部分是数据统计等功能,使用hibernate+Struts2+spring 框架,Oracle数据库,分前台后台两个项目,前台功能较单一就不说了,主要审计的后台。。

开发商说是个老项目了,2333,拿来练手还是不错的。

0x02.项目分析

先看src文件夹下,主要的逻辑处理代码,数据库操作代码等都存放在src下的包中,还有struts.xml struts的配置文件,以及一些初始化参数,比如init.properties中存放着数据库连接地址及账号密码。

common包下action存放 Action逻辑处理类,service包中存放service类,dao包中的类主要是对数据库的操作

filter包中主要存放的是 web.xml中配置的过滤类,所有请求都需要先经过filter中的过滤操作

pojo包中存放着实体类和hibernate的映射文件

util包中是一些常用工具类

来看下webroot文件夹下,web-inf中是常见的spring配置文件及web.xml等 其余的文件夹存放的都是页面jsp文件。

大致的执行流程就是,通过web.xml加载spring及struts2的配置文件等,然后执行的url先经过web.xml中配置的过滤类,如果请求的是action会通过struts2.xml中的配置 去找到src/com.aoyi.erp/common/action下的Action类中的函数并执行,action类中会调用service包下的service类再进行进一步处理,service类又会调用dao包中的数据库操作类进行操作,最后通过struts2.xml中的配置返回结果,当然其中会调用一些其余的工具类之类的。

url->filter->action->service->dao->.jsp页面或者数据等

这项目结构还是比较清晰明了的。。

0x03.越权

不管是前台还是后台,基本都是一个登录框,登录进去以后才有功能。

他本身是通过filter包中的类做过滤及权限判断的,可惜一个配置文件的疏忽,,,导致所有.action的请求都可未授权访问,,只要匹配action中需要的get或post参数就可以使用任意后台功能。。

我们先来看下其web.xml文件中设置的过滤器

<!-- Struts2.0清理过滤器 -->
     <filter>
           <filter-name>struts-cleanup</filter-name>
          <filter-class>org.apache.struts2.dispatcher.ActionContextCleanUp</filter-class>
     </filter>
     <filter-mapping>
           <filter-name>struts-cleanup</filter-name>
           <url-pattern>/*</url-pattern>
     </filter-mapping>

     <!-- Struts2.0转发过滤器 -->
     <filter>
           <filter-name>Struts2</filter-name>
          <filter-class>org.apache.struts2.dispatcher.FilterDispatcher</filter-class>
     </filter>
     <filter-mapping>
           <filter-name>Struts2</filter-name>
           <url-pattern>/*</url-pattern>
     </filter-mapping>
<!-- 登陆与字符过滤器  -->
     <filter>
           <filter-name>ChartsetFilter</filter-name>
          <filter-class>com.aoyi.erp.filter.ChartsetFilter</filter-class>
     </filter>

     <filter-mapping>
           <filter-name>ChartsetFilter</filter-name>
           <url-pattern>/*</url-pattern>
     </filter-mapping>
     <filter>

           <filter-name>loginfilter</filter-name>
          <filter-class>com.aoyi.erp.filter.LoginFilter</filter-class>
     </filter>
     <filter-mapping>
           <filter-name>loginfilter</filter-name>
           <url-pattern>/*</url-pattern>
     </filter-mapping>

一个是默认的struts2的过滤器,还自定义了2个过滤器,chartsetFilter和loginfilter,我们先来看看这两个类

chartsetFilter只是设置了下编码,

public void doFilter(ServletRequest arg0, ServletResponse arg1,
                FilterChain arg2) throws IOException, ServletException {
         arg0.setCharacterEncoding("utf-8");
         arg2.doFilter(arg0,arg1);
}

loginfilter中设置了登录权限验证

public void doFilter(ServletRequest arg0, ServletResponse arg1,
                FilterChain chain) throws IOException, ServletException {
           
           HttpServletRequest req = (HttpServletRequest)arg0;
         HttpServletResponse res = (HttpServletResponse)arg1;
        
             String uri = req.getRequestURI();
            
             if (!uri.endsWith(".jsp") && !uri.endsWith(".action")&&!uri.endsWith(".html")) {
                  chain.doFilter(req, res);
                  return ;
             }
             if (uri.endsWith("imgYezhe.action")||uri.endsWith("relogin.jsp")||uri.endsWith("image.jsp")||uri.endsWith("login.jsp")||uri.endsWith("loginAction.action")) {
                  chain.doFilter(req, res);
                  return ;
             }
            
             Bankuser admin=(Bankuser)req.getSession().getAttribute("username");

             uri = uri + (req.getQueryString()!=null?("?" + req.getQueryString()): "");
     
             if(admin!=null){
                  chain.doFilter(req, res);
             }else{
                  res.sendRedirect(req.getContextPath()+"/login.jsp");
             }

     }

先进行判断,如果请求的url中后缀不是 .jsp .action .html 的,放行。。

如果后缀为relogin.jsp,image.jsp之类的,放行。。

其余的则需要经过判断,如果session为空,未登录状态,则默认全部跳到login.jsp。。

想法是美好的,也确实对.jsp .action做了判断,但是,,配置文件的伤,

web.xml中 自定义的过滤器配置写到了struts2过滤器配置的后面,,这就出现了一个问题,所有.action的请求都会先走 struts2的默认过滤器,而不会去走自定义的过滤器了。。这就造成了所有action请求都可以正常访问,而后台的逻辑处理基本都在action中,可以通过action请求直接使用后台功能。
解决办法,,只需将自定义的过滤器配置写到 struts2默认的过滤器配置之前就可以了。。。

我们来选个功能测试下,,防止报错,需要传入action所需的get或post参数即可,因为struts2 Action接收参数的方式,get参数名写成bankuser.busername

url:http://localhost:8080/accountbank/testJdbc/selectAdmin.action?bankuser.busername=asd

可以使用该功能查出所有账户信息,,

通过越权,我们可以无需登录,去执行存在漏洞的action了。这里也有个getshell的问题,.jsp的请求再未登录的情况下都会跳转到login.jsp,getshell的话我们传jspx后缀的即可。。

0x04.注入

该项目大部分SQL语句都是直接拼接,但是有些地方因为代码问题及oracle数据库的特性,还有的地方使用了HQL语句,大部分注入都跑不出数据或者只能延时跑,这里我就复现一处可union的注入。

在src/com.aoyi.erp/common/action/admin/ClientAction.java文件selectList函数中

/**
      * 客户密码重置模块的查询action
      */
     public String selectList(){
           log.info("客户密码重置模块的查询-----action");
           String clicode = getRequest().getParameter("clicode");
           String cliname = getRequest().getParameter("cliname");
           List list = clientService.selectList(clicode, cliname);
           Map map =null;
           if(list.size()>0){
           map =(Map)list.get(0);
           }
           getRequest().setAttribute("returnlist", list);
           getRequest().setAttribute("clicode", clicode);
           getRequest().setAttribute("cliname", map.get("cliname"));
           return SUCCESS;
     }

调用clientService.selectList函数进行处理,跟进一下

该函数在accountbank/src/ com.aoyi.erp/common/ service/admin/ClientService.java文件中

public List selectList(String clicode,String cliname){
           String sql =
                     " select t.cliname cliname,t.clicode clicode, t.cusername cname1,t.cusername2 cname2, t.linkmanonephone  phone1,t.linkmanonemobile mobile1, " +
                     " t.linkmantwophone  phone2, t.linkmantwomobile mobile2,t.linkmanone lname1,t.linkmantwo lname2   " +
                     " from clientcontact t   ";
           List list = new ArrayList();
          if(clicode!=""&&clicode.length()>0&&cliname==""&&cliname.length()<=0){
                sql+=" where  t.clicode = '"+clicode+"' ";
           }
          if(cliname!=""&&cliname.length()>0&&clicode==""&&clicode.length()<=0){
                sql+=" where  t.cliname = '"+cliname+"' ";
           }
          if(cliname!=""&&cliname.length()>0&&clicode!=""&&clicode.length()>0){
                sql+=" where  t.cliname = '"+cliname+"' and t.clicode = '"+clicode+"' ";
           }
            try {
                System.out.println(sql);
                list = sqlDAO.findListBySQL(sql);
           } catch (SQLException e) {
                // TODO Auto-generated catch block
                log.info("sql出错"+e);
                e.printStackTrace();
           } catch (Exception e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
           }
           return list;
     }

直接拼接,最后调用dao包的sqlDAO.findListBySQL函数进行数据库操作。。通过struts2.xml找到其对应的action名为selectList


url:http://localhost:8080/accountbank/client/selectList.action

post:clicode=123' union select NULL,NULL,NULL,NULL,(select banner from sys.v_$version where rownum=1),NULL,NULL,NULL,NULL,NULL from dual-- -&cliname=

还可以利用注入更改任意账户密码,,23333

在src/com.aoyi.erp/common/action/admin/AdminAction.java文件updateAdminpass函数中

public String updateAdminpass(){
     try{
      boolean b=adminService.updateAdminpassword(bankuser);
      if(b){
            this.getRequest().setAttribute("updatepass","<font color='blue' size='2' >"+bankuser.getBusername()+"管理员修改密码成功,下次登陆请使用新密码。</font>");
      }else{
            this.getRequest().setAttribute("updatepass","<font color='red' size='2' >对不起,"+bankuser.getBusername()+"管理员修改密码失败,请重新修改密码。</font>");
        }
     }catch(Exception e){log.error("修改密码失败--->"+e);}
           return this.SUCCESS;
     }

调用adminService.updateAdminpassword函数,跟进一下

/*
      * 修改网点管理员密码
      * */
     public boolean updateAdminpassword(Bankuser bankuser)throws Exception
     {
           boolean b=false;
              String hql="update bankuser b set b.bpassword='"+PasswordEncrypt.encryptPassword(bankuser.getBpassword().trim())+"' where b.busername='"+bankuser.getBusername().trim()+"' and  b.bpassword='"+PasswordEncrypt.encryptPassword(bankuser.getBankcode().trim())+"'";
              int  q=sqlDAO.executeSQL(hql);                  
              log.info(q+"修改对账管理员:"+bankuser.getBusername().trim()+"的密码成功");      
                 if(q>0){ b=true;
                 }else{
                     log.error("修改对账管理员:"+bankuser.getBusername().trim()+"的密码失败");
                }
           return b;
     }

匹配bpassword新密码,bankcode旧密码,busername用户名,

用户名需要知道,可以通过上文的功能查到所有用户名,,

找到其对应的action名

0x05.有限制文件上传及文件遍历

对文件名后缀只是js验证,但是因为上传路径有默认值,不在web根路径下,所以我们需要知道绝对路径才可上传,或者可以使用相对路径,但是文件处理的操作在java类中,执行时相对路径也不在web根路径下,

在src/com.aoyi.erp/common/action/dataimport/DataImportAction文件dayUpload函数中,功能url:http://localhost:8080/accountbank/dataimport/importData.jsp

     private String savePath=InitConfigListener.rootpath;//下载要保存的路径
     //日上传文件使用
     private File[] dayUpload;
     private String[] dayUploadContentType;
     private String[] dayUploadFileName;  
     
     
     public String dayUpload() {          
           String upload="";
           File file = new File(savePath);
           savePath = file.getParent();

           File file2 = new File(savePath);
           savePath = file2.getParent()+File.separator;

           try{
                String month = this.getRequest().getParameter("month");
                //dataImportService.deleteImportData("dpfm31.txt", month);
                String clientPath = this.getRequest().getParameter("clientPath");              
                byte db[];
                db = clientPath.getBytes("iso-8859-1");
                clientPath = new String(db,"UTF-8");
                log.info(clientPath);
                upload=dataImportService.dayUpload(dayUpload, dayUploadFileName, savePath,clientPath,month);
                this.getRequest().setAttribute("dayMeg", "<font color='blue' >"+upload+"导入成功</font>");
           }catch(Exception e){
                this.getRequest().setAttribute("dayMeg", "<font color='red' >"+upload+"导入失败</font>");
                e.printStackTrace();
           }
           return SUCCESS;
     }

这里可以同时上传3个文件,所以 dayUpload和dayUploadFileName都为数组,dayUploadFileName就是最终的上传文件名。

还有个重要的参数就是 上传路径savePath,已经有了默认值,并且也没有获取get或post等参数,而且默认值不在web根目录下,因为savePath声明了get/set方法,所以我们可以利用struts2 Action接收参数的特性,构造一个get或者post参数,名为savePath,这样我们传入的值就可以覆盖savePath的默认值了。

然后对savePath进行了两次操作,获取2次其父路径,比如F:/test/test2/test3/ 经过两次getParent后 变为F:/test/,然后传入dataImportService.dayUpload函数进行上传操作,

在src/com.aoyi.erp/common/service/dataimport/DataImportService文件中

public String dayUpload(File[] files,String[] getUploadFileName,String savePath,String clientPath,String month) throws Exception{
           String result="";
           savePath = getPath(savePath);//得到根据日期的路径
           upload.multiUpload(files, getUploadFileName, savePath);//上传文件

           for (int i = 0; i < getUploadFileName.length; i++) {
                     if(!"accbook3.txt".equalsIgnoreCase(getUploadFileName[i].toString())){
                           //清除数据
                          System.out.println(getUploadFileName[i]);
                           System.out.println(month);
                           cleanImportData(getUploadFileName[i], month);
                         commonImport(getUploadFileName[i],savePath);
                         }else if("accbook3.txt".equalsIgnoreCase(getUploadFileName[i].toString())){
                         result+=insertIntoAccbook1(savePath);
                        }
                     result+=saveUploadLog(clientPath,savePath,getUploadFileName[i]);                                                                           
                     //记录日志
                     if(getUploadFileName[i].toString().equals("dpfm31.txt")){
                     }
                }
                return result;
     }

其中又对savePath进行了操作,取得了最终的上传路径,跟进一下getPath函数

     public String getPath(String savePath) throws IOException{
           Date date = new Date();
           String tempDate = new java.text.SimpleDateFormat("yyyy-MM-dd").format(date);
           savePath =savePath+tempDate.substring(0,4)+File.separator+tempDate.substring(5,7)+File.separator+tempDate+File.separator;
           FileUtils.forceMkdir(new File(savePath));
           log.info("要上传到的文件夹是:"+savePath);
           return savePath;
     }

根据日期生成路径,不存在的日期文件夹则进行创建,最终的文件路径为 savePath+2017/12/2017-12-29/ 也就是当前日期,

最后通过multiUpload函数进行文件批量上传操作。

通过struts2.xml找到其对应的action名为dayUpload
.

可以直接将他的上传页面复制下来多加一个savePath的表单进行上传。也可以直接把savePath通过get方式传递。


或者

这里savePath的值改成F:\Apache Software Foundation\Tomcat 7.0\webapps\accountbank\asd\as\ 这样的,经过两次getParent后则为正确路径了。

image

这里的jspx马我是用的cknife中的。

这时候要getshell还有个问题,就是要找到其绝对路径。找到一处可以遍历文件的地方,可以通过文件遍历找到项目绝对路径。

在src/com.aoyi.erp/common/action/dataimport/DataImportAction文件dataBackopendir函数中,

public String dataBackopendir(){
           try{
              String filepath=getRequest().getParameter("filepath").trim();     
              log.info("***************"+filepath+"****************");
            List list=dataImportService.getFile(filepath);
            StringBuffer xml=new StringBuffer();
            DateFormat form=new SimpleDateFormat("yyyy-MM-dd");
            if(list!=null&&list.size()>0){
                for(int i=0;i<list.size();i++){
                          File objfile =  (File)list.get(i);
                          xml.append(objfile.getName()+"|"+form.format(new Date(objfile.lastModified()))+"||");
                     }}
                 PrintWriter out=getResponse().getWriter();
                
                     System.out.println("文件夹"+xml.toString());
                      out.println(xml.toString());
                      out.flush();
           }catch(Exception e){log.info("打开文件夹出错"+e);e.printStackTrace();}
      return  null;
         
  }

获取filepath参数,根据filepath的值调用dataImportService.getFile函数获取List集合,然后遍历List,按 文件名|最后修改时间|| 添加到StringBuffer中,最后通过out输出。

跟进一下getFile函数

public  List getFile(String filedir){
           String filePath=ftpPath+filedir;
           log.info("文件路径---****---"+filePath);
           File file = new File(filePath);
           if(!file.exists()) file.mkdirs();
           File[] listFile = file.listFiles();
           List list = new ArrayList();
           for(int i=0;i<listFile.length;i++){
                System.out.println(listFile[i]);
                File tempFile = listFile[i];
                  list.add(tempFile);
             }
           return list;
     }

该函数根据文件路径,获取当前文件夹下的所有文件及文件夹,最后循环遍历,添加进List集合中。

文件夹路径是由ftpPath+filedir组合的,filedir使我们可控的,ftpPath则获取的是WEB-INF下的sys-config.xml中的savePath的值,默认为/data,组合一下就是/data+filedir

通过struts2.xml配置文件找到其对应的action名,为dataBackopendir

可以看到默认路径其实是linux的,如果是linux服务器搭建可以直接遍历,但如果是windows搭建也是可以的。

windows下,测试了一下发现 /data/../ 也可以遍历路径,默认遍历的是 tomcat所在盘符下的根路径,不影响我们找到网站的绝对路径。

手找太慢的话当然我们可以写个脚本去跑一下,最后找到项目绝对路径就可以结合上传getshell了~~~

0x06.后记

感觉java的审计首先项目所用的框架我们必须要了解,最好还是有可以部署的源码,因为有些项目设计比较繁琐,如果能通过断点追踪程序运行更有利于我们去理解整个项目,如果做过java方面的开发就更好了,

---------华丽的分割线--------
审什么计啊?有这时间找个对象不好吗???,我现在已经是条废鱼了,话说新论坛的界面很好看鸭。

1 个赞