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]