How I Hacked Into One of the Most Popular Dating Websites

A story of poor backend security in midst of scandals and new regulations.

Even though they promote smart dating by using science and machine learning, their website was so easy to hack into in 15 minutes.

Will Smith being friend-zoned by the robot Sophia. So much for Hitch.

Disclaimer:

I am not a fan of online dating, nor do I have any online dating apps installed on my devices. I have tried few of the most famous online dating apps and they did not appeal to me. I love approaching people anywhere and saying Hi.

So why did I sign up for this one?

They promoted it in the underground as a dating website based on science. That really intrigued me into seeing how this works.

You’d register, answer tens of questions about yourself, then they’d show you some matches with blurred photos, telling you that they have something like 95% compatibility with you. Without paying for full membership, you’ll only be able to look at how compatible you are, smile at people, and send pre-defined ice-breaking messages such as “If you are famous, who would you be?” or “If you had one last day in your life, what would you do?”. If they did reply, you wouldn’t know what they replied or be able to send a personal message unless if you pay.

This dating website charges more than £50 per month to be able to see photos and to message people. That surely is because they are providing such smart service.

DeveloperHub.io

Tonight while working on my startup DeveloperHub.io — A service to create your own beautiful product documentation, API reference, user guides in hosted developer hubs (portals) — I got a message from someone with 100% compatibility as the dating website claims, so I was highly intrigued to know who she was.

The dating website does not even allow you to read the message. So I thought: Hmm, let’s see how smart these “smart” people are.

If you are not a technical person, jump to Moral of the Story below.

Let the Reverse Engineering Begin

I thought, first thing I can do is to see the network traffic coming in and out of the app. I am using the app on my iPhone. So I installed a proxy on my Mac, Charles, and ran the iPhone’s WiFi through that proxy.

Profile

Well I can see the profile and every detail she has entered about herself. Kinda creepy, but okay, anyway this kind of shows on the application. But wait, did they just send the girl’s full profile over non-secure HTTP? Hmm…

There is a list of blurry photos, but I couldn’t get access to the non-blurred photos easily. No problem, will leave it for later.

All important requests seem to be happening on SSL. I activated Charles SSL Proxy, and installed Charles SSL certificate on my iPhone but that just didn’t work, and the app could not connect anymore. Seems that they did a good job here in knowing that I am not using the proper SSL certificates and that I am performing a man in the middle attack.

SSL all over

Web Application

I said, well if the iOS application is a bit hard to hack, let’s try the web application. I head over to their website and logged on. I could almost see the same interface, same blurred faces, same inbox which I cannot read.

On Chrome it is pretty easy to read the HTTPS requests, and so I did. Filtered Network tab to XHR, and looked at the GET requests and voila… Here is the inbox chat message I just received!

Inbox Message
[
{
"messageId": "b123738-5123-4123-9123-1232333b1234",
"type": "CHAT",
"value": "Hi Zed! I feel like I should send an interesting message but I'm all Mondayed out. How are you?",
"createdTimeStamp": 1523914585468,
"readTimeStamp": 1523914778123,
"sender": false
},
{
"messageId": "ABC1235C-AABC-4ABC-8ABC-1ABC4EBC7ABC",
"type": "SMILE",
"createdTimeStamp": 1523883156123,
"readTimeStamp": 1523886591123,
"sender": true
}
]

Ha! That was easy.

Okay, well cool, but still I cannot pinpoint who this person is, nor reply back. Since we got this far, probably we can go even farther.

At this point — I started writing this Medium post because I realised that their security does not seem to be marvellous.

Sending a Message — Will It Work?

If I need to send a message, then the first thing I’d have to do is to see how does sending a message look like. So I switched to any other person there is on my match list, clicked on the button to send a pre-defined message, selected one of them “If you are famous, who would you be?”, and sent it out.

Meanwhile I was preserving the log of Chrome Network Requests.

Okay, looking over the PUT and POST requests that we just created, I cannot find the word “famous” anywhere. Is it that the word does not get sent, or is there something else going on?

In one of the POST requests that happened after I sent the message, the payload was:

{
"logs": [
{
"logMessage": "Message Sent (Soft ACK) - on server sender",
"method": "WEBSOCKET",
"logLevel": "INFO",
"additionalInfo": "{\"messageId\":\"12351f23-fABC-4ABC-9ABC-ABCc123a0ABC\",\"matchId\":12309078132}"
}
]
}

Websocket. Oh Damn, the chat is happening over websockets (I should’ve expected that). Let’s see what the websocket is doing.

Websocket Inspection

Moving over to websocket filtering in Chrome Network tab, gladly there was only one websocket to monitor.

Websocket frames

Okay let’s do the simplest thing, filter by word “famous”.

Damn, “famous” also does not exist in the websocket. Looping over the messages trying to understand the XML being sent (who the hell uses XML these days for websocket communication?), it looks like that it is:

  • Opening a connection
  • Authentication to the websocket service
  • Connecting to a Jabber client and setting some configuration here and there
  • Then sending a message!

