Mark Mossberg's Blog

Hacker Nonsense

Abusing Admin Privileges via CSRF

Exploiting a classic CSRF vulnerability

When I took over responsibility as the webmaster for Northeastern University’s IEEE student chapter around January 2014 (yes, this is a very belated post), I was suddenly reponsible for maintaining a custom LAMP stack Content Management System (CMS) whose core functionality was letting an admin post to a news feed on the front page of the site. This site has since been completely redone, but given that it is notoriously difficult to program securely in PHP, I decided to poke around a little and see if I could find any cool bugs.

Initially, I started looking for the most blatant web vulns, SQLi and XSS, but was pleasantly surprised to find in the register.php file, for example, that handles user registration, to find a input sanitation check.

if ((isValid($_POST['newusername']))&&(isValid($_POST['newpassword'])))
    // continue with registration

where isValid() looks like

function isValid($varx)
    $valid = true;
    $bad_stuff = array("#","(",")","<",">","?","/","\\","[","]","|","$","'",":",";", "@");
    for($index = 0; $index < strlen($varx); $index++) {
        if(in_array(substr($varx,$index,1), $bad_stuff)) {
            $valid = false;
    if(substr($varx, 0, 1) == " ") {
        $valid = false;
    return $valid;

This code has a pretty substantial blacklist of commonly used characters in injections and operates by iterating over each character of the questionable input and testing if the character is in the blacklist, if so, setting the $valid variable to false. This seems to be an effective technique at ensuring the input is safe to use, however OWASP tends to discourage this model.

After ruling this out of potential vulns, I looked a little deeper into the code that powered the posting of news to the website, add-news.php.

Here we can see that if the HTTP request is a “POST” and the PHP session variables “isadmin” and “isofficer” are set to “yessir” and “true” respectively, then the code that adds news gets executed.

        if(isset($_SESSION['isadmin']) || isset($_SESSION['isofficer']))
            if($_SESSION['isadmin'] == "yessir" || $_SESSION['isofficer'] == "true")
                // add news

After these access checks pass, the POST request data is processed and ultimately a SQL query string is generated.

// continued from above
$title = htmlspecialchars($_POST['news_title'], ENT_QUOTES);
$text = htmlspecialchars($_POST['post'], ENT_QUOTES);
{ ... } // some omitted stuff
$user_query = "SELECT user_id, username FROM " . $INFO['sql_prefix'] . "users WHERE username = '" . $_SESSION['username'] . "'";
$user_result = mysql_query($user_query);
    //Returns an array with the data from the SQL select statement
    $user_row = mysql_fetch_row($user_result);

    $query1 = "INSERT INTO " . $INFO['sql_prefix'] . "news (news_title, news_type, time_posted, time_meeting, news_body, author_id, author_name, meeting_location) ";
    $query2 = "VALUES ('" . $title . "', '" . $type_of_news . "', '" . time() . "', '" . $time_of_meeting . "', '" . $text . "', '" . $user_row[0] . "', '" . $user_row[1] . "', '" . $meetinglocation . "')";
    $add_news_query = $query1 . $query2;
    $add_news_result = mysql_query($add_news_query);

// done

The important part to notice here is that there is no code that ensures the legitimacy of the request, that is, that a currently authenticated admin user actually meant to make this request. In this scenario if we can somehow find a way to get the admin to submit an arbitrary POST request to the add-news.php page, since she already has the session all set up in her browser, we can bypass the session checks previously shown and add arbitrary news to the website, for example.

You might be wondering how we can get the admin to submit arbitrary POST requests without her noticing. The most obvious answer is physical access to the her machine while she’s away or something, but a much more realistic scenario would be if we would get the admin to browse to a web page that we (the attacker) control, we can use some nifty JavaScript magic to get her to automatically submit the proper POST request on our behalf.

This is called Cross Site Request Forgery).

It turns out that these types of malicious pages are actually very simple to write. Remember, all that’s really needed is JavaScript execution, so for example if you had previous knowledge of a site that the admin frequented that had an XSS vulnerability, that would be a perfect way to chain these attacks. Anyway, here’s an example malicious page 1.

    <h1>non-malicious website! :)</h1>
    <form id="thisform" action="" method="POST"
        <input type="text" name="news_title" value="breaking news: u got hacked" />
        <input type="text" name="post" value="insert website defacement here" />
        <input type="submit"/>
    <script type="text/javascript" charset="utf-8">
        var frm = document.getElementById("thisform");

Nothing fancy here, just a simple hidden form with the values you want to submit to the page, and some JS that submits the form.

A caveat: Yes, it is true that you would have to guess the proper field names (“posts”, “news_title”) but even if you can only guess “posts”, you can write anything into the body of the news post.

End result looks something like this.

There was an identical bug in the code that handles editing users. Let’s have some fun!

In both of these gifs, the admin user was logged in, and then they opened a malicious html file, simulating visiting a malicious website. The mere act of opening the web page triggered the payload which sent the POST to the server, adding the news, and changing the user.

So how do we fix this sort of thing? The most common way involves the server requiring a randomly generated, non-predictable token that is associated with the user session with each request. An example could be that every time the actual admin web interface form is loaded, it contains a hidden field, invisible to the admin, with this token that is sent along with the request. The server verifies that it sent this token out previously, and that the token hasn’t expired, and if everything else checks out, the request goes through. This crucial missing piece of information will prevent attackers from successfully faking requests as the authenticated user.

  1. For more examples of how to trigger your CSRF payload via JavaScript, check out the excellent Ruby on Rails Security Guide