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
|