
문제에서 제공받은 Access Key와 Secret Key를 통해 Profile을 생성하고, sts get-caller-identity 명령을 사용하여 User에 대한 정보를 확인한다.

cg-bilbo-cgid8yo1qu7twp라는 이름의 User임을 확인할 수 있었으며 해당 User의 Group, Policy, Role 등을 조회하여 부여된 권한을 확인해 보기로 했다. 우선 list-groups-for-user 명령을 통해 User가 소속된 Group을 확인했고,

소속된 Group은 없는 것으로 확인되었다. 다음으로 User에게 연결된 Policy를 확인해 보자.
User에게 연결된 관리형 정책을 list-attached-user-policies 명령으로 조회했지만,

관리형 정책은 연결되어있지 않았다.
이후 인라인 정책을 list-user-policies 명령으로 조회했고,

cg-bilbo-cgid8yo1qu7twp-standard-user-assumer라는 이름의 고객 관리형 정책을 발견할 수 있었다. 이제 해당 정책의 세부 권한을 확인하기 위해 get-user-policy 명령으로 JSON 구조를 확인했다.

해당 정책은 cg-lambda-invoker로 시작하는 이름의 역할을 assume 할 수 있었다. 또한, IAM에 대한 조회가 가능하고 생성 예정인 정책과 생성되어 있는 정책을 이용해 어떤 동작이 가능한지 실험해 볼 수 있는 권한도 존재했다.
현재 User가 Assuem 할 수 있는 cg-lambda-invoker로 시작하는 역할에 대한 정보를 수집해 보자.
우선 IAM에 대한 조회가 가능한 권한을 보유했기 때문에, list-roles 명령으로 계정에 존재하는 모든 Role을 조회했다.


cg-lambda-invoker-cgid8yo1qu7twp, cgid8yo1qu7twp-policy_applier_lambda1 총 2개의 역할이 발견되었다.
cg-lambda-invoker-cgid8yo1qu7twp는 cg-bilbo-cgid8yo1qu7twp-standard-user-assumer 정책에서 확인할 수 있었듯, cg-bilbo-cgid8yo1qu7twp라는 User가 Assume 가능한 역할이었고,
cgid8yo1qu7twp-policy_applier_lambda1는 처음 발견하는 역할로, Lambda라는 서비스가 Assume 할 수 있었다.
각 Role의 권한을 자세히 살펴보자.
1. cg-lambda-invoker-cgid8yo1qu7twp
해당 Role에 연결된 관리형 정책과 인라인 정책을 각각 list-attached-role-policies 명령과 list-role-policies 명령으로 조회해 보았다.


그 결과, 관리형 정책은 연결되어 있지 않았고, lambda-invoker라는 인라인 정책이 연결된 것을 확인할 수 있었다.
get-role-policy 명령을 통해 lambda-invoker 정책의 JSON 구조를 확인해 세부적인 권한을 조회했다.

해당 정책은 cgid8yo1qu7twp-policy_applier_lambda1라는 Lambda 함수를 대상으로 정보 조회와 호출이 가능했다. (앞서 발견한 또 다른 Role과 이름이 같은 Lambda 함수였다.)
또한, 모든 Lambda 함수의 목록 조회와 iam 조회, 시뮬레이션 권한이 존재했다.
2. cgid8yo1qu7twp-policy_applier_lambda1
이번에는 Role 목록 조회를 통해 발견되었던 또 다른 역할인 cgid8yo1qu7twp-policy_applier_lambda1에 연결된 정책을 조회해 보았다. list-attached-role-policies 명령으로 연결된 관리형 정책은 존재하지 않음을 알게 되었고,


list-role-policies 명령으로 policy_applier_lambda1라는 인라인 정책이 연결되어 있다는 것을 알 수 있었다.
get-role-policy 명령을 통해 발견된 인라인 정책의 JSON 구조를 확인했고,

해당 정책에는 cg-bilbo-cgid8yo1qu7twp User에게 정책을 연결시킬 수 있는 권한과 cgidvuqbsdfb3f-policy_applier_lambda1:로 시작하는 이름의 로그 그룹을 대상으로 로그를 남길 수 있는 권한이 존재했다.
그렇다면 예상할 수 있는 시나리오는 현재 cg-bilbo-cgid8yo1qu7twp User의 권한으로 cg-lambda-invoker-cgid8yo1qu7twp 역할을 Assuem하고, 존재하는 Lambda 목록을 조회한다. 만약 cgid8yo1qu7twp-policy_applier_lambda1라는 Lambda 함수가 존재한다면, 해당 함수에 권한으로 cg-bilbo-cgid8yo1qu7twp User에게 높은 권한의 정책을 연결시켜 권한 상승을 진행한다. 최종적으로 권한 상승에 성공하면 문제의 목표였던 Secret Manager의 Secret 값을 읽을 수 있을 것이라 예상했다.
차례대로 우선 cg-lambda-invoker-cgid8yo1qu7twp 역할을 Assume 해보자.
sts assume-role 명령을 통해 옵션으로 둔 역할을 Assume 할 Session을 만들었고,

임시 자격 증명이 가능한 Access Key, Secret Key, Token이 출력되었다.
출력된 값을 아래와 같이 환경변수로 설정해 주면 profile을 명시하지 않고 명령어를 입력하였을 때, 해당 임시 자격 증명의 권한으로 명령어 사용이 가능하다.

이제 Assume한 역할의 권한으로 계정 내에 존재하는 Lambda 함수 목록을 조회해 보자. list-functions 명령을 통해 아래와 같은 함수 목록을 조회할 수 있었다.

그 결과 하나의 Lambda 함수를 조회할 수 있었고, 이미 cgid8yo1qu7twp-policy_applier_lambda1 역할이 연결되어 있었다.
get-function 명령을 통해 함수의 세부적인 정보를 조회하였고,

Lambda 함수의 실행 시작점인 Handler가 main.handler로 설정된 것과 코드가 S3에 저장되어 있다는 것과 미리 서명된 URL이 노출되어 있는 것을 확인할 수 있다.
이 URL은 객체 소유자만 접근할 수 있는 S3 객체에 접근할 수 있도록 하는 서명값이 포함되어, S3에 업로드된 Lambda 코드에 접근을 가능하게 해 준다.
https://docs.aws.amazon.com/ko_kr/AmazonS3/latest/userguide/ShareObjectPreSignedURL.html
미리 서명된 URL을 통해 객체 공유 - Amazon Simple Storage Service
AWS SDK를 사용할 때 태그 지정 속성은 쿼리 파라미터가 아닌 헤더여야 합니다. 다른 모든 속성은 미리 서명된 URL의 파라미터로 전달할 수 있습니다.
docs.aws.amazon.com
curl을 통해 해당 객체에 접근하여 lambda.zip이라는 이름으로 코드를 저장했다.

