Introduction
I participated with some colleagues from the UAC ctf team on “Cyber Apocalypse 2023 - The Cursed Mission” capture the flag competition hosted by Hack The Box. During the event, I undertook the challenge of exploring the field of blockchain-related challenges. In this post, I present my experience with two introductory-level smart contract challenges, which provide insight into their functionality and usage in this environment. It should be noted that no exploitation of these contracts was involved, and completion of the challenges only required a small amount of software development.
Smart Contracts
Acording to IBM, smart contracts refer to programs that are stored on the blockchain, and are commonly utilized for automating workflows or executing agreements in a manner that provides immediate certainty to all participants, without the involvement of intermediaries. These contracts are typically designed to receive funds from other users, and are most commonly deployed on the Ethereum network, which imposes a tax, known as gas, on transactions conducted on its platform. It is worth noting that once a smart contract has been uploaded, it becomes immutable, preventing any unauthorized tampering or deactivation, even by its creator.
Tools
Tool | Description |
---|---|
Web3.py | Python library used for interacting with the Ethereum network |
NetCat | Tool used to perform all kinds of network interactions |
Tutorial | Video about interacting with contract using web3.py |
Challenges
Navigating the Unknown
To begin, we were provided with two .sol files named Setup.sol and Unknown.sol. Our objective is to successfully execute the isSolved() function represented below, thereby transitioning it to a true state, in order to retrieve the flag.
pragma solidity ^0.8.18;
import {Unknown} from "./Unknown.sol";
contract Setup {
Unknown public immutable TARGET;
constructor() {
TARGET = new Unknown();
}
function isSolved() public view returns (bool) {
return TARGET.updated();
}
}
pragma solidity ^0.8.18;
contract Unknown {
bool public updated;
function updateSensors(uint256 version) external {
if (version == 10) {
updated = true;
}
}
}
As we can see the modification of the ‘updated’ variable is necessary to trigger the isSolved() function to return a true state. This can be accomplished by interacting with the contract through a specific call to the ‘updateSensors()’ function, using the argument ‘10’.
In addition to the provided files, we are able to create two instances. The first instance provides us with the RPC URL used for connecting to the contract chain, while the second instance provides us with a TCP port through which we can interact with the box via Netcat.
After that, i developed a Python script utilizing the web3.py library. The initial step involves establishing a connection with the provider through the implementation of Web3(Web3.HTTPProvider(url))
, enabling the program to interact with the network.
url = "http://139.59.176.230:30836"
provider = Web3(Web3.HTTPProvider(url))
In the next step i define the contract and keys related information. This information can be obtained trough the netcat connection.
private_key = '0xc350968e76eebceddf1e50f8a0a5ae642ae57ae89a9d69e7ef5a20f933d273a3'
address = '0x8108dF985e4c3A0D37561f54Ba61be089Ce2895b'
target_contract = '0xc7Ec76d9598933f43bDbB823f099232A0eB18BDb'
setup_contract = '0xDdbf779e2c6aCe4C87962a5938FEB436A78aD464'
After that I obtain the contract Application Binary Interface (ABI) which gives the program the ability to establish communications and interact with other smart contracts and applications on the Ethereum network.
compiled_sol = compile_source('''
pragma solidity ^0.8.18;
import {Unknown} from "./Unknown.sol";
contract Setup {
Unknown public immutable TARGET;
constructor() {
TARGET = new Unknown();
}
function isSolved() public view returns (bool) {
return TARGET.updated();
}
}
''', output_values=['abi', 'bin'])
contract_id, contract_interface = compiled_sol.popitem()
abi = contract_interface['abi']
With the ABI, I can connect to the target contract by creating an instance of the contract object using the ABI and Target Contract Address so it’s easier to interact with it. Additionaly, I also define the sender account with the given private key also obtained from the netcat connection.
target = provider.eth.contract(abi=abi,address=target_contract)
sender_account = provider.eth.account.from_key(private_key)
Finally, I built the transaction by calling the function updateSensors(version) from our adress. If the transaction is successful, the program should display a message indicating it.
nonce = provider.eth.get_transaction_count(address)
version = 10
transaction = target.functions.updateSensors(version).build_transaction({
'from': address,
'gas': 200000,
'nonce': nonce
})
Considering all the necessary elements, the final script is represented below:
from web3 import Web3
from solcx import compile_source, install_solc
url = "http://139.59.176.230:30836"
provider = Web3(Web3.HTTPProvider(url))
if provider.is_connected() == False:
print("Error connecting to provider")
exit()
private_key = '0xc350968e76eebceddf1e50f8a0a5ae642ae57ae89a9d69e7ef5a20f933d273a3'
address = '0x8108dF985e4c3A0D37561f54Ba61be089Ce2895b'
target_contract = '0xc7Ec76d9598933f43bDbB823f099232A0eB18BDb'
setup_contract = '0xDdbf779e2c6aCe4C87962a5938FEB436A78aD464'
install_solc(version='latest')
compiled_sol = compile_source('''
pragma solidity ^0.8.18;
import {Unknown} from "./Unknown.sol";
contract Setup {
Unknown public immutable TARGET;
constructor() {
TARGET = new Unknown();
}
function isSolved() public view returns (bool) {
return TARGET.updated();
}
}
''', output_values=['abi', 'bin'])
contract_id, contract_interface = compiled_sol.popitem()
abi = contract_interface['abi']
target = provider.eth.contract(abi=abi,address=target_contract)
sender_account = provider.eth.account.from_key(private_key)
nonce = provider.eth.get_transaction_count(address)
version = 10
transaction = target.functions.updateSensors(version).build_transaction({
'from': address,
'gas': 200000,
'nonce': nonce
})
signed_transaction = sender_account.sign_transaction(transaction)
transaction_hash = provider.eth.send_raw_transaction(signed_transaction.rawTransaction)
transaction_receipt = provider.eth.wait_for_transaction_receipt(transaction_hash)
if transaction_receipt.status == 1:
print("Transaction was successful!")
After this, we can run the script and it is possible to reconnect to the tcp port via and proceed to input “3” as well to confirm the presence of the flag, as shown in the image below.
Shooting 101
In the Shooting 101 challenge, two solidity files, namely Setup.sol and ShootingArea.sol, have been provided. The primary objective remains the same as the previous challenge, which is to ensure that the isSolved() function returns a true value. The contracts are represented as the following.
pragma solidity ^0.8.18;
import {ShootingArea} from "./ShootingArea.sol";
contract Setup {
ShootingArea public immutable TARGET;
constructor() {
TARGET = new ShootingArea();
}
function isSolved() public view returns (bool) {
return TARGET.firstShot() && TARGET.secondShot() && TARGET.thirdShot();
}
}
pragma solidity ^0.8.18;
contract ShootingArea {
bool public firstShot;
bool public secondShot;
bool public thirdShot;
modifier firstTarget() {
require(!firstShot && !secondShot && !thirdShot);
_;
}
modifier secondTarget() {
require(firstShot && !secondShot && !thirdShot);
_;
}
modifier thirdTarget() {
require(firstShot && secondShot && !thirdShot);
_;
}
receive() external payable secondTarget {
secondShot = true;
}
fallback() external payable firstTarget {
firstShot = true;
}
function third() public thirdTarget {
thirdShot = true;
}
}
The aim of this challenge is to sequentially “shoot” three designated targets in a prescribed order. To achieve this, it is are required to initially turn the firstShot variable to true, followed by the secondShot , and ultimately targeting the thirdShot.
In order to modify the value of the variable named “firstShot”, it is necessary to invoke the “firstTarget” function which serves as a fallback mechanism. This function is activated by the contract when it receives a transaction that does not correspond to any of its defined functions. Similarly, to modify the value of the variable named “secondShot”, the “secondTarget” function must be invoked. This function is associated with the “receive()” function and is triggered by the receipt of a value in a transaction. Finally, to set the value of the variable named “thirdShot” to true, it is necessary to invoke the third function. This is a standard function, similar to the one presented in the previous challenge, but does not take any arguments.
Upon comprehending the order of operations and what I needed to do to achieve each step, I start building the following script. First connect to the provider.
url = "http://165.232.108.249:30659"
provider = Web3(Web3.HTTPProvider(url))
Next define the contract and keys information required for interaction with the target contract.
private_key = '0x3d3139f960f206ba961eef2cb672b093bcc0fd2a01a655ef474148eed2c675de'
address = '0xc01BEDE1180cbAe04d1719CAf3ed1C35799392B7'
target_contract = '0x5F5325Ab209923C3aB71a73ec1a3e6b31D32E08B'
setup_contract = '0x5c5e7B57fd93429758E27ED62Fe475c66d2ec998'
Then proceeded to obtain the contract ABI
compiled_sol = compile_source('''
pragma solidity ^0.8.18;
import {ShootingArea} from "./ShootingArea.sol";
contract Setup {
ShootingArea public immutable TARGET;
constructor() {
TARGET = new ShootingArea();
}
function isSolved() public view returns (bool) {
return TARGET.firstShot() && TARGET.secondShot() && TARGET.thirdShot();
}
}
''', output_values=['abi', 'bin'])
contract_id, contract_interface = compiled_sol.popitem()
abi = contract_interface['abi']
Next step is to create an instance of the contract object and define the sender account to be used for the transaction.
setup_instance = provider.eth.contract(abi=abi, address=target_contract)
sender_account = provider.eth.account.from_key(private_key)
Then i started by invoking the fallback() function of the contract by defining the data with a fallback selector that triggers an invalid function call.
fallback_selector = "0x" + "0" * 62 + "1"
transaction = provider.eth.send_transaction({
'from': sender_account.address,
'to': target_contract,
'value': to_wei(0.1, "ether"),
'data': fallback_selector,
})
After that i create and send a transaction that transfers some coins to the target contract.
value = to_wei(0.1, 'ether')
transaction = provider.eth.send_transaction({
'from': sender_account.address,
'to': target_contract,
'value': value,
})
Finally, call the third() function of the contract, which is similar to the previous challenges.
nonce = provider.eth.get_transaction_count(sender_account.address)
transaction = setup_instance.functions.third().build_transaction({
'from': address,
'gas': 2000000,
'nonce': nonce
})
Ending up with the following script:
from web3 import Web3
from solcx import compile_source, install_solc
from eth_utils import to_wei
def verify_shots(setup_instance):
first_shot = setup_instance.functions.firstShot().call()
second_shot = setup_instance.functions.secondShot().call()
third_shot = setup_instance.functions.thirdShot().call()
print(f"firstShot: {first_shot}")
print(f"secondShot: {second_shot}")
print(f"thirdShot: {third_shot}")
url = "http://165.232.108.249:30659"
provider = Web3(Web3.HTTPProvider(url))
if provider.is_connected() == False:
print("Error connecting to provider")
exit()
private_key = '0x3d3139f960f206ba961eef2cb672b093bcc0fd2a01a655ef474148eed2c675de'
address = '0xc01BEDE1180cbAe04d1719CAf3ed1C35799392B7'
target_contract = '0x5F5325Ab209923C3aB71a73ec1a3e6b31D32E08B'
setup_contract = '0x5c5e7B57fd93429758E27ED62Fe475c66d2ec998'
install_solc(version='latest')
compiled_sol = compile_source('''
pragma solidity ^0.8.18;
import {ShootingArea} from "./ShootingArea.sol";
contract Setup {
ShootingArea public immutable TARGET;
constructor() {
TARGET = new ShootingArea();
}
function isSolved() public view returns (bool) {
return TARGET.firstShot() && TARGET.secondShot() && TARGET.thirdShot();
}
}
''', output_values=['abi', 'bin'])
contract_id, contract_interface = compiled_sol.popitem()
abi = contract_interface['abi']
setup_instance = provider.eth.contract(abi=abi, address=target_contract)
sender_account = provider.eth.account.from_key(private_key)
# First Shot
fallback_selector = "0x" + "0" * 62 + "1"
transaction = provider.eth.send_transaction({
'from': sender_account.address,
'to': target_contract,
'value': to_wei(0.1, "ether"),
'data': fallback_selector,
})
print(f"Transaction sent: {transaction.hex()}")
verify_shots(setup_instance)
# Second Shot
value = to_wei(0.1, 'ether')
transaction = provider.eth.send_transaction({
'from': sender_account.address,
'to': target_contract,
'value': value,
})
print(f"Transaction sent: {transaction.hex()}")
verify_shots(setup_instance)
# Third Shot
nonce = provider.eth.get_transaction_count(sender_account.address)
transaction = setup_instance.functions.third().build_transaction({
'from': address,
'gas': 2000000,
'nonce': nonce
})
signed_transaction = sender_account.sign_transaction(transaction)
transaction_hash = provider.eth.send_raw_transaction(signed_transaction.rawTransaction)
transaction_receipt = provider.eth.wait_for_transaction_receipt(transaction_hash)
verify_shots(setup_instance)
Now we can execute it and retrieve the flag as shown in the following picture.
Conclusion
To wrap things up these challenges provided an enjoyable opportunity to explore and experiment with blockchain technology.
Hope you liked this post.
Best Regards, Diogo.