485 words
2 minutes
[SekaiCTF 2025] Writeups

Sekai’s Bank - Signature#

Let me introduce you to Sekai Bank!

Static Analysis using Jadx#

I started analyze apk file using Jadx GUI. I loaded the apk file and started exploring the code.

package com.sekai.bank;
import android.app.Application;
import com.sekai.bank.network.ApiClient;
import com.sekai.bank.utils.TokenManager;
import com.sekai.bank.utils.delayed_transaction.DelayedTransactionManager;
/* loaded from: classes2.dex */
public class SekaiApplication extends Application {
private static SekaiApplication instance;
private ApiClient apiClient;
private TokenManager tokenManager;
@Override // android.app.Application
public void onCreate() {
super.onCreate();
instance = this;
this.tokenManager = new TokenManager(this);
this.apiClient = new ApiClient(this.tokenManager);
initializeDelayedTransactionMonitoring();
}
private void initializeDelayedTransactionMonitoring() {
new DelayedTransactionManager(this).startPeriodicChecking();
}
public static SekaiApplication getInstance() {
return instance;
}
public ApiClient getApiClient() {
return this.apiClient;
}
public TokenManager getTokenManager() {
return this.tokenManager;
}
}

From the SekaiApplication class, see that it initializes several components for the application, including the

  • ApiClient
  • TokenManager
  • DelayedTransactionManager

Navigated to the ApiClient and get important information about baseURL:

public class ApiClient {
private static final String BASE_URL = "https://sekaibank-api.chals.sekai.team/api/";
private static final int REFRESH_TIMEOUT_SECONDS = 3;
private static final String TAG = "SekaiBank-API";
private static final int TIMEOUT_SECONDS = 30;
private static final int TOKEN_TIMEOUT_SECONDS = 2;
private final ApiService apiService;
private final Retrofit retrofit;
private final TokenManager tokenManager;

And all endpoints:

classes2.dex
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);

Looking into the FlagRequest class:

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;
}
}

So we have to add unmask_flag to the request body, then craft a request to this endpoint with the appropriate payload to retrieve the flag.


Break down the request#

Trying to send the request to /flag endpoint:

Terminal window
curl -X POST https://sekai-bank.ctf/api/flag -H "Content-Type: application/json"

Got response: X-Signature is missing. Looking into how requests are handled, I found this class:

private class SignatureInterceptor implements Interceptor {
...
@Override
public Response intercept(Interceptor.Chain chain) throws IOException {
Request request = chain.request();
try {
return chain.proceed(request.newBuilder().header("X-Signature", generateSignature(request)).build());
} catch (Exception e) {
Log.e(ApiClient.TAG, "Failed to generate signature: " + e.getMessage());
return chain.proceed(request);
}
}

From here, X-Signature is generated using HMAC-SHA256, with:

  1. The request body
  2. The app’s signing certificate

as inputs. This value is placed in the header.


Get App Signature#

Terminal window
apksigner verify --print-certs SekaiBank.apk
Signer #1 certificate DN: C=ID, ST=Bali, L=Indonesia, O=HYPERHUG, OU=Development, CN=Aimar S. Adhitya
Signer #1 certificate SHA-256 digest: 3f3cf8830acc96530d5564317fe480ab581dfc55ec8fe55e67dddbe1fdb605be
Signer #1 certificate SHA-1 digest: 2c9760ee9615adabdee0e228aed91e3d4ebdebdf
Signer #1 certificate MD5 digest: fcab4af1f7411b4ba70ec2fa915dee8e

From the generateSignature method, SHA256 is used:

if (signingCertificateHistory != null && signingCertificateHistory.length > 0) {
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
for (Signature signature : signingCertificateHistory) {
messageDigest.update(signature.toByteArray());
}

Crafting X-Signature Header#

The string used in HMAC is:

String str = request.method() + "/api".concat(getEndpointPath(request)) + getRequestBodyAsString(request);

So for our case: POST/api/flag{"unmask_flag":true}

Python script to generate the signature:

import hmac, hashlib
key_hex = "3f3cf8830acc96530d5564317fe480ab581dfc55ec8fe55e67dddbe1fdb605be" # from apksigner
key = bytes.fromhex(key_hex)
msg = b'POST/api/flag{"unmask_flag":true}'
print(hmac.new(key, msg, hashlib.sha256).hexdigest())

Output:

440ba2925730d137259f297fd6fba02af2f7b6c414dd16a1ac336e9047cdb8f5

Now final request:

Terminal window
curl -X POST "https://sekaibank-api.chals.sekai.team/api/flag" \
-H "Content-Type: application/json" \
-H "X-Signature: 440ba2925730d137259f297fd6fba02af2f7b6c414dd16a1ac336e9047cdb8f5" \
-d '{"unmask_flag":true}'

And the flag is retrieved successfully.

[SekaiCTF 2025] Writeups
https://plugspakuko.github.io/posts/ctf/sekaictf2025/
Author
kpakkawat
Published at
2025-08-19
License
CC BY-NC-SA 4.0