From 9f3cd571ddcc14674e5f6c72b93774b2b99195aa Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Sun, 21 Jul 2019 00:55:12 +0300 Subject: [PATCH 01/74] Reformat code. Activate deploy for all commits. --- .circleci/config.yml | 9 ++++++--- theo/interfaces/cli.py | 7 +++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index a005a9b..5adf8c6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -77,10 +77,13 @@ workflows: tags: only: /.*/ - pypi_release: + # filters: + # branches: + # ignore: /.*/ + # tags: + # only: /v[0-9]+(\.[0-9]+(dev|)([0-9]+|))*/ filters: - branches: - ignore: /.*/ tags: - only: /v[0-9]+(\.[0-9]+(dev|)([0-9]+|))*/ + only: /.*/ requires: - test diff --git a/theo/interfaces/cli.py b/theo/interfaces/cli.py index 6a8d4a8..a761bbc 100644 --- a/theo/interfaces/cli.py +++ b/theo/interfaces/cli.py @@ -29,9 +29,7 @@ def main(): parser.add_argument("--account-pk", help="The account's private key") # Contract to monitor - parser.add_argument( - "--contract", help="Contract to monitor", metavar="ADDRESS" - ) + parser.add_argument("--contract", help="Contract to monitor", metavar="ADDRESS") # Find exploits with Mythril parser.add_argument( @@ -48,7 +46,7 @@ def main(): # Print version and exit parser.add_argument( - '--version', action='version', version='Version: {}'.format(__version__) + "--version", action="version", version="Version: {}".format(__version__) ) args = parser.parse_args() @@ -63,6 +61,7 @@ def main(): start_repl(args) + def start_repl(args): exploits = [] From a7374dfcce184e64ae0dd48104639ab34b388ab1 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Sun, 21 Jul 2019 00:59:10 +0300 Subject: [PATCH 02/74] Only trigger pypi release when tag is found. --- .circleci/config.yml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 5adf8c6..a005a9b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -77,13 +77,10 @@ workflows: tags: only: /.*/ - pypi_release: - # filters: - # branches: - # ignore: /.*/ - # tags: - # only: /v[0-9]+(\.[0-9]+(dev|)([0-9]+|))*/ filters: + branches: + ignore: /.*/ tags: - only: /.*/ + only: /v[0-9]+(\.[0-9]+(dev|)([0-9]+|))*/ requires: - test From b30a066d31a6b7002fd8edadd31117633a9b3cbd Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Sun, 21 Jul 2019 00:59:26 +0300 Subject: [PATCH 03/74] Bump version. --- theo/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/theo/version.py b/theo/version.py index 5938482..28005d8 100644 --- a/theo/version.py +++ b/theo/version.py @@ -1 +1 @@ -__version__ = "v0.3.0" +__version__ = "v0.3.1" From 12bc997afbe8ee0c262899d0b48a30fcf135ad74 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Sun, 21 Jul 2019 01:00:58 +0300 Subject: [PATCH 04/74] Update mythril version. --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2f4b284..d7673c7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ web3==4.9.2 -mythril==0.21.8 \ No newline at end of file +mythril==0.21.12 \ No newline at end of file From d5e25d9db7169e57f42d728200d6355c0e34acb6 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Sun, 21 Jul 2019 01:01:14 +0300 Subject: [PATCH 05/74] Bump version. --- theo/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/theo/version.py b/theo/version.py index 28005d8..0d7c0b7 100644 --- a/theo/version.py +++ b/theo/version.py @@ -1 +1 @@ -__version__ = "v0.3.1" +__version__ = "v0.3.2" From b273f15a29d93e46c5e7fbddd2174268c4a300f9 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Sun, 21 Jul 2019 01:58:28 +0300 Subject: [PATCH 06/74] Bump version. --- theo/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/theo/version.py b/theo/version.py index 0d7c0b7..dda920b 100644 --- a/theo/version.py +++ b/theo/version.py @@ -1 +1 @@ -__version__ = "v0.3.2" +__version__ = "v0.3.3" From 2911f73c4694b8e7b584561f0eeb9007d79996cb Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Sun, 21 Jul 2019 02:05:21 +0300 Subject: [PATCH 07/74] Set deadline for Readme rewrite. --- Readme.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Readme.md b/Readme.md index 99bc973..88f9469 100644 --- a/Readme.md +++ b/Readme.md @@ -1,3 +1,5 @@ +**Readme is obsolete; will be rewritten until 2019-08-31.** + # Theo Theo is a great hacker showing the other script kiddies how things should be done. From ac45841aa94bdd72174af0abf54396107cce784a Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Mon, 22 Jul 2019 11:16:03 +0300 Subject: [PATCH 08/74] Add home url. --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 7964633..1ca710f 100644 --- a/setup.py +++ b/setup.py @@ -42,4 +42,5 @@ def run(self): long_description_content_type="text/markdown", entry_points={"console_scripts": ["theo=theo.interfaces.cli:main"]}, cmdclass={"verify": VerifyVersionCommand}, + url="https://github.com/cleanunicorn/theo", ) From e1d7c68821756db15ac5ee1bcf48521b006bac5d Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Mon, 22 Jul 2019 11:16:22 +0300 Subject: [PATCH 09/74] Add more examples in the exploits JSON file. --- Readme.md | 99 +++++++++++++++++++++++++++++++++++++---- test/input-tx.json | 23 ++++++---- theo/exploit/exploit.py | 8 ++-- theo/file/__init__.py | 29 +++++++----- theo/interfaces/cli.py | 50 ++++++++++++++------- 5 files changed, 160 insertions(+), 49 deletions(-) diff --git a/Readme.md b/Readme.md index 88f9469..44a672b 100644 --- a/Readme.md +++ b/Readme.md @@ -2,7 +2,14 @@ # Theo -Theo is a great hacker showing the other script kiddies how things should be done. +Theo aims to be an exploitation framework or a blockchain recon and interaction tool. + +Features: + +- automatic smart contract scanning which generates a list of possible exploits. +- generating and sending transactions to exploit a smart contract. +- waiting for an actor to interact with a monitored smart contract, in order to frontrun them. +- web3 console ![Theo](./static/theo-profile.png) @@ -14,25 +21,77 @@ Theo's purpose is to fight script kiddies that try to be leet hackers. He can li ## Install +Theo is available as a PyPI package: + +```console +$ pip install theo +$ theo --help +usage: theo [-h] [--rpc-http RPC_HTTP] [--rpc-ws RPC_WS] [--rpc-ipc RPC_IPC] + [--account-pk ACCOUNT_PK] [--contract ADDRESS] + [--skip-mythril SKIP_MYTHRIL] [--load-file LOAD_FILE] [--version] + +Monitor contracts for balance changes or tx pool. + +optional arguments: + -h, --help show this help message and exit + --rpc-http RPC_HTTP Connect to this HTTP RPC (default: + http://127.0.0.1:8545) + --account-pk ACCOUNT_PK + The account's private key (default: None) + --contract ADDRESS Contract to monitor (default: None) + --skip-mythril SKIP_MYTHRIL + Don't try to find exploits with Mythril (default: + False) + --load-file LOAD_FILE + Load exploit from file (default: ) + --version show program's version number and exit + +RPC connections: + --rpc-ws RPC_WS Connect to this WebSockets RPC (default: None) + --rpc-ipc RPC_IPC Connect to this IPC RPC (default: None) +``` + +Install from sources + ```console $ git clone https://github.com/cleanunicorn/theo $ cd theo +$ virtualenv ./venv +$ . ./venv/bin/activate $ pip install -r requirements.txt +$ pip install -e . +$ theo --help ``` -It's recommended to use [virtualenv](https://virtualenv.pypa.io/en/latest/) if you're familiar with it. - Requirements: -- Python 3.5 or higher -- An Ethereum node with RPC available -- Accounts unlocked to be able to send transactions +- Python 3.5 or higher. +- An Ethereum node with RPC available. [Ganache](https://github.com/trufflesuite/ganache-cli) works really well for testing or for validating exploits. + +## Demos -## Demo +### Find exploit and run it -[Scrooge McEtherface](https://github.com/b-mueller/scrooge-mcetherface) tries to exploit a contract but Theo is able to successfully frontrun him. +Scan a smart contract, find exploits, exploit it: -[![asciicast](https://asciinema.org/a/KVbZpYZee39eWavEwiXMaemPI.svg)](https://asciinema.org/a/KVbZpYZee39eWavEwiXMaemPI) +- Start Ganache as our local Ethereum node +- Deploy the vulnerable contract (happens in a different window) +- Scan for exploits +- Run exploit + +[![asciicast](https://asciinema.org/a/CgTH8tIAoGsgEYsd7XN65tJSp.svg)](https://asciinema.org/a/CgTH8tIAoGsgEYsd7XN65tJSp) + +### Frontrun victim + +Setup a honeypot, deploy honeypot, wait for attacker, frontrun: + +- Start geth as our local Ethereum node +- Start mining +- Deploy the honeypot +- Start Theo and scan the mem pool for transactions +- Frontrun the attacker and steal his ether + +[![asciicast](https://asciinema.org/a/n2HnSJvgopf8AKCoSfEJVgvxU.svg)](https://asciinema.org/a/n2HnSJvgopf8AKCoSfEJVgvxU?speed=2) ## Usage @@ -158,3 +217,25 @@ Type "help", "copyright", "credits" or "license" for more information. Waiting for a victim to reach into the honey jar. Listening for Transaction: {'input': '0x4e71e0c8', 'value': '0xde0b6b3a7640000'}. ``` + +# Troubleshooting + +## openssl/aes.h: No such file or directory + +If you get this error, you need the libssl source libraries: + +``` + scrypt-1.2.1/libcperciva/crypto/crypto_aes.c:6:10: fatal error: openssl/aes.h: No such file or directory + #include + ^~~~~~~~~~~~~~~ + compilation terminated. + error: command 'x86_64-linux-gnu-gcc' failed with exit status 1 + + ---------------------------------------- +Command "/usr/bin/python3 -u -c "import setuptools, tokenize;__file__='/tmp/pip-build-5rl4ep94/scrypt/setup.py';f=getattr(tokenize, 'open', open)(__file__);code=f.read().replace('\r\n', '\n');f.close();exec(compile(code, __file__, 'exec'))" install --record /tmp/pip-mnbzx9qe-record/install-record.txt --single-version-externally-managed --compile" failed with error code 1 in /tmp/pip-build-5rl4ep94/scrypt/ +``` + +On Ubuntu you can install them with: +```console +$ sudo apt install libssl-dev +``` \ No newline at end of file diff --git a/test/input-tx.json b/test/input-tx.json index 8c416ff..bab63d8 100644 --- a/test/input-tx.json +++ b/test/input-tx.json @@ -1,10 +1,17 @@ [ - { - "input": "0x4e71e0c8", - "value": 100000000000000000 - }, - { - "input": "0x2e64cec1", - "value": "0x0" - } + [ + { + "input": "0x4e71e0c8", + "value": 100000000000000000 + }, + { + "input": "0x2e64cec1", + "value": "0x0" + } + ], + [ + { + "input": "0xdeadbeef" + } + ] ] \ No newline at end of file diff --git a/theo/exploit/exploit.py b/theo/exploit/exploit.py index 21fa844..0ac0ed4 100644 --- a/theo/exploit/exploit.py +++ b/theo/exploit/exploit.py @@ -47,7 +47,8 @@ def __init__( logger_stream = logging.StreamHandler() logger_stream.setLevel(verbosity) logger_stream.setFormatter(logging.Formatter("%(asctime)s - %(message)s")) - self.logger.addHandler(logger_stream) + if self.logger.hasHandlers() is False: + self.logger.addHandler(logger_stream) def __repr__(self): return "Exploit: (txs={})".format(self.txs) @@ -143,8 +144,9 @@ def send_tx(self, tx: dict) -> str: tx_receipt = self.w3.eth.waitForTransactionReceipt(tx_hash, timeout=300) self.logger.info("Mined") self.logger.debug("Receipt: {}".format(tx_receipt)) - - return tx_receipt + return tx_receipt + else: + return None def wait_for(self, contract, tx, flush=False): # Setting up filter diff --git a/theo/file/__init__.py b/theo/file/__init__.py index 66a6bf9..efcb98b 100644 --- a/theo/file/__init__.py +++ b/theo/file/__init__.py @@ -9,7 +9,7 @@ def exploits_from_file( file, rpcHTTP=None, rpcWS=None, rpcIPC=None, contract="", account_pk="" ): with open(file) as f: - transaction_list = json.load(f) + exploit_list = json.load(f) if rpcIPC is not None: print("Connecting to IPC: {rpc}.".format(rpc=rpcIPC)) @@ -21,16 +21,21 @@ def exploits_from_file( print("Connecting to HTTP: {rpc}.".format(rpc=rpcHTTP)) w3 = Web3(Web3.HTTPProvider(rpcHTTP)) - txs = [] - for tx in transaction_list: - txs.append(Tx(data=tx.get("input", "0x"), value=tx.get("value", 0))) + exploits = [] - exploit = Exploit( - txs=txs, - w3=w3, - contract=contract, - account=private_key_to_account(account_pk), - account_pk=account_pk, - ) + for exploit in exploit_list: + txs = [] + for tx in exploit: + txs.append(Tx(data=tx.get("input", "0x"), value=tx.get("value", 0))) - return exploit + exploits.append( + Exploit( + txs=txs, + w3=w3, + contract=contract, + account=private_key_to_account(account_pk), + account_pk=account_pk, + ) + ) + + return exploits diff --git a/theo/interfaces/cli.py b/theo/interfaces/cli.py index a761bbc..f679677 100644 --- a/theo/interfaces/cli.py +++ b/theo/interfaces/cli.py @@ -2,6 +2,8 @@ import argparse import code import json +import getpass +from web3 import Web3 from theo.version import __version__ from theo.server import Server from theo.scanner import exploits_from_mythril @@ -29,14 +31,16 @@ def main(): parser.add_argument("--account-pk", help="The account's private key") # Contract to monitor - parser.add_argument("--contract", help="Contract to monitor", metavar="ADDRESS") + parser.add_argument( + "--contract", help="Contract to interact with", metavar="ADDRESS" + ) # Find exploits with Mythril parser.add_argument( "--skip-mythril", - type=bool, - help="Don't try to find exploits with Mythril", + help="Skip scanning the contract with Mythril", default=False, + action="store_true", ) # Load exploits from file @@ -53,11 +57,13 @@ def main(): # Get account from the private key if args.account_pk is None: - args.account_pk = input("Enter a private key: ") + args.account_pk = getpass.getpass( + prompt="The account's private key (input hidden)\n> " + ) args.account = private_key_to_account(args.account_pk) if args.contract is None: - args.contract = input("Enter a contract to scan: ") + args.contract = input("Contract to interact with\n> ") start_repl(args) @@ -80,21 +86,22 @@ def start_repl(args): account_pk=args.account_pk, ) if args.load_file != "": - exploits += [ - exploits_from_file( - file=args.load_file, - rpcHTTP=args.rpc_http, - rpcWS=args.rpc_ws, - rpcIPC=args.rpc_ipc, - contract=args.contract, - account_pk=args.account_pk, - ) - ] + exploits += exploits_from_file( + file=args.load_file, + rpcHTTP=args.rpc_http, + rpcWS=args.rpc_ws, + rpcIPC=args.rpc_ipc, + contract=args.contract, + account_pk=args.account_pk, + ) if len(exploits) == 0: print("No exploits found. You're going to need to load some exploits.") else: - print("Found exploits(s)", exploits) + print("Found exploits(s):\n", exploits) + + # Create a web3 instance + w3 = Web3(Web3.HTTPProvider(args.rpc_http)) # Load history history_path = "./.theo_history" @@ -122,6 +129,15 @@ def save_history(historyPath=history_path): readline.set_completer(rlcompleter.Completer(vars).complete) readline.parse_and_bind("tab: complete") del os, atexit, readline, rlcompleter, save_history - code.InteractiveConsole(vars).interact() + code.InteractiveConsole(vars).interact( + banner=""" +A few objects are available in the console: +- `exploits` is an array of loaded exploits found by Mythril or read from a file +- `w3` an initialized instance of web3py for the provided HTTP RPC endpoint + +Check the readme for more info: +https://github.com/cleanunicorn/theo +""" + ) print("Shutting down") From 77d93154bb77bd30a3bee28039ab36453f629c05 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Mon, 22 Jul 2019 11:28:07 +0300 Subject: [PATCH 10/74] Improve Readme. --- Readme.md | 112 +++++++++++++++++++++++++++--------------------------- 1 file changed, 55 insertions(+), 57 deletions(-) diff --git a/Readme.md b/Readme.md index 44a672b..5d37fda 100644 --- a/Readme.md +++ b/Readme.md @@ -1,5 +1,3 @@ -**Readme is obsolete; will be rewritten until 2019-08-31.** - # Theo Theo aims to be an exploitation framework or a blockchain recon and interaction tool. @@ -100,77 +98,89 @@ Setup a honeypot, deploy honeypot, wait for attacker, frontrun: It's a good idea to check the help screen first. ```console -$ python ./theo.py --help -usage: theo.py [-h] [--rpc-http RPC_HTTP] [--rpc-ws RPC_WS] - [--rpc-ipc RPC_IPC] [--account ACCOUNT] [--contract ADDRESS] - [--txs {mythril,file}] [--txs-file FILE] - {tx-pool} +$ theo --help +usage: theo [-h] [--rpc-http RPC_HTTP] [--rpc-ws RPC_WS] [--rpc-ipc RPC_IPC] + [--account-pk ACCOUNT_PK] [--contract ADDRESS] [--skip-mythril] + [--load-file LOAD_FILE] [--version] Monitor contracts for balance changes or tx pool. -positional arguments: - {tx-pool} Choose between: balance (not implemented: monitor - contract balance changes), tx-pool (if any - transactions want to call methods). - optional arguments: -h, --help show this help message and exit - --contract ADDRESS Contract to monitor - -Monitor transaction pool: - --rpc-http RPC_HTTP Connect to this HTTP RPC - --rpc-ws RPC_WS Connect to this WebSockets RPC - --rpc-ipc RPC_IPC Connect to this IPC RPC - --account ACCOUNT Use this account to send transactions from - -Transactions to wait for: - --txs {mythril,file} Choose between: mythril (find transactions - automatically with mythril), file (use the - transactions specified in a JSON file). - --txs-file FILE The file which contains the transactions to frontrun + --rpc-http RPC_HTTP Connect to this HTTP RPC (default: + http://127.0.0.1:8545) + --account-pk ACCOUNT_PK + The account's private key (default: None) + --contract ADDRESS Contract to interact with (default: None) + --skip-mythril Skip scanning the contract with Mythril (default: + False) + --load-file LOAD_FILE + Load exploit from file (default: ) + --version show program's version number and exit + +RPC connections: + --rpc-ws RPC_WS Connect to this WebSockets RPC (default: None) + --rpc-ipc RPC_IPC Connect to this IPC RPC (default: None) ``` ### Symbolic execution -A list of expoits is automatically identified using [mythril](https://github.com/ConsenSys/mythril). +A list of exploits is automatically identified using [mythril](https://github.com/ConsenSys/mythril). Start a session by running: ```console -$ python ./theo.py tx-pool --account= --contract= +$ theo --contract= --account-pk= +Scanning for exploits in contract: 0xa586074fa4fe3e546a132a16238abe37951d41fe +Connecting to HTTP: http://127.0.0.1:8545. +Found exploits(s): + [Exploit: (txs=[Transaction {Data: 0xcf7a8965, Value: 1000000000000000000}])] + +A few objects are available in the console: +- `exploits` is an array of loaded exploits found by Mythril or read from a file +- `w3` an initialized instance of web3py for the provided HTTP RPC endpoint + +Check the readme for more info: +https://github.com/cleanunicorn/theo + +>>> ``` It will analyze the contract and will find a list of available exploits. +You can see the available exploits found. In this case one exploit was found. Each exploit is an [Exploit](https://github.com/cleanunicorn/theo/blob/master/theo/exploit/exploit.py) object. + ```console -$ python theo.py tx-pool --account=0xffcf8fdee72ac11b5c542428b35eef5769c409f0 --contract=0xd833215cbcc3f914bd1c9ece3ee7bf8b14f841bb -Scanning for exploits in contract: 0xd833215cbcc3f914bd1c9ece3ee7bf8b14f841bb -Found exploit(s) [Exploit: (txs=[Transaction: {'input': '0xcf7a8965', 'value': '0xde0b6b3a7640000'}])] -Python 3.7.3 (default, Jun 24 2019, 04:54:02) -[GCC 9.1.0] on linux -Type "help", "copyright", "credits" or "license" for more information. -(InteractiveConsole) ->>> +>>> exploits[0] +Exploit: (txs=[Transaction: {'input': '0xcf7a8965', 'value': '0xde0b6b3a7640000'}]) ``` -You can see the available exploits found. In this case one exploit was found. Each exploit is an [Exploit](https://github.com/cleanunicorn/theo/blob/263dc9f0cd34c4a0904529128c93f30b29eae415/theo/scanner/__init__.py#L9) object, having a list of transactions to exploit a bug. +### Running exploits + +The exploit steps can be run by calling `.execute()` on the exploit object. The transactions will be signed and sent to the node you're connected to. ```console ->>> exploits[0] -Exploit: (txs=[Transaction: {'input': '0xcf7a8965', 'value': '0xde0b6b3a7640000'}]) +>>> exploits[0].execute() +2019-07-22 11:26:12,196 - Sending tx: {'to': '0xA586074FA4Fe3E546A132a16238abe37951D41fE', 'gasPrice': 1, 'gas': 30521, 'value': 1000000000000000000, 'data': '0xcf7a8965', 'nonce': 47} +2019-07-22 11:26:12,200 - Waiting for 0x41b489c78f654cab0b0451fc573010ddb20ee6437cdbf5098b6b03ee1936c33c to be mined... +2019-07-22 11:26:16,337 - Mined +2019-07-22 11:26:16,341 - Initial balance: 1155999450759997797167 (1156.00 ether) +2019-07-22 11:26:16,342 - Final balance: 1156999450759997768901 (1157.00 ether) ``` -You can start the frontrunning monitor to listen for other hackers (script kiddies really) trying to exploit his honeypots. +### Frontrunning -Use `.frontrun()` to start listening for the exploit and when found send a transaction with a higher gas price. +You can start the frontrunning monitor to listen for other hackers trying to exploit the honeypot. + +Use `.frontrun()` to start listening for the exploit and when found, send a transaction with a higher gas price. ```console >>> exploits[0].frontrun() -Waiting for a victim to reach into the honey jar. -Listening for Transaction: {'input': '0xcf7a8965', 'value': '0xde0b6b3a7640000'}. -Found pending tx: 0x74eb78557b4659f27e7a8b82804ae97be9d0adfefd6a5652a097045f6de77a0b from: 0x1df62f291b2e969fb0849d99d9ce41e2f137006e. -Frontrunning with tx: {'from': '0xffcf8fdee72ac11b5c542428b35eef5769c409f0', 'to': '0xd833215cbcc3f914bd1c9ece3ee7bf8b14f841bb', 'gasPrice': '0x3b9aca01', 'input': '0xcf7a8965', 'gas': '0x4c4b40', 'value': '0xde0b6b3a7640000'} -Mined transaction: 0x0b5e7ceedd600eaf013ca8bc74900e6d29b25ed422baaa776f42bec01870a288 +2019-07-22 11:22:26,285 - Scanning the mem pool for transactions... +2019-07-22 11:22:45,369 - Found tx: 0xf6041abe6e547cea93e80a451fdf53e6bdae67820244246fde44098f91ce1c20 +2019-07-22 11:22:45,375 - Sending tx: {'to': '0xA586074FA4Fe3E546A132a16238abe37951D41fE', 'gasPrice': '0x2', 'data': '0xcf7a8965', 'gas': 30522, 'value': 1000000000000000000, 'nonce': 45} +2019-07-22 11:22:45,380 - Waiting for 0xa73316daf806e7eef83d09e467c32ce5faa239c6eda3a270a8ce7a7aae48fb7e to be mined... +2019-07-22 11:22:56,852 - Mined ``` > "Oh, my God! The quarterback is toast!" @@ -206,18 +216,6 @@ Create a file that looks like this [input-tx.json](./test/input-tx.json): This one defines 2 exploits, the first one has 2 transactions and the second one only 1 transaction. After the exploits are loaded, frontrunning is the same. -```console -$ python ./theo.py --txs=file --contract=0xe78a0f7e598cc8b0bb87894b0f60dd2a88d6a8ab --account=0xffcf8fdee72ac11b5c542428b35eef5769c409f0 --txs-file=./test/input-tx.json tx-pool 130 ↵ -Found exploits(s) [Exploit: (txs=[Transaction: {'input': '0x4e71e0c8', 'value': '0xde0b6b3a7640000'}, Transaction: {'input': '0x2e64cec1', 'value': '0x0'}]), Exploit: (txs=[Transaction: {'input': '0x4e71e0c8', 'value': '0xde0b6b3a7640000'}])] -Python 3.7.3 (default, Jun 24 2019, 04:54:02) -[GCC 9.1.0] on linux -Type "help", "copyright", "credits" or "license" for more information. -(InteractiveConsole) ->>> exploits[0].frontrun() -Waiting for a victim to reach into the honey jar. -Listening for Transaction: {'input': '0x4e71e0c8', 'value': '0xde0b6b3a7640000'}. -``` - # Troubleshooting ## openssl/aes.h: No such file or directory From 92a199196d861545e7fa0d341eb3b4037ab9629d Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Mon, 22 Jul 2019 11:29:21 +0300 Subject: [PATCH 11/74] Increase demo speed to 2. --- Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index 5d37fda..9f8e2f9 100644 --- a/Readme.md +++ b/Readme.md @@ -77,7 +77,7 @@ Scan a smart contract, find exploits, exploit it: - Scan for exploits - Run exploit -[![asciicast](https://asciinema.org/a/CgTH8tIAoGsgEYsd7XN65tJSp.svg)](https://asciinema.org/a/CgTH8tIAoGsgEYsd7XN65tJSp) +[![asciicast](https://asciinema.org/a/CgTH8tIAoGsgEYsd7XN65tJSp.svg)](https://asciinema.org/a/CgTH8tIAoGsgEYsd7XN65tJSp?speed=2) ### Frontrun victim From c293401f108b4e8efd891392e3364a5ad485dda2 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Mon, 22 Jul 2019 11:39:08 +0300 Subject: [PATCH 12/74] Add Apache2 license. --- License | 190 +++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 189 insertions(+), 1 deletion(-) diff --git a/License b/License index b335484..83f8ce4 100644 --- a/License +++ b/License @@ -1,10 +1,198 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + Copyright 2019 Daniel Luca Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, From eb1d4d343c9a432134f2c982763458c27bdddba8 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Mon, 22 Jul 2019 11:47:34 +0300 Subject: [PATCH 13/74] Add sonarcloud template. --- .sonarcloud.properties | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .sonarcloud.properties diff --git a/.sonarcloud.properties b/.sonarcloud.properties new file mode 100644 index 0000000..3344df9 --- /dev/null +++ b/.sonarcloud.properties @@ -0,0 +1,15 @@ +# Path to sources +#sonar.sources=. +#sonar.exclusions= +#sonar.inclusions= + +# Path to tests +#sonar.tests= +#sonar.test.exclusions= +#sonar.test.inclusions= + +# Source encoding +#sonar.sourceEncoding=UTF-8 + +# Exclusions for copy-paste detection +#sonar.cpd.exclusions= \ No newline at end of file From bbacf5c39fe77c2d556d9147d71a795807afca18 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Mon, 22 Jul 2019 11:49:06 +0300 Subject: [PATCH 14/74] Update readme with shields. --- Readme.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index 9f8e2f9..a74ed36 100644 --- a/Readme.md +++ b/Readme.md @@ -1,6 +1,12 @@ # Theo -Theo aims to be an exploitation framework or a blockchain recon and interaction tool. +![License](https://img.shields.io/github/license/cleanunicorn/theo.svg) +[![CircleCI](https://circleci.com/gh/cleanunicorn/theo/tree/master.svg?style=shield)](https://circleci.com/gh/cleanunicorn/theo) +[![Codacy Badge](https://api.codacy.com/project/badge/Grade/71da66211eff42f298062a883b7fa5e9)](https://www.codacy.com/app/lucadanielcostin/theo) +[![PyPI](https://img.shields.io/pypi/v/theo.svg)](https://pypi.org/project/theo/) +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) + +Theo aims to be an exploitation framework and a blockchain recon and interaction tool. Features: From cc5af8337bb121098848094fd7294c0feb8f18bf Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Mon, 22 Jul 2019 11:53:54 +0300 Subject: [PATCH 15/74] Housekeeping. --- theo/exploit/exploit.py | 4 ++-- theo/exploit/tx.py | 5 +---- theo/interfaces/cli.py | 1 - theo/server/__init__.py | 39 --------------------------------------- theo/server/__main__.py | 0 5 files changed, 3 insertions(+), 46 deletions(-) delete mode 100644 theo/server/__init__.py delete mode 100644 theo/server/__main__.py diff --git a/theo/exploit/exploit.py b/theo/exploit/exploit.py index 0ac0ed4..473ce62 100644 --- a/theo/exploit/exploit.py +++ b/theo/exploit/exploit.py @@ -166,8 +166,8 @@ def wait_for(self, contract, tx, flush=False): self.logger.debug( "Processing {} transactions.".format(len(pending_txs_hashes)) ) - for hash in pending_txs_hashes: - pending_tx = self.w3.eth.getTransaction(hash) + for tx_hash in pending_txs_hashes: + pending_tx = self.w3.eth.getTransaction(tx_hash) # Skip some uninteresting transactions if (pending_tx is None) or (pending_tx.get("to") is None): diff --git a/theo/exploit/tx.py b/theo/exploit/tx.py index 1d32fd5..8e37f7e 100644 --- a/theo/exploit/tx.py +++ b/theo/exploit/tx.py @@ -1,12 +1,9 @@ -from web3 import Web3 - - class Tx: def __init__(self, data: str, value: str): # Transaction input (data) self.data = data # Transaction value - if type(value) == str: + if isinstance(value, str): # Transform from hex string if value[0:2].lower() == "0x": self.value = int(value, base=16) diff --git a/theo/interfaces/cli.py b/theo/interfaces/cli.py index f679677..d2188fd 100644 --- a/theo/interfaces/cli.py +++ b/theo/interfaces/cli.py @@ -1,7 +1,6 @@ # from argparse_prompt import PromptParser import argparse import code -import json import getpass from web3 import Web3 from theo.version import __version__ diff --git a/theo/server/__init__.py b/theo/server/__init__.py deleted file mode 100644 index 08e9f09..0000000 --- a/theo/server/__init__.py +++ /dev/null @@ -1,39 +0,0 @@ -from http.server import BaseHTTPRequestHandler, HTTPServer - - -def MakeHTTPRequestHandler(context): - class HTTPRequestHandler(BaseHTTPRequestHandler): - def __init__(self, *args, **kwargs): - super(HTTPRequestHandler, self).__init__(*args, **kwargs) - - def do_GET(self): - # https://docs.python.org/3.7/library/http.server.html?highlight=basehttprequesthandler#http.server.BaseHTTPRequestHandler - self.send_response(200) - self.send_header("Content-type", "text") - self.end_headers() - - print("Context", context) - - self.wfile.write(bytes(self.path, "UTF-8")) - - return HTTPRequestHandler - - -class Server: - host = None - port = None - - def __init__(self, host, port): - self.host = host - self.port = port - - def start(self): - handler = MakeHTTPRequestHandler(context={"server": "postgres"}) - - httpd = HTTPServer((self.host, self.port), handler) - - try: - httpd = httpd.serve_forever() - except KeyboardInterrupt: - httpd.socket.close() - pass diff --git a/theo/server/__main__.py b/theo/server/__main__.py deleted file mode 100644 index e69de29..0000000 From 60742db76fb93f9b42da91ad56753b4e82f03b42 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Mon, 22 Jul 2019 11:54:57 +0300 Subject: [PATCH 16/74] Bump version. --- theo/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/theo/version.py b/theo/version.py index dda920b..d123ede 100644 --- a/theo/version.py +++ b/theo/version.py @@ -1 +1 @@ -__version__ = "v0.3.3" +__version__ = "v0.4.0" From f94aca0b60f765c837cde534037f8d8b5f1e489a Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Mon, 22 Jul 2019 12:00:55 +0300 Subject: [PATCH 17/74] Fix readme typo. --- Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index a74ed36..5f69333 100644 --- a/Readme.md +++ b/Readme.md @@ -74,7 +74,7 @@ Requirements: ## Demos -### Find exploit and run it +### Find exploit and execute it Scan a smart contract, find exploits, exploit it: From f99d009907c3e7733a3436a2453a72d88db251b6 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Mon, 22 Jul 2019 13:15:04 +0300 Subject: [PATCH 18/74] Completely remove the server module. --- theo/interfaces/cli.py | 1 - theo/version.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/theo/interfaces/cli.py b/theo/interfaces/cli.py index d2188fd..33cf48a 100644 --- a/theo/interfaces/cli.py +++ b/theo/interfaces/cli.py @@ -4,7 +4,6 @@ import getpass from web3 import Web3 from theo.version import __version__ -from theo.server import Server from theo.scanner import exploits_from_mythril from theo.file import exploits_from_file from theo import private_key_to_account diff --git a/theo/version.py b/theo/version.py index d123ede..ed6df84 100644 --- a/theo/version.py +++ b/theo/version.py @@ -1 +1 @@ -__version__ = "v0.4.0" +__version__ = "v0.4.1" From 243631bf94fc658d4db141e8ad46530ebe1030e3 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Mon, 22 Jul 2019 13:56:47 +0300 Subject: [PATCH 19/74] Display balance before and after a frontrun attack. --- theo/exploit/exploit.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/theo/exploit/exploit.py b/theo/exploit/exploit.py index 473ce62..bfb9092 100644 --- a/theo/exploit/exploit.py +++ b/theo/exploit/exploit.py @@ -97,6 +97,8 @@ def frontrun(self, flush=False): nonce = self.w3.eth.getTransactionCount(self.account) + initial_balance = self.w3.eth.getBalance(self.account) + # Wait for each tx and frontrun it. for tx in self.txs: victim_tx = self.wait_for(self.contract, tx, flush=flush) @@ -130,6 +132,18 @@ def frontrun(self, flush=False): else: self.send_tx(frontrun_tx) + final_balance = self.w3.eth.getBalance(self.account) + self.logger.info( + "Initial balance: \t{balance} ({balance_ether:.2f} ether)".format( + balance=initial_balance, balance_ether=initial_balance / 10 ** 18 + ) + ) + self.logger.info( + "Final balance: \t{balance} ({balance_ether:.2f} ether)".format( + balance=final_balance, balance_ether=final_balance / 10 ** 18 + ) + ) + def send_tx(self, tx: dict) -> str: # Make sure the addresses are checksummed. tx["to"] = Web3.toChecksumAddress(tx["to"]) From 6c7be749112af58dce5b6501d34b318b1beb4ce1 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Mon, 22 Jul 2019 13:57:00 +0300 Subject: [PATCH 20/74] Checksum the contract and the account. --- theo/interfaces/cli.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/theo/interfaces/cli.py b/theo/interfaces/cli.py index 33cf48a..35c1f5f 100644 --- a/theo/interfaces/cli.py +++ b/theo/interfaces/cli.py @@ -63,6 +63,9 @@ def main(): if args.contract is None: args.contract = input("Contract to interact with\n> ") + args.contract = Web3.toChecksumAddress(args.contract) + args.account = Web3.toChecksumAddress(args.account) + start_repl(args) From e21522caa03d9991c1b340a3153b5f7eaff37332 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Mon, 22 Jul 2019 13:57:12 +0300 Subject: [PATCH 21/74] Bump version. --- theo/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/theo/version.py b/theo/version.py index ed6df84..5a33f25 100644 --- a/theo/version.py +++ b/theo/version.py @@ -1 +1 @@ -__version__ = "v0.4.1" +__version__ = "v0.4.2" From d70aec7c686c224e577b405079e63d2e67330cd9 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Mon, 22 Jul 2019 18:08:54 +0300 Subject: [PATCH 22/74] Add new contract resistant to proxies. --- contracts/VulnerableNoContract.sol | 42 ++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 contracts/VulnerableNoContract.sol diff --git a/contracts/VulnerableNoContract.sol b/contracts/VulnerableNoContract.sol new file mode 100644 index 0000000..8be09b4 --- /dev/null +++ b/contracts/VulnerableNoContract.sol @@ -0,0 +1,42 @@ +contract VulnerableTwoStep { + address payable public owner; + bool public owner_reset = false; + uint256 public owner_set_block = 0; + + constructor() public payable { + owner = msg.sender; + } + + function() external payable {} + + function become_owner() public payable { + require(msg.value == 1 ether); + require(!isContract(msg.sender)); + + if (owner_reset == false) { + owner_reset = true; + owner_set_block = block.number; + owner = msg.sender; + } + } + + function retrieve() public payable { + require(block.number > owner_set_block); + require(owner_set_block > 0); + require(!isContract(owner)); + + owner.transfer(address(this).balance); + } + + function isContract(address _addr) + private + view + returns (bool) + { + uint32 size; + assembly { + size := extcodesize(_addr) + } + return (size > 0); + } +} \ No newline at end of file From 9e50da8cada9742a3ff3387c026d924e31bf0cdb Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Mon, 22 Jul 2019 18:09:17 +0300 Subject: [PATCH 23/74] Rename steal to retrieve. --- contracts/VulnerableOneStep.sol | 2 +- contracts/VulnerableTwoStep.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/VulnerableOneStep.sol b/contracts/VulnerableOneStep.sol index 230de6b..c4c6237 100644 --- a/contracts/VulnerableOneStep.sol +++ b/contracts/VulnerableOneStep.sol @@ -9,7 +9,7 @@ contract Vulnerable { function() external payable {} - function steal() public payable { + function retrieve() public payable { require(msg.value >= 1 ether); msg.sender.transfer(address(this).balance); diff --git a/contracts/VulnerableTwoStep.sol b/contracts/VulnerableTwoStep.sol index b0fc89f..5dbf80b 100644 --- a/contracts/VulnerableTwoStep.sol +++ b/contracts/VulnerableTwoStep.sol @@ -18,7 +18,7 @@ contract VulnerableTwoStep { } } - function steal() public payable { + function retrieve() public payable { owner.transfer(address(this).balance); } } \ No newline at end of file From d78e3c8fee22c6a6349cabfbcac857cc4ce7e2d9 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Mon, 22 Jul 2019 18:10:02 +0300 Subject: [PATCH 24/74] Reformat code. --- theo/exploit/exploit.py | 2 +- theo/version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/theo/exploit/exploit.py b/theo/exploit/exploit.py index bfb9092..1ff33e5 100644 --- a/theo/exploit/exploit.py +++ b/theo/exploit/exploit.py @@ -142,7 +142,7 @@ def frontrun(self, flush=False): "Final balance: \t{balance} ({balance_ether:.2f} ether)".format( balance=final_balance, balance_ether=final_balance / 10 ** 18 ) - ) + ) def send_tx(self, tx: dict) -> str: # Make sure the addresses are checksummed. diff --git a/theo/version.py b/theo/version.py index 5a33f25..ef32fb9 100644 --- a/theo/version.py +++ b/theo/version.py @@ -1 +1 @@ -__version__ = "v0.4.2" +__version__ = "v0.4.3" From dec1d96a6c1014c1a290135d11ade56d6a3a9576 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Mon, 22 Jul 2019 22:17:34 +0300 Subject: [PATCH 25/74] Can specify start nonce when sending transactions. --- theo/exploit/exploit.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/theo/exploit/exploit.py b/theo/exploit/exploit.py index 1ff33e5..a8f95a8 100644 --- a/theo/exploit/exploit.py +++ b/theo/exploit/exploit.py @@ -53,10 +53,14 @@ def __init__( def __repr__(self): return "Exploit: (txs={})".format(self.txs) - def execute(self): + def execute(self, nonce=None): receipts = [] - nonce = self.w3.eth.getTransactionCount(self.account) + if nonce is None: + nonce_index = self.w3.eth.getTransactionCount(self.account) + else: + nonce_index = nonce + initial_balance = self.w3.eth.getBalance(self.account) for tx in self.txs: @@ -68,9 +72,9 @@ def execute(self): "data": tx.data.replace( "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef", self.account[2:] ), - "nonce": nonce, + "nonce": nonce_index, } - nonce += 1 + nonce_index += 1 # Estimate gas if self.gas_estimate is True: @@ -92,10 +96,13 @@ def execute(self): self.logger.debug(receipts) - def frontrun(self, flush=False): + def frontrun(self, flush=False, nonce_index=None): self.logger.info("Scanning the mem pool for transactions...") - nonce = self.w3.eth.getTransactionCount(self.account) + if nonce_index is None: + nonce_index = self.w3.eth.getTransactionCount(self.account) + else: + nonce_index = nonce_index initial_balance = self.w3.eth.getBalance(self.account) @@ -114,9 +121,9 @@ def frontrun(self, flush=False): ), "gas": victim_tx["gas"] + self.gas_increment, "value": victim_tx["value"], - "nonce": nonce, + "nonce": nonce_index, } - nonce += 1 + nonce_index += 1 # Estimate gas if self.gas_estimate is True: From abaa8096f02f19e5f7cb31dc0ae9c2a5069b4e43 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Mon, 22 Jul 2019 22:18:27 +0300 Subject: [PATCH 26/74] Initialize account when it's loaded from cli flags. --- theo/interfaces/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/theo/interfaces/cli.py b/theo/interfaces/cli.py index 35c1f5f..7553c5b 100644 --- a/theo/interfaces/cli.py +++ b/theo/interfaces/cli.py @@ -58,7 +58,7 @@ def main(): args.account_pk = getpass.getpass( prompt="The account's private key (input hidden)\n> " ) - args.account = private_key_to_account(args.account_pk) + args.account = private_key_to_account(args.account_pk) if args.contract is None: args.contract = input("Contract to interact with\n> ") From 40efe1a2a22c7fc3ce7f07f640888af9c51718d3 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Mon, 22 Jul 2019 22:18:41 +0300 Subject: [PATCH 27/74] Bump version. --- theo/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/theo/version.py b/theo/version.py index ef32fb9..e0442fe 100644 --- a/theo/version.py +++ b/theo/version.py @@ -1 +1 @@ -__version__ = "v0.4.3" +__version__ = "v0.4.4" From 59003e286a51639164ca5bfc3e82c813a28e4aaa Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Tue, 23 Jul 2019 15:13:12 +0300 Subject: [PATCH 28/74] Display value as ether first and wei second. --- theo/exploit/exploit.py | 4 ++-- theo/exploit/tx.py | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/theo/exploit/exploit.py b/theo/exploit/exploit.py index a8f95a8..7a3abba 100644 --- a/theo/exploit/exploit.py +++ b/theo/exploit/exploit.py @@ -84,12 +84,12 @@ def execute(self, nonce=None): final_balance = self.w3.eth.getBalance(self.account) self.logger.info( - "Initial balance: \t{balance} ({balance_ether:.2f} ether)".format( + "Initial balance: \t{balance_ether:.2f} ether ({balance})".format( balance=initial_balance, balance_ether=initial_balance / 10 ** 18 ) ) self.logger.info( - "Final balance: \t{balance} ({balance_ether:.2f} ether)".format( + "Final balance: \t{balance_ether:.2f} ether ({balance})".format( balance=final_balance, balance_ether=final_balance / 10 ** 18 ) ) diff --git a/theo/exploit/tx.py b/theo/exploit/tx.py index 8e37f7e..bce4fa5 100644 --- a/theo/exploit/tx.py +++ b/theo/exploit/tx.py @@ -13,6 +13,8 @@ def __init__(self, data: str, value: str): self.value = int(value) def __repr__(self): - return "Transaction {{Data: {input}, Value: {value}}}".format( - input=self.data, value=self.value + return "Transaction {{Data: {input}, Value: {value_eth:.2f} ether ({value})}}".format( + input=self.data, + value_eth=self.value / 10 ** 18, + value=self.value, ) From f31ef5dccf28c007b6de9c62ed40211d1acfe666 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Tue, 23 Jul 2019 15:13:20 +0300 Subject: [PATCH 29/74] Housekeeping. --- theo/interfaces/cli.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/theo/interfaces/cli.py b/theo/interfaces/cli.py index 7553c5b..51ffb36 100644 --- a/theo/interfaces/cli.py +++ b/theo/interfaces/cli.py @@ -51,6 +51,7 @@ def main(): "--version", action="version", version="Version: {}".format(__version__) ) + # Parse all arguments args = parser.parse_args() # Get account from the private key @@ -114,19 +115,16 @@ def save_history(historyPath=history_path): import os import readline - if os.path.isfile(history_path): readline.read_history_file(history_path) # Trigger history save on exit import atexit - atexit.register(save_history) # Load variables vars = globals() vars.update(locals()) # Start REPL import rlcompleter - readline.set_completer(rlcompleter.Completer(vars).complete) readline.parse_and_bind("tab: complete") del os, atexit, readline, rlcompleter, save_history From 7aa54331159053417116eb5d553428d483c184c9 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Tue, 23 Jul 2019 15:35:40 +0300 Subject: [PATCH 30/74] Add dump method to dump an object to a file. --- requirements.txt | 3 ++- theo/__init__.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d7673c7..eccc9fd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ web3==4.9.2 -mythril==0.21.12 \ No newline at end of file +mythril==0.21.12 +jsonpickle==1.2 \ No newline at end of file diff --git a/theo/__init__.py b/theo/__init__.py index fe7419d..e8093b0 100644 --- a/theo/__init__.py +++ b/theo/__init__.py @@ -1,6 +1,20 @@ +import jsonpickle, time + + def private_key_to_account(pk: str): from eth_keys import keys from web3 import Web3 account = keys.PrivateKey(Web3.toBytes(hexstr=pk)) return account.public_key.to_checksum_address() + + +def dump(ob=None, filename=None): + """Dumps the provided object to a file in json format. + """ + if filename is None: + filename = "{name}.json".format(name=time.time_ns()) + + pickled = jsonpickle.encode(ob) + with open(filename, "w") as f: + f.write(pickled) From e71c962e88118e79d72c3ac45d3be512f89e5da0 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Tue, 23 Jul 2019 15:35:55 +0300 Subject: [PATCH 31/74] Reformat code. --- theo/exploit/tx.py | 4 +--- theo/interfaces/cli.py | 20 +++++++++++++------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/theo/exploit/tx.py b/theo/exploit/tx.py index bce4fa5..b3b21b0 100644 --- a/theo/exploit/tx.py +++ b/theo/exploit/tx.py @@ -14,7 +14,5 @@ def __init__(self, data: str, value: str): def __repr__(self): return "Transaction {{Data: {input}, Value: {value_eth:.2f} ether ({value})}}".format( - input=self.data, - value_eth=self.value / 10 ** 18, - value=self.value, + input=self.data, value_eth=self.value / 10 ** 18, value=self.value ) diff --git a/theo/interfaces/cli.py b/theo/interfaces/cli.py index 51ffb36..abe28e1 100644 --- a/theo/interfaces/cli.py +++ b/theo/interfaces/cli.py @@ -2,11 +2,15 @@ import argparse import code import getpass +import os +import readline +import atexit +import rlcompleter from web3 import Web3 from theo.version import __version__ from theo.scanner import exploits_from_mythril from theo.file import exploits_from_file -from theo import private_key_to_account +from theo import * def main(): @@ -103,7 +107,7 @@ def start_repl(args): print("Found exploits(s):\n", exploits) # Create a web3 instance - w3 = Web3(Web3.HTTPProvider(args.rpc_http)) + w3 = Web3(Web3.HTTPProvider(args.rpc_http, request_kwargs={"timeout": 60})) # Load history history_path = "./.theo_history" @@ -113,30 +117,32 @@ def save_history(historyPath=history_path): readline.write_history_file(history_path) - import os - import readline if os.path.isfile(history_path): readline.read_history_file(history_path) # Trigger history save on exit - import atexit atexit.register(save_history) # Load variables vars = globals() vars.update(locals()) # Start REPL - import rlcompleter readline.set_completer(rlcompleter.Completer(vars).complete) readline.parse_and_bind("tab: complete") del os, atexit, readline, rlcompleter, save_history code.InteractiveConsole(vars).interact( banner=""" + +Theo version {version}. + A few objects are available in the console: - `exploits` is an array of loaded exploits found by Mythril or read from a file - `w3` an initialized instance of web3py for the provided HTTP RPC endpoint Check the readme for more info: https://github.com/cleanunicorn/theo -""" + +""".format( + version=__version__ + ) ) print("Shutting down") From 18b3bde5b0df1f10696b12602597b77f86831ad3 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Tue, 23 Jul 2019 15:42:02 +0300 Subject: [PATCH 32/74] Fix repl integration. --- theo/interfaces/cli.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/theo/interfaces/cli.py b/theo/interfaces/cli.py index abe28e1..d730f4f 100644 --- a/theo/interfaces/cli.py +++ b/theo/interfaces/cli.py @@ -2,7 +2,6 @@ import argparse import code import getpass -import os import readline import atexit import rlcompleter @@ -109,12 +108,13 @@ def start_repl(args): # Create a web3 instance w3 = Web3(Web3.HTTPProvider(args.rpc_http, request_kwargs={"timeout": 60})) + # Imports for REPL + import os, atexit, readline, rlcompleter # Load history history_path = "./.theo_history" def save_history(historyPath=history_path): import readline - readline.write_history_file(history_path) if os.path.isfile(history_path): @@ -130,12 +130,12 @@ def save_history(historyPath=history_path): del os, atexit, readline, rlcompleter, save_history code.InteractiveConsole(vars).interact( banner=""" - Theo version {version}. -A few objects are available in the console: +Tools available in the console: - `exploits` is an array of loaded exploits found by Mythril or read from a file - `w3` an initialized instance of web3py for the provided HTTP RPC endpoint +- `dump()` writing a json representation of an object to a local file Check the readme for more info: https://github.com/cleanunicorn/theo From 10ea3277199efb66c2447ecb6f7147968a71bb7f Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Tue, 23 Jul 2019 15:42:34 +0300 Subject: [PATCH 33/74] Add dump(). --- theo/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/theo/version.py b/theo/version.py index e0442fe..1c7d256 100644 --- a/theo/version.py +++ b/theo/version.py @@ -1 +1 @@ -__version__ = "v0.4.4" +__version__ = "v0.5.0" From bc456ea1507ca2ab82fbc05c8440ae49e402809c Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Tue, 23 Jul 2019 15:45:02 +0300 Subject: [PATCH 34/74] Default Theo's history to homedir. --- theo/interfaces/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/theo/interfaces/cli.py b/theo/interfaces/cli.py index d730f4f..132bd50 100644 --- a/theo/interfaces/cli.py +++ b/theo/interfaces/cli.py @@ -111,7 +111,7 @@ def start_repl(args): # Imports for REPL import os, atexit, readline, rlcompleter # Load history - history_path = "./.theo_history" + history_path = "~/.theo_history" def save_history(historyPath=history_path): import readline From 5592ad97344a8a4d4e976119966d97ca5a6c65c8 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Tue, 23 Jul 2019 15:45:13 +0300 Subject: [PATCH 35/74] Bump version. --- theo/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/theo/version.py b/theo/version.py index 1c7d256..a2434ee 100644 --- a/theo/version.py +++ b/theo/version.py @@ -1 +1 @@ -__version__ = "v0.5.0" +__version__ = "v0.5.1" From c11a0feabba8911d2a647672eb28fd517f3669ee Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Tue, 23 Jul 2019 15:46:15 +0300 Subject: [PATCH 36/74] Remove unnecessary imports. --- theo/interfaces/cli.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/theo/interfaces/cli.py b/theo/interfaces/cli.py index 132bd50..81d291f 100644 --- a/theo/interfaces/cli.py +++ b/theo/interfaces/cli.py @@ -2,9 +2,6 @@ import argparse import code import getpass -import readline -import atexit -import rlcompleter from web3 import Web3 from theo.version import __version__ from theo.scanner import exploits_from_mythril @@ -110,11 +107,13 @@ def start_repl(args): # Imports for REPL import os, atexit, readline, rlcompleter + # Load history history_path = "~/.theo_history" def save_history(historyPath=history_path): import readline + readline.write_history_file(history_path) if os.path.isfile(history_path): From 3497a0c969879efdd6a53000ee21e7900ad44da2 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Tue, 23 Jul 2019 15:46:39 +0300 Subject: [PATCH 37/74] Bump version. --- theo/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/theo/version.py b/theo/version.py index a2434ee..fe3ca77 100644 --- a/theo/version.py +++ b/theo/version.py @@ -1 +1 @@ -__version__ = "v0.5.1" +__version__ = "v0.5.2" From 93be55ce88e283e53f7463cf993678dc95c17dcb Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Tue, 23 Jul 2019 15:50:14 +0300 Subject: [PATCH 38/74] Remove cache and other artifacts. --- .circleci/config.yml | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index a005a9b..01170d0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -16,13 +16,6 @@ jobs: - run: sudo chown -R circleci:circleci /usr/local/bin - run: sudo chown -R circleci:circleci /usr/local/lib/python3.7/site-packages - # Download and cache dependencies - - restore_cache: - keys: - - v1-dependencies-{{ checksum "requirements.txt" }} - # fallback to using the latest cache if no exact match is found - - v1-dependencies- - - run: name: Install dependencies command: | @@ -39,18 +32,6 @@ jobs: command: python3 setup.py install working_directory: ~/app - - store_test_results: - path: test-results - - - store_artifacts: - path: test-results - destination: tr1 - - - save_cache: - paths: - - ./venv - key: v1-dependencies-{{ checksum "requirements.txt" }} - pypi_release: <<: *defaults steps: From d67956536fe6acfdceb666c6ae882a458a9e6ed5 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Tue, 23 Jul 2019 15:50:54 +0300 Subject: [PATCH 39/74] Bump version. --- theo/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/theo/version.py b/theo/version.py index fe3ca77..7239172 100644 --- a/theo/version.py +++ b/theo/version.py @@ -1 +1 @@ -__version__ = "v0.5.2" +__version__ = "v0.5.3" From b435e8a4acbaeddd45769cc3d81dded282dce25b Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Tue, 23 Jul 2019 16:07:09 +0300 Subject: [PATCH 40/74] Bump version. --- theo/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/theo/version.py b/theo/version.py index 7239172..d01969a 100644 --- a/theo/version.py +++ b/theo/version.py @@ -1 +1 @@ -__version__ = "v0.5.3" +__version__ = "v0.5.4" From 9194cd0048671af0cf2fe239d0dce43704927fde Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Tue, 23 Jul 2019 16:12:01 +0300 Subject: [PATCH 41/74] Add cached dependency restore. --- .circleci/config.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 01170d0..7eea345 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -16,6 +16,13 @@ jobs: - run: sudo chown -R circleci:circleci /usr/local/bin - run: sudo chown -R circleci:circleci /usr/local/lib/python3.7/site-packages + # Download and cache dependencies + - restore_cache: + keys: + - v1-dependencies-{{ checksum "requirements.txt" }} + # fallback to using the latest cache if no exact match is found + - v1-dependencies- + - run: name: Install dependencies command: | From 8251b273f2f3fab63955bfec2fbcab1a22334361 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Tue, 23 Jul 2019 16:12:38 +0300 Subject: [PATCH 42/74] Bump version. --- theo/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/theo/version.py b/theo/version.py index d01969a..8679af5 100644 --- a/theo/version.py +++ b/theo/version.py @@ -1 +1 @@ -__version__ = "v0.5.4" +__version__ = "v0.5.5" From ab9cf76119465d02a1ee94f2549e8f488b43ba25 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Tue, 23 Jul 2019 16:16:15 +0300 Subject: [PATCH 43/74] Add dep install in release phase. --- .circleci/config.yml | 4 ++++ theo/version.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 7eea345..954c480 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -44,6 +44,10 @@ jobs: steps: - checkout: path: ~/app + - run: + name: Install dependencies + command: | + pip install --user -r requirements.txt - run: name: Verify Git tag vs. version command: python3 setup.py verify diff --git a/theo/version.py b/theo/version.py index 8679af5..cd60234 100644 --- a/theo/version.py +++ b/theo/version.py @@ -1 +1 @@ -__version__ = "v0.5.5" +__version__ = "v0.5.6" From 62ab9cb19115a3b4a2abaef0ec35152eb4facbb3 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Tue, 23 Jul 2019 19:25:01 +0300 Subject: [PATCH 44/74] Fix history save. --- theo/interfaces/cli.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/theo/interfaces/cli.py b/theo/interfaces/cli.py index 81d291f..776ad24 100644 --- a/theo/interfaces/cli.py +++ b/theo/interfaces/cli.py @@ -109,24 +109,18 @@ def start_repl(args): import os, atexit, readline, rlcompleter # Load history - history_path = "~/.theo_history" - - def save_history(historyPath=history_path): - import readline - - readline.write_history_file(history_path) - + history_path = os.path.join(os.environ["HOME"], ".theo_history") if os.path.isfile(history_path): readline.read_history_file(history_path) # Trigger history save on exit - atexit.register(save_history) + atexit.register(readline.write_history_file, history_path) # Load variables vars = globals() vars.update(locals()) # Start REPL readline.set_completer(rlcompleter.Completer(vars).complete) readline.parse_and_bind("tab: complete") - del os, atexit, readline, rlcompleter, save_history + del os, atexit, readline, rlcompleter code.InteractiveConsole(vars).interact( banner=""" Theo version {version}. From 94679a0b08e6ab138f451590bc64ab807ee160b2 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Tue, 23 Jul 2019 19:27:10 +0300 Subject: [PATCH 45/74] Add dump_to_file() functionality for exploit. --- test/input-tx.json | 6 +++--- theo/exploit/exploit.py | 9 +++++++++ theo/file/__init__.py | 2 +- theo/version.py | 2 +- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/test/input-tx.json b/test/input-tx.json index bab63d8..d2e0b55 100644 --- a/test/input-tx.json +++ b/test/input-tx.json @@ -1,17 +1,17 @@ [ [ { - "input": "0x4e71e0c8", + "data": "0x4e71e0c8", "value": 100000000000000000 }, { - "input": "0x2e64cec1", + "data": "0x2e64cec1", "value": "0x0" } ], [ { - "input": "0xdeadbeef" + "data": "0xdeadbeef" } ] ] \ No newline at end of file diff --git a/theo/exploit/exploit.py b/theo/exploit/exploit.py index 7a3abba..8d53a3d 100644 --- a/theo/exploit/exploit.py +++ b/theo/exploit/exploit.py @@ -208,3 +208,12 @@ def wait_for(self, contract, tx, flush=False): ) ) return pending_tx + + def dump_to_file(self, file=None): + from theo import dump + + exploit_object = [] + for tx in self.txs: + exploit_object.append(tx.__dict__) + + dump(ob=[exploit_object], filename=file) \ No newline at end of file diff --git a/theo/file/__init__.py b/theo/file/__init__.py index efcb98b..171d453 100644 --- a/theo/file/__init__.py +++ b/theo/file/__init__.py @@ -26,7 +26,7 @@ def exploits_from_file( for exploit in exploit_list: txs = [] for tx in exploit: - txs.append(Tx(data=tx.get("input", "0x"), value=tx.get("value", 0))) + txs.append(Tx(data=tx.get("data", "0x"), value=tx.get("value", 0))) exploits.append( Exploit( diff --git a/theo/version.py b/theo/version.py index cd60234..458018d 100644 --- a/theo/version.py +++ b/theo/version.py @@ -1 +1 @@ -__version__ = "v0.5.6" +__version__ = "v0.6.0" From 2a2a33bb1f1ed229395c2e4632d72a7028b19b21 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Tue, 23 Jul 2019 19:27:30 +0300 Subject: [PATCH 46/74] Bump version. --- theo/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/theo/version.py b/theo/version.py index 458018d..ed1be6a 100644 --- a/theo/version.py +++ b/theo/version.py @@ -1 +1 @@ -__version__ = "v0.6.0" +__version__ = "v0.6.1" From 4ef61f18510d4f7fac90bc0c3e7bec16fa39be1a Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Wed, 24 Jul 2019 07:31:13 +0300 Subject: [PATCH 47/74] Display exploit description on startup. --- theo/exploit/exploit.py | 19 +++++++++++++++++-- theo/exploit/tx.py | 11 ++++++++--- theo/interfaces/cli.py | 8 +++++--- theo/scanner/__init__.py | 6 +++++- 4 files changed, 35 insertions(+), 9 deletions(-) diff --git a/theo/exploit/exploit.py b/theo/exploit/exploit.py index 8d53a3d..fe6039c 100644 --- a/theo/exploit/exploit.py +++ b/theo/exploit/exploit.py @@ -11,10 +11,17 @@ def __init__( contract: str, account: str, account_pk: str, + title: str = None, + description: str = None, + swc_id: int = 0, verbosity: int = logging.INFO, ): # Web3 instance (can be HTTP, WebSockets, IPC) self.w3 = w3 + # Meta information + self.title = title + self.description = description + self.swc_id = swc_id # Transaction list self.txs = txs @@ -51,7 +58,15 @@ def __init__( self.logger.addHandler(logger_stream) def __repr__(self): - return "Exploit: (txs={})".format(self.txs) + return """Exploit: {title} +Description: {description} +SWC ID: {swc_id} +Transacion list: {txs}""".format( + title=self.title, + description=self.description, + swc_id=self.swc_id, + txs=self.txs, + ) def execute(self, nonce=None): receipts = [] @@ -216,4 +231,4 @@ def dump_to_file(self, file=None): for tx in self.txs: exploit_object.append(tx.__dict__) - dump(ob=[exploit_object], filename=file) \ No newline at end of file + dump(ob=[exploit_object], filename=file) diff --git a/theo/exploit/tx.py b/theo/exploit/tx.py index b3b21b0..cedf4ea 100644 --- a/theo/exploit/tx.py +++ b/theo/exploit/tx.py @@ -1,5 +1,5 @@ class Tx: - def __init__(self, data: str, value: str): + def __init__(self, data: str, value: str, name: str = None): # Transaction input (data) self.data = data # Transaction value @@ -11,8 +11,13 @@ def __init__(self, data: str, value: str): self.value = int(value) else: self.value = int(value) + # Name + self.name = name def __repr__(self): - return "Transaction {{Data: {input}, Value: {value_eth:.2f} ether ({value})}}".format( - input=self.data, value_eth=self.value / 10 ** 18, value=self.value + return "Transaction {{Name: {name}, Data: {input}, Value: {value_eth:.2f} ether ({value})}}".format( + name=self.name, + input=self.data, + value_eth=self.value / 10 ** 18, + value=self.value, ) diff --git a/theo/interfaces/cli.py b/theo/interfaces/cli.py index 776ad24..9ec5e24 100644 --- a/theo/interfaces/cli.py +++ b/theo/interfaces/cli.py @@ -100,7 +100,9 @@ def start_repl(args): if len(exploits) == 0: print("No exploits found. You're going to need to load some exploits.") else: - print("Found exploits(s):\n", exploits) + print("") + print("Found exploits(s):") + print(exploits) # Create a web3 instance w3 = Web3(Web3.HTTPProvider(args.rpc_http, request_kwargs={"timeout": 60})) @@ -123,8 +125,6 @@ def start_repl(args): del os, atexit, readline, rlcompleter code.InteractiveConsole(vars).interact( banner=""" -Theo version {version}. - Tools available in the console: - `exploits` is an array of loaded exploits found by Mythril or read from a file - `w3` an initialized instance of web3py for the provided HTTP RPC endpoint @@ -133,6 +133,8 @@ def start_repl(args): Check the readme for more info: https://github.com/cleanunicorn/theo +Theo version {version}. + """.format( version=__version__ ) diff --git a/theo/scanner/__init__.py b/theo/scanner/__init__.py index 464a24e..b897c8c 100644 --- a/theo/scanner/__init__.py +++ b/theo/scanner/__init__.py @@ -71,12 +71,13 @@ def exploits_from_mythril( w3 = Web3(Web3.HTTPProvider(rpcHTTP)) exploits = [] + for ri in report.issues: txs = [] issue = report.issues[ri] for si in issue.transaction_sequence["steps"]: - txs.append(Tx(data=si["input"], value=si["value"])) + txs.append(Tx(data=si["input"], value=si["value"], name=si["name"])) exploits.append( Exploit( @@ -85,6 +86,9 @@ def exploits_from_mythril( contract=contract, account=private_key_to_account(account_pk), account_pk=account_pk, + title=issue.title, + description=issue.description, + swc_id=issue.swc_id, ) ) From 0ec0f737567a6986c9a60de6f615fd223f9bfaee Mon Sep 17 00:00:00 2001 From: Luca Daniel Date: Wed, 24 Jul 2019 07:43:00 +0300 Subject: [PATCH 48/74] Update Readme.md --- Readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Readme.md b/Readme.md index 5f69333..1f35568 100644 --- a/Readme.md +++ b/Readme.md @@ -15,7 +15,7 @@ Features: - waiting for an actor to interact with a monitored smart contract, in order to frontrun them. - web3 console -![Theo](./static/theo-profile.png) + He knows [Karl](https://github.com/cleanunicorn/karl) from work. @@ -242,4 +242,4 @@ Command "/usr/bin/python3 -u -c "import setuptools, tokenize;__file__='/tmp/pip- On Ubuntu you can install them with: ```console $ sudo apt install libssl-dev -``` \ No newline at end of file +``` From 0d1943f2b83cc9fe52544ac8280e5742cf27b826 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Wed, 24 Jul 2019 07:47:26 +0300 Subject: [PATCH 49/74] Load tx function name when loading files. --- theo/file/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/theo/file/__init__.py b/theo/file/__init__.py index 171d453..f593060 100644 --- a/theo/file/__init__.py +++ b/theo/file/__init__.py @@ -26,7 +26,7 @@ def exploits_from_file( for exploit in exploit_list: txs = [] for tx in exploit: - txs.append(Tx(data=tx.get("data", "0x"), value=tx.get("value", 0))) + txs.append(Tx(data=tx.get("data", "0x"), value=tx.get("value", 0), name=tx.get("name", ""))) exploits.append( Exploit( From d275ab0c56db6d24a93a664ba48c6a5015b431a2 Mon Sep 17 00:00:00 2001 From: Luca Daniel Date: Wed, 24 Jul 2019 07:34:26 +0100 Subject: [PATCH 50/74] Wait txs send txs (#8) * Merge master. * [WIP] * Can specify a different set of txs to send on frontrunning than the ones identified. * Restyle code. --- theo/exploit/exploit.py | 36 +++++++++++++++++++++++++++++++----- theo/file/__init__.py | 8 +++++++- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/theo/exploit/exploit.py b/theo/exploit/exploit.py index fe6039c..9170109 100644 --- a/theo/exploit/exploit.py +++ b/theo/exploit/exploit.py @@ -80,6 +80,7 @@ def execute(self, nonce=None): for tx in self.txs: run_tx = { + "from": self.account, "to": self.contract, "gasPrice": self.gas_price, "gas": self.gas, @@ -93,6 +94,7 @@ def execute(self, nonce=None): # Estimate gas if self.gas_estimate is True: + self.logger.debug("Estimating gas for tx: {tx}".format(tx=run_tx)) run_tx["gas"] = self.w3.eth.estimateGas(run_tx) receipts.append(self.send_tx(run_tx)) @@ -111,7 +113,7 @@ def execute(self, nonce=None): self.logger.debug(receipts) - def frontrun(self, flush=False, nonce_index=None): + def frontrun(self, flush=False, nonce_index=None, wait_txs=None, send_txs=None): self.logger.info("Scanning the mem pool for transactions...") if nonce_index is None: @@ -121,8 +123,27 @@ def frontrun(self, flush=False, nonce_index=None): initial_balance = self.w3.eth.getBalance(self.account) + # If we don't specify a different set of transactions to wait for, use the ones in the exploit + if wait_txs is None: + wait_txs = self.txs + + # If we don't specify a different set of transactions to send, use the ones in the exploit + if send_txs is None: + send_txs = self.txs + + if len(wait_txs) != len(send_txs): + self.logger.error( + "The number of transactions we're waiting for needs to match the number of transactions to send. {wait_len} != {send_len}".format( + wait_len=len(wait_txs), send_len=len(send_txs) + ) + ) + return + # Wait for each tx and frontrun it. - for tx in self.txs: + index = 0 + for tx in wait_txs: + self.logger.info("Waiting for tx: {tx}".format(tx=tx)) + victim_tx = self.wait_for(self.contract, tx, flush=flush) self.logger.info( "Found tx: {hash}".format(hash=victim_tx.get("hash").hex()) @@ -131,11 +152,13 @@ def frontrun(self, flush=False, nonce_index=None): frontrun_tx = { "to": self.contract, "gasPrice": hex(victim_tx["gasPrice"] + self.gas_price_increment), - "data": victim_tx["input"].replace( + "data": send_txs[index] + .data.replace( "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef", self.account[2:] - ), + ) + .replace(victim_tx["from"], self.account[2:]), "gas": victim_tx["gas"] + self.gas_increment, - "value": victim_tx["value"], + "value": send_txs[index].value, "nonce": nonce_index, } nonce_index += 1 @@ -154,6 +177,8 @@ def frontrun(self, flush=False, nonce_index=None): else: self.send_tx(frontrun_tx) + index += 1 + final_balance = self.w3.eth.getBalance(self.account) self.logger.info( "Initial balance: \t{balance} ({balance_ether:.2f} ether)".format( @@ -215,6 +240,7 @@ def wait_for(self, contract, tx, flush=False): if (pending_tx.get("to", str("")).lower() == contract.lower()) and ( pending_tx.get("input", "").lower() == tx.data.lower() + and (pending_tx.get("value", 0) == tx.value) ): self.logger.debug( "Found pending tx: {tx} from: {sender}.".format( diff --git a/theo/file/__init__.py b/theo/file/__init__.py index f593060..2379ef7 100644 --- a/theo/file/__init__.py +++ b/theo/file/__init__.py @@ -26,7 +26,13 @@ def exploits_from_file( for exploit in exploit_list: txs = [] for tx in exploit: - txs.append(Tx(data=tx.get("data", "0x"), value=tx.get("value", 0), name=tx.get("name", ""))) + txs.append( + Tx( + data=tx.get("data", "0x"), + value=tx.get("value", 0), + name=tx.get("name", ""), + ) + ) exploits.append( Exploit( From c51aebed654a45ce2abddb76dbf38625dbe978ac Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Wed, 24 Jul 2019 09:35:15 +0300 Subject: [PATCH 51/74] Fix port on blockchain start script. --- blockchain/blockchain-start.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blockchain/blockchain-start.sh b/blockchain/blockchain-start.sh index 6b686c4..7551dba 100755 --- a/blockchain/blockchain-start.sh +++ b/blockchain/blockchain-start.sh @@ -1,3 +1,3 @@ #!/bin/bash -./geth.bin --datadir=./ --syncmode=full --rpc --rpcapi="eth,net,rpc,web3,txpool,personal,debug,account" --ws --miner.gasprice=1 --txpool.locals="0x1dF62f291b2E969fB0849d99D9Ce41e2F137006e" --rpccorsdomain='*' --allow-insecure-unlock --unlock "0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1,0xffcf8fdee72ac11b5c542428b35eef5769c409f0" --password ./account-password.txt --port 4030 --port 40303 console +./geth.bin --datadir=./ --syncmode=full --rpc --rpcapi="eth,net,rpc,web3,txpool,personal,debug,account" --ws --miner.gasprice=1000 --txpool.locals="0x1dF62f291b2E969fB0849d99D9Ce41e2F137006e" --rpccorsdomain='*' --allow-insecure-unlock --unlock "0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1,0xffcf8fdee72ac11b5c542428b35eef5769c409f0,0x1dF62f291b2E969fB0849d99D9Ce41e2F137006e" --password ./account-password.txt --port 40303 console From 249861d2f1cdb8a170b9c87c15d7e9e6a29ca6f6 Mon Sep 17 00:00:00 2001 From: Luca Daniel Date: Wed, 24 Jul 2019 08:17:05 +0100 Subject: [PATCH 52/74] Implement backrunning. (#9) * Implement backrunning. * Restyle code. --- theo/exploit/exploit.py | 49 ++++++++++++++++++++++++++++++++++------- 1 file changed, 41 insertions(+), 8 deletions(-) diff --git a/theo/exploit/exploit.py b/theo/exploit/exploit.py index 9170109..08b652b 100644 --- a/theo/exploit/exploit.py +++ b/theo/exploit/exploit.py @@ -113,7 +113,15 @@ def execute(self, nonce=None): self.logger.debug(receipts) - def frontrun(self, flush=False, nonce_index=None, wait_txs=None, send_txs=None): + def _front_back_run( + self, flush=False, nonce_index=None, wait_txs=None, send_txs=None, run_type=None + ): + if run_type is None: + self.logger.error( + "Must specify if it should frontrun or backrun transactions." + ) + return + self.logger.info("Scanning the mem pool for transactions...") if nonce_index is None: @@ -149,9 +157,9 @@ def frontrun(self, flush=False, nonce_index=None, wait_txs=None, send_txs=None): "Found tx: {hash}".format(hash=victim_tx.get("hash").hex()) ) - frontrun_tx = { + run_tx = { + "from": self.account, "to": self.contract, - "gasPrice": hex(victim_tx["gasPrice"] + self.gas_price_increment), "data": send_txs[index] .data.replace( "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef", self.account[2:] @@ -161,21 +169,28 @@ def frontrun(self, flush=False, nonce_index=None, wait_txs=None, send_txs=None): "value": send_txs[index].value, "nonce": nonce_index, } + if run_type == "frontrun": + run_tx["gasPrice"] = hex( + victim_tx["gasPrice"] + self.gas_price_increment + ) + elif run_type == "backrun": + run_tx["gasPrice"] = hex( + victim_tx["gasPrice"] - self.gas_price_increment + ) + nonce_index += 1 # Estimate gas if self.gas_estimate is True: try: - frontrun_tx["gas"] = ( - self.w3.eth.estimateGas(frontrun_tx) + self.gas_increment - ) - self.send_tx(frontrun_tx) + run_tx["gas"] = self.w3.eth.estimateGas(run_tx) + self.gas_increment + self.send_tx(run_tx) except ValueError: self.logger.error("Could not estimate gas.") except Exception as e: self.logger.error("Exception caught: {}".format(e)) else: - self.send_tx(frontrun_tx) + self.send_tx(run_tx) index += 1 @@ -191,6 +206,24 @@ def frontrun(self, flush=False, nonce_index=None, wait_txs=None, send_txs=None): ) ) + def frontrun(self, flush=False, nonce_index=None, wait_txs=None, send_txs=None): + self._front_back_run( + flush=flush, + nonce_index=nonce_index, + wait_txs=wait_txs, + send_txs=send_txs, + run_type="frontrun", + ) + + def backrun(self, flush=False, nonce_index=None, wait_txs=None, send_txs=None): + self._front_back_run( + flush=flush, + nonce_index=nonce_index, + wait_txs=wait_txs, + send_txs=send_txs, + run_type="backrun", + ) + def send_tx(self, tx: dict) -> str: # Make sure the addresses are checksummed. tx["to"] = Web3.toChecksumAddress(tx["to"]) From bcb56e45bc3fc96e9f0fc7bea51ea6ef23a0bbfe Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Wed, 24 Jul 2019 10:25:28 +0300 Subject: [PATCH 53/74] Update readme with features. --- Readme.md | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/Readme.md b/Readme.md index 1f35568..e37b75b 100644 --- a/Readme.md +++ b/Readme.md @@ -10,10 +10,14 @@ Theo aims to be an exploitation framework and a blockchain recon and interaction Features: -- automatic smart contract scanning which generates a list of possible exploits. -- generating and sending transactions to exploit a smart contract. -- waiting for an actor to interact with a monitored smart contract, in order to frontrun them. -- web3 console +- Automatic smart contract scanning which generates a list of possible exploits. +- Sending transactions to exploit a smart contract. +- Transaction pool monitor. +- Web3 console +- Frontrunning and backrunning transactions. +- Waiting for a list of transactions and sending out others. +- Estimating gas for transactions means only successful transactions are sent. +- Disabling gas estimation will send transactions with a fixed gas quantity. @@ -197,22 +201,25 @@ This works very well for some specially crafted [contracts](./contracts/) or som Instead of identifying the exploits with mythril, you can specify the list of exploits yourself. -Create a file that looks like this [input-tx.json](./test/input-tx.json): +Create a file that looks like this [exploits.json](./test/input-tx.json): ```json [ [ { + "name": "claimOwnership()", "input": "0x4e71e0c8", "value": "0xde0b6b3a7640000" }, { + "name": "retrieve()", "input": "0x2e64cec1", "value": "0x0" } ], [ { + "name": "claimOwnership()", "input": "0x4e71e0c8", "value": "0xde0b6b3a7640000" } @@ -220,7 +227,13 @@ Create a file that looks like this [input-tx.json](./test/input-tx.json): ] ``` -This one defines 2 exploits, the first one has 2 transactions and the second one only 1 transaction. After the exploits are loaded, frontrunning is the same. +This one defines 2 exploits, the first one has 2 transactions and the second one only has 1 transaction. + +You can load it with: + +```console +$ theo --load-file=./exploits.json +``` # Troubleshooting From 1ed66f1a8342bb37ce5f022eab0e63ba0c1e7861 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Wed, 24 Jul 2019 10:26:38 +0300 Subject: [PATCH 54/74] Bump version. --- theo/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/theo/version.py b/theo/version.py index ed1be6a..9793744 100644 --- a/theo/version.py +++ b/theo/version.py @@ -1 +1 @@ -__version__ = "v0.6.1" +__version__ = "v0.7.0" From b93ed5a35215b0e846d3668b3f88a0cbca9c8963 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Wed, 24 Jul 2019 23:16:28 +0300 Subject: [PATCH 55/74] Be more specific with requirements. --- requirements.txt | 71 ++++++++++++++++++++++++++++++++++++++++++++++-- theo/version.py | 2 +- 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index eccc9fd..dc2fa1c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,70 @@ -web3==4.9.2 +asn1crypto==0.24.0 +astroid==2.2.5 +attrdict==2.0.1 +certifi==2019.6.16 +cffi==1.12.3 +chardet==3.0.4 +coincurve==12.0.0 +coloredlogs==10.0 +configparser==3.7.4 +coverage==4.5.3 +cycler==0.10.0 +cytoolz==0.10.0 +dictionaries==0.0.1 +eth-abi==1.3.0 +eth-account==0.3.0 +eth-hash==0.2.0 +eth-keyfile==0.5.1 +eth-keys==0.2.4 +eth-rlp==0.1.2 +eth-tester==0.1.0b32 +eth-typing==2.1.0 +eth-utils==1.6.2 +ethereum==2.3.2 +ethereum-input-decoder==0.2.2 +future==0.17.1 +hexbytes==0.2.0 +humanfriendly==4.18 +idna==2.8 +isort==4.3.21 +Jinja2==2.10.1 +jsonpickle==1.2 +kiwisolver==1.1.0 +lazy-object-proxy==1.4.1 +lru-dict==1.1.6 +MarkupSafe==1.1.1 +matplotlib==3.1.1 +mccabe==0.6.1 +mock==3.0.5 mythril==0.21.12 -jsonpickle==1.2 \ No newline at end of file +numpy==1.16.4 +parsimonious==0.8.1 +pbkdf2==1.3 +persistent==4.5.0 +plyvel==1.1.0 +py-ecc==1.4.2 +py-flags==1.1.2 +py-solc==3.2.0 +pycparser==2.19 +pycryptodome==3.8.2 +pyethash==0.1.27 +pylint==2.3.1 +pyparsing==2.4.0 +pysha3==1.0.2 +python-dateutil==2.8.0 +PyYAML==5.1.1 +repoze.lru==0.7 +requests==2.22.0 +rlp==1.1.0 +scrypt==0.8.13 +semantic-version==2.6.0 +six==1.12.0 +toolz==0.10.0 +transaction==2.4.0 +typed-ast==1.4.0 +urllib3==1.25.3 +web3==4.9.2 +websockets==6.0 +wrapt==1.11.2 +z3-solver==4.8.5.0 +zope.interface==4.6.0 diff --git a/theo/version.py b/theo/version.py index 9793744..c88d9ba 100644 --- a/theo/version.py +++ b/theo/version.py @@ -1 +1 @@ -__version__ = "v0.7.0" +__version__ = "v0.7.1" From 7d8907cf397cb805aff93c2aa4dfd0c7d30689a0 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Thu, 25 Jul 2019 00:14:06 +0300 Subject: [PATCH 56/74] Trying to fix the pip package. --- .circleci/config.yml | 11 +++++++---- theo/version.py | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 954c480..53bb949 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -39,15 +39,18 @@ jobs: command: python3 setup.py install working_directory: ~/app + - store_test_results: + path: test-results + + - store_artifacts: + path: test-results + destination: tr1 + pypi_release: <<: *defaults steps: - checkout: path: ~/app - - run: - name: Install dependencies - command: | - pip install --user -r requirements.txt - run: name: Verify Git tag vs. version command: python3 setup.py verify diff --git a/theo/version.py b/theo/version.py index c88d9ba..3670ab9 100644 --- a/theo/version.py +++ b/theo/version.py @@ -1 +1 @@ -__version__ = "v0.7.1" +__version__ = "v0.7.2" From e2cd8109b70a9b6bb6920e4049787e6f27c184f3 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Thu, 25 Jul 2019 00:40:01 +0300 Subject: [PATCH 57/74] Remove jsonpickle. --- .circleci/config.yml | 11 ++++++++--- theo/__init__.py | 4 ++-- theo/version.py | 2 +- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 53bb949..993eeab 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,7 +3,7 @@ defaults: &defaults docker: - - image: circleci/python:3.7.1 + - image: circleci/python:3.7.3 working_directory: ~/app version: 2 @@ -44,7 +44,12 @@ jobs: - store_artifacts: path: test-results - destination: tr1 + destination: tr1 + + - save_cache: + paths: + - ./venv + key: v1-dependencies-{{ checksum "requirements.txt" }} pypi_release: <<: *defaults @@ -78,4 +83,4 @@ workflows: tags: only: /v[0-9]+(\.[0-9]+(dev|)([0-9]+|))*/ requires: - - test + - test \ No newline at end of file diff --git a/theo/__init__.py b/theo/__init__.py index e8093b0..2d7c782 100644 --- a/theo/__init__.py +++ b/theo/__init__.py @@ -1,4 +1,4 @@ -import jsonpickle, time +import time, json def private_key_to_account(pk: str): @@ -15,6 +15,6 @@ def dump(ob=None, filename=None): if filename is None: filename = "{name}.json".format(name=time.time_ns()) - pickled = jsonpickle.encode(ob) + pickled = json.dumps(ob) with open(filename, "w") as f: f.write(pickled) diff --git a/theo/version.py b/theo/version.py index 3670ab9..0b83c3b 100644 --- a/theo/version.py +++ b/theo/version.py @@ -1 +1 @@ -__version__ = "v0.7.2" +__version__ = "v0.7.3" From 9fdefcf4f0f4392d9a2eb173ac36622d34a1f5e4 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Thu, 25 Jul 2019 00:41:48 +0300 Subject: [PATCH 58/74] Rewrite version string without prefix. --- theo/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/theo/version.py b/theo/version.py index 0b83c3b..4910b9e 100644 --- a/theo/version.py +++ b/theo/version.py @@ -1 +1 @@ -__version__ = "v0.7.3" +__version__ = "0.7.3" From a8481947024d52609c7a0b0677d4bee72945b752 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Thu, 25 Jul 2019 12:56:45 +0300 Subject: [PATCH 59/74] Make classes Exploit and Tx available. --- theo/interfaces/cli.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/theo/interfaces/cli.py b/theo/interfaces/cli.py index 9ec5e24..b996594 100644 --- a/theo/interfaces/cli.py +++ b/theo/interfaces/cli.py @@ -104,8 +104,10 @@ def start_repl(args): print("Found exploits(s):") print(exploits) - # Create a web3 instance + # Add local tools for console w3 = Web3(Web3.HTTPProvider(args.rpc_http, request_kwargs={"timeout": 60})) + from theo.exploit.exploit import Exploit + from theo.exploit.tx import Tx # Imports for REPL import os, atexit, readline, rlcompleter From b4fa2aa1ed905e0810ba22b934f615cc8aecef47 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Thu, 25 Jul 2019 12:57:03 +0300 Subject: [PATCH 60/74] Bump version. --- theo/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/theo/version.py b/theo/version.py index 4910b9e..b8c0cbc 100644 --- a/theo/version.py +++ b/theo/version.py @@ -1 +1 @@ -__version__ = "0.7.3" +__version__ = "v0.7.4" From 4cc5c4325fd134e4061a5901d25a741defa4cd9a Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Sat, 27 Jul 2019 10:31:33 +0300 Subject: [PATCH 61/74] Update 2 step contract. --- contracts/VulnerableTwoStep.sol | 34 +++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/contracts/VulnerableTwoStep.sol b/contracts/VulnerableTwoStep.sol index 5dbf80b..993afdf 100644 --- a/contracts/VulnerableTwoStep.sol +++ b/contracts/VulnerableTwoStep.sol @@ -1,24 +1,38 @@ +// pragma solidity ^0.5.0; contract VulnerableTwoStep { - address payable public owner; - bool public owner_reset = false; + address public player; + address public owner; + bool public claimed; constructor() public payable { owner = msg.sender; } + function reset() public payable { + require(owner == msg.sender); + + player = address(0); + claimed = false; + } + function() external payable {} - function become_owner() public payable { - require(msg.value == 1 ether); + function claimOwnership() public payable { + require(msg.value == 0.1 ether); - if (owner_reset == false) { - owner_reset = true; - owner = msg.sender; + if (claimed == false) { + player = msg.sender; + claimed = true; } } - function retrieve() public payable { - owner.transfer(address(this).balance); + function retrieve() public { + require(msg.sender == player); + + msg.sender.transfer(address(this).balance); + + player = address(0); + claimed = false; } -} \ No newline at end of file +} From 1bfe803398201baf63648e49d1a70f8fc8720817 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Sat, 27 Jul 2019 13:22:05 +0300 Subject: [PATCH 62/74] Fix gas price to match 1 gwei. --- theo/exploit/exploit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/theo/exploit/exploit.py b/theo/exploit/exploit.py index 08b652b..9432bea 100644 --- a/theo/exploit/exploit.py +++ b/theo/exploit/exploit.py @@ -33,7 +33,7 @@ def __init__( self.account_private_key = account_pk # Gas price when executing exploit - self.gas_price = 1 ** 10 + self.gas_price = 10 ** 10 # Gas price increment when frontrunning self.gas_price_increment = 1 # Gas value when `gas_estimate` is False From 8c45ca5f89875f170d7cd3fe4078534e195b5455 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Sat, 27 Jul 2019 19:38:37 +0300 Subject: [PATCH 63/74] Add timeout argument for RPC connection. --- theo/file/__init__.py | 8 ++++---- theo/interfaces/cli.py | 5 ++++- theo/scanner/__init__.py | 7 ++++--- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/theo/file/__init__.py b/theo/file/__init__.py index 2379ef7..0d04986 100644 --- a/theo/file/__init__.py +++ b/theo/file/__init__.py @@ -6,20 +6,20 @@ def exploits_from_file( - file, rpcHTTP=None, rpcWS=None, rpcIPC=None, contract="", account_pk="" + file, rpcHTTP=None, rpcWS=None, rpcIPC=None, contract="", account_pk="", timeout=300, ): with open(file) as f: exploit_list = json.load(f) if rpcIPC is not None: print("Connecting to IPC: {rpc}.".format(rpc=rpcIPC)) - w3 = Web3(Web3.IPCProvider(rpcIPC)) + w3 = Web3(Web3.IPCProvider(rpcIPC, timeout=timeout)) elif rpcWS is not None: print("Connecting to WebSocket: {rpc}.".format(rpc=rpcWS)) - w3 = Web3(Web3.WebsocketProvider(rpcWS)) + w3 = Web3(Web3.WebsocketProvider(rpcWS, websocket_kwargs={"timeout": timeout})) else: print("Connecting to HTTP: {rpc}.".format(rpc=rpcHTTP)) - w3 = Web3(Web3.HTTPProvider(rpcHTTP)) + w3 = Web3(Web3.HTTPProvider(rpcHTTP, request_kwargs={"timeout": timeout})) exploits = [] diff --git a/theo/interfaces/cli.py b/theo/interfaces/cli.py index b996594..7c68a62 100644 --- a/theo/interfaces/cli.py +++ b/theo/interfaces/cli.py @@ -24,6 +24,7 @@ def main(): rpc = parser.add_argument_group("RPC connections") rpc.add_argument("--rpc-ws", help="Connect to this WebSockets RPC", default=None) rpc.add_argument("--rpc-ipc", help="Connect to this IPC RPC", default=None) + rpc.add_argument("--timeout", help="Timeout for RPC connections", default=300) # Account to use for attacking parser.add_argument("--account-pk", help="The account's private key") @@ -86,6 +87,7 @@ def start_repl(args): rpcIPC=args.rpc_ipc, contract=args.contract, account_pk=args.account_pk, + timeout=args.timeout, ) if args.load_file != "": exploits += exploits_from_file( @@ -95,6 +97,7 @@ def start_repl(args): rpcIPC=args.rpc_ipc, contract=args.contract, account_pk=args.account_pk, + timeout=args.timeout, ) if len(exploits) == 0: @@ -105,7 +108,7 @@ def start_repl(args): print(exploits) # Add local tools for console - w3 = Web3(Web3.HTTPProvider(args.rpc_http, request_kwargs={"timeout": 60})) + w3 = Web3(Web3.HTTPProvider(args.rpc_http, request_kwargs={"timeout": args.timeout})) from theo.exploit.exploit import Exploit from theo.exploit.tx import Tx diff --git a/theo/scanner/__init__.py b/theo/scanner/__init__.py index b897c8c..45f9a80 100644 --- a/theo/scanner/__init__.py +++ b/theo/scanner/__init__.py @@ -13,6 +13,7 @@ def exploits_from_mythril( rpcHTTP="http://localhost:8545", rpcWS=None, rpcIPC=None, + timeout=300, contract="", account_pk="", strategy="bfs", @@ -62,13 +63,13 @@ def exploits_from_mythril( if rpcIPC is not None: print("Connecting to IPC: {rpc}.".format(rpc=rpcIPC)) - w3 = Web3(Web3.IPCProvider(rpcIPC)) + w3 = Web3(Web3.IPCProvider(rpcIPC, timeout=timeout)) elif rpcWS is not None: print("Connecting to WebSocket: {rpc}.".format(rpc=rpcWS)) - w3 = Web3(Web3.WebsocketProvider(rpcWS)) + w3 = Web3(Web3.WebsocketProvider(rpcWS, websocket_kwargs={"timeout": timeout})) else: print("Connecting to HTTP: {rpc}.".format(rpc=rpcHTTP)) - w3 = Web3(Web3.HTTPProvider(rpcHTTP)) + w3 = Web3(Web3.HTTPProvider(rpcHTTP, request_kwargs={"timeout": timeout})) exploits = [] From daa6ed8e73824d5b271ba2f436507a2880ad26d3 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Sat, 27 Jul 2019 19:38:56 +0300 Subject: [PATCH 64/74] Send_tx always returns tx_hash. --- theo/exploit/exploit.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/theo/exploit/exploit.py b/theo/exploit/exploit.py index 9432bea..285a993 100644 --- a/theo/exploit/exploit.py +++ b/theo/exploit/exploit.py @@ -69,8 +69,6 @@ def __repr__(self): ) def execute(self, nonce=None): - receipts = [] - if nonce is None: nonce_index = self.w3.eth.getTransactionCount(self.account) else: @@ -97,7 +95,7 @@ def execute(self, nonce=None): self.logger.debug("Estimating gas for tx: {tx}".format(tx=run_tx)) run_tx["gas"] = self.w3.eth.estimateGas(run_tx) - receipts.append(self.send_tx(run_tx)) + self.send_tx(run_tx) final_balance = self.w3.eth.getBalance(self.account) self.logger.info( @@ -111,7 +109,6 @@ def execute(self, nonce=None): ) ) - self.logger.debug(receipts) def _front_back_run( self, flush=False, nonce_index=None, wait_txs=None, send_txs=None, run_type=None @@ -235,12 +232,12 @@ def send_tx(self, tx: dict) -> str: self.logger.info( "Waiting for {tx_hash} to be mined...".format(tx_hash=tx_hash.hex()) ) - tx_receipt = self.w3.eth.waitForTransactionReceipt(tx_hash, timeout=300) + tx_receipt = self.w3.eth.waitForTransactionReceipt(tx_hash) self.logger.info("Mined") self.logger.debug("Receipt: {}".format(tx_receipt)) - return tx_receipt + return tx_hash else: - return None + return tx_hash def wait_for(self, contract, tx, flush=False): # Setting up filter From 817df6749000a4b9c59c0bacab212159c3f90f13 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Sat, 27 Jul 2019 19:39:10 +0300 Subject: [PATCH 65/74] Bump version. --- theo/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/theo/version.py b/theo/version.py index b8c0cbc..929af9e 100644 --- a/theo/version.py +++ b/theo/version.py @@ -1 +1 @@ -__version__ = "v0.7.4" +__version__ = "v0.8.0" From 8b735e6ad5383848347cb07d1e5149910548a8b3 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Sat, 27 Jul 2019 19:41:17 +0300 Subject: [PATCH 66/74] Reformat code. --- theo/exploit/exploit.py | 1 - theo/file/__init__.py | 2 +- theo/interfaces/cli.py | 4 +++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/theo/exploit/exploit.py b/theo/exploit/exploit.py index 285a993..112d843 100644 --- a/theo/exploit/exploit.py +++ b/theo/exploit/exploit.py @@ -109,7 +109,6 @@ def execute(self, nonce=None): ) ) - def _front_back_run( self, flush=False, nonce_index=None, wait_txs=None, send_txs=None, run_type=None ): diff --git a/theo/file/__init__.py b/theo/file/__init__.py index 0d04986..77fe3b7 100644 --- a/theo/file/__init__.py +++ b/theo/file/__init__.py @@ -6,7 +6,7 @@ def exploits_from_file( - file, rpcHTTP=None, rpcWS=None, rpcIPC=None, contract="", account_pk="", timeout=300, + file, rpcHTTP=None, rpcWS=None, rpcIPC=None, contract="", account_pk="", timeout=300 ): with open(file) as f: exploit_list = json.load(f) diff --git a/theo/interfaces/cli.py b/theo/interfaces/cli.py index 7c68a62..eabfc16 100644 --- a/theo/interfaces/cli.py +++ b/theo/interfaces/cli.py @@ -108,7 +108,9 @@ def start_repl(args): print(exploits) # Add local tools for console - w3 = Web3(Web3.HTTPProvider(args.rpc_http, request_kwargs={"timeout": args.timeout})) + w3 = Web3( + Web3.HTTPProvider(args.rpc_http, request_kwargs={"timeout": args.timeout}) + ) from theo.exploit.exploit import Exploit from theo.exploit.tx import Tx From 374ff0a3e0d30d5c414600cfdb6e8f9da34af010 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Sat, 27 Jul 2019 19:43:03 +0300 Subject: [PATCH 67/74] Bump version. --- theo/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/theo/version.py b/theo/version.py index 929af9e..79fad2a 100644 --- a/theo/version.py +++ b/theo/version.py @@ -1 +1 @@ -__version__ = "v0.8.0" +__version__ = "v0.8.1" From d1606cafa9b3fb4420762a77ac2b0f9f70df2ab5 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Wed, 31 Jul 2019 20:17:54 +0300 Subject: [PATCH 68/74] Fix spelling of "transaction". --- theo/exploit/exploit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/theo/exploit/exploit.py b/theo/exploit/exploit.py index 112d843..3e8876a 100644 --- a/theo/exploit/exploit.py +++ b/theo/exploit/exploit.py @@ -61,7 +61,7 @@ def __repr__(self): return """Exploit: {title} Description: {description} SWC ID: {swc_id} -Transacion list: {txs}""".format( +Transaction list: {txs}""".format( title=self.title, description=self.description, swc_id=self.swc_id, From 7ab1acb188e26fc3e48fdc00eb1728c0044c48b2 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Sat, 10 Aug 2019 15:57:40 -0700 Subject: [PATCH 69/74] Update Mythril version. --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index dc2fa1c..d4adc42 100644 --- a/requirements.txt +++ b/requirements.txt @@ -36,7 +36,7 @@ MarkupSafe==1.1.1 matplotlib==3.1.1 mccabe==0.6.1 mock==3.0.5 -mythril==0.21.12 +mythril==0.21.15 numpy==1.16.4 parsimonious==0.8.1 pbkdf2==1.3 From 1ca9ac8f6e89e3e48ecb1bcb069a84eb5fc75a28 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Sat, 10 Aug 2019 16:04:10 -0700 Subject: [PATCH 70/74] Bump version. --- theo/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/theo/version.py b/theo/version.py index 79fad2a..59fafc7 100644 --- a/theo/version.py +++ b/theo/version.py @@ -1 +1 @@ -__version__ = "v0.8.1" +__version__ = "v0.8.2" From bc2ab93c97c2fa95eddeec86d96c2cf71f0349c4 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Sun, 29 Sep 2019 14:50:01 +0300 Subject: [PATCH 71/74] Add Solidity syntax highlight. --- .gitattributes | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..7cc88f0 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.sol linguist-language=Solidity \ No newline at end of file From 17a8144084b05492f9e7ef7a1c142878b7955301 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Thu, 21 Nov 2019 16:49:51 +0200 Subject: [PATCH 72/74] Sync --- theo/interfaces/cli.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/theo/interfaces/cli.py b/theo/interfaces/cli.py index eabfc16..88d7249 100644 --- a/theo/interfaces/cli.py +++ b/theo/interfaces/cli.py @@ -22,29 +22,29 @@ def main(): ) # Optional connections rpc = parser.add_argument_group("RPC connections") - rpc.add_argument("--rpc-ws", help="Connect to this WebSockets RPC", default=None) - rpc.add_argument("--rpc-ipc", help="Connect to this IPC RPC", default=None) - rpc.add_argument("--timeout", help="Timeout for RPC connections", default=300) + rpc.add_argument("--rpc-ws", help="connect to this WebSockets RPC", default=None) + rpc.add_argument("--rpc-ipc", help="connect to this IPC RPC", default=None) + rpc.add_argument("--timeout", help="timeout for RPC connections", default=300) # Account to use for attacking - parser.add_argument("--account-pk", help="The account's private key") + parser.add_argument("--account-pk", help="the account's private key") # Contract to monitor parser.add_argument( - "--contract", help="Contract to interact with", metavar="ADDRESS" + "--contract", help="contract to interact with", metavar="ADDRESS" ) # Find exploits with Mythril parser.add_argument( "--skip-mythril", - help="Skip scanning the contract with Mythril", + help="skip scanning the contract with Mythril", default=False, action="store_true", ) # Load exploits from file parser.add_argument( - "--load-file", type=str, help="Load exploit from file", default="" + "--load-file", help="load exploit from file", default=None ) # Print version and exit @@ -89,7 +89,7 @@ def start_repl(args): account_pk=args.account_pk, timeout=args.timeout, ) - if args.load_file != "": + if args.load_file is not None: exploits += exploits_from_file( file=args.load_file, rpcHTTP=args.rpc_http, From b1d1425f1479f240027e7be8c39ca9f9bfd94da6 Mon Sep 17 00:00:00 2001 From: Daniel Luca Date: Sun, 13 Mar 2022 11:48:40 +0200 Subject: [PATCH 73/74] =?UTF-8?q?=F0=9F=A7=90=20Lint=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- theo/__init__.py | 3 +-- theo/interfaces/cli.py | 4 +--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/theo/__init__.py b/theo/__init__.py index 2d7c782..0dfe81d 100644 --- a/theo/__init__.py +++ b/theo/__init__.py @@ -10,8 +10,7 @@ def private_key_to_account(pk: str): def dump(ob=None, filename=None): - """Dumps the provided object to a file in json format. - """ + """Dumps the provided object to a file in json format.""" if filename is None: filename = "{name}.json".format(name=time.time_ns()) diff --git a/theo/interfaces/cli.py b/theo/interfaces/cli.py index 88d7249..0949acc 100644 --- a/theo/interfaces/cli.py +++ b/theo/interfaces/cli.py @@ -43,9 +43,7 @@ def main(): ) # Load exploits from file - parser.add_argument( - "--load-file", help="load exploit from file", default=None - ) + parser.add_argument("--load-file", help="load exploit from file", default=None) # Print version and exit parser.add_argument( From 0ea34c301a9ec3069e2fc80e2f30481409fcb519 Mon Sep 17 00:00:00 2001 From: TIMBER2024 Date: Mon, 14 Oct 2024 07:14:46 -0400 Subject: [PATCH 74/74] Create SECURITY.md Proposal --- SECURITY.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..034e848 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,21 @@ +# Security Policy + +## Supported Versions + +Use this section to tell people about which versions of your project are +currently being supported with security updates. + +| Version | Supported | +| ------- | ------------------ | +| 5.1.x | :white_check_mark: | +| 5.0.x | :x: | +| 4.0.x | :white_check_mark: | +| < 4.0 | :x: | + +## Reporting a Vulnerability + +Use this section to tell people how to report a vulnerability. + +Tell them where to go, how often they can expect to get an update on a +reported vulnerability, what to expect if the vulnerability is accepted or +declined, etc.