LOADING

首次加载会比较慢,果咩~

请打开缓存,下次打开就会很快啦

从SignIn_Java 学jar调试

#碎碎念

  当时做这题本身每抱太大希望,但稍微看了一下代码,确实能看出来就是任意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包的依赖没有展开,导致出现行断点无法使用。
  正确的调试方法步骤如下:

  1. 首先将jar包中的lib目录提取出来,放到一个项目目录中。将其设置为库(add as library)
  2. 再将jar包中的源码部分(这里是icu.Liki4.signin)反编译,放入项目目录。这里要将其识别为源代码根目录。
  3. 将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这种经常可以动态修改的),可以考虑调试来得到一些信息或者做到某些事情。