ByteCTF 2021 bytecert Writeup

证书校验

@Override  // javax.net.ssl.X509TrustManager
public void checkServerTrusted(X509Certificate[] arg4, String arg5) {
    int v1 = 0;
    try {
        while (v1 < arg4.length) {
            arg4[v1].checkValidity();
            ++v1;
        }

        String v4 = arg4[0].getSubjectDN().getName();
        Log.d("BYTECERT", v4);
        if ((v4.contains("bytedance.com")) && (v4.contains("字节跳动"))) {
            return;
        }
    } catch (Exception unused_ex) {
    }

    throw new CertificateException("Bad Cert");
}

checkServerTrusted 检查证书时间是否有效,以及证书 subject 是否包含 bytedance.com字节跳动,但是后续没有 CA 校验。

域名校验

@Override  // javax.net.ssl.HostnameVerifier
public boolean verify(String arg2, SSLSession arg3) {
    return b.a.verify("bytedance.com", arg3);
}

HostnameVerifier 检查证书 dNSName 是否包含 bytedance.com

伪造证书

使用 OpenSSL 构造满足上述要求的 X.509 证书即可。

openssl req -x509 -nodes -days 730 -newkey rsa:2048 -keyout server-key.pem -out server-cert.pem -utf8 -config <(
    cat <<-EOF
    [req]
    default_bits  = 2048
    distinguished_name = dn
    req_extensions = req_ext
    x509_extensions = v3_req
    prompt = no
    [ dn ]
    C=CN
    ST=bytedance.com
    L=bytedance.com
    O=字节跳动
    OU=bytedance.com
    CN = bytecert.gwyn.me
    [ req_ext ]
    subjectAltName = @alt_names
    [v3_req]
    subjectAltName = @alt_names
    [ alt_names ]
    DNS.1 = bytedance.com
    DNS.2 = gwyn.me
    DNS.3 = *.gwyn.me
    DNS.4 = bytecert.gwyn.me
    EOF
)

客户端交互

使用伪造的 X.509 证书启动 HTTPS 服务,截获请求参数后得到 flag 的第一部分。

import http.server, ssl

class HookHandler(http.server.BaseHTTPRequestHandler):
    def _set_response(self):
        self.send_response(200)
        self.send_header('Content-type', 'text/html')
        self.end_headers()

    def do_GET(self):
        print("GET request,\nPath: %s\nHeaders:\n%s\n", str(self.path), str(self.headers))
        self._set_response()
        self.wfile.write("GET request for {}".format(self.path).encode('utf-8'))

    def do_POST(self):
        content_length = int(self.headers['Content-Length'])
        post_data = self.rfile.read(content_length)
        print("POST request,\nPath: %s\nHeaders:\n%s\n\nBody:\n%s\n",
                str(self.path), str(self.headers), post_data.decode('utf-8'))

server_address = ('0.0.0.0', 8080)
httpd = http.server.HTTPServer(server_address, HookHandler)

httpd.socket = ssl.wrap_socket(httpd.socket,
                               server_side=True,
                               keyfile='server-key.pem',
                               certfile='server-cert.pem',
                               ssl_version=ssl.PROTOCOL_TLS)

httpd.serve_forever()

服务端交互

服务端要求请求数据中的 packnamecom.ss.android.ugc.aweme,且 packsign 的内容与之相对应。

这里 com.ss.android.ugc.aweme 对应的是抖音客户端的包名,使用 JEB 导出抖音客户端的签名即可。

得到返回消息后使用 AES 进行解密得到 flag 的第二部分。

package com.company;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.net.ssl.HttpsURLConnection;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URL;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.security.spec.X509EncodedKeySpec;
import java.security.KeyFactory;
import java.security.MessageDigest;
import java.security.PublicKey;
import org.json.JSONObject;

public class Main {

    public static String MD5_HEX(byte b[]) throws Exception {
        byte[] v7_1 = MessageDigest.getInstance("MD5").digest(b);
        char[] v1_1 = new char[v7_1.length * 2];
        int v2;
        for(v2 = 0; v2 < v7_1.length; ++v2) {
            int v3_1 = v7_1[v2] & 15;
            int v4_1 = (v7_1[v2] & 0xF0) >> 4;
            int v5_1 = v2 * 2;
            char[] v6 = new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
            v1_1[v5_1] = v6[v4_1];
            v1_1[v5_1 + 1] = v6[v3_1];
        }
        return new String(v1_1);
    }

