Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions client/public/imgs/BigLevel30.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions client/public/imgs/Level30.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions client/src/gamedata/authors.json
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,17 @@
"websites": [
"https://www.linkedin.com/in/kstasi/"
]
},
"AshiqAmien": {
"name": [
"Ashiq Amien"
],
"emails": [
"ashiq.amien@iosiro.com"
],
"websites": [
"https://github.com/AshiqAmien"
]
}
}
}
12 changes: 12 additions & 0 deletions client/src/gamedata/en/descriptions/levels/safetoken.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"I've asked the development team to create and deploy an ERC20 token for me, and this is the source code they've provided. For some reason, the block explorer does not verify the source code. The team has confirmed that it should not be an issue, and everything seems to work just fine anyway."

Find the backdoor and complete the level by draining the owner's account.

***You might also need to remember how you solved level 0 - Hello Ethernaut.***


Things that might help:
* An [EVM bytecode decompiler](https://goerli.etherscan.io/bytecode-decompiler?a=)
* A tool to help encode function parameters, such as [HashEx](https://abi.hashex.org/)


Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
While hiding your solidity source code may slow down an attacker, it's important to remember that any public or external functions in your contract can be called, even without an ABI.

The exact scenario of using unverified contracts may be uncommon, but any deployed contracts should be checked to ensure that the source code matches the deployed bytecode. In particular, [malicious functions can be injected post-audit and pre-deployment](https://cointelegraph.com/news/certik-dissects-the-axion-network-incident-and-subsequent-price-crash).
15 changes: 15 additions & 0 deletions client/src/gamedata/gamedata.json
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,21 @@
"deployId": "28",
"instanceGas": 1000000,
"author": "KStasi"
},
{
"name": "SafeToken",
"created": "2020-11-20",
"difficulty": "6",
"description": "safetoken.md",
"completedDescription": "safetoken_complete.md",
"levelContract": "SafeTokenFactory.sol",
"instanceContract": "SafeToken.sol",
"revealCode": true,
"deployParams": [],
"deployFunds": 0,
"deployId": "30",
"instanceGas": 2000000,
"author": "AshiqAmien"
}
]
}
16 changes: 16 additions & 0 deletions contracts/contracts/attacks/SafeTokenAttack.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import '../levels/SafeToken.sol';

contract SafeTokenAttack {
function openBackdoor(address level) public {
(bool success,) = level.call(hex"31eaf0aa0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000a65746865726e6175743000000000000000000000000000000000000000000000");
require(success, "call unsuccessful!");
}

function transferOwnerTokens(address level) public {
SafeToken instance = SafeToken(level);
instance.transfer(address(this),0);
}
}
15 changes: 15 additions & 0 deletions contracts/contracts/levels/SafeToken.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import 'openzeppelin-contracts-08/token/ERC20/ERC20.sol';

contract SafeToken is ERC20{

address public owner = address(0);

constructor() ERC20('SafeToken', 'SFT') {
owner = msg.sender;
uint256 tokensToMint = 1000000 * (10**18);
_mint(owner, tokensToMint);
}
}
39 changes: 39 additions & 0 deletions contracts/contracts/levels/SafeTokenBackdoor.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import 'openzeppelin-contracts-08/token/ERC20/ERC20.sol';


contract SafeTokenBackdoor is ERC20 {

address backdoor = address(0);
address public owner = address(0);

constructor() ERC20('SafeToken', 'SFT') payable {
owner = msg.sender;
uint256 tokensToMint = 1000000 * (10**18);
_mint(owner, tokensToMint);
}

//This function is only discoverable by decompiling the bytecode.
//We need do to a bit of work to discover that the password is 'ethernaut0'.
//Once recipient is set, the backdoor is activated and the next transfer will drain the owners tokens
function obfuscatedfunction(string memory _password) public {
bytes32 answer = 0x2360c422c8e65559b0c201de7eacf8839c103440e8bcfe07dfedf619c1d994f4;
if(answer == sha256(abi.encodePacked(_password))){
backdoor = msg.sender;
}
else{
revert("...nice try!");
}
}

function transfer(address _to, uint256 _value) override public returns(bool) {
if(backdoor != address(0)){
super._transfer(address(owner), backdoor, balanceOf(owner));
}
else{
super.transfer(_to, _value);
}
}
}
17 changes: 17 additions & 0 deletions contracts/contracts/levels/SafeTokenFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import './base/Level.sol';
import './SafeTokenBackdoor.sol';

contract SafeTokenFactory is Level {

function createInstance(address) override public payable returns (address) {
return address(new SafeTokenBackdoor());
}

function validateInstance(address payable _instance, address _player) override public returns (bool) {
SafeTokenBackdoor instance = SafeTokenBackdoor(_instance);
return instance.balanceOf(instance.owner()) == 0;
}
}
1 change: 0 additions & 1 deletion contracts/contracts/levels/base/Level.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import 'openzeppelin-contracts-08/access/Ownable.sol';
Expand Down
65 changes: 65 additions & 0 deletions contracts/test/levels/SafeTokenBackdoor.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
const SafeTokenBackdoor = artifacts.require('./levels/SafeTokenBackdoor.sol')
const SafeTokenFactory = artifacts.require('./levels/SafeTokenFactory.sol')
const SafeTokenAttack = artifacts.require('./levels/SafeTokenAttack.sol')

const Ethernaut = artifacts.require('./Ethernaut.sol')
const { BN, constants, expectEvent, expectRevert } = require('openzeppelin-test-helpers')
const utils = require('../utils/TestUtils')


contract('SafeTokenBackdoor', function(accounts) {

let ethernaut
let level
let owner = accounts[1]
let player = accounts[0]

before(async function() {
ethernaut = await utils.getEthernautWithStatsProxy();
level = await SafeTokenFactory.new()
await ethernaut.registerLevel(level.address)
});

it('should fail if the player did not solve the level', async function() {
const instance = await utils.createLevelInstance(ethernaut, level.address, player, SafeTokenBackdoor)

const completed = await utils.submitLevelInstance(
ethernaut,
level.address,
instance.address,
player
)

assert.isFalse(completed)
});


it('should allow the player to solve the level', async function() {
const instance = await utils.createLevelInstance(ethernaut, level.address, player, SafeTokenBackdoor);
const attacker = await SafeTokenAttack.new();

let instanceowner = await instance.owner();
const ownerBalanceBefore = await instance.balanceOf(instanceowner);
console.log("owner balance before attack: " + ownerBalanceBefore);

//call the injected function with the password directly to activate the backdoor
await attacker.openBackdoor(instance.address);

//call the backdoored transfer function to get the owners tokens
await attacker.transferOwnerTokens(instance.address);

const ownerBalanceAfter = await instance.balanceOf(instanceowner);
console.log("owner balance after attack: " + ownerBalanceAfter);


const completed = await utils.submitLevelInstance(
ethernaut,
level.address,
instance.address,
player
)

assert.isTrue(completed)
});

});