Critical Authentication Bypass in MStore API < 3.2.0
January 27, 2021
Hello world! This is my first write-up for an authentication bypass vulnerability I found in MStore API, potentially leading to complete account takeover, even for unauthenticated users.
My name is Vincent Datrier, I’m a Web developer recently turned mobile developer. In this blog, we’ll look over how I found out about this bug, how I approached InspireUI, the vendor, and how their response and fix was. Let’s go!
Report Summary
A critical authentication bypass was found in MStore API < 3.2.0, leading to potentially being able to take over any account on a Wordpress installation with MStore API enabled.
Severity: Critical
Background
During the lockdowns, I decided to use the down time to learn new skills, in order to expand my offering as a freelance developer. A project soon cropped up, to develop a mobile app for a local store, building upon their Woocommerce instance.
As a practical learner, I mostly manage to pick up new skills by directly applying them on a set project, with clear goals. This project was the perfect opportunity to start learning about cross-platform mobile development! I needed, however, a fairly ready-made project to tweak to the client’s requirements, if he wanted to get his app before 2025. I looked around online, to see whether there was such a thing as templates for mobile apps existed. To my surprise, there is!
Enter Fluxstore, a Flutter template for e-commerce:
This looked promising! I bought the template, and got to work. After a few initial frustrations learning Dart and Flutter, I came around and configured everything I needed to have a full-blown mobile shopping app!
Now was the time to finally configure all the social login good stuff:
// TODO: 3.Update Social Login Loginconst kLoginSetting = {"IsRequiredLogin": true,'showAppleLogin': true,'showFacebook': true,'showSMSLogin': true,'showGoogleLogin': true,"showPhoneNumberWhenRegister": false,"requirePhoneNumberWhenRegister": false,};
Simple enough: enable Sign In With Apple in XCode, then enable it in Firebase, and you’re off to the races! Except there was a slight issue: throughout testing, I signed up through Apple, cleared the app data then tried signing in again, only to be met with a cryptic error message asking me to delete my account and try again!
So, I started digging…
The Apple Sign In Flow
Logging in with Apple usually goes like this:
- You click the button “Sign in with Apple”;
- Apple asks you whether you want to share your data with the app;
- If you agree, you’re signed in.
Except when it doesn’t. One particular case is when you already signed in through Apple. The vendor knows, and mentions it in the support documents:
🐞Bug Login: revoke the account access on your iOS device if could not login on FluxStore, by opening the
Setting
on iOS in your iPhone and tap on your name at the top, pressPassword & Security
, then you could see a list of theApps that using Apple ID
, you candelete
any of them torevoke
access
Fair enough; I delete my account through the settings, try signing in again, it works! But it’s far from ideal; if I have to instruct every customer that they need to dig through their iPhone settings everytime they reinstall the app (or if their session expires somehow), I’ll have a pretty big support headache coming my way.
So, I decided to dig into the code responsible for logging in.
The template implements the apple_sign_in
Flutter package, and requests the e-mail
and full name from Apple, then generates a user or signs them in. If Apple authorizes
the sign-in, the resulting JWT is decoded and the e-mail and name are pulled from it.
However, if the token doesn’t contain the name and e-mail (which is the case for all subsequent logins, Apple only gives you this info on first login; you are supposed to save them in your database at this point):
else {return fail("Open up the Setting app in your iPhone and tap on your name at the top. Then press Password & Security, then Apps using Apple ID They listed all the apps there and you can delete your app to revoke access and try to run app again.");}}
Aha! So that’s why the subsequent logins fail!
So I set out to try and implement another Flutter package, called sign_in_with_apple
,
to replace this implementation. In the documentation, I see there’s an entire server-side
part to it. InspireUI provides a Wordpress plug-in adding many needed REST routes to
deal with the app.
The Server-Side Issue
I decide to dig into the Wordpress extension, which is downloaded from the Wordpress repository, and this is how the Apple Sign In is checked:
public function apple_login(){global $json_api;if (!$json_api->query->email) {$json_api->error("You must include a 'email' variable.");} else {$display_name = $json_api->query->display_name;$user_name = $json_api->query->user_name;$user_email = $json_api->query->email;$email_exists = email_exists($user_email);if ($email_exists) {$user = get_user_by('email', $user_email);$user_id = $user->ID;$user_name = $user->user_login;}if (!$user_id && $email_exists == false) {while (username_exists($user_name)) {$i++;$user_name = strtolower($user_name) . '.' . $i;}$random_password = wp_generate_password($length = 12, $include_standard_special_chars = false);$userdata = array('user_login' => $user_name,'user_email' => $user_email,'user_pass' => $random_password,'display_name' => $display_name,'first_name' => $display_name,'last_name' => "");$user_id = wp_insert_user($userdata);if ($user_id) $user_account = 'user registered.';} else {if ($user_id) $user_account = 'user logged in.';}$expiration = time() + apply_filters('auth_cookie_expiration', 120960000, $user_id, true);$cookie = wp_generate_auth_cookie($user_id, $expiration, 'logged_in');$response['msg'] = $user_account;$response['wp_user_id'] = $user_id;$response['cookie'] = $cookie;$response['user_login'] = $user_name;$response['user'] = $result;}return $response;}
The function above checks whether the e-mail exists in the database, and returns the user associated with it. If it doesn’t, it creates the user with a random password and returns the user.
At no point the JWT is checked, nor even a password is provided: if the e-mail exists, you get an authentication cookie.
Believing I missed a part of the flow, I fired up Postman, pointed it to the endpoint. And sure enough…
{"wp_user_id": 1,"cookie": "[REDACTED]","user_login": "vdatrier","user": {"id": 1,"username": "vdatrier","nicename": "vdatrier","email": "contact@webble.fr","url": "https://webble.fr","registered": "2020-12-17 16:05:46","displayname": "webble_admin","firstname": "Vincent","lastname": "Datrier","nickname": "webble_admin","description": "","capabilities": "","role": ["administrator"],"shipping": null,"billing": null,"avatar": "https://secure.gravatar.com/avatar/ad17e78df0c401630720d28224f2ddd2?s=96&d=mm&r=g","dokan_enable_selling": ""}}
The cookie
variable here contains a typical Wordpress cookie, which I could use
for any nefarious purposes. While I had to find an admin user’s email address first,
I could either enumerate users through the Wordpress REST API, or just order something
from the store and hope the sender’s email address that’s set by default by
Woocommerce would be one of an administrator.
Contacting the vendor
- January 26th, 2021: Vendor contacted through official channels
- January 26th, 2021: Vendor acknowledged the issue after asking for more details
- January 27th, 2021: Vendor posted a partial fix to the issue to the Wordpress plugin database
- January 28th, 2021: Vendor published MStore API 3.2.0 fixing the issue, with an updated Fluxstore version to fix the vulnerability
- February 2nd, 2021: Vulnerability report published by WPScan.com
Overall, InspireUI responded very fast to this issue, and provided a fix in record time.
Wrapping Up
This experience was a very positive one, from finding the issue to getting it fixed and reported through all the official channels. InspireUI was a delight to work with, and their speedy resolution was a refreshing sight to see!