vindeckyy opened a new issue, #4426:
URL: https://github.com/apache/logging-log4j2/issues/4426

   ## Description
   
   `log4j-api` reads two resources by fixed name from the classpath during its 
own initialization and applies them silently:
   
   | file | read by | effect |
   | --- | --- | --- |
   | `log4j2.system.properties` | `PropertiesUtil` (api) | every key is copied 
into the **JVM system properties** — global state, not Log4j's namespace |
   | `log4j2.component.properties` | `PropertiesUtil` (api) | Log4j's own 
properties, including `log4j.configurationFile` and 
`log4j2.Script.enableLanguages` |
   
   The relevant code in `log4j-api` 2.26.1:
   
   ```java
   // PropertiesUtil.java:506-519 — the whole of the system-properties behavior
   private Environment(final PropertySource propertySource) {
       final PropertySource sysProps = new 
PropertyFilePropertySource(LOG4J_SYSTEM_PROPERTIES_FILE_NAME, false);
       try {
           sysProps.forEach((key, value) -> {
               if (System.getProperty(key) == null) {
                   System.setProperty(key, value);          // JVM-global 
state, from a classpath file
               }
           });
       } catch (final SecurityException e) { /* ... */ }
       // ... no record that this happened
   ```
   
   The resource is located by `PropertyFilePropertySource.loadPropertiesFile()` 
(`PropertyFilePropertySource.java:50-60`) via `LoaderUtil.findResources`, which 
probes the thread-context loader, Log4j's own loader and — importantly — the 
**system class loader** (`LoaderUtil.findUrlResources`, ~470-475). So a file 
(or resource) of this name **inside any jar on the classpath** is honored. 
Nothing is logged when the file is found or applied: no `StatusLogger` message, 
no warning for the component-properties half.
   
   The manual documents the existence of `log4j2.system.properties` ("its 
contents are sourced into Java system properties at Log4j startup", 
<https://logging.apache.org/log4j/2.x/manual/systemproperties.html>), but not:
   
   - that the **classpath is a trust boundary** here — any artifact in the 
dependency chain can carry the file (the project's threat-model page 
<https://logging.apache.org/log4j/2.x/security.html> does not state this 
assumption either);
   - that the file is applied to the **JVM's global namespace** rather than 
Log4j's own;
   - that a planted file can redirect Log4j itself — `log4j.configurationFile` 
may point at a configuration inside the same jar, and a single file can 
simultaneously enable scripting (`log4j2.Script.enableLanguages`), yielding 
**code execution in the application before its first log statement is 
rendered**;
   - that no observable trace exists in the product when this happens.
   
   ### What a single planted file achieves (measured on 2.14.1 – 2.26.1)
   
   A `log4j2.system.properties` containing:
   
   ```properties
   log4j.configurationFile=pwn.xml
   log4j2.configurationFile=pwn.xml
   log4j2.Script.enableLanguages=Groovy
   ```
   
   placed in a jar on the classpath of an application with no other Log4j 
configuration results in: the attacker's configuration replaces the 
application's, the script filter executes during configuration initialization — 
**before any of the application's own log statements are rendered** — and the 
effective configuration location and enabled script language report the planted 
values. The same file can also redirect outbound traffic 
(`http.proxyHost`/`http.proxyPort` as system properties). With `log4j-api` 
alone on the classpath (no core, no appender), the system-properties half still 
executes — the exposed population is effectively every application carrying 
Log4j.
   
   The precondition is the ability to place a file on the victim's classpath — 
a writable classpath directory or control of a dependency artifact (a 
local-write / dependency-chain primitive; scored 
`AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H` = 7.8 High in the write-up below, with 
the dependency-delivery mode noted separately). I am not reporting this as an 
obscure settings change, but because the outcome of the settings change is code 
execution, the file is applied silently, and the trust assumption on which it 
rests is not written down.
   
   ## Affected configuration
   
   **Version:** verified by execution on `log4j-api`/`log4j-core` 2.14.1, 
2.17.1, 2.24.0, 2.25.1 and **2.26.1** (current release); the 3.x line has 
identical code (`PropertiesUtil.java` in `3.0.0-beta2`, lines 675-682) — 
source-level comparison for 3.x.
   **Component:** `log4j-api` reads the files; the code-execution consequence 
involves `log4j-core`.
   **JDK:** OpenJDK 17.0.21
   **Operating system:** Linux
   
   ## Reproduction
   
   Full reproduction (planted files, victim JVM without any Log4j configuration 
of its own, attacker-supplied configuration hijacking initialization) and the 
measured outputs are public in: 
<https://github.com/vindeckyy/log4j-classpath-plant-rce> — `lab/` (15 
assertions in `lab/verify.sh`, all passing), `WORKUP.md` for 
`file:line`-annotated root cause, and `scan/scan_planted.py` for detection. Key 
measured output:
   
   ```text
   [1] planted system property BEFORE log4j init : null
   === PAYLOAD EXECUTED IN VICTIM JVM (planted config) ===
   RCE via planted log4j2.system.properties (classpath pre-positioning) as 
hayden
   uid=1000(hayden) gid=1000(hayden) 
groups=1000(hayden),4(adm),24(cdrom),27(sudo),...
   ...
   [3] effective log4j config location             : pwn.xml
   [4] script languages enabled                    : Groovy
   [5] attacker script executed (marker file)      : true
   ```
   
   ## Proposed fix
   
   The behavior itself is documented and has legitimate users, so I am *not* 
