ID Product Version Vulnerability
CVE-2019-25022 Scytl Secure Vote (sVote) 2.1 SDM RCE

Description

Root-Cause:
File:
evoting-solution/source-code/online-voting-secure-data-manager/secure-data-manager-backend/secure-data-manager-integration/src/main/java/com/scytl/products/ov/sdm/plugin/SequentialExecutorImpl.java


Contains the following function:
public void execute(List commands, Parameters parameters, ExecutionListener listener) {

    for(String command : commands) {
        String mockCommand = command;
        try {
            //Replace the parameters and execute the command
            String[] partialCommands = replaceParameters(command, parameters);
            String fullCommand = partialCommands[0];
            mockCommand = partialCommands[1];
            Process proc = Runtime.getRuntime().exec(fullCommand);
The mentioned replace function doesn't do anything security wise:
private String[] replaceParameters(String command, Parameters parameters) {
    String partialCommand = command;
    String replacedCommand = command;
    
    for (KeyParameter key : KeyParameter.values()) {
        if(replacedCommand.contains(key.toString())) {
            String value = parameters.getParam(key.name());
            if(value == null || value.isEmpty()){
                throw new IllegalArgumentException("Parameter #" + key.name() + "# is null or empty");
            } else {
            replacedCommand = replacedCommand.replaceAll("#" + key + "#", value);
                if (key == KeyParameter.PRIVATE_KEY) {
                    partialCommand = partialCommand.replaceAll("#" + key + "#", "PRIVATE_KEY");
                } else {
                    partialCommand = partialCommand.replaceAll("#" + key + "#", value);
                }
            }
Trigger:
File:
/secure-data-manager-backend/sdm-ws-rest/src/main/java/com/scytl/products/ov/sdm/ui/ws/rs/application/OperationsResource.java


The vulnerability can be triggered from the ws-rest backend (api):
@RequestMapping(value = "/generate-ea-structure/{electionEventId}", method = RequestMethod.POST)
@ApiOperation(value = "Export operation service", notes = "", response = Void.class)
@ApiResponses(value = {@ApiResponse(code = 404, message = "Not Found"),
        @ApiResponse(code = 403, message = "Forbidden"),
        @ApiResponse(code = 500, message = "Internal Server Error") })
public ResponseEntity extendedAuthenticationMappingDataOperation(
        @ApiParam(value = "String", required = true) @PathVariable String electionEventId,
        @RequestBody final OperationsData request) {
    Parameters parameters = buildParameters(electionEventId, request.getPrivateKeyInBase64(), null);
    return executeOperationForPhase(parameters, PhaseName.PREPARE_VC_GENERATION, true, null, null);
}
The electionEventId is declared as a string instead of an id without validation which allows us to pass arbitrary data:
@ApiParam(value = "String", required = true) @PathVariable String electionEventId,

Then buildParameters() is called:
private Parameters buildParameters(String electionEventId, String privateKeyInBase64, String path) {
    Parameters parameters = new Parameters();

    if (StringUtils.isNotEmpty(electionEventId)) {
        String electionEventAlias = electionEventService.getElectionEventAlias(electionEventId);
        parameters.addParam(KeyParameter.EE_ALIAS.name(), electionEventAlias);
        parameters.addParam(KeyParameter.EE_ID.name(), electionEventId);
    }
    Path sdmPath = pathResolver.resolve(ConfigConstants.SDM_DIR_NAME);
    parameters.addParam(KeyParameter.SDM_PATH.name(), sdmPath.toString().replace("\\", "/"));
    if (StringUtils.isNotEmpty(privateKeyInBase64)) {
        parameters.addParam(KeyParameter.PRIVATE_KEY.name(), privateKeyInBase64);
    }
    if (StringUtils.isNotEmpty(path)) {
        parameters.addParam(KeyParameter.USB_LETTER.name(), path.replace("\\", "/"));
    }
    return parameters;
}
Which adds two parameters to the list:
- electionEventAlias (retrieved by electionEventService.getElectionEventAlias())
- electionEventId (user controlled!)
The following method adds parameters to our request:
/secure-data-manager-backend/secure-data-manager-services/src/main/java/com/scytl/products/ov/sdm/infrastructure/electionevent/ElectionEventRepositoryImpl.java
@Override
public String getElectionEventAlias(final String electionEventId) {
    String sql =
        "select alias from " + entityName() + " where id = :id";
    Map parameters = singletonMap(
        JsonConstants.JSON_ATTRIBUTE_NAME_ID, electionEventId);
    List documents;
    try {
        documents = selectDocuments(sql, parameters, 1);
    } catch (OException e) {
        throw new DatabaseException(
            "Failed to get election event alias.", e);
    }
    return documents.isEmpty() ? ""
            : documents.get(0).field("alias", String.class);
}	
Which leaves us with two values:
- parameters.addParam(KeyParameter.EE_ALIAS.name(), "");
- parameters.addParam(KeyParameter.EE_ID.name(), "evil payload");
That get passed to:
return executeOperationForPhase(parameters, PhaseName.PREPARE_VC_GENERATION, true, null, null);

Which redirects to sequentialExecutor.execute:
private ResponseEntity executeOperationForPhase(Parameters parameters, PhaseName phaseName, boolean failOnEmptyCommandsForPhase, SdmSecureLogEvent secureLogEvent, String electionEventId) {
    try {
        List commandsForPhase = getCommands(phaseName);

        if (failOnEmptyCommandsForPhase && commandsForPhase.isEmpty()) {
            logSecure(secureLogEvent, electionEventId,
                "The request can not be performed for 4005, Missing commands for phase");

            return handleException(OperationsOutputCode.MISSING_COMMANDS_FOR_PHASE);
        }
        ExecutionListenerImpl listener = new ExecutionListenerImpl();
        sequentialExecutor.execute(commandsForPhase, parameters, listener);
Which then executes our command and leaves us with control over the application.

Timeline

Date Event
February 08 2020 Submitted vulnerability to vendor
February 09 2020 Submitted additional details to vendor
February 18 2020 Vendor acknowledged the vulnerability

Credits

Name Team
Anthony Schneiter SUID
Jannis Kirschner SUID