Midnight Monologues

日々勉強したことを書いてきます

picoCTF 2023 - Writeup

March 14-28, 2023 UTCに開催されたpicoCTF 2023 に個人で参加した。 簡単な問題を軸に24/45問解いたので、特に印象的だった3問のWriteupを記載する。

Web

More SQLi



指定されたURLへアクセスするとログイン画面が表示される。



問題文よりSQLインジェクションを疑う。 ユーザ名に適当な文字列、パスワードに ' or 'a'='a' -- を入力するとログインできる。



Search Office画面にもSQLインジェクション脆弱性がある。 ' or true -- を入力すると全ての値が表示される。



HintよりバックエンドDBはSQLiteとあるので、sqlite_masterより情報を確認する。

テーブル名の取得
' union all select tbl_name,2,3 from sqlite_master  -- 



sqlの取得

SQLiteではDB作成時のCREATE文がsqlカラムに記載されており、構造が容易にわかる

' union all select sql,2,3 from sqlite_master  -- 



union句を使ったSQLインジェクションについて復習してみよう。 今回の問題ではoffice TABLEのカラムデータがWebサイト上に表示されている。



確認できたmore_table TABLEのidとflagカラムのデータをWebサイトに表示されているoffice TABLEのカラムに当てはめればよい。(結合先のカラム数が3のため、more_tableの2カラム+適当な数字で3カラムに合わせて当てはめる。)

flagの取得
' union all select id,flag,3 from more_table -- 



General Skills

Special



指定された接続先へアクセスするとUbuntu環境に接続される。

# ssh -p 53015 ctf-player@saturn.picoctf.net
ctf-player@saturn.picoctf.net's password: 
Welcome to Ubuntu 20.04.3 LTS (GNU/Linux 5.15.0-1031-aws x86_64)


適当なコマンドを入力しても別の文字に置換されて処理されない

Special$ pwd
Pod 
sh: 1: Pod: not found
Special$ ls
Is 


OSコマンドインジェクションをぼんやり考えながら以下のコマンドを打つと最後のコマンドが処理される。

Special$ a;a;pwd
A;a;pwd 
sh: 1: A: not found
sh: 1: a: not found
/home/ctf-player


catコマンドを使ってファイルを確認する。ホームディレクトリは/home/ctf-playerで配下にblarghフォルダがあるようだ。

Special$ a;a;cat *              
A;a;cat * 
sh: 1: A: not found
sh: 1: a: not found
cat: blargh: Is a directory


blarghフォルダ配下を確認するとflagが確認できる。