해당 zip 파일을 unzip을 통해 압축 해제하고, Lambda 함수의 Handler로 설정된 main.py의 코드를 handler 함수를 확인해 보았다.
아래는 main.py의 내용과 설명이다.
import boto3 #python에서 aws의 서비스를 이용할 수 있는 라이브러리
from sqlite_utils import Database
#sqlite 사용중
db = Database("my_database.db") #db 파일 my_database.db 지정
iam_client = boto3.client('iam') #iam 이용을 위해 객체로 정의
# db["policies"].insert_all([
# {"policy_name": "AmazonSNSReadOnlyAccess", "public": 'True'},
# {"policy_name": "AmazonRDSReadOnlyAccess", "public": 'True'},
# {"policy_name": "AWSLambda_ReadOnlyAccess", "public": 'True'},
# {"policy_name": "AmazonS3ReadOnlyAccess", "public": 'True'},
# {"policy_name": "AmazonGlacierReadOnlyAccess", "public": 'True'},
# {"policy_name": "AmazonRoute53DomainsReadOnlyAccess", "public": 'True'},
# {"policy_name": "AdministratorAccess", "public": 'False'}
# ])
#db에 위의 값들이 저장되어 있음 policies라는 테이블 안에 policy_name의 값으로 정책 이름이, public의 값으로 허용 또는 거부를 정의해둠
#인자값인 event와 context는 람다 함수의 기본 구성임. event는 람다를 트리거 한 이벤트에 대한 모든 정보를 포함하고, context는 실행되는 런타임 정보를 포함
def handler(event, context): #핸들러 함수로 lambda 실행 시 해당 함수가 실행됨
target_policys = event['policy_names'] #호출 시 전달된 policy_names를 객체로 생성
user_name = event['user_name'] #호출 시 전달된 user_name을 객체로 생성
print(f"target policys are : {target_policys}")
for policy in target_policys: #전달된 인자값 policy_names의 수 만큼 for문 실행
statement_returns_valid_policy = False #허용된 정책이 아니라면 false가 반환되도록 초기 for문에서 초기화
statement = f"select policy_name from policies where policy_name='{policy}' and public='True'" #statement 객체에 sql 구문 저장 -> db에서 현재 for문의 정책의 이름과 public은 true로 고정되어 있기에 public이 false인 값은 불러올 수 없음
for row in db.query(statement): #위의 sql 구문을 db에서 실행시키고 그값을 row에 저장
statement_returns_valid_policy = True #해당 for문이 돌아갔다는 것은 sql 쿼리에서 public이 true인 값을 반환했다는 것이기에 이전 for문에서 초기화 한 값을 true로 변경
print(f"applying {row['policy_name']} to {user_name}")
response = iam_client.attach_user_policy(
UserName=user_name,
PolicyArn=f"arn:aws:iam::aws:policy/{row['policy_name']}"
) #boto3를 통해 생성한 iam 객체를 이용해서 전달받은 인자값의 user에게 policy 연결
print("result: " + str(response['ResponseMetadata']['HTTPStatusCode']))
if not statement_returns_valid_policy: #만약 두번째 for문이 실행되지 않았다면 public이 false인 것이기에 if문이 동작
invalid_policy_statement = f"{policy} is not an approved policy, please only choose from approved " \
f"policies and don't cheat. :) "
print(invalid_policy_statement) #에러 메시지 반환
return invalid_policy_statement
return "All managed policies were applied as expected."
if __name__ == "__main__":
payload = {
"policy_names": [
"AmazonSNSReadOnlyAccess",
"AWSLambda_ReadOnlyAccess"
],
"user_name": "cg-bilbo-user"
} #페이로드 예시
print(handler(payload, 'uselessinfo'))
코드를 요약하자면 Lambda 함수 호출 시 전달되는 payload의 값으로 지정된 정책을 cg-bilbo-user에게 연결시키는 코드이다.
여기서 DB에 저장된 이름의 정책만 부여 가능하다는 것과, public이라는 값이 true로 설정되어 있어야 한다는 것을 알 수 있다.
때문에 평범한 상황에서는 public이 false로 설정된 AdministratorAccess를 User에게 연결시키지 못한다.
하지만, 해당 코드에는 SQL Injection에 대한 대응이 이루어지지 않았다. SQL Injection이란, SQL 쿼리의 의도와 다르게 SQL 구문을 조작하는 공격이다.
즉, public이 true인 값만 허용되도록 의도된 코드를 public이 false임에도 허용되도록 조작할 수 있다는 의미이다.
조금 더 쉽게 쿼리의 동작을 설명하자면,

원래 SQL 쿼리가 위와 같은 형태에서

주석을 추가하여 뒷부분의 조건을 제외시켜 주면 위와 같이 쿼리를 조작할 수 있다는 것이다.
이제 정말 최종적으로 시나리오의 Flow를 그려보자.

위 Flow에서 현재 4번까지 진행된 상태이며, 조작된 SQL을 payload에 포함시켜 Lambda 함수를 호출시켜 보자.
lambda invoker 명령으로 아래와 같이 Lambda 함수를 호출하려 했는데,

중간에 따옴표가 제대로 닫히지 않아 dquote가 발생하여 실패하였다. 이는 payload의 값을 file로 작성해 요청에 포함시키기로 했다.

이제 다시 lambda invoker 명령을 통해 Lambda 함수를 정상적으로 호출하는 것에 성공했다.

cg-bilbo-cgid8yo1qu7twp User에게 정상적으로 Admin 권한이 부여되었는지 list-attached-user-policies 명령을 통해 확인했고,

이전에는 존재하지 않았던 cg-bilbo-cgid8yo1qu7twp User의 관리형 정책에 AdministratorAccess이 추가된 것을 확인할 수 있었다.
해당 권한을 이용해 최종적으로 문제의 목표였던 Secret Manager의 Secret 값을 조회해 보자.
list-secrets 명령을 통해 계정에 존재하는 Secret의 목록을 조회했다.

그 결과 cgid8yo1qu7twp-final_flag라는 이름의 Secret을 확인할 수 있었으며, get-secret-value 명령을 통해 해당 Secret의 값을 불러와

flag를 획득할 수 있었다.
'CloudGoat' 카테고리의 다른 글
| codebuild_secrets (Hard) Write up (0) | 2026.01.18 |
|---|---|
| iam_privesc_by_attachment(Medium) Write up (0) | 2025.10.30 |
| RDS_snapshot(Medium) Write up (0) | 2025.05.01 |
| iam_privesc_by_rollback(Easy) Write Up (0) | 2025.04.27 |
| SQS_FLAG_Shop(Easy) Write-up (0) | 2025.04.23 |