CVE-2017-2808
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.
Ledger HEAD Ledger 3.1.1
http://ledger-cli.org
https://github.com/ledger/ledger.git
https://github.com/ledger/ledger/tree/v3.1.1
7.5 - CVSS:3.0/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H
CWE-416: Use After Free
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();
$ 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
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.
2017-04-07 - Vendor Disclosure
2017-08-30 - Public Release
Discovered by Cory Duplantis and another member of Cisco Talos.