ERC 표준은 단순한 인터페이스 규약이 아니라,
컨트랙트 간 자산 이동 방식을 설계하는 핵심 패턴
이 글은 코드 레벨에서 approve/transferFrom의 내부 동작, ERC-721과 ERC-1155의 구현 차이,
NFT 메타데이터가 체인 밖에서 어떻게 관리되는지를 다룸


ERC-20 approve / transferFrom — 내부 구현까지

transfer vs transferFrom — 컨트랙트 관점의 차이

transfermsg.sender가 직접 토큰을 보내는 구조
외부 컨트랙트(DEX, 마켓플레이스)가 사용자 대신 토큰을 가져가려면 transfer로는 불가능
컨트랙트는 msg.sender가 될 수 없기 때문

// transfer — msg.sender가 직접 보냄
function transfer(address to, uint256 amount) public returns (bool) {
    _transfer(msg.sender, to, amount);
    return true;
}

// transferFrom — from의 allowance를 소진하면서 대리 전송
function transferFrom(address from, address to, uint256 amount) public returns (bool) {
    // 1. spender(msg.sender)의 잔여 allowance 확인
    uint256 currentAllowance = allowance(from, msg.sender);
    require(currentAllowance >= amount, "ERC20: insufficient allowance");

    // 2. allowance 차감
    unchecked {
        _approve(from, msg.sender, currentAllowance - amount);
    }

    // 3. 실제 잔액 이동
    _transfer(from, to, amount);
    return true;
}

_approve_allowances[owner][spender] = amount를 기록하는 내부 함수
_transfer_balances mapping을 직접 수정
두 함수 모두 storage write이므로 가스 비용 발생

allowance의 내부 저장 구조

// OpenZeppelin ERC20 내부 — allowance는 중첩 mapping으로 관리
mapping(address owner => mapping(address spender => uint256)) private _allowances;

// approve 호출 시
function approve(address spender, uint256 amount) public returns (bool) {
    _approve(msg.sender, spender, amount);
    return true;
}

function _approve(address owner, address spender, uint256 amount) internal {
    require(owner != address(0), "ERC20: approve from the zero address");
    require(spender != address(0), "ERC20: approve to the zero address");
    _allowances[owner][spender] = amount;
    emit Approval(owner, spender, amount);
}

// allowance 조회
function allowance(address owner, address spender) public view returns (uint256) {
    return _allowances[owner][spender];
}

approve 경쟁 조건(Race Condition) 공격

ERC-20 스펙 원문에 경고로 명시된 내용
allowance를 100에서 50으로 바꾸려 할 때, spender가 트랜잭션 사이에 먼저 100을 소진하면
최종적으로 100 + 50 = 150을 가져가는 공격이 가능

// 취약한 패턴 — 100 → 50으로 직접 변경
token.approve(spender, 50); // 트랜잭션 사이 spender가 기존 100을 먼저 소진 가능

// 안전한 패턴 1 — 0으로 먼저 초기화
token.approve(spender, 0);
token.approve(spender, 50);

// 안전한 패턴 2 — OpenZeppelin의 increaseAllowance / decreaseAllowance 사용
// (내부적으로 현재 값에 더하거나 빼는 방식으로 원자적 처리)
token.increaseAllowance(spender, 50);
token.decreaseAllowance(spender, 30);

다만 OpenZeppelin v5부터 increaseAllowance / decreaseAllowance가 제거되고
EIP-2612의 permit 방식(서명 기반 approve)이 권장 대안으로 제시됨

EIP-2612 permit — 가스 없는 approve

permit은 오프체인 서명으로 allowance를 설정하는 방식
사용자가 approve 트랜잭션을 별도로 보낼 필요 없이,
서명 데이터를 첨부해 transferFrom과 한 번에 처리 가능

// EIP-2612 permit 인터페이스
function permit(
    address owner,
    address spender,
    uint256 value,
    uint256 deadline,    // 서명 만료 시각
    uint8 v, bytes32 r, bytes32 s  // EIP-712 서명
) external;

// 사용 예: permit + transferFrom을 한 트랜잭션에 묶음
// approve 트랜잭션 불필요 — UX 개선 + 가스 절감
router.permitAndSwap(token, amount, deadline, v, r, s);

ERC-721 vs ERC-1155 — 구현 레벨 차이

ERC-721 — tokenId별 소유권 추적

ERC-721의 핵심은 각 tokenId를 독립적으로 추적하는 두 개의 mapping

// OpenZeppelin ERC721 내부 구조
mapping(uint256 tokenId => address) private _owners;      // tokenId → 소유자
mapping(address owner => uint256) private _balances;       // 주소 → 보유 수량

// ownerOf — 특정 토큰의 소유자 반환
function ownerOf(uint256 tokenId) public view returns (address) {
    address owner = _owners[tokenId];
    require(owner != address(0), "ERC721: invalid token ID");
    return owner;
}

// _transfer 내부 — 소유권 mapping 교체
function _transfer(address from, address to, uint256 tokenId) internal {
    require(ownerOf(tokenId) == from, "ERC721: transfer from incorrect owner");

    delete _tokenApprovals[tokenId]; // 기존 approve 초기화
    _balances[from] -= 1;
    _balances[to] += 1;
    _owners[tokenId] = to;

    emit Transfer(from, to, tokenId);
}

ERC-721 safeTransferFrom — 수신 컨트랙트 검증

transferFrom은 수신 주소가 컨트랙트여도 무조건 전송
수신 컨트랙트가 ERC-721을 처리하는 로직이 없으면 NFT가 영구적으로 잠김
safeTransferFrom은 수신자가 컨트랙트일 경우 onERC721Received를 호출해서 응답을 검증

// safeTransferFrom 내부 검증 로직
function _checkOnERC721Received(
    address from, address to, uint256 tokenId, bytes memory data
) private {
    if (to.code.length > 0) { // 수신자가 컨트랙트인지 확인
        try IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, data)
            returns (bytes4 retval) {
            // 올바른 selector를 반환해야 전송 성공
            require(retval == IERC721Receiver.onERC721Received.selector,
                "ERC721: transfer to non ERC721Receiver implementer");
        } catch {
            revert("ERC721: transfer to non ERC721Receiver implementer");
        }
    }
}

// 수신 컨트랙트가 반드시 구현해야 하는 함수
function onERC721Received(
    address operator, address from, uint256 tokenId, bytes calldata data
) external returns (bytes4) {
    // NFT 수신 처리 로직
    return IERC721Receiver.onERC721Received.selector; // 이 값을 반환해야 전송 완료
}

ERC-1155 — id별 잔액 관리와 배치 처리

ERC-721이 tokenId당 소유자 1명을 추적하는 것과 달리,
ERC-1155는 (주소, id) 쌍별 잔액을 관리하는 구조

// ERC-1155 내부 구조 — (owner, id) 쌍으로 잔액 관리
mapping(address account => mapping(uint256 id => uint256)) private _balances;

// 단건 잔액 조회
function balanceOf(address account, uint256 id) public view returns (uint256) {
    return _balances[account][id];
}

// 배치 잔액 조회 — 한 번의 호출로 여러 (account, id) 쌍 조회
function balanceOfBatch(
    address[] calldata accounts,
    uint256[] calldata ids
) public view returns (uint256[] memory) {
    require(accounts.length == ids.length, "ERC1155: accounts and ids length mismatch");
    uint256[] memory batchBalances = new uint256[](accounts.length);
    for (uint256 i = 0; i < accounts.length; ++i) {
        batchBalances[i] = balanceOf(accounts[i], ids[i]);
    }
    return batchBalances;
}
// 배치 전송 — 여러 토큰 유형을 한 트랜잭션으로 처리
function safeBatchTransferFrom(
    address from, address to,
    uint256[] calldata ids,
    uint256[] calldata amounts,
    bytes calldata data
) public {
    require(from == msg.sender || isApprovedForAll(from, msg.sender),
        "ERC1155: caller is not token owner or approved");

    for (uint256 i = 0; i < ids.length; ++i) {
        uint256 id = ids[i];
        uint256 amount = amounts[i];
        uint256 fromBalance = _balances[from][id];
        require(fromBalance >= amount, "ERC1155: insufficient balance for transfer");
        unchecked { _balances[from][id] = fromBalance - amount; }
        _balances[to][id] += amount;
    }
    emit TransferBatch(msg.sender, from, to, ids, amounts);
}