<message xmlns=”jabber:client” to=”123jnwrvd7_123gd2abcv12_12@chat.xyz.com” id=”84123ff6-f123-4123-9123-c123458a0abc" type=”chat”><body>{“message”:{“messageId”:”84123ff6-f123-4123-9123-c123458a0abc",”type”:”CEQ”,”value”:”62"}}</body><request xmlns=”urn:xmpp:receipts”/><data><accesstoken>84123ff6-f123-4123-9123-c123458a0abc</accesstoken><header name=”User-Agent” value=”Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36"/><header name=”X-xyz-gdid”/><resourceid>12309078132</resourceid></data></message>

Awesome, now since I understand how the sending works, let’s try to replicate this.

Simple Websocket Client

I installed Simple Websocket Client Chrome extension, copied the websocket URL, opened a websocket connection and that was a success. Next steps:

  • Okay, let’s do the first request to open a connection. Good.
  • Authenticate. Good.
  • Connect to the Jabber client and set those settings. Good.

Hah, that’s easy. Okay, how do we send a message now to this match.

Looking at the JSON payload, it seems that there is a message object, and then the pre-defined message has an ID and we’re sending that. I decided to try to set the message key to the value “Hey there!”, just to try it out.

{“message”:"Hey There!"}

Aaah, error.

<error code=’400' type=’modify’><bad-request xmlns=’urn:ietf:params:xml:ns:xmpp-stanzas’/><text xmlns=’urn:ietf:params:xml:ns:xmpp-stanzas’>Can not instantiate value of type [simple type, class com.xyz.services.comm.api.message.Message] from String value (&apos;Hey there!&apos;); no single-String constructor/factory method
 at [Source: org.glassfish.jersey.message.internal.ReaderInterceptorExecutor$UnCloseableInputStream@b5a4c95; line: 1, column: 2] (through reference chain: com.xyz.services.comm.api.message.ClientMessageWrapper[&quot;message&quot;])</text></error></message>

Hmm, interesting. It is not expecting such schema. Let’s have another look. I am sending the pre-defined message ID, so the ID must exist somewhere. Let’s have a look at the list of pre-defined messages.

I opened the list to send more messages and I inspected the HTML and it turns out that that message has the ID 62.

Ah okay I see where I went wrong, messageId is some other ID, while the value is 62 for the pre-defined message. What about type “CEQ”, what should I set that to?

I remembered that while looking through the GET requests, I saw such a thing. Here it is:

Types of Actions

I see what to do now, just set the type to CHAT, and the value to my chat message. Let’s try it out.

Response:

<message xmlns=’jabber:client’ from=’1231234x95rjy-y27r7c8tjky@chat.xyz.com/android.phone.emulator’ to=’1231232yr2_3–6sgyt-c612337t@chat.xyz.com/android.phone.emulator’ xml:lang=’en’ id=’123f7–32' type=’chat’><data/><received xmlns=’urn:xmpp:receipts’ id=’81231236-f5ce-abcd-9abc-c6e12312312c0'/></message>

OH YEAH, I GOT A RECEIPT. Refresh the inbox page, and voila we have a message written.

Talk to my Match

Last piece of the puzzle is to know how to talk to anyone on this website, rather than just to that person. There does not seem to be any identifier to the person I am chatting with except in the message websocket frame. It seems that the chat address that looks like an e-mail address is the identifier of the person I am sending to.

to=”123jnwrvd7_123gd2abcv12_12@chat.xyz.com”

Where do I get this identifier from.

Copy the extended profile information to Sublime Text. Find the chat address in text. Ah, it is the encrypted user ID. Okay, let’s try that.