    public static void main(String[] args) throws Exception {
        String fileName = "Certificate.txt";
        byte[] bytes = Files.readAllBytes(Paths.get(fileName));
        String PAKGSIG = MD5_HEX(bytes).substring(0, 0x20);
        String PACKNAME = "com.ss.android.ugc.aweme";
        String RAWKEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAi3o9aMYew7zmjoFCBz3RJl/IFQOHxEUZlargUog+TPLtsQAvNwJ/X4ypkEL9c6T40jQp3qVNcJkjJn5WNcXX5YEt6qpF+18TVqlNLIBoQBag/pCtwDNPi+8dYeEDkusougKlBIvu44M8v3B/VFedAAMuYbG8d+6L8S/ZitMMaVOn9/UlVcPUOi/PL6N/fRrjgwM2A/FePquReq86pO2ZtUDDJ4GK9H+hwSxgEKYJ0i68oSxAaqHg3pAy8BjyWZ5zBQg60JtVMfmjYqFdTqitrJzbj41k3bO6+7VBiV/saIVMrQHaUyqIEKcYFlihRfExE67PZ79n7F7FvrLtF9NsVQIDAQAB";
        String FLAG = "ByteCTF{b6b31f89-a3ee-";
        String flag = FLAG;
        String packname = PACKNAME;
        Cipher v4 = Cipher.getInstance("AES/CBC/PKCS5Padding");
        IvParameterSpec v5 = new IvParameterSpec(new byte[v4.getBlockSize()]);
        v4.init(1, new SecretKeySpec(PAKGSIG.getBytes(), "AES"), v5);
        String packsign = Base64.getEncoder().encodeToString(v4.doFinal(packname.getBytes("UTF-8")));
        String v3 = UUID.randomUUID().toString().replace("-", "").substring(0, 16);
        X509EncodedKeySpec v0 = new X509EncodedKeySpec(Base64.getDecoder().decode(RAWKEY));
        PublicKey v3_0 = KeyFactory.getInstance("RSA").generatePublic(v0);
        Cipher v0_1 = Cipher.getInstance("RSA/ECB/PKCS1Padding");
        v0_1.init(1, v3_0);
        String key = URLEncoder.encode(Base64.getEncoder().encodeToString(v0_1.doFinal(v3.getBytes())), "UTF-8");
        long timeStamp = System.currentTimeMillis();
        String sign = MD5_HEX((packname + packsign + key + "json" + flag + "1.0" + Long.toString(timeStamp).toString()).getBytes());
        JSONObject v1 = new JSONObject();
        v1.put("key", key);
        v1.put("packname", packname);
        v1.put("packsign", packsign);
        v1.put("sign", sign);
        v1.put("flag", flag);
        v1.put("timeStamp", timeStamp);
        v1.put("clientType", "android");
        v1.put("format", "json");
        v1.put("version", "1.0");
        String urljson = URLEncoder.encode(v1.toString(),"UTF-8");
        URL url = new URL("https://bytecert.gwyn.me:8080/decrypt?json="+urljson);
        HttpsURLConnection con = (HttpsURLConnection)url.openConnection();
        BufferedReader br = new BufferedReader(new InputStreamReader(con.getInputStream()));
        String input = br.readLine();
        br.close();
        String rawinput = new JSONObject(input).get("message").toString();
        Cipher v0_3 = Cipher.getInstance("AES/CBC/PKCS5Padding");
        IvParameterSpec v1_3 = new IvParameterSpec(new byte[v0_3.getBlockSize()]);
        v0_3.init(2, new SecretKeySpec(v3.getBytes(), "AES"), v1_3);
        String v6 = new String(v0_3.doFinal(Base64.getDecoder().decode(rawinput)));
        System.out.print(v6);
    }
}
posted @ 2021-10-18 23:02  Byaidu  阅读(407)  评论(0编辑  收藏  举报