CVE-2021-23419 open-graph 原型链污染分析
文章首发于安全客:CVE-2021-23419 open-graph 原型链污染分析
0x00 前言
CVE-2021-23419是Vulnerability DB在2021 年 8 月 5 日披露的出现在 node.js open-graph 包中的一个原型链污染漏洞,CVSS 分数为7.3,攻击难度低,下面是笔者的分析过程,以此来学习 open graph 协议与 JavaScript 原型链污染形成原理。
0x01 open graph 简介
开放图谱 (OG) 协议是 Facebook 于 2010 年创建的,旨在使网页链接成为具有与 Facebook 上发布的其他内容相似的功能和外观的丰富对象。 如果您已经在社交媒体上分享了一个链接,并且看到该平台甚至在您点击 Post 之前就自动向您显示了您想要分享的链接的大图片、标题、描述和 URL。
Open Graph 元标记在 HTML 页面的<head>
内容中,用一系列以“og”为前缀的属性进行标识。
例如:
<html prefix="og: https://ogp.me/ns#">
<head>
<title>The Rock (1996)</title>
<meta property="og:title" content="The Rock" />
<meta property="og:type" content="video.movie" />
<meta property="og:url" content="https://www.imdb.com/title/tt0117500/" />
<meta property="og:image" content="https://ia.media-imdb.com/images/rock.jpg" />
...
</head>
...
</html>
OG 元标记还可用于根据其共享平台自定义网页的外观。 例如,Twitter 基于OG 协议对其进行了自定义实现,下面的代码告诉 Twitter 显示大图像网页预览。
<meta name="twitter:card" content="summary_large_image" />
<meta
name="twitter:image"
content="https://example.com/image.png"
/>
在twitter中查看源码并搜索 og:
就可以看到 open graph 的标签。
open graph 中定义的元数据有很多种类型:
基本元数据:
og:title
- 应该出现在图表中的对象的标题。og:type
-对象的类型,例如”video.movie”。og:image
- 一个图像 URL。og:url
- 这一个 url 将用作该图片的永久 ID。
示例:
<html prefix="og: https://ogp.me/ns#">
<head>
<title>The Rock (1996)</title>
<meta property="og:title" content="The Rock" />
<meta property="og:type" content="video.movie" />
<meta property="og:url" content="https://www.imdb.com/title/tt0117500/" />
<meta property="og:image" content="https://ia.media-imdb.com/images/rock.jpg" />
...
</head>
...
</html>
可选元数据:
og:audio
:伴随该对象的音频文件的 URL。og:description
: 对此对象的描述。og:determiner
:语法中的限定词。og:locale
:语言 ,默认为en_US
.og:locale:alternate
:其他可以代替的语言,一个数组。og:site_name
:站点名词,如果您的对象是一个大型网站的一部分,则应为整个网站显示的名称。og:video
:补充此对象的视频文件的 URL。
元数据的属性:
某些元数据可以进一步补充其属性,以og:image
为例,可以补充如下的属性:
og:image:type
:此图像的 MIME 类型。og:image:width
:像素宽度。og:image:height
: 高像素数。og:image:alt
:对图像内容的描述(不是标题)。og:image:url
:与og:image
完全相同.og:image:secure_url
:网页需要 HTTPS 时使用的备用网址。
示例如下:
<meta property="og:audio" content="https://example.com/bond/theme.mp3" />
<meta property="og:description"
content="Sean Connery found fame and fortune as the
suave, sophisticated British agent, James Bond." />
<meta property="og:determiner" content="the" />
<meta property="og:locale" content="en_GB" />
<meta property="og:locale:alternate" content="fr_FR" />
<meta property="og:locale:alternate" content="es_ES" />
<meta property="og:site_name" content="IMDb" />
<meta property="og:video" content="https://example.com/bond/trailer.swf" /
0x02 open-graph 包简介
- github 地址: node-open-graph
- snyk.io 地址:open-graph vulnerabilities
open-graph 是 Node.js 的 Open Graph 实现。
用法如下:
const og = require('open-graph');
const url = 'http://github.com/samholmes/node-open-graph/raw/master/test.html';
og(url, function (err, meta) {
console.log(meta);
});
Outputs:
{
title: 'OG Testing',
type: 'website',
url: 'http://github.com/samholmes/node-open-graph/raw/master/test.html',
site_name: 'irrelavent',
description: 'This is a test bed for Open Graph protocol.',
image: {
url: 'http://google.com/images/logo.gif',
width: '100',
height: '100'
}
}
open-graph 向外提供 og 函数,主要功能可以分为两个部分:
- 根据 url 获得 mata 标签内容。
- 很具 open graph 协议解析 meta 标签的内容并存入一个 Object 对象中。
其中第二步涉及到了对象的递归赋值,因此造成原型链污染漏洞。
0x03 漏洞原理分析
open-graph 库在 8月5日被披露存在原型链污染漏洞,CVE号为 CVE-2021-23419,目前最新版本为 0.2.6,漏洞已修复,复现分析原理时可以安装 0.2.4 版本。
poc 如下:
分为server.js 与 client.js。
server.js,其实也就是一个回显 meta 标签的 web 服务。
const http = require('http');
http.createServer((req, res) => {
res.write('<meta property="og:__proto__:foo" content="polluted" />');
res.end();
}).listen(8080);
client.js,访问 meta 标签内容,即可看到
const og = require('open-graph');
og('http://127.0.0.1:8080', () => {
console.log(({}).foo); // polluted
});
下面结合这个poc进行进一步分析。
打开源码 node_modules\open-graph\index.js,源码比较短,主要有两个函数,getHTML 与 parse。
getHTML 没有什么可以说的,向某个页面发起请求,解析页面内容,cb 是一个回调函数,在 client.js 调用 og 函数时没有指定,这里就无需关注。
exports.getHTML = function(url, cb){
var purl = require('url').parse(url);
if (!purl.protocol)
purl = require('url').parse("http://"+url);
url = require('url').format(purl);
request({
url: url,
encoding: 'utf8',
gzip: true,
jar: true
},
function(err, res, body) {
if (err) return cb(err);
if (res.statusCode === 200) {
cb(null, body);
}
else {
cb(new Error("Request failed with HTTP status code: "+res.statusCode));
}
})
}
parse 函数用于将 open graph 内容解析为一个Object 对象。
我们可以直接下断到这一行。
可以看到 mata 是一个 object 对象,mataTags 用于存放所有的 mata 标签内容。然后对所有 mata 标签进行遍历。其中:
- namespace 内容为 ‘og’。
var property = propertyAttr.substring(namespace.length+1),
是将 poc 中的og:__proto__:foo
截取og:
后面的内容,因此 property 的内容为'__proto__:foo'
。 - keys 为 property 根据
:
切分而来,因此内容为一个数组:(2) ['__proto__', 'foo']
。 - meta 赋值给了 ptr,因此 ptr 是一个 object 对象。
后面这块代码为递归进行赋值,第一个循环的结尾处 ptr = ptr[key];
,ptr 即被赋值成了 object 类的原型。
由于只有一个键值对,只会循环一次,跳出循环后对 ptr 进行,由于前面获取到了原型链,此处key=foo,content=polluted
所以 ptr[key] = content;
造成了原型链污染。
0x04 漏洞修复
在 open-graph 0.2.6 中已经修复该漏洞,修复的方式是加入一个黑名单,并且在循环键名时进行判断。
var keyBlacklist = [
'__proto__',
'constructor',
'prototype'
]
...
while (keys.length > 1) {
key = keys.shift();
if (keyBlacklist.includes(key)) continue
if (Array.isArray(ptr[key])) {
// the last index of ptr[key] should become
// the object we are examining.
tmp = ptr[key].length-1;
ptr = ptr[key];
key = tmp;
}
if (typeof ptr[key] === 'string') {
// if it's a string, convert it
ptr[key] = { '': ptr[key] };
} else if (ptr[key] === undefined) {
// create a new key
ptr[key] = {};
}
// move our pointer to the next subnode
ptr = ptr[key];
}