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()
服务端交互
服务端要求请求数据中的 packname
为 com.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);
}
}