172 lines
6.2 KiB
Diff
172 lines
6.2 KiB
Diff
|
From 7757d1b046ecb67b830151d20715c658867df1ec Mon Sep 17 00:00:00 2001
|
||
|
From: Jim Meyering <meyering@redhat.com>
|
||
|
Date: Mon, 23 Apr 2012 20:06:35 +0000
|
||
|
Subject: do not write outside heap buffer
|
||
|
|
||
|
* parsing.c (substr): Handle tail < head.
|
||
|
|
||
|
This started when I noticed some cgit segfaults on savannah.gnu.org.
|
||
|
Finding the offending URL/commit and then constructing a stand-alone
|
||
|
reproducer were far more time-consuming than writing the actual patch.
|
||
|
|
||
|
The problem arises with a commit like this, in which the user name
|
||
|
part of the "Author" field is empty:
|
||
|
|
||
|
$ git log -1
|
||
|
commit 6f3f41d73393278f3ede68a2cb1e7a2a23fa3421
|
||
|
Author: <T at h.or>
|
||
|
Date: Mon Apr 23 22:29:16 2012 +0200
|
||
|
|
||
|
Here's what happens:
|
||
|
|
||
|
(this is due to buf=malloc(0); strncpy (buf, head, -1);
|
||
|
where "head" may point to plenty of attacker-specified non-NUL bytes,
|
||
|
so we can overwrite a zero-length heap buffer with arbitrary data)
|
||
|
|
||
|
Invalid write of size 1
|
||
|
at 0x4A09361: strncpy (mc_replace_strmem.c:463)
|
||
|
by 0x408977: substr (parsing.c:61)
|
||
|
by 0x4089EF: parse_user (parsing.c:73)
|
||
|
by 0x408D10: cgit_parse_commit (parsing.c:153)
|
||
|
by 0x40A540: cgit_mk_refinfo (shared.c:171)
|
||
|
by 0x40A581: cgit_refs_cb (shared.c:181)
|
||
|
by 0x43DEB3: do_for_each_ref (refs.c:690)
|
||
|
by 0x41075E: cgit_print_branches (ui-refs.c:191)
|
||
|
by 0x416EF2: cgit_print_summary (ui-summary.c:56)
|
||
|
by 0x40780A: summary_fn (cmd.c:120)
|
||
|
by 0x40667A: process_request (cgit.c:544)
|
||
|
by 0x404078: cache_process (cache.c:322)
|
||
|
Address 0x4c718d0 is 0 bytes after a block of size 0 alloc'd
|
||
|
at 0x4A0884D: malloc (vg_replace_malloc.c:263)
|
||
|
by 0x455C85: xmalloc (wrapper.c:35)
|
||
|
by 0x40894C: substr (parsing.c:60)
|
||
|
by 0x4089EF: parse_user (parsing.c:73)
|
||
|
by 0x408D10: cgit_parse_commit (parsing.c:153)
|
||
|
by 0x40A540: cgit_mk_refinfo (shared.c:171)
|
||
|
by 0x40A581: cgit_refs_cb (shared.c:181)
|
||
|
by 0x43DEB3: do_for_each_ref (refs.c:690)
|
||
|
by 0x41075E: cgit_print_branches (ui-refs.c:191)
|
||
|
by 0x416EF2: cgit_print_summary (ui-summary.c:56)
|
||
|
by 0x40780A: summary_fn (cmd.c:120)
|
||
|
by 0x40667A: process_request (cgit.c:544)
|
||
|
|
||
|
Invalid write of size 1
|
||
|
at 0x4A09400: strncpy (mc_replace_strmem.c:463)
|
||
|
by 0x408977: substr (parsing.c:61)
|
||
|
by 0x4089EF: parse_user (parsing.c:73)
|
||
|
by 0x408D10: cgit_parse_commit (parsing.c:153)
|
||
|
by 0x40A540: cgit_mk_refinfo (shared.c:171)
|
||
|
by 0x40A581: cgit_refs_cb (shared.c:181)
|
||
|
by 0x43DEB3: do_for_each_ref (refs.c:690)
|
||
|
by 0x41075E: cgit_print_branches (ui-refs.c:191)
|
||
|
by 0x416EF2: cgit_print_summary (ui-summary.c:56)
|
||
|
by 0x40780A: summary_fn (cmd.c:120)
|
||
|
by 0x40667A: process_request (cgit.c:544)
|
||
|
by 0x404078: cache_process (cache.c:322)
|
||
|
Address 0x4c7192b is not stack'd, malloc'd or (recently) free'd
|
||
|
|
||
|
Invalid write of size 1
|
||
|
at 0x4A0940E: strncpy (mc_replace_strmem.c:463)
|
||
|
by 0x408977: substr (parsing.c:61)
|
||
|
by 0x4089EF: parse_user (parsing.c:73)
|
||
|
by 0x408D10: cgit_parse_commit (parsing.c:153)
|
||
|
by 0x40A540: cgit_mk_refinfo (shared.c:171)
|
||
|
by 0x40A581: cgit_refs_cb (shared.c:181)
|
||
|
by 0x43DEB3: do_for_each_ref (refs.c:690)
|
||
|
by 0x41075E: cgit_print_branches (ui-refs.c:191)
|
||
|
by 0x416EF2: cgit_print_summary (ui-summary.c:56)
|
||
|
by 0x40780A: summary_fn (cmd.c:120)
|
||
|
by 0x40667A: process_request (cgit.c:544)
|
||
|
by 0x404078: cache_process (cache.c:322)
|
||
|
Address 0x4c7192d is not stack'd, malloc'd or (recently) free'd
|
||
|
|
||
|
Process terminating with default action of signal 11 (SIGSEGV)
|
||
|
Access not within mapped region at address 0x502F000
|
||
|
at 0x4A09400: strncpy (mc_replace_strmem.c:463)
|
||
|
by 0x408977: substr (parsing.c:61)
|
||
|
by 0x4089EF: parse_user (parsing.c:73)
|
||
|
by 0x408D10: cgit_parse_commit (parsing.c:153)
|
||
|
by 0x40A540: cgit_mk_refinfo (shared.c:171)
|
||
|
by 0x40A581: cgit_refs_cb (shared.c:181)
|
||
|
by 0x43DEB3: do_for_each_ref (refs.c:690)
|
||
|
by 0x41075E: cgit_print_branches (ui-refs.c:191)
|
||
|
by 0x416EF2: cgit_print_summary (ui-summary.c:56)
|
||
|
by 0x40780A: summary_fn (cmd.c:120)
|
||
|
by 0x40667A: process_request (cgit.c:544)
|
||
|
by 0x404078: cache_process (cache.c:322)
|
||
|
|
||
|
This happens when tail - head == -1 here:
|
||
|
(parsing.c)
|
||
|
|
||
|
char *substr(const char *head, const char *tail)
|
||
|
{
|
||
|
char *buf;
|
||
|
|
||
|
buf = xmalloc(tail - head + 1);
|
||
|
strncpy(buf, head, tail - head);
|
||
|
buf[tail - head] = '\0';
|
||
|
return buf;
|
||
|
}
|
||
|
|
||
|
char *parse_user(char *t, char **name, char **email, unsigned long *date)
|
||
|
{
|
||
|
char *p = t;
|
||
|
int mode = 1;
|
||
|
|
||
|
while (p && *p) {
|
||
|
if (mode == 1 && *p == '<') {
|
||
|
*name = substr(t, p - 1);
|
||
|
t = p;
|
||
|
mode++;
|
||
|
} else if (mode == 1 && *p == '\n') {
|
||
|
|
||
|
The fix is to handle the case of (tail < head) before calling xmalloc,
|
||
|
thus avoiding passing an invalid value to xmalloc.
|
||
|
|
||
|
And here's the reproducer:
|
||
|
It was tricky to reproduce, because git prohibits use of an empty "name"
|
||
|
in a commit ID. To construct the offending commit, I had to resort to
|
||
|
using "git hash-object".
|
||
|
|
||
|
git init -q foo &&
|
||
|
( cd foo &&
|
||
|
echo a > j && git add . && git ci -q --author='au <T at h.or>' -m. . &&
|
||
|
h=$(git cat-file commit HEAD|sed 's/au //' \
|
||
|
|git hash-object -t commit -w --stdin) &&
|
||
|
git co -q -b test $h &&
|
||
|
git br -q -D master &&
|
||
|
git br -q -m test master)
|
||
|
git clone -q --bare foo foo.git
|
||
|
|
||
|
cat <<EOF > in
|
||
|
repo.url=foo.git
|
||
|
repo.path=foo.git
|
||
|
EOF
|
||
|
CGIT_CONFIG=in QUERY_STRING=url=foo.git valgrind ./cgit
|
||
|
|
||
|
The valgrind output is what you see above.
|
||
|
|
||
|
AFAICS, this is not exploitable thanks (ironically) to the use of strncpy.
|
||
|
Since that -1 translates to SIZE_MAX and this is strncpy, not only does it
|
||
|
copy whatever is in "head" (up to first NUL), but it also writes
|
||
|
SIZE_MAX - strlen(head) NUL bytes into the destination buffer, and that
|
||
|
latter is guaranteed to evoke a segfault. Since cgit is single-threaded,
|
||
|
AFAICS, there is no way that the buffer clobbering can be turned into
|
||
|
an exploit.
|
||
|
---
|
||
|
diff --git a/parsing.c b/parsing.c
|
||
|
index 602e3de..1b2a551 100644
|
||
|
--- a/parsing.c
|
||
|
+++ b/parsing.c
|
||
|
@@ -56,6 +56,8 @@ char *substr(const char *head, const char *tail)
|
||
|
{
|
||
|
char *buf;
|
||
|
|
||
|
+ if (tail < head)
|
||
|
+ return xstrdup("");
|
||
|
buf = xmalloc(tail - head + 1);
|
||
|
strncpy(buf, head, tail - head);
|
||
|
buf[tail - head] = '\0';
|
||
|
--
|
||
|
cgit v0.9.0.3-79-g88f8
|