Before the CTF I was learning about some Android stuff. I practiced decompiling and reversing APKs, and this challenge was exactly that. We need to reverse engineer the APK to find the flag. It was awesome being able to solve this challenge after practicing it right before, it really paid off.

The Challenge

We simply get an OTPVault.apk file which is the app we need to inspect. If we load the APK into a program like Android Studio we can run it in an emulator. If you haven't set it up already, you can create an emulator device in the AVD Manager (Android Virtual Device Manager). Then you can run the app in the emulator to see what it does.

When we launch the application we just get a form with an "OTP code" input field. If we try to submit anything we just get an "Invalid OTP" message.

One of the first steps to do when dealing with an APK is to look at the code. From there we can really understand how the app works, and if we can somehow bypass this screen.

Reverse Engineering the APK

A simple way to view the code of an APK is to load it up in Android Studio. When doing this make sure to use the "Profile or Debug APK" option. This will open the APK and list the files contained in it.

On the left side, we see the Project panel. This is a directory structure with decompiled code and files. First, open the main OTPVault folder, then we see a java folder. This is interesting because it might contain some of the logic of the application. Then com is an interesting one because java paths are pretty much a reverse domain name. First comes something like com. and then a name like otpvault. In here the MainActivity file is a default class that almost every app has.

When we double click this file it opens a MainActivity.smali file. From what I understand Smali is a sort of a human-readable version of the Android bytecode.

Smali

.class public Lcom/otpvault/MainActivity;
.super Lcom/facebook/react/ReactActivity;
.source "MainActivity.java"


# annotations
.annotation system Ldalvik/annotation/MemberClasses;
    value = {
        Lcom/otpvault/MainActivity$MainActivityDelegate;
    }
.end annotation


# direct methods
.method public constructor <init>()V
    .registers 1

    .line 7
    invoke-direct {p0}, Lcom/facebook/react/ReactActivity;-><init>()V

    return-void

.end method

...

We see a lot of "React" stuff here. React is a popular JavaScript framework to build web apps. But I had heard of React Native as well, which is an Android version of this framework that can be used in native apps. So if we're trying to understand the logic of the app we need to look at the React code.

Reverse Engineering React Native

We can easily recognize React Native by looking in the assets folder. You can't find it in the Project panel like before, but it's in the main OTPVault.apk file. In this folder, we should find an index.android.bundle file. And indeed, we find this file in the assets folder of the APK. We can't directly open it in Android Studio, we need to export all these files first.

Luckily this is a lot simpler than it sounds. An APK file is basically just a ZIP file, so we can just extract it into a folder to view the contents.

Shell

$ unzip OTPVault.apk -d apk
...

$ ls apk
AndroidManifest.xml  META-INF  assets  classes.dex  kotlin  lib  okhttp3  res  resources.arsc

After unzipping we see a lot of files. In the assets folder is the index.android.bundle file again. If you are to try and read this file it's an unreadable compiled mess of JavaScript. We first need to decompile it to get anything useful out of it. There is a handy tool I found on StackOverflow for someone with the same problem. Using the npm module called react-native-decompiler we can easily decompile the file into separate readable JavaScript files.

Shell

npx react-native-decompiler -i index.android.bundle -o output

This will create an output folder containing the JavaScript files. This isn't perfect because the code is decompiled, there is just no source available. But it's way better than before.

It created 45 .js files which all combined are hard to look through manually. But a handy trick is to start by looking for a known string, like "Invalid OTP". We saw this when we inputted a wrong code, so we might be able to find why it was wrong. We can use grep to find this string in the files.

Shell

$ grep "Invalid OTP" *.js
396.js:  output: 'Invalid OTP',

And sure enough, we find that in the 396.js file there is our error string. We can open this whole file now to see what it does. Around line 70 we get some interesting code with a getFlag function:

JavaScript

function O() {
  var n;
  module7.default(this, O);
  (n = b.call(this, ...args)).state = {
  output: 'Insert your OTP to unlock your vault',
  text: '',
  };
  n.s = 'JJ2XG5CIMFRWW2LOM4';
  n.url = 'http://congon4tor.com:7777';
  n.token = '652W8NxdsHFTorqLXgo=';

  n.getFlag = function () {
    var module7, o;
    return regeneratorRuntime.default.async(
      function (u) {
        for (;;)
          switch ((u.prev = u.next)) {
            case 0:
              u.prev = 0;
              module7 = {
              headers: {
                Authorization: 'Bearer KMGQ0YTYgIMTk5Mjc2NzZY4OMjJlNzAC0WU2DgiYzE41ZDwN',
              },
              };
              u.next = 4;
              return regeneratorRuntime.default.awrap(module400.default.get(n.url + '/flag', module7));

We see a URL and some tokens. If we visit the URL (http://congon4tor.com:7777/) we get a message:

No flag sorry

So no flag on the index page directly, but we have some other values we might need to send with it.

Firstly, we see the n.url + '/flag' which is the URL plus /flag. If we now try to visit this URL (http://congon4tor.com:7777/flag) we get another message:

{"error":"Missing authorization header"}

So we're now missing the Authorization header. Luckily we also see this header with a Bearer token in the code. If we use a program like Burp Suite to send a request with this specific header added:

HTTP

GET /flag HTTP/1.1
Host: congon4tor.com:7777
Authorization: Bearer KMGQ0YTYgIMTk5Mjc2NzZY4OMjJlNzAC0WU2DgiYzE41ZDwN

...we get the flag in the response!

HTTP

HTTP/1.1 200 OK
Server: nginx/1.14.0 (Ubuntu)
Content-Type: application/json
Content-Length: 50
Access-Control-Allow-Origin: *

{"flag":"flag{5450384e093a0444e6d3d39795dd7ddd}"}