On December 30th, while most of us were preparing for a New Year’s Eve celebration, the US Treasury was prepping a notice to lawmakers to notify them that their systems, which (obviously) contain highly sensitive, confidential data, had been compromised.
(Honestly, I’m not sure how I missed this news. Usually I’m pretty plugged in, especially to, like, open-source software vulnerabilities that affect my government’s treasury department. 🤷♂️)
Out of compliance, the US Treasury posted this notice to US lawmakers, breaking the news that a “China state-sponsored Advanced Persistent Threat (APT) actor” had breached their systems.
And that’s not even the craziest part! Wait till I tell you how they did it!
Well, I’m not going to keep it a secret. It was good, ol’ SQL injection. (More on SQL injection in a little bit.)
These US Treasury servers were/are protected, in part, by a Privileged Access Management (PAM) tool from Beyond Trust (which, I must say, is a fantastic name for a security company). Unfortunately for them (and, well, for all of us with records at the US Treasury (😅)), it would be Beyond Trust that would have to break the news to the government, as it was their software that served as the entry point for the hackers.
But let’s not all dogpile on and throw tomatoes at Beyond Trust. This could have been any of us. Because at the core of the vulnerability was PostgreSQL–one of the most commonly used relational databases in the world.
With one caveat—it’s not exactly a blatant SQL injection vulnerability. The attack requires you to be using the output of a Postgres internal string escaping method and feeding that directly into psql (the CLI tool for Postgres). Or how they put it:
> Specifically, SQL injection requires the application to use the function result to construct input to psql, the PostgreSQL interactive terminal. – From the PostgreSQL CVE Announcement
So how was there a zero-day in PostgreSQL, that had just been sitting there for at least 9 years, maybe longer? And not just that, but a SQL injection vulnerability?
For the uninitiated, SQL injection is a vulnerability as old as time.
“Do not cite the Deep Magic to me, Witch! I was there when it was written!” – Aslan (King of Narnia)
SQL injection has long been a cornerstone of security 101 for developers and security researchers alike. It’s, like, the first thing, in the first chapter, of every cybersecurity textbook, ever.
Take, for example, this horrible ruby code.
# @Substack, please add syntax highlighting. I promise it wouldn't be that hard.
require 'pg'
# Connect to PostgreSQL database
conn = PG.connect(dbname: 'testdb', user: 'user', password: 'password')
# Get user input from command line
puts "Enter your username:"
username = gets.chomp
# Simple query vulnerable to SQL injection
result = conn.exec("SELECT * FROM users WHERE username = '#{username}'")
# Display the result
puts result.getvalue(0, 0) # Assuming the result has a column
Here, if the user enters admin' OR '1' = '1
as their username, the query that actually gets executed is:
SELECT * FROM users WHERE username = 'admin' OR '1' = '1'
Since 1 == 1
, this query will return all users in the database.
This is a contrived example, but we can extrapolate how this pattern could be used for must more sinister things. Like bobby tables. (You thought I was going to talk about SQL injection without bringing up bobby tables??)
A better way to write this would be:
conn.exec_params("SELECT * FROM users WHERE username = $1", [username])
This code uses the pg
gem’s exec_params
method, which handles placing the sanitized username at our placeholder, $1
.
NOTE: This improved ruby code includes what is sometimes referred to as a “prepared statement”. Prepared statements are sort of the industry standard in protecting against SQL injections.
This category of attack is so well-known that its existence has almost become a given – it’s just something that all developers know to guard against. So, it’s nothing short of wild that a SQL injection vulnerability sat undiscovered in PostgreSQL, one of the most heavily scrutinized open source projects (up there with the linux kernel), for ten years. In a system that has been used by countless developers and security experts, how could something so basic go unnoticed for so long?
Well, this SQL injection wasn’t as simple as our contrived example (or the case of bobby tables).
At the core of the entire attack was just two bytes: c0 27
.
NOTE: The bytes,
c0 27
are displayed in hexadecima