Talos Vulnerability Report

TALOS-2017-0304

Ledger CLI Account Directive Use-After-Free Vulnerability

August 30, 2017
CVE Number

CVE-2017-2808

Summary

An exploitable use-after-free vulnerability exists in the account parsing component of the Ledger-CLI 3.1.1. A specially crafted ledger file can cause a use-after-free vulnerability resulting in arbitrary code execution. An attacker can convince a user to load a journal file to trigger this vulnerability.

Tested Versions

Ledger HEAD Ledger 3.1.1

Product URLs

http://ledger-cli.org
https://github.com/ledger/ledger.git
https://github.com/ledger/ledger/tree/v3.1.1

CVSSv3 Score

7.5 - CVSS:3.0/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H

CWE

CWE-416: Use After Free

Details

Ledger-cli is a plain-text, double-entry accounting system that is useable from the command line. It is based around a plain-text format known as a journal file that contains each transaction for each account.

When parsing a malformed journal file, the application will misuse the std::unique_ptr type when exchanging objects between different parts of the parser. Due to this, when these pointers go out of scope they will be released and destroyed. However, due to the pointers still being retained by the application a use-after-free vulnerability will occur when the application attempts to process a user's commands.

When first parsing a journal file, the application will execute the following code. This code will instantiate an instance_t, and then call its .parse() method [1]. The .parse() method will simply enter a loop that processes each line of the journal file using the read_next_directive method [2].

src/textual.cc:1987
std::size_t journal_t::read_textual(parse_context_stack_t& context_stack)
{
  TRACE_START(parsing_total, 1, "Total time spent parsing text:");
  {
    instance_t instance(context_stack, context_stack.get_current(), NULL,
                        checking_style == journal_t::CHECK_PERMISSIVE);
    instance.apply_stack.push_front
      (application_t("account", context_stack.get_current().master));
    instance.parse();                                                   // \ [1]
  }
  TRACE_STOP(parsing_total, 1);

  // Apply any deferred postings at this time
  master->apply_deferred_posts();
...
}
\
src/textual.cc:236
void instance_t::parse(()
{
...
  while (in.good() && ! in.eof()) {
    try {
      read_next_directive(error_flag);                                  // [2]
    }
    catch (const std::exception& err) {

The read_next_directive method will iterate through each line of the journal file whilst looking at the first character to determine which command to process. If the first character is numeric, then the application will assume that it's an account directive and will call the xact_directive method to handle it [1].

src/textual.cc:334
void instance_t::read_next_directive(bool& error_flag)

  char * line;
  std::streamsize len = read_line(line);
  if (len == 0 || line == NULL)
    return;

  if (! std::isspace(line[0]))
    error_flag = false;

  switch (line[0]) {
...
  case '0':
  case '1':
  case '2':
  case '3':
  case '4':
  case '5':
  case '6':
  case '7':
  case '8':
  case '9':
    xact_directive(line, len);          // [1]
    break;
...

Inside the xact_directive method, the application will call the parse_xact [1] method which will return a pointer. This pointer will then be assigned to a unique_ptr<xact_t> [2]. Due to it being incorrectly initialized from a pointer, there are no references to it. When the variable goes out of scope at [3], it will then be freed.

src/textual.cc:694
void instance_t::xact_directive(char * line, std::streamsize len)

  TRACE_START(xacts, 1, "Time spent handling transactions:");

  if (xact_t * xact = parse_xact(line, len, top_account())) {               // [1]
    unique_ptr<xact_t> manager(xact);                                       // [2]

    if (context.journal->add_xact(xact)) {
      manager.release();        // it's owned by the journal now
      context.count++;
    }
    // It's perfectly valid for the journal to reject the xact, which it
    // will do if the xact has no substantive effect (for example, a
    // checking xact, all of whose postings have null amounts).
  } else {                                                                  // [3]
    throw parse_error(_("Failed to parse transaction"));


  TRACE_STOP(xacts, 1);

The parse_xact method, is responsible for allocating the xact_t object [1]. Near the end of this function, the application will call the parse_post method [2]. This method will allocate a post_t object, and then link the xact_t object to the post_t. Afterwards, the returned post_t will then be linked back to the xact_t object [3].

src/textual.cc:1776
xact_t * instance_t::parse_xact(char *          line,
                                std::streamsize len,
                                account_t *     account)

  TRACE_START(xact_text, 1, "Time spent parsing transaction text:");

  unique_ptr<xact_t> xact(new xact_t);                              // [1]

  xact->pos           = position_t();
  xact->pos->pathname = context.pathname;
  xact->pos->beg_pos  = context.line_beg_pos;
  xact->pos->beg_line = context.linenum;
  xact->pos->sequence = context.sequence++;

  bool reveal_context = true;
...
      if (post_t * post =
          parse_post(p, len - (p - line), account, xact.get())) {   // [2]
        reveal_context = true;
        xact->add_post(post);                                       // [3]
        last_post = post;
      }
      reveal_context = true;
    }
  }
...
  return xact.release();

Inside the parse_post method, the application will allocate a post_t via it's constructor [1], and then begin to parse any extra flags that were specified in the post. In order to reach the most vulnerable path which leads directly to code eecution, the POST_DEFERRED flag must be specified. This requires that there be an entry that begins and ends with the < and > characters [2].

src/textual.cc:1404
post_t * instance_t::parse_post(char *          line,
                                std::streamsize len,
                                account_t *     account,
                                xact_t *        xact,
                                bool            defer_expr)

  TRACE_START(post_details, 1, "Time spent parsing postings:");

  unique_ptr<post_t> post(new post_t);                          // [1]

  post->xact          = xact;   // this could be NULL
  post->pos           = position_t();
  post->pos->pathname = context.pathname;
  post->pos->beg_pos  = context.line_beg_pos;
  post->pos->beg_line = context.linenum;
  post->pos->sequence = context.sequence++;
...
  else if (*p == '<' && *(e - 1) == '>') {
    post->add_flags(POST_DEFERRED);                             // [2]
    DEBUG("textual.parse", "line " << context.linenum << ": "
          << "Parsed a deferred account name");
    p++; e--;
  }
...
  return post.release();
...

Once the file is done parsing, the application will return back to the journal_t::read_textual method. At [1], the application will then proceed to apply any transactional information for the deferred posts in the master account that have been parsed by the instance_t parser.

src/textual.cc:1987
std::size_t journal_t::read_textual(parse_context_stack_t& context_stack)

  TRACE_START(parsing_total, 1, "Total time spent parsing text:");
  {
    instance_t instance(context_stack, context_stack.get_current(), NULL,
                        checking_style == journal_t::CHECK_PERMISSIVE);
    instance.apply_stack.push_front
      (application_t("account", context_stack.get_current().master));
    instance.parse();
  }
  TRACE_STOP(parsing_total, 1);

  // Apply any deferred postings at this time
  master->apply_deferred_posts();                               // [1]

This is done by the following code. At this point, the post object has already been freed due to it being out of scope due to lack of references. At [1], since the pointer was still assigned to the object, the application will iterate through all the deferred posts within the account. However, due to the object being released, the virtual method dereference at [2] will dereference memory that has gone out of scope.

src/account.cc:157
void account_t::apply_deferred_posts()

  if (deferred_posts) {
    foreach (deferred_posts_map_t::value_type& pair, *deferred_posts) {     // [1]
      foreach (post_t * post, pair.second)
        post->account->add_post(post);                                      // [2]
    }
    deferred_posts = none;
  }

  // Also apply in child accounts
  foreach (const accounts_map::value_type& pair, accounts)
    pair.second->apply_deferred_posts();

Crash Information

$ ledger -f poc.journal register

=================================================================
==20621==ERROR: AddressSanitizer: heap-use-after-free on address 0x6150000012d0 at pc 0x7f50d6b0767b bp      
0x7ffe5b6bca70 sp 0x7ffe5b6bca68
READ of size 8 at 0x6150000012d0 thread T0
    #0 0x7f50d6b0767a in ledger::account_t::apply_deferred_posts() /root/ledger/src/account.cc:162:15
    #1 0x7f50d6b07638 in ledger::account_t::apply_deferred_posts() /root/ledger/src/account.cc:169:18
    #2 0x7f50d6a776ce in ledger::journal_t::read_textual(ledger::parse_context_stack_t&) /root/ledger/src/textual.cc:2000:11
    #3 0x7f50d6af9d3f in ledger::journal_t::read(ledger::parse_context_stack_t&) /root/ledger/src/journal.cc:505:13
    #4 0x7f50d6a1056b in ledger::session_t::read_data(std::string const&) /root/ledger/src/session.cc:171:30
    #5 0x7f50d6a150bc in ledger::session_t::read_journal_files() /root/ledger/src/session.cc:203:5
    #6 0x567893 in ledger::global_scope_t::execute_command(std::list<std::string, std::allocator<std::string> >, bool) 
/root/ledger/src/global.cc:228:17
    #7 0x56c3e4 in ledger::global_scope_t::execute_command_wrapper(std::list<std::string, std::allocator<std::string> >, bool) 
/root/ledger/src/global.cc:273:5
    #8 0x53c996 in main /root/ledger/src/main.cc:121:30
    #9 0x7f50d3c297ec in __libc_start_main /build/eglibc-wIuxyX/eglibc-2.15/csu/libc-start.c:226
    #10 0x43fa20 in _start (/root/ledger/build/ledger+0x43fa20)

0x6150000012d0 is located 208 bytes inside of 472-byte region [0x615000001200,0x6150000013d8)
freed by thread T0 here:
    #0 0x538410 in operator delete(void*) (/root/ledger/build/ledger+0x538410)
    #1 0x7f50d6b2137d in void boost::checked_delete<ledger::post_t>(ledger::post_t*) 
/usr/include/boost/checked_delete.hpp:34:5
    #2 0x7f50d6b2137d in ledger::xact_base_t::~xact_base_t() /root/ledger/src/xact.cc:62
    #3 0x7f50d6b413fd in ledger::xact_t::~xact_t() /root/ledger/src/xact.h:113:3
    #4 0x7f50d6b413fd in ledger::xact_t::~xact_t() /root/ledger/src/xact.h:111
    #5 0x7f50d6a8447b in std::default_delete<ledger::xact_t>::operator()(ledger::xact_t*) const /usr/bin/../lib/gcc/x86_64-linux-
gnu/4.9/../../../../include/c++/4.9/bits/unique_ptr.h:76:2
    #6 0x7f50d6a8447b in std::unique_ptr<ledger::xact_t, std::default_delete<ledger::xact_t> >::~unique_ptr() 
/usr/bin/../lib/gcc/x86_64-linux-gnu/4.9/../../../../include/c++/4.9/bits/unique_ptr.h:236
    #7 0x7f50d6a8447b in ledger::(anonymous namespace)::instance_t::xact_directive(char*, long) 
/root/ledger/src/textual.cc:708
    #8 0x7f50d6a8447b in ledger::(anonymous namespace)::instance_t::read_next_directive(bool&) 
/root/ledger/src/textual.cc:375
    #9 0x7f50d6a79322 in ledger::(anonymous namespace)::instance_t::parse() /root/ledger/src/textual.cc:252:7
    #10 0x7f50d6a77594 in ledger::journal_t::read_textual(ledger::parse_context_stack_t&) /root/ledger/src/textual.cc:1995:14
    #11 0x7f50d6af9d3f in ledger::journal_t::read(ledger::parse_context_stack_t&) /root/ledger/src/journal.cc:505:13
    #12 0x7f50d6a1056b in ledger::session_t::read_data(std::string const&) /root/ledger/src/session.cc:171:30
    #13 0x7f50d6a150bc in ledger::session_t::read_journal_files() /root/ledger/src/session.cc:203:5
    #14 0x567893 in ledger::global_scope_t::execute_command(std::list<std::string, std::allocator<std::string> >, bool) 
/root/ledger/src/global.cc:228:17
    #15 0x56c3e4 in ledger::global_scope_t::execute_command_wrapper(std::list<std::string, std::allocator<std::string> >, bool) 
/root/ledger/src/global.cc:273:5
    #16 0x53c996 in main /root/ledger/src/main.cc:121:30
    #17 0x7f50d3c297ec in __libc_start_main /build/eglibc-wIuxyX/eglibc-2.15/csu/libc-start.c:226

previously allocated by thread T0 here:
    #0 0x5376d0 in operator new(unsigned long) (/root/ledger/build/ledger+0x5376d0)
    #1 0x7f50d6ab8e85 in ledger::(anonymous namespace)::instance_t::parse_post(char*, long, ledger::account_t*, 
ledger::xact_t*, bool) /root/ledger/src/textual.cc:1412:27
    #2 0x7f50d6a82791 in ledger::(anonymous namespace)::instance_t::parse_xact(char*, long, ledger::account_t*) 
/root/ledger/src/textual.cc:1932:11
    #3 0x7f50d6a82791 in ledger::(anonymous namespace)::instance_t::xact_directive(char*, long) 
/root/ledger/src/textual.cc:698
    #4 0x7f50d6a82791 in ledger::(anonymous namespace)::instance_t::read_next_directive(bool&) 
/root/ledger/src/textual.cc:375
    #5 0x7f50d6a79322 in ledger::(anonymous namespace)::instance_t::parse() /root/ledger/src/textual.cc:252:7
    #6 0x7f50d6a77594 in ledger::journal_t::read_textual(ledger::parse_context_stack_t&) /root/ledger/src/textual.cc:1995:14
    #7 0x7f50d6af9d3f in ledger::journal_t::read(ledger::parse_context_stack_t&) /root/ledger/src/journal.cc:505:13
    #8 0x7f50d6a1056b in ledger::session_t::read_data(std::string const&) /root/ledger/src/session.cc:171:30
    #9 0x7f50d6a150bc in ledger::session_t::read_journal_files() /root/ledger/src/session.cc:203:5
    #10 0x567893 in ledger::global_scope_t::execute_command(std::list<std::string, std::allocator<std::string> >, bool) 
/root/ledger/src/global.cc:228:17
    #11 0x56c3e4 in ledger::global_scope_t::execute_command_wrapper(std::list<std::string, std::allocator<std::string> >, bool) 
/root/ledger/src/global.cc:273:5
    #12 0x53c996 in main /root/ledger/src/main.cc:121:30
    #13 0x7f50d3c297ec in __libc_start_main /build/eglibc-wIuxyX/eglibc-2.15/csu/libc-start.c:226

SUMMARY: AddressSanitizer: heap-use-after-free /root/ledger/src/account.cc:162:15 in   
ledger::account_t::apply_deferred_posts()
Shadow bytes around the buggy address:
  0x0c2a7fff8200: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x0c2a7fff8210: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x0c2a7fff8220: fd fd fd fd fd fd fd fd fd fd fd fa fa fa fa fa
  0x0c2a7fff8230: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c2a7fff8240: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
=>0x0c2a7fff8250: fd fd fd fd fd fd fd fd fd fd[fd]fd fd fd fd fd
  0x0c2a7fff8260: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x0c2a7fff8270: fd fd fd fd fd fd fd fd fd fd fd fa fa fa fa fa
  0x0c2a7fff8280: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c2a7fff8290: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c2a7fff82a0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==20621==ABORTING

Exploit Proof-of-Concept

Simply run the ledger binary with the provided proof-of-concept as an argument followed by the command type. Both xml and register will trigger the vulnerability.

$ ledger -f poc.sample [xml|register]

The proof-of-concept simply needs to have an account entry within it that includes a deferred posting. This means that a line must exist that begins with a number to specify an account directive. Then before the next account directive, there must be something within '<' and '>' symbols. This will enter the path that will dereference a function pointer.

Timeline

2017-04-07 - Vendor Disclosure
2017-08-30 - Public Release

Credit

Discovered by Cory Duplantis and another member of Cisco Talos.