/tmp/

雑なメモを置く場所。書いた内容の責任は取らないし、正確性、永続性なども保証しない。

mail コマンドで任意のコマンドが実行できる

Fail2ban の mail-whois と呼ばれる機能(Ban の通知を whois 結果付きでメール送るやつ)で CVE-2021-32749 が出ていた。

Possible RCE vulnerability in mailing action using mailutils (mail-whois) · Advisory · fail2ban/fail2ban · GitHub

これは mail コマンドに whois の内容を標準入力で渡しているのだけれど、mailtuils の mail コマンドには ~! エスケープ修飾子があると、その引数をコマンドみなしてシェル経由で実行するらしい。

確かに man を読むと書いてある。

mail(1): send/receive Internet mail - Linux man page

~!command
Execute the indicated shell command, then return to the message.

確かに実行できる。

root@8a57e227da26:/# cat /tmp/pwn.txt
aaa
~! uname -a

bbb
ccc

root@8a57e227da26:/# cat /tmp/pwn.txt | mail -s 'aaa' user@example.com
Linux 8a57e227da26 5.10.25-linuxkit #1 SMP Tue Mar 23 09:27:39 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
mail: cannot send message: Process exited with a non-zero status

man を読むと他にも色々あるっぽい。

~| でもコマンド実行につながるケースがあるらしいし、~r~w でファイルの書き込みができる。

mail コマンド経由で外部入力値を操作するようなアプリケーションをもし実装する場合は ~エスケープするだけで十分なのかな? 修正コミットを見た感じだと -E 'set escape' というオプションを使うことで対応している。

CVE-2021-32740

cve.mitre.org

修正コミットはこれ。

github.com

PoC は次の通り。テンプレートで利用される変数名を外部から操作できる場合に発生する。
GHSA では High になっているが、このようなケースは少ないだろうと思う。

irb(main):032:0> template = Addressable::Template.new("http://example.com/{000000000000000000000000000000!}")
=> #<Addressable::Template:0x1e0 PATTERN:http://example.com/{000000000000000000000000000000!}>
irb(main):033:0> Timeout.timeout(10) { template.expand({}) }
Traceback (most recent call last):
        23: from /Users/pivot_root/.anyenv/envs/rbenv/versions/2.7.2/bin/irb:23:in `<main>'
        22: from /Users/pivot_root/.anyenv/envs/rbenv/versions/2.7.2/bin/irb:23:in `load'
        21: from /Users/pivot_root/.anyenv/envs/rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/irb-1.3.3/exe/irb:11:in `<top (required)>'
         4: from (irb):33:in `<main>'
         3: from /Users/pivot_root/.anyenv/envs/rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/timeout-0.1.1/lib/timeout.rb:112:in `timeout'
         2: from (irb):33:in `block in <main>'
         1: from /Users/pivot_root/.anyenv/envs/rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/addressable-2.7.0/lib/addressable/template.rb:595:in `expand'
/Users/pivot_root/.anyenv/envs/rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/addressable-2.7.0/lib/addressable/template.rb:595:in `gsub!': execution expired (Timeout::Error)

ローカル環境開発を acme.sh + docker compose で実現する

色々試しつつも、最終的に落ち着いた。次のような感じ。

  • localhost.sandbox.directory の証明書を acme.sh で取得
  • localhost.sandbox.directory の A レコードに 127.0.0.1 をセットする
  • 証明書は nginx のコンテナに volume mount する

1. acme.sh で証明書を取得

GitHub - acmesh-official/acme.sh: A pure Unix shell script implementing ACME client protocol

❯ ~/.acme.sh/acme.sh --issue -k ec-256 -d 'localhost.sandbox.directory' --dns --yes-I-know-dns-manual-mode-enough-go-ahead-please
# ↑で取得した TXT レコードを localhost.sandbox.directory に設定する
❯ ~/.acme.sh/acme.sh --renew --ecc -k ec-256 -d 'localhost.sandbox.directory' --dns --yes-I-know-dns-manual-mode-enough-go-ahead-please

2. docker-compose で証明書をマウント