{“message”:{“messageId”:”84123ff6-f123-4123-123b-c6123e8a1230",”type”:”CHAT”,”value”:”Good evening Sophie! Haha already tired? Interesting messages are over-rated anyway 😜”}}

Well that was a fail, I sent it to the same girl that I tested on. Hah. Shouldn’t have added the name, it’ll look super weird now… Weeeeellll. Let’s try again.

After a long look at all these IDs and chat addresses, it turns out it is the resource ID:

<resourceid>12309078132</resourceid>

Trial number 2:

Find what that resource ID is. Okay that’s the user ID that’s not encrypted. Easy peasy. Edit the resource ID, and voila. We have a message sent to the cutie!

Why Stop Here

I started thinking, well this is getting fun. How about we try to see those blurred photos now. In the profile JSON array, there is a list of photos, and the URLs look as such:

https://images.xyz.com/photos/v2/photo/NORMAL/I1/d5abcttnp5yxjytb227v6fp56p.jpg?blur=60&crop=faces&fit=crop&g=2&h=160&ixlib=java-1.1.1&w=160&s=cda2e652b4182b123a1f5f6781daa36a

I tried to modify the query parameters, but I always got an empty image.

I was thinking, maybe if I have a paid account, then I can see how can I map the blurred images to the original images. Well I’m not really gonna do that.

So what can we do?

💡💡💡💡💡💡💡💡💡

Well just check my own profile picture, what does the URL consist of?

In fact I did:

https://www.xyz.com/photos/v1/photo/THUMB/I3/1236VKj18jtm5Ih8Cr2pSAabc.jpg

What are those parameters? Let’s pretty print my profile at jsonprettyprint.com, copy to Sublime Text, and make a search. It turns out that the website has a way of numerating the images I1, I2 , I3 , and so on, that long identifier is my encryptedUserId, photos have versions (1 or 2), and sizes are defined by THUMB, ICON, NORMAL, and so on. So:

https://www.xyz.com/photos/v<version>/photo/<SIZE>/<IMAGE-NUMBER>/<ENCRYPTED-USER-ID>.jpg

That’s easy, let’s apply that to another match’s image. And voila, we’ve got the image there.

Tried to paint over her face to cover her features, then I realised that’s creepy. So I just added an emoticon. You can thank me later!

Is this service as insecure as I think it is?

Okay let’s check how insecure this is. Can we read other people’s profiles without even being matched to them? Let’s see.

To get somebody’s profile, the HTTP GET request is as such:

https://www.xyz.com/publicapi/v2/matchprofile/<match-id>/profile

If we copy the CURL from Chrome’s network, we get such nonsense:

curl 'https://www.xyz.com/publicapi/v2/matchprofile/12303942525/profile?' -H 'authorization: Bearer 12339f23-2302-4e6f-b9ae-1f9c99a6e123' -H 'Accept-Encoding: gzip, deflate, br' -H 'Accept-Language: en-US,en;q=0.9,ar;q=0.8' -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36' -H 'x-xyz-gdid: ' -H 'accept: application/json' -H 'Connection: keep-alive' -H 'content-type: application/json' -H 'x-xyz-platform: desktop' --compressed

Alright, let’s change one number of the match ID, and see if we can get data.

404 Not Found.

Try again with different numbers: 404 Not Found.

That’s good. Can we get those profiles though using a user ID? I cannot see how we can do that now. No problem. I won’t waste more time on this, my point’s proven.


Moral of the Story

I am not a hacker, nor do I want to cause damage. I just understand how web services work. The reverse engineering I just did is 99% done on Chrome without the need of any other tools. Gaining full membership features to a service that charges so highly was so easy as most of the security was done at the frontend, not the backend. It is a high-walled castle with an open gate and no guards inside it.

My idiom for their security

Recommendations to their engineers (if they cared):

  • For God’s sake use only secure connections. Sending someone’s profile over HTTP is unacceptable.
  • Set up permissions in your Jabber messaging system. Check when a message is being sent having CHAT action that the user sending is actually a premium member.
  • Filter out your messages endpoint (which shows the conversations) to only show messages if the user is a premium member.
  • Do not serve images without checking membership. You can re-route your web server (Jetty) when an image is requested, check membership, and serve it blurred if the user is not a premium user, or normal if the user is.
  • And most importantly, build your security inside out!

Your membership could easily be replaced by a Chrome extension that replaces URLs for photos, replaces HTML of the inbox to match what you get in the requests, and send out messages using your websocket.

Why Does All of This Matter?

Following Facebook’s scandal, I would recommend every company to hire some ethical hackers to understand where your service is insecure. Your privacy policy states that you have extensive security measures including the use of SSL, that you’ll exercise reasonable care in providing secure transmission of information, but you also state that you accept no liability of any unintentional disclosure of information.

We are at an age where data collection is technically easy for companies, and the users are willing to foolishly and unhesitantly give out their data, unaware of the vague privacy policies behind them. The amount of data you gather around users is huge, and you are very responsible for this. If you are unable to protect this data, then do not collect it.

The General Data Protection Regulation (GDPR) is coming on the 25th of May 2018. When the regulation comes into place, you better not send any European’s profile on an insecure layer as you might be fined €20 million or 4% of your global turnover, whichever is higher.

Update (25th of May 18): Hello GDPR!

My Hopes

With GDPR, I am hoping that your awareness about the amount of data services collect about you will be greater. With the greater awareness, people will start to hesitate to supply information about themselves that may be unnecessary for the services to work, and companies will be forced to be more transparent about how they are using the data.

Remember that with GDPR, you can request a copy of your data in human readable format from any service provider, and that this request must be fulfilled in 72 hours.

Once news about companies being fined start to come out, companies will start employing practices to secure their systems. After all, it seems that only 27% of business thought that GDPR applies to their business, and half of UK’s businesses know about GDPR.

For the rest of you, US citizens, sorry but your government seems to be outdated for today’s technology.

Note: I blurred out and replaced all occurrences of the website’s name and all identifying information about the members in respect for them. This post is not targeted at the website or is intended to cause them any harm.

DeveloperHub.io

If you liked this story, 👏 👏 👏 for it below!