proposing to change what is applied by default out of the blue. The minimal, 
compatibility-preserving change is to make the mechanism self-announcing and 
the assumption explicit:
   
   1. **Log what is found.** In 
`PropertyFilePropertySource.loadPropertiesFile()` (or a wrapper around it), 
emit a `StatusLogger` message for every resource URL that is found and loaded, 
including the file name, the resolved URL (which names the jar/directory it 
came from) and the number of properties:
   
      ```java
      LOGGER.info("Log4j read the properties file `{}` from `{}` ({} properties 
found).", fileName, url, count);
      ```
   
      This gives every deployment an audit trail naming the artifact that 
supplied the file — for *both* fixed-name files, including the widely used 
`log4j2.component.properties`.
   
   2. **Warn when JVM-global state is written.** In 
`PropertiesUtil.Environment`'s constructor, when the loop actually set *N* keys 
into `System` properties, emit one `WARN` after the loop:
   
      ```java
      LOGGER.warn("Log4j copied {} property key(s) from the classpath resource 
`log4j2.system.properties` "
              + "into JVM system properties (this file sets JVM-global state; 
if its source is "
              + "unexpected, treat the artifact as a supply-chain risk).");
      ```
   
      The current implementation only logs on a `SecurityException`, i.e. on 
failure — the success path is exactly the one that needs the message.
   
   3. **Document the trust boundary.** In `src/site` (*manual/systemproperties* 
page) state explicitly:
      - both fixed-name files are read from the **whole classpath** (system 
class loader included, jars included) — any artifact on the classpath can carry 
them;
      - `log4j2.system.properties` writes into the **JVM's global** property 
namespace;
      - a planted file can select Log4j's configuration file and enable 
scripting;
      - Log4j records these actions via the `StatusLogger` (with the new 
messages above).
      If the project's position is "the classpath is trusted", that is 
defensible for a library — but it should be *stated*, because the threat-model 
page currently does not state it, and this file is one of the places where that 
assumption quietly turns into code execution.
   
   4. *(Optional, for the project to decide)* An explicit opt-out/opt-in for 
the system-properties copy (e.g. a component property that must be set 
externally — JVM flag or environment variable, deliberately not read from the 
classpath — to allow the copies), for estates that want to shrink the surface. 
I'd leave the default behavior as-is to preserve compatibility; the warning 
alone removes the silence that makes the planted file undetectable.
   
   A test sketch (matching `log4j-api-test` / `PropertiesUtilTest` style):
   
   ```java
   @Test
   void applicationWarnsWhenSystemPropertiesFileIsApplied() throws Exception {
       // place a log4j2.system.properties on the classpath of a test (existing 
tests like
       // PropertyFilePropertySourceTest already construct such sources)
       // trigger PropertiesUtil initialization and assert:
       // - keys that did not exist were copied (unchanged behavior)
       // - existing keys are never overwritten (management rule preserved)
       // - a StatusLogger record names the source URL (add a 
ListStatusListener)
   }
   ```
   
   ## Disclosure note
   
   This finding came out of the same 2026-10-23 audit as the configuration-time 
`${ctx:…}` subgroup defect (filed separately, since the two are independent) 
and both are documented in one disclosure record: 
<https://github.com/vindeckyy/log4j-classpath-plant-rce> and 
<https://github.com/vindeckyy/log4j-config-time-lookup-rce> (`DISCLOSURE.md`: 
one report covering both findings is in progress with the Apache Logging 
Services security process). I'm happy to test any proposed fix and can provide 
the patch as a PR here if you prefer — happy to shape the change however the 
maintainers want it.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to