|
COMMAND IBM Informix Web DataBlade SQL injection via HTML header SYSTEMS AFFECTED Web DataBlade 4.12, IDS 9.20/9.21, Linux 2.2/2.4, SunOS 5.7 PROBLEM Simon Lodal says : Auto-decoding HTML entities ============================ HTML encoded strings are automatically being decoded when used in SQL statements. It causes developers to create code that looks fine but actually contains holes, since the logic is circumvented by WDB. Any worthy web/database programmer checks all user input before using it in an SQL query. WDB has a function $(WEBUNHTML) which converts the characters <>\"& to their HTML entities. When a string has been $(WEBUNHTML)\'ed it should thus be safe to use it in an SQL query, provided that you enclose the string in double quotes (there can not be any doublequotes inside the string). But somewhere on the path before the SQL query is being executed the HTML entities are actually decoded into their original character representations. I have not found this documented anywhere, and even if it is documented I would consider it a bug, since this \"feature\" certainly breaks the \"least surprise\" principle, which is a bad thing to do in security related areas. Example: <!-- Make inputstr harmless --> <?MIVAR NAME=inputstr>$(WEBUNHTML,$inputstr)<?/MIVAR> <!-- Build query to insert the checked string --> <?MIVAR NAME=qstr>INSERT into mytable VALUES (\"$inputstr\")<?/MIVAR> <!-- Execute query --> <?MISQL SQL=\"$qstr\"><?/MISQL> Besides of being an exampe of just how ugly WDB code is, this code looks correct; it runs $inputstr though the $(WEBUNHTML) function before inserting. But the query will actually fail if the original $inputstr contained a double quote, and it can therefore be exploited to execute other SQL code. The string is HTML decoded again somewhere, that is, the \" is converted back to a real doublequote. At first one may think that all the user can do is to make a query fail, by inserting just one quote somewhere, and that the attacker would have to know the exact query in order to actually make it succeed while being circumvented. But it is much simpler than that. The webexplode() function will always be available, and it can be used to execute SQL of choice. Since it returns string data it can simply be concatenated to other string data, thus executing any SQL, even without interrupting the original query. Proof of concept: Given the code above, the malicious user would have to put something like the following into an \"inputstr\" field in an HTML form and submit it: \" || webexplode(\"<?MISQL SQL=\'INSERT INTO sysusers VALUES (...)\'><?/MISQL>\", NULL) || \" This INSERT query writes to a sensitive part of the database, and returns nothing at all. The query on the HTML page would therefore succeed; nothing is actually changed in the input that it sees. And the attacker does not even have to know the query that is circumvented. SQL Injection ============= When a user makes a page request, webdriver executes a query that will both fetch and process the page. This query is vulnerable to SQL injection attacks, due to bad filtering/escaping of user input. Example: Request for \"http://victim.com/site/page.html\". HTTP authentication is in use, and a correct user/passwd has been supplied (have not tested this without HTTP auth). The webdriver log reports the following query being executed: SELECT webexplode(object,?::html),req_level FROM wbpages WHERE name=\'page\' AND path=\'/\' AND req_level <= 100; Explanation: webexplode() invokes the page engine, returns some processed HTML. wbpages is the table storing HTML pages, and the rest is a breakdown of the request. The path is \"/\" not \"/site/\" because webdriver is configured to operate only under the (virtual) directory \"/site\"; that is it\'s root directory. The .html extension is not part of the query since the extension has already been used in another query to figure out which table to fetch the page from. The value \"100\" is my personal \"user level\", which is assigned all users; when not using HTTP auth all users have a value of 0. Each page has a corresponding \"page level\" (req_level), thus the protection scheme is that to access a page with page level 200 you must be authenticated as a user who has a user level >= 200, or get access denied. Webdriver fails to properly escape quotes in input data. A request string of \"http://victim.com/site/\'--/page.html\" will modify the \"path\" part of the query, resulting in the following SQL query being executed: SELECT webexplode(object,?::html),req_level FROM wbpages WHERE name=\'page\' AND path=\'/\'--\' and req_level <= 100; Now we get \"http://victim.com/site/page.html\", or any other page we want, regardless of our user level. Adding a semicolon raises an error, so you can not execute multiple queries in one operation, and so you can only modify the existing clauses, or add others that will mostly only limit, not widen, what you get. But that is only until you start using UNION queries, these allow SQL of choice to be inserted. The point is that webdriver simply expects to get a processed page (essentially just a string) and an int value back from the query. How these values are created does not matter. As long as the final result contains exactly one row, having a string type column and an int type column, webdriver will return the string part to the user and be happy. So the trick is to make the default part of the query (see above) return nothing (no rows), then add another UNION\'ed query that returns the data we actually want. The webexplode() function returns data of type \"html\", and since all text types can be cast\'ed to \"html\" is is easy to create a UNION select; it can simply return any text type plus an int type. Consider: http://victim.com/site/\' UNION ALL SELECT FileToClob(\'/etc/passwd\',\'server\')::html,0 FROM sysusers WHERE username = USER --/.html This will get you: SELECT webexplode(object,?::html),req_level FROM wbpages WHERE name=\'\' AND path=\'/\' UNION ALL SELECT FileToClob(\'/etc/passwd\',\'server\')::html,0 FROM sysusers WHERE username = USER --\' and req_level <= 100; The first part of the query returns no rows (as long as http://victim.com/site/.html does not exist). The second part will read /etc/passwd and return it as the HTML page. The clause \"FROM sysusers WHERE username=USER\" is a dummy; there must be a FROM clause, and it must produce exactly one row. This hole is still not fully exploited. Adding a UNION\'ed query restricts us to using SELECT statements; even though you can use function expressions to do file I/O it is still not the same as being able to execute INSERT, UPDATE, CREATE, DROP etc. So we go looking for a way to execute entirely standalone SQL statements ... and we find the immediate solution is the webexplode() function, which is by definition available since we are running Web DataBlade. It takes as parameter some text and a list of environment variables. The first parameter is AppPage code (HTML code with embedded queries and ugly programming constructs) which is interpreted by webexplode(). webexplode() processes HTML code with embbeded SQL. The above request are plain GET request which can be typed into the address bar of a browser. However there is a limit on the query size, so we want to use POST instead. The following retrieves an HTML formatted list of all database users and passwords (may be encrypted depending on setup); substitute with any SQL (INSERT, UPDATE, DROP, etc): > telnet victim.com 80 Trying x.x.x.x... Connected to victim.com. Escape character is \'^]\'. POST /site/ HTTP/1.0 Content-Length: 215 Content-Type: application/x-www-form-urlencoded MIval=/\'UNION%20SELECT%20webexplode(\'<html><body><table><?MISQL%20SQL=%22SELECT%20*%20FROM%20wbusers%22><tr>{<td>$*</td>}</tr><?/MISQL></table></body></html>\',\'\'),0%20FROM%20sysusers%20WHERE%20username=USER--/.htmlm [ENTER] Similar bugs ============ The query exploited here is only the one used to fetch a page from the database. If the site is password protected you would need a valid login/pass to even get to the point where the page query is executed. The HTTP authentication is carried out by webdriver, which means it makes a query for the provided username and password. Not surprisingly this query is also buggy. So instead of spoofing the URL you could simply add quote tricks to the username provided. There are some problems with this approach however: - I have not found the exact username/password query in any logs, so it is hard to say what exactly the query expects. I have just seen the log emitting errors when putting quotes in the username. - Authentication info may be cached depending on configuration, which might mean that the query is not executed (not tested). SOLUTION None yet.