--- --@author Sven Klemm module(... or "sedusa", package.seeall) require 'stdnse' require 'http' require 'xml' hints = {} verify = {} document_cache = {} Document = { new = function( doc ) doc.xml = nil if string.len( doc.body ) > 0 then doc.xml = xml.parse_html( doc.body ) end return doc end } http_get = function( u, options ) stdnse.print_debug( 2, "Fetching %s", u ) local document = http.get_url( u, options ) return Document.new( document ) end get_document = function( url ) local document if not document_cache[ url ] then document = http_get( url ) document_cache[ url ] = document else document = document_cache[ url ] end if document.status == 301 or document.status == 302 then -- take care of relative redirects if not document.header.location:match('https?://') then local base = url:match('^(.*/)[^/]*$') document.header.location = base .. document.header.location end document = get_document( document.header.location ) end return document end detect_app = function( u, no_verify ) local app,doc,exps,index,xpath local apps = {} doc = get_document( u ) for app, exps in pairs( sedusa.hints ) do for index, xpath in pairs( exps ) do if doc.xml:find( xpath ) then if no_verify then table.insert( apps, app ) else if sedusa.verify[app] then local version = sedusa.verify[app]( u ) if version then table.insert( apps, app .. " " .. version ) else table.insert( apps, app ) end else stdnse.print_debug( "No verify function for %s found.", app ) table.insert( apps, app ) end end break end end end return apps end -- setup default detector for some web applications for key, app in pairs( {'b2evolution', 'bBlog', 'C3D2-Web', 'DokuWiki', 'gitweb', 'Midgard', 'Nucleus CMS', 'Pentabarf', 'PhpWiki', 'Plone', 'PostNuke', 'TYPO3', 'vBulletin'} ) do hints[app] = { '//meta[@name="generator" and starts-with(@content,"' .. app .. '")]' } verify[app] = function( url ) local document = get_document( url ) -- look in generator meta tag local generator = document.xml:find( '//meta[@name="generator"]/@content' ) if generator and string.match( generator, app .. " (.*)" ) then return string.match( generator, app .. " (.*)" ) end end end hints['Bugzilla'] = { '//div[@id="banner"]/p[@id="banner-version"]/a[@href="http://www.bugzilla.org/"]/span[text()="Bugzilla"]', '//div[@id="header"]/table/tr/td[@id="title"]/p[starts-with(text(),"Bugzilla")]', } verify['Bugzilla'] = function( u ) local doc = get_document( u ) local version = doc.xml:find( '//div[@id="banner"]/p[@id="banner-version" and a[@href="http://www.bugzilla.org/"]/span[text()="Bugzilla"]]/span[starts-with(text(),"Version ")]/text()' ) if version then return version:match('Version (.*)') end version = doc.xml:find( '//div[@id="header"]/table/tr/td[@id="information"]/p[@class="header_addl_info"]/text()' ) if version then return version:match('version (.*)') end end hints['Burning Board'] = { '//span[@class="smallfont" and text()="Powered by "]/b[starts-with(text(),"WoltLab Burning Board")]', '//span[@class="smallfont"]/a[@href="http://www.woltlab.de"]/b[contains(text(),"Burning Board")]', } verify['Burning Board'] = function( u ) local doc = get_document( u ) local version = doc.xml:find( hints['Burning Board'][1] ) if not version then version = doc.xml:find( hints['Burning Board'][2] ) end if version then return version:match('Burning Board (.*)') end end hints['CommunityServer'] = { '//meta[@name="GENERATOR" and starts-with(@content, "CommunityServer")]/@content', '//a[@href="http://communityserver.org/r.ashx?1" and @target="_blank"]/img[starts-with(@alt,"Powered by CommunityServer")]', } verify['CommunityServer'] = function( u ) local doc = get_document( u ) local version = doc.xml:find( hints['CommunityServer'][1] ) if version then return version:match("^CommunityServer (.*)$") end end hints['Cisco VPN 3000 Concentrator'] = { '//title[starts-with(@text,"Cisco Systems, Inc. VPN 3000 Concentrator [")]', '//form/table/tr/td[@align="right" and @colspan="2" and text()="VPN 3000 Concentrator"]', } hints['Drupal'] = { '//div[@id="block-user-0" and h2[text()="User login"]]/div[@class="content"]/form[@id="user-login-form"]/div[div[@class="form-item"]/input[@type="text" and @class="form-text required"]]', '//p[@class="foot2"]/a[@href="http://drupal.org"]/img[contains(@title,"Powered by Drupal")]', } hints['Flyspray'] = { '//p[@id="footer"]/a[@href="http://flyspray.org/" and @class="offsite" and starts-with(text(),"Powered by Flyspray")]', '//p[@id="footer"]/a[@href="http://flyspray.rocks.cc/" and @class="offsite" and starts-with(text(),"Powered by Flyspray")]', } verify['Flyspray'] = function( u ) local doc = get_document( u ) local foot = doc.xml:find( hints['Flyspray'][1] ) if foot then return foot:match('Powered by Flyspray (.*)') end end hints['Joomla!'] = { '//meta[@name="Generator" and starts-with(@content,"Joomla!")]' } hints['GForge'] = { '//a[@href="http://gforge.org"]/img[@alt="Powered by GForge"]', } hints['Instiki'] = { '//head/link[@rel="stylesheet" and contains(@href,"/stylesheets/instiki.css")]', '//div[@id="footer"]/div[text()="This site is running on "]/a[@href="http://instiki.org" and text()="Instiki"]' } hints['MediaWiki'] = { '//body[contains(@class, "mediawiki")]', '//div[@id="footer"]/div[@id="f-poweredbyico"]/a[@href="http://www.mediawiki.org/"]/img', '//head/script[@type="text/javascript" and contains(@text,"var wgVersion")]' } verify['MediaWiki'] = function( u ) local document = get_document( u ) -- find out location of index.php local link = document.xml:find( '//a[contains(@href,"index.php?title=") and starts-with(@href, "/")]/@href' ) if link then link = link:match( "^(.*/index.php[?]title=).*") else link = document.xml:find( '//script[@type="text/javascript" and contains(text(), "var wgScriptPath = ")]' ) link = link:gsub( "[\r\n]", "" ) if link then link = link:match( 'var wgScriptPath = "([^"]+)"') .. '/index.php?title=' end end -- Special:Version has the most exact version including svn revision local version = get_document( url.absolute( u, link .. "Special:Version" ) ) if version.xml:find('//div/ul/li[a[@href="http://www.mediawiki.org/" and text()="MediaWiki"]]') then return string.match( version.xml:find('//div/ul/li[a[@href="http://www.mediawiki.org/" and text()="MediaWiki"]]'), "MediaWiki: ([^\r\n]+)" ) end if version.xml:find('//table[@id="sv-software"]/tr[td[a[@href="http://www.mediawiki.org/" and text()="MediaWiki"]]]') then return version.xml:find('//table[@id="sv-software"]/tr[td[a[@href="http://www.mediawiki.org/" and text()="MediaWiki"]]]/td[2]') end -- the version without revision is included on every page as javascript variable on recent versions local js = document.xml:find('//script[@type="text/javascript" and contains(text(), "var wgVersion = ")]') if js then js = js:gsub( "[\r\n]", "" ) return js:match( 'var wgVersion = "([^"]+)"') end -- get version from atom feed local atom = get_document( url.absolute( u, link .. "Special:Recentchanges&feed=atom" ) ) if atom.xml:find( '//generator[starts-with(text(), "MediaWiki")]' ) then return string.match( atom.xml:find( '//generator/text()' ), "^MediaWiki (.*)$" ) end end hints['OpenWRT'] = { '//head/title[text()="OpenWrt Administrative Console"]' } verify['OpenWRT'] = function( u ) local document = get_document( u ) local webif = document.xml:find( '//head/meta[@http-equiv="refresh"]/@content' ) if webif and webif:match('0; URL=') then document = get_document( url.absolute( u, webif:match( '0; URL=(.*)' )) ) local version = document.xml:find( '//div[@id="header"]/div[@id="header-title"]/div[@id="short-status"]/ul/li[strong[text()="Version:"]]/text()' ) if version then return version:match( ' +(.*)' ) end end end hints['PHP-Nuke'] = { '//meta[@name="GENERATOR" and starts-with(@content,"PHP-Nuke")]' } hints['phpMyAdmin'] = { '//title[starts-with(text(), "phpMyAdmin")]', '//link[@rel="stylesheet" and @type="text/css" and contains(@href,"css/phpmyadmin.css.php")]', '//a[@href="http://www.phpmyadmin.net" and @class="logo" and @target="_blank"]/img[@alt="phpMyAdmin"]' } verify['phpMyAdmin'] = function( url ) local document = get_document( url ) local title = document.xml:find( '//title[starts-with(text(), "phpMyAdmin")]/text()' ) local version = title:match('^phpMyAdmin (.*)$') if version then return version end end hints['phpSysInfo'] = { '//a[@href="http://phpsysinfo.sourceforge.net" and @target="_blank" and starts-with(text(),"phpSysInfo")]' } verify['phpSysInfo'] = function( url ) local document = get_document( url ) local version = document.xml:find( '//a[@href="http://phpsysinfo.sourceforge.net" and @target="_blank" and starts-with(text(),"phpSysInfo")]/text()' ) version = version:match('^phpSysInfo[-](.*)$') if version then return version end end verify['PhpWiki'] = function( url ) local document = get_document( url ) local generator = document.xml:find( '//meta[@name="PHPWIKI_VERSION"]/@content' ) if generator then return generator end end verify['Plone'] = function( u ) local document = get_document( u ) -- look in header if document.header.server then local version = document.header.server:match( 'Plone/(%d.%d.%d)' ) if version then return version end end end hints['Serendipity'] = { '//meta[@name="Powered-By" and starts-with(@content, "Serendipity")]', '//div[@id="serendipity_banner"]', '//div[@class="serendipity_entry_body"]', '//div[@class="serendipity_entryFooter"]', } verify['Serendipity'] = function( u ) local document = get_document( u ) local generator = document.xml:find( '//meta[@name="Powered-By"]/@content' ) if generator and string.match( generator, 'Serendipity v[.](.*)' ) then return string.match( generator, 'Serendipity v[.](.*)' ) end local rss_url = document.xml:find( '//link[@rel="alternate" and @type="application/rss+xml"]/@href' ) if rss_url then document = get_document( url.absolute( u, rss_url ) ) generator = document.xml:find( '//generator') if generator and string.match( generator, 'Serendipity [0-9.]+' ) then return string.match( generator, 'Serendipity ([0-9.]+)' ) end end end hints['SMF'] = { '//span[@class="smalltext"]/a[@href="http://www.simplemachines.org/" and contains(text(),"Powered by SMF")]', } verify['SMF'] = function(u) local doc = get_document( u ) local foot = doc.xml:find( hints['SMF'][1] ) if foot then return foot:match('Powered by SMF (.*)') end end hints['Trac'] = { '//div[@id="footer"]/a[@id="tracpowered" and @href="http://trac.edgewall.org/"]/img[@alt="Trac Powered"]', } verify['Trac'] = function( u ) local document = get_document( u ) local version = document.xml:find( '//div[@id="footer" and a[@id="tracpowered" and @href="http://trac.edgewall.org/"]]/p[@class="left"]/a/strong/text()' ) if version and version:match( 'Trac (.*)' ) then return version:match( 'Trac (.*)' ) end end hints['WordPress'] = { '//meta[@name="generator" and starts-with(@content,"WordPress")]', '//head/link[@rel="stylesheet" and @type="text/css" and contains( @href, "/wp-content/")]', '//head/style[contains( text(), "/wp-content/")]', '//div[@id="content"]/div[@class="post" and starts-with(@id, "post-") and div[@class="posttitle"] and div[@class="postmeta"] and div[@class="postbody"] and div[@class="postfooter"]]', '//ul/li/a[@href="http://wordpress.org/" and starts-with(@title,"Powered by WordPress")]', } verify['WordPress'] = function( u ) local document = get_document( u ) -- look in generator meta tag local generator = document.xml:find( '//meta[@name="generator"]/@content' ) if generator and string.match( generator, "WordPress (.*)" ) then return string.match( generator, "WordPress (.*)" ) end -- look in atom feed local atom = document.xml:find( '//link[@rel="alternate" and @type="application/atom+xml"]/@href' ) local feed = get_document( atom ) if feed.xml:find( '//generator[text()="WordPress"]/@version' ) then return feed.xml:find( '//generator[text()="WordPress"]/@version' ) end end