The popularity of a security consultant within a development oriented organisation is most certainly bi-polar. Occasionally, after thwarting a breach or reporting a bug directly to a developer rather than through JIRA (where it would expose their incompetence), we are gifted the opportunity of feeling a little more human and receive – for once – some warmth from our fellow compatriots. Most of the time, however, we’re that troll under the bridge pulling at peoples ankles, standing in their way and grunting orders at them as they try to cross. On the other hand, the reality is that the very nature of our jobs is to protect and help others, and to do so requires a solid understanding of all layers of the stack. So, for the most part we’re not grunting orders whilst having no clue as to what we’re talking about: we’re making well informed observations that warrant attention.

Many a dev shop I’ve stepped into can be likened to the Lord of the Flies, where the developers are so focused on design, functionality and UX that they lose touch with what really makes a product: engineering. Design may sell a product, but without solid engineering it will almost certainly see a short lifespan, significant downtime, no sales via word-of-mouth and/or reputational harm. What I’ve been trying to teach developers is that security not only has the function of protecting data and users, but it also promotes robust engineering. Making security a priority throughout the entire design and development process ultimately forms a more reliable product that will require less ongoing downtime to patch bugs – allowing developers to focus more on functionality and design during post go-live sprints. Think of it this way: if you cut corners when constructing the foundations and frame of a house, only to later discover that there is a critical issue with either, you’re going to have one hell of a time trying to address the issue without seriously impacting it’s occupants. So, the key to forcing a shift toward secure development practices is education: knowing vulnerabilities and their impact, coding securely, testing and how to efficiently integrate standards into projects. An effective tool to illustrate this and to get developers adopting more of a hacker mindset are HackMe applications. Previously I developed and released vuln_demo, however I’ve recently ended this project and created FooBl0g.

 

introducting foobl0g

FooBl0g takes the form your average blog. It has posts, categories, a merchandise store (sans the checkout), user accounts with profiles and of course management functions.


main page


footer (anonymous)


footer (user)


footer (admin)


a post

a merchandise item


profile editor


content editor


merchandise editor


user management

The application is developed using ASP.NET on top of a PostgreSQL database. Generally, the ‘post’ functions are implemented insecurely and the ‘merchandise’ functions securely.

step-by-step

To demonstrate some of the vulnerabilities I’ll step through the functionality of the application from the perspective of a new user.

registration

A new user must, of course, first register to make comments and have a profile. It’s worth noting at this point that all forms in this application (apart from one, that I’ll shortly mention) require a CSRF token in the form of a hidden input value and a cookie – both of which are machine-key encrypted with AES256-CBC and validated with HMAC-SHA256.

Upon successful validation of entered details an account is created and the user is able to log on.

 

password reset

Users are able to reset their password either of two ways:

  • Via their user profile editor.
  • Via the forgotten password mechanism.

I’ll cover off the first of those a little later, but the second is pretty straight forward: a user forgets their password so enters their email and is set a reset link. The reset link contains an ID which is used to fetch the reset record from the database, and a token that is used to decrypt the reset request (containing their username). Upon successful decryption they are permitted to reset the password. Sound fine? Well, it’s not. As there is no verification of their identity (e.g. using security questions or an SMS code), it does not protect against the scenario that the registered email has been compromised, and the attacker is leapfrogging between the users accounts.


user enters email


the reset request is confirmed

 
a reset link is sent to the registered email (which includes the username)


visiting the link allows the user to reset their password


confirmation of the reset

access-control

As illustrated in the outline of the blog functionlaity, dependent upon the type of user logged in they will have specific functions listed in the footer of the page. All users will be able to edit their profile, however admins will also be provided content and user management abilities. The flaw, however, is that the admin pages only assess whether the user is authenticated – there is no authorisation that takes place. Therefore, if a regular user knows the admin page names, they can access admin functionality.

Another flaw is that, even though you are able to disable posts in the content editor, they use predictable keys (insecure direct object reference) and the SQL does not ensure that posts are disabled before returning their data:

var cmd = 
    new NpgsqlCommand(
        "SELECT T1.postid, T1.posttime, T1.catid, T1.posttitle, T1.postbody, T2.catid, T2.catname FROM posts AS T1 LEFT OUTER JOIN categories AS T2 ON T1.catid = T2.catid WHERE postid='" + 
        Request.QueryString["id"] + "'", 
        conn);


