Diberikan sebuah web beserta source code.
Link source code: https://drive.google.com/open?id=1cJPV4_bjRzMO_woqrx6_2vHp7UFsXzAY
Berikut adalah source codenya (hanya diambil bagian modul fungsionalitas website saja):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
package api import ( "encoding/json" "net/http" "golang.org/x/crypto/bcrypt" "heejin/database" "heejin/model" "heejin/response" "heejin/session" ) type loginData struct { Token string `json:"token"` } func (h Handler) Login(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() var userLogin model.User err := json.NewDecoder(r.Body).Decode(&userLogin) if err != nil { response.Write(w, response.BuildError(err), http.StatusBadRequest) return } var user model.User if database.MySQL.Where("username = ?", userLogin.Username).First(&user).RecordNotFound() { response.Write(w, response.BuildError(response.ErrInvalidUsernameOrPassword), http.StatusUnauthorized) return } if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(userLogin.Password)); err != nil { response.Write(w, response.BuildError(response.ErrInvalidUsernameOrPassword), http.StatusUnauthorized) return } token, err := session.Sign(user.ID) if err != nil { response.Write(w, response.BuildError(response.ErrInternalServerError), http.StatusInternalServerError) return } response.Write(w, response.BuildSuccess(loginData{token}), http.StatusOK) } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
package api import ( "net/http" "heejin/database" "heejin/model" "heejin/response" ) func (h Handler) Me(w http.ResponseWriter, r *http.Request) { userID := r.Context().Value("UserID").(uint64) var user model.User if result := database.MySQL.First(&user, userID); result.Error != nil { response.Write(w, response.BuildError(response.ErrInternalServerError), http.StatusInternalServerError) return } user.Password = "" response.Write(w, response.BuildSuccess(user), http.StatusOK) } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
package api import ( "fmt" "net/http" "strconv" "github.com/gorilla/mux" "heejin/database" "heejin/model" "heejin/response" ) var errNotEnoughMoney = fmt.Errorf("Not enough money") func (h Handler) ProductAll(w http.ResponseWriter, r *http.Request) { var products []model.Product if result := database.MySQL.Find(&products); result.Error != nil { response.Write(w, response.BuildError(response.ErrInternalServerError), http.StatusInternalServerError) return } response.Write(w, response.BuildSuccess(products), http.StatusOK) } func (h Handler) ProductBuy(w http.ResponseWriter, r *http.Request) { id, err := strconv.ParseInt(mux.Vars(r)["id"], 10, 64) if err != nil { response.Write(w, response.BuildError(err), http.StatusBadRequest) return } var product model.Product if result := database.MySQL.First(&product, id); result.Error != nil { if result.RecordNotFound() { response.Write(w, response.BuildError(response.ErrNotFound), http.StatusNotFound) return } response.Write(w, response.BuildError(response.ErrInternalServerError), http.StatusInternalServerError) return } tx := database.MySQL.Begin() if err := tx.Error; err != nil { response.Write(w, response.BuildError(response.ErrInternalServerError), http.StatusInternalServerError) return } userId := r.Context().Value("UserID").(uint64) var user model.User if result := tx.Set("gorm:query_option", "FOR UPDATE").First(&user, userId); result.Error != nil { tx.Rollback() response.Write(w, response.BuildError(response.ErrInternalServerError), http.StatusInternalServerError) return } if user.Money < product.Price { tx.Rollback() response.Write(w, response.BuildError(errNotEnoughMoney), http.StatusUnprocessableEntity) return } user.Money -= product.Price if result := tx.Model(&user).Update("money", user.Money); result.Error != nil { tx.Rollback() response.Write(w, response.BuildError(response.ErrInternalServerError), http.StatusInternalServerError) return } order := model.Order{UserID: user.ID, ProductID: product.ID} if result := tx.Create(&order); result.Error != nil { tx.Rollback() response.Write(w, response.BuildError(response.ErrInternalServerError), http.StatusInternalServerError) return } tx.Commit() response.Write(w, response.BuildSuccess(nil), http.StatusOK) } func (h Handler) ProductFlag(w http.ResponseWriter, r *http.Request) { productID, err := strconv.ParseInt(mux.Vars(r)["id"], 10, 64) if err != nil { response.Write(w, response.BuildError(err), http.StatusBadRequest) return } userID := r.Context().Value("UserID").(uint64) var count uint64 if result := database.MySQL.Model(&model.Order{}).Where("product_id = ? AND user_id = ?", productID, userID).Count(&count); result.Error != nil && !result.RecordNotFound() { response.Write(w, response.BuildError(response.ErrInternalServerError), http.StatusInternalServerError) return } if count == 0 { err = fmt.Errorf("You have to buy this product") response.Write(w, response.BuildError(err), http.StatusForbidden) return } var product model.Product if result := database.MySQL.First(&product, productID); result.Error != nil { response.Write(w, response.BuildError(response.ErrInternalServerError), http.StatusInternalServerError) return } response.Write(w, response.BuildSuccess(product.Flag), http.StatusOK) } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
package api import ( "encoding/json" "fmt" "net/http" "regexp" "github.com/go-sql-driver/mysql" "golang.org/x/crypto/bcrypt" "heejin/database" "heejin/model" "heejin/response" ) var ( usernameRegex = regexp.MustCompile("^[A-Za-z0-9]{6,20}$") minPasswordLength = 6 ) func (h Handler) Register(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() var user model.User err := json.NewDecoder(r.Body).Decode(&user) if err != nil { response.Write(w, response.BuildError(err), http.StatusBadRequest) return } err = validateRegister(user) if err != nil { response.Write(w, response.BuildError(err), http.StatusUnprocessableEntity) return } hashed, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost) if err != nil { response.Write(w, response.BuildError(err), http.StatusInternalServerError) return } user.Password = string(hashed) if result := database.MySQL.Create(&user); result.Error != nil { sqlErr, ok := result.Error.(*mysql.MySQLError) if ok { if sqlErr.Number == 1062 { err = fmt.Errorf("User already exists") response.Write(w, response.BuildError(err), http.StatusUnprocessableEntity) return } } response.Write(w, response.BuildError(response.ErrInternalServerError), http.StatusInternalServerError) return } response.Write(w, response.BuildSuccess(nil), http.StatusCreated) } func validateRegister(user model.User) error { if !usernameRegex.MatchString(user.Username) { return fmt.Errorf("Username must be alphanumeric with 6-20 characters") } if len(user.Password) < minPasswordLength { return fmt.Errorf("Password must be at least %d characters", minPasswordLength) } return nil } |
1 2 3 4 5 6 7 8 9 |
package model type User struct { ID uint64 `json:"id" gorm:"primary_key;type:bigint"` Username string `json:"username" gorm:"type:varchar(20);unique;not null"` Password string `json:"password,omitempty" gorm:"type:varchar(255)";not null` Money uint64 `json:"money" gorm:"type:bigint;not null;default:100000"` Orders []Order `json:"-"` } |
Web ini sebenarnya cukup sederhana, yang kita bisa lakukan adalah:
- Register
- Login
- Akan mendapatkan sebuah akun dengan jumlah uang Rp 100.000
Lalu untuk mendapatkan flag, kita tidak bisa semena-mena klik ke “Flag” karena untuk produk yang kita mau dapatkan flagnya, kita harus membeli produk tersebut terlebih dahulu.
Sekarang, untuk goal kita mendapatkan flag, akun yang kita miliki harus memiliki minimal uang Rp 13.371.337. Tapi bagaimana kita bisa mau mendapatkan flag? Sedangkan yang kita dapat saat register hanyalah Rp 100.000.
Disini, saat proses pengerjaan soal kualifikasi lalu, penulis lupa memeriksa bagian user.go yang mengakibatkan penulis menulis beberapa informasi kecil sebagai asumsi pada writeup yang dikumpulkan ke panitia.
Untuk bagaimana caranya kita mendapatkan flag, awal penulis memeriksa source code untuk kelemahan sebagai berikut:
- JWT
- SQL Injection
Struggle penulis berlanjut sampai di satu titik penulis mendapatkan sebuah pencerahan tapi dengan cara yang menurut penulis terbilang aneh 😀 .Penulis teringat pada soal dari Harekaze CTF 2019 soal Encode and Encode yang berhubungan dengan JSON decode di PHP. Untuk alasan aneh soal itu memberikan pencerahan sehingga penulis bisa solve soal ini sebelum waktu kualifikasi selesai lel.
Mari kita lihat kembali di source bagian register.go.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
func (h Handler) Register(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() var user model.User err := json.NewDecoder(r.Body).Decode(&user) if err != nil { response.Write(w, response.BuildError(err), http.StatusBadRequest) return } err = validateRegister(user) if err != nil { response.Write(w, response.BuildError(err), http.StatusUnprocessableEntity) return } hashed, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost) if err != nil { response.Write(w, response.BuildError(err), http.StatusInternalServerError) return } user.Password = string(hashed) if result := database.MySQL.Create(&user); result.Error != nil { sqlErr, ok := result.Error.(*mysql.MySQLError) if ok { if sqlErr.Number == 1062 { err = fmt.Errorf("User already exists") response.Write(w, response.BuildError(err), http.StatusUnprocessableEntity) return } } response.Write(w, response.BuildError(response.ErrInternalServerError), http.StatusInternalServerError) return } response.Write(w, response.BuildSuccess(nil), http.StatusCreated) } |
Pada fungsi ini, program akan melakukan proses decode pada semua value pada JSON (source) yang disubmit. Lalu memeriksa apakah di titik tertentu ada error atau tidak jika iya maka proses registrasi akan gagal.
Lalu apa yang dapat dilakukan dengan informasi ini? Untuk lebih lanjutnya lebih baik dilihat langsung proses request dengan menggunakan burpsuite.
Jika dilihat, saat akan melakukan registrasi, user akan mengirimkan request dalam bentuk JSON dengan 2 parameter yakni username dan password. Lalu untuk JSON ini yang nantinya akan di decode oleh API secara mentah-mentah dan dari sana akan dimasukkan sebagai parameter untuk insert ke dalam database. Lalu apa?
Setelah melihat source secara penuh, penulis menemukan titik menarik di dalam user.go.
1 |
Money uint64 `json:"money" gorm:"type:bigint;not null;default:100000"` |
Pada barisan code diatas, sebenarnya jika user tidak menentukan jumlah uang yang ingin dimasukkan ke dalam database, secara otomatis akan dimasukkan angka “100000” sesuai dengan yang di default.
Maka dari itu, bagaimana jika saat proses registrasi, user menentukan jumlah uang dalam request? Tentu saja uang yang masuk ke dalam database akan sesuai dengan yang user tentukan.
Jadi dari burpsuite, penulis tinggal melakukan intercept pada request yang akan dikirim dan menambahkan parameter “money”.
Disini penulis menambahkan di JSON parameter money dengan jumlah 50jt. Dari sini, maka uang yang masuk ke dalam database tidak akan default.
Setelah berhasil melakukan registrasi, penulis tinggal login dan membeli dan mendapatkan flag.
Flag: CJ2019{l3t5_9eT_r1cH_l1k3_H33j1n}