mirror of
https://github.com/corda/corda.git
synced 2025-02-21 09:51:57 +00:00
Initial import
This commit is contained in:
parent
1e6ccfdb60
commit
b82ac8de68
26
tools/aegis4j/.github/workflows/gradle-ci-build.yml
vendored
Normal file
26
tools/aegis4j/.github/workflows/gradle-ci-build.yml
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
name: Gradle CI Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
java: [ '11', '17' ]
|
||||
name: JDK ${{ matrix.Java }} Build
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: ${{ matrix.java }}
|
||||
- name: Build with Gradle
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE
|
||||
./gradlew build
|
7
tools/aegis4j/.gitignore
vendored
Normal file
7
tools/aegis4j/.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
bin/
|
||||
build/
|
||||
.gradle/
|
||||
.settings/
|
||||
.classpath
|
||||
.project
|
||||
gradle.properties
|
202
tools/aegis4j/LICENSE.txt
Normal file
202
tools/aegis4j/LICENSE.txt
Normal file
@ -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 [yyyy] [name of copyright owner]
|
||||
|
||||
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.
|
124
tools/aegis4j/README.md
Normal file
124
tools/aegis4j/README.md
Normal file
@ -0,0 +1,124 @@
|
||||
# aegis4j
|
||||
|
||||
Avoid the NEXT Log4Shell vulnerability!
|
||||
|
||||
The Java platform has accrued a number of features over the years. Some of these features are no longer commonly used,
|
||||
but their existence remains a security liability, providing attackers with a diverse toolkit to leverage against
|
||||
Java-based systems.
|
||||
|
||||
It is possible to eliminate some of this attack surface area by creating custom JVM images with
|
||||
[jlink](https://docs.oracle.com/en/java/javase/17/docs/specs/man/jlink.html), but this is not always feasible or desired.
|
||||
Another option is to use the [--limit-modules](https://docs.oracle.com/en/java/javase/17/docs/specs/man/java.html) command
|
||||
line parameter when running your application, but this is a relatively coarse tool that cannot be used to disable
|
||||
individual features like serialization or native process execution.
|
||||
|
||||
A third option is aegis4j, a Java agent which can patch key system classes to completely disable a number of standard
|
||||
Java features:
|
||||
|
||||
- `jndi`: all JNDI functionality (`javax.naming.*`)
|
||||
- `rmi`: all RMI functionality (`java.rmi.*`)
|
||||
- `process`: all process execution functionality (`Runtime.exec()`, `ProcessBuilder`)
|
||||
- `httpserver`: all use of the JDK HTTP server (`com.sun.net.httpserver.*`)
|
||||
- `serialization`: all Java serialization (`ObjectInputStream`, `ObjectOutputStream`)
|
||||
- `unsafe`: all use of `sun.misc.Unsafe`
|
||||
- `scripting`: all JSR 223 scripting (`javax.script.*`)
|
||||
- `jshell`: all use of the Java Shell API (`jdk.jshell.*`)
|
||||
|
||||
### Download
|
||||
|
||||
The aegis4j JAR is available in the [Maven Central](https://repo1.maven.org/maven2/net/gredler/aegis4j/1.1/) repository.
|
||||
|
||||
### Usage: Attach at Application Startup
|
||||
|
||||
To attach at application startup, blocking all features listed above, add the agent to your java command line:
|
||||
|
||||
`java -cp <classpath> -javaagent:aegis4j-1.1.jar <main-class> <arguments>`
|
||||
|
||||
Or, if you want to configure the specific features to block:
|
||||
|
||||
`java -cp <classpath> -javaagent:aegis4j-1.1.jar=block=<features> <main-class> <arguments>`
|
||||
|
||||
Or, if you want to use the default block list, but unblock specific features:
|
||||
|
||||
`java -cp <classpath> -javaagent:aegis4j-1.1.jar=unblock=<features> <main-class> <arguments>`
|
||||
|
||||
Feature lists should be comma-delimited (e.g. `jndi,rmi,unsafe`).
|
||||
|
||||
### Usage: Attach to a Running Application
|
||||
|
||||
To attach to a running application, blocking all features listed above, run the following command:
|
||||
|
||||
`java -jar aegis4j-1.1.jar <application-pid>`
|
||||
|
||||
Or, if you want to configure the specific features to block:
|
||||
|
||||
`java -jar aegis4j-1.1.jar <application-pid> block=<features>`
|
||||
|
||||
Or, if you want to use the default block list, but unblock specific features:
|
||||
|
||||
`java -jar aegis4j-1.1.jar <application-pid> unblock=<features>`
|
||||
|
||||
Feature lists should be comma-delimited (e.g. `jndi,rmi,unsafe`).
|
||||
|
||||
The application process ID, or PID, can usually be determined by running the `jps` command.
|
||||
|
||||
### Compatibility
|
||||
|
||||
The aegis4j Java agent is compatible with JDK 11 and newer.
|
||||
|
||||
### Monitoring
|
||||
|
||||
The list of Java features blocked by aegis4j is available via the `aegis4j.blocked.features` system property, which
|
||||
can be queried at runtime via Java code, JMX, APM agents, etc.
|
||||
|
||||
When an attempt is made to use a blocked feature, the type of exception thrown varies according to context, but the exception
|
||||
message always uses the format `"<action> blocked by aegis4j"`.
|
||||
|
||||
### Building
|
||||
|
||||
To build aegis4j, run `gradlew build`.
|
||||
|
||||
### Digging Deeper
|
||||
|
||||
Class modifications are performed using [Javassist](https://www.javassist.org/). The specific class modifications performed are
|
||||
configured in the [mods.properties](src/main/resources/net/gredler/aegis4j/mods.properties) file.
|
||||
|
||||
Some of the tests validate the agent against actual vulnerabilities (e.g.
|
||||
[CVE-2015-7501](src/test/java/net/gredler/aegis4j/CVE_2015_7501.java),
|
||||
[CVE-2019-17531](src/test/java/net/gredler/aegis4j/CVE_2019_17531.java),
|
||||
[CVE-2021-44228](src/test/java/net/gredler/aegis4j/CVE_2021_44228.java)).
|
||||
The tests are run with the `jdk.attach.allowAttachSelf=true` system property, so that the agent can be attached and tested
|
||||
locally. Tests are also run in individual VM instances, so that the class modifications performed in one test do not affect other
|
||||
tests.
|
||||
|
||||
Ideally aegis4j could block all reflection as well, since it's often used in exploit chains. However, reflection is used *everywhere*,
|
||||
including the JDK lambda internals, Spring Boot, JUnit, and many other libraries and frameworks. The best way to mitigate the dangers
|
||||
of reflection is to upgrade to JDK 17 or later, where many of the internal platform classes have been made inaccessible via reflection
|
||||
(see [JEP 403](https://openjdk.java.net/jeps/403), or the [full list](https://cr.openjdk.java.net/~mr/jigsaw/jdk8-packages-strongly-encapsulated)
|
||||
of packages that were locked down between JDK 8 and JDK 17).
|
||||
|
||||
### Related Work
|
||||
|
||||
[log4j-jndi-be-gone](https://github.com/nccgroup/log4j-jndi-be-gone):
|
||||
A Java agent which patches the Log4Shell vulnerability (CVE-2021-44228).
|
||||
|
||||
[Log4jHotPatch](https://github.com/corretto/hotpatch-for-apache-log4j2/):
|
||||
A similar Java agent from the Amazon Corretto team.
|
||||
|
||||
[Logout4Shell](https://github.com/Cybereason/Logout4Shell):
|
||||
Vaccine exploit which leverages the Log4Shell vulnerability to patch the Log4Shell vulnerability.
|
||||
|
||||
[Logpresso log4j2-scan](https://github.com/logpresso/CVE-2021-44228-Scanner):
|
||||
Command line tool for scanning (and patching) JAR files for Log4Shell vulnerabilities.
|
||||
|
||||
[ysoserial](https://github.com/frohoff/ysoserial):
|
||||
A proof-of-concept tool for generating Java serialization vulnerability payloads.
|
||||
|
||||
[NotSoSerial](https://github.com/kantega/notsoserial):
|
||||
A Java agent which attempts to mitigate serialization vulnerabilities by selectively blocking serialization attempts.
|
||||
|
||||
[An In-Depth Study of More Than Ten Years of Java Exploitation](https://www.abartel.net/static/p/ccs2016-10yearsJavaExploits.pdf):
|
||||
Study of real-world Java exploits between 2003 and 2013 ([citations](https://scholar.google.com/scholar?cites=17190152291480177134)).
|
||||
|
||||
[A Systematic Analysis and Hardening of the Java Security Architecture](https://www.bodden.de/pubs/phdHolzinger.pdf):
|
||||
PhD thesis which incorporates the above research and proposes specific hardening measures.
|
126
tools/aegis4j/build.gradle
Normal file
126
tools/aegis4j/build.gradle
Normal file
@ -0,0 +1,126 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'jacoco'
|
||||
id 'eclipse'
|
||||
id 'signing'
|
||||
id 'maven-publish'
|
||||
id 'com.github.johnrengelman.shadow' version '7.1.0'
|
||||
}
|
||||
|
||||
group = 'net.gredler'
|
||||
archivesBaseName = 'aegis4j'
|
||||
version = '1.1'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.javassist:javassist:3.28.0-GA'
|
||||
testImplementation 'org.springframework:spring-core:5.3.14'
|
||||
testImplementation 'com.unboundid:unboundid-ldapsdk:3.1.1'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
|
||||
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
|
||||
// vulnerable to CVE-2021-44228
|
||||
testImplementation 'org.apache.logging.log4j:log4j-core:2.14.1'
|
||||
// vulnerable to CVE-2015-7501
|
||||
testImplementation 'org.apache.commons:commons-collections4:4.0'
|
||||
// vulnerable to CVE-2019-17531
|
||||
testImplementation 'com.fasterxml.jackson.core:jackson-databind:2.9.10'
|
||||
testImplementation('log4j:apache-log4j-extras:1.2.17') {
|
||||
exclude group: 'log4j', module: 'log4j'
|
||||
}
|
||||
}
|
||||
|
||||
sourceCompatibility = 11
|
||||
targetCompatibility = 11
|
||||
|
||||
compileJava.options.encoding = 'UTF-8'
|
||||
compileTestJava.options.encoding = 'UTF-8'
|
||||
javadoc.options.encoding = 'UTF-8'
|
||||
|
||||
java {
|
||||
withJavadocJar()
|
||||
withSourcesJar()
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
systemProperty 'jdk.attach.allowAttachSelf', 'true' // tests attach agent to local VM
|
||||
forkEvery 1 // tests cannot undo class modifications to clean up after themselves
|
||||
testLogging {
|
||||
events 'passed', 'skipped', 'failed'
|
||||
showExceptions true
|
||||
showStackTraces true
|
||||
showStandardStreams false
|
||||
exceptionFormat 'full'
|
||||
}
|
||||
}
|
||||
|
||||
jar {
|
||||
archiveClassifier.set('no-deps')
|
||||
manifest.attributes(
|
||||
'Implementation-Title': 'aegis4j',
|
||||
'Implementation-Version': archiveVersion,
|
||||
'Main-Class': 'net.gredler.aegis4j.AegisAgent',
|
||||
'Agent-Class': 'net.gredler.aegis4j.AegisAgent',
|
||||
'Premain-Class': 'net.gredler.aegis4j.AegisAgent',
|
||||
'Can-Redefine-Classes': true,
|
||||
'Can-Retransform-Classes': true,
|
||||
'Can-Set-Native-Method-Prefix': false
|
||||
)
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
archiveClassifier.set('')
|
||||
relocate 'javassist', 'net.gredler.aegis4j.javassist'
|
||||
}
|
||||
|
||||
tasks.build.dependsOn tasks.shadowJar
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
mavenJava(MavenPublication) { publication ->
|
||||
project.shadow.component(publication)
|
||||
artifact sourcesJar
|
||||
artifact javadocJar
|
||||
pom {
|
||||
groupId = 'net.gredler'
|
||||
name = 'aegis4j'
|
||||
url = 'https://github.com/gredler/aegis4j'
|
||||
description = 'A Java agent which disables dangerous rarely-used runtime features.'
|
||||
licenses {
|
||||
license {
|
||||
name = 'The Apache License, Version 2.0'
|
||||
url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
|
||||
}
|
||||
}
|
||||
developers {
|
||||
developer {
|
||||
id = 'gredler'
|
||||
name = 'Daniel Gredler'
|
||||
email = 'daniel.gredler@gmail.com'
|
||||
}
|
||||
}
|
||||
scm {
|
||||
url = 'https://github.com/gredler/aegis4j'
|
||||
connection = 'scm:git:git://github.com/gredler/aegis4j.git'
|
||||
developerConnection = 'scm:git:ssh:git@github.com:gredler/aegis4j.git'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
repositories {
|
||||
maven {
|
||||
url = 'https://oss.sonatype.org/service/local/staging/deploy/maven2/'
|
||||
credentials {
|
||||
username = project.hasProperty('ossrhUsername') ? ossrhUsername : 'unknown'
|
||||
password = project.hasProperty('ossrhPassword') ? ossrhPassword : 'unknown'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
signing {
|
||||
sign publishing.publications.mavenJava
|
||||
}
|
BIN
tools/aegis4j/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
tools/aegis4j/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
5
tools/aegis4j/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
5
tools/aegis4j/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.1-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
234
tools/aegis4j/gradlew
vendored
Executable file
234
tools/aegis4j/gradlew
vendored
Executable file
@ -0,0 +1,234 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# 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
|
||||
#
|
||||
# https://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.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=${0##*/}
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
89
tools/aegis4j/gradlew.bat
vendored
Executable file
89
tools/aegis4j/gradlew.bat
vendored
Executable file
@ -0,0 +1,89 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
134
tools/aegis4j/src/main/java/net/gredler/aegis4j/AegisAgent.java
Normal file
134
tools/aegis4j/src/main/java/net/gredler/aegis4j/AegisAgent.java
Normal file
@ -0,0 +1,134 @@
|
||||
/* Copyright (c) 2022, Daniel Gredler. All rights reserved. */
|
||||
|
||||
package net.gredler.aegis4j;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.instrument.Instrumentation;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.sun.tools.attach.VirtualMachine;
|
||||
|
||||
/**
|
||||
* The main agent class. This class fails fast on any configuration errors, so that e.g. a typo
|
||||
* which prevents a feature from blocking correctly does not create a false sense of security.
|
||||
* Once the hand-off to {@link Patcher} has occurred and the class patching has begun, errors are
|
||||
* handled more leniently.
|
||||
*
|
||||
* @see <a href="https://www.baeldung.com/java-instrumentation">Java instrumentation primer</a>
|
||||
* @see <a href="https://github.com/nccgroup/log4j-jndi-be-gone">Alternate Java agent project</a>
|
||||
*/
|
||||
public final class AegisAgent {
|
||||
|
||||
/**
|
||||
* Supports easy dynamic attach via the command line.
|
||||
*
|
||||
* @param args the command line parameters (should contain the PID of the application to patch)
|
||||
* @throws Exception if any error occurs during the attach process
|
||||
*/
|
||||
public static void main(String[] args) throws Exception {
|
||||
|
||||
if (args.length < 1) {
|
||||
throw new IllegalArgumentException("ERROR: Missing required argument: pid");
|
||||
}
|
||||
|
||||
if (args.length > 2) {
|
||||
throw new IllegalArgumentException("ERROR: Too many arguments provided");
|
||||
}
|
||||
|
||||
String pid = args[0];
|
||||
String options = args.length > 1 ? args[1] : "";
|
||||
|
||||
File jar = new File(AegisAgent.class.getProtectionDomain().getCodeSource().getLocation().toURI());
|
||||
VirtualMachine jvm = VirtualMachine.attach(pid);
|
||||
jvm.loadAgent(jar.getAbsolutePath(), options);
|
||||
jvm.detach();
|
||||
}
|
||||
|
||||
/**
|
||||
* Supports static attach (via -javaagent parameter at JVM startup).
|
||||
*
|
||||
* @param args agent arguments
|
||||
* @param instr instrumentation services
|
||||
*/
|
||||
public static void premain(String args, Instrumentation instr) {
|
||||
Patcher.start(instr, toBlockList(args));
|
||||
}
|
||||
|
||||
/**
|
||||
* Supports dynamic attach (via the com.sun.tools.attach.* API).
|
||||
*
|
||||
* @param args agent arguments
|
||||
* @param instr instrumentation services
|
||||
*/
|
||||
public static void agentmain(String args, Instrumentation instr) {
|
||||
Patcher.start(instr, toBlockList(args));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the agent argument string (e.g. {@code "block=jndi,rmi,serialization"} or {@code "unblock=process"})
|
||||
* into a feature block list.
|
||||
*
|
||||
* @param args agent arguments
|
||||
* @return the block list derived from the agent arguments
|
||||
*/
|
||||
protected static Set< String > toBlockList(String args) {
|
||||
|
||||
Set< String > all = Set.of("jndi", "rmi", "process", "httpserver", "serialization", "unsafe", "scripting", "jshell");
|
||||
if (args == null || args.isBlank()) {
|
||||
// no arguments provided by user
|
||||
return all;
|
||||
}
|
||||
|
||||
args = args.trim().toLowerCase();
|
||||
int eq = args.indexOf('=');
|
||||
if (eq == -1) {
|
||||
// incorrect argument format, we expect a single "name=value" parameter
|
||||
throw new IllegalArgumentException("ERROR: Invalid agent configuration string");
|
||||
}
|
||||
|
||||
String name = args.substring(0, eq).trim();
|
||||
String value = args.substring(eq + 1).trim();
|
||||
|
||||
if ("block".equals(name)) {
|
||||
// user is providing their own block list
|
||||
return split(value, all);
|
||||
} else if ("unblock".equals(name)) {
|
||||
// user is modifying the default block list
|
||||
Set< String > block = new HashSet<>(all);
|
||||
Set< String > unblock = split(value, all);
|
||||
block.removeAll(unblock);
|
||||
return Collections.unmodifiableSet(block);
|
||||
} else {
|
||||
// no idea what the user is doing...
|
||||
throw new IllegalArgumentException("ERROR: Unrecognized parameter name (should be one of 'block' or 'unblock'): " + name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits the specified comma-delimited feature list, validating that all specified features are valid.
|
||||
*
|
||||
* @param values the comma-delimited feature list
|
||||
* @param all the list of valid features to validate against
|
||||
* @return the feature list, split into individual (validated) feature names
|
||||
* @throws IllegalArgumentException if any unrecognized feature names are included in the comma-delimited feature list
|
||||
*/
|
||||
private static Set< String > split(String values, Set< String > all) {
|
||||
|
||||
Set< String > features = Arrays.asList(values.split(","))
|
||||
.stream()
|
||||
.map(String::trim)
|
||||
.collect(Collectors.toUnmodifiableSet());
|
||||
|
||||
for (String feature : features) {
|
||||
if (!all.contains(feature)) {
|
||||
throw new IllegalArgumentException("ERROR: Unrecognized feature name: " + feature);
|
||||
}
|
||||
}
|
||||
|
||||
return features;
|
||||
}
|
||||
}
|
165
tools/aegis4j/src/main/java/net/gredler/aegis4j/Patcher.java
Normal file
165
tools/aegis4j/src/main/java/net/gredler/aegis4j/Patcher.java
Normal file
@ -0,0 +1,165 @@
|
||||
/* Copyright (c) 2022, Daniel Gredler. All rights reserved. */
|
||||
|
||||
package net.gredler.aegis4j;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.instrument.ClassFileTransformer;
|
||||
import java.lang.instrument.Instrumentation;
|
||||
import java.lang.instrument.UnmodifiableClassException;
|
||||
import java.security.ProtectionDomain;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javassist.ByteArrayClassPath;
|
||||
import javassist.CannotCompileException;
|
||||
import javassist.ClassPool;
|
||||
import javassist.CtClass;
|
||||
import javassist.CtConstructor;
|
||||
import javassist.CtMethod;
|
||||
import javassist.LoaderClassPath;
|
||||
import javassist.NotFoundException;
|
||||
|
||||
/**
|
||||
* Disables rarely-used platform features like JNDI and RMI so that they cannot be exploited by attackers.
|
||||
* See the {@code mods.properties} file for the actual class modification configuration.
|
||||
*/
|
||||
public final class Patcher implements ClassFileTransformer {
|
||||
|
||||
private final Map< String, List< Modification > > modifications; // class name -> modifications
|
||||
|
||||
/**
|
||||
* Creates a new class patcher which blocks the specified features.
|
||||
*
|
||||
* @param block the features to block
|
||||
*/
|
||||
public Patcher(Set< String > block) {
|
||||
modifications = loadModifications(block);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new patcher on the specified instrumentation instance and triggers class transformation.
|
||||
*
|
||||
* @param instr the instrumentation instance to add a new patcher to
|
||||
* @param block the features to block
|
||||
*/
|
||||
public static void start(Instrumentation instr, Set< String > block) {
|
||||
|
||||
System.out.println("Aegis4j patching starting");
|
||||
Patcher patcher = new Patcher(block);
|
||||
instr.addTransformer(patcher, true);
|
||||
|
||||
for (String className : patcher.modifications.keySet()) {
|
||||
try {
|
||||
System.out.println("Aegis4j patching " + className + "...");
|
||||
Class< ? > clazz = Class.forName(className);
|
||||
instr.retransformClasses(clazz);
|
||||
} catch (ClassNotFoundException | UnmodifiableClassException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
System.setProperty("aegis4j.blocked.features", String.join(",", block));
|
||||
System.out.println("Aegis4j patching finished");
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] transform(Module module, ClassLoader loader, String className, Class< ? > clazz, ProtectionDomain domain, byte[] classBytes) {
|
||||
return transform(loader, className, clazz, domain, classBytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] transform(ClassLoader loader, String className, Class< ? > clazz, ProtectionDomain domain, byte[] classBytes) {
|
||||
return patch(className.replace('/', '.'), classBytes);
|
||||
}
|
||||
|
||||
private byte[] patch(String className, byte[] classBytes) {
|
||||
|
||||
List< Modification > mods = modifications.get(className);
|
||||
if (mods == null || mods.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ClassPool pool = new ClassPool();
|
||||
pool.appendClassPath(new ByteArrayClassPath(className, classBytes));
|
||||
pool.appendClassPath(new LoaderClassPath(ClassLoader.getPlatformClassLoader()));
|
||||
pool.appendClassPath(new LoaderClassPath(getClass().getClassLoader()));
|
||||
|
||||
try {
|
||||
CtClass clazz = pool.get(className);
|
||||
for (Modification mod : mods) {
|
||||
if (mod.isConstructor()) {
|
||||
for (CtConstructor constructor : clazz.getConstructors()) {
|
||||
constructor.setBody(mod.newBody);
|
||||
}
|
||||
} else {
|
||||
CtMethod[] methods = mod.isAll() ? clazz.getDeclaredMethods() : clazz.getDeclaredMethods(mod.methodName);
|
||||
if (methods.length == 0) {
|
||||
System.err.println("ERROR: Method not found: " + className + "." + mod.methodName);
|
||||
}
|
||||
for (CtMethod method : methods) {
|
||||
method.setBody(mod.newBody);
|
||||
}
|
||||
}
|
||||
}
|
||||
return clazz.toBytecode();
|
||||
} catch (NotFoundException | CannotCompileException | IOException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static Map< String, List< Modification > > loadModifications(Set< String > block) {
|
||||
|
||||
Properties props = new Properties();
|
||||
try {
|
||||
props.load(AegisAgent.class.getResourceAsStream("mods.properties"));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
List< Modification > mods = new ArrayList<>();
|
||||
for (String key : props.stringPropertyNames()) {
|
||||
int first = key.indexOf('.');
|
||||
int last = key.lastIndexOf('.');
|
||||
String feature = key.substring(0, first).toLowerCase();
|
||||
if (block.contains(feature)) {
|
||||
String className = key.substring(first + 1, last);
|
||||
String methodName = key.substring(last + 1);
|
||||
String newBody = props.getProperty(key);
|
||||
Modification mod = new Modification(className, methodName, newBody);
|
||||
mods.add(mod);
|
||||
}
|
||||
}
|
||||
|
||||
return Collections.unmodifiableMap(new TreeMap<>(
|
||||
mods.stream().collect(Collectors.groupingBy(mod -> mod.className, Collectors.toUnmodifiableList()))
|
||||
));
|
||||
}
|
||||
|
||||
private static final class Modification {
|
||||
|
||||
public final String className;
|
||||
public final String methodName;
|
||||
public final String newBody;
|
||||
|
||||
public Modification(String className, String methodName, String newBody) {
|
||||
this.className = className;
|
||||
this.methodName = methodName;
|
||||
this.newBody = newBody;
|
||||
}
|
||||
|
||||
public boolean isConstructor() {
|
||||
return className.substring(className.lastIndexOf('.') + 1).equals(methodName);
|
||||
}
|
||||
|
||||
public boolean isAll() {
|
||||
return "*".equals(methodName);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
# format: <feature>.<class-name>.<method-name>=<replacement-code>
|
||||
|
||||
# JNDI
|
||||
# ----
|
||||
# Patches the 3 protected methods which InitialContext uses internally to get all Context instances,
|
||||
# so that these methods always throw a NoInitialContextException (NICE). As a result, no JNDI lookups
|
||||
# are possible.
|
||||
JNDI.javax.naming.InitialContext.getURLOrDefaultInitCtx=throw new javax.naming.NoInitialContextException("JNDI context creation blocked by aegis4j");
|
||||
JNDI.javax.naming.InitialContext.getDefaultInitCtx=throw new javax.naming.NoInitialContextException("JNDI context creation blocked by aegis4j");
|
||||
|
||||
# RMI
|
||||
# ---
|
||||
# Outside of traditional JEE application servers, few people use the RMI libraries anymore.
|
||||
RMI.java.rmi.registry.LocateRegistry.getRegistry=throw new java.rmi.StubNotFoundException("RMI registry creation blocked by aegis4j");
|
||||
RMI.java.rmi.registry.LocateRegistry.createRegistry=throw new java.rmi.StubNotFoundException("RMI registry creation blocked by aegis4j");
|
||||
RMI.java.rmi.server.RemoteObject.RemoteObject=throw new java.lang.RuntimeException("RMI remote object creation blocked by aegis4j");
|
||||
|
||||
# Process Execution
|
||||
# -----------------
|
||||
# Many remote code execution exploits culminate in local process execution, but it's relatively rare to
|
||||
# need to use these methods for legitimate purposes.
|
||||
PROCESS.java.lang.Runtime.exec=throw new java.io.IOException("Process execution blocked by aegis4j");
|
||||
PROCESS.java.lang.ProcessBuilder.start=throw new java.io.IOException("Process execution blocked by aegis4j");
|
||||
PROCESS.java.lang.ProcessBuilder.startPipeline=throw new java.io.IOException("Process execution blocked by aegis4j");
|
||||
|
||||
# JDK HTTP Server
|
||||
# ---------------
|
||||
# The JDK HTTP server is intended for quick testing, especially for platform beginners. It is rarely (if
|
||||
# ever) used in production, so we can eliminate this little bit of attack surface.
|
||||
HTTPSERVER.com.sun.net.httpserver.HttpServer.HttpServer=throw new java.lang.RuntimeException("HTTP server creation blocked by aegis4j");
|
||||
HTTPSERVER.com.sun.net.httpserver.HttpsServer.HttpsServer=throw new java.lang.RuntimeException("HTTPS server creation blocked by aegis4j");
|
||||
HTTPSERVER.com.sun.net.httpserver.spi.HttpServerProvider.HttpServerProvider=throw new java.lang.RuntimeException("HTTP server provider creation blocked by aegis4j");
|
||||
HTTPSERVER.com.sun.net.httpserver.spi.HttpServerProvider.provider=throw new java.lang.RuntimeException("HTTP server provider lookup blocked by aegis4j");
|
||||
|
||||
# Java Serialization
|
||||
# ------------------
|
||||
# Probably a bit more commonly used than most of the other features on this list, but a huge security
|
||||
# liability for applications that don't use it. Best to live without it, if at all possible.
|
||||
SERIALIZATION.java.io.ObjectInputStream.ObjectInputStream=throw new java.lang.RuntimeException("Java deserialization blocked by aegis4j");
|
||||
SERIALIZATION.java.io.ObjectOutputStream.ObjectOutputStream=throw new java.lang.RuntimeException("Java serialization blocked by aegis4j");
|
||||
|
||||
# Unsafe
|
||||
# ------
|
||||
# Quite commonly used in the olden days, but many applications should be able to run without it these days.
|
||||
UNSAFE.sun.misc.Unsafe.*=throw new java.lang.RuntimeException("Unsafe blocked by aegis4j");
|
||||
|
||||
# Scripting
|
||||
# ---------
|
||||
# Nashorn was removed from the platform in JDK 15. There are other JSR 223 script engines out there,
|
||||
# but they're probably not common enough to leave this capability enabled by default.
|
||||
SCRIPTING.javax.script.ScriptEngineManager.ScriptEngineManager=throw new java.lang.RuntimeException("Scripting blocked by aegis4j");
|
||||
SCRIPTING.javax.script.AbstractScriptEngine.AbstractScriptEngine=throw new java.lang.RuntimeException("Scripting blocked by aegis4j");
|
||||
SCRIPTING.javax.script.SimpleScriptContext.SimpleScriptContext=throw new java.lang.RuntimeException("Scripting blocked by aegis4j");
|
||||
SCRIPTING.javax.script.CompiledScript.CompiledScript=throw new java.lang.RuntimeException("Scripting blocked by aegis4j");
|
||||
|
||||
# JShell
|
||||
# ------
|
||||
# Introduced in JDK 9, the Java Shell is intended for rapid prototyping and testing. It is not usually used in production.
|
||||
JSHELL.jdk.jshell.JShell.JShell=throw new java.lang.RuntimeException("JShell blocked by aegis4j");
|
||||
JSHELL.jdk.jshell.JShell.create=throw new java.lang.RuntimeException("JShell blocked by aegis4j");
|
||||
JSHELL.jdk.jshell.JShell.builder=throw new java.lang.RuntimeException("JShell blocked by aegis4j");
|
||||
JSHELL.jdk.jshell.tool.JavaShellToolBuilder.builder=throw new java.lang.RuntimeException("JShell blocked by aegis4j");
|
||||
JSHELL.jdk.jshell.Snippet.Snippet=throw new java.lang.RuntimeException("JShell blocked by aegis4j");
|
||||
JSHELL.jdk.jshell.TaskFactory.parse=throw new java.lang.RuntimeException("JShell blocked by aegis4j");
|
@ -0,0 +1,118 @@
|
||||
/* Copyright (c) 2022, Daniel Gredler. All rights reserved. */
|
||||
|
||||
package net.gredler.aegis4j;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* <p>Tests {@link AegisAgent} command line use (both static and dynamic attach).
|
||||
*
|
||||
* <p><b>NOTE:</b> Code covered by these tests will not be included in the code coverage stats, because it runs
|
||||
* in the manually-forked VMs, which JaCoCo does not know about. Because of this (and because forking extra VMs
|
||||
* is extra slow), this class should only be used to test the agent entry points; most functional testing belongs
|
||||
* in {@link AegisAgentTest} instead.
|
||||
*/
|
||||
public class AegisAgentCommandLineTest {
|
||||
|
||||
@Test
|
||||
public void testStaticAttach() throws Exception {
|
||||
String jar = TestUtils.createAgentJar();
|
||||
// CONFIG EXPECTED ERROR
|
||||
testStaticAttach(jar, "block=jndi", "");
|
||||
testStaticAttach(jar, "unblock=serialization", "");
|
||||
testStaticAttach(jar, "block=serialization", "Java serialization blocked by aegis4j");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDynamicAttach() throws Exception {
|
||||
String jar = TestUtils.createAgentJar();
|
||||
// PID CONFIG EXTRA EXPECTED APP ERROR EXPECTED AGENT ERROR
|
||||
testDynamicAttach(jar, true, "block=jndi", false, "", "");
|
||||
testDynamicAttach(jar, true, "unblock=serialization", false, "", "");
|
||||
testDynamicAttach(jar, true, "block=serialization", false, "Java serialization blocked by aegis4j", "");
|
||||
testDynamicAttach(jar, true, "", false, "Java serialization blocked by aegis4j", "");
|
||||
testDynamicAttach(jar, false, "block=serialization", false, "", "Invalid process identifier");
|
||||
testDynamicAttach(jar, false, "", false, "", "ERROR: Missing required argument: pid");
|
||||
testDynamicAttach(jar, true, "block=serialization", true, "", "ERROR: Too many arguments provided");
|
||||
}
|
||||
|
||||
private static void testStaticAttach(String jar, String config, String expectedErr) throws Exception {
|
||||
|
||||
String main = Main.class.getName();
|
||||
String cp = System.getProperty("java.class.path");
|
||||
Process process = new ProcessBuilder("java", "-javaagent:" + jar + "=" + config, "-cp", cp, main).start();
|
||||
process.waitFor(5, TimeUnit.SECONDS);
|
||||
assertFalse(process.isAlive());
|
||||
|
||||
String out = new String(process.getInputStream().readAllBytes(), UTF_8);
|
||||
String err = new String(process.getErrorStream().readAllBytes(), UTF_8);
|
||||
String summary = "OUT: " + out + "\nERR: " + err;
|
||||
assertEquals(expectedErr.isEmpty(), out.endsWith("done" + System.lineSeparator()), summary);
|
||||
assertEmptyOrContains(expectedErr, err, summary);
|
||||
}
|
||||
|
||||
private static void testDynamicAttach(String jar, boolean addPid, String config, boolean addThirdParam, String expectedErr, String expectedAttachErr) throws Exception {
|
||||
|
||||
String main = Main.class.getName();
|
||||
String cp = System.getProperty("java.class.path");
|
||||
Process process = new ProcessBuilder("java", "-cp", cp, main).start();
|
||||
|
||||
List< String > cmd2 = new ArrayList<>();
|
||||
cmd2.addAll(Arrays.asList("java", "-jar", jar));
|
||||
if (addPid) cmd2.add(String.valueOf(process.pid()));
|
||||
if (!config.isEmpty()) cmd2.add(config);
|
||||
if (addThirdParam) cmd2.add("foo");
|
||||
Process process2 = new ProcessBuilder(cmd2).start();
|
||||
|
||||
process.waitFor(5, TimeUnit.SECONDS);
|
||||
process2.waitFor(5, TimeUnit.SECONDS);
|
||||
assertFalse(process.isAlive());
|
||||
assertFalse(process2.isAlive());
|
||||
|
||||
String out = new String(process.getInputStream().readAllBytes(), UTF_8);
|
||||
String err = new String(process.getErrorStream().readAllBytes(), UTF_8);
|
||||
String out2 = new String(process2.getInputStream().readAllBytes(), UTF_8);
|
||||
String err2 = new String(process2.getErrorStream().readAllBytes(), UTF_8);
|
||||
String summary = "OUT 1: " + out + "\nERR 1: " + err + "\nOUT 2: " + out2 + "\nERR 2: " + err2;
|
||||
assertEquals(expectedErr.isEmpty(), out.endsWith("done" + System.lineSeparator()), summary);
|
||||
assertEmptyOrContains(expectedErr, err, summary);
|
||||
assertEmptyOrContains(expectedAttachErr, err2, summary);
|
||||
}
|
||||
|
||||
private static void assertEmptyOrContains(String expected, String actual, String message) {
|
||||
if (expected.isEmpty()) {
|
||||
// actual value should be empty, as well
|
||||
assertEquals(expected, actual, message);
|
||||
} else {
|
||||
// actual value should contain the expected value
|
||||
assertTrue(actual.contains(expected), message);
|
||||
}
|
||||
}
|
||||
|
||||
public static class Main {
|
||||
public static void main(String[] args) throws Exception {
|
||||
// sleep until the agent finishes attaching, or time out after 30 * 100 ms = 3 seconds
|
||||
// really only relevant for dynamic attach -- no wait will be needed with static attach
|
||||
for (int i = 0; i < 30; i++) {
|
||||
if (System.getProperty("aegis4j.blocked.features") != null) {
|
||||
break; // agent finished attaching
|
||||
}
|
||||
Thread.sleep(100);
|
||||
}
|
||||
new ObjectOutputStream(new ByteArrayOutputStream()); // triggers serialization block (if enabled)
|
||||
System.out.println("done");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
/* Copyright (c) 2022, Daniel Gredler. All rights reserved. */
|
||||
|
||||
package net.gredler.aegis4j;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Tests {@link AegisAgent} monitoring via system properties.
|
||||
*/
|
||||
public class AegisAgentMonitoringTest {
|
||||
|
||||
@Test
|
||||
public void testSystemProperty() throws Exception {
|
||||
assertNull(System.getProperty("aegis4j.blocked.features"));
|
||||
TestUtils.installAgent("unblock=jndi,rmi,unsafe,scripting");
|
||||
assertEquals("serialization,jshell,process,httpserver", System.getProperty("aegis4j.blocked.features"));
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,333 @@
|
||||
/* Copyright (c) 2022, Daniel Gredler. All rights reserved. */
|
||||
|
||||
package net.gredler.aegis4j;
|
||||
|
||||
import static net.gredler.aegis4j.AegisAgent.toBlockList;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.lang.reflect.Field;
|
||||
import java.rmi.StubNotFoundException;
|
||||
import java.rmi.registry.LocateRegistry;
|
||||
import java.rmi.server.RMIClientSocketFactory;
|
||||
import java.rmi.server.RMIServerSocketFactory;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.naming.InitialContext;
|
||||
import javax.naming.Name;
|
||||
import javax.naming.NoInitialContextException;
|
||||
import javax.naming.ldap.LdapName;
|
||||
import javax.script.CompiledScript;
|
||||
import javax.script.ScriptContext;
|
||||
import javax.script.ScriptEngine;
|
||||
import javax.script.ScriptEngineManager;
|
||||
import javax.script.SimpleScriptContext;
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.function.Executable;
|
||||
import org.springframework.objenesis.SpringObjenesis;
|
||||
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import com.sun.net.httpserver.spi.HttpServerProvider;
|
||||
|
||||
import jdk.jshell.JShell;
|
||||
import jdk.jshell.tool.JavaShellToolBuilder;
|
||||
import sun.misc.Unsafe;
|
||||
|
||||
/**
|
||||
* Tests {@link AegisAgent}.
|
||||
*/
|
||||
public class AegisAgentTest {
|
||||
|
||||
@BeforeAll
|
||||
public static void installAgent() throws Exception {
|
||||
TestUtils.installAgent(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseBlockList() {
|
||||
|
||||
assertEquals(Set.of("jndi", "rmi", "process", "httpserver", "serialization", "unsafe", "scripting", "jshell"), toBlockList(""));
|
||||
assertEquals(Set.of("jndi", "rmi", "process", "httpserver", "serialization", "unsafe", "scripting", "jshell"), toBlockList(" "));
|
||||
assertEquals(Set.of("jndi", "rmi", "process", "httpserver", "unsafe", "scripting", "jshell"), toBlockList("unblock=serialization"));
|
||||
assertEquals(Set.of("jndi", "rmi", "httpserver", "unsafe", "scripting", "jshell"), toBlockList("unblock=serialization,process"));
|
||||
assertEquals(Set.of("jndi", "rmi", "httpserver", "unsafe", "scripting", "jshell"), toBlockList("UNbloCk=SERIALIZATION,Process"));
|
||||
assertEquals(Set.of("jndi", "rmi", "httpserver", "unsafe", "scripting", "jshell"), toBlockList(" unblock\t= serialization , process\t"));
|
||||
assertEquals(Set.of(), toBlockList("unblock=jndi,rmi,process,httpserver,serialization,unsafe,scripting,jshell"));
|
||||
assertEquals(Set.of("jndi"), toBlockList("block=jndi"));
|
||||
assertEquals(Set.of("jndi", "rmi", "process"), toBlockList("block=jndi,rmi,process"));
|
||||
assertEquals(Set.of("jndi", "rmi", "process"), toBlockList("block = jndi\t, rmi ,\nprocess"));
|
||||
assertEquals(Set.of("jndi", "rmi", "process"), toBlockList("BLOck = JNDI\t, rmi ,\nProcESs"));
|
||||
|
||||
assertThrowsIAE(() -> toBlockList("blahblah"), "ERROR: Invalid agent configuration string");
|
||||
assertThrowsIAE(() -> toBlockList("foo=bar"), "ERROR: Unrecognized parameter name (should be one of 'block' or 'unblock'): foo");
|
||||
assertThrowsIAE(() -> toBlockList("block=incorrect"), "ERROR: Unrecognized feature name: incorrect");
|
||||
assertThrowsIAE(() -> toBlockList("unblock=incorrect"), "ERROR: Unrecognized feature name: incorrect");
|
||||
assertThrowsIAE(() -> toBlockList("block=serialization,process,incorrect,jndi"), "ERROR: Unrecognized feature name: incorrect");
|
||||
assertThrowsIAE(() -> toBlockList("unblock=serialization,process,incorrect,jndi"), "ERROR: Unrecognized feature name: incorrect");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testJndi() throws Exception {
|
||||
|
||||
String string = "foo";
|
||||
Name name = new LdapName("cn=foo");
|
||||
Object object = new Object();
|
||||
InitialContext initialContext = new InitialContext();
|
||||
|
||||
assertThrowsNICE(() -> initialContext.lookup(string));
|
||||
assertThrowsNICE(() -> initialContext.lookup(name));
|
||||
assertThrowsNICE(() -> initialContext.bind(string, object));
|
||||
assertThrowsNICE(() -> initialContext.bind(name, object));
|
||||
assertThrowsNICE(() -> initialContext.rebind(string, object));
|
||||
assertThrowsNICE(() -> initialContext.rebind(name, object));
|
||||
assertThrowsNICE(() -> initialContext.unbind(string));
|
||||
assertThrowsNICE(() -> initialContext.unbind(name));
|
||||
assertThrowsNICE(() -> initialContext.rename(string, string));
|
||||
assertThrowsNICE(() -> initialContext.rename(name, name));
|
||||
assertThrowsNICE(() -> initialContext.list(string));
|
||||
assertThrowsNICE(() -> initialContext.list(name));
|
||||
assertThrowsNICE(() -> initialContext.listBindings(string));
|
||||
assertThrowsNICE(() -> initialContext.listBindings(name));
|
||||
assertThrowsNICE(() -> initialContext.destroySubcontext(string));
|
||||
assertThrowsNICE(() -> initialContext.destroySubcontext(name));
|
||||
assertThrowsNICE(() -> initialContext.createSubcontext(string));
|
||||
assertThrowsNICE(() -> initialContext.createSubcontext(name));
|
||||
assertThrowsNICE(() -> initialContext.lookupLink(string));
|
||||
assertThrowsNICE(() -> initialContext.lookupLink(name));
|
||||
assertThrowsNICE(() -> initialContext.getNameParser(string));
|
||||
assertThrowsNICE(() -> initialContext.getNameParser(name));
|
||||
assertThrowsNICE(() -> initialContext.addToEnvironment(string, object));
|
||||
assertThrowsNICE(() -> initialContext.removeFromEnvironment(string));
|
||||
assertThrowsNICE(() -> initialContext.getEnvironment());
|
||||
assertThrowsNICE(() -> initialContext.getNameInNamespace());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRmi() {
|
||||
|
||||
int integer = 9090;
|
||||
String string = "foo";
|
||||
RMIClientSocketFactory clientSocketFactory = null;
|
||||
RMIServerSocketFactory serverSocketFactory = null;
|
||||
|
||||
assertThrowsSNFE(() -> LocateRegistry.getRegistry(integer));
|
||||
assertThrowsSNFE(() -> LocateRegistry.getRegistry(string));
|
||||
assertThrowsSNFE(() -> LocateRegistry.getRegistry(string, integer));
|
||||
assertThrowsSNFE(() -> LocateRegistry.getRegistry(string, integer, clientSocketFactory));
|
||||
assertThrowsSNFE(() -> LocateRegistry.createRegistry(integer));
|
||||
assertThrowsSNFE(() -> LocateRegistry.createRegistry(integer, clientSocketFactory, serverSocketFactory));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProcess() {
|
||||
|
||||
Runtime runtime = Runtime.getRuntime();
|
||||
String string = "foo";
|
||||
String[] array = new String[] { "foo" };
|
||||
File file = new File(".");
|
||||
|
||||
assertThrowsIOE(() -> runtime.exec(string));
|
||||
assertThrowsIOE(() -> runtime.exec(array));
|
||||
assertThrowsIOE(() -> runtime.exec(string, array));
|
||||
assertThrowsIOE(() -> runtime.exec(array, array));
|
||||
assertThrowsIOE(() -> runtime.exec(string, array, file));
|
||||
assertThrowsIOE(() -> runtime.exec(array, array, file));
|
||||
|
||||
assertThrowsIOE(() -> new ProcessBuilder(string).start());
|
||||
assertThrowsIOE(() -> new ProcessBuilder(array).start());
|
||||
assertThrowsIOE(() -> new ProcessBuilder(List.of()).start());
|
||||
assertThrowsIOE(() -> ProcessBuilder.startPipeline(List.of()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHttpServer() {
|
||||
assertThrowsRE(() -> HttpServer.create(), "HTTP server provider lookup blocked by aegis4j");
|
||||
assertThrowsRE(() -> HttpServer.create(null, 0), "HTTP server provider lookup blocked by aegis4j");
|
||||
assertThrowsRE(() -> HttpServerProvider.provider(), "HTTP server provider lookup blocked by aegis4j");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testJShell() {
|
||||
assertThrowsRE(() -> JShell.builder(), "JShell blocked by aegis4j");
|
||||
assertThrowsRE(() -> JShell.create(), "JShell blocked by aegis4j");
|
||||
assertThrowsRE(() -> JavaShellToolBuilder.builder(), "JShell blocked by aegis4j");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testScripting() {
|
||||
assertThrowsRE(() -> new ScriptEngineManager(), "Scripting blocked by aegis4j");
|
||||
assertThrowsRE(() -> new ScriptEngineManager(getClass().getClassLoader()), "Scripting blocked by aegis4j");
|
||||
assertThrowsRE(() -> new SimpleScriptContext(), "Scripting blocked by aegis4j");
|
||||
assertThrowsRE(() -> new CompiledScript() {
|
||||
@Override public Object eval(ScriptContext context) { return null; }
|
||||
@Override public ScriptEngine getEngine() { return null; }
|
||||
}, "Scripting blocked by aegis4j");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSerialization() {
|
||||
|
||||
ByteArrayInputStream bais = new ByteArrayInputStream(new byte[0]);
|
||||
assertThrowsRE(() -> new ObjectInputStream(bais), "Java deserialization blocked by aegis4j");
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
assertThrowsRE(() -> new ObjectOutputStream(baos), "Java serialization blocked by aegis4j");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnsafe() throws Exception {
|
||||
|
||||
Field f = Unsafe.class.getDeclaredField("theUnsafe");
|
||||
f.setAccessible(true);
|
||||
Unsafe unsafe = (Unsafe) f.get(null);
|
||||
|
||||
String msg = "Unsafe blocked by aegis4j";
|
||||
|
||||
assertThrowsRE(() -> Unsafe.getUnsafe(), msg);
|
||||
assertThrowsRE(() -> unsafe.addressSize(), msg);
|
||||
assertThrowsRE(() -> unsafe.allocateInstance(null), msg);
|
||||
assertThrowsRE(() -> unsafe.allocateMemory(1), msg);
|
||||
assertThrowsRE(() -> unsafe.arrayBaseOffset(null), msg);
|
||||
assertThrowsRE(() -> unsafe.arrayIndexScale(null), msg);
|
||||
assertThrowsRE(() -> unsafe.compareAndSwapInt(null, 0, 0, 0), msg);
|
||||
assertThrowsRE(() -> unsafe.compareAndSwapLong(null, 0, 0, 0), msg);
|
||||
assertThrowsRE(() -> unsafe.compareAndSwapObject(null, 0, null, null), msg);
|
||||
assertThrowsRE(() -> unsafe.copyMemory(0, 0, 0), msg);
|
||||
assertThrowsRE(() -> unsafe.copyMemory(null, 0, null, 0, 0), msg);
|
||||
assertThrowsRE(() -> unsafe.ensureClassInitialized(null), msg);
|
||||
assertThrowsRE(() -> unsafe.freeMemory(0), msg);
|
||||
assertThrowsRE(() -> unsafe.fullFence(), msg);
|
||||
assertThrowsRE(() -> unsafe.getAddress(0), msg);
|
||||
assertThrowsRE(() -> unsafe.getAndAddInt(null, 0, 0), msg);
|
||||
assertThrowsRE(() -> unsafe.getAndAddLong(null, 0, 0), msg);
|
||||
assertThrowsRE(() -> unsafe.getAndSetInt(null, 0, 0), msg);
|
||||
assertThrowsRE(() -> unsafe.getAndSetLong(null, 0, 0), msg);
|
||||
assertThrowsRE(() -> unsafe.getAndSetObject(null, 0, null), msg);
|
||||
assertThrowsRE(() -> unsafe.getBoolean(null, 0), msg);
|
||||
assertThrowsRE(() -> unsafe.getBooleanVolatile(null, 0), msg);
|
||||
assertThrowsRE(() -> unsafe.getByte(0), msg);
|
||||
assertThrowsRE(() -> unsafe.getByte(null, 0), msg);
|
||||
assertThrowsRE(() -> unsafe.getByteVolatile(null, 0), msg);
|
||||
assertThrowsRE(() -> unsafe.getChar(0), msg);
|
||||
assertThrowsRE(() -> unsafe.getChar(null, 0), msg);
|
||||
assertThrowsRE(() -> unsafe.getCharVolatile(null, 0), msg);
|
||||
assertThrowsRE(() -> unsafe.getDouble(0), msg);
|
||||
assertThrowsRE(() -> unsafe.getDouble(null, 0), msg);
|
||||
assertThrowsRE(() -> unsafe.getDoubleVolatile(null, 0), msg);
|
||||
assertThrowsRE(() -> unsafe.getFloat(0), msg);
|
||||
assertThrowsRE(() -> unsafe.getFloat(null, 0), msg);
|
||||
assertThrowsRE(() -> unsafe.getFloatVolatile(null, 0), msg);
|
||||
assertThrowsRE(() -> unsafe.getInt(0), msg);
|
||||
assertThrowsRE(() -> unsafe.getInt(null, 0), msg);
|
||||
assertThrowsRE(() -> unsafe.getIntVolatile(null, 0), msg);
|
||||
assertThrowsRE(() -> unsafe.getLoadAverage(null, 0), msg);
|
||||
assertThrowsRE(() -> unsafe.getLong(0), msg);
|
||||
assertThrowsRE(() -> unsafe.getLong(null, 0), msg);
|
||||
assertThrowsRE(() -> unsafe.getLongVolatile(null, 0), msg);
|
||||
assertThrowsRE(() -> unsafe.getObject(null, 0), msg);
|
||||
assertThrowsRE(() -> unsafe.getObjectVolatile(null, 0), msg);
|
||||
assertThrowsRE(() -> unsafe.getShort(0), msg);
|
||||
assertThrowsRE(() -> unsafe.getShort(null, 0), msg);
|
||||
assertThrowsRE(() -> unsafe.getShortVolatile(null, 0), msg);
|
||||
assertThrowsRE(() -> unsafe.invokeCleaner(null), msg);
|
||||
assertThrowsRE(() -> unsafe.loadFence(), msg);
|
||||
assertThrowsRE(() -> unsafe.objectFieldOffset(null), msg);
|
||||
assertThrowsRE(() -> unsafe.pageSize(), msg);
|
||||
assertThrowsRE(() -> unsafe.park(false, 0), msg);
|
||||
assertThrowsRE(() -> unsafe.putAddress(0, 0), msg);
|
||||
assertThrowsRE(() -> unsafe.putBoolean(null, 0, false), msg);
|
||||
assertThrowsRE(() -> unsafe.putBooleanVolatile(null, 0, false), msg);
|
||||
assertThrowsRE(() -> unsafe.putByte(0, (byte) 0), msg);
|
||||
assertThrowsRE(() -> unsafe.putByte(null, 0, (byte) 0), msg);
|
||||
assertThrowsRE(() -> unsafe.putByteVolatile(null, 0, (byte) 0), msg);
|
||||
assertThrowsRE(() -> unsafe.putChar(0, 'x'), msg);
|
||||
assertThrowsRE(() -> unsafe.putChar(null, 0, 'x'), msg);
|
||||
assertThrowsRE(() -> unsafe.putCharVolatile(null, 0, 'x'), msg);
|
||||
assertThrowsRE(() -> unsafe.putDouble(0, 0), msg);
|
||||
assertThrowsRE(() -> unsafe.putDouble(null, 0, 0), msg);
|
||||
assertThrowsRE(() -> unsafe.putDoubleVolatile(null, 0, 0), msg);
|
||||
assertThrowsRE(() -> unsafe.putFloat(0, 0), msg);
|
||||
assertThrowsRE(() -> unsafe.putFloat(null, 0, 0), msg);
|
||||
assertThrowsRE(() -> unsafe.putFloatVolatile(null, 0, 0), msg);
|
||||
assertThrowsRE(() -> unsafe.putInt(0, 0), msg);
|
||||
assertThrowsRE(() -> unsafe.putInt(null, 0, 0), msg);
|
||||
assertThrowsRE(() -> unsafe.putIntVolatile(null, 0, 0), msg);
|
||||
assertThrowsRE(() -> unsafe.putLong(0, 0), msg);
|
||||
assertThrowsRE(() -> unsafe.putLong(null, 0, 0), msg);
|
||||
assertThrowsRE(() -> unsafe.putLongVolatile(null, 0, 0), msg);
|
||||
assertThrowsRE(() -> unsafe.putObject(null, 0, null), msg);
|
||||
assertThrowsRE(() -> unsafe.putObjectVolatile(null, 0, null), msg);
|
||||
assertThrowsRE(() -> unsafe.putOrderedInt(null, 0, 0), msg);
|
||||
assertThrowsRE(() -> unsafe.putOrderedLong(null, 0, 0), msg);
|
||||
assertThrowsRE(() -> unsafe.putOrderedObject(null, 0, null), msg);
|
||||
assertThrowsRE(() -> unsafe.putShort(0, (short) 0), msg);
|
||||
assertThrowsRE(() -> unsafe.putShort(null, 0, (short) 0), msg);
|
||||
assertThrowsRE(() -> unsafe.putShortVolatile(null, 0, (short) 0), msg);
|
||||
assertThrowsRE(() -> unsafe.reallocateMemory(0, 0), msg);
|
||||
assertThrowsRE(() -> unsafe.setMemory(0, 0, (byte) 0), msg);
|
||||
assertThrowsRE(() -> unsafe.setMemory(null, 0, 0, (byte) 0), msg);
|
||||
assertThrowsRE(() -> unsafe.shouldBeInitialized(null), msg);
|
||||
assertThrowsRE(() -> unsafe.staticFieldBase(null), msg);
|
||||
assertThrowsRE(() -> unsafe.staticFieldOffset(null), msg);
|
||||
assertThrowsRE(() -> unsafe.storeFence(), msg);
|
||||
assertThrowsRE(() -> unsafe.throwException(null), msg);
|
||||
assertThrowsRE(() -> unsafe.unpark(null), msg);
|
||||
|
||||
// Spring should still work with Unsafe disabled
|
||||
SpringObjenesis so = new SpringObjenesis();
|
||||
assertInstanceOf(SerializablePojo.class, so.newInstance(SerializablePojo.class));
|
||||
assertInstanceOf(TestUtils.class, so.newInstance(TestUtils.class));
|
||||
assertInstanceOf(LocalDate.class, so.newInstance(LocalDate.class));
|
||||
}
|
||||
|
||||
private static void assertThrowsNICE(Executable task) {
|
||||
assertThrows(task, NoInitialContextException.class, "JNDI context creation blocked by aegis4j");
|
||||
}
|
||||
|
||||
private static void assertThrowsSNFE(Executable task) {
|
||||
assertThrows(task, StubNotFoundException.class, "RMI registry creation blocked by aegis4j");
|
||||
}
|
||||
|
||||
private static void assertThrowsIOE(Executable task) {
|
||||
assertThrows(task, IOException.class, "Process execution blocked by aegis4j");
|
||||
}
|
||||
|
||||
private static void assertThrowsIAE(Executable task, String msg) {
|
||||
assertThrows(task, IllegalArgumentException.class, msg);
|
||||
}
|
||||
|
||||
private static void assertThrowsRE(Executable task, String msg) {
|
||||
assertThrows(task, RuntimeException.class, msg);
|
||||
}
|
||||
|
||||
private static void assertThrows(Executable task, Class< ? extends Throwable > type, String msg) {
|
||||
Throwable root;
|
||||
try {
|
||||
task.execute();
|
||||
root = null;
|
||||
} catch (Throwable t) {
|
||||
root = getRootCause(t);
|
||||
}
|
||||
assertNotNull(root, "No exception thrown");
|
||||
assertInstanceOf(type, root, "Exception is wrong type");
|
||||
assertEquals(msg, root.getMessage(), "Exception has wrong message");
|
||||
}
|
||||
|
||||
private static Throwable getRootCause(Throwable t) {
|
||||
while (t.getCause() != null) {
|
||||
t = t.getCause();
|
||||
}
|
||||
return t;
|
||||
}
|
||||
}
|
@ -0,0 +1,104 @@
|
||||
/* Copyright (c) 2022, Daniel Gredler. All rights reserved. */
|
||||
|
||||
package net.gredler.aegis4j;
|
||||
|
||||
import static net.gredler.aegis4j.TestUtils.OWNED;
|
||||
import static net.gredler.aegis4j.TestUtils.installAgent;
|
||||
import static net.gredler.aegis4j.TestUtils.toBytes;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Comparator;
|
||||
import java.util.PriorityQueue;
|
||||
|
||||
import org.apache.commons.collections4.FunctorException;
|
||||
import org.apache.commons.collections4.Transformer;
|
||||
import org.apache.commons.collections4.comparators.TransformingComparator;
|
||||
import org.apache.commons.collections4.functors.ChainedTransformer;
|
||||
import org.apache.commons.collections4.functors.ConstantTransformer;
|
||||
import org.apache.commons.collections4.functors.InvokerTransformer;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Tests mitigation of CVE-2015-7501, both at the process execution level and at the serialization level.
|
||||
* We test using a {@link PriorityQueue} with a custom {@link Comparator}. The comparator is actually a
|
||||
* {@link ChainedTransformer} which uses reflection to invoke {@link Runtime#exec(String)}. The comparator
|
||||
* magic is invoked when the queue needs to be sorted, which happens when items are added to the queue
|
||||
* and also as a last step when the queue is deserialized.
|
||||
*
|
||||
* @see <a href="https://nvd.nist.gov/vuln/detail/CVE-2015-7501"></a>
|
||||
* @see <a href="https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/CommonsCollections6.java">Exploit POC</a>
|
||||
*/
|
||||
public class CVE_2015_7501 {
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
public void test() throws Exception {
|
||||
|
||||
Path temp = Files.createTempFile("aegis4j-", ".tmp");
|
||||
temp.toFile().deleteOnExit();
|
||||
String path = temp.toAbsolutePath().toString();
|
||||
|
||||
boolean windows = System.getProperty("os.name").toLowerCase().contains("windows");
|
||||
String[] cmd = windows ? new String[] { "cmd.exe", "/c", "echo " + OWNED + ">" + path }
|
||||
: new String[] { "sh", "-c", "echo " + OWNED + ">" + path };
|
||||
|
||||
Transformer transformerChain = new ChainedTransformer(new Transformer[] {
|
||||
new ConstantTransformer(Runtime.class),
|
||||
new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] }),
|
||||
new InvokerTransformer("invoke", new Class[] { Object.class, Object[].class }, new Object[] { null, new Object[0] }),
|
||||
new InvokerTransformer("exec", new Class[] { String[].class }, new Object[] { cmd }),
|
||||
new ConstantTransformer(1)
|
||||
});
|
||||
|
||||
// trigger directly, verify owned
|
||||
PriorityQueue queue = new PriorityQueue(2, new TransformingComparator(transformerChain));
|
||||
queue.add(1);
|
||||
queue.add(1);
|
||||
Thread.sleep(500); // wait for file changes to sync
|
||||
assertEquals(OWNED + System.lineSeparator(), Files.readString(temp), path);
|
||||
|
||||
// reset
|
||||
Files.write(temp, new byte[0]);
|
||||
assertEquals("", Files.readString(temp), path);
|
||||
|
||||
// trigger via deserialization, verify owned again
|
||||
byte[] serialized = toBytes(queue);
|
||||
new ObjectInputStream(new ByteArrayInputStream(serialized)).readObject();
|
||||
Thread.sleep(500); // wait for file changes to sync
|
||||
assertEquals(OWNED + System.lineSeparator(), Files.readString(temp), path);
|
||||
|
||||
// reset
|
||||
Files.write(temp, new byte[0]);
|
||||
assertEquals("", Files.readString(temp), path);
|
||||
|
||||
// install aegis4j agent
|
||||
installAgent(null);
|
||||
|
||||
// trigger again directly, verify not owned
|
||||
try {
|
||||
PriorityQueue queue2 = new PriorityQueue(2, new TransformingComparator(transformerChain));
|
||||
queue2.add(1);
|
||||
queue2.add(1);
|
||||
fail("Exception expected");
|
||||
} catch (FunctorException e) {
|
||||
Thread.sleep(500); // wait for file changes to sync
|
||||
assertEquals("", Files.readString(temp), path);
|
||||
assertEquals("Process execution blocked by aegis4j", e.getCause().getCause().getMessage());
|
||||
}
|
||||
|
||||
// trigger again via deserialization, verify not owned
|
||||
try {
|
||||
new ObjectInputStream(new ByteArrayInputStream(serialized)).readObject();
|
||||
fail("Exception expected");
|
||||
} catch (RuntimeException e) {
|
||||
Thread.sleep(500); // wait for file changes to sync
|
||||
assertEquals("", Files.readString(temp), path);
|
||||
assertEquals("Java deserialization blocked by aegis4j", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
/* Copyright (c) 2022, Daniel Gredler. All rights reserved. */
|
||||
|
||||
package net.gredler.aegis4j;
|
||||
|
||||
import static net.gredler.aegis4j.TestUtils.testLdap;
|
||||
|
||||
import org.apache.log4j.receivers.db.JNDIConnectionSource;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.function.Executable;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* Tests mitigation of CVE-2019-17531. No setup is required besides starting the LDAP server that
|
||||
* serves serialized {@link SerializableDataSource} instances. The vulnerability is triggered when
|
||||
* we deserialize a JSON payload which contains a {@link JNDIConnectionSource} which references
|
||||
* our LDAP server, and re-serializing the deserialized {@link JNDIConnectionSource} triggers a
|
||||
* JNDI lookup.
|
||||
*
|
||||
* @see <a href="https://nvd.nist.gov/vuln/detail/CVE-2019-17531">CVE-2019-17531</a>
|
||||
* @see <a href="https://github.com/FasterXML/jackson-databind/issues/2498">Jackson issue #2498</a>
|
||||
* @see <a href="https://cowtowncoder.medium.com/on-jackson-cves-dont-panic-here-is-what-you-need-to-know-54cd0d6e8062">On Jackson CVEs</a>
|
||||
* @see <a href="https://swapneildash.medium.com/understanding-insecure-implementation-of-jackson-deserialization-7b3d409d2038">Understanding Jackson deserialization</a>
|
||||
*/
|
||||
public class CVE_2019_17531 {
|
||||
|
||||
@Test
|
||||
public void test() throws Throwable {
|
||||
|
||||
Executable setup = () -> {
|
||||
// only the LDAP server is needed
|
||||
};
|
||||
|
||||
Executable trigger = () -> {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
String json = "{ \"property\": { \"@class\": \"org.apache.log4j.receivers.db.JNDIConnectionSource\", \"jndiLocation\": \"ldap://localhost:8181/dc=foo\" } }";
|
||||
JsonPayload payload = mapper.readValue(json, JsonPayload.class);
|
||||
mapper.writeValueAsString(payload); // triggers payload.property.getConnection()
|
||||
};
|
||||
|
||||
testLdap(setup, trigger, SerializableDataSource.class, true);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
/* Copyright (c) 2022, Daniel Gredler. All rights reserved. */
|
||||
|
||||
package net.gredler.aegis4j;
|
||||
|
||||
import static net.gredler.aegis4j.TestUtils.testLdap;
|
||||
|
||||
import org.apache.logging.log4j.Level;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.logging.log4j.core.config.Configurator;
|
||||
import org.apache.logging.log4j.core.config.builder.api.AppenderComponentBuilder;
|
||||
import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilder;
|
||||
import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilderFactory;
|
||||
import org.apache.logging.log4j.core.config.builder.api.LayoutComponentBuilder;
|
||||
import org.apache.logging.log4j.core.config.builder.api.RootLoggerComponentBuilder;
|
||||
import org.apache.logging.log4j.core.config.builder.impl.BuiltConfiguration;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.function.Executable;
|
||||
|
||||
/**
|
||||
* Tests mitigation of CVE-2021-44228 (a.k.a. Log4Shell). During setup we simply configure log4j to
|
||||
* perform basic logging, and set up our LDAP server to serve serialized {@link SerializablePojo}
|
||||
* instances. The vulnerability is triggered when we log a message which contains a JNDI lookup.
|
||||
*
|
||||
* @see <a href="https://nvd.nist.gov/vuln/detail/CVE-2021-44228">CVE-2021-44228</a>
|
||||
* @see <a href="https://www.wired.com/story/log4j-log4shell/">Everybody freaking out</a>
|
||||
* @see <a href="https://research.nccgroup.com/2021/12/12/log4j-jndi-be-gone-a-simple-mitigation-for-cve-2021-44228/">log4j-jndi-be-gone</a>
|
||||
*/
|
||||
public class CVE_2021_44228 {
|
||||
|
||||
@Test
|
||||
public void test() throws Throwable {
|
||||
|
||||
Executable setup = () -> {
|
||||
configureLog4J2();
|
||||
};
|
||||
|
||||
Executable trigger = () -> {
|
||||
Logger logger = LogManager.getLogger();
|
||||
logger.info("${jndi:ldap://localhost:8181/dc=foo}");
|
||||
};
|
||||
|
||||
testLdap(setup, trigger, SerializablePojo.class, false);
|
||||
}
|
||||
|
||||
// https://logging.apache.org/log4j/2.x/manual/customconfig.html
|
||||
// https://www.baeldung.com/log4j2-programmatic-config
|
||||
private static void configureLog4J2() {
|
||||
|
||||
ConfigurationBuilder< BuiltConfiguration > builder = ConfigurationBuilderFactory.newConfigurationBuilder();
|
||||
|
||||
LayoutComponentBuilder standard = builder.newLayout("PatternLayout");
|
||||
standard.addAttribute("pattern", "%d [%t] %-5level: %msg%n");
|
||||
|
||||
AppenderComponentBuilder console = builder.newAppender("stdout", "console");
|
||||
console.add(standard);
|
||||
builder.add(console);
|
||||
|
||||
RootLoggerComponentBuilder root = builder.newRootLogger(Level.INFO);
|
||||
root.add(builder.newAppenderRef("stdout"));
|
||||
builder.add(root);
|
||||
|
||||
Configurator.initialize(builder.build());
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
/* Copyright (c) 2022, Daniel Gredler. All rights reserved. */
|
||||
|
||||
package net.gredler.aegis4j;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo.Id;
|
||||
|
||||
/**
|
||||
* Playing with deserialization (polymorphic type handling) fire.
|
||||
*/
|
||||
public class JsonPayload {
|
||||
|
||||
@JsonTypeInfo(use = Id.CLASS)
|
||||
public Object property;
|
||||
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
/* Copyright (c) 2022, Daniel Gredler. All rights reserved. */
|
||||
|
||||
package net.gredler.aegis4j;
|
||||
|
||||
import static net.gredler.aegis4j.TestUtils.OWNED;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.io.Serializable;
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.SQLFeatureNotSupportedException;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
/**
|
||||
* Sets a system property when {@link #getConnection()} is called, as proof of vulnerability.
|
||||
*/
|
||||
public class SerializableDataSource implements DataSource, Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1663970917224862202L;
|
||||
|
||||
@Override
|
||||
public Connection getConnection() throws SQLException {
|
||||
System.setProperty(OWNED, Boolean.TRUE.toString());
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Connection getConnection(String username, String password) throws SQLException {
|
||||
System.setProperty(OWNED, Boolean.TRUE.toString());
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public < T > T unwrap(Class< T > iface) throws SQLException {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWrapperFor(Class< ? > iface) throws SQLException {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrintWriter getLogWriter() throws SQLException {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLogWriter(PrintWriter out) throws SQLException {
|
||||
// empty
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLoginTimeout(int seconds) throws SQLException {
|
||||
// empty
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLoginTimeout() throws SQLException {
|
||||
return 0;
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
/* Copyright (c) 2022, Daniel Gredler. All rights reserved. */
|
||||
|
||||
package net.gredler.aegis4j;
|
||||
|
||||
import static net.gredler.aegis4j.TestUtils.OWNED;
|
||||
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Sets a system property upon deserialization, as proof of vulnerability.
|
||||
*/
|
||||
public class SerializablePojo implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = -3148228827965096990L;
|
||||
|
||||
private void readObject(ObjectInputStream input) {
|
||||
// code executed when object is deserialized
|
||||
System.setProperty(OWNED, Boolean.TRUE.toString());
|
||||
}
|
||||
}
|
133
tools/aegis4j/src/test/java/net/gredler/aegis4j/TestUtils.java
Normal file
133
tools/aegis4j/src/test/java/net/gredler/aegis4j/TestUtils.java
Normal file
@ -0,0 +1,133 @@
|
||||
/* Copyright (c) 2022, Daniel Gredler. All rights reserved. */
|
||||
|
||||
package net.gredler.aegis4j;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarOutputStream;
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
import org.junit.jupiter.api.function.Executable;
|
||||
|
||||
import com.sun.tools.attach.VirtualMachine;
|
||||
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
|
||||
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
|
||||
import com.unboundid.ldap.listener.InMemoryListenerConfig;
|
||||
import com.unboundid.ldap.sdk.DN;
|
||||
import com.unboundid.ldap.sdk.Entry;
|
||||
import com.unboundid.ldap.sdk.LDAPException;
|
||||
|
||||
/**
|
||||
* Base test class which makes it easy to set up an embedded LDAP server, trigger an LDAP-related
|
||||
* vulnerability, enable the Java agent, and then verify that the vulnerability is no longer exploitable.
|
||||
*/
|
||||
public final class TestUtils {
|
||||
|
||||
public static final String OWNED = "owned";
|
||||
|
||||
private TestUtils() {}
|
||||
|
||||
public static void testLdap(Executable setup, Executable trigger, Class< ? > ldapPayload, boolean expectException) throws Throwable {
|
||||
|
||||
setup.execute();
|
||||
InMemoryDirectoryServer ldapServer = createLdapServer(8181, "dc=foo", ldapPayload);
|
||||
assertNull(System.getProperty(OWNED));
|
||||
|
||||
trigger.execute();
|
||||
assertTrue(Boolean.valueOf(System.getProperty(OWNED)));
|
||||
|
||||
System.clearProperty(OWNED);
|
||||
assertNull(System.getProperty(OWNED));
|
||||
|
||||
installAgent(null);
|
||||
|
||||
try {
|
||||
trigger.execute();
|
||||
assertFalse(expectException);
|
||||
assertNull(System.getProperty(OWNED));
|
||||
} catch (Exception e) {
|
||||
assertTrue(expectException);
|
||||
assertNull(System.getProperty(OWNED));
|
||||
assertTrue(e.getMessage().contains("JNDI context creation blocked by aegis4j"));
|
||||
}
|
||||
|
||||
ldapServer.shutDown(true);
|
||||
}
|
||||
|
||||
// https://docs.oracle.com/javase/jndi/tutorial/objects/representation/ldap.html
|
||||
// https://www.blackhat.com/docs/us-16/materials/us-16-Munoz-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE.pdf
|
||||
private static InMemoryDirectoryServer createLdapServer(int port, String partitionSuffix, Class< ? > payload)
|
||||
throws IOException, LDAPException, InstantiationException, ReflectiveOperationException {
|
||||
|
||||
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(partitionSuffix);
|
||||
config.setListenerConfigs(InMemoryListenerConfig.createLDAPConfig("LDAP", port));
|
||||
config.setSchema(null);
|
||||
|
||||
Entry entry = new Entry(new DN(partitionSuffix));
|
||||
entry.addAttribute("objectClass", "javaNamingReference");
|
||||
entry.addAttribute("javaClassName", "Test");
|
||||
entry.addAttribute("javaCodeBase", "http://localhost/");
|
||||
entry.addAttribute("javaSerializedData", toBytes(payload.getDeclaredConstructor().newInstance()));
|
||||
|
||||
InMemoryDirectoryServer directoryServer = new InMemoryDirectoryServer(config);
|
||||
directoryServer.add(entry);
|
||||
directoryServer.startListening();
|
||||
return directoryServer;
|
||||
}
|
||||
|
||||
public static String createAgentJar() throws IOException {
|
||||
Class< ? > clazz = AegisAgent.class;
|
||||
Path jar = Files.createTempFile("aegis4j-", ".jar");
|
||||
jar.toFile().deleteOnExit();
|
||||
Manifest manifest = new Manifest();
|
||||
manifest.getMainAttributes().putValue("Manifest-Version", "1.0");
|
||||
manifest.getMainAttributes().putValue("Main-Class", clazz.getName());
|
||||
manifest.getMainAttributes().putValue("Agent-Class", clazz.getName());
|
||||
manifest.getMainAttributes().putValue("Premain-Class", clazz.getName());
|
||||
manifest.getMainAttributes().putValue("Can-Redefine-Classes", "true");
|
||||
manifest.getMainAttributes().putValue("Can-Retransform-Classes", "true");
|
||||
manifest.getMainAttributes().putValue("Can-Set-Native-Method-Prefix", "false");
|
||||
try (OutputStream os = Files.newOutputStream(jar); JarOutputStream jos = new JarOutputStream(os, manifest)) {
|
||||
JarEntry entry = new JarEntry(clazz.getName().replace('.', '/') + ".class");
|
||||
entry.setTime(System.currentTimeMillis());
|
||||
jos.putNextEntry(entry);
|
||||
jos.write(toBytes(clazz));
|
||||
jos.closeEntry();
|
||||
}
|
||||
return jar.toAbsolutePath().toString();
|
||||
}
|
||||
|
||||
private static byte[] toBytes(Class< ? > clazz) throws IOException {
|
||||
String path = clazz.getName().replace('.', '/') + ".class";
|
||||
InputStream stream = clazz.getClassLoader().getResourceAsStream(path);
|
||||
return stream.readAllBytes();
|
||||
}
|
||||
|
||||
public static byte[] toBytes(Object object) throws IOException {
|
||||
try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream out = new ObjectOutputStream(bos)) {
|
||||
out.writeObject(object);
|
||||
out.flush();
|
||||
return bos.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requires {@code -Djdk.attach.allowAttachSelf=true} on the command line.
|
||||
*/
|
||||
public static void installAgent(String options) throws Exception {
|
||||
long pid = ProcessHandle.current().pid();
|
||||
VirtualMachine jvm = VirtualMachine.attach(String.valueOf(pid));
|
||||
jvm.loadAgent(createAgentJar(), options);
|
||||
jvm.detach();
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user