Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@righettod
Created August 1, 2021 17:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save righettod/11895cd038dc59a6de42b568ae05feb5 to your computer and use it in GitHub Desktop.
Save righettod/11895cd038dc59a6de42b568ae05feb5 to your computer and use it in GitHub Desktop.
Method to try to decrease the exploitability/interest of the SSRF by design exposed by HTTP Signature in PSD2 STET usage context.
package eu.righettod;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.Arrays;
import java.util.Locale;
import java.util.Optional;
import java.util.regex.Pattern;
public class PSD2StetHelper {
public static void main(String[] args) throws Exception {
String[] testUrl = {
"https://test.com/myQsealCertificate_714f8154ec259ac40b8a9786c9908488b2582X68b17e865fede4636d726b709fX",
"https://test.com/myQsealCertificate_714f8154ec259ac40b8a9786c9908488b2582b68b17e865fede4636d726b709f?a=b",
"http://test.com/myQsealCertificate_714f8154ec259ac40b8a9786c9908488b2582b68b17e865fede4636d726b709f",
"https://test.com/myQsealCertificate_714f8154ec259ac40b8a9786c99",
"https://test.com/myQsealCertificate-714f8154ec259ac40b8a9786c9908488b2582b68b17e865fede4636d726b709f",
"https://gist.githubusercontent.com/righettod/4e230425c40e114579c53e59a1ca21b2/raw/58f7bf3730a33bb630412846239b341e8d8f2b18/myQsealCertificate_714f8154ec259ac40b8a9786c9908488b2582b68b17e865fede4636d726b709f"
};
Arrays.stream(testUrl).forEach(u -> System.out.printf("[%s] %s\n", isSafeUrl(u), u));
}
/**
* The PSD2 STET specification require to use HTTP Signature.
* <br>
* Section 3.5 of the document "Version 1.5.1 - Documentation Framework".
* <br>
* The problem is that, by design, the HTTP Signature specification is prone to blind SSRF.
* <br>
* The objective of this code is to try to decrease the "exploitability/interest" of this SSRF for an attacker.
* <br>
* URL example taken from the STET specification: https://path.to/myQsealCertificate_714f8154ec259ac40b8a9786c9908488b2582b68b17e865fede4636d726b709f
* <br>
* Require java 11+
* <br>
* Not external dependencies
*
* @param certificateUrl Url pointing to a Qualified Certificate (QSealC) encoded in PEM format and respecting the ETSI/TS119495 technical Specification .
* @return TRUE only if the url point to a Qualified Certificate in PEM format.
* @see "https://www.stet.eu/assets/files/PSD2/1-5-1-6/api-dsp2-stet-v1.5.1.6-part-1-framework.pdf"
* @see "https://portswigger.net/web-security/ssrf"
* @see "https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12"
* @see "https://openjdk.java.net/groups/net/httpclient/intro.html"
*/
private static boolean isSafeUrl(String certificateUrl) {
boolean isValid = false;
String userAgent = "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0";
try {
//1. Ensure that the URL end with the SHA-256 fingerprint encoded in HEX of the certificate like requested by STET
if (certificateUrl != null && certificateUrl.lastIndexOf("_") != -1) {
String digestPart = certificateUrl.substring(certificateUrl.lastIndexOf("_") + 1);
if (Pattern.matches("[0-9a-f]{64}", digestPart)) {
//2. Ensure that the URL is a valid url by creating a instance of the class URI
URI uri = URI.create(certificateUrl);
//3. Require usage of HTTPS and reject any url containing query parameters
if ("https".equalsIgnoreCase(uri.getScheme()) && uri.getQuery() == null) {
//4. Perform a HTTP HEAD request in order to get the content type of the remote resource
//and limit the interest to use the SSRF because to pass the check the url need to:
//- Cannot have query parameters
//- Use HTTPS protocol
//- End with a string having the format "_[0-9a-f]{64}"
//- Trigger the malicious action that the attacker want but with a HTTP HEAD without any redirect
HttpClient client = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NEVER).build();
HttpRequest request = HttpRequest.newBuilder()
.uri(uri)
.timeout(Duration.ofSeconds(10))
.method("HEAD", HttpRequest.BodyPublishers.noBody())
.header("User-Agent", userAgent).build();//To not remotely disclose the version of java used
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 200) {
//5. Ensure that the response content type is "text/plain"
Optional<String> contentType = response.headers().firstValue("Content-Type");
isValid = (contentType.isPresent() && contentType.get().trim().toLowerCase(Locale.ENGLISH).startsWith("text/plain"));
}
}
}
}
} catch (Exception e) {
//TODO: Log error correctly :)
e.printStackTrace();
//Override value - paranoid mode :)
isValid = false;
}
return isValid;
}
}
@righettod
Copy link
Author

Execution example:

image

$ java -version
openjdk version "12.0.2" 2019-07-16
OpenJDK Runtime Environment AdoptOpenJDK (build 12.0.2+10)
OpenJDK 64-Bit Server VM AdoptOpenJDK (build 12.0.2+10, mixed mode, sharing)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment