
On Monday, the NSA announced a critical vulnerability (CVE-2020–060) in Windows 10 which allows an attacker to “undermine how Windows verifies cryptographic trust and can enable remote code execution.”
“Exploitation of the vulnerability allows attackers to defeat trusted network connections and deliver executable code while appearing as legitimately trusted entities. Examples where validation of trust may be impacted include:
- HTTPS connections
- Signed files and emails
- Signed executable code launched as user-mode processes”
Understanding which devices have not been patched within your organization is a critical first-step. For users who need cross-platform device visibility, a common solution is osquery.
Osquery is an open-source endpoint agent which allows you to query devices in real-time with SQL as if they were a relational database. You can retrieve lists of installed applications, running processes, listening ports and more.
Finding vulnerable devices
In this case, we can utilize the power of osquery to determine which patches have been installed and whether the CVE-2020–0601 hotfix has been applied. You can use the following query to find devices who lack the hotfix entirely:
SELECT'true'ASCVE_2020_0601_vulnerableWHERENOTEXISTS(SELECT1FROMpatchesWHEREhotfix_idIN('KB4534306','KB4534271','KB4534276','KB4534293','KB4534273','KB4535550','KB4528760'));
While this query is good, we can make it better. We run a slightly different Check within Kolide that is more complicated which you can find at the bottom of this article.
Patching the vulnerability
Now we know which devices have the problem, but we still don’t have a way to fix it.
That’s where Kolide’s Checks feature comes in. Kolide identifies the devices that are vulnerable, and then notifies your end-users automatically via Slack, alerting them of the problem, and giving them self-fix instructions.
When a user fixes an issue, they can click a button in Slack to recheck the device in real-time. No tedious back and forth with the call-center, just resolution.
And there you have it, a way to identify this issue at scale, educate your affected users, and patch the vulnerability, all without lifting a finger.
Not a Kolide customer yet? No problem! You can trial Kolide for free (no payment information required), by signing up here: Kolide Free Trial
Nuances of patch reporting (improving our original query)
After running Windows Update, the hotfix reports back as installed. Unfortunately, however, it technically does not complete the installation until after the device has been restarted.
What follows, is a step-wise walkthrough of crafting a SQL query which accommodates this behavior. If you are not interested in learning about writing SQL, I would recommend ducking out. If you are an osquery power-user buckle up and let’s dive in.
We need to look for the following:
- Hotfix not installed
- Computer not rebooted since Hotfix installation
Time of last reboot
Let’s start with determining whether the device has been rebooted since the patch was installed. We can compute the user’s local time of last reboot for a device using the following query:
SELECTdatetime(time.unix_time-uptime.total_seconds,'unixepoch')ASlast_rebootedFROMtime,uptime;
+---------------------+
| last_rebooted |
+---------------------+
| 2019-12-13 14:25:11 |
+---------------------+
Time of patch installation
We need to compare the last_rebooted
value against the time of the of patch
installation. We can see that the patches
table includes a column called
installed_on
, unfortunately for us it is not formatted using the standard
YYYYMMDD
ISO8601 convention.
SELECT*FROMpatchesWHEREhotfix_idIN('KB4534306','KB4534271','KB4534276','KB4534293','KB4534273','KB4535550','KB4528760');
csname = LENOVO-THINKPAD
hotfix_id = KB4534273
caption = http://support.microsoft.com/?kbid=4534273
description = Security Update
fix_comments =
installed_by = NT AUTHORITY\SYSTEM
install_date =
installed_on = 1/15/2020
As we can see our times are quite different:
The string 2019–12–13 14:25:11
cannot be compared against 1/15/2020
In order to compare the installed_on
time to the last_rebooted
time we will
need to perform some string operations to get this value into YYYYMMDD
.
Typically, we could use a standard SPLIT
operator to pull the date apart into
3 separate columns: year
, month
, day
:
WITHdate_valueAS(SELECT'1/15/2020'ASinstalled_on),split_dateAS(SELECTSPLIT(installed_on,'/',2)ASyear,SPLIT(installed_on,'/',0)ASmonth,SPLIT(installed_on,'/',1)ASdayFROMdate_value)SELECT*FROMsplit_date;
+------+-------+-----+
| year | month | day |
+------+-------+-----+
| 2020 | 1 | 15 |
+------+-------+-----+
Already, we can see a problem. Microsoft does not adhere to the convention of
leading 0
‘s for its dates. We will need to prepend some of our months and days
with 0
but only the ones that are single digits, otherwise we will get:
WITHdate_valueAS(SELECT'1/15/2020'ASinstalled_on),split_dateAS(SELECTSPLIT(installed_on,'/',2)ASyear,SPLIT(installed_on,'/',0)ASmonth,SPLIT(installed_on,'/',1)ASdayFROMdate_value)SELECTyear,('0'||month)ASmonth,('0'||day)ASdayFROMsplit_date;
+------+-------+-----+
| year | month | day |
+------+-------+-----+
| 2020 | 01 | 015 |
+------+-------+-----+
Our day column is reporting back as 015
because we prepended an existing 2 digit string. Let’s wrap our CONCAT
(||
) function in a
SUBSTR(substring)so that we can pull only the 2 ending characters from the
monthand
day` columns:
WITHdate_valueAS(SELECT'1/15/2020'ASinstalled_on),split_dateAS(SELECTSPLIT(installed_on,'/',2)ASyear,SPLIT(installed_on,'/',0)ASmonth,SPLIT(installed_on,'/',1)ASdayFROMdate_value)SELECTyear,SUBSTR(('0'||month),-2)ASmonth,SUBSTR(('0'||day),-2)ASdayFROMsplit_date;
+------+-------+-----+
| year | month | day |
+------+-------+-----+
| 2020 | 01 | 15 |
+------+-------+-----+
Great! We can now take our SPLIT
columns and reassemble them into a viable ISO 8601 timestamp using the ||
concatenate function. Between each column we will place a || '-' ||
and at the end we will add 00:00:01
.
* Assuming the install time is the start of the day 00:00:01
is less than perfect and could lead to the occasional false positive. However, we feel it is better to fail safe, rather than relying on a query that may leave machines in an incomplete patched state:
WITHdate_valueAS(SELECT'1/15/2020'ASinstalled_on),split_dateAS(SELECTSPLIT(installed_on,'/',2)ASyear,SPLIT(installed_on,'/',0)ASmonth,SPLIT(installed_on,'/',1)ASdayFROMdate_value)SELECTyear||'-'||SUBSTR(('0'||month),-2)||'-'||SUBSTR(('0'||day),-2)||' '||'00:00:01'ASinstall_date_utcFROMsplit_date;
+---------------------+
| install_date_utc |
+---------------------+
| 2020-01-15 00:00:01 |
+---------------------+
Phew! We are almost there! Now we just need to pull the installed_on
values dynamically and compare against our last_rebooted
query.
Let’s start with pulling in our values dynamically. We will remove our static date_value
and query directly from the patches
table:
WITHsplit_dateAS(SELECT*,SPLIT(installed_on,'/',2)ASyear,SPLIT(installed_on,'/',0)ASmonth,SPLIT(installed_on,'/',1)ASdayFROMpatches),date_reconstructedAS(SELECT*,year||'-'||SUBSTR(('0'||month),-2)||'-'||SUBSTR(('0'||day),-2)||' '||'00:00:01'ASinstall_date_utcFROMsplit_date)SELECT*FROMdate_reconstructedLIMIT1;
csname = LENOVO-THINKPAD
hotfix_id = KB4534273
caption = http://support.microsoft.com/?kbid=4534273
description = Security Update
fix_comments =
installed_by = NT AUTHORITY\SYSTEM
install_date =
installed_on = 1/15/2020
year = 2020
month = 1
day = 15
install_date_utc = 2020-01-15 00:00:01
To accommodate our two possible vulnerable conditions we will utilize CASE
logic to create two boolean conditions:
- Hotfix not installed
- Computer not rebooted since Hotfix installation
Let’s begin by adding our reboot query as a CASE
boolean column:
WITHsplit_dateAS(SELECT*,SPLIT(installed_on,'/',2)ASyear,SPLIT(installed_on,'/',0)ASmonth,SPLIT(installed_on,'/',1)ASdayFROMpatches),date_reconstructedAS(SELECT*,year||'-'||SUBSTR(('0'||month),-2)||'-'||SUBSTR(('0'||day),-2)||' '||'00:00:01'ASinstall_date_utcFROMsplit_date),restart_checkAS(SELECT*,CASEWHEN(SELECTdatetime(time.unix_time-uptime.total_seconds,'unixepoch')FROMtime,uptime)>install_date_utcTHEN'true'ELSE'false'ENDASrestart_since_installFROMdate_reconstructed)SELECT*FROMrestart_checkLIMIT1;
csname = LENOVO-THINKPAD
hotfix_id = KB4534273
caption = http://support.microsoft.com/?kbid=4534273
description = Security Update
fix_comments =
installed_by = NT AUTHORITY\SYSTEM
install_date =
installed_on = 1/15/2020
year = 2020
month = 1
day = 15
install_date_utc = 2020-01-15 00:00:01
restart_since_install = true
We’re so close! All that is left is adding our very first query, looking for the specific hotfixes. Let’s start by writing the CASE
query.
SELECTCASEWHENNOTEXISTS(SELECT1FROMpatchesWHEREhotfix_idIN('KB4534306','KB4534271','KB4534276','KB4534293','KB4534273','KB4535550','KB4528760'))THEN'true'ELSE'false'ENDASCVE_2020_0601_vulnerable;
+--------------------------+
| CVE_2020_0601_vulnerable |
+--------------------------+
| false |
+--------------------------+
And now we combine them for the final query:
WITHsplit_dateAS(SELECT*,SPLIT(installed_on,'/',2)ASyear,SPLIT(installed_on,'/',0)ASmonth,SPLIT(installed_on,'/',1)ASdayFROMpatches),date_reconstructedAS(SELECT*,year||'-'||SUBSTR(('0'||month),-2)||'-'||SUBSTR(('0'||day),-2)||' '||'00:00:01'ASinstall_date_utcFROMsplit_date),restart_checkAS(SELECT*,CASEWHEN(SELECTdatetime(time.unix_time-uptime.total_seconds,'unixepoch')FROMtime,uptime)>install_date_utcTHEN'true'ELSE'false'ENDASrestart_since_installFROMdate_reconstructed),operating_systemAS(SELECTCAST(SPLIT(version,'.',0)ASinteger)ASmajor,CAST(SPLIT(version,'.',2)ASinteger)ASbuild,CAST(SPLIT(version,'.',3)ASinteger)ASpatchFROMkernel_info),vulnerable_buildAS(SELECT*,CASEWHENmajor=10ANDbuild=10240ANDpatch<18453THEN'true'WHENmajor=10ANDbuild=14393ANDpatch<3443THEN'true'WHENmajor=10ANDbuild=16299ANDpatch<1625THEN'true'WHENmajor=10ANDbuild=17134ANDpatch<1246THEN'true'WHENmajor=10ANDbuild=17763ANDpatch<973THEN'true'WHENmajor=10ANDbuild=18362ANDpatch<592THEN'true'WHENmajor=10ANDbuild=18363ANDpatch<592THEN'true'ELSE'false'ENDasaffected_buildFROMoperating_system),failing_stateAS(SELECTCASEWHEN(SELECT1FROMrestart_checkWHEREhotfix_idIN('KB4534306','KB4534271','KB4534276','KB4534293','KB4534273','KB4535550','KB4528760'))THEN'true'ELSE'false'ENDASCVE_2020_0601_patch_installed,CASEWHEN(SELECT1FROMrestart_checkWHEREhotfix_idIN('KB4534306','KB4534271','KB4534276','KB4534293','KB4534273','KB4535550','KB4528760')ANDrestart_since_install='false')THEN'false'ENDASrestart_since_install,CASEWHEN(SELECT1FROMvulnerable_buildWHEREaffected_build='true')THEN'true'ENDASaffected_build)SELECT*,CASEWHEN(restart_since_install='false'ORcve_2020_0601_patch_installed='false')ANDaffected_build='true'THEN'true'ENDASvulnerableFROMfailing_stateWHEREvulnerable='true'
All that fun with Osquery SQL!
Or you could just use [Kolide])(https://k2.kolide.com/trial/new) and have it written and automatically notifying for you without so much as opening your terminal. ;)
Get started today, with a free trial of Kolide(no payment information or credit card required).