-
Website
http://www.matasano.com/log -
Original page
http://www.matasano.com/log/592/finger-79tcp-mcdonald-dowd-and-schuh-challenge-part-2/ -
Subscribe
All Comments -
Community
-
Top Commenters
-
Press Controls
3 comments · 2 points
-
ChrisMtso
12 comments · 1 points
-
Eric Monti
11 comments · 1 points
-
StatlerAndWaldorf
12 comments · 3 points
-
Dave G.
7 comments · 1 points
-
-
Popular Threads
The first sequence {decrement maxsize; increment ptr;} adjusts things correctly. The second sequence {increment ptr; decrement maxsize;} is using a wrong (always zero) strlen(ptr) to bump maxsize, so vsnprintf() is passed a too-long maxsize and the buffer can be overflowed.
(b)
The env pointer is always incremented past the one I just copied into. So doubling up an environment variable, e.g.:
LD_PRELOAD=xxx
LD_PRELOAD=yyy
PATH=...
will only check and wipe the first occurence but not the second -- leaving an oppprtunity to pass through a dangerous environment.
(c)
With no content constraints on the password argument, it may contain embedded newlines allowing it to stuff in extra newline-delimited lines of its choosing.
Real-world example was IDENT protocol processing in sendmail 8.6.9 and earlier -- IDENT servers were able to inject arbitrary information into sendmail queue files (allowing remote command execution). I and another person independently found that one back in Feburary 1995.
There are potential security vulnerabilities in each sample. Find them, and explain them...
Good call on both... sorry about that.
Challenge 1:
After the output of get_origin_ip() is formatted and appended to the buffer, ptr is first increased by the number of bytes written (i.e. ptr now points to a null byte), and maxsize is decreased by 0. That means it is possible to write up to the maximum 16 bytes returned by getoriginip beyond the end of the buffer.
Challenge 2:
If one has an environment where the last two environment variables are for example "LD_PRELOAD==something" and "LD_PRELOAD=nastylib", and LD_PRELOAD is to be cleared, then the innermost loop will move the last entry "down" one environment variable, overwriting the first of the two. Then env (pointing to that entry) will be increased to fetch the next variable (which doesn't exist), so env is 0 and the while loop terminates. On second though, this should work everywhere with two consecutive variables names like above, as the loop will just skip the one it copied down.
Now that I've heard "locale" mentioned in conjunction with "isalnum" here and there, I found out that isalnum() is locale dependent. Thus, if an attacker could use a locale where the newline and colon characters are considered alphanumeric characters, it would be possible for him or her to inject strings like "\nuser:password" into the password variable, to either create new user accounts, or add another entry for an existing user, using a different password.
Unfortunately, I couldn't find a way to make the libc accept my user-generated LC_CTYPE files, and I couldn't find one in the system locale path that fulfills the mentioned requirements.
Plus, I'm out of ideas on this one. I give up. :(
I expanded #2 to an openbsd0day.
If you've got a suid that sets real uid/gid = 0 then there you go :)
sprintf(ptr, "%s: ", get_origin_ip());
ptr += strlen(ptr);
maxsize -= strlen(ptr);
The problem is that ptr is being incremented AND THEN maxsize is being decremented with the new length of ptr. The problem is at that point strlen(ptr) will return 0, as it's already been jumped to point to the end of the current string. This leads to more data being placed in buf than it can hold.
The line:
vsnprintf(ptr, maxsize, fmt, ap);
Tries to copy all the variable length data supplied in ap into ptr. Even assuming the format string is ok, that still leaves a small amount of extra data overflowing the buf variable.
From what I can tell this extra data is of the size of strlen(origin_string) - which is not a huge amount but enough to craft an overflow.
There may also be something funky in relation to the varargs and the ap variable popping things off the call stack, but I haven't thought that part through yet.
Regarding my solution for Challenge 2: the additional "=" character is a result of me playing with environment variables in the shell, and should not really be needed when passing the environment as an argument to a call to execve running the vulnerable program.
With the confusion about isalnum and locales gone, I was able to focus on my original idea of race conditions and possible file truncation.
- It might be possiple for a user to start the suid program, have it populate the file_entries list and authenticate the user. Then the user could send it a SIGSTOP, suspending its operation before the program blocks signals. The user could later continue to run the program by sending it a SIGCONT to reset the password file to its original state, effectively overwriting any chances made by other users between SIGSTOP and SIGCONT.
- The glibc implementation of rename(old, new) even documents a possible race. Should the file indicated by "new" already exist, rename() will first try to unlink(new) and then link(old, new), leaving a window of vulnerability where "new" does not exist. Depending on how the suid program populates the file_entries list, this might lead to truncation of the password file, if another instance of the suid program were running at the same time. I'm a little unsure about this one.
- Still on the subject of rename(), after successfully executing link(old, new), glibc's rename() will call unlink(old). This opens another window of vulnerability within the suid program where it is possible for another instance to create the TEMP_PASSWORD_FILE and populate it, with the first instance yet to call unlink(TEMP_PASSWORD_FILE).
- As I noticed just now, the last two bugs together could lead to removal of the password file as follows:
Instance 1: creates TEMP_PASSWORD_FILE
Instance 1: populates TEMP_PASSWORD_FILE
Instance 1: calls rename()
Instance 1: rename() calls unlink(PASSWORD_FILE) and link(TEMP_PASSWORD_FILE, PASSWORD_FILE)
Instance 1: rename() calls unlink(TEMP_PASSWORD_FILE)
---> TEMP_PASSWORD_FILE is gone, Instance 2 can start
Instance 2: creates TEMP_PASSWORD_FILE
Instance 2: populates TEMP_PASSWORD_FILE
Instance 2: calls rename()
Instance 2: rename() calls unlink(PASSWORD_FILE)
---> PASSWORD_FILE is gone, now Instance 1 gets scheduled again
Instance 1: calls unlink(TEMP_PASSWORD_FILE)
---> Instance 1 removes the TEMP_PASSWORD_FILE
Instance 2: rename() tries to link(TEMP_PASSWORD_FILE, PASSWORD_FILE)
---> Instance 2 fails, both TEMP_PASSWORD_FILE and PASSWORD_FILE are gone.
1. After writing the origin IP, maxsize is decremented after pointer is incremented. This is just wrong, because it results in maxsize not getting decremented. This can result in an overflow, because maxsize now exceeds the amount of space left in the buffer.
2. If you put two of the same environment variable in a row, the algorith will result in one still being present. This is because it copies the second instance into the slot formerly containing the first one, and then advances env to point to the next slot.
3. I was working on filling up the disk to cause the write to fail when the file is part way written, and go to epilog. It would be good if I could open the tempfile after it is closed but before it is unlinked, but permissions appear to prevent that.
I think Kasperle's answer is a lot more plausible.
One other loose end is that the strcmp() should be strncmp().
The code as written cannot change passwords for 32-byte usernames with non-empty passwords. It's a bug and a red flag at least, but I don't see an exploit there yet.
Regarding races, the Linux man page I'm looking at guarantees that at no time does the destination name disappear.
(3) I think the trick here is to cause the file to truncate by setting RLIMIT_FSIZE prior to exec()ing this program.
If you set the limit just so that the last write() trips partially over the limit, on Linux it will return the amount of data written, so the function will return successfully.
So instead of dying because of a signal when the resource limit is exceeded, it will happily chug along.
(Though if another write() is performed, it will return an error.)
See also http://www.gnu.org/software/libc/manual/html_no...
I started looking at the challenges earlier today, the
first ones took a few minutes, the third one was a bit
trickier but I think I got it right now. ;>
I had this page loaded in my browser since a week
back, when I saw someone mentioning the page on IRC,
but hadn't had time to look at it until now. At the
time it only said:
UPDATED (11/13/06 7:12PM/Eastern): The prize remains unclaimed. Challenge #3 may be OS specific, but we know it works on at least one OS (Linux).
:)
My solutions were:
1.
ptr += strlen(ptr);
maxsize -= strlen(ptr);
Uh-oh, maxsize will be decreased by 0 here
-> potential 16 bytes stackbof.
2.
for (P = env;; ++P)
if (!(*P = *(P + 1)))
break;
}
env++;
Uh-oh, what if we use two adjacent LD_PRELOAD:s? :>
The second one will not be cleansed.
3.
At first I was thinking it had to do with signals,
since SIGSTOP can't be blocked for instance. Then I
noticed we probably couldn't do anything fun/useful
with that & thought about whether there is a way to
cut write() short and get an empty password for some
user. ulimit -f / setrlimit(RLIMIT_FSIZE, ...) turned
out to be exactly what I needed. ;>
While the first two challenges took me perhaps five
minutes each, this one took me probably 30-45 minutes.
Nice ones! Interesting challenges are always
appreciated. :)
--
Best Regards,
Joel Eriksson
CTO Bitsec AB