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!