Ethernaut 02 Fallout
Ethernaut 02 FalloutEthernaut 02 FalloutEthernaut 02 Fallout 문제 풀이Ethernaut 02 Fallout writeup
Contents
문제
지문
Claim ownership of the contract below to complete this level. Things that might help Solidity Remix IDE
코드
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "openzeppelin-contracts-06/math/SafeMath.sol";
contract Fallout {
using SafeMath for uint256;
mapping(address => uint256) allocations;
address payable public owner;
/* constructor */
function Fal1out() public payable {
owner = msg.sender;
allocations[owner] = msg.value;
}
modifier onlyOwner() {
require(msg.sender == owner, "caller is not the owner");
_;
}
function allocate() public payable {
allocations[msg.sender] = allocations[msg.sender].add(msg.value);
}
function sendAllocation(address payable allocator) public {
require(allocations[allocator] > 0);
allocator.transfer(allocations[allocator]);
}
function collectAllocations() public onlyOwner {
msg.sender.transfer(address(this).balance);
}
function allocatorBalance(address allocator) public view returns (uint256) {
return allocations[allocator];
}
}배경지식
현재 Solidity에서는 생성자를 constructor()로 작성한다. 생성자는 컨트랙트가 배포될 때 한 번만 실행되고, 배포 이후에는 외부에서 다시 호출할 수 없다.
그런데 Solidity 0.4.22 이전에는 생성자를 constructor() 키워드가 아니라 컨트랙트 이름과 같은 함수로 작성했다. 예를 들어 컨트랙트명이 Fallout이면 생성자도 function Fallout() public { ... } 형태였다.
이 방식은 오타에 취약하다. 함수명이 컨트랙트명과 정확히 같지 않으면 컴파일러는 그 함수를 일반 public 함수로 취급하고, 배포 이후에도 누구나 호출할 수 있다.
payable은 함수가 ETH를 받을 수 있게 하는 키워드다. msg.value는 해당 함수 호출과 함께 전송된 ETH의 양이다.
이 문제의 Fal1out()은 public payable이므로 외부 계정이 직접 호출하면서 ETH를 보낼 수 있다. 그리고 함수 내부에서 owner = msg.sender를 실행하므로, 호출자가 그대로 owner가 된다.
문제 코드 분석
먼저 owner와 onlyOwner다.
address payable public owner;
modifier onlyOwner() {
require(msg.sender == owner, "caller is not the owner");
_;
}owner는 컨트랙트의 관리자 주소다. onlyOwner는 msg.sender가 현재 owner와 같을 때만 함수를 실행하게 한다.
따라서 이 레벨의 목표인 소유권 탈취는 owner 값을 내 주소로 바꾸는 것이다.
문제는 생성자처럼 보이는 아래 함수다.
/* constructor */
function Fal1out() public payable {
owner = msg.sender;
allocations[owner] = msg.value;
}주석에는 constructor라고 적혀 있지만 실제 함수명은 Fal1out이다. Fallout의 두 번째 l처럼 보이는 자리에 숫자 1이 들어가 있다.
컨트랙트 이름은 Fallout이고 함수 이름은 Fal1out이라 둘은 같지 않다. 구버전 생성자 문법으로 봐도 생성자가 아니고, Solidity 0.6.0 기준에서도 constructor()가 아니다. 따라서 이 함수는 배포 후에도 누구나 호출할 수 있는 일반 public 함수다.
owner = msg.sender에 접근 제어가 없기 때문에 아무 주소나 Fal1out()을 호출하면 그 주소가 owner가 된다.
풀이
Fal1out()은 내부에서 바로 owner = msg.sender를 실행하므로, 내 계정으로 한 번 호출하면 ownership claim 조건이 충족된다.
함수에 payable이 붙어 있어 ETH를 함께 보낼 수 있지만, 소유권을 바꾸는 데는 value: 1이면 충분하다.
익스플로잇
await contract.Fal1out({ value: 1 })
Challenge
Prompt
Claim ownership of the contract below to complete this level. Things that might help Solidity Remix IDE
Code
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "openzeppelin-contracts-06/math/SafeMath.sol";
contract Fallout {
using SafeMath for uint256;
mapping(address => uint256) allocations;
address payable public owner;
/* constructor */
function Fal1out() public payable {
owner = msg.sender;
allocations[owner] = msg.value;
}
modifier onlyOwner() {
require(msg.sender == owner, "caller is not the owner");
_;
}
function allocate() public payable {
allocations[msg.sender] = allocations[msg.sender].add(msg.value);
}
function sendAllocation(address payable allocator) public {
require(allocations[allocator] > 0);
allocator.transfer(allocations[allocator]);
}
function collectAllocations() public onlyOwner {
msg.sender.transfer(address(this).balance);
}
function allocatorBalance(address allocator) public view returns (uint256) {
return allocations[allocator];
}
}Background
In current Solidity, a constructor is written as constructor(). It runs once at deployment and cannot be called externally afterward.
Before Solidity 0.4.22, a constructor was written as a function with the same name as the contract instead of with the constructor() keyword. For a contract named Fallout, the constructor looked like function Fallout() public { ... }.
This approach is vulnerable to typos. If the function name does not exactly match the contract name, the compiler treats it as an ordinary public function, which anyone can call after deployment.
payable is the keyword that lets a function receive ETH. msg.value is the amount of ETH sent with the call.
This challenge's Fal1out() is public payable, so an external account can call it directly while sending ETH. Since it runs owner = msg.sender, the caller becomes owner.
Challenge code analysis
First, owner and onlyOwner.
address payable public owner;
modifier onlyOwner() {
require(msg.sender == owner, "caller is not the owner");
_;
}owner is the contract's admin address. onlyOwner only runs the function when msg.sender equals the current owner.
So this level's goal, taking over ownership, means changing the owner value to my address.
The problem is the function below that looks like a constructor.
/* constructor */
function Fal1out() public payable {
owner = msg.sender;
allocations[owner] = msg.value;
}The comment says constructor, but the actual function name is Fal1out. The digit 1 sits where the second l of Fallout appears to be.
The contract name is Fallout and the function name is Fal1out, so the two differ. It is not a constructor under the old syntax, and under Solidity 0.6.0 it is not constructor() either. So this is an ordinary public function that anyone can call after deployment.
Since owner = msg.sender has no access control, any address that calls Fal1out() becomes owner.
Solution
Fal1out() runs owner = msg.sender directly, so calling it once from my account satisfies the ownership claim.
The function is payable, so ETH can be sent along, but value: 1 is enough to change ownership.
Exploit
await contract.Fal1out({ value: 1 })