Seccon_Bignners_2022
今回初めて本コンペに参加した。
しかし、1問も解けずに時間切れになってしまった。(Welcomeは誰でもできる)
それで、easyレベルの問題についての復習をするために本記事を記載した。(完全に自分用)
参考にしたサイトや解説
Web問題
1.Util
問題のサイトに行くと以下のようなサイトが表示される。
とりあえず、localhost(127.0.0.1)のアドレスを入力してみる。
pingの結果を返してくれている模様。
入力フォームがあるので、うまくコマンドインジェクションができそう。。。
メインのソースコードを見てみる。
func main() {
r := gin.Default()
r.LoadHTMLGlob("pages/*")
r.GET("/", func(c *gin.Context) {
c.HTML(200, "index.html", nil)
})
r.POST("/util/ping", func(c *gin.Context) {
var param IP
if err := c.Bind(¶m); err != nil {
c.JSON(400, gin.H{"message": "Invalid parameter"})
return
}
commnd := "ping -c 1 -W 1 " + param.Address + " 1>&2"
result, _ := exec.Command("sh", "-c", commnd).CombinedOutput()
c.JSON(200, gin.H{
"result": string(result),
})
})
if err := r.Run(); err != nil {
panic(err)
}
}
commndという変数にparam.Addressという変数をつなげて、pingのコマンドとし実行している。
つまり、param.Addressにコマンドを埋め込めれば目的のflagがゲットできそう。
ここでのparam.AddressはWebページからのフォームから入力されたデータに該当している。(詳しくはWebページのHTMLとmain.goを参照。パラメータのaddressに該当していることが分かる。)
そしたら、入力フォームにそれっぽいコマンドを入れてみる。
「127.0.0.1;ls /」とか
無効なIPアドレスだと怒られました。
表示されているWebページのHTMLを見てみる。
しっかりと入力制限がかけられているおり、正しく入力されていない場合は「Invalid IP address」を返す制御をしている。
しかし、該当の処理はフロントエンド側(Webページ)で完結している感じがする、、、
(通信をしている痕跡がなく、メインのプログラムコードにも上記のような制御は存在しない)
つまり、リクエストをWebページからではなく別の方法で行えば、コマンドインジェクションが成立しそう。
正常に入力するとpingというリクエストが投げられているので、このリクエストをcURLでコピーしてターミナルで実行してみる。
ちゃんと、Webページと同じ結果が確認できた。
次に、入力フォームのデータに該当する「address:127.0.0.1」の部分に先ほどのコマンドを入力してみる。
そしたら、ファイル一覧の中に「flag_A74FIBkN9sELAjOc.txt」というflagっぽいtxtファイルを発見。
中身をcatで見てみる
flagをゲット
3.Gallery
問題のサイトに行くと以下のようなサイトが表示される。
Plese Select!の部分がドロップダウンリストになっている。
pngを選択すると、それに該当する.pngファイルが表示される。
ファイルをクリックすると、画像が表示される。
おそらく、ドロップダウンリストの選択肢にはないが、flagに関連する画像ファイルが用意されていると思われる。
とりあえず、メインのプログラムを見てみる。
func main() {
r := mux.NewRouter()
r.PathPrefix("/images/").Methods("GET").Handler(http.StripPrefix("/images/", http.FileServer(http.Dir(DIR))))
r.HandleFunc("/", IndexHandler)
http.ListenAndServe(":"+PORT, middleware()(r))
}
どうやら、Httpリクエストをもらって、IndexHandlerとmiddlewareって関数で処理をしてから出力を返してる模様。
IndexHandler関数の中身を見てみる。
func IndexHandler(w http.ResponseWriter, r *http.Request) {
t, err := template.New("index.html").ParseFiles("./static/index.html")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Println(err)
return
}
// replace suspicious chracters
fileExtension := strings.ReplaceAll(r.URL.Query().Get("file_extension"), ".", "")
fileExtension = strings.ReplaceAll(fileExtension, "flag", "")
if fileExtension == "" {
fileExtension = "jpeg"
}
log.Println(fileExtension)
data := Embed{}
data.ImageList, err = getImageList(fileExtension)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Println(err)
return
}
if err := t.Execute(w, data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Println(err)
return
}
}
クエリのfile_extensionがflagだった場合は.jpegを返すような制御がされている。
さらに、次の処理でgetImageListという関数を呼び出して、file_extensionの内容に応じたfileを出力するように制御している。
func getImageList(fileExtension string) ([]string, error) {
files, err := os.ReadDir("static")
if err != nil {
return nil, err
}
res := make([]string, 0, len(files))
for _, file := range files {
if !strings.Contains(file.Name(), fileExtension) {
continue
}
res = append(res, file.Name())
}
return res, nil
}
getImageListでは、file_extensionの内容の中にファイル名に含めれているかを検証し、含まれていればそれに該当するファイル名を返している模様。
つまり、リクエストでfile_extensionにflagを入力しても.jpegしか表示されないが、f,l,a,gのどれかを入力すればflagに該当するファイル名のものを表示してくれるかもしれない。
とりあえず、file_extension=fでURLを入力してみる。
flagぽいpdfが表示された。該当のファイルを開いてみる。
全て「?」になってしまっている、、、
なんか暗号化されてるのかと思ったが、メインプログラムでIndexHandler関数とは別にmiddleware関数が呼ばれてたはず。
次に、middleware関数を見てみる。
func middleware() func(http.Handler) http.Handler {
return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
h.ServeHTTP(&MyResponseWriter{
ResponseWriter: rw,
lengthLimit: 10240, // SUPER SECURE THRESHOLD
}, r)
})
}
}
lengthLimitって変数が10240に限定されている。
変数の名前的に何かしらの長さの限界値を示しているよう。
他にlengthLimitが使われているところがないか探してみる。
以下の関数で使用されている。
func (w *MyResponseWriter) Write(data []byte) (int, error) {
filledVal := []byte("?")
length := len(data)
if length > w.lengthLimit {
w.ResponseWriter.Write(bytes.Repeat(filledVal, length))
return length, nil
}
w.ResponseWriter.Write(data[:length])
return length, nil
}
データの長さがlengthLimit以上であったら全て「?」に書き換える処理を行なっている。
つまり、一定以上のデータの長さよりデータを受け取れないようにしている。
したがって、データを分割してもらってから復元すれば良さそう。(やり方はわからんが、、、)
どうやらHTTPには範囲指定してデータをもらえる方法があるっぽい。
https://developer.mozilla.org/ja/docs/Web/HTTP/Range_requests
こんな感じで指定できるらしい。
curl http://i.imgur.com/z4d4kWk.jpg -i -H "Range: bytes=0-1023"
いけたっぽい、これで0〜1023[bytes]まで抜き取れた。
ちまちま抜き取るのはめんどいから1回で10000[bytes]まで抜き取る。
2回で全て抜き取れた、データファイルは16084[bytes]だった。
(lengthLimitの10240より6000ぐらいオーバーしていた、、、)
分割した2つのファイルをcatでくっつける。
cat out1 out2 > out3
くっつけたら、しっかりとpdfとして認識された。
開いてみると、flagが記載されている。
最後
その他の問題の解説気が向いたら更新します、、、、