#碎碎念
当时做这题本身每抱太大希望,但稍微看了一下代码,确实能看出来就是任意Bean的方法调用,或者可能是fastjson的洞(这里只用到一点特性)。但是问题很多,那就是手头只有jar包,怎么知道有哪些已有的bean呢?为什么传入规定格式的请求,会报``呢?事后问了学长才知道是得调试的然而我完全不会调试,搜也搜不到一个靠谱的,最后还是学长指导才整会,这里先感谢Liki4学长和柏师傅的耐心指导喵。
#题面
题目给了jar包,结构长这样:
下面贴几段比较关键的源码:
// SpringContextHolder.java
package icu.Liki4.signin.util;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
@Component
public class SpringContextHolder implements ApplicationContextAware, DisposableBean {
private static final Logger logger = LoggerFactory.getLogger((Class<?>) SpringContextHolder.class);
private static ApplicationContext applicationContext = null;
@Override // org.springframework.beans.factory.DisposableBean
public void destroy() throws Exception {
clear();
}
@Override // org.springframework.context.ApplicationContextAware
public void setApplicationContext(ApplicationContext applicationContext2) throws BeansException {
applicationContext = applicationContext2;
}
public static ApplicationContext getApplicationContext() {
assertContextInjected();
return applicationContext;
}
public static ApplicationContext getApplicationContextNoEx() {
return applicationContext;
}
public static <T> T getExistBean(String str) {
try {
return (T) getBean(str);
} catch (NoSuchBeanDefinitionException e) {
logger.error(e.getMessage());
return null;
}
}
public static <T> T getBean(String str) {
assertContextInjected();
return (T) applicationContext.getBean(str);
}
public static <T> T getBean(Class<T> cls) {
assertContextInjected();
return (T) applicationContext.getBean(cls);
}
public static <T> T getBean(String str, Class<T> cls) {
assertContextInjected();
return (T) applicationContext.getBean(str, cls);
}
public static <T> Map<String, T> getBeansOfType(Class<T> type) {
return applicationContext.getBeansOfType(type);
}
public static void clear() {
applicationContext = null;
}
private static void assertContextInjected() {
if (applicationContext == null) {
throw new IllegalStateException(">> in SpringContextHolder's ApplicationContext is null");
}
}
}
// InvokeUtils.java
package icu.Liki4.signin.util;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONException;
import com.alibaba.fastjson2.JSONReader;
import com.alibaba.fastjson2.filter.Filter;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.context.annotation.Lazy;
public class InvokeUtils {
@Lazy
private static final Filter autoTypeFilter = JSONReader.autoTypeFilter((String[]) ((Set) Arrays.stream(SpringContextHolder.getApplicationContext().getBeanDefinitionNames()).map(name -> {
int secondDotIndex = name.indexOf(46, name.indexOf(46) + 1);
if (secondDotIndex != -1) {
return name.substring(0, secondDotIndex + 1);
}
return null;
}).filter((v0) -> {
return Objects.nonNull(v0);
}).collect(Collectors.toSet())).toArray(new String[0]));
public static Object invokeBeanMethod(String beanName, String methodName, Map<String, Object> params) throws Exception {
Object beanObject = SpringContextHolder.getBean(beanName);
Method beanMethod = (Method) Arrays.stream(beanObject.getClass().getMethods()).filter(method -> {
return method.getName().equals(methodName);
}).findFirst().orElse(null);
if (beanMethod.getParameterCount() == 0) {
return beanMethod.invoke(beanObject, new Object[0]);
}
String[] parameterTypes = new String[beanMethod.getParameterCount()];
Object[] parameterArgs = new Object[beanMethod.getParameterCount()];
for (int i = 0; i < beanMethod.getParameters().length; i++) {
Class<?> parameterType = beanMethod.getParameterTypes()[i];
String parameterName = beanMethod.getParameters()[i].getName();
parameterTypes[i] = parameterType.getName();
if (!parameterType.isPrimitive() && !Date.class.equals(parameterType) && !Long.class.equals(parameterType) && !Integer.class.equals(parameterType) && !Boolean.class.equals(parameterType) && !Double.class.equals(parameterType) && !Float.class.equals(parameterType) && !Short.class.equals(parameterType) && !Byte.class.equals(parameterType) && !Character.class.equals(parameterType) && !String.class.equals(parameterType) && !List.class.equals(parameterType) && !Set.class.equals(parameterType) && !Map.class.equals(parameterType)) {
if (params.containsKey(parameterName)) {
parameterArgs[i] = JSON.parseObject(JSON.toJSONString(params.get(parameterName)), (Class) parameterType, autoTypeFilter, new JSONReader.Feature[0]);
} else {
try {
parameterArgs[i] = JSON.parseObject(JSON.toJSONString(params), (Class) parameterType, autoTypeFilter, new JSONReader.Feature[0]);
} catch (JSONException e) {
for (Map.Entry<String, Object> entry : params.entrySet()) {
Object value = entry.getValue();
if ((value instanceof String) && ((String) value).contains("\"")) {
params.put(entry.getKey(), JSON.parse((String) value));
}
}
parameterArgs[i] = JSON.parseObject(JSON.toJSONString(params), (Class) parameterType, autoTypeFilter, new JSONReader.Feature[0]);
}
}
} else {
parameterArgs[i] = params.getOrDefault(parameterName, null);
}
}
return beanMethod.invoke(beanObject, parameterArgs);
}
}
// APIGatewayController.java
package icu.Liki4.signin.controller;
import ch.qos.logback.classic.encoder.JsonEncoder;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import icu.Liki4.signin.base.BaseResponse;
import icu.Liki4.signin.util.InvokeUtils;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Map;
import java.util.Objects;
import org.apache.commons.io.IOUtils;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
@RequestMapping({"/api"})
@Controller
/* loaded from: SigninJava.jar:BOOT-INF/classes/icu/Liki4/signin/controller/APIGatewayController.class */
public class APIGatewayController {
@RequestMapping(value = {"/gateway"}, method = {RequestMethod.POST})
@ResponseBody
public BaseResponse doPost(HttpServletRequest request) throws Exception {
try {
String body = IOUtils.toString(request.getReader());
Map<String, Object> map = (Map) JSON.parseObject(body, Map.class);
String beanName = (String) map.get("beanName");
String methodName = (String) map.get(JsonEncoder.METHOD_NAME_ATTR_NAME);
Map<String, Object> params = (Map) map.get("params");
if (StrUtil.containsAnyIgnoreCase(beanName, "flag")) {
return new BaseResponse(403, "flagTestService offline", null);
}
Object result = InvokeUtils.invokeBeanMethod(beanName, methodName, params);
return new BaseResponse(200, null, result);
} catch (Exception e) {
return new BaseResponse(500, ((Throwable) Objects.requireNonNullElse(e.getCause(), e)).getMessage(), null);
}
}
}
在网上一查就能知道,得到bean的一个方案是调用ApplicationContext类实例的getBean()方法,那我们没法改源码,但可以通过调试直接获得ApplicationContext类对象的各种属性,也就很顺理成章地拿到已有的bean了。接下来的问题是怎么调试。
在协会时Liki4学长当场开课讲了用远程JVM调试,即在本地启动jar服务,用idea连上它,就可以在idea上调试(这个教程网上很多,不过多赘述了)。但是在真正尝试的时候才发现,这样调试只能断方法断点或者异常断点,这完全达不到我们的需求。问了学长才知道,反编译之后的源码的每一行必须和实际执行的每一行对应,才能正常调试jar。这里的问题是出在了jar包的依赖没有展开,导致出现行断点无法使用。
正确的调试方法步骤如下:
- 首先将jar包中的lib目录提取出来,放到一个项目目录中。将其设置为库(add as library)
- 再将jar包中的源码部分(这里是icu.Liki4.signin)反编译,放入项目目录。这里要将其识别为源代码根目录。
- 将jar包本身放入当前目录,可以直接右键它进行调试了。
终于可以正常地调试了!真是坎坷。
接着我们就要拿ApplicationContext,将断点断在SpringContextHolder.java中的第49行,我们就能拿到ApplicationContext类对象了,其中存在beanFactory,也就是bean存储的地方了,发现其中的beanDefinitionNames,就能得到所有已注册bean的名字了。
这里注意到(通过wp注意到QwQ)cn.hutool.extra.spring.SpringUtil这个bean,它存在一个registerBean方法:
也就是说,我们只要传入一个自定义的beanName,以及它的类型就可以动态注册一个恶意bean(比如注册cn.hutool.core.util.RuntimeUtil)来实现rce。
那么在我们传这个json的时候问题又出现了,传入看起来完全没问题的json,却会一直报错Bean name must not be null
,这里其实并非BeanName出了问题,而是params的格式问题(有点小脑洞的),我们在InvokeUtils.java的第43行下断点,就能发现parameterName被设定为了arg0,arg1的形式:
这是String parameterName = beanMethod.getParameters()[i].getName();
所导致的,也就是我们希望执行的方法规定传入的参数名字就是arg0这样的形式。
到这里,总算是大功告成,能够成功的注册bean并且执行了。
#稍微总结一下
调试真是十分好的技巧捏。之后存在源码的话(尤其是Java这种经常可以动态修改的),可以考虑调试来得到一些信息或者做到某些事情。