tl;dr
Generating licenses using MD5(email)
is bad.
Validating licenses using HTTP in plain text is bad.
The Software
This post covers reverse engineering and cracking the license validation process of a popular and closed source self hosted music server application. The developer has been notified about the findings, however no reply has been received. The validation issues still exist as of now and affects all issued licenses. Because of this, the name of the application won’t be disclosed.
Source Code
The server application consists of a software package written in java. This means that decompilation can easily be performed using jd-gui
or similar tools. Afterwards simple grep
ing for license
yields these interesting methods:
void validateLicense()
boolean isLicenseValid(String email, String license)
Further analysis shows that licensed are validated in two steps:
- Local checks using
isLicenseValid(String email, String license)
- Server side checks using `validateLicense()``
Local Checks
After digging into the source code, this code snippet has been identified to be responsible for local license checks:
public boolean isLicenseValid(String email, String license) {
if ((email == null) || (license == null)) {
return false;
}
return license.equalsIgnoreCase(StringUtil.md5Hex(email.toLowerCase()));
}
That’s right: All licensed issued are basically MD5(email)
. Bypassing this check is done by generating a MD5 hash of a random email address. This also means that you can hijack another persons license only by knowing his/her email address.
Remote Checks
Bypassing local checks is easily possible - not let’s have a look at the API call which triggers server side checks:
private void validateLicense() {
String email = getLicenseEmail();
Date date = getLicenseDate();
if ((email == null) || (date == null)) {
licenseValidated = false;
return;
}
try
{
String content = HttpUtil.httpGet("http://<domain>/<endpoint>?email=" + StringUtil.urlEncode(email) + "&date=" + date
.getTime() + "&version=" + versionService.getLocalVersion(), 120, 120);
licenseValidated = ((content != null) && (!content.contains("false")));
if (!licenseValidated) {
LOG.warn("License key is not valid.");
}
String[] lines = StringUtils.split(content);
if (lines.length > 1) {
licenseExpires = new Date(Long.parseLong(lines[1]));
}
}
catch (Throwable x) {
LOG.warn("Failed to validate license.", x);
}
}
To bypass the second validation stage, one could perform an easy attack: Redirecting domain
locally to an attacker provided server which always returns the string true
. However, another approach seemed to require even less work.
By spawning a docker container with the name domain
and linking it to the application server, this check seemed to be bypassed. This step basically redirects domain
to a non existing server which tricks the validation process into being successfull all the time. Most likely this is intended behaviour because people don’t need an internet connection to validate licenses.