From e6252db6b7a40b59550c533eba913b0778361962 Mon Sep 17 00:00:00 2001 From: six <51x@keemail.me> Date: Wed, 5 Oct 2022 20:05:05 +0200 Subject: [PATCH] Init --- README.md | 41 ++++++++++- ink/token_minter.ink | 0 python/bridge.py | 0 python/eth_keygen.py | 112 ++++++++++++++++++++++++++++++ python/forgery.py | 0 solidity/multichain_safu.sol | 128 +++++++++++++++++++++++++++++++++++ 6 files changed, 280 insertions(+), 1 deletion(-) create mode 100644 ink/token_minter.ink create mode 100644 python/bridge.py create mode 100644 python/eth_keygen.py create mode 100644 python/forgery.py create mode 100644 solidity/multichain_safu.sol diff --git a/README.md b/README.md index ab3053d..cc121e3 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,42 @@ # pwn_w3bridges -Workshop for web3 bridge hacking at Hacktivity 2022 \ No newline at end of file +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 +$ diff --git a/ink/token_minter.ink b/ink/token_minter.ink new file mode 100644 index 0000000..e69de29 diff --git a/python/bridge.py b/python/bridge.py new file mode 100644 index 0000000..e69de29 diff --git a/python/eth_keygen.py b/python/eth_keygen.py new file mode 100644 index 0000000..2858aea --- /dev/null +++ b/python/eth_keygen.py @@ -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.") diff --git a/python/forgery.py b/python/forgery.py new file mode 100644 index 0000000..e69de29 diff --git a/solidity/multichain_safu.sol b/solidity/multichain_safu.sol new file mode 100644 index 0000000..9f683ec --- /dev/null +++ b/solidity/multichain_safu.sol @@ -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 {} +}