Special$ a;a;cat blargh/*
A;a;cat blargh/* 
sh: 1: A: not found
sh: 1: a: not found
picoCTF{5p311ch3ck_15_7h3_w0r57_6a2763f6}


※因みに問題のスクリプトは以下の通り

Special$ a;a;cat ../../challenge/*
A;a;cat ../../challenge/* 
sh: 1: A: not found
sh: 1: a: not found
################################################################################
# Configure a box for the custom ssh example challenge.
################################################################################


import sys
import os
import subprocess
import re
import zlib
import json



def main():

    try:
        # Generate password from seed =========================================
        seed = os.environ.get("SEED")
        
        if seed == "":
            print("Seed was not read from filesystem. Aborting.")
            sys.exit(1)

        password = hex(zlib.crc32(seed.encode()))
        password = password[2:]
        
        # =====================================================================


        # Make new dirs =======================================================
        subprocess.run(
            ["/bin/sh", "-c", "mkdir /home/ctf-player"], check=True
        )
        
        subprocess.run(
            ["/bin/sh", "-c", "mkdir /home/ctf-player/blargh"], check=True
        )

        # =====================================================================



        # Create ctf-player user
        subprocess.run(
            [
                "/usr/sbin/useradd",
                "-U",
                "ctf-player",
                "-d",
                "/home/ctf-player",
                "-s",
                "/usr/local/Special.py",
            ],
            check=True,
        )
        
        # Pipe the output of echo into chpasswd to change password of
        # ctf-user
        pEcho = subprocess.Popen(
            (
                'echo', 
                f'ctf-player:{password}'
            ),
            stdout=subprocess.PIPE
        )
        
        output = subprocess.check_output(('chpasswd'), stdin=pEcho.stdout)
        
        pEcho.wait()
        
        # Make sure ownership is changed to ctf-player
        subprocess.run(
            [
                "/usr/bin/chown",
                "-R",
                "ctf-player:ctf-player",
                "/home/ctf-player/",
            ],
            check=True,
        )
        
        # Copy profile to ctf-player
        #subprocess.run(
        #    [
        #        "/usr/bin/cp",
        #        "/challenge/profile",
        #        "/home/ctf-player/.profile",
        #    ],
        #    check=True,
        #)
        
        
        # =====================================================================


        # Split flag into 3 parts  ============================================
        flag = os.environ.get("FLAG")

        if flag == "":
            print("Flag was not read from environment. Aborting.")
            sys.exit(-1)
        else:
            # Get hash part
            flag_rand = re.search("{.*}$", flag)
            if flag_rand == None:
                print("Flag isn't wrapped by curly braces. Aborting.")
                sys.exit(-2)
            else:
                flag_rand = flag_rand.group()
                flag_rand = flag_rand[1:-1]

        flag = "picoCTF{5p311ch3ck_15_7h3_w0r57_" + flag_rand + "}"
        
        with open("/home/ctf-player/blargh/flag.txt", "w") as f:
            f.write(flag)
            
        # =====================================================================


        # Create and update metadata.json =====================================

        metadata = {}
        metadata['flag'] = str(flag)
        metadata['password'] = str(password)
        json_metadata = json.dumps(metadata)
        
        with open("/challenge/metadata.json", "w") as f:
            f.write(json_metadata)

        # =====================================================================

    except subprocess.CalledProcessError:
        print("A subprocess has returned an error code")
        sys.exit(1)



# =============================================================================


if __name__ == "__main__":
    main()

{"flag": "picoCTF{5p311ch3ck_15_7h3_w0r57_6a2763f6}", "password": "af86add3"}


Special$ a;a;cat ../../../../usr/local/Special.py  
A;a;cat ../../../../usr/local/Special.py 
sh: 1: A: not found
sh: 1: a: not found
#!/usr/bin/python3

import os                                                                                                                         
from spellchecker import SpellChecker                                                                                             
                                                                                                                                  
                                                                                                                                  
                                                                                                                                  
spell = SpellChecker()                                                                                                            
                                                                                                                                  
while True:                                                                                                                       
  cmd = input("Special$ ")
  rval = 0
  
  if cmd == 'exit':
    break
  elif 'sh' in cmd:
    print('Why go back to an inferior shell?')
    continue
  elif cmd[0] == '/':
    print('Absolutely not paths like that, please!')
    continue
    
  # Spellcheck
  spellcheck_cmd = ''
  for word in cmd.split():
    fixed_word = spell.correction(word)
    if fixed_word is None:
      fixed_word = word
    spellcheck_cmd += fixed_word + ' '

  # Capitalize
  fixed_cmd = list(spellcheck_cmd)
  words = spellcheck_cmd.split()
  first_word = words[0]
  first_letter = first_word[0]
  if ord(first_letter) >= 97 and ord(first_letter) <= 122:
    fixed_cmd[0] = chr(ord(spellcheck_cmd[0]) - 0x20)
  fixed_cmd = ''.join(fixed_cmd)
  
  try:
    print(fixed_cmd)
    os.system(fixed_cmd)
  except:
    print("Bad command!")



Specialer



指定された接続先へアクセスするとbash環境に接続される。

# ssh -p 64029 ctf-player@saturn.picoctf.net
ctf-player@saturn.picoctf.net's password: 
Specialer$


今度は利用できるコマンドと利用できないコマンドがある。

Specialer$ pwd
/home/ctf-player
Specialer$ 
Specialer$ ls
-bash: ls: command not found
Specialer$  <-----------------------Tabキーを入力してコマンドの保管処理を実施
!          bg         compgen    do         exec       function   kill       pwd        shopt      true       wait
./         bind       complete   done       exit       getopts    let        read       source     type       while
:          break      compopt    echo       export     hash       local      readarray  suspend    typeset    {
[          builtin    continue   elif       false      help       logout     readonly   test       ulimit     }
[[         caller     coproc     else       fc         history    mapfile    return     then       umask      
]]         case       declare    enable     fg         if         popd       select     time       unalias    
alias      cd         dirs       esac       fi         in         printf     set        times      unset      
bash       command    disown     eval       for        jobs       pushd      shift      trap       until      


cdコマンドを使いながら構成を確認すると以下の構成であることが分かる。

root/
 ├ lib/
 ├ lib64/
 ├ home/
 │ └ ctf-player/
 │   ├ .hushlogin 
 │   ├ .profile
 │   ├ abra/ 
 │     │    ├ cadabra.txt
 │     │    └ cadaniel.txt
 │   ├ ala/
 │     │    ├ kazam.txt
 │     │    └ mode.txt
 │   └ sim/
 │          ├ city.txt 
 │          └ salabim.txt 
 └ bin/
    └ bash


利用できるコマンド一覧からwhileコマンドとreadコマンドを利用して読み取りを試みるが、.profile以外は権限不足で何も表示されない。

Specialer$ while read line; do echo "read line:$line"; done < ./.profile
read line:export PS1='Specialer$ '
Specialer$ while read line; do echo "read line:$line"; done < ./.hushlogin 
Specialer$ while read line; do echo "read line:$line"; done < ./abra/cadabra.txt 
Specialer$ while read line; do echo "read line:$line"; done < ./ala/mode.txt 


アプローチを変えて以下コマンドで実行するとテキスト文字列を確認できる。

Specialer$ echo "$(<./abra/cadaniel.txt)"
Yes, I did it! I really did it! I'm a true wizard!
Specialer$ echo "$(<./ala/kazam.txt)"
return 0 picoCTF{y0u_d0n7_4ppr3c1473_wh47_w3r3_d01ng_h3r3_38f5cc78}
Specialer$ echo "$(<./sim/city.txt)"
05ed181c-4aa0-4d4a-8505-2fe6ca9097d3
Specialer$ echo "$(<./sim/salabim.txt)"
#He was so kind, such a gentleman tied to the oceanside#