Description#
Welcome to the Sekai Bank challenge!
Difficulty: πΆπ·π·π·
Author: Marc
Given files
Solution#
The goal of this challenge is to reverse engineer an APK to retrieve a hidden flag.
I began by using Jadx to decompile the APK and retrieve the source code.
Next, I searched for the string flag
across the decompiled files, which led me to an interesting file.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
| public interface ApiService {
@PUT("auth/pin/change")
Call<ApiResponse<Void>> changePin(@Body PinRequest pinRequest);
@GET("user/search/{username}")
Call<ApiResponse<User>> findUserByUsername(@Path("username") String str);
@GET("user/balance")
Call<ApiResponse<BalanceResponse>> getBalance();
@POST("flag")
Call<String> getFlag(@Body FlagRequest flagRequest);
@GET("user/profile")
Call<ApiResponse<User>> getProfile();
@GET("transactions/recent")
Call<ApiResponse<List<Transaction>>> getRecentTransactions();
@GET("transactions/{id}")
Call<ApiResponse<Transaction>> getTransaction(@Path("id") String str);
@GET("transactions")
Call<ApiResponse<List<Transaction>>> getTransactions(@Query("page") int i, @Query("limit") int i2);
@GET("user/profile")
Call<ApiResponse<User>> getUserProfile();
@GET("health")
Call<ApiResponse<HealthResponse>> healthCheck();
@POST("auth/login")
Call<ApiResponse<AuthResponse>> login(@Body LoginRequest loginRequest);
@POST("auth/logout")
Call<ApiResponse<Void>> logout();
@POST("auth/refresh")
Call<ApiResponse<AuthResponse>> refreshToken(@Body RefreshTokenRequest refreshTokenRequest);
@POST("auth/register")
Call<ApiResponse<AuthResponse>> register(@Body RegisterRequest registerRequest);
@POST("transactions/send")
Call<ApiResponse<Transaction>> sendMoney(@Body SendMoneyRequest sendMoneyRequest);
@POST("auth/pin/setup")
Call<ApiResponse<Void>> setupPin(@Body PinRequest pinRequest);
@POST("auth/pin/verify")
Call<ApiResponse<Void>> verifyPin(@Body PinRequest pinRequest);
}
|
From the interface, it looks like we need to retrieve the flag by calling the POST /flag
endpoint.
To make that request, we need the base URL of the API.
Luckily, I easily found it in another file.
1
| private static final String BASE_URL = "https://sekaibank-api.chals.sekai.team/api/";
|
I then tried to call the flag endpoint using curl
:
1
2
| $ curl -X POST https://sekaibank-api.chals.sekai.team/api/flag
{"success":false,"error":"X-Signature header is required"}
|
However, the request failed with an error message saying that the X-Signature
header was missing.
After some investigation, I found the function that computes it in the decompiled code.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| private String generateSignature(Request request) throws IOException, GeneralSecurityException {
Signature[] signatureArr;
String str = request.method() + "/api".concat(getEndpointPath(request)) + getRequestBodyAsString(request);
SekaiApplication sekaiApplication = SekaiApplication.getInstance();
PackageManager packageManager = sekaiApplication.getPackageManager();
String packageName = sekaiApplication.getPackageName();
try {
if (Build.VERSION.SDK_INT >= 28) {
PackageInfo packageInfo = packageManager.getPackageInfo(packageName, 134217728);
SigningInfo signingInfo = packageInfo.signingInfo;
if (signingInfo != null) {
if (signingInfo.hasMultipleSigners()) {
signatureArr = signingInfo.getApkContentsSigners();
} else {
signatureArr = signingInfo.getSigningCertificateHistory();
}
} else {
signatureArr = packageInfo.signatures;
}
} else {
signatureArr = packageManager.getPackageInfo(packageName, 64).signatures;
}
if (signatureArr != null && signatureArr.length > 0) {
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
for (Signature signature : signatureArr) {
messageDigest.update(signature.toByteArray());
}
return calculateHMAC(str, messageDigest.digest());
}
throw new GeneralSecurityException("No app signature found");
} catch (PackageManager.NameNotFoundException | NoSuchAlgorithmException e) {
throw new GeneralSecurityException("Unable to extract app signature", e);
}
}
|
The signature is generated using two main components:
str
: A concatenation of the request method (e.g., POST
), the API endpoint (e.g., /api/flag
), and the request body.signatureArr
: The APK’s signing certificates.
To find the body of the request, I went back to the ApiService.java
file and looked at the getFlag
method:
1
2
| @POST("flag")
Call<String> getFlag(@Body FlagRequest flagRequest);
|
Here, we see that the body is a FlagRequest
object.
The object contains a boolean field unmask_flag
that determines whether the flag should be exposed or masked.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| public class FlagRequest {
private boolean unmask_flag;
public FlagRequest(boolean z) {
this.unmask_flag = z;
}
public boolean getUnmaskFlag() {
return this.unmask_flag;
}
public void setUnmaskFlag(boolean z) {
this.unmask_flag = z;
}
}
|
To retrieve the flag, we want to set unmask_flag
to true
.
So the body will look like this:
The next step is to obtain the signatures of the APK.
Thankfully, Jadx-gui provides an easy way to extract this information.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| Signer 1
Type: X.509
Version: 1
Serial number: 0x1
Subject: C=ID, ST=Bali, L=Indonesia, O=HYPERHUG, OU=Development, CN=Aimar S. Adhitya
Valid from: Sun May 18 14:38:07 CEST 2025
Valid until: Thu May 12 14:38:07 CEST 2050
Public key type: RSA
Exponent: 65537
Modulus size (bits): 2048
Modulus: [MODULUS DATA HERE]
Signature type: SHA256withRSA
Signature OID: 1.2.840.113549.1.1.11
MD5 Fingerprint: FC AB 4A F1 F7 41 1B 4B A7 0E C2 FA 91 5D EE 8E
SHA-1 Fingerprint: 2C 97 60 EE 96 15 AD AB DE E0 E2 28 AE D9 1E 3D 4E BD EB DF
SHA-256 Fingerprint: 3F 3C F8 83 0A CC 96 53 0D 55 64 31 7F E4 80 AB 58 1D FC 55 EC 8F E5 5E 67 DD DB E1 FD B6 05 BE
|
With this information, we can now generate the X-Signature
header using a Python script.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| import hmac
import hashlib
import json
# --- Step 1: Use provided SHA-256 digest of certificate ---
key_hex = "3f3cf8830acc96530d5564317fe480ab581dfc55ec8fe55e67dddbE1fdb605be"
key = bytes.fromhex(key_hex)
# --- Step 2: Construct the exact message to be signed ---
http_method = "POST"
endpoint_path = "/flag"
body_dict = {"unmask_flag": True}
# Mimic Java's JSON serialization: compact, no spaces
request_body = json.dumps(body_dict, separators=(",", ":"))
# Final message
message = http_method + "/api" + endpoint_path + request_body
# --- Step 3: HMAC-SHA256 signature ---
mac = hmac.new(key, msg=message.encode("utf-8"), digestmod=hashlib.sha256)
signature = mac.hexdigest()
print("X-Signature:", signature)
|
Finally, after generating the X-Signature
, I was able to send the following curl
request to retrieve the flag:
1
2
| $ curl -X POST https://sekaibank-api.chals.sekai.team/api/flag -H 'X-Signature: 440ba2925730d137259f297fd6fba02af2f7b6c414dd16a1ac336e9047cdb8f5' -H 'Content-Type: application/json' -d '{"unmask_flag":true}'
SEKAI{are-you-ready-for-the-real-challenge?}
|