ERC-721 vs ERC-1155 — 코드 구조 차이 요약

항목ERC-721ERC-1155
소유권 저장mapping(uint256 => address)
tokenId → 소유자 1명
mapping(address => mapping(uint256 => uint256))
(주소, id) → 잔액
approve 단위tokenId 단위 또는 전체전체(setApprovalForAll)만 지원
배치 전송미지원safeBatchTransferFrom 기본 제공
수신 검증onERC721Received selector 검증onERC1155Received / onERC1155BatchReceived
FT 표현불가 (tokenId마다 고유)supply > 1로 FT처럼 사용 가능

NFT 메타데이터와 IPFS — tokenURI 내부 설계

tokenURI가 체인에 저장하는 것

ERC-721은 tokenURI(uint256 tokenId)를 통해 메타데이터 위치를 반환
이미지 자체는 체인에 없고, 메타데이터 JSON의 주소만 기록
이 주소가 IPFS CID를 가리키도록 설계하는 것이 현재 표준 관행

// 기본 tokenURI 구현 — baseURI + tokenId 조합
contract MyNFT is ERC721, ERC721URIStorage {
    uint256 private _nextTokenId;

    function safeMint(address to, string memory uri) public onlyOwner returns (uint256) {
        uint256 tokenId = _nextTokenId++;
        _safeMint(to, tokenId);
        _setTokenURI(tokenId, uri);  // tokenId → URI 매핑 저장
        return tokenId;
    }

    // ERC721URIStorage의 내부 — tokenId별 URI를 storage에 저장
    // mapping(uint256 => string) private _tokenURIs;
    function tokenURI(uint256 tokenId)
        public view override(ERC721, ERC721URIStorage) returns (string memory) {
        return super.tokenURI(tokenId);
    }
}

메타데이터 JSON 구조 — ERC-721 Metadata 표준

tokenURI가 가리키는 JSON의 스키마는 ERC-721 Metadata Extension에서 정의
IPFS에 올라가는 파일이 이 구조를 따라야 지갑과 마켓플레이스에서 올바르게 표시됨

// IPFS에 저장되는 메타데이터 JSON
{
  "name": "My NFT #42",
  "description": "A unique collectible from the MyNFT collection",
  "image": "ipfs://QmXyz.../image.png",   // 이미지 CID
  "attributes": [
    { "trait_type": "Background", "value": "Blue" },
    { "trait_type": "Rarity",     "value": "Legendary" },
    { "trait_type": "Level",      "display_type": "number", "value": 10 }
  ]
}

// tokenURI 예시
// "ipfs://QmMetaCID123.../metadata.json"
// 이 JSON 안의 image 필드가 다시 IPFS를 가리킴
// → 체인에는 CID 문자열만, 실제 파일은 IPFS 노드에

온체인 vs IPFS — 저장 방식별 비교

방식저장 위치비용영속성변조 가능성
온체인 (Base64 SVG)블록체인 storage매우 높음영구 보장불가 (immutable)
IPFS (CID 기반)분산 노드낮음핀닝 필요불가 (CID가 해시)
중앙 서버 (HTTP URL)단일 서버가장 낮음서버 의존가능 (서버 측 변경)

