[JS专项训练]blitzprop WP
前置知识
抽象语法树
抽象语法树,类比来说,就是把你写的代码变成一棵树,每个节点都是代码的一部分,这样可以方便地对代码进行分析和操作。可以想象一下,你写的代码就像一本书,每个单词、标点符号、空格都有自己的含义和作用。但是如果你想要修改或者优化这本书,你可能需要花很多时间去找到你想要改变的地方,而且还要注意不要影响其他地方的内容。这时候,如果你能把这本书变成一棵树,每个节点都代表一个单词、标点符号、空格等等,那么你就可以很容易地找到你想要修改的节点,而且还可以用一些工具来帮助你完成修改。这就是抽象语法树AST的作用。
以本题中的pug.compile('span Hello #{user}, thank you for letting us know!')
为例,它的语法树如下:
最外层
1 | { |
首先,这个语法树的类型是Block
,表示这是一个代码块,它可以包含多个节点。
它有一个属性nodes
,它是一个数组,存储了这个代码块中的所有节点。它还有一个属性line
,表示这个代码块在源代码中的行号,这里是0,表示这是整个源代码的开始。
1 | "nodes": [ |
nodes
数组中的第一个元素,它是一个类型为Tag
的节点,表示这是一个HTML标签。它有以下属性:
name
:表示标签的名称,这里是span
。selfClosing
:表示标签是否是自闭合的,即是否以/>
结尾,这里是false,表示不是。block
:表示标签内部的内容,它也是一个类型为Block
的节点,我们稍后再看。attrs
:表示标签的属性,比如class
、id
等等,这里是一个空数组,表示没有属性。attributeBlocks
:表示标签的属性块,比如:class="{active: isActive}"
等等,这里也是一个空数组,表示没有属性块(属性块是PUG语言的一个特征
)。isInline
:表示标签是否是内联的,即是否可以和其他标签在同一行显示。line
:表示标签在源代码中的行号。column
:表示标签在源代码中的列号。
中间层
1 | "block": { |
nodes
:表示代码块中的所有节点,它是一个数组,存储了三个元素。line
:表示代码块在源代码中的行号,这里是1。
最内层
接着,我们看看代码块中的三个元素。
第一个元素是一个类型为Text
的节点,表示这是一段文本。
1 | { |
它有以下属性:
val
:表示文本的值。line
:表示文本在源代码中的行号column
:表示文本在源代码中的列号
第二个元素是一个类型为Code
的节点,表示这是一段JavaScript代码。
1 | { |
它有以下属性:
val
:表示代码的内容是什么,这里是”user”。buffer
: 表示代码是否需要输出到缓冲区中,即是否需要显示在页面上。如果为true,则需要用=
开头;如果为false,则需要用-
开头。这里是true。mustEscape
: 表示代码是否需要转义特殊字符。如果为true,则需要用=
开头;如果为false,则需要用!=
开头。这里是true。isInline
: 表示代码是否是内联的。如果为true,则可以和其他文本或标签在同一行显示;如果为false,则需要单独占一行。这里是true。line
: 表示代码在源代码中的行号。column
: 表示代码在源代码中的列号。
第三个元素与第一个元素同为文本类型
AST注入
所谓渲染就是将一种代码通过词法分析和语法分析后,生成另一种类代码的过程
在这个过程中,免不了变量拼接
,函数执行
等操作,如果我们能够控制其中某些有用的变量,那么就可能产生一些背离了正常编译逻辑的操作,造成危害,这就是我理解的AST Injection
题目分析
这里需要输入的内容包含其中一个歌曲名才会触发pug的渲染
1 | 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')) { |
下断点,跟进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 | const pug = require('pug'); |
pug最终会通过Function内置函数来执行以下由complieBody生成的js代码:
1 | (function anonymous(pug |
最终Payload
靶机上没有bash,无创建文件权限,有nc,可以用 -e 来弹shell
1 | POST /api/submit |
参考文章: