[하루한줄] CVE-2025-4428 : Ivanti EPPM 에서 발생한 Spring EL Injection 취약점
URL
https://labs.watchtowr.com/expression-payloads-meet-mayhem-cve-2025-4427-and-cve-2025-4428/
https://projectdiscovery.io/blog/ivanti-remote-code-execution
https://www.wiz.io/blog/ivanti-epmm-rce-vulnerability-chain-cve-2025-4427-cve-2025-4428
Target
- Ivanti Endpoint Manager Mobile
- 11.12.0.4 이하
- 12.3.0.1 이하
- 12.4.0.1 이하
- 12.5.0.0 이하
Explain
Ivanti EPPM 에서 Spring EL Injection 취약점이 발견되었습니다. 이 취약점은 ITW 에서 활발하게 Exploit 되던 취약점입니다.
/mifs/admin/rest/api/v2/featureusage
에서 사용되던 format 파라메터에서 취약점이 발생하여 다음과 같이 요청을 보내면 Spring EL Injection 를 일으킬 수 있었습니다.
GET /mifs/admin/rest/api/v2/featureusage?format=${7*7} HTTP/1.1
Host: 192.168.111.148
Cookie: ...
/api/v2/featureusage
를 라우팅하는 컨트롤러에서 downloadDeviceFeatureUsageReport
메서드를 확인할 수 있는데요 @Valid
어노테이션으로 DeviceFeatureUsageReportQueryRequest
를 ConstraintValidator
를 통해 입력값 검증을 하고 있습니다.
@PreAuthorize("hasPermissionForSpace(#adminDeviceSpaceId, {'PERM_FEATURE_USAGE_DATA_VIEW'})")
@RequestMapping(method = {RequestMethod.GET}, value = {"/api/v2/featureusage"})
@ResponseBody
@ApiOperation(value = "Download Device Feature Usage Report", notes = "Download Device Feature Usage Report", tags = {"DeviceFeatureUsage: All device feature usage related API"})
@ApiResponses({@ApiResponse(code = 500, message = "Internal Server Error")})
@PublicApi
public Response downloadDeviceFeatureUsageReport(@Valid @ModelAttribute DeviceFeatureUsageReportQueryRequest queryRequest, @RequestParam(defaultValue = "0") int adminDeviceSpaceId, @ApiIgnore Locale locale, @ApiIgnore HttpServletResponse httpServletResponse) throws IOException {
Response response = setResponseSuccess(httpServletResponse, locale);
try {
this.deviceFeatureUsageReportService.downloadDeviceFeatureUsageReport(getCurrentUserName(), queryRequest, httpServletResponse);
} catch (ResultNotFoundException e) {
MessageKeys messageKeys; httpServletResponse.setStatus(HttpStatus.NOT_FOUND.value());
if (StringUtils.isNotBlank(queryRequest.getDatafile())) {
messageKeys = MessageKeys.DEVICE_FEATURE_USAGE_DATAFILE_NOT_FOUND;
} else {
messageKeys = MessageKeys.DEVICE_FEATURE_USAGE_NOT_FOUND;
}
setErrorResponse((MessageCode)messageKeys, locale, HttpStatus.NOT_FOUND, response, httpServletResponse);
}
return response;
}
DeviceFeatureUsageReportQueryRequest
에서 format 이라는 변수가 정의되어 있습니다. [1]
public class DeviceFeatureUsageReportQueryRequest extends QueryRequestWithPagination {
public static final SortOrder DEFAULT_SORT_ORDER = SortOrder.DESC;
public static final String DEFAULT_SORT_COLUMN_NAME = "job_fired_at";
private String format = "json";
private String datafile;
public DeviceFeatureUsageReportQueryRequest() {
this.sortOrder = SortOrder.DESC;
}
public String getFormat() { // [1]
return this.format;
}
//...
}
@Valid
어노테이션을 사용하면 DeviceFeatureUsageReportQueryRequestValidator
에 구현된ConstraintValidator
의 isValid
가 호출되어 입력값 검증을 수행합니다.
해당 코드를 보면 format 이 json 이나 csv 가 아니면 에러를 발생시키도록 구현되어 있습니다. [1] 에서 format 값을 받고 [2] 에서 비교하며 [3] 에서 formatMessage 가 에러메시지에 매핑되죠.
implements ConstraintValidator<ValidDeviceFeatureUsageReportQueryRequest, DeviceFeatureUsageReportQueryRequest>
{
@Autowired
private LocalizedMessageBuilder localizedMessageBuilder;
public void initialize(ValidDeviceFeatureUsageReportQueryRequest constraintAnnotation) {}
public boolean isValid(DeviceFeatureUsageReportQueryRequest value, ConstraintValidatorContext context) {
String format = value.getFormat(); // [1]
if (format == null) {
return true;
}
boolean isValid = (format.equalsIgnoreCase("json") || format.equalsIgnoreCase("csv")); // [2]
if (!isValid) {
String formatMessage = this.localizedMessageBuilder.getLocalizedMessage((MessageCode)MessageKeys.DEVICE_FEATURE_USAGE_INVALID_FORMAT, new Object[] { format }); // [3]
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(formatMessage).addConstraintViolation(); // [4]
}
return isValid;
}
}
에러메시지는 다음과 같이 되어 있으며 {0} 부분에 유저 입력값이 들어가서 포맷팅이 되는 것입니다.
@Localize(
value = "Format ''{0}'' is invalid. Valid formats are ''json'', ''csv''.",
key = "com.mobileiron.vsp.messages.device.feature.usage.report.invalid.format"
)
그리고 [4] context.buildConstraintViolationWithTemplate(formatMessage)
를 호출하며 해당 포맷이 템플릿화 됩니다. 해당 부분 때문에 SSTI 가 발생하는 것입니다.
Reference
https://labs.watchtowr.com/expression-payloads-meet-mayhem-cve-2025-4427-and-cve-2025-4428/
https://projectdiscovery.io/blog/ivanti-remote-code-execution
https://www.wiz.io/blog/ivanti-epmm-rce-vulnerability-chain-cve-2025-4427-cve-2025-4428
https://xen0vas.github.io/Leveraging-the-SpEL-Injection-Vulnerability-to-get-RCE/#
https://www.hahwul.com/blog/2018/spel-injection-springboot-rce/
본 글은 CC BY-SA 4.0 라이선스로 배포됩니다. 공유 또는 변경 시 반드시 출처를 남겨주시기 바랍니다.