CMD 에서 아래 명령어를 입력해 보자.
C:\Users\crpark> curl -v http://victim:8080/WebGoat

위 사진에서는 요청과 응답 구조를 확인 할 수 있다.
- 연결 : 내 PC에서 연결 목적지(http://victim:8080/WebGoat) 연결 시도를 한다.
- 요청 시작 : GET /WebGoat ... => URL 프로토콜/버전 결정
- 요청 헤더 시작 : Host: victim .. => 요청 처리 시 추가 정보
- ~Accept : 요청 헤더 끝 (GET 요청 방식 : 요청 본문 생략)
- HTTP/1.1 : 응답 시작 => 프로토콜/버전 응답(상태)코드 메시지
- Server : 응답 헤더 시작 => 브라우저에서 참조하는 값
- ~Date : 응답 헤더 끝 (리다이렉션이 일어나므로 응답 본문이 생략) -> 다른 곳으로 /WebGoat 으로 보내지기 때문에 응답 본문이 생략된다.
리다이렉션 주소인 /WebGoat/ 로 바로 연결을 시도해보자.
CMD 에서 아래 명령어를 입력해 보자.
C:\Users\crpark> curl -v http://victim:8080/WebGoat/

http://victim:8080/WebGoat 주소를 브라우저에 사용입력하면 아래와 같이 자동으로 /가 붙어 리다이렉션 된다.

즉, 웹 서버가 자동으로 다시 접속하게 한다 == 리다이렉션
MDN 문서에서는 HTTP 인증에 대해 아래와 같이 설명한다.
https://developer.mozilla.org/ko/docs/Web/HTTP/Authentication
클라이언트가 서버에게 요청을 보내면, 웹 서버는 인증 여부를 결정해 인증되지 않은 사용자에게 인증을 요구한다. 위에서 나온 사진이 이 인증 정보 입력 창이다.

이는 기본적인 HTTP 인증 프레임워크 과정이다.
위 사진에서는 인증 정보를 BASE64 인코딩으로 인증 정보(로그인 정보)를 전송한다고 나와있다.
아이디와 패스워드는 webgoat / webgoat 이다.
이를 : 를 사용해 전송하므로 아래 그림과 같이 BASE64로 webgoat:webgoat 를 인코딩 값으로 변환하자.
webgoat:webgoat => d2ViZ29hdDp3ZWJnb2F0

CMD창에서 인증 정보를 요청 헤더를 추가(BASE64 인코딩 값) 후 같이 보내보자.
curl -v http://victim:8080/WebGoat/ -H "Authorization: basic d2ViZ29hdDp3ZWJnb2F0"

위 사진에서, 사용자가 base 인코딩 값을 가지고 요청한다.
그러나 기본 디렉터리 리스팅 설정이 되어 있지 않고 WebGoat 디렉터리에 기본 페이지가 존재하지 않아서 오류 반환된다.
위 방식에서 사용하는 인증 방식은 안전한 인증 방식이 아니다.
=> 암호화가 아닌 BASE64 인코딩한 인증 정보를 다시 디코딩 하면 사용자 계정 정보를 획득 가능하기 때문이다.
위 과정에서 HTTP 프로토콜은 사용하는 요청/응답은 상태를 유지하지 않는다.
요청을 보낸다. 상태 유지하지 않고, 응답을 보낸다.
때문에, 요청 간의 관계를 알 수 없다.
쿠키를 사용하면, 상태를 저장하는 매개체로 사용가능하다.

쿠키의 도입으로 인해
요청 헤더, 응답 헤더를 통해 전달되는 쿠키는 브라우저에 저장된다.
JavaScript를 이용해 접근이 가능하므로 쉽게 접근 가능하다.
=> 쿠키는 도용, 탈추, 위변조가 가능하다는 단점 존재.
따라서 쿠키에는 중요 정보를 저장하거나 사용하면 안된다.
⇒ HttpOnly 속성이 활성화되어 있다면 브라우저(클라이언트)에서 쿠키를 직접 핸들링할 수 없음 (document.cookie 호출 X)
Tomcat 7에서 HttpOnly 속성을 활성화하는 것이 기본값으로 변경되었음 ⇒ 비활성화를 위해서는 별도의 설정을 추가해야 함
HttpOnly 속성 비활성화시켜 보자.
크게 전역 설정, 웹 애플리케이션 코드에서 설정하는 방법이 있다.
전역 설정 : server.xml 또는 context.xml에서 useHttpOnly 속성을 설정하는 방법
웹 애플리케이션 코드 설정 : 쿠키 생성 시 설정
Cookie cookie = new Cookie("myCookie", "cookieValue");
cookie.setHttpOnly(true); // 이 부분에서 개발자가 직접 설정
response.addCookie(cookie);
context.xml 파일 설정 변경
/Servers/Tomcat v7.0 Server at localhost-config/context.xml 파일에서 아래와 같이 HTTP Only 설정을 false로 변경한다.
소스코드 저장 및 서버 재기동.

브라우저 쿠키 초기화
브라우저 - 개발자 도구 - 네트워크 - 로그인 응답 우클릭으로 쿠키 초기화 시킨다.


쿠키의 단점으로 인해 세션이라는 개념이 등장한다.
중요 정보를 세션에 저장하고, 사용자는 해당 세션에 접근할 수 있는 ID를 발급받아 접근한다.
쿠키 : 클라이언트 측 유지 데이터
세션 : 서버 측 유지 데이터

세션을 사용함으로써 중요 정보에 대한 직접적인 유출을 막을 수 있으나, 중요 정보 접근에 사용되는 세션 ID 생성 및 관리가 중요하다.
인증 전, 후 쿠키 값이 동일하다.

공격자가 인증 전(로그인 전) 쿠키 값을 탈취한다면, 로그인 가능할 것이다.
공격 예시 : 공격자가 희생자의 세션 ID를 미리 설정 후 공격 대상이 로그인 대기 -> 희생자 로그인 시, 인증 전후 세션 ID 동일하므로 공격자도 로그인 한 것으로 처리 -> 공격자가 희생자 권한으로 사이트 이용 가능.
세션 ID 훔치기
세션 ID가 요청, 응답을 통해 전달되는 과정 또는 사용자 PC, 브라우저에 저장된 것을 탈취, 조작할 수 있을 때 발생한다.
공격자가 전달 과정을 확인하는 것을 스니핑이라고 하며, 탈취는 XSS 공격을 통해 세션 ID를 획득 가능하다.
세션 ID 추측
세션 ID의 생성 규칙을 유추 가능한 경우 발생한다. 공격자가 생성될 세션 ID를 미리 설정 후 불특정 다수의 희생자에게 동일한 세션 ID가 생성되기를 대기한다. 동일 세션 ID가 발급되는 희생자가 발생하면, 희생자의 권한으로 공격자는 사이트를 이용한다.
TYPE1 지식 기반 : 사용자만 알고있는 정보를 이용해서 인증 - 패스워드,
TYPE2 소유기반 : 사용자가 가지고 있는 정보를 이용해 인증 : OTP, 인증서, 주민등록증, 스마트폰
TYPE3 특징기반 : 사용자만 가지고 있는 특징을 이용해서 인증 : 필기체, 서명, 홍채, 지문, 등..
두 개 이상의 인증 방법 사용 시 => MFA(Multi-Facor Authentication)라고 한다.
보안과 관련해 많은 곳에서 MFA 추천을 권고한다. (AWS의 IAM 보안 모범 사례)
접근 통제는 권한에 의해 실행할 수 있는 기능이 정해져야 한다.
따라서 접근 통제공격은 악의적인 의도로 취약한 접근 통제를 공격해 기능을 실행한다.
접근 통제 취약점이란?
특정한 기능 수행 과정에서 해당하는 권한을 가진 정상적 사용자 뿐 아니라 권한이 부적절한 사용자도 기능을 수행하게끔 하는 취약점
실습에서 접근 제어가 필요한 종류에 대해 알아보자.
1~2번. 비지니스 계층 접근 제어 Bypass
3~4번. 데이터 계층 접근 제어 Bypass
Tom 로그인 시(우측사진), 보이는 기능이 존재한다. ( tom )

이번에는 Jerry로 로그인 해보자. ( jerry )

Tom과 Jerry의 로그인 시, 사용 기능이 다르다.
Tom 로그인 시 사용 가능한 기능을 Burp Suite로 확인해보자.
아래 사진과 같이 기능 버튼 라벨 이름이 요청 시 사용된다.

action에 Jerry가 가지고 있던 DeleteProfile 기능을 사용하기 위해서는
Jerry는 DeleteProfile 버튼 누른다 => 요청 action에 DeleteProfile 파라미터를 사용한다 (에상)
따라서 ViewProfile 요청을 Brup Suite에서 아래와 같이 action에 DeleteProfile을 입력해 전송해보자.

결과를 확인해보면 아래와 같이 문제가 풀렸다고 나온다.
WebGoat가 삭제 기능으로 접속이 아닌, 요청만 보내도 해결했다고 처리해준다.

위 문제에서 알 수 있는 것은 무엇일까?
바로 접근 통제 취약점이 존재하며, 이를 악용 시 권한이 없는 사용자가 특정 권한을 가진 사용자의 기능을 수행할 수 있다.
Jerry만 사용가능한 기능을 Tom이 접근 제어 취약점을 악용, 서버측에서 이 요청을 처리했다.
따라서 해당 페이지 구성해주는 웹 서버에 접근 통제 취약점 존재한다.
/WebGoat/src/main/java/org/owasp/webgoat/lessons/RoleBasedAccessControl/RoleBasedAccessControl.java 소스코드에서 취약점을 탐색해보자.
웹 서버가 어떤 경로를 사용해 사용자가 원하는 파일을 가져오는지 확인 가능하다.
public void handleRequest(WebSession s)
{
// Here is where dispatching to the various action handlers happens.
// It would be a good place verify authorization to use an action.
// System.out.println("RoleBasedAccessControl.handleRequest()");
if (s.getLessonSession(this) == null) s.openLessonSession(this);
String requestedActionName = null;
try
{
requestedActionName = s.getParser().getStringParameter("action"); // action 으로부터 요청하는 기능을 받는다.
} catch (ParameterNotFoundException pnfe)
{
// Let them eat login page.
requestedActionName = LOGIN_ACTION;
}
// System.out.println("Requested lesson action: " + requestedActionName);
try
{
DefaultLessonAction action = (DefaultLessonAction) getAction(requestedActionName);
if (action != null)
{
// System.out.println("RoleBasedAccessControl.handleRequest() dispatching to: " +
// action.getActionName());
if (!action.requiresAuthentication())
{
// Access to Login does not require authentication.
action.handleRequest(s);
}
else
{
// ***************CODE HERE*************************
// *************************************************
if (action.isAuthenticated(s)) // 인증 여부를 확인
{
action.handleRequest(s); // 요청을 처리한다
}
else
throw new UnauthenticatedException();
}
}
요청한 사용자의 요청 처리 시, 인증여부만 확인하고, 별도의 권한 여부를 사용하지 않는다.
CODE HERE에 아래와 같은 요청 실행 시, 권한을 확인하는 코드를 삽입해보자.
if (action.isAuthenticated(s))
{
action.handleRequest(s);
}
else
throw new UnauthenticatedException();
// 요청 사용자의 인증 여부를 확인
if (action.isAuthenticated(s)) {
// 요청 사용자의 요청 권한 여부를 확인
// isAuthorized(WebSession s, int employeeId, String functionId)
if (action.isAuthorized(s, action.getUserId(s), action.getActionName())) {
action.handleRequest(s);
} else {
// 권한 오류
throw new UnauthorizedException();
}
} else {
// 인증 오류
throw new UnauthenticatedException();
}
이제 Stage 1에서처럼 Tom으로 로그인 후, View Profile 기능 요청을 Brup Suite로 DeleteProfile 로 변조해 공격해보자.

문제가 해결됐다고 나오지만, Stage 1에서 설정한 요청 실행 시, 인증 뿐 아니라 권한 확인으로 아래와 같이 오류 페이지가 출력된다.

수정된 코드에서는 isAuthorized 메소드를 사용해 (소스코드에서 정의) 사용자 ID가 요청 가능한 Action 실행 여부를 판별해준다.
HR 역할 Jerry는 로그인 후, 아래와 같이 다른 사용자의 프로파일을 열람 가능하다

만약 Tom이 다른 사용자의 프로파일을 열람하려고 한다면, 어떻게 해야 할까?
Stage 1에서 처럼, action에 ViewProfile으로 변조하면 오류가 발생할 것이다.
아래 사진에는 Jerry의 프로필을 확인하면, Manger 의 값이 102임을 확인 가능하다.

따라서 아래와 같이 employee_id 를 102 로, action을 ViewProfile으로 변조 후 보내보자.

위 사진처럼, Moe 사용자의 프로필에 접근 가능하다.
위 문제에서 알 수 있는 것은 무엇일까?
Stage 1에서 설정한 action을 변조 시, 기능 실행 시 권한이 없다면 오류를 발생시켰다.
그러나, employee_id까지 변조하자, 웹 서버측에서는 해당 값을 기준으로 권한이 있다고 착각하고 기능을 실행시켜 주는 것이다.
따라서 이는 데이터 계층에서 접근 권한에 취약한 점이 있다고 볼 수 있다.
웹 페이지에서는 프로필 조회 기능이 존재하지 않지만, 특정 권한을 가진 사용자(HR역할인 Jerry)만 가진 키 값을 가지고 프로필 목록을 조회한다. 이 키 값을 탈취 및 사용하면 프로필 조회 기능을 사용 가능하다.
아래와 같이 두 테이블이 존재한다.
만약 고길동 사용자만, view.do를 2002라는 키 값을 사용해 조회한다.

상세 정보를 확인하기 위해 두 가지 쿼리문을 사용해 조회한다.
여기서 발생하는 문제는 다음과 같다.
1. 첫 번째 쿼리문 실행 시, 유일 키 값을 가지고 view.do를 가져온다.
2. 두번째 쿼리문을 실행 시에는 어떠한 조건 없이 실행된다.
HR이라는 역할을 가진 사용자만, 상세 정보를 확인할 수 있는 접근 권한이 있다.
그러나, 해당 접근 권한을 가지고 상세 정보를 확인 시, 사용하는 로직에서 분리된 권한 확인 (위에서는 최초 1회만 확인)으로 인해 유일 키 값을 가지고 상세 정보에 접근 가능할 것이다.
이렇게 취약한 접근 통제를 보완하기 위해 두 쿼리문의 조건을 아래와 같이 한 번에 사용해야 한다.

상세 조회 기능을 안전하게 구현해보자
1. 프로필 조회에서 적용한 조건과 레코드 선택 조건을 함께 사용해야 한다.
/WebGoat/src/main/java/org/owasp/webgoat/lessons/RoleBasedAccessControl/ViewProfile.java 소스코드에서 취약점을 탐색해보자.

public Employee getEmployeeProfile(WebSession s, int userId, int subjectUserId) throws UnauthorizedException
{
Employee profile = null;
// Query the database for the profile data of the given employee
try
{
String query = "SELECT * FROM employee WHERE userid = " + subjectUserId; // userid를 유일키로 사용한다. 즉, 프로필 조회 기능 실행의 조건이 userid 보유 여부이다. (Jerry는 해당 유일 키 값을 가지고 있다)
try
{
Statement answer_statement = WebSession.getConnection(s)
.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
ResultSet answer_results = answer_statement.executeQuery(query);
if (answer_results.next())
{
// Note: Do NOT get the password field.
profile = new Employee(answer_results.getInt("userid"), answer_results.getString("first_name"),
answer_results.getString("last_name"), answer_results.getString("ssn"), answer_results
.getString("title"), answer_results.getString("phone"), answer_results
.getString("address1"), answer_results.getString("address2"), answer_results
.getInt("manager"), answer_results.getString("start_date"), answer_results
.getInt("salary"), answer_results.getString("ccn"), answer_results
.getInt("ccn_limit"), answer_results.getString("disciplined_date"), answer_results
.getString("disciplined_notes"), answer_results.getString("personal_description"));
/*
* System.out.println("Retrieved employee from db: " + profile.getFirstName() +
* " " + profile.getLastName() + " (" + profile.getId() + ")");
*/}
} catch (SQLException sqle)
{
s.setMessage("Error getting employee profile");
sqle.printStackTrace();
}
} catch (Exception e)
{
s.setMessage("Error getting employee profile");
e.printStackTrace();
}
return profile;
}
query를 만드는 조건을 추가한다.
String query = "SELECT * FROM employee, ownership
WHERE employee.userid = ownership.employee_id
and " + "ownership.employer_id = " + userId + "
and ownership.employee_id = " + subjectUserId;
기존 쿼리 :