❯ cp -a ~/.acme.sh/localhost.sandbox.directory.in_ecc/* nginx/certs/

❯ cat docker-compose.yml
version: '3'
services:
  web:
    image: nginx:1.21.0
    volumes:
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf
      - ./nginx/certs:/etc/nginx/certs
    ports:
      - "8443:443"
    depends_on:
      - php
    links:
      - php
  php:
    image: php:7-fpm
    volumes:
      - ./app:/app

❯ cat nginx/default.conf
server {
  index index.php index.html;

  listen 443 ssl;
  server_name localhost.sandbox.directory;
  ssl_certificate /etc/nginx/certs/fullchain.cer;
  ssl_certificate_key /etc/nginx/certs/localhost.sandbox.directory.key;

  root /app;

  location / {
    try_files $uri $uri/ /index.php$is_args$args;
  }

  location ~ \.php$ {
    fastcgi_split_path_info ^(.+\.php)(/.+)$;
    fastcgi_pass php:9000;
    fastcgi_index index.php;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_param PATH_INFO $fastcgi_path_info;
  }
}

User-Agent Client Hints のメモ

従来の User-Agent の問題

  • 現在の UA は多くの情報が入っており、それをデフォルトで送信することはプライバシーの観点からやめたい
  • パースが複雑になるので、バグの原因となりやすい

User-Agent Client Hints

従来の UA と同じ情報にアクセスにできるようにしつつ、プライバシーの保護や、よりパースをしやすく整備した。 サーバーサイドでは特定のヘッダを、フロントエンドは新しいシンプルな API を使って取得できる。

Chrome 84 以降では chrome://flags/#enable-experimental-web-platform-features を有効にすることで利用できる。

User-Agent Client Hints にデフォルトで含まれるもの

  • Sec-CH-UA ... ブラウザ名と major/significant バージョン
  • Sec-CH-UA-Mobile ... モバイルデバイスかどうか (bool)

仕様の変更は今現在進んでいるので、デフォルトセットに変更があるかも。

これまでの UA の取得方法

  • navigator.userAgent
  • navigator.platform (非推奨)
  • navigator.appVersion (非推奨)
  • User-Agent HTTP ヘッダ

User-Agent Client Hints への移行

前提として Secure Context である必要があるので注意。

フロントエンド

  1. navigator.userAgentData を使う
if (navigator.userAgentData) {
 // use hints
} else {
  // fall back to user-agent string parse
}

navigator.userAgentData はブラウザのブランド名とバージョンをプロパティとして持っている。

> navigator.userAgentData
< NavigatorUAData {brands: Array(3), mobile: false}
    brands: Array(3)
      0: {brand: " Not;A Brand", version: "99"}
      1: {brand: "Google Chrome", version: "91"}
      2: {brand: "Chromium", version: "91"}
        length: 3
      __proto__: Array(0)
      mobile: false
    __proto__: NavigatorUAData

より詳細な情報がほしい場合は navigator.userAgentData.getHighEntroyValues を使う。Promise で返ってくることに注意。

> navigator.userAgentData
.getHighEntropyValues(["architecture", "bitness", "model", "platform", "platformVersion", "uaFullVersion"])
.then(ua => { console.log(ua) });
< Promise {<pending>}
< VM770:3 {architecture: "x86", model: "", platform: "macOS", platformVersion: "10_15_7", uaFullVersion: "91.0.4472.77"}
  1. モバイルの判定には navigrator.userAgentData.mobile を使う
const isMobile = navigator.userAgentData.mobile;

サーバーサイド

User-Agent の代わりに Sec-CH-* ヘッダを見るようにする。Chrome 91.0.4472.77 では、次のような HTTP リクエストヘッダがついている。

sec-ch-ua: " Not;A Brand";v="99", "Google Chrome";v="91", "Chromium";v="91"
sec-ch-ua-mobile: ?0

さらに情報がほしい場合は Accept-CH で指定すると、後続のリクエストから取得できる。

Accept-CH: Sec-CH-UA-Mobile, Sec-CH-UA-Model, Sec-CH-UA-Platform, Sec-CH-UA

もし First リクエストから取得したい場合は Critical-CH ヘッダを使うらしい(試していないので未確認)。Critical-CH ヘッダを使うと、ブラウザはそのヘッダを付けてリクエストをやり直す。

Critical-CHAccept-CH と同じ値を持たなければいけない。

Critical-CH: Sec-CH-UA-Mobile, Sec-CH-UA-Model, Sec-CH-UA-Platform, Sec-CH-UA

Cross Origin リクエストの場合

https://blog.sitehttps://cdn.site のリソースを読み込んでいる場合、https://blog.siteSec-CH-UA-Platform をリクエストして取得できるが、https://cdn.site は取得できない。そのため、Permissions-Policy で明示的に https://cdn.site に委任する必要がある。委任できるリストは https://wicg.github.io/client-hints-infrastructure/#policy-controlled-client-hints-features で確認できる。

iframe の場合

blog.site に次のような iframe を埋め込むと、そのさきの https://widget.siteUH-UA-Model を取得できる。

<iframe src="https://widget.site" allow="ch-ua-model"></iframe>

References

Clear Site Data

Clear Site Data

SPA とかで Cookie とか LocalStorage とか、そういうものを消し忘れてバグる、みたいなのがあるので、ヘッダで消せるようにする提案。

Clear Site Data

<?php
header('Clear-Site-Data: "cookies", "executionContexts');
?>

これで Cookie が全部消える。cachestorage などが指定できる。Chrome M61 で既に使えるようになっている。

ログアウト時になにか消し忘れるのを防止できるし、永続化する XSS などへの対応もできる。

Credential Management Level 1

Credential Management

Credential Management Level 1

  • Web サイトが UA に資格情報をリクエストし、UA がそれを使用できるようにする API
  • スキームがダウングレードする場合はクレデンシャルを共有してはいけない(MUST NOT)
  • 呼び出された Origin と Credential の Origin を比較して決定される
    • admin.example.com に保存されているクレデンシャルを www.example.com から呼び出すことは可能
    • XSS でパスワード抜かれることに繋がりそう
  • CSP で保護することが推奨されている
  • Secure Context のみで利用可能
  • Timing Attack 防止のため、回数に制限もかける (SHOULD)
<html>
  <body>
    <form action="/login.php" method="post" id="login">
      <input name="username" />
      <input type="password" name="password" />
      <button type="submit">Login</button>
    </form>

    <button onclick="autoLogin()">Auto Login</button>
  <script>
    function autoLogin() {
      // navigator.credentials.get({ 'password': true }).then(credential => { if (credential) { console.log(credential); } else { console.log("has not credentials"); }})
      navigator.credentials
        .get({ 'password': true })
        .then(credential => {
          if (!credential) { return; }
          if (credential.type == 'password') {
            document.querySelector('input[name=username]').value = credential.id;
            document.querySelector('input[name=password]').value = credential.password;
            document.getElementById('login').submit();
          }
      });
    }
  </script>
  </body>
</html>

f:id:pivot_root:20210508190951p:plain

Trusted Types

Trusted Types (TT) とは

DOM Based XSS を緩和するための機能。次のような危険な Sink にデータを渡す前に処理する(Trusted Type 型に変換する)ことができる。

  • <script src> 及び <script> のテキストコンテンツの設定
  • 文字列から HTML を生成するもの
    • innerHTML
    • outerHTML
    • inserAdjacementHTML
    • <iframe> srcodc
    • document.write
    • document.writeln
    • DOMParser.parseFromString
  • プラグインの実行
    • <embed src>
    • <object data>
    • <object codebase>
  • JavaScript コードの実行
    • eval
    • setTimeout
    • setInterval
    • new Function()
// ❌ TypeError
element.innerHTML = location.href;

// ✅ TrustedHTML Object のみ可能
element.innerHTML = trustedHTML;

Trusted Types の使い方

Example

<?php

header("Content-Security-Policy: trusted-types example dompurify; require-trusted-types-for 'script'");
?>

<html>
  <head>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.2.6/purify.min.js" integrity="sha512-rXAHWSMciPq2KsOxTvUeYNBb45apbcEXUVSIexVPOBnKfD/xo99uUe5M2OOsC49hGdUrkRLYsATkQQHMzUo/ew==" crossorigin="anonymous"></script>
  </head>
  <body>
    <script>
    const policy = trustedTypes.createPolicy('example', {
      createHTML: (untsutedValue) => {
        return DOMPurify.sanitize(untsutedValue, {RETURN_TRUSTED_TYPE: true})
      }
    });

    // http://localhost/#'%22%3E%3Csvg/onload=alert(1)%3E
    const rawHTML = decodeURIComponent(location.hash.substring(1));
    // document.body.innerHTML = rawHTML;
    document.body.innerHTML = policy.createHTML(rawHTML);
    </script>
  </body>
</html>

当然、 meta タグでも可能。

 <meta http-equiv=content-security-policy content="require-trusted-types-for 'script'; trusted-types example dompurify">

report-only で適用する

Content-Security-Policy-Report-Only: require-trusted-types-for 'script'; report-uri //my-csp-endpoint.example

Enforce

Content-Security-Policy: require-trusted-types-for 'script'; report-uri //my-csp-endpoint.example

違反レポートの収集

window.addEventListener('securitypolicyviolation', console.error.bind(console));

ポリシーの作り方

自前でポリシーを用意する。

// createHTML() 関数を介して、TrustedHTML オブジェクトを生成できる myEscapePolocy を定義
// trustedTypes.createPolicy() に渡される JavaScript 関数は文字列を返すが、
// createPolocy() は戻り値を TrustedHTML などの正しい型で wrap する
if (window.trustedTypes && trustedTypes.createPolicy) {
  const escapeHTMLPolicy = trustedTypes.createPolicy('myEscapePolicy', {
    createHTML: string => string.replace(/\</g, '&lt;')
  });
}
const escaped = escapeHTMLPolicy.createHTML('<img src=x onerror=alert(1)>');
console.log(escaped instanceof TrustedHTML);  // true
el.innerHTML = escaped;  // '&lt;img src=x onerror=alert(1)>'

DOMPurify は Trusted Types をサポートしているので、Sanitize 結果として TrustedHTML オブジェクトを返すことができる。

if (window.trustedTypes && trustedTypes.createPolicy) { // Feature testing
  trustedTypes.createPolicy('default', {
    createHTML: (string, sink) => DOMPurify.sanitize(string, {RETURN_TRUSTED_TYPE: true})
  });
}

この辺は Sanitize API が普及すると、それを使うようになるだろう。

default ポリシー

default というポリシー名を利用すると、Sink にデータが渡されると暗黙的に呼び出される。第一引数は文字列値、第二引数はシンク名となる。

TrustedType

  • trustedHTML ... HTML としてレンダリングしてよい信頼された文字列。 TrustedTypePolicycreateHTML によって生成される。
  • trustedScript ... Script として実行してよい信頼された文字列。 TrustedTypePolicycreateScript によって生成される。
  • trustedScriptURL ... 外部のスクリプトリソースとして信頼された文字列。 TrustedTypePolicycreateScriptURL によって生成される。

ブラウザの対応状況

2020/05/08 現在

Chrome 83 で対応。他のブラウザでは polyfill で対応できる。

ライブラリでの対応

当然使っているライブラリで危険な Sink を使っている場合は対応が必要になる。

e.g. https://github.com/bvaughn/react-virtualized/pull/1614

Security

色々バイパスする方法はあるっぽい。

Trusted Types bypass challenge solutions | shhnjk.github.io

Blob URL

const  blob = new Blob([untrustedString], {type: 'text/html'});
const  url = URL.createObjectURL(blob);
location = url;

Blob URL は TT で制限できない。その理由は次の通り。

  • type 引数は安全なもの (text/plain など) にすることができる
  • URL.createObjectURL は File オブジェクトと MediaSource オブジェクトも受け入れる

Non-DOM API Based script

DOM API ではないスクリプトの読み込みは TT で制限できない。

import(`/${untrustedString}.js`)

Cross Document

TT は HTMLAnchorElement.hrefHTMLIFrameElement.src などの sink に JavaScript URL を代入することをブロックしない。
CSP と同じように、TT が適用されたドキュメントで JavaScript URL への navigation をブロックするようになっている。

しかしながら、CSP でも既知の問題である Cross Document 間での相互作用の問題があり、一方のドキュメントに TT が適用されていない場合、もう一方の TT を Bypass できてしまう。これは https://wicg.github.io/origin-policy/ で対応しようとしている。

具体例は次の通り。robots.txt に TT が適用されていない場合、JavaScript URL は robots.txt の iframe のドキュメントで実行されるので、XSS が生じる。

<!DOCTYPE html>
<html lang="en">
<head>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.2.6/purify.min.js" integrity="sha512-rXAHWSMciPq2KsOxTvUeYNBb45apbcEXUVSIexVPOBnKfD/xo99uUe5M2OOsC49hGdUrkRLYsATkQQHMzUo/ew==" crossorigin="anonymous"></script>
  <meta http-equiv=content-security-policy content="require-trusted-types-for 'script'; trusted-types default dompurify">
</head>
<body>
  <iframe src="robots.txt" name="new"></iframe>
  <script>
    trustedTypes.createPolicy('default', {
      createHTML: (untsutedValue) => {
        return DOMPurify.sanitize(untsutedValue, {RETURN_TRUSTED_TYPE: true})
      },
      createScriptURL: (url) => {
        return ""
      }
    });

    const url = "javascript:alert(origin)";
    const a = document.createElement('a');
    a.href = url;
    a.target = 'new';
    a.textContent = 'go';
    document.body.appendChild(a);
  </script>
</body>
</html>

HTMLAnchorElement properties (Polyfill only)

2020/05/08 現在だと Polyfill の場合は次のような形でバイパスが可能。

github.com

    const policy = trustedTypes.createPolicy('default', {
      createScriptURL: (untsutedValue) => {
        return '';
      }
    });

    const rawHTML = decodeURIComponent(location.hash.substring(1));
    const a = document.createElement('a');
    a.href = policy.createScriptURL('http://example.com');
    a.pathname = '\nalert(1)';
    a.protocol = 'javascript:';
    document.body.appendChild(a);
    a.click();

その他

よく tt と略されるので issue とか追いかけるときは頭に入れておくといい。

References