disabled post in content editor


viewing disabled post

Conversely, mechandise items use 16 character random strings as keys, so are very difficult to predict. Additionally, the merchandise item viewer does not display disabled items as it explicitly ensures that an item is disabled before returning it’s data:
var cmd = 
    new NpgsqlCommand(
        "SELECT merchid, merchname, merchprice, merchimg, merchbody FROM merchandise WHERE merchenabled= true AND merchid= @MERCHID LIMIT 1", 
        conn);


disabled item in merchandise editor


viewing disabled item

injection

As illustrated above all category, post and merchandise viewers use a query string to retrieve specific items. Also shown above are two different methods for constructing a SQL query:
  • String concatenation.
  • Parameterised.

String concatenation using an unvalidated query string creates a SQL injection vulnerability. For example:

SELECT T1.postid, T1.posttime, T1.catid, T1.posttitle, T1.postbody, T2.catid, T2.catname FROM posts AS T1 LEFT OUTER JOIN categories AS T2 ON T1.catid = T2.catid WHERE postid=’4

Could become:

SELECT T1.postid, T1.posttime, T1.catid, T1.posttitle, T1.postbody, T2.catid, T2.catname FROM posts AS T1 LEFT OUTER JOIN categories AS T2 ON T1.catid = T2.catid WHERE postid=’4‘ AND ‘1’=’0′ UNION SELECT ‘1’,null,’3′,username,passwordhash,’6′,’7′ FROM users–

Where the text in red is the injected query string. As this forms valid SQL syntax the query is executed and the results returned. Whereas, in a parametrised SQL query the injected query string would simply form a parameter that cannot be escaped out of:

SELECT T1.postid, T1.posttime, T1.catid, T1.posttitle, T1.postbody, T2.catid, T2.catname FROM posts AS T1 LEFT OUTER JOIN categories AS T2 ON T1.catid = T2.catid WHERE postid=’‘4‘ AND ‘1’=’0′ UNION SELECT ‘1’,null,’3′,username,passwordhash,’6′,’7′ FROM users–

var cmd =
    new NpgsqlCommand(
        "SELECT merchid, merchname, merchprice, merchimg, merchbody FROM merchandise WHERE merchenabled= true AND merchid= @MERCHID LIMIT 1",
        conn);
var idParam = new NpgsqlParameter
{
ParameterName = "@MERCHID",
NpgsqlDbType = NpgsqlDbType.Varchar,
Size = 16,
Direction = ParameterDirection.Input,
Value = merchId
};
cmd.Parameters.Add(idParam);

var da = new NpgsqlDataAdapter(cmd);
var ds = new DataSet();
da.Fill(ds);

Furthermore, the query string is validated prior to even executing any SQL commands:

if (FooStringHelper.IsValidAlphanumeric(merchId, 16))
{
    RequestToken.Value = FooSessionHelper.SetToken(HttpContext.Current);
    Load_Forms(merchId);
}
public static bool IsValidAlphanumeric(string input, int length)
{
    var r = new Regex(String.Format("^[a-zA-Z0-9]{{{0}}}$", length.ToString()));

if (r.IsMatch(input))
{
return true;
}

return false;
}

The impact of such a small, simple change in coding methodology is quite large:


testing for sqli


dumping user accounts via sqli


injection into the merchandise page fails

SQLi via POST is also possible via the search function, however due to client-side validation the exploitation is a little more complex as it requires the use of an intercepting proxy like OWASP ZAP.

As search queries really only need to be alphanumeric – with perhaps some basic punctuation, Javascript is first used to strip any HTML tags out of the search term:

function stripTags(input) {
    var tmp = document.implementation.createHTMLDocument("New").body;
    tmp.innerHTML = input;
    return tmp.textContent || tmp.innerText || "";
}


search query including script tags


the search term is queried, minus the tags

The result is then posted to the search page, where is passed straight into the SQL query – thus permitting SQL injection:

var cmd =
    new NpgsqlCommand(
        "SELECT T1.postid, T1.posttime, T1.catid, T1.posttitle, T1.postbrief, T1.postenabled, T2.catid, T2.catname FROM posts AS T1 LEFT OUTER JOIN categories AS T2 ON T1.catid = T2.catid WHERE T1.postenabled= true AND LOWER(T1.posttitle) LIKE '%" +
        searchTerm.ToLower() + "%' ORDER BY T1.posttime",
        conn);


dummy term entered


term altered in ZAP


results of sqli (also vulnerable to reflective xss)

credential storage

Credentials are ‘protected’ using a single round of SHA256, and are encoded as hex:

public static string CreateShaHash(string input)
{
    var hasher = new SHA256Managed();
    byte[] passwordAsByte = Encoding.UTF8.GetBytes(input);
    byte[] encryptedBytes = hasher.ComputeHash(passwordAsByte);
    hasher.Clear();
    // Return as hex.
    return BitConverter.ToString(encryptedBytes).Replace("-", string.Empty);
}

Authentication should be, by design, as slow of a process as possible. Credential specific algorithms such as bcrypt are CPU intensive (you tune the ‘work factor’ of bcrypt based on what CPU resource you have available, and how long of a wait your users can handle), thus slowing brute force attacks. For home PC’s, cracking standard hashes has really jumped out of the feasible box and into the incredibly practical box. Using an overclocked GTX Titan X and hashcat you’re looking at:

  • 20,000,000,000 hashes per second of MD5.
  • 6,000,000,000 hashes per second of SHA1.
  • 2,000,000,000 hashes per second of SHA256.
  • 520,000,000 hashes per second of SHA512.
  • 17,000 hashes per second of bcrypt.

My work PC has an FX5000 series GPU that is truly workstation grade. Even still, it managed to crack the hashes obtained from FooBl0g via SQLi with relative ease:


hashcat running with a large dictionary and basic ruleset

xss+csrf

The comment feature of posts implements the same tag stripping as the search function, so an intercepting proxy is also required to inject HTML/JS into a page. After the text is injected into a posted form, no server-side validation takes place, nor does any output encoding.

Also, as aforementioned the ASHX handler used to reset passwords from the user profile editor requires no CSRF token – only a valid cookie and properly formatted JSON string. As the JSON string is created client-side in JS, the same code used to reset a users password legitimately can be used to formulate a combined XSS+CSRF attack.

var jsonData=JSON.stringify({Password:"foo",Confirmation:"foo"});$.ajax({url:"https://x.x.x.x/fooblog/reset_handler.ashx",type:"POST",data:jsonData,xhrFields:{withCredentials:!0}});

This script is encrypted:

 

… and then put in a JS file hosted on an internet accessible webserver. The script is linked to in a comment, so whenever the post is viewed the script executes in their browser and resets their password:


dummy comment entered


xss injected into post


script executes in an admin session

A mitigation can be as simple as calling the AntiXSS method ‘GetSafeHtmlFragment’ on output can prevent the script executing by sanitising and encoding the output, therefore loading the injected HTML as plaintext.

This can be observed by instead viewing the comment in the content editor, or by attempting the same injection for an item review:

safe output
the same markup is injected into a review post
the output is safe

xss+session hijacking

Using the same method of XSS as above, BeEF can be used to steal the users session cookie (as well as everything else that BeEF offers). This is made possible due to the fact that, whilst the cookie is encrypted, it is only tied to a specific account and not the machine of the user – permitting it to be tranferred from one browser to another and carrying the user session with it. The encryption only protects against user tampering of the encrypted contents.


the beef book script is injected into a comment

the victim browses to the post, their browser hooked and their cookie dumped


the cookie is loaded into the attacking browser


upon refresh of the page, the victim session is hijacked

shell upload

The final class of vulnerability is a shell upload. A shell is (typically) a single ASP or PHP document that facilitates arbitrary code execution on a web server, and can be either uploaded directly to the server where improper file validation occurs, or executed via Remote File Inclusion (RFI).

In FooBl0g there are two file upload facilities:

  • Profile editor: insecure.
  • Merchandise management: secure.

The profile editor uses regex to determine the type of the selected file:

$("input[id^='mainContent_userView_imageUploadForm']").each(function(i, el) {
    if (el.value) {
        var validExt = ['.bmp', '.jpeg', '.jpg', '.gif', '.png', '.tiff'];
        var isValidFileExt = (new RegExp('(' + validExt.join('|').replace(/\./g, '\\.') + ')$')).test(el.value);
if (!isValidFileExt || el.files[0].size > 2097152) {
Materialize.toast('Invalid Image', 4000);
isValid = false;
}
}
});


client-side file validation in action

Whereas the merchandise editor uses the HTML5 file API:

$("input[id^='mainContent_merchView_imageUploadForm']").each(function(i, el) {
    if (el.value) {
        if (el.files[0].type.indexOf("image") != 0 || el.files[0].size > 2097152) {
            Materialize.toast('Invalid Image', 4000);
            isValid = false;
        }
    }
});

However, both methods can be bypassed by first modifying the filename, uploading it, and editing the filename during POST:


the shell is renamed to have a valid file extension 


the file is uploaded as shell.jpg 


during upload, it is renamed back to shell.aspx


after the upload, where the image usually is located is a link to the shell

my shell

Whilst the profile editor performs no validation of the file (besides the size) prior to commiting it:

if (imageUploadForm.HasFile)
{
    string path = HttpContext.Current.Server.MapPath("~/uploads");
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}

HttpPostedFile file = HttpContext.Current.Request.Files[0];

if (file.ContentLength < 2097152)
{
string fileName;

if (HttpContext.Current.Request.Browser.Browser.ToUpper() == "IE")
{
string[] files = file.FileName.Split(new[] {'\\'});
fileName = files[files.Length - 1];
}
else
{
fileName = file.FileName;
}

fileName = FooStringHelper.RandomFileName(fileName);
string filePath = Path.Combine(path, fileName);

try
{
file.SaveAs(filePath);
Insert_NewImage(fileName, userId);
Reset_Page();
}
catch (Exception ex)
{
FooLogging.WriteLog(ex.ToString());
errorLabel.Text = "Upload failed.";
}
}

else
{
errorLabel.Text = "Invalid file.";
}
}

… the merchandise editor validates the file signature to ensure that it is an image:

if (FooFileHelper.IsImage(fileBytes) && fileBytes.Length < 2097152)
{
...
}
public static bool IsImage(byte[] fileBytes)
{
    ImageFormat fileType = GetImageFormat(fileBytes);
    return fileType != ImageFormat.unknown;
}
public enum ImageFormat
{
bmp,
jpeg,
gif,
tiff,
png,
unknown
}

public static ImageFormat GetImageFormat(byte[] bytes)
{
byte[] bmp = Encoding.ASCII.GetBytes("BM");
byte[] gif = Encoding.ASCII.GetBytes("GIF");
var png = new byte[] {137, 80, 78, 71};
var tiff = new byte[] {73, 73, 42};
var tiff2 = new byte[] {77, 77, 42};
var jpeg = new byte[] {255, 216, 255, 224};
var jpeg2 = new byte[] {255, 216, 255, 225};

if (bmp.SequenceEqual(bytes.Take(bmp.Length)))
return ImageFormat.bmp;

if (gif.SequenceEqual(bytes.Take(gif.Length)))
return ImageFormat.gif;

if (png.SequenceEqual(bytes.Take(png.Length)))
return ImageFormat.png;

if (tiff.SequenceEqual(bytes.Take(tiff.Length)))
return ImageFormat.tiff;

if (tiff2.SequenceEqual(bytes.Take(tiff2.Length)))
return ImageFormat.tiff;

if (jpeg.SequenceEqual(bytes.Take(jpeg.Length)))
return ImageFormat.jpeg;

if (jpeg2.SequenceEqual(bytes.Take(jpeg2.Length)))
return ImageFormat.jpeg;

return ImageFormat.unknown;
}

public static byte[] GetFileBytesFromHttpStream(HttpPostedFile file)
{
byte[] fileData = null;

using (var binaryReader = new BinaryReader(file.InputStream))
{
fileData = binaryReader.ReadBytes(file.ContentLength);
}

return fileData;
}

try fooblog

This project is hosted on GitHub.

One thought on “lord of the flies

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 )

Google+ photo

You are commenting using your Google+ 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 )

w

Connecting to %s