edquota(8) feature
X-RDate: Tue, 24 Mar 1998 13:04:02 +0500 (ESK)
Date: Sat, 21 Mar 1998 09:37:47 -0300
From: Solar Designer <solar@FALSE.COM>
To: BUGTRAQ@NETSPACE.ORG
Subject: edquota(8) feature
Hello,
Okay, at least two different bugs today, but let me start with a tiny FAQ:
Q: How do I crash a Linux-based shell provider?
A: Register with username "67108864".
Q: How do I just bypass the quota? My admin uses BSD-derived edquota(8).
A: Register as "65535".
Q: How do I consume some hours of their CPU time, as root?
A: Register as "12345678". The next quotacheck run (usually at reboot)
will take hours to complete.
Q: How do I reduce someone else's increased quota to the default?
A: Register with their UID as your username.
Q: How do I corrupt their quota.user file?
A: They have to allow 9 character long usernames for that. Read below.
Of these, only the first scenario is Linux-specific. Others apply to many
systems: BSD 4.3, BSDI 2.0, FreeBSD 2.2.5, SunOS 4.1.4, Solaris 2.5 seem to
be affected -- at least their edquota(8) got the "feature", too. I didn't
actually find that many victims yet, so feedback is welcome. ;-)
In general, only some setups are affected: [free] shell providers mostly.
Users should be allowed to pick all-digit usernames for these exploits to
work. However, the reason I was investigating this is that our quota.user
file grew 449 Megs large one day, so this _can_ happen.
Now, to the edquota feature (yes, this was meant to be a feature): it has
"special support" for all-digit usernames. Simply, it treats them as UIDs,
and I was unable to find a mention of this in the manpages I have. Other
user-level quota utilities have the same feature, but that doesn't seem to
be a security problem there. However, a typical (I think) ISP setup would
use edquota in a script, running as root, to set the quota for every new
user created.
While this feature by itself is a security problem (see the 2nd and 4th
questions above), things are even worse in reality. Only some versions of
edquota check and disallow negative UIDs, and none of those I've seen do
any check for UIDs past 65535.
Now, everything depends on the way quota file is updated. There're several
approaches here. Some versions of edquota will only work when the quota is
on at the moment, and use quotactl(2). Others first try to use quotactl(),
and, if that fails, assume the quota is off (some are wise enough to check
errno though), and write to the file directly. (Of those, many don't care
to check return value from lseek(), which brings a reliability problem, but
I won't go into that now.)
If our version of edquota supports direct quota file access, _and_ it is
run while the quota is off, then the attacker is probably lucky, since it
will happily lseek() to whatever UID it got from the username.
Otherwise, everything depends on how well the kernel checks if the values
passed to quotactl() are valid. Again, many systems seem to let the attacker
succeed, perhaps thinking that they did the super-user check already. Some
check for negative UIDs only, which is definitely not enough.
Let's assume the attacker succeeded in making the quota file really huge.
What's the problem with this, it's just the filesize, and doesn't take that
much of physical storage anyway? Still, there're several problems. First,
some versions of quotacheck(8), which typically runs at reboot, got the
following code:
if (fstat(fd, &st) == 0) {
max_id = st.st_size / sizeof(struct dqblk);
[...]
for (id = 1; id <= max_id; id++) {
That is, its execution time will increase with the file size. For 449 Megs,
this was over 8 hours of CPU time.
Then, there's a problem when 9 character long usernames are allowed, _and_
sizeof(struct dqblk) is not a power of 2. Nine decimal digits are enough
to cause an integer overflow when edquota (or the kernel) multiplies UID
by sizeof(struct dqblk). This can be used to write a block not at a block
boundary, corrupting the quota file.
Finally, there's a Linux kernel bug (might be present on some other systems
also, I just didn't have a chance to check; the impact will likely differ
though). There's no check whether the UID supplied via quotactl() is valid,
so that it is possible to get negative file offsets. Now, if it used lseek()
the way it is accessible via the syscall, everything would be fine. However,
the kernel simply does:
filp->f_pos = dqoff(dquot->dq_id);
The system stops responding, and the console gets flooded with ext2 warning
messages. Hopefully there's someone around to hit that reset button. The
username from first FAQ question exploits exactly this bug (combined with
the edquota feature, of course). Here's another exploit, just to show this
specific problem:
#include <stdio.h>
#include <unistd.h>
#include <linux/quota.h>
#define DEVICE "/dev/hda3"
int main()
{
struct dqblk block;
if (quotactl(QCMD(Q_SETQUOTA, USRQUOTA), DEVICE,
(unsigned int)~0 / sizeof(block), (caddr_t)&block))
perror("quotactl");
return 0;
}
It should be run as root, and is mainly for checking whether the bug got
fixed -- it's not a real exploit. Be sure to run it with quota enabled,
and don't forget to set DEVICE correctly. This crashes my 2.0.33 just fine.
Well, probably it's the time for fixes. If you don't need the edquota
feature, you can just disable it (patch for Linux quota utils, v1.51):
--- edquota.c.orig Fri Mar 20 18:20:54 1998
+++ edquota.c Fri Mar 20 18:23:30 1998
@@ -173,8 +173,6 @@
struct passwd *pw;
struct group *gr;
- if (alldigits(name))
- return (atoi(name));
switch (quotatype) {
case USRQUOTA:
if (pw = getpwnam(name))
A real fix should probably either add an extra option (like '-n') for
numeric UIDs, or at least check getpwnam() _before_ alldigits(). (The
latter is still a bit dangerous though.)
Another obvious workaround for a particular site would be to disallow
all-digit usernames.
And finally, here's the Linux kernel patch, for 2.0.33:
--- linux/fs/dquot.c.orig Sat Mar 21 06:37:47 1998
+++ linux/fs/dquot.c Sat Mar 21 06:40:02 1998
@@ -1075,6 +1075,9 @@
return(-EINVAL);
}
+ if (id & ~0xFFFF)
+ return(-EINVAL);
+
flags |= QUOTA_SYSCALL;
if (has_quota_enabled(dev, type))
return(set_dqblk(dev, id, type, flags, (struct dqblk *) addr));
Signed,
Solar Designer