Saturday, February 8, 2020

Starbucks Account Takeover, from MITM in the Instant App — New Scene New Challenges


It was another boring "hard working" afternoon. My sleepiness was fighting against the boooooring smali code. I opened the takeaways app and wanted to order a cup of coffee.

When I tapped the Starbucks Delivers item in the app, an instant app  popped up. Although various instant apps embed in various apps are common in China, it's the first time I see the instant app in this takeaways app. I think that is not a public interface for ordinary developers, it's for contractors and business partners.

Business Logic

When I used it, I found an interesting business logic. At first, you should bind your original Starbucks Delivers account to use your membership benefits and preferences. If your Starbucks account and the takeaways app use the same phone number, you can just bind them with a tap for auto-populate in the takeaways app, without any other authentication. The implementation of this logic is a issue worthy of consideration.

I analyzed the data transmission in the process above. And get the following authentication process.

TAA : the takeaways app
SD : Starbucks Delivers

My first thought is if there is a lateral movement at the decision process. That's I use a same phone number to pass the decision but change it at the process of binding account. Unfortunately after capturing packet, I got a detailed authentication process s, showing lateral movement is impossible at this point.

Authentication Process

Processes "auto-populate" and "input number & send sms" use the same server-end API of the takeaways app, /membership/istore/sendVerificationCode .  If the phone number in the request body is same as the number of the takeaways app account, a token will be returned directly. Otherwise it will return nothing but send a sms to the phone. You should call another API of the takeaways app, /membership/istore/bindMember, to verify the code and then get the token.

The "token" above is a key field in the whole authorization process. Getting the "token" from ://restapi.TAA.domain is the beginning of the authorization. Then it will call the api
://restapi.TAA.domain/membership/istore/queryEncryptAuthInfo to
encrypt more info about the account, but I think the cipher returned is reserved and not used in the following. Next, enter the stage of Starbucks domains. It will open a webview in the instant app and access https://3pph5.starbucks.com.cn/api_path/?token=xxxx&params=xxxx&returnUrl=/pages/join-member-redirect/join-member-redirect

There are three params in the GET request, token is the token above, params is the cipher authinfo returned above and the returnUrl is a location path in the instant app. The url is a pure static html page. It will load a complex file named main.version_code.js to handle APIs and communicate with instant app framework. And from that html page, it will call the APIs of https://3ppapi.starbucks.com.cn .

Call https://3ppapi.starbucks.com.cn/external/h5/member/verifyByMobile 

The token will be filled in Authorization bearer token. And another token will be returned, decoded as a JWT:

Next, I use the JWT as a parameter to call API https://3ppapi.starbucks.com.cn/external/h5/member/verifyBind3PP

That will return two fields. Field "sbuxid" is a unique identifier for Starbucks accounts ( but I am not sure its suitable range ). Another field is also named token. But I have no idea about what it means. Just ignore it. It's nothing in the rest of the process.

The above is what happened in the embed webview and Starbucks  domains. And we can see, that's a variant JWT Authorization process. The first token got from TAA API is just like a session id. It's used to get JWT from Starbucks  API. Next, we get the sbuxid and return it to the outside instant app.

Finally, call the api https://restapi.TAA.domain/membership/istore/bindMember

The parameter "partnerMemberId" in the request body is the field "sbuxid" above. According to this field, the Starbucks  account will be bound to current TAA account directly.

MITM

When I audited the traffic, I found there is a http redirection at the stage of loading Starbucks  html pages.

When the instant app starts to loading Starbucks  domain, the first request is based on https. We can’t decrypt it even in the MITM. But the response contains a 301 redirection.




That's obviously an issue of route adaptation. It leaks the token in the MITM environment. So we can use it to complete the rest of the authorization process. But in my practice, there are some problems need to solve.

In the first test, as soon as I catch the token in the http request, I will block the subsequent requests by the victim. But when I finally access the bindMember API to bind the victim's sbuxid with attacker's TAA account , the API returns a system error with ambiguous comments.

More details about implementation

After auditing every API request, I found it's a error about duplicate binding. When you call the API https://3ppapi.starbucks.com.cn/external/h5/member/verifyBind3PP with the JWT, the Starbucks  account will be bound with the original TAA account directly. I think the original TAA account info is saved in the JWT, maybe the field customerId or uuid. So that's a bidirectional binding process. To get the sbuxid, you must bind the original TAA account in the Starbucks server at first. And then when you call the API to bind Starbucks  account in the TAA server, TAA back-end will communicate with Starbucks  server and bind them. If the current TAA account is not same as the original one bound with previously, API will return a error.

The whole process is maybe like the following.

Because the information about binding is saved in JWT which cant be tampered, we must unbind the original member before binding attacker's TAA account. It's possible in the webview and MITM environment. Go back to the stage of MITM, the request to http url will also return a 301 redirection. We can redirect to our website and use CORS ajax to complete /membership/istore/bindMember and /membership/istore/unbindMember API call, Just like normal CSRF attack. But there is a basic problem that requires attention: Content Type should be set to application/x-www-form-urlencoded and request body should be converted to corresponding format like that:

request="{\"uniCrmId\":2634283647,\"brandCode\":2634283647184908,\"extParam\":{…}}"

Otherwise, OPTIONS preflight request will check failed.

Finally, call bindMember API with attack’s TAA account and use the `sbuxid` as the field ` partnerMemberId ` to bind them together.

Ending

As we can see in instant apps, the implementation of  the business logic usually involves multi-party authentication and data sync.This is a new and more complex multi-party Interaction scenario. The servers and clients communicate, overlap and and even embed with each other.But most of the protocols are non-standard and the implementation is in the black box without effective audits. The convenience maybe cost the complete authentication elements. 

There are new challenges in the new mobile app scenarioWe need to explore more potential attack surface.

Responsible Disclosure And More

I have reported this bug to Starbucks  at hackerone. But it's out of scope because of MITM. So I disclose it as an example for Introducing the attack scenario in the instant apps.

And you can learn more about this new scenario in my talk at BlackHat Asia 2020. The inside story: there are apps in apps and here is how to break them. It's about compromising instant app frameworks instead of a specific instant app.

No comments:

Post a Comment