Init
parent
2b904a7d0d
commit
e6252db6b7
41
README.md
41
README.md
|
@ -1,3 +1,42 @@
|
|||
# pwn_w3bridges
|
||||
|
||||
Workshop for web3 bridge hacking at Hacktivity 2022
|
||||
Workshop for web3 bridge hacking at Hacktivity 2022
|
||||
|
||||
|
||||
# Scenario 1 - Receipt reuse - Topology
|
||||
|
||||
Story: Substrate system being built after ERC20 token is sold.
|
||||
|
||||
- Substrate node with EVM pallet || Token minter smart contract (vuln here)
|
||||
- Bridge providing receipts || Checks bridge balance on Substrate node
|
||||
- Ethereum node || Token minter smart contract (target for mint)
|
||||
|
||||
https://remix.ethereum.org/
|
||||
Faucet?
|
||||
https://polkadot.js.org/apps/
|
||||
https://ethereum.org/en/developers/docs/standards/tokens/erc-20/
|
||||
https://git.hsbp.org/six/eth_keygen
|
||||
|
||||
https://github.com/paritytech/substrate-contracts-node
|
||||
https://docs.substrate.io/quick-start/
|
||||
https://substrate.io/developers/playground/ | alternative
|
||||
https://github.com/substrate-developer-hub/substrate-front-end-template
|
||||
|
||||
|
||||
## Commands
|
||||
$
|
||||
|
||||
# Scenario 2 - ECDSA signature forgery - Topology
|
||||
|
||||
Story: ink! smart contract interoperability.
|
||||
|
||||
- Substrate node with ink!
|
||||
- No bridge, but signature forgery
|
||||
- Ethereum node
|
||||
|
||||
https://github.com/paritytech/ink
|
||||
https://use.ink/getting-started/setup
|
||||
https://medium.com/block-journal/introducing-substrate-smart-contracts-with-ink-d486289e2b59
|
||||
|
||||
## Commands
|
||||
$
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
#!/usr/bin/python3
|
||||
# Eth ECDSA key generator, signer and arg recovery for web3 hacking
|
||||
# Author: six
|
||||
# References: https://web3py.readthedocs.io/en/stable/web3.eth.account.html?highlight=sign#sign-a-message
|
||||
# Ethereum Private Keys Directory -> https://privatekeys.pw/keys/ethereum/1
|
||||
|
||||
print("\033[38;5;111m\n " + 16*'=' + "\n || EthKeyGen ||\n " + 16*'=' + "\n\033[0;0m")
|
||||
|
||||
from web3.auto import w3
|
||||
from web3 import Web3
|
||||
from eth_account.messages import encode_defunct
|
||||
|
||||
# Generate signed message and print it out
|
||||
def generate(_msg, _privkey):
|
||||
message = encode_defunct(text=_msg)
|
||||
try:
|
||||
signed_message = w3.eth.account.sign_message(message, _privkey)
|
||||
except:
|
||||
print("Error while signing message. Make sure the private key is in hex and correctly generated.")
|
||||
sys.exit()
|
||||
print("\033[38;5;111mPrivate key:\033[0;0m " + _privkey)
|
||||
print("\033[38;5;111mRecovered signer:\033[0;0m " + w3.eth.account.recover_message(message, signature=signed_message.signature))
|
||||
print("\033[38;5;111mMessage str:\033[0;0m " + str(message))
|
||||
print("\033[38;5;111mSigned message:\033[0;0m " + str(signed_message))
|
||||
return signed_message
|
||||
|
||||
|
||||
# ecrecover in Solidity expects v as a native uint8, but r and s as left-padded bytes32
|
||||
# Remix / web3.js expect r and s to be encoded to hex. This method does the pad & hex:
|
||||
def to_32byte_hex(val):
|
||||
return Web3.toHex(Web3.toBytes(val).rjust(32, b'\0'))
|
||||
|
||||
# Recover for tx
|
||||
def gen_vrs(signed_message):
|
||||
ec_recover_args = (msghash, v, r, s) = (
|
||||
Web3.toHex(signed_message.messageHash),
|
||||
signed_message.v,
|
||||
to_32byte_hex(signed_message.r),
|
||||
to_32byte_hex(signed_message.s),)
|
||||
print("\n\033[38;5;111mRecovered message hash:\033[0;0m " + ec_recover_args[0])
|
||||
print(" \033[38;5;111mv:\033[0;0m " + str(ec_recover_args[1]))
|
||||
print(" \033[38;5;111mr:\033[0;0m " + ec_recover_args[2])
|
||||
print(" \033[38;5;111ms:\033[0;0m " + ec_recover_args[3])
|
||||
print("\033[38;5;111m="*108+"\033[0;0m")
|
||||
|
||||
# Generate random string
|
||||
import string
|
||||
import random
|
||||
def random_str(N):
|
||||
res = ''.join(random.choices(string.ascii_uppercase + string.digits, k=N))
|
||||
return(str(res))
|
||||
|
||||
# Generate random eth private key
|
||||
from eth_account import Account
|
||||
import secrets
|
||||
def random_privkey():
|
||||
priv = secrets.token_hex(32)
|
||||
private_key = "0x" + priv
|
||||
acct = Account.from_key(private_key)
|
||||
return private_key
|
||||
|
||||
# Solidity code example for verification
|
||||
def print_sol_recovery():
|
||||
print('''// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.16;
|
||||
|
||||
contract Recover {
|
||||
function ecr (bytes32 msgh, uint8 v, bytes32 r, bytes32 s) public pure
|
||||
returns (address sender) {
|
||||
return ecrecover(msgh, v, r, s);
|
||||
}
|
||||
}''')
|
||||
|
||||
# Take command line arguments for convinience
|
||||
import argparse
|
||||
import sys
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("-d", "--dgen", help="Sign message with default keys", action="store_true")
|
||||
parser.add_argument("-g", "--gen", help="Sign message, requires -m at least, if no -p is provided then it will be random", action="store_true")
|
||||
parser.add_argument("-r", "--rgen", help="Sign with random key and msg, provide a number for amount")
|
||||
parser.add_argument("-p", "--pk", help="Provide private key in hex for signed message generation")
|
||||
parser.add_argument("-m", "--msg", help="Provide message as string for signed message generation")
|
||||
parser.add_argument("-s", "--sol", help="Print solidity code for ecrecover", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.dgen:
|
||||
forv = generate("Anything","0000000000000000000000000000000000000000000000000000000000000001") # hex|0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf
|
||||
gen_vrs(forv)
|
||||
sys.exit()
|
||||
if args.rgen:
|
||||
if int(args.rgen) > 999999:
|
||||
print("That is probably too much.")
|
||||
sys.exit()
|
||||
for i in range(int(args.rgen)):
|
||||
forv = generate(random_str(9),random_privkey())
|
||||
gen_vrs(forv)
|
||||
sys.exit()
|
||||
if args.gen:
|
||||
_pk = str(args.pk)
|
||||
if _pk == "None":
|
||||
_pk = random_privkey()
|
||||
_msg = str(args.msg)
|
||||
forv = generate(_msg,_pk)
|
||||
gen_vrs(forv)
|
||||
sys.exit()
|
||||
if args.sol:
|
||||
print_sol_recovery()
|
||||
sys.exit()
|
||||
|
||||
forv = generate("Anything","0000000000000000000000000000000000000000000000000000000000000001") # hex|0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf
|
||||
gen_vrs(forv)
|
||||
print("\nNo arguments were provided. You can use --help.")
|
|
@ -0,0 +1,128 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
pragma solidity ^0.8.11;
|
||||
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
|
||||
|
||||
contract SafuDotNFT is AccessControlUpgradeable {
|
||||
uint256 public maxNFTs;
|
||||
uint256 public NFTCount;
|
||||
uint256 public NFTPrice;
|
||||
mapping (uint256 => string) internal idToHash;
|
||||
mapping (uint256 => address) internal idToOwner;
|
||||
uint256 public blockTime;
|
||||
string private correct_password;
|
||||
|
||||
// Part inspired by CCTF
|
||||
uint160 answer = 0;
|
||||
address private admin = 0xdD870fA1b7C4700F2BD7f44238821C26f7392148;
|
||||
event contractStart(address indexed _admin);
|
||||
mapping(address => uint256) public calls;
|
||||
mapping(address => uint256) public tries;
|
||||
|
||||
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
|
||||
|
||||
constructor(address O) payable {
|
||||
emit contractStart(admin);
|
||||
answer = uint160(admin);
|
||||
admin = 0==0?O:0x583031D1113aD414F02576BD6afaBfb302140225;
|
||||
maxNFTs = 99;
|
||||
NFTCount = 0;
|
||||
NFTPrice = 10000000000000000;
|
||||
blockTime = block.timestamp;
|
||||
_setupRole(MINTER_ROLE, admin);
|
||||
}
|
||||
|
||||
function mint(string memory _hashu) public payable {
|
||||
require(msg.sender == admin, 'You are not the central admin!');
|
||||
require(blockTime <= block.timestamp + 5 minutes, 'Chill bro!');
|
||||
require(NFTCount <= 99, 'You shall not pass! All NFTz are minted!');
|
||||
require(msg.value >= NFTPrice, 'Where are da fundz?');
|
||||
blockTime = block.timestamp;
|
||||
NFTCount = NFTCount + 1;
|
||||
NFTPrice = NFTPrice * 2;
|
||||
idToOwner[NFTCount] = msg.sender;
|
||||
idToHash[NFTCount] = _hashu;
|
||||
}
|
||||
|
||||
function mintWithReceipt(
|
||||
address recipient,
|
||||
uint256 amount,
|
||||
uint256 uuid,
|
||||
uint8 v,
|
||||
bytes32 r,
|
||||
bytes32 s
|
||||
) public {
|
||||
|
||||
bytes32 payloadHash = keccak256(abi.encode(recipient, amount, uuid));
|
||||
bytes32 hash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", payloadHash));
|
||||
//require(!_receipts[hash], "Receipt already used"); // Dumb without it.
|
||||
_checkSignature(hash, v, r, s);
|
||||
mint("mintWithReceipt");
|
||||
//_receipts[hash] = true;
|
||||
}
|
||||
|
||||
function _checkSignature(
|
||||
bytes32 hash,
|
||||
uint8 v,
|
||||
bytes32 r,
|
||||
bytes32 s
|
||||
) internal view {
|
||||
address signer = ecrecover(hash, v, r, s);
|
||||
require(hasRole(MINTER_ROLE, signer), "Signature invalid");
|
||||
}
|
||||
|
||||
|
||||
function transfer(uint256 _tokenId, address _toAddress) external {
|
||||
require(msg.sender == idToOwner[_tokenId], 'But it is not yours!');
|
||||
idToOwner[_tokenId] = _toAddress;
|
||||
}
|
||||
|
||||
function adminWithdraw() external returns (bool) {
|
||||
require(msg.sender == admin, 'You are not the central admin!');
|
||||
(bool sent,) = msg.sender.call{value: address(this).balance}("");
|
||||
return sent;
|
||||
}
|
||||
|
||||
function WhoGotchaThat(uint256 _whichOne) public view returns (address) {
|
||||
return(idToOwner[_whichOne]);
|
||||
}
|
||||
|
||||
function WhatIsTheHash(uint256 _tokenId) public view returns (string memory){
|
||||
return(idToHash[_tokenId]);
|
||||
}
|
||||
|
||||
function adminChange(address _newAdmin) external returns (bool) {
|
||||
require(blockTime <= block.timestamp + 6 minutes, 'Welcome to the game!');
|
||||
admin = _newAdmin;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
function set_password(string memory _password) external {
|
||||
require(msg.sender == admin, 'You are not the central admin!');
|
||||
correct_password = _password;
|
||||
}
|
||||
|
||||
function su1c1d3(address payable _addr, string memory _password) external {
|
||||
require(msg.sender == admin, 'You are not the central admin!');
|
||||
require(keccak256(abi.encodePacked(correct_password)) == keccak256(abi.encodePacked(_password)), 'Very sekur.');
|
||||
selfdestruct(_addr);
|
||||
}
|
||||
|
||||
function callOnlyOnce() public {
|
||||
require(tries[msg.sender] < 1, "No more tries");
|
||||
calls[msg.sender] += 1;
|
||||
answer = answer ^ uint160(admin);
|
||||
(bool sent, ) = msg.sender.call{value: 1}("");
|
||||
require(sent, "Failed to call");
|
||||
tries[msg.sender] += 1;
|
||||
}
|
||||
|
||||
function answerReveal() public view returns(uint256 ) {
|
||||
require(calls[msg.sender] == 2, "Try more :)");
|
||||
return answer;
|
||||
}
|
||||
|
||||
function deposit() public payable {}
|
||||
fallback() external payable {}
|
||||
receive() external payable {}
|
||||
}
|
Loading…
Reference in New Issue