TeamCyprus ECSC Qualifiers CTF — WassApp Challenge Writeup

Andreas Pogiatzis
6 min readJul 11, 2018

--

Recently I participated in a CTF qualifier to join the Cypriot Team for European Cyber Security Competition. I really enjoyed the competition and the challenges were quite fun as well. This is my write up for challenge 14 which was also one of my favorites.

The description of the challenge was as follows:

WassApp is an End to End Encryption messaging service that uses ECDH key exchange between 2 friends to establish a secure connection and ensure that only the 2 friends can read the messages, even though the conversation takes place via an unsecure medium, the Internet.

Visit WassApp website at https://192.168.125.112 where your account is waiting. Unfortunately, someone has messed up the decryption algorithm. Your friend is trying to send you the flag but the data is encrypted. Decrypt the data and ask your friend for the flag until it’s given to you!

Okay that’s pretty straightforward so let’s dive go right into the hacking part!

Gathering information

By visiting the page provided, I got the following chat like setup:

First of all, hat’s off to the authors, firstly for coming up with a good design although it was just a CTF competition and secondly because concept was inspired from Silicon Valley series!

First thing that popped into my head was to check the network requests when interacting with the app. So I clicked on Nelson Bighetti( only one online ) to open a chat window, opened chrome dev tools and tried to send some stuff.

As expected I received an encrypted response which most likely included the flag. Observing the requests in dev tools more closely I noticed two interesting ones.

POST /api/handshake HTTP/1.1Request:{  
"with":"1842042",
"jwk":{
"crv":"P-256",
"ext":true,
"key_ops":[],
"kty":"EC",
"x":"mZSbHWKlhRmDHOmThoaa8plRNejzeavpEbPSEVGs7hI",
"y":"La3z3VfSxacl7iLKZEMNISFB_dkmLA-0yjlFjPBblos"
}
}
Response:{
"kty":"EC",
"crv":"P-256",
"x":"8-U6UxCRGJlnCLtB78R3JytBd9A4DKndobXGZDsfqwk",
"y":"Ph9OsNWDnriGECHioB7wpboFFIJY36gYcd3MnxDtsg0"
}
POST /api/reply HTTP/1.1Request:{
"to":"1842042",
"encMsg":{
"ciphertext":"1DjaZ6PsPysxerzK1u3xEg==",
"iv":"1zxHfnjMdyonoMdlAkm5Pg=="
}
}
Response:{
"iv":"x1IHO6KN6fYXOk9hlyto7w==",
"ciphertext":"AbwZFJtHVT1qGMzJ5JCwd+Gs2/gEAAXdsiMsLfBsYrkJvifw5pJToqSud0zyeYgB2s4fA4Nz0OJPbAioby36HnYJGF1eC0EsEtkOdr4cj3g="
}

Obviously the reply endpoint was for communicating the encrypted messages with the server but the handshake seemed to contain information that will be useful for decryption.

Examining further, I noticed they had a “We are hiring” link at the bottom of the page. This redirected me to a page listing the requirements of potential applicants.

Random pages like this always serve some purpose in these competitions.

In this case, a specific line caught my attention: “Experienced with Web Crypto API.”. Looking this up quickly, it turns out that it is an interface for using cryptographic primitives in scripts. It was very likely that this is the technology used for encrypting and potentially decrypting the message.

Keeping this in mind, the next step was to take a look in the source code. The most important part was a javascript file which was minified and obfuscated so it was rather difficult to read. I used the dev tools debugger to step in some of the code while sending and receiving a message and I found some interesting code snippets.

return new Promise(function(e, t) {
window.crypto.subtle.generateKey({
name: "ECDH",
namedCurve: "P-256"
}, !1, ["deriveKey", "deriveBits"]).then(function(t) {
e(t)
}).catch(function(e) {
t(e)
})
}
)
}
, i = function(e, t) {
return new Promise(function(n, r) {
window.crypto.subtle.deriveKey({
name: "ECDH",
namedCurve: "P-256",
public: t
}, e, {
name: "AES-CBC",
length: 256
}, !1, ["encrypt", "decrypt"]).then(function(e) {
n(e)
}).catch(function(e) {
r(e)
})
}
)
}
, l = function(e, t) {
var n = new TextEncoder
, r = n.encode(t)
, a = window.crypto.getRandomValues(new Uint8Array(16));
return new Promise(function(t, n) {
window.crypto.subtle.encrypt({
name: "AES-CBC",
iv: a
}, e, r).then(function(e) {
t({
ciphertext: o(e),
iv: o(a)
})
}).catch(function(e) {
n(e)
})
}
)
}

This snippet covered the whole procedure from key-generation to encryption. Checked in with the Web Crypto API saved from earlier and bingo! This is exactly what was used.

After browsing through the Web Crypto API documentation I realized that I had everything that I needed to decrypt the messages.

There were two options from here. Try to import the public key using the information from the handshake network request and use it with the appropriate private key which was somewhere in the codebase, OR get directly the secret key required for decryption from the function generating it!

Of course, I went with the quick and dirty way. Why bother re-deriving the keys, let’s just snag it from that function deriveKey. It returns a promise resolved with the key we need.

Getting the Key

For that task, I used a handy chrome plugin called Resource Override. This allows to setup regular expression patterns and replace any resources ( css, js, images etc..) that match, with the ones that you specify — even locally! Using that I was able to create a local version of the main javascript file that was used for crpyptography and apply persistent changes to it.

In order to access the Crypto Key I just made it to a global variable by adding the following line into the code.

       i = function(e, t) {
return new Promise(function(n, r) {
window.crypto.subtle.deriveKey({
name: "ECDH",
namedCurve: "P-256",
public: t
}, e, {
name: "AES-CBC",
length: 256
}, !1, ["encrypt", "decrypt"]).then(function(e) {
window.cryptoKey = e;
n(e)
}).catch(function(e) {
r(e)
})
})
},

I reloaded the page, made sure that the resource was replaced, crossed my fingers and hoped for the best!

Voila! We got the key! Alright now what? I needed to get the base64 ciphertext and initialization vector from the reply request convert it to an ArrayBuffer and decrypt it!

For that I used the code snippet below in the javascript console:

window.crypto.subtle.decrypt(
{
name: "AES-CBC",
iv: _base64ToArrayBuffer(<initialization_vector>)
},
window.cryptoKey,
_base64ToArrayBuffer(<data>)
).then((decrypted) => {
console.log(atob(_arrayBufferToBase64(decrypted)));
})

Note that I defined the _base64ToArrayBuffer(), and _arrayBufferToBase64() functions in the console before hand.

I sent my first message “Hey Bighead!”, executed the code below and bingo!

The response what decrypted successfully and printed crystal clear on the console!

I sent my next message “Give me the flag!” and that was it!

A success! We got the flag!

Conclusions

Overall, it was a really fun challenge with some interesting cryptography primitives being used in javascript which was something that I had never seen before!

Interestingly enough, after checking out the official write up from the author the key was stored in IndexedDB on the browser so I could get it from there! (facepalm). But yeah, having alternative out of the box solutions for un expected problems is always a good thing!

For the official write up and source code of the challenge visit the repo of the author below:

Till next time fellas! 🤘

--

--

Andreas Pogiatzis
Andreas Pogiatzis

Written by Andreas Pogiatzis

☰ PhD Candidate @ UoG ● Combining Cyber Security with Data Science ● Writing to Understand

No responses yet