diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..68dfbc0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.idea/ +target/ +*.iml +dependency-reduced-pom.xml diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a512353 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Azisaba Network + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8731cf6 --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# Log4j2Fix + +This program fixes remote code execution vulnerability in log4j2 (v2.0.0 - v2.14.1). + +## Usage +This program can be used as wrapper for another jar file: + +`java -javaagent:Log4j2Fix-1.0.0.jar ...` or `java -jar Log4j2Fix-1.0.0.jar another-jar-file.jar [main class if MANIFEST.MF does not have Main-Class attribute] [arguments]` + +## Note +- This does not protect from ldap server hosted by localhost (127.0.0.1) +- If installed on server, it does not protect client from being abused. + To protect the client, you would need a different solution such as blocking a malicious packet. +- If installed on Minecraft server, you can protect the client by doing these additionally: + - Cancel malicious `ChatEvent` on BungeeCord + - Cancel malicious `AsyncPlayerChatEvent` on Spigot/Paper + - Cancel outbound packet that contains malicious string +- Or alternatively, you can upgrade log4j2 to `2.15.0-SNAPSHOT`. diff --git a/log4j2-LICENSE.txt b/log4j2-LICENSE.txt new file mode 100644 index 0000000..6279e52 --- /dev/null +++ b/log4j2-LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 1999-2005 The Apache Software Foundation + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..2e03ecb --- /dev/null +++ b/pom.xml @@ -0,0 +1,71 @@ + + + 4.0.0 + + net.azisaba + Log4j2Fix + 1.0.0 + + + 8 + 8 + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.1.0 + + + package + + shade + + + false + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.2.0 + + + true + + true + + + net.azisaba.log4j2Fix.Log4j2Fix + net.azisaba.log4j2Fix.Log4j2Fix + + + + + + + + + acrylic-repo + https://repo2.acrylicstyle.yz/ + + + + + net.blueberrymc + native-util + 1.2.5 + + + org.apache.logging.log4j + log4j-core + 2.14.1 + provided + + + diff --git a/src/main/java/net/azisaba/log4j2Fix/Log4j2Fix.java b/src/main/java/net/azisaba/log4j2Fix/Log4j2Fix.java new file mode 100644 index 0000000..89f556d --- /dev/null +++ b/src/main/java/net/azisaba/log4j2Fix/Log4j2Fix.java @@ -0,0 +1,122 @@ +package net.azisaba.log4j2Fix; + +import net.blueberrymc.native_util.NativeUtil; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.instrument.Instrumentation; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +public class Log4j2Fix { + public static void main(String[] args) throws IOException { + transformClasses(); + List arguments = new ArrayList<>(Arrays.asList(args)); + if (arguments.isEmpty()) { + System.out.println("wat"); + return; + } + String main = arguments.remove(0); + File file = new File(main); + ClassLoader classLoader; + if (file.exists()) { + classLoader = new URLClassLoader(new URL[]{file.toURI().toURL()}, Log4j2Fix.class.getClassLoader()); + ZipFile zipFile = new ZipFile(file); + ZipEntry zipEntry = zipFile.getEntry("META-INF/MANIFEST.MF"); + if (zipEntry == null) { + if (arguments.isEmpty()) { + System.out.println("Could not find Main-Class attribute from " + file.getPath()); + System.exit(1); + } + main = arguments.remove(0); + } else { + BufferedReader reader = new BufferedReader(new InputStreamReader(zipFile.getInputStream(zipEntry))); + String read; + boolean found = false; + while ((read = reader.readLine()) != null) { + if (read.startsWith("Main-Class: ")) { + main = read.replace("Main-Class: ", ""); + found = true; + break; + } + } + reader.close(); + if (!found) { + if (arguments.isEmpty()) { + System.out.println("Could not find Main-Class attribute from " + file.getPath()); + System.exit(1); + } + main = arguments.remove(0); + } + } + } else { + classLoader = Log4j2Fix.class.getClassLoader(); + } + try { + Class clazz = Class.forName(main, false, classLoader); + Method m = clazz.getMethod("main", String[].class); + m.invoke(null, (Object) arguments.toArray(new String[0])); + } catch (ReflectiveOperationException e) { + System.err.println("Failed to invoke main method of class " + main); + e.printStackTrace(); + } + } + + public static void agentmain(String args, Instrumentation instrumentation) throws IOException { + transformClasses(); + } + + public static void premain(String args, Instrumentation instrumentation) throws IOException { + transformClasses(); + } + + public static void transformClasses() throws IOException { + transformClass("org.apache.logging.log4j.core.appender.mom.JmsAppender"); + transformClass("org.apache.logging.log4j.core.appender.mom.JmsAppender$1"); + transformClass("org.apache.logging.log4j.core.appender.mom.JmsAppender$Builder"); + transformClass("org.apache.logging.log4j.core.net.JndiManager"); + transformClass("org.apache.logging.log4j.core.net.JndiManager$1"); + transformClass("org.apache.logging.log4j.core.net.JndiManager$JndiManagerFactory"); + transformClass("org.apache.logging.log4j.core.util.NetUtils"); + } + + public static void transformClass(String className) throws IOException { + Class clazz = Arrays.stream(NativeUtil.getLoadedClasses()).filter(clazz2 -> clazz2.getTypeName().equals(className)).findFirst().orElse(null); + if (clazz == null) { + String path = "/classes/" + className.replace('.', '/') + ".class"; + InputStream in = Log4j2Fix.class.getResourceAsStream(path); + if (in == null) throw new RuntimeException("Could not find '" + path + "' in jar file"); + byte[] newClassBytes = readAllBytes(in); + System.out.println(className + " is not loaded, registering class load hook"); + NativeUtil.registerClassLoadHook((classLoader, s, aClass, protectionDomain, bytes) -> { + if (s.equals(className.replace('.', '/'))) { + System.out.println("Transformed " + className); + return newClassBytes; + } + return null; + }); + } else { + System.err.println(className + " is already loaded, cannot process " + className); + } + } + + public static byte[] readAllBytes(InputStream in) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] buf = new byte[1024 * 16]; + int read; + while ((read = in.read(buf, 0, 1024 * 16)) > 0) { + baos.write(buf, 0, read); + } + return baos.toByteArray(); + } +} diff --git a/src/main/resources/classes/org/apache/logging/log4j/core/appender/mom/JmsAppender$1.class b/src/main/resources/classes/org/apache/logging/log4j/core/appender/mom/JmsAppender$1.class new file mode 100644 index 0000000..9562dbd Binary files /dev/null and b/src/main/resources/classes/org/apache/logging/log4j/core/appender/mom/JmsAppender$1.class differ diff --git a/src/main/resources/classes/org/apache/logging/log4j/core/appender/mom/JmsAppender$Builder.class b/src/main/resources/classes/org/apache/logging/log4j/core/appender/mom/JmsAppender$Builder.class new file mode 100644 index 0000000..75fdb33 Binary files /dev/null and b/src/main/resources/classes/org/apache/logging/log4j/core/appender/mom/JmsAppender$Builder.class differ diff --git a/src/main/resources/classes/org/apache/logging/log4j/core/appender/mom/JmsAppender.class b/src/main/resources/classes/org/apache/logging/log4j/core/appender/mom/JmsAppender.class new file mode 100644 index 0000000..87e240d Binary files /dev/null and b/src/main/resources/classes/org/apache/logging/log4j/core/appender/mom/JmsAppender.class differ diff --git a/src/main/resources/classes/org/apache/logging/log4j/core/net/JndiManager$1.class b/src/main/resources/classes/org/apache/logging/log4j/core/net/JndiManager$1.class new file mode 100644 index 0000000..8264251 Binary files /dev/null and b/src/main/resources/classes/org/apache/logging/log4j/core/net/JndiManager$1.class differ diff --git a/src/main/resources/classes/org/apache/logging/log4j/core/net/JndiManager$JndiManagerFactory.class b/src/main/resources/classes/org/apache/logging/log4j/core/net/JndiManager$JndiManagerFactory.class new file mode 100644 index 0000000..bcf8d5b Binary files /dev/null and b/src/main/resources/classes/org/apache/logging/log4j/core/net/JndiManager$JndiManagerFactory.class differ diff --git a/src/main/resources/classes/org/apache/logging/log4j/core/net/JndiManager.class b/src/main/resources/classes/org/apache/logging/log4j/core/net/JndiManager.class new file mode 100644 index 0000000..81779a1 Binary files /dev/null and b/src/main/resources/classes/org/apache/logging/log4j/core/net/JndiManager.class differ diff --git a/src/main/resources/classes/org/apache/logging/log4j/core/util/NetUtils.class b/src/main/resources/classes/org/apache/logging/log4j/core/util/NetUtils.class new file mode 100644 index 0000000..61df204 Binary files /dev/null and b/src/main/resources/classes/org/apache/logging/log4j/core/util/NetUtils.class differ diff --git a/src/main/resources/sources/org/apache/logging/log4j/core/appender/mom/JmsAppender.java b/src/main/resources/sources/org/apache/logging/log4j/core/appender/mom/JmsAppender.java new file mode 100644 index 0000000..d5b2ebe --- /dev/null +++ b/src/main/resources/sources/org/apache/logging/log4j/core/appender/mom/JmsAppender.java @@ -0,0 +1,294 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache license, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package org.apache.logging.log4j.core.appender.mom; + +import java.io.Serializable; +import java.util.Properties; +import java.util.concurrent.TimeUnit; + +import org.apache.logging.log4j.core.Appender; +import org.apache.logging.log4j.core.Filter; +import org.apache.logging.log4j.core.Layout; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.appender.AbstractManager; +import org.apache.logging.log4j.core.appender.mom.JmsManager.JmsManagerConfiguration; +import org.apache.logging.log4j.core.config.Node; +import org.apache.logging.log4j.core.config.Property; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.config.plugins.PluginAliases; +import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute; +import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory; +import org.apache.logging.log4j.core.config.plugins.validation.constraints.Required; +import org.apache.logging.log4j.core.net.JndiManager; + +/** + * Generic JMS Appender plugin for both queues and topics. This Appender replaces the previous split ones. However, + * configurations set up for the 2.0 version of the JMS appenders will still work. + */ +@Plugin(name = "JMS", category = Node.CATEGORY, elementType = Appender.ELEMENT_TYPE, printObject = true) +@PluginAliases({ "JMSQueue", "JMSTopic" }) +public class JmsAppender extends AbstractAppender { + + public static class Builder> extends AbstractAppender.Builder + implements org.apache.logging.log4j.core.util.Builder { + + public static final int DEFAULT_RECONNECT_INTERVAL_MILLIS = 5000; + + @PluginBuilderAttribute + private String factoryName; + + @PluginBuilderAttribute + private String providerUrl; + + @PluginBuilderAttribute + private String urlPkgPrefixes; + + @PluginBuilderAttribute + private String securityPrincipalName; + + @PluginBuilderAttribute(sensitive = true) + private String securityCredentials; + + @PluginBuilderAttribute + @Required(message = "A javax.jms.ConnectionFactory JNDI name must be specified") + private String factoryBindingName; + + @PluginBuilderAttribute + @PluginAliases({ "queueBindingName", "topicBindingName" }) + @Required(message = "A javax.jms.Destination JNDI name must be specified") + private String destinationBindingName; + + @PluginBuilderAttribute + private String userName; + + @PluginBuilderAttribute(sensitive = true) + private char[] password; + + @PluginBuilderAttribute + private long reconnectIntervalMillis = DEFAULT_RECONNECT_INTERVAL_MILLIS; + + @PluginBuilderAttribute + private boolean immediateFail; + + @PluginBuilderAttribute + private String allowedLdapClasses; + + @PluginBuilderAttribute + private String allowedLdapHosts; + + @PluginBuilderAttribute + private String allowedJndiProtocols; + + // Programmatic access only for now. + private JmsManager jmsManager; + + private Builder() { + } + + @SuppressWarnings("resource") // actualJmsManager and jndiManager are managed by the JmsAppender + @Override + public JmsAppender build() { + JmsManager actualJmsManager = jmsManager; + JmsManagerConfiguration configuration = null; + if (actualJmsManager == null) { + Properties additionalProperties = null; + if (allowedLdapClasses != null || allowedLdapHosts != null) { + additionalProperties = new Properties(); + if (allowedLdapHosts != null) { + additionalProperties.put(JndiManager.ALLOWED_HOSTS, allowedLdapHosts); + } + if (allowedLdapClasses != null) { + additionalProperties.put(JndiManager.ALLOWED_CLASSES, allowedLdapClasses); + } + if (allowedJndiProtocols != null) { + additionalProperties.put(JndiManager.ALLOWED_PROTOCOLS, allowedJndiProtocols); + } + } + final Properties jndiProperties = JndiManager.createProperties(factoryName, providerUrl, urlPkgPrefixes, + securityPrincipalName, securityCredentials, additionalProperties); + configuration = new JmsManagerConfiguration(jndiProperties, factoryBindingName, destinationBindingName, + userName, password, false, reconnectIntervalMillis); + actualJmsManager = AbstractManager.getManager(getName(), JmsManager.FACTORY, configuration); + } + if (actualJmsManager == null) { + // JmsManagerFactory has already logged an ERROR. + return null; + } + final Layout layout = getLayout(); + if (layout == null) { + LOGGER.error("No layout provided for JmsAppender"); + return null; + } + //try { + return new JmsAppender(getName(), getFilter(), layout, isIgnoreExceptions(), getPropertyArray(), + actualJmsManager); + /*} catch (final JMSException e) { + // Never happens since the ctor no longer actually throws a JMSException. + throw new IllegalStateException(e); + }*/ + } + + public Builder setDestinationBindingName(final String destinationBindingName) { + this.destinationBindingName = destinationBindingName; + return this; + } + + public Builder setFactoryBindingName(final String factoryBindingName) { + this.factoryBindingName = factoryBindingName; + return this; + } + + public Builder setFactoryName(final String factoryName) { + this.factoryName = factoryName; + return this; + } + + public Builder setImmediateFail(final boolean immediateFail) { + this.immediateFail = immediateFail; + return this; + } + + public Builder setJmsManager(final JmsManager jmsManager) { + this.jmsManager = jmsManager; + return this; + } + + public Builder setPassword(final char[] password) { + this.password = password; + return this; + } + + /** + * @deprecated Use setPassword(char[]) + */ + @Deprecated + public Builder setPassword(final String password) { + this.password = password == null ? null : password.toCharArray(); + return this; + } + + public Builder setProviderUrl(final String providerUrl) { + this.providerUrl = providerUrl; + return this; + } + + public Builder setReconnectIntervalMillis(final long reconnectIntervalMillis) { + this.reconnectIntervalMillis = reconnectIntervalMillis; + return this; + } + + public Builder setSecurityCredentials(final String securityCredentials) { + this.securityCredentials = securityCredentials; + return this; + } + + public Builder setSecurityPrincipalName(final String securityPrincipalName) { + this.securityPrincipalName = securityPrincipalName; + return this; + } + + public Builder setUrlPkgPrefixes(final String urlPkgPrefixes) { + this.urlPkgPrefixes = urlPkgPrefixes; + return this; + } + + /** + * @deprecated Use {@link #setUserName(String)}. + */ + @Deprecated + public Builder setUsername(final String username) { + this.userName = username; + return this; + } + + public Builder setUserName(final String userName) { + this.userName = userName; + return this; + } + + public Builder setAllowedLdapClasses(final String allowedLdapClasses) { + this.allowedLdapClasses = allowedLdapClasses; + return this; + } + + public Builder setAllowedLdapHosts(final String allowedLdapHosts) { + this.allowedLdapHosts = allowedLdapHosts; + return this; + } + + public Builder setAllowedJndiProtocols(final String allowedJndiProtocols) { + this.allowedJndiProtocols = allowedJndiProtocols; + return this; + } + + /** + * Does not include the password. + */ + @Override + public String toString() { + return "Builder [name=" + getName() + ", factoryName=" + factoryName + ", providerUrl=" + providerUrl + + ", urlPkgPrefixes=" + urlPkgPrefixes + ", securityPrincipalName=" + securityPrincipalName + + ", securityCredentials=" + securityCredentials + ", factoryBindingName=" + factoryBindingName + + ", destinationBindingName=" + destinationBindingName + ", username=" + userName + ", layout=" + + getLayout() + ", filter=" + getFilter() + ", ignoreExceptions=" + isIgnoreExceptions() + + ", jmsManager=" + jmsManager + ", allowedLdapClasses=" + allowedLdapClasses + + ", allowedLdapHosts=" + allowedLdapHosts + ", allowedJndiProtocols=" + allowedJndiProtocols + "]"; + } + + } + + @PluginBuilderFactory + public static Builder newBuilder() { + return new Builder(); + } + + private volatile JmsManager manager; + + protected JmsAppender(final String name, final Filter filter, final Layout layout, + final boolean ignoreExceptions, final Property[] properties, final JmsManager manager) /*throws JMSException*/ { + super(name, filter, layout, ignoreExceptions, properties); + this.manager = manager; + } + + @Deprecated + protected JmsAppender(final String name, final Filter filter, final Layout layout, + final boolean ignoreExceptions, final JmsManager manager) /*throws JMSException*/ { + super(name, filter, layout, ignoreExceptions, Property.EMPTY_ARRAY); + this.manager = manager; + } + + @Override + public void append(final LogEvent event) { + this.manager.send(event, toSerializable(event)); + } + + public JmsManager getManager() { + return manager; + } + + @Override + public boolean stop(final long timeout, final TimeUnit timeUnit) { + setStopping(); + boolean stopped = super.stop(timeout, timeUnit, false); + stopped &= this.manager.stop(timeout, timeUnit); + setStopped(); + return stopped; + } + +} diff --git a/src/main/resources/sources/org/apache/logging/log4j/core/net/JndiManager.java b/src/main/resources/sources/org/apache/logging/log4j/core/net/JndiManager.java new file mode 100644 index 0000000..0dd757e --- /dev/null +++ b/src/main/resources/sources/org/apache/logging/log4j/core/net/JndiManager.java @@ -0,0 +1,300 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache license, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package org.apache.logging.log4j.core.net; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.TimeUnit; + +import javax.naming.Context; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.directory.DirContext; +import javax.naming.directory.InitialDirContext; + +import org.apache.logging.log4j.core.appender.AbstractManager; +import org.apache.logging.log4j.core.appender.ManagerFactory; +import org.apache.logging.log4j.core.util.JndiCloser; +import org.apache.logging.log4j.core.util.NetUtils; +import org.apache.logging.log4j.util.PropertiesUtil; + +/** + * Manages a JNDI {@link javax.naming.directory.DirContext}. + * + * @since 2.1 + */ +public class JndiManager extends AbstractManager { + public static final String ALLOWED_HOSTS = "allowedLdapHosts"; + public static final String ALLOWED_CLASSES = "allowedLdapClasses"; + public static final String ALLOWED_PROTOCOLS = "allowedJndiProtocols"; + + private static final JndiManagerFactory FACTORY = new JndiManagerFactory(); + private static final String PREFIX = "log4j2."; + private static final String LDAP = "ldap"; + private static final String LDAPS = "ldaps"; + private static final String JAVA = "java"; + private static final List permanentAllowedHosts = NetUtils.getLocalIps(); + private static final List permanentAllowedClasses = Arrays.asList(Boolean.class.getName(), + Byte.class.getName(), Character.class.getName(), Double.class.getName(), Float.class.getName(), + Integer.class.getName(), Long.class.getName(), Short.class.getName(), String.class.getName()); + private static final List permanentAllowedProtocols = Arrays.asList(JAVA, LDAP, LDAPS); + private static final String SERIALIZED_DATA = "javaSerializedData"; + private static final String CLASS_NAME = "javaClassName"; + private static final String REFERENCE_ADDRESS = "javaReferenceAddress"; + private static final String OBJECT_FACTORY = "javaFactory"; + private final List allowedHosts; + private final List allowedClasses; + private final List allowedProtocols; + + private final DirContext context; + + private JndiManager(final String name, final DirContext context, final List allowedHosts, + final List allowedClasses, final List allowedProtocols) { + super(null, name); + this.context = context; + this.allowedHosts = allowedHosts; + this.allowedClasses = allowedClasses; + this.allowedProtocols = allowedProtocols; + } + + /** + * Gets the default JndiManager using the default {@link javax.naming.InitialContext}. + * + * @return the default JndiManager + */ + public static JndiManager getDefaultManager() { + return getManager(JndiManager.class.getName(), FACTORY, null); + } + + /** + * Gets a named JndiManager using the default {@link javax.naming.InitialContext}. + * + * @param name the name of the JndiManager instance to create or use if available + * @return a default JndiManager + */ + public static JndiManager getDefaultManager(final String name) { + return getManager(name, FACTORY, null); + } + + /** + * Gets a JndiManager with the provided configuration information. + * + * @param initialContextFactoryName Fully qualified class name of an implementation of + * {@link javax.naming.spi.InitialContextFactory}. + * @param providerURL The provider URL to use for the JNDI connection (specific to the above factory). + * @param urlPkgPrefixes A colon-separated list of package prefixes for the class name of the factory + * class that will create a URL context factory + * @param securityPrincipal The name of the identity of the Principal. + * @param securityCredentials The security credentials of the Principal. + * @param additionalProperties Any additional JNDI environment properties to set or {@code null} for none. + * @return the JndiManager for the provided parameters. + */ + public static JndiManager getJndiManager(final String initialContextFactoryName, + final String providerURL, + final String urlPkgPrefixes, + final String securityPrincipal, + final String securityCredentials, + final Properties additionalProperties) { + final Properties properties = createProperties(initialContextFactoryName, providerURL, urlPkgPrefixes, + securityPrincipal, securityCredentials, additionalProperties); + return getManager(createManagerName(), FACTORY, properties); + } + + /** + * Gets a JndiManager with the provided configuration information. + * + * @param properties JNDI properties, usually created by calling {@link #createProperties(String, String, String, String, String, Properties)}. + * @return the JndiManager for the provided parameters. + * @see #createProperties(String, String, String, String, String, Properties) + * @since 2.9 + */ + public static JndiManager getJndiManager(final Properties properties) { + return getManager(createManagerName(), FACTORY, properties); + } + + private static String createManagerName() { + return JndiManager.class.getName() + '@' + JndiManager.class.hashCode(); + } + + /** + * Creates JNDI Properties with the provided configuration information. + * + * @param initialContextFactoryName + * Fully qualified class name of an implementation of {@link javax.naming.spi.InitialContextFactory}. + * @param providerURL + * The provider URL to use for the JNDI connection (specific to the above factory). + * @param urlPkgPrefixes + * A colon-separated list of package prefixes for the class name of the factory class that will create a + * URL context factory + * @param securityPrincipal + * The name of the identity of the Principal. + * @param securityCredentials + * The security credentials of the Principal. + * @param additionalProperties + * Any additional JNDI environment properties to set or {@code null} for none. + * @return the Properties for the provided parameters. + * @since 2.9 + */ + public static Properties createProperties(final String initialContextFactoryName, final String providerURL, + final String urlPkgPrefixes, final String securityPrincipal, final String securityCredentials, + final Properties additionalProperties) { + if (initialContextFactoryName == null) { + return null; + } + final Properties properties = new Properties(); + properties.setProperty(Context.INITIAL_CONTEXT_FACTORY, initialContextFactoryName); + if (providerURL != null) { + properties.setProperty(Context.PROVIDER_URL, providerURL); + } else { + LOGGER.warn("The JNDI InitialContextFactory class name [{}] was provided, but there was no associated " + + "provider URL. This is likely to cause problems.", initialContextFactoryName); + } + if (urlPkgPrefixes != null) { + properties.setProperty(Context.URL_PKG_PREFIXES, urlPkgPrefixes); + } + if (securityPrincipal != null) { + properties.setProperty(Context.SECURITY_PRINCIPAL, securityPrincipal); + if (securityCredentials != null) { + properties.setProperty(Context.SECURITY_CREDENTIALS, securityCredentials); + } else { + LOGGER.warn("A security principal [{}] was provided, but with no corresponding security credentials.", + securityPrincipal); + } + } + if (additionalProperties != null) { + properties.putAll(additionalProperties); + } + return properties; + } + + @Override + protected boolean releaseSub(final long timeout, final TimeUnit timeUnit) { + return JndiCloser.closeSilently(this.context); + } + + /** + * Looks up a named object through this JNDI context. + * + * @param name name of the object to look up. + * @param the type of the object. + * @return the named object if it could be located. + * @throws NamingException if a naming exception is encountered + */ + @SuppressWarnings("unchecked") + public synchronized T lookup(final String name) throws NamingException { + try { + URI uri = new URI(name); + if (uri.getScheme() != null) { + if (!allowedProtocols.contains(uri.getScheme().toLowerCase(Locale.ROOT))) { + LOGGER.warn("Log4j JNDI does not allow protocol {}", uri.getScheme()); + return null; + } + if (LDAP.equalsIgnoreCase(uri.getScheme()) || LDAPS.equalsIgnoreCase(uri.getScheme())) { + if (!allowedHosts.contains(uri.getHost())) { + LOGGER.warn("Attempt to access ldap server not in allowed list"); + return null; + } + Attributes attributes = this.context.getAttributes(name); + if (attributes != null) { + // In testing the "key" for attributes seems to be lowercase while the attribute id is + // camelcase, but that may just be true for the test LDAP used here. This copies the Attributes + // to a Map ignoring the "key" and using the Attribute's id as the key in the Map so it matches + // the Java schema. + Map attributeMap = new HashMap<>(); + NamingEnumeration enumeration = attributes.getAll(); + while (enumeration.hasMore()) { + Attribute attribute = enumeration.next(); + attributeMap.put(attribute.getID(), attribute); + } + Attribute classNameAttr = attributeMap.get(CLASS_NAME); + if (attributeMap.get(SERIALIZED_DATA) != null) { + if (classNameAttr != null) { + String className = classNameAttr.get().toString(); + if (!allowedClasses.contains(className)) { + LOGGER.warn("Deserialization of {} is not allowed", className); + return null; + } + } else { + LOGGER.warn("No class name provided for {}", name); + return null; + } + } else if (attributeMap.get(REFERENCE_ADDRESS) != null + || attributeMap.get(OBJECT_FACTORY) != null) { + LOGGER.warn("Referenceable class is not allowed for {}", name); + return null; + } + } + } + } + } catch (URISyntaxException ex) { + // This is OK. + } + return (T) this.context.lookup(name); + } + + private static class JndiManagerFactory implements ManagerFactory { + + @Override + public JndiManager createManager(final String name, final Properties data) { + String hosts = data != null ? data.getProperty(ALLOWED_HOSTS) : null; + String classes = data != null ? data.getProperty(ALLOWED_CLASSES) : null; + String protocols = data != null ? data.getProperty(ALLOWED_PROTOCOLS) : null; + List allowedHosts = new ArrayList<>(); + List allowedClasses = new ArrayList<>(); + List allowedProtocols = new ArrayList<>(); + addAll(hosts, allowedHosts, permanentAllowedHosts, ALLOWED_HOSTS, data); + addAll(classes, allowedClasses, permanentAllowedClasses, ALLOWED_CLASSES, data); + addAll(protocols, allowedProtocols, permanentAllowedProtocols, ALLOWED_PROTOCOLS, data); + try { + return new JndiManager(name, new InitialDirContext(data), allowedHosts, allowedClasses, + allowedProtocols); + } catch (final NamingException e) { + LOGGER.error("Error creating JNDI InitialContext.", e); + return null; + } + } + + private void addAll(String toSplit, List list, List permanentList, String propertyName, + Properties data) { + if (toSplit != null) { + list.addAll(Arrays.asList(toSplit.split("\\s*,\\s*"))); + data.remove(propertyName); + } + toSplit = PropertiesUtil.getProperties().getStringProperty(PREFIX + propertyName); + if (toSplit != null) { + list.addAll(Arrays.asList(toSplit.split("\\s*,\\s*"))); + } + list.addAll(permanentList); + } + } + + @Override + public String toString() { + return "JndiManager [context=" + context + ", count=" + count + "]"; + } + +} diff --git a/src/main/resources/sources/org/apache/logging/log4j/core/util/NetUtils.java b/src/main/resources/sources/org/apache/logging/log4j/core/util/NetUtils.java new file mode 100644 index 0000000..932fa27 --- /dev/null +++ b/src/main/resources/sources/org/apache/logging/log4j/core/util/NetUtils.java @@ -0,0 +1,215 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache license, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ +package org.apache.logging.log4j.core.util; + +import java.io.File; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.MalformedURLException; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.List; + +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.core.util.ArrayUtils; +import org.apache.logging.log4j.status.StatusLogger; +import org.apache.logging.log4j.util.Strings; + +/** + * Networking-related convenience methods. + */ +public final class NetUtils { + + private static final Logger LOGGER = StatusLogger.getLogger(); + private static final String UNKNOWN_LOCALHOST = "UNKNOWN_LOCALHOST"; + + private NetUtils() { + // empty + } + + /** + * This method gets the network name of the machine we are running on. Returns "UNKNOWN_LOCALHOST" in the unlikely + * case where the host name cannot be found. + * + * @return String the name of the local host + */ + public static String getLocalHostname() { + try { + final InetAddress addr = InetAddress.getLocalHost(); + return addr == null ? UNKNOWN_LOCALHOST : addr.getHostName(); + } catch (final UnknownHostException uhe) { + try { + final Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); + if (interfaces != null) { + while (interfaces.hasMoreElements()) { + final NetworkInterface nic = interfaces.nextElement(); + final Enumeration addresses = nic.getInetAddresses(); + while (addresses.hasMoreElements()) { + final InetAddress address = addresses.nextElement(); + if (!address.isLoopbackAddress()) { + final String hostname = address.getHostName(); + if (hostname != null) { + return hostname; + } + } + } + } + } + } catch (final SocketException se) { + // ignore and log below. + } + LOGGER.error("Could not determine local host name", uhe); + return UNKNOWN_LOCALHOST; + } + } + + /** + * Returns all the local host names and ip addresses. + * @return The local host names and ip addresses. + */ + public static List getLocalIps() { + List localIps = new ArrayList<>(); + localIps.add("localhost"); + localIps.add("127.0.0.1"); + try { + final InetAddress addr = Inet4Address.getLocalHost(); + setHostName(addr, localIps); + } catch (final UnknownHostException ex) { + // Ignore this. + } + try { + final Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); + if (interfaces != null) { + while (interfaces.hasMoreElements()) { + final NetworkInterface nic = interfaces.nextElement(); + final Enumeration addresses = nic.getInetAddresses(); + while (addresses.hasMoreElements()) { + final InetAddress address = addresses.nextElement(); + setHostName(address, localIps); + } + } + } + } catch (final SocketException se) { + // ignore. + } + return localIps; + } + + private static void setHostName(InetAddress address, List localIps) { + String[] parts = address.toString().split("\\s*/\\s*"); + if (parts.length > 0) { + for (String part : parts) { + if (Strings.isNotBlank(part) && !localIps.contains(part)) { + localIps.add(part); + } + } + } + } + + /** + * Returns the local network interface's MAC address if possible. The local network interface is defined here as + * the {@link java.net.NetworkInterface} that is both up and not a loopback interface. + * + * @return the MAC address of the local network interface or {@code null} if no MAC address could be determined. + */ + public static byte[] getMacAddress() { + byte[] mac = null; + try { + final InetAddress localHost = InetAddress.getLocalHost(); + try { + final NetworkInterface localInterface = NetworkInterface.getByInetAddress(localHost); + if (isUpAndNotLoopback(localInterface)) { + mac = localInterface.getHardwareAddress(); + } + if (mac == null) { + final Enumeration networkInterfaces = NetworkInterface.getNetworkInterfaces(); + if (networkInterfaces != null) { + while (networkInterfaces.hasMoreElements() && mac == null) { + final NetworkInterface nic = networkInterfaces.nextElement(); + if (isUpAndNotLoopback(nic)) { + mac = nic.getHardwareAddress(); + } + } + } + } + } catch (final SocketException e) { + LOGGER.catching(e); + } + if (ArrayUtils.isEmpty(mac) && localHost != null) { + // Emulate a MAC address with an IP v4 or v6 + final byte[] address = localHost.getAddress(); + // Take only 6 bytes if the address is an IPv6 otherwise will pad with two zero bytes + mac = Arrays.copyOf(address, 6); + } + } catch (final UnknownHostException ignored) { + // ignored + } + return mac; + } + + /** + * Returns the mac address, if it is available, as a string with each byte separated by a ":" character. + * @return the mac address String or null. + */ + public static String getMacAddressString() { + final byte[] macAddr = getMacAddress(); + if (!ArrayUtils.isEmpty(macAddr)) { + StringBuilder sb = new StringBuilder(String.format("%02x", macAddr[0])); + for (int i = 1; i < macAddr.length; ++i) { + sb.append(":").append(String.format("%02x", macAddr[i])); + } + return sb.toString(); + + } + return null; + } + + private static boolean isUpAndNotLoopback(final NetworkInterface ni) throws SocketException { + return ni != null && !ni.isLoopback() && ni.isUp(); + } + + /** + * Converts a URI string or file path to a URI object. + * + * @param path the URI string or path + * @return the URI object + */ + public static URI toURI(final String path) { + try { + // Resolves absolute URI + return new URI(path); + } catch (final URISyntaxException e) { + // A file path or a Apache Commons VFS URL might contain blanks. + // A file path may start with a driver letter + try { + final URL url = new URL(path); + return new URI(url.getProtocol(), url.getHost(), url.getPath(), null); + } catch (MalformedURLException | URISyntaxException nestedEx) { + return new File(path).toURI(); + } + } + } + +}