misc
Astea(等待学习)
所以正如题目描述,这是一道jail类型,我还没有尝试过这种类型,源代码看起来有些复杂,即使我把它丢给了chatgpt进行分析,依然暂时没有理解。我尝试用dc社区其他队伍的wp,果然拿到了flag,但具体原理我暂时还不太懂,等待学习。。
safe_call:0=safe_import.__builtins__['license'];safe_call._Printer__filenames:0=['flag.txt'];_()
web
Fare Evasion
web页面初探
首先我们可以用浏览器插件查看它的cms等信息:
这是用python写的web程序。
然后我们需要尝试点击所有功能点,结合给出的交互提示等信息,来了解当前web程序主要是做什么的,并寻找出题者给的提示信息。
这是个交通管理系统,当我们点击乘客按钮时,给出的提示中出现了个类似密钥的值,并且只有列车长才能登录该系统。此时联想到是否能够尝试越过权限成为列车长。然后列车长按钮无法使用。
这个页面很简单,没发现其他额外的内容,接着查看源代码看是否有提示:
显然这里的提示很重要,可以先用chatgpt帮助分析下代码:
- async function pay() {:定义了一个异步函数pay,表示执行支付操作。
- 注释部分:这是一个注释块,其中描述了一个查询数据库的操作,但是由于在前端无法使用SQLite,此部分代码被注释掉。
- const r = await fetch("/pay", { method: "POST" });:使用fetch函数向服务器发送一个POST请求,请求的URL是/pay。该函数返回一个Promise对象,所以使用await关键字等待Promise对象的结果。
- const j = await r.json();:使用await关键字等待服务器返回的响应,并将响应内容解析为JSON格式。这里假设服务器返回的是有效的JSON数据。
- document.getElementById("alert").classList.add("opacity-100");:通过getElementById方法获取具有指定ID的DOM元素,然后使用classList.add方法添加opacity-100样式类,以显示一个警告或提示框。
- document.getElementById("alert").innerText = j["message"];:将警告或提示框的文本内容设置为从服务器响应中获取的message属性值。
- setTimeout(() => { document.getElementById("alert").classList.remove("opacity-100") }, 5000);:使用setTimeout函数设置一个定时器,5秒后执行指定的函数。该函数用于在5秒后移除警告或提示框的opacity-100样式类,以隐藏该元素。
前端发送请求到后端的/pay
接口,返回的响应内容格式是json,注意到弹窗的文本内容来自于响应包中的message
字段。注释中的sql查询语句很敏感,从SELECT * FROM keys WHERE kid = '${md5(headerKid)}'
中的调用函数md5(headerKid)
注意到参数headerKid
看起来像是和身份认证相关的,先记下来。
显然下一步就是用burp抓包,更深入地了解请求发出时都做了哪些事情:
JWT黑盒初步测试
这里显然是个JWT,黑盒测试中,首先直接将JWT置为空或删除,查看是否能够实现未授权访问,尝试后未成功:
接着尝试是否可以解密JWT还原信息,利用在线网站:
尝试爆破JWT的key
注意到这个JWT带有签名校验,不能将alg
设置为None
来绕过签名校验,也不能删除签名校验部分,既然有签名,尝试是否能爆破出key,因为加密算法存在爆破可能性:
没有成功。
其实结合前面源码中的提示,也能明白为什么不能爆破成功,因为token在从前端传递到后端数据库之前,会被md5
相关函数做转换处理,headerKid
显然就是这里JWT解密出的HEADER
中的字段,说明要对token做md5唯一性校验,因此爆破变得很困难。
联想到最初提示框中的boring_passenger_signing_key_?
有可能是这里SIGNATURE
中的key,将其分别用base64加密或解密后生成的新JWT进行测试,依然不行。
综合上述尝试,有理由怀疑kid
字段是有可能存在脆弱性的,首先我们需要知道在JWT中kid
字段代表的具体含义。
服务器可能会使用多个加密密钥来为不同类型的数据进行签名。出于这个原因,JWT的头部可能包含一个kid(密钥ID)参数,用来帮助服务器识别在验证签名时要使用的密钥。
首先在解析出的JWT中,kid
字段的值并不是路径格式,存在路径遍历的可能性不大先不尝试,结合获取到的信息,kid存在sql注入的可能性更大,因为请求发送到后端时,kid中的值先用md5做处理,然后代入到了数据库中进行比对!!所以可以优先尝试。
首先可以尝试搜索相关的利用:
sql注入绕过md5校验的方式-神奇的ffifdyop
但是显然由于md5()
的存在,如果尝试用标准的sql注入payload,肯定无法成功,那么现在问题就转换为sql注入如何绕过md5校验类型的检测,搜索关键词如下:
最后我们综合了几篇文章找到了可尝试的方式:
https://cvk.posthaven.com/sql-injection-with-raw-md5-hashes
https://book.hacktricks.xyz/pentesting-web/sql-injection
尤其是这篇文章中提到的,和我们获取到的信息和情况极为相似:
这里末尾的sql语句部分和源码中提示的结构相同,只是没有后面的True
,先了解下这个函数:
函数原型:md5(string,raw)
了解到当raw
为True
时是把目标字符串用MD5加密后表示为二进制,反之则表示成十六进制。我们并不能确定目标网站后台中的md5()函数的raw
默认值,但是根据文章所说,只要是调用这种函数配合raw
的设置,会产生隐患,因为raw
如果是二进制表示,当被数据库解析时,很大程度上当二进制转换为字符串后,很可能包含构造sql注入payload必要的危险字符,所以这里仅仅通过md5做校验方式是非常危险的。并且关于这里的ffifdyop
其实非常关键,在搜索后发现当将其用md5()
处理后并再经过数据库解析成字符串后,其结果为:'or'6�]��!r,��b
。
参考文章:
https://www.cnblogs.com/tqing/p/11852990.html
https://123123.men/index.php/203.html
如果将其拼接到源码提示中的sql语句中,发现达成了永远为真的逻辑效果,也就是万能密码的逻辑:
SELECT * FROM keys WHERE kid = '${md5(headerKid)}'
SELECT * FROM keys WHERE kid = '' or '6'
所以这样就实现绕过了,这将导致查询结果返回 keys 表中的所有数据,而不考虑实际的 kid 字段值,因此也就能泄露出包括conductor的key值。
重新构造JWT实现垂直越权
然后以该神奇的字符串ffifdyop
为基础,重新构造JWT如下:
重新发送请求包:
成功查询到conductor的key,接下来按照同样的步骤,将得到的key填入JWT校验中,就可以成功实现垂直越权:
然后我们就拿到了flag~
总结考察点
- jwt黑盒测试
- jwt结合sql注入漏洞的利用
- 利用md5函数的raw参数实现sql注入bypass
Log Action(等待解决)
web页面初探
先在虚拟机中用docker-compose up -d
启动题目环境,刚进入是一个简易登录页面:
先查看一下cms等信息:
点击here
,随意输入账号密码:
有密码长度限制,满足长度后再尝试:
查看源码,并没有任何发现。
利用burp抓包,看输入的账号密码是如何被传输的:
明文传输,并且请求体的格式使用了multipart/form-data
,这是一种常见的用于在 HTTP 请求中传输表单数据的格式。请求中还注意到某些KEY,暂时还不知道有什么用处。响应体中,注意到返回的是json格式,它表示一个数组,包含一个索引为 0 的元素,并且数组中还包含数组。尝试用base系列逐个解码这个看起来像编码后的字符串,没有结果。
fuzz
首先尝试目录爆破:
扫到了admin目录,尝试访问,然后burp抓包:
请求和访问/login
时非常相似,只是多了个包含csrf-token
的cookie,先记下来。由于密码至少要满足十位,为了不浪费太多时间,这里就不尝试爆破密码了。
白盒测试
到这里,我们并没有发现任何更多有价值的信息,但是别忘了这题的文件给了我们源代码,大致浏览下目录结构,前端后端分离,并发现对于这里的身份验证逻辑主要是通过前端验证,后端没有任何代码只有一个测试flag,在前端的src根目录中我们很快定位到了验证代码,文件是auth.ts
:
这里的密码是使用 randomBytes 函数从 crypto 模块生成长度为 16 字节的随机字节,然后转换为十六进制字符串表示形式。
看看是否能从docker启动脚本中发现什么:
这里出现了个环境变量AUTH_SECRET
,随机生成然后通过base64编码后赋值给它,我们可以尝试进入前端容器中获取到该值进行解码:
很遗憾,并没有什么有价值发现。
到这里,我没有任何思路了,试图在discord中寻找其他选手写的wp获得启发,顺着我已经探索的路,我只看了前面的部分,后面暂时不想让它剧透,因为我只是想适当参考,接着独立解决。
npm检测前端库和模块已知漏洞
注意到,他尝试利用npm自带的npm audit
检测该node前端中调用的库和模块是否存在已知的安全风险,由此看来,关于前端安全我还是欠缺经验。尝试:
竟然真的检测到了,其中基于 React 的前端框架Next
存在高风险的SSRF漏洞!我们的路瞬间再次明亮起来,存在更多利用可能性。查看提到的链接
寻找漏洞公开利用并分析
在末尾,我们还找到了该漏洞对应的CVE编号CVE-2024-34351
,尝试通过谷歌寻找是否有公开的漏洞利用poc:
定位到了下面的项目,在它提供的参考文章中(实际上就是发现这个漏洞的作者写的),可以了解这个漏洞利用的细节,原理:
也就是说我们往往认为这些静态的框架是非常安全的,轻量且风险又很少,但实际上是我们的想法太天真了,因为这些框架通常依赖于大量底层应用程序接口(API)和逻辑,因此存在相当大的攻击面。
在浏览到src/app/logout/page.tsx
时,发现存在重定向:
符合该漏洞的条件。另外,在文章中的示例提到了Next-Action ID
:
在用burp抓包时,发现请求头中确实也有这个ID:
再尝试抓xx/logout
触发请求后的数据包:
发现此时的Next-Action ID
变化了,也印证了文章的说法。并且还注意到cookie中包含的authjs.callback-url
值正好就是Origin
值。
另外,为了更好地了解它,我们还可以定位到nextjs开源项目打补丁的位置,这样我们就能定位到出现漏洞的函数:
同样还是原来的漏洞报告中有提到:
点击后跳转:
根据漏洞发现者的说明,我们需要审视一下这段存在漏洞函数提出请求的逻辑代码部分:
也许我们对这里的请求头HEAD
感到陌生,意义如下:
用于获取与指定资源相关的响应头信息,而不返回实际的响应主体内容。它与GET方法非常相似,但不同之处在于,HEAD方法只请求服务器返回响应头部信息,而不包含响应体。当使用HEAD方法发送请求时,服务器将会执行与GET方法类似的处理,但只返回响应的头信息,包括状态码、响应头字段和其他元数据。这对于获取资源的元数据或仅检查资源的可用性而不需要完整响应内容的情况非常有用。
发现者的文章对该部分代码进行了分析,刚开始感觉有些难理解,需要反复阅读然后去想象作为攻击者该如何利用这个场景。另外这里提到的preflight check
也是本次利用的关键因素,它的具体意义如下:
指在发送实际请求之前,浏览器发送一个 OPTIONS 请求来检查服务器是否支持跨域请求(CORS)。
- 当浏览器执行跨域请求时,例如从域A的网页向域B的服务器发送AJAX请求,浏览器会在发送实际请求之前先发送一个 OPTIONS 请求。这个 OPTIONS 请求包含了一组预检请求头(preflight headers),其中包括了实际请求中可能会发送的请求头信息,比如自定义的头部字段、授权信息等。
- 服务器收到 OPTIONS 请求后,会进行预检查(preflight check),检查请求头中的信息以验证是否允许跨域请求。服务器会检查请求头中的 Origin 字段,比对允许的来源,然后检查请求头中的其他字段和值是否满足服务器的安全策略。服务器可以根据自己的配置和需求,决定是否接受该跨域请求。
- 如果服务器验证通过,返回的响应中会包含一组预检响应头(preflight headers),用于告知浏览器实际请求是否被允许。这些预检响应头包括了允许的请求方法、允许的请求头字段、缓存设置等信息。
- 浏览器在收到预检响应后,会检查响应头中的信息。如果服务器允许跨域请求,且实际请求满足预检条件,浏览器会继续发送实际请求。如果预检不通过,浏览器会阻止实际请求的发送,并抛出跨域请求被拒绝的错误。
ssrf利用前的请求状态检测&利用ngrok实现内网穿透
首先我们可以在攻击机器上(我选择在我的云服务器,如果是本地的kali记得做内网穿透)用python搭建一个简易服务器作为ssrf服务器,来接收由目标发送的请求,主要目的是验证上面的请求逻辑过程,不过不知道为什么突然无法访问到,因此我又参考其他队伍的wp用ngrok来实现内网穿透,注意需要先注册用户,配置好Authtoken
:
然后就可以执行了:
这里的就是内网穿透后ngrok分配给我们攻击机器的公网域名(记下来)。然后用python启动简易web服务器后,我们就可以通过在控制台接收到的请求信息来判断每次请求的状态。接下来,用burp抓访问/logout
触发退出登录的包:
特别关注这几个字段,因为我们要实现ssrf,在这之前,要测试目标发送的请求方法逻辑是否符合上面分析的,也就是先要判断是否能通过preflight check
(预检测),因为通过上面的源代码分析,只有通过该检测(第一次目标服务器用HEAD
方法发给我们的ssrf服务器的请求,然后从响应头来判断是否能够符合预检测要求headResponse.headers.get('content-type') === RSC_CONTENT_TYPE_HEADER
,即content-type=text/x-component
),才能让目标服务器向ssrf服务器再发送下一步的GET
请求,从而返回响应体的内容,从ssrf的利用角度来看,当请求的来源等关键字段是我们可以控制的,我们就可以伪造它们,改成由目标服务器本身请求自己,从而就可以利用这里的GET
请求获取到包含服务器内部敏感信息等的响应体,这就是我们需要实现的。
首先我们伪造HOST
和Origin
字段,看看请求状态是什么:
捕获到的请求状态:
说明我们没有通过预检测。而我们如果要实现上面的要求,必须特殊构造一个新的ssrf服务器,发现者的文章已经给了我们答案,可以用flask
搭建web程序来实现,根据我们的情况进行适当修改:
#!/usr/bin/env python3
from flask import Flask, Response, request, redirect
app = Flask(__name__)
@app.route('/login')
def catch():
# Preflight check
if request.method == 'HEAD':
resp = Response()
resp.headers['Content-Type'] = 'text/x-component'
return resp
# After checking
elif request.method == 'GET':
return 'Pass the Preflight check successfully!'
# Start listening
if __name__ == '__main__':
app.run(port=80, debug=True)
这段代码实现了一个简单的Flask应用程序,用于处理/login
路径(因为)的HEAD
和GET
请求。当收到HEAD请求时,返回一个带有特定内容类型的空响应,用于处理预检请求。当收到GET请求时,返回一个简单的成功消息,表示通过了预检查。其中,在代码的最后,使用app.run(port=80, debug=True)
启动应用程序,并监听端口80。debug=True
参数用于启用调试模式,以便在开发过程中捕获和显示错误信息。
这里有个细节,为什么在开头装饰器创建的路由要对应于URL路径为/login
的GET请求?
因为在burp抓包过程中发现,当访问
/login
时,才满足响应头content-type=text/x-component
的预检测条件,并且当我们访问/logout
后,会重定向到/login
,正如发现者所说,这里的重定向不是前端路由直接触发跳转,而是服务器先在内部获取到/login
的页面再返回给浏览器,所以这个过程是利用ssrf的关键。
但是不知道为什么,我重新抓包修改ORIGIN和HOST后,最终捕获到的只有通过预检测的请求,而没有触发GET请求返回我设定的响应体内容Pass the Preflight check successfully!
(等待解决中。。。)
如下图所示:
Comments NOTHING