단순 텍스트나 SVG처럼 크기가 작은 경우 온체인에 직접 Base64로 인코딩해 저장하는 방식도 사용됨
이 경우 외부 의존성 없이 완전한 탈중앙화가 가능하지만 배포/민팅 비용이 급격히 증가

온체인 메타데이터 예시 — Base64 SVG

// 이미지까지 온체인에 담는 패턴
function tokenURI(uint256 tokenId) public view override returns (string memory) {
    string memory svg = string(abi.encodePacked(
        '<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200">',
        '<rect width="200" height="200" fill="black"/>',
        '<text x="100" y="110" text-anchor="middle" fill="white" font-size="24">#',
        tokenId.toString(),
        '</text></svg>'
    ));

    string memory json = Base64.encode(bytes(string(abi.encodePacked(
        '{"name":"NFT #', tokenId.toString(), '",',
        '"image":"data:image/svg+xml;base64,', Base64.encode(bytes(svg)), '"}'
    ))));

    return string(abi.encodePacked("data:application/json;base64,", json));
}
// 외부 서버/IPFS 없이 완전 온체인 — Loot, Nouns 등이 이 방식 사용

Pinata를 통한 IPFS 업로드 흐름

// Next.js API Route — Pinata에 파일 업로드 후 CID 반환
// /api/upload/route.ts
export async function POST(req: Request) {
  const formData = await req.formData()
  const file = formData.get('file') as File

  // 1. 이미지를 Pinata에 업로드 → 이미지 CID 획득
  const imageRes = await fetch('https://api.pinata.cloud/pinning/pinFileToIPFS', {
    method: 'POST',
    headers: { Authorization: `Bearer ${process.env.PINATA_JWT}` },
    body: (() => { const fd = new FormData(); fd.append('file', file); return fd })()
  })
  const { IpfsHash: imageCID } = await imageRes.json()

  // 2. 메타데이터 JSON을 Pinata에 업로드 → 메타데이터 CID 획득
  const metadata = {
    name: formData.get('name'),
    description: formData.get('description'),
    image: `ipfs://${imageCID}`          // 이미지 CID를 메타데이터에 포함
  }
  const metaRes = await fetch('https://api.pinata.cloud/pinning/pinJSONToIPFS', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${process.env.PINATA_JWT}`
    },
    body: JSON.stringify({ pinataContent: metadata })
  })
  const { IpfsHash: metaCID } = await metaRes.json()

  // 3. 메타데이터 CID를 tokenURI로 컨트랙트에 전달
  return Response.json({ tokenURI: `ipfs://${metaCID}` })
}

정리

주제핵심 구현 포인트
ERC-20 approve / transferFrom _allowances[owner][spender] mapping으로 한도 관리
연속 approve 시 경쟁 조건 주의 — EIP-2612 permit으로 대체 가능
ERC-721 safeTransferFrom 수신자가 컨트랙트면 onERC721Received selector 검증 필수
미구현 컨트랙트로 전송 시 NFT 영구 잠김
ERC-1155 배치 처리 (address, id) 쌍으로 잔액 관리
safeBatchTransferFrom으로 여러 유형 한 번에 처리
NFT 메타데이터 체인에는 CID 문자열만, 실제 파일은 IPFS에
온체인 SVG 방식은 외부 의존 없는 완전한 탈중앙화 가능

참고 자료

[1] Fabian Vogelsteller, Vitalik Buterin — ERC-20: Token Standard (2015)
    https://eips.ethereum.org/EIPS/eip-20

[2] William Entriken et al. — ERC-721: Non-Fungible Token Standard (2018)
    https://eips.ethereum.org/EIPS/eip-721

[3] Witek Radomski et al. — ERC-1155: Multi Token Standard (2018)
    https://eips.ethereum.org/EIPS/eip-1155

[4] Martin Lundfall et al. — EIP-2612: Permit Extension for EIP-20 (2020)
    https://eips.ethereum.org/EIPS/eip-2612

[5] OpenZeppelin — ERC20, ERC721, ERC1155 Contracts Source
    https://github.com/OpenZeppelin/openzeppelin-contracts

[6] Protocol Labs — IPFS Docs: Content Identifiers (CIDs)
    https://docs.ipfs.tech/concepts/content-addressing/

[7] ethereum.org — ERC-721 Non-Fungible Token Standard
    https://ethereum.org/en/developers/docs/standards/tokens/erc-721/

이더리움 스마트 컨트랙트에서 가스(Gas)는 곧 비용
같은 기능이라도 코드 작성 방식에 따라 가스 소비량이 수 배 이상 차이
배포 이후에는 수정 불가이므로 사전 최적화가 유일한 기회


Gas와 EVM 연산 비용

EVM(이더리움 가상머신)은 각 연산(Opcode)마다 고정된 가스 비용 부과
실제 수수료는 사용된 가스량 × Gas Price(Gwei)로 계산

storage 블록체인 영구 저장 SLOAD: 2,100 gas SSTORE: 20,000+ gas 트랜잭션 간 데이터 유지 memory 함수 실행 중 임시 저장 MLOAD/MSTORE: 3 gas 크기 따라 소폭 증가 트랜잭션 종료 시 소멸 stack EVM 연산 처리 공간 PUSH/POP: 2~3 gas 최대 depth 1,024 immutable/constant 참조 비용 높음 ◀──────────────────────────────── 비용 낮음

EVM 데이터 저장 위치별 비용 구조 — storage(영구), memory(임시), stack(연산용)
출처: ethereum.org — Ethereum Virtual Machine

위치별 접근 비용 차이가 최적화의 핵심 근거

데이터 위치Opcode가스 비용특징
storage 읽기 (cold)SLOAD2,100블록체인 영구 저장, EIP-2929 이후
storage 쓰기 (신규)SSTORE20,000+가장 비싼 연산
memory 읽기/쓰기MLOAD/MSTORE3트랜잭션 종료 시 소멸
바이트코드 인라인 값PUSH3immutable / constant 적용 시
산술 연산ADD, MUL3~5매우 저렴

2021년 Berlin 하드포크(EIP-2929)로 SLOAD 비용이 800 → 2,100으로 인상
스토리지 접근 횟수를 줄이는 패턴의 절감 효과가 이때부터 더욱 크게 작용


패턴 1. Custom Error — revert 비용을 4바이트로

require의 문자열은 배포 시 바이트코드에 그대로 포함
문자열 길이에 비례해 배포 비용 증가, revert 시마다 전체 문자열 ABI 인코딩 반환
Custom Error는 함수 시그니처 selector 4바이트만 반환 — 길이 고정

방식revert 반환 크기바이트코드 영향
require("Only owner can call this.")문자열 길이(~30바이트) 비례문자열 전체 포함
revert NotOwner()selector 4바이트 고정시그니처 해시만 포함
// Custom Error 선언 및 사용
error NotOwner();
error InsufficientBalance(uint256 available, uint256 required);

modifier onlyOwner() {
    if (msg.sender != owner) revert NotOwner();
    _;
}

function withdraw(uint256 amount) public {
    if (balance < amount)
        revert InsufficientBalance(balance, amount);
}

Solidity 0.8.4부터 도입, 파라미터를 추가해도 string 방식보다 항상 유리


패턴 2. immutable / constant — SLOAD를 PUSH로 교체

배포 후 변경되지 않는 값을 일반 상태 변수로 선언하면 읽을 때마다 SLOAD 발생
immutableconstant는 값을 바이트코드에 직접 인라인 삽입
읽기 연산이 SLOAD(2,100 gas) → PUSH(3 gas)로 교체됨

키워드값 결정 시점읽기 비용대표 사용 예
constant컴파일 시점~3 gas (PUSH)고정 상수 (uint256 MAX = 1000)
immutableconstructor 실행 시~3 gas (PUSH)배포자 주소 (msg.sender)
일반 상태 변수런타임 언제든2,100 gas (SLOAD)런타임 변경이 필요한 값
contract Counter {
    uint256 public constant MAX_VALUE = 10000; // 컴파일 타임 고정
    address public immutable owner;            // 배포 시 결정, 이후 불변

    constructor() {
        owner = msg.sender;
    }
}

패턴 3. storage 캐싱 — SLOAD 횟수 최소화

같은 스토리지 변수를 함수 내에서 반복 참조하면 그 횟수만큼 2,100 gas가 누적
로컬 변수에 한 번 캐싱하면 이후 접근은 memory 비용(~3 gas)으로 처리
루프와 결합 시 절감폭이 배열 크기에 비례해 선형으로 증가

// Before — SLOAD 3회 (6,300 gas)
function report() public {
    emit Log(counter);
    uint256 x = counter * 2;
    uint256 y = counter + 1;
}

// After — SLOAD 1회 (2,100 gas) + MLOAD 2회 (6 gas)
function report() public {
    uint256 c = counter;   // 단 한 번의 SLOAD
    emit Log(c);
    uint256 x = c * 2;
    uint256 y = c + 1;
}

배열을 순회하는 경우 arr.length도 스토리지 읽기 대상
루프 전에 uint256 len = arr.length로 캐싱하는 것만으로 반복 SLOAD 제거 가능


패턴 4. mapping으로 O(n) → O(1)

배열 순회는 데이터 크기에 비례해 가스가 선형 증가
데이터가 충분히 커지면 블록 가스 한도 초과로 트랜잭션 자체가 실패하는 상황 발생
mapping은 해시 테이블 구조로, 크기와 무관하게 항상 일정한 가스 소비

조회 방식시간 복잡도가스 특성
배열 순회 (for)O(n)데이터 크기에 비례해 선형 증가
mapping 조회O(1)크기 무관, 항상 고정
// 역매핑 패턴 — O(1) 조회 + 전체 목록 순회를 모두 지원
address[] public userList;
mapping(address => uint256) public userIndex;

function register(address user) internal {
    userIndex[user] = userList.length; // mapping으로 인덱스 저장
    userList.push(user);               // 배열로 전체 목록 유지
}

function getIndex(address user) public view returns (uint256) {
    return userIndex[user]; // O(1) 조회
}

정리

패턴BeforeAfter절감 핵심
Custom Error require("string") revert Error() revert 비용 고정 4바이트
immutable 일반 상태 변수 immutable 선언 SLOAD(2,100) → PUSH(3)
constant 일반 상태 변수 constant 선언 스토리지 슬롯 미사용
storage 캐싱 반복 SLOAD 로컬 변수 1회 캐싱 SLOAD 횟수 최소화
mapping 활용 배열 순회 O(n) mapping 조회 O(1) 가스 선형 증가 차단

이더리움 컨트랙트는 배포 후 수정 불가
가스 최적화는 비용 절감인 동시에 복잡한 스토리지 로직을 단순화해
reentrancy 등 보안 취약점 노출 면적 축소의 의미도 존재


참고 자료

[1] Ethereum Foundation — Ethereum Virtual Machine (EVM)
    https://ethereum.org/en/developers/docs/evm/

[2] Vitalik Buterin, Martin Swende — EIP-2929: Gas cost increases for state access opcodes (2020)
    https://eips.ethereum.org/EIPS/eip-2929

[3] Solidity Documentation — Custom Errors
    https://docs.soliditylang.org/en/latest/control-structures.html#custom-errors

[4] Solidity Documentation — The Optimizer
    https://docs.soliditylang.org/en/latest/internals/optimizer.html

[5] ethereum.org — Gas and Fees
    https://ethereum.org/en/developers/docs/gas/

 

cloudgoat/cloudgoat/scenarios/aws/iam_privesc_by_key_rotation at master · RhinoSecurityLabs/cloudgoat

CloudGoat is Rhino Security Labs' "Vulnerable by Design" AWS deployment tool - RhinoSecurityLabs/cloudgoat

github.com

목적

AWS SecretManager로부터 secret값을 가져올 것


1. User / Policy

AWS CLI 기본 자격증명 설정

aws configure --profile [ ]

 

자격증명자 신원 확인

자격증명자(User)는 manager_cgid1mqd5rnmh로 확인

aws sts get-caller-identity

 

manager_cgid1mqd5rnmh의 관리형 정책 list

관리형 정책 IAMReadOnlyAccess 확인

aws iam list-attached-user-policies --user-name [ ]

 

manager_cgid1mqd5rnmh의 인라인 정책 list

인라인 정책 SelfManageAccess, TagResources 확인

aws iam list-user-policies --user-name [ ]

관리형 정책(Managed Policy) vs 인라인 정책(Inline Policy)   

관리형 정책
AWS 또는 User가 별도로 생성한 독립적 정책
여러 User, Group, Role에 Attach 가능 (재사용성)

인라인 정책
단일 IAM에 대해 생성되는 직접적 정책
정책과 자격 증명이 엄격한 1:1 관계 (종속적)

 

관리형 정책과 인라인 정책 - AWS Identity and Access Management

관리형 정책과 인라인 정책 IAM에서 자격 증명에 대한 권한을 설정하는 경우 AWS 관리형 정책, 고객 관리형 정책 또는 인라인 정책 중 어느 것을 사용할지를 결정해야 합니다. 다음 주제에서는 각

docs.aws.amazon.com


2. 관리형 정책

관리형 정책 IAMReadOnlyAccess get

관리형 정책은 항상 version별로 관리
해당 정책은 v(ersion)4

aws iam get-policy --policy-arn [ ]

 

관리형 정책 버전 조회 및 Default list

현재 버전을 포함하여 총 4개의 버전 발견

aws iam list-policy-versions --policy-arn [ ]

 

2015.02 / iam:List*, Get*
2015.04 / 2015.02로부터 Credential 사용 현황 보고 권한 추가
2016.09 / 2015.04로부터 특정 User/Role의 Service 세부 정보 확인 권한 추가
2018.01 / 최종적으로 시뮬레이션 권한 추가

aws iam get-policy-version --policy-arn [ ] --version-id [ ]

버전별 권한 내용을 JSON 형태로 전부 조회
권한 상승 read 정도만 가능하고 직접 실행 X
Default 버전 외 버전도 취약하다기보다는 구식 버전 수준
결국 관리형 정책에서 얻을 수 있는 것 X


3. 인라인 정책

인라인 정책 SelfManageAccess 권한 get (JSON)

AccessKey에 대한 create/delete/update 권한 확인
MFA Device 권한 확인
해당 권한 사용 조건은 ResourceTag : developer
즉, developer Tag가 있는 대상에게 권한 행사 가능 

인라인 정책 TagResources 권한 get (JSON)

조건 없이 IAM의 모든 Resources Tag 조작 가능 권한 확인

aws iam get-user-policy --user-name [ ] --policy-name [ ]

developer Tag를 특정 User에게 부여하여 AccessKey와 MFA 관련 권한행사를 하는 것으로 추론 가능


MFA

Multi-Factor Authentication, 다중인증으로 Id/Password만으로 로그인하지 않고,
추가 인증 수단을 하나 이상 더 요구하는 로그인 방식 (다단계 프로세스)

 

MFA란 무엇인가요? - 다중 인증 및 2FA 설명 - AWS

MFA의 정의, 필요한 이유 및 모범 사례를 알아봅니다.

aws.amazon.com


4. Users - Tag

manager_cgid1mqd5rnmh Tag list

developer Tag 확인
Access Key와 MFA Device 권한 사용 가능

aws iam list-user-tags --user-name [ ]

 

모든 IAM User list

manager 말고 admin, developer User 존재 확인

aws iam list-users [ ]

 

admin의 각 정책 list

admin의 인라인 정책 AssumeRoles 발견

인라인 정책 AssumeRoles 권한 get (JSON)

cg_secretsmanager_cgid1mqd5rnmh Role Assume 권한 확인

 

developer의 각 정책 list

developer의 인라인 정책 DeveloperViewSecrets 발견

인라인 정책 AssumeRoles 권한 get (JSON)

단지, secret을 확인만 가능할 뿐 값은 열람하지 못하는 권한 확인

 

admin에게 developer Tag 추가

 admin을 대상으로 AccessKey 생성 가능

aws iam tag-user --user-name [ ] --tags Key=[ ], Value=[ ]

admin의 새 profile 등록 후 assume role을 하여 SecretManager로 접근하는 것으로 추론


5. admin

Access-Key는 User당 2개가 최대 (AWS 계정 공유 이슈)
aws iam delete-access-key --user-name [ ] --access-key-id [ ]
admin Access Key 생성

aws iam create-access-key --user-name [ ]

admin profile 등록
cg_secretmanager_cgid1mqd5rnmh Role의 관리형 정책 list

관리형 정책 cg_view_secrets_cgid1mqd5rnmh 확인
*Role은 유저에게 부여 X

aws iam list-attached-role-policies --role-name [ ]

 

관리형 정책 cg_view_secrets_cgidi1mq5rnmh get
관리형 정책 버전 조회 및 Default list
secretmanager에 대한 list 권한 확인

목적에 필요한 권한 발견
해당 권한이 있는 관리형 정책의 Role을 필요로 하는 이유 충분

 

cg_secretsmanager_cgidi1mqd5rnmh Role get

Assume을 하기 위해서는 MFA가 존재해야 한다는 조건 확인

aws iam get-role --role-name [ ]

6. MFA

admin_cgid1mqd5rnmh의 MFA device 목록 조회

MFA 존재 X

aws iam list-mfa-devices --user-name [ ]

 

MFA create

본인 PC 경로로 QRcode PNG 생성

aws iam create-virtual-mfa-device --virtual-mfa-device-name [ ] --outfile [ ] --bootstrap-method QRCodePNG

MFA 생성 확인

 

MFA 활성화 실패

authentication-code를 필요로 하면서 ERROR 발생
사실 실제로 QR코드를 활용하여 인증 필요

Google Authenticator

해당 어플로 QR코드를 스캔하여 OTP 번호 부여
authentication-code1은 최초 부여 번호,
authentication-code2는 1의 timeover 이후 번호

MFA 활성화

MFA 활성화로 Role Assume이 가능할 것으로 추측

aws iam enable-mfa-device --user-name [ ] --serial-number [ ] --authentication-code1 [ ] --authentication-code2 [ ]

 

Role assume

Role Assume에 성공하여 Access Key, Secret Key, Token 부여
해당 값으로 임시 자격 증명 획득 필요

aws sts assume-role --role-arn [ ] --role-session-name [ ] --serial-number [ ] --token-code [ ]

 

AWS CLI 임시 자격 증명 설정

set AWS_ACCESS_KEY_ID=[]
set AWS_SECRET_ACCESS_KEY=[]
set AWS_SESSION_TOKEN=[]

AWS Secrets Manager 모든 Secret list

aws secretmanager list-secrets

cg_secret_cgid1mqd5rnmh Secret 확인

cg_secret_cgid1mqd5rnmh secret get

aws secretsmanager get-secret-value --secret-id [ ]

flag 획득


AWS Secrets Manager

비밀번호/API 키/토큰 같은 민감한 비밀정보(Secrets)를 안전하게 저장/관리/자동 교체하는 AWS 관리형 서비스

 

AWS Secrets Manager란 무엇인가요? - AWS Secrets Manager

이 페이지에 작업이 필요하다는 점을 알려 주셔서 감사합니다. 실망시켜 드려 죄송합니다. 잠깐 시간을 내어 설명서를 향상시킬 수 있는 방법에 대해 말씀해 주십시오.

docs.aws.amazon.com

 

+ Recent posts