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: 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: 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.

Disclosure Timeline

Credit

Jannis Kirschner & Anthony Schneiter from Team SUID