SQL Injection
SQL Injection 공격은 입력 값 검증의 취약점을 이용한 공격이다.
흔히 웹 개발을 하게 되면 특정 입력 값에 대해서 SQL 문을 실행하게 되는데 이러한 점을 이용하여 입력 값으로 SQL 문을 삽입해 의도치 않게 SQL 문을 실행하게 된다.
OWASP라는 웹 애플리케이션 보안을 담당하는 비영리 단체에서 4년마다 보안 취약점 TOP 10을 제공하고 있다.
위의 결과를 보면 2021년도에 SQL Injection 공격이 3위에 랭크되어 있다.
위의 링크를 클릭하면 더 자세한 정보들을 확인할 수 있다.
사용자의 정보가 저장되어 있는 데이터베이스는 항상 보안에 신경써야 한다.
종종 뉴스를 보면 개인 정보가 해커에 의해 탈취되면 엄청난 사회적 질타와 회사의 존립에 문제가 될 수 있기 때문에 데이터베이스 보안에는 엄청난 신경을 써야 한다.
내가 생각했을 때는 SQL Injection이 무서운 이유가 단순함이지 않을까 생각한다.
그냥 단순히 SQL 문을 실행시키는 입력 값에 SQL 쿼리 문을 작성해서 전송해 보고 어떤 결과 값이 나오는지 확인할 수 있어 항상 조심해야 된다.
SQL Injection 예방하는 방법
- Parameterized Queries(매개변수화된 쿼리)
Parameterized Queries는 간단하게 얘기하면 매개변수화된 쿼리라고 할 수 있다.
어떻게 매개변수화를 하는지 살펴보면 쿼리문에서 사용자의 입력 값에 대한 플레이스 홀더를 생성하고 대입되는 값을 나중에 입력 값으로 받아서 매개변수에 대입하는 방식이다.
Parameterized Queries를 적용하기 전에 쿼리문을 작성한 예시를 살펴보자.
String sql = "SELECT * FROM user WHERE username = " + username;
위의 코드를 보면 사용자 입력 값이 문자열 연결을 통해 쿼리문으로 포함되어 있다.
예를 들어 username에 ' OR '1'='1'이라는 입력 값이 들어왔다고 가정해보자.
OR '1'='1'을 쿼리문으로 실행하면 참 값이 되므로 DB에 저장된 user 테이블의 데이터를 반환해 주게 되므로 보안상 문제가 있는 코드가 된다.
즉, 입력 값을 문자열 연결을 통해 쿼리의 일부로 간주되면서 SQL 문법 자체로 해석된다.
그렇다면 Parameterized Queries를 적용하면 어떻게 변경될까?
Parameterized Queries를 적용하면 쿼리문과 사용자 입력 값이 별도로 처리가 된다. 이를 통해서 실행 시점의 데이터베이스 엔진은 이를 해당 입력 값을 데이터로 처리하게 된다.
이러한 Parameterized Queries는 다른 말로 Prepared Statement라고 부른다.
String username = "test";
String password = "qwer1234";
String sql = "SELECT * FROM user WHERE username = ? AND password = ?";
PreparedStatement stmt = conn.prepareStatement(sql);
stmt.setString(1, username);
stmt.setString(2, password);
위의 코드와 같이 쿼리문을 작성할 때 직접 입력 값을 대입하는 것이 아닌 ?로 대입한다.
?에는 각각 username과 password가 대입되면서 실행되는데 실행 가능한 SQL 쿼리문이 아닌 그냥 단순 문자열 형태로 대입된다.
즉, 1' or '1' = '1과 같이 SQL 문을 실행시킬 수 있는 코드가 아닌 그냥 "1' or '1' = '1"처럼 문자열로 대입하게 된다는 뜻이다.
- ORM
ORM 기술에서도 기본적으로 SQL Injection을 예방할 방법이 포함되어 있다.
JPA는 자바 진영의 ORM 기술인 Hibernate의 구현체인데 해당 Hibernate가 내부적으로 SQL문을 실행할 때 Prepared Statement로 실행하게 된다.
따라서 ORM 기술을 적용하는 것도 SQL Injection을 해결하는 방법 중 하나이다.
- 입력 값 검증
입력 값 검증은 가장 단순한 방법으로 데이터베이스에 영향을 끼치는 문자나 기호를 입력 값에서 필터링을 거는 방법이다.
위의 링크에 들어가면 어떤 문자나 기호가 SQL에 영향을 주는지 알 수 있다.
이러한 값들을 필터링하여 요청을 거부하면 SQL Injection 공격에 대해서 예방할 수 있다.
실제 문제를 통해 SQL Injection 해보기
드림핵에서 간단한 SQL Injection 문제를 풀어보자.
먼저 문제에서 제공하는 링크를 클릭하면 로그인을 할 수 있는 페이지로 이동한다.
로그인을 할 때 admin 계정으로 로그인을 해야 문제의 해답을 얻을 수 있는데 현재는 비밀번호를 모르는 상태이다.
아무 비밀번호나 입력하여 로그인을 해보면 위의 사진과 같이 wrong을 alert 창으로 보여준다.
문제에서 제공하는 소스코드를 살펴보자.
DATABASE = "database.db"
if os.path.exists(DATABASE) == False:
db = sqlite3.connect(DATABASE)
db.execute('create table users(userid char(100), userpassword char(100));')
db.execute(f'insert into users(userid, userpassword) values ("guest", "guest"), ("admin", "{binascii.hexlify(os.urandom(16)).decode("utf8")}");')
db.commit()
db.close()
코드는 파이썬으로 되어 있는데 코드를 보면 일반 guest 유저는 비밀번호가 guest인 것을 알 수 있지만 admin 계정은 16진수의 문자열로 생성하는 것을 알 수 있다.
즉, 현재 admin 계정의 아이디는 알 수 있지만, 비밀번호가 무엇인지 알 수 없다.
def login():
if request.method == 'GET':
return render_template('login.html')
else:
userid = request.form.get('userid')
userpassword = request.form.get('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}'
return f'<script>alert("hello {userid}");history.go(-1);</script>'
return '<script>alert("wrong");history.go(-1);</script>'
다음으로 로그인을 하는 코드를 살펴보면 쿼리문을 날려서 조건에 부합하는 사용자의 정보를 응답으로 받는다.
응답을 클라이언트에 반환해 줄 때 admin 계정의 아이디와 FLAG 값을 반환해 주는 것을 알 수 있다.
여기서 드림핵 문제의 요구사항은 해당 FLAG 값을 얻는 것으로 로그인만 성공한다면 자동으로 FLAG 값을 알 수 있다.
select * from users where userid="{userid}" and userpassword="{userpassword}"
문제에서 db에 쿼리문을 전송할 때 위의 select 문을 실행하게 되는데 아무런 입력 값 검증 없이 그냥 입력으로 받은 데이터를 그대로 userid에 대입하는 것을 알 수 있다.
그러면 아이디에 admin이 아닌 admin"--을 입력하면 어떻게 될까?
select * from users where userid="admin"-- " and userpassword="{userpassword}"
위의 쿼리문과 같이 실행이 될 것이고, 쿼리 문에서 -- 은 주석이기 때문에 뒤에 있는 and부터의 쿼리문은 실행되지 않는다.
따라서 비밀번호 검증하는 부분이 사라지므로 admin만 입력 값에 넣고 비밀번호는 아무거나 입력해서 로그인을 해보면
위의 사진과 같이 FLAG 값을 알 수 있게 된다.
'CS > 보안' 카테고리의 다른 글
[보안] - XSS (2) | 2024.08.26 |
---|