What is BeReal?
BeReal is a new social media app which is said to be more “in the moment” compared to conventional social media platforms. The premise of the app is that once per day, everyone gets a notification (at the same time) saying “It’s time to Be Real!” Everyone then has two minutes to take a picture of what they’re doing at that moment. The phone simultaneously snaps a front and back camera photo, before uploading it to their feed for all (friends) to see. There is no option to choose existing photos to upload or make edits to photos. You must post as you are, in the moment.
I wanted to see how much I could manipulate BeReal with the use of a software called Mitmproxy between my iPhone and my Macbook. This software allows me to see all unencrypted HTTPS requests made between my phone and the internet. With this tool, I have the ability to view, pause, edit and cancel any requests at my will. This software also has a Python API for writing custom scripts which I will touch on later.
Notable domains
I spent some time using the app and learning which endpoints were associated with which functions. With this information, I am able to build a profile of how this app works. I knew already that BeReal was built on top of Firebase but there seem to be an additional endpoint to handle certain requests.
- firebasestorage.googleapis.com - Firebase cloud storage, used to upload files (images).
- us-central1-alexisbarreyat-bereal.cloudfunctions.net - Firebase cloud function endpoint, used to create posts, delete posts, set post captions, send reactions, etc.
- mobile.bereal.com - Secondary endpoint - Used to get posts feed, view friends, add friends.
Hijacking authentication tokens
The requests made from the phone are authenticated using a (JSON Web Token) JWT which is sent in the “Authorization” header of each request. this header expires after a few minutes, however until then, I can use this token to do whatever I like as an authenticated user.
BeReal’s most commonly fetched endpoint is mobile.bereal.com/api/feeds/friends which gets the list of friends’ posts on a user’s feed. If I call this endpoint from Postman without the authorization header, I instantly get a 403 - Forbidden
status code which is expected, as I have no credentials to say who I am.
On the Mitmproxy feed, I can just copy and paste the “Authorization” JWT included in any of the BeReal API requests and use that for myself. Upon including this header in Postman, I am allowed through the security and I get greeted with a nice JSON of all of my friends’ posts with links to all of their Images, location data, how many retakes they took, etc.
I can use this same approach to set the caption of my BeReal post as many times as I like. Normally, you are only allowed to set it one time and then it is permanent; however, by making a POST
request to the /setCaptionPost endpoint I can bypass this rule and set my caption over and over again at my leisure.
Custom RealMoji
To show appreciation in BeReal, instead of sending a “Like”, you send a “RealMoji” which is an image of your face as a reaction. This adds a level of personality to a reaction which is normally missed. The fact that you can retake RealMojis as much as you like makes it a good opportunity to insert custom images as reactions as we are not only limited to one per day, unlike the BeReal post. Let’s start by looking at what is happening here when we send a RealMoji.
A POST
request is made to the firebase storage endpoint which initiates a file upload request.
A PUT
request is made to firebase, using the upload ID returned from the previous request to upload the image. The actual image data is sent in the body of this request.
A final POST request is sent to a cloud function with details of the reaction to be sent (Image storage location, reaction type, etc). Atotal of three requests are necessary to send a RealMoji reaction.
Manipulating expected content size
Without fully understanding the workings of Firebase file uploads, I started by simply modifying the data of the PUT
request, as that is when the image content is sent to the server.
I set a filter in Mitmproxy to pause the request as it comes through and then modified the body of the request to contain the data of my chosen Jpeg
I was then met with a 400 status error, stating that the size of the content did not match what it was expecting. Hmm…
I searched for this number 45243 and found it defined on the prior request. For my second attempt, I also modified this POST
request in order to match the content size of the file to be uploaded.
After doing this, I received a 200 status on both requests. Success. My manipulated payload has been uploaded! I then refreshed the app to see my “FakeMoji” in all its glory.
Custom BeReal Post
The flow of uploading a BeReal and sending a RealMoji is mostly identical. After working out how to manipulate the firebase upload, creating a custom post was basically no different.
Automating
Mitmproxy also has a Python API which is great for manipulating HTTP requests automatically. Based on the process above, I wrote a script which automates the process of intercepting the request and replacing the data.
import json
import os
import mitmproxy.http
BASE_PATH = os.path.dirname(os.path.abspath(__file__))
FAKEMOJI = os.path.join(BASE_PATH, 'fakemoji.jpg')
def read_content(file_path):
with open(file_path, 'rb') as f:
data = f.read()
f.close()
return data
class Interceptor:
def __init__(self):
self.upload_type = None
# http request trigger
def request(self, flow: mitmproxy.http.HTTPFlow):
method = flow.request.method
url = flow.request.url
# Firebase upload domain
if ("firebasestorage.googleapis.com" in url):
# handle upload request (POST)
if method == "POST":
# content body
json_body = json.loads(flow.request.content)
# record upload type (necessary for following PUT request)
self.upload_type = json_body["metadata"]["type"]
# replace content length definition
flow.request.headers["x-goog-upload-content-length"] = os.stat(FAKEMOJI).st_size
# handle sending file content (PUT)
elif method == "PUT":
# only replace image if realmoji upload
if (self.upload_type == "realmoji"):
# replace file content
flow.request.content = read_content(FAKEMOJI)
addons = [
Interceptor()
]
The script listens out for Firebase requests with a metadata type of “RealMoji” and then intercepts and manipulates them accordingly. The expected content size for the upload is altered and the content of the PUT
request is automatically replaced with the data of a local file: fakemoji.jpg.
How to protect yourself from a MITM attack
If you use a company-managed phone which has custom certificates installed, there is a chance that they can see you making these sorts of requests.
In reality, performing this man-in-the-middle attack on someone else’s phone maliciously would be easier said than done. To get it working for me required configuration of the iPhone’s proxy settings as well as approval of certain device certificates which would not be practical for an attacker to do remotely; however, with a compromised device, something like this would be more than possible.
If you use a company-managed phone which has custom certificates installed, there is a chance that they can see you making these sorts of requests. Obviously, this can’t be said for all companies but it can be hard to tell how “big brother” they are and how much of a grasp they have on your data and internet usage.
Use a VPN.
In order to prevent such attacks, you can try to protect your physical device from malicious fingers and for an added layer of security, use a VPN! (Shameless referral link here. Get 30 days free!). Yes, VPNs are not perfect, and 90% of the time they are advertised to the general public incorrectly but for this instance, they are effective at masking your actions from prying network eyes.
When I turn my VPN on and then send a RealMoji again, nothing is detected by Mitmproxy. I would include a screenshot to example but there is literally nothing to see. The requests which were shown before simply do not appear, as if the phone is not doing anything at all.
Closing thoughts
This fun experiment shows how you can manipulate your own device HTTP traffic and see what requests apps are making behind the curtain. It is just another proof of how you can never trust the front end, even from a native app. As a developer, in order to have real security in place, you must install effective security measures on your back end to prevent misuse; otherwise, your production app is not much better than a sandbox environment!
Top comments (21)
Learnt a lot, thank you :) I used mitmproxy to try this out for myself, however it seems like it complains about it already being finalised..?
This is the request:
Is it trying to cancel the upload?
Keep in mind that the "upload command" for the requests that succeeded was to upload and finalize.
Do you have any ideas? :D
Awesome! That looks to me like you are trying to resend a request which has already been sent. You need to make sure you intercept it before it has a chance to reach the server. You can do this by adding the domain to the "intercept" input.
Once it is paused you can edit the payload and press the green play button to execute the request. Let me know how you get on!
That would be the app trying to retry. You can see in the image below that the first PUT request succeeds but then it tries again and fails with HTTP 400 "Upload has already been finalized" as seen in my first comment.
But if you add the domain into the intercept filter then both of those requests should have been paused. I noticed that the app seems to try every request twice. You only need to let one of them through after editing.
That way doesn't work for me. It keeps re-requesting a new upload (POST)
But either way those requests should also be paused, no?
Yes they are paused but the photo does not go through on the client and it is probably expecting different upload IDs
Here you can see the modified PUT request returns a success but the client requests to start another 3 uploads, 2 of them being the one just returned as a success.
That
PUT
request looks ok to me actually. Looks like a decent amount of data was sent and it was a200
status. If you close and open the app now, is your custom RealMoji there?Nope. I’m trying to send a BeReal photo though, but you said it acts relatively the same. If I close and open the app, it just tries to upload again.
Ah but if you post a normal BeReal then you upload two pictures (front and back) so the app is making two consecutive requests followed by a bereal API request to say where the photos are stored. Have a play around with sending a RealMoji and see if you can get that to work!
Ah yeah, a RealMoji worked with the script. However both manually and automatically I can't upload a fake normal BeReal. What am I getting wrong?
Okay, so:
"This software allows me to see all unencrypted HTTPS requests made between my phone and the internet"
So this means that BeReal is NOT consistently using HTTPS/SSL for everything it does, and if so, why not? As it's one of the most basic 'rules' out there ...
And if they did, wouldn't that make MITM right away impossible?
Or am I completely missing something here?
P.S. but I suppose that that's exactly what the MITM proxy (which obviously needs to be installed in the phone itself) does, somehow: insert itself into the "network stack" BEFORE the beginning of the HTTPS/SSL endpoint? Obviously the BeReal devs do use HTTPS for their stuff ... cool article!
That is correct, the proxying of the connection via mitmproxy results in the HTTPS connection only being established from the intercepting computer!
I think your question is irrelevant, since it's the ability to "fake" their API invocations. As long as you can create something that mimics the client, you can pretend to be a client, while you're really not. Since the BL is implemented mostly on the client for simplicity reasons, this implies you can bypass their business logic, right ...?
For instance, if we imagine something similar for Facebook ...
With stuff that allows you to dynamically build CRUD queries on the client, such as Firebase, GraphQL, and PostgREST, the above becomes a legitimate concern, and although I realise it is possible to fix and secure, by using "schemas" or "resolvers", and in addition adding "trigger functions" (or whatever) - At this point you've lost most of the simplicity these things gives you, or ...?
Am I wrong here or ...?
@oscar ...?
Yes my question was based on a misunderstanding ... and we agree that if an app has its business logic on the client, it's vulnerable to these kinds of attacks and to abuse - the reason why BeReal gets away with it is because nobody's going to jump through hoops and put in that much effort to fake their actions in the app, it's just too trivial, so the incentive isn't really there :)
Thx mate. Just checking. I'm not always correct, so it helps to confirm my assumptions every now and then 😊
Awesome post! Very interesting. I did not know it was possible to intercept and modify http requests, great to know :D
So am I right that a BeReal user could (if they wanted) install this proxy on their phone themselves and then go on and manipulate how the app works for them? Just goes to show how fragile and unsafe it is to put "business" logic in the frontend ... but well, I'm sure that the makers of BeReal don't really care, they're probably laughing all the way to the bank :)
I enjoyed reading your article and wanted to try it out, but unfortunately, I encountered an error message: "Client TLS handshake failed. The client does not trust the proxy's certificate for cdn.bereal.network. OpenSSL Error: ('SSL routines', '', 'sslv3 alert certificate unknown')."